harnery 0.44.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commander.js +1 -0
- package/dist/commands/files.d.ts +2 -2
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +7 -4
- package/dist/commands/servers.d.ts.map +1 -1
- package/dist/commands/servers.js +11 -17
- package/dist/commands/tunnel.d.ts +15 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +453 -153
- package/dist/commands/turn-rollback.d.ts +5 -0
- package/dist/commands/turn-rollback.d.ts.map +1 -0
- package/dist/commands/turn-rollback.js +76 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +44 -0
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +18 -0
- package/dist/core/agents/state/proc-start.d.ts +5 -6
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +13 -9
- package/dist/core/agents/state/process-handle.d.ts +26 -0
- package/dist/core/agents/state/process-handle.d.ts.map +1 -0
- package/dist/core/agents/state/process-handle.js +121 -0
- package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
- package/dist/core/artifacts/delivery-card.js +2 -1
- package/dist/core/resources/service.d.ts +2 -16
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +1 -78
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +2 -9
- package/dist/core/servers/index.d.ts +12 -11
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +194 -81
- package/dist/core/servers/tunnels.d.ts +5 -3
- package/dist/core/servers/tunnels.d.ts.map +1 -1
- package/dist/core/servers/tunnels.js +45 -2
- package/dist/core/supervisor/service.d.ts +2 -4
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/turn-rollback/content.d.ts +53 -0
- package/dist/core/turn-rollback/content.d.ts.map +1 -0
- package/dist/core/turn-rollback/content.js +396 -0
- package/dist/core/turn-rollback/index.d.ts +72 -0
- package/dist/core/turn-rollback/index.d.ts.map +1 -0
- package/dist/core/turn-rollback/index.js +443 -0
- package/dist/core/workflow/index.d.ts +4 -2
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +2 -1
- package/dist/core/workflow/recovery-audit.d.ts +30 -0
- package/dist/core/workflow/recovery-audit.d.ts.map +1 -0
- package/dist/core/workflow/recovery-audit.js +116 -0
- package/dist/core/workflow/run-state.d.ts +19 -2
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +113 -8
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +70 -14
- package/dist/core/workflow/workspaces/reclaim.d.ts +6 -1
- package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.d.ts +2 -0
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +24 -0
- package/dist/lib/tunnel/state.d.ts +13 -6
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +58 -12
- package/package.json +6 -1
- package/src/commander.ts +10 -0
- package/src/commands/files.ts +13 -5
- package/src/commands/servers.ts +12 -23
- package/src/commands/tunnel.ts +533 -166
- package/src/commands/turn-rollback.ts +111 -0
- package/src/commands/workflow.ts +59 -0
- package/src/core/agents/coord-client.ts +20 -0
- package/src/core/agents/state/proc-start.ts +14 -11
- package/src/core/agents/state/process-handle.ts +165 -0
- package/src/core/artifacts/delivery-card.ts +7 -1
- package/src/core/resources/service.ts +3 -88
- package/src/core/semantic/service.ts +2 -8
- package/src/core/servers/index.ts +234 -85
- package/src/core/servers/tunnels.ts +55 -3
- package/src/core/supervisor/service.ts +2 -3
- package/src/core/turn-rollback/content.ts +499 -0
- package/src/core/turn-rollback/index.ts +580 -0
- package/src/core/workflow/index.ts +12 -1
- package/src/core/workflow/recovery-audit.ts +181 -0
- package/src/core/workflow/run-state.ts +137 -8
- package/src/core/workflow/workspaces/local-git.ts +86 -15
- package/src/core/workflow/workspaces/reclaim.ts +4 -0
- package/src/core/workflow/workspaces/recovery.ts +23 -0
- package/src/lib/tunnel/state.ts +70 -15
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { RepoSnapshot } from "../context/index.js";
|
|
2
2
|
import { type NormalizedPolicy, type PolicyIsolation, type PolicyNetworkAccess } from "../policy/index.js";
|
|
3
|
+
import { type WorkflowRecoveryAuthorization } from "./recovery-audit.js";
|
|
3
4
|
import type { WorkflowAttemptContext, WorkflowDiagnosticAdmissionConfig, WorkflowSpecialistProfile, WorkflowWorkContext } from "./types.js";
|
|
4
5
|
import type { WorkspaceBinding, WorkspaceCompatibilityExecutionEvidence } from "./workspaces/index.js";
|
|
5
6
|
export declare const WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION: 2;
|
|
@@ -48,7 +49,23 @@ export declare function assertWorkflowRunResumable(coordRoot: string, runId: str
|
|
|
48
49
|
export declare function assertWorkflowScriptUnchanged(manifest: WorkflowRunManifest): void;
|
|
49
50
|
export declare function workflowRunManifestPath(coordRoot: string, runId: string): string;
|
|
50
51
|
/** Acquire an exclusive lease shared by resume and reclaim. Existing locks
|
|
51
|
-
* always refuse
|
|
52
|
-
* Crash recovery requires an audited ownership check outside this operation. */
|
|
52
|
+
* always refuse; explicit audited recovery is a separate operation. */
|
|
53
53
|
export declare function acquireWorkflowResumeLease(coordRoot: string, runId: string): () => void;
|
|
54
|
+
export interface WorkflowResumeRecoveryOptions extends Partial<WorkflowRecoveryAuthorization> {
|
|
55
|
+
yes?: boolean;
|
|
56
|
+
lockSha256?: string;
|
|
57
|
+
}
|
|
58
|
+
export interface WorkflowResumeRecoveryResult {
|
|
59
|
+
action: "preview" | "unlocked";
|
|
60
|
+
run_id: string;
|
|
61
|
+
lock_sha256: string | null;
|
|
62
|
+
owner_state: "dead" | "live" | "unknown" | "absent";
|
|
63
|
+
owner: {
|
|
64
|
+
pid: number;
|
|
65
|
+
host: string;
|
|
66
|
+
created_at: string;
|
|
67
|
+
} | null;
|
|
68
|
+
recovery_id?: string;
|
|
69
|
+
}
|
|
70
|
+
export declare function recoverWorkflowResumeLease(coordRoot: string, runId: string, options: WorkflowResumeRecoveryOptions): WorkflowResumeRecoveryResult;
|
|
54
71
|
//# sourceMappingURL=run-state.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-state.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/run-state.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"run-state.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/run-state.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { chmodSync, closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { hostname } from "node:os";
|
|
4
4
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { normalizePolicy, policyDigest, } from "../policy/index.js";
|
|
6
6
|
import { stateFileMode } from "../storage/modes.js";
|
|
7
7
|
import { assertWorkflowRunId, readWorkflowApproval } from "./approvals.js";
|
|
8
8
|
import { isCanonicalWorkflowAttemptContext } from "./attempt-context.js";
|
|
9
|
-
import { readJsonRecord, writeImmutableJson } from "./durable-record.js";
|
|
9
|
+
import { fsyncParentDirectory, readJsonRecord, stableDigest, writeImmutableJson, } from "./durable-record.js";
|
|
10
|
+
import { beginWorkflowRecovery } from "./recovery-audit.js";
|
|
10
11
|
import { normalizeWorkflowSpecialists } from "./specialists.js";
|
|
11
12
|
import { WORKFLOW_DIAGNOSTIC_ADMISSION_SCHEMA_VERSION } from "./types.js";
|
|
12
13
|
import { isCanonicalWorkflowWorkContext } from "./work-context.js";
|
|
14
|
+
import { acquireNoClobberLease } from "./workspaces/leases.js";
|
|
13
15
|
import { assertWorkspaceManifestAuthority, isWorkspaceBinding, isWorkspaceCompatibilityExecutionEvidence, } from "./workspaces/validate.js";
|
|
14
16
|
export const WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION = 2;
|
|
15
17
|
const MANIFEST_LIMIT_BYTES = 256 * 1024;
|
|
@@ -128,8 +130,7 @@ export function workflowRunManifestPath(coordRoot, runId) {
|
|
|
128
130
|
return join(coordRoot, ".harnery", "workflows", runId, "run.json");
|
|
129
131
|
}
|
|
130
132
|
/** Acquire an exclusive lease shared by resume and reclaim. Existing locks
|
|
131
|
-
* always refuse
|
|
132
|
-
* Crash recovery requires an audited ownership check outside this operation. */
|
|
133
|
+
* always refuse; explicit audited recovery is a separate operation. */
|
|
133
134
|
export function acquireWorkflowResumeLease(coordRoot, runId) {
|
|
134
135
|
assertWorkflowRunId(runId);
|
|
135
136
|
const path = join(coordRoot, ".harnery", "workflows", runId, "resume.lock");
|
|
@@ -146,12 +147,15 @@ export function acquireWorkflowResumeLease(coordRoot, runId) {
|
|
|
146
147
|
const fd = openSync(path, "wx", stateFileMode());
|
|
147
148
|
try {
|
|
148
149
|
writeFileSync(fd, ownerBytes, "utf8");
|
|
150
|
+
fsyncSync(fd);
|
|
149
151
|
}
|
|
150
152
|
finally {
|
|
151
153
|
closeSync(fd);
|
|
152
154
|
}
|
|
153
155
|
chmodSync(path, stateFileMode());
|
|
154
|
-
|
|
156
|
+
fsyncParentDirectory(path);
|
|
157
|
+
const identity = lstatSync(path);
|
|
158
|
+
return { device: identity.dev, inode: identity.ino };
|
|
155
159
|
}
|
|
156
160
|
catch (error) {
|
|
157
161
|
if (error.code !== "EEXIST")
|
|
@@ -159,7 +163,8 @@ export function acquireWorkflowResumeLease(coordRoot, runId) {
|
|
|
159
163
|
return false;
|
|
160
164
|
}
|
|
161
165
|
};
|
|
162
|
-
|
|
166
|
+
const acquired = withResumeLeaseMutation(coordRoot, runId, acquire);
|
|
167
|
+
if (!acquired) {
|
|
163
168
|
const existing = readResumeLease(path);
|
|
164
169
|
const holder = existing
|
|
165
170
|
? ` by pid ${existing.pid} on ${existing.host}; its liveness has not been verified`
|
|
@@ -168,14 +173,113 @@ export function acquireWorkflowResumeLease(coordRoot, runId) {
|
|
|
168
173
|
}
|
|
169
174
|
return () => {
|
|
170
175
|
try {
|
|
171
|
-
|
|
176
|
+
// Only this live process can release this nonce. Acquisition never
|
|
177
|
+
// replaces a present lock, and unlock requires ESRCH for its owner, so
|
|
178
|
+
// neither supported writer can replace this lock during synchronous
|
|
179
|
+
// release. Taking their mutation gate would strand a live owner's lock
|
|
180
|
+
// on ordinary contention. Pin both identity and bytes for stale closures.
|
|
181
|
+
const identity = lstatSync(path);
|
|
182
|
+
if (identity.isFile() &&
|
|
183
|
+
identity.dev === acquired.device &&
|
|
184
|
+
identity.ino === acquired.inode &&
|
|
185
|
+
readFileSync(path, "utf8") === ownerBytes) {
|
|
172
186
|
unlinkSync(path);
|
|
187
|
+
fsyncParentDirectory(path);
|
|
188
|
+
}
|
|
173
189
|
}
|
|
174
190
|
catch {
|
|
175
191
|
// Leave an unreadable or replaced lock untouched.
|
|
176
192
|
}
|
|
177
193
|
};
|
|
178
194
|
}
|
|
195
|
+
export function recoverWorkflowResumeLease(coordRoot, runId, options) {
|
|
196
|
+
assertWorkflowRunId(runId);
|
|
197
|
+
const path = join(coordRoot, ".harnery", "workflows", runId, "resume.lock");
|
|
198
|
+
const inspect = () => {
|
|
199
|
+
let lockSha256;
|
|
200
|
+
try {
|
|
201
|
+
const stat = lstatSync(path);
|
|
202
|
+
if (!stat.isFile() || stat.size > 8 * 1024)
|
|
203
|
+
throw new Error("resume lock is not a bounded regular file");
|
|
204
|
+
lockSha256 = stableDigest({
|
|
205
|
+
bytes: readFileSync(path, "utf8"),
|
|
206
|
+
device: stat.dev,
|
|
207
|
+
inode: stat.ino,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
if (error.code !== "ENOENT")
|
|
212
|
+
throw error;
|
|
213
|
+
return {
|
|
214
|
+
action: "preview",
|
|
215
|
+
run_id: runId,
|
|
216
|
+
lock_sha256: null,
|
|
217
|
+
owner_state: "absent",
|
|
218
|
+
owner: null,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const owner = readResumeLease(path);
|
|
222
|
+
let ownerState = "unknown";
|
|
223
|
+
if (owner?.host === hostname()) {
|
|
224
|
+
try {
|
|
225
|
+
process.kill(owner.pid, 0);
|
|
226
|
+
ownerState = "live";
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
if (error.code === "ESRCH")
|
|
230
|
+
ownerState = "dead";
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
action: "preview",
|
|
235
|
+
run_id: runId,
|
|
236
|
+
lock_sha256: lockSha256,
|
|
237
|
+
owner_state: ownerState,
|
|
238
|
+
owner,
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
if (!options.yes)
|
|
242
|
+
return inspect();
|
|
243
|
+
return withResumeLeaseMutation(coordRoot, runId, () => {
|
|
244
|
+
const observed = inspect();
|
|
245
|
+
if (observed.owner_state !== "dead")
|
|
246
|
+
throw new Error(`workflow resume unlock refused: owner is ${observed.owner_state}`);
|
|
247
|
+
if (!options.lockSha256 || options.lockSha256 !== observed.lock_sha256)
|
|
248
|
+
throw new Error("workflow resume unlock requires the exact current preview digest");
|
|
249
|
+
const audit = beginWorkflowRecovery(coordRoot, {
|
|
250
|
+
run_id: runId,
|
|
251
|
+
action: "unlock_resume",
|
|
252
|
+
target_sha256: options.lockSha256,
|
|
253
|
+
target: { lock: "resume.lock", owner: observed.owner },
|
|
254
|
+
actor: options.actor,
|
|
255
|
+
reason: options.reason,
|
|
256
|
+
});
|
|
257
|
+
const current = inspect();
|
|
258
|
+
if (current.lock_sha256 !== observed.lock_sha256 || current.owner_state !== "dead")
|
|
259
|
+
throw new Error("workflow resume lock authority changed before unlock");
|
|
260
|
+
audit.assertCurrent();
|
|
261
|
+
unlinkSync(path);
|
|
262
|
+
fsyncParentDirectory(path);
|
|
263
|
+
audit.complete();
|
|
264
|
+
return { ...observed, action: "unlocked", recovery_id: audit.intent.operation_id };
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
function withResumeLeaseMutation(coordRoot, runId, action) {
|
|
268
|
+
const authority = stableDigest({ run_id: runId, lock: "resume.lock" });
|
|
269
|
+
const lease = acquireNoClobberLease({
|
|
270
|
+
path: join(coordRoot, ".harnery", "workflows", runId, "resume-mutation.lease"),
|
|
271
|
+
scope: "workflow-resume-mutation",
|
|
272
|
+
authoritySha256: authority,
|
|
273
|
+
staleAfterMs: 300_000,
|
|
274
|
+
validateStaleOwner: (owner) => owner.authority_sha256 === authority,
|
|
275
|
+
});
|
|
276
|
+
try {
|
|
277
|
+
return action();
|
|
278
|
+
}
|
|
279
|
+
finally {
|
|
280
|
+
lease.release();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
179
283
|
function readResumeLease(path) {
|
|
180
284
|
try {
|
|
181
285
|
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
@@ -183,7 +287,8 @@ function readResumeLease(path) {
|
|
|
183
287
|
Number.isSafeInteger(value.pid) &&
|
|
184
288
|
value.pid > 0 &&
|
|
185
289
|
typeof value.host === "string" &&
|
|
186
|
-
|
|
290
|
+
value.host.length > 0 &&
|
|
291
|
+
validTimestamp(value.created_at)
|
|
187
292
|
? { pid: value.pid, host: value.host, created_at: value.created_at }
|
|
188
293
|
: null;
|
|
189
294
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-git.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/local-git.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"local-git.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/local-git.ts"],"names":[],"mappings":"AA8CA,OAAO,EAML,KAAK,uBAAuB,EAE5B,KAAK,sBAAsB,EAC5B,MAAM,cAAc,CAAC;AAiBtB,OAAO,KAAK,EAYV,gBAAgB,EAIhB,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAUpB,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,wBAAwB,CAAC,EAAE,MAAM,OAAO,CAAC;CAC1C;AAED,MAAM,WAAW,wBAAyB,SAAQ,iBAAiB;IACjE,OAAO,CACL,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACpC;AAED,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,+BAA+B,GACvC,wBAAwB,CAkB1B;AAED,wBAAsB,KAAK,CACzB,KAAK,EAAE,mBAAmB,EAC1B,wBAAwB,GAAE,MAAM,OAAwC,GACvE,OAAO,CAAC,oBAAoB,CAAC,CA4E/B"}
|
|
@@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { stateDirMode, stateFileMode } from "../../storage/modes.js";
|
|
5
|
+
import { beginWorkflowRecovery, validateWorkflowRecoveryAuthorization } from "../recovery-audit.js";
|
|
5
6
|
import { acquireWorkflowResumeLease } from "../run-state.js";
|
|
6
7
|
import { WorkspaceAttestationError } from "./attestation-error.js";
|
|
7
8
|
import { cleanupWorkspace } from "./cleanup.js";
|
|
@@ -10,7 +11,7 @@ import { acquireNoClobberLease } from "./leases.js";
|
|
|
10
11
|
import { claimOverlapsWorkspace, reserveWorkspaceReclaim, } from "./ownership.js";
|
|
11
12
|
import { assertContainedExisting, assertPathIdentity, candidateUnderRoot, containsPath, createContainedDirectories, descriptorBackedPathsSupported, descriptorPath, filesystemIdentity, openContainedDirectory, sameFilesystemIdentity, validateConfiguredRoot, } from "./paths.js";
|
|
12
13
|
import { prepareReclaim, WorkflowReclaimError, } from "./reclaim.js";
|
|
13
|
-
import { readWorkflowLiveness } from "./recovery.js";
|
|
14
|
+
import { hasActiveWorkflowEvidence, readWorkflowLiveness } from "./recovery.js";
|
|
14
15
|
import { appendWorkspaceEvent, fileSha256, readWorkflowSupplement, readWorkspaceBinding, readWorkspaceClaim, readWorkspaceClaims, readWorkspaceEvents, stableDigest, workspaceClaimPath, workspaceLockPath, writeWorkspaceBinding, writeWorkspaceClaim, } from "./state.js";
|
|
15
16
|
import { isWorkspaceAttestation } from "./validate.js";
|
|
16
17
|
const PROVIDER_ID = "local-git-worktree";
|
|
@@ -597,6 +598,7 @@ function reclaimPreview(coordRoot, binding, mode) {
|
|
|
597
598
|
run_id: binding.run_id,
|
|
598
599
|
mode,
|
|
599
600
|
liveness: readWorkflowLiveness(coordRoot, binding.run_id),
|
|
601
|
+
evidence_sha256: reclaimEvidenceDigest(coordRoot, binding),
|
|
600
602
|
ownership: "unproven",
|
|
601
603
|
dirty_paths: null,
|
|
602
604
|
lease_holder: reclaimLeaseHolders(coordRoot, binding),
|
|
@@ -617,13 +619,36 @@ function reclaimPreview(coordRoot, binding, mode) {
|
|
|
617
619
|
}
|
|
618
620
|
return preview;
|
|
619
621
|
}
|
|
620
|
-
function
|
|
622
|
+
function reclaimEvidenceDigest(coordRoot, binding) {
|
|
623
|
+
const evidence = (name) => {
|
|
624
|
+
try {
|
|
625
|
+
return readFileSync(join(coordRoot, ".harnery", "workflows", binding.run_id, name), "utf8");
|
|
626
|
+
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
if (error.code === "ENOENT")
|
|
629
|
+
return null;
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
return stableDigest({
|
|
634
|
+
binding,
|
|
635
|
+
proof: evidence("proof.json"),
|
|
636
|
+
transcript: evidence("transcript.jsonl"),
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
function assertReclaimLiveness(coordRoot, binding, recovery) {
|
|
621
640
|
const liveness = readWorkflowLiveness(coordRoot, binding.run_id);
|
|
641
|
+
if (liveness === "active" || (recovery && hasActiveWorkflowEvidence(coordRoot, binding.run_id))) {
|
|
642
|
+
throw new WorkflowReclaimError("workflow_reclaim_run_live", `run ${binding.run_id} has active evidence; its workspace was left untouched`);
|
|
643
|
+
}
|
|
644
|
+
if (recovery && reclaimEvidenceDigest(coordRoot, binding) !== recovery.evidenceSha256) {
|
|
645
|
+
throw new WorkflowReclaimError("workflow_reclaim_recovery_mismatch", "workflow reclaim requires the exact current preview evidence digest");
|
|
646
|
+
}
|
|
622
647
|
if (liveness === "inactive")
|
|
623
648
|
return;
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
649
|
+
if (liveness === "unknown" && recovery)
|
|
650
|
+
return;
|
|
651
|
+
throw new WorkflowReclaimError("workflow_reclaim_liveness_unknown", `run ${binding.run_id} has missing, corrupt, or ambiguous liveness evidence; its workspace was left untouched`);
|
|
627
652
|
}
|
|
628
653
|
function assertReclaimOwnership(coordRoot, binding) {
|
|
629
654
|
try {
|
|
@@ -666,10 +691,10 @@ function assertPeerWorkspaceAuthority(coordRoot, claim) {
|
|
|
666
691
|
throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", error.message);
|
|
667
692
|
}
|
|
668
693
|
}
|
|
669
|
-
function reclaimAuthorityGuard(coordRoot, binding, claim, reservation) {
|
|
694
|
+
function reclaimAuthorityGuard(coordRoot, binding, claim, reservation, recovery) {
|
|
670
695
|
try {
|
|
671
696
|
reservation.assertCurrent();
|
|
672
|
-
assertReclaimLiveness(coordRoot, binding);
|
|
697
|
+
assertReclaimLiveness(coordRoot, binding, recovery);
|
|
673
698
|
assertReclaimOwnership(coordRoot, binding);
|
|
674
699
|
assertPeerWorkspaceAuthority(coordRoot, claim);
|
|
675
700
|
}
|
|
@@ -685,9 +710,11 @@ async function reclaim(coordRoot, provider, binding, options) {
|
|
|
685
710
|
}
|
|
686
711
|
if (!options.yes)
|
|
687
712
|
return reclaimPreview(coordRoot, binding, options.mode);
|
|
713
|
+
if (options.unknownLiveness)
|
|
714
|
+
validateWorkflowRecoveryAuthorization(options.unknownLiveness);
|
|
688
715
|
// Liveness comes before terminal-proof cleanup helpers. A live run may not
|
|
689
716
|
// have a proof yet, and a missing proof must never be discovered after reset.
|
|
690
|
-
assertReclaimLiveness(coordRoot, binding);
|
|
717
|
+
assertReclaimLiveness(coordRoot, binding, options.unknownLiveness);
|
|
691
718
|
const claim = assertReclaimOwnership(coordRoot, binding);
|
|
692
719
|
let reservation;
|
|
693
720
|
try {
|
|
@@ -697,7 +724,7 @@ async function reclaim(coordRoot, provider, binding, options) {
|
|
|
697
724
|
throw new WorkflowReclaimError("workflow_reclaim_lease_held", error.message);
|
|
698
725
|
}
|
|
699
726
|
try {
|
|
700
|
-
reclaimAuthorityGuard(coordRoot, binding, claim, reservation);
|
|
727
|
+
reclaimAuthorityGuard(coordRoot, binding, claim, reservation, options.unknownLiveness);
|
|
701
728
|
let releaseResume;
|
|
702
729
|
try {
|
|
703
730
|
releaseResume = acquireWorkflowResumeLease(coordRoot, binding.run_id);
|
|
@@ -722,9 +749,24 @@ async function reclaim(coordRoot, provider, binding, options) {
|
|
|
722
749
|
throw new WorkflowReclaimError("workflow_reclaim_lease_held", error.message);
|
|
723
750
|
}
|
|
724
751
|
try {
|
|
725
|
-
assertReclaimLiveness(coordRoot, binding);
|
|
752
|
+
assertReclaimLiveness(coordRoot, binding, options.unknownLiveness);
|
|
726
753
|
assertReclaimOwnership(coordRoot, binding);
|
|
727
754
|
const preview = reclaimPreview(coordRoot, binding, options.mode);
|
|
755
|
+
const audit = options.unknownLiveness
|
|
756
|
+
? beginWorkflowRecovery(coordRoot, {
|
|
757
|
+
run_id: binding.run_id,
|
|
758
|
+
action: "reclaim_unknown_liveness",
|
|
759
|
+
actor: options.unknownLiveness.actor,
|
|
760
|
+
reason: options.unknownLiveness.reason,
|
|
761
|
+
target_sha256: options.unknownLiveness.evidenceSha256,
|
|
762
|
+
target: {
|
|
763
|
+
binding_id: binding.binding_id,
|
|
764
|
+
workspace_root: binding.workspace_root,
|
|
765
|
+
mode: options.mode,
|
|
766
|
+
liveness: preview.liveness,
|
|
767
|
+
},
|
|
768
|
+
})
|
|
769
|
+
: undefined;
|
|
728
770
|
let preparation;
|
|
729
771
|
// Keep ordinary cleanup's terminal-evidence checks, intent and receipts.
|
|
730
772
|
// Its provider callback uses the leases already held here instead of
|
|
@@ -736,11 +778,22 @@ async function reclaim(coordRoot, provider, binding, options) {
|
|
|
736
778
|
provider: {
|
|
737
779
|
...provider,
|
|
738
780
|
cleanup: async (candidate, intent) => {
|
|
739
|
-
assertReclaimLiveness(coordRoot, candidate);
|
|
781
|
+
assertReclaimLiveness(coordRoot, candidate, options.unknownLiveness);
|
|
740
782
|
assertReclaimOwnership(coordRoot, candidate);
|
|
741
783
|
return cleanupUnderLease(coordRoot, candidate, claim, intent, {
|
|
742
784
|
mode: options.mode,
|
|
743
|
-
|
|
785
|
+
recovery: options.unknownLiveness,
|
|
786
|
+
beforeEffect: () => {
|
|
787
|
+
audit?.assertCurrent();
|
|
788
|
+
reclaimAuthorityGuard(coordRoot, candidate, claim, reservation, options.unknownLiveness);
|
|
789
|
+
},
|
|
790
|
+
beforeRefEffect: () => {
|
|
791
|
+
audit?.assertCurrent();
|
|
792
|
+
reservation.assertCurrent();
|
|
793
|
+
assertReclaimLiveness(coordRoot, candidate, options.unknownLiveness);
|
|
794
|
+
requiredClaim(coordRoot, candidate);
|
|
795
|
+
assertPeerWorkspaceAuthority(coordRoot, claim);
|
|
796
|
+
},
|
|
744
797
|
prepared: (value) => {
|
|
745
798
|
preparation = value;
|
|
746
799
|
},
|
|
@@ -754,6 +807,8 @@ async function reclaim(coordRoot, provider, binding, options) {
|
|
|
754
807
|
throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", ("reason" in cleanupResult ? cleanupResult.reason : undefined) ??
|
|
755
808
|
"workspace cleanup preconditions could not be proved");
|
|
756
809
|
}
|
|
810
|
+
if (cleanupResult.status === "released" || cleanupResult.status === "already_released")
|
|
811
|
+
audit?.complete();
|
|
757
812
|
return {
|
|
758
813
|
...preview,
|
|
759
814
|
action: "reclaimed",
|
|
@@ -891,7 +946,7 @@ function cleanupUnderLease(coordRoot, binding, claim, intent, reclaimRequest, be
|
|
|
891
946
|
};
|
|
892
947
|
}
|
|
893
948
|
const liveness = readWorkflowLiveness(coordRoot, binding.run_id);
|
|
894
|
-
if (liveness !== "inactive") {
|
|
949
|
+
if (liveness !== "inactive" && !(liveness === "unknown" && reclaimRequest?.recovery)) {
|
|
895
950
|
return {
|
|
896
951
|
...base,
|
|
897
952
|
status: "blocked",
|
|
@@ -962,7 +1017,7 @@ function cleanupUnderLease(coordRoot, binding, claim, intent, reclaimRequest, be
|
|
|
962
1017
|
};
|
|
963
1018
|
}
|
|
964
1019
|
if (reclaimRequest) {
|
|
965
|
-
assertReclaimLiveness(coordRoot, binding);
|
|
1020
|
+
assertReclaimLiveness(coordRoot, binding, reclaimRequest.recovery);
|
|
966
1021
|
assertReclaimOwnership(coordRoot, binding);
|
|
967
1022
|
revalidateClaimResources(claim, root);
|
|
968
1023
|
reclaimRequest.prepared(prepareReclaim({
|
|
@@ -1002,6 +1057,7 @@ function cleanupUnderLease(coordRoot, binding, claim, intent, reclaimRequest, be
|
|
|
1002
1057
|
return { ...base, status: "blocked", reason: "workspace ref changed during cleanup" };
|
|
1003
1058
|
}
|
|
1004
1059
|
revalidateClaimResources(claim, root);
|
|
1060
|
+
reclaimRequest?.beforeRefEffect();
|
|
1005
1061
|
const deleted = gitMaybe(repository.source_root.realpath, [
|
|
1006
1062
|
"update-ref",
|
|
1007
1063
|
"-d",
|
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
* always has. So reclaim adds no second removal path that could diverge from the
|
|
15
15
|
* audited one, and a force-delete of live work exists nowhere in the codebase.
|
|
16
16
|
*/
|
|
17
|
+
import type { WorkflowRecoveryAuthorization } from "../recovery-audit.js";
|
|
17
18
|
import type { WorkflowLiveness } from "./recovery.js";
|
|
18
19
|
import type { WorkspaceCleanupAttempt, WorkspaceCleanupReceipt } from "./types.js";
|
|
19
20
|
export type ReclaimMode = "salvage" | "discard";
|
|
20
|
-
export type WorkflowReclaimRefusalCode = "workflow_reclaim_run_live" | "workflow_reclaim_liveness_unknown" | "workflow_reclaim_ownership_unproven" | "workflow_reclaim_lease_held";
|
|
21
|
+
export type WorkflowReclaimRefusalCode = "workflow_reclaim_run_live" | "workflow_reclaim_liveness_unknown" | "workflow_reclaim_recovery_mismatch" | "workflow_reclaim_ownership_unproven" | "workflow_reclaim_lease_held";
|
|
21
22
|
export declare class WorkflowReclaimError extends Error {
|
|
22
23
|
readonly code: WorkflowReclaimRefusalCode;
|
|
23
24
|
constructor(code: WorkflowReclaimRefusalCode, message: string);
|
|
@@ -25,6 +26,9 @@ export declare class WorkflowReclaimError extends Error {
|
|
|
25
26
|
export interface WorkspaceReclaimOptions {
|
|
26
27
|
mode: ReclaimMode;
|
|
27
28
|
yes?: boolean;
|
|
29
|
+
unknownLiveness?: WorkflowRecoveryAuthorization & {
|
|
30
|
+
evidenceSha256: string;
|
|
31
|
+
};
|
|
28
32
|
}
|
|
29
33
|
export interface ReclaimLeaseHolder {
|
|
30
34
|
scope: "binding" | "repository" | "resume";
|
|
@@ -38,6 +42,7 @@ export interface WorkspaceReclaimPreview {
|
|
|
38
42
|
run_id: string;
|
|
39
43
|
mode: ReclaimMode;
|
|
40
44
|
liveness: WorkflowLiveness;
|
|
45
|
+
evidence_sha256: string;
|
|
41
46
|
ownership: "ok" | "unproven";
|
|
42
47
|
ownership_detail?: string;
|
|
43
48
|
dirty_paths: string[] | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reclaim.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/reclaim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;
|
|
1
|
+
{"version":3,"file":"reclaim.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/reclaim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,sBAAsB,CAAC;AAE1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAEnF,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,SAAS,CAAC;AAEhD,MAAM,MAAM,0BAA0B,GAClC,2BAA2B,GAC3B,mCAAmC,GACnC,oCAAoC,GACpC,qCAAqC,GACrC,6BAA6B,CAAC;AAElC,qBAAa,oBAAqB,SAAQ,KAAK;IAE3C,QAAQ,CAAC,IAAI,EAAE,0BAA0B;gBAAhC,IAAI,EAAE,0BAA0B,EACzC,OAAO,EAAE,MAAM;CAKlB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,WAAW,CAAC;IAClB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,eAAe,CAAC,EAAE,6BAA6B,GAAG;QAAE,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9E;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,SAAS,GAAG,YAAY,GAAG,QAAQ,CAAC;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,IAAI,GAAG,UAAU,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,kBAAkB,EAAE,CAAC;CACpC;AAED,MAAM,MAAM,sBAAsB,GAC9B,uBAAuB,GACvB,CAAC,IAAI,CAAC,uBAAuB,EAAE,QAAQ,CAAC,GAAG;IACzC,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,kBAAkB,CAAC;IAChC,OAAO,EAAE,uBAAuB,GAAG,uBAAuB,CAAC;CAC5D,CAAC,CAAC;AAEP,MAAM,MAAM,kBAAkB,GAC1B;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACtE;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,MAAM,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3C;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/C,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,kBAAkB,CAwD7E;AAYD;qEACqE;AACrE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export type WorkflowLiveness = "active" | "inactive" | "unknown";
|
|
2
|
+
/** Corruption must not hide a readable active marker from an audited override. */
|
|
3
|
+
export declare function hasActiveWorkflowEvidence(coordRoot: string, runId: string): boolean;
|
|
2
4
|
export declare function readWorkflowLiveness(coordRoot: string, runId: string): WorkflowLiveness;
|
|
3
5
|
//# sourceMappingURL=recovery.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/recovery.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AAEjE,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,gBAAgB,CA6DvF"}
|
|
1
|
+
{"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/recovery.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AAEjE,kFAAkF;AAClF,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBnF;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,gBAAgB,CA6DvF"}
|
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { readWorkflowProof } from "../proof.js";
|
|
4
|
+
/** Corruption must not hide a readable active marker from an audited override. */
|
|
5
|
+
export function hasActiveWorkflowEvidence(coordRoot, runId) {
|
|
6
|
+
const path = join(resolve(coordRoot), ".harnery", "workflows", runId, "transcript.jsonl");
|
|
7
|
+
if (!existsSync(path))
|
|
8
|
+
return false;
|
|
9
|
+
let active = false;
|
|
10
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
11
|
+
try {
|
|
12
|
+
const event = JSON.parse(line);
|
|
13
|
+
if (event?.run_id !== runId)
|
|
14
|
+
continue;
|
|
15
|
+
if (["run.start", "run.resume", "agent.start"].includes(event.event))
|
|
16
|
+
active = true;
|
|
17
|
+
if (event.event === "run.parked" ||
|
|
18
|
+
(event.event === "run.end" && typeof event.ok === "boolean") ||
|
|
19
|
+
(event.event === "workspace.reattach.failed" && ["blocked", "lost"].includes(event.status)))
|
|
20
|
+
active = false;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
/* Unreadable evidence cannot establish inactivity. */
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return active;
|
|
27
|
+
}
|
|
4
28
|
export function readWorkflowLiveness(coordRoot, runId) {
|
|
5
29
|
const root = resolve(coordRoot);
|
|
6
30
|
const transcriptPath = join(root, ".harnery", "workflows", runId, "transcript.jsonl");
|
|
@@ -32,10 +32,12 @@ export interface TunnelState {
|
|
|
32
32
|
provider: TunnelProvider;
|
|
33
33
|
url: string;
|
|
34
34
|
gate_pid: number;
|
|
35
|
+
gate_start_token?: string;
|
|
36
|
+
cloudflared_start_token?: string;
|
|
37
|
+
registration_id?: string;
|
|
38
|
+
provider_state?: "pending" | "ready";
|
|
35
39
|
/** Present for Cloudflare quick tunnels; absent for Tailscale Serve/Funnel. */
|
|
36
40
|
cloudflared_pid?: number;
|
|
37
|
-
/** Optional provider-side process when a provider owns one. */
|
|
38
|
-
provider_pid?: number;
|
|
39
41
|
started_at: string;
|
|
40
42
|
target: string;
|
|
41
43
|
vhost: string;
|
|
@@ -57,9 +59,14 @@ export declare function clearState(name?: string, root?: string): void;
|
|
|
57
59
|
*/
|
|
58
60
|
export declare function listStates(root?: string): TunnelState[];
|
|
59
61
|
export declare function isProcessAlive(pid: number): boolean;
|
|
60
|
-
export type
|
|
61
|
-
|
|
62
|
-
export
|
|
62
|
+
export type TunnelReadiness = "ready" | "dead" | "unknown";
|
|
63
|
+
export type TunnelProcessObservation = "same" | "absent" | "different" | "unknown";
|
|
64
|
+
export interface TunnelReadinessVerifier {
|
|
65
|
+
observeProcess(pid: number, originalToken: string): TunnelProcessObservation;
|
|
66
|
+
withRegistration(state: TunnelState, inspect: () => TunnelReadiness): TunnelReadiness;
|
|
67
|
+
}
|
|
68
|
+
/** Classify saved original witnesses under the verifier's registration custody. */
|
|
69
|
+
export declare function tunnelReadiness(state: TunnelState, verifier: TunnelReadinessVerifier): TunnelReadiness;
|
|
63
70
|
export declare function tunnelTargetPort(state: TunnelState): number | null;
|
|
64
71
|
/**
|
|
65
72
|
* Newest live tunnel that serves `vhost` on the requested local web port.
|
|
@@ -72,7 +79,7 @@ export declare function tunnelTargetPort(state: TunnelState): number | null;
|
|
|
72
79
|
* link 400s. Matching the vhost too keeps the returned tunnel the one that
|
|
73
80
|
* actually serves the caller's origin.
|
|
74
81
|
*/
|
|
75
|
-
export declare function findLiveTunnelForOrigin(port: number, vhost: string, root
|
|
82
|
+
export declare function findLiveTunnelForOrigin(port: number, vhost: string, root: string, verifier: TunnelReadinessVerifier): TunnelState | null;
|
|
76
83
|
/** True when the tunnel's path scope forwards every one of `paths`. */
|
|
77
84
|
export declare function tunnelServesPaths(state: TunnelState, paths: readonly string[]): boolean;
|
|
78
85
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/lib/tunnel/state.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/lib/tunnel/state.ts"],"names":[],"mappings":"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"}
|