Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions hypaware-core/plugins-workspace/ai-gateway/src/control.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ const MAX_BODY_BYTES = 64 * 1024
* `.total`. The `session_id` is an opaque token: the gateway never
* interprets it, keeping the LLP 0050 provider-agnostic boundary exact.
*
* **`ignored: true` is set membership, and is not a verified drop.** The
* gateway holds tokens, not traffic: the drop happens in the client adapter,
* keyed on the `session_id` it stamps on the row (LLP 0066 R5), so this route
* cannot tell a live session id from a Codex thread id or a typo and answers
* `ignored: true` for all three. Making it able to would mean teaching a
* deliberately provider-agnostic route about client grain, which is the
* boundary above. So the contract is the narrow one and the CALLER owns
* resolving the right key before it posts; responses that read as more than
* that are what LLP 0066 R14 forbids.
* @ref LLP 0066#receipt-is-membership [constrained-by]: the route confirms the
* write only, so callers must resolve the key rather than expect an echo to
* prove the drop.
*
* @ref LLP 0066#control-path [implements]: the reserved `/_hypaware/`
* prefix is a local control surface; this handler owns the routes served
* under it, holding only opaque session-id tokens.
Expand Down
52 changes: 50 additions & 2 deletions hypaware-core/plugins-workspace/ai-gateway/src/session_command.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,45 @@ const FOLDER_GOVERNOR_NOTE = 'folder: see `hyp policy show` (this verb reports
const EPHEMERAL_NOTE =
'this opt-out is in-memory only: a gateway restart drops it, and a fork (`claude --fork-session`, `codex fork`) mints a new session id it no longer covers. Re-check with `hyp session status`.'

/**
* What a confirmed `ignored` establishes, printed next to it by the writer and
* the reader alike, for the same no-drift reason as `EPHEMERAL_NOTE`.
*
* The control route is a `Set` over opaque tokens: `POST` adds whatever it was
* handed and answers `ignored: true`, `GET` is the same `Set.has`. Neither verb
* sees a single exchange, so neither can establish that the id is one live
* traffic carries - the drop happens later, in the client adapter, keyed on the
* `session_id` it stamps on the row (LLP 0066 R5). "the gateway will drop this
* session" was therefore a promise the receipt could not support: an id no
* exchange will ever carry (a Codex thread id, a finished session's id, a typo)
* prints exactly the same line and suppresses nothing.
*
* Naming the caller's own responsibility is the other half. The gap is closed
* by resolving the right key BEFORE the call (`resolveSessionIdForCli`), not by
* anything the route can answer afterwards, so the receipt says which of the
* two claims it is making.
*
* @ref LLP 0066#receipt-is-membership [implements]: R14 - the receipt states
* set membership and names who owns the key, rather than reporting a drop
* nothing verified.
*/
const MEMBERSHIP_NOTE =
'what this proves: the gateway holds this exact id in its drop set, and nothing more. It never inspects traffic, so an exchange is dropped only where the client adapter stamps it with this same session_id - an id this session does not carry prints this same line and suppresses nothing. Naming the right id is on the caller, which is why this verb resolves it (or takes it explicitly) rather than asking the gateway to confirm it afterwards.'

/**
* The machine-readable form of `MEMBERSHIP_NOTE`, carried by the write verbs'
* `--json` receipt. `status: 'ok'` on its own reads as "done" to the skills
* that parse it, which is the same overclaim in the shape an agent actually
* consumes.
*
* The reader's `--json` needs no equivalent: it already reports `status` as
* `ignored` / `not_ignored` / `unknown` beside a tri-state `ignored`, which is
* a membership answer on its face.
*
* @ref LLP 0066#receipt-is-membership [implements]: R14
*/
const MEMBERSHIP_GUARANTEE = 'set_membership'

