Skip to content

Degrade OpenAPI action_id collisions instead of crashing the scan#226

Merged
pengfei-threemoonslab merged 1 commit into
mainfrom
claude/modest-robinson-7c065c
Jun 17, 2026
Merged

Degrade OpenAPI action_id collisions instead of crashing the scan#226
pengfei-threemoonslab merged 1 commit into
mainfrom
claude/modest-robinson-7c065c

Conversation

@pengfei-threemoonslab

Copy link
Copy Markdown
Contributor

Summary

  • A valid third-party OpenAPI spec (block/goose, found by the merged-PR miner) crashed agents-shipgate scan with exit 2 / Config error: Duplicate action_surface action_id. The OpenAPI action_id is derived from METHOD + normalized_path only — the operationId is dropped — so two distinct operations whose paths normalize identically (goose ships GET /sessions/{session_id} plus a trailing-slash variant, each with its own operationId) collapse to one action_id and hard-fail the duplicate-id guard.
  • Make this fail-soft, matching the symlink-loop (Add the merged-PR history miner + first real run (WS-P) #212) and MCP-as-tools (init: never write a tool source that scan's input adapters reject #214) precedent: a third-party spec must never crash a scan. Collisions now degrade to a source_warning (which routes the release to review_required) while keeping both operations as distinct actions.
  • build_action_surface_facts gains an optional warnings sink. When provided (the live scan path), _disambiguate_duplicate_action_ids deterministically resolves collisions: the first operation (by tool_name) keeps the bare id, the rest gain a #<operationId> suffix (ordinal fallback when operationIds also tie), identity_hash is refreshed, and one warning is recorded per collision. Without a sink the legacy ConfigError is preserved, so the redaction path's existing public-only ordinal disambiguator (surface_redaction.py) is untouched.
  • The warnings are threaded from Phase 5 (decision.py_ChecksDecisionsanitization.py) into report.source_warnings, appended last so output stays byte-stable for the common no-collision case (the list is empty there).

Type

  • Check or risk-model change
  • Input adapter change

Verification

CI is authoritative for python -m ruff check ., python -m compileall -q src tests, and python -m pytest.

Additional local checks run:

  • Full pytest suite passes (exit 0); tests/test_adapter_static_only.py static-only invariant holds; ruff check clean on all changed files.
  • New network-free regression tests: test_action_surface_disambiguates_openapi_action_id_collisions_fail_soft (unit) and test_openapi_action_id_collision_degrades_instead_of_crashing (full run_scan on the goose-shaped fixture). The existing test_action_surface_rejects_action_id_collisions (no sink → ConfigError) still passes.
  • End-to-end CLI on the fixture now returns exit 0 with the disambiguation source_warning and two distinct action_ids, instead of the previous exit 2 / Config error.

Release-readiness notes

  • No user-code import added to default scan paths
  • No network access added to default scan paths
  • New or changed check IDs are documented in docs/checks.md — n/a, no new check IDs
  • Report/schema changes are additive or documented in STABILITY.md — no schema change; source_warnings only gains an extra entry in the (previously crashing) collision case

🤖 Generated with Claude Code

@pengfei-threemoonslab pengfei-threemoonslab force-pushed the claude/modest-robinson-7c065c branch 2 times, most recently from 7a11f38 to 9160a28 Compare June 17, 2026 04:24
… scan

A valid third-party OpenAPI spec (block/goose) crashed `agents-shipgate
scan` with `exit 2 / Config error: Duplicate action_surface action_id`.
The OpenAPI action_id is derived from METHOD + normalized path only —
the operationId is dropped — so two distinct operations whose paths
normalize identically (goose ships `GET /sessions/{session_id}` plus a
trailing-slash variant, each with its own operationId) collapse to one
action_id and hard-fail the duplicate-id guard.

Make this fail-soft, matching the symlink-loop (#212) and MCP-as-tools
(#214) precedent: a third-party spec must never crash a scan.

- build_action_surface_facts takes an optional `warnings` sink. On the
  live scan path, collisions between purely INFERRED ids (e.g. OpenAPI
  method+path normalization) are disambiguated deterministically (first
  operation by tool_name keeps the bare id, the rest gain a
  `#<operationId>` suffix, ordinal fallback on ties), the identity_hash is
  refreshed, and one warning is recorded per collision.
- A collision that touches a manifest-authored action identity stays a
  hard ConfigError on both paths — a declared id is a contract, never
  silently rewritten. "Manifest-authored" means any action_surface
  declaration that sets an explicit action_id component: `id`, `provider`,
  or `operation` (all three feed build_action / _provider / _operation).
  Without a sink the legacy ConfigError is preserved for all collisions,
  so the redaction path's public-only ordinal disambiguator is untouched.
- Thread the warnings from Phase 5 into report.source_warnings (appended
  last, so byte-stable for the common no-collision case). A source_warning
  routes the release to review_required rather than crashing.
- Add network-free regression tests: inferred OpenAPI collisions degrade;
  explicit collisions via `id` and via `provider`/`operation` still raise
  on both the no-sink and warnings-sink paths; plus a full run_scan
  integration test on the goose-shaped fixture.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pengfei-threemoonslab pengfei-threemoonslab force-pushed the claude/modest-robinson-7c065c branch from 9160a28 to 08dfa76 Compare June 17, 2026 06:06
@pengfei-threemoonslab pengfei-threemoonslab merged commit a968a6e into main Jun 17, 2026
2 checks passed
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.

1 participant