Files
alkhttp/tasks/adapters/review-001-ref-cycle-guard.md
T
glm-5.3-flash 0a8d4d731f fix(adapters): bounded, cycle-safe $ref resolution (OAI-01, OAI-08)
resolve_refs_recursive recursed with no cycle detection and no depth
budget; a self-referential OpenAPI component stack-overflowed and
aborted the process (uncatchable, kills import()).

- add branch-scoped visited set on the JSON-pointer ref path: a ref
  re-entering its own expansion chain errors cleanly with
  AdapterError::SchemaParse naming the offending ref (OAI-01)
- add depth budget (MAX_REF_RESOLUTION_DEPTH = 64) bounding $ref hop
  chains and schema nesting height; over-deep specs error cleanly
  instead of exhausting the stack (OAI-01)
- clean loud error over depth-limited expansion: recursive schemas
  (trees, linked lists, cursor pagination) fail import rather than
  expand unboundedly
- shared refs to a common schema (diamond/repeated) still resolve —
  visited set is branch-scoped, not global
- OAI-08: replace the two guarded expects in from_value
  ("paths is object", "schemas is object") with if-let paths

Verification: cargo test 188 passed / 0 failed; clippy
--all-targets -D warnings clean; fmt --check clean
2026-08-29 07:53:01 +00:00

4.6 KiB
Raw Blame History

id, name, status, depends_on, scope, risk, impact, level, tags
id name status depends_on scope risk impact level tags
review-001-ref-cycle-guard Bounded, cycle-safe $ref resolution (OAI-01, OAI-08) completed
narrow high component implementation
adapters
review-001
from-openapi

Description

Review 001 finding OAI-01 — the single highest-severity finding in the review (borderline critical, empirically verified): resolve_refs_recursive (src/adapters/openapi_spec.rs:199-221) recurses with no cycle detection and no depth budget. A self-referential component ({"$ref":"#/components/schemas/Node"} inside Node — trees, linked lists, cursor pagination: common and valid OpenAPI) recurses until the stack is exhausted: thread has overflowed its stack; fatal runtime errorprocess abort. Not a catchable panic; import() kills the whole process — startup crash-loop, or remote DoS if specs are ever runtime-refreshed/peer-supplied.

Fix: depth budget + visited set keyed on the JSON-pointer path, returning a clean CallError/import error on cycles and over-deep specs. Do not try to preserve full recursive expansion of recursive schemas — the goal is a clean, loud error (or a depth-limited expansion where the adapter can safely represent it), not unbounded expansion.

Ride-along: OAI-08 — guarded expects in openapi_spec.rs:144,170 (the "no expect outside tests" convention; if let costs nothing).

Acceptance Criteria

  • Import of a self-referential spec returns an error — the process does not abort (the review's named acceptance gate: a test importing a self-referential spec would have caught the abort immediately)
  • Deeply-nested non-circular spec beyond the budget errors cleanly at import (test)
  • A non-recursive spec with $ref sharing (refs to a common schema) still imports identically — no false cycle positives (existing suite green)
  • OAI-08 expects replaced
  • cargo test and cargo clippy --all-targets -- -D warnings pass

References

  • docs/reviews/001-initial-implementation-review.md (Part E, OAI-01, OAI-08)

Notes

Agent fills during implementation. Do before anything deployment-facing. Deliberately split from the parameter/requestBody work (review-001-openapi-import-integrity) so the delicate cycle-detection change lands alone.

Implementation decisions:

  • Depth budget set to 64 (MAX_REF_RESOLUTION_DEPTH, pub(crate) const in openapi_spec.rs) — far above any legitimately expandable schema height, 2× serde_json's parse recursion limit. It bounds both $ref hop chains and plain schema nesting.
  • Clean loud error chosen over depth-limited expansion, per the task's stated preference: recursive schemas (trees, linked lists, cursor pagination) fail import with AdapterError::SchemaParse naming the offending ref, rather than expanding unboundedly or truncating silently.
  • The visited set is branch-scoped (insert before recursing, remove on return), so shared refs to one common schema — diamond or repeated — resolve normally; only a ref re-entering its own expansion chain is a cycle.
  • OAI-08: both guarded expects (paths is object, schemas is object) replaced with if let Some(..); the inner-body re-shape is behavior-preserving (both sites were immediately preceded by an is_object() check).
  • Tests added in src/adapters/openapi_spec.rs (mod tests): direct recursive + mutually-recursive error, over-deep non-circular error, two no-false-positive cases (shared common schema, diamond reuse), and an end-to-end FromOpenAPI::import of a self-referential spec asserting an error return (process does not abort).

Summary

Implemented bounded, cycle-safe $ref resolution in src/adapters/openapi_spec.rs (review 001 OAI-01 + OAI-08): resolve_refs_recursive now delegates to a resolve_refs_bounded worker carrying a branch-scoped HashSet of in-flight JSON-pointer refs and a depth counter against MAX_REF_RESOLUTION_DEPTH (64). A ref re-entering its own expansion chain errors with AdapterError::SchemaParse ("circular $ref detected …: "); a ref-chain or nesting height beyond the budget errors similarly ("exceeded depth budget of 64"). Import of a self-referential spec therefore returns a clean import error instead of aborting the process via stack overflow (OAI-01); OAI-08's two guarded expects in from_value became plain if let paths. Six tests added in openapi_spec.rs covering the cyclic, over-deep, and shared-ref matrices plus the named acceptance gate (end-to-end self-referential import errors, no abort). 188 lib tests green; full cargo test, cargo clippy --all-targets -- -D warnings, and cargo fmt --check pass.