commit - babda3eaa7c8cc9a67a59ab6e6de39a31910713f
commit + 17d56da6b3808a59155d2afbc938cd8484cc43b7
blob - /dev/null
blob + 49e397a2af6dd576dd3c51bd0798fae3a53ce7d2 (mode 644)
--- /dev/null
+++ .scratch/codex-subscription-adapter/issues/01-implement-pure-c-codex-subscription-adapter.md
+# Implement the pure-C Codex subscription Adapter
+
+Status: needs-triage
+Type: task
+
+## What to build
+
+Add a fourth Provider, `codex`, that lets the one Fugu user consume their
+personal ChatGPT Codex subscription from Fugu without per-token Platform API
+billing and without adding Node.js, Python, an SDK, the Codex executable,
+app-server, a proxy, or another daemon.
+
+This is one experimental compatibility route through Fugu's existing Provider
+Seam. Fugu remains the agent: the Coordinator owns the canonical Projection,
+Turn mechanics, Tool Palette, Journal, and resume behavior. The new Adapter
+only translates model requests and Responses events.
+
+The implementation must follow:
+
+- `docs/research/codex-subscription-native-clients.md`
+- `docs/design/codex-subscription.md`
+
+## User story
+
+As Fugu's sole user, I want to run Codex models against the ChatGPT
+subscription I already pay for, while preserving Fugu's native OpenBSD
+dependency ceiling, process isolation, Tool authorization, durable Turns, and
+single-user release pattern.
+
+## Research verdict
+
+The direct path is technically feasible. OpenAI's Rust client and independent
+TypeScript, Go, and Python clients converge on the same behavior:
+
+- ChatGPT authentication selects
+ `https://chatgpt.com/backend-api/codex/responses` rather than the metered
+ Platform endpoint.
+- The request uses a bearer access token plus `ChatGPT-Account-ID`, whose value
+ comes from a namespaced claim in the token.
+- Requests use the Responses format with `stream:true`, `store:false`, full
+ history, and `include:["reasoning.encrypted_content"]`.
+- Current OpenAI prefers WebSocket but retains HTTP SSE fallback; maintained
+ direct clients still demonstrate SSE.
+- Encrypted reasoning is continuation state, not display-only thinking.
+- Responses function calls carry both a canonical `call_id` and an `fc_...`
+ output-item id. The latter may be paired with the encrypted reasoning item
+ and must survive replay without leaking into canonical Tool ids.
+- Mature clients durably persist replacement refresh credentials after OAuth
+ refresh. In-memory refresh without atomic persistence is not safe.
+
+No maintained pure-C direct client was found. Implement the behavior cleanly
+against pinned sources and Fugu fixtures; do not port another project's
+runtime, request builder, stream parser, credential store, or WebSocket stack.
+
+The raw ChatGPT backend is not a documented third-party API. This route must
+remain explicitly experimental and fail closed on profile drift.
+
+## Fixed decisions
+
+### Provider and configuration
+
+The minimal route is:
+
+```conf
+provider "codex"
+model "<configured Codex model>"
+oauth_token "<personal ChatGPT Codex access token>"
+```
+
+Named routes use `type "codex"` with the same model and token fields.
+
+- Add `PROVIDER_CODEX`; do not overload the existing metered `openai`
+ Provider.
+- Keep the token directly in protected Fugu configuration.
+- Extract the one account id from the token in `fugu-api`; add no `owner`,
+ account selector, account pool, or account-routing plumbing.
+- Reject `api_key`, `api_key_file`, `api_host`, `api_port`, and `api_path` for
+ this Provider.
+- Never fall back to a Platform API key or another Provider.
+- Reject unsupported FedRAMP/data-residency/compute-residency token profiles
+ before sending a request rather than guessing routing headers.
+
+### Compatibility profile
+
+Create one compile-time profile, initially
+`chatgpt-codex-2026-07-17`, containing:
+
+- fixed host `chatgpt.com`, port `443`, and path
+ `/backend-api/codex/responses`;
+- the pinned OpenAI Codex source commit and snapshot date;
+- JWT claim paths and bounds;
+- exact account, user-agent, session, request, and content headers;
+- exact request flags, event names, terminal mappings, and usage fields; and
+- model-specific request differences proven by fixtures.
+
+Use `User-Agent: fugu/<version>`. Do not claim
+`originator: codex_cli_rs` or another first-party identity. Start with no
+optional originator; only an honest `fugu` value may be considered through a
+reviewed profile update. If honest identification is rejected, stop the
+Adapter instead of probing fingerprints.
+
+Do not send cookies, redirects, `previous_response_id`, a Platform key, or the
+WebSocket beta header. Never follow a bearer credential through an HTTP
+redirect.
+
+### Request and stream Adapter
+
+Implement one deep `codex_sub` Module split only for Role linkage:
+
+- credential-free request translation linked into the Coordinator and
+ read-only Subagent Role;
+- JWT/account extraction, credentialed headers, Responses SSE decoding, and
+ bounded error handling linked only into `fugu-api`.
+
+The request builder must:
+
+- render the Fugu system block as Responses instructions;
+- render canonical user/assistant text, function calls, and function-call
+ outputs from the Projection without mutating it;
+- advertise only Fugu's offered function Tools, never Codex built-ins;
+- set `stream:true`, `store:false`, request encrypted reasoning, and send full
+ history;
+- omit `previous_response_id` and, for the first profile,
+ `max_output_tokens`;
+- preserve Fugu's `call_id` as the canonical Tool id while adding a matching
+ provider `fc_...` item id only from validated continuity state; and
+- use deterministic, nonsecret session/prompt-cache metadata.
+
+Because the observed backend rejects or ignores the normal output-token field,
+`max_tokens` is not an exact provider-side output limit in v1. Document this
+plainly; Fugu's independent response-byte and Turn caps remain authoritative.
+
+The dedicated Responses SSE decoder must handle output text, function calls,
+fragmented arguments, usage, completed reasoning items, completion, failure,
+incomplete responses, malformed sequences, every byte split, and all existing
+Fugu bounds. Unknown output-item or terminal semantics fail closed.
+
+Start with the existing HTTP/1.1 libevent/libtls transport. Do not add
+WebSocket in this ticket. If the opt-in live gate proves SSE is no longer
+accepted, stop and create a separately reviewed WebSocket ticket.
+
+### Durable provider continuity
+
+Extend `struct msg` with one optional generic provider-state attachment,
+separate from canonical text and Tool blocks. The logical envelope is:
+
+```text
+provider = codex
+profile = chatgpt-codex-2026-07-17
+model = <producing model>
+kind = responses-continuity
+data = {
+ reasoning: [<output index plus completed reasoning item>, ...],
+ calls: [<output index, call_id, and provider item id>, ...]
+}
+```
+
+- Retain only final reasoning items with nonempty encrypted content and final
+ function-call identity records.
+- Cross-check `response.output_item.done` with any copy in
+ `response.completed.output`; duplicate conflicts fail.
+- Bound the complete attachment to 2 MiB per Generation on every side of the
+ stream/imsg/Generation/Journal/request seams.
+- Credential-reflection filter it before it leaves `fugu-api`.
+- Send it in generic `FUGU_IMSG_A_PROVIDER_STATE` chunks immediately before a
+ successful terminal event.
+- Attach it only to a normally completed assistant message.
+- Encode opaque bytes as a JSON string in the Journal; never splice raw
+ provider JSON into NDJSON record syntax.
+- Make it atomic with the assistant message under existing Turn commit and
+ abandonment rules.
+- Preserve it on committed resume; discard it with messages on abandonment,
+ `/clear`, and `/compact`.
+- Keep Subagent state only in the Subagent's existing in-memory conversation.
+- Replay only exact Provider/profile/model matches. Other Providers ignore it.
+- Join call metadata to canonical Tool blocks by exact `call_id`; provider
+ state never supplies Tool names or arguments and never gains Tool authority.
+- Do not concatenate the two Responses ids into `block.tool_id`.
+
+Provider-state bytes count toward the serialized 16 MiB request ceiling but
+not the visible Turn data budget or Context token meter.
+
+### Credential lifetime
+
+The first experimental slice accepts one access token from `oauth_token` and
+fails hard when its JWT expiry has passed or the backend returns 401/403.
+Replacement is manual.
+
+Do not add in-memory refresh. Refresh can rotate the refresh token, while
+`fugu-api` has no durable credential store and must not send replacement
+secrets back to the Coordinator. Automatic device login and refresh require a
+separate credential-custody design with atomic persistence, crash recovery,
+and new pledge/unveil proofs.
+
+## Security architecture
+
+The existing Role topology must remain intact:
+
+- `fugu-api` is the sole long-lived model-credential Custodian and sole Role
+ parsing the hostile subscription network stream.
+- The production endpoint is compile-time fixed and independently checked by
+ `fugu-api` after HELLO.
+- The Coordinator briefly relays the configured token during startup, wipes
+ its copy, and never receives it again.
+- `fugu-api` retains only `stdio inet dns`; it gains no file write, open, or
+ execution authority.
+- The Coordinator receives canonical events and bounded, credential-filtered
+ continuity bytes, not an OAuth token or endpoint selector.
+- Every model-selected Tool still passes through the existing offered-Palette,
+ imsg, Turn, and kernel-confinement checks.
+- Journals remain mode 0600 and contain no credential.
+- No Worker gains a new channel or capability.
+
+Before making the Provider selectable, add a new ADR and update the charter,
+invariants, `CONTEXT.md`, behavior, and verification language that currently
+names only the fixed Anthropic subscription endpoint or only three Providers.
+Do not silently broaden the Claude-specific ADR-0007.
+
+## Implementation sequence
+
+Implement as reviewable commits in this order:
+
+1. Evidence, proposed ADR, and normative contract changes; no selectable code.
+2. Generic provider-state attachment, Generation/imsg carrier, Journal
+ transaction/replay, bounds, and hermetic tests; existing Providers do not
+ emit or consume it.
+3. Pure request/profile/SSE codecs with synthetic text, Tool, usage, terminal,
+ and continuity fixtures; still not selectable.
+4. `PROVIDER_CODEX`, config validation, fixed endpoint, JWT/account parsing,
+ headers, redaction, wiping, and one text-only local TLS path.
+5. Complete Lead Tool round with encrypted-state replay, fresh-process resume,
+ and abandoned-turn exclusion.
+6. Named routing, configured-model discovery, read-only Subagents,
+ cancellation, retries, and accounting.
+7. Explicitly opt-in credentialed live gate with honest client identity.
+8. Manuals, sample configuration, live-check target, linkage/package checks,
+ and full normal/hardened release gates.
+
+Each commit keeps Anthropic, OpenAI, and Claude Subscription regressions green.
+No intermediate commit selects `codex` by default.
+
+## Acceptance criteria
+
+- [ ] A new accepted ADR authorizes the experimental fixed OpenAI subscription
+ endpoint, direct configured access token, manual expiry behavior, honest
+ client identity, and no metered fallback.
+- [ ] `provider "codex"` and named `type "codex"` routes parse, print with
+ secrets redacted, wipe secrets, and reject API-key/endpoint override forms.
+- [ ] A synthetic access JWT yields exactly one bounded account header;
+ malformed, expired, ambiguous, oversized, residency-routed, NUL, and CR/LF
+ cases fail before network use.
+- [ ] The Coordinator and Subagent request fixtures pin exact Responses JSON
+ for text, mixed Tool history, Tool errors, suffixes, compaction history,
+ continuity metadata, and mismatch omission.
+- [ ] The SSE decoder passes adversarial split, ordering, duplicate, conflict,
+ malformed JSON/UTF-8, usage, terminal, and every cap test.
+- [ ] Canonical Tool ids remain Responses `call_id`; provider item ids survive
+ only in validated provider state and replay with their paired reasoning.
+- [ ] Continuity state is capped at 2 MiB, reflection-filtered, invisible to the
+ UI, transactionally journaled, resumable, and removed by abandonment,
+ `/clear`, and `/compact`.
+- [ ] A fresh-process local TLS fixture completes
+ `user -> function call -> function result -> final assistant text` using the
+ journaled continuity state and no `previous_response_id`.
+- [ ] Only `fugu-api` adds bearer/account headers; captured Coordinator request
+ bodies and every imsg other than startup secret custody remain credential
+ free.
+- [ ] 401/403 fail once with a bounded replacement hint; 429/5xx use existing
+ pre-output retry policy; no condition triggers a metered request or account
+ rotation.
+- [ ] Input/cache usage does not double-count cached tokens, and Subagent usage
+ remains excluded from the Lead Context display.
+- [ ] Existing ktrace and hardened checks prove that `fugu-api` still cannot
+ open, write, or execute and that no new Role gains authority.
+- [ ] An opt-in, non-root live check proves HTTP SSE, honest Fugu identity, one
+ text response, one harmless `read` Tool round, and continuity capture without
+ exposing token, account id, prompt, or raw response in terminal output,
+ Journal, syslog, or diagnostics.
+- [ ] README/manual/config examples explain the experimental unsupported
+ contract, direct token placement, expiry replacement, no exact
+ `max_tokens` enforcement, fixed endpoint, and lack of API fallback.
+- [ ] Normal, hardened, package/linkage, and release live gates pass without
+ Node, Python, an SDK, Codex, app-server, proxy, or added `WANTLIB` entries.
+
+## Triage decisions
+
+1. Accept manual access-token replacement at JWT expiry for the first
+ experimental release, or require a separate credential-persistence design
+ before the Provider becomes selectable.
+2. Accept the fixed honest HTTP identity policy: Fugu user-agent, no optional
+ originator initially, and fail closed if first-party identity is required.
+3. Accept the 2 MiB continuity-state cap and omission of provider-side
+ `max_output_tokens` for the first compatibility profile.
+4. Select the first configured Codex model and Effective context window for the
+ credentialed fixture; do not add model discovery in this ticket.
+
+## Code-cost analysis
+
+Baseline, including durable continuity:
+
+| Work | Production | Regressions/docs |
+|---|---:|---:|
+| Request/profile/JWT helpers | 500-800 C lines | 500-800 lines |
+| Responses decoder and state extraction | 650-1,000 C lines | 850-1,300 lines |
+| Generic state carrier, imsg, Generation, Journal | 450-750 C lines | 650-1,000 lines |
+| Config/custody/routing integration | 300-500 C lines | 400-700 lines |
+| Whole-Turn/Subagent fixtures and release docs | 150-300 C/shell lines | 600-1,000 lines |
+| **Total** | **2,050-3,350** | **3,000-4,800** |
+
+Not included:
+
+- durable device login/refresh: approximately 700-1,300 production and
+ 700-1,200 regression/documentation lines;
+- WebSocket transport: approximately 600-1,000 production and 700-1,200
+ adversarial test lines.
+
+The Responses decoder, provider-state lifecycle, credentialed whole-Role
+fixture, and live profile gate dominate risk. The provider enum and parser
+grammar are not the expensive parts.
+
+## Evidence
+
+- [OpenAI Codex source snapshot](https://github.com/openai/codex/tree/315195492c80fdade38e917c18f9584efd599304)
+- [OpenAI ChatGPT-plan guidance](https://help.openai.com/en/articles/11369540-using-codex-with-your-chatgpt-plan)
+- [pi direct Responses request](https://github.com/badlogic/pi-mono/blob/64f83c85d9f36c831e96b29477750f92cd5a2230/packages/ai/src/api/openai-codex-responses.ts#L482-L585)
+- [pi reasoning and function-call replay](https://github.com/badlogic/pi-mono/blob/64f83c85d9f36c831e96b29477750f92cd5a2230/packages/ai/src/api/openai-responses-shared.ts#L170-L228)
+- [openai-oauth stateless direct request](https://github.com/EvanZhouDev/openai-oauth/blob/ec7dab2fcd8dab9da970a7a2b5dc34046c94905e/packages/core/src/runtime.ts#L570-L663)
+- [Docker Agent credential refresh](https://github.com/docker/docker-agent/blob/1ec6c9a1835ed4d9476960faabda57c1107a7d22/pkg/chatgpt/token.go#L35-L121)
+
+## Blocked by
+
+None - ADR and hermetic implementation can start immediately. Credentialed
+live acceptance requires explicit Owner opt-in and a current personal token.
+
+## Comments
+
blob - /dev/null
blob + 0da8205ea342c0aabcd71a680123c13b631dc6c1 (mode 644)
--- /dev/null
+++ .scratch/interactive-welcome/issues/01-dismissing-empty-transcript-welcome.md
+# Add a dismissible empty-transcript welcome to the curses interface
+
+Status: needs-triage
+Type: task
+
+## What to build
+
+Replace the unsolicited Claude Subscription profile warning in the curses
+transcript with a presentation-only, Vim-inspired welcome shown while the
+interactive transcript has not yet received Owner input. The welcome is
+centered in the transcript viewport and uses this copy:
+
+```text
+I like Fugu — Japanese for pufferfish.
+by Isaac Meerleo
+git ssh://anonymous@got.fugu.farm
+Fugu is open source and freely available!
+
+type /<Tab> to explore commands
+type /model<Enter> to choose a model
+press Esc, then v to compose in your editor
+type /quit<Enter> to exit
+```
+
+Unused transcript rows remain blank; do not copy Vim's `~` gutter markers.
+
+While the welcome is visible, add one plain-language caption row immediately
+above the persistent status bar. Align `agent state`, `input mode`, and
+`context used / limit` above the corresponding `ready`/working marker,
+`[insert]`/`[normal]` marker, and `Context current/max N%` field. These captions
+are welcome-screen help, not a permanent extra status row, and disappear with
+the welcome.
+
+The welcome is a local `fugu-tty` rendering state, not a transcript item. It
+must never enter the Lead Projection, Session Journal, scrollback, queue, or
+machine-readable output. It disappears permanently for the invocation as soon
+as an editing action first makes the prompt non-empty, including ordinary
+typing, bracketed paste, completion, recalled text, or an external-editor
+result. Deleting that text must not make the welcome reappear.
+
+Remove the current long profile warning only from curses startup so it cannot
+become the first transcript item and suppress the welcome. Keep the explicit
+risk disclosure in `fugu -n`, manuals/README, the accepted subscription ADR,
+and profile-specific failure diagnostics. Keep line and print mode's stderr
+startup warning unchanged. This preserves ADR-0007's experimental-route
+disclosure while making the blank interactive surface quiet.
+
+## Acceptance criteria
+
+- [ ] Starting the curses interface with an empty on-screen transcript shows
+ the exact welcome copy centered horizontally and approximately vertically in
+ the transcript viewport; the editor, queue area, and status bar retain their
+ normal geometry.
+- [ ] Empty rows contain spaces only: no Vim-style `~` markers or fake buffer
+ coordinates are rendered.
+- [ ] The byline is exactly `by Isaac Meerleo`, and the repository location is
+ exactly `git ssh://anonymous@got.fugu.farm`.
+- [ ] A caption row labels the live bottom fields as `agent state`, `input
+ mode`, and `context used / limit`, aligned over the values they describe and
+ omitted after welcome dismissal.
+- [ ] The first edit that makes the prompt non-empty dismisses the welcome and
+ captions before submission. Typing then deleting, cancelling the line,
+ `/clear`, resize, or a failed Turn never resurrects them during that
+ invocation.
+- [ ] Ordinary typing, bracketed paste, slash completion, history recall, and
+ accepted external-editor text all use the same one-way dismissal rule.
+- [ ] Opening the drawer or model picker before typing overlays the welcome
+ normally; closing it restores the still-undismissed welcome without adding
+ scrollback content.
+- [ ] Narrow terminals omit lower-priority hint/caption lines rather than
+ overlap, wrap into the editor, or displace the status bar. Resize recomputes
+ centering without changing dismissal state.
+- [ ] `ascii yes` transliterates the em dash consistently, and color and
+ monochrome terminals remain readable without making the welcome look like
+ model output.
+- [ ] Configuring a default or named `claude` Provider no longer inserts
+ `experimental Claude subscription profile claude-code-2.1.185 ...` into the
+ curses transcript. Line/print warnings, redacted `fugu -n` profile output,
+ manuals, ADR-0007, and profile-naming failure tests remain intact.
+- [ ] Renderer tests cover centering, blank rows, captions, narrow/resize
+ behavior, ASCII mode, and one-way dismissal. A curses PTY regression proves
+ the welcome is visible before input, gone after the first prompt begins, and
+ absent from the Journal and Provider request.
+- [ ] `handoff/behavior.md`, `docs/design/m7-curses.md`, `fugu(1)`, and the
+ verification matrix document the presentation-only welcome and warning
+ placement.
+
+## Code-cost analysis
+
+Expected size: **small-to-medium, roughly 220-420 lines including tests and
+documentation, across 2-4 focused commits**. The visual copy is small; most of
+the cost is keeping centering, narrow-terminal fallback, status captions, and
+one-way dismissal independent from transcript and Journal state.
+
+## Blocked by
+
+None - can start immediately.
+
+## Comments
+
blob - /dev/null
blob + 20e317c55c98227ffc193a3ccf09f7efb3011c69 (mode 644)
--- /dev/null
+++ docs/design/codex-subscription.md
+# Experimental pure-C Codex subscription adapter
+
+Status: proposed design, 2026-07-17. No production code has been written.
+Protocol constants and the evidence behind them are pinned in
+[`docs/research/codex-subscription-native-clients.md`](../research/codex-subscription-native-clients.md).
+
+## Outcome
+
+Add a fourth Provider type, `codex`, that sends the Owner's personal ChatGPT
+Codex subscription traffic directly from OpenBSD. The route is implemented in
+C, uses only base-system libraries, and requires no Node.js, Python, Codex
+executable, app-server, SDK, proxy, or daemon at runtime.
+
+The first experimental slice uses a directly configured access token and
+requires manual replacement when it expires. Automatic OAuth login and refresh
+are deliberately outside this Adapter because rotated refresh credentials need
+a new durable-custody design. This limitation is acceptable for proving the
+native inference path, but it must be prominent in setup documentation.
+
+This is not the existing `openai` Provider under another credential. The two
+routes remain deliberately distinct:
+
+- `openai` is the supported, configurable `/v1/chat/completions` dialect using
+ a Platform API key and usage-based billing;
+- `codex` is an experimental, fixed-endpoint compatibility profile using a
+ personal ChatGPT Codex credential and subscription quota.
+
+There is no fallback between them. A stale Codex profile, expired credential,
+quota rejection, or incompatible response fails the Generation without making
+a metered Platform request.
+
+## Hard constraints
+
+The implementation must preserve all of these together:
+
+1. OpenBSD-only C and base libraries; the port's `WANTLIB` ceiling remains
+ `c curses event tls util`.
+2. No language runtime, SDK, Codex child process, app-server, local proxy, or
+ listening socket.
+3. Fugu remains the agent. The Coordinator owns Turn mechanics and the
+ Projection; Codex supplies only model inference.
+4. `fugu-api` remains the sole model-credential Custodian and the only Role
+ that parses the hostile subscription network stream.
+5. Every model-selected Tool still settles through Fugu's existing Palette,
+ imsg checks, and kernel-confined Tool Roles.
+6. The production inference host and path are compile-time profile data and
+ cannot be redirected by configuration.
+7. The credential is stored directly in the protected Fugu configuration.
+ No Owner/account-routing plumbing or credential-store discovery is added.
+8. Subscription mode is personal and interactive. It does not add hosting,
+ account pooling, quota aggregation, or traffic for another human.
+9. The route is visibly experimental and versioned. It never probes alternate
+ fingerprints or attempts to evade quotas, safety controls, or account
+ enforcement.
+10. A `store:false` conversation remains resumable from Fugu's Journal. Opaque
+ Codex reasoning continuity is bounded and journaled transactionally; no
+ hidden server-side conversation id becomes authoritative.
+
+## The seam
+
+The existing Provider seam is real: Anthropic, OpenAI Chat Completions, and
+Claude Subscription already translate the same canonical Projection. Codex
+Subscription is a fourth Adapter at that seam. Do not add a generic Provider
+vtable merely to remove four small switches; that would expose wire-specific
+state and make the interface shallower.
+
+The Codex Subscription adapter is one deep Module with an interface split by
+Role linkage:
+
+```c
+/* Credential-free: linked into the Coordinator and read-only Subagent Role. */
+int codex_sub_build_request(struct buf *, const struct codex_sub_req *);
+
+/* Credentialed/hostile: linked only into fugu-api. */
+int codex_sub_build_headers(char *, size_t, const struct codex_sub_auth *,
+ const struct codex_sub_wire *);
+struct codex_sub_stream *codex_sub_stream_new(astream_cb, void *);
+int codex_sub_stream_feed(struct codex_sub_stream *, const void *, size_t);
+const char *codex_sub_stream_error(const struct codex_sub_stream *);
+void codex_sub_stream_free(struct codex_sub_stream *);
+```
+
+`struct codex_sub_req` borrows only the model, output ceiling, system
+instructions, Tool definitions, canonical Projection, optional suffix, and
+non-secret session/request metadata. Callers do not know Codex item shapes,
+header names, event names, or compatibility constants. The stream emits the
+existing `astream_event` vocabulary plus one generic provider-state span, so
+Lead and Subagent Turn mechanics do not acquire Codex branches.
+
+Internally, keep profile constants, request translation, JWT/account
+extraction, credentialed headers, and Responses event decoding in separate C
+files for least linkage. They are one Module conceptually; those internal
+seams exist because the Coordinator must not link the hostile parser and the
+Custodian must not link canonical Projection construction.
+
+## Configuration and credential contract
+
+The minimal configuration is intentionally the same shape as Claude
+Subscription:
+
+```conf
+provider "codex"
+model "<configured Codex model>"
+oauth_token "<personal ChatGPT Codex access token>"
+```
+
+For named routes:
+
+```conf
+provider "personal-codex" {
+ type "codex"
+ model "<configured Codex model>"
+ oauth_token "<personal ChatGPT Codex access token>"
+}
+```
+
+For `codex`, reject `api_key`, `api_key_file`, `api_host`, `api_port`, and
+`api_path`. Do not add a configurable account selector. The credentialed half
+decodes the bounded JWT payload and extracts the one ChatGPT account identifier
+carried by the token; absence, ambiguity, an unsupported claim shape, or an
+expired token fails closed before inference.
+
+The first profile supports an ordinary personal account only. If token claims
+indicate FedRAMP, data-residency, compute-residency, or another routing regime,
+Fugu reports that the account profile is unsupported and sends nothing. It
+does not guess or omit a routing header required by that account.
+
+This local JWT parse is not signature verification and must never be described
+as authentication. Fugu has no reason to import an RSA/JWK stack or use raw
+libcrypto: the token comes from protected configuration over the trusted
+startup path, and the fixed OpenAI TLS endpoint remains authoritative for its
+signature, entitlement, account match, and expiry. Local issuer/audience/expiry
+checks are profile-consistency diagnostics plus header-injection defense. Every
+decoded claim is still length-bounded, type-checked, and NUL/CR/LF-rejected.
+
+The fixture must also prove that the current token fits `FUGU_SECRET_MAX` and
+the bounded HELLO payload. If it does not, increase those bounds deliberately
+and add chunk/aggregate tests; never truncate a JWT or quietly accept an
+unbounded auth bundle.
+
+Parse the token once in `fugu-api` while accepting HELLO, before any Provider
+request. Retain the raw token plus only the bounded account identifier and
+expiry needed by the wire profile. The Coordinator never decodes JWT claims,
+and request builders never receive either value.
+
+### Why v1 does not refresh automatically
+
+A personal ChatGPT access token is expiring state. Refresh can also produce a
+replacement refresh token. Persisting that replacement safely is not a small
+HTTP feature:
+
+- `fugu-api` cannot open or write files after loading the CA store (I1/I9);
+- the Coordinator can write its already-open Journal but must not receive a
+ model credential back from the Custodian (I1/I2);
+- `/etc/fugu.conf` is commonly root-owned and group-readable, not writable by
+ the invoking process;
+- rewriting an arbitrary yacc configuration with includes, macros, and
+ comments would itself be a new security-sensitive configuration editor.
+
+Therefore the first profile accepts one already-provisioned access token and
+fails hard when it expires or receives an authentication rejection. Token
+replacement is manual, like the current Claude profile. The provisioning
+procedure must never print another field from a credential store or retain a
+token in the repository, shell history, Journal, test fixture, or logs.
+
+A later, separately reviewed `fugu-codex-auth` utility may implement the
+documented device flow and print a configuration stanza to the controlling
+terminal. It would be a short-lived, non-setgid C program with `stdio inet
+dns`, no listener, no project access, and no ability to edit the configuration.
+It is not part of the inference adapter and is not required for the first live
+slice.
+
+Do not implement in-memory refresh until the upstream rotation contract is
+known and there is a persistence design that keeps the replacement credential
+inside one Custodian. Silently discarding a rotated refresh token would make a
+successful session break the next invocation.
+
+## Compatibility profile
+
+Create `src/common/codex_sub.h` and keep every volatile constant in
+`codex_sub_profile.c`:
+
+- profile identifier `chatgpt-codex-2026-07-17` and snapshot date;
+- pinned upstream Codex version and commit used as the behavioral reference;
+- fixed inference host `chatgpt.com`, TLS port `443`, and path
+ `/backend-api/codex/responses`;
+- observed OAuth issuer/audience and JWT claim paths used for bounded local
+ profile-consistency checks (not signature verification);
+- required bearer, account, user-agent, session, and request headers;
+- required Responses request flags and include fields;
+- model-specific request differences, if the evidence proves any;
+- event names and finish-status mappings that are part of the observed
+ profile.
+
+The HTTP profile identifies the client with `User-Agent: fugu/<version>` and
+does not send `originator` initially. Current direct-client evidence shows that
+the header is not intrinsic to bearer/account authentication, while reports
+about accepted values conflict. Fugu never sends `originator: codex_cli_rs` or
+another first-party identity. If the live gate requires an originator, only an
+honest `fugu` value is eligible; rejection of that value stops the Adapter.
+
+The SSE profile does not send the WebSocket-only `OpenAI-Beta` value. Headers
+are a single compile-time profile, not a retry matrix. The implementation does
+not vary capitalization, user-agent, beta values, or client identity in search
+of an accepted fingerprint.
+
+The production endpoint check follows `provider_endpoint_ok()` for Claude:
+only the compiled host/port/path is accepted. A non-setgid-only
+`FUGU_REGRESS_CODEX_PORT` substitution may target `127.0.0.1` for TLS fixtures;
+`fugu-api` independently validates that substitution after HELLO.
+
+Updating the profile is a source change. It requires new exact fixtures, the
+full normal and hardened gates, and an explicit personal live check. Fugu does
+not negotiate a profile, scrape the installed Codex version, or retry with
+alternate headers.
+
+## Request translation
+
+`codex_sub_build_request()` re-renders the full canonical Projection into the
+observed Responses input format. It never mutates or relinks canonical
+messages. The implementation owns all of these details behind its interface:
+
+1. The composed Fugu system block becomes the profile's `instructions`
+ representation.
+2. User text becomes Responses user input text.
+3. Assistant text becomes Responses assistant output text.
+4. Canonical Tool use becomes a function-call item. Canonical Tool result
+ becomes the matching function-call-output item. Fugu's canonical Tool id is
+ the Responses `call_id`; a separate `fc_...` output-item id, when present,
+ comes only from the matching provider-state attachment.
+5. Tool definitions become only the function tools Fugu offered for this
+ Generation. Built-in Codex shell, patch, web, computer, and MCP tools are
+ never advertised.
+6. `stream` is true, `store` is false, and `previous_response_id` is absent.
+ The request asks for `reasoning.encrypted_content`.
+7. The pinned first profile omits `max_output_tokens`: current direct clients
+ remove it for Codex compatibility. Fugu's independent Turn and response
+ byte ceilings remain authoritative. Consequently, `max_tokens` is not a
+ provider-side output-token limit for `codex` v1; the manual and ADR must say
+ so rather than implying exact enforcement.
+8. Reasoning and text options use one pinned profile default. Configurable
+ thinking effort belongs to the separate model-thinking ticket and does not
+ widen this slice.
+9. Prompt-cache/session metadata is deterministic per Fugu Session and never
+ contains a credential, path, prompt excerpt, invoking username, or account
+ identifier.
+10. A matching opaque continuity state attached to an assistant message
+ restores reasoning items and function-call item ids while rendering text,
+ Tool names, arguments, and results from canonical blocks. Mismatched
+ Provider, profile, or model state is omitted.
+
+Tool names remain canonical unless the pinned fixture proves a reversible wire
+mapping is required. Never copy Codex's built-in tool definitions or system
+prompt: Fugu is the agent and its Palette is the authority.
+
+### Opaque continuation state
+
+Encrypted reasoning is required v1 state, not a discovery gate. OpenAI and
+independent clients request, retain, and replay the completed reasoning item so
+that a `store:false` function-call conversation can continue. Responses also
+uses both a canonical `call_id` and a provider output-item id such as `fc_...`;
+independent clients preserve the pair because reasoning/function-call pairing
+can be validated by those item ids. Fugu carries both through a generic
+envelope while keeping Codex item semantics inside the Adapter.
+
+Extend `struct msg` with an optional provider-state attachment separate from
+its canonical content blocks. The attachment is one bounded byte string with
+this logical envelope:
+
+```text
+provider = codex
+profile = chatgpt-codex-2026-07-17
+model = <model that produced it>
+kind = responses-continuity
+data = {
+ reasoning: [<output index plus completed reasoning item>, ...],
+ calls: [<output index, call_id, and provider item id>, ...]
+}
+```
+
+The envelope is generic infrastructure; `msg` and the Journal copy, free, and
+round-trip it but do not understand the data. It is not a text block, Tool
+block, UI event, summary, or token credential. Other Provider request builders
+ignore it. `codex_sub_build_request()` is the only credential-free consumer and
+revalidates the envelope and each raw JSON object before emitting state that
+matches the active Provider, profile, and model.
+
+The request Adapter joins call metadata to canonical Tool blocks by exact
+`call_id`; it never trusts state for the Tool name or arguments. Missing state
+may omit an optional item id, but duplicate, conflicting, unknown, or
+many-to-one mappings fail request construction. The completed output indices
+preserve reasoning/function-call order. Do not concatenate ids into
+`block.tool_id`: Tool execution and Tool results continue to use only the
+canonical `call_id`.
+
+The hostile path is explicit:
+
+1. `codex_sub_stream` accepts only final reasoning and function-call metadata;
+ partial items are never retained. A reasoning item requires nonempty
+ `encrypted_content`; a call record requires bounded, distinct `call_id` and
+ provider item id. It normally takes `response.output_item.done`. If
+ `response.completed.output` repeats an item, identity and content must agree;
+ if terminal output is the only final copy, it supplies the item. Duplicate
+ or conflicting copies fail.
+2. It emits `ASTREAM_PROVIDER_STATE` records to `fugu-api`. The Custodian
+ validates each complete record, applies credential-reflection filtering,
+ and builds one deterministic continuity object under a 2 MiB
+ per-Generation cap.
+3. Immediately before a successful `A_DONE`, `fugu-api` sends the envelope in
+ `FUGU_IMSG_A_PROVIDER_STATE` chunks. `generation` buffers it under the same
+ cap and attaches it only when building a normally completed assistant
+ message.
+4. `turn_txn_accept()` journals the assistant message and its attachment before
+ Projection visibility. Existing Turn commit/abandon replay rules therefore
+ make the state atomic with the Tool call that needs it.
+
+Journal `msg` records encode the opaque data as a JSON string, not as raw
+record syntax. Replay requires the exact provider/profile/model/kind fields, a
+single valid data object, unique output/call/item identities, and all
+byte/count bounds. A malformed attachment
+invalidates the containing Turn just as a malformed Tool block does; it is
+never partially recovered.
+
+Lead state survives a committed resume and is discarded by abandonment,
+`/clear`, and `/compact` with the messages it describes. Subagent state lives
+in the Subagent's existing in-memory conversation for its Tool rounds and is
+freed with that conversation; it never enters the Lead Journal. State from an
+older profile or another model remains inert history and is not replayed to the
+backend.
+
+The state bytes count toward the 16 MiB serialized request ceiling but not the
+visible 4 MiB Turn text/Tool budget or Context token meter. The independent 2
+MiB state cap prevents opaque reasoning from consuming the full request or
+Journal-record allowance.
+
+Do not substitute `previous_response_id`. Hidden server state would make
+resume fidelity, `/clear`, crash recovery, and retention semantics depend on
+the backend.
+
+## Streaming translation
+
+`codex_sub_stream` is a dedicated Responses SSE decoder, not a mode in the
+Chat Completions decoder. It incrementally parses bounded `data:` events and
+emits the existing canonical `astream_event` values plus the generic
+`ASTREAM_PROVIDER_STATE` carrier described above. No Codex event type crosses
+the seam.
+
+The decoder must:
+
+- accept output-text deltas with explicit lengths;
+- correlate function calls by bounded output index and call identifier;
+- buffer fragmented function names and argument JSON under the existing
+ per-call and aggregate Tool caps;
+- validate every completed argument as exactly one JSON object before exposing
+ any completed Tool batch;
+- map one completed response to exactly one `ASTREAM_DONE`;
+- map response failure, cancellation, or incomplete terminal status to a
+ bounded Provider error rather than success;
+- emit at most one usage event, with nonnegative bounded counters;
+- treat Codex `input_tokens` as inclusive of cached input and expose cached
+ input only as the `cache_read` detail, matching the current OpenAI accounting
+ convention;
+- reject output Tool kinds Fugu did not offer;
+- retain only completed reasoning items and function-call identity records,
+ require nonempty encrypted content and distinct bounded ids, and preserve
+ their response order in one bounded continuity object;
+- ignore only well-formed informational events explicitly classified by the
+ profile; unknown terminal or output-item semantics fail closed;
+- reject data after a terminal event, duplicate terminals, conflicting item
+ identities, malformed UTF-8/JSON where the existing JSON contract requires
+ it, embedded NUL in identifiers, and every cap overrun.
+
+Reasoning summary text is not assistant output. It is ignored unless a future
+Fugu thinking interface explicitly adopts it. Opaque reasoning bytes use the
+provider-state carrier and never cross the existing text event.
+
+All decoded Tool names still pass through the existing canonical Turn and Tool
+authorization path before work can execute; `fugu-api` performs the hostile
+identifier, sequence, bound, and credential-reflection checks but does not gain
+Tool authority. Credential-reflection filtering covers assistant text, Tool
+identifiers, Tool arguments, error bodies, stop reasons, and every configured
+subscription token.
+
+## Transport, retries, and accounting
+
+Reuse the existing HTTP/1.1 libevent/libtls request state machine and Responses
+SSE. Codex does not add a socket type, daemon, proxy, cookie jar, HTTP library,
+or WebSocket implementation in v1. OpenAI's current client prefers WebSocket,
+but it and independent clients retain SSE fallback. If the live endpoint no
+longer accepts SSE, implementation stops for a separate WebSocket design; it
+does not silently grow a second transport inside this slice.
+
+- `Authorization: Bearer` and all profile identity/account headers are built
+ only in `fugu-api`, immediately before the HTTP request head.
+- No cookies, Platform API key, `previous_response_id`, first-party
+ `originator`, or WebSocket beta header are sent.
+- Redirects fail closed. A bearer token is never followed to a Location chosen
+ by the backend, even when the destination appears to be another OpenAI host.
+- HTTP 429 and 5xx before the first canonical output event use Fugu's existing
+ five-attempt backoff and bounded `Retry-After` handling.
+- A 401/403 is never refreshed, retried with another identity, or sent to the
+ Platform endpoint. It names the active profile and tells the Owner the token
+ may need replacement.
+- Once text, a Tool call, or another externally visible canonical event has
+ streamed, failure ends the Generation; it is never replayed automatically.
+- Subscription quota headers may be retained in bounded diagnostic state, but
+ they are not token counts and do not alter the Context meter.
+- `PROVIDER_CODEX` follows OpenAI accounting: input tokens already include
+ cached input, so `lead_context_tokens()` must not add `cache_read` again.
+ Subagent usage remains excluded from the Lead's Context display.
+
+Model discovery is not assumed. `/model` contributes the configured Codex
+model and its compiled/configured Effective window, just as Claude Subscription
+does. No subscription token is sent to `/v1/models`.
+
+## Security review
+
+The design preserves the existing Role topology:
+
+```text
+canonical Projection hostile subscription SSE
+ | |
+Coordinator / Subagent v
+ | credential-free JSON fugu-api Custodian
+ +------------------------------>| |
+ | | | fixed TLS endpoint
+ | canonical events + bounded | | Bearer + account headers
+ | validated provider state | v
+ |<------------------------------+ ChatGPT Codex backend
+ v
+mode-0600 Lead Journal
+```
+
+No Worker gains a new channel. The Coordinator briefly holds the configured
+token only during startup, passes it once through HELLO, wipes its buffers and
+configuration copy, and never receives it again. `fugu-api` retains `stdio
+inet dns`, cannot open or write files, cannot execute, and remains the sole
+Role linked with libtls and the hostile Codex stream parser.
+
+The only new data crossing back from the Custodian is the bounded reasoning
+envelope. It is not an OAuth token, cannot select a Tool or endpoint, is
+credential-reflection filtered, and is never displayed. The Coordinator treats
+live bytes opaquely; journal replay and the request Adapter independently
+revalidate their exact JSON envelope before they can be sent back to the same
+fixed Provider/profile. This adds data handling, not authority, to the
+Coordinator.
+
+The profile does require normative wording changes before acceptance:
+
+- invariant I1 currently names only a fixed Anthropic subscription endpoint;
+ it must permit each accepted Subscription adapter's separately fixed TLS
+ endpoint;
+- the charter currently names only Claude Subscription in the native-runtime
+ and experimental-protocol clauses;
+- `CONTEXT.md` defines Subscription adapter as Claude-specific;
+- behavior and verification currently enumerate three Provider routes.
+
+Those are specification amendments, not implementation details. Record the
+decision in a new ADR rather than silently broadening ADR-0007, which remains
+the Claude-specific decision.
+
+## Source-change map
+
+Expected production changes:
+
+| Area | Files | Change |
+|---|---|---|
+| Profile interface | `src/common/codex_sub.h`, `codex_sub_profile.c` | Fixed profile, token/account validation, constants |
+| Request codec | `src/common/codex_sub_req.c` | Canonical Projection to Responses input |
+| Stream codec | `src/common/codex_sub_stream.c` | Responses SSE to `astream_event` |
+| Credentialed wire | `src/common/codex_sub_wire.c` | Bearer/account/profile headers and bounded errors |
+| State carrier | `src/common/msg.[ch]`, `generation.[ch]`, `provider_stream.h` | Generic bounded attachment and event span |
+| Durable state | `src/fugu/journal.c`, `turn_txn.c`, `proto.h` | Transactional encoding/replay and chunked imsg |
+| Provider taxonomy | `src/fugu/proto.h`, `conf.[ch]`, `parse.y`, `agentcfg.c` | `PROVIDER_CODEX`, config validation, route availability |
+| Lead/Subagent | `src/fugu/coord.c`, `src/fugu-tool/agent.c` | Invoke the credential-free request Adapter |
+| Custodian | `src/fugu-api/main.c` | Fixed endpoint validation, auth bundle, stream selection, accounting |
+| Build | three role Makefiles, `regress/Makefile` | Link only the required profile half |
+| Documentation | README, manuals, CONTEXT, handoff, ADR, live checks | Experimental contract and setup |
+
+Do not combine this work with a general Provider registry refactor, Responses
+support for Platform API keys, configurable reasoning effort, automatic
+compaction, or UI redesign.
+
+## Verification
+
+Add `regress/codex_subscription/` with fixture-driven tests for:
+
+1. exact request JSON for text, mixed text/Tool history, Tool-only assistant
+ output, Tool errors, steers, compaction suffixes, empty optional fields,
+ matching continuity state, preserved function-call item ids, and omitted
+ mismatched profile/model state;
+2. exact fixed headers with a synthetic JWT and account claim; wrong issuer,
+ audience, expiry, claim type, NUL, CR/LF, oversize token, and ambiguous
+ account fail closed;
+3. every SSE byte split, multi-event reads, interleaved function calls,
+ fragmented names/arguments, usage, normal completion, explicit failure,
+ incomplete response, duplicate/conflicting items, output after terminal,
+ completed versus partial reasoning items, multiple reasoning items,
+ `call_id`/item-id pairing and conflicts, unknown Tool types, malformed JSON,
+ embedded NUL, and every byte/count cap;
+4. credential reflection split across adjacent text, Tool, reasoning-state,
+ error, and HTTP-body fragments;
+5. config acceptance/redaction/wiping for implicit and named `codex` routes,
+ and rejection of API keys, endpoint overrides, match-block secrets, missing
+ token, and silent fallback;
+6. full TLS worker tests proving the Coordinator sends credential-free JSON,
+ only `fugu-api` adds auth, Lead and Subagent Tool rounds complete, 401/403
+ fail once, 429/5xx follow bounded retry, and cancellation preserves the Turn
+ transaction;
+7. provider-state imsg chunking, normal-completion attachment, Journal
+ round-trip, fresh-process Tool continuation, abandoned-turn exclusion,
+ `/clear`, `/compact`, corrupted records, profile/model mismatch, and the 2
+ MiB cap on both sides of each seam;
+8. model switching, Context accounting without cache double-counting, cold
+ cache state, and configured-model-only discovery;
+9. existing ktrace proofs that the Custodian cannot open, write, or execute.
+
+Fixtures contain only synthetic tokens and captured response shapes scrubbed
+of prompts, account identifiers, request identifiers, and credentials. Exact
+fixture provenance records the upstream version/commit and capture date.
+
+The opt-in live check uses the ordinary Owner's configured route and spends
+subscription quota deliberately. It performs one exact text response and one
+harmless `read` Tool round, confirms HTTP SSE with the honest fixed header
+profile, and verifies that the completed continuity state is present in the
+mode-0600 Journal while no configured token appears in terminal output,
+Journal, syslog, or captured diagnostics. It never runs in `make check` and
+never runs as root.
+
+## Implementation slices
+
+Land the work as small, independently reviewable commits:
+
+1. **Evidence and ADR.** Accept the profile risk, fixed endpoints, manual token
+ lifetime, and normative wording changes. No code.
+2. **Provider-state carrier.** Add the generic `msg` attachment, Generation and
+ imsg chunking, Journal transaction/replay, caps, and hermetic regressions.
+ No existing Provider emits or consumes it.
+3. **Pure codec.** Add profile/request/stream Modules for text, function calls,
+ usage, terminal events, and reasoning state. Synthetic regressions exercise
+ the entire codec; no configuration path can select it.
+4. **Credential custody.** Add `PROVIDER_CODEX`, config validation, fixed HELLO
+ endpoint, JWT/account extraction, header attachment, redaction, and 401
+ behavior. A text-only local TLS fixture proves the complete Role path.
+5. **Canonical Tool and resume loop.** Prove one Lead Tool round, state replay
+ on the continuation request, committed resume in a fresh process, and
+ abandonment without state leakage.
+6. **Subagents and routing.** Add named route selection, configured-model
+ discovery, concurrent read-only Subagents, cancellation, and accounting.
+7. **Credentialed live gate.** With explicit Owner opt-in, prove the exact SSE
+ profile, account header, text response, Tool continuation, encrypted-state
+ capture, and quota/error behavior. Stop if honest identity is rejected.
+8. **Release surface.** Manuals, sample configuration, profile disclosure,
+ live-check opt-in, package/linkage checks, and full normal/hardened gates.
+9. **Provisioning follow-up.** Only after the adapter is stable, decide whether
+ a separate pure-C device-flow utility is worth its authentication surface.
+
+Each commit keeps all existing direct Anthropic, OpenAI, and Claude
+Subscription regressions green. No intermediate commit silently selects the
+new route as a default.
+
+## Cost estimate
+
+Opaque provider state is included in the baseline:
+
+| Work | Production | Regressions/docs |
+|---|---:|---:|
+| Request/profile/JWT helpers | 500-800 C lines | 500-800 test lines |
+| Responses stream decoder and state extraction | 650-1,000 C lines | 850-1,300 test lines |
+| Generic state carrier, imsg, Generation, and Journal | 450-750 C lines | 650-1,000 test lines |
+| Config/custody/routing integration | 300-500 C lines | 400-700 test lines |
+| Whole-Turn/Subagent fixtures and release docs | 150-300 C/shell lines | 600-1,000 test/doc lines |
+| **Total** | **2,050-3,350** | **3,000-4,800** |
+
+A device-flow provisioning utility plus durable refresh store is another
+roughly 700-1,300 production lines and 700-1,200 regression/doc lines because
+atomic credential persistence, rotation, crash recovery, and pledge/unveil
+proofs dominate the HTTP exchange. A WebSocket fallback would separately add
+roughly 600-1,000 C lines and 700-1,200 adversarial test lines. Neither is in
+the v1 estimate.
+
+The stream decoder and credentialed whole-Role fixtures dominate risk, not the
+provider enum or configuration grammar. The first useful tracer bullet is
+therefore one fixed-profile, text-only Generation through a local TLS stub,
+followed immediately by the real subscription text-and-Tool live gate.
+
+## Acceptance gates
+
+Research closes the static questions: the account id comes from the token;
+the fixed endpoint is known; Responses fields, SSE events, and encrypted-state
+continuity are pinned; and multiple direct implementations prove that an SDK
+process is unnecessary. Implementation still has these stop/go gates:
+
+| Gate | How it closes | Failure consequence |
+|---|---|---|
+| Normative authorization | New ADR and charter/invariant wording accept the experimental OpenAI endpoint and manual token lifetime | Do not make `codex` selectable |
+| Token envelope | Synthetic and real tokens fit the configured secret/HELLO bounds; issuer, audience, expiry, account, and residency claims match the profile | Increase bounds deliberately or reject the profile; never truncate |
+| Honest HTTP identity | Opt-in live request succeeds over HTTP SSE with Fugu's fixed user-agent, no cookies, and no first-party impersonation | Stop; do not probe identities |
+| Model/request profile | Configured model accepts the exact instructions, tools, reasoning defaults, and omitted output ceiling | Pin a reviewed source change or stop |
+| Durable Tool continuation | Local TLS and live Tool rounds replay completed encrypted state; fresh-process resume sends the same bounded state | Do not release Tool-enabled Codex support |
+| Manual-token usability | The Owner accepts replacement at JWT expiry for the experimental release | Design credential persistence before release |
+| Security gates | Normal, hardened, ktrace, secret-reflection, Journal, and package checks all pass | Do not ship the route |
+
+No credentialed experiment may log a real token, full prompt, account
+identifier, or unredacted response. A failed gate narrows or stops the design;
+it does not justify fingerprint probing, account rotation, or metered fallback.
blob - 4bf8c11f404c33c0431efa925ed9dcb13de14855
blob + d246d795a3bbf6ae26e363738c23e91767f82a41
--- docs/design/m5-tools.md
+++ docs/design/m5-tools.md
The worker validates and `pread`s the snapshot, unveiling every path
with empty permissions before any untrusted input is touched (I5/I9).
The working directory is inherited through `fork`, so no cwd path is
-sent.
+sent. Every tool role also unveils `/dev/null` with `rwc` permissions so
+ordinary shell redirection works without generating an unveil-denial
+accounting record. The `c` permission is needed because shells open output
+redirections with `O_CREAT`; it applies only to that exact device path and
+does not expose any other path.
### Why the subprocess pledge is broad
- **tools** — unit-level executor behavior on a temp tree under the
real unveil/pledge: read windowing and caps, atomic mode-preserving
write, edit uniqueness, multi_edit all-or-nothing, shell cap and
- timeout, grep/find/ls caps, NUL rejection, freshness staleness,
- protect ENOENT-shaping.
+ timeout, `/dev/null` redirection, grep/find/ls caps, NUL rejection,
+ freshness staleness, protect ENOENT-shaping.
- **sandbox** — ktrace/kdump PLDG records: a `shell` subprocess that
attempts `connect` trips a pledge violation (I4); an out-of-tree
write and a write to a `protect` path fail ENOENT-shaped (I5); under
blob - b276003cb91cfc767e9147af854ff9031fdd8fab
blob + a2d0f85c129b3981c63a794afb366ca4adefbf63
--- docs/design/m7-curses.md
+++ docs/design/m7-curses.md
event (NUL rejected, other ESC stripped, 64 KiB cap, overflow
discarded with a notice). Decoded events feed the pure editor/UI
model; nothing about input depends on `wgetch`, so it is testable.
+`Ctrl-L` records a reflow-stable display boundary and forces a physical clear.
+The tail view starts after that boundary, while scrolling up can still reach
+earlier lines. It sends no coordinator message and does not alter conversation
+context, journal state, current input, prompt history, or queued messages.
Bracketed-paste mode is enabled on taking the terminal and disabled on
every teardown path the process survives; a pledge-violation death
blob - /dev/null
blob + 48ff278dc4595ac90125f107827d360be3388b88 (mode 644)
--- /dev/null
+++ docs/research/codex-subscription-native-clients.md
+# Native clients for ChatGPT Codex subscription inference
+
+Research snapshot: 2026-07-17.
+
+## Answer
+
+A pure-C Codex subscription Adapter is technically feasible. The observed
+inference path is ordinary HTTPS plus JSON and a Responses event stream; it
+does not intrinsically require the Codex executable, Node.js, Python, or an
+OpenAI SDK. Several independent clients perform the same subscription
+inference directly in TypeScript or Go.
+
+No maintained pure-C implementation was found in repository and code searches
+for the endpoint, account header, OAuth client, and encrypted-reasoning field.
+That is not proof that none exists. It does mean Fugu cannot sensibly vendor
+or port a known C library: it must implement and test a small compatibility
+profile against the primary sources.
+
+The important qualification is contractual rather than technical. OpenAI
+documents ChatGPT-plan access through Codex and documents app-server/SDK
+integration, but it does not document the raw ChatGPT backend as a stable
+third-party API. A direct client is therefore an unsupported, volatile
+compatibility implementation. It must be experimental, identify itself as
+Fugu, use one personal account, and fail closed rather than impersonating
+Codex, probing fingerprints, pooling accounts, or falling back to metered API
+billing.
+
+## Research method
+
+The assessment used:
+
+- current OpenAI product documentation for the supported boundary;
+- a pinned checkout of OpenAI's Codex source as the primary wire reference;
+- pinned source checkouts of independent clients, following the actual request,
+ stream, history, and refresh paths rather than README claims;
+- broad GitHub/web searches for the endpoint, `ChatGPT-Account-ID`,
+ `reasoning.encrypted_content`, Codex OAuth, and C implementations.
+
+No real credential, account identifier, prompt, or response was captured for
+this report. A credentialed Fugu fixture remains an explicit implementation
+gate.
+
+## Supported product boundary
+
+OpenAI says Codex is included with eligible ChatGPT plans and that signing in
+with ChatGPT is the way to use that allowance. Supplying a Platform API key is
+a different authentication mode billed through the API account. The
+documented integration surfaces are the Codex
+[app-server](https://learn.chatgpt.com/docs/app-server) and
+[SDK](https://learn.chatgpt.com/docs/codex-sdk); the SDK starts a Codex runtime
+and its supported language packages are not a native C inference library. See
+also [Using Codex with your ChatGPT plan](https://help.openai.com/en/articles/11369540-using-codex-with-your-chatgpt-plan).
+
+That supported route conflicts with Fugu's two key constraints: Fugu remains
+the agent, and the model-credential Custodian does not gain filesystem or
+execution authority. The direct Adapter is useful precisely because it keeps
+those constraints, but it receives no stability guarantee from the product
+documentation.
+
+## Primary wire reference: OpenAI Codex
+
+Reference checkout:
+
+- repository: [openai/codex](https://github.com/openai/codex)
+- commit: [`315195492c80fdade38e917c18f9584efd599304`](https://github.com/openai/codex/tree/315195492c80fdade38e917c18f9584efd599304)
+- license: [Apache-2.0](https://github.com/openai/codex/blob/315195492c80fdade38e917c18f9584efd599304/LICENSE)
+
+The source establishes the following profile:
+
+1. ChatGPT authentication selects
+ `https://chatgpt.com/backend-api/codex`, while API-key authentication uses
+ the Platform endpoint. The provider's wire API is Responses
+ ([provider selection](https://github.com/openai/codex/blob/315195492c80fdade38e917c18f9584efd599304/codex-rs/model-provider-info/src/lib.rs#L35-L60),
+ [auth-dependent URL](https://github.com/openai/codex/blob/315195492c80fdade38e917c18f9584efd599304/codex-rs/model-provider-info/src/lib.rs#L241-L259)).
+2. Requests carry a bearer access token and, when present, the ChatGPT account
+ identifier. Residency-aware accounts add a separate routing header
+ ([auth headers](https://github.com/openai/codex/blob/315195492c80fdade38e917c18f9584efd599304/codex-rs/model-provider/src/bearer_auth_provider.rs#L31-L45)).
+3. The token bundle contains access, ID, and refresh tokens. The account id is
+ read from the namespaced ChatGPT auth claim, not from Fugu configuration
+ routing
+ ([token and claim model](https://github.com/openai/codex/blob/315195492c80fdade38e917c18f9584efd599304/codex-rs/login/src/token_data.rs#L10-L160)).
+4. The request is `store:false`, streams a full Responses input, and asks for
+ `reasoning.encrypted_content`
+ ([request shape](https://github.com/openai/codex/blob/315195492c80fdade38e917c18f9584efd599304/codex-rs/codex-api/src/common.rs#L215-L239),
+ [client construction](https://github.com/openai/codex/blob/315195492c80fdade38e917c18f9584efd599304/codex-rs/core/src/client.rs#L890-L907)).
+5. The decoder handles text, function calls, reasoning items, usage, completed,
+ failed, and incomplete Responses events
+ ([SSE event mapping](https://github.com/openai/codex/blob/315195492c80fdade38e917c18f9584efd599304/codex-rs/codex-api/src/sse/responses.rs#L327-L472)).
+6. Current Codex prefers a Responses WebSocket when available but retains an
+ HTTP event-stream fallback
+ ([transport selection and fallback](https://github.com/openai/codex/blob/315195492c80fdade38e917c18f9584efd599304/codex-rs/core/src/client.rs#L1766-L1825)).
+7. Access tokens are refreshed proactively and the resulting access, ID, and
+ refresh tokens are persisted together
+ ([refresh decision](https://github.com/openai/codex/blob/315195492c80fdade38e917c18f9584efd599304/codex-rs/login/src/auth/manager.rs#L2506-L2528),
+ [persist and reload](https://github.com/openai/codex/blob/315195492c80fdade38e917c18f9584efd599304/codex-rs/login/src/auth/manager.rs#L2593-L2610)).
+
+The last point matters architecturally: automatic refresh is a durable
+credential-state feature, not merely another HTTP request.
+
+## Independent implementations
+
+| Project | Runtime | Direct subscription path | Transport | Durable/continuation behavior |
+|---|---|---|---|---|
+| [pi-mono](https://github.com/badlogic/pi-mono/tree/64f83c85d9f36c831e96b29477750f92cd5a2230) | TypeScript | Yes | WebSocket first, SSE fallback | Persists and replays encrypted reasoning with full history |
+| [openai-oauth](https://github.com/EvanZhouDev/openai-oauth/tree/ec7dab2fcd8dab9da970a7a2b5dc34046c94905e) | TypeScript | Yes | HTTP Responses/SSE | Expands cached references into bounded, stateless full history |
+| [docker-agent](https://github.com/docker/docker-agent/tree/1ec6c9a1835ed4d9476960faabda57c1107a7d22) | Go | Yes | WebSocket with pooled connection state | Refreshes and atomically stores rotated credentials |
+| [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI/tree/9f4f53ca5a4d1474e3f7eb61d6ffc984995f1f66) | Go | Yes, behind a translation proxy | SSE and WebSocket | Explicit reasoning-replay cache and invalid-signature recovery |
+| [Hermes Agent](https://github.com/NousResearch/hermes-agent/tree/abc22cdf1a5c0fe30bf1a226bfe3caf489e8316e) | Python | Yes; it also offers app-server mode | Responses transport | Persists endpoint-tagged reasoning and its own refresh-token pair |
+
+### pi-mono
+
+pi is the clearest small direct-client reference. Its request sets
+`store:false`, streams full input, asks for encrypted reasoning, and uses
+WebSocket with SSE fallback
+([request body](https://github.com/badlogic/pi-mono/blob/64f83c85d9f36c831e96b29477750f92cd5a2230/packages/ai/src/api/openai-codex-responses.ts#L482-L585),
+[SSE and WebSocket paths](https://github.com/badlogic/pi-mono/blob/64f83c85d9f36c831e96b29477750f92cd5a2230/packages/ai/src/api/openai-codex-responses.ts#L693-L803)).
+Its shared Responses history code explicitly retains the encrypted item so a
+stateless `store:false` request can continue
+([reasoning replay](https://github.com/badlogic/pi-mono/blob/64f83c85d9f36c831e96b29477750f92cd5a2230/packages/ai/src/api/openai-responses-shared.ts#L392-L408),
+[capture on completion](https://github.com/badlogic/pi-mono/blob/64f83c85d9f36c831e96b29477750f92cd5a2230/packages/ai/src/api/openai-responses-shared.ts#L533-L591)).
+Its OAuth implementation persists access, refresh, expiry, and account state
+([OAuth exchange and refresh](https://github.com/badlogic/pi-mono/blob/64f83c85d9f36c831e96b29477750f92cd5a2230/packages/ai/src/auth/oauth/openai-codex.ts#L126-L187),
+[account extraction and storage](https://github.com/badlogic/pi-mono/blob/64f83c85d9f36c831e96b29477750f92cd5a2230/packages/ai/src/auth/oauth/openai-codex.ts#L395-L414)).
+
+### openai-oauth
+
+This project is useful because its current path is stateless HTTP rather than
+a Codex wrapper. It forces `store:false`, `stream:true`, instructions, and
+encrypted reasoning, then sends directly to the ChatGPT backend
+([request normalization](https://github.com/EvanZhouDev/openai-oauth/blob/ec7dab2fcd8dab9da970a7a2b5dc34046c94905e/packages/core/src/runtime.ts#L570-L663),
+[auth headers and request](https://github.com/EvanZhouDev/openai-oauth/blob/ec7dab2fcd8dab9da970a7a2b5dc34046c94905e/packages/core/src/runtime.ts#L811-L827),
+[direct fetch](https://github.com/EvanZhouDev/openai-oauth/blob/ec7dab2fcd8dab9da970a7a2b5dc34046c94905e/packages/core/src/runtime.ts#L914-L1010)).
+Its bounded state cache expands response references back to full history
+([state expansion](https://github.com/EvanZhouDev/openai-oauth/blob/ec7dab2fcd8dab9da970a7a2b5dc34046c94905e/packages/core/src/state.ts#L111-L203)); its compatibility endpoint deliberately rejects hidden
+`previous_response_id` state
+([stateless contract](https://github.com/EvanZhouDev/openai-oauth/blob/ec7dab2fcd8dab9da970a7a2b5dc34046c94905e/packages/openai-oauth/src/responses.ts#L4-L37)).
+
+This is strong evidence that HTTP SSE remains a viable first transport and
+that Fugu can preserve resume fidelity without a server-side conversation id.
+
+### docker-agent
+
+Docker Agent directly advertises ChatGPT subscription support and refreshes
+when the token is near expiry
+([credential lifecycle](https://github.com/docker/docker-agent/blob/1ec6c9a1835ed4d9476960faabda57c1107a7d22/pkg/chatgpt/chatgpt.go#L33-L85)).
+It handles the refresh response and persists any replacement refresh token
+([token refresh](https://github.com/docker/docker-agent/blob/1ec6c9a1835ed4d9476960faabda57c1107a7d22/pkg/chatgpt/token.go#L35-L121)).
+The model provider adds bearer/account/session headers and forces the Codex
+Responses settings
+([provider request](https://github.com/docker/docker-agent/blob/1ec6c9a1835ed4d9476960faabda57c1107a7d22/pkg/model/provider/openai/chatgpt.go#L61-L133)).
+
+It is useful behavioral evidence, but not a dependency candidate: its
+implementation brings the OpenAI Go SDK, a WebSocket package, and a Go runtime,
+all outside Fugu's native dependency ceiling.
+
+### CLIProxyAPI and Hermes
+
+CLIProxyAPI is a larger translation proxy, but its source independently
+confirms the same endpoint, HTTP event stream, WebSocket beta, request flags,
+and replay of the final encrypted reasoning item. In particular, it removes
+`previous_response_id`, asks for `reasoning.encrypted_content`, and caches the
+completed reasoning item for the next request
+([request and endpoint](https://github.com/router-for-me/CLIProxyAPI/blob/9f4f53ca5a4d1474e3f7eb61d6ffc984995f1f66/internal/runtime/executor/codex_executor.go#L779-L849),
+[WebSocket profile and HTTP fallback](https://github.com/router-for-me/CLIProxyAPI/blob/9f4f53ca5a4d1474e3f7eb61d6ffc984995f1f66/internal/runtime/executor/codex_websockets_executor.go#L37-L47),
+[reasoning replay](https://github.com/router-for-me/CLIProxyAPI/blob/9f4f53ca5a4d1474e3f7eb61d6ffc984995f1f66/internal/runtime/executor/codex_executor.go#L466-L520),
+[Codex request normalization](https://github.com/router-for-me/CLIProxyAPI/blob/9f4f53ca5a4d1474e3f7eb61d6ffc984995f1f66/internal/translator/codex/openai/responses/codex_openai-responses_request.go#L15-L45)).
+
+Hermes is additional operational evidence for a direct Python path. Its
+reasoning records are tagged by issuing endpoint before replay, and its auth
+store deliberately remains separate from Codex's because refresh-token
+rotation can invalidate a session
+([reasoning issuer boundary](https://github.com/NousResearch/hermes-agent/blob/abc22cdf1a5c0fe30bf1a226bfe3caf489e8316e/agent/codex_responses_adapter.py#L20-L52),
+[separate token custody](https://github.com/NousResearch/hermes-agent/blob/abc22cdf1a5c0fe30bf1a226bfe3caf489e8316e/hermes_cli/auth.py#L3311-L3365)).
+
+### A claimed implementation that is not evidence
+
+[`Securiteru/codex-openai-proxy`](https://github.com/Securiteru/codex-openai-proxy/tree/8be4f5616a4a9da0706c05444a97f4afe8f7c709)
+advertises a Rust Codex-to-OpenAI proxy. At the pinned commit, however, the
+active request handler returns a canned response while the apparent backend
+function is dead code
+([active handler](https://github.com/Securiteru/codex-openai-proxy/blob/8be4f5616a4a9da0706c05444a97f4afe8f7c709/src/main.rs#L223-L249)).
+It is excluded from the feasibility conclusion. This is why the report treats
+running source paths, tests, and persistence behavior as evidence rather than
+repository descriptions.
+
+## Cross-implementation findings
+
+### 1. The SDK is avoidable; the protocol risk is not
+
+Every credible direct implementation constructs JSON, adds a bearer and
+account header, and decodes Responses events itself. Fugu can do those things
+with its existing JSON, libevent, libtls, HTTP, and chunked-imsg Modules. A
+language runtime would add no necessary protocol capability.
+
+The endpoint and accepted request profile are still undocumented. Compile-time
+profile constants, exact fixtures, and an opt-in live check are therefore part
+of the Adapter, not optional release polish.
+
+### 2. Encrypted reasoning is continuation state
+
+OpenAI requests it, and independent clients persist/replay it specifically for
+subsequent function-call rounds under `store:false`. Fugu must retain a
+bounded, profile-tagged reasoning item transactionally with the assistant
+message. Dropping it and hoping text/tool history is sufficient would ship a
+known continuity defect.
+
+Responses function calls also carry both `call_id` and an output-item id. pi
+stores the pair and notes that the backend associates `fc_...` call items with
+reasoning items
+([function-call replay identity](https://github.com/badlogic/pi-mono/blob/64f83c85d9f36c831e96b29477750f92cd5a2230/packages/ai/src/api/openai-responses-shared.ts#L206-L224)).
+Fugu should keep `call_id` as its canonical Tool id and retain the provider
+item id beside the reasoning item in the same bounded attachment. It should
+not overload Tool ids with a provider-specific composite string.
+
+`previous_response_id` is not an acceptable substitute. It delegates resume,
+`/clear`, crash recovery, and provider availability to hidden server state.
+Fugu should continue sending complete canonical history plus the matching
+opaque reasoning items.
+
+### 3. HTTP SSE is the right first transport
+
+OpenAI now prefers WebSocket and several clients implement it, but OpenAI and
+pi retain fallback and openai-oauth demonstrates a current HTTP-only path.
+Fugu already has a confined HTTP/SSE transport. Implementing WebSocket in v1
+would add handshake, masking, frame, ping/close, timeout, and reconnect state
+without proving it is necessary. The first credentialed gate must verify SSE;
+failure at that gate stops the slice and triggers a separate WebSocket design.
+
+### 4. Account identity comes from the token
+
+The access/ID token carries one namespaced ChatGPT account identifier used in
+`ChatGPT-Account-ID`. Fugu does not need an `owner`, account selector, or
+account-routing field. Local decoding only selects and bounds the header; TLS
+and the backend remain authoritative for token validity.
+
+### 5. Refresh requires writable durable credential state
+
+All mature clients store access and refresh tokens together and write the
+replacement after refresh. Sharing another client's refresh token or
+discarding a rotated token creates races and restart failures. Under Fugu's
+current topology, `fugu-api` has no writable credential store and must not send
+secrets back to the Coordinator. The first Adapter can accept a directly
+configured access token and fail hard at expiry, but automatic login/refresh
+requires a separate custody and persistence design.
+
+### 6. Client identity is an acceptance gate
+
+Independent implementations disagree about `originator`: some send their own
+name, some omit it, and some imitate Codex. There are also reports of unknown
+originators being rejected. Fugu must not claim to be `codex_cli_rs`. The
+live gate will try one deterministic, honest profile (`fugu` or no optional
+originator, as fixed by the accepted ADR). If the backend requires
+first-party impersonation, this design fails closed.
+
+### 7. Clean-room implementation is small but not trivial
+
+The checked projects are Apache-2.0 or MIT licensed, but their runtime and
+agent architectures do not fit Fugu. The useful output is the convergent
+behavioral contract. Fugu should implement its own bounded codec and tests,
+using no copied request builder, stream parser, OAuth store, or WebSocket
+client.
+
+## Implications for Fugu
+
+The implementation design is recorded in
+[`docs/design/codex-subscription.md`](../design/codex-subscription.md). Its
+non-negotiable consequences from this research are:
+
+- a distinct `codex` Provider, never an API-key fallback mode;
+- fixed ChatGPT endpoint and an explicit compatibility snapshot;
+- pure-C Responses request and SSE decoder behind the existing Provider seam;
+- account-id extraction in the credential Custodian, with no owner plumbing;
+- full-history `store:false` requests;
+- bounded opaque reasoning and function-call identity state carried in the
+ Turn transaction and Journal;
+- manual access-token replacement for the first experimental slice;
+- an honest client identity and a live stop/go gate before general use;
+- no Node, Python, SDK, Codex executable, app-server, proxy, or new daemon.
blob - 001035f60bfd1222df5c9ec3cc0a4c23a32e90b1
blob + 4a797f13fee9166dbbba6ce13a27ab6b59a9a652
--- handoff/behavior.md
+++ handoff/behavior.md
- Either mode: `Enter` submits; `Tab`/`Shift-Tab` cycle slash-command
completions when the line starts with `/` (built-ins first, then
skills alphabetically); `PgUp`/`PgDn` and the mouse wheel scroll the
- transcript; `Up`/`Down` recall prompt history (this session); `Ctrl-L` repaints;
+ transcript; `Up`/`Down` recall prompt history (this session); `Ctrl-L`
+ clears the transcript viewport while retaining earlier lines in scrollback
+ and without resetting conversation context, current input, history, or
+ queued messages;
`Ctrl-C` clears a non-empty line, quits on an empty one; `Ctrl-D`
quits on an empty line; `Esc` enters normal mode.
- Normal mode: `h l 0 $ ^ w b e` motions; `i a A I O` (and `o` is
blob - 73e5c50c9a204d60a4dcd85f6e9591e1e66115b9
blob + d99806e29c16f1832f8c21dee5c341e3d117c21b
--- handoff/invariants.md
+++ handoff/invariants.md
untrusted input is touched (a pure narrowing, per I9) so their
contents are kernel-unreachable — the error is ENOENT-shaped, though
names may remain visible in listings — the system executable and
- library paths (read/execute), and
+ library paths (read/execute), `/dev/null` (`rwc`; shell output
+ redirection opens it with `O_CREAT`), and
nothing else. Writes outside the working tree are impossible, not
forbidden; access to a protected path fails like any out-of-tree
path and the tools normalize it to the same clean error. When
blob - 798f9e90dbe3a9a6332e8b66247410dc489e689f
blob + 5cb831a06e63f1c702d3fc803249456ec2d0e736
--- regress/editor/editor_test.c
+++ regress/editor/editor_test.c
CHECK(ed_nitems(&e) == 0); /* non-empty: cleared */
CHECK(ed_feed(&e, K_CTRL_C, 0, NULL, 0) == R_QUIT);
CHECK(ed_feed(&e, K_CTRL_D, 0, NULL, 0) == R_QUIT);
- CHECK(ed_feed(&e, K_CTRL_L, 0, NULL, 0) == R_REPAINT);
+ CHECK(ed_feed(&e, K_CTRL_L, 0, NULL, 0) == R_CLEAR_SCREEN);
ed_free(&e);
/* ---- normal-mode routing keys ---- */
blob - 825cd8859876138e433d10148b60be87682a21c1
blob + e3cb883d0b5c9c2820c1174582e015b39cb195ee
--- regress/tools/tools_test.c
+++ regress/tools/tools_test.c
make_long_line("longline.txt", 1024 * 1024 + 1);
make_big("bigcat.txt", 300 * 1024); /* > 256 KiB */
- /* fugu-tool's confinement: the tree rwxc, system paths rx, the
- * protect dir subtracted, no network */
+ /* fugu-tool's confinement: the tree rwxc, system paths rx,
+ * /dev/null rwc, the protect dir subtracted, no network */
if (unveil(".", "rwxc") == -1)
err(1, "unveil .");
+ if (unveil("/dev/null", "rwc") == -1)
+ err(1, "unveil /dev/null");
if (unveil("/usr", "rx") == -1 || unveil("/bin", "rx") == -1 ||
unveil("/sbin", "rx") == -1 || unveil("/lib", "rx") == -1 ||
unveil("/libexec", "rx") == -1)
run("shell", "{\"command\":\"printf fast-tail-marker\"}");
CHECK(outhas("fast-tail-marker"));
+ /* Shell redirection must use the real device without tripping unveil.
+ * Exercise both write and read; ksh opens the output with O_CREAT, so
+ * this also exercises the otherwise non-obvious "c" permission. */
+ run("shell", "{\"command\":\"printf dev-null-hidden >/dev/null && "
+ "/bin/cat </dev/null && printf dev-null-ok\"}");
+ CHECK(outhas("dev-null-ok") && !outhas("dev-null-hidden"));
+
/* The model command sees a direct-child supervisor as $PPID, not
* fugu-tool. Killing it must make the worker kill the command group
* promptly and remain alive for the following checks. */
blob - c842be5413d99c5bbb441fd8cb9ef666c916c8ec
blob + f7c35cbc03654903dfa84d3caded4baf0ebccbea
--- src/fugu/fugu.1
+++ src/fugu/fugu.1
.It Ic Mouse wheel
Scroll the transcript one line per report without changing prompt history.
.It Ic Ctrl-L
-Repaint the display.
+Clear the transcript viewport while retaining earlier lines in scrollback.
+Conversation context, current input, prompt history, and queued messages are
+unchanged.
.It Ic Ctrl-C
Clear non-empty input; with empty input, quit.
.It Ic Ctrl-D
.Sh SECURITY CONSIDERATIONS
The model and all model-selected file names, commands, tool arguments, web
pages, and provider streams are untrusted.
-The tool executor is confined to the starting project directory and selected
-base-system executable and library paths.
+The tool executor is confined to the starting project directory, selected
+base-system executable and library paths, and read/write/create access to
+.Pa /dev/null
+for shell redirection; shells require create permission because they open
+output redirections with
+.Dv O_CREAT .
It has no network capability unless the Owner explicitly enables network
access for shell subprocesses.
The web worker cannot write files or execute programs, and model credentials
blob - 7302d1885910203803ab6bdd772ce678a258f08b
blob + 5358055180d7225fbaeda4b7382a0c9f86b76757
--- src/fugu-tool/main.c
+++ src/fugu-tool/main.c
/*
* The project tree (read/write/create per the gate), then the
- * system executable and library paths (read/execute only, I5
- * "nothing else"). /etc stays hidden unless subprocess
+ * system executable and library paths (read/execute only) and
+ * /dev/null (read/write/create for ordinary shell redirection;
+ * shells open output redirections with O_CREAT). /etc stays hidden
+ * unless subprocess
* networking is on, when the resolver and TLS-trust files are
* exposed read-only -- the minimal widening I5 permits.
*/
if (unveil(".", write_ok ? "rwxc" : "rx") == -1)
fatal("unveil .");
+ if (unveil("/dev/null", "rwc") == -1)
+ fatal("unveil /dev/null");
if (unveil("/usr", "rx") == -1 || unveil("/bin", "rx") == -1 ||
unveil("/sbin", "rx") == -1 || unveil("/lib", "rx") == -1 ||
unveil("/libexec", "rx") == -1)
blob - 5409a30504055bbd8724f303b025b81677ac02d2
blob + 5b067769c083e34d32f84fed895da3b0e9ba0742
--- src/fugu-tty/editor.c
+++ src/fugu-tty/editor.c
case K_PGDN:
return (R_PAGE_DOWN);
case K_CTRL_L:
- return (R_REPAINT);
+ return (R_CLEAR_SCREEN);
case K_CTRL_C:
if (e->n == 0)
return (R_QUIT);
case K_PGDN:
return (R_PAGE_DOWN);
case K_CTRL_L:
- return (R_REPAINT);
+ return (R_CLEAR_SCREEN);
case K_CTRL_C:
if (e->n == 0)
return (R_QUIT);
blob - e1002aeb280f617e8da7ed1cef8f8a3bc4fcd134
blob + 22dacce44c398472f114e73eda2ebf38ea3dd66b
--- src/fugu-tty/editor.h
+++ src/fugu-tty/editor.h
R_SCROLL_LINE_DOWN,
R_PAGE_UP,
R_PAGE_DOWN,
- R_REPAINT, /* Ctrl-L */
+ R_CLEAR_SCREEN, /* Ctrl-L: clear the displayed transcript */
R_COMPOSE, /* v: run the external editor on ed_text */
R_DRAWER, /* o: open the conversation drawer */
R_QUEUE /* q: focus the pending-message queue */
blob - a01c9a8bd283e2f1a2f45eca0eebf5effc9fb437
blob + 9c20233d383357a221d7316c0cccb7b58dfad897
--- src/fugu-tty/ui.c
+++ src/fugu-tty/ui.c
#include "ui.h"
/* transcript item kinds */
-enum { IT_USER, IT_ASSIST, IT_TOOL, IT_DIFF, IT_NOTE, IT_ERROR };
+enum { IT_USER, IT_ASSIST, IT_TOOL, IT_DIFF, IT_NOTE, IT_ERROR, IT_CLEAR };
struct titem {
int kind;
size_t nlines, lcap;
int dirty;
int scroll; /* top line; -1 follows the tail */
+ int clear_line; /* latest Ctrl-L display boundary */
/* input */
struct editor ed;
int w = wrap_width(u);
lines_reset(u);
+ u->clear_line = 0;
for (i = 0; i < u->nitems; i++) {
struct titem *it = &u->items[i];
const char *t = (const char *)it->raw.data;
render_plain(u, t, n, w,
u->color ? COLOR_PAIR(P_ERR) : A_BOLD, "! ", NULL);
break;
+ case IT_CLEAR:
+ u->clear_line = (int)u->nlines;
+ break;
}
}
u->dirty = 0;
u->nitems = 0;
u->dirty = 1;
u->scroll = -1;
+ u->clear_line = 0;
}
/* ---- drawing -------------------------------------------------------- */
attrset(A_NORMAL);
}
+/* Tail view starts after the latest Ctrl-L boundary until enough new output
+ * fills the viewport. Explicit scrollback may still cross the boundary. */
+static int
+tail_top(const struct ui *u, int height)
+{
+ int top = (int)u->nlines - height;
+
+ if (top < u->clear_line)
+ top = u->clear_line;
+ return (top < 0 ? 0 : top);
+}
+
static void
draw(struct ui *u)
{
rebuild(u);
if (u->scroll == -1)
- top = (int)u->nlines - th;
+ top = tail_top(u, th);
else
top = u->scroll;
if (top < 0)
top = 0;
- if (top > (int)u->nlines - 1 && u->nlines > 0)
- top = (int)u->nlines - 1;
+ if (top > (int)u->nlines)
+ top = (int)u->nlines;
erase();
for (r = 0; r < th; r++) {
if (u->dirty)
rebuild(u); /* refresh nlines before deriving maxtop */
- maxtop = (int)u->nlines - th;
- if (maxtop < 0)
- maxtop = 0;
+ maxtop = tail_top(u, th);
if (u->scroll == -1)
u->scroll = maxtop;
u->scroll += delta;
case R_SCROLL_LINE_DOWN: scroll_by(u, 1); break;
case R_PAGE_UP: scroll_by(u, -(th - 1)); break;
case R_PAGE_DOWN: scroll_by(u, th - 1); break;
- case R_REPAINT: clearok(stdscr, TRUE); break;
+ case R_CLEAR_SCREEN:
+ /* Add a reflow-stable display boundary. Scrollback remains behind
+ * it; coordinator state, input, history, and queue remain intact. */
+ (void)item_new(u, IT_CLEAR);
+ u->scroll = -1;
+ clearok(stdscr, TRUE);
+ break;
case R_COMPOSE:
u->notice[0] = '\0';
compose_begin(u);