@inflexa-ai/harness 0.19.2 → 0.20.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.
@@ -11,7 +11,8 @@ const IdConvResponseSchema = z.object({
11
11
  });
12
12
  export function ncbiUrl(apiKey, base, params) {
13
13
  const url = new URL(base);
14
- if (apiKey !== undefined)
14
+ // The URL must not include an empty key. NCBI rejects `api_key=` with HTTP 400.
15
+ if (apiKey)
15
16
  url.searchParams.set("api_key", apiKey);
16
17
  for (const [key, value] of Object.entries(params))
17
18
  if (value !== undefined)
@@ -32,10 +32,6 @@ const MANAGED_BY_VALUE = "cortex";
32
32
  const OWNER_WORKFLOW_LABEL = "cortex/owner-workflow-id";
33
33
  const RUN_ID_LABEL = "cortex/run-id";
34
34
  const STEP_ID_LABEL = "cortex/step-id";
35
- // OpenCost sanitizes `/` and `-` to `_`, so these arrive at the metering
36
- // reconciler as cortex_billing_context / cortex_user_id / cortex_analysis_id.
37
- const BILLING_CONTEXT_LABEL = "cortex/billing-context";
38
- const USER_ID_LABEL = "cortex/user-id";
39
35
  const ANALYSIS_ID_LABEL = "cortex/analysis-id";
40
36
  /**
41
37
  * Coerce an arbitrary identifier into a valid K8s label value:
@@ -219,17 +215,15 @@ function buildJobSpec(meta, config, identity) {
219
215
  podSpec.tolerations = config.tolerations;
220
216
  if (config.runtimeClassName)
221
217
  podSpec.runtimeClassName = config.runtimeClassName;
222
- // Pod-template placement is load-bearing: the OpenCost reconciler allocates
223
- // by pod labels and filters on a non-empty cortex_billing_context — a
224
- // Job-only label is invisible to compute metering.
225
- const billingLabels = meta.billing
226
- ? {
227
- [BILLING_CONTEXT_LABEL]: sanitizeLabelValue(meta.billing.billingContextId),
228
- [USER_ID_LABEL]: sanitizeLabelValue(meta.billing.userId),
229
- [ANALYSIS_ID_LABEL]: sanitizeLabelValue(meta.analysisId),
230
- [RUN_ID_LABEL]: sanitizeLabelValue(meta.runId),
231
- }
232
- : {};
218
+ // Pod-template placement is load-bearing: a cost reconciler allocates by pod
219
+ // labels, thus a Job-only label is invisible to it. The host labels stay
220
+ // opaque — every value passes through `sanitizeLabelValue`, because one
221
+ // malformed value makes the API server reject the whole Job at admission.
222
+ const attributionLabels = {
223
+ [ANALYSIS_ID_LABEL]: sanitizeLabelValue(meta.analysisId),
224
+ [RUN_ID_LABEL]: sanitizeLabelValue(meta.runId),
225
+ ...Object.fromEntries(Object.entries(meta.podLabels ?? {}).map(([key, value]) => [key, sanitizeLabelValue(value)])),
226
+ };
233
227
  return {
234
228
  apiVersion: "batch/v1",
235
229
  kind: "Job",
@@ -241,9 +235,8 @@ function buildJobSpec(meta, config, identity) {
241
235
  role: "sandbox",
242
236
  "cortex/sandbox-id": sandboxId,
243
237
  [OWNER_WORKFLOW_LABEL]: sanitizeLabelValue(meta.childWorkflowId),
244
- [RUN_ID_LABEL]: sanitizeLabelValue(meta.runId),
245
238
  [STEP_ID_LABEL]: sanitizeLabelValue(meta.stepId),
246
- ...billingLabels,
239
+ ...attributionLabels,
247
240
  },
248
241
  },
249
242
  spec: {
@@ -254,7 +247,7 @@ function buildJobSpec(meta, config, identity) {
254
247
  labels: {
255
248
  role: "sandbox",
256
249
  "cortex/sandbox-id": sandboxId,
257
- ...billingLabels,
250
+ ...attributionLabels,
258
251
  },
259
252
  },
260
253
  spec: podSpec,
@@ -243,12 +243,10 @@ export interface CreateSandboxMeta {
243
243
  /** Enforced read-only: provision with no read-write step mount, only the
244
244
  * read-only analysis tree for generic read-only agents. */
245
245
  readOnly?: boolean;
246
- /** Billing attribution stamped as pod labels for OpenCost compute metering.
247
- * Absent ⇒ the pod is invisible to the metering reconciler (filter-level). */
248
- billing?: {
249
- billingContextId: string;
250
- userId: string;
251
- };
246
+ /** Host-supplied labels stamped onto the sandbox pod, verbatim. Opaque here:
247
+ * the harness reads no key and no value, it only sanitizes each value into a
248
+ * valid label. Absent ⇒ the pod carries the harness's own labels only. */
249
+ podLabels?: Record<string, string>;
252
250
  }
