@ravefox/space 0.2.0 → 0.3.0

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/LICENSE +21 -203
  2. package/README.md +89 -26
  3. package/bin/space.js +106 -0
  4. package/lib/agents.js +119 -0
  5. package/lib/credentials.js +95 -0
  6. package/lib/daemon.js +294 -0
  7. package/lib/harness.js +166 -0
  8. package/lib/http.js +101 -0
  9. package/lib/login.js +68 -0
  10. package/lib/mcp.js +34 -0
  11. package/lib/runtime.js +241 -0
  12. package/lib/store.js +96 -0
  13. package/lib/tools.js +93 -0
  14. package/package.json +28 -40
  15. package/NOTICE +0 -44
  16. package/assets/webhook-sdk/python/bot.py +0 -63
  17. package/assets/webhook-sdk/python/commonly.py +0 -232
  18. package/node_modules/@commonlyai/seats/README.md +0 -25
  19. package/node_modules/@commonlyai/seats/package.json +0 -22
  20. package/node_modules/@commonlyai/seats/src/adapters/hermes.js +0 -264
  21. package/node_modules/@commonlyai/seats/src/adapters/stub.js +0 -24
  22. package/node_modules/@commonlyai/seats/src/contract.js +0 -48
  23. package/node_modules/@commonlyai/seats/src/failures.js +0 -44
  24. package/node_modules/@commonlyai/seats/src/harness-plan.js +0 -329
  25. package/node_modules/@commonlyai/seats/src/index.js +0 -22
  26. package/node_modules/@commonlyai/seats/src/process-group.js +0 -269
  27. package/node_modules/@commonlyai/seats/src/record/format.js +0 -737
  28. package/node_modules/@commonlyai/seats/src/record/otel.js +0 -238
  29. package/node_modules/@commonlyai/seats/src/runtime-observation.js +0 -29
  30. package/node_modules/@commonlyai/seats/src/seat-model.js +0 -17
  31. package/node_modules/@commonlyai/seats/src/supervisor.js +0 -308
  32. package/node_modules/@commonlyai/seats/src/trace/events.js +0 -46
  33. package/node_modules/@commonlyai/seats/src/trace/store.js +0 -276
  34. package/node_modules/@commonlyai/seats/src/turn-processes.js +0 -226
  35. package/node_modules/@commonlyai/seats/src/upstream-refusal.js +0 -418
  36. package/node_modules/@commonlyai/seats/src/valet/env.js +0 -201
  37. package/node_modules/@commonlyai/seats/src/valet/grant.js +0 -123
  38. package/node_modules/@commonlyai/seats/src/valet/permissions.js +0 -213
  39. package/node_modules/@commonlyai/seats/src/wire/provider-proxy.js +0 -529
  40. package/skills/commonly/SKILL.md +0 -233
  41. package/src/commands/agent.js +0 -6334
  42. package/src/commands/daemon.js +0 -933
  43. package/src/commands/dev.js +0 -153
  44. package/src/commands/doctor.js +0 -141
  45. package/src/commands/inbox.js +0 -330
  46. package/src/commands/login.js +0 -194
  47. package/src/commands/mcp.js +0 -56
  48. package/src/commands/pod.js +0 -120
  49. package/src/commands/task.js +0 -279
  50. package/src/commands/verify.js +0 -48
  51. package/src/index.js +0 -99
  52. package/src/lib/adapter-login.js +0 -76
  53. package/src/lib/adapters/broker-bridge-entry.js +0 -76
  54. package/src/lib/adapters/claude.js +0 -2207
  55. package/src/lib/adapters/codex-broker-bridge.mjs +0 -298
  56. package/src/lib/adapters/codex.js +0 -1898
  57. package/src/lib/adapters/dsh.js +0 -513
  58. package/src/lib/adapters/goose.js +0 -510
  59. package/src/lib/adapters/hermes.js +0 -18
  60. package/src/lib/adapters/index.js +0 -68
  61. package/src/lib/adapters/opencode.js +0 -1681
  62. package/src/lib/adapters/pi-commonly-mcp.mjs +0 -71
  63. package/src/lib/adapters/pi-mcp-client.mjs +0 -617
  64. package/src/lib/adapters/pi-shell-env.mjs +0 -41
  65. package/src/lib/adapters/pi.js +0 -1025
  66. package/src/lib/adapters/qwen.js +0 -523
  67. package/src/lib/adapters/seat-model.js +0 -8
  68. package/src/lib/adapters/stub.js +0 -1
  69. package/src/lib/adapters/tested-version.js +0 -106
  70. package/src/lib/api.js +0 -205
  71. package/src/lib/claim-outcome.js +0 -128
  72. package/src/lib/claimed-work-gate.js +0 -73
  73. package/src/lib/cli-hint.js +0 -33
  74. package/src/lib/commit-attribution.js +0 -391
  75. package/src/lib/config.js +0 -155
  76. package/src/lib/content-refusal.js +0 -94
  77. package/src/lib/credential-file.js +0 -208
  78. package/src/lib/daemon-agent-states.js +0 -95
  79. package/src/lib/daemon-hint.js +0 -18
  80. package/src/lib/daemon-logs.js +0 -43
  81. package/src/lib/daemon-service-windows.js +0 -263
  82. package/src/lib/daemon-service.js +0 -486
  83. package/src/lib/daemon-state.js +0 -84
  84. package/src/lib/daemon-store.js +0 -119
  85. package/src/lib/daemon-supervisor.js +0 -629
  86. package/src/lib/daemon-watch.js +0 -115
  87. package/src/lib/declared-mcp-guard.js +0 -284
  88. package/src/lib/default-environment.js +0 -207
  89. package/src/lib/device-login.js +0 -126
  90. package/src/lib/digest-frame.js +0 -151
  91. package/src/lib/doctor.js +0 -291
  92. package/src/lib/egress-proxy.js +0 -160
  93. package/src/lib/enforcement.js +0 -946
  94. package/src/lib/environment.js +0 -681
  95. package/src/lib/first-output-watchdog.js +0 -191
  96. package/src/lib/git-readonly-server.js +0 -138
  97. package/src/lib/git-readonly.js +0 -463
  98. package/src/lib/git-remote-commonly.mjs +0 -100
  99. package/src/lib/grant-broker-guard.js +0 -345
  100. package/src/lib/hooks-config.js +0 -238
  101. package/src/lib/host-gh-broker.js +0 -152
  102. package/src/lib/host-gh-shim.mjs +0 -98
  103. package/src/lib/inbox.js +0 -483
  104. package/src/lib/instance-proxy.js +0 -76
  105. package/src/lib/machine-host.js +0 -29
  106. package/src/lib/mcp-credential-delivery.js +0 -121
  107. package/src/lib/mcp-first-run.js +0 -50
  108. package/src/lib/mcp-home.js +0 -644
  109. package/src/lib/mcp-ready.js +0 -68
  110. package/src/lib/mcp-server-version.js +0 -123
  111. package/src/lib/mcp-token.js +0 -36
  112. package/src/lib/mcp-warm-child.mjs +0 -412
  113. package/src/lib/memory-bridge.js +0 -191
  114. package/src/lib/memory-import.js +0 -156
  115. package/src/lib/missing-harness.js +0 -31
  116. package/src/lib/model-unavailable.js +0 -154
  117. package/src/lib/native-runtime-inventory.js +0 -40
  118. package/src/lib/outsider-grant-mcp.js +0 -82
  119. package/src/lib/own-key-launch.js +0 -42
  120. package/src/lib/pod-catch-up.js +0 -55
  121. package/src/lib/pod-focus.js +0 -224
  122. package/src/lib/pod-tail.js +0 -68
  123. package/src/lib/poll-liveness.js +0 -103
  124. package/src/lib/poll-retry.js +0 -81
  125. package/src/lib/poller.js +0 -200
  126. package/src/lib/provider-proxy.js +0 -42
  127. package/src/lib/public-seat-mcp-flag.js +0 -36
  128. package/src/lib/public-tool-error.js +0 -60
  129. package/src/lib/public-withheld-tools.js +0 -48
  130. package/src/lib/reply-receipt.js +0 -55
  131. package/src/lib/resolve-on-path.js +0 -22
  132. package/src/lib/runtime-inventory.js +0 -56
  133. package/src/lib/runtime-observation.js +0 -37
  134. package/src/lib/sandbox/bwrap.js +0 -275
  135. package/src/lib/sandbox/codex-perimeter.js +0 -16
  136. package/src/lib/sandbox/host-check.js +0 -268
  137. package/src/lib/sandbox/mode.js +0 -49
  138. package/src/lib/sandbox/opencode-loopback.mjs +0 -68
  139. package/src/lib/sandbox/seatbelt.js +0 -684
  140. package/src/lib/sandbox/turn-dir.js +0 -56
  141. package/src/lib/sandbox/windows-unsandboxed.js +0 -183
  142. package/src/lib/sandbox/workspace-git-config.js +0 -243
  143. package/src/lib/sandbox/workspace-pin.js +0 -158
  144. package/src/lib/seat-runtime-controls.js +0 -61
  145. package/src/lib/seat-stop.js +0 -97
  146. package/src/lib/server-skills.js +0 -32
  147. package/src/lib/session-recycle.js +0 -219
  148. package/src/lib/session-store.js +0 -215
  149. package/src/lib/skill-prompt.js +0 -74
  150. package/src/lib/skills-import.js +0 -113
  151. package/src/lib/spawn-credential.js +0 -402
  152. package/src/lib/spawn-retry.js +0 -311
  153. package/src/lib/stable-install.js +0 -176
  154. package/src/lib/supervised-turn.js +0 -352
  155. package/src/lib/task.js +0 -235
  156. package/src/lib/turn-failure-notice.js +0 -177
  157. package/src/lib/unconfined-seat-guard.js +0 -94
  158. package/src/lib/update-check.js +0 -192
  159. package/src/lib/upstream-refusal.js +0 -1
  160. package/src/lib/webhook-server.js +0 -109
  161. package/src/lib/windows-command.js +0 -393
  162. package/src/lib/workspace-git.js +0 -87
  163. package/src/lib/workspace-trust.js +0 -162
