@aragon-agent/cli 0.4.3

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 (156) hide show
  1. package/CHANGELOG.md +292 -0
  2. package/README.md +558 -0
  3. package/dist/agent/coalesce.d.ts +14 -0
  4. package/dist/agent/coalesce.js +41 -0
  5. package/dist/agent/controller.d.ts +132 -0
  6. package/dist/agent/controller.js +332 -0
  7. package/dist/agent/headless.d.ts +22 -0
  8. package/dist/agent/headless.js +74 -0
  9. package/dist/agent/reducer.d.ts +181 -0
  10. package/dist/agent/reducer.js +420 -0
  11. package/dist/agent/system-prompt.d.ts +30 -0
  12. package/dist/agent/system-prompt.js +58 -0
  13. package/dist/agent/usage.d.ts +13 -0
  14. package/dist/agent/usage.js +41 -0
  15. package/dist/cli.d.ts +11 -0
  16. package/dist/cli.js +482 -0
  17. package/dist/commands/builtins.d.ts +16 -0
  18. package/dist/commands/builtins.js +223 -0
  19. package/dist/commands/registry.d.ts +48 -0
  20. package/dist/commands/registry.js +45 -0
  21. package/dist/commands/skills.d.ts +22 -0
  22. package/dist/commands/skills.js +625 -0
  23. package/dist/config/app-paths.d.ts +11 -0
  24. package/dist/config/app-paths.js +16 -0
  25. package/dist/config/env.d.ts +28 -0
  26. package/dist/config/env.js +92 -0
  27. package/dist/config/load.d.ts +48 -0
  28. package/dist/config/load.js +158 -0
  29. package/dist/config/migrate-legacy-state.d.ts +78 -0
  30. package/dist/config/migrate-legacy-state.js +181 -0
  31. package/dist/config/schema.d.ts +249 -0
  32. package/dist/config/schema.js +247 -0
  33. package/dist/config/store.d.ts +44 -0
  34. package/dist/config/store.js +126 -0
  35. package/dist/input/keymap.d.ts +20 -0
  36. package/dist/input/keymap.js +85 -0
  37. package/dist/session/persist.d.ts +24 -0
  38. package/dist/session/persist.js +51 -0
  39. package/dist/skills/archive.d.ts +41 -0
  40. package/dist/skills/archive.js +152 -0
  41. package/dist/skills/cli-commands.d.ts +52 -0
  42. package/dist/skills/cli-commands.js +502 -0
  43. package/dist/skills/fetch-source.d.ts +104 -0
  44. package/dist/skills/fetch-source.js +448 -0
  45. package/dist/skills/installer.d.ts +117 -0
  46. package/dist/skills/installer.js +414 -0
  47. package/dist/skills/lock.d.ts +75 -0
  48. package/dist/skills/lock.js +223 -0
  49. package/dist/skills/manifest.d.ts +47 -0
  50. package/dist/skills/manifest.js +95 -0
  51. package/dist/skills/node-host.d.ts +23 -0
  52. package/dist/skills/node-host.js +59 -0
  53. package/dist/skills/paths.d.ts +61 -0
  54. package/dist/skills/paths.js +107 -0
  55. package/dist/skills/service.d.ts +256 -0
  56. package/dist/skills/service.js +730 -0
  57. package/dist/skills/tools.d.ts +35 -0
  58. package/dist/skills/tools.js +164 -0
  59. package/dist/skills/updater.d.ts +101 -0
  60. package/dist/skills/updater.js +330 -0
  61. package/dist/skills/usage.d.ts +72 -0
  62. package/dist/skills/usage.js +216 -0
  63. package/dist/tools/bash-tool.d.ts +12 -0
  64. package/dist/tools/bash-tool.js +131 -0
  65. package/dist/tools/diff.d.ts +15 -0
  66. package/dist/tools/diff.js +50 -0
  67. package/dist/tools/fs-tools.d.ts +16 -0
  68. package/dist/tools/fs-tools.js +215 -0
  69. package/dist/tools/index.d.ts +93 -0
  70. package/dist/tools/index.js +157 -0
  71. package/dist/tools/search-tools.d.ts +11 -0
  72. package/dist/tools/search-tools.js +225 -0
  73. package/dist/ui/App.d.ts +24 -0
  74. package/dist/ui/App.js +522 -0
  75. package/dist/ui/AutocompletePopup.d.ts +21 -0
  76. package/dist/ui/AutocompletePopup.js +19 -0
  77. package/dist/ui/Composer.d.ts +40 -0
  78. package/dist/ui/Composer.js +61 -0
  79. package/dist/ui/Header.d.ts +30 -0
  80. package/dist/ui/Header.js +25 -0
  81. package/dist/ui/Logo.d.ts +50 -0
  82. package/dist/ui/Logo.js +85 -0
  83. package/dist/ui/Markdown.d.ts +23 -0
  84. package/dist/ui/Markdown.js +122 -0
  85. package/dist/ui/PromptInput.d.ts +81 -0
  86. package/dist/ui/PromptInput.js +383 -0
  87. package/dist/ui/SessionOpener.d.ts +30 -0
  88. package/dist/ui/SessionOpener.js +12 -0
  89. package/dist/ui/StatusBar.d.ts +39 -0
  90. package/dist/ui/StatusBar.js +43 -0
  91. package/dist/ui/ToastStack.d.ts +24 -0
  92. package/dist/ui/ToastStack.js +38 -0
  93. package/dist/ui/Transcript.d.ts +62 -0
  94. package/dist/ui/Transcript.js +128 -0
  95. package/dist/ui/capabilities.d.ts +25 -0
  96. package/dist/ui/capabilities.js +59 -0
  97. package/dist/ui/console-bridge.d.ts +19 -0
  98. package/dist/ui/console-bridge.js +52 -0
  99. package/dist/ui/density.d.ts +26 -0
  100. package/dist/ui/density.js +34 -0
  101. package/dist/ui/entries/AssistantEntry.d.ts +23 -0
  102. package/dist/ui/entries/AssistantEntry.js +13 -0
  103. package/dist/ui/entries/EntryFrame.d.ts +23 -0
  104. package/dist/ui/entries/EntryFrame.js +6 -0
  105. package/dist/ui/entries/ToolCard.d.ts +38 -0
  106. package/dist/ui/entries/ToolCard.js +72 -0
  107. package/dist/ui/entries/ToolPreview.d.ts +57 -0
  108. package/dist/ui/entries/ToolPreview.js +144 -0
  109. package/dist/ui/entries/UserEntry.d.ts +14 -0
  110. package/dist/ui/entries/UserEntry.js +5 -0
  111. package/dist/ui/exit-snapshot.d.ts +27 -0
  112. package/dist/ui/exit-snapshot.js +26 -0
  113. package/dist/ui/gauge.d.ts +24 -0
  114. package/dist/ui/gauge.js +36 -0
  115. package/dist/ui/glyphs.d.ts +87 -0
  116. package/dist/ui/glyphs.js +127 -0
  117. package/dist/ui/gradient.d.ts +11 -0
  118. package/dist/ui/gradient.js +50 -0
  119. package/dist/ui/layout/AppShell.d.ts +24 -0
  120. package/dist/ui/layout/AppShell.js +9 -0
  121. package/dist/ui/layout/Gutter.d.ts +67 -0
  122. package/dist/ui/layout/Gutter.js +42 -0
  123. package/dist/ui/layout/OverlayFrame.d.ts +62 -0
  124. package/dist/ui/layout/OverlayFrame.js +92 -0
  125. package/dist/ui/layout/ScrollViewport.d.ts +33 -0
  126. package/dist/ui/layout/ScrollViewport.js +71 -0
  127. package/dist/ui/layout/budget.d.ts +42 -0
  128. package/dist/ui/layout/budget.js +47 -0
  129. package/dist/ui/layout/frame.d.ts +58 -0
  130. package/dist/ui/layout/frame.js +75 -0
  131. package/dist/ui/layout/overlay-window.d.ts +43 -0
  132. package/dist/ui/layout/overlay-window.js +51 -0
  133. package/dist/ui/layout/scroll.d.ts +28 -0
  134. package/dist/ui/layout/scroll.js +76 -0
  135. package/dist/ui/layout/useTerminalSize.d.ts +13 -0
  136. package/dist/ui/layout/useTerminalSize.js +50 -0
  137. package/dist/ui/markdown-blocks.d.ts +62 -0
  138. package/dist/ui/markdown-blocks.js +120 -0
  139. package/dist/ui/overlays/ConfirmDialog.d.ts +30 -0
  140. package/dist/ui/overlays/ConfirmDialog.js +25 -0
  141. package/dist/ui/overlays/HelpOverlay.d.ts +17 -0
  142. package/dist/ui/overlays/HelpOverlay.js +60 -0
  143. package/dist/ui/overlays/ModelPicker.d.ts +29 -0
  144. package/dist/ui/overlays/ModelPicker.js +29 -0
  145. package/dist/ui/overlays/SettingsScreen.d.ts +37 -0
  146. package/dist/ui/overlays/SettingsScreen.js +106 -0
  147. package/dist/ui/palettes.d.ts +79 -0
  148. package/dist/ui/palettes.js +115 -0
  149. package/dist/ui/screen.d.ts +27 -0
  150. package/dist/ui/screen.js +57 -0
  151. package/dist/ui/theme.d.ts +78 -0
  152. package/dist/ui/theme.js +127 -0
  153. package/dist/ui/transcript-text.d.ts +27 -0
  154. package/dist/ui/transcript-text.js +110 -0
  155. package/package.json +73 -0
  156. package/skills/skill-creator/SKILL.md +96 -0
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Filesystem tools: read_file / write_file / edit_file / list_dir (spec §3.6).
3
+ *
4
+ * All relative paths resolve against the session cwd (via the injected
5
+ * `getCwd`). Every tool returns `errorResult` on bad input — it never throws.
6
+ * Timeout + 100 KB truncation are owned by the core ToolExecutor (R4); these
7
+ * tools do not re-implement them.
8
+ */
9
+ import { promises as fs } from 'node:fs';
10
+ import { isAbsolute, join, resolve, dirname } from 'node:path';
11
+ import { defineTool, errorResult, textResult, } from '@aragon-agent/core';
12
+ import { renderUnifiedDiff } from './diff.js';
13
+ function resolvePath(cwd, p) {
14
+ return isAbsolute(p) ? p : resolve(cwd, p);
15
+ }
16
+ /** Heuristic binary detection: a NUL byte in the first chunk. */
17
+ function looksBinary(buf) {
18
+ const scan = buf.subarray(0, Math.min(buf.length, 8000));
19
+ return scan.includes(0);
20
+ }
21
+ // ---------------------------------------------------------------------------
22
+ // read_file
23
+ // ---------------------------------------------------------------------------
24
+ export function makeReadFile(deps) {
25
+ return defineTool({
26
+ name: 'read_file',
27
+ label: 'Read File',
28
+ description: 'Read a UTF-8 text file and return its contents as numbered lines. ' +
29
+ 'Optionally read a window with `offset` (1-based start line) and `limit` (line count).',
30
+ parameters: {
31
+ type: 'object',
32
+ properties: {
33
+ path: { type: 'string', description: 'File path (relative to the working directory or absolute).' },
34
+ offset: { type: 'number', description: '1-based line to start from.' },
35
+ limit: { type: 'number', description: 'Maximum number of lines to return.' },
36
+ },
37
+ required: ['path'],
38
+ },
39
+ async execute(_id, rawParams) {
40
+ const params = rawParams;
41
+ const target = resolvePath(deps.getCwd(), params.path);
42
+ try {
43
+ const buf = await fs.readFile(target);
44
+ if (looksBinary(buf)) {
45
+ return errorResult(`File appears to be binary: ${params.path}`);
46
+ }
47
+ const content = buf.toString('utf-8');
48
+ const allLines = content.split('\n');
49
+ const offset = params.offset && params.offset > 0 ? params.offset : 1;
50
+ const start = offset - 1;
51
+ const end = params.limit && params.limit > 0 ? start + params.limit : allLines.length;
52
+ const slice = allLines.slice(start, end);
53
+ const numbered = slice
54
+ .map((line, i) => `${String(start + i + 1).padStart(5)} ${line}`)
55
+ .join('\n');
56
+ return textResult(numbered.length > 0 ? numbered : '(empty file)');
57
+ }
58
+ catch (err) {
59
+ return errorResult(readError(err, params.path));
60
+ }
61
+ },
62
+ });
63
+ }
64
+ // ---------------------------------------------------------------------------
65
+ // write_file
66
+ // ---------------------------------------------------------------------------
67
+ export function makeWriteFile(deps) {
68
+ return defineTool({
69
+ name: 'write_file',
70
+ label: 'Write File',
71
+ description: 'Create or overwrite a file with the given content, creating parent ' +
72
+ 'directories as needed. Returns the number of bytes written.',
73
+ parameters: {
74
+ type: 'object',
75
+ properties: {
76
+ path: { type: 'string', description: 'File path (relative or absolute).' },
77
+ content: { type: 'string', description: 'Full file content to write.' },
78
+ },
79
+ required: ['path', 'content'],
80
+ },
81
+ async execute(_id, rawParams) {
82
+ const params = rawParams;
83
+ const target = resolvePath(deps.getCwd(), params.path);
84
+ try {
85
+ await fs.mkdir(dirname(target), { recursive: true });
86
+ await fs.writeFile(target, params.content, 'utf-8');
87
+ const bytes = Buffer.byteLength(params.content, 'utf-8');
88
+ return textResult(`Wrote ${bytes} bytes to ${params.path}`);
89
+ }
90
+ catch (err) {
91
+ return errorResult(readError(err, params.path));
92
+ }
93
+ },
94
+ });
95
+ }
96
+ // ---------------------------------------------------------------------------
97
+ // edit_file
98
+ // ---------------------------------------------------------------------------
99
+ export function makeEditFile(deps) {
100
+ return defineTool({
101
+ name: 'edit_file',
102
+ label: 'Edit File',
103
+ description: 'Replace an exact string in a file with a new string. Unless ' +
104
+ '`replace_all` is true, `old_string` must occur exactly once. Returns a ' +
105
+ 'unified-diff preview of the change.',
106
+ parameters: {
107
+ type: 'object',
108
+ properties: {
109
+ path: { type: 'string', description: 'File path (relative or absolute).' },
110
+ old_string: { type: 'string', description: 'Exact text to replace.' },
111
+ new_string: { type: 'string', description: 'Replacement text.' },
112
+ replace_all: {
113
+ type: 'boolean',
114
+ description: 'Replace every occurrence instead of requiring uniqueness.',
115
+ },
116
+ },
117
+ required: ['path', 'old_string', 'new_string'],
118
+ },
119
+ async execute(_id, rawParams) {
120
+ const params = rawParams;
121
+ const target = resolvePath(deps.getCwd(), params.path);
122
+ try {
123
+ const original = await fs.readFile(target, 'utf-8');
124
+ if (params.old_string === params.new_string) {
125
+ return errorResult('old_string and new_string are identical - nothing to change.');
126
+ }
127
+ const occurrences = countOccurrences(original, params.old_string);
128
+ if (occurrences === 0) {
129
+ return errorResult(`old_string not found in ${params.path}.`);
130
+ }
131
+ if (occurrences > 1 && !params.replace_all) {
132
+ return errorResult(`old_string occurs ${occurrences} times in ${params.path}; pass replace_all:true or make it unique.`);
133
+ }
134
+ // Use a function replacer so `$`-patterns (`$&`, `$'`, `` $` ``, `$$`,
135
+ // `$n`) in new_string are inserted literally rather than interpreted by
136
+ // String.prototype.replace. The replace_all branch is already literal.
137
+ const updated = params.replace_all
138
+ ? original.split(params.old_string).join(params.new_string)
139
+ : original.replace(params.old_string, () => params.new_string);
140
+ await fs.writeFile(target, updated, 'utf-8');
141
+ const diff = renderUnifiedDiff(original, updated, { path: params.path });
142
+ return textResult(`Applied edit to ${params.path}:\n${diff}`);
143
+ }
144
+ catch (err) {
145
+ return errorResult(readError(err, params.path));
146
+ }
147
+ },
148
+ });
149
+ }
150
+ // ---------------------------------------------------------------------------
151
+ // list_dir
152
+ // ---------------------------------------------------------------------------
153
+ export function makeListDir(deps) {
154
+ return defineTool({
155
+ name: 'list_dir',
156
+ label: 'List Directory',
157
+ description: 'List entries in a directory with type (dir/file) and size in bytes.',
158
+ parameters: {
159
+ type: 'object',
160
+ properties: {
161
+ path: { type: 'string', description: 'Directory path (default: working directory).' },
162
+ },
163
+ },
164
+ async execute(_id, rawParams) {
165
+ const params = rawParams;
166
+ const dir = resolvePath(deps.getCwd(), params.path ?? '.');
167
+ try {
168
+ const entries = await fs.readdir(dir, { withFileTypes: true });
169
+ if (entries.length === 0)
170
+ return textResult('(empty directory)');
171
+ const rows = await Promise.all(entries
172
+ .sort((a, b) => a.name.localeCompare(b.name))
173
+ .map(async (e) => {
174
+ if (e.isDirectory())
175
+ return `dir ${e.name}/`;
176
+ try {
177
+ const st = await fs.stat(join(dir, e.name));
178
+ return `file ${e.name} (${st.size}b)`;
179
+ }
180
+ catch {
181
+ return `file ${e.name}`;
182
+ }
183
+ }));
184
+ return textResult(rows.join('\n'));
185
+ }
186
+ catch (err) {
187
+ return errorResult(readError(err, params.path ?? '.'));
188
+ }
189
+ },
190
+ });
191
+ }
192
+ // ---------------------------------------------------------------------------
193
+ // Helpers
194
+ // ---------------------------------------------------------------------------
195
+ function countOccurrences(haystack, needle) {
196
+ if (needle.length === 0)
197
+ return 0;
198
+ let count = 0;
199
+ let idx = haystack.indexOf(needle);
200
+ while (idx !== -1) {
201
+ count += 1;
202
+ idx = haystack.indexOf(needle, idx + needle.length);
203
+ }
204
+ return count;
205
+ }
206
+ function readError(err, path) {
207
+ const code = err.code;
208
+ if (code === 'ENOENT')
209
+ return `No such file or directory: ${path}`;
210
+ if (code === 'EISDIR')
211
+ return `Path is a directory: ${path}`;
212
+ if (code === 'EACCES')
213
+ return `Permission denied: ${path}`;
214
+ return err instanceof Error ? err.message : String(err);
215
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Built-in toolset assembly (spec §3.6). The core engine ships no tools, so the
3
+ * CLI supplies the entire `tools: AgentTool[]` array: filesystem, search, and
4
+ * shell — all at full permission.
5
+ *
6
+ * An optional `confirmTools` mode wraps the mutating tools (write_file /
7
+ * edit_file / bash) in a Yes/No gate. It is off by default per the max-permission
8
+ * requirement; when enabled, a `confirm` callback (wired by the TUI) is awaited
9
+ * before the tool runs.
10
+ */
11
+ import { type AgentTool, type ToolPolicyDecision, type ToolPolicyVerdict } from '@aragon-agent/core';
12
+ export interface ConfirmRequest {
13
+ tool: string;
14
+ summary: string;
15
+ }
16
+ export interface BuiltinToolsOptions {
17
+ getCwd: () => string;
18
+ /** When true (and `confirm` is provided), mutating tools are gated. */
19
+ confirmTools?: boolean;
20
+ /** Awaited before a mutating tool runs; resolve `false` to cancel. */
21
+ confirm?: (req: ConfirmRequest) => Promise<boolean>;
22
+ /**
23
+ * `skill` / `skill_install` / `skill_create`, appended verbatim.
24
+ *
25
+ * Deliberately NOT added to `MUTATING_TOOLS`: the installer runs its own
26
+ * `ApprovalGate` (§8.3.1), which is strictly stronger than the `--confirm`
27
+ * wrapper. Adding them here too would ask the user twice for one action and,
28
+ * worse, the outer wrapper would auto-approve where the inner gate refuses.
29
+ *
30
+ * An empty array (the `--no-skills` path) leaves the tool list byte-identical
31
+ * to the pre-Skills seven — invariant I-S1 / AC-10.
32
+ */
33
+ skillTools?: AgentTool[];
34
+ /**
35
+ * Read the ceiling AT CALL TIME. Never cache the returned decision: the frame
36
+ * changes mid-turn (a `skill` call adds to it, a rescan can empty it), and a
37
+ * cached decision would ignore the skill the model just loaded.
38
+ *
39
+ * Omitting this leaves every tool unwrapped — see the early return below.
40
+ */
41
+ toolPolicy?: () => ToolPolicyDecision;
42
+ /**
43
+ * Report a refusal or a warn-mode pass so the user hears about it (D-G10).
44
+ *
45
+ * The field is `sourceNames`, NOT `sources`. `ToolPolicyDecision.sources` is a
46
+ * list of structured `ToolPolicySource` objects; two same-named fields of
47
+ * different shape in one flow is the easiest possible thing to mix up, so the
48
+ * event carries the plain names under a different name.
49
+ */
50
+ onToolPolicyEvent?: (e: {
51
+ tool: string;
52
+ verdict: ToolPolicyVerdict;
53
+ sourceNames: string[];
54
+ }) => void;
55
+ }
56
+ /**
57
+ * Tools the ceiling always permits (D-G6). The ONLY place that knows which of
58
+ * this host's tools are read-only — core must not (D-G7).
59
+ *
60
+ * Two groups, two different arguments. The read-only tools cannot change the
61
+ * world, so an author who simply forgot to declare `read_file` gets an annoyance
62
+ * rather than an incident. `skill` / `skill_find` have to stay reachable or the
63
+ * Level 1 escape hatch would be visible to the model and uncallable inside a
64
+ * frame — reintroducing the exact dead end iteration 2 spent a round removing.
65
+ *
66
+ * What is left under actual control is therefore `write_file` / `edit_file` /
67
+ * `bash` / `skill_install` / `skill_create`: the five that change something. That
68
+ * is the whole of what this mechanism is worth, and saying so plainly beats
69
+ * implying it covers more.
70
+ */
71
+ export declare const SKILL_TOOL_FLOOR: readonly ["read_file", "list_dir", "glob", "grep", "skill", "skill_find"];
72
+ /**
73
+ * Every tool name this host CAN register (7 built-ins + 4 skill tools).
74
+ *
75
+ * Exists because `aragon skills doctor` runs in a one-shot process with no
76
+ * `AgentController` and therefore no live tool array, yet it is the primary
77
+ * mitigation for "your declaration will never be enforceable" (RG1 / RG3 / RG5).
78
+ * Without this it would have to report every skill as NOT ENFORCEABLE or check
79
+ * nothing at all.
80
+ *
81
+ * Deliberately the SUPERSET rather than "what is registered right now": doctor
82
+ * answers "can this declaration ever take effect?", not "is it in effect at this
83
+ * instant". So `--no-skills` must still resolve `skill_create`. The runtime
84
+ * ceiling keeps using the live `toolNames()` (§5.4c); these two lists are not
85
+ * interchangeable, and a test asserts they agree.
86
+ *
87
+ * `/skills info` and the `/skills` ceiling summary use it for the same reason,
88
+ * even though a TUI session does have a live tool array: both describe what a
89
+ * declaration MEANS, and that reading must not change with how the CLI was
90
+ * started.
91
+ */
92
+ export declare const HOST_TOOL_NAMES: readonly ["read_file", "write_file", "edit_file", "list_dir", "glob", "grep", "bash", "skill", "skill_find", "skill_install", "skill_create"];
93
+ export declare function createBuiltinTools(options: BuiltinToolsOptions): AgentTool[];
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Built-in toolset assembly (spec §3.6). The core engine ships no tools, so the
3
+ * CLI supplies the entire `tools: AgentTool[]` array: filesystem, search, and
4
+ * shell — all at full permission.
5
+ *
6
+ * An optional `confirmTools` mode wraps the mutating tools (write_file /
7
+ * edit_file / bash) in a Yes/No gate. It is off by default per the max-permission
8
+ * requirement; when enabled, a `confirm` callback (wired by the TUI) is awaited
9
+ * before the tool runs.
10
+ */
11
+ import { errorResult, evaluateToolCall, } from '@aragon-agent/core';
12
+ import { makeReadFile, makeWriteFile, makeEditFile, makeListDir } from './fs-tools.js';
13
+ import { makeGlob, makeGrep } from './search-tools.js';
14
+ import { makeBash } from './bash-tool.js';
15
+ const MUTATING_TOOLS = new Set(['write_file', 'edit_file', 'bash']);
16
+ /**
17
+ * Tools the ceiling always permits (D-G6). The ONLY place that knows which of
18
+ * this host's tools are read-only — core must not (D-G7).
19
+ *
20
+ * Two groups, two different arguments. The read-only tools cannot change the
21
+ * world, so an author who simply forgot to declare `read_file` gets an annoyance
22
+ * rather than an incident. `skill` / `skill_find` have to stay reachable or the
23
+ * Level 1 escape hatch would be visible to the model and uncallable inside a
24
+ * frame — reintroducing the exact dead end iteration 2 spent a round removing.
25
+ *
26
+ * What is left under actual control is therefore `write_file` / `edit_file` /
27
+ * `bash` / `skill_install` / `skill_create`: the five that change something. That
28
+ * is the whole of what this mechanism is worth, and saying so plainly beats
29
+ * implying it covers more.
30
+ */
31
+ export const SKILL_TOOL_FLOOR = [
32
+ 'read_file',
33
+ 'list_dir',
34
+ 'glob',
35
+ 'grep',
36
+ 'skill',
37
+ 'skill_find',
38
+ ];
39
+ /**
40
+ * Every tool name this host CAN register (7 built-ins + 4 skill tools).
41
+ *
42
+ * Exists because `aragon skills doctor` runs in a one-shot process with no
43
+ * `AgentController` and therefore no live tool array, yet it is the primary
44
+ * mitigation for "your declaration will never be enforceable" (RG1 / RG3 / RG5).
45
+ * Without this it would have to report every skill as NOT ENFORCEABLE or check
46
+ * nothing at all.
47
+ *
48
+ * Deliberately the SUPERSET rather than "what is registered right now": doctor
49
+ * answers "can this declaration ever take effect?", not "is it in effect at this
50
+ * instant". So `--no-skills` must still resolve `skill_create`. The runtime
51
+ * ceiling keeps using the live `toolNames()` (§5.4c); these two lists are not
52
+ * interchangeable, and a test asserts they agree.
53
+ *
54
+ * `/skills info` and the `/skills` ceiling summary use it for the same reason,
55
+ * even though a TUI session does have a live tool array: both describe what a
56
+ * declaration MEANS, and that reading must not change with how the CLI was
57
+ * started.
58
+ */
59
+ export const HOST_TOOL_NAMES = [
60
+ 'read_file',
61
+ 'write_file',
62
+ 'edit_file',
63
+ 'list_dir',
64
+ 'glob',
65
+ 'grep',
66
+ 'bash',
67
+ 'skill',
68
+ 'skill_find',
69
+ 'skill_install',
70
+ 'skill_create',
71
+ ];
72
+ function summarize(toolName, params) {
73
+ if (toolName === 'bash')
74
+ return `Run: ${String(params.command ?? '')}`;
75
+ if (toolName === 'write_file')
76
+ return `Write: ${String(params.path ?? '')}`;
77
+ if (toolName === 'edit_file')
78
+ return `Edit: ${String(params.path ?? '')}`;
79
+ return toolName;
80
+ }
81
+ /** Wrap a tool so its execution is gated behind a confirmation callback. */
82
+ function withConfirmation(tool, confirm) {
83
+ const original = tool.execute;
84
+ return {
85
+ ...tool,
86
+ async execute(id, params, ctx) {
87
+ const approved = await confirm({
88
+ tool: tool.name,
89
+ summary: summarize(tool.name, params),
90
+ });
91
+ if (!approved)
92
+ return errorResult('Cancelled by user.');
93
+ return original(id, params, ctx);
94
+ },
95
+ };
96
+ }
97
+ /**
98
+ * Wrap a tool in the turn-scoped ceiling (§5.4b). Same shape as
99
+ * `withConfirmation` — `{...tool, execute}` — so nothing in the engine needs to
100
+ * know this happened.
101
+ *
102
+ * NO DEDUPLICATION HERE. Announce-once bookkeeping belongs to `SkillRegistry`,
103
+ * the only object that knows when a turn starts; a `Set` closed over in this
104
+ * function would be session-scoped and would mute `warn` mode after one message.
105
+ */
106
+ function withToolPolicy(tool, getDecision, notify) {
107
+ const original = tool.execute;
108
+ return {
109
+ ...tool,
110
+ async execute(id, params, ctx) {
111
+ const decision = getDecision();
112
+ const verdict = evaluateToolCall(tool.name, decision);
113
+ if (verdict.notice) {
114
+ // Runs BEFORE the refusal is returned, which is what lets the service
115
+ // append its escalation line to `verdict.message` in place.
116
+ notify?.({ tool: tool.name, verdict, sourceNames: decision.sourceNames });
117
+ }
118
+ if (!verdict.allow)
119
+ return errorResult(verdict.message ?? `Tool "${tool.name}" is not permitted.`);
120
+ return original(id, params, ctx);
121
+ },
122
+ };
123
+ }
124
+ export function createBuiltinTools(options) {
125
+ const deps = { getCwd: options.getCwd };
126
+ let tools = [
127
+ makeReadFile(deps),
128
+ makeWriteFile(deps),
129
+ makeEditFile(deps),
130
+ makeListDir(deps),
131
+ makeGlob(deps),
132
+ makeGrep(deps),
133
+ makeBash(deps),
134
+ ];
135
+ if (options.confirmTools && options.confirm) {
136
+ const confirm = options.confirm;
137
+ tools = tools.map((t) => (MUTATING_TOOLS.has(t.name) ? withConfirmation(t, confirm) : t));
138
+ }
139
+ if (options.skillTools && options.skillTools.length > 0) {
140
+ tools = [...tools, ...options.skillTools];
141
+ }
142
+ // No decision provider (`--no-skills`, or a caller that never enabled the
143
+ // policy) means the tools go out exactly as they came in.
144
+ //
145
+ // A ceiling that always says "allow" would behave identically, so this early
146
+ // return buys nothing at run time — it buys the ability to PROVE the claim.
147
+ // AC-G17 asserts object identity on this path; with a wrapper in place, "the
148
+ // `--no-skills` path is unchanged from iteration 2" would demote from a fact
149
+ // anyone can check to an argument someone has to trust.
150
+ if (!options.toolPolicy)
151
+ return tools;
152
+ // Outermost on purpose (D-G11). Asking a human "run bash?", waiting for them
153
+ // to say yes, and only then reporting that policy refused it is the worst
154
+ // available ordering. The ceiling needs no I/O, so it goes first.
155
+ const getDecision = options.toolPolicy;
156
+ return tools.map((t) => withToolPolicy(t, getDecision, options.onToolPolicyEvent));
157
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Search tools: glob / grep (spec §3.6).
3
+ *
4
+ * `glob` uses tinyglobby and sorts matches by mtime (newest first). `grep`
5
+ * prefers ripgrep when it is on PATH and falls back to a pure-JS scan otherwise;
6
+ * both produce `path:line:text` output.
7
+ */
8
+ import { type AgentTool } from '@aragon-agent/core';
9
+ import type { ToolDeps } from './fs-tools.js';
10
+ export declare function makeGlob(deps: ToolDeps): AgentTool;
11
+ export declare function makeGrep(deps: ToolDeps): AgentTool;