@tangle-network/agent-profile-materialize 0.21.0 → 0.22.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.
@@ -6,6 +6,11 @@ export declare const OPENCODE_NATIVE_TOOL_IDS: readonly ["apply_patch", "bash",
6
6
  export declare function openCodeNativeToolId(name: string): string | undefined;
7
7
  export declare function openCodeToolName(name: string): string | undefined;
8
8
  export declare function openCodePermissionName(name: string): string | undefined;
9
+ /** OpenCode 1.x registers MCP tools as sanitized server + "_" + tool.
10
+ * Accept exact tools and a server-wide rule only for this profile's servers.
11
+ * A generic permission spelling must never become a grant to an undeclared MCP.
12
+ */
13
+ export declare function openCodeDeclaredMcpPermissionName(name: string, aliases: readonly string[]): string | undefined;
9
14
  export declare const PI_EXECUTION_CONTROL_EXTENSION_PATH = ".tangle/pi-execution-controls.ts";
10
15
  /**
11
16
  * Translate only portable spellings whose Pi equivalent differs.
@@ -68,6 +68,8 @@ export function openCodeToolName(name) {
68
68
  return openCodeNativeToolId(name);
69
69
  }
70
70
  export function openCodePermissionName(name) {
71
+ if (name.includes("*") || name.includes("?"))
72
+ return undefined;
71
73
  const normalized = normalizedControlName(name);
72
74
  if (["write", "edit"].includes(normalized))
73
75
  return "edit";
@@ -83,6 +85,36 @@ export function openCodePermissionName(name) {
83
85
  ? normalized
84
86
  : undefined;
85
87
  }
88
+ /** OpenCode 1.x registers MCP tools as sanitized server + "_" + tool.
89
+ * Accept exact tools and a server-wide rule only for this profile's servers.
90
+ * A generic permission spelling must never become a grant to an undeclared MCP.
91
+ */
92
+ export function openCodeDeclaredMcpPermissionName(name, aliases) {
93
+ const servers = aliases.map((alias) => ({
94
+ alias,
95
+ native: alias.replace(/[^a-zA-Z0-9_-]/g, "_"),
96
+ }));
97
+ const matches = servers.filter(({ native }) => name.startsWith(`${native}_`));
98
+ const match = matches[0];
99
+ if (!match)
100
+ return undefined;
101
+ const { native } = match;
102
+ const tool = name.slice(native.length + 1);
103
+ if (tool !== "*" && !/^[a-zA-Z0-9_-]+$/.test(tool))
104
+ return undefined;
105
+ // Colliding or nested prefixes cannot identify one server's authority:
106
+ // foo_* also matches foo_bar_read, including a server named foo_bar.
107
+ if (matches.length !== 1 ||
108
+ servers.some((server) => server !== matches[0] &&
109
+ (server.native.startsWith(`${native}_`) ||
110
+ native.startsWith(`${server.native}_`)))) {
111
+ throw new Error(`opencode: ambiguous declared MCP permission "${name}"`);
112
+ }
113
+ if (tool !== "*" && openCodePermissionName(name) !== undefined) {
114
+ throw new Error(`opencode: permission "${name}" names both a native and MCP tool`);
115
+ }
116
+ return name;
117
+ }
86
118
  export const PI_EXECUTION_CONTROL_EXTENSION_PATH = ".tangle/pi-execution-controls.ts";
87
119
  /**
88
120
  * Translate only portable spellings whose Pi equivalent differs.
@@ -1,4 +1,4 @@
1
- export declare function materializeExecutionControls(h: HarnessId, tools: Record<string, boolean> | undefined, permissions: Record<string, AgentProfilePermission> | undefined, modes: AgentProfile["modes"], plan: WorkspacePlan, add: AddPlanFile, unsupported: (d: Unsupported["dimension"], r: string) => void): void;
1
+ export declare function materializeExecutionControls(h: HarnessId, tools: Record<string, boolean> | undefined, permissions: Record<string, AgentProfilePermission> | undefined, modes: AgentProfile["modes"], plan: WorkspacePlan, add: AddPlanFile, unsupported: (d: Unsupported["dimension"], r: string) => void, mcpAliases?: readonly string[]): void;
2
2
  import type { AgentProfile, AgentProfilePermission } from "@tangle-network/agent-interface";
3
3
  import type { HarnessId } from "./harness.js";
4
4
  import type { AddPlanFile } from "./plan-json.js";
@@ -1,4 +1,4 @@
1
- export function materializeExecutionControls(h, tools, permissions, modes, plan, add, unsupported) {
1
+ export function materializeExecutionControls(h, tools, permissions, modes, plan, add, unsupported, mcpAliases = []) {
2
2
  const toolEntries = Object.entries(tools ?? {}).sort(([left], [right]) => compareStrings(left, right));
3
3
  const permissionEntries = Object.entries(permissions ?? {}).sort(([left], [right]) => compareStrings(left, right));
4
4
  switch (h) {
@@ -33,7 +33,8 @@ export function materializeExecutionControls(h, tools, permissions, modes, plan,
33
33
  return false;
34
34
  });
35
35
  const nativeTools = mapSupportedBooleanEntries(enforceableTools, openCodeToolName, (name) => unsupported("tools", `opencode: no verified native tool mapping for "${name}"`));
36
- const nativePermissions = mapSupportedPermissionEntries(enforceablePermissions, openCodePermissionName, (name) => unsupported("permissions", `opencode: no verified native permission mapping for "${name}"`), "opencode permission");
36
+ const nativePermissions = mapSupportedPermissionEntries(enforceablePermissions, (name) => openCodeDeclaredMcpPermissionName(name, mcpAliases) ??
37
+ openCodePermissionName(name), (name) => unsupported("permissions", `opencode: no verified native permission mapping for "${name}"`), "opencode permission");
37
38
  if (enforceableTools.length > 0 || permissionEntries.length > 0) {
38
39
  add(OPENCODE_CONFIG_FILE, serializeWorkspacePlanJson({
39
40
  $schema: OPENCODE_CONFIG_SCHEMA,
@@ -183,7 +184,7 @@ function piExecutionControlExtensionSource(enabledTools, excludedTools) {
183
184
  ].join("\n");
184
185
  }
185
186
  import { resolveNativeWebTools } from "@tangle-network/agent-interface";
186
- import { claudePermissionRules, claudeToolName, mapSupportedBooleanEntries, mapSupportedPermissionEntries, normalizedControlName, openCodePermissionName, openCodeToolName, PI_EXECUTION_CONTROL_EXTENSION_PATH, permissionAllowsEverything, piToolId, sealClaudePermissionRules, } from "./control-maps.js";
187
+ import { claudePermissionRules, claudeToolName, mapSupportedBooleanEntries, mapSupportedPermissionEntries, normalizedControlName, openCodeDeclaredMcpPermissionName, openCodePermissionName, openCodeToolName, PI_EXECUTION_CONTROL_EXTENSION_PATH, permissionAllowsEverything, piToolId, sealClaudePermissionRules, } from "./control-maps.js";
187
188
  import { CLAUDE_SETTINGS_PATH, OPENCODE_CONFIG_FILE, OPENCODE_CONFIG_SCHEMA, } from "./harness.js";
188
189
  import { registerClaudeSettings } from "./hooks.js";
189
190
  import { serializeWorkspacePlanJson } from "./plan-json.js";
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- /** Stable public facade for profile materialization and workspace application. */
2
1
  export { applyAgentCandidateWorkspacePlan, materializeCandidateProfile, } from "./candidate.js";
3
2
  export type { AgentCandidateWorkspacePlan, AgentCandidateWorkspacePlanReceipt, ApplyAgentCandidateWorkspacePlanOptions, MaterializeCandidateProfileOptions, } from "./candidate-types.js";
4
3
  export { OPENCODE_NATIVE_TOOL_IDS, openCodeNativeToolId, } from "./control-maps.js";
5
4
  export type { HarnessId } from "./harness.js";
6
5
  export { HARNESS_SKILL_DIRS, harnessAutoLoadsAgentContextFile, harnessAutoLoadsContextFile, harnessContextFile, isMaterializerHarness, MATERIALIZER_CONTEXT_PATHS, MATERIALIZER_HARNESSES, markMaterializerContextFile, skillDirForHarness, } from "./harness.js";
7
- export { joinSystemPromptAdditions, keepsHarnessSystemPrompt, } from "./harness-prompt.js";
6
+ export * from "./harness-prompt.js";
7
+ export { type ConcurrentProfileRevision, ensurePublicConcurrentProfileRoots, type ManagedWorkspaceFileDigest, type ManagedWorkspaceManifest, preparePublicConcurrentProfileRevision, } from "./managed-workspace.js";
8
8
  export type { OpenCodeMcpConfig } from "./mcp.js";
9
9
  export { openCodeMcpConfig } from "./mcp.js";
10
10
  export { modelIdsMatch, qualifyModelId, unqualifyModelId, } from "./model-id.js";
@@ -24,3 +24,4 @@ export { assertPrimeModelAgreement, hasPrimeProfileMaterial, primeProfileNeedsAg
24
24
  export { executionControlConflict, materializeProfile, UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER, } from "./profile-materializer.js";
25
25
  export type { ResolveAgentProfileResourcesOptions } from "./resource-resolution.js";
26
26
  export { normalizeSkillMd, resolveAgentProfileResources, } from "./resource-resolution.js";
27
+ export { prepareSecretConcurrentProfileRevision } from "./secret-concurrent-revision.js";
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- /** Stable public facade for profile materialization and workspace application. */
2
1
  export { applyAgentCandidateWorkspacePlan, materializeCandidateProfile, } from "./candidate.js";
3
2
  export { OPENCODE_NATIVE_TOOL_IDS, openCodeNativeToolId, } from "./control-maps.js";
4
3
  export { HARNESS_SKILL_DIRS, harnessAutoLoadsAgentContextFile, harnessAutoLoadsContextFile, harnessContextFile, isMaterializerHarness, MATERIALIZER_CONTEXT_PATHS, MATERIALIZER_HARNESSES, markMaterializerContextFile, skillDirForHarness, } from "./harness.js";
5
- export { joinSystemPromptAdditions, keepsHarnessSystemPrompt, } from "./harness-prompt.js";
4
+ export * from "./harness-prompt.js";
5
+ export { ensurePublicConcurrentProfileRoots, preparePublicConcurrentProfileRevision, } from "./managed-workspace.js";
6
6
  export { openCodeMcpConfig } from "./mcp.js";
7
7
  export { modelIdsMatch, qualifyModelId, unqualifyModelId, } from "./model-id.js";
8
8
  export { buildPiModelsConfig, buildPiModelsConfigFile, PI_MODELS_CONFIG_PATH, resolvePiRuntimePaths, } from "./pi-models-config.js";
@@ -16,3 +16,4 @@ export { matchPrimeKernelDeadMarker, PRIME_AGENT_DIR_ENV_VAR, PRIME_AGENT_DIR_EN
16
16
  export { assertPrimeModelAgreement, hasPrimeProfileMaterial, primeProfileNeedsAgentDir, primeSubagentEntryId, readPrimeProfileControls, } from "./prime-profile.js";
17
17
  export { executionControlConflict, materializeProfile, UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER, } from "./profile-materializer.js";
18
18
  export { normalizeSkillMd, resolveAgentProfileResources, } from "./resource-resolution.js";
19
+ export { prepareSecretConcurrentProfileRevision } from "./secret-concurrent-revision.js";
@@ -0,0 +1,4 @@
1
+ import type { WorkspacePlan, WorkspacePlanApplication } from "./plan-types.js";
2
+ export declare function bindConcurrentLaunch(plan: WorkspacePlan, application: WorkspacePlanApplication, revisionWorkspaceDir: string, options?: {
3
+ readonly secretOpenCodeRevisionCwd?: boolean;
4
+ }): WorkspacePlanApplication["spawn"];
@@ -0,0 +1,94 @@
1
+ import { join } from "node:path";
2
+ export function bindConcurrentLaunch(plan, application, revisionWorkspaceDir, options = {}) {
3
+ if (plan.requires?.length) {
4
+ throw new Error(`Concurrent ${plan.harness} profile needs a native extension that this launcher cannot attest`);
5
+ }
6
+ if (application.spawn.systemPromptBinding) {
7
+ throw new Error(`Concurrent ${plan.harness} profile has an unbound system prompt control`);
8
+ }
9
+ const flags = [...application.spawn.flags];
10
+ const env = { ...application.spawn.env };
11
+ const supportedFiles = new Set();
12
+ const bindFlagFile = (flag, relPath) => {
13
+ const index = flags.findIndex((value, at) => value === flag && flags[at + 1] === relPath);
14
+ if (index < 0)
15
+ throw new Error(`Concurrent ${plan.harness} profile lacks ${flag} for ${relPath}`);
16
+ flags[index + 1] = join(revisionWorkspaceDir, relPath);
17
+ supportedFiles.add(relPath);
18
+ };
19
+ switch (plan.harness) {
20
+ case "claude-code":
21
+ for (const relPath of [
22
+ ".tangle/claude-mcp.json",
23
+ ".tangle/claude-settings.json",
24
+ ]) {
25
+ if (plan.files.some((file) => file.relPath === relPath)) {
26
+ bindFlagFile(relPath.endsWith("mcp.json") ? "--mcp-config" : "--settings", relPath);
27
+ }
28
+ }
29
+ break;
30
+ case "codex":
31
+ if (plan.files.some((file) => file.relPath === ".codex/system-prompt.md")) {
32
+ const original = "model_instructions_file=.codex/system-prompt.md";
33
+ const index = flags.findIndex((value, at) => value === original && flags[at - 1] === "-c");
34
+ if (index < 0)
35
+ throw new Error("Concurrent codex profile lacks model_instructions_file binding");
36
+ flags[index] =
37
+ `model_instructions_file=${join(revisionWorkspaceDir, ".codex/system-prompt.md")}`;
38
+ supportedFiles.add(".codex/system-prompt.md");
39
+ }
40
+ break;
41
+ case "kimi-code":
42
+ if (plan.files.some((file) => file.relPath === ".kimi/mcp.json")) {
43
+ bindFlagFile("--mcp-config-file", ".kimi/mcp.json");
44
+ }
45
+ break;
46
+ case "opencode": {
47
+ const runtimeConfig = application.runtimeOnlyFiles["opencode.json"];
48
+ if (runtimeConfig !== undefined)
49
+ env.OPENCODE_CONFIG_CONTENT = runtimeConfig;
50
+ const configFile = plan.files.find((file) => file.relPath === "opencode.json");
51
+ if (configFile) {
52
+ env.OPENCODE_CONFIG = join(revisionWorkspaceDir, "opencode.json");
53
+ env.TANGLE_PROFILE_REVISION_WORKSPACE = revisionWorkspaceDir;
54
+ supportedFiles.add("opencode.json");
55
+ const config = JSON.parse(configFile.content);
56
+ if (Array.isArray(config.instructions)) {
57
+ for (const entry of config.instructions) {
58
+ const prefix = "{env:TANGLE_PROFILE_REVISION_WORKSPACE}/";
59
+ if (typeof entry !== "string" || !entry.startsWith(prefix)) {
60
+ throw new Error("Concurrent OpenCode instruction lacks an explicit revision binding");
61
+ }
62
+ supportedFiles.add(entry.slice(prefix.length));
63
+ }
64
+ }
65
+ }
66
+ break;
67
+ }
68
+ }
69
+ for (const file of plan.files) {
70
+ if (file.source !== "generated") {
71
+ if (options.secretOpenCodeRevisionCwd &&
72
+ plan.harness === "opencode" &&
73
+ (file.root ?? "workspace") === "workspace" &&
74
+ (file.relPath.startsWith("reference/") ||
75
+ file.relPath.startsWith(".opencode/skills/"))) {
76
+ // The secret launcher uses this immutable revision tree as native cwd.
77
+ // These content-only resources are therefore visible without writing
78
+ // into the shared workspace or changing native authority config.
79
+ continue;
80
+ }
81
+ throw new Error(`Concurrent ${plan.harness} profile resource ${file.relPath} needs a shared-workspace collision-safe copy`);
82
+ }
83
+ if ((file.root ?? "workspace") !== "workspace" ||
84
+ !supportedFiles.has(file.relPath)) {
85
+ throw new Error(`Concurrent ${plan.harness} profile cannot redirect native file ${file.relPath}; use a verified launch control`);
86
+ }
87
+ }
88
+ for (const name of Object.keys(application.runtimeOnlyFiles)) {
89
+ if (plan.harness !== "opencode" || name !== "opencode.json") {
90
+ throw new Error(`Concurrent ${plan.harness} profile cannot bind runtime-only file ${name}`);
91
+ }
92
+ }
93
+ return { ...application.spawn, flags, env };
94
+ }
@@ -0,0 +1,24 @@
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
+ export 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">): Promise<PreparedManagedWorkspace>;
24
+ export declare function lstatIfPresent(path: string): ReturnType<typeof lstatSync> | null;
@@ -0,0 +1,195 @@
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
+ export function assertPrivateManagedRoot(root) {
8
+ if (!root || resolve(root) !== root) {
9
+ throw new Error("Managed workspace root must be an absolute path");
10
+ }
11
+ const rootStat = lstatSync(root);
12
+ if (!rootStat.isDirectory() ||
13
+ realpathSync(root) !== root ||
14
+ (rootStat.mode & 0o077) !== 0 ||
15
+ rootStat.uid !== process.getuid?.()) {
16
+ throw new Error("Managed workspace root must be a private real directory");
17
+ }
18
+ }
19
+ /**
20
+ * Stage one complete plan under an executor-owned reserved root. The ordinary
21
+ * materializer still resolves secrets and validates/writes every native file;
22
+ * only its destination changes. A failed stage removes its private files.
23
+ * The caller must keep the returned tree while an admitted turn uses it and
24
+ * dispose it after that turn, so a later binding cannot expose old secrets.
25
+ */
26
+ export async function prepareManagedWorkspacePlan(plan, managedRoot, sharedWorkspaceDir, secretProvider, options = {}) {
27
+ assertPrivateManagedRoot(managedRoot);
28
+ assertSharedWorkspaceRoot(sharedWorkspaceDir);
29
+ const planDigest = hashWorkspacePlan(plan);
30
+ const stage = mkdtempSync(join(managedRoot, `plan-${planDigest.slice(7)}-`));
31
+ const workspaceDir = join(stage, "workspace");
32
+ const agentDir = join(stage, "agent");
33
+ try {
34
+ mkdirSync(workspaceDir);
35
+ mkdirSync(agentDir);
36
+ const application = await applyWorkspacePlanWithSecretProvider(plan, workspaceDir, secretProvider, { ...options, agentDir, existingFiles: "reject" });
37
+ const files = application.receipt.writtenFiles.map((written) => {
38
+ const path = join(written.root === "agent" ? agentDir : workspaceDir, written.relPath);
39
+ const stat = lstatSync(path);
40
+ if (!stat.isFile() || stat.isSymbolicLink()) {
41
+ throw new Error(`Managed workspace file is not regular: ${written.relPath}`);
42
+ }
43
+ const bytes = readFileSync(path);
44
+ const authored = plan.files.find((file) => (file.root ?? "workspace") === written.root &&
45
+ normalizeWorkspaceRelativePath(file.relPath) === written.relPath);
46
+ return {
47
+ ...written,
48
+ source: authored?.source,
49
+ ownership: authored?.ownership,
50
+ size: bytes.length,
51
+ mode: stat.mode & 0o777,
52
+ visible: true,
53
+ sha256: createHash("sha256").update(bytes).digest("hex"),
54
+ };
55
+ });
56
+ const conflicts = [];
57
+ for (const file of [...files]) {
58
+ if (file.root !== "workspace")
59
+ continue;
60
+ const reason = sharedPathCollision(sharedWorkspaceDir, file.relPath);
61
+ if (!reason)
62
+ continue;
63
+ const copyRelPath = sideCopyPath(sharedWorkspaceDir, workspaceDir, file.relPath, planDigest, reason);
64
+ const copyPath = join(workspaceDir, copyRelPath);
65
+ mkdirSync(dirname(copyPath), { recursive: true });
66
+ copyFileSync(join(workspaceDir, file.relPath), copyPath);
67
+ const copyStat = lstatSync(copyPath);
68
+ const copyBytes = readFileSync(copyPath);
69
+ file.visible = false;
70
+ files.push({
71
+ ...file,
72
+ relPath: copyRelPath,
73
+ relocatedFrom: file.relPath,
74
+ size: copyBytes.length,
75
+ mode: copyStat.mode & 0o777,
76
+ visible: true,
77
+ sha256: createHash("sha256").update(copyBytes).digest("hex"),
78
+ });
79
+ conflicts.push({ relPath: file.relPath, copyRelPath, reason });
80
+ }
81
+ files.sort((left, right) => `${left.root}:${left.relPath}`.localeCompare(`${right.root}:${right.relPath}`));
82
+ conflicts.sort((left, right) => left.relPath.localeCompare(right.relPath));
83
+ const runtimeOnlyFiles = Object.entries(application.runtimeOnlyFiles)
84
+ .sort(([left], [right]) => left.localeCompare(right))
85
+ .map(([name, content]) => ({
86
+ name,
87
+ size: Buffer.byteLength(content),
88
+ sha256: createHash("sha256").update(content).digest("hex"),
89
+ }));
90
+ const spawnDigest = createHash("sha256")
91
+ .update(JSON.stringify(application.spawn))
92
+ .digest("hex");
93
+ const materializedDigest = `sha256:${createHash("sha256")
94
+ .update(JSON.stringify({
95
+ planDigest,
96
+ files,
97
+ conflicts,
98
+ runtimeOnlyFiles,
99
+ spawnDigest,
100
+ }))
101
+ .digest("hex")}`;
102
+ const manifest = {
103
+ workspacePlanDigest: planDigest,
104
+ materializedDigest,
105
+ files,
106
+ conflicts,
107
+ runtimeOnlyFiles,
108
+ spawnDigest,
109
+ };
110
+ writeFileSync(join(stage, "manifest.json"), JSON.stringify(manifest), {
111
+ mode: 0o600,
112
+ flag: "wx",
113
+ });
114
+ return {
115
+ root: stage,
116
+ workspaceDir,
117
+ agentDir,
118
+ manifest,
119
+ application,
120
+ dispose: () => rmSync(stage, { recursive: true, force: true }),
121
+ };
122
+ }
123
+ catch (error) {
124
+ rmSync(stage, { recursive: true, force: true });
125
+ throw error;
126
+ }
127
+ }
128
+ export function lstatIfPresent(path) {
129
+ try {
130
+ return lstatSync(path);
131
+ }
132
+ catch (error) {
133
+ if (error.code === "ENOENT")
134
+ return null;
135
+ throw error;
136
+ }
137
+ }
138
+ function assertSharedWorkspaceRoot(root) {
139
+ if (!root ||
140
+ resolve(root) !== root ||
141
+ !lstatSync(root).isDirectory() ||
142
+ realpathSync(root) !== root) {
143
+ throw new Error("Shared workspace must be an absolute real directory");
144
+ }
145
+ }
146
+ function sharedPathCollision(root, relPath) {
147
+ const target = resolve(root, relPath);
148
+ if (target !== root && !target.startsWith(`${root}${sep}`)) {
149
+ throw new Error(`Profile path escapes shared workspace: ${relPath}`);
150
+ }
151
+ let cursor = root;
152
+ const segments = relPath.split(/[\\/]+/);
153
+ for (const [index, segment] of segments.entries()) {
154
+ cursor = join(cursor, segment);
155
+ const stat = lstatIfPresent(cursor);
156
+ if (!stat)
157
+ return null;
158
+ if (stat.isSymbolicLink())
159
+ return "symlink";
160
+ if (index < segments.length - 1 && !stat.isDirectory())
161
+ return "existing";
162
+ }
163
+ return "existing";
164
+ }
165
+ function sideCopyPath(sharedRoot, stagedRoot, relPath, planDigest, reason) {
166
+ const ancestor = reason === "symlink" ? firstSymlinkAncestor(sharedRoot, relPath) : null;
167
+ const neighbor = ancestor
168
+ ? join(dirname(ancestor), `${basename(ancestor)}.${relPath.slice(ancestor.length + 1).replace(/[\\/]+/g, ".")}`)
169
+ : relPath;
170
+ const name = basename(neighbor);
171
+ const extension = extname(name);
172
+ const stem = name.slice(0, name.length - extension.length);
173
+ for (let attempt = 0; attempt < 100; attempt++) {
174
+ const suffix = attempt ? `-${attempt}` : "";
175
+ const candidate = join(dirname(neighbor), `${stem}.profile-${planDigest.slice(7, 19)}${suffix}${extension}`);
176
+ if (!lstatIfPresent(join(stagedRoot, candidate)) &&
177
+ !sharedPathCollision(sharedRoot, candidate)) {
178
+ return candidate;
179
+ }
180
+ }
181
+ throw new Error(`No unused profile copy path beside ${relPath}`);
182
+ }
183
+ function firstSymlinkAncestor(root, relPath) {
184
+ const segments = relPath.split(/[\\/]+/);
185
+ let cursor = root;
186
+ for (const [index, segment] of segments.entries()) {
187
+ if (index === segments.length - 1)
188
+ break;
189
+ cursor = join(cursor, segment);
190
+ if (lstatIfPresent(cursor)?.isSymbolicLink()) {
191
+ return segments.slice(0, index + 1).join(sep);
192
+ }
193
+ }
194
+ return null;
195
+ }
@@ -0,0 +1,38 @@
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
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
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;
@@ -0,0 +1,283 @@
1
+ import { createHash } from "node:crypto";
2
+ import { chmodSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, writeFileSync, } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { bindConcurrentLaunch } from "./managed-workspace-bind.js";
5
+ import { assertPrivateManagedRoot, lstatIfPresent, prepareManagedWorkspacePlan, } from "./managed-workspace-stage.js";
6
+ import { assertWorkspacePlanSupported } from "./plan-support.js";
7
+ /** Create the two executor-owned roots on one filesystem. */
8
+ export function ensurePublicConcurrentProfileRoots(base) {
9
+ if (process.getuid?.() !== 0 || !base || resolve(base) !== base) {
10
+ throw new Error("Concurrent profile roots require an absolute path and a root executor");
11
+ }
12
+ if (!lstatIfPresent(base)) {
13
+ mkdirSync(base, { mode: 0o711 });
14
+ chmodSync(base, 0o711);
15
+ }
16
+ const managedRoot = join(base, "private");
17
+ const launchRoot = join(base, "launch");
18
+ if (!lstatIfPresent(managedRoot))
19
+ mkdirSync(managedRoot, { mode: 0o700 });
20
+ if (!lstatIfPresent(launchRoot)) {
21
+ mkdirSync(launchRoot, { mode: 0o711 });
22
+ chmodSync(launchRoot, 0o711);
23
+ }
24
+ assertPrivateManagedRoot(managedRoot);
25
+ assertLaunchRoot(launchRoot);
26
+ const baseStat = lstatSync(base);
27
+ if (!baseStat.isDirectory() ||
28
+ baseStat.isSymbolicLink() ||
29
+ realpathSync(base) !== base ||
30
+ baseStat.uid !== 0 ||
31
+ (baseStat.mode & 0o777) !== 0o711) {
32
+ throw new Error("Concurrent profile base must be a root-owned 0711 real directory");
33
+ }
34
+ return { managedRoot, launchRoot };
35
+ }
36
+ /**
37
+ * Materialize PUBLIC profile material beside the shared workspace. Processes
38
+ * in one sandbox share a uid and can read each other's revision files and env.
39
+ * Callers must not use this path for confidential profile text or credentials.
40
+ * Only native controls with a verified explicit launch path are admitted.
41
+ * A separate reaper owns removal after no native process reads the revision.
42
+ */
43
+ export async function preparePublicConcurrentProfileRevision(input) {
44
+ if (!input.ownerId)
45
+ throw new Error("Concurrent profile revision needs a binding owner");
46
+ if (input.options?.ownership) {
47
+ throw new Error("Concurrent profile files must remain executor-owned and read-only");
48
+ }
49
+ if (input.plan.files.some((file) => (file.secretSlots?.length ?? 0) > 0) ||
50
+ Object.values(input.plan.env).some((value) => typeof value !== "string") ||
51
+ input.plan.flags.some((value) => typeof value !== "string")) {
52
+ throw new Error("Concurrent profile secrets need per-profile process identity; this sandbox has one subprocess identity");
53
+ }
54
+ const plan = prepareExplicitOpenCodePlan(input.plan);
55
+ assertWorkspacePlanSupported(plan);
56
+ assertLaunchRoot(input.launchRoot);
57
+ const prepared = await prepareManagedWorkspacePlan(plan, input.managedRoot, input.sharedWorkspaceDir, input.secretProvider, input.options);
58
+ try {
59
+ const identity = createHash("sha256")
60
+ .update(`${input.ownerId}\0${prepared.manifest.materializedDigest}`)
61
+ .digest("hex");
62
+ const directory = join(input.launchRoot, `revision-${identity}`);
63
+ // Validate every native file before publishing a revision that a launcher
64
+ // could mistake for a complete profile.
65
+ const spawn = bindConcurrentLaunch(plan, prepared.application, join(directory, "workspace"));
66
+ prepared.manifest.launchDigest = createHash("sha256")
67
+ .update(JSON.stringify(spawn))
68
+ .digest("hex");
69
+ writeFileSync(join(prepared.root, "manifest.json"), JSON.stringify(prepared.manifest));
70
+ const existing = lstatIfPresent(directory);
71
+ if (existing) {
72
+ assertPublishedRevision(directory, prepared.manifest);
73
+ prepared.dispose();
74
+ }
75
+ else {
76
+ try {
77
+ // Keep the top directory private and writable for rename. The child
78
+ // tree is complete and read-only before publication.
79
+ makeRevisionReadOnly(prepared.root, false);
80
+ renameSync(prepared.root, directory);
81
+ // This is the publication point: agents cannot traverse the root
82
+ // before its mode changes from 0700 to 0555.
83
+ chmodSync(directory, 0o555);
84
+ }
85
+ catch (error) {
86
+ const raced = lstatIfPresent(directory);
87
+ if (!raced?.isDirectory() || raced.isSymbolicLink())
88
+ throw error;
89
+ assertPublishedRevision(directory, prepared.manifest);
90
+ makeStageWritable(prepared.root);
91
+ prepared.dispose();
92
+ }
93
+ }
94
+ return {
95
+ directory,
96
+ manifest: prepared.manifest,
97
+ application: { ...prepared.application, spawn },
98
+ };
99
+ }
100
+ catch (error) {
101
+ // A published revision is never removed here; only an unpublished stage is.
102
+ if (lstatIfPresent(prepared.root)) {
103
+ makeStageWritable(prepared.root);
104
+ prepared.dispose();
105
+ }
106
+ throw error;
107
+ }
108
+ }
109
+ /** Move OpenCode's cwd-discovered primary agent into its explicit JSON config. */
110
+ export function prepareExplicitOpenCodePlan(plan) {
111
+ if (plan.harness !== "opencode")
112
+ return plan;
113
+ for (const name of [
114
+ "OPENCODE_CONFIG",
115
+ "OPENCODE_CONFIG_CONTENT",
116
+ "TANGLE_PROFILE_REVISION_WORKSPACE",
117
+ ]) {
118
+ if (Object.hasOwn(plan.env, name)) {
119
+ throw new Error(`Concurrent OpenCode reserves ${name} for its launch binding`);
120
+ }
121
+ }
122
+ const configFile = plan.files.find((file) => file.relPath === "opencode.json");
123
+ if (configFile?.source !== "generated" ||
124
+ (configFile.root ?? "workspace") !== "workspace") {
125
+ throw new Error("Concurrent OpenCode requires a generated opencode.json");
126
+ }
127
+ const config = JSON.parse(configFile.content);
128
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
129
+ throw new Error("Concurrent OpenCode config must be an object");
130
+ }
131
+ const body = config;
132
+ let files = [...plan.files];
133
+ if (plan.systemPrompt !== undefined) {
134
+ const agentFlag = plan.flags.indexOf("--agent");
135
+ const agentName = plan.flags[agentFlag + 1];
136
+ if (agentFlag < 0 ||
137
+ typeof agentName !== "string" ||
138
+ !/^tangle-profile-[a-f0-9]{16}$/.test(agentName)) {
139
+ throw new Error("Concurrent OpenCode requires the materializer's primary agent selection");
140
+ }
141
+ const primaryPath = `.opencode/agents/${agentName}.md`;
142
+ const primary = files.find((file) => file.relPath === primaryPath);
143
+ if (primary?.source !== "generated" ||
144
+ (primary.root ?? "workspace") !== "workspace") {
145
+ throw new Error("Concurrent OpenCode primary agent must be generated");
146
+ }
147
+ files = files.filter((file) => file !== primary);
148
+ const agents = body.agent;
149
+ if (agents !== undefined &&
150
+ (typeof agents !== "object" || agents === null || Array.isArray(agents))) {
151
+ throw new Error("Concurrent OpenCode agent config must be an object");
152
+ }
153
+ if (agents && Object.hasOwn(agents, agentName)) {
154
+ throw new Error("Concurrent OpenCode primary agent collides with config");
155
+ }
156
+ body.agent = {
157
+ ...agents,
158
+ [agentName]: { mode: "primary", prompt: plan.systemPrompt },
159
+ };
160
+ }
161
+ const instructions = body.instructions;
162
+ if (instructions !== undefined) {
163
+ if (!Array.isArray(instructions) ||
164
+ instructions.some((entry) => typeof entry !== "string")) {
165
+ throw new Error("Concurrent OpenCode instructions must be generated file paths");
166
+ }
167
+ body.instructions = instructions.map((relPath) => {
168
+ const file = files.find((candidate) => candidate.relPath === relPath);
169
+ if (file?.source !== "generated" ||
170
+ (file.root ?? "workspace") !== "workspace") {
171
+ throw new Error(`Concurrent OpenCode cannot redirect instruction ${relPath}`);
172
+ }
173
+ return `{env:TANGLE_PROFILE_REVISION_WORKSPACE}/${relPath}`;
174
+ });
175
+ }
176
+ return {
177
+ ...plan,
178
+ files: files.map((file) => file === configFile ? { ...file, content: JSON.stringify(body) } : file),
179
+ };
180
+ }
181
+ function assertPublishedRevision(directory, expected) {
182
+ const root = lstatSync(directory);
183
+ if (!root.isDirectory() ||
184
+ root.isSymbolicLink() ||
185
+ (root.mode & 0o777) !== 0o555) {
186
+ throw new Error("Concurrent profile revision is not fully published");
187
+ }
188
+ const persisted = JSON.parse(readFileSync(join(directory, "manifest.json"), "utf8"));
189
+ if (persisted.materializedDigest !== expected.materializedDigest ||
190
+ persisted.launchDigest !== expected.launchDigest) {
191
+ throw new Error("Concurrent profile revision digest collision");
192
+ }
193
+ for (const file of expected.files) {
194
+ const path = join(directory, file.root, file.relPath);
195
+ const stat = lstatSync(path);
196
+ if (!stat.isFile() ||
197
+ stat.isSymbolicLink() ||
198
+ (stat.mode & 0o222) !== 0 ||
199
+ createHash("sha256").update(readFileSync(path)).digest("hex") !==
200
+ file.sha256) {
201
+ throw new Error(`Concurrent profile revision file changed: ${file.relPath}`);
202
+ }
203
+ }
204
+ const expectedPaths = new Set(expected.files.map((file) => `${file.root}/${file.relPath}`));
205
+ for (const rootName of ["workspace", "agent"]) {
206
+ for (const relPath of listRevisionFiles(join(directory, rootName))) {
207
+ if (!expectedPaths.delete(`${rootName}/${relPath}`)) {
208
+ throw new Error(`Concurrent profile revision has an unexpected file: ${rootName}/${relPath}`);
209
+ }
210
+ }
211
+ }
212
+ if (expectedPaths.size) {
213
+ throw new Error("Concurrent profile revision is missing a manifest file");
214
+ }
215
+ }
216
+ export function listRevisionFiles(root, prefix = "") {
217
+ const found = [];
218
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
219
+ const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
220
+ if (entry.isDirectory())
221
+ found.push(...listRevisionFiles(join(root, entry.name), relPath));
222
+ else if (entry.isFile())
223
+ found.push(relPath);
224
+ else
225
+ throw new Error(`Concurrent profile revision has a non-regular path: ${relPath}`);
226
+ }
227
+ return found;
228
+ }
229
+ export function assertLaunchRoot(root) {
230
+ if (!root || resolve(root) !== root) {
231
+ throw new Error("Concurrent profile launch root must be absolute");
232
+ }
233
+ const stat = lstatSync(root);
234
+ if (!stat.isDirectory() ||
235
+ stat.isSymbolicLink() ||
236
+ realpathSync(root) !== root ||
237
+ (stat.mode & 0o022) !== 0 ||
238
+ (stat.mode & 0o111) !== 0o111 ||
239
+ stat.uid !== process.getuid?.()) {
240
+ throw new Error("Concurrent profile launch root must be executor-owned, real, traversable, and not writable by agent users");
241
+ }
242
+ let ancestor = dirname(root);
243
+ while (ancestor !== root) {
244
+ const ancestorStat = lstatSync(ancestor);
245
+ if (!ancestorStat.isDirectory() ||
246
+ ancestorStat.isSymbolicLink() ||
247
+ (ancestorStat.mode & 0o001) === 0) {
248
+ throw new Error(`Concurrent profile launch root has a non-traversable ancestor: ${ancestor}`);
249
+ }
250
+ if (ancestor === dirname(ancestor))
251
+ break;
252
+ ancestor = dirname(ancestor);
253
+ }
254
+ }
255
+ function makeRevisionReadOnly(root, publishRoot = true) {
256
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
257
+ const path = join(root, entry.name);
258
+ if (entry.isDirectory()) {
259
+ makeRevisionReadOnly(path);
260
+ }
261
+ else if (entry.isFile()) {
262
+ const mode = lstatSync(path).mode & 0o111;
263
+ chmodSync(path, mode ? 0o555 : 0o444);
264
+ }
265
+ else {
266
+ throw new Error(`Concurrent profile revision contains a non-regular path: ${entry.name}`);
267
+ }
268
+ }
269
+ if (publishRoot)
270
+ chmodSync(root, 0o555);
271
+ }
272
+ export function makeStageWritable(root) {
273
+ if (!lstatIfPresent(root))
274
+ return;
275
+ chmodSync(root, 0o700);
276
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
277
+ const path = join(root, entry.name);
278
+ if (entry.isDirectory())
279
+ makeStageWritable(path);
280
+ else if (entry.isFile())
281
+ chmodSync(path, 0o600);
282
+ }
283
+ }
@@ -11,7 +11,7 @@ export declare const UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER: AgentProfileSecr
11
11
  * user's turn materializes it. Controls a harness cannot enforce are not