@@ -1,191 +0,0 @@
1
- /**
2
- * Memory bridge — ADR-005 §Memory bridge, ADR-003 Phase 2.
3
- *
4
- * Two thin CAP shims the run loop calls around every spawn cycle:
5
- *
6
- * readLongTerm(client) — GET /api/agents/runtime/memory,
7
- * returns sections.long_term.content,
8
- * falling back to legacy content
9
- * syncBack(client, { summary }) — POST /api/agents/runtime/memory/sync
10
- * with mode:'patch', sourceRuntime:'local-cli'.
11
- * No-op when summary is falsy.
12
- *
13
- * Identity (agentName, instanceId) is derived server-side from the runtime
14
- * token (agentRuntimeAuth), so neither helper needs to pass it. Keeping it
15
- * server-derived is invariant-preserving: a bug in the wrapper can't write
16
- * to the wrong agent's memory.
17
- *
18
- * ADR-003 invariant #9: the wrapper supplies `content` + `visibility` ONLY.
19
- * `byteSize`, `updatedAt`, and `schemaVersion` are server-stamped; supplying
20
- * them from the client is wasted bytes and the kernel discards them.
21
- */
22
-
23
- import { KEEP_PROCESS_ENV } from '@commonlyai/seats/turn-processes';
24
-
25
- export const SOURCE_RUNTIME = 'local-cli';
26
-
27
- /**
28
- * Row slicing, the runtime-agnostic half of TASK-545. A row too large
29
- * for one turn drifts when one seat carries it whole, so the seat splits it on
30
- * the board before any code. It names the exact call and field for the reason
31
- * the memory cue below names `long_term`: a seat adopts a habit through a
32
- * surface it can see. Each adapter decides whether to pass it.
33
- */
34
- export const ROW_SLICING_CUE = 'Slicing: when a row is too large for one turn, split it before '
35
- + 'writing any code. Post each slice as a child row (commonly_create_task with parentTask set '
36
- + "to the row's taskId) with one assignee, a written file or area boundary, and its own "
37
- + 'acceptance line.';
38
-
39
- // The turn's leftovers are stopped when it ends (@commonlyai/seats/turn-processes),
40
- // so a seat that means to reuse a server has to say so when it starts it.
41
- export const BACKGROUND_PROCESS_CUE = 'Background processes: anything you start in the background is '
42
- + 'stopped when this turn ends, including a server that starts on first use, such as tmux, and an agent you '
43
- + 'restart with nohup. To keep one for a later turn, such as a dev server you will reuse, start it '
44
- + `with ${KEEP_PROCESS_ENV}=1 set, and stop it yourself when you are done with it.`;
45
-
46
- /**
47
- * The turn preamble, shared by every adapter so the two cannot drift.
48
- *
49
- * Non-empty memory is prepended verbatim — that path is load-bearing and is
50
- * not touched here.
51
- *
52
- * The EMPTY case used to return the bare prompt, which made two very different
53
- * states byte-identical from inside the session: an agent that has never saved
54
- * anything, and an agent on a runtime with no memory bridge at all. A seat
55
- * cannot adopt a habit whose surface it has no evidence exists, so the empty
56
- * case now says so.
57
- *
58
- * It names `long_term` specifically because that is the only v2 section read
59
- * back. Legacy `content` is a fallback for records without that section. A write to `daily` —
60
- * the section whose name most invites exactly this use — succeeds, returns 200,
61
- * and is never seen again; the write and the silence are indistinguishable from
62
- * a correct round trip. Naming the section is the whole point of the cue.
63
- *
64
- * `workCues` are working habits an adapter teaches on a fresh session, beside
65
- * the memory cues and under the same once-per-session rule. An adapter passes
66
- * only the habits its CLI can act on; passing none leaves the preamble as it
67
- * was.
68
- *
69
- * `memoryReadOnly` is a public seat on a turn its owner didn't wake (F118, F145):
70
- * it reads its memory and is shown the long_term it read, but it is not offered
71
- * the save tool, so no cue names one, and it is told why. A resumed session may
72
- * hold an earlier turn's offer, so that line goes in every turn, not just fresh
73
- * ones. `memoryWithheld` wins over it: that turn reads nothing either.
74
- */
75
- export const buildMemoryPreamble = (
76
- prompt,
77
- memoryLongTerm,
78
- { freshSession = false, workCues = [], memoryWithheld = false, memoryReadOnly = false } = {},
79
- ) => {
80
- // `null` is the UNREADABLE signal, and it is deliberately not the same value
81
- // as `''`. Telling a seat whose token was revoked that "nothing has ever been
82
- // saved here" is a false claim about its own history, and it is the same
83
- // defect this cue exists to fix — one state over. When we could not read, say
84
- // that, and say nothing about what is stored.
85
- const ownerOnly = 'You can read your memory on this turn but not change it: '
86
- + 'a memory write works only when your owner started the turn.';
87
- let context;
88
- if (memoryWithheld) {
89
- // A workspace seat's turn that runs public: not unreadable and not empty,
90
- // so neither of those cues is true. Its memory tools are off as well.
91
- context = `=== Context (your persistent memory) ===\n`
92
- + `(withheld this turn — this turn runs as a public seat because not every `
93
- + `pod you answer in is confirmed private, so your memory stays out of it and `
94
- + `the memory tools are off. This says nothing about what you have saved.)`;
95
- } else if (memoryLongTerm === null) {
96
- context = `=== Context (your persistent memory) ===\n`
97
- + `(unreadable this turn — the memory read failed, so this says NOTHING `
98
- + `about what you have saved. Do not treat it as empty`
99
- + (memoryReadOnly ? `.)\n${ownerOnly}` : ` and do not re-save state you may already hold.)`);
100
- } else if (memoryLongTerm) {
101
- context = `=== Context (your persistent memory) ===\n${memoryLongTerm}`
102
- + (memoryReadOnly ? `\n(${ownerOnly})` : '');
103
- } else if (memoryReadOnly) {
104
- // F118: this turn isn't offered the save tool, so the cue that teaches it
105
- // would name a tool the seat cannot call.
106
- context = `=== Context (your persistent memory) ===\n`
107
- + `(empty — nothing has ever been saved here)\n${ownerOnly}`;
108
- } else {
109
- context = `=== Context (your persistent memory) ===\n`
110
- + `(empty — nothing has ever been saved here)\n`
111
- + `Only the \`long_term\` section is read back into this prompt. To make `
112
- + `something survive your next session, call commonly_save_my_memory({ `
113
- + `section: 'long_term', content: '...' }). A write to any other section `
114
- + `succeeds and is never shown to you again.`;
115
- }
116
-
117
- context = '=== Workspace identity ===\n'
118
- + 'This workspace is Space. Always call the product Space when explaining where you are. '
119
- + 'The upstream-compatible tools and environment variables keep their commonly_* and COMMONLY_* names.\n'
120
- + context;
121
-
122
- if (!freshSession) return `${context}\n=== Current turn ===\n${prompt}`;
123
-
124
- // This belongs on a fresh underlying CLI session, rather than every turn:
125
- // a resumed session already carries the earlier instruction in its own
126
- // transcript. Repeating it on each wake spends prompt budget while making
127
- // the cue easier to ignore. The wrapper cannot know the final event of a
128
- // session in advance, so it gives the end-of-session reminder while the
129
- // agent can still act on it.
130
- const freshReadCue = '=== Fresh session ===\n'
131
- + 'This is a fresh session. Read the persistent memory context above before acting; '
132
- + 'it carries durable state from prior sessions.\n';
133
- let sessionEndCue = '=== Before this session ends ===\n';
134
- if (memoryWithheld) {
135
- sessionEndCue += 'Memory is withheld this turn, so there is nothing to save.';
136
- } else if (memoryReadOnly) {
137
- sessionEndCue += 'Memory is read-only on this turn, so there is nothing to save.';
138
- } else if (memoryLongTerm === null) {
139
- sessionEndCue += 'Memory was unreadable on this fresh session. Do not treat it as empty or '
140
- + 'write a replacement based on this cue.';
141
- } else if (memoryLongTerm) {
142
- sessionEndCue += 'At a natural end to meaningful work, save durable working state — '
143
- + 'gates held, decisions pending, and task context, not a transcript — with '
144
- + "commonly_save_my_memory({ section: 'long_term', content: '...' }).";
145
- } else {
146
- sessionEndCue += 'At a natural end to meaningful work, save durable working state — '
147
- + 'gates held, decisions pending, and task context, not a transcript — using '
148
- + 'the long_term write above.';
149
- }
150
- const workCue = workCues.length
151
- ? `\n=== Working a row ===\n${workCues.map((cue) => `- ${cue}`).join('\n')}\n`
152
- : '';
153
- return `${context}\n${freshReadCue}${workCue}\n=== Current turn ===\n${prompt}\n${sessionEndCue}`;
154
- };
155
-
156
- export const readLongTerm = async (client, { onError } = {}) => {
157
- try {
158
- const body = await client.get('/api/agents/runtime/memory');
159
- return body?.sections?.long_term?.content ?? body?.content ?? '';
160
- } catch (err) {
161
- // A 404 is the ONLY error that means what '' means: a fresh agent with no
162
- // memory row yet. The kernel upserts on first write, so this is genuine
163
- // absence and the empty cue is true.
164
- if (err?.status === 404) return '';
165
- // Everything else — auth revoked, 500, connection refused — is a failure to
166
- // READ, which tells us nothing about what is stored. Returning '' here made
167
- // the caller assert emptiness on no evidence.
168
- //
169
- // `err.status` is undefined for a transport failure, so the old guard
170
- // (`err?.status && err.status !== 404`) skipped onError precisely when the
171
- // backend was unreachable: the loudest condition was the silent one, and
172
- // nothing contradicted the false cue.
173
- onError?.(err);
174
- return null;
175
- }
176
- };
177
-
178
- export const syncBack = async (client, { summary } = {}) => {
179
- if (!summary) return { skipped: true };
180
- await client.post('/api/agents/runtime/memory/sync', {
181
- mode: 'patch',
182
- sourceRuntime: SOURCE_RUNTIME,
183
- sections: {
184
- long_term: {
185
- content: summary,
186
- visibility: 'private',
187
- },
188
- },
189
- });
190
- return { skipped: false };
191
- };
@@ -1,156 +0,0 @@
1
- /**
2
- * Local memory import — Phase C of the retention plan ("your agent arrives
3
- * whole"). Detects the memory a local agent already has on this machine
4
- * (project CLAUDE.md / MEMORY.md, Claude Code's per-project auto-memory
5
- * directory), composes it into one document, and promotes it into the
6
- * kernel envelope's `long_term` section via POST /memory/sync.
7
- *
8
- * Design constraints:
9
- * - Explicitly opt-in with preview (plan D3) — local memory files can
10
- * contain private material; callers must confirm before anything is
11
- * sent. This module only detects/composes/sends; the command layer
12
- * owns the prompt.
13
- * - APPEND, never clobber: `mode: 'patch'` replaces a section wholesale
14
- * (mergePatchSections is section-level), so we read the existing
15
- * long_term and append the import after it.
16
- * - Server stamps byteSize/updatedAt/schemaVersion (ADR-003 invariant
17
- * #9); we send content + visibility only.
18
- */
19
-
20
- import { existsSync, readFileSync, readdirSync, statSync, realpathSync } from 'fs';
21
- import { homedir } from 'os';
22
- import { join, basename } from 'path';
23
-
24
- export const SOURCE_RUNTIME = 'import-local';
25
-
26
- // Envelope sections are curated context, not dumps. The auto-clearer story
27
- // on the agent side starts caring around 400KB of session; a quarter of
28
- // that is a generous ceiling for a one-shot import.
29
- export const MAX_IMPORT_BYTES = 256 * 1024;
30
-
31
- // Claude Code keeps per-project auto-memory under
32
- // ~/.claude/projects/<slug>/memory where <slug> is the project path with
33
- // every '/' replaced by '-'.
34
- export const claudeProjectMemoryDir = (cwd, home) => join(
35
- home,
36
- '.claude',
37
- 'projects',
38
- cwd.replaceAll('/', '-'),
39
- 'memory',
40
- );
41
-
42
- const fileCandidate = (path, label) => {
43
- if (!existsSync(path)) return null;
44
- const stat = statSync(path);
45
- if (!stat.isFile() || stat.size === 0) return null;
46
- return { path, label, bytes: stat.size };
47
- };
48
-
49
- /**
50
- * Find importable memory sources. An explicit path wins outright (file, or
51
- * directory whose *.md files are all taken); otherwise auto-detect the
52
- * well-known locations. Duplicates are collapsed by realpath — this repo,
53
- * for instance, symlinks AGENTS.md → CLAUDE.md.
54
- */
55
- export const detectMemorySources = ({
56
- cwd = process.cwd(),
57
- home = homedir(),
58
- explicitPath = null,
59
- } = {}) => {
60
- const candidates = [];
61
-
62
- if (explicitPath) {
63
- if (!existsSync(explicitPath)) {
64
- throw new Error(`No such file or directory: ${explicitPath}`);
65
- }
66
- if (statSync(explicitPath).isDirectory()) {
67
- for (const f of readdirSync(explicitPath).filter((n) => n.endsWith('.md')).sort()) {
68
- const c = fileCandidate(join(explicitPath, f), `from ${basename(explicitPath)}/`);
69
- if (c) candidates.push(c);
70
- }
71
- if (!candidates.length) {
72
- throw new Error(`No .md files found in ${explicitPath}`);
73
- }
74
- } else {
75
- const c = fileCandidate(explicitPath, 'explicit');
76
- if (!c) throw new Error(`${explicitPath} is empty or not a regular file`);
77
- candidates.push(c);
78
- }
79
- } else {
80
- const projectInstructions = fileCandidate(join(cwd, 'CLAUDE.md'), 'project instructions');
81
- if (projectInstructions) candidates.push(projectInstructions);
82
- const agentsMd = fileCandidate(join(cwd, 'AGENTS.md'), 'project instructions');
83
- if (agentsMd) candidates.push(agentsMd);
84
- const projectMemory = fileCandidate(join(cwd, 'MEMORY.md'), 'project memory');
85
- if (projectMemory) candidates.push(projectMemory);
86
-
87
- const memDir = claudeProjectMemoryDir(cwd, home);
88
- if (existsSync(memDir) && statSync(memDir).isDirectory()) {
89
- for (const f of readdirSync(memDir).filter((n) => n.endsWith('.md')).sort()) {
90
- const c = fileCandidate(join(memDir, f), 'auto-memory');
91
- if (c) candidates.push(c);
92
- }
93
- }
94
- }
95
-
96
- // Collapse symlink duplicates (first mention wins its label).
97
- const seen = new Set();
98
- return candidates.filter((c) => {
99
- let real;
100
- try {
101
- real = realpathSync(c.path);
102
- } catch {
103
- real = c.path;
104
- }
105
- if (seen.has(real)) return false;
106
- seen.add(real);
107
- return true;
108
- });
109
- };
110
-
111
- /** Compose the sources into one markdown document with per-file provenance. */
112
- export const composeImport = (sources) => {
113
- if (!sources.length) throw new Error('Nothing to import');
114
- const total = sources.reduce((n, s) => n + s.bytes, 0);
115
- if (total > MAX_IMPORT_BYTES) {
116
- throw new Error(
117
- `Import is ${Math.round(total / 1024)}KB — over the ${MAX_IMPORT_BYTES / 1024}KB ceiling. `
118
- + 'Pass an explicit --path to a smaller file, or trim the sources.',
119
- );
120
- }
121
- const parts = sources.map((s) => {
122
- const content = readFileSync(s.path, 'utf8').trim();
123
- return `## Imported: ${basename(s.path)} (${s.label})\n\n${content}`;
124
- });
125
- return `# Imported local memory\n\n${parts.join('\n\n---\n\n')}`;
126
- };
127
-
128
- /**
129
- * Promote the composed document into the agent's long_term section,
130
- * appending after any existing content. `client` is an api.js client
131
- * authenticated with the agent's runtime token.
132
- */
133
- export const importMemory = async (client, { sources }) => {
134
- const imported = composeImport(sources);
135
-
136
- let existing = '';
137
- try {
138
- const body = await client.get('/api/agents/runtime/memory');
139
- existing = body?.sections?.long_term?.content || '';
140
- } catch (err) {
141
- // Fresh agents 404 — that's the common case. Anything else should stop
142
- // the import: appending to memory we couldn't read risks clobbering it.
143
- if (err?.status && err.status !== 404) throw err;
144
- }
145
-
146
- const content = existing ? `${existing.trimEnd()}\n\n${imported}` : imported;
147
- await client.post('/api/agents/runtime/memory/sync', {
148
- mode: 'patch',
149
- sourceRuntime: SOURCE_RUNTIME,
150
- sections: {
151
- long_term: { content, visibility: 'private' },
152
- },
153
- });
154
-
155
- return { files: sources.length, bytes: content.length, appended: Boolean(existing) };
156
- };
@@ -1,31 +0,0 @@
1
- // F457: what a quick-try run says when the harness it needs isn't on PATH, and
2
- // the report it leaves first. The run exits before it ever checks in, so
3
- // without the report the connect page waits for it forever.
4
-
5
- import { NPM } from './cli-hint.js';
6
-
7
- // Only harnesses whose install command we have run ourselves get one here.
8
- const INSTALL = {
9
- claude: { label: 'Claude Code', command: `${NPM} i -g @anthropic-ai/claude-code`, signIn: 'claude' },
10
- codex: { label: 'Codex', command: `${NPM} i -g @openai/codex`, signIn: 'codex' },
11
- };
12
-
13
- /** One sentence on how to get the named harness, ending with "run this line again". */
14
- export const missingHarnessHint = (name) => {
15
- const known = INSTALL[name];
16
- if (!known) return `Install ${name}, then run this line again.`;
17
- return `Install ${known.label} (${known.command}), sign in once by running ${known.signIn}, then run this line again.`;
18
- };
19
-
20
- /**
21
- * Tell the instance what this run looked for (a harness id, or null for none
22
- * of the ones it probes). Best effort: an older instance without the route, or
23
- * a failed call, must not hide the message the person needs in the terminal.
24
- */
25
- export const reportMissingHarness = async (client, harness) => {
26
- try {
27
- await client?.post?.('/api/agents/runtime/harness-missing', { harness });
28
- } catch {
29
- // The terminal message below is the answer either way.
30
- }
31
- };
@@ -1,154 +0,0 @@
1
- /**
2
- * F178 — a turn the model provider refused because the model itself is not
3
- * available, and the one line the wrapper posts about it.
4
- *
5
- * Found on a Growth walk, 2026-11-05: a daemon seat with no server-side model
6
- * ran claude with a model the person's Anthropic key was not scoped for. Every
7
- * turn failed with "403 The requested model ... is not available for this API
8
- * key", the seat log said so, and the pod said nothing. The person saw a silent
9
- * agent and no way to fix it.
10
- *
11
- * The match is deliberately narrow. A 403 or 404 alone is not a model problem:
12
- * a disabled organization, a revoked key and a wrong route all answer 403/404,
13
- * and telling that person to change the model would send them the wrong way.
14
- * So a failure counts only when it carries one of these shapes:
15
- *
16
- * 1. claude / Anthropic 403: "... model ... is not available for this API key",
17
- * with the 403 attached as the status or on the phrase's own line
18
- * 2. OpenAI-style error code `model_not_found` (codex, opencode, LiteLLM), as
19
- * the structured `providerCode` adapterFailure keeps, not message text
20
- * 3. a 404 whose text is model-shaped, on the same line or in one JSON
21
- * error body: Anthropic's `not_found_error` body,
22
- * kept as "model: <id>" by the adapter's keep-list, or "The model `<id>`
23
- * does not exist", or "model <id> not found".
24
- *
25
- * A public seat's withheld-tool error quotes a TOOL's vendor text, not the
26
- * turn's own provider, so it never counts (public-tool-error.js).
27
- */
28
- import { PUBLIC_MCP_NOT_READY_CODE, PUBLIC_TOOL_FAILED_CODE } from './public-tool-error.js';
29
- import { CLI } from './cli-hint.js';
30
-
31
- /** How long one model's hint stays posted before the seat may post it again. */
32
- export const MODEL_UNAVAILABLE_HINT_WINDOW_MS = 60 * 60 * 1000;
33
-
34
- const NOT_AVAILABLE_FOR_KEY_RE = /\bmodel\b[^\n]{0,160}?\bis not available for (?:this|your) API key\b/i;
35
- // A status read out of text counts only where a status is written: at a line
36
- // start, just inside a bracket, or after an explicit HTTP, HTTP/1.1, HTTP status,
37
- // status, status_code, code, our own `upstream` label (adapterFailure's "(upstream 404)") or
38
- // claude's `API Error`, each with an optional `:`, `=`, `_` or `-`. It is never
39
- // followed by a word character, `.` or `-`. So `request_id=404`, `gpt-404-mini`
40
- // and `claude-403x` are not statuses.
41
- const statusRe = (code) => new RegExp('(?:^\\s*|[([{]\\s*|\\b(?:HTTP(?:/\\d(?:\\.\\d)?|\\s+status)?'
42
- + `|status(?:_?code)?|code|upstream|API\\s+Error)\\s*[:=_-]?\\s*)${code}(?![\\w.-])`, 'i');
43
- const STATUS_403_RE = statusRe(403);
44
- const MODEL_404_SHAPES = [
45
- /\bmodel:\s*[`'"]?[\w.:/@-]+/i,
46
- /\bthe model\b[^\n]{0,120}?\bdoes not exist\b/i,
47
- /\bmodel\b[^\n]{0,120}?\bnot found\b/i,
48
- ];
49
- const STATUS_404_RE = statusRe(404);
50
- const modelShaped = (text) => MODEL_404_SHAPES.some((pattern) => pattern.test(text));
51
-
52
- // A 404 counts only when the status and the model shape are one statement: the
53
- // same line, or one JSON error body. An unrelated 404 on one stderr line must
54
- // not be joined to a model mentioned on another.
55
- const sameLine404 = (text) => text.split('\n').some((line) => STATUS_404_RE.test(line) && modelShaped(line));
56
-
57
- const jsonBody404 = (text) => {
58
- const start = text.indexOf('{');
59
- const end = text.lastIndexOf('}');
60
- if (start < 0 || end <= start) return false;
61
- let parsed;
62
- try {
63
- parsed = JSON.parse(text.slice(start, end + 1));
64
- } catch {
65
- return false;
66
- }
67
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false;
68
- const error = parsed.error && typeof parsed.error === 'object' ? parsed.error : parsed;
69
- const statuses = [parsed.status, parsed.code, error.status, error.code];
70
- if (statuses.some((value) => Number(value) >= 500)) return false;
71
- const is404 = statuses.some((value) => String(value) === '404') || error.type === 'not_found_error';
72
- const message = error.message ?? parsed.message;
73
- return is404 && typeof message === 'string' && modelShaped(message);
74
- };
75
-
76
- // The model id, when the provider named it. Only this charset is ever echoed
77
- // into a pod, so provider prose and anything redacted (`[redacted]`) cannot be.
78
- const MODEL_NAME_RES = [
79
- /\brequested model\s+[`'"]?([\w.:/@-]+?)[`'"]?\s+is not available\b/i,
80
- /\bmodel:\s*[`'"]?([\w.:/@-]+)/i,
81
- /\bmodel\s+[`'"]([\w.:/@-]+)[`'"]/i,
82
- /\bmodel\s+([\w.:/@-]+)\s+(?:not found|does not exist)\b/i,
83
- ];
84
- // An id read out of prose must look like one ("is", "not" never do); a seat's
85
- // own configured model may be a bare alias such as `sonnet`.
86
- const ID_LIKE_RE = /[\d.:/-]/;
87
- const SAFE_MODEL_RE = /^(?=.*[A-Za-z])[\w.:/@-]{1,100}$/;
88
-
89
- const failureText = (error) => [error?.message, error?.stderr, error?.body?.error, error?.body?.message]
90
- .filter((value) => typeof value === 'string' && value)
91
- .join('\n');
92
-
93
- export const safeModelName = (value) => {
94
- const text = typeof value === 'string' ? value.trim() : '';
95
- // A leading `@` would read as a mention in the pod, and `://` as a link.
96
- if (text.startsWith('@') || text.includes('://')) return null;
97
- return SAFE_MODEL_RE.test(text) ? text : null;
98
- };
99
-
100
- /**
101
- * `{ model }` when the failure is a provider saying the model is unavailable,
102
- * else null. `model` is the id the provider named, then `fallbackModel` (the
103
- * seat's own setting), then null when neither is known.
104
- */
105
- export const modelUnavailableFailure = (error, { fallbackModel = null } = {}) => {
106
- if (!error || error.code === PUBLIC_TOOL_FAILED_CODE || error.code === PUBLIC_MCP_NOT_READY_CODE) return null;
107
- const text = failureText(error);
108
- if (!text) return null;
109
- // The key-scope phrase counts only on a 403: the attached status, or a 403 on
110
- // the phrase's own line. The provider's code counts only as the structured
111
- // field adapterFailure keeps, never as words in a message.
112
- // An attached status is the adapter's own reading of the failure, so it wins:
113
- // when it is set, only that status decides, and the text is not searched for
114
- // another one. Without it, the status must be written on the same line.
115
- const attached = typeof error.status === 'number' ? error.status : null;
116
- const lines = text.split('\n');
117
- const keyScope = NOT_AVAILABLE_FOR_KEY_RE.test(text) && (attached !== null ? attached === 403
118
- : lines.some((line) => STATUS_403_RE.test(line) && NOT_AVAILABLE_FOR_KEY_RE.test(line)));
119
- // The attached status reads the message, so its model shape must be on the
120
- // message's own first line, not in what later stderr happens to mention.
121
- const notFound = attached !== null
122
- ? attached === 404 && (modelShaped(String(error.message).split('\n')[0]) || jsonBody404(text))
123
- : sameLine404(text) || jsonBody404(text);
124
- const matched = keyScope
125
- || (error.providerCode === 'model_not_found' && !(attached >= 500))
126
- || notFound;
127
- if (!matched) return null;
128
- for (const pattern of MODEL_NAME_RES) {
129
- const named = safeModelName(pattern.exec(text)?.[1]);
130
- if (named && ID_LIKE_RE.test(named)) return { model: named };
131
- }
132
- return { model: safeModelName(fallbackModel) };
133
- };
134
-
135
- const CLI_AGENT_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
136
-
137
- /**
138
- * The one line posted in the pod. The model is set in the agent's Settings,
139
- * under the "How it runs" section, which starts collapsed (F354: a walk missed
140
- * it). `cliAgentName` is for a `commonly agent run` seat whose record has no
141
- * saved environment: Settings doesn't reach that seat until `agent config`
142
- * writes one, so the line names that command instead. A name that can't be
143
- * printed safely becomes `<name>`: Settings still wouldn't reach that seat.
144
- */
145
- export const modelUnavailableHint = (model, { cliAgentName = null } = {}) => {
146
- const what = model
147
- ? `The model provider says ${model} isn't available for this agent.`
148
- : "The model provider says this agent's model isn't available.";
149
- if (typeof cliAgentName !== 'string' || !cliAgentName) {
150
- return `${what} To change it, open the agent's Settings, then How it runs, then Model.`;
151
- }
152
- const name = CLI_AGENT_NAME_RE.test(cliAgentName) ? cliAgentName : '<name>';
153
- return `${what} To change it, run ${CLI} agent config ${name} --model <model> on the computer it runs on, then restart ${CLI} agent run ${name}.`;
154
- };
@@ -1,40 +0,0 @@
1
- import { spawnSync } from 'node:child_process';
2
- import { readFile, stat } from 'node:fs/promises';
3
- import { homedir, tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import { resolveWindowsCommandWithReason } from './windows-command.js';
6
-
7
- export const claudeEffortsFromHelp = (help) => {
8
- const section = String(help).match(/--effort\s+<[^>]+>([\s\S]*?)(?=\n\s+--|$)/)?.[1];
9
- const declared = section?.match(/\((low(?:\s*,\s*(?:medium|high|xhigh|max))*)\)/)?.[1];
10
- return declared ? declared.split(',').map((level) => level.trim()) : [];
11
- };
12
- export const codexModelsFromCache = (cache) => {
13
- const models = Array.isArray(cache?.models) ? cache.models.filter((row) => typeof row.slug === 'string'
14
- && row.slug.length > 0 && row.slug.length <= 256 && row.visibility !== 'hidden' && row.visibility !== 'hide') : [];
15
- const levels = models.map((row) => (Array.isArray(row.supported_reasoning_levels)
16
- ? row.supported_reasoning_levels.flatMap((level) => typeof level.effort === 'string' ? [level.effort] : []) : []));
17
- return { models: models.length ? models.map((row) => row.slug) : null,
18
- // Only levels supported by every advertised model are offered globally.
19
- efforts: levels.length ? levels[0].filter((level) => levels.every((list) => list.includes(level))) : [] };
20
- };
21
- export const claudeRuntimeInventory = async ({
22
- spawnSyncImpl = spawnSync, env = process.env, commandResolution = {},
23
- } = {}) => {
24
- // Windows: an npm `claude.cmd` is resolved to its target, never run through
25
- // a shell (#2141). Elsewhere this is `claude --help`, unchanged.
26
- const located = resolveWindowsCommandWithReason('claude', { env, ...commandResolution });
27
- if (!located.command) return { models: null, efforts: [] };
28
- const result = spawnSyncImpl(located.command, [...located.args, '--help'], {
29
- encoding: 'utf8', timeout: 5000, maxBuffer: 128 * 1024, cwd: tmpdir(), env,
30
- });
31
- return { models: null, efforts: result.status === 0 ? claudeEffortsFromHelp(result.stdout) : [] };
32
- };
33
- export const codexRuntimeInventory = async ({ readFileImpl = readFile, statImpl = stat, env = process.env } = {}) => {
34
- // Native cache only: no command that could execute workspace extensions.
35
- const path = join(env.CODEX_HOME || join(homedir(), '.codex'), 'models_cache.json');
36
- try {
37
- if ((await statImpl(path)).size > 2 * 1024 * 1024) return { models: null, efforts: [] };
38
- return codexModelsFromCache(JSON.parse(await readFileImpl(path, 'utf8')));
39
- } catch { return { models: null, efforts: [] }; }
40
- };
@@ -1,82 +0,0 @@
1
- /**
2
- * A confined seat that an outsider-writable grant reaches runs with only the
3
- * shipped MCP servers (docs/plans/hosted-mcp-connection-scope.md §12 C).
4
- *
5
- * While a grant whose source outsiders can write reaches one of a seat's pods,
6
- * the server narrows what the seat's token can do (§12 A). It cannot see the
7
- * MCP servers the seat loads on this machine, though. The daemon admits a
8
- * declared stdio entry that equals one in the local token record
9
- * (`auditDeclaredMcp`), so a server its owner installed locally can sit beside
10
- * the grant's results, and content an outsider wrote can steer it. So before
11
- * each turn, a confined seat that may be narrowed is refused while its MCP set
12
- * holds anything but the shipped Commonly server or a shipped grant broker.
13
- *
14
- * The effective set is the declared one. A confined seat's adapter excludes
15
- * user- and project-scope MCP config (claude `--strict-mcp-config`, codex
16
- * `--ignore-user-config`, isolated homes for the others), and
17
- * `outsider-grant-mcp.test.mjs` pins those flags, so a scope added to an
18
- * adapter fails that test before it widens this set.
19
- *
20
- * Only confined seats (`sandbox.trust` public or workspace, with the legacy
21
- * `internal` read as public) are checked. An
22
- * unconfined seat named on a grant runs on its owner's machine with a shell,
23
- * under the owner's consent that what it reads can leave, so a local MCP
24
- * server adds nothing it lacks (§12, the F94 amendment).
25
- */
26
- import { isShippedCommonlyMcpEntry, isShippedGrantBrokerEntry } from './declared-mcp-guard.js';
27
- import { effectiveSandboxTrust } from './environment.js';
28
-
29
- export const OUTSIDER_GRANT_MCP_UNCONFINED = 'outsider_grant_mcp_unconfined';
30
-
31
- const CONFINED_TRUSTS = new Set(['public', 'workspace']);
32
-
33
- // Read through the legacy table, as the broker guard and the adapters read it:
34
- // a record still carrying `internal` is confined as `public`, and is handed
35
- // the broker as a public seat, so it is checked as one.
36
- export const seatIsConfined = (environment) => CONFINED_TRUSTS.has(
37
- effectiveSandboxTrust(environment?.sandbox?.trust),
38
- );
39
-
40
- // The server flags a broker on a grant that names this seat (`byoNamed`). It is
41
- // a marker for the guard, not something the client reads, so it is set aside
42
- // before the whole-entry match, and only as exactly `true`.
43
- const withoutByoNamedFlag = (server) => {
44
- if (!server || typeof server !== 'object' || server.byoNamed !== true) return server;
45
- const { byoNamed: _byoNamed, ...rest } = server;
46
- return rest;
47
- };
48
-
49
- /** The entries of a turn's MCP set that are neither shipped server. */
50
- export const unshippedMcpEntries = (environment, instanceUrl) => {
51
- const servers = Array.isArray(environment?.mcp) ? environment.mcp : [];
52
- return servers.filter((server) => !isShippedCommonlyMcpEntry(server)
53
- && !isShippedGrantBrokerEntry(withoutByoNamedFlag(server), instanceUrl));
54
- };
55
-
56
- /**
57
- * Whether the server's answer leaves the seat possibly narrowed. Only an
58
- * explicit `outsiderGrantNarrowed: false` clears it. A server from before the
59
- * field, or an answer that lacks it, counts as narrowed, because the CLI
60
- * publishes before the server change is deployed.
61
- */
62
- export const mayBeNarrowed = (identity) => identity?.outsiderGrantNarrowed !== false;
63
-
64
- /**
65
- * @returns {null | { code: string, names: string[], message: string }}
66
- */
67
- export const outsiderGrantMcpRefusal = ({ environment, identity, instanceUrl }) => {
68
- if (!seatIsConfined(environment)) return null;
69
- const extra = unshippedMcpEntries(environment, instanceUrl);
70
- if (!extra.length || !mayBeNarrowed(identity)) return null;
71
- const names = extra.map((server) => String(server?.name || '(unnamed)'));
72
- const why = identity && 'outsiderGrantNarrowed' in identity
73
- ? 'a connector grant whose source outsiders can write reaches one of its pods'
74
- : 'the server did not say whether a connector grant reaches its pods, so it is treated as if one does';
75
- return {
76
- code: OUTSIDER_GRANT_MCP_UNCONFINED,
77
- names,
78
- message: `${OUTSIDER_GRANT_MCP_UNCONFINED}: this seat loads MCP servers beyond the shipped Space server `
79
- + `and grant broker (${names.join(', ')}), and ${why}. Remove them from the seat's environment, `
80
- + 'or use a separate agent for the connector pod.',
81
- };
82
- };