opencode: scope a manifest credential id to its provider segment - #44
Conversation
There was a problem hiding this comment.
1 issue found across 4 files
You’re at about 92% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
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="docs/opencode-custody-design.md">
<violation number="1" location="docs/opencode-custody-design.md:49">
P2: These rows promise resolution even though this parser only validates shape and never checks vault existence. Change the expectation to `MUST PARSE` (or `MUST ACCEPT`) so fixtures and consumers do not treat a syntactically valid, operator-chosen ID as a resolvable credential.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| | MUST RESOLVE | `openai` | `main` | `chatgpt:openai` | A kind-prefix rule would reject this live OpenAI shape. | | ||
| | MUST RESOLVE | `google` | `main` | `antigravity:google` | A kind-prefix rule would reject this live Google shape. | | ||
| | MUST RESOLVE | `anthropic` | `work-alt` | `oauth:anthropic:something-else` | The label must not be derived or consulted. | |
There was a problem hiding this comment.
P2: These rows promise resolution even though this parser only validates shape and never checks vault existence. Change the expectation to MUST PARSE (or MUST ACCEPT) so fixtures and consumers do not treat a syntactically valid, operator-chosen ID as a resolvable credential.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/opencode-custody-design.md, line 49:
<comment>These rows promise resolution even though this parser only validates shape and never checks vault existence. Change the expectation to `MUST PARSE` (or `MUST ACCEPT`) so fixtures and consumers do not treat a syntactically valid, operator-chosen ID as a resolvable credential.</comment>
<file context>
@@ -34,6 +34,48 @@ provider with in-request failover for providers the generic plugin serves.
+
+| expectation | provider | account label | `credential_id` | reason |
+|---|---|---|---|---|
+| MUST RESOLVE | `openai` | `main` | `chatgpt:openai` | A kind-prefix rule would reject this live OpenAI shape. |
+| MUST RESOLVE | `google` | `main` | `antigravity:google` | A kind-prefix rule would reject this live Google shape. |
+| MUST RESOLVE | `anthropic` | `work-alt` | `oauth:anthropic:something-else` | The label must not be derived or consulted. |
</file context>
| | MUST RESOLVE | `openai` | `main` | `chatgpt:openai` | A kind-prefix rule would reject this live OpenAI shape. | | |
| | MUST RESOLVE | `google` | `main` | `antigravity:google` | A kind-prefix rule would reject this live Google shape. | | |
| | MUST RESOLVE | `anthropic` | `work-alt` | `oauth:anthropic:something-else` | The label must not be derived or consulted. | | |
| | MUST PARSE | `openai` | `main` | `chatgpt:openai` | A kind-prefix rule would reject this live OpenAI shape. | | |
| | MUST PARSE | `google` | `main` | `antigravity:google` | A kind-prefix rule would reject this live Google shape. | | |
| | MUST PARSE | `anthropic` | `work-alt` | `oauth:anthropic:something-else` | The label must not be derived or consulted. | |
|
A peer tenant re-derived their vendored copy of this predicate against the diff and asked whether an absent It does not. The empty-string row is worth noting since it changes route: Their question also surfaced something this PR does not change but should be recorded: a malformed row in any provider block refuses the entire document, so one tenant's bad entry denies the others. Filed separately as #45 — it is the file-wide |
0a15cb6 to
a9bf60e
Compare
|
Tightened: no segment of a The provider-segment rule alone accepted two ids that name credentials which cannot exist, because a position-1 check ignores the rest of the string: Neither of the accepted two is a cross-provider smuggle — segment 2 still fences the provider — but both defer a guaranteed resolve-time failure past the door. Under custody that matters more than it used to: a resolve-time failure on a tombstoned account is a dark route, not a refused row. The third line is the other half of the argument. Empty-at-position-1 rejected while empty-at-0 and empty-at-2 passed, purely because of where the check indexed. Nobody designed that, and the next reader to meet it either "fixes" it unilaterally or works around it. This was surfaced by the anthropic-auth tenant re-deriving their vendored copy of this predicate against the diff, and running the same inputs through both parsers. Their table matched the prediction exactly — no verdict divergence on any of the eight edge cases, including these two — so the rule as both sides had written it admitted them identically. The tightening is agreed on both sides and mirrored there, which makes it chosen rather than defaulted; the point of #45 is that the previous state was two independent defaults nobody had compared. Checked before tightening, since a rule that breaks production is worse than the gap it closes:
Mutation-proved, unlike the emptiness guard noted earlier: removing the clause reddens both new arms ( Explicitly not tightened, so these are chosen too: unknown keys inside an account, and extra top-level keys, both still parse. A strict-key parser makes every future field addition a flag-day for the oldest tenant in a shared manifest. An empty segment can never become meaningful; an unknown key can. |
4224e8c to
5c77827
Compare
|
Extended to the Rust validator, because leaving it out made this PR a divergence rather than a fix. A peer tenant checked their writer against this tightening rather than only their reader, on the grounds that I parse what they write. That sent me to mine, and if account.credential_id.is_empty() {The Rust side never got the provider-segment rule at all — not this PR's emptiness tightening, and not the original scoping. And Now matched: segment 2 must be the provider block, no segment may be empty, same rule and same rejection message. ArmsThree new Rust tests in The third is a positive control against the real deployment — all five live credential-id shapes ( Workspace floor measured on this tree and raised 610 → 613, red-checked both ways (614 fails naming the shortfall, 613 passes). A regression this PR introduces, stated plainlyBefore this change, So this strictly increases the blast radius of that input in exchange for catching it earlier. That trade is only correct once the radius is bounded, which means #45 is not a follow-up to this PR — it is this PR's precondition for being safe, and I had them in the wrong order. Recording it here rather than leaving it as a judgement call: the argument for per-row refusal is now "strict validation requires a bounded blast radius", not "nicer error messages". Not holding this PR for it. No writer on either side emits an empty segment today, both live manifests are clean, and the peer is mirroring the rule at their writer first for exactly this reason. |
There was a problem hiding this comment.
2 issues found across 2 reviewed files. 1 file intentionally excluded from review.
You’re at about 93% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
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/tests/cli_opencode.rs">
<violation number="1" location="crates/credentials-module/tests/cli_opencode.rs:362">
P2: These tests do not exercise the write path described in their comment: every case only parses a raw file through `read_handle_file`. Add an invalid in-memory `HandleFile` write assertion so removing `validate_handle_file` from `write_handle_file_for_tenant` cannot go undetected.</violation>
</file>
<file name="scripts/gate.sh">
<violation number="1" location="scripts/gate.sh:257">
P2: The floor bump to 613 leaves the measurement note immediately above it stale: it still claims "The current measured total is 608 (debug profile, the same command this arm runs)." Taken literally, a floor above the documented measured total means this arm fails every run (ran 608, expected at least 613). Per this file's own discipline the floor is the freshly measured total on the final tree, so raising it should update that sentence to the new measured number. Update the note to the actual measurement that produced 613.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| /// than left to look like an independent local rule. | ||
| #[test] | ||
| fn a_handle_file_with_a_cross_provider_credential_id_is_refused() { | ||
| let err = read_raw_handle_fixture( |
There was a problem hiding this comment.
P2: These tests do not exercise the write path described in their comment: every case only parses a raw file through read_handle_file. Add an invalid in-memory HandleFile write assertion so removing validate_handle_file from write_handle_file_for_tenant cannot go undetected.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/credentials-module/tests/cli_opencode.rs, line 362:
<comment>These tests do not exercise the write path described in their comment: every case only parses a raw file through `read_handle_file`. Add an invalid in-memory `HandleFile` write assertion so removing `validate_handle_file` from `write_handle_file_for_tenant` cannot go undetected.</comment>
<file context>
@@ -349,6 +349,71 @@ fn a_handle_file_with_an_empty_credential_id_is_refused() {
+/// than left to look like an independent local rule.
+#[test]
+fn a_handle_file_with_a_cross_provider_credential_id_is_refused() {
+ let err = read_raw_handle_fixture(
+ "cross-provider-credential-id",
+ r#"{"version":1,"providers":[{"provider":"deepseek","shape":"api","serve":"opencode-claustrum","accounts":[{"label":"main","handle":"ckh_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","credential_id":"oauth:anthropic"}]}]}"#,
</file context>
| # Raise this when tests are added. A failure here is normally that, not a defect -- | ||
| # but it should be a deliberate edit rather than a number nobody revisits. | ||
| run_expect 610 "workspace unit + integration" \ | ||
| run_expect 613 "workspace unit + integration" \ |
There was a problem hiding this comment.
P2: The floor bump to 613 leaves the measurement note immediately above it stale: it still claims "The current measured total is 608 (debug profile, the same command this arm runs)." Taken literally, a floor above the documented measured total means this arm fails every run (ran 608, expected at least 613). Per this file's own discipline the floor is the freshly measured total on the final tree, so raising it should update that sentence to the new measured number. Update the note to the actual measurement that produced 613.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/gate.sh, line 257:
<comment>The floor bump to 613 leaves the measurement note immediately above it stale: it still claims "The current measured total is 608 (debug profile, the same command this arm runs)." Taken literally, a floor above the documented measured total means this arm fails every run (ran 608, expected at least 613). Per this file's own discipline the floor is the freshly measured total on the final tree, so raising it should update that sentence to the new measured number. Update the note to the actual measurement that produced 613.</comment>
<file context>
@@ -254,7 +254,7 @@ stream and pass the arm without ever seeing it skip."
# Raise this when tests are added. A failure here is normally that, not a defect --
# but it should be a deliberate edit rather than a number nobody revisits.
-run_expect 610 "workspace unit + integration" \
+run_expect 613 "workspace unit + integration" \
cargo test --locked --workspace --features credentials-core/test-support
</file context>
The check was non-empty-string only, so a binding carrying `oauth:openai` parsed
cleanly inside an `anthropic` provider block and every tenant reading the shared
manifest would have honoured it. Two peer tenants found the same hole in their own
parsers independently, which is the argument for fixing it in the shared parser
rather than three times.
Segment 2 must BE the provider block it sits in. Scoped to segment 2 only, and the
two exclusions are deliberate rather than unfinished:
segment 1 (kind) an OPEN SET -- oauth:, chatgpt:, antigravity:, apikey: are all
live in this deployment, so a kind allowlist refuses real ids
segment 3+ (label) operator-chosen and optional -- main is the 2-segment
`oauth:anthropic`, fallbacks are 3-segment
Constraining either would reject the deployment this contract describes.
The predicate moved to packages/client since this was first written: handles.ts in
packages/opencode is now a re-export shim, so the fix lands in the shared parser and
both tenants inherit it.
Mutation-proved: restoring the weak predicate fails `rejects a credential id scoped
to another provider` and `rejects a credential id without a provider segment`.
5c77827 to
06badee
Compare
|
Both review findings were valid. Fixed in P2 (cli_opencode.rs): the comment claimed write-path coverage the arms did not haveCorrect, and it is the exact failure the comment itself was warning about. The doc block said "this is a WRITER" while every arm went through Added The first version of this arm was itself decorative, and the mutation is what caught it. Removing So the arm holds the write path as a whole and does not pin either call site individually. That limitation is now written at the arm, because the alternative is a reader inferring coverage from the name — which is precisely how the gap being fixed here was created. P2 (gate.sh): stale measurement noteCorrect. The note said 608 while the floor read 613; taken literally that arm fails every run. Re-measured on the final tree after adding the write-path arm: 614. Floor and note both updated, red-checked in both directions (615 fails, 614 passes). Worth recording how the measurement nearly went wrong: the first count returned 358, which is not what adding one test does. The number came from a truncated stream rather than a completed run — |
parseHandleFileaccepted any non-empty string as acredential_id, so a binding carryingoauth:openaiparsed cleanly inside ananthropicprovider block. Every tenant reading the shared manifest would have honoured it.Two peer tenants (anthropic-auth, openai-auth) found the same hole in their own parsers independently and proposed the same fix. That is the argument for landing it in the shared parser rather than three times in three repos.
The rule
Segment 2 of the credential id must be the provider block it sits in.
Why only segment 2
Both exclusions are deliberate rather than unfinished, and both were checked against the live vault before choosing:
oauth:,chatgpt:,antigravity:,apikey:are all in use in this deployment today. A kind allowlist would refuse real ids and would need editing every time a provider family is added.oauth:anthropic; fallbacks are 3-segment. Requiring a label rejects main, and constraining its contents rejects operator naming.An earlier draft of this rule proposed
oauth:Poroauth:P:— that kind-prefixed form would have refused four of the five credentials this vault serves.Where it landed
The predicate moved while this was in the queue:
packages/opencode/src/handles.tsis now a re-export shim over@cortexkit/claustrum-client, so the fix belongs in the shared parser. Both tenants inherit it from one place.Verification
Mutation-proved rather than asserted. Restoring the weak predicate (exact-occurrence-count checked before substituting, so the mutation cannot land on a different line):
Restored byte-identically (
cmp), green again. Fullscripts/gate.shexit 0 on the rebuilt tree; hermetic suite 197 pass.Two-way revert sweep against
6817148: of the 1770 distinct lines master added since this branch's original base, exactly one is deleted here — the weak predicate being replaced.Rebuilt on master rather than rebased: #33's squash reformatted the file this branch patched, and the implementation had since moved packages, so a rebase would have resolved a conflict between a style migration and a relocation. Applying the logical change to its current home is the smaller, checkable operation.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Scopes a manifest
credential_idto the provider block it sits in. Previously any non-empty ID was accepted, so a binding likeoauth:openaiinside ananthropicblock parsed cleanly and every tenant reading the shared manifest honored it; now the second colon-separated segment must match the provider.ck authcan no longer originate a row the reader refuses.Written for commit 06badee. Summary will update on new commits.