aegis-desktop 0.8.20 → 0.8.21
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/lib/local/engine.js +119 -24
- package/lib/local/local.js +293 -27
- package/lib/local/prompt.js +51 -2
- package/lib/local/tools.js +134 -23
- package/package.json +1 -1
- package/vendor/aegis.js +317 -33
- package/vendor/agent-gate.mjs +37 -5
- package/vendor/env-file.js +11 -4
- package/vendor/platform.js +259 -0
package/lib/local/tools.js
CHANGED
|
@@ -70,6 +70,25 @@ try {
|
|
|
70
70
|
|
|
71
71
|
const OUTPUT_CAP = 30_000; // chars fed back to the model per tool result
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* What every path argument accepts, stated ONCE so the ten schemas below cannot
|
|
75
|
+
* drift from each other or from what normalizeArgs actually does.
|
|
76
|
+
*
|
|
77
|
+
* It names the platform requirement explicitly because the failure it prevents
|
|
78
|
+
* was a model guessing posix on a Windows host (see client/platform.js
|
|
79
|
+
* resolvePath): `/home/you/Desktop/x.txt` there is drive-relative, so the write
|
|
80
|
+
* SUCCEEDED at `C:\home\you\Desktop\x.txt` and the user found nothing on their
|
|
81
|
+
* Desktop. The schema is the first place to say so — a refusal after the fact
|
|
82
|
+
* costs the model a round, a correct description costs it nothing.
|
|
83
|
+
*/
|
|
84
|
+
const PATH_ARG_DESC =
|
|
85
|
+
'Absolute path in THIS machine\'s own form (the Environment block names the platform and home ' +
|
|
86
|
+
'directory — a win32 host needs C:\\Users\\you\\file.txt, never /home/you/file.txt), or "~" / ' +
|
|
87
|
+
'"~/…" for the home directory. A relative path resolves against the working directory.';
|
|
88
|
+
|
|
89
|
+
/** `<what>. <the path rules>` — the shape every path argument's description takes. */
|
|
90
|
+
const pathDesc = (what) => `${what}. ${PATH_ARG_DESC}`;
|
|
91
|
+
|
|
73
92
|
// The dialect probe (Phase 30.3), from the same module the session lives in: a
|
|
74
93
|
// one-shot `exec` must run the command in a shell that was PROBED to speak the
|
|
75
94
|
// dialect the command is written in, never in an unprobed `$SHELL`. Lives next
|
|
@@ -111,11 +130,11 @@ const fail = (error) => ({ ok: false, error: cap(error) });
|
|
|
111
130
|
const SCHEMAS = {
|
|
112
131
|
readFile: {
|
|
113
132
|
name: 'readFile',
|
|
114
|
-
description: 'Read a file.
|
|
133
|
+
description: 'Read a file. Returns line-numbered content.',
|
|
115
134
|
parameters: {
|
|
116
135
|
type: 'object',
|
|
117
136
|
properties: {
|
|
118
|
-
file_path: { type: 'string', description: '
|
|
137
|
+
file_path: { type: 'string', description: pathDesc('The file to read') },
|
|
119
138
|
offset: { type: 'number', description: 'Start line (0-based)' },
|
|
120
139
|
limit: { type: 'number', description: `The number of lines to read (max 10000, default ${READ_LINE_CAP})` },
|
|
121
140
|
},
|
|
@@ -130,7 +149,7 @@ const SCHEMAS = {
|
|
|
130
149
|
parameters: {
|
|
131
150
|
type: 'object',
|
|
132
151
|
properties: {
|
|
133
|
-
file_path: { type: 'string', description: '
|
|
152
|
+
file_path: { type: 'string', description: pathDesc('The file to write') },
|
|
134
153
|
content: { type: 'string', description: 'Full file contents' },
|
|
135
154
|
},
|
|
136
155
|
required: ['file_path', 'content'],
|
|
@@ -145,7 +164,7 @@ const SCHEMAS = {
|
|
|
145
164
|
parameters: {
|
|
146
165
|
type: 'object',
|
|
147
166
|
properties: {
|
|
148
|
-
file_path: { type: 'string', description: '
|
|
167
|
+
file_path: { type: 'string', description: pathDesc('The file to edit') },
|
|
149
168
|
old_string: { type: 'string', description: 'Text to replace; must be unique unless replace_all' },
|
|
150
169
|
new_string: { type: 'string', description: 'Replacement text' },
|
|
151
170
|
replace_all: { type: 'boolean', description: 'Replace every occurrence' },
|
|
@@ -160,7 +179,7 @@ const SCHEMAS = {
|
|
|
160
179
|
parameters: {
|
|
161
180
|
type: 'object',
|
|
162
181
|
properties: {
|
|
163
|
-
path: { type: 'string', description: 'The directory to list (default: the working directory)' },
|
|
182
|
+
path: { type: 'string', description: pathDesc('The directory to list (default: the working directory)') },
|
|
164
183
|
},
|
|
165
184
|
required: [],
|
|
166
185
|
additionalProperties: false,
|
|
@@ -174,7 +193,7 @@ const SCHEMAS = {
|
|
|
174
193
|
type: 'object',
|
|
175
194
|
properties: {
|
|
176
195
|
pattern: { type: 'string', description: 'Glob, e.g. "**/*.test.js"' },
|
|
177
|
-
path: { type: 'string', description: 'Search root (default:
|
|
196
|
+
path: { type: 'string', description: pathDesc('Search root (default: the working directory)') },
|
|
178
197
|
},
|
|
179
198
|
required: ['pattern'],
|
|
180
199
|
additionalProperties: false,
|
|
@@ -187,7 +206,7 @@ const SCHEMAS = {
|
|
|
187
206
|
type: 'object',
|
|
188
207
|
properties: {
|
|
189
208
|
pattern: { type: 'string', description: 'The regular expression to search for' },
|
|
190
|
-
path: { type: 'string', description: 'The directory to search in (default: the working directory)' },
|
|
209
|
+
path: { type: 'string', description: pathDesc('The directory to search in (default: the working directory)') },
|
|
191
210
|
},
|
|
192
211
|
required: ['pattern'],
|
|
193
212
|
additionalProperties: false,
|
|
@@ -202,7 +221,7 @@ const SCHEMAS = {
|
|
|
202
221
|
type: 'object',
|
|
203
222
|
properties: {
|
|
204
223
|
command: { type: 'string', description: 'Command to run' },
|
|
205
|
-
cwd: { type: 'string', description: 'Run this one command elsewhere; session cwd unchanged' },
|
|
224
|
+
cwd: { type: 'string', description: pathDesc('Run this one command elsewhere; session cwd unchanged') },
|
|
206
225
|
timeout: { type: 'number', description: `Timeout in milliseconds (max ${EXEC_TIMEOUT_CAP}, default ${EXEC_TIMEOUT_DEFAULT})` },
|
|
207
226
|
description: { type: 'string', description: 'A brief description of what the command does (for display)' },
|
|
208
227
|
},
|
|
@@ -221,7 +240,7 @@ const SCHEMAS = {
|
|
|
221
240
|
type: 'object',
|
|
222
241
|
properties: {
|
|
223
242
|
sub: { type: 'string', enum: ['status', 'list'], description: 'status (default) or the full config listing' },
|
|
224
|
-
cwd: { type: 'string', description: 'Repo to inspect (default: the working directory)' },
|
|
243
|
+
cwd: { type: 'string', description: pathDesc('Repo to inspect (default: the working directory)') },
|
|
225
244
|
},
|
|
226
245
|
required: [],
|
|
227
246
|
additionalProperties: false,
|
|
@@ -237,7 +256,7 @@ const SCHEMAS = {
|
|
|
237
256
|
parameters: {
|
|
238
257
|
type: 'object',
|
|
239
258
|
properties: {
|
|
240
|
-
cwd: { type: 'string', description: 'Repo to install into (default: the working directory)' },
|
|
259
|
+
cwd: { type: 'string', description: pathDesc('Repo to install into (default: the working directory)') },
|
|
241
260
|
dry_run: { type: 'boolean', description: 'Print the commands instead of running them' },
|
|
242
261
|
},
|
|
243
262
|
required: [],
|
|
@@ -301,6 +320,85 @@ function toolsFor({ includeSubagent = true } = {}) {
|
|
|
301
320
|
return openaiTools({ includeSubagent });
|
|
302
321
|
}
|
|
303
322
|
|
|
323
|
+
// ── Path normalisation ──────────────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Which argument of each tool is a path on this machine. `task` has none (its
|
|
327
|
+
* argument is a prompt), and `exec`'s `command` is deliberately absent: a shell
|
|
328
|
+
* command is not a path and its contents are the shell's business, not ours.
|
|
329
|
+
*/
|
|
330
|
+
const TOOL_PATH_ARGS = Object.freeze({
|
|
331
|
+
readFile: ['file_path'],
|
|
332
|
+
writeFile: ['file_path'],
|
|
333
|
+
editFile: ['file_path'],
|
|
334
|
+
listDir: ['path'],
|
|
335
|
+
glob: ['path'],
|
|
336
|
+
grep: ['path'],
|
|
337
|
+
exec: ['cwd'],
|
|
338
|
+
hooks: ['cwd'],
|
|
339
|
+
installHooks: ['cwd'],
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Resolve every path argument of one tool call against THIS machine, returning
|
|
344
|
+
* `{ ok, args, error }` — `args` is a copy with each path replaced by its real
|
|
345
|
+
* absolute form, and a single refusal fails the whole call.
|
|
346
|
+
*
|
|
347
|
+
* Three reasons this is one function applied to the call, rather than a
|
|
348
|
+
* `resolvePath` sprinkled through each executor:
|
|
349
|
+
*
|
|
350
|
+
* 1. The GUARDS read the path too. tools/agent-gate.mjs does
|
|
351
|
+
* `path.resolve(cwd, args.file_path)` to decide whether the target sits in
|
|
352
|
+
* an agent-gated tree, and engine.js's covenant check inspects `args`
|
|
353
|
+
* before anything runs. A `~/…` reaching them unexpanded means they rule on
|
|
354
|
+
* `<cwd>/~/…` — a different file than the one that would be written. The
|
|
355
|
+
* only way the guard and the write can agree is for the path to be real
|
|
356
|
+
* BEFORE either looks at it.
|
|
357
|
+
* 2. The approval gate shows a diff for one path and then writes to another
|
|
358
|
+
* otherwise: previewMutation and applyChecked must resolve identically, and
|
|
359
|
+
* sharing this function is what makes that true by construction.
|
|
360
|
+
* 3. An absent or empty argument is left ALONE, so each executor keeps its own
|
|
361
|
+
* "required" error and its own default (listDir with no `path` still means
|
|
362
|
+
* the working directory). Normalising only what was actually supplied is
|
|
363
|
+
* what keeps this a path fix and not a behaviour change.
|
|
364
|
+
*
|
|
365
|
+
* Idempotent, because resolvePath is: engine.js normalises early for the guards
|
|
366
|
+
* and executeTool normalises again for a direct caller, and the second pass is a
|
|
367
|
+
* no-op rather than a second resolution.
|
|
368
|
+
*/
|
|
369
|
+
function normalizeArgs(name, args, ctx = {}) {
|
|
370
|
+
const input = args && typeof args === 'object' ? args : {};
|
|
371
|
+
const keys = TOOL_PATH_ARGS[name];
|
|
372
|
+
if (!keys) return { ok: true, args: input, error: null };
|
|
373
|
+
|
|
374
|
+
let out = input;
|
|
375
|
+
for (const key of keys) {
|
|
376
|
+
const value = input[key];
|
|
377
|
+
if (typeof value !== 'string' || !value.trim()) continue; // absent → the executor's default
|
|
378
|
+
const res = platform.resolvePath(value, { cwd: ctx && ctx.cwd ? ctx.cwd : undefined });
|
|
379
|
+
if (!res.ok) return { ok: false, args: input, error: `${name}: ${key} — ${res.error}` };
|
|
380
|
+
if (res.path !== value) {
|
|
381
|
+
if (out === input) out = { ...input };
|
|
382
|
+
out[key] = res.path;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return { ok: true, args: out, error: null };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* The directory a tool with no explicit `path` works in: this TURN's working
|
|
390
|
+
* directory, not the host process's.
|
|
391
|
+
*
|
|
392
|
+
* `process.cwd()` is wrong here and was visibly wrong off Linux: the Electron
|
|
393
|
+
* main process inherits whatever the OS launched the app from — `/` from a .desktop
|
|
394
|
+
* entry, `C:\Program Files\AEGIS Desktop` from a Windows shortcut — so a bare
|
|
395
|
+
* `listDir`/`glob`/`grep` searched the install directory instead of the user's
|
|
396
|
+
* repo. It stays the fallback for a direct call that supplies no ctx.
|
|
397
|
+
*/
|
|
398
|
+
function baseDir(ctx) {
|
|
399
|
+
return (ctx && typeof ctx.cwd === 'string' && ctx.cwd) || process.cwd();
|
|
400
|
+
}
|
|
401
|
+
|
|
304
402
|
// ── Executors ───────────────────────────────────────────────────────────────
|
|
305
403
|
|
|
306
404
|
function readFile({ file_path, offset = 0, limit } = {}) {
|
|
@@ -538,9 +636,11 @@ function previewEditFile({ file_path, old_string, new_string, replace_all } = {}
|
|
|
538
636
|
|
|
539
637
|
/** Dispatch a preview by tool name. Only writeFile/editFile have one — exec
|
|
540
638
|
* has nothing to diff, and the approval gate skips this call for it. */
|
|
541
|
-
function previewMutation(name, args) {
|
|
542
|
-
|
|
543
|
-
if (
|
|
639
|
+
function previewMutation(name, args, ctx = {}) {
|
|
640
|
+
const norm = normalizeArgs(name, args, ctx);
|
|
641
|
+
if (!norm.ok) return { ok: false, error: norm.error };
|
|
642
|
+
if (name === 'writeFile') return previewWriteFile(norm.args);
|
|
643
|
+
if (name === 'editFile') return previewEditFile(norm.args);
|
|
544
644
|
return { ok: false, error: `no diff preview for ${name}` };
|
|
545
645
|
}
|
|
546
646
|
|
|
@@ -591,16 +691,18 @@ function applyEditChecked({ file_path, after } = {}, expectedHash) {
|
|
|
591
691
|
/** Apply an approved writeFile/editFile call using the hash captured in its
|
|
592
692
|
* `preview` (see previewMutation) — the single entry point engine.js calls
|
|
593
693
|
* once the user has said yes. */
|
|
594
|
-
function applyChecked(name, args, preview) {
|
|
595
|
-
|
|
596
|
-
|
|
694
|
+
function applyChecked(name, args, preview, ctx = {}) {
|
|
695
|
+
const norm = normalizeArgs(name, args, ctx);
|
|
696
|
+
if (!norm.ok) return fail(norm.error);
|
|
697
|
+
if (name === 'writeFile') return applyWriteChecked(norm.args, preview.hash);
|
|
698
|
+
return applyEditChecked({ file_path: norm.args.file_path, after: preview.after }, preview.hash);
|
|
597
699
|
}
|
|
598
700
|
|
|
599
701
|
const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', '.aegiscode']);
|
|
600
702
|
|
|
601
|
-
function listDir({ path: dir } = {}) {
|
|
703
|
+
function listDir({ path: dir } = {}, ctx = {}) {
|
|
602
704
|
try {
|
|
603
|
-
const base = dir ||
|
|
705
|
+
const base = dir || baseDir(ctx);
|
|
604
706
|
const entries = fs.readdirSync(base, { withFileTypes: true });
|
|
605
707
|
const rows = entries
|
|
606
708
|
.filter((e) => !IGNORED_DIRS.has(e.name))
|
|
@@ -645,10 +747,10 @@ function walk(dir, fn, depth = 0) {
|
|
|
645
747
|
}
|
|
646
748
|
}
|
|
647
749
|
|
|
648
|
-
function glob({ pattern, path: root } = {}) {
|
|
750
|
+
function glob({ pattern, path: root } = {}, ctx = {}) {
|
|
649
751
|
try {
|
|
650
752
|
if (!pattern) return fail('pattern is required');
|
|
651
|
-
const base = root ||
|
|
753
|
+
const base = root || baseDir(ctx);
|
|
652
754
|
const re = globToRegex(String(pattern));
|
|
653
755
|
const hits = [];
|
|
654
756
|
walk(base, (full) => {
|
|
@@ -662,11 +764,11 @@ function glob({ pattern, path: root } = {}) {
|
|
|
662
764
|
}
|
|
663
765
|
}
|
|
664
766
|
|
|
665
|
-
function grep({ pattern, path: root } = {}) {
|
|
767
|
+
function grep({ pattern, path: root } = {}, ctx = {}) {
|
|
666
768
|
try {
|
|
667
769
|
if (!pattern) return fail('pattern is required');
|
|
668
770
|
const re = new RegExp(pattern);
|
|
669
|
-
const base = root ||
|
|
771
|
+
const base = root || baseDir(ctx);
|
|
670
772
|
const hits = [];
|
|
671
773
|
walk(base, (full) => {
|
|
672
774
|
if (hits.length >= MATCH_CAP) return;
|
|
@@ -990,7 +1092,12 @@ function nearestManifest(start) {
|
|
|
990
1092
|
|
|
991
1093
|
async function executeTool(name, args, ctx) {
|
|
992
1094
|
if (!isTool(name)) return fail(`unknown tool "${name}" (known: ${toolNames().join(', ')})`);
|
|
993
|
-
|
|
1095
|
+
// Paths become real BEFORE the agent-key gate below resolves them itself —
|
|
1096
|
+
// see normalizeArgs. A `~/…` that reached the gate unexpanded was ruled on as
|
|
1097
|
+
// `<cwd>/~/…`, i.e. a different file than the one about to be written.
|
|
1098
|
+
const norm = normalizeArgs(name, args, ctx || {});
|
|
1099
|
+
if (!norm.ok) return fail(norm.error);
|
|
1100
|
+
const input = norm.args;
|
|
994
1101
|
// ── agent-key gate (tools/agent-gate.mjs) ──────────────────────────────
|
|
995
1102
|
// The in-process half of the lock whose other half is .githooks/. A tree
|
|
996
1103
|
// that declares .aegis/guard.json must not be mutated through this app
|
|
@@ -1060,6 +1167,10 @@ module.exports = {
|
|
|
1060
1167
|
MUTATING_TOOLS,
|
|
1061
1168
|
previewMutation,
|
|
1062
1169
|
applyChecked,
|
|
1170
|
+
// path handling (client/platform.js resolvePath applied to a tool call)
|
|
1171
|
+
normalizeArgs,
|
|
1172
|
+
TOOL_PATH_ARGS,
|
|
1173
|
+
PATH_ARG_DESC,
|
|
1063
1174
|
// limits (unit tests assert against them instead of hard-coding numbers)
|
|
1064
1175
|
OUTPUT_CAP,
|
|
1065
1176
|
READ_LINE_CAP,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegis-desktop",
|
|
3
3
|
"productName": "AEGIS Desktop",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.21",
|
|
5
5
|
"description": "Thin Electron host for AEGIS — a local chat UI over the shared client/aegis.js transport. Ships transport + UI only; engine logic stays server-side.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "AEGIS Code",
|