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.
Files changed (79) hide show
  1. package/README.md +30 -0
  2. package/config.multi-agent.example.json +4 -1
  3. package/dist/agents/agent-resolver.d.ts.map +1 -1
  4. package/dist/agents/agent-resolver.js +8 -2
  5. package/dist/agents/agent-resolver.js.map +1 -1
  6. package/dist/agents/cli-runner.d.ts +3 -3
  7. package/dist/agents/cli-runner.d.ts.map +1 -1
  8. package/dist/agents/cli-runner.js +87 -76
  9. package/dist/agents/cli-runner.js.map +1 -1
  10. package/dist/agents/cli-session-store.d.ts +3 -0
  11. package/dist/agents/cli-session-store.d.ts.map +1 -1
  12. package/dist/agents/cli-session-store.js +31 -10
  13. package/dist/agents/cli-session-store.js.map +1 -1
  14. package/dist/agents/codex-app-server-runtime.d.ts +37 -1
  15. package/dist/agents/codex-app-server-runtime.d.ts.map +1 -1
  16. package/dist/agents/codex-app-server-runtime.js +37 -14
  17. package/dist/agents/codex-app-server-runtime.js.map +1 -1
  18. package/dist/agents/managed-backend.d.ts +4 -0
  19. package/dist/agents/managed-backend.d.ts.map +1 -0
  20. package/dist/agents/managed-backend.js +18 -0
  21. package/dist/agents/managed-backend.js.map +1 -0
  22. package/dist/agents/managed-role.d.ts +17 -0
  23. package/dist/agents/managed-role.d.ts.map +1 -0
  24. package/dist/agents/managed-role.js +13 -0
  25. package/dist/agents/managed-role.js.map +1 -0
  26. package/dist/agents/model-auto-binding.d.ts.map +1 -1
  27. package/dist/agents/model-auto-binding.js +15 -23
  28. package/dist/agents/model-auto-binding.js.map +1 -1
  29. package/dist/agents/model-control-files.d.ts +5 -0
  30. package/dist/agents/model-control-files.d.ts.map +1 -0
  31. package/dist/agents/model-control-files.js +101 -0
  32. package/dist/agents/model-control-files.js.map +1 -0
  33. package/dist/agents/model-controller.d.ts +39 -2
  34. package/dist/agents/model-controller.d.ts.map +1 -1
  35. package/dist/agents/model-controller.js +211 -66
  36. package/dist/agents/model-controller.js.map +1 -1
  37. package/dist/agents/trusted-launcher.js +5 -1
  38. package/dist/agents/trusted-launcher.js.map +1 -1
  39. package/dist/channels/bot-run-protocol.d.ts +13 -0
  40. package/dist/channels/bot-run-protocol.d.ts.map +1 -0
  41. package/dist/channels/bot-run-protocol.js +20 -0
  42. package/dist/channels/bot-run-protocol.js.map +1 -0
  43. package/dist/channels/bot-run-receipts.d.ts +14 -0
  44. package/dist/channels/bot-run-receipts.d.ts.map +1 -0
  45. package/dist/channels/bot-run-receipts.js +88 -0
  46. package/dist/channels/bot-run-receipts.js.map +1 -0
  47. package/dist/channels/botlink.d.ts +3 -0
  48. package/dist/channels/botlink.d.ts.map +1 -1
  49. package/dist/channels/botlink.js +95 -17
  50. package/dist/channels/botlink.js.map +1 -1
  51. package/dist/channels/managed-receipts.d.ts +14 -0
  52. package/dist/channels/managed-receipts.d.ts.map +1 -0
  53. package/dist/channels/managed-receipts.js +136 -0
  54. package/dist/channels/managed-receipts.js.map +1 -0
  55. package/dist/cli/cmd-start.d.ts.map +1 -1
  56. package/dist/cli/cmd-start.js +49 -7
  57. package/dist/cli/cmd-start.js.map +1 -1
  58. package/dist/cloud/main.d.ts +2 -0
  59. package/dist/cloud/main.d.ts.map +1 -0
  60. package/dist/cloud/main.js +14 -0
  61. package/dist/cloud/main.js.map +1 -0
  62. package/dist/cloud/worker.d.ts +98 -0
  63. package/dist/cloud/worker.d.ts.map +1 -0
  64. package/dist/cloud/worker.js +212 -0
  65. package/dist/cloud/worker.js.map +1 -0
  66. package/dist/config/agent-dirs.d.ts.map +1 -1
  67. package/dist/config/agent-dirs.js +2 -0
  68. package/dist/config/agent-dirs.js.map +1 -1
  69. package/dist/config/attachment-handoff.d.ts +5 -0
  70. package/dist/config/attachment-handoff.d.ts.map +1 -0
  71. package/dist/config/attachment-handoff.js +60 -0
  72. package/dist/config/attachment-handoff.js.map +1 -0
  73. package/dist/config/zod-schema.d.ts +447 -282
  74. package/dist/config/zod-schema.d.ts.map +1 -1
  75. package/dist/config/zod-schema.js +15 -1
  76. package/dist/config/zod-schema.js.map +1 -1
  77. package/dist/types/index.d.ts +12 -4
  78. package/dist/types/index.d.ts.map +1 -1
  79. 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 explicit = config.agents.list?.find(a => a.id === agentId)?.modelControl;
