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,7 @@
1
+ export { resolve, assertNarrowing, toPiToolsAllowlist, UNIVERSAL_CAPABILITIES, type Capability, type ResolveInput, type ResolveResult, } from "./resolve.ts";
2
+ export { appendRecord, buildRecord, isEscalationAttempt, type GrantRecord, type LedgerOptions, } from "./ledger.ts";
3
+ export { planSpawn, type SpawnPlan, type SpawnPlanInput } from "./spawn.ts";
4
+ export { APPROVAL_TTL_DAYS, DELEGATE_SUBJECT, approvalKey, entryVerdict, expiryFor, inheritApprovals, offeredScopes, resolveApprovals, shouldSeekApproval, type ApprovalEntry, type ApprovalPath, type ApprovalScope, type ApprovalSource, type EntryVerdict, type SubjectSnapshot, } from "./approval.ts";
5
+ export { approvalsPath, loadApprovals, revokeAll, revokeApproval, saveApproval, type DroppedApproval, type SubjectLookup, } from "./approval-store.ts";
6
+ export { createApprovalGate, createApprovalGateProvider, timeoutMsFromEnv, type ApprovalGate, type InFlightApprovals, type ApprovalUI, type PromptOutcome, type PromptOutcomeKind, type PromptRequest, } from "./approval-prompt.ts";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,eAAe,EACf,kBAAkB,EAClB,sBAAsB,EACtB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,aAAa,GACnB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,YAAY,EACZ,WAAW,EACX,mBAAmB,EACnB,KAAK,WAAW,EAChB,KAAK,aAAa,GACnB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5E,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,WAAW,EACX,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,aAAa,EACb,gBAAgB,EAChB,kBAAkB,EAClB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,eAAe,GACrB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,aAAa,EACb,aAAa,EACb,SAAS,EACT,cAAc,EACd,YAAY,EACZ,KAAK,eAAe,EACpB,KAAK,aAAa,GACnB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,kBAAkB,EAClB,0BAA0B,EAC1B,gBAAgB,EAChB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,aAAa,GACnB,MAAM,sBAAsB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { resolve, assertNarrowing, toPiToolsAllowlist, UNIVERSAL_CAPABILITIES, } from "./resolve.js";
2
+ export { appendRecord, buildRecord, isEscalationAttempt, } from "./ledger.js";
3
+ export { planSpawn } from "./spawn.js";
4
+ export { APPROVAL_TTL_DAYS, DELEGATE_SUBJECT, approvalKey, entryVerdict, expiryFor, inheritApprovals, offeredScopes, resolveApprovals, shouldSeekApproval, } from "./approval.js";
5
+ export { approvalsPath, loadApprovals, revokeAll, revokeApproval, saveApproval, } from "./approval-store.js";
6
+ export { createApprovalGate, createApprovalGateProvider, timeoutMsFromEnv, } from "./approval-prompt.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,eAAe,EACf,kBAAkB,EAClB,sBAAsB,GAIvB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,YAAY,EACZ,WAAW,EACX,mBAAmB,GAGpB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,SAAS,EAAuC,MAAM,YAAY,CAAC;AAE5E,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,WAAW,EACX,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,aAAa,EACb,gBAAgB,EAChB,kBAAkB,GAOnB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,aAAa,EACb,aAAa,EACb,SAAS,EACT,cAAc,EACd,YAAY,GAGb,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,kBAAkB,EAClB,0BAA0B,EAC1B,gBAAgB,GAOjB,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Reading a ledger back — integrity, instruction versions, and the ADR-0020 approval tally.
3
+ *
4
+ * Split from `ledger.ts` when the file-size guard refused it at 419 lines. The seam is real rather than
5
+ * arbitrary: `ledger.ts` **writes** one record at a time and fails closed when it cannot, while everything
6
+ * here **reads** a whole file and must never fail at all — a report is a diagnostic, and a diagnostic that
7
+ * throws on damaged input is useless on exactly the input it exists for. Every defect this half has had
8
+ * (R-63's bias, R-64's three malformed shapes) was a *reporting* defect, and none of them could have
9
+ * touched the write path.
10
+ *
11
+ * Re-exported from `./ledger` so the published subpath is unchanged.
12
+ */
13
+ import type { ApprovalSource } from "./approval.ts";
14
+ export interface LedgerReport {
15
+ /** False when the file is absent — a configuration state, not damage. */
16
+ exists: boolean;
17
+ /** Lines that parsed as records. */
18
+ records: number;
19
+ /** Lines that did not, with 1-based line numbers so the report is actionable. */
20
+ corrupt: Array<{
21
+ line: number;
22
+ text: string;
23
+ }>;
24
+ /** Records where an agent asked for more than it held — ADR-0008's designated signal. */
25
+ escalationAttempts: number;
26
+ /**
27
+ * Every distinct set of instructions this ledger saw run, with how many spawns used it (R-51).
28
+ *
29
+ * ADR-0018 advertises that a record answers *"did these four children run the same instructions?"* and
30
+ * *"has this definition changed since?"* — and until this existed **nothing read `definitionDigest` at
31
+ * all**, so both questions required hand-written `jq` and the second was not even reproducible with
32
+ * `sha256sum`, because the digest covers the body and not the frontmatter. A field no tool reads is a
33
+ * field that quietly becomes decoration.
34
+ *
35
+ * Grouped by `name` + `sha256`, so two entries with one name are exactly the evidence that a definition
36
+ * changed mid-ledger. Sorted by name then digest so two runs of the same fan-out produce a diffable
37
+ * report, like the ids themselves.
38
+ */
39
+ definitions: Array<{
40
+ name: string;
41
+ source: string;
42
+ sha256: string;
43
+ spawns: number;
44
+ }>;
45
+ /**
46
+ * Where the yes came from, per approved capability, tallied across the whole ledger.
47
+ *
48
+ * **This is the measurement ADR-0020 asks for.** That ADR keeps the persistence layer on R-25's fatigue
49
+ * argument with *no number behind it*, and named the evidence that would settle it: counting `persisted`
50
+ * against `prompt` over a few weeks of real use. It also said this "needs no new machinery" — true of the
51
+ * data and false of the answer, which required hand-written `jq`. Same shape as R-51: a field no tool
52
+ * reads becomes decoration, and a measurement nobody can run does not get run.
53
+ *
54
+ * **`bySource` counts RECORDS and is an upper bound, not an answer.** Deleting the persistence layer does
55
+ * not turn every `persisted` record back into a prompt: precedence is `inherited → session → persisted →
56
+ * prompt`, and `session` approvals live in memory and do not depend on the store at all. So a session that
57
+ * spawns `deploy` twenty times under one persisted entry writes twenty `persisted` records, while without
58
+ * the store it would raise **one** prompt and satisfy the other nineteen from the session cache. Reporting
59
+ * twenty prompts avoided would overstate the layer's value twentyfold, on the one number that decides
60
+ * whether to keep it — the same direction of bias `unattributed` exists to avoid, arrived at a different way.
61
+ *
62
+ * `distinctBySource` is the closer estimate: distinct `capability@subject` pairs, which bounds the cost of
63
+ * deletion at one prompt per pair per session. The ledger carries no session id, so the exact figure is not
64
+ * computable from it; both numbers are printed and labelled rather than one being presented as the truth.
65
+ *
66
+ * Counted from `approvalSources` **only**. `approvalSource` is deliberately not used as a fallback: before
67
+ * 0.11.1 that scalar was written for the whole set even when the sources differed (R-46), so folding it in
68
+ * would report humans as having been asked about capabilities they were never asked about — biasing the
69
+ * one direction this measurement must not be biased in. Those records are counted as `unattributed`
70
+ * instead, so the sample size is visible rather than silently smaller.
71
+ */
72
+ approvals: {
73
+ /** Raw record counts. An UPPER bound on prompts avoided — see above before quoting one. */
74
+ bySource: Record<ApprovalSource, number>;
75
+ /** Distinct `capability@subject` pairs per source. The closer estimate. */
76
+ distinctBySource: Record<ApprovalSource, number>;
77
+ /** Records carrying approvals from before per-capability sources existed. Not attributable; see above. */
78
+ unattributed: number;
79
+ /** Records where a human was asked and said no — the fatigue argument's other half. */
80
+ humanDenied: number;
81
+ /**
82
+ * Distinct `capability@subject` pairs a human declined.
83
+ *
84
+ * Same reason `distinctBySource` exists: R-29 shares a decline across every concurrent caller, so one
85
+ * click of *Deny* under an eight-wide fan-out writes eight `humanDenied` records. Reporting the raw
86
+ * count as "times a human declined" is the per-record bias R-63 removed from `persisted`, left in place
87
+ * on the number that argues hardest FOR the layer — which is the direction that flatters this package's
88
+ * own gating and therefore the one to be most careful with.
89
+ */
90
+ humanDeniedPairs: number;
91
+ };
92
+ ok: boolean;
93
+ }
94
+ /**
95
+ * Read the ledger back and report what is wrong with it.
96
+ *
97
+ * **This is the gap that mattered most.** `appendRecord`'s strict mode catches write *errors*, never
98
+ * corruption, and nothing in this package had ever read a ledger back — so a torn line was silently
99
+ * indistinguishable from a spawn that never happened. An audit trail whose damage is invisible is not a
100
+ * compensating control, and ADR-0008 leans on the ledger as exactly that.
101
+ *
102
+ * Deliberately reports rather than repairs. A corrupt line is evidence; rewriting the file to make it parse
103
+ * would destroy the one artifact an investigation has.
104
+ */
105
+ export declare function verifyLedger(path: string): Promise<LedgerReport>;
106
+ //# sourceMappingURL=ledger-report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger-report.d.ts","sourceRoot":"","sources":["../src/ledger-report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAIpD,MAAM,WAAW,YAAY;IAC3B,yEAAyE;IACzE,MAAM,EAAE,OAAO,CAAC;IAChB,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,yFAAyF;IACzF,kBAAkB,EAAE,MAAM,CAAC;IAC3B;;;;;;;;;;;;OAYG;IACH,WAAW,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrF;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,SAAS,EAAE;QACT,2FAA2F;QAC3F,QAAQ,EAAE,MAAM,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;QACzC,2EAA2E;QAC3E,gBAAgB,EAAE,MAAM,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;QACjD,0GAA0G;QAC1G,YAAY,EAAE,MAAM,CAAC;QACrB,uFAAuF;QACvF,WAAW,EAAE,MAAM,CAAC;QACpB;;;;;;;;WAQG;QACH,gBAAgB,EAAE,MAAM,CAAC;KAC1B,CAAC;IACF,EAAE,EAAE,OAAO,CAAC;CACb;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CA0HtE"}
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Reading a ledger back — integrity, instruction versions, and the ADR-0020 approval tally.
3
+ *
4
+ * Split from `ledger.ts` when the file-size guard refused it at 419 lines. The seam is real rather than
5
+ * arbitrary: `ledger.ts` **writes** one record at a time and fails closed when it cannot, while everything
6
+ * here **reads** a whole file and must never fail at all — a report is a diagnostic, and a diagnostic that
7
+ * throws on damaged input is useless on exactly the input it exists for. Every defect this half has had
8
+ * (R-63's bias, R-64's three malformed shapes) was a *reporting* defect, and none of them could have
9
+ * touched the write path.
10
+ *
11
+ * Re-exported from `./ledger` so the published subpath is unchanged.
12
+ */
13
+ import { readFile } from "node:fs/promises";
14
+ import { DELEGATE_SUBJECT } from "./approval.js";
15
+ import { isEscalationAttempt } from "./ledger.js";
16
+ /**
17
+ * Read the ledger back and report what is wrong with it.
18
+ *
19
+ * **This is the gap that mattered most.** `appendRecord`'s strict mode catches write *errors*, never
20
+ * corruption, and nothing in this package had ever read a ledger back — so a torn line was silently
21
+ * indistinguishable from a spawn that never happened. An audit trail whose damage is invisible is not a
22
+ * compensating control, and ADR-0008 leans on the ledger as exactly that.
23
+ *
24
+ * Deliberately reports rather than repairs. A corrupt line is evidence; rewriting the file to make it parse
25
+ * would destroy the one artifact an investigation has.
26
+ */
27
+ export async function verifyLedger(path) {
28
+ let text;
29
+ try {
30
+ text = await readFile(path, "utf8");
31
+ }
32
+ catch (error) {
33
+ if (error.code === "ENOENT") {
34
+ return {
35
+ exists: false,
36
+ records: 0,
37
+ corrupt: [],
38
+ escalationAttempts: 0,
39
+ definitions: [],
40
+ approvals: {
41
+ bySource: { prompt: 0, session: 0, persisted: 0, inherited: 0 },
42
+ distinctBySource: { prompt: 0, session: 0, persisted: 0, inherited: 0 },
43
+ unattributed: 0,
44
+ humanDenied: 0,
45
+ humanDeniedPairs: 0,
46
+ },
47
+ ok: true,
48
+ };
49
+ }
50
+ throw error;
51
+ }
52
+ const corrupt = [];
53
+ // Keyed by name+digest: one name with two entries is the signal that the definition changed mid-ledger.
54
+ const digests = new Map();
55
+ let records = 0;
56
+ let escalationAttempts = 0;
57
+ const bySource = { prompt: 0, session: 0, persisted: 0, inherited: 0 };
58
+ // `capability@subject` seen per source, so the report can state a bound as well as a raw count.
59
+ const distinct = {
60
+ prompt: new Set(),
61
+ session: new Set(),
62
+ persisted: new Set(),
63
+ inherited: new Set(),
64
+ };
65
+ let unattributed = 0;
66
+ let humanDenied = 0;
67
+ const deniedPairs = new Set();
68
+ const lines = text.split("\n");
69
+ lines.forEach((raw, index) => {
70
+ // A trailing newline yields one empty final element, which is normal rather than damage.
71
+ if (raw.trim().length === 0)
72
+ return;
73
+ try {
74
+ const parsed = JSON.parse(raw);
75
+ if (!Array.isArray(parsed.denied))
76
+ throw new Error("not a grant record");
77
+ records += 1;
78
+ if (isEscalationAttempt(parsed))
79
+ escalationAttempts += 1;
80
+ if (parsed.humanDenied) {
81
+ humanDenied += 1;
82
+ const subject = parsed.agentType === undefined || parsed.agentType === "delegate" ? DELEGATE_SUBJECT : parsed.agentType;
83
+ for (const capability of parsed.gatedBlocked ?? [])
84
+ deniedPairs.add(`${capability}@${subject}`);
85
+ }
86
+ // A **plain, non-empty** object. Three shapes were accepted here that must not be, all of them
87
+ // reachable from a torn, hand-edited or foreign line — which is the input class `verifyLedger` exists
88
+ // for, so "this package never writes that" is not a defence:
89
+ // - `{}` beside a non-empty `approved` counted NOWHERE, silently shrinking the sample the comment
90
+ // below promises to keep visible;
91
+ // - an ARRAY passed `typeof === "object"` and was tallied with numeric indices as capability names;
92
+ // - `null` is an object.
93
+ const rawSources = parsed.approvalSources;
94
+ const sources = rawSources && typeof rawSources === "object" && !Array.isArray(rawSources) && Object.keys(rawSources).length > 0
95
+ ? rawSources
96
+ : undefined;
97
+ if (sources) {
98
+ // The subject half of `capability@subject`. `agentType` is the definition's name, or the literal
99
+ // `"delegate"` for the `tools:` form — which is NOT `DELEGATE_SUBJECT`, the `<delegate>` the approval
100
+ // layer keys on. Mapped here rather than at the write site so old ledgers read correctly too.
101
+ // **Stated limit:** a definition genuinely named `delegate` is indistinguishable from the `tools:`
102
+ // form in this field, and their two distinct approvals count as one pair. `DELEGATE_SUBJECT`'s own
103
+ // angle brackets exist to make that collision impossible, and the ledger drops them.
104
+ const subject = parsed.agentType === undefined || parsed.agentType === "delegate" ? DELEGATE_SUBJECT : parsed.agentType;
105
+ for (const [capability, source] of Object.entries(sources)) {
106
+ // `Object.hasOwn`, never `in`: `in` walks the prototype, so a source of `"toString"` or
107
+ // `"valueOf"` passed the check, wrote a STRING into a counter, made `attributed` a string, and
108
+ // deleted the entire measurement from the report while marking an intact ledger corrupt.
109
+ // An unrecognised source is counted as unattributed rather than dropped: a tally that silently
110
+ // ignores what it does not understand reports a smaller sample as a cleaner one.
111
+ if (Object.hasOwn(bySource, source)) {
112
+ bySource[source] += 1;
113
+ distinct[source].add(`${capability}@${subject}`);
114
+ }
115
+ else
116
+ unattributed += 1;
117
+ }
118
+ }
119
+ else if (parsed.approved && parsed.approved.length > 0) {
120
+ unattributed += parsed.approved.length;
121
+ }
122
+ const d = parsed.definitionDigest;
123
+ if (d?.name && d.sha256) {
124
+ const key = `${d.name}\u0000${d.sha256}`;
125
+ const seen = digests.get(key);
126
+ if (seen)
127
+ seen.spawns += 1;
128
+ else
129
+ digests.set(key, { name: d.name, source: d.source, sha256: d.sha256, spawns: 1 });
130
+ }
131
+ }
132
+ catch {
133
+ corrupt.push({ line: index + 1, text: raw.slice(0, 120) });
134
+ }
135
+ });
136
+ return {
137
+ exists: true,
138
+ records,
139
+ corrupt,
140
+ escalationAttempts,
141
+ definitions: [...digests.values()].sort((a, b) => a.name.localeCompare(b.name) || a.sha256.localeCompare(b.sha256)),
142
+ approvals: {
143
+ bySource,
144
+ distinctBySource: {
145
+ prompt: distinct.prompt.size,
146
+ session: distinct.session.size,
147
+ persisted: distinct.persisted.size,
148
+ inherited: distinct.inherited.size,
149
+ },
150
+ unattributed,
151
+ humanDenied,
152
+ humanDeniedPairs: deniedPairs.size,
153
+ },
154
+ ok: corrupt.length === 0,
155
+ };
156
+ }
157
+ //# sourceMappingURL=ledger-report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger-report.js","sourceRoot":"","sources":["../src/ledger-report.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Append-only grant ledger.
3
+ *
4
+ * Exists because pi-fabric's persisted execution trace records `args: {}` — it captures *that* a child
5
+ * ran, not *what it was authorised to do* (docs/probes/pi-fabric-eval probe 5). Without this record
6
+ * you cannot answer "what was this sub-agent permitted to do?" after the fact, which is the whole
7
+ * point of a governance layer.
8
+ *
9
+ * `denied` is the field that earns the file: an agent repeatedly requesting capabilities it does not
10
+ * hold is an escalation attempt, and it is invisible without a record.
11
+ *
12
+ * PRIVACY: capability ids, counts, and identifiers only. Never prompts, tool arguments, or results.
13
+ *
14
+ * ADR-0018 makes the boundary explicit rather than leaving it to be inferred, because a record now carries
15
+ * something about the child's instructions. **`definitionDigest` is an identifier**: a SHA-256 of an
16
+ * operator-authored file already committed to a repository, which names a version without reproducing it.
17
+ * **The task is not recorded, anywhere, ever** — it is assembled by the model from the parent's context and
18
+ * can carry anything the parent could see, so a ledger holding it would be a secrets sink. That half of
19
+ * "what was this child told to do?" is out of the ledger by decision, not by omission.
20
+ */
21
+ import type { Capability, ResolveResult } from "./resolve.ts";
22
+ import type { DefinitionDigest } from "./definitions.ts";
23
+ import type { ApprovalScope, ApprovalSource } from "./approval.ts";
24
+ import type { PromptOutcomeKind } from "./approval-prompt.ts";
25
+ export interface GrantRecord {
26
+ ts: string;
27
+ parentId: string;
28
+ childId: string;
29
+ depth: number;
30
+ agentType?: string;
31
+ requested: Capability[];
32
+ parentGrant: Capability[];
33
+ effective: Capability[];
34
+ denied: Capability[];
35
+ clipped: Capability[];
36
+ gatedBlocked: Capability[];
37
+ /** True when the spawn was refused outright rather than narrowed. */
38
+ blocked: boolean;
39
+ /** Present when the spawn was refused, or when resolution failed. */
40
+ reason?: string;
41
+ /**
42
+ * Gated capabilities satisfied for this spawn.
43
+ *
44
+ * The ledger used to have ONE flavour of no. It now has three, and they call for different responses:
45
+ * - `denied` non-empty -> an agent asked for more than it holds. ESCALATION ATTEMPT.
46
+ * - `humanDenied` -> a person was asked and said no. WORKING AS DESIGNED.
47
+ * - `gatedBlocked` with no source -> nobody was there to ask. A background run hit a gate; the fix is
48
+ * an operator pre-approving it, not an incident.
49
+ */
50
+ approved?: Capability[];
51
+ approvalSource?: ApprovalSource;
52
+ /**
53
+ * WHERE each approved capability's yes came from — one entry per capability (R-46).
54
+ *
55
+ * `approvalSource` above is a single scalar and was written for a set: gate `tool:bash` and `tool:write`,
56
+ * let a persisted entry cover `bash` while a human clicks *Allow once* for `write`, and the record read
57
+ * `approved: ["tool:bash","tool:write"], approvalSource: "prompt"` — **asserting a human was asked about
58
+ * `tool:bash`, which they were not.** The ledger's whole job is answering "did a human authorise this?",
59
+ * so over-claiming in that direction is the worst available failure.
60
+ *
61
+ * The scalar is kept and is now written **only when every approved capability shares one source**, so a
62
+ * reader of old and new lines alike can trust it; when sources differ it is omitted and this map carries
63
+ * the truth. Two fields, one of which is a safe summary of the other — not two competing answers.
64
+ */
65
+ approvalSources?: Record<Capability, ApprovalSource>;
66
+ /**
67
+ * How far each prompted capability's yes reaches (F5). Same shape and same reason as `approvalSources`:
68
+ * `approvalScope` below is a **derived summary**, emitted only when every prompted capability shares one
69
+ * scope. This field decides propagation — `inheritApprovals` drops `once` — so a scalar that described
70
+ * one capability while claiming to describe the set was not merely a reporting defect.
71
+ */
72
+ approvalScopes?: Record<Capability, ApprovalScope>;
73
+ /** Present only when the source was a live prompt, and only when one scope covers the whole set. */
74
+ approvalScope?: ApprovalScope;
75
+ /** A human was asked and declined. Distinct from `denied`, which is an escalation attempt. */
76
+ humanDenied?: boolean;
77
+ /**
78
+ * WHY a gate went unsatisfied, when the answer was not a yes.
79
+ *
80
+ * `PromptOutcomeKind` has five members and this record kept exactly one of them (`humanDenied`, from
81
+ * `declined`). So `no-ui`, `dismissed` (a timeout or an abort) and `error` produced **identical** records
82
+ * — `gatedBlocked` non-empty, no `approvalSource`, `blocked: true` — and the only thing separating them
83
+ * was free-text `reason`, written for a human at the call site. Given a failed run, *"was there an
84
+ * operator who timed out, or was there nobody to ask?"* was not answerable from any field, and the fix
85
+ * for each is different: one is a queue or a longer `PI_GRANTS_APPROVAL_TIMEOUT`, the other is an
86
+ * operator pre-approving.
87
+ *
88
+ * The discriminant was already computed and thrown away. ADR-0026 leans on this vocabulary being able to
89
+ * say *"nobody was there to ask"* and be believed, so it is recorded rather than inferred.
90
+ *
91
+ * **Privacy is unchanged**: this is a fixed five-member enum, not text — nothing model-authored, nothing
92
+ * a task could carry.
93
+ */
94
+ gateOutcome?: PromptOutcomeKind;
95
+ /**
96
+ * WHICH operator-authored instructions this child was given (ADR-0018).
97
+ *
98
+ * Identifies, never reproduces: matching digests prove two children ran the same text, and a digest that
99
+ * no longer matches the file proves the definition changed since. **It says nothing about whether those
100
+ * instructions were correct or whether the child obeyed them** — it identifies text, it does not evaluate
101
+ * it. Absent for a `tools:`-style delegation, which has no definition.
102
+ */
103
+ definitionDigest?: DefinitionDigest;
104
+ }
105
+ export interface LedgerOptions {
106
+ /** Path to the JSONL file. Parent directories are created on demand. */
107
+ path: string;
108
+ /**
109
+ * When true, a ledger write failure throws instead of being swallowed.
110
+ *
111
+ * Default is `true` and that is deliberate: for a security control, an unrecorded grant should fail
112
+ * closed. Set false only where the ledger is advisory.
113
+ */
114
+ strict?: boolean;
115
+ }
116
+ export declare function buildRecord(args: {
117
+ parentId: string;
118
+ childId: string;
119
+ depth: number;
120
+ agentType?: string;
121
+ requested: Capability[];
122
+ parentGrant: Capability[];
123
+ result: ResolveResult;
124
+ blocked: boolean;
125
+ reason?: string;
126
+ approved?: Capability[];
127
+ approvalSources?: Record<Capability, ApprovalSource>;
128
+ approvalScopes?: Record<Capability, ApprovalScope>;
129
+ humanDenied?: boolean;
130
+ gateOutcome?: PromptOutcomeKind;
131
+ definitionDigest?: DefinitionDigest;
132
+ now: Date;
133
+ }): GrantRecord;
134
+ export { LOCK_TIMEOUT_MS, STALE_LOCK_MS } from "./file-lock.ts";
135
+ export { verifyLedger, type LedgerReport } from "./ledger-report.ts";
136
+ export declare function appendRecord(options: LedgerOptions, record: GrantRecord): Promise<void>;
137
+ /** True when this record shows an agent asking for more than it holds. */
138
+ export declare function isEscalationAttempt(record: GrantRecord): boolean;
139
+ //# sourceMappingURL=ledger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"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"}
package/dist/ledger.js ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Append-only grant ledger.
3
+ *
4
+ * Exists because pi-fabric's persisted execution trace records `args: {}` — it captures *that* a child
5
+ * ran, not *what it was authorised to do* (docs/probes/pi-fabric-eval probe 5). Without this record
6
+ * you cannot answer "what was this sub-agent permitted to do?" after the fact, which is the whole
7
+ * point of a governance layer.
8
+ *
9
+ * `denied` is the field that earns the file: an agent repeatedly requesting capabilities it does not
10
+ * hold is an escalation attempt, and it is invisible without a record.
11
+ *
12
+ * PRIVACY: capability ids, counts, and identifiers only. Never prompts, tool arguments, or results.
13
+ *
14
+ * ADR-0018 makes the boundary explicit rather than leaving it to be inferred, because a record now carries
15
+ * something about the child's instructions. **`definitionDigest` is an identifier**: a SHA-256 of an
16
+ * operator-authored file already committed to a repository, which names a version without reproducing it.
17
+ * **The task is not recorded, anywhere, ever** — it is assembled by the model from the parent's context and
18
+ * can carry anything the parent could see, so a ledger holding it would be a secrets sink. That half of
19
+ * "what was this child told to do?" is out of the ledger by decision, not by omission.
20
+ */
21
+ import { appendFile, mkdir, readFile } from "node:fs/promises";
22
+ import { withFileLock } from "./file-lock.js";
23
+ import { dirname } from "node:path";
24
+ import { DELEGATE_SUBJECT } from "./approval.js";
25
+ export function buildRecord(args) {
26
+ // R-46: the scalar is a SUMMARY, emitted only when it cannot mislead. `buildRecord` derives it rather
27
+ // than accepting it, so a call site cannot supply one that disagrees with the map beside it.
28
+ const sources = args.approvalSources ?? {};
29
+ const distinct = [...new Set(Object.values(sources))];
30
+ const scopes = args.approvalScopes ?? {};
31
+ const distinctScopes = [...new Set(Object.values(scopes))];
32
+ return {
33
+ ts: args.now.toISOString(),
34
+ parentId: args.parentId,
35
+ childId: args.childId,
36
+ depth: args.depth,
37
+ agentType: args.agentType,
38
+ requested: args.requested,
39
+ parentGrant: args.parentGrant,
40
+ effective: args.result.effective,
41
+ denied: args.result.denied,
42
+ clipped: args.result.clipped,
43
+ gatedBlocked: args.result.gatedBlocked,
44
+ blocked: args.blocked,
45
+ reason: args.reason,
46
+ ...(args.approved && args.approved.length > 0 ? { approved: args.approved } : {}),
47
+ ...(distinct.length === 1 ? { approvalSource: distinct[0] } : {}),
48
+ ...(Object.keys(sources).length > 0 ? { approvalSources: sources } : {}),
49
+ ...(distinctScopes.length === 1 ? { approvalScope: distinctScopes[0] } : {}),
50
+ ...(Object.keys(scopes).length > 0 ? { approvalScopes: scopes } : {}),
51
+ ...(args.humanDenied ? { humanDenied: true } : {}),
52
+ // Written whenever a gate was reached and not satisfied by a yes. `granted` is omitted deliberately —
53
+ // an approved spawn already says so through `approvalSources`, and a field that appears on every record
54
+ // stops being a signal.
55
+ ...(args.gateOutcome && args.gateOutcome !== "granted" ? { gateOutcome: args.gateOutcome } : {}),
56
+ ...(args.definitionDigest ? { definitionDigest: args.definitionDigest } : {}),
57
+ };
58
+ }
59
+ // R-49: the lock moved to `src/file-lock.ts` so the approvals store could use the SAME one rather than
60
+ // grow a second copy. Re-exported because `./ledger` is a published subpath and these were part of it.
61
+ export { LOCK_TIMEOUT_MS, STALE_LOCK_MS } from "./file-lock.js";
62
+ // The reading half, split out under the file-size guard and re-exported so `./ledger` is unchanged for
63
+ // anyone importing it. See `ledger-report.ts` for why the seam is where it is.
64
+ export { verifyLedger } from "./ledger-report.js";
65
+ /**
66
+ * Serialise appends across processes.
67
+ *
68
+ * **Why the ledger needs it.** For most of this package's life cardinality was bounded to one by `delegate`
69
+ * being blocking, so there was never a second writer. Fan-out removes that: `ENV_LEDGER` propagates to
70
+ * children, so a subtree can have many processes appending to one file.
71
+ *
72
+ * A ledger write that cannot take the lock **fails the delegation closed** — see `appendRecord`'s `strict`
73
+ * — because a child running with granted capabilities and no audit line is what the ledger exists to
74
+ * prevent. That is the opposite of what the approvals store does with the same lock, and deliberately so.
75
+ */
76
+ const withLedgerLock = (path, write) => withFileLock(path, "grant ledger", write);
77
+ export async function appendRecord(options, record) {
78
+ const line = `${JSON.stringify(record)}\n`;
79
+ try {
80
+ await mkdir(dirname(options.path), { recursive: true });
81
+ // O_APPEND alone is not enough once several processes write to one ledger — see `withLedgerLock`.
82
+ await withLedgerLock(options.path, () => appendFile(options.path, line, { encoding: "utf8", flag: "a" }));
83
+ }
84
+ catch (error) {
85
+ if (options.strict ?? true) {
86
+ throw new Error(`grant ledger write failed (failing closed): ${String(error)}`);
87
+ }
88
+ }
89
+ }
90
+ /** True when this record shows an agent asking for more than it holds. */
91
+ export function isEscalationAttempt(record) {
92
+ return record.denied.length > 0;
93
+ }
94
+ //# sourceMappingURL=ledger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger.js","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Close herdr panes this process opened but never got to close.
3
+ *
4
+ * `runHerdrPane` closes its pane in a `finally`, which covers a thrown error and a timeout — **not the
5
+ * process being killed**. A pi session interrupted mid-fan-out left one pane per in-flight child, and
6
+ * `docs/probes/g16-herdr` records that an orphaned pane is not trivially closable afterwards.
7
+ *
8
+ * **Registered on `exit` only, deliberately — not on SIGINT or SIGTERM.** That is the part worth reading,
9
+ * because the obvious fix is the dangerous one. Adding a signal listener *suppresses Node's default
10
+ * termination*, so a library that adds one takes over an application-level decision it has no standing to
11
+ * make: pi uses SIGINT to interrupt a turn, and a listener here that re-raised would turn "cancel this
12
+ * delegation" into "exit pi". A governance package quietly changing the host's interrupt semantics is a
13
+ * worse defect than the leak it fixes, and it would land on **every** session rather than the opt-in ones.
14
+ *
15
+ * So the coverage is exact and stated rather than implied:
16
+ *
17
+ * - **Covered:** normal exit, `process.exit()`, an uncaught exception that unwinds to the default handler.
18
+ * - **NOT covered:** SIGKILL, and SIGTERM/SIGINT where nothing else in the process has installed a
19
+ * listener. Node terminates without running `exit` handlers in those cases, by design. A pane can still
20
+ * be orphaned there, and `herdr tab close <id>` is the manual remedy.
21
+ *
22
+ * Everything here is **synchronous**, because an `exit` handler is: a promise scheduled there never runs.
23
+ */
24
+ export interface OpenPane {
25
+ /** herdr tab id — what `tab close` takes. */
26
+ tab: string;
27
+ /** Agent name, so the agent is stopped before its tab goes away. */
28
+ name: string;
29
+ /** Staged system-prompt directory, removed with the pane it belonged to. */
30
+ promptDir?: string;
31
+ }
32
+ /** A run has opened a pane. Idempotent per tab, and installs the exit hook on first use only. */
33
+ export declare function trackPane(pane: OpenPane): void;
34
+ /** A run closed its own pane the normal way. */
35
+ export declare function untrackPane(tab: string): void;
36
+ /** How many panes are currently outstanding — for tests and for `/grants`. */
37
+ export declare function openPaneCount(): number;
38
+ /**
39
+ * Close every outstanding pane and return the tab ids closed.
40
+ *
41
+ * Exported and parameterised so it can be tested without herdr installed: the exit hook is unreachable from
42
+ * a test (registering a real `exit` handler would run during the test runner's own shutdown), so the hook is
43
+ * one line and *this* is where the behaviour lives.
44
+ *
45
+ * Failures are swallowed per pane rather than per call — one pane herdr will not close must not strand the
46
+ * other seven, and nothing at exit has anywhere to report to anyway.
47
+ */
48
+ export declare function reapOpenPanes(syncExec?: (args: string[]) => void, now?: () => number): string[];
49
+ //# sourceMappingURL=pane-reaper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pane-reaper.d.ts","sourceRoot":"","sources":["../src/pane-reaper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAKH,MAAM,WAAW,QAAQ;IACvB,6CAA6C;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAeD,iGAAiG;AACjG,wBAAgB,SAAS,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAO9C;AAED,gDAAgD;AAChD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAE7C;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAyBD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,QAAQ,GAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,IAAsB,EAAE,GAAG,eAAW,GAAG,MAAM,EAAE,CA6B5G"}