@namzu/cli 0.5.1 → 0.6.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 (58) hide show
  1. package/dist/commands/__tests__/resume.test.d.ts +15 -0
  2. package/dist/commands/__tests__/resume.test.d.ts.map +1 -0
  3. package/dist/commands/__tests__/resume.test.js +116 -0
  4. package/dist/commands/__tests__/resume.test.js.map +1 -0
  5. package/dist/commands/__tests__/run-flags.test.d.ts +11 -0
  6. package/dist/commands/__tests__/run-flags.test.d.ts.map +1 -0
  7. package/dist/commands/__tests__/run-flags.test.js +190 -0
  8. package/dist/commands/__tests__/run-flags.test.js.map +1 -0
  9. package/dist/commands/__tests__/run-stream-flags.test.js +161 -11
  10. package/dist/commands/__tests__/run-stream-flags.test.js.map +1 -1
  11. package/dist/commands/doctor.js.map +1 -1
  12. package/dist/commands/resume.d.ts +60 -0
  13. package/dist/commands/resume.d.ts.map +1 -0
  14. package/dist/commands/resume.js +107 -0
  15. package/dist/commands/resume.js.map +1 -0
  16. package/dist/commands/run-flags.d.ts +76 -0
  17. package/dist/commands/run-flags.d.ts.map +1 -0
  18. package/dist/commands/run-flags.js +163 -0
  19. package/dist/commands/run-flags.js.map +1 -0
  20. package/dist/commands/run-stream.d.ts +0 -36
  21. package/dist/commands/run-stream.d.ts.map +1 -1
  22. package/dist/commands/run-stream.js +71 -98
  23. package/dist/commands/run-stream.js.map +1 -1
  24. package/dist/commands/run.d.ts +25 -0
  25. package/dist/commands/run.d.ts.map +1 -1
  26. package/dist/commands/run.js +185 -16
  27. package/dist/commands/run.js.map +1 -1
  28. package/dist/config/schema.d.ts +9 -0
  29. package/dist/config/schema.d.ts.map +1 -1
  30. package/dist/config/schema.js.map +1 -1
  31. package/dist/permissions/__tests__/mode.test.d.ts +10 -0
  32. package/dist/permissions/__tests__/mode.test.d.ts.map +1 -0
  33. package/dist/permissions/__tests__/mode.test.js +115 -0
  34. package/dist/permissions/__tests__/mode.test.js.map +1 -0
  35. package/dist/permissions/__tests__/rules.test.d.ts +10 -0
  36. package/dist/permissions/__tests__/rules.test.d.ts.map +1 -0
  37. package/dist/permissions/__tests__/rules.test.js +196 -0
  38. package/dist/permissions/__tests__/rules.test.js.map +1 -0
  39. package/dist/permissions/mode.d.ts +64 -0
  40. package/dist/permissions/mode.d.ts.map +1 -0
  41. package/dist/permissions/mode.js +32 -0
  42. package/dist/permissions/mode.js.map +1 -0
  43. package/dist/permissions/rules.d.ts +105 -0
  44. package/dist/permissions/rules.d.ts.map +1 -0
  45. package/dist/permissions/rules.js +163 -0
  46. package/dist/permissions/rules.js.map +1 -0
  47. package/dist/tui/App.d.ts.map +1 -1
  48. package/dist/tui/App.js +8 -4
  49. package/dist/tui/App.js.map +1 -1
  50. package/dist/tui/__tests__/working-directory.test.d.ts +13 -0
  51. package/dist/tui/__tests__/working-directory.test.d.ts.map +1 -0
  52. package/dist/tui/__tests__/working-directory.test.js +115 -0
  53. package/dist/tui/__tests__/working-directory.test.js.map +1 -0
  54. package/dist/tui/agent.d.ts +30 -3
  55. package/dist/tui/agent.d.ts.map +1 -1
  56. package/dist/tui/agent.js +58 -14
  57. package/dist/tui/agent.js.map +1 -1
  58. package/package.json +4 -4
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The mode, and its precedence against the config file.
3
+ *
4
+ * The rule being protected is one sentence: a mode governs only the calls no
5
+ * rule decided, so it can never reopen what a rule closed. The tests that
6
+ * matter are the ones that would catch a future change making
7
+ * `--permission-mode auto` able to run something the operator wrote `deny` for.
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=mode.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mode.test.d.ts","sourceRoot":"","sources":["../../../src/permissions/__tests__/mode.test.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The mode, and its precedence against the config file.
3
+ *
4
+ * The rule being protected is one sentence: a mode governs only the calls no
5
+ * rule decided, so it can never reopen what a rule closed. The tests that
6
+ * matter are the ones that would catch a future change making
7
+ * `--permission-mode auto` able to run something the operator wrote `deny` for.
8
+ */
9
+ import { VerificationGate, configureLogger, getRootLogger } from '@namzu/sdk';
10
+ import { describe, expect, it } from 'vitest';
11
+ import { makeResumeHandler } from '../../tui/agent.js';
12
+ import { PERMISSION_MODES, isPermissionMode, resolvePermissionMode } from '../mode.js';
13
+ import { compilePermissions } from '../rules.js';
14
+ configureLogger({ level: 'silent' });
15
+ describe('resolving the mode from flag, alias and terminal', () => {
16
+ it('takes an explicit flag over everything else', () => {
17
+ expect(resolvePermissionMode({ flag: 'strict', interactive: true })).toEqual({ mode: 'strict' });
18
+ expect(resolvePermissionMode({ flag: 'prompt', skipPermissions: true, interactive: false })).toEqual({ mode: 'prompt' });
19
+ });
20
+ it('refuses a mode it does not know, and names the ones it does', () => {
21
+ const result = resolvePermissionMode({ flag: 'yolo', interactive: false });
22
+ expect('error' in result && result.error).toContain('prompt, auto, strict');
23
+ });
24
+ it('maps the bypass aliases to auto', () => {
25
+ expect(resolvePermissionMode({ skipPermissions: true, interactive: true })).toEqual({
26
+ mode: 'auto',
27
+ });
28
+ });
29
+ it('asks when there is someone to ask, and does not when there is not', () => {
30
+ // `prompt` without a terminal would silently become `auto` anyway;
31
+ // resolving it as `auto` makes the resolved mode honest in a log.
32
+ expect(resolvePermissionMode({ interactive: true })).toEqual({ mode: 'prompt' });
33
+ expect(resolvePermissionMode({ interactive: false })).toEqual({ mode: 'auto' });
34
+ });
35
+ it('exposes exactly the modes it accepts', () => {
36
+ expect(PERMISSION_MODES.every(isPermissionMode)).toBe(true);
37
+ expect(isPermissionMode('trusted')).toBe(false);
38
+ });
39
+ });
40
+ describe('a mode decides only what no rule decided', () => {
41
+ function gate(config) {
42
+ const { rules } = compilePermissions(config);
43
+ return new VerificationGate({
44
+ enabled: true,
45
+ rules: [...rules],
46
+ allowReadOnlyTools: false,
47
+ denyDangerousPatterns: false,
48
+ logDecisions: false,
49
+ }, getRootLogger());
50
+ }
51
+ it('a denied call never reaches the mode at all', async () => {
52
+ // THE precedence test. `deny` is settled by the gate, so no mode — not
53
+ // even the one spelled --yolo — is ever consulted about it. If this ever
54
+ // fails, a flag has become able to lift a prohibition someone wrote down.
55
+ const decision = gate({ bash: 'deny' }).evaluate({
56
+ toolName: 'bash',
57
+ toolInput: { command: 'rm -rf /tmp/x' },
58
+ toolDef: undefined,
59
+ });
60
+ expect(decision.decision).toBe('deny');
61
+ });
62
+ it('an allowed call never reaches the mode either', () => {
63
+ const decision = gate({ bash: { 'git status*': 'allow' } }).evaluate({
64
+ toolName: 'bash',
65
+ toolInput: { command: 'git status' },
66
+ toolDef: undefined,
67
+ });
68
+ expect(decision.decision).toBe('allow');
69
+ });
70
+ });
71
+ describe('what each mode does with an undecided call', () => {
72
+ const destructive = [
73
+ { id: '1', name: 'write', input: { path: 'a.txt' }, isDestructive: true },
74
+ ];
75
+ it('auto approves it', async () => {
76
+ const handler = makeResumeHandler({ all: false }, undefined, 'auto');
77
+ const decision = await handler({ type: 'tool_review', toolCalls: destructive });
78
+ expect(decision).toEqual({ action: 'approve_tools' });
79
+ });
80
+ it('strict refuses it, and says asking again will not help', async () => {
81
+ // The mode that did not exist: an unattended run could only be `auto`, so
82
+ // a CI job either trusted the agent with everything or could not use it.
83
+ const handler = makeResumeHandler({ all: false }, undefined, 'strict');
84
+ const decision = (await handler({
85
+ type: 'tool_review',
86
+ toolCalls: destructive,
87
+ }));
88
+ expect(decision.action).toBe('reject_tools');
89
+ expect(decision.feedback).toContain('Asking again will not change it');
90
+ });
91
+ it('prompt asks the human, and honours the answer', async () => {
92
+ let asked = false;
93
+ const handler = makeResumeHandler({ all: false }, async () => {
94
+ asked = true;
95
+ return { kind: 'reject', feedback: 'not that one' };
96
+ }, 'prompt');
97
+ const decision = (await handler({
98
+ type: 'tool_review',
99
+ toolCalls: destructive,
100
+ }));
101
+ expect(asked).toBe(true);
102
+ expect(decision.action).toBe('reject_tools');
103
+ expect(decision.feedback).toBe('not that one');
104
+ });
105
+ it('read-only batches run under every mode, including strict', async () => {
106
+ // `strict` refuses what needs asking, and a read never needed asking.
107
+ // Refusing reads would make the mode unusable rather than strict.
108
+ const readOnly = [{ id: '1', name: 'read', input: {}, isDestructive: false }];
109
+ const handler = makeResumeHandler({ all: false }, undefined, 'strict');
110
+ expect(await handler({ type: 'tool_review', toolCalls: readOnly })).toEqual({
111
+ action: 'approve_tools',
112
+ });
113
+ });
114
+ });
115
+ //# sourceMappingURL=mode.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mode.test.js","sourceRoot":"","sources":["../../../src/permissions/__tests__/mode.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The rule compiler.
3
+ *
4
+ * The load-bearing case is the LAST one: a tool nobody wrote a rule for must
5
+ * still be asked about. A declarative permission model that quietly widened the
6
+ * default would be a security downgrade wearing a feature's clothes, and it
7
+ * would look exactly like this change from the outside.
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=rules.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.test.d.ts","sourceRoot":"","sources":["../../../src/permissions/__tests__/rules.test.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
@@ -0,0 +1,196 @@
1
+ /**
2
+ * The rule compiler.
3
+ *
4
+ * The load-bearing case is the LAST one: a tool nobody wrote a rule for must
5
+ * still be asked about. A declarative permission model that quietly widened the
6
+ * default would be a security downgrade wearing a feature's clothes, and it
7
+ * would look exactly like this change from the outside.
8
+ */
9
+ import { VerificationGate, configureLogger, getRootLogger } from '@namzu/sdk';
10
+ import { describe, expect, it } from 'vitest';
11
+ import { bySpecificity, compilePermissions, matchesPattern, patternToRegExpSource, } from '../rules.js';
12
+ describe('pattern matching', () => {
13
+ it('matches a literal command exactly', () => {
14
+ expect(matchesPattern('git status', 'git status')).toBe(true);
15
+ expect(matchesPattern('git statuses', 'git status')).toBe(false);
16
+ });
17
+ it('lets * stand for any run of characters and ? for one', () => {
18
+ expect(matchesPattern('git push --force', 'git push*')).toBe(true);
19
+ expect(matchesPattern('cat a.ts', 'cat ?.ts')).toBe(true);
20
+ expect(matchesPattern('cat ab.ts', 'cat ?.ts')).toBe(false);
21
+ });
22
+ it('matches the bare command for a trailing " *"', () => {
23
+ // `git push *` is the obvious way to write "any git push". Without this
24
+ // the rule silently misses the exact invocation it was written for.
25
+ expect(matchesPattern('git push', 'git push *')).toBe(true);
26
+ expect(matchesPattern('git push origin main', 'git push *')).toBe(true);
27
+ });
28
+ it('treats a backslash path as its forward-slash self, both sides', () => {
29
+ // One rule has to work on every platform; an operator should not have to
30
+ // write the same path twice.
31
+ expect(matchesPattern('src\\index.ts', 'src/*.ts')).toBe(true);
32
+ expect(matchesPattern('src/index.ts', 'src\\*.ts')).toBe(true);
33
+ });
34
+ it('does not let a pattern character smuggle in a regex', () => {
35
+ // `.` is a literal dot, not "any character", or a rule meant to allow
36
+ // `a.ts` would also allow `axts`.
37
+ expect(matchesPattern('axts', 'a.ts')).toBe(false);
38
+ expect(matchesPattern('a.ts', 'a.ts')).toBe(true);
39
+ expect(patternToRegExpSource('a+b')).toBe('^a\\+b$');
40
+ });
41
+ it('anchors, so a pattern is not a substring search', () => {
42
+ expect(matchesPattern('sudo git status', 'git status')).toBe(false);
43
+ });
44
+ });
45
+ describe('specificity ordering', () => {
46
+ // The kernel evaluates first-match-wins, so order decides meaning: a bare
47
+ // `*` sorted first would make every rule after it dead config.
48
+ it('puts a bare star last', () => {
49
+ expect(['*', 'git push*', 'git status'].sort(bySpecificity)).toEqual([
50
+ 'git status',
51
+ 'git push*',
52
+ '*',
53
+ ]);
54
+ });
55
+ it('puts a longer literal prefix first', () => {
56
+ expect(['git*', 'git push*'].sort(bySpecificity)).toEqual(['git push*', 'git*']);
57
+ });
58
+ it('is stable for two patterns of equal specificity', () => {
59
+ expect(['b*', 'a*'].sort(bySpecificity)).toEqual(['a*', 'b*']);
60
+ });
61
+ });
62
+ describe('compiling a permissions table', () => {
63
+ it('turns a bare allow and deny into by-name rules', () => {
64
+ const { rules } = compilePermissions({ read: 'allow', bash: 'deny' });
65
+ expect(rules).toEqual([
66
+ { type: 'allow_by_name', toolNames: ['read'] },
67
+ { type: 'deny_by_name', toolNames: ['bash'] },
68
+ ]);
69
+ });
70
+ it('emits pattern rules most-specific-first', () => {
71
+ const { rules } = compilePermissions({ bash: { '*': 'deny', 'git status*': 'allow' } });
72
+ expect(rules.map((r) => ('decision' in r ? r.decision : r.type))).toEqual(['allow', 'deny']);
73
+ });
74
+ it('emits nothing for ask, because the gate already asks by default', () => {
75
+ // If "ask" emitted a rule it would have to mean something different from
76
+ // the absence of a rule, and it does not.
77
+ const { rules } = compilePermissions({ edit: 'ask', bash: { 'rm *': 'ask' } });
78
+ expect(rules).toEqual([]);
79
+ });
80
+ it('reports a bad effect instead of dropping it in silence', () => {
81
+ // A permission someone believes is in force and which was quietly
82
+ // discarded is the worst outcome here — worse than refusing to start.
83
+ const { rules, diagnostics } = compilePermissions({
84
+ bash: { 'git push*': 'maybe' },
85
+ });
86
+ expect(rules).toEqual([]);
87
+ expect(diagnostics).toHaveLength(1);
88
+ expect(diagnostics[0]?.tool).toBe('bash');
89
+ expect(diagnostics[0]?.pattern).toBe('git push*');
90
+ });
91
+ it('keeps the other rules when one line is unreadable', () => {
92
+ const { rules, diagnostics } = compilePermissions({
93
+ read: 'allow',
94
+ bash: 7,
95
+ glob: 'allow',
96
+ });
97
+ expect(rules).toHaveLength(2);
98
+ expect(diagnostics).toHaveLength(1);
99
+ });
100
+ it('a tool nobody wrote a rule for produces no rule, so the gate asks', () => {
101
+ // THE case. The kernel's fallback for an unmatched call is `review`, so
102
+ // emitting nothing is what makes an unconfigured or newly bridged tool
103
+ // prompt. There is deliberately no way to spell "allow by omission":
104
+ // widening the default has to be something an operator typed.
105
+ const { rules } = compilePermissions({ read: 'allow' });
106
+ expect(rules.some((r) => 'toolNames' in r && r.toolNames.includes('some_new_tool'))).toBe(false);
107
+ expect(rules).toHaveLength(1);
108
+ });
109
+ it('is empty for absent config, which is the same as asking about everything', () => {
110
+ expect(compilePermissions(undefined)).toEqual({ rules: [], diagnostics: [] });
111
+ });
112
+ });
113
+ configureLogger({ level: 'silent' });
114
+ describe('the rules actually decide a real call', () => {
115
+ // The compiler tests above assert the SHAPE of the emitted rules. Shape is
116
+ // not behaviour: the first version of this compiler emitted `target: 'args'`
117
+ // with an anchored pattern, which matches `JSON.stringify(input)` — so
118
+ // `^git push.*$` could never match `{"command":"git push origin main"}` and
119
+ // every pattern rule decided nothing at all. It looked correct in every
120
+ // shape assertion. These drive the kernel's own gate instead.
121
+ function decide(config, tool, input) {
122
+ const { rules } = compilePermissions(config);
123
+ const gate = new VerificationGate({
124
+ enabled: true,
125
+ rules: [...rules],
126
+ allowReadOnlyTools: false,
127
+ denyDangerousPatterns: false,
128
+ logDecisions: false,
129
+ }, getRootLogger());
130
+ return gate.evaluate({ toolName: tool, toolInput: input, toolDef: undefined });
131
+ }
132
+ it('allows the command an operator allowed', () => {
133
+ expect(decide({ bash: { 'git status*': 'allow' } }, 'bash', { command: 'git status' }).decision).toBe('allow');
134
+ });
135
+ it('denies the command an operator denied', () => {
136
+ const result = decide({ bash: { 'git push*': 'deny' } }, 'bash', {
137
+ command: 'git push origin main',
138
+ });
139
+ expect(result.decision).toBe('deny');
140
+ });
141
+ it('does not let one tool rule decide another tool call', () => {
142
+ // A rule written under `bash` must not decide an `edit` call whose
143
+ // arguments happen to contain the same text. The kernel's pattern rules
144
+ // carry no tool scope, so this is the CLI's job and it is easy to lose.
145
+ const result = decide({ bash: { 'git push*': 'deny' } }, 'edit', {
146
+ content: 'run git push to deploy',
147
+ });
148
+ expect(result.decision).toBe('review');
149
+ });
150
+ it('asks about a tool nobody wrote a rule for', () => {
151
+ // The load-bearing case, driven end to end rather than inferred.
152
+ expect(decide({ read: 'allow' }, 'some_new_bridged_tool', { x: 1 }).decision).toBe('review');
153
+ });
154
+ it('prefers the specific rule over the catch-all, whatever the config order', () => {
155
+ const config = { bash: { '*': 'allow', 'git push*': 'deny' } };
156
+ expect(decide(config, 'bash', { command: 'git push --force' }).decision).toBe('deny');
157
+ expect(decide(config, 'bash', { command: 'ls' }).decision).toBe('allow');
158
+ });
159
+ });
160
+ describe('what a denial actually says to the model', () => {
161
+ // The decision was never the part that was broken. A model told only
162
+ // "denied" rewords the command and tries again; a model told WHICH rule
163
+ // stopped it, and whether a different input could ever pass, can stop and
164
+ // say so. These assert the sentence, not the verdict.
165
+ function reasonFor(config, tool, input) {
166
+ const { rules } = compilePermissions(config);
167
+ const gate = new VerificationGate({
168
+ enabled: true,
169
+ rules: [...rules],
170
+ allowReadOnlyTools: false,
171
+ denyDangerousPatterns: false,
172
+ logDecisions: false,
173
+ }, getRootLogger());
174
+ return gate.evaluate({ toolName: tool, toolInput: input, toolDef: undefined }).reason;
175
+ }
176
+ it('a by-name denial says the tool is refused, so a reworded input will not help', () => {
177
+ const reason = reasonFor({ bash: 'deny' }, 'bash', { command: 'ls' });
178
+ expect(reason).toContain('bash');
179
+ // The part that stops the retry loop.
180
+ expect(reason).toContain('a different input will not change it');
181
+ });
182
+ it('a pattern denial quotes the rule that matched', () => {
183
+ const reason = reasonFor({ bash: { 'git push*': 'deny' } }, 'bash', {
184
+ command: 'git push --force',
185
+ });
186
+ expect(reason).toContain('pattern rule');
187
+ // The model is shown the rule itself, not the name of its category, so
188
+ // it can tell which of its options are closed and which are not.
189
+ expect(reason).toContain('git push');
190
+ });
191
+ it('an unmatched call says nothing matched, rather than naming a rule', () => {
192
+ const reason = reasonFor({ read: 'allow' }, 'some_new_tool', {});
193
+ expect(reason).toBe('No matching rule found');
194
+ });
195
+ });
196
+ //# sourceMappingURL=rules.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.test.js","sourceRoot":"","sources":["../../../src/permissions/__tests__/rules.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * How a run resolves the calls no rule decided.
3
+ *
4
+ * The `[permissions]` table says what a tool may do. This says what happens to
5
+ * everything it did not cover. The two axes are separate on purpose: a rule is
6
+ * a durable statement an operator reviewed, and a mode is a property of ONE
7
+ * invocation — the difference between "we never force-push" and "this run is
8
+ * unattended".
9
+ *
10
+ * ## Precedence between a flag and the config file
11
+ *
12
+ * A mode only governs calls the gate routed to REVIEW. A rule that denied a
13
+ * call already stopped it, and a rule that allowed one never asked, so neither
14
+ * reaches the mode at all. **A mode can therefore never reopen what a rule
15
+ * closed, and `--permission-mode` cannot widen a `deny`.**
16
+ *
17
+ * That direction is deliberate. The config file is written once, read by
18
+ * whoever reviews the repository, and changed on purpose; a flag is typed in a
19
+ * hurry by someone who wants to get on with it. Letting the hurried thing
20
+ * overrule the considered one would make every `deny` in the file advisory, and
21
+ * a prohibition that a flag can lift is not a prohibition. The dangerous-pattern
22
+ * floor sits above both and no mode reaches it either.
23
+ */
24
+ export type PermissionMode =
25
+ /** Ask a human. The default when a terminal is attached. */
26
+ 'prompt'
27
+ /**
28
+ * Approve it. The default with no terminal, and what every headless run has
29
+ * always done — named here rather than left implicit.
30
+ */
31
+ | 'auto'
32
+ /**
33
+ * Refuse it. Nothing runs unless a rule allowed it by name or pattern.
34
+ *
35
+ * The mode that did not exist before: an unattended run could previously
36
+ * only be `auto`, so a CI job either trusted the agent with everything or
37
+ * could not use it. This makes the allowlist the whole permission surface,
38
+ * which is the only form an unattended run can actually be reasoned about.
39
+ */
40
+ | 'strict';
41
+ export declare const PERMISSION_MODES: readonly PermissionMode[];
42
+ export declare function isPermissionMode(value: unknown): value is PermissionMode;
43
+ /**
44
+ * The mode for a run, from the flag, the bypass alias, and whether anyone is
45
+ * there to answer.
46
+ *
47
+ * `--yolo` / `--dangerously-skip-permissions` map to `auto`. They were accepted
48
+ * and documented as doing nothing, which was true and unsatisfying; now they
49
+ * say the same thing the mode vocabulary says, and in the TUI they do what
50
+ * their name has always implied. Neither reaches a `deny` rule or the
51
+ * dangerous-pattern floor, so the name still overstates what it can do —
52
+ * deliberately, because it should read as more dangerous than it is rather than
53
+ * less.
54
+ */
55
+ export declare function resolvePermissionMode(opts: {
56
+ readonly flag?: string | null;
57
+ readonly skipPermissions?: boolean;
58
+ readonly interactive: boolean;
59
+ }): {
60
+ mode: PermissionMode;
61
+ } | {
62
+ error: string;
63
+ };
64
+ //# sourceMappingURL=mode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mode.d.ts","sourceRoot":"","sources":["../../src/permissions/mode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,MAAM,cAAc;AACzB,4DAA4D;AAC1D,QAAQ;AACV;;;GAGG;GACD,MAAM;AACR;;;;;;;GAOG;GACD,QAAQ,CAAA;AAEX,eAAO,MAAM,gBAAgB,EAAE,SAAS,cAAc,EAAiC,CAAA;AAEvF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,cAAc,CAExE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAC3C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAA;IAClC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;CAC7B,GAAG;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAa/C"}
@@ -0,0 +1,32 @@
1
+ export const PERMISSION_MODES = ['prompt', 'auto', 'strict'];
2
+ export function isPermissionMode(value) {
3
+ return typeof value === 'string' && PERMISSION_MODES.includes(value);
4
+ }
5
+ /**
6
+ * The mode for a run, from the flag, the bypass alias, and whether anyone is
7
+ * there to answer.
8
+ *
9
+ * `--yolo` / `--dangerously-skip-permissions` map to `auto`. They were accepted
10
+ * and documented as doing nothing, which was true and unsatisfying; now they
11
+ * say the same thing the mode vocabulary says, and in the TUI they do what
12
+ * their name has always implied. Neither reaches a `deny` rule or the
13
+ * dangerous-pattern floor, so the name still overstates what it can do —
14
+ * deliberately, because it should read as more dangerous than it is rather than
15
+ * less.
16
+ */
17
+ export function resolvePermissionMode(opts) {
18
+ if (opts.flag != null && opts.flag !== '') {
19
+ if (!isPermissionMode(opts.flag)) {
20
+ return {
21
+ error: `--permission-mode must be one of: ${PERMISSION_MODES.join(', ')} (got "${opts.flag}")`,
22
+ };
23
+ }
24
+ return { mode: opts.flag };
25
+ }
26
+ if (opts.skipPermissions)
27
+ return { mode: 'auto' };
28
+ // No terminal means nobody to ask, so `prompt` would silently become `auto`
29
+ // anyway. Saying so here keeps the resolved mode honest in a log.
30
+ return { mode: opts.interactive ? 'prompt' : 'auto' };
31
+ }
32
+ //# sourceMappingURL=mode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mode.js","sourceRoot":"","sources":["../../src/permissions/mode.ts"],"names":[],"mappings":"AAyCA,MAAM,CAAC,MAAM,gBAAgB,GAA8B,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAA;AAEvF,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC9C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,gBAAsC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;AAC5F,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAIrC;IACA,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,EAAE,EAAE,CAAC;QAC3C,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAClC,OAAO;gBACN,KAAK,EAAE,qCAAqC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,CAAC,IAAI,IAAI;aAC9F,CAAA;QACF,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAA;IAC3B,CAAC;IACD,IAAI,IAAI,CAAC,eAAe;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;IACjD,4EAA4E;IAC5E,kEAAkE;IAClE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAA;AACtD,CAAC"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Operator-authored tool permissions, compiled to the kernel's rule vocabulary.
3
+ *
4
+ * The kernel already has the engine — `VerificationRule[]` with an
5
+ * allow/deny/review decision, evaluated first-match-wins. What was missing was
6
+ * any way for a person to author those rules: the CLI passed `rules: []`, so a
7
+ * gate with seven rule types ran with none of them and every decision fell back
8
+ * to "ask a human". This turns a `[permissions]` table into that array.
9
+ *
10
+ * The whole surface is pure: config in, rules out. The gate stays the only
11
+ * thing that decides anything, which is what keeps this testable without a run.
12
+ */
13
+ import type { VerificationRule } from '@namzu/sdk';
14
+ /** What the operator wants to happen when a rule matches. */
15
+ export type PermissionEffect = 'allow' | 'ask' | 'deny';
16
+ export declare function isPermissionEffect(value: unknown): value is PermissionEffect;
17
+ /**
18
+ * One tool's policy: a single effect for every call, or per-argument patterns.
19
+ *
20
+ * `bash = "ask"` and
21
+ * `bash = { "git status*" = "allow", "*" = "ask" }`
22
+ * are both meant to be readable by someone who has never seen this file before,
23
+ * which is the whole point of putting it in config rather than in code.
24
+ */
25
+ export type ToolPermission = PermissionEffect | Readonly<Record<string, PermissionEffect>>;
26
+ export type PermissionsConfig = Readonly<Record<string, ToolPermission>>;
27
+ export interface CompileDiagnostic {
28
+ readonly tool: string;
29
+ readonly pattern?: string;
30
+ readonly message: string;
31
+ }
32
+ export interface CompiledPermissions {
33
+ readonly rules: readonly VerificationRule[];
34
+ /**
35
+ * What was wrong with the config, for the caller to print.
36
+ *
37
+ * Collected rather than thrown: one unreadable line should cost that line,
38
+ * not the other nine rules an operator wrote. But it must be SAID — a
39
+ * permission someone believes is in force and which was silently dropped is
40
+ * the worst outcome available here, worse than refusing to start.
41
+ */
42
+ readonly diagnostics: readonly CompileDiagnostic[];
43
+ }
44
+ /**
45
+ * Turn a glob-ish pattern into an anchored regex source.
46
+ *
47
+ * `*` matches any run of characters, `?` exactly one; everything else is
48
+ * literal. Backslashes become forward slashes on both sides so one rule works
49
+ * on every platform — an operator writing `src/*` should not have to write it
50
+ * twice.
51
+ *
52
+ * A pattern ending in `<space>*` also matches the bare command: `git push *`
53
+ * covers `git push`. Without that, the obvious rule silently misses the exact
54
+ * invocation it was written to catch, which is the failure this whole file
55
+ * exists to stop.
56
+ */
57
+ export declare function patternToRegExpSource(pattern: string): string;
58
+ /**
59
+ * A pattern that matches this tool, and only this tool, doing this thing.
60
+ *
61
+ * The kernel's `custom_pattern` has no tool scope: `target: 'args'` tests
62
+ * `JSON.stringify(input)` for ANY tool, so a rule written under `bash` would
63
+ * also decide an `edit` call whose arguments happened to match. `target:
64
+ * 'both'` tests `` `${toolName} ${JSON.stringify(input)}` ``, so the tool name
65
+ * can be pinned by writing it into the pattern — which is what this does.
66
+ *
67
+ * The argument half is deliberately unanchored on both sides, because the
68
+ * operator writes `git push*` while the gate sees
69
+ * `bash {"command":"git push origin main"}`. An anchored argument pattern would
70
+ * match nothing at all — a rule that reads like a prohibition and silently
71
+ * permits everything, which is worse than having no rule.
72
+ *
73
+ * The looseness is real and worth naming: a pattern can match text in any
74
+ * argument field, not only the one the operator had in mind. Narrowing that
75
+ * needs a rule type that names the argument, which is the kernel's to add.
76
+ */
77
+ export declare function toolScopedPattern(tool: string, pattern: string): string;
78
+ /** Whether `input` satisfies `pattern`, with the matching above. */
79
+ export declare function matchesPattern(input: string, pattern: string): boolean;
80
+ /**
81
+ * Order patterns so the most specific is tried first.
82
+ *
83
+ * The kernel evaluates first-match-wins, so `bash = { "*" = "ask", "git push*"
84
+ * = "deny" }` would otherwise resolve to `ask` and the deny would be dead —
85
+ * config that reads like a prohibition and is not one. Sorting here means an
86
+ * operator does not have to know the evaluation order to write what they mean.
87
+ *
88
+ * "Specific" is: a bare `*` is always last, then longer literal prefixes before
89
+ * shorter ones, then alphabetical so the result is stable to read and to test.
90
+ */
91
+ export declare function bySpecificity(a: string, b: string): number;
92
+ /**
93
+ * Compile a `[permissions]` table into kernel rules.
94
+ *
95
+ * `ask` emits nothing. That is not an omission: the gate's fallback for an
96
+ * unmatched call is already `review`, so the rule that says "ask" and the
97
+ * absence of any rule have to mean the same thing or the config would be
98
+ * lying about one of them. It also keeps the emitted array small enough for a
99
+ * person to read in a log.
100
+ *
101
+ * A tool nobody wrote a rule for is therefore asked about, and there is no way
102
+ * to spell "allow everything by omission" — the only way to widen is to say so.
103
+ */
104
+ export declare function compilePermissions(config: PermissionsConfig | undefined): CompiledPermissions;
105
+ //# sourceMappingURL=rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/permissions/rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAElD,6DAA6D;AAC7D,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAA;AAIvD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAE5E;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,GAAG,gBAAgB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAA;AAE1F,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAA;AAExE,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,KAAK,EAAE,SAAS,gBAAgB,EAAE,CAAA;IAC3C;;;;;;;OAOG;IACH,QAAQ,CAAC,WAAW,EAAE,SAAS,iBAAiB,EAAE,CAAA;CAClD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAQ7D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAIvE;AAMD,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAKtE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAU1D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,iBAAiB,GAAG,SAAS,GAAG,mBAAmB,CAgD7F"}