harnery 0.45.0 → 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 (235) hide show
  1. package/dist/commander.js +0 -1
  2. package/dist/commands/files.d.ts +2 -2
  3. package/dist/commands/files.d.ts.map +1 -1
  4. package/dist/commands/files.js +4 -7
  5. package/dist/commands/servers.d.ts.map +1 -1
  6. package/dist/commands/servers.js +17 -11
  7. package/dist/commands/tunnel.d.ts +0 -15
  8. package/dist/commands/tunnel.d.ts.map +1 -1
  9. package/dist/commands/tunnel.js +153 -453
  10. package/dist/commands/workflow.d.ts.map +1 -1
  11. package/dist/commands/workflow.js +6 -45
  12. package/dist/core/agents/coord-client.d.ts.map +1 -1
  13. package/dist/core/agents/coord-client.js +0 -18
  14. package/dist/core/agents/live-authority-v3.d.ts +0 -1
  15. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  16. package/dist/core/agents/live-authority-v3.js +0 -11
  17. package/dist/core/agents/live-lifecycle-v3.d.ts +2 -4
  18. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  19. package/dist/core/agents/live-lifecycle-v3.js +7 -28
  20. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  21. package/dist/core/agents/state/heartbeat-writer.js +14 -25
  22. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  23. package/dist/core/agents/state/live-coordination-writer.js +0 -5
  24. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  25. package/dist/core/agents/state/pidmap.js +2 -7
  26. package/dist/core/agents/state/proc-start.d.ts +20 -31
  27. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  28. package/dist/core/agents/state/proc-start.js +37 -70
  29. package/dist/core/agents/state/service-process.d.ts +61 -0
  30. package/dist/core/agents/state/service-process.d.ts.map +1 -0
  31. package/dist/core/agents/state/service-process.js +143 -0
  32. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  33. package/dist/core/agents/state/stale-sweep.js +70 -180
  34. package/dist/core/artifacts/activity.d.ts +4 -1
  35. package/dist/core/artifacts/activity.d.ts.map +1 -1
  36. package/dist/core/artifacts/activity.js +8 -2
  37. package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
  38. package/dist/core/artifacts/delivery-card.js +1 -2
  39. package/dist/core/artifacts/index.d.ts.map +1 -1
  40. package/dist/core/artifacts/index.js +38 -12
  41. package/dist/core/context/index.d.ts +6 -4
  42. package/dist/core/context/index.d.ts.map +1 -1
  43. package/dist/core/context/index.js +194 -256
  44. package/dist/core/context/observation.d.ts +9 -9
  45. package/dist/core/context/observation.d.ts.map +1 -1
  46. package/dist/core/context/observation.js +37 -6
  47. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  48. package/dist/core/events/v3/authority-outbox.js +0 -29
  49. package/dist/core/events/v3/producers/coordination-recorder.d.ts +0 -1
  50. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  51. package/dist/core/events/v3/producers/coordination-recorder.js +0 -7
  52. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  53. package/dist/core/events/v3/producers/recorder.js +64 -77
  54. package/dist/core/events/v3/runtime-build.d.ts +14 -0
  55. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  56. package/dist/core/events/v3/runtime-build.js +44 -0
  57. package/dist/core/governor/index.d.ts +2 -1
  58. package/dist/core/governor/index.d.ts.map +1 -1
  59. package/dist/core/governor/index.js +2 -1
  60. package/dist/core/governor/service-read.d.ts +3 -10
  61. package/dist/core/governor/service-read.d.ts.map +1 -1
  62. package/dist/core/governor/service-read.js +10 -94
  63. package/dist/core/governor/service.d.ts +3 -7
  64. package/dist/core/governor/service.d.ts.map +1 -1
  65. package/dist/core/governor/service.js +17 -182
  66. package/dist/core/hooks/cli.js +78 -48
  67. package/dist/core/hooks/resolve/owner.js +2 -2
  68. package/dist/core/resources/sampler.js +2 -2
  69. package/dist/core/resources/service-status.d.ts +2 -14
  70. package/dist/core/resources/service-status.d.ts.map +1 -1
  71. package/dist/core/resources/service-status.js +3 -23
  72. package/dist/core/resources/service.d.ts +3 -7
  73. package/dist/core/resources/service.d.ts.map +1 -1
  74. package/dist/core/resources/service.js +12 -101
  75. package/dist/core/resources/status.d.ts.map +1 -1
  76. package/dist/core/resources/status.js +3 -2
  77. package/dist/core/semantic/service.d.ts.map +1 -1
  78. package/dist/core/semantic/service.js +9 -2
  79. package/dist/core/servers/index.d.ts +11 -10
  80. package/dist/core/servers/index.d.ts.map +1 -1
  81. package/dist/core/servers/index.js +82 -192
  82. package/dist/core/servers/tunnels.d.ts +3 -5
  83. package/dist/core/servers/tunnels.d.ts.map +1 -1
  84. package/dist/core/servers/tunnels.js +2 -45
  85. package/dist/core/storage/durable-history.js +56 -22
  86. package/dist/core/storage/segments.d.ts.map +1 -1
  87. package/dist/core/storage/segments.js +73 -17
  88. package/dist/core/supervisor/service.d.ts +4 -7
  89. package/dist/core/supervisor/service.d.ts.map +1 -1
  90. package/dist/core/supervisor/service.js +23 -122
  91. package/dist/core/supervisor/services.js +2 -2
  92. package/dist/core/supervisor/status.d.ts +3 -17
  93. package/dist/core/supervisor/status.d.ts.map +1 -1
  94. package/dist/core/supervisor/status.js +7 -70
  95. package/dist/core/work/state.js +7 -7
  96. package/dist/core/workflow/engine.d.ts.map +1 -1
  97. package/dist/core/workflow/engine.js +23 -66
  98. package/dist/core/workflow/index.d.ts +3 -4
  99. package/dist/core/workflow/index.d.ts.map +1 -1
  100. package/dist/core/workflow/index.js +2 -2
  101. package/dist/core/workflow/live-session-v3.d.ts +0 -6
  102. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  103. package/dist/core/workflow/live-session-v3.js +5 -126
  104. package/dist/core/workflow/proof.d.ts +4 -4
  105. package/dist/core/workflow/proof.d.ts.map +1 -1
  106. package/dist/core/workflow/proof.js +34 -86
  107. package/dist/core/workflow/resume-lock.d.ts +17 -0
  108. package/dist/core/workflow/resume-lock.d.ts.map +1 -0
  109. package/dist/core/workflow/resume-lock.js +158 -0
  110. package/dist/core/workflow/run-state.d.ts +0 -21
  111. package/dist/core/workflow/run-state.d.ts.map +1 -1
  112. package/dist/core/workflow/run-state.js +3 -174
  113. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  114. package/dist/core/workflow/spawn-claude.js +0 -19
  115. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  116. package/dist/core/workflow/spawn-codex.js +0 -13
  117. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  118. package/dist/core/workflow/spawn-cursor.js +0 -19
  119. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  120. package/dist/core/workflow/spawn-opencode.js +0 -14
  121. package/dist/core/workflow/types.d.ts +11 -54
  122. package/dist/core/workflow/types.d.ts.map +1 -1
  123. package/dist/core/workflow/workspaces/leases.d.ts +4 -0
  124. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  125. package/dist/core/workflow/workspaces/leases.js +1 -1
  126. package/dist/core/workflow/workspaces/local-git.js +91 -210
  127. package/dist/core/workflow/workspaces/ownership.d.ts +0 -7
  128. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -1
  129. package/dist/core/workflow/workspaces/ownership.js +3 -130
  130. package/dist/core/workflow/workspaces/reclaim.d.ts +4 -7
  131. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  132. package/dist/core/workflow/workspaces/reclaim.js +0 -4
  133. package/dist/core/workflow/workspaces/recovery.d.ts +1 -1
  134. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  135. package/dist/core/workflow/workspaces/recovery.js +1 -1
  136. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  137. package/dist/core/workflow/workspaces/state.js +1 -4
  138. package/dist/lib/identities/assume.d.ts.map +1 -1
  139. package/dist/lib/identities/assume.js +0 -4
  140. package/dist/lib/tunnel/state.d.ts +6 -13
  141. package/dist/lib/tunnel/state.d.ts.map +1 -1
  142. package/dist/lib/tunnel/state.js +12 -58
  143. package/package.json +1 -6
  144. package/src/commander.ts +0 -10
  145. package/src/commands/files.ts +5 -13
  146. package/src/commands/servers.ts +23 -12
  147. package/src/commands/tunnel.ts +166 -533
  148. package/src/commands/workflow.ts +8 -58
  149. package/src/core/agents/coord-client.ts +0 -20
  150. package/src/core/agents/live-authority-v3.ts +0 -17
  151. package/src/core/agents/live-lifecycle-v3.ts +14 -52
  152. package/src/core/agents/state/heartbeat-writer.ts +12 -29
  153. package/src/core/agents/state/live-coordination-writer.ts +0 -29
  154. package/src/core/agents/state/pidmap.ts +2 -15
  155. package/src/core/agents/state/proc-start.ts +41 -69
  156. package/src/core/agents/state/service-process.ts +217 -0
  157. package/src/core/agents/state/stale-sweep.ts +82 -247
  158. package/src/core/artifacts/activity.ts +9 -2
  159. package/src/core/artifacts/delivery-card.ts +1 -7
  160. package/src/core/artifacts/index.ts +45 -12
  161. package/src/core/context/index.ts +207 -272
  162. package/src/core/context/observation.ts +40 -9
  163. package/src/core/events/v3/authority-outbox.ts +0 -56
  164. package/src/core/events/v3/producers/coordination-recorder.ts +0 -15
  165. package/src/core/events/v3/producers/recorder.ts +68 -94
  166. package/src/core/events/v3/runtime-build.ts +62 -0
  167. package/src/core/governor/index.ts +1 -2
  168. package/src/core/governor/service-read.ts +15 -114
  169. package/src/core/governor/service.ts +36 -220
  170. package/src/core/hooks/cli.ts +85 -47
  171. package/src/core/hooks/resolve/owner.ts +2 -2
  172. package/src/core/resources/sampler.ts +3 -2
  173. package/src/core/resources/service-status.ts +6 -35
  174. package/src/core/resources/service.ts +23 -133
  175. package/src/core/resources/status.ts +3 -2
  176. package/src/core/semantic/service.ts +8 -2
  177. package/src/core/servers/index.ts +86 -228
  178. package/src/core/servers/tunnels.ts +3 -55
  179. package/src/core/storage/durable-history.ts +52 -20
  180. package/src/core/storage/segments.ts +79 -17
  181. package/src/core/supervisor/service.ts +33 -155
  182. package/src/core/supervisor/services.ts +2 -2
  183. package/src/core/supervisor/status.ts +10 -85
  184. package/src/core/work/state.ts +7 -7
  185. package/src/core/workflow/engine.ts +22 -74
  186. package/src/core/workflow/index.ts +2 -13
  187. package/src/core/workflow/live-session-v3.ts +5 -146
  188. package/src/core/workflow/proof.ts +40 -104
  189. package/src/core/workflow/resume-lock.ts +179 -0
  190. package/src/core/workflow/run-state.ts +3 -207
  191. package/src/core/workflow/spawn-claude.ts +0 -18
  192. package/src/core/workflow/spawn-codex.ts +0 -13
  193. package/src/core/workflow/spawn-cursor.ts +0 -18
  194. package/src/core/workflow/spawn-opencode.ts +0 -14
  195. package/src/core/workflow/types.ts +11 -41
  196. package/src/core/workflow/workspaces/leases.ts +1 -1
  197. package/src/core/workflow/workspaces/local-git.ts +120 -239
  198. package/src/core/workflow/workspaces/ownership.ts +3 -158
  199. package/src/core/workflow/workspaces/reclaim.ts +3 -9
  200. package/src/core/workflow/workspaces/recovery.ts +1 -1
  201. package/src/core/workflow/workspaces/state.ts +1 -5
  202. package/src/lib/identities/assume.ts +0 -4
  203. package/src/lib/tunnel/state.ts +15 -70
  204. package/dist/commands/turn-rollback.d.ts +0 -5
  205. package/dist/commands/turn-rollback.d.ts.map +0 -1
  206. package/dist/commands/turn-rollback.js +0 -76
  207. package/dist/core/agents/state/process-handle.d.ts +0 -26
  208. package/dist/core/agents/state/process-handle.d.ts.map +0 -1
  209. package/dist/core/agents/state/process-handle.js +0 -121
  210. package/dist/core/agents/state/unchanged-file.d.ts +0 -44
  211. package/dist/core/agents/state/unchanged-file.d.ts.map +0 -1
  212. package/dist/core/agents/state/unchanged-file.js +0 -390
  213. package/dist/core/events/v3/generation-custody.d.ts +0 -5
  214. package/dist/core/events/v3/generation-custody.d.ts.map +0 -1
  215. package/dist/core/events/v3/generation-custody.js +0 -50
  216. package/dist/core/turn-rollback/content.d.ts +0 -53
  217. package/dist/core/turn-rollback/content.d.ts.map +0 -1
  218. package/dist/core/turn-rollback/content.js +0 -396
  219. package/dist/core/turn-rollback/index.d.ts +0 -72
  220. package/dist/core/turn-rollback/index.d.ts.map +0 -1
  221. package/dist/core/turn-rollback/index.js +0 -443
  222. package/dist/core/workflow/failure-evidence.d.ts +0 -16
  223. package/dist/core/workflow/failure-evidence.d.ts.map +0 -1
  224. package/dist/core/workflow/failure-evidence.js +0 -114
  225. package/dist/core/workflow/recovery-audit.d.ts +0 -30
  226. package/dist/core/workflow/recovery-audit.d.ts.map +0 -1
  227. package/dist/core/workflow/recovery-audit.js +0 -116
  228. package/src/commands/turn-rollback.ts +0 -111
  229. package/src/core/agents/state/process-handle.ts +0 -165
  230. package/src/core/agents/state/unchanged-file.ts +0 -456
  231. package/src/core/events/v3/generation-custody.ts +0 -59
  232. package/src/core/turn-rollback/content.ts +0 -499
  233. package/src/core/turn-rollback/index.ts +0 -580
  234. package/src/core/workflow/failure-evidence.ts +0 -141
  235. package/src/core/workflow/recovery-audit.ts +0 -181
