Skip to content

[V1] 按 originating prompt 关联流式事件与 terminal 状态 #8

Open

Description

@tbrandenburg

Goal

Verify and, only if reproduced and provably fixable, correct potential OpenCode V1 streaming cross-prompt attribution on a session used by more than one writer. An A2A task must never show another prompt's assistant output, progress, intervention, or terminal state. Keep normal single-writer V1 behavior intact.

Status: Suspected correctness risk from code review; not a confirmed live vulnerability. This issue concerns V1 1.18.19, explicitly selected with OPENCODE_API_VERSION=v1 (V2 is now the API default). The fork's V2 exclusive profile is separate; unrestricted V2 remains #9.

Background

  • execution/stream_runtime.py handles V1 ExecutionUpdate events primarily using update.session_id == session_id; part events carry message_id but are not proved to originate from the submitted prompt.
  • execution/stream_events.py maps V1 session.idle to a completed terminal signal for that session.
  • execution/coordinator.py uses V1's synchronous submit_prompt() -> send_message() result, but streaming also waits for an event terminal. The final response can be authoritative for the prompt while earlier stream chunks are still incorrectly attributed.
  • The in-process session lock only serializes this gateway's work, not external OpenCode clients.
  • Existing pinned V1 model-backed smoke passed; it did not test an external second writer on the same session.

Relevant code/tests: src/opencode_a2a/execution/{coordinator.py,stream_runtime.py,stream_events.py,session_manager.py}, src/opencode_a2a/opencode_upstream_client.py, tests/execution/test_streaming_output_contract_*.py, tests/execution/test_session_ownership.py, scripts/live_opencode_smoke.sh.

KISS principles

  1. Reproduce before fixing. Do not assume that session-wide idle proves a bug or that assistant message_id proves prompt ancestry.
  2. Keep the existing A2A runtime, typed upstream contract, persistence, event renderer and authentication. Make any fix V1-scoped and minimal.
  3. Prefer a verified prompt/response ownership link over timing, arrival order, last-message heuristics, or session.idle alone. Do not drop legitimate multi-step/tool updates by naively requiring one assistant message ID.
  4. If attribution cannot be established, reject / fail uncertain for the affected case; never leak another writer's content or falsely report success. No blind resubmission.
  5. Do not turn this into V2 support, a new replay database, a generic event router, or a rewrite. Preserve existing V1 single-writer streams, tool orchestration, interrupts and cancellation.

Pitfalls to check before implementation

  • Sync response vs streaming: V1 HTTP message completion is not equivalent to ownership of all SSE parts already emitted.
  • Assistant IDs vs originating input: distinguish user input ID, assistant message ID, part ID, and session ID; inspect actual pinned V1 payloads and code. A tool turn may produce multiple assistant parts/messages.
  • Race ordering: a foreign session.idle, failure or part can arrive before/after the owner's synchronous response. Don't prematurely complete or permanently hang.
  • Streaming exposure: removing a foreign chunk from the final artifact is too late if it was streamed live. Bounded buffering may be needed until ownership can be proven.
  • Identity/security: use two controlled, authorized test clients; this is not proof of a cross-tenant exploit. Retain principal/session ownership and callback authorization.
  • Observability: sanitize or redact event traces, credentials, workspace paths, prompt/model text and generated content; use distinct harmless sentinel markers only in assertions.

Detailed implementation plan (junior developer)

  1. Baseline and map the call chain. On current fork main, record SHA and confirm V1 opt-in. Trace A2A message:stream through executor.py -> coordinator.py -> stream_runtime.py -> stream_events.py and native GET /event. Run the existing V1 streaming, cancellation and ownership tests first.
  2. Build one deterministic reproducer, no production changes yet. Use a disposable, authenticated OpenCode V1 1.18.19 server, private workspace and a real model. Start an A2A streaming prompt A containing marker A; while A runs, submit a separate direct OpenCode prompt B to the same native session, containing marker B. Synchronize on observable admission/parts rather than fixed sleeps. Save only a sanitized event/ID/ordering trace. Repeat with A/B start order reversed and with B finishing first.
  3. Decide GO / NO-GO based on evidence. Compare A's A2A streamed artifacts, persisted terminal, returned native response, and actual origin IDs. Document whether another writer's output, error or idle affected task A. If no failure reproduces, add the reproducer as regression coverage and document the unproven residual risk; do not invent a fix. If it reproduces, prove whether V1 exposes enough causal identity to filter parts and terminal states.
  4. Implement the smallest safe V1 fix, only if justified. Keep change local to the V1 event normalization / execution coordinator / stream runtime. Use verified IDs or completion evidence, ensure all pre-proof chunks are safe before emission, and bound any buffer. Prevent foreign text, reasoning, tools, progress, permissions and error/terminal signals from entering the task. If the V1 API cannot prove attribution, explicitly fail uncertain or disallow that sharing pattern; do not silently assume safety.
  5. Add targeted automated regressions. Use synthetic interleaved A/B native events for: foreign parts before/after owned parts, early foreign idle/error, distinct tool-step message IDs, interrupts, late events after completion, unknown/missing identity, and stream disconnect. Test no foreign chunks reach the output event queue, no false terminal, and no extra prompt submission. Preserve normal V1 single-writer, unary, follow-up, cancellation and V2 exclusive behavior.
  6. Run repository gates. bash ./scripts/doctor.sh and bash ./scripts/conformance.sh. If shared coordinator or renderer code changed, run the pinned V2 smoke/acceptance checks too; don't assume V1-focused unit tests prove V2 unchanged.
  7. Final live E2E acceptance (mandatory for a fix). Against a fresh real V1 1.18.19 instance with model, Basic auth and two clients: (a) normal single-writer streaming and unary complete; (b) simultaneous A2A task A and direct upstream writer B in the same session keep A/B content isolated; (c) reverse arrival and completion order; (d) same-client follow-up and tool/permission interaction remain intact; (e) cancellation of A does not falsely complete it from B's idle or inject B's output; (f) an unauthorized A2A principal still cannot claim the session. Check intermediate SSE frames, final artifacts and durable task status—not only final text. Rerun multiple times with controlled synchronization; record counts and sanitized evidence, never credentials/raw responses.

