pi-daddy 0.44.4 → 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 (88) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +383 -25
  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/dist/kernel/definition-sources.d.ts +13 -0
  39. package/dist/kernel/definition-sources.d.ts.map +1 -0
  40. package/dist/kernel/definition-sources.js +2 -0
  41. package/dist/kernel/definition-sources.js.map +1 -0
  42. package/dist/kernel/definitions.d.ts +2 -0
  43. package/dist/kernel/definitions.d.ts.map +1 -1
  44. package/dist/kernel/definitions.js.map +1 -1
  45. package/dist/kernel/env-names.d.ts +2 -0
  46. package/dist/kernel/env-names.d.ts.map +1 -1
  47. package/dist/kernel/env-names.js +3 -0
  48. package/dist/kernel/env-names.js.map +1 -1
  49. package/dist/kernel/propagation.d.ts +1 -1
  50. package/dist/kernel/propagation.d.ts.map +1 -1
  51. package/dist/kernel/propagation.js +2 -0
  52. package/dist/kernel/propagation.js.map +1 -1
  53. package/dist/kernel/selected-definitions.d.ts +1 -1
  54. package/dist/kernel/selected-definitions.d.ts.map +1 -1
  55. package/dist/kernel/selected-definitions.js +19 -10
  56. package/dist/kernel/selected-definitions.js.map +1 -1
  57. package/dist/products/public-evidence.d.ts +62 -0
  58. package/dist/products/public-evidence.d.ts.map +1 -0
  59. package/dist/products/public-evidence.js +116 -0
  60. package/dist/products/public-evidence.js.map +1 -0
  61. package/extensions/definition-describe.ts +4 -1
  62. package/extensions/delegate-chain.ts +21 -15
  63. package/extensions/delegation.ts +57 -27
  64. package/extensions/execute-child.ts +98 -74
  65. package/extensions/execution-evidence.ts +10 -1
  66. package/extensions/executor-session.ts +2 -1
  67. package/extensions/grants.ts +3 -0
  68. package/extensions/public-evidence.ts +90 -0
  69. package/extensions/reload-environment.ts +8 -0
  70. package/extensions/run-delegation.ts +20 -4
  71. package/extensions/runtime-snapshot.ts +123 -0
  72. package/extensions/session.ts +11 -2
  73. package/package.json +1 -1
  74. package/src/executors/captured-execution.ts +2 -1
  75. package/src/executors/executor.ts +4 -2
  76. package/src/executors/herdr-cli.ts +19 -12
  77. package/src/executors/herdr-launcher.ts +124 -0
  78. package/src/executors/herdr-owned.ts +260 -0
  79. package/src/executors/herdr-qualification.ts +33 -0
  80. package/src/executors/herdr-wire.ts +43 -0
  81. package/src/governance/runtime-settlement-lock.ts +76 -0
  82. package/src/governance/runtime-settlement.ts +271 -0
  83. package/src/kernel/definition-sources.ts +12 -0
  84. package/src/kernel/definitions.ts +2 -0
  85. package/src/kernel/env-names.ts +3 -0
  86. package/src/kernel/propagation.ts +2 -0
  87. package/src/kernel/selected-definitions.ts +31 -10
  88. package/src/products/public-evidence.ts +181 -0
