@tangle-network/agent-profile-materialize 0.27.1 → 0.27.2

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.
@@ -2,121 +2,18 @@ export function materializeExecutionControls(h, tools, permissions, modes, plan,
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) {
5
- case "claude-code": {
6
- if (toolEntries.length > 0 || permissionEntries.length > 0) {
7
- const nativeTools = mapSupportedBooleanEntries(toolEntries, claudeToolName, (name) => unsupported("tools", `claude-code: no verified native tool mapping for "${name}". Accepted: ${CLAUDE_CODE_NATIVE_TOOL_IDS.join(", ")}`));
8
- const nativePermissions = mapSupportedPermissionEntries(permissionEntries, claudeToolName, (name) => unsupported("permissions", `claude-code: no verified native permission mapping for "${name}". Accepted: ${CLAUDE_CODE_NATIVE_PERMISSION_IDS.join(", ")}`), "claude-code permission");
9
- const rules = claudePermissionRules(nativePermissions);
10
- for (const [tool, enabled] of nativeTools) {
11
- rules[enabled ? "allow" : "deny"].push(tool);
12
- }
13
- add(CLAUDE_SETTINGS_PATH, serializeWorkspacePlanJson({
14
- permissions: sealClaudePermissionRules(rules),
15
- }));
16
- registerClaudeSettings(plan);
17
- }
5
+ case "claude-code":
6
+ materializeClaudeControls(toolEntries, permissionEntries, plan, add, unsupported);
18
7
  break;
19
- }
20
- case "opencode": {
21
- const enforceableTools = toolEntries.filter(([name, enabled]) => {
22
- if (normalizedControlName(name) !== "lsp" || !enabled)
23
- return true;
24
- unsupported("tools", "opencode: the LSP tool is disabled by the runtime and cannot be enabled exactly");
25
- return false;
26
- });
27
- const enforceablePermissions = permissionEntries.filter(([name, policy]) => {
28
- if (normalizedControlName(name) !== "read")
29
- return true;
30
- if (!permissionAllowsEverything(policy)) {
31
- unsupported("permissions", "opencode: read access cannot be denied or approval-gated");
32
- }
33
- return false;
34
- });
35
- const nativeTools = mapSupportedBooleanEntries(enforceableTools, openCodeToolName, (name) => unsupported("tools", `opencode: no verified native tool mapping for "${name}". Accepted: ${OPENCODE_NATIVE_TOOL_IDS.join(", ")}`));
36
- const nativePermissions = mapSupportedPermissionEntries(enforceablePermissions, (name) => openCodeDeclaredMcpPermissionName(name, mcpAliases) ??
37
- openCodePermissionName(name), (name) => unsupported("permissions", `opencode: no verified native permission mapping for "${name}". Accepted: ${OPENCODE_NATIVE_PERMISSION_IDS.join(", ")}`), "opencode permission");
38
- if (enforceableTools.length > 0 || permissionEntries.length > 0) {
39
- add(OPENCODE_CONFIG_FILE, serializeWorkspacePlanJson({
40
- $schema: OPENCODE_CONFIG_SCHEMA,
41
- ...(nativeTools.length > 0
42
- ? { tools: Object.fromEntries(nativeTools) }
43
- : {}),
44
- ...(nativePermissions.length > 0
45
- ? { permission: Object.fromEntries(nativePermissions) }
46
- : {}),
47
- }));
48
- }
8
+ case "opencode":
9
+ materializeOpenCodeControls(toolEntries, permissionEntries, add, unsupported, mcpAliases);
49
10
  break;
50
- }
51
- case "codex": {
52
- const searchEntries = [];
53
- const fetchEntries = [];
54
- for (const [name, enabled] of toolEntries) {
55
- const posture = resolveNativeWebTools({ [name]: enabled });
56
- if (posture.search === enabled && posture.fetch === undefined) {
57
- searchEntries.push([name, enabled]);
58
- continue;
59
- }
60
- if (posture.fetch === enabled && posture.search === undefined) {
61
- fetchEntries.push([name, enabled]);
62
- continue;
63
- }
64
- unsupported("tools", `codex: no verified primary-process tool mapping for "${name}"`);
65
- }
66
- // Codex has no native fetch: only a fetch that stays on after its
67
- // spellings merge asks for something this harness cannot run.
68
- const nativeFetch = mapSupportedBooleanEntries(fetchEntries, () => "web_fetch", () => undefined);
69
- if (nativeFetch[0]?.[1]) {
70
- for (const [name] of fetchEntries) {
71
- unsupported("tools", `codex: native web fetch is unavailable for "${name}"`);
72
- }
73
- }
74
- const nativeSearch = mapSupportedBooleanEntries(searchEntries, () => "web_search", () => undefined);
75
- if (nativeSearch[0]) {
76
- plan.flags.push("-c", `tools.web_search=${nativeSearch[0][1]}`);
77
- }
78
- if (permissionEntries.length > 0) {
79
- unsupported("permissions", "codex: no verified primary-process permission policy mapping");
80
- }
11
+ case "codex":
12
+ materializeCodexControls(toolEntries, permissionEntries, plan, unsupported);
81
13
  break;
82
- }
83
- case "pi": {
84
- const nativeTools = mapSupportedBooleanEntries(toolEntries, piToolId, (name) => unsupported("tools", `pi: tool name "${name}" is empty after trimming`));
85
- const nativePermissions = mapSupportedPermissionEntries(permissionEntries, piToolId, (name) => unsupported("permissions", `pi: permission name "${name}" is empty after trimming`), "pi permission");
86
- const enabled = new Set(nativeTools
87
- .filter(([, value]) => value)
88
- .map(([name]) => name)
89
- .sort(compareStrings));
90
- const excluded = new Set(nativeTools
91
- .filter(([, value]) => !value)
92
- .map(([name]) => name)
93
- .sort(compareStrings));
94
- for (const [name, policy] of nativePermissions) {
95
- if (typeof policy !== "string") {
96
- unsupported("permissions", `pi: conditional permission policy for "${name}" has no verified headless mapping`);
97
- continue;
98
- }
99
- if (policy === "ask") {
100
- unsupported("permissions", `pi: permission policy "ask" for "${name}" requires an interactive decision surface`);
101
- continue;
102
- }
103
- // The stricter control holds, as across spellings of one tool: an
104
- // allow keeps a disabled tool disabled, and a deny excludes an
105
- // enabled one.
106
- if (policy === "allow")
107
- continue;
108
- enabled.delete(name);
109
- excluded.add(name);
110
- }
111
- if (enabled.size > 0 || excluded.size > 0) {
112
- add(PI_EXECUTION_CONTROL_EXTENSION_PATH, piExecutionControlExtensionSource([...enabled], [...excluded]));
113
- plan.flags.push("--extension", PI_EXECUTION_CONTROL_EXTENSION_PATH);
114
- }
115
- if (excluded.size > 0) {
116
- plan.flags.push("--exclude-tools", [...excluded].sort(compareStrings).join(","));
117
- }
14
+ case "pi":
15
+ materializePiControls(toolEntries, permissionEntries, plan, add, unsupported);
118
16
  break;
119
- }
120
17
  default:
121
18
  if (toolEntries.length > 0) {
122
19
  unsupported("tools", `${h}: no verified primary-process tool availability mapping`);
@@ -129,6 +26,117 @@ export function materializeExecutionControls(h, tools, permissions, modes, plan,
129
26
  unsupported("modes", `${h}: resolve one named AgentProfile mode before materialization`);
130
27
  }
131
28
  }
29
+ function materializeClaudeControls(toolEntries, permissionEntries, plan, add, unsupported) {
30
+ if (toolEntries.length > 0 || permissionEntries.length > 0) {
31
+ const nativeTools = mapSupportedBooleanEntries(toolEntries, claudeToolName, (name) => unsupported("tools", `claude-code: no verified native tool mapping for "${name}". Accepted: ${CLAUDE_CODE_NATIVE_TOOL_IDS.join(", ")}`));
32
+ const nativePermissions = mapSupportedPermissionEntries(permissionEntries, claudeToolName, (name) => unsupported("permissions", `claude-code: no verified native permission mapping for "${name}". Accepted: ${CLAUDE_CODE_NATIVE_PERMISSION_IDS.join(", ")}`), "claude-code permission");
33
+ const rules = claudePermissionRules(nativePermissions);
34
+ for (const [tool, enabled] of nativeTools) {
35
+ rules[enabled ? "allow" : "deny"].push(tool);
36
+ }
37
+ add(CLAUDE_SETTINGS_PATH, serializeWorkspacePlanJson({
38
+ permissions: sealClaudePermissionRules(rules),
39
+ }));
40
+ registerClaudeSettings(plan);
41
+ }
42
+ }
43
+ function materializeOpenCodeControls(toolEntries, permissionEntries, add, unsupported, mcpAliases) {
44
+ const enforceableTools = toolEntries.filter(([name, enabled]) => {
45
+ if (normalizedControlName(name) !== "lsp" || !enabled)
46
+ return true;
47
+ unsupported("tools", "opencode: the LSP tool is disabled by the runtime and cannot be enabled exactly");
48
+ return false;
49
+ });
50
+ const enforceablePermissions = permissionEntries.filter(([name, policy]) => {
51
+ if (normalizedControlName(name) !== "read")
52
+ return true;
53
+ if (!permissionAllowsEverything(policy)) {
54
+ unsupported("permissions", "opencode: read access cannot be denied or approval-gated");
55
+ }
56
+ return false;
57
+ });
58
+ const nativeTools = mapSupportedBooleanEntries(enforceableTools, openCodeToolName, (name) => unsupported("tools", `opencode: no verified native tool mapping for "${name}". Accepted: ${OPENCODE_NATIVE_TOOL_IDS.join(", ")}`));
59
+ const nativePermissions = mapSupportedPermissionEntries(enforceablePermissions, (name) => openCodeDeclaredMcpPermissionName(name, mcpAliases) ??
60
+ openCodePermissionName(name), (name) => unsupported("permissions", `opencode: no verified native permission mapping for "${name}". Accepted: ${OPENCODE_NATIVE_PERMISSION_IDS.join(", ")}`), "opencode permission");
61
+ if (enforceableTools.length > 0 || permissionEntries.length > 0) {
62
+ add(OPENCODE_CONFIG_FILE, serializeWorkspacePlanJson({
63
+ $schema: OPENCODE_CONFIG_SCHEMA,
64
+ ...(nativeTools.length > 0
65
+ ? { tools: Object.fromEntries(nativeTools) }
66
+ : {}),
67
+ ...(nativePermissions.length > 0
68
+ ? { permission: Object.fromEntries(nativePermissions) }
69
+ : {}),
70
+ }));
71
+ }
72
+ }
73
+ function materializeCodexControls(toolEntries, permissionEntries, plan, unsupported) {
74
+ const searchEntries = [];
75
+ const fetchEntries = [];
76
+ for (const [name, enabled] of toolEntries) {
77
+ const posture = resolveNativeWebTools({ [name]: enabled });
78
+ if (posture.search === enabled && posture.fetch === undefined) {
79
+ searchEntries.push([name, enabled]);
80
+ continue;
81
+ }
82
+ if (posture.fetch === enabled && posture.search === undefined) {
83
+ fetchEntries.push([name, enabled]);
84
+ continue;
85
+ }
86
+ unsupported("tools", `codex: no verified primary-process tool mapping for "${name}"`);
87
+ }
88
+ // Codex has no native fetch: only a fetch that stays on after its
89
+ // spellings merge asks for something this harness cannot run.
90
+ const nativeFetch = mapSupportedBooleanEntries(fetchEntries, () => "web_fetch", () => undefined);
91
+ if (nativeFetch[0]?.[1]) {
92
+ for (const [name] of fetchEntries) {
93
+ unsupported("tools", `codex: native web fetch is unavailable for "${name}"`);
94
+ }
95
+ }
96
+ const nativeSearch = mapSupportedBooleanEntries(searchEntries, () => "web_search", () => undefined);
97
+ if (nativeSearch[0]) {
98
+ plan.flags.push("-c", `tools.web_search=${nativeSearch[0][1]}`);
99
+ }
100
+ if (permissionEntries.length > 0) {
101
+ unsupported("permissions", "codex: no verified primary-process permission policy mapping");
102
+ }
103
+ }
104
+ function materializePiControls(toolEntries, permissionEntries, plan, add, unsupported) {
105
+ const nativeTools = mapSupportedBooleanEntries(toolEntries, piToolId, (name) => unsupported("tools", `pi: tool name "${name}" is empty after trimming`));
106
+ const nativePermissions = mapSupportedPermissionEntries(permissionEntries, piToolId, (name) => unsupported("permissions", `pi: permission name "${name}" is empty after trimming`), "pi permission");
107
+ const enabled = new Set(nativeTools
108
+ .filter(([, value]) => value)
109
+ .map(([name]) => name)
110
+ .sort(compareStrings));
111
+ const excluded = new Set(nativeTools
112
+ .filter(([, value]) => !value)
113
+ .map(([name]) => name)
114
+ .sort(compareStrings));
115
+ for (const [name, policy] of nativePermissions) {
116
+ if (typeof policy !== "string") {
117
+ unsupported("permissions", `pi: conditional permission policy for "${name}" has no verified headless mapping`);
118
+ continue;
119
+ }
120
+ if (policy === "ask") {
121
+ unsupported("permissions", `pi: permission policy "ask" for "${name}" requires an interactive decision surface`);
122
+ continue;
123
+ }
124
+ // The stricter control holds, as across spellings of one tool: an
125
+ // allow keeps a disabled tool disabled, and a deny excludes an
126
+ // enabled one.
127
+ if (policy === "allow")
128
+ continue;
129
+ enabled.delete(name);
130
+ excluded.add(name);
131
+ }
132
+ if (enabled.size > 0 || excluded.size > 0) {
133
+ add(PI_EXECUTION_CONTROL_EXTENSION_PATH, piExecutionControlExtensionSource([...enabled], [...excluded]));
134
+ plan.flags.push("--extension", PI_EXECUTION_CONTROL_EXTENSION_PATH);
135
+ }
136
+ if (excluded.size > 0) {
137
+ plan.flags.push("--exclude-tools", [...excluded].sort(compareStrings).join(","));
138
+ }
139
+ }
132
140
  function piExecutionControlExtensionSource(enabledTools, excludedTools) {
133
141
  return [
134
142
  'import { writeSync } from "node:fs";',
package/dist/harness.d.ts CHANGED
@@ -70,31 +70,6 @@ export declare const MATERIALIZER_RESERVED_PATHS: ReadonlySet<string>;
70
70
  * invented, already owned by the name alone.
71
71
  */
72
72
  export declare const MATERIALIZER_CONTEXT_PATHS: ReadonlySet<string>;
73
- /**
74
- * The paths whose generated content is wholly this package's output, per
75
- * {@link PlanFile.ownership}: the reserved names, the context files, and
76
- * `opencode.json`, which carries only the registrations this package generates
77
- * from the profile.
78
- *
79
- * `opencode.json` and every context file is a documented name a user repository
80
- * can carry, so the name alone proves nothing and a profile resource may still
81
- * mount one. An existing file there is replaced only when
82
- * {@link materializerReplacement} recognizes its bytes as this package's own
83
- * earlier output.
84
- *
85
- * Deliberately absent: `.gemini/system.md`, the file `prompt.systemPrompt`
86
- * lowers to. The line a marker would have to add is the reason. A REPLACEMENT
87
- * is the caller's exact bytes and this package may add none of its own, so that
88
- * path keeps refusing. A context file carries `prompt.instructions`, which this
89
- * package already composes — it joins the blocks and appends the final newline
90
- * — so one more composed line is inside the same authority.
91
- *
92
- * That line, not "which channel the harness reads it on", is what separates the
93
- * two. `GEMINI.md` is owned even though gemini lowers its context file into the
94
- * system-prompt position as an ADDITION ({@link appendSystemPromptRefusal}),
95
- * because the intent it carries is still instructions this package composed.
96
- */
97
- export declare const MATERIALIZER_OWNED_PATHS: ReadonlySet<string>;
98
73
  /** Mark one generated native agent file without changing its harness meaning. */
99
74
  export declare function markMaterializerSubagent(relPath: string, content: string): string;
100
75
  /**
package/dist/harness.js CHANGED
@@ -168,7 +168,7 @@ export const MATERIALIZER_CONTEXT_PATHS = new Set([...CONTEXT_FILE_PATHS].filter
168
168
  * system-prompt position as an ADDITION ({@link appendSystemPromptRefusal}),
169
169
  * because the intent it carries is still instructions this package composed.
170
170
  */
171
- export const MATERIALIZER_OWNED_PATHS = new Set([
171
+ const MATERIALIZER_OWNED_PATHS = new Set([
172
172
  ...MATERIALIZER_RESERVED_PATHS,
173
173
  ...MATERIALIZER_CONTEXT_PATHS,
174
174
  OPENCODE_CONFIG_FILE,
@@ -355,7 +355,7 @@ export function materializerReplacement(relPath, content, plannedContent) {
355
355
  function isMaterializerOutput(relPath, content, plannedContent) {
356
356
  if (MATERIALIZER_RESERVED_PATHS.has(relPath))
357
357
  return true;
358
- if (relPath === ".pi/mcp.json" || relPath === ".kimi/mcp.json")
358
+ if (relPath === ".pi/mcp.json" || relPath === ".kimi-code/mcp.json")
359
359
  return isMaterializerMcpConfig(relPath, content);
360
360
  if (MATERIALIZER_SUBAGENT_PATH.test(relPath)) {
361
361
  const existing = content();
package/dist/index.d.ts CHANGED
@@ -5,8 +5,8 @@ export type { HarnessId } from "./harness.js";
5
5
  export { HARNESS_SKILL_DIRS, harnessAutoLoadsAgentContextFile, harnessAutoLoadsContextFile, harnessContextFile, isMaterializerHarness, MATERIALIZER_CONTEXT_PATHS, MATERIALIZER_HARNESSES, markMaterializerContextFile, skillDirForHarness, } from "./harness.js";
6
6
  export * from "./harness-prompt.js";
7
7
  export { type ConcurrentProfileRevision, ensurePublicConcurrentProfileRoots, type ManagedWorkspaceFileDigest, type ManagedWorkspaceManifest, preparePublicConcurrentProfileRevision, } from "./managed-workspace.js";
8
- export { type OpenCodeMcpConfig, openCodeMcpConfig } from "./mcp.js";
9
8
  export { isMaterializerMcpConfig } from "./mcp-config-ownership.js";
9
+ export { type OpenCodeMcpConfig, openCodeMcpConfig } from "./mcp-opencode.js";
10
10
  export * from "./model-id.js";
11
11
  export { buildPiModelsConfig, buildPiModelsConfigFile, PI_MODELS_CONFIG_PATH, type PiModelsConfigFile, type PiModelsConfigInput, type PiRuntimePaths, resolvePiRuntimePaths, } from "./pi-models-config.js";
12
12
  export { applyWorkspacePlan, applyWorkspacePlanWithSecretProvider, } from "./plan-application.js";
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ export { OPENCODE_NATIVE_TOOL_IDS, openCodeNativeToolId, } from "./control-maps.
3
3
  export { HARNESS_SKILL_DIRS, harnessAutoLoadsAgentContextFile, harnessAutoLoadsContextFile, harnessContextFile, isMaterializerHarness, MATERIALIZER_CONTEXT_PATHS, MATERIALIZER_HARNESSES, markMaterializerContextFile, skillDirForHarness, } from "./harness.js";
4
4
  export * from "./harness-prompt.js";
5
5
  export { ensurePublicConcurrentProfileRoots, preparePublicConcurrentProfileRevision, } from "./managed-workspace.js";
6
- export { openCodeMcpConfig } from "./mcp.js";
7
6
  export { isMaterializerMcpConfig } from "./mcp-config-ownership.js";
7
+ export { openCodeMcpConfig } from "./mcp-opencode.js";
8
8
  export * from "./model-id.js";
9
9
  export { buildPiModelsConfig, buildPiModelsConfigFile, PI_MODELS_CONFIG_PATH, resolvePiRuntimePaths, } from "./pi-models-config.js";
10
10
  export { applyWorkspacePlan, applyWorkspacePlanWithSecretProvider, } from "./plan-application.js";
@@ -60,11 +60,10 @@ export function bindConcurrentLaunch(plan, application, revisionWorkspaceDir, op
60
60
  supportedFiles.add(".codex/system-prompt.md");
61
61
  }
62
62
  break;
63
- case "kimi-code":
64
- if (plan.files.some((file) => file.relPath === ".kimi/mcp.json")) {
65
- bindFlagFile("--mcp-config-file", ".kimi/mcp.json");
66
- }
67
- break;
63
+ // kimi-code has no case: kimi-code 2.x discovers MCP only from
64
+ // <cwd>/.kimi-code/mcp.json and has no launch flag that rebinds it to the
65
+ // revision, so a concurrent kimi-code plan carrying one fails closed in
66
+ // the redirect check below.
68
67
  case "opencode": {
69
68
  const runtimeConfig = application.runtimeOnlyFiles["opencode.json"];
70
69
  if (runtimeConfig !== undefined)
@@ -2,7 +2,7 @@ import { lstatSync } from "node:fs";
2
2
  import type { AgentProfileSecretProvider } from "@tangle-network/agent-interface";
3
3
  import type { ManagedWorkspaceManifest } from "./managed-workspace-types.js";
4
4
  import type { ApplyWorkspacePlanOptions, WorkspacePlan, WorkspacePlanApplication } from "./plan-types.js";
5
- export interface PreparedManagedWorkspace {
5
+ interface PreparedManagedWorkspace {
6
6
  /** Entire profile-owned tree. The caller owns its lifetime through the turn. */
7
7
  root: string;
8
8
  /** Use as the native harness cwd only inside an isolated workspace view. */
@@ -22,3 +22,4 @@ export declare function assertPrivateManagedRoot(root: string): void;
22
22
  */
23
23
  export declare function prepareManagedWorkspacePlan(plan: WorkspacePlan, managedRoot: string, sharedWorkspaceDir: string, secretProvider: AgentProfileSecretProvider, options?: Pick<ApplyWorkspacePlanOptions, "ownership" | "runtimeOnlyFiles" | "secretFileSlotReferences">): Promise<PreparedManagedWorkspace>;
24
24
  export declare function lstatIfPresent(path: string): ReturnType<typeof lstatSync> | null;
25
+ export {};
@@ -8,24 +8,6 @@
8
8
  * state). Anything else at the path (a user's own keys, extra fields) still
9
9
  * refuses: the name alone proves nothing, same rule as `opencode.json`.
10
10
  */
11
- /** Paths owned by this rule. `.gemini/settings.json` is deliberately absent:
12
- * it is a broader user settings file the package only contributes one key
13
- * to, so its bytes cannot prove authorship. */
14
- export declare const MATERIALIZER_MCP_CONFIG_PATHS: Set<string>;
15
- /** The parsed form this rule accepts: `JSON.parse` output narrowed at the
16
- * I/O boundary by {@link parseMcpServersConfig}. */
17
- export interface McpServersConfig {
18
- mcpServers: Record<string, Record<string, unknown>>;
19
- }
20
- /** Parse workspace bytes into the one shape this rule owns, or `null` when
21
- * the bytes are anything else (not an object, other top-level keys, or
22
- * server entries that are not objects). The I/O boundary for
23
- * {@link isGeneratedMcpConfigShape}. */
24
- export declare function parseMcpServersConfig(bytes: string): McpServersConfig | null;
25
- /** Whether a parsed MCP config carries ONLY the registrations this package
26
- * generates: no extra top-level keys, and every server entry holds only
27
- * generated fields. A user's own key anywhere makes this `false`. */
28
- export declare function isGeneratedMcpConfigShape(config: McpServersConfig): boolean;
29
11
  /** Whether the existing file at `relPath` is this package's own earlier MCP
30
12
  * config output (shape rule above). `undefined` content = no regular file. */
31
13
  export declare function isMaterializerMcpConfig(relPath: string, content: () => string | undefined): boolean;
@@ -11,9 +11,9 @@
11
11
  /** Paths owned by this rule. `.gemini/settings.json` is deliberately absent:
12
12
  * it is a broader user settings file the package only contributes one key
13
13
  * to, so its bytes cannot prove authorship. */
14
- export const MATERIALIZER_MCP_CONFIG_PATHS = new Set([
14
+ const MATERIALIZER_MCP_CONFIG_PATHS = new Set([
15
15
  ".pi/mcp.json",
16
- ".kimi/mcp.json",
16
+ ".kimi-code/mcp.json",
17
17
  ]);
18
18
  const GENERATED_MCP_SERVER_KEYS = new Set([
19
19
  "command",
@@ -31,7 +31,7 @@ const GENERATED_MCP_SERVER_KEYS = new Set([
31
31
  * the bytes are anything else (not an object, other top-level keys, or
32
32
  * server entries that are not objects). The I/O boundary for
33
33
  * {@link isGeneratedMcpConfigShape}. */
34
- export function parseMcpServersConfig(bytes) {
34
+ function parseMcpServersConfig(bytes) {
35
35
  let parsed;
36
36
  try {
37
37
  parsed = JSON.parse(bytes);
@@ -60,7 +60,7 @@ export function parseMcpServersConfig(bytes) {
60
60
  /** Whether a parsed MCP config carries ONLY the registrations this package
61
61
  * generates: no extra top-level keys, and every server entry holds only
62
62
  * generated fields. A user's own key anywhere makes this `false`. */
63
- export function isGeneratedMcpConfigShape(config) {
63
+ function isGeneratedMcpConfigShape(config) {
64
64
  const keys = Object.keys(config);
65
65
  if (keys.length !== 1 || keys[0] !== "mcpServers")
66
66
  return false;
@@ -0,0 +1,25 @@
1
+ /** OpenCode-native MCP configuration used by workspace and startup paths. */
2
+ export type OpenCodeMcpConfig = Record<string, {
3
+ type: "remote";
4
+ url: string;
5
+ enabled: boolean;
6
+ headers?: Record<string, AgentProfileConfigValue>;
7
+ /** Milliseconds; OpenCode bounds this server's connect, tool listing and tool calls by it. */
8
+ timeout?: number;
9
+ /** `false` turns off OpenCode's OAuth discovery for this server. */
10
+ oauth?: false;
11
+ } | {
12
+ type: "local";
13
+ command: Array<string | AgentProfileConfigValue>;
14
+ enabled: boolean;
15
+ environment?: Record<string, AgentProfileConfigValue>;
16
+ timeout?: number;
17
+ }>;
18
+ /**
19
+ * Lower canonical MCP definitions to OpenCode's native startup configuration.
20
+ * `toolTimeouts` holds the validated `metadata.toolTimeoutMs` of each server
21
+ * that declared one; without it OpenCode applies the MCP SDK's 60 s default to
22
+ * every tool call.
23
+ */
24
+ export declare function openCodeMcpConfig(mcp: Record<string, AgentProfileMcpServer>, toolTimeouts?: ReadonlyMap<string, number>): OpenCodeMcpConfig;
25
+ import type { AgentProfileConfigValue, AgentProfileMcpServer } from "@tangle-network/agent-interface";
@@ -0,0 +1,52 @@
1
+ const carriesAuthorization = (headers) => Object.keys(headers ?? {}).some((header) => header.toLowerCase() === "authorization");
2
+ /**
3
+ * Lower canonical MCP definitions to OpenCode's native startup configuration.
4
+ * `toolTimeouts` holds the validated `metadata.toolTimeoutMs` of each server
5
+ * that declared one; without it OpenCode applies the MCP SDK's 60 s default to
6
+ * every tool call.
7
+ */
8
+ export function openCodeMcpConfig(mcp, toolTimeouts = new Map()) {
9
+ const out = {};
10
+ for (const [name, s] of Object.entries(mcp)) {
11
+ if (s.enabled === false)
12
+ continue;
13
+ // OpenCode also bounds connect and tool listing by this value, so a server
14
+ // that hangs on connect holds startup that long; a refused connection still
15
+ // fails at once.
16
+ const timeout = toolTimeouts.get(name);
17
+ if (s.url) {
18
+ if (s.transport === "sse") {
19
+ throw new Error(`OpenCode MCP server "${name}" uses unsupported SSE transport`);
20
+ }
21
+ const headers = withAppliedToolTimeout(s.headers, timeout);
22
+ out[name] = {
23
+ type: "remote",
24
+ url: s.url,
25
+ enabled: true,
26
+ ...(headers ? { headers } : {}),
27
+ ...(timeout !== undefined ? { timeout } : {}),
28
+ // A server that takes its credential from these headers has no OAuth
29
+ // flow. With OAuth on, OpenCode answers its 401 with OAuth discovery at
30
+ // the server's origin, and the agent sees that discovery's failure
31
+ // instead of the server's refusal.
32
+ ...(carriesAuthorization(s.headers) ? { oauth: false } : {}),
33
+ };
34
+ continue;
35
+ }
36
+ if (!s.command) {
37
+ throw new Error(`OpenCode MCP server "${name}" must define either url or command`);
38
+ }
39
+ if (s.cwd !== undefined) {
40
+ throw new Error(`OpenCode MCP server "${name}" does not support a cwd field`);
41
+ }
42
+ out[name] = {
43
+ type: "local",
44
+ command: [s.command, ...(s.args ?? [])],
45
+ enabled: true,
46
+ ...(s.env ? { environment: s.env } : {}),
47
+ ...(timeout !== undefined ? { timeout } : {}),
48
+ };
49
+ }
50
+ return out;
51
+ }
52
+ import { withAppliedToolTimeout } from "./mcp-timeouts.js";
@@ -0,0 +1,7 @@
1
+ export type ActiveAgentProfileMcpServer = Exclude<AgentProfileMcpServer, {
2
+ enabled: false;
3
+ }>;
4
+ export declare function mcpToolTimeouts(mcp: Record<string, ActiveAgentProfileMcpServer>, h: HarnessId, unsupported: (d: Unsupported["dimension"], r: string) => void): Map<string, number>;
5
+ export declare function withAppliedToolTimeout(headers: Record<string, AgentProfileConfigValue> | undefined, appliedTimeoutMs: number | undefined): Record<string, AgentProfileConfigValue> | undefined;
6
+ import type { AgentProfileConfigValue, AgentProfileMcpServer } from "@tangle-network/agent-interface";
7
+ import type { HarnessId, Unsupported } from "./plan-types.js";
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The header a harness sends to a remote MCP server whose tool-call timeout it
3
+ * raised, carrying the timeout it applied in milliseconds. A server may hold a
4
+ * call for nearly that long. A server that does not see the header must assume
5
+ * the harness default, 60 s for Claude Code, Codex and OpenCode.
6
+ */
7
+ const MCP_TOOL_TIMEOUT_HEADER = "X-Mcp-Tool-Timeout-Ms";
8
+ const MIN_MCP_TOOL_TIMEOUT_MS = 1_000;
9
+ const MAX_MCP_TOOL_TIMEOUT_MS = 3_600_000;
10
+ /**
11
+ * The tool-call timeout a profile asks the harness to give one MCP server,
12
+ * from `metadata.toolTimeoutMs`. Profile metadata is open in every published
13
+ * schema, so a profile carrying it validates against older consumers, which
14
+ * ignore it. A malformed value is reported, never guessed.
15
+ */
16
+ function mcpToolTimeoutMs(server, name, h, unsupported) {
17
+ const value = server.metadata?.toolTimeoutMs;
18
+ if (value === undefined)
19
+ return undefined;
20
+ if (typeof value !== "number" ||
21
+ !Number.isSafeInteger(value) ||
22
+ value < MIN_MCP_TOOL_TIMEOUT_MS ||
23
+ value > MAX_MCP_TOOL_TIMEOUT_MS) {
24
+ unsupported("mcp", `${h}: MCP server "${name}" metadata.toolTimeoutMs must be an integer from ${MIN_MCP_TOOL_TIMEOUT_MS} to ${MAX_MCP_TOOL_TIMEOUT_MS}`);
25
+ return undefined;
26
+ }
27
+ return value;
28
+ }
29
+ export function mcpToolTimeouts(mcp, h, unsupported) {
30
+ const timeouts = new Map();
31
+ for (const [name, server] of Object.entries(mcp)) {
32
+ const value = mcpToolTimeoutMs(server, name, h, unsupported);
33
+ if (value !== undefined)
34
+ timeouts.set(name, value);
35
+ }
36
+ return timeouts;
37
+ }
38
+ export function withAppliedToolTimeout(headers, appliedTimeoutMs) {
39
+ if (appliedTimeoutMs === undefined)
40
+ return headers;
41
+ return {
42
+ ...headers,
43
+ [MCP_TOOL_TIMEOUT_HEADER]: {
44
+ kind: "public",
45
+ value: String(appliedTimeoutMs),
46
+ },
47
+ };
48
+ }
package/dist/mcp.d.ts CHANGED
@@ -1,39 +1,4 @@
1
1
  export declare function materializeMcp(h: HarnessId, mcp: Record<string, ActiveAgentProfileMcpServer>, plan: WorkspacePlan, add: AddPlanFile, unsupported: (d: Unsupported["dimension"], r: string) => void): void;
2
- /**
3
- * The header a harness sends to a remote MCP server whose tool-call timeout it
4
- * raised, carrying the timeout it applied in milliseconds. A server may hold a
5
- * call for nearly that long. A server that does not see the header must assume
6
- * the harness default, 60 s for Claude Code, Codex and OpenCode.
7
- */
8
- export declare const MCP_TOOL_TIMEOUT_HEADER = "X-Mcp-Tool-Timeout-Ms";
9
- /** OpenCode-native MCP configuration used by workspace and startup paths. */
10
- export type OpenCodeMcpConfig = Record<string, {
11
- type: "remote";
12
- url: string;
13
- enabled: boolean;
14
- headers?: Record<string, AgentProfileConfigValue>;
15
- /** Milliseconds; OpenCode bounds this server's connect, tool listing and tool calls by it. */
16
- timeout?: number;
17
- /** `false` turns off OpenCode's OAuth discovery for this server. */
18
- oauth?: false;
19
- } | {
20
- type: "local";
21
- command: Array<string | AgentProfileConfigValue>;
22
- enabled: boolean;
23
- environment?: Record<string, AgentProfileConfigValue>;
24
- timeout?: number;
25
- }>;
26
- /**
27
- * Lower canonical MCP definitions to OpenCode's native startup configuration.
28
- * `toolTimeouts` holds the validated `metadata.toolTimeoutMs` of each server
29
- * that declared one; without it OpenCode applies the MCP SDK's 60 s default to
30
- * every tool call.
31
- */
32
- export declare function openCodeMcpConfig(mcp: Record<string, AgentProfileMcpServer>, toolTimeouts?: ReadonlyMap<string, number>): OpenCodeMcpConfig;
33
- import type { AgentProfileConfigValue, AgentProfileMcpServer } from "@tangle-network/agent-interface";
2
+ import { type ActiveAgentProfileMcpServer } from "./mcp-timeouts.js";
34
3
  import type { AddPlanFile } from "./plan-json.js";
35
4
  import type { HarnessId, Unsupported, WorkspacePlan } from "./plan-types.js";
36
- type ActiveAgentProfileMcpServer = Exclude<AgentProfileMcpServer, {
37
- enabled: false;
38
- }>;
39
- export {};