pi-better-subagents 0.4.0 → 0.4.1
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/package.json +1 -1
- package/shared-sandbox-core.ts +132 -13
- package/shared-task-sandbox.ts +41 -14
- package/task-guard.ts +1 -1
- package/task-policy.ts +2 -2
package/package.json
CHANGED
package/shared-sandbox-core.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* `SandboxSeams` argument so callers can plan deterministically in tests.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import { spawnSync } from "node:child_process";
|
|
21
22
|
import { platform as osPlatform } from "node:os";
|
|
22
23
|
import {
|
|
23
24
|
accessSync,
|
|
@@ -67,6 +68,8 @@ export type SandboxWritePolicy = {
|
|
|
67
68
|
permissions?: SandboxPermissions;
|
|
68
69
|
/** Trusted per-launch runtime state, never supplied by model tool arguments. */
|
|
69
70
|
runtimeWrite?: readonly string[];
|
|
71
|
+
/** Bounded historical default runtime directories; inactive without a capability profile. */
|
|
72
|
+
runtimeCompatibility?: boolean;
|
|
70
73
|
};
|
|
71
74
|
|
|
72
75
|
/** The executable and argv to run inside the sandbox, preserved verbatim. */
|
|
@@ -116,6 +119,8 @@ export type SandboxSeams = {
|
|
|
116
119
|
* host running them.
|
|
117
120
|
*/
|
|
118
121
|
materializeDenyPath?: (path: string) => boolean;
|
|
122
|
+
/** Defaults to /usr/bin/getconf with a minimal environment (macOS only). */
|
|
123
|
+
getconf?: (name: "DARWIN_USER_TEMP_DIR" | "DARWIN_USER_CACHE_DIR") => string | undefined;
|
|
119
124
|
};
|
|
120
125
|
|
|
121
126
|
/** A policy with every path canonicalized, deduplicated, and ordered. */
|
|
@@ -126,6 +131,7 @@ export type CompiledSandboxWritePolicy = {
|
|
|
126
131
|
readonly permissions?: SandboxPermissions;
|
|
127
132
|
readonly credentialPaths?: readonly string[];
|
|
128
133
|
readonly runtimeWrite?: readonly string[];
|
|
134
|
+
readonly compatibilityWrite?: readonly string[];
|
|
129
135
|
};
|
|
130
136
|
|
|
131
137
|
/** Why a write target is or is not permitted by a compiled policy. */
|
|
@@ -176,6 +182,46 @@ const RUNTIME_ROOTS = ["/usr", "/bin", "/sbin", "/lib", "/lib64", "/System/Libra
|
|
|
176
182
|
const TEMP_ROOTS = ["/private/var/folders", "/private/tmp", "/tmp", "/dev"];
|
|
177
183
|
const READ_RUNTIME_ROOTS = [...RUNTIME_ROOTS, "/dev"];
|
|
178
184
|
|
|
185
|
+
function systemGetconf(name: "DARWIN_USER_TEMP_DIR" | "DARWIN_USER_CACHE_DIR"): string | undefined {
|
|
186
|
+
const result = spawnSync("/usr/bin/getconf", [name], {
|
|
187
|
+
encoding: "utf8", env: { PATH: "/usr/bin:/bin", LANG: "C" }, timeout: 3000, maxBuffer: 4096,
|
|
188
|
+
});
|
|
189
|
+
return result.status === 0 ? result.stdout.trim() : undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function compatibilityPaths(policy: SandboxWritePolicy, seams: SandboxSeams): string[] {
|
|
193
|
+
if (!policy.permissions || !policy.runtimeCompatibility || policy.permissions.outsideProject === "off") return [];
|
|
194
|
+
const platform = currentPlatform(seams);
|
|
195
|
+
if (platform === "linux") return [canonicalizePath("/tmp", seams)];
|
|
196
|
+
if (platform !== "darwin") return [];
|
|
197
|
+
const home = canonicalizePath(policy.home, seams);
|
|
198
|
+
const project = canonicalizePath(policy.writableRoot, seams);
|
|
199
|
+
const paths = [canonicalizePath("/tmp", seams)];
|
|
200
|
+
for (const [key, leaf] of [["DARWIN_USER_TEMP_DIR", ""], ["DARWIN_USER_CACHE_DIR", "mds"]] as const) {
|
|
201
|
+
const raw = (seams.getconf ?? systemGetconf)(key);
|
|
202
|
+
if (!raw || !raw.startsWith("/") || raw.includes("\0") ||
|
|
203
|
+
raw.split("/").some((part) => part === "." || part === "..")) {
|
|
204
|
+
throw new Error(`Invalid ${key} runtime directory`);
|
|
205
|
+
}
|
|
206
|
+
// getconf returns /var/folders on macOS; /var is a system alias for /private/var.
|
|
207
|
+
const normalized = resolve(raw.replace(/^\/var\/folders\//, "/private/var/folders/"));
|
|
208
|
+
const expected = /^\/private\/var\/folders\/[^/]+\/[^/]+\/[TC]$/.test(normalized) &&
|
|
209
|
+
normalized.endsWith(key === "DARWIN_USER_TEMP_DIR" ? "/T" : "/C");
|
|
210
|
+
const canonical = canonicalizePath(normalized, seams);
|
|
211
|
+
if (!expected || canonical !== normalized ||
|
|
212
|
+
(contains(home, canonical) || contains(canonical, home) || contains(project, canonical))) {
|
|
213
|
+
throw new Error(`Unsafe ${key} runtime directory`);
|
|
214
|
+
}
|
|
215
|
+
paths.push(leaf ? join(canonical, leaf) : canonical);
|
|
216
|
+
}
|
|
217
|
+
// An existing MDS symlink must not redirect the narrow write allowance.
|
|
218
|
+
const mds = paths[2];
|
|
219
|
+
if (!mds || canonicalizePath(mds, seams) !== mds || contains(mds, project) || contains(project, mds)) {
|
|
220
|
+
throw new Error("Unsafe MDS runtime directory");
|
|
221
|
+
}
|
|
222
|
+
return [...new Set(paths)];
|
|
223
|
+
}
|
|
224
|
+
|
|
179
225
|
/** Only known on-disk credentials: keychains, services and inherited env tokens are out of scope. */
|
|
180
226
|
export function credentialFilePaths(home: string, seams: SandboxSeams = {}): string[] {
|
|
181
227
|
const paths = CREDENTIAL_LOCATIONS.map((name) => join(home, name));
|
|
@@ -235,12 +281,17 @@ function compile(
|
|
|
235
281
|
...new Set((policy.denyWrite ?? []).map((entry) => canonicalizePath(entry, seams))),
|
|
236
282
|
].sort();
|
|
237
283
|
|
|
284
|
+
const compatibilityWrite = compatibilityPaths(policy, seams);
|
|
238
285
|
return {
|
|
239
286
|
writableRoot, denyWrite, home: policy.home,
|
|
240
287
|
...(policy.permissions && {
|
|
241
288
|
permissions: { ...policy.permissions },
|
|
242
289
|
credentialPaths: credentialFilePaths(policy.home, seams),
|
|
243
|
-
|
|
290
|
+
compatibilityWrite,
|
|
291
|
+
runtimeWrite: [...new Set([
|
|
292
|
+
...(policy.runtimeWrite ?? []).map((path) => canonicalizePath(path, seams)),
|
|
293
|
+
...compatibilityWrite,
|
|
294
|
+
])],
|
|
244
295
|
}),
|
|
245
296
|
};
|
|
246
297
|
}
|
|
@@ -279,8 +330,9 @@ export function evaluateReadAccess(
|
|
|
279
330
|
const permissions = policy.permissions;
|
|
280
331
|
if (!permissions) return { allowed: true, path };
|
|
281
332
|
const mode = isCredential(path, policy) ? permissions.storedCredentials
|
|
282
|
-
: policy.runtimeWrite?.some((root) => contains(root, path)) ? "read-write"
|
|
333
|
+
: policy.runtimeWrite?.some((root) => !policy.compatibilityWrite?.includes(root) && contains(root, path)) ? "read-write"
|
|
283
334
|
: contains(policy.writableRoot, path) ? permissions.projectFiles
|
|
335
|
+
: policy.compatibilityWrite?.some((root) => contains(root, path)) ? "read-write"
|
|
284
336
|
: path === sep || runtimeRoots(seams).some((root) => contains(root, path)) ? "read"
|
|
285
337
|
: permissions.outsideProject;
|
|
286
338
|
return mode === "off" ? { allowed: false, path, reason: "read-denied" } : { allowed: true, path };
|
|
@@ -307,8 +359,9 @@ export function evaluateWriteAccess(
|
|
|
307
359
|
}
|
|
308
360
|
if (policy.permissions) {
|
|
309
361
|
const mode = isCredential(path, policy) ? policy.permissions.storedCredentials
|
|
310
|
-
: policy.runtimeWrite?.some((root) => contains(root, path)) ? "read-write"
|
|
362
|
+
: policy.runtimeWrite?.some((root) => !policy.compatibilityWrite?.includes(root) && contains(root, path)) ? "read-write"
|
|
311
363
|
: contains(policy.writableRoot, path) ? policy.permissions.projectFiles
|
|
364
|
+
: policy.compatibilityWrite?.some((root) => contains(root, path)) ? "read-write"
|
|
312
365
|
: contains(canonicalizePath("/dev", seams), path) ? "read-write"
|
|
313
366
|
: policy.permissions.outsideProject;
|
|
314
367
|
if (mode !== "read-write") return { allowed: false, path, reason: "permission-denied" };
|
|
@@ -344,6 +397,7 @@ function buildPermissionProfile(policy: CompiledSandboxWritePolicy, seams: Sandb
|
|
|
344
397
|
...policy.denyWrite,
|
|
345
398
|
...(permissions.projectFiles !== "read-write" ? [policy.writableRoot] : []),
|
|
346
399
|
...(permissions.storedCredentials !== "read-write" ? policy.credentialPaths ?? [] : []),
|
|
400
|
+
...(policy.compatibilityWrite ?? []),
|
|
347
401
|
];
|
|
348
402
|
const rules = ["(version 1)", "(allow default)", "(deny file-write*)"];
|
|
349
403
|
if (permissions.outsideProject === "off") {
|
|
@@ -367,13 +421,15 @@ function buildPermissionProfile(policy: CompiledSandboxWritePolicy, seams: Sandb
|
|
|
367
421
|
};
|
|
368
422
|
// Last matching SBPL rule wins. Credential rules override project and outside;
|
|
369
423
|
// explicit denyWrite entries always override every write allowance.
|
|
424
|
+
for (const path of policy.compatibilityWrite ?? []) scoped(path, "read-write");
|
|
370
425
|
scoped(policy.writableRoot, permissions.projectFiles);
|
|
371
|
-
for (const path of policy.runtimeWrite ?? []) scoped(path, "read-write");
|
|
426
|
+
for (const path of (policy.runtimeWrite ?? []).filter((path) => !policy.compatibilityWrite?.includes(path))) scoped(path, "read-write");
|
|
372
427
|
for (const path of policy.credentialPaths ?? []) scoped(path, permissions.storedCredentials);
|
|
373
428
|
for (const path of policy.denyWrite) rules.push(`(deny file-write* (subpath ${sbpl(path)}))`);
|
|
374
429
|
// Protect the directory entries, not their contents: unrelated children can
|
|
375
430
|
// still be created, while renaming a parent cannot move a denied subtree.
|
|
376
431
|
for (const path of protectedAncestors(protectedPaths)) rules.push(`(deny file-write-unlink (literal ${sbpl(path)}))`);
|
|
432
|
+
for (const path of policy.compatibilityWrite ?? []) rules.push(`(deny file-write-unlink (literal ${sbpl(path)}))`);
|
|
377
433
|
if (!permissions.network) rules.push("(deny network*)");
|
|
378
434
|
return [...rules, ""].join("\n");
|
|
379
435
|
}
|
|
@@ -615,33 +671,96 @@ function buildLinuxPermissionCommand(
|
|
|
615
671
|
}
|
|
616
672
|
}
|
|
617
673
|
} else {
|
|
618
|
-
mounts.push(permissions.outsideProject === "read" ? "--ro-bind" : "--bind", "/tmp", "/tmp");
|
|
674
|
+
mounts.push(permissions.outsideProject === "read" && !policy.compatibilityWrite?.includes(canonicalizePath("/tmp", seams)) ? "--ro-bind" : "--bind", "/tmp", "/tmp");
|
|
619
675
|
}
|
|
620
676
|
mounts.push("--dev", "/dev");
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
677
|
+
const scopedMounts: { option: "--bind" | "--ro-bind"; path: string }[] = [];
|
|
678
|
+
const writableAncestors = new Set<string>();
|
|
679
|
+
const readOnlyGuards = new Set<string>();
|
|
624
680
|
if (permissions.outsideProject === "off" && permissions.storedCredentials === "read") {
|
|
625
681
|
for (const path of credentials) {
|
|
626
|
-
if (existsSync(path) && !contains(project, path))
|
|
682
|
+
if (existsSync(path) && !contains(project, path)) readOnlyGuards.add(path);
|
|
627
683
|
}
|
|
628
684
|
}
|
|
685
|
+
// A broad /tmp bind is already present for visible outside roots. Cover
|
|
686
|
+
// existing leaves and absent leaves via a read-only existing ancestor; do
|
|
687
|
+
// not create credential or control files in host runtime directories.
|
|
688
|
+
const compatibility = policy.compatibilityWrite ?? [];
|
|
689
|
+
if (compatibility.length) {
|
|
690
|
+
const tmp = canonicalizePath("/tmp", seams);
|
|
691
|
+
const protectedInTmp = [...policy.denyWrite, ...(permissions.storedCredentials !== "read-write" ? credentials : [])]
|
|
692
|
+
.filter((path) => contains(tmp, path) && !contains(project, path));
|
|
693
|
+
if (permissions.storedCredentials === "off" && protectedInTmp.some((path) =>
|
|
694
|
+
credentials.some((credential) => credential === path))) {
|
|
695
|
+
throw new Error("Linux bubblewrap cannot hide credentials under a writable runtime directory.");
|
|
696
|
+
}
|
|
697
|
+
if (policy.denyWrite.some((path) => contains(path, tmp))) {
|
|
698
|
+
throw new Error("Runtime directory overlaps a write-denied control path.");
|
|
699
|
+
}
|
|
700
|
+
const guards = new Set<string>();
|
|
701
|
+
for (const path of protectedInTmp) {
|
|
702
|
+
let guard = path;
|
|
703
|
+
while (!existsSync(guard) && contains(tmp, guard) && guard !== tmp) guard = dirname(guard);
|
|
704
|
+
if (guard === tmp || !contains(tmp, guard)) {
|
|
705
|
+
throw new Error("Cannot protect an absent path directly under writable runtime directory.");
|
|
706
|
+
}
|
|
707
|
+
guards.add(guard);
|
|
708
|
+
}
|
|
709
|
+
for (const guard of [...guards].sort((a, b) => a.length - b.length)) {
|
|
710
|
+
if ([...guards].some((parent) => parent !== guard && contains(parent, guard))) continue;
|
|
711
|
+
for (const parent of protectedAncestors([guard]).filter((parent) => contains(tmp, parent) && parent !== tmp)) {
|
|
712
|
+
writableAncestors.add(parent);
|
|
713
|
+
}
|
|
714
|
+
readOnlyGuards.add(guard);
|
|
715
|
+
}
|
|
716
|
+
// A writable /tmp can rename any directory above the captured project
|
|
717
|
+
// root, then substitute a symlink before the next launch. Anchor each
|
|
718
|
+
// ancestor even when there are no explicit control or credential guards.
|
|
719
|
+
if (permissions.projectFiles !== "off" && contains(tmp, project)) {
|
|
720
|
+
for (const parent of protectedAncestors([project]).filter((path) => contains(tmp, path) && path !== tmp)) {
|
|
721
|
+
writableAncestors.add(parent);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (permissions.projectFiles !== "off") {
|
|
726
|
+
scopedMounts.push({ option: writableProject ? "--bind" : "--ro-bind", path: project });
|
|
727
|
+
}
|
|
629
728
|
for (const path of policy.runtimeWrite ?? []) {
|
|
729
|
+
if (compatibility.includes(path)) continue;
|
|
630
730
|
if (credentials.some((protectedPath) => contains(path, protectedPath) || contains(protectedPath, path)) ||
|
|
631
731
|
policy.denyWrite.some((protectedPath) => contains(protectedPath, path))) {
|
|
632
732
|
throw new Error("Runtime directory overlaps protected credentials or control paths.");
|
|
633
733
|
}
|
|
634
|
-
|
|
734
|
+
scopedMounts.push({ option: "--bind", path });
|
|
635
735
|
}
|
|
636
736
|
const protectedPaths = [...policy.denyWrite,
|
|
637
737
|
...(permissions.storedCredentials !== "read-write" ? overlappingCredentials : [])];
|
|
638
|
-
for (const writableRoot of [...(writableProject ? [project] : []),
|
|
738
|
+
for (const writableRoot of [...(writableProject ? [project] : []),
|
|
739
|
+
...(policy.runtimeWrite ?? []).filter((path) => !compatibility.includes(path))]) {
|
|
639
740
|
const materialize = seams.materializeDenyPath ?? materializeDenyPath;
|
|
640
741
|
const leaves = protectedPaths.filter((path) => contains(writableRoot, path) && materialize(path));
|
|
641
742
|
for (const parent of protectedAncestors(leaves).filter((path) => contains(writableRoot, path) && path !== writableRoot)) {
|
|
642
|
-
|
|
743
|
+
writableAncestors.add(parent);
|
|
643
744
|
}
|
|
644
|
-
for (const path of leaves)
|
|
745
|
+
for (const path of leaves) readOnlyGuards.add(path);
|
|
746
|
+
}
|
|
747
|
+
// A nearest-existing guard can be an ancestor of an unrelated writable
|
|
748
|
+
// project (e.g. absent ~/.npmrc with a project under ~/work). Mount that
|
|
749
|
+
// broad guard in path order, keeping its intervening anchors read-only;
|
|
750
|
+
// only the disjoint project subtree may be rebound writable afterwards.
|
|
751
|
+
const ancestorGuards = [...readOnlyGuards].filter((guard) =>
|
|
752
|
+
scopedMounts.some(({ option, path }) => option === "--bind" && guard !== path && contains(guard, path)));
|
|
753
|
+
for (const guard of ancestorGuards) {
|
|
754
|
+
scopedMounts.push({ option: "--ro-bind", path: guard });
|
|
755
|
+
readOnlyGuards.delete(guard);
|
|
756
|
+
}
|
|
757
|
+
for (const path of writableAncestors) scopedMounts.push({
|
|
758
|
+
option: ancestorGuards.some((guard) => contains(guard, path)) ? "--ro-bind" : "--bind", path,
|
|
759
|
+
});
|
|
760
|
+
scopedMounts.sort((a, b) => a.path.length - b.path.length);
|
|
761
|
+
for (const { option, path } of scopedMounts) mounts.push(option, path, path);
|
|
762
|
+
for (const path of [...readOnlyGuards].sort((a, b) => a.length - b.length)) {
|
|
763
|
+
mounts.push("--ro-bind", path, path);
|
|
645
764
|
}
|
|
646
765
|
return {
|
|
647
766
|
file: bwrap,
|
package/shared-task-sandbox.ts
CHANGED
|
@@ -6,10 +6,10 @@ const { createBashToolDefinition, createReadToolDefinition, createWriteToolDefin
|
|
|
6
6
|
createEditToolDefinition, createLocalBashOperations, getShellConfig } = PiCodingAgent;
|
|
7
7
|
import { pathToFileURL } from "node:url";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
|
-
import { accessSync, constants, lstatSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
10
|
-
import { tmpdir } from "node:os";
|
|
11
|
-
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
12
|
-
import { canonicalizePath, maybeBuildSandboxCommand } from "./shared-sandbox-core.ts";
|
|
9
|
+
import { accessSync, constants, lstatSync, mkdirSync, mkdtempSync, readlinkSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { homedir, tmpdir } from "node:os";
|
|
11
|
+
import { basename, dirname, isAbsolute, join, parse, resolve, sep } from "node:path";
|
|
12
|
+
import { canonicalizePath, compileWritePolicy, maybeBuildSandboxCommand, type SandboxPermissions } from "./shared-sandbox-core.ts";
|
|
13
13
|
import { createTaskFileOperations, type TaskFileController } from "./shared-task-files.ts";
|
|
14
14
|
|
|
15
15
|
export const TASK_BUILTINS = Object.freeze(["read", "write", "edit", "bash"] as const);
|
|
@@ -27,16 +27,43 @@ export function runtimeCodeRoot(path: string): string {
|
|
|
27
27
|
|
|
28
28
|
export function writableRuntimeAlias(path: string, root: string, permissions: {
|
|
29
29
|
projectFiles: string; outsideProject: string; storedCredentials: string;
|
|
30
|
-
}): string | undefined {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
}, runtimeCompatibility = false): string | undefined {
|
|
31
|
+
const compatibility = runtimeCompatibility ? compileWritePolicy({ writableRoot: root, home: homedir(),
|
|
32
|
+
permissions: { ...permissions, commands: true, network: true } as SandboxPermissions,
|
|
33
|
+
runtimeCompatibility: true,
|
|
34
|
+
}).compatibilityWrite ?? [] : [];
|
|
35
|
+
const absolute = resolve(path);
|
|
36
|
+
let current = parse(absolute).root;
|
|
37
|
+
let pending = absolute.slice(current.length).split(sep);
|
|
38
|
+
let links = 0;
|
|
39
|
+
while (pending.length) {
|
|
40
|
+
const component = pending.shift()!;
|
|
41
|
+
if (!component || component === ".") continue;
|
|
42
|
+
if (component === "..") { current = dirname(current); continue; }
|
|
43
|
+
const entry = join(current, component);
|
|
44
|
+
let stat;
|
|
45
|
+
try { stat = lstatSync(entry); }
|
|
46
|
+
catch (error) {
|
|
47
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
if (!stat.isSymbolicLink()) { current = entry; continue; }
|
|
51
|
+
if (++links > 40) throw new Error("Too many symlinks in Pi runtime path");
|
|
52
|
+
let replaceable = false;
|
|
53
|
+
try { accessSync(current, constants.W_OK); replaceable = true; }
|
|
54
|
+
catch { /* An OS-protected entry is immutable; its target still needs inspection. */ }
|
|
55
|
+
const inProject = entry === root || entry.startsWith(root + sep);
|
|
56
|
+
const access = inProject ? permissions.projectFiles
|
|
57
|
+
: compatibility.some((directory) => entry === directory || entry.startsWith(directory + sep)) ? "read-write"
|
|
58
|
+
: permissions.outsideProject;
|
|
59
|
+
if (replaceable && (access === "read-write" || permissions.storedCredentials === "read-write")) return entry;
|
|
60
|
+
// Resolve targets component-by-component: realpath would erase the
|
|
61
|
+
// intermediate links whose directory entries need protection.
|
|
62
|
+
const target = readlinkSync(entry);
|
|
63
|
+
if (isAbsolute(target)) {
|
|
64
|
+
current = parse(target).root;
|
|
65
|
+
pending = [...target.slice(current.length).split(sep), ...pending];
|
|
66
|
+
} else pending = [...target.split(sep), ...pending];
|
|
40
67
|
}
|
|
41
68
|
return undefined;
|
|
42
69
|
}
|
package/task-guard.ts
CHANGED
|
@@ -12,7 +12,7 @@ export default function taskGuard(pi: ExtensionAPI, input: unknown, fatal: (erro
|
|
|
12
12
|
const plan = Object.freeze({
|
|
13
13
|
confined: true as const, profilePath: policy.profilePath,
|
|
14
14
|
policy: Object.freeze({ writableRoot: policy.root, home: policy.home, permissions: policy.permissions, denyWrite: policy.denyWrite,
|
|
15
|
-
runtimeWrite: Object.freeze([policy.scratch]) }),
|
|
15
|
+
runtimeCompatibility: true, runtimeWrite: Object.freeze([policy.scratch]) }),
|
|
16
16
|
});
|
|
17
17
|
const controller = Object.freeze({ requireLaunchPlan: () => plan });
|
|
18
18
|
let shellPath: string | undefined;
|
package/task-policy.ts
CHANGED
|
@@ -60,7 +60,7 @@ export function prepareTaskRuntime(options: {
|
|
|
60
60
|
if (root === dirname(root) || root === canonicalizePath(home)) throw new Error("Task sandbox requires a project directory, not the filesystem or home root.");
|
|
61
61
|
const permissions = options.permissions ?? DEFAULT_TASK_PERMISSIONS;
|
|
62
62
|
const alias = [PiCodingAgent.getAgentDir(), PiCodingAgent.getPackageDir(), tmpdir()]
|
|
63
|
-
.map((path) => writableRuntimeAlias(path, root, permissions)).find(Boolean);
|
|
63
|
+
.map((path) => writableRuntimeAlias(path, root, permissions, true)).find(Boolean);
|
|
64
64
|
if (alias) throw new Error(`Task sandbox runtime path uses a task-writable symlink (${alias}); restart Pi with canonical runtime paths.`);
|
|
65
65
|
const runtimeDirectories = ensureHarnessRuntimeDirectories();
|
|
66
66
|
const agentDir = canonicalizePath(PiCodingAgent.getAgentDir());
|
|
@@ -82,7 +82,7 @@ export function prepareTaskRuntime(options: {
|
|
|
82
82
|
])],
|
|
83
83
|
tools: options.tools,
|
|
84
84
|
});
|
|
85
|
-
compileWritePolicy({ writableRoot: root, home, permissions: policy.permissions, denyWrite: policy.denyWrite });
|
|
85
|
+
compileWritePolicy({ writableRoot: root, home, permissions: policy.permissions, denyWrite: policy.denyWrite, runtimeCompatibility: true });
|
|
86
86
|
const policyPath = join(controlDir, "task-policy.json");
|
|
87
87
|
writeFileSync(policyPath, JSON.stringify(policy), { mode: 0o600 });
|
|
88
88
|
return {
|