ysagc-agent 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -60
- package/dist/agent.d.ts +17 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +331 -0
- package/dist/agent.js.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +320 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +58 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +152 -0
- package/dist/config.js.map +1 -0
- package/dist/executor.d.ts +48 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +458 -0
- package/dist/executor.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +37 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +121 -0
- package/dist/logger.js.map +1 -0
- package/dist/path-utils.d.ts +44 -0
- package/dist/path-utils.d.ts.map +1 -0
- package/dist/path-utils.js +206 -0
- package/dist/path-utils.js.map +1 -0
- package/dist/protocol.d.ts +92 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +94 -0
- package/dist/protocol.js.map +1 -0
- package/dist/security.d.ts +32 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +228 -0
- package/dist/security.js.map +1 -0
- package/dist/skills.d.ts +16 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +105 -0
- package/dist/skills.js.map +1 -0
- package/dist/tokens.d.ts +26 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +50 -0
- package/dist/tokens.js.map +1 -0
- package/dist/tools.d.ts +69 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +694 -0
- package/dist/tools.js.map +1 -0
- package/package.json +36 -21
- package/src/config.js +0 -94
- package/src/http-fs.js +0 -178
- package/src/index.js +0 -442
- package/src/logger.js +0 -84
- package/src/methods/browse.js +0 -91
- package/src/methods/dialog.js +0 -84
- package/src/methods/fs.js +0 -674
- package/src/methods/git.js +0 -391
- package/src/methods/project.js +0 -57
- package/src/methods/scaffold.js +0 -131
- package/src/methods/skill.js +0 -472
- package/src/methods/system.js +0 -82
- package/src/methods/terminal.js +0 -234
- package/src/origin.js +0 -38
- package/src/pairing.js +0 -168
- package/src/rpc.js +0 -62
- package/src/sandbox.js +0 -178
package/dist/tools.js
ADDED
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ToolError = void 0;
|
|
37
|
+
exports.preExecute = preExecute;
|
|
38
|
+
exports.postExecute = postExecute;
|
|
39
|
+
exports.createToolRegistry = createToolRegistry;
|
|
40
|
+
const fs = __importStar(require("node:fs"));
|
|
41
|
+
const path = __importStar(require("node:path"));
|
|
42
|
+
const path_utils_1 = require("./path-utils");
|
|
43
|
+
const security_1 = require("./security");
|
|
44
|
+
const skills_1 = require("./skills");
|
|
45
|
+
const executor_1 = require("./executor");
|
|
46
|
+
/** Thrown when a tool invocation is invalid or unsafe. */
|
|
47
|
+
class ToolError extends Error {
|
|
48
|
+
constructor(code, message) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = 'ToolError';
|
|
51
|
+
this.code = code;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.ToolError = ToolError;
|
|
55
|
+
const MAX_WRITE_BYTES = 10 * 1024 * 1024;
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// argument coercion helpers
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
function requireString(args, key) {
|
|
60
|
+
const v = args[key];
|
|
61
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
62
|
+
throw new ToolError('INVALID_ARG', `"${key}" is required and must be a non-empty string.`);
|
|
63
|
+
}
|
|
64
|
+
return v;
|
|
65
|
+
}
|
|
66
|
+
function optionalString(args, key, fallback) {
|
|
67
|
+
const v = args[key];
|
|
68
|
+
if (v === undefined || v === null)
|
|
69
|
+
return fallback;
|
|
70
|
+
if (typeof v !== 'string') {
|
|
71
|
+
throw new ToolError('INVALID_ARG', `"${key}" must be a string.`);
|
|
72
|
+
}
|
|
73
|
+
return v;
|
|
74
|
+
}
|
|
75
|
+
function optionalInt(args, key, fallback, opts = {}) {
|
|
76
|
+
const v = args[key];
|
|
77
|
+
if (v === undefined || v === null)
|
|
78
|
+
return fallback;
|
|
79
|
+
const n = typeof v === 'number' ? v : Number(v);
|
|
80
|
+
if (!Number.isFinite(n) || !Number.isInteger(n)) {
|
|
81
|
+
throw new ToolError('INVALID_ARG', `"${key}" must be an integer.`);
|
|
82
|
+
}
|
|
83
|
+
if (opts.min !== undefined && n < opts.min) {
|
|
84
|
+
throw new ToolError('INVALID_ARG', `"${key}" must be >= ${opts.min}.`);
|
|
85
|
+
}
|
|
86
|
+
if (opts.max !== undefined && n > opts.max) {
|
|
87
|
+
throw new ToolError('INVALID_ARG', `"${key}" must be <= ${opts.max}.`);
|
|
88
|
+
}
|
|
89
|
+
return n;
|
|
90
|
+
}
|
|
91
|
+
function requiredArray(args, key) {
|
|
92
|
+
const v = args[key];
|
|
93
|
+
if (!Array.isArray(v)) {
|
|
94
|
+
throw new ToolError('INVALID_ARG', `"${key}" must be an array.`);
|
|
95
|
+
}
|
|
96
|
+
return v;
|
|
97
|
+
}
|
|
98
|
+
function abortIfRequested(signal) {
|
|
99
|
+
if (signal?.aborted) {
|
|
100
|
+
throw new ToolError('ABORTED', 'Operation was cancelled.');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function assertNotRoot(realRoot, target) {
|
|
104
|
+
if (path.relative(realRoot, target) === '') {
|
|
105
|
+
throw new ToolError('ROOT_PATH', 'The authorisation root itself cannot be written as a file.');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function assertNotSensitive(target) {
|
|
109
|
+
if ((0, security_1.isSensitivePath)(target)) {
|
|
110
|
+
throw new ToolError('SENSITIVE_PATH', `Path is sensitive and cannot be accessed: ${target}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function isUtf8(buf) {
|
|
114
|
+
try {
|
|
115
|
+
new TextDecoder('utf-8', { fatal: true }).decode(buf);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function readUpTo(target, length) {
|
|
123
|
+
const fd = fs.openSync(target, 'r');
|
|
124
|
+
try {
|
|
125
|
+
const buf = Buffer.alloc(length);
|
|
126
|
+
const bytesRead = fs.readSync(fd, buf, 0, length, 0);
|
|
127
|
+
return buf.subarray(0, Math.max(0, bytesRead));
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
fs.closeSync(fd);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function rel(root, target) {
|
|
134
|
+
return path.relative(root, target).replace(/\\/g, '/');
|
|
135
|
+
}
|
|
136
|
+
function shellQuote(arg) {
|
|
137
|
+
if (arg === '')
|
|
138
|
+
return `''`;
|
|
139
|
+
// Commands are run through a shell on Windows; safer to reject metacharacters.
|
|
140
|
+
if (/[|&;<>`\n]/.test(arg)) {
|
|
141
|
+
throw new ToolError('UNSAFE_ARG', 'git argument contains shell metacharacters.');
|
|
142
|
+
}
|
|
143
|
+
if (/[\s]/.test(arg)) {
|
|
144
|
+
return `"${arg.replace(/"/g, '\\"')}"`;
|
|
145
|
+
}
|
|
146
|
+
return arg;
|
|
147
|
+
}
|
|
148
|
+
/** Tools that mutate the workspace or execute commands (blocked by read-only). */
|
|
149
|
+
const WRITE_OR_COMMAND_TOOLS = new Set([
|
|
150
|
+
'write_file',
|
|
151
|
+
'edit_file',
|
|
152
|
+
'generate',
|
|
153
|
+
'run_command',
|
|
154
|
+
'git',
|
|
155
|
+
]);
|
|
156
|
+
/** Reconstruct the shell command a command-tool intends to run, if any. */
|
|
157
|
+
function commandForTool(tool, args) {
|
|
158
|
+
if (tool === 'run_command') {
|
|
159
|
+
return typeof args.cmd === 'string' ? args.cmd : undefined;
|
|
160
|
+
}
|
|
161
|
+
if (tool === 'git') {
|
|
162
|
+
if (typeof args.command === 'string' && args.command.trim() !== '') {
|
|
163
|
+
return `git ${args.command.trim()}`;
|
|
164
|
+
}
|
|
165
|
+
if (Array.isArray(args.args)) {
|
|
166
|
+
return ['git', ...args.args.map((a) => String(a))].join(' ');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
/** Collect the path-bearing argument values for a tool, if any. */
|
|
172
|
+
function pathArgsForTool(tool, args) {
|
|
173
|
+
const keys = ['path', 'cwd'];
|
|
174
|
+
if (tool === 'generate')
|
|
175
|
+
keys.push('name');
|
|
176
|
+
const out = [];
|
|
177
|
+
for (const key of keys) {
|
|
178
|
+
const v = args[key];
|
|
179
|
+
if (typeof v === 'string' && v.trim() !== '')
|
|
180
|
+
out.push(v);
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
/** Default preset used when the caller did not specify one on the context. */
|
|
185
|
+
const DEFAULT_PRESET = 'workspace-write';
|
|
186
|
+
/** Whether the active preset permits a danger-flagged command without an
|
|
187
|
+
* approval prompt. Only the most-permissive mode allows non-critical danger
|
|
188
|
+
* commands; every other mode blocks them at the gate. */
|
|
189
|
+
function permitsDangerCommand(preset, severity) {
|
|
190
|
+
return preset === 'danger-full-access' && severity !== 'critical';
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The guarded pre-execution hook. Runs before every tool:
|
|
194
|
+
* 1. enforces the permission preset (read-only blocks writes/commands);
|
|
195
|
+
* 2. flags dangerous commands (workspace-write asks for approval, most
|
|
196
|
+
* permissive mode only blocks critical commands, read-only blocks all);
|
|
197
|
+
* 3. canonicalises and validates every path argument (inside-root + sensitive
|
|
198
|
+
* path check).
|
|
199
|
+
* Returns `{ ok: true }` when execution may proceed, otherwise a structured
|
|
200
|
+
* block with an error and optional `needsApproval`.
|
|
201
|
+
*/
|
|
202
|
+
function preExecute(ctx, tool, args) {
|
|
203
|
+
const preset = ctx.permissionPreset ?? DEFAULT_PRESET;
|
|
204
|
+
// 1) Permission preset: read-only denies anything that writes or executes.
|
|
205
|
+
if (preset === 'read-only') {
|
|
206
|
+
if (WRITE_OR_COMMAND_TOOLS.has(tool)) {
|
|
207
|
+
return {
|
|
208
|
+
ok: false,
|
|
209
|
+
error: `Tool "${tool}" is blocked in read-only mode (preset: read-only).`,
|
|
210
|
+
reason: 'read-only preset denies writes and commands',
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// 2) Dangerous-command gate for shell tools.
|
|
215
|
+
const cmd = commandForTool(tool, args);
|
|
216
|
+
if (cmd !== undefined) {
|
|
217
|
+
const assessment = (0, security_1.isDangerCommand)(cmd);
|
|
218
|
+
if (assessment.danger) {
|
|
219
|
+
if (preset === 'danger-full-access' && assessment.severity === 'danger') {
|
|
220
|
+
// Most permissive: allow non-critical danger commands but record why.
|
|
221
|
+
// (critical severity falls through and is blocked below.)
|
|
222
|
+
}
|
|
223
|
+
else if (preset === 'workspace-write') {
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
needsApproval: true,
|
|
227
|
+
error: `Dangerous command requires approval (${assessment.severity}): ${assessment.reason}`,
|
|
228
|
+
reason: assessment.reason,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
return {
|
|
233
|
+
ok: false,
|
|
234
|
+
error: `Blocked dangerous command (${assessment.severity}): ${assessment.reason}`,
|
|
235
|
+
reason: assessment.reason,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// 3) Canonicalise + sensitive-path check for every path argument.
|
|
241
|
+
let realRoot;
|
|
242
|
+
for (const p of pathArgsForTool(tool, args)) {
|
|
243
|
+
try {
|
|
244
|
+
realRoot = realRoot ?? (0, path_utils_1.canonicalize)(ctx.root);
|
|
245
|
+
const target = (0, path_utils_1.safeResolve)(ctx.root, p, realRoot, { allowEqual: true });
|
|
246
|
+
if ((0, security_1.isSensitivePath)(target)) {
|
|
247
|
+
return {
|
|
248
|
+
ok: false,
|
|
249
|
+
error: `Sensitive path is blocked: ${p}`,
|
|
250
|
+
reason: 'sensitive path',
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
if (err instanceof path_utils_1.PathError) {
|
|
256
|
+
return { ok: false, error: err.message, reason: 'path validation failed' };
|
|
257
|
+
}
|
|
258
|
+
throw err;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return { ok: true };
|
|
262
|
+
}
|
|
263
|
+
/** Deep-mask secrets in a tool result so internal keys never leak out. */
|
|
264
|
+
function maskSecretsDeep(value) {
|
|
265
|
+
if (typeof value === 'string')
|
|
266
|
+
return (0, security_1.maskSecrets)(value);
|
|
267
|
+
if (Array.isArray(value))
|
|
268
|
+
return value.map((v) => maskSecretsDeep(v));
|
|
269
|
+
if (value && typeof value === 'object') {
|
|
270
|
+
const out = {};
|
|
271
|
+
for (const [k, v] of Object.entries(value)) {
|
|
272
|
+
out[k] = maskSecretsDeep(v);
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
return value;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* The guarded post-execution hook. Runs after every tool: masks secrets in the
|
|
280
|
+
* result and records the reason the invocation ended up here.
|
|
281
|
+
*/
|
|
282
|
+
function postExecute(_ctx, tool, result) {
|
|
283
|
+
if (result.result !== undefined) {
|
|
284
|
+
result.result = maskSecretsDeep(result.result);
|
|
285
|
+
}
|
|
286
|
+
if (typeof result.error === 'string') {
|
|
287
|
+
result.error = (0, security_1.maskSecrets)(result.error);
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
ok: true,
|
|
291
|
+
reason: `tool ${tool} completed; secrets masked`,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// executors
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
const listDir = async (ctx, args) => {
|
|
298
|
+
const p = optionalString(args, 'path', '.') ?? '.';
|
|
299
|
+
const depth = optionalInt(args, 'depth', 1, { min: 0, max: 10 });
|
|
300
|
+
const realRoot = (0, path_utils_1.canonicalize)(ctx.root);
|
|
301
|
+
const target = (0, path_utils_1.safeResolve)(ctx.root, p, realRoot, { allowEqual: true });
|
|
302
|
+
assertNotSensitive(target);
|
|
303
|
+
let stat;
|
|
304
|
+
try {
|
|
305
|
+
stat = fs.statSync(target);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
throw new ToolError('NOT_FOUND', `Path does not exist: ${p}`);
|
|
309
|
+
}
|
|
310
|
+
if (!stat.isDirectory()) {
|
|
311
|
+
throw new ToolError('NOT_A_DIR', `Path is not a directory: ${p}`);
|
|
312
|
+
}
|
|
313
|
+
const entries = [];
|
|
314
|
+
const walkDepth = (dir, cur) => {
|
|
315
|
+
if (cur >= depth)
|
|
316
|
+
return;
|
|
317
|
+
let names;
|
|
318
|
+
try {
|
|
319
|
+
names = fs.readdirSync(dir);
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
for (const name of names) {
|
|
325
|
+
const full = path.join(dir, name);
|
|
326
|
+
let st;
|
|
327
|
+
try {
|
|
328
|
+
st = fs.lstatSync(full);
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const type = st.isDirectory() ? 'dir' : st.isSymbolicLink() ? 'symlink' : 'file';
|
|
334
|
+
entries.push({
|
|
335
|
+
name,
|
|
336
|
+
path: rel(realRoot, full),
|
|
337
|
+
type,
|
|
338
|
+
...(st.isFile() ? { size: st.size } : {}),
|
|
339
|
+
mtime: st.mtimeMs,
|
|
340
|
+
});
|
|
341
|
+
if (st.isDirectory() && cur + 1 < depth) {
|
|
342
|
+
walkDepth(full, cur + 1);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
walkDepth(target, 0);
|
|
347
|
+
entries.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
348
|
+
return { ok: true, result: { path: rel(realRoot, target), depth, entries } };
|
|
349
|
+
};
|
|
350
|
+
const readFile = async (ctx, args, signal) => {
|
|
351
|
+
abortIfRequested(signal);
|
|
352
|
+
const targetStr = requireString(args, 'path');
|
|
353
|
+
const maxBytes = optionalInt(args, 'maxBytes', ctx.maxBytes, { min: 1, max: MAX_WRITE_BYTES });
|
|
354
|
+
const realRoot = (0, path_utils_1.canonicalize)(ctx.root);
|
|
355
|
+
const target = (0, path_utils_1.safeResolve)(ctx.root, targetStr, realRoot);
|
|
356
|
+
assertNotSensitive(target);
|
|
357
|
+
let stat;
|
|
358
|
+
try {
|
|
359
|
+
stat = fs.statSync(target);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
throw new ToolError('NOT_FOUND', `Path does not exist: ${targetStr}`);
|
|
363
|
+
}
|
|
364
|
+
if (stat.isDirectory()) {
|
|
365
|
+
throw new ToolError('IS_DIR', `Path is a directory, not a file: ${targetStr}`);
|
|
366
|
+
}
|
|
367
|
+
const totalBytes = stat.size;
|
|
368
|
+
const readLen = Math.min(maxBytes + 1, totalBytes);
|
|
369
|
+
const buf = readUpTo(target, readLen);
|
|
370
|
+
const truncated = totalBytes > maxBytes;
|
|
371
|
+
const bytesRead = buf.length;
|
|
372
|
+
const utf8 = isUtf8(buf);
|
|
373
|
+
const content = utf8 ? buf.toString('utf8') : buf.toString('hex');
|
|
374
|
+
return {
|
|
375
|
+
ok: true,
|
|
376
|
+
result: {
|
|
377
|
+
path: rel(realRoot, target),
|
|
378
|
+
bytesRead,
|
|
379
|
+
totalBytes,
|
|
380
|
+
truncated,
|
|
381
|
+
encoding: utf8 ? 'utf8' : 'hex',
|
|
382
|
+
content,
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
};
|
|
386
|
+
const writeFile = async (ctx, args, signal) => {
|
|
387
|
+
abortIfRequested(signal);
|
|
388
|
+
const targetStr = requireString(args, 'path');
|
|
389
|
+
const rawContent = args.content ?? args.contents;
|
|
390
|
+
const content = typeof rawContent === 'string' ? rawContent : '';
|
|
391
|
+
if (typeof rawContent !== 'string') {
|
|
392
|
+
throw new ToolError('INVALID_ARG', '"content" must be a string.');
|
|
393
|
+
}
|
|
394
|
+
const mode = optionalInt(args, 'mode', 0o644, { min: 0, max: 0o777 });
|
|
395
|
+
const realRoot = (0, path_utils_1.canonicalize)(ctx.root);
|
|
396
|
+
const target = (0, path_utils_1.safeResolve)(ctx.root, targetStr, realRoot);
|
|
397
|
+
assertNotRoot(realRoot, target);
|
|
398
|
+
assertNotSensitive(target);
|
|
399
|
+
const totalBytes = Buffer.byteLength(content);
|
|
400
|
+
if (totalBytes > MAX_WRITE_BYTES) {
|
|
401
|
+
throw new ToolError('TOO_LARGE', `Content exceeds ${MAX_WRITE_BYTES} bytes.`);
|
|
402
|
+
}
|
|
403
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
404
|
+
fs.writeFileSync(target, content, { mode });
|
|
405
|
+
return {
|
|
406
|
+
ok: true,
|
|
407
|
+
result: { path: rel(realRoot, target), bytesWritten: totalBytes, mode },
|
|
408
|
+
};
|
|
409
|
+
};
|
|
410
|
+
const editFile = async (ctx, args, signal) => {
|
|
411
|
+
abortIfRequested(signal);
|
|
412
|
+
const targetStr = requireString(args, 'path');
|
|
413
|
+
const oldText = requireString(args, 'oldText');
|
|
414
|
+
const newText = typeof args.newText === 'string' ? args.newText : '';
|
|
415
|
+
const realRoot = (0, path_utils_1.canonicalize)(ctx.root);
|
|
416
|
+
const target = (0, path_utils_1.safeResolve)(ctx.root, targetStr, realRoot);
|
|
417
|
+
assertNotRoot(realRoot, target);
|
|
418
|
+
assertNotSensitive(target);
|
|
419
|
+
let stat;
|
|
420
|
+
try {
|
|
421
|
+
stat = fs.statSync(target);
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
throw new ToolError('NOT_FOUND', `Path does not exist: ${targetStr}`);
|
|
425
|
+
}
|
|
426
|
+
if (stat.isDirectory()) {
|
|
427
|
+
throw new ToolError('IS_DIR', `Path is a directory, not a file: ${targetStr}`);
|
|
428
|
+
}
|
|
429
|
+
const buf = fs.readFileSync(target);
|
|
430
|
+
if (!isUtf8(buf)) {
|
|
431
|
+
throw new ToolError('BINARY_FILE', 'edit_file only supports UTF-8 text files.');
|
|
432
|
+
}
|
|
433
|
+
const text = buf.toString('utf8');
|
|
434
|
+
const count = text.split(oldText).length - 1;
|
|
435
|
+
if (count === 0) {
|
|
436
|
+
throw new ToolError('NO_MATCH', 'oldText was not found in the file.');
|
|
437
|
+
}
|
|
438
|
+
fs.writeFileSync(target, text.split(oldText).join(newText), 'utf8');
|
|
439
|
+
return { ok: true, result: { path: rel(realRoot, target), replacements: count } };
|
|
440
|
+
};
|
|
441
|
+
const runCommandTool = async (ctx, args, signal) => {
|
|
442
|
+
const cmd = requireString(args, 'cmd');
|
|
443
|
+
const cwdArg = optionalString(args, 'cwd', '.') ?? '.';
|
|
444
|
+
const timeout = optionalInt(args, 'timeout', ctx.timeoutMs, { min: 1 });
|
|
445
|
+
const realRoot = (0, path_utils_1.canonicalize)(ctx.root);
|
|
446
|
+
const cwd = (0, path_utils_1.safeResolve)(ctx.root, cwdArg, realRoot, { allowEqual: true });
|
|
447
|
+
const assessment = (0, security_1.isDangerCommand)(cmd);
|
|
448
|
+
if (assessment.danger && !permitsDangerCommand(ctx.permissionPreset, assessment.severity)) {
|
|
449
|
+
return {
|
|
450
|
+
ok: false,
|
|
451
|
+
error: `Blocked dangerous command (${assessment.severity}): ${assessment.reason}`,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
if (ctx.mode === 'supervise') {
|
|
455
|
+
return { ok: false, error: 'run_command requires supervision; denied in supervise mode.' };
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
const res = await (0, executor_1.runCommand)({
|
|
459
|
+
command: cmd,
|
|
460
|
+
cwd,
|
|
461
|
+
timeoutMs: timeout,
|
|
462
|
+
maxBytes: ctx.maxBytes,
|
|
463
|
+
signal,
|
|
464
|
+
});
|
|
465
|
+
return {
|
|
466
|
+
ok: true,
|
|
467
|
+
result: {
|
|
468
|
+
exitCode: res.exitCode,
|
|
469
|
+
stdout: (0, security_1.maskSecrets)(res.stdout),
|
|
470
|
+
stderr: (0, security_1.maskSecrets)(res.stderr),
|
|
471
|
+
durationMs: res.durationMs,
|
|
472
|
+
timedOut: res.timedOut,
|
|
473
|
+
truncated: res.truncated,
|
|
474
|
+
...(res.stdoutFile ? { stdoutFile: res.stdoutFile } : {}),
|
|
475
|
+
...(res.stderrFile ? { stderrFile: res.stderrFile } : {}),
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
catch (err) {
|
|
480
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
481
|
+
return { ok: false, error: `run_command failed: ${msg}` };
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
const webSearch = async (ctx, args) => {
|
|
485
|
+
const query = optionalString(args, 'query', '') ?? '';
|
|
486
|
+
const base = {
|
|
487
|
+
stub: true,
|
|
488
|
+
provider: 'platform',
|
|
489
|
+
// The real search runs platform-side; the local agent never touches a
|
|
490
|
+
// search backend, so it cannot leak anything.
|
|
491
|
+
note: 'Placeholder: real web search is performed by the platform, not the local agent.',
|
|
492
|
+
query,
|
|
493
|
+
results: [],
|
|
494
|
+
};
|
|
495
|
+
if (!ctx.webEnabled) {
|
|
496
|
+
return {
|
|
497
|
+
ok: true,
|
|
498
|
+
result: { ...base, enabled: false, note: 'web_search is disabled on this agent.' },
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
return { ok: true, result: { ...base, enabled: true } };
|
|
502
|
+
};
|
|
503
|
+
const git = async (ctx, args, signal) => {
|
|
504
|
+
const realRoot = (0, path_utils_1.canonicalize)(ctx.root);
|
|
505
|
+
const cwdArg = optionalString(args, 'cwd', '.') ?? '.';
|
|
506
|
+
const cwd = (0, path_utils_1.safeResolve)(ctx.root, cwdArg, realRoot, { allowEqual: true });
|
|
507
|
+
const timeout = optionalInt(args, 'timeout', ctx.timeoutMs, { min: 1 });
|
|
508
|
+
let cmdStr;
|
|
509
|
+
if (typeof args.command === 'string' && args.command.trim() !== '') {
|
|
510
|
+
cmdStr = `git ${args.command.trim()}`;
|
|
511
|
+
}
|
|
512
|
+
else if (Array.isArray(args.args)) {
|
|
513
|
+
const parts = requiredArray(args, 'args').map((a) => shellQuote(String(a)));
|
|
514
|
+
cmdStr = ['git', ...parts].join(' ');
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
throw new ToolError('INVALID_ARG', '"git" requires either "args" (array) or "command" (string).');
|
|
518
|
+
}
|
|
519
|
+
const assessment = (0, security_1.isDangerCommand)(cmdStr);
|
|
520
|
+
if (assessment.danger && !permitsDangerCommand(ctx.permissionPreset, assessment.severity)) {
|
|
521
|
+
return { ok: false, error: `Blocked dangerous git command (${assessment.severity}): ${assessment.reason}` };
|
|
522
|
+
}
|
|
523
|
+
if (ctx.mode === 'supervise') {
|
|
524
|
+
return { ok: false, error: 'git requires supervision; denied in supervise mode.' };
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
const res = await (0, executor_1.runCommand)({ command: cmdStr, cwd, timeoutMs: timeout, maxBytes: ctx.maxBytes, signal });
|
|
528
|
+
return {
|
|
529
|
+
ok: true,
|
|
530
|
+
result: {
|
|
531
|
+
exitCode: res.exitCode,
|
|
532
|
+
stdout: (0, security_1.maskSecrets)(res.stdout),
|
|
533
|
+
stderr: (0, security_1.maskSecrets)(res.stderr),
|
|
534
|
+
durationMs: res.durationMs,
|
|
535
|
+
timedOut: res.timedOut,
|
|
536
|
+
truncated: res.truncated,
|
|
537
|
+
...(res.stdoutFile ? { stdoutFile: res.stdoutFile } : {}),
|
|
538
|
+
...(res.stderrFile ? { stderrFile: res.stderrFile } : {}),
|
|
539
|
+
},
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
544
|
+
return { ok: false, error: `git failed: ${msg}` };
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
const TEMPLATES = {
|
|
548
|
+
node: [
|
|
549
|
+
{ path: 'package.json', content: JSON.stringify({ name: 'project', version: '0.0.1', main: 'index.js', scripts: { start: 'node index.js' } }, null, 2) + '\n' },
|
|
550
|
+
{ path: 'index.js', content: "console.log('hello from node');\n" },
|
|
551
|
+
{ path: 'README.md', content: '# Node project\n\nA generated skeleton.\n' },
|
|
552
|
+
],
|
|
553
|
+
react: [
|
|
554
|
+
{ path: 'index.html', content: '<!doctype html>\n<html><body><div id="root"></div><script src="./src/main.tsx"></script></body></html>\n' },
|
|
555
|
+
{ path: 'src/main.tsx', content: "import { createRoot } from 'react-dom/client';\ncreateRoot(document.getElementById('root')!).render(<h1>Hello React</h1>);\n" },
|
|
556
|
+
{ path: 'package.json', content: JSON.stringify({ name: 'project', version: '0.0.1', scripts: { dev: 'vite', build: 'vite build' } }, null, 2) + '\n' },
|
|
557
|
+
],
|
|
558
|
+
vue: [
|
|
559
|
+
{ path: 'src/App.vue', content: '<template><h1>Hello Vue</h1></template>\n<script setup>\n</script>\n' },
|
|
560
|
+
{ path: 'index.html', content: '<!doctype html>\n<html><body><div id="app"></div><script src="/src/main.ts"></script></body></html>\n' },
|
|
561
|
+
{ path: 'package.json', content: JSON.stringify({ name: 'project', version: '0.0.1', scripts: { dev: 'vite', build: 'vite build' } }, null, 2) + '\n' },
|
|
562
|
+
],
|
|
563
|
+
python: [
|
|
564
|
+
{ path: 'main.py', content: "def main():\n print('hello from python')\n\nif __name__ == '__main__':\n main()\n" },
|
|
565
|
+
{ path: 'requirements.txt', content: '# runtime dependencies\n' },
|
|
566
|
+
{ path: 'README.md', content: '# Python project\n\nA generated skeleton.\n' },
|
|
567
|
+
],
|
|
568
|
+
cli: [
|
|
569
|
+
{ path: 'cli.js', content: "#!/usr/bin/env node\nconsole.log('cli');\n" },
|
|
570
|
+
{ path: 'package.json', content: JSON.stringify({ name: 'project', version: '0.0.1', bin: { project: 'cli.js' } }, null, 2) + '\n' },
|
|
571
|
+
],
|
|
572
|
+
readme: [
|
|
573
|
+
{ path: 'README.md', content: '# {NAME}\n\nGenerated by ysagc-agent.\n' },
|
|
574
|
+
],
|
|
575
|
+
};
|
|
576
|
+
const generate = async (ctx, args, signal) => {
|
|
577
|
+
abortIfRequested(signal);
|
|
578
|
+
const template = (optionalString(args, 'template', 'node') ?? 'node').toLowerCase();
|
|
579
|
+
const name = requireString(args, 'name');
|
|
580
|
+
if (!/^[A-Za-z0-9._-]+$/.test(name)) {
|
|
581
|
+
throw new ToolError('INVALID_NAME', 'name may only contain letters, digits, ".", "_" and "-".');
|
|
582
|
+
}
|
|
583
|
+
const files = TEMPLATES[template];
|
|
584
|
+
if (!files) {
|
|
585
|
+
throw new ToolError('BAD_TEMPLATE', `Unknown template "${template}". Available: ${Object.keys(TEMPLATES).join(', ')}.`);
|
|
586
|
+
}
|
|
587
|
+
const realRoot = (0, path_utils_1.canonicalize)(ctx.root);
|
|
588
|
+
const targetDir = (0, path_utils_1.safeResolve)(ctx.root, name, realRoot);
|
|
589
|
+
assertNotSensitive(targetDir);
|
|
590
|
+
const written = [];
|
|
591
|
+
for (const tpl of files) {
|
|
592
|
+
const dest = (0, path_utils_1.safeResolve)(ctx.root, path.join(name, tpl.path), realRoot);
|
|
593
|
+
assertNotSensitive(dest);
|
|
594
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
595
|
+
const content = tpl.content.replace(/\{NAME\}/g, name);
|
|
596
|
+
const bytes = Buffer.byteLength(content);
|
|
597
|
+
fs.writeFileSync(dest, content, 'utf8');
|
|
598
|
+
written.push({ path: rel(realRoot, dest), bytes });
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
ok: true,
|
|
602
|
+
result: { template, name, root: rel(realRoot, targetDir), files: written },
|
|
603
|
+
};
|
|
604
|
+
};
|
|
605
|
+
// --- skills ----------------------------------------------------------------
|
|
606
|
+
/** Load a skill (`.dsh/skills/<name>.md` under the workspace root). No-op when
|
|
607
|
+
* no skills directory or no matching skill exists. */
|
|
608
|
+
const skillTool = async (ctx, args, signal) => {
|
|
609
|
+
abortIfRequested(signal);
|
|
610
|
+
const name = optionalString(args, 'name', '') ?? '';
|
|
611
|
+
if (name === '') {
|
|
612
|
+
const available = (0, skills_1.listSkills)(ctx.root);
|
|
613
|
+
const note = available.length === 0
|
|
614
|
+
? 'No skills are configured.'
|
|
615
|
+
: `Name required; available skills: ${available.join(', ')}.`;
|
|
616
|
+
return { ok: true, result: { loaded: false, available, note } };
|
|
617
|
+
}
|
|
618
|
+
const record = (0, skills_1.loadSkillRecord)(ctx.root, name);
|
|
619
|
+
return { ok: true, result: record };
|
|
620
|
+
};
|
|
621
|
+
const TOOLS = {
|
|
622
|
+
list_dir: listDir,
|
|
623
|
+
read_file: readFile,
|
|
624
|
+
write_file: writeFile,
|
|
625
|
+
edit_file: editFile,
|
|
626
|
+
run_command: runCommandTool,
|
|
627
|
+
web_search: webSearch,
|
|
628
|
+
git,
|
|
629
|
+
generate,
|
|
630
|
+
skill: skillTool,
|
|
631
|
+
};
|
|
632
|
+
/** Build a tool registry bound to a single execution context. */
|
|
633
|
+
function createToolRegistry(ctx) {
|
|
634
|
+
const names = Object.keys(TOOLS);
|
|
635
|
+
return {
|
|
636
|
+
ctx,
|
|
637
|
+
tools: names,
|
|
638
|
+
has(name) {
|
|
639
|
+
return Object.prototype.hasOwnProperty.call(TOOLS, name);
|
|
640
|
+
},
|
|
641
|
+
async dispatch(name, args, signal) {
|
|
642
|
+
const started = Date.now();
|
|
643
|
+
const executor = TOOLS[name];
|
|
644
|
+
if (!executor) {
|
|
645
|
+
return { ok: false, error: `Unknown tool: ${name}`, durationMs: 0 };
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
// Guarded gate: pre-execution check (permission preset, danger
|
|
649
|
+
// commands, canonicalise + sensitive-path).
|
|
650
|
+
const pre = preExecute(ctx, name, args ?? {});
|
|
651
|
+
if (!pre.ok) {
|
|
652
|
+
const durationMs = Date.now() - started;
|
|
653
|
+
return {
|
|
654
|
+
ok: false,
|
|
655
|
+
error: pre.error,
|
|
656
|
+
durationMs,
|
|
657
|
+
...(pre.needsApproval ? { needsApproval: true } : {}),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
// A cancel message may arrive while pre-checking; honour it.
|
|
661
|
+
abortIfRequested(signal);
|
|
662
|
+
const outcome = await executor(ctx, args ?? {}, signal);
|
|
663
|
+
const result = {
|
|
664
|
+
ok: outcome.ok,
|
|
665
|
+
result: outcome.result,
|
|
666
|
+
error: outcome.error,
|
|
667
|
+
durationMs: Date.now() - started,
|
|
668
|
+
...(outcome.needsApproval ? { needsApproval: true } : {}),
|
|
669
|
+
};
|
|
670
|
+
// Guarded gate: post-execution check (mask secrets + record reason).
|
|
671
|
+
const post = postExecute(ctx, name, result);
|
|
672
|
+
if (!post.ok) {
|
|
673
|
+
const durationMs = Date.now() - started;
|
|
674
|
+
return {
|
|
675
|
+
ok: false,
|
|
676
|
+
error: post.error ?? 'post-execution check failed',
|
|
677
|
+
durationMs,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
return result;
|
|
681
|
+
}
|
|
682
|
+
catch (err) {
|
|
683
|
+
const durationMs = Date.now() - started;
|
|
684
|
+
if (err instanceof path_utils_1.PathError || err instanceof ToolError) {
|
|
685
|
+
return { ok: false, error: err.message, durationMs };
|
|
686
|
+
}
|
|
687
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
688
|
+
ctx.logger.error('tool crashed', { tool: name, error: msg });
|
|
689
|
+
return { ok: false, error: `Internal error executing ${name}: ${msg}`, durationMs };
|
|
690
|
+
}
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
//# sourceMappingURL=tools.js.map
|