#100: Make validation execution authority explicit and fail closed - #216
Merged
Merged
Conversation
Squash the PR #206 integration branch onto current origin/main (107 commits ahead) and reconcile the eval row schema: main's per-quartet matched-row validation now reads `effectiveAuthorityMode`, and the requested/effective parity check folds into the same pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The authority model shipped with no production resolver: the executor's resolveExecutionAuthority was an optional dependency that only a test stub ever supplied, so every non-yolo payload denied with server-authority-not-resolved and no mode but yolo could execute. Add src/server/validation-plans/authority-resolver.ts as the production resolver. Per payload it resolves target authorization coverage from the ledger, derives task scope from that authorization, resolves a containment policy snapshot whose content-addressed id serves as both the policy decision and the containment snapshot, creates the Tool Run, and settles the durable approval: strict requires an exact operator approval covering validationPayloadApprovalIntent(planId, payloadId), auto mints and consumes its own exact one-time approval, self claims none. It never throws; every missing fact becomes a denial record. Wire it through both public seams. All four executing tool actions and the validation plan workflow now construct the resolver, and both accept a typed model decision (modelProceed / modelConfirm) so self is reachable without widening model authority. executeMastraSecurityAction now stamps the real selection into Tool Run metadata, and finding-helpers pins the authority on the plans it creates. Harden the executor so a throwing resolver denies the payload instead of aborting the run - executionApprovalGranted returns true for auto and self and relies entirely on this per-payload guard. Operators select the mode as project.settings.validationAuthorityMode, plumbed through the chat route into run metadata. Absence means strict; the mode is never inferred from approvalMode. Plans persisted before this contract carry no pin and would have hard-failed record_result and confirm. They are adopted as self with a recorded validationAuthorityMigration, preserving their original model-asserted semantics without granting a stronger claim. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`pnpm check` now exits 0. Fixes organizeImports in trajectory-export, authority-resolver and the two validation-authority test files, plus formatting in security-actions/execution.ts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #100. Unblocks #123, #203, #67, #68 and the #60–#66 qualification cluster. Last open blocker on umbrella #97 alongside the custody chain.
Supersedes draft PR #206, which carried the authority model but never connected it.
The problem
The branch defined a complete authority model —
ValidationAuthorityMode(strict | auto | self | yolo) pinned immutably per plan, andValidationExecutionAuthorityas the server-owned fact record — but the resolver was never constructed in production. It existed only as a stub in one test. With no resolver and mode ≠yolo,resolvePayloadAuthoritiesreturned an empty map and every payload denied withserver-authority-not-resolved. Self mode was unreachable becausemodelDecisionwas never supplied by the tool. Nothing anywhere setmetadata.validationAuthorityMode, so the effective mode was alwaysstrictin production.What this adds
src/server/validation-plans/authority-resolver.ts— the production resolver. Per payload it gathers target authorization from the ledger (findTargetAuthorizationCoverage, falling back to the plan target's locator for manual and evidence-only payloads), derivestaskScopeVerifiedfrom that authorization's scope, takes aresolveContainmentPolicysnapshot whose content-addressed id serves as bothpolicyDecisionIdandcontainmentPolicySnapshotId, creates a real Tool Run, then resolves the durable approval:strictrequires an exact operator approval covering the exportedvalidationPayloadApprovalIntent(planId, payloadId)automints and consumes its own exact one-time approvalselfclaims noneIt never throws — every missing fact becomes a denial record.
Note the two fields that appeared to have no producer do in fact have one:
policyDecisionId/containmentPolicySnapshotIdanchor to the containment snapshot from #152, andtaskScopeVerifiedderives from the existing authorization ledger. No new identifier scheme was invented.Both public seams wired. All four executing tool actions and
validation-plan-workflownow construct the resolver; the workflow input schema gainedvalidationAuthorityMode,threadIdanddurableApprovalId, having previously had no authority field at all.executeMastraSecurityActionpasses the real selection into Tool Run metadata — previously every Tool Run recorded defaultstrictregardless of actual mode.finding-helperspins authority on plans it creates.Self mode reachable. Flat
modelProceed/modelConfirmbooleans on the tool and workflow input schemas — no unions or combinators, per the schema-subset rule — threaded into everyexecutePlancall.Operator selection.
project.settings.validationAuthorityMode, modelled as a meaningful value where absence meansstrict, never inferred fromapprovalMode. Validated inprojectAdapterandprojects/setup, plumbed through the chat route into run metadata and onward to the request context the tool reads.A real bug the tests found
executionApprovalGrantedreturns unconditionaltrueforautoandself, relying entirely on the per-payload resolver to fail closed. Writing the adversarial test exposed that a throwing resolver escapedexecutePlanand crashed the tool rather than denying. It now becomes a per-payload denial. Given what that line delegates, an exception escaping the resolver was the difference between fail-closed and no destructive guard at all.Unpinned-plan migration
requireModelValidationAuthoritythrows on plans with no pinned authority, so plans fromfinding-helpers, from the workflow, or persisted before this change would have hard-failedrecord_resultandconfirm.Legacy unpinned plans are adopted as
selfwith a recordedvalidationAuthorityMigration. That preserves exactly what those plans always were — model-asserted with no server facts — and never grants them a stronger claim. It does not bypass the per-payload resolver.Tests
New
tests/integration/validation-authority-execution.test.ts, 9 cases against a real database, real authorization ledger, and realDatabaseDurableDecisionRepository/authorizeAction— no injected approval literals:autoandselfwhen the resolver denies, is absent, or throwsThe two tool tests that had been pinned to
yolo— the one mode that bypasses the resolver entirely — now run underself.Verification
pnpm typecheckpnpm test:unitpnpm test:integrationpnpm test:evalspnpm test:packagespnpm buildbiomeon 11 changed filesNo live or paid model calls.
Rebase note
The branch sat 107 commits behind. A commit-by-commit rebase conflicted repeatedly — main had reformatted the same files and renamed the eval row's
authorityModetorequested/effective— so the cumulative diff was squash-applied ontoorigin/mainas one commit. Two conflicts, both in the eval foundation, resolved by keeping main's per-quartet matched-row validation and v2 scorer/manifest versions while folding in the branch's requested/effective parity check. No redaction conflicts arose and nothing removed by #215 was reinstated.Deliberately deferred
The eval manifest still declares
executionAdmission: blocked by #100. Unblocking paid admission is a gated operations decision under the eval honesty rules, not a code change — flip it through the documented gate once this lands.No UI control was added for the new project setting; it is settable through the project settings API and the setup path.
🤖 Generated with Claude Code