feat(preflight): required-tools preflight — deterministic availability check with reason taxonomy (spec 098) - #996
Conversation
Related #969 Spec for item 3 of #969: deterministic side-effect-free availability check for caller-supplied tool IDs. Shared eligibility evaluator, 15-code failure enum with precedence chain, REST POST /api/v1/preflight, CLI with typed exit codes 0/10/11/12, activity-log transparency, sabotage E2E matrix. Cross-model reviewed (opencode/gpt-5.6-sol, 3 rounds, APPROVE).
Related #969 plan.md, research.md, data-model.md, contracts/preflight-api.yaml, quickstart.md, tasks.md. Cross-model reviewed (opencode/gpt-5.6-sol): round 1 returned 25 findings incl. 12 P1s (activity seam ownership, swagger generation path, nonexistent 093 HTTP shed budget, ForProfile mutation, dropped agent-token ProfilePin, dispatch fail-open directionality); all incorporated; rounds 2-3 APPROVE.
…ions extraction (098) Related #969 internal/preflight: locked reason enum + precedence chain + exit mapping, precedence-walking evaluator (existence = index OR approval record; connection verdicts outrank not_found on non-Ready servers), tier disclosure with byte-indistinguishable scope-silence (incl. server_not_configured masking at the token tier), schema-versioned hash pins, scope-filtered did_you_mean. internal/toolannotations extracted from mcp_annotations (spec-094 behavior byte-identical). Agent-token ProfilePin propagated via AuthContext(); contracts DTOs + generator.
…ronous activity seam (098) Related #969 RunPreflight glue over index/approvals/stateview/config (zero upstream I/O; ForProfile unreachable; annotations enriched from the stateview snapshot). tool_gate.go: one shared classification consumed by call_tool variants, direct mode, code_execution and stored scripts — scripts previously enforced NO per-tool policy (FR-002). Synchronous durable RecordPreflight bypassing the bounded event channel. Sabotage E2E matrix (22 cells + covers-every-reason), dispatch-parity tests, merge-base tools/list goldens (byte-identical, FR-015).
…rowsability (098) Related #969 POST /api/v1/preflight (APIResponse envelope, tier detection, wait_ms poll with 4-slot semaphore degrade, durable-record-before-200 else 503). mcpproxy tools preflight with exit codes 0/10/11/12 (worst class wins). Operator-tier hash-pin authoring on tool listings (never to tokens). Activity type preflight across CLI/REST/frontend. Swagger + contracts.ts regen; serveredition staticcheck fixes (pre-existing).
…mples (098) Related #969 docs/features/tools-preflight.md (taxonomy, precedence, tiers, cron/GHA/ n8n recipes, code-exec composition), rest-api.md + CLI reference, README agent-workflow section (discover/call/audit + preflight-gated automation). Spec amendments from live verification: existence source, scope-silent server_not_configured.
Related #969 Storage read failures propagate to 503 instead of collapsing into server_not_configured (new storage.ErrUpstreamNotFound sentinel); authoritative-snapshot contract (RequireRuntimeEntry) refuses with ErrRuntimeUnavailable instead of answering ready/not_found without runtime state; stale agent-token ProfilePin now intersects to deny-all instead of widening; code_execution gate refuses on record read errors (fail-open kept only for genuinely unknown servers); direct mode reads the live quarantine switch (was construction-time, fail-open on nil) and keeps server-level refusals ahead of approval locks (pre-098 response selection); CLI wraps non-verdict failures so classifyError cannot remap them to exit 4/5; strict request decoding (1MiB cap, unknown fields, trailing JSON); wait polls bounded by the wait deadline; -o format case-insensitive; --wait validated pre-truncation via shared preflight.MaxWaitMS. Reviewer: opencode/gpt-5.6-sol round 1 (VERDICT: REVISE, 11 findings, 0 false positives; each fix verified by restoring old code and watching the new test fail).
…alizing, not 503 Related #969 The stateview starts empty and fills per-server asynchronously (first supervisor reconcile ~500ms), so the round-1 authoritative-snapshot contract made startup/reconcile/config-add windows answer spurious 503s for valid configured servers. A configured server with no published entry now answers the retryable server_initializing verdict; ErrRuntimeUnavailable remains only for a wired runtime with no stateview snapshot object at all. Reviewer: opencode/gpt-5.6-sol round 2 (1 P1 regression).
Deploying mcpproxy-docs with
|
| Latest commit: |
b8e5321
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://ff2bbbbd.mcpproxy-docs.pages.dev |
| Branch Preview URL: | https://098-tools-preflight.mcpproxy-docs.pages.dev |
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
📦 Build ArtifactsWorkflow Run: View Run Available Artifacts
How to DownloadOption 1: GitHub Web UI (easiest)
Option 2: GitHub CLI gh run download 31908260253 --repo smart-mcp-proxy/mcpproxy-go
|
…ng \r drift TestToolsListSnapshot_MatchesMergeBaseGoldens byte-compares checked-in goldens under internal/server/testdata/toolslist_goldens/. Those files are plain .json, so the existing .gitattributes patterns (*.golden.json) never reached them — Windows runners default to core.autocrlf=true and checked them out as CRLF, failing all three surfaces on \r alone while the JSON was semantically identical (no added/removed/changed tool assertions fired). Two-layer fix, matching the established pattern for the other byte-compared fixtures in this repo: 1. .gitattributes pins the directory to eol=lf (the real fix). 2. The test normalizes CRLF -> LF before comparing (defense in depth, so a stale local git config cannot produce a spurious FR-015 regression). Verified by rewriting the goldens to CRLF locally: fails before, passes after. Related #996
# Conflicts: # internal/server/mcp.go
There was a problem hiding this comment.
Approved: CI green at this head SHA (46 pass / 6 skip / 0 fail) after merging main (#995) into the branch; the internal/server/mcp.go conflict was resolved keeping BOTH the spec-098 gate call sites and the spec-994/995 structured reasonKey, re-verified locally with go build (both tag sets), go test -race on internal/server + telemetry + preflight + toolannotations, and golangci-lint v2 (0 issues). Cross-model review returned APPROVE. Arming auto-merge.
Related #969
Implements item 3 of #969 (the reporter-confirmed shape: cron/CI-first, REST+CLI, stat-only): a deterministic, side-effect-free availability check for a caller-supplied list of tool IDs, answering per-ID with a machine-readable reason instead of failing silently at discovery time.
Depends on #995 (baseline counters) merging one release ahead, per the Phase-0 decision.
What
POST /api/v1/preflight—{tools:[{id, pin_hash?}], profile?, policy?, wait_ms?}→ per-ID verdicts. HTTP 200 whenever the check executed (the verdict is data); 400 validation / 503 runtime-or-persistence failure. StandardAPIResponseenvelope.mcpproxy tools preflight— typed exit codes0ready ·10retryable (backoff) ·11blocked (page operator) ·12unknown ID (fix manifest); worst class wins. A cron wrapper branches without parsing JSON.internal/preflight): closed 15-code reason taxonomy with a fixed precedence chain; existence = index ∨ approval record (the runtime de-indexes blocked/pending/changed tools — index absence alone proves nothing); zero upstream I/O and zero runtime mutation, structurally (the evaluator cannot reach a transport; asserted by instrumented-transport + state-snapshot tests).mcpproxy activity list --request-id …and the Web UI.sha256/v{N}:{hex}hash pins; agent tokens get scope-silence (out-of-scope, unconfigured — byte-indistinguishablenot_found; no hashes;did_you_meannever crosses scope). Stale token ProfilePin now intersects to deny-all.tools/listbyte-identical to merge-base across all three routing modes (golden-snapshot test, goldens captured from origin/main).Verification
testdata/preflight_sabotage_matrix.json): 23 scenario cells against a real binary + live fixture upstreams — quarantine flip, rug-pull drift, block, config-deny, disable, SIGSTOP/kill, mid-indexing, annotation cells per filter, unknown id/server, hash + schema-version mismatch, PendingAuth, profile scope at both tiers — each asserting exact {reason, retryable, action} + the activity record by request ID; a reflection test forbids enum codes without a cell.go test -race ./...(79 pkgs), server-edition build/tests/lint, 65/65 API E2E, golangci v2 both tag sets, swagger + generate-types idempotent, frontend build + 605 unit tests.Review
Spec, plan, and full diff cross-model reviewed (opencode / gpt-5.6-sol): spec 3 rounds → APPROVE; plan 25 findings (12 P1) incorporated → APPROVE; code round 1 = 11 findings (6 P1, all genuine, each fix verified by restoring the old code), round 2 = 1 regression (startup-window 503 →
server_initializing), round 3 → APPROVE.Docs
New
docs/features/tools-preflight.md(taxonomy, precedence, tiers, transparency, cron/GHA/n8n recipes, code-exec composition), REST + CLI references, README "How AI Agents Work Through MCPProxy" section with a preflight-gated automation example.Known follow-up (deliberately out of scope, pre-existing):
resolveActiveProfilewarn-skips a stale ProfilePin on the live session path — preflight is now stricter than dispatch there; the session path deserves its own reviewed fix.