Skip to content

report: record the consumer-asserted reporter source as a closed vocabulary - #27

Merged
ualtinok merged 1 commit into
cortexkit:masterfrom
legion-works:feat/reporter-source
Aug 31, 2026
Merged

report: record the consumer-asserted reporter source as a closed vocabulary#27
ualtinok merged 1 commit into
cortexkit:masterfrom
legion-works:feat/reporter-source

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Implements #26 as agreed: your counter-proposal shape, seeded with the three-way set the consumer confirmed.

The shape:

  • ReporterSource closed enum in audit.rs beside the other vocabularies: direct, relay_status_field, relay_message_parse, reserved unrecognised. Out-of-contract input maps to Unrecognised; the submitted string is dropped without being persisted or logged anywhere.
  • New nullable auth_events.reporter_source column (migration 8). detail keeps its vault-observed meaning; the new column is consumer-asserted-unverified, doc-commented as such at the struct and in the runbook.
  • Optional serde-default wire param on report_auth_failure; absent → NULL, old consumers untouched.
  • Rider isolation: the fence, stale-vs-latch arm, and rate limiting are unchanged; a superseded-version report carrying a reporter_source is still a silent no-op (asserted).
  • Runbook subsection with the same exhaustive-match doc pin the other three vocabularies carry — an undocumented variant fails the build.

Verification (all on this tree, gate.sh exit 0):

mutation result
mapping site → raw passthrough (params.reporter_source.as_deref()) RED: "must never appear in any string column"
store INSERT → NULL for the column RED: left: None, right: Some("unrecognised")
runbook line for relay_message_parse removed RED: "missing from its documented runbook section"
unmutated GREEN, full gate

The load-bearing test reports a 40-char lowercase token-shaped string — the exact case that passes ^[a-z0-9_]{1,64}$ — and asserts it appears in no string column of any row, not merely that reporter_source came back mapped. First draft of that assertion checked one column while its message claimed all of them; strengthened before this went up.

Consumer side: anthropic-auth confirmed they'll thread {status, source} out of the relay parse (the arm is known at match time) and assert direct on the direct path, so the seeded set reflects what will actually be reported.

Base is 6094a4e (one behind — 0aa6f79 touches only scripts/check-doc-status.py, no overlap).


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Records the consumer-asserted source of an auth failure so a relay parse false positive is distinguishable from the direct path and the structured relay field. report_auth_failure now accepts an optional reporter_source with a closed vocabulary (direct, relay_status_field, relay_message_parse, unrecognised), stored in a new nullable auth_events.reporter_source column via migration 8.

  • Implements report_auth_failure: optional consumer-supplied detail token for observation provenance #26: the new column holds consumer-asserted values; detail keeps recording only vault-observed values.
  • Out-of-contract input maps to unrecognised; the submitted string is dropped, never persisted or logged.
  • The fence, stale-vs-latch arm, and rate limiting are unchanged; old consumers are unaffected since the absent wire field deserializes to None and writes NULL.
  • The load-bearing test reports a token-shaped string and asserts it appears in no string column of auth_events, not merely in reporter_source.
  • The runbook documents all four values with a build-time pin, so an undocumented variant fails the build.

Written for commit 618caf7. Summary will update on new commits.

Review in cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 9 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="crates/credentials-module/src/main.rs">

<violation number="1" location="crates/credentials-module/src/main.rs:5209">
P2: When a stale report carrying `reporter_source` is recorded, this test still passes because it checks only state and the newest event. Assert that the audit-event set remains unchanged after the stale call, including that no `reporter_source` is persisted, to pin the required silent no-op behavior.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

.expect("bump the record version");
surface
.report_auth_failure(7, &params(401, 1))
.report_auth_failure(7, &params(401, 1, Some("relay_message_parse")))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a stale report carrying reporter_source is recorded, this test still passes because it checks only state and the newest event. Assert that the audit-event set remains unchanged after the stale call, including that no reporter_source is persisted, to pin the required silent no-op behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/credentials-module/src/main.rs, line 5209:

<comment>When a stale report carrying `reporter_source` is recorded, this test still passes because it checks only state and the newest event. Assert that the audit-event set remains unchanged after the stale call, including that no `reporter_source` is persisted, to pin the required silent no-op behavior.</comment>

