Skip to content

feat(as): P7 approvals & security — ApprovalManager + permission modes + rule persistence#448

Merged
Luis85 merged 44 commits into
nextfrom
feature/approvals-security
May 26, 2026
Merged

feat(as): P7 approvals & security — ApprovalManager + permission modes + rule persistence#448
Luis85 merged 44 commits into
nextfrom
feature/approvals-security

Conversation

@Luis85
Copy link
Copy Markdown
Owner

@Luis85 Luis85 commented May 26, 2026

P7 — Approvals & Security (claudian-reboot)

The approval decision/rules engine + permission management on the P1–P6 surface (charter §3.9). Spec: specs/approvals-security/ (PRD-AS-001, DESIGN-AS-001, SPEC-AS-001..028, ADR-AS-001..003, TASKS-AS-001 = 40 tasks).

Delivered (T-AS-001..033 + gate)

  • Permission modes normal/plan/yolo (claudian PermissionMode) — the P6 permission-toggle seam is now live (3-mode), per-tab, folded into the turn via the P6 foldControlOptions (additive ChatRuntimeQueryOptions.permissionMode?, non-normal only).
  • ApprovalManager (application use case) — mode-gate (yolo→allow, plan→gate) → pure rule match (deny-wins, claudian matchesRulePattern semantics: bash explicit-wildcard-only, path-segment boundaries) → auto-decide OR the unchanged P4 inline prompt → an *-always decision persists a rule. Fail-safe-to-prompt on store error (never silent auto-allow).
  • ApprovalRuleStorePort + APPROVAL_RULE_STORE_PORTdevice-local (saveLocalStorage, never data.json/vault, no migration; ADR-AS-001 / CHARTER-REQ-SET). 3 bridges (Obsidian device-local coverage-excluded / Mock scriptable / LS localStorage).
  • UI — live PermissionToggle, ApprovalsPanel + ApprovalRuleRow, InlinePlanApproval +deny-always, the ApprovalGateRuntime decorator wiring the engine into the P4 approval path (degrades to byte-identical P4 always-prompt when the store port is absent). status-panel/permission-toggle --sp-* tokens; en+de i18n.

Gate (local, green)

vue-tsc 0 · eslint 0 · vitest 1574 passed (+ a new en↔de locale key-parity guard) · build + build:web + docs:api clean · npm audit --audit-level=high clean.

Parity self-review (Stage 9)

review.md approve-with-conditions. Security correctness confirmed: fail-safe-to-prompt, deny-wins, matcher safety ("git *"github, /a/b/a/bc), device-local-not-vault. Live wiring + provide verified. The one HIGH finding (R-AS-001: en.ts missing the permission.mode labels → raw keys in English) is fixed, plus a locale key-parity test added (guards P8–P12). CLAR-AS-006 (structured action-pattern vs the frozen P4 ApprovalRequest) ruled P3 deferral — the CLI transport emits no live approval_request, so req.context matching is correct; carried to the interactive-transport phase.

Deferred — final epic gate

Manual legs TEST-AS-M1 (real device-local store round-trip + data.json/vault untouched), TEST-AS-M3 (real Claude SDK-mode mapping + plan-exit setMode + plan edit-gating), TEST-AS-M2 (parity screenshots), the live npm run dev flow, and CLAR-AS-006.

🤖 Generated with Claude Code

