@tangle-network/agent-profile-materialize 0.9.4 → 0.10.1
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 +52 -1
- package/dist/index.js +80 -3
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import type { AgentCandidateConfigValue, AgentCandidateProfile, AgentCandidateProfileActivation, AgentCandidateProfileApplication, AgentCandidateProfilePlanEvidence, AgentCandidateProfilePlanMaterial, AgentProfile, AgentProfileConfigValue, AgentProfileMcpServer, AgentProfileSecretProvider, AgentProfileSecretRef, Sha256Digest } from "@tangle-network/agent-interface";
|
|
25
25
|
export declare const UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER: AgentProfileSecretProvider;
|
|
26
|
+
/**
|
|
27
|
+
* The harnesses this materializer can lower an AgentProfile into. `satisfies` binds every id to
|
|
28
|
+
* the canonical harness enum, so a rename upstream is a compile error here rather than an
|
|
29
|
+
* `isMaterializerHarness` that silently returns false for a harness that still exists.
|
|
30
|
+
*/
|
|
26
31
|
export declare const MATERIALIZER_HARNESSES: readonly ["claude-code", "nanoclaw", "codex", "opencode", "kimi-code", "pi", "gemini", "hermes", "openclaw"];
|
|
27
32
|
export type HarnessId = (typeof MATERIALIZER_HARNESSES)[number];
|
|
28
33
|
export declare function isMaterializerHarness(harness: string): harness is HarnessId;
|
|
@@ -80,6 +85,16 @@ export interface Unsupported {
|
|
|
80
85
|
dimension: "files" | "skills" | "mcp" | "hooks" | "subagents" | "commands" | "instructions" | "systemPrompt" | "connections" | "confidential" | "permissions" | "tools" | "modes" | "extensions";
|
|
81
86
|
reason: string;
|
|
82
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* The dimensions `materializeProfile`'s `skip` option honors.
|
|
90
|
+
*
|
|
91
|
+
* Every other dimension is materialized unconditionally: generated plan files are pushed through
|
|
92
|
+
* a single site that carries no dimension tag, so there is nothing to gate them on. Naming the
|
|
93
|
+
* honored set keeps `skip: ["hooks"]` a loud rejection instead of a silent no-op.
|
|
94
|
+
*/
|
|
95
|
+
export declare const SKIPPABLE_DIMENSIONS: readonly ["mcp", "extensions"];
|
|
96
|
+
export type SkippableDimension = (typeof SKIPPABLE_DIMENSIONS)[number];
|
|
97
|
+
export declare function isSkippableDimension(value: string): value is SkippableDimension;
|
|
83
98
|
/** The plan a caller writes into the run cwd, then applies to the spawn. */
|
|
84
99
|
export interface WorkspacePlan {
|
|
85
100
|
harness: HarnessId;
|
|
@@ -93,6 +108,42 @@ export interface WorkspacePlan {
|
|
|
93
108
|
systemPrompt?: string;
|
|
94
109
|
/** Dimensions requested but with NO cwd path on this harness — fail-closed record. */
|
|
95
110
|
unsupported: Unsupported[];
|
|
111
|
+
/**
|
|
112
|
+
* Prerequisites the workspace files CANNOT establish on their own.
|
|
113
|
+
*
|
|
114
|
+
* A plan is a set of files, an env map, and flags. Some harness capabilities need more than
|
|
115
|
+
* that to actually function: pi's MCP support is not built in, it arrives from an extension
|
|
116
|
+
* that must be installed into the harness's own package directory and permitted to load. No
|
|
117
|
+
* file written into a run workspace can make either true.
|
|
118
|
+
*
|
|
119
|
+
* Writing the config anyway and hoping is the failure this field exists to prevent: the
|
|
120
|
+
* harness starts, silently has no MCP, and the agent runs without the tools it was configured
|
|
121
|
+
* with while nothing reports a problem. So the plan states the requirement and the executor —
|
|
122
|
+
* which knows where its harness is installed and how it spawns — verifies or refuses it.
|
|
123
|
+
*
|
|
124
|
+
* Optional because this is an additive field on a published type: a plan built before it
|
|
125
|
+
* existed, or by a caller that does not construct one, is still a valid plan with no
|
|
126
|
+
* prerequisites. Every plan this package builds sets it.
|
|
127
|
+
*/
|
|
128
|
+
requires?: PlanRequirement[];
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* One prerequisite a {@link WorkspacePlan} depends on but cannot create.
|
|
132
|
+
*
|
|
133
|
+
* Declarative on purpose: this package builds plans and never probes a filesystem, spawns a
|
|
134
|
+
* process, or reaches the network. The executor holds that context and does the checking.
|
|
135
|
+
*/
|
|
136
|
+
export interface PlanRequirement {
|
|
137
|
+
/** The only kind so far: a harness extension/plugin that must be loaded for a dimension to work. */
|
|
138
|
+
kind: "harness-extension";
|
|
139
|
+
/** Dimension that produced the requirement, so a refusal can name what will not work. */
|
|
140
|
+
dimension: Unsupported["dimension"];
|
|
141
|
+
/** Package spec in the harness's own vocabulary, e.g. `npm:pi-mcp-adapter`. */
|
|
142
|
+
id: string;
|
|
143
|
+
/** Why the plan needs it — goes verbatim into the executor's refusal message. */
|
|
144
|
+
reason: string;
|
|
145
|
+
/** The exact command that satisfies it, so a failure is actionable without a doc lookup. */
|
|
146
|
+
remedy: string;
|
|
96
147
|
}
|
|
97
148
|
/**
|
|
98
149
|
* Proof of the profile workspace inputs applied before a harness is spawned.
|
|
@@ -225,7 +276,7 @@ export declare function normalizeSkillMd(name: string, raw: string): string;
|
|
|
225
276
|
* The explicit target harness takes precedence over the profile's default.
|
|
226
277
|
*/
|
|
227
278
|
export declare function materializeProfile(inputProfile: AgentProfile, harness: HarnessId, opts?: {
|
|
228
|
-
skip?:
|
|
279
|
+
skip?: SkippableDimension[];
|
|
229
280
|
}): WorkspacePlan;
|
|
230
281
|
/**
|
|
231
282
|
* Materialize a sealed candidate profile without weakening its process values.
|
package/dist/index.js
CHANGED
|
@@ -59,6 +59,11 @@ function resolveInlineRef(ref) {
|
|
|
59
59
|
}
|
|
60
60
|
return null; // github / unresolved
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* The harnesses this materializer can lower an AgentProfile into. `satisfies` binds every id to
|
|
64
|
+
* the canonical harness enum, so a rename upstream is a compile error here rather than an
|
|
65
|
+
* `isMaterializerHarness` that silently returns false for a harness that still exists.
|
|
66
|
+
*/
|
|
62
67
|
export const MATERIALIZER_HARNESSES = [
|
|
63
68
|
"claude-code",
|
|
64
69
|
"nanoclaw",
|
|
@@ -79,6 +84,32 @@ function assertMaterializerHarness(harness) {
|
|
|
79
84
|
throw new Error(`Unsupported profile materialization harness "${harness}"`);
|
|
80
85
|
}
|
|
81
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* The dimensions `materializeProfile`'s `skip` option honors.
|
|
89
|
+
*
|
|
90
|
+
* Every other dimension is materialized unconditionally: generated plan files are pushed through
|
|
91
|
+
* a single site that carries no dimension tag, so there is nothing to gate them on. Naming the
|
|
92
|
+
* honored set keeps `skip: ["hooks"]` a loud rejection instead of a silent no-op.
|
|
93
|
+
*/
|
|
94
|
+
export const SKIPPABLE_DIMENSIONS = [
|
|
95
|
+
"mcp",
|
|
96
|
+
"extensions",
|
|
97
|
+
];
|
|
98
|
+
const SKIPPABLE_DIMENSION_SET = new Set(SKIPPABLE_DIMENSIONS);
|
|
99
|
+
export function isSkippableDimension(value) {
|
|
100
|
+
return SKIPPABLE_DIMENSION_SET.has(value);
|
|
101
|
+
}
|
|
102
|
+
/** Reject a dimension `materializeProfile` would not actually skip, rather than ignoring it. */
|
|
103
|
+
function assertSkippableDimensions(skip) {
|
|
104
|
+
if (!skip)
|
|
105
|
+
return [];
|
|
106
|
+
const unsupportedSkips = skip.filter((dimension) => !isSkippableDimension(dimension));
|
|
107
|
+
if (unsupportedSkips.length > 0) {
|
|
108
|
+
throw new Error(`materializeProfile: cannot skip ${unsupportedSkips.join(", ")}. ` +
|
|
109
|
+
`Skippable dimensions are: ${SKIPPABLE_DIMENSIONS.join(", ")}.`);
|
|
110
|
+
}
|
|
111
|
+
return skip;
|
|
112
|
+
}
|
|
82
113
|
/** The plan was applied, but one or more private staging files remain. */
|
|
83
114
|
export class WorkspacePlanCleanupError extends AggregateError {
|
|
84
115
|
receipt;
|
|
@@ -543,13 +574,14 @@ export function materializeProfile(inputProfile, harness, opts = {}) {
|
|
|
543
574
|
assertMaterializerHarness(harness);
|
|
544
575
|
const profile = agentProfileSchema.parse(inputProfile);
|
|
545
576
|
const h = harness;
|
|
546
|
-
const skip = new Set(opts.skip
|
|
577
|
+
const skip = new Set(assertSkippableDimensions(opts.skip));
|
|
547
578
|
const plan = {
|
|
548
579
|
harness: h,
|
|
549
580
|
files: [],
|
|
550
581
|
env: {},
|
|
551
582
|
flags: [],
|
|
552
583
|
unsupported: [],
|
|
584
|
+
requires: [],
|
|
553
585
|
};
|
|
554
586
|
// Every plan file is tagged by provenance at the single push site: `add`
|
|
555
587
|
// (harness-generated native config) vs `addExplicitResource` (a profile
|
|
@@ -1186,6 +1218,9 @@ export function hashWorkspacePlan(plan) {
|
|
|
1186
1218
|
: agentProfileSecretRefSchema.parse(value),
|
|
1187
1219
|
]));
|
|
1188
1220
|
const unsupported = [...plan.unsupported].sort((a, b) => compareStrings(`${a.dimension}\0${a.reason}`, `${b.dimension}\0${b.reason}`));
|
|
1221
|
+
// A plan that needs an extension is not the same plan as one that does not, even when every
|
|
1222
|
+
// byte it writes is identical — the digest is what a receipt proves, so it must say so.
|
|
1223
|
+
const requires = [...(plan.requires ?? [])].sort((a, b) => compareStrings(`${a.kind}\0${a.id}`, `${b.kind}\0${b.id}`));
|
|
1189
1224
|
const canonical = JSON.stringify({
|
|
1190
1225
|
harness: plan.harness,
|
|
1191
1226
|
files,
|
|
@@ -1195,6 +1230,7 @@ export function hashWorkspacePlan(plan) {
|
|
|
1195
1230
|
? {}
|
|
1196
1231
|
: { systemPrompt: plan.systemPrompt }),
|
|
1197
1232
|
unsupported,
|
|
1233
|
+
...(requires.length === 0 ? {} : { requires }),
|
|
1198
1234
|
});
|
|
1199
1235
|
return `sha256:${sha256(canonical)}`;
|
|
1200
1236
|
}
|
|
@@ -2141,9 +2177,39 @@ function materializeMcp(h, mcp, plan, add, unsupported) {
|
|
|
2141
2177
|
plan.flags.push("--mcp-config-file", rel);
|
|
2142
2178
|
break;
|
|
2143
2179
|
}
|
|
2144
|
-
case "pi":
|
|
2145
|
-
|
|
2180
|
+
case "pi": {
|
|
2181
|
+
// pi ships NO MCP. It arrives from the `pi-mcp-adapter` extension, which discovers
|
|
2182
|
+
// `<cwd>/.pi/mcp.json` (adapter 2.17.0 config.ts:17) and also registers a `--mcp-config`
|
|
2183
|
+
// flag (index.ts:252). The flag is registered BY the extension, so passing it when the
|
|
2184
|
+
// extension is absent is not a fallback — it is an unknown flag.
|
|
2185
|
+
//
|
|
2186
|
+
// This was previously marked unsupported because a workspace file cannot install an
|
|
2187
|
+
// extension. That reasoning was right about the constraint and wrong about the conclusion:
|
|
2188
|
+
// the file half is perfectly materializable, and the half that is not becomes a declared
|
|
2189
|
+
// requirement the executor enforces. Marking the whole dimension unsupported pushed both
|
|
2190
|
+
// halves onto every consumer, and two of them independently rebuilt it.
|
|
2191
|
+
const rel = ".pi/mcp.json";
|
|
2192
|
+
const servers = Object.fromEntries(Object.entries(mcp).map(([name, server]) => [
|
|
2193
|
+
name,
|
|
2194
|
+
// `directTools` promotes this server's verbs to first-class pi tools. Without it the
|
|
2195
|
+
// agent gets one generic `mcp` tool and must connect, then describe, then call — three
|
|
2196
|
+
// round trips of discovery before it can do anything. Per-server, because the adapter
|
|
2197
|
+
// lets a per-server value override the global `settings.directTools` (direct-tools.ts:135).
|
|
2198
|
+
{ ...piMcpServer(server), directTools: true },
|
|
2199
|
+
]));
|
|
2200
|
+
// serializeWorkspacePlanJson, NOT JSON.stringify: it unwraps public config values to their
|
|
2201
|
+
// bytes and lifts any secret-ref into a tracked slot the applier resolves. Stringifying
|
|
2202
|
+
// straight would write `{"kind":"public","value":"..."}` into the file as an argv entry.
|
|
2203
|
+
add(rel, serializeWorkspacePlanJson({ mcpServers: servers }));
|
|
2204
|
+
plan.requires?.push({
|
|
2205
|
+
kind: "harness-extension",
|
|
2206
|
+
dimension: "mcp",
|
|
2207
|
+
id: "npm:pi-mcp-adapter",
|
|
2208
|
+
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",
|
|
2209
|
+
remedy: "pi install npm:pi-mcp-adapter",
|
|
2210
|
+
});
|
|
2146
2211
|
break;
|
|
2212
|
+
}
|
|
2147
2213
|
case "hermes":
|
|
2148
2214
|
unsupported("mcp", "hermes MCP is global config.yaml (mcp_servers), no cwd discovery — write to the hermes global config or pass a config flag");
|
|
2149
2215
|
break;
|
|
@@ -2203,6 +2269,17 @@ function setPlanEnvironment(plan, name, value) {
|
|
|
2203
2269
|
}
|
|
2204
2270
|
plan.env[name] = normalized;
|
|
2205
2271
|
}
|
|
2272
|
+
/**
|
|
2273
|
+
* Lower one canonical MCP server to the shape `pi-mcp-adapter` reads from `.pi/mcp.json`.
|
|
2274
|
+
*
|
|
2275
|
+
* The adapter consumes the same `{mcpServers: {...}}` document Claude and Kimi use, so this
|
|
2276
|
+
* mirrors {@link kimiMcpServer} rather than inventing a pi dialect. `directTools` is added by the
|
|
2277
|
+
* caller, not here, because it is a policy about tool surfacing rather than a property of the
|
|
2278
|
+
* server definition.
|
|
2279
|
+
*/
|
|
2280
|
+
function piMcpServer(server) {
|
|
2281
|
+
return kimiMcpServer(server);
|
|
2282
|
+
}
|
|
2206
2283
|
function kimiMcpServer(server) {
|
|
2207
2284
|
if (server.url) {
|
|
2208
2285
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-profile-materialize",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
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
|
"main": "./dist/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"vitest": "^4.1.10"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
|
-
"@tangle-network/agent-interface": ">=0.38.0 <0.
|
|
29
|
+
"@tangle-network/agent-interface": ">=0.38.0 <0.43.0"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsc -p tsconfig.json",
|