Add persistent SDK sessions, native context controls and durable attachments - #427
raullopez-sandbox wants to merge 88 commits into
Conversation
Review candidate reconstructed from cc3bb22 on e45a620. Preserve the SDK and shared server implementation, move the Python wire contract and projection into sessions, and extract request-card rendering, publication, callbacks and storage into a companion layer. Shared protocol code retains contributions introduced in 5eb9105..0bf7f88. This is an isolated squash candidate, not a rewrite of the active integration branch.
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
| } | ||
| } | ||
| const sourceConfig = await optionalText(join(input.sourceHome, 'config.toml')); | ||
| const config = { ...(sourceConfig ? parse(sourceConfig) : {}), ...parse(input.config) }; |
There was a problem hiding this comment.
Risk: Affected versions of smol-toml are vulnerable to Loop with Unreachable Exit Condition ('Infinite Loop') / Unchecked Input for Loop Condition. smol-toml's parse() never returns when a value inside an array or inline table is followed by a comment that ends the document without a trailing newline (for example a=[1 #). The internal skipUntil() helper resets its cursor to the start of the input instead of exiting, so the loop's exit condition becomes unreachable. An attacker who can supply a TOML document to be parsed can block the event loop indefinitely with a few bytes, pinning a CPU core and denying service to the entire process. Serialization via stringify() is unaffected.
Fix: Upgrade this library to at least version 1.7.1 at switch/console/pnpm-lock.yaml:5190.
Reference(s): https://euvd.enisa.europa.eu/vulnerability/EUVD-2026-71261, GHSA-7w5x-hrqm-74c2, CVE-2026-85730
🧹 Fixed in commit 621369a 🧹
| skill: 'Switch workflow', | ||
| }; | ||
| const home = await prepareCodexSessionHome(input); | ||
| expect(parse(await readFile(join(home, 'config.toml'), 'utf8'))).toEqual({ |
There was a problem hiding this comment.
Risk: Affected versions of smol-toml are vulnerable to Loop with Unreachable Exit Condition ('Infinite Loop') / Unchecked Input for Loop Condition. smol-toml's parse() never returns when a value inside an array or inline table is followed by a comment that ends the document without a trailing newline (for example a=[1 #). The internal skipUntil() helper resets its cursor to the start of the input instead of exiting, so the loop's exit condition becomes unreachable. An attacker who can supply a TOML document to be parsed can block the event loop indefinitely with a few bytes, pinning a CPU core and denying service to the entire process. Serialization via stringify() is unaffected.
Fix: Upgrade this library to at least version 1.7.1 at switch/console/pnpm-lock.yaml:5190.
Reference(s): https://euvd.enisa.europa.eu/vulnerability/EUVD-2026-71261, GHSA-7w5x-hrqm-74c2, CVE-2026-85730
🧼 Fixed in commit 621369a 🧼
|
Semgrep found 1 Risk: Affected versions of smol-toml are vulnerable to Loop with Unreachable Exit Condition ('Infinite Loop') / Unchecked Input for Loop Condition. smol-toml's Fix: Upgrade this library to at least version 1.7.1 at switch/console/pnpm-lock.yaml:5187. Reference(s): https://euvd.enisa.europa.eu/vulnerability/EUVD-2026-71261, GHSA-7w5x-hrqm-74c2, CVE-2026-85730 |
…rams The Codex adapter sends an `effort` value with `turn/start`, but the `CodexTurnStartParams` type did not declare the field. The generated protocol for codex-cli 0.153.4 has `effort` as an optional reasoning effort string, which overrides the effort for this turn and the turns after it. Add the field to the type. Also assert that a reasoning effort selected at session start reaches the first turn, not only an effort set later with `setModel`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJqrbLGEn65miwfNgZ3JXd
The OpenCode adapter kept the `items`, `emittedText`, `partTypes` and `messageRoles` maps for the life of the session. When a turn failed, `completeTurn` walked all of the items and marked every item that was still in progress as failed against the current turn. A tool left in progress in an early turn was therefore reported as failed again in every later turn that failed, and the maps grew without limit. Clear the four maps when a new turn starts, as the Gemini adapter does. A steered message joins the running turn, so it keeps the maps. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJqrbLGEn65miwfNgZ3JXd
The stream retried every failed open with a backoff. A 401, or a 403 that names no declared room, carries the same token on each retry, so the loop ran forever, logged a warning only, and told nobody that the credentials were refused. Both statuses now end the stream and the heartbeat and report the refusal over the eviction callback, which the hosts already turn into a terminal failure. The message gives the status code. The explicit refused-room case still drops the room and reopens. Network errors, 5xx and 429 still reconnect with the existing backoff. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJqrbLGEn65miwfNgZ3JXd
A 401 or 403 on the heartbeat ends the stream the same way it does on the stream open. When both fail together the owner is told once. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJqrbLGEn65miwfNgZ3JXd
Mattermost reports a plain text file as `text/plain; charset=utf-8` and Discord does the same, so the attachment allowlist — which holds bare media types — refused every `.txt` relayed from a room with "Unsupported attachment MIME type" while a PNG, whose type carries no parameter, went through. Strip the parameters where the type is derived: on the collaboration bridge's inbound and outbound attachment models, on the agent protocol's media upload, and on the value the session service stores and delivers. `validate_attachment` normalises its input too, so the check and the stored type cannot disagree. The allowlist itself already matched the host list in `console/packages/agent-providers/src/host/attachments.ts`; a new test pins them together so a later edit to one fails rather than silently delivering a file the other end refuses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4xR1K4CdvTmRUPJA4zidf
Outside `full-access` the Antigravity CLI reads only the directories it was given at launch, so a file fetched by the `download_attachment` Switch tool was denied with "headless mode cannot ask for permission". The per-turn grant for staged attachments cannot cover it: the runtime writes under `~/.switch/sessions/<runtime pid>/media`, keyed by a process the adapter never sees, and the directory appears in the middle of the turn that asked for the file. Grant `~/.switch/sessions` at spawn instead, in every mode that needs grants. The CLI accepts a directory that does not exist yet, so a session that never downloads anything is unaffected, and `full-access` is left alone because it needs no grant at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4xR1K4CdvTmRUPJA4zidf
A refactor commit swept in a test for behaviour whose implementation is still uncommitted work in progress, so the branch failed that test on its own. The test returns to what the branch actually implements; the in-progress change lands with its implementation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4xR1K4CdvTmRUPJA4zidf
Four conflicts, resolved as follows. `gateway/app.py` and `protocol/service.py` were import collisions with no behavioural content: both sides' imports are kept. `console/.../main/rpc.ts` gains main's `roomTemplates` controller and keeps this branch's `sdkHost`. The `sidecar` registration main still carries stays removed — `main/core/sidecar/` no longer exists on this branch, so restoring the registration would not resolve. `test_row_level_security.py` is the substantive one. This branch tightened the frozen-inventory check to full equality, reaching that by adding the second scoping migration's table list to the comparison. main instead relaxed the assertion to one direction, on the ground that a table added to the models after a revision cannot appear in that revision's frozen list, and covered the other direction against a real database in `test_frozen_ddl_matches_create_all.py`. main's reasoning generalises where enumerating migrations does not, so its assertions win; this branch's combined inventory is kept as their input, so both frozen lists are still checked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main added the invitations revision while this branch added the room-control follow-up column, so bringing the two together left the chain with two heads and `upgrade head` ambiguous. An empty merge revision joins them, as the four earlier head collisions in this directory were joined. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pre-existing on this branch rather than a product of the merge: oxfmt rejects all six at 522da97 and accepts main's copies of the four main also carries. The terminal removal left them behind, which is why the console job cannot go green here. The same six are already formatted downstream on work/refresh-messaging-platforms at 90df974, and oxfmt's output here is byte-identical to that copy, so merging this branch upward finds the same content on both sides. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Switch Console uses persistent SDK hosts for local and SSH sessions. Closing Console leaves execution running. The server owns authorization, tenant isolation, leases, recovery epochs, command receipts and answer arbitration. Tmux agent execution is removed.
Reset retains transcript history and starts a fresh native conversation. An interrupted reset or a missing Codex rollout requires an explicit fresh-reset decision; Switch never chooses a replacement conversation or repeats uncertain actions automatically. Recovery persists uploads, acknowledgements and room cursor changes. Process fencing requires confirmed provider-group exit before another host starts. Discovery restores room bindings and isolates failed sessions.
The composer provides searchable model and effort menus plus Context actions. Confirmed model choices persist across restart. Native compaction is exposed only where supported. Picker, paste and drop attachments use authenticated durable references; the execution host downloads and verifies the actual bytes. Skills, MCP, credentials and native configuration load on the execution machine. Discord sender-name rejection uses a safe, stable name with visible agent attribution.
Validation: final provider suite 240 passed; runtime suite 113 passed; Discord/display-name suite 177 passed. Ten recovery tests passed against real PostgreSQL. Provider/runtime types, Console types, affected lint and Discord adapter mypy passed. Earlier isolated local SSH checks covered bundle startup, staging, retries, native skill paths and MCP environment with simulated provider/download inputs.
Live Discord and Console checks verified both channel-creation directions, chat-triggered Claude and Codex sessions, sidebar room binding, transcript replay, unaddressed-message cues, queued delivery during approval, Codex interrupt/reset, and explicit recovery of preserved Claude and Codex failures. A later Codex process crash resumed the same native conversation ID. After fixing the server-reset cursor regression, the original waiting Discord message arrived without being resent. Reserved-name delivery also passed in native Discord with visible attribution. Approvals were answered through Console.
Earlier live provider checks covered context controls, file contents, project skills and MCP with Claude, Codex, OpenCode and Gemini; image recognition passed with Claude, Codex and Gemini. The tested OpenCode model rejects images. Cursor generation remains blocked by its account limit. The current Discord run covers Claude and Codex only.
Remaining limits: the in-memory room stream can detect a server restart only while the old cursor is ahead of the new buffer; it has no persistent generation identity. Legacy delivery acknowledgements without substitution records require server-evidence reconciliation. Discord-only approval cards and publication changes remain on the separate messaging companion branch, which is not merged here. Automatic attachment retention is not implemented. No authenticated external remote host was supplied, so local SSH tests do not establish external deployment readiness.