Symprowire and others added 30 commits May 26, 2026 00:03
Cut feature/approvals-security off next (P0-P6 merged). Scope = parity-charter
§3.9 — ApprovalManager + permission updates + approval rules + persistence; backs
the P6 permission-toggle seam; consumes the P4 inline approval blocks. Key ADR:
ApprovalRuleStorePort persistence (device-local, CHARTER-REQ-SET). Autonomous
full-epic drive (P7→P12). Next: /spec:requirements.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
35 EARS REQ-AS (permission mode / rules+matching / decision flow / persistence /
status UI / a11y+additivity) + 16 NFR-AS, each mapped to a claudian path + TEST-AS
id. Permission modes = normal/plan/yolo (claudian PermissionMode, all Claude-backed).
Rule model {toolName, action-pattern?, decision allow|deny, lifetime} with claudian
matchesRulePattern semantics + explicit deny. Persistence (CLAR-AS-001→ADR-AS-001):
dedicated ApprovalRuleStorePort, device-local (ADR-PSR-002), no migration. Additive
default = P4 always-prompt path (REQ-AS-052). CLAR-AS-001..005 resolved-by-recommendation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ADR-AS-001 ApprovalRuleStorePort (store-only narrow port) + ApprovalRule DTO +
pure domain matcher (claudian matchesRulePattern semantics) + device-local backing
(saveLocalStorage, no data.json/vault, no migration), fail-safe-to-prompt.
ADR-AS-002 additive ChatRuntimeQueryOptions.permissionMode? + TabControls.permissionMode?
(folded non-normal only by P6 foldControlOptions); ToolbarCapabilities.permissionMode
widens to live normal|plan|yolo; SDK mapping in the Claude runtime, no providerId branch.
ADR-AS-003 application ApprovalManager decision flow (mode-gate → match → unchanged
P4 prompt → persist; deny-wins; +deny-always). No-rule+normal = byte-identical P4.
Components: PermissionToggle(live)/ApprovalsPanel/ApprovalRuleRow/InlineApproval.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 layer groups, 28 SPEC items. Pins PermissionMode (normal|plan|yolo), additive
ChatRuntimeQueryOptions.permissionMode?/TabControls.permissionMode? (fold non-normal),
ApprovalDecision +deny-always (P4 byte-identical), ToolbarCapabilities.permissionMode
widen, ApprovalRule DTO + ApprovalRuleStorePort + APPROVAL_RULE_STORE_PORT (device-local),
the pure matcher (claudian matchesRulePattern: bash explicit-wildcard-only, path-segment
boundaries, deny-wins), ApprovalManager.decide (mode-gate→match→prompt→persist,
fail-safe-to-prompt). Manual legs TEST-AS-M1/M2/M3 + plan-gate. Full coverage table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T-AS-001 baseline+guard-verify (no guard-relax needed — verified); DOMAIN 002..011,
INFRA 012..015, APPLICATION 016..019, UI 020..029, STYLES 030, WIRE-IN 031..033,
GATE 034..040. RED(qa)→green(dev) per contract; ToolbarCapabilities.permissionMode
widen lands its implements fan-out (3 runtimes + EnqueueRuntime + ScriptedRuntime
doubles) in T-AS-011 (build-green discipline). Coverage-excluded Obsidian device-local
store + Claude SDK-map/setMode → manual legs T-AS-036/037/038 (TEST-AS-M1/M2/M3).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Scaffold the P7 parity-screenshot matrix (baseline column from claudian-main
ApprovalManager/ClaudeApprovalHandler/ClaudePermissionUpdates +
permission-toggle.css/status-panel.css), the test-plan (guard-verify note +
manual legs TEST-AS-M1/M2/M3 + DOMAIN-batch status), and the implementation-log.
Confirms APPROVAL_RULE_STORE_PORT + the new approvals domain/app/ui paths are not
guard-banned (no relaxation task). No src/ change.

NFR-AS-012 NFR-AS-001 SPEC-AS-004/012/013/015/020/026.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…provalDecision

Failing structural/serialisation legs: PermissionMode = 'normal'|'plan'|'yolo'
(closed union + barrel surface); ChatRuntimeQueryOptions.permissionMode? appended
after serviceTier with a P6-shaped query byte-identical; TabControls.permissionMode?
appended; ApprovalDecision grown to the four-member union (deny-always) with the
P4 members + ApprovalRequest/ApprovalOption byte-identical. vue-tsc fails RED.

TEST-AS-001 TEST-AS-002 TEST-AS-016 SPEC-AS-001/002/003/021
REQ-AS-001/002/006/016/052 NFR-AS-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…alDecision

Add src/domain/chat/PermissionMode.ts ('normal'|'plan'|'yolo' closed union);
append permissionMode?: PermissionMode after serviceTier on ChatRuntimeQueryOptions
+ TabControls; grow the ApprovalDecision union by 'deny-always'; re-export
PermissionMode from the ports barrel. Purely additive — no implements break (the
runtimes read the optional field; the union grows additively). The P4
inlineBlockDtos union-exactness assertion is updated to the grown four-member
union (the union-grow fan-out). Greens TEST-AS-001/002/016; whole-project vue-tsc 0.

