@tea-agent/loop-agent 0.41.0 → 0.42.0-next.1
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/CHANGELOG.md +102 -14
- package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
- package/dist/adapters/context-transfer/pi-session.js +61 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +12 -0
- package/dist/cli/program.js +23 -1
- package/dist/commands/dag-artifact.js +284 -0
- package/dist/commands/dag-context.js +184 -0
- package/dist/commands/dag-rerun.js +206 -1
- package/dist/executors/dag-pi-executor.js +39 -12
- package/dist/executors/pi-sdk-executor.js +3 -0
- package/dist/{worker → infrastructure}/console/app-data.js +4 -0
- package/dist/infrastructure/console/artifact-revision-store.js +430 -0
- package/dist/infrastructure/console/context-export-store.js +160 -0
- package/dist/infrastructure/console/dir-lock.js +132 -0
- package/dist/{worker → infrastructure}/console/operation-store.js +28 -0
- package/dist/infrastructure/harness/artifact-store.js +10 -1
- package/dist/infrastructure/harness/atomic-write.js +12 -2
- package/dist/shared/context-transfer/artifact-revision.js +172 -0
- package/dist/shared/context-transfer.js +418 -0
- package/dist/shared/operator/capabilities.js +14 -0
- package/dist/shared/operator/safe-run-summary.js +1 -0
- package/dist/shared/path-safety.js +93 -0
- package/dist/shared/preview.js +28 -4
- package/dist/task/config-types.js +2 -1
- package/dist/task/task-demand-routing.js +3 -0
- package/dist/worker/console/chat/chat-event-store.js +2 -2
- package/dist/worker/console/chat/resource-preferences-store.js +1 -1
- package/dist/worker/console/chat/routes.js +1 -1
- package/dist/worker/console/chat/session-store.js +1 -1
- package/dist/worker/console/chat/user-questions.js +1 -1
- package/dist/worker/console/console-update-runtime.js +1 -1
- package/dist/worker/console/context-transfer-diagnostics.js +198 -0
- package/dist/worker/console/dag-confirmation.js +1 -1
- package/dist/worker/console/dag-execution-receipt.js +1 -1
- package/dist/worker/console/doctor.js +1 -1
- package/dist/worker/console/draft-store.js +1 -1
- package/dist/worker/console/human-gate-token.js +1 -1
- package/dist/worker/console/index.js +3 -3
- package/dist/worker/console/interview/assessment.js +1 -1
- package/dist/worker/console/interview/session.js +1 -1
- package/dist/worker/console/operation-runner.js +45 -4
- package/dist/worker/console/operation-sse.js +1 -1
- package/dist/worker/console/operation-wait.js +1 -1
- package/dist/worker/console/operator-actions.js +104 -3
- package/dist/worker/console/operator-surface-health.js +1 -1
- package/dist/worker/console/operator-user-error.js +169 -0
- package/dist/worker/console/pi-plugins.js +57 -0
- package/dist/worker/console/routes.js +637 -8
- package/dist/worker/console/security.js +35 -0
- package/dist/worker/console/server.js +6 -3
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DwVJxUxz.js → abnfDiagram-N423BO3Z-DZZ8m3PO.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DqfCcXdu.js → arc-D6PvaVd-.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-qMVtggRv.js → architectureDiagram-T3A2C74G-B_OTOiI8.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-ECrV38dh.js → blockDiagram-VBNYF7ZC-Bv6rqHBg.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DBaaLqQv.js → c4Diagram-5PPSVZJV-B8eHr0oz.js} +1 -1
- package/dist/worker/console/static/assets/channel-BU5gOilw.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BvfNHkAz.js → chunk-2GRJ4B5K-DYwR0im2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-C5StiIbc.js → chunk-2Q5K7J3B-D2WPGqXt.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-HabGiJkS.js → chunk-5RXB4S5H-CQISJ_I7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BiTg127i.js → chunk-5VM5RSS4-C0o2Du1e.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-CZsXBPuR.js → chunk-6Q2QTUOP-4f8kr-U7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D-r1MxLK.js → chunk-GF5L2VYU-D_OWvXzX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DQxb9plr.js → chunk-JWPE2WC7-CasdPz5X.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DMndGvCT.js → chunk-KBJHAD2P-Cj8lRrla.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-ByL4BgOJ.js → chunk-RYQCIY6F-CUvD1FSd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BVe1b1gL.js → chunk-XXDRQBXY-CqLw_eqB.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BAyGzN8j.js → cose-bilkent-JH36ORCC-ku-WqJPx.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-C5yVYU7E.js → cynefin-VYW2F7L2-Bq_PYDhl.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-x-MwsEm9.js → cynefinDiagram-MW4NZA55-BiGZV641.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CT5Tle5F.js → dagre-VZM6K2ZE-C4JRlglF.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BARERhlS.js → diagram-7IWD3JNH-B0dNeWYG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BwfUz0VM.js → diagram-B4RE2ZJO-Cj35YvYM.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Mk4r2WvJ.js → diagram-LBJQPF4R-uzQoJ2-8.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-KUrxuBs0.js → diagram-Q27KOJAE-D1a-Buoz.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CjZOceZn.js → diagram-UB23O5K3-XjRrLRSs.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-rnwUCiEI.js → ebnfDiagram-BXEA7PRR-Da_O24cW.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DM4j23FM.js → erDiagram-JOGREHBK-BLJ8jrYU.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DD5HdDEs.js → flowDiagram-UKHOOZJN-B9GrLjM0.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CIAGSF47.js → ganttDiagram-PKOTCBZU-CeJ0TqiK.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DbwFbVEL.js → gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js} +1 -1
- package/dist/worker/console/static/assets/index-H9rFJiGL.css +1 -0
- package/dist/worker/console/static/assets/{index-Bih9w9h9.js → index-xwu9GxEc.js} +80 -80
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-B-Pb0T66.js → infoDiagram-6WML65LV-DB26i3d8.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B9Unk-YX.js → ishikawaDiagram-WSZJBQD7-BOGRyeZT.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-C2F3JkPt.js → journeyDiagram-NVQOT4AX-CZRoBO_6.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Bl8-Xcoz.js → kanban-definition-27J2QSJJ-qlWhJyoB.js} +1 -1
- package/dist/worker/console/static/assets/{linear-yK4uZmC5.js → linear-CfUiDDB3.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Bpa6PmuE.js → mermaid.core-BQe6fpqj.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BfGRoCSe.js → mindmap-definition-FAOFIHXS-zFzWHw64.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Bz6_HtST.js → pegDiagram-VL7TDLO6-DFGUqjZO.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-i0Umbsby.js → pieDiagram-7S7Q4E2Y-BB5i0l8Z.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DaVQBobe.js → quadrantDiagram-CIZ2JOQS-obbVZ_X5.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DMTGnSq7.js → railroadDiagram-AXF67PYL-BwdaNzc1.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-2ao_DaSq.js → requirementDiagram-LRYGKXZP-CkTJaugh.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-2SG1lTB9.js → sankeyDiagram-W5VNT64P-BwJ-hIgq.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BQYOLybR.js → sequenceDiagram-SI44F4Z6-BzSgmm7w.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-C8g3yjuI.js → sizeCapture-X5ZJPWSS-Bn3oj5Q1.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-q_F8nAQG.js → stateDiagram-OKZ733FA-dxhGtL8K.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-8LrWpH2s.js → swimlanes-SLNWSIFB-BLpEInya.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BQoY3qzl.js → timeline-definition-Z64GVDOM-ChJGYcXy.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BDu1UjSp.js → vennDiagram-T6HMQDX7-DK-qjRer.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-C48M7Y0a.js → wardleyDiagram-T6FBY63Y-3qeaWAg-.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YU6FTl_F.js → xychartDiagram-ELKLHX3M-CootlyP9.js} +1 -1
- package/dist/worker/console/static/index.html +7 -2
- package/dist/worker/console/static-src/app/useOperatorActions.js +3 -2
- package/dist/worker/console/static-src/shell/console-update-reload.js +25 -0
- package/dist/worker/console/workspace-context.js +28 -2
- package/dist/worker/console/workspace-registry.js +2 -2
- package/dist/worker/continuation/worker-continuation.js +278 -0
- package/dist/worker/observe/health.js +1 -1
- package/dist/worker/observe/node-transparency.js +572 -0
- package/dist/worker/observe/routes.js +51 -1
- package/dist/worker/observe/static/api.js +69 -5
- package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
- package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
- package/dist/worker/observe/static/state.js +3 -1
- package/dist/worker/observe/static/styles.css +1037 -126
- package/dist/worker/observe/static/views/dag-inspector.js +1215 -58
- package/dist/worker/observe/static/views/session-timeline.js +75 -10
- package/dist/worker/outcomes/adapters.js +19 -0
- package/dist/worker/outcomes/types.js +1 -0
- package/dist/worker/pool/attempt-lease.js +97 -66
- package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
- package/dist/worker/pool/reconcile.js +46 -1
- package/dist/worker/pool/run-store.js +2 -0
- package/dist/worker/pool/state-projection.js +2 -0
- package/dist/worker/task-spec/workflow-routing.js +8 -3
- package/dist/workflows/dag/artifact-bindings.js +149 -0
- package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
- package/dist/workflows/dag/budget-enforcement.js +10 -0
- package/dist/workflows/dag/context-receipt.js +305 -0
- package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
- package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
- package/dist/workflows/dag/context-transfer/renderers.js +93 -0
- package/dist/workflows/dag/frontend-writer-rollback.js +32 -0
- package/dist/workflows/dag/init-hybrid.js +146 -0
- package/dist/workflows/dag/node-execution.js +112 -27
- package/dist/workflows/dag/path-safety.js +1 -0
- package/dist/workflows/dag/prompt.js +29 -4
- package/dist/workflows/dag/rerun-plan.js +555 -1
- package/dist/workflows/dag/rerun-run.js +601 -15
- package/dist/workflows/dag/runner.js +20 -1
- package/dist/workflows/dag/skill-snapshot.js +17 -0
- package/dist/workflows/dag/types.js +183 -7
- package/docs/architecture/runtime-boundaries.md +6 -6
- package/docs/architecture/worker-and-feature.md +1 -1
- package/docs/templates/agent-dag.schema.json +32 -0
- package/docs/templates/product-line/task.yaml +1 -1
- package/package.json +3 -1
- package/skills/loop-agent/references/command-reference.md +6 -0
- package/dist/worker/console/static/assets/channel-CL1NRAmS.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-PBGzzjZA.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-PBGzzjZA.js +0 -1
- package/dist/worker/console/static/assets/index-BfleX-qj.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrxGBDJu.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BhpDxzAf.js +0 -8
- /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { chmod, mkdir, open, readdir, rm, writeFile, } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { assertSafeAppDataPath, ensureSecureDir, readJsonIfExists, writeSecureJson, } from "./app-data.js";
|
|
6
|
+
export const DEFAULT_CONTEXT_EXPORT_TTL_MS = 60 * 60 * 1000;
|
|
7
|
+
export const DEFAULT_CONTEXT_EXPORT_LEASE_TTL_MS = 5 * 60 * 1000;
|
|
8
|
+
export const CONTEXT_EXPORT_SCHEMA_VERSION = 1;
|
|
9
|
+
const exportWriteChains = new Map();
|
|
10
|
+
function serializeExport(exportId, operation) {
|
|
11
|
+
const previous = exportWriteChains.get(exportId) ?? Promise.resolve();
|
|
12
|
+
const next = previous.then(operation, operation);
|
|
13
|
+
exportWriteChains.set(exportId, next.then(() => undefined, () => undefined));
|
|
14
|
+
return next;
|
|
15
|
+
}
|
|
16
|
+
function exportRoot(appData) {
|
|
17
|
+
const root = appData.contextExports;
|
|
18
|
+
if (!root)
|
|
19
|
+
throw new Error("console context export root unavailable");
|
|
20
|
+
return assertSafeAppDataPath(appData.root, root);
|
|
21
|
+
}
|
|
22
|
+
function exportDir(appData, exportId) {
|
|
23
|
+
if (!/^[a-f0-9]{32}$/.test(exportId))
|
|
24
|
+
throw new Error("invalid context export id");
|
|
25
|
+
return assertSafeAppDataPath(exportRoot(appData), path.join(exportRoot(appData), exportId));
|
|
26
|
+
}
|
|
27
|
+
function metadataPath(appData, exportId) {
|
|
28
|
+
return path.join(exportDir(appData, exportId), "metadata.json");
|
|
29
|
+
}
|
|
30
|
+
export async function createContextExport(input) {
|
|
31
|
+
const now = input.now ?? new Date();
|
|
32
|
+
const ttlMs = input.ttlMs ?? DEFAULT_CONTEXT_EXPORT_TTL_MS;
|
|
33
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs < 1)
|
|
34
|
+
throw new Error("invalid context export TTL");
|
|
35
|
+
const exportId = randomBytes(16).toString("hex");
|
|
36
|
+
const dir = exportDir(input.appData, exportId);
|
|
37
|
+
ensureSecureDir(dir);
|
|
38
|
+
const payloadPath = assertSafeAppDataPath(dir, path.join(dir, "bundle.zip"));
|
|
39
|
+
await writeFile(payloadPath, input.bundle.zipBytes, {
|
|
40
|
+
flag: "wx",
|
|
41
|
+
mode: 0o600,
|
|
42
|
+
});
|
|
43
|
+
await chmod(payloadPath, 0o600).catch(() => undefined);
|
|
44
|
+
const record = {
|
|
45
|
+
schemaVersion: CONTEXT_EXPORT_SCHEMA_VERSION,
|
|
46
|
+
exportId,
|
|
47
|
+
repoFingerprint: input.appData.fingerprint,
|
|
48
|
+
bundleId: input.bundle.manifest.bundleId,
|
|
49
|
+
bundleSha256: input.bundle.manifest.bundleSha256,
|
|
50
|
+
profile: input.bundle.manifest.profile,
|
|
51
|
+
target: input.bundle.manifest.target,
|
|
52
|
+
state: "ready",
|
|
53
|
+
createdAt: now.toISOString(),
|
|
54
|
+
expiresAt: new Date(now.getTime() + ttlMs).toISOString(),
|
|
55
|
+
bytes: input.bundle.zipBytes.byteLength,
|
|
56
|
+
};
|
|
57
|
+
await writeSecureJson(metadataPath(input.appData, exportId), record);
|
|
58
|
+
return record;
|
|
59
|
+
}
|
|
60
|
+
export async function readContextExport(appData, exportId) {
|
|
61
|
+
return readJsonIfExists(metadataPath(appData, exportId));
|
|
62
|
+
}
|
|
63
|
+
export async function acquireContextExportLease(input) {
|
|
64
|
+
return serializeExport(input.exportId, async () => {
|
|
65
|
+
const now = input.now ?? new Date();
|
|
66
|
+
const leaseTtlMs = input.leaseTtlMs ?? DEFAULT_CONTEXT_EXPORT_LEASE_TTL_MS;
|
|
67
|
+
if (!Number.isSafeInteger(leaseTtlMs) || leaseTtlMs < 1) {
|
|
68
|
+
throw new Error("invalid context export lease TTL");
|
|
69
|
+
}
|
|
70
|
+
const record = await readContextExport(input.appData, input.exportId);
|
|
71
|
+
if (!record || record.repoFingerprint !== input.appData.fingerprint) {
|
|
72
|
+
throw new Error("context export not found");
|
|
73
|
+
}
|
|
74
|
+
const expiresAt = Date.parse(record.expiresAt);
|
|
75
|
+
if (record.state !== "ready" ||
|
|
76
|
+
!Number.isFinite(expiresAt) ||
|
|
77
|
+
expiresAt <= now.getTime()) {
|
|
78
|
+
throw new Error("context export expired");
|
|
79
|
+
}
|
|
80
|
+
if (record.lease) {
|
|
81
|
+
const acquiredAt = Date.parse(record.lease.acquiredAt);
|
|
82
|
+
if (Number.isFinite(acquiredAt) &&
|
|
83
|
+
acquiredAt + leaseTtlMs > now.getTime()) {
|
|
84
|
+
throw new Error("context export already leased");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const payloadPath = path.join(exportDir(input.appData, input.exportId), "bundle.zip");
|
|
88
|
+
const handle = await open(payloadPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
89
|
+
let bytes;
|
|
90
|
+
try {
|
|
91
|
+
const stat = await handle.stat();
|
|
92
|
+
if (!stat.isFile() || stat.nlink !== 1) {
|
|
93
|
+
throw new Error("context export payload is not an owned regular file");
|
|
94
|
+
}
|
|
95
|
+
bytes = await handle.readFile();
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
await handle.close();
|
|
99
|
+
}
|
|
100
|
+
const leaseId = randomBytes(16).toString("hex");
|
|
101
|
+
const leased = {
|
|
102
|
+
...record,
|
|
103
|
+
lease: { leaseId, acquiredAt: now.toISOString() },
|
|
104
|
+
};
|
|
105
|
+
await writeSecureJson(metadataPath(input.appData, input.exportId), leased);
|
|
106
|
+
return { record: leased, leaseId, bytes };
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
export async function releaseContextExportLease(input) {
|
|
110
|
+
await serializeExport(input.exportId, async () => {
|
|
111
|
+
const record = await readContextExport(input.appData, input.exportId);
|
|
112
|
+
if (!record?.lease || record.lease.leaseId !== input.leaseId)
|
|
113
|
+
return;
|
|
114
|
+
const { lease: _lease, ...released } = record;
|
|
115
|
+
await writeSecureJson(metadataPath(input.appData, input.exportId), released);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
export async function reconcileContextExports(input) {
|
|
119
|
+
const root = exportRoot(input.appData);
|
|
120
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
121
|
+
const now = input.now ?? new Date();
|
|
122
|
+
const leaseTtlMs = input.leaseTtlMs ?? DEFAULT_CONTEXT_EXPORT_LEASE_TTL_MS;
|
|
123
|
+
if (!Number.isSafeInteger(leaseTtlMs) || leaseTtlMs < 1) {
|
|
124
|
+
throw new Error("invalid context export lease TTL");
|
|
125
|
+
}
|
|
126
|
+
const removed = [];
|
|
127
|
+
const retainedLeased = [];
|
|
128
|
+
for (const name of await readdir(root)) {
|
|
129
|
+
if (!/^[a-f0-9]{32}$/.test(name))
|
|
130
|
+
continue;
|
|
131
|
+
const record = await readContextExport(input.appData, name).catch(() => undefined);
|
|
132
|
+
let reconciledRecord = record;
|
|
133
|
+
if (record?.lease) {
|
|
134
|
+
const acquiredAt = Date.parse(record.lease.acquiredAt);
|
|
135
|
+
if (Number.isFinite(acquiredAt) &&
|
|
136
|
+
acquiredAt + leaseTtlMs > now.getTime()) {
|
|
137
|
+
retainedLeased.push(name);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const { lease: _lease, ...released } = record;
|
|
141
|
+
reconciledRecord = released;
|
|
142
|
+
}
|
|
143
|
+
const expiresAt = reconciledRecord
|
|
144
|
+
? Date.parse(reconciledRecord.expiresAt)
|
|
145
|
+
: Number.NaN;
|
|
146
|
+
if (!reconciledRecord ||
|
|
147
|
+
!Number.isFinite(expiresAt) ||
|
|
148
|
+
expiresAt <= now.getTime()) {
|
|
149
|
+
await rm(exportDir(input.appData, name), {
|
|
150
|
+
recursive: true,
|
|
151
|
+
force: true,
|
|
152
|
+
});
|
|
153
|
+
removed.push(name);
|
|
154
|
+
}
|
|
155
|
+
else if (record?.lease) {
|
|
156
|
+
await writeSecureJson(metadataPath(input.appData, name), reconciledRecord);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { removed, retainedLeased };
|
|
160
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, readFile, rename, rm, utimes, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { ensureSecureDir } from "./app-data.js";
|
|
5
|
+
const OWNER_FILE = "owner";
|
|
6
|
+
const DEFAULT_STALE_MS = 30_000;
|
|
7
|
+
function delay(ms) {
|
|
8
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
+
}
|
|
10
|
+
function freshToken() {
|
|
11
|
+
return randomBytes(16).toString("hex");
|
|
12
|
+
}
|
|
13
|
+
function ownerPath(lockDir) {
|
|
14
|
+
return path.join(lockDir, OWNER_FILE);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A lock's freshness is the owner-file heartbeat, not the containing
|
|
18
|
+
* directory mtime. Creating or editing a file does not refresh the directory
|
|
19
|
+
* mtime, so using the latter could make a healthy long-running lock appear
|
|
20
|
+
* stale and permit a second holder into its critical section.
|
|
21
|
+
*/
|
|
22
|
+
async function lockTimestamp(lockDir) {
|
|
23
|
+
const ownerStat = await lstat(ownerPath(lockDir)).catch(() => undefined);
|
|
24
|
+
if (ownerStat?.isFile())
|
|
25
|
+
return ownerStat.mtimeMs;
|
|
26
|
+
return (await lstat(lockDir).catch(() => undefined))?.mtimeMs;
|
|
27
|
+
}
|
|
28
|
+
export async function isDirLockStale(lockDir, staleMs, nowMs = Date.now()) {
|
|
29
|
+
const timestamp = await lockTimestamp(lockDir);
|
|
30
|
+
return timestamp === undefined || nowMs - timestamp > staleMs;
|
|
31
|
+
}
|
|
32
|
+
async function refreshDirLock(lockDir, token) {
|
|
33
|
+
const owner = await readFile(ownerPath(lockDir), "utf-8").catch(() => undefined);
|
|
34
|
+
if (owner !== token)
|
|
35
|
+
return;
|
|
36
|
+
const now = new Date();
|
|
37
|
+
await utimes(ownerPath(lockDir), now, now).catch(() => { });
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Cross-process exclusive directory lock (mkdir-based). Ownership is
|
|
41
|
+
* token-guarded: each holder writes a unique owner file inside the lock
|
|
42
|
+
* directory and only ever removes a lock whose owner token matches its own.
|
|
43
|
+
*
|
|
44
|
+
* Release protocol (B2): verify owner AND freshness BEFORE touching the lock
|
|
45
|
+
* path. Takeover only ever targets a stale lock, so a holder that releases a
|
|
46
|
+
* lock it still owns (fresh) cannot have been taken over between the verify
|
|
47
|
+
* and the rename — it moves its own lock, never a successor's. A stale lock is
|
|
48
|
+
* deliberately left alone (a takeover may be in flight); only the post-rename
|
|
49
|
+
* tombstone check restores a lock moved by an unexpected race, never deletes a
|
|
50
|
+
* successor's live lock.
|
|
51
|
+
*/
|
|
52
|
+
export async function acquireDirLock(lockDir, options) {
|
|
53
|
+
const token = options?.token ?? freshToken();
|
|
54
|
+
const staleMs = options?.staleMs ?? DEFAULT_STALE_MS;
|
|
55
|
+
ensureSecureDir(path.dirname(lockDir));
|
|
56
|
+
for (;;) {
|
|
57
|
+
try {
|
|
58
|
+
await mkdir(lockDir);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const code = error?.code;
|
|
62
|
+
if (code !== "EEXIST")
|
|
63
|
+
throw error;
|
|
64
|
+
const stat = await lstat(lockDir).catch(() => undefined);
|
|
65
|
+
if (stat?.isDirectory() && await isDirLockStale(lockDir, staleMs)) {
|
|
66
|
+
const tombstone = `${lockDir}.${freshToken()}.tomb`;
|
|
67
|
+
try {
|
|
68
|
+
await rename(lockDir, tombstone);
|
|
69
|
+
await rm(tombstone, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// another process won the takeover; retry the loop
|
|
73
|
+
}
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
await delay(20);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
await writeFile(ownerPath(lockDir), token, {
|
|
81
|
+
flag: "wx",
|
|
82
|
+
mode: 0o600,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
const heartbeat = setInterval(() => { void refreshDirLock(lockDir, token); }, Math.max(10, Math.floor(staleMs / 3)));
|
|
92
|
+
heartbeat.unref();
|
|
93
|
+
let released = false;
|
|
94
|
+
const release = async () => {
|
|
95
|
+
if (released)
|
|
96
|
+
return;
|
|
97
|
+
released = true;
|
|
98
|
+
clearInterval(heartbeat);
|
|
99
|
+
// Verify the lock is still ours AND fresh before renaming it away. If the
|
|
100
|
+
// owner no longer matches, a successor holds the lock — never touch it.
|
|
101
|
+
const stat = await lstat(lockDir).catch(() => undefined);
|
|
102
|
+
if (!stat?.isDirectory())
|
|
103
|
+
return;
|
|
104
|
+
if (await isDirLockStale(lockDir, staleMs)) {
|
|
105
|
+
// Our lock went stale: a takeover may be in flight or imminent. Leave
|
|
106
|
+
// it alone — removing it now could delete a successor's live lock.
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const owner = await readFile(ownerPath(lockDir), "utf-8").catch(() => undefined);
|
|
110
|
+
if (owner !== token)
|
|
111
|
+
return;
|
|
112
|
+
const tombstone = `${lockDir}.${token}.release`;
|
|
113
|
+
try {
|
|
114
|
+
await rename(lockDir, tombstone);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// lock path already gone (a takeover removed it); nothing of ours.
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const movedOwner = await readFile(path.join(tombstone, OWNER_FILE), "utf-8").catch(() => undefined);
|
|
121
|
+
if (movedOwner !== token) {
|
|
122
|
+
// Raced with a takeover in the microsecond window between the fresh
|
|
123
|
+
// verify and the rename: restore the moved lock if the path is still
|
|
124
|
+
// free; otherwise leave the tombstone for reconcile — never delete a
|
|
125
|
+
// successor's live lock.
|
|
126
|
+
await rename(tombstone, lockDir).catch(() => { });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
await rm(tombstone, { recursive: true, force: true }).catch(() => { });
|
|
130
|
+
};
|
|
131
|
+
return { token, release };
|
|
132
|
+
}
|
|
@@ -139,6 +139,21 @@ export class OperationStore {
|
|
|
139
139
|
for (const op of await this.list()) {
|
|
140
140
|
if (TERMINAL_OPERATION_STATES.has(op.state))
|
|
141
141
|
continue;
|
|
142
|
+
// A consoleUpdateAndInit whose record already proves a requested
|
|
143
|
+
// handoff died before writing its terminal state: the update and
|
|
144
|
+
// restart themselves succeeded — the booting process IS the
|
|
145
|
+
// handoff target. Mark it succeeded instead of surfacing the
|
|
146
|
+
// generic needs-reconcile failure copy to the operator.
|
|
147
|
+
if (op.action === "consoleUpdateAndInit" &&
|
|
148
|
+
op.state === "needs-reconcile" &&
|
|
149
|
+
isProvenConsoleUpdateHandoff(op.result)) {
|
|
150
|
+
changed.push(await this.update(op.operationId, {
|
|
151
|
+
state: "succeeded",
|
|
152
|
+
errorCode: undefined,
|
|
153
|
+
errorMessage: undefined,
|
|
154
|
+
}));
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
142
157
|
if (op.state === "needs-reconcile")
|
|
143
158
|
continue;
|
|
144
159
|
let next;
|
|
@@ -162,6 +177,19 @@ export class OperationStore {
|
|
|
162
177
|
export function isTerminalOperationState(state) {
|
|
163
178
|
return TERMINAL_OPERATION_STATES.has(state);
|
|
164
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* True when a consoleUpdateAndInit result proves the handoff was requested
|
|
182
|
+
* (install verified + init clean + new Console spawned). Used by boot
|
|
183
|
+
* reconcile to close the record as succeeded even though the old process
|
|
184
|
+
* died before writing its terminal state.
|
|
185
|
+
*/
|
|
186
|
+
function isProvenConsoleUpdateHandoff(result) {
|
|
187
|
+
if (!result || typeof result !== "object")
|
|
188
|
+
return false;
|
|
189
|
+
const shape = result;
|
|
190
|
+
return (shape.state === "restarting" &&
|
|
191
|
+
shape.maintenance?.handoff?.status === "requested");
|
|
192
|
+
}
|
|
165
193
|
export function clipPreview(text, max = 4096) {
|
|
166
194
|
if (text.length <= max)
|
|
167
195
|
return text;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { writeJsonAtomic, writeTextAtomic, } from "./atomic-write.js";
|
|
3
|
+
import { writeJsonAtomic, writeBufferAtomic, writeTextAtomic, } from "./atomic-write.js";
|
|
4
4
|
function resolveRepoRootFromDagRunDir(runDir) {
|
|
5
5
|
const normalized = path.resolve(runDir);
|
|
6
6
|
const lifecycleDir = path.dirname(normalized);
|
|
@@ -26,12 +26,16 @@ function dagWriteOptions(runDir, options) {
|
|
|
26
26
|
return {
|
|
27
27
|
repoRoot: options?.repoRoot ?? resolveRepoRootFromDagRunDir(runDir),
|
|
28
28
|
allowCompletedFactsWrite: options?.allowCompletedFactsWrite,
|
|
29
|
+
mode: options?.mode,
|
|
30
|
+
directoryMode: options?.directoryMode,
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
function pathWriteOptions(targetPath, options) {
|
|
32
34
|
return {
|
|
33
35
|
repoRoot: options?.repoRoot ?? resolveRepoRootFromHarnessPath(targetPath),
|
|
34
36
|
allowCompletedFactsWrite: options?.allowCompletedFactsWrite,
|
|
37
|
+
mode: options?.mode,
|
|
38
|
+
directoryMode: options?.directoryMode,
|
|
35
39
|
};
|
|
36
40
|
}
|
|
37
41
|
function completedRelocationWriteOptions(runDir) {
|
|
@@ -48,6 +52,11 @@ export async function writeDagNodeTextArtifact(runDir, nodeId, fileName, content
|
|
|
48
52
|
await writeTextAtomic(targetPath, content, dagWriteOptions(runDir, options));
|
|
49
53
|
return targetPath;
|
|
50
54
|
}
|
|
55
|
+
export async function writeDagNodeBufferArtifact(runDir, nodeId, fileName, bytes, options) {
|
|
56
|
+
const targetPath = path.join(runDir, nodeId, fileName);
|
|
57
|
+
await writeBufferAtomic(targetPath, bytes, dagWriteOptions(runDir, options));
|
|
58
|
+
return targetPath;
|
|
59
|
+
}
|
|
51
60
|
export async function writeDagNodeJsonArtifact(runDir, nodeId, fileName, value, options) {
|
|
52
61
|
const targetPath = path.join(runDir, nodeId, fileName);
|
|
53
62
|
await writeJsonAtomic(targetPath, value, dagWriteOptions(runDir, options));
|
|
@@ -30,10 +30,17 @@ async function writeAtomic(targetPath, content, options) {
|
|
|
30
30
|
const resolved = options?.repoRoot && !path.isAbsolute(targetPath)
|
|
31
31
|
? path.resolve(options.repoRoot, targetPath)
|
|
32
32
|
: path.resolve(targetPath);
|
|
33
|
-
await mkdir(path.dirname(resolved), {
|
|
33
|
+
await mkdir(path.dirname(resolved), {
|
|
34
|
+
recursive: true,
|
|
35
|
+
...(options?.directoryMode !== undefined
|
|
36
|
+
? { mode: options.directoryMode }
|
|
37
|
+
: {}),
|
|
38
|
+
});
|
|
34
39
|
const tempPath = `${resolved}.${process.pid}.${(atomicWriteSequence += 1)}.tmp`;
|
|
35
40
|
try {
|
|
36
|
-
await writeFile(tempPath, content,
|
|
41
|
+
await writeFile(tempPath, content, {
|
|
42
|
+
...(options?.mode !== undefined ? { mode: options.mode } : {}),
|
|
43
|
+
});
|
|
37
44
|
try {
|
|
38
45
|
await rename(tempPath, resolved);
|
|
39
46
|
}
|
|
@@ -54,6 +61,9 @@ export async function writeJsonAtomic(targetPath, value, options) {
|
|
|
54
61
|
export async function writeTextAtomic(targetPath, text, options) {
|
|
55
62
|
await writeAtomic(targetPath, text, options);
|
|
56
63
|
}
|
|
64
|
+
export async function writeBufferAtomic(targetPath, bytes, options) {
|
|
65
|
+
await writeAtomic(targetPath, bytes, options);
|
|
66
|
+
}
|
|
57
67
|
export function serializeJsonl(entries) {
|
|
58
68
|
if (entries.length === 0)
|
|
59
69
|
return "";
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { artifactRevisionV1Schema } from "../context-transfer.js";
|
|
3
|
+
import { redactSecretsWithFindings } from "../preview.js";
|
|
4
|
+
export const ARTIFACT_REVISION_LIMITS = Object.freeze({
|
|
5
|
+
singleFileBytes: 10 * 1024 * 1024,
|
|
6
|
+
diffPreviewBytes: 64 * 1024,
|
|
7
|
+
});
|
|
8
|
+
function sha256(bytes) {
|
|
9
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
10
|
+
}
|
|
11
|
+
function isTextMedia(mediaType) {
|
|
12
|
+
return mediaType.startsWith("text/") || mediaType.includes("json") || mediaType.includes("xml");
|
|
13
|
+
}
|
|
14
|
+
function stableJson(bytes) {
|
|
15
|
+
try {
|
|
16
|
+
const value = JSON.parse(bytes.toString("utf8"));
|
|
17
|
+
const sort = (input) => {
|
|
18
|
+
if (Array.isArray(input))
|
|
19
|
+
return input.map(sort);
|
|
20
|
+
if (input && typeof input === "object") {
|
|
21
|
+
return Object.fromEntries(Object.entries(input)
|
|
22
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
23
|
+
.map(([key, child]) => [key, sort(child)]));
|
|
24
|
+
}
|
|
25
|
+
return input;
|
|
26
|
+
};
|
|
27
|
+
return `${JSON.stringify(sort(value), null, 2)}\n`;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function boundedLineDiff(before, after) {
|
|
34
|
+
const beforeLines = before.split(/\r?\n/);
|
|
35
|
+
const afterLines = after.split(/\r?\n/);
|
|
36
|
+
const rows = [];
|
|
37
|
+
const limit = Math.max(beforeLines.length, afterLines.length);
|
|
38
|
+
for (let index = 0; index < limit; index += 1) {
|
|
39
|
+
if (beforeLines[index] === afterLines[index])
|
|
40
|
+
continue;
|
|
41
|
+
if (beforeLines[index] !== undefined)
|
|
42
|
+
rows.push(`- ${beforeLines[index]}`);
|
|
43
|
+
if (afterLines[index] !== undefined)
|
|
44
|
+
rows.push(`+ ${afterLines[index]}`);
|
|
45
|
+
}
|
|
46
|
+
const complete = rows.join("\n");
|
|
47
|
+
const preview = Buffer.from(complete, "utf8");
|
|
48
|
+
if (preview.byteLength <= ARTIFACT_REVISION_LIMITS.diffPreviewBytes) {
|
|
49
|
+
return { summary: complete, truncated: false };
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
summary: preview.subarray(0, ARTIFACT_REVISION_LIMITS.diffPreviewBytes).toString("utf8") + "\n[diff truncated]",
|
|
53
|
+
truncated: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function validateAndDescribeArtifactRevision(input) {
|
|
57
|
+
if (input.declaration.revisionPolicy !== "replace-file") {
|
|
58
|
+
throw new Error("ARTIFACT_NOT_REVISION_ELIGIBLE");
|
|
59
|
+
}
|
|
60
|
+
if (input.candidateBytes.byteLength > ARTIFACT_REVISION_LIMITS.singleFileBytes) {
|
|
61
|
+
throw new Error("ARTIFACT_REVISION_SCHEMA_INVALID: candidate exceeds size limit");
|
|
62
|
+
}
|
|
63
|
+
if (sha256(input.parentBytes) !== input.parentSha256) {
|
|
64
|
+
throw new Error("ARTIFACT_REVISION_DRIFT: parent artifact hash mismatch");
|
|
65
|
+
}
|
|
66
|
+
if (input.mediaType !== input.declaration.mediaType) {
|
|
67
|
+
throw new Error("ARTIFACT_REVISION_SCHEMA_INVALID: media type mismatch");
|
|
68
|
+
}
|
|
69
|
+
const textCandidate = isTextMedia(input.mediaType);
|
|
70
|
+
if (textCandidate &&
|
|
71
|
+
redactSecretsWithFindings(input.candidateBytes.toString("utf8"))
|
|
72
|
+
.findingCount > 0) {
|
|
73
|
+
throw new Error("SECRET_SCAN_CONFIRMATION_REQUIRED");
|
|
74
|
+
}
|
|
75
|
+
const candidateSha256 = sha256(input.candidateBytes);
|
|
76
|
+
if (candidateSha256 === input.parentSha256) {
|
|
77
|
+
throw new Error("ARTIFACT_REVISION_SCHEMA_INVALID: replacement is unchanged");
|
|
78
|
+
}
|
|
79
|
+
let kind = "binary";
|
|
80
|
+
let summary = `binary ${input.parentBytes.byteLength} -> ${input.candidateBytes.byteLength} bytes; ${input.parentSha256} -> ${candidateSha256}`;
|
|
81
|
+
let truncated = false;
|
|
82
|
+
let diffPreview;
|
|
83
|
+
const schemaChecks = [];
|
|
84
|
+
if (input.declaration.schemaId || input.mediaType.includes("json")) {
|
|
85
|
+
const before = stableJson(input.parentBytes);
|
|
86
|
+
const after = stableJson(input.candidateBytes);
|
|
87
|
+
if (!before || !after) {
|
|
88
|
+
throw new Error("ARTIFACT_REVISION_SCHEMA_INVALID: expected valid JSON");
|
|
89
|
+
}
|
|
90
|
+
kind = "json";
|
|
91
|
+
const declaredSchemaId = input.declaration.schemaId;
|
|
92
|
+
if (declaredSchemaId) {
|
|
93
|
+
const validator = input.artifactSchemaResolver?.(declaredSchemaId);
|
|
94
|
+
if (!validator) {
|
|
95
|
+
// Fail closed (W4.3): a declared schema without a registered
|
|
96
|
+
// validator must not pass as "JSON syntax only" — revision
|
|
97
|
+
// eligibility would silently outrun actual verification.
|
|
98
|
+
throw new Error(`ARTIFACT_REVISION_SCHEMA_INVALID: no registered revision validator for ${declaredSchemaId}; artifact cannot be staged until its schema is registered`);
|
|
99
|
+
}
|
|
100
|
+
const parsed = JSON.parse(after);
|
|
101
|
+
const result = validator.safeParse(parsed);
|
|
102
|
+
if (!result.success) {
|
|
103
|
+
const issue = result.error.issues[0];
|
|
104
|
+
const issuePath = issue
|
|
105
|
+
? issue.path.map((part) => String(part)).join(".")
|
|
106
|
+
: "(root)";
|
|
107
|
+
throw new Error(`ARTIFACT_REVISION_SCHEMA_INVALID: candidate does not satisfy ${declaredSchemaId} at ${issuePath}: ${issue?.message ?? "schema violation"}`);
|
|
108
|
+
}
|
|
109
|
+
schemaChecks.push({
|
|
110
|
+
id: "artifact-schema",
|
|
111
|
+
status: "pass",
|
|
112
|
+
detail: declaredSchemaId,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const diff = boundedLineDiff(before, after);
|
|
116
|
+
summary = diff.summary;
|
|
117
|
+
truncated = diff.truncated;
|
|
118
|
+
diffPreview = Buffer.from(diff.summary, "utf8");
|
|
119
|
+
}
|
|
120
|
+
else if (isTextMedia(input.mediaType)) {
|
|
121
|
+
kind = "text";
|
|
122
|
+
const diff = boundedLineDiff(input.parentBytes.toString("utf8"), input.candidateBytes.toString("utf8"));
|
|
123
|
+
summary = diff.summary;
|
|
124
|
+
truncated = diff.truncated;
|
|
125
|
+
diffPreview = Buffer.from(diff.summary, "utf8");
|
|
126
|
+
}
|
|
127
|
+
const revision = artifactRevisionV1Schema.parse({
|
|
128
|
+
schemaVersion: 1,
|
|
129
|
+
revisionId: input.revisionId,
|
|
130
|
+
state: "validated",
|
|
131
|
+
parent: {
|
|
132
|
+
runId: input.parentRunId,
|
|
133
|
+
nodeId: input.producerNodeId,
|
|
134
|
+
artifactPath: input.declaration.path,
|
|
135
|
+
artifactSha256: input.parentSha256,
|
|
136
|
+
...(input.declaration.schemaId ? { artifactSchemaId: input.declaration.schemaId } : {}),
|
|
137
|
+
},
|
|
138
|
+
candidate: {
|
|
139
|
+
stagingPath: input.stagingPath,
|
|
140
|
+
sha256: candidateSha256,
|
|
141
|
+
bytes: input.candidateBytes.byteLength,
|
|
142
|
+
mediaType: input.mediaType,
|
|
143
|
+
uploadedAt: input.uploadedAt,
|
|
144
|
+
uploadedBy: "operator",
|
|
145
|
+
},
|
|
146
|
+
validation: {
|
|
147
|
+
status: "pass",
|
|
148
|
+
checks: [
|
|
149
|
+
{ id: "parent-hash", status: "pass" },
|
|
150
|
+
{ id: "declared-path", status: "pass" },
|
|
151
|
+
{ id: "media-type", status: "pass" },
|
|
152
|
+
textCandidate
|
|
153
|
+
? { id: "secret-scan", status: "pass" }
|
|
154
|
+
: {
|
|
155
|
+
id: "secret-scan",
|
|
156
|
+
status: "warning",
|
|
157
|
+
detail: "binary candidate was not decoded for text secret scanning",
|
|
158
|
+
},
|
|
159
|
+
...(kind === "json" ? [{ id: "json-syntax", status: "pass" }] : []),
|
|
160
|
+
...schemaChecks,
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
diff: {
|
|
164
|
+
kind,
|
|
165
|
+
summary,
|
|
166
|
+
...(diffPreview ? { previewPath: `artifact-revisions/${input.revisionId}/diff.txt` } : {}),
|
|
167
|
+
truncated,
|
|
168
|
+
},
|
|
169
|
+
expiresAt: input.expiresAt,
|
|
170
|
+
});
|
|
171
|
+
return { revision, ...(diffPreview ? { diffPreview } : {}) };
|
|
172
|
+
}
|