@tangle-network/agent-profile-materialize 0.26.9 → 0.27.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
@@ -27,3 +27,4 @@ export type { ResolveAgentProfileResourcesOptions } from "./resource-resolution.
27
27
  export { normalizeSkillMd, resolveAgentProfileResources, } from "./resource-resolution.js";
28
28
  export { prepareSecretConcurrentProfileRevision } from "./secret-concurrent-revision.js";
29
29
  export * from "./secret-session-view.js";
30
+ export * from "./tool-bin-path.js";
package/dist/index.js CHANGED
@@ -21,3 +21,4 @@ export * from "./profile-start-check.js";
21
21
  export { normalizeSkillMd, resolveAgentProfileResources, } from "./resource-resolution.js";
22
22
  export { prepareSecretConcurrentProfileRevision } from "./secret-concurrent-revision.js";
23
23
  export * from "./secret-session-view.js";
24
+ export * from "./tool-bin-path.js";
@@ -1,4 +1,4 @@
1
- import { join } from "node:path";
1
+ import { dirname, join } from "node:path";
2
2
  /**
3
3
  * Paths a secret OpenCode revision serves from its own immutable tree:
4
4
  * read-only profile content under `reference/`, skills, and OpenCode's
@@ -88,9 +88,19 @@ export function bindConcurrentLaunch(plan, application, revisionWorkspaceDir, op
88
88
  break;
89
89
  }
90
90
  }
91
+ // A declared executable reference/ tool runs from the immutable revision:
92
+ // its directory, absolute in that revision, joins the launch PATH.
93
+ const toolBinDirs = [];
91
94
  for (const file of plan.files) {
92
- if (ridesSecretRevision(plan, file, options))
95
+ if (ridesSecretRevision(plan, file, options)) {
96
+ if (file.relPath.startsWith("reference/") &&
97
+ ((file.mode ?? 0) & 0o111) !== 0) {
98
+ const dir = join(revisionWorkspaceDir, dirname(file.relPath));
99
+ if (!toolBinDirs.includes(dir))
100
+ toolBinDirs.push(dir);
101
+ }
93
102
  continue;
103
+ }
94
104
  if (file.source !== "generated") {
95
105
  throw new Error(`Concurrent ${plan.harness} profile resource ${file.relPath} needs a shared-workspace collision-safe copy`);
96
106
  }
@@ -104,5 +114,10 @@ export function bindConcurrentLaunch(plan, application, revisionWorkspaceDir, op
104
114
  throw new Error(`Concurrent ${plan.harness} profile cannot bind runtime-only file ${name}`);
105
115
  }
106
116
  }
107
- return { ...application.spawn, flags, env };
117
+ return {
118
+ ...application.spawn,
119
+ flags,
120
+ env,
121
+ ...(toolBinDirs.length > 0 ? { toolBinDirs } : {}),
122
+ };
108
123
  }
@@ -7,9 +7,6 @@ export interface NormalizedPlanFile {
7
7
  ownership: NonNullable<PlanFile["ownership"]>;
8
8
  secretSlots: PlanFileSecretSlot[];
9
9
  }
10
- export type JsonValue = null | boolean | number | string | JsonValue[] | {
11
- [key: string]: JsonValue;
12
- };
13
10
  export declare function normalizePlanFiles(files: PlanFile[]): NormalizedPlanFile[];
14
11
  export declare function normalizeWorkspacePlanArgument(value: WorkspacePlanArgument): WorkspacePlanArgument;
15
12
  import type { PlanFile, PlanFileRoot, PlanFileSecretSlot, WorkspacePlanArgument } from "./plan-types.js";
@@ -284,6 +284,13 @@ export interface WorkspacePlanSpawnInputs {
284
284
  appendSystemPrompt?: string;
285
285
  /** Read it with {@link takeSystemPromptBinding}, which refuses a foreign control. */
286
286
  systemPromptBinding?: WorkspacePlanSystemPromptBinding;
287
+ /**
288
+ * Absolute dirs of the profile's declared executable `reference/` tools in
289
+ * the immutable revision the launch runs from. The provider puts them on
290
+ * the launch PATH in place of the app's own tool dirs (bindLaunchToolBinDirs),
291
+ * so a profile names its CLI bare.
292
+ */
293
+ toolBinDirs?: string[];
287
294
  }
288
295
  /** Result of applying a public plan at the private executor boundary. */
