@stage-labs/metro 0.1.0-beta.45 → 0.1.0-beta.46
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/api.js +5 -5
- package/dist/bedrock-proxy.js +1 -1
- package/dist/cli.js +6 -6
- package/dist/plugin.js +1 -1
- package/dist/prompt.js +1 -1
- package/dist/runtime.js +1 -1
- package/dist/serve.js +1 -1
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +0 -1
- package/runtime/node_modules/@metro-labs/mcp/package.json +1 -5
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-import.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +2 -5
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +5 -23
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-files.ts +1 -3
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +2 -4
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +2 -7
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +3 -5
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +1 -4
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +1 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +4 -83
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/is-record.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-cli-api.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +1 -37
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +1 -4
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +1 -4
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +0 -33
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +3 -3
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-config.ts +1 -6
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +1 -42
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-auth.ts +2 -11
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +8 -90
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/version.ts +1 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +1 -4
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +6 -4
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +1 -3
- package/runtime/node_modules/@metro-labs/mcp/src/db/local-connectors.ts +11 -22
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +3 -13
- package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +3 -3
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +2 -4
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +1 -21
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +1 -1
- package/runtime/node_modules/@metro-labs/telegram/package.json +0 -2
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +0 -1
- package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +0 -1
- package/runtime/node_modules/@metro-labs/webhook/src/station.ts +2 -5
- package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +0 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +0 -1
- package/runtime/runtime.json +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +0 -45
package/dist/api.js
CHANGED
|
@@ -103,10 +103,10 @@ export async function claimCode(code) {
|
|
|
103
103
|
const body = await post('/api/cli/claim', { code });
|
|
104
104
|
if (!isRecord(body) ||
|
|
105
105
|
typeof body.token !== 'string' ||
|
|
106
|
-
typeof body.
|
|
106
|
+
typeof body.subject !== 'string' ||
|
|
107
107
|
typeof body.agent !== 'string')
|
|
108
108
|
throw new Error('metro returned an unexpected response');
|
|
109
|
-
return { token: body.token,
|
|
109
|
+
return { token: body.token, subject: body.subject, agent: body.agent };
|
|
110
110
|
}
|
|
111
111
|
export async function claimRuntime(code, label) {
|
|
112
112
|
const body = await post('/api/run/claim', { code, label });
|
|
@@ -133,7 +133,7 @@ export async function whoisAuthorized(wanted) {
|
|
|
133
133
|
const agents = localAgents();
|
|
134
134
|
if (agents.length > 0) {
|
|
135
135
|
const agent = pickLocalAgent(agents, wanted);
|
|
136
|
-
return {
|
|
136
|
+
return { subject: 'this machine', agent: agent.name, where: `${localUrl()} (local)` };
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
return { ...(await hostedIdentity()), where: metroUrl() };
|
|
@@ -141,8 +141,8 @@ export async function whoisAuthorized(wanted) {
|
|
|
141
141
|
async function hostedIdentity() {
|
|
142
142
|
const body = await get('/api/cli/session');
|
|
143
143
|
if (!isRecord(body) ||
|
|
144
|
-
typeof body.
|
|
144
|
+
typeof body.subject !== 'string' ||
|
|
145
145
|
typeof body.agent !== 'string')
|
|
146
146
|
throw new Error('metro returned an unexpected response');
|
|
147
|
-
return {
|
|
147
|
+
return { subject: body.subject, agent: body.agent };
|
|
148
148
|
}
|
package/dist/bedrock-proxy.js
CHANGED
|
@@ -3,7 +3,7 @@ import { EventStreamDecoder } from './bedrock-eventstream.js';
|
|
|
3
3
|
import { callBedrock, freshAdaptations, invoke, invokeUrl, upstreamMessage, } from './bedrock-upstream.js';
|
|
4
4
|
export class BedrockConfigError extends Error {
|
|
5
5
|
}
|
|
6
|
-
|
|
6
|
+
class ProxyError extends Error {
|
|
7
7
|
status;
|
|
8
8
|
kind;
|
|
9
9
|
constructor(status, kind, message) {
|
package/dist/cli.js
CHANGED
|
@@ -65,7 +65,7 @@ function hostLabel() {
|
|
|
65
65
|
return name === '' ? 'unnamed machine' : name;
|
|
66
66
|
}
|
|
67
67
|
async function authorizeRuntime(agentId) {
|
|
68
|
-
process.stderr.write(`Authorize this machine
|
|
68
|
+
process.stderr.write(`Authorize this machine with a pairing code for agent ${agentId} from ${metroWebUrl()}\n`);
|
|
69
69
|
const code = (await askSecret('Paste the code (input is hidden): ')).trim();
|
|
70
70
|
if (code === '')
|
|
71
71
|
throw new Error('no code given');
|
|
@@ -134,20 +134,20 @@ function logs(argv) {
|
|
|
134
134
|
async function pastedCode(given) {
|
|
135
135
|
if (given !== undefined && given.trim() !== '')
|
|
136
136
|
return given.trim();
|
|
137
|
-
process.stderr.write(`Get a pairing code
|
|
137
|
+
process.stderr.write(`Get a pairing code for the agent from ${metroWebUrl()}\n`);
|
|
138
138
|
return askSecret('Paste the code here (input is hidden): ');
|
|
139
139
|
}
|
|
140
140
|
async function login(given) {
|
|
141
|
-
const { token,
|
|
141
|
+
const { token, subject, agent } = await claimCode(await pastedCode(given));
|
|
142
142
|
writeToken(token);
|
|
143
|
-
process.stderr.write(`Authorized '${agent}' for ${
|
|
143
|
+
process.stderr.write(`Authorized '${agent}' for ${subject}. Stored in ${credentialsPath()}\n`);
|
|
144
144
|
if (syncPluginServers())
|
|
145
145
|
process.stderr.write('Claude Code plugin refreshed — new sessions have the connectors; ' +
|
|
146
146
|
'run /reload-plugins in any session already open.\n');
|
|
147
147
|
}
|
|
148
148
|
async function whoami(wanted) {
|
|
149
|
-
const {
|
|
150
|
-
process.stdout.write(`${
|
|
149
|
+
const { subject, agent, where } = await whoisAuthorized(wanted);
|
|
150
|
+
process.stdout.write(`${subject} · agent '${agent}' on ${where}\n`);
|
|
151
151
|
}
|
|
152
152
|
const HELP = new Set([undefined, 'help', '--help', '-h']);
|
|
153
153
|
const COMMANDS = {
|
package/dist/plugin.js
CHANGED
|
@@ -76,7 +76,7 @@ export async function installPlugin() {
|
|
|
76
76
|
'run /reload-plugins in any session already open.\n');
|
|
77
77
|
return 0;
|
|
78
78
|
}
|
|
79
|
-
process.stderr.write('Now sign in: metro login (
|
|
79
|
+
process.stderr.write('Now sign in: metro login (with a pairing code from https://metro.box), ' +
|
|
80
80
|
'or run /metro:login <code> inside Claude Code.\n');
|
|
81
81
|
return Promise.resolve(0);
|
|
82
82
|
}
|
package/dist/prompt.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline';
|
|
2
2
|
const CTRL_C = '\u0003';
|
|
3
3
|
const BACKSPACE = '\u007f';
|
|
4
|
-
|
|
4
|
+
function askLine(question) {
|
|
5
5
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
6
6
|
return new Promise((resolve) => {
|
|
7
7
|
rl.question(question, (answer) => {
|
package/dist/runtime.js
CHANGED
|
@@ -18,7 +18,7 @@ export function runtimeDir() {
|
|
|
18
18
|
throw new MissingRuntime(`no metro daemon at ${dir}. Reinstall with: npm i -g @stage-labs/metro@beta`);
|
|
19
19
|
return dir;
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
class MissingBun extends Error {
|
|
22
22
|
}
|
|
23
23
|
export function assertAgentId(id) {
|
|
24
24
|
if (id === undefined || !AGENT_RE.test(id))
|
package/dist/serve.js
CHANGED
|
@@ -34,7 +34,7 @@ export function parseServeArgs(argv) {
|
|
|
34
34
|
}
|
|
35
35
|
return { port, tunnel };
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
function findCloudflared() {
|
|
38
38
|
const found = spawnSync('cloudflared', ['--version'], { stdio: 'ignore' });
|
|
39
39
|
if (found.error !== undefined || found.status !== 0)
|
|
40
40
|
throw new Error('metro serve --tunnel needs cloudflared, which is not on PATH.\n' +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stage-labs/metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.46",
|
|
4
4
|
"description": "The metro command line. Sign in once per machine, then hand your MCP connector list to Claude Code without the credentials touching disk, argv or shell history.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -22,7 +22,6 @@ export interface AccountConfig {
|
|
|
22
22
|
export const { loadAccounts } = makeAccountStore<AccountConfig>({
|
|
23
23
|
prefix: 'discord-bot',
|
|
24
24
|
file: ACCOUNTS_FILE,
|
|
25
|
-
allowlistEnv: ['DISCORD_BOT_ONLY_ACCOUNTS', 'DISCORD_BOT_ACCOUNTS'],
|
|
26
25
|
validate(raw, die) {
|
|
27
26
|
const seen = new Set<string>();
|
|
28
27
|
for (const a of raw) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@metro-labs/mcp",
|
|
3
3
|
"version": "0.1.0-beta.15",
|
|
4
4
|
"private": true,
|
|
5
|
-
"description": "Event-interception wire. Supervises train subprocesses in ~/.metro/trains/, multiplexes their JSON event stream onto stdout, and routes outbound action calls back via stdin. Per-platform code is written by the agent (or user) as train scripts
|
|
5
|
+
"description": "Event-interception wire. Supervises train subprocesses in ~/.metro/trains/, multiplexes their JSON event stream onto stdout, and routes outbound action calls back via stdin. Per-platform code is written by the agent (or user) as train scripts \u2014 metro core is pure transport.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -14,12 +14,8 @@
|
|
|
14
14
|
"engines": {
|
|
15
15
|
"node": ">=22"
|
|
16
16
|
},
|
|
17
|
-
"bin": {
|
|
18
|
-
"metro-daemon": "./dist/server.js"
|
|
19
|
-
},
|
|
20
17
|
"exports": {
|
|
21
18
|
".": "./src/mcp/index.ts",
|
|
22
|
-
"./server": "./dist/server.js",
|
|
23
19
|
"./log": "./src/daemon/log.ts",
|
|
24
20
|
"./train-error": "./src/daemon/train-error.ts",
|
|
25
21
|
"./secure-fs": "./src/daemon/secure-fs.ts",
|
|
@@ -9,7 +9,7 @@ import type { LoadedAgent } from '../db/materialize.js';
|
|
|
9
9
|
const CLAIM_TIMEOUT_MS = 15_000;
|
|
10
10
|
const CODE_RE = /^ma_[A-Za-z0-9_-]{16}$/;
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
function metroBaseUrl(): string {
|
|
13
13
|
const raw = (process.env.METRO_URL?.trim() ?? '').replace(/\/+$/, '');
|
|
14
14
|
const url = raw === '' ? 'https://mcp.metro.box' : raw;
|
|
15
15
|
if (!carriesSecretsSafely(url))
|
|
@@ -53,7 +53,7 @@ async function claim(base: string, code: string, label: string): Promise<string>
|
|
|
53
53
|
return token;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
function storeRunToken(agentId: string, token: string, url: string): string {
|
|
57
57
|
const dir = configDir();
|
|
58
58
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
59
59
|
const path = join(dir, `runtime-${agentId}.json`);
|
|
@@ -34,7 +34,7 @@ function parseGrant(raw: string): AttachmentGrant | undefined {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
function readGrant(path: string): AttachmentGrant | undefined {
|
|
38
38
|
try {
|
|
39
39
|
return parseGrant(readFileSync(`${path}${GRANT_SUFFIX}`, 'utf8'));
|
|
40
40
|
} catch {
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { attachmentOwner, recordAttachmentOwner } from './attach-owner.js';
|
|
15
15
|
import { grantAllows, issueAttachmentGrant } from './attach-grant.js';
|
|
16
16
|
import { errMsg, log } from './log.js';
|
|
17
|
-
import {
|
|
17
|
+
import { currentTunnelUrl, webhookPort } from './tunnel.js';
|
|
18
18
|
import { isLocalMode } from './paths.js';
|
|
19
19
|
|
|
20
20
|
function authorized(req: IncomingMessage, name: string): boolean {
|
|
@@ -31,10 +31,7 @@ const DEFAULT_PUBLIC_BASE = 'https://mcp.metro.box';
|
|
|
31
31
|
export function publicBaseUrl(): string | null {
|
|
32
32
|
const env = process.env.METRO_PUBLIC_URL?.trim();
|
|
33
33
|
if (env) return env.replace(/\/+$/, '');
|
|
34
|
-
|
|
35
|
-
if (live !== null) return live;
|
|
36
|
-
const host = configuredTunnelHost();
|
|
37
|
-
return host ? `https://${host}` : null;
|
|
34
|
+
return currentTunnelUrl();
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
const servesLocally = (): boolean =>
|
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
import type { ProjectApiDeps } from './project-api.js';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { type Server } from 'node:http';
|
|
4
|
-
import {
|
|
4
|
+
import { userSelf } from './events.js';
|
|
5
5
|
import { setTrainCallBackend } from './train-call.js';
|
|
6
6
|
import { errMsg, log, logFatalSync } from './log.js';
|
|
7
|
-
import {
|
|
8
|
-
acquireLock,
|
|
9
|
-
isLocalMode,
|
|
10
|
-
loadMetroEnv,
|
|
11
|
-
STATE_DIR,
|
|
12
|
-
trainsDir,
|
|
13
|
-
} from './paths.js';
|
|
7
|
+
import { acquireLock, isLocalMode, STATE_DIR, trainsDir } from './paths.js';
|
|
14
8
|
import { installCrashGuard, markDaemonReady } from './crash-guard.js';
|
|
15
|
-
import {
|
|
16
|
-
loadTunnelConfig,
|
|
17
|
-
Tunnel,
|
|
18
|
-
tunnelConfigFromEnv,
|
|
19
|
-
warnOnLegacyWebhooks,
|
|
20
|
-
webhookPort,
|
|
21
|
-
} from './tunnel.js';
|
|
9
|
+
import { quickTunnelWanted, Tunnel, webhookPort } from './tunnel.js';
|
|
22
10
|
import {
|
|
23
11
|
localConnectHint,
|
|
24
12
|
publicConnectHint,
|
|
@@ -115,11 +103,9 @@ import type { AgentApiDeps } from './agent-api.js';
|
|
|
115
103
|
import type { ConnectorApiDeps } from './connector-api.js';
|
|
116
104
|
|
|
117
105
|
installCrashGuard();
|
|
118
|
-
loadMetroEnv();
|
|
119
106
|
acquireLock(join(STATE_DIR, '.tail-lock'));
|
|
120
107
|
|
|
121
|
-
|
|
122
|
-
log.info({ self, line: selfLine() }, 'user identity');
|
|
108
|
+
log.info({ self: userSelf() }, 'user identity');
|
|
123
109
|
|
|
124
110
|
process.stdout.on('error', (err) => {
|
|
125
111
|
if ((err as NodeJS.ErrnoException).code !== 'EPIPE')
|
|
@@ -135,8 +121,7 @@ supervisor.onTrainEvent((env, train) => {
|
|
|
135
121
|
});
|
|
136
122
|
|
|
137
123
|
let webhookServer: Server | null = null;
|
|
138
|
-
const
|
|
139
|
-
const tunnel = tunnelCfg ? new Tunnel(tunnelCfg, webhookPort(), announcePublic) : null;
|
|
124
|
+
const tunnel = quickTunnelWanted() ? new Tunnel(webhookPort(), announcePublic) : null;
|
|
140
125
|
|
|
141
126
|
function announcePublic(url: string): void {
|
|
142
127
|
process.stderr.write(`\n${publicConnectHint(url)}`);
|
|
@@ -321,7 +306,6 @@ async function main(): Promise<void> {
|
|
|
321
306
|
if (isLocalMode()) ensureLocalSessionSecret();
|
|
322
307
|
if (localSource === null) await materializeFromDb();
|
|
323
308
|
else await materializeFrom(localSource, { allowEmpty: linkedSource === null });
|
|
324
|
-
warnOnLegacyWebhooks();
|
|
325
309
|
supervisor.start();
|
|
326
310
|
const metroMcp = await createMetroMcp();
|
|
327
311
|
webhookServer = await startWebhookServer(
|
|
@@ -373,8 +357,6 @@ const onShutdown = (): void => {
|
|
|
373
357
|
process.exit(1);
|
|
374
358
|
});
|
|
375
359
|
};
|
|
376
|
-
if (process.env.METRO_STDIN_SHUTDOWN === '1')
|
|
377
|
-
process.stdin.on('end', onShutdown).on('close', onShutdown);
|
|
378
360
|
for (const sig of ['SIGINT', 'SIGTERM'] as const) process.on(sig, onShutdown);
|
|
379
361
|
|
|
380
362
|
await main().catch((err: unknown) => {
|
|
@@ -12,6 +12,7 @@ import { homedir } from 'node:os';
|
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import { createInterface } from 'node:readline';
|
|
14
14
|
import { ApiError } from './api-error.js';
|
|
15
|
+
import { isRecord } from './is-record.js';
|
|
15
16
|
|
|
16
17
|
const PROJECT_RE = /^[A-Za-z0-9._-]{1,200}$/;
|
|
17
18
|
const SESSION_RE = /^[A-Za-z0-9][A-Za-z0-9-]{7,63}$/;
|
|
@@ -31,9 +32,6 @@ export function claudeDir(): string {
|
|
|
31
32
|
return join(homedir(), '.claude');
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
35
|
-
typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
36
|
-
|
|
37
35
|
const str = (v: unknown): string | null => (typeof v === 'string' && v !== '' ? v : null);
|
|
38
36
|
|
|
39
37
|
function safeName(value: string, re: RegExp, what: string): string {
|
|
@@ -13,7 +13,7 @@ import { extractToken } from '../mcp/request-identity.js';
|
|
|
13
13
|
import { publicBaseOrDefault } from './attach-serve.js';
|
|
14
14
|
import { AGENT_CODE_RE, takeAgentCode } from './agent-pair.js';
|
|
15
15
|
import { relayServersJson } from './connector-json.js';
|
|
16
|
-
import {
|
|
16
|
+
import { SESSION_TTL_SEC } from './session-config.js';
|
|
17
17
|
import { signAgentToken } from './session.js';
|
|
18
18
|
import type { ConnectorApiDeps } from './connector-api.js';
|
|
19
19
|
|
|
@@ -64,12 +64,11 @@ async function handleClaim(
|
|
|
64
64
|
const token = signAgentToken(
|
|
65
65
|
{ subject: taken.subject, agentId: agent.id },
|
|
66
66
|
secret,
|
|
67
|
-
{ ttlSec:
|
|
67
|
+
{ ttlSec: SESSION_TTL_SEC },
|
|
68
68
|
);
|
|
69
69
|
log.info({ subject: taken.subject, agent: agent.id }, 'cli: code claimed');
|
|
70
70
|
sendJson(req, res, 200, {
|
|
71
71
|
token,
|
|
72
|
-
email: taken.subject,
|
|
73
72
|
subject: taken.subject,
|
|
74
73
|
agent: agent.name,
|
|
75
74
|
});
|
|
@@ -90,7 +89,6 @@ async function handleRead(
|
|
|
90
89
|
const agent = await deps.agentConnectors(who.subject, who.agentId);
|
|
91
90
|
if (path === SESSION_PATH) {
|
|
92
91
|
sendJson(req, res, 200, {
|
|
93
|
-
email: who.subject,
|
|
94
92
|
subject: who.subject,
|
|
95
93
|
agent: agent.name,
|
|
96
94
|
});
|
|
@@ -2,13 +2,8 @@ import { hostname } from 'node:os';
|
|
|
2
2
|
|
|
3
3
|
const uiBase = (): string => process.env.METRO_UI_URL ?? 'https://metro.box';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return `${uiBase()}/#/${new URL(base).host}`;
|
|
8
|
-
} catch {
|
|
9
|
-
return `${uiBase()}/#/connect/${encodeURIComponent(base)}`;
|
|
10
|
-
}
|
|
11
|
-
};
|
|
5
|
+
const connectLink = (base: string): string =>
|
|
6
|
+
`${uiBase()}/#/${new URL(base).host}`;
|
|
12
7
|
|
|
13
8
|
export function localConnectHint(port: number): string {
|
|
14
9
|
const here = `http://127.0.0.1:${String(port)}`;
|
|
@@ -19,9 +19,9 @@ export { takePending, type PendingAuth };
|
|
|
19
19
|
|
|
20
20
|
const REFRESH_SKEW_MS = 300_000;
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
const CALLBACK_PATH = '/api/connectors/callback';
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
const callbackUri = (): string =>
|
|
25
25
|
`${publicBaseOrDefault()}${CALLBACK_PATH}`;
|
|
26
26
|
|
|
27
27
|
function refused(message: string): ConnectorVerifyError {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isRecord } from './is-record.js';
|
|
2
|
+
|
|
1
3
|
export const TOOL_KINDS = ['read', 'write'] as const;
|
|
2
4
|
|
|
3
5
|
export type ToolKind = (typeof TOOL_KINDS)[number];
|
|
@@ -16,10 +18,6 @@ export interface ToolInfo {
|
|
|
16
18
|
const MAX_TOOLS = 500;
|
|
17
19
|
const MAX_DESCRIPTION = 400;
|
|
18
20
|
|
|
19
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
20
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
21
|
const str = (value: unknown, cap: number): string =>
|
|
24
22
|
typeof value === 'string' ? value.slice(0, cap) : '';
|
|
25
23
|
|
|
@@ -66,7 +64,7 @@ export function toToolList(raw: unknown): ToolInfo[] {
|
|
|
66
64
|
return out;
|
|
67
65
|
}
|
|
68
66
|
|
|
69
|
-
|
|
67
|
+
function toStoredTool(raw: unknown): ToolInfo | null {
|
|
70
68
|
if (!isRecord(raw)) return null;
|
|
71
69
|
const name = str(raw.name, 200);
|
|
72
70
|
if (name === '') return null;
|
|
@@ -13,6 +13,7 @@ export {
|
|
|
13
13
|
connectorUrlText,
|
|
14
14
|
parseConnectorUrl,
|
|
15
15
|
} from './connector-url.js';
|
|
16
|
+
import { isRecord } from './is-record.js';
|
|
16
17
|
|
|
17
18
|
export interface OAuthTokens {
|
|
18
19
|
accessToken: string;
|
|
@@ -58,10 +59,6 @@ function notMcp(url: URL): ConnectorNotMcp {
|
|
|
58
59
|
const PROBE_TIMEOUT_MS = 10_000;
|
|
59
60
|
const ACCEPT = 'application/json, text/event-stream';
|
|
60
61
|
|
|
61
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
62
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
62
|
async function discard(res: Response): Promise<void> {
|
|
66
63
|
await res.body?.cancel().catch((err: unknown) => {
|
|
67
64
|
log.debug(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { Line } from '../stations/lines.js';
|
|
3
3
|
|
|
4
|
-
export { userSelf, daemonSelf
|
|
4
|
+
export { userSelf, daemonSelf } from './identity.js';
|
|
5
5
|
|
|
6
6
|
export type StructuredEvent =
|
|
7
7
|
| { type: 'msg' }
|
|
@@ -81,7 +81,7 @@ export interface BufferedEvent {
|
|
|
81
81
|
event: MetroEvent;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
const BUS_BUFFER_MAX = 500;
|
|
85
85
|
|
|
86
86
|
const listeners = new Set<BusListener>();
|
|
87
87
|
const buffer: BufferedEvent[] = [];
|
|
@@ -35,11 +35,10 @@ import {
|
|
|
35
35
|
type MonitorCall,
|
|
36
36
|
} from '../monitor/api.js';
|
|
37
37
|
import { applyMcpCors, handleMcpPreflight } from './cors.js';
|
|
38
|
+
import { METRO_VERSION } from './version.js';
|
|
38
39
|
|
|
39
40
|
const LRU_CAP = 2_000;
|
|
40
41
|
|
|
41
|
-
const METRO_VERSION = process.env.npm_package_version ?? '0.1.0-beta.15';
|
|
42
|
-
|
|
43
42
|
function dedupKey(
|
|
44
43
|
e: Pick<MetroEvent, 'station' | 'line' | 'messageId'>,
|
|
45
44
|
): string | null {
|
|
@@ -1,86 +1,7 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { errMsg, log } from './log.js';
|
|
3
1
|
import { Line } from '../stations/lines.js';
|
|
4
2
|
|
|
5
|
-
const
|
|
6
|
-
|
|
3
|
+
export const userSelf = (): Line =>
|
|
4
|
+
(process.env.METRO_FROM ?? 'metro://user') as Line;
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return () => {
|
|
11
|
-
if (cache && Date.now() - cache.at < TTL_MS) return cache.id;
|
|
12
|
-
const id = loader();
|
|
13
|
-
cache = { id, at: Date.now() };
|
|
14
|
-
return id;
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const claudeAccountId = memo(() => {
|
|
19
|
-
let raw: string;
|
|
20
|
-
try {
|
|
21
|
-
raw = execFileSync('claude', ['auth', 'status', '--json'], {
|
|
22
|
-
encoding: 'utf8',
|
|
23
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
24
|
-
});
|
|
25
|
-
} catch (e) {
|
|
26
|
-
throw new Error(
|
|
27
|
-
`metro: failed to run 'claude auth status --json' — is Claude Code installed? (${errMsg(e)})`,
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
let p: { loggedIn?: boolean; orgId?: string };
|
|
31
|
-
try {
|
|
32
|
-
p = JSON.parse(raw) as { loggedIn?: boolean; orgId?: string };
|
|
33
|
-
} catch {
|
|
34
|
-
throw new Error(
|
|
35
|
-
`metro: 'claude auth status --json' returned non-JSON: ${raw.slice(0, 200)}`,
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
if (!p.loggedIn || !p.orgId)
|
|
39
|
-
throw new Error(
|
|
40
|
-
"metro: Claude Code is not logged in — run 'claude auth login'",
|
|
41
|
-
);
|
|
42
|
-
return p.orgId;
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
export const claudeUserId = (): string =>
|
|
46
|
-
process.env.METRO_USER_ID ?? claudeAccountId();
|
|
47
|
-
|
|
48
|
-
export const claudeSessionId = (): string | null =>
|
|
49
|
-
process.env.METRO_USER_SESSION_ID ??
|
|
50
|
-
process.env.CLAUDE_CODE_SESSION_ID ??
|
|
51
|
-
null;
|
|
52
|
-
|
|
53
|
-
export function userSelf(): Line {
|
|
54
|
-
const explicit = process.env.METRO_FROM;
|
|
55
|
-
if (explicit) return explicit as Line;
|
|
56
|
-
if (!process.env.CLAUDECODE) return 'metro://user' as Line;
|
|
57
|
-
try {
|
|
58
|
-
return Line.user('claude', claudeUserId());
|
|
59
|
-
} catch (e) {
|
|
60
|
-
log.warn(
|
|
61
|
-
{ reason: errMsg(e) },
|
|
62
|
-
'could not resolve a Claude account id; relaying as metro://user',
|
|
63
|
-
);
|
|
64
|
-
return 'metro://user' as Line;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function daemonSelf(): Line {
|
|
69
|
-
const explicit = process.env.METRO_FROM ?? process.env.METRO_SELF_URI;
|
|
70
|
-
return (explicit ?? 'metro://user') as Line;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function selfLine(): Line | null {
|
|
74
|
-
if (!process.env.CLAUDECODE) return null;
|
|
75
|
-
const s = claudeSessionId();
|
|
76
|
-
if (!s) return null;
|
|
77
|
-
try {
|
|
78
|
-
return Line.claude(claudeUserId(), s);
|
|
79
|
-
} catch (e) {
|
|
80
|
-
log.warn(
|
|
81
|
-
{ reason: errMsg(e) },
|
|
82
|
-
'could not resolve a Claude account id; relaying with no self line',
|
|
83
|
-
);
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
6
|
+
export const daemonSelf = (): Line =>
|
|
7
|
+
(process.env.METRO_FROM ?? process.env.METRO_SELF_URI ?? 'metro://user') as Line;
|
|
@@ -14,7 +14,7 @@ export interface LocalCliDeps {
|
|
|
14
14
|
connectorSummaries: (agentId: string) => Promise<ConnectorSummary[]>;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
function keyOf(req: IncomingMessage): string {
|
|
18
18
|
const header = req.headers.authorization ?? '';
|
|
19
19
|
if (header.toLowerCase().startsWith('bearer ')) return header.slice(7).trim();
|
|
20
20
|
const query = new URLSearchParams((req.url ?? '').split('?')[1] ?? '');
|
|
@@ -29,7 +29,7 @@ export function keyIdentity(req: IncomingMessage): AgentIdentity | null {
|
|
|
29
29
|
|
|
30
30
|
async function answer(path: string, key: string, agentId: string, deps: LocalCliDeps): Promise<unknown> {
|
|
31
31
|
const agent = deps.agentName(agentId) ?? '';
|
|
32
|
-
if (path === `${PREFIX}/session`) return {
|
|
32
|
+
if (path === `${PREFIX}/session`) return { subject: 'this machine', agent };
|
|
33
33
|
if (path === `${PREFIX}/connectors`) return { agent, connectors: await deps.connectorSummaries(agentId) };
|
|
34
34
|
return { json: relayServersJson(await deps.connectorEntries(agentId), publicBaseOrDefault(), key), agent };
|
|
35
35
|
}
|
|
@@ -5,7 +5,6 @@ import { AttachSessions } from './attach-session.js';
|
|
|
5
5
|
import type { AgentApiDeps } from './agent-api.js';
|
|
6
6
|
import { ATTACHABLE, type AccountApiDeps } from './account-api.js';
|
|
7
7
|
import type { AgentConnectorApiDeps } from './agent-connector-api.js';
|
|
8
|
-
import type { ProjectApiDeps } from './project-api.js';
|
|
9
8
|
import type { SessionApis } from './session-apis.js';
|
|
10
9
|
import type { ModeInfo } from './mode-api.js';
|
|
11
10
|
import type { ImportApiDeps } from './import-api.js';
|
|
@@ -46,9 +45,7 @@ import {
|
|
|
46
45
|
localOwner,
|
|
47
46
|
localResetAgentKey,
|
|
48
47
|
} from '../db/file-admin.js';
|
|
49
|
-
import { normalizeAddress } from '../db/users.js';
|
|
50
48
|
import { listAgentFiles, readAgentFile } from '../db/file-source.js';
|
|
51
|
-
import type { Project } from '../db/projects.js';
|
|
52
49
|
import type { StationName } from '../db/schema.js';
|
|
53
50
|
|
|
54
51
|
export interface LocalModeDeps {
|
|
@@ -66,16 +63,6 @@ const notHere = (what: string): Promise<never> =>
|
|
|
66
63
|
new ApiError(`${what} are managed on metro.box, not on a local daemon`, 400),
|
|
67
64
|
);
|
|
68
65
|
|
|
69
|
-
function localProject(): Project {
|
|
70
|
-
return {
|
|
71
|
-
id: LOCAL_PROJECT_ID,
|
|
72
|
-
name: hostname(),
|
|
73
|
-
isDefault: true,
|
|
74
|
-
owner: true,
|
|
75
|
-
role: 'admin',
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
66
|
function attachSessions(deps: LocalModeDeps): AttachSessions {
|
|
80
67
|
return new AttachSessions({
|
|
81
68
|
authorize: async (owner) => {
|
|
@@ -124,28 +111,6 @@ function agentApi(deps: LocalModeDeps): AgentApiDeps {
|
|
|
124
111
|
};
|
|
125
112
|
}
|
|
126
113
|
|
|
127
|
-
const projectApi: ProjectApiDeps = {
|
|
128
|
-
listProjects: (subject) => {
|
|
129
|
-
const owner = localOwner();
|
|
130
|
-
const mine = owner !== null && owner === normalizeAddress(subject);
|
|
131
|
-
return Promise.resolve(mine ? [localProject()] : []);
|
|
132
|
-
},
|
|
133
|
-
createProject: () => notHere('projects'),
|
|
134
|
-
renameProject: () => notHere('projects'),
|
|
135
|
-
deleteProject: () => notHere('projects'),
|
|
136
|
-
listMembers: (subject, id) => {
|
|
137
|
-
const owner = localOwner();
|
|
138
|
-
if (id !== LOCAL_PROJECT_ID || owner === null || owner !== normalizeAddress(subject))
|
|
139
|
-
return Promise.reject(new ApiError('no such project', 404));
|
|
140
|
-
return Promise.resolve([
|
|
141
|
-
{ id: 'owner', address: owner, email: null, role: 'admin', owner: true },
|
|
142
|
-
]);
|
|
143
|
-
},
|
|
144
|
-
addMember: () => notHere('members'),
|
|
145
|
-
setMemberRole: () => notHere('members'),
|
|
146
|
-
removeMember: () => notHere('members'),
|
|
147
|
-
};
|
|
148
|
-
|
|
149
114
|
function connectorIdsOfLocalAgents(ids: string[]): Promise<Map<string, string[]>> {
|
|
150
115
|
return Promise.resolve(new Map(ids.map((id) => [id, connectorIdsOfLocalAgent(id) ?? []] as const)));
|
|
151
116
|
}
|
|
@@ -211,7 +176,7 @@ function importApi(deps: LocalModeDeps): ImportApiDeps {
|
|
|
211
176
|
};
|
|
212
177
|
}
|
|
213
178
|
|
|
214
|
-
|
|
179
|
+
function localModeInfo(): ModeInfo {
|
|
215
180
|
return { mode: 'local', owner: localOwner(), project: LOCAL_PROJECT_ID };
|
|
216
181
|
}
|
|
217
182
|
|
|
@@ -225,7 +190,6 @@ export function localSessionApis(deps: LocalModeDeps): SessionApis {
|
|
|
225
190
|
relayApi,
|
|
226
191
|
localCli,
|
|
227
192
|
claudeApi: { authorize: (subject) => { assertLocalOwner(subject); } },
|
|
228
|
-
projectApi,
|
|
229
193
|
siwe: { ensureUser: claimLocalOwner },
|
|
230
194
|
mode: localModeInfo,
|
|
231
195
|
};
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type OAuthTokens,
|
|
5
5
|
} from './connector-verify.js';
|
|
6
6
|
import type { OAuthServer } from './oauth-discovery.js';
|
|
7
|
+
import { isRecord } from './is-record.js';
|
|
7
8
|
|
|
8
9
|
const TOKEN_TIMEOUT_MS = 15_000;
|
|
9
10
|
const CLIENT_NAME = 'Metro';
|
|
@@ -17,10 +18,6 @@ function refused(message: string): ConnectorVerifyError {
|
|
|
17
18
|
return new ConnectorVerifyError(message, 400);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
21
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
21
|
export function newVerifier(): string {
|
|
25
22
|
return randomBytes(32).toString('base64url');
|
|
26
23
|
}
|