pi-daddy 0.19.0 → 0.20.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 +65 -0
- package/README.md +105 -36
- package/contracts/ledger/v3/README.md +36 -0
- package/contracts/ledger/v3/fixtures/capability-decision.json +98 -0
- package/contracts/ledger/v3/fixtures/check-receipt.json +42 -0
- package/contracts/ledger/v3/fixtures/child-lifecycle.json +45 -0
- package/contracts/ledger/v3/fixtures/workflow-fact.json +41 -0
- package/contracts/ledger/v3/fixtures/workspace-lease.json +43 -0
- package/contracts/ledger/v3/ledger-event.schema.json +930 -0
- package/dist/check-runner.d.ts.map +1 -1
- package/dist/check-runner.js +14 -0
- package/dist/check-runner.js.map +1 -1
- package/dist/cli.js +0 -0
- package/dist/correlation.d.ts.map +1 -1
- package/dist/correlation.js +10 -0
- package/dist/correlation.js.map +1 -1
- package/dist/dashboard-cli.d.ts +18 -0
- package/dist/dashboard-cli.d.ts.map +1 -0
- package/dist/dashboard-cli.js +154 -0
- package/dist/dashboard-cli.js.map +1 -0
- package/dist/dashboard-handshake.d.ts +37 -0
- package/dist/dashboard-handshake.d.ts.map +1 -0
- package/dist/dashboard-handshake.js +127 -0
- package/dist/dashboard-handshake.js.map +1 -0
- package/dist/dashboard-herdr.d.ts +54 -0
- package/dist/dashboard-herdr.d.ts.map +1 -0
- package/dist/dashboard-herdr.js +286 -0
- package/dist/dashboard-herdr.js.map +1 -0
- package/dist/dashboard-projection.d.ts +73 -0
- package/dist/dashboard-projection.d.ts.map +1 -0
- package/dist/dashboard-projection.js +294 -0
- package/dist/dashboard-projection.js.map +1 -0
- package/dist/dashboard-render.d.ts +10 -0
- package/dist/dashboard-render.d.ts.map +1 -0
- package/dist/dashboard-render.js +208 -0
- package/dist/dashboard-render.js.map +1 -0
- package/dist/delegate-types.d.ts +6 -2
- package/dist/delegate-types.d.ts.map +1 -1
- package/dist/delegate-types.js.map +1 -1
- package/dist/delegate.d.ts.map +1 -1
- package/dist/delegate.js +4 -1
- package/dist/delegate.js.map +1 -1
- package/dist/execution-id.d.ts +6 -0
- package/dist/execution-id.d.ts.map +1 -0
- package/dist/execution-id.js +13 -0
- package/dist/execution-id.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/ledger-events.d.ts +27 -4
- package/dist/ledger-events.d.ts.map +1 -1
- package/dist/ledger-events.js +30 -9
- package/dist/ledger-events.js.map +1 -1
- package/dist/ledger-identifiers.d.ts +7 -0
- package/dist/ledger-identifiers.d.ts.map +1 -0
- package/dist/ledger-identifiers.js +20 -0
- package/dist/ledger-identifiers.js.map +1 -0
- package/dist/ledger-report.d.ts +5 -2
- package/dist/ledger-report.d.ts.map +1 -1
- package/dist/ledger-report.js +38 -14
- package/dist/ledger-report.js.map +1 -1
- package/dist/ledger-v3-validation.d.ts +11 -0
- package/dist/ledger-v3-validation.d.ts.map +1 -0
- package/dist/ledger-v3-validation.js +265 -0
- package/dist/ledger-v3-validation.js.map +1 -0
- package/dist/ledger.d.ts +18 -5
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +29 -7
- package/dist/ledger.js.map +1 -1
- package/dist/propagation.d.ts +3 -1
- package/dist/propagation.d.ts.map +1 -1
- package/dist/propagation.js +3 -0
- package/dist/propagation.js.map +1 -1
- package/dist/run-child.d.ts +3 -1
- package/dist/run-child.d.ts.map +1 -1
- package/dist/run-child.js +30 -2
- package/dist/run-child.js.map +1 -1
- package/dist/run-herdr.d.ts +2 -0
- package/dist/run-herdr.d.ts.map +1 -1
- package/dist/run-herdr.js +8 -0
- package/dist/run-herdr.js.map +1 -1
- package/dist/workflow-fact-id.d.ts +4 -0
- package/dist/workflow-fact-id.d.ts.map +1 -0
- package/dist/workflow-fact-id.js +14 -0
- package/dist/workflow-fact-id.js.map +1 -0
- package/dist/workflow-facts.d.ts +34 -0
- package/dist/workflow-facts.d.ts.map +1 -0
- package/dist/workflow-facts.js +43 -0
- package/dist/workflow-facts.js.map +1 -0
- package/extensions/chain-ledger.ts +4 -0
- package/extensions/chain-plan.ts +98 -0
- package/extensions/delegate-chain.ts +63 -122
- package/extensions/delegation-ledger.ts +60 -0
- package/extensions/delegation.ts +13 -13
- package/extensions/execute-child.ts +85 -21
- package/extensions/execution-occurrence.ts +20 -0
- package/extensions/grants-command.ts +22 -3
- package/extensions/grants.ts +44 -0
- package/extensions/run-delegation.ts +36 -56
- package/extensions/session.ts +7 -2
- package/extensions/workspace-runtime.ts +10 -0
- package/herdr-plugin/herdr-plugin.toml +18 -0
- package/package.json +16 -4
- package/src/check-runner.ts +16 -0
- package/src/correlation.ts +11 -0
- package/src/dashboard-cli.ts +171 -0
- package/src/dashboard-handshake.ts +185 -0
- package/src/dashboard-herdr.ts +355 -0
- package/src/dashboard-projection.ts +390 -0
- package/src/dashboard-render.ts +254 -0
- package/src/delegate-types.ts +6 -2
- package/src/delegate.ts +4 -2
- package/src/execution-id.ts +18 -0
- package/src/index.ts +25 -0
- package/src/ledger-events.ts +54 -10
- package/src/ledger-identifiers.ts +21 -0
- package/src/ledger-report.ts +40 -19
- package/src/ledger-v3-validation.ts +266 -0
- package/src/ledger.ts +49 -10
- package/src/propagation.ts +3 -0
- package/src/run-child.ts +35 -3
- package/src/run-herdr.ts +9 -0
- package/src/workflow-fact-id.ts +16 -0
- package/src/workflow-facts.ts +65 -0
|
@@ -60,6 +60,8 @@ export interface GrantsCommandContext {
|
|
|
60
60
|
* from the caller, visibly.
|
|
61
61
|
*/
|
|
62
62
|
runInit: () => Promise<void>;
|
|
63
|
+
/** Open or reuse the read-only Herdr dashboard; injected so this diagnostic never becomes enforcement. */
|
|
64
|
+
openDashboard: () => Promise<{ kind: "opened" | "reused"; paneId: string; visibleBesideCaller: boolean }>;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
/**
|
|
@@ -69,12 +71,12 @@ export interface GrantsCommandContext {
|
|
|
69
71
|
const PREVIEW_LIMIT = 12;
|
|
70
72
|
|
|
71
73
|
/** The verbs `/grants` answers to. Anything else is refused rather than silently treated as no verb. */
|
|
72
|
-
const KNOWN_SUBCOMMANDS: readonly string[] = ["init", "ledger", "approvals", "revoke"];
|
|
74
|
+
const KNOWN_SUBCOMMANDS: readonly string[] = ["init", "dashboard", "ledger", "approvals", "revoke"];
|
|
73
75
|
|
|
74
76
|
export const grantsCommand = {
|
|
75
77
|
description:
|
|
76
78
|
"Show this session's capability grant, delegation depth, and known agent-type ceilings; " +
|
|
77
|
-
"/grants approvals | /grants ledger | /grants revoke <key>|--all",
|
|
79
|
+
"/grants dashboard | /grants approvals | /grants ledger | /grants revoke <key>|--all",
|
|
78
80
|
handler: async (args: string, ctx: any) => {
|
|
79
81
|
// Everything this command may see, named in one place. Previously these were whatever happened to be in
|
|
80
82
|
// the enclosing closure — which is how a diagnostic came to disagree with the enforcer (R-28).
|
|
@@ -90,6 +92,20 @@ handler: async (args: string, ctx: any) => {
|
|
|
90
92
|
return;
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
if (sub === "dashboard") {
|
|
96
|
+
try {
|
|
97
|
+
const opened = await ctx.grants.openDashboard();
|
|
98
|
+
ctx.ui.notify(
|
|
99
|
+
`grants: dashboard ${opened.kind} in Herdr pane ${opened.paneId} without changing focus` +
|
|
100
|
+
(opened.visibleBesideCaller ? "." : " (the existing pane is in another tab)."),
|
|
101
|
+
"info",
|
|
102
|
+
);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
ctx.ui.notify(`grants: dashboard unavailable — ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
93
109
|
if (sub === "ledger") {
|
|
94
110
|
// The detector, made reachable. `verifyLedger` exists because nothing in this package had ever read
|
|
95
111
|
// a ledger back, so a torn line was indistinguishable from a spawn that never happened — and a
|
|
@@ -109,7 +125,7 @@ handler: async (args: string, ctx: any) => {
|
|
|
109
125
|
` escalation ${report.escalationAttempts} attempt(s) — grants refused for exceeding what the session held`,
|
|
110
126
|
` integrity ${report.ok ? "OK" : `${report.corrupt.length} UNPARSEABLE LINE(S)`}`,
|
|
111
127
|
];
|
|
112
|
-
for (const bad of report.corrupt.slice(0, 5)) lines.push(` line ${bad.line}: ${bad.
|
|
128
|
+
for (const bad of report.corrupt.slice(0, 5)) lines.push(` line ${bad.line}: ${bad.reason}`);
|
|
113
129
|
// ADR-0031, and R-51's lesson applied on the day the field was added rather than a release later: a field
|
|
114
130
|
// the writer sets and no diagnostic reads is one that needs `jq`, and `docs/SPEC.md` claims the executor is
|
|
115
131
|
// announced "per child in the ledger". `unknown` is shown only when present, because on a fresh ledger it
|
|
@@ -121,6 +137,9 @@ handler: async (args: string, ctx: any) => {
|
|
|
121
137
|
? ` · ${report.executors.unknown} not recorded (written before 0.16.0, which added the field)`
|
|
122
138
|
: ""),
|
|
123
139
|
);
|
|
140
|
+
if (report.workflowFacts > 0) {
|
|
141
|
+
lines.push(` workflow ${report.workflowFacts} provenance-labelled fact(s) — not counted as enforced children`);
|
|
142
|
+
}
|
|
124
143
|
// R-51. ADR-0018 advertises that the ledger answers "did these four children run the same
|
|
125
144
|
// instructions?" and "has this definition changed since?" — and nothing read `definitionDigest`, so
|
|
126
145
|
// both needed hand-written jq and the second was not reproducible with `sha256sum` (the digest covers
|
package/extensions/grants.ts
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { homedir } from "node:os";
|
|
26
|
+
import { join, resolve } from "node:path";
|
|
25
27
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
26
28
|
import { WILDCARD } from "../src/pi-tools.ts";
|
|
27
29
|
import { buildCatalog } from "../src/catalog.ts";
|
|
@@ -38,6 +40,11 @@ import { planWithApprovals } from "./run-delegation.ts";
|
|
|
38
40
|
import { createGrantsSession, loadProjectDefinitions, resolveExecutor, type GrantsSession } from "./session.ts";
|
|
39
41
|
import { reportSessionStart } from "./session-report.ts";
|
|
40
42
|
import { SPAWN_TOOLS, tripwireReason } from "./tripwire.ts";
|
|
43
|
+
import {
|
|
44
|
+
defaultDashboardPaths,
|
|
45
|
+
offerDashboardHandshake,
|
|
46
|
+
openDashboardCommand,
|
|
47
|
+
} from "../src/dashboard-handshake.ts";
|
|
41
48
|
|
|
42
49
|
export default function (pi: ExtensionAPI) {
|
|
43
50
|
// The path pi loads as the extension, so a child granted `tool:delegate` can be started with `-e <this>`.
|
|
@@ -51,6 +58,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
51
58
|
})();
|
|
52
59
|
|
|
53
60
|
const session = createGrantsSession(extensionPath);
|
|
61
|
+
const dashboardPluginRoot = fileURLToPath(new URL("../herdr-plugin/", import.meta.url));
|
|
62
|
+
const dashboardPaths = defaultDashboardPaths(
|
|
63
|
+
process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent"),
|
|
64
|
+
);
|
|
54
65
|
|
|
55
66
|
// Filled in by `registerDelegationTools` at the bottom of this function. A holder rather than a reordering,
|
|
56
67
|
// because the hooks below have to be registered before the tools and both need to call it — the tool
|
|
@@ -59,6 +70,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
59
70
|
|
|
60
71
|
pi.on("session_start", async (_event, ctx) => {
|
|
61
72
|
session.cwd = ctx.cwd;
|
|
73
|
+
// A routed child changes cwd. Publishing a relative ledger string unchanged therefore fragments one
|
|
74
|
+
// execution tree across workspaces. Resolve it once at the root's actual pi cwd; descendants inherit
|
|
75
|
+
// this absolute identity verbatim, and resolve(abs) remains abs at every deeper session start.
|
|
76
|
+
if (session.ledgerPath) session.ledgerPath = resolve(ctx.cwd, session.ledgerPath);
|
|
62
77
|
// The one case the stored-grant lookup can get wrong (ADR-0030). The factory reads the store keyed by
|
|
63
78
|
// `process.cwd()` because it runs before any hook and therefore before `ctx` exists — and S-5 forces
|
|
64
79
|
// that ordering, since whether `delegate` is registered is decided there. Almost always the two agree.
|
|
@@ -152,6 +167,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
152
167
|
"warning",
|
|
153
168
|
);
|
|
154
169
|
}
|
|
170
|
+
// Explicit installation handshake. It only runs in a Herdr-hosted TUI, asks once, and the only branch
|
|
171
|
+
// that links software is the literal human choice "Install and open".
|
|
172
|
+
try {
|
|
173
|
+
await offerDashboardHandshake({
|
|
174
|
+
mode: ctx.mode,
|
|
175
|
+
env: process.env,
|
|
176
|
+
pid: process.pid,
|
|
177
|
+
cwd: ctx.cwd,
|
|
178
|
+
ledgerPath: session.ledgerPath,
|
|
179
|
+
pluginRoot: dashboardPluginRoot,
|
|
180
|
+
preferencePath: dashboardPaths.preferencePath,
|
|
181
|
+
paneStatePath: dashboardPaths.paneStatePath,
|
|
182
|
+
ui: ctx.ui,
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
ctx.ui.notify(
|
|
186
|
+
`grants: dashboard installation handshake could not run (${error instanceof Error ? error.message : String(error)}). ` +
|
|
187
|
+
`Nothing was installed; /grants dashboard will report manual setup.`,
|
|
188
|
+
"warning",
|
|
189
|
+
);
|
|
190
|
+
}
|
|
155
191
|
} catch (error) {
|
|
156
192
|
// Rule 8 — fail closed, and be LOUD about it. Swallowing is still right: a startup fault must not
|
|
157
193
|
// reach the agent loop. Swallowing SILENTLY is what let R-60 exist, and would let the next one exist
|
|
@@ -316,6 +352,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
316
352
|
await loadProjectDefinitions(session, ctx.cwd);
|
|
317
353
|
delegation.refreshSpawnable();
|
|
318
354
|
}),
|
|
355
|
+
openDashboard: () => openDashboardCommand({
|
|
356
|
+
env: process.env,
|
|
357
|
+
pid: process.pid,
|
|
358
|
+
cwd: ctx.cwd,
|
|
359
|
+
ledgerPath: session.ledgerPath,
|
|
360
|
+
pluginRoot: dashboardPluginRoot,
|
|
361
|
+
paneStatePath: dashboardPaths.paneStatePath,
|
|
362
|
+
}),
|
|
319
363
|
previewDelegation: (name: string) =>
|
|
320
364
|
planWithApprovals(session, { task: "(preview)", agent: name }, {}, null),
|
|
321
365
|
},
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
|
|
14
14
|
import { DELEGATE_SUBJECT, shouldSeekApproval } from "../src/approval.ts";
|
|
15
15
|
import { planDelegation } from "../src/delegate.ts";
|
|
16
|
-
import { appendRecord, buildRecord } from "../src/ledger.ts";
|
|
17
16
|
import {
|
|
18
17
|
obtainApprovals,
|
|
19
18
|
republishable,
|
|
@@ -27,6 +26,8 @@ import type { GrantsSession } from "./session.ts";
|
|
|
27
26
|
import type { CorrelationMetadata } from "../src/correlation.ts";
|
|
28
27
|
import { GovernanceRefusal, refusal as structuredRefusal } from "../src/refusals.ts";
|
|
29
28
|
import { executePlannedChild, type DelegationOutcome } from "./execute-child.ts";
|
|
29
|
+
import { recordDelegationDecision, type ApprovalLedgerFacts } from "./delegation-ledger.ts";
|
|
30
|
+
import type { ExecutionOccurrenceIds } from "./execution-occurrence.ts";
|
|
30
31
|
import {
|
|
31
32
|
governedWorkspaceAccess,
|
|
32
33
|
prepareDelegationWorkspace,
|
|
@@ -178,7 +179,7 @@ export async function planWithApprovals(
|
|
|
178
179
|
export async function runOneDelegation(
|
|
179
180
|
session: GrantsSession,
|
|
180
181
|
spec: ChildSpec,
|
|
181
|
-
ids:
|
|
182
|
+
ids: ExecutionOccurrenceIds,
|
|
182
183
|
budget: number | undefined,
|
|
183
184
|
ctx: DelegationToolContext,
|
|
184
185
|
signal: AbortSignal | undefined,
|
|
@@ -207,7 +208,7 @@ export async function runOneDelegation(
|
|
|
207
208
|
paneId?: string;
|
|
208
209
|
/** The name herdr actually knows this child by — minted in `runHerdrPane`, so it cannot be derived. */
|
|
209
210
|
agentName?: string;
|
|
210
|
-
state?: "running" | "completed" | "failed";
|
|
211
|
+
state?: "starting" | "running" | "completed" | "failed";
|
|
211
212
|
}) => void;
|
|
212
213
|
/** Approvals already obtained by the caller — see `planWithApprovals`. `delegate_chain` uses it. */
|
|
213
214
|
preApproved?: InheritableApproval[];
|
|
@@ -218,6 +219,8 @@ export async function runOneDelegation(
|
|
|
218
219
|
* the question the chain's framed-rather-than-enforced handoff makes worth asking.
|
|
219
220
|
*/
|
|
220
221
|
taskFrom?: string;
|
|
222
|
+
/** Unique occurrence whose output composed this task; separate from the logical taskFrom position. */
|
|
223
|
+
taskFromExecutionId?: string;
|
|
221
224
|
/**
|
|
222
225
|
* What the caller's own gate decided, for the LEDGER — not for the plan.
|
|
223
226
|
*
|
|
@@ -228,10 +231,10 @@ export async function runOneDelegation(
|
|
|
228
231
|
* where nothing was ever gated — `/grants ledger` counted it in neither `bySource` nor `unattributed`, so it did
|
|
229
232
|
* not even show up as a gap, and ADR-0010's compensating control was blind to every chain step.
|
|
230
233
|
*/
|
|
231
|
-
approvalFacts?:
|
|
234
|
+
approvalFacts?: ApprovalLedgerFacts;
|
|
232
235
|
} = {},
|
|
233
236
|
): Promise<DelegationOutcome> {
|
|
234
|
-
const { onProgress, preApproved, taskFrom, approvalFacts } = options;
|
|
237
|
+
const { onProgress, preApproved, taskFrom, taskFromExecutionId, approvalFacts } = options;
|
|
235
238
|
// pi resolves a BARE model id to an unauthenticated provider and the child dies at startup — the id
|
|
236
239
|
// alone is not enough, it must be qualified with its provider (`Model<Api>` carries both).
|
|
237
240
|
const defaultModel = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
|
|
@@ -249,7 +252,12 @@ export async function runOneDelegation(
|
|
|
249
252
|
boundWorkspaceId: spec.workspace?.workspace_id,
|
|
250
253
|
boundContextId: spec.correlation?.context_id,
|
|
251
254
|
};
|
|
252
|
-
const extra = {
|
|
255
|
+
const extra = {
|
|
256
|
+
fanoutBudget: budget,
|
|
257
|
+
spawnId: ids.parentId,
|
|
258
|
+
childSpawnId: ids.childId,
|
|
259
|
+
childExecutionId: ids.executionId,
|
|
260
|
+
};
|
|
253
261
|
|
|
254
262
|
// ADR-0031: herdr was DEMANDED (`PI_GRANTS_HERDR=1`) and is not answering. Refused rather than relocated —
|
|
255
263
|
// the operator chose that over falling back, so the ledger can never name a child that ran somewhere nobody
|
|
@@ -269,7 +277,6 @@ export async function runOneDelegation(
|
|
|
269
277
|
let preparedWorkspace: PreparedWorkspace | undefined;
|
|
270
278
|
let approvalOutcome: ApprovalOutcome | undefined;
|
|
271
279
|
let plan: ReturnType<typeof planDelegation>;
|
|
272
|
-
let ledgerDenied = false;
|
|
273
280
|
|
|
274
281
|
if (spec.workspace && !executorRefusal) {
|
|
275
282
|
// Check non-liftable refusals before taking a lease, and take the lease before asking a human. This
|
|
@@ -283,6 +290,8 @@ export async function runOneDelegation(
|
|
|
283
290
|
spec: { ...spec.workspace, access: governedWorkspaceAccess(spec.workspace.access, plan.requested) },
|
|
284
291
|
correlation: spec.correlation,
|
|
285
292
|
childId: ids.childId,
|
|
293
|
+
executionId: ids.executionId,
|
|
294
|
+
parentExecutionId: ids.parentExecutionId,
|
|
286
295
|
signal,
|
|
287
296
|
ledgerPath: session.ledgerPath,
|
|
288
297
|
});
|
|
@@ -308,55 +317,19 @@ export async function runOneDelegation(
|
|
|
308
317
|
plan = { ...plan, ok: false, reason: message, refusal: structuredRefusal("EXECUTOR_UNAVAILABLE", message) };
|
|
309
318
|
}
|
|
310
319
|
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
await
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
// F8: real ids, not depth labels. Four concurrent siblings used to produce four lines identical
|
|
317
|
-
// except `ts`, so the ledger could not be joined to a result, a process, or the child's own
|
|
318
|
-
// lines one level down.
|
|
319
|
-
parentId: ids.parentId,
|
|
320
|
-
childId: ids.childId,
|
|
321
|
-
depth: plan.childDepth,
|
|
322
|
-
agentType: spec.agent ?? "delegate",
|
|
323
|
-
// ADR-0031: where this child actually ran. Read off the live session, which the probe has settled
|
|
324
|
-
// by now, so the record and the executor cannot disagree.
|
|
325
|
-
executor: session.executor.kind,
|
|
326
|
-
taskFrom,
|
|
327
|
-
requested: plan.requested,
|
|
328
|
-
parentGrant: session.ownGrant,
|
|
329
|
-
result: plan.result,
|
|
330
|
-
blocked: !plan.ok,
|
|
331
|
-
reason: plan.reason,
|
|
332
|
-
// `approvalOutcome` when this call's own gate ran; `approvalFacts` when a caller gated upfront on our behalf
|
|
333
|
-
// (a chain). Without the second, an approved chain step recorded nothing about the human who authorised it.
|
|
334
|
-
approved: approvalOutcome?.approved ?? approvalFacts?.approved,
|
|
335
|
-
approvalSources: approvalOutcome?.sources ?? approvalFacts?.sources,
|
|
336
|
-
approvalScopes: approvalOutcome?.recordedScopes ?? approvalFacts?.scopes,
|
|
337
|
-
approvalExpiresAt: approvalOutcome?.expiresAt ?? approvalFacts?.expiresAt,
|
|
338
|
-
approvalUses: approvalOutcome?.uses ?? approvalFacts?.uses,
|
|
339
|
-
humanDenied: approvalOutcome?.humanDenied ?? approvalFacts?.humanDenied,
|
|
340
|
-
gateOutcome: approvalOutcome?.gateOutcome,
|
|
341
|
-
// ADR-0018: taken from the PLAN, never re-derived here. The B-I3 lesson — a call site that
|
|
342
|
-
// recomputed the digest could record one the planner never used.
|
|
343
|
-
definitionDigest: plan.definitionDigest,
|
|
344
|
-
taskDigest: plan.taskDigest,
|
|
345
|
-
correlation: plan.correlation,
|
|
346
|
-
refusal: plan.refusal,
|
|
347
|
-
now: new Date(),
|
|
348
|
-
}),
|
|
349
|
-
).catch((error) => {
|
|
350
|
-
// G6 / A-R4 + B-I2: fail closed. This path PROVISIONS, so an unrecorded delegation would be a
|
|
351
|
-
// child running with granted capabilities and no audit line.
|
|
352
|
-
plan = {
|
|
353
|
-
...plan,
|
|
354
|
-
ok: false,
|
|
355
|
-
reason: `grants: ledger write failed, denying — ${String(error)}`,
|
|
356
|
-
refusal: structuredRefusal("LEDGER_WRITE_FAILED", `grants: ledger write failed, denying — ${String(error)}`),
|
|
357
|
-
};
|
|
358
|
-
ledgerDenied = true;
|
|
320
|
+
// The capability decision provisions the child, so this append remains load-bearing and fails closed.
|
|
321
|
+
try {
|
|
322
|
+
await recordDelegationDecision({
|
|
323
|
+
session, plan, ids, agent: spec.agent, taskFrom, taskFromExecutionId,
|
|
324
|
+
approval: approvalOutcome, approvalFacts,
|
|
359
325
|
});
|
|
326
|
+
} catch (error) {
|
|
327
|
+
plan = {
|
|
328
|
+
...plan,
|
|
329
|
+
ok: false,
|
|
330
|
+
reason: `grants: ledger write failed, denying — ${String(error)}`,
|
|
331
|
+
refusal: structuredRefusal("LEDGER_WRITE_FAILED", `grants: ledger write failed, denying — ${String(error)}`),
|
|
332
|
+
};
|
|
360
333
|
}
|
|
361
334
|
|
|
362
335
|
if (!plan.ok) {
|
|
@@ -368,7 +341,12 @@ export async function runOneDelegation(
|
|
|
368
341
|
// unwritable an unguarded call replaced the governance refusal, and its code, with a ledger error.
|
|
369
342
|
try {
|
|
370
343
|
await releaseDelegationWorkspace({
|
|
371
|
-
prepared: preparedWorkspace,
|
|
344
|
+
prepared: preparedWorkspace,
|
|
345
|
+
childId: ids.childId,
|
|
346
|
+
executionId: ids.executionId,
|
|
347
|
+
parentExecutionId: ids.parentExecutionId,
|
|
348
|
+
ledgerPath: session.ledgerPath,
|
|
349
|
+
reason: "refused",
|
|
372
350
|
});
|
|
373
351
|
} catch (error) {
|
|
374
352
|
plan = { ...plan, reason: `${plan.reason ?? "refused"}; workspace release record failed: ${String(error)}` };
|
|
@@ -389,6 +367,8 @@ export async function runOneDelegation(
|
|
|
389
367
|
plan,
|
|
390
368
|
agent: spec.agent,
|
|
391
369
|
childId: ids.childId,
|
|
370
|
+
executionId: ids.executionId,
|
|
371
|
+
parentExecutionId: ids.parentExecutionId,
|
|
392
372
|
cwd: ctx.cwd,
|
|
393
373
|
preparedWorkspace,
|
|
394
374
|
signal,
|
package/extensions/session.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
gatedFromEnv,
|
|
32
32
|
ENV_APPROVED,
|
|
33
33
|
ENV_DEPTH,
|
|
34
|
+
ENV_EXECUTION_ID,
|
|
34
35
|
ENV_FANOUT,
|
|
35
36
|
ENV_GATED,
|
|
36
37
|
ENV_GRANT,
|
|
@@ -95,7 +96,8 @@ export interface GrantsSession {
|
|
|
95
96
|
/** Bound variables that could not be read as non-negative integers — spawning is disabled, loudly. */
|
|
96
97
|
readonly malformedBounds: string[];
|
|
97
98
|
readonly gated: Capability[];
|
|
98
|
-
|
|
99
|
+
/** Resolved against the actual pi cwd at session start, then inherited verbatim by every descendant. */
|
|
100
|
+
ledgerPath?: string;
|
|
99
101
|
/**
|
|
100
102
|
* Which executor runs this session's children — ADR-0031.
|
|
101
103
|
*
|
|
@@ -108,8 +110,10 @@ export interface GrantsSession {
|
|
|
108
110
|
* before the probe, which is the same hazard as capturing `ownGrant` before the tool surface is observed.
|
|
109
111
|
*/
|
|
110
112
|
executor: ExecutorChoice;
|
|
111
|
-
/** This session's ledger identity; children descend from it (F8). */
|
|
113
|
+
/** This session's readable logical ledger identity; children descend from it (F8). */
|
|
112
114
|
readonly ownSpawnId: string;
|
|
115
|
+
/** Unique identity when this session is itself a governed child; roots have no governed parent. */
|
|
116
|
+
readonly ownExecutionId?: string;
|
|
113
117
|
/** Descendants this subtree may still create — the cardinality bound ADR-0008 never had. */
|
|
114
118
|
readonly fanoutBudget: number;
|
|
115
119
|
/** Whether `delegate` / `delegate_all` are registered at all (S-5). Decided on the INHERITED grant. */
|
|
@@ -284,6 +288,7 @@ export function createGrantsSession(extensionPath: string | undefined): GrantsSe
|
|
|
284
288
|
// `ownSpawnId` comes from the parent (F8), so ids form one tree across process boundaries instead of
|
|
285
289
|
// every level restarting at `d0` and the ledger becoming unjoinable.
|
|
286
290
|
ownSpawnId: process.env[ENV_PARENT_ID]?.trim() || `d${depth}`,
|
|
291
|
+
ownExecutionId: process.env[ENV_EXECUTION_ID]?.trim() || undefined,
|
|
287
292
|
// The cardinality bound ADR-0008 never had: it attenuates downward like depth, so a subtree can never
|
|
288
293
|
// create more descendants than its root was given — with no shared state, no lock and no counter file.
|
|
289
294
|
fanoutBudget: budgetFromEnv(process.env[ENV_FANOUT]),
|
|
@@ -69,6 +69,8 @@ export async function prepareDelegationWorkspace(input: {
|
|
|
69
69
|
spec: DelegationWorkspaceSpec;
|
|
70
70
|
correlation?: CorrelationMetadata;
|
|
71
71
|
childId: string;
|
|
72
|
+
executionId: string;
|
|
73
|
+
parentExecutionId: string | null;
|
|
72
74
|
signal?: AbortSignal;
|
|
73
75
|
ledgerPath?: string;
|
|
74
76
|
}): Promise<PreparedWorkspace> {
|
|
@@ -102,6 +104,8 @@ export async function prepareDelegationWorkspace(input: {
|
|
|
102
104
|
await appendLedgerEvent(
|
|
103
105
|
{ path: input.ledgerPath, strict: true },
|
|
104
106
|
buildWorkspaceLeaseEvent({
|
|
107
|
+
executionId: input.executionId,
|
|
108
|
+
parentExecutionId: input.parentExecutionId,
|
|
105
109
|
childId: input.childId,
|
|
106
110
|
workspaceId: workspace.workspaceId,
|
|
107
111
|
root: workspace.root,
|
|
@@ -125,6 +129,8 @@ export async function prepareDelegationWorkspace(input: {
|
|
|
125
129
|
await appendLedgerEvent(
|
|
126
130
|
{ path: input.ledgerPath, strict: true },
|
|
127
131
|
buildWorkspaceLeaseEvent({
|
|
132
|
+
executionId: input.executionId,
|
|
133
|
+
parentExecutionId: input.parentExecutionId,
|
|
128
134
|
childId: input.childId,
|
|
129
135
|
workspaceId: workspace.workspaceId,
|
|
130
136
|
root: workspace.root,
|
|
@@ -151,6 +157,8 @@ export async function prepareDelegationWorkspace(input: {
|
|
|
151
157
|
export async function releaseDelegationWorkspace(input: {
|
|
152
158
|
prepared: PreparedWorkspace | undefined;
|
|
153
159
|
childId: string;
|
|
160
|
+
executionId: string;
|
|
161
|
+
parentExecutionId: string | null;
|
|
154
162
|
ledgerPath?: string;
|
|
155
163
|
reason: string;
|
|
156
164
|
/** Deliberately keep the lease: a herdr writer tab would not close, so the pane may still be live. */
|
|
@@ -170,6 +178,8 @@ export async function releaseDelegationWorkspace(input: {
|
|
|
170
178
|
await appendLedgerEvent(
|
|
171
179
|
{ path: input.ledgerPath, strict: true },
|
|
172
180
|
buildWorkspaceLeaseEvent({
|
|
181
|
+
executionId: input.executionId,
|
|
182
|
+
parentExecutionId: input.parentExecutionId,
|
|
173
183
|
childId: input.childId,
|
|
174
184
|
workspaceId: input.prepared.workspace.workspaceId,
|
|
175
185
|
root: input.prepared.workspace.root,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
id = "pi-daddy.dashboard"
|
|
2
|
+
name = "pi-daddy dashboard"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
min_herdr_version = "0.8.0"
|
|
5
|
+
description = "Read-only live projection of a pi-daddy governance ledger"
|
|
6
|
+
platforms = ["linux", "macos", "windows"]
|
|
7
|
+
|
|
8
|
+
[[panes]]
|
|
9
|
+
id = "dashboard"
|
|
10
|
+
title = "PI-DADDY"
|
|
11
|
+
description = "Live governed execution and refusal tree"
|
|
12
|
+
placement = "split"
|
|
13
|
+
command = [
|
|
14
|
+
"node",
|
|
15
|
+
"--input-type=module",
|
|
16
|
+
"-e",
|
|
17
|
+
"import('node:url').then(({ pathToFileURL }) => import(pathToFileURL(process.env.HERDR_PLUGIN_ROOT + '/../dist/dashboard-cli.js').href)).then(m => m.runDashboard()).catch(e => { console.error(e); process.exitCode = 1; })"
|
|
18
|
+
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-daddy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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",
|
|
@@ -129,27 +129,39 @@
|
|
|
129
129
|
"types": "./dist/check-runner.d.ts",
|
|
130
130
|
"default": "./dist/check-runner.js"
|
|
131
131
|
},
|
|
132
|
+
"./dashboard-projection": {
|
|
133
|
+
"types": "./dist/dashboard-projection.d.ts",
|
|
134
|
+
"default": "./dist/dashboard-projection.js"
|
|
135
|
+
},
|
|
136
|
+
"./dashboard-render": {
|
|
137
|
+
"types": "./dist/dashboard-render.d.ts",
|
|
138
|
+
"default": "./dist/dashboard-render.js"
|
|
139
|
+
},
|
|
140
|
+
"./contracts/ledger/v3/ledger-event.schema.json": "./contracts/ledger/v3/ledger-event.schema.json",
|
|
141
|
+
"./contracts/ledger/v3/fixtures/*.json": "./contracts/ledger/v3/fixtures/*.json",
|
|
132
142
|
"./contracts/ledger/v2/ledger-event.schema.json": "./contracts/ledger/v2/ledger-event.schema.json",
|
|
133
143
|
"./contracts/ledger/v2/fixtures/*.json": "./contracts/ledger/v2/fixtures/*.json"
|
|
134
144
|
},
|
|
135
145
|
"bin": {
|
|
136
|
-
"pi-daddy": "./dist/cli.js"
|
|
146
|
+
"pi-daddy": "./dist/cli.js",
|
|
147
|
+
"pi-daddy-dashboard": "./dist/dashboard-cli.js"
|
|
137
148
|
},
|
|
138
149
|
"files": [
|
|
139
150
|
"dist",
|
|
140
151
|
"extensions",
|
|
141
152
|
"src",
|
|
142
153
|
"contracts",
|
|
154
|
+
"herdr-plugin",
|
|
143
155
|
"README.md",
|
|
144
156
|
"CHANGELOG.md",
|
|
145
157
|
"LICENSE"
|
|
146
158
|
],
|
|
147
159
|
"scripts": {
|
|
148
|
-
"build": "tsc -p tsconfig.json",
|
|
160
|
+
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
|
|
149
161
|
"typecheck": "tsc -p tsconfig.check.json",
|
|
150
162
|
"test": "node --test test/*.test.ts",
|
|
151
163
|
"test:integration": "node --test test-integration/*.it.ts",
|
|
152
|
-
"contracts:generate": "node scripts/generate-ledger-
|
|
164
|
+
"contracts:generate": "node scripts/generate-ledger-v3-contract.ts",
|
|
153
165
|
"prepack": "npm run build",
|
|
154
166
|
"test:smoke": "node scripts/smoke-installed.mjs",
|
|
155
167
|
"test:mutation": "node scripts/mutation-audit.mjs"
|
package/src/check-runner.ts
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
leaseAcquisitionOutcome,
|
|
20
20
|
leaseReleaseLedgerOutcome,
|
|
21
21
|
} from "./workspace.ts";
|
|
22
|
+
import { newExecutionId } from "./execution-id.ts";
|
|
23
|
+
import { isLedgerDisplayIdentifier } from "./ledger-identifiers.ts";
|
|
22
24
|
|
|
23
25
|
export interface CheckDefinition {
|
|
24
26
|
/** Absolute operator-owned executable. PATH lookup is deliberately not part of the contract. */
|
|
@@ -85,6 +87,12 @@ export function buildCheckEnvironment(
|
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
function validateDefinition(checkId: string, value: CheckDefinition | undefined): CheckDefinition {
|
|
90
|
+
// `check:<id>:<uuid>` is also the v3 childId, whose closed display-identifier bound is 512 characters.
|
|
91
|
+
if (!isLedgerDisplayIdentifier(checkId) || checkId.length > 460) {
|
|
92
|
+
throw new GovernanceRefusal(refusal(
|
|
93
|
+
"CHECK_CONFIGURATION_INVALID", `check ${JSON.stringify(checkId)} must be an ASCII identifier`, { check_id: checkId },
|
|
94
|
+
));
|
|
95
|
+
}
|
|
88
96
|
if (!value) {
|
|
89
97
|
throw new GovernanceRefusal(refusal(
|
|
90
98
|
"CHECK_NOT_CONFIGURED", `check ${JSON.stringify(checkId)} is not present in operator-owned configuration`, { check_id: checkId },
|
|
@@ -147,6 +155,8 @@ export async function runNamedCheck(input: {
|
|
|
147
155
|
// governed writer. `access` still records what the configured executable is intended to do.
|
|
148
156
|
const leaseAccess: WorkspaceAccess = "write";
|
|
149
157
|
const ownerId = `check:${input.checkId}:${randomUUID()}`;
|
|
158
|
+
const executionId = newExecutionId();
|
|
159
|
+
const parentExecutionId = null;
|
|
150
160
|
const correlation = normaliseCorrelation(input.correlation);
|
|
151
161
|
let lease: Awaited<ReturnType<typeof acquireWorkspaceLease>> | undefined;
|
|
152
162
|
let stagedDir: string | undefined;
|
|
@@ -163,6 +173,7 @@ export async function runNamedCheck(input: {
|
|
|
163
173
|
if (input.ledgerPath) await appendLedgerEvent(
|
|
164
174
|
{ path: input.ledgerPath, strict: true },
|
|
165
175
|
buildWorkspaceLeaseEvent({
|
|
176
|
+
executionId, parentExecutionId,
|
|
166
177
|
childId: ownerId, workspaceId: input.workspace.workspaceId, root: input.workspace.root, access: leaseAccess,
|
|
167
178
|
outcome: "refused",
|
|
168
179
|
correlation, now: new Date(),
|
|
@@ -178,6 +189,7 @@ export async function runNamedCheck(input: {
|
|
|
178
189
|
await appendLedgerEvent(
|
|
179
190
|
{ path: input.ledgerPath, strict: true },
|
|
180
191
|
buildWorkspaceLeaseEvent({
|
|
192
|
+
executionId, parentExecutionId,
|
|
181
193
|
childId: ownerId, workspaceId: input.workspace.workspaceId, root: input.workspace.root, access: leaseAccess,
|
|
182
194
|
outcome: leaseAcquisitionOutcome(leaseAccess, lease.recovered), recovered: lease.recovered,
|
|
183
195
|
correlation, now: new Date(),
|
|
@@ -256,6 +268,7 @@ export async function runNamedCheck(input: {
|
|
|
256
268
|
if (input.ledgerPath) await appendLedgerEvent(
|
|
257
269
|
{ path: input.ledgerPath, strict: true },
|
|
258
270
|
buildWorkspaceLeaseEvent({
|
|
271
|
+
executionId, parentExecutionId,
|
|
259
272
|
childId: ownerId, workspaceId: input.workspace.workspaceId, root: input.workspace.root, access: leaseAccess,
|
|
260
273
|
outcome: leaseReleaseLedgerOutcome(releaseOutcome, releaseReason), releaseReason, correlation, now: new Date(),
|
|
261
274
|
}),
|
|
@@ -274,6 +287,7 @@ export async function runNamedCheck(input: {
|
|
|
274
287
|
await appendLedgerEvent(
|
|
275
288
|
{ path: input.ledgerPath, strict: true },
|
|
276
289
|
buildCheckReceiptLedgerEvent({
|
|
290
|
+
executionId, parentExecutionId,
|
|
277
291
|
childId: ownerId, receiptId: receipt.receipt_id, workspaceId: input.workspace.workspaceId,
|
|
278
292
|
checkId: input.checkId, treeSha: receipt.tree_sha, correlation, now: ended,
|
|
279
293
|
}),
|
|
@@ -304,6 +318,7 @@ export async function runNamedCheck(input: {
|
|
|
304
318
|
onFailure: (cause) => { notes.push(`check refusal record failed: ${String(cause)}`); },
|
|
305
319
|
},
|
|
306
320
|
buildWorkspaceLeaseEvent({
|
|
321
|
+
executionId, parentExecutionId,
|
|
307
322
|
childId: ownerId, workspaceId: input.workspace.workspaceId, root: input.workspace.root, access: leaseAccess,
|
|
308
323
|
outcome: "refused",
|
|
309
324
|
refusal: error instanceof GovernanceRefusal
|
|
@@ -327,6 +342,7 @@ export async function runNamedCheck(input: {
|
|
|
327
342
|
onFailure: (cause) => { notes.push(`check lease release record failed: ${String(cause)}`); },
|
|
328
343
|
},
|
|
329
344
|
buildWorkspaceLeaseEvent({
|
|
345
|
+
executionId, parentExecutionId,
|
|
330
346
|
childId: ownerId, workspaceId: input.workspace.workspaceId, root: input.workspace.root, access: leaseAccess,
|
|
331
347
|
outcome: outcome === "lost"
|
|
332
348
|
? "lost"
|
package/src/correlation.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import type { DefinitionDigest } from "./definitions.ts";
|
|
3
3
|
import type { Capability } from "./resolve.ts";
|
|
4
4
|
import { GovernanceRefusal, refusal } from "./refusals.ts";
|
|
5
|
+
import { isLedgerCorrelationIdentifier } from "./ledger-identifiers.ts";
|
|
5
6
|
|
|
6
7
|
export type JsonPrimitive = string | number | boolean | null;
|
|
7
8
|
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
|
|
@@ -71,6 +72,13 @@ const CORRELATION_NUMERIC = new Set<keyof CorrelationMetadata>([
|
|
|
71
72
|
"event_seq", "last_change_seq", "last_authority_seq",
|
|
72
73
|
]);
|
|
73
74
|
|
|
75
|
+
// These fields are rendered as labels/identities. Treating arbitrary prose as an "id" let a model copy
|
|
76
|
+
// task or output text into the ledger and dashboard through correlation while staying schema-valid.
|
|
77
|
+
const CORRELATION_IDENTIFIERS = new Set<keyof CorrelationMetadata>([
|
|
78
|
+
"schema_version", "run_id", "task_id", "workspace_id", "context_id", "phase", "assurance",
|
|
79
|
+
"assurance_effective", "policy_label", "assurance_source",
|
|
80
|
+
]);
|
|
81
|
+
|
|
74
82
|
function correlationRefusal(message: string, details?: Record<string, string | number>): GovernanceRefusal {
|
|
75
83
|
return new GovernanceRefusal(refusal("CORRELATION_INVALID", `correlation metadata: ${message}`, details));
|
|
76
84
|
}
|
|
@@ -147,6 +155,9 @@ export function normaliseCorrelation(input: CorrelationMetadata | undefined): Co
|
|
|
147
155
|
{ field: key, limit: MAX_CORRELATION_FIELD_CHARS, actual: value.length },
|
|
148
156
|
);
|
|
149
157
|
}
|
|
158
|
+
if (CORRELATION_IDENTIFIERS.has(key as keyof CorrelationMetadata) && !isLedgerCorrelationIdentifier(value)) {
|
|
159
|
+
throw correlationRefusal(`${key} must be an ASCII identifier, not free-form text`, { field: key });
|
|
160
|
+
}
|
|
150
161
|
output[key] = value;
|
|
151
162
|
}
|
|
152
163
|
return output as CorrelationMetadata;
|