289
296
  export interface WorkspacePlanApplication {
@@ -3,7 +3,7 @@ import { mkdirSync, readdirSync, rmSync } from "node:fs";
3
3
  import { mkdir, open, readFile, rename, rm, stat, writeFile, } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
5
  import { PROFILE_BUNDLE_DEFAULT_MAX_BYTES, PROFILE_BUNDLE_MAX_BYTES, PROFILE_BUNDLE_MAX_FILE_BYTES, PROFILE_BUNDLE_MAX_FILES, PROFILE_BUNDLE_SCHEMA, } from "./profile-bundle-key.js";
6
- import { evictProfileBundles, evictUnlocked, fsyncDir, hasPendingRootWork, loadLru, pathExists, profileBundleManifestPath, readManifestFile, saveLru, sweepProfileBundleCache, sweepUnlocked, withRootLock, } from "./profile-bundle-lru.js";
6
+ import { evictProfileBundles, evictUnlocked, fsyncDir, hasPendingRootWork, loadLru, pathExistsFollowingLinks, profileBundleManifestPath, readManifestFile, saveLru, sweepProfileBundleCache, sweepUnlocked, withRootLock, } from "./profile-bundle-lru.js";
7
7
  import { hasControlCharacter, normalizeWorkspaceRelativePath, validateFileMode, } from "./workspace-files.js";
8
8
  export * from "./profile-bundle-key.js";
9
9
  export { evictProfileBundles, profileBundleManifestPath, sweepProfileBundleCache, } from "./profile-bundle-lru.js";
@@ -142,7 +142,7 @@ export async function admitProfileBundle(root, manifest, readObject) {
142
142
  }
143
143
  totalBytes += buffer.byteLength;
144
144
  const object = join(root, "objects", entry.sha256);
145
- if (await pathExists(object)) {
145
+ if (await pathExistsFollowingLinks(object)) {
146
146
  // Already admitted; the bytes above were re-verified, so drop the temp.
147
147
  continue;
148
148
  }
@@ -17,7 +17,7 @@ export interface ProfileBundleLru {
17
17
  createdAt: string;
18
18
  }>;
19
19
  }
20
- export declare function pathExists(path: string): Promise<boolean>;
20
+ export declare function pathExistsFollowingLinks(path: string): Promise<boolean>;
21
21
  export declare function fsyncDir(dir: string): Promise<void>;
22
22
  export declare function withRootLock<T>(root: string, run: () => Promise<T>): Promise<T>;
23
23
  /** Synchronous open must not sweep a root with pending asynchronous work. */
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { open, readdir, readFile, rename, rm, stat, writeFile, } from "node:fs/promises";
3
3
  import { join, resolve } from "node:path";
4
4
  import { compareStrings } from "./workspace-files.js";
5
- export async function pathExists(path) {
5
+ export async function pathExistsFollowingLinks(path) {
6
6
  try {
7
7
  await stat(path);
8
8
  return true;
@@ -150,7 +150,7 @@ export async function evictUnlocked(root, maxBytes) {
150
150
  const lru = await loadLru(root);
151
151
  const objectsDir = join(root, "objects");
152
152
  for (const sha of Object.keys(lru.objects)) {
153
- if (!(await pathExists(join(objectsDir, sha)))) {
153
+ if (!(await pathExistsFollowingLinks(join(objectsDir, sha)))) {
154
154
  delete lru.objects[sha];
155
155
  }
156
156
  }
@@ -0,0 +1,23 @@
1
+ /** The colon-separated, absolute CLI bin dirs a sandbox app mounts for its tools. */
2
+ export declare const SANDBOX_TOOL_BIN_DIRS_ENV = "SANDBOX_TOOL_BIN_DIRS";
3
+ /**
4
+ * The tool bin dirs a `SANDBOX_TOOL_BIN_DIRS` value names, or `[]` when it is
5
+ * unset or blank.
6
+ *
7
+ * Fail loud, never silently degrade: a malformed value (a relative segment,
8
+ * or an empty segment from a stray/leading/trailing colon) throws rather than
9
+ * silently dropping a tool dir from PATH or admitting a relative path that
10
+ * resolves against whatever the process cwd happens to be.
11
+ */
12
+ export declare function parseSandboxToolBinDirs(value: string | undefined): string[];
13
+ /**
14
+ * A launch environment whose tool bin dirs are `toolBinDirs`, at the TAIL of
15
+ * PATH after every Nix, npm, image and system entry, so a tool never shadows
16
+ * a platform-owned binary of the same name.
17
+ *
18
+ * They replace `appToolBinDirs` (by default the dirs the environment's own
19
+ * `SANDBOX_TOOL_BIN_DIRS` names) wherever those sit in PATH, so an app's older
20
+ * copy of the same tool cannot win by PATH order. Relative PATH entries are
21
+ * dropped too: they resolve against whatever cwd the turn moves to.
22
+ */
23
+ export declare function bindLaunchToolBinDirs(env: Readonly<Record<string, string | undefined>>, toolBinDirs: readonly string[], appToolBinDirs?: readonly string[]): Record<string, string | undefined>;
@@ -0,0 +1,61 @@
1
+ import { isAbsolute } from "node:path";
2
+ /** The colon-separated, absolute CLI bin dirs a sandbox app mounts for its tools. */
3
+ export const SANDBOX_TOOL_BIN_DIRS_ENV = "SANDBOX_TOOL_BIN_DIRS";
4
+ /**
5
+ * The tool bin dirs a `SANDBOX_TOOL_BIN_DIRS` value names, or `[]` when it is
6
+ * unset or blank.
7
+ *
8
+ * Fail loud, never silently degrade: a malformed value (a relative segment,
9
+ * or an empty segment from a stray/leading/trailing colon) throws rather than
10
+ * silently dropping a tool dir from PATH or admitting a relative path that
11
+ * resolves against whatever the process cwd happens to be.
12
+ */
13
+ export function parseSandboxToolBinDirs(value) {
14
+ const trimmed = value?.trim();
15
+ if (!trimmed)
16
+ return [];
17
+ const segments = trimmed.split(":");
18
+ for (const segment of segments) {
19
+ if (!segment || !isAbsolute(segment)) {
20
+ throw new Error(`SANDBOX_TOOL_BIN_DIRS must be a colon-separated list of absolute paths with no empty segments, got: ${trimmed}`);
21
+ }
22
+ }
23
+ return segments;
24
+ }
25
+ function assertToolBinDir(dir) {
26
+ // A ':' would split one declared dir into several PATH entries, admitting
27
+ // a dir no profile declared.
28
+ if (!dir || !isAbsolute(dir) || dir.includes(":")) {
29
+ throw new Error(`a tool bin dir must be an absolute path without ':', got: ${dir}`);
30
+ }
31
+ }
32
+ /**
33
+ * A launch environment whose tool bin dirs are `toolBinDirs`, at the TAIL of
34
+ * PATH after every Nix, npm, image and system entry, so a tool never shadows
35
+ * a platform-owned binary of the same name.
36
+ *
37
+ * They replace `appToolBinDirs` (by default the dirs the environment's own
38
+ * `SANDBOX_TOOL_BIN_DIRS` names) wherever those sit in PATH, so an app's older
39
+ * copy of the same tool cannot win by PATH order. Relative PATH entries are
40
+ * dropped too: they resolve against whatever cwd the turn moves to.
41
+ */
42
+ export function bindLaunchToolBinDirs(env, toolBinDirs, appToolBinDirs = parseSandboxToolBinDirs(env[SANDBOX_TOOL_BIN_DIRS_ENV])) {
43
+ for (const dir of toolBinDirs)
44
+ assertToolBinDir(dir);
45
+ const dirs = [...new Set(toolBinDirs)];
46
+ // No PATH to bind: the spawner appends SANDBOX_TOOL_BIN_DIRS to its default.
47
+ if (env.PATH === undefined) {
48
+ return { ...env, [SANDBOX_TOOL_BIN_DIRS_ENV]: dirs.join(":") };
49
+ }
50
+ const replaced = new Set(appToolBinDirs);
51
+ const entries = env.PATH.split(":").filter((entry) => isAbsolute(entry) && !replaced.has(entry));
52
+ for (const dir of dirs) {
53
+ if (!entries.includes(dir))
54
+ entries.push(dir);
55
+ }
56
+ return {
57
+ ...env,
58
+ PATH: entries.join(":"),
59
+ [SANDBOX_TOOL_BIN_DIRS_ENV]: dirs.join(":"),
60
+ };
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-profile-materialize",
3
- "version": "0.26.9",
3
+ "version": "0.27.1",
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": {
@@ -35,8 +35,8 @@
35
35
  "@tangle-network/agent-interface": "^3.0.0 || ^4.0.0 || ^5.0.0"
36
36
  },
37
37
  "tangleRelease": {
38
- "sourceSha": "be89262c7e1faaedf04926a27d0ecbf17d91a318",
39
- "mergedAt": "2026-10-19T23:54:34-06:00"
38
+ "sourceSha": "5a536a79fbd83dfa11da194bf5348cf0d73f224d",
39
+ "mergedAt": "2026-10-21T05:59:51-06:00"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "tsc -p tsconfig.json",