@stage-labs/metro 0.1.0-beta.201 → 0.1.0-beta.202

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 (163) hide show
  1. package/dist/claude.js +4 -15
  2. package/dist/cli.js +5 -3
  3. package/dist/hold.js +1 -2
  4. package/dist/runtime-install.js +4 -5
  5. package/dist/runtime.js +1 -4
  6. package/dist/serve.js +4 -5
  7. package/dist/service.js +82 -21
  8. package/dist/update.js +1 -1
  9. package/package.json +2 -2
  10. package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
  11. package/runtime/node_modules/@metro-labs/core/src/events.ts +4 -2
  12. package/runtime/node_modules/@metro-labs/core/src/lines.ts +12 -25
  13. package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +45 -22
  14. package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +0 -1
  15. package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +8 -80
  16. package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +3 -5
  17. package/runtime/node_modules/@metro-labs/core/src/stations/train-events.ts +4 -9
  18. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +9 -39
  19. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/job-env.ts +1 -1
  20. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +9 -28
  21. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedule-detail.ts +0 -1
  22. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules-api.ts +3 -3
  23. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +8 -116
  24. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/unit-files.ts +11 -35
  25. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +7 -9
  26. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +1 -1
  27. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-lookups.ts +1 -1
  28. package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +1 -1
  29. package/runtime/node_modules/@metro-labs/daemon/src/agents/allowlist.ts +1 -1
  30. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +3 -3
  31. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +2 -2
  32. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +3 -29
  33. package/runtime/node_modules/@metro-labs/daemon/src/approvals/pending.ts +5 -8
  34. package/runtime/node_modules/@metro-labs/daemon/src/approvals/preview.ts +1 -1
  35. package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +5 -8
  36. package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +3 -6
  37. package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +25 -3
  38. package/runtime/node_modules/@metro-labs/daemon/src/channels/media-note.ts +2 -3
  39. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +2 -2
  40. package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +1 -1
  41. package/runtime/node_modules/@metro-labs/daemon/src/claude/memory.ts +6 -56
  42. package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +3 -3
  43. package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +1 -1
  44. package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +7 -6
  45. package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +1 -1
  46. package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +7 -7
  47. package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +4 -4
  48. package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +1 -1
  49. package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +2 -4
  50. package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +19 -22
  51. package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +1 -2
  52. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +6 -8
  53. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +3 -6
  54. package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +1 -1
  55. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +2 -3
  56. package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +28 -1
  57. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +1 -1
  58. package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +4 -4
  59. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +1 -1
  60. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +1 -1
  61. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +1 -1
  62. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +3 -2
  63. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +1 -1
  64. package/runtime/node_modules/@metro-labs/daemon/src/gateway/effort.ts +2 -2
  65. package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +1 -1
  66. package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +3 -3
  67. package/runtime/node_modules/@metro-labs/daemon/src/gateway/frames.ts +1 -1
  68. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +3 -3
  69. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +6 -6
  70. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +7 -7
  71. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +1 -1
  72. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +3 -2
  73. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +1 -1
  74. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +10 -31
  75. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +1 -1
  76. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +1 -1
  77. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +2 -2
  78. package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +3 -3
  79. package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +1 -2
  80. package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +2 -2
  81. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +5 -6
  82. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-snapshot.ts +3 -3
  83. package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-lookup.ts +2 -2
  84. package/runtime/node_modules/@metro-labs/daemon/src/mcp/read-tool.ts +2 -2
  85. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +2 -2
  86. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/install.ts +37 -0
  87. package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +2 -2
  88. package/runtime/node_modules/@metro-labs/daemon/src/policy/policy.ts +1 -1
  89. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +1 -4
  90. package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +3 -8
  91. package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +2 -2
  92. package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +2 -2
  93. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +2 -2
  94. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-xmtp.ts +6 -7
  95. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor-io.ts +1 -1
  96. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +2 -2
  97. package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +2 -2
  98. package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +1 -1
  99. package/runtime/node_modules/@metro-labs/daemon/src/vault/firewall.ts +5 -44
  100. package/runtime/node_modules/@metro-labs/daemon/src/vault/index.ts +4 -4
  101. package/runtime/node_modules/@metro-labs/daemon/src/vault/proxy.ts +6 -7
  102. package/runtime/node_modules/@metro-labs/daemon/src/vault/store.ts +12 -13
  103. package/runtime/node_modules/@metro-labs/daemon/src/vault/trust.ts +5 -22
  104. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +1 -19
  105. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +7 -35
  106. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +5 -10
  107. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +1 -1
  108. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +1 -2
  109. package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +1 -1
  110. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +11 -12
  111. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +2 -5
  112. package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +0 -1
  113. package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +1 -15
  114. package/runtime/node_modules/@metro-labs/outlook/src/actions.ts +0 -6
  115. package/runtime/node_modules/@metro-labs/outlook/src/format.ts +0 -1
  116. package/runtime/node_modules/@metro-labs/outlook/src/state.ts +1 -1
  117. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +4 -22
  118. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +2 -2
  119. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +1 -3
  120. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +1 -1
  121. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +1 -2
  122. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +1 -1
  123. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +4 -37
  124. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +7 -23
  125. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +1 -4
  126. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +4 -9
  127. package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +3 -3
  128. package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +0 -11
  129. package/runtime/node_modules/@metro-labs/threema/src/actions.ts +2 -12
  130. package/runtime/node_modules/@metro-labs/threema/src/files.ts +4 -15
  131. package/runtime/node_modules/@metro-labs/threema/src/format.ts +2 -7
  132. package/runtime/node_modules/@metro-labs/whatsapp/package.json +1 -2
  133. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +5 -26
  134. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +5 -6
  135. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +1 -3
  136. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +1 -1
  137. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +1 -12
  138. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +1 -1
  139. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +14 -5
  140. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +1 -1
  141. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +31 -78
  142. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +3 -4
  143. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +1 -6
  144. package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +1 -1
  145. package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +1 -1
  146. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +1 -1
  147. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +2 -2
  148. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +0 -2
  149. package/runtime/runtime.json +1 -1
  150. package/runtime/node_modules/@metro-labs/core/src/identity.ts +0 -7
  151. package/runtime/node_modules/@metro-labs/daemon/src/approvals/needs.ts +0 -2
  152. package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +0 -12
  153. package/runtime/node_modules/@metro-labs/daemon/src/channels/react-note.ts +0 -13
  154. package/runtime/node_modules/@metro-labs/daemon/src/channels/reply-id.ts +0 -1
  155. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +0 -29
  156. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/api.ts +0 -43
  157. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/move.ts +0 -165
  158. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +0 -1
  159. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +0 -22
  160. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +0 -1
  161. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +0 -1
  162. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +0 -22
  163. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +0 -1
