@tangle-network/agent-profile-materialize 1.1.6 → 1.1.8
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/index.d.ts +1226 -29
- package/dist/index.js +4987 -23
- package/package.json +7 -5
- package/dist/candidate-types.d.ts +0 -36
- package/dist/candidate-types.js +0 -1
- package/dist/candidate.d.ts +0 -9
- package/dist/candidate.js +0 -283
- package/dist/commands.d.ts +0 -2
- package/dist/commands.js +0 -32
- package/dist/control-maps.d.ts +0 -58
- package/dist/control-maps.js +0 -277
- package/dist/execution-controls.d.ts +0 -5
- package/dist/execution-controls.js +0 -199
- package/dist/harness-prompt.d.ts +0 -22
- package/dist/harness-prompt.js +0 -64
- package/dist/harness.d.ts +0 -131
- package/dist/harness.js +0 -416
- package/dist/hooks.d.ts +0 -6
- package/dist/hooks.js +0 -250
- package/dist/managed-workspace-bind.d.ts +0 -4
- package/dist/managed-workspace-bind.js +0 -112
- package/dist/managed-workspace-stage.d.ts +0 -25
- package/dist/managed-workspace-stage.js +0 -199
- package/dist/managed-workspace-types.d.ts +0 -38
- package/dist/managed-workspace-types.js +0 -1
- package/dist/managed-workspace.d.ts +0 -37
- package/dist/managed-workspace.js +0 -290
- package/dist/mcp-config-ownership.d.ts +0 -13
- package/dist/mcp-config-ownership.js +0 -79
- package/dist/mcp-opencode.d.ts +0 -25
- package/dist/mcp-opencode.js +0 -52
- package/dist/mcp-timeouts.d.ts +0 -7
- package/dist/mcp-timeouts.js +0 -48
- package/dist/mcp.d.ts +0 -4
- package/dist/mcp.js +0 -246
- package/dist/model-id.d.ts +0 -55
- package/dist/model-id.js +0 -67
- package/dist/pi-models-config.d.ts +0 -32
- package/dist/pi-models-config.js +0 -65
- package/dist/plan-application.d.ts +0 -10
- package/dist/plan-application.js +0 -190
- package/dist/plan-digest.d.ts +0 -18
- package/dist/plan-digest.js +0 -116
- package/dist/plan-json.d.ts +0 -13
- package/dist/plan-json.js +0 -91
- package/dist/plan-normalize.d.ts +0 -12
- package/dist/plan-normalize.js +0 -168
- package/dist/plan-prompt.d.ts +0 -38
- package/dist/plan-prompt.js +0 -49
- package/dist/plan-support.d.ts +0 -9
- package/dist/plan-support.js +0 -22
- package/dist/plan-types.d.ts +0 -341
- package/dist/plan-types.js +0 -105
- package/dist/prime-agent-dir.d.ts +0 -64
- package/dist/prime-agent-dir.js +0 -173
- package/dist/prime-fork.d.ts +0 -155
- package/dist/prime-fork.js +0 -262
- package/dist/prime-profile.d.ts +0 -88
- package/dist/prime-profile.js +0 -297
- package/dist/profile-bundle-cache.d.ts +0 -37
- package/dist/profile-bundle-cache.js +0 -320
- package/dist/profile-bundle-key.d.ts +0 -69
- package/dist/profile-bundle-key.js +0 -62
- package/dist/profile-bundle-lru.d.ts +0 -56
- package/dist/profile-bundle-lru.js +0 -220
- package/dist/profile-materializer.d.ts +0 -24
- package/dist/profile-materializer.js +0 -114
- package/dist/profile-plan.d.ts +0 -16
- package/dist/profile-plan.js +0 -90
- package/dist/profile-prompt.d.ts +0 -4
- package/dist/profile-prompt.js +0 -107
- package/dist/profile-support.d.ts +0 -8
- package/dist/profile-support.js +0 -6
- package/dist/resource-lowering.d.ts +0 -6
- package/dist/resource-lowering.js +0 -103
- package/dist/resource-resolution.d.ts +0 -27
- package/dist/resource-resolution.js +0 -265
- package/dist/secret-concurrent-revision.d.ts +0 -30
- package/dist/secret-concurrent-revision.js +0 -291
- package/dist/secret-session-view.d.ts +0 -36
- package/dist/secret-session-view.js +0 -179
- package/dist/subagents.d.ts +0 -6
- package/dist/subagents.js +0 -137
- package/dist/tool-bin-path.d.ts +0 -23
- package/dist/tool-bin-path.js +0 -61
- package/dist/workspace-apply-files.d.ts +0 -57
- package/dist/workspace-apply-files.js +0 -193
- package/dist/workspace-apply-publish.d.ts +0 -22
- package/dist/workspace-apply-publish.js +0 -178
- package/dist/workspace-apply.d.ts +0 -11
- package/dist/workspace-apply.js +0 -124
- package/dist/workspace-files.d.ts +0 -8
- package/dist/workspace-files.js +0 -46
- package/dist/workspace-launch.d.ts +0 -3
- package/dist/workspace-launch.js +0 -13
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { chmodSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, writeFileSync, } from "node:fs";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
import { bindConcurrentLaunch } from "./managed-workspace-bind.js";
|
|
5
|
-
import { assertPrivateManagedRoot, lstatIfPresent, prepareManagedWorkspacePlan, } from "./managed-workspace-stage.js";
|
|
6
|
-
import { assertWorkspacePlanSupported } from "./plan-support.js";
|
|
7
|
-
/** Create the two executor-owned roots on one filesystem. */
|
|
8
|
-
export function ensurePublicConcurrentProfileRoots(base) {
|
|
9
|
-
if (process.getuid?.() !== 0 || !base || resolve(base) !== base) {
|
|
10
|
-
throw new Error("Concurrent profile roots require an absolute path and a root executor");
|
|
11
|
-
}
|
|
12
|
-
if (!lstatIfPresent(base)) {
|
|
13
|
-
mkdirSync(base, { mode: 0o711 });
|
|
14
|
-
chmodSync(base, 0o711);
|
|
15
|
-
}
|
|
16
|
-
const managedRoot = join(base, "private");
|
|
17
|
-
const launchRoot = join(base, "launch");
|
|
18
|
-
if (!lstatIfPresent(managedRoot))
|
|
19
|
-
mkdirSync(managedRoot, { mode: 0o700 });
|
|
20
|
-
if (!lstatIfPresent(launchRoot)) {
|
|
21
|
-
mkdirSync(launchRoot, { mode: 0o711 });
|
|
22
|
-
chmodSync(launchRoot, 0o711);
|
|
23
|
-
}
|
|
24
|
-
assertPrivateManagedRoot(managedRoot);
|
|
25
|
-
assertLaunchRoot(launchRoot);
|
|
26
|
-
const baseStat = lstatSync(base);
|
|
27
|
-
if (!baseStat.isDirectory() ||
|
|
28
|
-
baseStat.isSymbolicLink() ||
|
|
29
|
-
realpathSync(base) !== base ||
|
|
30
|
-
baseStat.uid !== 0 ||
|
|
31
|
-
(baseStat.mode & 0o777) !== 0o711) {
|
|
32
|
-
throw new Error("Concurrent profile base must be a root-owned 0711 real directory");
|
|
33
|
-
}
|
|
34
|
-
return { managedRoot, launchRoot };
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Materialize PUBLIC profile material beside the shared workspace. Processes
|
|
38
|
-
* in one sandbox share a uid and can read each other's revision files and env.
|
|
39
|
-
* Callers must not use this path for confidential profile text or credentials.
|
|
40
|
-
* Only native controls with a verified explicit launch path are admitted.
|
|
41
|
-
* A separate reaper owns removal after no native process reads the revision.
|
|
42
|
-
*/
|
|
43
|
-
export async function preparePublicConcurrentProfileRevision(input) {
|
|
44
|
-
if (!input.ownerId)
|
|
45
|
-
throw new Error("Concurrent profile revision needs a binding owner");
|
|
46
|
-
if (input.options?.ownership) {
|
|
47
|
-
throw new Error("Concurrent profile files must remain executor-owned and read-only");
|
|
48
|
-
}
|
|
49
|
-
if (input.plan.files.some((file) => (file.secretSlots?.length ?? 0) > 0) ||
|
|
50
|
-
Object.values(input.plan.env).some((value) => typeof value !== "string") ||
|
|
51
|
-
input.plan.flags.some((value) => typeof value !== "string")) {
|
|
52
|
-
throw new Error("Concurrent profile secrets need per-profile process identity; this sandbox has one subprocess identity");
|
|
53
|
-
}
|
|
54
|
-
const plan = prepareExplicitOpenCodePlan(input.plan);
|
|
55
|
-
assertWorkspacePlanSupported(plan);
|
|
56
|
-
assertLaunchRoot(input.launchRoot);
|
|
57
|
-
const prepared = await prepareManagedWorkspacePlan(plan, input.managedRoot, input.sharedWorkspaceDir, input.secretProvider, input.options);
|
|
58
|
-
try {
|
|
59
|
-
const identity = createHash("sha256")
|
|
60
|
-
.update(`${input.ownerId}\0${prepared.manifest.materializedDigest}`)
|
|
61
|
-
.digest("hex");
|
|
62
|
-
const directory = join(input.launchRoot, `revision-${identity}`);
|
|
63
|
-
// Validate every native file before publishing a revision that a launcher
|
|
64
|
-
// could mistake for a complete profile.
|
|
65
|
-
const spawn = bindConcurrentLaunch(plan, prepared.application, join(directory, "workspace"));
|
|
66
|
-
prepared.manifest.launchDigest = createHash("sha256")
|
|
67
|
-
.update(JSON.stringify(spawn))
|
|
68
|
-
.digest("hex");
|
|
69
|
-
writeFileSync(join(prepared.root, "manifest.json"), JSON.stringify(prepared.manifest));
|
|
70
|
-
const existing = lstatIfPresent(directory);
|
|
71
|
-
if (existing) {
|
|
72
|
-
assertPublishedRevision(directory, prepared.manifest);
|
|
73
|
-
prepared.dispose();
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
try {
|
|
77
|
-
// Keep the top directory private and writable for rename. The child
|
|
78
|
-
// tree is complete and read-only before publication.
|
|
79
|
-
makeRevisionReadOnly(prepared.root, false);
|
|
80
|
-
renameSync(prepared.root, directory);
|
|
81
|
-
// This is the publication point: agents cannot traverse the root
|
|
82
|
-
// before its mode changes from 0700 to 0555.
|
|
83
|
-
chmodSync(directory, 0o555);
|
|
84
|
-
}
|
|
85
|
-
catch (error) {
|
|
86
|
-
const raced = lstatIfPresent(directory);
|
|
87
|
-
if (!raced?.isDirectory() || raced.isSymbolicLink())
|
|
88
|
-
throw error;
|
|
89
|
-
assertPublishedRevision(directory, prepared.manifest);
|
|
90
|
-
makeStageWritable(prepared.root);
|
|
91
|
-
prepared.dispose();
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return {
|
|
95
|
-
directory,
|
|
96
|
-
manifest: prepared.manifest,
|
|
97
|
-
application: {
|
|
98
|
-
...prepared.application,
|
|
99
|
-
receipt: {
|
|
100
|
-
...prepared.application.receipt,
|
|
101
|
-
flags: [...spawn.flags],
|
|
102
|
-
},
|
|
103
|
-
spawn,
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
catch (error) {
|
|
108
|
-
// A published revision is never removed here; only an unpublished stage is.
|
|
109
|
-
if (lstatIfPresent(prepared.root)) {
|
|
110
|
-
makeStageWritable(prepared.root);
|
|
111
|
-
prepared.dispose();
|
|
112
|
-
}
|
|
113
|
-
throw error;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
/** Move OpenCode's cwd-discovered primary agent into its explicit JSON config. */
|
|
117
|
-
export function prepareExplicitOpenCodePlan(plan) {
|
|
118
|
-
if (plan.harness !== "opencode")
|
|
119
|
-
return plan;
|
|
120
|
-
for (const name of [
|
|
121
|
-
"OPENCODE_CONFIG",
|
|
122
|
-
"OPENCODE_CONFIG_CONTENT",
|
|
123
|
-
"TANGLE_PROFILE_REVISION_WORKSPACE",
|
|
124
|
-
]) {
|
|
125
|
-
if (Object.hasOwn(plan.env, name)) {
|
|
126
|
-
throw new Error(`Concurrent OpenCode reserves ${name} for its launch binding`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
const configFile = plan.files.find((file) => file.relPath === "opencode.json");
|
|
130
|
-
if (configFile?.source !== "generated" ||
|
|
131
|
-
(configFile.root ?? "workspace") !== "workspace") {
|
|
132
|
-
throw new Error("Concurrent OpenCode requires a generated opencode.json");
|
|
133
|
-
}
|
|
134
|
-
const config = JSON.parse(configFile.content);
|
|
135
|
-
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
136
|
-
throw new Error("Concurrent OpenCode config must be an object");
|
|
137
|
-
}
|
|
138
|
-
const body = config;
|
|
139
|
-
let files = [...plan.files];
|
|
140
|
-
if (plan.systemPrompt !== undefined) {
|
|
141
|
-
const agentFlag = plan.flags.indexOf("--agent");
|
|
142
|
-
const agentName = plan.flags[agentFlag + 1];
|
|
143
|
-
if (agentFlag < 0 ||
|
|
144
|
-
typeof agentName !== "string" ||
|
|
145
|
-
!/^tangle-profile-[a-f0-9]{16}$/.test(agentName)) {
|
|
146
|
-
throw new Error("Concurrent OpenCode requires the materializer's primary agent selection");
|
|
147
|
-
}
|
|
148
|
-
const primaryPath = `.opencode/agents/${agentName}.md`;
|
|
149
|
-
const primary = files.find((file) => file.relPath === primaryPath);
|
|
150
|
-
if (primary?.source !== "generated" ||
|
|
151
|
-
(primary.root ?? "workspace") !== "workspace") {
|
|
152
|
-
throw new Error("Concurrent OpenCode primary agent must be generated");
|
|
153
|
-
}
|
|
154
|
-
files = files.filter((file) => file !== primary);
|
|
155
|
-
const agents = body.agent;
|
|
156
|
-
if (agents !== undefined &&
|
|
157
|
-
(typeof agents !== "object" || agents === null || Array.isArray(agents))) {
|
|
158
|
-
throw new Error("Concurrent OpenCode agent config must be an object");
|
|
159
|
-
}
|
|
160
|
-
if (agents && Object.hasOwn(agents, agentName)) {
|
|
161
|
-
throw new Error("Concurrent OpenCode primary agent collides with config");
|
|
162
|
-
}
|
|
163
|
-
body.agent = {
|
|
164
|
-
...agents,
|
|
165
|
-
[agentName]: { mode: "primary", prompt: plan.systemPrompt },
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
const instructions = body.instructions;
|
|
169
|
-
if (instructions !== undefined) {
|
|
170
|
-
if (!Array.isArray(instructions) ||
|
|
171
|
-
instructions.some((entry) => typeof entry !== "string")) {
|
|
172
|
-
throw new Error("Concurrent OpenCode instructions must be generated file paths");
|
|
173
|
-
}
|
|
174
|
-
body.instructions = instructions.map((relPath) => {
|
|
175
|
-
const file = files.find((candidate) => candidate.relPath === relPath);
|
|
176
|
-
if (file?.source !== "generated" ||
|
|
177
|
-
(file.root ?? "workspace") !== "workspace") {
|
|
178
|
-
throw new Error(`Concurrent OpenCode cannot redirect instruction ${relPath}`);
|
|
179
|
-
}
|
|
180
|
-
return `{env:TANGLE_PROFILE_REVISION_WORKSPACE}/${relPath}`;
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
return {
|
|
184
|
-
...plan,
|
|
185
|
-
files: files.map((file) => file === configFile ? { ...file, content: JSON.stringify(body) } : file),
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
function assertPublishedRevision(directory, expected) {
|
|
189
|
-
const root = lstatSync(directory);
|
|
190
|
-
if (!root.isDirectory() ||
|
|
191
|
-
root.isSymbolicLink() ||
|
|
192
|
-
(root.mode & 0o777) !== 0o555) {
|
|
193
|
-
throw new Error("Concurrent profile revision is not fully published");
|
|
194
|
-
}
|
|
195
|
-
const persisted = JSON.parse(readFileSync(join(directory, "manifest.json"), "utf8"));
|
|
196
|
-
if (persisted.materializedDigest !== expected.materializedDigest ||
|
|
197
|
-
persisted.launchDigest !== expected.launchDigest) {
|
|
198
|
-
throw new Error("Concurrent profile revision digest collision");
|
|
199
|
-
}
|
|
200
|
-
for (const file of expected.files) {
|
|
201
|
-
const path = join(directory, file.root, file.relPath);
|
|
202
|
-
const stat = lstatSync(path);
|
|
203
|
-
if (!stat.isFile() ||
|
|
204
|
-
stat.isSymbolicLink() ||
|
|
205
|
-
(stat.mode & 0o222) !== 0 ||
|
|
206
|
-
createHash("sha256").update(readFileSync(path)).digest("hex") !==
|
|
207
|
-
file.sha256) {
|
|
208
|
-
throw new Error(`Concurrent profile revision file changed: ${file.relPath}`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
const expectedPaths = new Set(expected.files.map((file) => `${file.root}/${file.relPath}`));
|
|
212
|
-
for (const rootName of ["workspace", "agent"]) {
|
|
213
|
-
for (const relPath of listRevisionFiles(join(directory, rootName))) {
|
|
214
|
-
if (!expectedPaths.delete(`${rootName}/${relPath}`)) {
|
|
215
|
-
throw new Error(`Concurrent profile revision has an unexpected file: ${rootName}/${relPath}`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
if (expectedPaths.size) {
|
|
220
|
-
throw new Error("Concurrent profile revision is missing a manifest file");
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
export function listRevisionFiles(root, prefix = "") {
|
|
224
|
-
const found = [];
|
|
225
|
-
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
226
|
-
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
227
|
-
if (entry.isDirectory())
|
|
228
|
-
found.push(...listRevisionFiles(join(root, entry.name), relPath));
|
|
229
|
-
else if (entry.isFile())
|
|
230
|
-
found.push(relPath);
|
|
231
|
-
else
|
|
232
|
-
throw new Error(`Concurrent profile revision has a non-regular path: ${relPath}`);
|
|
233
|
-
}
|
|
234
|
-
return found;
|
|
235
|
-
}
|
|
236
|
-
export function assertLaunchRoot(root) {
|
|
237
|
-
if (!root || resolve(root) !== root) {
|
|
238
|
-
throw new Error("Concurrent profile launch root must be absolute");
|
|
239
|
-
}
|
|
240
|
-
const stat = lstatSync(root);
|
|
241
|
-
if (!stat.isDirectory() ||
|
|
242
|
-
stat.isSymbolicLink() ||
|
|
243
|
-
realpathSync(root) !== root ||
|
|
244
|
-
(stat.mode & 0o022) !== 0 ||
|
|
245
|
-
(stat.mode & 0o111) !== 0o111 ||
|
|
246
|
-
stat.uid !== process.getuid?.()) {
|
|
247
|
-
throw new Error("Concurrent profile launch root must be executor-owned, real, traversable, and not writable by agent users");
|
|
248
|
-
}
|
|
249
|
-
let ancestor = dirname(root);
|
|
250
|
-
while (ancestor !== root) {
|
|
251
|
-
const ancestorStat = lstatSync(ancestor);
|
|
252
|
-
if (!ancestorStat.isDirectory() ||
|
|
253
|
-
ancestorStat.isSymbolicLink() ||
|
|
254
|
-
(ancestorStat.mode & 0o001) === 0) {
|
|
255
|
-
throw new Error(`Concurrent profile launch root has a non-traversable ancestor: ${ancestor}`);
|
|
256
|
-
}
|
|
257
|
-
if (ancestor === dirname(ancestor))
|
|
258
|
-
break;
|
|
259
|
-
ancestor = dirname(ancestor);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
function makeRevisionReadOnly(root, publishRoot = true) {
|
|
263
|
-
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
264
|
-
const path = join(root, entry.name);
|
|
265
|
-
if (entry.isDirectory()) {
|
|
266
|
-
makeRevisionReadOnly(path);
|
|
267
|
-
}
|
|
268
|
-
else if (entry.isFile()) {
|
|
269
|
-
const mode = lstatSync(path).mode & 0o111;
|
|
270
|
-
chmodSync(path, mode ? 0o555 : 0o444);
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
throw new Error(`Concurrent profile revision contains a non-regular path: ${entry.name}`);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
if (publishRoot)
|
|
277
|
-
chmodSync(root, 0o555);
|
|
278
|
-
}
|
|
279
|
-
export function makeStageWritable(root) {
|
|
280
|
-
if (!lstatIfPresent(root))
|
|
281
|
-
return;
|
|
282
|
-
chmodSync(root, 0o700);
|
|
283
|
-
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
284
|
-
const path = join(root, entry.name);
|
|
285
|
-
if (entry.isDirectory())
|
|
286
|
-
makeStageWritable(path);
|
|
287
|
-
else if (entry.isFile())
|
|
288
|
-
chmodSync(path, 0o600);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Harness MCP config files this package generates wholesale from
|
|
3
|
-
* `profile.mcp`: exactly one `mcpServers` object whose entries carry only
|
|
4
|
-
* the fields the generators write. Recognizable by that shape alone (no
|
|
5
|
-
* schema id to key on), so an existing file holding ONLY those registrations
|
|
6
|
-
* is this package's earlier output and a new plan replaces it — a per-turn
|
|
7
|
-
* profile changes these bytes routinely (physim's MCP env carries turn
|
|
8
|
-
* state). Anything else at the path (a user's own keys, extra fields) still
|
|
9
|
-
* refuses: the name alone proves nothing, same rule as `opencode.json`.
|
|
10
|
-
*/
|
|
11
|
-
/** Whether the existing file at `relPath` is this package's own earlier MCP
|
|
12
|
-
* config output (shape rule above). `undefined` content = no regular file. */
|
|
13
|
-
export declare function isMaterializerMcpConfig(relPath: string, content: () => string | undefined): boolean;
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Harness MCP config files this package generates wholesale from
|
|
3
|
-
* `profile.mcp`: exactly one `mcpServers` object whose entries carry only
|
|
4
|
-
* the fields the generators write. Recognizable by that shape alone (no
|
|
5
|
-
* schema id to key on), so an existing file holding ONLY those registrations
|
|
6
|
-
* is this package's earlier output and a new plan replaces it — a per-turn
|
|
7
|
-
* profile changes these bytes routinely (physim's MCP env carries turn
|
|
8
|
-
* state). Anything else at the path (a user's own keys, extra fields) still
|
|
9
|
-
* refuses: the name alone proves nothing, same rule as `opencode.json`.
|
|
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
|
-
const MATERIALIZER_MCP_CONFIG_PATHS = new Set([
|
|
15
|
-
".pi/mcp.json",
|
|
16
|
-
".kimi-code/mcp.json",
|
|
17
|
-
]);
|
|
18
|
-
const GENERATED_MCP_SERVER_KEYS = new Set([
|
|
19
|
-
"command",
|
|
20
|
-
"args",
|
|
21
|
-
"env",
|
|
22
|
-
"cwd",
|
|
23
|
-
"url",
|
|
24
|
-
"httpUrl",
|
|
25
|
-
"headers",
|
|
26
|
-
"transport",
|
|
27
|
-
"enabled",
|
|
28
|
-
"directTools",
|
|
29
|
-
]);
|
|
30
|
-
/** Parse workspace bytes into the one shape this rule owns, or `null` when
|
|
31
|
-
* the bytes are anything else (not an object, other top-level keys, or
|
|
32
|
-
* server entries that are not objects). The I/O boundary for
|
|
33
|
-
* {@link isGeneratedMcpConfigShape}. */
|
|
34
|
-
function parseMcpServersConfig(bytes) {
|
|
35
|
-
let parsed;
|
|
36
|
-
try {
|
|
37
|
-
parsed = JSON.parse(bytes);
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
const { mcpServers } = parsed;
|
|
46
|
-
if (mcpServers === null ||
|
|
47
|
-
typeof mcpServers !== "object" ||
|
|
48
|
-
Array.isArray(mcpServers)) {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
for (const server of Object.values(mcpServers)) {
|
|
52
|
-
if (server === null ||
|
|
53
|
-
typeof server !== "object" ||
|
|
54
|
-
Array.isArray(server)) {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return { mcpServers: mcpServers };
|
|
59
|
-
}
|
|
60
|
-
/** Whether a parsed MCP config carries ONLY the registrations this package
|
|
61
|
-
* generates: no extra top-level keys, and every server entry holds only
|
|
62
|
-
* generated fields. A user's own key anywhere makes this `false`. */
|
|
63
|
-
function isGeneratedMcpConfigShape(config) {
|
|
64
|
-
const keys = Object.keys(config);
|
|
65
|
-
if (keys.length !== 1 || keys[0] !== "mcpServers")
|
|
66
|
-
return false;
|
|
67
|
-
return Object.values(config.mcpServers).every((server) => Object.keys(server).every((key) => GENERATED_MCP_SERVER_KEYS.has(key)));
|
|
68
|
-
}
|
|
69
|
-
/** Whether the existing file at `relPath` is this package's own earlier MCP
|
|
70
|
-
* config output (shape rule above). `undefined` content = no regular file. */
|
|
71
|
-
export function isMaterializerMcpConfig(relPath, content) {
|
|
72
|
-
if (!MATERIALIZER_MCP_CONFIG_PATHS.has(relPath))
|
|
73
|
-
return false;
|
|
74
|
-
const bytes = content();
|
|
75
|
-
if (bytes === undefined)
|
|
76
|
-
return false;
|
|
77
|
-
const config = parseMcpServersConfig(bytes);
|
|
78
|
-
return config !== null && isGeneratedMcpConfigShape(config);
|
|
79
|
-
}
|
package/dist/mcp-opencode.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
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";
|
package/dist/mcp-opencode.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
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";
|
package/dist/mcp-timeouts.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
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";
|
package/dist/mcp-timeouts.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
export declare function materializeMcp(h: HarnessId, mcp: Record<string, ActiveAgentProfileMcpServer>, plan: WorkspacePlan, add: AddPlanFile, unsupported: (d: Unsupported["dimension"], r: string) => void): void;
|
|
2
|
-
import { type ActiveAgentProfileMcpServer } from "./mcp-timeouts.js";
|
|
3
|
-
import type { AddPlanFile } from "./plan-json.js";
|
|
4
|
-
import type { HarnessId, Unsupported, WorkspacePlan } from "./plan-types.js";
|