Skip to content

hyp session states that the control responder is never authenticated (#451) - #520

Merged
philcunliffe merged 4 commits into
masterfrom
fix/issue-451
Jul 31, 2026
Merged

hyp session states that the control responder is never authenticated (#451)#520
philcunliffe merged 4 commits into
masterfrom
fix/issue-451

Conversation

@philcunliffe

@philcunliffe philcunliffe commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

The defect

hyp session status | ignore | unignore resolves a control endpoint and trusts whatever answers there. validateControlResponse (PR #439) refuses a reply that is not an object, whose ignored is not a boolean, or whose session_id is not echoed byte-for-byte. A listener that simply echoes the token back satisfies all three by construction, so a local impostor still yields exit 0, ignored: true and the privacy control tells the user "you are opted out" while nothing recorded the decision.

The decision this implements

Maintainer direction on #451: option 3, accept and document. The trigger condition already implies local code execution as this user, a gateway-written secret is readable by anyone who can bind the port, and peer-process identity has no portable form (/proc/net/tcp + /proc/<pid>/fd, lsof, GetExtendedTcpTable). So the guarantee is stated rather than proved, in the CLI's own output.

This PR therefore does not make the impostor go away, and its test says so out loud.

What changed

hypaware-core/plugins-workspace/ai-gateway/src/session_command.js:

  • Human output gains a trust: note beside every confirmed answer, on status and on both mutation verbs, naming the endpoint that was trusted:
    trust: nothing proves the responder at http://127.0.0.1:8787 is the HypAware gateway - any process on this machine could bind that port and answer. This answer is only as trustworthy as this machine.
  • --json gains endpoint_authenticated: false on status (including unknown reports) and on ignore / unignore. It is false by contract, not by outcome, so a consumer never has to infer authenticity from silence; a future peer-identity check would need a distinct field rather than flipping this one.
  • The disclosure is unconditional, and rides the daemon_status path too, which previously said nothing at all: a live daemon's status.json is evidence about a past bind, not about who answers now. A note printed only "when spoofed" would be a claim the verb cannot make.
  • endpoint_source and every existing fail-closed refusal are untouched. Nothing new is believed.

Docs

Reconcile note: #460 is in flight on fix/issue-460 and may mint an LLP number of its own; if both land on 0164 this needs the usual renumber (cf. LLP 0156). The hyp session JSON is the other likely overlap point.

Test evidence

New: test/plugins/ai-gateway-session-responder-trust.test.js. It stands up an impostor responder (reads the session id off the query string or body, echoes it back with ignored: true) and pins both halves of the accepted outcome: that the impostor is believed, including on the daemon_status path with a live pid file and a status.json naming the impostor's port, and that every such answer discloses the missing authentication.

Before the fix (impostor accepted with zero disclosure - the reproduction):

not ok 1 - an impostor that echoes the token is believed - and every answer discloses that it was never authenticated
  error: 'the JSON must state that the responder was not authenticated'
  + actual - expected
  + undefined
  - false
not ok 2 - `hyp session ignore` carries the disclosure too, where a spoofed success reads as done
not ok 3 - the disclosure is unconditional: a real gateway answer carries it too
not ok 4 - an UNKNOWN answer reports the field too, so a reader never has to infer it from absence
# tests 4  # pass 0  # fail 4

Note test 1 got past its exit 0 / ignored: true / endpoint_source: daemon_status assertions before failing: the impostor was believed, silently.

After:

ok 1 - an impostor that echoes the token is believed - and every answer discloses that it was never authenticated
ok 2 - `hyp session ignore` carries the disclosure too, where a spoofed success reads as done
ok 3 - the disclosure is unconditional: a real gateway answer carries it too
ok 4 - an UNKNOWN answer reports the field too, so a reader never has to infer it from absence
# tests 4  # pass 4  # fail 0

Full suite: # tests 3091 # pass 3082 # fail 8. The 8 failures are the pre-existing leave ... tests, identical on a clean origin/master baseline (# tests 3087 # pass 3078 # fail 8). The @ref validation test passes with the new annotations. npm run build:types is clean.

Fixes #451

test and others added 2 commits July 31, 2026 04:13
…451)

`validateControlResponse` (PR #439) proves the responder echoed our token;
it cannot prove the responder IS the gateway. A local process that binds the
resolved port and echoes the token back still yields `exit 0, ignored: true`,
so the privacy control tells a user "you are opted out" when nothing recorded
the decision.

Per the maintainer's direction on #451, this is accept-and-document: the
attack presupposes local code execution as this user, a gateway-written secret
is readable by whoever can bind the port, and peer-process identity has no
portable form (three platform implementations plus a native dependency). So
the guarantee is stated rather than proved.

- human output gains a `trust:` note beside every confirmed answer, on
  `status` and on both mutation verbs, naming the endpoint that was trusted
- `--json` gains `endpoint_authenticated: false`, constant by contract rather
  than by outcome, so a consumer never infers authenticity from silence
- the disclosure is unconditional and rides the `daemon_status` path too,
  which said nothing at all before: a live daemon's status.json is evidence
  about a past bind, not about who answers now
- `endpoint_source` and every existing fail-closed refusal are unchanged

LLP 0164 records the decision and carries the companion #460 contract
statement (membership is not a match) so the control-plane guarantees read as
one story; LLP 0067 §cli-response-check gets the forward-ref.

test/plugins/ai-gateway-session-responder-trust.test.js stands up an impostor
that echoes the token, pins that its answer is still believed (the accepted
residual), and asserts the disclosure on every surface.

Fixes #451

Co-Authored-By: Claude <noreply@anthropic.com>
Three open branches independently minted 0164 off the same master
high-water mark, none able to see the others:

  fix/issue-421 (PR #502)  status-names-recent-clients-from-gateway-entrypoints
  fix/issue-473 (PR #517)  codex-flat-pair-needs-a-namespace-signal -> 0165
  fix/issue-451 (PR #520)  session-control-plane-states-its-guarantees -> 0166

PR #502 is held and approved awaiting a human merge, so it keeps 0164.
This branch takes 0166; 0165 went to PR #517.

No content change: the file is renamed and the number updated at every
reference site (3 in session_command.js, 1 in the new responder-trust
test, the Extended-by forward-ref in LLP 0067, and the doc title). The
#stated-not-proved and #membership-not-grain anchors still resolve.
@philcunliffe

Copy link
Copy Markdown
Contributor Author

🤖 neutral: renumbered LLP 0164 -> 0166 (cross-branch collision)

You flagged this risk in the PR body, and it was real - though wider than expected. Three open branches independently minted 0164 this cycle, each off the same master high-water mark and none able to see the others:

Branch PR Doc Resolution
fix/issue-421 #502 status-names-recent-clients-from-gateway-entrypoints keeps 0164
fix/issue-473 #517 codex-flat-pair-needs-a-namespace-signal -> 0165
fix/issue-451 #520 session-control-plane-states-its-guarantees -> 0166 (this PR)

#502 is held, approved, and awaiting your merge, so it has the prior claim on 0164.

Pushed as 8d9f03b. No content change - the file is renamed and the number updated at every reference site: 3 @refs in session_command.js, 1 in the new ai-gateway-session-responder-trust.test.js, the Extended-by forward-ref in LLP 0067 §cli-response-check, and the doc title. Both {#stated-not-proved} and {#membership-not-grain} anchors still resolve.

npm test after the rename: 3082 pass / 8 fail - the 8 being the pre-existing test/core/leave-command.test.js baseline (tracked in #512), matching this branch's own pre-rename count.

Note your §membership-not-grain contract for #460 rides along in this doc as written; the #460 fix worker is still running and, if it mints its own LLP, will be reconciled against 0164-0166 the same way.

@philcunliffe

Copy link
Copy Markdown
Contributor Author

🤖 neutral: PR #520 and PR #522 both write the #460 contract, in different documents

These two PRs were built in parallel this tick by workers blind to each other. Each is coherent alone; the overlap only bites if both merge. Flagging rather than resolving, because where a normative statement lives is your call.

PR #520 (fix/issue-451) mints LLP 0166 §membership-not-grain, which states the #460 contract in full prose - ignored: true means set membership, the caller owns resolving the key, option 1 declined because the route treats the token as opaque - and explicitly defers the code: "realized in the change set for #460 rather than here." It was written that way because you asked in the #451 thread that the two contract statements be written together.

PR #522 (fix/issue-460) states the same contract as LLP 0066 §receipt-is-membership + R14, and implements it.

So after both merges the rule is stated normatively in two places. That is the drift LLP discipline exists to prevent, and a future reader has no signal which is authoritative.

Options:

  • (a) Keep LLP 0066 R14 as the normative home (it is the spec, and it is where the requirement is numbered and implemented); trim LLP 0166 §membership-not-grain to a one-line citation of R14 instead of a restatement.
  • (b) Keep LLP 0166 as the joint decision record you asked for; drop R14 and have LLP 0066 §receipt-is-membership cite LLP 0166.
  • (c) Accept both as-is if you read 0166 as narrative framing and 0066 R14 as the binding requirement - in which case say so, and 0166 should say so too.

Also note: both PRs edit llp/0067-session-opt-out.design.md - #520 adds an Extended-by forward-ref at §cli-response-check, #522 adds §cli-receipt. Different hunks, so git should merge them cleanly, but whichever lands second will need a merge-base pass.

Neither PR is blocked on this; both remain individually reviewable. Reply here (or on the sibling PR) and neutral will apply your choice on a later tick.

Three review fixes on top of #451's disclosure work, no behaviour change:

- `types.d.ts`: `SessionStatusReport` bills itself as "what `hyp session
  status` reports, in `--json` field order", and a maintainer adding a report
  shape reads it. It said nothing about `endpoint_authenticated`, so the
  constant-by-contract rule lived only inside `writeStatus`. State the
  envelope, the by-contract choice, and the "a real check needs a new field"
  consequence there, with the LLP 0166 ref.
- LLP 0166 named `RESPONDER_TRUST_NOTE`, a symbol that does not exist; the
  code has `responderTrustNote(endpoint)`, and the reason it is a function
  (it names the endpoint) is the point.
- Drop the unused `@import { IncomingMessage, ServerResponse }` copied into
  the new test from its sibling suite.

Co-Authored-By: Claude <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
Contributor Author

Verdict: approve, with three low-severity fixes applied on the branch (97f4620)

The change matches the maintainer's direction on #451 (option 3, accept and
document
) as written, and it does the harder half of accept-and-document
correctly: it makes the residual legible without pretending to have closed it.
Real authentication (peer-process identity, non-squattable transport) was
deliberately not chosen and is not raised here.

The central question: does the output state its trust bound honestly?

Yes. I checked for both failure modes and found neither.

No overclaim. The note is a plain negative: nothing proves the responder is
the gateway, any process on this machine could bind that port, the answer is
only as trustworthy as this machine. It names no mitigation it does not have,
and it names the address it trusted, which on the daemon_status path is the
only line about the endpoint at all (session_command.js:69-71).

No implied guarantee. The disclosure's completeness is what makes it honest,
and I walked every confirmed-answer path rather than trusting the summary:

surface path disclosure
status human, ignored session_command.js:371-383 trust: via provenanceNotes
status human, not_ignored session_command.js:384-396 trust: via provenanceNotes
status human, unknown session_command.js:365-370 none - correctly, nothing was confirmed
status --json (all three) session_command.js:351-364 endpoint_authenticated: false
ignore/unignore human session_command.js:329-337 trust: via provenanceNotes
ignore/unignore --json session_command.js:299-316 endpoint_authenticated: false

The if (endpoint) guard at session_command.js:476 is not a hidden condition:
provenanceNotes is only ever reached from the three confirmed branches, and
report.endpoint is non-null on every one of them. The unconditional contract
holds in fact, not just in prose.

The false-by-contract choice is right and its consequence is stated: a future
peer-identity check needs a new field rather than flipping this one, so a
consumer that learned "false means nobody checked" is never silently retaught
"false means the check ran and failed".

No new belief - confirmed

endpoint_source and every fail-closed refusal are genuinely untouched. The only
deletions in session_command.js are three JSDoc lines, one JSON.stringify
line reflowed to add a field, and one destructuring line widened by one name.
validateControlResponse, controlRequest, resolveGatewayEndpointForCli and
resolveSessionIdForCli are byte-identical. Nothing that was refused before is
believed now.

Test quality - reproduced, and the impostor is real

Reverted session_command.js alone (git checkout 3c45ce5~1 -- <src>), left the
new test in place: 0/4. Restored: 4/4. The claimed fail-then-pass is real,
and it is the source that carries it.

The impostor is a genuine impostor, not a string assertion. withImpostorServer
stands up an independent http.createServer that parses the session id off the
query string or the JSON body and echoes it back with ignored: true - it never
touches createControlHandler. It defeats validateControlResponse by
construction, which is exactly the issue's claim. daemonHome() then writes a
live pid file and a status.json naming the impostor's port, so test 1 runs on
the strongest endpoint-evidence path (endpoint_source: 'daemon_status'),
and the test asserts the impostor is believed before asserting the
disclosure. Pinning the accepted residual, not just the new text, is the right
shape for an accept-and-document change: the day someone "fixes" it, this test
tells them they changed a contract.

Test 3 using the real createControlHandler to prove the disclosure fires on a
genuine answer is what makes "unconditional" testable rather than asserted.

Full suite

# tests 3091 # pass 3082 # fail 8 - all 8 are the leave ... tests in
test/core/leave-command.test.js, the known origin/master baseline (#512), and
nothing else. tsc --noEmit and npm run build:types are clean;
test/core/llp-ref-hygiene.test.js is 9/9.

Renumber 0164 -> 0166 - verified complete

  • Zero residual 0164 references anywhere in the tree.
  • Both code-referenced anchors resolve: {#stated-not-proved} (line 71) and
    {#membership-not-grain} (line 110), enforced by the ref-hygiene test.
  • The Extended-by: forward-ref in llp/0067-session-opt-out.design.md:296 sits
    inside §cli-response-check, the section it amends, and points at the right
    file and anchor.
  • 0166 is collision-free and above the highest number claimed anywhere, per
    LLP 0156 §renumber: origin/fix/issue-421 holds 0164, origin/fix/issue-473
    holds 0165, and no other branch claims 0166.

Findings

[Low] llp/0166-session-control-plane-states-its-guarantees.decision.md:141 - the LLP named a symbol that does not exist. (fixed)
"Scope of this change set" credited RESPONDER_TRUST_NOTE; the code has
responderTrustNote(endpoint). Not cosmetic for an LLP whose job is to be the
durable record: a reader greps for the constant and finds nothing. Fixed, and the
fix carries why it is a function rather than a constant (it names the endpoint,
which on the daemon_status path is the only line about the endpoint at all).

[Low] hypaware-core/plugins-workspace/ai-gateway/src/types.d.ts:206 - the constant-by-contract rule was not documented where a maintainer will meet it. (fixed)
This was the one real gap. The rule was stated twice - a comment inside
writeStatus, and LLP 0166 - but SessionStatusReport bills itself as "What
hyp session status reports, in --json field order"
, and that docblock is what
someone adding a fifth report shape reads. It said nothing about
endpoint_authenticated, so a new shape would ship without the constant and
nothing would catch it. Fixed by stating the envelope (this record plus
folder_policy and endpoint_authenticated), why the field is constant by
contract, the "a real check needs a NEW field" consequence, and the obligation on
anything that adds a shape, with an @ref LLP 0166#stated-not-proved [constrained-by].

[Nit] test/plugins/ai-gateway-session-responder-trust.test.js:18-20 - unused @import. (fixed)
IncomingMessage / ServerResponse were copied in from
ai-gateway-session-status.test.js (where they are used) and referenced nowhere
here. Removed.

[Informational, no action] the skill shell path still answers with no disclosure.
claude/skills/hypaware-ignore/SKILL.md and its siblings curl the control
route directly and print Ignored session <id>. Total ignored: N with neither
the echo check nor any trust note - the same responder, the same gap, a louder
surface. This is correctly out of scope: the maintainer's direction named
hyp session output, LLP 0166 §membership-not-grain assigns the shell-path echo
check to #460's change set, and #435 removes the independent resolver entirely.
Noted only so it is not mistaken for covered. Worth confirming the trust note
rides along when #435 folds these onto the CLI.

[Known overlap, already surfaced to the maintainer - not resolved here, sibling branch untouched].
LLP 0166 §membership-not-grain states #460's contract in prose while PR #522
(fix/issue-460) states the same rule as LLP 0066 §receipt-is-membership + R14
and implements it; both PRs also edit llp/0067-session-opt-out.design.md in
different hunks. The maintainer will choose which document owns it.


What I changed (97f4620, pushed to fix/issue-451)

review: document endpoint_authenticated where the report shape lives - the
three fixes above. No behaviour change: no runtime code was touched, only a
.d.ts docblock, an LLP paragraph, and a dead JSDoc import. Re-verified after
the commit: # tests 3091 # pass 3082 # fail 8 (the same 8 baseline leave
failures, none new), tsc --noEmit clean, npm run build:types clean,
ref-hygiene 9/9.

The new `responderTrustNote` docblock links `[LLP 0067 §cli-response-check]`
at the file with no fragment, so a reader who follows the label lands at the
top of a 600-line design doc. Every other fragment-bearing LLP link in the
tree carries its anchor (`sync.js`, `verb_codec.js`, `remote_commands.js`,
`first_sync_hold.js`); this one was the outlier.

Co-Authored-By: Claude <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
Contributor Author

Verdict: approve. Round 2 of 2, one nit found and fixed (f27e093)

Reviewed fresh at 97f4620, not by trusting round 1's summary. The change
matches the maintainer's direction on #451 (option 3, accept and document)
and does the hard half of accept-and-document right: it makes the residual
legible without pretending to have closed it. Real authentication was
deliberately not chosen and is not raised here.

Round-1 fixes: all three verified in the committed tree

Checked against the remote head, not the comment.

round-1 item verification state
LLP 0166 credited RESPONDER_TRUST_NOTE, which does not exist grep RESPONDER_TRUST_NOTE llp/0166-*.md -> 0 hits; llp/0166-...decision.md:141 now names responderTrustNote(endpoint) and says why it is a function (it names the endpoint) landed
types.d.ts documented nothing about endpoint_authenticated hypaware-core/plugins-workspace/ai-gateway/src/types.d.ts:205-228 now states the envelope, the constant-by-contract rationale, the "a real check needs a NEW field" consequence, and the obligation on anything adding a report shape, with @ref LLP 0166#stated-not-proved [constrained-by] landed
unused @import in the new test grep IncomingMessage test/plugins/ai-gateway-session-responder-trust.test.js -> 0 hits landed

The central question, re-derived: does the output state its trust bound honestly?

Yes. I enumerated every stdout/stderr write in session_command.js rather
than re-walking round 1's table, and the coverage is exhaustive:

surface site disclosure
ignore/unignore --json session_command.js:300-316 endpoint_authenticated: false
ignore/unignore human session_command.js:318-338 trust: via provenanceNotes
status --json, all three statuses session_command.js:352-364 endpoint_authenticated: false
status human, unknown session_command.js:367-370 none, correctly: nothing was confirmed
status human, ignored session_command.js:372-383 trust: via provenanceNotes
status human, not_ignored session_command.js:385-396 trust: via provenanceNotes
refusals (:178, :269, :277, :283, :293) stderr, nonzero exit n/a, no answer to qualify

The if (endpoint) guard at session_command.js:473 cannot suppress a
confirmed answer, and I checked this from the type rather than the call sites.

SessionStatusReport['endpoint'] is string | null (types.d.ts:243), so the
guard looks like a hidden condition. It is not: endpoint: null is written at
exactly two sites (session_command.js:212, :225), both of which also set
status: 'unknown' and are reached only when resolveSessionIdForCli or
resolveGatewayEndpointForCli failed. Every branch that reaches provenanceNotes
has status of ignored or not_ignored, and on those the field is
endpoint.endpoint off the ok: true arm of SessionEndpointResolution, a
non-nullable string. Unconditional in fact, not just in prose.

endpoint_authenticated: false is constant by contract, not by outcome, by
construction.
Both writers emit a bare false literal (:313, :361); it is
never derived from a check, a variable, or a response field. On the status
side it rides { ...report, endpoint_authenticated: false, ... }, and since
SessionStatusReport deliberately does not carry the key, the spread cannot
override it, so it holds on unknown reports too. The consequence is stated in
all three places a maintainer might land (types.d.ts:213-224, the comment at
:355-360, LLP 0166 §stated-not-proved): a peer-identity check needs a new
field, so a consumer that learned "false means nobody checked" is never silently
retaught "false means the check ran and failed".

No overclaim. The note is a plain negative that names no mitigation it lacks,
and names the address it trusted, which on the daemon_status path is the only
line about the endpoint at all.

No new belief

validateControlResponse, controlRequest, resolveGatewayEndpointForCli and
resolveSessionIdForCli are untouched. endpoint_source and every fail-closed
refusal survive intact. The diff adds one function, one field at two sites, one
endpoint key threaded into provenanceNotes, and one guarded push.

Test quality

test/plugins/ai-gateway-session-responder-trust.test.js stands up a real
impostor (withImpostorServer, :143-170): an independent http.createServer
that parses the session id off the query string or JSON body and echoes it back
with ignored: true, never touching createControlHandler. It defeats
validateControlResponse by construction, which is exactly the issue's claim.
daemonHome (:204-223) writes a live pid file and a status.json naming the
impostor's port, so test 1 runs on the strongest evidence path
(endpoint_source: 'daemon_status') and asserts the impostor is believed
before asserting the disclosure. Pinning the accepted residual is the right shape
for accept-and-document: the day someone "fixes" it, this test says they changed
a contract. Test 3 proves unconditionality against the genuine
createControlHandler; test 4 covers unknown. 4/4.

Numbering, merge, and gates


Findings

[Nit] hypaware-core/plugins-workspace/ai-gateway/src/session_command.js:51 - a section link that does not go to the section. (fixed in f27e093)
The new responderTrustNote docblock links
[LLP 0067 §cli-response-check](../../../../llp/0067-session-opt-out.design.md)
with the label naming a section and the URL carrying no fragment, so a reader who
follows it lands at the top of a 600-line design doc instead of the dead-end
analysis the sentence is citing. Every other fragment-bearing LLP link in the
tree carries its anchor (src/core/commands/sync.js:225,
src/core/cli/verb_codec.js:11, src/core/cli/remote_commands.js:178,
src/core/usage-policy/first_sync_hold.js:145); this one was the outlier, and it
is a link this PR introduced. Added #cli-response-check.

[Considered, no action] hyp session status --help does not restate the trust bound.
hypaware-core/plugins-workspace/ai-gateway/src/index.js:84-92 documents the exit
codes and the folder-governor caveat and says nothing about authenticity. I
looked at whether the maintainer's "state plainly in hyp session output"
reaches the help text and concluded it does not: the two pre-existing caveats of
the same kind (EPHEMERAL_NOTE and the endpoint_source note) also live only
beside the answer, never in help. The house pattern is caveats-at-the-answer, and
adding one of the three to help would be the inconsistent move. Recording the
reasoning so a later reader does not re-open it as an oversight.

[Informational, still out of scope - confirmed, not newly in scope]
hypaware-core/plugins-workspace/claude/skills/hypaware-{ignore,unignore,privacy}/SKILL.md
and the codex sibling still curl the control route and print a confirmed answer
with neither the echo check nor any trust note. This PR touches no skill file, and
the assignment has not moved: LLP 0166 §membership-not-grain puts the shell-path
echo check in #460's change set, PR #522 is open and carries it, and #435
removes the independent resolver entirely. Correctly out of scope. Worth
confirming the trust note rides along when #435 folds these onto the CLI.

[Known overlap, already surfaced - not resolved here, sibling untouched].
LLP 0166 §membership-not-grain states #460's contract in prose while PR #522
(fix/issue-460, open, mergeable) states the same rule as LLP 0066
§receipt-is-membership + R14 and implements it. Both PRs also edit
llp/0067-session-opt-out.design.md in different hunks (§cli-response-check
here, §cli-receipt there). Per the standing cross-ref comment this is the
maintainer's call; nothing was changed on either side.


What I changed

f27e093 - review: point the LLP 0067 link at the section its label names.
One character-range edit to a JSDoc markdown link. No behaviour change, no
runtime code path touched.

Re-verified after the commit and confirmed on the pushed remote head: npm test
3091 / 3082 pass / 8 fail (the same 8 baseline leave failures, none new),
npm run typecheck clean, npm run build:types clean, ref-hygiene 9/9, still
merges cleanly with origin/master.

@philcunliffe

Copy link
Copy Markdown
Contributor Author

neutral triage (LLP 0017): can this ship safely? Yes.

Review rounds exhausted (2/2) with three residuals left open at head f27e093.
Judged each against true blocker (production defect: wrong behaviour, data
loss, security hole, crash, perf regression) vs preference (style, doc
completeness, scope note):

  1. hyp session status --help doesn't restate the trust: bound -
    preference. Consistent with the house pattern: the two pre-existing
    caveats of the same shape (EPHEMERAL_NOTE, the endpoint_source
    weak-evidence note) also live only beside the answer, never in --help.
    A --help-only reader has no confirmed answer to be misled by; every actual
    run (human or --json) carries the caveat inline. Not a behavioural defect.

  2. The skill shell paths (claude/skills/hypaware-{ignore,unignore,privacy},
    codex/skills/hypaware-privacy) still curl the control route and print a
    confirmed answer with no trust note
    - preference / tracked, not a
    blocker for this PR. hyp session states that the control responder is never authenticated (#451) #520 touches no skill file, and the maintainer's
    direction on hyp session cannot prove the control responder is the real gateway #451 named hyp session output specifically; LLP 0166
    §membership-not-grain assigned the skill-path work to hyp session ignore's POST receipt cannot prove the id matches live traffic - it's Set membership, not drop verification #460's change set.
    PR hyp session ignore's receipt reports the write it made, not a drop nothing verified #522 has since added the echo check to all four callers, but the
    responder-trust note is still missing there after both hyp session states that the control responder is never authenticated (#451) #520 and hyp session ignore's receipt reports the write it made, not a drop nothing verified #522 land -
    a real, currently-unassigned gap, just not one hyp session states that the control responder is never authenticated (#451) #520 created or is scoped to
    close.

  3. The hyp session states that the control responder is never authenticated (#451) #520/hyp session ignore's receipt reports the write it made, not a drop nothing verified #522 documentation overlap (LLP 0166 §membership-not-grain
    vs LLP 0066 R14) - already surfaced to you directly on both PR threads
    (see the neutral-cross-ref comment above); not re-filed.

All residuals are non-blocking, so #520 can merge safely once you dispose of
it. Findings 1 and 2 are captured with evidence in a follow-up:
#533.

@philcunliffe
philcunliffe marked this pull request as ready for review July 31, 2026 06:41
@philcunliffe philcunliffe added the neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030) label Jul 31, 2026
@philcunliffe
philcunliffe merged commit c9b2e5e into master Jul 31, 2026
9 checks passed
@philcunliffe
philcunliffe deleted the fix/issue-451 branch July 31, 2026 18:01
philcunliffe pushed a commit that referenced this pull request Jul 31, 2026
…the removed one

The merge is textually one conflict (both sides added a constant after
EPHEMERAL_NOTE in session_command.js) and one semantic one. Master's
ai-gateway-session-responder-trust.test.js, landed with #520, pinned the
headline this branch deletes - `ignored - the gateway will drop this session`
- as the precondition for "a spoofed success reads as done". That string is
exactly what LLP 0066 R14 forbids, so the assertion moves to the receipt this
branch prints instead. The test's own claim is untouched: an impostor's
confirmed `ignored` still has to carry the trust disclosure, and the drop set
it names is still the impostor's.

Co-Authored-By: Claude <noreply@anthropic.com>
philcunliffe added a commit that referenced this pull request Jul 31, 2026
…thing verified (#522)

* the ignore receipt reports the write it made, not a drop nothing verified

`hyp session ignore` printed "ignored - the gateway will drop this session".
The gateway cannot know that: `control.js` adds an opaque token to a `Set` and
answers `ignored: true` for whatever it was handed, and the drop happens later
in the client adapter, keyed on the `session_id` that adapter stamps on the row
(LLP 0066 R5). Nothing compares the two, so an id no exchange will ever carry -
a Codex thread id, a finished session's id, a typo - printed the same line while
the session kept being recorded.

Per the maintainer direction on #460 (options 3 + 4), this is accept-and-document
rather than a new gateway capability: making the route answer the grain question
would put client-grain knowledge in a deliberately provider-agnostic control
surface (LLP 0050). So the contract becomes the stated one and the receipt stops
implying more.

- LLP 0066 gains §receipt-is-membership and R14: `ignored: true` is set
  membership, the caller owns resolving the container first (R13), a read-back
  does not upgrade the claim, and any surface calling the route directly applies
  the CLI's response validation. LLP 0067 gains §cli-receipt.
- The CLI receipt says "this id is in the gateway drop set" and prints what that
  proves, next to the success where a caller reading "ignored" as done sees it.
  `--json` carries `guarantee: "set_membership"` beside `status: "ok"`, since a
  bare `ok` is what the skills parse. The reader prints the same qualifier, from
  one shared constant, for the reason EPHEMERAL_NOTE is shared.
- The three skills that POST the route directly gained the echo check the JS
  resolver already had (`session_id` compared byte-exactly against the id sent,
  `total` a real number), and each states the narrow contract in prose.

Regression test constructs the exact false-confidence case rather than asserting
a string: an id live traffic does not carry is registered successfully, the real
Codex projector is then shown recording that traffic anyway, and the receipt
printed over that outcome must not have promised a drop.

Co-Authored-By: Claude <noreply@anthropic.com>

* review: the unignore receipt and the skills' numeric check are held to R14 too

Two findings from the #460 review, both the reviewed change's own rule applied
where it was not yet applied.

The removal receipt printed "recording resumed", which is the same inference
from the same `Set` answer read the other way: a token no exchange carried was
suppressing nothing to resume, and `.hypignore` is an independent governor (R7)
that can keep a session unrecorded whatever this verb removed. It now reports
the removal it made.

The three skill verifiers gate `total` with `isinstance(..., int)` while
claiming to mirror `validateControlResponse`. `isinstance(True, int)` is True in
Python, so a responder answering `total: true` passed the skill check and would
have failed the CLI's `typeof total !== 'number'` - a gap in the very check
LLP 0066 R14 makes a MUST for surfaces calling the route directly. bool is now
excluded.

Both are pinned by tests that fail without the fix, and LLP 0067 §cli-receipt
records the removal half of the contract.

Co-Authored-By: Claude <noreply@anthropic.com>

* review: the unignore skill is held to R14 too, and reads the reply it was believing

The CLI's removal receipt was corrected in round 1, but the skill that DELETEs
the same route was left printing "Recording re-enabled for session <id>" off
`--fail-with-body` alone, with the response body sent to /dev/null. That is the
same mirrored overclaim, plus R14's last bullet (any surface calling the route
directly applies the CLI's response validation) unapplied entirely rather than
merely weakly: any 200 from whatever owns that port read as success.

It now reads the reply and asserts `ignored` is a real boolean false, `total` a
real number with bool excluded, and `session_id` echoed byte-exactly, then
reports the removal in the CLI's words. Pinned by the shared skill loop plus a
removal-specific test; LLP 0067 §cli-receipt records it.

Co-Authored-By: Claude <noreply@anthropic.com>

* merge master: the #451 trust test asserts this branch's receipt, not the removed one

The merge is textually one conflict (both sides added a constant after
EPHEMERAL_NOTE in session_command.js) and one semantic one. Master's
ai-gateway-session-responder-trust.test.js, landed with #520, pinned the
headline this branch deletes - `ignored - the gateway will drop this session`
- as the precondition for "a spoofed success reads as done". That string is
exactly what LLP 0066 R14 forbids, so the assertion moves to the receipt this
branch prints instead. The test's own claim is untouched: an impostor's
confirmed `ignored` still has to carry the trust disclosure, and the drop set
it names is still the impostor's.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: test <test@test.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: test <test@example.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hyp session cannot prove the control responder is the real gateway

1 participant