Skip to content

docs(adr): openab-pty composable runtime (3 deployment profiles) - #1478

Open
chaodu-obk[bot] wants to merge 9 commits into
mainfrom
adr/openab-pty-sidecar
Open

docs(adr): openab-pty composable runtime (3 deployment profiles)#1478
chaodu-obk[bot] wants to merge 9 commits into
mainfrom
adr/openab-pty-sidecar

Conversation

@chaodu-obk

@chaodu-obk chaodu-obk Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces the in-process "PTY Mode" proposal (#1477) with openab-pty: an independently runnable, composable runtime — one codebase, two composable runtimes, three deployment modes (ACP only / PTY only / ACP+PTY colocated). The broker binary is untouched in every profile; openab-pty owns its own session manager, listener, token model, and failure domain. Not deployed by default; highest-reversibility path.

Review Contract

Goal

Record the design decision for delivering remote sandboxed terminal sessions as a separate openab-pty runtime (standalone or colocated sidecar), resolving the five blocker clusters from the #1477 review: positioning vs the Thin Bridge pillar, same-pod blast radius, auth below shell-equivalent capability, scrollback secrets contradiction, and ACP-pool lifecycle incompatibility.

Non-goals

  • No code in this PR — ADR only; implementation follows in phased PRs if the ADR is accepted.
  • No changes to the OAB broker binary, the ACP session pool, or any shipped behavior.
  • No identity layer for PTY tokens in MVP (explicitly stated per identity-trust-none.md); no A2A messaging (future ADR); no pod-lifetime session durability; no notification capability in the PTY-only profile (colocated profile only, relayed via the broker).

Accepted Residual Risks

  • A second binary/image adds build and release surface; mitigated by the existing multi-binary workspace and default-off deployment.
  • Until a shared lifecycle crate is justified, capacity/teardown logic is partially duplicated between the ACP pool and the PTY manager; accepted deliberately to avoid refactoring the shipped ACP path before demand is proven.
  • The Phase 4 notification heuristic (prompt-like burst then silence) is best-effort and may false-positive; it is one-way, feature-gated, and colocated-profile-only.
  • The standalone profile positions openab-pty as a small product in OpenDray's category; scope is bounded by an explicit statement that it never grows platform adapters, orchestration, or memory features.

Acceptance Criteria

  • The ADR resolves each of the five docs(adr): PTY Mode — remote sandboxed terminal sessions #1477 blocker clusters with a concrete mechanism (container/process boundary, per-session tokens + WSS + fail-closed, scrollback off-by-default, own session manager, default-off reversible deployment).
  • The three deployment profiles are specified with their config, image, Service/Ingress, and Helm UX implications; the broker container is byte-identical across profiles.
  • Config design keeps one operator source of truth while guaranteeing platform tokens are never mounted into the PTY runtime.
  • Phase 1 scope includes the lifecycle contract (liveness, TTLs incl. absolute cap, single-attach, cursor reconnect with gap signaling, teardown order) and MVP audit logging.
  • Prior-art adopt-in targets are consistent with the Section 5 phase plan.

Follow-ups

  • ACP observability extension ADR (Alternative B track, complementary).
  • Future A2A ADR if in-pane agent coordination demand appears.
  • Demand-gated: shared lifecycle crate extraction; single-process merge evaluation; identity layer for PTY tokens.

Architecture

flowchart LR
    subgraph clients_acp["ACP clients (unchanged)"]
        discord["Discord / Slack / Telegram"]
    end
    subgraph clients_pty["Terminal clients (new)"]
        web["Web terminal (xterm.js)"]
        mobile["Mobile / desktop terminal"]
    end
    subgraph pod["K8s Pod — profile 3 (colocated); profile 2 deploys the green half alone"]
        subgraph broker["Container: openab (broker) — byte-identical in all profiles"]
            adapters["Platform adapters"]
            pool["ACP session pool"]
        end
        subgraph sidecar["Container: openab-pty (ghcr.io/openabdev/openab-pty)"]
            ws["GET /pty/{session}<br/>WSS + per-session tokens"]
            mgr["PTY session manager (own)"]
            cli["CLI subprocess<br/>(operator-configured command)"]
        end
        vol[("Shared workspace volume")]
    end
    discord --> adapters --> pool
    web --> ws
    mobile --> ws
    ws --> mgr --> cli
    pool -.-> vol
    cli -.-> vol
    mgr -. "Phase 4 (colocated only):<br/>one-way notification webhook" .-> adapters

    style sidecar fill:#1a7f37,color:#fff
    style ws fill:#116329,color:#fff
    style mgr fill:#116329,color:#fff
    style cli fill:#116329,color:#fff
Loading

Key points:

  • Three deployment profiles: ACP only (openab), PTY only (openab-pty standalone with workspace PVC + [pty] config + PTY auth secret only), ACP+PTY (colocated, shared workspace volume). Helm: openab.enabled / pty.enabled or --set profile=acp|pty|full.
  • Coexistence without cohabitation: separate PID/cgroup/filesystem/listeners/tokens/failure domains; broker platform tokens never mounted into the PTY runtime; own Service/Ingress (/pty/*).
  • One config source, two views: single config.toml via the configUrl flow; broker ignores [pty]; openab-pty reads only [pty] plus shared basics.
  • Auth designed for shell-equivalent trust: WSS mandatory, fail-closed off-loopback, browser bearer-subprotocol reused from the as-built /acp implementation, short-lived per-session tokens from a signing secret via the secrets resolver.
  • Lifecycle designed for byte streams: detached-idle TTL plus absolute lifetime cap, single-attach exclusive MVP, monotonic cursor reconnect with gap signaling, Phase 1 audit logging.
  • Bounded standalone positioning: PTY-only competes in OpenDray's category (differentiator: K8s sandbox + short-lived tokens) and never grows adapters/orchestration/memory — that boundary protects the broker's thin-bridge identity.

Notes

  • Supersedes docs(adr): PTY Mode — remote sandboxed terminal sessions #1477 (closed); the group-review record there is the rationale for rejecting the in-process form.
  • At a Glance: mermaid above. Prior Art & Industry Research: ADR Section 6 (OpenDray, Herdr, Claude Code cross-session messaging; ttyd/gotty and kubectl-exec baselines addressed in Section 4).

Context: Discord thread discussion on 2026-08-15.

@chaodu-obk chaodu-obk Bot changed the title docs(adr): openab-pty optional sidecar runtime docs(adr): openab-pty composable runtime (3 deployment profiles) Aug 15, 2026
@chaodu-obk

This comment has been minimized.

@chaodu-obk

This comment has been minimized.

@chaodu-obk

This comment has been minimized.

@chaodu-obk

This comment has been minimized.

@chaodu-obk

This comment has been minimized.

@chaodu-obk

This comment has been minimized.

- F1: resolution asymmetry -- broker resolves [secrets.refs]; PTY runtime
  fails closed on any [secrets.refs] table or unresolved cloud reference;
  config example split into logical source vs delivered projection
- F2: Phase 4 bridge redesigned as broker-pull over pod-local loopback --
  no bridge secret ever enters the PTY container; events are display-only
  rate-limited hints; push+HMAC allowed only with an external signer or
  the runtime/child privilege boundary (non-MVP)
- F3: hard kill domain MUST -- per-session cgroup.kill or pidfd descendant
  reaper; pgid is the first signal path, not the containment guarantee
- F4: same-UID residual risks converted to a MUST checklist (read-only
  mounts, constant-time verify, token zeroize, 128-bit admin entropy
  floor, Linux-only PR_SET_DUMPABLE assumption, accepted SIGKILL risk)
- F5: filesystem layout specified (/run/openab-pty runtime-only,
  /etc/openab-pty read-only projection, workspace as sole writable mount)
- F6: session renew defined (admin-authenticated, process survives,
  generation bump, one-time token) and added to the Phase 1 CLI
- F7: stale pre-keyless terms swept (PTY auth secret -> admin bootstrap
  credential; Phase 4 signing-key mention removed)
@chaodu-obk

chaodu-obk Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor Author

Important

CHANGES REQUESTED ⚠️ -- The round-3 F1-F7 fix set is verified resolved at the mechanism level by ten independent lanes, but reconciling the complete round-3 record surfaces one unaddressed critical carry (the admin-credential delivery channels that the whole MVP containment model rests on), three multi-round carries (Origin policy accuracy, client-side token storage, admin-plane rate limiting), and a bounded list of specification gaps exposed by the fixes themselves.

What This PR Does

Records the ADR for openab-pty: remote sandboxed terminal sessions delivered as a separate, independently runnable runtime -- one codebase, two composable runtimes, three deployment profiles (ACP only / PTY only / colocated). Supersedes the rejected in-process form (#1477). ADR only; no code.

How It Works (round-4 delta)

Commit 7b54b30 addresses the round-3 F1-F7 set:

  • Resolution asymmetry (fail closed): the PTY runtime MUST NOT link a cloud resolver; a delivered projection still containing [secrets.refs] or an unresolved cloud reference is a startup error. The config example is split into a logical operator source and a delivered projection.
  • Pull-model bridge: Phase 4 changes from webhook push + HMAC to broker pull over a loopback-only stream; no bridge secret enters the PTY container; bridge events are display-only, rate-limited hints that never mutate broker state.
  • Kill domain (MUST): pgid declared as first signal path only; a per-session cgroup.kill or a pidfd-based descendant reaper is required; slot release and the absolute TTL enforce against the hard boundary.
  • Filesystem layout (MUST) plus a six-item same-UID residual-risk checklist (read-only mounts, constant-time verification, attach-token zeroization, 128-bit entropy floor, Linux-only PR_SET_DUMPABLE caveat, stated SIGKILL availability risk).
  • session renew <name>: admin-authenticated re-issue; the process survives, the generation bumps (outstanding tokens die), a fresh token is returned exactly once -- distinct from restart-in-place.
  • Terminology sweep: "admin bootstrap credential" throughout; stale signing-key wording removed.

Round-3 Reconciliation

The round-3 record contains two parallel finding sets published under the same round key (an audit-trail irregularity this round reconciles):

Round-3 set Status at 7b54b30
F1-F7 set (resolution asymmetry, bridge HMAC, kill domain, same-UID checklist, filesystem layout, renewal, terminology) ALL RESOLVED -- verified against mechanisms, not wording; no regressions
Critical set F1-F3 (admin-credential delivery channels + admin-plane throttling; threat-model wording with load-bearing dumpable; attach/takeover fencing) F1 NOT addressed -> this round's F1 (🔴); F2 partially absorbed by the checklist -> residual F9; F3 partially covered by the existing CAS text -> residual F8

Findings

# Severity Finding Location
F1 🔴 Admin-credential delivery channels are not closed -- the load-bearing MVP containment premise. The ADR forbids the plaintext credential in the child's environment and filesystem view, but never forbids delivery via argv (/proc/<pid>/cmdline is world-readable), inherited environ, an stdin race, or temporary files. A same-UID shell observing the credential once during a create/renew window gains permanent mint/kill authority. Also state the rationale for the 128-bit entropy floor versus the previously recommended 256-bit, or raise it openab-pty-runtime.md:112-120,181
F2 🟡 Admin control-plane rate limiting absent (3rd-round carry): per-IP limits cover only WS-upgrade failures; session create/renew/kill over loopback/UDS and the Phase 2 remote admin endpoints have no attempt throttle, request/body bounds, concurrency limits, or audit requirement openab-pty-runtime.md:190,259-263
F3 🟡 Origin-policy claim inaccurate for the 4th consecutive round: "origin policy ... carry over" -- the as-built /acp consults Origin only on the keyless loopback path; the keyed bearer path (the only mode PTY has) never reaches it. Decide explicitly: bearer-only trust boundary, or a deliberate Origin allowlist divergence openab-pty-runtime.md:179; crates/openab-gateway/src/adapters/acp_server.rs:219-225,703-712
F4 🟡 Client-side token storage / XSS contract absent (carry): Phase 2 ships an xterm.js web client receiving a shell-equivalent bearer token, but the ADR never states storage posture (memory-only vs localStorage), URL/referrer/log hygiene, or CSP guidance openab-pty-runtime.md:179,272-274
F5 🟡 Filesystem-layout contract is internally impossible and overclaimed: /run/openab-pty/ must be writable at startup while the workspace is declared the sole writable mount under readOnlyRootFilesystem: true (needs a dedicated tmpfs/emptyDir); and "never in the child's filesystem view" / "not reachable" is a path convention, not a kernel boundary -- a same-UID child in the same mount namespace can open those paths by absolute path openab-pty-runtime.md:115-119,189
F6 🟡 Kill domain is not deployable as specified: cgroup.kill requires cgroup v2 subtree delegation that the stated container contract (non-root UID 1000, all capabilities dropped) does not provide; a pidfd reaper does not track descendants automatically (needs PR_SET_CHILD_SUBREAPER plus race-safe discovery); no MVP default is named, no startup probe (fail closed if neither mechanism is operational), and the Phase 1 implementation checklist still lists only setpgid + escalating kill -- the exact path by which the round-3 finding regresses at coding time openab-pty-runtime.md:189,202,259-261
F7 🟡 ${secrets.*} interpolation is not explicitly rejected by the startup guard: the enforcement condition lists [secrets.refs] tables and aws-sm:// URIs, but ${secrets.pty_admin_hash} shares ${} delimiters with the accepted ${VAR} env form -- an implementer could treat it as an unset env var and let a cloud reference leak through openab-pty-runtime.md:128,156
F8 🟡 Attach/takeover fencing residuals (prior critical, partially resolved by the CAS text): a stolen still-valid token can repeatedly win the owner_conn_generation CAS and starve the legitimate client; successful preempts are neither audited as anomalies nor rate-limited; and the total ordering of generation bump, stored-hash deletion, CAS, and replay under one session lock is undefined openab-pty-runtime.md:183,198
F9 🟡 Threat-model precision (prior critical, partially absorbed): "runtime state contains no minting authority to steal" remains overstated -- the runtime is still the mint and freshly minted tokens transit process memory; PR_SET_DUMPABLE=0 should be a load-bearing MUST with its dependency stated (blocks same-UID /proc/<pid>/fd, /proc/<pid>/mem, ptrace); the admin-credential buffer should be zeroized after verification (only attach-token zeroization is stated) openab-pty-runtime.md:120,124
F10 🟡 Renew-while-attached fate undefined: renew bumps the generation and invalidates tokens, but the ADR does not state whether an actively attached WebSocket is disconnected (evict order) or write-revoked -- two implementations with different session semantics openab-pty-runtime.md:183
F11 🟡 Restart-in-place has no Phase 1 command surface: the recovery taxonomy offers it for reattach-to-dead (a Phase 1 scenario), but Phase 1 bootstrap documents only session create and session renew. Add session restart <name> or mark restart-in-place Phase 2+ openab-pty-runtime.md:203,259
F12 🟡 Output-path buffering is unbounded: scrollback_kib bounds retained history and input backpressure is specified, but the replay-to-live handoff queue and the PTY-output-to-slow-client backlog have no stated bound or overflow behavior (gap/disconnect) openab-pty-runtime.md:199,261-264
F13 🟡 Phase 4 pull-stream resource contract unspecified: no stream timeout/heartbeat, reconnect backoff, concurrent-stream limit, event queue cap, coalescing/dedupe, or drop policy -- "rate-limited hints" constrains semantic delivery, not retained stream/event resources openab-pty-runtime.md:283-284
F14 🟡 Admin-credential rotation procedure unstated: renew rotates attach tokens, not the admin credential; state that rotation = redeploy with a new hash (runtime restart clears sessions -- acceptable, but say it) openab-pty-runtime.md:181
F15 🟡 argon2id$... example conflicts with the 128-bit CSPRNG credential contract: memory-hard hashing implies an operator-chosen low-entropy secret; for a generated high-entropy credential a fast hash suffices -- clarify the choice or change the example openab-pty-runtime.md:125,173
F16 🟡 Docs clarity: the delivered-projection example binds 0.0.0.0:8090 with tls_terminated_upstream = false and no certificate note (the fail-closed guard would refuse this bind; the example misleads); the bridge diagram arrow ---- notification bridge (pull) ---> reads as broker-push data flow openab-pty-runtime.md:65,166-167
F17 🟡 Operability gaps: rollback is only "toggle drains then kills" (no termination-grace/rollout ordering, projection-update behavior for live sessions, PVC residue, or re-enable semantics); and the deploy tooling that materializes the two config views is unassigned ("operator/Helm/CI") with no verification mechanism -- the security boundary currently rests on human discipline. Make both Phase 2 acceptance criteria Configuration; Phase 2
G1-G6 🟢 See What's Good --
Finding Details

🔴 F1: Close the admin-credential delivery channels

With UID separation demoted to optional hardening, the entire MVP control-plane security rests on "the plaintext admin credential is never observable by the same-UID shell child". The ADR forbids only the child's environment and filesystem view. Requested change: state as MUST that the plaintext exists only on the operator's trusted side; in-container verification accepts the credential only via short-lived non-echoing stdin or a UDS message body (never argv-style flags, which are world-readable via /proc/<pid>/cmdline); no temporary files; audit logs never contain the plaintext. State the 128-vs-256-bit rationale or raise the floor.

🟡 F2-F4: The three long-running carries

Each has a one-paragraph fix: (F2) add a failure throttle and bounded-work policy on every admin credential verifier, not just WS upgrades; (F3) replace "origin policy ... carry over" with the accurate statement that Origin gating is a keyless-mode-only /acp mechanism inapplicable to the always-keyed PTY transport, or specify a deliberate Origin allowlist; (F4) state that the web client holds attach tokens in memory only (never localStorage/URLs/history), or explicitly defer with a Phase 2 acceptance criterion.

🟡 F5-F6: Make the two new MUST sections implementable

(F5) add a dedicated writable tmpfs/emptyDir for /run/openab-pty/, correct "only writable mount", and reword reachability as organization/accident prevention -- confidentiality rests on the hash-only design, not filesystem invisibility. (F6) name pidfd + PR_SET_CHILD_SUBREAPER with race-safe descendant discovery as the MVP default (no extra capabilities needed); gate the cgroup path on an explicit delegation prerequisite; require a startup probe that fails closed when neither mechanism is operational; add the kill-domain bullet to the Phase 1 checklist.

🟡 F7-F17

Bounded spec repairs; see the table. F8 and F9 are the residuals of the two partially-resolved prior criticals: F8 needs preempt audit/rate-limit plus a lock-ordered revoke/attach/replay sequence; F9 needs the precise threat-model wording, load-bearing dumpable, and admin-credential zeroization.

Baseline Check
  • PR opened 2026-08-15; head 7b54b30633648d6117cfa7f5b8a23c2aa96b11c8 (8 commits); base main @ 94354a75 (merge-base equals base tip; branch is current)
  • Diff: one file, docs/adr/openab-pty-runtime.md (+344); no code changes
  • Round history: rounds 1-2 reviewed b562c38; round 3 reviewed 28ed874; this round reviews the single fix commit 28ed874..7b54b30. Note: three round-3 summaries were published under one round key (two CHANGES REQUESTED with different finding sets, one LGTM) -- this round reconciles both finding sets and restores a single canonical record
  • Eleven lanes reported (architecture x2, performance, correctness, readability, safety, docs/UX, security/CI, operability, simplicity, general); verdicts: 9 CHANGES REQUESTED, 2 LGTM-labeled with important findings (counted as CHANGES REQUESTED per verdict rules)
  • Fix verification performed against the local checkout at the exact head SHA; the critical-carry claims (F1, F8, F9) were independently re-verified against the ADR text before severity assignment
  • CI at head: check, validate, and poll-and-review all pass

Addressing External Reviewer Feedback

No external reviewer (bot or human) has commented on this PR; the requested reviewer has not yet reviewed. This section will address any such feedback in future rounds.

What's Good (🟢)
  • G1: The pull-model bridge is the strongest change this round -- it removes the in-container HMAC authority by design instead of guarding it, exactly consistent with the keyless token model's intent, and honestly downgrades forged events to display-only hints
  • G2: The fail-closed resolution-asymmetry startup guard is a real control, not prose -- it structurally prevents re-importing a cloud fetch identity into the PTY trust boundary
  • G3: The two-view config split (logical source vs delivered projection) makes the trust boundary concrete, scannable, and guard-testable
  • G4: The same-UID residual-risk checklist names the real boundary honestly: constant-time verification, zeroization, entropy floor, Linux-scoped dumpable, and SIGKILL correctly classified as availability rather than confidentiality
  • G5: Renew vs restart-in-place is a crisp distinction with correct generation fencing -- theft, expiry, and dead-process recovery each have a defined path
  • G6: All seven round-3 F1-F7 fixes survived mechanism-level re-verification by ten independent lanes with zero regressions -- this is substantive design work, not wording repair

5. Three Reasons We Might Not Need This PR

  1. Four ADR-only rounds is a diminishing-returns signal -- most remaining findings are implementation-phase acceptance criteria; a Phase 1 spike proving pidfd kill and the fail-closed config guard would validate the design faster than more prose.
  2. The standalone profile is a product commitment -- a second image, release surface, and CVE response in an existing tool category, for demand still evidenced by one discussion thread; the 12-month review point bounds but does not remove the carrying cost.
  3. The security guarantees lean on tooling that does not exist yet -- the two-view projection's "never delivered" invariant is enforced by unbuilt deploy tooling with an unassigned owner; until that exists, the boundary is operational discipline, not mechanism.

Disposition

Verdict: CHANGES REQUESTED -- the direction is confirmed and the round-3 F1-F7 repairs all held up under mechanism-level scrutiny. F1 is the single blocker: close the plaintext delivery channels for the credential the whole MVP model depends on. F2-F4 are multi-round carries that deserve priority; F5-F17 are bounded spec repairs, most of them one-sentence to one-paragraph fixes. A revised head addressing F1 and the carries has a clear path to LGTM.

- F1 (critical): close all admin-credential delivery channels -- plaintext
  only on the operator side; in-container presentation via non-echoing
  stdin or UDS body only; never argv (/proc/cmdline), env, temp files, or
  logs; entropy floor raised to 256 bits matching attach tokens
- F2: admin control plane throttled (failure backoff, bounded bodies,
  verification concurrency cap, audited failures) -- not just WS upgrades
- F3: Origin policy decided explicitly: bearer-only trust boundary;
  the /acp Origin gate is keyless-loopback-only and does not carry over
- F4: web client token storage contract: memory-only, never
  localStorage/URLs/cookies; CSP; Phase 2 acceptance criterion
- F5: /run/openab-pty backed by dedicated tmpfs; 'only writable mount'
  corrected; path separation reworded as convention, not kernel boundary
- F6: kill domain deployable: pidfd reaper + PR_SET_CHILD_SUBREAPER as
  MVP default; cgroup.kill gated on subtree delegation; fail-closed
  startup probe; added to the Phase 1 checklist
- F7: startup guard explicitly rejects ${secrets.*} interpolation
- F8: takeover preempts audited + rate-limited; revoke/attach/replay
  total order under one session lock
- F9: threat model reworded (runtime is the mint; at-rest vs transient);
  PR_SET_DUMPABLE=0 load-bearing MUST with stated /proc+ptrace dependency;
  admin credential buffer zeroized after verification
- F10: renew-while-attached evicts the active connection (defined)
- F11: session restart <name> added to the Phase 1 CLI
- F12: output path bounded (handoff queue, slow-client backlog; gap or
  fail-closed disconnect)
- F13: Phase 4 pull-stream resource contract (single stream, heartbeat,
  capped backoff, bounded coalescing queue, drop-oldest)
- F14: admin-credential rotation = redeploy new hash + restart, stated
- F15: sha256 verifier example with 256-bit-CSPRNG rationale (argon2
  targets human-chosen secrets, forbidden here)
- F16: delivered-projection example satisfies the fail-closed TLS guard;
  bridge diagram arrow reads as broker-initiated pull
- F17: rollback contract detailed; Helm owns projection generation with
  a CI guard test (poisoned projection must be rejected)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant