@element47/ag 4.5.1 → 4.5.2
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 +106 -4
- package/dist/cli/repl.d.ts +7 -2
- package/dist/cli/repl.d.ts.map +1 -1
- package/dist/cli/repl.js +180 -22
- package/dist/cli/repl.js.map +1 -1
- package/dist/cli.js +8 -8
- package/dist/cli.js.map +1 -1
- package/dist/core/__tests__/agent.test.js +42 -1
- package/dist/core/__tests__/agent.test.js.map +1 -1
- package/dist/core/__tests__/context.test.js +37 -0
- package/dist/core/__tests__/context.test.js.map +1 -1
- package/dist/core/__tests__/guardrails.test.d.ts +2 -0
- package/dist/core/__tests__/guardrails.test.d.ts.map +1 -0
- package/dist/core/__tests__/guardrails.test.js +400 -0
- package/dist/core/__tests__/guardrails.test.js.map +1 -0
- package/dist/core/__tests__/permission-manager.test.d.ts +2 -0
- package/dist/core/__tests__/permission-manager.test.d.ts.map +1 -0
- package/dist/core/__tests__/permission-manager.test.js +246 -0
- package/dist/core/__tests__/permission-manager.test.js.map +1 -0
- package/dist/core/__tests__/streaming.test.js +8 -1
- package/dist/core/__tests__/streaming.test.js.map +1 -1
- package/dist/core/agent.d.ts +9 -0
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +50 -15
- package/dist/core/agent.js.map +1 -1
- package/dist/core/guardrails.d.ts +32 -0
- package/dist/core/guardrails.d.ts.map +1 -0
- package/dist/core/guardrails.js +149 -0
- package/dist/core/guardrails.js.map +1 -0
- package/dist/core/loader.d.ts +6 -2
- package/dist/core/loader.d.ts.map +1 -1
- package/dist/core/loader.js +23 -9
- package/dist/core/loader.js.map +1 -1
- package/dist/core/permissions.d.ts +60 -0
- package/dist/core/permissions.d.ts.map +1 -0
- package/dist/core/permissions.js +252 -0
- package/dist/core/permissions.js.map +1 -0
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/registry.js +16 -0
- package/dist/core/registry.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +26 -3
- package/dist/core/skills.js.map +1 -1
- package/dist/core/types.d.ts +15 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/memory/__tests__/memory.test.js +28 -0
- package/dist/memory/__tests__/memory.test.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -127,6 +127,46 @@ export default {
|
|
|
127
127
|
|
|
128
128
|
That's it. No config, no registry. Use `/tools` in the REPL to see what's loaded.
|
|
129
129
|
|
|
130
|
+
### Permission Keys
|
|
131
|
+
|
|
132
|
+
By default, custom tools require approval for every call (or you allow all calls with `toolname(*)`). To enable fine-grained permission patterns, add a `permissionKey` to your tool:
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
// .ag/tools/deploy.mjs
|
|
136
|
+
export default {
|
|
137
|
+
type: "function",
|
|
138
|
+
function: {
|
|
139
|
+
name: "deploy",
|
|
140
|
+
description: "Deploy to an environment",
|
|
141
|
+
parameters: {
|
|
142
|
+
type: "object",
|
|
143
|
+
properties: {
|
|
144
|
+
target: { type: "string", enum: ["staging", "production"] },
|
|
145
|
+
branch: { type: "string" }
|
|
146
|
+
},
|
|
147
|
+
required: ["target"]
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
permissionKey: { qualifier: "target" },
|
|
151
|
+
execute: async ({ target, branch }) => { /* ... */ }
|
|
152
|
+
};
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Now permission patterns can target specific argument values:
|
|
156
|
+
|
|
157
|
+
| Pattern | Effect |
|
|
158
|
+
|---------|--------|
|
|
159
|
+
| `deploy(staging)` | Allow staging deploys |
|
|
160
|
+
| `deploy(production)` | Allow production deploys |
|
|
161
|
+
| `deploy(*)` | Allow all deploys |
|
|
162
|
+
|
|
163
|
+
**`permissionKey` fields:**
|
|
164
|
+
|
|
165
|
+
- `qualifier` (required) — arg name whose value becomes the pattern qualifier. E.g., `{ qualifier: "target" }` + `target: "staging"` produces `deploy(staging)`.
|
|
166
|
+
- `value` (optional) — arg name whose value is matched by the glob portion. E.g., `{ qualifier: "action", value: "path" }` produces `mytool(read:configs/**)`.
|
|
167
|
+
|
|
168
|
+
Without `permissionKey`, the only available pattern is `toolname(*)`.
|
|
169
|
+
|
|
130
170
|
## Skills
|
|
131
171
|
|
|
132
172
|
Skills are reusable prompt instructions (with optional tools) that the agent activates on-demand. Browse and install from [skills.sh](https://skills.sh):
|
|
@@ -234,22 +274,84 @@ Or set it permanently:
|
|
|
234
274
|
|
|
235
275
|
## Permissions
|
|
236
276
|
|
|
237
|
-
In REPL mode, ag prompts before executing mutating operations:
|
|
277
|
+
In REPL mode, ag prompts before executing mutating operations. You can allow once, remember for the session, or save to the project:
|
|
238
278
|
|
|
239
279
|
```
|
|
240
|
-
? bash: npm test (y/n)
|
|
280
|
+
? bash: npm test (y)es / (a)lways / (p)roject / (n)o a
|
|
281
|
+
+ Session rule: bash(npm:*)
|
|
241
282
|
✓ [bash] All tests passed
|
|
242
|
-
? file(write): src/utils.ts (y/n)
|
|
283
|
+
? file(write): src/utils.ts (y)es / (a)lways / (p)roject / (n)o p
|
|
284
|
+
+ Saved to .ag/permissions.json: file(write:src/**)
|
|
243
285
|
✓ [file] Wrote src/utils.ts (24 lines, 680B)
|
|
244
286
|
```
|
|
245
287
|
|
|
288
|
+
**Prompt options:**
|
|
289
|
+
- **y** — allow this one time
|
|
290
|
+
- **a** — allow and remember the pattern for this session
|
|
291
|
+
- **p** — allow and save the pattern to project (`.ag/permissions.json`)
|
|
292
|
+
- **n** — deny this one time
|
|
293
|
+
|
|
294
|
+
### Pattern Syntax
|
|
295
|
+
|
|
296
|
+
Patterns use `Tool(qualifier:glob)` format:
|
|
297
|
+
|
|
298
|
+
| Pattern | Matches |
|
|
299
|
+
|---------|---------|
|
|
300
|
+
| `bash(npm:*)` | Any bash command starting with `npm` |
|
|
301
|
+
| `bash(git:*)` | Any bash command starting with `git` |
|
|
302
|
+
| `file(write:src/**)` | File writes anywhere under `src/` |
|
|
303
|
+
| `file(edit:*)` | All file edits |
|
|
304
|
+
| `git(commit)` | Git commit |
|
|
305
|
+
| `web(fetch:*github.com*)` | Fetch from GitHub domains |
|
|
306
|
+
| `bash(*)` | All bash commands |
|
|
307
|
+
| `*` | Everything |
|
|
308
|
+
|
|
309
|
+
### Rule Scopes
|
|
310
|
+
|
|
311
|
+
| Scope | Storage | Lifetime |
|
|
312
|
+
|-------|---------|----------|
|
|
313
|
+
| Session | In-memory | Until REPL exits |
|
|
314
|
+
| Project | `.ag/permissions.json` | Persists across sessions |
|
|
315
|
+
| Global | `~/.ag/permissions.json` | Persists everywhere |
|
|
316
|
+
|
|
317
|
+
Deny rules always override allow rules. Use `/permissions` to manage rules interactively.
|
|
318
|
+
|
|
319
|
+
### Built-in Classifications
|
|
320
|
+
|
|
246
321
|
**Always allowed (no prompt):** `file(read)`, `file(list)`, `grep(*)`, `memory(*)`, `plan(*)`, `skill(*)`, `git(status)`, `web(search)`
|
|
247
322
|
|
|
248
323
|
**Prompted:** `bash`, `file(write)`, `file(edit)`, `git(commit/push/branch)`, `web(fetch)`
|
|
249
324
|
|
|
250
325
|
**Always blocked:** `rm -rf /`, fork bombs, `sudo rm`, pipe-to-shell (enforced in code regardless of approval)
|
|
251
326
|
|
|
252
|
-
Skip prompts with `ag -y` or `--yes`. One-shot mode (`ag "query"`) auto-approves.
|
|
327
|
+
Skip all prompts with `ag -y` or `--yes`. One-shot mode (`ag "query"`) auto-approves.
|
|
328
|
+
|
|
329
|
+
## Guardrails
|
|
330
|
+
|
|
331
|
+
All externally-loaded tools and skills are scanned at load time for prompt injection and other security issues. This applies to:
|
|
332
|
+
|
|
333
|
+
- Custom tools (`.mjs` files in `~/.ag/tools/` and `.ag/tools/`)
|
|
334
|
+
- Skills (`SKILL.md` files in `~/.ag/skills/` and `.ag/skills/`)
|
|
335
|
+
- Skills installed from the registry via `/skill add`
|
|
336
|
+
|
|
337
|
+
**What gets checked:**
|
|
338
|
+
|
|
339
|
+
| Category | Severity | Examples |
|
|
340
|
+
|----------|----------|---------|
|
|
341
|
+
| Direct injection | Block | "ignore previous instructions", "system override", "reveal prompt" |
|
|
342
|
+
| Encoded payloads | Block | Base64-encoded injection attempts, HTML entity obfuscation |
|
|
343
|
+
| Hidden content | Block | HTML comments with instructions, zero-width characters, control chars |
|
|
344
|
+
| Exfiltration | Block/Warn | `fetch()` calls in descriptions (block), URLs/emails (warn) |
|
|
345
|
+
| Suspicious overrides | Warn | "bypass security", "auto-approve", "run without permission" |
|
|
346
|
+
|
|
347
|
+
**Blocked** items are skipped entirely with a warning. **Warned** items still load but emit a warning to stderr:
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
Warning: evil-tool.mjs blocked by guardrails: tool "evil" description: prompt injection: "ignore previous instructions"
|
|
351
|
+
Warning: shady-tool.mjs: tool "shady" description: description contains URL
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
When installing a skill from the registry, files are scanned before being written to disk. If the core `SKILL.md` is blocked, the entire installation is aborted.
|
|
253
355
|
|
|
254
356
|
## Streaming
|
|
255
357
|
|
package/dist/cli/repl.d.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { Interface } from 'node:readline';
|
|
2
2
|
import { Agent } from '../core/agent.js';
|
|
3
|
+
import { PermissionManager } from '../core/permissions.js';
|
|
3
4
|
import type { ConfirmToolCall } from '../core/types.js';
|
|
4
5
|
export declare function createConfirmCallback(sharedRl?: Interface): ConfirmToolCall & {
|
|
5
6
|
pauseSpinner: (() => void) | null;
|
|
6
7
|
};
|
|
8
|
+
export declare function createPermissionCallback(pm: PermissionManager, sharedRl?: Interface): ConfirmToolCall & {
|
|
9
|
+
pauseSpinner: (() => void) | null;
|
|
10
|
+
};
|
|
7
11
|
export declare class REPL {
|
|
8
12
|
private readonly agent;
|
|
9
13
|
private readonly rl;
|
|
10
|
-
private readonly
|
|
11
|
-
|
|
14
|
+
private readonly pm;
|
|
15
|
+
private confirmCb;
|
|
16
|
+
constructor(agent: Agent, pm?: PermissionManager, confirmCb?: ReturnType<typeof createPermissionCallback>);
|
|
12
17
|
start(): Promise<void>;
|
|
13
18
|
private handleCommand;
|
|
14
19
|
private ask;
|
package/dist/cli/repl.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repl.d.ts","sourceRoot":"","sources":["../../src/cli/repl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,SAAS,EAAsB,MAAM,eAAe,CAAC;AAC/E,OAAO,EAAE,KAAK,EAA0B,MAAM,kBAAkB,CAAC;AAKjE,OAAO,KAAK,EAAE,eAAe,
|
|
1
|
+
{"version":3,"file":"repl.d.ts","sourceRoot":"","sources":["../../src/cli/repl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,SAAS,EAAsB,MAAM,eAAe,CAAC;AAC/E,OAAO,EAAE,KAAK,EAA0B,MAAM,kBAAkB,CAAC;AAKjE,OAAO,EAAE,iBAAiB,EAAgB,MAAM,wBAAwB,CAAC;AACzE,OAAO,KAAK,EAAE,eAAe,EAAiB,MAAM,kBAAkB,CAAC;AAqCvE,wBAAgB,qBAAqB,CAAC,QAAQ,CAAC,EAAE,SAAS,GAAG,eAAe,GAAG;IAAE,YAAY,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;CAAE,CAgBnH;AAED,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,iBAAiB,EAAE,QAAQ,CAAC,EAAE,SAAS,GAAG,eAAe,GAAG;IAAE,YAAY,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;CAAE,CAiD7I;AA8GD,qBAAa,IAAI;IACf,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAY;IAC/B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAA2B;IAC9C,OAAO,CAAC,SAAS,CAAqD;gBAE1D,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,iBAAiB,EAAE,SAAS,CAAC,EAAE,UAAU,CAAC,OAAO,wBAAwB,CAAC;IAqBnG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAwMd,aAAa;IA8Y3B,OAAO,CAAC,GAAG;CAGZ"}
|
package/dist/cli/repl.js
CHANGED
|
@@ -3,6 +3,7 @@ import { loadConfig, saveConfig, configPath } from '../core/config.js';
|
|
|
3
3
|
import { searchRegistry, installSkill, removeSkill, formatInstalls } from '../core/registry.js';
|
|
4
4
|
import { C, renderMarkdown } from '../core/colors.js';
|
|
5
5
|
import { VERSION } from '../core/version.js';
|
|
6
|
+
import { inferPattern } from '../core/permissions.js';
|
|
6
7
|
function truncateCommand(command, maxLen = 80) {
|
|
7
8
|
const firstLine = command.split('\n')[0];
|
|
8
9
|
if (firstLine.length <= maxLen) {
|
|
@@ -19,29 +20,90 @@ function formatToolSummary(toolName, args) {
|
|
|
19
20
|
default: return `${toolName}(${JSON.stringify(args).slice(0, 80)})`;
|
|
20
21
|
}
|
|
21
22
|
}
|
|
23
|
+
function promptQuestion(prompt, sharedRl) {
|
|
24
|
+
if (sharedRl) {
|
|
25
|
+
// Shared readline already manages terminal — no raw mode changes needed
|
|
26
|
+
return new Promise(resolve => sharedRl.question(prompt, resolve));
|
|
27
|
+
}
|
|
28
|
+
// Fallback: create a temporary readline; exit raw mode so it can work properly
|
|
29
|
+
const wasRaw = process.stdin.isTTY && process.stdin.isRaw;
|
|
30
|
+
if (wasRaw)
|
|
31
|
+
process.stdin.setRawMode(false);
|
|
32
|
+
return new Promise(resolve => {
|
|
33
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
34
|
+
rl.question(prompt, a => { rl.close(); resolve(a); });
|
|
35
|
+
}).finally(() => { if (wasRaw)
|
|
36
|
+
process.stdin.setRawMode(true); });
|
|
37
|
+
}
|
|
38
|
+
const SIMPLE_OPTS = ` ${C.cyan}y${C.reset}/${C.cyan}n${C.reset} > `;
|
|
39
|
+
const FULL_OPTS = ` ${C.cyan}y${C.reset}/${C.cyan}n${C.reset} ${C.dim}a${C.reset}=${C.dim}always ${C.dim}p${C.reset}=${C.dim}project ${C.dim}d${C.reset}=${C.dim}deny session ${C.dim}D${C.reset}=${C.dim}deny project${C.reset} > `;
|
|
22
40
|
export function createConfirmCallback(sharedRl) {
|
|
23
41
|
const cb = async (toolName, args) => {
|
|
24
|
-
// Stop any active spinner before prompting
|
|
42
|
+
// Stop any active spinner before prompting — clear line to avoid garbled output
|
|
25
43
|
if (cb.pauseSpinner) {
|
|
26
44
|
cb.pauseSpinner();
|
|
27
45
|
cb.pauseSpinner = null;
|
|
28
46
|
}
|
|
47
|
+
process.stderr.write('\x1b[K'); // ensure current line is clear
|
|
29
48
|
const summary = formatToolSummary(toolName, args);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
49
|
+
process.stderr.write(` ${C.yellow}?${C.reset} ${C.dim}${summary}${C.reset}\n`);
|
|
50
|
+
const answer = await promptQuestion(SIMPLE_OPTS, sharedRl);
|
|
51
|
+
if (answer.trim().toLowerCase() === 'n') {
|
|
52
|
+
process.stderr.write(` ${C.red}−${C.reset} ${C.dim}Denied${C.reset}\n`);
|
|
53
|
+
return 'deny';
|
|
54
|
+
}
|
|
55
|
+
return 'allow';
|
|
56
|
+
};
|
|
57
|
+
cb.pauseSpinner = null;
|
|
58
|
+
return cb;
|
|
59
|
+
}
|
|
60
|
+
export function createPermissionCallback(pm, sharedRl) {
|
|
61
|
+
const cb = async (toolName, args, permissionKey) => {
|
|
62
|
+
// Check permission rules first
|
|
63
|
+
const decision = pm.check(toolName, args, permissionKey);
|
|
64
|
+
if (decision === 'allow')
|
|
65
|
+
return 'allow';
|
|
66
|
+
if (decision === 'deny')
|
|
67
|
+
return 'deny';
|
|
68
|
+
// Stop any active spinner before prompting — clear line to avoid garbled output
|
|
69
|
+
if (cb.pauseSpinner) {
|
|
70
|
+
cb.pauseSpinner();
|
|
71
|
+
cb.pauseSpinner = null;
|
|
72
|
+
}
|
|
73
|
+
process.stderr.write('\x1b[K'); // ensure current line is clear
|
|
74
|
+
const summary = formatToolSummary(toolName, args);
|
|
75
|
+
const pattern = inferPattern(toolName, args, permissionKey);
|
|
76
|
+
process.stderr.write(` ${C.yellow}?${C.reset} ${C.dim}${summary}${C.reset}\n`);
|
|
77
|
+
const answer = await promptQuestion(FULL_OPTS, sharedRl);
|
|
78
|
+
// Check for uppercase D before lowercasing
|
|
79
|
+
const raw = answer.trim();
|
|
80
|
+
if (raw === 'D') {
|
|
81
|
+
pm.addRule({ pattern, effect: 'deny' }, 'project');
|
|
82
|
+
pm.save('project');
|
|
83
|
+
process.stderr.write(` ${C.red}−${C.reset} ${C.dim}Saved deny to .ag/permissions.json: ${pattern}${C.reset}\n`);
|
|
84
|
+
return 'deny';
|
|
85
|
+
}
|
|
86
|
+
const choice = raw.toLowerCase();
|
|
87
|
+
switch (choice) {
|
|
88
|
+
case 'a':
|
|
89
|
+
pm.addRule({ pattern, effect: 'allow' }, 'session');
|
|
90
|
+
process.stderr.write(` ${C.green}+${C.reset} ${C.dim}Session rule: ${pattern}${C.reset}\n`);
|
|
91
|
+
return 'allow';
|
|
92
|
+
case 'p':
|
|
93
|
+
pm.addRule({ pattern, effect: 'allow' }, 'project');
|
|
94
|
+
pm.save('project');
|
|
95
|
+
process.stderr.write(` ${C.green}+${C.reset} ${C.dim}Saved to .ag/permissions.json: ${pattern}${C.reset}\n`);
|
|
96
|
+
return 'allow';
|
|
97
|
+
case 'n':
|
|
98
|
+
process.stderr.write(` ${C.red}−${C.reset} ${C.dim}Denied: ${pattern}${C.reset}\n`);
|
|
99
|
+
return 'deny';
|
|
100
|
+
case 'd':
|
|
101
|
+
pm.addRule({ pattern, effect: 'deny' }, 'session');
|
|
102
|
+
process.stderr.write(` ${C.red}−${C.reset} ${C.dim}Session deny: ${pattern}${C.reset}\n`);
|
|
103
|
+
return 'deny';
|
|
104
|
+
default: // 'y' or anything else = allow once
|
|
105
|
+
return 'allow';
|
|
36
106
|
}
|
|
37
|
-
// Fallback: create a temporary readline (non-REPL usage)
|
|
38
|
-
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
39
|
-
return new Promise(resolve => {
|
|
40
|
-
rl.question(` ${C.yellow}?${C.reset} ${C.dim}${summary}${C.reset} ${C.yellow}(y/n)${C.reset} `, answer => {
|
|
41
|
-
rl.close();
|
|
42
|
-
resolve(answer.trim().toLowerCase() === 'n' ? 'deny' : 'allow');
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
107
|
};
|
|
46
108
|
cb.pauseSpinner = null;
|
|
47
109
|
return cb;
|
|
@@ -145,12 +207,21 @@ function maskKey(key) {
|
|
|
145
207
|
export class REPL {
|
|
146
208
|
agent;
|
|
147
209
|
rl;
|
|
210
|
+
pm;
|
|
148
211
|
confirmCb;
|
|
149
|
-
constructor(agent, confirmCb) {
|
|
212
|
+
constructor(agent, pm, confirmCb) {
|
|
150
213
|
this.agent = agent;
|
|
214
|
+
this.pm = pm ?? null;
|
|
151
215
|
this.rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
152
216
|
// Rebind the confirm callback to use the shared readline
|
|
153
|
-
if (confirmCb) {
|
|
217
|
+
if (confirmCb && pm) {
|
|
218
|
+
const shared = createPermissionCallback(pm, this.rl);
|
|
219
|
+
shared.pauseSpinner = confirmCb.pauseSpinner;
|
|
220
|
+
this.confirmCb = shared;
|
|
221
|
+
this.agent.setConfirmToolCall(shared);
|
|
222
|
+
}
|
|
223
|
+
else if (confirmCb) {
|
|
224
|
+
// Fallback: no permission manager, use basic confirm
|
|
154
225
|
const shared = createConfirmCallback(this.rl);
|
|
155
226
|
shared.pauseSpinner = confirmCb.pauseSpinner;
|
|
156
227
|
this.confirmCb = shared;
|
|
@@ -163,16 +234,28 @@ export class REPL {
|
|
|
163
234
|
async start() {
|
|
164
235
|
const stats = this.agent.getStats();
|
|
165
236
|
const skills = this.agent.getSkills();
|
|
237
|
+
console.error('');
|
|
166
238
|
console.error(`${C.bold}ag v${VERSION}${C.reset} ${C.dim}(${this.agent.getModel()} via OpenRouter)${C.reset}`);
|
|
239
|
+
const customTools = this.agent.getTools().filter(t => !t.isBuiltin);
|
|
167
240
|
const loaded = [
|
|
168
241
|
stats.globalMemory && 'global',
|
|
169
242
|
stats.projectMemory && 'project',
|
|
170
243
|
stats.planCount > 0 && `${stats.planCount} plan(s)`,
|
|
171
244
|
skills.length > 0 && `${skills.length} skill(s)`,
|
|
245
|
+
customTools.length > 0 && `${customTools.length} tool(s)`,
|
|
172
246
|
].filter(Boolean);
|
|
173
247
|
if (loaded.length > 0) {
|
|
174
248
|
console.error(`${C.dim}Loaded: ${loaded.join(', ')}${C.reset}`);
|
|
175
249
|
}
|
|
250
|
+
for (const t of customTools) {
|
|
251
|
+
const desc = t.description.slice(0, 60) + (t.description.length > 60 ? '...' : '');
|
|
252
|
+
console.error(` ${C.green}+${C.reset} ${C.cyan}${t.name}${C.reset} ${C.dim}${desc}${C.reset}`);
|
|
253
|
+
}
|
|
254
|
+
for (const f of this.agent.getToolFailures()) {
|
|
255
|
+
const label = f.name ?? f.file;
|
|
256
|
+
const reason = f.reason.split('\n')[0].slice(0, 60) + (f.reason.length > 60 ? '...' : '');
|
|
257
|
+
console.error(` ${C.red}−${C.reset} ${C.cyan}${label}${C.reset} ${C.red}${reason}${C.reset}`);
|
|
258
|
+
}
|
|
176
259
|
const activePlan = this.agent.getActivePlanName();
|
|
177
260
|
if (activePlan) {
|
|
178
261
|
const label = activePlan.replace(/^\d{4}-\d{2}-\d{2}T[\d-]+-?/, '').replace(/-/g, ' ').trim() || activePlan;
|
|
@@ -253,8 +336,7 @@ export class REPL {
|
|
|
253
336
|
const parts = lineBuf.split('\n');
|
|
254
337
|
lineBuf = final ? '' : (parts.pop() || '');
|
|
255
338
|
for (let i = 0; i < parts.length; i++) {
|
|
256
|
-
|
|
257
|
-
process.stderr.write(rendered + '\n');
|
|
339
|
+
process.stderr.write(renderMarkdown(parts[i]) + '\n');
|
|
258
340
|
}
|
|
259
341
|
if (final && lineBuf) {
|
|
260
342
|
process.stderr.write(renderMarkdown(lineBuf));
|
|
@@ -403,6 +485,11 @@ export class REPL {
|
|
|
403
485
|
console.error(` ${C.cyan}/skill search [query]${C.reset} Search skills.sh`);
|
|
404
486
|
console.error(` ${C.cyan}/skill add <source>${C.reset} Install skill from registry`);
|
|
405
487
|
console.error(` ${C.cyan}/skill remove <name>${C.reset} Uninstall a skill`);
|
|
488
|
+
console.error(` ${C.cyan}/permissions${C.reset} Show permission rules`);
|
|
489
|
+
console.error(` ${C.cyan}/permissions allow <p>${C.reset} Add allow rule (session)`);
|
|
490
|
+
console.error(` ${C.cyan}/permissions deny <p>${C.reset} Add deny rule (session)`);
|
|
491
|
+
console.error(` ${C.cyan}/permissions save${C.reset} Save session rules to project`);
|
|
492
|
+
console.error(` ${C.cyan}/permissions clear${C.reset} Clear session rules`);
|
|
406
493
|
console.error(` ${C.cyan}/exit${C.reset} Exit`);
|
|
407
494
|
console.error('');
|
|
408
495
|
break;
|
|
@@ -599,12 +686,16 @@ export class REPL {
|
|
|
599
686
|
// Apply immediately: toggle confirmation prompts in this session
|
|
600
687
|
if (parsed) {
|
|
601
688
|
this.agent.setConfirmToolCall(null);
|
|
689
|
+
this.confirmCb = null;
|
|
602
690
|
console.error(`${C.green}Auto-approve enabled — tool calls will no longer prompt.${C.reset}`);
|
|
603
691
|
}
|
|
604
692
|
else {
|
|
605
|
-
const freshCb =
|
|
693
|
+
const freshCb = this.pm
|
|
694
|
+
? createPermissionCallback(this.pm, this.rl)
|
|
695
|
+
: createConfirmCallback(this.rl);
|
|
606
696
|
if (this.confirmCb)
|
|
607
697
|
freshCb.pauseSpinner = this.confirmCb.pauseSpinner;
|
|
698
|
+
this.confirmCb = freshCb;
|
|
608
699
|
this.agent.setConfirmToolCall(freshCb);
|
|
609
700
|
console.error(`${C.yellow}Auto-approve disabled — tool calls will prompt again.${C.reset}`);
|
|
610
701
|
}
|
|
@@ -628,9 +719,12 @@ export class REPL {
|
|
|
628
719
|
this.agent.setModel('anthropic/claude-sonnet-4.6');
|
|
629
720
|
}
|
|
630
721
|
if (key === 'autoApprove') {
|
|
631
|
-
const freshCb =
|
|
722
|
+
const freshCb = this.pm
|
|
723
|
+
? createPermissionCallback(this.pm, this.rl)
|
|
724
|
+
: createConfirmCallback(this.rl);
|
|
632
725
|
if (this.confirmCb)
|
|
633
726
|
freshCb.pauseSpinner = this.confirmCb.pauseSpinner;
|
|
727
|
+
this.confirmCb = freshCb;
|
|
634
728
|
this.agent.setConfirmToolCall(freshCb);
|
|
635
729
|
}
|
|
636
730
|
console.error(`${C.yellow}Config: ${key} removed${C.reset}\n`);
|
|
@@ -670,7 +764,8 @@ export class REPL {
|
|
|
670
764
|
const tools = this.agent.getTools();
|
|
671
765
|
console.error(`${C.bold}Tools (${tools.length}):${C.reset}`);
|
|
672
766
|
for (const t of tools) {
|
|
673
|
-
|
|
767
|
+
const prefix = t.isBuiltin ? ' ' : `${C.green}+${C.reset}`;
|
|
768
|
+
console.error(` ${prefix} ${C.cyan}${t.name}${C.reset} ${C.dim}${t.description.slice(0, 60)}${t.description.length > 60 ? '...' : ''}${C.reset}`);
|
|
674
769
|
}
|
|
675
770
|
console.error('');
|
|
676
771
|
break;
|
|
@@ -743,6 +838,69 @@ export class REPL {
|
|
|
743
838
|
}
|
|
744
839
|
break;
|
|
745
840
|
}
|
|
841
|
+
// ── /permissions ────────────────────────────────────────────────────
|
|
842
|
+
case 'permissions':
|
|
843
|
+
case 'perms': {
|
|
844
|
+
if (!this.pm) {
|
|
845
|
+
console.error(`${C.dim}Permissions not available (auto-approve mode).${C.reset}\n`);
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
848
|
+
const subCmd = args[0]?.toLowerCase();
|
|
849
|
+
if (subCmd === 'allow' && args[1]) {
|
|
850
|
+
const pattern = args.slice(1).join(' ');
|
|
851
|
+
this.pm.addRule({ pattern, effect: 'allow' }, 'session');
|
|
852
|
+
console.error(`${C.green}+ Session allow: ${pattern}${C.reset}\n`);
|
|
853
|
+
}
|
|
854
|
+
else if (subCmd === 'deny' && args[1]) {
|
|
855
|
+
const pattern = args.slice(1).join(' ');
|
|
856
|
+
this.pm.addRule({ pattern, effect: 'deny' }, 'session');
|
|
857
|
+
console.error(`${C.green}+ Session deny: ${pattern}${C.reset}\n`);
|
|
858
|
+
}
|
|
859
|
+
else if (subCmd === 'save') {
|
|
860
|
+
const scope = args[1]?.toLowerCase() === 'global' ? 'global' : 'project';
|
|
861
|
+
this.pm.save(scope);
|
|
862
|
+
console.error(`${C.yellow}Saved to ${scope} permissions.${C.reset}\n`);
|
|
863
|
+
}
|
|
864
|
+
else if (subCmd === 'clear') {
|
|
865
|
+
const scope = args[1]?.toLowerCase();
|
|
866
|
+
if (scope === 'project' || scope === 'global') {
|
|
867
|
+
this.pm.clear(scope);
|
|
868
|
+
this.pm.save(scope);
|
|
869
|
+
console.error(`${C.yellow}Cleared ${scope} permissions.${C.reset}\n`);
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
this.pm.clear('session');
|
|
873
|
+
console.error(`${C.yellow}Cleared session permissions.${C.reset}\n`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
else if (subCmd === 'remove' && args[1]) {
|
|
877
|
+
const pattern = args.slice(1).join(' ');
|
|
878
|
+
const removed = this.pm.removeRule(pattern, 'session')
|
|
879
|
+
|| this.pm.removeRule(pattern, 'project')
|
|
880
|
+
|| this.pm.removeRule(pattern, 'global');
|
|
881
|
+
if (removed) {
|
|
882
|
+
console.error(`${C.yellow}Removed: ${pattern}${C.reset}\n`);
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
console.error(`${C.dim}No matching rule found.${C.reset}\n`);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
else {
|
|
889
|
+
// List all rules
|
|
890
|
+
const rules = this.pm.getRules();
|
|
891
|
+
if (rules.length === 0) {
|
|
892
|
+
console.error(`${C.dim}No permission rules. Approve with (a)lways or (p)roject to add rules.${C.reset}\n`);
|
|
893
|
+
break;
|
|
894
|
+
}
|
|
895
|
+
console.error(`${C.bold}Permission Rules:${C.reset}`);
|
|
896
|
+
for (const r of rules) {
|
|
897
|
+
const icon = r.effect === 'allow' ? `${C.green}✓` : `${C.red}✗`;
|
|
898
|
+
console.error(` ${icon} ${C.dim}[${r.scope}]${C.reset} ${r.effect} ${C.cyan}${r.pattern}${C.reset}`);
|
|
899
|
+
}
|
|
900
|
+
console.error('');
|
|
901
|
+
}
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
746
904
|
// ── /exit ─────────────────────────────────────────────────────────
|
|
747
905
|
case 'exit':
|
|
748
906
|
case 'quit':
|