docs(tasks): decompose review-002 into 24 tasks (23 implementation + 1 bracketed follow-up)
Decomposition of docs/reviews/002-post-remediation-review.md per its 5-unit remediation plan: - Unit 1 (security-critical): gw15-publish-body-cap, prj16-schema-via-call (CF-004 filed alkcall-side), fwd13-dot-segments, fwd16-missing-capability, oai11-ref-memoization - Unit 2 (timeout/terminality): ws13-idle-progress, fwd15-stream-timeout, cli01-retry-after-budget, con17-mcp-pagination, con18-wss-sweep-exit - Unit 3 (projection/docs): projection-truthfulness, mcp-batch-cap, gw16-status-drift - Unit 4 (spec-import): yaml-normalization, oai13-path-item-wildcards, import-loudness-cluster, js01-placeholder-check, fwd17-19-contract-decisions - Unit 5 (WS polish + tests): con18b-ws-polish, client-policy-wire-tests, cov-deployment-knobs, cov13-dead-code, srv11-srv12-router-ordering - review-002-bracketed-followup: tentatively planned post-bulk pass (stale-check, OQA-18 enforcement decision, CON-08/09 close() lever, cross-crate re-checks) — deliberately not serialized against the bulk Also: review-002 numbering repair (CON-14 was double-booked; MCP pagination now CON-14, from_wss monitor renumbered CON-18, missing CON-14 section added). taskgraph: 66 valid, no cycles; 24 pending (all review-002); gen-1/gen-2 parallel waves identified; workflow-cost hotspots are prj16 (12.8) and ws13 (11.1), both carrying the reviewed slicing guidance in their Notes.
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
---
|
||||
id: review-002-con17-mcp-pagination
|
||||
name: Bound MCP tools/list pagination (max pages + deadline) (CON-14)
|
||||
status: pending
|
||||
depends_on: []
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
tags: [adapters, review-002, mcp]
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Review 002 CON-14 [major]. `from_mcp/mod.rs:114-119` delegates to
|
||||
rmcp 1.8.0's `Peer::list_all_tools`, which loops `while
|
||||
cursor.is_some()` with **no page cap and no timeout** (verified against
|
||||
rmcp source `service/client.rs:390-407`). A malicious or buggy server
|
||||
that always returns `next_cursor: Some(...)` (cycling or ignoring the
|
||||
cursor) hangs `import()` indefinitely while appending every page's
|
||||
tools → unbounded memory growth alongside the hang. Tools from
|
||||
completed pages are not lost (extend per page), but nothing in alkhttp
|
||||
bounds the loop. The paging test that exists (from_mcp_integration.rs)
|
||||
terminates after 3 pages — the cycling shape is untested.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Replace `list_all_tools` with a bounded loop over `list_tools`:
|
||||
hard page-count cap + overall deadline (constants or config;
|
||||
pick generous defaults — e.g. 100 pages / 60 s — and note them
|
||||
in the module doc)
|
||||
- [ ] Budget trips → `AdapterError::DiscoveryFailed`-family error
|
||||
naming the budget (pages seen, tools accumulated), no partial
|
||||
import left behind (the existing import-fails-closed behavior
|
||||
holds)
|
||||
- [ ] Integration test: a paging server that cycles its cursor
|
||||
(always `next_cursor: Some("a")`) → import terminates with the
|
||||
clean error, bounded time (no hang)
|
||||
- [ ] Existing 3-page pagination test unchanged and passing
|
||||
(well-formed pagination still imports all pages)
|
||||
- [ ] `cargo test --features mcp`, `cargo clippy --all-features --all-targets -- -D warnings`,
|
||||
`cargo fmt --check` pass
|
||||
|
||||
## References
|
||||
|
||||
- docs/reviews/002-post-remediation-review.md (Part F', CON-14)
|
||||
- src/adapters/from_mcp/mod.rs:114-119 (the delegation), rmcp 1.8.0 src/service/client.rs:390-407 (the unbounded loop)
|
||||
- tests/from_mcp_integration.rs:302-317 (the existing paging server)
|
||||
- tasks/adapters/review-001-consumer-adapter-hygiene.md (CON-01's pagination work this bounds)
|
||||
|
||||
## Notes
|
||||
|
||||
Small, self-contained, feature-gated (`mcp`). Consider (optional) a
|
||||
`max_tools` sanity cap as well — same budget family, one more guard
|
||||
against a hostile server — but do not let scope creep: the page cap +
|
||||
deadline closes the hang, which is the finding.
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
id: review-002-con18-wss-sweep-exit
|
||||
name: Dead-connection fast-fail + bounded from_wss sweep exit (CON-18)
|
||||
status: pending
|
||||
depends_on: []
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
tags: [adapters, review-002, from-wss]
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Review 002 CON-18 [minor]. The review-001 CON-02 fix added a 1 s
|
||||
pending-map sweep to `from_wss`'s drop monitor — but it never exits:
|
||||
once `eof_observed` holds, the sweep branch runs `fail_all` every
|
||||
second **forever** (`from_wss.rs:254-272`). Each fire-and-forget
|
||||
`import()` whose peer dies leaves a spawned task + pending map + watch
|
||||
receiver alive for the process lifetime (a true task/allocator leak
|
||||
scaling with import count). Note the naive fix is wrong: the sweep
|
||||
test proves post-EOF registrations race `fail_all` and hang without
|
||||
the sweep — so exit requires making post-EOF registration fail fast
|
||||
*first*.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Post-EOF (dead connection) registration fails fast: calls
|
||||
registered after the observed EOF resolve immediately with
|
||||
`CONNECTION_CLOSED` (retryable, matching the established
|
||||
mapping) instead of relying on the next sweep tick
|
||||
- [ ] The sweep then exits after a bounded grace period following EOF
|
||||
(long enough that the fast-fail path is exercised; not forever)
|
||||
— no per-dead-session permanent task remains
|
||||
- [ ] Re-verify the existing race tests still pass (pre-drop,
|
||||
forgotten/held drop-during-registration, post-EOF sweep —
|
||||
from_wss.rs:809-1001) and tighten assertions where the fast-fail
|
||||
makes timing deterministic
|
||||
- [ ] A task-lifecycle test (or task-count assertion) proving the
|
||||
monitor ends after teardown
|
||||
- [ ] `cargo test --features wss`, `cargo clippy --features wss --all-targets -- -D warnings`,
|
||||
`cargo fmt --check` pass
|
||||
|
||||
## References
|
||||
|
||||
- docs/reviews/002-post-remediation-review.md (Part B', CON-18 / CON-16-withdrawn merge note)
|
||||
- src/adapters/from_wss.rs:244-273 (the monitor loop), :75 (interval), :305 (fire-and-forget import)
|
||||
- tasks/adapters/review-001-ws-eof-signal.md (the WS-02/CON-02 fix this bounds)
|
||||
- alkcall/docs/reviews/consumer-findings-ledger.md CF-001 (the retryability classification — keep consistent)
|
||||
|
||||
## Notes
|
||||
|
||||
Implementation shape suggestion: an `AtomicBool`/watch-dead flag on
|
||||
the call connection consulted in the registration path (fail-fast)
|
||||
plus a sweep-generation counter (exit after N idle sweeps post-EOF).
|
||||
The alkcall-side CF-001 note matters: if/when the dead-mux write
|
||||
mapping changes, the fail-fast class here should match. Keep the
|
||||
WS-02 losslessness invariant intact — fast-fail is an *additional*
|
||||
resolution path, not a replacement for the watch signal.
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
id: review-002-fwd13-dot-segments
|
||||
name: Reject lone dot-dot path values that Url-set_path normalizes away (FWD-13)
|
||||
status: pending
|
||||
depends_on: []
|
||||
scope: narrow
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
tags: [adapters, review-002, security, from-openapi]
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Review 002 FWD-13 [major, security]. Lone `.`/`..` path-parameter
|
||||
values survive the PATH_VALUE_ENCODE_SET (which has no `.`) and are
|
||||
**silently normalized away by `Url::set_path`** — empirically
|
||||
reproduced against the locked `url 2.5.8`:
|
||||
|
||||
- `set_path("/tenants/../admin")` → `/admin`
|
||||
- `set_path("/files/..")` → `/`
|
||||
- `set_path("/repos/%2e%2e/x")` → `/x` (so adding `%2e` to the encode
|
||||
set would NOT fix it — the parser normalizes every case-insensitive
|
||||
spelling)
|
||||
|
||||
Scenario: template `/tenants/{tenant}/resources` with peer input
|
||||
`tenant = ".."` → the upstream receives `/resources` — a *different*
|
||||
(east commonly less-scoped, list-everything) endpoint than the template
|
||||
describes, **with the namespace's injected credentials attached**. The
|
||||
existing dot-segment test (`traversal_value_cannot_escape_template_path`,
|
||||
forward.rs:1155) only covers the multi-segment `../../admin` form the
|
||||
encoder *does* catch. The origin check in `assemble_request_url`
|
||||
(:520-522) cannot catch this — the origin never changes.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Rendered path **values** that are exactly `.` or `..`
|
||||
(case-insensitive, including `%2e` spellings before encoding) are
|
||||
rejected with a loud `INVALID_INPUT`-family CallError — the
|
||||
cleanest point is `value_to_path_segment` / `render_path_template`
|
||||
- [ ] A post-`set_path` invariant assert (decoded path segments =
|
||||
base_dir + rendered segments, byte-identical) protects against
|
||||
future normalizer surprises (implementer's choice: debug_assert
|
||||
+ loud runtime check, or a property test with a dot/percent/binary
|
||||
corpus)
|
||||
- [ ] Tests: lone `..` value → error; lone `.` value → error; `%2e%2e`
|
||||
spelling → error; a *legitimate* segment containing a dot
|
||||
(`v1.2.3`, `.hidden-file` as a value) still works (the rejection
|
||||
is exact-match, not substring)
|
||||
- [ ] Existing traversal/encoding tests unchanged and passing
|
||||
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
|
||||
`cargo fmt --check` pass
|
||||
|
||||
## References
|
||||
|
||||
- docs/reviews/002-post-remediation-review.md (Part D', FWD-13)
|
||||
- src/adapters/forward.rs:340-352 (encode set lacking `.`), :413-448 (renderer), :481-531 (set_path + origin check), :1155 (the test family)
|
||||
- tasks/adapters/review-001-forward-url-safety.md (the FWD-01 fix this completes — one case short)
|
||||
|
||||
## Notes
|
||||
|
||||
The error message must not echo the raw value's neighbor inputs (no
|
||||
spec/content leakage beyond the parameter name — follow the existing
|
||||
message style in `render_path_template`'s unbound-placeholder error).
|
||||
FWD-18 (object/array placeholder values double-routing to query) is
|
||||
tracked separately in review-002-fwd17-19-contract-decisions — do not
|
||||
fold it here, the mechanisms are adjacent but the decisions differ.
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
id: review-002-fwd16-missing-capability
|
||||
name: Loud error on missing capability instead of silent unauthenticated request (FWD-16)
|
||||
status: pending
|
||||
depends_on: []
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
tags: [adapters, review-002, security, from-openapi]
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Review 002 FWD-16 [major]. The FWD-08 remediation made every
|
||||
*malformed* credential fail loudly, but the **missing-capability arm
|
||||
stays silent**: `forward.rs:200-238` —
|
||||
|
||||
```rust
|
||||
if let Some(scheme) = auth_scheme {
|
||||
if let Some(secret) = context.capabilities.get(namespace) {
|
||||
// ... four loud arms for malformed names/values
|
||||
}
|
||||
// capability == None → falls through: the request is sent
|
||||
// with NO credential and NO diagnostic
|
||||
}
|
||||
```
|
||||
|
||||
`Capabilities::get` keys on `api_key:`/`http_token:` prefixes
|
||||
(alkcall `core/types.rs:97-107`), so an assembly layer that registered
|
||||
the secret under a wrong key form (or forgot) produces corrupted
|
||||
upstream 401s at call time with zero local diagnostics. This is the
|
||||
*most probable* misconfiguration shape and the one inconsistent arm in
|
||||
an otherwise-loud match — every other arm was made loud by review-001's
|
||||
FWD-08 fix (tested at forward.rs:1646-1736).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] When `auth_scheme.is_some()` and the capability is absent, the
|
||||
request is NOT sent; the caller gets an `INTERNAL`-family error
|
||||
naming the missing capability key (e.g. "capability
|
||||
`http_token:{namespace}` is absent; refusing to send the request
|
||||
unauthenticated") — message contains no secret material
|
||||
- [ ] When `auth_scheme.is_none()` the behavior is unchanged
|
||||
(unauthenticated ops stay unauthenticated)
|
||||
- [ ] Wire test: authed op, empty capabilities → error envelope (and
|
||||
the test asserts via the existing `spawn_responder`-style seam
|
||||
that the upstream received zero requests)
|
||||
- [ ] Existing FWD-08 loud-arm tests unchanged and passing
|
||||
- [ ] Doc note in the module doc: the loud-missing matrix now covers
|
||||
malformed name/value AND absent capability
|
||||
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
|
||||
`cargo fmt --check` pass
|
||||
|
||||
## References
|
||||
|
||||
- docs/reviews/002-post-remediation-review.md (Part D', FWD-16)
|
||||
- src/adapters/forward.rs:200-238 (the silent arm), :1646-1736 (the FWD-08 test family to mirror)
|
||||
- docs/architecture/decisions/014-secret-material-flow-and-capability-injection.md
|
||||
- tasks/adapters/review-001-forward-url-safety.md (FWD-08's loud-error work this completes)
|
||||
|
||||
## Notes
|
||||
|
||||
Error class choice: `INTERNAL` (misconfiguration, not caller fault) —
|
||||
matches the FWD-08 precedent. Non-retryable, since re-sending cannot
|
||||
succeed without the assembly layer changing. If a deployment legitimately
|
||||
runs authed-ops with *optionally*-present credentials (none known
|
||||
today), that would be a new config flag — do not preemptively add one;
|
||||
the invariant (ADR-014: credentials flow only via Capabilities) argues
|
||||
loud-and-closed.
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
id: review-002-fwd17-19-contract-decisions
|
||||
name: Forwarding contract decisions — non-JSON SSE payloads, double-routed placeholders, percent preservation (FWD-17/18/19 decide+document or fix)
|
||||
status: pending
|
||||
depends_on: [review-002-fwd15-stream-timeout]
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
tags: [adapters, review-002, from-openapi]
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Three review-002 forwarding findings that are design decisions more
|
||||
than bugs — decide, document, and fix where the decision says so:
|
||||
|
||||
- **FWD-17**: non-JSON SSE payloads silently degrade to JSON strings
|
||||
(`forward.rs:774-781`, `unwrap_or(Value::String)`) and the parser's
|
||||
`event:` field is discarded (`:977-979`). A consumer cannot extract
|
||||
fields from a legitimately non-JSON stream, and an upstream's
|
||||
SSE `event: error` convention is indistinguishable from data.
|
||||
Decide: (a) document the JSON-or-string contract as-is, or (b) carry
|
||||
the raw payload + the `event:` field name in the envelope for
|
||||
non-JSON payloads (e.g. `{data, event}` object wrapper).
|
||||
- **FWD-18**: non-scalar path-placeholder values double-route —
|
||||
rendered into the path (as JSON text) *and* appended as a query
|
||||
param (`forward.rs:144-157`). Fix the routing rule: a key that
|
||||
matched a placeholder never also emits as query, regardless of value
|
||||
shape (plus a spec-decision on whether object/array path values are
|
||||
an error instead).
|
||||
- **FWD-19 [info]**: preserved literal `%` in template/base text is
|
||||
upstream-semantics-changing (`%2F` survives; most stacks route it
|
||||
differently from `/`). Inputs are assembly-supplied (ADR-066 trust);
|
||||
document the trade-off in the module doc (+ ADR-066 note) — or
|
||||
reject raw `%` in *template text* while allowing it in values
|
||||
(the strict shape).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] A decision per finding, recorded (module doc + ADR-066 section
|
||||
as appropriate); doc-only outcomes still need the doc change +
|
||||
a doc-asserting test where applicable
|
||||
- [ ] FWD-17: whichever shape is chosen, non-JSON payload behavior is
|
||||
pinned by a test (string case, number-vs-string distinction,
|
||||
event-field presence if option (b))
|
||||
- [ ] FWD-18: placeholder keys never double-emit (query test with an
|
||||
object value under a placeholder key) — and the object-in-path
|
||||
outcome (error vs JSON-segment) decided and tested
|
||||
- [ ] FWD-19: documented (and if the reject-raw-% shape is chosen, its
|
||||
import- or call-time error tested)
|
||||
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
|
||||
`cargo fmt --check` pass
|
||||
|
||||
## References
|
||||
|
||||
- docs/reviews/002-post-remediation-review.md (Part D', FWD-17/18/19)
|
||||
- src/adapters/forward.rs:774-781, :977-979, :144-157, :353-370, :481-505
|
||||
- docs/architecture/decisions/066-from-jsonschema-as-http-adapter.md
|
||||
- tasks/adapters/review-002-fwd13-dot-segments.md (adjacent template-renderer work — sequence or coordinate to avoid churn in the same helpers)
|
||||
|
||||
## Notes
|
||||
|
||||
Deliberately a decide-first task: none of the three is a crash or
|
||||
leak; each is a contract the code half-implies. Keep the decisions
|
||||
small and documented rather than building speculative machinery (e.g.
|
||||
no new envelope schema unless (b) is actually chosen). Coordinate with
|
||||
review-002-fwd13-dot-segments (same helpers, different concerns).
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
id: review-002-import-loudness-cluster
|
||||
name: Import loudness cluster — oneOf body, self-ref requestBody, siblings, collisions, header precedence, error truncation (OAI-10/14/15/17/19, JS-02/03)
|
||||
status: pending
|
||||
depends_on: [review-002-oai11-ref-memoization]
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
tags: [adapters, review-002, from-openapi, from-jsonschema]
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Six review-002 minor findings across the two import files, all in the
|
||||
"unsupported-feature loudness + import robustness" family (the OAI-06
|
||||
matrix completion). Batched — each is small:
|
||||
|
||||
- **OAI-10**: `$ref` sibling keys are silently discarded
|
||||
(openapi_spec.rs:396-410). Under 3.1 semantics siblings apply
|
||||
alongside the `$ref` (advertise/enforce drift); there is no
|
||||
`openapi: 3.1` version gate anywhere. Fix: warn on `$ref` siblings,
|
||||
and/or gate `openapi: "3.1*"` like other unsupported features
|
||||
(at minimum document the 3.0-only stance).
|
||||
- **OAI-14**: top-level `oneOf` requestBodies (unconstraining `body`
|
||||
contract — import-time gate), `discriminator`, `xml`, `callbacks`,
|
||||
`security` blocks silently ignored. Extend the OAI-06 loudness
|
||||
matrix: reject-or-warn at import with the established message
|
||||
template.
|
||||
- **OAI-15**: a self-`$ref`'d requestBody resolves to content-less →
|
||||
body-less op registered silently (openapi_spec.rs:486-498). Fail
|
||||
import with the OAI-04 error shape when a resolved requestBody
|
||||
still contains a top-level `$ref` or lacks `content`.
|
||||
- **OAI-17**: import errors echo unbounded spec-derived strings
|
||||
(openapi_spec.rs:264-272 servers locations join; from_openapi.rs
|
||||
placeholder/path interpolation). Truncate (first N + count) and cap
|
||||
interpolated path/ref strings in `SchemaParse` messages.
|
||||
- **OAI-19**: declared `in: header` params silently lose to
|
||||
`default_headers` and credential headers (forward.rs:169-238 insert
|
||||
order). Reject or warn at import when a header param collides with
|
||||
a configured `default_headers` key or the auth scheme's header;
|
||||
also reject peer-visible header params named `Authorization` on
|
||||
authed namespaces.
|
||||
- **JS-02**: `from_openapi` templates are not validated for balanced
|
||||
braces at import (`/x{open` imports, fails per-call INTERNAL) —
|
||||
reuse/hoist `from_jsonschema`'s `validate_path_template` into the
|
||||
shared path and run it in `build_registration`.
|
||||
- **JS-03**: `assert_eq!` in library code (`reject_collisions`,
|
||||
from_openapi.rs:74-75) — return `Err(AdapterError::internal(…))` or
|
||||
zip-iterate and drop the asserts.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Each item landed with its import-time test (loud-error content
|
||||
asserted; sibling-warning case; header-collision warning/reject;
|
||||
unterminated-template import failure; no asserts in non-test code)
|
||||
- [ ] OAI-17: a 100k-path servers-list import error message is
|
||||
bounded (test with a large synthetic spec asserting message
|
||||
length < a sane cap)
|
||||
- [ ] The OAI-06 matrix (or its successor doc section) lists the newly
|
||||
loud features
|
||||
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
|
||||
`cargo fmt --check` pass
|
||||
|
||||
## References
|
||||
|
||||
- docs/reviews/002-post-remediation-review.md (Part E', OAI-10/14/15/17/19, JS-02, JS-03)
|
||||
- src/adapters/openapi_spec.rs:396-410, :486-498, :241-273, :314-320, :264-272, :539-573
|
||||
- src/adapters/from_openapi.rs:69-89 (JS-03), :162-171 (the "body" collision pattern to extend for OAI-19), from_jsonschema.rs (the template validator to hoist)
|
||||
- tasks/adapters/review-001-openapi-loud-degradation.md (the OAI-06 matrix), tasks/adapters/review-001-openapi-import-integrity.md
|
||||
|
||||
## Notes
|
||||
|
||||
Sequence after review-002-oai11-ref-memoization (same resolver
|
||||
function for OAI-10/15 — or rebase-verify). Slice per finding, commit
|
||||
per finding. The OAI-19 import-time check may not see the assembly
|
||||
layer's `default_headers` (config lives elsewhere) — if the
|
||||
import-time surface cannot know, the loud point moves to
|
||||
first-call-time (warn-once) — implementer documents which.
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
id: review-002-js01-placeholder-check
|
||||
name: Fix placeholder check skipped when input_schema has no properties (JS-01)
|
||||
status: pending
|
||||
depends_on: []
|
||||
scope: single
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
tags: [adapters, review-002, from-jsonschema]
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Review 002 JS-01 [major]. `from_jsonschema`'s eager placeholder-binding
|
||||
check is skipped when `input_schema` has no `properties` key:
|
||||
`spec_name_references_undeclared` (`from_jsonschema.rs:166-188`) hits
|
||||
`if properties.is_empty() { return false; }` at :173-175. So:
|
||||
|
||||
```rust
|
||||
FromJsonSchema::new(spec_with_input_schema_type_object_no_properties,
|
||||
path_template = "/widgets/{id}")
|
||||
```
|
||||
|
||||
**passes construction** — then every call fails (`{id}` unbound →
|
||||
INTERNAL at forward.rs:441-446; a peer-supplied `id` key →
|
||||
INVALID_INPUT). The from_openapi equivalent (`unbound_placeholders`,
|
||||
from_openapi.rs:36-57) handles the empty case correctly: with no
|
||||
declared properties, any placeholder is unbound. The existing test
|
||||
(from_jsonschema.rs:699-724) covers only the properties-present case.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Drop the `is_empty` early return (or return `true` for
|
||||
placeholders when properties are empty — same semantics as the
|
||||
from_openapi path)
|
||||
- [ ] Test: `FromJsonSchema::new` with a no-properties schema +
|
||||
`{id}` template fails at construction with the placeholder
|
||||
error; a no-properties schema with a placeholder-free template
|
||||
still constructs fine
|
||||
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
|
||||
`cargo fmt --check` pass
|
||||
|
||||
## References
|
||||
|
||||
- docs/reviews/002-post-remediation-review.md (Part E', cluster item JS-01; Test-gap 11)
|
||||
- src/adapters/from_jsonschema.rs:166-188 (the early return), :77-93 (the construction-time validation), :699-724 (the present-props test)
|
||||
- src/adapters/from_openapi.rs:36-57 (the correct reference implementation)
|
||||
- docs/architecture/decisions/066-from-jsonschema-as-http-adapter.md
|
||||
|
||||
## Notes
|
||||
|
||||
One-line fix + one test. Explicitly approved to fold into any commit
|
||||
that is already touching from_jsonschema.rs (e.g. the wire-tests task)
|
||||
if a session prefers — but if touched standalone, keep it its own
|
||||
commit per the small-units convention.
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
id: review-002-mcp-batch-cap
|
||||
name: Cap MCP batch tool at MAX_BATCH_OPERATIONS (PRJ-22)
|
||||
status: pending
|
||||
depends_on: []
|
||||
scope: single
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
tags: [adapters, review-002, mcp]
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Review 002 PRJ-22 [minor]. The MCP `batch` tool has no cap on the
|
||||
`calls` array (`to_mcp.rs:215-247` executes serially) while HTTP
|
||||
`/batch` rejects at 101 (`routes.rs:173-184`,
|
||||
`MAX_BATCH_OPERATIONS`). A 10,000-entry MCP batch occupies the
|
||||
dispatch spine serially (each invoke up to the 30 s deadline) — the
|
||||
doc (PRJ-09's fix) advertises no limit either.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] The `batch` tool enforces the same `MAX_BATCH_OPERATIONS` cap
|
||||
(share the constant; do not duplicate the literal) — over-cap →
|
||||
structured `INVALID_INPUT`-family error consistent with the
|
||||
tool's other argument errors (including `retryable`, per PRJ-13)
|
||||
- [ ] Tool description states the limit (mirroring how `/batch` docs
|
||||
it) — ADR-041 shape unchanged (4 fixed tools)
|
||||
- [ ] Test: over-cap `calls` array → structured error, no dispatch
|
||||
happened (spy/gateway-count assert via the existing invoke_tool
|
||||
harness)
|
||||
- [ ] `cargo test --features mcp`, `cargo clippy --all-features
|
||||
--all-targets -- -D warnings`, `cargo fmt --check` pass
|
||||
|
||||
## References
|
||||
|
||||
- docs/reviews/002-post-remediation-review.md (Part F', PRJ-22)
|
||||
- src/adapters/to_mcp.rs:215-247 (the loop), src/gateway/routes.rs:42,173-184 (the cap to share)
|
||||
- tasks/adapters/review-001-mcp-tool-fidelity.md (the PRJ-07..10/13 work whose conventions to follow)
|
||||
|
||||
## Notes
|
||||
|
||||
One of the smallest tasks in the batch — a good first warm-up slice or
|
||||
fold-in candidate if a session runs short (but keep the commit
|
||||
separate; it is its own unit of work).
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
id: review-002-oai11-ref-memoization
|
||||
name: Memoize $ref expansion to bound resolver work (OAI-11)
|
||||
status: pending
|
||||
depends_on: []
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
tags: [adapters, review-002, from-openapi, openapi-spec]
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Review 002 OAI-11 [major, borderline critical]. The OAI-01 fix
|
||||
(cycle guard + depth budget, `openapi_spec.rs:382-437`) bounds *stack*,
|
||||
not *work*: every `$ref` hop re-expands its target from a fresh clone
|
||||
with no memoization. A chain of shared but acyclic refs —
|
||||
|
||||
```
|
||||
S_i = { a: { $ref: "S_{i+1}" }, b: { $ref: "S_{i+1}" } }
|
||||
```
|
||||
|
||||
(the doubly-linked-list `prev`/`next` shape — valid, common) expands as
|
||||
a full binary tree. Empirically (verbatim harness): 20 levels → 7.3M
|
||||
node visits / 3.6 s; growth ≈ 2^levels → 30 levels ≈ ~1 hour, 40 ≈ days.
|
||||
Neither guard fires (`err=false`); the import just wedges — no stack
|
||||
overflow, no error. The same "valid document kills the process" class
|
||||
as OAI-01, one mitigation shy.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `resolve_refs_bounded` memoizes resolved, cycle-free `$ref`
|
||||
targets per document (cache keyed on the ref string; clone the
|
||||
resolved `Value` on hit instead of re-resolving) — or an
|
||||
equivalent total-node-count budget with a clean
|
||||
`SchemaParse`-family error (implementer's choice; memoization
|
||||
strongly preferred — it preserves full acyclic support)
|
||||
- [ ] The cycle guard and both depth checks remain (memoization must
|
||||
not alter cycle detection semantics — a cycle still errors)
|
||||
- [ ] Acceptance gate: a 30+ level shared-chain spec imports in
|
||||
bounded time (add the test with a wall-clock or node-count
|
||||
assert; a 30-level chain must import in < 1 s)
|
||||
- [ ] Tests: diamond (non-cyclic shared refs) unchanged-correct;
|
||||
cycle-through-shared-node still errors; memoized result equals
|
||||
the old expansion for a shared-ref schema (golden compare on one
|
||||
case)
|
||||
- [ ] The four missing cycle-shape tests from OAI-16 (array-items,
|
||||
allOf, additionalProperties, `$ref`-sibling) land here too —
|
||||
same function, same risk envelope
|
||||
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
|
||||
`cargo fmt --check` pass
|
||||
|
||||
## References
|
||||
|
||||
- docs/reviews/002-post-remediation-review.md (Part E', OAI-11 + OAI-16)
|
||||
- src/adapters/openapi_spec.rs:382-437 (the resolver), :713-805 (existing cycle tests), from_openapi.rs:736 (the requestBody-chain test)
|
||||
- tasks/adapters/review-001-ref-cycle-guard.md (the OAI-01 fix this completes)
|
||||
- docs/architecture/decisions/066-from-jsonschema-as-http-adapter.md
|
||||
|
||||
## Notes
|
||||
|
||||
Clone cost per hit is fine (schemas are small; the win is avoiding
|
||||
exponential re-resolution, not allocation). If memoizing, note the
|
||||
`resolving`-set interaction: a memo entry must only be written for
|
||||
*completed* (cycle-free) expansions — a partial expansion under an
|
||||
active cycle attempt must never be cached (write-on-success at the
|
||||
top of the recursion return path, or post-order). This is the one
|
||||
subtle part; the existing cycle tests + the four new shape tests are
|
||||
the safety net.
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
id: review-002-oai13-path-item-wildcards
|
||||
name: Path-item parameters, 2XX/4XX/5XX wildcards, webhooks handling (OAI-13)
|
||||
status: pending
|
||||
depends_on: [review-002-oai11-ref-memoization]
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
tags: [adapters, review-002, from-openapi, openapi-spec]
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Review 002 OAI-13 [major]. Three common real-world OpenAPI shapes are
|
||||
still silently mishandled (verified at `openapi_spec.rs:276-336` +
|
||||
`from_openapi.rs:136-145`):
|
||||
|
||||
1. **Path-item-level `parameters`** (shared params declared next to
|
||||
the path key — extremely common in real specs) never merge into
|
||||
operations → `{id}` unbound → the whole import fails with a
|
||||
*misleading* diagnosis (the skip filter at :314-320 explicitly
|
||||
whitelists `"parameters"` out of the unsupported-methods warning,
|
||||
so nothing names the cause).
|
||||
2. **Response wildcard keys**: `"2XX"` is missed by the SSE-detection
|
||||
success sweep (an SSE stream under `2XX` imports as a giant-text
|
||||
Mutation — the exact OAI-06 misbehavior, one spelling away);
|
||||
`"4XX"/"5XX"` error keys drop silently with the generic warn (the
|
||||
test at from_openapi.rs:840 enshrines silent `5XX` dropping).
|
||||
3. **Top-level `webhooks`** silently vanish from mixed documents.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Path-item `parameters` merge into each operation under the path
|
||||
(operation-level entries override per OpenAPI spec semantics);
|
||||
the import no longer fails with "unbound placeholder" for
|
||||
shared-param specs
|
||||
- [ ] Success sweep accepts `"2XX"` (SSE detection + output schema)
|
||||
and error sweep accepts `"4XX"/"5XX"` (`HTTP_`-class mapping per
|
||||
the wildcard's implied status range — choose the mapping, e.g.
|
||||
4XX → the strongest available code, and document it)
|
||||
- [ ] `webhooks`: import (as Mutations/Webhook-kind ops or a decided
|
||||
visibility) or reject the key loudly — pick per ADR-066's
|
||||
single-endpoint adapter philosophy and record the decision
|
||||
- [ ] The misleading-diagnosis fix: when import fails after skipping
|
||||
path-item parameters, the error names the actual cause (the
|
||||
skip-filter whitelist must not hide the parameter key from the
|
||||
unsupported-features warning)
|
||||
- [ ] Tests: shared-path-params spec imports correctly (incl.
|
||||
operation-override precedence); `2XX`-declared SSE imports as
|
||||
Sub; `5XX`-declared error lands in error schemas; webhooks-only
|
||||
and mixed docs behave per the decision
|
||||
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
|
||||
`cargo fmt --check` pass
|
||||
|
||||
## References
|
||||
|
||||
- docs/reviews/002-post-remediation-review.md (Part E', OAI-13; Test-gaps: Petstore-with-shared-params)
|
||||
- src/adapters/openapi_spec.rs:276-336 (method sweep + skip filter), from_openapi.rs:131-151 (success sweep), :248-270 (error sweep), :840 (the enshrining test)
|
||||
- docs/architecture/decisions/066-from-jsonschema-as-http-adapter.md, /051-yaml-input-for-from-openapi.md
|
||||
- tasks/adapters/review-001-openapi-loud-degradation.md (the OAI-06 matrix this extends)
|
||||
|
||||
## Notes
|
||||
|
||||
Slice suggestion: (1) path-item parameters + diagnosis fix, (2)
|
||||
wildcard sweeps, (3) webhooks decision. The wildcard-to-error-status
|
||||
mapping is the one judgment call worth a line in ADR-066 (e.g. "4XX
|
||||
wildcards project to their range's first legal HTTP_ code per error
|
||||
dedup rules"). Sequence after review-002-oai11-ref-memoization (same
|
||||
file, resolver shape changes first) — or rebase-verify.
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
id: review-002-projection-truthfulness
|
||||
name: Projection doc truthfulness — dangling CallError ref, status clobber/drifts, OAS-invalid extension (PRJ-16b/17/18/19/20/21/23/24)
|
||||
status: pending
|
||||
depends_on: []
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
tags: [adapters, review-002, to-openapi]
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Eight review-002 findings in `to_openapi.rs` (+ one in `to_mcp.rs`),
|
||||
all "the generated document must be runtime-truthful and
|
||||
OAS-valid" — the PRJ-01..15 remediation's remaining tail:
|
||||
|
||||
- **PRJ-16b [major]**: `BatchResultEntry.error` refs
|
||||
`#/components/schemas/CallError` which does not exist
|
||||
(to_openapi.rs:602 vs components at :387-413 defines `CallFailure` +
|
||||
per-code variants). The openapiv3 parse test runs on an **empty
|
||||
registry** so nothing trips. Generated clients/validators fail to
|
||||
resolve.
|
||||
- **PRJ-17**: an op declaring `HTTP_404@404` clobbers the shared
|
||||
NotFound response on `/call` (:694/:237-239/:295-300) — losing
|
||||
`NOT_FOUND` which the runtime still emits; every real 404 then
|
||||
violates the documented schema. Merge op codes into the shared
|
||||
response (oneOf append) instead of overwriting.
|
||||
- **PRJ-18**: `/publish` 400 over-declares `INVALID_OPERATION_TYPE`;
|
||||
runtime maps that condition to 401 without a token (the 401 entry in
|
||||
the same doc is the true one) (:268-277).
|
||||
- **PRJ-19**: extractor plain-text 415 (missing content-type) and
|
||||
data-error 422 (JsonDataError) undocumented on `/call`, `/batch`,
|
||||
`/subscribe` (axum 0.8 emits them; the module doc admits
|
||||
"plain-text 400/415/422") (:673-682).
|
||||
- **PRJ-20**: `/call` 401 under-declares — unauthenticated Sub/Pub
|
||||
call → 401 with `INVALID_OPERATION_TYPE`, absent from the enum
|
||||
(:218-219 vs error.rs:67-73; `/publish` got this split right).
|
||||
- **PRJ-21**: `/batch` documents a 500 the runtime never emits (all
|
||||
dispatch failures are in-band entries) (:329-334).
|
||||
- **PRJ-23**: `x-operation-error-statuses` is emitted *inside*
|
||||
`components.schemas` (:414-417) — extension keys are legal on
|
||||
`components`, not as a schema name; OAS-invalid on any registry with
|
||||
op errors. Move to `components` level or delete (nothing consumes
|
||||
it).
|
||||
- **PRJ-24**: dead `search_filter` parameter in `to_mcp::handle_batch`
|
||||
(computed then `let _ =` discarded) — remove.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] PRJ-16b: `CallError` defined (generic code/message/retryable/
|
||||
details) OR `error` points at existing per-code components via
|
||||
oneOf; the openapiv3 parse test runs on a **populated** registry
|
||||
(with op errors — exercising PRJ-23's key too)
|
||||
- [ ] PRJ-17: protocol-status op errors merge into the shared response
|
||||
(oneOf append); test with `HTTP_404@404` asserting `NOT_FOUND`
|
||||
survives alongside
|
||||
- [ ] PRJ-18/20: 400 and 401 oneOf lists match error.rs's actual
|
||||
mapping (drop IOT from publish-400; add IOT to call-401) —
|
||||
conformance tests for both bodies
|
||||
- [ ] PRJ-19: 415 + plain-text 422 slots documented (extend
|
||||
`plain_text_extractor_rejection` or an equivalent shared
|
||||
component)
|
||||
- [ ] PRJ-21: unreachable /batch 500 removed or marked reserved
|
||||
- [ ] PRJ-23: extension key legal placement (or deleted); parse test
|
||||
covers the with-errors path
|
||||
- [ ] PRJ-24: dead parameter removed from `handle_batch`
|
||||
- [ ] Golden-doc tests updated (byte-identical assertion refreshed);
|
||||
`cargo test --all-features`, `cargo clippy --all-features
|
||||
--all-targets -- -D warnings`, `cargo fmt --check` pass
|
||||
|
||||
## References
|
||||
|
||||
- docs/reviews/002-post-remediation-review.md (Part F', PRJ-16b/17/18/19/20/21/23/24; Test-gaps 5/6)
|
||||
- src/adapters/to_openapi.rs:387-433 (components), :602 (dangling ref), :218-300 (status oneOfs), :414-417 (extension), :673-682 (extractor text)
|
||||
- src/gateway/error.rs:57-81 (the runtime mapping these must match), routes.rs (the runtime truth)
|
||||
- tasks/adapters/review-001-openapi-projection-fidelity.md (the PRJ-01..15 work this completes)
|
||||
|
||||
## Notes
|
||||
|
||||
The review-001 lesson applies again: every fix here needs its test to
|
||||
run against a populated registry (the empty-registry parse test is how
|
||||
PRJ-16b and PRJ-23 escaped). Consider one shared
|
||||
`golden_doc_with_ops()` fixture replacing per-test registry setup.
|
||||
ADR-045's version bump: these are doc-contract corrections — bump
|
||||
`info.version` per ADR-045's tracking rule (gateway endpoint contract
|
||||
itself unchanged).
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
id: review-002-yaml-normalization
|
||||
name: YAML input normalization — duplicates, .inf, merge keys, non-string keys (OAI-12)
|
||||
status: pending
|
||||
depends_on: []
|
||||
scope: narrow
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
tags: [adapters, review-002, from-openapi, openapi-spec]
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Review 002 OAI-12 [major]. `OpenAPISpec::from_yaml`
|
||||
(`openapi_spec.rs:153-158`) is a single `yaml_serde::from_str::<Value>`
|
||||
with zero post-parse normalization; three verified corruptions flow
|
||||
through unimpeded (empirically confirmed against yaml_serde 0.10.7):
|
||||
|
||||
1. **Duplicate keys silently last-win** on the Value path
|
||||
(`visit_map → values.insert`) while the JSON path *errors* on the
|
||||
same document — the two input formats disagree, YAML's failure
|
||||
mode is silent.
|
||||
2. **`.inf`/`.nan` numbers become `Value::Null`** through
|
||||
`Number::from_f64(∞) → None` — a declared constraint (`maximum:
|
||||
.inf` is unusual, but any float inf) silently vanishes from the
|
||||
advertised schema.
|
||||
3. **Merge keys (`<<: *anchor`) are not applied** (yaml_serde's
|
||||
`apply_merge()` is opt-in and unused) — `<<` survives as a literal
|
||||
property name in the resolved/advertised schema.
|
||||
|
||||
Non-string keys (`200:` response codes as numbers etc.) were checked —
|
||||
plain statuses survive via string round-trips — but numeric inference
|
||||
into any string-matched field is a live hazard (e.g. `style: 1`).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] A post-parse normalization pass on the YAML path (before
|
||||
`from_value`): reject duplicate keys (loud, naming the path or
|
||||
at least the document context), reject `.inf/.nan`-derived
|
||||
nulls-from-floats (either reject or reject-with-context), and
|
||||
decide merge keys: either invoke `apply_merge()` (matching YAML
|
||||
1.1 user expectations; ADR-051 declares YAML 1.2 via yaml_serde —
|
||||
verify which the crate claims) or reject `<<` keys loudly
|
||||
- [ ] Non-string keys: reject or coerce-with-loudness (decide; JSON
|
||||
path parity is the goal — the same document should mean the same
|
||||
thing through both entry points or the difference must be loud)
|
||||
- [ ] Tests at the `from_yaml` seam (not the parser): duplicate keys →
|
||||
error; `.inf` → loud error (not silent null); merge key applied
|
||||
or rejected; quoted-always spec (`"200":`) unchanged
|
||||
- [ ] A doc note on the YAML/JSON parity contract (ADR-051 section or
|
||||
the module doc)
|
||||
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
|
||||
`cargo fmt --check` pass
|
||||
|
||||
## References
|
||||
|
||||
- docs/reviews/002-post-remediation-review.md (Part E', OAI-12; Test-gap 9)
|
||||
- src/adapters/openapi_spec.rs:153-158 (the seam), from_openapi.rs (the consumers)
|
||||
- docs/architecture/decisions/051-yaml-input-for-from-openapi.md
|
||||
- tasks/adapters/review-001-openapi-import-integrity.md (the import-integrity context)
|
||||
|
||||
## Notes
|
||||
|
||||
Keep the walk cheap (one pass, no allocation beyond error messages).
|
||||
The alias-bomb/depth bounds are dependency-provided and verified
|
||||
working (review 002 re-verified) — do not re-implement them; the task
|
||||
is about *semantic* normalization, not resource limits. ADR-051 is the
|
||||
decision record to update if the merge-key stance changes behavior.
|
||||
Reference in New Issue
Block a user