@stage-labs/metro 0.1.0-beta.306 → 0.1.0-beta.308
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/dist/agent.js +23 -28
- package/dist/child.js +33 -0
- package/dist/claude-env.js +104 -0
- package/dist/cli.js +7 -21
- package/dist/lock.js +62 -0
- package/dist/memory.js +12 -27
- package/dist/root-release-io.js +1 -2
- package/dist/root-release.js +1 -1
- package/dist/route.js +0 -1
- package/hooks/guard.mjs +1 -5
- package/hooks/session-start.mjs +1 -1
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +13 -38
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +21 -1
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +5 -8
- package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +6 -18
- package/runtime/node_modules/@metro-labs/daemon/src/claude/github-skills.ts +3 -7
- package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +6 -5
- package/runtime/node_modules/@metro-labs/daemon/src/claude/onboarding.ts +1 -23
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +32 -112
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup-api.ts +14 -61
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +10 -42
- package/runtime/node_modules/@metro-labs/daemon/src/claude/usage-probe.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/agent-key.ts +9 -12
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/enroll-api.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/leftovers.ts +59 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +0 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +1 -13
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +15 -35
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +1 -2
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +5 -15
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/instructions.ts +0 -11
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-slot.ts +3 -7
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +12 -24
- package/runtime/node_modules/@metro-labs/daemon/src/routes/bearer.ts +1 -4
- package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +1 -2
- package/runtime/node_modules/@metro-labs/daemon/src/voice/call.ts +9 -92
- package/runtime/node_modules/@metro-labs/daemon/src/voice/calls.ts +1 -4
- package/runtime/node_modules/@metro-labs/daemon/src/voice/scribe.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/voice/shared.ts +0 -2
- package/runtime/node_modules/@metro-labs/daemon/src/voice/speech.ts +0 -4
- package/runtime/node_modules/@metro-labs/daemon/src/voice/store.ts +1 -14
- package/runtime/node_modules/@metro-labs/http/src/agent-signature.ts +5 -20
- package/runtime/node_modules/@metro-labs/http/src/api-http.ts +0 -1
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/runner.json +1 -1
- package/dist/background.js +0 -141
- package/dist/claude.js +0 -204
- package/dist/mcp-config.js +0 -27
- package/dist/native-recovery-scan.js +0 -246
- package/dist/native-recovery-state.js +0 -154
- package/dist/native-recovery.js +0 -164
- package/dist/onboarding.js +0 -44
- package/dist/voice.js +0 -56
- package/runtime/node_modules/@metro-labs/daemon/src/claude/channel-check.ts +0 -56
- package/runtime/node_modules/@metro-labs/daemon/src/claude/pinned.ts +0 -16
- package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-cleanup.ts +0 -121
- package/runtime/node_modules/@metro-labs/daemon/src/claude/runner.ts +0 -45
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session-continuity.ts +0 -57
- package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +0 -76
- package/runtime/node_modules/@metro-labs/daemon/src/voice/approvals.ts +0 -38
- package/runtime/node_modules/@metro-labs/daemon/src/voice/brain.ts +0 -146
- package/runtime/node_modules/@metro-labs/daemon/src/voice/speakable.ts +0 -54
- package/runtime/node_modules/@metro-labs/daemon/src/voice/talk.ts +0 -264
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { IncomingMessage } from 'node:http';
|
|
2
|
-
import { log } from '@metro-labs/core/log';
|
|
3
2
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
4
3
|
import { ApiError } from '@metro-labs/http/api-error';
|
|
5
|
-
import { readJsonBody
|
|
4
|
+
import { readJsonBody } from '@metro-labs/http/api-http';
|
|
6
5
|
import {
|
|
7
6
|
claudeSetupStatus,
|
|
8
7
|
ensureClaudeSetup,
|
|
@@ -19,12 +18,10 @@ import {
|
|
|
19
18
|
type PermissionMode,
|
|
20
19
|
type SetupDeps,
|
|
21
20
|
} from './setup.js';
|
|
22
|
-
import {
|
|
23
|
-
import { assertSdkDrained, assertSessionStopped, prepareSessionRunner, setAutostart, withSessionChange, type SessionDeps } from './session.js';
|
|
21
|
+
import { assertSessionStopped, type SessionDeps } from './session.js';
|
|
24
22
|
import { tryMemoryJob } from './memory-routine.js';
|
|
25
23
|
|
|
26
24
|
const SETUP_BODY_MAX = SYSTEM_PROMPT_MAX * 2;
|
|
27
|
-
const OPERATOR_ONLY = 'only the Metro operator can allow the Agent SDK on a Claude login';
|
|
28
25
|
|
|
29
26
|
export interface SetupApiDeps {
|
|
30
27
|
session?: SessionDeps;
|
|
@@ -36,8 +33,6 @@ export interface SetupApiDeps {
|
|
|
36
33
|
interface SetupChange {
|
|
37
34
|
privacy?: boolean;
|
|
38
35
|
permissionMode?: PermissionMode;
|
|
39
|
-
runner?: HarnessRunner;
|
|
40
|
-
sdkOnLogin?: boolean;
|
|
41
36
|
systemPrompt?: string;
|
|
42
37
|
liveEvents?: boolean;
|
|
43
38
|
memoryRoutine?: boolean;
|
|
@@ -62,19 +57,11 @@ function modeChange(raw: unknown): PermissionMode | undefined {
|
|
|
62
57
|
return raw;
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
function runnerChange(raw: unknown): HarnessRunner | undefined {
|
|
66
|
-
if (raw === undefined) return undefined;
|
|
67
|
-
if (!isHarnessRunner(raw)) throw new ApiError('runner must be cli or sdk', 400);
|
|
68
|
-
return raw;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
60
|
function setupChange(body: unknown): SetupChange {
|
|
72
61
|
if (!isRecord(body)) throw new ApiError('a body is required', 400);
|
|
73
62
|
const change: SetupChange = {
|
|
74
63
|
privacy: flagChange(body.privacy, 'privacy'),
|
|
75
64
|
permissionMode: modeChange(body.permissionMode),
|
|
76
|
-
runner: runnerChange(body.runner),
|
|
77
|
-
sdkOnLogin: flagChange(body.sdkOnLogin, 'sdkOnLogin'),
|
|
78
65
|
systemPrompt: promptChange(body.systemPrompt),
|
|
79
66
|
liveEvents: flagChange(body.liveEvents, 'liveEvents'),
|
|
80
67
|
memoryRoutine: flagChange(body.memoryRoutine, 'memoryRoutine'),
|
|
@@ -84,44 +71,11 @@ function setupChange(body: unknown): SetupChange {
|
|
|
84
71
|
return given;
|
|
85
72
|
}
|
|
86
73
|
|
|
87
|
-
function checkRunnerChange(change: SetupChange, session: ApiSession, agents: string | undefined): void {
|
|
88
|
-
if (change.sdkOnLogin !== undefined && session.operator !== true) throw new ApiError(OPERATOR_ONLY, 403);
|
|
89
|
-
if (change.runner === 'sdk' && !sdkAllowed(agents, undefined, change.sdkOnLogin ?? sdkOnLogin(agents))) throw new ApiError(SDK_NEEDS_KEY, 403);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function nextRunner(change: SetupChange, agents: string | undefined): HarnessRunner {
|
|
93
|
-
const runner = change.runner ?? harnessRunner(agents);
|
|
94
|
-
return runner === 'sdk' && change.sdkOnLogin === false && !sdkAllowed(agents, undefined, false) ? 'cli' : runner;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
74
|
function changesSession(change: SetupChange, agents: string | undefined): boolean {
|
|
98
|
-
return
|
|
99
|
-
|| (change.permissionMode !== undefined && change.permissionMode !== permissionMode(agents))
|
|
75
|
+
return (change.permissionMode !== undefined && change.permissionMode !== permissionMode(agents))
|
|
100
76
|
|| (change.systemPrompt !== undefined && change.systemPrompt.trim() !== systemPrompt(agents));
|
|
101
77
|
}
|
|
102
78
|
|
|
103
|
-
function saveSessionSettings(change: SetupChange, runner: HarnessRunner, agents: string | undefined): void {
|
|
104
|
-
if (change.sdkOnLogin !== undefined && change.sdkOnLogin !== sdkOnLogin(agents)) {
|
|
105
|
-
setSdkOnLogin(change.sdkOnLogin, agents);
|
|
106
|
-
log.info({ on: change.sdkOnLogin }, 'claude-setup: the operator changed whether the Agent SDK may run on this box’s Claude login');
|
|
107
|
-
}
|
|
108
|
-
if (runner !== harnessRunner(agents)) {
|
|
109
|
-
setAutostart(false, agents);
|
|
110
|
-
setHarnessRunner(runner, agents);
|
|
111
|
-
}
|
|
112
|
-
if (change.permissionMode !== undefined) setPermissionMode(change.permissionMode, agents);
|
|
113
|
-
if (change.systemPrompt !== undefined) setSystemPrompt(change.systemPrompt, agents);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function checkStoppedChange(change: SetupChange, deps: SetupApiDeps): HarnessRunner {
|
|
117
|
-
const agents = deps.setup?.agents;
|
|
118
|
-
const session = { ...deps.session, agents: deps.session?.agents ?? agents };
|
|
119
|
-
const runner = nextRunner(change, agents);
|
|
120
|
-
if (changesSession(change, agents)) assertSessionStopped(session);
|
|
121
|
-
if (runner !== harnessRunner(agents) && harnessRunner(agents) === 'sdk') assertSdkDrained(session);
|
|
122
|
-
return runner;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
79
|
function applyLiveEvents(on: boolean, deps: SetupApiDeps): void {
|
|
126
80
|
const agents = deps.setup?.agents;
|
|
127
81
|
if (on !== liveEvents(agents)) setLiveEvents(on, agents);
|
|
@@ -133,29 +87,28 @@ function applyMemoryRoutine(on: boolean, deps: SetupApiDeps): void {
|
|
|
133
87
|
(deps.memoryJob ?? tryMemoryJob)(on);
|
|
134
88
|
}
|
|
135
89
|
|
|
136
|
-
|
|
90
|
+
function saveSessionSettings(change: SetupChange, deps: SetupApiDeps): void {
|
|
91
|
+
const agents = deps.setup?.agents;
|
|
92
|
+
if (changesSession(change, agents)) assertSessionStopped({ ...deps.session, agents: deps.session?.agents ?? agents });
|
|
93
|
+
if (change.permissionMode !== undefined) setPermissionMode(change.permissionMode, agents);
|
|
94
|
+
if (change.systemPrompt !== undefined) setSystemPrompt(change.systemPrompt, agents);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function applySetupChange(change: SetupChange, deps: SetupApiDeps): void {
|
|
137
98
|
const setup = deps.setup ?? {};
|
|
138
|
-
|
|
139
|
-
const runner = checkStoppedChange(change, deps);
|
|
140
|
-
if (runner !== harnessRunner(setup.agents)) {
|
|
141
|
-
await prepareSessionRunner(runner, deps.session);
|
|
142
|
-
checkRunnerChange(change, session, setup.agents);
|
|
143
|
-
if (checkStoppedChange(change, deps) !== runner) throw new ApiError('The model setup changed during preparation. Choose the runner again.', 409);
|
|
144
|
-
}
|
|
145
|
-
if (req.socket.destroyed) throw new ApiError('The request disconnected during preparation. The runner choice was not changed.', 409);
|
|
99
|
+
saveSessionSettings(change, deps);
|
|
146
100
|
if (change.privacy !== undefined) setPrivacy(change.privacy, setup.agents);
|
|
147
101
|
if (change.liveEvents !== undefined) applyLiveEvents(change.liveEvents, deps);
|
|
148
102
|
if (change.memoryRoutine !== undefined) applyMemoryRoutine(change.memoryRoutine, deps);
|
|
149
|
-
saveSessionSettings(change, runner, setup.agents);
|
|
150
103
|
ensureClaudeSetup(setup);
|
|
151
104
|
}
|
|
152
105
|
|
|
153
|
-
export async function setupAnswer(req: IncomingMessage, deps: SetupApiDeps
|
|
106
|
+
export async function setupAnswer(req: IncomingMessage, deps: SetupApiDeps): Promise<unknown> {
|
|
154
107
|
const setup = deps.setup ?? {};
|
|
155
108
|
const method = req.method ?? 'GET';
|
|
156
109
|
if (method === 'GET') return claudeSetupStatus(setup);
|
|
157
110
|
if (method !== 'POST') throw new ApiError('method not allowed', 405);
|
|
158
111
|
const change = setupChange(await readJsonBody(req, SETUP_BODY_MAX));
|
|
159
|
-
|
|
112
|
+
applySetupChange(change, deps);
|
|
160
113
|
return claudeSetupStatus(setup);
|
|
161
114
|
}
|
|
@@ -11,8 +11,6 @@ import { claudeDir } from './files.js';
|
|
|
11
11
|
import { memoryJobStatus, type MemoryJob } from './memory-routine.js';
|
|
12
12
|
import { cliPackageDir, hooksDir, skillsDir } from './cli-files.js';
|
|
13
13
|
import { hasMetroGuard, withMetroHooks } from './hooks.js';
|
|
14
|
-
import { readModelConfig, routedConnection, type ModelConfig } from '../gateway/model-config.js';
|
|
15
|
-
import { harnessRunner, runnerModel, sdkAllowed, sdkOnLogin, type HarnessRunner } from './runner.js';
|
|
16
14
|
import { readSetupState, writeSetupState } from './setup-state.js';
|
|
17
15
|
|
|
18
16
|
export const PRIVACY_ENV: Record<string, string> = {
|
|
@@ -69,9 +67,6 @@ export interface SetupReport {
|
|
|
69
67
|
export interface SetupStatus {
|
|
70
68
|
privacy: boolean;
|
|
71
69
|
permissionMode: PermissionMode;
|
|
72
|
-
runner: HarnessRunner;
|
|
73
|
-
runnerAllowed: boolean;
|
|
74
|
-
sdkOnLogin: boolean;
|
|
75
70
|
systemPrompt: string;
|
|
76
71
|
liveEvents: boolean;
|
|
77
72
|
memoryRoutine: boolean;
|
|
@@ -218,16 +213,23 @@ interface SettingsApplied {
|
|
|
218
213
|
guard: boolean;
|
|
219
214
|
}
|
|
220
215
|
|
|
221
|
-
|
|
216
|
+
const MODELS_KEYS = new Set(['availableModels', 'enforceAvailableModels']);
|
|
217
|
+
|
|
218
|
+
const withoutMetroModels = (settings: Record<string, unknown>, metroWrote: boolean): Record<string, unknown> =>
|
|
219
|
+
metroWrote ? Object.fromEntries(Object.entries(settings).filter(([key]) => !MODELS_KEYS.has(key))) : settings;
|
|
220
|
+
|
|
221
|
+
function applySettings(dir: string, agents: string, privacy: boolean, hooks: string | null): SettingsApplied {
|
|
222
|
+
const metroModels = readSetupState(agents).modelsByMetro === true;
|
|
222
223
|
let guard = false;
|
|
223
224
|
const settings = mergeSettings(dir, (current) => {
|
|
224
|
-
const next = withPrivacy(current, privacy);
|
|
225
|
+
const next = withPrivacy(withoutMetroModels(current, metroModels), privacy);
|
|
225
226
|
const hooked = hooks === null ? null : withMetroHooks(next, hooks);
|
|
226
227
|
if (hooks !== null && hooked === null)
|
|
227
228
|
log.warn({ path: join(dir, 'settings.json') }, 'claude-setup: the hooks in settings.json are not in the shape Claude Code reads, so metro did not add its own');
|
|
228
229
|
guard = hasMetroGuard(hooked ?? next);
|
|
229
230
|
return hooked ?? next;
|
|
230
231
|
});
|
|
232
|
+
if (metroModels && settings !== 'unreadable') writeSetupState(agents, { modelsByMetro: false });
|
|
231
233
|
return { settings, guard };
|
|
232
234
|
}
|
|
233
235
|
|
|
@@ -280,9 +282,8 @@ export function ensureClaudeSetup(deps: SetupDeps = {}): SetupReport {
|
|
|
280
282
|
const skill = placeSkill(dir, skills, METRO_SKILL);
|
|
281
283
|
const stage = placeSkill(dir, skills, STAGE_SKILL);
|
|
282
284
|
const memory = placeSkill(dir, skills, MEMORY_SKILL);
|
|
283
|
-
const applied = applySettings(dir, privacy, hooks);
|
|
285
|
+
const applied = applySettings(dir, agents, privacy, hooks);
|
|
284
286
|
const report: SetupReport = { privacy, guard: applied.guard, worker, skill, stage, memory, settings: applied.settings };
|
|
285
|
-
syncAvailableModelsQuietly({ ...deps, dir, agents });
|
|
286
287
|
if ([report.worker, report.skill, report.stage, report.memory].some(written) || report.settings === 'written')
|
|
287
288
|
log.info(report, 'claude-setup: applied the Claude Code setup for a metro box');
|
|
288
289
|
if (report.settings === 'unreadable') log.warn({ path: join(dir, 'settings.json') }, 'claude-setup: settings.json is not valid JSON, so the privacy settings and metro hooks were not written');
|
|
@@ -299,9 +300,6 @@ export function claudeSetupStatus(deps: SetupDeps = {}): SetupStatus {
|
|
|
299
300
|
return {
|
|
300
301
|
privacy: privacyEnabled(agents),
|
|
301
302
|
permissionMode: permissionMode(agents),
|
|
302
|
-
runner: harnessRunner(agents),
|
|
303
|
-
runnerAllowed: sdkAllowed(agents),
|
|
304
|
-
sdkOnLogin: sdkOnLogin(agents),
|
|
305
303
|
systemPrompt: systemPrompt(agents),
|
|
306
304
|
liveEvents: liveEvents(agents),
|
|
307
305
|
memoryRoutine: memoryRoutine(agents),
|
|
@@ -323,33 +321,3 @@ export function tryClaudeSetup(deps: SetupDeps = {}): void {
|
|
|
323
321
|
log.warn({ err: errMsg(err) }, 'claude-setup: could not apply the Claude Code setup');
|
|
324
322
|
}
|
|
325
323
|
}
|
|
326
|
-
|
|
327
|
-
const MODELS_KEY = 'availableModels';
|
|
328
|
-
const ENFORCE_KEY = 'enforceAvailableModels';
|
|
329
|
-
|
|
330
|
-
export const routeOf = (cfg: ModelConfig): string | null => (routedConnection(cfg)?.provider === 'anthropic' ? null : runnerModel(cfg));
|
|
331
|
-
|
|
332
|
-
function withAvailableModels(settings: Record<string, unknown>, route: string | null, metroWrote: boolean): Record<string, unknown> {
|
|
333
|
-
if (route !== null) return { ...settings, [MODELS_KEY]: [route], [ENFORCE_KEY]: true };
|
|
334
|
-
if (!metroWrote) return settings;
|
|
335
|
-
return Object.fromEntries(Object.entries(settings).filter(([key]) => key !== MODELS_KEY && key !== ENFORCE_KEY));
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
export function syncAvailableModels(cfg: ModelConfig, deps: SetupDeps = {}): SettingsOutcome {
|
|
339
|
-
const dir = deps.dir ?? claudeDir();
|
|
340
|
-
const agents = deps.agents ?? agentsDir();
|
|
341
|
-
const route = routeOf(cfg);
|
|
342
|
-
const metroWrote = readSetupState(agents).modelsByMetro === true;
|
|
343
|
-
return mergeSettings(dir, (current) => {
|
|
344
|
-
writeSetupState(agents, { modelsByMetro: route !== null });
|
|
345
|
-
return withAvailableModels(current, route, metroWrote);
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
export function syncAvailableModelsQuietly(deps: SetupDeps = {}, cfg?: ModelConfig): void {
|
|
350
|
-
try {
|
|
351
|
-
syncAvailableModels(cfg ?? readModelConfig(deps.agents ?? agentsDir()), deps);
|
|
352
|
-
} catch (err) {
|
|
353
|
-
log.warn({ err: errMsg(err) }, 'claude-setup: could not sync the model allowlist');
|
|
354
|
-
}
|
|
355
|
-
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
3
3
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
4
|
-
import { agentCommand, agentUser, agentUserExpected, claudeBin } from '../agent-user/user.js';
|
|
4
|
+
import { agentCommand, agentUser, agentUserExpected, CLAUDE_ENV, claudeBin } from '../agent-user/user.js';
|
|
5
5
|
import { runningAsRoot } from '../metro-user/privilege.js';
|
|
6
6
|
import { PRIVACY_ENV } from './setup.js';
|
|
7
7
|
|
|
@@ -24,7 +24,7 @@ const ARGS = [
|
|
|
24
24
|
const BLOCKS_USAGE = 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC';
|
|
25
25
|
const PROBE_ENV: Record<string, string> = {
|
|
26
26
|
...Object.fromEntries(Object.entries(PRIVACY_ENV).filter(([name]) => name !== BLOCKS_USAGE)),
|
|
27
|
-
|
|
27
|
+
...CLAUDE_ENV,
|
|
28
28
|
DISABLE_BUG_COMMAND: '1',
|
|
29
29
|
ENABLE_CLAUDEAI_MCP_SERVERS: 'false',
|
|
30
30
|
};
|
|
@@ -21,19 +21,17 @@ export interface Enrollment {
|
|
|
21
21
|
export interface AgentKey {
|
|
22
22
|
keyId: string;
|
|
23
23
|
signingKey: string;
|
|
24
|
-
sealingKey: string;
|
|
25
24
|
signing: KeyObject;
|
|
26
|
-
sealing: KeyObject;
|
|
27
25
|
enrollment: Enrollment | null;
|
|
28
26
|
}
|
|
29
27
|
|
|
30
28
|
const pathOf = (dir: string): string => join(dir, FILE);
|
|
31
29
|
|
|
32
|
-
function
|
|
30
|
+
function privateSigningKey(raw: unknown): KeyObject | null {
|
|
33
31
|
if (typeof raw !== 'string' || raw === '') return null;
|
|
34
32
|
try {
|
|
35
33
|
const key = createPrivateKey({ key: Buffer.from(raw, 'base64'), format: 'der', type: 'pkcs8' });
|
|
36
|
-
return key.asymmetricKeyType ===
|
|
34
|
+
return key.asymmetricKeyType === 'ed25519' ? key : null;
|
|
37
35
|
} catch {
|
|
38
36
|
return null;
|
|
39
37
|
}
|
|
@@ -45,9 +43,9 @@ function enrollmentOf(raw: unknown): Enrollment | null {
|
|
|
45
43
|
return server !== null && isOrganizationId(raw.organization) ? { server, organization: raw.organization, at: raw.at } : null;
|
|
46
44
|
}
|
|
47
45
|
|
|
48
|
-
function agentKeyOf(signing: KeyObject,
|
|
46
|
+
function agentKeyOf(signing: KeyObject, enrollment: Enrollment | null): AgentKey {
|
|
49
47
|
const signingKey = rawPublicKey(createPublicKey(signing));
|
|
50
|
-
return { keyId: agentKeyId(signingKey), signingKey,
|
|
48
|
+
return { keyId: agentKeyId(signingKey), signingKey, signing, enrollment };
|
|
51
49
|
}
|
|
52
50
|
|
|
53
51
|
const unreadable = (): null => {
|
|
@@ -67,10 +65,9 @@ function storedOf(path: string): Record<string, unknown> | null {
|
|
|
67
65
|
|
|
68
66
|
function keyOf(stored: Record<string, unknown> | null): AgentKey | null {
|
|
69
67
|
if (stored === null) return null;
|
|
70
|
-
const signing =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return agentKeyOf(signing, sealing, enrollmentOf(stored.enrollment));
|
|
68
|
+
const signing = privateSigningKey(stored.signing);
|
|
69
|
+
if (stored.version !== 1 || signing === null) return unreadable();
|
|
70
|
+
return agentKeyOf(signing, enrollmentOf(stored.enrollment));
|
|
74
71
|
}
|
|
75
72
|
|
|
76
73
|
export function readAgentKey(dir = agentsDir()): AgentKey | null {
|
|
@@ -132,12 +129,12 @@ export function clearPendingAgentKey(dir: string): void {
|
|
|
132
129
|
}
|
|
133
130
|
|
|
134
131
|
export function newAgentKey(): AgentKey {
|
|
135
|
-
return agentKeyOf(generateKeyPairSync('ed25519').privateKey,
|
|
132
|
+
return agentKeyOf(generateKeyPairSync('ed25519').privateKey, null);
|
|
136
133
|
}
|
|
137
134
|
|
|
138
135
|
const exported = (key: KeyObject): string => key.export({ format: 'der', type: 'pkcs8' }).toString('base64');
|
|
139
136
|
|
|
140
|
-
const privateKeys = (key: AgentKey): Record<string, unknown> => ({ version: 1, signing: exported(key.signing)
|
|
137
|
+
const privateKeys = (key: AgentKey): Record<string, unknown> => ({ version: 1, signing: exported(key.signing) });
|
|
141
138
|
|
|
142
139
|
export function savePendingAgentKey(key: AgentKey, owner: string, ticketHash: string, dir: string, expectedServer?: string): void {
|
|
143
140
|
writeKey(pendingPath(dir), { ...privateKeys(key), owner, ticketHash, ...(expectedServer === undefined ? {} : { expectedServer }) });
|
|
@@ -66,7 +66,7 @@ async function exchange(ticket: string, owner: string, dir: string, deps: Enroll
|
|
|
66
66
|
if (finished !== null) return finished;
|
|
67
67
|
const key = pending?.ticketHash === ticketHash ? pending : newAgentKey();
|
|
68
68
|
if (key !== pending) savePendingAgentKey(key, owner, ticketHash, dir);
|
|
69
|
-
const enrolled = enrolledOf(await agentCall(api, key, '/api/agents/enroll', { ticket, organization: owner, signingKey: key.signingKey
|
|
69
|
+
const enrolled = enrolledOf(await agentCall(api, key, '/api/agents/enroll', { ticket, organization: owner, signingKey: key.signingKey }), key, owner);
|
|
70
70
|
sameOwner(deps, owner);
|
|
71
71
|
savePendingAgentKey(key, owner, ticketHash, dir, enrolled.server);
|
|
72
72
|
const confirmed = enrolledOf(await agentCall(api, key, '/api/agents/session'), key, owner);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { lstatSync, readdirSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { errMsg, log } from '@metro-labs/core/log';
|
|
4
|
+
import { agentsDir } from '../agents/files.js';
|
|
5
|
+
|
|
6
|
+
const WRITTEN = new Set([
|
|
7
|
+
'connectors.json',
|
|
8
|
+
'connector-migrations.json',
|
|
9
|
+
'connector-migrations.initialized',
|
|
10
|
+
'connector-server-keys.json',
|
|
11
|
+
'connector-server-keys.initialized',
|
|
12
|
+
'org-connector-sync.json',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const OPERATIONS = 'connector-migration-operations-[A-Za-z0-9][A-Za-z0-9_-]{10}\\.json';
|
|
16
|
+
|
|
17
|
+
const PATTERNS = [
|
|
18
|
+
new RegExp(`^${OPERATIONS}(?:\\.initialized)?$`),
|
|
19
|
+
new RegExp(`^(?:connectors\\.json|connector-server-keys\\.json|connector-server-keys\\.initialized|org-connector-sync\\.json|${OPERATIONS})\\.tmp-[0-9]+$`),
|
|
20
|
+
/^connector-migrations\.json\.[0-9a-f]{24}\.tmp$/,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const isConnectorLeftover =(name: string): boolean => WRITTEN.has(name) || PATTERNS.some((pattern) => pattern.test(name));
|
|
24
|
+
|
|
25
|
+
const codeOf = (err: unknown): unknown => (err instanceof Error && 'code' in err ? err.code : undefined);
|
|
26
|
+
|
|
27
|
+
function namesIn(dir: string): string[] {
|
|
28
|
+
try {
|
|
29
|
+
return readdirSync(dir).filter(isConnectorLeftover);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if (codeOf(err) !== 'ENOENT') log.warn({ err: errMsg(err), dir }, 'connectors: could not list the old connector files');
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function removeOne(path: string): boolean {
|
|
37
|
+
try {
|
|
38
|
+
if (!lstatSync(path).isFile()) {
|
|
39
|
+
log.warn({ path }, 'connectors: left an old connector file alone, it is not a regular file');
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
unlinkSync(path);
|
|
43
|
+
return true;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (codeOf(err) !== 'ENOENT') log.warn({ err: errMsg(err), path }, 'connectors: could not remove an old connector file');
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function removeConnectorLeftovers(dir = agentsDir()): number {
|
|
51
|
+
try {
|
|
52
|
+
const removed = namesIn(dir).filter((name) => removeOne(join(dir, name))).length;
|
|
53
|
+
log.info({ removed }, 'connectors: removed the files of the old connector path');
|
|
54
|
+
return removed;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
log.warn({ err: errMsg(err) }, 'connectors: could not remove the files of the old connector path');
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -2,7 +2,6 @@ import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from 'node:
|
|
|
2
2
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
3
3
|
import { cameThroughProxy } from '@metro-labs/http/api-http';
|
|
4
4
|
import { agentIdForKey } from '../agents/keys.js';
|
|
5
|
-
import { sdkAllowed, sdkConnectionAllowed, SDK_NEEDS_KEY } from '../claude/runner.js';
|
|
6
5
|
import {
|
|
7
6
|
assertBedrockReady,
|
|
8
7
|
bedrockBase,
|
|
@@ -98,11 +97,6 @@ const keyOf = (req: IncomingMessage): string => {
|
|
|
98
97
|
return (Array.isArray(raw) ? raw[0] : raw)?.trim() ?? '';
|
|
99
98
|
};
|
|
100
99
|
|
|
101
|
-
const fromSdkRunner = (req: IncomingMessage): boolean => {
|
|
102
|
-
const raw = req.headers['x-metro-runner'];
|
|
103
|
-
return (Array.isArray(raw) ? raw[0] : raw)?.trim() === 'sdk';
|
|
104
|
-
};
|
|
105
|
-
|
|
106
100
|
const defaultIdentify = (key: string): boolean => key !== '' && agentIdForKey(key) !== undefined;
|
|
107
101
|
|
|
108
102
|
function modelsBody(cfg: ModelConfig): Record<string, unknown> {
|
|
@@ -299,15 +293,9 @@ interface Request {
|
|
|
299
293
|
cfg: ModelConfig;
|
|
300
294
|
}
|
|
301
295
|
|
|
302
|
-
function checkSdkRoute(req: IncomingMessage, route: Route, deps: GatewayDeps): void {
|
|
303
|
-
if (fromSdkRunner(req) && (!sdkAllowed(undefined, deps.config()) || !sdkConnectionAllowed(route.connection)))
|
|
304
|
-
throw new GatewayError(403, 'permission_error', SDK_NEEDS_KEY);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
296
|
async function attempt(req: IncomingMessage, res: ServerResponse, ask: Request, route: Route, deps: GatewayDeps): Promise<void> {
|
|
308
297
|
const { path, body } = ask;
|
|
309
298
|
const conn = route.connection;
|
|
310
|
-
checkSdkRoute(req, route, deps);
|
|
311
299
|
log.info({ route: routeLabel(route), connection: conn.label, path }, 'gateway: routing');
|
|
312
300
|
if (path === MESSAGES) noteServed({ connection: conn.id, provider: conn.provider, model: route.model, at: new Date().toISOString() });
|
|
313
301
|
if (conn.provider === 'bedrock') {
|
|
@@ -391,6 +379,6 @@ export function handleGatewayRequest(req: IncomingMessage, res: ServerResponse,
|
|
|
391
379
|
}
|
|
392
380
|
if (cameThroughProxy(req)) sendError(res, 401, 'authentication_error', 'metro gateway: the agent key works only on this machine, not through its public address');
|
|
393
381
|
else if ((deps.identify ?? defaultIdentify)(keyOf(req))) answer(req, res, path, deps);
|
|
394
|
-
else sendError(res, 401, 'authentication_error', 'metro gateway: no agent key (x-metro-key); start
|
|
382
|
+
else sendError(res, 401, 'authentication_error', 'metro gateway: no agent key (x-metro-key); start the agent with metro agent');
|
|
395
383
|
return true;
|
|
396
384
|
}
|
|
@@ -5,9 +5,7 @@ import { isRecord } from '@metro-labs/core/is-record';
|
|
|
5
5
|
import { log } from '@metro-labs/core/log';
|
|
6
6
|
import { openrouterModels, openrouterZdrModels } from './openrouter.js';
|
|
7
7
|
import { anthropicModels, bedrockModels } from './provider-models.js';
|
|
8
|
-
import {
|
|
9
|
-
import { harnessRunner, runnerModel, sdkAllowed } from '../claude/runner.js';
|
|
10
|
-
import { sessionLive, sessionRunning, stopSession } from '../claude/session.js';
|
|
8
|
+
import { runnerAlive } from '../claude/session.js';
|
|
11
9
|
import { forgetOne, forgetReported } from './usage.js';
|
|
12
10
|
import {
|
|
13
11
|
addConnection,
|
|
@@ -16,6 +14,7 @@ import {
|
|
|
16
14
|
readModelConfig,
|
|
17
15
|
readModelSnapshot,
|
|
18
16
|
removeConnection,
|
|
17
|
+
routedConnection,
|
|
19
18
|
setFallbacks,
|
|
20
19
|
setRoute,
|
|
21
20
|
updateConnection,
|
|
@@ -45,40 +44,29 @@ const DEVICE_ID_RE = /^[A-Za-z0-9_-]{16,64}$/;
|
|
|
45
44
|
|
|
46
45
|
export type { ModelApiDeps } from './model-store.js';
|
|
47
46
|
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return
|
|
47
|
+
function runnerModel(cfg: ModelConfig): string | null {
|
|
48
|
+
const conn = routedConnection(cfg);
|
|
49
|
+
if (conn === null || conn.model === '') return null;
|
|
50
|
+
return conn.provider === 'anthropic' ? conn.model : `${conn.provider}:${conn.model}`;
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
function switchLive(before: ModelConfig, next: ModelConfig, deps: ModelApiDeps): void {
|
|
55
54
|
const [was, now] = [runnerModel(before), runnerModel(next)];
|
|
56
55
|
if (was === now) return;
|
|
57
56
|
if (deps.switchModel?.(now) === true) log.info({ was, now }, 'model-api: the model changed, so the Agent SDK session switches to it live, with no restart');
|
|
58
|
-
else if (
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function followModel(before: ModelConfig, next: ModelConfig, deps: ModelApiDeps): void {
|
|
62
|
-
const restart = deps.restartSession ?? restartRunningSession;
|
|
63
|
-
const agents = deps.setup?.agents;
|
|
64
|
-
if (harnessRunner(agents) === 'sdk') {
|
|
65
|
-
if (sdkAllowed(agents, next)) switchLive(before, next, deps);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
if (routeOf(next) !== routeOf(before) && restart())
|
|
69
|
-
log.info({ was: routeOf(before), now: routeOf(next) }, 'model-api: the model changed, so the Claude session restarts on it');
|
|
57
|
+
else if (runnerIsUp(deps)) throw new ApiError('The running Agent SDK could not be told about this model change. Nothing was saved. Let its work finish, stop it, then save and start it again.', 409);
|
|
58
|
+
else log.info({ was, now }, 'model-api: the model changed while no Agent SDK runner is up; its next start loads it');
|
|
70
59
|
}
|
|
71
60
|
|
|
72
61
|
function kept(store: Store, next: ModelConfig, deps: ModelApiDeps, note: string, fields: Record<string, unknown>): unknown {
|
|
73
62
|
const before = store.read();
|
|
74
63
|
store.write(next);
|
|
75
64
|
try {
|
|
76
|
-
|
|
65
|
+
switchLive(before, next, deps);
|
|
77
66
|
} catch (err) {
|
|
78
67
|
store.write(before);
|
|
79
68
|
throw err;
|
|
80
69
|
}
|
|
81
|
-
syncAvailableModelsQuietly(deps.setup ?? {}, next);
|
|
82
70
|
log.info(fields, note);
|
|
83
71
|
return settingsBody(next);
|
|
84
72
|
}
|
|
@@ -218,14 +206,12 @@ function routeFor(path: string, method: string | undefined): Route | number {
|
|
|
218
206
|
return codexRoute(path.slice(CODEX.length), method);
|
|
219
207
|
}
|
|
220
208
|
|
|
221
|
-
const
|
|
209
|
+
const runnerIsUp = (deps: ModelApiDeps): boolean => (deps.runnerAlive ?? (() => runnerAlive()))();
|
|
222
210
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
throw new ApiError('The Agent SDK session is running, and this change would leave a model or fallback it may not use. Let it finish its work, stop it, then save the change.', 409);
|
|
228
|
-
}
|
|
211
|
+
const storeFor = (path: string, deps: ModelApiDeps): Store => ({
|
|
212
|
+
read: deps.read ?? (path === SNAPSHOT ? readModelSnapshot : readModelConfig),
|
|
213
|
+
write: deps.write ?? writeModelConfig,
|
|
214
|
+
});
|
|
229
215
|
|
|
230
216
|
export function handleModelRequest(req: IncomingMessage, res: ServerResponse, deps: ModelApiDeps): boolean {
|
|
231
217
|
const path = (req.url ?? '').split('?')[0] ?? '';
|
|
@@ -234,13 +220,7 @@ export function handleModelRequest(req: IncomingMessage, res: ServerResponse, de
|
|
|
234
220
|
res.writeHead(204, cors(req)).end();
|
|
235
221
|
return true;
|
|
236
222
|
}
|
|
237
|
-
const store
|
|
238
|
-
read: deps.read ?? (path === SNAPSHOT ? readModelSnapshot : readModelConfig),
|
|
239
|
-
write: (cfg) => {
|
|
240
|
-
checkRunnerRoute(store.read(), cfg, deps);
|
|
241
|
-
(deps.write ?? writeModelConfig)(cfg);
|
|
242
|
-
},
|
|
243
|
-
};
|
|
223
|
+
const store = storeFor(path, deps);
|
|
244
224
|
if (handleOpenRouterSignIn(req, res, deps, store)) return true;
|
|
245
225
|
const route = routeFor(path, req.method);
|
|
246
226
|
if (typeof route === 'number') {
|
|
@@ -27,8 +27,7 @@ export interface ModelApiDeps {
|
|
|
27
27
|
anthropicBase?: string;
|
|
28
28
|
bedrockControlBase?: string;
|
|
29
29
|
setup?: SetupDeps;
|
|
30
|
-
|
|
31
|
-
sessionRunning?: () => boolean;
|
|
30
|
+
runnerAlive?: () => boolean;
|
|
32
31
|
switchModel?: (model: string | null) => boolean;
|
|
33
32
|
claudeUsage?: (env?: Record<string, string>) => Promise<unknown>;
|
|
34
33
|
claudeLoginsRoot?: string;
|
|
@@ -2,7 +2,6 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
2
2
|
import {
|
|
3
3
|
BodyTooLargeError,
|
|
4
4
|
readBody as readBodyBuffer,
|
|
5
|
-
VOICE_MCP_PATH,
|
|
6
5
|
} from '../routes/http.js';
|
|
7
6
|
import {
|
|
8
7
|
authenticate,
|
|
@@ -97,9 +96,7 @@ async function serveGet(
|
|
|
97
96
|
if (served) session.replayMissed();
|
|
98
97
|
}
|
|
99
98
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
let activeSlots: SessionSlot[] = [];
|
|
99
|
+
let activeSlot: SessionSlot | undefined;
|
|
103
100
|
let stopToolNotices: (() => void) | undefined;
|
|
104
101
|
|
|
105
102
|
const MODEL_NOTICE = 'notifications/metro/model';
|
|
@@ -118,13 +115,12 @@ export async function createMetroMcp(options: { liveEvents?: boolean } = {}): Pr
|
|
|
118
115
|
setLiveEvents: (on: boolean) => void;
|
|
119
116
|
switchModel: (model: string | null) => boolean;
|
|
120
117
|
}> {
|
|
121
|
-
await
|
|
118
|
+
await activeSlot?.close();
|
|
122
119
|
const slot = new SessionSlot(options.liveEvents);
|
|
123
|
-
|
|
124
|
-
activeSlots = [slot, voiceSlot];
|
|
120
|
+
activeSlot = slot;
|
|
125
121
|
stopToolNotices?.();
|
|
126
122
|
stopToolNotices = onConnectorToolsChanged(() => {
|
|
127
|
-
|
|
123
|
+
slot.current?.toolsChanged();
|
|
128
124
|
});
|
|
129
125
|
connectCallBridge(() => slot.current);
|
|
130
126
|
connectServerApprovals(() => slot.current);
|
|
@@ -142,13 +138,7 @@ export async function createMetroMcp(options: { liveEvents?: boolean } = {}): Pr
|
|
|
142
138
|
const parsed = await readOrReject(req, res);
|
|
143
139
|
if (!parsed.ok) return;
|
|
144
140
|
await runWithIdentity(identity, async () => {
|
|
145
|
-
const session = await resolveSession(
|
|
146
|
-
isVoicePath(req) ? voiceSlot : slot,
|
|
147
|
-
req,
|
|
148
|
-
parsed.body,
|
|
149
|
-
identity,
|
|
150
|
-
res,
|
|
151
|
-
);
|
|
141
|
+
const session = await resolveSession(slot, req, parsed.body, identity, res);
|
|
152
142
|
if (!session) return;
|
|
153
143
|
if (isStandaloneGet(req)) {
|
|
154
144
|
await serveGet(session, req, res);
|
|
@@ -19,14 +19,3 @@ export const MCP_INSTRUCTIONS =
|
|
|
19
19
|
'prompts are relayed to the chat; the owner answers "yes <id>"/"no <id>". Connectors ' +
|
|
20
20
|
'(third-party tools like Notion, n8n, Gmail) are reached only through `tool_search` then ' +
|
|
21
21
|
'`tool_call`, from a worker.';
|
|
22
|
-
|
|
23
|
-
export const VOICE_MCP_INSTRUCTIONS =
|
|
24
|
-
"Metro's chat tools, opened for a live voice call. The caller hears what you say, so do not " +
|
|
25
|
-
'answer them with `send` or `reply` and do not post the call in a chat; post, react or act on ' +
|
|
26
|
-
'a channel only when the caller asks for it, once. `read` with a `line` (passed verbatim) ' +
|
|
27
|
-
'shows that chat; `group_info` and `list_members` show who is in it; `get_profile` with a ' +
|
|
28
|
-
'`from` names a sender. The owner may block a tool on an account or make it need approval ' +
|
|
29
|
-
'(`list_accounts` shows each `policy`). A blocked call errors. A call needing approval must ' +
|
|
30
|
-
'run in a background worker: on the main thread it is refused, so delegate that exact call ' +
|
|
31
|
-
'and keep talking; the worker waits while the owner is asked in the chat of the call, where ' +
|
|
32
|
-
'they answer "yes <id>"/"no <id>". Connectors are reached through `tool_search` then `tool_call`.';
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { currentBusSeq } from '@metro-labs/core/events';
|
|
3
3
|
import { errMsg } from '@metro-labs/core/log';
|
|
4
4
|
import { newReplayLedger } from '../channels/relay.js';
|
|
5
|
-
import { McpSession, channelLog
|
|
5
|
+
import { McpSession, channelLog } from './session.js';
|
|
6
6
|
import { allowedAgents, type RequestIdentity } from './request-identity.js';
|
|
7
7
|
import { diagnosticTag, type SessionDiagnostic, type SlotDiagnostic } from './diagnostics.js';
|
|
8
8
|
|
|
@@ -25,10 +25,7 @@ export class SessionSlot {
|
|
|
25
25
|
private changing: Promise<void> = Promise.resolve();
|
|
26
26
|
private readonly replacements: SlotReplacement[] = [];
|
|
27
27
|
|
|
28
|
-
constructor(
|
|
29
|
-
live = true,
|
|
30
|
-
private readonly kind: SessionKind = 'chat',
|
|
31
|
-
) {
|
|
28
|
+
constructor(live = true) {
|
|
32
29
|
this.live = live;
|
|
33
30
|
}
|
|
34
31
|
|
|
@@ -111,7 +108,6 @@ export class SessionSlot {
|
|
|
111
108
|
if (this.ledger.startAt < 0) this.ledger.startAt = currentBusSeq();
|
|
112
109
|
const id = adoptId ?? randomUUID();
|
|
113
110
|
const session = await McpSession.open({
|
|
114
|
-
kind: this.kind,
|
|
115
111
|
id,
|
|
116
112
|
scope: allowedAgents(identity),
|
|
117
113
|
adopted: adoptId !== undefined,
|
|
@@ -129,7 +125,7 @@ export class SessionSlot {
|
|
|
129
125
|
this.session = session;
|
|
130
126
|
this.recordReplacement(session, adoptId, previous);
|
|
131
127
|
if (this.started && this.live) session.startChannel();
|
|
132
|
-
channelLog('session: opened', 'id', id, '
|
|
128
|
+
channelLog('session: opened', 'id', id, 'adopted', adoptId !== undefined);
|
|
133
129
|
return session;
|
|
134
130
|
}
|
|
135
131
|
|