<file context>
@@ -5200,7 +5206,7 @@ mod tests {
             .expect("bump the record version");
         surface
-            .report_auth_failure(7, &params(401, 1))
+            .report_auth_failure(7, &params(401, 1, Some("relay_message_parse")))
             .await
             .expect("a stale report is accepted, not errored");
</file context>

Comment thread crates/credentials-core/src/store.rs Outdated
Comment thread crates/credentials-module/src/read_surface.rs
Comment thread docs/operator-runbook.md
@iceteaSA
iceteaSA force-pushed the feat/reporter-source branch from 38e4dc2 to 11d063a Compare August 31, 2026 16:00
@iceteaSA

Copy link
Copy Markdown
Collaborator Author

All four findings addressed in 11d063a. Three were valid as stated; one was a valid gap with a prescription that would have pinned the designed behaviour out of existence — detail below since it's the interesting one.

Finding 1 (stale-report test gap): the suggested fix — "assert the audit-event set remains unchanged after the stale call" — is backwards. mark_stale_if_version_reported appends the auth_events row unconditionally, applied=false, and the migration-3 comment names the stale-report-leaving-no-trace case as the exact defect auth_events exists to fix. A superseded report is a state no-op, not a diagnostic no-op. The test now pins the designed behaviour: row written, applied=false, carrying the mapped reporter_source. Mutation-proved by wrapping the append behind n > 0 → RED, restored → GREEN.

Finding 3 (type-enforced unwritability): accepted and it's the right escalation. AuthObservation.reporter_source is now Option<ReporterSource> — the comment-promised invariant became a type. The raw-passthrough mutation that previously needed a runtime assertion to catch is now a compile error (expected Option<ReporterSource>, found Option<&str>), verified by applying it and pasting the failure. The store renders as_str() at the final SQL boundary only.

Findings 2 and 4: field doc comment added, plus the two wire-contract sites in docs/cortexkit-credentials-contract.md that also listed the op shape without the new param (the review flagged the struct only); runbook now documents NULL (omitted/legacy — normal) as distinct from unrecognised (sent and refused).

Full gate re-run at 11d063a: exit 0.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 10 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="crates/credentials-core/src/store.rs">

<violation number="1" location="crates/credentials-core/src/store.rs:3023">
P2: `ck auth events` drops `AuthEvent::reporter_source` from its output, so operators cannot see the source this field records through the documented event workflow. Update the CLI event rendering to include the optional reporter source.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

pub provider_status: Option<u16>,
pub detail: Option<String>,
/// Consumer-asserted, unverified; from `ReporterSource::as_str`, never raw consumer input.
pub reporter_source: Option<String>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: ck auth events drops AuthEvent::reporter_source from its output, so operators cannot see the source this field records through the documented event workflow. Update the CLI event rendering to include the optional reporter source.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/credentials-core/src/store.rs, line 3023:

<comment>`ck auth events` drops `AuthEvent::reporter_source` from its output, so operators cannot see the source this field records through the documented event workflow. Update the CLI event rendering to include the optional reporter source.</comment>

<file context>
@@ -3003,6 +3019,8 @@ pub struct AuthEvent {
     pub provider_status: Option<u16>,
     pub detail: Option<String>,
+    /// Consumer-asserted, unverified; from `ReporterSource::as_str`, never raw consumer input.
+    pub reporter_source: Option<String>,
     pub record_version: Option<u64>,
     /// Whether this observation actually changed the credential. False for a report
</file context>

Comment thread crates/credentials-module/src/read_surface.rs
Comment thread docs/cortexkit-credentials-contract.md Outdated
…bulary

A report_auth_failure now carries an optional reporter_source naming which
observation path saw the failure. The need comes from anthropic-auth#175: the
same 401 can arrive from a direct provider response or from a WebSocket relay
event, and the relay path itself has a strong arm (a structured status field)
and a weak arm (a regex over message text). The weak arm is where a parse
false-positive will eventually come from, and the version fence cannot stop a
wrong-but-current report -- so when one happens, the forensic question is which
path produced it, and auth_events could not answer that.

The column is consumer-asserted and the vault does not vouch for it, which is
why it is a NEW column rather than a widening of detail: every existing detail
value is a variant name from an enum this repo owns, and a reader of that
column may rely on it recording what the vault observed. The two kinds of
statement stay structurally apart (issue cortexkit#26).

The value set is a vault-owned closed enum, not a validated string. Charset
validation was proposed and rejected for a concrete reason: a 40-character
lowercase hex token satisfies ^[a-z0-9_]{1,64}$, so a character-class filter
passes exactly the secrets that do not look like secrets. With a closed set,
consumer input is unwritable by construction -- and the enum is now carried as
a typed Option<ReporterSource> through AuthObservation itself, so raw consumer
strings cannot reach ANY auth_events writer, not just the read surface. The
store renders the stable label only at the final SQL boundary; an unrecognised
wire value becomes "unrecognised" and the submitted string is dropped without
being persisted or logged. The load-bearing test reports a token-shaped string
and asserts it appears in NO string column of auth_events.

The rider never reaches the state machine: the fence, the stale-vs-latch arm
choice, and rate limiting are unchanged. A superseded-version report is a
STATE no-op that still records its applied=false diagnostic row -- deliberately,
since a stale report leaving no trace is the exact case auth_events was created
to fix -- and the row retains the mapped reporter_source, which the test pins.
Wire compatibility is serde-default: a params object without the field
deserializes to None and writes NULL, documented in the runbook as distinct
from "unrecognised" (omitted vs sent-and-refused). The ck-auth events listing
renders the source as a trailing src= field only when present, so legacy NULL
rows print byte-identically to the pre-column format.

Seeded values: direct, relay_status_field, relay_message_parse. The three-way
split (rather than one merged relay label) is deliberate: the column exists to
forensicate the weak parse arm, so the weak arm must be distinguishable by
construction. Documented in the runbook and the wire contract with the same
exhaustive-match pin the other vocabularies carry; adding a variant without
documenting it fails the build.
@iceteaSA
iceteaSA force-pushed the feat/reporter-source branch from 11d063a to 618caf7 Compare August 31, 2026 16:14
@iceteaSA

Copy link
Copy Markdown
Collaborator Author

Round 2 addressed in 618caf7 — all three valid.

The CLI one was the real catch: the runbook's documented workflow for this table is ck auth events, and the column was invisible through it. Now rendered as a trailing src=<label> only when present, so legacy NULL rows print byte-identically to the pre-column format — verified live on a scratch vault with one mapped row and one NULL row:

...  consumer_report_latch  401  v1  applied=no
...  consumer_report_latch  401  v1  applied=yes src=relay_message_parse

Module-level op doc now carries reporter_source?, and the contract-doc bullet is re-aligned to its siblings (bullet at column 0, 2-space continuations).

Full gate at 618caf7: exit 0.

@ualtinok
ualtinok merged commit 1538b76 into cortexkit:master Aug 31, 2026
4 of 6 checks passed
ualtinok added a commit that referenced this pull request Aug 31, 2026
MEASURED, NOT ASSUMED: the only principal holding grants is prefrontal-core, and
it holds three -- github_app: read, signing:agent-assertion: read and sign -- with
no capability handle. So the documented answer to 'how does a consumer observe a
credential coming back' (poll credential.status, which reads plaintext metadata
and never mints) was UNREACHABLE for the one consumer that asked the question, on
issue #6, two weeks ago. Its only route to a named credential was get_scoped,
which mints on a stale record: polling for repair meant repeatedly buying upstream
token exchanges, which is the cost this surface exists to avoid.

THE REFUSAL IS DELIBERATELY NOT A REFUSAL. An unauthorized or unknown scoped call
returns the IDENTICAL body an unresolvable handle returns -- ready false, version
and mark omitted -- from a single  constructor, so the two cannot
drift apart into an oracle for which credential ids exist. A surface that answered
'no grant' differently from 'no such credential' would enumerate the vault for any
principal that could reach it. The discrimination goes to auth_events, named with
the principal, exactly as get_scoped does: silent on the wire, loud in the log --
the split I got wrong in the route-epoch path, where the drop was silent in both.

VERIFIED HERE RATHER THAN TAKEN FROM THE REPORT:

  no-grant arm returns a DISTINGUISHABLE body
    -> 4 tests RED, incl. scoped_status_unknown_and_no_grant_are_
       indistinguishable_on_the_wire, by name

  wire-key pin           UNTOUCHED and green -- the reply shape does not
                         move, only the request shape, so no consumer
                         announcement is owed
  force_..._for_test     #[cfg(test)], absent from release
  Sign-only test         uses a prefix where the principal holds Sign and
                         NOT Read, so it cannot pass under either rule

The conflict on merge was the module header: this branch predates #27, which added
reporter_source? to the same doc line. Both belong; kept both.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants