@stage-labs/metro 0.1.0-beta.45 → 0.1.0-beta.47
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 +7 -7
- package/dist/plugin.js +1 -1
- package/dist/prompt.js +1 -1
- package/dist/runtime.js +1 -1
- package/dist/serve.js +35 -5
- 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 +13 -26
- 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 +11 -10
- 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 +3 -39
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-owner.ts +14 -0
- 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 +20 -34
- 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
|
@@ -16,7 +16,7 @@ const USAGE = `metro — the command line for your MCP connectors
|
|
|
16
16
|
|
|
17
17
|
metro start <agent-id> [--detach]
|
|
18
18
|
run that agent's stations on this machine
|
|
19
|
-
metro serve [--port <n>] [--tunnel]
|
|
19
|
+
metro serve [--port <n>] [--tunnel] [--owner <address>]
|
|
20
20
|
run a daemon of your own: its agents and stations live in ~/.metro/agents
|
|
21
21
|
here, and metro.box manages it through the link it prints; --tunnel adds
|
|
22
22
|
a public https address through a Cloudflare quick tunnel (needs
|
|
@@ -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
|
@@ -1,25 +1,45 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
2
3
|
import { join } from 'node:path';
|
|
4
|
+
import { agentsDir } from './local.js';
|
|
3
5
|
import { serveLockedBy, serveStateDir } from './control.js';
|
|
4
6
|
import { findBun, localPort, runtimeDir, SERVER_ENTRY, spawnPlan, } from './runtime.js';
|
|
5
7
|
const SCRUBBED = new Set(['METRO_RUN_TOKEN', 'METRO_AGENT', 'DATABASE_URL']);
|
|
6
8
|
const PORT_FLAG = /^--port=(.*)$/;
|
|
7
|
-
const
|
|
9
|
+
const OWNER_FLAG = /^--owner=(.*)$/;
|
|
10
|
+
const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
11
|
+
const USAGE = 'usage: metro serve [--port <n>] [--tunnel] [--owner <address>]';
|
|
8
12
|
function portOf(raw) {
|
|
9
13
|
const n = Number(raw);
|
|
10
14
|
if (raw === undefined || raw === '' || !Number.isInteger(n) || n < 1 || n > 65535)
|
|
11
15
|
throw new Error(`'${raw ?? ''}' is not a port — ${USAGE}`);
|
|
12
16
|
return n;
|
|
13
17
|
}
|
|
18
|
+
function ownerOf(raw) {
|
|
19
|
+
if (raw === undefined || !ADDRESS.test(raw))
|
|
20
|
+
throw new Error(`'${raw ?? ''}' is not an Ethereum address — ${USAGE}`);
|
|
21
|
+
return raw.toLowerCase();
|
|
22
|
+
}
|
|
14
23
|
export function parseServeArgs(argv) {
|
|
15
24
|
let port = localPort();
|
|
16
25
|
let tunnel = false;
|
|
26
|
+
let owner = null;
|
|
17
27
|
for (let i = 0; i < argv.length; i += 1) {
|
|
18
28
|
const arg = argv[i] ?? '';
|
|
19
29
|
if (arg === '--tunnel') {
|
|
20
30
|
tunnel = true;
|
|
21
31
|
continue;
|
|
22
32
|
}
|
|
33
|
+
const inlineOwner = OWNER_FLAG.exec(arg);
|
|
34
|
+
if (inlineOwner) {
|
|
35
|
+
owner = ownerOf(inlineOwner[1]);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (arg === '--owner') {
|
|
39
|
+
owner = ownerOf(argv[i + 1]);
|
|
40
|
+
i += 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
23
43
|
const inline = PORT_FLAG.exec(arg);
|
|
24
44
|
if (inline) {
|
|
25
45
|
port = portOf(inline[1]);
|
|
@@ -32,9 +52,17 @@ export function parseServeArgs(argv) {
|
|
|
32
52
|
}
|
|
33
53
|
throw new Error(`unknown argument '${arg}' — ${USAGE}`);
|
|
34
54
|
}
|
|
35
|
-
return { port, tunnel };
|
|
55
|
+
return { port, tunnel, owner };
|
|
56
|
+
}
|
|
57
|
+
export function requireOwner(owner, dir = agentsDir()) {
|
|
58
|
+
if (owner !== null || existsSync(join(dir, '.owner')))
|
|
59
|
+
return;
|
|
60
|
+
throw new Error('no owner is set for this machine, so no wallet could sign in.\n' +
|
|
61
|
+
'Pass the wallet that owns it once; it is remembered in ' +
|
|
62
|
+
join(dir, '.owner') +
|
|
63
|
+
':\n metro serve --owner <address>');
|
|
36
64
|
}
|
|
37
|
-
|
|
65
|
+
function findCloudflared() {
|
|
38
66
|
const found = spawnSync('cloudflared', ['--version'], { stdio: 'ignore' });
|
|
39
67
|
if (found.error !== undefined || found.status !== 0)
|
|
40
68
|
throw new Error('metro serve --tunnel needs cloudflared, which is not on PATH.\n' +
|
|
@@ -60,18 +88,20 @@ export function servePlan(opts) {
|
|
|
60
88
|
METRO_TRAINS_DIR: join(opts.dir, 'trains'),
|
|
61
89
|
METRO_STATE_DIR: process.env.METRO_STATE_DIR ?? serveStateDir(),
|
|
62
90
|
...(opts.tunnel ? { METRO_TUNNEL: 'quick' } : {}),
|
|
91
|
+
...(opts.owner === null ? {} : { METRO_OWNER: opts.owner }),
|
|
63
92
|
},
|
|
64
93
|
};
|
|
65
94
|
}
|
|
66
95
|
export function serve(argv) {
|
|
67
|
-
const { port, tunnel } = parseServeArgs(argv);
|
|
96
|
+
const { port, tunnel, owner } = parseServeArgs(argv);
|
|
68
97
|
const running = serveLockedBy();
|
|
69
98
|
if (running !== null)
|
|
70
99
|
throw new Error(`a metro serve daemon is already running on this machine (pid ${String(running)}). ` +
|
|
71
100
|
'Stop it first: metro stop');
|
|
101
|
+
requireOwner(owner);
|
|
72
102
|
if (tunnel)
|
|
73
103
|
findCloudflared();
|
|
74
|
-
const plan = servePlan({ dir: runtimeDir(), port, tunnel });
|
|
104
|
+
const plan = servePlan({ dir: runtimeDir(), port, tunnel, owner });
|
|
75
105
|
process.stderr.write(`Starting a metro daemon of your own on http://127.0.0.1:${String(port)}\n`);
|
|
76
106
|
return spawnPlan(plan);
|
|
77
107
|
}
|
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.47",
|
|
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,
|
|
@@ -45,6 +33,8 @@ import {
|
|
|
45
33
|
import { loadAgentForRuntime, localAgentKey } from '../db/materialize.js';
|
|
46
34
|
import { fileSource } from '../db/file-source.js';
|
|
47
35
|
import { ensureLocalSessionSecret } from './local-secret.js';
|
|
36
|
+
import { applyLocalOwner } from './local-owner.js';
|
|
37
|
+
import { localOwner } from '../db/file-admin.js';
|
|
48
38
|
import { localSessionApis } from './local-mode.js';
|
|
49
39
|
import type { ModeInfo } from './mode-api.js';
|
|
50
40
|
import type { SessionApis } from './session-apis.js';
|
|
@@ -115,11 +105,9 @@ import type { AgentApiDeps } from './agent-api.js';
|
|
|
115
105
|
import type { ConnectorApiDeps } from './connector-api.js';
|
|
116
106
|
|
|
117
107
|
installCrashGuard();
|
|
118
|
-
loadMetroEnv();
|
|
119
108
|
acquireLock(join(STATE_DIR, '.tail-lock'));
|
|
120
109
|
|
|
121
|
-
|
|
122
|
-
log.info({ self, line: selfLine() }, 'user identity');
|
|
110
|
+
log.info({ self: userSelf() }, 'user identity');
|
|
123
111
|
|
|
124
112
|
process.stdout.on('error', (err) => {
|
|
125
113
|
if ((err as NodeJS.ErrnoException).code !== 'EPIPE')
|
|
@@ -135,11 +123,10 @@ supervisor.onTrainEvent((env, train) => {
|
|
|
135
123
|
});
|
|
136
124
|
|
|
137
125
|
let webhookServer: Server | null = null;
|
|
138
|
-
const
|
|
139
|
-
const tunnel = tunnelCfg ? new Tunnel(tunnelCfg, webhookPort(), announcePublic) : null;
|
|
126
|
+
const tunnel = quickTunnelWanted() ? new Tunnel(webhookPort(), announcePublic) : null;
|
|
140
127
|
|
|
141
128
|
function announcePublic(url: string): void {
|
|
142
|
-
process.stderr.write(`\n${publicConnectHint(url)}`);
|
|
129
|
+
process.stderr.write(`\n${publicConnectHint(url, localOwner())}`);
|
|
143
130
|
}
|
|
144
131
|
|
|
145
132
|
setTrainCallBackend((train, action, args) =>
|
|
@@ -153,7 +140,7 @@ const localSource = linkedSource ?? (isLocalMode() ? fileSource : null);
|
|
|
153
140
|
function announceLocalEndpoint(): void {
|
|
154
141
|
if (isLocalMode())
|
|
155
142
|
process.stderr.write(
|
|
156
|
-
`\n${tunnel === null ? localConnectHint(webhookPort()) : tunnelPendingHint()}`,
|
|
143
|
+
`\n${tunnel === null ? localConnectHint(webhookPort(), localOwner()) : tunnelPendingHint()}`,
|
|
157
144
|
);
|
|
158
145
|
const key = localAgentKey();
|
|
159
146
|
if (key === null) {
|
|
@@ -318,10 +305,12 @@ function sessionApis(): SessionApis {
|
|
|
318
305
|
}
|
|
319
306
|
|
|
320
307
|
async function main(): Promise<void> {
|
|
321
|
-
if (isLocalMode())
|
|
308
|
+
if (isLocalMode()) {
|
|
309
|
+
ensureLocalSessionSecret();
|
|
310
|
+
applyLocalOwner();
|
|
311
|
+
}
|
|
322
312
|
if (localSource === null) await materializeFromDb();
|
|
323
313
|
else await materializeFrom(localSource, { allowEmpty: linkedSource === null });
|
|
324
|
-
warnOnLegacyWebhooks();
|
|
325
314
|
supervisor.start();
|
|
326
315
|
const metroMcp = await createMetroMcp();
|
|
327
316
|
webhookServer = await startWebhookServer(
|
|
@@ -373,8 +362,6 @@ const onShutdown = (): void => {
|
|
|
373
362
|
process.exit(1);
|
|
374
363
|
});
|
|
375
364
|
};
|
|
376
|
-
if (process.env.METRO_STDIN_SHUTDOWN === '1')
|
|
377
|
-
process.stdin.on('end', onShutdown).on('close', onShutdown);
|
|
378
365
|
for (const sig of ['SIGINT', 'SIGTERM'] as const) process.on(sig, onShutdown);
|
|
379
366
|
|
|
380
367
|
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,15 +2,15 @@ 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
|
+
const ownerLine = (owner: string | null): string =>
|
|
9
|
+
owner === null
|
|
10
|
+
? 'No owner is set, so no wallet can sign in. Restart with: metro serve --owner <address>'
|
|
11
|
+
: `Only ${owner} can sign in.`;
|
|
12
|
+
|
|
13
|
+
export function localConnectHint(port: number, owner: string | null): string {
|
|
14
14
|
const here = `http://127.0.0.1:${String(port)}`;
|
|
15
15
|
const forward = `ssh -L ${String(port)}:127.0.0.1:${String(port)} ${hostname()}`;
|
|
16
16
|
return [
|
|
@@ -19,17 +19,18 @@ export function localConnectHint(port: number): string {
|
|
|
19
19
|
` ${connectLink(here)}`,
|
|
20
20
|
'',
|
|
21
21
|
`From another computer, forward the port first: ${forward}`,
|
|
22
|
+
ownerLine(owner),
|
|
22
23
|
'',
|
|
23
24
|
].join('\n');
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
export function publicConnectHint(url: string): string {
|
|
27
|
+
export function publicConnectHint(url: string, owner: string | null): string {
|
|
27
28
|
return [
|
|
28
29
|
'Manage this machine from the web UI, from anywhere:',
|
|
29
30
|
'',
|
|
30
31
|
` ${connectLink(url)}`,
|
|
31
32
|
'',
|
|
32
|
-
|
|
33
|
+
ownerLine(owner),
|
|
33
34
|
'',
|
|
34
35
|
].join('\n');
|
|
35
36
|
}
|
|
@@ -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 {
|