/**
* The control plane's authenticity contract, printed beside every **confirmed**
* answer, by the writer and the reader alike.
Expand Down Expand Up @@ -300,6 +339,8 @@ async function runMutation(argv, ctx, method, usage) {
ctx.stdout.write(
JSON.stringify({
status: 'ok',
// What the `ok` above is an `ok` about, for the agent parsing this.
guarantee: MEMBERSHIP_GUARANTEE,
session_id: resolvedId.sessionId,
session_id_source: resolvedId.source,
session_id_evidence: resolvedId.evidence ?? null,
Expand All @@ -315,13 +356,19 @@ async function runMutation(argv, ctx, method, usage) {
)
return 0
}
// The headline states the write that happened, not a drop nobody verified:
// the route added an opaque token to a set (LLP 0066#receipt-is-membership).
// The removal says no more than that either. "recording resumed" was the same
// overclaim mirrored: a token nothing carried suppressed nothing to resume,
// and the folder governor below is a separate reason a session stays unrecorded.
ctx.stdout.write(
ignored
? `session ${resolvedId.sessionId}: ignored - the gateway will drop this session (${total} ignored)\n`
: `session ${resolvedId.sessionId}: not ignored - recording resumed (${total} ignored)\n`
? `session ${resolvedId.sessionId}: ignored - this id is in the gateway drop set (${total} ignored)\n`
: `session ${resolvedId.sessionId}: not ignored - this id is out of the gateway drop set, so this opt-out suppresses nothing now (${total} ignored)\n`
)
if (ignored) {
ctx.stdout.write(`${EPHEMERAL_NOTE}\n`)
ctx.stdout.write(`${MEMBERSHIP_NOTE}\n`)
}
// The write verbs carry the same provenance caveats as the read: "ignored"
// printed off an inferred id is a claim about a session the user may not be
Expand Down Expand Up @@ -371,6 +418,7 @@ function writeStatus(ctx, json, report) {
} else if (report.status === 'ignored') {
ctx.stdout.write(`session ${report.session_id}: ignored (${report.total} ignored in total)\n`)
ctx.stdout.write(`${EPHEMERAL_NOTE}\n`)
ctx.stdout.write(`${MEMBERSHIP_NOTE}\n`)
for (const note of provenanceNotes({
idSource: report.session_id_source,
idEvidence: report.session_id_evidence,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,34 @@ response="$(curl --fail-with-body --silent --show-error \
-H 'content-type: application/json' \
--data "$(printf '{"session_id":"%s"}' "$CLAUDE_CODE_SESSION_ID")")"

total="$(printf '%s' "$response" | python3 -c 'import json,sys; print(json.load(sys.stdin)["total"])')"
# Check the reply before believing it, the same three ways `hyp session ignore`
# does: `ignored` a real boolean true, `total` a real number, and `session_id`
# echoed back byte-for-byte. The route echoes the token verbatim, so a reply
# about a different session establishes nothing about this one - and reaching
# *something* on that port is not the same as reaching the gateway.
total="$(printf '%s' "$response" | python3 -c '
import json, sys
expected = sys.argv[1]
try:
r = json.load(sys.stdin)
except Exception:
sys.exit("opt-out NOT confirmed: the reply was not JSON, so it is not the control route")
# bool is excluded because isinstance(True, int) is True in Python: the CLI
# check this mirrors is `typeof total !== "number"`, which a JSON true fails.
if not isinstance(r, dict) or r.get("ignored") is not True or isinstance(r.get("total"), bool) or not isinstance(r.get("total"), int):
sys.exit("opt-out NOT confirmed: " + json.dumps(r))
if r.get("session_id") != expected:
sys.exit("opt-out NOT confirmed: the reply is about session %s, not %s" % (json.dumps(r.get("session_id")), json.dumps(expected)))
print(r["total"])
' "$CLAUDE_CODE_SESSION_ID")"
printf 'Ignored session %s. Total ignored: %s\n' "$CLAUDE_CODE_SESSION_ID" "$total"
```

If that check fails, say the session is **still being recorded**; do not report a partial success.

## Notes

- **What the confirmation proves.** `ignored: true` means the id is in the gateway's in-memory drop set, and nothing more. The gateway holds the id as an opaque token and never inspects traffic, so it cannot confirm the id is one this session's exchanges carry; the match happens later, in the client adapter, against the `session_id` it stamps on the row. For Claude the session *is* the conversation and `CLAUDE_CODE_SESSION_ID` is that same id, which is what makes the opt-out real - the reply is a receipt for the write, not a verified drop.
- The opt-out is held in-memory by the running AI gateway. A gateway restart drops the entry; if a long-running gateway is restarted mid-session, re-run `/hypaware-ignore`.
- This only affects the *current* Claude session. Concurrent sessions in the same working directory continue to record unless covered by a `.hypignore` file.
- For committable / team-wide opt-out, drop an empty `.hypignore` file at the top of the repo instead.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,33 @@ response="$(curl --fail-with-body --silent --show-error \
-H 'content-type: application/json' \
--data "$(printf '{"session_id":"%s"}' "$CLAUDE_CODE_SESSION_ID")")"

# Verify the gateway reports this session as ignored. `ignored` must be true.
# Verify the gateway accepted the opt-out, and that the reply is about THIS
# session. Same three checks `hyp session ignore` applies (validateControlResponse
# in ai-gateway/src/session_command.js): `ignored` a real boolean true, `total` a
# real number, and `session_id` echoed back byte-for-byte. The route echoes the
# token verbatim, so a reply naming a different session establishes nothing about
# this one, and reaching *something* on the port is not reaching the gateway.
printf '%s' "$response" | python3 -c '
import json, sys
r = json.load(sys.stdin)
if r.get("ignored") is not True:
expected = sys.argv[1]
try:
r = json.load(sys.stdin)
except Exception:
sys.exit("opt-out NOT confirmed: the reply was not JSON, so it is not the control route")
# bool is excluded because isinstance(True, int) is True in Python: the CLI
# check this mirrors is `typeof total !== "number"`, which a JSON true fails.
if not isinstance(r, dict) or r.get("ignored") is not True or isinstance(r.get("total"), bool) or not isinstance(r.get("total"), int):
sys.exit("opt-out NOT confirmed: " + json.dumps(r))
print("opt-out confirmed for session %s (total ignored: %s)" % (r.get("session_id"), r.get("total")))
'
if r.get("session_id") != expected:
sys.exit("opt-out NOT confirmed: the reply is about session %s, not %s" % (json.dumps(r.get("session_id")), json.dumps(expected)))
print("opt-out confirmed for session %s (total ignored: %s)" % (expected, r["total"]))
' "$CLAUDE_CODE_SESSION_ID"
```

If the `curl` fails (gateway not running, wrong port) or the verification line does not print `opt-out confirmed`, **stop and tell the user the review session is still being recorded**. Only proceed if they explicitly accept that risk.

**What `opt-out confirmed` proves, exactly.** The gateway holds the id as an opaque token: `ignored: true` means the id is in its drop set, and nothing more. It never inspects traffic, so it cannot tell you the id is one your exchanges carry - that match happens later, in the client adapter, against the `session_id` it stamps on the row. For Claude the session *is* the conversation and `CLAUDE_CODE_SESSION_ID` is that same id, so sending it is what makes the opt-out real; the reply is a receipt for the write, not a verified drop. Do not report it to the user as more than that, and never treat a follow-up `GET` as extra proof: it is the same set lookup answering the same question.

The opt-out is held in memory by the running gateway and keyed on that one session id, so two things drop it: a **gateway restart**, and a **new session id** minted under what the user experiences as the same conversation (`claude --fork-session`; a plain `--resume` / `--continue` reuses the id). If the review spans either, re-run this step. `hyp session status` reports the current answer for the session you are in at any point. Reverse later with `/hypaware-unignore`.

## Step 2 - Check that backfill has settled (before surveying)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,39 @@ fi
BASE="${ANTHROPIC_BASE_URL:-http://127.0.0.1:8787}"
URL="${BASE%/}/_hypaware/ignore/session"

curl --fail-with-body --silent --show-error \
response="$(curl --fail-with-body --silent --show-error \
-X DELETE "$URL" \
-H 'content-type: application/json' \
--data "$(printf '{"session_id":"%s"}' "$CLAUDE_CODE_SESSION_ID")" \
> /dev/null

printf 'Recording re-enabled for session %s\n' "$CLAUDE_CODE_SESSION_ID"
--data "$(printf '{"session_id":"%s"}' "$CLAUDE_CODE_SESSION_ID")")"

# Check the reply before believing it, the same three ways `hyp session unignore`
# does (`validateControlResponse` in ai-gateway/src/session_command.js): `ignored`
# a real boolean - `false` here, since removal is what was asked for - `total` a
# real number, and `session_id` echoed back byte-for-byte. The route echoes the
# token verbatim, so a reply about a different session establishes nothing about
# this one, and reaching *something* on that port is not reaching the gateway.
total="$(printf '%s' "$response" | python3 -c '
import json, sys
expected = sys.argv[1]
try:
r = json.load(sys.stdin)
except Exception:
sys.exit("removal NOT confirmed: the reply was not JSON, so it is not the control route")
# bool is excluded because isinstance(True, int) is True in Python: the CLI
# check this mirrors is `typeof total !== "number"`, which a JSON true fails.
if not isinstance(r, dict) or r.get("ignored") is not False or isinstance(r.get("total"), bool) or not isinstance(r.get("total"), int):
sys.exit("removal NOT confirmed: " + json.dumps(r))
if r.get("session_id") != expected:
sys.exit("removal NOT confirmed: the reply is about session %s, not %s" % (json.dumps(r.get("session_id")), json.dumps(expected)))
print(r["total"])
' "$CLAUDE_CODE_SESSION_ID")"
printf 'Session %s is out of the gateway drop set, so this opt-out suppresses nothing now. Total ignored: %s\n' "$CLAUDE_CODE_SESSION_ID" "$total"
```

If that check fails, do not report the opt-out as lifted; say the gateway did not confirm the removal.

## Notes

- **What the confirmation proves.** `ignored: false` means the id is no longer in the gateway's in-memory drop set, and nothing more. The gateway holds the id as an opaque token and never inspects traffic, so it cannot tell you recording resumed: an id this session's exchanges never carried was suppressing nothing to resume, and a `.hypignore` ancestor is an independent reason the session stays unrecorded. The reply is a receipt for the removal, not a verified resumption.
- Only the *temporary, in-memory* opt-out is reversed. Recording stays suppressed if the working directory is covered by a `.hypignore` ancestor file. Remove those by deleting the marker file.
- The CLI is idempotent: it returns success even when the session was not currently ignored.
Original file line number Diff line number Diff line change
Expand Up @@ -127,22 +127,38 @@ response="$(curl --fail-with-body --silent --show-error \
-H 'content-type: application/json' \
--data "$(printf '{"session_id":"%s"}' "$SESSION_ID")")"

# Verify the gateway accepted the opt-out. `ignored` must be true. Note the
# bound of this check: the control route holds the id as an opaque token, so a
# true here proves the id is in the drop set, not that it is the id this
# Verify the gateway accepted the opt-out, and that the reply is about THIS
# session. Same three checks `hyp session ignore` applies (validateControlResponse
# in ai-gateway/src/session_command.js): `ignored` a real boolean true, `total` a
# real number, and `session_id` echoed back byte-for-byte. The route echoes the
# token verbatim, so a reply naming a different session establishes nothing about
# this one, and reaching *something* on the port is not reaching the gateway.
#
# Note the bound of all three: the control route holds the id as an opaque token,
# so a true here proves the id is in the drop set, not that it is the id this
# session's exchanges carry. That is why the id above is established from the
# rollout's container rather than guessed.
printf '%s' "$response" | python3 -c '
import json, sys
r = json.load(sys.stdin)
if r.get("ignored") is not True:
expected = sys.argv[1]
try:
r = json.load(sys.stdin)
except Exception:
sys.exit("opt-out NOT confirmed: the reply was not JSON, so it is not the control route")
# bool is excluded because isinstance(True, int) is True in Python: the CLI
# check this mirrors is `typeof total !== "number"`, which a JSON true fails.
if not isinstance(r, dict) or r.get("ignored") is not True or isinstance(r.get("total"), bool) or not isinstance(r.get("total"), int):
sys.exit("opt-out NOT confirmed: " + json.dumps(r))
print("opt-out confirmed for session %s (total ignored: %s)" % (r.get("session_id"), r.get("total")))
'
if r.get("session_id") != expected:
sys.exit("opt-out NOT confirmed: the reply is about session %s, not %s" % (json.dumps(r.get("session_id")), json.dumps(expected)))
print("opt-out confirmed for session %s (total ignored: %s)" % (expected, r["total"]))
' "$SESSION_ID"
```

If the session id cannot be resolved (the script refuses on ambiguity, staleness, or a missing container by design), the `curl` fails, or the verification line does not print `opt-out confirmed`, **stop and tell the user the review session may still be recorded**. Only proceed if they explicitly accept that risk.

**What `opt-out confirmed` proves, exactly.** `ignored: true` means the id is in the gateway's drop set, and nothing more. The gateway never inspects traffic, so it cannot tell a live session container from a thread id or a finished session's id: it answers `ignored: true` for whatever it was handed. The drop happens later, in the client adapter, against the `session_id` it stamps on the row. Everything that makes this opt-out real therefore happened *before* the POST, in resolving `payload.session_id` above - the reply is a receipt for the write, not a verified drop. Report it to the user that way, and never treat a follow-up `GET` as extra proof: it is the same set lookup answering the same question.

The opt-out is held in memory by the running gateway and keyed on that one session id, so two things drop it: a **gateway restart**, and a **new session id** minted under what the user experiences as the same conversation (`codex fork <id>`; a plain `codex resume <id>` reuses the id). If the review spans either, re-run this step. `hyp session status` reports the current answer for the session you are in at any point.

## Step 2 - Check that backfill has settled (before surveying)
Expand Down
Loading
Loading