@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 +5 -0
- package/package.json +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +42 -2
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/imds.ts +17 -0
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/install.ts +13 -2
- package/runtime/runtime.json +1 -1
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": "metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
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 =
|
|
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
|
-
|
|
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
|
}
|
package/runtime/runtime.json
CHANGED