@tangle-network/agent-profile-materialize 0.15.2 → 0.17.0

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 (57) hide show
  1. package/dist/candidate-types.d.ts +32 -0
  2. package/dist/candidate-types.js +1 -0
  3. package/dist/candidate.d.ts +9 -0
  4. package/dist/candidate.js +279 -0
  5. package/dist/commands.d.ts +2 -0
  6. package/dist/commands.js +32 -0
  7. package/dist/control-maps.d.ts +34 -0
  8. package/dist/control-maps.js +185 -0
  9. package/dist/execution-controls.d.ts +5 -0
  10. package/dist/execution-controls.js +186 -0
  11. package/dist/harness.d.ts +81 -0
  12. package/dist/harness.js +167 -0
  13. package/dist/hooks.d.ts +6 -0
  14. package/dist/hooks.js +261 -0
  15. package/dist/index.d.ts +24 -623
  16. package/dist/index.js +15 -3733
  17. package/dist/mcp.d.ts +22 -0
  18. package/dist/mcp.js +244 -0
  19. package/dist/pi-models-config.d.ts +30 -0
  20. package/dist/pi-models-config.js +46 -0
  21. package/dist/plan-application.d.ts +10 -0
  22. package/dist/plan-application.js +152 -0
  23. package/dist/plan-digest.d.ts +18 -0
  24. package/dist/plan-digest.js +90 -0
  25. package/dist/plan-json.d.ts +18 -0
  26. package/dist/plan-json.js +91 -0
  27. package/dist/plan-normalize.d.ts +13 -0
  28. package/dist/plan-normalize.js +156 -0
  29. package/dist/plan-prompt.d.ts +38 -0
  30. package/dist/plan-prompt.js +49 -0
  31. package/dist/plan-support.d.ts +9 -0
  32. package/dist/plan-support.js +22 -0
  33. package/dist/plan-types.d.ts +294 -0
  34. package/dist/plan-types.js +106 -0
  35. package/dist/prime-agent-dir.d.ts +47 -0
  36. package/dist/prime-agent-dir.js +164 -0
  37. package/dist/prime-profile.d.ts +88 -0
  38. package/dist/prime-profile.js +284 -0
  39. package/dist/profile-materializer.d.ts +12 -0
  40. package/dist/profile-materializer.js +59 -0
  41. package/dist/profile-plan.d.ts +14 -0
  42. package/dist/profile-plan.js +86 -0
  43. package/dist/profile-prompt.d.ts +4 -0
  44. package/dist/profile-prompt.js +105 -0
  45. package/dist/profile-support.d.ts +8 -0
  46. package/dist/profile-support.js +6 -0
  47. package/dist/resource-lowering.d.ts +7 -0
  48. package/dist/resource-lowering.js +103 -0
  49. package/dist/resource-resolution.d.ts +27 -0
  50. package/dist/resource-resolution.js +241 -0
  51. package/dist/subagents.d.ts +6 -0
  52. package/dist/subagents.js +133 -0
  53. package/dist/workspace-apply.d.ts +12 -0
  54. package/dist/workspace-apply.js +334 -0
  55. package/dist/workspace-files.d.ts +8 -0
  56. package/dist/workspace-files.js +46 -0
  57. package/package.json +3 -3
