pi-daddy 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +187 -0
  2. package/LICENSE +21 -0
  3. package/README.md +584 -0
  4. package/dist/approval-prompt.d.ts +116 -0
  5. package/dist/approval-prompt.d.ts.map +1 -0
  6. package/dist/approval-prompt.js +124 -0
  7. package/dist/approval-prompt.js.map +1 -0
  8. package/dist/approval-store.d.ts +149 -0
  9. package/dist/approval-store.d.ts.map +1 -0
  10. package/dist/approval-store.js +311 -0
  11. package/dist/approval-store.js.map +1 -0
  12. package/dist/approval.d.ts +221 -0
  13. package/dist/approval.d.ts.map +1 -0
  14. package/dist/approval.js +244 -0
  15. package/dist/approval.js.map +1 -0
  16. package/dist/capabilities.d.ts +31 -0
  17. package/dist/capabilities.d.ts.map +1 -0
  18. package/dist/capabilities.js +45 -0
  19. package/dist/capabilities.js.map +1 -0
  20. package/dist/catalog.d.ts +81 -0
  21. package/dist/catalog.d.ts.map +1 -0
  22. package/dist/catalog.js +164 -0
  23. package/dist/catalog.js.map +1 -0
  24. package/dist/definitions.d.ts +101 -0
  25. package/dist/definitions.d.ts.map +1 -0
  26. package/dist/definitions.js +182 -0
  27. package/dist/definitions.js.map +1 -0
  28. package/dist/delegate.d.ts +133 -0
  29. package/dist/delegate.d.ts.map +1 -0
  30. package/dist/delegate.js +261 -0
  31. package/dist/delegate.js.map +1 -0
  32. package/dist/fanout.d.ts +62 -0
  33. package/dist/fanout.d.ts.map +1 -0
  34. package/dist/fanout.js +84 -0
  35. package/dist/fanout.js.map +1 -0
  36. package/dist/file-lock.d.ts +54 -0
  37. package/dist/file-lock.d.ts.map +1 -0
  38. package/dist/file-lock.js +142 -0
  39. package/dist/file-lock.js.map +1 -0
  40. package/dist/index.d.ts +7 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +7 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/ledger-report.d.ts +106 -0
  45. package/dist/ledger-report.d.ts.map +1 -0
  46. package/dist/ledger-report.js +157 -0
  47. package/dist/ledger-report.js.map +1 -0
  48. package/dist/ledger.d.ts +139 -0
  49. package/dist/ledger.d.ts.map +1 -0
  50. package/dist/ledger.js +94 -0
  51. package/dist/ledger.js.map +1 -0
  52. package/dist/pane-reaper.d.ts +49 -0
  53. package/dist/pane-reaper.d.ts.map +1 -0
  54. package/dist/pane-reaper.js +120 -0
  55. package/dist/pane-reaper.js.map +1 -0
  56. package/dist/pi-tools.d.ts +20 -0
  57. package/dist/pi-tools.d.ts.map +1 -0
  58. package/dist/pi-tools.js +21 -0
  59. package/dist/pi-tools.js.map +1 -0
  60. package/dist/propagation.d.ts +185 -0
  61. package/dist/propagation.d.ts.map +1 -0
  62. package/dist/propagation.js +255 -0
  63. package/dist/propagation.js.map +1 -0
  64. package/dist/resolve.d.ts +110 -0
  65. package/dist/resolve.d.ts.map +1 -0
  66. package/dist/resolve.js +181 -0
  67. package/dist/resolve.js.map +1 -0
  68. package/dist/run-child.d.ts +50 -0
  69. package/dist/run-child.d.ts.map +1 -0
  70. package/dist/run-child.js +109 -0
  71. package/dist/run-child.js.map +1 -0
  72. package/dist/run-herdr.d.ts +91 -0
  73. package/dist/run-herdr.d.ts.map +1 -0
  74. package/dist/run-herdr.js +306 -0
  75. package/dist/run-herdr.js.map +1 -0
  76. package/dist/spawn.d.ts +60 -0
  77. package/dist/spawn.d.ts.map +1 -0
  78. package/dist/spawn.js +116 -0
  79. package/dist/spawn.js.map +1 -0
  80. package/extensions/approvals.ts +285 -0
  81. package/extensions/delegation.ts +237 -0
  82. package/extensions/grants-command.ts +310 -0
  83. package/extensions/grants.ts +346 -0
  84. package/extensions/run-delegation.ts +270 -0
  85. package/extensions/session.ts +234 -0
  86. package/package.json +122 -0
  87. package/src/approval-prompt.ts +222 -0
  88. package/src/approval-store.ts +383 -0
  89. package/src/approval.ts +364 -0
  90. package/src/capabilities.ts +51 -0
  91. package/src/catalog.ts +186 -0
  92. package/src/definitions.ts +233 -0
  93. package/src/delegate.ts +383 -0
  94. package/src/fanout.ts +96 -0
  95. package/src/file-lock.ts +142 -0
  96. package/src/index.ts +59 -0
  97. package/src/ledger-report.ts +227 -0
  98. package/src/ledger.ts +214 -0
  99. package/src/pane-reaper.ts +132 -0
  100. package/src/pi-tools.ts +25 -0
  101. package/src/propagation.ts +308 -0
  102. package/src/resolve.ts +235 -0
  103. package/src/run-child.ts +149 -0
  104. package/src/run-herdr.ts +357 -0
  105. package/src/spawn.ts +156 -0
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Approval model for gated capabilities — pure, so it is exhaustively testable without pi.
3
+ *
4
+ * `resolve()` computes `gatedBlocked`: capabilities a session legitimately holds but which may not enter
5
+ * a child's grant without a human saying so. This module decides WHAT a yes means — how far it reaches,
6
+ * how long it lasts, and what it is keyed to. It never performs I/O and never prompts; see
7
+ * `approval-store.ts` and `approval-prompt.ts` for those.
8
+ *
9
+ * ADR-0010 records the four decisions encoded here.
10
+ */
11
+ import { WILDCARD } from "./pi-tools.js";
12
+ /**
13
+ * Subject used for delegate-path approvals.
14
+ *
15
+ * The delegate tool has no human-authored subject: the only things naming a child are the task string and
16
+ * the tool list, both chosen by the model. A key the model controls is not a key, so delegate approvals
17
+ * use this fixed literal — "allow write for delegations this session" — and are never persisted.
18
+ * Angle brackets cannot appear in an agent-type name (`parseAgentType` reads an identifier), so this can
19
+ * never collide with a real type.
20
+ */
21
+ export const DELEGATE_SUBJECT = "<delegate>";
22
+ /**
23
+ * Lifetime of a persisted approval.
24
+ *
25
+ * Deliberately a constant rather than an environment variable: a knob that silently extends every gate is
26
+ * the first thing an impatient operator would reach for.
27
+ */
28
+ export const APPROVAL_TTL_DAYS = 30;
29
+ const DAY_MS = 86_400_000;
30
+ /** `tool:write` + `docs-writer` -> `tool:write@docs-writer`. */
31
+ export function approvalKey(capability, subject) {
32
+ return `${capability}@${subject}`;
33
+ }
34
+ /**
35
+ * May this resolution raise an approval dialog?
36
+ *
37
+ * `resolve()` computes `denied` and `gatedBlocked` INDEPENDENTLY (deliberately — each rejection reason is
38
+ * reported rather than masked by whichever filter ran first), and both `decideSpawn` and `planDelegation`
39
+ * refuse outright on `denied` while still attaching the full result. So a request mixing a gated capability
40
+ * with one the session does not hold at all would otherwise prompt a human about the gated one and then be
41
+ * refused anyway for the other.
42
+ *
43
+ * That is not merely noise. A *session* yes is recorded and republished to children, and an *always* yes is
44
+ * written to disk for 30 days — both banked against a spawn that never happened, and both reachable by a
45
+ * model that appends one unheld capability to an otherwise ordinary request. A person is therefore only
46
+ * asked about a spawn that would actually proceed if they said yes.
47
+ *
48
+ * `clipped` is deliberately NOT a bar: it does not refuse a spawn, it just drops those capabilities from
49
+ * the child's grant, so the spawn does proceed and the question is real.
50
+ */
51
+ export function shouldSeekApproval(result) {
52
+ if (!result)
53
+ return false;
54
+ if (result.denied.length > 0)
55
+ return false;
56
+ // ADR-0011. A grant retaining a universal capability is refused by `assertNarrowing` no matter what
57
+ // a human says, so asking is worse than useless: the dialog cannot change the outcome, and a
58
+ // `session`- or `always`-scoped yes given there is banked and reused for later spawns that DO
59
+ // proceed. Same harm the `denied` guard above exists to prevent.
60
+ if (result.universal.length > 0)
61
+ return false;
62
+ return result.gatedBlocked.length > 0;
63
+ }
64
+ /**
65
+ * Scopes a given call site may offer. `always` requires a **human-authored subject**.
66
+ *
67
+ * ADR-0019. The rule is unchanged; which paths satisfy it is not. `"interceptor"` used to be the only one
68
+ * and ADR-0016 deleted it, leaving `always` offerable from nowhere — 220 lines of persistence no live path
69
+ * could write to. `"definition"` is `delegate({agent})`, where ADR-0017 makes the subject an
70
+ * operator-authored file the session must hold `agent:<name>` to name at all. `"delegate"` is the `tools:`
71
+ * form, where the original reasoning stands untouched: the only things naming that child are the task and
72
+ * the tool list, both model-chosen, and a key the model controls is not a key.
73
+ */
74
+ export function offeredScopes(path) {
75
+ return path === "definition" ? ["once", "session", "always"] : ["once", "session"];
76
+ }
77
+ /**
78
+ * What a child may inherit, as `capability@subject` keys.
79
+ *
80
+ * ADR-0014 changed this in two ways, and both were cases of a human's explicit choice being discarded
81
+ * one hop down:
82
+ *
83
+ * - **`once` is dropped** (A-S1). The scope chosen was not carried, so a `once` approval was written
84
+ * into the child's `PI_GRANTS_APPROVED` and republished onward — the most conservative answer a human
85
+ * can give produced the least conservative outcome, across an entire descendant subtree.
86
+ * - **The subject is kept** (A-S6). Bare capabilities were published, so a `<delegate>`-subject approval
87
+ * matched *any* subject below. `approvalKey`'s own doc argues at length that a model-controlled name
88
+ * is not a key; erasing the subject made that argument moot exactly where it mattered.
89
+ *
90
+ * The clamp to `grant` is unchanged and still load-bearing: **approval cannot conjure a capability**, so
91
+ * an inherited yes is only ever honoured for something the child independently holds.
92
+ */
93
+ export function inheritApprovals(approved, grant) {
94
+ const held = new Set(grant);
95
+ return [
96
+ ...new Set(approved
97
+ .filter((a) => a.scope !== "once" && a.capability !== WILDCARD && held.has(a.capability))
98
+ // A definition subject MUST carry a pin to cross a boundary (ADR-0022, hardened after F1).
99
+ //
100
+ // `verifyInherited` honours an unpinned entry by decision — `<delegate>` names no file and a
101
+ // pre-0.11 parent sends none — so an unpinned entry for a subject that *does* name a file is an
102
+ // approval exempt from the digest check. Enforcing it at the point of PUBLICATION rather than
103
+ // trusting each caller to attach a digest is what makes it structural: two call sites build these
104
+ // entries, one of them forgot, and every fresh approval crossed unpinned as a result. A caller that
105
+ // cannot produce a digest (the definition is not on disk here) now publishes nothing, which is the
106
+ // fail-closed direction.
107
+ .filter((a) => a.subject === DELEGATE_SUBJECT || Boolean(a.bodySha256))
108
+ .map((a) => approvalKey(a.capability, a.subject) + (a.bodySha256 ? `#${a.bodySha256}` : ""))),
109
+ ].sort();
110
+ }
111
+ /**
112
+ * Read the inherited set back on the child side.
113
+ *
114
+ * Anything that is not a well-formed `capability@subject` pair is dropped rather than guessed at. An
115
+ * unparseable entry granting nothing is a missing prompt; an unparseable entry granting *something* is a
116
+ * silent escalation, so the direction of the failure is not a matter of taste.
117
+ */
118
+ export function parseInherited(raw) {
119
+ const out = new Map();
120
+ for (const item of (raw ?? "").split(",")) {
121
+ const trimmed = item.trim();
122
+ // ADR-0022 appends `#<sha256>`. Split it off FIRST: a `#` cannot appear in a capability id or in a
123
+ // definition name (identity comes from a path component), so this is unambiguous.
124
+ const hash = trimmed.indexOf("#");
125
+ const key = hash === -1 ? trimmed : trimmed.slice(0, hash);
126
+ const digest = hash === -1 ? undefined : trimmed.slice(hash + 1);
127
+ const at = key.indexOf("@");
128
+ if (at <= 0 || at === key.length - 1)
129
+ continue;
130
+ if (hash !== -1 && !digest)
131
+ continue; // `key#` promises a pin and carries none — drop it, do not guess
132
+ out.set(key, digest);
133
+ }
134
+ return out;
135
+ }
136
+ /**
137
+ * Drop inherited approvals that were given for different instructions (ADR-0022).
138
+ *
139
+ * The hole this closes: `resolveApprovals` checks `inherited` FIRST and none of it had ever been through
140
+ * `entryVerdict`, so ADR-0019's headline property — an approval is void once the instructions change — held
141
+ * on the one path that persists and neither of the two that do not. A child is a fresh process: it re-reads
142
+ * the definition from disk, so a `git pull` between the parent's approval and the child's spawn meant the
143
+ * child ran a rewritten body under a yes given about the old one, recording `approvalSource: "inherited"`.
144
+ *
145
+ * An entry with **no** digest is honoured, and that is not a hole: `<delegate>` legitimately has none, and
146
+ * neither does a pre-0.11 parent. It is the same trade `entryVerdict` refuses to make for a *persisted*
147
+ * entry — but that entry is 30 days old and read off disk, whereas this one was handed over by a live parent
148
+ * process in the same tree, which is a much shorter chain to trust.
149
+ */
150
+ export function verifyInherited(parsed, snapshotOf) {
151
+ const out = new Set();
152
+ for (const [key, digest] of parsed) {
153
+ if (digest === undefined) {
154
+ out.add(key);
155
+ continue;
156
+ }
157
+ const current = snapshotOf(key.slice(key.indexOf("@") + 1));
158
+ if (current && current.bodySha256 === digest)
159
+ out.add(key);
160
+ }
161
+ return out;
162
+ }
163
+ /** When an approval granted now stops being valid. Computed once at write time and stored, so an entry's
164
+ * lifetime is visible in the file rather than implied by whichever version of the code reads it. */
165
+ export function expiryFor(approvedAt) {
166
+ return new Date(approvedAt.getTime() + APPROVAL_TTL_DAYS * DAY_MS).toISOString();
167
+ }
168
+ /**
169
+ * Decide whether a persisted approval still means what the human meant.
170
+ *
171
+ * Four ways it can stop meaning that, and each is reported distinctly so `/grants approvals` can explain
172
+ * itself rather than silently showing fewer rows:
173
+ *
174
+ * - `foreign-cwd` — the file was copied or committed and opened somewhere else. Nobody in THIS checkout
175
+ * was asked, so it authorises nothing here (R-27).
176
+ * - `expired` — a gate opened during one project must not still be open next quarter.
177
+ * - `type-changed` — the confused deputy. The key names a file whose contents can change after approval:
178
+ * approve `tool:write@docs-writer` when it declares `read, write`, and later that file
179
+ * gains `bash`. The entry would still match the key while describing something the
180
+ * human never saw.
181
+ * - `instructions-changed` — the same confused deputy, one level deeper. The tools are untouched but the
182
+ * BODY was rewritten, so the child would now be told to do something the human never
183
+ * saw. Only checkable since ADR-0018 gave the body a digest; an entry carrying no body
184
+ * pin lands here too, because unverifiable is not the same as unchanged.
185
+ * - `type-missing` — the type was deleted or renamed; a new file could later claim the same name.
186
+ */
187
+ export function entryVerdict(input) {
188
+ if (input.entry.cwd !== input.cwd)
189
+ return "foreign-cwd";
190
+ const expiresAt = new Date(input.entry.expiresAt).getTime();
191
+ // NaN <= x is false in JS, so an unparseable date would otherwise fail OPEN. A cache we cannot
192
+ // read the expiry of is a cache we do not trust — treat it as expired, per the spec's rule that a
193
+ // broken cache grants nothing.
194
+ if (!Number.isFinite(expiresAt) || expiresAt <= input.now.getTime())
195
+ return "expired";
196
+ if (input.current === null)
197
+ return "type-missing";
198
+ // Compare as sorted lists: reformatting or reordering a `tools:` line is not a change; adding,
199
+ // removing, or renaming a capability is.
200
+ const approved = [...input.entry.grantAtApproval].sort().join(",");
201
+ const current = [...input.current.ceiling].sort().join(",");
202
+ if (approved !== current)
203
+ return "type-changed";
204
+ // Fail closed on an unpinned entry: it was written before bodies were digested, so "unchanged" is not
205
+ // something this code can assert about it. One re-approval is the honest cost.
206
+ if (input.entry.bodyAtApproval !== input.current.bodySha256)
207
+ return "instructions-changed";
208
+ return "valid";
209
+ }
210
+ /**
211
+ * Satisfy as much of `gated` as possible without asking anyone.
212
+ *
213
+ * Precedence is inherited -> session -> persisted -> prompt. Order matters only for what gets REPORTED
214
+ * (the ledger's `approvalSource`); any hit satisfies equally. Checking all three before prompting is what
215
+ * stops an orchestrator's tenth delegation from raising a tenth identical dialog.
216
+ */
217
+ export function resolveApprovals(input) {
218
+ // ADR-0014: these are `capability@subject` keys now, not bare capabilities, so an approval given for
219
+ // one subject can no longer satisfy another.
220
+ const inherited = input.inherited ?? new Set();
221
+ const approved = [];
222
+ const needsPrompt = [];
223
+ const sources = {};
224
+ for (const capability of [...new Set(input.gated)].sort()) {
225
+ const key = approvalKey(capability, input.subject);
226
+ if (inherited.has(key)) {
227
+ approved.push(capability);
228
+ sources[capability] = "inherited";
229
+ }
230
+ else if (input.sessionApprovals.has(key)) {
231
+ approved.push(capability);
232
+ sources[capability] = "session";
233
+ }
234
+ else if (input.persisted.has(key)) {
235
+ approved.push(capability);
236
+ sources[capability] = "persisted";
237
+ }
238
+ else {
239
+ needsPrompt.push(capability);
240
+ }
241
+ }
242
+ return { approved, needsPrompt, sources };
243
+ }
244
+ //# sourceMappingURL=approval.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval.js","sourceRoot":"","sources":["../src/approval.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Capability ids: their grammar, and the two authority predicates over them.
3
+ *
4
+ * Split out of `delegate.ts` when ADR-0024 pushed that file past the 400-line ceiling and
5
+ * `test/file-size.test.ts` refused it — for the second time, and answered the same way as the first: split
6
+ * the file the way the failure message says rather than raise the cap on the guard.
7
+ *
8
+ * The seam is not arbitrary. Three modules outside `delegate.ts` already imported these
9
+ * (`extensions/session.ts` for `DELEGATE_CAPABILITY`, `extensions/delegation.ts` and
10
+ * `extensions/run-delegation.ts` for `maySpawnDefinition`), which is the evidence that they were a separate
11
+ * concern living in the wrong file: *what an id means and who may use it* is a different question from
12
+ * *what one delegation does*.
13
+ */
14
+ import { type Capability } from "./resolve.ts";
15
+ /** The capability that authorises spawning a definition (ADR-0017). `tool:*` satisfies any of them. */
16
+ export declare const agentCapability: (name: string) => Capability;
17
+ /**
18
+ * May this grant spawn that definition? (ADR-0017.)
19
+ *
20
+ * `resolve()` is exact-match plus subsumption and has no wildcard rule — a wildcard session works only
21
+ * because `deriveOwnGrant` *enumerates* its observed tools alongside `tool:*`. Definitions are not tools,
22
+ * so nothing enumerates them, and the wildcard has to be honoured here explicitly. Without that an
23
+ * UNGOVERNED session would stop being able to spawn, and "governance is opt-in" is the one rule this
24
+ * package must never break by accident.
25
+ */
26
+ export declare function maySpawnDefinition(ownGrant: Capability[], name: string): boolean;
27
+ /** The tool name that confers the ability to delegate further. */
28
+ export declare const DELEGATE_CAPABILITY: Capability;
29
+ /** Accept `read` or `tool:read` or `ext:pkg/tool` and normalise to a capability id. */
30
+ export declare function normaliseCapability(raw: string): Capability;
31
+ //# sourceMappingURL=capabilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAG/D,uGAAuG;AACvG,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,UAA6B,CAAC;AAE7E;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAShF;AAED,kEAAkE;AAClE,eAAO,MAAM,mBAAmB,EAAE,UAA4B,CAAC;AAE/D,uFAAuF;AACvF,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAM3D"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Capability ids: their grammar, and the two authority predicates over them.
3
+ *
4
+ * Split out of `delegate.ts` when ADR-0024 pushed that file past the 400-line ceiling and
5
+ * `test/file-size.test.ts` refused it — for the second time, and answered the same way as the first: split
6
+ * the file the way the failure message says rather than raise the cap on the guard.
7
+ *
8
+ * The seam is not arbitrary. Three modules outside `delegate.ts` already imported these
9
+ * (`extensions/session.ts` for `DELEGATE_CAPABILITY`, `extensions/delegation.ts` and
10
+ * `extensions/run-delegation.ts` for `maySpawnDefinition`), which is the evidence that they were a separate
11
+ * concern living in the wrong file: *what an id means and who may use it* is a different question from
12
+ * *what one delegation does*.
13
+ */
14
+ import { AGENT_WILDCARD } from "./resolve.js";
15
+ import { WILDCARD } from "./pi-tools.js";
16
+ /** The capability that authorises spawning a definition (ADR-0017). `tool:*` satisfies any of them. */
17
+ export const agentCapability = (name) => `agent:${name}`;
18
+ /**
19
+ * May this grant spawn that definition? (ADR-0017.)
20
+ *
21
+ * `resolve()` is exact-match plus subsumption and has no wildcard rule — a wildcard session works only
22
+ * because `deriveOwnGrant` *enumerates* its observed tools alongside `tool:*`. Definitions are not tools,
23
+ * so nothing enumerates them, and the wildcard has to be honoured here explicitly. Without that an
24
+ * UNGOVERNED session would stop being able to spawn, and "governance is opt-in" is the one rule this
25
+ * package must never break by accident.
26
+ */
27
+ export function maySpawnDefinition(ownGrant, name) {
28
+ // ADR-0023 adds the middle case. `tool:*` is authority to grant every tool and satisfies this too;
29
+ // `agent:*` is authority to spawn any definition and grants no tools at all, which is the configuration
30
+ // an operator wanting "any of our definitions, narrow tools" previously had to fake with `tool:*`.
31
+ return (ownGrant.includes(WILDCARD) ||
32
+ ownGrant.includes(AGENT_WILDCARD) ||
33
+ ownGrant.includes(agentCapability(name)));
34
+ }
35
+ /** The tool name that confers the ability to delegate further. */
36
+ export const DELEGATE_CAPABILITY = "tool:delegate";
37
+ /** Accept `read` or `tool:read` or `ext:pkg/tool` and normalise to a capability id. */
38
+ export function normaliseCapability(raw) {
39
+ const value = raw.trim();
40
+ if (value.startsWith("tool:") || value.startsWith("ext:") || value.startsWith("skill:") || value.startsWith("agent:")) {
41
+ return value;
42
+ }
43
+ return `tool:${value}`;
44
+ }
45
+ //# sourceMappingURL=capabilities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,cAAc,EAAmB,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,uGAAuG;AACvG,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAY,EAAc,EAAE,CAAC,SAAS,IAAI,EAAE,CAAC;AAE7E;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAsB,EAAE,IAAY;IACrE,mGAAmG;IACnG,wGAAwG;IACxG,mGAAmG;IACnG,OAAO,CACL,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAC3B,QAAQ,CAAC,QAAQ,CAAC,cAAc,CAAC;QACjC,QAAQ,CAAC,QAAQ,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CACzC,CAAC;AACJ,CAAC;AAED,kEAAkE;AAClE,MAAM,CAAC,MAAM,mBAAmB,GAAe,eAAe,CAAC;AAE/D,uFAAuF;AACvF,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IACzB,IAAI,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACtH,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,QAAQ,KAAK,EAAE,CAAC;AACzB,CAAC"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Live capability catalog — what *can* be granted, enumerated at runtime.
3
+ *
4
+ * Until now grants were checked against agent-type files alone, which covers `tool:` capabilities and
5
+ * nothing else. That leaves two gaps: extension tools are invisible (so `ext:` grants cannot be
6
+ * validated), and **skills were ungovernable** despite "skills and tools" being half the requirement.
7
+ *
8
+ * Sources, and why each is trusted:
9
+ * - **tools** — the `tools` array of a live provider request. Authoritative: it is exactly what pi sent
10
+ * the model, so it includes extension-registered tools and reflects any `--tools` allowlist already in
11
+ * force. Nothing else can see the real surface.
12
+ * - **skills** — `SKILL.md` directories and top-level `.md` files under pi's skill roots.
13
+ * - **definitions** — spawnable `SKILL.md` agents (ADR-0016), as `agent:<name>`.
14
+ *
15
+ * Provenance note: a provider payload gives tool NAMES, not owning packages, so extension tools cannot be
16
+ * qualified as `ext:<pkg>/<tool>` from that source alone. They are catalogued as `tool:<name>` — which is
17
+ * also how pi's `--tools` matches — and marked `kind: "extension"` for display. `ext:` ids remain
18
+ * supported for hand-authored grants; `deriveOwnGrant` already matches them by bare name.
19
+ */
20
+ import { type SkillDefinition } from "./definitions.ts";
21
+ import { type Capability } from "./resolve.ts";
22
+ export type CapabilityKind = "builtin" | "extension" | "skill" | "agentType";
23
+ export interface CatalogEntry {
24
+ capability: Capability;
25
+ kind: CapabilityKind;
26
+ /** Where it was found, for display and debugging. */
27
+ source?: string;
28
+ }
29
+ export interface Catalog {
30
+ entries: CatalogEntry[];
31
+ /** Every capability id in the catalog. */
32
+ all: Capability[];
33
+ byKind(kind: CapabilityKind): Capability[];
34
+ has(capability: Capability): boolean;
35
+ }
36
+ /** Split observed tool names into pi built-ins and extension-provided tools. */
37
+ export declare function classifyToolNames(observed: string[]): CatalogEntry[];
38
+ /** Skill roots pi discovers, project first. */
39
+ export declare function skillDirs(cwd: string): string[];
40
+ /**
41
+ * Discover skills: a directory containing `SKILL.md` is one skill named after the directory; a top-level
42
+ * `.md` file is a skill named after the file. Mirrors pi's documented convention.
43
+ *
44
+ * Directories are not descended into beyond one level, matching pi's rule that a directory containing
45
+ * `SKILL.md` is a single skill rather than a tree to explore.
46
+ */
47
+ export declare function loadSkills(cwd: string): Promise<CatalogEntry[]>;
48
+ /**
49
+ * Spawnable definitions, as `agent:<name>` capabilities.
50
+ *
51
+ * A definition is BOTH a skill (loadable into a session) and an agent (spawnable as a child) — ADR-0016
52
+ * collapsed those into one file — so the same `SKILL.md` legitimately appears twice in the catalog under
53
+ * two capability ids. That is not duplication: `skill:review` means "may load these instructions" and
54
+ * `agent:review` means "may spawn a child running them", and a grant can hold either without the other.
55
+ */
56
+ export declare function definitionEntries(definitions: Map<string, SkillDefinition>): CatalogEntry[];
57
+ /** Assemble a catalog from parts. Pure, so it is testable without a filesystem. */
58
+ export declare function makeCatalog(entries: CatalogEntry[]): Catalog;
59
+ /** Build the live catalog. `observedTools` comes from a provider payload; null when not yet seen. */
60
+ export declare function buildCatalog(input: {
61
+ cwd: string;
62
+ observedTools: string[] | null;
63
+ }): Promise<Catalog>;
64
+ /**
65
+ * Capabilities requested that the catalog does not contain.
66
+ *
67
+ * Reported separately from `denied` because the causes differ and so do the fixes: `denied` means the
68
+ * delegator lacks authority, `unknown` means the capability does not exist here — usually a typo or a
69
+ * stale grant referring to an uninstalled package. Silently treating unknown as denied hides that.
70
+ */
71
+ export declare function unknownCapabilities(requested: Capability[], catalog: Catalog): Capability[];
72
+ /**
73
+ * Skill name -> absolute path, for `planSpawn`'s `--skill` flags (R-32).
74
+ *
75
+ * Derived from the catalog's own `source` field rather than re-scanning, so what a child is handed
76
+ * cannot drift from what was discovered and offered. A skill entry without a source is omitted, which
77
+ * makes it *unresolvable* rather than silently absent — `planDelegation` refuses on that, because a
78
+ * grant naming a skill the child never receives is a ledger line that lies.
79
+ */
80
+ export declare function skillPathsFromCatalog(catalog: Catalog): Record<string, string>;
81
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAKH,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEzE,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/D,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,WAAW,GAAG,OAAO,GAAG,WAAW,CAAC;AAE7E,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,UAAU,CAAC;IACvB,IAAI,EAAE,cAAc,CAAC;IACrB,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,0CAA0C;IAC1C,GAAG,EAAE,UAAU,EAAE,CAAC;IAClB,MAAM,CAAC,IAAI,EAAE,cAAc,GAAG,UAAU,EAAE,CAAC;IAC3C,GAAG,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC;CACtC;AAED,gFAAgF;AAChF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,YAAY,EAAE,CAMpE;AAED,+CAA+C;AAC/C,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAE/C;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CA+BrE;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,YAAY,EAAE,CAM3F;AAED,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,OAAO,CAY5D;AAED,qGAAqG;AACrG,wBAAsB,YAAY,CAAC,KAAK,EAAE;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CAChC,GAAG,OAAO,CAAC,OAAO,CAAC,CAkBnB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,UAAU,EAAE,EAAE,OAAO,EAAE,OAAO,GAAG,UAAU,EAAE,CAQ3F;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAO9E"}
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Live capability catalog — what *can* be granted, enumerated at runtime.
3
+ *
4
+ * Until now grants were checked against agent-type files alone, which covers `tool:` capabilities and
5
+ * nothing else. That leaves two gaps: extension tools are invisible (so `ext:` grants cannot be
6
+ * validated), and **skills were ungovernable** despite "skills and tools" being half the requirement.
7
+ *
8
+ * Sources, and why each is trusted:
9
+ * - **tools** — the `tools` array of a live provider request. Authoritative: it is exactly what pi sent
10
+ * the model, so it includes extension-registered tools and reflects any `--tools` allowlist already in
11
+ * force. Nothing else can see the real surface.
12
+ * - **skills** — `SKILL.md` directories and top-level `.md` files under pi's skill roots.
13
+ * - **definitions** — spawnable `SKILL.md` agents (ADR-0016), as `agent:<name>`.
14
+ *
15
+ * Provenance note: a provider payload gives tool NAMES, not owning packages, so extension tools cannot be
16
+ * qualified as `ext:<pkg>/<tool>` from that source alone. They are catalogued as `tool:<name>` — which is
17
+ * also how pi's `--tools` matches — and marked `kind: "extension"` for display. `ext:` ids remain
18
+ * supported for hand-authored grants; `deriveOwnGrant` already matches them by bare name.
19
+ */
20
+ import { readdir, stat } from "node:fs/promises";
21
+ import { homedir } from "node:os";
22
+ import { join } from "node:path";
23
+ import { loadDefinitions } from "./definitions.js";
24
+ import { PI_BUILTIN_TOOLS, WILDCARD } from "./pi-tools.js";
25
+ import { AGENT_WILDCARD } from "./resolve.js";
26
+ /** Split observed tool names into pi built-ins and extension-provided tools. */
27
+ export function classifyToolNames(observed) {
28
+ const builtins = new Set(PI_BUILTIN_TOOLS);
29
+ return [...new Set(observed)].sort().map((name) => ({
30
+ capability: `tool:${name}`,
31
+ kind: builtins.has(name) ? "builtin" : "extension",
32
+ }));
33
+ }
34
+ /** Skill roots pi discovers, project first. */
35
+ export function skillDirs(cwd) {
36
+ return [join(cwd, ".pi", "skills"), join(homedir(), ".pi", "agent", "skills")];
37
+ }
38
+ /**
39
+ * Discover skills: a directory containing `SKILL.md` is one skill named after the directory; a top-level
40
+ * `.md` file is a skill named after the file. Mirrors pi's documented convention.
41
+ *
42
+ * Directories are not descended into beyond one level, matching pi's rule that a directory containing
43
+ * `SKILL.md` is a single skill rather than a tree to explore.
44
+ */
45
+ export async function loadSkills(cwd) {
46
+ const found = new Map();
47
+ for (const dir of skillDirs(cwd)) {
48
+ let names;
49
+ try {
50
+ names = await readdir(dir);
51
+ }
52
+ catch {
53
+ continue; // absent skill root is normal
54
+ }
55
+ for (const name of names.sort()) {
56
+ const path = join(dir, name);
57
+ try {
58
+ const info = await stat(path);
59
+ if (info.isDirectory()) {
60
+ const inner = await readdir(path).catch(() => []);
61
+ if (inner.includes("SKILL.md") && !found.has(name)) {
62
+ found.set(name, { capability: `skill:${name}`, kind: "skill", source: path });
63
+ }
64
+ }
65
+ else if (name.endsWith(".md")) {
66
+ const skill = name.replace(/\.md$/, "");
67
+ if (!found.has(skill)) {
68
+ found.set(skill, { capability: `skill:${skill}`, kind: "skill", source: path });
69
+ }
70
+ }
71
+ }
72
+ catch {
73
+ // An unreadable entry is simply not catalogued; it therefore cannot be granted, which is the
74
+ // fail-closed direction.
75
+ }
76
+ }
77
+ }
78
+ return [...found.values()];
79
+ }
80
+ /**
81
+ * Spawnable definitions, as `agent:<name>` capabilities.
82
+ *
83
+ * A definition is BOTH a skill (loadable into a session) and an agent (spawnable as a child) — ADR-0016
84
+ * collapsed those into one file — so the same `SKILL.md` legitimately appears twice in the catalog under
85
+ * two capability ids. That is not duplication: `skill:review` means "may load these instructions" and
86
+ * `agent:review` means "may spawn a child running them", and a grant can hold either without the other.
87
+ */
88
+ export function definitionEntries(definitions) {
89
+ return [...definitions.values()].map((d) => ({
90
+ capability: `agent:${d.name}`,
91
+ kind: "agentType",
92
+ source: d.source,
93
+ }));
94
+ }
95
+ /** Assemble a catalog from parts. Pure, so it is testable without a filesystem. */
96
+ export function makeCatalog(entries) {
97
+ const deduped = new Map();
98
+ for (const entry of entries)
99
+ if (!deduped.has(entry.capability))
100
+ deduped.set(entry.capability, entry);
101
+ const list = [...deduped.values()].sort((a, b) => a.capability.localeCompare(b.capability));
102
+ const ids = list.map((e) => e.capability);
103
+ const idSet = new Set(ids);
104
+ return {
105
+ entries: list,
106
+ all: ids,
107
+ byKind: (kind) => list.filter((e) => e.kind === kind).map((e) => e.capability),
108
+ has: (capability) => idSet.has(capability),
109
+ };
110
+ }
111
+ /** Build the live catalog. `observedTools` comes from a provider payload; null when not yet seen. */
112
+ export async function buildCatalog(input) {
113
+ const [skills, definitions] = await Promise.all([loadSkills(input.cwd), loadDefinitions(input.cwd)]);
114
+ return makeCatalog([
115
+ // pi's built-ins are seeded unconditionally, because they are known statically and the catalog is
116
+ // consulted BEFORE any provider request has happened — `/grants` runs at that point. Without this,
117
+ // every capability looked "unknown" until the first model call, so the preview refused grants that
118
+ // enforcement would have allowed: R-28's failure shape (a diagnostic disagreeing with the enforcer)
119
+ // reappearing through a different door.
120
+ //
121
+ // The trade-off, stated plainly: in a session started with `--tools read`, this still lists `bash`
122
+ // as an existing capability, so a delegation naming it passes the *unknown* check and is refused by
123
+ // the *grant* check instead ("this session does not hold it"). That is the better error anyway, and
124
+ // the grant check — not this catalog — is the authority. Nothing here grants anything.
125
+ ...PI_BUILTIN_TOOLS.map((name) => ({ capability: `tool:${name}`, kind: "builtin" })),
126
+ ...(input.observedTools ? classifyToolNames(input.observedTools) : []),
127
+ ...skills,
128
+ ...definitionEntries(definitions),
129
+ ]);
130
+ }
131
+ /**
132
+ * Capabilities requested that the catalog does not contain.
133
+ *
134
+ * Reported separately from `denied` because the causes differ and so do the fixes: `denied` means the
135
+ * delegator lacks authority, `unknown` means the capability does not exist here — usually a typo or a
136
+ * stale grant referring to an uninstalled package. Silently treating unknown as denied hides that.
137
+ */
138
+ export function unknownCapabilities(requested, catalog) {
139
+ // Wildcards are GRAMMAR, not entries. Nothing enumerates them into the catalog — `definitionEntries`
140
+ // emits `agent:<name>` per discovered definition and `PI_BUILTIN_TOOLS` contains no `*` — so this check
141
+ // reported `agent:*` as *"not present in this session's catalog (typo, or an uninstalled package?)"* and
142
+ // refused it BEFORE `resolve` could apply ADR-0023's rule. That made the ADR's "a parent holding
143
+ // `agent:*` may hand down `agent:*`" false, and made a definition declaring `allowed-tools: agent:*`
144
+ // unspawnable from any grant. The wildcard is live only at the root without this.
145
+ return requested.filter((c) => c !== WILDCARD && c !== AGENT_WILDCARD && !catalog.has(c)).sort();
146
+ }
147
+ /**
148
+ * Skill name -> absolute path, for `planSpawn`'s `--skill` flags (R-32).
149
+ *
150
+ * Derived from the catalog's own `source` field rather than re-scanning, so what a child is handed
151
+ * cannot drift from what was discovered and offered. A skill entry without a source is omitted, which
152
+ * makes it *unresolvable* rather than silently absent — `planDelegation` refuses on that, because a
153
+ * grant naming a skill the child never receives is a ledger line that lies.
154
+ */
155
+ export function skillPathsFromCatalog(catalog) {
156
+ const paths = {};
157
+ for (const entry of catalog.entries) {
158
+ if (entry.kind !== "skill" || !entry.source)
159
+ continue;
160
+ paths[entry.capability.slice("skill:".length)] = entry.source;
161
+ }
162
+ return paths;
163
+ }
164
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"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"}