zelari-code 2.55.0 → 2.57.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.
Files changed (106) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/app.js +2 -2
  3. package/dist/cli/app.js.map +1 -1
  4. package/dist/cli/budget/modelContextBuilder.js +4 -1
  5. package/dist/cli/budget/modelContextBuilder.js.map +1 -1
  6. package/dist/cli/commands/evolve.js +247 -0
  7. package/dist/cli/commands/evolve.js.map +1 -0
  8. package/dist/cli/commands/replay.js +202 -0
  9. package/dist/cli/commands/replay.js.map +1 -0
  10. package/dist/cli/commands/session.js +264 -0
  11. package/dist/cli/commands/session.js.map +1 -0
  12. package/dist/cli/commands/spineSession.js +77 -0
  13. package/dist/cli/commands/spineSession.js.map +1 -0
  14. package/dist/cli/components/StatusBar.js +6 -2
  15. package/dist/cli/components/StatusBar.js.map +1 -1
  16. package/dist/cli/components/statusChips.js +43 -0
  17. package/dist/cli/components/statusChips.js.map +1 -1
  18. package/dist/cli/councilDispatcher.js +12 -0
  19. package/dist/cli/councilDispatcher.js.map +1 -1
  20. package/dist/cli/evolution/controller.js +107 -0
  21. package/dist/cli/evolution/controller.js.map +1 -0
  22. package/dist/cli/evolution/ledger.js +127 -0
  23. package/dist/cli/evolution/ledger.js.map +1 -1
  24. package/dist/cli/evolution/patternLedger.js +165 -0
  25. package/dist/cli/evolution/patternLedger.js.map +1 -0
  26. package/dist/cli/headless/runOneTurn.js +31 -1
  27. package/dist/cli/headless/runOneTurn.js.map +1 -1
  28. package/dist/cli/hooks/useChatTurn.js +60 -2
  29. package/dist/cli/hooks/useChatTurn.js.map +1 -1
  30. package/dist/cli/hooks/useSlashDispatch.js +8 -0
  31. package/dist/cli/hooks/useSlashDispatch.js.map +1 -1
  32. package/dist/cli/inbox.js +63 -10
  33. package/dist/cli/inbox.js.map +1 -1
  34. package/dist/cli/inboxSources.js +173 -0
  35. package/dist/cli/inboxSources.js.map +1 -0
  36. package/dist/cli/main.bundled.js +11889 -8078
  37. package/dist/cli/main.bundled.js.map +4 -4
  38. package/dist/cli/main.js +80 -0
  39. package/dist/cli/main.js.map +1 -1
  40. package/dist/cli/memory/mcpAdapter.js +3 -1
  41. package/dist/cli/memory/mcpAdapter.js.map +1 -1
  42. package/dist/cli/memory/onePager.js +93 -0
  43. package/dist/cli/memory/onePager.js.map +1 -0
  44. package/dist/cli/memory/serviceFactory.js +5 -0
  45. package/dist/cli/memory/serviceFactory.js.map +1 -1
  46. package/dist/cli/memory/sqliteBackend.js +40 -3
  47. package/dist/cli/memory/sqliteBackend.js.map +1 -1
  48. package/dist/cli/memory/sqliteCodec.js +2 -0
  49. package/dist/cli/memory/sqliteCodec.js.map +1 -1
  50. package/dist/cli/memory/sqliteSchema.js +11 -1
  51. package/dist/cli/memory/sqliteSchema.js.map +1 -1
  52. package/dist/cli/plugins/bundleCommand.js +238 -0
  53. package/dist/cli/plugins/bundleCommand.js.map +1 -0
  54. package/dist/cli/plugins/bundleDiscover.js +74 -0
  55. package/dist/cli/plugins/bundleDiscover.js.map +1 -0
  56. package/dist/cli/plugins/bundleFs.js +57 -0
  57. package/dist/cli/plugins/bundleFs.js.map +1 -0
  58. package/dist/cli/plugins/bundleHookDef.js +70 -0
  59. package/dist/cli/plugins/bundleHookDef.js.map +1 -0
  60. package/dist/cli/plugins/bundleLoad.js +180 -0
  61. package/dist/cli/plugins/bundleLoad.js.map +1 -0
  62. package/dist/cli/plugins/bundleManifest.js +233 -0
  63. package/dist/cli/plugins/bundleManifest.js.map +1 -0
  64. package/dist/cli/plugins/bundleMcp.js +41 -0
  65. package/dist/cli/plugins/bundleMcp.js.map +1 -0
  66. package/dist/cli/plugins/bundleState.js +142 -0
  67. package/dist/cli/plugins/bundleState.js.map +1 -0
  68. package/dist/cli/runHeadless.js +102 -12
  69. package/dist/cli/runHeadless.js.map +1 -1
  70. package/dist/cli/safety/decisionEmit.js +43 -0
  71. package/dist/cli/safety/decisionEmit.js.map +1 -0
  72. package/dist/cli/safety/permissionGate.js +239 -0
  73. package/dist/cli/safety/permissionGate.js.map +1 -0
  74. package/dist/cli/safety/permissionPolicy.js +279 -0
  75. package/dist/cli/safety/permissionPolicy.js.map +1 -0
  76. package/dist/cli/safety/permissionRules.js +145 -0
  77. package/dist/cli/safety/permissionRules.js.map +1 -0
  78. package/dist/cli/safety/sessionSink.js +169 -0
  79. package/dist/cli/safety/sessionSink.js.map +1 -0
  80. package/dist/cli/slashCommands.js +29 -3
  81. package/dist/cli/slashCommands.js.map +1 -1
  82. package/dist/cli/slashHandlers/evolve.js +41 -0
  83. package/dist/cli/slashHandlers/evolve.js.map +1 -0
  84. package/dist/cli/slashHandlers/memory.js +21 -0
  85. package/dist/cli/slashHandlers/memory.js.map +1 -1
  86. package/dist/cli/slashHandlers/permissions.js +170 -0
  87. package/dist/cli/slashHandlers/permissions.js.map +1 -0
  88. package/dist/cli/statusline/inboxFeed.js +76 -0
  89. package/dist/cli/statusline/inboxFeed.js.map +1 -0
  90. package/dist/cli/statusline/statuslineItems.js +15 -0
  91. package/dist/cli/statusline/statuslineItems.js.map +1 -1
  92. package/dist/cli/toolRegistry.js +161 -7
  93. package/dist/cli/toolRegistry.js.map +1 -1
  94. package/dist/cli/toolResultCache.js +4 -22
  95. package/dist/cli/toolResultCache.js.map +1 -1
  96. package/dist/cli/tools/askUser.js +17 -2
  97. package/dist/cli/tools/askUser.js.map +1 -1
  98. package/dist/cli/tools/execProcess.js +15 -1
  99. package/dist/cli/tools/execProcess.js.map +1 -1
  100. package/dist/cli/tools/krakenWorktree.js +334 -43
  101. package/dist/cli/tools/krakenWorktree.js.map +1 -1
  102. package/dist/cli/tools/taskTool.js +179 -43
  103. package/dist/cli/tools/taskTool.js.map +1 -1
  104. package/dist/cli/tools/verifyDebtSpine.js +41 -0
  105. package/dist/cli/tools/verifyDebtSpine.js.map +1 -1
  106. package/package.json +6 -2
