harnery 0.45.1 → 0.45.2

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.
Files changed (119) hide show
  1. package/dist/commands/workflow.d.ts.map +1 -1
  2. package/dist/commands/workflow.js +6 -45
  3. package/dist/core/agents/coord-client.d.ts.map +1 -1
  4. package/dist/core/agents/coord-client.js +0 -18
  5. package/dist/core/agents/state/pidmap.js +2 -2
  6. package/dist/core/agents/state/proc-start.d.ts +20 -31
  7. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  8. package/dist/core/agents/state/proc-start.js +37 -70
  9. package/dist/core/agents/state/service-process.d.ts +61 -0
  10. package/dist/core/agents/state/service-process.d.ts.map +1 -0
  11. package/dist/core/agents/state/service-process.js +143 -0
  12. package/dist/core/context/index.d.ts +6 -4
  13. package/dist/core/context/index.d.ts.map +1 -1
  14. package/dist/core/context/index.js +194 -256
  15. package/dist/core/context/observation.d.ts +9 -9
  16. package/dist/core/context/observation.d.ts.map +1 -1
  17. package/dist/core/context/observation.js +37 -6
  18. package/dist/core/events/v3/runtime-build.d.ts +14 -0
  19. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  20. package/dist/core/events/v3/runtime-build.js +44 -0
  21. package/dist/core/governor/index.d.ts +2 -1
  22. package/dist/core/governor/index.d.ts.map +1 -1
  23. package/dist/core/governor/index.js +2 -1
  24. package/dist/core/governor/service-read.d.ts +3 -10
  25. package/dist/core/governor/service-read.d.ts.map +1 -1
  26. package/dist/core/governor/service-read.js +10 -94
  27. package/dist/core/governor/service.d.ts +3 -7
  28. package/dist/core/governor/service.d.ts.map +1 -1
  29. package/dist/core/governor/service.js +17 -182
  30. package/dist/core/hooks/resolve/owner.js +2 -2
  31. package/dist/core/resources/sampler.js +2 -2
  32. package/dist/core/resources/service-status.d.ts +2 -14
  33. package/dist/core/resources/service-status.d.ts.map +1 -1
  34. package/dist/core/resources/service-status.js +3 -23
  35. package/dist/core/resources/service.d.ts +3 -7
  36. package/dist/core/resources/service.d.ts.map +1 -1
  37. package/dist/core/resources/service.js +12 -101
  38. package/dist/core/resources/status.d.ts.map +1 -1
  39. package/dist/core/resources/status.js +3 -2
  40. package/dist/core/semantic/service.d.ts.map +1 -1
  41. package/dist/core/semantic/service.js +9 -2
  42. package/dist/core/servers/index.d.ts +0 -2
  43. package/dist/core/servers/index.d.ts.map +1 -1
  44. package/dist/core/servers/index.js +3 -18
  45. package/dist/core/supervisor/service.d.ts +4 -7
  46. package/dist/core/supervisor/service.d.ts.map +1 -1
  47. package/dist/core/supervisor/service.js +23 -122
  48. package/dist/core/supervisor/services.js +2 -2
  49. package/dist/core/supervisor/status.d.ts +3 -17
  50. package/dist/core/supervisor/status.d.ts.map +1 -1
  51. package/dist/core/supervisor/status.js +7 -70
  52. package/dist/core/workflow/engine.js +2 -1
  53. package/dist/core/workflow/index.d.ts +3 -4
  54. package/dist/core/workflow/index.d.ts.map +1 -1
  55. package/dist/core/workflow/index.js +2 -2
  56. package/dist/core/workflow/proof.d.ts.map +1 -1
  57. package/dist/core/workflow/proof.js +30 -74
  58. package/dist/core/workflow/resume-lock.d.ts +17 -0
  59. package/dist/core/workflow/resume-lock.d.ts.map +1 -0
  60. package/dist/core/workflow/resume-lock.js +158 -0
  61. package/dist/core/workflow/run-state.d.ts +0 -21
  62. package/dist/core/workflow/run-state.d.ts.map +1 -1
  63. package/dist/core/workflow/run-state.js +3 -174
  64. package/dist/core/workflow/workspaces/leases.d.ts +4 -0
  65. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  66. package/dist/core/workflow/workspaces/leases.js +1 -1
  67. package/dist/core/workflow/workspaces/local-git.js +91 -210
  68. package/dist/core/workflow/workspaces/ownership.d.ts +0 -7
  69. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -1
  70. package/dist/core/workflow/workspaces/ownership.js +3 -130
  71. package/dist/core/workflow/workspaces/reclaim.d.ts +4 -7
  72. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  73. package/dist/core/workflow/workspaces/reclaim.js +0 -4
  74. package/dist/core/workflow/workspaces/recovery.d.ts +1 -1
  75. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  76. package/dist/core/workflow/workspaces/recovery.js +1 -1
  77. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  78. package/dist/core/workflow/workspaces/state.js +1 -4
  79. package/package.json +1 -1
  80. package/src/commands/workflow.ts +8 -58
  81. package/src/core/agents/coord-client.ts +0 -20
  82. package/src/core/agents/state/pidmap.ts +2 -2
  83. package/src/core/agents/state/proc-start.ts +41 -69
  84. package/src/core/agents/state/service-process.ts +217 -0
  85. package/src/core/context/index.ts +207 -272
  86. package/src/core/context/observation.ts +40 -9
  87. package/src/core/events/v3/runtime-build.ts +62 -0
  88. package/src/core/governor/index.ts +1 -2
  89. package/src/core/governor/service-read.ts +15 -114
  90. package/src/core/governor/service.ts +36 -220
  91. package/src/core/hooks/resolve/owner.ts +2 -2
  92. package/src/core/resources/sampler.ts +3 -2
  93. package/src/core/resources/service-status.ts +6 -35
  94. package/src/core/resources/service.ts +23 -133
  95. package/src/core/resources/status.ts +3 -2
  96. package/src/core/semantic/service.ts +8 -2
  97. package/src/core/servers/index.ts +3 -23
  98. package/src/core/supervisor/service.ts +33 -155
  99. package/src/core/supervisor/services.ts +2 -2
  100. package/src/core/supervisor/status.ts +10 -85
  101. package/src/core/workflow/engine.ts +1 -1
  102. package/src/core/workflow/index.ts +2 -13
  103. package/src/core/workflow/proof.ts +35 -83
  104. package/src/core/workflow/resume-lock.ts +179 -0
  105. package/src/core/workflow/run-state.ts +3 -207
  106. package/src/core/workflow/workspaces/leases.ts +1 -1
  107. package/src/core/workflow/workspaces/local-git.ts +120 -239
  108. package/src/core/workflow/workspaces/ownership.ts +3 -158
  109. package/src/core/workflow/workspaces/reclaim.ts +3 -9
  110. package/src/core/workflow/workspaces/recovery.ts +1 -1
  111. package/src/core/workflow/workspaces/state.ts +1 -5
  112. package/dist/core/agents/state/process-handle.d.ts +0 -26
  113. package/dist/core/agents/state/process-handle.d.ts.map +0 -1
  114. package/dist/core/agents/state/process-handle.js +0 -121
  115. package/dist/core/workflow/recovery-audit.d.ts +0 -30
  116. package/dist/core/workflow/recovery-audit.d.ts.map +0 -1
  117. package/dist/core/workflow/recovery-audit.js +0 -116
  118. package/src/core/agents/state/process-handle.ts +0 -165
  119. package/src/core/workflow/recovery-audit.ts +0 -181
