@tangle-network/agent-profile-materialize 0.15.1 → 0.16.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.
- package/dist/candidate-types.d.ts +32 -0
- package/dist/candidate-types.js +1 -0
- package/dist/candidate.d.ts +9 -0
- package/dist/candidate.js +279 -0
- package/dist/commands.d.ts +2 -0
- package/dist/commands.js +32 -0
- package/dist/control-maps.d.ts +34 -0
- package/dist/control-maps.js +185 -0
- package/dist/execution-controls.d.ts +5 -0
- package/dist/execution-controls.js +186 -0
- package/dist/harness.d.ts +81 -0
- package/dist/harness.js +167 -0
- package/dist/hooks.d.ts +6 -0
- package/dist/hooks.js +261 -0
- package/dist/index.d.ts +23 -623
- package/dist/index.js +14 -3733
- package/dist/mcp.d.ts +22 -0
- package/dist/mcp.js +244 -0
- package/dist/plan-application.d.ts +10 -0
- package/dist/plan-application.js +152 -0
- package/dist/plan-digest.d.ts +18 -0
- package/dist/plan-digest.js +90 -0
- package/dist/plan-json.d.ts +18 -0
- package/dist/plan-json.js +91 -0
- package/dist/plan-normalize.d.ts +13 -0
- package/dist/plan-normalize.js +156 -0
- package/dist/plan-prompt.d.ts +38 -0
- package/dist/plan-prompt.js +49 -0
- package/dist/plan-support.d.ts +9 -0
- package/dist/plan-support.js +22 -0
- package/dist/plan-types.d.ts +294 -0
- package/dist/plan-types.js +106 -0
- package/dist/prime-agent-dir.d.ts +47 -0
- package/dist/prime-agent-dir.js +164 -0
- package/dist/prime-profile.d.ts +88 -0
- package/dist/prime-profile.js +284 -0
- package/dist/profile-materializer.d.ts +12 -0
- package/dist/profile-materializer.js +59 -0
- package/dist/profile-plan.d.ts +14 -0
- package/dist/profile-plan.js +86 -0
- package/dist/profile-prompt.d.ts +4 -0
- package/dist/profile-prompt.js +105 -0
- package/dist/profile-support.d.ts +8 -0
- package/dist/profile-support.js +6 -0
- package/dist/resource-lowering.d.ts +7 -0
- package/dist/resource-lowering.js +103 -0
- package/dist/resource-resolution.d.ts +27 -0
- package/dist/resource-resolution.js +241 -0
- package/dist/subagents.d.ts +6 -0
- package/dist/subagents.js +133 -0
- package/dist/workspace-apply.d.ts +12 -0
- package/dist/workspace-apply.js +334 -0
- package/dist/workspace-files.d.ts +8 -0
- package/dist/workspace-files.js +46 -0
- package/package.json +3 -3
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
const PRIME_SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2
|
+
const PRIME_MAX_SKILL_NAME_LENGTH = 64;
|
|
3
|
+
export const PRIME_PROFILE_DIR_NAME = "profile";
|
|
4
|
+
export const PRIME_SKILLS_DIR_NAME = "skills";
|
|
5
|
+
export const PRIME_SYSTEM_PROMPT_FILE = "system-prompt.md";
|
|
6
|
+
export const PRIME_APPEND_SYSTEM_PROMPT_FILE = "append-system-prompt.md";
|
|
7
|
+
/** Agent-dir global context file the fork reads (resource-loader.ts:59,86). */
|
|
8
|
+
export const PRIME_CONTEXT_FILE = "AGENTS.md";
|
|
9
|
+
/** Fork continual-harness store location under the agent dir (refinement.ts:269-279). */
|
|
10
|
+
export const PRIME_HARNESS_DIR_NAME = "harness";
|
|
11
|
+
export const PRIME_HARNESS_STATE_FILE = "harness_state.json";
|
|
12
|
+
export const PRIME_HARNESS_KINDS = [
|
|
13
|
+
"prompt",
|
|
14
|
+
"memory",
|
|
15
|
+
"skill",
|
|
16
|
+
"subagent",
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* Provenance marker for entries pinned from the profile. Stale removal keys on
|
|
20
|
+
* it, so agent-written (`source: "agent"`) and user-written entries are never
|
|
21
|
+
* touched.
|
|
22
|
+
*/
|
|
23
|
+
export const PRIME_PROFILE_ENTRY_SOURCE = "profile";
|
|
24
|
+
/**
|
|
25
|
+
* Provenance markers written by executors that predate this shared lowering.
|
|
26
|
+
*
|
|
27
|
+
* Pruning keys on the source tag, so an executor that switches to the shared
|
|
28
|
+
* implementation would otherwise leave its own earlier entries behind forever:
|
|
29
|
+
* they match no current profile and no current source, and the model's
|
|
30
|
+
* spawnable roster silently accumulates duplicates of every subagent. They are
|
|
31
|
+
* adopted (pruned or replaced) on the first shared run instead.
|
|
32
|
+
*/
|
|
33
|
+
export const PRIME_LEGACY_PROFILE_ENTRY_SOURCES = [
|
|
34
|
+
"cli-bridge-profile",
|
|
35
|
+
];
|
|
36
|
+
/** True when the controls materialize anything at all. */
|
|
37
|
+
export function hasPrimeProfileMaterial(controls) {
|
|
38
|
+
return (controls.systemPrompt !== undefined ||
|
|
39
|
+
controls.appendSystemPrompt !== undefined ||
|
|
40
|
+
primeProfileNeedsAgentDir(controls) ||
|
|
41
|
+
controls.skills.length > 0);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* True when the controls must WRITE INTO the agent dir itself.
|
|
45
|
+
*
|
|
46
|
+
* Prompt files and skill dirs are named by a flag, so they can live anywhere
|
|
47
|
+
* the agent can read (`--system-prompt` / `--append-system-prompt` resolve a
|
|
48
|
+
* path when it exists, fork resource-loader.ts:41-56; `--skill` takes an
|
|
49
|
+
* arbitrary dir, :437-442). The context file and the continual-harness store
|
|
50
|
+
* are located BY the agent dir and have no flag, so they cannot move.
|
|
51
|
+
*
|
|
52
|
+
* An executor pointed at an agent dir it does not own (an operator's shared
|
|
53
|
+
* dir) uses this to refuse exactly the controls it would have to write there,
|
|
54
|
+
* while still binding the ones it can materialize elsewhere.
|
|
55
|
+
*/
|
|
56
|
+
export function primeProfileNeedsAgentDir(controls) {
|
|
57
|
+
return controls.instructions !== undefined || controls.subagents.length > 0;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Read one instruction block, which the profile may carry inline or as a
|
|
61
|
+
* resource ref. Only inline refs are accepted: resolving a GitHub-backed ref
|
|
62
|
+
* fetches remote content, which is `resolveAgentProfileResources`' job and
|
|
63
|
+
* must happen before a profile reaches an executor.
|
|
64
|
+
*/
|
|
65
|
+
function primeInstructionBlock(value, control) {
|
|
66
|
+
if (typeof value === "string")
|
|
67
|
+
return value;
|
|
68
|
+
if (value.kind === "inline")
|
|
69
|
+
return value.content;
|
|
70
|
+
rejectPrimeControl(control, "only inline resource refs are accepted — resolve GitHub-backed refs " +
|
|
71
|
+
"(resolveAgentProfileResources) before handing the profile to this lowering.");
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Validate and extract the profile controls the prime agent-dir lowering
|
|
75
|
+
* materializes. Throws {@link PrimeProfileError} on any control (or control
|
|
76
|
+
* facet) the fork has no verified read path for — partial acceptance would
|
|
77
|
+
* silently drop the rest.
|
|
78
|
+
*/
|
|
79
|
+
export function readPrimeProfileControls(profile) {
|
|
80
|
+
const controls = { skills: [], subagents: [] };
|
|
81
|
+
if (!profile)
|
|
82
|
+
return controls;
|
|
83
|
+
if (profile.harness !== undefined && profile.harness !== "prime") {
|
|
84
|
+
rejectPrimeDeclaration("harness", `prime cannot run a profile pinned to harness "${profile.harness}" — ` +
|
|
85
|
+
'only "prime" (or an unset harness preference) is accepted.');
|
|
86
|
+
}
|
|
87
|
+
// An entry with `enabled: false` is OFF, not requested. A fully disabled map
|
|
88
|
+
// asks for nothing, so accepting it drops no control — which is the only
|
|
89
|
+
// thing this refusal exists to prevent.
|
|
90
|
+
const requestedMcp = Object.keys(activeMcpServers(profile.mcp));
|
|
91
|
+
if (requestedMcp.length > 0) {
|
|
92
|
+
rejectPrimeControl(`mcp: ${requestedMcp.join(", ")}`, PRIME_MCP_UNSUPPORTED_REASON);
|
|
93
|
+
}
|
|
94
|
+
if (profile.connections !== undefined) {
|
|
95
|
+
rejectPrimeControl("connections", `a hub connection grant resolves to MCP tools, and ${PRIME_MCP_UNSUPPORTED_REASON}`);
|
|
96
|
+
}
|
|
97
|
+
if (profile.permissions !== undefined) {
|
|
98
|
+
rejectPrimeControl("permissions", "prime-agent exposes no permission control at all (full flag scan of fork " +
|
|
99
|
+
"cli/args.ts:93-295; native tools run unconditionally), so a permission map " +
|
|
100
|
+
"would be accepted and never enforced.");
|
|
101
|
+
}
|
|
102
|
+
if (profile.tools !== undefined) {
|
|
103
|
+
rejectPrimeControl("tools", "the only per-run tool control is `--tools` over the single builtin `ipython` " +
|
|
104
|
+
"(fork cli/args.ts:63-64,156-167); bash/edit and the rest are extension-provided " +
|
|
105
|
+
"and not addressable per run, so a harness-neutral tool map cannot bind to it.");
|
|
106
|
+
}
|
|
107
|
+
if (profile.hooks !== undefined) {
|
|
108
|
+
rejectPrimeControl("hooks", "prime-agent has no hook mechanism (fork cli/args.ts flag scan; extensions are " +
|
|
109
|
+
"host-trusted JS modules loaded into the agent process, not caller command hooks).");
|
|
110
|
+
}
|
|
111
|
+
if (profile.modes !== undefined) {
|
|
112
|
+
rejectPrimeControl("modes", "prime-agent has no mode concept (fork cli/args.ts flag scan).");
|
|
113
|
+
}
|
|
114
|
+
if (profile.confidential !== undefined) {
|
|
115
|
+
rejectPrimeControl("confidential", "confidential execution is a property of the executor's isolation boundary, not " +
|
|
116
|
+
"of the agent dir; this lowering cannot establish it.");
|
|
117
|
+
}
|
|
118
|
+
const instructionBlocks = [];
|
|
119
|
+
if (profile.prompt) {
|
|
120
|
+
const { systemPrompt, appendSystemPrompt, instructions } = profile.prompt;
|
|
121
|
+
const knownPromptFields = new Set([
|
|
122
|
+
"systemPrompt",
|
|
123
|
+
"appendSystemPrompt",
|
|
124
|
+
"instructions",
|
|
125
|
+
]);
|
|
126
|
+
const unknown = Object.entries(profile.prompt)
|
|
127
|
+
.filter(([key, value]) => !knownPromptFields.has(key) && value !== undefined)
|
|
128
|
+
.map(([key]) => key);
|
|
129
|
+
if (unknown.length > 0) {
|
|
130
|
+
rejectPrimeControl(`prompt fields: ${unknown.join(", ")}`, "no verified prime-agent read path exists for them.");
|
|
131
|
+
}
|
|
132
|
+
if (systemPrompt !== undefined) {
|
|
133
|
+
if (typeof systemPrompt !== "string" || systemPrompt.trim() === "") {
|
|
134
|
+
rejectPrimeDeclaration("prompt.systemPrompt", "an empty replacement would delete the built-in system prompt and " +
|
|
135
|
+
"install nothing.");
|
|
136
|
+
}
|
|
137
|
+
controls.systemPrompt = systemPrompt;
|
|
138
|
+
}
|
|
139
|
+
if (appendSystemPrompt !== undefined) {
|
|
140
|
+
if (typeof appendSystemPrompt !== "string" ||
|
|
141
|
+
appendSystemPrompt.trim() === "") {
|
|
142
|
+
rejectPrimeDeclaration("prompt.appendSystemPrompt", "must be non-empty text.");
|
|
143
|
+
}
|
|
144
|
+
controls.appendSystemPrompt = appendSystemPrompt;
|
|
145
|
+
}
|
|
146
|
+
for (const block of instructions ?? []) {
|
|
147
|
+
if (typeof block === "string" && block.trim() !== "")
|
|
148
|
+
instructionBlocks.push(block);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (profile.resources) {
|
|
152
|
+
const { skills, instructions, failOnError, ...unsupported } = profile.resources;
|
|
153
|
+
const rejectedKeys = Object.entries(unsupported)
|
|
154
|
+
.filter(([, value]) => value !== undefined)
|
|
155
|
+
.map(([key]) => key);
|
|
156
|
+
if (rejectedKeys.length > 0) {
|
|
157
|
+
rejectPrimeControl(`resources: ${rejectedKeys.join(", ")}`, "only resources.skills (--skill dirs, fork resource-loader.ts:437-442) and " +
|
|
158
|
+
"resources.instructions (agent-dir AGENTS.md, resource-loader.ts:86) have a " +
|
|
159
|
+
"verified prime-agent read path. Generic workspace files, tool files, " +
|
|
160
|
+
"subagent-definition files and prompt templates either have no headless read " +
|
|
161
|
+
"path or would run caller-authored code inside the agent process.");
|
|
162
|
+
}
|
|
163
|
+
if (failOnError === false) {
|
|
164
|
+
rejectPrimeControl("resources.failOnError: false", "this lowering always fails loud on a resource it cannot " +
|
|
165
|
+
"materialize; lenient materialization is not implemented.");
|
|
166
|
+
}
|
|
167
|
+
if (instructions !== undefined) {
|
|
168
|
+
const block = primeInstructionBlock(instructions, "resources.instructions");
|
|
169
|
+
if (block.trim() !== "")
|
|
170
|
+
instructionBlocks.push(block);
|
|
171
|
+
}
|
|
172
|
+
const seen = new Set();
|
|
173
|
+
for (const [index, ref] of (skills ?? []).entries()) {
|
|
174
|
+
if (ref.kind !== "inline") {
|
|
175
|
+
rejectPrimeControl(`resources.skills[${index}]`, "only inline skill refs are accepted — resolve GitHub-backed refs " +
|
|
176
|
+
"(resolveAgentProfileResources) before handing the profile to " +
|
|
177
|
+
"this lowering.");
|
|
178
|
+
}
|
|
179
|
+
const name = ref.name;
|
|
180
|
+
if (!PRIME_SKILL_NAME_RE.test(name) ||
|
|
181
|
+
name.length > PRIME_MAX_SKILL_NAME_LENGTH) {
|
|
182
|
+
rejectPrimeDeclaration(`resources.skills[${index}]`, `name ${JSON.stringify(name)} is not a valid prime skill name — it becomes the ` +
|
|
183
|
+
"skill directory name, so it must be lowercase a-z0-9 with single hyphens, at " +
|
|
184
|
+
`most ${PRIME_MAX_SKILL_NAME_LENGTH} chars (fork skills.ts:122-147).`);
|
|
185
|
+
}
|
|
186
|
+
if (seen.has(name)) {
|
|
187
|
+
rejectPrimeDeclaration("resources.skills", `declares ${JSON.stringify(name)} twice; duplicate skill names collide in the ` +
|
|
188
|
+
"fork's skill registry, which keeps only the first and warns.");
|
|
189
|
+
}
|
|
190
|
+
seen.add(name);
|
|
191
|
+
if (ref.content.trim() === "") {
|
|
192
|
+
rejectPrimeDeclaration(`resources.skills[${index}]`, `skill ${JSON.stringify(name)} declares no content.`);
|
|
193
|
+
}
|
|
194
|
+
controls.skills.push({ name, content: ref.content });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (instructionBlocks.length > 0) {
|
|
198
|
+
controls.instructions = instructionBlocks.join("\n\n");
|
|
199
|
+
}
|
|
200
|
+
if (profile.subagents) {
|
|
201
|
+
const usedIds = new Map();
|
|
202
|
+
for (const [name, spec] of Object.entries(profile.subagents)) {
|
|
203
|
+
if (name.trim() === "") {
|
|
204
|
+
rejectPrimeDeclaration("subagents", "a subagent name must be non-empty.");
|
|
205
|
+
}
|
|
206
|
+
const inexpressible = [
|
|
207
|
+
["model", spec.model],
|
|
208
|
+
["tools", spec.tools],
|
|
209
|
+
["permissions", spec.permissions],
|
|
210
|
+
["maxSteps", spec.maxSteps],
|
|
211
|
+
]
|
|
212
|
+
.filter(([, value]) => value !== undefined)
|
|
213
|
+
.map(([key]) => key);
|
|
214
|
+
if (inexpressible.length > 0) {
|
|
215
|
+
rejectPrimeControl(`subagents[${JSON.stringify(name)}] fields: ${inexpressible.join(", ")}`, "a prime continual-harness subagent spec is prompt-roster " +
|
|
216
|
+
"guidance for `await rlm('<task>')` (fork refinement.ts:483-496) " +
|
|
217
|
+
"and pins no model, tool set, permission, or step budget on the " +
|
|
218
|
+
"spawned child.");
|
|
219
|
+
}
|
|
220
|
+
if (!spec.description?.trim() && !spec.prompt?.trim()) {
|
|
221
|
+
rejectPrimeDeclaration(`subagents[${JSON.stringify(name)}]`, "declares no behavior — a spec needs a description and/or prompt, since an " +
|
|
222
|
+
"empty entry gives the model an empty roster line.");
|
|
223
|
+
}
|
|
224
|
+
const id = primeSubagentEntryId(name);
|
|
225
|
+
const clash = usedIds.get(id);
|
|
226
|
+
if (clash !== undefined) {
|
|
227
|
+
rejectPrimeDeclaration(`subagents[${JSON.stringify(name)}]`, `collides with subagent ${JSON.stringify(clash)} on harness-state id ` +
|
|
228
|
+
`${JSON.stringify(id)}.`);
|
|
229
|
+
}
|
|
230
|
+
usedIds.set(id, name);
|
|
231
|
+
controls.subagents.push({
|
|
232
|
+
name,
|
|
233
|
+
description: spec.description,
|
|
234
|
+
prompt: spec.prompt,
|
|
235
|
+
metadata: spec.metadata,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return controls;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Refuse a run whose selected model disagrees with the profile's own.
|
|
243
|
+
*
|
|
244
|
+
* One authority per run: a profile names the model it was written and scored
|
|
245
|
+
* against, so running it on a different one silently scores a different agent.
|
|
246
|
+
* Both sides go through {@link modelIdsMatch}, so the qualification level a
|
|
247
|
+
* caller happened to use is not a conflict.
|
|
248
|
+
*
|
|
249
|
+
* `provider` is the executor's PROVIDER SELECTION, and is compared only when
|
|
250
|
+
* the executor's provider is part of model identity. An executor whose
|
|
251
|
+
* provider key is merely a namespace inside the agent dir passes it as
|
|
252
|
+
* undefined and gets the model-name comparison alone.
|
|
253
|
+
*/
|
|
254
|
+
export function assertPrimeModelAgreement(profile, selected) {
|
|
255
|
+
const declared = profile?.model?.default;
|
|
256
|
+
const declaredProvider = profile?.model?.provider;
|
|
257
|
+
if (declared !== undefined) {
|
|
258
|
+
const selectedId = qualifyModelId(selected.provider, selected.model);
|
|
259
|
+
const declaredId = qualifyModelId(declaredProvider, declared);
|
|
260
|
+
if (!modelIdsMatch(selectedId, declaredId, selected.provider)) {
|
|
261
|
+
rejectPrimeDeclaration("model.default", `the run selects ${JSON.stringify(selectedId)} but the profile names ` +
|
|
262
|
+
`${JSON.stringify(declaredId)}; a profile must not be scored against a model it ` +
|
|
263
|
+
"does not name.");
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (selected.provider !== undefined &&
|
|
268
|
+
declaredProvider !== undefined &&
|
|
269
|
+
declaredProvider !== selected.provider) {
|
|
270
|
+
rejectPrimeDeclaration("model.provider", `the run selects provider ${JSON.stringify(selected.provider)} but the profile names ` +
|
|
271
|
+
`${JSON.stringify(declaredProvider)}.`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/** Deterministic harness-state entry id for a profile subagent name. */
|
|
275
|
+
export function primeSubagentEntryId(name) {
|
|
276
|
+
const slug = name
|
|
277
|
+
.toLowerCase()
|
|
278
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
279
|
+
.replace(/^-+|-+$/g, "");
|
|
280
|
+
return `profile-${slug || "subagent"}`;
|
|
281
|
+
}
|
|
282
|
+
import { modelIdsMatch, qualifyModelId } from "./model-id.js";
|
|
283
|
+
import { PRIME_MCP_UNSUPPORTED_REASON, rejectPrimeControl, rejectPrimeDeclaration, } from "./prime-fork.js";
|
|
284
|
+
import { activeMcpServers } from "./profile-support.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AgentProfile, AgentProfileSecretProvider } from "@tangle-network/agent-interface";
|
|
2
|
+
import { type HarnessId } from "./harness.js";
|
|
3
|
+
import { type BindableLaunchControl, type SkippableDimension, type WorkspacePlan } from "./plan-types.js";
|
|
4
|
+
export declare const UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER: AgentProfileSecretProvider;
|
|
5
|
+
/**
|
|
6
|
+
* Build a pure workspace plan for one canonical harness.
|
|
7
|
+
* The caller applies files, environment, and flags at its process boundary.
|
|
8
|
+
*/
|
|
9
|
+
export declare function materializeProfile(inputProfile: AgentProfile, harness: HarnessId, opts?: {
|
|
10
|
+
skip?: SkippableDimension[];
|
|
11
|
+
binds?: readonly BindableLaunchControl[];
|
|
12
|
+
}): WorkspacePlan;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { agentProfileSchema } from "@tangle-network/agent-interface";
|
|
2
|
+
import { materializeExecutionControls } from "./execution-controls.js";
|
|
3
|
+
import { assertMaterializerHarness } from "./harness.js";
|
|
4
|
+
import { materializeHooks } from "./hooks.js";
|
|
5
|
+
import { materializeMcp } from "./mcp.js";
|
|
6
|
+
import { assertBindableLaunchControls, assertSkippableDimensions, } from "./plan-types.js";
|
|
7
|
+
import { createProfilePlanBuilder } from "./profile-plan.js";
|
|
8
|
+
import { lowerProfilePrompt } from "./profile-prompt.js";
|
|
9
|
+
import { activeMcpServers, extensionNamespaces } from "./profile-support.js";
|
|
10
|
+
import { lowerProfileCommands, lowerProfileResources, lowerProfileSkills, } from "./resource-lowering.js";
|
|
11
|
+
import { materializeSubagent } from "./subagents.js";
|
|
12
|
+
export const UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER = Object.freeze({
|
|
13
|
+
async get() {
|
|
14
|
+
return undefined;
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Build a pure workspace plan for one canonical harness.
|
|
19
|
+
* The caller applies files, environment, and flags at its process boundary.
|
|
20
|
+
*/
|
|
21
|
+
export function materializeProfile(inputProfile, harness, opts = {}) {
|
|
22
|
+
assertMaterializerHarness(harness);
|
|
23
|
+
const profile = agentProfileSchema.parse(inputProfile);
|
|
24
|
+
const skip = new Set(assertSkippableDimensions(opts.skip));
|
|
25
|
+
const binds = assertBindableLaunchControls(opts.binds);
|
|
26
|
+
const builder = createProfilePlanBuilder(harness);
|
|
27
|
+
lowerProfileResources(profile, harness, builder);
|
|
28
|
+
lowerProfilePrompt(profile, harness, binds, builder);
|
|
29
|
+
materializeExecutionControls(harness, profile.tools, profile.permissions, profile.modes, builder.plan, builder.add, builder.unsupported);
|
|
30
|
+
if ((profile.connections?.length ?? 0) > 0) {
|
|
31
|
+
builder.unsupported("connections", `${harness}: Hub connections must be resolved to authorized MCP servers by the execution runtime`);
|
|
32
|
+
}
|
|
33
|
+
if (profile.confidential !== undefined) {
|
|
34
|
+
builder.unsupported("confidential", `${harness}: confidential execution requires runtime placement and attestation, not workspace files`);
|
|
35
|
+
}
|
|
36
|
+
lowerProfileSkills(profile, harness, builder);
|
|
37
|
+
const mcp = activeMcpServers(profile.mcp);
|
|
38
|
+
if (!skip.has("mcp") && Object.keys(mcp).length > 0) {
|
|
39
|
+
materializeMcp(harness, mcp, builder.plan, builder.add, builder.unsupported);
|
|
40
|
+
}
|
|
41
|
+
const hooks = profile.hooks ?? {};
|
|
42
|
+
if (Object.keys(hooks).length > 0) {
|
|
43
|
+
materializeHooks(harness, hooks, builder.add, builder.unsupported, builder.plan);
|
|
44
|
+
}
|
|
45
|
+
const subagents = profile.subagents ?? {};
|
|
46
|
+
for (const [name, subagent] of Object.entries(subagents)) {
|
|
47
|
+
materializeSubagent(harness, name, subagent, builder.plan, builder.add, builder.unsupported);
|
|
48
|
+
}
|
|
49
|
+
if (!skip.has("extensions")) {
|
|
50
|
+
for (const namespace of extensionNamespaces(harness)) {
|
|
51
|
+
const extension = profile.extensions?.[namespace];
|
|
52
|
+
if (extension && Object.keys(extension).length > 0) {
|
|
53
|
+
builder.unsupported("extensions", `${harness}: extension namespace "${namespace}" requires provider-native handling`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
lowerProfileCommands(profile, harness, builder);
|
|
58
|
+
return builder.finalize(profile.resources?.failOnError);
|
|
59
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AgentProfileConfigValue } from "@tangle-network/agent-interface";
|
|
2
|
+
import type { HarnessId } from "./harness.js";
|
|
3
|
+
import type { AddPlanFile } from "./plan-json.js";
|
|
4
|
+
import type { Unsupported, WorkspacePlan } from "./plan-types.js";
|
|
5
|
+
export interface ProfilePlanBuilder {
|
|
6
|
+
plan: WorkspacePlan;
|
|
7
|
+
add: AddPlanFile;
|
|
8
|
+
addExplicitResource: AddExplicitResource;
|
|
9
|
+
unsupported: (dimension: Unsupported["dimension"], reason: string) => void;
|
|
10
|
+
finalize: (failOnError?: boolean) => WorkspacePlan;
|
|
11
|
+
}
|
|
12
|
+
export type AddExplicitResource = (dimension: Unsupported["dimension"], path: string, content: string, mode?: number) => void;
|
|
13
|
+
export declare function createProfilePlanBuilder(harness: HarnessId): ProfilePlanBuilder;
|
|
14
|
+
export declare function setPlanEnvironment(plan: WorkspacePlan, name: string, value: string | AgentProfileConfigValue): void;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { MATERIALIZER_OWNED_PATHS } from "./harness.js";
|
|
2
|
+
import { workspacePlanConfigValue } from "./plan-json.js";
|
|
3
|
+
import { mergeJsonFiles } from "./plan-normalize.js";
|
|
4
|
+
import { formatUnsupported } from "./plan-support.js";
|
|
5
|
+
import { compareStrings, normalizeWorkspaceRelativePath, PRIVATE_FILE_MODE, validateFileMode, } from "./workspace-files.js";
|
|
6
|
+
export function createProfilePlanBuilder(harness) {
|
|
7
|
+
const plan = {
|
|
8
|
+
harness,
|
|
9
|
+
files: [],
|
|
10
|
+
env: {},
|
|
11
|
+
flags: [],
|
|
12
|
+
unsupported: [],
|
|
13
|
+
requires: [],
|
|
14
|
+
};
|
|
15
|
+
const pushPlanFile = (relPath, serialized, source, mode) => {
|
|
16
|
+
const normalizedPath = normalizeWorkspaceRelativePath(relPath);
|
|
17
|
+
const content = typeof serialized === "string" ? serialized : serialized.content;
|
|
18
|
+
const secretSlots = typeof serialized === "string" ? undefined : serialized.secretSlots;
|
|
19
|
+
const fileMode = mode ??
|
|
20
|
+
(secretSlots !== undefined && secretSlots.length > 0
|
|
21
|
+
? PRIVATE_FILE_MODE
|
|
22
|
+
: undefined);
|
|
23
|
+
plan.files.push({
|
|
24
|
+
relPath: normalizedPath,
|
|
25
|
+
content,
|
|
26
|
+
source,
|
|
27
|
+
...(source === "generated" && MATERIALIZER_OWNED_PATHS.has(normalizedPath)
|
|
28
|
+
? { ownership: "materializer" }
|
|
29
|
+
: {}),
|
|
30
|
+
...(fileMode === undefined ? {} : { mode: validateFileMode(fileMode) }),
|
|
31
|
+
...(secretSlots === undefined
|
|
32
|
+
? {}
|
|
33
|
+
: {
|
|
34
|
+
secretSlots: secretSlots.map((slot) => ({
|
|
35
|
+
path: [...slot.path],
|
|
36
|
+
ref: { ...slot.ref },
|
|
37
|
+
})),
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
const add = (relPath, content, mode) => pushPlanFile(relPath, content, "generated", mode);
|
|
42
|
+
const unsupported = (dimension, reason) => plan.unsupported.push({ dimension, reason });
|
|
43
|
+
const explicitResourcePaths = new Map();
|
|
44
|
+
const addExplicitResource = (dimension, relPath, content, mode) => {
|
|
45
|
+
const normalizedPath = normalizeWorkspaceRelativePath(relPath);
|
|
46
|
+
const previousDimension = explicitResourcePaths.get(normalizedPath);
|
|
47
|
+
if (previousDimension) {
|
|
48
|
+
throw new Error(`Duplicate profile resource path: ${normalizedPath} (${previousDimension}, ${dimension})`);
|
|
49
|
+
}
|
|
50
|
+
if (MATERIALIZER_OWNED_PATHS.has(normalizedPath)) {
|
|
51
|
+
throw new Error(`Profile resource path is reserved by the materializer: ${normalizedPath} (${dimension}). ` +
|
|
52
|
+
"This package writes that path itself and replaces it on re-application, so a mount " +
|
|
53
|
+
"there cannot survive; choose another path.");
|
|
54
|
+
}
|
|
55
|
+
explicitResourcePaths.set(normalizedPath, dimension);
|
|
56
|
+
pushPlanFile(normalizedPath, content, "user", mode);
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
plan,
|
|
60
|
+
add,
|
|
61
|
+
addExplicitResource,
|
|
62
|
+
unsupported,
|
|
63
|
+
finalize(failOnError) {
|
|
64
|
+
for (const [relPath, dimension] of explicitResourcePaths) {
|
|
65
|
+
const writers = plan.files.filter((file) => file.relPath === relPath).length;
|
|
66
|
+
if (writers > 1) {
|
|
67
|
+
throw new Error(`Profile ${dimension} resource conflicts with generated harness file: ${relPath}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
plan.files = mergeJsonFiles(plan.files).sort((a, b) => compareStrings(a.relPath, b.relPath));
|
|
71
|
+
if (failOnError && plan.unsupported.length) {
|
|
72
|
+
throw new Error(formatUnsupported(plan.unsupported));
|
|
73
|
+
}
|
|
74
|
+
return plan;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function setPlanEnvironment(plan, name, value) {
|
|
79
|
+
const normalized = typeof value === "string" ? value : workspacePlanConfigValue(value);
|
|
80
|
+
const previous = plan.env[name];
|
|
81
|
+
if (previous !== undefined &&
|
|
82
|
+
JSON.stringify(previous) !== JSON.stringify(normalized)) {
|
|
83
|
+
throw new Error(`profile environment values disagree for ${name}`);
|
|
84
|
+
}
|
|
85
|
+
plan.env[name] = normalized;
|
|
86
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AgentProfile } from "@tangle-network/agent-interface";
|
|
2
|
+
import type { BindableLaunchControl, HarnessId } from "./plan-types.js";
|
|
3
|
+
import type { ProfilePlanBuilder } from "./profile-plan.js";
|
|
4
|
+
export declare function lowerProfilePrompt(profile: AgentProfile, harness: HarnessId, binds: ReadonlySet<BindableLaunchControl>, builder: ProfilePlanBuilder): void;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { appendSystemPromptRefusal, CODEX_SYSTEM_PROMPT_FILE, CONTEXT_FILE, OPENCODE_APPEND_SYSTEM_PROMPT_FILE, OPENCODE_PRIMARY_AGENT_PREFIX, systemPromptRefusal, } from "./harness.js";
|
|
2
|
+
import { sha256 } from "./plan-digest.js";
|
|
3
|
+
import { serializeWorkspacePlanJson } from "./plan-json.js";
|
|
4
|
+
export function lowerProfilePrompt(profile, harness, binds, builder) {
|
|
5
|
+
const { plan, add, unsupported } = builder;
|
|
6
|
+
if (profile.prompt?.systemPrompt !== undefined) {
|
|
7
|
+
if (harness === "claude-code" || harness === "pi" || harness === "prime") {
|
|
8
|
+
plan.systemPrompt = profile.prompt.systemPrompt;
|
|
9
|
+
}
|
|
10
|
+
else if (harness === "codex") {
|
|
11
|
+
add(CODEX_SYSTEM_PROMPT_FILE, profile.prompt.systemPrompt);
|
|
12
|
+
plan.flags.push("-c", `model_instructions_file=${CODEX_SYSTEM_PROMPT_FILE}`);
|
|
13
|
+
}
|
|
14
|
+
else if (harness === "gemini") {
|
|
15
|
+
add(".gemini/system.md", profile.prompt.systemPrompt);
|
|
16
|
+
plan.env.GEMINI_SYSTEM_MD = "1";
|
|
17
|
+
}
|
|
18
|
+
else if (harness === "opencode") {
|
|
19
|
+
const systemPrompt = profile.prompt.systemPrompt;
|
|
20
|
+
if (systemPrompt.trim().length === 0) {
|
|
21
|
+
throw new Error("opencode cannot express an empty exact system-prompt replacement");
|
|
22
|
+
}
|
|
23
|
+
if (binds.has("opencode.agent-prompt")) {
|
|
24
|
+
plan.systemPromptBinding = {
|
|
25
|
+
intent: "replace",
|
|
26
|
+
control: "opencode.agent-prompt",
|
|
27
|
+
systemPrompt,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
else if (binds.has("systemPrompt")) {
|
|
31
|
+
const primaryAgentName = `${OPENCODE_PRIMARY_AGENT_PREFIX}${sha256(systemPrompt).slice(0, 16)}`;
|
|
32
|
+
const primaryAgentPath = `.opencode/agents/${primaryAgentName}.md`;
|
|
33
|
+
if (profile.subagents?.[primaryAgentName] !== undefined) {
|
|
34
|
+
throw new Error(`opencode: subagent name "${primaryAgentName}" is reserved for the profile primary agent`);
|
|
35
|
+
}
|
|
36
|
+
plan.systemPrompt = systemPrompt;
|
|
37
|
+
add(primaryAgentPath, ["---", "mode: primary", "---", systemPrompt, ""].join("\n"));
|
|
38
|
+
plan.flags.push("--agent", primaryAgentName);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
unsupported("systemPrompt", systemPromptRefusal(harness));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
unsupported("systemPrompt", systemPromptRefusal(harness));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const openCodeInstructionFiles = [];
|
|
49
|
+
const openCodeConfigUnreachable = harness === "opencode" && binds.has("opencode.agent-prompt");
|
|
50
|
+
if (profile.prompt?.appendSystemPrompt !== undefined) {
|
|
51
|
+
const appendSystemPrompt = profile.prompt.appendSystemPrompt;
|
|
52
|
+
if (appendSystemPrompt.trim().length === 0) {
|
|
53
|
+
throw new Error("prompt.appendSystemPrompt must be non-empty text");
|
|
54
|
+
}
|
|
55
|
+
if (openCodeConfigUnreachable) {
|
|
56
|
+
unsupported("appendSystemPrompt", "opencode: the launcher that binds opencode.agent-prompt composes opencode's config itself and loads no workspace opencode.json, so an addition registered there would never reach the model; compose it into the launcher's own instructions[]");
|
|
57
|
+
}
|
|
58
|
+
else if (harness === "opencode") {
|
|
59
|
+
add(OPENCODE_APPEND_SYSTEM_PROMPT_FILE, appendSystemPrompt.endsWith("\n")
|
|
60
|
+
? appendSystemPrompt
|
|
61
|
+
: `${appendSystemPrompt}\n`);
|
|
62
|
+
openCodeInstructionFiles.push(OPENCODE_APPEND_SYSTEM_PROMPT_FILE);
|
|
63
|
+
}
|
|
64
|
+
else if (harness === "claude-code" ||
|
|
65
|
+
harness === "pi" ||
|
|
66
|
+
harness === "prime") {
|
|
67
|
+
plan.appendSystemPrompt = appendSystemPrompt;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
unsupported("appendSystemPrompt", appendSystemPromptRefusal(harness));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const instructions = [];
|
|
74
|
+
if (profile.prompt?.instructions?.length) {
|
|
75
|
+
instructions.push(...profile.prompt.instructions);
|
|
76
|
+
}
|
|
77
|
+
const resourceInstructions = profile.resources?.instructions;
|
|
78
|
+
if (typeof resourceInstructions === "string") {
|
|
79
|
+
instructions.push(resourceInstructions);
|
|
80
|
+
}
|
|
81
|
+
else if (resourceInstructions && resourceInstructions.kind === "inline") {
|
|
82
|
+
instructions.push(resourceInstructions.content);
|
|
83
|
+
}
|
|
84
|
+
else if (resourceInstructions) {
|
|
85
|
+
unsupported("instructions", "instruction ref is not inline (github refs need async pre-resolution)");
|
|
86
|
+
}
|
|
87
|
+
if (instructions.length) {
|
|
88
|
+
if (openCodeConfigUnreachable) {
|
|
89
|
+
unsupported("instructions", "opencode: the launcher that binds opencode.agent-prompt composes opencode's config itself and loads no workspace opencode.json, so an instruction file registered there would never reach the model; compose it into the launcher's own instructions[]");
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const instructionPath = CONTEXT_FILE[harness];
|
|
93
|
+
add(instructionPath, `${instructions.join("\n\n")}\n`);
|
|
94
|
+
if (harness === "opencode") {
|
|
95
|
+
openCodeInstructionFiles.push(instructionPath);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (openCodeInstructionFiles.length > 0) {
|
|
100
|
+
add("opencode.json", serializeWorkspacePlanJson({
|
|
101
|
+
$schema: "https://opencode.ai/config.json",
|
|
102
|
+
instructions: openCodeInstructionFiles,
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AgentProfile, AgentProfileMcpServer } from "@tangle-network/agent-interface";
|
|
2
|
+
import type { HarnessId } from "./harness.js";
|
|
3
|
+
type ActiveAgentProfileMcpServer = Exclude<AgentProfileMcpServer, {
|
|
4
|
+
enabled: false;
|
|
5
|
+
}>;
|
|
6
|
+
export declare function activeMcpServers(servers: AgentProfile["mcp"]): Record<string, ActiveAgentProfileMcpServer>;
|
|
7
|
+
export declare function extensionNamespaces(harness: HarnessId): string[];
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AgentProfile } from "@tangle-network/agent-interface";
|
|
2
|
+
import type { HarnessId } from "./harness.js";
|
|
3
|
+
import type { ProfilePlanBuilder } from "./profile-plan.js";
|
|
4
|
+
export declare function lowerProfileResources(profile: AgentProfile, harness: HarnessId, builder: ProfilePlanBuilder): void;
|
|
5
|
+
export declare function lowerProfileSkills(profile: AgentProfile, harness: HarnessId, builder: ProfilePlanBuilder): void;
|
|
6
|
+
export declare function lowerProfileCommands(profile: AgentProfile, harness: HarnessId, builder: ProfilePlanBuilder): void;
|
|
7
|
+
export declare function agentResourceDirectory(harness: HarnessId): string | null;
|