pi-daddy 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +187 -0
  2. package/LICENSE +21 -0
  3. package/README.md +584 -0
  4. package/dist/approval-prompt.d.ts +116 -0
  5. package/dist/approval-prompt.d.ts.map +1 -0
  6. package/dist/approval-prompt.js +124 -0
  7. package/dist/approval-prompt.js.map +1 -0
  8. package/dist/approval-store.d.ts +149 -0
  9. package/dist/approval-store.d.ts.map +1 -0
  10. package/dist/approval-store.js +311 -0
  11. package/dist/approval-store.js.map +1 -0
  12. package/dist/approval.d.ts +221 -0
  13. package/dist/approval.d.ts.map +1 -0
  14. package/dist/approval.js +244 -0
  15. package/dist/approval.js.map +1 -0
  16. package/dist/capabilities.d.ts +31 -0
  17. package/dist/capabilities.d.ts.map +1 -0
  18. package/dist/capabilities.js +45 -0
  19. package/dist/capabilities.js.map +1 -0
  20. package/dist/catalog.d.ts +81 -0
  21. package/dist/catalog.d.ts.map +1 -0
  22. package/dist/catalog.js +164 -0
  23. package/dist/catalog.js.map +1 -0
  24. package/dist/definitions.d.ts +101 -0
  25. package/dist/definitions.d.ts.map +1 -0
  26. package/dist/definitions.js +182 -0
  27. package/dist/definitions.js.map +1 -0
  28. package/dist/delegate.d.ts +133 -0
  29. package/dist/delegate.d.ts.map +1 -0
  30. package/dist/delegate.js +261 -0
  31. package/dist/delegate.js.map +1 -0
  32. package/dist/fanout.d.ts +62 -0
  33. package/dist/fanout.d.ts.map +1 -0
  34. package/dist/fanout.js +84 -0
  35. package/dist/fanout.js.map +1 -0
  36. package/dist/file-lock.d.ts +54 -0
  37. package/dist/file-lock.d.ts.map +1 -0
  38. package/dist/file-lock.js +142 -0
  39. package/dist/file-lock.js.map +1 -0
  40. package/dist/index.d.ts +7 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +7 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/ledger-report.d.ts +106 -0
  45. package/dist/ledger-report.d.ts.map +1 -0
  46. package/dist/ledger-report.js +157 -0
  47. package/dist/ledger-report.js.map +1 -0
  48. package/dist/ledger.d.ts +139 -0
  49. package/dist/ledger.d.ts.map +1 -0
  50. package/dist/ledger.js +94 -0
  51. package/dist/ledger.js.map +1 -0
  52. package/dist/pane-reaper.d.ts +49 -0
  53. package/dist/pane-reaper.d.ts.map +1 -0
  54. package/dist/pane-reaper.js +120 -0
  55. package/dist/pane-reaper.js.map +1 -0
  56. package/dist/pi-tools.d.ts +20 -0
  57. package/dist/pi-tools.d.ts.map +1 -0
  58. package/dist/pi-tools.js +21 -0
  59. package/dist/pi-tools.js.map +1 -0
  60. package/dist/propagation.d.ts +185 -0
  61. package/dist/propagation.d.ts.map +1 -0
  62. package/dist/propagation.js +255 -0
  63. package/dist/propagation.js.map +1 -0
  64. package/dist/resolve.d.ts +110 -0
  65. package/dist/resolve.d.ts.map +1 -0
  66. package/dist/resolve.js +181 -0
  67. package/dist/resolve.js.map +1 -0
  68. package/dist/run-child.d.ts +50 -0
  69. package/dist/run-child.d.ts.map +1 -0
  70. package/dist/run-child.js +109 -0
  71. package/dist/run-child.js.map +1 -0
  72. package/dist/run-herdr.d.ts +91 -0
  73. package/dist/run-herdr.d.ts.map +1 -0
  74. package/dist/run-herdr.js +306 -0
  75. package/dist/run-herdr.js.map +1 -0
  76. package/dist/spawn.d.ts +60 -0
  77. package/dist/spawn.d.ts.map +1 -0
  78. package/dist/spawn.js +116 -0
  79. package/dist/spawn.js.map +1 -0
  80. package/extensions/approvals.ts +285 -0
  81. package/extensions/delegation.ts +237 -0
  82. package/extensions/grants-command.ts +310 -0
  83. package/extensions/grants.ts +346 -0
  84. package/extensions/run-delegation.ts +270 -0
  85. package/extensions/session.ts +234 -0
  86. package/package.json +122 -0
  87. package/src/approval-prompt.ts +222 -0
  88. package/src/approval-store.ts +383 -0
  89. package/src/approval.ts +364 -0
  90. package/src/capabilities.ts +51 -0
  91. package/src/catalog.ts +186 -0
  92. package/src/definitions.ts +233 -0
  93. package/src/delegate.ts +383 -0
  94. package/src/fanout.ts +96 -0
  95. package/src/file-lock.ts +142 -0
  96. package/src/index.ts +59 -0
  97. package/src/ledger-report.ts +227 -0
  98. package/src/ledger.ts +214 -0
  99. package/src/pane-reaper.ts +132 -0
  100. package/src/pi-tools.ts +25 -0
  101. package/src/propagation.ts +308 -0
  102. package/src/resolve.ts +235 -0
  103. package/src/run-child.ts +149 -0
  104. package/src/run-herdr.ts +357 -0
  105. package/src/spawn.ts +156 -0
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Run a governed child in a herdr pane — ADR-0016 point 6.
3
+ *
4
+ * The second executor for the same plan. `runChild` spawns `pi` directly and captures its stdout;
5
+ * `runHerdrPane` asks herdr to launch it in a visible, attachable terminal pane. `planSpawn` produces the
6
+ * argv either way, so the grant is identical and only the *place it runs* differs.
7
+ *
8
+ * **Why go through herdr's CLI rather than the third-party `pi-herdr` extension.** That extension exposes
9
+ * `agentArgs` and `env` as MODEL-facing tool parameters (R-30), which hands a model an argv array and the
10
+ * environment variable the grant travels on. Here the model chooses a definition and a task; this package
11
+ * builds the argv. Measured facts this relies on (`docs/probes/g16-herdr`):
12
+ *
13
+ * - `herdr agent start … -- <args>` delivers argv **verbatim**, echoed back in the reply.
14
+ * - `--tools` is enforced inside a pane exactly as it is for a direct spawn; `--no-tools` yields none.
15
+ * - `herdr agent start` has **no `--env`**, but `tab create` / `pane split` do, and a pane's environment
16
+ * reaches the shell that launches the agent — verified by reading `$PI_GRANTS_GRANT` back out of a
17
+ * pane created with it. That is how the grant, depth and ledger path propagate on this path.
18
+ *
19
+ * **What a pane is not: a boundary.** It is a terminal. `--tools` remains the enforcement point, ADR-0012's
20
+ * `bash` escape is unchanged, and a pane is *attachable by design*, so a human can type into a governed
21
+ * child. Humans are not this project's threat model, but nothing here should be read as containing one.
22
+ */
23
+ import { execFile } from "node:child_process";
24
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
25
+ import { tmpdir } from "node:os";
26
+ import { join } from "node:path";
27
+ import { DEFAULT_MAX_OUTPUT_BYTES, DEFAULT_TIMEOUT_MS } from "./run-child.js";
28
+ import { trackPane, untrackPane } from "./pane-reaper.js";
29
+ /**
30
+ * Move a multi-line `--append-system-prompt` out of argv, because herdr cannot encode it.
31
+ *
32
+ * **Measured.** `herdr agent start` types the argv into the pane's shell, so a value containing newlines
33
+ * is rejected outright: `invalid_agent_argument — agent arguments cannot be encoded safely for the target
34
+ * shell`. A definition's `SKILL.md` body is always multi-line, so every `delegate({agent})` spawn would
35
+ * fail on this path.
36
+ *
37
+ * pi accepts a **file path** there as readily as literal text (`resolvePromptInput` + `existsSync` in
38
+ * `dist/core/resource-loader.js`), so the fix is to write the body to a temp file and pass its path — one
39
+ * short, shell-safe argument.
40
+ *
41
+ * The split lives here rather than in `planSpawn` because the constraint is **herdr's**, not pi's: the
42
+ * direct executor passes the same text inline with no trouble, and a plan builder that pre-emptively wrote
43
+ * temp files for everybody would be paying one executor's tax on both paths.
44
+ */
45
+ export function splitSystemPrompt(args) {
46
+ const at = args.indexOf("--append-system-prompt");
47
+ if (at === -1 || at + 1 >= args.length)
48
+ return { args };
49
+ return { args: [...args.slice(0, at), ...args.slice(at + 2)], systemPrompt: args[at + 1] };
50
+ }
51
+ /** Statuses herdr reports for a settled agent. `blocked` counts: it is waiting for a human, not working. */
52
+ const TERMINAL = new Set(["idle", "done", "blocked"]);
53
+ /** How often to poll `agent get` while waiting for the child to settle. */
54
+ export const POLL_INTERVAL_MS = 750;
55
+ /** How often to retry `agent start` while a freshly created pane is still reaching its shell prompt. */
56
+ export const PANE_READY_POLL_MS = 300;
57
+ const defaultExec = (args) => new Promise((settle) => {
58
+ execFile("herdr", args, { maxBuffer: 32 * 1024 * 1024 }, (error, stdout, stderr) => {
59
+ const code = error && typeof error.code === "number" ? error.code : error ? 1 : 0;
60
+ settle({ code, stdout: String(stdout), stderr: String(stderr) });
61
+ });
62
+ });
63
+ /**
64
+ * Parse herdr's JSON envelope. Every command replies `{id, result}` or `{id, error:{code,message}}`.
65
+ *
66
+ * `stderr` is folded into the message because the first end-to-end run failed with an EMPTY stdout and the
67
+ * real reason on stderr, producing the useless diagnostic "unparseable herdr reply: ". A wrapper that
68
+ * hides the substrate's own error message costs more time than it saves.
69
+ */
70
+ function parseReply(reply) {
71
+ try {
72
+ const parsed = JSON.parse(reply.stdout);
73
+ if (parsed.error)
74
+ return { error: parsed.error.message ?? parsed.error.code ?? "herdr reported an error" };
75
+ return { result: parsed.result };
76
+ }
77
+ catch {
78
+ // A non-JSON reply is a herdr-version or PATH problem, not a governance decision. Surfaced as a spawn
79
+ // error so the caller reports "could not start" rather than "the child produced nothing".
80
+ const detail = [reply.stdout.trim(), reply.stderr.trim()].filter((t) => t.length > 0).join(" | ");
81
+ return { error: `unparseable herdr reply: ${detail.slice(0, 300) || "(no output)"}` };
82
+ }
83
+ }
84
+ /**
85
+ * Run one governed child in a pane and return its output.
86
+ *
87
+ * Deliberately returns `ChildRunResult` — the same shape as `runChild` — so the extension can choose an
88
+ * executor without knowing which one it got.
89
+ */
90
+ export async function runHerdrPane(request) {
91
+ const exec = request.exec ?? defaultExec;
92
+ const timeoutMs = request.timeoutMs ?? DEFAULT_TIMEOUT_MS;
93
+ const maxOutputBytes = request.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
94
+ const deadline = Date.now() + timeoutMs;
95
+ const empty = { code: null, text: "", truncated: false, timedOut: false, aborted: false };
96
+ // A-R3's rule, kept on this path too: checked BEFORE anything is created. An `AbortSignal` does not
97
+ // replay, so a check after the first `await` cannot see an abort that already happened — and we would
98
+ // have left a pane behind for a run that was never wanted.
99
+ if (request.signal?.aborted)
100
+ return { ...empty, aborted: true };
101
+ // `--print` makes pi process the prompt and exit, so it never reaches the interactive readiness that
102
+ // `herdr agent start` waits for — the agent is simply never detected. Found by the first end-to-end run,
103
+ // which failed inside herdr with an empty reply. Caught here instead, naming the flag, because a
104
+ // mismatch between a plan and its executor is a caller bug and should read like one.
105
+ if (request.args.includes("--print") || request.args.includes("-p")) {
106
+ return {
107
+ ...empty,
108
+ spawnError: "herdr needs an INTERACTIVE agent, but the plan contains --print, which makes pi exit immediately " +
109
+ "and never be detected. Build the plan with `print: false` for this executor.",
110
+ };
111
+ }
112
+ // A multi-line system prompt becomes a file; see `splitSystemPrompt`. Removed in `cleanup`, so a
113
+ // definition's instructions do not accumulate in /tmp across a fan-out.
114
+ const split = splitSystemPrompt(request.args);
115
+ let promptDir;
116
+ let effectiveArgs = split.args;
117
+ if (split.systemPrompt !== undefined) {
118
+ try {
119
+ promptDir = await mkdtemp(join(tmpdir(), "grants-herdr-"));
120
+ const file = join(promptDir, "system-prompt.md");
121
+ await writeFile(file, split.systemPrompt, "utf8");
122
+ effectiveArgs = [...split.args, "--append-system-prompt", file];
123
+ }
124
+ catch (error) {
125
+ return { ...empty, spawnError: `could not stage the system prompt for herdr: ${String(error)}` };
126
+ }
127
+ }
128
+ const create = ["tab", "create", "--label", request.name, "--cwd", request.cwd];
129
+ if (request.workspace)
130
+ create.push("--workspace", request.workspace);
131
+ for (const [key, value] of Object.entries(request.env))
132
+ create.push("--env", `${key}=${value}`);
133
+ const created = parseReply(await exec(create));
134
+ if (created.error)
135
+ return { ...empty, spawnError: `herdr tab create failed: ${created.error}` };
136
+ const rootPane = (created.result?.root_pane ?? {});
137
+ const paneId = rootPane.pane_id;
138
+ const tabId = rootPane.tab_id;
139
+ // **Tracked BEFORE the pane-id check, not after.** A tab can exist from the moment this reply is parsed,
140
+ // so registering later leaves a window — one herdr round-trip wide — in which a killed process orphans a
141
+ // tab nothing would reap. The normal path had no such window and the error path did, which is backwards:
142
+ // the error path is the one more likely to be taken while something is already going wrong.
143
+ if (tabId && !request.keepPane)
144
+ trackPane({ tab: tabId, name: request.name, promptDir });
145
+ if (!paneId) {
146
+ // The tab may exist even though the reply carried no pane id, and this return used to be BEFORE
147
+ // `cleanup` was defined — so the one path where herdr half-succeeded was the one that leaked a tab.
148
+ if (tabId && !request.keepPane) {
149
+ const reply = await exec(["tab", "close", tabId]).catch(() => undefined);
150
+ // Same rule as `cleanup`: untrack only what is provably gone, so a close herdr refused stays the
151
+ // reaper's problem rather than being dropped on the assumption that it worked.
152
+ if (reply !== undefined && !parseReply(reply).error)
153
+ untrackPane(tabId);
154
+ }
155
+ // `keepPane` keeps the staged prompt for the same reason `cleanup` does — a human inspecting the pane
156
+ // may want to see what the child was told. This branch used to remove it unconditionally, which threw
157
+ // that away on the one path where there is no agent in the pane to ask instead.
158
+ if (promptDir && !request.keepPane)
159
+ await rm(promptDir, { recursive: true, force: true }).catch(() => undefined);
160
+ return { ...empty, spawnError: "herdr tab create returned no pane id" };
161
+ }
162
+ /** Close what we opened, whatever happened. A leaked pane per child is how fan-out fills a workspace. */
163
+ const cleanup = async () => {
164
+ await exec(["agent", "stop", request.name]).catch(() => undefined);
165
+ let closed = false;
166
+ if (!request.keepPane && tabId) {
167
+ // **The reply must be PARSED, not merely awaited.** `defaultExec` resolves with `{code: 1}` on
168
+ // failure and never rejects, so the `.catch` here was dead code and a herdr that REFUSED to close the
169
+ // pane looked identical to one that closed it. The pane was then untracked, so the exit reaper — the
170
+ // one thing built for exactly this failure — would not retry it. The single case the reaper exists
171
+ // for was the case that disabled it.
172
+ const reply = await exec(["tab", "close", tabId]).catch(() => undefined);
173
+ closed = reply !== undefined && !parseReply(reply).error;
174
+ }
175
+ // Kept when the pane is kept: a human inspecting the pane may want to see what the child was told.
176
+ if (promptDir && !request.keepPane)
177
+ await rm(promptDir, { recursive: true, force: true }).catch(() => undefined);
178
+ // Untrack only what is genuinely gone. A pane we failed to close stays registered so `exit` tries once
179
+ // more; `openPaneCount()` is what a test asserts to prove the registry does not grow per delegation.
180
+ if (tabId && (closed || request.keepPane))
181
+ untrackPane(tabId);
182
+ };
183
+ try {
184
+ const started = await startAgent(exec, request.name, paneId, effectiveArgs, deadline);
185
+ if (started.error)
186
+ return { ...empty, spawnError: `herdr agent start failed: ${started.error}` };
187
+ // The state counter BEFORE prompting is what makes the wait correct — see the R-33 note below.
188
+ const before = seqOf(started.result);
189
+ const prompted = parseReply(await exec(["agent", "prompt", request.name, request.prompt]));
190
+ if (prompted.error)
191
+ return { ...empty, spawnError: `herdr agent prompt failed: ${prompted.error}` };
192
+ const settled = await waitForSettled(exec, request, before, deadline);
193
+ if (settled.aborted || settled.timedOut) {
194
+ // Still read: a timed-out child usually produced something, and a partial answer labelled partial is
195
+ // more useful than none. R-03's rule — a missing result must never look like an empty one.
196
+ const partial = await readPane(exec, request.name, maxOutputBytes);
197
+ return { ...empty, ...settled, text: partial.text, truncated: partial.truncated };
198
+ }
199
+ if (settled.spawnError)
200
+ return { ...empty, spawnError: settled.spawnError };
201
+ const out = await readPane(exec, request.name, maxOutputBytes);
202
+ return {
203
+ code: settled.status === "blocked" ? 1 : 0,
204
+ text: settled.status === "blocked"
205
+ ? `${out.text}\n\n[grants] this agent is BLOCKED waiting for a human in pane ${paneId}.`
206
+ : out.text,
207
+ truncated: out.truncated,
208
+ timedOut: false,
209
+ aborted: false,
210
+ };
211
+ }
212
+ finally {
213
+ await cleanup();
214
+ }
215
+ }
216
+ /**
217
+ * Start the agent, retrying while the pane is still coming up.
218
+ *
219
+ * **Measured, and only visible once automated.** A pane created by `tab create` is not immediately at a
220
+ * shell prompt, and `herdr agent start` requires one — it fails with
221
+ * `agent_pane_busy: … is not an available shell`. Driving the two commands by hand hid this completely,
222
+ * because the think-time between them was longer than the shell took to start; the first scripted run hit
223
+ * it every time.
224
+ *
225
+ * Retried rather than preceded by a fixed sleep: a sleep long enough for a loaded machine is wasted on
226
+ * every spawn, and a fan-out pays it per child. Only the busy condition is retried — any other error is a
227
+ * real failure and returns immediately.
228
+ */
229
+ async function startAgent(exec, name, paneId, args, deadline) {
230
+ for (;;) {
231
+ const reply = parseReply(await exec(["agent", "start", name, "--kind", "pi", "--pane", paneId, "--", ...args]));
232
+ if (!reply.error)
233
+ return reply;
234
+ const busy = /not an available shell|agent_pane_busy/.test(reply.error);
235
+ if (!busy || Date.now() >= deadline)
236
+ return reply;
237
+ await new Promise((r) => setTimeout(r, PANE_READY_POLL_MS));
238
+ }
239
+ }
240
+ function seqOf(result) {
241
+ const agent = (result?.agent ?? {});
242
+ return typeof agent.state_change_seq === "number" ? agent.state_change_seq : -1;
243
+ }
244
+ /**
245
+ * Wait for the child to settle, without accepting the state it was already in.
246
+ *
247
+ * **R-33, measured.** `herdr agent wait --until idle` called right after `agent prompt` returned
248
+ * *immediately*, matching the agent's **pre-existing** idle state with `state_change_seq` unchanged — a
249
+ * reply indistinguishable from a completed run. For fan-out that is not an inconvenience but a
250
+ * correctness bug: an orchestrator would "collect" N children that never ran and merge N empty results
251
+ * into a confident summary (R-03 with a new cause).
252
+ *
253
+ * So this polls `agent get` and requires **both** that the status is terminal **and** that
254
+ * `state_change_seq` has advanced past the value observed before prompting. `agent wait` is deliberately
255
+ * not used at all: its contract cannot express "settled *after* this point".
256
+ */
257
+ async function waitForSettled(exec, request, before, deadline) {
258
+ for (;;) {
259
+ if (request.signal?.aborted)
260
+ return { aborted: true };
261
+ if (Date.now() >= deadline)
262
+ return { timedOut: true };
263
+ const reply = parseReply(await exec(["agent", "get", request.name]));
264
+ if (reply.error)
265
+ return { spawnError: `herdr agent get failed: ${reply.error}` };
266
+ const agent = (reply.result?.agent ?? reply.result ?? {});
267
+ const status = agent.agent_status;
268
+ const seq = typeof agent.state_change_seq === "number" ? agent.state_change_seq : -1;
269
+ if (status && TERMINAL.has(status) && seq > before)
270
+ return { status };
271
+ await new Promise((r) => setTimeout(r, Math.min(POLL_INTERVAL_MS, Math.max(0, deadline - Date.now()))));
272
+ }
273
+ }
274
+ /**
275
+ * Read the pane's contents.
276
+ *
277
+ * `agent read` is the ONE command that does not return herdr's JSON envelope — it writes the terminal's
278
+ * text straight to stdout. Running it through `parseReply` turned every successful read into
279
+ * "unparseable herdr reply", i.e. reported the child's actual answer as a failure to read it. Found by the
280
+ * end-to-end run; the unit fake had been written to the envelope shape and so agreed with the bug.
281
+ *
282
+ * A JSON envelope is still accepted first, because an `error` reply here IS JSON and must not be mistaken
283
+ * for terminal output.
284
+ */
285
+ async function readPane(exec, name, maxOutputBytes) {
286
+ const reply = await exec(["agent", "read", name]);
287
+ let text;
288
+ try {
289
+ const parsed = JSON.parse(reply.stdout);
290
+ if (parsed.error) {
291
+ return { text: `[grants] could not read the agent pane: ${parsed.error.message ?? "unknown error"}`, truncated: false };
292
+ }
293
+ const raw = parsed.result?.output ?? parsed.result?.text ?? parsed.result?.content ?? "";
294
+ text = typeof raw === "string" ? raw : JSON.stringify(raw);
295
+ }
296
+ catch {
297
+ text = reply.stdout;
298
+ }
299
+ if (Buffer.byteLength(text) <= maxOutputBytes)
300
+ return { text, truncated: false };
301
+ // Keep the TAIL, not the head: a terminal's useful content is its most recent output, and the head is
302
+ // the startup banner. `runChild` keeps the head because it streams and must stop a runaway producer;
303
+ // here the output is already complete, so the choice is free and the tail is the answer.
304
+ return { text: text.slice(-maxOutputBytes), truncated: true };
305
+ }
306
+ //# sourceMappingURL=run-herdr.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-herdr.js","sourceRoot":"","sources":["../src/run-herdr.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Build the argv for a governed descendant pi process.
3
+ *
4
+ * The enforcement point is pi core, not this package: `--tools` and `--no-tools` hard-block extension
5
+ * tools, and an explicitly `-e`-loaded extension cannot re-add its tool past them (verified,
6
+ * docs/probes/pi-fabric-eval probes 9–11). So governance reduces to "compute the allowlist correctly
7
+ * and hand it to pi", with no runtime inside the descendant.
8
+ */
9
+ import { type Capability } from "./resolve.ts";
10
+ export interface SpawnPlanInput {
11
+ effective: Capability[];
12
+ prompt: string;
13
+ model?: string;
14
+ provider?: string;
15
+ thinking?: string;
16
+ /** Session file path, or omit for an ephemeral child. */
17
+ sessionFile?: string;
18
+ /** Non-interactive by default: a governed child should not prompt a human. */
19
+ print?: boolean;
20
+ /**
21
+ * Absolute path per skill NAME (`review` -> `/…/review`), as the catalog discovered them.
22
+ *
23
+ * R-32. Only skills named by a `skill:` capability in `effective` are passed, and a granted skill
24
+ * absent from this map is reported in `unresolvedSkills` rather than dropped.
25
+ */
26
+ skillPaths?: Record<string, string>;
27
+ /**
28
+ * Let the child load `AGENTS.md` / `CLAUDE.md`. **Default false** — see the `--no-context-files`
29
+ * note in `planSpawn`.
30
+ */
31
+ contextFiles?: boolean;
32
+ /**
33
+ * Instructions appended to the child's system prompt — a definition's `SKILL.md` body (ADR-0016).
34
+ *
35
+ * Appended rather than replacing, so pi's own coding-assistant prompt survives underneath and a
36
+ * definition only has to describe its own job.
37
+ */
38
+ systemPrompt?: string;
39
+ }
40
+ export interface SpawnPlan {
41
+ args: string[];
42
+ /** The tool names pi will allow, for the ledger. `null` means no tools at all. */
43
+ allowlist: string[] | null;
44
+ /** Skill paths handed to the child, for the ledger. */
45
+ skills: string[];
46
+ /**
47
+ * Granted `skill:` capabilities with no known path. **Non-empty means the caller should refuse**:
48
+ * the child would silently lack a capability its grant says it holds.
49
+ */
50
+ unresolvedSkills: Capability[];
51
+ }
52
+ /**
53
+ * Produce `pi` arguments enforcing `effective`.
54
+ *
55
+ * Note the `--no-tools` branch: pi rejects an empty `--tools` list, and passing no flag at all would
56
+ * silently fall back to pi's defaults — which is the opposite of a zero grant. An empty grant must be
57
+ * expressed explicitly.
58
+ */
59
+ export declare function planSpawn(input: SpawnPlanInput): SpawnPlan;
60
+ //# sourceMappingURL=spawn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spawn.d.ts","sourceRoot":"","sources":["../src/spawn.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAEnE,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,UAAU,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,kFAAkF;IAClF,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC3B,uDAAuD;IACvD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB;;;OAGG;IACH,gBAAgB,EAAE,UAAU,EAAE,CAAC;CAChC;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,SAAS,CA8D1D"}
package/dist/spawn.js ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Build the argv for a governed descendant pi process.
3
+ *
4
+ * The enforcement point is pi core, not this package: `--tools` and `--no-tools` hard-block extension
5
+ * tools, and an explicitly `-e`-loaded extension cannot re-add its tool past them (verified,
6
+ * docs/probes/pi-fabric-eval probes 9–11). So governance reduces to "compute the allowlist correctly
7
+ * and hand it to pi", with no runtime inside the descendant.
8
+ */
9
+ import { toPiToolsAllowlist } from "./resolve.js";
10
+ /**
11
+ * Produce `pi` arguments enforcing `effective`.
12
+ *
13
+ * Note the `--no-tools` branch: pi rejects an empty `--tools` list, and passing no flag at all would
14
+ * silently fall back to pi's defaults — which is the opposite of a zero grant. An empty grant must be
15
+ * expressed explicitly.
16
+ */
17
+ export function planSpawn(input) {
18
+ const allowlist = toPiToolsAllowlist(input.effective);
19
+ const args = [];
20
+ if (input.print !== false)
21
+ args.push("--print");
22
+ if (input.provider)
23
+ args.push("--provider", input.provider);
24
+ if (input.model)
25
+ args.push("--model", input.model);
26
+ if (input.thinking)
27
+ args.push("--thinking", input.thinking);
28
+ if (input.sessionFile)
29
+ args.push("--session", input.sessionFile);
30
+ else
31
+ args.push("--no-session");
32
+ // Disable discovery so ambient user extensions cannot widen a governed child's surface. Explicit
33
+ // `-e` paths would still load, so this package never passes one.
34
+ args.push("--no-extensions");
35
+ // R-32. `--no-extensions` governs EXTENSIONS ONLY — measured, not assumed: a child spawned with
36
+ // `--tools read` still loaded all eight of the operator's skills and `CLAUDE.md`
37
+ // (`docs/probes/g16-herdr` §4-5). Skills are injected into the system prompt rather than passed as
38
+ // tools, so `--tools` cannot reach them and the `skill:` namespace enforced nothing at all.
39
+ //
40
+ // `--no-skills` is unconditional and `--skill` is added on top, because that is exactly how pi
41
+ // resolves them (`dist/core/resource-loader.js:329`): with `noSkills` set, discovered skills are
42
+ // dropped and explicitly-passed paths are kept. Passing `--skill` WITHOUT `--no-skills` would add to
43
+ // the discovered set instead of replacing it — an allowlist that widens, which is the failure
44
+ // direction this package exists to prevent.
45
+ const skills = [];
46
+ const unresolvedSkills = [];
47
+ for (const capability of input.effective) {
48
+ if (!capability.startsWith("skill:"))
49
+ continue;
50
+ const path = input.skillPaths?.[capability.slice(6)];
51
+ if (path)
52
+ skills.push(path);
53
+ else
54
+ unresolvedSkills.push(capability);
55
+ }
56
+ args.push("--no-skills");
57
+ for (const path of skills)
58
+ args.push("--skill", path);
59
+ // Context files are model-directing text that NO capability describes and no ledger line records, so
60
+ // a child inheriting them holds influence its grant does not express. Under ADR-0012's threat model
61
+ // — prompt injection explicitly in scope — an untrusted repository's `CLAUDE.md` reaching a governed
62
+ // child is the injection vector. Off by default therefore, but deliberately still expressible:
63
+ // inheriting a project's conventions is often exactly what an operator wants, and that should be a
64
+ // decision they make rather than one this package makes silently in either direction.
65
+ if (!input.contextFiles)
66
+ args.push("--no-context-files");
67
+ // Prompt templates are the third resource class that `--no-extensions` does not cover — found in the
68
+ // banner of the very run that verified the two above. Lower risk than skills (a template is expanded
69
+ // when a human invokes `/name`, not injected into the system prompt) but withheld for consistency:
70
+ // under a herdr backend a governed child runs in an attachable pane WITH a human, so "only what the
71
+ // grant names" must not depend on which executor ran it.
72
+ args.push("--no-prompt-templates");
73
+ // A definition's body, so the child IS the reviewer rather than merely holding a reviewer's tools.
74
+ // Note this is operator-authored text from a file on disk, not a model-chosen string — the model
75
+ // picks WHICH definition, never its contents. That is what keeps it out of `neutralisePrompt`'s
76
+ // remit: the G1 hazard is a model-controlled string reaching a parser, and this is not one.
77
+ if (input.systemPrompt)
78
+ args.push("--append-system-prompt", input.systemPrompt);
79
+ if (allowlist)
80
+ args.push("--tools", allowlist.join(","));
81
+ else
82
+ args.push("--no-tools");
83
+ args.push(neutralisePrompt(input.prompt));
84
+ return { args, allowlist, skills, unresolvedSkills };
85
+ }
86
+ /**
87
+ * Make a model-controlled task unparseable by pi's CLI.
88
+ *
89
+ * pi's usage is `pi [options] [@files...] [messages...]`, and both of the non-message forms are reached
90
+ * by the FIRST CHARACTER of an argv element:
91
+ *
92
+ * - `@…` is resolved as a file and its contents injected into the child's prompt — absolute paths, `~`
93
+ * expansion, no sandbox. This happens in `main.js` before any tool is constructed, so `--tools` and
94
+ * `--no-tools` never apply to it. A child granted nothing at all still reads the file. Verified
95
+ * against pi 0.83.0 (review finding A-C1 / B-C7, and `docs/probes/g1-argv`).
96
+ * - `-…` is parsed as a flag, and pi ships `--approve` ("trust project-local files for this run").
97
+ *
98
+ * The task comes from the model, so this is the one place in the package where a model-authored string
99
+ * would otherwise reach a parser that outranks the enforcement point. A single leading space removes it
100
+ * from both branches: pi does not trim before dispatching on the prefix (measured), and a leading space
101
+ * is semantically nil inside a prompt.
102
+ *
103
+ * **It is applied unconditionally, and that is the point.** Rewriting only arguments that start with
104
+ * `@` or `-` would encode pi 0.83.0's current parser into this package and silently re-open the hole
105
+ * the moment pi — or an extension registering its own flags — adds a third prefix. The guarantee here is
106
+ * positional rather than pattern-based: the first character of that argv element is never the task's.
107
+ *
108
+ * Deliberately NOT a refusal. The review also suggested rejecting `@`/`-` tasks in `planDelegation` so
109
+ * they land in the ledger. Neutralising costs no false refusals and needs no judgement about which
110
+ * prompts are legitimate, so it is the whole fix; recording an attempt is an audit question (G6), not a
111
+ * security one, and is left to that group.
112
+ */
113
+ function neutralisePrompt(prompt) {
114
+ return ` ${prompt}`;
115
+ }
116
+ //# sourceMappingURL=spawn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spawn.js","sourceRoot":"","sources":["../src/spawn.ts"],"names":[],"mappings":"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"}