pi-daddy 0.35.0 → 0.36.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 +46 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -1
- package/dist/cli.js.map +1 -1
- package/dist/kernel/bounded-read.d.ts +57 -0
- package/dist/kernel/bounded-read.d.ts.map +1 -0
- package/dist/kernel/bounded-read.js +100 -0
- package/dist/kernel/bounded-read.js.map +1 -0
- package/dist/kernel/catalog.d.ts +12 -1
- package/dist/kernel/catalog.d.ts.map +1 -1
- package/dist/kernel/catalog.js +12 -5
- package/dist/kernel/catalog.js.map +1 -1
- package/dist/kernel/definitions.d.ts +19 -1
- package/dist/kernel/definitions.d.ts.map +1 -1
- package/dist/kernel/definitions.js +34 -9
- package/dist/kernel/definitions.js.map +1 -1
- package/dist/kernel/skill-packages.d.ts.map +1 -1
- package/dist/kernel/skill-packages.js +20 -9
- package/dist/kernel/skill-packages.js.map +1 -1
- package/dist/kernel/workspace.d.ts +7 -1
- package/dist/kernel/workspace.d.ts.map +1 -1
- package/dist/kernel/workspace.js +39 -81
- package/dist/kernel/workspace.js.map +1 -1
- package/extensions/grants-command.ts +7 -0
- package/extensions/init-command.ts +7 -1
- package/extensions/session-report.ts +15 -0
- package/extensions/session.ts +14 -1
- package/package.json +1 -1
- package/src/cli.ts +9 -1
- package/src/kernel/bounded-read.ts +125 -0
- package/src/kernel/catalog.ts +42 -21
- package/src/kernel/definitions.ts +38 -8
- package/src/kernel/skill-packages.ts +26 -9
- package/src/kernel/workspace.ts +43 -109
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* Names, ceilings and bytes are checked before they can enter the generated shell grant.
|
|
6
6
|
*/
|
|
7
7
|
import { readdir, readFile, realpath } from "node:fs/promises";
|
|
8
|
+
import { readBoundedBytes } from "./bounded-read.js";
|
|
8
9
|
import { homedir } from "node:os";
|
|
9
10
|
import { join, resolve, sep } from "node:path";
|
|
10
|
-
import { ceilingForDefinition, parseSkillDefinition } from "./definitions.js";
|
|
11
|
+
import { ceilingForDefinition, parseSkillDefinition, DEFINITION_MAX_BYTES, DEFINITION_READ_TIMEOUT_MS, } from "./definitions.js";
|
|
11
12
|
import { resolveSkillResources, skillResourceName } from "./skill-resources.js";
|
|
12
13
|
import { WILDCARD } from "./pi-tools.js";
|
|
13
14
|
import { AGENT_WILDCARD, WORKSPACE_WILDCARD } from "./resolve.js";
|
|
@@ -102,13 +103,18 @@ async function readSkill(packageDir, entry) {
|
|
|
102
103
|
// smoke test one day earlier and not applied here.
|
|
103
104
|
const realPackageDir = await realpath(packageDir).catch(() => packageDir);
|
|
104
105
|
for (const path of [join(target, "SKILL.md"), ...(target.endsWith(".md") ? [target] : [])]) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
// **Bounded with the SAME limit the runtime loader uses, because the two must agree about which
|
|
107
|
+
// definitions exist.** Review measured the divergence: a 2 MiB `SKILL.md` was refused by
|
|
108
|
+
// `loadDefinitions` and accepted here, so `pi-daddy init` wrote `agent:advice` into the operator's grant
|
|
109
|
+
// for a definition the session could never load, and the eventual `delegate` said only
|
|
110
|
+
// `unknown agent "advice"`. An unreadable entry lands in `unreadable`, which `init` already prints.
|
|
111
|
+
const read = await readBoundedBytes(path, {
|
|
112
|
+
maxBytes: DEFINITION_MAX_BYTES,
|
|
113
|
+
timeoutMs: DEFINITION_READ_TIMEOUT_MS,
|
|
114
|
+
});
|
|
115
|
+
if (!read.ok)
|
|
110
116
|
continue;
|
|
111
|
-
|
|
117
|
+
const bytes = read.bytes;
|
|
112
118
|
const realPath = await realpath(path).catch(() => path);
|
|
113
119
|
if (!realPath.startsWith(realPackageDir + sep))
|
|
114
120
|
return null;
|
|
@@ -212,9 +218,14 @@ export async function discoverSkillPackages(cwd) {
|
|
|
212
218
|
}
|
|
213
219
|
}
|
|
214
220
|
for (const resource of resolved.skills) {
|
|
215
|
-
|
|
216
|
-
|
|
221
|
+
// Same bound, same reason: this is the third `SKILL.md` reader and it feeds `planInit`.
|
|
222
|
+
const resourceRead = await readBoundedBytes(resource.path, {
|
|
223
|
+
maxBytes: DEFINITION_MAX_BYTES,
|
|
224
|
+
timeoutMs: DEFINITION_READ_TIMEOUT_MS,
|
|
225
|
+
});
|
|
226
|
+
if (!resourceRead.ok)
|
|
217
227
|
continue;
|
|
228
|
+
const bytes = resourceRead.bytes;
|
|
218
229
|
const text = bytes.toString("utf8");
|
|
219
230
|
const resourceName = skillResourceName(resource.path);
|
|
220
231
|
if (seenSkills.has(resourceName))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill-packages.js","sourceRoot":"","sources":["../../src/kernel/skill-packages.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,
|
|
1
|
+
{"version":3,"file":"skill-packages.js","sourceRoot":"","sources":["../../src/kernel/skill-packages.ts"],"names":[],"mappings":"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"}
|
|
@@ -36,8 +36,14 @@ export declare function loadWorkspaceRegistry(path: string): Promise<WorkspaceRe
|
|
|
36
36
|
* that module — documents itself as "Pure: no filesystem". It is a registry concern; this is where the
|
|
37
37
|
* registry lives. Moved when `init.ts` crossed the 400-line ceiling, which this project splits for rather
|
|
38
38
|
* than raising (`delegate.ts` at 413, `grants.ts` at 398).
|
|
39
|
+
*
|
|
40
|
+
* **`onRefusal` exists because `catch { return [] }` was rule 8's silent safe-mode.** A malformed registry
|
|
41
|
+
* made `pi-daddy init` scaffold with no workspace capabilities at all and say nothing about why, which an
|
|
42
|
+
* operator cannot tell apart from having registered none. Failing soft stays — `init` must work without a
|
|
43
|
+
* registry — but the reason is handed to the caller instead of discarded. A caller that passes nothing keeps
|
|
44
|
+
* the old behaviour, which is why this is an optional parameter and not a changed return type.
|
|
39
45
|
*/
|
|
40
|
-
export declare function registeredWorkspaceIds(registryPath?: string | undefined): Promise<string[]>;
|
|
46
|
+
export declare function registeredWorkspaceIds(registryPath?: string | undefined, onRefusal?: (reason: string) => void): Promise<string[]>;
|
|
41
47
|
export declare function resolveWorkspace(registry: WorkspaceRegistryFile, workspaceId: string): Promise<ValidatedWorkspace>;
|
|
42
48
|
/**
|
|
43
49
|
* Canonicalize and validate the initial workspace against Git's registered worktree list.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../src/kernel/workspace.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../src/kernel/workspace.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAIxD,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,OAAO,CAAC;AAE/C,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7C;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,wFAAwF;IACxF,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB;AAsBD,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAyFxF;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,sBAAsB,CAC1C,YAAY,qBAAsC,EAClD,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GACnC,OAAO,CAAC,MAAM,EAAE,CAAC,CAQnB;AAED,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,qBAAqB,EAC/B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,kBAAkB,CAAC,CAe7B;AAED;;;GAGG;AACH,wBAAsB,2BAA2B,CAAC,KAAK,EAAE;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA0D9B"}
|
package/dist/kernel/workspace.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { once } from "node:events";
|
|
4
|
-
import { mkdir,
|
|
5
|
-
import {
|
|
4
|
+
import { mkdir, readFile, realpath, rename, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { readBoundedFile } from "./bounded-read.js";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { isAbsolute, join } from "node:path";
|
|
8
8
|
import { promisify } from "node:util";
|
|
@@ -30,84 +30,35 @@ const REGISTRY_READ_TIMEOUT_MS = 2_000;
|
|
|
30
30
|
/** A registry is an operator-authored JSON file; anything approaching this is not one. */
|
|
31
31
|
const REGISTRY_MAX_BYTES = 1 << 20;
|
|
32
32
|
export async function loadWorkspaceRegistry(path) {
|
|
33
|
-
//
|
|
34
|
-
// the
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
let handle;
|
|
46
|
-
try {
|
|
47
|
-
handle = await open(path, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
48
|
-
}
|
|
49
|
-
catch (error) {
|
|
50
|
-
throw new GovernanceRefusal(refusal("WORKSPACE_NOT_REGISTERED", `workspace registry ${path} could not be opened (${String(error)})`, {
|
|
51
|
-
registry_path: path,
|
|
52
|
-
}));
|
|
53
|
-
}
|
|
54
|
-
let raw;
|
|
55
|
-
try {
|
|
56
|
-
const info = await handle.stat();
|
|
57
|
-
if (!info.isFile()) {
|
|
58
|
-
throw new GovernanceRefusal(refusal("WORKSPACE_NOT_REGISTERED", `workspace registry ${path} is not a regular file — refusing to read it. A FIFO, device or socket ` +
|
|
33
|
+
// The guards this call carries — non-blocking open, every check on the held descriptor, a deadline between
|
|
34
|
+
// chunks, the size bound checked twice — were worked out HERE and now live in `bounded-read.ts`, because a
|
|
35
|
+
// second session-start reader went on using a bare `readFile` rather than copying them. The mapping from a
|
|
36
|
+
// reason to a refusal stays here: only this caller knows that an unreadable registry is a governance
|
|
37
|
+
// refusal naming the file, and its messages are unchanged.
|
|
38
|
+
const read = await readBoundedFile(path, {
|
|
39
|
+
maxBytes: REGISTRY_MAX_BYTES,
|
|
40
|
+
timeoutMs: REGISTRY_READ_TIMEOUT_MS,
|
|
41
|
+
});
|
|
42
|
+
if (!read.ok) {
|
|
43
|
+
const message = read.why === "not-a-regular-file"
|
|
44
|
+
? `workspace registry ${path} is not a regular file — refusing to read it. A FIFO, device or socket ` +
|
|
59
45
|
`at ${ENV_WORKSPACE_REGISTRY} would block session start rather than fail, because opening one ` +
|
|
60
|
-
`waits for a writer that may never come
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// implementation — review measured 74 of 200 one-MiB reads completing in full with `signal.aborted`
|
|
77
|
-
// already true, because a signal is never observed *inside* a libuv read request. An explicit check makes
|
|
78
|
-
// the bound as real as it can be in-process, and its limit is the same one honestly stated below: a
|
|
79
|
-
// stalled `open` or a single wedged read cannot be interrupted from here.
|
|
80
|
-
const deadline = Date.now() + REGISTRY_READ_TIMEOUT_MS;
|
|
81
|
-
const buffer = Buffer.allocUnsafe(REGISTRY_MAX_BYTES + 1);
|
|
82
|
-
let filled = 0;
|
|
83
|
-
while (filled < buffer.length) {
|
|
84
|
-
if (Date.now() > deadline) {
|
|
85
|
-
throw new GovernanceRefusal(refusal("WORKSPACE_NOT_REGISTERED", `workspace registry ${path} did not finish reading within ${REGISTRY_READ_TIMEOUT_MS}ms — ` +
|
|
86
|
-
`refusing rather than waiting, because session start awaits this read.`, { registry_path: path }));
|
|
87
|
-
}
|
|
88
|
-
const { bytesRead } = await handle.read(buffer, filled, buffer.length - filled, filled);
|
|
89
|
-
if (bytesRead === 0)
|
|
90
|
-
break;
|
|
91
|
-
filled += bytesRead;
|
|
92
|
-
}
|
|
93
|
-
if (filled > REGISTRY_MAX_BYTES) {
|
|
94
|
-
throw new GovernanceRefusal(refusal("WORKSPACE_NOT_REGISTERED", `workspace registry ${path} exceeded the ${REGISTRY_MAX_BYTES} limit while being read — it grew ` +
|
|
95
|
-
`after its size was checked. Refusing rather than allocating it.`, { registry_path: path }));
|
|
96
|
-
}
|
|
97
|
-
raw = buffer.subarray(0, filled).toString("utf8");
|
|
98
|
-
}
|
|
99
|
-
catch (error) {
|
|
100
|
-
if (error instanceof GovernanceRefusal)
|
|
101
|
-
throw error;
|
|
102
|
-
const timedOut = error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
103
|
-
throw new GovernanceRefusal(refusal("WORKSPACE_NOT_REGISTERED", timedOut
|
|
104
|
-
? `workspace registry ${path} did not return within ${REGISTRY_READ_TIMEOUT_MS}ms — refusing rather ` +
|
|
105
|
-
`than waiting, because session start awaits this read.`
|
|
106
|
-
: `workspace registry ${path} could not be read (${String(error)})`, { registry_path: path }));
|
|
107
|
-
}
|
|
108
|
-
finally {
|
|
109
|
-
await handle.close().catch(() => { });
|
|
110
|
-
}
|
|
46
|
+
`waits for a writer that may never come.`
|
|
47
|
+
: read.why === "too-large"
|
|
48
|
+
? `workspace registry ${path} is ${read.size} bytes, over the ${REGISTRY_MAX_BYTES} limit — ` +
|
|
49
|
+
`refusing rather than reading it into memory at session start.`
|
|
50
|
+
: read.why === "grew-while-reading"
|
|
51
|
+
? `workspace registry ${path} exceeded the ${REGISTRY_MAX_BYTES} limit while being read — it ` +
|
|
52
|
+
`grew after its size was checked. Refusing rather than allocating it.`
|
|
53
|
+
: read.why === "timed-out"
|
|
54
|
+
? `workspace registry ${path} did not finish reading within ${REGISTRY_READ_TIMEOUT_MS}ms — ` +
|
|
55
|
+
`refusing rather than waiting, because session start awaits this read.`
|
|
56
|
+
: read.why === "unopenable"
|
|
57
|
+
? `workspace registry ${path} could not be opened (${read.detail})`
|
|
58
|
+
: `workspace registry ${path} could not be read (${read.detail})`;
|
|
59
|
+
throw new GovernanceRefusal(refusal("WORKSPACE_NOT_REGISTERED", message, { registry_path: path }));
|
|
60
|
+
}
|
|
61
|
+
const raw = read.text;
|
|
111
62
|
let parsed;
|
|
112
63
|
try {
|
|
113
64
|
parsed = JSON.parse(raw);
|
|
@@ -158,14 +109,21 @@ export async function loadWorkspaceRegistry(path) {
|
|
|
158
109
|
* that module — documents itself as "Pure: no filesystem". It is a registry concern; this is where the
|
|
159
110
|
* registry lives. Moved when `init.ts` crossed the 400-line ceiling, which this project splits for rather
|
|
160
111
|
* than raising (`delegate.ts` at 413, `grants.ts` at 398).
|
|
112
|
+
*
|
|
113
|
+
* **`onRefusal` exists because `catch { return [] }` was rule 8's silent safe-mode.** A malformed registry
|
|
114
|
+
* made `pi-daddy init` scaffold with no workspace capabilities at all and say nothing about why, which an
|
|
115
|
+
* operator cannot tell apart from having registered none. Failing soft stays — `init` must work without a
|
|
116
|
+
* registry — but the reason is handed to the caller instead of discarded. A caller that passes nothing keeps
|
|
117
|
+
* the old behaviour, which is why this is an optional parameter and not a changed return type.
|
|
161
118
|
*/
|
|
162
|
-
export async function registeredWorkspaceIds(registryPath = process.env[ENV_WORKSPACE_REGISTRY]) {
|
|
119
|
+
export async function registeredWorkspaceIds(registryPath = process.env[ENV_WORKSPACE_REGISTRY], onRefusal) {
|
|
163
120
|
if (!registryPath)
|
|
164
121
|
return [];
|
|
165
122
|
try {
|
|
166
123
|
return Object.keys((await loadWorkspaceRegistry(registryPath)).workspaces).sort();
|
|
167
124
|
}
|
|
168
|
-
catch {
|
|
125
|
+
catch (error) {
|
|
126
|
+
onRefusal?.(error instanceof Error ? error.message : String(error));
|
|
169
127
|
return [];
|
|
170
128
|
}
|
|
171
129
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../src/kernel/workspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../src/kernel/workspace.ts"],"names":[],"mappings":"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"}
|
|
@@ -381,6 +381,13 @@ export const grantsCommand = {
|
|
|
381
381
|
...(catalog.byKind("workspace").length > 0
|
|
382
382
|
? [` routable ${catalog.byKind("workspace").join(", ")} — held ones only are usable (ADR-0035)`]
|
|
383
383
|
: []),
|
|
384
|
+
// Rule 8's loud half again. The catalog fails soft on an unreadable registry, which is right, and used
|
|
385
|
+
// to discard the reason with it, which was not: one malformed entry removed every workspace from this
|
|
386
|
+
// screen, from the catalog and from `init`, and an operator saw an empty list identical to the one a
|
|
387
|
+
// machine with no registry shows. Placed under `routable` because it explains that line's absence.
|
|
388
|
+
...(catalog.registryRefusal
|
|
389
|
+
? [` routable (none — workspace registry unreadable) ${catalog.registryRefusal}`]
|
|
390
|
+
: []),
|
|
384
391
|
];
|
|
385
392
|
// Runs the REAL planner AND the real approval step over each definition, so this listing cannot
|
|
386
393
|
// disagree with what a spawn would do — the R-28 lesson, kept structural rather than remembered.
|
|
@@ -52,7 +52,13 @@ export async function runInit(
|
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
const plan = planInit(
|
|
55
|
+
const plan = planInit(
|
|
56
|
+
packages,
|
|
57
|
+
ctx.cwd,
|
|
58
|
+
await registeredWorkspaceIds(undefined, (reason) =>
|
|
59
|
+
ctx.ui.notify(`grants: workspace registry unreadable, scaffolding no workspaces — ${reason}`, "warning"),
|
|
60
|
+
),
|
|
61
|
+
);
|
|
56
62
|
const outcome = await applyInit(plan);
|
|
57
63
|
const lines = [
|
|
58
64
|
`grants: ${plan.skills.length} definition(s) from ${packages.map((p) => `${p.name}@${p.version}`).join(", ")}`,
|
|
@@ -57,6 +57,21 @@ export async function reportSessionStart(session: GrantsSession, ctx: SessionRep
|
|
|
57
57
|
"warning",
|
|
58
58
|
);
|
|
59
59
|
}
|
|
60
|
+
// **Rule 8's loud half for the two reads that discover what exists.** Both of these used to fail soft and
|
|
61
|
+
// say nothing, and review established that session start — not `/grants` — is where every other such
|
|
62
|
+
// notice lives (a malformed bound above, ledger corruption and an executor refusal below). A reason an
|
|
63
|
+
// operator has to ask for is a reason most operators never see.
|
|
64
|
+
//
|
|
65
|
+
// The definitions one matters more than it looks: the 1 MiB bound is NEW behaviour, so a definition that
|
|
66
|
+
// loaded yesterday can be absent today, and without this line the only symptom is `unknown agent "x"` at
|
|
67
|
+
// the moment somebody tries to delegate to it.
|
|
68
|
+
for (const reason of session.definitionSkips)
|
|
69
|
+
ctx.ui.notify(`grants: a definition was not loaded — ${reason}`, "warning");
|
|
70
|
+
if (session.catalog.registryRefusal)
|
|
71
|
+
ctx.ui.notify(
|
|
72
|
+
`grants: workspace registry unreadable, no workspace is routable this session — ${session.catalog.registryRefusal}`,
|
|
73
|
+
"warning",
|
|
74
|
+
);
|
|
60
75
|
// ADR-0014: a pre-0.6 in-workspace approvals file is IGNORED, not migrated — importing it would
|
|
61
76
|
// import exactly the entries whose trustworthiness the move exists to remove. Say so, because an
|
|
62
77
|
// operator whose approvals silently stopped applying deserves to know why.
|
package/extensions/session.ts
CHANGED
|
@@ -186,6 +186,16 @@ export interface GrantsSession extends NativeSessionHost {
|
|
|
186
186
|
observedTools: string[] | null;
|
|
187
187
|
/** ADR-0016: `SKILL.md` definitions, keyed by name. The format this package spawns from now. */
|
|
188
188
|
definitions: Map<string, SkillDefinition>;
|
|
189
|
+
/**
|
|
190
|
+
* Definitions discovery dropped, and why — reported at session start (ADR-0076, rule 8).
|
|
191
|
+
*
|
|
192
|
+
* Review found the bound shipped without this: `loadDefinitions` grew a `skipped` callback and NO
|
|
193
|
+
* production caller passed one, so an oversized or unreadable `SKILL.md` still vanished with nothing
|
|
194
|
+
* anywhere explaining it. That is worse than the `catch { continue }` it replaced, because the bound is
|
|
195
|
+
* new behaviour — a 2 MiB definition used to load. A capability that only a test can observe is not a
|
|
196
|
+
* capability.
|
|
197
|
+
*/
|
|
198
|
+
definitionSkips: string[];
|
|
189
199
|
catalog: Catalog;
|
|
190
200
|
/**
|
|
191
201
|
* The in-flight catalog build, so `delegate` can wait for it instead of racing it.
|
|
@@ -255,7 +265,9 @@ export interface GrantsSession extends NativeSessionHost {
|
|
|
255
265
|
* about what loading means, so there is one.
|
|
256
266
|
*/
|
|
257
267
|
export async function loadProjectDefinitions(session: GrantsSession, cwd: string): Promise<void> {
|
|
258
|
-
|
|
268
|
+
const skips: string[] = [];
|
|
269
|
+
session.definitions = await loadDefinitions(cwd, (_path, reason) => skips.push(reason));
|
|
270
|
+
session.definitionSkips = skips;
|
|
259
271
|
session.catalogReady = buildCatalog({
|
|
260
272
|
cwd,
|
|
261
273
|
observedTools: session.observedTools,
|
|
@@ -318,6 +330,7 @@ export function createGrantsSession(
|
|
|
318
330
|
depth,
|
|
319
331
|
maxDepth,
|
|
320
332
|
malformedBounds: bounds.malformed,
|
|
333
|
+
definitionSkips: [],
|
|
321
334
|
// ADR-0012: `bash` is gated by DEFAULT — but only in a governed session. An ungoverned one
|
|
322
335
|
// (no PI_DADDY_GRANT) still blocks nothing, so "governance is opt-in" holds exactly where it always
|
|
323
336
|
// did. Inside a session the operator already chose to govern, handing a child `bash` hands it an
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-daddy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"description": "Capability governance for pi sub-agents: spawn Agent Skills (SKILL.md) definitions whose allowed-tools becomes a grant that can only narrow going down a delegation tree, enforced by pi's own --tools allowlist, with an append-only ledger.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/src/cli.ts
CHANGED
|
@@ -150,7 +150,15 @@ async function init(cwd: string, force: boolean): Promise<number> {
|
|
|
150
150
|
|
|
151
151
|
let plan: InitPlan;
|
|
152
152
|
try {
|
|
153
|
-
|
|
153
|
+
// A registry that cannot be read scaffolds no workspace capabilities. That was silent, so the operator
|
|
154
|
+
// saw a plan with none and no way to tell it apart from having registered none (rule 8).
|
|
155
|
+
plan = planInit(
|
|
156
|
+
packages,
|
|
157
|
+
cwd,
|
|
158
|
+
await registeredWorkspaceIds(undefined, (reason) =>
|
|
159
|
+
console.error(`pi-daddy init: workspace registry unreadable, scaffolding none — ${reason}`),
|
|
160
|
+
),
|
|
161
|
+
);
|
|
154
162
|
} catch (error) {
|
|
155
163
|
// R-78's backstop reaching the surface. Nothing is written: a grant that could mean something to a
|
|
156
164
|
// shell is not a grant, and half-scaffolding a project would be worse than scaffolding none of it.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One bounded reader for the operator-authored files this package reads at SESSION START.
|
|
3
|
+
*
|
|
4
|
+
* **Why this is a module rather than a pattern to copy.** `loadWorkspaceRegistry` worked out the correct
|
|
5
|
+
* shape the expensive way — R-79 hung forever on a FIFO, a `stat`-then-`readFile` rewrite reintroduced the
|
|
6
|
+
* hang through a TOCTOU, and `AbortSignal.timeout` turned out never to interrupt a libuv read. That comment
|
|
7
|
+
* block ends with "One reader is why that cannot happen again", and then a second session-start read
|
|
8
|
+
* (`loadDefinitions`, reading each `SKILL.md`) went on using a bare `readFile`. The guards were not copied
|
|
9
|
+
* because copying them correctly is exactly what the registry's own history says nobody manages.
|
|
10
|
+
*
|
|
11
|
+
* What the shape buys, restated once here so neither caller has to:
|
|
12
|
+
*
|
|
13
|
+
* - `O_NONBLOCK` on the open. A FIFO blocks inside `open(2)` before any read starts, so a signal or a
|
|
14
|
+
* deadline checked between chunks can never rescue it; a non-blocking open returns instead.
|
|
15
|
+
* - every check against the HELD DESCRIPTOR. `stat` by name followed by a read by name is a TOCTOU, and the
|
|
16
|
+
* attacker is any process at the same uid — swapping a regular file for a FIFO between the two hangs the
|
|
17
|
+
* reader. `fstat` on a descriptor has no name left to re-resolve.
|
|
18
|
+
* - a deadline checked BETWEEN chunks, which is all an `AbortSignal` ever managed. Its honest limit: a
|
|
19
|
+
* stalled open or a single wedged read cannot be interrupted from in-process.
|
|
20
|
+
* - a size bound checked twice, before the read from `fstat` and again after, because a file can grow
|
|
21
|
+
* between the two. **The buffer is `maxBytes + 1` regardless of the file's size, and that is deliberate.**
|
|
22
|
+
* Review proposed sizing it from `fstat` instead; that would be faster and would silently break the second
|
|
23
|
+
* check, because a file whose reported size understates its content — anything under procfs, and any file
|
|
24
|
+
* that grows — would fill its small buffer exactly and come back as a successful truncated read. The cost
|
|
25
|
+
* was measured rather than assumed: 500 allocations of 1 MiB + 1 take 8.1ms for an RSS delta of 2.8 MB,
|
|
26
|
+
* because `allocUnsafe` never touches the pages.
|
|
27
|
+
*
|
|
28
|
+
* **The clock is injected** so the deadline is forced by a test rather than by a reviewer. Before this
|
|
29
|
+
* module the registry's deadline could be deleted outright and all 876 tests still passed (measured at
|
|
30
|
+
* `7096f78`), which is rule 7's "a test that cannot fail is worse than no test" with the production change
|
|
31
|
+
* named: delete the `now() > deadline` branch below and `bounded-read.test.ts` fails.
|
|
32
|
+
*/
|
|
33
|
+
import { constants } from "node:fs";
|
|
34
|
+
import { open, type FileHandle } from "node:fs/promises";
|
|
35
|
+
|
|
36
|
+
export interface BoundedReadLimits {
|
|
37
|
+
/** Refuse anything at or over this, before allocating it. */
|
|
38
|
+
maxBytes: number;
|
|
39
|
+
/** How long the chunk loop may run before the read is a refusal rather than a wait. */
|
|
40
|
+
timeoutMs: number;
|
|
41
|
+
/** Injected only by tests; production passes nothing and gets the wall clock. */
|
|
42
|
+
now?: () => number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Why a bounded read did not produce text.
|
|
47
|
+
*
|
|
48
|
+
* A discriminated reason rather than a thrown error, because the two callers disagree about what a failure
|
|
49
|
+
* MEANS — an unreadable registry is a governance refusal naming the file, while an unreadable `SKILL.md` is
|
|
50
|
+
* one definition that will not be offered — and a shared reader must not decide that for them.
|
|
51
|
+
*/
|
|
52
|
+
export type BoundedReadFailure =
|
|
53
|
+
| { why: "unopenable"; detail: string }
|
|
54
|
+
| { why: "not-a-regular-file"; detail: string }
|
|
55
|
+
| { why: "too-large"; detail: string; size: number }
|
|
56
|
+
| { why: "grew-while-reading"; detail: string }
|
|
57
|
+
| { why: "timed-out"; detail: string }
|
|
58
|
+
| { why: "unreadable"; detail: string };
|
|
59
|
+
|
|
60
|
+
export type BoundedReadResult = { ok: true; text: string } | ({ ok: false } & BoundedReadFailure);
|
|
61
|
+
export type BoundedReadBytes = { ok: true; bytes: Buffer } | ({ ok: false } & BoundedReadFailure);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The same read, handing back raw bytes.
|
|
65
|
+
*
|
|
66
|
+
* `skill-packages.ts` needs the bytes rather than a string, because it asserts that a definition survives a
|
|
67
|
+
* UTF-8 round trip unchanged — a latin-1 byte that decodes to U+FFFD changes the file's digest, and that
|
|
68
|
+
* check is only possible against the original buffer. Decoding here and re-encoding there would defeat it.
|
|
69
|
+
*/
|
|
70
|
+
export async function readBoundedFile(path: string, limits: BoundedReadLimits): Promise<BoundedReadResult> {
|
|
71
|
+
const read = await readBoundedBytes(path, limits);
|
|
72
|
+
return read.ok ? { ok: true, text: read.bytes.toString("utf8") } : read;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function readBoundedBytes(path: string, limits: BoundedReadLimits): Promise<BoundedReadBytes> {
|
|
76
|
+
const now = limits.now ?? Date.now;
|
|
77
|
+
let handle: FileHandle;
|
|
78
|
+
try {
|
|
79
|
+
handle = await open(path, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return { ok: false, why: "unopenable", detail: String(error) };
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const info = await handle.stat();
|
|
85
|
+
if (!info.isFile())
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
why: "not-a-regular-file",
|
|
89
|
+
detail: `${path} is not a regular file — a FIFO, device or socket here would block rather than fail`,
|
|
90
|
+
};
|
|
91
|
+
if (info.size > limits.maxBytes)
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
why: "too-large",
|
|
95
|
+
size: info.size,
|
|
96
|
+
detail: `${path} is ${info.size} bytes, over the ${limits.maxBytes} limit`,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const deadline = now() + limits.timeoutMs;
|
|
100
|
+
const buffer = Buffer.allocUnsafe(limits.maxBytes + 1);
|
|
101
|
+
let filled = 0;
|
|
102
|
+
while (filled < buffer.length) {
|
|
103
|
+
if (now() > deadline)
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
why: "timed-out",
|
|
107
|
+
detail: `${path} did not finish reading within ${limits.timeoutMs}ms`,
|
|
108
|
+
};
|
|
109
|
+
const { bytesRead } = await handle.read(buffer, filled, buffer.length - filled, filled);
|
|
110
|
+
if (bytesRead === 0) break;
|
|
111
|
+
filled += bytesRead;
|
|
112
|
+
}
|
|
113
|
+
if (filled > limits.maxBytes)
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
why: "grew-while-reading",
|
|
117
|
+
detail: `${path} exceeded the ${limits.maxBytes} limit while being read — it grew after its size was checked`,
|
|
118
|
+
};
|
|
119
|
+
return { ok: true, bytes: buffer.subarray(0, filled) };
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return { ok: false, why: "unreadable", detail: String(error) };
|
|
122
|
+
} finally {
|
|
123
|
+
await handle.close().catch(() => {});
|
|
124
|
+
}
|
|
125
|
+
}
|