Skip to content

feat(preflight): required-tools preflight — deterministic availability check with reason taxonomy (spec 098) - #996

Merged
Dumbris merged 10 commits into
mainfrom
098-tools-preflight
Aug 15, 2026
Merged

feat(preflight): required-tools preflight — deterministic availability check with reason taxonomy (spec 098)#996
Dumbris merged 10 commits into
mainfrom
098-tools-preflight

Conversation

@Dumbris

@Dumbris Dumbris commented Aug 15, 2026

Copy link
Copy Markdown
Member

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. Standard APIResponse envelope.
  • mcpproxy tools preflight — typed exit codes 0 ready · 10 retryable (backoff) · 11 blocked (page operator) · 12 unknown ID (fix manifest); worst class wins. A cron wrapper branches without parsing JSON.
  • One shared eligibility evaluator (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).
  • Four-path dispatch consolidation (FR-002 no-skew): call_tool variants, direct mode, code_execution and stored scripts all consume the same gate. Notable hardening: scripts previously enforced no per-tool policy at all — a code_execution/stored script could call a quarantined tool; now refused like every other path.
  • Transparency: every executed preflight writes a durable activity record (synchronous, before the 200 — persistence failure ⇒ 503), request-ID-correlated and browsable via mcpproxy activity list --request-id … and the Web UI.
  • Disclosure tiers: operator (API key/socket/pipe) gets full diagnosis incl. sha256/v{N}:{hex} hash pins; agent tokens get scope-silence (out-of-scope, unconfigured — byte-indistinguishable not_found; no hashes; did_you_mean never crosses scope). Stale token ProfilePin now intersects to deny-all.
  • Zero MCP-surface change: tools/list byte-identical to merge-base across all three routing modes (golden-snapshot test, goldens captured from origin/main).

Verification

  • Sabotage E2E matrix (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.
  • Full gates: 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.
  • code_execution + stored scripts: byte-identical transcripts vs main on a 9-probe harness; live no-skew check across all four dispatch paths.

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): resolveActiveProfile warn-skips a stale ProfilePin on the live session path — preflight is now stricter than dispatch there; the session path deserves its own reviewed fix.

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).
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 15, 2026

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

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

View logs

@codecov-commenter

codecov-commenter commented Aug 15, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown

📦 Build Artifacts

Workflow Run: View Run
Branch: 098-tools-preflight

Available Artifacts

  • archive-darwin-amd64 (29 MB)
  • archive-darwin-arm64 (26 MB)
  • archive-linux-amd64 (17 MB)
  • archive-linux-arm64 (15 MB)
  • archive-windows-amd64 (28 MB)
  • archive-windows-arm64 (25 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (23 MB)
  • installer-dmg-darwin-arm64 (20 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 31908260253 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

…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
@Dumbris
Dumbris enabled auto-merge (squash) August 15, 2026 21:01

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Dumbris
Dumbris merged commit f374810 into main Aug 15, 2026
53 checks passed
@Dumbris Dumbris mentioned this pull request Aug 17, 2026
3 tasks
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.

2 participants