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.
- package/CHANGELOG.md +187 -0
- package/LICENSE +21 -0
- package/README.md +584 -0
- package/dist/approval-prompt.d.ts +116 -0
- package/dist/approval-prompt.d.ts.map +1 -0
- package/dist/approval-prompt.js +124 -0
- package/dist/approval-prompt.js.map +1 -0
- package/dist/approval-store.d.ts +149 -0
- package/dist/approval-store.d.ts.map +1 -0
- package/dist/approval-store.js +311 -0
- package/dist/approval-store.js.map +1 -0
- package/dist/approval.d.ts +221 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +244 -0
- package/dist/approval.js.map +1 -0
- package/dist/capabilities.d.ts +31 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +45 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/catalog.d.ts +81 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +164 -0
- package/dist/catalog.js.map +1 -0
- package/dist/definitions.d.ts +101 -0
- package/dist/definitions.d.ts.map +1 -0
- package/dist/definitions.js +182 -0
- package/dist/definitions.js.map +1 -0
- package/dist/delegate.d.ts +133 -0
- package/dist/delegate.d.ts.map +1 -0
- package/dist/delegate.js +261 -0
- package/dist/delegate.js.map +1 -0
- package/dist/fanout.d.ts +62 -0
- package/dist/fanout.d.ts.map +1 -0
- package/dist/fanout.js +84 -0
- package/dist/fanout.js.map +1 -0
- package/dist/file-lock.d.ts +54 -0
- package/dist/file-lock.d.ts.map +1 -0
- package/dist/file-lock.js +142 -0
- package/dist/file-lock.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/ledger-report.d.ts +106 -0
- package/dist/ledger-report.d.ts.map +1 -0
- package/dist/ledger-report.js +157 -0
- package/dist/ledger-report.js.map +1 -0
- package/dist/ledger.d.ts +139 -0
- package/dist/ledger.d.ts.map +1 -0
- package/dist/ledger.js +94 -0
- package/dist/ledger.js.map +1 -0
- package/dist/pane-reaper.d.ts +49 -0
- package/dist/pane-reaper.d.ts.map +1 -0
- package/dist/pane-reaper.js +120 -0
- package/dist/pane-reaper.js.map +1 -0
- package/dist/pi-tools.d.ts +20 -0
- package/dist/pi-tools.d.ts.map +1 -0
- package/dist/pi-tools.js +21 -0
- package/dist/pi-tools.js.map +1 -0
- package/dist/propagation.d.ts +185 -0
- package/dist/propagation.d.ts.map +1 -0
- package/dist/propagation.js +255 -0
- package/dist/propagation.js.map +1 -0
- package/dist/resolve.d.ts +110 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +181 -0
- package/dist/resolve.js.map +1 -0
- package/dist/run-child.d.ts +50 -0
- package/dist/run-child.d.ts.map +1 -0
- package/dist/run-child.js +109 -0
- package/dist/run-child.js.map +1 -0
- package/dist/run-herdr.d.ts +91 -0
- package/dist/run-herdr.d.ts.map +1 -0
- package/dist/run-herdr.js +306 -0
- package/dist/run-herdr.js.map +1 -0
- package/dist/spawn.d.ts +60 -0
- package/dist/spawn.d.ts.map +1 -0
- package/dist/spawn.js +116 -0
- package/dist/spawn.js.map +1 -0
- package/extensions/approvals.ts +285 -0
- package/extensions/delegation.ts +237 -0
- package/extensions/grants-command.ts +310 -0
- package/extensions/grants.ts +346 -0
- package/extensions/run-delegation.ts +270 -0
- package/extensions/session.ts +234 -0
- package/package.json +122 -0
- package/src/approval-prompt.ts +222 -0
- package/src/approval-store.ts +383 -0
- package/src/approval.ts +364 -0
- package/src/capabilities.ts +51 -0
- package/src/catalog.ts +186 -0
- package/src/definitions.ts +233 -0
- package/src/delegate.ts +383 -0
- package/src/fanout.ts +96 -0
- package/src/file-lock.ts +142 -0
- package/src/index.ts +59 -0
- package/src/ledger-report.ts +227 -0
- package/src/ledger.ts +214 -0
- package/src/pane-reaper.ts +132 -0
- package/src/pi-tools.ts +25 -0
- package/src/propagation.ts +308 -0
- package/src/resolve.ts +235 -0
- package/src/run-child.ts +149 -0
- package/src/run-herdr.ts +357 -0
- 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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"}
|
package/dist/spawn.d.ts
ADDED
|
@@ -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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"}
|