feat(agent): add auth token config. (#649)
* 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.release-notes-05
parent
c51e00ee45
commit
e88097320b
|
|
@ -10,6 +10,6 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cpy-cli": "^4.2.0",
|
"cpy-cli": "^4.2.0",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"tabby-agent": "1.0.0"
|
"tabby-agent": "1.1.0-dev"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "tabby-agent",
|
"name": "tabby-agent",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0-dev",
|
||||||
"description": "Generic client agent for Tabby AI coding assistant IDE extensions.",
|
"description": "Generic client agent for Tabby AI coding assistant IDE extensions.",
|
||||||
"repository": "https://github.com/TabbyML/tabby",
|
"repository": "https://github.com/TabbyML/tabby",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { isBrowser } from "./env";
|
||||||
export type AgentConfig = {
|
export type AgentConfig = {
|
||||||
server: {
|
server: {
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
|
token: string;
|
||||||
requestHeaders: Record<string, string | number | boolean | null | undefined>;
|
requestHeaders: Record<string, string | number | boolean | null | undefined>;
|
||||||
requestTimeout: number;
|
requestTimeout: number;
|
||||||
};
|
};
|
||||||
|
|
@ -50,6 +51,7 @@ export type PartialAgentConfig = RecursivePartial<AgentConfig>;
|
||||||
export const defaultAgentConfig: AgentConfig = {
|
export const defaultAgentConfig: AgentConfig = {
|
||||||
server: {
|
server: {
|
||||||
endpoint: "http://localhost:8080",
|
endpoint: "http://localhost:8080",
|
||||||
|
token: "",
|
||||||
requestHeaders: {},
|
requestHeaders: {},
|
||||||
requestTimeout: 30000, // 30s
|
requestTimeout: 30000, // 30s
|
||||||
},
|
},
|
||||||
|
|
@ -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
|
const configTomlTemplate = `## Tabby agent configuration file
|
||||||
|
|
||||||
## You can uncomment any block to enable settings.
|
## You can uncomment any block to enable settings.
|
||||||
## Configurations in this file has lower priority than in IDE settings.
|
## Configurations in this file has lower priority than in IDE settings.
|
||||||
|
|
||||||
## Server
|
## Server
|
||||||
## You can set the server endpoint here.
|
## You can set the server endpoint and authentication token here.
|
||||||
# [server]
|
# [server]
|
||||||
# endpoint = "http://localhost:8080" # http or https URL
|
# 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]
|
# [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
|
## Logs
|
||||||
## You can set the log level here. The log file is located at ~/.tabby-client/agent/logs/.
|
## You can set the log level here. The log file is located at ~/.tabby-client/agent/logs/.
|
||||||
# [logs]
|
# [logs]
|
||||||
# level = "silent" # or "error" or "debug"
|
# level = "silent" # "silent" or "error" or "debug"
|
||||||
|
|
||||||
## Anonymous usage tracking
|
## Anonymous usage tracking
|
||||||
## You can disable anonymous usage tracking here.
|
## You can disable anonymous usage tracking here.
|
||||||
|
|
@ -158,6 +120,7 @@ export const userAgentConfig = isBrowser
|
||||||
const fs = require("fs-extra");
|
const fs = require("fs-extra");
|
||||||
const toml = require("toml");
|
const toml = require("toml");
|
||||||
const chokidar = require("chokidar");
|
const chokidar = require("chokidar");
|
||||||
|
const deepEqual = require("deep-equal");
|
||||||
|
|
||||||
class ConfigFile extends EventEmitter {
|
class ConfigFile extends EventEmitter {
|
||||||
filepath: string;
|
filepath: string;
|
||||||
|
|
@ -177,14 +140,13 @@ export const userAgentConfig = isBrowser
|
||||||
async load() {
|
async load() {
|
||||||
try {
|
try {
|
||||||
const fileContent = await fs.readFile(this.filepath, "utf8");
|
const fileContent = await fs.readFile(this.filepath, "utf8");
|
||||||
// If the config file is the old template, and user has not modified it,
|
const data = toml.parse(fileContent);
|
||||||
// Overwrite it with the new template.
|
// If the config file contains no value, overwrite it with the new template.
|
||||||
if (fileContent.trim() === oldConfigTomlTemplate.trim()) {
|
if (Object.keys(data).length === 0 && fileContent.trim() !== configTomlTemplate.trim()) {
|
||||||
await this.createTemplate();
|
await this.createTemplate();
|
||||||
return await this.load();
|
return;
|
||||||
}
|
}
|
||||||
this.data = toml.parse(fileContent);
|
this.data = data;
|
||||||
super.emit("updated", this.data);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === "ENOENT") {
|
if (error.code === "ENOENT") {
|
||||||
await this.createTemplate();
|
await this.createTemplate();
|
||||||
|
|
@ -206,8 +168,15 @@ export const userAgentConfig = isBrowser
|
||||||
this.watcher = chokidar.watch(this.filepath, {
|
this.watcher = chokidar.watch(this.filepath, {
|
||||||
interval: 1000,
|
interval: 1000,
|
||||||
});
|
});
|
||||||
this.watcher.on("add", this.load.bind(this));
|
const onChanged = async () => {
|
||||||
this.watcher.on("change", this.load.bind(this));
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,16 @@ export class Auth extends EventEmitter {
|
||||||
constructor(options: { endpoint: string; dataStore?: DataStore }) {
|
constructor(options: { endpoint: string; dataStore?: DataStore }) {
|
||||||
super();
|
super();
|
||||||
this.endpoint = options.endpoint;
|
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.authApi = createClient<CloudApi>({ baseUrl: "https://app.tabbyml.com/api" });
|
||||||
this.scheduleRefreshToken();
|
this.scheduleRefreshToken();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,13 +95,13 @@ export class TabbyAgent extends EventEmitter implements Agent {
|
||||||
this.config = deepmerge(defaultAgentConfig, this.userConfig, this.clientConfig);
|
this.config = deepmerge(defaultAgentConfig, this.userConfig, this.clientConfig);
|
||||||
allLoggers.forEach((logger) => (logger.level = this.config.logs.level));
|
allLoggers.forEach((logger) => (logger.level = this.config.logs.level));
|
||||||
this.anonymousUsageLogger.disabled = this.config.anonymousUsageTracking.disable;
|
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) {
|
if (this.config.server.endpoint !== this.auth?.endpoint) {
|
||||||
this.auth = await Auth.create({ endpoint: this.config.server.endpoint, dataStore: this.dataStore });
|
this.auth = await Auth.create({ endpoint: this.config.server.endpoint, dataStore: this.dataStore });
|
||||||
this.auth.on("updated", this.setupApi.bind(this));
|
this.auth.on("updated", this.setupApi.bind(this));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If `Authorization` request header is provided, use it directly.
|
// If auth token is provided, use it directly.
|
||||||
this.auth = null;
|
this.auth = null;
|
||||||
}
|
}
|
||||||
await this.setupApi();
|
await this.setupApi();
|
||||||
|
|
@ -126,10 +126,15 @@ export class TabbyAgent extends EventEmitter implements Agent {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setupApi() {
|
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>({
|
this.api = createClient<TabbyApi>({
|
||||||
baseUrl: this.config.server.endpoint.replace(/\/+$/, ""), // remove trailing slash
|
baseUrl: this.config.server.endpoint.replace(/\/+$/, ""), // remove trailing slash
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: this.auth?.token ? `Bearer ${this.auth.token}` : undefined,
|
Authorization: auth,
|
||||||
...this.config.server.requestHeaders,
|
...this.config.server.requestHeaders,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -225,6 +230,7 @@ export class TabbyAgent extends EventEmitter implements Agent {
|
||||||
error instanceof HttpError &&
|
error instanceof HttpError &&
|
||||||
[401, 403, 405].indexOf(error.status) !== -1 &&
|
[401, 403, 405].indexOf(error.status) !== -1 &&
|
||||||
new URL(this.config.server.endpoint).hostname.endsWith("app.tabbyml.com") &&
|
new URL(this.config.server.endpoint).hostname.endsWith("app.tabbyml.com") &&
|
||||||
|
isBlank(this.config.server.token) &&
|
||||||
this.config.server.requestHeaders["Authorization"] === undefined
|
this.config.server.requestHeaders["Authorization"] === undefined
|
||||||
) {
|
) {
|
||||||
this.logger.debug({ requestId, path, error }, "API unauthorized");
|
this.logger.debug({ requestId, path, error }, "API unauthorized");
|
||||||
|
|
@ -254,8 +260,10 @@ export class TabbyAgent extends EventEmitter implements Agent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
this.changeStatus("disconnected");
|
if (this.status === "ready" || this.status === "notInitialized") {
|
||||||
this.serverHealthState = null;
|
this.changeStatus("disconnected");
|
||||||
|
this.serverHealthState = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,28 +11,48 @@ export interface DataStore {
|
||||||
save(): PromiseLike<void>;
|
save(): PromiseLike<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dataStore: DataStore = isBrowser
|
export const dataStore = isBrowser
|
||||||
? null
|
? null
|
||||||
: (() => {
|
: (() => {
|
||||||
const dataFile = require("path").join(require("os").homedir(), ".tabby-client", "agent", "data.json");
|
const EventEmitter = require("events");
|
||||||
const fs = require("fs-extra");
|
const fs = require("fs-extra");
|
||||||
return {
|
const deepEqual = require("deep-equal");
|
||||||
data: {},
|
const chokidar = require("chokidar");
|
||||||
load: async function () {
|
|
||||||
await this.migrateFrom_0_3_0();
|
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 })) || {};
|
this.data = (await fs.readJson(dataFile, { throws: false })) || {};
|
||||||
},
|
}
|
||||||
save: async function () {
|
|
||||||
|
async save() {
|
||||||
await fs.outputJson(dataFile, this.data);
|
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");
|
watch() {
|
||||||
const migratedFlag = require("path").join(require("os").homedir(), ".tabby", "agent", ".data_json_migrated");
|
this.watcher = chokidar.watch(this.filepath, {
|
||||||
if ((await fs.pathExists(dataFile_0_3_0)) && !(await fs.pathExists(migratedFlag))) {
|
interval: 1000,
|
||||||
const data = await fs.readJson(dataFile_0_3_0);
|
});
|
||||||
await fs.outputJson(dataFile, data);
|
const onChanged = async () => {
|
||||||
await fs.outputFile(migratedFlag, "");
|
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);
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,6 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cpy-cli": "^4.2.0",
|
"cpy-cli": "^4.2.0",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"tabby-agent": "1.0.0"
|
"tabby-agent": "1.1.0-dev"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
"repository": "https://github.com/TabbyML/tabby",
|
"repository": "https://github.com/TabbyML/tabby",
|
||||||
"bugs": "https://github.com/TabbyML/tabby/issues",
|
"bugs": "https://github.com/TabbyML/tabby/issues",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0-dev",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ai",
|
"ai",
|
||||||
"autocomplete",
|
"autocomplete",
|
||||||
|
|
@ -217,6 +217,6 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xstate/fsm": "^2.0.1",
|
"@xstate/fsm": "^2.0.1",
|
||||||
"tabby-agent": "1.0.0"
|
"tabby-agent": "1.1.0-dev"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue