Skip to content

kiro: isolate delivered final answers to exact child conversation and serving identity - #403

Closed
luvs01 wants to merge 9 commits into
devfrom
codex/propose-fix-for-kiro-final-answer-vulnerability
Closed

luvs01 wants to merge 9 commits into
devfrom
codex/propose-fix-for-kiro-final-answer-vulnerability

Conversation

@luvs01

@luvs01 luvs01 commented Sep 1, 2026 •

Copy link
Copy Markdown
Owner

Motivation

  • The delivered-final-answer cache was previously bound using a parent-prioritized logging digest before route/auth/account resolution, which could let one conversation suppress another sibling that shared the same parent identifier.
  • The change isolates suppression to the exact child conversation and serving identity so cached final answers cannot cross conversation, principal, or provider boundaries.

Description

  • Defer binding the turn-termination scope until after adapter, provider, model, admission, and serving-account resolution and only bind a composite scope for the Kiro adapter.
  • Build the composite scope from the child-specific session lane (sessionSpecificLaneIdFromRequest: thread ?? session, with the parent included only as a coalescing prefix — a parent-only lane binds no scope), the admission identity, route.providerName, route.modelId, and the resolved serving account, then normalize/hash the composite before using it as the cache key via bindTurnTerminationScope.
  • Resolve the serving credential lazily at check/record time: for key-authenticated routes the element is credentialIdentity — the digest of the resolved wire credential rather than the configured $ENV/keychain: reference — so a rotation behind a stable reference terminates the old identity's scope; an OAuth snapshot account id wins when bound, and the Codex auth context is the last resort.
  • Revalidate the live credential selection (selectionIsCurrent/refreshDispatchAdapter) before evaluating localTerminal, so a record a since-replaced credential made cannot suppress work the send path would have moved onto the live one; a failed refresh skips the terminal entirely and the send path surfaces the credential error.
  • Remove the earlier premature bindTurnTerminationScope usage so no insufficiently scoped record is retained before routing and auth are known.
  • Add end-to-end regressions in tests/server-kiro-completion-e2e.test.ts (sibling isolation, key-pool failover following the serving credential, env-backed key rotation, parent-only lane binds nothing) plus dispatch-level coverage in tests/responses/adapter-dispatch-local-terminal.test.ts for the pre-terminal staleness check and the failed-refresh path.

Testing

  • bun test tests/server-kiro-completion-e2e.test.ts — 19/19 pass including the new regressions.
  • bun test tests/responses/adapter-dispatch-local-terminal.test.ts — 4/4 pass.
  • bun run typecheck, bun run privacy:scan, bun run structure:check — all pass.

Codex Task

Link to Devin session: https://app.devin.ai/sessions/d60838d35393455abb53c5ab4a54ca18
Open in Devin Desktop: https://app.devin.ai/desktop/session/d60838d35393455abb53c5ab4a54ca18?variant=devin
Requested by: @luvs01


Devin Review

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

✅ Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Sep 1, 2026
@github-actions github-actions Bot changed the title kiro: isolate delivered final answers to exact child conversation and serving identity [WRONG BRANCH] kiro: isolate delivered final answers to exact child conversation and serving identity Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026 •

Copy link
Copy Markdown

✅ READY

  • all PR quality gates passed.

Hygiene

✅ Deterministic PR hygiene checks passed.

@github-actions
github-actions Bot marked this pull request as draft September 1, 2026 05:52
@coderabbitai

coderabbitai Bot commented Sep 1, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Repository: luvs01/opencodex/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 0a1ca093-9637-4f1f-9f84-f383024b99ba


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@luvs01 luvs01 changed the title [WRONG BRANCH] kiro: isolate delivered final answers to exact child conversation and serving identity kiro: isolate delivered final answers to exact child conversation and serving identity Sep 3, 2026
@luvs01
luvs01 changed the base branch from main to dev September 3, 2026 06:22
@github-actions
github-actions Bot marked this pull request as ready for review September 3, 2026 06:28
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Encrypted-task recovery rebound the conversation-only termination scope
on the reparsed request after the early binding was removed. The kiro
composite binding in transport overwrote it only by ordering, and a bind
that produces no composite digest would leave the stale scope live for
the adapter's delivered-answer suppression.

Document the cross-request scope contract in structure/: the binding
site, composite material, recording and consumption now live in
providers/kiro.md, with the transport ownership row updated.

Co-Authored-By: Epinephrine <luvs01@hanmail.net>
@devin-ai-integration

Copy link
Copy Markdown

Fixed in c680397 — removed bindTurnTerminationScope(reparsed, resolvedConversationId) from the encrypted-task recovery path in request-prepare.ts and dropped the now-unused import. The Kiro composite binding in request-transport.ts is now the only write of the termination scope, so correctness no longer depends on it overwriting a stale conversation-only bind (which it would skip entirely when no lane/conversation identity exists, since bindTurnTerminationScope early-returns on undefined).

@devin-ai-integration

Copy link
Copy Markdown

Fixed in c680397 — documented the new scope contract in structure/providers/kiro.md ("Delivered final-answer termination scope": binding site, composite digest material, recording at delivery, and consumption in the adapter), updated the request-transport.ts ownership row in structure/transports/responses.md, and refreshed the manifest scope + regenerated INDEX.md. bun run structure:check passes.

…kiro route

