@tangle-network/agent-profile-materialize 0.15.2 → 0.17.0

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.
Files changed (57) hide show
  1. package/dist/candidate-types.d.ts +32 -0
  2. package/dist/candidate-types.js +1 -0
  3. package/dist/candidate.d.ts +9 -0
  4. package/dist/candidate.js +279 -0
  5. package/dist/commands.d.ts +2 -0
  6. package/dist/commands.js +32 -0
  7. package/dist/control-maps.d.ts +34 -0
  8. package/dist/control-maps.js +185 -0
  9. package/dist/execution-controls.d.ts +5 -0
  10. package/dist/execution-controls.js +186 -0
  11. package/dist/harness.d.ts +81 -0
  12. package/dist/harness.js +167 -0
  13. package/dist/hooks.d.ts +6 -0
  14. package/dist/hooks.js +261 -0
  15. package/dist/index.d.ts +24 -623
  16. package/dist/index.js +15 -3733
  17. package/dist/mcp.d.ts +22 -0
  18. package/dist/mcp.js +244 -0
  19. package/dist/pi-models-config.d.ts +30 -0
  20. package/dist/pi-models-config.js +46 -0
  21. package/dist/plan-application.d.ts +10 -0
  22. package/dist/plan-application.js +152 -0
  23. package/dist/plan-digest.d.ts +18 -0
  24. package/dist/plan-digest.js +90 -0
  25. package/dist/plan-json.d.ts +18 -0
  26. package/dist/plan-json.js +91 -0
  27. package/dist/plan-normalize.d.ts +13 -0
  28. package/dist/plan-normalize.js +156 -0
  29. package/dist/plan-prompt.d.ts +38 -0
  30. package/dist/plan-prompt.js +49 -0
  31. package/dist/plan-support.d.ts +9 -0
  32. package/dist/plan-support.js +22 -0
  33. package/dist/plan-types.d.ts +294 -0
  34. package/dist/plan-types.js +106 -0
  35. package/dist/prime-agent-dir.d.ts +47 -0
  36. package/dist/prime-agent-dir.js +164 -0
  37. package/dist/prime-profile.d.ts +88 -0
  38. package/dist/prime-profile.js +284 -0
  39. package/dist/profile-materializer.d.ts +12 -0
  40. package/dist/profile-materializer.js +59 -0
  41. package/dist/profile-plan.d.ts +14 -0
  42. package/dist/profile-plan.js +86 -0
  43. package/dist/profile-prompt.d.ts +4 -0
  44. package/dist/profile-prompt.js +105 -0
  45. package/dist/profile-support.d.ts +8 -0
  46. package/dist/profile-support.js +6 -0
  47. package/dist/resource-lowering.d.ts +7 -0
  48. package/dist/resource-lowering.js +103 -0
  49. package/dist/resource-resolution.d.ts +27 -0
  50. package/dist/resource-resolution.js +241 -0
  51. package/dist/subagents.d.ts +6 -0
  52. package/dist/subagents.js +133 -0
  53. package/dist/workspace-apply.d.ts +12 -0
  54. package/dist/workspace-apply.js +334 -0
  55. package/dist/workspace-files.d.ts +8 -0
  56. package/dist/workspace-files.js +46 -0
  57. package/package.json +3 -3
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ export declare function materializeMcp(h: HarnessId, mcp: Record<string, ActiveAgentProfileMcpServer>, plan: WorkspacePlan, add: AddPlanFile, unsupported: (d: Unsupported["dimension"], r: string) => void): void;
2
+ /** OpenCode-native MCP configuration used by workspace and startup paths. */
3
+ export type OpenCodeMcpConfig = Record<string, {
4
+ type: "remote";
5
+ url: string;
6
+ enabled: boolean;
7
+ headers?: Record<string, AgentProfileConfigValue>;
8
+ } | {
9
+ type: "local";
10
+ command: Array<string | AgentProfileConfigValue>;
11
+ enabled: boolean;
12
+ environment?: Record<string, AgentProfileConfigValue>;
13
+ }>;
14
+ /** Lower canonical MCP definitions to OpenCode's native startup configuration. */
15
+ export declare function openCodeMcpConfig(mcp: Record<string, AgentProfileMcpServer>): OpenCodeMcpConfig;
16
+ import type { AgentProfileConfigValue, AgentProfileMcpServer } from "@tangle-network/agent-interface";
17
+ import type { AddPlanFile } from "./plan-json.js";
18
+ import type { HarnessId, Unsupported, WorkspacePlan } from "./plan-types.js";
19
+ type ActiveAgentProfileMcpServer = Exclude<AgentProfileMcpServer, {
20
+ enabled: false;
21
+ }>;
22
+ export {};
package/dist/mcp.js ADDED
@@ -0,0 +1,244 @@
1
+ export function materializeMcp(h, mcp, plan, add, unsupported) {
2
+ switch (h) {
3
+ case "claude-code": {
4
+ const servers = {};
5
+ for (const [name, server] of Object.entries(mcp)) {
6
+ if (server.url) {
7
+ servers[name] = {
8
+ type: server.transport ?? "http",
9
+ url: server.url,
10
+ ...(server.headers ? { headers: server.headers } : {}),
11
+ };
12
+ continue;
13
+ }
14
+ if (server.cwd !== undefined) {
15
+ unsupported("mcp", `${h}: local MCP server "${name}" does not support a cwd field`);
16
+ }
17
+ servers[name] = {
18
+ type: "stdio",
19
+ command: server.command,
20
+ ...(server.args ? { args: server.args } : {}),
21
+ ...(server.env ? { env: server.env } : {}),
22
+ };
23
+ }
24
+ const relPath = ".tangle/claude-mcp.json";
25
+ add(relPath, serializeWorkspacePlanJson({ mcpServers: servers }));
26
+ plan.flags.push("--mcp-config", relPath, "--strict-mcp-config");
27
+ break;
28
+ }
29
+ case "nanoclaw":
30
+ unsupported("mcp", "nanoclaw MCP must be injected through options.mcpServers by its SDK adapter");
31
+ break;
32
+ case "codex": {
33
+ const servers = [];
34
+ for (const [name, server] of Object.entries(mcp).sort(([left], [right]) => compareStrings(left, right))) {
35
+ if (server.url && server.transport === "sse") {
36
+ unsupported("mcp", `codex: remote MCP server "${name}" uses unsupported SSE transport`);
37
+ continue;
38
+ }
39
+ servers.push(concatWorkspacePlanArguments(`${tomlStr(name)} = `, codexMcpServerToml(name, server, plan)));
40
+ }
41
+ if (servers.length > 0) {
42
+ // Runtime overrides do not require persisted project trust. Header values
43
+ // ride environment variables so credentials never appear in argv.
44
+ plan.flags.push("-c", concatWorkspacePlanArguments("mcp_servers={ ", joinWorkspacePlanArguments(servers, ", "), " }"));
45
+ }
46
+ break;
47
+ }
48
+ case "opencode":
49
+ // opencode.json "mcp" block (local|remote).
50
+ add("opencode.json", serializeWorkspacePlanJson({
51
+ $schema: "https://opencode.ai/config.json",
52
+ mcp: openCodeMcpConfig(mcp),
53
+ }));
54
+ break;
55
+ case "gemini": {
56
+ const servers = {};
57
+ for (const [name, server] of Object.entries(mcp)) {
58
+ if (server.url) {
59
+ servers[name] = {
60
+ [server.transport === "sse" ? "url" : "httpUrl"]: server.url,
61
+ ...(server.headers ? { headers: server.headers } : {}),
62
+ };
63
+ continue;
64
+ }
65
+ servers[name] = {
66
+ command: server.command,
67
+ ...(server.args ? { args: server.args } : {}),
68
+ ...(server.env ? { env: server.env } : {}),
69
+ ...(server.cwd ? { cwd: server.cwd } : {}),
70
+ };
71
+ }
72
+ add(".gemini/settings.json", serializeWorkspacePlanJson({ mcpServers: servers }));
73
+ break;
74
+ }
75
+ case "kimi-code": {
76
+ // NO cwd auto-discovery — kimi needs an explicit --mcp-config-file FLAG.
77
+ const rel = ".kimi/mcp.json";
78
+ const servers = Object.fromEntries(Object.entries(mcp).map(([name, server]) => [
79
+ name,
80
+ kimiMcpServer(server),
81
+ ]));
82
+ add(rel, serializeWorkspacePlanJson({ mcpServers: servers }));
83
+ plan.flags.push("--mcp-config-file", rel);
84
+ break;
85
+ }
86
+ case "pi": {
87
+ // pi ships NO MCP. It arrives from the `pi-mcp-adapter` extension, which discovers
88
+ // `<cwd>/.pi/mcp.json` (adapter 2.17.0 config.ts:17) and also registers a `--mcp-config`
89
+ // flag (index.ts:252). The flag is registered BY the extension, so passing it when the
90
+ // extension is absent is not a fallback — it is an unknown flag.
91
+ //
92
+ // This was previously marked unsupported because a workspace file cannot install an
93
+ // extension. That reasoning was right about the constraint and wrong about the conclusion:
94
+ // the file half is perfectly materializable, and the half that is not becomes a declared
95
+ // requirement the executor enforces. Marking the whole dimension unsupported pushed both
96
+ // halves onto every consumer, and two of them independently rebuilt it.
97
+ const rel = ".pi/mcp.json";
98
+ const servers = Object.fromEntries(Object.entries(mcp).map(([name, server]) => [
99
+ name,
100
+ // `directTools` promotes this server's verbs to first-class pi tools. Without it the
101
+ // agent gets one generic `mcp` tool and must connect, then describe, then call — three
102
+ // round trips of discovery before it can do anything. Per-server, because the adapter
103
+ // lets a per-server value override the global `settings.directTools` (direct-tools.ts:135).
104
+ { ...piMcpServer(server), directTools: true },
105
+ ]));
106
+ // serializeWorkspacePlanJson, NOT JSON.stringify: it unwraps public config values to their
107
+ // bytes and lifts any secret-ref into a tracked slot the applier resolves. Stringifying
108
+ // straight would write `{"kind":"public","value":"..."}` into the file as an argv entry.
109
+ add(rel, serializeWorkspacePlanJson({ mcpServers: servers }));
110
+ plan.requires?.push({
111
+ kind: "harness-extension",
112
+ dimension: "mcp",
113
+ id: "npm:pi-mcp-adapter",
114
+ 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",
115
+ remedy: "pi install npm:pi-mcp-adapter",
116
+ });
117
+ break;
118
+ }
119
+ case "prime":
120
+ // The fork's MCP config surface (`mcpServers` in <agentDir>/settings.json,
121
+ // fork settings-manager.ts:98-120,219) IS read at startup, but a
122
+ // user-declared server never becomes model-visible: the host McpManager
123
+ // only registers OAuth providers and serves mcp.config/refresh
124
+ // host-requests (fork core/mcp/mcp-manager.ts:62-124,156-181), skips
125
+ // stdio servers entirely (mcp-manager.ts:73), and nothing generates a
126
+ // skill or prompt section for it — materializing profile.mcp would be a
127
+ // silently dropped control.
128
+ unsupported("mcp", "prime: settings.json mcpServers never becomes model-visible (fork mcp-manager.ts:62-124 " +
129
+ "registers OAuth providers only and skips stdio servers) — model-visible MCP is exactly " +
130
+ "the fork's built-in catalog skills");
131
+ break;
132
+ case "hermes":
133
+ unsupported("mcp", "hermes MCP is global config.yaml (mcp_servers), no cwd discovery — write to the hermes global config or pass a config flag");
134
+ break;
135
+ case "openclaw":
136
+ unsupported("mcp", "openclaw MCP is central ~/.openclaw/openclaw.json (mcp.servers), no cwd discovery");
137
+ break;
138
+ }
139
+ }
140
+ function tomlInlineStringTable(values) {
141
+ return `{ ${Object.entries(values)
142
+ .sort(([left], [right]) => compareStrings(left, right))
143
+ .map(([key, value]) => `${tomlStr(key)} = ${tomlStr(value)}`)
144
+ .join(", ")} }`;
145
+ }
146
+ function codexMcpServerToml(name, server, plan) {
147
+ const fields = [];
148
+ if (server.url) {
149
+ fields.push(`url = ${tomlStr(server.url)}`);
150
+ if (server.headers && Object.keys(server.headers).length > 0) {
151
+ const envHeaders = {};
152
+ for (const [header, value] of Object.entries(server.headers).sort(([left], [right]) => compareStrings(left, right))) {
153
+ const envName = `TANGLE_MCP_HEADER_${sha256(`${name}\0${header}`)
154
+ .slice(0, 24)
155
+ .toUpperCase()}`;
156
+ setPlanEnvironment(plan, envName, value);
157
+ envHeaders[header] = envName;
158
+ }
159
+ fields.push(`env_http_headers = ${tomlInlineStringTable(envHeaders)}`);
160
+ }
161
+ }
162
+ else {
163
+ if (!server.command) {
164
+ throw new Error(`Codex MCP server "${name}" must define url or command`);
165
+ }
166
+ fields.push(`command = ${tomlStr(server.command)}`);
167
+ if (server.args?.length) {
168
+ fields.push(concatWorkspacePlanArguments("args = [", joinWorkspacePlanArguments(server.args.map(tomlProfileConfigValue), ", "), "]"));
169
+ }
170
+ if (server.cwd)
171
+ fields.push(`cwd = ${tomlStr(server.cwd)}`);
172
+ if (server.env && Object.keys(server.env).length > 0) {
173
+ const envNames = Object.keys(server.env).sort(compareStrings);
174
+ for (const envName of envNames) {
175
+ setPlanEnvironment(plan, envName, server.env[envName]);
176
+ }
177
+ fields.push(`env_vars = [${envNames.map(tomlStr).join(", ")}]`);
178
+ }
179
+ }
180
+ return concatWorkspacePlanArguments("{ ", joinWorkspacePlanArguments(fields, ", "), " }");
181
+ }
182
+ /**
183
+ * Lower one canonical MCP server to the shape `pi-mcp-adapter` reads from `.pi/mcp.json`.
184
+ *
185
+ * The adapter consumes the same `{mcpServers: {...}}` document Claude and Kimi use, so this
186
+ * mirrors {@link kimiMcpServer} rather than inventing a pi dialect. `directTools` is added by the
187
+ * caller, not here, because it is a policy about tool surfacing rather than a property of the
188
+ * server definition.
189
+ */
190
+ function piMcpServer(server) {
191
+ return kimiMcpServer(server);
192
+ }
193
+ function kimiMcpServer(server) {
194
+ if (server.url) {
195
+ return {
196
+ url: server.url,
197
+ transport: server.transport ?? "http",
198
+ ...(server.headers ? { headers: server.headers } : {}),
199
+ };
200
+ }
201
+ return {
202
+ command: server.command,
203
+ ...(server.args ? { args: server.args } : {}),
204
+ ...(server.env ? { env: server.env } : {}),
205
+ ...(server.cwd ? { cwd: server.cwd } : {}),
206
+ };
207
+ }
208
+ /** Lower canonical MCP definitions to OpenCode's native startup configuration. */
209
+ export function openCodeMcpConfig(mcp) {
210
+ const out = {};
211
+ for (const [name, s] of Object.entries(mcp)) {
212
+ if (s.enabled === false)
213
+ continue;
214
+ if (s.url) {
215
+ if (s.transport === "sse") {
216
+ throw new Error(`OpenCode MCP server "${name}" uses unsupported SSE transport`);
217
+ }
218
+ out[name] = {
219
+ type: "remote",
220
+ url: s.url,
221
+ enabled: true,
222
+ ...(s.headers ? { headers: s.headers } : {}),
223
+ };
224
+ continue;
225
+ }
226
+ if (!s.command) {
227
+ throw new Error(`OpenCode MCP server "${name}" must define either url or command`);
228
+ }
229
+ if (s.cwd !== undefined) {
230
+ throw new Error(`OpenCode MCP server "${name}" does not support a cwd field`);
231
+ }
232
+ out[name] = {
233
+ type: "local",
234
+ command: [s.command, ...(s.args ?? [])],
235
+ enabled: true,
236
+ ...(s.env ? { environment: s.env } : {}),
237
+ };
238
+ }
239
+ return out;
240
+ }
241
+ import { sha256 } from "./plan-digest.js";
242
+ import { concatWorkspacePlanArguments, joinWorkspacePlanArguments, serializeWorkspacePlanJson, tomlProfileConfigValue, tomlStr, } from "./plan-json.js";
243
+ import { setPlanEnvironment } from "./profile-plan.js";
244
+ import { compareStrings } from "./workspace-files.js";
@@ -0,0 +1,30 @@
1
+ /**
2
+ * One custom-model shape for upstream Pi and the Prime fork.
3
+ *
4
+ * Both agents read the same `models.json` provider schema.
5
+ * Callers own the file lifecycle, but they must not build different payloads.
6
+ */
7
+ export interface PiModelsConfigInput {
8
+ /** Provider key in models.json and the matching `--provider` value. */
9
+ providerName: string;
10
+ /** Model id in models.json and the matching `--model` value. */
11
+ modelId: string;
12
+ /** Provider endpoint. OpenAI-compatible endpoints include `/v1`. */
13
+ baseUrl: string;
14
+ /** Literal in-box credential or an environment reference. */
15
+ apiKey: string;
16
+ /** Pi wire protocol. */
17
+ api?: "anthropic-messages" | "google-generative-ai" | "openai-completions" | "openai-responses";
18
+ contextWindow?: number;
19
+ maxTokens?: number;
20
+ reasoning?: boolean;
21
+ }
22
+ export interface PiModelsConfigFile {
23
+ path: string;
24
+ content: string;
25
+ mode: number;
26
+ }
27
+ export declare const PI_MODELS_CONFIG_PATH = ".pi/agent/models.json";
28
+ export declare function buildPiModelsConfig(input: PiModelsConfigInput): Record<string, unknown>;
29
+ /** Build the owner-only Pi file used by headless and interactive processes. */
30
+ export declare function buildPiModelsConfigFile(input: PiModelsConfigInput): PiModelsConfigFile;
@@ -0,0 +1,46 @@
1
+ export const PI_MODELS_CONFIG_PATH = ".pi/agent/models.json";
2
+ const DEFAULT_CONTEXT_WINDOW = 128_000;
3
+ export function buildPiModelsConfig(input) {
4
+ for (const [field, value] of [
5
+ ["providerName", input.providerName],
6
+ ["modelId", input.modelId],
7
+ ["baseUrl", input.baseUrl],
8
+ ["apiKey", input.apiKey],
9
+ ]) {
10
+ if (typeof value !== "string" || value.trim().length === 0) {
11
+ throw new Error(`pi models.json requires a non-empty "${field}"; ` +
12
+ "the caller must supply provider, model, baseUrl, and apiKey");
13
+ }
14
+ }
15
+ return {
16
+ providers: {
17
+ [input.providerName]: {
18
+ name: input.providerName,
19
+ baseUrl: input.baseUrl,
20
+ api: input.api ?? "openai-completions",
21
+ apiKey: input.apiKey,
22
+ models: [
23
+ {
24
+ id: input.modelId,
25
+ name: input.modelId,
26
+ reasoning: input.reasoning ?? false,
27
+ input: ["text", "image"],
28
+ contextWindow: input.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
29
+ ...(input.maxTokens !== undefined
30
+ ? { maxTokens: input.maxTokens }
31
+ : {}),
32
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
33
+ },
34
+ ],
35
+ },
36
+ },
37
+ };
38
+ }
39
+ /** Build the owner-only Pi file used by headless and interactive processes. */
40
+ export function buildPiModelsConfigFile(input) {
41
+ return {
42
+ path: PI_MODELS_CONFIG_PATH,
43
+ content: `${JSON.stringify(buildPiModelsConfig(input), null, 2)}\n`,
44
+ mode: 0o600,
45
+ };
46
+ }
@@ -0,0 +1,10 @@
1
+ export declare function applyWorkspacePlan(plan: WorkspacePlan, workspaceDir: string, options?: ApplyWorkspacePlanOptions): WorkspacePlanReceipt;
2
+ /**
3
+ * Resolve opaque profile references through an executor-owned provider, then
4
+ * apply the resulting private native files and launch inputs. Resolution
5
+ * completes before any workspace write, and resolved values never enter the
6
+ * public plan, digest, receipt, logs, or error messages.
7
+ */
8
+ export declare function applyWorkspacePlanWithSecretProvider(plan: WorkspacePlan, workspaceDir: string, secretProvider: AgentProfileSecretProvider, options?: ApplyWorkspacePlanOptions): Promise<WorkspacePlanApplication>;
9
+ import type { AgentProfileSecretProvider } from "@tangle-network/agent-interface";
10
+ import type { ApplyWorkspacePlanOptions, WorkspacePlan, WorkspacePlanApplication, WorkspacePlanReceipt } from "./plan-types.js";
@@ -0,0 +1,152 @@
1
+ export function applyWorkspacePlan(plan, workspaceDir, options = {}) {
2
+ return applyResolvedWorkspacePlan(plan, resolvePublicWorkspacePlan(plan), workspaceDir, options).receipt;
3
+ }
4
+ /**
5
+ * Resolve opaque profile references through an executor-owned provider, then
6
+ * apply the resulting private native files and launch inputs. Resolution
7
+ * completes before any workspace write, and resolved values never enter the
8
+ * public plan, digest, receipt, logs, or error messages.
9
+ */
10
+ export async function applyWorkspacePlanWithSecretProvider(plan, workspaceDir, secretProvider, options = {}) {
11
+ const planDigest = hashWorkspacePlan(plan);
12
+ const resolved = await resolvePrivateWorkspacePlan(plan, secretProvider);
13
+ return applyResolvedWorkspacePlan(plan, resolved, workspaceDir, options, planDigest);
14
+ }
15
+ function secretRefLabel(ref) {
16
+ return JSON.stringify(ref.key);
17
+ }
18
+ function requireResolvedSecretValue(ref, value) {
19
+ if (typeof value !== "string" || value.trim().length === 0) {
20
+ throw new Error(`AgentProfile secret provider returned no value for ${secretRefLabel(ref)}`);
21
+ }
22
+ return value;
23
+ }
24
+ function setValueAtJsonPath(root, path, value) {
25
+ let current = root;
26
+ for (const segment of path.slice(0, -1)) {
27
+ if (current === null ||
28
+ typeof current !== "object" ||
29
+ !Object.hasOwn(current, segment)) {
30
+ throw new Error("Generated profile JSON secret path no longer exists");
31
+ }
32
+ current = current[segment];
33
+ }
34
+ const finalSegment = path.at(-1);
35
+ if (finalSegment === undefined ||
36
+ current === null ||
37
+ typeof current !== "object" ||
38
+ !Object.hasOwn(current, finalSegment)) {
39
+ throw new Error("Generated profile JSON secret path no longer exists");
40
+ }
41
+ Object.defineProperty(current, finalSegment, {
42
+ value,
43
+ enumerable: true,
44
+ configurable: true,
45
+ writable: true,
46
+ });
47
+ }
48
+ function resolvePublicWorkspacePlan(plan) {
49
+ const files = normalizePlanFiles(plan.files);
50
+ for (const file of files) {
51
+ const ref = file.secretSlots[0]?.ref;
52
+ if (ref) {
53
+ throw new Error(`Workspace plan requires an AgentProfileSecretProvider for ${secretRefLabel(ref)}`);
54
+ }
55
+ }
56
+ const env = Object.fromEntries(Object.entries(plan.env).map(([name, value]) => {
57
+ if (typeof value !== "string") {
58
+ throw new Error(`Workspace plan requires an AgentProfileSecretProvider for ${secretRefLabel(value)}`);
59
+ }
60
+ return [name, value];
61
+ }));
62
+ const flags = plan.flags.map((argument) => {
63
+ const normalized = normalizeWorkspacePlanArgument(argument);
64
+ if (typeof normalized !== "string") {
65
+ const segment = normalized.segments.find((value) => typeof value !== "string");
66
+ if (!segment) {
67
+ throw new Error("Workspace plan argument template is malformed");
68
+ }
69
+ throw new Error(`Workspace plan requires an AgentProfileSecretProvider for ${secretRefLabel(segment.ref)}`);
70
+ }
71
+ return normalized;
72
+ });
73
+ return {
74
+ files,
75
+ env,
76
+ flags,
77
+ ...promptIntentFields(plan, samePromptIntentValue),
78
+ ...systemPromptBindingField(plan),
79
+ };
80
+ }
81
+ async function resolvePrivateWorkspacePlan(plan, secretProvider) {
82
+ if (!secretProvider || typeof secretProvider.get !== "function") {
83
+ throw new Error("AgentProfileSecretProvider.get is required");
84
+ }
85
+ const pending = new Map();
86
+ const resolveRef = async (input) => {
87
+ const ref = agentProfileSecretRefSchema.parse(input);
88
+ let raw = pending.get(ref.key);
89
+ if (!raw) {
90
+ raw = (async () => {
91
+ let value;
92
+ try {
93
+ value = await secretProvider.get(ref.key);
94
+ }
95
+ catch {
96
+ throw new Error(`AgentProfile secret provider failed for ${secretRefLabel(ref)}`);
97
+ }
98
+ return requireResolvedSecretValue(ref, value);
99
+ })();
100
+ pending.set(ref.key, raw);
101
+ }
102
+ const value = await raw;
103
+ return ref.format === "bearer" ? `Bearer ${value}` : value;
104
+ };
105
+ const files = await Promise.all(normalizePlanFiles(plan.files).map(async (file) => {
106
+ if (file.secretSlots.length === 0)
107
+ return file;
108
+ const parsed = JSON.parse(file.content);
109
+ const values = await Promise.all(file.secretSlots.map(async (slot) => ({
110
+ path: slot.path,
111
+ value: await resolveRef(slot.ref),
112
+ })));
113
+ for (const resolved of values) {
114
+ setValueAtJsonPath(parsed, resolved.path, resolved.value);
115
+ }
116
+ return {
117
+ ...file,
118
+ content: JSON.stringify(parsed, null, 2),
119
+ secretSlots: [],
120
+ };
121
+ }));
122
+ const env = Object.fromEntries(await Promise.all(Object.entries(plan.env).map(async ([name, value]) => [
123
+ name,
124
+ typeof value === "string" ? value : await resolveRef(value),
125
+ ])));
126
+ const flags = await Promise.all(plan.flags.map(async (argument) => {
127
+ const normalized = normalizeWorkspacePlanArgument(argument);
128
+ if (typeof normalized === "string")
129
+ return normalized;
130
+ const segments = await Promise.all(normalized.segments.map(async (segment) => {
131
+ if (typeof segment === "string")
132
+ return segment;
133
+ const value = await resolveRef(segment.ref);
134
+ return segment.encoding === "json-string"
135
+ ? JSON.stringify(value)
136
+ : value;
137
+ }));
138
+ return segments.join("");
139
+ }));
140
+ return {
141
+ files,
142
+ env,
143
+ flags,
144
+ ...promptIntentFields(plan, samePromptIntentValue),
145
+ ...systemPromptBindingField(plan),
146
+ };
147
+ }
148
+ import { agentProfileSecretRefSchema } from "@tangle-network/agent-interface";
149
+ import { hashWorkspacePlan } from "./plan-digest.js";
150
+ import { normalizePlanFiles, normalizeWorkspacePlanArgument, } from "./plan-normalize.js";
151
+ import { promptIntentFields, samePromptIntentValue, systemPromptBindingField, } from "./plan-prompt.js";
152
+ import { applyResolvedWorkspacePlan, } from "./workspace-apply.js";
@@ -0,0 +1,18 @@
1
+ export declare const sha256: (value: string) => string;
2
+ export declare function hashWorkspacePlan(plan: WorkspacePlan): string;
3
+ /** Build the exact digest-free identity document required by agent-interface. */
4
+ export declare function agentCandidateProfilePlanMaterial(plan: AgentCandidateWorkspacePlan): AgentCandidateProfilePlanMaterial;
5
+ /**
6
+ * Return the RFC 8785 UTF-8 bytes and SHA-256 identity of a candidate profile
7
+ * plan. The returned bytes are the exact bytes embedded in receipt evidence.
8
+ */
9
+ export declare function canonicalizeAgentCandidateProfilePlan(plan: AgentCandidateWorkspacePlan): {
10
+ material: AgentCandidateProfilePlanMaterial;
11
+ bytes: Uint8Array;
12
+ digest: Sha256Digest;
13
+ };
14
+ /** Capture portable evidence of the exact canonical profile-plan bytes. */
15
+ export declare function captureAgentCandidateProfilePlan(plan: AgentCandidateWorkspacePlan): AgentCandidateProfilePlanEvidence;
16
+ import type { AgentCandidateProfilePlanEvidence, AgentCandidateProfilePlanMaterial, Sha256Digest } from "@tangle-network/agent-interface";
17
+ import type { AgentCandidateWorkspacePlan } from "./candidate-types.js";
18
+ import type { WorkspacePlan } from "./plan-types.js";
@@ -0,0 +1,90 @@
1
+ export const sha256 = (value) => createHash("sha256").update(value, "utf8").digest("hex");
2
+ export function hashWorkspacePlan(plan) {
3
+ const files = normalizePlanFiles(plan.files).map((file) => ({
4
+ relPath: file.relPath,
5
+ mode: file.mode,
6
+ contentSha256: sha256(file.content),
7
+ // Ownership decides whether re-application may replace the file, so two
8
+ // plans identical in bytes but different here behave differently in the
9
+ // same workspace — the digest a receipt proves must say which one ran.
10
+ ownership: file.ownership,
11
+ ...(file.secretSlots.length === 0
12
+ ? {}
13
+ : {
14
+ secretSlots: [...file.secretSlots].sort((left, right) => compareStrings(JSON.stringify(left.path), JSON.stringify(right.path))),
15
+ }),
16
+ }));
17
+ const env = Object.fromEntries(Object.entries(plan.env)
18
+ .sort(([a], [b]) => compareStrings(a, b))
19
+ .map(([name, value]) => [
20
+ name,
21
+ typeof value === "string"
22
+ ? value
23
+ : agentProfileSecretRefSchema.parse(value),
24
+ ]));
25
+ const unsupported = [...plan.unsupported].sort((a, b) => compareStrings(`${a.dimension}\0${a.reason}`, `${b.dimension}\0${b.reason}`));
26
+ // A plan that needs an extension is not the same plan as one that does not, even when every
27
+ // byte it writes is identical — the digest is what a receipt proves, so it must say so.
28
+ const requires = [...(plan.requires ?? [])].sort((a, b) => compareStrings(`${a.kind}\0${a.id}`, `${b.kind}\0${b.id}`));
29
+ const canonical = JSON.stringify({
30
+ harness: plan.harness,
31
+ files,
32
+ env,
33
+ flags: plan.flags.map(normalizeWorkspacePlanArgument),
34
+ ...promptIntentFields(plan, samePromptIntentValue),
35
+ // A plan that owes a launcher a replacement is not the same plan as one
36
+ // that owes nothing, even when every byte it writes is identical.
37
+ ...systemPromptBindingField(plan),
38
+ unsupported,
39
+ ...(requires.length === 0 ? {} : { requires }),
40
+ });
41
+ return `sha256:${sha256(canonical)}`;
42
+ }
43
+ /** Build the exact digest-free identity document required by agent-interface. */
44
+ export function agentCandidateProfilePlanMaterial(plan) {
45
+ const material = {
46
+ sourceProfileDigest: plan.sourceProfileDigest,
47
+ harness: plan.harness,
48
+ files: normalizePlanFiles(plan.files).map((file) => ({
49
+ relPath: file.relPath,
50
+ mode: file.mode,
51
+ contentSha256: `sha256:${sha256(file.content)}`,
52
+ })),
53
+ env: Object.fromEntries(Object.entries(plan.env).sort(([left], [right]) => compareStrings(left, right))),
54
+ flags: [...plan.flags],
55
+ ...promptIntentFields(plan, samePromptIntentValue),
56
+ unsupported: [...plan.unsupported].sort((left, right) => compareStrings(`${left.dimension}\0${left.reason}`, `${right.dimension}\0${right.reason}`)),
57
+ };
58
+ return agentCandidateProfilePlanMaterialSchema.parse(material);
59
+ }
60
+ /**
61
+ * Return the RFC 8785 UTF-8 bytes and SHA-256 identity of a candidate profile
62
+ * plan. The returned bytes are the exact bytes embedded in receipt evidence.
63
+ */
64
+ export function canonicalizeAgentCandidateProfilePlan(plan) {
65
+ const material = agentCandidateProfilePlanMaterial(plan);
66
+ const bytes = canonicalCandidateBytes(material);
67
+ const digest = canonicalCandidateDigest(material);
68
+ return { material, bytes, digest };
69
+ }
70
+ /** Capture portable evidence of the exact canonical profile-plan bytes. */
71
+ export function captureAgentCandidateProfilePlan(plan) {
72
+ const canonical = canonicalizeAgentCandidateProfilePlan(plan);
73
+ const artifact = {
74
+ encoding: "base64",
75
+ content: Buffer.from(canonical.bytes).toString("base64"),
76
+ sha256: canonical.digest,
77
+ byteLength: canonical.bytes.byteLength,
78
+ };
79
+ return agentCandidateProfilePlanEvidenceSchema.parse({
80
+ kind: "agent-profile-workspace-plan",
81
+ digest: canonical.digest,
82
+ material: canonical.material,
83
+ artifact,
84
+ });
85
+ }
86
+ import { createHash } from "node:crypto";
87
+ import { agentCandidateProfilePlanEvidenceSchema, agentCandidateProfilePlanMaterialSchema, agentProfileSecretRefSchema, canonicalCandidateBytes, canonicalCandidateDigest, } from "@tangle-network/agent-interface";
88
+ import { normalizePlanFiles, normalizeWorkspacePlanArgument, } from "./plan-normalize.js";
89
+ import { promptIntentFields, samePromptIntentValue, systemPromptBindingField, } from "./plan-prompt.js";
90
+ import { compareStrings } from "./workspace-files.js";
@@ -0,0 +1,18 @@
1
+ export declare const tomlStr: (s: string) => string;
2
+ export interface SerializedPlanFileContent {
3
+ content: string;
4
+ secretSlots?: PlanFileSecretSlot[];
5
+ }
6
+ export type AddPlanFile = (path: string, content: string | SerializedPlanFileContent, mode?: number) => void;
7
+ export declare function isProfilePublicConfigValue(value: unknown): value is Extract<AgentProfileConfigValue, {
8
+ kind: "public";
9
+ }>;
10
+ export declare function isProfileSecretRef(value: unknown): value is AgentProfileSecretRef;
11
+ export declare function workspacePlanConfigValue(value: AgentProfileConfigValue): WorkspacePlanConfigValue;
12
+ export declare function serializeWorkspacePlanJson(value: unknown): SerializedPlanFileContent;
13
+ export declare function workspacePlanArgumentSegments(value: WorkspacePlanArgument): Array<string | WorkspacePlanSecretSegment>;
14
+ export declare function concatWorkspacePlanArguments(...values: WorkspacePlanArgument[]): WorkspacePlanArgument;
15
+ export declare function joinWorkspacePlanArguments(values: WorkspacePlanArgument[], separator: string): WorkspacePlanArgument;
16
+ export declare function tomlProfileConfigValue(value: AgentProfileConfigValue): WorkspacePlanArgument;
17
+ import type { AgentProfileConfigValue, AgentProfileSecretRef } from "@tangle-network/agent-interface";
18
+ import type { PlanFileSecretSlot, WorkspacePlanArgument, WorkspacePlanConfigValue, WorkspacePlanSecretSegment } from "./plan-types.js";