@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.
- package/dist/execution-controls.js +119 -111
- package/dist/harness.d.ts +0 -25
- package/dist/harness.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/managed-workspace-bind.js +4 -5
- package/dist/managed-workspace-stage.d.ts +2 -1
- package/dist/mcp-config-ownership.d.ts +0 -18
- package/dist/mcp-config-ownership.js +4 -4
- package/dist/mcp-opencode.d.ts +25 -0
- package/dist/mcp-opencode.js +52 -0
- package/dist/mcp-timeouts.d.ts +7 -0
- package/dist/mcp-timeouts.js +48 -0
- package/dist/mcp.d.ts +1 -36
- package/dist/mcp.js +119 -201
- package/dist/plan-json.d.ts +2 -7
- package/dist/plan-json.js +3 -3
- package/dist/plan-types.d.ts +1 -1
- package/dist/prime-profile.js +124 -111
- package/dist/profile-bundle-lru.d.ts +2 -1
- package/dist/secret-concurrent-revision.js +45 -29
- package/dist/workspace-apply-publish.d.ts +26 -0
- package/dist/workspace-apply-publish.js +178 -0
- package/dist/workspace-apply.js +60 -196
- package/package.json +4 -4
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {};
|