SPEC-AS-001/002/003/021 REQ-AS-001/002/006/016/052 NFR-AS-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Failing unit tests for getActionPattern/getActionDescription/matchesRulePattern
(@/domain/chat/approvals/ApprovalMatcher): the full SPEC-AS-026 table — per-tool
pattern/description derivation, no-rule/'*'/exact, the null-action guard (EC-AS-9),
bash explicit-wildcard only ("git *"↦"git status" yes, "git"↦"git status" no,
"npm:*"↦"npm install" yes, "git *"↦"github" no — EC-AS-7), file path-segment
boundary ("/a/b"↦"/a/b/c" yes, ↦"/a/bc" no, trailing-/ subtree, \->/ normalise —
EC-AS-8), other-tool simple prefix, and never-throws. Module missing → RED.

TEST-AS-010/011/012/013/014/015 SPEC-AS-004/026 REQ-AS-010..015 NFR-AS-009
EC-AS-7/8/9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…matchesRulePattern

Add src/domain/chat/approvals/ApprovalMatcher.ts ported verbatim from claudian
core/security/ApprovalManager.ts: the seven tool-name constants;
getActionPattern (string|null), getActionDescription (string), matchesRulePattern
(boolean) with the private isPathPrefixMatch + matchesBashPrefix helpers — \->/
normalise, no-rule/'*' match-all, exact, bash explicit-wildcard-only, file
path-segment boundary, other-tool simple prefix, the null-action guard. Pure +
total, never throws (NFR-AS-009); string comparison only, no eval/exec
(NFR-AS-002). Barrel re-export added. Greens TEST-AS-010/011/012/013/014/015.
Two targeted complexity disables (irreducible per-tool dispatch, justified).

SPEC-AS-004/026 REQ-AS-010..015 NFR-AS-002/009.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…upeKey

Failing structural + dedupe-key tests: the six readonly members
(id/toolName/actionPattern?/decision:'allow'|'deny'/lifetime:'session'|'persisted'/
createdAt); ApprovalRuleInput = Omit<ApprovalRule,'id'|'createdAt'>; ruleDedupeKey
returns the `${toolName} ${actionPattern ?? ''} ${decision}` triple (absent vs ''
collapse, opposite decision distinct); no secret/token field; barrel re-export.
Module missing → RED.

TEST-AS-016 SPEC-AS-005/024 REQ-AS-016/030/031 NFR-AS-002/008.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add src/domain/chat/approvals/ApprovalRule.ts: the six readonly members
(id/toolName/actionPattern?/decision:'allow'|'deny'/lifetime:'session'|'persisted'/
createdAt), ApprovalRuleInput = Omit<ApprovalRule,'id'|'createdAt'>, and
ruleDedupeKey returning the `${toolName} ${actionPattern ?? ''} ${decision}`
triple (pure, string-only). Plain inert DTO — no secret/token field, no class, no
obsidian/node/Vue (NFR-AS-002/008). Barrel re-export added. Greens TEST-AS-016
DTO leg (6/6).

SPEC-AS-005/024 REQ-AS-016/030/031 NFR-AS-002/008.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…RT key + barrel

Failing structural tests: the four Result-typed methods (loadRules: Promise<Result
<readonly ApprovalRule[]>>, addRule: (input)=>Promise<Result<ApprovalRule>>,
removeRule: (id)=>Promise<Result<void>>, clear: ()=>Promise<Result<void>>); the own
APPROVAL_RULE_STORE_PORT InjectionKey; the @/domain/ports barrel re-exports of the
port + ApprovalRule/ApprovalRuleInput/PermissionMode. vue-tsc fails RED.

TEST-AS-053 SPEC-AS-006 REQ-AS-001/032/033/034/053 NFR-AS-005.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ey + barrel

Add src/domain/ports/ApprovalRuleStorePort.ts (loadRules/addRule/removeRule/clear,
all Promise<Result<...>>, store-only persisted lifetime, documented per-method
contract: load-or-default, dedupe-by-ruleDedupeKey, idempotent remove,
fail-safe-via-err); add the APPROVAL_RULE_STORE_PORT InjectionKey to bridge/ports
(own key, no aggregate); re-export the port + ApprovalRule/ApprovalRuleInput from
the @/domain/ports barrel (PermissionMode already re-exported in T-AS-003). Greens
TEST-AS-053 port-shape leg (2/2); deleted-symbol guard green (new key/port resolve
clean, no relaxation).

