pi-daddy 0.45.0 → 0.46.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 (53) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +355 -45
  3. package/dist/executors/captured-execution.d.ts +2 -2
  4. package/dist/executors/captured-execution.d.ts.map +1 -1
  5. package/dist/executors/captured-execution.js +2 -2
  6. package/dist/executors/captured-execution.js.map +1 -1
  7. package/dist/executors/executor.d.ts.map +1 -1
  8. package/dist/executors/executor.js +4 -2
  9. package/dist/executors/executor.js.map +1 -1
  10. package/dist/executors/herdr-cli.d.ts +2 -0
  11. package/dist/executors/herdr-cli.d.ts.map +1 -1
  12. package/dist/executors/herdr-cli.js +2 -2
  13. package/dist/executors/herdr-cli.js.map +1 -1
  14. package/dist/executors/herdr-launcher.d.ts +2 -0
  15. package/dist/executors/herdr-launcher.d.ts.map +1 -0
  16. package/dist/executors/herdr-launcher.js +123 -0
  17. package/dist/executors/herdr-launcher.js.map +1 -0
  18. package/dist/executors/herdr-owned.d.ts +13 -0
  19. package/dist/executors/herdr-owned.d.ts.map +1 -0
  20. package/dist/executors/herdr-owned.js +229 -0
  21. package/dist/executors/herdr-owned.js.map +1 -0
  22. package/dist/executors/herdr-qualification.d.ts +6 -0
  23. package/dist/executors/herdr-qualification.d.ts.map +1 -0
  24. package/dist/executors/herdr-qualification.js +33 -0
  25. package/dist/executors/herdr-qualification.js.map +1 -0
  26. package/dist/executors/herdr-wire.d.ts +5 -0
  27. package/dist/executors/herdr-wire.d.ts.map +1 -0
  28. package/dist/executors/herdr-wire.js +47 -0
  29. package/dist/executors/herdr-wire.js.map +1 -0
  30. package/dist/governance/runtime-settlement-lock.d.ts +5 -0
  31. package/dist/governance/runtime-settlement-lock.d.ts.map +1 -0
  32. package/dist/governance/runtime-settlement-lock.js +72 -0
  33. package/dist/governance/runtime-settlement-lock.js.map +1 -0
  34. package/dist/governance/runtime-settlement.d.ts +22 -0
  35. package/dist/governance/runtime-settlement.d.ts.map +1 -0
  36. package/dist/governance/runtime-settlement.js +236 -0
  37. package/dist/governance/runtime-settlement.js.map +1 -0
  38. package/extensions/execute-child.ts +98 -74
  39. package/extensions/executor-session.ts +2 -1
  40. package/extensions/grants.ts +3 -0
  41. package/extensions/reload-environment.ts +8 -0
  42. package/extensions/runtime-snapshot.ts +123 -0
  43. package/extensions/session.ts +1 -1
  44. package/package.json +1 -1
  45. package/src/executors/captured-execution.ts +2 -1
  46. package/src/executors/executor.ts +4 -2
  47. package/src/executors/herdr-cli.ts +19 -12
  48. package/src/executors/herdr-launcher.ts +124 -0
  49. package/src/executors/herdr-owned.ts +260 -0
  50. package/src/executors/herdr-qualification.ts +33 -0
  51. package/src/executors/herdr-wire.ts +43 -0
  52. package/src/governance/runtime-settlement-lock.ts +76 -0
  53. package/src/governance/runtime-settlement.ts +271 -0