@@ -0,0 +1,43 @@
1
+ import { decisionPayloadError } from '@zelari/core/session';
2
+ /** Actor used by the decision seams: the harness itself, never the model. */
3
+ export const DECISION_ACTOR = { type: 'system' };
4
+ function seqFrom(result) {
5
+ const raw = result !== null && typeof result === 'object' && 'seq' in result
6
+ ? result.seq
7
+ : undefined;
8
+ const seq = typeof raw === 'number' ? raw : Number(raw);
9
+ return Number.isFinite(seq) && seq > 0 ? seq : undefined;
10
+ }
11
+ /**
12
+ * Validate → append, best-effort. Returns what actually happened; never throws.
13
+ */
14
+ export async function emitDecisionEvent(sink, kind, data, actor = DECISION_ACTOR) {
15
+ const contractError = decisionPayloadError(kind, data);
16
+ if (contractError !== null)
17
+ return { recorded: false, error: contractError };
18
+ if (!sink)
19
+ return { recorded: false };
20
+ try {
21
+ const result = await sink({ kind, actor, data });
22
+ const seq = seqFrom(result);
23
+ return seq !== undefined ? { recorded: true, seq } : { recorded: true };
24
+ }
25
+ catch {
26
+ return { recorded: false };
27
+ }
28
+ }
29
+ /**
30
+ * `jail.blocked` — the OS jail refused a spawn (`ZELARI_OS_JAIL=required` with
31
+ * no honest backend ⇒ typed `[jail]` deny, never a silent unjailed run). The
32
+ * payload is the deny's OWN facts: which backend failed, under which mode, why,
33
+ * and which exec tool owned the spawn.
34
+ */
35
+ export async function emitJailBlocked(sink, payload) {
36
+ return emitDecisionEvent(sink, 'jail.blocked', {
37
+ ...(payload.tool !== undefined ? { tool: payload.tool } : {}),
38
+ backend: payload.backend,
39
+ mode: payload.mode,
40
+ reason: payload.reason,
41
+ });
42
+ }
43
+ //# sourceMappingURL=decisionEmit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decisionEmit.js","sourceRoot":"","sources":["../../../src/cli/safety/decisionEmit.ts"],"names":[],"mappings":"AAyBA,OAAO,EAAE,oBAAoB,EAA0B,MAAM,sBAAsB,CAAC;AAcpF,6EAA6E;AAC7E,MAAM,CAAC,MAAM,cAAc,GAAiB,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAE/D,SAAS,OAAO,CAAC,MAAe;IAC9B,MAAM,GAAG,GACP,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,IAAI,MAAM;QAC9D,CAAC,CAAE,MAA2B,CAAC,GAAG;QAClC,CAAC,CAAC,SAAS,CAAC;IAChB,MAAM,GAAG,GAAG,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACxD,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;AAC3D,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,IAAmC,EACnC,IAAuB,EACvB,IAA6B,EAC7B,QAAsB,cAAc;IAEpC,MAAM,aAAa,GAAG,oBAAoB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACvD,IAAI,aAAa,KAAK,IAAI;QAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC;IAC7E,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IACtC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACjD,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;QAC5B,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAC7B,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAmC,EACnC,OAA2E;IAE3E,OAAO,iBAAiB,CAAC,IAAI,EAAE,cAAc,EAAE;QAC7C,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7D,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,MAAM,EAAE,OAAO,CAAC,MAAM;KACvB,CAAC,CAAC;AACL,CAAC"}
@@ -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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"}