@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.
- package/dist/literature/sources/pubmed.js +2 -1
- package/dist/sandbox/k8s-client.js +11 -18
- package/dist/sandbox/types.d.ts +4 -6
- package/dist/tasks/data-profile.d.ts +7 -4
- package/dist/tasks/data-profile.js +9 -9
- package/dist/workflows/sandbox-step.d.ts +7 -4
- package/dist/workflows/sandbox-step.js +10 -12
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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:
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
...
|
|
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
|
-
...
|
|
250
|
+
...attributionLabels,
|
|
258
251
|
},
|
|
259
252
|
},
|
|
260
253
|
spec: podSpec,
|
package/dist/sandbox/types.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
247
|
-
*
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
/**
|
|
61
|
-
*
|
|
62
|
-
|
|
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
|
|
232
|
-
if (deps.
|
|
229
|
+
let podLabels;
|
|
230
|
+
if (deps.resolvePodLabels) {
|
|
233
231
|
try {
|
|
234
|
-
|
|
232
|
+
podLabels = await deps.resolvePodLabels(childSession);
|
|
235
233
|
}
|
|
236
234
|
catch (err) {
|
|
237
|
-
logger.
|
|
235
|
+
logger.warn("pod-label resolution failed", logger.errorFields(err));
|
|
238
236
|
}
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
237
|
-
*
|
|
238
|
-
|
|
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.
|
|
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
|
|
170
|
-
if (deps.
|
|
167
|
+
let podLabels;
|
|
168
|
+
if (deps.resolvePodLabels) {
|
|
171
169
|
try {
|
|
172
|
-
|
|
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.
|
|
173
|
+
logger.warn("pod-label resolution failed", { analysisId: input.analysisId, ...logger.errorFields(err) });
|
|
178
174
|
}
|
|
179
|
-
|
|
180
|
-
|
|
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,
|
|
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