Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions docs/policy-safety-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Stronger Safety Model Guide

This guide explains how to interpret policy denials and adjust scoped approvals and path allowlists safely.

## Decision Envelope

Policy outcomes now include:
- `decision`: `allow`, `deny`, or `approval_required`
- `matchedRules`: stable identifiers for matched policy checks
- `scopeContext`: approval scope tuple (`principal`, `operationClass`, `resourceScope`, `scopeId`) plus optional lifecycle fields (`expiresAt`, `revoked`)
- `reasonCodes`: normalized denial/approval reasons

Legacy fields (`allowed`, `requiresApproval`, `reason`, `sideEffect`) remain available for compatibility.
When present in `scopeContext`, `expiresAt` is the ISO-8601 expiration timestamp for a persisted scope and `revoked` indicates that scope cannot be reused.

## Actionable Reason Codes

- `approval_required_side_effect`: interactive approval is required for the operation class.
- `approval_scope_granted`: interactive approval was granted and persisted for the exact scope.
- `approval_scope_reused`: an existing scoped approval matched exactly and was reused.
- `approval_expired`: the prior scoped approval has expired and must be re-approved.
- `approval_revoked`: the scope was explicitly revoked and cannot be reused.
- `missing_allowlist`: allowlist policy is active but no valid allowlist exists for this operation.
- `missing_target_paths`: guarded command did not provide explicit target paths for allowlist evaluation.
- `path_out_of_allowlist`: at least one canonicalized path is outside allowed roots.
- `path_canonicalization_failure`: canonicalization failed for a configured root or target path.
- `blocked_command_pattern`: command matched a hard-block pattern.

## Managing Path Allowlists

Use policy config keys:
- `pathAllowlistByOperation`: operation-specific allow roots keyed by side effect (`write`, `destructive`, etc.)
- `pathAllowlist`: fallback roots when operation-specific roots are not present
- `allowlistPolicyOperations`: operation classes where strict allowlist enforcement is required

Guidance:
1. Add only absolute, canonical roots for intended write targets.
2. Keep roots narrow and operation-specific where possible.
3. For automation writes, ensure allowlists are configured before execution to avoid deny-by-default outcomes.

## Managing Scoped Approvals

Scoped approvals are keyed by:
- principal identity
- operation class
- resource scope (derived from command + cwd)

Approvals are reused only when all scope keys match exactly and the record is both unexpired and not revoked.

Revocation:
- Revoke by `scopeId` when permissions should no longer be reused.
- After revocation, the same request will return `approval_revoked` until re-approved.
30 changes: 30 additions & 0 deletions openspec/changes/archive/2026-03-03-stronger-safety-model/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## 1. Policy Contract and Data Model

- [x] 1.1 Define the structured policy decision envelope (`decision`, `matchedRules`, `scopeContext`, `reasonCodes`) in shared types/contracts
- [x] 1.2 Add scoped approval persistence model fields (principal, operation class, resource scope, expiration, revocation state)
- [x] 1.3 Implement approval lookup/match logic that requires exact scope-key matching before reuse

## 2. Scoped Persisted Approvals

- [x] 2.1 Implement expiration validation for persisted approvals at decision time
- [x] 2.2 Implement approval revocation by scope identifier and ensure revoked approvals are excluded from reuse
- [x] 2.3 Add tests for scope-match success, scope mismatch denial, expiration, and revocation behavior

## 3. Strict Path Allowlist Enforcement

- [x] 3.1 Implement centralized path canonicalization utility for guarded operations
- [x] 3.2 Implement allowlist matcher over canonical absolute paths with deny-by-default semantics when allowlist policy is enabled for the operation
- [x] 3.3 Integrate a mandatory guard at all file-mutation/sensitive command operation boundaries
- [x] 3.4 Add tests for in-allowlist allow, out-of-allowlist deny, missing allowlist deny in allowlist-enabled mode, interactive approval-gated fallback without allowlist, and canonicalization-failure deny

## 4. Explainable Policy Decisions

- [x] 4.1 Update policy evaluation pipeline to emit structured explanation fields for both allow and deny outcomes
- [x] 4.2 Standardize reason-code taxonomy for path enforcement and approval-scope outcomes
- [x] 4.3 Add deterministic-output tests to ensure identical inputs return identical decision explanation payloads

## 5. Rollout and Validation

- [x] 5.1 Add compatibility/adapter handling for legacy decision consumers during rollout
- [x] 5.2 Add integration tests that cover end-to-end guarded execution with scoped approvals and path allowlists
- [x] 5.3 Document operator-facing guidance for interpreting denial reasons and updating allowlists/scopes
30 changes: 0 additions & 30 deletions openspec/changes/stronger-safety-model/tasks.md

This file was deleted.

22 changes: 22 additions & 0 deletions openspec/specs/explainable-policy-decisions/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# explainable-policy-decisions Specification

## Purpose
Define requirements for deterministic, structured, and user-interpretable policy decision explanations across allow, deny, and approval-required outcomes.
## Requirements
### Requirement: Policy decisions SHALL include structured explanations
Each policy evaluation result SHALL include a structured explanation envelope containing decision outcome, matched rule identifiers, scope context, and standardized reason codes.