@@ -16,7 +16,6 @@
16
16
  */
17
17
 
18
18
  import { existsSync } from "node:fs";
19
- import type { WorkflowRecoveryAuthorization } from "../recovery-audit.ts";
20
19
  import { git, gitMaybe } from "./git.ts";
21
20
  import type { WorkflowLiveness } from "./recovery.ts";
22
21
  import type { WorkspaceCleanupAttempt, WorkspaceCleanupReceipt } from "./types.ts";
@@ -26,7 +25,6 @@ export type ReclaimMode = "salvage" | "discard";
26
25
  export type WorkflowReclaimRefusalCode =
27
26
  | "workflow_reclaim_run_live"
28
27
  | "workflow_reclaim_liveness_unknown"
29
- | "workflow_reclaim_recovery_mismatch"
30
28
  | "workflow_reclaim_ownership_unproven"
31
29
  | "workflow_reclaim_lease_held";
32
30
 
@@ -43,7 +41,8 @@ export class WorkflowReclaimError extends Error {
43
41
  export interface WorkspaceReclaimOptions {
44
42
  mode: ReclaimMode;
45
43
  yes?: boolean;
46
- unknownLiveness?: WorkflowRecoveryAuthorization & { evidenceSha256: string };
44
+ /** Admit missing or corrupt liveness evidence; active evidence still refuses. */
45
+ force?: boolean;
47
46
  }
48
47
 
49
48
  export interface ReclaimLeaseHolder {
@@ -59,7 +58,6 @@ export interface WorkspaceReclaimPreview {
59
58
  run_id: string;
60
59
  mode: ReclaimMode;
61
60
  liveness: WorkflowLiveness;
62
- evidence_sha256: string;
63
61
  ownership: "ok" | "unproven";
64
62
  ownership_detail?: string;
65
63
  dirty_paths: string[] | null;
@@ -84,7 +82,7 @@ export interface PrepareReclaimInput {
84
82
  worktreePath: string;
85
83
  mode: ReclaimMode;
86
84
  runId: string;
87
- /** Revalidate the held reservation and peer authority immediately before effects. */
85
+ /** Recheck peer authority once, immediately before the first destructive step. */
88
86
  beforeEffect?: () => void;
89
87
  }
90
88
 
@@ -117,7 +115,6 @@ export function prepareReclaim(input: PrepareReclaimInput): ReclaimPreparation {
117
115
  // cannot see. Running clean first would leave staged deletions behind.
118
116
  input.beforeEffect?.();
119
117
  git(input.worktreePath, ["reset", "--hard"]);
120
- input.beforeEffect?.();
121
118
  git(input.worktreePath, ["clean", "-fd"]);
122
119
  return { action: "discarded", detail: "uncommitted changes were discarded on request" };
123
120
  }
@@ -138,13 +135,10 @@ export function prepareReclaim(input: PrepareReclaimInput): ReclaimPreparation {
138
135
  const before = git(input.worktreePath, ["rev-parse", "HEAD"]).trim();
139
136
  input.beforeEffect?.();
140
137
  git(input.worktreePath, ["add", "-A"]);
141
- input.beforeEffect?.();
142
138
  git(input.worktreePath, ["commit", "--no-verify", "-m", salvageMessage(input.runId)]);
143
139
  const commit = git(input.worktreePath, ["rev-parse", "HEAD"]).trim();
144
140
  const branch = salvageBranch(input.runId);
145
- input.beforeEffect?.();
146
141
  git(input.worktreePath, ["branch", "--force", branch, commit]);
147
- input.beforeEffect?.();
148
142
  git(input.worktreePath, ["reset", "--hard", before]);
149
143
  return {
150
144
  action: "salvaged",
@@ -4,7 +4,7 @@ import { readWorkflowProof } from "../proof.ts";
4
4
 
5
5
  export type WorkflowLiveness = "active" | "inactive" | "unknown";
6
6
 
7
- /** Corruption must not hide a readable active marker from an audited override. */
7
+ /** Corruption must not hide a readable active marker from a forced reclaim. */
8
8
  export function hasActiveWorkflowEvidence(coordRoot: string, runId: string): boolean {
9
9
  const path = join(resolve(coordRoot), ".harnery", "workflows", runId, "transcript.jsonl");
10
10
  if (!existsSync(path)) return false;
@@ -19,7 +19,7 @@ import {
19
19
  stableDigest,
20
20
  writeImmutableJson,
21
21
  } from "../durable-record.ts";
22
- import { assertClaimAuthority, publishWorkspaceClaim } from "./ownership.ts";
22
+ import { assertClaimAuthority } from "./ownership.ts";
23
23
 
24
24
  export { appendWorkflowTranscriptEvent } from "../transcript.ts";
25
25
 
@@ -143,10 +143,6 @@ export function readWorkspaceRequest(
143
143
  }
144
144
 
145
145
  export function writeWorkspaceClaim(coordRoot: string, claim: WorkspaceClaim): boolean {
146
- return publishWorkspaceClaim(coordRoot, claim, () => writeClaim(coordRoot, claim));
147
- }
148
-
149
- function writeClaim(coordRoot: string, claim: WorkspaceClaim): boolean {
150
146
  const path = workspaceClaimPath(coordRoot, claim.provider_id, claim.binding_id);
151
147
  try {
152
148
  return writeImmutableJson(path, claim);
@@ -1,26 +0,0 @@
1
- import type { ProcessGenerationVerdict } from "./proc-start.js";
2
- export interface ProcessHandleSignalResult {
3
- sent: boolean;
4
- verdict: ProcessGenerationVerdict;
5
- }
6
- export type NativeProcessHelperRunner = (command: string, args: string[], options: {
7
- input: string;
8
- encoding: "utf8";
9
- timeout: number;
10
- maxBuffer: number;
11
- windowsHide: boolean;
12
- }) => {
13
- status: number | null;
14
- stdout: string | Buffer | null;
15
- error?: Error;
16
- };
17
- export declare const LINUX_PROCESS_HANDLE_SCRIPT = "\nimport json, os, pathlib, signal, sys\nresult = {\"sent\": False, \"verdict\": \"unknown\"}\nfd = None\ntry:\n request = json.loads(sys.stdin.readline())\n if not hasattr(os, \"pidfd_open\") or not hasattr(signal, \"pidfd_send_signal\"):\n raise RuntimeError(\"pidfd support unavailable\")\n fd = os.pidfd_open(request[\"pid\"])\n stat = pathlib.Path(\"/proc/%d/stat\" % request[\"pid\"]).read_text()\n ticks = stat[stat.rfind(\") \") + 2:].split()[19]\n boot = pathlib.Path(\"/proc/sys/kernel/random/boot_id\").read_text().strip().replace(\"-\", \"\")[:8]\n token = \"l%s.%s\" % (boot, ticks)\n result[\"token\"] = token\n recorded = request.get(\"token\")\n result[\"verdict\"] = \"same\" if token == recorded else \"different\"\n if request[\"mode\"] == \"signal\" and result[\"verdict\"] == \"same\":\n print(json.dumps({\"phase\": \"verified\"}), flush=True)\n if sys.stdin.readline().strip() == \"signal\":\n signal.pidfd_send_signal(fd, request[\"signal\"])\n result[\"sent\"] = True\nexcept Exception:\n pass\nfinally:\n if fd is not None:\n os.close(fd)\nprint(json.dumps(result), flush=True)\n";
18
- export declare const WINDOWS_PROCESS_HANDLE_SCRIPT = "\n$ErrorActionPreference = 'Stop'\n$result = @{ sent = $false; verdict = 'unknown' }\n$handle = [IntPtr]::Zero\ntry {\n Add-Type -TypeDefinition @'\nusing System;\nusing System.Runtime.InteropServices;\npublic static class HarneryProcessHandle {\n [DllImport(\"kernel32.dll\", SetLastError=true)] public static extern IntPtr OpenProcess(uint access, bool inherit, uint processId);\n [DllImport(\"kernel32.dll\", SetLastError=true)] public static extern bool GetProcessTimes(IntPtr handle, out long creation, out long exit, out long kernel, out long user);\n [DllImport(\"kernel32.dll\", SetLastError=true)] public static extern bool TerminateProcess(IntPtr handle, uint exitCode);\n [DllImport(\"kernel32.dll\")] public static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);\n [DllImport(\"kernel32.dll\")] public static extern bool CloseHandle(IntPtr handle);\n}\n'@\n $request = [Console]::In.ReadLine() | ConvertFrom-Json\n $access = [uint32]0x101000\n if ($request.mode -eq 'signal') { $access = $access -bor 1 }\n $handle = [HarneryProcessHandle]::OpenProcess($access, $false, [uint32]$request.pid)\n if ($handle -eq [IntPtr]::Zero) { throw 'process handle unavailable' }\n [long]$creation = 0; [long]$exitTime = 0; [long]$kernel = 0; [long]$userTime = 0\n if (-not [HarneryProcessHandle]::GetProcessTimes($handle, [ref]$creation, [ref]$exitTime, [ref]$kernel, [ref]$userTime)) { throw 'process time unavailable' }\n $token = 'w' + $creation.ToString([Globalization.CultureInfo]::InvariantCulture)\n $result.token = $token\n $result.verdict = if ($token -ceq $request.token) { 'same' } else { 'different' }\n if ($request.mode -eq 'signal' -and $result.verdict -eq 'same') {\n [Console]::Out.WriteLine('{\"phase\":\"verified\"}')\n [Console]::Out.Flush()\n if ([Console]::In.ReadLine() -eq 'signal' -and [HarneryProcessHandle]::WaitForSingleObject($handle, 0) -eq 258) {\n $result.sent = [HarneryProcessHandle]::TerminateProcess($handle, 1)\n }\n }\n} catch {} finally {\n if ($handle -ne [IntPtr]::Zero) { [void][HarneryProcessHandle]::CloseHandle($handle) }\n}\n$result | ConvertTo-Json -Compress\n";
19
- /** Windows creation FILETIME, read from an OpenProcess handle. */
20
- export declare function windowsProcessStartToken(pid: number, run?: NativeProcessHelperRunner): string | null;
21
- /** No numeric-PID signaling fallback is permitted when an exact handle fails. */
22
- export declare function signalProcessWithHandle(pid: number, recordedToken: string, signal: NodeJS.Signals | number, options?: {
23
- platform?: NodeJS.Platform;
24
- run?: NativeProcessHelperRunner;
25
- }): ProcessHandleSignalResult;
26
- //# sourceMappingURL=process-handle.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"process-handle.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/process-handle.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAEhE,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,wBAAwB,CAAC;CACnC;AAMD,MAAM,MAAM,yBAAyB,GAAG,CACtC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE;IACP,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;CACtB,KACE;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC;AAI9E,eAAO,MAAM,2BAA2B,+qCA2BvC,CAAC;AAEF,eAAO,MAAM,6BAA6B,qnEAqCzC,CAAC;AAwCF,kEAAkE;AAClE,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,MAAM,EACX,GAAG,GAAE,yBAAqC,GACzC,MAAM,GAAG,IAAI,CAGf;AAED,iFAAiF;AACjF,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,MAAM,EAC/B,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAAC,GAAG,CAAC,EAAE,yBAAyB,CAAA;CAAO,GAC5E,yBAAyB,CAgB3B"}
@@ -1,121 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import { constants } from "node:os";
3
- // The helper owns the descriptor throughout observation and effect. The second
4
- // input line authorizes the effect; EOF releases the handle without signaling.
5
- export const LINUX_PROCESS_HANDLE_SCRIPT = `
6
- import json, os, pathlib, signal, sys
7
- result = {"sent": False, "verdict": "unknown"}
8
- fd = None
9
- try:
10
- request = json.loads(sys.stdin.readline())
11
- if not hasattr(os, "pidfd_open") or not hasattr(signal, "pidfd_send_signal"):
12
- raise RuntimeError("pidfd support unavailable")
13
- fd = os.pidfd_open(request["pid"])
14
- stat = pathlib.Path("/proc/%d/stat" % request["pid"]).read_text()
15
- ticks = stat[stat.rfind(") ") + 2:].split()[19]
16
- boot = pathlib.Path("/proc/sys/kernel/random/boot_id").read_text().strip().replace("-", "")[:8]
17
- token = "l%s.%s" % (boot, ticks)
18
- result["token"] = token
19
- recorded = request.get("token")
20
- result["verdict"] = "same" if token == recorded else "different"
21
- if request["mode"] == "signal" and result["verdict"] == "same":
22
- print(json.dumps({"phase": "verified"}), flush=True)
23
- if sys.stdin.readline().strip() == "signal":
24
- signal.pidfd_send_signal(fd, request["signal"])
25
- result["sent"] = True
26
- except Exception:
27
- pass
28
- finally:
29
- if fd is not None:
30
- os.close(fd)
31
- print(json.dumps(result), flush=True)
32
- `;
33
- export const WINDOWS_PROCESS_HANDLE_SCRIPT = `
34
- $ErrorActionPreference = 'Stop'
35
- $result = @{ sent = $false; verdict = 'unknown' }
36
- $handle = [IntPtr]::Zero
37
- try {
38
- Add-Type -TypeDefinition @'
39
- using System;
40
- using System.Runtime.InteropServices;
41
- public static class HarneryProcessHandle {
42
- [DllImport("kernel32.dll", SetLastError=true)] public static extern IntPtr OpenProcess(uint access, bool inherit, uint processId);
43
- [DllImport("kernel32.dll", SetLastError=true)] public static extern bool GetProcessTimes(IntPtr handle, out long creation, out long exit, out long kernel, out long user);
44
- [DllImport("kernel32.dll", SetLastError=true)] public static extern bool TerminateProcess(IntPtr handle, uint exitCode);
45
- [DllImport("kernel32.dll")] public static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
46
- [DllImport("kernel32.dll")] public static extern bool CloseHandle(IntPtr handle);
47
- }
48
- '@
49
- $request = [Console]::In.ReadLine() | ConvertFrom-Json
50
- $access = [uint32]0x101000
51
- if ($request.mode -eq 'signal') { $access = $access -bor 1 }
52
- $handle = [HarneryProcessHandle]::OpenProcess($access, $false, [uint32]$request.pid)
53
- if ($handle -eq [IntPtr]::Zero) { throw 'process handle unavailable' }
54
- [long]$creation = 0; [long]$exitTime = 0; [long]$kernel = 0; [long]$userTime = 0
55
- if (-not [HarneryProcessHandle]::GetProcessTimes($handle, [ref]$creation, [ref]$exitTime, [ref]$kernel, [ref]$userTime)) { throw 'process time unavailable' }
56
- $token = 'w' + $creation.ToString([Globalization.CultureInfo]::InvariantCulture)
57
- $result.token = $token
58
- $result.verdict = if ($token -ceq $request.token) { 'same' } else { 'different' }
59
- if ($request.mode -eq 'signal' -and $result.verdict -eq 'same') {
60
- [Console]::Out.WriteLine('{"phase":"verified"}')
61
- [Console]::Out.Flush()
62
- if ([Console]::In.ReadLine() -eq 'signal' -and [HarneryProcessHandle]::WaitForSingleObject($handle, 0) -eq 258) {
63
- $result.sent = [HarneryProcessHandle]::TerminateProcess($handle, 1)
64
- }
65
- }
66
- } catch {} finally {
67
- if ($handle -ne [IntPtr]::Zero) { [void][HarneryProcessHandle]::CloseHandle($handle) }
68
- }
69
- $result | ConvertTo-Json -Compress
70
- `;
71
- function runHelper(request, platform, run) {
72
- const unknown = { sent: false, verdict: "unknown" };
73
- if (!Number.isInteger(request.pid) || request.pid <= 0)
74
- return unknown;
75
- if (platform !== "linux" && platform !== "win32")
76
- return unknown;
77
- try {
78
- const command = platform === "linux" ? "python3" : "powershell.exe";
79
- const args = platform === "linux"
80
- ? ["-I", "-c", LINUX_PROCESS_HANDLE_SCRIPT]
81
- : ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", WINDOWS_PROCESS_HANDLE_SCRIPT];
82
- const output = run(command, args, {
83
- input: `${JSON.stringify(request)}\nsignal\n`,
84
- encoding: "utf8",
85
- timeout: 10_000,
86
- maxBuffer: 64 * 1024,
87
- windowsHide: true,
88
- });
89
- if (output.error || output.status !== 0)
90
- return unknown;
91
- const lines = String(output.stdout ?? "")
92
- .trim()
93
- .split(/\r?\n/);
94
- const parsed = JSON.parse(lines.at(-1) ?? "");
95
- if (typeof parsed.sent !== "boolean" ||
96
- !["same", "different", "unknown"].includes(parsed.verdict) ||
97
- (parsed.sent && parsed.verdict !== "same"))
98
- return unknown;
99
- return parsed;
100
- }
101
- catch {
102
- return unknown;
103
- }
104
- }
105
- /** Windows creation FILETIME, read from an OpenProcess handle. */
106
- export function windowsProcessStartToken(pid, run = spawnSync) {
107
- const result = runHelper({ mode: "probe", pid }, "win32", run);
108
- return typeof result.token === "string" && /^w\d+$/.test(result.token) ? result.token : null;
109
- }
110
- /** No numeric-PID signaling fallback is permitted when an exact handle fails. */
111
- export function signalProcessWithHandle(pid, recordedToken, signal, options = {}) {
112
- const platform = options.platform ?? process.platform;
113
- const number = typeof signal === "number" ? signal : constants.signals[signal];
114
- if (!Number.isInteger(number) ||
115
- number <= 0 ||
116
- (platform === "linux" && !/^l[0-9a-f]{8}\.\d+$/.test(recordedToken)) ||
117
- (platform === "win32" && (!/^w\d+$/.test(recordedToken) || ![9, 15].includes(number))))
118
- return { sent: false, verdict: "unknown" };
119
- const result = runHelper({ mode: "signal", pid, token: recordedToken, signal: number }, platform, options.run ?? spawnSync);
120
- return { sent: result.sent, verdict: result.verdict };
121
- }
@@ -1,30 +0,0 @@
1
- export interface WorkflowRecoveryAuthorization {
2
- actor: string;
3
- reason: string;
4
- }
5
- export interface WorkflowRecoveryIntent extends WorkflowRecoveryAuthorization {
6
- schema_version: 1;
7
- operation_id: string;
8
- run_id: string;
9
- action: "reclaim_unknown_liveness" | "unlock_resume";
10
- target_sha256: string;
11
- target: Record<string, unknown>;
12
- recorded_at: string;
13
- pid: number;
14
- host: string;
15
- outcome: "unknown";
16
- }
17
- export interface WorkflowRecoveryRecord {
18
- intent: WorkflowRecoveryIntent;
19
- outcome: "unknown" | "completed";
20
- }
21
- export declare function validateWorkflowRecoveryAuthorization(authorization: WorkflowRecoveryAuthorization): void;
22
- /** Canonical recovery ledger: immutable intent is durable before effects.
23
- * Completion cites that exact intent; absence always means unknown. */
24
- export declare function beginWorkflowRecovery(coordRoot: string, input: Omit<WorkflowRecoveryIntent, "schema_version" | "operation_id" | "recorded_at" | "pid" | "host" | "outcome">): {
25
- intent: WorkflowRecoveryIntent;
26
- assertCurrent(): void;
27
- complete(): void;
28
- };
29
- export declare function readWorkflowRecoveryLedger(coordRoot: string, runId: string): WorkflowRecoveryRecord[];
30
- //# sourceMappingURL=recovery-audit.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"recovery-audit.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/recovery-audit.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,6BAA6B;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAuB,SAAQ,6BAA6B;IAC3E,cAAc,EAAE,CAAC,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,0BAA0B,GAAG,eAAe,CAAC;IACrD,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,SAAS,CAAC;CACpB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,sBAAsB,CAAC;IAC/B,OAAO,EAAE,SAAS,GAAG,WAAW,CAAC;CAClC;AAED,wBAAgB,qCAAqC,CACnD,aAAa,EAAE,6BAA6B,GAC3C,IAAI,CAmBN;AAED;uEACuE;AACvE,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,IAAI,CACT,sBAAsB,EACtB,gBAAgB,GAAG,cAAc,GAAG,aAAa,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,CAC/E,GACA;IAAE,MAAM,EAAE,sBAAsB,CAAC;IAAC,aAAa,IAAI,IAAI,CAAC;IAAC,QAAQ,IAAI,IAAI,CAAA;CAAE,CAmD7E;AAkBD,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,sBAAsB,EAAE,CAyC1B"}
@@ -1,116 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { existsSync, lstatSync, mkdirSync, readdirSync } from "node:fs";
3
- import { hostname } from "node:os";
4
- import { dirname, join } from "node:path";
5
- import { stateDirMode } from "../storage/modes.js";
6
- import { assertWorkflowRunId } from "./approvals.js";
7
- import { fsyncParentDirectory, readJsonRecord, stableDigest, writeImmutableJson, } from "./durable-record.js";
8
- export function validateWorkflowRecoveryAuthorization(authorization) {
9
- for (const [field, limit] of [
10
- ["actor", 200],
11
- ["reason", 2_000],
12
- ]) {
13
- const value = authorization[field];
14
- if (typeof value !== "string" ||
15
- !value.trim() ||
16
- value.length > limit ||
17
- Array.from(value).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)) {
18
- throw new Error(`workflow recovery requires a nonempty ${field} of at most ${limit} characters without control characters`);
19
- }
20
- }
21
- }
22
- /** Canonical recovery ledger: immutable intent is durable before effects.
23
- * Completion cites that exact intent; absence always means unknown. */
24
- export function beginWorkflowRecovery(coordRoot, input) {
25
- assertWorkflowRunId(input.run_id);
26
- validateWorkflowRecoveryAuthorization(input);
27
- if (!/^[a-f0-9]{64}$/.test(input.target_sha256))
28
- throw new Error("workflow recovery requires an exact preview digest");
29
- const intent = {
30
- ...input,
31
- schema_version: 1,
32
- operation_id: `recovery-${randomUUID()}`,
33
- recorded_at: new Date().toISOString(),
34
- pid: process.pid,
35
- host: hostname(),
36
- outcome: "unknown",
37
- };
38
- const directory = join(coordRoot, ".harnery", "workflows", input.run_id, "recovery", intent.operation_id);
39
- // Sync each newly published directory name too. Syncing only the intent's
40
- // immediate parent cannot make a newly created ancestor durable.
41
- const runDirectory = join(coordRoot, ".harnery", "workflows", input.run_id);
42
- ensureDurableDirectory(runDirectory);
43
- const ledgerDirectory = join(runDirectory, "recovery");
44
- ensureDurableDirectory(ledgerDirectory);
45
- fsyncParentDirectory(ledgerDirectory);
46
- mkdirSync(directory, { mode: stateDirMode() });
47
- fsyncParentDirectory(directory);
48
- const intentPath = join(directory, "intent.json");
49
- writeImmutableJson(intentPath, intent);
50
- const assertCurrent = () => {
51
- if (stableDigest(readJsonRecord(intentPath, "recovery intent")) !== stableDigest(intent))
52
- throw new Error("workflow recovery audit authority changed");
53
- };
54
- return {
55
- intent,
56
- assertCurrent,
57
- complete: () => {
58
- assertCurrent();
59
- writeImmutableJson(join(directory, "completion.json"), {
60
- schema_version: 1,
61
- operation_id: intent.operation_id,
62
- intent_sha256: stableDigest(intent),
63
- outcome: "completed",
64
- recorded_at: new Date().toISOString(),
65
- });
66
- },
67
- };
68
- }
69
- function ensureDurableDirectory(path) {
70
- if (existsSync(path)) {
71
- if (!lstatSync(path).isDirectory())
72
- throw new Error("workflow recovery ledger is not a directory");
73
- return;
74
- }
75
- ensureDurableDirectory(dirname(path));
76
- try {
77
- mkdirSync(path, { mode: stateDirMode() });
78
- }
79
- catch (error) {
80
- if (error.code !== "EEXIST" || !lstatSync(path).isDirectory())
81
- throw error;
82
- }
83
- fsyncParentDirectory(path);
84
- }
85
- export function readWorkflowRecoveryLedger(coordRoot, runId) {
86
- assertWorkflowRunId(runId);
87
- const directory = join(coordRoot, ".harnery", "workflows", runId, "recovery");
88
- if (!existsSync(directory))
89
- return [];
90
- return readdirSync(directory)
91
- .sort()
92
- .map((entry) => {
93
- if (!/^recovery-[a-f0-9-]{36}$/.test(entry))
94
- throw new Error("invalid workflow recovery ledger entry");
95
- const intent = readJsonRecord(join(directory, entry, "intent.json"), "recovery intent");
96
- if (intent.schema_version !== 1 ||
97
- intent.operation_id !== entry ||
98
- intent.run_id !== runId ||
99
- intent.outcome !== "unknown" ||
100
- !["reclaim_unknown_liveness", "unlock_resume"].includes(intent.action) ||
101
- !/^[a-f0-9]{64}$/.test(intent.target_sha256)) {
102
- throw new Error("invalid workflow recovery intent authority");
103
- }
104
- validateWorkflowRecoveryAuthorization(intent);
105
- const completionPath = join(directory, entry, "completion.json");
106
- if (!existsSync(completionPath))
107
- return { intent, outcome: "unknown" };
108
- const completion = readJsonRecord(completionPath, "recovery completion");
109
- if (completion.schema_version !== 1 ||
110
- completion.operation_id !== entry ||
111
- completion.intent_sha256 !== stableDigest(intent) ||
112
- completion.outcome !== "completed")
113
- throw new Error("invalid workflow recovery completion authority");
114
- return { intent, outcome: "completed" };
115
- });
116
- }
@@ -1,165 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import { constants } from "node:os";
3
- import type { ProcessGenerationVerdict } from "./proc-start.ts";
4
-
5
- export interface ProcessHandleSignalResult {
6
- sent: boolean;
7
- verdict: ProcessGenerationVerdict;
8
- }
9
-
10
- interface NativeHelperResult extends ProcessHandleSignalResult {
11
- token?: string;
12
- }
13
-
14
- export type NativeProcessHelperRunner = (
15
- command: string,
16
- args: string[],
17
- options: {
18
- input: string;
19
- encoding: "utf8";
20
- timeout: number;
21
- maxBuffer: number;
22
- windowsHide: boolean;
23
- },
24
- ) => { status: number | null; stdout: string | Buffer | null; error?: Error };
25
-
26
- // The helper owns the descriptor throughout observation and effect. The second
27
- // input line authorizes the effect; EOF releases the handle without signaling.
28
- export const LINUX_PROCESS_HANDLE_SCRIPT = `
29
- import json, os, pathlib, signal, sys
30
- result = {"sent": False, "verdict": "unknown"}
31
- fd = None
32
- try:
33
- request = json.loads(sys.stdin.readline())
34
- if not hasattr(os, "pidfd_open") or not hasattr(signal, "pidfd_send_signal"):
35
- raise RuntimeError("pidfd support unavailable")
36
- fd = os.pidfd_open(request["pid"])
37
- stat = pathlib.Path("/proc/%d/stat" % request["pid"]).read_text()
38
- ticks = stat[stat.rfind(") ") + 2:].split()[19]
39
- boot = pathlib.Path("/proc/sys/kernel/random/boot_id").read_text().strip().replace("-", "")[:8]
40
- token = "l%s.%s" % (boot, ticks)
41
- result["token"] = token
42
- recorded = request.get("token")
43
- result["verdict"] = "same" if token == recorded else "different"
44
- if request["mode"] == "signal" and result["verdict"] == "same":
45
- print(json.dumps({"phase": "verified"}), flush=True)
46
- if sys.stdin.readline().strip() == "signal":
47
- signal.pidfd_send_signal(fd, request["signal"])
48
- result["sent"] = True
49
- except Exception:
50
- pass
51
- finally:
52
- if fd is not None:
53
- os.close(fd)
54
- print(json.dumps(result), flush=True)
55
- `;
56
-
57
- export const WINDOWS_PROCESS_HANDLE_SCRIPT = `
58
- $ErrorActionPreference = 'Stop'
59
- $result = @{ sent = $false; verdict = 'unknown' }
60
- $handle = [IntPtr]::Zero
61
- try {
62
- Add-Type -TypeDefinition @'
63
- using System;
64
- using System.Runtime.InteropServices;
65
- public static class HarneryProcessHandle {
66
- [DllImport("kernel32.dll", SetLastError=true)] public static extern IntPtr OpenProcess(uint access, bool inherit, uint processId);
67
- [DllImport("kernel32.dll", SetLastError=true)] public static extern bool GetProcessTimes(IntPtr handle, out long creation, out long exit, out long kernel, out long user);
68
- [DllImport("kernel32.dll", SetLastError=true)] public static extern bool TerminateProcess(IntPtr handle, uint exitCode);
69
- [DllImport("kernel32.dll")] public static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
70
- [DllImport("kernel32.dll")] public static extern bool CloseHandle(IntPtr handle);
71
- }
72
- '@
73
- $request = [Console]::In.ReadLine() | ConvertFrom-Json
74
- $access = [uint32]0x101000
75
- if ($request.mode -eq 'signal') { $access = $access -bor 1 }
76
- $handle = [HarneryProcessHandle]::OpenProcess($access, $false, [uint32]$request.pid)
77
- if ($handle -eq [IntPtr]::Zero) { throw 'process handle unavailable' }
78
- [long]$creation = 0; [long]$exitTime = 0; [long]$kernel = 0; [long]$userTime = 0
79
- if (-not [HarneryProcessHandle]::GetProcessTimes($handle, [ref]$creation, [ref]$exitTime, [ref]$kernel, [ref]$userTime)) { throw 'process time unavailable' }
80
- $token = 'w' + $creation.ToString([Globalization.CultureInfo]::InvariantCulture)
81
- $result.token = $token
82
- $result.verdict = if ($token -ceq $request.token) { 'same' } else { 'different' }
83
- if ($request.mode -eq 'signal' -and $result.verdict -eq 'same') {
84
- [Console]::Out.WriteLine('{"phase":"verified"}')
85
- [Console]::Out.Flush()
86
- if ([Console]::In.ReadLine() -eq 'signal' -and [HarneryProcessHandle]::WaitForSingleObject($handle, 0) -eq 258) {
87
- $result.sent = [HarneryProcessHandle]::TerminateProcess($handle, 1)
88
- }
89
- }
90
- } catch {} finally {
91
- if ($handle -ne [IntPtr]::Zero) { [void][HarneryProcessHandle]::CloseHandle($handle) }
92
- }
93
- $result | ConvertTo-Json -Compress
94
- `;
95
-
96
- function runHelper(
97
- request: { mode: "probe" | "signal"; pid: number; token?: string; signal?: number },
98
- platform: NodeJS.Platform,
99
- run: NativeProcessHelperRunner,
100
- ): NativeHelperResult {
101
- const unknown = { sent: false, verdict: "unknown" } as const;
102
- if (!Number.isInteger(request.pid) || request.pid <= 0) return unknown;
103
- if (platform !== "linux" && platform !== "win32") return unknown;
104
- try {
105
- const command = platform === "linux" ? "python3" : "powershell.exe";
106
- const args =
107
- platform === "linux"
108
- ? ["-I", "-c", LINUX_PROCESS_HANDLE_SCRIPT]
109
- : ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", WINDOWS_PROCESS_HANDLE_SCRIPT];
110
- const output = run(command, args, {
111
- input: `${JSON.stringify(request)}\nsignal\n`,
112
- encoding: "utf8",
113
- timeout: 10_000,
114
- maxBuffer: 64 * 1024,
115
- windowsHide: true,
116
- });
117
- if (output.error || output.status !== 0) return unknown;
118
- const lines = String(output.stdout ?? "")
119
- .trim()
120
- .split(/\r?\n/);
121
- const parsed = JSON.parse(lines.at(-1) ?? "") as NativeHelperResult;
122
- if (
123
- typeof parsed.sent !== "boolean" ||
124
- !["same", "different", "unknown"].includes(parsed.verdict) ||
125
- (parsed.sent && parsed.verdict !== "same")
126
- )
127
- return unknown;
128
- return parsed;
129
- } catch {
130
- return unknown;
131
- }
132
- }
133
-
134
- /** Windows creation FILETIME, read from an OpenProcess handle. */
135
- export function windowsProcessStartToken(
136
- pid: number,
137
- run: NativeProcessHelperRunner = spawnSync,
138
- ): string | null {
139
- const result = runHelper({ mode: "probe", pid }, "win32", run);
140
- return typeof result.token === "string" && /^w\d+$/.test(result.token) ? result.token : null;
141
- }
142
-
143
- /** No numeric-PID signaling fallback is permitted when an exact handle fails. */
144
- export function signalProcessWithHandle(
145
- pid: number,
146
- recordedToken: string,
147
- signal: NodeJS.Signals | number,
148
- options: { platform?: NodeJS.Platform; run?: NativeProcessHelperRunner } = {},
149
- ): ProcessHandleSignalResult {
150
- const platform = options.platform ?? process.platform;
151
- const number = typeof signal === "number" ? signal : constants.signals[signal];
152
- if (
153
- !Number.isInteger(number) ||
154
- number <= 0 ||
155
- (platform === "linux" && !/^l[0-9a-f]{8}\.\d+$/.test(recordedToken)) ||
156
- (platform === "win32" && (!/^w\d+$/.test(recordedToken) || ![9, 15].includes(number)))
157
- )
158
- return { sent: false, verdict: "unknown" };
159
- const result = runHelper(
160
- { mode: "signal", pid, token: recordedToken, signal: number },
161
- platform,
162
- options.run ?? spawnSync,
163
- );
164
- return { sent: result.sent, verdict: result.verdict };
165
- }