@datename/fastcode-termux 0.1.6-beta → 0.1.8-beta.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/LICENSE +4 -0
- package/README.md +50 -17
- package/dist/agent/approval-json.d.ts +43 -0
- package/dist/agent/approval-json.js +69 -0
- package/dist/agent/approval-native.d.ts +3 -0
- package/dist/agent/approval-native.js +165 -0
- package/dist/agent/approval-store.d.ts +16 -0
- package/dist/agent/approval-store.js +21 -0
- package/dist/agent/approval.d.ts +33 -0
- package/dist/agent/approval.js +40 -5
- package/dist/agent/compaction.d.ts +2 -0
- package/dist/agent/compaction.js +60 -17
- package/dist/agent/guards.d.ts +16 -16
- package/dist/agent/guards.js +978 -24
- package/dist/agent/policy/defaults.d.ts +16 -0
- package/dist/agent/policy/defaults.js +17 -0
- package/dist/agent/policy/egress.d.ts +32 -0
- package/dist/agent/policy/egress.js +280 -0
- package/dist/agent/process-tree.d.ts +20 -0
- package/dist/agent/process-tree.js +81 -10
- package/dist/agent/protocol.d.ts +20 -1
- package/dist/agent/protocol.js +165 -38
- package/dist/agent/registry.d.ts +3 -1
- package/dist/agent/registry.js +168 -11
- package/dist/agent/runner.d.ts +21 -0
- package/dist/agent/runner.js +44 -5
- package/dist/agent/shell-ast.d.ts +11 -0
- package/dist/agent/shell-ast.js +204 -0
- package/dist/agent/shell-session.d.ts +14 -0
- package/dist/agent/shell-session.js +251 -40
- package/dist/agent/tool-schemas.js +26 -7
- package/dist/agent/tools/append_file.js +3 -1
- package/dist/agent/tools/apply_patch.js +44 -4
- package/dist/agent/tools/copy_file.js +26 -3
- package/dist/agent/tools/delete_file.js +22 -3
- package/dist/agent/tools/grep.js +35 -4
- package/dist/agent/tools/make_dir.js +3 -1
- package/dist/agent/tools/move_path.d.ts +7 -0
- package/dist/agent/tools/move_path.js +31 -5
- package/dist/agent/tools/read_file.js +63 -3
- package/dist/agent/tools/read_many.js +21 -3
- package/dist/agent/tools/readers.d.ts +53 -1
- package/dist/agent/tools/readers.js +236 -26
- package/dist/agent/tools/replace_in_file.js +38 -5
- package/dist/agent/tools/shell.js +2 -2
- package/dist/agent/tools/types.d.ts +26 -0
- package/dist/agent/tools/validate.d.ts +15 -2
- package/dist/agent/tools/validate.js +66 -3
- package/dist/agent/tools/write_file.js +3 -1
- package/dist/app.d.ts +2 -1
- package/dist/app.js +526 -85
- package/dist/cli-main.js +363 -0
- package/dist/cli.js +397 -337
- package/dist/commands/about.js +1 -1
- package/dist/commands/clear.d.ts +1 -0
- package/dist/commands/clear.js +1 -1
- package/dist/commands/compact.d.ts +12 -0
- package/dist/commands/compact.js +14 -2
- package/dist/commands/context.js +5 -4
- package/dist/commands/index.js +2 -0
- package/dist/commands/jobs.js +3 -2
- package/dist/commands/kill.js +3 -1
- package/dist/commands/login.d.ts +2 -2
- package/dist/commands/login.js +15 -15
- package/dist/commands/model.d.ts +1 -13
- package/dist/commands/model.js +6 -38
- package/dist/commands/provider.d.ts +1 -15
- package/dist/commands/provider.js +4 -22
- package/dist/commands/skills.js +34 -4
- package/dist/commands/threads.d.ts +9 -0
- package/dist/commands/threads.js +26 -0
- package/dist/config/store.d.ts +21 -0
- package/dist/config/store.js +47 -9
- package/dist/config/trust.d.ts +9 -0
- package/dist/config/trust.js +41 -2
- package/dist/headless.d.ts +7 -0
- package/dist/headless.js +213 -0
- package/dist/index.js +19 -3
- package/dist/lib/debug.d.ts +2 -0
- package/dist/lib/debug.js +22 -0
- package/dist/lib/diagnostics.d.ts +19 -0
- package/dist/lib/diagnostics.js +32 -6
- package/dist/lib/highlight-subset.d.ts +21 -0
- package/dist/lib/highlight-subset.js +88 -0
- package/dist/lib/lifecycle.d.ts +5 -0
- package/dist/lib/lifecycle.js +62 -0
- package/dist/lib/markdown.js +22 -4
- package/dist/providers/deepseek.js +1 -1
- package/dist/providers/openaiCompatible.d.ts +24 -2
- package/dist/providers/openaiCompatible.js +20 -6
- package/dist/providers/spec.d.ts +12 -0
- package/dist/providers/spec.js +29 -0
- package/dist/session/log.d.ts +14 -2
- package/dist/session/log.js +56 -27
- package/dist/session/projection.d.ts +34 -1
- package/dist/session/projection.js +315 -46
- package/dist/skills/manager.d.ts +15 -0
- package/dist/skills/manager.js +135 -18
- package/dist/types.d.ts +15 -0
- package/dist/ui/StatusBar.js +1 -1
- package/package.json +3 -2
- package/src/agent/approval-json.ts +103 -0
- package/src/agent/approval-native.ts +168 -0
- package/src/agent/approval-store.ts +38 -0
- package/src/agent/approval.ts +58 -11
- package/src/agent/compaction.ts +62 -17
- package/src/agent/guards.ts +1049 -25
- package/src/agent/policy/defaults.ts +34 -0
- package/src/agent/policy/egress.ts +335 -0
- package/src/agent/process-tree.ts +105 -23
- package/src/agent/protocol.ts +187 -39
- package/src/agent/registry.ts +186 -14
- package/src/agent/runner.ts +50 -7
- package/src/agent/shell-ast.ts +227 -0
- package/src/agent/shell-session.ts +265 -41
- package/src/agent/tool-schemas.ts +26 -7
- package/src/agent/tools/append_file.ts +3 -1
- package/src/agent/tools/apply_patch.ts +45 -4
- package/src/agent/tools/copy_file.ts +27 -3
- package/src/agent/tools/delete_file.ts +22 -3
- package/src/agent/tools/grep.ts +36 -4
- package/src/agent/tools/make_dir.ts +3 -1
- package/src/agent/tools/move_path.ts +38 -5
- package/src/agent/tools/read_file.ts +64 -3
- package/src/agent/tools/read_many.ts +21 -3
- package/src/agent/tools/readers.ts +289 -26
- package/src/agent/tools/replace_in_file.ts +39 -5
- package/src/agent/tools/shell.ts +2 -2
- package/src/agent/tools/types.ts +43 -1
- package/src/agent/tools/validate.ts +73 -4
- package/src/agent/tools/write_file.ts +3 -1
- package/src/app.ts +533 -82
- package/src/{cli.ts → cli-main.ts} +34 -6
- package/src/commands/about.ts +1 -1
- package/src/commands/clear.ts +1 -1
- package/src/commands/compact.ts +14 -2
- package/src/commands/context.ts +5 -4
- package/src/commands/index.ts +2 -0
- package/src/commands/jobs.ts +3 -2
- package/src/commands/kill.ts +3 -1
- package/src/commands/login.ts +15 -15
- package/src/commands/model.ts +6 -38
- package/src/commands/provider.ts +4 -22
- package/src/commands/skills.ts +34 -4
- package/src/commands/threads.ts +28 -0
- package/src/config/store.ts +47 -9
- package/src/config/trust.ts +45 -2
- package/src/headless.ts +225 -0
- package/src/index.ts +19 -3
- package/src/lib/debug.ts +24 -0
- package/src/lib/diagnostics.ts +32 -6
- package/src/lib/highlight-subset.ts +93 -0
- package/src/lib/lifecycle.ts +69 -0
- package/src/lib/markdown.ts +22 -4
- package/src/providers/deepseek.ts +1 -1
- package/src/providers/openaiCompatible.ts +47 -10
- package/src/providers/spec.ts +42 -0
- package/src/session/log.ts +70 -27
- package/src/session/projection.ts +360 -47
- package/src/skills/manager.ts +140 -18
- package/src/types/highlight-shims.d.ts +14 -0
- package/src/types.ts +16 -1
- package/src/ui/StatusBar.ts +1 -1
- package/tsconfig.json +1 -0
- /package/dist/{cli.d.ts → cli-main.d.ts} +0 -0
package/dist/agent/protocol.js
CHANGED
|
@@ -1,18 +1,68 @@
|
|
|
1
|
-
const TOOL_RE = /<fastcode_tool>([\s\S]*?)<\/fastcode_tool>/g;
|
|
2
1
|
// Some models (notably DeepSeek) emit their native "DSML" tool-call markup
|
|
3
2
|
// instead of <fastcode_tool>. The markers look like `<|DSML|invoke ...>`
|
|
4
3
|
// (U+FF5C fullwidth bar, sometimes doubled or spaced). We canonicalize those
|
|
5
4
|
// markers back to plain angle-bracket tags and then parse the
|
|
6
5
|
// calls -> invoke -> parameter structure as a second dialect.
|
|
6
|
+
const FASTCODE_TOOL_OPEN = "<fastcode_tool>";
|
|
7
|
+
const FASTCODE_TOOL_CLOSE_RE = /<\/\s*fastcode_tool\s*>/gi;
|
|
8
|
+
const RESIDUAL_CLOSE_RE = /<\/\s*(?:fastcode_tool|parameter|invoke|calls)\s*>/i;
|
|
7
9
|
const DSML_OPEN_RE = /<\s*(?:[||]\s*)+DSML\s*(?:[||]\s*)+/gi;
|
|
8
10
|
const DSML_CLOSE_RE = /<\/\s*(?:[||]\s*)+DSML\s*(?:[||]\s*)+/gi;
|
|
9
|
-
const DSML_BLOCK_RE = /<([a-z_]*calls?)>([\s\S]*?)<\/\1
|
|
10
|
-
const DSML_INVOKE_RE = /<invoke\s+name="([^"]+)"\s*>([\s\S]*?)
|
|
11
|
-
const DSML_PARAM_RE = /<parameter\s+name="([^"]+)"([^>]*)>([\s\S]*?)
|
|
11
|
+
const DSML_BLOCK_RE = /<([a-z_]*calls?)>([\s\S]*?)<\/\s*\1\s*>/gi;
|
|
12
|
+
const DSML_INVOKE_RE = /<invoke\s+name="([^"]+)"\s*>([\s\S]*?)<\/\s*invoke\s*>/gi;
|
|
13
|
+
const DSML_PARAM_RE = /<parameter\s+name="([^"]+)"([^>]*)>([\s\S]*?)<\/\s*parameter\s*>/gi;
|
|
14
|
+
const INVOKE_OPEN_RE = /<invoke\s+name="([^"]+)"\s*>/gi;
|
|
12
15
|
/** Turn the DSML glyph markers into ordinary `<tag>` / `</tag>` tags. */
|
|
13
16
|
function canonicalizeDsml(text) {
|
|
14
17
|
return text.replace(DSML_CLOSE_RE, "</").replace(DSML_OPEN_RE, "<");
|
|
15
18
|
}
|
|
19
|
+
function invokeToCall(invoke) {
|
|
20
|
+
const call = { tool: invoke[1] };
|
|
21
|
+
for (const param of (invoke[2] ?? "").matchAll(DSML_PARAM_RE))
|
|
22
|
+
call[param[1]] = coerceDsmlValue(param[2] ?? "", param[3] ?? "");
|
|
23
|
+
return call;
|
|
24
|
+
}
|
|
25
|
+
function insideSpan(start, spans) {
|
|
26
|
+
return spans.some((span) => start >= span.start && start < span.end);
|
|
27
|
+
}
|
|
28
|
+
function matchFrom(text, from, re) {
|
|
29
|
+
re.lastIndex = from;
|
|
30
|
+
const match = re.exec(text);
|
|
31
|
+
if (!match || match.index === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
return { start: match.index, end: match.index + match[0].length };
|
|
34
|
+
}
|
|
35
|
+
function parseJsonCall(body) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(body.trim());
|
|
38
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && typeof parsed.tool === "string")
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Malformed JSON is reported by the caller.
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Recover a JSON object that starts at the beginning of `text` but may be
|
|
48
|
+
* followed by more markup (for example a drifted close tag). Returns the
|
|
49
|
+
* consumed prefix length so the caller can continue after it.
|
|
50
|
+
*/
|
|
51
|
+
function salvageJsonPrefix(text) {
|
|
52
|
+
let searchFrom = 0;
|
|
53
|
+
while (searchFrom < text.length) {
|
|
54
|
+
const end = text.indexOf("}", searchFrom);
|
|
55
|
+
if (end < 0)
|
|
56
|
+
return undefined;
|
|
57
|
+
const call = parseJsonCall(text.slice(0, end + 1));
|
|
58
|
+
if (call) {
|
|
59
|
+
const close = matchFrom(text, end + 1, FASTCODE_TOOL_CLOSE_RE);
|
|
60
|
+
return { call, consumed: close ? close.end : end + 1 };
|
|
61
|
+
}
|
|
62
|
+
searchFrom = end + 1;
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
16
66
|
function coerceDsmlValue(attrs, raw) {
|
|
17
67
|
const value = raw.replace(/^\n/, "").replace(/\n$/, "");
|
|
18
68
|
if (/\bstring\s*=\s*"true"/i.test(attrs))
|
|
@@ -32,44 +82,119 @@ function coerceDsmlValue(attrs, raw) {
|
|
|
32
82
|
return Number.isNaN(n) ? value : n;
|
|
33
83
|
}
|
|
34
84
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Parse all complete tool calls, then salvage only the *remaining* text.
|
|
87
|
+
*
|
|
88
|
+
* Invariants (regression-tested):
|
|
89
|
+
* - a complete construct is consumed exactly once and never rescanned;
|
|
90
|
+
* - the salvage pass receives `raw.slice(consumedEnd)`, never the full text;
|
|
91
|
+
* - two adjacent normal calls plus a stray close tag stays at two calls.
|
|
92
|
+
*/
|
|
93
|
+
export function parseToolCalls(text) {
|
|
94
|
+
const raw = canonicalizeDsml(String(text ?? ""));
|
|
42
95
|
const calls = [];
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
96
|
+
const errors = [];
|
|
97
|
+
const spans = [];
|
|
98
|
+
let cursor = 0;
|
|
99
|
+
while (cursor < raw.length) {
|
|
100
|
+
const open = raw.indexOf(FASTCODE_TOOL_OPEN, cursor);
|
|
101
|
+
if (open < 0)
|
|
102
|
+
break;
|
|
103
|
+
const innerStart = open + FASTCODE_TOOL_OPEN.length;
|
|
104
|
+
const close = matchFrom(raw, innerStart, FASTCODE_TOOL_CLOSE_RE);
|
|
105
|
+
if (!close) {
|
|
106
|
+
const salvaged = salvageJsonPrefix(raw.slice(innerStart));
|
|
107
|
+
if (salvaged) {
|
|
108
|
+
calls.push(salvaged.call);
|
|
109
|
+
cursor = innerStart + salvaged.consumed;
|
|
110
|
+
spans.push({ start: open, end: cursor });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
errors.push("unclosed <fastcode_tool> block; re-emit a complete tool call");
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
const body = raw.slice(innerStart, close.start);
|
|
117
|
+
const full = parseJsonCall(body);
|
|
118
|
+
if (full) {
|
|
119
|
+
calls.push(full);
|
|
120
|
+
spans.push({ start: open, end: close.end });
|
|
121
|
+
cursor = close.end;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
// Closing drift: the JSON may be valid even when extra markup (or a
|
|
125
|
+
// second opener) sits before the matched close tag.
|
|
126
|
+
const prefix = salvageJsonPrefix(body);
|
|
127
|
+
if (prefix) {
|
|
128
|
+
calls.push(prefix.call);
|
|
129
|
+
cursor = innerStart + prefix.consumed;
|
|
130
|
+
spans.push({ start: open, end: cursor });
|
|
131
|
+
continue;
|
|
49
132
|
}
|
|
133
|
+
errors.push("invalid JSON inside <fastcode_tool>; re-emit a complete tool call");
|
|
134
|
+
spans.push({ start: open, end: close.end });
|
|
135
|
+
cursor = close.end;
|
|
50
136
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
137
|
+
// DSML dialect: parse each complete block once, then complete invokes that
|
|
138
|
+
// did not belong to a block. Spans prevent a second parse of the same text.
|
|
139
|
+
for (const block of raw.matchAll(DSML_BLOCK_RE)) {
|
|
140
|
+
const start = block.index ?? 0;
|
|
141
|
+
const end = start + block[0].length;
|
|
142
|
+
if (insideSpan(start, spans))
|
|
143
|
+
continue;
|
|
144
|
+
for (const invoke of (block[2] ?? "").matchAll(DSML_INVOKE_RE))
|
|
145
|
+
calls.push(invokeToCall(invoke));
|
|
146
|
+
spans.push({ start, end });
|
|
147
|
+
}
|
|
148
|
+
for (const invoke of raw.matchAll(DSML_INVOKE_RE)) {
|
|
149
|
+
const start = invoke.index ?? 0;
|
|
150
|
+
if (insideSpan(start, spans))
|
|
58
151
|
continue;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
152
|
+
calls.push(invokeToCall(invoke));
|
|
153
|
+
spans.push({ start, end: start + invoke[0].length });
|
|
154
|
+
}
|
|
155
|
+
const consumedEnd = spans.reduce((max, span) => Math.max(max, span.end), 0);
|
|
156
|
+
salvageResidual(raw.slice(consumedEnd), calls, errors);
|
|
157
|
+
return { calls, consumedEnd, errors };
|
|
158
|
+
}
|
|
159
|
+
function salvageResidual(tail, calls, errors) {
|
|
160
|
+
if (!tail.trim())
|
|
161
|
+
return;
|
|
162
|
+
let salvaged = false;
|
|
163
|
+
const toolOpen = tail.indexOf(FASTCODE_TOOL_OPEN);
|
|
164
|
+
if (toolOpen >= 0) {
|
|
165
|
+
const recovered = salvageJsonPrefix(tail.slice(toolOpen + FASTCODE_TOOL_OPEN.length));
|
|
166
|
+
if (recovered) {
|
|
167
|
+
calls.push(recovered.call);
|
|
168
|
+
salvaged = true;
|
|
63
169
|
}
|
|
64
|
-
|
|
65
|
-
|
|
170
|
+
else {
|
|
171
|
+
errors.push("unclosed <fastcode_tool> block; re-emit a complete tool call");
|
|
66
172
|
}
|
|
67
173
|
}
|
|
68
|
-
|
|
69
|
-
|
|
174
|
+
const openers = Array.from(tail.matchAll(INVOKE_OPEN_RE));
|
|
175
|
+
openers.forEach((opener, index) => {
|
|
176
|
+
const start = opener.index ?? 0;
|
|
177
|
+
const bodyStart = start + opener[0].length;
|
|
178
|
+
const nextStart = openers[index + 1]?.index ?? tail.length;
|
|
179
|
+
const call = invokeToCall({ 1: opener[1], 2: tail.slice(bodyStart, nextStart) });
|
|
180
|
+
if (Object.keys(call).length > 1) {
|
|
181
|
+
calls.push(call);
|
|
182
|
+
salvaged = true;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
errors.push(`incomplete invoke for "${opener[1]}"; re-emit a complete tool call`);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
if (!salvaged && openers.length === 0 && toolOpen < 0 && RESIDUAL_CLOSE_RE.test(tail))
|
|
189
|
+
errors.push("stray closing tag without an opening tool call; re-emit a complete tool call");
|
|
70
190
|
}
|
|
71
191
|
export function stripToolCalls(text) {
|
|
72
|
-
return canonicalizeDsml(text
|
|
192
|
+
return canonicalizeDsml(String(text ?? ""))
|
|
193
|
+
.replace(/<fastcode_tool>[\s\S]*?<\/\s*fastcode_tool\s*>/gi, "")
|
|
194
|
+
.replace(DSML_BLOCK_RE, "")
|
|
195
|
+
.replace(/<fastcode_tool>[\s\S]*$/i, "")
|
|
196
|
+
.replace(/<\/\s*(?:fastcode_tool|parameter|invoke|calls)\s*>/gi, "")
|
|
197
|
+
.trim();
|
|
73
198
|
}
|
|
74
199
|
export function formatToolResults(results) {
|
|
75
200
|
const blocks = results.map((r, index) => {
|
|
@@ -101,19 +226,21 @@ Available tools:
|
|
|
101
226
|
Polling returns only new output; use waitMs to wait briefly for more.
|
|
102
227
|
- pwd: {"tool":"pwd"}
|
|
103
228
|
- read_file: {"tool":"read_file","path":"path/to/file"}
|
|
104
|
-
-
|
|
229
|
+
- read_file range: {"tool":"read_file","path":"large.ts","offset":800,"limit":50}
|
|
230
|
+
offset is 1-based; use it instead of reading a large file from the start.
|
|
231
|
+
- read_many: {"tool":"read_many","paths":["file1","file2"],"maxBytes":300000}
|
|
105
232
|
- write_file: {"tool":"write_file","path":"path/to/file","content":"full file contents"}
|
|
106
233
|
- append_file: {"tool":"append_file","path":"path/to/file","content":"text to append"}
|
|
107
234
|
- apply_patch: {"tool":"apply_patch","patch":"*** Begin Patch\\n*** Update File: path\\n@@\\n-old\\n+new\\n*** End Patch\\n"}
|
|
108
235
|
- list_files: {"tool":"list_files","path":"optional/path","maxDepth":2,"detail":false}
|
|
109
236
|
- read_file is capped (default 200KB); grep skips files over 1MB. .fastcodeignore is honored.
|
|
110
|
-
- grep: {"tool":"grep","pattern":"text or regex","path":"optional/path"}
|
|
111
|
-
- replace_in_file: {"tool":"replace_in_file","path":"path/to/file","find":"exact text","replace":"replacement"}
|
|
237
|
+
- grep: {"tool":"grep","pattern":"text or regex","path":"optional/path","caseSensitive":false,"glob":"*.ts","context":2,"maxResults":200}
|
|
238
|
+
- replace_in_file: {"tool":"replace_in_file","path":"path/to/file","find":"exact text","replace":"replacement","expectedMatches":1}
|
|
112
239
|
- stat_path: {"tool":"stat_path","path":"path/to/file"}
|
|
113
240
|
- make_dir: {"tool":"make_dir","path":"path/to/dir"}
|
|
114
|
-
- copy_file: {"tool":"copy_file","from":"src","to":"dest"}
|
|
241
|
+
- copy_file: {"tool":"copy_file","from":"src","to":"dest","overwrite":false}
|
|
115
242
|
- move_path: {"tool":"move_path","from":"src","to":"dest"}
|
|
116
|
-
- delete_file: {"tool":"delete_file","path":"path/to/file"}
|
|
243
|
+
- delete_file: {"tool":"delete_file","path":"path/to/file","recursive":false}
|
|
117
244
|
|
|
118
245
|
Rules:
|
|
119
246
|
- Prefer inspecting files before editing.
|
package/dist/agent/registry.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ApprovalDecision } from "./approval.js";
|
|
2
2
|
import type { Tool, ToolCall, ToolCtx, ToolResult } from "./tools/types.js";
|
|
3
3
|
export declare function assertToolContract(tools?: Tool[]): void;
|
|
4
4
|
export declare function toolCatalogHash(tools?: Tool[]): string;
|
|
5
5
|
export declare class ToolRegistry {
|
|
6
6
|
private readonly tools;
|
|
7
|
+
private readonly catalogHash;
|
|
7
8
|
constructor(tools?: Tool[]);
|
|
8
9
|
get(name: string): Tool | undefined;
|
|
9
10
|
list(): Tool[];
|
|
@@ -14,4 +15,5 @@ export declare const registry: ToolRegistry;
|
|
|
14
15
|
export declare function runAgentTool(call: ToolCall, cwd?: string, options?: {
|
|
15
16
|
signal?: AbortSignal;
|
|
16
17
|
turn?: number;
|
|
18
|
+
requestApproval?: import("./approval.js").ApprovalResolver;
|
|
17
19
|
}): Promise<ToolResult>;
|
package/dist/agent/registry.js
CHANGED
|
@@ -1,9 +1,47 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { isTrusted } from "../config/trust.js";
|
|
3
|
+
import { DEFAULT_ALLOW_OPTIONS, approvalRequestId, } from "./approval.js";
|
|
4
|
+
import { approvalKey, isPreApproved, rememberApproval } from "./approval-store.js";
|
|
3
5
|
import { evaluateShellCommand } from "./guards.js";
|
|
4
6
|
import { TOOLS } from "./tools/index.js";
|
|
7
|
+
import { loadConfig } from "../config/store.js";
|
|
8
|
+
import { egressAudit, evaluateEgressCall, evaluateEgressOutput, formatEgressDenial, normalizeEgressConfig, } from "./policy/egress.js";
|
|
5
9
|
const STATE_SCOPES = new Set(["call", "turn", "session"]);
|
|
6
10
|
const REVALIDATE_KEYS = new Set(["trust", "sandboxRoots", "schema", "modelCaps"]);
|
|
11
|
+
const TOOL_ERROR_CODES = new Set([
|
|
12
|
+
"ENOENT", "EACCES", "EEXIST", "ENOTDIR", "EISDIR", "EXDEV", "INVALID_ARGS",
|
|
13
|
+
"SANDBOX_DENY", "APPROVAL_DENY", "EGRESS_DENY", "NOT_FOUND", "ABORTED", "UNKNOWN",
|
|
14
|
+
]);
|
|
15
|
+
function errorCode(error) {
|
|
16
|
+
const code = String(error?.code ?? "");
|
|
17
|
+
if (TOOL_ERROR_CODES.has(code))
|
|
18
|
+
return code;
|
|
19
|
+
const message = String(error?.message ?? error ?? "");
|
|
20
|
+
if (/outside sandbox/i.test(message))
|
|
21
|
+
return "SANDBOX_DENY";
|
|
22
|
+
if (/not trusted/i.test(message))
|
|
23
|
+
return "APPROVAL_DENY";
|
|
24
|
+
if (/aborted/i.test(message))
|
|
25
|
+
return "ABORTED";
|
|
26
|
+
return "UNKNOWN";
|
|
27
|
+
}
|
|
28
|
+
function unique(values) {
|
|
29
|
+
return Array.from(new Set((values ?? []).filter(Boolean)));
|
|
30
|
+
}
|
|
31
|
+
function normalizeSideEffects(sideEffects) {
|
|
32
|
+
if (!sideEffects)
|
|
33
|
+
return undefined;
|
|
34
|
+
const touched = unique(sideEffects.touched);
|
|
35
|
+
const created = unique(sideEffects.created);
|
|
36
|
+
const removed = unique(sideEffects.removed);
|
|
37
|
+
if (touched.length === 0 && created.length === 0 && removed.length === 0)
|
|
38
|
+
return undefined;
|
|
39
|
+
return {
|
|
40
|
+
...(touched.length > 0 ? { touched } : {}),
|
|
41
|
+
...(created.length > 0 ? { created } : {}),
|
|
42
|
+
...(removed.length > 0 ? { removed } : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
7
45
|
export function assertToolContract(tools = TOOLS) {
|
|
8
46
|
const names = new Set();
|
|
9
47
|
for (const tool of tools) {
|
|
@@ -36,10 +74,12 @@ export function toolCatalogHash(tools = TOOLS) {
|
|
|
36
74
|
}
|
|
37
75
|
export class ToolRegistry {
|
|
38
76
|
tools = new Map();
|
|
77
|
+
catalogHash;
|
|
39
78
|
constructor(tools = TOOLS) {
|
|
40
79
|
assertToolContract(tools);
|
|
41
80
|
for (const tool of tools)
|
|
42
81
|
this.tools.set(tool.name, tool);
|
|
82
|
+
this.catalogHash = toolCatalogHash(tools);
|
|
43
83
|
}
|
|
44
84
|
get(name) {
|
|
45
85
|
return this.tools.get(name);
|
|
@@ -53,6 +93,14 @@ export class ToolRegistry {
|
|
|
53
93
|
return { kind: "deny", reason: `unknown tool: ${String(call.tool)}` };
|
|
54
94
|
if (tool.name === "shell") {
|
|
55
95
|
const verdict = evaluateShellCommand(call.command);
|
|
96
|
+
if (verdict.action === "hard_block")
|
|
97
|
+
return { kind: "deny", reason: verdict.output, code: verdict.code, target: verdict.target };
|
|
98
|
+
if (verdict.action === "soft_gate") {
|
|
99
|
+
const key = approvalKey(tool.name, call, ctx.cwd);
|
|
100
|
+
if (isPreApproved(key, this.catalogHash))
|
|
101
|
+
return { kind: "allow", scope: "once" };
|
|
102
|
+
return { kind: "pending", request: shellApprovalRequest(call, verdict) };
|
|
103
|
+
}
|
|
56
104
|
if (!verdict.allow)
|
|
57
105
|
return { kind: "deny", reason: verdict.output };
|
|
58
106
|
}
|
|
@@ -67,24 +115,133 @@ export class ToolRegistry {
|
|
|
67
115
|
async execute(call, ctx) {
|
|
68
116
|
const tool = this.get(call.tool);
|
|
69
117
|
if (!tool)
|
|
70
|
-
return { ok: false, output: `unknown tool: ${String(call.tool)}
|
|
71
|
-
const
|
|
118
|
+
return { ok: false, output: `unknown tool: ${String(call.tool)}`, code: "INVALID_ARGS" };
|
|
119
|
+
const toolCtx = { ...ctx, catalogHash: this.catalogHash };
|
|
120
|
+
const prepared = tool.prepare(call, toolCtx);
|
|
72
121
|
if (prepared.ok === false)
|
|
73
|
-
return { ok: false, output: prepared.output };
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return {
|
|
122
|
+
return { ok: false, output: prepared.output, code: prepared.code ?? "INVALID_ARGS", hint: prepared.hint };
|
|
123
|
+
const egressConfig = normalizeEgressConfig(loadConfig().egress);
|
|
124
|
+
const preEgress = evaluateEgressCall(call, ctx.cwd, egressConfig);
|
|
125
|
+
if (preEgress.action === "deny") {
|
|
126
|
+
const egressDecision = preEgress.decision;
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
output: formatEgressDenial(egressDecision),
|
|
130
|
+
code: "EGRESS_DENY",
|
|
131
|
+
hint: "Egress policy blocked this call before execution; no approval request was created.",
|
|
132
|
+
egress: egressAudit(egressDecision),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const preAudit = preEgress.decision ? egressAudit(preEgress.decision) : undefined;
|
|
136
|
+
const decision = this.approve(call, toolCtx);
|
|
137
|
+
if (decision.kind === "deny") {
|
|
138
|
+
const output = decision.reason ?? "denied by approval policy";
|
|
139
|
+
if (tool.name === "shell" && (decision.code === "sensitive_file_egress" || decision.code === "credential_read")) {
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
output,
|
|
143
|
+
code: "EGRESS_DENY",
|
|
144
|
+
egress: {
|
|
145
|
+
outlet: "shell.command",
|
|
146
|
+
...(decision.target ? { target: decision.target } : {}),
|
|
147
|
+
rule: decision.code,
|
|
148
|
+
mode: "block",
|
|
149
|
+
wouldBlock: true,
|
|
150
|
+
code: "EGRESS_DENY",
|
|
151
|
+
reason: decision.code,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return { ok: false, output, code: "APPROVAL_DENY" };
|
|
156
|
+
}
|
|
157
|
+
if (decision.kind === "pending") {
|
|
158
|
+
if (!ctx.requestApproval)
|
|
159
|
+
return { ok: false, output: "approval required, but no approval resolver is registered (fail closed)", code: "APPROVAL_DENY" };
|
|
160
|
+
let resolved;
|
|
161
|
+
try {
|
|
162
|
+
resolved = await ctx.requestApproval(decision.request);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
return { ok: false, output: `approval failed (fail closed): ${err?.message ?? String(err)}`, code: "APPROVAL_DENY" };
|
|
166
|
+
}
|
|
167
|
+
if (!resolved || resolved.kind === "pending")
|
|
168
|
+
return { ok: false, output: "approval unresolved; nothing was executed (fail closed)", code: "APPROVAL_DENY" };
|
|
169
|
+
if (resolved.kind !== "allow") {
|
|
170
|
+
const reason = resolved.kind === "deny" ? resolved.reason : resolved.kind === "feedback" ? resolved.text : `decision ${resolved.kind} requires a later batch`;
|
|
171
|
+
return { ok: false, output: `approval denied: ${reason ?? "no reason"}`, code: "APPROVAL_DENY" };
|
|
172
|
+
}
|
|
173
|
+
rememberApproval(approvalKey(tool.name, call, ctx.cwd), this.catalogHash, resolved.scope);
|
|
174
|
+
}
|
|
79
175
|
try {
|
|
80
|
-
|
|
176
|
+
let result = await tool.execute(call, toolCtx);
|
|
177
|
+
result = {
|
|
178
|
+
...result,
|
|
179
|
+
sideEffects: normalizeSideEffects(result.sideEffects),
|
|
180
|
+
...(preAudit && !result.egress ? { egress: preAudit } : {}),
|
|
181
|
+
};
|
|
182
|
+
const postEgress = evaluateEgressOutput(call.tool, result.output, egressConfig);
|
|
183
|
+
if (postEgress.action === "deny") {
|
|
184
|
+
const egressDecision = postEgress.decision;
|
|
185
|
+
return {
|
|
186
|
+
ok: false,
|
|
187
|
+
output: formatEgressDenial(egressDecision),
|
|
188
|
+
code: "EGRESS_DENY",
|
|
189
|
+
hint: "Egress policy blocked this tool output before it reached the model.",
|
|
190
|
+
sideEffects: result.sideEffects,
|
|
191
|
+
egress: egressAudit(egressDecision),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (postEgress.decision)
|
|
195
|
+
result = { ...result, egress: egressAudit(postEgress.decision) };
|
|
196
|
+
return result;
|
|
81
197
|
}
|
|
82
198
|
catch (err) {
|
|
83
|
-
|
|
199
|
+
const output = err?.message ?? String(err);
|
|
200
|
+
const postEgress = evaluateEgressOutput(call.tool, output, egressConfig);
|
|
201
|
+
if (postEgress.action === "deny") {
|
|
202
|
+
const egressDecision = postEgress.decision;
|
|
203
|
+
return {
|
|
204
|
+
ok: false,
|
|
205
|
+
output: formatEgressDenial(egressDecision),
|
|
206
|
+
code: "EGRESS_DENY",
|
|
207
|
+
hint: "Egress policy blocked this error output before it reached the model.",
|
|
208
|
+
egress: egressAudit(egressDecision),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
output,
|
|
214
|
+
code: errorCode(err),
|
|
215
|
+
...(preAudit ? { egress: preAudit } : {}),
|
|
216
|
+
...(postEgress.decision ? { egress: egressAudit(postEgress.decision) } : {}),
|
|
217
|
+
};
|
|
84
218
|
}
|
|
85
219
|
}
|
|
86
220
|
}
|
|
221
|
+
function shellApprovalRequest(call, verdict) {
|
|
222
|
+
const options = DEFAULT_ALLOW_OPTIONS.map((option) => ({ ...option }));
|
|
223
|
+
const denyIndex = options.findIndex((option) => option.kind === "deny");
|
|
224
|
+
const target = verdict.target ?? "";
|
|
225
|
+
const prompt = `Shell command needs approval: ${verdict.code}${target ? ` (${target})` : ""}`;
|
|
226
|
+
return {
|
|
227
|
+
id: approvalRequestId(),
|
|
228
|
+
tool: "shell",
|
|
229
|
+
args: call,
|
|
230
|
+
reason: "destructive",
|
|
231
|
+
options,
|
|
232
|
+
tier: 2,
|
|
233
|
+
prompt,
|
|
234
|
+
source: "system",
|
|
235
|
+
defaultIndex: denyIndex >= 0 ? denyIndex : 0,
|
|
236
|
+
timeoutMs: 60_000,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
87
239
|
export const registry = new ToolRegistry();
|
|
88
240
|
export async function runAgentTool(call, cwd = process.cwd(), options = {}) {
|
|
89
|
-
return registry.execute(call, {
|
|
241
|
+
return registry.execute(call, {
|
|
242
|
+
cwd,
|
|
243
|
+
signal: options.signal,
|
|
244
|
+
turn: options.turn,
|
|
245
|
+
requestApproval: options.requestApproval,
|
|
246
|
+
});
|
|
90
247
|
}
|
package/dist/agent/runner.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ActiveFile } from "../session/log.js";
|
|
|
3
3
|
import type { SideEffects } from "./tools/types.js";
|
|
4
4
|
import { type CompactionConfig } from "./compaction.js";
|
|
5
5
|
import { type ToolProtocol } from "./protocol-mode.js";
|
|
6
|
+
import type { ApprovalResolver } from "./approval.js";
|
|
6
7
|
export interface ToolCall {
|
|
7
8
|
tool: string;
|
|
8
9
|
[key: string]: any;
|
|
@@ -39,6 +40,7 @@ export interface RunOptions {
|
|
|
39
40
|
onNativeOk?: () => void;
|
|
40
41
|
takeSteering?: () => string[];
|
|
41
42
|
onDelta?: (delta: string, text: string) => void;
|
|
43
|
+
requestApproval?: ApprovalResolver;
|
|
42
44
|
}
|
|
43
45
|
export type AgentStopReason = "complete" | "max_turns" | "token_budget" | "cost_budget" | "loop_detected" | "aborted" | "incomplete" | "stream_error";
|
|
44
46
|
export type AgentErrorKind = "token_budget" | "cost_budget" | "loop_detected" | "aborted" | "stream_error";
|
|
@@ -72,6 +74,25 @@ export type AgentEvent = {
|
|
|
72
74
|
ok: boolean;
|
|
73
75
|
output: string;
|
|
74
76
|
sideEffects?: SideEffects;
|
|
77
|
+
code?: string;
|
|
78
|
+
hint?: string;
|
|
79
|
+
truncated?: boolean;
|
|
80
|
+
totalLines?: number;
|
|
81
|
+
returnedLines?: number;
|
|
82
|
+
totalBytes?: number;
|
|
83
|
+
returnedBytes?: number;
|
|
84
|
+
filesRead?: number;
|
|
85
|
+
filesSkipped?: number;
|
|
86
|
+
matches?: number;
|
|
87
|
+
} | {
|
|
88
|
+
type: "egress_denied";
|
|
89
|
+
outlet: string;
|
|
90
|
+
target?: string;
|
|
91
|
+
rule: string;
|
|
92
|
+
mode: "audit" | "block";
|
|
93
|
+
wouldBlock: boolean;
|
|
94
|
+
code: "EGRESS_DENY";
|
|
95
|
+
reason: string;
|
|
75
96
|
} | {
|
|
76
97
|
type: "usage";
|
|
77
98
|
usage: NonNullable<ChatChunk["usage"]>;
|
package/dist/agent/runner.js
CHANGED
|
@@ -171,6 +171,8 @@ export async function* runAgent(options) {
|
|
|
171
171
|
}
|
|
172
172
|
if (resetNotice)
|
|
173
173
|
yield { type: "notice", text: resetNotice };
|
|
174
|
+
if (streamed.retried > 0)
|
|
175
|
+
yield { type: "notice", text: `Network stream was interrupted; retried ${streamed.retried} time(s).` };
|
|
174
176
|
if (streamed.usage) {
|
|
175
177
|
consumedInput += streamed.usage.inputTokens ?? promptTokens;
|
|
176
178
|
spentCost += streamed.usage.costUsd ?? 0;
|
|
@@ -234,9 +236,11 @@ export async function* runAgent(options) {
|
|
|
234
236
|
yield { type: "tool_call", id, call, native: true };
|
|
235
237
|
const result = parseError
|
|
236
238
|
? { ok: false, output: parseError }
|
|
237
|
-
: await runAgentTool(call, cwd, { signal, turn: turn + 1 });
|
|
239
|
+
: await runAgentTool(call, cwd, { signal, turn: turn + 1, requestApproval: options.requestApproval });
|
|
238
240
|
trackSideEffects(activeFiles, result.sideEffects);
|
|
239
|
-
|
|
241
|
+
if (result.egress)
|
|
242
|
+
yield { type: "egress_denied", ...result.egress };
|
|
243
|
+
yield { type: "tool_result", id, call, ok: Boolean(result.ok), output: String(result.output ?? ""), ...toolResultMeta(result) };
|
|
240
244
|
history = [...history, { role: "tool", toolCallId: id, content: String(result.output ?? "") }];
|
|
241
245
|
}
|
|
242
246
|
yield { type: "checkpoint", turn: turn + 1, cwd, providerId: provider.id, model, activeFiles: Array.from(activeFiles.values()) };
|
|
@@ -246,9 +250,27 @@ export async function* runAgent(options) {
|
|
|
246
250
|
break;
|
|
247
251
|
continue;
|
|
248
252
|
}
|
|
249
|
-
const
|
|
253
|
+
const parsed = options.allowTools === false
|
|
254
|
+
? { calls: [], consumedEnd: 0, errors: [] }
|
|
255
|
+
: parseToolCalls(streamed.text);
|
|
256
|
+
const calls = parsed.calls;
|
|
250
257
|
yield { type: "assistant_raw", text: streamed.text };
|
|
251
258
|
if (calls.length === 0) {
|
|
259
|
+
if (parsed.errors.length > 0) {
|
|
260
|
+
// Never let a broken tool-call block end the turn silently: the
|
|
261
|
+
// model would wait forever for a tool result. Inject a visible
|
|
262
|
+
// tool error and continue so it can re-emit valid markup.
|
|
263
|
+
const id = `tc_${++toolCallSeq}`;
|
|
264
|
+
const call = { tool: "protocol_error" };
|
|
265
|
+
const output = "Previous output contained invalid tool-call markup. " +
|
|
266
|
+
"Re-emit a complete <fastcode_tool>{\"tool\":...}</fastcode_tool> block.\n" +
|
|
267
|
+
parsed.errors.join("\n");
|
|
268
|
+
yield { type: "tool_call", id, call };
|
|
269
|
+
yield { type: "tool_result", id, call, ok: false, output, code: "INVALID_ARGS", hint: "Re-emit a complete tool call." };
|
|
270
|
+
history = [...history, { role: "user", content: formatToolResults([{ call, ok: false, output }]) }];
|
|
271
|
+
yield { type: "checkpoint", turn: turn + 1, cwd, providerId: provider.id, model, activeFiles: Array.from(activeFiles.values()) };
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
252
274
|
lastVisible = visibleText || streamed.text;
|
|
253
275
|
naturalFinish = true;
|
|
254
276
|
stopReason = "complete";
|
|
@@ -280,10 +302,12 @@ export async function* runAgent(options) {
|
|
|
280
302
|
}
|
|
281
303
|
const id = `tc_${++toolCallSeq}`;
|
|
282
304
|
yield { type: "tool_call", id, call };
|
|
283
|
-
const result = await runAgentTool(call, cwd, { signal, turn: turn + 1 });
|
|
305
|
+
const result = await runAgentTool(call, cwd, { signal, turn: turn + 1, requestApproval: options.requestApproval });
|
|
284
306
|
results.push({ call, ok: Boolean(result.ok), output: String(result.output ?? ""), sideEffects: result.sideEffects });
|
|
285
307
|
trackSideEffects(activeFiles, result.sideEffects);
|
|
286
|
-
|
|
308
|
+
if (result.egress)
|
|
309
|
+
yield { type: "egress_denied", ...result.egress };
|
|
310
|
+
yield { type: "tool_result", id, call, ok: Boolean(result.ok), output: String(result.output ?? ""), ...toolResultMeta(result) };
|
|
287
311
|
}
|
|
288
312
|
if (results.length > 0)
|
|
289
313
|
history = [...history, { role: "user", content: formatToolResults(results) }];
|
|
@@ -325,6 +349,21 @@ function positive(value, fallback) {
|
|
|
325
349
|
function optionalPositive(value) {
|
|
326
350
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
327
351
|
}
|
|
352
|
+
function toolResultMeta(result) {
|
|
353
|
+
return {
|
|
354
|
+
sideEffects: result.sideEffects,
|
|
355
|
+
...(result.code ? { code: result.code } : {}),
|
|
356
|
+
...(result.hint ? { hint: result.hint } : {}),
|
|
357
|
+
...(result.truncated !== undefined ? { truncated: result.truncated } : {}),
|
|
358
|
+
...(result.totalLines !== undefined ? { totalLines: result.totalLines } : {}),
|
|
359
|
+
...(result.returnedLines !== undefined ? { returnedLines: result.returnedLines } : {}),
|
|
360
|
+
...(result.totalBytes !== undefined ? { totalBytes: result.totalBytes } : {}),
|
|
361
|
+
...(result.returnedBytes !== undefined ? { returnedBytes: result.returnedBytes } : {}),
|
|
362
|
+
...(result.filesRead !== undefined ? { filesRead: result.filesRead } : {}),
|
|
363
|
+
...(result.filesSkipped !== undefined ? { filesSkipped: result.filesSkipped } : {}),
|
|
364
|
+
...(result.matches !== undefined ? { matches: result.matches } : {}),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
328
367
|
function trackSideEffects(activeFiles, sideEffects) {
|
|
329
368
|
if (!sideEffects)
|
|
330
369
|
return;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ShellAstHard {
|
|
2
|
+
code: string;
|
|
3
|
+
target: string;
|
|
4
|
+
reason: string;
|
|
5
|
+
alternatives: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface ShellAstAnalysis {
|
|
8
|
+
derived: string[];
|
|
9
|
+
hard?: ShellAstHard;
|
|
10
|
+
}
|
|
11
|
+
export declare function analyzeShellAst(command: string): ShellAstAnalysis;
|