@@ -0,0 +1,123 @@
1
+ /** Trusted extension event bridge. Tool calls and model-produced text cannot supply runtime facts. */
2
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { realpath } from "node:fs/promises";
4
+ import { openRuntimeSettlement, settlementHash } from "../src/governance/runtime-settlement.ts";
5
+ import { reconcileDelegationCapacity } from "./session-capacity.ts";
6
+ import { agentDir } from "../src/kernel/project-paths.ts";
7
+ import type { GrantsSession } from "./session.ts";
8
+ export const RUNTIME_SNAPSHOT_EVENT = "pi-daddy:runtime-snapshot:v1";
9
+ export function registerRuntimeSnapshot(
10
+ pi: ExtensionAPI,
11
+ session: GrantsSession,
12
+ ): {
13
+ bind(ctx: ExtensionContext): Promise<void>;
14
+ } {
15
+ let bound: { sessionId: string; cwd: string; lifecycle: object } | undefined, problem: string | undefined;
16
+ const generation = {};
17
+ let bindSequence = 0;
18
+ const unsubscribe = pi.events?.on?.(RUNTIME_SNAPSHOT_EVENT, (input: unknown) => {
19
+ const request = input as {
20
+ version?: number;
21
+ requestId?: string;
22
+ sessionId?: string;
23
+ cwd?: string;
24
+ reply?: (value: unknown) => void;
25
+ };
26
+ if (
27
+ request?.version !== 1 ||
28
+ typeof request.requestId !== "string" ||
29
+ request.requestId.length > 512 ||
30
+ typeof request.reply !== "function"
31
+ )
32
+ return;
33
+ if (bound && session.reloadLifecycle.runtimeSnapshotGeneration !== generation) return;
34
+ void (async () => {
35
+ const current = bound,
36
+ lifecycle = session.reloadLifecycle;
37
+ const unavailable = (reason: string) => ({
38
+ version: 1,
39
+ requestId: request.requestId,
40
+ sessionId: current?.sessionId ?? "",
41
+ cwd: current?.cwd ?? "",
42
+ ownerScope: "",
43
+ ownerId: "",
44
+ backend: session.executor.kind,
45
+ qualified: false,
46
+ state: "unknown",
47
+ evidenceDigest: settlementHash({ reason }),
48
+ settledExecutionIds: [],
49
+ outstandingExecutionIds: [],
50
+ reason,
51
+ });
52
+ if (
53
+ !current ||
54
+ current.lifecycle !== lifecycle ||
55
+ request.sessionId !== current.sessionId ||
56
+ request.cwd !== current.cwd ||
57
+ !lifecycle.runtimeSettlement
58
+ ) {
59
+ request.reply!(unavailable(problem ?? "runtime owner is not ready"));
60
+ return;
61
+ }
62
+ try {
63
+ if (Array.isArray(session.capacity.retainedReservations)) await reconcileDelegationCapacity(session);
64
+ const snapshot = await lifecycle.runtimeSettlement.snapshot();
65
+ if (
66
+ bound !== current ||
67
+ session.reloadLifecycle !== lifecycle ||
68
+ lifecycle.runtimeSnapshotGeneration !== generation
69
+ ) {
70
+ request.reply!(unavailable("runtime owner changed during snapshot"));
71
+ return;
72
+ }
73
+ const qualified = !session.executor.refusal && !session.discoveryCleanupFailure && !session.capacityRefusal;
74
+ request.reply!({
75
+ version: 1,
76
+ requestId: request.requestId,
77
+ sessionId: current.sessionId,
78
+ cwd: current.cwd,
79
+ ...snapshot,
80
+ backend: session.executor.kind,
81
+ qualified,
82
+ state: !qualified
83
+ ? "unknown"
84
+ : session.capacity.reserved > 0 && snapshot.state === "idle"
85
+ ? "busy"
86
+ : snapshot.state,
87
+ ...(problem ? { reason: problem } : {}),
88
+ });
89
+ } catch (error) {
90
+ request.reply!(unavailable(String(error)));
91
+ }
92
+ })().catch(() => undefined);
93
+ });
94
+ return {
95
+ async bind(ctx) {
96
+ const sequence = ++bindSequence,
97
+ lifecycle = session.reloadLifecycle;
98
+ lifecycle.runtimeSettlement = undefined;
99
+ bound = undefined;
100
+ problem = undefined;
101
+ try {
102
+ const sessionId = ctx.sessionManager.getSessionId(),
103
+ cwd = await realpath(ctx.cwd);
104
+ if (sequence !== bindSequence || session.reloadLifecycle !== lifecycle) return;
105
+ if (lifecycle.runtimeSnapshotUnsubscribe !== unsubscribe) lifecycle.runtimeSnapshotUnsubscribe?.();
106
+ lifecycle.runtimeSnapshotUnsubscribe = unsubscribe;
107
+ lifecycle.runtimeSnapshotGeneration = generation;
108
+ lifecycle.runtimeSettlements ??= new Map();
109
+ const key = JSON.stringify([sessionId, cwd]);
110
+ let runtime = lifecycle.runtimeSettlements.get(key);
111
+ if (!runtime) {
112
+ runtime = await openRuntimeSettlement(agentDir(), sessionId, cwd);
113
+ lifecycle.runtimeSettlements.set(key, runtime);
114
+ }
115
+ if (sequence !== bindSequence || session.reloadLifecycle !== lifecycle) return;
116
+ lifecycle.runtimeSettlement = runtime;
117
+ bound = { sessionId, cwd, lifecycle: session.reloadLifecycle };
118
+ } catch (error) {
119
+ if (sequence === bindSequence && session.reloadLifecycle === lifecycle) problem = String(error);
120
+ }
121
+ },
122
+ };
123
+ }
@@ -693,7 +693,7 @@ export function createGrantsSession(
693
693
  // Read live off `session.executor` (ADR-0031) rather than a boolean captured in the factory: the probe
694
694
  // has not run when this session object is built, so a captured value would plan `--print` for a session
695
695
  // that turns out to use panes — and `runHerdrPane` refuses a plan containing `--print` by design.
696
- interactive: session.executor.kind === "herdr",
696
+ interactive: false, // Both qualified backends use the same one-shot Pi JSON protocol.
697
697
  ...(approved ? { approved } : {}),
698
698
  }),
