pi-daddy 0.18.1 → 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 +201 -0
- package/README.md +136 -23
- package/contracts/ledger/v2/README.md +59 -0
- package/contracts/ledger/v2/fixtures/capability-decision.json +95 -0
- package/contracts/ledger/v2/fixtures/check-receipt.json +40 -0
- package/contracts/ledger/v2/fixtures/child-lifecycle.json +42 -0
- package/contracts/ledger/v2/fixtures/workspace-lease.json +41 -0
- package/contracts/ledger/v2/ledger-event.schema.json +633 -0
- 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/approval-prompt.d.ts +2 -1
- package/dist/approval-prompt.d.ts.map +1 -1
- package/dist/approval-prompt.js +9 -0
- package/dist/approval-prompt.js.map +1 -1
- package/dist/approval.d.ts +4 -2
- package/dist/approval.d.ts.map +1 -1
- package/dist/approval.js +4 -0
- package/dist/approval.js.map +1 -1
- package/dist/capabilities.d.ts +90 -0
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +114 -3
- package/dist/capabilities.js.map +1 -1
- package/dist/catalog.d.ts +15 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +54 -3
- package/dist/catalog.js.map +1 -1
- package/dist/check-runner.d.ts.map +1 -1
- package/dist/check-runner.js +19 -7
- package/dist/check-runner.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +21 -1
- package/dist/cli.js.map +1 -1
- 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/definitions.d.ts.map +1 -1
- package/dist/definitions.js +7 -1
- package/dist/definitions.js.map +1 -1
- 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 +34 -4
- package/dist/delegate.js.map +1 -1
- package/dist/delegation-approval.d.ts.map +1 -1
- package/dist/delegation-approval.js +37 -12
- package/dist/delegation-approval.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/executor.d.ts +2 -1
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +1 -0
- package/dist/executor.js.map +1 -1
- package/dist/grant-env.d.ts +2 -0
- package/dist/grant-env.d.ts.map +1 -1
- package/dist/grant-env.js +26 -3
- package/dist/grant-env.js.map +1 -1
- 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/init.d.ts +13 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +35 -2
- package/dist/init.js.map +1 -1
- package/dist/lease-helper.d.ts +58 -0
- package/dist/lease-helper.d.ts.map +1 -0
- package/dist/lease-helper.js +94 -0
- package/dist/lease-helper.js.map +1 -0
- package/dist/lease-record.d.ts +15 -3
- package/dist/lease-record.d.ts.map +1 -1
- package/dist/lease-record.js.map +1 -1
- package/dist/ledger-events.d.ts +59 -15
- package/dist/ledger-events.d.ts.map +1 -1
- package/dist/ledger-events.js +67 -5
- 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 +27 -9
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +38 -6
- package/dist/ledger.js.map +1 -1
- package/dist/propagation.d.ts +19 -1
- package/dist/propagation.d.ts.map +1 -1
- package/dist/propagation.js +23 -1
- package/dist/propagation.js.map +1 -1
- package/dist/refusals.d.ts +1 -1
- package/dist/refusals.d.ts.map +1 -1
- package/dist/refusals.js +1 -0
- package/dist/refusals.js.map +1 -1
- package/dist/resolve.d.ts +10 -0
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +27 -3
- package/dist/resolve.js.map +1 -1
- package/dist/routing-authority.d.ts +71 -0
- package/dist/routing-authority.d.ts.map +1 -0
- package/dist/routing-authority.js +100 -0
- package/dist/routing-authority.js.map +1 -0
- 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/skill-packages.d.ts +11 -5
- package/dist/skill-packages.d.ts.map +1 -1
- package/dist/skill-packages.js +20 -11
- package/dist/skill-packages.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/dist/workspace-lease.d.ts +15 -3
- package/dist/workspace-lease.d.ts.map +1 -1
- package/dist/workspace-lease.js +81 -24
- package/dist/workspace-lease.js.map +1 -1
- package/dist/workspace.d.ts +25 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +142 -5
- package/dist/workspace.js.map +1 -1
- 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 +20 -14
- package/extensions/execute-child.ts +85 -21
- package/extensions/execution-occurrence.ts +20 -0
- package/extensions/grants-command.ts +33 -4
- package/extensions/grants.ts +50 -1
- package/extensions/init-command.ts +33 -2
- package/extensions/run-delegation.ts +36 -56
- package/extensions/session-report.ts +13 -20
- package/extensions/session.ts +16 -3
- package/extensions/workspace-runtime.ts +44 -4
- package/herdr-plugin/herdr-plugin.toml +18 -0
- package/package.json +22 -5
- package/src/approval-prompt.ts +2 -1
- package/src/approval.ts +4 -2
- package/src/capabilities.ts +120 -4
- package/src/catalog.ts +62 -4
- package/src/check-runner.ts +24 -8
- package/src/cli.ts +24 -1
- 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/definitions.ts +7 -1
- package/src/delegate-types.ts +6 -2
- package/src/delegate.ts +43 -4
- package/src/delegation-approval.ts +37 -12
- package/src/execution-id.ts +18 -0
- package/src/executor.ts +2 -1
- package/src/grant-env.ts +39 -7
- package/src/index.ts +26 -0
- package/src/init.ts +40 -2
- package/src/lease-helper.ts +97 -0
- package/src/lease-record.ts +15 -3
- package/src/ledger-events.ts +116 -28
- 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 +75 -12
- package/src/propagation.ts +24 -1
- package/src/refusals.ts +1 -0
- package/src/resolve.ts +29 -3
- package/src/routing-authority.ts +121 -0
- package/src/run-child.ts +35 -3
- package/src/run-herdr.ts +9 -0
- package/src/skill-packages.ts +20 -13
- package/src/workflow-fact-id.ts +16 -0
- package/src/workflow-facts.ts +65 -0
- package/src/workspace-lease.ts +84 -24
- package/src/workspace.ts +179 -6
|
@@ -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
|
|
@@ -330,9 +349,19 @@ handler: async (args: string, ctx: any) => {
|
|
|
330
349
|
` approvals ${sessionApprovals.size} this session, ${valid.size} persisted` +
|
|
331
350
|
`${inheritedApprovals.size > 0 ? `, ${inheritedApprovals.size} inherited` : ""}` +
|
|
332
351
|
` — /grants approvals`,
|
|
352
|
+
// Every kind the catalog can hold is named, and the numbers add up to the total on purpose. `workspace`
|
|
353
|
+
// was missing when ADR-0035's review added the kind: the total counted them and the breakdown did not,
|
|
354
|
+
// so `catalog 16 capabilities — 9 builtin, 0 extension, 2 skill, 2 agent-type` printed a sum of 13. And
|
|
355
|
+
// the comment that justified putting workspaces in the catalog said it was "for `/grants` to list what
|
|
356
|
+
// this session may route to", which nothing then did — the claim was in the commit and the behaviour
|
|
357
|
+
// was not. A kind added without a term here silently stops adding up.
|
|
333
358
|
` catalog ${catalog.all.length} capabilities — ` +
|
|
334
359
|
`${catalog.byKind("builtin").length} builtin, ${catalog.byKind("extension").length} extension, ` +
|
|
335
|
-
`${catalog.byKind("skill").length} skill, ${catalog.byKind("agentType").length} agent-type
|
|
360
|
+
`${catalog.byKind("skill").length} skill, ${catalog.byKind("agentType").length} agent-type, ` +
|
|
361
|
+
`${catalog.byKind("workspace").length} workspace`,
|
|
362
|
+
...(catalog.byKind("workspace").length > 0
|
|
363
|
+
? [` routable ${catalog.byKind("workspace").join(", ")} — held ones only are usable (ADR-0035)`]
|
|
364
|
+
: []),
|
|
336
365
|
];
|
|
337
366
|
// Runs the REAL planner AND the real approval step over each definition, so this listing cannot
|
|
338
367
|
// disagree with what a spawn would do — the R-28 lesson, kept structural rather than remembered.
|
package/extensions/grants.ts
CHANGED
|
@@ -22,9 +22,12 @@
|
|
|
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";
|
|
30
|
+
import { ENV_WORKSPACE_REGISTRY } from "../src/workspace.ts";
|
|
28
31
|
import { appendRecord, buildRecord } from "../src/ledger.ts";
|
|
29
32
|
import { openPaneCount, reapOpenPanesAsync } from "../src/pane-reaper.ts";
|
|
30
33
|
import {
|
|
@@ -37,6 +40,11 @@ import { planWithApprovals } from "./run-delegation.ts";
|
|
|
37
40
|
import { createGrantsSession, loadProjectDefinitions, resolveExecutor, type GrantsSession } from "./session.ts";
|
|
38
41
|
import { reportSessionStart } from "./session-report.ts";
|
|
39
42
|
import { SPAWN_TOOLS, tripwireReason } from "./tripwire.ts";
|
|
43
|
+
import {
|
|
44
|
+
defaultDashboardPaths,
|
|
45
|
+
offerDashboardHandshake,
|
|
46
|
+
openDashboardCommand,
|
|
47
|
+
} from "../src/dashboard-handshake.ts";
|
|
40
48
|
|
|
41
49
|
export default function (pi: ExtensionAPI) {
|
|
42
50
|
// The path pi loads as the extension, so a child granted `tool:delegate` can be started with `-e <this>`.
|
|
@@ -50,6 +58,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
50
58
|
})();
|
|
51
59
|
|
|
52
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
|
+
);
|
|
53
65
|
|
|
54
66
|
// Filled in by `registerDelegationTools` at the bottom of this function. A holder rather than a reordering,
|
|
55
67
|
// because the hooks below have to be registered before the tools and both need to call it — the tool
|
|
@@ -58,6 +70,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
58
70
|
|
|
59
71
|
pi.on("session_start", async (_event, ctx) => {
|
|
60
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);
|
|
61
77
|
// The one case the stored-grant lookup can get wrong (ADR-0030). The factory reads the store keyed by
|
|
62
78
|
// `process.cwd()` because it runs before any hook and therefore before `ctx` exists — and S-5 forces
|
|
63
79
|
// that ordering, since whether `delegate` is registered is decided there. Almost always the two agree.
|
|
@@ -151,6 +167,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
151
167
|
"warning",
|
|
152
168
|
);
|
|
153
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
|
+
}
|
|
154
191
|
} catch (error) {
|
|
155
192
|
// Rule 8 — fail closed, and be LOUD about it. Swallowing is still right: a startup fault must not
|
|
156
193
|
// reach the agent loop. Swallowing SILENTLY is what let R-60 exist, and would let the next one exist
|
|
@@ -216,7 +253,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
216
253
|
// Keep the handle: a concurrent `delegate` awaits this rather than reading a half-built catalog.
|
|
217
254
|
// The `catch` resolves to the CURRENT catalog rather than rejecting, so a failed refresh degrades
|
|
218
255
|
// to the previous view instead of failing every delegation in the session.
|
|
219
|
-
session.catalogReady = buildCatalog({
|
|
256
|
+
session.catalogReady = buildCatalog({
|
|
257
|
+
cwd: session.cwd,
|
|
258
|
+
observedTools: names,
|
|
259
|
+
registryPath: process.env[ENV_WORKSPACE_REGISTRY],
|
|
260
|
+
})
|
|
220
261
|
.then((c) => (session.catalog = c))
|
|
221
262
|
.catch(() => session.catalog);
|
|
222
263
|
} catch {
|
|
@@ -311,6 +352,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
311
352
|
await loadProjectDefinitions(session, ctx.cwd);
|
|
312
353
|
delegation.refreshSpawnable();
|
|
313
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
|
+
}),
|
|
314
363
|
previewDelegation: (name: string) =>
|
|
315
364
|
planWithApprovals(session, { task: "(preview)", agent: name }, {}, null),
|
|
316
365
|
},
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { discoverSkillPackages } from "../src/skill-packages.ts";
|
|
11
11
|
import { applyInit, planInit } from "../src/init.ts";
|
|
12
|
+
import { registeredWorkspaceIds } from "../src/workspace.ts";
|
|
12
13
|
import { saveGrant, grantStorePath } from "../src/grant-store.ts";
|
|
13
14
|
import { expandSubsumed, SUBSUMPTION, type Capability } from "../src/resolve.ts";
|
|
14
15
|
import type { GrantsSession } from "./session.ts";
|
|
@@ -50,7 +51,7 @@ export async function runInit(
|
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
const plan = planInit(packages, ctx.cwd);
|
|
54
|
+
const plan = planInit(packages, ctx.cwd, await registeredWorkspaceIds());
|
|
54
55
|
const outcome = await applyInit(plan);
|
|
55
56
|
const lines = [
|
|
56
57
|
`grants: ${plan.skills.length} definition(s) from ${packages.map((p) => `${p.name}@${p.version}`).join(", ")}`,
|
|
@@ -81,9 +82,28 @@ export async function runInit(
|
|
|
81
82
|
for (const name of neededBy) grant.add(`agent:${name}` as Capability);
|
|
82
83
|
continue;
|
|
83
84
|
}
|
|
85
|
+
// The consequence sentence is per capability, and it reads the gate **in effect for this session** —
|
|
86
|
+
// `session.gated`, which is `PI_GRANTS_GATED` when the operator set it and `DEFAULT_GATED` otherwise.
|
|
87
|
+
//
|
|
88
|
+
// The first version read `DEFAULT_GATED` directly, which is the compile-time constant `["tool:bash"]`, and
|
|
89
|
+
// `tool:bash` is consumed by the branch above — so the branch was **unreachable in every configuration**,
|
|
90
|
+
// and with `PI_GRANTS_GATED="tool:bash,tool:write"` (the value `renderGrantEnv` itself suggests) the
|
|
91
|
+
// dialog still printed the exact false sentence the fix claimed to remove. Dead code beside a claim that
|
|
92
|
+
// it worked, which is this project's failure mode, in the commit correcting that failure mode.
|
|
93
|
+
// No subsumption closure here, and the absence is deliberate. A `|| SUBSUMPTION[capability]…` disjunct was
|
|
94
|
+
// added and is **dead in every configuration**: `SUBSUMPTION` has one key, `tool:bash`, and `tool:bash` is
|
|
95
|
+
// consumed by the ternary below before `gatedAtSpawn` is ever read. Two reviewers and a mutation confirmed
|
|
96
|
+
// it changed no result — dead code beside a claim that it worked, in the hunk whose comment denounces
|
|
97
|
+
// exactly that. A second `SUBSUMPTION` entry is the moment to add it back.
|
|
98
|
+
const gatedAtSpawn = session.gated.includes(capability);
|
|
84
99
|
const answer = await ctx.ui.select(
|
|
85
100
|
`grants: grant ${capability} to sub-agents?\n needed by: ${neededBy.join(", ")}\n` +
|
|
86
|
-
` this can change your machine —
|
|
101
|
+
` this can change your machine — ` +
|
|
102
|
+
(capability === "tool:bash"
|
|
103
|
+
? "and bash also confers write and edit"
|
|
104
|
+
: gatedAtSpawn
|
|
105
|
+
? "a human is still asked at spawn time"
|
|
106
|
+
: "it is not gated, so no dialog at spawn time"),
|
|
87
107
|
["No", "Yes"],
|
|
88
108
|
);
|
|
89
109
|
if (answer === "Yes") {
|
|
@@ -121,6 +141,17 @@ export async function runInit(
|
|
|
121
141
|
lines.push(` ALREADY CONFERRED, not asked about: ${alreadyConferred.join(", ")}`);
|
|
122
142
|
}
|
|
123
143
|
if (declined.length > 0) lines.push(` withheld: ${declined.join("; ")}`);
|
|
144
|
+
// Routing is LISTED, never granted (ADR-0028) — so the operator has to be told it exists, or the listing is
|
|
145
|
+
// invisible to anyone running `/grants init` in-session rather than reading the generated file. 0.19.0 made
|
|
146
|
+
// `workspace:<id>` mandatory for routing, so this is also the migration hint, and a breaking change whose
|
|
147
|
+
// migration is only discoverable by opening a file is not much of a migration.
|
|
148
|
+
if (plan.routableWorkspaces.length > 0) {
|
|
149
|
+
lines.push(
|
|
150
|
+
` ROUTABLE WORKSPACES, listed and NOT granted: ${plan.routableWorkspaces.join(", ")}`,
|
|
151
|
+
` routing a child to one needs its id in the grant (ADR-0035); add the ones this project may use to ` +
|
|
152
|
+
`${plan.grantEnvPath}. Which worktree a child starts in is not something a package can declare for you.`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
124
155
|
lines.push(` live now (${finalGrant.length} capabilities) — no restart. /grants shows the verdicts.`);
|
|
125
156
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
126
157
|
}
|
|
@@ -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,
|
|
@@ -80,16 +80,6 @@ export async function reportSessionStart(session: GrantsSession, ctx: SessionRep
|
|
|
80
80
|
} catch {
|
|
81
81
|
/* never throw into the agent loop */
|
|
82
82
|
}
|
|
83
|
-
// R-47. `gatedBlocked` filters `requested`, and for a definition spawn `requested` is that
|
|
84
|
-
// definition's CEILING — which never contains `agent:<name>`, because the authorisation check
|
|
85
|
-
// (ADR-0017) is a separate, ungated branch. So `PI_GRANTS_GATED=agent:deploy`, written by an operator
|
|
86
|
-
// who read "it attenuates like any other capability" and meant "ask me before deploy runs", produces
|
|
87
|
-
// no dialog and no warning. It DOES bite when a definition passes the id down in its own
|
|
88
|
-
// `allowed-tools`, so the flag half-works — which is worse than not working, and is R-25's shape in
|
|
89
|
-
// the namespace ADR-0017 just promoted out of exactly that state.
|
|
90
|
-
//
|
|
91
|
-
// Warned rather than enforced: making it gate the spawn is a behaviour change and wants a decision.
|
|
92
|
-
// Silence is the part that is indefensible either way.
|
|
93
83
|
// `agent:*` grants no tools, but it authorises every definition in BOTH skill roots — including
|
|
94
84
|
// `~/.pi/agent/skills/`, which other software installs into, so ADR-0017's "an operator-authored
|
|
95
85
|
// file" is not true of everything it covers. Paired with a shell that is every body on disk running
|
|
@@ -103,16 +93,19 @@ export async function reportSessionStart(session: GrantsSession, ctx: SessionRep
|
|
|
103
93
|
"warning",
|
|
104
94
|
);
|
|
105
95
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
96
|
+
// REMOVED 2026-08-21. This warned that an `agent:` id in `PI_GRANTS_GATED` "does NOT gate spawning that
|
|
97
|
+
// definition — a human is never asked". It was R-47's PARTIAL fix (0.11.1) and became false one release
|
|
98
|
+
// later at 0.12.0, when ADR-0024's gate landed (`4673348`, "gating a definition asks before it runs") and
|
|
99
|
+
// a gated `agent:<name>` began blocking the spawn until somebody approved it. ADR-0024's own Costs section
|
|
100
|
+
// leaned on this warning — "mitigated by the fact that the warning shipped hours earlier tells them it
|
|
101
|
+
// currently does nothing" — and nothing retired it when its own decision made the mitigation false.
|
|
102
|
+
//
|
|
103
|
+
// So from 0.12.0 through 0.18.1 the session banner told operators to delete a gate that works, and
|
|
104
|
+
// `test-integration/governance.it.ts` REQUIRED it to, which is how a stale claim outlives the person who
|
|
105
|
+
// notices. It is R-28's shape in the direction that talks somebody OUT of a control. ADR-0035 then
|
|
106
|
+
// repeated the pattern one namespace over by claiming a `workspace:` gate that did not exist; both are
|
|
107
|
+
// gates now, so there is nothing inert left to warn about. Deleted rather than reworded — a warning with
|
|
108
|
+
// no live case is the next stale claim. R-134.
|
|
116
109
|
// R-34. `verifyLedger` existed and nothing ran it, so a torn line was detectable and undetected —
|
|
117
110
|
// and a check an operator has to know to run is not a control, it is a feature. Setting
|
|
118
111
|
// `PI_GRANTS_LEDGER` already means "I want an audit trail"; noticing that the trail is damaged is
|
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,
|
|
@@ -42,6 +43,7 @@ import {
|
|
|
42
43
|
import type { Capability } from "../src/resolve.ts";
|
|
43
44
|
import { loadDefinitions } from "../src/definitions.ts";
|
|
44
45
|
import { buildCatalog } from "../src/catalog.ts";
|
|
46
|
+
import { ENV_WORKSPACE_REGISTRY } from "../src/workspace.ts";
|
|
45
47
|
import { loadGrantSync, grantStorePath } from "../src/grant-store.ts";
|
|
46
48
|
import { republishable } from "./approvals.ts";
|
|
47
49
|
|
|
@@ -94,7 +96,8 @@ export interface GrantsSession {
|
|
|
94
96
|
/** Bound variables that could not be read as non-negative integers — spawning is disabled, loudly. */
|
|
95
97
|
readonly malformedBounds: string[];
|
|
96
98
|
readonly gated: Capability[];
|
|
97
|
-
|
|
99
|
+
/** Resolved against the actual pi cwd at session start, then inherited verbatim by every descendant. */
|
|
100
|
+
ledgerPath?: string;
|
|
98
101
|
/**
|
|
99
102
|
* Which executor runs this session's children — ADR-0031.
|
|
100
103
|
*
|
|
@@ -107,8 +110,10 @@ export interface GrantsSession {
|
|
|
107
110
|
* before the probe, which is the same hazard as capturing `ownGrant` before the tool surface is observed.
|
|
108
111
|
*/
|
|
109
112
|
executor: ExecutorChoice;
|
|
110
|
-
/** This session's ledger identity; children descend from it (F8). */
|
|
113
|
+
/** This session's readable logical ledger identity; children descend from it (F8). */
|
|
111
114
|
readonly ownSpawnId: string;
|
|
115
|
+
/** Unique identity when this session is itself a governed child; roots have no governed parent. */
|
|
116
|
+
readonly ownExecutionId?: string;
|
|
112
117
|
/** Descendants this subtree may still create — the cardinality bound ADR-0008 never had. */
|
|
113
118
|
readonly fanoutBudget: number;
|
|
114
119
|
/** Whether `delegate` / `delegate_all` are registered at all (S-5). Decided on the INHERITED grant. */
|
|
@@ -210,7 +215,14 @@ export interface GrantsSession {
|
|
|
210
215
|
*/
|
|
211
216
|
export async function loadProjectDefinitions(session: GrantsSession, cwd: string): Promise<void> {
|
|
212
217
|
session.definitions = await loadDefinitions(cwd);
|
|
213
|
-
session.catalogReady = buildCatalog({
|
|
218
|
+
session.catalogReady = buildCatalog({
|
|
219
|
+
cwd,
|
|
220
|
+
observedTools: session.observedTools,
|
|
221
|
+
// ADR-0035: `workspace:<id>` is a capability, so the registered ids belong in the catalog the same way
|
|
222
|
+
// discovered definitions do — for `/grants` to list what this session may route to and for `init` to
|
|
223
|
+
// scaffold them. Read live rather than cached at load, because the registry is an operator file.
|
|
224
|
+
registryPath: process.env[ENV_WORKSPACE_REGISTRY],
|
|
225
|
+
});
|
|
214
226
|
session.catalog = await session.catalogReady;
|
|
215
227
|
}
|
|
216
228
|
|
|
@@ -276,6 +288,7 @@ export function createGrantsSession(extensionPath: string | undefined): GrantsSe
|
|
|
276
288
|
// `ownSpawnId` comes from the parent (F8), so ids form one tree across process boundaries instead of
|
|
277
289
|
// every level restarting at `d0` and the ledger becoming unjoinable.
|
|
278
290
|
ownSpawnId: process.env[ENV_PARENT_ID]?.trim() || `d${depth}`,
|
|
291
|
+
ownExecutionId: process.env[ENV_EXECUTION_ID]?.trim() || undefined,
|
|
279
292
|
// The cardinality bound ADR-0008 never had: it attenuates downward like depth, so a subtree can never
|
|
280
293
|
// create more descendants than its root was given — with no shared state, no lock and no counter file.
|
|
281
294
|
fanoutBudget: budgetFromEnv(process.env[ENV_FANOUT]),
|
|
@@ -23,12 +23,39 @@ export interface DelegationWorkspaceSpec {
|
|
|
23
23
|
access: WorkspaceAccess;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Tools that cannot change a worktree.
|
|
28
|
+
*
|
|
29
|
+
* **`tool:delegate` is deliberately NOT here, and that is a scope decision.** It was added, and it was true of
|
|
30
|
+
* the capability and false of the code: a routed child's cwd IS the leased root, `PI_GRANTS_LEDGER` was
|
|
31
|
+
* passed through relative (`init` scaffolds `.pi/grants.jsonl`), and a `read` lease takes no kernel lock — so
|
|
32
|
+
* two delegating children classified `read` both created `.pi/` in one worktree and neither excluded the
|
|
33
|
+
* other. Making `tool:delegate` non-writing requires first making every child-inherited path absolute, and
|
|
34
|
+
* that is a change to the ledger and lease plumbing rather than to ADR-0035. Tracked as R-141.
|
|
35
|
+
*/
|
|
26
36
|
const KNOWN_READ_ONLY_TOOLS = new Set(["tool:read", "tool:grep", "tool:find", "tool:ls"]);
|
|
27
37
|
|
|
28
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* A model may ask for stricter coordination but cannot label a write-capable grant read-only.
|
|
40
|
+
*
|
|
41
|
+
* **Only a TOOL is considered, and that is a fix rather than an oversight.** The check used to require *every*
|
|
42
|
+
* requested capability to be a known read-only tool, which was correct while `tool:` and `ext:` were the only
|
|
43
|
+
* things that could appear — and became wrong the moment ADR-0035's review made `workspace:<id>` grantable to
|
|
44
|
+
* a child. Measured: `governedWorkspaceAccess("read", ["tool:read", "workspace:staging"])` returned `"write"`,
|
|
45
|
+
* so the intended shape — *route this child read-only and let it route its own grandchild* — silently took an
|
|
46
|
+
* exclusive writer lease, blocked every other writer on that root, and recorded `access: "write"` in the
|
|
47
|
+
* ledger when the operator had asked for `read`. A record asserting a stronger claim than anybody made is the
|
|
48
|
+
* same failure this whole review is about, pointing the other way.
|
|
49
|
+
*
|
|
50
|
+
* A `workspace:`, `agent:` or `skill:` id confers no filesystem ability whatsoever: routing chooses a
|
|
51
|
+
* directory, `agent:` authorises a definition whose ceiling is still clipped to the child's own grant, and
|
|
52
|
+
* `skill:` loads instructions. None of them can write, so none of them should force a writer lease. If a
|
|
53
|
+
* descendant does hold a write tool, that tool is in `requested` and this check refuses on its own terms.
|
|
54
|
+
*/
|
|
29
55
|
export function governedWorkspaceAccess(declared: WorkspaceAccess, requested: readonly Capability[]): WorkspaceAccess {
|
|
30
56
|
if (declared === "write") return "write";
|
|
31
|
-
|
|
57
|
+
const tools = requested.filter((c) => c.startsWith("tool:") || c.startsWith("ext:"));
|
|
58
|
+
return tools.every((capability) => KNOWN_READ_ONLY_TOOLS.has(capability)) ? "read" : "write";
|
|
32
59
|
}
|
|
33
60
|
|
|
34
61
|
export interface PreparedWorkspace {
|
|
@@ -42,6 +69,8 @@ export async function prepareDelegationWorkspace(input: {
|
|
|
42
69
|
spec: DelegationWorkspaceSpec;
|
|
43
70
|
correlation?: CorrelationMetadata;
|
|
44
71
|
childId: string;
|
|
72
|
+
executionId: string;
|
|
73
|
+
parentExecutionId: string | null;
|
|
45
74
|
signal?: AbortSignal;
|
|
46
75
|
ledgerPath?: string;
|
|
47
76
|
}): Promise<PreparedWorkspace> {
|
|
@@ -75,6 +104,8 @@ export async function prepareDelegationWorkspace(input: {
|
|
|
75
104
|
await appendLedgerEvent(
|
|
76
105
|
{ path: input.ledgerPath, strict: true },
|
|
77
106
|
buildWorkspaceLeaseEvent({
|
|
107
|
+
executionId: input.executionId,
|
|
108
|
+
parentExecutionId: input.parentExecutionId,
|
|
78
109
|
childId: input.childId,
|
|
79
110
|
workspaceId: workspace.workspaceId,
|
|
80
111
|
root: workspace.root,
|
|
@@ -98,6 +129,8 @@ export async function prepareDelegationWorkspace(input: {
|
|
|
98
129
|
await appendLedgerEvent(
|
|
99
130
|
{ path: input.ledgerPath, strict: true },
|
|
100
131
|
buildWorkspaceLeaseEvent({
|
|
132
|
+
executionId: input.executionId,
|
|
133
|
+
parentExecutionId: input.parentExecutionId,
|
|
101
134
|
childId: input.childId,
|
|
102
135
|
workspaceId: workspace.workspaceId,
|
|
103
136
|
root: workspace.root,
|
|
@@ -124,6 +157,8 @@ export async function prepareDelegationWorkspace(input: {
|
|
|
124
157
|
export async function releaseDelegationWorkspace(input: {
|
|
125
158
|
prepared: PreparedWorkspace | undefined;
|
|
126
159
|
childId: string;
|
|
160
|
+
executionId: string;
|
|
161
|
+
parentExecutionId: string | null;
|
|
127
162
|
ledgerPath?: string;
|
|
128
163
|
reason: string;
|
|
129
164
|
/** Deliberately keep the lease: a herdr writer tab would not close, so the pane may still be live. */
|
|
@@ -133,13 +168,18 @@ export async function releaseDelegationWorkspace(input: {
|
|
|
133
168
|
// A retained lease writes no `state: "released"`, so the record stays `active` and the NEXT owner reads
|
|
134
169
|
// it as a crash — the exact blame `retained` was added to remove. Marking it keeps the successor honest;
|
|
135
170
|
// R-104 was fixed in the release event's wording only.
|
|
136
|
-
|
|
137
|
-
|
|
171
|
+
// **Ledger what happened, not what was intended (R-152).** This used to discard `markRetained`'s result and
|
|
172
|
+
// write the word "retained" unconditionally, so a helper that had already died, or a lease already released,
|
|
173
|
+
// was still recorded as a pane that may still be live.
|
|
174
|
+
const outcome: LeaseReleaseOutcome = input.retain
|
|
175
|
+
? await input.prepared.lease.markRetained(input.reason)
|
|
138
176
|
: await input.prepared.lease.release(input.reason);
|
|
139
177
|
if (input.ledgerPath) {
|
|
140
178
|
await appendLedgerEvent(
|
|
141
179
|
{ path: input.ledgerPath, strict: true },
|
|
142
180
|
buildWorkspaceLeaseEvent({
|
|
181
|
+
executionId: input.executionId,
|
|
182
|
+
parentExecutionId: input.parentExecutionId,
|
|
143
183
|
childId: input.childId,
|
|
144
184
|
workspaceId: input.prepared.workspace.workspaceId,
|
|
145
185
|
root: input.prepared.workspace.root,
|