pi-daddy 0.35.0 → 0.37.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 +98 -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/context-handoff.d.ts +71 -1
- package/dist/kernel/context-handoff.d.ts.map +1 -1
- package/dist/kernel/context-handoff.js +65 -6
- package/dist/kernel/context-handoff.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/context-staging.ts +32 -5
- package/extensions/grants-command.ts +7 -0
- package/extensions/init-command.ts +7 -1
- package/extensions/pruning-advice.ts +20 -8
- 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/context-handoff.ts +96 -6
- package/src/kernel/definitions.ts +38 -8
- package/src/kernel/skill-packages.ts +26 -9
- package/src/kernel/workspace.ts +43 -109
|
@@ -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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"}
|
|
@@ -19,6 +19,7 @@ import { randomUUID } from "node:crypto";
|
|
|
19
19
|
import { closeSync, mkdirSync, openSync, readSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
20
20
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
21
21
|
import {
|
|
22
|
+
CONTEXT_RANK,
|
|
22
23
|
fenceContext,
|
|
23
24
|
selectPrunedTurns,
|
|
24
25
|
type ContextRequest,
|
|
@@ -69,14 +70,21 @@ export function createHandoffStager(input: StagingInput) {
|
|
|
69
70
|
return (granted: ContextRequest, options: { keepTurnIds?: readonly string[] } = {}): StagedHandoff => {
|
|
70
71
|
if (granted.mode === "fork") return stageFork(input);
|
|
71
72
|
const sections: ContextSection[] = [];
|
|
73
|
+
/** Which entries of `sections` are parent turns, so the record can count the ones that survived the cap. */
|
|
74
|
+
const turnSectionIndices: number[] = [];
|
|
72
75
|
let keptTurns: number | undefined;
|
|
73
76
|
let droppedTurns: number | undefined;
|
|
74
77
|
let rule: string | undefined;
|
|
75
78
|
|
|
76
79
|
if (granted.mode === "summary")
|
|
77
|
-
sections.push({
|
|
80
|
+
sections.push({
|
|
81
|
+
label: "what your parent says you need to know",
|
|
82
|
+
body: granted.summary ?? "",
|
|
83
|
+
keepRank: CONTEXT_RANK.summary,
|
|
84
|
+
});
|
|
78
85
|
if (granted.mode === "files" || granted.mode === "pruned")
|
|
79
|
-
for (const path of granted.files ?? [])
|
|
86
|
+
for (const path of granted.files ?? [])
|
|
87
|
+
sections.push({ ...readSection(input.cwd, path), keepRank: CONTEXT_RANK.file });
|
|
80
88
|
if (granted.mode === "pruned") {
|
|
81
89
|
const all = parentTurns(input.parentSession);
|
|
82
90
|
const selection = selectPrunedTurns(all, {
|
|
@@ -93,20 +101,39 @@ export function createHandoffStager(input: StagingInput) {
|
|
|
93
101
|
keptTurns = chosen.length;
|
|
94
102
|
droppedTurns = all.length - chosen.length;
|
|
95
103
|
rule = options.keepTurnIds === undefined ? selection.rule : `${selection.rule}+advice`;
|
|
96
|
-
|
|
104
|
+
// Two bands, then position within the band. The probe measured array-order filling losing exactly the
|
|
105
|
+
// turns nearest the task; review then measured the first fix losing the file-matched turns instead,
|
|
106
|
+
// because those sit at the FRONT of `chosen` and a bare positional rank made oldest mean least.
|
|
107
|
+
const matched = new Set(selection.fileMatched);
|
|
108
|
+
chosen.forEach((turn, index) => {
|
|
109
|
+
turnSectionIndices.push(sections.length);
|
|
110
|
+
sections.push({
|
|
111
|
+
label: `parent turn ${turn.id}`,
|
|
112
|
+
body: turn.text,
|
|
113
|
+
keepRank: (matched.has(turn.id) ? CONTEXT_RANK.fileMatchedTurn : CONTEXT_RANK.recentTurn) + index,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
97
116
|
if (chosen.length === 0)
|
|
98
117
|
sections.push({ label: "parent turns", body: "(no turn of your parent's session matched the selection)" });
|
|
99
118
|
}
|
|
100
119
|
|
|
101
120
|
const fenced = fenceContext(sections);
|
|
121
|
+
// **The record counts what CROSSED, which is what this module says it does.** These were counted before
|
|
122
|
+
// the budget ran, so a handoff could record `keptTurns: 21` having sent thirteen. Review measured it, and
|
|
123
|
+
// raising the default turn count moved the cap from binding in 13% of handoffs to 60% — so the wrong
|
|
124
|
+
// number became the usual number.
|
|
125
|
+
const crossed = new Set(fenced.keptIndices);
|
|
126
|
+
const turnsCrossed = turnSectionIndices.filter((index) => crossed.has(index)).length;
|
|
102
127
|
return {
|
|
103
128
|
contextPrompt: fenced.text,
|
|
104
129
|
record: {
|
|
105
130
|
mode: granted.mode,
|
|
106
|
-
sections:
|
|
131
|
+
sections: fenced.keptIndices.length,
|
|
107
132
|
bytes: Buffer.byteLength(fenced.text),
|
|
108
133
|
truncatedBytes: fenced.truncatedBytes,
|
|
109
|
-
...(keptTurns !== undefined
|
|
134
|
+
...(keptTurns !== undefined
|
|
135
|
+
? { keptTurns: turnsCrossed, droppedTurns: droppedTurns! + (keptTurns - turnsCrossed), rule }
|
|
136
|
+
: {}),
|
|
110
137
|
},
|
|
111
138
|
};
|
|
112
139
|
};
|
|
@@ -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(", ")}`,
|
|
@@ -22,7 +22,8 @@ export const PRUNING_PURPOSE = "handoff-pruning";
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* How many candidates may be judged. One question per turn, and a decision a human is waiting on should not carry
|
|
25
|
-
* an unbounded number of them
|
|
25
|
+
* an unbounded number of them. Beyond this the OLDEST candidates go unjudged and are kept, which is the safe
|
|
26
|
+
* direction: an unjudged turn is never dropped, so the advisor can still only narrow.
|
|
26
27
|
*/
|
|
27
28
|
export const MAX_JUDGED_TURNS = 12;
|
|
28
29
|
|
|
@@ -43,11 +44,21 @@ export async function advisePruning(input: {
|
|
|
43
44
|
...(input.granted.turns !== undefined ? { turns: input.granted.turns } : {}),
|
|
44
45
|
...(input.granted.files !== undefined ? { files: input.granted.files } : {}),
|
|
45
46
|
}).kept;
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
if (candidates.length < 2) return undefined;
|
|
48
|
+
// **Judge the MOST RECENT candidates, and keep the rest unjudged.** This was `> MAX_JUDGED_TURNS` → give up
|
|
49
|
+
// entirely, which was fine while the rule offered six turns and became a silent feature death the moment
|
|
50
|
+
// `DEFAULT_CONTEXT_TURNS` rose to 20: every default request would have exceeded the bound, so the pruning
|
|
51
|
+
// decision point shipped in 0.35.0 would never have fired again. Found while raising the default, which is
|
|
52
|
+
// the only reason it was found at all.
|
|
53
|
+
//
|
|
54
|
+
// Judging a SUBSET is still only narrowing — an unjudged turn is kept, never dropped — so the attenuation
|
|
55
|
+
// property is untouched. The recent end is judged because that is where the rule's own recall is
|
|
56
|
+
// concentrated, and because a turn adjacent to the task is the one a wrong answer costs most.
|
|
57
|
+
const unjudged = candidates.slice(0, Math.max(0, candidates.length - MAX_JUDGED_TURNS));
|
|
58
|
+
const judged = candidates.slice(unjudged.length);
|
|
48
59
|
|
|
49
60
|
const questions: Record<string, Question> = {};
|
|
50
|
-
for (const [index, turn] of
|
|
61
|
+
for (const [index, turn] of judged.entries())
|
|
51
62
|
questions[`turn${index}`] = {
|
|
52
63
|
kind: "noul",
|
|
53
64
|
// Both bounded: the task is embedded once per candidate, so an unbounded task became a request twelve times
|
|
@@ -67,10 +78,11 @@ export async function advisePruning(input: {
|
|
|
67
78
|
if (!advice) return undefined;
|
|
68
79
|
// Every candidate or none. A response missing eleven of twelve answers would otherwise read as "drop eleven",
|
|
69
80
|
// which is a narrowing nobody asked for rather than the "unrecognised response means no advice" contract.
|
|
70
|
-
if (
|
|
71
|
-
const kept =
|
|
72
|
-
.
|
|
73
|
-
.map((turn) => turn.id)
|
|
81
|
+
if (judged.some((_, index) => advice.answers[`turn${index}`]?.kind !== "noul")) return undefined;
|
|
82
|
+
const kept = [
|
|
83
|
+
...unjudged.map((turn) => turn.id),
|
|
84
|
+
...judged.filter((_, index) => (advice.answers[`turn${index}`] as { value: boolean }).value).map((turn) => turn.id),
|
|
85
|
+
];
|
|
74
86
|
// An advisor that drops everything is answering a different question from the one that was asked; the rule's
|
|
75
87
|
// selection stands rather than handing a child a handoff with nothing in it.
|
|
76
88
|
return kept.length === 0 ? undefined : kept;
|
|
@@ -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.37.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
|
+
}
|
package/src/kernel/catalog.ts
CHANGED
|
@@ -44,6 +44,17 @@ export interface Catalog {
|
|
|
44
44
|
all: Capability[];
|
|
45
45
|
byKind(kind: CapabilityKind): Capability[];
|
|
46
46
|
has(capability: Capability): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Why the workspace registry contributed nothing, when one was named and could not be read.
|
|
49
|
+
*
|
|
50
|
+
* The catalog fails SOFT on a bad registry — a malformed one must not stop a session starting — and the
|
|
51
|
+
* refusal used to be discarded by `() => []` at the call below. That is rule 8's silent safe-mode: one
|
|
52
|
+
* malformed entry removed every workspace from `/grants`, from the catalog and from `init`, and produced
|
|
53
|
+
* no message anywhere, so an operator saw an empty list and could not tell it apart from having
|
|
54
|
+
* registered nothing. Failing soft is still right; failing soft and SILENTLY was not. `undefined` means
|
|
55
|
+
* no registry was named, or it read cleanly.
|
|
56
|
+
*/
|
|
57
|
+
registryRefusal?: string;
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
/** Split observed tool names into pi built-ins and extension-provided tools. */
|
|
@@ -107,7 +118,7 @@ export function workspaceEntries(registry: WorkspaceRegistryFile, source?: strin
|
|
|
107
118
|
}
|
|
108
119
|
|
|
109
120
|
/** Assemble a catalog from parts. Pure, so it is testable without a filesystem. */
|
|
110
|
-
export function makeCatalog(entries: CatalogEntry[]): Catalog {
|
|
121
|
+
export function makeCatalog(entries: CatalogEntry[], registryRefusal?: string): Catalog {
|
|
111
122
|
const deduped = new Map<Capability, CatalogEntry>();
|
|
112
123
|
for (const entry of entries) if (!deduped.has(entry.capability)) deduped.set(entry.capability, entry);
|
|
113
124
|
const list = [...deduped.values()].sort((a, b) => a.capability.localeCompare(b.capability));
|
|
@@ -118,6 +129,7 @@ export function makeCatalog(entries: CatalogEntry[]): Catalog {
|
|
|
118
129
|
all: ids,
|
|
119
130
|
byKind: (kind) => list.filter((e) => e.kind === kind).map((e) => e.capability),
|
|
120
131
|
has: (capability) => idSet.has(capability),
|
|
132
|
+
...(registryRefusal === undefined ? {} : { registryRefusal }),
|
|
121
133
|
};
|
|
122
134
|
}
|
|
123
135
|
|
|
@@ -135,30 +147,39 @@ export async function buildCatalog(input: {
|
|
|
135
147
|
// session from starting — `loadWorkspaceRegistry` throws a GovernanceRefusal naming the file, and that
|
|
136
148
|
// refusal is the operator's signal at the point of USE, where routing actually depends on it. Swallowing
|
|
137
149
|
// it there would be unsafe; swallowing it here costs a display list.
|
|
150
|
+
//
|
|
151
|
+
// **What the refusal is no longer allowed to do is vanish.** This handler was `() => []`, so the display
|
|
152
|
+
// list was lost AND the reason with it. The reason now rides on the catalog and `/grants` prints it.
|
|
138
153
|
input.registryPath
|
|
139
154
|
? loadWorkspaceRegistry(input.registryPath).then(
|
|
140
|
-
(r) => workspaceEntries(r, input.registryPath),
|
|
141
|
-
() =>
|
|
155
|
+
(r) => ({ entries: workspaceEntries(r, input.registryPath), refusal: undefined as string | undefined }),
|
|
156
|
+
(error: unknown) => ({
|
|
157
|
+
entries: [] as CatalogEntry[],
|
|
158
|
+
refusal: error instanceof Error ? error.message : String(error),
|
|
159
|
+
}),
|
|
142
160
|
)
|
|
143
|
-
: Promise.resolve([] as CatalogEntry[]),
|
|
144
|
-
]);
|
|
145
|
-
return makeCatalog([
|
|
146
|
-
// pi's built-ins are seeded unconditionally, because they are known statically and the catalog is
|
|
147
|
-
// consulted BEFORE any provider request has happened — `/grants` runs at that point. Without this,
|
|
148
|
-
// every capability looked "unknown" until the first model call, so the preview refused grants that
|
|
149
|
-
// enforcement would have allowed: R-28's failure shape (a diagnostic disagreeing with the enforcer)
|
|
150
|
-
// reappearing through a different door.
|
|
151
|
-
//
|
|
152
|
-
// The trade-off, stated plainly: in a session started with `--tools read`, this still lists `bash`
|
|
153
|
-
// as an existing capability, so a delegation naming it passes the *unknown* check and is refused by
|
|
154
|
-
// the *grant* check instead ("this session does not hold it"). That is the better error anyway, and
|
|
155
|
-
// the grant check — not this catalog — is the authority. Nothing here grants anything.
|
|
156
|
-
...PI_BUILTIN_TOOLS.map((name) => ({ capability: `tool:${name}` as const, kind: "builtin" as const })),
|
|
157
|
-
...(input.observedTools ? classifyToolNames(input.observedTools) : []),
|
|
158
|
-
...skills,
|
|
159
|
-
...definitionEntries(definitions),
|
|
160
|
-
...workspaces,
|
|
161
|
+
: Promise.resolve({ entries: [] as CatalogEntry[], refusal: undefined as string | undefined }),
|
|
161
162
|
]);
|
|
163
|
+
return makeCatalog(
|
|
164
|
+
[
|
|
165
|
+
// pi's built-ins are seeded unconditionally, because they are known statically and the catalog is
|
|
166
|
+
// consulted BEFORE any provider request has happened — `/grants` runs at that point. Without this,
|
|
167
|
+
// every capability looked "unknown" until the first model call, so the preview refused grants that
|
|
168
|
+
// enforcement would have allowed: R-28's failure shape (a diagnostic disagreeing with the enforcer)
|
|
169
|
+
// reappearing through a different door.
|
|
170
|
+
//
|
|
171
|
+
// The trade-off, stated plainly: in a session started with `--tools read`, this still lists `bash`
|
|
172
|
+
// as an existing capability, so a delegation naming it passes the *unknown* check and is refused by
|
|
173
|
+
// the *grant* check instead ("this session does not hold it"). That is the better error anyway, and
|
|
174
|
+
// the grant check — not this catalog — is the authority. Nothing here grants anything.
|
|
175
|
+
...PI_BUILTIN_TOOLS.map((name) => ({ capability: `tool:${name}` as const, kind: "builtin" as const })),
|
|
176
|
+
...(input.observedTools ? classifyToolNames(input.observedTools) : []),
|
|
177
|
+
...skills,
|
|
178
|
+
...definitionEntries(definitions),
|
|
179
|
+
...workspaces.entries,
|
|
180
|
+
],
|
|
181
|
+
workspaces.refusal,
|
|
182
|
+
);
|
|
162
183
|
}
|
|
163
184
|
|
|
164
185
|
/**
|