253
251
  /**
254
252
  * The identity minted for a sandbox machine *before* it is spawned — the
@@ -23,7 +23,6 @@ import type { RunAuthorization, RunAuthorizer } from "../execution/run-authorize
23
23
  import type { StagedInput } from "../execution/staged-input.js";
24
24
  import type { BioToolKeys } from "../tools/bio/keys.js";
25
25
  import type { Logger } from "../lib/logger.js";
26
- import type { ResolveBilling } from "../billing/resolver.js";
27
26
  import type { UsageRecorder } from "../billing/usage-recorder.js";
28
27
  import type { ChatProvider, EmbeddingProvider } from "../providers/types.js";
29
28
  import type { SandboxClient } from "../sandbox/client.js";
@@ -57,9 +56,13 @@ export interface DataProfileDeps extends EnvironmentStorePaths {
57
56
  readonly skillsDir: string;
58
57
  /** LLM usage-accounting seam for the profiler agent loop; omitted falls back to the no-op recorder. */
59
58
  readonly usageRecorder?: UsageRecorder;
60
- /** Resolves the billing context stamped as the sandbox pod's OpenCost
61
- * labels. Absent in upstream-less (dev/OSS) wiring — pods spawn unlabeled. */
62
- readonly resolveBilling?: ResolveBilling;
59
+ /**
60
+ * Host-supplied labels for the profiler's sandbox pod, resolved under the
61
+ * profiling session. The map is opaque to the harness: it stamps each entry
62
+ * and interprets none of it. Absent in a wiring that attributes nothing —
63
+ * the pod then carries the harness's own labels only.
64
+ */
65
+ readonly resolvePodLabels?: (session: RunSession) => Promise<Record<string, string>>;
63
66
  }
64
67
  /**
65
68
  * Workflow input. JSON-serialisable — DBOS persists it as the workflow's
@@ -42,8 +42,6 @@ import { ensureSearchIndex, searchIndexName } from "../workspace/search-config.j
42
42
  import { DATA_PROFILE_RUN_LITERAL } from "../contracts/data-profile.js";
43
43
  const DATA_PROFILE_STEP_LITERAL = "profile";
44
44
  const DATA_PROFILE_AGENT_ID = "data-profiler";
45
- /** Resolved-header key carrying the billing-context id (see `ResolveBilling`). */
46
- const BILLING_CONTEXT_HEADER = "X-Inflexa-Billing-Context";
47
45
  /** Sandbox-server exec budget for the profile run. */
48
46
  const DEFAULT_DEADLINE_MS = 300_000;
49
47
  /** Build the canonical artifact path for a staged input file. */