Acceptance criteria

  • The original risk is either reproduced with a sanitized trace or explicitly classified as not reproduced after the specified tests; no speculative fix.
  • For every supported concurrent-writer scenario, task A receives only proven A-owned output and terminates on its own proven outcome; otherwise it deterministically fails/rejects without leaking B's output.
  • Foreign idle/error, intervention, tool progress, cancellation and late events cannot corrupt task A's output/state.
  • V1 single-writer behavior, request-scoped ownership and existing A2A contracts remain compatible; V2 exclusive behavior is unchanged.
  • Automated regression tests, doctor, conformance and the applicable real V1 E2E pass; evidence distinguishes a reproduced bug from an unverified risk.

Contribution / PR workflow (when a code fix is justified)

  1. Complete the reproduction and GO/NO-GO investigation first. If no change is justified, record the evidence here; do not create empty PRs.
  2. For an actual fix, create a focused PR against Intelligent-Internet/opencode-a2a upstream first, then create a corresponding PR against this fork. Link both PRs in this issue, with the supported scope, limitations and exact-head validation.
  3. Validate the fork PR and merge it into the fork only after applicable CI and real V1 E2E acceptance pass. Do not wait for upstream review or merge; upstream acceptance and fork delivery are independent.
  4. Preserve existing contribution branches; no force-push/history rewrite to make PRs look tidy. Do not change the V2 profile as part of this fix.

Definition of Done (DoD)

  • Small reviewed PR with test-first reproduction and justified minimal fix or evidence-based no-change finding.
  • Exact head SHA, reproducible two-client instructions, sanitized observations, pinned V1 runtime/model and actual test outcomes linked here.
  • Support statement documents safe sharing boundary and any remaining unsupported pattern; no inflated security claim.
  • Merge only after applicable CI and E2E pass. If input-to-output correlation is impossible with V1's contract, leave the feature explicitly unsupported / the issue blocked rather than claiming full completion.

Activity

  1. tbrandenburg commented on Oct 25, 2026

    @tbrandenburg
    OwnerAuthor

    验证范围补充

    新的 V1 1.18.19 expanded live matrix 已通过 streaming、unary、follow-up、真实 question、permission allow/reject、auth/ownership 与6次取消;取消后额外46秒验证持久化状态。这些检查没有向同一 upstream session 交错第二个外部客户端输入,因此既不能证明也不能否定本 issue 的 stream attribution 风险。

    本轮 restricted V2 profile 通过 private exclusive deployment 明确排除这种外部 writer;没有把该策略冒充 V1 shared-session 修复。本 issue 保持 open / 待独立真实并发复现。

  2. tbrandenburg commented on Oct 25, 2026

    @tbrandenburg
    OwnerAuthor

    Fork release 关联;本风险保持 open

    Fork #7 已 normal merge 到 19943955704bf05347619ca5b98addb94a683c97;experimental private-exclusive V2 prerelease 已发布。

    V1 仍默认。新 doctor 1021 passed、conformance 8/8;recorded real V1 1.18.19 evidence 的生产 source digest 与 release 匹配。但这些通过 没有验证或修复同 V1 session 外部 writer 的归属问题,也没有把未真实复现的风险提升为已证实漏洞。本 issue 保持 open,继续按原双客户端 live reproduction/最小 attribution fix 范围处理。

    Compatibility limitations 和 release notes 明确保留此限制;这不是 fork merge 的隐藏承诺。五个 upstream contribution PR 尚未合并,完整跨仓库链接见 #1。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions