@tangle-network/agent-profile-materialize 0.25.2 → 0.25.4

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/harness.js CHANGED
@@ -348,14 +348,15 @@ export function materializerReplacement(relPath, content, plannedContent) {
348
348
  * there.
349
349
  *
350
350
  * An `opencode.json` this package wrote holds the opencode schema id, only the
351
- * keys this package writes, and only instruction paths this package invented.
352
- * A user config that holds nothing else is indistinguishable from that output
353
- * and is replaced too; a config with any other key, or with an instruction path
354
- * of the user's own, is refused.
351
+ * keys this package writes, and only instruction paths this package invented;
352
+ * a user config holding nothing else is replaced too, any other key or own
353
+ * instruction path is refused.
355
354
  */
356
355
  function isMaterializerOutput(relPath, content, plannedContent) {
357
356
  if (MATERIALIZER_RESERVED_PATHS.has(relPath))
358
357
  return true;
358
+ if (relPath === ".pi/mcp.json" || relPath === ".kimi/mcp.json")
359
+ return isMaterializerMcpConfig(relPath, content);
359
360
  if (MATERIALIZER_SUBAGENT_PATH.test(relPath)) {
360
361
  const existing = content();
361
362
  if (existing === undefined)
@@ -435,3 +436,4 @@ export function skillDirForHarness(harness) {
435
436
  return HARNESS_SKILL_DIRS[harness];
436
437
  }
437
438
  import { createHash } from "node:crypto";
439
+ import { isMaterializerMcpConfig } from "./mcp-config-ownership.js";
package/dist/index.d.ts CHANGED
@@ -5,8 +5,8 @@ export type { HarnessId } from "./harness.js";
5
5
  export { HARNESS_SKILL_DIRS, harnessAutoLoadsAgentContextFile, harnessAutoLoadsContextFile, harnessContextFile, isMaterializerHarness, MATERIALIZER_CONTEXT_PATHS, MATERIALIZER_HARNESSES, markMaterializerContextFile, skillDirForHarness, } from "./harness.js";
6
6
  export * from "./harness-prompt.js";
7
7
  export { type ConcurrentProfileRevision, ensurePublicConcurrentProfileRoots, type ManagedWorkspaceFileDigest, type ManagedWorkspaceManifest, preparePublicConcurrentProfileRevision, } from "./managed-workspace.js";
8
- export type { OpenCodeMcpConfig } from "./mcp.js";
9
- export { openCodeMcpConfig } from "./mcp.js";
8
+ export { type OpenCodeMcpConfig, openCodeMcpConfig } from "./mcp.js";
9
+ export { isMaterializerMcpConfig } from "./mcp-config-ownership.js";
10
10
  export { modelIdsMatch, qualifyModelId, unqualifyModelId, } from "./model-id.js";
11
11
  export { buildPiModelsConfig, buildPiModelsConfigFile, PI_MODELS_CONFIG_PATH, type PiModelsConfigFile, type PiModelsConfigInput, type PiRuntimePaths, resolvePiRuntimePaths, } from "./pi-models-config.js";
12
12
  export { applyWorkspacePlan, applyWorkspacePlanWithSecretProvider, } from "./plan-application.js";
@@ -15,8 +15,7 @@ export { takeSystemPromptBinding } from "./plan-prompt.js";
15
15
  export { assertWorkspacePlanSupported, enforceWorkspacePlanSupport, } from "./plan-support.js";
16
16
  export type { ApplyWorkspacePlanOptions, BindableLaunchControl, ExecutorSystemPromptBinding, PlanFile, PlanFileRoot, PlanFileSecretSlot, PlanRequirement, SkippableDimension, Unsupported, WorkspacePlan, WorkspacePlanApplication, WorkspacePlanArgument, WorkspacePlanArgumentTemplate, WorkspacePlanConfigValue, WorkspacePlanReceipt, WorkspacePlanSecretSegment, WorkspacePlanSpawnInputs, WorkspacePlanSystemPromptBinding, WorkspacePlanWrittenFile, } from "./plan-types.js";
17
17
  export { BINDABLE_LAUNCH_CONTROLS, EXECUTOR_SYSTEM_PROMPT_BINDINGS, isExecutorSystemPromptBinding, isSkippableDimension, SKIPPABLE_DIMENSIONS, WorkspacePlanCleanupError, } from "./plan-types.js";
18
- export type { PrimeMaterializeOptions } from "./prime-agent-dir.js";
19
- export { materializePrimeProfileControls, prunePrimeProfileMaterial, seedPrimeProfileSubagents, writePrimeAgentFile, } from "./prime-agent-dir.js";
18
+ export { materializePrimeProfileControls, type PrimeMaterializeOptions, primeHarnessStatePath, prunePrimeProfileMaterial, seedPrimeProfileSubagents, writePrimeAgentFile, } from "./prime-agent-dir.js";
20
19
  export type { PrimeProfileErrorKind, PrimeThinkingLevel, } from "./prime-fork.js";
21
20
  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";
22
21
  export type { PrimeProfileControls, PrimeProfileSkill, PrimeProfileSubagent, } from "./prime-profile.js";
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export { HARNESS_SKILL_DIRS, harnessAutoLoadsAgentContextFile, harnessAutoLoadsC
4
4
  export * from "./harness-prompt.js";
5
5
  export { ensurePublicConcurrentProfileRoots, preparePublicConcurrentProfileRevision, } from "./managed-workspace.js";
6
6
  export { openCodeMcpConfig } from "./mcp.js";
7
+ export { isMaterializerMcpConfig } from "./mcp-config-ownership.js";
7
8
  export { modelIdsMatch, qualifyModelId, unqualifyModelId, } from "./model-id.js";
8
9
  export { buildPiModelsConfig, buildPiModelsConfigFile, PI_MODELS_CONFIG_PATH, resolvePiRuntimePaths, } from "./pi-models-config.js";
9
10
  export { applyWorkspacePlan, applyWorkspacePlanWithSecretProvider, } from "./plan-application.js";
@@ -11,7 +12,7 @@ export { agentCandidateProfilePlanMaterial, canonicalizeAgentCandidateProfilePla
11
12
  export { takeSystemPromptBinding } from "./plan-prompt.js";
12
13
  export { assertWorkspacePlanSupported, enforceWorkspacePlanSupport, } from "./plan-support.js";
13
14
  export { BINDABLE_LAUNCH_CONTROLS, EXECUTOR_SYSTEM_PROMPT_BINDINGS, isExecutorSystemPromptBinding, isSkippableDimension, SKIPPABLE_DIMENSIONS, WorkspacePlanCleanupError, } from "./plan-types.js";
14
- export { materializePrimeProfileControls, prunePrimeProfileMaterial, seedPrimeProfileSubagents, writePrimeAgentFile, } from "./prime-agent-dir.js";
15
+ export { materializePrimeProfileControls, primeHarnessStatePath, prunePrimeProfileMaterial, seedPrimeProfileSubagents, writePrimeAgentFile, } from "./prime-agent-dir.js";
15
16
  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";
16
17
  export { assertPrimeModelAgreement, hasPrimeProfileMaterial, primeProfileNeedsAgentDir, primeSubagentEntryId, readPrimeProfileControls, } from "./prime-profile.js";
17
18
  export * from "./profile-bundle-cache.js";
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Harness MCP config files this package generates wholesale from
3
+ * `profile.mcp`: exactly one `mcpServers` object whose entries carry only
4
+ * the fields the generators write. Recognizable by that shape alone (no
5
+ * schema id to key on), so an existing file holding ONLY those registrations
6
+ * is this package's earlier output and a new plan replaces it — a per-turn
7
+ * profile changes these bytes routinely (physim's MCP env carries turn
8
+ * state). Anything else at the path (a user's own keys, extra fields) still
9
+ * refuses: the name alone proves nothing, same rule as `opencode.json`.
10
+ */
11
+ /** Paths owned by this rule. `.gemini/settings.json` is deliberately absent:
12
+ * it is a broader user settings file the package only contributes one key
13
+ * to, so its bytes cannot prove authorship. */
14
+ export declare const MATERIALIZER_MCP_CONFIG_PATHS: Set<string>;
15
+ /** The parsed form this rule accepts: `JSON.parse` output narrowed at the
16
+ * I/O boundary by {@link parseMcpServersConfig}. */
17
+ export interface McpServersConfig {
18
+ mcpServers: Record<string, Record<string, unknown>>;
19
+ }
20
+ /** Parse workspace bytes into the one shape this rule owns, or `null` when
21
+ * the bytes are anything else (not an object, other top-level keys, or
22
+ * server entries that are not objects). The I/O boundary for
23
+ * {@link isGeneratedMcpConfigShape}. */
24
+ export declare function parseMcpServersConfig(bytes: string): McpServersConfig | null;
25
+ /** Whether a parsed MCP config carries ONLY the registrations this package
26
+ * generates: no extra top-level keys, and every server entry holds only
27
+ * generated fields. A user's own key anywhere makes this `false`. */
28
+ export declare function isGeneratedMcpConfigShape(config: McpServersConfig): boolean;
29
+ /** Whether the existing file at `relPath` is this package's own earlier MCP
30
+ * config output (shape rule above). `undefined` content = no regular file. */
31
+ export declare function isMaterializerMcpConfig(relPath: string, content: () => string | undefined): boolean;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Harness MCP config files this package generates wholesale from
3
+ * `profile.mcp`: exactly one `mcpServers` object whose entries carry only
4
+ * the fields the generators write. Recognizable by that shape alone (no
5
+ * schema id to key on), so an existing file holding ONLY those registrations
6
+ * is this package's earlier output and a new plan replaces it — a per-turn
7
+ * profile changes these bytes routinely (physim's MCP env carries turn
8
+ * state). Anything else at the path (a user's own keys, extra fields) still
9
+ * refuses: the name alone proves nothing, same rule as `opencode.json`.
10
+ */
11
+ /** Paths owned by this rule. `.gemini/settings.json` is deliberately absent:
12
+ * it is a broader user settings file the package only contributes one key
13
+ * to, so its bytes cannot prove authorship. */
14
+ export const MATERIALIZER_MCP_CONFIG_PATHS = new Set([
15
+ ".pi/mcp.json",
16
+ ".kimi/mcp.json",
17
+ ]);
18
+ const GENERATED_MCP_SERVER_KEYS = new Set([
19
+ "command",
20
+ "args",
21
+ "env",
22
+ "cwd",
23
+ "url",
24
+ "httpUrl",
25
+ "headers",
26
+ "transport",
27
+ "enabled",
28
+ "directTools",
29
+ ]);
30
+ /** Parse workspace bytes into the one shape this rule owns, or `null` when
31
+ * the bytes are anything else (not an object, other top-level keys, or
32
+ * server entries that are not objects). The I/O boundary for
33
+ * {@link isGeneratedMcpConfigShape}. */
34
+ export function parseMcpServersConfig(bytes) {
35
+ let parsed;
36
+ try {
37
+ parsed = JSON.parse(bytes);
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
43
+ return null;
44
+ }
45
+ const { mcpServers } = parsed;
46
+ if (mcpServers === null ||
47
+ typeof mcpServers !== "object" ||
48
+ Array.isArray(mcpServers)) {
49
+ return null;
50
+ }
51
+ for (const server of Object.values(mcpServers)) {
52
+ if (server === null ||
53
+ typeof server !== "object" ||
54
+ Array.isArray(server)) {
55
+ return null;
56
+ }
57
+ }
58
+ return { mcpServers: mcpServers };
59
+ }
60
+ /** Whether a parsed MCP config carries ONLY the registrations this package
61
+ * generates: no extra top-level keys, and every server entry holds only
62
+ * generated fields. A user's own key anywhere makes this `false`. */
63
+ export function isGeneratedMcpConfigShape(config) {
64
+ const keys = Object.keys(config);
65
+ if (keys.length !== 1 || keys[0] !== "mcpServers")
66
+ return false;
67
+ return Object.values(config.mcpServers).every((server) => Object.keys(server).every((key) => GENERATED_MCP_SERVER_KEYS.has(key)));
68
+ }
69
+ /** Whether the existing file at `relPath` is this package's own earlier MCP
70
+ * config output (shape rule above). `undefined` content = no regular file. */
71
+ export function isMaterializerMcpConfig(relPath, content) {
72
+ if (!MATERIALIZER_MCP_CONFIG_PATHS.has(relPath))
73
+ return false;
74
+ const bytes = content();
75
+ if (bytes === undefined)
76
+ return false;
77
+ const config = parseMcpServersConfig(bytes);
78
+ return config !== null && isGeneratedMcpConfigShape(config);
79
+ }
@@ -1,4 +1,19 @@
1
1
  export declare function writePrimeAgentFile(path: string, data: string, mode?: number): void;
2
+ /**
3
+ * File access on a prime agent dir. The default reads and writes the local
4
+ * filesystem as the calling process. An executor whose agent dir belongs to
5
+ * another user (a root sidecar that demotes prime-agent) passes reads it took
6
+ * as that user and replays the writes as that user, so it never touches the
7
+ * agent's files with its own rights.
8
+ */
9
+ export interface PrimeAgentDirIo {
10
+ /** The file's text, or undefined when it does not exist. */
11
+ read(path: string): string | undefined;
12
+ /** Replace the whole file: atomic and owner-only. */
13
+ write(path: string, data: string): void;
14
+ }
15
+ /** The fork's continual-harness store, the only agent-dir file a materialization reads. */
16
+ export declare function primeHarnessStatePath(agentDir: string): string;
2
17
  export interface PrimeMaterializeOptions {
3
18
  /**
4
19
  * Where the FLAG-NAMED files go — the prompt files and the skills root.
@@ -14,6 +29,8 @@ export interface PrimeMaterializeOptions {
14
29
  * (currently: adopting entries a pre-shared executor left behind).
15
30
  */
16
31
  warn?: (msg: string, meta?: Record<string, unknown>) => void;
32
+ /** Agent-dir file access; the local filesystem when omitted. */
33
+ io?: PrimeAgentDirIo;
17
34
  }
18
35
  /**
19
36
  * Materialize the validated controls and return the extra spawn args that bind
@@ -43,5 +60,5 @@ export declare function prunePrimeProfileMaterial(agentDir: string, options?: Pi
43
60
  * {@link PrimeHarnessStateError}: the file is the agent's own self-modification
44
61
  * state, and overwriting it destroys what the run was supposed to observe.
45
62
  */
46
- export declare function seedPrimeProfileSubagents(agentDir: string, subagents: PrimeProfileSubagent[], warn?: (msg: string, meta?: Record<string, unknown>) => void): void;
63
+ export declare function seedPrimeProfileSubagents(agentDir: string, subagents: PrimeProfileSubagent[], warn?: (msg: string, meta?: Record<string, unknown>) => void, io?: PrimeAgentDirIo): void;
47
64
  import type { PrimeProfileControls, PrimeProfileSubagent } from "./prime-profile.js";
@@ -4,6 +4,14 @@ export function writePrimeAgentFile(path, data, mode = 0o600) {
4
4
  writeFileSync(temp, data, { mode });
5
5
  renameSync(temp, path);
6
6
  }
7
+ const localPrimeAgentDirIo = {
8
+ read: (path) => (existsSync(path) ? readFileSync(path, "utf8") : undefined),
9
+ write: (path, data) => writePrimeAgentFile(path, data),
10
+ };
11
+ /** The fork's continual-harness store, the only agent-dir file a materialization reads. */
12
+ export function primeHarnessStatePath(agentDir) {
13
+ return join(agentDir, PRIME_HARNESS_DIR_NAME, PRIME_HARNESS_STATE_FILE);
14
+ }
7
15
  /**
8
16
  * Materialize the validated controls and return the extra spawn args that bind
9
17
  * them. Runs before every spawn (same place models.json is materialized), so a
@@ -11,30 +19,30 @@ export function writePrimeAgentFile(path, data, mode = 0o600) {
11
19
  * process.
12
20
  */
13
21
  export function materializePrimeProfileControls(agentDir, controls, options = {}) {
14
- const { flagFileRoot = agentDir, warn } = options;
22
+ const { flagFileRoot = agentDir, warn, io = localPrimeAgentDirIo } = options;
15
23
  const args = [];
16
24
  const profileDir = join(flagFileRoot, PRIME_PROFILE_DIR_NAME);
17
25
  if (controls.systemPrompt !== undefined) {
18
26
  const promptPath = join(profileDir, PRIME_SYSTEM_PROMPT_FILE);
19
- writePrimeAgentFile(promptPath, controls.systemPrompt);
27
+ io.write(promptPath, controls.systemPrompt);
20
28
  args.push("--system-prompt", promptPath);
21
29
  }
22
30
  if (controls.appendSystemPrompt !== undefined) {
23
31
  const appendPath = join(profileDir, PRIME_APPEND_SYSTEM_PROMPT_FILE);
24
- writePrimeAgentFile(appendPath, controls.appendSystemPrompt);
32
+ io.write(appendPath, controls.appendSystemPrompt);
25
33
  args.push("--append-system-prompt", appendPath);
26
34
  }
27
35
  if (controls.instructions !== undefined) {
28
- writePrimeAgentFile(join(agentDir, PRIME_CONTEXT_FILE), `${controls.instructions}\n`);
36
+ io.write(join(agentDir, PRIME_CONTEXT_FILE), `${controls.instructions}\n`);
29
37
  }
30
38
  if (controls.skills.length > 0) {
31
39
  const skillsRoot = join(profileDir, PRIME_SKILLS_DIR_NAME);
32
40
  for (const skill of controls.skills) {
33
- writePrimeAgentFile(join(skillsRoot, skill.name, "SKILL.md"), normalizeSkillMd(skill.name, skill.content));
41
+ io.write(join(skillsRoot, skill.name, "SKILL.md"), normalizeSkillMd(skill.name, skill.content));
34
42
  }
35
43
  args.push("--skill", skillsRoot);
36
44
  }
37
- seedPrimeProfileSubagents(agentDir, controls.subagents, warn);
45
+ seedPrimeProfileSubagents(agentDir, controls.subagents, warn, io);
38
46
  return args;
39
47
  }
40
48
  /**
@@ -65,13 +73,14 @@ export function prunePrimeProfileMaterial(agentDir, options = {}) {
65
73
  * {@link PrimeHarnessStateError}: the file is the agent's own self-modification
66
74
  * state, and overwriting it destroys what the run was supposed to observe.
67
75
  */
68
- export function seedPrimeProfileSubagents(agentDir, subagents, warn) {
69
- const statePath = join(agentDir, PRIME_HARNESS_DIR_NAME, PRIME_HARNESS_STATE_FILE);
76
+ export function seedPrimeProfileSubagents(agentDir, subagents, warn, io = localPrimeAgentDirIo) {
77
+ const statePath = primeHarnessStatePath(agentDir);
70
78
  let state = {};
71
- if (existsSync(statePath)) {
79
+ const stored = io.read(statePath);
80
+ if (stored !== undefined) {
72
81
  let parsed;
73
82
  try {
74
- parsed = JSON.parse(readFileSync(statePath, "utf8"));
83
+ parsed = JSON.parse(stored);
75
84
  }
76
85
  catch (err) {
77
86
  throw new PrimeHarnessStateError(statePath, err instanceof Error ? err.message : String(err));
@@ -149,7 +158,7 @@ export function seedPrimeProfileSubagents(agentDir, subagents, warn) {
149
158
  }
150
159
  if (!changed)
151
160
  return;
152
- writePrimeAgentFile(statePath, `${JSON.stringify({
161
+ io.write(statePath, `${JSON.stringify({
153
162
  ...state,
154
163
  schema: typeof state.schema === "number" ? state.schema : 1,
155
164
  entries,
@@ -1,4 +1,4 @@
1
- export declare const compareStrings: (a: string, b: string) => 1 | -1 | 0;
1
+ export declare const compareStrings: (a: string, b: string) => 1 | 0 | -1;
2
2
  export declare const DEFAULT_FILE_MODE = 420;
3
3
  export declare const PRIVATE_FILE_MODE = 384;
4
4
  export declare const EXECUTABLE_FILE_MODE = 493;
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-profile-materialize",
3
- "version": "0.25.2",
4
- "description": "Materialize an AgentProfile into the native per-harness workspace files (the one shared materializer for cli-bridge, agent-dev-container, and VB).",
3
+ "version": "0.25.4",
4
+ "description": "Materialize an AgentProfile into the native per-harness workspace files (the one shared materializer for cli-bridge, agent-container, and VB).",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/tangle-network/agent-dev-container.git",
8
+ "url": "git+https://github.com/tangle-network/agent-container.git",
9
9
  "directory": "packages/agent-profile-materialize"
10
10
  },
11
11
  "main": "./dist/index.js",