agents-bridge 0.3.5 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/config.multi-agent.example.json +4 -1
- package/dist/agents/agent-resolver.d.ts.map +1 -1
- package/dist/agents/agent-resolver.js +8 -2
- package/dist/agents/agent-resolver.js.map +1 -1
- package/dist/agents/cli-runner.d.ts +3 -3
- package/dist/agents/cli-runner.d.ts.map +1 -1
- package/dist/agents/cli-runner.js +87 -76
- package/dist/agents/cli-runner.js.map +1 -1
- package/dist/agents/cli-session-store.d.ts +3 -0
- package/dist/agents/cli-session-store.d.ts.map +1 -1
- package/dist/agents/cli-session-store.js +31 -10
- package/dist/agents/cli-session-store.js.map +1 -1
- package/dist/agents/codex-app-server-runtime.d.ts +37 -1
- package/dist/agents/codex-app-server-runtime.d.ts.map +1 -1
- package/dist/agents/codex-app-server-runtime.js +37 -14
- package/dist/agents/codex-app-server-runtime.js.map +1 -1
- package/dist/agents/managed-backend.d.ts +4 -0
- package/dist/agents/managed-backend.d.ts.map +1 -0
- package/dist/agents/managed-backend.js +18 -0
- package/dist/agents/managed-backend.js.map +1 -0
- package/dist/agents/managed-role.d.ts +17 -0
- package/dist/agents/managed-role.d.ts.map +1 -0
- package/dist/agents/managed-role.js +13 -0
- package/dist/agents/managed-role.js.map +1 -0
- package/dist/agents/model-auto-binding.d.ts.map +1 -1
- package/dist/agents/model-auto-binding.js +15 -23
- package/dist/agents/model-auto-binding.js.map +1 -1
- package/dist/agents/model-control-files.d.ts +5 -0
- package/dist/agents/model-control-files.d.ts.map +1 -0
- package/dist/agents/model-control-files.js +101 -0
- package/dist/agents/model-control-files.js.map +1 -0
- package/dist/agents/model-controller.d.ts +39 -2
- package/dist/agents/model-controller.d.ts.map +1 -1
- package/dist/agents/model-controller.js +211 -66
- package/dist/agents/model-controller.js.map +1 -1
- package/dist/agents/trusted-launcher.js +5 -1
- package/dist/agents/trusted-launcher.js.map +1 -1
- package/dist/channels/bot-run-protocol.d.ts +13 -0
- package/dist/channels/bot-run-protocol.d.ts.map +1 -0
- package/dist/channels/bot-run-protocol.js +20 -0
- package/dist/channels/bot-run-protocol.js.map +1 -0
- package/dist/channels/bot-run-receipts.d.ts +14 -0
- package/dist/channels/bot-run-receipts.d.ts.map +1 -0
- package/dist/channels/bot-run-receipts.js +88 -0
- package/dist/channels/bot-run-receipts.js.map +1 -0
- package/dist/channels/botlink.d.ts +3 -0
- package/dist/channels/botlink.d.ts.map +1 -1
- package/dist/channels/botlink.js +95 -17
- package/dist/channels/botlink.js.map +1 -1
- package/dist/channels/managed-receipts.d.ts +14 -0
- package/dist/channels/managed-receipts.d.ts.map +1 -0
- package/dist/channels/managed-receipts.js +136 -0
- package/dist/channels/managed-receipts.js.map +1 -0
- package/dist/cli/cmd-start.d.ts.map +1 -1
- package/dist/cli/cmd-start.js +49 -7
- package/dist/cli/cmd-start.js.map +1 -1
- package/dist/cloud/main.d.ts +2 -0
- package/dist/cloud/main.d.ts.map +1 -0
- package/dist/cloud/main.js +14 -0
- package/dist/cloud/main.js.map +1 -0
- package/dist/cloud/worker.d.ts +98 -0
- package/dist/cloud/worker.d.ts.map +1 -0
- package/dist/cloud/worker.js +212 -0
- package/dist/cloud/worker.js.map +1 -0
- package/dist/config/agent-dirs.d.ts.map +1 -1
- package/dist/config/agent-dirs.js +2 -0
- package/dist/config/agent-dirs.js.map +1 -1
- package/dist/config/attachment-handoff.d.ts +5 -0
- package/dist/config/attachment-handoff.d.ts.map +1 -0
- package/dist/config/attachment-handoff.js +60 -0
- package/dist/config/attachment-handoff.js.map +1 -0
- package/dist/config/zod-schema.d.ts +447 -282
- package/dist/config/zod-schema.d.ts.map +1 -1
- package/dist/config/zod-schema.js +15 -1
- package/dist/config/zod-schema.js.map +1 -1
- package/dist/types/index.d.ts +12 -4
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import { mkdir, open, readFile } from 'node:fs/promises';
|
|
3
|
-
import { dirname } from 'node:path';
|
|
4
2
|
import { normalizeBotlinkChannelConfigs } from '../config/zod-schema.js';
|
|
5
3
|
import { resolveAgentConfigs } from './agent-resolver.js';
|
|
6
4
|
import { resolveAgent } from './agent-router.js';
|
|
7
5
|
import { resolveOpenApiBaseUrl } from '../cli/botlink-openapi.js';
|
|
6
|
+
import { createSecureModelControlFile, readSecureModelControlFile } from './model-control-files.js';
|
|
8
7
|
export function modelSwitchAllowed(config, agentId) {
|
|
9
8
|
return config.agents.list?.find(a => a.id === agentId)?.allowModelSwitch ?? config.agents.defaults.allowModelSwitch ?? true;
|
|
10
9
|
}
|
|
11
10
|
/** Reuses legacy channel routing and credentials; never rewrites Agent IDs or sessions. */
|
|
12
11
|
export async function automaticModelControl(config, agentId, statePath, logger) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
const agentSetting = config.agents.list?.find(a => a.id === agentId)?.modelControl;
|
|
13
|
+
const configured = agentSetting !== undefined ? agentSetting : config.agents.defaults.modelControl;
|
|
14
|
+
if (configured === false) {
|
|
15
|
+
logger.info({ agentId, event: 'MODEL_CONTROL_DISABLED' }, 'Model control disabled by configuration');
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
if (configured)
|
|
19
|
+
return { ...configured, allowModelSwitch: modelSwitchAllowed(config, agentId) };
|
|
16
20
|
const resolved = resolveAgentConfigs(config, logger);
|
|
17
21
|
const ref = resolved.agents.get(agentId).model.primary;
|
|
18
22
|
const slash = ref.indexOf('/'), provider = ref.slice(0, slash), initial = ref.slice(slash + 1);
|
|
@@ -31,37 +35,25 @@ export async function automaticModelControl(config, agentId, statePath, logger)
|
|
|
31
35
|
}
|
|
32
36
|
const account = accounts[0].config;
|
|
33
37
|
const endpoint = `${resolveOpenApiBaseUrl(account)}/openapi/agent/model-control/poll`;
|
|
38
|
+
const endpointUrl = new URL(endpoint);
|
|
39
|
+
if (endpointUrl.protocol !== 'https:' || endpointUrl.username || endpointUrl.password || endpointUrl.hash)
|
|
40
|
+
throw new Error('MODEL_CONTROL_HTTPS_REQUIRED');
|
|
34
41
|
const path = statePath + '.device';
|
|
35
|
-
await mkdir(dirname(path), { recursive: true });
|
|
36
42
|
let secret;
|
|
37
43
|
try {
|
|
38
|
-
secret = (await
|
|
44
|
+
secret = (await readSecureModelControlFile(path)).trim();
|
|
39
45
|
}
|
|
40
46
|
catch (e) {
|
|
41
47
|
if (e.code !== 'ENOENT')
|
|
42
48
|
throw e;
|
|
43
49
|
secret = randomBytes(32).toString('hex');
|
|
44
50
|
try {
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
await f.writeFile(secret);
|
|
48
|
-
await f.sync();
|
|
49
|
-
}
|
|
50
|
-
finally {
|
|
51
|
-
await f.close();
|
|
52
|
-
}
|
|
51
|
+
await createSecureModelControlFile(path, secret);
|
|
53
52
|
}
|
|
54
53
|
catch (e) {
|
|
55
54
|
if (e.code !== 'EEXIST')
|
|
56
55
|
throw e;
|
|
57
|
-
secret = (await
|
|
58
|
-
}
|
|
59
|
-
const dir = await open(dirname(path), 'r');
|
|
60
|
-
try {
|
|
61
|
-
await dir.sync();
|
|
62
|
-
}
|
|
63
|
-
finally {
|
|
64
|
-
await dir.close();
|
|
56
|
+
secret = (await readSecureModelControlFile(path)).trim();
|
|
65
57
|
}
|
|
66
58
|
}
|
|
67
59
|
if (!/^[a-f0-9]{64}$/.test(secret))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-auto-binding.js","sourceRoot":"","sources":["../../src/agents/model-auto-binding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"model-auto-binding.js","sourceRoot":"","sources":["../../src/agents/model-auto-binding.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function ensureSecureModelControlDirectory(path: string): Promise<void>;
|
|
2
|
+
export declare function readSecureModelControlFile(path: string): Promise<string>;
|
|
3
|
+
export declare function createSecureModelControlFile(path: string, contents: string): Promise<void>;
|
|
4
|
+
export declare function writeSecureModelControlFile(path: string, contents: string): Promise<void>;
|
|
5
|
+
//# sourceMappingURL=model-control-files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-control-files.d.ts","sourceRoot":"","sources":["../../src/agents/model-control-files.ts"],"names":[],"mappings":"AAWA,wBAAsB,iCAAiC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASnF;AAWD,wBAAsB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAS9E;AAOD,wBAAsB,4BAA4B,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhG;AAED,wBAAsB,2BAA2B,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwB/F"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { lstat, mkdir, open, realpath, rename, unlink } from 'node:fs/promises';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
const MAX_MODEL_CONTROL_FILE_BYTES = 64 * 1024;
|
|
6
|
+
function expectedUid() {
|
|
7
|
+
return typeof process.geteuid === 'function' ? process.geteuid() : typeof process.getuid === 'function' ? process.getuid() : undefined;
|
|
8
|
+
}
|
|
9
|
+
export async function ensureSecureModelControlDirectory(path) {
|
|
10
|
+
const directory = resolve(path);
|
|
11
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
12
|
+
const info = await lstat(directory);
|
|
13
|
+
if (!info.isDirectory() || info.isSymbolicLink())
|
|
14
|
+
throw new Error('INSECURE_MODEL_CONTROL_DIRECTORY');
|
|
15
|
+
const uid = expectedUid();
|
|
16
|
+
if (uid !== undefined && info.uid !== uid)
|
|
17
|
+
throw new Error('INSECURE_MODEL_CONTROL_DIRECTORY_OWNER');
|
|
18
|
+
if ((info.mode & 0o022) !== 0)
|
|
19
|
+
throw new Error('INSECURE_MODEL_CONTROL_DIRECTORY_MODE');
|
|
20
|
+
if (await realpath(directory) !== directory)
|
|
21
|
+
throw new Error('INSECURE_MODEL_CONTROL_DIRECTORY_PATH');
|
|
22
|
+
}
|
|
23
|
+
async function validateOpenFile(file) {
|
|
24
|
+
const info = await file.stat();
|
|
25
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
26
|
+
throw new Error('INSECURE_MODEL_CONTROL_FILE_TYPE');
|
|
27
|
+
const uid = expectedUid();
|
|
28
|
+
if (uid !== undefined && info.uid !== uid)
|
|
29
|
+
throw new Error('INSECURE_MODEL_CONTROL_FILE_OWNER');
|
|
30
|
+
if ((info.mode & 0o777) !== 0o600)
|
|
31
|
+
throw new Error('INSECURE_MODEL_CONTROL_FILE_MODE');
|
|
32
|
+
if (info.size > MAX_MODEL_CONTROL_FILE_BYTES)
|
|
33
|
+
throw new Error('MODEL_CONTROL_FILE_TOO_LARGE');
|
|
34
|
+
}
|
|
35
|
+
export async function readSecureModelControlFile(path) {
|
|
36
|
+
await ensureSecureModelControlDirectory(dirname(path));
|
|
37
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
38
|
+
try {
|
|
39
|
+
await validateOpenFile(file);
|
|
40
|
+
return await file.readFile('utf8');
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await file.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function syncDirectory(path) {
|
|
47
|
+
const directory = await open(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
48
|
+
try {
|
|
49
|
+
await directory.sync();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
await directory.close();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export async function createSecureModelControlFile(path, contents) {
|
|
56
|
+
await ensureSecureModelControlDirectory(dirname(path));
|
|
57
|
+
const file = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
58
|
+
try {
|
|
59
|
+
await validateOpenFile(file);
|
|
60
|
+
await file.writeFile(contents);
|
|
61
|
+
await validateOpenFile(file);
|
|
62
|
+
await file.sync();
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
await file.close();
|
|
66
|
+
}
|
|
67
|
+
await syncDirectory(dirname(path));
|
|
68
|
+
}
|
|
69
|
+
export async function writeSecureModelControlFile(path, contents) {
|
|
70
|
+
await ensureSecureModelControlDirectory(dirname(path));
|
|
71
|
+
try {
|
|
72
|
+
const existing = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
73
|
+
try {
|
|
74
|
+
await validateOpenFile(existing);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
await existing.close();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error?.code !== 'ENOENT')
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
const temporaryPath = `${path}.${randomUUID()}.tmp`;
|
|
85
|
+
const file = await open(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
86
|
+
try {
|
|
87
|
+
await validateOpenFile(file);
|
|
88
|
+
await file.writeFile(contents);
|
|
89
|
+
await validateOpenFile(file);
|
|
90
|
+
await file.sync();
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
await file.close().catch(() => undefined);
|
|
94
|
+
await unlink(temporaryPath).catch(() => undefined);
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
await file.close();
|
|
98
|
+
await rename(temporaryPath, path);
|
|
99
|
+
await syncDirectory(dirname(path));
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=model-control-files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-control-files.js","sourceRoot":"","sources":["../../src/agents/model-control-files.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Logger } from '../types/index.js';
|
|
1
2
|
import type { CliSessionStore } from './cli-session-store.js';
|
|
2
3
|
export interface ModelControlConfig {
|
|
3
4
|
allowModelSwitch?: boolean;
|
|
@@ -6,6 +7,18 @@ export interface ModelControlConfig {
|
|
|
6
7
|
deviceSecret: string;
|
|
7
8
|
models: string[];
|
|
8
9
|
}
|
|
10
|
+
interface ModelControllerDependencies {
|
|
11
|
+
fetch?: typeof fetch;
|
|
12
|
+
now?: () => number;
|
|
13
|
+
random?: () => number;
|
|
14
|
+
setTimeout?: typeof setTimeout;
|
|
15
|
+
clearTimeout?: typeof clearTimeout;
|
|
16
|
+
pollIntervalMs?: number;
|
|
17
|
+
baseBackoffMs?: number;
|
|
18
|
+
maxBackoffMs?: number;
|
|
19
|
+
requestTimeoutMs?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare const MODEL_CONTROL_MAX_RESPONSE_BYTES: number;
|
|
9
22
|
/** Agent-scoped durable overlay. No context or session identifiers are rewritten. */
|
|
10
23
|
export declare class ModelController {
|
|
11
24
|
private config;
|
|
@@ -13,20 +26,44 @@ export declare class ModelController {
|
|
|
13
26
|
private initialModel;
|
|
14
27
|
private sessions;
|
|
15
28
|
private provider;
|
|
29
|
+
private logger?;
|
|
16
30
|
private connectionId;
|
|
17
31
|
private timer?;
|
|
18
|
-
private
|
|
32
|
+
private inFlight?;
|
|
19
33
|
private ready;
|
|
20
34
|
private lastSeen;
|
|
21
35
|
private stopped;
|
|
36
|
+
private started;
|
|
37
|
+
private failures;
|
|
38
|
+
private availability;
|
|
22
39
|
private state?;
|
|
23
40
|
private loaded;
|
|
24
|
-
|
|
41
|
+
private loadPromise?;
|
|
42
|
+
private readonly fetchFn;
|
|
43
|
+
private readonly now;
|
|
44
|
+
private readonly random;
|
|
45
|
+
private readonly setTimer;
|
|
46
|
+
private readonly clearTimer;
|
|
47
|
+
private readonly pollIntervalMs;
|
|
48
|
+
private readonly baseBackoffMs;
|
|
49
|
+
private readonly maxBackoffMs;
|
|
50
|
+
private readonly requestTimeoutMs;
|
|
51
|
+
constructor(config: ModelControlConfig, path: string, initialModel: string, sessions: CliSessionStore, provider?: 'claude-cli' | 'codex-cli', logger?: Pick<Logger, "info" | "warn"> | undefined, dependencies?: ModelControllerDependencies);
|
|
25
52
|
setAllowModelSwitch(allowed: boolean): void;
|
|
26
53
|
start(): void;
|
|
27
54
|
stop(): void;
|
|
28
55
|
modelAtStart(): Promise<string>;
|
|
56
|
+
private markUnavailable;
|
|
57
|
+
private markAvailable;
|
|
29
58
|
private save;
|
|
59
|
+
private ensureLoaded;
|
|
60
|
+
private schedule;
|
|
61
|
+
private retryDelay;
|
|
62
|
+
private runScheduled;
|
|
30
63
|
tick(): Promise<void>;
|
|
64
|
+
private runSingleFlight;
|
|
65
|
+
private parseResponse;
|
|
66
|
+
private performTick;
|
|
31
67
|
}
|
|
68
|
+
export {};
|
|
32
69
|
//# sourceMappingURL=model-controller.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-controller.d.ts","sourceRoot":"","sources":["../../src/agents/model-controller.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"model-controller.d.ts","sourceRoot":"","sources":["../../src/agents/model-controller.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAO7D,MAAM,WAAW,kBAAkB;IACjC,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,MAAM,EAAE,CAAA;CACjB;AAMD,UAAU,2BAA2B;IACnC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IACpB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,OAAO,UAAU,CAAA;IAC9B,YAAY,CAAC,EAAE,OAAO,YAAY,CAAA;IAClC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,eAAO,MAAM,gCAAgC,QAAY,CAAA;AAkBzD,qFAAqF;AACrF,qBAAa,eAAe;IAwBxB,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,MAAM,CAAC;IA5BjB,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,KAAK,CAAC,CAA+B;IAC7C,OAAO,CAAC,QAAQ,CAAC,CAAkB;IACnC,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,YAAY,CAAqD;IACzE,OAAO,CAAC,KAAK,CAAC,CAAY;IAC1B,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,WAAW,CAAC,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmB;IAC5C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAChD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IACvC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAQ;gBAG/B,MAAM,EAAE,kBAAkB,EAC1B,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,eAAe,EACzB,QAAQ,GAAE,YAAY,GAAG,WAA0B,EACnD,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,YAAA,EAC9C,YAAY,GAAE,2BAAgC;IAehD,mBAAmB,CAAC,OAAO,EAAE,OAAO;IAEpC,KAAK;IAOL,IAAI;IAOE,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAUrC,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,aAAa;YAMP,IAAI;YAKJ,YAAY;IAwB1B,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,UAAU;YAKJ,YAAY;IAapB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAE3B,OAAO,CAAC,eAAe;YAMT,aAAa;YAyCb,WAAW;CAyC1B"}
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { createSecureModelControlFile, readSecureModelControlFile, writeSecureModelControlFile, } from './model-control-files.js';
|
|
3
|
+
export const MODEL_CONTROL_MAX_RESPONSE_BYTES = 64 * 1024;
|
|
4
|
+
function parseSavedState(raw, models) {
|
|
5
|
+
const value = JSON.parse(raw);
|
|
6
|
+
if (!value || !models.includes(String(value.modelId)) || !Number.isSafeInteger(value.version) || Number(value.version) < 0) {
|
|
7
|
+
throw new Error('INVALID_MODEL_CONTROL_STATE');
|
|
8
|
+
}
|
|
9
|
+
if (value.result) {
|
|
10
|
+
const result = value.result;
|
|
11
|
+
if (typeof result.requestId !== 'string' || !['applied', 'failed'].includes(result.state)
|
|
12
|
+
|| !models.includes(result.modelId) || !Number.isSafeInteger(result.version) || result.version < 0) {
|
|
13
|
+
throw new Error('INVALID_MODEL_CONTROL_STATE');
|
|
14
|
+
}
|
|
15
|
+
if (result.modelId !== value.modelId || result.version !== value.version)
|
|
16
|
+
throw new Error('INVALID_MODEL_CONTROL_STATE');
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
4
20
|
/** Agent-scoped durable overlay. No context or session identifiers are rewritten. */
|
|
5
21
|
export class ModelController {
|
|
6
22
|
config;
|
|
@@ -8,97 +24,226 @@ export class ModelController {
|
|
|
8
24
|
initialModel;
|
|
9
25
|
sessions;
|
|
10
26
|
provider;
|
|
27
|
+
logger;
|
|
11
28
|
connectionId = randomUUID();
|
|
12
29
|
timer;
|
|
13
|
-
|
|
30
|
+
inFlight;
|
|
14
31
|
ready = false;
|
|
15
32
|
lastSeen = 0;
|
|
16
33
|
stopped = false;
|
|
34
|
+
started = false;
|
|
35
|
+
failures = 0;
|
|
36
|
+
availability = 'unknown';
|
|
17
37
|
state;
|
|
18
38
|
loaded = false;
|
|
19
|
-
|
|
39
|
+
loadPromise;
|
|
40
|
+
fetchFn;
|
|
41
|
+
now;
|
|
42
|
+
random;
|
|
43
|
+
setTimer;
|
|
44
|
+
clearTimer;
|
|
45
|
+
pollIntervalMs;
|
|
46
|
+
baseBackoffMs;
|
|
47
|
+
maxBackoffMs;
|
|
48
|
+
requestTimeoutMs;
|
|
49
|
+
constructor(config, path, initialModel, sessions, provider = 'claude-cli', logger, dependencies = {}) {
|
|
20
50
|
this.config = config;
|
|
21
51
|
this.path = path;
|
|
22
52
|
this.initialModel = initialModel;
|
|
23
53
|
this.sessions = sessions;
|
|
24
54
|
this.provider = provider;
|
|
55
|
+
this.logger = logger;
|
|
56
|
+
const endpoint = new URL(config.endpoint);
|
|
57
|
+
if (endpoint.protocol !== 'https:' || endpoint.username || endpoint.password || endpoint.hash)
|
|
58
|
+
throw new Error('MODEL_CONTROL_HTTPS_REQUIRED');
|
|
59
|
+
this.fetchFn = dependencies.fetch ?? globalThis.fetch;
|
|
60
|
+
this.now = dependencies.now ?? Date.now;
|
|
61
|
+
this.random = dependencies.random ?? Math.random;
|
|
62
|
+
this.setTimer = dependencies.setTimeout ?? setTimeout;
|
|
63
|
+
this.clearTimer = dependencies.clearTimeout ?? clearTimeout;
|
|
64
|
+
this.pollIntervalMs = dependencies.pollIntervalMs ?? 2_000;
|
|
65
|
+
this.baseBackoffMs = dependencies.baseBackoffMs ?? 2_000;
|
|
66
|
+
this.maxBackoffMs = dependencies.maxBackoffMs ?? 60_000;
|
|
67
|
+
this.requestTimeoutMs = dependencies.requestTimeoutMs ?? 8_000;
|
|
25
68
|
}
|
|
26
69
|
setAllowModelSwitch(allowed) { this.config = { ...this.config, allowModelSwitch: allowed }; this.ready = false; }
|
|
27
|
-
start() {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
await new Promise(resolve => setTimeout(resolve, 200));
|
|
34
|
-
}
|
|
35
|
-
throw new Error('MODEL_CONTROL_UNAVAILABLE: model change is awaiting executor confirmation');
|
|
70
|
+
start() {
|
|
71
|
+
if (this.started && !this.stopped)
|
|
72
|
+
return;
|
|
73
|
+
this.started = true;
|
|
74
|
+
this.stopped = false;
|
|
75
|
+
this.schedule(0);
|
|
36
76
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
finally {
|
|
46
|
-
await file.close();
|
|
47
|
-
}
|
|
48
|
-
await rename(tmp, this.path);
|
|
49
|
-
const dir = await open(dirname(this.path), 'r');
|
|
77
|
+
stop() {
|
|
78
|
+
this.stopped = true;
|
|
79
|
+
this.ready = false;
|
|
80
|
+
if (this.timer)
|
|
81
|
+
this.clearTimer(this.timer);
|
|
82
|
+
this.timer = undefined;
|
|
83
|
+
}
|
|
84
|
+
async modelAtStart() {
|
|
50
85
|
try {
|
|
51
|
-
await
|
|
86
|
+
await this.ensureLoaded();
|
|
52
87
|
}
|
|
53
|
-
|
|
54
|
-
|
|
88
|
+
catch {
|
|
89
|
+
this.markUnavailable('STATE_UNAVAILABLE', 'initial');
|
|
90
|
+
return this.initialModel;
|
|
55
91
|
}
|
|
92
|
+
const model = this.state?.modelId ?? this.initialModel;
|
|
93
|
+
if (this.availability !== 'available' || this.now() - this.lastSeen >= 20_000)
|
|
94
|
+
this.markUnavailable('SERVICE_UNAVAILABLE', this.state ? 'durable' : 'initial');
|
|
95
|
+
return model;
|
|
96
|
+
}
|
|
97
|
+
markUnavailable(reason, source) {
|
|
98
|
+
if (this.availability === 'unavailable')
|
|
99
|
+
return;
|
|
100
|
+
this.availability = 'unavailable';
|
|
101
|
+
this.logger?.warn({ event: 'MODEL_CONTROL_AVAILABILITY', state: 'unavailable', reason, source }, 'Model control unavailable; using safe fallback model');
|
|
102
|
+
}
|
|
103
|
+
markAvailable() {
|
|
104
|
+
const recovered = this.availability === 'unavailable';
|
|
105
|
+
this.availability = 'available';
|
|
106
|
+
if (recovered)
|
|
107
|
+
this.logger?.info({ event: 'MODEL_CONTROL_AVAILABILITY', state: 'available' }, 'Model control availability recovered');
|
|
108
|
+
}
|
|
109
|
+
async save(state) {
|
|
110
|
+
await writeSecureModelControlFile(this.path, JSON.stringify(state));
|
|
56
111
|
this.state = state;
|
|
57
112
|
}
|
|
58
|
-
async
|
|
59
|
-
if (this.
|
|
113
|
+
async ensureLoaded() {
|
|
114
|
+
if (this.loaded)
|
|
60
115
|
return;
|
|
61
|
-
this.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
116
|
+
if (this.loadPromise)
|
|
117
|
+
return this.loadPromise;
|
|
118
|
+
this.loadPromise = (async () => {
|
|
119
|
+
try {
|
|
120
|
+
const connection = JSON.parse(await readSecureModelControlFile(this.path + '.connection'));
|
|
121
|
+
this.connectionId = String(connection.id ?? '');
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error?.code !== 'ENOENT')
|
|
125
|
+
throw error;
|
|
65
126
|
try {
|
|
66
|
-
|
|
127
|
+
await createSecureModelControlFile(this.path + '.connection', JSON.stringify({ id: this.connectionId }));
|
|
67
128
|
}
|
|
68
|
-
catch (
|
|
69
|
-
if (
|
|
70
|
-
throw
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
await f.writeFile(JSON.stringify({ id: this.connectionId }));
|
|
74
|
-
await f.sync();
|
|
75
|
-
}
|
|
76
|
-
finally {
|
|
77
|
-
await f.close();
|
|
78
|
-
}
|
|
129
|
+
catch (createError) {
|
|
130
|
+
if (createError?.code !== 'EEXIST')
|
|
131
|
+
throw createError;
|
|
132
|
+
const connection = JSON.parse(await readSecureModelControlFile(this.path + '.connection'));
|
|
133
|
+
this.connectionId = String(connection.id ?? '');
|
|
79
134
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
135
|
+
}
|
|
136
|
+
if (!/^[a-f0-9-]{36}$/.test(this.connectionId))
|
|
137
|
+
throw new Error('INVALID_EXECUTOR_CONNECTION');
|
|
138
|
+
try {
|
|
139
|
+
this.state = parseSavedState(await readSecureModelControlFile(this.path), this.config.models);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (error?.code !== 'ENOENT')
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
this.loaded = true;
|
|
146
|
+
})().finally(() => { this.loadPromise = undefined; });
|
|
147
|
+
return this.loadPromise;
|
|
148
|
+
}
|
|
149
|
+
schedule(delayMs) {
|
|
150
|
+
if (this.stopped)
|
|
151
|
+
return;
|
|
152
|
+
this.timer = this.setTimer(() => void this.runScheduled(), delayMs);
|
|
153
|
+
this.timer.unref?.();
|
|
154
|
+
}
|
|
155
|
+
retryDelay() {
|
|
156
|
+
const exponential = Math.min(this.maxBackoffMs, this.baseBackoffMs * (2 ** Math.max(0, this.failures - 1)));
|
|
157
|
+
return Math.min(this.maxBackoffMs, Math.round(exponential * (0.5 + this.random())));
|
|
158
|
+
}
|
|
159
|
+
async runScheduled() {
|
|
160
|
+
if (this.stopped)
|
|
161
|
+
return;
|
|
162
|
+
const succeeded = await this.runSingleFlight();
|
|
163
|
+
if (this.stopped)
|
|
164
|
+
return;
|
|
165
|
+
if (succeeded) {
|
|
166
|
+
this.failures = 0;
|
|
167
|
+
this.schedule(this.pollIntervalMs);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
this.failures += 1;
|
|
171
|
+
this.schedule(this.retryDelay());
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async tick() { await this.runSingleFlight(); }
|
|
175
|
+
runSingleFlight() {
|
|
176
|
+
if (this.inFlight)
|
|
177
|
+
return this.inFlight;
|
|
178
|
+
this.inFlight = this.performTick().finally(() => { this.inFlight = undefined; });
|
|
179
|
+
return this.inFlight;
|
|
180
|
+
}
|
|
181
|
+
async parseResponse(response) {
|
|
182
|
+
const declaredHeader = response.headers.get('content-length');
|
|
183
|
+
if (declaredHeader !== null) {
|
|
184
|
+
const declaredLength = Number(declaredHeader);
|
|
185
|
+
if (!/^\d+$/.test(declaredHeader) || !Number.isSafeInteger(declaredLength)) {
|
|
186
|
+
await response.body?.cancel().catch(() => undefined);
|
|
187
|
+
throw new Error('INVALID_MODEL_CONTROL_CONTENT_LENGTH');
|
|
188
|
+
}
|
|
189
|
+
if (declaredLength > MODEL_CONTROL_MAX_RESPONSE_BYTES) {
|
|
190
|
+
await response.body?.cancel().catch(() => undefined);
|
|
191
|
+
throw new Error('MODEL_CONTROL_RESPONSE_TOO_LARGE');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (!response.body)
|
|
195
|
+
throw new Error('INVALID_MODEL_CONTROL_RESPONSE_BODY');
|
|
196
|
+
const reader = response.body.getReader();
|
|
197
|
+
const chunks = [];
|
|
198
|
+
let byteLength = 0;
|
|
199
|
+
try {
|
|
200
|
+
while (true) {
|
|
201
|
+
const { done, value } = await reader.read();
|
|
202
|
+
if (done)
|
|
203
|
+
break;
|
|
204
|
+
if (!value?.byteLength)
|
|
205
|
+
continue;
|
|
206
|
+
byteLength += value.byteLength;
|
|
207
|
+
if (byteLength > MODEL_CONTROL_MAX_RESPONSE_BYTES) {
|
|
208
|
+
await reader.cancel().catch(() => undefined);
|
|
209
|
+
throw new Error('MODEL_CONTROL_RESPONSE_TOO_LARGE');
|
|
88
210
|
}
|
|
89
|
-
|
|
211
|
+
chunks.push(value);
|
|
90
212
|
}
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
await reader.cancel().catch(() => undefined);
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
reader.releaseLock();
|
|
220
|
+
}
|
|
221
|
+
const bytes = new Uint8Array(byteLength);
|
|
222
|
+
let offset = 0;
|
|
223
|
+
for (const chunk of chunks) {
|
|
224
|
+
bytes.set(chunk, offset);
|
|
225
|
+
offset += chunk.byteLength;
|
|
226
|
+
}
|
|
227
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
228
|
+
}
|
|
229
|
+
async performTick() {
|
|
230
|
+
if (this.stopped)
|
|
231
|
+
return false;
|
|
232
|
+
try {
|
|
233
|
+
await this.ensureLoaded();
|
|
91
234
|
const capabilityRevision = createHash('sha256').update(JSON.stringify([this.config.models, this.config.allowModelSwitch ?? true])).digest('hex');
|
|
92
|
-
const response = await
|
|
93
|
-
method: 'POST',
|
|
235
|
+
const response = await this.fetchFn(this.config.endpoint, {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
redirect: 'error',
|
|
238
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${this.config.botToken}`, 'X-Model-Device-Secret': this.config.deviceSecret },
|
|
94
239
|
body: JSON.stringify({ protocolVersion: 2, allowModelSwitch: this.config.allowModelSwitch ?? true, initialModel: this.initialModel, connectionId: this.connectionId, provider: this.provider, models: this.config.models, capabilityRevision, result: this.state?.result }),
|
|
95
|
-
signal: AbortSignal.timeout(
|
|
240
|
+
signal: AbortSignal.timeout(this.requestTimeoutMs),
|
|
96
241
|
});
|
|
97
242
|
if (!response.ok) {
|
|
98
243
|
await response.body?.cancel();
|
|
99
244
|
throw new Error('MODEL_CONTROL_REJECTED');
|
|
100
245
|
}
|
|
101
|
-
const r = await
|
|
246
|
+
const r = await this.parseResponse(response);
|
|
102
247
|
if (r.protocolVersion !== 2)
|
|
103
248
|
throw new Error('MODEL_SERVER_UPGRADE_REQUIRED');
|
|
104
249
|
if (!Number.isSafeInteger(r.version) || !this.config.models.includes(r.modelId))
|
|
@@ -113,26 +258,26 @@ export class ModelController {
|
|
|
113
258
|
throw new Error('MODEL_CONFIGURATION_DRIFT');
|
|
114
259
|
if (current.result)
|
|
115
260
|
await this.save({ modelId: current.modelId, version: current.version });
|
|
116
|
-
this.lastSeen =
|
|
261
|
+
this.lastSeen = this.now();
|
|
117
262
|
if (r.command) {
|
|
118
263
|
this.ready = false;
|
|
119
264
|
const command = r.command;
|
|
120
|
-
if (Number(command.expected_version) !== current.version)
|
|
265
|
+
if (typeof command.id !== 'string' || !command.id || Number(command.expected_version) !== current.version)
|
|
121
266
|
throw new Error('MODEL_VERSION_CONFLICT');
|
|
122
267
|
const entries = Object.values(await this.sessions.list());
|
|
123
268
|
const compatible = this.config.allowModelSwitch !== false && this.config.models.includes(command.target_model) && entries.every(s => !s.provider || (s.provider === this.provider && !!s.cliSessionIds?.[this.provider]));
|
|
124
269
|
const next = compatible ? { modelId: command.target_model, version: current.version + 1 } : { modelId: current.modelId, version: current.version };
|
|
125
270
|
await this.save({ ...next, result: { requestId: command.id, state: compatible ? 'applied' : 'failed', ...next } });
|
|
126
|
-
// Keep the start gate closed until the durable result is acknowledged on the next poll.
|
|
127
271
|
}
|
|
128
272
|
else
|
|
129
273
|
this.ready = true;
|
|
274
|
+
this.markAvailable();
|
|
275
|
+
return true;
|
|
130
276
|
}
|
|
131
277
|
catch {
|
|
132
278
|
this.ready = false;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
this.busy = false;
|
|
279
|
+
this.markUnavailable('POLL_FAILED', this.state ? 'durable' : 'initial');
|
|
280
|
+
return false;
|
|
136
281
|
}
|
|
137
282
|
}
|
|
138
283
|
}
|