omniharness-cli 0.1.60 → 0.1.61
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/dist/tools/systemTools.js +43 -7
- package/package.json +1 -1
|
@@ -6,12 +6,48 @@ import { z } from 'zod';
|
|
|
6
6
|
const execFileAsync = promisify(execFile);
|
|
7
7
|
const MAX_OUTPUT = 32_000;
|
|
8
8
|
const relativePath = z.string().min(1).refine((value) => !path.isAbsolute(value) && value !== '..' && !value.startsWith(`..${path.sep}`));
|
|
9
|
-
function
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
function escapes(root, target) {
|
|
10
|
+
const relative = path.relative(root, target);
|
|
11
|
+
return relative.startsWith('..') || path.isAbsolute(relative);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Resolve symlinks as far down the path as actually exists, then rejoin the
|
|
15
|
+
* components that do not. A path being created cannot be resolved outright, and
|
|
16
|
+
* it is exactly the one confinement has to judge before the write happens.
|
|
17
|
+
*/
|
|
18
|
+
async function resolveDeepest(target) {
|
|
19
|
+
const trailing = [];
|
|
20
|
+
let current = target;
|
|
21
|
+
for (;;) {
|
|
22
|
+
try {
|
|
23
|
+
return path.join(await fs.realpath(current), ...trailing);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
const parent = path.dirname(current);
|
|
27
|
+
if (parent === current)
|
|
28
|
+
return target; // reached the root, nothing resolved
|
|
29
|
+
trailing.unshift(path.basename(current));
|
|
30
|
+
current = parent;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Confine a path to the workspace, lexically and through symlinks.
|
|
36
|
+
*
|
|
37
|
+
* The lexical check alone is not confinement: a symlink inside the workspace
|
|
38
|
+
* pointing out of it satisfies every string comparison while resolving
|
|
39
|
+
* somewhere else entirely, and repositories carry symlinks routinely — the
|
|
40
|
+
* agent does not have to create one. Both the read and the write path were
|
|
41
|
+
* escaping through them.
|
|
42
|
+
*/
|
|
43
|
+
async function within(root, target) {
|
|
44
|
+
const absoluteRoot = await resolveDeepest(path.resolve(root));
|
|
45
|
+
const absoluteTarget = path.resolve(absoluteRoot, target);
|
|
46
|
+
if (escapes(absoluteRoot, absoluteTarget))
|
|
14
47
|
throw new Error('path escapes workspace root');
|
|
48
|
+
if (escapes(absoluteRoot, await resolveDeepest(absoluteTarget))) {
|
|
49
|
+
throw new Error('path escapes workspace root through a symlink');
|
|
50
|
+
}
|
|
15
51
|
return absoluteTarget;
|
|
16
52
|
}
|
|
17
53
|
export function createSystemTools(workspaceRoot, shellAllowed = false) {
|
|
@@ -20,7 +56,7 @@ export function createSystemTools(workspaceRoot, shellAllowed = false) {
|
|
|
20
56
|
name: 'read_file', description: 'Read a UTF-8 file inside the workspace.', risk: 'low', inputSchema: { path: 'relative file path' },
|
|
21
57
|
async execute(input) {
|
|
22
58
|
const parsed = relativePath.parse(input.path);
|
|
23
|
-
const target = within(root, parsed);
|
|
59
|
+
const target = await within(root, parsed);
|
|
24
60
|
return { path: parsed, content: await fs.readFile(target, 'utf8') };
|
|
25
61
|
},
|
|
26
62
|
};
|
|
@@ -28,7 +64,7 @@ export function createSystemTools(workspaceRoot, shellAllowed = false) {
|
|
|
28
64
|
name: 'write_file', description: 'Write a UTF-8 file inside the workspace.', risk: 'high', inputSchema: { path: 'relative file path', content: 'file contents' },
|
|
29
65
|
async execute(input) {
|
|
30
66
|
const parsed = relativePath.parse(input.path);
|
|
31
|
-
const target = within(root, parsed);
|
|
67
|
+
const target = await within(root, parsed);
|
|
32
68
|
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
33
69
|
await fs.writeFile(target, input.content, 'utf8');
|
|
34
70
|
return { path: parsed, bytes: Buffer.byteLength(input.content) };
|