@inflexa-ai/harness 0.4.4 → 0.4.5

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.
@@ -44,7 +44,12 @@ export interface DockerClientConfig {
44
44
  resolveWorkspaceRoot: ResolveWorkspaceRoot;
45
45
  /** Host lib store; bind-mounted read-only at `/mnt/libs` when set. */
46
46
  libStorePath?: string;
47
- /** Host ref store; bind-mounted read-only at `/mnt/refs` when set. */
47
+ /**
48
+ * Host ref store; bind-mounted read-only at `/mnt/refs`. Embedders pass the
49
+ * configured store location unconditionally — existence is (re-)checked at each
50
+ * sandbox creation, so a store installed mid-session is mounted into subsequent
51
+ * sandboxes without a runtime restart.
52
+ */
48
53
  refStorePath?: string;
49
54
  /**
50
55
  * Force the container platform (e.g. `linux/amd64`). Set by hosts that mount
@@ -28,7 +28,7 @@
28
28
  * Either way the workload ends as uid 1000 with `no-new-privileges` and no
29
29
  * effective capabilities.
30
30
  */
31
- import { existsSync, statSync } from "node:fs";
31
+ import { existsSync, lstatSync, statSync } from "node:fs";
32
32
  import { join } from "node:path";
33
33
  import Docker from "dockerode";
34
34
  import { ResultAsync, err, ok } from "neverthrow";
@@ -80,6 +80,30 @@ function libStoreUsable(libStorePath) {
80
80
  }
81
81
  return existsSync(join(current, "packages.txt")) && existsSync(join(current, "meta.json"));
82
82
  }
83
+ /**
84
+ * Whether `refStorePath` is, right now, a real directory fit to be a bind authority.
85
+ * Deliberately shallow — it checks existence-as-a-directory only, never the store's
86
+ * interior: the harness knows nothing of the ref store's layout, and dataset
87
+ * completeness/receipts are the embedder's concern, so (unlike `libStoreUsable`, which
88
+ * validates harness-known `current` layout) there is nothing here to validate beyond
89
+ * "a directory is present."
90
+ *
91
+ * `lstatSync` (not `statSync`) does NOT follow symlinks, so a symlinked path is rejected
92
+ * even when it resolves to a directory: the bind authority must itself be the store
93
+ * directory, not an indirection that may later point elsewhere.
94
+ *
95
+ * Binding a missing source makes Docker auto-create a root-owned directory at the path —
96
+ * the same hazard `libStoreUsable` documents — which would also brick the embedder's
97
+ * later store install (its store tool cannot write into a root-owned dir).
98
+ */
99
+ function refStoreUsable(refStorePath) {
100
+ try {
101
+ return lstatSync(refStorePath).isDirectory();
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ }
83
107
  async function pollHealth(fetchImpl, url, timeoutMs) {
84
108
  const deadline = Date.now() + timeoutMs;
85
109
  let lastErr;
@@ -187,9 +211,16 @@ export function createDockerSandboxOps(config) {
187
211
  if (config.libStorePath && !libsMounted) {
188
212
  logger.warn("lib store configured but `current` is missing or incomplete at sandbox creation — mounting no library store (sandbox degrades to available:false)", { libStorePath: config.libStorePath, sandboxId });
189
213
  }
214
+ // Re-checked here too, so a ref store installed after boot is mounted into
215
+ // the next sandbox. Unlike libs, a missing ref store is NOT warned: a missing
216
+ // lib store is a degradation (it was configured and working, then broke),
217
+ // whereas a missing ref store is the normal cold state — the user simply has
218
+ // not installed reference data yet — and warning on every create would be
219
+ // noise. A store that appears mid-session is picked up by the next create.
220
+ const refsMounted = !!config.refStorePath && refStoreUsable(config.refStorePath);
190
221
  const plan = buildMountPlan(meta, {
191
222
  libs: libsMounted,
192
- refs: !!config.refStorePath,
223
+ refs: refsMounted,
193
224
  });
194
225
  const hostTreePath = config.resolveWorkspaceRoot(meta.analysisId);
195
226
  // `stepWritePrefix` (not a raw `join`) so the RW bind source runs through
@@ -200,7 +231,7 @@ export function createDockerSandboxOps(config) {
200
231
  `${hostTreePath}:${plan.readonlyTreePath}:ro`,
201
232
  ...(plan.writableStepPath ? [`${hostStepPath}:${plan.writableStepPath}:rw`] : []),
202
233
  ...(libsMounted && config.libStorePath ? [`${config.libStorePath}:${plan.libsPath}:ro`] : []),
203
- ...(config.refStorePath ? [`${config.refStorePath}:${plan.refsPath}:ro`] : []),
234
+ ...(refsMounted && config.refStorePath ? [`${config.refStorePath}:${plan.refsPath}:ro`] : []),
204
235
  ];
205
236
  const createFailed = (status, cause) => ({
206
237
  type: "container_create_failed",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Host-agnostic bioinformatics agent harness \u2014 hand-rolled agent loop, DBOS-durable workflows, sandboxed compute.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",