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.
@@ -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 within(root, target) {
10
- const absoluteRoot = path.resolve(root);
11
- const absoluteTarget = path.resolve(root, target);
12
- const relative = path.relative(absoluteRoot, absoluteTarget);
13
- if (relative.startsWith('..') || path.isAbsolute(relative))
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) };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omniharness-cli",
3
- "version": "0.1.60",
3
+ "version": "0.1.61",
4
4
  "description": "OmniHarness — local-first agent orchestration harness for OmniRoute.",
5
5
  "license": "MIT",
6
6
  "type": "module",