@@ -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,111 +0,0 @@
1
- import type { Command } from "commander";
2
- import type { EmitContext, HarneryProgramContext } from "../commander.ts";
3
- import { monorepoRoot } from "../core/agents/index.ts";
4
- import {
5
- applyTurnRollback,
6
- beginTurnRollback,
7
- inspectTurnRollback,
8
- prepareTurnRollback,
9
- sealTurnRollback,
10
- type TurnRollbackPlan,
11
- } from "../core/turn-rollback/index.ts";
12
- import { readJsonRecord } from "../core/workflow/durable-record.ts";
13
- import { readWorkspaceBinding } from "../core/workflow/workspaces/state.ts";
14
-
15
- /** Explicit turn boundaries; hooks cannot infer provider ownership or path scope. */
16
- export function registerTurnRollbackCommand(
17
- program: Command,
18
- emit: EmitContext,
19
- context?: HarneryProgramContext,
20
- ): void {
21
- const command = program
22
- .command("turn-rollback")
23
- .description("Capture and review exact-path content rollback in an owned workspace");
24
- type Options = {
25
- binding: string;
26
- turn?: string;
27
- generation?: string;
28
- path?: string[];
29
- yes?: boolean;
30
- actor?: string;
31
- reason?: string;
32
- plan?: string;
33
- };
34
- const run =
35
- (operation: (coordRoot: string, options: Options) => unknown | Promise<unknown>) =>
36
- async (options: Options) => {
37
- try {
38
- const coordRoot = context?.resolveCoordRoot?.() ?? context?.repoRoot ?? monorepoRoot();
39
- if (!coordRoot) throw new Error("project coordination root is unavailable");
40
- emit.config({ format: "json" });
41
- const result = await operation(coordRoot, options);
42
- emit.data(result);
43
- if (
44
- result &&
45
- typeof result === "object" &&
46
- "status" in result &&
47
- result.status === "unknown"
48
- )
49
- emit.setExitCode(1);
50
- } catch (error) {
51
- emit.error({ code: "turn_rollback_refused", message: String(error) });
52
- emit.setExitCode(1);
53
- }
54
- };
55
- const binding = (root: string, options: Options) => {
56
- const value = readWorkspaceBinding(root, "local-git-worktree", options.binding);
57
- if (!value) throw new Error("immutable workspace binding was not found");
58
- return value;
59
- };
60
- command
61
- .command("begin")
62
- .description("Capture exact literal paths before a turn")
63
- .requiredOption("--binding <id>", "Owned workspace binding ID")
64
- .requiredOption("--turn <id>", "Caller turn identity")
65
- .requiredOption("--generation <number>", "Increasing turn generation")
66
- .requiredOption("--path <file...>", "Exact relative file paths, including planned new files")
67
- .action(
68
- run((root, options) =>
69
- beginTurnRollback(root, {
70
- binding: binding(root, options),
71
- turn: { turn_id: options.turn!, generation: Number(options.generation) },
72
- paths: options.path!,
73
- }),
74
- ),
75
- );
76
- for (const operation of ["seal", "prepare", "show"] as const) {
77
- command
78
- .command(`${operation} <capture-id>`)
79
- .description(`${operation} an exact turn capture`)
80
- .requiredOption("--binding <id>", "Owned workspace binding ID")
81
- .action((id: string, options: Options) =>
82
- run((root, opts) => {
83
- const input = { binding: binding(root, opts), capture_id: id };
84
- return operation === "seal"
85
- ? sealTurnRollback(root, input)
86
- : operation === "prepare"
87
- ? prepareTurnRollback(root, input)
88
- : inspectTurnRollback(root, input);
89
- })(options),
90
- );
91
- }
92
- command
93
- .command("apply")
94
- .description("Preview a saved proposal, or apply it after exact confirmation")
95
- .requiredOption("--binding <id>", "Owned workspace binding ID")
96
- .requiredOption("--plan <file>", "Saved JSON proposal returned by prepare")
97
- .option("--yes", "Apply this exact proposal; preview is the default")
98
- .option("--actor <name>", "Who authorized the restore")
99
- .option("--reason <text>", "Why this turn is being restored")
100
- .action(
101
- run((root, options) =>
102
- applyTurnRollback(root, {
103
- binding: binding(root, options),
104
- plan: readJsonRecord<TurnRollbackPlan>(options.plan!, "rollback proposal"),
105
- yes: options.yes,
106
- actor: options.actor,
107
- reason: options.reason,
108
- }),
109
- ),
110
- );
111
- }
@@ -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
- }