14 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 51 | Generalize session approvals to all permission surfaces with wildcard patterns |
Generalize session approvals to all permission surfaces
Problem Statement
Session-scoped approvals currently work only for external_directory.
When working in a session, the user is still prompted repeatedly for the same class of bash command, MCP tool, or skill load.
There is no way to say "yes, allow git status* for the rest of this session" without changing the on-disk policy to allow.
The SessionRules infrastructure (#57) and unified evaluate path (#65) are both landed.
The remaining work is to wire session approvals into all permission surfaces and add pattern suggestion logic.
Goals
- Extend session approvals to all permission surfaces: bash, mcp, skills, tools.
- Each surface suggests an approval pattern when prompting (e.g., bash suggests
git *, MCP suggestsserver:*). - Show the suggested pattern in the dialog "for session" option label.
- Record
resolution: "session_approved"with the matched pattern in the review log when a future check hits a session rule. - Handle session-hit detection in
checkPermission()for all surfaces (not justexternal_directory).
Non-Goals
- Bash arity table for smarter pattern suggestions (#52 — follow-up).
~/$HOMEexpansion in patterns (#53 — follow-up).- Persisting session approvals to disk ("Always" across sessions — future work).
- Per-agent scoping of session approvals.
- "Deny for session" option — defer unless demand emerges.
Background
Current state
| File | Role |
|---|---|
src/session-rules.ts |
SessionRules class with approve(surface, pattern) + deriveApprovalPattern() for paths |
src/rule.ts |
Rule, Ruleset, evaluate() — pure last-match-wins decision engine |
src/permission-manager.ts |
checkPermission() — accepts sessionRules param but only checks them for external_directory |
src/permission-gate.ts |
applyPermissionGate() — deny/ask/allow branching via callbacks |
src/permission-dialog.ts |
Dialog options including "Yes, for this session" → approved_for_session state |
src/permission-prompts.ts |
User-facing message formatting per surface |
src/handlers/tool-call.ts |
Consumes gates; has session pre-check and approval recording for external_directory only |
What's already working
external_directorysession approvals: pre-check viacheckPermission(..., sessionRules), approval recording viaderiveApprovalPattern(), review log withsession_approved.SessionRules.approve(surface, pattern)stores any surface/pattern pair.evaluate()matches session rules by wildcard — the engine is surface-agnostic.
What's missing
checkPermission()doesn't passsessionRulestoevaluate()for bash/mcp/skill/tool surfaces.- No pattern suggestion logic for non-directory surfaces.
- The "for session" recording in
tool-call.tsis only wired forexternal_directory. - The dialog "for session" label is static — doesn't show what pattern will be approved.
Design Overview
1. Extend checkPermission() to check session rules for all surfaces
Currently, the session check is inside the external_directory branch only.
Extend it to bash, mcp, skill, and tool branches:
// For each surface branch, after building composedRules:
if (sessionRules && sessionRules.length > 0) {
const sessionRule = evaluate(surface, value, sessionRules);
if (sessionRules.includes(sessionRule)) {
return {
toolName,
state: "allow",
matchedPattern: sessionRule.pattern,
source: "session",
// surface-specific fields as needed
};
}
}
This is a uniform pattern already established for external_directory.
Architectural note: the target architecture (docs/architecture/target-architecture.md) envisions session rules composed directly into the main ruleset array (highest priority at the end) so evaluate() is called once with no separate pre-check.
The current implementation passes sessionRules as a separate parameter.
This plan extends the current pattern to all surfaces (least-risk path); a future refactor can inline session rules into the composed array for a single evaluate() call per surface.
2. Pattern suggestion module
New file src/pattern-suggest.ts — pure functions, no IO:
export interface SessionApprovalSuggestion {
surface: string;
pattern: string;
label: string; // Human-readable label for dialog
}
export function suggestSessionPattern(
surface: string,
value: string,
input?: unknown,
): SessionApprovalSuggestion;
Per-surface heuristics
| Surface | Input | Suggested pattern | Example |
|---|---|---|---|
| bash | git status --short |
git * |
First word + * |
| bash (no args) | ls |
ls |
Exact command |
| mcp (qualified) | exa:search |
exa:* |
Server prefix + :* |
| mcp (munged) | exa_search |
exa_* |
Server prefix + _* |
| mcp (bare) | mcp |
* |
Wildcard |
| skill | librarian |
librarian |
Exact skill name |
| tool (read, write, etc.) | read |
* |
All uses of this tool surface |
| external_directory | /tmp/foo.txt |
/tmp/* |
deriveApprovalPattern() |
Bash heuristic: split on first space → <command> *.
This is intentionally conservative — git * is broader than ideal but visible in the dialog.
The arity table (#52) will refine this later.
3. Wire session approvals in tool-call.ts
The normal tool permission gate section already calls checkPermission().
Changes needed:
- Pass
sessionRulesto the normal-toolcheckPermission()call (it's only passed in theexternal_directorybranch today). - Detect
source === "session"in the result → logsession_approved, skip the gate. - Compute
suggestSessionPattern()before calling the gate. - After gate returns with
decision.state === "approved_for_session", callsessionRules.approve(surface, pattern).
4. Dynamic dialog label
Update the "for session" option to show the pattern:
Agent 'default' requested bash command 'git status --short'. Allow?
● Yes
● Yes, allow "git *" for this session
● No
● No, provide reason
This requires requestPermissionDecisionFromUi() to accept a dynamic session label or the suggestion pattern.
Approach: add an optional sessionLabel?: string parameter that overrides the default APPROVE_FOR_SESSION_OPTION when provided.
5. Review log entries
When session rule matches a future check:
{
"event": "permission_request.session_approved",
"resolution": "session_approved",
"surface": "bash",
"value": "git status --short",
"sessionApprovalPattern": "git *"
}
When user selects "for session":
{
"event": "permission_request.approved",
"resolution": "approved_for_session",
"sessionApprovalPattern": "git *"
}
6. Gate extension
Add optional sessionApproval data to PermissionGateParams and PermissionGateResult:
export interface PermissionGateParams {
// ... existing ...
sessionApproval?: { surface: string; pattern: string; label: string };
}
export type PermissionGateResult =
| { action: "allow"; sessionApproval?: { surface: string; pattern: string } }
| { action: "block"; reason: string };
When the promptForApproval callback returns approved_for_session and sessionApproval is provided, the gate attaches it to the result.
The caller inspects it and records into SessionRules.
Module-Level Changes
src/pattern-suggest.ts (new)
suggestSessionPattern(surface, value, input?)→SessionApprovalSuggestion.suggestBashPattern(command)— first-word heuristic.suggestMcpPattern(target)— server-level prefix wildcard.- Pure functions, fully testable.
src/permission-manager.ts (modified)
- Add session rule evaluation to the bash branch, mcp branch, skill branch, and tool branch — same pattern as
external_directory.
src/permission-gate.ts (modified)
- Add
sessionApproval?toPermissionGateParams. - Extend
PermissionGateResultallow variant with optionalsessionApproval. - Gate attaches
sessionApprovalwhendecision.state === "approved_for_session".
src/permission-dialog.ts (modified)
requestPermissionDecisionFromUi()accepts optionalsessionLabelto customize the "for session" option.
src/handlers/tool-call.ts (modified)
- Pass
sessionRulesto the normal-toolcheckPermission()call. - Detect
source === "session"→ log + skip gate. - Compute
suggestSessionPattern()and pass to gate. - On
sessionApprovalin result →sessionRules.approve(...).
src/permission-prompts.ts (modified)
- Add
formatSessionOptionLabel(pattern)→'Yes, allow "<pattern>" for this session'.
tests/pattern-suggest.test.ts (new)
- Unit tests for all pattern suggestion heuristics.
tests/permission-manager.test.ts (modified)
- Test session rule evaluation for bash, mcp, skill, and tool surfaces.
tests/permission-gate.test.ts (modified)
- Test
sessionApprovalpass-through onapproved_for_session.
tests/handlers/tool-call.test.ts (modified)
- Test session-hit detection across surfaces.
- Test session recording on "for session" approval.
TDD Order
-
test: pattern suggestion unit tests for all surfaces
- Red: tests for
suggestBashPattern,suggestMcpPattern,suggestSessionPattern. - Green: implement
src/pattern-suggest.ts. - Commit:
feat: add pattern-suggest module for session approval patterns
- Red: tests for
-
test: checkPermission returns session hit for bash/mcp/skill/tool
- Red: tests that
checkPermission("bash", { command: "git status" }, agent, sessionRules)returns{ source: "session" }when session rules contain a matching bash rule. - Green: extend the bash/mcp/skill/tool branches in
checkPermission(). - Commit:
feat: extend checkPermission session evaluation to all surfaces
- Red: tests that
-
test: gate attaches sessionApproval on approved_for_session
- Red: test that gate returns
{ action: "allow", sessionApproval: {...} }when decision isapproved_for_sessionand params includesessionApproval. - Green: extend
PermissionGateParamsandPermissionGateResult, wire in gate logic. - Commit:
feat: extend permission gate with sessionApproval pass-through
- Red: test that gate returns
-
test: dialog shows dynamic session label
- Red: test that
requestPermissionDecisionFromUi()passes the custom session label toui.select(). - Green: add
sessionLabelparam. - Commit:
feat: dynamic session approval label in permission dialog
- Red: test that
-
feat: wire session approvals into tool-call handler for all surfaces
- Pass
sessionRulesto the normalcheckPermission()call. - Detect
source === "session"→ logsession_approved, skip gate. - Compute
suggestSessionPattern()and pass to gate params. - Record
sessionRules.approve(...)when result carriessessionApproval. - Update existing tests that assert on the gate call to account for new params.
- Commit:
feat: generalize session approvals to all permission surfaces (#51)
- Pass
-
docs: update README with session approval behavior
- Document pattern suggestion behavior per surface.
- Commit:
docs: document generalized session approvals (#51)
Risks and Mitigations
| Risk | Mitigation |
|---|---|
Bash pattern too broad (git * allows git push --force) |
Pattern is shown in dialog label. User sees what they approve. #52 refines with arity table. |
MCP server-level pattern (exa:*) allows all tools on server |
Shown in dialog. Users can decline for per-tool prompting. |
| Session allow overrides a config deny | Session rules use evaluate() where last-match-wins. Session rules should NOT override explicit deny. Fix: in checkPermission(), only check session rules when config result is ask (not deny). |
| Could this silently weaken a permission? | No. Every session rule requires explicit user approval via dialog. Pattern is visible in the label. No rule added without user action. Deny rules are not overridable by session. |
Tool surface * pattern too permissive |
For tools like write/edit, approving * means "allow all writes." This matches the granularity of the tool-level config. Path-specific patterns are a follow-up. |
Open Questions
- Should "for session" for write/edit tools suggest a path-based pattern (e.g.
src/*) instead of blanket*? Leaning no — the tool permission surface doesn't currently match on file paths, only tool names. Path-based session rules would need a new surface or evaluation mode. - Should session approvals be blocked from overriding explicit
denyrules? Yes — the implementation should only offer "for session" when the config state isask, notdeny. Adenybypasses the gate entirely (no prompt shown).