@@ -83,7 +83,7 @@ export function executionEvidenceContent(
83
83
  outcomes: readonly DelegationOutcome[],
84
84
  requested: number,
85
85
  ) {
86
- const evidence = { version: 1, tool, requested, outcomes: outcomes.map(outcomeEvidence) };
86
+ const evidence = executionEvidence(tool, outcomes, requested);
87
87
  return {
88
88
  type: "text" as const,
89
89
  text:
@@ -92,3 +92,12 @@ export function executionEvidenceContent(
92
92
  "\n```",
93
93
  };
94
94
  }
95
+
96
+ /** Shared allowlisted projection for the model-visible text and exact local capture. */
97
+ export function executionEvidence(
98
+ tool: "delegate" | "delegate_all" | "delegate_chain",
99
+ outcomes: readonly DelegationOutcome[],
100
+ requested: number,
101
+ ) {
102
+ return { version: 1, tool, requested, outcomes: outcomes.map(outcomeEvidence) };
103
+ }
@@ -1,9 +1,10 @@
1
1
  import { chooseExecutor, needsProbe, ENV_HERDR } from "../src/executors/executor.ts";
2
+ import { qualifyHerdr } from "../src/executors/herdr-qualification.ts";
2
3
  import { probeHerdr } from "../src/executors/herdr-cli.ts";
3
4
  import type { GrantsSession } from "./session.ts";
4
5
 
5
6
  /** Settle one executor at session start; a later daemon change never relocates siblings. */
6
7
  export async function resolveExecutor(session: GrantsSession): Promise<void> {
7
8
  const raw = process.env[ENV_HERDR];
8
- session.executor = chooseExecutor(raw, needsProbe(raw) ? await probeHerdr() : null);
9
+ session.executor = chooseExecutor(raw, needsProbe(raw) ? await qualifyHerdr(await probeHerdr()) : null);
9
10
  }
@@ -20,6 +20,7 @@
20
20
  * spawn triggered the human prompt, and `childEnv` clamps it to the grant again on the way out. Each
21
21
  * child derives its own grant from the tool array of its first provider request.
22
22
  */
23
+ import { registerRuntimeSnapshot } from "./runtime-snapshot.ts";
23
24
  import { fileURLToPath } from "node:url";
24
25
  import { homedir } from "node:os";
25
26
  import { join, resolve } from "node:path";
@@ -80,6 +81,7 @@ export default function (pi: ExtensionAPI, trustedSession?: GrantsSession) {
80
81
  const session = trustedSession ?? createGrantsSession(extensionPath, undefined, observerExtensionPath);
81
82
  session.currentThinking = typeof pi.getThinkingLevel === "function" ? () => pi.getThinkingLevel() : undefined;
82
83
  registerActivityTimeline(pi, session);
84
+ const runtimeSnapshot = registerRuntimeSnapshot(pi, session);
83
85
  const dashboardPluginRoot = fileURLToPath(new URL("../herdr-plugin/", import.meta.url));
84
86
  const dashboardPaths = defaultDashboardPaths(agentDir());
85
87
  // Definitions are registered only after owner-bound session_start. Until then there is no delegation
@@ -215,6 +217,7 @@ export default function (pi: ExtensionAPI, trustedSession?: GrantsSession) {
215
217
  // (R-60), and `probeHerdr` is documented as never throwing precisely so this is belt-and-braces.
216
218
  try {
217
219
  await resolveExecutor(session);
220
+ if (session.reloadLifecycle === reload.lifecycle) await runtimeSnapshot.bind(ctx);
218
221
  } catch (error) {
219
222
  if (session.reloadLifecycle !== reload.lifecycle) return undefined;
220
223
  // Says what is actually true of the state left behind, which depends on the variable: with
@@ -0,0 +1,90 @@
1
+ /** Composition-only bridge. Snapshot definitions when dispatch actually selects them, never reread paths. */
2
+ import {
3
+ capturePublicEvidence,
4
+ type PublicEvidenceOwner,
5
+ type PublicEvidenceRequest,
6
+ type PublicDefinitionEvidence,
7
+ } from "../src/products/public-evidence.ts";
8
+ import { digestDefinition, type SkillDefinition } from "../src/kernel/definitions.ts";
9
+ import { executionEvidence } from "./execution-evidence.ts";
10
+ import type { DelegationOutcome } from "./execute-child.ts";
11
+ type Tool = "delegate_describe" | "delegate" | "delegate_all" | "delegate_chain";
12
+ interface Result {
13
+ isError?: boolean;
14
+ content: { type: "text"; text: string }[];
15
+ details?: unknown;
16
+ }
17
+ export function publicEvidenceCall(
18
+ owner: PublicEvidenceOwner | undefined,
19
+ toolCallId: string,
20
+ tool: Tool,
21
+ specs: readonly { agent?: string; definitionId?: string }[],
22
+ executionIds: readonly string[] = [],
23
+ ) {
24
+ const requested: PublicEvidenceRequest[] = specs.map((spec, index) => ({
25
+ ordinal: index + 1,
26
+ agent: spec.agent ?? null,
27
+ requestedDefinitionId: spec.definitionId ?? null,
28
+ definitionId: null,
29
+ executionId: executionIds[index] ?? null,
30
+ }));
31
+ const definitions = new Map<number, PublicDefinitionEvidence>();
32
+ return {
33
+ selected(index: number, definition: SkillDefinition | undefined) {
34
+ if (!owner) return;
35
+ definitions.delete(index);
36
+ if (requested[index]) requested[index].definitionId = null;
37
+ if (!definition) return;
38
+ const request = requested[index];
39
+ if (!request || !definition.definitionId) return;
40
+ request.definitionId = definition.definitionId;
41
+ definitions.set(index, {
42
+ agent: definition.name,
43
+ definitionId: definition.definitionId,
44
+ sourceHash: definition.sourceHash ?? null,
45
+ bodySha256: digestDefinition(definition).sha256,
46
+ binding: definition.binding ?? null,
47
+ snapshot: definition.sourceSnapshot,
48
+ });
49
+ },
50
+ finish<T extends Result>(result: T, outcomes: readonly DelegationOutcome[] = []): T {
51
+ if (!owner) return result;
52
+ let status: unknown;
53
+ try {
54
+ const ref = capturePublicEvidence(owner, {
55
+ toolCallId,
56
+ tool,
57
+ response: { isError: result.isError ?? false, content: result.content },
58
+ requested,
59
+ runtimeEvidence: tool === "delegate_describe" ? null : executionEvidence(tool, outcomes, specs.length),
60
+ definitions: [...definitions.values()],
61
+ finals: outcomes.map((outcome, index) => ({
62
+ ordinal: index + 1,
63
+ executionId: requested[index]?.executionId ?? null,
64
+ final:
65
+ outcome.final?.state === "complete"
66
+ ? {
67
+ state: "complete",
68
+ text: outcome.final.text,
69
+ sessionId: outcome.final.sessionId,
70
+ messageId: outcome.final.messageId,
71
+ leafId: outcome.final.leafId,
72
+ sha256: outcome.final.sha256,
73
+ }
74
+ : null,
75
+ })),
76
+ });
77
+ status = { status: "captured", ref: { path: ref.path, sha256: ref.sha256 } };
78
+ } catch (error) {
79
+ status = {
80
+ status: "failed",
81
+ reason: error instanceof Error ? error.message : "public evidence capture failed",
82
+ };
83
+ }
84
+ return {
85
+ ...result,
86
+ content: [...result.content, { type: "text", text: "Public evidence capture: " + JSON.stringify(status) }],
87
+ };
88
+ },
89
+ };
90
+ }
@@ -4,6 +4,10 @@ import type { BoundedReadCleanupError } from "../src/kernel/bounded-read.ts";
4
4
 
5
5
  /** The root baseline and latest child publication for one real Pi session owner. */
6
6
  export interface ReloadLifecycle {
7
+ runtimeSnapshotUnsubscribe?: () => void;
8
+ runtimeSnapshotGeneration?: object;
9
+ runtimeSettlements?: Map<string, import("../src/governance/runtime-settlement.ts").RuntimeSettlement>;
10
+ runtimeSettlement?: import("../src/governance/runtime-settlement.ts").RuntimeSettlement;
7
11
  root: Record<string, string | undefined>;
8
12
  /** Never replaced for the same owner, even when an explicit root environment changes. */
9
13
  capacity?: OwnerCapacity;
@@ -99,6 +103,10 @@ export function bindReloadLifecycle(
99
103
  activityRootId: existing.activityRootId,
100
104
  episodeId: existing.episodeId,
101
105
  capacity: existing.capacity,
106
+ runtimeSettlement: existing.runtimeSettlement,
107
+ runtimeSettlements: existing.runtimeSettlements,
108
+ runtimeSnapshotUnsubscribe: existing.runtimeSnapshotUnsubscribe,
109
+ runtimeSnapshotGeneration: existing.runtimeSnapshotGeneration,
102
110
  };
103
111
  holder.owners.set(owner, existing);
104
112
  }
@@ -73,6 +73,8 @@ interface DelegationToolContext extends ApprovalUIContext {
73
73
  /** A planned delegation, plus whatever approvals contributed to it. */
74
74
  export interface GatedPlan {
75
75
  plan: ReturnType<typeof planDelegation>;
76
+ /** Opt-in observation of the exact context used by the final planner invocation. */
77
+ definition?: import("../src/kernel/definitions.ts").SkillDefinition;
76
78
  /** Absent when the gate was never reached — i.e. the plan succeeded or failed for another reason. */
77
79
  approval?: ApprovalOutcome;
78
80
  }
@@ -112,8 +114,17 @@ export async function planWithApprovals(
112
114
  // the re-plan — and two spellings of one argument is the defect R-28 was.
113
115
  const approvalSubject = request.agent ?? DELEGATE_SUBJECT;
114
116
 
115
- let plan = planDelegation(request, { ...(await session.delegationContext(preApproved)), ...extra });
116
- if (plan.ok || !shouldSeekApproval(plan.result)) return { plan };
117
+ let definition: GatedPlan["definition"];
118
+ const observedPlan = (context: Parameters<typeof planDelegation>[1]) => {
119
+ const next = planDelegation(request, context);
120
+ definition =
121
+ session.publicEvidence && next.definitionDigest && request.agent
122
+ ? context.definitions?.get(request.agent)
123
+ : undefined;
124
+ return next;
125
+ };
126
+ let plan = observedPlan({ ...(await session.delegationContext(preApproved)), ...extra });
127
+ if (plan.ok || !shouldSeekApproval(plan.result)) return { plan, ...(definition ? { definition } : {}) };
117
128
 
118
129
  let approval: ApprovalOutcome | undefined;
119
130
  try {
@@ -133,7 +144,7 @@ export async function planWithApprovals(
133
144
  );
134
145
  const outcome = approval;
135
146
  if (outcome.approved.length > 0) {
136
- plan = planDelegation(request, {
147
+ plan = observedPlan({
137
148
  // The scope is the REAL one: a `once` approval still authorises this spawn, and
138
149
  // `inheritApprovals` then keeps it from reaching the child. See ADR-0014. R-29 is what makes
139
150
  // this safe under fan-out: a `once` is consumed by exactly one concurrent caller.
@@ -170,7 +181,7 @@ export async function planWithApprovals(
170
181
  plan = { ...plan, reason: message, refusal: structuredRefusal("APPROVAL_FLOW_FAILED", message) };
171
182
  }
172
183
 
173
- return { plan, approval };
184
+ return { plan, approval, ...(definition ? { definition } : {}) };
174
185
  }
175
186
 
176
187
  /**
@@ -211,6 +222,7 @@ export async function runOneDelegation(
211
222
  /** Trusted reservation made before a chain's upfront gate. */
212
223
  capacityReservation?: CapacityReservation;
213
224
  /** Exact pair selected during the chain preflight; never reselect after its approval. */
225
+ onDefinition?: (definition: import("../src/kernel/definitions.ts").SkillDefinition | undefined) => void;
214
226
  resolvedRuntime?: import("./definition-runtime.ts").ResolvedDefinitionRuntime;
215
227
  /** Actual public execute argument, never read from model-authored correlation or output. */
216
228
  toolCallId?: string;
@@ -266,6 +278,7 @@ export async function runOneDelegation(
266
278
  try {
267
279
  await session.ensureDefinitions?.();
268
280
  assertDefinitionIdentity(session, spec);
281
+
269
282
  const { toolCallId, onProgress, preApproved, taskFrom, taskFromExecutionId, approvalFacts } = options;
270
283
  // pi resolves a BARE model id to an unauthenticated provider and the child dies at startup — the id
271
284
  // alone is not enough, it must be qualified with its provider (`Model<Api>` carries both).
@@ -347,6 +360,7 @@ export async function runOneDelegation(
347
360
  // no child process.
348
361
  const preview = await planWithApprovals(session, request, planContext, null, signal, preApproved);
349
362
  plan = preview.plan;
363
+ options.onDefinition?.(preview.definition);
350
364
  if (plan.ok || shouldSeekApproval(plan.result)) {
351
365
  try {
352
366
  preparedWorkspace = await prepareDelegationWorkspace({
@@ -363,6 +377,7 @@ export async function runOneDelegation(
363
377
  request.correlation = preparedWorkspace.correlation;
364
378
  const gated = await planWithApprovals(session, request, planContext, ctx, signal, preApproved);
365
379
  plan = gated.plan;
380
+ options.onDefinition?.(gated.definition);
366
381
  approvalOutcome = gated.approval;
367
382
  } catch (error) {
368
383
  const value =
@@ -382,6 +397,7 @@ export async function runOneDelegation(
382
397
  preApproved,
383
398
  );
384
399
  plan = gated.plan;
400
+ options.onDefinition?.(gated.definition);
385
401
  approvalOutcome = gated.approval;
386
402
  }
387
403
 
@@ -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
+ }
@@ -13,6 +13,8 @@
13
13
  * module reads them **through this object**, live, rather than capturing a copy at load time; capturing a
14
14
  * copy of `ownGrant` before observation is exactly how a stale upper bound would become an enforced one.
15
15
  */
16
+ import { createPublicEvidenceOwner, type PublicEvidenceOwner } from "../src/products/public-evidence.ts";
17
+ import { ENV_PUBLIC_EVIDENCE_DIR } from "../src/kernel/env-names.ts";
16
18
  import { randomUUID } from "node:crypto";
17
19
  import { BoundedReadCleanupError } from "../src/kernel/bounded-read.ts";
18
20
  import type { discoverSkillPackages } from "../src/kernel/skill-packages.ts";
@@ -139,6 +141,8 @@ export function activityChildEnv(activity: { rootId: string; path: string; taskI
139
141
  }
140
142
  /** Keep each child's pane after it finishes, for inspection. Off by default: fan-out would flood it. */
141
143
  export interface GrantsSession extends NativeSessionHost {
144
+ /** Reconciled from the actual owner root on reload; captured call owners stay immutable. */
145
+ publicEvidence?: PublicEvidenceOwner;
142
146
  /** Legacy PI_GRANTS_* names adopted at construction (ADR-0076 PR 3b); the session-start warning names them. */
143
147
  readonly adoptedLegacyEnv: readonly string[];
144
148
  /** False only for the explicit PI_DADDY_GOVERNANCE opt-out; otherwise roots are observed-bound. */
@@ -519,7 +523,8 @@ async function loadProjectDiscovery(
519
523
  await establishRootPin(session, lifecycle);
520
524
  const skips: string[] = [];
521
525
  assertDiscoveryHealthy(session, lifecycle);
522
- const snapshot = selected === undefined ? undefined : await selectedDefinitions(selected);
526
+ const snapshot =
527
+ selected === undefined ? undefined : await selectedDefinitions(selected, session.publicEvidence !== undefined);
523
528
  const definitions =
524
529
  snapshot?.definitions ??
525
530
  (await (session.discovery.definitions ?? loadDefinitions)(cwd, (_path, reason) => skips.push(reason)));
@@ -632,6 +637,7 @@ export function createGrantsSession(
632
637
  */
633
638
  mayDelegate: !governed || inherited.includes(DELEGATE_CAPABILITY) || inherited.includes(WILDCARD),
634
639
  ownerBound: false,
640
+ publicEvidence: createPublicEvidenceOwner(environment[ENV_PUBLIC_EVIDENCE_DIR]),
635
641
  allowUnresolvedModels: environment[ENV_ALLOW_UNRESOLVED_MODELS] === "1",
636
642
  nativeSessionRoot: nativeSessionRootFromEnv(process.env),
637
643
  modelResolutionCache: new Map<string, boolean>(),
@@ -687,7 +693,7 @@ export function createGrantsSession(
687
693
  // Read live off `session.executor` (ADR-0031) rather than a boolean captured in the factory: the probe
688
694
  // has not run when this session object is built, so a captured value would plan `--print` for a session
689
695
  // that turns out to use panes — and `runHerdrPane` refuses a plan containing `--print` by design.
690
- interactive: session.executor.kind === "herdr",
696
+ interactive: false, // Both qualified backends use the same one-shot Pi JSON protocol.
691
697
  ...(approved ? { approved } : {}),
692
698
  }),
693
699
  storeCwd,
@@ -707,6 +713,9 @@ export function createGrantsSession(
707
713
  session.publishChildEnv();
708
714
  },
709
715
  reconcileEnvironment: (environment, lifecycle) => {
716
+ const evidenceDirectory = environment[ENV_PUBLIC_EVIDENCE_DIR];
717
+ if (session.reloadLifecycle !== lifecycle || session.publicEvidence?.directory !== evidenceDirectory)
718
+ session.publicEvidence = createPublicEvidenceOwner(evidenceDirectory);
710
719
  if (session.reloadLifecycle !== lifecycle) {
711
720
  session.pinSettled = false;
712
721
  session.workspacePin = undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-daddy",
3
- "version": "0.44.4",
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
+ }