14
- if (explicit)
15
- return { ...explicit, allowModelSwitch: modelSwitchAllowed(config, agentId) };
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 readFile(path, 'utf8')).trim();
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
- const f = await open(path, 'wx', 0o600);
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 readFile(path, 'utf8')).trim();
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":"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"}
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 busy;
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
- constructor(config: ModelControlConfig, path: string, initialModel: string, sessions: CliSessionStore, provider?: 'claude-cli' | 'codex-cli');
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":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAE7D,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,qFAAqF;AACrF,qBAAa,eAAe;IASd,OAAO,CAAC,MAAM;IAAsB,OAAO,CAAC,IAAI;IAAU,OAAO,CAAC,YAAY;IAAU,OAAO,CAAC,QAAQ;IAAmB,OAAO,CAAC,QAAQ;IARvJ,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,KAAK,CAAC,CAAgC;IAC9C,OAAO,CAAC,IAAI,CAAQ;IACpB,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,KAAK,CAAC,CAAY;IAC1B,OAAO,CAAC,MAAM,CAAQ;gBACF,MAAM,EAAE,kBAAkB,EAAU,IAAI,EAAE,MAAM,EAAU,YAAY,EAAE,MAAM,EAAU,QAAQ,EAAE,eAAe,EAAU,QAAQ,GAAE,YAAY,GAAG,WAA0B;IAClM,mBAAmB,CAAC,OAAO,EAAE,OAAO;IACpC,KAAK;IACL,IAAI;IACE,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;YAOvB,IAAI;IAUZ,IAAI;CA+CX"}
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 { mkdir, open, readFile, rename } from 'node:fs/promises';
3
- import { dirname } from 'node:path';
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
- busy = false;
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
- constructor(config, path, initialModel, sessions, provider = 'claude-cli') {
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() { void this.tick(); this.timer = setInterval(() => void this.tick(), 2000); this.timer.unref(); }
28
- stop() { this.stopped = true; this.ready = false; clearInterval(this.timer); }
29
- async modelAtStart() {
30
- for (let i = 0; i < 300 && !this.stopped; i++) {
31
- if (this.ready && this.state && Date.now() - this.lastSeen < 20000)
32
- return this.state.modelId;
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
- async save(state) {
38
- await mkdir(dirname(this.path), { recursive: true });
39
- const tmp = `${this.path}.${randomUUID()}.tmp`;
40
- const file = await open(tmp, 'wx', 0o600);
41
- try {
42
- await file.writeFile(JSON.stringify(state));
43
- await file.sync();
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 dir.sync();
86
+ await this.ensureLoaded();
52
87
  }
53
- finally {
54
- await dir.close();
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 tick() {
59
- if (this.busy || this.stopped)
113
+ async ensureLoaded() {
114
+ if (this.loaded)
60
115
  return;
61
- this.busy = true;
62
- try {
63
- if (!this.loaded) {
64
- await mkdir(dirname(this.path), { recursive: true });
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
- this.connectionId = JSON.parse(await readFile(this.path + '.connection', 'utf8')).id;
127
+ await createSecureModelControlFile(this.path + '.connection', JSON.stringify({ id: this.connectionId }));
67
128
  }
68
- catch (e) {
69
- if (e.code !== 'ENOENT')
70
- throw e;
71
- const f = await open(this.path + '.connection', 'wx', 0o600);
72
- try {
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
- if (!/^[a-f0-9-]{36}$/.test(this.connectionId))
81
- throw new Error('INVALID_EXECUTOR_CONNECTION');
82
- try {
83
- this.state = JSON.parse(await readFile(this.path, 'utf8'));
84
- }
85
- catch (e) {
86
- if (e.code !== 'ENOENT')
87
- throw e;
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
- this.loaded = true;
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 fetch(this.config.endpoint, {
93
- method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${this.config.botToken}`, 'X-Model-Device-Secret': this.config.deviceSecret },
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(8000),
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 response.json();
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 = Date.now();
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
- finally {
135
- this.busy = false;
279
+ this.markUnavailable('POLL_FAILED', this.state ? 'durable' : 'initial');
280
+ return false;
136
281
  }
137
282
  }
138
283
  }