@evomap/evolver-core 2.0.0-beta.19 → 2.0.0-beta.22
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/assets/gep/genes.jsonl +5 -5
- package/dist/algo/bans.d.ts +10 -1
- package/dist/algo/bans.js +56 -6
- package/dist/algo/candidateAssembly.d.ts +25 -2
- package/dist/algo/candidateAssembly.js +143 -20
- package/dist/algo/capabilityCandidates.js +10 -0
- package/dist/algo/cycleEngine.d.ts +40 -5
- package/dist/algo/cycleEngine.js +277 -37
- package/dist/algo/evolutionEvent.d.ts +26 -0
- package/dist/algo/evolutionEvent.js +32 -0
- package/dist/algo/exploration.d.ts +7 -0
- package/dist/algo/exploration.js +16 -3
- package/dist/algo/geneHealth.d.ts +34 -1
- package/dist/algo/geneHealth.js +42 -0
- package/dist/algo/geneIntake.d.ts +46 -1
- package/dist/algo/geneIntake.js +121 -10
- package/dist/algo/geneSelection.d.ts +115 -6
- package/dist/algo/geneSelection.js +459 -32
- package/dist/algo/index.d.ts +4 -1
- package/dist/algo/index.js +4 -1
- package/dist/algo/kautoProjection.d.ts +41 -0
- package/dist/algo/kautoProjection.js +95 -0
- package/dist/algo/kautoValidator.d.ts +68 -0
- package/dist/algo/kautoValidator.js +256 -0
- package/dist/algo/orchestrator.d.ts +14 -1
- package/dist/algo/orchestrator.js +9 -2
- package/dist/algo/solidify.d.ts +11 -2
- package/dist/algo/solidify.js +37 -7
- package/dist/algo/ucb1.d.ts +53 -0
- package/dist/algo/ucb1.js +156 -0
- package/dist/assetrepair/hubRejection.d.ts +12 -0
- package/dist/assetrepair/hubRejection.js +109 -0
- package/dist/assetrepair/index.d.ts +2 -0
- package/dist/assetrepair/index.js +2 -0
- package/dist/assetrepair/repair.d.ts +33 -0
- package/dist/assetrepair/repair.js +155 -0
- package/dist/assetstore/assetSidecarRecords.js +5 -1
- package/dist/assetstore/assetStoreHealth.d.ts +2 -3
- package/dist/assetstore/assetStoreHealth.js +4 -10
- package/dist/assetstore/foreignJsonlSource.d.ts +48 -0
- package/dist/assetstore/foreignJsonlSource.js +150 -0
- package/dist/assetstore/index.d.ts +3 -0
- package/dist/assetstore/index.js +3 -0
- package/dist/assetstore/learningHistory.js +3 -3
- package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
- package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
- package/dist/assetstore/localJsonl.d.ts +8 -0
- package/dist/assetstore/localJsonl.js +128 -3
- package/dist/assetstore/provenance.d.ts +38 -3
- package/dist/assetstore/provenance.js +103 -45
- package/dist/assetstore/provider.d.ts +4 -0
- package/dist/assetstore/reviewFilter.d.ts +19 -1
- package/dist/assetstore/reviewFilter.js +36 -0
- package/dist/assetstore/unionReadStore.d.ts +25 -0
- package/dist/assetstore/unionReadStore.js +119 -0
- package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
- package/dist/benchmark/antiGeneBenchmark.js +4 -3
- package/dist/benchmark/antiGeneRollout.d.ts +2 -0
- package/dist/benchmark/antiGeneRollout.js +4 -3
- package/dist/benchmark/index.d.ts +2 -1
- package/dist/benchmark/index.js +2 -1
- package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
- package/dist/benchmark/selectionFlatAbstention.js +481 -0
- package/dist/bootstrap/envFingerprint.d.ts +9 -0
- package/dist/bootstrap/envFingerprint.js +5 -0
- package/dist/bootstrap/index.d.ts +2 -1
- package/dist/bootstrap/index.js +2 -1
- package/dist/bootstrap/lifecycleBootstrap.d.ts +111 -0
- package/dist/bootstrap/lifecycleBootstrap.js +433 -0
- package/dist/bootstrap/v1EnvCompat.d.ts +3 -0
- package/dist/bootstrap/v1EnvCompat.js +48 -1
- package/dist/events/eventArchive.d.ts +2 -0
- package/dist/events/eventArchive.js +13 -3
- package/dist/events/eventSchema.d.ts +7 -7
- package/dist/events/eventStore.d.ts +2 -0
- package/dist/events/eventStore.js +5 -1
- package/dist/events/ingest.d.ts +2 -1
- package/dist/events/ingest.js +6 -0
- package/dist/exec/autoExec.d.ts +30 -3
- package/dist/exec/autoExec.js +294 -29
- package/dist/exec/autonomousCycle.d.ts +7 -0
- package/dist/exec/autonomousCycle.js +2 -0
- package/dist/exec/claudeBridge.d.ts +28 -6
- package/dist/exec/claudeBridge.js +411 -258
- package/dist/exec/executionBinding.d.ts +414 -0
- package/dist/exec/executionBinding.js +588 -0
- package/dist/exec/index.d.ts +1 -0
- package/dist/exec/index.js +1 -0
- package/dist/exec/proofOfWork.d.ts +1 -1
- package/dist/exec/proofOfWork.js +2 -2
- package/dist/exec/runnerRegistry.d.ts +5 -0
- package/dist/exec/runnerRegistry.js +54 -14
- package/dist/hub/capability.d.ts +40 -1
- package/dist/hub/conversationDistiller.d.ts +19 -0
- package/dist/hub/conversationDistiller.js +115 -37
- package/dist/hub/fake.d.ts +1 -0
- package/dist/hub/fake.js +1 -0
- package/dist/hub/index.d.ts +1 -0
- package/dist/hub/index.js +1 -0
- package/dist/hub/recipeCompose.d.ts +27 -0
- package/dist/hub/recipeCompose.js +90 -0
- package/dist/hub/sanitize.js +119 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mailbox/ipcServer.d.ts +2 -0
- package/dist/mailbox/ipcServer.js +20 -0
- package/dist/mailbox/store.d.ts +52 -2
- package/dist/mailbox/store.js +546 -81
- package/dist/modelCompatibility.d.ts +164 -0
- package/dist/modelCompatibility.js +309 -0
- package/dist/observers/valueDigestObserver.d.ts +9 -0
- package/dist/observers/valueDigestObserver.js +35 -2
- package/dist/ops/cleanup.js +1 -1
- package/dist/ops/evolutionGraphProjection.d.ts +20 -0
- package/dist/ops/evolutionGraphProjection.js +315 -0
- package/dist/ops/index.d.ts +2 -1
- package/dist/ops/index.js +2 -1
- package/dist/ops/selfUpdate.d.ts +8 -0
- package/dist/ops/selfUpdate.js +24 -8
- package/dist/ops/valueOutreach.d.ts +3 -1
- package/dist/ops/valueOutreach.js +5 -1
- package/dist/personality/schema.d.ts +18 -18
- package/dist/schema/evolutionGraph.d.ts +784 -0
- package/dist/schema/evolutionGraph.js +187 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +1 -0
- package/dist/schema/proofOfWork.d.ts +125 -6
- package/dist/schema/proofOfWork.js +102 -4
- package/dist/shadow/shadowHub.js +1 -0
- package/dist/signals/expand.d.ts +15 -1
- package/dist/signals/expand.js +148 -0
- package/dist/signals/index.d.ts +3 -1
- package/dist/signals/index.js +3 -1
- package/dist/signals/scopeVocabulary.d.ts +75 -0
- package/dist/signals/scopeVocabulary.js +91 -0
- package/dist/signals/taskDomain.d.ts +22 -0
- package/dist/signals/taskDomain.js +43 -0
- package/dist/trace/learningTrace.d.ts +7 -0
- package/dist/trace/learningTrace.js +22 -0
- package/dist/trace/trajectory.d.ts +8 -0
- package/dist/trace/trajectory.js +14 -2
- package/dist/util/fileLock.d.ts +92 -5
- package/dist/util/fileLock.js +809 -70
- package/dist/util/index.d.ts +2 -1
- package/dist/util/index.js +1 -1
- package/dist/verify/index.d.ts +2 -1
- package/dist/verify/index.js +1 -1
- package/dist/verify/sandboxRunner.d.ts +2 -0
- package/dist/verify/sandboxRunner.js +154 -10
- package/dist/verify/sandboxedValidation.d.ts +7 -2
- package/dist/verify/sandboxedValidation.js +102 -24
- package/dist/verify/validation.d.ts +24 -3
- package/dist/verify/validation.js +141 -24
- package/dist/wire/geneHints.d.ts +88 -16
- package/dist/wire/geneHints.js +124 -15
- package/dist/wire/index.d.ts +8 -3
- package/dist/wire/index.js +2 -2
- package/dist/wire/schemaGate.d.ts +21 -0
- package/dist/wire/schemaGate.js +119 -10
- package/package.json +5 -4
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { canonicalize } from '../wire/index.js';
|
|
6
|
+
import { MAX_LINE_BYTES } from '../events/eventStore.js';
|
|
7
|
+
import { acquireLock, releaseLock } from '../util/fileLock.js';
|
|
8
|
+
import { isValidationCommandAllowed } from '../verify/validation.js';
|
|
9
|
+
import { proofOfWork } from '../schema/proofOfWork.js';
|
|
10
|
+
const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
11
|
+
const digest = z.string().regex(DIGEST_PATTERN, 'must be sha256:<64 lowercase hex>');
|
|
12
|
+
const MAX_INLINE_BINDING_BYTES = MAX_LINE_BYTES - 512;
|
|
13
|
+
const isoDate = z.string().datetime({ offset: true });
|
|
14
|
+
const boundedId = z.string().trim().min(1).max(512);
|
|
15
|
+
const boundedText = z.string().max(4_000);
|
|
16
|
+
const boundedStringArray = z.array(z.string().max(4_000)).max(128);
|
|
17
|
+
const targetDescriptorSchema = z.object({
|
|
18
|
+
repo_id: boundedId,
|
|
19
|
+
selector: boundedId,
|
|
20
|
+
expected_effect: boundedText,
|
|
21
|
+
base_revision: boundedId.nullable(),
|
|
22
|
+
}).strict();
|
|
23
|
+
const acceptanceSpecSchema = z.object({
|
|
24
|
+
spec_id: boundedId,
|
|
25
|
+
version: boundedId,
|
|
26
|
+
digest,
|
|
27
|
+
validation_plan_digest: digest,
|
|
28
|
+
}).strict();
|
|
29
|
+
const resourceGrantSchema = z.object({
|
|
30
|
+
allowed_roots: z.array(boundedId).min(1).max(32),
|
|
31
|
+
max_runtime_ms: z.number().int().positive().safe(),
|
|
32
|
+
max_files: z.number().int().nonnegative().safe(),
|
|
33
|
+
max_lines: z.number().int().nonnegative().safe(),
|
|
34
|
+
validation_commands: z.array(z.string().trim().max(400)).max(64),
|
|
35
|
+
}).strict();
|
|
36
|
+
const consentGrantSchema = z.object({
|
|
37
|
+
grant_id: boundedId,
|
|
38
|
+
subject: boundedId,
|
|
39
|
+
scopes: z.array(boundedId).min(1).max(64),
|
|
40
|
+
issued_at: isoDate,
|
|
41
|
+
expires_at: isoDate,
|
|
42
|
+
target_digest: digest,
|
|
43
|
+
}).strict();
|
|
44
|
+
const selectedContextSchema = z.object({
|
|
45
|
+
gene_id: boundedId.nullable(),
|
|
46
|
+
gene_asset_id: digest.nullable(),
|
|
47
|
+
capsule_id: boundedId.nullable(),
|
|
48
|
+
capsule_asset_id: digest.nullable(),
|
|
49
|
+
context_digest: digest,
|
|
50
|
+
}).strict();
|
|
51
|
+
export const executionBindingInputSchema = z.object({
|
|
52
|
+
schema_version: z.literal('execution-binding.v1'),
|
|
53
|
+
bounty_id: boundedId.nullable(),
|
|
54
|
+
task_id: boundedId,
|
|
55
|
+
claim_identity: z.object({ claim_id: boundedId, claimant_id: boundedId }).strict(),
|
|
56
|
+
lease_identity: z.object({ lease_id: boundedId, lease_expires_at: isoDate }).strict(),
|
|
57
|
+
target_descriptor: targetDescriptorSchema,
|
|
58
|
+
target_digest: digest,
|
|
59
|
+
acceptance_spec: acceptanceSpecSchema,
|
|
60
|
+
deadline: isoDate,
|
|
61
|
+
budget: z.object({ max_credits: z.number().int().nonnegative().safe() }).strict(),
|
|
62
|
+
resource_grant: resourceGrantSchema,
|
|
63
|
+
consent_grant: consentGrantSchema,
|
|
64
|
+
correlation: z.object({ run_id: boundedId, cycle_id: boundedId }).strict(),
|
|
65
|
+
selected_context: selectedContextSchema,
|
|
66
|
+
}).strict();
|
|
67
|
+
export class ExecutionBindingError extends Error {
|
|
68
|
+
code;
|
|
69
|
+
detail;
|
|
70
|
+
unsafeToReplay;
|
|
71
|
+
constructor(code, detail, unsafeToReplay = false) {
|
|
72
|
+
super(`${code}: ${detail}`);
|
|
73
|
+
this.code = code;
|
|
74
|
+
this.detail = detail;
|
|
75
|
+
this.unsafeToReplay = unsafeToReplay;
|
|
76
|
+
this.name = 'ExecutionBindingError';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const terminalDispositionSchema = z.enum([
|
|
80
|
+
'completed', 'denied', 'invalid_target', 'deadline_exceeded', 'budget_exceeded',
|
|
81
|
+
'resource_denied', 'consent_denied', 'timed_out', 'cancelled', 'crashed', 'rejected', 'unsafe_to_replay',
|
|
82
|
+
]);
|
|
83
|
+
const toolDecisionSchema = z.object({
|
|
84
|
+
tool_name: boundedId,
|
|
85
|
+
decision: z.enum(['allowed', 'denied']),
|
|
86
|
+
status: z.enum(['not_run', 'started', 'completed', 'failed']).optional(),
|
|
87
|
+
call_id: boundedId.optional(),
|
|
88
|
+
duration_ms: z.number().int().nonnegative().safe().optional(),
|
|
89
|
+
}).strict();
|
|
90
|
+
const policyDecisionSchema = z.object({
|
|
91
|
+
version: boundedId,
|
|
92
|
+
allowed: z.boolean(),
|
|
93
|
+
violations: z.array(z.object({
|
|
94
|
+
kind: z.enum(['blast_radius', 'forbidden_path', 'protected_path', 'destructive']),
|
|
95
|
+
detail: boundedText,
|
|
96
|
+
}).strict()).max(128),
|
|
97
|
+
}).strict();
|
|
98
|
+
const validatorEvidenceSchema = z.object({
|
|
99
|
+
id: boundedId,
|
|
100
|
+
version: boundedId,
|
|
101
|
+
status: z.enum(['ran', 'not_run']),
|
|
102
|
+
reason: z.enum(['policy_denied', 'no_commands', 'malformed_plan', 'sandbox_unavailable', 'not_configured']).optional(),
|
|
103
|
+
plan_digest: digest.optional(),
|
|
104
|
+
passed: z.boolean().optional(),
|
|
105
|
+
score: z.number().finite().optional(),
|
|
106
|
+
results: z.array(z.object({
|
|
107
|
+
label: boundedText,
|
|
108
|
+
cmd: z.string().max(400),
|
|
109
|
+
allowed: z.boolean(),
|
|
110
|
+
exitCode: z.number().int().nullable(),
|
|
111
|
+
stdoutSummary: z.string().max(600),
|
|
112
|
+
passed: z.boolean(),
|
|
113
|
+
}).strict()).max(64),
|
|
114
|
+
skipped: z.array(z.object({ cmd: z.string().max(400), script: z.string().max(400), reason: z.literal('missing_script') }).strict()).max(64),
|
|
115
|
+
isolated: z.boolean(),
|
|
116
|
+
}).strict();
|
|
117
|
+
const provenanceSchema = z.object({
|
|
118
|
+
gene_ids: boundedStringArray,
|
|
119
|
+
capsule_ids: boundedStringArray,
|
|
120
|
+
tool_decisions: z.array(toolDecisionSchema).max(128),
|
|
121
|
+
policy_decisions: z.array(policyDecisionSchema).max(32),
|
|
122
|
+
validator: validatorEvidenceSchema.nullable(),
|
|
123
|
+
permit: z.object({ ok: z.boolean(), reason: boundedText.optional(), detail: boundedText.optional() }).strict().optional(),
|
|
124
|
+
result_asset_refs: boundedStringArray,
|
|
125
|
+
proof_refs: z.array(z.object({ kind: z.enum(['git_diff', 'artifact_hash', 'external_receipt', 'tool_call_trace']), ref: boundedId.optional(), asset_id: boundedId.optional() }).strict()).max(64).superRefine((refs, ctx) => {
|
|
126
|
+
refs.forEach((reference, index) => {
|
|
127
|
+
if (!reference.ref && !reference.asset_id)
|
|
128
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: [index], message: 'proof reference requires ref or asset_id' });
|
|
129
|
+
});
|
|
130
|
+
}),
|
|
131
|
+
terminal_disposition: terminalDispositionSchema,
|
|
132
|
+
}).strict();
|
|
133
|
+
const terminalRecordSchema = z.object({
|
|
134
|
+
binding_digest: digest,
|
|
135
|
+
task_id: boundedId,
|
|
136
|
+
run_id: boundedId,
|
|
137
|
+
disposition: terminalDispositionSchema,
|
|
138
|
+
final_stage: boundedId.optional(),
|
|
139
|
+
outcome: z.object({ status: boundedId, score: z.number().finite(), reason: boundedText.optional() }).strict().optional(),
|
|
140
|
+
proof_of_work: proofOfWork.optional(),
|
|
141
|
+
provenance: provenanceSchema,
|
|
142
|
+
}).strict();
|
|
143
|
+
function sha256(value) {
|
|
144
|
+
return `sha256:${createHash('sha256').update(value, 'utf8').digest('hex')}`;
|
|
145
|
+
}
|
|
146
|
+
function withoutDigest(value, key) {
|
|
147
|
+
const copy = { ...value };
|
|
148
|
+
delete copy[key];
|
|
149
|
+
return copy;
|
|
150
|
+
}
|
|
151
|
+
function freezeDeep(value) {
|
|
152
|
+
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
153
|
+
Object.freeze(value);
|
|
154
|
+
for (const child of Object.values(value))
|
|
155
|
+
freezeDeep(child);
|
|
156
|
+
}
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
function parseOrThrow(input) {
|
|
160
|
+
const result = executionBindingInputSchema.safeParse(input);
|
|
161
|
+
if (!result.success)
|
|
162
|
+
throw new ExecutionBindingError('binding_malformed', result.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join('; '));
|
|
163
|
+
return result.data;
|
|
164
|
+
}
|
|
165
|
+
function parseDate(value, field) {
|
|
166
|
+
const parsed = Date.parse(value);
|
|
167
|
+
if (!Number.isFinite(parsed))
|
|
168
|
+
throw new ExecutionBindingError('binding_malformed', `${field} is not a valid date`);
|
|
169
|
+
return parsed;
|
|
170
|
+
}
|
|
171
|
+
function canonicalPath(path) {
|
|
172
|
+
try {
|
|
173
|
+
return realpathSync(resolve(path));
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function pathWithin(root, target) {
|
|
180
|
+
const canonicalRoot = canonicalPath(root);
|
|
181
|
+
const canonicalTarget = canonicalPath(target);
|
|
182
|
+
if (!canonicalRoot || !canonicalTarget)
|
|
183
|
+
return false;
|
|
184
|
+
const rel = relative(canonicalRoot, canonicalTarget);
|
|
185
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${requirePathSeparator()}`) && !isAbsolute(rel));
|
|
186
|
+
}
|
|
187
|
+
function canonicalOverlap(left, right) {
|
|
188
|
+
const leftPath = canonicalPath(left);
|
|
189
|
+
const rightPath = canonicalPath(right);
|
|
190
|
+
if (!leftPath || !rightPath)
|
|
191
|
+
return undefined;
|
|
192
|
+
if (pathWithin(leftPath, rightPath))
|
|
193
|
+
return rightPath;
|
|
194
|
+
if (pathWithin(rightPath, leftPath))
|
|
195
|
+
return leftPath;
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
function requirePathSeparator() {
|
|
199
|
+
return process.platform === 'win32' ? '\\' : '/';
|
|
200
|
+
}
|
|
201
|
+
export function canonicalExecutionBinding(input) {
|
|
202
|
+
const withoutBindingDigest = withoutDigest(input, 'binding_digest');
|
|
203
|
+
return canonicalize(parseOrThrow(withoutBindingDigest));
|
|
204
|
+
}
|
|
205
|
+
export function computeTargetDigest(target) {
|
|
206
|
+
const parsed = targetDescriptorSchema.safeParse(target);
|
|
207
|
+
if (!parsed.success)
|
|
208
|
+
throw new ExecutionBindingError('binding_target_invalid', parsed.error.message);
|
|
209
|
+
return sha256(canonicalize(parsed.data));
|
|
210
|
+
}
|
|
211
|
+
export function computeValidationPlanDigest(commands) {
|
|
212
|
+
if (!Array.isArray(commands) || commands.some((command) => typeof command !== 'string')) {
|
|
213
|
+
throw new ExecutionBindingError('binding_acceptance_invalid', 'validation plan must contain strings');
|
|
214
|
+
}
|
|
215
|
+
return sha256(canonicalize({ commands: [...commands] }));
|
|
216
|
+
}
|
|
217
|
+
export function computeSelectedContextDigest(context) {
|
|
218
|
+
return sha256(canonicalize(context));
|
|
219
|
+
}
|
|
220
|
+
export function computeExecutionBindingDigest(input) {
|
|
221
|
+
return sha256(canonicalExecutionBinding(input));
|
|
222
|
+
}
|
|
223
|
+
export function freezeExecutionBinding(input) {
|
|
224
|
+
const parsed = parseOrThrow(input);
|
|
225
|
+
const targetDigest = computeTargetDigest(parsed.target_descriptor);
|
|
226
|
+
if (parsed.target_digest !== targetDigest) {
|
|
227
|
+
throw new ExecutionBindingError('binding_target_invalid', 'target_digest does not match target_descriptor');
|
|
228
|
+
}
|
|
229
|
+
const validationDigest = computeValidationPlanDigest(parsed.resource_grant.validation_commands);
|
|
230
|
+
if (parsed.acceptance_spec.validation_plan_digest !== validationDigest) {
|
|
231
|
+
throw new ExecutionBindingError('binding_acceptance_invalid', 'validation_plan_digest does not match validation_commands');
|
|
232
|
+
}
|
|
233
|
+
if (parsed.resource_grant.validation_commands.some((command) => !isValidationCommandAllowed(command))) {
|
|
234
|
+
throw new ExecutionBindingError('binding_acceptance_invalid', 'validation command is not allowed by the sandbox contract');
|
|
235
|
+
}
|
|
236
|
+
const contextDigest = computeSelectedContextDigest({
|
|
237
|
+
gene_id: parsed.selected_context.gene_id,
|
|
238
|
+
gene_asset_id: parsed.selected_context.gene_asset_id,
|
|
239
|
+
capsule_id: parsed.selected_context.capsule_id,
|
|
240
|
+
capsule_asset_id: parsed.selected_context.capsule_asset_id,
|
|
241
|
+
});
|
|
242
|
+
if (parsed.selected_context.context_digest !== contextDigest) {
|
|
243
|
+
throw new ExecutionBindingError('binding_mismatched', 'context_digest does not match selected_context');
|
|
244
|
+
}
|
|
245
|
+
if (parsed.consent_grant.target_digest !== parsed.target_digest) {
|
|
246
|
+
throw new ExecutionBindingError('binding_consent_denied', 'consent target_digest does not match target_digest');
|
|
247
|
+
}
|
|
248
|
+
if (parsed.consent_grant.subject !== parsed.claim_identity.claimant_id) {
|
|
249
|
+
throw new ExecutionBindingError('binding_consent_denied', 'consent subject does not match claimant identity');
|
|
250
|
+
}
|
|
251
|
+
const canonical = canonicalize(parsed);
|
|
252
|
+
if (Buffer.byteLength(canonical, 'utf8') > MAX_INLINE_BINDING_BYTES) {
|
|
253
|
+
throw new ExecutionBindingError('binding_malformed', 'canonical binding exceeds durable root-event capacity');
|
|
254
|
+
}
|
|
255
|
+
const binding_digest = sha256(canonical);
|
|
256
|
+
return freezeDeep({ ...parsed, binding_digest });
|
|
257
|
+
}
|
|
258
|
+
export async function preflightExecutionBinding(input, context) {
|
|
259
|
+
if (!context || typeof context.taskId !== 'string' || context.taskId.length === 0) {
|
|
260
|
+
throw new ExecutionBindingError('binding_missing', 'taskId is required');
|
|
261
|
+
}
|
|
262
|
+
const suppliedDigest = input && typeof input === 'object' && !Array.isArray(input)
|
|
263
|
+
? input['binding_digest']
|
|
264
|
+
: undefined;
|
|
265
|
+
const externalInput = suppliedDigest === undefined
|
|
266
|
+
? input
|
|
267
|
+
: withoutDigest(input, 'binding_digest');
|
|
268
|
+
const binding = freezeExecutionBinding(externalInput);
|
|
269
|
+
if (suppliedDigest !== undefined && suppliedDigest !== binding.binding_digest) {
|
|
270
|
+
throw new ExecutionBindingError('binding_mismatched', 'binding_digest does not match canonical binding');
|
|
271
|
+
}
|
|
272
|
+
const now = typeof context.now === 'function' ? context.now() : context.now ?? Date.now();
|
|
273
|
+
if (!Number.isFinite(now))
|
|
274
|
+
throw new ExecutionBindingError('binding_malformed', 'execution clock must be finite');
|
|
275
|
+
for (const [field, value] of [['maxRuntimeMs', context.maxRuntimeMs], ['maxFiles', context.maxFiles], ['maxLines', context.maxLines]]) {
|
|
276
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0))
|
|
277
|
+
throw new ExecutionBindingError('binding_malformed', `${field} must be a nonnegative safe integer`);
|
|
278
|
+
}
|
|
279
|
+
const deadline = parseDate(binding.deadline, 'deadline');
|
|
280
|
+
const leaseExpiry = parseDate(binding.lease_identity.lease_expires_at, 'lease_expires_at');
|
|
281
|
+
const consentIssued = parseDate(binding.consent_grant.issued_at, 'consent issued_at');
|
|
282
|
+
const consentExpiry = parseDate(binding.consent_grant.expires_at, 'consent expires_at');
|
|
283
|
+
if (binding.task_id !== context.taskId)
|
|
284
|
+
throw new ExecutionBindingError('binding_mismatched', 'task_id does not match execution task');
|
|
285
|
+
if (context.runId !== undefined && binding.correlation.run_id !== context.runId)
|
|
286
|
+
throw new ExecutionBindingError('binding_mismatched', 'run_id does not match execution context');
|
|
287
|
+
if (context.cycleId !== undefined && binding.correlation.cycle_id !== context.cycleId)
|
|
288
|
+
throw new ExecutionBindingError('binding_mismatched', 'cycle_id does not match execution context');
|
|
289
|
+
if (deadline <= now)
|
|
290
|
+
throw new ExecutionBindingError('binding_deadline_exceeded', 'execution deadline has expired');
|
|
291
|
+
if (leaseExpiry <= now)
|
|
292
|
+
throw new ExecutionBindingError('binding_stale', 'execution lease has expired');
|
|
293
|
+
if (consentExpiry <= now)
|
|
294
|
+
throw new ExecutionBindingError('binding_consent_denied', 'consent grant has expired');
|
|
295
|
+
if (consentIssued > now)
|
|
296
|
+
throw new ExecutionBindingError('binding_consent_denied', 'consent is not yet valid');
|
|
297
|
+
const remaining = Math.min(deadline, leaseExpiry, consentExpiry) - now;
|
|
298
|
+
if (binding.resource_grant.max_runtime_ms > remaining)
|
|
299
|
+
throw new ExecutionBindingError('binding_deadline_exceeded', 'max_runtime_ms exceeds remaining binding lifetime');
|
|
300
|
+
let effectiveRoots = [];
|
|
301
|
+
if (context.allowedRoots === undefined || context.allowedRoots.length === 0) {
|
|
302
|
+
throw new ExecutionBindingError('binding_resource_denied', 'local AutonomousSafety allowed roots are required');
|
|
303
|
+
}
|
|
304
|
+
if (context.repoPath !== undefined) {
|
|
305
|
+
const bindingRepo = canonicalPath(binding.target_descriptor.repo_id);
|
|
306
|
+
const executionRepo = canonicalPath(context.repoPath);
|
|
307
|
+
if (!bindingRepo || !executionRepo || bindingRepo !== executionRepo)
|
|
308
|
+
throw new ExecutionBindingError('binding_target_invalid', 'repo_id does not match execution repo');
|
|
309
|
+
const grantRoots = binding.resource_grant.allowed_roots.filter((root) => pathWithin(root, executionRepo));
|
|
310
|
+
const localRoots = context.allowedRoots.filter((root) => pathWithin(root, executionRepo));
|
|
311
|
+
if (grantRoots.length === 0)
|
|
312
|
+
throw new ExecutionBindingError('binding_resource_denied', 'execution repo is outside resource grant roots');
|
|
313
|
+
if (localRoots.length === 0)
|
|
314
|
+
throw new ExecutionBindingError('binding_resource_denied', 'execution repo is outside AutonomousSafety allowed roots');
|
|
315
|
+
effectiveRoots = [executionRepo];
|
|
316
|
+
if (context.target !== undefined && binding.target_descriptor.selector !== context.target)
|
|
317
|
+
throw new ExecutionBindingError('binding_target_invalid', 'selector does not match execution target');
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
effectiveRoots = [...new Set(binding.resource_grant.allowed_roots.flatMap((grantRoot) => (context.allowedRoots.map((localRoot) => canonicalOverlap(grantRoot, localRoot)).filter((root) => root !== undefined))))];
|
|
321
|
+
if (effectiveRoots.length === 0)
|
|
322
|
+
throw new ExecutionBindingError('binding_resource_denied', 'resource grant has no canonical overlap with allowed roots');
|
|
323
|
+
}
|
|
324
|
+
if (context.expectedEffect !== undefined && binding.target_descriptor.expected_effect !== context.expectedEffect)
|
|
325
|
+
throw new ExecutionBindingError('binding_target_invalid', 'expected_effect does not match execution task');
|
|
326
|
+
if (context.baseRevision !== undefined && binding.target_descriptor.base_revision !== context.baseRevision)
|
|
327
|
+
throw new ExecutionBindingError('binding_target_invalid', 'base_revision does not match repository HEAD');
|
|
328
|
+
if (context.currentRevision !== undefined) {
|
|
329
|
+
const currentRevision = typeof context.currentRevision === 'string' || context.currentRevision === null
|
|
330
|
+
? context.currentRevision
|
|
331
|
+
: await context.currentRevision;
|
|
332
|
+
if (binding.target_descriptor.base_revision !== currentRevision)
|
|
333
|
+
throw new ExecutionBindingError('binding_target_invalid', 'base_revision does not match current repository revision');
|
|
334
|
+
}
|
|
335
|
+
if (context.maxRuntimeMs !== undefined && binding.resource_grant.max_runtime_ms > context.maxRuntimeMs)
|
|
336
|
+
throw new ExecutionBindingError('binding_resource_denied', 'runtime grant exceeds local safety limit');
|
|
337
|
+
if (context.maxFiles !== undefined && binding.resource_grant.max_files > context.maxFiles)
|
|
338
|
+
throw new ExecutionBindingError('binding_resource_denied', 'file grant exceeds local safety limit');
|
|
339
|
+
if (context.maxLines !== undefined && binding.resource_grant.max_lines > context.maxLines)
|
|
340
|
+
throw new ExecutionBindingError('binding_resource_denied', 'line grant exceeds local safety limit');
|
|
341
|
+
if (context.requiredConsentScope !== undefined && !binding.consent_grant.scopes.includes(context.requiredConsentScope))
|
|
342
|
+
throw new ExecutionBindingError('binding_consent_denied', 'required consent scope is absent');
|
|
343
|
+
if (binding.resource_grant.validation_commands.some((command) => !isValidationCommandAllowed(command)))
|
|
344
|
+
throw new ExecutionBindingError('binding_acceptance_invalid', 'validation command is not allowed by the sandbox contract');
|
|
345
|
+
const authority = context.authoritative;
|
|
346
|
+
if (!authority)
|
|
347
|
+
throw new ExecutionBindingError('binding_stale', 'authoritative claim, acceptance, budget, and consent checks are required');
|
|
348
|
+
if (!(await authority.claimLease(binding)))
|
|
349
|
+
throw new ExecutionBindingError('binding_stale', 'claim or lease is no longer authoritative');
|
|
350
|
+
if (!(await authority.acceptanceSpec(binding)))
|
|
351
|
+
throw new ExecutionBindingError('binding_acceptance_invalid', 'acceptance specification is not authoritative');
|
|
352
|
+
if (!(await authority.budget(binding)))
|
|
353
|
+
throw new ExecutionBindingError('binding_budget_exceeded', 'budget grant is not authoritative');
|
|
354
|
+
if (!(await authority.consent(binding)))
|
|
355
|
+
throw new ExecutionBindingError('binding_consent_denied', 'consent grant is not authoritative');
|
|
356
|
+
const completedAt = typeof context.now === 'function' ? context.now() : context.now ?? Date.now();
|
|
357
|
+
if (completedAt >= deadline)
|
|
358
|
+
throw new ExecutionBindingError('binding_deadline_exceeded', 'execution deadline expired during authority checks');
|
|
359
|
+
if (completedAt >= leaseExpiry)
|
|
360
|
+
throw new ExecutionBindingError('binding_stale', 'execution lease expired during authority checks');
|
|
361
|
+
if (completedAt >= consentExpiry)
|
|
362
|
+
throw new ExecutionBindingError('binding_consent_denied', 'consent expired during authority checks');
|
|
363
|
+
const remainingAfterAuthority = Math.min(deadline, leaseExpiry, consentExpiry) - completedAt;
|
|
364
|
+
if (binding.resource_grant.max_runtime_ms > remainingAfterAuthority)
|
|
365
|
+
throw new ExecutionBindingError('binding_deadline_exceeded', 'max_runtime_ms no longer fits remaining binding lifetime');
|
|
366
|
+
return { binding, binding_digest: binding.binding_digest, remaining_deadline_ms: remainingAfterAuthority, effective_allowed_roots: effectiveRoots };
|
|
367
|
+
}
|
|
368
|
+
function normalizeTerminalRecord(input) {
|
|
369
|
+
const provenance = input.provenance.validator && input.provenance.validator.plan_digest !== undefined
|
|
370
|
+
? { ...input.provenance.validator, plan_digest: input.provenance.validator.plan_digest }
|
|
371
|
+
: input.provenance.validator;
|
|
372
|
+
return {
|
|
373
|
+
...input,
|
|
374
|
+
binding_digest: input.binding_digest,
|
|
375
|
+
provenance: { ...input.provenance, validator: provenance },
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function eventPayload(event) {
|
|
379
|
+
return event.payload && typeof event.payload === 'object' ? event.payload : null;
|
|
380
|
+
}
|
|
381
|
+
export class ExecutionBindingJournal {
|
|
382
|
+
ingestor;
|
|
383
|
+
lockPath;
|
|
384
|
+
constructor(ingestor) {
|
|
385
|
+
this.ingestor = ingestor;
|
|
386
|
+
this.lockPath = `${ingestor.path}.execution-binding.lock`;
|
|
387
|
+
}
|
|
388
|
+
async withLock(fn) {
|
|
389
|
+
acquireLock(this.lockPath);
|
|
390
|
+
try {
|
|
391
|
+
return await fn();
|
|
392
|
+
}
|
|
393
|
+
finally {
|
|
394
|
+
releaseLock(this.lockPath);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async appendCreated(binding) {
|
|
398
|
+
const canonical_binding = canonicalExecutionBinding(binding);
|
|
399
|
+
const payload = { task_id: binding.task_id, binding_digest: binding.binding_digest, canonical_binding };
|
|
400
|
+
if (Buffer.byteLength(JSON.stringify(payload), 'utf8') > MAX_INLINE_BINDING_BYTES) {
|
|
401
|
+
throw new ExecutionBindingError('binding_malformed', 'created binding event exceeds durable root-event capacity');
|
|
402
|
+
}
|
|
403
|
+
await this.ingestor.ingest({
|
|
404
|
+
type: 'execution.binding.created',
|
|
405
|
+
human: { title: `execution binding ${binding.task_id} created` },
|
|
406
|
+
payload,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
async claim(binding) {
|
|
410
|
+
return this.withLock(async () => {
|
|
411
|
+
let recovery = this.recover(binding.task_id, binding.binding_digest);
|
|
412
|
+
if (recovery.kind === 'new') {
|
|
413
|
+
await this.appendCreated(binding);
|
|
414
|
+
recovery = { kind: 'created', task_id: binding.task_id, binding_digest: binding.binding_digest };
|
|
415
|
+
}
|
|
416
|
+
if (recovery.kind === 'created') {
|
|
417
|
+
await this.ingestor.ingest({
|
|
418
|
+
type: 'execution.started',
|
|
419
|
+
human: { title: `execution ${binding.correlation.run_id} started` },
|
|
420
|
+
payload: { task_id: binding.task_id, run_id: binding.correlation.run_id, binding_digest: binding.binding_digest },
|
|
421
|
+
});
|
|
422
|
+
return { kind: 'claimed', task_id: binding.task_id, binding_digest: binding.binding_digest };
|
|
423
|
+
}
|
|
424
|
+
return recovery;
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
async recordCreated(binding) {
|
|
428
|
+
await this.withLock(async () => {
|
|
429
|
+
const recovery = this.recover(binding.task_id, binding.binding_digest);
|
|
430
|
+
if (recovery.kind === 'new')
|
|
431
|
+
await this.appendCreated(binding);
|
|
432
|
+
else if (recovery.kind !== 'created')
|
|
433
|
+
throw new ExecutionBindingError('binding_replay_duplicate', `cannot create binding from ${recovery.kind}`);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
async recordStarted(binding) {
|
|
437
|
+
await this.withLock(async () => {
|
|
438
|
+
const recovery = this.recover(binding.task_id, binding.binding_digest);
|
|
439
|
+
if (recovery.kind !== 'created') {
|
|
440
|
+
throw new ExecutionBindingError('binding_replay_unsafe', `cannot start binding from ${recovery.kind}`, true);
|
|
441
|
+
}
|
|
442
|
+
await this.ingestor.ingest({
|
|
443
|
+
type: 'execution.started',
|
|
444
|
+
human: { title: `execution ${binding.correlation.run_id} started` },
|
|
445
|
+
payload: { task_id: binding.task_id, run_id: binding.correlation.run_id, binding_digest: binding.binding_digest },
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
async recordDecision(first, second, third) {
|
|
450
|
+
const legacyDecision = third === undefined && typeof second === 'object' ? second : undefined;
|
|
451
|
+
const taskId = third === undefined
|
|
452
|
+
? (typeof legacyDecision?.['task_id'] === 'string' ? legacyDecision['task_id'] : undefined)
|
|
453
|
+
: first;
|
|
454
|
+
const bindingDigest = (third === undefined ? first : second);
|
|
455
|
+
const decision = third ?? legacyDecision;
|
|
456
|
+
if (!taskId)
|
|
457
|
+
throw new ExecutionBindingError('binding_malformed', 'execution decision requires task_id');
|
|
458
|
+
await this.ingestor.ingest({
|
|
459
|
+
type: 'execution.decision',
|
|
460
|
+
human: { title: `execution decision ${bindingDigest.slice(0, 20)}` },
|
|
461
|
+
payload: { task_id: taskId, binding_digest: bindingDigest, decision },
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
async recordTerminal(record) {
|
|
465
|
+
const parsed = terminalRecordSchema.safeParse(record);
|
|
466
|
+
if (!parsed.success)
|
|
467
|
+
throw new ExecutionBindingError('binding_provenance_unavailable', parsed.error.message);
|
|
468
|
+
const normalized = normalizeTerminalRecord(parsed.data);
|
|
469
|
+
if (normalized.disposition !== normalized.provenance.terminal_disposition) {
|
|
470
|
+
throw new ExecutionBindingError('binding_provenance_unavailable', 'terminal disposition disagrees with provenance', true);
|
|
471
|
+
}
|
|
472
|
+
await this.withLock(async () => {
|
|
473
|
+
const recovery = this.recover(normalized.task_id, normalized.binding_digest);
|
|
474
|
+
if (recovery.kind === 'terminal') {
|
|
475
|
+
if (canonicalize(recovery.terminal) === canonicalize(normalized))
|
|
476
|
+
return;
|
|
477
|
+
throw new ExecutionBindingError('binding_replay_duplicate', 'conflicting terminal record already exists', true);
|
|
478
|
+
}
|
|
479
|
+
// An incomplete started marker is the only unsafe state that can be
|
|
480
|
+
// repaired by appending a terminal. Other unsafe states (invalid
|
|
481
|
+
// provenance, conflicting terminals, activity without a created
|
|
482
|
+
// binding, or a started event after a terminal) must remain immutable;
|
|
483
|
+
// appending another terminal would hide the evidence and make recovery
|
|
484
|
+
// non-convergent.
|
|
485
|
+
const recoverableUnsafe = recovery.kind === 'unsafe_to_replay'
|
|
486
|
+
&& recovery.reason === 'execution started without terminal record';
|
|
487
|
+
if ((!recoverableUnsafe && recovery.kind !== 'created' && recovery.kind !== 'claimed')) {
|
|
488
|
+
throw new ExecutionBindingError('binding_replay_unsafe', `cannot record terminal from ${recovery.kind}`, true);
|
|
489
|
+
}
|
|
490
|
+
const payload = normalized;
|
|
491
|
+
if (Buffer.byteLength(JSON.stringify(payload), 'utf8') > MAX_INLINE_BINDING_BYTES) {
|
|
492
|
+
throw new ExecutionBindingError('binding_provenance_unavailable', 'terminal event exceeds durable root-event capacity', true);
|
|
493
|
+
}
|
|
494
|
+
await this.ingestor.ingest({
|
|
495
|
+
type: 'execution.terminal',
|
|
496
|
+
human: { title: `execution ${record.disposition}` },
|
|
497
|
+
payload,
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
async recordRecovered(taskId, bindingDigest, disposition) {
|
|
502
|
+
await this.ingestor.ingest({
|
|
503
|
+
type: 'execution.recovered',
|
|
504
|
+
human: { title: `execution recovery ${disposition}` },
|
|
505
|
+
payload: { task_id: taskId, binding_digest: bindingDigest, disposition },
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
recover(taskId, bindingDigest) {
|
|
509
|
+
const events = this.ingestor.readAllStrict();
|
|
510
|
+
const created = events.filter((event) => event.type === 'execution.binding.created')
|
|
511
|
+
.map((event) => eventPayload(event))
|
|
512
|
+
.filter((payload) => payload?.['task_id'] === taskId);
|
|
513
|
+
const existingActivity = events.some((event) => {
|
|
514
|
+
const payload = eventPayload(event);
|
|
515
|
+
return (event.type === 'execution.started' || event.type === 'execution.terminal' || event.type === 'execution.decision')
|
|
516
|
+
&& payload?.['task_id'] === taskId;
|
|
517
|
+
});
|
|
518
|
+
if (created.length === 0) {
|
|
519
|
+
return existingActivity
|
|
520
|
+
? { kind: 'unsafe_to_replay', task_id: taskId, binding_digest: bindingDigest ?? 'sha256:' + '0'.repeat(64), reason: 'execution activity exists without binding.created' }
|
|
521
|
+
: { kind: 'new', task_id: taskId, binding_digest: bindingDigest ?? 'sha256:' + '0'.repeat(64) };
|
|
522
|
+
}
|
|
523
|
+
const digests = new Set(created.map((payload) => typeof payload['binding_digest'] === 'string' ? payload['binding_digest'] : ''));
|
|
524
|
+
if (bindingDigest && [...digests].some((digestValue) => digestValue && digestValue !== bindingDigest)) {
|
|
525
|
+
return { kind: 'mismatched', task_id: taskId, binding_digest: bindingDigest, reason: 'another binding digest already exists for task' };
|
|
526
|
+
}
|
|
527
|
+
if (!bindingDigest && digests.size !== 1)
|
|
528
|
+
return { kind: 'mismatched', task_id: taskId, binding_digest: 'sha256:' + '0'.repeat(64), reason: 'multiple binding digests exist for task' };
|
|
529
|
+
const chosen = bindingDigest && digests.has(bindingDigest) ? bindingDigest : [...digests][0];
|
|
530
|
+
if (!chosen || !DIGEST_PATTERN.test(chosen))
|
|
531
|
+
return { kind: 'unsafe_to_replay', task_id: taskId, binding_digest: bindingDigest ?? 'sha256:' + '0'.repeat(64), reason: 'created event has invalid binding digest' };
|
|
532
|
+
const createdRecord = created.find((payload) => payload['binding_digest'] === chosen);
|
|
533
|
+
const canonicalBinding = createdRecord?.['canonical_binding'];
|
|
534
|
+
if (typeof canonicalBinding !== 'string' || sha256(canonicalBinding) !== chosen) {
|
|
535
|
+
return { kind: 'unsafe_to_replay', task_id: taskId, binding_digest: chosen, reason: 'created binding digest does not match canonical binding' };
|
|
536
|
+
}
|
|
537
|
+
let createdBinding;
|
|
538
|
+
try {
|
|
539
|
+
createdBinding = freezeExecutionBinding(JSON.parse(canonicalBinding));
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
return { kind: 'unsafe_to_replay', task_id: taskId, binding_digest: chosen, reason: 'created canonical binding is invalid' };
|
|
543
|
+
}
|
|
544
|
+
if (createdBinding.binding_digest !== chosen)
|
|
545
|
+
return { kind: 'unsafe_to_replay', task_id: taskId, binding_digest: chosen, reason: 'created canonical binding is inconsistent' };
|
|
546
|
+
const activity = events.filter((event) => {
|
|
547
|
+
const payload = eventPayload(event);
|
|
548
|
+
return (event.type === 'execution.started' || event.type === 'execution.terminal')
|
|
549
|
+
&& payload?.['task_id'] === taskId && payload?.['binding_digest'] === chosen;
|
|
550
|
+
});
|
|
551
|
+
const startedEvents = activity.filter((event) => event.type === 'execution.started');
|
|
552
|
+
const started = startedEvents.length > 0;
|
|
553
|
+
const terminalEvents = activity.filter((event) => event.type === 'execution.terminal');
|
|
554
|
+
if (startedEvents.some((event) => {
|
|
555
|
+
const payload = eventPayload(event);
|
|
556
|
+
return payload?.['run_id'] !== undefined && payload?.['run_id'] !== createdBinding.correlation.run_id;
|
|
557
|
+
}))
|
|
558
|
+
return { kind: 'unsafe_to_replay', task_id: taskId, binding_digest: chosen, reason: 'started event run_id disagrees with binding' };
|
|
559
|
+
if (terminalEvents.length > 0) {
|
|
560
|
+
const parsed = terminalEvents.map((event) => terminalRecordSchema.safeParse(eventPayload(event) ?? {}));
|
|
561
|
+
if (parsed.some((result) => !result.success))
|
|
562
|
+
return { kind: 'unsafe_to_replay', task_id: taskId, binding_digest: chosen, reason: 'terminal provenance is invalid' };
|
|
563
|
+
const terminals = parsed.map((result) => normalizeTerminalRecord(result.data));
|
|
564
|
+
if (terminals.some((terminal) => terminal.run_id !== createdBinding.correlation.run_id
|
|
565
|
+
|| terminal.disposition !== terminal.provenance.terminal_disposition)) {
|
|
566
|
+
return { kind: 'unsafe_to_replay', task_id: taskId, binding_digest: chosen, reason: 'terminal record disagrees with binding or provenance' };
|
|
567
|
+
}
|
|
568
|
+
const canonicalTerminal = canonicalize(terminals[0]);
|
|
569
|
+
if (terminals.some((terminal) => canonicalize(terminal) !== canonicalTerminal)) {
|
|
570
|
+
return { kind: 'unsafe_to_replay', task_id: taskId, binding_digest: chosen, reason: 'conflicting terminal records exist' };
|
|
571
|
+
}
|
|
572
|
+
const terminalSeq = terminalEvents[0].seq;
|
|
573
|
+
if (startedEvents.some((event) => event.seq > terminalSeq)) {
|
|
574
|
+
return { kind: 'unsafe_to_replay', task_id: taskId, binding_digest: chosen, reason: 'execution started after terminal record' };
|
|
575
|
+
}
|
|
576
|
+
return { kind: 'terminal', task_id: taskId, binding_digest: chosen, terminal: terminals[0] };
|
|
577
|
+
}
|
|
578
|
+
if (started)
|
|
579
|
+
return { kind: 'unsafe_to_replay', task_id: taskId, binding_digest: chosen, reason: 'execution started without terminal record' };
|
|
580
|
+
return { kind: 'created', task_id: taskId, binding_digest: chosen };
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
export function parseExecutionTerminalRecord(input) {
|
|
584
|
+
const parsed = terminalRecordSchema.safeParse(input);
|
|
585
|
+
if (!parsed.success)
|
|
586
|
+
throw new ExecutionBindingError('binding_provenance_unavailable', parsed.error.message, true);
|
|
587
|
+
return freezeDeep(normalizeTerminalRecord(parsed.data));
|
|
588
|
+
}
|
package/dist/exec/index.d.ts
CHANGED
package/dist/exec/index.js
CHANGED
|
@@ -5,5 +5,5 @@ export interface DiffStat {
|
|
|
5
5
|
}
|
|
6
6
|
/** Parse `git diff --shortstat` output, e.g. " 3 files changed, 12 insertions(+), 4 deletions(-)". */
|
|
7
7
|
export declare function parseGitShortstat(out: string): DiffStat;
|
|
8
|
-
/** Build a git_diff ProofOfWork from a parsed diff stat. */
|
|
8
|
+
/** Build a git_diff ProofOfWork from a parsed diff stat. #961: wire keys follow gep-sdk snake_case. */
|
|
9
9
|
export declare function gitDiffProof(stat: DiffStat, patchRef?: string): ProofOfWork;
|
package/dist/exec/proofOfWork.js
CHANGED
|
@@ -5,7 +5,7 @@ export function parseGitShortstat(out) {
|
|
|
5
5
|
const del = /(\d+)\s+deletions?\(-\)/.exec(out)?.[1];
|
|
6
6
|
return { files: Number(files ?? 0), lines: Number(ins ?? 0) + Number(del ?? 0) };
|
|
7
7
|
}
|
|
8
|
-
/** Build a git_diff ProofOfWork from a parsed diff stat. */
|
|
8
|
+
/** Build a git_diff ProofOfWork from a parsed diff stat. #961: wire keys follow gep-sdk snake_case. */
|
|
9
9
|
export function gitDiffProof(stat, patchRef) {
|
|
10
|
-
return { kind: 'git_diff',
|
|
10
|
+
return { kind: 'git_diff', git_diff: { files: stat.files, lines: stat.lines, ...(patchRef ? { patch_ref: patchRef } : {}) } };
|
|
11
11
|
}
|
|
@@ -33,6 +33,11 @@ export interface AgentRunResult {
|
|
|
33
33
|
error?: string;
|
|
34
34
|
failureKind?: 'spawn_failed' | 'timeout' | 'cancelled' | 'permission_denied' | 'non_zero_exit' | 'invalid_output' | 'runtime_error';
|
|
35
35
|
exitCode?: number | null;
|
|
36
|
+
/**
|
|
37
|
+
* Native harness session id reported by the runner, when available.
|
|
38
|
+
* Used as the Learning Ops exact-join key (`traceEvents[].sessionId`) when proxy llm_turn fold cannot supply one.
|
|
39
|
+
*/
|
|
40
|
+
sessionId?: string;
|
|
36
41
|
/** Runner-reported worktree used for the run; the bridge must verify it before reading or cleanup. */
|
|
37
42
|
managedWorktreePath?: string;
|
|
38
43
|
}
|