@tangle-network/agent-profile-materialize 0.22.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";
@@ -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
  }
@@ -8,6 +8,8 @@ import type { ApplyWorkspacePlanOptions, WorkspacePlan } from "./plan-types.js";
8
8
  */
9
9
  export declare function prepareSecretConcurrentProfileRevision(input: {
10
10
  plan: WorkspacePlan;
11
+ /** Trusted executor transport only; never read from the mutable native HOME. */
12
+ providerConfig?: Record<string, unknown>;
11
13
  managedRoot: string;
12
14
  launchRoot: string;
13
15
  sharedWorkspaceDir: string;
@@ -20,7 +20,23 @@ export async function prepareSecretConcurrentProfileRevision(input) {
20
20
  !input.ownerId) {
21
21
  throw new Error("Secret concurrent profile needs a root executor and reserved identity");
22
22
  }
23
- const plan = prepareExplicitOpenCodePlan(input.plan);
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
+ }
24
40
  if (plan.files.some((file) => file.relPath.startsWith(".config/opencode/"))) {
25
41
  throw new Error("Secret OpenCode reserves its native config directory");
26
42
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-profile-materialize",
3
- "version": "0.22.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": {