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.
Files changed (87) hide show
  1. package/dist/commander.js +1 -0
  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 +7 -4
  5. package/dist/commands/servers.d.ts.map +1 -1
  6. package/dist/commands/servers.js +11 -17
  7. package/dist/commands/tunnel.d.ts +15 -0
  8. package/dist/commands/tunnel.d.ts.map +1 -1
  9. package/dist/commands/tunnel.js +453 -153
  10. package/dist/commands/turn-rollback.d.ts +5 -0
  11. package/dist/commands/turn-rollback.d.ts.map +1 -0
  12. package/dist/commands/turn-rollback.js +76 -0
  13. package/dist/commands/workflow.d.ts.map +1 -1
  14. package/dist/commands/workflow.js +44 -0
  15. package/dist/core/agents/coord-client.d.ts.map +1 -1
  16. package/dist/core/agents/coord-client.js +18 -0
  17. package/dist/core/agents/state/proc-start.d.ts +5 -6
  18. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  19. package/dist/core/agents/state/proc-start.js +13 -9
  20. package/dist/core/agents/state/process-handle.d.ts +26 -0
  21. package/dist/core/agents/state/process-handle.d.ts.map +1 -0
  22. package/dist/core/agents/state/process-handle.js +121 -0
  23. package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
  24. package/dist/core/artifacts/delivery-card.js +2 -1
  25. package/dist/core/resources/service.d.ts +2 -16
  26. package/dist/core/resources/service.d.ts.map +1 -1
  27. package/dist/core/resources/service.js +1 -78
  28. package/dist/core/semantic/service.d.ts.map +1 -1
  29. package/dist/core/semantic/service.js +2 -9
  30. package/dist/core/servers/index.d.ts +12 -11
  31. package/dist/core/servers/index.d.ts.map +1 -1
  32. package/dist/core/servers/index.js +194 -81
  33. package/dist/core/servers/tunnels.d.ts +5 -3
  34. package/dist/core/servers/tunnels.d.ts.map +1 -1
  35. package/dist/core/servers/tunnels.js +45 -2
  36. package/dist/core/supervisor/service.d.ts +2 -4
  37. package/dist/core/supervisor/service.d.ts.map +1 -1
  38. package/dist/core/turn-rollback/content.d.ts +53 -0
  39. package/dist/core/turn-rollback/content.d.ts.map +1 -0
  40. package/dist/core/turn-rollback/content.js +396 -0
  41. package/dist/core/turn-rollback/index.d.ts +72 -0
  42. package/dist/core/turn-rollback/index.d.ts.map +1 -0
  43. package/dist/core/turn-rollback/index.js +443 -0
  44. package/dist/core/workflow/index.d.ts +4 -2
  45. package/dist/core/workflow/index.d.ts.map +1 -1
  46. package/dist/core/workflow/index.js +2 -1
  47. package/dist/core/workflow/recovery-audit.d.ts +30 -0
  48. package/dist/core/workflow/recovery-audit.d.ts.map +1 -0
  49. package/dist/core/workflow/recovery-audit.js +116 -0
  50. package/dist/core/workflow/run-state.d.ts +19 -2
  51. package/dist/core/workflow/run-state.d.ts.map +1 -1
  52. package/dist/core/workflow/run-state.js +113 -8
  53. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  54. package/dist/core/workflow/workspaces/local-git.js +70 -14
  55. package/dist/core/workflow/workspaces/reclaim.d.ts +6 -1
  56. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  57. package/dist/core/workflow/workspaces/recovery.d.ts +2 -0
  58. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  59. package/dist/core/workflow/workspaces/recovery.js +24 -0
  60. package/dist/lib/tunnel/state.d.ts +13 -6
  61. package/dist/lib/tunnel/state.d.ts.map +1 -1
  62. package/dist/lib/tunnel/state.js +58 -12
  63. package/package.json +6 -1
  64. package/src/commander.ts +10 -0
  65. package/src/commands/files.ts +13 -5
  66. package/src/commands/servers.ts +12 -23
  67. package/src/commands/tunnel.ts +533 -166
  68. package/src/commands/turn-rollback.ts +111 -0
  69. package/src/commands/workflow.ts +59 -0
  70. package/src/core/agents/coord-client.ts +20 -0
  71. package/src/core/agents/state/proc-start.ts +14 -11
  72. package/src/core/agents/state/process-handle.ts +165 -0
  73. package/src/core/artifacts/delivery-card.ts +7 -1
  74. package/src/core/resources/service.ts +3 -88
  75. package/src/core/semantic/service.ts +2 -8
  76. package/src/core/servers/index.ts +234 -85
  77. package/src/core/servers/tunnels.ts +55 -3
  78. package/src/core/supervisor/service.ts +2 -3
  79. package/src/core/turn-rollback/content.ts +499 -0
  80. package/src/core/turn-rollback/index.ts +580 -0
  81. package/src/core/workflow/index.ts +12 -1
  82. package/src/core/workflow/recovery-audit.ts +181 -0
  83. package/src/core/workflow/run-state.ts +137 -8
  84. package/src/core/workflow/workspaces/local-git.ts +86 -15
  85. package/src/core/workflow/workspaces/reclaim.ts +4 -0
  86. package/src/core/workflow/workspaces/recovery.ts +23 -0
  87. 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;AA0EnD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAmvBjF"}
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,CA2CxD;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"}
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
- kill?: (pid: number, signal: NodeJS.Signals | number) => unknown;
78
+ signalProcess?: typeof signalProcessWithHandle;
78
79
  }
79
80
  /**
80
- * Recheck the process generation immediately before sending the signal.
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;AAMH,0EAA0E;AAC1E,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,IAAI,CAAC;AAc9C;;;;;;GAMG;AACH,wBAAgB,eAAe,IAAI,eAAe,CAOjD;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,CAG5D;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,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,MAAM,KAAK,OAAO,CAAC;CAClE;AAED;;;;;GAKG;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,CAUtD;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"}
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
- return startTokenProbe() === "procfs" ? procfsStartToken(pid) : psStartToken(pid);
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
- * Recheck the process generation immediately before sending the signal.
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
- kill(pid, signal);
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":"AAoBA,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"}
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":"AAcA,OAAO,EACL,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,MAAM,2BAA2B,GACnC;IAAE,KAAK,EAAE,SAAS,GAAG,SAAS,CAAC;IAAC,MAAM,EAAE,qBAAqB,CAAA;CAAE,GAC/D;IAAE,KAAK,EAAE,aAAa,GAAG,aAAa,CAAC;IAAC,MAAM,EAAE,qBAAqB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3F,wBAAsB,oBAAoB,CACxC,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GACpC,OAAO,CAAC,qBAAqB,CAAC,CAyChC;AAED,wBAAsB,4BAA4B,CAChD,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,2BAA2B,CAAC,CAoBtC;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;IAC1C,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,MAAM,KAAK,OAAO,CAAC;CAC7D,GACL,yBAAyB,CAuB3B;AAED,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,uBAAuB,GAC7B,OAAO,CAAC,2BAA2B,CAAC,CA4FtC"}
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, platform } from "node:os";
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
  }