@tangle-network/agent-profile-materialize 0.27.0 → 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.
package/dist/mcp.js CHANGED
@@ -1,59 +1,14 @@
1
1
  export function materializeMcp(h, mcp, plan, add, unsupported) {
2
2
  switch (h) {
3
- case "claude-code": {
4
- // Claude applies one MCP tool-call timeout to every server, so the
5
- // longest declared one wins and is what each declaring server is told.
6
- const timeouts = mcpToolTimeouts(mcp, h, unsupported);
7
- const toolTimeoutMs = timeouts.size > 0 ? Math.max(...timeouts.values()) : undefined;
8
- if (toolTimeoutMs !== undefined) {
9
- setPlanEnvironment(plan, "MCP_TOOL_TIMEOUT", String(toolTimeoutMs));
10
- }
11
- const servers = {};
12
- for (const [name, server] of Object.entries(mcp)) {
13
- if (server.url) {
14
- const headers = withAppliedToolTimeout(server.headers, timeouts.has(name) ? toolTimeoutMs : undefined);
15
- servers[name] = {
16
- type: server.transport ?? "http",
17
- url: server.url,
18
- ...(headers ? { headers } : {}),
19
- };
20
- continue;
21
- }
22
- if (server.cwd !== undefined) {
23
- unsupported("mcp", `${h}: local MCP server "${name}" does not support a cwd field`);
24
- }
25
- servers[name] = {
26
- type: "stdio",
27
- command: server.command,
28
- ...(server.args ? { args: server.args } : {}),
29
- ...(server.env ? { env: server.env } : {}),
30
- };
31
- }
32
- const relPath = ".tangle/claude-mcp.json";
33
- add(relPath, serializeWorkspacePlanJson({ mcpServers: servers }));
34
- plan.flags.push("--mcp-config", relPath, "--strict-mcp-config");
3
+ case "claude-code":
4
+ materializeClaudeMcp(h, mcp, plan, add, unsupported);
35
5
  break;
36
- }
37
6
  case "nanoclaw":
38
7
  unsupported("mcp", "nanoclaw MCP must be injected through options.mcpServers by its SDK adapter");
39
8
  break;
40
- case "codex": {
41
- const timeouts = mcpToolTimeouts(mcp, h, unsupported);
42
- const servers = [];
43
- for (const [name, server] of Object.entries(mcp).sort(([left], [right]) => compareStrings(left, right))) {
44
- if (server.url && server.transport === "sse") {
45
- unsupported("mcp", `codex: remote MCP server "${name}" uses unsupported SSE transport`);
46
- continue;
47
- }
48
- servers.push(concatWorkspacePlanArguments(`${tomlStr(name)} = `, codexMcpServerToml(name, server, plan, timeouts.get(name))));
49
- }
50
- if (servers.length > 0) {
51
- // Runtime overrides do not require persisted project trust. Header values
52
- // ride environment variables so credentials never appear in argv.
53
- plan.flags.push("-c", concatWorkspacePlanArguments("mcp_servers={ ", joinWorkspacePlanArguments(servers, ", "), " }"));
54
- }
9
+ case "codex":
10
+ materializeCodexMcp(h, mcp, plan, unsupported);
55
11
  break;
56
- }
57
12
  case "opencode":
58
13
  // opencode.json "mcp" block (local|remote).
59
14
  add(OPENCODE_CONFIG_FILE, serializeWorkspacePlanJson({
@@ -61,70 +16,30 @@ export function materializeMcp(h, mcp, plan, add, unsupported) {
61
16
  mcp: openCodeMcpConfig(mcp, mcpToolTimeouts(mcp, h, unsupported)),
62
17
  }));
63
18
  break;
64
- case "gemini": {
65
- const servers = {};
66
- for (const [name, server] of Object.entries(mcp)) {
67
- if (server.url) {
68
- servers[name] = {
69
- [server.transport === "sse" ? "url" : "httpUrl"]: server.url,
70
- ...(server.headers ? { headers: server.headers } : {}),
71
- };
72
- continue;
73
- }
74
- servers[name] = {
75
- command: server.command,
76
- ...(server.args ? { args: server.args } : {}),
77
- ...(server.env ? { env: server.env } : {}),
78
- ...(server.cwd ? { cwd: server.cwd } : {}),
79
- };
80
- }
81
- add(".gemini/settings.json", serializeWorkspacePlanJson({ mcpServers: servers }));
19
+ case "gemini":
20
+ materializeGeminiMcp(mcp, add);
82
21
  break;
83
- }
84
22
  case "kimi-code": {
85
- // NO cwd auto-discovery — kimi needs an explicit --mcp-config-file FLAG.
86
- const rel = ".kimi/mcp.json";
23
+ // kimi-code 2.x auto-discovers project MCP config from
24
+ // <cwd>/.kimi-code/mcp.json (2.3.0 dist/main.mjs:
25
+ // `project: join(input.cwd, ".kimi-code", "mcp.json")`; live-probed on
26
+ // 2.2.0). The previous lowering — .kimi/mcp.json plus a
27
+ // --mcp-config-file flag — targeted the legacy Python kimi-cli:
28
+ // kimi-code 2.x exits 1 with `error: unknown option '--mcp-config-file'`
29
+ // and never reads that file. Project MCP is gated behind workspace
30
+ // trust; an unattended executor exports KIMI_CODE_TRUST_WORKSPACE=1
31
+ // itself — the plan never forces trust, that posture is the launcher's.
32
+ const rel = ".kimi-code/mcp.json";
87
33
  const servers = Object.fromEntries(Object.entries(mcp).map(([name, server]) => [
88
34
  name,
89
35
  kimiMcpServer(server),
90
36
  ]));
91
37
  add(rel, serializeWorkspacePlanJson({ mcpServers: servers }));
92
- plan.flags.push("--mcp-config-file", rel);
93
38
  break;
94
39
  }
95
- case "pi": {
96
- // pi ships NO MCP. It arrives from the `pi-mcp-adapter` extension, which discovers
97
- // `<cwd>/.pi/mcp.json` (adapter 2.17.0 config.ts:17) and also registers a `--mcp-config`
98
- // flag (index.ts:252). The flag is registered BY the extension, so passing it when the
99
- // extension is absent is not a fallback — it is an unknown flag.
100
- //
101
- // This was previously marked unsupported because a workspace file cannot install an
102
- // extension. That reasoning was right about the constraint and wrong about the conclusion:
103
- // the file half is perfectly materializable, and the half that is not becomes a declared
104
- // requirement the executor enforces. Marking the whole dimension unsupported pushed both
105
- // halves onto every consumer, and two of them independently rebuilt it.
106
- const rel = ".pi/mcp.json";
107
- const servers = Object.fromEntries(Object.entries(mcp).map(([name, server]) => [
108
- name,
109
- // `directTools` promotes this server's verbs to first-class pi tools. Without it the
110
- // agent gets one generic `mcp` tool and must connect, then describe, then call — three
111
- // round trips of discovery before it can do anything. Per-server, because the adapter
112
- // lets a per-server value override the global `settings.directTools` (direct-tools.ts:135).
113
- { ...piMcpServer(server), directTools: true },
114
- ]));
115
- // serializeWorkspacePlanJson, NOT JSON.stringify: it unwraps public config values to their
116
- // bytes and lifts any secret-ref into a tracked slot the applier resolves. Stringifying
117
- // straight would write `{"kind":"public","value":"..."}` into the file as an argv entry.
118
- add(rel, serializeWorkspacePlanJson({ mcpServers: servers }));
119
- plan.requires?.push({
120
- kind: "harness-extension",
121
- dimension: "mcp",
122
- id: "npm:pi-mcp-adapter",
123
- reason: "pi has no built-in MCP; without this extension the servers in .pi/mcp.json are never read and the agent starts with none of their tools",
124
- remedy: "pi install npm:pi-mcp-adapter",
125
- });
40
+ case "pi":
41
+ materializePiMcp(mcp, plan, add);
126
42
  break;
127
- }
128
43
  case "prime":
129
44
  // The fork's MCP config surface (`mcpServers` in <agentDir>/settings.json,
130
45
  // fork settings-manager.ts:98-120,219) IS read at startup, but a
@@ -146,59 +61,111 @@ export function materializeMcp(h, mcp, plan, add, unsupported) {
146
61
  break;
147
62
  }
148
63
  }
149
- function tomlInlineStringTable(values) {
150
- return `{ ${Object.entries(values)
151
- .sort(([left], [right]) => compareStrings(left, right))
152
- .map(([key, value]) => `${tomlStr(key)} = ${tomlStr(value)}`)
153
- .join(", ")} }`;
64
+ function materializeClaudeMcp(h, mcp, plan, add, unsupported) {
65
+ // Claude applies one MCP tool-call timeout to every server, so the
66
+ // longest declared one wins and is what each declaring server is told.
67
+ const timeouts = mcpToolTimeouts(mcp, h, unsupported);
68
+ const toolTimeoutMs = timeouts.size > 0 ? Math.max(...timeouts.values()) : undefined;
69
+ if (toolTimeoutMs !== undefined) {
70
+ setPlanEnvironment(plan, "MCP_TOOL_TIMEOUT", String(toolTimeoutMs));
71
+ }
72
+ const servers = {};
73
+ for (const [name, server] of Object.entries(mcp)) {
74
+ if (server.url) {
75
+ const headers = withAppliedToolTimeout(server.headers, timeouts.has(name) ? toolTimeoutMs : undefined);
76
+ servers[name] = {
77
+ type: server.transport ?? "http",
78
+ url: server.url,
79
+ ...(headers ? { headers } : {}),
80
+ };
81
+ continue;
82
+ }
83
+ if (server.cwd !== undefined) {
84
+ unsupported("mcp", `${h}: local MCP server "${name}" does not support a cwd field`);
85
+ }
86
+ servers[name] = {
87
+ type: "stdio",
88
+ command: server.command,
89
+ ...(server.args ? { args: server.args } : {}),
90
+ ...(server.env ? { env: server.env } : {}),
91
+ };
92
+ }
93
+ const relPath = ".tangle/claude-mcp.json";
94
+ add(relPath, serializeWorkspacePlanJson({ mcpServers: servers }));
95
+ plan.flags.push("--mcp-config", relPath, "--strict-mcp-config");
154
96
  }
155
- /**
156
- * The header a harness sends to a remote MCP server whose tool-call timeout it
157
- * raised, carrying the timeout it applied in milliseconds. A server may hold a
158
- * call for nearly that long. A server that does not see the header must assume
159
- * the harness default, 60 s for Claude Code, Codex and OpenCode.
160
- */
161
- export const MCP_TOOL_TIMEOUT_HEADER = "X-Mcp-Tool-Timeout-Ms";
162
- const MIN_MCP_TOOL_TIMEOUT_MS = 1_000;
163
- const MAX_MCP_TOOL_TIMEOUT_MS = 3_600_000;
164
- /**
165
- * The tool-call timeout a profile asks the harness to give one MCP server,
166
- * from `metadata.toolTimeoutMs`. Profile metadata is open in every published
167
- * schema, so a profile carrying it validates against older consumers, which
168
- * ignore it. A malformed value is reported, never guessed.
169
- */
170
- function mcpToolTimeoutMs(server, name, h, unsupported) {
171
- const value = server.metadata?.toolTimeoutMs;
172
- if (value === undefined)
173
- return undefined;
174
- if (typeof value !== "number" ||
175
- !Number.isSafeInteger(value) ||
176
- value < MIN_MCP_TOOL_TIMEOUT_MS ||
177
- value > MAX_MCP_TOOL_TIMEOUT_MS) {
178
- unsupported("mcp", `${h}: MCP server "${name}" metadata.toolTimeoutMs must be an integer from ${MIN_MCP_TOOL_TIMEOUT_MS} to ${MAX_MCP_TOOL_TIMEOUT_MS}`);
179
- return undefined;
97
+ function materializeCodexMcp(h, mcp, plan, unsupported) {
98
+ const timeouts = mcpToolTimeouts(mcp, h, unsupported);
99
+ const servers = [];
100
+ for (const [name, server] of Object.entries(mcp).sort(([left], [right]) => compareStrings(left, right))) {
101
+ if (server.url && server.transport === "sse") {
102
+ unsupported("mcp", `codex: remote MCP server "${name}" uses unsupported SSE transport`);
103
+ continue;
104
+ }
105
+ servers.push(concatWorkspacePlanArguments(`${tomlStr(name)} = `, codexMcpServerToml(name, server, plan, timeouts.get(name))));
106
+ }
107
+ if (servers.length > 0) {
108
+ // Runtime overrides do not require persisted project trust. Header values
109
+ // ride environment variables so credentials never appear in argv.
110
+ plan.flags.push("-c", concatWorkspacePlanArguments("mcp_servers={ ", joinWorkspacePlanArguments(servers, ", "), " }"));
180
111
  }
181
- return value;
182
112
  }
183
- function mcpToolTimeouts(mcp, h, unsupported) {
184
- const timeouts = new Map();
113
+ function materializeGeminiMcp(mcp, add) {
114
+ const servers = {};
185
115
  for (const [name, server] of Object.entries(mcp)) {
186
- const value = mcpToolTimeoutMs(server, name, h, unsupported);
187
- if (value !== undefined)
188
- timeouts.set(name, value);
116
+ if (server.url) {
117
+ servers[name] = {
118
+ [server.transport === "sse" ? "url" : "httpUrl"]: server.url,
119
+ ...(server.headers ? { headers: server.headers } : {}),
120
+ };
121
+ continue;
122
+ }
123
+ servers[name] = {
124
+ command: server.command,
125
+ ...(server.args ? { args: server.args } : {}),
126
+ ...(server.env ? { env: server.env } : {}),
127
+ ...(server.cwd ? { cwd: server.cwd } : {}),
128
+ };
189
129
  }
190
- return timeouts;
130
+ add(".gemini/settings.json", serializeWorkspacePlanJson({ mcpServers: servers }));
191
131
  }
192
- function withAppliedToolTimeout(headers, appliedTimeoutMs) {
193
- if (appliedTimeoutMs === undefined)
194
- return headers;
195
- return {
196
- ...headers,
197
- [MCP_TOOL_TIMEOUT_HEADER]: {
198
- kind: "public",
199
- value: String(appliedTimeoutMs),
200
- },
201
- };
132
+ function materializePiMcp(mcp, plan, add) {
133
+ // pi ships NO MCP. It arrives from the `pi-mcp-adapter` extension, which discovers
134
+ // `<cwd>/.pi/mcp.json` (adapter 2.17.0 config.ts:17) and also registers a `--mcp-config`
135
+ // flag (index.ts:252). The flag is registered BY the extension, so passing it when the
136
+ // extension is absent is not a fallback — it is an unknown flag.
137
+ //
138
+ // This was previously marked unsupported because a workspace file cannot install an
139
+ // extension. That reasoning was right about the constraint and wrong about the conclusion:
140
+ // the file half is perfectly materializable, and the half that is not becomes a declared
141
+ // requirement the executor enforces. Marking the whole dimension unsupported pushed both
142
+ // halves onto every consumer, and two of them independently rebuilt it.
143
+ const rel = ".pi/mcp.json";
144
+ const servers = Object.fromEntries(Object.entries(mcp).map(([name, server]) => [
145
+ name,
146
+ // `directTools` promotes this server's verbs to first-class pi tools. Without it the
147
+ // agent gets one generic `mcp` tool and must connect, then describe, then call — three
148
+ // round trips of discovery before it can do anything. Per-server, because the adapter
149
+ // lets a per-server value override the global `settings.directTools` (direct-tools.ts:135).
150
+ { ...piMcpServer(server), directTools: true },
151
+ ]));
152
+ // serializeWorkspacePlanJson, NOT JSON.stringify: it unwraps public config values to their
153
+ // bytes and lifts any secret-ref into a tracked slot the applier resolves. Stringifying
154
+ // straight would write `{"kind":"public","value":"..."}` into the file as an argv entry.
155
+ add(rel, serializeWorkspacePlanJson({ mcpServers: servers }));
156
+ plan.requires?.push({
157
+ kind: "harness-extension",
158
+ dimension: "mcp",
159
+ id: "npm:pi-mcp-adapter",
160
+ reason: "pi has no built-in MCP; without this extension the servers in .pi/mcp.json are never read and the agent starts with none of their tools",
161
+ remedy: "pi install npm:pi-mcp-adapter",
162
+ });
163
+ }
164
+ function tomlInlineStringTable(values) {
165
+ return `{ ${Object.entries(values)
166
+ .sort(([left], [right]) => compareStrings(left, right))
167
+ .map(([key, value]) => `${tomlStr(key)} = ${tomlStr(value)}`)
168
+ .join(", ")} }`;
202
169
  }
203
170
  function codexMcpServerToml(name, server, plan, toolTimeoutMs) {
204
171
  const fields = [];
@@ -270,58 +237,9 @@ function kimiMcpServer(server) {
270
237
  ...(server.cwd ? { cwd: server.cwd } : {}),
271
238
  };
272
239
  }
273
- const carriesAuthorization = (headers) => Object.keys(headers ?? {}).some((header) => header.toLowerCase() === "authorization");
274
- /**
275
- * Lower canonical MCP definitions to OpenCode's native startup configuration.
276
- * `toolTimeouts` holds the validated `metadata.toolTimeoutMs` of each server
277
- * that declared one; without it OpenCode applies the MCP SDK's 60 s default to
278
- * every tool call.
279
- */
280
- export function openCodeMcpConfig(mcp, toolTimeouts = new Map()) {
281
- const out = {};
282
- for (const [name, s] of Object.entries(mcp)) {
283
- if (s.enabled === false)
284
- continue;
285
- // OpenCode also bounds connect and tool listing by this value, so a server
286
- // that hangs on connect holds startup that long; a refused connection still
287
- // fails at once.
288
- const timeout = toolTimeouts.get(name);
289
- if (s.url) {
290
- if (s.transport === "sse") {
291
- throw new Error(`OpenCode MCP server "${name}" uses unsupported SSE transport`);
292
- }
293
- const headers = withAppliedToolTimeout(s.headers, timeout);
294
- out[name] = {
295
- type: "remote",
296
- url: s.url,
297
- enabled: true,
298
- ...(headers ? { headers } : {}),
299
- ...(timeout !== undefined ? { timeout } : {}),
300
- // A server that takes its credential from these headers has no OAuth
301
- // flow. With OAuth on, OpenCode answers its 401 with OAuth discovery at
302
- // the server's origin, and the agent sees that discovery's failure
303
- // instead of the server's refusal.
304
- ...(carriesAuthorization(s.headers) ? { oauth: false } : {}),
305
- };
306
- continue;
307
- }
308
- if (!s.command) {
309
- throw new Error(`OpenCode MCP server "${name}" must define either url or command`);
310
- }
311
- if (s.cwd !== undefined) {
312
- throw new Error(`OpenCode MCP server "${name}" does not support a cwd field`);
313
- }
314
- out[name] = {
315
- type: "local",
316
- command: [s.command, ...(s.args ?? [])],
317
- enabled: true,
318
- ...(s.env ? { environment: s.env } : {}),
319
- ...(timeout !== undefined ? { timeout } : {}),
320
- };
321
- }
322
- return out;
323
- }
324
240
  import { OPENCODE_CONFIG_FILE, OPENCODE_CONFIG_SCHEMA } from "./harness.js";
241
+ import { openCodeMcpConfig } from "./mcp-opencode.js";
242
+ import { mcpToolTimeouts, withAppliedToolTimeout, } from "./mcp-timeouts.js";
325
243
  import { sha256 } from "./plan-digest.js";
326
244
  import { concatWorkspacePlanArguments, joinWorkspacePlanArguments, serializeWorkspacePlanJson, tomlProfileConfigValue, tomlStr, } from "./plan-json.js";
327
245
  import { setPlanEnvironment } from "./profile-plan.js";
@@ -4,15 +4,10 @@ export interface SerializedPlanFileContent {
4
4
  secretSlots?: PlanFileSecretSlot[];
5
5
  }
6
6
  export type AddPlanFile = (path: string, content: string | SerializedPlanFileContent, mode?: number) => void;
7
- export declare function isProfilePublicConfigValue(value: unknown): value is Extract<AgentProfileConfigValue, {
8
- kind: "public";
9
- }>;
10
- export declare function isProfileSecretRef(value: unknown): value is AgentProfileSecretRef;
11
7
  export declare function workspacePlanConfigValue(value: AgentProfileConfigValue): WorkspacePlanConfigValue;
12
8
  export declare function serializeWorkspacePlanJson(value: unknown): SerializedPlanFileContent;
13
- export declare function workspacePlanArgumentSegments(value: WorkspacePlanArgument): Array<string | WorkspacePlanSecretSegment>;
14
9
  export declare function concatWorkspacePlanArguments(...values: WorkspacePlanArgument[]): WorkspacePlanArgument;
15
10
  export declare function joinWorkspacePlanArguments(values: WorkspacePlanArgument[], separator: string): WorkspacePlanArgument;
16
11
  export declare function tomlProfileConfigValue(value: AgentProfileConfigValue): WorkspacePlanArgument;
17
- import type { AgentProfileConfigValue, AgentProfileSecretRef } from "@tangle-network/agent-interface";
18
- import type { PlanFileSecretSlot, WorkspacePlanArgument, WorkspacePlanConfigValue, WorkspacePlanSecretSegment } from "./plan-types.js";
12
+ import type { AgentProfileConfigValue } from "@tangle-network/agent-interface";
13
+ import type { PlanFileSecretSlot, WorkspacePlanArgument, WorkspacePlanConfigValue } from "./plan-types.js";
package/dist/plan-json.js CHANGED
@@ -1,12 +1,12 @@
1
1
  export const tomlStr = (s) => JSON.stringify(s); // valid TOML basic string
2
- export function isProfilePublicConfigValue(value) {
2
+ function isProfilePublicConfigValue(value) {
3
3
  return (typeof value === "object" &&
4
4
  value !== null &&
5
5
  !Array.isArray(value) &&
6
6
  value.kind === "public" &&
7
7
  typeof value.value === "string");
8
8
  }
9
- export function isProfileSecretRef(value) {
9
+ function isProfileSecretRef(value) {
10
10
  return (typeof value === "object" &&
11
11
  value !== null &&
12
12
  !Array.isArray(value) &&
@@ -54,7 +54,7 @@ export function serializeWorkspacePlanJson(value) {
54
54
  ...(secretSlots.length === 0 ? {} : { secretSlots }),
55
55
  };
56
56
  }
57
- export function workspacePlanArgumentSegments(value) {
57
+ function workspacePlanArgumentSegments(value) {
58
58
  return typeof value === "string" ? [value] : [...value.segments];
59
59
  }
60
60
  export function concatWorkspacePlanArguments(...values) {
@@ -7,9 +7,6 @@ export interface NormalizedPlanFile {
7
7
  ownership: NonNullable<PlanFile["ownership"]>;
8
8
  secretSlots: PlanFileSecretSlot[];
9
9
  }
10
- export type JsonValue = null | boolean | number | string | JsonValue[] | {
11
- [key: string]: JsonValue;
12
- };
13
10
  export declare function normalizePlanFiles(files: PlanFile[]): NormalizedPlanFile[];
14
11
  export declare function normalizeWorkspacePlanArgument(value: WorkspacePlanArgument): WorkspacePlanArgument;
15
12
  import type { PlanFile, PlanFileRoot, PlanFileSecretSlot, WorkspacePlanArgument } from "./plan-types.js";
@@ -174,7 +174,7 @@ export interface WorkspacePlan {
174
174
  files: PlanFile[];
175
175
  /** Env vars to set on the spawned process (for harnesses needing them). */
176
176
  env: Record<string, WorkspacePlanConfigValue>;
177
- /** Extra CLI flags to pass (e.g. kimi --mcp-config-file <path>). */
177
+ /** Extra CLI flags to pass (e.g. claude --mcp-config <path>). */
178
178
  flags: WorkspacePlanArgument[];
179
179
  /**
180
180
  * Exact text that REPLACES the harness's own system prompt, for harnesses