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,311 @@
1
+ /**
2
+ * Persistence for `always`-scoped approvals — the package's only mutable state outside the ledger.
3
+ *
4
+ * DESIGN NOTE, because it is easy to get backwards: this file is a CONVENIENCE CACHE, not a security
5
+ * control. The security decision was already made by a human at the moment of approval. So a failure here
6
+ * must never fail the work — an unwritable file downgrades the approval to session scope (see the boolean
7
+ * return of `saveApproval`), and an unreadable one simply grants nothing.
8
+ *
9
+ * Read on demand, never cached at session start, so **a revoke takes effect at the next gate check** —
10
+ * including a revoke performed from another session while this one is running.
11
+ *
12
+ * That sentence used to read *"takes effect immediately"* and claimed two different things, one of which was
13
+ * false and one of which is impossible:
14
+ *
15
+ * - **False, and fixed (R-49).** Every write is load → modify → write and it was unlocked, so a save could
16
+ * restore an entry another session had just revoked. Writes now hold the same file lock the ledger uses
17
+ * (`src/file-lock.ts`, `underLock` below).
18
+ * - **Impossible, and stated rather than fixed.** A spawn whose gate check has already passed is not
19
+ * retracted by a revoke arriving microseconds later. No lock closes that: the read has to finish before
20
+ * the spawn starts, so there is always an instant where the decision is made and the process is not yet
21
+ * running. Inherent to revoking anything, not a gap in this one.
22
+ *
23
+ * Reads deliberately take no lock. A read that loses a race sees the previous state, which is exactly what
24
+ * "at the next gate check" already means.
25
+ *
26
+ * **One file per project** (ADR-0020), and **no model-authored text, ever** (ADR-0021) — see `approvalsPath`
27
+ * and `sanitise` for why each of those is a decision rather than a detail.
28
+ *
29
+ * Pruning is deliberately lazy: `loadApprovals` never writes, so a read is a read. Invalid entries are
30
+ * dropped from the file on the next `saveApproval` or `revokeApproval`.
31
+ */
32
+ import { createHash, randomUUID } from "node:crypto";
33
+ import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
34
+ import { withFileLock } from "./file-lock.js";
35
+ import { homedir } from "node:os";
36
+ import { basename, dirname, join } from "node:path";
37
+ import { entryVerdict } from "./approval.js";
38
+ /**
39
+ * Where persisted approvals live — **outside the governed workspace** (ADR-0014).
40
+ *
41
+ * It used to be `<cwd>/.pi/grants-approvals.json`, which was self-defeating in this package's own
42
+ * recommended configuration: `PI_GRANTS_GATED=tool:write` means *"may use write, may not pass it down
43
+ * without a human"*, and **a session that may use `write` can write the approvals file**. A reviewer
44
+ * demonstrated it end to end, including authoring a matching agent-type file so `grantAtApproval`
45
+ * compared equal — no dialog, and a ledger line reading `approvalSource: "persisted"`, indistinguishable
46
+ * from a real human approval.
47
+ *
48
+ * A narrowed child does not hold write access to the user's home directory, so it cannot forge an entry
49
+ * here. Each entry still carries its own `cwd` and `entryVerdict` still checks it — that check exists for
50
+ * R-27 and refuses an entry copied between checkouts or machines, which per-project files do not make
51
+ * redundant.
52
+ *
53
+ * **This does not defend against a child holding `bash`** — see ADR-0012, which accepts that such a
54
+ * child can escape governance entirely. The point of this change is to close the *self-defeating* case,
55
+ * not to claim a boundary the package does not have.
56
+ *
57
+ * **ONE FILE PER PROJECT since 0.11.0 (ADR-0020).** It was one shared document keyed only by
58
+ * `capability@subject`, which produced four defects in eight lines — approving in one checkout deleted
59
+ * another's entries (R-41), `revoke --all` cleared every project (R-43), two concurrent writes lost both
60
+ * (R-42), and an unlocked read-modify-write could resurrect a revoked entry (R-49, fixed in 0.13.0). The
61
+ * unfixable one was the keyspace: two checkouts holding definitions of the same name — `review`, `deploy`,
62
+ * i.e. what happens the moment an operator reuses their own conventions — could not both hold an approval,
63
+ * so they took turns indefinitely. Per-project files make the collision **inexpressible** rather than
64
+ * handled, and `revoke --all` cannot name another project's file.
65
+ *
66
+ * The `cwd` is hashed as well as named: the basename keeps the file legible to a human reading the
67
+ * directory, and the hash is what makes it unambiguous, since two checkouts can share a basename.
68
+ *
69
+ * **It took a `cwd` parameter and ignored it until 0.10.2**, which was not a harmless vestige: the unit
70
+ * suite passed a `mkdtemp` directory to it, reasonably believed the result was hermetic, and spent every
71
+ * `npm test` rewriting and clearing the developer's real store in `$HOME` (R-40). That was invisible while
72
+ * the store was unwritable and became destructive the day ADR-0019 made it reachable. The parameter is now
73
+ * real and required, which is the opposite failure mode: forgetting it is a type error.
74
+ */
75
+ export function approvalsPath(cwd) {
76
+ const slug = (basename(cwd) || "root").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40);
77
+ // 16 hex = 64 bits, not the 6 this shipped with. ADR-0020 deleted the `foreign-cwd` carry-through on the
78
+ // premise that one file means one directory — so inside a hash collision R-41 returns *with its
79
+ // mitigation removed*: the second project's save deletes the first's entries. At 24 bits a deliberate
80
+ // collision costs about 16.7M hashes, well under a second, and an accidental one arrives at a few
81
+ // thousand governed directories. The premise has to be worth what was removed to rely on it.
82
+ const hash = createHash("sha256").update(cwd, "utf8").digest("hex").slice(0, 16);
83
+ return join(agentDir(), "grants-approvals", `${slug}-${hash}.json`);
84
+ }
85
+ /**
86
+ * The shared single-file store, so it can be REPORTED rather than read (ADR-0020).
87
+ *
88
+ * Deliberately not migrated. Splitting it by each entry's own `cwd` would be mechanical and lossless — the
89
+ * trust root is unchanged, unlike ADR-0014's move out of the workspace — but it is code that runs once, is
90
+ * exercised on exactly one input per machine, and lives in the layer with nine recorded defects. Re-approving
91
+ * costs a click; a migration bug costs a silently wrong approval.
92
+ */
93
+ export function sharedApprovalsPath() {
94
+ return join(agentDir(), "grants-approvals.json");
95
+ }
96
+ /**
97
+ * The old in-workspace location, so it can be REPORTED rather than read.
98
+ *
99
+ * Deliberately not migrated. Importing a legacy file would import exactly the entries whose
100
+ * trustworthiness this change exists to remove — a forged approval would survive the fix that was
101
+ * supposed to stop it. The extension names the file and ignores it; re-approving is a few keystrokes and
102
+ * the only honest path.
103
+ */
104
+ export function legacyApprovalsPath(cwd) {
105
+ return join(cwd, ".pi", "grants-approvals.json");
106
+ }
107
+ /** `$PI_CODING_AGENT_DIR`, or pi's default. Matches how pi-subagents resolves the same directory. */
108
+ function agentDir() {
109
+ return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
110
+ }
111
+ /** The subject half of `capability@subject`. Capability ids contain `:` but never `@`. */
112
+ function subjectOf(key) {
113
+ return key.slice(key.indexOf("@") + 1);
114
+ }
115
+ /** Guard: reject entries that don't have the shape of an ApprovalEntry. */
116
+ function isValidEntryShape(entry) {
117
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
118
+ return false;
119
+ }
120
+ const obj = entry;
121
+ return (typeof obj.approvedAt === "string" &&
122
+ typeof obj.expiresAt === "string" &&
123
+ typeof obj.cwd === "string" &&
124
+ Array.isArray(obj.grantAtApproval));
125
+ }
126
+ /**
127
+ * Project every entry through its DECLARED fields on the way out (ADR-0021).
128
+ *
129
+ * The instance this closes: `taskAtApproval` stored the model-authored task string, which `src/ledger.ts`
130
+ * forbids in unqualified terms — *"the task is not recorded, anywhere, ever"*. Removing the field from the
131
+ * type is not enough on its own, because entries are parsed from JSON and a rewrite would carry any
132
+ * undeclared property straight back to disk.
133
+ *
134
+ * A whitelist rather than a delete, so this closes the class: no future field can reach the store by being
135
+ * present on a parsed object, and adding one is a deliberate edit here.
136
+ */
137
+ function sanitise(valid) {
138
+ return Object.fromEntries([...valid].map(([key, e]) => [
139
+ key,
140
+ {
141
+ approvedAt: e.approvedAt,
142
+ expiresAt: e.expiresAt,
143
+ cwd: e.cwd,
144
+ grantAtApproval: e.grantAtApproval,
145
+ ...(e.bodyAtApproval !== undefined ? { bodyAtApproval: e.bodyAtApproval } : {}),
146
+ },
147
+ ]));
148
+ }
149
+ async function readFileSafely(cwd) {
150
+ try {
151
+ const parsed = JSON.parse(await readFile(approvalsPath(cwd), "utf8"));
152
+ if (!parsed || typeof parsed !== "object")
153
+ return { version: 1, approvals: {} };
154
+ const file = parsed;
155
+ if (file.version !== 1 || !file.approvals || typeof file.approvals !== "object") {
156
+ return { version: 1, approvals: {} };
157
+ }
158
+ return { version: 1, approvals: file.approvals };
159
+ }
160
+ catch {
161
+ // Missing is normal; corrupt grants nothing. Either way the caller re-prompts, which is safe.
162
+ return { version: 1, approvals: {} };
163
+ }
164
+ }
165
+ /** Load the approvals valid HERE and NOW, plus the ones that were dropped and why. */
166
+ export async function loadApprovals(input) {
167
+ const file = await readFileSafely(input.cwd);
168
+ const valid = new Map();
169
+ const dropped = [];
170
+ for (const [key, entry] of Object.entries(file.approvals)) {
171
+ // Validate entry shape before passing to entryVerdict. Malformed entries cannot take valid ones
172
+ // with them — one bad shape means that one entry is dropped, the rest are still evaluated.
173
+ if (!isValidEntryShape(entry)) {
174
+ dropped.push({ key, entry: entry, verdict: "expired" });
175
+ continue;
176
+ }
177
+ const verdict = entryVerdict({
178
+ entry,
179
+ cwd: input.cwd,
180
+ now: input.now,
181
+ current: input.snapshotOf(subjectOf(key)),
182
+ });
183
+ if (verdict === "valid")
184
+ valid.set(key, entry);
185
+ else
186
+ dropped.push({ key, entry, verdict });
187
+ }
188
+ return { valid, dropped };
189
+ }
190
+ /**
191
+ * Write the file atomically, and never through a symlink (ADR-0014).
192
+ *
193
+ * Two defects this closes:
194
+ *
195
+ * - **B-I6** — writes followed project-controlled symlinks and were not atomic, so a crash or a
196
+ * concurrent writer could leave a half-written file that the next read discards entirely.
197
+ * - **A-R2** — a corrupt file made the next legitimate write destroy every other entry, because entries
198
+ * are validated on read but pruned only on write. Writing to a temp file and renaming makes the
199
+ * replacement all-or-nothing; `wx` on the temp refuses to follow an existing link.
200
+ *
201
+ * `rename` is atomic within a filesystem, and the temp file is created in the same directory precisely so
202
+ * that holds.
203
+ */
204
+ async function writeFileSafely(cwd, file) {
205
+ const path = approvalsPath(cwd);
206
+ // Same directory as the target: `rename` is only atomic within one filesystem.
207
+ //
208
+ // **Unique per CALL, not per process.** It was `${path}.${pid}.tmp`, so two concurrent `saveApproval`
209
+ // calls in one process collided: the second's `wx` failed EEXIST, its `catch` unlinked the *first's*
210
+ // in-flight temp, and the first's `rename` then failed ENOENT — **both returned false and nothing was
211
+ // written**, on a perfectly writable file. Measured with two different keys, so it was not limited to
212
+ // the shared-dialog case: any two concurrent writes lost both. `delegate_all` is exactly that shape, and
213
+ // both callers would report "could not persist the approval — it applies for this session only", which
214
+ // named the wrong cause.
215
+ const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
216
+ try {
217
+ await mkdir(dirname(path), { recursive: true });
218
+ // `wx` fails rather than following a pre-existing symlink or clobbering another writer's temp.
219
+ await writeFile(temp, `${JSON.stringify(file, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
220
+ await rename(temp, path);
221
+ return true;
222
+ }
223
+ catch {
224
+ try {
225
+ await unlink(temp);
226
+ }
227
+ catch {
228
+ /* nothing to clean up */
229
+ }
230
+ return false;
231
+ }
232
+ }
233
+ /**
234
+ * Persist one approval, pruning anything THIS session can see has become invalid.
235
+ *
236
+ * Returns false when the write failed. The caller must then downgrade to session scope and warn — NOT
237
+ * refuse the delegation. The human already said yes; refusing work because a cache could not be written
238
+ * would be failing closed on the wrong thing.
239
+ */
240
+ export async function saveApproval(cwd, key, entry, snapshotOf, now) {
241
+ return underLock(cwd, false, false, async () => {
242
+ const { valid } = await loadApprovals({ cwd, now, snapshotOf });
243
+ valid.set(key, entry);
244
+ // ADR-0020: this file belongs to ONE project, so there is nothing here that another project could own and
245
+ // pruning cannot reach across a boundary. The `foreign-cwd` carry-through 0.10.2 needed is gone with the
246
+ // shared file that made it necessary.
247
+ return writeFileSafely(cwd, { version: 1, approvals: sanitise(valid) });
248
+ });
249
+ }
250
+ export async function revokeApproval(cwd, key, snapshotOf, now) {
251
+ return underLock(cwd, "busy", "failed", async () => {
252
+ const { valid } = await loadApprovals({ cwd, now, snapshotOf });
253
+ if (!valid.has(key))
254
+ return "absent";
255
+ valid.delete(key);
256
+ return (await writeFileSafely(cwd, { version: 1, approvals: sanitise(valid) })) ? "revoked" : "failed";
257
+ });
258
+ }
259
+ /**
260
+ * Clear every approval **for this directory**. Returns false if the write failed.
261
+ *
262
+ * Scoped rather than global, and the old behaviour was the surprising one: `/grants revoke --all` wrote an
263
+ * empty file, so revoking in one project silently revoked every other project's approvals too. An operator
264
+ * running it in one checkout is answering for that checkout — there is no interface for "and everywhere
265
+ * else", and it should not be the default reading of a command that names neither.
266
+ */
267
+ export async function revokeAll(cwd) {
268
+ return underLock(cwd, false, false, () => writeFileSafely(cwd, { version: 1, approvals: {} }));
269
+ }
270
+ /**
271
+ * Hold the store's lock for one read-modify-write (R-49).
272
+ *
273
+ * **The race it closes.** Every write here is load → modify → write, and it was unlocked, so: session 1
274
+ * loads; session 2 revokes; session 1 saves an unrelated approval and **restores the revoked entry** for the
275
+ * rest of its 30 days, with no error and no warning. `approval-store.ts` documents that *"a revoke takes
276
+ * effect immediately — including one performed from another session while this one is running"*, and that
277
+ * sentence was false. Narrow (ADR-0020 scoped it to two sessions in the same directory) and cheap to close,
278
+ * because the lock already existed for the ledger — `src/file-lock.ts`, one implementation, two callers.
279
+ *
280
+ * **A lock this cannot take does NOT fail the work**, which is the opposite of the ledger's choice with the
281
+ * same lock and follows from what the two files are. The ledger is a security control: no audit line, no
282
+ * spawn. This store is a convenience cache (ADR-0020) — the human already said yes, and refusing their work
283
+ * because a cache was busy would be failing closed on the wrong thing. So a timeout yields `busy`, which the
284
+ * caller reports as an ordinary write failure and downgrades to session scope.
285
+ */
286
+ async function underLock(cwd, onBusy, onError, work) {
287
+ const path = approvalsPath(cwd);
288
+ // **The discriminant is whether anything was READ, not which error was thrown.** Keying on
289
+ // `LockTimeoutError` looked right and was not: `EMFILE` — the classic transient, and one a fan-out of
290
+ // children plus herdr panes produces — happens before the load just as a timeout does, yet took the
291
+ // `failed` branch, whose message asserts the approval "is still in effect" about an entry nobody looked
292
+ // for, and blames a path that is perfectly writable. That is the very defect the fourth outcome was
293
+ // added to prevent, one error code to the left.
294
+ let entered = false;
295
+ try {
296
+ // The lock lives beside the file, so its directory must exist before the lock can be taken — and on a
297
+ // first-ever approval it does not. `writeFileSafely` creates it, which is one step too late: every
298
+ // write failed with ENOENT on the LOCK and was reported as busy. Caught by the existing round-trip
299
+ // tests within a minute of adding the lock, which is the argument for having them.
300
+ await mkdir(dirname(path), { recursive: true });
301
+ return await withFileLock(path, "approvals file", () => {
302
+ entered = true;
303
+ return work();
304
+ });
305
+ }
306
+ catch {
307
+ // Never reached the load, so nothing may be claimed about the entry — whatever the cause.
308
+ return entered ? onError : onBusy;
309
+ }
310
+ }
311
+ //# sourceMappingURL=approval-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-store.js","sourceRoot":"","sources":["../src/approval-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9E,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,YAAY,EAA+D,MAAM,eAAe,CAAC;AAqB1G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,MAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACrF,yGAAyG;IACzG,gGAAgG;IAChG,sGAAsG;IACtG,kGAAkG;IAClG,6FAA6F;IAC7F,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACjF,OAAO,IAAI,CAAC,QAAQ,EAAE,EAAE,kBAAkB,EAAE,GAAG,IAAI,IAAI,IAAI,OAAO,CAAC,CAAC;AACtE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO,IAAI,CAAC,QAAQ,EAAE,EAAE,uBAAuB,CAAC,CAAC;AACnD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,OAAO,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,uBAAuB,CAAC,CAAC;AACnD,CAAC;AAED,qGAAqG;AACrG,SAAS,QAAQ;IACf,OAAO,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;AAC5E,CAAC;AAED,0FAA0F;AAC1F,SAAS,SAAS,CAAC,GAAW;IAC5B,OAAO,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;AACzC,CAAC;AAED,2EAA2E;AAC3E,SAAS,iBAAiB,CAAC,KAAc;IACvC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAChE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,GAAG,GAAG,KAAgC,CAAC;IAC7C,OAAO,CACL,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ;QAClC,OAAO,GAAG,CAAC,SAAS,KAAK,QAAQ;QACjC,OAAO,GAAG,CAAC,GAAG,KAAK,QAAQ;QAC3B,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CACnC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,QAAQ,CAAC,KAAiC;IACjD,OAAO,MAAM,CAAC,WAAW,CACvB,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;QAC3B,GAAG;QACH;YACE,UAAU,EAAE,CAAC,CAAC,UAAU;YACxB,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,GAAG,EAAE,CAAC,CAAC,GAAG;YACV,eAAe,EAAE,CAAC,CAAC,eAAe;YAClC,GAAG,CAAC,CAAC,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAChF;KACF,CAAC,CACH,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,GAAW;IACvC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAY,CAAC;QACjF,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;QAChF,MAAM,IAAI,GAAG,MAA+B,CAAC;QAC7C,IAAI,IAAI,CAAC,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC;YAChF,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;QACvC,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,8FAA8F;QAC9F,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;IACvC,CAAC;AACH,CAAC;AAQD,sFAAsF;AACtF,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,KAAyB;IAEzB,MAAM,IAAI,GAAG,MAAM,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAyB,CAAC;IAC/C,MAAM,OAAO,GAAsB,EAAE,CAAC;IAEtC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QAC1D,gGAAgG;QAChG,2FAA2F;QAC3F,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAE,CAAC;YAC9B,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,KAAsB,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;YACzE,SAAS;QACX,CAAC;QAED,MAAM,OAAO,GAAG,YAAY,CAAC;YAC3B,KAAK;YACL,GAAG,EAAE,KAAK,CAAC,GAAG;YACd,GAAG,EAAE,KAAK,CAAC,GAAG;YACd,OAAO,EAAE,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;SAC1C,CAAC,CAAC;QACH,IAAI,OAAO,KAAK,OAAO;YAAE,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;;YAC1C,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AAC5B,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,KAAK,UAAU,eAAe,CAAC,GAAW,EAAE,IAAkB;IAC5D,MAAM,IAAI,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IAChC,+EAA+E;IAC/E,EAAE;IACF,sGAAsG;IACtG,qGAAqG;IACrG,sGAAsG;IACtG,sGAAsG;IACtG,yGAAyG;IACzG,uGAAuG;IACvG,yBAAyB;IACzB,MAAM,IAAI,GAAG,GAAG,IAAI,IAAI,OAAO,CAAC,GAAG,IAAI,UAAU,EAAE,MAAM,CAAC;IAC1D,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,+FAA+F;QAC/F,MAAM,SAAS,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9F,MAAM,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACzB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACP,yBAAyB;QAC3B,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,GAAW,EACX,GAAW,EACX,KAAoB,EACpB,UAAyB,EACzB,GAAS;IAET,OAAO,SAAS,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,IAAI,EAAE;QAC7C,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,aAAa,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,CAAC;QAChE,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACtB,0GAA0G;QAC1G,yGAAyG;QACzG,sCAAsC;QACtC,OAAO,eAAe,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;AACL,CAAC;AAuBD,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,GAAW,EACX,GAAW,EACX,UAAyB,EACzB,GAAS;IAET,OAAO,SAAS,CAAgB,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,IAAI,EAAE;QAChE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,aAAa,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,CAAC;QAChE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,QAAQ,CAAC;QACrC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAClB,OAAO,CAAC,MAAM,eAAe,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC;IACzG,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,GAAW;IACzC,OAAO,SAAS,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,eAAe,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;AACjG,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,KAAK,UAAU,SAAS,CAAI,GAAW,EAAE,MAAS,EAAE,OAAU,EAAE,IAAsB;IACpF,MAAM,IAAI,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IAChC,2FAA2F;IAC3F,sGAAsG;IACtG,oGAAoG;IACpG,wGAAwG;IACxG,oGAAoG;IACpG,gDAAgD;IAChD,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,CAAC;QACH,sGAAsG;QACtG,mGAAmG;QACnG,mGAAmG;QACnG,mFAAmF;QACnF,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,OAAO,MAAM,YAAY,CAAC,IAAI,EAAE,gBAAgB,EAAE,GAAG,EAAE;YACrD,OAAO,GAAG,IAAI,CAAC;YACf,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,0FAA0F;QAC1F,OAAO,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;IACpC,CAAC;AACH,CAAC"}
@@ -0,0 +1,221 @@
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 type { Capability, ResolveResult } from "./resolve.ts";
12
+ /** How far a single yes reaches in time. */
13
+ export type ApprovalScope = "once" | "session" | "always";
14
+ /** Where a yes came from, for the ledger. These call for different follow-ups, so they stay distinct. */
15
+ export type ApprovalSource = "prompt" | "session" | "persisted" | "inherited";
16
+ /** Which call site is asking. Determines the scopes offered — see `offeredScopes`. */
17
+ export type ApprovalPath = "definition" | "delegate";
18
+ /**
19
+ * Subject used for delegate-path approvals.
20
+ *
21
+ * The delegate tool has no human-authored subject: the only things naming a child are the task string and
22
+ * the tool list, both chosen by the model. A key the model controls is not a key, so delegate approvals
23
+ * use this fixed literal — "allow write for delegations this session" — and are never persisted.
24
+ * Angle brackets cannot appear in an agent-type name (`parseAgentType` reads an identifier), so this can
25
+ * never collide with a real type.
26
+ */
27
+ export declare const DELEGATE_SUBJECT = "<delegate>";
28
+ /**
29
+ * Lifetime of a persisted approval.
30
+ *
31
+ * Deliberately a constant rather than an environment variable: a knob that silently extends every gate is
32
+ * the first thing an impatient operator would reach for.
33
+ */
34
+ export declare const APPROVAL_TTL_DAYS = 30;
35
+ /** `tool:write` + `docs-writer` -> `tool:write@docs-writer`. */
36
+ export declare function approvalKey(capability: Capability, subject: string): string;
37
+ /**
38
+ * May this resolution raise an approval dialog?
39
+ *
40
+ * `resolve()` computes `denied` and `gatedBlocked` INDEPENDENTLY (deliberately — each rejection reason is
41
+ * reported rather than masked by whichever filter ran first), and both `decideSpawn` and `planDelegation`
42
+ * refuse outright on `denied` while still attaching the full result. So a request mixing a gated capability
43
+ * with one the session does not hold at all would otherwise prompt a human about the gated one and then be
44
+ * refused anyway for the other.
45
+ *
46
+ * That is not merely noise. A *session* yes is recorded and republished to children, and an *always* yes is
47
+ * written to disk for 30 days — both banked against a spawn that never happened, and both reachable by a
48
+ * model that appends one unheld capability to an otherwise ordinary request. A person is therefore only
49
+ * asked about a spawn that would actually proceed if they said yes.
50
+ *
51
+ * `clipped` is deliberately NOT a bar: it does not refuse a spawn, it just drops those capabilities from
52
+ * the child's grant, so the spawn does proceed and the question is real.
53
+ */
54
+ export declare function shouldSeekApproval(result: ResolveResult | undefined): boolean;
55
+ /**
56
+ * Scopes a given call site may offer. `always` requires a **human-authored subject**.
57
+ *
58
+ * ADR-0019. The rule is unchanged; which paths satisfy it is not. `"interceptor"` used to be the only one
59
+ * and ADR-0016 deleted it, leaving `always` offerable from nowhere — 220 lines of persistence no live path
60
+ * could write to. `"definition"` is `delegate({agent})`, where ADR-0017 makes the subject an
61
+ * operator-authored file the session must hold `agent:<name>` to name at all. `"delegate"` is the `tools:`
62
+ * form, where the original reasoning stands untouched: the only things naming that child are the task and
63
+ * the tool list, both model-chosen, and a key the model controls is not a key.
64
+ */
65
+ export declare function offeredScopes(path: ApprovalPath): ApprovalScope[];
66
+ /**
67
+ * The approval an inheriting child may hold.
68
+ *
69
+ * Intersecting with the child's grant is what keeps ADR-0008 intact once approvals became inheritable:
70
+ * `approved ⊆ grant` at every level, by construction. An approval can therefore never name a capability
71
+ * the session does not hold — it only ever unblocks part of a grant, never widens one.
72
+ *
73
+ * The wildcard is filtered for the same reason `childEnv` filters it out of grants (R-26): inheriting it
74
+ * would let a descendant treat every future gate as pre-approved.
75
+ */
76
+ /** An approval as it crosses a boundary: the capability, WHO it was for, and HOW LONG it was meant to last. */
77
+ export interface InheritableApproval {
78
+ capability: Capability;
79
+ /** The agent type, or `<delegate>`. Carried because an approval is for a subject, not for a word. */
80
+ subject: string;
81
+ scope: ApprovalScope;
82
+ /**
83
+ * The definition's body digest at the time of approval (ADR-0022). Absent for `<delegate>`, which names
84
+ * no file to hash — and, per ADR-0019, is never persisted or offered `always` for the same reason.
85
+ */
86
+ bodySha256?: string;
87
+ }
88
+ /**
89
+ * What a child may inherit, as `capability@subject` keys.
90
+ *
91
+ * ADR-0014 changed this in two ways, and both were cases of a human's explicit choice being discarded
92
+ * one hop down:
93
+ *
94
+ * - **`once` is dropped** (A-S1). The scope chosen was not carried, so a `once` approval was written
95
+ * into the child's `PI_GRANTS_APPROVED` and republished onward — the most conservative answer a human
96
+ * can give produced the least conservative outcome, across an entire descendant subtree.
97
+ * - **The subject is kept** (A-S6). Bare capabilities were published, so a `<delegate>`-subject approval
98
+ * matched *any* subject below. `approvalKey`'s own doc argues at length that a model-controlled name
99
+ * is not a key; erasing the subject made that argument moot exactly where it mattered.
100
+ *
101
+ * The clamp to `grant` is unchanged and still load-bearing: **approval cannot conjure a capability**, so
102
+ * an inherited yes is only ever honoured for something the child independently holds.
103
+ */
104
+ export declare function inheritApprovals(approved: InheritableApproval[], grant: Capability[]): string[];
105
+ /**
106
+ * Read the inherited set back on the child side.
107
+ *
108
+ * Anything that is not a well-formed `capability@subject` pair is dropped rather than guessed at. An
109
+ * unparseable entry granting nothing is a missing prompt; an unparseable entry granting *something* is a
110
+ * silent escalation, so the direction of the failure is not a matter of taste.
111
+ */
112
+ export declare function parseInherited(raw: string | undefined): Map<string, string | undefined>;
113
+ /**
114
+ * Drop inherited approvals that were given for different instructions (ADR-0022).
115
+ *
116
+ * The hole this closes: `resolveApprovals` checks `inherited` FIRST and none of it had ever been through
117
+ * `entryVerdict`, so ADR-0019's headline property — an approval is void once the instructions change — held
118
+ * on the one path that persists and neither of the two that do not. A child is a fresh process: it re-reads
119
+ * the definition from disk, so a `git pull` between the parent's approval and the child's spawn meant the
120
+ * child ran a rewritten body under a yes given about the old one, recording `approvalSource: "inherited"`.
121
+ *
122
+ * An entry with **no** digest is honoured, and that is not a hole: `<delegate>` legitimately has none, and
123
+ * neither does a pre-0.11 parent. It is the same trade `entryVerdict` refuses to make for a *persisted*
124
+ * entry — but that entry is 30 days old and read off disk, whereas this one was handed over by a live parent
125
+ * process in the same tree, which is a much shorter chain to trust.
126
+ */
127
+ export declare function verifyInherited(parsed: ReadonlyMap<string, string | undefined>, snapshotOf: (subject: string) => SubjectSnapshot | null): Set<string>;
128
+ /** When an approval granted now stops being valid. Computed once at write time and stored, so an entry's
129
+ * lifetime is visible in the file rather than implied by whichever version of the code reads it. */
130
+ export declare function expiryFor(approvedAt: Date): string;
131
+ /** A persisted approval, keyed externally by subject and capability. Only the `"definition"` path writes one. */
132
+ export interface ApprovalEntry {
133
+ approvedAt: string;
134
+ expiresAt: string;
135
+ /** The directory the human was sitting in. See `entryVerdict` and R-27. */
136
+ cwd: string;
137
+ /** The agent type's ceiling AT APPROVAL TIME. Load-bearing, not decorative — see `entryVerdict`. */
138
+ grantAtApproval: Capability[];
139
+ /**
140
+ * The definition's body digest AT APPROVAL TIME (ADR-0019, using ADR-0018's hash).
141
+ *
142
+ * `grantAtApproval` pins the tools; this pins the INSTRUCTIONS. Without it an `always` approval survives
143
+ * a total rewrite of what the child is told to do, because `ceilingForDefinition` reads only
144
+ * `allowed-tools` — R-35's hazard reappearing inside the persistence layer. Absent means the entry
145
+ * predates 0.10.0 and cannot be verified, which `entryVerdict` treats as changed: fail closed.
146
+ */
147
+ bodyAtApproval?: string;
148
+ }
149
+ export type EntryVerdict = "valid" | "expired" | "foreign-cwd" | "type-changed" | "instructions-changed" | "type-missing";
150
+ /**
151
+ * What a subject looks like RIGHT NOW — one lookup, not two (ADR-0019).
152
+ *
153
+ * Deliberately a single snapshot rather than parallel `ceilingOf` / `digestOf` callbacks. R-28 was one
154
+ * call site supplying one argument and omitting another; a shape that cannot be half-supplied is the
155
+ * structural form of remembering.
156
+ */
157
+ export interface SubjectSnapshot {
158
+ ceiling: Capability[];
159
+ /** SHA-256 of the definition body — `digestDefinition(...).sha256`. */
160
+ bodySha256: string;
161
+ }
162
+ export interface EntryValidityInput {
163
+ entry: ApprovalEntry;
164
+ /** The directory this session is running in. */
165
+ cwd: string;
166
+ now: Date;
167
+ /** The subject's CURRENT ceiling and body digest, or null when it no longer exists. */
168
+ current: SubjectSnapshot | null;
169
+ }
170
+ /**
171
+ * Decide whether a persisted approval still means what the human meant.
172
+ *
173
+ * Four ways it can stop meaning that, and each is reported distinctly so `/grants approvals` can explain
174
+ * itself rather than silently showing fewer rows:
175
+ *
176
+ * - `foreign-cwd` — the file was copied or committed and opened somewhere else. Nobody in THIS checkout
177
+ * was asked, so it authorises nothing here (R-27).
178
+ * - `expired` — a gate opened during one project must not still be open next quarter.
179
+ * - `type-changed` — the confused deputy. The key names a file whose contents can change after approval:
180
+ * approve `tool:write@docs-writer` when it declares `read, write`, and later that file
181
+ * gains `bash`. The entry would still match the key while describing something the
182
+ * human never saw.
183
+ * - `instructions-changed` — the same confused deputy, one level deeper. The tools are untouched but the
184
+ * BODY was rewritten, so the child would now be told to do something the human never
185
+ * saw. Only checkable since ADR-0018 gave the body a digest; an entry carrying no body
186
+ * pin lands here too, because unverifiable is not the same as unchanged.
187
+ * - `type-missing` — the type was deleted or renamed; a new file could later claim the same name.
188
+ */
189
+ export declare function entryVerdict(input: EntryValidityInput): EntryVerdict;
190
+ export interface ResolveApprovalsInput {
191
+ /** `ResolveResult.gatedBlocked` — held and within ceiling, but awaiting a human. */
192
+ gated: Capability[];
193
+ subject: string;
194
+ /** Approval KEYS approved for this session, in memory only. */
195
+ sessionApprovals: ReadonlySet<string>;
196
+ /** Persisted entries by key, ALREADY validity-filtered by the store. */
197
+ persisted: ReadonlyMap<string, ApprovalEntry>;
198
+ /** Capabilities approved further up the tree and inherited with the grant. */
199
+ /**
200
+ * `capability@subject` keys inherited from the delegator (ADR-0014).
201
+ *
202
+ * Was `Capability[]` — bare names that matched any subject, and included `once` approvals that were
203
+ * never meant to leave the level they were given at.
204
+ */
205
+ inherited?: Set<string>;
206
+ }
207
+ export interface ResolveApprovalsResult {
208
+ approved: Capability[];
209
+ /** Gated capabilities still requiring a live human. */
210
+ needsPrompt: Capability[];
211
+ sources: Record<Capability, ApprovalSource>;
212
+ }
213
+ /**
214
+ * Satisfy as much of `gated` as possible without asking anyone.
215
+ *
216
+ * Precedence is inherited -> session -> persisted -> prompt. Order matters only for what gets REPORTED
217
+ * (the ledger's `approvalSource`); any hit satisfies equally. Checking all three before prompting is what
218
+ * stops an orchestrator's tenth delegation from raising a tenth identical dialog.
219
+ */
220
+ export declare function resolveApprovals(input: ResolveApprovalsInput): ResolveApprovalsResult;
221
+ //# sourceMappingURL=approval.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval.d.ts","sourceRoot":"","sources":["../src/approval.ts"],"names":[],"mappings":"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"}