699
699
  storeCwd,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-daddy",
3
- "version": "0.45.0",
3
+ "version": "0.46.0",
4
4
  "description": "Capability governance for pi sub-agents: spawn Agent Skills (SKILL.md) definitions whose allowed-tools becomes a grant that can only narrow going down a delegation tree, enforced by pi's own --tools allowlist, with an append-only ledger.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -15,6 +15,7 @@ export async function runCapturedExecution(
15
15
  sessionPath: string;
16
16
  onReadCleanup?: (error: BoundedReadCleanupError) => void;
17
17
  },
18
+ runOwned: typeof runOwnedChild = runOwnedChild,
18
19
  ): Promise<CapturedExecutionResult> {
19
20
  let directory: string;
20
21
  try {
@@ -36,7 +37,7 @@ export async function runCapturedExecution(
36
37
  }
37
38
  const capture = new ChildFinalCapture(request.onOutput, request.onReadCleanup);
38
39
  const args = [...request.args.slice(0, -1), "--mode", "json", request.args.at(-1)!];
39
- const output = await runOwnedChild({
40
+ const output = await runOwned({
40
41
  ...request,
41
42
  args,
42
43
  ownershipDir: join(directory, "ownership"),
@@ -44,15 +44,17 @@ export function needsProbe(raw: string | undefined): boolean {
44
44
  }
45
45
 
46
46
  export const HERDR_UNQUALIFIED_REASON =
47
- "Herdr governed execution is not qualified for complete current finals and subtree cleanup in this release. " +
47
+ "Herdr governed execution is not qualified for complete current finals and subtree cleanup on the detected transport. " +
48
48
  "Delegation is refused; set PI_DADDY_HERDR=0 explicitly to select captured execution. No backend fallback occurs.";
49
49
 
50
50
  export function chooseExecutor(raw: string | undefined, probe: HerdrProbe | null): ExecutorChoice {
51
51
  const choice = selectExecutor(raw, probe);
52
52
  if (choice.kind !== "herdr") return choice;
53
+ if (!choice.refusal && probe?.qualified === true)
54
+ return { ...choice, disclosure: `${choice.disclosure} — qualified native ownership` };
53
55
  return {
54
56
  ...choice,
55
- refusal: [choice.refusal, HERDR_UNQUALIFIED_REASON].filter(Boolean).join(" "),
57
+ refusal: [choice.refusal, HERDR_UNQUALIFIED_REASON, probe?.qualificationReason].filter(Boolean).join(" "),
56
58
  disclosure: `${choice.disclosure} — unqualified; governed delegation will refuse`,
57
59
  };
58
60
  }
@@ -19,17 +19,22 @@ export type HerdrExec = (args: string[]) => Promise<{ code: number | null; stdou
19
19
 
20
20
  export const defaultExec: HerdrExec = (args) =>
21
21
  new Promise((settle) => {
22
- execFile("herdr", args, { maxBuffer: 32 * 1024 * 1024 }, (error, stdout, stderr) => {
23
- const raw = (error as { code?: unknown } | null)?.code;
24
- const code = typeof raw === "number" ? raw : error ? 1 : 0;
25
- // **A string `code` is a spawn failure, and it used to be thrown away.** `ENOENT` — herdr not installed —
26
- // arrives as `code: "ENOENT"`, so the numeric test failed, the message was dropped, and an operator with
27
- // `PI_DADDY_HERDR=1` on a machine without herdr was told *"herdr is not answering (unparseable herdr
28
- // reply: (no output))"* rather than that the binary is missing. Rule 8 wants the loud version, and this is
29
- // the first diagnostic such an operator meets.
30
- const spawnFailure = typeof raw === "string" ? `herdr could not be run (${raw}): ${error?.message ?? ""}` : "";
31
- settle({ code, stdout: String(stdout), stderr: spawnFailure || String(stderr) });
32
- });
22
+ execFile(
23
+ "herdr",
24
+ args,
25
+ { maxBuffer: 32 * 1024 * 1024, timeout: 10000, killSignal: "SIGKILL" },
26
+ (error, stdout, stderr) => {
27
+ const raw = (error as { code?: unknown } | null)?.code;
28
+ const code = typeof raw === "number" ? raw : error ? 1 : 0;
29
+ // **A string `code` is a spawn failure, and it used to be thrown away.** `ENOENT` — herdr not installed —
30
+ // arrives as `code: "ENOENT"`, so the numeric test failed, the message was dropped, and an operator with
31
+ // `PI_DADDY_HERDR=1` on a machine without herdr was told *"herdr is not answering (unparseable herdr
32
+ // reply: (no output))"* rather than that the binary is missing. Rule 8 wants the loud version, and this is
33
+ // the first diagnostic such an operator meets.
34
+ const spawnFailure = typeof raw === "string" ? `herdr could not be run (${raw}): ${error?.message ?? ""}` : "";
35
+ settle({ code, stdout: String(stdout), stderr: spawnFailure || String(stderr) });
36
+ },
37
+ );
33
38
  });
34
39
 
35
40
  /**
@@ -44,7 +49,7 @@ export function parseReply(reply: { stdout: string; stderr: string }): {
44
49
  error?: string;
45
50
  } {
46
51
  try {
47
- const parsed = JSON.parse(reply.stdout) as {
52
+ const parsed = JSON.parse(reply.stdout.trim() || reply.stderr.trim()) as {
48
53
  result?: Record<string, unknown>;
49
54
  error?: { message?: string; code?: string };
50
55
  };
@@ -63,6 +68,8 @@ export const PROBE_TIMEOUT_MS = 2000;
63
68
 
64
69
  export interface HerdrProbe {
65
70
  ok: boolean;
71
+ qualified?: boolean;
72
+ qualificationReason?: string;
66
73
  /** herdr's own words when it is not reachable. Carried so the disclosure line can name the reason. */
67
74
  error?: string;
68
75
  }
@@ -0,0 +1,124 @@
1
+ /** Packaged pane entrypoint. Parent liveness is a private socket, native worker liveness is its control pipe. */
2
+ import { stat } from "node:fs/promises";
3
+ import { processTreeActivity } from "./process-activity.ts";
4
+ import { createConnection } from "node:net";
5
+ import { runOwnedChild, type OwnedChildRunRequest } from "./owned-worker.ts";
6
+ import { wire } from "./herdr-wire.ts";
7
+ export async function launchHerdrWorker(socketPath: string, token: string): Promise<void> {
8
+ const socket = createConnection(socketPath);
9
+ const abort = new AbortController();
10
+ let started = false,
11
+ authenticated = false,
12
+ acknowledge: (() => void) | undefined,
13
+ rejectGate: ((error: Error) => void) | undefined;
14
+ const lost = (error: Error) => {
15
+ abort.abort();
16
+ rejectGate?.(error);
17
+ };
18
+ socket.on("close", () => lost(Error("Herdr coordinator connection closed")));
19
+ const send = wire(
20
+ socket,
21
+ (message) => {
22
+ if (message?.type === "cancel") {
23
+ abort.abort();
24
+ rejectGate?.(Error("Herdr coordinator cancelled startup"));
25
+ return;
26
+ }
27
+ if (message?.type === "start" && authenticated) {
28
+ acknowledge?.();
29
+ acknowledge = undefined;
30
+ rejectGate = undefined;
31
+ return;
32
+ }
33
+ if (message?.type === "display" && authenticated && typeof message.text === "string") {
34
+ process.stdout.write(message.text);
35
+ return;
36
+ }
37
+ if (message?.type !== "request" || started) {
38
+ lost(Error("invalid Herdr coordinator request"));
39
+ socket.destroy();
40
+ return;
41
+ }
42
+ started = true;
43
+ authenticated = true;
44
+ const request = message.request as OwnedChildRunRequest & { sessionPath?: string };
45
+ let childPid: number | undefined;
46
+ void (async () => {
47
+ try {
48
+ const result = await runOwnedChild({
49
+ ...request,
50
+ signal: abort.signal,
51
+ activityProbe: async () => {
52
+ const file = request.sessionPath
53
+ ? await stat(request.sessionPath)
54
+ .then((s) => String(s.size) + ":" + s.mtimeMs)
55
+ .catch(() => undefined)
56
+ : undefined;
57
+ const tree = childPid === undefined ? undefined : await processTreeActivity(childPid);
58
+ return file === undefined && tree === undefined ? undefined : String(file) + "|" + tree;
59
+ },
60
+ onOwnership: (identity) =>
61
+ new Promise<void>((resolve, reject) => {
62
+ acknowledge = resolve;
63
+ rejectGate = reject;
64
+ if (abort.signal.aborted) {
65
+ reject(Error("Herdr coordinator unavailable before ownership gate"));
66
+ return;
67
+ }
68
+ send({ type: "ownership", identity });
69
+ }),
70
+ onObservation: (stream, bytes) => {
71
+ try {
72
+ const input = Buffer.from(bytes);
73
+ for (let offset = 0; offset < input.length; offset += 65536)
74
+ send({ type: "bytes", stream, data: input.subarray(offset, offset + 65536).toString("base64") });
75
+ } catch (error) {
76
+ lost(error instanceof Error ? error : Error(String(error)));
77
+ }
78
+ },
79
+ onStreamEnd: (stream) => {
80
+ try {
81
+ send({ type: "stream-end", stream });
82
+ } catch {}
83
+ },
84
+ onSpawn: (pid) => {
85
+ childPid = pid;
86
+ send({ type: "spawned", pid });
87
+ process.stdout.write("pi-daddy: governed child running\n");
88
+ },
89
+ });
90
+ if (!socket.destroyed) {
91
+ send({ type: "result", result });
92
+ socket.end();
93
+ }
94
+ process.stdout.write("\npi-daddy: " + result.cleanup.state + "\n");
95
+ } catch (error) {
96
+ if (!socket.destroyed) {
97
+ try {
98
+ send({ type: "failure", reason: String(error) });
99
+ } finally {
100
+ socket.end();
101
+ }
102
+ }
103
+ }
104
+ })();
105
+ },
106
+ lost,
107
+ );
108
+ await new Promise<void>((resolve, reject) => {
109
+ socket.once("connect", () => {
110
+ try {
111
+ send({ type: "hello", token });
112
+ } catch (error) {
113
+ reject(error);
114
+ }
115
+ });
116
+ socket.once("close", resolve);
117
+ socket.once("error", reject);
118
+ });
119
+ }
120
+ if (/herdr-launcher\.(?:js|ts)$/.test(process.argv[1] ?? "")) {
121
+ const [, , path, token] = process.argv;
122
+ if (!path || !token) throw Error("Herdr launcher requires its private socket and nonce");
123
+ await launchHerdrWorker(path, token);
124
+ }
@@ -0,0 +1,260 @@
1
+ /** Governed work runs in its own Herdr pane, under the same independently settling native owner as captured work. */
2
+ import { createServer, type Socket } from "node:net";
3
+ import { randomUUID } from "node:crypto";
4
+ import { chmod, mkdtemp, rm } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import type { OwnedChildRunRequest, OwnedChildRunResult } from "./owned-worker.ts";
9
+ import type { CapturedWorkerIdentity } from "../kernel/captured-worker-contract.ts";
10
+ import {
11
+ isCapturedWorkerIdentity,
12
+ readCapturedWorkerReceipt,
13
+ validateLiveWorker,
14
+ } from "../governance/captured-worker-record.ts";
15
+ import { defaultExec, parseReply, type HerdrExec } from "./herdr-cli.ts";
16
+ import { WORKER_SHA256 } from "./worker-artifact.ts";
17
+ import { wire } from "./herdr-wire.ts";
18
+ const quote = (value: string) => "'" + value.replaceAll("'", "'\\''") + "'";
19
+ export interface HerdrOwnedOptions {
20
+ exec?: HerdrExec;
21
+ workspace?: string;
22
+ keepPane?: boolean;
23
+ onPane?: (paneId: string, tabId: string) => void;
24
+ onDisplay?: (display: (text: string) => void) => void;
25
+ /** Tests may select a freshly built launcher. Production always uses the packaged build. */
26
+ launcherPath?: string;
27
+ }
28
+ export async function runHerdrOwned(
29
+ request: OwnedChildRunRequest,
30
+ options: HerdrOwnedOptions = {},
31
+ ): Promise<OwnedChildRunResult> {
32
+ const exec = options.exec ?? defaultExec;
33
+ const directory = await mkdtemp(join(tmpdir(), "pd-herdr-"));
34
+ const path = join(directory, "owner.sock"),
35
+ token = randomUUID();
36
+ const launcher =
37
+ options.launcherPath ?? fileURLToPath(new URL("../../dist/executors/herdr-launcher.js", import.meta.url));
38
+ const helper = fileURLToPath(new URL("../../native/linux-x64/worker", import.meta.url));
39
+ let socket: Socket | undefined, send: ((value: unknown) => void) | undefined;
40
+ let identity: CapturedWorkerIdentity | undefined, outcome: OwnedChildRunResult | undefined, error: string | undefined;
41
+ let tabId: string | undefined,
42
+ entered = false,
43
+ done = false,
44
+ authenticated = false;
45
+ let finish!: () => void;
46
+ const startupTimer = setTimeout(
47
+ () => {
48
+ fail("Herdr launcher did not connect before startup deadline");
49
+ socket?.destroy();
50
+ finish();
51
+ },
52
+ Math.min(request.timeoutMs ?? 10000, 10000),
53
+ );
54
+ const completed = new Promise<void>((resolve) => {
55
+ finish = resolve;
56
+ });
57
+ const abort = () => {
58
+ try {
59
+ send?.({ type: "cancel" });
60
+ } catch {}
61
+ };
62
+ const fail = (cause: unknown) => {
63
+ error ??= String(cause);
64
+ abort();
65
+ };
66
+ const server = createServer((candidate) => {
67
+ if (socket) {
68
+ candidate.destroy();
69
+ return;
70
+ }
71
+ socket = candidate;
72
+ send = wire(
73
+ candidate,
74
+ (message) => {
75
+ if (!authenticated) {
76
+ if (message?.type !== "hello" || message.token !== token) throw Error("Herdr launcher nonce mismatch");
77
+ authenticated = true;
78
+ clearTimeout(startupTimer);
79
+ const {
80
+ signal: _signal,
81
+ onOwnership: _owner,
82
+ onSpawn: _spawn,
83
+ onOutput: _out,
84
+ onObservation: _observation,
85
+ onStreamEnd: _end,
86
+ activityProbe: _probe,
87
+ ...serializable
88
+ } = request;
89
+ send!({ type: "request", request: serializable });
90
+ return;
91
+ }
92
+ if (message?.type === "ownership") {
93
+ if (identity || !isCapturedWorkerIdentity(message.identity)) throw Error("ambiguous Herdr worker ownership");
94
+ const actual = message.identity as CapturedWorkerIdentity;
95
+ if (
96
+ actual.executionId !== request.executionId ||
97
+ actual.root !== request.cwd ||
98
+ actual.helperSha256 !== WORKER_SHA256 ||
99
+ actual.ownershipPath !== join(request.ownershipDir, "ownership.json")
100
+ )
101
+ throw Error("Herdr worker identity differs from requested execution");
102
+ identity = actual;
103
+ void (async () => {
104
+ try {
105
+ await validateLiveWorker(actual, helper);
106
+ await request.onOwnership?.(actual);
107
+ if (!done && !request.signal?.aborted && !error) {
108
+ request.onSpawn?.(actual.workerPid);
109
+ send!({ type: "start" });
110
+ } else abort();
111
+ } catch (cause) {
112
+ fail(cause);
113
+ abort();
114
+ }
115
+ })();
116
+ return;
117
+ }
118
+ if (
119
+ message?.type === "bytes" &&
120
+ ["stdout", "stderr"].includes(message.stream) &&
121
+ typeof message.data === "string"
122
+ ) {
123
+ if (
124
+ !identity ||
125
+ message.data.length > 90000 ||
126
+ !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(message.data)
127
+ )
128
+ throw Error("invalid Herdr output frame");
129
+ try {
130
+ request.onObservation?.(message.stream, Buffer.from(message.data, "base64"));
131
+ } catch {}
132
+ return;
133
+ }
134
+ if (message?.type === "stream-end" && ["stdout", "stderr"].includes(message.stream)) {
135
+ try {
136
+ request.onStreamEnd?.(message.stream);
137
+ } catch {}
138
+ return;
139
+ }
140
+ if (message?.type === "spawned" && identity && message.pid === identity.workerPid) {
141
+ return;
142
+ }
143
+ if (message?.type === "result" && !outcome) {
144
+ outcome = message.result;
145
+ return;
146
+ }
147
+ if (message?.type === "failure") {
148
+ fail(message.reason);
149
+ return;
150
+ }
151
+ throw Error("invalid Herdr worker transport message");
152
+ },
153
+ fail,
154
+ );
155
+ options.onDisplay?.((text) => {
156
+ if (authenticated && send) {
157
+ for (let at = 0; at < text.length; at += 16000) send({ type: "display", text: text.slice(at, at + 16000) });
158
+ }
159
+ });
160
+ candidate.on("close", finish);
161
+ });
162
+ const timeout = setTimeout(
163
+ () => {
164
+ fail("Herdr execution exceeded its transport deadline");
165
+ socket?.destroy();
166
+ finish();
167
+ },
168
+ (request.timeoutMs ?? 3600000) + Math.min(request.killGraceMs ?? 1500, 60000) + 7000,
169
+ );
170
+ request.signal?.addEventListener("abort", abort, { once: true });
171
+ try {
172
+ await new Promise<void>((resolve, reject) => {
173
+ server.once("error", reject);
174
+ server.listen(path, resolve);
175
+ });
176
+ await chmod(path, 0o600);
177
+ if (request.signal?.aborted || (request.hardDeadlineAt !== undefined && request.hardDeadlineAt <= Date.now()))
178
+ return {
179
+ code: null,
180
+ text: "",
181
+ truncated: false,
182
+ timedOut: false,
183
+ aborted: true,
184
+ cleanup: { state: "not-started", reason: "cancelled before Herdr pane launch" },
185
+ };
186
+ const created = parseReply(
187
+ await exec([
188
+ "tab",
189
+ "create",
190
+ "--cwd",
191
+ request.cwd,
192
+ "--label",
193
+ "pi-daddy governed child",
194
+ "--no-focus",
195
+ ...(options.workspace ? ["--workspace", options.workspace] : []),
196
+ ]),
197
+ );
198
+ if (created.error) throw Error(created.error);
199
+ const pane = created.result?.root_pane as { pane_id?: string; tab_id?: string } | undefined;
200
+ tabId = pane?.tab_id;
201
+ if (!pane?.pane_id || !tabId) throw Error("Herdr creation returned no exact pane/tab identity");
202
+ options.onPane?.(pane.pane_id, tabId);
203
+ if (error || request.signal?.aborted) throw Error(error ?? "cancelled before Herdr command launch");
204
+ entered = true;
205
+ const launchReply = await exec([
206
+ "pane",
207
+ "run",
208
+ pane.pane_id,
209
+ (options.keepPane ? "" : "exec ") + [process.execPath, launcher, path, token].map(quote).join(" "),
210
+ ]);
211
+ if (launchReply.code !== 0) throw Error(parseReply(launchReply).error ?? "Herdr pane launch failed");
212
+ if (launchReply.stdout.trim()) {
213
+ const launched = parseReply(launchReply);
214
+ if (launched.error) throw Error(launched.error);
215
+ }
216
+ await completed;
217
+ if (outcome?.cleanup?.state === "not-started" && !identity) return outcome;
218
+ if (!outcome || !identity) throw Error(error ?? "Herdr launcher closed without a complete result/ownership");
219
+ const receipt = await readCapturedWorkerReceipt(identity);
220
+ if (!receipt) throw Error("Herdr worker supplied no independently verified cleanup receipt");
221
+ return { ...outcome, cleanup: { state: "settled", identity, receipt }, ...(error ? { spawnError: error } : {}) };
222
+ } catch (cause) {
223
+ fail(cause);
224
+ socket?.destroy();
225
+ // Lost transport is never evidence of cleanup. Reconcile only the original bound receipt.
226
+ let receipt = identity ? await readCapturedWorkerReceipt(identity) : null;
227
+ const deadline = Date.now() + Math.min(request.killGraceMs ?? 1500, 60000) + 5500;
228
+ while (identity && !receipt && Date.now() < deadline) {
229
+ await new Promise((resolve) => setTimeout(resolve, 50));
230
+ receipt = await readCapturedWorkerReceipt(identity);
231
+ }
232
+ return {
233
+ code: receipt?.workerCode ?? null,
234
+ text: "",
235
+ truncated: false,
236
+ timedOut: false,
237
+ aborted: Boolean(request.signal?.aborted),
238
+ spawnError: error ?? String(cause),
239
+ cleanup:
240
+ identity && receipt
241
+ ? { state: "settled", identity, receipt }
242
+ : entered
243
+ ? { state: "unknown", ...(identity ? { identity } : {}), reason: error ?? String(cause) }
244
+ : { state: "not-started", reason: error ?? String(cause) },
245
+ };
246
+ } finally {
247
+ done = true;
248
+ clearTimeout(timeout);
249
+ clearTimeout(startupTimer);
250
+ request.signal?.removeEventListener("abort", abort);
251
+ socket?.destroy();
252
+ await new Promise<void>((resolve) => server.close(() => resolve()));
253
+ if (tabId && !options.keepPane) {
254
+ const closed = parseReply(await exec(["tab", "close", tabId]));
255
+ if (closed.error && !/tab .* not found|tab_not_found/.test(closed.error))
256
+ console.error("pi-daddy: settled worker pane could not close:", closed.error);
257
+ }
258
+ await rm(directory, { recursive: true, force: true });
259
+ }
260
+ }
@@ -0,0 +1,33 @@
1
+ /** Measured transport matrix. Reachability alone never qualifies a backend or changes execution location. */
2
+ import { defaultExec, type HerdrExec, type HerdrProbe } from "./herdr-cli.ts";
3
+ export const QUALIFIED_HERDR_VERSION = "0.8.2";
4
+ export const QUALIFIED_HERDR_PROTOCOL = 20;
5
+ export async function qualifyHerdr(probe: HerdrProbe, exec: HerdrExec = defaultExec): Promise<HerdrProbe> {
6
+ if (!probe.ok) return probe;
7
+ try {
8
+ if (process.platform !== "linux" || process.arch !== "x64") throw Error("Herdr owned execution requires Linux x64");
9
+ const [client, server] = await Promise.all([exec(["--version"]), exec(["status", "server"])]);
10
+ if (client.code !== 0 || client.stdout.trim() !== "herdr " + QUALIFIED_HERDR_VERSION)
11
+ throw Error("Herdr client version is not qualified");
12
+ const fields = Object.fromEntries(
13
+ server.stdout
14
+ .trim()
15
+ .split("\n")
16
+ .map((line) => {
17
+ const at = line.indexOf(":");
18
+ return [line.slice(0, at).trim(), line.slice(at + 1).trim()];
19
+ }),
20
+ );
21
+ if (
22
+ server.code !== 0 ||
23
+ fields.status !== "running" ||
24
+ fields.version !== QUALIFIED_HERDR_VERSION ||
25
+ fields.protocol !== String(QUALIFIED_HERDR_PROTOCOL) ||
26
+ fields.compatible !== "yes"
27
+ )
28
+ throw Error("Herdr server version/protocol is not qualified");
29
+ return { ...probe, qualified: true };
30
+ } catch (error) {
31
+ return { ...probe, qualified: false, qualificationReason: String(error) };
32
+ }
33
+ }