SPEC-AS-006 REQ-AS-001/032/033/034/053 NFR-AS-005/010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…itivity

Extend tests/domain/ports/ChatRuntimePort.ts.test.ts: assert
ToolbarCapabilities.permissionMode is WIDENED from 'default'|'plan' to the live
PermissionMode ('normal'|'plan'|'yolo'), the P6 'default' value mapping to
'normal'; the four other ToolbarCapabilities flags + the five RuntimeCapabilities
flags + the P0-P6 ChatRuntimePort members stay byte-identical; all three live modes
representable. vue-tsc fails RED (permissionMode still narrow 'default'|'plan').

TEST-AS-001 TEST-AS-021 SPEC-AS-006/021 REQ-AS-003 NFR-AS-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nts fan-out

Widen ToolbarCapabilities.permissionMode from 'default'|'plan' to PermissionMode
('normal'|'plan'|'yolo') in ChatRuntimePort (importing from PermissionMode; the four
other ToolbarCapabilities flags + the five RuntimeCapabilities flags + the P0-P6
members byte-identical). In the SAME commit, map the P6 'default' -> 'normal' on
every getToolbarCapabilities() impl that implements ChatRuntimePort — the three
runtimes (MockChatRuntime, FixtureChatRuntime, ClaudeCliChatRuntime) + the two
ScriptedRuntime test doubles (RunChatTurnUseCase.test/.rr.test) + the P6 capability
fixtures (buildToolbarViewModel, MockToolbarCapabilities, LocalStorageToolbar,
main.ts) — so vue-tsc + lint + the suite stay green (the P6 T-TC-008 lesson).
EnqueueRuntime forwards getToolbarCapabilities() verbatim — no change. No
providerId branch; synchronous + total. Greens TEST-AS-001 capabilities-shape +
TEST-AS-021 additivity (68/68 across the affected files).

SPEC-AS-006/021 REQ-AS-003 NFR-AS-001/005.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tate)

Log T-AS-002..011 entries (RED/green commits, files, verify, deviations) + the
DOMAIN-batch close-out (vue-tsc 0, lint 0, vitest tests/domain 116/116, additivity
proven). Advance workflow-state to stage 7 implementation in-progress; record the
DOMAIN-batch hand-off to the INFRA batch (T-AS-012..015). implementation-log.md +
test-plan.md set in-progress (INFRA/APP/UI/GATE batches remain).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… gate

The P6 gate committed a stale styles.css built BEFORE the tokens.css lightningcss
comment fix — the old slash/brace-laden §4.13 comment had confused the plugin
build's CSS processor, mangling `.specorator-root {` into invalid
`.specorator-root) {` with selector text bled into the comment. This commits the
clean rebuild (from the fixed tokens.css; the P6 testvault deploy already shipped
this clean version). Corrects the artifact on next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…de SDK mode mapping + plan-exit setMode

