@inflexa-ai/harness 0.43.0 → 0.44.0

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.
@@ -94,7 +94,7 @@ export interface ReportSessionAgentDeps {
94
94
  readonly makeResolver?: (scope: {
95
95
  analysisId: string;
96
96
  auth: AuthContext;
97
- }) => ReferenceResolver;
97
+ }) => Promise<ReferenceResolver>;
98
98
  /**
99
99
  * Page-asset lookup -- the preview tool maps the module specifier of a manifest
100
100
  * entry onto a file on disk. Omitted falls back to the module resolution of the
@@ -26,7 +26,7 @@ export async function synthesizeRun(deps, params) {
26
26
  const { analysisId, runId, completedSteps, session, emit, onProgress } = params;
27
27
  await onProgress("starting", "Beginning literature-grounded synthesis");
28
28
  try {
29
- const workspaceRoot = resolveWorkspaceRoot(analysisId);
29
+ const workspaceRoot = await resolveWorkspaceRoot(analysisId);
30
30
  const summaries = await loadStepSummariesFromDisk({
31
31
  workspaceRoot,
32
32
  runId,
@@ -116,7 +116,7 @@ export function describeStepRegistrationFailure(failure) {
116
116
  export async function reconcileAndRegisterStepArtifacts(deps, postCtx, manifest) {
117
117
  const { input, session, lineageCollector } = postCtx;
118
118
  const reconciled = await reconcileManifestWithDisk({
119
- workspaceRoot: deps.resolveWorkspaceRoot(input.analysisId),
119
+ workspaceRoot: await deps.resolveWorkspaceRoot(input.analysisId),
120
120
  resourceId: input.analysisId,
121
121
  runId: input.runId,
122
122
  stepId: input.stepId,
package/dist/index.d.ts CHANGED
@@ -184,8 +184,8 @@ export type { CortexRunRow, StepExecutionRow, RunStatus } from "./state/schema.j
184
184
  export type { DbError } from "./lib/db-result.js";
185
185
  export { REFERENCE_DATA_CATALOG, REFERENCE_DATA_CATALOG_VERSION, ReferenceArtifactPathSchema, ReferenceArtifactSchema, ReferenceDataCatalogSchema, ReferenceDatasetSchema, ReferenceSha256Schema, UnknownReferenceDatasetError, referenceArtifactKey, resolveReferenceInstallPlan, } from "./reference-data/catalog.js";
186
186
  export type { ReferenceArtifact, ReferenceDataCatalog, ReferenceDataset, ReferenceInstallPlan, ReferenceInstallPlanDataset } from "./reference-data/catalog.js";
187
- export { REFERENCE_INSTALL_RECEIPT_VERSION, ReferenceInstallReceiptSchema, parseReferenceInstallReceipt } from "./reference-data/receipt.js";
188
- export type { ReferenceInstallReceipt, ReferenceReceiptArtifact } from "./reference-data/receipt.js";
187
+ export { REFERENCE_INSTALL_RECEIPT_VERSION, ReferenceInstallReceiptSchema, parseReferenceInstallReceipt, referenceReceiptContentDigest, } from "./reference-data/receipt.js";
188
+ export type { ReferenceInstallReceipt, ReferenceReceiptArtifact, ReferenceReceiptContent } from "./reference-data/receipt.js";
189
189
  export { runStepDir } from "./workspace/paths.js";
190
190
  export { reportSessionDir } from "./workspace/paths.js";
191
191
  export type { LogFields, LogLevel, Logger } from "./lib/logger.js";
package/dist/index.js CHANGED
@@ -288,7 +288,7 @@ export { queryStepsByRun } from "./state/step-executions.js";
288
288
  // artifact names the third-party upstream it is fetched from; embedders own all
289
289
  // transfer/storage behavior but never choose a different source.
290
290
  export { REFERENCE_DATA_CATALOG, REFERENCE_DATA_CATALOG_VERSION, ReferenceArtifactPathSchema, ReferenceArtifactSchema, ReferenceDataCatalogSchema, ReferenceDatasetSchema, ReferenceSha256Schema, UnknownReferenceDatasetError, referenceArtifactKey, resolveReferenceInstallPlan, } from "./reference-data/catalog.js";
291
- export { REFERENCE_INSTALL_RECEIPT_VERSION, ReferenceInstallReceiptSchema, parseReferenceInstallReceipt } from "./reference-data/receipt.js";
291
+ export { REFERENCE_INSTALL_RECEIPT_VERSION, ReferenceInstallReceiptSchema, parseReferenceInstallReceipt, referenceReceiptContentDigest, } from "./reference-data/receipt.js";
292
292
  // Workspace-tree path convention — the root-relative step directory each
293
293
  // sandbox step confines its artifact writes to (join onto the resolved root).
294
294
  export { runStepDir } from "./workspace/paths.js";
@@ -1,24 +1,36 @@
1
1
  import { z } from "zod";
2
2
  /** Schema version for installation receipts shared by all embedders. */
