@ravefox/space 0.2.1 → 0.4.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 (167) hide show
  1. package/LICENSE +21 -205
  2. package/README.md +104 -31
  3. package/bin/space.js +113 -0
  4. package/lib/agents.js +181 -0
  5. package/lib/boardTools.js +89 -0
  6. package/lib/credentials.js +95 -0
  7. package/lib/daemon.js +294 -0
  8. package/lib/harness.js +166 -0
  9. package/lib/http.js +101 -0
  10. package/lib/login.js +68 -0
  11. package/lib/mcp.js +39 -0
  12. package/lib/runtime.js +253 -0
  13. package/lib/store.js +96 -0
  14. package/lib/tools.js +93 -0
  15. package/lib/workspaceSandbox.js +77 -0
  16. package/lib/workspaceTools.js +160 -0
  17. package/lib/workspaceWorker.js +17 -0
  18. package/package.json +27 -46
  19. package/NOTICE +0 -55
  20. package/assets/webhook-sdk/python/bot.py +0 -63
  21. package/assets/webhook-sdk/python/commonly.py +0 -232
  22. package/node_modules/@commonlyai/seats/README.md +0 -25
  23. package/node_modules/@commonlyai/seats/package.json +0 -22
  24. package/node_modules/@commonlyai/seats/src/adapters/hermes.js +0 -264
  25. package/node_modules/@commonlyai/seats/src/adapters/stub.js +0 -24
  26. package/node_modules/@commonlyai/seats/src/contract.js +0 -48
  27. package/node_modules/@commonlyai/seats/src/failures.js +0 -44
  28. package/node_modules/@commonlyai/seats/src/harness-plan.js +0 -329
  29. package/node_modules/@commonlyai/seats/src/index.js +0 -22
  30. package/node_modules/@commonlyai/seats/src/process-group.js +0 -269
  31. package/node_modules/@commonlyai/seats/src/record/format.js +0 -737
  32. package/node_modules/@commonlyai/seats/src/record/otel.js +0 -238
  33. package/node_modules/@commonlyai/seats/src/runtime-observation.js +0 -29
  34. package/node_modules/@commonlyai/seats/src/seat-model.js +0 -17
  35. package/node_modules/@commonlyai/seats/src/supervisor.js +0 -308
  36. package/node_modules/@commonlyai/seats/src/trace/events.js +0 -46
  37. package/node_modules/@commonlyai/seats/src/trace/store.js +0 -276
  38. package/node_modules/@commonlyai/seats/src/turn-processes.js +0 -226
  39. package/node_modules/@commonlyai/seats/src/upstream-refusal.js +0 -418
  40. package/node_modules/@commonlyai/seats/src/valet/env.js +0 -201
  41. package/node_modules/@commonlyai/seats/src/valet/grant.js +0 -123
  42. package/node_modules/@commonlyai/seats/src/valet/permissions.js +0 -213
  43. package/node_modules/@commonlyai/seats/src/wire/provider-proxy.js +0 -529
  44. package/skills/commonly/SKILL.md +0 -233
  45. package/src/commands/agent.js +0 -6334
  46. package/src/commands/daemon.js +0 -933
  47. package/src/commands/dev.js +0 -153
  48. package/src/commands/doctor.js +0 -141
  49. package/src/commands/inbox.js +0 -330
  50. package/src/commands/login.js +0 -194
  51. package/src/commands/mcp.js +0 -56
  52. package/src/commands/pod.js +0 -120
  53. package/src/commands/task.js +0 -279
  54. package/src/commands/verify.js +0 -48
  55. package/src/index.js +0 -99
  56. package/src/lib/adapter-login.js +0 -76
  57. package/src/lib/adapters/broker-bridge-entry.js +0 -76
  58. package/src/lib/adapters/claude.js +0 -2207
  59. package/src/lib/adapters/codex-broker-bridge.mjs +0 -298
  60. package/src/lib/adapters/codex.js +0 -1898
  61. package/src/lib/adapters/dsh.js +0 -513
  62. package/src/lib/adapters/goose.js +0 -510
  63. package/src/lib/adapters/hermes.js +0 -18
  64. package/src/lib/adapters/index.js +0 -68
  65. package/src/lib/adapters/opencode.js +0 -1681
  66. package/src/lib/adapters/pi-commonly-mcp.mjs +0 -71
  67. package/src/lib/adapters/pi-mcp-client.mjs +0 -617
  68. package/src/lib/adapters/pi-shell-env.mjs +0 -41
  69. package/src/lib/adapters/pi.js +0 -1025
  70. package/src/lib/adapters/qwen.js +0 -523
  71. package/src/lib/adapters/seat-model.js +0 -8
  72. package/src/lib/adapters/stub.js +0 -1
  73. package/src/lib/adapters/tested-version.js +0 -106
  74. package/src/lib/api.js +0 -205
  75. package/src/lib/claim-outcome.js +0 -128
  76. package/src/lib/claimed-work-gate.js +0 -73
  77. package/src/lib/cli-hint.js +0 -33
  78. package/src/lib/commit-attribution.js +0 -391
  79. package/src/lib/config.js +0 -155
  80. package/src/lib/content-refusal.js +0 -94
  81. package/src/lib/credential-file.js +0 -208
  82. package/src/lib/daemon-agent-states.js +0 -95
  83. package/src/lib/daemon-hint.js +0 -18
  84. package/src/lib/daemon-logs.js +0 -43
  85. package/src/lib/daemon-service-windows.js +0 -263
  86. package/src/lib/daemon-service.js +0 -486
  87. package/src/lib/daemon-state.js +0 -84
  88. package/src/lib/daemon-store.js +0 -119
  89. package/src/lib/daemon-supervisor.js +0 -629
  90. package/src/lib/daemon-watch.js +0 -115
  91. package/src/lib/declared-mcp-guard.js +0 -284
  92. package/src/lib/default-environment.js +0 -207
  93. package/src/lib/device-login.js +0 -126
  94. package/src/lib/digest-frame.js +0 -151
  95. package/src/lib/doctor.js +0 -291
  96. package/src/lib/egress-proxy.js +0 -160
  97. package/src/lib/enforcement.js +0 -946
  98. package/src/lib/environment.js +0 -681
  99. package/src/lib/first-output-watchdog.js +0 -191
  100. package/src/lib/git-readonly-server.js +0 -138
  101. package/src/lib/git-readonly.js +0 -463
  102. package/src/lib/git-remote-commonly.mjs +0 -100
  103. package/src/lib/grant-broker-guard.js +0 -345
  104. package/src/lib/hooks-config.js +0 -238
  105. package/src/lib/host-gh-broker.js +0 -152
  106. package/src/lib/host-gh-shim.mjs +0 -98
  107. package/src/lib/inbox.js +0 -483
  108. package/src/lib/instance-proxy.js +0 -76
  109. package/src/lib/machine-host.js +0 -29
  110. package/src/lib/mcp-credential-delivery.js +0 -121
  111. package/src/lib/mcp-first-run.js +0 -50
  112. package/src/lib/mcp-home.js +0 -644
  113. package/src/lib/mcp-ready.js +0 -68
  114. package/src/lib/mcp-server-version.js +0 -123
  115. package/src/lib/mcp-token.js +0 -36
  116. package/src/lib/mcp-warm-child.mjs +0 -412
  117. package/src/lib/memory-bridge.js +0 -191
  118. package/src/lib/memory-import.js +0 -156
  119. package/src/lib/missing-harness.js +0 -31
  120. package/src/lib/model-unavailable.js +0 -154
  121. package/src/lib/native-runtime-inventory.js +0 -40
  122. package/src/lib/outsider-grant-mcp.js +0 -82
  123. package/src/lib/own-key-launch.js +0 -42
  124. package/src/lib/pod-catch-up.js +0 -55
  125. package/src/lib/pod-focus.js +0 -224
  126. package/src/lib/pod-tail.js +0 -68
  127. package/src/lib/poll-liveness.js +0 -103
  128. package/src/lib/poll-retry.js +0 -81
  129. package/src/lib/poller.js +0 -200
  130. package/src/lib/provider-proxy.js +0 -42
  131. package/src/lib/public-seat-mcp-flag.js +0 -36
  132. package/src/lib/public-tool-error.js +0 -60
  133. package/src/lib/public-withheld-tools.js +0 -48
  134. package/src/lib/reply-receipt.js +0 -55
  135. package/src/lib/resolve-on-path.js +0 -22
  136. package/src/lib/runtime-inventory.js +0 -56
  137. package/src/lib/runtime-observation.js +0 -37
  138. package/src/lib/sandbox/bwrap.js +0 -275
  139. package/src/lib/sandbox/codex-perimeter.js +0 -16
  140. package/src/lib/sandbox/host-check.js +0 -268
  141. package/src/lib/sandbox/mode.js +0 -49
  142. package/src/lib/sandbox/opencode-loopback.mjs +0 -68
  143. package/src/lib/sandbox/seatbelt.js +0 -684
  144. package/src/lib/sandbox/turn-dir.js +0 -56
  145. package/src/lib/sandbox/windows-unsandboxed.js +0 -183
  146. package/src/lib/sandbox/workspace-git-config.js +0 -243
  147. package/src/lib/sandbox/workspace-pin.js +0 -158
  148. package/src/lib/seat-runtime-controls.js +0 -61
  149. package/src/lib/seat-stop.js +0 -97
  150. package/src/lib/server-skills.js +0 -32
  151. package/src/lib/session-recycle.js +0 -219
  152. package/src/lib/session-store.js +0 -215
  153. package/src/lib/skill-prompt.js +0 -74
  154. package/src/lib/skills-import.js +0 -113
  155. package/src/lib/spawn-credential.js +0 -402
  156. package/src/lib/spawn-retry.js +0 -311
  157. package/src/lib/stable-install.js +0 -176
  158. package/src/lib/supervised-turn.js +0 -352
  159. package/src/lib/task.js +0 -235
  160. package/src/lib/turn-failure-notice.js +0 -177
  161. package/src/lib/unconfined-seat-guard.js +0 -94
  162. package/src/lib/update-check.js +0 -192
  163. package/src/lib/upstream-refusal.js +0 -1
  164. package/src/lib/webhook-server.js +0 -109
  165. package/src/lib/windows-command.js +0 -393
  166. package/src/lib/workspace-git.js +0 -87
  167. package/src/lib/workspace-trust.js +0 -162
