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.
- package/CHANGELOG.md +10 -0
- package/README.md +383 -25
- package/dist/executors/captured-execution.d.ts +2 -2
- package/dist/executors/captured-execution.d.ts.map +1 -1
- package/dist/executors/captured-execution.js +2 -2
- package/dist/executors/captured-execution.js.map +1 -1
- package/dist/executors/executor.d.ts.map +1 -1
- package/dist/executors/executor.js +4 -2
- package/dist/executors/executor.js.map +1 -1
- package/dist/executors/herdr-cli.d.ts +2 -0
- package/dist/executors/herdr-cli.d.ts.map +1 -1
- package/dist/executors/herdr-cli.js +2 -2
- package/dist/executors/herdr-cli.js.map +1 -1
- package/dist/executors/herdr-launcher.d.ts +2 -0
- package/dist/executors/herdr-launcher.d.ts.map +1 -0
- package/dist/executors/herdr-launcher.js +123 -0
- package/dist/executors/herdr-launcher.js.map +1 -0
- package/dist/executors/herdr-owned.d.ts +13 -0
- package/dist/executors/herdr-owned.d.ts.map +1 -0
- package/dist/executors/herdr-owned.js +229 -0
- package/dist/executors/herdr-owned.js.map +1 -0
- package/dist/executors/herdr-qualification.d.ts +6 -0
- package/dist/executors/herdr-qualification.d.ts.map +1 -0
- package/dist/executors/herdr-qualification.js +33 -0
- package/dist/executors/herdr-qualification.js.map +1 -0
- package/dist/executors/herdr-wire.d.ts +5 -0
- package/dist/executors/herdr-wire.d.ts.map +1 -0
- package/dist/executors/herdr-wire.js +47 -0
- package/dist/executors/herdr-wire.js.map +1 -0
- package/dist/governance/runtime-settlement-lock.d.ts +5 -0
- package/dist/governance/runtime-settlement-lock.d.ts.map +1 -0
- package/dist/governance/runtime-settlement-lock.js +72 -0
- package/dist/governance/runtime-settlement-lock.js.map +1 -0
- package/dist/governance/runtime-settlement.d.ts +22 -0
- package/dist/governance/runtime-settlement.d.ts.map +1 -0
- package/dist/governance/runtime-settlement.js +236 -0
- package/dist/governance/runtime-settlement.js.map +1 -0
- package/dist/kernel/definition-sources.d.ts +13 -0
- package/dist/kernel/definition-sources.d.ts.map +1 -0
- package/dist/kernel/definition-sources.js +2 -0
- package/dist/kernel/definition-sources.js.map +1 -0
- package/dist/kernel/definitions.d.ts +2 -0
- package/dist/kernel/definitions.d.ts.map +1 -1
- package/dist/kernel/definitions.js.map +1 -1
- package/dist/kernel/env-names.d.ts +2 -0
- package/dist/kernel/env-names.d.ts.map +1 -1
- package/dist/kernel/env-names.js +3 -0
- package/dist/kernel/env-names.js.map +1 -1
- package/dist/kernel/propagation.d.ts +1 -1
- package/dist/kernel/propagation.d.ts.map +1 -1
- package/dist/kernel/propagation.js +2 -0
- package/dist/kernel/propagation.js.map +1 -1
- package/dist/kernel/selected-definitions.d.ts +1 -1
- package/dist/kernel/selected-definitions.d.ts.map +1 -1
- package/dist/kernel/selected-definitions.js +19 -10
- package/dist/kernel/selected-definitions.js.map +1 -1
- package/dist/products/public-evidence.d.ts +62 -0
- package/dist/products/public-evidence.d.ts.map +1 -0
- package/dist/products/public-evidence.js +116 -0
- package/dist/products/public-evidence.js.map +1 -0
- package/extensions/definition-describe.ts +4 -1
- package/extensions/delegate-chain.ts +21 -15
- package/extensions/delegation.ts +57 -27
- package/extensions/execute-child.ts +98 -74
- package/extensions/execution-evidence.ts +10 -1
- package/extensions/executor-session.ts +2 -1
- package/extensions/grants.ts +3 -0
- package/extensions/public-evidence.ts +90 -0
- package/extensions/reload-environment.ts +8 -0
- package/extensions/run-delegation.ts +20 -4
- package/extensions/runtime-snapshot.ts +123 -0
- package/extensions/session.ts +11 -2
- package/package.json +1 -1
- package/src/executors/captured-execution.ts +2 -1
- package/src/executors/executor.ts +4 -2
- package/src/executors/herdr-cli.ts +19 -12
- package/src/executors/herdr-launcher.ts +124 -0
- package/src/executors/herdr-owned.ts +260 -0
- package/src/executors/herdr-qualification.ts +33 -0
- package/src/executors/herdr-wire.ts +43 -0
- package/src/governance/runtime-settlement-lock.ts +76 -0
- package/src/governance/runtime-settlement.ts +271 -0
- package/src/kernel/definition-sources.ts +12 -0
- package/src/kernel/definitions.ts +2 -0
- package/src/kernel/env-names.ts +3 -0
- package/src/kernel/propagation.ts +2 -0
- package/src/kernel/selected-definitions.ts +31 -10
- 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 =
|
|
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
|
}
|
package/extensions/grants.ts
CHANGED
|
@@ -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
|
|
116
|
-
|
|
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 =
|
|
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
|
+
}
|
package/extensions/session.ts
CHANGED
|
@@ -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 =
|
|
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:
|
|
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.
|
|
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
|
|
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
|
|
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(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
}
|