@@ -13,12 +13,11 @@ export interface AgentUser {
13
13
  }
14
14
 
15
15
  export interface UserHost {
16
- platform: string;
17
- uid: number | undefined;
16
+ metro: boolean;
18
17
  lookup: (name: string) => AgentUser | null;
19
18
  }
20
19
 
21
- export function lookupUser(name: string): AgentUser | null {
20
+ function lookupUser(name: string): AgentUser | null {
22
21
  const run = spawnSync('getent', ['passwd', name], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
23
22
  if (run.error !== undefined || run.status !== 0) return null;
24
23
  const [, , uid = '', gid = '', , home = ''] = run.stdout.trim().split(':');
@@ -27,13 +26,13 @@ export function lookupUser(name: string): AgentUser | null {
27
26
  return Number.isInteger(u) && u > 0 && Number.isInteger(g) && home.startsWith('/') ? { name, uid: u, gid: g, home } : null;
28
27
  }
29
28
 
30
- const realHost: UserHost = { platform: process.platform, uid: process.getuid?.(), lookup: lookupUser };
29
+ const realHost = (): UserHost => ({ metro: runningAsMetro(), lookup: lookupUser });
31
30
 
32
- export const agentUserExpected = (host: UserHost = realHost): boolean => host.platform === 'linux' && (host.uid === 0 || runningAsMetro());
31
+ export const agentUserExpected = (host: UserHost = realHost()): boolean => host.metro;
33
32
 
34
33
  let cached: AgentUser | null = null;
35
34
 
36
- export function agentUser(host: UserHost = realHost): AgentUser | null {
35
+ export function agentUser(host: UserHost = realHost()): AgentUser | null {
37
36
  if (!agentUserExpected(host)) return null;
38
37
  cached ??= host.lookup(AGENT_NAME);
39
38
  return cached;
@@ -51,7 +50,7 @@ export function setAgentExtraEnv(env: Record<string, string>): void {
51
50
 
52
51
  export const agentExtraEnv = (): Record<string, string> => ({ ...extraEnv });
53
52
 
54
- export function agentEnv(user: AgentUser, extra: Record<string, string> = {}): Record<string, string> {
53
+ function agentEnv(user: AgentUser, extra: Record<string, string> = {}): Record<string, string> {
55
54
  return {
56
55
  HOME: user.home,
57
56
  USER: user.name,
@@ -68,8 +67,7 @@ export function agentEnv(user: AgentUser, extra: Record<string, string> = {}): R
68
67
  export function asUser(user: AgentUser | null, file: string, args: readonly string[], extra: Record<string, string> = {}): [string, string[]] {
69
68
  if (user === null) return [file, [...args]];
70
69
  const env = Object.entries(agentEnv(user, extra)).map(([k, v]) => `${k}=${v}`);
71
- if (runningAsMetro()) return ['sudo', ['-n', '-u', user.name, '--', 'env', '-i', ...env, file, ...args]];
72
- return ['setpriv', [`--reuid=${String(user.uid)}`, `--regid=${String(user.gid)}`, '--init-groups', 'env', '-i', ...env, file, ...args]];
70
+ return ['sudo', ['-n', '-u', user.name, '--', 'env', '-i', ...env, file, ...args]];
73
71
  }
74
72
 
75
73
  export const asAgent = (file: string, args: readonly string[], extra: Record<string, string> = {}): [string, string[]] =>
@@ -54,7 +54,7 @@ export function viewFiles(dir = agentsDir()): Map<string, string | null> {
54
54
 
55
55
  const written = new Map<string, string | null>();
56
56
 
57
- export function syncAgentView(user: AgentUser | null = agentUser(), dir = agentsDir()): number {
57
+ function syncAgentView(user: AgentUser | null = agentUser(), dir = agentsDir()): number {
58
58
  if (user === null) return 0;
59
59
  const target = agentViewDir(user);
60
60
  let changed = 0;
@@ -11,7 +11,7 @@ interface Target {
11
11
  accountId: string;
12
12
  }
13
13
 
14
- export interface LookupDeps extends SenderCardDeps {
14
+ interface LookupDeps extends SenderCardDeps {
15
15
  resolveSender: (station: StationName, accountId: string, query: string) => Promise<unknown>;
16
16
  }
17
17
 
@@ -32,7 +32,7 @@ import {
32
32
  type AttachView,
33
33
  } from '../stations/attach-session.js';
34
34
 
35
- export interface AttachSessionApi {
35
+ interface AttachSessionApi {
36
36
  start: (
37
37
  owner: AttachOwner,
38
38
  station: InteractiveStation,
@@ -1,6 +1,6 @@
1
1
  import { ApiError } from '@metro-labs/http/api-error';
2
2
 
3
- export const EVERYONE = '*';
3
+ const EVERYONE = '*';
4
4
  const MAX_ENTRIES = 500;
5
5
  const MAX_LENGTH = 200;
6
6
 
@@ -27,21 +27,21 @@ export const loadedAgentOf = (bundle: AgentBundle): LoadedAgent => ({
27
27
  accounts: bundle.agent.stations,
28
28
  });
29
29
 
30
- export interface RestoredAgent {
30
+ interface RestoredAgent {
31
31
  id: string;
32
32
  name: string;
33
33
  stations: number;
34
34
  connectors: number;
35
35
  }
36
36
 
37
- export type ImportMode = 'append' | 'overwrite';
37
+ type ImportMode = 'append' | 'overwrite';
38
38
 
39
39
  export interface BundleApiDeps {
40
40
  bundle: (agentId: string) => Promise<AgentBundle>;
41
41
  restore: (bundle: AgentBundle, mode: ImportMode) => Promise<RestoredAgent>;
42
42
  }
43
43
 
44
- export function parseMode(raw: unknown): ImportMode {
44
+ function parseMode(raw: unknown): ImportMode {
45
45
  const mode = isRecord(raw) && typeof raw.mode === 'string' ? raw.mode : 'overwrite';
46
46
  if (mode !== 'append' && mode !== 'overwrite') throw bad("mode is 'append' or 'overwrite'");
47
47
  return mode;
@@ -35,7 +35,7 @@ interface Stored {
35
35
 
36
36
  const missing = (): AgentAdminError => new AgentAdminError('no such agent', 404);
37
37
 
38
- export function parseOwner(raw: string): string | null {
38
+ function parseOwner(raw: string): string | null {
39
39
  const text = raw.trim();
40
40
  return isOrganizationId(text) ? text : null;
41
41
  }
@@ -56,7 +56,7 @@ export function setLocalOwner(raw: string, dir = agentsDir()): string {
56
56
  return owner;
57
57
  }
58
58
 
59
- export function storedAgents(dir: string): Stored[] {
59
+ function storedAgents(dir: string): Stored[] {
60
60
  return listAgentFiles(dir).map((path) => ({ path, file: readAgentFile(path) }));
61
61
  }
62
62
 
@@ -1,5 +1,4 @@
1
- import { existsSync, readdirSync, readFileSync, renameSync, rmdirSync } from 'node:fs';
2
- import { log } from '@metro-labs/core/log';
1
+ import { existsSync, readFileSync } from 'node:fs';
3
2
  import { homedir } from 'node:os';
4
3
  import { join } from 'node:path';
5
4
  import { AGENT_NAME_RE, ID_RE } from '@metro-labs/core/ids';
@@ -8,7 +7,7 @@ import type { LoadedAccount, LoadedAgent, StationSource } from '../stations/mate
8
7
  import { isRecord } from '@metro-labs/core/is-record';
9
8
  import { parsePolicy } from '../policy/policy.js';
10
9
 
11
- export const AGENT_FILE = 'agent.json';
10
+ const AGENT_FILE = 'agent.json';
12
11
 
13
12
  const KEY_RE = /^[A-Za-z0-9_-]{16,128}$/;
14
13
  const STATION_NAMES = new Set<string>(STATIONS);
@@ -126,34 +125,9 @@ export function readAgentFile(path: string): AgentFile {
126
125
  return parseAgentFile(raw, path);
127
126
  }
128
127
 
129
- function legacyAgentFiles(dir: string): string[] {
130
- if (!existsSync(dir)) return [];
131
- return readdirSync(dir, { withFileTypes: true })
132
- .filter((entry) => entry.isDirectory())
133
- .map((entry) => join(dir, entry.name, AGENT_FILE))
134
- .filter((path) => existsSync(path))
135
- .sort();
136
- }
137
-
138
128
  export function listAgentFiles(dir = agentsDir()): string[] {
139
129
  const fixed = agentFilePath(dir);
140
- return existsSync(fixed) ? [fixed] : legacyAgentFiles(dir);
141
- }
142
-
143
- export function migrateAgentLayout(dir = agentsDir()): 'moved' | 'kept' | 'none' {
144
- const fixed = agentFilePath(dir);
145
- if (existsSync(fixed)) return 'kept';
146
- const [first, ...rest] = legacyAgentFiles(dir);
147
- if (first === undefined) return 'none';
148
- renameSync(first, fixed);
149
- try {
150
- rmdirSync(join(first, '..'));
151
- } catch {
152
- log.warn({ folder: join(first, '..') }, 'agent layout: the old agent folder is not empty and was left in place');
153
- }
154
- for (const extra of rest) log.warn({ path: extra }, 'agent layout: a second agent file is ignored; a box holds one agent');
155
- log.info({ from: first, to: fixed }, 'agent layout: moved the agent file to its fixed place');
156
- return 'moved';
130
+ return existsSync(fixed) ? [fixed] : [];
157
131
  }
158
132
 
159
133
  export function loadFileAgents(dir = agentsDir()): LoadedAgent[] {
@@ -1,6 +1,9 @@
1
1
  import { errMsg, log } from '@metro-labs/core/log';
2
2
  import { previewMatches } from './preview.js';
3
3
 
4
+ export const NEEDS_APPROVAL = (where: string, tool: string): string =>
5
+ `Needs the owner's approval for ${where} (${tool}). Make this exact call from a background worker so Claude Code asks the owner in chat, and wait for the answer. An approval given only in the terminal does not count.`;
6
+
4
7
  export type Behavior = 'allow' | 'deny';
5
8
 
6
9
  export type AnsweredVia = 'chat' | 'page' | 'expiry';
@@ -22,8 +25,7 @@ interface Held extends PendingPrompt {
22
25
 
23
26
  const PENDING_MAX = 500;
24
27
  const SWEEP_MS = 60_000;
25
- const DEFAULT_TTL_H = 24;
26
- const HOUR_MS = 3_600_000;
28
+ const APPROVAL_TTL_MS = 24 * 3_600_000;
27
29
 
28
30
  const held = new Map<string, Held>();
29
31
 
@@ -48,11 +50,6 @@ export function takeGrant(toolMatches: (tool: string) => boolean, args: Record<s
48
50
  return true;
49
51
  }
50
52
 
51
- export function approvalTtlMs(env: NodeJS.ProcessEnv = process.env): number {
52
- const hours = Number(env.METRO_APPROVAL_TTL_H);
53
- return (Number.isFinite(hours) && hours > 0 ? hours : DEFAULT_TTL_H) * HOUR_MS;
54
- }
55
-
56
53
  const shown = ({ requestId, tool, description, preview, line, at }: Held): PendingPrompt => ({
57
54
  requestId,
58
55
  tool,
@@ -122,7 +119,7 @@ export function settlePromptsFor(name: string, args: Record<string, unknown>): v
122
119
  }
123
120
  }
124
121
 
125
- export async function expirePrompts(now = Date.now(), ttl = approvalTtlMs()): Promise<number> {
122
+ export async function expirePrompts(now = Date.now(), ttl = APPROVAL_TTL_MS): Promise<number> {
126
123
  const overdue = [...held.values()].filter((entry) => now - entry.at >= ttl);
127
124
  for (const entry of overdue)
128
125
  await answerPrompt(entry.requestId, 'deny', 'expiry').catch((err: unknown) => {
@@ -1,7 +1,7 @@
1
1
  import { isDeepStrictEqual } from 'node:util';
2
2
  import { isRecord } from '@metro-labs/core/is-record';
3
3
 
4
- export const ELIDED = '\u0000';
4
+ const ELIDED = '\u0000';
5
5
 
6
6
  const CODE_POINTS = /(?:\\n|\n)⋯ \d+ code points elided ⋯(?:\\n|\n)/g;
7
7
  const FIELD_BLOCK = /[\s,]*(?:\\n|\n)?\s*⋯[^]*$/;
@@ -23,17 +23,14 @@ import { agentsDir, fileSource } from '../agents/files.js';
23
23
  import { syncPluginServers } from '../connectors/plugin-sync.js';
24
24
  import { loadConnectorPolicies, readLocalConnectors } from '../connectors/store.js';
25
25
  import { ensureMetroPlugin } from '../claude/plugin-install.js';
26
+ import { runningAsRoot } from '../metro-user/privilege.js';
26
27
  import { provisionAgentUser } from '../agent-user/provision.js';
27
- import { convertRootJobs } from '../agent-user/schedules.js';
28
28
  import { applyVault } from '../vault/index.js';
29
- import { agentUser as currentAgentUser } from '../agent-user/user.js';
30
- import { ensureServiceOomPolicy } from '../claude/memory.js';
31
29
  import { unwatchSession, watchSession } from '../claude/session.js';
32
30
  import { tryClaudeSetup } from '../claude/setup.js';
33
31
  import { applyLocalOwner } from './local-owner.js';
34
32
  import { installBearerSessions } from '../routes/bearer.js';
35
33
  import { ensureLocalAgent, localOwner } from '../agents/file-admin.js';
36
- import { migrateAgentLayout } from '../agents/files.js';
37
34
  import { ensureStationDeps } from '../stations/runtime-deps.js';
38
35
  import { localSessionApis } from '../routes/local-mode.js';
39
36
  import type { SessionApis } from '../routes/session-apis.js';
@@ -124,7 +121,6 @@ function sessionApis(): SessionApis {
124
121
  async function main(): Promise<void> {
125
122
  applyLocalOwner();
126
123
  installBearerSessions(agentsDir(), localOwner);
127
- migrateAgentLayout();
128
124
  watchPolicySnapshot();
129
125
  loadConnectorPolicies().catch((err: unknown) => {
130
126
  log.warn({ err: errMsg(err) }, 'connector policy: loading the policies failed');
@@ -158,13 +154,14 @@ installBearerSessions(agentsDir(), localOwner);
158
154
  }
159
155
 
160
156
  async function startClaude(): Promise<void> {
161
- ensureServiceOomPolicy();
157
+ if (runningAsRoot()) {
158
+ log.error('claude: Metro runs as root, so Claude Code never starts here; reinstall Metro as the metro user');
159
+ return;
160
+ }
162
161
  const agentUser = await provisionAgentUser();
163
162
  if (agentUser !== 'off') log.info({ agentUser }, 'agent-user: Claude Code runs as its own user');
164
163
  if (agentUser === 'failed') return;
165
164
  if (agentUser === 'ready') {
166
- const switched = convertRootJobs(currentAgentUser());
167
- if (switched > 0) log.info({ switched }, "schedules: jobs that pointed into root's home now run as the agent user");
168
165
  const vault = (await applyVault()).status;
169
166
  if (vault.enabled) log.info({ running: vault.running, problem: vault.problem }, 'vault: the agent reaches the internet through the vault');
170
167
  }
@@ -1,14 +1,11 @@
1
1
  import { hostname } from 'node:os';
2
2
 
3
- const uiBase = (): string => process.env.METRO_UI_URL ?? 'https://metro.box';
4
-
5
- const connectLink = (base: string): string =>
6
- `${uiBase()}/#/${new URL(base).host}`;
3
+ const connectLink = (base: string): string => `https://metro.box/#/${new URL(base).host}`;
7
4
 
8
5
  const ownerLine = (owner: string | null): string =>
9
6
  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.`;
7
+ ? 'No owner is set, so nobody can sign in. Restart with: metro serve --owner <organization id>'
8
+ : `Only members of the organization ${owner} can sign in.`;
12
9
 
13
10
  export function localConnectHint(port: number, owner: string | null): string {
14
11
  const here = `http://127.0.0.1:${String(port)}`;
@@ -1,7 +1,5 @@
1
1
  import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
2
  import { str } from '@metro-labs/core/str';
3
- import { dedupeKey } from './dedupe.js';
4
- import { PERMISSION_REPLY_RE } from './reply-id.js';
5
3
  import {
6
4
  buildMediaFailureNote,
7
5
  buildMediaNote,
@@ -9,7 +7,6 @@ import {
9
7
  type SavedMedia,
10
8
  } from './media-note.js';
11
9
  import { buildWebhookNote } from './webhook-note.js';
12
- import { reactContent, reactionEmoji } from './react-note.js';
13
10
  import { replyMeta } from './addressed.js';
14
11
  import {
15
12
  capSet,
@@ -23,6 +20,31 @@ import {
23
20
  } from './pending.js';
24
21
  import { agentUser } from '../agent-user/user.js';
25
22
 
23
+ const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i;
24
+
25
+ const accountStrippedLine = (line: string): string => {
26
+ const parts = line.split('/');
27
+ if (parts.length < 5) return line;
28
+ return [parts[0], parts[1], parts[2], ...parts.slice(4)].join('/');
29
+ };
30
+
31
+ const dedupeKey = (station: string, line: string, kind: string, messageId: string): string =>
32
+ `${station} ${accountStrippedLine(line)} ${kind} ${messageId}`;
33
+
34
+ const shortId = (id: string): string => (id.length > 10 ? `${id.slice(0, 6)}…` : id);
35
+
36
+ function reactionEmoji(raw: unknown): string {
37
+ if (typeof raw === 'string') return raw;
38
+ const obj = raw as { name?: string; reaction?: string } | undefined;
39
+ return obj?.name ?? obj?.reaction ?? '';
40
+ }
41
+
42
+ function reactContent(emoji: string, target: string, removed: boolean): string {
43
+ const verb = removed ? 'removed from' : 'reacted to';
44
+ const label = removed ? emoji || 'reaction' : emoji || 'reacted';
45
+ return `${label} ${verb} message ${shortId(target)}`.trim();
46
+ }
47
+
26
48
  interface InboundDeps {
27
49
  mcp: Server;
28
50
  log: (...a: unknown[]) => void;
@@ -6,7 +6,6 @@ export interface SavedMedia {
6
6
  contentType?: string;
7
7
  attachmentFor?: string;
8
8
  attachmentPath?: string;
9
- localPath?: string;
10
9
  url?: string;
11
10
  mime?: string;
12
11
  name?: string;
@@ -22,7 +21,7 @@ export interface MediaNote {
22
21
  path: string;
23
22
  }
24
23
 
25
- export function mediaKind(mime?: string, name?: string): string {
24
+ function mediaKind(mime?: string, name?: string): string {
26
25
  const m = (mime ?? '').toLowerCase();
27
26
  if (m.startsWith('image/')) return 'image';
28
27
  if (m.startsWith('video/')) return 'video';
@@ -61,7 +60,7 @@ export async function buildMediaNote(
61
60
  caption: string,
62
61
  showPath = true,
63
62
  ): Promise<MediaNote | null> {
64
- const path = p.attachmentPath ?? p.localPath;
63
+ const path = p.attachmentPath;
65
64
  if (!path) return null;
66
65
  const kind = p.kind ?? mediaKind(p.mime, p.name);
67
66
  const name = p.name ?? path.split('/').pop() ?? 'attachment';
@@ -167,14 +167,14 @@ export function listClaudeSessions(project: string, dir = claudeDir()): ClaudeSe
167
167
  );
168
168
  }
169
169
 
170
- export type Block =
170
+ type Block =
171
171
  | { kind: 'text'; text: string }
172
172
  | { kind: 'tool_use'; name: string; input: string }
173
173
  | { kind: 'tool_result'; text: string; isError: boolean }
174
174
  | { kind: 'thinking' }
175
175
  | { kind: 'image' };
176
176
 
177
- export interface TranscriptEntry {
177
+ interface TranscriptEntry {
178
178
  uuid: string;
179
179
  at: string | null;
180
180
  role: 'user' | 'assistant';
@@ -16,7 +16,7 @@ const ESC = String.fromCharCode(27);
16
16
  const CSI = /^\[[0-9;?]*[a-zA-Z]/;
17
17
  const URL_RE = /https:\/\/[^\s"'`]+/g;
18
18
 
19
- export type LoginState = 'pending' | 'done' | 'failed';
19
+ type LoginState = 'pending' | 'done' | 'failed';
20
20
 
21
21
  export interface LoginView {
22
22
  id: string;
@@ -1,27 +1,19 @@
1
- import { spawnSync } from 'node:child_process';
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
1
+ import { existsSync } from 'node:fs';
3
2
  import { totalmem } from 'node:os';
4
- import { dirname } from 'node:path';
5
- import { errMsg, log } from '@metro-labs/core/log';
6
3
  import { helperArgv, runningAsMetro } from '../metro-user/privilege.js';
7
4
 
8
- const UNIT = '/etc/systemd/system/metro.service';
9
- const DROP_IN = '/etc/systemd/system/metro.service.d/10-metro-memory.conf';
10
- const DROP_IN_TEXT = '[Service]\nOOMPolicy=continue\n';
11
5
  const GIB = 1024 ** 3;
12
6
  const RESERVE = 1.5 * GIB;
13
7
  const FLOOR = GIB;
14
8
 
15
9
  export interface ScopeHost {
16
- platform: string;
17
- uid: number | undefined;
10
+ metro: boolean;
18
11
  systemd: boolean;
19
12
  total: number;
20
13
  }
21
14
 
22
15
  const realHost = (): ScopeHost => ({
23
- platform: process.platform,
24
- uid: process.getuid?.(),
16
+ metro: runningAsMetro(),
25
17
  systemd: existsSync('/run/systemd/system'),
26
18
  total: totalmem(),
27
19
  });
@@ -31,52 +23,10 @@ export function sessionMemoryLimit(total: number): number {
31
23
  return Math.max(FLOOR, Math.floor(kept / (1024 * 1024)) * 1024 * 1024);
32
24
  }
33
25
 
34
- const usesScopes = (host: ScopeHost): boolean => host.platform === 'linux' && host.uid === 0 && host.systemd;
35
-
36
- function metroScope(command: [string, string[]], total: number): [string, string[]] {
26
+ export function inSessionScope(command: [string, string[]], host: ScopeHost = realHost()): [string, string[]] {
37
27
  const [file, args] = command;
38
28
  const asAgent = file === 'sudo' && args[0] === '-n' && args[1] === '-u' && args[3] === '--';
39
- if (!asAgent) return command;
40
- const limit = sessionMemoryLimit(total);
41
- return helperArgv(['as-agent-scope', String(limit), String(Math.floor(limit * 0.9)), ...args.slice(4)]);
42
- }
43
-
44
- export function inSessionScope(command: [string, string[]], host: ScopeHost = realHost(), now = Date.now()): [string, string[]] {
45
- if (host.systemd && runningAsMetro()) return metroScope(command, host.total);
46
- if (!usesScopes(host)) return command;
29
+ if (!host.metro || !host.systemd || !asAgent) return command;
47
30
  const limit = sessionMemoryLimit(host.total);
48
- const [file, args] = command;
49
- return [
50
- 'systemd-run',
51
- [
52
- '--scope',
53
- '--quiet',
54
- '--collect',
55
- `--unit=metro-claude-${String(now)}`,
56
- '-p',
57
- `MemoryMax=${String(limit)}`,
58
- '-p',
59
- `MemoryHigh=${String(Math.floor(limit * 0.9))}`,
60
- '-p',
61
- 'OOMPolicy=continue',
62
- '--',
63
- file,
64
- ...args,
65
- ],
66
- ];
67
- }
68
-
69
- export function ensureServiceOomPolicy(host: ScopeHost = realHost(), env: NodeJS.ProcessEnv = process.env): 'written' | 'present' | 'skipped' {
70
- if (!usesScopes(host) || (env.INVOCATION_ID ?? '') === '' || !existsSync(UNIT)) return 'skipped';
71
- try {
72
- if (existsSync(DROP_IN) && readFileSync(DROP_IN, 'utf8') === DROP_IN_TEXT) return 'present';
73
- mkdirSync(dirname(DROP_IN), { recursive: true });
74
- writeFileSync(DROP_IN, DROP_IN_TEXT, { mode: 0o644 });
75
- const reload = spawnSync('systemctl', ['daemon-reload'], { stdio: 'ignore' });
76
- log.info({ reloaded: reload.status === 0 }, 'claude-memory: the metro service no longer stops as a whole when the kernel kills one process for memory');
77
- return 'written';
78
- } catch (err) {
79
- log.warn({ err: errMsg(err) }, 'claude-memory: could not set the service memory policy');
80
- return 'skipped';
81
- }
31
+ return helperArgv(['as-agent-scope', String(limit), String(Math.floor(limit * 0.9)), ...args.slice(4)]);
82
32
  }
@@ -26,7 +26,7 @@ export interface PluginInstallDeps {
26
26
 
27
27
  export type PluginOutcome = 'skipped' | 'unchanged' | 'installed' | 'updated' | 'failed';
28
28
 
29
- export function storeMarketplaceDir(env: NodeJS.ProcessEnv = process.env): string | null {
29
+ function storeMarketplaceDir(env: NodeJS.ProcessEnv = process.env): string | null {
30
30
  const store = env.METRO_RUNTIME_STORE?.trim() ?? '';
31
31
  if (store === '') return null;
32
32
  const dir = join(store, 'marketplace');
@@ -60,7 +60,7 @@ const runner =
60
60
  });
61
61
  });
62
62
 
63
- export async function findClaude(candidates = agentUser() === null ? ['claude', join(homedir(), '.local', 'bin', 'claude')] : [claudeBin()]): Promise<Run | null> {
63
+ async function findClaude(candidates = agentUser() === null ? ['claude', join(homedir(), '.local', 'bin', 'claude')] : [claudeBin()]): Promise<Run | null> {
64
64
  for (const bin of candidates) {
65
65
  const run = runner(bin);
66
66
  if ((await run(['--version'])).status === 0) return run;
@@ -129,7 +129,7 @@ function explainMissingMarketplace(env: NodeJS.ProcessEnv = process.env): void {
129
129
  if (store === '') return;
130
130
  log.warn(
131
131
  { store },
132
- 'plugin: the runtime store holds no plugin marketplace, so metro cannot install its Claude Code plugin; a `metro serve` started before beta.97 never copied it, restart the service once',
132
+ 'plugin: the runtime store holds no plugin marketplace, so metro cannot install its Claude Code plugin; restart the metro service once so `metro serve` stages it again',
133
133
  );
134
134
  }
135
135
 
@@ -6,7 +6,7 @@ import { ApiError } from '@metro-labs/http/api-error';
6
6
  import { isRecord } from '@metro-labs/core/is-record';
7
7
  import { claudeDir, PROJECT_RE, safeName, SESSION_RE } from './files.js';
8
8
 
9
- export const SESSION_FILE_MAX = 512 * 1024 * 1024;
9
+ const SESSION_FILE_MAX = 512 * 1024 * 1024;
10
10
  const FILE_MODE = 0o644;
11
11
 
12
12
  export interface SessionFile {
@@ -12,7 +12,7 @@ import { claudeDir, listClaudeProjects } from './files.js';
12
12
  import { claudeAccount, claudeInstalled } from './login.js';
13
13
  import { trustFolder } from './onboarding.js';
14
14
  import { inSessionScope } from './memory.js';
15
- import { runningAsMetro } from '../metro-user/privilege.js';
15
+ import { runningAsRoot } from '../metro-user/privilege.js';
16
16
  import { agentExtraEnv, agentUser, agentUserExpected, agentViewDir, asAgent, claudeHome } from '../agent-user/user.js';
17
17
 
18
18
  function sessionEnv(): Record<string, string> {
@@ -22,7 +22,7 @@ function sessionEnv(): Record<string, string> {
22
22
  return { ...agentExtraEnv(), METRO_AGENTS_DIR: agentViewDir(user), ...(port === '' ? {} : { METRO_WEBHOOK_PORT: port }) };
23
23
  }
24
24
 
25
- export const SESSION_NAME = 'metro';
25
+ const SESSION_NAME = 'metro';
26
26
  const STATE_FILE = 'claude-session.json';
27
27
  const WARNING = 'WARNING: Loading development channels';
28
28
  const CONFIRM_POLL_MS = 500;
@@ -78,7 +78,7 @@ export function setAutostart(enabled: boolean, agents = agentsDir()): void {
78
78
  writeState(agents, { autostart: enabled });
79
79
  }
80
80
 
81
- export function startedVersion(agents = agentsDir()): string | null {
81
+ function startedVersion(agents = agentsDir()): string | null {
82
82
  const version = readState(agents).version;
83
83
  return typeof version === 'string' ? version : null;
84
84
  }
@@ -136,7 +136,10 @@ function credentialReady(deps: SessionDeps): string | null {
136
136
  const agentUserMissing = (deps: SessionDeps): boolean =>
137
137
  deps.metro === undefined && agentUserExpected() && agentUser() === null;
138
138
 
139
+ const ROOT_REFUSED = 'Metro runs as root; reinstall it as the metro user';
140
+
139
141
  export function sessionBlocked(deps: SessionDeps = {}): string | null {
142
+ if (runningAsRoot()) return ROOT_REFUSED;
140
143
  const agents = deps.agents ?? agentsDir();
141
144
  if (listAgentFiles(agents).length === 0) return 'no agent on this machine yet';
142
145
  if (agentUserMissing(deps))
@@ -176,8 +179,6 @@ function recordStart(deps: SessionDeps, tmux: string, now: number, run: { error?
176
179
  confirmChannels(tmux, now + CONFIRM_WAIT_MS);
177
180
  }
178
181
 
179
- const spawnCwd = (home: string): string => (runningAsMetro() ? '/' : home);
180
-
181
182
  export function startSession(deps: SessionDeps = {}): SessionStatus {
182
183
  const tmux = deps.tmux ?? 'tmux';
183
184
  const home = deps.home ?? claudeHome() ?? homedir();
@@ -186,7 +187,7 @@ export function startSession(deps: SessionDeps = {}): SessionStatus {
186
187
  const [command = 'metro', ...args] = metroCommand(deps, home);
187
188
  const tmuxArgs = ['new-session', '-d', '-s', SESSION_NAME, '-c', home, '-x', '200', '-y', '50', command, ...args];
188
189
  const launch = deps.tmux === undefined && !tmuxServerUp(tmux) ? inSessionScope(asAgent(tmux, tmuxArgs)) : asAgent(tmux, tmuxArgs);
189
- const run = spawnSync(...launch, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], cwd: spawnCwd(home) });
190
+ const run = spawnSync(...launch, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], cwd: '/' });
190
191
  recordStart(deps, tmux, now, run);
191
192
  if (memory.lastError === null) log.info({ home, trusted, command: [command, ...args].join(' ') }, 'claude-session: started Claude Code in tmux');
192
193
  return sessionStatus(deps);
@@ -10,7 +10,7 @@ export const SETTINGS_MAX = 256 * 1024;
10
10
  const USER_ID = 'user';
11
11
  const LOCAL_SUFFIX = '.local';
12
12
 
13
- export type SettingsScope = 'user' | 'project' | 'local';
13
+ type SettingsScope = 'user' | 'project' | 'local';
14
14
 
15
15
  export interface ClaudeSettingsFile {
16
16
  id: string;
@@ -19,13 +19,13 @@ export const PRIVACY_ENV: Record<string, string> = {
19
19
  CLAUDE_CODE_GB_DISK_CACHE_WHEN_TELEMETRY_OFF: '1',
20
20
  };
21
21
  export const RETENTION_DAYS = 7;
22
- export const SKILL_NAME = 'metro-orchestrator';
22
+ const SKILL_NAME = 'metro-orchestrator';
23
23
  const STATE_FILE = 'claude-setup.json';
24
24
  const GUIDANCE = 'orchestrator.md';
25
- export const SYSTEM_PROMPT_FILE = 'system-prompt.md';
25
+ const SYSTEM_PROMPT_FILE = 'system-prompt.md';
26
26
  export const SYSTEM_PROMPT_MAX = 64 * 1024;
27
27
 
28
- export const WORKER_AGENT = `---
28
+ const WORKER_AGENT = `---
29
29
  name: worker
30
30
  description: Default execution agent for delegated work. Full tool access and maximum reasoning effort. Use for any substantive task the orchestrator main thread cannot perform itself - reading and writing code, running commands, research, analysis.
31
31
  effort: xhigh
@@ -73,7 +73,7 @@ export interface SetupStatus {
73
73
 
74
74
  const statePath = (agents: string): string => join(agents, STATE_FILE);
75
75
 
76
- export const PERMISSION_MODES = ['auto', 'bypass'] as const;
76
+ const PERMISSION_MODES = ['auto', 'bypass'] as const;
77
77
  export type PermissionMode = (typeof PERMISSION_MODES)[number];
78
78
 
79
79
  function readState(agents: string): Record<string, unknown> {
@@ -86,7 +86,7 @@ function writeState(agents: string, patch: Record<string, unknown>): void {
86
86
  writeJson(statePath(agents), { ...readState(agents), ...patch });
87
87
  }
88
88
 
89
- export const privacyEnabled = (agents = agentsDir()): boolean => readState(agents).privacy !== false;
89
+ const privacyEnabled = (agents = agentsDir()): boolean => readState(agents).privacy !== false;
90
90
 
91
91
  export function setPrivacy(enabled: boolean, agents = agentsDir()): void {
92
92
  writeState(agents, { privacy: enabled });
@@ -120,7 +120,7 @@ export function setSystemPrompt(text: string, agents = agentsDir()): void {
120
120
  writeAtomic(promptPath(agents), `${trimmed}\n`, 0o600);
121
121
  }
122
122
 
123
- export function guidancePath(env: NodeJS.ProcessEnv = process.env): string {
123
+ function guidancePath(env: NodeJS.ProcessEnv = process.env): string {
124
124
  const staged = stagedMarketplaceDir(env);
125
125
  if (staged !== null) {
126
126
  const path = join(staged, 'plugin', GUIDANCE);
@@ -177,7 +177,7 @@ function mergeSettings(dir: string, change: (settings: Record<string, unknown>)
177
177
  return 'written';
178
178
  }
179
179
 
180
- export function applyPrivacy(dir: string, enabled: boolean): SettingsOutcome {
180
+ function applyPrivacy(dir: string, enabled: boolean): SettingsOutcome {
181
181
  return mergeSettings(dir, (current) => withPrivacy(current, enabled));
182
182
  }
183
183