@@ -1,177 +0,0 @@
1
- /**
2
- * A seat that can't run a turn goes quiet (2026-11-06).
3
- *
4
- * The turn fails, the run loop opens the circuit and retries in 15 minutes, and
5
- * the only trace is a line in a terminal the person in the pod may not own.
6
- * They see their @mention go unanswered, with no reason. The wrapper, not the
7
- * model, posts one plain line instead, since no model turn can run. It names
8
- * the cause and who can fix it. It never quotes the error text, which is vendor
9
- * output and can carry key material.
10
- *
11
- * The failure classes in spawn-retry.js are too loose for this. They only pick a
12
- * backoff, so a stray "401" or "billing" in a stack trace costs a slower retry.
13
- * Here the match becomes a statement to a person, so each line below fires only
14
- * on a wording a seat has logged, a walk has produced, a vendor publishes, or
15
- * the wrapper or our own gateway config writes. A failure that matches none of
16
- * them posts nothing, which is what it did before.
17
- */
18
- import { PUBLIC_MCP_NOT_READY_CODE, PUBLIC_TOOL_FAILED_CODE } from './public-tool-error.js';
19
- import { PUBLIC_CODEX_VERSION_CODE } from './adapters/tested-version.js';
20
- import { classifySpawnFailure, GATEWAY_BUDGET_RE, SPAWN_FAILURE_CLASS } from './spawn-retry.js';
21
- import { modelUnavailableFailure } from './model-unavailable.js';
22
-
23
- /** How long one cause stays posted in a pod before the seat may post it again. */
24
- export const CANT_ANSWER_NOTICE_WINDOW_MS = 60 * 60 * 1000;
25
-
26
- const LOGIN_FAILED_WORDINGS = [
27
- // Not `(upstream 401)` alone: adapterFailure writes that for any "status 401"
28
- // in a tail, including a tool's stderr. A failed Codex turn is its own report.
29
- /^codex turn failed \(upstream 401\)/,
30
- /\bupstream refused 401\b/, // the same refusal, read from pi's stream
31
- /^\w+ exited(?: with code)? \d+: 401\b/, // pi's text, which names no upstream
32
- /workspace routing discovery unauthorized/,
33
- // Any other text carrying Claude Code's wording keeps this general line.
34
- /(?:^|: | \| )Not logged in · Please run \/login/,
35
- /API Error: 400 This API key is not scoped to a workspace/,
36
- // The wrapper's own refusals of a Claude login (adapters/claude.js).
37
- /the Claude login on this machine expires in/,
38
- /refusing a Claude login/,
39
- ];
40
- // F436: what Claude Code prints, and exits 1 on, when it has no login on the
41
- // computer (2.1.318, walked on windows-latest). No key is wrong and no allowance
42
- // is spent: someone signs in to Claude Code there, and the next turn works.
43
- // Anchored on the claude adapter's own exit line, first in its tail or after
44
- // the stderr | stdout join, because this match acks the message unanswered:
45
- // the same words from another adapter or a tool keep the general login line.
46
- const CLAUDE_SIGNED_OUT_WORDINGS = [
47
- /^claude exited(?: with code)? \d+: (?:[^\n]*? \| )?Not logged in · Please run \/login/,
48
- ];
49
- const WONT_START_WORDINGS = [
50
- /^spawn \S+ ENOENT/,
51
- /^\w+ adapter: \S+ (?:is not set|is missing)/, // the seat has no provider key
52
- ];
53
- // The vendors' credit wordings open at the adapter's own label or the claude
54
- // adapter's stderr | stdout join, because any `: ` or status number can open a
55
- // log line. An unknown label stays silent. `upstream refused` is the line the run
56
- // loop builds for pi, and the pi adapter puts its label before it when a refused
57
- // turn exits 0 after an interim message (F320). The transports are how a CLI
58
- // wraps a refusal it keeps whole.
59
- const ADAPTER_LABEL = String.raw`(?:claude|codex|pi|opencode|goose|qwen|dsh|Hermes)(?: turn failed)?(?: exited(?: with code)? \d+)?(?: \(upstream \d{3}\))?`;
60
- const OWN_LEAD = String.raw`(?:^(?:${ADAPTER_LABEL}(?:: upstream refused \d{3})?|upstream refused \d{3}): | \| )`;
61
- const TRANSPORT = String.raw`(?:unexpected status \d{3}[^:\n]*: |API Error: \d{3} |Error code: \d{3} - |Error running remote compact task: )*`;
62
- const OPENAI_CODE = '(?:insufficient_quota|credit_balance_exhausted)';
63
- const ownWording = (wording) => new RegExp(`${OWN_LEAD}${TRANSPORT}${wording}`);
64
-
65
- // Not the whole of QUOTA_RE, which also holds "billing" and "quota" for backoff.
66
- const OUT_OF_ALLOWANCE_WORDINGS = [
67
- /(?:^|: | \| )you['’]?ve (?:hit|reached) your [^\n]{0,40}?limit/i,
68
- /\bout of credits\b/i,
69
- GATEWAY_BUDGET_RE,
70
- // An API key with no credit left, in the vendors' published text, case-sensitive.
71
- // OpenAI's API (openai-cookbook error-codes, codex tests/suite/quota_exceeded.rs).
72
- ownWording('You exceeded your current quota, please check your plan and billing details'),
73
- // codex-rs source maps insufficient_quota, credit_balance_exhausted and the
74
- // spend-limit codes to this text (CodexErr::QuotaExceeded). Not run on a binary.
75
- ownWording(String.raw`Quota exceeded\. Check your plan and billing details`),
76
- // OpenAI's error.type and error.code (guides/error-codes): bare, or in a body kept whole.
77
- ownWording(String.raw`(?:\d{3} )?${OPENAI_CODE}\b`),
78
- ownWording(String.raw`\{['"]error['"]: ?\{[^{}\n]{0,400}?['"](?:code|type)['"]: ?['"]${OPENAI_CODE}['"]`),
79
- // Anthropic's API (about-claude/api-credits-for-subscribers): reduced, or in its body.
80
- ownWording(String.raw`(?:\{"type":"error","error":\{"type":"invalid_request_error","message":")?Your credit balance is too low to access the Anthropic API`),
81
- // Claude Code: the same page, and its errors page.
82
- ownWording(String.raw`Credit balance (?:is )?too low(?: ·| \||\n|$)`),
83
- ];
84
-
85
- const AGAIN = 'After that, send your message again if I still haven’t answered.';
86
-
87
- export const LOGIN_NOTICE = 'I can’t answer right now: the key or login on the computer I run on isn’t working. '
88
- + `Whoever runs me needs to fix it and start me again. ${AGAIN}`;
89
- export const CLAUDE_SIGNED_OUT_NOTICE = 'I can’t answer right now: Claude Code isn’t signed in on the computer I run on. '
90
- + `Whoever runs me needs to open Claude Code there and sign in. ${AGAIN}`;
91
- // What the seat's own log says, for whoever runs it.
92
- export const CLAUDE_SIGNED_OUT_FIX = 'Claude Code is not signed in on this computer: open Claude Code here and sign in '
93
- + '(/login). The waiting message was acked, not held, so the next message starts a turn at once.';
94
- export const WONT_START_NOTICE = 'I can’t answer right now: the tool I run on wouldn’t start on my computer. '
95
- + `Whoever runs me needs to fix that and start me again. ${AGAIN}`;
96
- export const OUT_OF_ALLOWANCE_NOTICE = 'I can’t answer right now: the AI account I run on has used up its allowance. '
97
- + `It needs to reset, or whoever runs me needs to add more. ${AGAIN}`;
98
-
99
- // A turn that had posted, whose event the server had settled, and that then
100
- // stopped. The event won't come back, so this is all the person hears. Hedged,
101
- // because a kill can land after the last word: a checkpoint says the seat
102
- // finished an item, and part one of an answer has been enough to earn one.
103
- // The kernel exempts this exact text from its duplicate guard: change both
104
- // (CUT_OFF_LINE in agentMessageService.ts, pinned by a test).
105
- export const CUT_OFF_NOTICE = 'I got cut off before I could finish that, so please ask me again if anything is missing.';
106
-
107
- // F421: a public seat whose Commonly tools never came up. Its model would have
108
- // answered without reading the pod, so the wrapper withholds that answer and
109
- // says this instead; the event stays unacked and is answered once the tools work.
110
- export const TOOLS_UNAVAILABLE_NOTICE = 'I can’t answer right now: the tools I use to read and post here '
111
- + `aren’t working on my computer. Whoever runs me needs to fix that. ${AGAIN}`;
112
-
113
- export const codexVersionNotice = (required) => 'I can’t answer right now: the computer I run on needs '
114
- + `Codex ${required} or newer. Whoever runs me needs to upgrade it and start me again. ${AGAIN}`;
115
-
116
- const matches = (wordings, text) => wordings.some((wording) => wording.test(text));
117
-
118
- /** The cause a pod should hear about, and its line, or null when it should hear nothing. */
119
- export const cantAnswerNotice = (error) => {
120
- // A public seat's withheld turn quotes a TOOL's error, not its own provider's.
121
- // A tool that failed mid-turn usually means Commonly itself was unreachable,
122
- // so a line could not reach the pod either; it stays silent and retries.
123
- if (error?.code === PUBLIC_TOOL_FAILED_CODE) return null;
124
- // Tools that never started leave the API reachable, and a stranger waiting.
125
- if (error?.code === PUBLIC_MCP_NOT_READY_CODE) return { cause: 'tools-unavailable', text: TOOLS_UNAVAILABLE_NOTICE };
126
- if (error?.code === PUBLIC_CODEX_VERSION_CODE) {
127
- return { cause: 'codex-version', text: codexVersionNotice(error.requiredVersion) };
128
- }
129
- // claudeFailureTail drops tool and assistant JSON. This cut is for what can
130
- // still arrive: a torn stream line, or an error event after stderr.
131
- const text = String(error?.message ?? '').split(/(?:^|: | \| |\n)\{"type":/)[0];
132
- // Before the general login line: this one names its own fix.
133
- if (matches(CLAUDE_SIGNED_OUT_WORDINGS, text)) return { cause: 'claude-signed-out', text: CLAUDE_SIGNED_OUT_NOTICE };
134
- // Before the allowance check: a 401 is settled, whatever else its body says.
135
- if (matches(LOGIN_FAILED_WORDINGS, text)) return { cause: 'login', text: LOGIN_NOTICE };
136
- if (matches(WONT_START_WORDINGS, text)) return { cause: 'wont-start', text: WONT_START_NOTICE };
137
- if (classifySpawnFailure(error) === SPAWN_FAILURE_CLASS.QUOTA && matches(OUT_OF_ALLOWANCE_WORDINGS, text)) {
138
- return { cause: 'out-of-allowance', text: OUT_OF_ALLOWANCE_NOTICE };
139
- }
140
- return null;
141
- };
142
-
143
- // F370 rough spot #11: Settings said "spawn failed" and nothing more; the cause
144
- // was only in the seat's log. The runner reports one of these fixed codes with
145
- // a failed outcome, never the error text. The server stores the code alone.
146
- export const SPAWN_FAILURE_REASONS = [
147
- 'claude-signed-out', 'login', 'wont-start', 'out-of-allowance', 'tools-unavailable', 'codex-version', 'model-unavailable',
148
- ];
149
- // A shell's "found but can't run it" exit, on the adapter's own exit line.
150
- const NOT_EXECUTABLE_EXIT = /^\w+ exited(?: with code)? 126\b/;
151
-
152
- /** The failed spawn's reason code, or null when no known cause matches. */
153
- export const spawnFailureReason = (error) => {
154
- if (!error) return null;
155
- if (modelUnavailableFailure(error)) return 'model-unavailable';
156
- const cause = cantAnswerNotice(error)?.cause;
157
- if (SPAWN_FAILURE_REASONS.includes(cause)) return cause;
158
- return NOT_EXECUTABLE_EXIT.test(String(error.message ?? '')) ? 'wont-start' : null;
159
- };
160
-
161
- /**
162
- * A turn that failed because Claude Code is signed out on this computer (F436).
163
- * It is a setup fault, not an outage: retrying cannot fix it, and holding the
164
- * event behind the 15-minute circuit kept the person waiting with no answer.
165
- * The run loop acks it instead and posts CLAUDE_SIGNED_OUT_NOTICE, and the
166
- * next event spawns at once, which is the re-probe.
167
- */
168
- export const isClaudeSignedOut = (error) => cantAnswerNotice(error)?.cause === 'claude-signed-out';
169
-
170
- /** The turn result a signed-out turn is acked with: a person's message is handed to another listener. */
171
- export const CLAUDE_SIGNED_OUT_REASON = 'claude-signed-out';
172
- export const claudeSignedOutResult = () => ({
173
- outcome: 'error',
174
- refused: { reason: 'delivery-refused' },
175
- reason: CLAUDE_SIGNED_OUT_REASON,
176
- details: { failureClass: 'configuration' },
177
- });
@@ -1,94 +0,0 @@
1
- /**
2
- * A seat with no sandbox answers only in pods confirmed private.
3
- *
4
- * An agent the adapters do not confine runs as its owner: their files, their
5
- * git and GitHub login, their shell. Anyone who can post in its pod can direct
6
- * it. Attach already refused a publicly readable pod, but not one anyone can
7
- * join, and nothing looked again once it ran, so a pod made joinable later
8
- * handed that access to whoever joined.
9
- *
10
- * So attach requires such a seat's pod to be confirmed private (publicRead and
11
- * communityListed reported false, joinPolicy invite-only:
12
- * workspace-trust.js `podIsConfirmedPrivate`), and before every turn the seat
13
- * re-reads its pods (GET /installations) and refuses the turn unless all of
14
- * them still are. There is no confined fallback: the seat's record was never
15
- * written for one, so a pod that is not private is a refused turn, and a read
16
- * that fails defers it. The Windows Codex opt-in keeps its own placement
17
- * (`unconfinedTurnPlacement`); this covers every other unconfined seat.
18
- *
19
- * "Confined" is what the adapters enforce, not what the file declares: a public
20
- * or workspace trust, or a shape one adapter enforces with no trust (claude's
21
- * `bwrap` mode, dsh's `isConfinedDsh` shape). Any other `sandbox.mode` with no
22
- * trust is spawned bare (claude, codex) or refused at spawn, so it is
23
- * unconfined here, and `mode: 'none'` always is.
24
- *
25
- * A legacy pod with no joinPolicy is reported 'open', and that is not only a
26
- * default: an agent may self-install into a pod an agent created unless it is
27
- * invite-only, so a missing value is not confirmed private.
28
- *
29
- * The test-only `stub` adapter runs nothing and is not checked.
30
- */
31
- import { normalizeSandboxTrust } from './environment.js';
32
- import { podIsConfirmedPrivate } from './workspace-trust.js';
33
-
34
- export const UNCONFINED_UNCHECKED_ADAPTERS = new Set(['stub']);
35
-
36
- // adapters/dsh.js isConfinedDsh's accepted shape, copied so this module stays
37
- // free of adapter imports; a test holds the two equal.
38
- const isConfinedDshShape = (sandbox) => {
39
- if (!sandbox || typeof sandbox !== 'object' || sandbox.trust !== undefined) return false;
40
- const { mode, network, ...rest } = sandbox;
41
- return mode === 'workspace' && network?.policy === 'restricted'
42
- && Object.keys(network).length === 1 && Object.keys(rest).length === 0;
43
- };
44
-
45
- // Confinement an adapter applies with no trust: claude wraps any `bwrap` mode
46
- // (adapters/claude.js), and dsh enforces its one shape. codex, opencode and the
47
- // rest apply a mode only under a public or workspace trust.
48
- const ADAPTER_OWN_CONFINEMENT = {
49
- claude: (sandbox) => sandbox?.mode === 'bwrap',
50
- dsh: isConfinedDshShape,
51
- };
52
-
53
- export const seatRunsUnconfined = ({ environment, adapterName }) => {
54
- if (UNCONFINED_UNCHECKED_ADAPTERS.has(adapterName)) return false;
55
- const sandbox = normalizeSandboxTrust(environment?.sandbox);
56
- // An explicit 'none' is no sandbox whatever the trust says.
57
- if (sandbox?.mode === 'none') return true;
58
- if (sandbox?.trust === 'public' || sandbox?.trust === 'workspace') return false;
59
- return !ADAPTER_OWN_CONFINEMENT[adapterName]?.(sandbox);
60
- };
61
-
62
- export const UNCONFINED_TURN_REFUSAL = 'This agent runs without a sandbox, with your own access to files, '
63
- + 'commands and logins, so it answers only while every pod it is in is private and invite-only. '
64
- + 'To fix it, the pod\'s owner opens it in Space and sets "Who can join" to Invite-only in its side '
65
- + 'panel (and makes it private if it is public or listed in Community), or you remove this agent from that pod.';
66
-
67
- const MAX_PODS_NAMED = 3;
68
-
69
- const describePod = (pod) => {
70
- const label = pod?.podName ? `"${pod.podName}" (${pod.podId})` : `pod ${pod?.podId || '(no id)'}`;
71
- if (typeof pod?.publicRead !== 'boolean' || typeof pod?.communityListed !== 'boolean') {
72
- return `${label} did not report whether it is private`;
73
- }
74
- if (pod.publicRead || pod.communityListed) return `${label} is public or listed in Community`;
75
- return `${label} is open to join`;
76
- };
77
-
78
- /** Null when every pod is confirmed private, otherwise why the turn is refused, naming the pods. */
79
- export const unconfinedPodRefusalReason = (installations) => {
80
- if (installations === null) return 'its pods could not be read';
81
- if (!Array.isArray(installations) || installations.length === 0) return 'the server listed no pods for it';
82
- const seen = new Set();
83
- const notPrivate = installations.filter((pod) => {
84
- if (podIsConfirmedPrivate(pod)) return false;
85
- const key = String(pod?.podId || '');
86
- if (seen.has(key)) return false;
87
- seen.add(key);
88
- return true;
89
- });
90
- if (!notPrivate.length) return null;
91
- const named = notPrivate.slice(0, MAX_PODS_NAMED).map(describePod).join('; ');
92
- const more = notPrivate.length - MAX_PODS_NAMED;
93
- return more > 0 ? `${named}; and ${more} more` : named;
94
- };
@@ -1,192 +0,0 @@
1
- /**
2
- * "Your CLI is N versions behind" — one line at `agent run` and `daemon run`
3
- * start-up (TASK-660, D213).
4
- *
5
- * A seat pinned to an old CLI fails in ways that look like platform bugs, and
6
- * nothing on the seat said it was old. This prints one line when the installed
7
- * CLI trails npm latest by UPDATE_NOTICE_MIN_BEHIND or more, and is otherwise
8
- * silent: offline, a slow registry, a malformed answer and
9
- * COMMONLY_NO_UPDATE_CHECK=1 all print nothing.
10
- *
11
- * It must never slow start-up. `startUpdateNotice` returns at once; the lookup
12
- * runs beside the poll loop, is capped at UPDATE_TIMEOUT_MS by its own timer
13
- * (a fetch that ignores its abort signal still cannot hold it), and is cached
14
- * for UPDATE_CACHE_TTL_MS so a restarting seat asks the registry once a day.
15
- */
16
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
17
- import { homedir } from 'node:os';
18
- import { dirname, join } from 'node:path';
19
- import { exactVersion, parseVersion } from './mcp-server-version.js';
20
- import { DAEMON_SETUP_LINE, runsFromStableInstall } from './stable-install.js';
21
- import { NPM } from './cli-hint.js';
22
-
23
- export const CLI_PACKAGE = '@ravefox/space';
24
- export const REGISTRY_LATEST_URL = 'https://registry.npmjs.org/@ravefox%2fspace/latest';
25
- export const UPDATE_CHECK_OPT_OUT_ENV = 'COMMONLY_NO_UPDATE_CHECK';
26
- export const UPDATE_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
27
- export const UPDATE_TIMEOUT_MS = 2000;
28
- // The CLI publishes often, so one or two releases behind is the normal gap
29
- // between a publish and a seat's next restart. Three is where it is stale.
30
- export const UPDATE_NOTICE_MIN_BEHIND = 3;
31
-
32
- export const updateCachePath = (home = homedir()) => join(home, '.commonly', 'update-check.json');
33
-
34
- /**
35
- * How far `installed` trails `latest`: 0 when equal or ahead, the patch gap
36
- * within one major.minor, and Infinity when latest is a newer minor or major
37
- * (the patch numbers of two release lines say nothing about the distance).
38
- * null when either side is not a version.
39
- */
40
- export const versionsBehind = (installed, latest) => {
41
- const a = parseVersion(installed);
42
- const b = parseVersion(latest);
43
- if (!a || !b) return null;
44
- if (a[0] !== b[0] || a[1] !== b[1]) {
45
- return (b[0] > a[0] || (b[0] === a[0] && b[1] > a[1])) ? Infinity : 0;
46
- }
47
- return Math.max(0, b[2] - a[2]);
48
- };
49
-
50
- /** The one line to print, or null when there is nothing worth saying. */
51
- export const updateNoticeLine = ({
52
- installed, latest, minBehind = UPDATE_NOTICE_MIN_BEHIND, fromSetup = runsFromStableInstall(),
53
- }) => {
54
- const behind = versionsBehind(installed, latest);
55
- if (behind === null || behind < minBehind) return null;
56
- // Only the parsed triple is printed: the registry's string never reaches the log.
57
- const from = exactVersion(installed);
58
- const to = exactVersion(latest);
59
- const gap = Number.isFinite(behind) ? `${behind} versions behind` : 'behind a newer release line';
60
- // A `daemon setup` copy lives under $HOME and has no global install to update;
61
- // running setup again updates it and restarts the daemon.
62
- const update = fromSetup ? DAEMON_SETUP_LINE : `${NPM} install -g ${CLI_PACKAGE}@latest`;
63
- return `your CLI is ${gap} (installed ${from}, latest ${to}). Update: ${update}`
64
- + ` (silence with ${UPDATE_CHECK_OPT_OUT_ENV}=1)`;
65
- };
66
-
67
- const defaultReadText = (path) => (existsSync(path) ? readFileSync(path, 'utf8') : null);
68
- const defaultWriteText = (path, text) => {
69
- mkdirSync(dirname(path), { recursive: true });
70
- writeFileSync(path, text, { encoding: 'utf8', mode: 0o600 });
71
- };
72
-
73
- /** The cached latest version when the cache is younger than the TTL, else null. */
74
- export const readCachedLatest = ({ cachePath, now, readText = defaultReadText, ttlMs = UPDATE_CACHE_TTL_MS }) => {
75
- let raw;
76
- try {
77
- raw = readText(cachePath);
78
- } catch {
79
- return null;
80
- }
81
- if (!raw) return null;
82
- try {
83
- const cache = JSON.parse(raw);
84
- const checkedAt = Date.parse(cache?.checkedAt);
85
- const latest = exactVersion(cache?.latest);
86
- if (!Number.isFinite(checkedAt) || !latest) return null;
87
- if (now - checkedAt < 0 || now - checkedAt >= ttlMs) return null;
88
- return latest;
89
- } catch {
90
- return null;
91
- }
92
- };
93
-
94
- /**
95
- * npm latest, or null on any failure. The timer is the cap, not the signal:
96
- * the signal asks fetch to stop, the race guarantees the answer is on time.
97
- */
98
- export const fetchLatestVersion = async ({
99
- fetchImpl = globalThis.fetch,
100
- timeoutMs = UPDATE_TIMEOUT_MS,
101
- url = REGISTRY_LATEST_URL,
102
- } = {}) => {
103
- if (typeof fetchImpl !== 'function') return null;
104
- const controller = new AbortController();
105
- let timer;
106
- const deadline = new Promise((resolve) => {
107
- timer = setTimeout(() => {
108
- controller.abort();
109
- resolve(null);
110
- }, timeoutMs);
111
- timer.unref?.();
112
- });
113
- const lookup = (async () => {
114
- const res = await fetchImpl(url, { signal: controller.signal, headers: { accept: 'application/json' } });
115
- if (!res?.ok) return null;
116
- const body = await res.json();
117
- return exactVersion(body?.version);
118
- })().catch(() => null);
119
- try {
120
- return await Promise.race([lookup, deadline]);
121
- } finally {
122
- clearTimeout(timer);
123
- }
124
- };
125
-
126
- /** The notice line for this start-up, or null. Never throws. */
127
- export const checkForCliUpdate = async ({
128
- installedVersion,
129
- env = process.env,
130
- now = Date.now(),
131
- cachePath = updateCachePath(),
132
- fetchImpl = globalThis.fetch,
133
- timeoutMs = UPDATE_TIMEOUT_MS,
134
- readText = defaultReadText,
135
- writeText = defaultWriteText,
136
- minBehind = UPDATE_NOTICE_MIN_BEHIND,
137
- } = {}) => {
138
- try {
139
- if (env?.[UPDATE_CHECK_OPT_OUT_ENV] === '1') return null;
140
- if (!parseVersion(installedVersion)) return null;
141
- let latest = readCachedLatest({ cachePath, now, readText });
142
- if (!latest) {
143
- latest = await fetchLatestVersion({ fetchImpl, timeoutMs });
144
- if (!latest) return null;
145
- try {
146
- writeText(cachePath, `${JSON.stringify({ checkedAt: new Date(now).toISOString(), latest })}\n`);
147
- } catch {
148
- // An unwritable cache costs one lookup per start, nothing more.
149
- }
150
- }
151
- return updateNoticeLine({ installed: installedVersion, latest, minBehind });
152
- } catch {
153
- return null;
154
- }
155
- };
156
-
157
- /**
158
- * Fire the check and return at once. The returned promise (for tests) settles
159
- * to the line that was logged, or null.
160
- */
161
- export const startUpdateNotice = (params = {}) => Promise.resolve()
162
- // Everything, the argument destructuring and checkForCliUpdate's default
163
- // parameters (homedir()) included, runs inside the chain, so the trailing
164
- // catch is the only exit a failure has. Start-up never sees a throw or an
165
- // unhandled rejection from here.
166
- .then(async () => {
167
- const { log, ...options } = params;
168
- const line = await checkForCliUpdate(options);
169
- if (line) {
170
- try {
171
- log(line);
172
- } catch {
173
- // A closed log sink is not start-up's problem.
174
- }
175
- }
176
- return line;
177
- })
178
- .catch(() => null);
179
-
180
- let cachedCliVersion;
181
- /** This CLI's own version, from the package.json it shipped with. */
182
- export const installedCliVersion = () => {
183
- if (cachedCliVersion === undefined) {
184
- try {
185
- const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
186
- cachedCliVersion = typeof pkg?.version === 'string' ? pkg.version : null;
187
- } catch {
188
- cachedCliVersion = null;
189
- }
190
- }
191
- return cachedCliVersion;
192
- };
@@ -1 +0,0 @@
1
- export * from '@commonlyai/seats/upstream-refusal';
@@ -1,109 +0,0 @@
1
- /**
2
- * Local webhook server — receives forwarded events from the poller
3
- * and delivers them to the developer's agent process.
4
- *
5
- * The developer's agent code handles POST /cap (or custom path)
6
- * and returns { outcome, content? }.
7
- */
8
-
9
- import { createServer } from 'http';
10
- import { createHmac } from 'crypto';
11
- import { EMPTY_OUTPUT } from './claim-outcome.js';
12
-
13
- export const startWebhookServer = ({
14
- port = 3001,
15
- path = '/cap',
16
- secret = null,
17
- onEvent, // async (event) => { outcome, content? }
18
- onReady, // (url) => void
19
- }) => new Promise((resolve, reject) => {
20
- const server = createServer(async (req, res) => {
21
- if (req.method !== 'POST' || req.url !== path) {
22
- res.writeHead(404);
23
- res.end();
24
- return;
25
- }
26
-
27
- // Read body
28
- const chunks = [];
29
- req.on('data', (c) => chunks.push(c));
30
- req.on('end', async () => {
31
- const rawBody = Buffer.concat(chunks).toString('utf8');
32
-
33
- // Verify signature if secret configured
34
- if (secret) {
35
- const sig = req.headers['x-commonly-signature'];
36
- const expected = `sha256=${createHmac('sha256', secret).update(rawBody).digest('hex')}`;
37
- if (sig !== expected) {
38
- res.writeHead(401);
39
- res.end(JSON.stringify({ error: 'Invalid signature' }));
40
- return;
41
- }
42
- }
43
-
44
- let event;
45
- try { event = JSON.parse(rawBody); } catch {
46
- res.writeHead(400);
47
- res.end(JSON.stringify({ error: 'Invalid JSON' }));
48
- return;
49
- }
50
-
51
- // A handler that returns nothing said nothing (#2753).
52
- let result = { outcome: 'no_action', reason: EMPTY_OUTPUT };
53
- try {
54
- result = (await onEvent(event)) || { outcome: 'no_action', reason: EMPTY_OUTPUT };
55
- } catch (err) {
56
- result = { outcome: 'error', reason: err.message };
57
- }
58
-
59
- res.writeHead(200, { 'Content-Type': 'application/json' });
60
- res.end(JSON.stringify(result));
61
- });
62
- });
63
-
64
- server.on('error', reject);
65
- server.listen(port, () => {
66
- const url = `http://localhost:${port}${path}`;
67
- onReady?.(url);
68
- resolve({ server, url, close: () => server.close() });
69
- });
70
- });
71
-
72
- /**
73
- * The handler `agent connect` serves on its own port. It only logs what
74
- * arrives, and no agent has read the event, so it states no outcome a read
75
- * would count as: the wake still settles, and the pod digest stays unread
76
- * (#2753).
77
- */
78
- export const logOnlyHandler = async (event) => {
79
- console.log(`[local] ${event.type} from pod ${event.podId}`);
80
- return { outcome: 'no_action', reason: EMPTY_OUTPUT };
81
- };
82
-
83
- /**
84
- * Forward a polled event to a local webhook server.
85
- * Used by `agent connect` to bridge polling → local HTTP.
86
- */
87
- export const forwardToLocalWebhook = async (event, webhookUrl, secret = null) => {
88
- const payload = JSON.stringify(event);
89
- const headers = {
90
- 'Content-Type': 'application/json',
91
- 'X-Commonly-Event': event.type,
92
- 'X-Commonly-Delivery': String(event._id),
93
- };
94
- if (secret) {
95
- headers['X-Commonly-Signature'] = `sha256=${createHmac('sha256', secret).update(payload).digest('hex')}`;
96
- }
97
-
98
- const res = await fetch(webhookUrl, { method: 'POST', headers, body: payload });
99
- if (!res.ok) throw new Error(`Local webhook returned HTTP ${res.status}`);
100
- // Only a stated outcome counts. An empty or non-JSON body, or one without
101
- // an outcome, is a handler that said nothing, the same as a CLI turn that
102
- // printed nothing, so it must not read the pod digest (#2753).
103
- let body = null;
104
- try { body = await res.json(); } catch { body = null; }
105
- if (!body || typeof body !== 'object' || typeof body.outcome !== 'string') {
106
- return { outcome: 'no_action', reason: EMPTY_OUTPUT };
107
- }
108
- return body;
109
- };