@inflexa-ai/harness 0.32.0 → 0.33.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/agents/conversation-agent.d.ts +8 -0
- package/dist/agents/conversation-agent.js +15 -6
- package/dist/agents/sandbox/shared.d.ts +13 -1
- package/dist/agents/sandbox/shared.js +22 -5
- package/dist/config/environment-stores.d.ts +53 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/prompts/briefing.d.ts +1 -1
- package/dist/prompts/briefing.js +4 -0
- package/dist/prompts/planner.js +13 -0
- package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multi-omics-integration-agent.js +5 -5
- package/dist/prompts/sandbox/network-agent.d.ts +1 -1
- package/dist/prompts/sandbox/network-agent.js +4 -3
- package/dist/prompts/sandbox-standards.d.ts +20 -0
- package/dist/prompts/sandbox-standards.js +65 -0
- package/dist/reference-data/catalog.js +11 -9
- package/dist/sandbox/create-sandbox.d.ts +29 -4
- package/dist/sandbox/create-sandbox.js +6 -0
- package/dist/sandbox/docker-client.d.ts +16 -6
- package/dist/sandbox/docker-client.js +47 -31
- package/dist/sandbox/farm.d.ts +95 -0
- package/dist/sandbox/farm.js +127 -0
- package/dist/sandbox/k8s-client.d.ts +24 -5
- package/dist/sandbox/k8s-client.js +78 -9
- package/dist/sandbox/mount-plan.d.ts +23 -1
- package/dist/sandbox/mount-plan.js +44 -7
- package/dist/sandbox/sandbox-error.d.ts +12 -0
- package/dist/sandbox/sandbox-error.js +2 -0
- package/dist/sandbox/types.d.ts +118 -0
- package/dist/schemas/plan-schemas.d.ts +3 -0
- package/dist/schemas/plan-schemas.js +1 -1
- package/dist/schemas/validate-plan.js +20 -1
- package/dist/schemas/workflow-state.d.ts +4 -0
- package/dist/schemas/workflow-state.js +10 -0
- package/dist/tasks/data-profile.d.ts +15 -0
- package/dist/tasks/data-profile.js +36 -26
- package/dist/tools/execute-analysis.d.ts +18 -0
- package/dist/tools/execute-analysis.js +87 -0
- package/dist/tools/research/generate-plan.js +12 -2
- package/dist/tools/sandbox/index.d.ts +1 -0
- package/dist/tools/sandbox/index.js +1 -0
- package/dist/tools/sandbox/link-packages.d.ts +27 -0
- package/dist/tools/sandbox/link-packages.js +52 -0
- package/dist/tools/sandbox/list-available-packages.d.ts +50 -30
- package/dist/tools/sandbox/list-available-packages.js +131 -40
- package/package.json +2 -2
|
@@ -20,7 +20,7 @@ import { clearSandboxRef } from "../state/index.js";
|
|
|
20
20
|
import { type AwaitExecOptions } from "./await-exec.js";
|
|
21
21
|
import type { SandboxClient } from "./client.js";
|
|
22
22
|
import { type SubmitExecDeps } from "./submit-exec.js";
|
|
23
|
-
import { type CreateSandboxMeta, type SandboxTransport } from "./types.js";
|
|
23
|
+
import { type CreateSandboxMeta, type FarmSource, type SandboxTransport, type ToolchainSource } from "./types.js";
|
|
24
24
|
/**
|
|
25
25
|
* Narrow config slice the sandbox factory reads off `Env` — the backend
|
|
26
26
|
* default and the K8s namespace fallback. Composition roots map their
|
|
@@ -65,6 +65,21 @@ export interface CreateSandboxClientConfig {
|
|
|
65
65
|
resolveWorkspaceRoot: ResolveWorkspaceRoot;
|
|
66
66
|
/** Docker: host dir bind-mounted read-only at `/mnt/libs`. */
|
|
67
67
|
libStorePath?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Where the farm of an analysis comes from. Required of every embedder —
|
|
70
|
+
* the harness never invents a farm location, and it reads no store-root
|
|
71
|
+
* `current` pointer. On Docker, `FarmLocation.farmPath` is a host
|
|
72
|
+
* directory. On K8s, it is a `subPath` into `libStorePvc`. Consulted only
|
|
73
|
+
* when a store is configured.
|
|
74
|
+
*/
|
|
75
|
+
farmSource: FarmSource;
|
|
76
|
+
/**
|
|
77
|
+
* The declared toolchain owner. `"image"` keys the resolver env and the
|
|
78
|
+
* orient-core prompt text onto the image-owned toolchain. Absent means
|
|
79
|
+
* `"store"`: the legacy environment, byte-identical to the one before
|
|
80
|
+
* this field existed.
|
|
81
|
+
*/
|
|
82
|
+
toolchainSource?: ToolchainSource;
|
|
68
83
|
/** Docker: host dir bind-mounted read-only at `/mnt/refs`. */
|
|
69
84
|
refStorePath?: string;
|
|
70
85
|
/** Docker: force the container platform (e.g. `linux/amd64`) so the sandbox matches the mounted lib store's arch. */
|
|
@@ -98,6 +113,15 @@ export interface CreateSandboxClientConfig {
|
|
|
98
113
|
sessionPvcRoot?: string;
|
|
99
114
|
/** K8s: PVC claim mounted read-only at `/mnt/libs`. */
|
|
100
115
|
libStorePvc?: string;
|
|
116
|
+
/**
|
|
117
|
+
* K8s: absolute mountpoint of `libStorePvc` on this process's filesystem,
|
|
118
|
+
* for a host that mounts the same volume. With it set, the backend proves
|
|
119
|
+
* the `inflexa.lock` of the resolved farm before the mount. Under a
|
|
120
|
+
* `fixed` farm source no resolver exists, thus this root carries the only
|
|
121
|
+
* lock gate that shape can have. Without it, the proof duty stays whole
|
|
122
|
+
* on the farm resolver.
|
|
123
|
+
*/
|
|
124
|
+
libStorePvcRoot?: string;
|
|
101
125
|
/** K8s: PVC claim mounted read-only at `/mnt/refs`. */
|
|
102
126
|
refStorePvc?: string;
|
|
103
127
|
/** K8s: node selector pinning sandbox pods to the dedicated agent pool. */
|
|
@@ -109,9 +133,10 @@ export interface CreateSandboxClientConfig {
|
|
|
109
133
|
/** Override the backend selection — defaults to `env.SANDBOX_BACKEND`. */
|
|
110
134
|
backend?: "docker" | "k8s";
|
|
111
135
|
/**
|
|
112
|
-
* Optional logger forwarded to the Docker backend so a
|
|
113
|
-
* (a
|
|
114
|
-
* observable rather than a silent mount drop. No-op when
|
|
136
|
+
* Optional logger forwarded to the Docker backend so a store degradation
|
|
137
|
+
* (a resolved farm whose `inflexa.lock` vanished or went invalid
|
|
138
|
+
* mid-session) is observable rather than a silent mount drop. No-op when
|
|
139
|
+
* unset.
|
|
115
140
|
*/
|
|
116
141
|
logger?: Logger;
|
|
117
142
|
/** Dependency seams (fetch, durable step/sleep, clock, recv, warn sink) forwarded to submit/await. */
|
|
@@ -86,10 +86,14 @@ export function createSandboxClient(config) {
|
|
|
86
86
|
sessionPvcRoot: config.sessionPvcRoot,
|
|
87
87
|
resolveWorkspaceRoot: config.resolveWorkspaceRoot,
|
|
88
88
|
libStorePvc: config.libStorePvc,
|
|
89
|
+
libStorePvcRoot: config.libStorePvcRoot,
|
|
90
|
+
farmSource: config.farmSource,
|
|
91
|
+
toolchainSource: config.toolchainSource,
|
|
89
92
|
refStorePvc: config.refStorePvc,
|
|
90
93
|
nodeSelector: config.nodeSelector,
|
|
91
94
|
tolerations: config.tolerations,
|
|
92
95
|
runtimeClassName: config.runtimeClassName,
|
|
96
|
+
logger: config.logger,
|
|
93
97
|
registerSandbox,
|
|
94
98
|
})
|
|
95
99
|
: createDockerSandboxOps({
|
|
@@ -98,6 +102,8 @@ export function createSandboxClient(config) {
|
|
|
98
102
|
transport,
|
|
99
103
|
resolveWorkspaceRoot: config.resolveWorkspaceRoot,
|
|
100
104
|
libStorePath: config.libStorePath,
|
|
105
|
+
farmSource: config.farmSource,
|
|
106
|
+
toolchainSource: config.toolchainSource,
|
|
101
107
|
refStorePath: config.refStorePath,
|
|
102
108
|
platform: config.platform,
|
|
103
109
|
engineSocketPath: config.engineSocketPath,
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Thin wrapper around `dockerode`. Storage is wired via `HostConfig.Binds`:
|
|
5
5
|
* a flat read-only mount of the analysis tree at `/{resourceId}`, a nested
|
|
6
|
-
* read-write mount of the step's artifact dir,
|
|
7
|
-
* `/mnt/libs`
|
|
8
|
-
*
|
|
6
|
+
* read-write mount of the step's artifact dir, the package store at
|
|
7
|
+
* `/mnt/libs` with the resolved farm nested inside it, and the ref store at
|
|
8
|
+
* `/mnt/refs` when its host path is configured. Container paths and
|
|
9
|
+
* lib-store env come from the shared mount plan (`mount-plan.ts`).
|
|
9
10
|
*
|
|
10
11
|
* ## The cpu files
|
|
11
12
|
*
|
|
@@ -42,7 +43,7 @@ import { ResultAsync } from "neverthrow";
|
|
|
42
43
|
import type { Logger } from "../lib/logger.js";
|
|
43
44
|
import { type ResolveWorkspaceRoot } from "../workspace/paths.js";
|
|
44
45
|
import { type SandboxError } from "./sandbox-error.js";
|
|
45
|
-
import type { CreateSandboxMeta, ManagedSandbox, SandboxIdentity, SandboxLiveness, SandboxRef, SandboxTransport } from "./types.js";
|
|
46
|
+
import type { CreateSandboxMeta, FarmSource, ManagedSandbox, SandboxIdentity, SandboxLiveness, SandboxRef, SandboxTransport, ToolchainSource } from "./types.js";
|
|
46
47
|
export interface DockerClientConfig {
|
|
47
48
|
image: string;
|
|
48
49
|
/** Cortex base URL injected into the sandbox env in callback mode so callbacks land here. Unused in poll mode. */
|
|
@@ -53,6 +54,15 @@ export interface DockerClientConfig {
|
|
|
53
54
|
resolveWorkspaceRoot: ResolveWorkspaceRoot;
|
|
54
55
|
/** Host lib store; bind-mounted read-only at `/mnt/libs` when set. */
|
|
55
56
|
libStorePath?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Where the farm of an analysis comes from. Required — the harness never
|
|
59
|
+
* invents a farm location. `FarmLocation.farmPath` is a host directory
|
|
60
|
+
* here, bind-mounted read-only at the farm container path. Consulted only
|
|
61
|
+
* when `libStorePath` is set.
|
|
62
|
+
*/
|
|
63
|
+
farmSource: FarmSource;
|
|
64
|
+
/** The declared toolchain owner. Absent means `"store"`, the legacy environment. */
|
|
65
|
+
toolchainSource?: ToolchainSource;
|
|
56
66
|
/**
|
|
57
67
|
* Host ref store; bind-mounted read-only at `/mnt/refs`. Embedders pass the
|
|
58
68
|
* configured store location unconditionally — existence is (re-)checked at each
|
|
@@ -85,8 +95,8 @@ export interface DockerClientConfig {
|
|
|
85
95
|
*/
|
|
86
96
|
readHostCpuinfo?: () => Promise<string | undefined>;
|
|
87
97
|
/**
|
|
88
|
-
* Optional logger so a
|
|
89
|
-
*
|
|
98
|
+
* Optional logger so a store degradation (a resolved farm whose `inflexa.lock`
|
|
99
|
+
* is absent or invalid by sandbox-create time) is observable instead of a
|
|
90
100
|
* silent libs-mount drop. Matches the `reaper`/`watchdog` logger seam.
|
|
91
101
|
*/
|
|
92
102
|
logger?: Logger;
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Thin wrapper around `dockerode`. Storage is wired via `HostConfig.Binds`:
|
|
5
5
|
* a flat read-only mount of the analysis tree at `/{resourceId}`, a nested
|
|
6
|
-
* read-write mount of the step's artifact dir,
|
|
7
|
-
* `/mnt/libs`
|
|
8
|
-
*
|
|
6
|
+
* read-write mount of the step's artifact dir, the package store at
|
|
7
|
+
* `/mnt/libs` with the resolved farm nested inside it, and the ref store at
|
|
8
|
+
* `/mnt/refs` when its host path is configured. Container paths and
|
|
9
|
+
* lib-store env come from the shared mount plan (`mount-plan.ts`).
|
|
9
10
|
*
|
|
10
11
|
* ## The cpu files
|
|
11
12
|
*
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
* Either way the workload ends as uid 1000 with `no-new-privileges` and no
|
|
38
39
|
* effective capabilities.
|
|
39
40
|
*/
|
|
40
|
-
import {
|
|
41
|
+
import { lstatSync } from "node:fs";
|
|
41
42
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
42
43
|
import { dirname, join } from "node:path";
|
|
43
44
|
import Docker from "dockerode";
|
|
@@ -45,6 +46,7 @@ import { ResultAsync, err, ok } from "neverthrow";
|
|
|
45
46
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
46
47
|
import { tailWritePrefix } from "../workspace/paths.js";
|
|
47
48
|
import { trySandbox } from "./sandbox-error.js";
|
|
49
|
+
import { readFarmLock, resolveFarmSource } from "./farm.js";
|
|
48
50
|
import { buildMountPlan, sandboxWriteTail } from "./mount-plan.js";
|
|
49
51
|
import { CPU_ONLINE_PATH, CPUINFO_PATH, cpuFiles, readHostCpuinfoOnce } from "./cpu-files.js";
|
|
50
52
|
import { threadLimitEnv } from "./thread-env.js";
|
|
@@ -74,26 +76,6 @@ const RUN_ID_LABEL = "cortex/run-id";
|
|
|
74
76
|
const STEP_ID_LABEL = "cortex/step-id";
|
|
75
77
|
/** The directory next to the workspace root that holds the cpu files of each sandbox. */
|
|
76
78
|
const CPU_FILES_DIR = ".cpu";
|
|
77
|
-
/**
|
|
78
|
-
* Whether the lib store's `current` resolves to a COMPLETE, usable version, not merely a
|
|
79
|
-
* present symlink. By sandbox-create time `current` may be gone (concurrent prune/`rm`), a
|
|
80
|
-
* dangling symlink (its target pruned), or an incomplete tree. Binding a missing source
|
|
81
|
-
* makes Docker auto-create a root-owned dir (bricking a later store refresh); binding a broken
|
|
82
|
-
* one mounts broken content. Require a resolved directory carrying both completeness
|
|
83
|
-
* markers `activate` writes before it flips the pointer.
|
|
84
|
-
*/
|
|
85
|
-
function libStoreUsable(libStorePath) {
|
|
86
|
-
const current = join(libStorePath, "current");
|
|
87
|
-
try {
|
|
88
|
-
// statSync FOLLOWS the symlink, so a dangling `current` throws here and is rejected.
|
|
89
|
-
if (!statSync(current).isDirectory())
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
return existsSync(join(current, "packages.txt")) && existsSync(join(current, "meta.json"));
|
|
96
|
-
}
|
|
97
79
|
/**
|
|
98
80
|
* Whether `refStorePath` is, right now, a real directory fit to be a bind authority.
|
|
99
81
|
* Deliberately shallow — it checks existence-as-a-directory only, never the store's
|
|
@@ -253,13 +235,41 @@ export function createDockerSandboxOps(config) {
|
|
|
253
235
|
createSandbox(meta, identity) {
|
|
254
236
|
return new ResultAsync((async () => {
|
|
255
237
|
const { sandboxId, callbackSecret } = identity;
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
238
|
+
// Farm resolution comes before any container work: a resolver
|
|
239
|
+
// refusal and a resolver throw refuse the whole call, and no
|
|
240
|
+
// container is made. The reason of the embedder rides in the error.
|
|
241
|
+
let farm;
|
|
242
|
+
if (config.libStorePath) {
|
|
243
|
+
const resolved = await resolveFarmSource(config.farmSource, meta.analysisId, "docker.createSandbox");
|
|
244
|
+
if (resolved.isErr())
|
|
245
|
+
return err(resolved.error);
|
|
246
|
+
farm = resolved.value;
|
|
247
|
+
}
|
|
248
|
+
// The `inflexa.lock` gate runs at each create: the store may have
|
|
249
|
+
// gone away, or the farm may be half-written. A gate failure
|
|
250
|
+
// degrades — both store mounts drop, the container is still made,
|
|
251
|
+
// and the sandbox reports the store as unavailable. The warning is
|
|
252
|
+
// logged because an otherwise-silent mount drop is invisible to
|
|
253
|
+
// operators.
|
|
254
|
+
let libsMounted = false;
|
|
255
|
+
if (config.libStorePath && farm) {
|
|
256
|
+
const lock = readFarmLock(farm.farmPath);
|
|
257
|
+
libsMounted = lock.isOk();
|
|
258
|
+
if (lock.isErr()) {
|
|
259
|
+
logger.warn("the farm of the analysis has no usable inflexa.lock at sandbox creation — mounting no package store (sandbox degrades to available:false)", { libStorePath: config.libStorePath, farmPath: farm.farmPath, lockError: lock.error.type, sandboxId });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// The cache path is the one farm value with no gate of its
|
|
263
|
+
// own: the lock gate proves `farmPath`, and this bind string
|
|
264
|
+
// would carry a malformed cache path verbatim. An unusable
|
|
265
|
+
// value degrades to no cache mount — the rule of a missing
|
|
266
|
+
// one — because the entrypoint fallback covers the caches.
|
|
267
|
+
const cacheBindable = farm?.cachePath !== undefined && farm.cachePath.startsWith("/") && !farm.cachePath.includes(":");
|
|
268
|
+
if (farm?.cachePath !== undefined && !cacheBindable) {
|
|
269
|
+
logger.warn("the farm cache path cannot ride a docker bind — mounting no cache", {
|
|
270
|
+
cachePath: farm.cachePath,
|
|
271
|
+
sandboxId,
|
|
272
|
+
});
|
|
263
273
|
}
|
|
264
274
|
// Re-checked here too, so a ref store installed after boot is mounted into
|
|
265
275
|
// the next sandbox. Unlike libs, a missing ref store is NOT warned: a missing
|
|
@@ -271,6 +281,8 @@ export function createDockerSandboxOps(config) {
|
|
|
271
281
|
const plan = buildMountPlan(meta, {
|
|
272
282
|
libs: libsMounted,
|
|
273
283
|
refs: refsMounted,
|
|
284
|
+
toolchainSource: config.toolchainSource,
|
|
285
|
+
cache: cacheBindable,
|
|
274
286
|
});
|
|
275
287
|
const hostTreePath = config.resolveWorkspaceRoot(meta.analysisId);
|
|
276
288
|
// `tailWritePrefix` (not a raw `join`) so the RW bind source runs through
|
|
@@ -290,12 +302,16 @@ export function createDockerSandboxOps(config) {
|
|
|
290
302
|
const cpuBinds = await writeCpuFiles(cpuDir, cpuFiles(threads, cpuinfo), logger, sandboxId);
|
|
291
303
|
if (cpuBinds.length > 0)
|
|
292
304
|
cpuDirs.set(sandboxId, cpuDir);
|
|
305
|
+
// The farm bind comes after the store bind, thus the nesting is
|
|
306
|
+
// stable; the cache bind comes after the two store binds.
|
|
293
307
|
const binds = [
|
|
294
308
|
`${hostTreePath}:${plan.readonlyTreePath}:ro`,
|
|
295
309
|
...(write && plan.writableStepPath
|
|
296
310
|
? [`${tailWritePrefix({ workspaceRoot: hostTreePath, tail: write.tail })}:${plan.writableStepPath}:rw`]
|
|
297
311
|
: []),
|
|
298
312
|
...(libsMounted && config.libStorePath ? [`${config.libStorePath}:${plan.libsPath}:ro`] : []),
|
|
313
|
+
...(libsMounted && farm && plan.farmPath ? [`${farm.farmPath}:${plan.farmPath}:ro`] : []),
|
|
314
|
+
...(libsMounted && cacheBindable && farm?.cachePath && plan.cachePath ? [`${farm.cachePath}:${plan.cachePath}:rw`] : []),
|
|
299
315
|
...(refsMounted && config.refStorePath ? [`${config.refStorePath}:${plan.refsPath}:ro`] : []),
|
|
300
316
|
...cpuBinds,
|
|
301
317
|
];
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Farm-source resolution and the `inflexa.lock` farm contract.
|
|
3
|
+
*
|
|
4
|
+
* A farm carries exactly one metadata file, `inflexa.lock`, beside its link
|
|
5
|
+
* trees and its caches (the lib-store spec). The mount gate and the package
|
|
6
|
+
* inventory read this one file — `packages.txt`, `meta.json`, and
|
|
7
|
+
* `lock.json` are not part of the farm contract.
|
|
8
|
+
*
|
|
9
|
+
* The resolution and the gate refuse differently, on purpose:
|
|
10
|
+
* - A resolver refusal (`unavailable`, or a throw) is a `farm_unavailable`
|
|
11
|
+
* `SandboxError` — the call refuses and no container is made.
|
|
12
|
+
* - A lock-gate failure degrades — the backend drops both store mounts,
|
|
13
|
+
* logs a warning, and still makes the container.
|
|
14
|
+
*/
|
|
15
|
+
import { type Result } from "neverthrow";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import type { SandboxError } from "./sandbox-error.js";
|
|
18
|
+
import type { FarmLocation, FarmSource } from "./types.js";
|
|
19
|
+
/** The one metadata file of a farm. */
|
|
20
|
+
export declare const FARM_LOCK_FILE = "inflexa.lock";
|
|
21
|
+
/** One linked distribution. `requested` obeys the PEP 376 meaning: true for a direct ask, false for a transitive dependency. */
|
|
22
|
+
export declare const FarmLockPackageSchema: z.ZodObject<{
|
|
23
|
+
name: z.ZodString;
|
|
24
|
+
version: z.ZodString;
|
|
25
|
+
track: z.ZodString;
|
|
26
|
+
store_dir: z.ZodString;
|
|
27
|
+
hash: z.ZodString;
|
|
28
|
+
requested: z.ZodBoolean;
|
|
29
|
+
}, z.core.$loose>;
|
|
30
|
+
/**
|
|
31
|
+
* `inflexa.lock` at schema version 1. Additive fields pass through — a
|
|
32
|
+
* breaking change to the shape must move the schema number instead.
|
|
33
|
+
*/
|
|
34
|
+
export declare const FarmLockSchema: z.ZodObject<{
|
|
35
|
+
schema: z.ZodLiteral<1>;
|
|
36
|
+
arch: z.ZodEnum<{
|
|
37
|
+
amd64: "amd64";
|
|
38
|
+
arm64: "arm64";
|
|
39
|
+
}>;
|
|
40
|
+
packages: z.ZodArray<z.ZodObject<{
|
|
41
|
+
name: z.ZodString;
|
|
42
|
+
version: z.ZodString;
|
|
43
|
+
track: z.ZodString;
|
|
44
|
+
store_dir: z.ZodString;
|
|
45
|
+
hash: z.ZodString;
|
|
46
|
+
requested: z.ZodBoolean;
|
|
47
|
+
}, z.core.$loose>>;
|
|
48
|
+
languages: z.ZodObject<{
|
|
49
|
+
python: z.ZodOptional<z.ZodObject<{
|
|
50
|
+
version: z.ZodString;
|
|
51
|
+
index: z.ZodString;
|
|
52
|
+
}, z.core.$loose>>;
|
|
53
|
+
r: z.ZodOptional<z.ZodObject<{
|
|
54
|
+
version: z.ZodString;
|
|
55
|
+
bioc_releases: z.ZodArray<z.ZodString>;
|
|
56
|
+
pak_lock: z.ZodOptional<z.ZodUnknown>;
|
|
57
|
+
}, z.core.$loose>>;
|
|
58
|
+
}, z.core.$loose>;
|
|
59
|
+
warm: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
60
|
+
script_sha256: z.ZodString;
|
|
61
|
+
cache_entries: z.ZodArray<z.ZodString>;
|
|
62
|
+
}, z.core.$loose>>>;
|
|
63
|
+
merge_conflicts: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
64
|
+
entry: z.ZodString;
|
|
65
|
+
action: z.ZodEnum<{
|
|
66
|
+
skipped: "skipped";
|
|
67
|
+
"kept-first": "kept-first";
|
|
68
|
+
}>;
|
|
69
|
+
}, z.core.$loose>>>;
|
|
70
|
+
}, z.core.$loose>;
|
|
71
|
+
export type FarmLock = z.infer<typeof FarmLockSchema>;
|
|
72
|
+
export type FarmLockError = {
|
|
73
|
+
readonly type: "lock_unreadable";
|
|
74
|
+
readonly lockPath: string;
|
|
75
|
+
readonly cause: unknown;
|
|
76
|
+
} | {
|
|
77
|
+
readonly type: "lock_invalid";
|
|
78
|
+
readonly lockPath: string;
|
|
79
|
+
readonly cause: unknown;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Read and validate the `inflexa.lock` of a farm directory. The mount gate
|
|
83
|
+
* accepts a farm only when this returns ok — a farm whose lock is absent,
|
|
84
|
+
* unparseable, or at an unknown schema version is not usable.
|
|
85
|
+
*/
|
|
86
|
+
export declare function readFarmLock(farmPath: string): Result<FarmLock, FarmLockError>;
|
|
87
|
+
/** Read and validate one `inflexa.lock` by its file path. The package inventory reads through this too. */
|
|
88
|
+
export declare function readFarmLockFile(lockPath: string): Result<FarmLock, FarmLockError>;
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the farm source of one analysis. Runs before any container work.
|
|
91
|
+
* `fixed` answers at once. `per-analysis` calls the resolver of the
|
|
92
|
+
* embedder — an `unavailable` result and a throw both refuse the call with
|
|
93
|
+
* `farm_unavailable`, and the reason of the embedder rides in the error.
|
|
94
|
+
*/
|
|
95
|
+
export declare function resolveFarmSource(source: FarmSource, analysisId: string, op: string): Promise<Result<FarmLocation, SandboxError>>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Farm-source resolution and the `inflexa.lock` farm contract.
|
|
3
|
+
*
|
|
4
|
+
* A farm carries exactly one metadata file, `inflexa.lock`, beside its link
|
|
5
|
+
* trees and its caches (the lib-store spec). The mount gate and the package
|
|
6
|
+
* inventory read this one file — `packages.txt`, `meta.json`, and
|
|
7
|
+
* `lock.json` are not part of the farm contract.
|
|
8
|
+
*
|
|
9
|
+
* The resolution and the gate refuse differently, on purpose:
|
|
10
|
+
* - A resolver refusal (`unavailable`, or a throw) is a `farm_unavailable`
|
|
11
|
+
* `SandboxError` — the call refuses and no container is made.
|
|
12
|
+
* - A lock-gate failure degrades — the backend drops both store mounts,
|
|
13
|
+
* logs a warning, and still makes the container.
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { err, ok } from "neverthrow";
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
/** The one metadata file of a farm. */
|
|
20
|
+
export const FARM_LOCK_FILE = "inflexa.lock";
|
|
21
|
+
/** One linked distribution. `requested` obeys the PEP 376 meaning: true for a direct ask, false for a transitive dependency. */
|
|
22
|
+
export const FarmLockPackageSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
name: z.string(),
|
|
25
|
+
version: z.string(),
|
|
26
|
+
/** The link subtree of the package (for example `python`, `cran`, `bioconductor`). */
|
|
27
|
+
track: z.string(),
|
|
28
|
+
store_dir: z.string(),
|
|
29
|
+
/** The full sha256 of the sorted store tree. */
|
|
30
|
+
hash: z.string(),
|
|
31
|
+
requested: z.boolean(),
|
|
32
|
+
})
|
|
33
|
+
.passthrough();
|
|
34
|
+
/**
|
|
35
|
+
* `inflexa.lock` at schema version 1. Additive fields pass through — a
|
|
36
|
+
* breaking change to the shape must move the schema number instead.
|
|
37
|
+
*/
|
|
38
|
+
export const FarmLockSchema = z
|
|
39
|
+
.object({
|
|
40
|
+
schema: z.literal(1),
|
|
41
|
+
arch: z.enum(["amd64", "arm64"]),
|
|
42
|
+
packages: z.array(FarmLockPackageSchema),
|
|
43
|
+
/** Per-language provenance. Each language object owns its own fields. */
|
|
44
|
+
languages: z
|
|
45
|
+
.object({
|
|
46
|
+
python: z.object({ version: z.string(), index: z.string() }).passthrough().optional(),
|
|
47
|
+
r: z
|
|
48
|
+
.object({
|
|
49
|
+
version: z.string(),
|
|
50
|
+
bioc_releases: z.array(z.string()),
|
|
51
|
+
/** The embedded pak lock of the build, verbatim. */
|
|
52
|
+
pak_lock: z.unknown().optional(),
|
|
53
|
+
})
|
|
54
|
+
.passthrough()
|
|
55
|
+
.optional(),
|
|
56
|
+
})
|
|
57
|
+
.passthrough(),
|
|
58
|
+
/** The warm replay record. Present in the catalog farm only. */
|
|
59
|
+
warm: z
|
|
60
|
+
.record(z.string(), z
|
|
61
|
+
.object({
|
|
62
|
+
script_sha256: z.string(),
|
|
63
|
+
cache_entries: z.array(z.string()),
|
|
64
|
+
})
|
|
65
|
+
.passthrough())
|
|
66
|
+
.optional(),
|
|
67
|
+
/** The top-level entries that the link merge kept-first or skipped. */
|
|
68
|
+
merge_conflicts: z
|
|
69
|
+
.array(z
|
|
70
|
+
.object({
|
|
71
|
+
entry: z.string(),
|
|
72
|
+
action: z.enum(["kept-first", "skipped"]),
|
|
73
|
+
})
|
|
74
|
+
.passthrough())
|
|
75
|
+
.optional(),
|
|
76
|
+
})
|
|
77
|
+
.passthrough();
|
|
78
|
+
/**
|
|
79
|
+
* Read and validate the `inflexa.lock` of a farm directory. The mount gate
|
|
80
|
+
* accepts a farm only when this returns ok — a farm whose lock is absent,
|
|
81
|
+
* unparseable, or at an unknown schema version is not usable.
|
|
82
|
+
*/
|
|
83
|
+
export function readFarmLock(farmPath) {
|
|
84
|
+
return readFarmLockFile(join(farmPath, FARM_LOCK_FILE));
|
|
85
|
+
}
|
|
86
|
+
/** Read and validate one `inflexa.lock` by its file path. The package inventory reads through this too. */
|
|
87
|
+
export function readFarmLockFile(lockPath) {
|
|
88
|
+
let raw;
|
|
89
|
+
try {
|
|
90
|
+
raw = readFileSync(lockPath, "utf8");
|
|
91
|
+
}
|
|
92
|
+
catch (cause) {
|
|
93
|
+
return err({ type: "lock_unreadable", lockPath, cause });
|
|
94
|
+
}
|
|
95
|
+
let parsed;
|
|
96
|
+
try {
|
|
97
|
+
parsed = JSON.parse(raw);
|
|
98
|
+
}
|
|
99
|
+
catch (cause) {
|
|
100
|
+
return err({ type: "lock_invalid", lockPath, cause });
|
|
101
|
+
}
|
|
102
|
+
const lock = FarmLockSchema.safeParse(parsed);
|
|
103
|
+
if (!lock.success) {
|
|
104
|
+
return err({ type: "lock_invalid", lockPath, cause: lock.error });
|
|
105
|
+
}
|
|
106
|
+
return ok(lock.data);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Resolve the farm source of one analysis. Runs before any container work.
|
|
110
|
+
* `fixed` answers at once. `per-analysis` calls the resolver of the
|
|
111
|
+
* embedder — an `unavailable` result and a throw both refuse the call with
|
|
112
|
+
* `farm_unavailable`, and the reason of the embedder rides in the error.
|
|
113
|
+
*/
|
|
114
|
+
export async function resolveFarmSource(source, analysisId, op) {
|
|
115
|
+
if (source.kind === "fixed")
|
|
116
|
+
return ok(source.location);
|
|
117
|
+
try {
|
|
118
|
+
const resolution = await source.resolve(analysisId);
|
|
119
|
+
if (resolution.kind === "unavailable") {
|
|
120
|
+
return err({ type: "farm_unavailable", op, analysisId, reason: resolution.reason });
|
|
121
|
+
}
|
|
122
|
+
return ok(resolution.location);
|
|
123
|
+
}
|
|
124
|
+
catch (cause) {
|
|
125
|
+
return err({ type: "farm_unavailable", op, analysisId, reason: "the farm resolver threw", cause });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -6,16 +6,17 @@
|
|
|
6
6
|
* Storage is wired via the shared session PVC: a flat read-only `volumeMount`
|
|
7
7
|
* of the analysis tree at `/{resourceId}` plus a nested read-write mount of the
|
|
8
8
|
* step's artifact dir, with the lib/ref stores mounted read-only at `/mnt/libs`
|
|
9
|
-
* / `/mnt/refs` when their PVCs are set.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* / `/mnt/refs` when their PVCs are set. The farm of the analysis mounts as a
|
|
10
|
+
* read-only `subPath` of the libs PVC, after the store mount. Container paths
|
|
11
|
+
* and lib-store env come from the shared mount plan; the PVC `subPath`s are
|
|
12
|
+
* derived from the same `resolveWorkspaceRoot` seam the harness pre-creates
|
|
13
|
+
* the step tree under, so both sides address one directory by construction.
|
|
13
14
|
*/
|
|
14
15
|
import { BatchV1Api, CoreV1Api, type V1Toleration } from "@kubernetes/client-node";
|
|
15
16
|
import { ResultAsync } from "neverthrow";
|
|
16
17
|
import type { ResolveWorkspaceRoot } from "../workspace/paths.js";
|
|
17
18
|
import { type SandboxError } from "./sandbox-error.js";
|
|
18
|
-
import type { CreateSandboxMeta, ManagedSandbox, SandboxIdentity, SandboxLiveness, SandboxRef, SandboxTransport } from "./types.js";
|
|
19
|
+
import type { CreateSandboxMeta, FarmSource, ManagedSandbox, SandboxIdentity, SandboxLiveness, SandboxRef, SandboxTransport, ToolchainSource } from "./types.js";
|
|
19
20
|
import type { Logger } from "../lib/logger.js";
|
|
20
21
|
/**
|
|
21
22
|
* Coerce an arbitrary identifier into a valid K8s label value:
|
|
@@ -52,6 +53,24 @@ export interface K8sClientConfig {
|
|
|
52
53
|
resolveWorkspaceRoot: ResolveWorkspaceRoot;
|
|
53
54
|
/** PVC claim mounted read-only at `/mnt/libs` when set. */
|
|
54
55
|
libStorePvc?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Absolute mountpoint of `libStorePvc` on THIS process's filesystem, for
|
|
58
|
+
* a host that mounts the same volume (cortex mounts it at `/mnt/libs`).
|
|
59
|
+
* With the root set, the backend proves the `inflexa.lock` of the
|
|
60
|
+
* resolved farm before the mount — the same gate, and the same degrade,
|
|
61
|
+
* as the Docker backend. Without it, the proof duty stays whole on the
|
|
62
|
+
* farm resolver — see {@link ResolveAnalysisFarm}.
|
|
63
|
+
*/
|
|
64
|
+
libStorePvcRoot?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Where the farm of an analysis comes from. Required — the harness never
|
|
67
|
+
* invents a farm location. The farm is a `subPath` into `libStorePvc`,
|
|
68
|
+
* thus `FarmLocation.farmPath` is PVC-relative here. Consulted only when
|
|
69
|
+
* `libStorePvc` is set.
|
|
70
|
+
*/
|
|
71
|
+
farmSource: FarmSource;
|
|
72
|
+
/** The declared toolchain owner. Absent means `"store"`, the legacy environment. */
|
|
73
|
+
toolchainSource?: ToolchainSource;
|
|
55
74
|
/** PVC claim mounted read-only at `/mnt/refs` when set. */
|
|
56
75
|
refStorePvc?: string;
|
|
57
76
|
/** Node selector pinning sandbox pods to the dedicated agent pool. Omit for default scheduling. */
|