The composite termination scope now binds only in transport for the kiro
adapter, so the openai-chat spy could no longer observe a bound scope.
Record the delivered answer through a real kiro turn and replay against
the kiro route, preserving the reparse-rebind and follow-up assertions.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown

Already addressed in c680397 — the recovery reparse no longer calls bindTurnTerminationScope; the transport binding is now the only write of the scope.

@devin-ai-integration

Copy link
Copy Markdown

Already addressed in c680397 — the scope contract is documented in structure/providers/kiro.md (binding site, composite material, recording, consumption), with the request-transport.ts ownership row updated in structure/transports/responses.md. bun run structure:check passes.

…d time

A Kiro 429/401 can rotate the serving credential after the termination
scope is bound, so an eagerly-hashed servingAccount pinned every record
to the credential that failed -- and for key-authenticated routes it was
always null, letting one key's delivered answer suppress a replay that
belonged to a different key. Bind a resolver instead: conversation,
admission, and route stay eager, while the serving credential resolves
when the answer is checked or recorded, tracking route.provider through
every failover site. Key routes now contribute their non-secret
apiKeyAccountLogLabel so distinct keys never share a scope.

Co-Authored-By: Epinephrine <luvs01@hanmail.net>
devin-ai-integration[bot]

This comment was marked as resolved.

…terminal

Two follow-on Devin Review findings on the termination scope:

- A request carrying only x-codex-parent-thread-id bound the shared
  parent lane as its scope, so every sibling under one parent collapsed
  into a single conversation and could suppress each other. The bind now
  takes the child-specific lane only (sessionSpecificLaneIdFromRequest)
  and falls back to the Cursor conversation id; a parent-only request
  binds no scope rather than treating a coalescing group as one child.

- The lazy credential resolver made the check read whatever selection
  the request was bound with, which can go stale between prepare and
  dispatch. prepareAdapterExchange now runs the same selection-binding
  revalidation dispatch uses (selectionIsCurrent/refreshDispatchAdapter)
  before evaluating localTerminal, then rebinds the reasoning replay
  scope, so a record made by a replaced credential cannot suppress work
  the send path would have moved onto the live one.

Regressions: an e2e asserting two requests sharing only a parent header
both reach upstream, and unit coverage of the stale-binding refresh
ordering at the local-terminal boundary.

Co-Authored-By: Epinephrine <luvs01@hanmail.net>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The serving-credential element hashed the configured env:/keychain:
reference, so a rotation behind a stable expression kept the old
identity's scope. credentialIdentity digests the resolved wire
credential instead — the same primitive reasoning-metadata uses to
bind learned refusals to what was actually sent.

A failed dispatch refresh left the route stale but still evaluated
localTerminal, letting a stale-credential record suppress a turn and
return success where the credential error belonged. The terminal is
now skipped after a failed refresh; the send path re-validates and
surfaces the mapped error on its own terms.

Adds an env-rotation e2e regression and a failed-refresh dispatch
test, and keeps structure/providers/kiro.md in sync.

Co-Authored-By: Epinephrine <luvs01@hanmail.net>
@devin-ai-integration

Copy link
Copy Markdown

End-to-end verification of kiro: bind termination identity to the resolved wire credential on 72c4e48.

Environment: Windows, real startServer proxy over HTTP against a scripted Kiro upstream (amazon eventstream, codex_kiro_final_answer toolUseEvents). Manual driver independently re-derives the feature behavior; bun test tests/server-kiro-completion-e2e.test.ts tests/responses/adapter-dispatch-local-terminal.test.ts → 23 pass, 0 fail.

Scenario Result
First turn delivers phase:"final_answer" 1 upstream send, real answer returned
Replay same conversation (client dropped phase) output:[], upstream stays at 1 — suppressed
Replay again still suppressed — record survives use
Sibling session_id, identical body sent upstream — lane isolation holds
Trailing NEW user message after answer sent upstream — only a trailing answer suppresses
Rotate env-backed key (apiKey:"$VAR" a→b), replay sent upstream — rotated credential = new scope
x-codex-parent-thread-id only (no session) both siblings sent — parent-only binds no scope
Streamed replay response.completed only, no deltas, no empty_completion_retry_failed
Env var deleted before replay 400 kiro token missing — real error, not fake empty success

Request log rows for suppressed turns: localTerminalReason:"kiro_final_answer_already_delivered", usageStatus:"reported", zero usage (not estimated).

Logs: suppressed replays show 0 tokens and Upstream sends 0
Detail: suppressed replay — Upstream sends 0, Completed
Dashboard loads against live proxy

GUI golden path: GET / serves the built dashboard (Online, provider count, token totals); Logs lists all rows — suppressed replays show 0 tokens / "Upstream sends: 0" while the delivered turn shows real usage.

Recorded end-to-end GUI verification

Tested via Devin session: https://app.devin.ai/sessions/d60838d35393455abb53c5ab4a54ca18

@luvs01

luvs01 commented Sep 24, 2026

Copy link
Copy Markdown
Owner Author

Closing: current dev already isolates delivered final answers — src/responses/turn-termination.ts has �indTurnTerminationScope keyed by a WeakMap on the parsed request, bound from
equest-prepare.ts with the resolved conversation identity, which is the exact-child-conversation isolation this PR introduced.

@luvs01 luvs01 closed this Sep 24, 2026
@devin-ai-integration

Copy link
Copy Markdown

Devin is currently unreachable - the session may have died.

View session

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

Labels

aardvark bug Something isn't working codex

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant