pi-daddy 0.45.0 → 0.46.1
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 +391 -45
- package/contracts/ledger-record/v1/governance-event.schema.json +2 -0
- 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/approval-prompt.d.ts +4 -11
- package/dist/governance/approval-prompt.d.ts.map +1 -1
- package/dist/governance/approval-prompt.js +83 -24
- package/dist/governance/approval-prompt.js.map +1 -1
- package/dist/governance/ledger-v3-validation.js +1 -1
- package/dist/governance/ledger-v3-validation.js.map +1 -1
- package/dist/governance/ledger.d.ts +3 -4
- package/dist/governance/ledger.d.ts.map +1 -1
- package/dist/governance/ledger.js +1 -1
- package/dist/governance/ledger.js.map +1 -1
- 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/extensions/activity-timeline.ts +94 -31
- package/extensions/execute-child.ts +98 -74
- package/extensions/execution-controls.ts +42 -0
- package/extensions/executor-session.ts +2 -1
- package/extensions/grants-command.ts +4 -0
- package/extensions/grants.ts +4 -0
- package/extensions/reload-environment.ts +8 -0
- package/extensions/runtime-snapshot.ts +123 -0
- package/extensions/session.ts +1 -1
- 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/approval-prompt.ts +86 -29
- package/src/governance/ledger-v3-validation.ts +1 -1
- package/src/governance/ledger.ts +3 -4
- package/src/governance/runtime-settlement-lock.ts +76 -0
- package/src/governance/runtime-settlement.ts +271 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BoundedReadCleanupError } from "../src/kernel/bounded-read.ts";
|
|
2
2
|
import { retainDiscoveryCleanupFailure } from "./session.ts";
|
|
3
3
|
import type { CapacityReservation } from "../src/kernel/capacity.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { runHerdrOwned } from "../src/executors/herdr-owned.ts";
|
|
5
5
|
import { runCapturedExecution } from "../src/executors/captured-execution.ts";
|
|
6
6
|
import type { ChildFinal } from "../src/executors/child-final.ts";
|
|
7
7
|
import type { CapturedWorkerCleanup } from "../src/kernel/captured-worker-contract.ts";
|
|
@@ -20,13 +20,12 @@ import {
|
|
|
20
20
|
ENV_CHILD_IDLE_TIMEOUT,
|
|
21
21
|
ENV_CHILD_TIMEOUT,
|
|
22
22
|
idleTimeoutFromEnv,
|
|
23
|
-
runChild,
|
|
24
23
|
timeoutFromEnv,
|
|
25
24
|
} from "../src/kernel/run-child.ts";
|
|
26
25
|
import { activitySessionFor } from "../src/executors/activity-session.ts";
|
|
27
26
|
import { processTreeActivity } from "../src/executors/process-activity.ts";
|
|
28
27
|
import { resolveWorkspace } from "../src/executors/herdr-cli.ts";
|
|
29
|
-
import { HerdrWriterCloseError
|
|
28
|
+
import { HerdrWriterCloseError } from "../src/executors/run-herdr.ts";
|
|
30
29
|
import { GovernanceRefusal, refusal, type StructuredRefusal } from "../src/kernel/refusals.ts";
|
|
31
30
|
import { ENV_HERDR_KEEP_PANE, type GrantsSession } from "./session.ts";
|
|
32
31
|
import { CHILD_ATTRIBUTION_ENV_KEYS, ENV_EPISODE_ID } from "../src/kernel/env-names.ts";
|
|
@@ -138,15 +137,49 @@ export interface ChildExecutionInput {
|
|
|
138
137
|
}
|
|
139
138
|
export async function executePlannedChild(input: ChildExecutionInput): Promise<DelegationOutcome> {
|
|
140
139
|
let executorEntered = false;
|
|
140
|
+
const runtime = input.session.reloadLifecycle?.runtimeSettlement;
|
|
141
|
+
let registered = false;
|
|
141
142
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
runtime?.begin(input.executionId);
|
|
144
|
+
registered = Boolean(runtime);
|
|
145
|
+
const result = await executePreparedChild(
|
|
146
|
+
input,
|
|
147
|
+
() => {
|
|
148
|
+
executorEntered = true;
|
|
149
|
+
},
|
|
150
|
+
runtime,
|
|
151
|
+
);
|
|
152
|
+
try {
|
|
153
|
+
await runtime?.finish(
|
|
154
|
+
input.executionId,
|
|
155
|
+
result.cleanup ?? { state: "unknown", reason: "executor omitted cleanup" },
|
|
156
|
+
result.control === "failed" ? (result.reason ?? "runtime control failed") : undefined,
|
|
157
|
+
);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return {
|
|
160
|
+
...result,
|
|
161
|
+
control: "failed",
|
|
162
|
+
reason: [result.reason, `runtime settlement record failed: ${String(error)}`].filter(Boolean).join("; "),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
145
166
|
} catch (error) {
|
|
146
|
-
if (executorEntered) throw error;
|
|
147
|
-
// Every operation above the executor-entry marker is local setup: no helper or worker exists yet.
|
|
148
|
-
input.capacityReservation?.finalize({ state: "not-started", reason: "child setup failed before executor entry" });
|
|
149
167
|
const failures: unknown[] = [error];
|
|
168
|
+
if (registered)
|
|
169
|
+
try {
|
|
170
|
+
await runtime?.finish(input.executionId, {
|
|
171
|
+
state: executorEntered ? "unknown" : "not-started",
|
|
172
|
+
reason: String(error),
|
|
173
|
+
});
|
|
174
|
+
} catch (recordError) {
|
|
175
|
+
failures.push(recordError);
|
|
176
|
+
}
|
|
177
|
+
if (executorEntered) {
|
|
178
|
+
if (failures.length > 1) throw new AggregateError(failures, failures.map(String).join("; "));
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
// Setup failures prove no worker started, even if recording that fact also failed.
|
|
182
|
+
input.capacityReservation?.finalize({ state: "not-started", reason: "child setup failed before executor entry" });
|
|
150
183
|
try {
|
|
151
184
|
const released = await releaseDelegationWorkspace({
|
|
152
185
|
prepared: input.preparedWorkspace,
|
|
@@ -174,6 +207,7 @@ export async function executePlannedChild(input: ChildExecutionInput): Promise<D
|
|
|
174
207
|
async function executePreparedChild(
|
|
175
208
|
input: ChildExecutionInput,
|
|
176
209
|
onExecutorEntry: () => void,
|
|
210
|
+
runtime?: import("../src/governance/runtime-settlement.ts").RuntimeSettlement,
|
|
177
211
|
): Promise<DelegationOutcome> {
|
|
178
212
|
const { session, plan, childId, executionId, parentExecutionId, preparedWorkspace, signal, onProgress } = input;
|
|
179
213
|
const ledgerPath = session.ledgerPath;
|
|
@@ -229,7 +263,7 @@ async function executePreparedChild(
|
|
|
229
263
|
const activitySession = await activitySessionFor(plan.args, executionId);
|
|
230
264
|
// Disposed on every path (review finding: it leaked on every throw), except when the operator keeps a Herdr pane,
|
|
231
265
|
// where the interactive pi in that pane is still alive and still appending to this file.
|
|
232
|
-
const keepPaneRequested =
|
|
266
|
+
const keepPaneRequested = false; // The qualified pane launcher exits; retained panes contain no live Pi writer.
|
|
233
267
|
try {
|
|
234
268
|
return await executeWithActivitySession();
|
|
235
269
|
} finally {
|
|
@@ -368,73 +402,63 @@ async function executePreparedChild(
|
|
|
368
402
|
}
|
|
369
403
|
};
|
|
370
404
|
try {
|
|
371
|
-
if (
|
|
372
|
-
throw new GovernanceRefusal(refusal("EXECUTOR_UNAVAILABLE",
|
|
405
|
+
if (session.executor.refusal)
|
|
406
|
+
throw new GovernanceRefusal(refusal("EXECUTOR_UNAVAILABLE", session.executor.refusal));
|
|
373
407
|
executorEntered = true;
|
|
374
408
|
onExecutorEntry();
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
executionId,
|
|
408
|
-
sessionPath: activitySession.path,
|
|
409
|
-
onReadCleanup,
|
|
410
|
-
onOwnership: async (identity) => {
|
|
411
|
-
input.capacityReservation?.bindOwnership(identity);
|
|
412
|
-
await preparedWorkspace?.lease.attachCapturedWorker(identity);
|
|
413
|
-
},
|
|
414
|
-
command: "pi",
|
|
415
|
-
args,
|
|
416
|
-
env: mergeChildEnv(process.env, plan.env),
|
|
417
|
-
cwd,
|
|
418
|
-
signal: executionSignal,
|
|
419
|
-
// SIGTERM gets only grace that fits INSIDE the recorded deadline. The independent hard timer
|
|
420
|
-
// prevents a delayed soft-timeout callback from starting a fresh grace period beyond that bound.
|
|
421
|
-
timeoutMs: Math.max(1, remainingTimeoutMs - terminationGraceMs),
|
|
422
|
-
hardDeadlineAt: Date.parse(deadlineAt),
|
|
423
|
-
idleTimeoutMs: configuredIdleMs,
|
|
424
|
-
activityProbe: probe,
|
|
425
|
-
onOutput: onProgress ? (chunk) => onProgress({ chunk }) : undefined,
|
|
426
|
-
onObservation: (stream, bytes) => retention.capture(stream, bytes),
|
|
427
|
-
onSpawn: (pid) => {
|
|
428
|
-
childPid = pid;
|
|
429
|
-
// Lease attachment is a security hook and may fail the spawn. The shared reporters isolate
|
|
430
|
-
// display exceptions before they reach this callback; runChild deliberately kills on any error
|
|
431
|
-
// here, so presentation must never be added directly without that reporter boundary.
|
|
409
|
+
let displayHerdr: ((text: string) => void) | undefined;
|
|
410
|
+
const output = await runCapturedExecution(
|
|
411
|
+
{
|
|
412
|
+
executionId,
|
|
413
|
+
sessionPath: activitySession.path,
|
|
414
|
+
onReadCleanup,
|
|
415
|
+
onOwnership: async (identity) => {
|
|
416
|
+
runtime?.bind(identity);
|
|
417
|
+
input.capacityReservation?.bindOwnership(identity);
|
|
418
|
+
await preparedWorkspace?.lease.attachCapturedWorker(identity);
|
|
419
|
+
},
|
|
420
|
+
command: "pi",
|
|
421
|
+
args,
|
|
422
|
+
env: mergeChildEnv(process.env, plan.env),
|
|
423
|
+
cwd,
|
|
424
|
+
signal: executionSignal,
|
|
425
|
+
// SIGTERM gets only grace that fits INSIDE the recorded deadline. The independent hard timer
|
|
426
|
+
// prevents a delayed soft-timeout callback from starting a fresh grace period beyond that bound.
|
|
427
|
+
timeoutMs: Math.max(1, remainingTimeoutMs - terminationGraceMs),
|
|
428
|
+
hardDeadlineAt: Date.parse(deadlineAt),
|
|
429
|
+
idleTimeoutMs: configuredIdleMs,
|
|
430
|
+
activityProbe: probe,
|
|
431
|
+
onOutput: (chunk) => {
|
|
432
|
+
displayHerdr?.(chunk);
|
|
433
|
+
onProgress?.({ chunk });
|
|
434
|
+
},
|
|
435
|
+
onObservation: (stream, bytes) => retention.capture(stream, bytes),
|
|
436
|
+
onSpawn: (pid) => {
|
|
437
|
+
childPid = pid;
|
|
438
|
+
// Lease attachment is a security hook and may fail the spawn. The shared reporters isolate
|
|
439
|
+
// display exceptions before they reach this callback; runChild deliberately kills on any error
|
|
440
|
+
// here, so presentation must never be added directly without that reporter boundary.
|
|
432
441
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
442
|
+
retention.native({ pid });
|
|
443
|
+
recordRunning(session.executor.kind);
|
|
444
|
+
onProgress?.({ state: "running" });
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
session.executor.kind === "herdr"
|
|
448
|
+
? (request) =>
|
|
449
|
+
runHerdrOwned(request, {
|
|
450
|
+
onDisplay: (display) => {
|
|
451
|
+
displayHerdr = display;
|
|
452
|
+
},
|
|
453
|
+
workspace: resolveWorkspace(process.env),
|
|
454
|
+
keepPane: process.env[ENV_HERDR_KEEP_PANE] === "1",
|
|
455
|
+
onPane: (paneId, tabId) => {
|
|
456
|
+
retention.native({ paneId, tabId });
|
|
457
|
+
onProgress?.({ paneId, state: "starting" });
|
|
458
|
+
},
|
|
459
|
+
})
|
|
460
|
+
: undefined,
|
|
461
|
+
);
|
|
438
462
|
|
|
439
463
|
if (sessionFlag >= 0) retention.observeSession({ source: "pi-session-file", value: plan.args[sessionFlag + 1] });
|
|
440
464
|
retention.capture("result", Buffer.from(output.text), true);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Operator-facing execution settings; display never changes admission, deadlines or retention. */
|
|
2
|
+
import { timeoutFromEnv, idleTimeoutFromEnv } from "../src/kernel/run-child.ts";
|
|
3
|
+
import { timeoutMsFromEnv } from "../src/governance/approval-prompt.ts";
|
|
4
|
+
import {
|
|
5
|
+
ENV_APPROVAL_TIMEOUT,
|
|
6
|
+
ENV_CHILD_TIMEOUT,
|
|
7
|
+
ENV_CHILD_IDLE_TIMEOUT,
|
|
8
|
+
ENV_EXECUTION_ARCHIVE,
|
|
9
|
+
} from "../src/kernel/env-names.ts";
|
|
10
|
+
|
|
11
|
+
export function renderExecutionControls(
|
|
12
|
+
nativeSessionRoot: string | undefined,
|
|
13
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
14
|
+
): string[] {
|
|
15
|
+
let approval: string;
|
|
16
|
+
try {
|
|
17
|
+
const timeout = timeoutMsFromEnv(env[ENV_APPROVAL_TIMEOUT]);
|
|
18
|
+
approval = timeout === undefined ? "wait for human; no deadline" : `${timeout / 1000}s configured deadline`;
|
|
19
|
+
} catch {
|
|
20
|
+
approval = `${ENV_APPROVAL_TIMEOUT} invalid; a needed new prompt will refuse`;
|
|
21
|
+
}
|
|
22
|
+
const source = (key: string) => (env[key] === undefined ? "default" : `from ${key}; zero/invalid uses default`);
|
|
23
|
+
return [
|
|
24
|
+
` child wall ${timeoutFromEnv(env[ENV_CHILD_TIMEOUT]) / 1000}s (${source(ENV_CHILD_TIMEOUT)})`,
|
|
25
|
+
` child idle ${idleTimeoutFromEnv(env[ENV_CHILD_IDLE_TIMEOUT]) / 1000}s (${source(ENV_CHILD_IDLE_TIMEOUT)})`,
|
|
26
|
+
" process safety controls; no time/token budget is added to the model prompt",
|
|
27
|
+
` approval ${approval}`,
|
|
28
|
+
` transcripts ${
|
|
29
|
+
nativeSessionRoot === undefined
|
|
30
|
+
? "off"
|
|
31
|
+
: nativeSessionRoot
|
|
32
|
+
? `opted in at ${JSON.stringify(nativeSessionRoot)}; destination checked at dispatch`
|
|
33
|
+
: "opted in but native session root is missing"
|
|
34
|
+
}`,
|
|
35
|
+
` diagnostics ${
|
|
36
|
+
env[ENV_EXECUTION_ARCHIVE]
|
|
37
|
+
? `configured at ${JSON.stringify(env[ENV_EXECUTION_ARCHIVE])}; check each result's retention status`
|
|
38
|
+
: "off"
|
|
39
|
+
}`,
|
|
40
|
+
" local diagnostic retention is separate from JEV/LoRA consent",
|
|
41
|
+
];
|
|
42
|
+
}
|
|
@@ -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
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* being whatever happened to be in scope.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { renderExecutionControls } from "./execution-controls.ts";
|
|
15
16
|
import type { Capability } from "../src/kernel/resolve.ts";
|
|
16
17
|
import type { ExecutorChoice } from "../src/executors/executor.ts";
|
|
17
18
|
import type { Catalog } from "../src/kernel/catalog.ts";
|
|
@@ -25,6 +26,8 @@ import type { ResolvedDefinitionRuntime } from "./definition-runtime.ts";
|
|
|
25
26
|
export interface GrantsCommandContext {
|
|
26
27
|
cwd: string;
|
|
27
28
|
governed: boolean;
|
|
29
|
+
/** Host opt-in captured by this session, not a later environment reread. */
|
|
30
|
+
nativeSessionRoot?: string;
|
|
28
31
|
ownGrant: Capability[];
|
|
29
32
|
/**
|
|
30
33
|
* Which executor this session settled on, and why — ADR-0031.
|
|
@@ -407,6 +410,7 @@ export const grantsCommand = {
|
|
|
407
410
|
// ADR-0031, and its probe is only defensible because this line exists. Placed next to `holding` so the
|
|
408
411
|
// two facts about what a spawn will be sit together.
|
|
409
412
|
` executor ${executor.disclosure}`,
|
|
413
|
+
...renderExecutionControls(ctx.grants.nativeSessionRoot),
|
|
410
414
|
` depth ${depth} of max ${maxDepth}${maxDepth <= 0 ? " (spawning disabled)" : ""}`,
|
|
411
415
|
` ledger ${ledgerPath || "(not recording — set PI_DADDY_LEDGER)"}`,
|
|
412
416
|
` approvals ${sessionApprovals.size} this session, ${valid.size} persisted` +
|
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
|
|
@@ -459,6 +462,7 @@ export default function (pi: ExtensionAPI, trustedSession?: GrantsSession) {
|
|
|
459
462
|
},
|
|
460
463
|
}
|
|
461
464
|
: {}),
|
|
465
|
+
nativeSessionRoot: session.nativeSessionRoot,
|
|
462
466
|
catalog: session.catalog,
|
|
463
467
|
definitions: session.definitions,
|
|
464
468
|
sessionApprovals: session.sessionApprovals,
|
|
@@ -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
|
}
|
|
@@ -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
|
@@ -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:
|
|
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.
|
|
3
|
+
"version": "0.46.1",
|
|
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
|
}
|