Skip to content

Commit

Permalink
feat(agent): add auth token config. (TabbyML#649)
Browse files Browse the repository at this point in the history
* feat(agent): add auth token config.

* fix: fix agent loading auth token.

* fix: update retain old config filepath.

* fix: update retain old config filepath.

* fix: lint.

* fix: remove auto migrate, update config template.
  • Loading branch information
icycodes authored Oct 30, 2023
1 parent c51e00e commit e880973
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 85 deletions.
2 changes: 1 addition & 1 deletion clients/intellij/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
"devDependencies": {
"cpy-cli": "^4.2.0",
"rimraf": "^5.0.1",
"tabby-agent": "1.0.0"
"tabby-agent": "1.1.0-dev"
}
}
2 changes: 1 addition & 1 deletion clients/tabby-agent/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tabby-agent",
"version": "1.0.0",
"version": "1.1.0-dev",
"description": "Generic client agent for Tabby AI coding assistant IDE extensions.",
"repository": "https://github.com/TabbyML/tabby",
"main": "./dist/index.js",
Expand Down
79 changes: 24 additions & 55 deletions clients/tabby-agent/src/AgentConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { isBrowser } from "./env";
export type AgentConfig = {
server: {
endpoint: string;
token: string;
requestHeaders: Record<string, string | number | boolean | null | undefined>;
requestTimeout: number;
};
Expand Down Expand Up @@ -50,6 +51,7 @@ export type PartialAgentConfig = RecursivePartial<AgentConfig>;
export const defaultAgentConfig: AgentConfig = {
server: {
endpoint: "http://localhost:8080",
token: "",
requestHeaders: {},
requestTimeout: 30000, // 30s
},
Expand Down Expand Up @@ -82,67 +84,27 @@ export const defaultAgentConfig: AgentConfig = {
},
};

const oldConfigTomlTemplate = `## Tabby agent configuration file
## You can uncomment any block to enable settings.
## Configurations in this file has lower priority than in IDE settings.
## Server
## You can set the server endpoint and request timeout here.
# [server]
# endpoint = "http://localhost:8080" # http or https URL
# requestTimeout = 30000 # ms
## You can add custom request headers, e.g. for authentication.
# [server.requestHeaders]
# Authorization = "Bearer eyJhbGciOiJ..........."
## Completion
## You can set the prompt context to send to the server for completion.
# [completion.prompt]
# maxPrefixLines = 20
# maxSuffixLines = 20
## You can set the debounce mode for auto completion requests when typing.
# [completion.debounce]
# mode = "adaptive" # or "fixed"
# interval = 250 # ms, only used when mode is "fixed"
## You can set the timeout for completion requests.
# [completion.timeout]
# auto = 5000 # ms, for auto completion when typing
# manually = 30000 # ms, for manually triggered completion
## Logs
## You can set the log level here. The log file is located at ~/.tabby-client/agent/logs/.
# [logs]
# level = "silent" # or "error" or "debug"
## Anonymous usage tracking
## You can disable anonymous usage tracking here.
# [anonymousUsageTracking]
# disable = false # set to true to disable
`;

const configTomlTemplate = `## Tabby agent configuration file
## You can uncomment any block to enable settings.
## Configurations in this file has lower priority than in IDE settings.
## Server
## You can set the server endpoint here.
## You can set the server endpoint and authentication token here.
# [server]
# endpoint = "http://localhost:8080" # http or https URL
# token = "your-token-here" # if server requires authentication
## You can add custom request headers, e.g. for authentication.
## You can add custom request headers.
# [server.requestHeaders]
# Authorization = "Bearer eyJhbGciOiJ..........."
# Header1 = "Value1" # list your custom headers here
# Header2 = "Value2" # value can be string, number or boolean
# Authorization = "Bearer your-token-here" # if Authorization header is set, server.token will be ignored
## Logs
## You can set the log level here. The log file is located at ~/.tabby-client/agent/logs/.
# [logs]
# level = "silent" # or "error" or "debug"
# level = "silent" # "silent" or "error" or "debug"
## Anonymous usage tracking
## You can disable anonymous usage tracking here.
Expand All @@ -158,6 +120,7 @@ export const userAgentConfig = isBrowser
const fs = require("fs-extra");
const toml = require("toml");
const chokidar = require("chokidar");
const deepEqual = require("deep-equal");

class ConfigFile extends EventEmitter {
filepath: string;
Expand All @@ -177,14 +140,13 @@ export const userAgentConfig = isBrowser
async load() {
try {
const fileContent = await fs.readFile(this.filepath, "utf8");
// If the config file is the old template, and user has not modified it,
// Overwrite it with the new template.
if (fileContent.trim() === oldConfigTomlTemplate.trim()) {
const data = toml.parse(fileContent);
// If the config file contains no value, overwrite it with the new template.
if (Object.keys(data).length === 0 && fileContent.trim() !== configTomlTemplate.trim()) {
await this.createTemplate();
return await this.load();
return;
}
this.data = toml.parse(fileContent);
super.emit("updated", this.data);
this.data = data;
} catch (error) {
if (error.code === "ENOENT") {
await this.createTemplate();
Expand All @@ -206,8 +168,15 @@ export const userAgentConfig = isBrowser
this.watcher = chokidar.watch(this.filepath, {
interval: 1000,
});
this.watcher.on("add", this.load.bind(this));
this.watcher.on("change", this.load.bind(this));
const onChanged = async () => {
const oldData = this.data;
await this.load();
if (!deepEqual(oldData, this.data)) {
super.emit("updated", this.data);
}
};
this.watcher.on("add", onChanged);
this.watcher.on("change", onChanged);
}
}

Expand Down
11 changes: 10 additions & 1 deletion clients/tabby-agent/src/Auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,16 @@ export class Auth extends EventEmitter {
constructor(options: { endpoint: string; dataStore?: DataStore }) {
super();
this.endpoint = options.endpoint;
this.dataStore = options.dataStore || dataStore;
if (options.dataStore) {
this.dataStore = options.dataStore;
} else {
this.dataStore = dataStore;
dataStore.on("updated", async () => {
await this.load();
super.emit("updated", this.jwt);
});
dataStore.watch();
}
this.authApi = createClient<CloudApi>({ baseUrl: "https://app.tabbyml.com/api" });
this.scheduleRefreshToken();
}
Expand Down
18 changes: 13 additions & 5 deletions clients/tabby-agent/src/TabbyAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,13 @@ export class TabbyAgent extends EventEmitter implements Agent {
this.config = deepmerge(defaultAgentConfig, this.userConfig, this.clientConfig);
allLoggers.forEach((logger) => (logger.level = this.config.logs.level));
this.anonymousUsageLogger.disabled = this.config.anonymousUsageTracking.disable;
if (this.config.server.requestHeaders["Authorization"] === undefined) {
if (isBlank(this.config.server.token) && this.config.server.requestHeaders["Authorization"] === undefined) {
if (this.config.server.endpoint !== this.auth?.endpoint) {
this.auth = await Auth.create({ endpoint: this.config.server.endpoint, dataStore: this.dataStore });
this.auth.on("updated", this.setupApi.bind(this));
}
} else {
// If `Authorization` request header is provided, use it directly.
// If auth token is provided, use it directly.
this.auth = null;
}
await this.setupApi();
Expand All @@ -126,10 +126,15 @@ export class TabbyAgent extends EventEmitter implements Agent {
}

private async setupApi() {
const auth = !isBlank(this.config.server.token)
? `Bearer ${this.config.server.token}`
: this.auth?.token
? `Bearer ${this.auth.token}`
: undefined;
this.api = createClient<TabbyApi>({
baseUrl: this.config.server.endpoint.replace(/\/+$/, ""), // remove trailing slash
headers: {
Authorization: this.auth?.token ? `Bearer ${this.auth.token}` : undefined,
Authorization: auth,
...this.config.server.requestHeaders,
},
});
Expand Down Expand Up @@ -225,6 +230,7 @@ export class TabbyAgent extends EventEmitter implements Agent {
error instanceof HttpError &&
[401, 403, 405].indexOf(error.status) !== -1 &&
new URL(this.config.server.endpoint).hostname.endsWith("app.tabbyml.com") &&
isBlank(this.config.server.token) &&
this.config.server.requestHeaders["Authorization"] === undefined
) {
this.logger.debug({ requestId, path, error }, "API unauthorized");
Expand Down Expand Up @@ -254,8 +260,10 @@ export class TabbyAgent extends EventEmitter implements Agent {
}
}
} catch (_) {
this.changeStatus("disconnected");
this.serverHealthState = null;
if (this.status === "ready" || this.status === "notInitialized") {
this.changeStatus("disconnected");
this.serverHealthState = null;
}
}
}

Expand Down
58 changes: 39 additions & 19 deletions clients/tabby-agent/src/dataStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,48 @@ export interface DataStore {
save(): PromiseLike<void>;
}

export const dataStore: DataStore = isBrowser
export const dataStore = isBrowser
? null
: (() => {
const dataFile = require("path").join(require("os").homedir(), ".tabby-client", "agent", "data.json");
const EventEmitter = require("events");
const fs = require("fs-extra");
return {
data: {},
load: async function () {
await this.migrateFrom_0_3_0();
const deepEqual = require("deep-equal");
const chokidar = require("chokidar");

class FileDataStore extends EventEmitter implements FileDataStore {
filepath: string;
data: Partial<StoredData> = {};
watcher: ReturnType<typeof chokidar.watch> | null = null;

constructor(filepath: string) {
super();
this.filepath = filepath;
}

async load() {
this.data = (await fs.readJson(dataFile, { throws: false })) || {};
},
save: async function () {
}

async save() {
await fs.outputJson(dataFile, this.data);
},
migrateFrom_0_3_0: async function () {
const dataFile_0_3_0 = require("path").join(require("os").homedir(), ".tabby", "agent", "data.json");
const migratedFlag = require("path").join(require("os").homedir(), ".tabby", "agent", ".data_json_migrated");
if ((await fs.pathExists(dataFile_0_3_0)) && !(await fs.pathExists(migratedFlag))) {
const data = await fs.readJson(dataFile_0_3_0);
await fs.outputJson(dataFile, data);
await fs.outputFile(migratedFlag, "");
}
},
};
}

watch() {
this.watcher = chokidar.watch(this.filepath, {
interval: 1000,
});
const onChanged = async () => {
const oldData = this.data;
await this.load();
if (!deepEqual(oldData, this.data)) {
super.emit("updated", this.data);
}
};
this.watcher.on("add", onChanged);
this.watcher.on("change", onChanged);
}
}

const dataFile = require("path").join(require("os").homedir(), ".tabby-client", "agent", "data.json");
return new FileDataStore(dataFile);
})();
2 changes: 1 addition & 1 deletion clients/vim/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
"devDependencies": {
"cpy-cli": "^4.2.0",
"rimraf": "^5.0.1",
"tabby-agent": "1.0.0"
"tabby-agent": "1.1.0-dev"
}
}
4 changes: 2 additions & 2 deletions clients/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"repository": "https://github.com/TabbyML/tabby",
"bugs": "https://github.com/TabbyML/tabby/issues",
"license": "Apache-2.0",
"version": "1.0.0",
"version": "1.1.0-dev",
"keywords": [
"ai",
"autocomplete",
Expand Down Expand Up @@ -217,6 +217,6 @@
},
"dependencies": {
"@xstate/fsm": "^2.0.1",
"tabby-agent": "1.0.0"
"tabby-agent": "1.1.0-dev"
}
}

0 comments on commit e880973

Please sign in to comment.