@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.
Files changed (62) hide show
  1. package/dist/api.js +5 -5
  2. package/dist/bedrock-proxy.js +1 -1
  3. package/dist/cli.js +7 -7
  4. package/dist/plugin.js +1 -1
  5. package/dist/prompt.js +1 -1
  6. package/dist/runtime.js +1 -1
  7. package/dist/serve.js +35 -5
  8. package/package.json +1 -1
  9. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +0 -1
  10. package/runtime/node_modules/@metro-labs/mcp/package.json +1 -5
  11. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-import.ts +2 -2
  12. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +1 -1
  13. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +2 -5
  14. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +13 -26
  15. package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-files.ts +1 -3
  16. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +2 -4
  17. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +11 -10
  18. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +2 -2
  19. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +3 -5
  20. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +1 -4
  21. package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +2 -2
  22. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +1 -2
  23. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +4 -83
  24. package/runtime/node_modules/@metro-labs/mcp/src/daemon/is-record.ts +2 -0
  25. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-cli-api.ts +2 -2
  26. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +3 -39
  27. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-owner.ts +14 -0
  28. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +1 -4
  29. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +1 -4
  30. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +0 -33
  31. package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +3 -3
  32. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +2 -2
  33. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-config.ts +1 -6
  34. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +1 -42
  35. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-auth.ts +2 -11
  36. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +2 -2
  37. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +8 -90
  38. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +1 -1
  39. package/runtime/node_modules/@metro-labs/mcp/src/daemon/version.ts +1 -0
  40. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +1 -1
  41. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +1 -4
  42. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +1 -1
  43. package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +20 -34
  44. package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +1 -3
  45. package/runtime/node_modules/@metro-labs/mcp/src/db/local-connectors.ts +11 -22
  46. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +3 -13
  47. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +3 -3
  48. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +1 -1
  49. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +2 -4
  50. package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +1 -1
  51. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +1 -1
  52. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +1 -21
  53. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +1 -1
  54. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +1 -1
  55. package/runtime/node_modules/@metro-labs/telegram/package.json +0 -2
  56. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +0 -1
  57. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +0 -1
  58. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +2 -5
  59. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +0 -1
  60. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +0 -1
  61. package/runtime/runtime.json +1 -1
  62. 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.email !== 'string' ||
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, email: body.email, agent: body.agent };
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 { email: 'this machine', agent: agent.name, where: `${localUrl()} (local)` };
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.email !== 'string' ||
144
+ typeof body.subject !== 'string' ||
145
145
  typeof body.agent !== 'string')
146
146
  throw new Error('metro returned an unexpected response');
147
- return { email: body.email, agent: body.agent };
147
+ return { subject: body.subject, agent: body.agent };
148
148
  }
@@ -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
- export class ProxyError extends Error {
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 at ${metroWebUrl()}/#/authorize/${agentId}\n`);
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 from the agent's page, or pick the agent at ${metroWebUrl()}/#/authorize\n`);
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, email, agent } = await claimCode(await pastedCode(given));
141
+ const { token, subject, agent } = await claimCode(await pastedCode(given));
142
142
  writeToken(token);
143
- process.stderr.write(`Authorized '${agent}' for ${email}. Stored in ${credentialsPath()}\n`);
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 { email, agent, where } = await whoisAuthorized(wanted);
150
- process.stdout.write(`${email} · agent '${agent}' on ${where}\n`);
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 (get a code at https://metro.box/#/authorize), ' +
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
- export function askLine(question) {
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
- export class MissingBun extends Error {
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 USAGE = 'usage: metro serve [--port <n>] [--tunnel]';
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
- export function findCloudflared() {
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.45",
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 — metro core is pure transport.",
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
- export function metroBaseUrl(): string {
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
- export function storeRunToken(agentId: string, token: string, url: string): string {
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
- export function readGrant(path: string): AttachmentGrant | undefined {
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 { configuredTunnelHost, currentTunnelUrl, webhookPort } from './tunnel.js';
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
- const live = currentTunnelUrl();
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 { selfLine, userSelf } from './events.js';
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
- const self = userSelf();
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 tunnelCfg = loadTunnelConfig() ?? tunnelConfigFromEnv();
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()) ensureLocalSessionSecret();
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 { sessionTtlFromEnv } from './session-config.js';
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: sessionTtlFromEnv() },
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
- export const connectLink = (base: string): string => {
6
- try {
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
- export function localConnectHint(port: number): string {
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
- 'Sign in right away: the first wallet to sign in owns this machine.',
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
- export const CALLBACK_PATH = '/api/connectors/callback';
22
+ const CALLBACK_PATH = '/api/connectors/callback';
23
23
 
24
- export const callbackUri = (): string =>
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
- export function toStoredTool(raw: unknown): ToolInfo | null {
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, selfLine } from './identity.js';
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
- export const BUS_BUFFER_MAX = 500;
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 {