@stage-labs/metro 0.1.0-beta.242 → 0.1.0-beta.243

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/service.js CHANGED
@@ -183,6 +183,11 @@ function install(argv, deps) {
183
183
  const args = parseServeArgs(serveArgs);
184
184
  const runAs = runAsOf(user, deps);
185
185
  const plan = servicePlan(deps.host, serveArgs, runAs);
186
+ if (deps.exists(plan.file) && runAs !== undefined) {
187
+ deps.run({ args: ['bun', runAs.helper] });
188
+ deps.out(`metro is already installed as a ${plan.kind} service (${plan.file}); its root helper, sudo rules and metadata guard are now current, and the service keeps running`);
189
+ return 0;
190
+ }
186
191
  if (deps.exists(plan.file)) {
187
192
  deps.out(`metro is already installed as a ${plan.kind} service (${plan.file}) and restarts on its own; metro service status says whether it is running. To change its arguments: metro service uninstall, then install again`);
188
193
  return 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.242",
3
+ "version": "0.1.0-beta.243",
4
4
  "description": "The metro command line: run your agent on this machine, relaying its chat channels and connectors to Claude Code.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metro",
3
- "version": "0.1.0-beta.242",
3
+ "version": "0.1.0-beta.243",
4
4
  "description": "Metro for Claude Code, installed by the metro daemon on this machine: every connector the agent holds as its own MCP server relayed through the daemon, the orchestrator-only guard on the main thread, and the standing rules loaded at each session start. No vendor credential ever sits in a config file.",
5
5
  "author": {
6
6
  "name": "Bonustrack",
@@ -26,6 +26,7 @@ import { ensureMetroPlugin } from '../claude/plugin-install.js';
26
26
  import { runningAsRoot } from '../metro-user/privilege.js';
27
27
  import { provisionAgentUser } from '../agent-user/provision.js';
28
28
  import { applyVault } from '../vault/index.js';
29
+ import { blockImds } from '../metro-user/imds.js';
29
30
  import { unwatchSession, watchSession } from '../claude/session.js';
30
31
  import { liveEvents, memoryRoutine, tryClaudeSetup } from '../claude/setup.js';
31
32
  import { tryMemoryJob } from '../claude/memory-routine.js';
@@ -172,6 +173,7 @@ async function startClaude(): Promise<void> {
172
173
  if (agentUser !== 'off') log.info({ agentUser }, 'agent-user: Claude Code runs as its own user');
173
174
  if (agentUser === 'failed') return;
174
175
  if (agentUser === 'ready') {
176
+ blockImds();
175
177
  const vault = (await applyVault()).status;
176
178
  if (vault.enabled) log.info({ running: vault.running, problem: vault.problem }, 'vault: the agent reaches the internet through the vault');
177
179
  }
@@ -1,6 +1,8 @@
1
1
  export const HELPER_PATH = '/usr/local/lib/metro/root-helper';
2
- const HELPER_VERSION = 1;
2
+ const HELPER_VERSION = 2;
3
3
  export const SUDOERS_PATH = '/etc/sudoers.d/metro';
4
+ export const IMDS_UNIT = 'metro-imds.service';
5
+ export const IMDS_UNIT_PATH = `/etc/systemd/system/${IMDS_UNIT}`;
4
6
  export const METRO_USER = 'metro';
5
7
  const METRO_HOME = '/var/lib/metro';
6
8
 
@@ -73,6 +75,43 @@ const FIREWALL = lines(
73
75
  '}',
74
76
  );
75
77
 
78
+ const IMDS = lines(
79
+ 'imds_block() {',
80
+ ` metro_uid=$(id -u ${METRO_USER} 2>/dev/null) || die "no ${METRO_USER} user"`,
81
+ ' for t in iptables ip6tables; do',
82
+ ' if [ "$t" = iptables ]; then dst=169.254.169.254; else dst=fd00:ec2::254; fi',
83
+ ' if ! $t -w -L OUTPUT -n >/dev/null 2>&1; then [ "$t" = ip6tables ] && continue; die "$t is not usable"; fi',
84
+ ' $t -w -N METRO_IMDS 2>/dev/null || true',
85
+ ' $t -w -F METRO_IMDS',
86
+ ' $t -w -A METRO_IMDS -m owner --uid-owner 0 -j RETURN',
87
+ ' $t -w -A METRO_IMDS -m owner --uid-owner "$metro_uid" -j RETURN',
88
+ ' $t -w -A METRO_IMDS -p tcp -j REJECT --reject-with tcp-reset',
89
+ ' $t -w -A METRO_IMDS -j REJECT',
90
+ ' $t -w -C OUTPUT -d "$dst" -j METRO_IMDS 2>/dev/null || $t -w -I OUTPUT -d "$dst" -j METRO_IMDS',
91
+ ' done',
92
+ '}',
93
+ );
94
+
95
+ export const imdsUnitText = (): string =>
96
+ lines(
97
+ '[Unit]',
98
+ `Description=Metro: only root and ${METRO_USER} reach the instance metadata service`,
99
+ 'DefaultDependencies=no',
100
+ 'After=local-fs.target',
101
+ 'Before=network-pre.target shutdown.target',
102
+ 'Wants=network-pre.target',
103
+ 'Conflicts=shutdown.target',
104
+ '',
105
+ '[Service]',
106
+ 'Type=oneshot',
107
+ 'RemainAfterExit=yes',
108
+ `ExecStart=${HELPER_PATH} imds-block`,
109
+ '',
110
+ '[Install]',
111
+ 'WantedBy=multi-user.target',
112
+ '',
113
+ );
114
+
76
115
  const ACTIONS = lines(
77
116
  'action="${1:-}"; [ "$#" -gt 0 ] && shift',
78
117
  'case "$action" in',
@@ -109,6 +148,7 @@ const ACTIONS = lines(
109
148
  ' [ "$found" = 1 ] || die "that cron line is gone"',
110
149
  ' install -m 600 "$cur" "$backups/crontab-root.$(date +%Y-%m-%dT%H-%M-%S).bak"',
111
150
  ' crontab -u root "$kept"; rm -f "$cur" "$kept" ;;',
151
+ ' imds-block) imds_block ;;',
112
152
  ' firewall-on) firewall_on ;;',
113
153
  ' firewall-off) firewall_off ;;',
114
154
  ' trust-ca)',
@@ -123,4 +163,4 @@ const ACTIONS = lines(
123
163
  );
124
164
 
125
165
  export const helperScript = (): string =>
126
- ['#!/bin/sh', 'set -eu', 'export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', CHECKS, FIREWALL, ACTIONS, ''].join('\n');
166
+ ['#!/bin/sh', 'set -eu', 'export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', CHECKS, FIREWALL, IMDS, ACTIONS, ''].join('\n');
@@ -0,0 +1,17 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { log } from '@metro-labs/core/log';
4
+ import { rootHelper, type HelperResult } from './privilege.js';
5
+
6
+ export function blockImds(run: (args: string[]) => HelperResult = rootHelper): boolean {
7
+ const result = run(['imds-block']);
8
+ if (result.status === 0) {
9
+ log.info('imds: only root and metro reach the instance metadata service');
10
+ return true;
11
+ }
12
+ log.warn(
13
+ { err: result.stderr.trim(), fix: `as root: ${join(homedir(), '.npm-global', 'bin', 'metro')} service install --user metro` },
14
+ 'imds: the agent can still read the instance metadata (setup data, instance role), since the root helper on this box predates the guard',
15
+ );
16
+ return false;
17
+ }
@@ -2,7 +2,7 @@ import { spawnSync } from 'node:child_process';
2
2
  import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { dirname, join } from 'node:path';
5
- import { HELPER_PATH, helperScript, SUDOERS_PATH, sudoersText } from './helper-script.js';
5
+ import { HELPER_PATH, helperScript, IMDS_UNIT, IMDS_UNIT_PATH, imdsUnitText, SUDOERS_PATH, sudoersText } from './helper-script.js';
6
6
  import { AGENT_NAME } from '../agent-user/user.js';
7
7
 
8
8
  interface InstallPaths {
@@ -10,6 +10,8 @@ interface InstallPaths {
10
10
  sudoers: string;
11
11
  }
12
12
 
13
+ type Run = (file: string, args: string[]) => void;
14
+
13
15
  function checked(file: string, args: string[]): void {
14
16
  const run = spawnSync(file, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
15
17
  if (run.error !== undefined) throw run.error;
@@ -31,7 +33,16 @@ export function installRootHelper(paths: InstallPaths = { helper: HELPER_PATH, s
31
33
  }
32
34
  }
33
35
 
36
+ export function installImdsGuard(unit: string = IMDS_UNIT_PATH, run: Run = checked): void {
37
+ writeFileSync(unit, imdsUnitText(), { mode: 0o644 });
38
+ chmodSync(unit, 0o644);
39
+ run('systemctl', ['daemon-reload']);
40
+ run('systemctl', ['enable', IMDS_UNIT]);
41
+ run('systemctl', ['restart', IMDS_UNIT]);
42
+ }
43
+
34
44
  if (import.meta.main) {
35
45
  installRootHelper();
36
- process.stdout.write(`metro: wrote ${HELPER_PATH} and ${SUDOERS_PATH}\n`);
46
+ installImdsGuard();
47
+ process.stdout.write(`metro: wrote ${HELPER_PATH}, ${SUDOERS_PATH} and ${IMDS_UNIT_PATH}; only root and metro reach the instance metadata service\n`);
37
48
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.242"
2
+ "version": "0.1.0-beta.243"
3
3
  }