@tangle-network/agent-profile-materialize 1.1.7 → 1.1.8

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.
Files changed (95) hide show
  1. package/dist/index.d.ts +1226 -29
  2. package/dist/index.js +4987 -23
  3. package/package.json +7 -5
  4. package/dist/candidate-types.d.ts +0 -36
  5. package/dist/candidate-types.js +0 -1
  6. package/dist/candidate.d.ts +0 -9
  7. package/dist/candidate.js +0 -283
  8. package/dist/commands.d.ts +0 -2
  9. package/dist/commands.js +0 -32
  10. package/dist/control-maps.d.ts +0 -58
  11. package/dist/control-maps.js +0 -277
  12. package/dist/execution-controls.d.ts +0 -5
  13. package/dist/execution-controls.js +0 -199
  14. package/dist/harness-prompt.d.ts +0 -22
  15. package/dist/harness-prompt.js +0 -64
  16. package/dist/harness.d.ts +0 -131
  17. package/dist/harness.js +0 -416
  18. package/dist/hooks.d.ts +0 -6
  19. package/dist/hooks.js +0 -250
  20. package/dist/managed-workspace-bind.d.ts +0 -4
  21. package/dist/managed-workspace-bind.js +0 -106
  22. package/dist/managed-workspace-stage.d.ts +0 -25
  23. package/dist/managed-workspace-stage.js +0 -199
  24. package/dist/managed-workspace-types.d.ts +0 -38
  25. package/dist/managed-workspace-types.js +0 -1
  26. package/dist/managed-workspace.d.ts +0 -31
  27. package/dist/managed-workspace.js +0 -271
  28. package/dist/mcp-config-ownership.d.ts +0 -13
  29. package/dist/mcp-config-ownership.js +0 -79
  30. package/dist/mcp-opencode.d.ts +0 -25
  31. package/dist/mcp-opencode.js +0 -52
  32. package/dist/mcp-timeouts.d.ts +0 -7
  33. package/dist/mcp-timeouts.js +0 -48
  34. package/dist/mcp.d.ts +0 -4
  35. package/dist/mcp.js +0 -246
  36. package/dist/model-id.d.ts +0 -55
  37. package/dist/model-id.js +0 -67
  38. package/dist/pi-models-config.d.ts +0 -32
  39. package/dist/pi-models-config.js +0 -65
  40. package/dist/plan-application.d.ts +0 -10
  41. package/dist/plan-application.js +0 -190
  42. package/dist/plan-digest.d.ts +0 -18
  43. package/dist/plan-digest.js +0 -116
  44. package/dist/plan-json.d.ts +0 -13
  45. package/dist/plan-json.js +0 -91
  46. package/dist/plan-normalize.d.ts +0 -12
  47. package/dist/plan-normalize.js +0 -168
  48. package/dist/plan-prompt.d.ts +0 -38
  49. package/dist/plan-prompt.js +0 -49
  50. package/dist/plan-support.d.ts +0 -9
  51. package/dist/plan-support.js +0 -22
  52. package/dist/plan-types.d.ts +0 -341
  53. package/dist/plan-types.js +0 -105
  54. package/dist/prime-agent-dir.d.ts +0 -64
  55. package/dist/prime-agent-dir.js +0 -173
  56. package/dist/prime-fork.d.ts +0 -155
  57. package/dist/prime-fork.js +0 -262
  58. package/dist/prime-profile.d.ts +0 -88
  59. package/dist/prime-profile.js +0 -297
  60. package/dist/profile-bundle-cache.d.ts +0 -37
  61. package/dist/profile-bundle-cache.js +0 -320
  62. package/dist/profile-bundle-key.d.ts +0 -69
  63. package/dist/profile-bundle-key.js +0 -62
  64. package/dist/profile-bundle-lru.d.ts +0 -56
  65. package/dist/profile-bundle-lru.js +0 -220
  66. package/dist/profile-materializer.d.ts +0 -24
  67. package/dist/profile-materializer.js +0 -114
  68. package/dist/profile-plan.d.ts +0 -16
  69. package/dist/profile-plan.js +0 -90
  70. package/dist/profile-prompt.d.ts +0 -4
  71. package/dist/profile-prompt.js +0 -107
  72. package/dist/profile-support.d.ts +0 -8
  73. package/dist/profile-support.js +0 -6
  74. package/dist/resource-lowering.d.ts +0 -6
  75. package/dist/resource-lowering.js +0 -103
  76. package/dist/resource-resolution.d.ts +0 -27
  77. package/dist/resource-resolution.js +0 -265
  78. package/dist/secret-concurrent-revision.d.ts +0 -30
  79. package/dist/secret-concurrent-revision.js +0 -249
  80. package/dist/secret-session-view.d.ts +0 -25
  81. package/dist/secret-session-view.js +0 -166
  82. package/dist/subagents.d.ts +0 -6
  83. package/dist/subagents.js +0 -137
  84. package/dist/tool-bin-path.d.ts +0 -23
  85. package/dist/tool-bin-path.js +0 -61
  86. package/dist/workspace-apply-files.d.ts +0 -57
  87. package/dist/workspace-apply-files.js +0 -193
  88. package/dist/workspace-apply-publish.d.ts +0 -22
  89. package/dist/workspace-apply-publish.js +0 -178
  90. package/dist/workspace-apply.d.ts +0 -11
  91. package/dist/workspace-apply.js +0 -124
  92. package/dist/workspace-files.d.ts +0 -8
  93. package/dist/workspace-files.js +0 -46
  94. package/dist/workspace-launch.d.ts +0 -3
  95. package/dist/workspace-launch.js +0 -13
