Repository navigation
feat(insurance): add read-only archera-comparison command - #2166
Conversation
Explicit, default-off comparison of an Archera commitment plan using pkg/insurance. Key from ARCHERA_API_KEY only (no flag); org and plan IDs from env or flags. Table or JSON output via cmd.OutOrStdout, exact decimal money, unknown never rendered as 0, vendor strings sanitized, disclosures verbatim. Never purchases and makes no request unless run. Closes #2156 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Gate review (independent, Opus) at 722808a: CHANGES REQUESTED, not mergedLocal evidence (
Security properties: hold. Mutations killed by the PR's tests: unknown-as-supported (JSON and table), disclosures replaced, 2-decimal money, JSON written to os.Stdout, key fallback, missing-setting message echoing the key, Retry-After hidden, one automatic retry, Findings (blocking):
CI: Integration Tests fail on |
|
Line-number correction for the gate comment above (same findings, head 722808a): archera_render.go financials L160-165, totals upfront L170, offer fields L188-205 (deltas L201-204), hypothetical fields L230-235, line item fields L240-244; test case "403 echoing key" is archera_comparison_test.go:296. |
Every money field in the fixture now carries a unique value (hundreds digit is the block, units digit the field) and table and JSON output are compared against golden files, so a swapped field mapping changes the output. One value is a long decimal that float64 cannot hold exactly. The vendor error cases use the real "message" body shape and assert the key is redacted. The credential-flag check matches any flag containing key, token, secret or password. Refs #2156 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Gate re-review (independent, Opus) at 7082bb8: CHANGES REQUESTED, not mergedThe diff since 722808a touches only test files and testdata; the renderer is unchanged. Local run ( I reran my mutation set against the PR's Archera tests: 48 of 50 mutants were killed. These include every money/offer/row/hypothetical/line-item field mapping, money via float64 (both goldens fail), deleting the caveat, an
|
|
Correction to the re-gate comment above: the offer-name mapping is archera_render.go:196, not :194. |
…omparison The fixture now injects ESC and C1 characters (and an over-256-byte name) into the vendor string fields, the goldens pin the sanitized values, and a direct DTO test covers the fields the decoder would reject. The printed fetch time is asserted to be the current instant in UTC with the process zone shifted. Pkg-supplied product support text is no longer sanitized. Refs #2156 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Gate finding 1 (blocker) at 222ef76: data race from mutating
Suggested fix: set the shifted zone once before any goroutine starts (in |
|
Gate re-review at 222ef76: REQUEST CHANGES (blocker is finding 1 above: the Since 66b752d the PR's own files are unchanged except CHANGELOG lines from main. Other evidence at this SHA (git archive, GOTOOLCHAIN=go1.26.9): full Independent mutation sweep, 73 mutants over every DTO field mapping, sanitizer call sites, money formatting, disclosures/caveat/premium note, Retry-After, format check, output writer, settings/key handling, loops: 65 killed, 8 survived. The Survivors (finding 2, should fix alongside the race):
Non-blocking: dropping Product-support Source/Evidence left unsanitized: accepted, they come from pkg constants in |
Set the test zone once in init instead of writing time.Local per test (data race with the httptest goroutines). Capture the real os.Stdout/os.Stderr, assert flag-over-env precedence, our own plan-ID message and the currency mapping, and add a second row and a third hypothetical to the fixture. Strip format and line/paragraph separator characters (bidi overrides) like the platform cleaner. Decode errors from pkg/insurance echo the rejected vendor value, so cap and sanitize all non-HTTP error text; tested with a 5000-byte hostile value in every validated enum field. Refs #2156 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Gate finding F1 at 8f95325 (credential handling, blocking): the API key can reach the error output Reproduced with a probe test in a
pkg masks a vendor-echoed key on the HTTP path, so vendor echo is an in-scope threat; the CLI now has two paths around it. Fix: in |
|
Gate verdict at 8f95325: REQUEST CHANGES (finding F1 above, credential handling, plus two credential/security-sink test gaps below). Evidence at this SHA (
Mutation sweep: 140 mutants, 123 killed, 17 survived. Blocking (credential handling / security sink):
Non-blocking (equivalent or cosmetic, one list): money via |
Clean vendor error text first (controls, format and separator characters, and the literal \uXXXX escapes pkg decode errors print with %q), then mask the key, then cap. A key echoed with U+200B between characters used to be restored by the strip after pkg exact-match masking missed it. Tests cover key split by zero-width, control and bidi characters, echoed twice, straddling the cap, as an enum value in a decode error, bidi in an HTTP error with Retry-After, and a log/slog capture proving the key is never logged. Refs #2156 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Gate finding F3 at 2d42cf8 (credential handling, blocking per the cross-repo ruling from the mcp #59 gate) F1 is fixed for Cc/Cf/Zl/Zp and for literal Still leaking (NB1): characters that survive
Fix (the mcp ruling, fail closed): build a skeleton of the cleaned message with Cc/Cf/Mn/Me/Zl/Zp/Zs, variation selectors (FE00-FE0F, E0100-E01EF), CGJ, the Hangul fillers and invalid bytes removed. If the skeleton contains the trimmed key, withhold the whole message (e.g. Accepted as documented limits (non-blocking): success-path table/JSON output is not key-masked (coordinator ruling: document in the PR body). Keys that themselves contain |
|
Gate verdict at 2d42cf8: REQUEST CHANGES (F3 above, plus one credential test gap). Evidence at this SHA (
Mutation sweep at this head: 126 mutants run, 113 killed, 13 survived. The 22 render-only table/sanitizer-site mutants were not re-run: Blocking:
Non-blocking (equivalent/cosmetic): final-cap strip removal (equivalent: the text is already cleaned before masking); regex without Documented limits (coordinator ruling): success table/JSON output is not key-masked; please state this in the PR body. |
Build a skeleton of the cleaned error text (controls, format, combining, enclosing and separator runes, Hangul fillers and invalid bytes dropped) and withhold the whole message when the masked text still contains the key. A key split by variation selectors, CGJ, U+0301, U+20DD, Hangul fillers or invalid UTF-8 no longer reaches the terminal. The check runs on the full text before the 512-byte cap, after masking the trimmed key. Tests cover HTTP, decode, transport and context-wrapped errors, a key prefix sweep at the cap boundary, and withholding at every cap offset. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Fixes pushed at 70a511b for the gate findings at 2d42cf8.
|
|
Gate verdict at 70a511b: REQUEST CHANGES (F4, F5; both credential handling, blocking) F3 from the previous round is addressed for the cases it named. Two bypasses remain. Evidence: F4: key prefix survives the pinned lib's 200-byte cutThe pinned Probe: vendor 400 body (The unsplit key straddling the cut is masked by the lib before truncation, so the split is required; that is exactly the vendor-controlled input F1/F3 covered.) Fix (test first):
F5: U+2800 and unassigned code points bypass
|
…able The pinned pkg cuts HTTP messages at 200 bytes before masking, so a split key surfaced as a readable prefix. Check for any 8+ character key run in the skeleton, and build the skeleton from only the runes in the key so unlisted invisible characters (U+2800, unassigned code points) drop out. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Addressed gate F4 and F5 in the new commit (head e2035fb, main merged in).
Synthetic keys only. Not merged; re-gate needed. |
|
Gate verdict at e2035fb: REQUEST CHANGES (G1, G2 blocking credential handling; G3, G4 test gaps; G5 low). Not merged. Evidence: F4/F5 from the last round are fixed. My F4 probe (U+200B-split key across the pkg's 200-byte cut, HTTP path) and F5 probe (U+2800, U+0378, U+E0080 between every key character) both PASS at this SHA and FAILED at 70a511b. CI: all checks complete, success except Trivy/gosec NEUTRAL; mergeStateStatus CLEAN. Labels match #2156. Local: G1 (blocking): a key character used as the separator defeats the allow-listThe skeleton keeps every rune that occurs in the key, so a separator taken from the key's own alphabet stays in the skeleton and breaks every 8-window. Same result with Fix (test first):
G2 (blocking): case-changed key is shown
Upper case behaves the same way. That output leaves only letter case to guess. The same holds for the fullwidth form ( Fix (test first): add lower, upper and fullwidth ( G3 (test gap): threshold boundary is untestedMutants G4 (test gap): window position is untestedMutants G5 (low): key containing a non-printing rune or inner space
Mutation sweep (21 mutants,
|
…oes in archera errors Match key windows as a gapped subsequence (gap <= 2) of the folded message skeleton, fold with NFKD and lower case, and clean the key the way the reader sees it. Adds boundary, window-position, gap-bound and negative tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Addressed gate G1-G5 at 0230025.
Synthetic keys only. Not merged. |
|
Gate verdict at 0230025: REQUEST CHANGES (H1 hang, H2 gap-3 key-alphabet separators; both blocking). Not merged. Head is 0230025 (lint-only commit on top of f668042). Evidence: What passes
H1 (blocking):
|
| value size | time |
|---|---|
| 4 KB (unit) | 2.8 ms |
| 64 KB (unit) | 545 ms |
| 256 KB (decode path) | 9.9 s |
| 1 MB (decode path) | 2m22s |
Extrapolated to the pkg's 8 MB limit: about 2.5 hours.
Fix (test first):
- Add
TestArcheraHostileLargeDecodeValueIsBounded: decode path with a 1 MB value shaped as above,requirethat it finishes within 1 s (or usetesting.Benchmark/an elapsed bound with headroom for CI), and assert the error is still capped. Confirm it fails at 0230025. - Make the matcher linear: keep a boolean "reachable at position i for run index k" and, for each next rune, mark j reachable if any of
j-1-g(g ≤ archeraMaxGap) was reachable. That is O(M x n) per window. Also bound M: only text that can reach the output matters, so checkarcheraSanitizeN(msg, archeraMaxErrorSize + slack)where slack covers one fully-gapped window (for examplearcheraFragmentLen*(archeraMaxGap+1)runes, times the max rune width). KeepTestArcheraErrorsWithholdSplitKeyAtAnyCapOffsetgreen.
H2 (blocking): three or more key-alphabet separator runes still hide the key
The gap bound of 2 closes - and --, but the same G1 class reopens at gap 3. HTTP path, 401, archeraLongKey with the separator after every char:
sep "---": archera API returned HTTP 401: bad key P---R---B-------s---Y---n---T---h---3---t---1---c-------Q...
sep "0x0": archera API returned HTTP 401: bad key P0x0R0x0B0x0-0x0s0x0Y0x0n0x0T0x0h0x030x0t0x010x0c0x0...
sep "-- -": archera API returned HTTP 401: bad key P-- -R-- -B-- ---- -s-- -Y-- -n-- -T-- -h-- -3-- -t-- ...
These are still readable at a glance. Raising the gap only moves the boundary, and false positives grow with it.
Fix (test first):
- Add
"---","0x0","-- -"and a 5-rune key-alphabet separator to the separator table inTestArcheraLongKeyVisibleDisguisesAreWithheld. Confirm they fail at 0230025. - Detect a repeated separator instead of bounding its length. For each occurrence of a window's first rune, take the text between it and the next occurrence of the window's second rune (up to some bound, for example 16 runes) as the candidate separator
s. Withhold if the window spelled withsbetween every neighbour occurs in the skeleton. Keep the gap-≤2 subsequence check for irregular single insertions. Both checks are linear per start once H1's bound is in place. Over-withholding stays low becauseshas to repeat exactly 7 times.
Notes (non-blocking)
- Confusables are not folded: Cyrillic
Тinside the key, or dotlessıfori, is shown. NFKD does not map these. This is acceptable residual risk unless you want a confusables table. I'd record it in the PR body. - Mutants: 35 run, 31 killed. They include every previous-round mutant, threshold 9/10, gap 3/1/0, fold removed, lower removed, NFKD removed, NFD instead of NFKD, fold on one side only, key graphic/space filters, gap off by one, skip first/last/prefix-only window, and first rune skipped. Four survive:
- Keeping Mn/Me, Hangul fillers or U+FFFD in the message skeleton survives because every test inserts one invisible rune per gap, and gap ≤2 absorbs it. Adding a 3-rune invisible separator (for example
"́́́") to the table kills these. gap-ignores-order, which lets a predecessor sit up to 3 runes after the current rune, survives. It only causes over-withholding, so a test that a locally scrambled fragment is shown would pin it. Low priority.
- Keeping Mn/Me, Hangul fillers or U+FFFD in the message skeleton survives because every test inserts one invisible rune per gap, and gap ≤2 absorbs it. Adding a 3-rune invisible separator (for example
The next gate re-runs these probes, the 1 MB timing and the mutants against the new head.
…rators Make the key-window match linear and limit it to the displayable prefix plus slack, so a hostile 1 MB rejected value no longer takes minutes. Detect a separator of up to 16 key-alphabet runes repeated between every key character. Adds the bounded-time test and the separator cases. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Addressed H1 and H2 at 1194b60.
Synthetic keys only. Not merged. |
There was a problem hiding this comment.
🔇 Additional comments (3)
cmd/archera_comparison.go (1)
171-171: LGTM!Also applies to: 184-273
go.mod (1)
72-72: LGTM!cmd/archera_comparison_key_test.go-178-192 (1)
178-192: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
⚠️ Unverified finding
Verification ran but could not confirm this finding. It is shown for review, not as a verified issue.The 1-second wall-clock assertion can make CI flaky.
go test -raceis used in CI, and the race detector slows code 5–20×. A fullrunArcheraround trip also decodes about 1 MiB of JSON through httptest. On a loaded runner, this test can go over one second even though the matcher is bounded. Use a looser bound such as 10 seconds. You can also test the bounded behavior without timing, by callingarcheraKeyReadabledirectly on the large input.Proposed fix
- assert.Less(t, elapsed, time.Second) + assert.Less(t, elapsed, 10*time.Second)
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository: LeanerCloud/cloud-commitments-cli/.coderabbit.yaml
- Review profile: CHILL
- Plan: Essentials
- Run ID:
af7d58a5-c158-46e4-8cd9-407bd5a1e557
📒 Files selected for processing (3)
cmd/archera_comparison.gocmd/archera_comparison_key_test.gogo.mod
Included review availability: This review used your included allowance. 0 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour.
|
Gate verdict at 1194b60: APPROVE. Merging at this SHA with Evidence: H1 (hang) fixed. The decode path with a
H2 fixed. HTTP path, separator after every key char: Still passing: F4 (U+200B-split key across the pkg's 200-byte cut), F5 (U+2800, U+0378, U+E0080), G1 ( False positives: none. 8 ordinary vendor messages against 6 key shapes (41-char synthetic, 32 hex, 64 hex, base62-40, Checks: CI complete, all success except Trivy/gosec NEUTRAL; mergeStateStatus CLEAN. Labels match #2156. Local Residual risk (non-blocking, adversarial-only shapes; follow-ups):
|
Summary
Adds
ri-helper archera-comparison: an explicit, read-only, default-off comparison of one Archera commitment plan usingpkg/insurance(pkg pin 73d3366, already on main). It never purchases, is independent of--purchase, and makes no Archera request unless run.ARCHERA_API_KEYfrom the environment only (no key flag);ARCHERA_ORG_ID/ARCHERA_PLAN_IDfrom env or--org-id/--plan-id(read inside RunE only, so--helpnever shows them). Missing setting: exit 1 naming the setting, no value echoed.insurance.Configand*insurance.Clientare RunE locals; nothing holds them in a package var or struct. Production passeshc=nil(hardened client, pinned origin, no redirects, no retries).cmd.OutOrStdout()only (AppLogger not used), so--format jsonis exactly one document. Exact decimal money (smallest round-tripping precision, no float, no division), unknown =unknown/null, 730-hour monthly vs one-time upfront, discount rate verbatim, vendor strings stripped of control chars and capped at 256 bytes, product support supported/unknown only, caveat and plan-wide note, disclosures verbatim from pkg/common (non_gating_disclosure,sponsorship_disclosure).Evidence (synthetic, offline)
All tests use httptest behind a RoundTripper that asserts
https://api.archera.aibefore rewriting; synthetic key and UUIDs; no live Archera calls. Covered: default-off (no request on --help), each missing setting, documented GET path/header/no query, table and JSON output (premium-inclusive, unknown, fallback reason, lease attached, supported vs unknown product), control-char stripping, 401/403 (body echoing the key)/429 (Retry-After 30)/invalid UUID/redirect (second URL never hit), no key-like flag,%v %+v %#v %sof CLI structs before and after a call, key grep on stdout/stderr/errors.make lint0 issues;go test ./cmdok (457s).No filter flags (defaults only), so no ContractTerms seam is needed yet.
Closes #2156
🤖 Generated with Claude Code
Summary by CodeRabbit
archera-comparisoncommand for viewing an Archera commitment plan comparison. It makes no Archera request unless run and supports table or JSON output.ARCHERA_API_KEYand organization and plan IDs supplied by flags or environment variables.