@tangle-network/agent-profile-materialize 0.12.0 → 0.13.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
@@ -22,6 +22,7 @@
22
22
  * cli-bridge and agent-dev-container — this is the single source those re-export.
23
23
  */
24
24
  export { modelIdsMatch, qualifyModelId } from "./model-id.js";
25
+ export { matchPrimeKernelDeadMarker, PRIME_AGENT_DIR_ENV_VAR, PRIME_AGENT_DIR_ENV_VARS, PRIME_FORK_INSTALL, PRIME_FORK_PIN, PRIME_HARNESS_STATE_ENV_DENYLIST, PRIME_KERNEL_DEAD_MARKERS, PRIME_KERNEL_ENV_PASSTHROUGH, PRIME_MCP_UNSUPPORTED_REASON, PRIME_THINKING_LEVELS, PrimeHarnessStateError, PrimeProfileError, type PrimeProfileErrorKind, type PrimeThinkingLevel, parsePrimeModelsJson, primeAgentDirEnv, primeApiKeyEnvNames, primeThinkingLevel, stripPrimeJsonComments, } from "./prime-fork.js";
25
26
  import type { AgentCandidateConfigValue, AgentCandidateProfile, AgentCandidateProfileActivation, AgentCandidateProfileApplication, AgentCandidateProfilePlanEvidence, AgentCandidateProfilePlanMaterial, AgentProfile, AgentProfileConfigValue, AgentProfileMcpServer, AgentProfileSecretProvider, AgentProfileSecretRef, Sha256Digest } from "@tangle-network/agent-interface";
26
27
  export declare const UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER: AgentProfileSecretProvider;
27
28
  /**
@@ -358,6 +359,12 @@ export declare function takeSystemPromptBinding(spawn: Pick<WorkspacePlanSpawnIn
358
359
  export interface MaterializeCandidateProfileOptions {
359
360
  /** Exact text fetched from pinned resource refs, keyed by declared SHA-256. */
360
361
  resolvedResources?: ReadonlyMap<Sha256Digest, string>;
362
+ /**
363
+ * Launcher controls this caller owns and will bind itself. The declaration
364
+ * is forwarded to the classic materializer so candidate execution uses the
365
+ * same lowering as ordinary profile execution.
366
+ */
367
+ binds?: readonly BindableLaunchControl[];
361
368
  }
362
369
  /**
363
370
  * Whether the harness LOADS its {@link CONTEXT_FILE} from cwd on its own.
@@ -503,31 +510,105 @@ export interface PrimeProfileSubagent {
503
510
  export interface PrimeProfileControls {
504
511
  systemPrompt?: string;
505
512
  appendSystemPrompt?: string;
513
+ /** Composed caller instructions, lowered to the agent dir's AGENTS.md. */
514
+ instructions?: string;
506
515
  skills: PrimeProfileSkill[];
507
516
  subagents: PrimeProfileSubagent[];
508
517
  }
518
+ /** True when the controls materialize anything at all. */
519
+ export declare function hasPrimeProfileMaterial(controls: PrimeProfileControls): boolean;
520
+ /**
521
+ * True when the controls must WRITE INTO the agent dir itself.
522
+ *
523
+ * Prompt files and skill dirs are named by a flag, so they can live anywhere
524
+ * the agent can read (`--system-prompt` / `--append-system-prompt` resolve a
525
+ * path when it exists, fork resource-loader.ts:41-56; `--skill` takes an
526
+ * arbitrary dir, :437-442). The context file and the continual-harness store
527
+ * are located BY the agent dir and have no flag, so they cannot move.
528
+ *
529
+ * An executor pointed at an agent dir it does not own (an operator's shared
530
+ * dir) uses this to refuse exactly the controls it would have to write there,
531
+ * while still binding the ones it can materialize elsewhere.
532
+ */
533
+ export declare function primeProfileNeedsAgentDir(controls: PrimeProfileControls): boolean;
509
534
  /**
510
535
  * Validate and extract the profile controls the prime agent-dir lowering
511
- * materializes. Throws on any control (or control facet) the fork has no
512
- * verified read path for — partial acceptance would silently drop the rest.
536
+ * materializes. Throws {@link PrimeProfileError} on any control (or control
537
+ * facet) the fork has no verified read path for — partial acceptance would
538
+ * silently drop the rest.
513
539
  */
514
540
  export declare function readPrimeProfileControls(profile: AgentProfile | undefined): PrimeProfileControls;
541
+ /**
542
+ * Refuse a run whose selected model disagrees with the profile's own.
543
+ *
544
+ * One authority per run: a profile names the model it was written and scored
545
+ * against, so running it on a different one silently scores a different agent.
546
+ * Both sides go through {@link modelIdsMatch}, so the qualification level a
547
+ * caller happened to use is not a conflict.
548
+ *
549
+ * `provider` is the executor's PROVIDER SELECTION, and is compared only when
550
+ * the executor's provider is part of model identity. An executor whose
551
+ * provider key is merely a namespace inside the agent dir passes it as
552
+ * undefined and gets the model-name comparison alone.
553
+ */
554
+ export declare function assertPrimeModelAgreement(profile: AgentProfile | undefined, selected: {
555
+ provider?: string;
556
+ model: string;
557
+ }): void;
515
558
  /** Deterministic harness-state entry id for a profile subagent name. */
516
559
  export declare function primeSubagentEntryId(name: string): string;
517
560
  /**
518
- * Materialize the validated controls into `agentDir` and return the extra
519
- * spawn args that bind them. Runs before every spawn (same place models.json
520
- * is materialized), so a continual agent dir is re-pinned to the current
521
- * profile on each fresh prime process.
561
+ * Write one agent-dir file atomically (temp + rename).
562
+ *
563
+ * Every file this lowering writes lands in a directory a LIVE agent may be
564
+ * reading: prime reloads models.json at model-resolution time and re-reads the
565
+ * harness store on every system-prompt build, so a partial write is observable.
566
+ * `mode` is applied to the temp file before the rename, so the final file is
567
+ * never briefly world-readable.
568
+ */
569
+ export declare function writePrimeAgentFile(path: string, data: string, mode?: number): void;
570
+ export interface PrimeMaterializeOptions {
571
+ /**
572
+ * Where the FLAG-NAMED files go — the prompt files and the skills root.
573
+ * Defaults to `agentDir`. An executor that does not own the agent dir points
574
+ * this at a per-run directory of its own, so binding a profile's prompts and
575
+ * skills writes nothing into a dir it must not modify. The context file and
576
+ * the harness store have no flag and always land in `agentDir`; refuse those
577
+ * with {@link primeProfileNeedsAgentDir} instead.
578
+ */
579
+ flagFileRoot?: string;
580
+ /**
581
+ * Reports state the executor should see but that does not fail the run
582
+ * (currently: adopting entries a pre-shared executor left behind).
583
+ */
584
+ warn?: (msg: string, meta?: Record<string, unknown>) => void;
585
+ }
586
+ /**
587
+ * Materialize the validated controls and return the extra spawn args that bind
588
+ * them. Runs before every spawn (same place models.json is materialized), so a
589
+ * continual agent dir is re-pinned to the current profile on each fresh prime
590
+ * process.
591
+ */
592
+ export declare function materializePrimeProfileControls(agentDir: string, controls: PrimeProfileControls, options?: PrimeMaterializeOptions): string[];
593
+ /**
594
+ * Remove the files a PREVIOUS materialization of this lowering wrote, so a
595
+ * profile that shrank cannot leave a stale instruction file or skill behind for
596
+ * the next run against the same dirs.
597
+ *
598
+ * Only the paths this lowering NAMES are removed. There is deliberately no
599
+ * manifest: a manifest would live where the agent can write it, and reading a
600
+ * delete list out of agent-writable state turns pruning into an arbitrary-path
601
+ * delete.
522
602
  */
523
- export declare function materializePrimeProfileControls(agentDir: string, controls: PrimeProfileControls, warn: (msg: string, meta?: Record<string, unknown>) => void): string[];
603
+ export declare function prunePrimeProfileMaterial(agentDir: string, options?: Pick<PrimeMaterializeOptions, "flagFileRoot">): void;
524
604
  /**
525
605
  * Upsert profile subagent specs into the fork's global continual-harness
526
606
  * store and remove profile-sourced entries the current profile no longer
527
- * declares. Agent-written entries (`source: "agent"`) are never touched.
607
+ * declares. Agent-written entries (`source: "agent"`) — and every top-level
608
+ * key this lowering does not manage — are preserved untouched.
528
609
  *
529
- * A pre-existing unparsable store degrades to empty exactly like the fork's
530
- * own loader (refinement.ts:290-300 — "a corrupt state file must degrade to
531
- * empty rather than throw"), with a warning so the reset is visible.
610
+ * A store that exists but no longer parses raises
611
+ * {@link PrimeHarnessStateError}: the file is the agent's own self-modification
612
+ * state, and overwriting it destroys what the run was supposed to observe.
532
613
  */
533
- export declare function seedPrimeProfileSubagents(agentDir: string, subagents: PrimeProfileSubagent[], warn: (msg: string, meta?: Record<string, unknown>) => void): void;
614
+ export declare function seedPrimeProfileSubagents(agentDir: string, subagents: PrimeProfileSubagent[], warn?: (msg: string, meta?: Record<string, unknown>) => void): void;
package/dist/index.js CHANGED
@@ -22,10 +22,13 @@
22
22
  * cli-bridge and agent-dev-container — this is the single source those re-export.
23
23
  */
24
24
  export { modelIdsMatch, qualifyModelId } from "./model-id.js";