12
12
  * contradictions: `resources.failOnError` decides those per turn.
13
13
  */
14
- export declare function executionControlConflict(profile: Pick<AgentProfile, "tools" | "permissions">, harness: HarnessId): string | undefined;
14
+ export declare function executionControlConflict(profile: Pick<AgentProfile, "tools" | "permissions" | "mcp">, harness: HarnessId): string | undefined;
15
15
  /**
16
16
  * Build a pure workspace plan for one canonical harness.
17
17
  * The caller applies files, environment, and flags at its process boundary.
@@ -26,7 +26,7 @@ export const UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER = Object.freeze({
26
26
  export function executionControlConflict(profile, harness) {
27
27
  const builder = createProfilePlanBuilder(harness);
28
28
  try {
29
- materializeExecutionControls(harness, profile.tools, profile.permissions, undefined, builder.plan, builder.add, builder.unsupported);
29
+ materializeExecutionControls(harness, profile.tools, profile.permissions, undefined, builder.plan, builder.add, builder.unsupported, Object.keys(activeMcpServers(profile.mcp)));
30
30
  }
31
31
  catch (error) {
32
32
  return error instanceof Error ? error.message : String(error);
@@ -53,7 +53,7 @@ export function materializeProfile(inputProfile, harness, opts = {}) {
53
53
  const builder = createProfilePlanBuilder(harness);
54
54
  lowerProfileResources(profile, harness, builder);
55
55
  lowerProfilePrompt(profile, harness, binds, instructionsRoot, builder);
56
- materializeExecutionControls(harness, profile.tools, profile.permissions, profile.modes, builder.plan, builder.add, builder.unsupported);
56
+ materializeExecutionControls(harness, profile.tools, profile.permissions, profile.modes, builder.plan, builder.add, builder.unsupported, skip.has("mcp") ? [] : Object.keys(activeMcpServers(profile.mcp)));
57
57
  if ((profile.connections?.length ?? 0) > 0) {
58
58
  builder.unsupported("connections", `${harness}: Hub connections must be resolved to authorized MCP servers by the execution runtime`);
59
59
  }
@@ -0,0 +1,21 @@
1
+ import type { AgentProfileSecretProvider } from "@tangle-network/agent-interface";
2
+ import { type ConcurrentProfileRevision } from "./managed-workspace.js";
3
+ import type { ApplyWorkspacePlanOptions, WorkspacePlan } from "./plan-types.js";
4
+ /**
5
+ * Publish a resolved revision for exactly one native subprocess UID. The
6
+ * executor-owned naming key hides low-entropy credential guesses from paths;
7
+ * the resolved digest and manifest never leave this server-side result.
8
+ */
9
+ export declare function prepareSecretConcurrentProfileRevision(input: {
10
+ plan: WorkspacePlan;
11
+ /** Trusted executor transport only; never read from the mutable native HOME. */
12
+ providerConfig?: Record<string, unknown>;
13
+ managedRoot: string;
14
+ launchRoot: string;
15
+ sharedWorkspaceDir: string;
16
+ ownerId: string;
17
+ subprocessUid: number;
18
+ namingKey: Uint8Array;
19
+ secretProvider: AgentProfileSecretProvider;
20
+ options?: Pick<ApplyWorkspacePlanOptions, "runtimeOnlyFiles">;
21
+ }): Promise<ConcurrentProfileRevision>;
@@ -0,0 +1,180 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createHash, createHmac } from "node:crypto";
3
+ import { chmodSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { assertLaunchRoot, listRevisionFiles, makeStageWritable, prepareExplicitOpenCodePlan, } from "./managed-workspace.js";
6
+ import { bindConcurrentLaunch } from "./managed-workspace-bind.js";
7
+ import { lstatIfPresent, prepareManagedWorkspacePlan, } from "./managed-workspace-stage.js";
8
+ import { assertWorkspacePlanSupported } from "./plan-support.js";
9
+ /**
10
+ * Publish a resolved revision for exactly one native subprocess UID. The
11
+ * executor-owned naming key hides low-entropy credential guesses from paths;
12
+ * the resolved digest and manifest never leave this server-side result.
13
+ */
14
+ export async function prepareSecretConcurrentProfileRevision(input) {
15
+ if (process.platform !== "linux" ||
16
+ process.getuid?.() !== 0 ||
17
+ !Number.isSafeInteger(input.subprocessUid) ||
18
+ input.subprocessUid < 200_000 ||
19
+ input.namingKey.length !== 32 ||
20
+ !input.ownerId) {
21
+ throw new Error("Secret concurrent profile needs a root executor and reserved identity");
22
+ }
23
+ let plan = prepareExplicitOpenCodePlan(input.plan);
24
+ if (input.providerConfig) {
25
+ plan = {
26
+ ...plan,
27
+ files: plan.files.map((file) => file.relPath === "opencode.json" &&
28
+ file.source === "generated" &&
29
+ (file.root ?? "workspace") === "workspace"
30
+ ? {
31
+ ...file,
32
+ content: JSON.stringify({
33
+ ...JSON.parse(file.content),
34
+ provider: input.providerConfig,
35
+ }),
36
+ }
37
+ : file),
38
+ };
39
+ }
40
+ if (plan.files.some((file) => file.relPath.startsWith(".config/opencode/"))) {
41
+ throw new Error("Secret OpenCode reserves its native config directory");
42
+ }
43
+ assertWorkspacePlanSupported(plan);
44
+ assertLaunchRoot(input.launchRoot);
45
+ const prepared = await prepareManagedWorkspacePlan(plan, input.managedRoot, input.sharedWorkspaceDir, input.secretProvider, input.options);
46
+ try {
47
+ // XDG_CONFIG_HOME is bound to this executor-owned tree at native launch.
48
+ // An agent-owned HOME may hold SQLite state, never native authority.
49
+ mkdirSync(join(prepared.root, "workspace", ".config", "opencode"), {
50
+ recursive: true,
51
+ mode: 0o700,
52
+ });
53
+ const identity = createHmac("sha256", input.namingKey)
54
+ .update(input.ownerId)
55
+ .update("\0")
56
+ .update(String(input.subprocessUid))
57
+ .update("\0")
58
+ .update(prepared.manifest.materializedDigest)
59
+ .digest("hex");
60
+ const directory = join(input.launchRoot, `revision-${identity}`);
61
+ const spawn = bindConcurrentLaunch(plan, prepared.application, join(directory, "workspace"), { secretOpenCodeRevisionCwd: true });
62
+ prepared.manifest.launchDigest = createHash("sha256")
63
+ .update(JSON.stringify(spawn))
64
+ .digest("hex");
65
+ writeFileSync(join(prepared.root, "manifest.json"), JSON.stringify(prepared.manifest));
66
+ const existing = lstatIfPresent(directory);
67
+ if (existing) {
68
+ assertPublishedSecretRevision(directory, prepared.manifest);
69
+ prepared.dispose();
70
+ }
71
+ else {
72
+ try {
73
+ makeSecretRevisionReadOnly(prepared.root, input.subprocessUid);
74
+ renameSync(prepared.root, directory);
75
+ }
76
+ catch (error) {
77
+ const raced = lstatIfPresent(directory);
78
+ if (!raced?.isDirectory() || raced.isSymbolicLink())
79
+ throw error;
80
+ assertPublishedSecretRevision(directory, prepared.manifest);
81
+ makeStageWritable(prepared.root);
82
+ prepared.dispose();
83
+ }
84
+ }
85
+ return {
86
+ directory,
87
+ manifest: prepared.manifest,
88
+ application: { ...prepared.application, spawn },
89
+ };
90
+ }
91
+ catch (error) {
92
+ if (lstatIfPresent(prepared.root)) {
93
+ makeStageWritable(prepared.root);
94
+ prepared.dispose();
95
+ }
96
+ throw error;
97
+ }
98
+ }
99
+ function secretRevisionPaths(root) {
100
+ const all = [root];
101
+ const exposed = [root];
102
+ const walk = (directory) => {
103
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
104
+ const child = join(directory, entry.name);
105
+ const stat = lstatSync(child);
106
+ if (stat.uid !== 0 || stat.isSymbolicLink()) {
107
+ throw new Error("Secret concurrent profile stage contains an untrusted inode");
108
+ }
109
+ if (!stat.isDirectory() && !stat.isFile()) {
110
+ throw new Error("Secret concurrent profile stage contains a special file");
111
+ }
112
+ all.push(child);
113
+ if (entry.name !== "manifest.json" || directory !== root) {
114
+ exposed.push(child);
115
+ }
116
+ if (stat.isDirectory())
117
+ walk(child);
118
+ }
119
+ };
120
+ walk(root);
121
+ return { all, exposed };
122
+ }
123
+ function runSetfacl(args, paths) {
124
+ for (let start = 0; start < paths.length; start += 64) {
125
+ execFileSync("/usr/bin/setfacl", [...args, "--", ...paths.slice(start, start + 64)], {
126
+ env: { PATH: "/usr/bin:/bin", LANG: "C" },
127
+ timeout: 30_000,
128
+ stdio: "pipe",
129
+ });
130
+ }
131
+ }
132
+ function makeSecretRevisionReadOnly(root, uid) {
133
+ const paths = secretRevisionPaths(root);
134
+ // Remove inherited named ACLs before widening the ACL mask for this UID.
135
+ runSetfacl(["-b"], paths.all);
136
+ for (const path of paths.all) {
137
+ const stat = lstatSync(path);
138
+ chmodSync(path, stat.isDirectory() ? 0o500 : stat.mode & 0o111 ? 0o500 : 0o400);
139
+ }
140
+ // The private manifest has resolved-file hashes. The CLI can read its native
141
+ // files, but never the executor inventory or another revision's files.
142
+ runSetfacl(["-m", `u:${uid}:r-X`], paths.exposed);
143
+ }
144
+ function assertPublishedSecretRevision(directory, expected) {
145
+ const root = lstatSync(directory);
146
+ if (!root.isDirectory() ||
147
+ root.isSymbolicLink() ||
148
+ root.uid !== 0 ||
149
+ (root.mode & 0o222) !== 0) {
150
+ throw new Error("Secret concurrent profile revision is not immutable");
151
+ }
152
+ const persisted = JSON.parse(readFileSync(join(directory, "manifest.json"), "utf8"));
153
+ if (persisted.materializedDigest !== expected.materializedDigest ||
154
+ persisted.launchDigest !== expected.launchDigest) {
155
+ throw new Error("Secret concurrent profile revision digest collision");
156
+ }
157
+ for (const file of expected.files) {
158
+ const filePath = join(directory, file.root, file.relPath);
159
+ const stat = lstatSync(filePath);
160
+ if (!stat.isFile() ||
161
+ stat.isSymbolicLink() ||
162
+ stat.uid !== 0 ||
163
+ (stat.mode & 0o222) !== 0 ||
164
+ createHash("sha256").update(readFileSync(filePath)).digest("hex") !==
165
+ file.sha256) {
166
+ throw new Error("Secret concurrent profile revision file changed");
167
+ }
168
+ }
169
+ const expectedPaths = new Set(expected.files.map((file) => `${file.root}/${file.relPath}`));
170
+ for (const rootName of ["workspace", "agent"]) {
171
+ for (const relPath of listRevisionFiles(join(directory, rootName))) {
172
+ if (!expectedPaths.delete(`${rootName}/${relPath}`)) {
173
+ throw new Error("Secret concurrent profile revision has an unexpected file");
174
+ }
175
+ }
176
+ }
177
+ if (expectedPaths.size) {
178
+ throw new Error("Secret concurrent profile revision is missing a manifest file");
179
+ }
180
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-profile-materialize",
3
- "version": "0.21.0",
3
+ "version": "0.22.1",
4
4
  "description": "Materialize an AgentProfile into the native per-harness workspace files (the one shared materializer for cli-bridge, agent-dev-container, and VB).",
5
5
  "type": "module",
6
6
  "repository": {