3
- export declare const REFERENCE_INSTALL_RECEIPT_VERSION: 1;
3
+ export declare const REFERENCE_INSTALL_RECEIPT_VERSION: 2;
4
4
  /**
5
5
  * What was actually written to disk for one artifact. The size and digest are
6
6
  * *observed at install* — the catalog carries neither, so this receipt is the
7
7
  * sole record of the bytes the upstream served, and what `verify` later checks
8
- * the local files against. A receipt written by an older build may still carry
9
- * an `integrity` field; it is simply ignored (unknown keys are stripped).
8
+ * the local files against.
10
9
  */
11
10
  declare const ReferenceReceiptArtifactSchema: z.ZodObject<{
12
11
  path: z.ZodString;
13
12
  bytes: z.ZodNumber;
14
13
  sha256: z.ZodString;
15
14
  }, z.core.$strip>;
15
+ /** The fields of a receipt that identify the installed content. */
16
+ export interface ReferenceReceiptContent {
17
+ readonly datasetId: string;
18
+ readonly datasetVersion: string;
19
+ readonly artifacts: readonly ReferenceReceiptArtifact[];
20
+ }
21
+ /**
22
+ * The identity of the installed bytes, independent of the install time. Consumers outside the
23
+ * harness recompute it, so the input bytes are a contract: the SHA-256 of the compact JSON array
24
+ * `[datasetId, datasetVersion, [[path, bytes, sha256], ...]]`, artifacts in code-unit order of path.
25
+ */
26
+ export declare function referenceReceiptContentDigest(content: ReferenceReceiptContent): string;
16
27
  /** Optional metadata describing one activated reference dataset version. */
17
28
  export declare const ReferenceInstallReceiptSchema: z.ZodObject<{
18
- version: z.ZodLiteral<1>;
29
+ version: z.ZodLiteral<2>;
19
30
  datasetId: z.ZodString;
20
31
  datasetVersion: z.ZodString;
21
32
  activatedAt: z.ZodISODateTime;
33
+ contentDigest: z.ZodString;
22
34
  artifacts: z.ZodArray<z.ZodObject<{
23
35
  path: z.ZodString;
24
36
  bytes: z.ZodNumber;
@@ -1,19 +1,32 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { z } from "zod";
2
3
  import { ReferenceArtifactPathSchema, ReferenceDatasetSchema, ReferenceSha256Schema } from "./catalog.js";
3
4
  /** Schema version for installation receipts shared by all embedders. */
4
- export const REFERENCE_INSTALL_RECEIPT_VERSION = 1;
5
+ export const REFERENCE_INSTALL_RECEIPT_VERSION = 2;
5
6
  /**
6
7
  * What was actually written to disk for one artifact. The size and digest are
7
8
  * *observed at install* — the catalog carries neither, so this receipt is the
8
9
  * sole record of the bytes the upstream served, and what `verify` later checks
9
- * the local files against. A receipt written by an older build may still carry
10
- * an `integrity` field; it is simply ignored (unknown keys are stripped).
10
+ * the local files against.
11
11
  */
12
12
  const ReferenceReceiptArtifactSchema = z.object({
13
13
  path: ReferenceArtifactPathSchema,
14
14
  bytes: z.number().int().nonnegative(),
15
15
  sha256: ReferenceSha256Schema,
16
16
  });
17
+ /**
18
+ * The identity of the installed bytes, independent of the install time. Consumers outside the
19
+ * harness recompute it, so the input bytes are a contract: the SHA-256 of the compact JSON array
20
+ * `[datasetId, datasetVersion, [[path, bytes, sha256], ...]]`, artifacts in code-unit order of path.
21
+ */
22
+ export function referenceReceiptContentDigest(content) {
23
+ const artifacts = [...content.artifacts]
24
+ .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
25
+ .map((artifact) => [artifact.path, artifact.bytes, artifact.sha256]);
26
+ return createHash("sha256")
27
+ .update(JSON.stringify([content.datasetId, content.datasetVersion, artifacts]), "utf8")
28
+ .digest("hex");
29
+ }
17
30
  /** Optional metadata describing one activated reference dataset version. */
18
31
  export const ReferenceInstallReceiptSchema = z
19
32
  .object({
@@ -21,6 +34,7 @@ export const ReferenceInstallReceiptSchema = z
21
34
  datasetId: ReferenceDatasetSchema.shape.id,
22
35
  datasetVersion: ReferenceDatasetSchema.shape.version,
23
36
  activatedAt: z.iso.datetime({ offset: true }),
37
+ contentDigest: ReferenceSha256Schema,
24
38
  artifacts: z.array(ReferenceReceiptArtifactSchema).min(1),
25
39
  })
26
40
  .superRefine((receipt, ctx) => {
@@ -31,6 +45,9 @@ export const ReferenceInstallReceiptSchema = z
31
45
  }
32
46
  paths.add(artifact.path);
33
47
  }
48
+ if (receipt.contentDigest !== referenceReceiptContentDigest(receipt)) {
49
+ ctx.addIssue({ code: "custom", message: "Receipt content digest does not match its dataset and artifacts", path: ["contentDigest"] });
50
+ }
34
51
  });
