Repository navigation
[EPIC-X][LOU-X2] Declarative permission policies evaluated before needsApproval, with an audit log - #105
Merged
Conversation
…dsApproval, with an audit log
`permissions?: PermissionRule[]` on createAgent(), ExecuteOptions and
resumeAfterApproval() options: ordered { tool, when?, action, reason? } rules
checked for every tool call after validation and preToolCall hooks, before
needsApproval. First match decides: allow runs without approval, deny gives
the model a `kind: 'denied'` tool error (ToolDeniedError, with reason) and
emits tool.error, ask pauses for approval (or the approve callback). No match
keeps today's behaviour. Helpers allow()/deny()/ask().
Audit log: onPermissionDecision(entry) per call and a typed
`permission.decision` stream event (schema stays v1); args omitted under
redactContent. Nothing is evaluated or emitted unless permissions or
onPermissionDecision is set. Sub-agents inherit the parent's rules (ahead
of their own) and its audit callback.
parseToolArguments moves to toolArgsValidation.ts (re-exported from
toolCallExecution.ts) so permissions.ts can reach the stream sink in
agentRun.ts without an import cycle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bY29ibQgv3MDdjAbUT4YR
…icies # Conflicts: # CHANGELOG.md # src/execution/agentEvents.test-d.ts # src/execution/agentRun.ts
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.
What
src/execution/permissions.ts:PermissionRule = { tool: string | string[] | RegExp | '*'; when?: (args, { toolName, toolCallId, sessionId? }) => boolean | Promise<boolean>; action: 'allow' | 'deny' | 'ask'; reason? }, helpersallow(tools),deny(tools, reason?),ask(tools), andPermissionOptions(permissions,onPermissionDecision).permissions/onPermissionDecisiononcreateAgent()(CreateAgentBase extends PermissionOptions, passed throughrunOptionsso resumed runs and sub-agent use get them too),ExecuteOptions(extends PermissionOptions) and soresumeAfterApproval()options.toolCallExecution.ts: after validation andpreToolCallhooks, rules are checked in order beforeneedsApproval. The first match decides:allowruns without approval,denyreturns a structured tool error (kind: 'denied',ToolDeniedError,reason) and so emitstool.error,asktakes the existing approval pause (or theapprovecallback). No match falls through toneedsApproval. A throwingwhenbecomes the call's error, the same as a throwingneedsApproval.onPermissionDecision({ toolName, toolCallId, decision: 'allow'|'deny'|'ask'|'default', rule?: { index, reason? }, args?, at }).argsis left out underredactContent. Streams get the same entry as a typedpermission.decisionevent (PermissionDecisionEvent). This is additive and the schema stays v1. Nothing is evaluated or emitted unlesspermissionsoronPermissionDecisionis set, so existing runs and streams are unchanged.onPermissionDecision. This is insubagentRuntime.ts(InheritedRuntime) anddelegation.ts(childOptions,SubagentSpec.permissions).toolErrors.ts: newToolErrorKind'denied'.permission.decisionrow in docs/streaming.md, adeniedrow in docs/tools.md, the README approvals feature line, CHANGELOG (Added), and regenerated llms-full.txt.Files outside the ticket's listed scope (forced)
src/execution/AgentExecutor.ts: 2 lines (import type { PermissionOptions }+export interface ExecuteOptions extends PermissionOptions).ExecuteOptionsis declared there, so the acceptance criterion cannot be met without touching it. I tried module augmentation from permissions.ts first. tsup's bundled.d.tskeeps a danglingdeclare module './AgentExecutor', so consumers would not see the fields, and I dropped that approach.src/execution/agentRun.ts:RunEventSink.permissionDecision()emits the stream event. Also,parseToolArgumentsmoved totoolArgsValidation.ts(still re-exported from toolCallExecution.ts). Without the move,toolCallExecution → permissions → agentRun → toolCallExecutionis an import cycle, which fallow rejects.src/execution/index.ts(exports) andsrc/execution/agentEvents.test-d.ts(the exhaustive event-type test).Why
Epic X matrix row "Permissions policy" is⚠️ . eve has approval policies and open-harness has one
approvecallback. We only had per-toolneedsApprovalpredicates and the shell tool's own allow/deny lists. This adds one declarative, ordered policy for the whole agent, plus an audit trail.Acceptance criteria
permissions?: PermissionRule[]oncreateAgent()andExecuteOptions. Rules are checked in order beforeneedsApprovaland the first match decides: allow / deny (kind: 'denied'+reason, emitstool.error) / ask (approval pause orapprove). No match keeps today's behaviour.allow(tools),deny(tools, reason?),ask(tools)in src/execution/permissions.ts'denied'added toToolErrorKindonPermissionDecisionaudit callback (args omitted underredactContent, ISOat) plus a typedpermission.decisionevent in agentEvents.ts (additive, schema v1)permissionsthrough the subagent runtime (a few lines, done here, not deferred to X2.2)mockModel: allow overrides needsApproval; deny reaches the model with its reason and emits the event; ask pauses (and theapprovecallback path); regex/array matching; asyncwhen; order precedence; audit entries (plus redaction, a throwingwhen, no events without a policy, sub-agent inheritance)How verified
npx tsc --noEmit: no errorsnpm run lint:✖ 404 problems (0 errors, 404 warnings), the same count as mainnpm run test:types:Test Files 6 passed (6),Tests 27 passed (27),Type Errors no errorsnpx vitest run src/execution/permissions.test.ts:Tests 11 passed (11)npx vitest run(after building):Tests 1 failed | 2234 passed | 3 skipped (2238). The one failure issrc/deploy/adapters/cloudflare.test.ts > real wrangler tooling > wrangler dev serves the worker on the local workerd runtime(expected undefined to be 200). It needs a local workerd runtime, which this sandbox cannot provide. It fails again when run alone and is unrelated to this change. Thecreate-loushy-agenttests pass once its package is built.npm run build && npm run build --workspace=packages/create-loushy-agent: both succeednpm run docs:verify-snippets -- --skip-build:all 116 snippet(s) type-check against src/; 6 also run cleanlynpm run docs:llms:check: passesnpm run test:coverage: the same single wrangler failure, so I re-ran with--coverage.reportOnFailureto write the coverage file. Thennpm run fallowexits 0 with✓ No issues found. Its one duplication group, AnthropicProvider/OpenAIProvider, was already there.🤖 Generated with Claude Code
https://claude.ai/code/session_016bY29ibQgv3MDdjAbUT4YR
Generated by Claude Code