@@ -1,249 +0,0 @@
1
- import { execFileSync } from "node:child_process";
2
- import { createHash, createHmac } from "node:crypto";
3
- import { chmodSync, lstatSync, mkdirSync, readdirSync } from "node:fs";
4
- import { join } from "node:path";
5
- import { agentProfileSecretRefSchema, } from "@tangle-network/agent-interface";
6
- import { assertLaunchRoot, prepareExplicitOpenCodePlan, publishConcurrentProfileRevision, } from "./managed-workspace.js";
7
- import { bindConcurrentLaunch } from "./managed-workspace-bind.js";
8
- import { prepareManagedWorkspacePlan } from "./managed-workspace-stage.js";
9
- import { normalizeWorkspacePlanArgument } from "./plan-normalize.js";
10
- import { assertWorkspacePlanSupported } from "./plan-support.js";
11
- /** Refuse an executor that cannot isolate the revision, or a plan that claims its launch names. */
12
- function assertSecretRevisionLaunch(input) {
13
- if (process.platform !== "linux" ||
14
- process.getuid?.() !== 0 ||
15
- !Number.isSafeInteger(input.subprocessUid) ||
16
- input.subprocessUid < 200_000 ||
17
- input.namingKey.length !== 32 ||
18
- !input.ownerId) {
19
- throw new Error("Secret concurrent profile needs a root executor and reserved identity");
20
- }
21
- for (const name of [
22
- "OPENCODE_DISABLE_PROJECT_CONFIG",
23
- "OPENCODE_CONFIG_DIR",
24
- "TANGLE_SHARED_WORKSPACE",
25
- "TANGLE_PROFILE_REVISION_WORKSPACE",
26
- ]) {
27
- if (Object.hasOwn(input.plan.env, name)) {
28
- throw new Error(`Secret ${input.plan.harness} reserves ${name} for its launch binding`);
29
- }
30
- }
31
- }
32
- /**
33
- * The OpenCode plan the revision materializes: the trusted provider config
34
- * merged in, and every public MCP header turned into a reference.
35
- */
36
- function secretOpenCodePlan(sourcePlan, providerConfig, secretReferences) {
37
- // Workspace `reference/` files stay in this revision's read-only tree. The
38
- // sealed launch runs from that tree, so a cwd-relative `reference/` path
39
- // reads this revision's copy and nothing is written to the shared workspace.
40
- let plan = prepareExplicitOpenCodePlan(sourcePlan);
41
- if (providerConfig) {
42
- plan = {
43
- ...plan,
44
- files: plan.files.map((file) => file.relPath === "opencode.json" &&
45
- file.source === "generated" &&
46
- (file.root ?? "workspace") === "workspace"
47
- ? {
48
- ...file,
49
- content: JSON.stringify({
50
- ...JSON.parse(file.content),
51
- provider: providerConfig,
52
- }),
53
- }
54
- : file),
55
- };
56
- }
57
- plan = referencePublicMcpHeaders(plan, secretReferences);
58
- if (plan.files.some((file) => file.relPath.startsWith(".config/opencode/"))) {
59
- throw new Error("Secret OpenCode reserves its native config directory");
60
- }
61
- // OpenCode and every process it spawns run as the session UID, so a value in
62
- // its environment, argv or config is readable by the agent's own shell
63
- // (`/proc/<pid>/environ`, `/proc/<pid>/cmdline`, `cat`). No profile secret
64
- // value may reach the launch: config slots stay references whose values the
65
- // launch never receives (#11875).
66
- if (Object.values(plan.env).some((value) => typeof value !== "string") ||
67
- plan.flags.some((argument) => typeof normalizeWorkspacePlanArgument(argument) !== "string")) {
68
- throw new Error("Secret OpenCode refuses a profile secret in its launch environment or arguments");
69
- }
70
- return plan;
71
- }
72
- /** Codex's native readback identifies which held values belong to MCP servers. */
73
- async function secretCodexPlan(plan, references, provider) {
74
- const env = { ...plan.env };
75
- for (const [variable, value] of Object.entries(env)) {
76
- if (/^(?:CODEX_|XDG_|HOME$)/u.test(variable)) {
77
- throw new Error(`Secret Codex reserves ${variable} for its launch binding`);
78
- }
79
- if (typeof value === "string" && !variable.startsWith("TANGLE_MCP_HEADER_"))
80
- continue;
81
- const ref = typeof value === "string"
82
- ? undefined
83
- : agentProfileSecretRefSchema.parse(value);
84
- const raw = ref ? await provider.get(ref.key) : value;
85
- if (typeof raw !== "string" || !raw.trim())
86
- throw new Error("Secret Codex MCP value is unavailable");
87
- const name = `${HELD_VALUE_PREFIX}${createHash("sha256").update(`codex-mcp-env\0${variable}`).digest("hex").slice(0, 24).toUpperCase()}`;
88
- references[name] = ref?.format === "bearer" ? `Bearer ${raw}` : raw;
89
- env[variable] = `{env:${name}}`;
90
- }
91
- if (plan.flags.some((argument) => typeof normalizeWorkspacePlanArgument(argument) !== "string")) {
92
- throw new Error("Secret Codex refuses a profile secret in native arguments");
93
- }
94
- return { ...plan, env };
95
- }
96
- /**
97
- * Publish a resolved revision for exactly one native subprocess UID. The
98
- * executor-owned naming key hides low-entropy credential guesses from paths;
99
- * the resolved digest and manifest never leave this server-side result.
100
- */
101
- export async function prepareSecretConcurrentProfileRevision(input) {
102
- assertSecretRevisionLaunch(input);
103
- const secretReferences = {};
104
- if (input.plan.harness !== "codex" && input.plan.harness !== "opencode") {
105
- throw new Error("Secret profile has no verified native launch binding");
106
- }
107
- const plan = input.plan.harness === "codex"
108
- ? await secretCodexPlan(input.plan, secretReferences, input.secretProvider)
109
- : secretOpenCodePlan(input.plan, input.providerConfig, secretReferences);
110
- assertWorkspacePlanSupported(plan);
111
- assertLaunchRoot(input.launchRoot);
112
- // File slots reference their secrets, so an unchanged profile with a rotated
113
- // per-turn secret reuses this revision.
114
- const prepared = await prepareManagedWorkspacePlan(plan, input.managedRoot, input.sharedWorkspaceDir, input.secretProvider, { ...input.options, secretFileSlotReferences: secretReferences });
115
- const identity = createHmac("sha256", input.namingKey)
116
- .update(input.sharedWorkspaceDir)
117
- .update("\0")
118
- .update(input.ownerId)
119
- .update("\0")
120
- .update(String(input.subprocessUid))
121
- .update("\0")
122
- .update(prepared.manifest.materializedDigest)
123
- .digest("hex");
124
- const revision = publishConcurrentProfileRevision(prepared, join(input.launchRoot, `revision-${identity}`), (directory) => {
125
- // OpenCode's sealed XDG tree never reads authority from native HOME.
126
- if (plan.harness === "opencode")
127
- for (const relPath of [".config/opencode", ".opencode"]) {
128
- mkdirSync(join(prepared.root, "workspace", relPath), {
129
- recursive: true,
130
- mode: 0o700,
131
- });
132
- }
133
- const bound = bindConcurrentLaunch(plan, prepared.application, join(directory, "workspace"), { secretRevision: true });
134
- return {
135
- ...bound,
136
- env: {
137
- ...bound.env,
138
- ...(plan.harness === "opencode"
139
- ? {
140
- OPENCODE_DISABLE_PROJECT_CONFIG: "1",
141
- OPENCODE_CONFIG_DIR: join(directory, "workspace", ".opencode"),
142
- }
143
- : {
144
- TANGLE_PROFILE_REVISION_WORKSPACE: join(directory, "workspace"),
145
- }),
146
- TANGLE_SHARED_WORKSPACE: input.sharedWorkspaceDir,
147
- },
148
- };
149
- }, (root) => makeSecretRevisionReadOnly(root, input.subprocessUid), true);
150
- return { ...revision, secretReferences };
151
- }
152
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
153
- /** How `servableMcpServer` recognizes a header value the executor holds. */
154
- const HELD_VALUE_PREFIX = "TANGLE_PROFILE_SECRET_";
155
- /**
156
- * A remote MCP server's public header values never reach the sealed launch:
157
- * the executor's loopback proxy replaces the server's url and headers there.
158
- * They did reach the revision's bytes, so a value that changes every turn (a
159
- * run id, a message id) gave each turn its own digest, revision directory,
160
- * receipt and native health check. Each such value becomes a reference the
161
- * executor resolves, as a secret slot already does, and the revision names
162
- * only the header. The value stays as public as the profile declared it; it is
163
- * held beside the secret references and nothing the native process can read
164
- * carries it.
165
- *
166
- * A value that already looks like a reference, or is empty, stays in place.
167
- */
168
- function referencePublicMcpHeaders(plan, references) {
169
- const configFile = plan.files.find((file) => file.relPath === "opencode.json" &&
170
- file.source === "generated" &&
171
- (file.root ?? "workspace") === "workspace");
172
- if (!configFile)
173
- return plan;
174
- const config = JSON.parse(configFile.content);
175
- if (!isRecord(config) || !isRecord(config.mcp))
176
- return plan;
177
- let changed = false;
178
- for (const [server, entry] of Object.entries(config.mcp)) {
179
- if (!isRecord(entry) || entry.type !== "remote")
180
- continue;
181
- if (!isRecord(entry.headers))
182
- continue;
183
- for (const [header, value] of Object.entries(entry.headers)) {
184
- if (typeof value !== "string" || value === "")
185
- continue;
186
- if (/\{(?:env|file):/.test(value))
187
- continue;
188
- const name = `${HELD_VALUE_PREFIX}${createHash("sha256")
189
- .update(`public-mcp-header\0${server}\0${header}`, "utf8")
190
- .digest("hex")
191
- .slice(0, 24)
192
- .toUpperCase()}`;
193
- references[name] = value;
194
- entry.headers[header] = `{env:${name}}`;
195
- changed = true;
196
- }
197
- }
198
- if (!changed)
199
- return plan;
200
- return {
201
- ...plan,
202
- files: plan.files.map((file) => file === configFile ? { ...file, content: JSON.stringify(config) } : file),
203
- };
204
- }
205
- function secretRevisionPaths(root) {
206
- const all = [root];
207
- const exposed = [root];
208
- const walk = (directory) => {
209
- for (const entry of readdirSync(directory, { withFileTypes: true })) {
210
- const child = join(directory, entry.name);
211
- const stat = lstatSync(child);
212
- if (stat.uid !== 0 || stat.isSymbolicLink()) {
213
- throw new Error("Secret concurrent profile stage contains an untrusted inode");
214
- }
215
- if (!stat.isDirectory() && !stat.isFile()) {
216
- throw new Error("Secret concurrent profile stage contains a special file");
217
- }
218
- all.push(child);
219
- if (entry.name !== "manifest.json" || directory !== root) {
220
- exposed.push(child);
221
- }
222
- if (stat.isDirectory())
223
- walk(child);
224
- }
225
- };
226
- walk(root);
227
- return { all, exposed };
228
- }
229
- export function runSetfacl(args, paths) {
230
- for (let start = 0; start < paths.length; start += 64) {
231
- execFileSync("/usr/bin/setfacl", [...args, "--", ...paths.slice(start, start + 64)], {
232
- env: { PATH: "/usr/bin:/bin", LANG: "C" },
233
- timeout: 30_000,
234
- stdio: "pipe",
235
- });
236
- }
237
- }
238
- function makeSecretRevisionReadOnly(root, uid) {
239
- const paths = secretRevisionPaths(root);
240
- // Remove inherited named ACLs before widening the ACL mask for this UID.
241
- runSetfacl(["-b"], paths.all);
242
- for (const path of paths.all) {
243
- const stat = lstatSync(path);
244
- chmodSync(path, stat.isDirectory() ? 0o500 : stat.mode & 0o111 ? 0o500 : 0o400);
245
- }
246
- // The private manifest has resolved-file hashes. The CLI can read its native
247
- // files, but never the executor inventory or another revision's files.
248
- runSetfacl(["-m", `u:${uid}:r-X`], paths.exposed);
249
- }
@@ -1,25 +0,0 @@
1
- /**
2
- * The stable, sealed cwd of one secret session. Native resume keeps this path
3
- * while each turn's revision changes. Its name binds the workspace, session
4
- * and subprocess UID, so two sessions never share a view.
5
- */
6
- export declare function secretSessionViewWorkspace(input: {
7
- launchRoot: string;
8
- sharedWorkspaceDir: string;
9
- sessionId: string;
10
- subprocessUid: number;
11
- }): string;
12
- /**
13
- * Bind every supplied workspace resource at its literal path, using the same
14
- * root-owned, read-only inode and ACL as the published revision. Generated
15
- * native configuration stays bound by absolute path, outside this cwd.
16
- *
17
- * The caller serializes a session's turns and binds before spawning: replacing
18
- * a cwd while a previous native process still reads it is not supported.
19
- */
20
- export declare function bindSecretSessionView(input: {
21
- launchRoot: string;
22
- viewWorkspaceDir: string;
23
- revisionWorkspaceDir: string;
24
- subprocessUid: number;
25
- }): void;
@@ -1,166 +0,0 @@
1
- import { createHash, randomBytes } from "node:crypto";
2
- import { chmodSync, linkSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, } from "node:fs";
3
- import { basename, dirname, join } from "node:path";
4
- import { assertLaunchRoot, assertPublishedRevision, } from "./managed-workspace.js";
5
- import { runSetfacl } from "./secret-concurrent-revision.js";
6
- import { normalizeWorkspaceRelativePath } from "./workspace-files.js";
7
- /**
8
- * The stable, sealed cwd of one secret session. Native resume keeps this path
9
- * while each turn's revision changes. Its name binds the workspace, session
10
- * and subprocess UID, so two sessions never share a view.
11
- */
12
- export function secretSessionViewWorkspace(input) {
13
- if (!input.sessionId)
14
- throw new Error("Secret session view needs a session");
15
- const name = createHash("sha256")
16
- .update("tangle-secret-session-view\0")
17
- .update(input.sharedWorkspaceDir)
18
- .update("\0")
19
- .update(input.sessionId)
20
- .update("\0")
21
- .update(String(input.subprocessUid))
22
- .digest("hex");
23
- return join(input.launchRoot, `session-${name}`, "workspace");
24
- }
25
- /**
26
- * Bind every supplied workspace resource at its literal path, using the same
27
- * root-owned, read-only inode and ACL as the published revision. Generated
28
- * native configuration stays bound by absolute path, outside this cwd.
29
- *
30
- * The caller serializes a session's turns and binds before spawning: replacing
31
- * a cwd while a previous native process still reads it is not supported.
32
- */
33
- export function bindSecretSessionView(input) {
34
- const { launchRoot, viewWorkspaceDir, revisionWorkspaceDir } = input;
35
- if (process.platform !== "linux" ||
36
- process.getuid?.() !== 0 ||
37
- !Number.isSafeInteger(input.subprocessUid) ||
38
- input.subprocessUid < 200_000) {
39
- throw new Error("Secret session view needs a root executor and reserved identity");
40
- }
41
- assertLaunchRoot(launchRoot);
42
- const viewRoot = dirname(viewWorkspaceDir);
43
- if (!isLaunchWorkspace(launchRoot, viewWorkspaceDir, /^session-[a-f0-9]{64}$/)) {
44
- throw new Error("Secret session view is not a launch-root session path");
45
- }
46
- const revisionRoot = dirname(revisionWorkspaceDir);
47
- const manifestPath = join(revisionRoot, "manifest.json");
48
- if (!isLaunchWorkspace(launchRoot, revisionWorkspaceDir, /^revision-[a-f0-9]{64}$/) ||
49
- realpathSync(revisionWorkspaceDir) !== revisionWorkspaceDir ||
50
- !isSealedNode(lstatSync(revisionRoot), "directory") ||
51
- !isSealedNode(lstatSync(revisionWorkspaceDir), "directory") ||
52
- !isSealedNode(lstatSync(manifestPath), "file")) {
53
- throw new Error("Secret session view needs a published revision");
54
- }
55
- const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
56
- for (const file of manifest.files) {
57
- if (!["workspace", "agent"].includes(file.root) ||
58
- normalizeWorkspaceRelativePath(file.relPath) !== file.relPath) {
59
- throw new Error("Secret revision resource path is not canonical");
60
- }
61
- }
62
- assertPublishedRevision(revisionRoot, manifest, 0);
63
- const current = lstatIfPresent(viewRoot);
64
- if (!current)
65
- mkdirSync(viewRoot, { mode: 0o700 });
66
- else if (!current.isDirectory() ||
67
- current.isSymbolicLink() ||
68
- current.uid !== 0 ||
69
- (current.mode & 0o022) !== 0) {
70
- throw new Error("Secret session view is not an executor-owned directory");
71
- }
72
- const workspace = lstatIfPresent(viewWorkspaceDir);
73
- if (workspace) {
74
- if (!isSealedNode(workspace, "directory")) {
75
- throw new Error("Secret session view is not an executor-owned directory");
76
- }
77
- assertSealedTree(viewWorkspaceDir);
78
- }
79
- // An interrupted bind leaves its staging/retired tree beside workspace.
80
- clearBesideWorkspace(viewRoot);
81
- const staged = besideWorkspace(viewRoot, "stage");
82
- const retired = besideWorkspace(viewRoot, "retired");
83
- mkdirSync(staged, { mode: 0o700 });
84
- const directories = new Set([staged]);
85
- try {
86
- for (const file of manifest.files) {
87
- if (file.root !== "workspace" || file.source !== "user")
88
- continue;
89
- const relative = file.relPath;
90
- const source = join(revisionWorkspaceDir, relative);
91
- let ancestor = dirname(source);
92
- while (ancestor !== revisionRoot) {
93
- if (!isSealedNode(lstatSync(ancestor), "directory")) {
94
- throw new Error("Secret revision resource has an unsealed ancestor");
95
- }
96
- ancestor = dirname(ancestor);
97
- }
98
- const target = join(staged, relative);
99
- mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
100
- linkSync(source, target);
101
- ancestor = dirname(target);
102
- while (ancestor !== staged) {
103
- directories.add(ancestor);
104
- ancestor = dirname(ancestor);
105
- }
106
- }
107
- sealDirectories([...directories], input.subprocessUid);
108
- if (lstatIfPresent(viewWorkspaceDir))
109
- renameSync(viewWorkspaceDir, retired);
110
- try {
111
- renameSync(staged, viewWorkspaceDir);
112
- }
113
- catch (error) {
114
- if (lstatIfPresent(retired))
115
- renameSync(retired, viewWorkspaceDir);
116
- throw error;
117
- }
118
- sealDirectories([viewRoot], input.subprocessUid);
119
- clearBesideWorkspace(viewRoot);
120
- }
121
- finally {
122
- rmSync(staged, { recursive: true, force: true });
123
- }
124
- }
125
- /** A fresh staged or retired path beside the view's workspace. */
126
- function besideWorkspace(viewRoot, kind) {
127
- return join(viewRoot, `.${kind}-${randomBytes(8).toString("hex")}`);
128
- }
129
- function isLaunchWorkspace(launchRoot, workspace, name) {
130
- return (basename(workspace) === "workspace" &&
131
- dirname(dirname(workspace)) === launchRoot &&
132
- name.test(basename(dirname(workspace))));
133
- }
134
- function lstatIfPresent(path) {
135
- return lstatSync(path, { throwIfNoEntry: false });
136
- }
137
- function isSealedNode(stat, kind) {
138
- return (!stat.isSymbolicLink() &&
139
- (kind === "directory" ? stat.isDirectory() : stat.isFile()) &&
140
- stat.uid === 0 &&
141
- (stat.mode & 0o222) === 0);
142
- }
143
- function assertSealedTree(root) {
144
- const stat = lstatSync(root);
145
- if (isSealedNode(stat, "directory")) {
146
- for (const name of readdirSync(root))
147
- assertSealedTree(join(root, name));
148
- }
149
- else if (!isSealedNode(stat, "file")) {
150
- throw new Error("Secret session view holds an unexpected or unsealed entry");
151
- }
152
- }
153
- /** Root-owned, read-only, and traversable only by the session's UID. */
154
- function sealDirectories(directories, uid) {
155
- runSetfacl(["-b"], directories);
156
- for (const directory of directories)
157
- chmodSync(directory, 0o500);
158
- runSetfacl(["-m", `u:${uid}:r-X`], directories);
159
- }
160
- function clearBesideWorkspace(viewRoot) {
161
- for (const name of readdirSync(viewRoot)) {
162
- if (name !== "workspace") {
163
- rmSync(join(viewRoot, name), { recursive: true, force: true });
164
- }
165
- }
166
- }
@@ -1,6 +0,0 @@
1
- export declare function materializeSubagent(h: HarnessId, name: string, sa: AgentSubagentProfile, plan: WorkspacePlan, add: AddPlanFile, unsupported: (d: Unsupported["dimension"], r: string) => void): void;
2
- export declare function codexAgentRoleName(resourceName: string): string | undefined;
3
- export declare function registerCodexAgent(plan: WorkspacePlan, name: string, configFile: string, description: string): void;
4
- import type { AgentSubagentProfile } from "@tangle-network/agent-interface";
5
- import type { AddPlanFile } from "./plan-json.js";
6
- import type { HarnessId, Unsupported, WorkspacePlan } from "./plan-types.js";
package/dist/subagents.js DELETED
@@ -1,137 +0,0 @@
1
- export function materializeSubagent(h, name, sa, plan, add, unsupported) {
2
- if (!/^[a-z0-9][a-z0-9_-]*$/.test(name)) {
3
- unsupported("subagents", `${h}: subagent name "${name}" must use lowercase letters, digits, underscores, or hyphens`);
4
- return;
5
- }
6
- switch (h) {
7
- case "claude-code":
8
- case "nanoclaw": {
9
- const fm = ["---", `name: ${name}`];
10
- fm.push(`description: ${JSON.stringify(sa.description ?? name)}`);
11
- if (sa.model)
12
- fm.push(`model: ${JSON.stringify(sa.model)}`);
13
- if (sa.tools) {
14
- const tools = Object.entries(sa.tools);
15
- const allowed = tools
16
- .filter(([, enabled]) => enabled)
17
- .map(([tool]) => tool);
18
- const denied = tools
19
- .filter(([, enabled]) => !enabled)
20
- .map(([tool]) => tool);
21
- if (allowed.length > 0)
22
- fm.push(`tools: ${JSON.stringify(allowed)}`);
23
- if (denied.length > 0)
24
- fm.push(`disallowedTools: ${JSON.stringify(denied)}`);
25
- }
26
- if (sa.maxSteps !== undefined)
27
- fm.push(`maxTurns: ${sa.maxSteps}`);
28
- reportUnsupportedSubagentFields(h, name, sa, new Set(["description", "prompt", "model", "tools", "maxSteps"]), unsupported);
29
- fm.push("---", "", sa.prompt ?? sa.description ?? name);
30
- const relPath = `.claude/agents/${name}.md`;
31
- add(relPath, markMaterializerSubagent(relPath, `${fm.join("\n")}\n`));
32
- break;
33
- }
34
- case "opencode": {
35
- const fm = [
36
- "---",
37
- `description: ${JSON.stringify(sa.description ?? name)}`,
38
- "mode: subagent",
39
- ];
40
- if (sa.model)
41
- fm.push(`model: ${JSON.stringify(sa.model)}`);
42
- if (sa.tools)
43
- fm.push(`tools: ${JSON.stringify(sa.tools)}`);
44
- if (sa.permissions)
45
- fm.push(`permission: ${JSON.stringify(sa.permissions)}`);
46
- if (sa.maxSteps !== undefined)
47
- fm.push(`steps: ${sa.maxSteps}`);
48
- reportUnsupportedSubagentFields(h, name, sa, new Set([
49
- "description",
50
- "prompt",
51
- "model",
52
- "tools",
53
- "permissions",
54
- "maxSteps",
55
- ]), unsupported);
56
- fm.push("---", "", sa.prompt ?? "");
57
- const relPath = `.opencode/agents/${name}.md`;
58
- add(relPath, markMaterializerSubagent(relPath, `${fm.join("\n")}\n`));
59
- break;
60
- }
61
- case "gemini": {
62
- const tools = sa.tools
63
- ? Object.entries(sa.tools)
64
- .filter(([, enabled]) => enabled)
65
- .map(([tool]) => tool)
66
- : undefined;
67
- reportUnsupportedSubagentFields(h, name, sa, new Set(["description", "prompt", "model", "tools", "maxSteps"]), unsupported);
68
- const relPath = `.gemini/agents/${name}.md`;
69
- add(relPath, markMaterializerSubagent(relPath, [
70
- "---",
71
- `name: ${name}`,
72
- `description: ${JSON.stringify(sa.description ?? name)}`,
73
- "kind: local",
74
- ...(sa.model ? [`model: ${JSON.stringify(sa.model)}`] : []),
75
- ...(tools ? [`tools: ${JSON.stringify(tools)}`] : []),
76
- ...(sa.maxSteps !== undefined ? [`max_turns: ${sa.maxSteps}`] : []),
77
- "---",
78
- "",
79
- sa.prompt ?? "",
80
- "",
81
- ].join("\n")));
82
- break;
83
- }
84
- case "codex": {
85
- const relPath = `.codex/agents/${name}.toml`;
86
- const lines = [
87
- `name = ${tomlStr(name)}`,
88
- `description = ${tomlStr(sa.description ?? name)}`,
89
- `developer_instructions = ${tomlStr(sa.prompt ?? sa.description ?? name)}`,
90
- ];
91
- if (sa.model)
92
- lines.push(`model = ${tomlStr(sa.model)}`);
93
- reportUnsupportedSubagentFields(h, name, sa, new Set(["description", "prompt", "model"]), unsupported);
94
- add(relPath, markMaterializerSubagent(relPath, `${lines.join("\n")}\n`));
95
- registerCodexAgent(plan, name, relPath, sa.description ?? name);
96
- break;
97
- }
98
- case "prime":
99
- // Prime subagents ARE expressible — as entries in the continual-harness
100
- // store at <agentDir>/harness/harness_state.json (fork refinement.ts:269-279,
101
- // re-read on every system-prompt build: agent-session.ts:7548-7554) — but
102
- // that store lives under the executor-owned agent dir, which no
103
- // workspace-relative plan file can reach. Executors that own the agent
104
- // dir seed it with seedPrimeProfileSubagents; the plan stays fail-closed
105
- // so a plan-forwarding caller can never believe the roster is in force.
106
- unsupported("subagents", `prime: subagents live in the agent-dir continual-harness store (<agentDir>/harness/harness_state.json), outside any workspace plan — seed "${name}" with seedPrimeProfileSubagents from the executor that owns the agent dir`);
107
- break;
108
- default:
109
- unsupported("subagents", `${h}: subagents have no cwd file (config/flag/global only); "${name}" not mounted`);
110
- }
111
- }
112
- export function codexAgentRoleName(resourceName) {
113
- const name = resourceName.endsWith(".toml")
114
- ? resourceName.slice(0, -".toml".length)
115
- : resourceName;
116
- return /^[a-z0-9][a-z0-9_-]*$/.test(name) ? name : undefined;
117
- }
118
- export function registerCodexAgent(plan, name, configFile, description) {
119
- plan.flags.push("-c", `agents.${name}.description=${tomlStr(description)}`, "-c", `agents.${name}.config_file=${tomlStr(configFile)}`);
120
- }
121
- function reportUnsupportedSubagentFields(harness, name, subagent, supported, unsupported) {
122
- for (const field of [
123
- "description",
124
- "prompt",
125
- "model",
126
- "tools",
127
- "permissions",
128
- "maxSteps",
129
- "metadata",
130
- ]) {
131
- if (subagent[field] !== undefined && !supported.has(field)) {
132
- unsupported("subagents", `${harness}: subagent "${name}" does not support ${field}`);
133
- }
134
- }
135
- }
136
- import { markMaterializerSubagent } from "./harness.js";
137
- import { tomlStr } from "./plan-json.js";
@@ -1,23 +0,0 @@
1
- /** The colon-separated, absolute CLI bin dirs a sandbox app mounts for its tools. */
2
- export declare const SANDBOX_TOOL_BIN_DIRS_ENV = "SANDBOX_TOOL_BIN_DIRS";
3
- /**
4
- * The tool bin dirs a `SANDBOX_TOOL_BIN_DIRS` value names, or `[]` when it is
5
- * unset or blank.
6
- *
7
- * Fail loud, never silently degrade: a malformed value (a relative segment,
8
- * or an empty segment from a stray/leading/trailing colon) throws rather than
9
- * silently dropping a tool dir from PATH or admitting a relative path that
10
- * resolves against whatever the process cwd happens to be.
11
- */
12
- export declare function parseSandboxToolBinDirs(value: string | undefined): string[];
13
- /**
14
- * A launch environment whose tool bin dirs are `toolBinDirs`, at the TAIL of
15
- * PATH after every Nix, npm, image and system entry, so a tool never shadows
16
- * a platform-owned binary of the same name.
17
- *
18
- * They replace `appToolBinDirs` (by default the dirs the environment's own
19
- * `SANDBOX_TOOL_BIN_DIRS` names) wherever those sit in PATH, so an app's older
20
- * copy of the same tool cannot win by PATH order. Relative PATH entries are
21
- * dropped too: they resolve against whatever cwd the turn moves to.
22
- */
23
- export declare function bindLaunchToolBinDirs(env: Readonly<Record<string, string | undefined>>, toolBinDirs: readonly string[], appToolBinDirs?: readonly string[]): Record<string, string | undefined>;
@@ -1,61 +0,0 @@
1
- import { isAbsolute } from "node:path";
2
- /** The colon-separated, absolute CLI bin dirs a sandbox app mounts for its tools. */
3
- export const SANDBOX_TOOL_BIN_DIRS_ENV = "SANDBOX_TOOL_BIN_DIRS";
4
- /**
5
- * The tool bin dirs a `SANDBOX_TOOL_BIN_DIRS` value names, or `[]` when it is
6
- * unset or blank.
7
- *
8
- * Fail loud, never silently degrade: a malformed value (a relative segment,
9
- * or an empty segment from a stray/leading/trailing colon) throws rather than
10
- * silently dropping a tool dir from PATH or admitting a relative path that
11
- * resolves against whatever the process cwd happens to be.
12
- */
13
- export function parseSandboxToolBinDirs(value) {
14
- const trimmed = value?.trim();
15
- if (!trimmed)
16
- return [];
17
- const segments = trimmed.split(":");
18
- for (const segment of segments) {
19
- if (!segment || !isAbsolute(segment)) {
20
- throw new Error(`SANDBOX_TOOL_BIN_DIRS must be a colon-separated list of absolute paths with no empty segments, got: ${trimmed}`);
21
- }
22
- }
23
- return segments;
24
- }
25
- function assertToolBinDir(dir) {
26
- // A ':' would split one declared dir into several PATH entries, admitting
27
- // a dir no profile declared.
28
- if (!dir || !isAbsolute(dir) || dir.includes(":")) {
29
- throw new Error(`a tool bin dir must be an absolute path without ':', got: ${dir}`);
30
- }
31
- }
32
- /**
33
- * A launch environment whose tool bin dirs are `toolBinDirs`, at the TAIL of
34
- * PATH after every Nix, npm, image and system entry, so a tool never shadows
35
- * a platform-owned binary of the same name.
36
- *
37
- * They replace `appToolBinDirs` (by default the dirs the environment's own
38
- * `SANDBOX_TOOL_BIN_DIRS` names) wherever those sit in PATH, so an app's older
39
- * copy of the same tool cannot win by PATH order. Relative PATH entries are
40
- * dropped too: they resolve against whatever cwd the turn moves to.
41
- */
42
- export function bindLaunchToolBinDirs(env, toolBinDirs, appToolBinDirs = parseSandboxToolBinDirs(env[SANDBOX_TOOL_BIN_DIRS_ENV])) {
43
- for (const dir of toolBinDirs)
44
- assertToolBinDir(dir);
45
- const dirs = [...new Set(toolBinDirs)];
46
- // No PATH to bind: the spawner appends SANDBOX_TOOL_BIN_DIRS to its default.
47
- if (env.PATH === undefined) {
48
- return { ...env, [SANDBOX_TOOL_BIN_DIRS_ENV]: dirs.join(":") };
49
- }
50
- const replaced = new Set(appToolBinDirs);
51
- const entries = env.PATH.split(":").filter((entry) => isAbsolute(entry) && !replaced.has(entry));
52
- for (const dir of dirs) {
53
- if (!entries.includes(dir))
54
- entries.push(dir);
55
- }
56
- return {
57
- ...env,
58
- PATH: entries.join(":"),
59
- [SANDBOX_TOOL_BIN_DIRS_ENV]: dirs.join(":"),
60
- };
61
- }