harnery 0.22.0 → 0.23.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/README.md +1 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +91 -2
- package/dist/core/work/state.d.ts +7 -0
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +112 -23
- package/dist/core/workflow/child-env.d.ts +10 -6
- package/dist/core/workflow/child-env.d.ts.map +1 -1
- package/dist/core/workflow/child-env.js +15 -7
- package/dist/core/workflow/durable-record.d.ts +7 -0
- package/dist/core/workflow/durable-record.d.ts.map +1 -0
- package/dist/core/workflow/durable-record.js +83 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +208 -29
- package/dist/core/workflow/index.d.ts +2 -0
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +1 -0
- package/dist/core/workflow/proof.d.ts +4 -0
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +50 -15
- package/dist/core/workflow/run-state.d.ts +3 -0
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +25 -13
- package/dist/core/workflow/types.d.ts +10 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/validate.d.ts +10 -1
- package/dist/core/workflow/validate.d.ts.map +1 -1
- package/dist/core/workflow/validate.js +75 -1
- package/dist/core/workflow/workspaces/attestation-error.d.ts +7 -0
- package/dist/core/workflow/workspaces/attestation-error.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/attestation-error.js +25 -0
- package/dist/core/workflow/workspaces/cancellation.d.ts +8 -0
- package/dist/core/workflow/workspaces/cancellation.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/cancellation.js +95 -0
- package/dist/core/workflow/workspaces/cleanup.d.ts +9 -0
- package/dist/core/workflow/workspaces/cleanup.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/cleanup.js +223 -0
- package/dist/core/workflow/workspaces/execution.d.ts +25 -0
- package/dist/core/workflow/workspaces/execution.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/execution.js +316 -0
- package/dist/core/workflow/workspaces/git.d.ts +25 -0
- package/dist/core/workflow/workspaces/git.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/git.js +102 -0
- package/dist/core/workflow/workspaces/index.d.ts +15 -0
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/index.js +7 -0
- package/dist/core/workflow/workspaces/inspect.d.ts +85 -0
- package/dist/core/workflow/workspaces/inspect.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/inspect.js +255 -0
- package/dist/core/workflow/workspaces/integration.d.ts +24 -0
- package/dist/core/workflow/workspaces/integration.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/integration.js +617 -0
- package/dist/core/workflow/workspaces/leases.d.ts +37 -0
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/leases.js +380 -0
- package/dist/core/workflow/workspaces/lifecycle.d.ts +43 -0
- package/dist/core/workflow/workspaces/lifecycle.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/lifecycle.js +622 -0
- package/dist/core/workflow/workspaces/local-git.d.ts +8 -0
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/local-git.js +1474 -0
- package/dist/core/workflow/workspaces/paths.d.ts +23 -0
- package/dist/core/workflow/workspaces/paths.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/paths.js +191 -0
- package/dist/core/workflow/workspaces/recovery.d.ts +3 -0
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/recovery.js +50 -0
- package/dist/core/workflow/workspaces/state.d.ts +60 -0
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/state.js +296 -0
- package/dist/core/workflow/workspaces/types.d.ts +445 -0
- package/dist/core/workflow/workspaces/types.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/types.js +2 -0
- package/dist/core/workflow/workspaces/validate.d.ts +14 -0
- package/dist/core/workflow/workspaces/validate.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/validate.js +476 -0
- package/package.json +5 -3
- package/src/commands/workflow.ts +110 -4
- package/src/core/work/state.ts +156 -28
- package/src/core/workflow/child-env.ts +18 -7
- package/src/core/workflow/durable-record.ts +94 -0
- package/src/core/workflow/engine.ts +230 -30
- package/src/core/workflow/index.ts +73 -0
- package/src/core/workflow/proof.ts +70 -22
- package/src/core/workflow/run-state.ts +46 -11
- package/src/core/workflow/types.ts +14 -0
- package/src/core/workflow/validate.ts +75 -1
- package/src/core/workflow/workspaces/attestation-error.ts +30 -0
- package/src/core/workflow/workspaces/cancellation.ts +137 -0
- package/src/core/workflow/workspaces/cleanup.ts +308 -0
- package/src/core/workflow/workspaces/execution.ts +412 -0
- package/src/core/workflow/workspaces/git.ts +125 -0
- package/src/core/workflow/workspaces/index.ts +85 -0
- package/src/core/workflow/workspaces/inspect.ts +421 -0
- package/src/core/workflow/workspaces/integration.ts +829 -0
- package/src/core/workflow/workspaces/leases.ts +495 -0
- package/src/core/workflow/workspaces/lifecycle.ts +792 -0
- package/src/core/workflow/workspaces/local-git.ts +1892 -0
- package/src/core/workflow/workspaces/paths.ts +247 -0
- package/src/core/workflow/workspaces/recovery.ts +56 -0
- package/src/core/workflow/workspaces/state.ts +481 -0
- package/src/core/workflow/workspaces/types.ts +517 -0
- package/src/core/workflow/workspaces/validate.ts +595 -0
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
import { assertWorkflowRunId, createWorkflowApproval } from "./approvals.ts";
|
|
39
39
|
import { freezeWorkflowAttemptContext } from "./attempt-context.ts";
|
|
40
40
|
import { type BillingProbe, probeBilling } from "./billing.ts";
|
|
41
|
+
import { stableDigest } from "./durable-record.ts";
|
|
41
42
|
import {
|
|
42
43
|
buildWorkflowProof,
|
|
43
44
|
createEvidenceRecord,
|
|
@@ -71,6 +72,18 @@ import type {
|
|
|
71
72
|
import { WORKFLOW_PROOF_SCHEMA_VERSION } from "./types.ts";
|
|
72
73
|
import { parseStageOutput, validateAgainstSchema } from "./validate.ts";
|
|
73
74
|
import { freezeWorkflowWorkContext } from "./work-context.ts";
|
|
75
|
+
import {
|
|
76
|
+
attestTerminal,
|
|
77
|
+
attestWorkspaceFailure,
|
|
78
|
+
resolveWorkspaceBinding,
|
|
79
|
+
} from "./workspaces/execution.ts";
|
|
80
|
+
import { appendWorkflowJournalEvent } from "./workspaces/state.ts";
|
|
81
|
+
import type {
|
|
82
|
+
WorkspaceAttestation,
|
|
83
|
+
WorkspaceBinding,
|
|
84
|
+
WorkspaceUnsupportedExecutionEvidence,
|
|
85
|
+
} from "./workspaces/types.ts";
|
|
86
|
+
import { isWorkspaceAttestation } from "./workspaces/validate.ts";
|
|
74
87
|
|
|
75
88
|
const DEFAULT_MAX_AGENTS = 50;
|
|
76
89
|
const DEFAULT_CONCURRENCY = 4;
|
|
@@ -193,13 +206,19 @@ async function executeWorkflow(
|
|
|
193
206
|
workContext: Readonly<WorkflowWorkContext> | undefined,
|
|
194
207
|
attemptContext: Readonly<WorkflowAttemptContext> | undefined,
|
|
195
208
|
): Promise<RunReport> {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
209
|
+
const frozen = resumeState?.manifest.execution;
|
|
210
|
+
const isolation = frozen?.isolation ?? opts.isolation ?? "shared";
|
|
211
|
+
let sharedWorkflow: WorkflowModule["default"] | undefined;
|
|
212
|
+
let sharedMeta: ReturnType<typeof normalizeWorkflowMeta> | undefined;
|
|
213
|
+
if (isolation === "shared") {
|
|
214
|
+
const mod = (await import(pathToFileURL(absScript).href)) as WorkflowModule;
|
|
215
|
+
if (typeof mod.default !== "function") {
|
|
216
|
+
throw new Error(`${scriptPath}: workflow script must \`export default async (ctx) => …\``);
|
|
217
|
+
}
|
|
218
|
+
const fallbackName = scriptPath.replace(/^.*\//, "").replace(/\.[cm]?js$/, "");
|
|
219
|
+
sharedWorkflow = mod.default;
|
|
220
|
+
sharedMeta = normalizeWorkflowMeta(mod.meta, fallbackName);
|
|
199
221
|
}
|
|
200
|
-
const fallbackName = scriptPath.replace(/^.*\//, "").replace(/\.[cm]?js$/, "");
|
|
201
|
-
const meta = normalizeWorkflowMeta(mod.meta, fallbackName);
|
|
202
|
-
const name = meta.name;
|
|
203
222
|
|
|
204
223
|
const runId =
|
|
205
224
|
opts.resumeRunId ??
|
|
@@ -210,7 +229,6 @@ async function executeWorkflow(
|
|
|
210
229
|
const journalPath = join(runDir, "journal.jsonl");
|
|
211
230
|
const proofPath = join(runDir, "proof.json");
|
|
212
231
|
|
|
213
|
-
const frozen = resumeState?.manifest.execution;
|
|
214
232
|
const maxAgents = frozen?.max_agents ?? opts.maxAgents ?? DEFAULT_MAX_AGENTS;
|
|
215
233
|
const concurrency = frozen?.concurrency ?? opts.concurrency ?? DEFAULT_CONCURRENCY;
|
|
216
234
|
assertPositiveWorkflowBound(maxAgents, "maxAgents");
|
|
@@ -221,7 +239,6 @@ async function executeWorkflow(
|
|
|
221
239
|
frozen?.default_harness ?? opts.defaultHarness ?? "claude-code";
|
|
222
240
|
const specialists =
|
|
223
241
|
frozen?.specialists ?? (resumeState ? {} : normalizeWorkflowSpecialists(opts.specialists));
|
|
224
|
-
const isolation = frozen?.isolation ?? opts.isolation ?? "shared";
|
|
225
242
|
const networkAccess = frozen?.network_access ?? opts.networkAccess ?? "unknown";
|
|
226
243
|
const policy =
|
|
227
244
|
frozen?.policy ?? (opts.policy ? normalizePolicy(opts.policy, { baseDir: cwd }) : undefined);
|
|
@@ -229,10 +246,102 @@ async function executeWorkflow(
|
|
|
229
246
|
const approvalAddressee = frozen?.approval_addressee ?? opts.approvalAddressee ?? "operator";
|
|
230
247
|
const subscriptionOnly = frozen?.subscription_only ?? Boolean(opts.subscriptionOnly);
|
|
231
248
|
const allowApiBilling = frozen?.allow_api_billing ?? Boolean(opts.allowApiBilling);
|
|
232
|
-
const repoBefore = resumeState?.manifest.repository_before ?? snapshotRepo(cwd);
|
|
233
249
|
const startedAt = resumeState?.manifest.started_at ?? new Date().toISOString();
|
|
234
250
|
const t0 = resumeState ? Date.parse(startedAt) : Date.now();
|
|
235
|
-
|
|
251
|
+
const scriptSha256 = resumeState?.manifest.script.sha256 ?? workflowScriptDigest(absScript);
|
|
252
|
+
const fallbackName = scriptPath.replace(/^.*\//, "").replace(/\.[cm]?js$/, "");
|
|
253
|
+
const fallbackMeta = normalizeWorkflowMeta(undefined, fallbackName);
|
|
254
|
+
const workspaceProvider = isolation === "shared" ? undefined : opts.workspace?.provider;
|
|
255
|
+
let workspaceBinding: WorkspaceBinding | undefined;
|
|
256
|
+
let workspaceFallback: WorkspaceUnsupportedExecutionEvidence | undefined =
|
|
257
|
+
resumeState?.manifest.execution.workspace_fallback;
|
|
258
|
+
try {
|
|
259
|
+
workspaceBinding = await resolveWorkspaceBinding({
|
|
260
|
+
opts,
|
|
261
|
+
resumeState,
|
|
262
|
+
runId,
|
|
263
|
+
cwd,
|
|
264
|
+
isolation,
|
|
265
|
+
absScript,
|
|
266
|
+
scriptSha256,
|
|
267
|
+
provider: workspaceProvider,
|
|
268
|
+
workContext,
|
|
269
|
+
attemptContext,
|
|
270
|
+
policy,
|
|
271
|
+
onUnsupportedFallback: (evidence) => {
|
|
272
|
+
workspaceFallback = evidence;
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
} catch (error) {
|
|
276
|
+
const frozenBinding = resumeState?.manifest.execution.workspace_binding;
|
|
277
|
+
if (!resumeState || !frozenBinding) throw error;
|
|
278
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
279
|
+
const endedAt = new Date().toISOString();
|
|
280
|
+
const durationMs = Date.now() - t0;
|
|
281
|
+
const failedAttestation = await attestWorkspaceFailure(workspaceProvider, frozenBinding, error);
|
|
282
|
+
appendWorkflowJournalEvent(opts.coordRoot, runId, "run.resume", {
|
|
283
|
+
approval_id: resumeState.approvalId,
|
|
284
|
+
script: absScript,
|
|
285
|
+
resumed_at: new Date().toISOString(),
|
|
286
|
+
});
|
|
287
|
+
appendWorkflowJournalEvent(opts.coordRoot, runId, "workspace.reattach.failed", {
|
|
288
|
+
binding_id: frozenBinding.binding_id,
|
|
289
|
+
provider_id: frozenBinding.provider.id,
|
|
290
|
+
status: failedAttestation.status,
|
|
291
|
+
reason: failure.message,
|
|
292
|
+
attestation_sha256: stableDigest(failedAttestation),
|
|
293
|
+
});
|
|
294
|
+
appendWorkflowJournalEvent(opts.coordRoot, runId, "run.end", {
|
|
295
|
+
ok: false,
|
|
296
|
+
error: failure.message,
|
|
297
|
+
agents: 0,
|
|
298
|
+
cached: 0,
|
|
299
|
+
cost_usd: 0,
|
|
300
|
+
duration_ms: durationMs,
|
|
301
|
+
});
|
|
302
|
+
try {
|
|
303
|
+
const proof = buildWorkflowProof({
|
|
304
|
+
runId,
|
|
305
|
+
workItemId: opts.workItemId ?? resumeState.manifest.work_item_id,
|
|
306
|
+
workContext,
|
|
307
|
+
attemptContext,
|
|
308
|
+
meta: fallbackMeta,
|
|
309
|
+
status: "failed",
|
|
310
|
+
startedAt,
|
|
311
|
+
endedAt,
|
|
312
|
+
durationMs,
|
|
313
|
+
journalPath,
|
|
314
|
+
before: resumeState.manifest.repository_before,
|
|
315
|
+
after: resumeState.manifest.repository_before,
|
|
316
|
+
agents: [],
|
|
317
|
+
evidence: [],
|
|
318
|
+
harnessEvidence: opts.harnessEvidence,
|
|
319
|
+
policy: policy
|
|
320
|
+
? {
|
|
321
|
+
config: policy,
|
|
322
|
+
decisions: [],
|
|
323
|
+
isolation,
|
|
324
|
+
networkAccess,
|
|
325
|
+
}
|
|
326
|
+
: undefined,
|
|
327
|
+
workspaceBinding: frozenBinding,
|
|
328
|
+
workspaceAttestation: failedAttestation,
|
|
329
|
+
error: failure.message,
|
|
330
|
+
});
|
|
331
|
+
writeWorkflowProof(proofPath, proof);
|
|
332
|
+
} catch (proofError) {
|
|
333
|
+
throw new WorkflowRunError(
|
|
334
|
+
`${failure.message}; recovery proof packet write also failed: ${(proofError as Error).message}`,
|
|
335
|
+
runId,
|
|
336
|
+
proofPath,
|
|
337
|
+
failure,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
throw new WorkflowRunError(failure.message, runId, proofPath, failure);
|
|
341
|
+
}
|
|
342
|
+
const effectiveIsolation = workspaceFallback?.effective_isolation ?? isolation;
|
|
343
|
+
const executionCwd = workspaceBinding?.active_root ?? cwd;
|
|
344
|
+
const executionRepoBefore = resumeState?.manifest.repository_before ?? snapshotRepo(executionCwd);
|
|
236
345
|
if (!resumeState) {
|
|
237
346
|
writeWorkflowRunManifest({
|
|
238
347
|
coordRoot: opts.coordRoot,
|
|
@@ -242,12 +351,12 @@ async function executeWorkflow(
|
|
|
242
351
|
work_item_id: opts.workItemId,
|
|
243
352
|
work_context: workContext,
|
|
244
353
|
attempt_context: attemptContext,
|
|
245
|
-
name,
|
|
354
|
+
name: sharedMeta?.name ?? fallbackMeta.name,
|
|
246
355
|
started_at: startedAt,
|
|
247
|
-
script: { path: absScript, sha256:
|
|
248
|
-
repository_before:
|
|
356
|
+
script: { path: absScript, sha256: scriptSha256 },
|
|
357
|
+
repository_before: executionRepoBefore,
|
|
249
358
|
execution: {
|
|
250
|
-
cwd,
|
|
359
|
+
cwd: executionCwd,
|
|
251
360
|
default_harness: defaultHarness,
|
|
252
361
|
max_agents: maxAgents,
|
|
253
362
|
concurrency,
|
|
@@ -259,19 +368,80 @@ async function executeWorkflow(
|
|
|
259
368
|
network_access: networkAccess,
|
|
260
369
|
policy: policy ? (policy as NormalizedPolicy) : undefined,
|
|
261
370
|
specialists,
|
|
371
|
+
workspace_binding: workspaceBinding,
|
|
372
|
+
workspace_fallback: workspaceFallback,
|
|
262
373
|
},
|
|
263
374
|
},
|
|
264
375
|
});
|
|
265
376
|
}
|
|
266
377
|
|
|
378
|
+
let workflow = sharedWorkflow;
|
|
379
|
+
let meta = sharedMeta ?? fallbackMeta;
|
|
380
|
+
let initializationError: Error | undefined;
|
|
381
|
+
let initializationAttestation: WorkspaceAttestation | undefined;
|
|
382
|
+
if (isolation !== "shared") {
|
|
383
|
+
try {
|
|
384
|
+
const mod = (await import(pathToFileURL(absScript).href)) as WorkflowModule;
|
|
385
|
+
if (typeof mod.default !== "function") {
|
|
386
|
+
throw new Error(`${scriptPath}: workflow script must \`export default async (ctx) => …\``);
|
|
387
|
+
}
|
|
388
|
+
workflow = mod.default;
|
|
389
|
+
meta = normalizeWorkflowMeta(mod.meta, fallbackName);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
initializationError = error instanceof Error ? error : new Error(String(error));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (workspaceBinding && workspaceProvider) {
|
|
395
|
+
try {
|
|
396
|
+
const postImport = await workspaceProvider.reattach(workspaceBinding);
|
|
397
|
+
if (!isWorkspaceAttestation(postImport, workspaceBinding) || postImport.status !== "ok") {
|
|
398
|
+
initializationAttestation = isWorkspaceAttestation(postImport, workspaceBinding)
|
|
399
|
+
? postImport
|
|
400
|
+
: undefined;
|
|
401
|
+
throw new Error("workspace authority changed during workflow module initialization");
|
|
402
|
+
}
|
|
403
|
+
appendWorkflowJournalEvent(opts.coordRoot, runId, "workspace.reattach.module_initialized", {
|
|
404
|
+
binding_id: workspaceBinding.binding_id,
|
|
405
|
+
provider_id: workspaceBinding.provider.id,
|
|
406
|
+
attestation_sha256: stableDigest(postImport),
|
|
407
|
+
});
|
|
408
|
+
} catch (error) {
|
|
409
|
+
const reattachmentError = error instanceof Error ? error : new Error(String(error));
|
|
410
|
+
initializationAttestation ??= await attestWorkspaceFailure(
|
|
411
|
+
workspaceProvider,
|
|
412
|
+
workspaceBinding,
|
|
413
|
+
error,
|
|
414
|
+
);
|
|
415
|
+
initializationError = initializationError
|
|
416
|
+
? new Error(`${initializationError.message}; ${reattachmentError.message}`, {
|
|
417
|
+
cause: reattachmentError,
|
|
418
|
+
})
|
|
419
|
+
: reattachmentError;
|
|
420
|
+
appendWorkflowJournalEvent(
|
|
421
|
+
opts.coordRoot,
|
|
422
|
+
runId,
|
|
423
|
+
"workspace.reattach.module_initialization_failed",
|
|
424
|
+
{
|
|
425
|
+
binding_id: workspaceBinding.binding_id,
|
|
426
|
+
provider_id: workspaceBinding.provider.id,
|
|
427
|
+
status: initializationAttestation.status,
|
|
428
|
+
reason: reattachmentError.message,
|
|
429
|
+
attestation_sha256: stableDigest(initializationAttestation),
|
|
430
|
+
},
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const name = meta.name;
|
|
435
|
+
let observedWorkspaceAttestation = initializationAttestation;
|
|
436
|
+
|
|
267
437
|
// Per-child fixed context overhead: children spawn in `cwd` and load its
|
|
268
438
|
// repo-instructions file into their system prompt, cache-writing it once
|
|
269
439
|
// per child. A fan-out multiplies this, so surface it BEFORE the burn.
|
|
270
|
-
const contextTokensPerChildEstimate = estimateInstructionTokens(
|
|
440
|
+
const contextTokensPerChildEstimate = estimateInstructionTokens(executionCwd);
|
|
271
441
|
if (contextTokensPerChildEstimate > 0) {
|
|
272
442
|
log(
|
|
273
443
|
`[context] each child cache-writes ~${Math.round(contextTokensPerChildEstimate / 1000)}K tokens of repo ` +
|
|
274
|
-
`instructions from ${
|
|
444
|
+
`instructions from ${executionCwd}; a fan-out multiplies this per agent`,
|
|
275
445
|
);
|
|
276
446
|
}
|
|
277
447
|
|
|
@@ -528,7 +698,7 @@ async function executeWorkflow(
|
|
|
528
698
|
const estimateRequest = summarizePolicyRequest({
|
|
529
699
|
phase: "dispatch",
|
|
530
700
|
action: "spawn agent",
|
|
531
|
-
path:
|
|
701
|
+
path: executionCwd,
|
|
532
702
|
harness,
|
|
533
703
|
model: agentOpts.model,
|
|
534
704
|
effort: agentOpts.effort,
|
|
@@ -536,7 +706,7 @@ async function executeWorkflow(
|
|
|
536
706
|
max_turns: agentOpts.maxTurns ?? DEFAULT_MAX_TURNS,
|
|
537
707
|
timeout_ms: agentOpts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
538
708
|
prompt_bytes: Buffer.byteLength(dispatchPrompt),
|
|
539
|
-
isolation,
|
|
709
|
+
isolation: effectiveIsolation,
|
|
540
710
|
network_access: networkAccess,
|
|
541
711
|
current_cost_usd: round4(costUsd + reservedCostUsd),
|
|
542
712
|
projected_cost_usd: null,
|
|
@@ -667,7 +837,7 @@ async function executeWorkflow(
|
|
|
667
837
|
effort: agentOpts.effort,
|
|
668
838
|
timeoutMs: agentOpts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
669
839
|
maxTurns: agentOpts.maxTurns ?? DEFAULT_MAX_TURNS,
|
|
670
|
-
cwd,
|
|
840
|
+
cwd: executionCwd,
|
|
671
841
|
runId,
|
|
672
842
|
subscriptionOnly,
|
|
673
843
|
});
|
|
@@ -797,8 +967,8 @@ async function executeWorkflow(
|
|
|
797
967
|
return authorizePolicyRequest({
|
|
798
968
|
phase: "external_mutation",
|
|
799
969
|
action: input.action,
|
|
800
|
-
path: input.path ? resolve(
|
|
801
|
-
isolation,
|
|
970
|
+
path: input.path ? resolve(executionCwd, input.path) : undefined,
|
|
971
|
+
isolation: effectiveIsolation,
|
|
802
972
|
network_access: network,
|
|
803
973
|
service: input.service,
|
|
804
974
|
target: input.target,
|
|
@@ -836,13 +1006,29 @@ async function executeWorkflow(
|
|
|
836
1006
|
concurrency,
|
|
837
1007
|
specialists: Object.keys(specialists),
|
|
838
1008
|
policy: policy ? { name: policy.name, sha256: policyDigest(policy) } : null,
|
|
839
|
-
isolation,
|
|
1009
|
+
isolation: effectiveIsolation,
|
|
840
1010
|
network_access: networkAccess,
|
|
1011
|
+
workspace_binding: workspaceBinding
|
|
1012
|
+
? { binding_id: workspaceBinding.binding_id, provider_id: workspaceBinding.provider.id }
|
|
1013
|
+
: null,
|
|
841
1014
|
});
|
|
842
1015
|
}
|
|
843
|
-
|
|
1016
|
+
if (initializationError) throw initializationError;
|
|
1017
|
+
if (!workflow) throw new Error("workflow module initialization did not produce an executable");
|
|
1018
|
+
const result = await workflow(ctx);
|
|
844
1019
|
const endedAt = new Date().toISOString();
|
|
845
1020
|
const durationMs = Date.now() - t0;
|
|
1021
|
+
const workspaceAttestation = workspaceBinding
|
|
1022
|
+
? await attestTerminal(workspaceProvider, workspaceBinding)
|
|
1023
|
+
: undefined;
|
|
1024
|
+
observedWorkspaceAttestation = workspaceAttestation;
|
|
1025
|
+
if (workspaceAttestation && workspaceAttestation.status !== "ok") {
|
|
1026
|
+
throw new Error(
|
|
1027
|
+
`terminal workspace attestation ${workspaceAttestation.status}: ${
|
|
1028
|
+
workspaceAttestation.provider_drift.join("; ") || "provider could not prove the workspace"
|
|
1029
|
+
}`,
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
846
1032
|
journal("run.end", {
|
|
847
1033
|
ok: true,
|
|
848
1034
|
agents: agentsSpawned,
|
|
@@ -863,8 +1049,8 @@ async function executeWorkflow(
|
|
|
863
1049
|
endedAt,
|
|
864
1050
|
durationMs,
|
|
865
1051
|
journalPath,
|
|
866
|
-
before:
|
|
867
|
-
after: snapshotRepo(
|
|
1052
|
+
before: executionRepoBefore,
|
|
1053
|
+
after: snapshotRepo(executionCwd),
|
|
868
1054
|
agents: Array.from(agentProofs.values()),
|
|
869
1055
|
evidence: evidenceRecords,
|
|
870
1056
|
harnessEvidence: opts.harnessEvidence,
|
|
@@ -872,10 +1058,13 @@ async function executeWorkflow(
|
|
|
872
1058
|
? {
|
|
873
1059
|
config: policy,
|
|
874
1060
|
decisions: Array.from(policyDecisions.values()),
|
|
875
|
-
isolation,
|
|
1061
|
+
isolation: effectiveIsolation,
|
|
876
1062
|
networkAccess,
|
|
877
1063
|
}
|
|
878
1064
|
: undefined,
|
|
1065
|
+
workspaceBinding,
|
|
1066
|
+
workspaceAttestation,
|
|
1067
|
+
workspaceFallback,
|
|
879
1068
|
result,
|
|
880
1069
|
});
|
|
881
1070
|
writeWorkflowProof(proofPath, proof);
|
|
@@ -905,10 +1094,14 @@ async function executeWorkflow(
|
|
|
905
1094
|
mode: subscriptionOnly ? "subscription" : p.mode,
|
|
906
1095
|
})),
|
|
907
1096
|
policy: proof.policy?.summary,
|
|
1097
|
+
workspaceBinding,
|
|
908
1098
|
};
|
|
909
1099
|
return report;
|
|
910
1100
|
} catch (err) {
|
|
911
|
-
if (err instanceof WorkflowParkedError
|
|
1101
|
+
if (err instanceof WorkflowParkedError) {
|
|
1102
|
+
throw err;
|
|
1103
|
+
}
|
|
1104
|
+
if (err instanceof WorkflowRunError) throw err;
|
|
912
1105
|
const endedAt = new Date().toISOString();
|
|
913
1106
|
const durationMs = Date.now() - t0;
|
|
914
1107
|
journal("run.end", {
|
|
@@ -920,6 +1113,10 @@ async function executeWorkflow(
|
|
|
920
1113
|
duration_ms: durationMs,
|
|
921
1114
|
});
|
|
922
1115
|
try {
|
|
1116
|
+
const workspaceAttestation = workspaceBinding
|
|
1117
|
+
? (observedWorkspaceAttestation ??
|
|
1118
|
+
(await attestTerminal(workspaceProvider, workspaceBinding)))
|
|
1119
|
+
: undefined;
|
|
923
1120
|
const proof = buildWorkflowProof({
|
|
924
1121
|
runId,
|
|
925
1122
|
workItemId: opts.workItemId ?? resumeState?.manifest.work_item_id,
|
|
@@ -931,8 +1128,8 @@ async function executeWorkflow(
|
|
|
931
1128
|
endedAt,
|
|
932
1129
|
durationMs,
|
|
933
1130
|
journalPath,
|
|
934
|
-
before:
|
|
935
|
-
after: snapshotRepo(
|
|
1131
|
+
before: executionRepoBefore,
|
|
1132
|
+
after: snapshotRepo(executionCwd),
|
|
936
1133
|
agents: Array.from(agentProofs.values()),
|
|
937
1134
|
evidence: evidenceRecords,
|
|
938
1135
|
harnessEvidence: opts.harnessEvidence,
|
|
@@ -940,10 +1137,13 @@ async function executeWorkflow(
|
|
|
940
1137
|
? {
|
|
941
1138
|
config: policy,
|
|
942
1139
|
decisions: Array.from(policyDecisions.values()),
|
|
943
|
-
isolation,
|
|
1140
|
+
isolation: effectiveIsolation,
|
|
944
1141
|
networkAccess,
|
|
945
1142
|
}
|
|
946
1143
|
: undefined,
|
|
1144
|
+
workspaceBinding,
|
|
1145
|
+
workspaceAttestation,
|
|
1146
|
+
workspaceFallback,
|
|
947
1147
|
error: (err as Error).message,
|
|
948
1148
|
});
|
|
949
1149
|
writeWorkflowProof(proofPath, proof);
|
|
@@ -87,3 +87,76 @@ export {
|
|
|
87
87
|
WORKFLOW_PROOF_SCHEMA_VERSION,
|
|
88
88
|
WORKFLOW_WORK_CONTEXT_SCHEMA_VERSION,
|
|
89
89
|
} from "./types.ts";
|
|
90
|
+
export type {
|
|
91
|
+
ApplyIntegrationInput,
|
|
92
|
+
AuthorizedIntegrationPlan,
|
|
93
|
+
AuthorizedProviderIntegrationInput,
|
|
94
|
+
CancelWorkspaceInput,
|
|
95
|
+
CleanupWorkspaceInput,
|
|
96
|
+
FilesystemIdentity,
|
|
97
|
+
IntegrationApplyAttempt,
|
|
98
|
+
IntegrationApplyReceipt,
|
|
99
|
+
IntegrationAuthorization,
|
|
100
|
+
IntegrationPlan,
|
|
101
|
+
IntegrationReviewRecord,
|
|
102
|
+
IntegrationTarget,
|
|
103
|
+
LocalGitWorktreeProviderOptions,
|
|
104
|
+
PrepareIntegrationInput,
|
|
105
|
+
ProviderIntegrationInput,
|
|
106
|
+
ProviderIntegrationPreview,
|
|
107
|
+
ProviderIntegrationResult,
|
|
108
|
+
ProviderUnknown,
|
|
109
|
+
ProviderUnsupportedReason,
|
|
110
|
+
RepositoryBinding,
|
|
111
|
+
ValidatedFilesystemPath,
|
|
112
|
+
WorkflowWorkspaceInspection,
|
|
113
|
+
WorkflowWorkspaceSelection,
|
|
114
|
+
WorkflowWorkspaceStatus,
|
|
115
|
+
WorkflowWorkspaceVerification,
|
|
116
|
+
WorkspaceAllocationRequest,
|
|
117
|
+
WorkspaceAttestation,
|
|
118
|
+
WorkspaceBinding,
|
|
119
|
+
WorkspaceBoundExecutionEvidence,
|
|
120
|
+
WorkspaceCancellationReceipt,
|
|
121
|
+
WorkspaceCancellationResult,
|
|
122
|
+
WorkspaceCleanupAttempt,
|
|
123
|
+
WorkspaceCleanupIntent,
|
|
124
|
+
WorkspaceCleanupMode,
|
|
125
|
+
WorkspaceCleanupReceipt,
|
|
126
|
+
WorkspaceCleanupResult,
|
|
127
|
+
WorkspaceCompatibilityExecutionEvidence,
|
|
128
|
+
WorkspaceExecutionEvidence,
|
|
129
|
+
WorkspaceIntegrationState,
|
|
130
|
+
WorkspaceIsolation,
|
|
131
|
+
WorkspaceLifecycleProjection,
|
|
132
|
+
WorkspaceLifecycleProjectionInput,
|
|
133
|
+
WorkspaceLifecycleState,
|
|
134
|
+
WorkspaceObservation,
|
|
135
|
+
WorkspaceOwner,
|
|
136
|
+
WorkspaceProbeInput,
|
|
137
|
+
WorkspaceProbeResult,
|
|
138
|
+
WorkspaceProofOutcome,
|
|
139
|
+
WorkspaceProvider,
|
|
140
|
+
WorkspaceProviderCapabilities,
|
|
141
|
+
WorkspaceProviderRef,
|
|
142
|
+
WorkspaceResourceState,
|
|
143
|
+
WorkspaceUnsupportedExecutionEvidence,
|
|
144
|
+
} from "./workspaces/index.ts";
|
|
145
|
+
export {
|
|
146
|
+
applyIntegration,
|
|
147
|
+
assertWorkspaceLifecycleTransition,
|
|
148
|
+
cancelWorkspace,
|
|
149
|
+
cleanupWorkspace,
|
|
150
|
+
createLocalGitWorktreeProvider,
|
|
151
|
+
deriveWorkspaceLifecycle,
|
|
152
|
+
inspectWorkflowWorkspace,
|
|
153
|
+
isWorkspaceLifecycleState,
|
|
154
|
+
listWorkflowWorkspaceInspections,
|
|
155
|
+
prepareIntegration,
|
|
156
|
+
probeLocalGitWorktreeProvider,
|
|
157
|
+
readWorkflowWorkspaceStatus,
|
|
158
|
+
renderWorkflowWorkspaceStatus,
|
|
159
|
+
validateProviderEventChain,
|
|
160
|
+
WORKSPACE_BINDING_SCHEMA_VERSION,
|
|
161
|
+
WORKSPACE_RECEIPT_SCHEMA_VERSION,
|
|
162
|
+
} from "./workspaces/index.ts";
|
|
@@ -1,15 +1,6 @@
|
|
|
1
|
-
import { createHash
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
existsSync,
|
|
5
|
-
mkdirSync,
|
|
6
|
-
readFileSync,
|
|
7
|
-
renameSync,
|
|
8
|
-
statSync,
|
|
9
|
-
unlinkSync,
|
|
10
|
-
writeFileSync,
|
|
11
|
-
} from "node:fs";
|
|
12
|
-
import { dirname, join } from "node:path";
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
13
4
|
import type { RepoSnapshot } from "../context/index.ts";
|
|
14
5
|
import type {
|
|
15
6
|
NormalizedPolicy,
|
|
@@ -19,6 +10,8 @@ import type {
|
|
|
19
10
|
} from "../policy/index.ts";
|
|
20
11
|
import { policyDigest } from "../policy/index.ts";
|
|
21
12
|
import { isCanonicalWorkflowAttemptContext } from "./attempt-context.ts";
|
|
13
|
+
import { stableDigest, writeImmutableJson } from "./durable-record.ts";
|
|
14
|
+
import { readWorkflowRunManifest } from "./run-state.ts";
|
|
22
15
|
import type {
|
|
23
16
|
AcceptanceCriterion,
|
|
24
17
|
AcceptanceResult,
|
|
@@ -40,6 +33,18 @@ import type {
|
|
|
40
33
|
} from "./types.ts";
|
|
41
34
|
import { WORKFLOW_PROOF_SCHEMA_VERSION } from "./types.ts";
|
|
42
35
|
import { isCanonicalWorkflowWorkContext } from "./work-context.ts";
|
|
36
|
+
import type {
|
|
37
|
+
WorkspaceAttestation,
|
|
38
|
+
WorkspaceBinding,
|
|
39
|
+
WorkspaceExecutionEvidence,
|
|
40
|
+
WorkspaceUnsupportedExecutionEvidence,
|
|
41
|
+
} from "./workspaces/index.ts";
|
|
42
|
+
import {
|
|
43
|
+
isWorkspaceBoundExecutionEvidence,
|
|
44
|
+
isWorkspaceExecutionEvidence,
|
|
45
|
+
isWorkspaceUnsupportedExecutionEvidence,
|
|
46
|
+
workspaceProofLifecycle,
|
|
47
|
+
} from "./workspaces/validate.ts";
|
|
43
48
|
|
|
44
49
|
const MAX_ACCEPTANCE_CRITERIA = 50;
|
|
45
50
|
const MAX_EVIDENCE_RECORDS = 200;
|
|
@@ -82,6 +87,9 @@ export interface BuildWorkflowProofInput {
|
|
|
82
87
|
isolation: PolicyIsolation;
|
|
83
88
|
networkAccess: PolicyNetworkAccess;
|
|
84
89
|
};
|
|
90
|
+
workspaceBinding?: WorkspaceBinding;
|
|
91
|
+
workspaceAttestation?: WorkspaceAttestation;
|
|
92
|
+
workspaceFallback?: WorkspaceUnsupportedExecutionEvidence;
|
|
85
93
|
result?: unknown;
|
|
86
94
|
error?: string;
|
|
87
95
|
}
|
|
@@ -237,6 +245,10 @@ export function buildWorkflowProof(input: BuildWorkflowProofInput): WorkflowProo
|
|
|
237
245
|
agents,
|
|
238
246
|
evidence: input.evidence,
|
|
239
247
|
policy: input.policy ? buildPolicyProof(input.policy) : undefined,
|
|
248
|
+
execution:
|
|
249
|
+
input.workspaceBinding && input.workspaceAttestation
|
|
250
|
+
? buildExecutionEvidence(input.status, input.workspaceBinding, input.workspaceAttestation)
|
|
251
|
+
: input.workspaceFallback,
|
|
240
252
|
repository,
|
|
241
253
|
harnesses,
|
|
242
254
|
unknowns,
|
|
@@ -286,15 +298,7 @@ export function writeWorkflowProof(path: string, proof: WorkflowProof): void {
|
|
|
286
298
|
if (bytes > MAX_PACKET_BYTES) {
|
|
287
299
|
throw new Error(`workflow proof is ${bytes} bytes; limit is ${MAX_PACKET_BYTES}`);
|
|
288
300
|
}
|
|
289
|
-
|
|
290
|
-
const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
|
|
291
|
-
try {
|
|
292
|
-
writeFileSync(temporary, body, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
293
|
-
renameSync(temporary, path);
|
|
294
|
-
chmodSync(path, 0o600);
|
|
295
|
-
} finally {
|
|
296
|
-
if (existsSync(temporary)) unlinkSync(temporary);
|
|
297
|
-
}
|
|
301
|
+
writeImmutableJson(path, proof);
|
|
298
302
|
}
|
|
299
303
|
|
|
300
304
|
export function readWorkflowProof(coordRoot: string, runId: string): WorkflowProof {
|
|
@@ -321,13 +325,57 @@ export function readWorkflowProof(coordRoot: string, runId: string): WorkflowPro
|
|
|
321
325
|
(proof.run.attempt_context !== undefined &&
|
|
322
326
|
(!proof.run.work_item_id ||
|
|
323
327
|
!proof.run.work_context ||
|
|
324
|
-
!isCanonicalWorkflowAttemptContext(proof.run.attempt_context)))
|
|
328
|
+
!isCanonicalWorkflowAttemptContext(proof.run.attempt_context))) ||
|
|
329
|
+
(proof.execution !== undefined &&
|
|
330
|
+
!isWorkspaceExecutionEvidence(proof.execution, runId, proof.run.status))
|
|
325
331
|
) {
|
|
326
332
|
throw new Error(`workflow proof at ${path} has an unsupported or mismatched schema`);
|
|
327
333
|
}
|
|
334
|
+
const manifestPath = join(coordRoot, ".harnery", "workflows", runId, "run.json");
|
|
335
|
+
if (proof.execution === undefined && !existsSync(manifestPath)) return proof;
|
|
336
|
+
const manifest = readWorkflowRunManifest(coordRoot, runId);
|
|
337
|
+
const manifestBinding = manifest.execution.workspace_binding;
|
|
338
|
+
const manifestFallback = manifest.execution.workspace_fallback;
|
|
339
|
+
const proofExecution = proof.execution;
|
|
340
|
+
const executionMatches =
|
|
341
|
+
manifestBinding !== undefined
|
|
342
|
+
? isWorkspaceBoundExecutionEvidence(proofExecution, runId) &&
|
|
343
|
+
stableDigest(proofExecution.binding) === stableDigest(manifestBinding)
|
|
344
|
+
: manifestFallback !== undefined
|
|
345
|
+
? isWorkspaceUnsupportedExecutionEvidence(proofExecution, runId) &&
|
|
346
|
+
stableDigest(proofExecution) === stableDigest(manifestFallback)
|
|
347
|
+
: proofExecution === undefined;
|
|
348
|
+
if (!executionMatches) {
|
|
349
|
+
throw new Error(`workflow proof at ${path} does not match the frozen execution manifest`);
|
|
350
|
+
}
|
|
328
351
|
return proof;
|
|
329
352
|
}
|
|
330
353
|
|
|
354
|
+
function buildExecutionEvidence(
|
|
355
|
+
workflowStatus: "succeeded" | "failed",
|
|
356
|
+
binding: WorkspaceBinding,
|
|
357
|
+
terminalAttestation: WorkspaceAttestation,
|
|
358
|
+
): WorkspaceExecutionEvidence {
|
|
359
|
+
return {
|
|
360
|
+
schema_version: 1,
|
|
361
|
+
binding,
|
|
362
|
+
terminal_attestation: terminalAttestation,
|
|
363
|
+
terminal_lifecycle_state: workspaceProofLifecycle(workflowStatus, terminalAttestation),
|
|
364
|
+
drift: [...terminalAttestation.provider_drift],
|
|
365
|
+
unsupported: [...terminalAttestation.unsupported],
|
|
366
|
+
unknowns: [...terminalAttestation.unknowns],
|
|
367
|
+
receipts: {
|
|
368
|
+
request: "workspace-request.json",
|
|
369
|
+
cancellation_outcome: "cancellation/outcome.json",
|
|
370
|
+
integration_plan: "integration/plan.json",
|
|
371
|
+
integration_authorization: "integration/authorization.json",
|
|
372
|
+
integration_apply: "integration/receipt.json",
|
|
373
|
+
cleanup_intent: "cleanup/intent.json",
|
|
374
|
+
cleanup_receipt: "cleanup/receipt.json",
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
331
379
|
export function renderWorkflowProof(proof: WorkflowProof): string {
|
|
332
380
|
const lines = [
|
|
333
381
|
`run ${proof.run.id} (${proof.run.name}): ${proof.run.status}`,
|