@tangle-network/agent-profile-materialize 0.22.2 → 0.22.4

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.
@@ -1,4 +1,22 @@
1
1
  import { join } from "node:path";
2
+ /**
3
+ * Paths OpenCode discovers relative to its native cwd: content-only resources,
4
+ * skills, and its standard agent and command definitions (OpenCode reads
5
+ * `.opencode/{agent,agents}/**\/*.md` and `.opencode/{command,commands}/**\/*.md`).
6
+ */
7
+ const SECRET_REVISION_NATIVE_PATH = /^(?:reference\/|\.opencode\/skills\/|\.opencode\/(?:agents?|commands?)\/(?:[^./][^/]*\/)*[^./][^/]*\.md$)/u;
8
+ /**
9
+ * A secret OpenCode revision runs OpenCode from its immutable, digest-attested
10
+ * revision tree, so a file at one of OpenCode's own discovery paths is visible
11
+ * there without a shared-workspace write. The binding stays limited to
12
+ * OpenCode's standard paths, never an app's own directory.
13
+ */
14
+ function ridesSecretRevision(plan, file, options) {
15
+ return (options.secretOpenCodeRevisionCwd === true &&
16
+ plan.harness === "opencode" &&
17
+ (file.root ?? "workspace") === "workspace" &&
18
+ SECRET_REVISION_NATIVE_PATH.test(file.relPath));
19
+ }
2
20
  export function bindConcurrentLaunch(plan, application, revisionWorkspaceDir, options = {}) {
3
21
  if (plan.requires?.length) {
4
22
  throw new Error(`Concurrent ${plan.harness} profile needs a native extension that this launcher cannot attest`);
@@ -67,17 +85,9 @@ export function bindConcurrentLaunch(plan, application, revisionWorkspaceDir, op
67
85
  }
68
86
  }
69
87
  for (const file of plan.files) {
88
+ if (ridesSecretRevision(plan, file, options))
89
+ continue;
70
90
  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
91
  throw new Error(`Concurrent ${plan.harness} profile resource ${file.relPath} needs a shared-workspace collision-safe copy`);
82
92
  }
83
93
  if ((file.root ?? "workspace") !== "workspace" ||
package/dist/mcp.d.ts CHANGED
@@ -3,7 +3,7 @@ export declare function materializeMcp(h: HarnessId, mcp: Record<string, ActiveA
3
3
  * The header a harness sends to a remote MCP server whose tool-call timeout it
4
4
  * raised, carrying the timeout it applied in milliseconds. A server may hold a
5
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 and Codex.
6
+ * the harness default, 60 s for Claude Code, Codex and OpenCode.
7
7
  */
8
8
  export declare const MCP_TOOL_TIMEOUT_HEADER = "X-Mcp-Tool-Timeout-Ms";
9
9
  /** OpenCode-native MCP configuration used by workspace and startup paths. */
@@ -12,14 +12,24 @@ export type OpenCodeMcpConfig = Record<string, {
12
12
  url: string;
13
13
  enabled: boolean;
14
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;
15
19
  } | {
16
20
  type: "local";
17
21
  command: Array<string | AgentProfileConfigValue>;
18
22
  enabled: boolean;
19
23
  environment?: Record<string, AgentProfileConfigValue>;
24
+ timeout?: number;
20
25
  }>;
21
- /** Lower canonical MCP definitions to OpenCode's native startup configuration. */
22
- export declare function openCodeMcpConfig(mcp: Record<string, AgentProfileMcpServer>): OpenCodeMcpConfig;
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;
23
33
  import type { AgentProfileConfigValue, AgentProfileMcpServer } from "@tangle-network/agent-interface";
24
34
  import type { AddPlanFile } from "./plan-json.js";
25
35
  import type { HarnessId, Unsupported, WorkspacePlan } from "./plan-types.js";
package/dist/mcp.js CHANGED
@@ -58,7 +58,7 @@ export function materializeMcp(h, mcp, plan, add, unsupported) {
58
58
  // opencode.json "mcp" block (local|remote).
59
59
  add(OPENCODE_CONFIG_FILE, serializeWorkspacePlanJson({
60
60
  $schema: OPENCODE_CONFIG_SCHEMA,
61
- mcp: openCodeMcpConfig(mcp),
61
+ mcp: openCodeMcpConfig(mcp, mcpToolTimeouts(mcp, h, unsupported)),
62
62
  }));
63
63
  break;
64
64
  case "gemini": {
@@ -156,7 +156,7 @@ function tomlInlineStringTable(values) {
156
156
  * The header a harness sends to a remote MCP server whose tool-call timeout it
157
157
  * raised, carrying the timeout it applied in milliseconds. A server may hold a
158
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 and Codex.
159
+ * the harness default, 60 s for Claude Code, Codex and OpenCode.
160
160
  */
161
161
  export const MCP_TOOL_TIMEOUT_HEADER = "X-Mcp-Tool-Timeout-Ms";
162
162
  const MIN_MCP_TOOL_TIMEOUT_MS = 1_000;
@@ -270,21 +270,38 @@ function kimiMcpServer(server) {
270
270
  ...(server.cwd ? { cwd: server.cwd } : {}),
271
271
  };
272
272
  }
273
- /** Lower canonical MCP definitions to OpenCode's native startup configuration. */
274
- export function openCodeMcpConfig(mcp) {
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()) {
275
281
  const out = {};
276
282
  for (const [name, s] of Object.entries(mcp)) {
277
283
  if (s.enabled === false)
278
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);
279
289
  if (s.url) {
280
290
  if (s.transport === "sse") {
281
291
  throw new Error(`OpenCode MCP server "${name}" uses unsupported SSE transport`);
282
292
  }
293
+ const headers = withAppliedToolTimeout(s.headers, timeout);
283
294
  out[name] = {
284
295
  type: "remote",
285
296
  url: s.url,
286
297
  enabled: true,
287
- ...(s.headers ? { headers: s.headers } : {}),
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 } : {}),
288
305
  };
289
306
  continue;
290
307
  }
@@ -299,6 +316,7 @@ export function openCodeMcpConfig(mcp) {
299
316
  command: [s.command, ...(s.args ?? [])],
300
317
  enabled: true,
301
318
  ...(s.env ? { environment: s.env } : {}),
319
+ ...(timeout !== undefined ? { timeout } : {}),
302
320
  };
303
321
  }
304
322
  return out;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-profile-materialize",
3
- "version": "0.22.2",
3
+ "version": "0.22.4",
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": {
@@ -41,6 +41,7 @@
41
41
  "test": "pnpm run test:installed-peer && pnpm run test:current-peer",
42
42
  "test:installed-peer": "vitest run",
43
43
  "test:current-peer": "vitest run --config vitest.current-peer.config.ts",
44
+ "test:root": "node ../../scripts/ci/vitest-as-root.mjs tests/concurrent-subagents.test.ts tests/secret-provider-revision.test.ts",
44
45
  "test:watch": "vitest"
45
46
  }
46
47
  }