@jinn-network/jinn-layer 0.1.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/adapters/contribution-adapter.d.ts +18 -0
- package/dist/adapters/contribution-adapter.js +134 -0
- package/dist/adapters/corpus-adapter.d.ts +12 -0
- package/dist/adapters/corpus-adapter.js +212 -0
- package/dist/adapters/index.d.ts +17 -0
- package/dist/adapters/index.js +17 -0
- package/dist/adapters/json-map-store.d.ts +4 -0
- package/dist/adapters/json-map-store.js +31 -0
- package/dist/adapters/local-learning-adapter.d.ts +25 -0
- package/dist/adapters/local-learning-adapter.js +49 -0
- package/dist/adapters/skills-adapter.d.ts +8 -0
- package/dist/adapters/skills-adapter.js +55 -0
- package/dist/admission-policy.d.ts +38 -0
- package/dist/admission-policy.js +17 -0
- package/dist/bin/jinn-distill-mcp.d.ts +2 -0
- package/dist/bin/jinn-distill-mcp.js +10 -0
- package/dist/bin/jinn-layer.d.ts +7 -0
- package/dist/bin/jinn-layer.js +15 -0
- package/dist/bridge-fetch-evidence.d.ts +102 -0
- package/dist/bridge-fetch-evidence.js +677 -0
- package/dist/bridge-verdict-source.d.ts +47 -0
- package/dist/bridge-verdict-source.js +216 -0
- package/dist/bridge.d.ts +247 -0
- package/dist/bridge.js +439 -0
- package/dist/capture.d.ts +98 -0
- package/dist/capture.js +617 -0
- package/dist/cli.d.ts +148 -0
- package/dist/cli.js +2221 -0
- package/dist/cluster-selection.d.ts +34 -0
- package/dist/cluster-selection.js +135 -0
- package/dist/cluster.d.ts +89 -0
- package/dist/cluster.js +142 -0
- package/dist/consume.d.ts +139 -0
- package/dist/consume.js +280 -0
- package/dist/corpus-probes.d.ts +61 -0
- package/dist/corpus-probes.js +120 -0
- package/dist/corpus-store.d.ts +16 -0
- package/dist/corpus-store.js +101 -0
- package/dist/distill-captures.d.ts +56 -0
- package/dist/distill-captures.js +283 -0
- package/dist/distill-feedback.d.ts +20 -0
- package/dist/distill-feedback.js +30 -0
- package/dist/distill-llm.d.ts +138 -0
- package/dist/distill-llm.js +438 -0
- package/dist/distill-mcp-server.d.ts +30 -0
- package/dist/distill-mcp-server.js +281 -0
- package/dist/distill-mode.d.ts +62 -0
- package/dist/distill-mode.js +107 -0
- package/dist/distill-progress.d.ts +78 -0
- package/dist/distill-progress.js +69 -0
- package/dist/distill-prompt.d.ts +34 -0
- package/dist/distill-prompt.js +85 -0
- package/dist/distill-render.d.ts +123 -0
- package/dist/distill-render.js +303 -0
- package/dist/distill-runs.d.ts +28 -0
- package/dist/distill-runs.js +39 -0
- package/dist/distill-traces.d.ts +67 -0
- package/dist/distill-traces.js +211 -0
- package/dist/distill.d.ts +180 -0
- package/dist/distill.js +337 -0
- package/dist/distiller.d.ts +118 -0
- package/dist/distiller.js +142 -0
- package/dist/envelope.d.ts +9 -0
- package/dist/envelope.js +9 -0
- package/dist/eval-prep.d.ts +129 -0
- package/dist/eval-prep.js +523 -0
- package/dist/execution-publish.d.ts +62 -0
- package/dist/execution-publish.js +72 -0
- package/dist/gate.d.ts +39 -0
- package/dist/gate.js +106 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +50 -0
- package/dist/ipfs-cid.d.ts +18 -0
- package/dist/ipfs-cid.js +149 -0
- package/dist/layer-default.d.ts +3 -0
- package/dist/layer-default.js +10 -0
- package/dist/ledger.d.ts +62 -0
- package/dist/ledger.js +104 -0
- package/dist/measurement.d.ts +90 -0
- package/dist/measurement.js +134 -0
- package/dist/path-safety.d.ts +19 -0
- package/dist/path-safety.js +303 -0
- package/dist/pipeline.d.ts +94 -0
- package/dist/pipeline.js +124 -0
- package/dist/plugin-wiring.d.ts +4 -0
- package/dist/plugin-wiring.js +54 -0
- package/dist/preview.d.ts +31 -0
- package/dist/preview.js +28 -0
- package/dist/process-contract.d.ts +303 -0
- package/dist/process-contract.js +118 -0
- package/dist/publish-skill.d.ts +86 -0
- package/dist/publish-skill.js +116 -0
- package/dist/publish.d.ts +294 -0
- package/dist/publish.js +1090 -0
- package/dist/scripts/scan-skills-registry.d.ts +24 -0
- package/dist/scripts/scan-skills-registry.js +207 -0
- package/dist/seed-import/curated-batch.d.ts +36 -0
- package/dist/seed-import/curated-batch.js +103 -0
- package/dist/seed-import/episode-execute.d.ts +50 -0
- package/dist/seed-import/episode-execute.js +235 -0
- package/dist/seed-import/episode-fetch.d.ts +119 -0
- package/dist/seed-import/episode-fetch.js +137 -0
- package/dist/seed-import/episode-plan.d.ts +11 -0
- package/dist/seed-import/episode-plan.js +27 -0
- package/dist/seed-import/episode-report.d.ts +39 -0
- package/dist/seed-import/episode-report.js +36 -0
- package/dist/seed-import/execute.d.ts +61 -0
- package/dist/seed-import/execute.js +245 -0
- package/dist/seed-import/fetch.d.ts +62 -0
- package/dist/seed-import/fetch.js +82 -0
- package/dist/seed-import/licence.d.ts +17 -0
- package/dist/seed-import/licence.js +36 -0
- package/dist/seed-import/plan.d.ts +10 -0
- package/dist/seed-import/plan.js +21 -0
- package/dist/seed-import/report.d.ts +34 -0
- package/dist/seed-import/report.js +31 -0
- package/dist/seed-import/state.d.ts +72 -0
- package/dist/seed-import/state.js +120 -0
- package/dist/signal.d.ts +36 -0
- package/dist/signal.js +48 -0
- package/dist/signing.d.ts +8 -0
- package/dist/signing.js +21 -0
- package/dist/skill-package.d.ts +70 -0
- package/dist/skill-package.js +141 -0
- package/dist/skill.d.ts +21 -0
- package/dist/skill.js +77 -0
- package/dist/snapshot-transcript.d.ts +54 -0
- package/dist/snapshot-transcript.js +117 -0
- package/dist/three-arm-run.d.ts +27 -0
- package/dist/three-arm-run.js +41 -0
- package/package.json +60 -0
package/dist/capture.js
ADDED
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness-layer capture path — the privacy surface (spec §5).
|
|
3
|
+
*
|
|
4
|
+
* `capture(task, opts)` runs a captured task through the mandatory scrub
|
|
5
|
+
* pipeline and returns a `PendingEnvelope`: the captured-but-not-yet-consented
|
|
6
|
+
* state. Per the 2026-07-02 schema review, consent flags on `TraceEnvelopeV0`
|
|
7
|
+
* are literal `true`, so the pre-consent state cannot BE a `TraceEnvelopeV0` —
|
|
8
|
+
* the pending wrapper converts to one only at consent (preview renders the
|
|
9
|
+
* would-be envelope; the Task 4 publish path performs the real conversion).
|
|
10
|
+
*
|
|
11
|
+
* Fail-closed: any scrub-stage failure throws `CaptureScrubError` — no
|
|
12
|
+
* envelope, no partial output, never a silent pass.
|
|
13
|
+
*
|
|
14
|
+
* Scrub pipeline: reuses the seller-side pipeline from
|
|
15
|
+
* `@jinn-network/core/scrub` (key-policy → openredaction →
|
|
16
|
+
* plain-patterns → secretlint → optional ML PII) via `scrubCaptureSpans()`.
|
|
17
|
+
* The email/home-path gap-closers this file originally carried graduated
|
|
18
|
+
* into the shared plain-patterns stage (#1330), so the daemon publish path
|
|
19
|
+
* and this capture path share one implementation.
|
|
20
|
+
*
|
|
21
|
+
* Fitting rule (docs/envelope-v0.md): the conversion MUST produce envelopes
|
|
22
|
+
* inside the frozen caps. Over-long sessions are head/tail-sampled to
|
|
23
|
+
* `MAX_STEPS` (never dropped); oversized step attributes are truncated with
|
|
24
|
+
* the cut keys listed in the step's `truncatedKeys` receipt (never silent —
|
|
25
|
+
* both surface in the `ScrubReport`).
|
|
26
|
+
*
|
|
27
|
+
* Plan: docs/superpowers/plans/2026-07-02-jinn-harness-network-v0-plan.md
|
|
28
|
+
* Task 3 (issue #1310).
|
|
29
|
+
*/
|
|
30
|
+
import { buildScrubPipeline, DEFAULT_KEY_POLICY, ScrubPipeline, scrubCaptureSpans, } from '@jinn-network/core/scrub';
|
|
31
|
+
import { MAX_STEPS, MAX_STEP_ATTRIBUTES_BYTES, parseTraceEnvelopeV0, TRACE_ENVELOPE_SCHEMA_VERSION, } from './envelope.js';
|
|
32
|
+
// The captured-task schema + parser moved into @jinn-network/core in C2
|
|
33
|
+
// (#1833). Re-export them so every downstream `./capture.js` importer of
|
|
34
|
+
// these symbols keeps working unchanged. The scrub half of this file stays
|
|
35
|
+
// here (C5's move).
|
|
36
|
+
export { CapturedTaskSchema, parseCapturedTask } from '@jinn-network/core';
|
|
37
|
+
// A re-export does not create a local binding, and capture() below references
|
|
38
|
+
// CapturedTask and parseCapturedTask below — import them locally too.
|
|
39
|
+
import { parseCapturedTask } from '@jinn-network/core';
|
|
40
|
+
export const PENDING_ENVELOPE_KIND = 'jinn.pending-envelope.v0';
|
|
41
|
+
/** A scrub-stage failure. capture() is fail-closed: no envelope was produced. */
|
|
42
|
+
export class CaptureScrubError extends Error {
|
|
43
|
+
constructor(cause) {
|
|
44
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
45
|
+
super(`scrub failed — capture aborted, no envelope produced (fail-closed): ${detail}`);
|
|
46
|
+
this.name = 'CaptureScrubError';
|
|
47
|
+
this.cause = cause;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* ScrubPipeline that chains the base pipeline with the capture-local stages
|
|
52
|
+
* and records each run's input + result, in order. `scrubCaptureSpans()`
|
|
53
|
+
* runs the pipeline exactly once per span, in span order, so run index i
|
|
54
|
+
* corresponds to step i — that correspondence is what lets the redaction
|
|
55
|
+
* diff carry `before` values without re-implementing the span walk.
|
|
56
|
+
*/
|
|
57
|
+
class RecordingScrubPipeline extends ScrubPipeline {
|
|
58
|
+
chain;
|
|
59
|
+
runs = [];
|
|
60
|
+
constructor(chain) {
|
|
61
|
+
super([]);
|
|
62
|
+
this.chain = chain;
|
|
63
|
+
}
|
|
64
|
+
get components() {
|
|
65
|
+
return this.chain.flatMap((p) => p.components);
|
|
66
|
+
}
|
|
67
|
+
async run(attributes) {
|
|
68
|
+
let current = attributes;
|
|
69
|
+
const redactions = [];
|
|
70
|
+
for (const pipeline of this.chain) {
|
|
71
|
+
const result = await pipeline.run(current);
|
|
72
|
+
current = result.attributes;
|
|
73
|
+
redactions.push(...result.redactions);
|
|
74
|
+
}
|
|
75
|
+
const result = { attributes: current, redactions };
|
|
76
|
+
this.runs.push({ before: attributes, result });
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Fitting (docs/envelope-v0.md, "Fitting rule")
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
const TRUNCATION_SUFFIX = '…[truncated]';
|
|
84
|
+
function serialisedBytes(value) {
|
|
85
|
+
return Buffer.byteLength(JSON.stringify(value), 'utf8');
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Truncate a step's scrubbed attributes until they fit
|
|
89
|
+
* `MAX_STEP_ATTRIBUTES_BYTES`, cutting the largest value first. Returns the
|
|
90
|
+
* fitted attributes plus the cut keys — the visible truncation receipt.
|
|
91
|
+
*/
|
|
92
|
+
function fitAttributes(attributes) {
|
|
93
|
+
const out = { ...attributes };
|
|
94
|
+
const cutsByKey = new Map();
|
|
95
|
+
// JSON escaping can make the serialised form of a cut string larger than
|
|
96
|
+
// the raw byte count suggests, so a single pass may land slightly over —
|
|
97
|
+
// the loop re-cuts (the same key if it is still the largest) until the
|
|
98
|
+
// step fits. Each iteration strictly shrinks the chosen value, so the only
|
|
99
|
+
// non-terminating shape is thousands of already-empty values: guard it.
|
|
100
|
+
let guard = Object.keys(out).length * 4 + 8;
|
|
101
|
+
while (serialisedBytes(out) > MAX_STEP_ATTRIBUTES_BYTES) {
|
|
102
|
+
guard -= 1;
|
|
103
|
+
const largest = Object.entries(out)
|
|
104
|
+
.sort((a, b) => serialisedBytes(b[1]) - serialisedBytes(a[1]))[0];
|
|
105
|
+
if (!largest || guard <= 0) {
|
|
106
|
+
throw new CaptureScrubError(new Error(`step attributes cannot be fitted under ${MAX_STEP_ATTRIBUTES_BYTES} bytes`));
|
|
107
|
+
}
|
|
108
|
+
const [key, value] = largest;
|
|
109
|
+
const overshoot = serialisedBytes(out) - MAX_STEP_ATTRIBUTES_BYTES;
|
|
110
|
+
const serialised = typeof value === 'string' ? value : JSON.stringify(value);
|
|
111
|
+
// Cut by bytes with a safety margin for JSON escaping + the suffix.
|
|
112
|
+
const keepBytes = Math.max(0, Buffer.byteLength(serialised, 'utf8') - overshoot - TRUNCATION_SUFFIX.length - 16);
|
|
113
|
+
const after = Buffer.from(serialised, 'utf8')
|
|
114
|
+
.subarray(0, keepBytes)
|
|
115
|
+
.toString('utf8')
|
|
116
|
+
// A byte cut can split a multi-byte character; drop the replacement char.
|
|
117
|
+
.replace(/�+$/, '') + TRUNCATION_SUFFIX;
|
|
118
|
+
out[key] = after;
|
|
119
|
+
const existing = cutsByKey.get(key);
|
|
120
|
+
if (existing) {
|
|
121
|
+
existing.after = after; // re-cut: keep the ORIGINAL value as `before`
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
cutsByKey.set(key, { key, before: attributes[key], after });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const cuts = [...cutsByKey.values()];
|
|
128
|
+
return { attributes: out, truncatedKeys: cuts.map((c) => c.key), cuts };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Head/tail-sample an over-long step list down to `MAX_STEPS` (keep the
|
|
132
|
+
* first half and the last half; the middle is sampled out). Never drops the
|
|
133
|
+
* session — that is the fitting rule's explicit prohibition.
|
|
134
|
+
*/
|
|
135
|
+
function sampleSteps(steps) {
|
|
136
|
+
if (steps.length <= MAX_STEPS)
|
|
137
|
+
return { steps, sampledOut: 0 };
|
|
138
|
+
const head = Math.ceil(MAX_STEPS / 2);
|
|
139
|
+
const tail = MAX_STEPS - head;
|
|
140
|
+
return {
|
|
141
|
+
steps: [...steps.slice(0, head), ...steps.slice(steps.length - tail)],
|
|
142
|
+
sampledOut: steps.length - MAX_STEPS,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// capture()
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
function fieldRef(stepIndex, key) {
|
|
149
|
+
return `steps[${stepIndex}].attributes[${JSON.stringify(key)}]`;
|
|
150
|
+
}
|
|
151
|
+
async function scrubStepObservations(steps, pipeline) {
|
|
152
|
+
const observations = [];
|
|
153
|
+
const redactions = [];
|
|
154
|
+
for (const [stepIndex, step] of steps.entries()) {
|
|
155
|
+
const events = [];
|
|
156
|
+
for (const [eventIndex, event] of (step.events ?? []).entries()) {
|
|
157
|
+
const originalAttributes = event.attributes ?? {};
|
|
158
|
+
const result = await pipeline.run(originalAttributes);
|
|
159
|
+
const fitted = fitAttributes(result.attributes);
|
|
160
|
+
for (const record of result.redactions) {
|
|
161
|
+
redactions.push({
|
|
162
|
+
field: `steps[${stepIndex}].events[${eventIndex}].attributes`
|
|
163
|
+
+ `[${JSON.stringify(record.key)}]`,
|
|
164
|
+
stage: record.stage,
|
|
165
|
+
...(record.detail ? { detail: record.detail } : {}),
|
|
166
|
+
before: originalAttributes[record.key],
|
|
167
|
+
after: fitted.attributes[record.key],
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
for (const cut of fitted.cuts) {
|
|
171
|
+
redactions.push({
|
|
172
|
+
field: `steps[${stepIndex}].events[${eventIndex}].attributes`
|
|
173
|
+
+ `[${JSON.stringify(cut.key)}]`,
|
|
174
|
+
stage: 'fit',
|
|
175
|
+
detail: `truncated to fit ${MAX_STEP_ATTRIBUTES_BYTES} bytes`,
|
|
176
|
+
before: cut.before,
|
|
177
|
+
after: cut.after,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
events.push({
|
|
181
|
+
timeUnixNano: event.timeUnixNano,
|
|
182
|
+
name: event.name,
|
|
183
|
+
...(event.attributes !== undefined
|
|
184
|
+
? { attributes: fitted.attributes }
|
|
185
|
+
: {}),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
let status = step.status;
|
|
189
|
+
if (status?.message !== undefined) {
|
|
190
|
+
const before = { 'status.message': status.message };
|
|
191
|
+
const result = await pipeline.run(before);
|
|
192
|
+
const fitted = fitAttributes(result.attributes);
|
|
193
|
+
const after = fitted.attributes['status.message'];
|
|
194
|
+
for (const record of result.redactions) {
|
|
195
|
+
redactions.push({
|
|
196
|
+
field: `steps[${stepIndex}].status.message`,
|
|
197
|
+
stage: record.stage,
|
|
198
|
+
...(record.detail ? { detail: record.detail } : {}),
|
|
199
|
+
before: status.message,
|
|
200
|
+
after,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
for (const cut of fitted.cuts) {
|
|
204
|
+
redactions.push({
|
|
205
|
+
field: `steps[${stepIndex}].status.message`,
|
|
206
|
+
stage: 'fit',
|
|
207
|
+
detail: `truncated to fit ${MAX_STEP_ATTRIBUTES_BYTES} bytes`,
|
|
208
|
+
before: cut.before,
|
|
209
|
+
after: cut.after,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
status = {
|
|
213
|
+
code: status.code,
|
|
214
|
+
...(typeof after === 'string' && after.length > 0
|
|
215
|
+
? { message: after }
|
|
216
|
+
: {}),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
observations.push({
|
|
220
|
+
...(step.events !== undefined ? { events } : {}),
|
|
221
|
+
...(status !== undefined ? { status } : {}),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return { observations, redactions };
|
|
225
|
+
}
|
|
226
|
+
function episodeFactBag(task) {
|
|
227
|
+
return {
|
|
228
|
+
...(task.session.kind !== undefined
|
|
229
|
+
? { 'episodeFacts.session.kind': task.session.kind }
|
|
230
|
+
: {}),
|
|
231
|
+
...(task.session.parentSessionId !== undefined
|
|
232
|
+
? { 'episodeFacts.session.parentSessionId': task.session.parentSessionId }
|
|
233
|
+
: {}),
|
|
234
|
+
...(task.task.repositorySlug !== undefined
|
|
235
|
+
? { 'episodeFacts.task.repositorySlug': task.task.repositorySlug }
|
|
236
|
+
: {}),
|
|
237
|
+
...(task.task.baseCommit !== undefined
|
|
238
|
+
? { 'episodeFacts.task.baseCommit': task.task.baseCommit }
|
|
239
|
+
: {}),
|
|
240
|
+
...(task.task.createdAt !== undefined
|
|
241
|
+
? { 'episodeFacts.task.createdAt': task.task.createdAt }
|
|
242
|
+
: {}),
|
|
243
|
+
...(task.task.instanceId !== undefined
|
|
244
|
+
? { 'episodeFacts.task.instanceId': task.task.instanceId }
|
|
245
|
+
: {}),
|
|
246
|
+
...(task.environment.skillsLoadout !== undefined
|
|
247
|
+
? { 'episodeFacts.environment.skillsLoadout': task.environment.skillsLoadout }
|
|
248
|
+
: {}),
|
|
249
|
+
...(task.environment.generatorModel !== undefined
|
|
250
|
+
? {
|
|
251
|
+
'episodeFacts.environment.generatorModel.id': task.environment.generatorModel.id,
|
|
252
|
+
...(task.environment.generatorModel.provider !== undefined
|
|
253
|
+
? {
|
|
254
|
+
'episodeFacts.environment.generatorModel.provider': task.environment.generatorModel.provider,
|
|
255
|
+
}
|
|
256
|
+
: {}),
|
|
257
|
+
...(task.environment.generatorModel.openWeights !== undefined
|
|
258
|
+
? {
|
|
259
|
+
'episodeFacts.environment.generatorModel.openWeights': task.environment.generatorModel.openWeights,
|
|
260
|
+
}
|
|
261
|
+
: {}),
|
|
262
|
+
'episodeFacts.environment.generatorModel.source': task.environment.generatorModel.source,
|
|
263
|
+
}
|
|
264
|
+
: {}),
|
|
265
|
+
...(task.environment.distributionClass !== undefined
|
|
266
|
+
? {
|
|
267
|
+
'episodeFacts.environment.distributionClass': task.environment.distributionClass,
|
|
268
|
+
}
|
|
269
|
+
: {}),
|
|
270
|
+
...(task.environment.verifier !== undefined
|
|
271
|
+
? {
|
|
272
|
+
'episodeFacts.environment.verifier.type': task.environment.verifier.type,
|
|
273
|
+
'episodeFacts.environment.verifier.failToPass': task.environment.verifier.failToPass,
|
|
274
|
+
'episodeFacts.environment.verifier.passToPass': task.environment.verifier.passToPass,
|
|
275
|
+
...(task.environment.verifier.evalSemanticsVersion !== undefined
|
|
276
|
+
? {
|
|
277
|
+
'episodeFacts.environment.verifier.evalSemanticsVersion': task.environment.verifier.evalSemanticsVersion,
|
|
278
|
+
}
|
|
279
|
+
: {}),
|
|
280
|
+
}
|
|
281
|
+
: {}),
|
|
282
|
+
'episodeFacts.trajectoryKinds': task.steps.map((step) => step.kind),
|
|
283
|
+
...(task.attemptGroup !== undefined
|
|
284
|
+
? {
|
|
285
|
+
'episodeFacts.attemptGroup.groupId': task.attemptGroup.groupId,
|
|
286
|
+
'episodeFacts.attemptGroup.attemptId': task.attemptGroup.attemptId,
|
|
287
|
+
'episodeFacts.attemptGroup.relatedAttemptRefs': task.attemptGroup.relatedAttemptRefs,
|
|
288
|
+
...(task.attemptGroup.groupSize !== undefined
|
|
289
|
+
? { 'episodeFacts.attemptGroup.groupSize': task.attemptGroup.groupSize }
|
|
290
|
+
: {}),
|
|
291
|
+
...(task.attemptGroup.nPass !== undefined
|
|
292
|
+
? { 'episodeFacts.attemptGroup.nPass': task.attemptGroup.nPass }
|
|
293
|
+
: {}),
|
|
294
|
+
...(task.attemptGroup.nFail !== undefined
|
|
295
|
+
? { 'episodeFacts.attemptGroup.nFail': task.attemptGroup.nFail }
|
|
296
|
+
: {}),
|
|
297
|
+
}
|
|
298
|
+
: {}),
|
|
299
|
+
...(task.lineage !== undefined
|
|
300
|
+
? {
|
|
301
|
+
'episodeFacts.lineage.episodeId': task.lineage.episodeId,
|
|
302
|
+
...(task.lineage.mintRef !== undefined
|
|
303
|
+
? { 'episodeFacts.lineage.mintRef': task.lineage.mintRef }
|
|
304
|
+
: {}),
|
|
305
|
+
}
|
|
306
|
+
: {}),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Rebuild a CapturedTask using only scrubbed Episode-only values. Parsing the
|
|
311
|
+
* result keeps this boundary fail-closed if a scrub stage changes a structural
|
|
312
|
+
* enum/type or removes a required member of a present nested block.
|
|
313
|
+
*/
|
|
314
|
+
function taskWithScrubbedEpisodeFacts(task, attributes) {
|
|
315
|
+
const fact = (key) => attributes[`episodeFacts.${key}`];
|
|
316
|
+
const kinds = fact('trajectoryKinds');
|
|
317
|
+
return parseCapturedTask({
|
|
318
|
+
...task,
|
|
319
|
+
session: {
|
|
320
|
+
sessionId: task.session.sessionId,
|
|
321
|
+
capturedAt: task.session.capturedAt,
|
|
322
|
+
...(fact('session.kind') !== undefined ? { kind: fact('session.kind') } : {}),
|
|
323
|
+
...(fact('session.parentSessionId') !== undefined
|
|
324
|
+
? { parentSessionId: fact('session.parentSessionId') }
|
|
325
|
+
: {}),
|
|
326
|
+
},
|
|
327
|
+
task: {
|
|
328
|
+
summary: task.task.summary,
|
|
329
|
+
distributionTags: task.task.distributionTags,
|
|
330
|
+
...(fact('task.repositorySlug') !== undefined
|
|
331
|
+
? { repositorySlug: fact('task.repositorySlug') }
|
|
332
|
+
: {}),
|
|
333
|
+
...(fact('task.baseCommit') !== undefined
|
|
334
|
+
? { baseCommit: fact('task.baseCommit') }
|
|
335
|
+
: {}),
|
|
336
|
+
...(fact('task.createdAt') !== undefined
|
|
337
|
+
? { createdAt: fact('task.createdAt') }
|
|
338
|
+
: {}),
|
|
339
|
+
...(fact('task.instanceId') !== undefined
|
|
340
|
+
? { instanceId: fact('task.instanceId') }
|
|
341
|
+
: {}),
|
|
342
|
+
},
|
|
343
|
+
environment: {
|
|
344
|
+
harness: task.environment.harness,
|
|
345
|
+
model: task.environment.model,
|
|
346
|
+
tools: task.environment.tools,
|
|
347
|
+
...(fact('environment.skillsLoadout') !== undefined
|
|
348
|
+
? { skillsLoadout: fact('environment.skillsLoadout') }
|
|
349
|
+
: {}),
|
|
350
|
+
...(task.environment.generatorModel !== undefined
|
|
351
|
+
? {
|
|
352
|
+
generatorModel: {
|
|
353
|
+
id: fact('environment.generatorModel.id'),
|
|
354
|
+
...(fact('environment.generatorModel.provider') !== undefined
|
|
355
|
+
? { provider: fact('environment.generatorModel.provider') }
|
|
356
|
+
: {}),
|
|
357
|
+
...(fact('environment.generatorModel.openWeights') !== undefined
|
|
358
|
+
? { openWeights: fact('environment.generatorModel.openWeights') }
|
|
359
|
+
: {}),
|
|
360
|
+
source: fact('environment.generatorModel.source'),
|
|
361
|
+
},
|
|
362
|
+
}
|
|
363
|
+
: {}),
|
|
364
|
+
...(fact('environment.distributionClass') !== undefined
|
|
365
|
+
? { distributionClass: fact('environment.distributionClass') }
|
|
366
|
+
: {}),
|
|
367
|
+
...(task.environment.verifier !== undefined
|
|
368
|
+
? {
|
|
369
|
+
verifier: {
|
|
370
|
+
type: fact('environment.verifier.type'),
|
|
371
|
+
failToPass: fact('environment.verifier.failToPass'),
|
|
372
|
+
passToPass: fact('environment.verifier.passToPass'),
|
|
373
|
+
...(fact('environment.verifier.evalSemanticsVersion') !== undefined
|
|
374
|
+
? {
|
|
375
|
+
evalSemanticsVersion: fact('environment.verifier.evalSemanticsVersion'),
|
|
376
|
+
}
|
|
377
|
+
: {}),
|
|
378
|
+
},
|
|
379
|
+
}
|
|
380
|
+
: {}),
|
|
381
|
+
},
|
|
382
|
+
steps: task.steps.map((step, index) => ({
|
|
383
|
+
...step,
|
|
384
|
+
...(Array.isArray(kinds) && kinds[index] !== undefined
|
|
385
|
+
? { kind: kinds[index] }
|
|
386
|
+
: { kind: undefined }),
|
|
387
|
+
})),
|
|
388
|
+
...(task.attemptGroup !== undefined
|
|
389
|
+
? {
|
|
390
|
+
attemptGroup: {
|
|
391
|
+
groupId: fact('attemptGroup.groupId'),
|
|
392
|
+
attemptId: fact('attemptGroup.attemptId'),
|
|
393
|
+
relatedAttemptRefs: fact('attemptGroup.relatedAttemptRefs'),
|
|
394
|
+
...(fact('attemptGroup.groupSize') !== undefined
|
|
395
|
+
? { groupSize: fact('attemptGroup.groupSize') }
|
|
396
|
+
: {}),
|
|
397
|
+
...(fact('attemptGroup.nPass') !== undefined
|
|
398
|
+
? { nPass: fact('attemptGroup.nPass') }
|
|
399
|
+
: {}),
|
|
400
|
+
...(fact('attemptGroup.nFail') !== undefined
|
|
401
|
+
? { nFail: fact('attemptGroup.nFail') }
|
|
402
|
+
: {}),
|
|
403
|
+
},
|
|
404
|
+
}
|
|
405
|
+
: {}),
|
|
406
|
+
...(task.lineage !== undefined
|
|
407
|
+
? {
|
|
408
|
+
lineage: {
|
|
409
|
+
episodeId: fact('lineage.episodeId'),
|
|
410
|
+
...(fact('lineage.mintRef') !== undefined
|
|
411
|
+
? { mintRef: fact('lineage.mintRef') }
|
|
412
|
+
: {}),
|
|
413
|
+
},
|
|
414
|
+
}
|
|
415
|
+
: {}),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Run the captured task through the scrub pipeline and the fitting rule and
|
|
420
|
+
* return the pending (pre-consent) envelope plus its redaction diff.
|
|
421
|
+
* Fail-closed: throws `CaptureScrubError` on any scrub-stage failure.
|
|
422
|
+
*/
|
|
423
|
+
export async function capture(task, opts = {}) {
|
|
424
|
+
const policy = opts.policy ?? DEFAULT_KEY_POLICY;
|
|
425
|
+
const base = opts.pipeline ?? buildScrubPipeline({
|
|
426
|
+
policy,
|
|
427
|
+
...(opts.piiDetector ? { piiDetector: opts.piiDetector } : {}),
|
|
428
|
+
});
|
|
429
|
+
// The email/home-path gap-closers graduated into the shared pipeline
|
|
430
|
+
// (plain-patterns stage, #1330) — the base pipeline alone now covers the
|
|
431
|
+
// seeded-secrets fixture. Injected test pipelines take the same path.
|
|
432
|
+
const recorder = new RecordingScrubPipeline([base]);
|
|
433
|
+
let scrubbedSteps;
|
|
434
|
+
let summaryRun;
|
|
435
|
+
let factRun;
|
|
436
|
+
let scrubbedFactTask;
|
|
437
|
+
let scrubbedObservations;
|
|
438
|
+
let observationRedactions;
|
|
439
|
+
const summaryBag = {
|
|
440
|
+
'task.summary': task.task.summary,
|
|
441
|
+
...(task.outcome.summary !== undefined ? { 'outcome.summary': task.outcome.summary } : {}),
|
|
442
|
+
};
|
|
443
|
+
const factBag = episodeFactBag(task);
|
|
444
|
+
try {
|
|
445
|
+
scrubbedSteps = await scrubCaptureSpans(task.steps, recorder);
|
|
446
|
+
const observationResult = await scrubStepObservations(task.steps, base);
|
|
447
|
+
scrubbedObservations = observationResult.observations;
|
|
448
|
+
observationRedactions = observationResult.redactions;
|
|
449
|
+
// The envelope calls `task.summary` a "scrubbed one-line descriptor" —
|
|
450
|
+
// capture is where that becomes true. Same pipeline, pseudo-attribute bag.
|
|
451
|
+
summaryRun = await recorder.run(summaryBag);
|
|
452
|
+
factRun = await recorder.run(factBag);
|
|
453
|
+
scrubbedFactTask = taskWithScrubbedEpisodeFacts(task, factRun.attributes);
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
throw new CaptureScrubError(err);
|
|
457
|
+
}
|
|
458
|
+
// Pair each scrubbed step with its pipeline run (one run per span, in
|
|
459
|
+
// order), then head/tail-sample. Redactions are reported only for retained
|
|
460
|
+
// steps: sampled-out steps never publish, so their diff is moot.
|
|
461
|
+
const paired = scrubbedSteps.map((step, i) => ({
|
|
462
|
+
step,
|
|
463
|
+
run: recorder.runs[i],
|
|
464
|
+
kind: scrubbedFactTask.steps[i].kind,
|
|
465
|
+
observation: scrubbedObservations[i],
|
|
466
|
+
}));
|
|
467
|
+
const { steps: retained, sampledOut } = sampleSteps(paired);
|
|
468
|
+
const redactions = [...observationRedactions];
|
|
469
|
+
if (sampledOut > 0) {
|
|
470
|
+
redactions.push({
|
|
471
|
+
field: 'steps',
|
|
472
|
+
stage: 'fit',
|
|
473
|
+
detail: `head/tail-sampled: ${sampledOut} middle step(s) removed to fit ${MAX_STEPS}`,
|
|
474
|
+
after: `${MAX_STEPS} steps`,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
const draftSteps = retained.map(({ step, run }, index) => {
|
|
478
|
+
const fitted = fitAttributes(run.result.attributes);
|
|
479
|
+
for (const record of run.result.redactions) {
|
|
480
|
+
redactions.push({
|
|
481
|
+
field: fieldRef(index, record.key),
|
|
482
|
+
stage: record.stage,
|
|
483
|
+
...(record.detail ? { detail: record.detail } : {}),
|
|
484
|
+
before: run.before[record.key],
|
|
485
|
+
after: fitted.attributes[record.key],
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
for (const cut of fitted.cuts) {
|
|
489
|
+
redactions.push({
|
|
490
|
+
field: fieldRef(index, cut.key),
|
|
491
|
+
stage: 'fit',
|
|
492
|
+
detail: `truncated to fit ${MAX_STEP_ATTRIBUTES_BYTES} bytes`,
|
|
493
|
+
before: cut.before,
|
|
494
|
+
after: cut.after,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
spanId: step.spanId,
|
|
499
|
+
parentSpanId: step.parentSpanId,
|
|
500
|
+
name: step.name,
|
|
501
|
+
startTimeUnixNano: step.startTimeUnixNano,
|
|
502
|
+
endTimeUnixNano: step.endTimeUnixNano,
|
|
503
|
+
attributes: fitted.attributes,
|
|
504
|
+
redactedKeys: step.redactedKeys,
|
|
505
|
+
...(fitted.truncatedKeys.length > 0 ? { truncatedKeys: fitted.truncatedKeys } : {}),
|
|
506
|
+
};
|
|
507
|
+
});
|
|
508
|
+
for (const record of summaryRun.redactions) {
|
|
509
|
+
redactions.push({
|
|
510
|
+
field: record.key,
|
|
511
|
+
stage: record.stage,
|
|
512
|
+
...(record.detail ? { detail: record.detail } : {}),
|
|
513
|
+
before: summaryBag[record.key],
|
|
514
|
+
after: summaryRun.attributes[record.key],
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
for (const record of factRun.redactions) {
|
|
518
|
+
redactions.push({
|
|
519
|
+
field: record.key,
|
|
520
|
+
stage: record.stage,
|
|
521
|
+
...(record.detail ? { detail: record.detail } : {}),
|
|
522
|
+
before: factBag[record.key],
|
|
523
|
+
after: factRun.attributes[record.key],
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
// Summaries are content keys the policy never drops; assert the invariant
|
|
527
|
+
// rather than publish an envelope with a silently-missing summary.
|
|
528
|
+
const scrubbedTaskSummary = summaryRun.attributes['task.summary'];
|
|
529
|
+
if (typeof scrubbedTaskSummary !== 'string') {
|
|
530
|
+
throw new CaptureScrubError(new Error('scrub removed task.summary entirely'));
|
|
531
|
+
}
|
|
532
|
+
const scrubbedOutcomeSummary = summaryRun.attributes['outcome.summary'];
|
|
533
|
+
if (task.outcome.summary !== undefined && typeof scrubbedOutcomeSummary !== 'string') {
|
|
534
|
+
throw new CaptureScrubError(new Error('scrub removed outcome.summary entirely'));
|
|
535
|
+
}
|
|
536
|
+
const draft = {
|
|
537
|
+
schemaVersion: TRACE_ENVELOPE_SCHEMA_VERSION,
|
|
538
|
+
session: {
|
|
539
|
+
sessionId: task.session.sessionId,
|
|
540
|
+
capturedAt: task.session.capturedAt,
|
|
541
|
+
},
|
|
542
|
+
task: {
|
|
543
|
+
summary: scrubbedTaskSummary.trim(),
|
|
544
|
+
distributionTags: task.task.distributionTags,
|
|
545
|
+
},
|
|
546
|
+
environment: {
|
|
547
|
+
harness: task.environment.harness,
|
|
548
|
+
model: task.environment.model,
|
|
549
|
+
tools: task.environment.tools,
|
|
550
|
+
},
|
|
551
|
+
steps: draftSteps,
|
|
552
|
+
outcome: {
|
|
553
|
+
...task.outcome,
|
|
554
|
+
...(typeof scrubbedOutcomeSummary === 'string' ? { summary: scrubbedOutcomeSummary.trim() } : {}),
|
|
555
|
+
},
|
|
556
|
+
cost: task.cost,
|
|
557
|
+
provenance: task.provenance,
|
|
558
|
+
};
|
|
559
|
+
// Fail-closed schema check: the would-be envelope must already be valid at
|
|
560
|
+
// capture time (fitting worked, caps hold, strings are inside their caps).
|
|
561
|
+
// Throwing here beats surfacing a broken envelope at preview/publish.
|
|
562
|
+
parseTraceEnvelopeV0({
|
|
563
|
+
...draft,
|
|
564
|
+
consent: { contributionConsent: true, scrubCompleted: true },
|
|
565
|
+
});
|
|
566
|
+
return {
|
|
567
|
+
kind: PENDING_ENVELOPE_KIND,
|
|
568
|
+
draft,
|
|
569
|
+
episodeFacts: {
|
|
570
|
+
session: {
|
|
571
|
+
...(scrubbedFactTask.session.kind
|
|
572
|
+
? { kind: scrubbedFactTask.session.kind }
|
|
573
|
+
: {}),
|
|
574
|
+
...(scrubbedFactTask.session.parentSessionId
|
|
575
|
+
? { parentSessionId: scrubbedFactTask.session.parentSessionId }
|
|
576
|
+
: {}),
|
|
577
|
+
},
|
|
578
|
+
task: {
|
|
579
|
+
...(scrubbedFactTask.task.repositorySlug
|
|
580
|
+
? { repositorySlug: scrubbedFactTask.task.repositorySlug }
|
|
581
|
+
: {}),
|
|
582
|
+
...(scrubbedFactTask.task.baseCommit
|
|
583
|
+
? { baseCommit: scrubbedFactTask.task.baseCommit }
|
|
584
|
+
: {}),
|
|
585
|
+
...(scrubbedFactTask.task.createdAt !== undefined
|
|
586
|
+
? { createdAt: scrubbedFactTask.task.createdAt }
|
|
587
|
+
: {}),
|
|
588
|
+
...(scrubbedFactTask.task.instanceId
|
|
589
|
+
? { instanceId: scrubbedFactTask.task.instanceId }
|
|
590
|
+
: {}),
|
|
591
|
+
},
|
|
592
|
+
environment: {
|
|
593
|
+
...(scrubbedFactTask.environment.skillsLoadout
|
|
594
|
+
? { skillsLoadout: scrubbedFactTask.environment.skillsLoadout }
|
|
595
|
+
: {}),
|
|
596
|
+
...(scrubbedFactTask.environment.generatorModel
|
|
597
|
+
? { generatorModel: scrubbedFactTask.environment.generatorModel }
|
|
598
|
+
: {}),
|
|
599
|
+
...(scrubbedFactTask.environment.distributionClass
|
|
600
|
+
? { distributionClass: scrubbedFactTask.environment.distributionClass }
|
|
601
|
+
: {}),
|
|
602
|
+
...(scrubbedFactTask.environment.verifier
|
|
603
|
+
? { verifier: scrubbedFactTask.environment.verifier }
|
|
604
|
+
: {}),
|
|
605
|
+
},
|
|
606
|
+
trajectoryKinds: retained.map(({ kind }) => kind),
|
|
607
|
+
trajectoryObservations: retained.map(({ observation }) => observation),
|
|
608
|
+
...(scrubbedFactTask.attemptGroup
|
|
609
|
+
? { attemptGroup: scrubbedFactTask.attemptGroup }
|
|
610
|
+
: {}),
|
|
611
|
+
...(scrubbedFactTask.lineage
|
|
612
|
+
? { lineage: scrubbedFactTask.lineage }
|
|
613
|
+
: {}),
|
|
614
|
+
},
|
|
615
|
+
redactions,
|
|
616
|
+
};
|
|
617
|
+
}
|