harnery 0.44.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commander.js +1 -0
- package/dist/commands/files.d.ts +2 -2
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +7 -4
- package/dist/commands/servers.d.ts.map +1 -1
- package/dist/commands/servers.js +11 -17
- package/dist/commands/tunnel.d.ts +15 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +453 -153
- package/dist/commands/turn-rollback.d.ts +5 -0
- package/dist/commands/turn-rollback.d.ts.map +1 -0
- package/dist/commands/turn-rollback.js +76 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +44 -0
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +18 -0
- package/dist/core/agents/state/proc-start.d.ts +5 -6
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +13 -9
- package/dist/core/agents/state/process-handle.d.ts +26 -0
- package/dist/core/agents/state/process-handle.d.ts.map +1 -0
- package/dist/core/agents/state/process-handle.js +121 -0
- package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
- package/dist/core/artifacts/delivery-card.js +2 -1
- package/dist/core/resources/service.d.ts +2 -16
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +1 -78
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +2 -9
- package/dist/core/servers/index.d.ts +12 -11
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +194 -81
- package/dist/core/servers/tunnels.d.ts +5 -3
- package/dist/core/servers/tunnels.d.ts.map +1 -1
- package/dist/core/servers/tunnels.js +45 -2
- package/dist/core/supervisor/service.d.ts +2 -4
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/turn-rollback/content.d.ts +53 -0
- package/dist/core/turn-rollback/content.d.ts.map +1 -0
- package/dist/core/turn-rollback/content.js +396 -0
- package/dist/core/turn-rollback/index.d.ts +72 -0
- package/dist/core/turn-rollback/index.d.ts.map +1 -0
- package/dist/core/turn-rollback/index.js +443 -0
- package/dist/core/workflow/index.d.ts +4 -2
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +2 -1
- package/dist/core/workflow/recovery-audit.d.ts +30 -0
- package/dist/core/workflow/recovery-audit.d.ts.map +1 -0
- package/dist/core/workflow/recovery-audit.js +116 -0
- package/dist/core/workflow/run-state.d.ts +19 -2
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +113 -8
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +70 -14
- package/dist/core/workflow/workspaces/reclaim.d.ts +6 -1
- package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.d.ts +2 -0
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +24 -0
- package/dist/lib/tunnel/state.d.ts +13 -6
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +58 -12
- package/package.json +6 -1
- package/src/commander.ts +10 -0
- package/src/commands/files.ts +13 -5
- package/src/commands/servers.ts +12 -23
- package/src/commands/tunnel.ts +533 -166
- package/src/commands/turn-rollback.ts +111 -0
- package/src/commands/workflow.ts +59 -0
- package/src/core/agents/coord-client.ts +20 -0
- package/src/core/agents/state/proc-start.ts +14 -11
- package/src/core/agents/state/process-handle.ts +165 -0
- package/src/core/artifacts/delivery-card.ts +7 -1
- package/src/core/resources/service.ts +3 -88
- package/src/core/semantic/service.ts +2 -8
- package/src/core/servers/index.ts +234 -85
- package/src/core/servers/tunnels.ts +55 -3
- package/src/core/supervisor/service.ts +2 -3
- package/src/core/turn-rollback/content.ts +499 -0
- package/src/core/turn-rollback/index.ts +580 -0
- package/src/core/workflow/index.ts +12 -1
- package/src/core/workflow/recovery-audit.ts +181 -0
- package/src/core/workflow/run-state.ts +137 -8
- package/src/core/workflow/workspaces/local-git.ts +86 -15
- package/src/core/workflow/workspaces/reclaim.ts +4 -0
- package/src/core/workflow/workspaces/recovery.ts +23 -0
- package/src/lib/tunnel/state.ts +70 -15
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { EmitContext, HarneryProgramContext } from "../commander.js";
|
|
3
|
+
/** Explicit turn boundaries; hooks cannot infer provider ownership or path scope. */
|
|
4
|
+
export declare function registerTurnRollbackCommand(program: Command, emit: EmitContext, context?: HarneryProgramContext): void;
|
|
5
|
+
//# sourceMappingURL=turn-rollback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-rollback.d.ts","sourceRoot":"","sources":["../../src/commands/turn-rollback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAa1E,qFAAqF;AACrF,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,WAAW,EACjB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,IAAI,CA2FN"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { monorepoRoot } from "../core/agents/index.js";
|
|
2
|
+
import { applyTurnRollback, beginTurnRollback, inspectTurnRollback, prepareTurnRollback, sealTurnRollback, } from "../core/turn-rollback/index.js";
|
|
3
|
+
import { readJsonRecord } from "../core/workflow/durable-record.js";
|
|
4
|
+
import { readWorkspaceBinding } from "../core/workflow/workspaces/state.js";
|
|
5
|
+
/** Explicit turn boundaries; hooks cannot infer provider ownership or path scope. */
|
|
6
|
+
export function registerTurnRollbackCommand(program, emit, context) {
|
|
7
|
+
const command = program
|
|
8
|
+
.command("turn-rollback")
|
|
9
|
+
.description("Capture and review exact-path content rollback in an owned workspace");
|
|
10
|
+
const run = (operation) => async (options) => {
|
|
11
|
+
try {
|
|
12
|
+
const coordRoot = context?.resolveCoordRoot?.() ?? context?.repoRoot ?? monorepoRoot();
|
|
13
|
+
if (!coordRoot)
|
|
14
|
+
throw new Error("project coordination root is unavailable");
|
|
15
|
+
emit.config({ format: "json" });
|
|
16
|
+
const result = await operation(coordRoot, options);
|
|
17
|
+
emit.data(result);
|
|
18
|
+
if (result &&
|
|
19
|
+
typeof result === "object" &&
|
|
20
|
+
"status" in result &&
|
|
21
|
+
result.status === "unknown")
|
|
22
|
+
emit.setExitCode(1);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
emit.error({ code: "turn_rollback_refused", message: String(error) });
|
|
26
|
+
emit.setExitCode(1);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const binding = (root, options) => {
|
|
30
|
+
const value = readWorkspaceBinding(root, "local-git-worktree", options.binding);
|
|
31
|
+
if (!value)
|
|
32
|
+
throw new Error("immutable workspace binding was not found");
|
|
33
|
+
return value;
|
|
34
|
+
};
|
|
35
|
+
command
|
|
36
|
+
.command("begin")
|
|
37
|
+
.description("Capture exact literal paths before a turn")
|
|
38
|
+
.requiredOption("--binding <id>", "Owned workspace binding ID")
|
|
39
|
+
.requiredOption("--turn <id>", "Caller turn identity")
|
|
40
|
+
.requiredOption("--generation <number>", "Increasing turn generation")
|
|
41
|
+
.requiredOption("--path <file...>", "Exact relative file paths, including planned new files")
|
|
42
|
+
.action(run((root, options) => beginTurnRollback(root, {
|
|
43
|
+
binding: binding(root, options),
|
|
44
|
+
turn: { turn_id: options.turn, generation: Number(options.generation) },
|
|
45
|
+
paths: options.path,
|
|
46
|
+
})));
|
|
47
|
+
for (const operation of ["seal", "prepare", "show"]) {
|
|
48
|
+
command
|
|
49
|
+
.command(`${operation} <capture-id>`)
|
|
50
|
+
.description(`${operation} an exact turn capture`)
|
|
51
|
+
.requiredOption("--binding <id>", "Owned workspace binding ID")
|
|
52
|
+
.action((id, options) => run((root, opts) => {
|
|
53
|
+
const input = { binding: binding(root, opts), capture_id: id };
|
|
54
|
+
return operation === "seal"
|
|
55
|
+
? sealTurnRollback(root, input)
|
|
56
|
+
: operation === "prepare"
|
|
57
|
+
? prepareTurnRollback(root, input)
|
|
58
|
+
: inspectTurnRollback(root, input);
|
|
59
|
+
})(options));
|
|
60
|
+
}
|
|
61
|
+
command
|
|
62
|
+
.command("apply")
|
|
63
|
+
.description("Preview a saved proposal, or apply it after exact confirmation")
|
|
64
|
+
.requiredOption("--binding <id>", "Owned workspace binding ID")
|
|
65
|
+
.requiredOption("--plan <file>", "Saved JSON proposal returned by prepare")
|
|
66
|
+
.option("--yes", "Apply this exact proposal; preview is the default")
|
|
67
|
+
.option("--actor <name>", "Who authorized the restore")
|
|
68
|
+
.option("--reason <text>", "Why this turn is being restored")
|
|
69
|
+
.action(run((root, options) => applyTurnRollback(root, {
|
|
70
|
+
binding: binding(root, options),
|
|
71
|
+
plan: readJsonRecord(options.plan, "rollback proposal"),
|
|
72
|
+
yes: options.yes,
|
|
73
|
+
actor: options.actor,
|
|
74
|
+
reason: options.reason,
|
|
75
|
+
})));
|
|
76
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../src/commands/workflow.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../src/commands/workflow.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAmFnD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAqyBjF"}
|
|
@@ -475,6 +475,9 @@ export function registerWorkflowCommand(program, emit) {
|
|
|
475
475
|
.description("Preview reclaim of an inactive owned workspace, or confirm salvage or discard and release.")
|
|
476
476
|
.option("--yes", "Confirm that this commits or discards uncommitted work, then releases")
|
|
477
477
|
.option("--discard", "Throw the uncommitted work away instead of salvaging it")
|
|
478
|
+
.option("--unknown-liveness <sha256>", "Recover unknown evidence pinned by the preview digest; active evidence still refuses")
|
|
479
|
+
.option("--actor <id>", "Actor responsible for an audited recovery")
|
|
480
|
+
.option("--reason <text>", "Reason for an audited recovery")
|
|
478
481
|
.option("--json", "Emit the reclaim preview or confirmed result as JSON")
|
|
479
482
|
.action(async (runId, opts) => {
|
|
480
483
|
const coordRoot = resolveCoordRoot();
|
|
@@ -499,6 +502,13 @@ export function registerWorkflowCommand(program, emit) {
|
|
|
499
502
|
const result = await provider.reclaim(binding, {
|
|
500
503
|
mode: opts.discard ? "discard" : "salvage",
|
|
501
504
|
yes: opts.yes,
|
|
505
|
+
unknownLiveness: opts.unknownLiveness
|
|
506
|
+
? {
|
|
507
|
+
evidenceSha256: opts.unknownLiveness,
|
|
508
|
+
actor: opts.actor,
|
|
509
|
+
reason: opts.reason,
|
|
510
|
+
}
|
|
511
|
+
: undefined,
|
|
502
512
|
});
|
|
503
513
|
if (opts.json) {
|
|
504
514
|
emit.config({ format: "json" });
|
|
@@ -509,6 +519,7 @@ export function registerWorkflowCommand(program, emit) {
|
|
|
509
519
|
const holders = result.lease_holder.map((holder) => `${holder.scope}: ${holder.owner_id ?? "unknown"}${holder.pid ? ` (pid ${holder.pid})` : ""}${holder.detail ? `; ${holder.detail}` : ""}`);
|
|
510
520
|
emit.text(`reclaim preview: ${result.run_id}\n` +
|
|
511
521
|
`mode: ${result.mode}\nliveness: ${result.liveness}\nownership: ${result.ownership}\n` +
|
|
522
|
+
`evidence sha256: ${result.evidence_sha256}\n` +
|
|
512
523
|
`lease holder: ${holders.length > 0 ? holders.join(", ") : "none observed"}\n` +
|
|
513
524
|
`dirty paths: ${result.dirty_paths === null ? "unknown" : result.dirty_paths.length > 0 ? result.dirty_paths.join(", ") : "none"}\n` +
|
|
514
525
|
(result.ownership_detail ? `ownership detail: ${result.ownership_detail}\n` : "") +
|
|
@@ -528,6 +539,39 @@ export function registerWorkflowCommand(program, emit) {
|
|
|
528
539
|
process.exit(1);
|
|
529
540
|
}
|
|
530
541
|
});
|
|
542
|
+
workflow
|
|
543
|
+
.command("unlock <run-id>")
|
|
544
|
+
.description("Preview or explicitly retire one exact resume lock whose local owner is proven dead.")
|
|
545
|
+
.option("--lock-sha256 <sha256>", "Exact lock identity digest returned by the preview")
|
|
546
|
+
.option("--actor <id>", "Actor responsible for the recovery")
|
|
547
|
+
.option("--reason <text>", "Reason for the recovery")
|
|
548
|
+
.option("--yes", "Confirm removal after dead-owner and exact-lock checks")
|
|
549
|
+
.option("--json", "Emit the recovery preview or result as JSON")
|
|
550
|
+
.action(async (runId, opts) => {
|
|
551
|
+
try {
|
|
552
|
+
const coordRoot = resolveCoordRoot();
|
|
553
|
+
if (!coordRoot)
|
|
554
|
+
throw new Error("no .harnery/ coordination root found");
|
|
555
|
+
const { recoverWorkflowResumeLease } = await import("../core/workflow/index.js");
|
|
556
|
+
const result = recoverWorkflowResumeLease(coordRoot, runId, opts);
|
|
557
|
+
if (opts.json) {
|
|
558
|
+
emit.config({ format: "json" });
|
|
559
|
+
emit.data(result);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
emit.text(`resume lock ${result.action}: ${runId}\nowner: ${result.owner_state}\nlock sha256: ${result.lock_sha256 ?? "absent"}\n` +
|
|
563
|
+
(result.action === "preview"
|
|
564
|
+
? "Pass --lock-sha256, --actor, --reason and --yes to confirm exact dead-owner recovery.\n"
|
|
565
|
+
: `recovery: ${result.recovery_id}\n`));
|
|
566
|
+
}
|
|
567
|
+
catch (error) {
|
|
568
|
+
emit.error({
|
|
569
|
+
code: "workflow_resume_unlock_failed",
|
|
570
|
+
message: error instanceof Error ? error.message : String(error),
|
|
571
|
+
});
|
|
572
|
+
process.exit(1);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
531
575
|
const approvals = program
|
|
532
576
|
.command("approval")
|
|
533
577
|
.description("Inspect and resolve durable run-policy approvals.");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coord-client.d.ts","sourceRoot":"","sources":["../../../src/core/agents/coord-client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAeH;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAWhE;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,MAAM,CAI5E;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAOhF;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAE5C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CA+B7E;AAED,iEAAiE;AACjE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE7D;AAeD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGtD;AAiED,qEAAqE;AACrE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIvD;AAED,+EAA+E;AAC/E,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,mFAAmF;AACnF,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAExE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"coord-client.d.ts","sourceRoot":"","sources":["../../../src/core/agents/coord-client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAeH;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAWhE;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,MAAM,CAI5E;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAOhF;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAE5C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CA+B7E;AAED,iEAAiE;AACjE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE7D;AAeD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGtD;AAiED,qEAAqE;AACrE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIvD;AAED,+EAA+E;AAC/E,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,mFAAmF;AACnF,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAExE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA+DxD;AAgCD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAE5C;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,IAAI;IACxC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,iBAAiB,GAAG,aAAa,GAAG,kBAAkB,GAAG,MAAM,CAAC;CAC5F,CAsEA;AAED;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,MAAM,GACX;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAsB5C;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG;IAClD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,QAAQ,GAAG,iBAAiB,GAAG,MAAM,CAAC;CAC/C,CAyBA;AAyBD,sFAAsF;AACtF;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,GAAG,IAAI,CAEtD;AAuCD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEpE;AAED;;;;;;GAMG;AACH,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE,MAAM,GACX;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,CAmB9C;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOpE"}
|
|
@@ -251,7 +251,25 @@ export function parsePidmapRowStartToken(row) {
|
|
|
251
251
|
* a test can hold this one against `processStartToken` and fail on drift.
|
|
252
252
|
*/
|
|
253
253
|
export function pidStartToken(pid) {
|
|
254
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
255
|
+
return null;
|
|
254
256
|
const forced = process.env.HARNERY_PID_PROBE;
|
|
257
|
+
if (process.platform === "win32" && forced !== "procfs" && forced !== "ps") {
|
|
258
|
+
try {
|
|
259
|
+
const out = spawnSync("powershell.exe", [
|
|
260
|
+
"-NoLogo",
|
|
261
|
+
"-NoProfile",
|
|
262
|
+
"-NonInteractive",
|
|
263
|
+
"-Command",
|
|
264
|
+
`$process = [Diagnostics.Process]::GetProcessById(${pid}); try { 'w' + $process.StartTime.ToFileTimeUtc().ToString([Globalization.CultureInfo]::InvariantCulture) } finally { $process.Dispose() }`,
|
|
265
|
+
], { encoding: "utf8", timeout: 10_000, windowsHide: true });
|
|
266
|
+
const token = String(out.stdout ?? "").trim();
|
|
267
|
+
return out.status === 0 && /^w\d+$/.test(token) ? token : null;
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
255
273
|
const useProcfs = forced === "procfs" || (forced !== "ps" && existsSync("/proc/self/stat"));
|
|
256
274
|
// One machine, one probe. Falling back to the other on a read failure would
|
|
257
275
|
// answer in the wrong dialect and read as a recycled pid.
|
|
@@ -30,8 +30,9 @@
|
|
|
30
30
|
* through the caller's locale and timezone, so both are pinned; without that,
|
|
31
31
|
* two callers reading the same live process disagree.
|
|
32
32
|
*/
|
|
33
|
+
import { signalProcessWithHandle } from "./process-handle.js";
|
|
33
34
|
/** Which probe answers "when did this process start?" on this machine. */
|
|
34
|
-
export type StartTokenProbe = "procfs" | "ps";
|
|
35
|
+
export type StartTokenProbe = "procfs" | "ps" | "windows";
|
|
35
36
|
/**
|
|
36
37
|
* Which probe this machine uses, decided once.
|
|
37
38
|
*
|
|
@@ -74,13 +75,11 @@ type ReadProcessStartToken = (pid: number) => string | null;
|
|
|
74
75
|
export declare function verifyProcessGeneration(pid: number, recordedToken: string | null | undefined, readToken?: ReadProcessStartToken): ProcessGenerationVerdict;
|
|
75
76
|
export interface ProcessGenerationSignalDependencies {
|
|
76
77
|
readToken?: ReadProcessStartToken;
|
|
77
|
-
|
|
78
|
+
signalProcess?: typeof signalProcessWithHandle;
|
|
78
79
|
}
|
|
79
80
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* The process can still exit and its PID can be reused between the token read
|
|
83
|
-
* and the signal. Eliminating that race requires a generation-bound OS handle.
|
|
81
|
+
* Observations may refuse a signal, but only the retained native process handle
|
|
82
|
+
* can authorize it. No numeric-PID effect follows the preliminary token read.
|
|
84
83
|
*/
|
|
85
84
|
export declare function signalIfSameGeneration(pid: number, recordedToken: string | null | undefined, signal: NodeJS.Signals | number, dependencies?: ProcessGenerationSignalDependencies): {
|
|
86
85
|
sent: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proc-start.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/proc-start.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;
|
|
1
|
+
{"version":3,"file":"proc-start.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/proc-start.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAKH,OAAO,EAAE,uBAAuB,EAA4B,MAAM,qBAAqB,CAAC;AAExF,0EAA0E;AAC1E,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,IAAI,GAAG,SAAS,CAAC;AAc1D;;;;;;GAMG;AACH,wBAAgB,eAAe,IAAI,eAAe,CAQjD;AAyBD,uDAAuD;AACvD,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc3D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,GAAG,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAiB,GAC7E,MAAM,GAAG,IAAI,CAUf;AAWD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQ5D;AAED,MAAM,MAAM,wBAAwB,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,CAAC;AAExE,KAAK,qBAAqB,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AAE5D,wEAAwE;AACxE,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACxC,SAAS,GAAE,qBAAyC,GACnD,wBAAwB,CAkB1B;AAED,MAAM,WAAW,mCAAmC;IAClD,SAAS,CAAC,EAAE,qBAAqB,CAAC;IAClC,aAAa,CAAC,EAAE,OAAO,uBAAuB,CAAC;CAChD;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACxC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,MAAM,EAC/B,YAAY,GAAE,mCAAwC,GACrD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,wBAAwB,CAAA;CAAE,CAQtD;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAQtE;AAOD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,OAAO,GAAG,UAAU,GAAG,YAAY,CAKrC"}
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
import { spawnSync } from "node:child_process";
|
|
34
34
|
import { existsSync, readFileSync } from "node:fs";
|
|
35
35
|
import { coordEnv } from "../../../lib/env.js";
|
|
36
|
+
import { signalProcessWithHandle, windowsProcessStartToken } from "./process-handle.js";
|
|
36
37
|
/**
|
|
37
38
|
* The `ps` probe is text formatted by `strftime("%c", localtime(...))`, so its
|
|
38
39
|
* output moves with `TZ` and `LC_TIME`. A hook running under `LC_ALL=C` (git
|
|
@@ -54,6 +55,8 @@ export function startTokenProbe() {
|
|
|
54
55
|
const override = coordEnv("PID_PROBE");
|
|
55
56
|
if (override === "procfs" || override === "ps")
|
|
56
57
|
return override;
|
|
58
|
+
if (process.platform === "win32")
|
|
59
|
+
return "windows";
|
|
57
60
|
if (cachedProbe === undefined) {
|
|
58
61
|
cachedProbe = existsSync("/proc/self/stat") ? "procfs" : "ps";
|
|
59
62
|
}
|
|
@@ -149,7 +152,12 @@ function defaultPsRun(pid) {
|
|
|
149
152
|
export function processStartToken(pid) {
|
|
150
153
|
if (!Number.isFinite(pid) || pid <= 0)
|
|
151
154
|
return null;
|
|
152
|
-
|
|
155
|
+
const probe = startTokenProbe();
|
|
156
|
+
return probe === "windows"
|
|
157
|
+
? windowsProcessStartToken(pid)
|
|
158
|
+
: probe === "procfs"
|
|
159
|
+
? procfsStartToken(pid)
|
|
160
|
+
: psStartToken(pid);
|
|
153
161
|
}
|
|
154
162
|
/** A missing or unreadable token never establishes process identity. */
|
|
155
163
|
export function verifyProcessGeneration(pid, recordedToken, readToken = processStartToken) {
|
|
@@ -174,22 +182,18 @@ export function verifyProcessGeneration(pid, recordedToken, readToken = processS
|
|
|
174
182
|
}
|
|
175
183
|
}
|
|
176
184
|
/**
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
* The process can still exit and its PID can be reused between the token read
|
|
180
|
-
* and the signal. Eliminating that race requires a generation-bound OS handle.
|
|
185
|
+
* Observations may refuse a signal, but only the retained native process handle
|
|
186
|
+
* can authorize it. No numeric-PID effect follows the preliminary token read.
|
|
181
187
|
*/
|
|
182
188
|
export function signalIfSameGeneration(pid, recordedToken, signal, dependencies = {}) {
|
|
183
|
-
const kill = dependencies.kill ?? process.kill;
|
|
184
189
|
const verdict = verifyProcessGeneration(pid, recordedToken, dependencies.readToken);
|
|
185
190
|
if (verdict !== "same")
|
|
186
191
|
return { sent: false, verdict };
|
|
187
192
|
try {
|
|
188
|
-
|
|
189
|
-
return { sent: true, verdict };
|
|
193
|
+
return (dependencies.signalProcess ?? signalProcessWithHandle)(pid, recordedToken, signal);
|
|
190
194
|
}
|
|
191
195
|
catch {
|
|
192
|
-
return { sent: false, verdict };
|
|
196
|
+
return { sent: false, verdict: "unknown" };
|
|
193
197
|
}
|
|
194
198
|
}
|
|
195
199
|
/**
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,121 @@
|
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"delivery-card.d.ts","sourceRoot":"","sources":["../../../src/core/artifacts/delivery-card.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"delivery-card.d.ts","sourceRoot":"","sources":["../../../src/core/artifacts/delivery-card.ts"],"names":[],"mappings":"AAqBA,eAAO,MAAM,0BAA0B,2BAA2B,CAAC;AACnE,eAAO,MAAM,gCAAgC,EAAG,CAAU,CAAC;AAC3D,eAAO,MAAM,iCAAiC,IAAI,CAAC;AAEnD,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,KAAK,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,oBAAoB,GAAG,mBAAmB,GAAG,oBAAoB,CAAC;AAE9E,MAAM,WAAW,wBAAwB;IACvC,cAAc,EAAE,OAAO,gCAAgC,CAAC;IACxD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,oBAAoB,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,UAAU,kBAAkB;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,IAAI,CAAC,wBAAwB,EAAE,gBAAgB,CAAC,GACtD,wBAAwB,CAmB1B;AAED,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,GACV,wBAAwB,CAY1B;AAED,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,GACV,wBAAwB,CAU1B;AAED,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,QAAQ,2BAAiD,EACzD,WAAW,GAAE,kBAAuB,GACnC,oBAAoB,CAsDtB;AAED,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,KAAK,GAAG,MAAM,EACpB,IAAI,EAAE,MAAM,GACX,oBAAoB,CAQtB"}
|
|
@@ -5,6 +5,7 @@ import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
|
5
5
|
import { encodeLinkSafeComponent } from "../../lib/local-file-url.js";
|
|
6
6
|
import { findLiveTunnelForOrigin, tunnelServesPaths } from "../../lib/tunnel/state.js";
|
|
7
7
|
import { resolveWebPort } from "../config.js";
|
|
8
|
+
import { createTunnelReadinessVerifier } from "../servers/index.js";
|
|
8
9
|
import { stateFileMode } from "../storage/modes.js";
|
|
9
10
|
import { ARTIFACT_MANIFEST } from "./constants.js";
|
|
10
11
|
import { resolveArtifactRef, showArtifact } from "./index.js";
|
|
@@ -243,7 +244,7 @@ function deliveryLinkBase(repoRoot, environment) {
|
|
|
243
244
|
// The same holds for a dashboard tunnel whose path scope leaves out either
|
|
244
245
|
// route: its gate would refuse every link on the card.
|
|
245
246
|
const tunnel = environment.tunnelUrl === undefined
|
|
246
|
-
? findLiveTunnelForOrigin(webPort, `localhost:${webPort}`, repoRoot)
|
|
247
|
+
? findLiveTunnelForOrigin(webPort, `localhost:${webPort}`, repoRoot, createTunnelReadinessVerifier({ coordRoot: repoRoot }))
|
|
247
248
|
: null;
|
|
248
249
|
const tunnelUrl = environment.tunnelUrl === undefined
|
|
249
250
|
? tunnel && tunnelServesPaths(tunnel, ["/browse", "/files"])
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ProcessGenerationVerdict } from "../agents/state/proc-start.js";
|
|
1
|
+
import { type ProcessGenerationSignalDependencies, type ProcessGenerationVerdict } from "../agents/state/proc-start.js";
|
|
2
2
|
import { type ResourceServiceStatus, type ResourceServiceStatusRecord } from "./contract.js";
|
|
3
3
|
import { sampleResources } from "./sampler.js";
|
|
4
4
|
import { type ResourceProcessDependencies } from "./service-status.js";
|
|
@@ -11,24 +11,10 @@ export interface RunResourceServiceInput {
|
|
|
11
11
|
sample?: typeof sampleResources;
|
|
12
12
|
wait?: (milliseconds: number) => Promise<void>;
|
|
13
13
|
}
|
|
14
|
-
export type EnsureResourceServiceResult = {
|
|
15
|
-
state: "running" | "started";
|
|
16
|
-
status: ResourceServiceStatus;
|
|
17
|
-
} | {
|
|
18
|
-
state: "unsupported" | "unavailable";
|
|
19
|
-
status: ResourceServiceStatus;
|
|
20
|
-
error: string;
|
|
21
|
-
};
|
|
22
|
-
export declare function spawnResourceService(coordRootRaw: string, options?: {
|
|
23
|
-
intervalMs?: number;
|
|
24
|
-
}): Promise<ResourceServiceStatus>;
|
|
25
|
-
export declare function ensureResourceServiceRunning(coordRoot: string): Promise<EnsureResourceServiceResult>;
|
|
26
14
|
export interface ResourceServiceStopResult extends ResourceServiceStatus {
|
|
27
15
|
signal_sent: boolean;
|
|
28
16
|
signal_verdict: ProcessGenerationVerdict;
|
|
29
17
|
}
|
|
30
|
-
export declare function requestResourceServiceStop(coordRootRaw: string, dependencies?: ResourceProcessDependencies &
|
|
31
|
-
kill?: (pid: number, signal: NodeJS.Signals | number) => unknown;
|
|
32
|
-
}): ResourceServiceStopResult;
|
|
18
|
+
export declare function requestResourceServiceStop(coordRootRaw: string, dependencies?: ResourceProcessDependencies & ProcessGenerationSignalDependencies): ResourceServiceStopResult;
|
|
33
19
|
export declare function runResourceService(input: RunResourceServiceInput): Promise<ResourceServiceStatusRecord>;
|
|
34
20
|
//# sourceMappingURL=service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/core/resources/service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/core/resources/service.ts"],"names":[],"mappings":"AAaA,OAAO,EACL,KAAK,mCAAmC,EACxC,KAAK,wBAAwB,EAG9B,MAAM,+BAA+B,CAAC;AAIvC,OAAO,EAGL,KAAK,qBAAqB,EAC1B,KAAK,2BAA2B,EACjC,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EACL,KAAK,2BAA2B,EAGjC,MAAM,qBAAqB,CAAC;AAG7B,eAAO,MAAM,oCAAoC,OAAQ,CAAC;AAY1D,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,eAAe,CAAC;IAChC,IAAI,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,yBAA0B,SAAQ,qBAAqB;IACtE,WAAW,EAAE,OAAO,CAAC;IACrB,cAAc,EAAE,wBAAwB,CAAC;CAC1C;AAED,wBAAgB,0BAA0B,CACxC,YAAY,EAAE,MAAM,EACpB,YAAY,GAAE,2BAA2B,GAAG,mCAAwC,GACnF,yBAAyB,CAuB3B;AAED,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,uBAAuB,GAC7B,OAAO,CAAC,2BAA2B,CAAC,CA4FtC"}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { randomUUID } from "node:crypto";
|
|
3
2
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
4
|
-
import { hostname
|
|
3
|
+
import { hostname } from "node:os";
|
|
5
4
|
import { resolve } from "node:path";
|
|
6
5
|
import { processStartToken, signalIfSameGeneration, } from "../agents/state/proc-start.js";
|
|
7
6
|
import { resolveBinName } from "../config.js";
|
|
@@ -14,72 +13,6 @@ import { resourcePaths, writePrivateJsonAtomic } from "./storage.js";
|
|
|
14
13
|
export const RESOURCE_SERVICE_DEFAULT_INTERVAL_MS = 2_000;
|
|
15
14
|
const RESOURCE_SERVICE_MIN_INTERVAL_MS = 500;
|
|
16
15
|
const RESOURCE_SERVICE_MAX_INTERVAL_MS = 60_000;
|
|
17
|
-
export async function spawnResourceService(coordRootRaw, options = {}) {
|
|
18
|
-
const coordRoot = resolve(coordRootRaw);
|
|
19
|
-
const current = readResourceServiceStatus(coordRoot);
|
|
20
|
-
if (current.liveness === "unknown")
|
|
21
|
-
throw unknownResourceOwnerError();
|
|
22
|
-
if (current.liveness === "running") {
|
|
23
|
-
throw new Error(`resource observer is already running under pid ${current.record?.pid}`);
|
|
24
|
-
}
|
|
25
|
-
const intervalMs = normalizeInterval(options.intervalMs);
|
|
26
|
-
const paths = resourcePaths(coordRoot);
|
|
27
|
-
assertResourceLeaseAvailable(paths.lease);
|
|
28
|
-
mkdirSync(paths.root, { recursive: true, mode: stateDirMode() });
|
|
29
|
-
const harnBin = new URL("../../../bin/harn", import.meta.url).pathname;
|
|
30
|
-
if (!existsSync(harnBin))
|
|
31
|
-
throw new Error(`cannot find harn executable at ${harnBin}`);
|
|
32
|
-
let spawnError;
|
|
33
|
-
const child = spawn(harnBin, ["resources", "service", "daemon", "--root", coordRoot, "--interval-ms", String(intervalMs)], {
|
|
34
|
-
cwd: coordRoot,
|
|
35
|
-
detached: true,
|
|
36
|
-
stdio: ["ignore", "ignore", "ignore"],
|
|
37
|
-
env: {
|
|
38
|
-
...process.env,
|
|
39
|
-
HARNERY_COORD_ROOT_OVERRIDE: coordRoot,
|
|
40
|
-
HARNERY_OUTPUT_SESSION_TEE: "0",
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
child.once("error", (error) => {
|
|
44
|
-
spawnError = error;
|
|
45
|
-
});
|
|
46
|
-
child.unref();
|
|
47
|
-
const deadline = Date.now() + 5_000;
|
|
48
|
-
while (Date.now() < deadline) {
|
|
49
|
-
await delay(50);
|
|
50
|
-
if (spawnError)
|
|
51
|
-
throw spawnError;
|
|
52
|
-
const status = readResourceServiceStatus(coordRoot);
|
|
53
|
-
if (status.running && status.record?.pid === child.pid)
|
|
54
|
-
return status;
|
|
55
|
-
if (child.exitCode !== null)
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
throw new Error(`resource observer failed to start; inspect ${paths.service}`);
|
|
59
|
-
}
|
|
60
|
-
export async function ensureResourceServiceRunning(coordRoot) {
|
|
61
|
-
const status = readResourceServiceStatus(coordRoot);
|
|
62
|
-
if (status.running)
|
|
63
|
-
return { state: "running", status };
|
|
64
|
-
if (platform() !== "linux") {
|
|
65
|
-
return {
|
|
66
|
-
state: "unsupported",
|
|
67
|
-
status,
|
|
68
|
-
error: `resource observer does not yet support ${platform()}`,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
try {
|
|
72
|
-
const started = await spawnResourceService(coordRoot);
|
|
73
|
-
return { state: "started", status: started };
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
return {
|
|
77
|
-
state: "unavailable",
|
|
78
|
-
status: readResourceServiceStatus(coordRoot),
|
|
79
|
-
error: error instanceof Error ? error.message : String(error),
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
16
|
export function requestResourceServiceStop(coordRootRaw, dependencies = {}) {
|
|
84
17
|
const coordRoot = resolve(coordRootRaw);
|
|
85
18
|
const paths = resourcePaths(coordRoot);
|
|
@@ -266,16 +199,6 @@ function readLease(path) {
|
|
|
266
199
|
function leaseIsLive(lease) {
|
|
267
200
|
return resourceProcessLiveness(lease).liveness;
|
|
268
201
|
}
|
|
269
|
-
function assertResourceLeaseAvailable(path) {
|
|
270
|
-
if (!existsSync(path))
|
|
271
|
-
return;
|
|
272
|
-
const lease = readLease(path);
|
|
273
|
-
const liveness = lease ? leaseIsLive(lease) : "unknown";
|
|
274
|
-
if (liveness === "unknown")
|
|
275
|
-
throw unknownResourceOwnerError();
|
|
276
|
-
if (liveness === "running")
|
|
277
|
-
throw new Error(`resource observer lease is held by pid ${lease?.pid}`);
|
|
278
|
-
}
|
|
279
202
|
function unknownResourceOwnerError() {
|
|
280
203
|
return new Error(`resource observer process identity is unknown; run ${resolveBinName()} supervisor stop to write the durable stop request before starting again`);
|
|
281
204
|
}
|