hyp session states that the control responder is never authenticated (#451) - #520
Conversation
…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.
🤖 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
#502 is held, approved, and awaiting your merge, so it has the prior claim on 0164. Pushed as
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. |
🤖 neutral: PR #520 and PR #522 both write the #460 contract, in different documentsThese 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 ( PR #522 ( 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:
Also note: both PRs edit 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>
Verdict: approve, with three low-severity fixes applied on the branch (
|
| 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
0164references 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 inllp/0067-session-opt-out.design.md:296sits
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-421holds 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>
Verdict: approve. Round 2 of 2, one nit found and fixed (
|
| 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
- 0166 is collision-free.
llp/0164is now onmaster(from hyp status names recent clients from gateway-tracked entrypoints (#421 item 5) #502, merged),
origin/fix/issue-473holds 0165, and no branch other than this one carries
anyllp/0166-*(swept every remote ref). Zero residual0164references
anywhere in this tree.origin/fix/issue-460mints no LLP of its own,
consistent with the cross-ref note. - Merges cleanly with the moved master (Codex workspace-cwd refusal is an ancestor test, not a byte test (#481, finding 2 of 2) #491, hyp status names recent clients from gateway-tracked entrypoints (#421 item 5) #502, purge's near-miss note names all three causes, not two (#497) #505):
git merge --no-commit --no-ff origin/mastersucceeds with zero conflicted
paths, at both97f4620and the new head. Reset, not committed. npm test: 3091 tests, 3082 pass, 8 fail - all 8 areleave ...in
test/core/leave-command.test.js(test/core/leave-command.test.js: 8 of 11 tests fail on master (exit 1 from hyp leave), red since the tests were introduced #512, fix pending in hyp leave: the daemon status query must answer, not throw #527). Expected set,
nothing else.npm run typecheckandnpm run build:typesexit 0.
test/core/llp-ref-hygiene.test.js9/9; both code-referenced anchors
({#stated-not-proved},{#membership-not-grain}) resolve, as do 0166's
outbound links to0067#cli-response-check,0067#cli-session-idand
0066#enforcement.- House rules: zero U+2014 on any added line across all five files (the
em dashes remaining intypes.d.tsandllp/0067are pre-existing on
master, counts unchanged at 5 and 20). No statement semicolons, no
@typedef, no inlineimport('...')types added,@importat file top,
repo-root-anchored.jsspecifiers.
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.
neutral triage (LLP 0017): can this ship safely? Yes.Review rounds exhausted (2/2) with three residuals left open at head
All residuals are non-blocking, so #520 can merge safely once you dispose of |
…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>
…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>
The defect
hyp session status | ignore | unignoreresolves a control endpoint and trusts whatever answers there.validateControlResponse(PR #439) refuses a reply that is not an object, whoseignoredis not a boolean, or whosesession_idis not echoed byte-for-byte. A listener that simply echoes the token back satisfies all three by construction, so a local impostor still yieldsexit 0, ignored: trueand 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:trust:note beside every confirmed answer, onstatusand 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.--jsongainsendpoint_authenticated: falseonstatus(includingunknownreports) and onignore/unignore. It isfalseby 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.daemon_statuspath too, which previously said nothing at all: a live daemon'sstatus.jsonis 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_sourceand every existing fail-closed refusal are untouched. Nothing new is believed.Docs
llp/0164-session-control-plane-states-its-guarantees.decision.md(Accepted). Per the maintainer's note that the two contracts "should be written together so the control-plane guarantees read as one story", it carries both statements: §stated-not-proved (this change, hyp session cannot prove the control responder is the real gateway #451) and §membership-not-grain (hyp session ignore's POST receipt cannot prove the id matches live traffic - it's Set membership, not drop verification #460 -ignored: truemeans "this token is in the drop set", the caller resolves the key). The hyp session ignore's POST receipt cannot prove the id matches live traffic - it's Set membership, not drop verification #460 half is explicitly recorded as landing in its own change set; no hyp session ignore's POST receipt cannot prove the id matches live traffic - it's Set membership, not drop verification #460 code is touched here.llp/0067-session-opt-out.design.md§cli-response-check gets anExtended-by:forward-ref (the doc is Active, so the decision lands as a new document per the house immutability rule).Reconcile note: #460 is in flight on
fix/issue-460and may mint an LLP number of its own; if both land on 0164 this needs the usual renumber (cf. LLP 0156). Thehyp sessionJSON 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 withignored: true) and pins both halves of the accepted outcome: that the impostor is believed, including on thedaemon_statuspath with a live pid file and astatus.jsonnaming the impostor's port, and that every such answer discloses the missing authentication.Before the fix (impostor accepted with zero disclosure - the reproduction):
Note test 1 got past its
exit 0 / ignored: true / endpoint_source: daemon_statusassertions before failing: the impostor was believed, silently.After:
Full suite:
# tests 3091 # pass 3082 # fail 8. The 8 failures are the pre-existingleave ...tests, identical on a cleanorigin/masterbaseline (# tests 3087 # pass 3078 # fail 8). The@refvalidation test passes with the new annotations.npm run build:typesis clean.Fixes #451