zelari-code 2.54.0 → 2.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli/app.js +2 -2
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/commands/evolve.js +247 -0
- package/dist/cli/commands/evolve.js.map +1 -0
- package/dist/cli/commands/replay.js +202 -0
- package/dist/cli/commands/replay.js.map +1 -0
- package/dist/cli/commands/session.js +264 -0
- package/dist/cli/commands/session.js.map +1 -0
- package/dist/cli/commands/spineSession.js +77 -0
- package/dist/cli/commands/spineSession.js.map +1 -0
- package/dist/cli/components/StatusBar.js +6 -2
- package/dist/cli/components/StatusBar.js.map +1 -1
- package/dist/cli/components/statusChips.js +43 -0
- package/dist/cli/components/statusChips.js.map +1 -1
- package/dist/cli/councilDispatcher.js +12 -0
- package/dist/cli/councilDispatcher.js.map +1 -1
- package/dist/cli/evolution/controller.js +107 -0
- package/dist/cli/evolution/controller.js.map +1 -0
- package/dist/cli/evolution/ledger.js +42 -0
- package/dist/cli/evolution/ledger.js.map +1 -1
- package/dist/cli/headless/runOneTurn.js +18 -0
- package/dist/cli/headless/runOneTurn.js.map +1 -1
- package/dist/cli/hooks/useChatTurn.js +39 -1
- package/dist/cli/hooks/useChatTurn.js.map +1 -1
- package/dist/cli/hooks/useSlashDispatch.js +8 -0
- package/dist/cli/hooks/useSlashDispatch.js.map +1 -1
- package/dist/cli/inbox.js +63 -10
- package/dist/cli/inbox.js.map +1 -1
- package/dist/cli/inboxSources.js +173 -0
- package/dist/cli/inboxSources.js.map +1 -0
- package/dist/cli/main.bundled.js +9650 -6423
- package/dist/cli/main.bundled.js.map +4 -4
- package/dist/cli/main.js +80 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/plugins/bundleCommand.js +238 -0
- package/dist/cli/plugins/bundleCommand.js.map +1 -0
- package/dist/cli/plugins/bundleDiscover.js +74 -0
- package/dist/cli/plugins/bundleDiscover.js.map +1 -0
- package/dist/cli/plugins/bundleFs.js +57 -0
- package/dist/cli/plugins/bundleFs.js.map +1 -0
- package/dist/cli/plugins/bundleHookDef.js +70 -0
- package/dist/cli/plugins/bundleHookDef.js.map +1 -0
- package/dist/cli/plugins/bundleLoad.js +180 -0
- package/dist/cli/plugins/bundleLoad.js.map +1 -0
- package/dist/cli/plugins/bundleManifest.js +233 -0
- package/dist/cli/plugins/bundleManifest.js.map +1 -0
- package/dist/cli/plugins/bundleMcp.js +41 -0
- package/dist/cli/plugins/bundleMcp.js.map +1 -0
- package/dist/cli/plugins/bundleState.js +142 -0
- package/dist/cli/plugins/bundleState.js.map +1 -0
- package/dist/cli/runHeadless.js +40 -0
- package/dist/cli/runHeadless.js.map +1 -1
- package/dist/cli/safety/decisionEmit.js +43 -0
- package/dist/cli/safety/decisionEmit.js.map +1 -0
- package/dist/cli/safety/permissionGate.js +239 -0
- package/dist/cli/safety/permissionGate.js.map +1 -0
- package/dist/cli/safety/permissionPolicy.js +279 -0
- package/dist/cli/safety/permissionPolicy.js.map +1 -0
- package/dist/cli/safety/permissionRules.js +145 -0
- package/dist/cli/safety/permissionRules.js.map +1 -0
- package/dist/cli/safety/sessionSink.js +169 -0
- package/dist/cli/safety/sessionSink.js.map +1 -0
- package/dist/cli/slashCommands.js +28 -2
- package/dist/cli/slashCommands.js.map +1 -1
- package/dist/cli/slashHandlers/evolve.js +41 -0
- package/dist/cli/slashHandlers/evolve.js.map +1 -0
- package/dist/cli/slashHandlers/permissions.js +170 -0
- package/dist/cli/slashHandlers/permissions.js.map +1 -0
- package/dist/cli/statusline/inboxFeed.js +76 -0
- package/dist/cli/statusline/inboxFeed.js.map +1 -0
- package/dist/cli/statusline/statuslineItems.js +15 -0
- package/dist/cli/statusline/statuslineItems.js.map +1 -1
- package/dist/cli/toolRegistry.js +161 -7
- package/dist/cli/toolRegistry.js.map +1 -1
- package/dist/cli/tools/askUser.js +17 -2
- package/dist/cli/tools/askUser.js.map +1 -1
- package/dist/cli/tools/execProcess.js +15 -1
- package/dist/cli/tools/execProcess.js.map +1 -1
- package/dist/cli/tools/krakenWorktree.js +334 -43
- package/dist/cli/tools/krakenWorktree.js.map +1 -1
- package/dist/cli/tools/taskTool.js +179 -43
- package/dist/cli/tools/taskTool.js.map +1 -1
- package/dist/cli/tools/verifyDebtSpine.js +41 -0
- package/dist/cli/tools/verifyDebtSpine.js.map +1 -1
- package/package.json +5 -2
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// WS5 (t137): the OBSERVER hook surface (PermissionRequest / Notification
|
|
2
|
+
// subscribers). Type + one pure formatter only — the gate never gates on it.
|
|
3
|
+
import { summarizeHookArgs, } from '@zelari/core/harness';
|
|
4
|
+
import { claimMatchValues, resourceClaimsFor } from './resourceClaims.js';
|
|
5
|
+
import { emitDecisionEvent } from './decisionEmit.js';
|
|
6
|
+
import { activePermissionRules } from './permissionRules.js';
|
|
7
|
+
import { evaluatePermissionPolicy, formatPermissionDenial, } from './permissionPolicy.js';
|
|
8
|
+
/** Spine event kind emitted on a rule denial (WS1 vocabulary addition). */
|
|
9
|
+
export const PERMISSION_DENIED_KIND = 'permission.denied';
|
|
10
|
+
/** Spine event kind emitted when a dispatch resolves to a PROMPT (WS7 slice 4). */
|
|
11
|
+
export const PERMISSION_ASKED_KIND = 'permission.asked';
|
|
12
|
+
/** Spine event kind emitted when a dispatch is allowed with NO prompt (WS7 slice 4). */
|
|
13
|
+
export const AUTO_APPROVE_GRANTED_KIND = 'auto_approve.granted';
|
|
14
|
+
/**
|
|
15
|
+
* Derive what the engine looks at from ONE tool call: the declared categories
|
|
16
|
+
* plus the concrete resources the invocation can touch (paths, host). Reuses
|
|
17
|
+
* the resource-claims table so a path deny can never be dodged by a second
|
|
18
|
+
* argument (apply_diff headers, observe_batch operations, …).
|
|
19
|
+
*/
|
|
20
|
+
export function buildPermissionRequest(input) {
|
|
21
|
+
const paths = [];
|
|
22
|
+
let host;
|
|
23
|
+
for (const claim of resourceClaimsFor(input.toolName, input.args)) {
|
|
24
|
+
if (claim.kind === 'path') {
|
|
25
|
+
for (const candidate of claimMatchValues(claim, input.root)) {
|
|
26
|
+
if (!paths.includes(candidate))
|
|
27
|
+
paths.push(candidate);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else if (claim.kind === 'network' && host === undefined) {
|
|
31
|
+
host = claim.host;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
toolName: input.toolName,
|
|
36
|
+
categories: input.required,
|
|
37
|
+
...(paths.length > 0 ? { paths } : {}),
|
|
38
|
+
...(host !== undefined ? { host } : {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The gate's verdict for one dispatch, or null when there is NOTHING to say:
|
|
43
|
+
* no rule configured (zero-rule fast path → the category decision stands) or
|
|
44
|
+
* no rule matched (the engine's "> category default" step). A malformed
|
|
45
|
+
* project config is never silent: it comes back as a fail-closed 'ask'.
|
|
46
|
+
*/
|
|
47
|
+
export function evaluateToolDispatch(input) {
|
|
48
|
+
const { rules, error } = activePermissionRules(input.root);
|
|
49
|
+
if (error !== undefined) {
|
|
50
|
+
return {
|
|
51
|
+
decision: 'ask',
|
|
52
|
+
source: 'fail-closed',
|
|
53
|
+
reason: `[permissions] ${error}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (rules.length === 0)
|
|
57
|
+
return null;
|
|
58
|
+
let request;
|
|
59
|
+
try {
|
|
60
|
+
request = buildPermissionRequest(input);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
64
|
+
return {
|
|
65
|
+
decision: 'ask',
|
|
66
|
+
source: 'fail-closed',
|
|
67
|
+
reason: `[permissions] could not derive the resources of "${input.toolName}" (${detail}) — failing closed`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const verdict = evaluatePermissionPolicy(rules, request);
|
|
71
|
+
// No rule matched ⇒ no opinion ⇒ the 5-category decision stands.
|
|
72
|
+
return verdict.source === 'fail-closed' ? null : verdict;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Promote an 'ask' to 'allow' only when the CATEGORY decided the ask. A
|
|
76
|
+
* category deny (env `ZELARI_PERMISSION_*`) and every other layer's ask/deny
|
|
77
|
+
* (agent rules, claims, contract) are left untouched: an allow rule can skip
|
|
78
|
+
* a prompt, never a restriction.
|
|
79
|
+
*/
|
|
80
|
+
export function applyAllowRule(categoryAction) {
|
|
81
|
+
return categoryAction === 'ask' ? 'allow' : categoryAction;
|
|
82
|
+
}
|
|
83
|
+
const MAX_RECENT_DENIALS = 20;
|
|
84
|
+
let denialLedger = [];
|
|
85
|
+
/** Record a denial for `/permissions`. Never throws, never blocks a dispatch. */
|
|
86
|
+
export function recordPermissionDenial(record) {
|
|
87
|
+
denialLedger = [record, ...denialLedger].slice(0, MAX_RECENT_DENIALS);
|
|
88
|
+
}
|
|
89
|
+
/** Most recent denials, newest first. */
|
|
90
|
+
export function listRecentPermissionDenials(limit = 10) {
|
|
91
|
+
return denialLedger.slice(0, Math.max(0, limit));
|
|
92
|
+
}
|
|
93
|
+
export function clearPermissionDenials() {
|
|
94
|
+
denialLedger = [];
|
|
95
|
+
}
|
|
96
|
+
/** The denial line the user sees ("denied "write_file" — [permissions:project] rule '…'"). */
|
|
97
|
+
export function permissionDenialMessage(toolName, verdict) {
|
|
98
|
+
return formatPermissionDenial(toolName, verdict);
|
|
99
|
+
}
|
|
100
|
+
function seqFrom(result) {
|
|
101
|
+
const raw = result !== null && typeof result === 'object' && 'seq' in result
|
|
102
|
+
? result.seq
|
|
103
|
+
: undefined;
|
|
104
|
+
const seq = typeof raw === 'number' ? raw : Number(raw);
|
|
105
|
+
return Number.isFinite(seq) && seq > 0 ? seq : undefined;
|
|
106
|
+
}
|
|
107
|
+
/** Pure: one dispatch decision → the `PermissionRequest` hook payload. */
|
|
108
|
+
export function buildPermissionRequestHookPayload(input) {
|
|
109
|
+
const verdict = input.verdict ?? null;
|
|
110
|
+
const argsSummary = summarizeHookArgs(input.args);
|
|
111
|
+
const reason = verdict?.reason?.trim();
|
|
112
|
+
return {
|
|
113
|
+
tool: input.tool,
|
|
114
|
+
categories: [...input.categories],
|
|
115
|
+
effect: input.effect,
|
|
116
|
+
...(verdict?.matchedRuleId ? { matchedRuleId: verdict.matchedRuleId } : {}),
|
|
117
|
+
...(verdict?.source ? { source: verdict.source } : {}),
|
|
118
|
+
...(reason ? { reason } : {}),
|
|
119
|
+
...(argsSummary ? { argsSummary } : {}),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** Fire `PermissionRequest` (+ `Notification` on deny). Never throws. */
|
|
123
|
+
export async function emitPermissionObserverHooks(hooks, input, ctx = {}) {
|
|
124
|
+
if (!hooks)
|
|
125
|
+
return;
|
|
126
|
+
const payload = buildPermissionRequestHookPayload(input);
|
|
127
|
+
try {
|
|
128
|
+
await hooks.runPermissionRequest(payload, ctx);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* an observer never propagates into the gate */
|
|
132
|
+
}
|
|
133
|
+
if (payload.effect !== 'deny')
|
|
134
|
+
return;
|
|
135
|
+
try {
|
|
136
|
+
await hooks.runNotification({
|
|
137
|
+
source: 'needs-input',
|
|
138
|
+
kind: PERMISSION_DENIED_KIND,
|
|
139
|
+
summary: `tool "${payload.tool}" was denied by ${payload.matchedRuleId || 'a permission rule'}`,
|
|
140
|
+
tool: payload.tool,
|
|
141
|
+
}, ctx);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
/* an observer never propagates into the gate */
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export async function emitPermissionDenied(sink, payload) {
|
|
148
|
+
const { tool, verdict } = payload;
|
|
149
|
+
const ts = payload.ts ?? Date.now();
|
|
150
|
+
recordPermissionDenial({
|
|
151
|
+
ts,
|
|
152
|
+
tool,
|
|
153
|
+
matchedRuleId: verdict.matchedRuleId ?? '',
|
|
154
|
+
source: verdict.source,
|
|
155
|
+
reason: verdict.reason,
|
|
156
|
+
...(payload.sessionId !== undefined ? { sessionId: payload.sessionId } : {}),
|
|
157
|
+
});
|
|
158
|
+
if (!sink)
|
|
159
|
+
return { recorded: false };
|
|
160
|
+
try {
|
|
161
|
+
const result = await sink({
|
|
162
|
+
kind: PERMISSION_DENIED_KIND,
|
|
163
|
+
actor: { type: 'system', role: 'permissions' },
|
|
164
|
+
data: {
|
|
165
|
+
tool,
|
|
166
|
+
matchedRuleId: verdict.matchedRuleId ?? '',
|
|
167
|
+
source: verdict.source,
|
|
168
|
+
reason: verdict.reason,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
const seq = seqFrom(result);
|
|
172
|
+
return seq !== undefined ? { recorded: true, seq } : { recorded: true };
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return { recorded: false };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* SELECTION RULE for `auto_approve.granted` — which allows the spine records.
|
|
180
|
+
* The deny side (WS1) records only RULE denials; an allow is recorded only when
|
|
181
|
+
* the harness actually DECIDED something:
|
|
182
|
+
*
|
|
183
|
+
* (a) an ALLOW RULE matched (project/session rule) — the rule made the call;
|
|
184
|
+
* (b) `--permissions yolo` promoted an ask to allow — an explicit opt-in;
|
|
185
|
+
* (c) an `execute` / `network` CATEGORY default — privileged side effects.
|
|
186
|
+
*
|
|
187
|
+
* Every other category default (`read`, `write`, `ui`) is deliberately NOT
|
|
188
|
+
* recorded: those are the bulk of all dispatches and recording them would
|
|
189
|
+
* flood the spine with events that carry no decision. Returns null ⇒ nothing
|
|
190
|
+
* to say (same contract as `evaluateToolDispatch`).
|
|
191
|
+
*/
|
|
192
|
+
export function autoApproveOrigin(input) {
|
|
193
|
+
if (input.effect !== 'allow')
|
|
194
|
+
return null;
|
|
195
|
+
if (input.yoloPromoted === true) {
|
|
196
|
+
return { source: 'preset', reason: 'yolo (--permissions yolo) promoted an ask to allow' };
|
|
197
|
+
}
|
|
198
|
+
const verdict = input.verdict ?? null;
|
|
199
|
+
if (verdict?.decision === 'allow') {
|
|
200
|
+
return {
|
|
201
|
+
source: verdict.source,
|
|
202
|
+
...(verdict.matchedRuleId ? { matchedRuleId: verdict.matchedRuleId } : {}),
|
|
203
|
+
...(verdict.reason ? { reason: verdict.reason } : {}),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const privileged = input.categories.filter((c) => c === 'execute' || c === 'network');
|
|
207
|
+
if (privileged.length > 0) {
|
|
208
|
+
return { source: 'default', reason: `${privileged.join('+')} category default` };
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* `permission.asked` — the gate resolved this dispatch to a PROMPT. Emitted for
|
|
214
|
+
* EVERY ask, including the fail-closed one (no interactive approver attached):
|
|
215
|
+
* the DECISION was "ask", whichever way it then resolved. Payload reuses the
|
|
216
|
+
* WS5 hook formatter (`buildPermissionRequestHookPayload`: tool, categories,
|
|
217
|
+
* effect `ask`, rule origin, bounded argsSummary) — the same fields the
|
|
218
|
+
* `permission.asked` contract in decisionEvents.ts mirrors.
|
|
219
|
+
*/
|
|
220
|
+
export async function emitPermissionAsked(sink, input) {
|
|
221
|
+
const base = buildPermissionRequestHookPayload({ ...input, effect: 'ask' });
|
|
222
|
+
const reason = input.reason?.trim();
|
|
223
|
+
return emitDecisionEvent(sink, PERMISSION_ASKED_KIND, { ...base, ...(reason ? { reason } : {}) }, { type: 'system', role: 'permissions' });
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* `auto_approve.granted` — the same decision block resolved to ALLOW without a
|
|
227
|
+
* prompt. The caller passes the origin `autoApproveOrigin()` selected.
|
|
228
|
+
*/
|
|
229
|
+
export async function emitAutoApproveGranted(sink, payload) {
|
|
230
|
+
const { origin } = payload;
|
|
231
|
+
return emitDecisionEvent(sink, AUTO_APPROVE_GRANTED_KIND, {
|
|
232
|
+
tool: payload.tool,
|
|
233
|
+
categories: [...payload.categories],
|
|
234
|
+
source: origin.source,
|
|
235
|
+
...(origin.matchedRuleId ? { matchedRuleId: origin.matchedRuleId } : {}),
|
|
236
|
+
...(origin.reason ? { reason: origin.reason } : {}),
|
|
237
|
+
}, { type: 'system', role: 'permissions' });
|
|
238
|
+
}
|
|
239
|
+
//# sourceMappingURL=permissionGate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissionGate.js","sourceRoot":"","sources":["../../../src/cli/safety/permissionGate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WS1 / t133 — permission policy engine (allow | ask | deny) for tool dispatch.
|
|
3
|
+
*
|
|
4
|
+
* A local, declarative rule layer evaluated BEFORE every CLI tool dispatch:
|
|
5
|
+
*
|
|
6
|
+
* .zelari/permissions.json (source 'project') ─┐
|
|
7
|
+
* /permissions add … (source 'session') ─┼→ evaluate → deny > ask > allow
|
|
8
|
+
* the 5 category defaults (source 'default') ─┘
|
|
9
|
+
*
|
|
10
|
+
* Precedence is deterministic (plan §WS1):
|
|
11
|
+
*
|
|
12
|
+
* 1. deny > ask > allow — the most restrictive EFFECT wins;
|
|
13
|
+
* 2. within the same effect the MOST SPECIFIC rule wins (a rule is more
|
|
14
|
+
* specific the more of its matchers it had to satisfy: tool / category /
|
|
15
|
+
* pathPrefix / host);
|
|
16
|
+
* 3. remaining ties keep declaration order — session rules are listed before
|
|
17
|
+
* project ones, so runtime intent wins an equal-specificity tie.
|
|
18
|
+
*
|
|
19
|
+
* With ZERO rules the engine is a no-op: the caller keeps the historical
|
|
20
|
+
* 5-category semantics of toolPermissions.ts, byte-identical. This module
|
|
21
|
+
* never widens a category on its own.
|
|
22
|
+
*
|
|
23
|
+
* Fail-closed: a malformed/unknown rule or config never silently allows.
|
|
24
|
+
* `parsePermissionRuleFile` returns the error IN PLACE of the rules (naming
|
|
25
|
+
* the offending file) and `evaluatePermissionPolicy` answers 'ask' — never
|
|
26
|
+
* 'allow' — when nothing matched and no fallback was supplied.
|
|
27
|
+
*
|
|
28
|
+
* Pure: no I/O, no process state, no clock. Rule SOURCES live in
|
|
29
|
+
* permissionRules.ts; the dispatch gate in permissionGate.ts.
|
|
30
|
+
*
|
|
31
|
+
* @since v2.56.0 (WS1 / t133)
|
|
32
|
+
*/
|
|
33
|
+
import { z } from 'zod';
|
|
34
|
+
import { EFFECT_RANK } from './policyEngine.js';
|
|
35
|
+
/** The three effects a rule can carry (same vocabulary as the category policy). */
|
|
36
|
+
export const PERMISSION_EFFECTS = ['allow', 'ask', 'deny'];
|
|
37
|
+
/** The 5 category semantics of toolPermissions.ts — the DEFAULT source. */
|
|
38
|
+
export const PERMISSION_RULE_CATEGORIES = ['read', 'write', 'execute', 'network', 'ui'];
|
|
39
|
+
/**
|
|
40
|
+
* `$comment` — the ONE non-policy key both schemas tolerate: a documentation
|
|
41
|
+
* slot the WS1 template writes at the top level of `.zelari/permissions.json`
|
|
42
|
+
* (a rule may carry its own for the same purpose).
|
|
43
|
+
*
|
|
44
|
+
* It is ACCEPTED AND STRIPPED: the content is never validated and never reaches
|
|
45
|
+
* the engine, so documentation can never change a decision. Without this the
|
|
46
|
+
* `.strict()` schemas rejected the shipped 0-rule template as malformed, and
|
|
47
|
+
* since a malformed config fails closed (every call 'ask') the effect was the
|
|
48
|
+
* exact OPPOSITE of a rule-less file: nothing at all was allowed in a
|
|
49
|
+
* non-interactive context. Every OTHER unknown key is still a MALFORMED config
|
|
50
|
+
* (a typo'd `efect:` keeps failing closed, naming its path).
|
|
51
|
+
*/
|
|
52
|
+
const commentSchema = z.string().optional();
|
|
53
|
+
/** Shallow copy of a validated object without its documentation slot. */
|
|
54
|
+
function stripComment(value) {
|
|
55
|
+
const { $comment: _comment, ...rest } = value;
|
|
56
|
+
return rest;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* One rule of `.zelari/permissions.json` (zod-validated, `.strict()`: an
|
|
60
|
+
* unknown key — `$comment` aside — is a MALFORMED config and fails closed
|
|
61
|
+
* instead of being silently ignored).
|
|
62
|
+
*
|
|
63
|
+
* A rule with no matcher is a catch-all (specificity 0) — the only way to say
|
|
64
|
+
* "deny everything" — and loses every same-effect tie against a rule that
|
|
65
|
+
* actually matched on something.
|
|
66
|
+
*/
|
|
67
|
+
export const PermissionRuleSchema = z
|
|
68
|
+
.object({
|
|
69
|
+
$comment: commentSchema,
|
|
70
|
+
id: z.string().min(1),
|
|
71
|
+
effect: z.enum(PERMISSION_EFFECTS),
|
|
72
|
+
/** Tool name or '*' glob pattern (e.g. `bash`, `mcp__*`). */
|
|
73
|
+
tool: z.string().min(1).optional(),
|
|
74
|
+
category: z.enum(PERMISSION_RULE_CATEGORIES).optional(),
|
|
75
|
+
/** Path prefix, root-relative or absolute (separator-insensitive). */
|
|
76
|
+
pathPrefix: z.string().min(1).optional(),
|
|
77
|
+
/** Host or domain suffix (`example.com`, `*.example.com`). */
|
|
78
|
+
host: z.string().min(1).optional(),
|
|
79
|
+
note: z.string().min(1).optional(),
|
|
80
|
+
})
|
|
81
|
+
.strict()
|
|
82
|
+
.transform(stripComment);
|
|
83
|
+
/** Project config location, relative to the workspace root. */
|
|
84
|
+
export const PERMISSION_RULE_FILE = '.zelari/permissions.json';
|
|
85
|
+
/** Envelope schema version of that file (additive changes keep it). */
|
|
86
|
+
export const PERMISSION_RULE_FILE_VERSION = 1;
|
|
87
|
+
/**
|
|
88
|
+
* Envelope of `.zelari/permissions.json`: the version and the rules, plus the
|
|
89
|
+
* `$comment` documentation slot (stripped, see above) — nothing else.
|
|
90
|
+
*/
|
|
91
|
+
export const PermissionRuleFileSchema = z
|
|
92
|
+
.object({
|
|
93
|
+
$comment: commentSchema,
|
|
94
|
+
version: z.literal(PERMISSION_RULE_FILE_VERSION).optional(),
|
|
95
|
+
rules: z.array(PermissionRuleSchema),
|
|
96
|
+
})
|
|
97
|
+
.strict()
|
|
98
|
+
.transform(stripComment);
|
|
99
|
+
function normalizeValue(v) {
|
|
100
|
+
return v.trim().replace(/\\/g, '/').replace(/\/{2,}/g, '/').toLowerCase();
|
|
101
|
+
}
|
|
102
|
+
function escapeRegExp(s) {
|
|
103
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
104
|
+
}
|
|
105
|
+
/** Tool name match: exact, or a `*` glob (`*`, `mcp__*`, `write_*`). */
|
|
106
|
+
function toolHits(pattern, toolName) {
|
|
107
|
+
const p = pattern.trim();
|
|
108
|
+
if (!p.includes('*'))
|
|
109
|
+
return p === toolName;
|
|
110
|
+
const body = p
|
|
111
|
+
.split('*')
|
|
112
|
+
.map((part) => escapeRegExp(part))
|
|
113
|
+
.join('.*');
|
|
114
|
+
return new RegExp(`^${body}$`).test(toolName);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Path prefix match against a whole-SEGMENT run: `docs` matches `docs/a.md`
|
|
118
|
+
* and `/repo/docs/a.md`, but never `docs-private/a.md`. Separator- and
|
|
119
|
+
* (Windows hosts, user-authored config): the DENY direction must never slip,
|
|
120
|
+
* so being forgiving here fails safe.
|
|
121
|
+
*/
|
|
122
|
+
function pathPrefixHits(prefix, paths) {
|
|
123
|
+
const head = `/${normalizeValue(prefix).replace(/^\/+|\/+$/g, '')}`;
|
|
124
|
+
if (head === '/')
|
|
125
|
+
return false;
|
|
126
|
+
return paths.some((p) => {
|
|
127
|
+
const value = `/${normalizeValue(p).replace(/^\/+|\/+$/g, '')}`;
|
|
128
|
+
return value === head || value.startsWith(`${head}/`) || value.includes(`${head}/`);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Host match: exact, or a domain suffix (`example.com` also matches `api.example.com`). */
|
|
132
|
+
function hostHits(pattern, host) {
|
|
133
|
+
const p = normalizeValue(pattern).replace(/^\*\.?/, '');
|
|
134
|
+
if (!p)
|
|
135
|
+
return true;
|
|
136
|
+
const h = normalizeValue(host).replace(/\/+$/, '');
|
|
137
|
+
return h === p || h.endsWith(`.${p}`);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Match ONE rule against ONE request. Every matcher the rule DECLARES must
|
|
141
|
+
* hit; the count of declared matchers is its specificity. Null = no match.
|
|
142
|
+
*/
|
|
143
|
+
export function matchPermissionRule(entry, request) {
|
|
144
|
+
const { rule } = entry;
|
|
145
|
+
let specificity = 0;
|
|
146
|
+
if (rule.tool !== undefined) {
|
|
147
|
+
if (!toolHits(rule.tool, request.toolName))
|
|
148
|
+
return null;
|
|
149
|
+
specificity += 1;
|
|
150
|
+
}
|
|
151
|
+
if (rule.category !== undefined) {
|
|
152
|
+
if (!(request.categories ?? []).includes(rule.category))
|
|
153
|
+
return null;
|
|
154
|
+
specificity += 1;
|
|
155
|
+
}
|
|
156
|
+
if (rule.pathPrefix !== undefined) {
|
|
157
|
+
if (!pathPrefixHits(rule.pathPrefix, request.paths ?? []))
|
|
158
|
+
return null;
|
|
159
|
+
specificity += 1;
|
|
160
|
+
}
|
|
161
|
+
if (rule.host !== undefined) {
|
|
162
|
+
if (typeof request.host !== 'string' || !hostHits(rule.host, request.host))
|
|
163
|
+
return null;
|
|
164
|
+
specificity += 1;
|
|
165
|
+
}
|
|
166
|
+
return { rule, source: entry.source, specificity };
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The winning rule (deny > ask > allow, then most specific, then first
|
|
170
|
+
* declared), or null when nothing matched — in which case the caller keeps
|
|
171
|
+
* the category decision ("... > category default").
|
|
172
|
+
*/
|
|
173
|
+
export function matchPermissionRules(rules, request) {
|
|
174
|
+
let best = null;
|
|
175
|
+
for (const entry of rules) {
|
|
176
|
+
const hit = matchPermissionRule(entry, request);
|
|
177
|
+
if (!hit)
|
|
178
|
+
continue;
|
|
179
|
+
if (best === null) {
|
|
180
|
+
best = hit;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const rank = EFFECT_RANK[hit.rule.effect] - EFFECT_RANK[best.rule.effect];
|
|
184
|
+
if (rank > 0 || (rank === 0 && hit.specificity > best.specificity))
|
|
185
|
+
best = hit;
|
|
186
|
+
}
|
|
187
|
+
return best;
|
|
188
|
+
}
|
|
189
|
+
/** `[permissions:session] rule 'no-push' — never push from this repo` */
|
|
190
|
+
export function describePermissionRule(source, rule) {
|
|
191
|
+
return `[permissions:${source}] rule '${rule.id}'${rule.note ? ` — ${rule.note}` : ''}`;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Stable, greppable deny line. The matched rule id is ALWAYS present (WS1
|
|
195
|
+
* contract: "structured denial that NAMES the matched rule id"); the
|
|
196
|
+
* fail-closed branch says so explicitly.
|
|
197
|
+
*/
|
|
198
|
+
export function formatPermissionDenial(toolName, verdict) {
|
|
199
|
+
const head = describePermissionRule(verdict.source, {
|
|
200
|
+
id: verdict.matchedRuleId ?? 'unknown (no rule matched)',
|
|
201
|
+
});
|
|
202
|
+
// A matched rule's `reason` IS `describePermissionRule(source, rule)` — the
|
|
203
|
+
// very same head PLUS the rule note (see evaluatePermissionPolicy). Keep it
|
|
204
|
+
// whenever it extends the head, or the note the user needs to understand WHY
|
|
205
|
+
// would be dropped; a reason about something else (fail-closed) is appended
|
|
206
|
+
// after the denial, as before.
|
|
207
|
+
const extendsHead = verdict.reason !== '' && verdict.reason.startsWith(head);
|
|
208
|
+
const named = extendsHead ? verdict.reason : head;
|
|
209
|
+
const extra = verdict.reason !== '' && !extendsHead ? ` ${verdict.reason}` : '';
|
|
210
|
+
return `${named} — denied "${toolName}".${extra}`;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Evaluate the request against `rules`.
|
|
214
|
+
*
|
|
215
|
+
* `opts.fallback` is the decision to use when NO rule matched — the dispatch
|
|
216
|
+
* gate passes the category decision so a rule-less setup stays a no-op. With
|
|
217
|
+
* no fallback the engine FAILS CLOSED ('ask', never 'allow').
|
|
218
|
+
*/
|
|
219
|
+
export function evaluatePermissionPolicy(rules, request, opts = {}) {
|
|
220
|
+
const hit = matchPermissionRules(rules, request);
|
|
221
|
+
if (hit) {
|
|
222
|
+
return {
|
|
223
|
+
decision: hit.rule.effect,
|
|
224
|
+
matchedRuleId: hit.rule.id,
|
|
225
|
+
source: hit.source,
|
|
226
|
+
specificity: hit.specificity,
|
|
227
|
+
reason: describePermissionRule(hit.source, hit.rule),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (opts.fallback !== undefined) {
|
|
231
|
+
return {
|
|
232
|
+
decision: opts.fallback,
|
|
233
|
+
source: 'default',
|
|
234
|
+
specificity: 0,
|
|
235
|
+
reason: opts.fallbackReason ?? '',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
decision: 'ask',
|
|
240
|
+
source: 'fail-closed',
|
|
241
|
+
specificity: 0,
|
|
242
|
+
reason: '[permissions] no rule matched and no category default was supplied — failing closed',
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Parse `.zelari/permissions.json` (already read as text). Any problem —
|
|
247
|
+
* invalid JSON, schema violation, unknown key (`$comment` aside, which is
|
|
248
|
+
* stripped as documentation), duplicate rule id — returns an
|
|
249
|
+
* `error` that NAMES the file and the offending field, and NO rules: the
|
|
250
|
+
* engine then answers 'ask' (fail-closed), never a silent allow.
|
|
251
|
+
*/
|
|
252
|
+
export function parsePermissionRuleFile(raw, filePath) {
|
|
253
|
+
let json;
|
|
254
|
+
try {
|
|
255
|
+
json = JSON.parse(raw);
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
259
|
+
return { rules: [], error: `${filePath}: invalid JSON (${detail}) — failing closed` };
|
|
260
|
+
}
|
|
261
|
+
const parsed = PermissionRuleFileSchema.safeParse(json);
|
|
262
|
+
if (!parsed.success) {
|
|
263
|
+
const issue = parsed.error.issues[0];
|
|
264
|
+
const at = issue && issue.path.length > 0 ? issue.path.join('.') : 'file';
|
|
265
|
+
return {
|
|
266
|
+
rules: [],
|
|
267
|
+
error: `${filePath}: invalid permissions config at '${at}': ${issue?.message ?? 'schema validation failed'} — failing closed`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const seen = new Set();
|
|
271
|
+
for (const rule of parsed.data.rules) {
|
|
272
|
+
if (seen.has(rule.id)) {
|
|
273
|
+
return { rules: [], error: `${filePath}: duplicate rule id '${rule.id}' — failing closed` };
|
|
274
|
+
}
|
|
275
|
+
seen.add(rule.id);
|
|
276
|
+
}
|
|
277
|
+
return { rules: parsed.data.rules.map((rule) => ({ rule, source: 'project' })) };
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=permissionPolicy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissionPolicy.js","sourceRoot":"","sources":["../../../src/cli/safety/permissionPolicy.ts"],"names":[],"mappings":"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"}
|