Skip to content

feat(tools): add read-only Archera comparison tool - #55

Open
cristim wants to merge 4 commits into
mainfrom
feat/48-archera-comparison-tool
Open

cristim wants to merge 4 commits into
mainfrom
feat/48-archera-comparison-tool

Conversation

@cristim

@cristim cristim commented Oct 10, 2026 •

Copy link
Copy Markdown
Member

Closes #48. Tracking: LeanerCloud/cloud-commitments-platform#786.

Adds cudly_archera_comparison, a read-only tool that compares an Archera commitment plan (current offer and alternatives per line item, plus plan-wide hypotheticals) using pkg/insurance at the existing go pin (16b57954, which includes the go#331 key-leak fix and ContractTerms()). No go.mod change.

  • Off by default: ARCHERA_API_KEY, ARCHERA_ORG_ID, ARCHERA_PLAN_ID are read from the environment on each call (never tool arguments). Unset or blank returns an error naming the missing variables and makes zero requests.
  • Key handling: the *insurance.Client and key live in call locals only, never a struct field; production passes hc=nil (SSRF-hardened client). Fixed origin https://api.archera.ai, GET only, no retries, redirects not followed.
  • Errors: archera request failed: HTTP <status>; retry after <n>s (delta-seconds, capped 24h by the library) or retry after: not given. Vendor message is dropped.
  • Output mirrors platform internal/archera/dto.go field for field: exact decimal strings (no float, no division, no USD), unknown as null, product support only supported/unknown, both disclosures verbatim from pkg/common, vendor strings control-char stripped and capped at 256 bytes, result capped at 512 KiB. Enums and maxItems for contract_terms/payment_options/line_item_ids are in the input schema.
  • Read-only: no purchase effect, not gated by CUDLY_MCP_ENABLE_REAL_PURCHASES or spend caps, no audit record. README, CHANGELOG, server.json (key isSecret) and the MCPB manifest (key sensitive) are updated.

Tests (offline; RoundTripper asserting https://api.archera.ai, no live calls): a distinct-value fixture for every field with full-output golden comparison driven through the MCP protocol, filters, not-configured/partial config (zero requests), 429/401-echo/302 errors with the key absent from result and protocol bytes, vendor-string cleaning, size cap, schema bounds, %v/%+v/%#v of tool and client.

Mutation sweep (91 hand-built mutants over the mapping, config, error and schema code): the first run had 9 real survivors (schema bounds/enums, descriptor fields, UTC fetched_at); tests were added and a re-run of those mutants has 0 survivors. Not re-run in full after a field-reorder lint fix.

Open owner item (#47 follow-up): the mcpb privacy_policies URL is unchanged and no URL was invented; it still needs an owner decision because this tool sends data to api.archera.ai.

Local verification: make lint clean, go test -short ./... green (fixture/httptest-based; no real Archera call was made).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added an optional, read-only Archera plan comparison that reports current and hypothetical plan costs, offer details, and product-support information. Exact decimal amounts and unknown values are clearly represented, with disclosures included.
    • Configure the Archera API key, organization ID, and plan ID to enable the tool; otherwise, it returns an error without making a request.
  • Documentation
    • Documented the comparison tool, its configuration, and its read-only behavior.

cristim and others added 3 commits October 10, 2026 05:49
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…den output

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@cristim cristim added priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-sprint Within the current sprint triaged Item has been triaged impact/few Limited audience effort/m Days type/feat New capability labels Oct 10, 2026
@coderabbitai

coderabbitai Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Review in Change Stack →Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: LeanerCloud/cloud-commitments-mcp/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 1720fade-11c5-4620-ae73-c1feb226434e

📥 Commits

Reviewing files that changed from the base of the PR and between b384614 and b4cddba.


📒 Files selected for processing (11)
  • CHANGELOG.md
  • README.md
  • annotations_test.go
  • mcpb/manifest.json
  • server.go
  • server.json
  • tools/archera_comparison.go
  • tools/archera_comparison_test.go
  • tools/archera_dto.go
  • tools/testdata/archera_comparison_golden.json
  • tools/testdata/archera_comparison_wire.json

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.



📝 Walkthrough

Walkthrough

Adds a read-only Archera plan comparison tool. The server loads its API key and plan identifiers from environment settings, sends comparison requests through the insurance client, and returns mapped results with exact decimal money, disclosures, and bounded output.

Changes

Archera comparison

Layer / File(s) Summary
Comparison response mapping
tools/archera_dto.go, tools/archera_comparison_test.go, tools/testdata/archera_comparison_*
Defines response types and maps totals, offers, support details, and disclosures. Exact finite decimals are rendered as strings; unknown values remain null. Tests and fixtures cover mapping, string cleaning, and invalid monetary values.
Tool request and error handling
tools/archera_comparison.go, tools/archera_comparison_test.go
Defines the tool schema and handler. It validates filters, checks configuration on each call, sends the comparison request, and rejects results above 512 KiB. Errors are sanitized, and tests cover requests, configuration, credentials, and output limits.
Registration and configuration
server.go, server.json, mcpb/manifest.json, annotations_test.go, README.md, CHANGELOG.md
Registers the tool and adds its optional environment settings to server and platform configuration. Documentation describes the tool, and annotation tests assert its read-only and non-destructive properties.

Priority: ⬇️ Low

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant MCPCaller
  participant ArcheraComparisonTool
  participant InsuranceClient
  participant ArcheraAPI
  MCPCaller->>ArcheraComparisonTool: Call comparison with filters
  ArcheraComparisonTool->>InsuranceClient: Request plan comparison
  InsuranceClient->>ArcheraAPI: Send comparison request
  ArcheraAPI-->>InsuranceClient: Return comparison data
  InsuranceClient-->>ArcheraComparisonTool: Return comparison result
  ArcheraComparisonTool-->>MCPCaller: Return mapped comparison or error
Loading

Merge Risk: ⚪ Minimal · up to b4cdd

The comparison tool is ready to merge after normal checks; the reported transport-read concern does not require a change.

Pre-merge checks | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 26.19% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 42 functions across 5 files. (6 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the primary change: adding a read-only Archera comparison tool.
Linked Issues check Passed The PR satisfies the coding requirements in #48. cudly_archera_comparison is registered and discoverable with read-only, non-destructive annotations. It reads ARCHERA_API_KEY, ARCHERA_ORG_ID, an…
Out of Scope Changes check Passed The changes stay within #48. Source changes implement the Archera comparison tool and its DTO mapping. Tests validate the tool, security behavior, schema, and protocol path. Registration, manifests, d…

Full details: Docstring Coverage

Explanation

Docstring coverage is 26.19% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 42 functions across 5 files. (6 skipped: 6 unsupported.)


  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR


🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@cristim

cristim commented Oct 10, 2026

Copy link
Copy Markdown
Member Author

Gate review (claude-opus-5-5) finding 1 at fc0c045: BLOCKER, CI red.

CI - Build & Test run 38022010709: Unit Tests and Integration Tests FAIL with 28 WARNING: DATA RACE under -race (TestArcheraComparisonNotConfigured, RejectsBadArgsWithoutRequest, RejectsTooManyLineItemsWithoutRequest, VendorErrors/429, ResultSizeCap).

Cause: tools/archera_comparison_test.go:89-99 (callArchera) hands an unsynchronized bytes.Buffer to mcp.LoggingTransport and reads wire.String() at :99 while the server's jsonrpc2 goroutine is still writing to it (stack: go-sdk mcp/transport.go:315 -> bytes/buffer.go). Test-only race, not production, but required CI is red.

Suggested fix: wrap the buffer in a mutex-guarded io.Writer, and close cs then ss (and wait) before reading it, so the snapshot is complete and race-free. Rerun go test -race -count=20 ./tools/ locally.

@cristim

cristim commented Oct 10, 2026

Copy link
Copy Markdown
Member Author

Gate review (claude-opus-5-5) finding 2 at fc0c045: BLOCKER, vendor text reaches the model uncapped on the decode-error path.

tools/archera_comparison.go:177-180 (archeraError) passes every non-HTTPError through unchanged (only the key is masked). The pinned library's decoder echoes the rejected vendor value with %q (cloud-commitments-go pkg/insurance/quote.go:459,481,493,507,516, "unsupported value %q"), with no length cap. So the owner rule "vendor strings control-char stripped + 256-byte cap" holds on the success path but not on the error path.

Reproduced at this head with a scratch test (not pushed) that drives the real tool over MCP and sets data[0].current.payment_option in the wire fixture to "x\x1b[31m\u009b" + 20*"y" + " IGNORE ALL PREVIOUS INSTRUCTIONS". The tool error text is:

data[0]: current: payment_option: unsupported value "x\x1b[31m\u009byyyyyyyyyyyyyyyyyyyy IGNORE ALL PREVIOUS INSTRUCTIONS"

With a 100000-byte value the tool error is 100126-100154 bytes for each enum-validated field: data[].current/candidates[].contract_term, payment_option, offer.provider, hypothetical_totals[].contract_term, payment_option, line_items[].actual_term, actual_payment_option, actual_term_reason. %q escapes the control bytes, so there is no raw ESC/C1, but the vendor-controlled text (a prompt-injection channel) is unbounded up to the 10MB response cap, and the 512KB result cap does not apply to errors.

Success path is fine: the same injection (ESC, CSI, C1 U+009B/U+0085, U+200B, U+2028, >256 bytes) into every free-text field (offer_id, commitment_type, offer.region, guaranteed_display_name, lease_menu_item_id, row and hypothetical line_item_id, actual_commitment_type) comes out clean and at most 256 bytes.

Suggested fix: in archeraError, return the non-HTTP error text through archeraCleanString (or a fixed "archera response could not be decoded: " without the value). Add a regression test that sets one enum field to a >256-byte value carrying ESC + C1, and asserts the tool error is at most 256 bytes and has no vendor tail. The same pass-through probably exists in the platform and cli consumers. The root fix could be in go (cap the %q echo), but this PR has to bound its own output.

@cristim

cristim commented Oct 10, 2026

Copy link
Copy Markdown
Member Author

Gate review (claude-opus-5-5) finding 3 at fc0c045: mutation survivors. The author's "0 survivors" does not reproduce after the field reorder.

I ran a fresh sweep: 118 valid hand-written mutants over tools/archera_dto.go and tools/archera_comparison.go, each tested with go test ./tools/ -run 'Archera|Annotation|Descriptor'. Result: 96 killed, 22 survived.

Every survivor was checked against the pinned library's input validation.

Real gaps: vendor free text the library accepts as-is, so dropping the cleaning goes unnoticed. Today only offer_id and region are injected (archera_comparison_test.go:261-264):

  • [39] archeraCleanString(e.CommitmentType) -> raw
  • [42] archeraCleanString(li.LineItemID) -> raw (hypothetical line items)
  • [44] archeraCleanString(r.LineItemID) -> raw (rows)
  • [49] archeraCleanPtr(e.LeaseMenuItemID) -> raw
  • [50] archeraCleanPtr(e.GuaranteedDisplayName) -> raw (shown to people as the offer name)
  • [53] archeraCleanPtr(li.ActualCommitmentType) -> raw

I confirmed each field above is accepted by the decoder with ESC + U+009B + U+0085 + U+200B + U+2028 + >256 bytes. The production code cleans them correctly today (my injection probe: all CLEAN, at most 256 bytes); the tests just would not catch a regression. This is the same class of survivor the cli gate required fixing. Fix: inject the payload into every vendor string field of the wire fixture, and assert the cleaned value and the 256-byte cap per field.

Minor test gaps (real but low impact):

  • [84]/[85] Candidates/Hypotheticals [] -> nil: an empty list would render as JSON null, and no test covers an empty vendor list.
  • [66] RFC3339 -> RFC3339Nano survives because time.Parse(time.RFC3339, ...) accepts fractional seconds. The .Local() mutant [64] is killed.
  • [93] break -> continue in archeraCleanString: no test puts a multibyte rune at the boundary followed by ASCII.
  • [108] result cap > -> 2* limit: no boundary test.
  • [107] key masking in the archeraError pass-through: no test, though the library redacts first.

Equivalent (no action): [40] plan_id (env-sourced, UUID-checked); [41] [43] [45] [46] [48] [51] [52] enum fields that the library rejects unless they are an exact enum value. [86]/[87] unrepresentable-decimal propagation: JSON decimal literals always terminate, so this is unreachable from the wire. It still deserves one DTO-level test, because silently dropping the error would turn money into null.

Lint: make lint gives 0 issues. go test -short ./... passes locally (558 passed), but -race reproduces finding 1 (17 race reports in 3 runs).

@cristim

cristim commented Oct 10, 2026

Copy link
Copy Markdown
Member Author

Gate verdict (claude-opus-5-5): CHANGES REQUESTED at fc0c045. Not merged.

Blockers:

  1. Required CI is red: data race in the callArchera test helper (finding 1).
  2. Vendor values echoed uncapped (100KB+) in the tool's decode-error text (finding 2).
  3. Six real mutation survivors on vendor-string cleaning (finding 3).

Verified OK at this head:

  • The DTO matches platform internal/archera/dto.go (origin/main): JSON tags, note texts (basis_note says "one-time amounts"), mapper bodies and cleaning are identical; only the field order differs.
  • Both disclosures come verbatim from pkg/common.
  • The go pin 16b57954 has ContractTerms() and the key-leak fix: key_leak_test.go, Config.Format redaction, CheckRedirect = ErrUseLastResponse, 24h Retry-After cap.
  • Configuration: an unconfigured or partly configured call sends zero requests. The key is read from env only. hc is nil in production.
  • The 302 is not followed. The 401 echo of the key in the real {"message":...} shape is dropped.
  • fetched_at is UTC (the .Local() mutant is killed).
  • server.json marks the key isSecret; mcpb/manifest.json marks it sensitive. The PR body flags privacy_policies as owner item fix(mcp): no spend ceiling on purchase tools and the preview never shows a price #47.
  • Labels match feat(insurance): read-only Archera comparison MCP tool using pkg/insurance #48. Branch is up to date with main.

Local evidence (fresh git archive, GOTOOLCHAIN=go1.26.9):

  • go test -short ./...: 558 passed.
  • make lint: 0 issues.
  • go test -race -run Archera -count=3 ./tools/: 17 DATA RACE reports.
  • Mutation sweep: 118 valid mutants, 96 killed, 22 survived (6 real, 8 equivalent, 8 minor).

Any new commit restarts the gate.

…race

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/few Limited audience priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/feat New capability urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(insurance): read-only Archera comparison MCP tool using pkg/insurance

1 participant