Implements SPEC-AS-007 (coverage-excluded src/infrastructure/obsidian/**):
- ObsidianApprovalRuleStore backs ApprovalRuleStorePort on the device-local
  store under 'specorator:approval-rules' (app.saveLocalStorage/loadLocalStorage,
  ADR-PSR-002 pattern) — never data.json, never a vault file (NFR-AS-003,
  REQ-AS-034). loadRules is load-or-default (missing/unparseable -> ok([]),
  malformed entries dropped); addRule dedupes by ruleDedupeKey + mints
  id/createdAt; removeRule idempotent; clear; all Result-typed, total (never
  throws -> tryAsync). Exposed via get approvalRuleStore on ObsidianBridge.
- ClaudeCliChatRuntime maps queryOptions.permissionMode to the SDK
  --permission-mode string (yolo->bypassPermissions / plan->plan /
  normal/absent->no flag); records the live mode so
  getToolbarCapabilities().permissionMode reflects it; on plan-exit syncs the
  session-scoped mode (parity ClaudeApprovalHandler setMode destination:session).
  No providerId branch (SPEC-AS-023, NG6).

Behavioural gate is the MANUAL legs TEST-AS-M1/M3 (scheduled in test-plan.md);
not self-claimed. REQ-AS-002/004/005/030/034/053, NFR-AS-003.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e mode + fake-ports member

Authors the failing unit tests for SPEC-AS-008:
- MockApprovalRuleStore.test.ts — the scriptable in-memory store (seedRules,
  loadRules-default-ok([]), addRule mint + dedupe-by-ruleDedupeKey + opposite-
  decision append, idempotent removeRule, clear, setFailMode('load'|'save'|'none')
  forcing Result.err, never-throws) + the MockBridge.approvalRuleStore accessor.
- MockApprovalRuntimeMode.test.ts — MockChatRuntime records the last query's
  permissionMode (getLastPermissionMode) + the scriptable
  getToolbarCapabilities().permissionMode three-mode representability.
- fake-ports.test.ts — the approvalRuleStore factory member (seedable + setFailMode).

RED confirmed: MockApprovalRuleStore module + MockBridge.approvalRuleStore +
getLastPermissionMode + the fake-ports member do not yet exist.

Traces: TEST-AS-002/003/006/020/021/030/032/033/040/053/054, SPEC-AS-008,
REQ-AS-020/021/032/053/054, NFR-AS-010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ime mode + fake-ports member

Greens the T-AS-013 RED tests (SPEC-AS-008):
- MockApprovalRuleStore — scriptable in-memory store: seedRules pre-populates;
  loadRules defaults ok([]); addRule mints id/createdAt + dedupes by
  ruleDedupeKey (same-triple no-op ok(existing), opposite-decision appended);
  removeRule idempotent; clear; setFailMode('load'|'save'|'none') forces
  Result.err for the fail-safe-to-prompt driver (TEST-AS-054); total, never
  throws (NFR-AS-010). Exposed via get approvalRuleStore on MockBridge.
- MockChatRuntime records the last query's permissionMode (getLastPermissionMode,
  TEST-AS-002); the scriptable getToolbarCapabilities().permissionMode covers the
  three-mode representability (TEST-AS-003/006/040).
- fake-ports.ts gains the approvalRuleStore member (seedable + setFailMode) so
  multi-port ApprovalManager + panel tests see it.

Runnability fix to the T-AS-013 RED fixture (no assertion change): ChatTurnRequest
has no conversationId; the drain loop no longer binds an unused chunk.

No node:*/obsidian in Mock. vitest run 32/32 green.

Traces: TEST-AS-002/003/006/020/021/030/032/033/053/054, SPEC-AS-008,
REQ-AS-020/021/032/053/054, NFR-AS-010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ePort + inert runtime mode

Implements SPEC-AS-009 (RED leg authored first, then greened):
- LocalStorageApprovalRuleStore backs ApprovalRuleStorePort on browser
  localStorage under the same key as the Obsidian device-local store
  ('specorator:approval-rules') so the GitHub Pages demo persists rules across a
  reload with no Obsidian runtime (REQ-AS-053). loadRules is load-or-default
  (missing/unparseable/corrupt -> ok([]), malformed entries dropped); addRule
  dedupes by ruleDedupeKey + mints id/createdAt; removeRule idempotent; clear;
  all Result-typed, never throws across the boundary (NFR-AS-010). Exposed via
  get approvalRuleStore on LocalStorageBridge.
- The runtime mode is inert: FixtureChatRuntime reports permissionMode 'normal'
  (T-AS-011) and fires no live setMode (no live SDK); the toggle/panel still
  reflect the per-tab mode draft via the fold.

No node:*. vitest run 8/8 (store) green; full infra+fakes surface 346/346 green.

Traces: TEST-AS-053 (LocalStorage round-trip leg), SPEC-AS-009, REQ-AS-053,
NFR-AS-010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…kflow-state)