@@ -0,0 +1,32 @@
1
+ export interface AgentCandidateWorkspacePlan {
2
+ sourceProfileDigest: Sha256Digest;
3
+ harness: HarnessId;
4
+ files: PlanFile[];
5
+ env: Record<string, AgentCandidateConfigValue>;
6
+ flags: AgentCandidateConfigValue[];
7
+ systemPrompt?: AgentCandidateConfigValue;
8
+ appendSystemPrompt?: AgentCandidateConfigValue;
9
+ unsupported: Unsupported[];
10
+ }
11
+ export interface AgentCandidateWorkspacePlanReceipt {
12
+ profileActivation: AgentCandidateProfileActivation;
13
+ application: AgentCandidateProfileApplication;
14
+ env: Record<string, AgentCandidateConfigValue>;
15
+ flags: AgentCandidateConfigValue[];
16
+ systemPrompt?: AgentCandidateConfigValue;
17
+ appendSystemPrompt?: AgentCandidateConfigValue;
18
+ unsupported: Unsupported[];
19
+ }
20
+ export interface MaterializeCandidateProfileOptions {
21
+ /** Exact text fetched from pinned resource refs, keyed by declared SHA-256. */
22
+ resolvedResources?: ReadonlyMap<Sha256Digest, string>;
23
+ /**
24
+ * Launcher controls this caller owns and will bind itself. The declaration
25
+ * is forwarded to the classic materializer so candidate execution uses the
26
+ * same lowering as ordinary profile execution.
27
+ */
28
+ binds?: readonly BindableLaunchControl[];
29
+ }
30
+ import type { AgentCandidateConfigValue, AgentCandidateProfileActivation, AgentCandidateProfileApplication, Sha256Digest } from "@tangle-network/agent-interface";
31
+ import type { HarnessId } from "./harness.js";
32
+ import type { BindableLaunchControl, PlanFile, Unsupported } from "./plan-types.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { AgentCandidateProfile, AgentCandidateProfileApplication } from "@tangle-network/agent-interface";
2
+ import type { AgentCandidateWorkspacePlan, AgentCandidateWorkspacePlanReceipt, MaterializeCandidateProfileOptions } from "./candidate-types.js";
3
+ import type { HarnessId } from "./harness.js";
4
+ export declare function materializeCandidateProfile(profile: AgentCandidateProfile, harness: HarnessId, opts?: MaterializeCandidateProfileOptions): AgentCandidateWorkspacePlan;
5
+ /**
6
+ * Apply a candidate plan and return its spawn values together with captured
7
+ * canonical evidence. No caller needs to re-serialize or reinterpret the plan.
8
+ */
9
+ export declare function applyAgentCandidateWorkspacePlan(plan: AgentCandidateWorkspacePlan, workspaceDir: string, targetWorkspace: AgentCandidateProfileApplication["targetWorkspace"]): AgentCandidateWorkspacePlanReceipt;
@@ -0,0 +1,279 @@
1
+ import { createHash } from "node:crypto";
2
+ import { agentCandidateProfileActivationSchema, agentCandidateProfileSchema, canonicalCandidateDigest, } from "@tangle-network/agent-interface";
3
+ import { assertMaterializerHarness } from "./harness.js";
4
+ import { hookWrapperSource } from "./hooks.js";
5
+ import { applyWorkspacePlan } from "./plan-application.js";
6
+ import { captureAgentCandidateProfilePlan } from "./plan-digest.js";
7
+ import { normalizePlanFiles } from "./plan-normalize.js";
8
+ import { promptIntentFields, samePromptIntentValue } from "./plan-prompt.js";
9
+ import { assertWorkspacePlanSupported } from "./plan-support.js";
10
+ import { materializeProfile } from "./profile-materializer.js";
11
+ import { compareStrings } from "./workspace-files.js";
12
+ export function materializeCandidateProfile(profile, harness, opts = {}) {
13
+ assertMaterializerHarness(harness);
14
+ const candidate = agentCandidateProfileSchema.parse(profile);
15
+ verifyCandidateInlineResources(candidate);
16
+ const hookLowering = lowerCandidateHooks(candidate.hooks, harness);
17
+ const candidateResources = candidate.resources
18
+ ? candidateResourcesForClassicMaterializer(candidate.resources, opts.resolvedResources)
19
+ : {};
20
+ candidateResources.files = [
21
+ ...(candidateResources.files ?? []),
22
+ ...hookLowering.wrapperFiles,
23
+ ];
24
+ const classicProfile = {
25
+ ...candidate,
26
+ resources: candidateResources,
27
+ mcp: candidateMcpRecordForClassicMaterializer(candidate.mcp),
28
+ hooks: hookLowering.hooks,
29
+ };
30
+ const classicPlan = materializeProfile(classicProfile, harness, {
31
+ binds: opts.binds,
32
+ });
33
+ // Canonical candidate evidence has no field for a launcher-addressed binding.
34
+ // Refuse that shape instead of returning a candidate plan whose receipt would
35
+ // omit the exact control still owed by the launcher.
36
+ if (classicPlan.systemPromptBinding !== undefined) {
37
+ throw new Error(`Sealed candidate evidence cannot represent a system-prompt binding to ${classicPlan.systemPromptBinding.control}`);
38
+ }
39
+ if (hookLowering.unsupportedReason) {
40
+ classicPlan.unsupported.push({
41
+ dimension: "hooks",
42
+ reason: hookLowering.unsupportedReason,
43
+ });
44
+ }
45
+ classicPlan.unsupported.sort((left, right) => compareStrings(`${left.dimension}\0${left.reason}`, `${right.dimension}\0${right.reason}`));
46
+ assertWorkspacePlanSupported(classicPlan);
47
+ return {
48
+ sourceProfileDigest: canonicalCandidateDigest(candidate),
49
+ harness: classicPlan.harness,
50
+ files: classicPlan.files,
51
+ env: candidatePublicRecord(classicPlan.env),
52
+ flags: classicPlan.flags.map(candidatePublicArgument),
53
+ ...promptIntentFields(classicPlan, candidatePublicValue),
54
+ unsupported: classicPlan.unsupported,
55
+ };
56
+ }
57
+ /**
58
+ * Apply a candidate plan and return its spawn values together with captured
59
+ * canonical evidence. No caller needs to re-serialize or reinterpret the plan.
60
+ */
61
+ export function applyAgentCandidateWorkspacePlan(plan, workspaceDir, targetWorkspace) {
62
+ assertWorkspacePlanSupported(plan);
63
+ const profilePlan = captureAgentCandidateProfilePlan(plan);
64
+ const activationFiles = normalizePlanFiles(plan.files).map((file) => ({
65
+ path: file.relPath,
66
+ mode: file.mode,
67
+ content: file.content,
68
+ }));
69
+ const activationMaterial = {
70
+ kind: "agent-candidate-profile-activation",
71
+ profilePlan,
72
+ files: activationFiles,
73
+ };
74
+ const profileActivation = agentCandidateProfileActivationSchema.parse({
75
+ ...activationMaterial,
76
+ digest: canonicalCandidateDigest(activationMaterial),
77
+ });
78
+ const applied = applyWorkspacePlan({
79
+ harness: plan.harness,
80
+ files: plan.files,
81
+ env: Object.fromEntries(Object.entries(plan.env).map(([name, value]) => [
82
+ name,
83
+ candidatePublicString(value),
84
+ ])),
85
+ flags: plan.flags.map(candidatePublicString),
86
+ ...promptIntentFields(plan, candidatePublicString),
87
+ unsupported: plan.unsupported,
88
+ }, workspaceDir);
89
+ return {
90
+ profileActivation,
91
+ application: {
92
+ planDigest: profilePlan.digest,
93
+ targetWorkspace,
94
+ mountPaths: applied.written,
95
+ },
96
+ env: { ...plan.env },
97
+ flags: [...plan.flags],
98
+ ...promptIntentFields(plan, samePromptIntentValue),
99
+ unsupported: [...plan.unsupported],
100
+ };
101
+ }
102
+ function lowerCandidateHooks(hooks, harness) {
103
+ if (!hooks || Object.keys(hooks).length === 0)
104
+ return { wrapperFiles: [] };
105
+ const nativeHarness = harness;
106
+ if (nativeHarness !== "codex" && nativeHarness !== "claude-code") {
107
+ return {
108
+ wrapperFiles: [],
109
+ unsupportedReason: `${nativeHarness} hook configuration requires an unverified command-string mapping; ` +
110
+ "sealed candidate hooks preserve executable, argv, and env separately",
111
+ };
112
+ }
113
+ const wrapperFiles = [];
114
+ const lowered = {};
115
+ for (const [event, commands] of Object.entries(hooks)) {
116
+ lowered[event] = commands.map((command, index) => {
117
+ const wrapper = candidateHookWrapper(event, index, command);
118
+ wrapperFiles.push({
119
+ path: wrapper.path,
120
+ resource: {
121
+ kind: "inline",
122
+ name: wrapper.path,
123
+ content: wrapper.content,
124
+ },
125
+ });
126
+ return {
127
+ command: `node ${wrapper.path}`,
128
+ timeoutMs: command.timeoutMs,
129
+ blocking: command.blocking,
130
+ matcher: command.matcher,
131
+ };
132
+ });
133
+ }
134
+ return { hooks: lowered, wrapperFiles };
135
+ }
136
+ function candidateHookWrapper(event, index, command) {
137
+ const args = (command.args ?? []).map(candidatePublicString);
138
+ const env = Object.fromEntries(Object.entries(command.env ?? {})
139
+ .sort(([left], [right]) => compareStrings(left, right))
140
+ .map(([name, value]) => [name, candidatePublicString(value)]));
141
+ const wrapperId = canonicalCandidateDigest({
142
+ event,
143
+ index,
144
+ executable: command.executable,
145
+ args,
146
+ env,
147
+ timeoutMs: command.timeoutMs ?? null,
148
+ blocking: command.blocking ?? null,
149
+ matcher: command.matcher ?? null,
150
+ }).slice("sha256:".length);
151
+ const path = `.tangle/profile-hooks/${wrapperId}.mjs`;
152
+ const content = hookWrapperSource(command.executable, args, env);
153
+ return { path, content };
154
+ }
155
+ function candidateMcpRecordForClassicMaterializer(servers) {
156
+ if (!servers)
157
+ return undefined;
158
+ return Object.fromEntries(Object.entries(servers)
159
+ .filter((entry) => entry[1].enabled !== false)
160
+ .map(([name, server]) => [
161
+ name,
162
+ candidateMcpForClassicMaterializer(server),
163
+ ]));
164
+ }
165
+ function candidateMcpForClassicMaterializer(server) {
166
+ return {
167
+ command: server.command,
168
+ ...(server.transport === undefined ? {} : { transport: server.transport }),
169
+ ...(server.args === undefined
170
+ ? {}
171
+ : {
172
+ args: server.args.map((value) => profilePublicValue(candidatePublicString(value))),
173
+ }),
174
+ ...(server.env === undefined
175
+ ? {}
176
+ : {
177
+ env: Object.fromEntries(Object.entries(server.env).map(([name, value]) => [
178
+ name,
179
+ profilePublicValue(candidatePublicString(value)),
180
+ ])),
181
+ }),
182
+ ...(server.cwd === undefined ? {} : { cwd: server.cwd }),
183
+ };
184
+ }
185
+ function candidateResourcesForClassicMaterializer(resources, resolvedResources) {
186
+ const convert = (ref) => {
187
+ if (ref.kind === "inline") {
188
+ return { kind: "inline", name: ref.name, content: ref.content };
189
+ }
190
+ const content = resolvedResources?.get(ref.sha256);
191
+ if (content !== undefined) {
192
+ verifyCandidateResourceBytes(ref, content, ref.name ?? ref.path);
193
+ return {
194
+ kind: "inline",
195
+ name: ref.name ?? ref.path.split("/").at(-1) ?? "resource",
196
+ content,
197
+ };
198
+ }
199
+ return {
200
+ kind: "github",
201
+ repository: `${ref.repository.owner}/${ref.repository.repo}`,
202
+ path: ref.path,
203
+ ref: ref.commit,
204
+ name: ref.name,
205
+ };
206
+ };
207
+ return {
208
+ files: resources.files?.map((mount) => ({
209
+ path: mount.path,
210
+ resource: convert(mount.resource),
211
+ executable: mount.executable,
212
+ })),
213
+ tools: resources.tools?.map(convert),
214
+ skills: resources.skills?.map(convert),
215
+ agents: resources.agents?.map(convert),
216
+ commands: resources.commands?.map(convert),
217
+ instructions: typeof resources.instructions === "string"
218
+ ? resources.instructions
219
+ : resources.instructions
220
+ ? convert(resources.instructions)
221
+ : undefined,
222
+ // The candidate wrapper owns the complete unsupported check after it adds
223
+ // dimensions that the classic materializer cannot represent.
224
+ failOnError: false,
225
+ };
226
+ }
227
+ function candidatePublicString(value) {
228
+ if (value.kind !== "public") {
229
+ throw new Error("Candidate materialization accepts public config values only");
230
+ }
231
+ return value.value;
232
+ }
233
+ function candidatePublicValue(value) {
234
+ return { kind: "public", value };
235
+ }
236
+ function profilePublicValue(value) {
237
+ return { kind: "public", value };
238
+ }
239
+ function candidatePublicArgument(value) {
240
+ if (typeof value !== "string") {
241
+ throw new Error("Candidate materialization cannot emit private launch arguments");
242
+ }
243
+ return candidatePublicValue(value);
244
+ }
245
+ function candidatePublicRecord(values) {
246
+ return Object.fromEntries(Object.entries(values).map(([name, value]) => {
247
+ if (typeof value !== "string") {
248
+ throw new Error("Candidate materialization cannot emit private environment values");
249
+ }
250
+ return [name, candidatePublicValue(value)];
251
+ }));
252
+ }
253
+ function verifyCandidateInlineResources(profile) {
254
+ const resources = profile.resources;
255
+ if (!resources)
256
+ return;
257
+ const refs = [
258
+ ...(resources.files?.map((mount) => mount.resource) ?? []),
259
+ ...(resources.tools ?? []),
260
+ ...(resources.skills ?? []),
261
+ ...(resources.agents ?? []),
262
+ ...(resources.commands ?? []),
263
+ ...(typeof resources.instructions === "object"
264
+ ? [resources.instructions]
265
+ : []),
266
+ ];
267
+ for (const ref of refs) {
268
+ if (ref.kind !== "inline")
269
+ continue;
270
+ verifyCandidateResourceBytes(ref, ref.content, ref.name);
271
+ }
272
+ }
273
+ function verifyCandidateResourceBytes(ref, content, label) {
274
+ const bytes = Buffer.from(content, "utf8");
275
+ const digest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
276
+ if (bytes.byteLength !== ref.byteLength || digest !== ref.sha256) {
277
+ throw new Error(`Candidate resource integrity mismatch: ${label}`);
278
+ }
279
+ }
@@ -0,0 +1,2 @@
1
+ export declare function materializeCommand(h: HarnessId, name: string, body: string, add: (path: string, content: string, mode?: number) => void, unsupported: (d: Unsupported["dimension"], r: string) => void): void;
2
+ import type { HarnessId, Unsupported } from "./plan-types.js";
@@ -0,0 +1,32 @@
1
+ export function materializeCommand(h, name, body, add, unsupported) {
2
+ switch (h) {
3
+ case "claude-code":
4
+ case "nanoclaw":
5
+ add(`.claude/commands/${name}.md`, body.endsWith("\n") ? body : `${body}\n`);
6
+ break;
7
+ case "opencode":
8
+ add(`.opencode/commands/${name}.md`, body.endsWith("\n") ? body : `${body}\n`);
9
+ break;
10
+ case "gemini":
11
+ add(`.gemini/commands/${name}.toml`, `prompt = ${tomlStr(body)}\n`);
12
+ break;
13
+ case "pi":
14
+ add(`.pi/prompts/${name}.md`, body.endsWith("\n") ? body : `${body}\n`);
15
+ break;
16
+ case "prime":
17
+ // Auto-discovered project prompt templates: <cwd>/.prime/agent/prompts
18
+ // (fork prompt-templates.ts:218 resolves CONFIG_DIR_NAME "prompts";
19
+ // package-manager.ts:2176-2180 auto-adds the same projectDirs.prompts).
20
+ add(`.prime/agent/prompts/${name}.md`, body.endsWith("\n") ? body : `${body}\n`);
21
+ break;
22
+ case "kimi-code":
23
+ unsupported("commands", "kimi commands ARE skills (/skill:<name>) — provide as a skill instead");
24
+ break;
25
+ case "codex":
26
+ unsupported("commands", "codex project commands unsupported (~/.codex/prompts is user-global only)");
27
+ break;
28
+ default:
29
+ unsupported("commands", `${h}: no cwd command path`);
30
+ }
31
+ }
32
+ import { tomlStr } from "./plan-json.js";
@@ -0,0 +1,34 @@
1
+ export declare function normalizedControlName(name: string): string;
2
+ export declare function claudeToolName(name: string): string | undefined;
3
+ /** Tool IDs reported by the OpenCode binary shipped in the Sandbox image. */
4
+ export declare const OPENCODE_NATIVE_TOOL_IDS: readonly ["apply_patch", "bash", "codesearch", "edit", "glob", "grep", "question", "read", "skill", "task", "todowrite", "webfetch", "websearch", "write"];
5
+ /** Resolve portable and legacy profile names to current OpenCode tool IDs. */
6
+ export declare function openCodeNativeToolId(name: string): string | undefined;
7
+ export declare function openCodeToolName(name: string): string | undefined;
8
+ export declare function openCodePermissionName(name: string): string | undefined;
9
+ export declare const PI_EXECUTION_CONTROL_EXTENSION_PATH = ".tangle/pi-execution-controls.ts";
10
+ /**
11
+ * Translate only portable spellings whose Pi equivalent differs.
12
+ *
13
+ * Every other exact name is resolved against Pi's live registry, keeping
14
+ * extension and MCP tools profile-configurable without a static catalog.
15
+ */
16
+ export declare function piToolId(name: string): string | undefined;
17
+ export declare function mapSupportedBooleanEntries(entries: [string, boolean][], mapName: (name: string) => string | undefined, onUnsupported: (name: string) => void, label: string): [string, boolean][];
18
+ export declare function permissionAllowsEverything(policy: AgentProfilePermission): boolean;
19
+ export declare function mapSupportedPermissionEntries(entries: [string, AgentProfilePermission][], mapName: (name: string) => string | undefined, onUnsupported: (name: string) => void, label: string): [string, AgentProfilePermission][];
20
+ export declare function claudePermissionRules(entries: [string, AgentProfilePermission][]): {
21
+ allow: string[];
22
+ ask: string[];
23
+ deny: string[];
24
+ };
25
+ export declare function sealClaudePermissionRules(rules: {
26
+ allow: string[];
27
+ ask: string[];
28
+ deny: string[];
29
+ }): {
30
+ allow: string[];
31
+ ask: string[];
32
+ deny: string[];
33
+ };
34
+ import type { AgentProfilePermission } from "@tangle-network/agent-interface";
@@ -0,0 +1,185 @@
1
+ export function normalizedControlName(name) {
2
+ return name.replace(/[^a-zA-Z0-9_]/g, "").toLowerCase();
3
+ }
4
+ function nativeWebToolKind(name) {
5
+ const posture = resolveNativeWebTools({ [name]: true });
6
+ if (posture.search === true && posture.fetch === undefined)
7
+ return "search";
8
+ if (posture.fetch === true && posture.search === undefined)
9
+ return "fetch";
10
+ return undefined;
11
+ }
12
+ const CLAUDE_TOOL_NAMES = {
13
+ askuserquestion: "AskUserQuestion",
14
+ bash: "Bash",
15
+ edit: "Edit",
16
+ glob: "Glob",
17
+ grep: "Grep",
18
+ notebookedit: "NotebookEdit",
19
+ read: "Read",
20
+ skill: "Skill",
21
+ task: "Task",
22
+ taskoutput: "TaskOutput",
23
+ todowrite: "TodoWrite",
24
+ webfetch: "WebFetch",
25
+ websearch: "WebSearch",
26
+ write: "Write",
27
+ };
28
+ export function claudeToolName(name) {
29
+ const web = nativeWebToolKind(name);
30
+ if (web === "search")
31
+ return "WebSearch";
32
+ if (web === "fetch")
33
+ return "WebFetch";
34
+ return CLAUDE_TOOL_NAMES[normalizedControlName(name)];
35
+ }
36
+ /** Tool IDs reported by the OpenCode binary shipped in the Sandbox image. */
37
+ export const OPENCODE_NATIVE_TOOL_IDS = [
38
+ "apply_patch",
39
+ "bash",
40
+ "codesearch",
41
+ "edit",
42
+ "glob",
43
+ "grep",
44
+ "question",
45
+ "read",
46
+ "skill",
47
+ "task",
48
+ "todowrite",
49
+ "webfetch",
50
+ "websearch",
51
+ "write",
52
+ ];
53
+ const OPENCODE_NATIVE_TOOL_ID_SET = new Set(OPENCODE_NATIVE_TOOL_IDS);
54
+ /** Resolve portable and legacy profile names to current OpenCode tool IDs. */
55
+ export function openCodeNativeToolId(name) {
56
+ const web = nativeWebToolKind(name);
57
+ if (web === "search")
58
+ return "websearch";
59
+ if (web === "fetch")
60
+ return "webfetch";
61
+ const normalized = normalizedControlName(name);
62
+ if (normalized === "patch")
63
+ return "apply_patch";
64
+ return OPENCODE_NATIVE_TOOL_ID_SET.has(normalized) ? normalized : undefined;
65
+ }
66
+ export function openCodeToolName(name) {
67
+ return openCodeNativeToolId(name);
68
+ }
69
+ export function openCodePermissionName(name) {
70
+ const normalized = normalizedControlName(name);
71
+ if (["write", "edit"].includes(normalized))
72
+ return "edit";
73
+ const web = nativeWebToolKind(name);
74
+ if (web === "search")
75
+ return "websearch";
76
+ if (web === "fetch")
77
+ return "webfetch";
78
+ if (["bash", "mcp", "doom_loop", "external_directory"].includes(normalized)) {
79
+ return normalized;
80
+ }
81
+ return normalized !== "read" && OPENCODE_NATIVE_TOOL_ID_SET.has(normalized)
82
+ ? normalized
83
+ : undefined;
84
+ }
85
+ export const PI_EXECUTION_CONTROL_EXTENSION_PATH = ".tangle/pi-execution-controls.ts";
86
+ /**
87
+ * Translate only portable spellings whose Pi equivalent differs.
88
+ *
89
+ * Every other exact name is resolved against Pi's live registry, keeping
90
+ * extension and MCP tools profile-configurable without a static catalog.
91
+ */
92
+ export function piToolId(name) {
93
+ const trimmed = name.trim();
94
+ if (trimmed.length === 0)
95
+ return undefined;
96
+ const portableName = trimmed.toLowerCase();
97
+ if (portableName === "shell")
98
+ return "bash";
99
+ if (portableName === "glob")
100
+ return "find";
101
+ return trimmed;
102
+ }
103
+ export function mapSupportedBooleanEntries(entries, mapName, onUnsupported, label) {
104
+ return mapControlEntries(entries, mapName, onUnsupported, (left, right, nativeName) => {
105
+ if (left !== right)
106
+ throw new Error(`${label} aliases disagree for "${nativeName}"`);
107
+ return left;
108
+ });
109
+ }
110
+ export function permissionAllowsEverything(policy) {
111
+ return typeof policy === "string"
112
+ ? policy === "allow"
113
+ : Object.values(policy).every((value) => value === "allow");
114
+ }
115
+ export function mapSupportedPermissionEntries(entries, mapName, onUnsupported, label) {
116
+ return mapControlEntries(entries, mapName, onUnsupported, (left, right, nativeName) => mergePermissionPolicies(left, right, `${label} "${nativeName}"`));
117
+ }
118
+ function mapControlEntries(entries, mapName, onUnsupported, merge) {
119
+ const mapped = new Map();
120
+ for (const [name, value] of entries) {
121
+ const nativeName = mapName(name);
122
+ if (!nativeName) {
123
+ onUnsupported(name);
124
+ continue;
125
+ }
126
+ const previous = mapped.get(nativeName);
127
+ mapped.set(nativeName, previous === undefined ? value : merge(previous, value, nativeName));
128
+ }
129
+ return [...mapped.entries()].sort(([left], [right]) => compareStrings(left, right));
130
+ }
131
+ function mergePermissionPolicies(left, right, label) {
132
+ if (typeof left === "string" || typeof right === "string") {
133
+ if (left !== right)
134
+ throw new Error(`${label} aliases disagree`);
135
+ return left;
136
+ }
137
+ const merged = { ...left };
138
+ for (const [specifier, decision] of Object.entries(right)) {
139
+ const previous = merged[specifier];
140
+ if (previous !== undefined && previous !== decision) {
141
+ throw new Error(`${label} aliases disagree for "${specifier}"`);
142
+ }
143
+ merged[specifier] = decision;
144
+ }
145
+ return Object.fromEntries(Object.entries(merged).sort(([leftKey], [rightKey]) => compareStrings(leftKey, rightKey)));
146
+ }
147
+ export function claudePermissionRules(entries) {
148
+ const rules = {
149
+ allow: [],
150
+ ask: [],
151
+ deny: [],
152
+ };
153
+ for (const [tool, policy] of entries) {
154
+ if (typeof policy === "string") {
155
+ rules[policy].push(tool);
156
+ continue;
157
+ }
158
+ for (const [specifier, decision] of Object.entries(policy).sort(([left], [right]) => compareStrings(left, right))) {
159
+ rules[decision].push(specifier === "*" ? tool : `${tool}(${specifier})`);
160
+ }
161
+ }
162
+ rules.allow.sort(compareStrings);
163
+ rules.ask.sort(compareStrings);
164
+ rules.deny.sort(compareStrings);
165
+ return rules;
166
+ }
167
+ export function sealClaudePermissionRules(rules) {
168
+ const owner = new Map();
169
+ for (const decision of ["allow", "ask", "deny"]) {
170
+ for (const rule of rules[decision]) {
171
+ const previous = owner.get(rule);
172
+ if (previous && previous !== decision) {
173
+ throw new Error(`claude-code execution control "${rule}" conflicts between ${previous} and ${decision}`);
174
+ }
175
+ owner.set(rule, decision);
176
+ }
177
+ }
178
+ return {
179
+ allow: [...new Set(rules.allow)].sort(compareStrings),
180
+ ask: [...new Set(rules.ask)].sort(compareStrings),
181
+ deny: [...new Set(rules.deny)].sort(compareStrings),
182
+ };
183
+ }
184
+ import { resolveNativeWebTools } from "@tangle-network/agent-interface";
185
+ import { compareStrings } from "./workspace-files.js";
@@ -0,0 +1,5 @@
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;
2
+ import type { AgentProfile, AgentProfilePermission } from "@tangle-network/agent-interface";
3
+ import type { HarnessId } from "./harness.js";
4
+ import type { AddPlanFile } from "./plan-json.js";
5
+ import type { Unsupported, WorkspacePlan } from "./plan-types.js";