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,170 @@
|
|
|
1
|
+
import { appendSystem } from '../hooks/messageHelpers.js';
|
|
2
|
+
import { defaultPermissionPolicy } from '../safety/toolPermissions.js';
|
|
3
|
+
import { PERMISSION_RULE_CATEGORIES } from '../safety/permissionPolicy.js';
|
|
4
|
+
import { addSessionPermissionRule, listSessionPermissionRules, loadProjectPermissionRules, removeSessionPermissionRule, clearSessionPermissionRules, } from '../safety/permissionRules.js';
|
|
5
|
+
import { clearPermissionDenials, listRecentPermissionDenials, } from '../safety/permissionGate.js';
|
|
6
|
+
export const PERMISSIONS_USAGE = [
|
|
7
|
+
'/permissions — active rules (source: default | project | session) + recent denials',
|
|
8
|
+
'/permissions add <id> <allow|ask|deny> [tool=<name|glob>] [category=read|write|execute|network|ui] [pathPrefix=<p>] [host=<h>] [note=<text>]',
|
|
9
|
+
'/permissions remove <id> — drop a session rule',
|
|
10
|
+
'/permissions clear — drop every session rule',
|
|
11
|
+
'/permissions denials [--clear] — recent denials',
|
|
12
|
+
].join('\n');
|
|
13
|
+
const MATCHER_KEYS = ['tool', 'category', 'pathPrefix', 'host', 'note'];
|
|
14
|
+
/** `id effect tool=bash pathPrefix=docs` → the raw rule object (zod validates it). */
|
|
15
|
+
function parseRuleArgs(args) {
|
|
16
|
+
const [id, effect, ...rest] = args;
|
|
17
|
+
if (!id || !effect)
|
|
18
|
+
return { ok: false, error: 'usage: /permissions add <id> <allow|ask|deny> [matchers…]' };
|
|
19
|
+
const raw = { id, effect };
|
|
20
|
+
for (const token of rest) {
|
|
21
|
+
const eq = token.indexOf('=');
|
|
22
|
+
if (eq <= 0)
|
|
23
|
+
return { ok: false, error: `unparseable argument '${token}' (expected key=value)` };
|
|
24
|
+
const key = token.slice(0, eq);
|
|
25
|
+
const value = token.slice(eq + 1);
|
|
26
|
+
if (!MATCHER_KEYS.includes(key)) {
|
|
27
|
+
return { ok: false, error: `unknown matcher '${key}' (expected ${MATCHER_KEYS.join('/')})` };
|
|
28
|
+
}
|
|
29
|
+
if (value === '')
|
|
30
|
+
return { ok: false, error: `empty value for '${key}'` };
|
|
31
|
+
raw[key] = value;
|
|
32
|
+
}
|
|
33
|
+
return { ok: true, raw };
|
|
34
|
+
}
|
|
35
|
+
function formatRuleLine(source, id, effect, matchers, note) {
|
|
36
|
+
return ` [${source}] ${id} → ${effect}${matchers ? ` (${matchers})` : ''}${note ? ` — ${note}` : ''}`;
|
|
37
|
+
}
|
|
38
|
+
function matchersOf(rule) {
|
|
39
|
+
return [
|
|
40
|
+
rule.tool !== undefined ? `tool=${rule.tool}` : '',
|
|
41
|
+
rule.category !== undefined ? `category=${rule.category}` : '',
|
|
42
|
+
rule.pathPrefix !== undefined ? `pathPrefix=${rule.pathPrefix}` : '',
|
|
43
|
+
rule.host !== undefined ? `host=${rule.host}` : '',
|
|
44
|
+
]
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join(' ');
|
|
47
|
+
}
|
|
48
|
+
export function formatPermissionDenialLine(d) {
|
|
49
|
+
const when = new Date(d.ts).toISOString();
|
|
50
|
+
return ` ${when} ${d.tool} blocked by [${d.source}] '${d.matchedRuleId}'`;
|
|
51
|
+
}
|
|
52
|
+
/** The full `/permissions` report (pure — same input, same text). */
|
|
53
|
+
export function formatPermissionsReport(cwd) {
|
|
54
|
+
const policy = defaultPermissionPolicy();
|
|
55
|
+
const project = loadProjectPermissionRules(cwd);
|
|
56
|
+
const session = listSessionPermissionRules();
|
|
57
|
+
const denials = listRecentPermissionDenials(10);
|
|
58
|
+
const lines = [
|
|
59
|
+
'[permissions] local policy engine (WS1) — evaluated before every tool dispatch',
|
|
60
|
+
' precedence: deny > ask > allow, then most specific rule, then session before project',
|
|
61
|
+
'',
|
|
62
|
+
`default (5 categories, unchanged): read=${policy.read} write=${policy.write} execute=${policy.execute} network=${policy.network} ui=${policy.ui}${policy.auto ? ' auto=on' : ''}`,
|
|
63
|
+
` editable via ZELARI_PERMISSION_<CATEGORY> / --permissions <preset> — not here`,
|
|
64
|
+
'',
|
|
65
|
+
`project: ${project.path}`,
|
|
66
|
+
];
|
|
67
|
+
if (project.error !== undefined) {
|
|
68
|
+
lines.push(` FAIL-CLOSED: ${project.error}`, ' (every unmatched tool call asks until the file is fixed)');
|
|
69
|
+
}
|
|
70
|
+
else if (project.rules.length === 0) {
|
|
71
|
+
lines.push(' (no rules — absent or empty file)');
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
for (const { rule } of project.rules) {
|
|
75
|
+
lines.push(formatRuleLine('project', rule.id, rule.effect, matchersOf(rule), rule.note));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
lines.push('', `session (${session.length} rule${session.length === 1 ? '' : 's'}, runtime-only)`);
|
|
79
|
+
if (session.length === 0)
|
|
80
|
+
lines.push(' (none — /permissions add <id> <effect> [matchers…])');
|
|
81
|
+
else {
|
|
82
|
+
for (const { rule } of session) {
|
|
83
|
+
lines.push(formatRuleLine('session', rule.id, rule.effect, matchersOf(rule), rule.note));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
lines.push('', `recent denials (${denials.length})`);
|
|
87
|
+
if (denials.length === 0)
|
|
88
|
+
lines.push(' (none this session)');
|
|
89
|
+
else
|
|
90
|
+
for (const d of denials)
|
|
91
|
+
lines.push(formatPermissionDenialLine(d));
|
|
92
|
+
lines.push('', `categories: ${PERMISSION_RULE_CATEGORIES.join(', ')}`, '', PERMISSIONS_USAGE);
|
|
93
|
+
return lines.join('\n');
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* `/permissions [subcommand] …`. Mirrors the /trust handler shape: it renders
|
|
97
|
+
* a system message and returns the text it renders (handy for tests).
|
|
98
|
+
*/
|
|
99
|
+
export function handlePermissions(ctx, subcommand, args = [], cwd = process.cwd()) {
|
|
100
|
+
const sub = (subcommand ?? '').toLowerCase();
|
|
101
|
+
switch (sub) {
|
|
102
|
+
case '':
|
|
103
|
+
case 'list':
|
|
104
|
+
case 'show': {
|
|
105
|
+
const text = formatPermissionsReport(cwd);
|
|
106
|
+
appendSystem(ctx.setMessages, text);
|
|
107
|
+
return text;
|
|
108
|
+
}
|
|
109
|
+
case 'add': {
|
|
110
|
+
const parsed = parseRuleArgs(args);
|
|
111
|
+
if (!parsed.ok) {
|
|
112
|
+
appendSystem(ctx.setMessages, `[permissions] ${parsed.error}\n\n${PERMISSIONS_USAGE}`);
|
|
113
|
+
return parsed.error;
|
|
114
|
+
}
|
|
115
|
+
const res = addSessionPermissionRule(parsed.raw);
|
|
116
|
+
if (!res.ok) {
|
|
117
|
+
// Return what is rendered: the caller-facing text must carry the
|
|
118
|
+
// REJECTION verdict, not just the bare schema error.
|
|
119
|
+
const rejected = `[permissions] rejected: ${res.error}`;
|
|
120
|
+
appendSystem(ctx.setMessages, rejected);
|
|
121
|
+
return rejected;
|
|
122
|
+
}
|
|
123
|
+
const text = `[permissions] session rule '${res.rule.id}' → ${res.rule.effect}${matchersOf(res.rule) ? ` (${matchersOf(res.rule)})` : ''} — effective on the next dispatch (this process only, never persisted).`;
|
|
124
|
+
appendSystem(ctx.setMessages, text);
|
|
125
|
+
return text;
|
|
126
|
+
}
|
|
127
|
+
case 'remove':
|
|
128
|
+
case 'rm':
|
|
129
|
+
case 'delete': {
|
|
130
|
+
const id = args[0];
|
|
131
|
+
if (!id) {
|
|
132
|
+
appendSystem(ctx.setMessages, '[permissions] usage: /permissions remove <id>');
|
|
133
|
+
return '';
|
|
134
|
+
}
|
|
135
|
+
const removed = removeSessionPermissionRule(id);
|
|
136
|
+
const text = removed
|
|
137
|
+
? `[permissions] removed session rule '${id}'.`
|
|
138
|
+
: `[permissions] no session rule '${id}' (project rules are edited in ${loadProjectPermissionRules(cwd).path}).`;
|
|
139
|
+
appendSystem(ctx.setMessages, text);
|
|
140
|
+
return text;
|
|
141
|
+
}
|
|
142
|
+
case 'clear': {
|
|
143
|
+
clearSessionPermissionRules();
|
|
144
|
+
appendSystem(ctx.setMessages, '[permissions] all session rules cleared.');
|
|
145
|
+
return 'cleared';
|
|
146
|
+
}
|
|
147
|
+
case 'denials':
|
|
148
|
+
case 'denied': {
|
|
149
|
+
if (args[0] === '--clear') {
|
|
150
|
+
clearPermissionDenials();
|
|
151
|
+
appendSystem(ctx.setMessages, '[permissions] denial ledger cleared.');
|
|
152
|
+
return 'cleared';
|
|
153
|
+
}
|
|
154
|
+
const denials = listRecentPermissionDenials(20);
|
|
155
|
+
const text = denials.length === 0
|
|
156
|
+
? '[permissions] no denials recorded this session.'
|
|
157
|
+
: ['[permissions] recent denials:', ...denials.map(formatPermissionDenialLine)].join('\n');
|
|
158
|
+
appendSystem(ctx.setMessages, text);
|
|
159
|
+
return text;
|
|
160
|
+
}
|
|
161
|
+
default: {
|
|
162
|
+
// Return the text that was rendered — it names the bogus subcommand —
|
|
163
|
+
// instead of the bare usage block, so a caller can SEE the rejection.
|
|
164
|
+
const text = `[permissions] unknown subcommand '${sub}'.\n\n${PERMISSIONS_USAGE}`;
|
|
165
|
+
appendSystem(ctx.setMessages, text);
|
|
166
|
+
return text;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=permissions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissions.js","sourceRoot":"","sources":["../../../src/cli/slashHandlers/permissions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* inboxFeed — the `inbox` status-line item: "what is waiting on YOU", derived
|
|
3
|
+
* from the CURRENT session spine (derive-only, ADR-0016/0024).
|
|
4
|
+
*
|
|
5
|
+
* Same discipline as verdictFeed (t124): this module READS entries.jsonl, never
|
|
6
|
+
* writes and never feeds the model. The item is OPT-IN (`/statusline on inbox`),
|
|
7
|
+
* so the default bar is unchanged; the counts come from the very same
|
|
8
|
+
* projections `/inbox` prints:
|
|
9
|
+
* - `questions` — unanswered `ask_user` calls (inbox.ts, t125);
|
|
10
|
+
* - `needs` — open needs: un-cleared verify debt + permission denials;
|
|
11
|
+
* - `finished` — tentacle completions you have not spoken past yet.
|
|
12
|
+
*
|
|
13
|
+
* Kill switch: `ZELARI_INBOX=0` (the t125 switch, reused — one switch for the
|
|
14
|
+
* whole inbox surface, not a second knob) ⇒ `inboxFeedText()` returns null and
|
|
15
|
+
* the item paints nothing.
|
|
16
|
+
*
|
|
17
|
+
* HONESTY: nothing recorded (or an unreadable spine) ⇒ zero rows ⇒ no item
|
|
18
|
+
* text. A zero is never printed as if it were a measurement of "all clear".
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { parseSessionLogText, resolveSessionsDir } from '@zelari/core/session';
|
|
23
|
+
import { getCurrentSessionId } from '../sessionManager.js';
|
|
24
|
+
import { deriveInboxQuestions, inboxEnabled } from '../inbox.js';
|
|
25
|
+
import { deriveInboxSourceRows } from '../inboxSources.js';
|
|
26
|
+
/** The honest "nothing derived" feed. */
|
|
27
|
+
export function emptyInboxFeed() {
|
|
28
|
+
return { questions: 0, needs: 0, finished: 0, total: 0 };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Project ONE session's events into the feed. Pure: no I/O, no clock. The
|
|
32
|
+
* session id is a label here (the rows are counted, not reported), hence the
|
|
33
|
+
* neutral `current` handle.
|
|
34
|
+
*/
|
|
35
|
+
export function deriveInboxFeed(events) {
|
|
36
|
+
const questions = deriveInboxQuestions(events, { sessionId: 'current' }).length;
|
|
37
|
+
const rows = deriveInboxSourceRows(events);
|
|
38
|
+
const needs = rows.filter((r) => r.source === 'needs-input').length;
|
|
39
|
+
return { questions, needs, finished: rows.length - needs, total: questions + rows.length };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Item text for the `inbox` statusline item, or null when there is nothing
|
|
43
|
+
* honest to say (kill switch off, or nothing waiting). Terse on purpose: the
|
|
44
|
+
* bar is one line. `inbox 3` / `inbox 3 (1 need)`.
|
|
45
|
+
*/
|
|
46
|
+
export function inboxFeedText(feed, env = process.env) {
|
|
47
|
+
if (!inboxEnabled(env))
|
|
48
|
+
return null;
|
|
49
|
+
if (!feed || feed.total === 0)
|
|
50
|
+
return null;
|
|
51
|
+
return feed.needs > 0 ? `inbox ${feed.total} (${feed.needs} need)` : `inbox ${feed.total}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Read the inbox feed of the CURRENT run from its spine log. Tolerant by
|
|
55
|
+
* contract: a missing marker/file, a corrupt log or a kill-switched inbox all
|
|
56
|
+
* return the empty feed.
|
|
57
|
+
*/
|
|
58
|
+
export function readInboxFeed(opts = {}) {
|
|
59
|
+
const env = opts.env ?? process.env;
|
|
60
|
+
if (!inboxEnabled(env))
|
|
61
|
+
return emptyInboxFeed();
|
|
62
|
+
const sessionId = opts.sessionId ?? getCurrentSessionId();
|
|
63
|
+
if (!sessionId)
|
|
64
|
+
return emptyInboxFeed();
|
|
65
|
+
const dir = opts.sessionsDir ?? resolveSessionsDir({ workspaceRoot: opts.cwd, env });
|
|
66
|
+
const file = path.join(dir, sessionId, 'events.jsonl');
|
|
67
|
+
try {
|
|
68
|
+
if (!existsSync(file))
|
|
69
|
+
return emptyInboxFeed();
|
|
70
|
+
return deriveInboxFeed(parseSessionLogText(file, readFileSync(file, 'utf-8')).events);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return emptyInboxFeed();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=inboxFeed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inboxFeed.js","sourceRoot":"","sources":["../../../src/cli/statusline/inboxFeed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC/E,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAuB,MAAM,aAAa,CAAC;AACtF,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAa3D,yCAAyC;AACzC,MAAM,UAAU,cAAc;IAC5B,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;AAC3D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,MAAiC;IAC/D,MAAM,SAAS,GAAG,oBAAoB,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC;IAChF,MAAM,IAAI,GAAG,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,aAAa,CAAC,CAAC,MAAM,CAAC;IACpE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,MAAM,GAAG,KAAK,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;AAC7F,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,IAAsB,EAAE,MAAyB,OAAO,CAAC,GAAG;IACxF,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3C,OAAO,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,KAAK,EAAE,CAAC;AAC7F,CAAC;AAWD;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,OAA6B,EAAE;IAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACpC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC;QAAE,OAAO,cAAc,EAAE,CAAC;IAChD,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,mBAAmB,EAAE,CAAC;IAC1D,IAAI,CAAC,SAAS;QAAE,OAAO,cAAc,EAAE,CAAC;IACxC,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,IAAI,kBAAkB,CAAC,EAAE,aAAa,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;IACrF,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC;IACvD,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,cAAc,EAAE,CAAC;QAC/C,OAAO,eAAe,CAAC,mBAAmB,CAAC,IAAI,EAAE,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IACxF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,cAAc,EAAE,CAAC;IAC1B,CAAC;AACH,CAAC"}
|
|
@@ -50,11 +50,21 @@ export const STATUSLINE_CUSTOM_ID = 'custom';
|
|
|
50
50
|
* that no renderer paints yet must not be reported as enabled.
|
|
51
51
|
*/
|
|
52
52
|
export const STATUSLINE_VERDICT_ID = 'verdict';
|
|
53
|
+
/**
|
|
54
|
+
* `inbox` — what is waiting on YOU, derived from the current session spine
|
|
55
|
+
* (inboxFeed.ts: the same projections `/inbox` prints — unanswered ask_user,
|
|
56
|
+
* open needs, tentacle completions). OPT-IN for the same reason as `verdict`:
|
|
57
|
+
* the default list mirrors the chips StatusBar paints with no configuration.
|
|
58
|
+
*
|
|
59
|
+
* WS2 (notification bus): shares the t125 kill switch `ZELARI_INBOX=0`.
|
|
60
|
+
*/
|
|
61
|
+
export const STATUSLINE_INBOX_ID = 'inbox';
|
|
53
62
|
/** Any id a configuration may reference (built-ins + the opt-in items). */
|
|
54
63
|
export const STATUSLINE_ITEM_IDS = [
|
|
55
64
|
...DEFAULT_STATUSLINE_ITEMS,
|
|
56
65
|
STATUSLINE_CUSTOM_ID,
|
|
57
66
|
STATUSLINE_VERDICT_ID,
|
|
67
|
+
STATUSLINE_INBOX_ID,
|
|
58
68
|
];
|
|
59
69
|
/** Catalog for `/statusline` (order = the default order). */
|
|
60
70
|
export const STATUSLINE_ITEMS = [
|
|
@@ -80,6 +90,11 @@ export const STATUSLINE_ITEMS = [
|
|
|
80
90
|
label: 'verdict',
|
|
81
91
|
description: 'verification progress from spine events (derive-only; opt-in)',
|
|
82
92
|
},
|
|
93
|
+
{
|
|
94
|
+
id: STATUSLINE_INBOX_ID,
|
|
95
|
+
label: 'inbox',
|
|
96
|
+
description: 'what is waiting on you: ask_user, needs input, tentacle finished (derive-only; opt-in)',
|
|
97
|
+
},
|
|
83
98
|
];
|
|
84
99
|
/** True when `id` is a known item (built-in, `custom` or `verdict`). */
|
|
85
100
|
export function isStatusLineItemId(id) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statuslineItems.js","sourceRoot":"","sources":["../../../src/cli/statusline/statuslineItems.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;GAGG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAsB;IACzD,WAAW;IACX,OAAO;IACP,MAAM;IACN,QAAQ;IACR,aAAa;IACb,MAAM;IACN,UAAU;IACV,OAAO;IACP,KAAK;IACL,YAAY;IACZ,SAAS;IACT,OAAO;IACP,OAAO;IACP,YAAY;IACZ,aAAa;IACb,SAAS;IACT,MAAM;IACN,SAAS;CACV,CAAC;AAEF,+EAA+E;AAC/E,MAAM,CAAC,MAAM,oBAAoB,GAAG,QAAQ,CAAC;AAE7C;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,SAAS,CAAC;AAE/C,2EAA2E;AAC3E,MAAM,CAAC,MAAM,mBAAmB,GAAsB;IACpD,GAAG,wBAAwB;IAC3B,oBAAoB;IACpB,qBAAqB;
|
|
1
|
+
{"version":3,"file":"statuslineItems.js","sourceRoot":"","sources":["../../../src/cli/statusline/statuslineItems.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/toolRegistry.js
CHANGED
|
@@ -57,6 +57,11 @@ import { PROVIDERS } from './keyStore.js';
|
|
|
57
57
|
import { activePermissionPreset, defaultPermissionPolicy, intersectPermissionPolicy, isSessionGranted, resolveToolPermission, } from './safety/toolPermissions.js';
|
|
58
58
|
// v2.32 (S5): destructive-shape escalation at the same choke-point.
|
|
59
59
|
import { destructiveCommandHit } from './safety/destructiveCommands.js';
|
|
60
|
+
// WS1 (t133): local permission policy (allow/ask/deny rules) — evaluated
|
|
61
|
+
// BEFORE dispatch, with the structured `permission.denied` spine event.
|
|
62
|
+
import { applyAllowRule, emitAutoApproveGranted, emitPermissionAsked, emitPermissionDenied, emitPermissionObserverHooks, evaluateToolDispatch, permissionDenialMessage, autoApproveOrigin, } from './safety/permissionGate.js';
|
|
63
|
+
// WS7 slice 4 (t139): best-effort writer for the remaining decision kinds.
|
|
64
|
+
import { emitJailBlocked } from './safety/decisionEmit.js';
|
|
60
65
|
import { provenanceAppliesTo, provenanceMatchIn, recordResultForProvenance, } from './safety/provenance.js';
|
|
61
66
|
import { createDefaultLifecycleHooks, HOOKS_FAILURE_ENV, resolveHookFailureMode } from './safety/lifecycleHooks.js';
|
|
62
67
|
// ADR-0033 (t76): post-write AST gate with auto-revert on the write path.
|
|
@@ -77,6 +82,9 @@ import { wrapWithBashWriteDetection } from './tools/bashWriteWatch.js';
|
|
|
77
82
|
import { appendKrakenRadio } from './tools/krakenRadio.js';
|
|
78
83
|
import { withResultCache } from './toolResultCache.js';
|
|
79
84
|
import { inspectCommand } from './safety/selfKillGuard.js';
|
|
85
|
+
// WS7 slice 4b (t139): the ONE binding that makes ToolContext.emitSessionEvent
|
|
86
|
+
// live on a real host path (see safety/sessionSink.ts for the full WHY).
|
|
87
|
+
import { withSessionEventSink } from './safety/sessionSink.js';
|
|
80
88
|
import { typedErr } from '@zelari/core/harness/tools/toolTypes';
|
|
81
89
|
import { cliToolToEnhanced, registerCustomTool } from '@zelari/core/skills';
|
|
82
90
|
/**
|
|
@@ -243,7 +251,10 @@ export function createBuiltinToolRegistry(options = {}) {
|
|
|
243
251
|
}
|
|
244
252
|
})();
|
|
245
253
|
const agentPolicyLayers = agentLayersFor(agentPolicySet, options.policyAgent ?? 'lead');
|
|
246
|
-
const withPerm = (t) => wrapWithPermissions(t, permPolicy, options.onPermissionAsk, agentPolicyLayers, agentPolicySet.precedence, root
|
|
254
|
+
const withPerm = (t) => wrapWithPermissions(t, permPolicy, options.onPermissionAsk, agentPolicyLayers, agentPolicySet.precedence, root,
|
|
255
|
+
// WS5 (t137): the SAME runner the registry gates Pre/PostToolUse with —
|
|
256
|
+
// one runner per registry, no second hook universe.
|
|
257
|
+
hooks);
|
|
247
258
|
// Observe tools — always registered.
|
|
248
259
|
registry.register(withPerm(safeReadFile));
|
|
249
260
|
registry.register(withPerm(safeGrepContent));
|
|
@@ -476,12 +487,21 @@ export function createBuiltinToolRegistry(options = {}) {
|
|
|
476
487
|
...(options.onPermissionAsk ? { onPermissionAsk: options.onPermissionAsk } : {}),
|
|
477
488
|
...(options.subAgentProvider ? { provider: options.subAgentProvider } : {}),
|
|
478
489
|
...(options.subAgentModel ? { model: options.subAgentModel } : {}),
|
|
490
|
+
// WS7 slice 4b (t139): a tentacle's own registry is built here, so it
|
|
491
|
+
// needs the parent turn's sink too — otherwise a tentacle's file.applied
|
|
492
|
+
// / file.rejected (and its permission refusals) would stay invisible on
|
|
493
|
+
// the very spine that recorded its `task` spawn.
|
|
494
|
+
...(options.sessionEventSink ? { sessionEventSink: options.sessionEventSink } : {}),
|
|
479
495
|
}),
|
|
480
496
|
...(options.memoryService ? { memoryService: options.memoryService } : {}),
|
|
481
497
|
...(options.memoryAutoWrite !== undefined
|
|
482
498
|
? { memoryAutoWrite: options.memoryAutoWrite }
|
|
483
499
|
: {}),
|
|
484
500
|
...(options.onTentacleEvent ? { onTentacleEvent: options.onTentacleEvent } : {}),
|
|
501
|
+
// WS5 (t137): the registry's own hook runner — SubagentStart/End (and
|
|
502
|
+
// the finished-tentacle Notification) ride the SAME runner that gates
|
|
503
|
+
// Pre/PostToolUse, so one config location covers every hook.
|
|
504
|
+
lifecycleHooks: hooks,
|
|
485
505
|
}, options.planMode === true ? { allowedAgents: ['explore'] } : undefined);
|
|
486
506
|
registry.register(withPerm(taskTool));
|
|
487
507
|
tools.push({
|
|
@@ -570,6 +590,25 @@ export function createBuiltinToolRegistry(options = {}) {
|
|
|
570
590
|
}
|
|
571
591
|
}
|
|
572
592
|
}
|
|
593
|
+
// WS7 slice 4b (t139): populate `ToolContext.emitSessionEvent` for EVERY
|
|
594
|
+
// registered tool, applied LAST (outermost) so the sink reaches the
|
|
595
|
+
// permission gate, the sandbox, the jail preflight and the tool body alike.
|
|
596
|
+
// WHY the host-side injection exists at all: both hosts hand this registry to
|
|
597
|
+
// `AgentHarness`, whose dispatch calls
|
|
598
|
+
// `registry.invoke(name, args, { cwd, sessionId, signal })` — the harness
|
|
599
|
+
// forwards NO host sink into the invoke options (AgentHarness.ts:839/1848).
|
|
600
|
+
// That one missing hop is why the whole tool-side telemetry surface (ADR-0033
|
|
601
|
+
// file.*, WS1 permission.denied, the WS7 slice-4 decision events) was dormant
|
|
602
|
+
// in production while its tests passed (they hand-build a ToolContext).
|
|
603
|
+
// No sink ⇒ no wrap: a registry built without one is byte-identical to before.
|
|
604
|
+
if (options.sessionEventSink) {
|
|
605
|
+
const sessionSink = options.sessionEventSink;
|
|
606
|
+
for (const name of registry.list()) {
|
|
607
|
+
const def = registry.get(name);
|
|
608
|
+
if (def)
|
|
609
|
+
registry.register(withSessionEventSink(def, sessionSink));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
573
612
|
return { registry, tools };
|
|
574
613
|
}
|
|
575
614
|
/**
|
|
@@ -720,7 +759,7 @@ export function resolveTentacleThinkingInput(agent, taskArg, env = process.env)
|
|
|
720
759
|
* (F6) — can build a `TaskToolDeps` without duplicating this wiring.
|
|
721
760
|
*/
|
|
722
761
|
export function createKrakenSubAgentContextFactory(opts) {
|
|
723
|
-
const { root, audit, sessionId, provider: providerOverride, model: modelOverride, parentPolicy, onPermissionAsk } = opts;
|
|
762
|
+
const { root, audit, sessionId, provider: providerOverride, model: modelOverride, parentPolicy, onPermissionAsk, sessionEventSink } = opts;
|
|
724
763
|
return async ({ agent, cwd: subCwd, thinkingEffort }) => {
|
|
725
764
|
const cfg = providerOverride
|
|
726
765
|
? await providerConfigFor(providerOverride)
|
|
@@ -792,6 +831,9 @@ export function createKrakenSubAgentContextFactory(opts) {
|
|
|
792
831
|
...(onPermissionAsk ? { onPermissionAsk } : {}),
|
|
793
832
|
// P0.5: the tentacle's agent identity drives per-agent policy rules.
|
|
794
833
|
policyAgent: agent,
|
|
834
|
+
// WS7 slice 4b (t139): inherit the parent turn's spine sink (absent ⇒
|
|
835
|
+
// the tentacle's tools emit nothing, i.e. today's behavior).
|
|
836
|
+
...(sessionEventSink ? { sessionEventSink } : {}),
|
|
795
837
|
});
|
|
796
838
|
const wrapTentacleStream = (primary, primaryProviderId) => wrapWithHeadlessFailover({
|
|
797
839
|
primary,
|
|
@@ -834,8 +876,13 @@ export function createKrakenSubAgentContextFactory(opts) {
|
|
|
834
876
|
}
|
|
835
877
|
/**
|
|
836
878
|
* Gate tool execution with allow/ask/deny policy (OpenCode-style permissions).
|
|
879
|
+
*
|
|
880
|
+
* WS5 (t137): `hooks` is the lifecycle-hook runner used ONLY for the
|
|
881
|
+
* observation-only `PermissionRequest` (+ denied-`Notification`) events. It is
|
|
882
|
+
* called fire-and-forget and its verdict is discarded by construction, so
|
|
883
|
+
* passing it can never change the verdict computed below.
|
|
837
884
|
*/
|
|
838
|
-
function wrapWithPermissions(original, policy, onAsk, agentLayers, precedence = 'restrict-only', root) {
|
|
885
|
+
function wrapWithPermissions(original, policy, onAsk, agentLayers, precedence = 'restrict-only', root, hooks) {
|
|
839
886
|
const required = (original.permissions ?? []);
|
|
840
887
|
// Pure read tools under allow policy — no wrap overhead.
|
|
841
888
|
const decisionProbe = resolveToolPermission(original.name, required, policy);
|
|
@@ -872,14 +919,36 @@ function wrapWithPermissions(original, policy, onAsk, agentLayers, precedence =
|
|
|
872
919
|
// other layer or the category decision (same deny>ask>allow lattice).
|
|
873
920
|
// No scoped contract ⇒ null ⇒ this slot is inert.
|
|
874
921
|
const contractRule = matchContractCapabilityRule(requiredNow, (input ?? {}), root ?? process.cwd());
|
|
875
|
-
|
|
922
|
+
// WS1 (t133): the local permission policy (`.zelari/permissions.json` +
|
|
923
|
+
// runtime session rules) is evaluated BEFORE the call is dispatched.
|
|
924
|
+
// null = no rule configured, or no rule matched → today's category
|
|
925
|
+
// decision stands, byte-identical. An 'allow' rule promotes only a
|
|
926
|
+
// CATEGORY ask (skip the prompt) — a category deny and every other
|
|
927
|
+
// layer's ask/deny stay restrict-only. An 'ask'/'deny' rule intersects
|
|
928
|
+
// like any other restriction.
|
|
929
|
+
const policyVerdict = evaluateToolDispatch({
|
|
930
|
+
toolName: original.name,
|
|
931
|
+
required: requiredNow,
|
|
932
|
+
args: input,
|
|
933
|
+
root: root ?? process.cwd(),
|
|
934
|
+
});
|
|
935
|
+
const categoryAction = policyVerdict?.decision === 'allow'
|
|
936
|
+
? applyAllowRule(decision.action)
|
|
937
|
+
: policyVerdict?.decision === 'ask' || policyVerdict?.decision === 'deny'
|
|
938
|
+
? mergeRuleEffect(decision.action, {
|
|
939
|
+
match: policyVerdict.matchedRuleId ?? 'permissions',
|
|
940
|
+
effect: policyVerdict.decision,
|
|
941
|
+
reason: policyVerdict.reason,
|
|
942
|
+
})
|
|
943
|
+
: decision.action;
|
|
944
|
+
let action = intersectEffects(mergeRuleEffect(categoryAction, rule), claims?.effect, contractRule?.effect);
|
|
876
945
|
// W3.1 (t46): provenance escalation at the choke-point. Write/execute
|
|
877
946
|
// args that EMBED fingerprinted non-user content (web fetch, MCP output,
|
|
878
947
|
// file reads — safety/provenance.ts) escalate an "allow" to "ask":
|
|
879
948
|
// injected instructions can no longer sail through on category
|
|
880
949
|
// defaults. Deterministic substring match, zero LLM (P2); ask/deny
|
|
881
950
|
// already gated pass through; ZELARI_PROVENANCE=0 opts out entirely.
|
|
882
|
-
let actionReason = decision.reason;
|
|
951
|
+
let actionReason = policyVerdict?.decision === 'ask' ? policyVerdict.reason : decision.reason;
|
|
883
952
|
if (action !== 'deny' && (requiredNow.includes('write') || requiredNow.includes('execute'))) {
|
|
884
953
|
const provHit = provenanceMatchIn(JSON.stringify(input ?? {}));
|
|
885
954
|
if (provHit && provenanceAppliesTo(provHit.source, requiredNow)) {
|
|
@@ -921,19 +990,88 @@ function wrapWithPermissions(original, policy, onAsk, agentLayers, precedence =
|
|
|
921
990
|
: claimHit
|
|
922
991
|
? `[policy] claim '${claimHit.match}'${claimHit.reason ? ` — ${claimHit.reason}` : ''}`
|
|
923
992
|
: '';
|
|
993
|
+
// WS7 slice 4 (t139): did THIS dispatch's ask get auto-answered by yolo?
|
|
994
|
+
// `autoApproveOrigin` needs the fact, not a re-derivation.
|
|
995
|
+
let yoloPromoted = false;
|
|
924
996
|
// t65: yolo = full access — every residual ask (policy rules, claims,
|
|
925
997
|
// contract, provenance) auto-approves instead of hitting the phone with
|
|
926
998
|
// a permission.request. Explicit deny still denies ("deny is never
|
|
927
999
|
// promoted", toolPermissions.ts contract).
|
|
928
1000
|
if (action === 'ask' && activePermissionPreset() === 'yolo') {
|
|
929
1001
|
action = 'allow';
|
|
1002
|
+
yoloPromoted = true;
|
|
1003
|
+
}
|
|
1004
|
+
// WS5 (t137): `action` is FINAL here — every layer merged, provenance
|
|
1005
|
+
// applied, yolo promoted — which is exactly the moment a permission is
|
|
1006
|
+
// requested or refused. Observation-only hooks fire from here:
|
|
1007
|
+
// fire-and-forget, and `emitPermissionObserverHooks` swallows internal
|
|
1008
|
+
// failures, so a hook can never block, slow or alter this dispatch.
|
|
1009
|
+
if (action === 'ask' || action === 'deny') {
|
|
1010
|
+
void emitPermissionObserverHooks(hooks, {
|
|
1011
|
+
tool: original.name,
|
|
1012
|
+
categories: requiredNow,
|
|
1013
|
+
effect: action,
|
|
1014
|
+
verdict: policyVerdict,
|
|
1015
|
+
args: input,
|
|
1016
|
+
}, { sessionId: ctx.sessionId, cwd: ctx.cwd });
|
|
1017
|
+
}
|
|
1018
|
+
// WS7 slice 4 (t139): the DECISION, on the spine — `permission.asked` when
|
|
1019
|
+
// the gate resolved this dispatch to a PROMPT (the fail-closed branch
|
|
1020
|
+
// below included: the decision WAS "ask", there was simply nobody to ask),
|
|
1021
|
+
// `auto_approve.granted` when it resolved to ALLOW with no prompt. Both go
|
|
1022
|
+
// through the SAME sink `permission.denied` and the file.* telemetry use;
|
|
1023
|
+
// both are best-effort (an absent or throwing sink changes nothing about
|
|
1024
|
+
// the dispatch) and both are validated against their slice-2 contract
|
|
1025
|
+
// before a single byte is written. The allow side is deliberately
|
|
1026
|
+
// selective — see `autoApproveOrigin`: rule match, yolo promotion, and
|
|
1027
|
+
// execute/network category grants only, never the read/write flood.
|
|
1028
|
+
if (action === 'ask') {
|
|
1029
|
+
await emitPermissionAsked(ctx.emitSessionEvent, {
|
|
1030
|
+
tool: original.name,
|
|
1031
|
+
categories: requiredNow,
|
|
1032
|
+
verdict: policyVerdict,
|
|
1033
|
+
args: input,
|
|
1034
|
+
reason: policyVerdict?.decision === 'ask' ? policyVerdict.reason : decision.reason,
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
else if (action === 'allow') {
|
|
1038
|
+
const origin = autoApproveOrigin({
|
|
1039
|
+
effect: action,
|
|
1040
|
+
categories: requiredNow,
|
|
1041
|
+
verdict: policyVerdict,
|
|
1042
|
+
yoloPromoted,
|
|
1043
|
+
});
|
|
1044
|
+
if (origin) {
|
|
1045
|
+
await emitAutoApproveGranted(ctx.emitSessionEvent, {
|
|
1046
|
+
tool: original.name,
|
|
1047
|
+
categories: requiredNow,
|
|
1048
|
+
origin,
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
930
1051
|
}
|
|
931
1052
|
if (action === 'deny') {
|
|
1053
|
+
// WS1 (t133): a RULE deny is structured — the message names the
|
|
1054
|
+
// matched rule id and the denial lands on the spine
|
|
1055
|
+
// (`permission.denied`) through the same sink the file.* telemetry
|
|
1056
|
+
// uses, so a replayed session exposes it. yolo never promotes a deny.
|
|
1057
|
+
if (policyVerdict?.decision === 'deny') {
|
|
1058
|
+
await emitPermissionDenied(ctx.emitSessionEvent, {
|
|
1059
|
+
tool: original.name,
|
|
1060
|
+
verdict: policyVerdict,
|
|
1061
|
+
sessionId: ctx.sessionId,
|
|
1062
|
+
});
|
|
1063
|
+
return typedErr(`[permission] ${permissionDenialMessage(original.name, policyVerdict)}`);
|
|
1064
|
+
}
|
|
932
1065
|
return typedErr(`[permission] ${rulePrefix || decision.reason}`);
|
|
933
1066
|
}
|
|
934
1067
|
if (action === 'ask') {
|
|
935
1068
|
if (!onAsk) {
|
|
936
|
-
|
|
1069
|
+
// WS1 (t133): when the RULE layer forced (or failed closed on) this
|
|
1070
|
+
// ask, its reason is the one that names the matched rule id and the
|
|
1071
|
+
// offending `.zelari/permissions.json` — surface THAT, not the bare
|
|
1072
|
+
// category reason, or a malformed config reads as a category ask.
|
|
1073
|
+
const askReason = policyVerdict?.decision === 'ask' ? policyVerdict.reason : decision.reason;
|
|
1074
|
+
return typedErr(`[permission] ${rulePrefix ? `${rulePrefix} ` : ''}${askReason} No interactive approval available ` +
|
|
937
1075
|
`(set ZELARI_AUTO=1 to auto-allow, or configure onPermissionAsk).`);
|
|
938
1076
|
}
|
|
939
1077
|
try {
|
|
@@ -1386,6 +1524,11 @@ function wrapWithShellSafety(original, audit, sessionId, root, jail) {
|
|
|
1386
1524
|
if (mode !== 'off' && !probe.available) {
|
|
1387
1525
|
if (mode === 'required') {
|
|
1388
1526
|
const reason = jailDenyReason(probe, mode);
|
|
1527
|
+
// WS7 slice 4 (t139): the deny this seam SURFACES is the typed
|
|
1528
|
+
// `[jail]` one, and the sibling spawn seam records exactly that
|
|
1529
|
+
// (spawnJailed already prefixed it) — so the spine carries the same
|
|
1530
|
+
// text the caller (and the model) got, never a twin spelling.
|
|
1531
|
+
const jailError = `[jail] ${reason}`;
|
|
1389
1532
|
await audit.append({
|
|
1390
1533
|
ts: new Date().toISOString(),
|
|
1391
1534
|
sessionId,
|
|
@@ -1396,7 +1539,18 @@ function wrapWithShellSafety(original, audit, sessionId, root, jail) {
|
|
|
1396
1539
|
durationMs: 0,
|
|
1397
1540
|
error: 'jail_denied',
|
|
1398
1541
|
});
|
|
1399
|
-
|
|
1542
|
+
// WS7 slice 4 (t139): `jail.blocked` — the jail REFUSED this spawn
|
|
1543
|
+
// (t28 golden rule: required + no honest backend ⇒ deny, never a
|
|
1544
|
+
// silent unjailed run). The backend + mode that failed it ride the
|
|
1545
|
+
// payload, so a replay can tell WHICH containment was missing.
|
|
1546
|
+
// Best-effort: same sink as every other decision event.
|
|
1547
|
+
await emitJailBlocked(ctx.emitSessionEvent, {
|
|
1548
|
+
backend: probe.backend,
|
|
1549
|
+
mode,
|
|
1550
|
+
reason: jailError,
|
|
1551
|
+
tool: original.name,
|
|
1552
|
+
});
|
|
1553
|
+
return { ok: false, error: jailError };
|
|
1400
1554
|
}
|
|
1401
1555
|
await audit.append({
|
|
1402
1556
|
ts: new Date().toISOString(),
|