35
52
  /** Parse optional/untrusted receipt metadata; invalid receipts are ignored. */
36
53
  export function parseReferenceInstallReceipt(input) {
@@ -166,10 +166,10 @@ function citationsOf(text) {
166
166
  async function collectCitations(pool, resolveWorkspaceRoot, analysisId) {
167
167
  let root;
168
168
  try {
169
- root = resolveWorkspaceRoot(analysisId);
169
+ root = await resolveWorkspaceRoot(analysisId);
170
170
  }
171
171
  catch {
172
- // The seam signals an unresolvable resource by a throw. The artifact map still states the
172
+ // The seam signals an unresolvable resource by a rejection. The artifact map still states the
173
173
  // membership of the session, thus the fault costs the citation list alone.
174
174
  return ok({ keys: [], records: new Map() });
175
175
  }
@@ -97,7 +97,7 @@ export interface ReportResolverScope {
97
97
  * The factory that gives the reference resolver of one report tool call. The harness wires the production
98
98
  * realization, and an embedder can bind its own at its composition root.
99
99
  */
100
- export type MakeReportReferenceResolver = (scope: ReportResolverScope) => ReferenceResolver;
100
+ export type MakeReportReferenceResolver = (scope: ReportResolverScope) => Promise<ReferenceResolver>;
101
101
  export interface CoreRuntimeDeps {
102
102
  readonly conversation: ConversationAssemblyDeps;
103
103
  readonly workflows: CoreWorkflowDeps;
@@ -152,10 +152,10 @@ export function assembleCoreRuntime(deps) {
152
152
  // for each tool call. The extraction arm falls through to the registered extract-values
153
153
  // callable for an over-cap file or a host parse fault. The cap gates the host read, and
154
154
  // the resolver defaults it to 16 MiB.
155
- const makeProductionReportResolver = (scope) => {
155
+ const makeProductionReportResolver = async (scope) => {
156
156
  const arm = createExtractionArm(bindExtractionTrigger({ runAuthorizer: conversation.runAuthorizer, workflow: extractValues }, { auth: scope.auth, analysisId: scope.analysisId }));
157
157
  return createProductionResolver({
158
- workspaceRoot: conversation.resolveWorkspaceRoot(scope.analysisId),
158
+ workspaceRoot: await conversation.resolveWorkspaceRoot(scope.analysisId),
159
159
  analysisId: scope.analysisId,
160
160
  extractionArm: arm,
161
161
  readCache: reportArtifactReads.forAnalysis(scope.analysisId),
@@ -43,7 +43,7 @@ export interface SandboxClient {
43
43
  * Each spawn failure is an `err` value, the refusal of the label hook
44
44
  * (`labels_refused`) included: a spawn path splits the result with
45
45
  * `keepSuspendingRefusal`. Only a defect of the deployment config, or a
46
- * throw of the workspace-root resolver, throws, and it fails the workflow.
46
+ * rejection of the workspace-root resolver, throws, and it fails the workflow.
47
47
  */
48
48
  createSandbox(session: SpawnSession, spec: SandboxSpec, identity: SandboxIdentity): ResultAsync<SandboxRef, SandboxError>;
49
49
  /**
@@ -57,7 +57,7 @@ export async function precreateStepTree(deps, coords) {
57
57
  // `tailWritePrefix` (not a raw `join`) so every segment runs through the same
58
58
  // id validation the mount builders apply — a crafted tail cannot escape the
59
59
  // resolved root.
60
- const writeDir = tailWritePrefix({ workspaceRoot: deps.resolveWorkspaceRoot(coords.analysisId), tail: write.tail });
60
+ const writeDir = tailWritePrefix({ workspaceRoot: await deps.resolveWorkspaceRoot(coords.analysisId), tail: write.tail });
61
61
  await mkdir(writeDir, { recursive: true });
62
62
  await Promise.all(write.subdirs.map((sub) => mkdir(join(writeDir, sub), { recursive: true })));
63
63
  if (deps.stepTreeAccess === "world-writable") {
@@ -287,7 +287,7 @@ export function createDockerSandboxOps(config) {
287
287
  toolchainSource: config.toolchainSource,
288
288
  cache: cacheBindable,
289
289
  });
290
- const hostTreePath = config.resolveWorkspaceRoot(analysisId);
290
+ const hostTreePath = await config.resolveWorkspaceRoot(analysisId);
291
291
  // `tailWritePrefix` (not a raw `join`) so the RW bind source runs through
292
292
  // the same validation as the pre-created write tree — a crafted stepId or
293
293
  // a crafted tail cannot escape the resolved root into the container. The
@@ -88,11 +88,11 @@ function buildKubeApi() {
88
88
  * mount a directory the harness never wrote to — fail loudly instead. `createSandbox` runs
89
89
  * inside a DBOS workflow body, where a throw is the durable failure signal.
90
90
  */
91
- function workspaceSubPathFor(config, analysisId) {
91
+ async function workspaceSubPathFor(config, analysisId) {
92
92
  if (!config.sessionPvcRoot) {
93
93
  throw new Error("k8s sandbox: sessionPvc is set but sessionPvcRoot is not — the PVC subPath of a workspace root cannot be derived");
94
94
  }
95
- const rel = relativePath(config.sessionPvcRoot, config.resolveWorkspaceRoot(analysisId));
95
+ const rel = relativePath(config.sessionPvcRoot, await config.resolveWorkspaceRoot(analysisId));
96
96
  const posix = rel.split(sep).join("/");
97
97
  if (posix.length === 0 || posix.startsWith("..")) {
98
98
  throw new Error(`k8s sandbox: workspace root for ${analysisId} does not live under sessionPvcRoot (${config.sessionPvcRoot})`);
@@ -111,7 +111,7 @@ function pvcSubPath(value, what) {
111
111
  }
112
112
  return value;
113
113
  }
114
- function buildJobSpec(session, sandboxSpec, hostLabels, config, identity, farm, libsMounted) {
114
+ async function buildJobSpec(session, sandboxSpec, hostLabels, config, identity, farm, libsMounted) {
115
115
  const { sandboxId } = identity;
116
116
  // The lock gate of the package-store spec: `farmPath` is PVC-relative,
117
117
  // thus this host can prove the lock only through `libStorePvcRoot`. The
@@ -148,7 +148,7 @@ function buildJobSpec(session, sandboxSpec, hostLabels, config, identity, farm,
148
148
  const volumes = [];
149
149
  const volumeMounts = [];
150
150
  if (config.sessionPvc) {
151
- const subPaths = buildSessionSubPaths(coords, workspaceSubPathFor(config, session.scope.analysisId));
151
+ const subPaths = buildSessionSubPaths(coords, await workspaceSubPathFor(config, session.scope.analysisId));
152
152
  volumes.push({
153
153
  name: SESSION_VOLUME_NAME,
154
154
  persistentVolumeClaim: { claimName: config.sessionPvc },
@@ -491,7 +491,7 @@ export function createK8sSandboxOps(config) {
491
491
  .warn("the farm of the analysis has no usable inflexa.lock at sandbox creation — mounting no package store (sandbox degrades to available:false)", { libStorePvc: config.libStorePvc, farmPath: farm.farmPath, lockError: lock.error.type, sandboxId });
492
492
  }
493
493
  }
494
- const job = buildJobSpec(session, spec, hostLabels, config, identity, farm, libsMounted);
494
+ const job = await buildJobSpec(session, spec, hostLabels, config, identity, farm, libsMounted);
495
495
  const adopted = await createOrAdoptJob(batchApi, coreApi, config.namespace, sandboxId, spec.childWorkflowId, job);
496
496
  if (adopted.isErr())
497
497
  return err(adopted.error);
@@ -356,10 +356,10 @@ export async function runDataProfileBody(input, deps) {
356
356
  const executionId = generateExecutionId(DATA_PROFILE_AGENT_ID);
357
357
  const workflowId = DBOS.workflowID ?? `${DATA_PROFILE_RUN_LITERAL}:${executionId}`;
358
358
  const childSession = forSubAgent(runSession, DATA_PROFILE_AGENT_ID);
359
- // A resolver throw fails the workflow loudly (workspace-root-resolution
359
+ // A resolver rejection fails the workflow loudly (workspace-root-resolution
360
360
  // contract) — profiling an analysis whose root cannot be resolved is
361
361
  // meaningless, so no softer handling is warranted.
362
- const workspaceRoot = deps.resolveWorkspaceRoot(analysisId);
362
+ const workspaceRoot = await deps.resolveWorkspaceRoot(analysisId);
363
363
  const profileWritePrefix = `${workspaceRoot}/runs/${DATA_PROFILE_RUN_LITERAL}/${DATA_PROFILE_STEP_LITERAL}`;
364
364
  // 3. The deterministic scan, before any container and before the agent's first turn.
365
365
  // It is always needed and its result does not depend on agent judgement, so spending
@@ -221,7 +221,7 @@ export function createDeriveTableTool(deps) {
221
221
  }
222
222
  let root;
223
223
  try {
224
- root = deps.resolveWorkspaceRoot(analysisId);
224
+ root = await deps.resolveWorkspaceRoot(analysisId);
225
225
  }
226
226
  catch (cause) {
227
227
  logger.warn("the workspace root did not resolve", { threadId, analysisId, ...defaultErrorFields(cause) });
@@ -276,7 +276,7 @@ export function createExaminePageTool(deps) {
276
276
  const { threadId, analysisId } = opened.value;
277
277
  let root;
278
278
  try {
279
- root = deps.resolveWorkspaceRoot(analysisId);
279
+ root = await deps.resolveWorkspaceRoot(analysisId);
280
280
  }
281
281
  catch (cause) {
282
282
  logger.warn("the workspace root did not resolve", { threadId, analysisId, ...defaultErrorFields(cause) });
@@ -170,11 +170,11 @@ export function createListPinnedArtifactsTool(deps) {
170
170
  return ok({ outcome: "refused", refusal: opened.error });
171
171
  }
172
172
  const { threadId, analysisId, state } = opened.value;
173
- // The seam signals an unresolvable resource by a throw. The listing still serves the path and
173
+ // The seam signals an unresolvable resource by a rejection. The listing still serves the path and
174
174
  // the file type of each entry, thus the fault costs the columns alone and never the whole call.
175
175
  let root;
176
176
  try {
177
- root = deps.resolveWorkspaceRoot(analysisId);
177
+ root = await deps.resolveWorkspaceRoot(analysisId);
178
178
  }
179
179
  catch (cause) {
180
180
  logger.warn("the workspace root did not resolve", { threadId, analysisId, ...defaultErrorFields(cause) });
@@ -139,7 +139,7 @@ export interface PreviewReportToolDeps {
139
139
  readonly makeResolver?: (scope: {
140
140
  analysisId: string;
141
141
  auth: AuthContext;
142
- }) => ReferenceResolver;
142
+ }) => Promise<ReferenceResolver>;
143
143
  readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
144
144
  readonly resolvePageAsset?: ResolvePageAsset;
145
145
  readonly makeSessionPages?: MakeSessionPagePublisher;
@@ -165,8 +165,8 @@ async function resolveDocument(document, resolver, snapshot) {
165
165
  /**
166
166
  * Resolve the page root, stage each bound image and each manifest entry, and write the page.
167
167
  *
168
- * The workspace-root seam signals an unresolvable resource by a throw, thus the resolution sits inside
169
- * the protection and its throw becomes a value. Each `fs` call runs through `tryFsWrite`, the one
168
+ * The workspace-root seam signals an unresolvable resource by a rejection, thus the resolution sits inside
169
+ * the protection and its rejection becomes a value. Each `fs` call runs through `tryFsWrite`, the one
170
170
  * sanctioned `fs` guard, thus a genuine I/O fault becomes a value and a control-flow exception such as a
171
171
  * cancellation propagates.
172
172
  *
@@ -186,7 +186,7 @@ async function resolveDocument(document, resolver, snapshot) {
186
186
  async function renderToWorkspace(args) {
187
187
  let root;
188
188
  try {
189
- root = args.resolveWorkspaceRoot(args.analysisId);
189
+ root = await args.resolveWorkspaceRoot(args.analysisId);
190
190
  }
191
191
  catch (cause) {
192
192
  return err({ kind: "fs", error: { type: "read_failed", op: "preview.resolveWorkspaceRoot", path: args.analysisId, cause } });
@@ -382,12 +382,12 @@ export function createPreviewReportTool(deps) {
382
382
  if (deps.makeResolver === undefined) {
383
383
  return ok({ outcome: "resolver-unavailable" });
384
384
  }
385
- // The resolver construction resolves the workspace root inside, and that seam throws on an
386
- // unresolvable root. The guard turns the throw into a typed outcome that names the fault, thus a
385
+ // The resolver construction resolves the workspace root inside, and that seam rejects on an
386
+ // unresolvable root. The guard turns the rejection into a typed outcome that names the fault, thus a
387
387
  // control-flow exception propagates and the unresolvable root does not.
388
388
  let resolver;
389
389
  try {
390
- resolver = deps.makeResolver({ analysisId, auth: ctx.session.auth });
390
+ resolver = await deps.makeResolver({ analysisId, auth: ctx.session.auth });
391
391
  }
392
392
  catch (cause) {
393
393
  logger.warn("the workspace root did not resolve", { threadId, analysisId, ...defaultErrorFields(cause) });
@@ -105,7 +105,7 @@ export interface RecordVersionToolDeps {
105
105
  readonly makeResolver?: (scope: {
106
106
  analysisId: string;
107
107
  auth: AuthContext;
108
- }) => ReferenceResolver;
108
+ }) => Promise<ReferenceResolver>;
109
109
  /** The provenance seam; an unbound session emit emits nothing and the record runs the same. */
110
110
  readonly provenance?: ProvenanceSeam;
111
111
  readonly logger?: Logger;
@@ -86,7 +86,7 @@ async function pruneUnusedDerivations(args) {
86
86
  let root;
87
87
  let derivedDir;
88
88
  try {
89
- root = args.resolveWorkspaceRoot(args.analysisId);
89
+ root = await args.resolveWorkspaceRoot(args.analysisId);
90
90
  derivedDir = resolvePath(root, reportSessionDerivedDir(args.threadId));
91
91
  }
92
92
  catch (cause) {
@@ -169,12 +169,12 @@ export function createRecordVersionTool(deps) {
169
169
  if (deps.makeResolver === undefined) {
170
170
  return ok({ outcome: "resolver-unavailable" });
171
171
  }
172
- // The resolver construction resolves the workspace root inside, and that seam throws on an
173
- // unresolvable root. The guard turns the throw into a typed outcome that names the fault, thus a
172
+ // The resolver construction resolves the workspace root inside, and that seam rejects on an
173
+ // unresolvable root. The guard turns the rejection into a typed outcome that names the fault, thus a
174
174
  // control-flow exception propagates and the unresolvable root does not.
175
175
  let resolver;
176
176
  try {
177
- resolver = deps.makeResolver({ analysisId, auth: ctx.session.auth });
177
+ resolver = await deps.makeResolver({ analysisId, auth: ctx.session.auth });
178
178
  }
179
179
  catch (cause) {
180
180
  logger.warn("the workspace root did not resolve", { threadId, analysisId, ...logger.errorFields(cause) });
@@ -201,9 +201,9 @@ export function createSessionWorkspaceMutator(deps) {
201
201
  const scope = session.scope;
202
202
  if (scope.kind !== "analysis")
203
203
  return { status: "out_of_scope", path };
204
- // Throws on an unknown analysis — the loop's dispatch catch maps
204
+ // Rejects on an unknown analysis — the loop's dispatch catch maps
205
205
  // it to an error tool result, like any unexpected host failure.
206
- const workspaceRoot = deps.resolveWorkspaceRoot(scope.analysisId);
206
+ const workspaceRoot = await deps.resolveWorkspaceRoot(scope.analysisId);
207
207
  const landed = await confinedWrite({
208
208
  workspaceRoot,
209
209
  analysisId: scope.analysisId,
@@ -250,7 +250,7 @@ export async function gatherStepPackage(args) {
250
250
  throw new Error(`executeAnalysis: step "${stepId}" missing from planStepById — every step must carry its plan data`);
251
251
  }
252
252
  const analysisId = input.analysisId;
253
- const workspaceRoot = deps.resolveWorkspaceRoot(analysisId);
253
+ const workspaceRoot = await deps.resolveWorkspaceRoot(analysisId);
254
254
  // The paths the agent actually sees: the tree is bind-mounted at
255
255
  // `/{analysisId}`, and the step's writable directory is its cwd. Derived
256
256
  // through the same helpers the sandbox mount and the mutate tools use, so
@@ -661,10 +661,10 @@ async function validateAndInit(input, runId, deps) {
661
661
  return { kind: "joined-existing", runId: existing.runId };
662
662
  }
663
663
  await DBOS.runStep(async () => {
664
- // A resolver throw here (unknown analysis, unresolvable root) fails the
664
+ // A resolver rejection here (unknown analysis, unresolvable root) fails the
665
665
  // step durably — exactly the contract the workspace-root-resolution spec
666
666
  // requires of failures inside DBOS bodies.
667
- await mkdir(join(deps.resolveWorkspaceRoot(input.analysisId), runDir(runId)), {
667
+ await mkdir(join(await deps.resolveWorkspaceRoot(input.analysisId), runDir(runId)), {
668
668
  recursive: true,
669
669
  });
670
670
  }, { name: "init-run-filesystem" });
@@ -263,12 +263,12 @@ export interface SandboxStepDeps {
263
263
  * agent from the catalog, wires its workspace tools, and returns the
264
264
  * configured `AgentDefinition` runAgent will drive.
265
265
  */
266
- buildAgent(ctx: SandboxAgentBuildContext): AgentDefinition;
266
+ buildAgent(ctx: SandboxAgentBuildContext): Promise<AgentDefinition>;
267
267
  /**
268
268
  * Resolve the absolute writable artifact prefix for this step. The
269
269
  * workspace mutate tools enforce write confinement against this path.
270
270
  */
271
- resolveWritePrefix(input: SandboxStepInput): string;
271
+ resolveWritePrefix(input: SandboxStepInput): Promise<string>;
272
272
  }
273
273
  /**
274
274
  * Per-step context passed to every post-step dep. Carries the durable input
@@ -176,7 +176,7 @@ export async function runSandboxStepBody(input, deps) {
176
176
  // "conversation-agent"). `run-agent.ts` builds source from
177
177
  // `session.provenance.agentId`, and billing tags ride the same provenance.
178
178
  const session = forSubAgent(input.runSession, input.agentId);
179
- const writePrefix = deps.resolveWritePrefix(input);
179
+ const writePrefix = await deps.resolveWritePrefix(input);
180
180
  const lineageCollector = createLineageCollector(input);
181
181
  // (2a) sandbox.mint — checkpoint the machine's identity (its name)
182
182
  // BEFORE it is spawned (see the harness-sandbox-exec spec). Durable-before-create is what makes (2b)
@@ -220,7 +220,7 @@ export async function runSandboxStepBody(input, deps) {
220
220
  const stepDeadlineMs = (await DBOS.now()) + (input.timeoutSeconds ?? DEFAULT_STEP_TIMEOUT_SECONDS) * 1000;
221
221
  const blockerHolder = createBlockerHolder();
222
222
  const fileMetadata = createFileMetadataCell();
223
- const agent = deps.buildAgent({
223
+ const agent = await deps.buildAgent({
224
224
  input,
225
225
  session,
226
226
  sandbox,
@@ -32,7 +32,7 @@ import { lstat as fsLstat, open as fsOpen, readdir as fsReaddir, stat as fsStat
32
32
  import { constants as fsConstants } from "node:fs";
33
33
  import { createInterface } from "node:readline";
34
34
  import { join } from "node:path";
35
- import { ResultAsync, err, errAsync, ok } from "neverthrow";
35
+ import { ResultAsync, ok } from "neverthrow";
36
36
  import { scopeResource } from "../auth/types.js";
37
37
  import { tryFetch, tryFs } from "../lib/fs-result.js";
38
38
  import { classifyWithinRoot } from "../lib/fs-helpers.js";
@@ -41,7 +41,7 @@ import { resolveWorkspacePath } from "./paths.js";
41
41
  export function createWorkspaceFilesystem(deps) {
42
42
  const { resolveWorkspaceRoot, presignedFallback } = deps;
43
43
  /**
44
- * `ResolveWorkspaceRoot` signals an unresolvable resource by throwing — the contract that
44
+ * `ResolveWorkspaceRoot` signals an unresolvable resource by a rejection — the contract that
45
45
  * makes a resolution failure inside a DBOS body a durably-failed step. This seam is not a
46
46
  * DBOS body: it promises `Result`, and the read tools it backs are reachable from a live
47
47
  * chat turn whose analysis folder may have been moved or deleted since the turn began.
@@ -50,14 +50,12 @@ export function createWorkspaceFilesystem(deps) {
50
50
  */
51
51
  function resolveFor(session, path, workingDir) {
52
52
  const { resourceId: analysisId } = scopeResource(session.scope);
53
- let workspaceRoot;
54
- try {
55
- workspaceRoot = resolveWorkspaceRoot(analysisId);
56
- }
57
- catch (cause) {
58
- return err({ type: "read_failed", op: "workspace.resolveWorkspaceRoot", path: analysisId, cause });
59
- }
60
- return ok({ resolved: resolveWorkspacePath({ workspaceRoot, analysisId, path, workingDir }), workspaceRoot });
53
+ return ResultAsync.fromThrowable(resolveWorkspaceRoot, (cause) => ({
54
+ type: "read_failed",
55
+ op: "workspace.resolveWorkspaceRoot",
56
+ path: analysisId,
57
+ cause,
58
+ }))(analysisId).map((workspaceRoot) => ({ resolved: resolveWorkspacePath({ workspaceRoot, analysisId, path, workingDir }), workspaceRoot }));
61
59
  }
62
60
  /**
63
61
  * Lexical resolution PLUS a symlink-following confinement check — the single
@@ -70,18 +68,16 @@ export function createWorkspaceFilesystem(deps) {
70
68
  * presigned-fallback handling still runs (a non-existent path leaks nothing).
71
69
  */
72
70
  function confinedResolve(session, path, workingDir) {
73
- const base = resolveFor(session, path, workingDir);
74
- if (base.isErr())
75
- return errAsync(base.error);
76
- const { resolved, workspaceRoot } = base.value;
77
- if (resolved.kind === "out_of_scope")
78
- return okAsync(resolved);
79
- return ResultAsync.fromPromise(classifyWithinRoot(workspaceRoot, resolved.absolute), (cause) => ({
80
- type: "read_failed",
81
- op: "workspace.confineRealpath",
82
- path: resolved.absolute,
83
- cause,
84
- })).map((verdict) => (verdict === "escaped" ? { kind: "out_of_scope" } : resolved));
71
+ return resolveFor(session, path, workingDir).andThen(({ resolved, workspaceRoot }) => {
72
+ if (resolved.kind === "out_of_scope")
73
+ return okAsync(resolved);
74
+ return ResultAsync.fromPromise(classifyWithinRoot(workspaceRoot, resolved.absolute), (cause) => ({
75
+ type: "read_failed",
76
+ op: "workspace.confineRealpath",
77
+ path: resolved.absolute,
78
+ cause,
79
+ })).map((verdict) => (verdict === "escaped" ? { kind: "out_of_scope" } : resolved));
80
+ });
85
81
  }
86
82
  return {
87
83
  readFile({ session, path, maxBytes, headLines, tailLines, workingDir }) {
@@ -43,11 +43,14 @@
43
43
  * - **Stable while the resource has an active run** — derived paths are
44
44
  * recorded in durable step outputs; preventing mid-run moves is the
45
45
  * embedder's job.
46
- * - **Throws on unknown resources** — inside DBOS bodies the failure must
47
- * cross the step boundary as a throw so the step is durably recorded as
48
- * failed (see `lib/result.ts` house rules).
46
+ * - **Rejects on unknown resources** — inside DBOS bodies the awaited
47
+ * rejection must cross the step boundary as a throw so the step is durably
48
+ * recorded as failed (see `lib/result.ts` house rules).
49
+ *
50
+ * Async so that a realization can read host state (a marker file on a network
51
+ * mount) without a block of the event loop.
49
52
  */
50
- export type ResolveWorkspaceRoot = (resourceId: string) => string;
53
+ export type ResolveWorkspaceRoot = (resourceId: string) => Promise<string>;
51
54
  /** Result of resolving a workspace path against the canonical layout. */
52
55
  export type ResolvedPath = {
53
56
  readonly kind: "ok";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.43.0",
3
+ "version": "0.44.0",
4
4
  "description": "Host-agnostic bioinformatics agent harness — hand-rolled agent loop, DBOS-durable workflows, sandboxed compute.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  "directory": "harness"
34
34
  },
35
35
  "scripts": {
36
- "build": "tsc -p tsconfig.json && node scripts/copy-assets.mjs",
36
+ "build": "tsc -p tsconfig.build.json && node scripts/copy-assets.mjs",
37
37
  "design:fixture": "bun scripts/render-fixture.ts",
38
38
  "design:gallery": "bun scripts/render-gallery.ts",
39
39
  "format": "prettier --write src/",
@@ -41,12 +41,12 @@
41
41
  "gallery:data": "bash scripts/gallery-data.sh",
42
42
  "lint": "eslint .",
43
43
  "lint:python": "ruff check src/input-scan/decoder && ruff format --check src/input-scan/decoder",
44
- "prepare": "tsc -p tsconfig.json && node scripts/copy-assets.mjs",
44
+ "prepare": "tsc -p tsconfig.build.json && node scripts/copy-assets.mjs",
45
45
  "prepublishOnly": "bun run build && bun run smoke",
46
46
  "smoke": "node scripts/smoke.mjs",
47
47
  "test:full": "bash scripts/test-full.sh",
48
48
  "test:python": "python3 -m pytest src/input-scan/decoder -q",
49
- "typecheck": "tsc --noEmit"
49
+ "typecheck": "tsc -p tsconfig.json --noEmit"
50
50
  },
51
51
  "dependencies": {
52
52
  "@ai-sdk/anthropic": "^4.0.62",
@@ -70,6 +70,8 @@
70
70
  "@opentelemetry/sdk-trace-base": "^2.9.0",
71
71
  "@opentelemetry/sdk-trace-node": "^2.9.0",
72
72
  "@opentelemetry/semantic-conventions": "^1.43.0",
73
+ "@types/dockerode": "^3.3.47",
74
+ "@types/pg": "^8.20.0",
73
75
  "ag-grid-community": "36.1.0",
74
76
  "ai": "^7.0.113",
75
77
  "cheerio": "^1.2.0",
@@ -101,10 +103,8 @@
101
103
  "@ai-sdk/amazon-bedrock": "^5.0.93",
102
104
  "@eslint/js": "^10.0.1",
103
105
  "@types/bun": "latest",
104
- "@types/dockerode": "^3.3.47",
105
106
  "@types/node": "^24.13.3",
106
107
  "@types/pdf-parse": "^1.1.5",
107
- "@types/pg": "^8.20.0",
108
108
  "csstree-validator": "^4.0.1",
109
109
  "eslint": "^10.7.0",
110
110
  "eslint-plugin-neverthrow": "^1.1.4",