@inflexa-ai/harness 0.7.1 → 0.7.2
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/contracts/chat-parts.d.ts +4 -2
- package/dist/contracts/schemas/chat-parts.d.ts +8 -0
- package/dist/contracts/schemas/chat-parts.js +1 -1
- package/dist/provenance/collector.d.ts +39 -2
- package/dist/provenance/collector.js +62 -33
- package/dist/provenance/exec-frame.d.ts +14 -2
- package/dist/provenance/exec-frame.js +27 -4
- package/dist/reference-data/catalog.js +680 -1
- package/dist/tools/workspace/execute-command.js +1 -0
- package/dist/workflows/execute-analysis.d.ts +10 -5
- package/dist/workflows/execute-analysis.js +102 -36
- package/dist/workflows/sandbox-step.d.ts +25 -0
- package/dist/workflows/sandbox-step.js +20 -9
- package/package.json +2 -1
|
@@ -105,8 +105,10 @@ export interface RunStartedPart {
|
|
|
105
105
|
stepCount: number;
|
|
106
106
|
}
|
|
107
107
|
/** `"pending"` = waiting on dependencies; `"queued"` = dependencies satisfied,
|
|
108
|
-
* held until in-flight steps free machine-budget capacity
|
|
109
|
-
|
|
108
|
+
* held until in-flight steps free machine-budget capacity; `"skipped"` =
|
|
109
|
+
* unreachable — an upstream dependency failed or blocked, so the step can
|
|
110
|
+
* never be dispatched. */
|
|
111
|
+
export type StepStatus = "pending" | "queued" | "running" | "completed" | "failed" | "skipped";
|
|
110
112
|
export interface DagStepState {
|
|
111
113
|
id: string;
|
|
112
114
|
name: string;
|
|
@@ -135,6 +135,8 @@ export declare const StepStatusSchema: z.ZodEnum<{
|
|
|
135
135
|
running: "running";
|
|
136
136
|
completed: "completed";
|
|
137
137
|
failed: "failed";
|
|
138
|
+
skipped: "skipped";
|
|
139
|
+
queued: "queued";
|
|
138
140
|
}>;
|
|
139
141
|
export declare const DagStepStateSchema: z.ZodObject<{
|
|
140
142
|
id: z.ZodString;
|
|
@@ -145,6 +147,8 @@ export declare const DagStepStateSchema: z.ZodObject<{
|
|
|
145
147
|
running: "running";
|
|
146
148
|
completed: "completed";
|
|
147
149
|
failed: "failed";
|
|
150
|
+
skipped: "skipped";
|
|
151
|
+
queued: "queued";
|
|
148
152
|
}>;
|
|
149
153
|
level: z.ZodNumber;
|
|
150
154
|
dependsOn: z.ZodArray<z.ZodString>;
|
|
@@ -166,6 +170,8 @@ export declare const DagStatePartSchema: z.ZodObject<{
|
|
|
166
170
|
running: "running";
|
|
167
171
|
completed: "completed";
|
|
168
172
|
failed: "failed";
|
|
173
|
+
skipped: "skipped";
|
|
174
|
+
queued: "queued";
|
|
169
175
|
}>;
|
|
170
176
|
level: z.ZodNumber;
|
|
171
177
|
dependsOn: z.ZodArray<z.ZodString>;
|
|
@@ -530,6 +536,8 @@ export declare const CortexChatPartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
|
|
|
530
536
|
running: "running";
|
|
531
537
|
completed: "completed";
|
|
532
538
|
failed: "failed";
|
|
539
|
+
skipped: "skipped";
|
|
540
|
+
queued: "queued";
|
|
533
541
|
}>;
|
|
534
542
|
level: z.ZodNumber;
|
|
535
543
|
dependsOn: z.ZodArray<z.ZodString>;
|
|
@@ -96,7 +96,7 @@ export const RunStartedPartSchema = z.object({
|
|
|
96
96
|
stepCount: z.number(),
|
|
97
97
|
});
|
|
98
98
|
// ── DAG State ───────────────────────────────────────────────────────
|
|
99
|
-
export const StepStatusSchema = z.enum(["pending", "running", "completed", "failed"]);
|
|
99
|
+
export const StepStatusSchema = z.enum(["pending", "queued", "running", "completed", "failed", "skipped"]);
|
|
100
100
|
export const DagStepStateSchema = z
|
|
101
101
|
.object({
|
|
102
102
|
id: z.string(),
|
|
@@ -39,6 +39,25 @@ export interface InputClassificationContext {
|
|
|
39
39
|
stepId?: string;
|
|
40
40
|
runId?: string;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The outcome of classifying one read: an input edge the step may assert, or
|
|
44
|
+
* an explicit refusal naming the producing step it would have pointed at.
|
|
45
|
+
*
|
|
46
|
+
* A refusal is a value rather than a thrown error or a `data` classification.
|
|
47
|
+
* Throwing would fail an exec over bookkeeping, and reclassifying as `data`
|
|
48
|
+
* would launder a sibling's scratch file into an attestable analysis-level
|
|
49
|
+
* input — a different fabrication, not a fix. The scraped ids ride along
|
|
50
|
+
* because they are the only thing that makes a drop attributable to a
|
|
51
|
+
* producing step in a log record.
|
|
52
|
+
*/
|
|
53
|
+
export type ReadClassification = {
|
|
54
|
+
readonly admissible: true;
|
|
55
|
+
readonly context: InputClassificationContext;
|
|
56
|
+
} | {
|
|
57
|
+
readonly admissible: false;
|
|
58
|
+
readonly refRunId: string;
|
|
59
|
+
readonly refStepId: string;
|
|
60
|
+
};
|
|
42
61
|
/**
|
|
43
62
|
* Build classification context for a file read using known step metadata.
|
|
44
63
|
*
|
|
@@ -50,7 +69,7 @@ export interface InputClassificationContext {
|
|
|
50
69
|
* any dependsOn entry), this falls back to path extraction — see the
|
|
51
70
|
* prior-run branch below for why.
|
|
52
71
|
*/
|
|
53
|
-
export declare function classifyReadPath(relativePath: string, ownStepId: string, ownRunId: string, dependsOn?: string[]):
|
|
72
|
+
export declare function classifyReadPath(relativePath: string, ownStepId: string, ownRunId: string, dependsOn?: string[]): ReadClassification;
|
|
54
73
|
export interface ProvenanceCollectorOptions {
|
|
55
74
|
stepId: string;
|
|
56
75
|
runId: string;
|
|
@@ -75,12 +94,26 @@ export declare class ProvenanceCollector {
|
|
|
75
94
|
private readonly fileToolRecords;
|
|
76
95
|
/** Command execution records, keyed by output path (relative to /artifacts). */
|
|
77
96
|
private readonly commandRecords;
|
|
97
|
+
/** Refused read paths already narrated, so the same edge is reported once per step. */
|
|
98
|
+
private readonly narratedRefusals;
|
|
78
99
|
constructor(opts: ProvenanceCollectorOptions);
|
|
79
100
|
/**
|
|
80
101
|
* Set the input file ID map (passed by reference from the data-mount filesystem).
|
|
81
102
|
* Called after construction when the data mount filesystem is available.
|
|
82
103
|
*/
|
|
83
104
|
setInputFileIdMap(map: Map<string, string>): void;
|
|
105
|
+
/**
|
|
106
|
+
* Claim the right to narrate a refused read, true only the first time this
|
|
107
|
+
* step sees the path.
|
|
108
|
+
*
|
|
109
|
+
* A refusal reports one fact — a lineage edge that will not exist — and the
|
|
110
|
+
* fact does not change when the same file is read again. Reads dedup within
|
|
111
|
+
* one frame but not across them, and a step issues many execs against a tree
|
|
112
|
+
* whose watch set spans the whole analysis, so an undeduped narration is a
|
|
113
|
+
* steady stream rather than an exception. The signal exists to be noticed;
|
|
114
|
+
* emitting it at that volume is what teaches a reader to filter it away.
|
|
115
|
+
*/
|
|
116
|
+
claimRefusalNarration(relativePath: string): boolean;
|
|
84
117
|
/**
|
|
85
118
|
* Track a file read from a read-only mount.
|
|
86
119
|
* Called by sandbox exec provenance frame processing.
|
|
@@ -92,8 +125,12 @@ export declare class ProvenanceCollector {
|
|
|
92
125
|
* @param hash - SHA-256 hash of the file content, or null if file no longer exists
|
|
93
126
|
* @param context - Explicit classification context from the caller. When provided,
|
|
94
127
|
* the collector uses it directly instead of classifying from the path.
|
|
128
|
+
* @returns the tracked ref, or `null` when the caller supplied no context and the
|
|
129
|
+
* path classifies as inadmissible — an undeclared same-run sibling, which must
|
|
130
|
+
* not become an attestation target.
|
|
95
131
|
*/
|
|
96
|
-
trackInputAccess(mountPath: string, relativePath: string, hash: string | null, context
|
|
132
|
+
trackInputAccess(mountPath: string, relativePath: string, hash: string | null, context: InputClassificationContext): InputRef;
|
|
133
|
+
trackInputAccess(mountPath: string, relativePath: string, hash: string | null): InputRef | null;
|
|
97
134
|
/**
|
|
98
135
|
* Record a file tool write (write_file, edit_file).
|
|
99
136
|
* Called after the data-mount filesystem records the artifact.
|
|
@@ -42,32 +42,45 @@ function inferScriptPath(command, args) {
|
|
|
42
42
|
*/
|
|
43
43
|
export function classifyReadPath(relativePath, ownStepId, ownRunId, dependsOn) {
|
|
44
44
|
if (relativePath.startsWith("data/") || relativePath.startsWith("dataprofile/")) {
|
|
45
|
-
return { source: "data" };
|
|
45
|
+
return { admissible: true, context: { source: "data" } };
|
|
46
46
|
}
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const upstreamPrefix = `runs/${ownRunId}/${depStepId}/`;
|
|
56
|
-
if (relativePath.startsWith(upstreamPrefix)) {
|
|
57
|
-
return { source: "upstream", stepId: depStepId, runId: ownRunId };
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
// Same-run sibling step. dependsOn only drives topo-sort ordering, not
|
|
62
|
-
// read authorization, so a read outside dependsOn is still a valid
|
|
63
|
-
// upstream input — just extract the stepId from the path.
|
|
47
|
+
// Everything under this run resolves off ONE segment — the first one after
|
|
48
|
+
// the run id, which names the step owning that subtree. It is read once and
|
|
49
|
+
// compared, rather than each case prefix-matching `runs/{runId}/{id}/`
|
|
50
|
+
// independently, because a trailing separator is not guaranteed to be
|
|
51
|
+
// present: an `opendir` of a step directory surfaces the bare name. Under
|
|
52
|
+
// per-case prefixes every such directory read misses its own case and lands
|
|
53
|
+
// on the refusal below — a step refusing an edge to itself, or to a
|
|
54
|
+
// dependency it did declare.
|
|
64
55
|
const ownRunPrefix = `runs/${ownRunId}/`;
|
|
65
56
|
if (relativePath.startsWith(ownRunPrefix)) {
|
|
66
57
|
const afterRun = relativePath.slice(ownRunPrefix.length);
|
|
67
58
|
const slashIdx = afterRun.indexOf("/");
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
return { source: "
|
|
59
|
+
const head = slashIdx >= 0 ? afterRun.slice(0, slashIdx) : afterRun;
|
|
60
|
+
if (head === ownStepId) {
|
|
61
|
+
return { admissible: true, context: { source: "artifacts", stepId: ownStepId, runId: ownRunId } };
|
|
62
|
+
}
|
|
63
|
+
if (dependsOn?.includes(head)) {
|
|
64
|
+
return { admissible: true, context: { source: "upstream", stepId: head, runId: ownRunId } };
|
|
65
|
+
}
|
|
66
|
+
// A same-run step this one never declared. It carries no ordering
|
|
67
|
+
// guarantee relative to this step: its directory is mounted read-write in
|
|
68
|
+
// its own container and may be mid-write, or hold a scratch file deleted
|
|
69
|
+
// before this step reconciles, so nothing observed there is a stable
|
|
70
|
+
// artifact this step can be said to have consumed. Declaration is the
|
|
71
|
+
// available proof of stability — the scheduler admits a step only once
|
|
72
|
+
// every declared dependency has completed, and a completed step never
|
|
73
|
+
// writes into its tree again. An absent `dependsOn` therefore refuses
|
|
74
|
+
// every sibling: absence of the declaration is not evidence of stability.
|
|
75
|
+
//
|
|
76
|
+
// `head` is the refusal's diagnostic attribution only; unlike the
|
|
77
|
+
// prior-run branch below it feeds no classification. It is a step id for
|
|
78
|
+
// any path that descends into a subtree, and a best guess for a bare
|
|
79
|
+
// name, which the run directory also admits for its own run-scoped files
|
|
80
|
+
// (`runs/{runId}/synthesis.json`) — indistinguishable from a step
|
|
81
|
+
// directory by path alone, and refused either way.
|
|
82
|
+
if (head) {
|
|
83
|
+
return { admissible: false, refRunId: ownRunId, refStepId: head };
|
|
71
84
|
}
|
|
72
85
|
}
|
|
73
86
|
// Prior-run fallback: a read under `runs/{otherRunId}/...` that is neither
|
|
@@ -80,10 +93,10 @@ export function classifyReadPath(relativePath, ownStepId, ownRunId, dependsOn) {
|
|
|
80
93
|
const priorRunId = parts[1];
|
|
81
94
|
const priorStepId = parts[2];
|
|
82
95
|
if (priorRunId && priorStepId) {
|
|
83
|
-
return { source: "prior", stepId: priorStepId, runId: priorRunId };
|
|
96
|
+
return { admissible: true, context: { source: "prior", stepId: priorStepId, runId: priorRunId } };
|
|
84
97
|
}
|
|
85
98
|
}
|
|
86
|
-
return { source: "data" };
|
|
99
|
+
return { admissible: true, context: { source: "data" } };
|
|
87
100
|
}
|
|
88
101
|
export class ProvenanceCollector {
|
|
89
102
|
stepId;
|
|
@@ -103,6 +116,8 @@ export class ProvenanceCollector {
|
|
|
103
116
|
fileToolRecords = new Map();
|
|
104
117
|
/** Command execution records, keyed by output path (relative to /artifacts). */
|
|
105
118
|
commandRecords = new Map();
|
|
119
|
+
/** Refused read paths already narrated, so the same edge is reported once per step. */
|
|
120
|
+
narratedRefusals = new Set();
|
|
106
121
|
constructor(opts) {
|
|
107
122
|
this.stepId = opts.stepId;
|
|
108
123
|
this.runId = opts.runId;
|
|
@@ -117,24 +132,38 @@ export class ProvenanceCollector {
|
|
|
117
132
|
this.inputFileIdMap = map;
|
|
118
133
|
}
|
|
119
134
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
135
|
+
* Claim the right to narrate a refused read, true only the first time this
|
|
136
|
+
* step sees the path.
|
|
122
137
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* the collector uses it directly instead of classifying from the path.
|
|
138
|
+
* A refusal reports one fact — a lineage edge that will not exist — and the
|
|
139
|
+
* fact does not change when the same file is read again. Reads dedup within
|
|
140
|
+
* one frame but not across them, and a step issues many execs against a tree
|
|
141
|
+
* whose watch set spans the whole analysis, so an undeduped narration is a
|
|
142
|
+
* steady stream rather than an exception. The signal exists to be noticed;
|
|
143
|
+
* emitting it at that volume is what teaches a reader to filter it away.
|
|
130
144
|
*/
|
|
145
|
+
claimRefusalNarration(relativePath) {
|
|
146
|
+
if (this.narratedRefusals.has(relativePath))
|
|
147
|
+
return false;
|
|
148
|
+
this.narratedRefusals.add(relativePath);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
131
151
|
trackInputAccess(mountPath, relativePath, hash, context) {
|
|
132
152
|
const key = `${mountPath}:${relativePath}`;
|
|
133
153
|
const existing = this.inputAccesses.get(key);
|
|
134
154
|
if (existing)
|
|
135
155
|
return existing;
|
|
136
156
|
// Use explicit context if provided, otherwise fall back to path classification
|
|
137
|
-
|
|
157
|
+
let classification;
|
|
158
|
+
if (context) {
|
|
159
|
+
classification = context;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const classified = classifyReadPath(relativePath, this.stepId, this.runId, this.dependsOn);
|
|
163
|
+
if (!classified.admissible)
|
|
164
|
+
return null;
|
|
165
|
+
classification = classified.context;
|
|
166
|
+
}
|
|
138
167
|
// A still-absolute `relativePath` is a report the caller could not
|
|
139
168
|
// relativize — it names somewhere outside the mount. It rides verbatim:
|
|
140
169
|
// prepending the mount root would forge an in-tree name for a foreign
|
|
@@ -8,10 +8,20 @@
|
|
|
8
8
|
* `runs/{priorRunId}/...`) so `classifyReadPath` and the collector's
|
|
9
9
|
* `stepPrefix` normalization see the shapes they expect.
|
|
10
10
|
*
|
|
11
|
+
* A read naming a same-run sibling this step never declared is refused here,
|
|
12
|
+
* before it reaches the collector: a ref the collector never holds cannot
|
|
13
|
+
* become an attestation target, so the edge is unrepresentable rather than
|
|
14
|
+
* merely filtered downstream. Each refused path is logged once per step — an
|
|
15
|
+
* edge asserted over a step that was still writing is invisible by nature, so a
|
|
16
|
+
* silent drop would rebuild exactly the blind spot the refusal exists to close,
|
|
17
|
+
* while re-narrating a path already reported adds no fact and buries the rest.
|
|
18
|
+
*
|
|
11
19
|
* Degrades safely: a disabled or absent frame records the command with no
|
|
12
|
-
* inputs and no writes (its outputs fall back to leaves at registration)
|
|
13
|
-
*
|
|
20
|
+
* inputs and no writes (its outputs fall back to leaves at registration), and a
|
|
21
|
+
* frame whose every read is refused still records its command. It never throws —
|
|
22
|
+
* provenance is best-effort and must not fail an exec.
|
|
14
23
|
*/
|
|
24
|
+
import type { Logger } from "../lib/logger.js";
|
|
15
25
|
import type { ProvenanceFrame } from "../sandbox/types.js";
|
|
16
26
|
import { type ProvenanceCollector } from "./collector.js";
|
|
17
27
|
/**
|
|
@@ -32,6 +42,8 @@ export interface FeedExecFrameArgs {
|
|
|
32
42
|
readonly durationMs: number | null;
|
|
33
43
|
/** The frame surfaced on `ExecResult.provenance` (may be absent). */
|
|
34
44
|
readonly provenance?: ProvenanceFrame;
|
|
45
|
+
/** Diagnostics seam. Left unwired, refusals still happen but go unnarrated. */
|
|
46
|
+
readonly logger?: Logger;
|
|
35
47
|
}
|
|
36
48
|
/**
|
|
37
49
|
* Translate one exec's frame into a collector record. Per-command input
|
|
@@ -8,10 +8,20 @@
|
|
|
8
8
|
* `runs/{priorRunId}/...`) so `classifyReadPath` and the collector's
|
|
9
9
|
* `stepPrefix` normalization see the shapes they expect.
|
|
10
10
|
*
|
|
11
|
+
* A read naming a same-run sibling this step never declared is refused here,
|
|
12
|
+
* before it reaches the collector: a ref the collector never holds cannot
|
|
13
|
+
* become an attestation target, so the edge is unrepresentable rather than
|
|
14
|
+
* merely filtered downstream. Each refused path is logged once per step — an
|
|
15
|
+
* edge asserted over a step that was still writing is invisible by nature, so a
|
|
16
|
+
* silent drop would rebuild exactly the blind spot the refusal exists to close,
|
|
17
|
+
* while re-narrating a path already reported adds no fact and buries the rest.
|
|
18
|
+
*
|
|
11
19
|
* Degrades safely: a disabled or absent frame records the command with no
|
|
12
|
-
* inputs and no writes (its outputs fall back to leaves at registration)
|
|
13
|
-
*
|
|
20
|
+
* inputs and no writes (its outputs fall back to leaves at registration), and a
|
|
21
|
+
* frame whose every read is refused still records its command. It never throws —
|
|
22
|
+
* provenance is best-effort and must not fail an exec.
|
|
14
23
|
*/
|
|
24
|
+
import { createNoopLogger } from "../lib/console-logger.js";
|
|
15
25
|
import { classifyReadPath } from "./collector.js";
|
|
16
26
|
/**
|
|
17
27
|
* Strip the `/{resourceId}/` mount prefix from an absolute frame path.
|
|
@@ -40,11 +50,24 @@ export function feedExecFrame(args) {
|
|
|
40
50
|
collector.recordCommandExecution(cmd, [...cmdArgs], code, duration, [], undefined, []);
|
|
41
51
|
return;
|
|
42
52
|
}
|
|
53
|
+
// Resolved once so the read loop logs unconditionally instead of threading
|
|
54
|
+
// `?.` through every diagnostic.
|
|
55
|
+
const logger = (args.logger ?? createNoopLogger()).named("exec-frame");
|
|
43
56
|
const commandReads = [];
|
|
44
57
|
for (const read of provenance.reads) {
|
|
45
58
|
const rel = stripMountPrefix(mountRoot, read.path);
|
|
46
|
-
const
|
|
47
|
-
|
|
59
|
+
const classification = classifyReadPath(rel, collector.stepId, collector.runId, collector.dependsOn);
|
|
60
|
+
if (!classification.admissible) {
|
|
61
|
+
if (collector.claimRefusalNarration(rel)) {
|
|
62
|
+
logger.warn("refusing lineage edge to an undeclared same-run path", {
|
|
63
|
+
path: rel,
|
|
64
|
+
refRunId: classification.refRunId,
|
|
65
|
+
refStepId: classification.refStepId,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
commandReads.push(collector.trackInputAccess(mountRoot, rel, null, classification.context));
|
|
48
71
|
}
|
|
49
72
|
const writes = provenance.writes.map((w) => ({
|
|
50
73
|
path: stripMountPrefix(mountRoot, w.path),
|