@tangle-network/agent-profile-materialize 1.1.6 → 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 -112
  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 -37
  27. package/dist/managed-workspace.js +0 -290
  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 -291
  80. package/dist/secret-session-view.d.ts +0 -36
  81. package/dist/secret-session-view.js +0 -179
  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
package/dist/hooks.js DELETED
@@ -1,250 +0,0 @@
1
- export function materializeHooks(h, hooks, add, unsupported, plan) {
2
- switch (h) {
3
- case "claude-code": {
4
- // Claude settings hooks{} — each event → [{matcher, hooks:[{type:command,command}]}].
5
- const settingsHooks = {};
6
- for (const [event, cmds] of Object.entries(hooks)) {
7
- if (!hookEventSupported("claude-code", event)) {
8
- unsupported("hooks", `claude-code: unsupported hook event "${event}"`);
9
- continue;
10
- }
11
- settingsHooks[event] = cmds.map((c) => ({
12
- matcher: c.matcher ?? "*",
13
- hooks: [
14
- {
15
- type: "command",
16
- command: materializedHookCommand(event, c, add, plan),
17
- ...(c.timeoutMs ? { timeout: timeoutSeconds(c.timeoutMs) } : {}),
18
- ...(c.blocking === false ? { async: true } : {}),
19
- },
20
- ],
21
- }));
22
- }
23
- add(CLAUDE_SETTINGS_PATH, serializeWorkspacePlanJson({ hooks: settingsHooks }));
24
- registerClaudeSettings(plan);
25
- break;
26
- }
27
- case "nanoclaw":
28
- unsupported("hooks", "nanoclaw's chat bridge does not expose Claude Code hook settings to the spawned agent");
29
- break;
30
- case "gemini": {
31
- const settingsHooks = {};
32
- for (const [event, cmds] of Object.entries(hooks)) {
33
- if (!hookEventSupported("gemini", event)) {
34
- unsupported("hooks", `gemini: unsupported hook event "${event}"`);
35
- continue;
36
- }
37
- if (cmds.some((command) => command.blocking === false)) {
38
- unsupported("hooks", `gemini: hook event "${event}" does not support non-blocking commands`);
39
- }
40
- settingsHooks[event] = cmds.map((c) => ({
41
- matcher: c.matcher ?? "*",
42
- hooks: [
43
- {
44
- type: "command",
45
- command: materializedHookCommand(event, c, add, plan),
46
- ...(c.timeoutMs ? { timeout: c.timeoutMs } : {}),
47
- },
48
- ],
49
- }));
50
- }
51
- add(".gemini/settings.json", serializeWorkspacePlanJson({ hooks: settingsHooks }));
52
- break;
53
- }
54
- case "codex": {
55
- for (const [event, cmds] of Object.entries(hooks).sort(([left], [right]) => compareStrings(left, right))) {
56
- if (!hookEventSupported("codex", event)) {
57
- unsupported("hooks", `codex: unsupported hook event "${event}"`);
58
- continue;
59
- }
60
- if (cmds.some((command) => command.blocking === false)) {
61
- unsupported("hooks", `codex: hook event "${event}" cannot execute asynchronous commands`);
62
- continue;
63
- }
64
- const entries = cmds.map((command) => {
65
- const handler = [
66
- `type = ${tomlStr("command")}`,
67
- `command = ${tomlStr(materializedHookCommand(event, command, add, plan))}`,
68
- ...(command.timeoutMs
69
- ? [`timeout = ${timeoutSeconds(command.timeoutMs)}`]
70
- : []),
71
- ];
72
- return `{ matcher = ${tomlStr(command.matcher ?? "*")}, hooks = [{ ${handler.join(", ")} }] }`;
73
- });
74
- plan.flags.push("-c", `hooks.${event}=[${entries.join(", ")}]`);
75
- }
76
- break;
77
- }
78
- case "opencode":
79
- unsupported("hooks", "opencode hooks require executable JS/TS plugins; command hooks cannot be translated exactly");
80
- break;
81
- case "kimi-code":
82
- unsupported("hooks", "kimi hooks are [[hooks]] in the config TOML loaded via --config-file, not cwd — pass --config-file");
83
- break;
84
- case "pi":
85
- unsupported("hooks", "pi hooks are TS extensions (.pi/extensions/*.ts) — build an extension");
86
- break;
87
- case "prime":
88
- unsupported("hooks", "prime hooks are TS extensions (fork core/extensions), not command hooks — build an extension");
89
- break;
90
- case "hermes":
91
- unsupported("hooks", "hermes project plugins do not expose an exact command-hook mapping");
92
- break;
93
- case "openclaw":
94
- unsupported("hooks", "openclaw hooks are DISABLED by default — enable in ~/.openclaw/openclaw.json; cwd hooks/ alone is a no-op");
95
- break;
96
- }
97
- }
98
- export function registerClaudeSettings(plan) {
99
- for (let index = 0; index < plan.flags.length - 1; index += 1) {
100
- if (plan.flags[index] === "--settings" &&
101
- plan.flags[index + 1] === CLAUDE_SETTINGS_PATH) {
102
- return;
103
- }
104
- }
105
- plan.flags.push("--settings", CLAUDE_SETTINGS_PATH);
106
- }
107
- const CLAUDE_HOOK_EVENTS = new Set([
108
- "ConfigChange",
109
- "CwdChanged",
110
- "Elicitation",
111
- "ElicitationResult",
112
- "FileChanged",
113
- "InstructionsLoaded",
114
- "MessageDisplay",
115
- "Notification",
116
- "PermissionDenied",
117
- "PermissionRequest",
118
- "PostCompact",
119
- "PostToolBatch",
120
- "PostToolUse",
121
- "PostToolUseFailure",
122
- "PreCompact",
123
- "PreToolUse",
124
- "SessionEnd",
125
- "SessionStart",
126
- "Setup",
127
- "Stop",
128
- "StopFailure",
129
- "SubagentStart",
130
- "SubagentStop",
131
- "TaskCompleted",
132
- "TaskCreated",
133
- "TeammateIdle",
134
- "UserPromptExpansion",
135
- "UserPromptSubmit",
136
- "WorktreeCreate",
137
- "WorktreeRemove",
138
- ]);
139
- const CODEX_HOOK_EVENTS = new Set([
140
- "PermissionRequest",
141
- "PostCompact",
142
- "PostToolUse",
143
- "PreCompact",
144
- "PreToolUse",
145
- "SessionStart",
146
- "Stop",
147
- "SubagentStart",
148
- "SubagentStop",
149
- "UserPromptSubmit",
150
- ]);
151
- const GEMINI_HOOK_EVENTS = new Set([
152
- "AfterAgent",
153
- "AfterModel",
154
- "AfterTool",
155
- "BeforeAgent",
156
- "BeforeModel",
157
- "BeforeTool",
158
- "BeforeToolSelection",
159
- "Notification",
160
- "PreCompress",
161
- "SessionEnd",
162
- "SessionStart",
163
- ]);
164
- function hookEventSupported(harness, event) {
165
- switch (harness) {
166
- case "claude-code":
167
- return CLAUDE_HOOK_EVENTS.has(event);
168
- case "codex":
169
- return CODEX_HOOK_EVENTS.has(event);
170
- case "gemini":
171
- return GEMINI_HOOK_EVENTS.has(event);
172
- default:
173
- return false;
174
- }
175
- }
176
- function timeoutSeconds(timeoutMs) {
177
- return Math.max(1, Math.ceil(timeoutMs / 1000));
178
- }
179
- function materializedHookCommand(event, command, add, plan) {
180
- if (!command.env || Object.keys(command.env).length === 0) {
181
- return command.command;
182
- }
183
- const wrapperId = canonicalCandidateDigest({
184
- event,
185
- command: command.command,
186
- env: Object.fromEntries(Object.entries(command.env).sort(([left], [right]) => compareStrings(left, right))),
187
- }).slice("sha256:".length);
188
- const path = `.tangle/profile-hooks/${wrapperId}.mjs`;
189
- const envSources = Object.fromEntries(Object.entries(command.env)
190
- .sort(([left], [right]) => compareStrings(left, right))
191
- .map(([name, value]) => {
192
- const sourceName = `TANGLE_PROFILE_HOOK_${sha256(`${wrapperId}\0${name}`)
193
- .slice(0, 24)
194
- .toUpperCase()}`;
195
- setPlanEnvironment(plan, sourceName, value);
196
- return [name, sourceName];
197
- }));
198
- add(path, hookWrapperSource("/bin/sh", ["-c", command.command], {}, envSources));
199
- return `node ${path}`;
200
- }
201
- // Classic plan env accepts string | AgentProfileConfigValue (public | secretRef)
202
- // in profile-plan.ts: keep resolved secrets out of workspace files by carrying
203
- // source names and resolving at runtime, failing closed with 127 when missing.
204
- // candidatePublicString rejects non-public values, so candidates inline them
205
- // to keep sealed bundles self-contained without a runtime environment channel.
206
- export function hookWrapperSource(executable, args, extraEnv, envSources) {
207
- if (envSources !== undefined && Object.keys(extraEnv).length > 0) {
208
- throw new Error("Hook wrapper cannot mix inline environment values and sources");
209
- }
210
- return [
211
- 'import { spawnSync } from "node:child_process"',
212
- `const executable = ${JSON.stringify(executable)}`,
213
- `const args = ${JSON.stringify(args)}`,
214
- ...(envSources === undefined
215
- ? [`const extraEnv = ${JSON.stringify(extraEnv)}`]
216
- : [
217
- `const envSources = ${JSON.stringify(envSources)}`,
218
- "const extraEnv = {}",
219
- "for (const [name, source] of Object.entries(envSources)) {",
220
- " const value = process.env[source]",
221
- " if (value === undefined) {",
222
- ' console.error("missing materialized hook environment " + source)',
223
- " process.exit(127)",
224
- " }",
225
- " extraEnv[name] = value",
226
- "}",
227
- ]),
228
- "const result = spawnSync(executable, args, {",
229
- ' stdio: "inherit",',
230
- " env: { ...process.env, ...extraEnv },",
231
- " shell: false,",
232
- "})",
233
- "if (result.error) {",
234
- " console.error(result.error.message)",
235
- " process.exit(127)",
236
- "}",
237
- "if (result.signal) {",
238
- ' console.error("hook terminated by " + result.signal)',
239
- " process.exit(1)",
240
- "}",
241
- "process.exit(result.status ?? 1)",
242
- "",
243
- ].join("\n");
244
- }
245
- import { canonicalCandidateDigest } from "@tangle-network/agent-interface";
246
- import { CLAUDE_SETTINGS_PATH } from "./harness.js";
247
- import { sha256 } from "./plan-digest.js";
248
- import { serializeWorkspacePlanJson, tomlStr } from "./plan-json.js";
249
- import { setPlanEnvironment } from "./profile-plan.js";
250
- import { compareStrings } from "./workspace-files.js";
@@ -1,4 +0,0 @@
1
- import type { WorkspacePlan, WorkspacePlanApplication } from "./plan-types.js";
2
- export declare function bindConcurrentLaunch(plan: WorkspacePlan, application: WorkspacePlanApplication, revisionWorkspaceDir: string, options?: {
3
- readonly secretOpenCodeRevision?: boolean;
4
- }): WorkspacePlanApplication["spawn"];
@@ -1,112 +0,0 @@
1
- import { dirname, join } from "node:path";
2
- import { CODEX_SYSTEM_PROMPT_FILE } from "./harness.js";
3
- import { bindWorkspaceLaunchFlags } from "./workspace-launch.js";
4
- /**
5
- * Paths a secret OpenCode revision serves from its own immutable tree:
6
- * read-only profile content under `reference/`, skills, and OpenCode's
7
- * standard agent and command definitions (OpenCode reads
8
- * `.opencode/{agent,agents}/**\/*.md` and `.opencode/{command,commands}/**\/*.md`).
9
- */
10
- const SECRET_REVISION_NATIVE_PATH = /^(?:reference\/|\.opencode\/skills\/|\.opencode\/(?:agents?|commands?)\/(?:[^./][^/]*\/)*[^./][^/]*\.md$)/u;
11
- export function bindConcurrentLaunch(plan, application, revisionWorkspaceDir, options = {}) {
12
- if (plan.requires?.length) {
13
- throw new Error(`Concurrent ${plan.harness} profile needs a native extension that this launcher cannot attest`);
14
- }
15
- if (application.spawn.systemPromptBinding) {
16
- throw new Error(`Concurrent ${plan.harness} profile has an unbound system prompt control`);
17
- }
18
- const flags = bindWorkspaceLaunchFlags(plan, application.spawn.flags, revisionWorkspaceDir);
19
- const env = { ...application.spawn.env };
20
- const supportedFiles = new Set();
21
- const bindFlagFile = (flag, relPath) => {
22
- const index = flags.findIndex((value, at) => value === flag && flags[at + 1] === relPath);
23
- if (index < 0)
24
- throw new Error(`Concurrent ${plan.harness} profile lacks ${flag} for ${relPath}`);
25
- flags[index + 1] = join(revisionWorkspaceDir, relPath);
26
- supportedFiles.add(relPath);
27
- };
28
- switch (plan.harness) {
29
- case "claude-code":
30
- for (const relPath of [
31
- ".tangle/claude-mcp.json",
32
- ".tangle/claude-settings.json",
33
- ]) {
34
- if (plan.files.some((file) => file.relPath === relPath)) {
35
- bindFlagFile(relPath.endsWith("mcp.json") ? "--mcp-config" : "--settings", relPath);
36
- }
37
- }
38
- break;
39
- case "codex":
40
- if (plan.files.some((file) => file.relPath === CODEX_SYSTEM_PROMPT_FILE)) {
41
- if (!plan.flags.some((value, at) => value === `model_instructions_file=${CODEX_SYSTEM_PROMPT_FILE}` &&
42
- plan.flags[at - 1] === "-c"))
43
- throw new Error("Concurrent codex profile lacks model_instructions_file binding");
44
- supportedFiles.add(CODEX_SYSTEM_PROMPT_FILE);
45
- }
46
- break;
47
- // kimi-code has no case: kimi-code 2.x discovers MCP only from
48
- // <cwd>/.kimi-code/mcp.json and has no launch flag that rebinds it to the
49
- // revision, so a concurrent kimi-code plan carrying one fails closed in
50
- // the redirect check below.
51
- case "opencode": {
52
- const runtimeConfig = application.runtimeOnlyFiles["opencode.json"];
53
- if (runtimeConfig !== undefined)
54
- env.OPENCODE_CONFIG_CONTENT = runtimeConfig;
55
- const configFile = plan.files.find((file) => file.relPath === "opencode.json");
56
- if (configFile) {
57
- env.OPENCODE_CONFIG = join(revisionWorkspaceDir, "opencode.json");
58
- env.TANGLE_PROFILE_REVISION_WORKSPACE = revisionWorkspaceDir;
59
- supportedFiles.add("opencode.json");
60
- const config = JSON.parse(configFile.content);
61
- if (Array.isArray(config.instructions)) {
62
- for (const entry of config.instructions) {
63
- const prefix = "{env:TANGLE_PROFILE_REVISION_WORKSPACE}/";
64
- if (typeof entry !== "string" || !entry.startsWith(prefix)) {
65
- throw new Error("Concurrent OpenCode instruction lacks an explicit revision binding");
66
- }
67
- supportedFiles.add(entry.slice(prefix.length));
68
- }
69
- }
70
- }
71
- break;
72
- }
73
- }
74
- // A declared executable reference/ tool runs from the immutable revision:
75
- // its directory, absolute in that revision, joins the launch PATH.
76
- const toolBinDirs = [];
77
- for (const file of plan.files) {
78
- // Secret OpenCode launches from its immutable revision workspace, with
79
- // that revision's .opencode as config directory. These relative native
80
- // resources therefore read the revision, never the shared workspace.
81
- if (options.secretOpenCodeRevision === true &&
82
- plan.harness === "opencode" &&
83
- (file.root ?? "workspace") === "workspace" &&
84
- SECRET_REVISION_NATIVE_PATH.test(file.relPath)) {
85
- if (file.relPath.startsWith("reference/") &&
86
- ((file.mode ?? 0) & 0o111) !== 0) {
87
- const dir = join(revisionWorkspaceDir, dirname(file.relPath));
88
- if (!toolBinDirs.includes(dir))
89
- toolBinDirs.push(dir);
90
- }
91
- continue;
92
- }
93
- if (file.source !== "generated") {
94
- throw new Error(`Concurrent ${plan.harness} profile resource ${file.relPath} needs a shared-workspace collision-safe copy`);
95
- }
96
- if ((file.root ?? "workspace") !== "workspace" ||
97
- !supportedFiles.has(file.relPath)) {
98
- throw new Error(`Concurrent ${plan.harness} profile cannot redirect native file ${file.relPath}; use a verified launch control`);
99
- }
100
- }
101
- for (const name of Object.keys(application.runtimeOnlyFiles)) {
102
- if (plan.harness !== "opencode" || name !== "opencode.json") {
103
- throw new Error(`Concurrent ${plan.harness} profile cannot bind runtime-only file ${name}`);
104
- }
105
- }
106
- return {
107
- ...application.spawn,
108
- flags,
109
- env,
110
- ...(toolBinDirs.length > 0 ? { toolBinDirs } : {}),
111
- };
112
- }
@@ -1,25 +0,0 @@
1
- import { lstatSync } from "node:fs";
2
- import type { AgentProfileSecretProvider } from "@tangle-network/agent-interface";
3
- import type { ManagedWorkspaceManifest } from "./managed-workspace-types.js";
4
- import type { ApplyWorkspacePlanOptions, WorkspacePlan, WorkspacePlanApplication } from "./plan-types.js";
5
- interface PreparedManagedWorkspace {
6
- /** Entire profile-owned tree. The caller owns its lifetime through the turn. */
7
- root: string;
8
- /** Use as the native harness cwd only inside an isolated workspace view. */
9
- workspaceDir: string;
10
- agentDir: string;
11
- manifest: ManagedWorkspaceManifest;
12
- application: WorkspacePlanApplication;
13
- dispose(): void;
14
- }
15
- export declare function assertPrivateManagedRoot(root: string): void;
16
- /**
17
- * Stage one complete plan under an executor-owned reserved root. The ordinary
18
- * materializer still resolves secrets and validates/writes every native file;
19
- * only its destination changes. A failed stage removes its private files.
20
- * The caller must keep the returned tree while an admitted turn uses it and
21
- * dispose it after that turn, so a later binding cannot expose old secrets.
22
- */
23
- export declare function prepareManagedWorkspacePlan(plan: WorkspacePlan, managedRoot: string, sharedWorkspaceDir: string, secretProvider: AgentProfileSecretProvider, options?: Pick<ApplyWorkspacePlanOptions, "ownership" | "runtimeOnlyFiles" | "secretFileSlotReferences">): Promise<PreparedManagedWorkspace>;
24
- export declare function lstatIfPresent(path: string): ReturnType<typeof lstatSync> | null;
25
- export {};
@@ -1,199 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { copyFileSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync, } from "node:fs";
3
- import { basename, dirname, extname, join, resolve, sep } from "node:path";
4
- import { applyWorkspacePlanWithSecretProvider } from "./plan-application.js";
5
- import { hashWorkspacePlan } from "./plan-digest.js";
6
- import { normalizeWorkspaceRelativePath } from "./workspace-files.js";
7
- import { bindWorkspaceLaunchFlags } from "./workspace-launch.js";
8
- export function assertPrivateManagedRoot(root) {
9
- if (!root || resolve(root) !== root) {
10
- throw new Error("Managed workspace root must be an absolute path");
11
- }
12
- const rootStat = lstatSync(root);
13
- if (!rootStat.isDirectory() ||
14
- realpathSync(root) !== root ||
15
- (rootStat.mode & 0o077) !== 0 ||
16
- rootStat.uid !== process.getuid?.()) {
17
- throw new Error("Managed workspace root must be a private real directory");
18
- }
19
- }
20
- /**
21
- * Stage one complete plan under an executor-owned reserved root. The ordinary
22
- * materializer still resolves secrets and validates/writes every native file;
23
- * only its destination changes. A failed stage removes its private files.
24
- * The caller must keep the returned tree while an admitted turn uses it and
25
- * dispose it after that turn, so a later binding cannot expose old secrets.
26
- */
27
- export async function prepareManagedWorkspacePlan(plan, managedRoot, sharedWorkspaceDir, secretProvider, options = {}) {
28
- assertPrivateManagedRoot(managedRoot);
29
- assertSharedWorkspaceRoot(sharedWorkspaceDir);
30
- const planDigest = hashWorkspacePlan(plan);
31
- const stage = mkdtempSync(join(managedRoot, `plan-${planDigest.slice(7)}-`));
32
- const workspaceDir = join(stage, "workspace");
33
- const agentDir = join(stage, "agent");
34
- try {
35
- mkdirSync(workspaceDir);
36
- mkdirSync(agentDir);
37
- const application = await applyWorkspacePlanWithSecretProvider(plan, workspaceDir, secretProvider, { ...options, agentDir, existingFiles: "reject" });
38
- const files = application.receipt.writtenFiles.map((written) => {
39
- const path = join(written.root === "agent" ? agentDir : workspaceDir, written.relPath);
40
- const stat = lstatSync(path);
41
- if (!stat.isFile() || stat.isSymbolicLink()) {
42
- throw new Error(`Managed workspace file is not regular: ${written.relPath}`);
43
- }
44
- const bytes = readFileSync(path);
45
- const authored = plan.files.find((file) => (file.root ?? "workspace") === written.root &&
46
- normalizeWorkspaceRelativePath(file.relPath) === written.relPath);
47
- return {
48
- ...written,
49
- source: authored?.source,
50
- ownership: authored?.ownership,
51
- size: bytes.length,
52
- mode: stat.mode & 0o777,
53
- visible: true,
54
- sha256: createHash("sha256").update(bytes).digest("hex"),
55
- };
56
- });
57
- const conflicts = [];
58
- for (const file of [...files]) {
59
- if (file.root !== "workspace")
60
- continue;
61
- const reason = sharedPathCollision(sharedWorkspaceDir, file.relPath);
62
- if (!reason)
63
- continue;
64
- const copyRelPath = sideCopyPath(sharedWorkspaceDir, workspaceDir, file.relPath, planDigest, reason);
65
- const copyPath = join(workspaceDir, copyRelPath);
66
- mkdirSync(dirname(copyPath), { recursive: true });
67
- copyFileSync(join(workspaceDir, file.relPath), copyPath);
68
- const copyStat = lstatSync(copyPath);
69
- const copyBytes = readFileSync(copyPath);
70
- file.visible = false;
71
- files.push({
72
- ...file,
73
- relPath: copyRelPath,
74
- relocatedFrom: file.relPath,
75
- size: copyBytes.length,
76
- mode: copyStat.mode & 0o777,
77
- visible: true,
78
- sha256: createHash("sha256").update(copyBytes).digest("hex"),
79
- });
80
- conflicts.push({ relPath: file.relPath, copyRelPath, reason });
81
- }
82
- files.sort((left, right) => `${left.root}:${left.relPath}`.localeCompare(`${right.root}:${right.relPath}`));
83
- conflicts.sort((left, right) => left.relPath.localeCompare(right.relPath));
84
- const runtimeOnlyFiles = Object.entries(application.runtimeOnlyFiles)
85
- .sort(([left], [right]) => left.localeCompare(right))
86
- .map(([name, content]) => ({
87
- name,
88
- size: Buffer.byteLength(content),
89
- sha256: createHash("sha256").update(content).digest("hex"),
90
- }));
91
- const spawnDigest = createHash("sha256")
92
- .update(JSON.stringify({
93
- ...application.spawn,
94
- flags: bindWorkspaceLaunchFlags(plan, application.spawn.flags),
95
- }))
96
- .digest("hex");
97
- const materializedDigest = `sha256:${createHash("sha256")
98
- .update(JSON.stringify({
99
- planDigest,
100
- files,
101
- conflicts,
102
- runtimeOnlyFiles,
103
- spawnDigest,
104
- }))
105
- .digest("hex")}`;
106
- const manifest = {
107
- workspacePlanDigest: planDigest,
108
- materializedDigest,
109
- files,
110
- conflicts,
111
- runtimeOnlyFiles,
112
- spawnDigest,
113
- };
114
- writeFileSync(join(stage, "manifest.json"), JSON.stringify(manifest), {
115
- mode: 0o600,
116
- flag: "wx",
117
- });
118
- return {
119
- root: stage,
120
- workspaceDir,
121
- agentDir,
122
- manifest,
123
- application,
124
- dispose: () => rmSync(stage, { recursive: true, force: true }),
125
- };
126
- }
127
- catch (error) {
128
- rmSync(stage, { recursive: true, force: true });
129
- throw error;
130
- }
131
- }
132
- export function lstatIfPresent(path) {
133
- try {
134
- return lstatSync(path);
135
- }
136
- catch (error) {
137
- if (error.code === "ENOENT")
138
- return null;
139
- throw error;
140
- }
141
- }
142
- function assertSharedWorkspaceRoot(root) {
143
- if (!root ||
144
- resolve(root) !== root ||
145
- !lstatSync(root).isDirectory() ||
146
- realpathSync(root) !== root) {
147
- throw new Error("Shared workspace must be an absolute real directory");
148
- }
149
- }
150
- function sharedPathCollision(root, relPath) {
151
- const target = resolve(root, relPath);
152
- if (target !== root && !target.startsWith(`${root}${sep}`)) {
153
- throw new Error(`Profile path escapes shared workspace: ${relPath}`);
154
- }
155
- let cursor = root;
156
- const segments = relPath.split(/[\\/]+/);
157
- for (const [index, segment] of segments.entries()) {
158
- cursor = join(cursor, segment);
159
- const stat = lstatIfPresent(cursor);
160
- if (!stat)
161
- return null;
162
- if (stat.isSymbolicLink())
163
- return "symlink";
164
- if (index < segments.length - 1 && !stat.isDirectory())
165
- return "existing";
166
- }
167
- return "existing";
168
- }
169
- function sideCopyPath(sharedRoot, stagedRoot, relPath, planDigest, reason) {
170
- const ancestor = reason === "symlink" ? firstSymlinkAncestor(sharedRoot, relPath) : null;
171
- const neighbor = ancestor
172
- ? join(dirname(ancestor), `${basename(ancestor)}.${relPath.slice(ancestor.length + 1).replace(/[\\/]+/g, ".")}`)
173
- : relPath;
174
- const name = basename(neighbor);
175
- const extension = extname(name);
176
- const stem = name.slice(0, name.length - extension.length);
177
- for (let attempt = 0; attempt < 100; attempt++) {
178
- const suffix = attempt ? `-${attempt}` : "";
179
- const candidate = join(dirname(neighbor), `${stem}.profile-${planDigest.slice(7, 19)}${suffix}${extension}`);
180
- if (!lstatIfPresent(join(stagedRoot, candidate)) &&
181
- !sharedPathCollision(sharedRoot, candidate)) {
182
- return candidate;
183
- }
184
- }
185
- throw new Error(`No unused profile copy path beside ${relPath}`);
186
- }
187
- function firstSymlinkAncestor(root, relPath) {
188
- const segments = relPath.split(/[\\/]+/);
189
- let cursor = root;
190
- for (const [index, segment] of segments.entries()) {
191
- if (index === segments.length - 1)
192
- break;
193
- cursor = join(cursor, segment);
194
- if (lstatIfPresent(cursor)?.isSymbolicLink()) {
195
- return segments.slice(0, index + 1).join(sep);
196
- }
197
- }
198
- return null;
199
- }
@@ -1,38 +0,0 @@
1
- import type { PlanFile, PlanFileRoot } from "./plan-types.js";
2
- export interface ManagedWorkspaceFileDigest {
3
- root: PlanFileRoot;
4
- relPath: string;
5
- source: PlanFile["source"];
6
- ownership: PlanFile["ownership"];
7
- size: number;
8
- mode: number;
9
- /** The host view exposes only visible entries for this pinned turn. */
10
- visible: boolean;
11
- /** A side copy of this original path, when a user file won the collision. */
12
- relocatedFrom?: string;
13
- /** Digest of the actual staged bytes, including resolved secret leaves. Private. */
14
- sha256: string;
15
- }
16
- /** Private executor inventory; never expose hashes of resolved credentials. */
17
- export interface ManagedWorkspaceManifest {
18
- workspacePlanDigest: string;
19
- /** Hashes the resolved stage inventory and launch inputs, not only public refs. */
20
- materializedDigest: string;
21
- files: ManagedWorkspaceFileDigest[];
22
- /** User-owned path stays in place; profile bytes are staged beside it. */
23
- conflicts: Array<{
24
- relPath: string;
25
- copyRelPath: string;
26
- reason: "existing" | "symlink";
27
- }>;
28
- /** Resolved for the runtime but intentionally absent from the visible tree. */
29
- runtimeOnlyFiles: Array<{
30
- name: string;
31
- size: number;
32
- sha256: string;
33
- }>;
34
- /** Private digest of resolved spawn controls, including secret-bearing env. */
35
- spawnDigest: string;
36
- /** Exact native spawn controls after absolute revision paths are bound. */
37
- launchDigest?: string;
38
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,37 +0,0 @@
1
- import type { AgentProfileSecretProvider } from "@tangle-network/agent-interface";
2
- import type { ManagedWorkspaceManifest } from "./managed-workspace-types.js";
3
- import type { ApplyWorkspacePlanOptions, WorkspacePlan, WorkspacePlanApplication } from "./plan-types.js";
4
- export type { ManagedWorkspaceFileDigest, ManagedWorkspaceManifest, } from "./managed-workspace-types.js";
5
- /** A private, write-once revision and the exact inputs for one native spawn. */
6
- export interface ConcurrentProfileRevision {
7
- directory: string;
8
- manifest: ManagedWorkspaceManifest;
9
- application: WorkspacePlanApplication;
10
- }
11
- /** Create the two executor-owned roots on one filesystem. */
12
- export declare function ensurePublicConcurrentProfileRoots(base: string): {
13
- managedRoot: string;
14
- launchRoot: string;
15
- };
16
- /**
17
- * Materialize PUBLIC profile material beside the shared workspace. Processes
18
- * in one sandbox share a uid and can read each other's revision files and env.
19
- * Callers must not use this path for confidential profile text or credentials.
20
- * Only native controls with a verified explicit launch path are admitted.
21
- * A separate reaper owns removal after no native process reads the revision.
22
- */
23
- export declare function preparePublicConcurrentProfileRevision(input: {
24
- plan: WorkspacePlan;
25
- managedRoot: string;
26
- /** Root-owned, agent-traversable publication root on the same filesystem. */
27
- launchRoot: string;
28
- sharedWorkspaceDir: string;
29
- ownerId: string;
30
- secretProvider: AgentProfileSecretProvider;
31
- options?: Pick<ApplyWorkspacePlanOptions, "ownership" | "runtimeOnlyFiles">;
32
- }): Promise<ConcurrentProfileRevision>;
33
- /** Move OpenCode's cwd-discovered primary agent into its explicit JSON config. */
34
- export declare function prepareExplicitOpenCodePlan(plan: WorkspacePlan): WorkspacePlan;
35
- export declare function listRevisionFiles(root: string, prefix?: string): string[];
36
- export declare function assertLaunchRoot(root: string): void;
37
- export declare function makeStageWritable(root: string): void;