25
+ export { matchPrimeKernelDeadMarker, PRIME_AGENT_DIR_ENV_VAR, PRIME_AGENT_DIR_ENV_VARS, PRIME_FORK_INSTALL, PRIME_FORK_PIN, PRIME_HARNESS_STATE_ENV_DENYLIST, PRIME_KERNEL_DEAD_MARKERS, PRIME_KERNEL_ENV_PASSTHROUGH, PRIME_MCP_UNSUPPORTED_REASON, PRIME_THINKING_LEVELS, PrimeHarnessStateError, PrimeProfileError, parsePrimeModelsJson, primeAgentDirEnv, primeApiKeyEnvNames, primeThinkingLevel, stripPrimeJsonComments, } from "./prime-fork.js";
25
26
  import { createHash, randomUUID } from "node:crypto";
26
- import { closeSync, existsSync, fchmodSync, constants as fsConstants, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
27
+ import { closeSync, existsSync, fchmodSync, constants as fsConstants, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
27
28
  import { basename, dirname, isAbsolute, join, relative, resolve, sep, win32, } from "node:path";
28
29
  import { agentCandidateProfileActivationSchema, agentCandidateProfilePlanEvidenceSchema, agentCandidateProfilePlanMaterialSchema, agentCandidateProfileSchema, agentProfileSchema, agentProfileSecretRefSchema, canonicalCandidateBytes, canonicalCandidateDigest, resolveNativeWebTools, } from "@tangle-network/agent-interface";
30
+ import { modelIdsMatch, qualifyModelId } from "./model-id.js";
31
+ import { PRIME_MCP_UNSUPPORTED_REASON, PrimeHarnessStateError, rejectPrimeControl, rejectPrimeDeclaration, } from "./prime-fork.js";
29
32
  export const UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER = Object.freeze({
30
33
  async get() {
31
34
  return undefined;
@@ -1192,7 +1195,15 @@ export function materializeCandidateProfile(profile, harness, opts = {}) {
1192
1195
  mcp: candidateMcpRecordForClassicMaterializer(candidate.mcp),
1193
1196
  hooks: hookLowering.hooks,
1194
1197
  };
1195
- const classicPlan = materializeProfile(classicProfile, harness);
1198
+ const classicPlan = materializeProfile(classicProfile, harness, {
1199
+ binds: opts.binds,
1200
+ });
1201
+ // Canonical candidate evidence has no field for a launcher-addressed binding.
1202
+ // Refuse that shape instead of returning a candidate plan whose receipt would
1203
+ // omit the exact control still owed by the launcher.
1204
+ if (classicPlan.systemPromptBinding !== undefined) {
1205
+ throw new Error(`Sealed candidate evidence cannot represent a system-prompt binding to ${classicPlan.systemPromptBinding.control}`);
1206
+ }
1196
1207
  if (hookLowering.unsupportedReason) {
1197
1208
  classicPlan.unsupported.push({
1198
1209
  dimension: "hooks",
@@ -3173,12 +3184,29 @@ function materializeCommand(h, name, body, add, unsupported) {
3173
3184
  // and the addition appends after it (system-prompt.ts:61,109-110). Values
3174
3185
  // are materialized as FILES under the agent dir because the flag value is
3175
3186
  // read as a file when the path exists (resource-loader.ts:41-56), which
3176
- // keeps literal-vs-path resolution deterministic.
3187
+ // keeps literal-vs-path resolution deterministic AND removes the argv size
3188
+ // ceiling an inline flag value would run into (Linux MAX_ARG_STRLEN).
3189
+ //
3190
+ // - `prompt.instructions` / `resources.instructions` → `<agentDir>/AGENTS.md`.
3191
+ // The agent dir IS an instruction surface: `loadProjectContextFiles`
3192
+ // (resource-loader.ts:76) reads the agent dir's own AGENTS.md/CLAUDE.md as
3193
+ // the GLOBAL context file (:86 `loadContextFileFromDir(resolvedAgentDir)`,
3194
+ // candidates at :59) BEFORE walking the cwd ancestors (:94-112), and the
3195
+ // result is loaded unless `--no-context-files` (:476). Writing it here
3196
+ // touches no caller workspace content, which is what the agent-dir lowering
3197
+ // exists to guarantee. (`materializeProfile(profile, "prime")` lowers the
3198
+ // same intent to the cwd AGENTS.md for callers that own the workspace
3199
+ // instead of the agent dir.)
3177
3200
  //
3178
3201
  // - `resources.skills` (inline refs) → `--skill <agentDir>/profile/skills`
3179
3202
  // (cli/args.ts:207-209 → main.ts:652 → agent-session-config.ts:67 →
3180
- // additionalSkillPaths, resource-loader.ts:437-442). The loader recurses
3181
- // into `<name>/SKILL.md` subdirs (skills.ts:328-372) and loaded skills are
3203
+ // additionalSkillPaths, resource-loader.ts:437-442). An explicit flag rather
3204
+ // than the agent dir's auto-discovered `skills/` tree (package-manager.ts:884
3205
+ // global base, :2171 skills dir, :2240-2246 discovery), which also loads but
3206
+ // goes through the fork's global-vs-project dedupe (package-manager.ts:880,
3207
+ // "project scope wins over global") — so an unrelated project-scope skill of
3208
+ // the same name silently shadows the profile's. The loader recurses into
3209
+ // `<name>/SKILL.md` subdirs (skills.ts:328-372) and loaded skills are
3182
3210
  // rendered into the system prompt (agent-session.ts:4272-4291,
3183
3211
  // system-prompt.ts:87-89). The fork silently DROPS a SKILL.md without a
3184
3212
  // frontmatter description (skills.ts:416-419), so content is normalized
@@ -3192,25 +3220,17 @@ function materializeCommand(h, name, body, add, unsupported) {
3192
3220
  // reaches the same store through the host-set RLM_GLOBAL_HARNESS_STATE_DIR
3193
3221
  // (agent-session.ts:8805).
3194
3222
  //
3195
- // Kept REJECTED with the fork evidence:
3223
+ // Kept REJECTED with the fork evidence — see {@link PRIME_MCP_UNSUPPORTED_REASON}
3224
+ // for `mcp`/`connections`, and the per-control reasons in
3225
+ // {@link readPrimeProfileControls} for permissions, tools, hooks, modes,
3226
+ // confidential, and the resource kinds with no agent-dir read path.
3196
3227
  //
3197
- // - `mcp`: the fork's MCP config surface (`mcpServers` in
3198
- // `<agentDir>/settings.json`, settings-manager.ts:98-120,219) IS read at
3199
- // startup, but a user-declared server never becomes model-visible: the host
3200
- // McpManager only registers OAuth providers and serves mcp.config/refresh
3201
- // host-requests (core/mcp/mcp-manager.ts:62-124,156-181), skips stdio
3202
- // servers entirely (mcp-manager.ts:73), and nothing generates a skill or
3203
- // prompt section for it — model-visible MCP is exactly the built-in
3204
- // linear/notion catalog skills (packages/coding-agent/skills/…). Wiring
3205
- // `profile.mcp` into settings.json would materialize config the model can
3206
- // never discover: a silently dropped control.
3207
- //
3208
- // - `prompt.instructions`: the fork's caller-instruction surface is the
3209
- // workspace AGENTS.md/CLAUDE.md context file (resource-loader.ts:58-66),
3210
- // i.e. caller workspace content this agent-dir lowering must not write
3211
- // into. (`materializeProfile(profile, "prime")` lowers instructions to the
3212
- // cwd AGENTS.md instead, which the fork walks from cwd:
3213
- // resource-loader.ts:94-109.)
3228
+ // The store is AGENT-OWNED: it is written by the agent mid-session and rides
3229
+ // every later system prompt. A store that exists but no longer parses
3230
+ // therefore fails the turn ({@link PrimeHarnessStateError}) rather than being
3231
+ // reseeded from empty — the fork's own loader degrades to empty in-process
3232
+ // (refinement.ts:290-300), but an executor that OVERWRITES the file makes that
3233
+ // degradation permanent and destroys the evidence of what happened.
3214
3234
  /** Skill name rules enforced by the fork's Agent Skills loader (skills.ts:122-147). */
3215
3235
  const PRIME_SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
3216
3236
  const PRIME_MAX_SKILL_NAME_LENGTH = 64;
@@ -3218,6 +3238,8 @@ const PRIME_PROFILE_DIR_NAME = "profile";
3218
3238
  const PRIME_SKILLS_DIR_NAME = "skills";
3219
3239
  const PRIME_SYSTEM_PROMPT_FILE = "system-prompt.md";
3220
3240
  const PRIME_APPEND_SYSTEM_PROMPT_FILE = "append-system-prompt.md";
3241
+ /** Agent-dir global context file the fork reads (resource-loader.ts:59,86). */
3242
+ const PRIME_CONTEXT_FILE = "AGENTS.md";
3221
3243
  /** Fork continual-harness store location under the agent dir (refinement.ts:269-279). */
3222
3244
  const PRIME_HARNESS_DIR_NAME = "harness";
3223
3245
  const PRIME_HARNESS_STATE_FILE = "harness_state.json";
@@ -3228,22 +3250,101 @@ const PRIME_HARNESS_KINDS = ["prompt", "memory", "skill", "subagent"];
3228
3250
  * touched.
3229
3251
  */
3230
3252
  const PRIME_PROFILE_ENTRY_SOURCE = "profile";
3231
- function rejectPrimeControl(control, reason) {
3232
- throw new Error(`prime does not support AgentProfile ${control}: ${reason}`);
3253
+ /**
3254
+ * Provenance markers written by executors that predate this shared lowering.
3255
+ *
3256
+ * Pruning keys on the source tag, so an executor that switches to the shared
3257
+ * implementation would otherwise leave its own earlier entries behind forever:
3258
+ * they match no current profile and no current source, and the model's
3259
+ * spawnable roster silently accumulates duplicates of every subagent. They are
3260
+ * adopted (pruned or replaced) on the first shared run instead.
3261
+ */
3262
+ const PRIME_LEGACY_PROFILE_ENTRY_SOURCES = [
3263
+ "cli-bridge-profile",
3264
+ ];
3265
+ /** True when the controls materialize anything at all. */
3266
+ export function hasPrimeProfileMaterial(controls) {
3267
+ return (controls.systemPrompt !== undefined ||
3268
+ controls.appendSystemPrompt !== undefined ||
3269
+ primeProfileNeedsAgentDir(controls) ||
3270
+ controls.skills.length > 0);
3271
+ }
3272
+ /**
3273
+ * True when the controls must WRITE INTO the agent dir itself.
3274
+ *
3275
+ * Prompt files and skill dirs are named by a flag, so they can live anywhere
3276
+ * the agent can read (`--system-prompt` / `--append-system-prompt` resolve a
3277
+ * path when it exists, fork resource-loader.ts:41-56; `--skill` takes an
3278
+ * arbitrary dir, :437-442). The context file and the continual-harness store
3279
+ * are located BY the agent dir and have no flag, so they cannot move.
3280
+ *
3281
+ * An executor pointed at an agent dir it does not own (an operator's shared
3282
+ * dir) uses this to refuse exactly the controls it would have to write there,
3283
+ * while still binding the ones it can materialize elsewhere.
3284
+ */
3285
+ export function primeProfileNeedsAgentDir(controls) {
3286
+ return controls.instructions !== undefined || controls.subagents.length > 0;
3287
+ }
3288
+ /**
3289
+ * Read one instruction block, which the profile may carry inline or as a
3290
+ * resource ref. Only inline refs are accepted: resolving a GitHub-backed ref
3291
+ * fetches remote content, which is `resolveAgentProfileResources`' job and
3292
+ * must happen before a profile reaches an executor.
3293
+ */
3294
+ function primeInstructionBlock(value, control) {
3295
+ if (typeof value === "string")
3296
+ return value;
3297
+ if (value.kind === "inline")
3298
+ return value.content;
3299
+ rejectPrimeControl(control, "only inline resource refs are accepted — resolve GitHub-backed refs " +
3300
+ "(resolveAgentProfileResources) before handing the profile to this lowering.");
3233
3301
  }
3234
3302
  /**
3235
3303
  * Validate and extract the profile controls the prime agent-dir lowering
3236
- * materializes. Throws on any control (or control facet) the fork has no
3237
- * verified read path for — partial acceptance would silently drop the rest.
3304
+ * materializes. Throws {@link PrimeProfileError} on any control (or control
3305
+ * facet) the fork has no verified read path for — partial acceptance would
3306
+ * silently drop the rest.
3238
3307
  */
3239
3308
  export function readPrimeProfileControls(profile) {
3240
3309
  const controls = { skills: [], subagents: [] };
3241
3310
  if (!profile)
3242
3311
  return controls;
3243
3312
  if (profile.harness !== undefined && profile.harness !== "prime") {
3244
- throw new Error(`prime cannot run a profile pinned to harness "${profile.harness}" — ` +
3313
+ rejectPrimeDeclaration("harness", `prime cannot run a profile pinned to harness "${profile.harness}" — ` +
3245
3314
  'only "prime" (or an unset harness preference) is accepted.');
3246
3315
  }
3316
+ // An entry with `enabled: false` is OFF, not requested. A fully disabled map
3317
+ // asks for nothing, so accepting it drops no control — which is the only
3318
+ // thing this refusal exists to prevent.
3319
+ const requestedMcp = Object.keys(activeMcpServers(profile.mcp));
3320
+ if (requestedMcp.length > 0) {
3321
+ rejectPrimeControl(`mcp: ${requestedMcp.join(", ")}`, PRIME_MCP_UNSUPPORTED_REASON);
3322
+ }
3323
+ if (profile.connections !== undefined) {
3324
+ rejectPrimeControl("connections", `a hub connection grant resolves to MCP tools, and ${PRIME_MCP_UNSUPPORTED_REASON}`);
3325
+ }
3326
+ if (profile.permissions !== undefined) {
3327
+ rejectPrimeControl("permissions", "prime-agent exposes no permission control at all (full flag scan of fork " +
3328
+ "cli/args.ts:93-295; native tools run unconditionally), so a permission map " +
3329
+ "would be accepted and never enforced.");
3330
+ }
3331
+ if (profile.tools !== undefined) {
3332
+ rejectPrimeControl("tools", "the only per-run tool control is `--tools` over the single builtin `ipython` " +
3333
+ "(fork cli/args.ts:63-64,156-167); bash/edit and the rest are extension-provided " +
3334
+ "and not addressable per run, so a harness-neutral tool map cannot bind to it.");
3335
+ }
3336
+ if (profile.hooks !== undefined) {
3337
+ rejectPrimeControl("hooks", "prime-agent has no hook mechanism (fork cli/args.ts flag scan; extensions are " +
3338
+ "host-trusted JS modules loaded into the agent process, not caller command hooks).");
3339
+ }
3340
+ if (profile.modes !== undefined) {
3341
+ rejectPrimeControl("modes", "prime-agent has no mode concept (fork cli/args.ts flag scan).");
3342
+ }
3343
+ if (profile.confidential !== undefined) {
3344
+ rejectPrimeControl("confidential", "confidential execution is a property of the executor's isolation boundary, not " +
3345
+ "of the agent dir; this lowering cannot establish it.");
3346
+ }
3347
+ const instructionBlocks = [];
3247
3348
  if (profile.prompt) {
3248
3349
  const { systemPrompt, appendSystemPrompt, instructions } = profile.prompt;
3249
3350
  const knownPromptFields = new Set([
@@ -3257,15 +3358,9 @@ export function readPrimeProfileControls(profile) {
3257
3358
  if (unknown.length > 0) {
3258
3359
  rejectPrimeControl(`prompt fields: ${unknown.join(", ")}`, "no verified prime-agent read path exists for them.");
3259
3360
  }
3260
- if (instructions !== undefined) {
3261
- rejectPrimeControl("prompt.instructions", "prime-agent's caller-instruction surface is the workspace " +
3262
- "AGENTS.md context file (fork resource-loader.ts:58-66), which is " +
3263
- "caller workspace content this adapter must not write into. Use " +
3264
- "prompt.appendSystemPrompt instead.");
3265
- }
3266
3361
  if (systemPrompt !== undefined) {
3267
3362
  if (typeof systemPrompt !== "string" || systemPrompt.trim() === "") {
3268
- rejectPrimeControl("prompt.systemPrompt", "an empty replacement would delete the built-in system prompt and " +
3363
+ rejectPrimeDeclaration("prompt.systemPrompt", "an empty replacement would delete the built-in system prompt and " +
3269
3364
  "install nothing.");
3270
3365
  }
3271
3366
  controls.systemPrompt = systemPrompt;
@@ -3273,52 +3368,69 @@ export function readPrimeProfileControls(profile) {
3273
3368
  if (appendSystemPrompt !== undefined) {
3274
3369
  if (typeof appendSystemPrompt !== "string" ||
3275
3370
  appendSystemPrompt.trim() === "") {
3276
- rejectPrimeControl("prompt.appendSystemPrompt", "must be non-empty text.");
3371
+ rejectPrimeDeclaration("prompt.appendSystemPrompt", "must be non-empty text.");
3277
3372
  }
3278
3373
  controls.appendSystemPrompt = appendSystemPrompt;
3279
3374
  }
3375
+ for (const block of instructions ?? []) {
3376
+ if (typeof block === "string" && block.trim() !== "")
3377
+ instructionBlocks.push(block);
3378
+ }
3280
3379
  }
3281
3380
  if (profile.resources) {
3282
- const { skills, failOnError, ...unsupported } = profile.resources;
3381
+ const { skills, instructions, failOnError, ...unsupported } = profile.resources;
3283
3382
  const rejectedKeys = Object.entries(unsupported)
3284
3383
  .filter(([, value]) => value !== undefined)
3285
3384
  .map(([key]) => key);
3286
3385
  if (rejectedKeys.length > 0) {
3287
- rejectPrimeControl(`resources: ${rejectedKeys.join(", ")}`, "only resources.skills has a verified prime-agent read path " +
3288
- "(--skill dirs, fork resource-loader.ts:437-442).");
3386
+ rejectPrimeControl(`resources: ${rejectedKeys.join(", ")}`, "only resources.skills (--skill dirs, fork resource-loader.ts:437-442) and " +
3387
+ "resources.instructions (agent-dir AGENTS.md, resource-loader.ts:86) have a " +
3388
+ "verified prime-agent read path. Generic workspace files, tool files, " +
3389
+ "subagent-definition files and prompt templates either have no headless read " +
3390
+ "path or would run caller-authored code inside the agent process.");
3289
3391
  }
3290
3392
  if (failOnError === false) {
3291
- rejectPrimeControl("resources.failOnError: false", "this adapter always fails loud on a resource it cannot " +
3393
+ rejectPrimeControl("resources.failOnError: false", "this lowering always fails loud on a resource it cannot " +
3292
3394
  "materialize; lenient materialization is not implemented.");
3293
3395
  }
3396
+ if (instructions !== undefined) {
3397
+ const block = primeInstructionBlock(instructions, "resources.instructions");
3398
+ if (block.trim() !== "")
3399
+ instructionBlocks.push(block);
3400
+ }
3294
3401
  const seen = new Set();
3295
- for (const ref of skills ?? []) {
3402
+ for (const [index, ref] of (skills ?? []).entries()) {
3296
3403
  if (ref.kind !== "inline") {
3297
- rejectPrimeControl(`resources.skills ref "${"path" in ref ? ref.path : ""}"`, "only inline skill refs are accepted — resolve GitHub refs " +
3404
+ rejectPrimeControl(`resources.skills[${index}]`, "only inline skill refs are accepted — resolve GitHub-backed refs " +
3298
3405
  "(resolveAgentProfileResources) before handing the profile to " +
3299
- "this adapter.");
3406
+ "this lowering.");
3300
3407
  }
3301
3408
  const name = ref.name;
3302
3409
  if (!PRIME_SKILL_NAME_RE.test(name) ||
3303
3410
  name.length > PRIME_MAX_SKILL_NAME_LENGTH) {
3304
- rejectPrimeControl(`resources.skills name "${name}"`, "prime-agent skill names must be lowercase a-z0-9 with single " +
3305
- `hyphens, at most ${PRIME_MAX_SKILL_NAME_LENGTH} chars (fork skills.ts:122-147).`);
3411
+ rejectPrimeDeclaration(`resources.skills[${index}]`, `name ${JSON.stringify(name)} is not a valid prime skill name — it becomes the ` +
3412
+ "skill directory name, so it must be lowercase a-z0-9 with single hyphens, at " +
3413
+ `most ${PRIME_MAX_SKILL_NAME_LENGTH} chars (fork skills.ts:122-147).`);
3306
3414
  }
3307
3415
  if (seen.has(name)) {
3308
- rejectPrimeControl(`resources.skills name "${name}"`, "duplicate skill names collide in the fork's skill registry.");
3416
+ rejectPrimeDeclaration("resources.skills", `declares ${JSON.stringify(name)} twice; duplicate skill names collide in the ` +
3417
+ "fork's skill registry, which keeps only the first and warns.");
3309
3418
  }
3310
3419
  seen.add(name);
3311
3420
  if (ref.content.trim() === "") {
3312
- rejectPrimeControl(`resources.skills "${name}"`, "skill content is empty.");
3421
+ rejectPrimeDeclaration(`resources.skills[${index}]`, `skill ${JSON.stringify(name)} declares no content.`);
3313
3422
  }
3314
3423
  controls.skills.push({ name, content: ref.content });
3315
3424
  }
3316
3425
  }
3426
+ if (instructionBlocks.length > 0) {
3427
+ controls.instructions = instructionBlocks.join("\n\n");
3428
+ }
3317
3429
  if (profile.subagents) {
3318
3430
  const usedIds = new Map();
3319
3431
  for (const [name, spec] of Object.entries(profile.subagents)) {
3320
3432
  if (name.trim() === "") {
3321
- rejectPrimeControl("subagents", "a subagent name must be non-empty.");
3433
+ rejectPrimeDeclaration("subagents", "a subagent name must be non-empty.");
3322
3434
  }
3323
3435
  const inexpressible = [
3324
3436
  ["model", spec.model],
@@ -3329,19 +3441,20 @@ export function readPrimeProfileControls(profile) {
3329
3441
  .filter(([, value]) => value !== undefined)
3330
3442
  .map(([key]) => key);
3331
3443
  if (inexpressible.length > 0) {
3332
- rejectPrimeControl(`subagents["${name}"] fields: ${inexpressible.join(", ")}`, "a prime continual-harness subagent spec is prompt-roster " +
3444
+ rejectPrimeControl(`subagents[${JSON.stringify(name)}] fields: ${inexpressible.join(", ")}`, "a prime continual-harness subagent spec is prompt-roster " +
3333
3445
  "guidance for `await rlm('<task>')` (fork refinement.ts:483-496) " +
3334
3446
  "and pins no model, tool set, permission, or step budget on the " +
3335
3447
  "spawned child.");
3336
3448
  }
3337
3449
  if (!spec.description?.trim() && !spec.prompt?.trim()) {
3338
- rejectPrimeControl(`subagents["${name}"]`, "a spec needs a description and/or prompt — an empty entry gives " +
3339
- "the model an empty roster line.");
3450
+ rejectPrimeDeclaration(`subagents[${JSON.stringify(name)}]`, "declares no behavior — a spec needs a description and/or prompt, since an " +
3451
+ "empty entry gives the model an empty roster line.");
3340
3452
  }
3341
3453
  const id = primeSubagentEntryId(name);
3342
3454
  const clash = usedIds.get(id);
3343
3455
  if (clash !== undefined) {
3344
- rejectPrimeControl(`subagents["${name}"]`, `its id "${id}" collides with subagent "${clash}".`);
3456
+ rejectPrimeDeclaration(`subagents[${JSON.stringify(name)}]`, `collides with subagent ${JSON.stringify(clash)} on harness-state id ` +
3457
+ `${JSON.stringify(id)}.`);
3345
3458
  }
3346
3459
  usedIds.set(id, name);
3347
3460
  controls.subagents.push({
@@ -3354,6 +3467,39 @@ export function readPrimeProfileControls(profile) {
3354
3467
  }
3355
3468
  return controls;
3356
3469
  }
3470
+ /**
3471
+ * Refuse a run whose selected model disagrees with the profile's own.
3472
+ *
3473
+ * One authority per run: a profile names the model it was written and scored
3474
+ * against, so running it on a different one silently scores a different agent.
3475
+ * Both sides go through {@link modelIdsMatch}, so the qualification level a
3476
+ * caller happened to use is not a conflict.
3477
+ *
3478
+ * `provider` is the executor's PROVIDER SELECTION, and is compared only when
3479
+ * the executor's provider is part of model identity. An executor whose
3480
+ * provider key is merely a namespace inside the agent dir passes it as
3481
+ * undefined and gets the model-name comparison alone.
3482
+ */
3483
+ export function assertPrimeModelAgreement(profile, selected) {
3484
+ const declared = profile?.model?.default;
3485
+ const declaredProvider = profile?.model?.provider;
3486
+ if (declared !== undefined) {
3487
+ const selectedId = qualifyModelId(selected.provider, selected.model);
3488
+ const declaredId = qualifyModelId(declaredProvider, declared);
3489
+ if (!modelIdsMatch(selectedId, declaredId, selected.provider)) {
3490
+ rejectPrimeDeclaration("model.default", `the run selects ${JSON.stringify(selectedId)} but the profile names ` +
3491
+ `${JSON.stringify(declaredId)}; a profile must not be scored against a model it ` +
3492
+ "does not name.");
3493
+ }
3494
+ return;
3495
+ }
3496
+ if (selected.provider !== undefined &&
3497
+ declaredProvider !== undefined &&
3498
+ declaredProvider !== selected.provider) {
3499
+ rejectPrimeDeclaration("model.provider", `the run selects provider ${JSON.stringify(selected.provider)} but the profile names ` +
3500
+ `${JSON.stringify(declaredProvider)}.`);
3501
+ }
3502
+ }
3357
3503
  /** Deterministic harness-state entry id for a profile subagent name. */
3358
3504
  export function primeSubagentEntryId(name) {
3359
3505
  const slug = name
@@ -3363,96 +3509,127 @@ export function primeSubagentEntryId(name) {
3363
3509
  return `profile-${slug || "subagent"}`;
3364
3510
  }
3365
3511
  /**
3366
- * Materialize the validated controls into `agentDir` and return the extra
3367
- * spawn args that bind them. Runs before every spawn (same place models.json
3368
- * is materialized), so a continual agent dir is re-pinned to the current
3369
- * profile on each fresh prime process.
3512
+ * Write one agent-dir file atomically (temp + rename).
3513
+ *
3514
+ * Every file this lowering writes lands in a directory a LIVE agent may be
3515
+ * reading: prime reloads models.json at model-resolution time and re-reads the
3516
+ * harness store on every system-prompt build, so a partial write is observable.
3517
+ * `mode` is applied to the temp file before the rename, so the final file is
3518
+ * never briefly world-readable.
3370
3519
  */
3371
- export function materializePrimeProfileControls(agentDir, controls, warn) {
3520
+ export function writePrimeAgentFile(path, data, mode = 0o600) {
3521
+ mkdirSync(dirname(path), { recursive: true });
3522
+ const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
3523
+ writeFileSync(temp, data, { mode });
3524
+ renameSync(temp, path);
3525
+ }
3526
+ /**
3527
+ * Materialize the validated controls and return the extra spawn args that bind
3528
+ * them. Runs before every spawn (same place models.json is materialized), so a
3529
+ * continual agent dir is re-pinned to the current profile on each fresh prime
3530
+ * process.
3531
+ */
3532
+ export function materializePrimeProfileControls(agentDir, controls, options = {}) {
3533
+ const { flagFileRoot = agentDir, warn } = options;
3372
3534
  const args = [];
3373
- const profileDir = join(agentDir, PRIME_PROFILE_DIR_NAME);
3535
+ const profileDir = join(flagFileRoot, PRIME_PROFILE_DIR_NAME);
3374
3536
  if (controls.systemPrompt !== undefined) {
3375
- mkdirSync(profileDir, { recursive: true });
3376
3537
  const promptPath = join(profileDir, PRIME_SYSTEM_PROMPT_FILE);
3377
- writeFileSync(promptPath, controls.systemPrompt, "utf8");
3538
+ writePrimeAgentFile(promptPath, controls.systemPrompt);
3378
3539
  args.push("--system-prompt", promptPath);
3379
3540
  }
3380
3541
  if (controls.appendSystemPrompt !== undefined) {
3381
- mkdirSync(profileDir, { recursive: true });
3382
3542
  const appendPath = join(profileDir, PRIME_APPEND_SYSTEM_PROMPT_FILE);
3383
- writeFileSync(appendPath, controls.appendSystemPrompt, "utf8");
3543
+ writePrimeAgentFile(appendPath, controls.appendSystemPrompt);
3384
3544
  args.push("--append-system-prompt", appendPath);
3385
3545
  }
3546
+ if (controls.instructions !== undefined) {
3547
+ writePrimeAgentFile(join(agentDir, PRIME_CONTEXT_FILE), `${controls.instructions}\n`);
3548
+ }
3386
3549
  if (controls.skills.length > 0) {
3387
3550
  const skillsRoot = join(profileDir, PRIME_SKILLS_DIR_NAME);
3388
3551
  for (const skill of controls.skills) {
3389
- const skillDir = join(skillsRoot, skill.name);
3390
- mkdirSync(skillDir, { recursive: true });
3391
- writeFileSync(join(skillDir, "SKILL.md"), normalizeSkillMd(skill.name, skill.content), "utf8");
3552
+ writePrimeAgentFile(join(skillsRoot, skill.name, "SKILL.md"), normalizeSkillMd(skill.name, skill.content));
3392
3553
  }
3393
3554
  args.push("--skill", skillsRoot);
3394
3555
  }
3395
3556
  seedPrimeProfileSubagents(agentDir, controls.subagents, warn);
3396
3557
  return args;
3397
3558
  }
3398
- function emptyPrimeHarnessState() {
3399
- const entries = {};
3400
- for (const kind of PRIME_HARNESS_KINDS)
3401
- entries[kind] = {};
3402
- return { schema: 1, entries, refinements: [] };
3559
+ /**
3560
+ * Remove the files a PREVIOUS materialization of this lowering wrote, so a
3561
+ * profile that shrank cannot leave a stale instruction file or skill behind for
3562
+ * the next run against the same dirs.
3563
+ *
3564
+ * Only the paths this lowering NAMES are removed. There is deliberately no
3565
+ * manifest: a manifest would live where the agent can write it, and reading a
3566
+ * delete list out of agent-writable state turns pruning into an arbitrary-path
3567
+ * delete.
3568
+ */
3569
+ export function prunePrimeProfileMaterial(agentDir, options = {}) {
3570
+ const { flagFileRoot = agentDir } = options;
3571
+ rmSync(join(flagFileRoot, PRIME_PROFILE_DIR_NAME), {
3572
+ recursive: true,
3573
+ force: true,
3574
+ });
3575
+ rmSync(join(agentDir, PRIME_CONTEXT_FILE), { force: true });
3403
3576
  }
3404
3577
  /**
3405
3578
  * Upsert profile subagent specs into the fork's global continual-harness
3406
3579
  * store and remove profile-sourced entries the current profile no longer
3407
- * declares. Agent-written entries (`source: "agent"`) are never touched.
3580
+ * declares. Agent-written entries (`source: "agent"`) — and every top-level
3581
+ * key this lowering does not manage — are preserved untouched.
3408
3582
  *
3409
- * A pre-existing unparsable store degrades to empty exactly like the fork's
3410
- * own loader (refinement.ts:290-300 — "a corrupt state file must degrade to
3411
- * empty rather than throw"), with a warning so the reset is visible.
3583
+ * A store that exists but no longer parses raises
3584
+ * {@link PrimeHarnessStateError}: the file is the agent's own self-modification
3585
+ * state, and overwriting it destroys what the run was supposed to observe.
3412
3586
  */
3413
3587
  export function seedPrimeProfileSubagents(agentDir, subagents, warn) {
3414
3588
  const statePath = join(agentDir, PRIME_HARNESS_DIR_NAME, PRIME_HARNESS_STATE_FILE);
3415
- let state = emptyPrimeHarnessState();
3589
+ let state = {};
3416
3590
  if (existsSync(statePath)) {
3591
+ let parsed;
3417
3592
  try {
3418
- const raw = JSON.parse(readFileSync(statePath, "utf8"));
3419
- if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
3420
- const parsed = raw;
3421
- state.schema =
3422
- typeof parsed.schema === "number" ? parsed.schema : state.schema;
3423
- for (const kind of PRIME_HARNESS_KINDS) {
3424
- const records = parsed.entries?.[kind];
3425
- if (records && typeof records === "object") {
3426
- state.entries[kind] = { ...records };
3427
- }
3428
- }
3429
- if (Array.isArray(parsed.refinements)) {
3430
- state.refinements = parsed.refinements;
3431
- }
3432
- }
3433
- else {
3434
- warn("prime: existing harness_state.json is not an object — reseeding", {
3435
- statePath,
3436
- });
3437
- }
3593
+ parsed = JSON.parse(readFileSync(statePath, "utf8"));
3438
3594
  }
3439
3595
  catch (err) {
3440
- warn("prime: existing harness_state.json is unparsable — reseeding", {
3441
- statePath,
3442
- error: err instanceof Error ? err.message : String(err),
3443
- });
3444
- state = emptyPrimeHarnessState();
3596
+ throw new PrimeHarnessStateError(statePath, err instanceof Error ? err.message : String(err));
3597
+ }
3598
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3599
+ throw new PrimeHarnessStateError(statePath, "the store is not a JSON object");
3445
3600
  }
3601
+ state = parsed;
3446
3602
  }
3447
3603
  else if (subagents.length === 0) {
3448
3604
  // Nothing to pin and nothing stale to remove.
3449
3605
  return;
3450
3606
  }
3451
- const existing = state.entries.subagent ?? {};
3607
+ const rawEntries = state.entries;
3608
+ const entries = rawEntries && typeof rawEntries === "object" && !Array.isArray(rawEntries)
3609
+ ? rawEntries
3610
+ : {};
3611
+ for (const kind of PRIME_HARNESS_KINDS) {
3612
+ const records = entries[kind];
3613
+ if (!records || typeof records !== "object" || Array.isArray(records)) {
3614
+ entries[kind] = {};
3615
+ }
3616
+ }
3617
+ const existing = entries.subagent;
3452
3618
  const desiredIds = new Set(subagents.map((s) => primeSubagentEntryId(s.name)));
3453
3619
  let changed = false;
3454
3620
  for (const [id, entry] of Object.entries(existing)) {
3455
- if (entry?.source === PRIME_PROFILE_ENTRY_SOURCE && !desiredIds.has(id)) {
3621
+ const source = entry?.source;
3622
+ const isLegacy = typeof source === "string" &&
3623
+ PRIME_LEGACY_PROFILE_ENTRY_SOURCES.includes(source);
3624
+ if (isLegacy) {
3625
+ warn?.("prime: adopting a subagent entry left by a pre-shared executor", {
3626
+ statePath,
3627
+ id,
3628
+ source,
3629
+ });
3630
+ }
3631
+ if ((source === PRIME_PROFILE_ENTRY_SOURCE || isLegacy) &&
3632
+ !desiredIds.has(id)) {
3456
3633
  delete existing[id];
3457
3634
  changed = true;
3458
3635
  }
@@ -3475,7 +3652,7 @@ export function seedPrimeProfileSubagents(agentDir, subagents, warn) {
3475
3652
  kind: "subagent",
3476
3653
  title: spec.name,
3477
3654
  content,
3478
- path: "profile",
3655
+ path: PRIME_PROFILE_ENTRY_SOURCE,
3479
3656
  scope: "global",
3480
3657
  reference: {},
3481
3658
  arguments: {},
@@ -3489,9 +3666,12 @@ export function seedPrimeProfileSubagents(agentDir, subagents, warn) {
3489
3666
  };
3490
3667
  changed = true;
3491
3668
  }
3492
- state.entries.subagent = existing;
3493
3669
  if (!changed)
3494
3670
  return;
3495
- mkdirSync(join(agentDir, PRIME_HARNESS_DIR_NAME), { recursive: true });
3496
- writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
3671
+ writePrimeAgentFile(statePath, `${JSON.stringify({
3672
+ ...state,
3673
+ schema: typeof state.schema === "number" ? state.schema : 1,
3674
+ entries,
3675
+ refinements: Array.isArray(state.refinements) ? state.refinements : [],
3676
+ }, null, 2)}\n`);
3497
3677
  }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Fork FACTS for prime-agent (PrimeIntellect-ai/prime-agent) — the constants,
3
+ * dialects and refusal reasons every executor that drives the fork needs, with
4
+ * no `node:fs` and no executor assumptions. The agent-dir MATERIALIZATION that
5
+ * consumes them lives in `./index.ts`; both consumers (cli-bridge's host-spawn
6
+ * backend on `--mode rpc`, sdk-provider-prime's in-sandbox adapter on
7
+ * `--mode acp`) read these instead of each keeping a private copy, because a
8
+ * fork bump must move one file, not two repositories.
9
+ *
10
+ * Every value here is verified against the pinned clone named by
11
+ * {@link PRIME_FORK_PIN}; `tests/prime-fork-pin.test.ts` re-asserts them
12
+ * against that clone when it is present, so a fork bump fails loudly in one
13
+ * place.
14
+ */
15
+ /** The prime-agent clone every constant in this module was read from. */
16
+ export declare const PRIME_FORK_PIN: {
17
+ readonly repo: "PrimeIntellect-ai/prime-agent";
18
+ readonly version: "0.7.0";
19
+ readonly commit: "be9e2fa0";
20
+ /** Local clone used by the fork-pin test when it exists. */
21
+ readonly clonePath: "/home/drew/bench-cache/prime-agent";
22
+ /** Package root inside the clone that the file:line citations are relative to. */
23
+ readonly packageRoot: "packages/coding-agent/src";
24
+ };
25
+ /**
26
+ * The fork's package identity, and why it cannot be installed from the npm
27
+ * registry.
28
+ *
29
+ * `@earendil-works/pi-coding-agent` on registry.npmjs.org serves the UPSTREAM
30
+ * pi line (0.84.x); the fork's 0.7.x versions were never published there. The
31
+ * two are wire-incompatible — different daemon schema revision, different
32
+ * config dir, different agent-dir env var (see
33
+ * {@link PRIME_AGENT_DIR_ENV_VARS}) — so `npm install -g` under that name
34
+ * yields an agent none of this lowering was verified against. The fork must be
35
+ * built from source and exposed under a `prime-agent` command name; its own
36
+ * `bin` entry is `pi` (fork package.json `bin`), which is exactly how the wrong
37
+ * binary ends up on `PATH` under the right-looking name.
38
+ */
39
+ export declare const PRIME_FORK_INSTALL: {
40
+ readonly npmName: "@earendil-works/pi-coding-agent";
41
+ /** The fork's own bin name — deliberately NOT the command to install it as. */
42
+ readonly forkBinName: "pi";
43
+ /** Command name an executor's binary path is expected to resolve to. */
44
+ readonly expectedCommandName: "prime-agent";
45
+ readonly sourceRepo: "https://github.com/PrimeIntellect-ai/prime-agent";
46
+ /** Built entrypoint to expose as `prime-agent`. */
47
+ readonly sourceEntrypoint: "packages/coding-agent/dist/bundle/cli.js";
48
+ readonly reason: string;
49
+ };
50
+ /**
51
+ * Env vars the agent resolves its config dir from, most-specific first.
52
+ *
53
+ * The fork derives the name from its own package identity —
54
+ * `ENV_AGENT_DIR = \`${envPrefix}_CODING_AGENT_DIR\`` where `envPrefix` comes
55
+ * from `package.json` `piConfig.name` (config.ts:485-498) — so the fork reads
56
+ * `PRIME_AGENT_CODING_AGENT_DIR` and upstream pi reads `PI_CODING_AGENT_DIR`.
57
+ * An executor that sets only the fork's name has its isolation SILENTLY
58
+ * disabled when the binary on PATH turns out to be upstream pi: `getAgentDir()`
59
+ * falls back to `homedir()/<CONFIG_DIR_NAME>` (config.ts:520-527), one shared
60
+ * dir — and therefore one shared self-modifying `harness_state.json` — for
61
+ * every session on the box. Setting BOTH names costs nothing and keeps the
62
+ * isolation fail-closed under either binary.
63
+ */
64
+ export declare const PRIME_AGENT_DIR_ENV_VARS: readonly ["PRIME_AGENT_CODING_AGENT_DIR", "PI_CODING_AGENT_DIR"];
65
+ /** Canonical agent-dir env var for the fork itself (config.ts:498). */
66
+ export declare const PRIME_AGENT_DIR_ENV_VAR: "PRIME_AGENT_CODING_AGENT_DIR";
67
+ /** Pin `agentDir` under every name the agent could resolve its config dir from. */
68
+ export declare function primeAgentDirEnv(agentDir: string): Record<string, string>;
69
+ /**
70
+ * Operator knobs an executor forwards into the agent when they are set.
71
+ *
72
+ * `PRIME_AGENT_KERNEL_VENV` is load-bearing for any executor that also pins
73
+ * HOME: the kernel venv defaults under `homedir()/.prime/agent/kernel-venv`
74
+ * (kernel/bootstrap.ts:331-342), so a fresh HOME re-bootstraps it through uv on
75
+ * every run unless the operator's venv path is forwarded
76
+ * (bootstrap.ts:340 override, :358 branch).
77
+ */
78
+ export declare const PRIME_KERNEL_ENV_PASSTHROUGH: readonly ["PRIME_AGENT_INSTALL_UV", "PRIME_AGENT_KERNEL_PYTHON", "PRIME_AGENT_KERNEL_VENV"];
79
+ /**
80
+ * Env vars an executor must NEVER forward from its own environment.
81
+ *
82
+ * These re-point the self-modifying continual-harness store OUTSIDE the agent
83
+ * dir (agent-session.ts:8805 hands the kernel its store location), which
84
+ * defeats per-session isolation without changing anything an executor inspects.
85
+ * Isolation is the fail-closed default; sharing state is an explicit opt-in
86
+ * that names a directory, never an inherited env var.
87
+ */
88
+ export declare const PRIME_HARNESS_STATE_ENV_DENYLIST: readonly ["RLM_GLOBAL_HARNESS_STATE_DIR", "RLM_HARNESS_STATE_DIR"];
89
+ /**
90
+ * A prime-agent IPython kernel that dies is never re-provisioned headlessly —
91
+ * every later `ipython` call fails with one of these markers until the process
92
+ * is replaced. An executor that cannot recognize them either reports a generic
93
+ * turn failure or burns its whole timeout against a permanently dead tool.
94
+ *
95
+ * Verified in `core/kernel/index.ts`: :723 ("forked kernel exited
96
+ * unexpectedly"), :750 ("Kernel exited before resolving ports"), :779 ("Kernel
97
+ * exited during startup"), :820/:837/:1172/:1295 ("Kernel has been shut down").
98
+ */
99
+ export declare const PRIME_KERNEL_DEAD_MARKERS: readonly string[];
100
+ /** The first kernel-death marker present in `text`, or undefined. */
101
+ export declare function matchPrimeKernelDeadMarker(text: string): string | undefined;
102
+ /**
103
+ * `--thinking` rungs the fork's argument parser accepts
104
+ * (cli/args.ts:62 `VALID_THINKING_LEVELS`, rejected at :175). Read from the
105
+ * fork itself rather than borrowed from upstream pi's measured table: the two
106
+ * agreeing today is a coincidence this module must not depend on.
107
+ */
108
+ export declare const PRIME_THINKING_LEVELS: readonly ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
109
+ export type PrimeThinkingLevel = (typeof PRIME_THINKING_LEVELS)[number];
110
+ /**
111
+ * Map the portable reasoning ladder onto the fork's `--thinking` value.
112
+ *
113
+ * Throws on an effort with no rung instead of returning undefined: omitting the
114
+ * flag runs the agent at its default thinking level while the caller believes
115
+ * the requested effort is in force, which is a silently dropped control.
116
+ */
117
+ export declare function primeThinkingLevel(effort: string | null | undefined): PrimeThinkingLevel | undefined;
118
+ /**
119
+ * Why `profile.mcp` (and any hub connection that resolves to MCP) is refused.
120
+ *
121
+ * The fork's `mcpServers` settings surface IS read at startup
122
+ * (settings-manager.ts:98-120,219) but a user-declared server never becomes
123
+ * model-visible: the host McpManager registers OAuth providers and serves
124
+ * mcp.config/refresh host-requests only (core/mcp/mcp-manager.ts:62-124),
125
+ * skips stdio servers entirely (mcp-manager.ts:73), and nothing generates a
126
+ * skill or prompt section for one. Model-visible MCP is exactly the built-in
127
+ * catalog skills, which are hand-written Python `McpIntegration` subclasses
128
+ * (prime-agent-runtime/src/rlm/mcp_base.py:112-125; only linear and notion
129
+ * ship). Materializing a per-run entry would run the agent WITHOUT the
130
+ * requested tools while the config on disk says otherwise.
131
+ */
132
+ export declare const PRIME_MCP_UNSUPPORTED_REASON: string;
133
+ /** Which half of a profile refusal this is: a fork limit, or a bad declaration. */
134
+ export type PrimeProfileErrorKind = "unsupported" | "invalid";
135
+ /**
136
+ * Refusal raised by the shared prime lowering, carrying the control and the
137
+ * reason separately so each executor can render them in its own error contract
138
+ * (an HTTP backend's typed `BackendError`, an SDK adapter's plain `Error`)
139
+ * without the reason text — which is where the fork evidence lives — being
140
+ * rewritten per repository.
141
+ */
142
+ export declare class PrimeProfileError extends Error {
143
+ readonly kind: PrimeProfileErrorKind;
144
+ readonly control: string;
145
+ readonly reason: string;
146
+ constructor(kind: PrimeProfileErrorKind, control: string, reason: string);
147
+ }
148
+ /** Raise the "the fork cannot honor this control" half of the refusal. */
149
+ export declare function rejectPrimeControl(control: string, reason: string): never;
150
+ /** Raise the "this declaration is malformed" half of the refusal. */
151
+ export declare function rejectPrimeDeclaration(control: string, reason: string): never;
152
+ /**
153
+ * The fork's continual-harness store exists but no longer parses.
154
+ *
155
+ * The store is the agent's own self-modification state — refinements, learned
156
+ * prompts, subagents it created mid-session — so an executor that reseeds it
157
+ * from empty destroys evidence and silently mutates the thing under study. The
158
+ * turn is failed instead; the file is the operator's to repair or remove.
159
+ */
160
+ export declare class PrimeHarnessStateError extends Error {
161
+ readonly statePath: string;
162
+ constructor(statePath: string, detail: string);
163
+ }
164
+ /**
165
+ * Strip `//` line comments and trailing commas, leaving string literals
166
+ * untouched — byte-for-byte the fork's own `stripJsonComments`
167
+ * (core/model-registry.ts), so a models.json the agent accepts is never refused
168
+ * by an executor and vice versa.
169
+ */
170
+ export declare function stripPrimeJsonComments(input: string): string;
171
+ /**
172
+ * Parse a models.json with the fork's own laxness and assert the one structural
173
+ * invariant it requires (a `providers` object). Throws naming `sourcePath`.
174
+ *
175
+ * This is the SHAPE and DIALECT only. Producing the file is deliberately not
176
+ * shared: an executor holding real operator credentials copies an
177
+ * operator-authored file and forwards only the env var names its `apiKey`
178
+ * entries reference, while an in-sandbox executor synthesizes a one-provider
179
+ * entry with a literal key that is a proxy dummy. Same schema, opposite threat
180
+ * models.
181
+ */
182
+ export declare function parsePrimeModelsJson(text: string, sourcePath: string): Record<string, unknown>;
183
+ /**
184
+ * Environment variables a models.json names as `apiKey` sources.
185
+ *
186
+ * prime resolves an `apiKey` value env-var first and LITERAL second
187
+ * (core/resolve-config-value.ts), so an executor that forwards exactly the
188
+ * named variables lets an operator keep secrets out of the file — and an
189
+ * executor that forwards NONE of them silently turns the variable name itself
190
+ * into the credential string.
191
+ */
192
+ export declare function primeApiKeyEnvNames(modelsConfig: Record<string, unknown>): string[];
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Fork FACTS for prime-agent (PrimeIntellect-ai/prime-agent) — the constants,
3
+ * dialects and refusal reasons every executor that drives the fork needs, with
4
+ * no `node:fs` and no executor assumptions. The agent-dir MATERIALIZATION that
5
+ * consumes them lives in `./index.ts`; both consumers (cli-bridge's host-spawn
6
+ * backend on `--mode rpc`, sdk-provider-prime's in-sandbox adapter on
7
+ * `--mode acp`) read these instead of each keeping a private copy, because a
8
+ * fork bump must move one file, not two repositories.
9
+ *
10
+ * Every value here is verified against the pinned clone named by
11
+ * {@link PRIME_FORK_PIN}; `tests/prime-fork-pin.test.ts` re-asserts them
12
+ * against that clone when it is present, so a fork bump fails loudly in one
13
+ * place.
14
+ */
15
+ /** The prime-agent clone every constant in this module was read from. */
16
+ export const PRIME_FORK_PIN = {
17
+ repo: "PrimeIntellect-ai/prime-agent",
18
+ version: "0.7.0",
19
+ commit: "be9e2fa0",
20
+ /** Local clone used by the fork-pin test when it exists. */
21
+ clonePath: "/home/drew/bench-cache/prime-agent",
22
+ /** Package root inside the clone that the file:line citations are relative to. */
23
+ packageRoot: "packages/coding-agent/src",
24
+ };
25
+ /**
26
+ * The fork's package identity, and why it cannot be installed from the npm
27
+ * registry.
28
+ *
29
+ * `@earendil-works/pi-coding-agent` on registry.npmjs.org serves the UPSTREAM
30
+ * pi line (0.84.x); the fork's 0.7.x versions were never published there. The
31
+ * two are wire-incompatible — different daemon schema revision, different
32
+ * config dir, different agent-dir env var (see
33
+ * {@link PRIME_AGENT_DIR_ENV_VARS}) — so `npm install -g` under that name
34
+ * yields an agent none of this lowering was verified against. The fork must be
35
+ * built from source and exposed under a `prime-agent` command name; its own
36
+ * `bin` entry is `pi` (fork package.json `bin`), which is exactly how the wrong
37
+ * binary ends up on `PATH` under the right-looking name.
38
+ */
39
+ export const PRIME_FORK_INSTALL = {
40
+ npmName: "@earendil-works/pi-coding-agent",
41
+ /** The fork's own bin name — deliberately NOT the command to install it as. */
42
+ forkBinName: "pi",
43
+ /** Command name an executor's binary path is expected to resolve to. */
44
+ expectedCommandName: "prime-agent",
45
+ sourceRepo: "https://github.com/PrimeIntellect-ai/prime-agent",
46
+ /** Built entrypoint to expose as `prime-agent`. */
47
+ sourceEntrypoint: "packages/coding-agent/dist/bundle/cli.js",
48
+ reason: "registry.npmjs.org serves the upstream pi line under @earendil-works/pi-coding-agent " +
49
+ "(0.84.x); the prime-agent fork's 0.7.x versions do not exist there. Installing from " +
50
+ "the registry yields a wire-incompatible agent with a different daemon schema, a " +
51
+ "different config dir, and a different agent-dir env var. Build from source and expose " +
52
+ "packages/coding-agent/dist/bundle/cli.js as `prime-agent`.",
53
+ };
54
+ /**
55
+ * Env vars the agent resolves its config dir from, most-specific first.
56
+ *
57
+ * The fork derives the name from its own package identity —
58
+ * `ENV_AGENT_DIR = \`${envPrefix}_CODING_AGENT_DIR\`` where `envPrefix` comes
59
+ * from `package.json` `piConfig.name` (config.ts:485-498) — so the fork reads
60
+ * `PRIME_AGENT_CODING_AGENT_DIR` and upstream pi reads `PI_CODING_AGENT_DIR`.
61
+ * An executor that sets only the fork's name has its isolation SILENTLY
62
+ * disabled when the binary on PATH turns out to be upstream pi: `getAgentDir()`
63
+ * falls back to `homedir()/<CONFIG_DIR_NAME>` (config.ts:520-527), one shared
64
+ * dir — and therefore one shared self-modifying `harness_state.json` — for
65
+ * every session on the box. Setting BOTH names costs nothing and keeps the
66
+ * isolation fail-closed under either binary.
67
+ */
68
+ export const PRIME_AGENT_DIR_ENV_VARS = [
69
+ "PRIME_AGENT_CODING_AGENT_DIR",
70
+ "PI_CODING_AGENT_DIR",
71
+ ];
72
+ /** Canonical agent-dir env var for the fork itself (config.ts:498). */
73
+ export const PRIME_AGENT_DIR_ENV_VAR = PRIME_AGENT_DIR_ENV_VARS[0];
74
+ /** Pin `agentDir` under every name the agent could resolve its config dir from. */
75
+ export function primeAgentDirEnv(agentDir) {
76
+ const env = {};
77
+ for (const name of PRIME_AGENT_DIR_ENV_VARS)
78
+ env[name] = agentDir;
79
+ return env;
80
+ }
81
+ /**
82
+ * Operator knobs an executor forwards into the agent when they are set.
83
+ *
84
+ * `PRIME_AGENT_KERNEL_VENV` is load-bearing for any executor that also pins
85
+ * HOME: the kernel venv defaults under `homedir()/.prime/agent/kernel-venv`
86
+ * (kernel/bootstrap.ts:331-342), so a fresh HOME re-bootstraps it through uv on
87
+ * every run unless the operator's venv path is forwarded
88
+ * (bootstrap.ts:340 override, :358 branch).
89
+ */
90
+ export const PRIME_KERNEL_ENV_PASSTHROUGH = [
91
+ /** "1" lets a headless first run install uv itself; "0" forbids it. */
92
+ "PRIME_AGENT_INSTALL_UV",
93
+ /** Offline escape hatch: pin the kernel venv python instead of uv-provisioning. */
94
+ "PRIME_AGENT_KERNEL_PYTHON",
95
+ /** Reuse an operator-provisioned kernel venv instead of re-bootstrapping it. */
96
+ "PRIME_AGENT_KERNEL_VENV",
97
+ ];
98
+ /**
99
+ * Env vars an executor must NEVER forward from its own environment.
100
+ *
101
+ * These re-point the self-modifying continual-harness store OUTSIDE the agent
102
+ * dir (agent-session.ts:8805 hands the kernel its store location), which
103
+ * defeats per-session isolation without changing anything an executor inspects.
104
+ * Isolation is the fail-closed default; sharing state is an explicit opt-in
105
+ * that names a directory, never an inherited env var.
106
+ */
107
+ export const PRIME_HARNESS_STATE_ENV_DENYLIST = [
108
+ "RLM_GLOBAL_HARNESS_STATE_DIR",
109
+ "RLM_HARNESS_STATE_DIR",
110
+ ];
111
+ /**
112
+ * A prime-agent IPython kernel that dies is never re-provisioned headlessly —
113
+ * every later `ipython` call fails with one of these markers until the process
114
+ * is replaced. An executor that cannot recognize them either reports a generic
115
+ * turn failure or burns its whole timeout against a permanently dead tool.
116
+ *
117
+ * Verified in `core/kernel/index.ts`: :723 ("forked kernel exited
118
+ * unexpectedly"), :750 ("Kernel exited before resolving ports"), :779 ("Kernel
119
+ * exited during startup"), :820/:837/:1172/:1295 ("Kernel has been shut down").
120
+ */
121
+ export const PRIME_KERNEL_DEAD_MARKERS = [
122
+ "Kernel has been shut down",
123
+ "Kernel exited before resolving ports",
124
+ "Kernel exited during startup",
125
+ "forked kernel exited unexpectedly",
126
+ ];
127
+ /** The first kernel-death marker present in `text`, or undefined. */
128
+ export function matchPrimeKernelDeadMarker(text) {
129
+ return PRIME_KERNEL_DEAD_MARKERS.find((marker) => text.includes(marker));
130
+ }
131
+ /**
132
+ * `--thinking` rungs the fork's argument parser accepts
133
+ * (cli/args.ts:62 `VALID_THINKING_LEVELS`, rejected at :175). Read from the
134
+ * fork itself rather than borrowed from upstream pi's measured table: the two
135
+ * agreeing today is a coincidence this module must not depend on.
136
+ */
137
+ export const PRIME_THINKING_LEVELS = [
138
+ "off",
139
+ "minimal",
140
+ "low",
141
+ "medium",
142
+ "high",
143
+ "xhigh",
144
+ "max",
145
+ ];
146
+ /** Canonical portable effort → fork `--thinking` rung. */
147
+ const PRIME_THINKING_BY_EFFORT = {
148
+ none: "off",
149
+ minimal: "minimal",
150
+ low: "low",
151
+ medium: "medium",
152
+ high: "high",
153
+ xhigh: "xhigh",
154
+ // The fork carries `max` above `xhigh`, so the ceiling level reaches it.
155
+ ultracode: "max",
156
+ };
157
+ /**
158
+ * Map the portable reasoning ladder onto the fork's `--thinking` value.
159
+ *
160
+ * Throws on an effort with no rung instead of returning undefined: omitting the
161
+ * flag runs the agent at its default thinking level while the caller believes
162
+ * the requested effort is in force, which is a silently dropped control.
163
+ */
164
+ export function primeThinkingLevel(effort) {
165
+ if (effort === undefined || effort === null || effort === "")
166
+ return undefined;
167
+ const level = PRIME_THINKING_BY_EFFORT[effort];
168
+ if (level === undefined) {
169
+ throw new PrimeProfileError("invalid", "model.reasoningEffort", `prime-agent has no --thinking rung for ${JSON.stringify(effort)} ` +
170
+ `(fork cli/args.ts:62 accepts ${PRIME_THINKING_LEVELS.join(", ")}).`);
171
+ }
172
+ return level;
173
+ }
174
+ /**
175
+ * Why `profile.mcp` (and any hub connection that resolves to MCP) is refused.
176
+ *
177
+ * The fork's `mcpServers` settings surface IS read at startup
178
+ * (settings-manager.ts:98-120,219) but a user-declared server never becomes
179
+ * model-visible: the host McpManager registers OAuth providers and serves
180
+ * mcp.config/refresh host-requests only (core/mcp/mcp-manager.ts:62-124),
181
+ * skips stdio servers entirely (mcp-manager.ts:73), and nothing generates a
182
+ * skill or prompt section for one. Model-visible MCP is exactly the built-in
183
+ * catalog skills, which are hand-written Python `McpIntegration` subclasses
184
+ * (prime-agent-runtime/src/rlm/mcp_base.py:112-125; only linear and notion
185
+ * ship). Materializing a per-run entry would run the agent WITHOUT the
186
+ * requested tools while the config on disk says otherwise.
187
+ */
188
+ export const PRIME_MCP_UNSUPPORTED_REASON = "a user-declared mcpServers entry is read from the fork's settings.json at startup but never " +
189
+ "becomes model-visible: the host only registers OAuth/config for it (fork core/mcp/" +
190
+ "mcp-manager.ts:62-124), skips stdio servers entirely (mcp-manager.ts:73), and generates no " +
191
+ "skill or prompt section for it. Model-visible MCP is exactly the built-in Python " +
192
+ "McpIntegration catalog skills (prime-agent-runtime/src/rlm/mcp_base.py:112-125), which no " +
193
+ "per-run configuration creates — so materializing it would be a silently dropped control.";
194
+ /**
195
+ * Refusal raised by the shared prime lowering, carrying the control and the
196
+ * reason separately so each executor can render them in its own error contract
197
+ * (an HTTP backend's typed `BackendError`, an SDK adapter's plain `Error`)
198
+ * without the reason text — which is where the fork evidence lives — being
199
+ * rewritten per repository.
200
+ */
201
+ export class PrimeProfileError extends Error {
202
+ kind;
203
+ control;
204
+ reason;
205
+ constructor(kind, control, reason) {
206
+ super(kind === "unsupported"
207
+ ? `prime does not support AgentProfile ${control}: ${reason}`
208
+ : `prime cannot accept AgentProfile ${control}: ${reason}`);
209
+ this.name = "PrimeProfileError";
210
+ this.kind = kind;
211
+ this.control = control;
212
+ this.reason = reason;
213
+ }
214
+ }
215
+ /** Raise the "the fork cannot honor this control" half of the refusal. */
216
+ export function rejectPrimeControl(control, reason) {
217
+ throw new PrimeProfileError("unsupported", control, reason);
218
+ }
219
+ /** Raise the "this declaration is malformed" half of the refusal. */
220
+ export function rejectPrimeDeclaration(control, reason) {
221
+ throw new PrimeProfileError("invalid", control, reason);
222
+ }
223
+ /**
224
+ * The fork's continual-harness store exists but no longer parses.
225
+ *
226
+ * The store is the agent's own self-modification state — refinements, learned
227
+ * prompts, subagents it created mid-session — so an executor that reseeds it
228
+ * from empty destroys evidence and silently mutates the thing under study. The
229
+ * turn is failed instead; the file is the operator's to repair or remove.
230
+ */
231
+ export class PrimeHarnessStateError extends Error {
232
+ statePath;
233
+ constructor(statePath, detail) {
234
+ super(`prime harness state ${statePath} is not valid JSON and may hold agent self-modification ` +
235
+ `state; refusing to overwrite it — repair or remove the file to continue (${detail})`);
236
+ this.name = "PrimeHarnessStateError";
237
+ this.statePath = statePath;
238
+ }
239
+ }
240
+ /**
241
+ * Strip `//` line comments and trailing commas, leaving string literals
242
+ * untouched — byte-for-byte the fork's own `stripJsonComments`
243
+ * (core/model-registry.ts), so a models.json the agent accepts is never refused
244
+ * by an executor and vice versa.
245
+ */
246
+ export function stripPrimeJsonComments(input) {
247
+ return input
248
+ .replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*/g, (m) => (m[0] === '"' ? m : ""))
249
+ .replace(/"(?:\\.|[^"\\])*"|,(\s*[}\]])/g, (m, tail) => tail !== undefined ? tail : m[0] === '"' ? m : "");
250
+ }
251
+ /**
252
+ * Parse a models.json with the fork's own laxness and assert the one structural
253
+ * invariant it requires (a `providers` object). Throws naming `sourcePath`.
254
+ *
255
+ * This is the SHAPE and DIALECT only. Producing the file is deliberately not
256
+ * shared: an executor holding real operator credentials copies an
257
+ * operator-authored file and forwards only the env var names its `apiKey`
258
+ * entries reference, while an in-sandbox executor synthesizes a one-provider
259
+ * entry with a literal key that is a proxy dummy. Same schema, opposite threat
260
+ * models.
261
+ */
262
+ export function parsePrimeModelsJson(text, sourcePath) {
263
+ let parsed;
264
+ try {
265
+ parsed = JSON.parse(stripPrimeJsonComments(text));
266
+ }
267
+ catch (err) {
268
+ throw new Error(`prime models.json ${sourcePath} is not valid JSON: ${err.message}`);
269
+ }
270
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
271
+ throw new Error(`prime models.json ${sourcePath} must be a JSON object`);
272
+ }
273
+ const providers = parsed.providers;
274
+ if (!providers || typeof providers !== "object" || Array.isArray(providers)) {
275
+ throw new Error(`prime models.json ${sourcePath} must carry a "providers" object (prime's models.json schema)`);
276
+ }
277
+ return parsed;
278
+ }
279
+ /**
280
+ * Environment variables a models.json names as `apiKey` sources.
281
+ *
282
+ * prime resolves an `apiKey` value env-var first and LITERAL second
283
+ * (core/resolve-config-value.ts), so an executor that forwards exactly the
284
+ * named variables lets an operator keep secrets out of the file — and an
285
+ * executor that forwards NONE of them silently turns the variable name itself
286
+ * into the credential string.
287
+ */
288
+ export function primeApiKeyEnvNames(modelsConfig) {
289
+ const names = [];
290
+ const providers = modelsConfig.providers;
291
+ if (!providers || typeof providers !== "object")
292
+ return names;
293
+ for (const provider of Object.values(providers)) {
294
+ if (!provider || typeof provider !== "object")
295
+ continue;
296
+ const key = provider.apiKey;
297
+ if (typeof key !== "string" || !/^[A-Z_][A-Z0-9_]*$/.test(key))
298
+ continue;
299
+ if (!names.includes(key))
300
+ names.push(key);
301
+ }
302
+ return names;
303
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-profile-materialize",
3
- "version": "0.12.0",
3
+ "version": "0.13.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",
@@ -20,13 +20,13 @@
20
20
  "dist"
21
21
  ],
22
22
  "devDependencies": {
23
- "@tangle-network/agent-interface": "0.45.0",
23
+ "@tangle-network/agent-interface": "0.46.1",
24
24
  "@types/node": "25.6.0",
25
25
  "typescript": "^6.0.3",
26
26
  "vitest": "^4.1.10"
27
27
  },
28
28
  "peerDependencies": {
29
- "@tangle-network/agent-interface": ">=0.45.0 <0.46.0"
29
+ "@tangle-network/agent-interface": ">=0.46.0 <0.47.0"
30
30
  },
31
31
  "scripts": {
32
32
  "build": "tsc -p tsconfig.json",