Skip to content

enforce caller execution-mode eligibility across provider pins, inheritance and fallback - #6393

Merged
atomantic merged 1 commit into
mainfrom
claim/issue-6368
Sep 6, 2026
Merged

enforce caller execution-mode eligibility across provider pins, inheritance and fallback#6393
atomantic merged 1 commit into
mainfrom
claim/issue-6368

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

A provider record's type (cli / tui / api) is its executable route mode, and callers are not interchangeable across them. Each caller's rule used to be re-derived at its own call site, so a rule enforced on the explicit pin was routinely missing from the fallback chain that could replace that pin:

  • a CLI-only caller could inherit a TUI route through a saved fallbackProvider;
  • an agent task picked an api fallback and was rejected only after it had burned the cascade's last retry;
  • server/services/providerStatus.js#getFallbackProvider silently dropped the requestCapabilities argument entirely, so no caller policy could reach the toolkit at all.

New server/lib/callerModePolicy.js is the single answer to "may THIS caller run on THAT route mode?" — four named policies (agent-harness, cli-harness, direct-api, any-text) plus an opt-in required-model-capability map. It is applied to explicit pins, to activeProvider inheritance, and to every fallback candidate (task-level, configured and system-priority alike), riding on requestCapabilities.allowedModes into the toolkit's getFallbackProvider.

Design points:

  • An ineligible candidate is skipped with a reason, never thrown on — "no eligible fallback" stays the transient condition callers already handle, rather than becoming a new exception class at every call site.
  • Unknown never becomes true. A record whose type names no executable mode is refused by every policy, and a required capability with no positive evidence is refused rather than assumed.
  • A caller that declares nothing routes exactly as before. No shipped policy declares a required capability, so today's advisory tool-use signal stays advisory.
  • The module is a zero-dependency leaf so the out-of-process autofixer can import it; enabled/benched state, prerequisites and text-transport consent stay with their existing owners.
  • The browser mirror (providerModeSelectionPolicy) only shows the rule: an ineligible saved pin stays visible and disabled with a reason instead of being hidden or silently replaced, and CLI/TUI siblings remain separate selectable options. Three ad-hoc inline { provider: p => p.type === 'api' } policies collapse onto it.

No connection graph, storage migration, or peer wire change — this slice ships on the executable provider records an install already runs.

Test plan

  • cd server && npm test — 2011 files / 39,976 tests pass.
  • cd client && npm test — 871 files / 10,589 tests pass.
  • New/extended boundary coverage:
    • server/lib/aiToolkit/providerStatus.test.js — a TUI candidate is skipped at the task, configured and system tiers; an all-ineligible chain returns null (never throws); an untyped record is refused; an undeclared caller still takes a TUI route.
    • server/services/agentProviderResolution.test.js — the agent policy is carried into fallback selection; an unrecognized type is refused permanently; a TUI pin remains a legitimate agent route (guards the generalization against over-reach).
    • server/services/promptRunner.test.js — an ineligible explicit provider is refused with PROVIDER_MODE_NOT_PERMITTED before any run record is created; the policy reaches createRun on requestCapabilities.
    • server/lib/callerModePolicy.test.js — unregistered policy names throw rather than resolving permissively; an unknown required capability does not satisfy the requirement; the locally-declared mode vocabulary is pinned to PROVIDER_TYPES and ROUTE_MODES.
    • client/src/utils/providerModePolicy.parity.test.js + ProviderModelSelector.test.jsx — client/server policy parity, and a saved TUI pin renders visible-but-disabled with a reason under a CLI-only policy.
  • server/lib/importScoping.test.js budget raised 89,500 → 89,700: the new leaf has zero imports and is reached by the routing boundary, which is the ordinary leaf growth the budget documents as tolerable rather than the eager-edge shape it exists to catch.

Closes #6368

…oundary (#6368)

A provider record's type (cli / tui / api) is its executable route mode, and
callers are not interchangeable across them: a CoS agent task needs a
file-writing harness, the standalone autofixer can only drive a headless CLI,
and a tool-free review stage must stay on a direct API provider. Each of those
rules used to be re-derived at its own call site, so a rule enforced on the
explicit pin was routinely missing from the fallback chain that could replace
that pin — a CLI-only run could inherit a TUI route through a saved
`fallbackProvider`, and an agent task picked an `api` fallback only to be
rejected after it had already burned the cascade's last retry.

`server/lib/callerModePolicy.js` is now the single answer to "may THIS caller
run on THAT route mode?": four named policies (`agent-harness`, `cli-harness`,
`direct-api`, `any-text`) plus an opt-in required-model-capability map. It is
applied to explicit pins, to activeProvider inheritance, and to every fallback
candidate — task-level, configured and system-priority alike — by riding on
`requestCapabilities.allowedModes` into the toolkit's `getFallbackProvider`
(which the PortOS wrapper had been dropping entirely). An ineligible candidate
is skipped with a reason, never thrown on, so "no eligible fallback" stays the
transient condition callers already handle.

Unknown never becomes true: a record whose type names no executable mode is
refused by every policy, and a required capability with no positive evidence is
refused rather than assumed. Callers that declare nothing route exactly as
before.

The browser mirror (`providerModeSelectionPolicy`) only shows the rule: an
ineligible SAVED pin stays visible and disabled with a reason instead of being
hidden or silently replaced, and CLI/TUI siblings remain separate options.
@atomantic
atomantic merged commit 4c0793e into main Sep 6, 2026
12 checks passed
@atomantic
atomantic deleted the claim/issue-6368 branch September 6, 2026 15:36
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.

Enforce execution-mode eligibility across provider selection and fallback

1 participant