@@ -228,16 +226,18 @@ export async function runDataProfileBody(input, deps) {
228
226
  // from the agent loop would leave the longest wait of all unreported — the same defect the
229
227
  // run panel's activity readout was built to remove.
230
228
  await activity.sandboxInit();
231
- let billingContextId;
232
- if (deps.resolveBilling) {
229
+ let podLabels;
230
+ if (deps.resolvePodLabels) {
233
231
  try {
234
- billingContextId = (await deps.resolveBilling(childSession))[BILLING_CONTEXT_HEADER];
232
+ podLabels = await deps.resolvePodLabels(childSession);
235
233
  }
236
234
  catch (err) {
237
- logger.error("[billing] data-profile billing resolution failed", logger.errorFields(err));
235
+ logger.warn("pod-label resolution failed", logger.errorFields(err));
238
236
  }
239
- if (!billingContextId)
240
- logger.error("[billing] sandbox spawned without billing labels");
237
+ // A wired resolver that yields nothing is the one loud case: the host
238
+ // asked for attribution and the pod spawns without it.
239
+ if (!podLabels || Object.keys(podLabels).length === 0)
240
+ logger.warn("sandbox spawned with no pod labels");
241
241
  }
242
242
  const sandbox = await deps.sandboxClient.createSandbox({
243
243
  runId: DATA_PROFILE_RUN_LITERAL,
@@ -245,7 +245,7 @@ export async function runDataProfileBody(input, deps) {
245
245
  analysisId,
246
246
  childWorkflowId: workflowId,
247
247
  resources: estimateDataProfileResources(stagedInputs),
248
- billing: billingContextId ? { billingContextId, userId: childSession.identity.user } : undefined,
248
+ podLabels,
249
249
  }, mintSandboxIdentity(DATA_PROFILE_RUN_LITERAL));
250
250
  try {
251
251
  // Checkpointed clock, not `Date.now()`: `awaitExec` gates on this absolute
@@ -29,7 +29,6 @@
29
29
  import type { TokenUsageRollup } from "@inflexa-ai/harness/contracts/usage.js";
30
30
  import type { Pool } from "pg";
31
31
  import type { Logger } from "../lib/logger.js";
32
- import type { ResolveBilling } from "../billing/resolver.js";
33
32
  import type { UsageRecorder } from "../billing/usage-recorder.js";
34
33
  import type { CitationResolver } from "../citations/types.js";
35
34
  import type { AgentDefinition, LoopMessage } from "../loop/types.js";
@@ -233,9 +232,13 @@ export interface SandboxStepDeps {
233
232
  /** Write-side embedder for the post-step vector index. */
234
233
  readonly embedding: EmbeddingProvider;
235
234
  readonly sandboxClient: SandboxClient;
236
- /** Resolves the billing context stamped as the sandbox pod's OpenCost
237
- * labels. Absent in upstream-less (dev/OSS) wiring — pods spawn unlabeled. */
238
- readonly resolveBilling?: ResolveBilling;
235
+ /**
236
+ * Host-supplied labels for this step's sandbox pod, resolved under the
237
+ * step's session. The map is opaque to the harness: it stamps each entry
238
+ * and interprets none of it. Absent in a wiring that attributes nothing —
239
+ * the pod then carries the harness's own labels only.
240
+ */
241
+ readonly resolvePodLabels?: (session: RunSession) => Promise<Record<string, string>>;
239
242
  /**
240
243
  * External artifact registration + sync seam. The harness's post-step pipeline
241
244
  * registers each step's outputs through it (filesystem index in the
@@ -46,8 +46,6 @@ import { createBlockerHolder } from "../tools/sandbox/report-blocker.js";
46
46
  * declare it explicitly.
47
47
  */
48
48
  export const DEFAULT_STEP_TIMEOUT_SECONDS = 3600;
49
- /** Resolved-header key carrying the billing-context id (see `ResolveBilling`). */
50
- const BILLING_CONTEXT_HEADER = "X-Inflexa-Billing-Context";
51
49
  /**
52
50
  * DBOS message topic the child uses to notify the parent that it is
53
51
  * about to self-cancel because of a 402 budget-exceeded error. The
@@ -153,7 +151,7 @@ export async function runSandboxStepBody(input, deps) {
153
151
  const identity = await DBOS.runStep(() => Promise.resolve(mintSandboxIdentity(input.runId)), { name: "sandbox.mint" });
154
152
  // (2b) sandbox.create — spawn (or adopt) the machine under the minted
155
153
  // identity. The handle (secret included) is cached so recovery picks the
156
- // same machine back up without re-provisioning. Billing resolution lives
154
+ // same machine back up without re-provisioning. Pod-label resolution lives
157
155
  // INSIDE this step (not as its own step) so the child's step sequence is
158
156
  // unchanged — in-flight workflows resumed across a deploy replay cleanly.
159
157
  const sandboxMeta = {
@@ -166,20 +164,20 @@ export async function runSandboxStepBody(input, deps) {
166
164
  resources: input.resources,
167
165
  };
168
166
  const sandbox = await DBOS.runStep(async () => {
169
- let billing;
170
- if (deps.resolveBilling) {
167
+ let podLabels;
168
+ if (deps.resolvePodLabels) {
171
169
  try {
172
- const bcId = (await deps.resolveBilling(session))[BILLING_CONTEXT_HEADER];
173
- if (bcId)
174
- billing = { billingContextId: bcId, userId: input.runSession.identity.user };
170
+ podLabels = await deps.resolvePodLabels(session);
175
171
  }
176
172
  catch (err) {
177
- logger.error("[billing] step billing resolution failed", { analysisId: input.analysisId, ...logger.errorFields(err) });
173
+ logger.warn("pod-label resolution failed", { analysisId: input.analysisId, ...logger.errorFields(err) });
178
174
  }
179
- if (!billing)
180
- logger.error("[billing] sandbox spawned without billing labels", { analysisId: input.analysisId });
175
+ // A wired resolver that yields nothing is the one loud case: the
176
+ // host asked for attribution and the pod spawns without it.
177
+ if (!podLabels || Object.keys(podLabels).length === 0)
178
+ logger.warn("sandbox spawned with no pod labels", { analysisId: input.analysisId });
181
179
  }
182
- return deps.sandboxClient.createSandbox({ ...sandboxMeta, billing }, identity);
180
+ return deps.sandboxClient.createSandbox({ ...sandboxMeta, podLabels }, identity);
183
181
  }, { name: "sandbox.create" });
184
182
  // Recovery path: re-check `isAlive` on the persisted ref before continuing.
185
183
  // A classified-dead sandbox triggers a fresh `createSandbox` (task 4.6).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.19.2",
3
+ "version": "0.20.0",
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",