harnery 0.44.0 → 0.45.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/live-authority-v3.d.ts +0 -1
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +0 -11
- package/dist/core/agents/live-lifecycle-v3.d.ts +2 -4
- package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
- package/dist/core/agents/live-lifecycle-v3.js +7 -28
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +14 -25
- package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-writer.js +0 -5
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +0 -5
- 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/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +70 -180
- package/dist/core/artifacts/activity.d.ts +4 -1
- package/dist/core/artifacts/activity.d.ts.map +1 -1
- package/dist/core/artifacts/activity.js +8 -2
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +38 -12
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
- package/dist/core/events/v3/authority-outbox.js +0 -29
- package/dist/core/events/v3/producers/coordination-recorder.d.ts +0 -1
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +0 -7
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +64 -77
- package/dist/core/hooks/cli.js +78 -48
- 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 +4 -0
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +21 -3
- package/dist/core/storage/durable-history.js +56 -22
- package/dist/core/storage/segments.d.ts.map +1 -1
- package/dist/core/storage/segments.js +73 -17
- package/dist/core/supervisor/service.d.ts +2 -4
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/work/state.js +7 -7
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +21 -65
- 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/live-session-v3.d.ts +0 -6
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +5 -126
- package/dist/core/workflow/proof.d.ts +4 -4
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +4 -12
- 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/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +0 -19
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +0 -13
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +0 -19
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +0 -14
- package/dist/core/workflow/types.d.ts +11 -54
- package/dist/core/workflow/types.d.ts.map +1 -1
- 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/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +0 -4
- package/package.json +1 -1
- package/src/commands/workflow.ts +59 -0
- package/src/core/agents/coord-client.ts +20 -0
- package/src/core/agents/live-authority-v3.ts +0 -17
- package/src/core/agents/live-lifecycle-v3.ts +14 -52
- package/src/core/agents/state/heartbeat-writer.ts +12 -29
- package/src/core/agents/state/live-coordination-writer.ts +0 -29
- package/src/core/agents/state/pidmap.ts +0 -13
- package/src/core/agents/state/proc-start.ts +14 -11
- package/src/core/agents/state/process-handle.ts +165 -0
- package/src/core/agents/state/stale-sweep.ts +82 -247
- package/src/core/artifacts/activity.ts +9 -2
- package/src/core/artifacts/index.ts +45 -12
- package/src/core/events/v3/authority-outbox.ts +0 -56
- package/src/core/events/v3/producers/coordination-recorder.ts +0 -15
- package/src/core/events/v3/producers/recorder.ts +68 -94
- package/src/core/hooks/cli.ts +85 -47
- package/src/core/resources/service.ts +3 -88
- package/src/core/semantic/service.ts +2 -8
- package/src/core/servers/index.ts +30 -3
- package/src/core/storage/durable-history.ts +52 -20
- package/src/core/storage/segments.ts +79 -17
- package/src/core/supervisor/service.ts +2 -3
- package/src/core/work/state.ts +7 -7
- package/src/core/workflow/engine.ts +21 -73
- package/src/core/workflow/index.ts +12 -1
- package/src/core/workflow/live-session-v3.ts +5 -146
- package/src/core/workflow/proof.ts +5 -21
- package/src/core/workflow/recovery-audit.ts +181 -0
- package/src/core/workflow/run-state.ts +137 -8
- package/src/core/workflow/spawn-claude.ts +0 -18
- package/src/core/workflow/spawn-codex.ts +0 -13
- package/src/core/workflow/spawn-cursor.ts +0 -18
- package/src/core/workflow/spawn-opencode.ts +0 -14
- package/src/core/workflow/types.ts +11 -41
- 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/identities/assume.ts +0 -4
- package/dist/core/agents/state/unchanged-file.d.ts +0 -44
- package/dist/core/agents/state/unchanged-file.d.ts.map +0 -1
- package/dist/core/agents/state/unchanged-file.js +0 -390
- package/dist/core/events/v3/generation-custody.d.ts +0 -5
- package/dist/core/events/v3/generation-custody.d.ts.map +0 -1
- package/dist/core/events/v3/generation-custody.js +0 -50
- package/dist/core/workflow/failure-evidence.d.ts +0 -16
- package/dist/core/workflow/failure-evidence.d.ts.map +0 -1
- package/dist/core/workflow/failure-evidence.js +0 -114
- package/src/core/agents/state/unchanged-file.ts +0 -456
- package/src/core/events/v3/generation-custody.ts +0 -59
- package/src/core/workflow/failure-evidence.ts +0 -141
|
@@ -24,11 +24,6 @@ import {
|
|
|
24
24
|
writeFileSync,
|
|
25
25
|
} from "node:fs";
|
|
26
26
|
import { dirname, join } from "node:path";
|
|
27
|
-
import {
|
|
28
|
-
assertCurrentGenerationV3,
|
|
29
|
-
withGenerationCustodyV3,
|
|
30
|
-
} from "../../events/v3/generation-custody.ts";
|
|
31
|
-
import { liveInstanceIdV3 } from "../../events/v3/live-routing.ts";
|
|
32
27
|
import { canonicalClaimPath } from "../claim-path.ts";
|
|
33
28
|
import { type Heartbeat, heartbeatPath, readHeartbeat } from "./heartbeat-reader.ts";
|
|
34
29
|
import type { TaskState } from "./session-state.ts";
|
|
@@ -68,15 +63,12 @@ function mutate(
|
|
|
68
63
|
instanceId: string,
|
|
69
64
|
fn: (hb: Heartbeat) => Heartbeat,
|
|
70
65
|
): Heartbeat | null {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
atomicWrite(heartbeatPath(coordRoot, instanceId), JSON.stringify(next, null, 2));
|
|
78
|
-
return next;
|
|
79
|
-
});
|
|
66
|
+
const hb = readHeartbeat(coordRoot, instanceId);
|
|
67
|
+
if (!hb) return null;
|
|
68
|
+
const next = fn(hb);
|
|
69
|
+
next.last_heartbeat = nowIsoSeconds();
|
|
70
|
+
atomicWrite(heartbeatPath(coordRoot, instanceId), JSON.stringify(next, null, 2));
|
|
71
|
+
return next;
|
|
80
72
|
}
|
|
81
73
|
|
|
82
74
|
export function setTask(coordRoot: string, instanceId: string, task: string): Heartbeat | null {
|
|
@@ -108,15 +100,12 @@ export function setIdentityCache(
|
|
|
108
100
|
name: string,
|
|
109
101
|
agentId: string,
|
|
110
102
|
): Heartbeat | null {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
atomicWrite(heartbeatPath(coordRoot, instanceId), JSON.stringify(heartbeat, null, 2));
|
|
118
|
-
return heartbeat;
|
|
119
|
-
});
|
|
103
|
+
const heartbeat = readHeartbeat(coordRoot, instanceId);
|
|
104
|
+
if (!heartbeat) return null;
|
|
105
|
+
heartbeat.name = name;
|
|
106
|
+
heartbeat.agent_id = agentId;
|
|
107
|
+
atomicWrite(heartbeatPath(coordRoot, instanceId), JSON.stringify(heartbeat, null, 2));
|
|
108
|
+
return heartbeat;
|
|
120
109
|
}
|
|
121
110
|
|
|
122
111
|
/** Restore the pool-assigned display name on a generation that was
|
|
@@ -339,12 +328,6 @@ export function clearCoordinationCache(coordRoot: string, instanceId: string): b
|
|
|
339
328
|
}
|
|
340
329
|
|
|
341
330
|
export function healPidmap(coordRoot: string, instanceId: string, pid: number): void {
|
|
342
|
-
withGenerationCustodyV3(coordRoot, [liveInstanceIdV3(instanceId)], () =>
|
|
343
|
-
healPidmapUnderCustody(coordRoot, instanceId, pid),
|
|
344
|
-
);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function healPidmapUnderCustody(coordRoot: string, instanceId: string, pid: number): void {
|
|
348
331
|
const dir = join(coordRoot, ".harnery", "pid-map");
|
|
349
332
|
mkdirSync(dir, { recursive: true });
|
|
350
333
|
const hb = readHeartbeat(coordRoot, instanceId);
|
|
@@ -5,10 +5,6 @@ import {
|
|
|
5
5
|
readCoordinationViewV3,
|
|
6
6
|
requireAuthoritySafeCoordinationViewV3,
|
|
7
7
|
} from "../../events/v3/coordination-view.ts";
|
|
8
|
-
import {
|
|
9
|
-
assertCurrentGenerationV3,
|
|
10
|
-
withGenerationCustodyV3,
|
|
11
|
-
} from "../../events/v3/generation-custody.ts";
|
|
12
8
|
import { liveInstanceIdV3, resolveLiveEventLedgerRouteV3 } from "../../events/v3/live-routing.ts";
|
|
13
9
|
import { type Heartbeat, readHeartbeat } from "./heartbeat-reader.ts";
|
|
14
10
|
import {
|
|
@@ -68,26 +64,6 @@ function materializeLiveCoordinationHeartbeat(
|
|
|
68
64
|
_adapter: Adapter,
|
|
69
65
|
model: string | undefined,
|
|
70
66
|
force: boolean,
|
|
71
|
-
): Heartbeat | null {
|
|
72
|
-
return withGenerationCustodyV3(coordRoot, [liveInstanceIdV3(nativeInstanceId)], () =>
|
|
73
|
-
materializeHeartbeatUnderCustody(
|
|
74
|
-
coordRoot,
|
|
75
|
-
nativeInstanceId,
|
|
76
|
-
nativeSessionId,
|
|
77
|
-
_adapter,
|
|
78
|
-
model,
|
|
79
|
-
force,
|
|
80
|
-
),
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function materializeHeartbeatUnderCustody(
|
|
85
|
-
coordRoot: string,
|
|
86
|
-
nativeInstanceId: string,
|
|
87
|
-
nativeSessionId: string,
|
|
88
|
-
_adapter: Adapter,
|
|
89
|
-
model: string | undefined,
|
|
90
|
-
force: boolean,
|
|
91
67
|
): Heartbeat | null {
|
|
92
68
|
const route = resolveLiveEventLedgerRouteV3(coordRoot);
|
|
93
69
|
if (route.state === "blocked") throw new Error(`event_v3_coordination_view:${route.reason}`);
|
|
@@ -157,11 +133,6 @@ function materializeHeartbeatUnderCustody(
|
|
|
157
133
|
v3_projection_event_id: generation.last_event_id,
|
|
158
134
|
v3_task_state: generation.task_state === "set" ? "set" : "cleared",
|
|
159
135
|
};
|
|
160
|
-
assertCurrentGenerationV3(
|
|
161
|
-
coordRoot,
|
|
162
|
-
liveInstanceIdV3(nativeInstanceId),
|
|
163
|
-
generation.generation_id,
|
|
164
|
-
);
|
|
165
136
|
writeHeartbeatCache(coordRoot, nativeInstanceId, materialized);
|
|
166
137
|
return materialized;
|
|
167
138
|
}
|
|
@@ -24,8 +24,6 @@ import {
|
|
|
24
24
|
writeFileSync,
|
|
25
25
|
} from "node:fs";
|
|
26
26
|
import { dirname, join } from "node:path";
|
|
27
|
-
import { withGenerationCustodyV3 } from "../../events/v3/generation-custody.ts";
|
|
28
|
-
import { liveInstanceIdV3 } from "../../events/v3/live-routing.ts";
|
|
29
27
|
import { checkPidToken, processStartToken } from "./proc-start.ts";
|
|
30
28
|
|
|
31
29
|
/**
|
|
@@ -128,17 +126,6 @@ export function writePidmapRow(
|
|
|
128
126
|
pid: number,
|
|
129
127
|
instanceId: string,
|
|
130
128
|
platform: string,
|
|
131
|
-
): void {
|
|
132
|
-
withGenerationCustodyV3(coordRoot, [liveInstanceIdV3(instanceId)], () =>
|
|
133
|
-
writePidmapRowUnderCustody(coordRoot, pid, instanceId, platform),
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function writePidmapRowUnderCustody(
|
|
138
|
-
coordRoot: string,
|
|
139
|
-
pid: number,
|
|
140
|
-
instanceId: string,
|
|
141
|
-
platform: string,
|
|
142
129
|
): void {
|
|
143
130
|
const dir = join(coordRoot, ".harnery", "pid-map");
|
|
144
131
|
const path = join(dir, String(pid));
|
|
@@ -34,9 +34,10 @@
|
|
|
34
34
|
import { spawnSync } from "node:child_process";
|
|
35
35
|
import { existsSync, readFileSync } from "node:fs";
|
|
36
36
|
import { coordEnv } from "../../../lib/env.ts";
|
|
37
|
+
import { signalProcessWithHandle, windowsProcessStartToken } from "./process-handle.ts";
|
|
37
38
|
|
|
38
39
|
/** Which probe answers "when did this process start?" on this machine. */
|
|
39
|
-
export type StartTokenProbe = "procfs" | "ps";
|
|
40
|
+
export type StartTokenProbe = "procfs" | "ps" | "windows";
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
43
|
* The `ps` probe is text formatted by `strftime("%c", localtime(...))`, so its
|
|
@@ -60,6 +61,7 @@ let cachedBootId: string | null | undefined;
|
|
|
60
61
|
export function startTokenProbe(): StartTokenProbe {
|
|
61
62
|
const override = coordEnv("PID_PROBE");
|
|
62
63
|
if (override === "procfs" || override === "ps") return override;
|
|
64
|
+
if (process.platform === "win32") return "windows";
|
|
63
65
|
if (cachedProbe === undefined) {
|
|
64
66
|
cachedProbe = existsSync("/proc/self/stat") ? "procfs" : "ps";
|
|
65
67
|
}
|
|
@@ -157,7 +159,12 @@ function defaultPsRun(pid: number): { status: number | null; stdout: string } {
|
|
|
157
159
|
*/
|
|
158
160
|
export function processStartToken(pid: number): string | null {
|
|
159
161
|
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
160
|
-
|
|
162
|
+
const probe = startTokenProbe();
|
|
163
|
+
return probe === "windows"
|
|
164
|
+
? windowsProcessStartToken(pid)
|
|
165
|
+
: probe === "procfs"
|
|
166
|
+
? procfsStartToken(pid)
|
|
167
|
+
: psStartToken(pid);
|
|
161
168
|
}
|
|
162
169
|
|
|
163
170
|
export type ProcessGenerationVerdict = "same" | "different" | "unknown";
|
|
@@ -191,14 +198,12 @@ export function verifyProcessGeneration(
|
|
|
191
198
|
|
|
192
199
|
export interface ProcessGenerationSignalDependencies {
|
|
193
200
|
readToken?: ReadProcessStartToken;
|
|
194
|
-
|
|
201
|
+
signalProcess?: typeof signalProcessWithHandle;
|
|
195
202
|
}
|
|
196
203
|
|
|
197
204
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* The process can still exit and its PID can be reused between the token read
|
|
201
|
-
* and the signal. Eliminating that race requires a generation-bound OS handle.
|
|
205
|
+
* Observations may refuse a signal, but only the retained native process handle
|
|
206
|
+
* can authorize it. No numeric-PID effect follows the preliminary token read.
|
|
202
207
|
*/
|
|
203
208
|
export function signalIfSameGeneration(
|
|
204
209
|
pid: number,
|
|
@@ -206,14 +211,12 @@ export function signalIfSameGeneration(
|
|
|
206
211
|
signal: NodeJS.Signals | number,
|
|
207
212
|
dependencies: ProcessGenerationSignalDependencies = {},
|
|
208
213
|
): { sent: boolean; verdict: ProcessGenerationVerdict } {
|
|
209
|
-
const kill = dependencies.kill ?? process.kill;
|
|
210
214
|
const verdict = verifyProcessGeneration(pid, recordedToken, dependencies.readToken);
|
|
211
215
|
if (verdict !== "same") return { sent: false, verdict };
|
|
212
216
|
try {
|
|
213
|
-
|
|
214
|
-
return { sent: true, verdict };
|
|
217
|
+
return (dependencies.signalProcess ?? signalProcessWithHandle)(pid, recordedToken!, signal);
|
|
215
218
|
} catch {
|
|
216
|
-
return { sent: false, verdict };
|
|
219
|
+
return { sent: false, verdict: "unknown" };
|
|
217
220
|
}
|
|
218
221
|
}
|
|
219
222
|
|
|
@@ -0,0 +1,165 @@
|
|
|
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
|
+
}
|