#### Scenario: Denial includes actionable explanation fields
- **WHEN** a request is denied by policy
- **THEN** the decision payload includes non-empty reason codes and the rule identifiers that contributed to denial

#### Scenario: Approval includes scope context
- **WHEN** a request is approved by policy
- **THEN** the decision payload includes the effective scope context used for that approval

### Requirement: Decision explanations MUST be deterministic
Given identical policy inputs, context, and configuration state, the system MUST return the same decision outcome and explanation fields.

#### Scenario: Repeated identical request yields same explanation
- **WHEN** the same request is evaluated repeatedly without any policy or context changes
- **THEN** each evaluation returns identical decision outcome and explanation payload fields
30 changes: 30 additions & 0 deletions openspec/specs/path-allowlist-enforcement/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# path-allowlist-enforcement Specification

## Purpose
Define canonical path-allowlist enforcement behavior for guarded file mutations and sensitive commands, including deny-by-default handling for missing or invalid policy inputs.
## Requirements
### Requirement: Guarded operations MUST enforce canonical path allowlists
For every guarded file-system mutation or sensitive command operation that uses allowlist policy (including automation safe-write policy), the system MUST resolve target paths to canonical absolute paths before evaluating allowlists. The operation SHALL proceed only if every target path is contained within an allowed root for that operation.

#### Scenario: Canonical in-allowlist path is permitted
- **WHEN** all requested target paths canonicalize to locations within configured allowed roots
- **THEN** the operation is permitted subject to other policy checks

#### Scenario: Canonical out-of-allowlist path is denied
- **WHEN** any requested target path canonicalizes outside configured allowed roots
- **THEN** the system denies the operation with a path-allowlist denial reason

### Requirement: Path enforcement SHALL be deny-by-default
The system SHALL deny guarded operations when allowlist policy is configured but is absent for the requested operation, invalid, or cannot be evaluated. In interactive mode where no allowlist policy is configured for that operation, the system SHALL fall back to approval-gated policy flow.

#### Scenario: Missing allowlist configuration denies operation
- **WHEN** an operation is evaluated under allowlist policy and no valid allowlist is available
- **THEN** the system denies the operation and returns an explicit missing-allowlist reason

#### Scenario: Interactive mode without allowlist remains approval-gated
- **WHEN** a guarded mutating operation is requested in interactive mode and no allowlist policy is configured for that operation
- **THEN** the system follows approval-gated policy flow and does not auto-deny solely due to missing allowlist

#### Scenario: Canonicalization failure denies operation
- **WHEN** target path canonicalization fails for any requested path
- **THEN** the system denies the operation and reports canonicalization-failure reason
26 changes: 26 additions & 0 deletions openspec/specs/scoped-persisted-approvals/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# scoped-persisted-approvals Specification

## Purpose
Define scope constraints, expiration, and revocation requirements for persisted approvals so prior approvals are reused only when all scoped conditions match.
## Requirements
### Requirement: Persisted approvals SHALL be scope-bound
The system SHALL persist approvals with explicit scope keys including principal identity, operation class, resource scope, and expiration metadata. A persisted approval MUST only be reusable when all scope keys match the current request.

#### Scenario: Approval is reused only within identical scope
- **WHEN** a request matches principal, operation class, resource scope, and approval validity window of a stored approval
- **THEN** the system reuses the persisted approval and marks the decision as approved without prompting

#### Scenario: Approval is rejected on scope mismatch
- **WHEN** a stored approval exists but any scope key differs from the current request
- **THEN** the system SHALL NOT reuse the approval and SHALL require a new policy decision

### Requirement: Persisted approvals SHALL expire and be revocable
The system SHALL enforce expiration on persisted approvals and MUST support explicit revocation by scope identifier.

#### Scenario: Expired approval is ignored
- **WHEN** a matching persisted approval has passed its expiration timestamp
- **THEN** the system treats the approval as invalid and requires a new decision

#### Scenario: Revoked approval is not reused
- **WHEN** an approval has been revoked for the matching scope
- **THEN** the system SHALL NOT apply it to subsequent requests
2 changes: 1 addition & 1 deletion src/cli/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ function getPolicyOutcome(result: ToolResult): {
reason: string;
sideEffect: ToolInvocation['sideEffect'];
} | null {
const candidate = result.payload.policyOutcome;
const candidate = result.payload.policyOutcome ?? result.payload.policyOutcomeLegacy;
if (!candidate || typeof candidate !== 'object') {
return null;
}
Expand Down
3 changes: 3 additions & 0 deletions src/policy/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ export function createDefaultApprovalPolicy(
commandAllowlist: ['ls', 'cat', 'rg', 'find', 'pwd', 'git status'],
pathAllowlist: [],
automationWriteAllowlist: [],
pathAllowlistByOperation: {},
allowlistPolicyOperations: ['write'],
approvalTtlMs: 10 * 60 * 1000,
blockedCommandPatterns: ['rm -rf /', 'mkfs', 'dd if=', ':(){ :|:& };:'],
...overrides,
};
Expand Down
Loading