@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 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?: Unsupported["dimension"][];
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
- unsupported("mcp", "pi MCP requires a loaded MCP extension; the base Pi runtime has no MCP control");
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.9.4",
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.41.0"
29
+ "@tangle-network/agent-interface": ">=0.38.0 <0.43.0"
30
30
  },
31
31
  "scripts": {
32
32
  "build": "tsc -p tsconfig.json",