Records the INFRA batch (T-AS-012..015) in implementation-log.md (per-task
entries + batch close-out), ticks the T-AS-012..015 DoD boxes in tasks.md, and
updates workflow-state.md (Stage 7 row, implementation-log.md artifact status,
INFRA-batch hand-off note -> APPLICATION batch T-AS-016..019). Manual legs
TEST-AS-M1/M3 remain scheduled for the final epic-review gate (not self-claimed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the failing fold-leg cases: non-normal ('plan'/'yolo') folded, the
'normal'/absent guard folds nothing (EC-AS-2/13 byte-identical P6), the
P6-clause byte-identity, and never-throws.
TEST-AS-002 (fold leg). SPEC-AS-011 §3. REQ-AS-002/052. NFR-AS-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tions

Writes folded.permissionMode only when present AND non-'normal', so a
no-rule/normal tab folds nothing -> byte-identical P6 (EC-AS-2/13). The
return type widens by the one optional permissionMode key; the P6
model/mode/reasoning/serviceTier clauses + behaviour stay byte-identical.
Pure + total; no providerId branch.
Implements SPEC-AS-011 §3. REQ-AS-002/052. NFR-AS-001. TEST-AS-002 (fold leg).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the failing use-case matrix over the scriptable Mock store + a
scripted mode: mode-gate-first (yolo auto-allow no-lookup / plan defer /
normal continue), load-await + match deny-wins, fail-safe-to-prompt on a
store err (notice, never auto-allow, no rule content logged, never throws),
applyDecision (session vs persisted, {-leading JSON-fallback stored without
actionPattern, dedupe, null cancel), listRules persisted-union-session, the
bash/path matcher edges (EC-AS-7/8), and the no-stale-snapshot re-read.
TEST-AS-003/004/020/021/023/025/030/031/032/033/052/054. SPEC-AS-010/023/027/028.
REQ-AS-004/005/020..025/030/031/052/054. NFR-AS-004/009. EC-AS-1/3/5/6/10/11/12/16/20.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
decide(action, mode): mode-gate-FIRST (yolo->ok('allow') no lookup /
plan->ok('prompt') defer to the P4 exit-plan gate / normal->continue) ->
await store.loadRules via tryAsync (err -> log no-content + storeError
notice + ok('prompt'), never auto-allow) -> match persisted-union-session
via the pure matcher (deny-wins -> ok('deny'), else allow -> ok('allow'),
else ok('prompt')). applyDecision: allow/deny -> in-memory session rule
(dedupe by ruleDedupeKey); allow-always/deny-always -> store.addRule
persisted (the {-leading JSON-fallback stored WITHOUT actionPattern); null
-> cancel. listRules -> persisted-union-session, Result-typed. No providerId
branch; never throws across the callback boundary; logs no rule content.

Also folds the lint fix to the T-AS-018 RED test (unnecessary optional
chains on non-nullish index access) so the file lints clean — no assertion
change.

Implements SPEC-AS-010/023/025/027/028. REQ-AS-004/005/020..025/030/031/052/054.
NFR-AS-002/004/009. ADR-AS-003.
TEST-AS-003/004/020/021/023/025/030/031/032/033/052/054. EC-AS-1/3/5/6/10/11/12/16/20.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TEST-AS-053 (composable leg). SPEC-AS-018 §4. REQ-AS-040/042/053, NFR-AS-005.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements SPEC-AS-018 §4 (inject-or-throw, ADR-008 one-port-one-composable).
REQ-AS-040/042/053, NFR-AS-005/006. Greens TEST-AS-053 composable leg.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TEST-AS-001/002/003/006/050/051 (A legs). SPEC-AS-012 §4.
REQ-AS-001/002/003/006/050/051, NFR-AS-006/013/015.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symprowire and others added 14 commits May 26, 2026 01:51
Implements SPEC-AS-012 §4 (live normal/plan/yolo control, additive over the P6
honest-defer seam) + SPEC-AS-022 i18n (en+de). Greens TEST-AS-001/002/003/006/050/051
A legs. REQ-AS-001/002/003/006/050/051, NFR-AS-006/007/013/015.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TEST-AS-040/041/042/043/050/051 (A legs). SPEC-AS-013/014 §4.
REQ-AS-040/041/042/043/050/051, NFR-AS-006/013/015.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements SPEC-AS-013/014 §4 (status/approvals surface + one-rule row, live,
remove-by-id). Greens TEST-AS-040/041/042/043/050/051 A legs. SPEC-AS-022 i18n.
REQ-AS-040/041/042/043/050/051, NFR-AS-006/007/013/015.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TEST-AS-016 (option-row leg), TEST-AS-022 (four-option-row leg), TEST-AS-025 (cancel
leg). SPEC-AS-015/018 §4. REQ-AS-022/025/030, NFR-AS-006/007/015.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements SPEC-AS-015/018 §4 (the fourth deny-always option arrives via
request.options; an additive data-decision attribute targets it; render byte-identical
to P4, NG4). Greens TEST-AS-016 option-row/022/025 legs. REQ-AS-022/025/030,
NFR-AS-006/007/015.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ssionMode

TEST-AS-002 (store-fold leg), TEST-AS-006/020/021/022/025/040/042/043 (surface legs).
SPEC-AS-016/017/023/028 §4. REQ-AS-002/004/005/006/020..025/040/043, NFR-AS-008,
EC-AS-17/18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements SPEC-AS-016/017/023/028 §4. A new ApprovalGateRuntime decorator gates the
active runtime's approval callback through the per-surface ApprovalManager
(mode-gate -> match -> auto-decide OR the unchanged P4 prompt -> applyDecision);
degrades to the byte-identical P4 always-prompt path when APPROVAL_RULE_STORE_PORT is
absent. The PermissionToggle set + the ApprovalsPanel remove + the live mode thread
through ToolbarStrip/ChatComposer to tabs.setControl('permissionMode'). No providerId
branch. Greens TEST-AS-002 store-fold + TEST-AS-006/020/021/022/025/040/042/043 surface
legs. REQ-AS-002/004/005/006/020..025/040/043, NFR-AS-006/007/008, EC-AS-17/18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ate)

Tick T-AS-020..029 DoD checkboxes; record the UI-batch hand-off + Stage 7 progress in
workflow-state.md (implementation-log.md remains in-progress — STYLES/WIRE-IN/GATE +
manual legs remain). SPEC-AS-012..018.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mint the §4.14 approvals/security --sp-* token slice (lightningcss-safe
ASCII comment): --sp-approvals-row-gap, --sp-approvals-decision-allow,
--sp-approvals-decision-deny, --sp-permission-mode-active. Apply the
active-mode token to PermissionToggle; extend the tokens-contract test
with the §4.14 presence + leak guard.

Implements SPEC-AS-020. NFR-AS-012, TEST-AS-062.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the standalone approvals-panel mount leg (RED: main.ts does not yet
provide APPROVAL_RULE_STORE_PORT) + the structured-action-pattern gate leg
(RED: the gate derives from req.context, not getActionPattern over input).

SPEC-AS-019. TEST-AS-022/040/043/053, TEST-AS-032. REQ-AS-002/030/053.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AgentSidebarView provides ObsidianBridge.approvalRuleStore (device-local);
src/ui/main.ts provides MockBridge.approvalRuleStore. The ChatSurface gate
now runs live + the approvals panel mounts; absent → the P4 degrade. Greens
the standalone approvals-panel mount RED leg. The action-pattern follow-up
is escalated (CLAR-AS-006): threading structured input onto ApprovalRequest
conflicts with the frozen SPEC-AS-003 byte-identical-shape QA test. Also
records the T-AS-033 standalone smoke (deterministic leg automated; live-dev
deferred-manual).

Implements SPEC-AS-019. REQ-AS-002/030/053. NFR-AS-005/006. TEST-AS-022/040/043/053.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage-9 review (REVIEW-AS-001, approve-with-conditions) found en.ts (default
locale) missing the permission.mode.{normal,plan,yolo} labels PermissionToggle
renders → English UI showed raw keys (de.ts had them). Add the en labels.

Add a locale key-parity test (tests/ui/i18n/index.test.ts) asserting en and de
declare the EXACT same leaf key set — snapshotted at module-load so the sibling
i18nMerge mutation tests don't pollute it. Would have caught R-AS-001; guards the
i18n-heavy P8-P12. Also commits the Stage-9 review.md + traceability.md (CLAR-AS-006
deferred P3 per reviewer).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Shipped CSS rebuilt for P7 — ApprovalsPanel/ApprovalRuleRow + the live
PermissionToggle mode styling + the §4.14 status-panel/permission-toggle token
slice. Gate green: typecheck 0, lint 0, build + build:web + docs:api clean,
npm audit (high) clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Luis85 Luis85 merged commit 06734d5 into next May 26, 2026
6 checks passed
@Luis85 Luis85 deleted the feature/approvals-security branch May 26, 2026 00:46
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