pi-daddy 0.46.1 → 0.47.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 (106) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +41 -11
  3. package/contracts/ledger-record/v1/governance-event.schema.json +65 -3
  4. package/dist/governance/approval-prompt.d.ts +4 -0
  5. package/dist/governance/approval-prompt.d.ts.map +1 -1
  6. package/dist/governance/approval-prompt.js +59 -4
  7. package/dist/governance/approval-prompt.js.map +1 -1
  8. package/dist/governance/auto-mode-policy.d.ts +11 -0
  9. package/dist/governance/auto-mode-policy.d.ts.map +1 -0
  10. package/dist/governance/auto-mode-policy.js +309 -0
  11. package/dist/governance/auto-mode-policy.js.map +1 -0
  12. package/dist/governance/execution-retention.d.ts +3 -0
  13. package/dist/governance/execution-retention.d.ts.map +1 -1
  14. package/dist/governance/execution-retention.js +30 -0
  15. package/dist/governance/execution-retention.js.map +1 -1
  16. package/dist/governance/ledger-events.d.ts +14 -1
  17. package/dist/governance/ledger-events.d.ts.map +1 -1
  18. package/dist/governance/ledger-events.js +13 -0
  19. package/dist/governance/ledger-events.js.map +1 -1
  20. package/dist/governance/ledger-report.d.ts.map +1 -1
  21. package/dist/governance/ledger-report.js +5 -3
  22. package/dist/governance/ledger-report.js.map +1 -1
  23. package/dist/governance/ledger-v3-validation.d.ts.map +1 -1
  24. package/dist/governance/ledger-v3-validation.js +17 -3
  25. package/dist/governance/ledger-v3-validation.js.map +1 -1
  26. package/dist/governance/ledger.d.ts +1 -1
  27. package/dist/governance/ledger.d.ts.map +1 -1
  28. package/dist/governance/ledger.js +1 -1
  29. package/dist/governance/ledger.js.map +1 -1
  30. package/dist/kernel/approval.d.ts +1 -1
  31. package/dist/kernel/approval.d.ts.map +1 -1
  32. package/dist/kernel/approval.js +1 -1
  33. package/dist/kernel/approval.js.map +1 -1
  34. package/dist/kernel/auto-mode.d.ts +35 -0
  35. package/dist/kernel/auto-mode.d.ts.map +1 -0
  36. package/dist/kernel/auto-mode.js +29 -0
  37. package/dist/kernel/auto-mode.js.map +1 -0
  38. package/dist/kernel/delegate-types.d.ts +2 -0
  39. package/dist/kernel/delegate-types.d.ts.map +1 -1
  40. package/dist/kernel/delegate-types.js.map +1 -1
  41. package/dist/kernel/delegate.d.ts.map +1 -1
  42. package/dist/kernel/delegate.js +2 -2
  43. package/dist/kernel/delegate.js.map +1 -1
  44. package/dist/kernel/env-names.d.ts +2 -0
  45. package/dist/kernel/env-names.d.ts.map +1 -1
  46. package/dist/kernel/env-names.js +4 -0
  47. package/dist/kernel/env-names.js.map +1 -1
  48. package/dist/kernel/propagation.d.ts +5 -1
  49. package/dist/kernel/propagation.d.ts.map +1 -1
  50. package/dist/kernel/propagation.js +8 -2
  51. package/dist/kernel/propagation.js.map +1 -1
  52. package/dist/products/dashboard-cli.d.ts +6 -0
  53. package/dist/products/dashboard-cli.d.ts.map +1 -1
  54. package/dist/products/dashboard-cli.js +248 -52
  55. package/dist/products/dashboard-cli.js.map +1 -1
  56. package/dist/products/dashboard-display-controls.d.ts +2 -1
  57. package/dist/products/dashboard-display-controls.d.ts.map +1 -1
  58. package/dist/products/dashboard-display-controls.js +61 -0
  59. package/dist/products/dashboard-display-controls.js.map +1 -1
  60. package/dist/products/dashboard-render.d.ts +2 -0
  61. package/dist/products/dashboard-render.d.ts.map +1 -1
  62. package/dist/products/dashboard-render.js +2 -2
  63. package/dist/products/dashboard-render.js.map +1 -1
  64. package/dist/products/dashboard-screen.d.ts +52 -0
  65. package/dist/products/dashboard-screen.d.ts.map +1 -0
  66. package/dist/products/dashboard-screen.js +262 -0
  67. package/dist/products/dashboard-screen.js.map +1 -0
  68. package/dist/products/dashboard-session-client.d.ts +34 -2
  69. package/dist/products/dashboard-session-client.d.ts.map +1 -1
  70. package/dist/products/dashboard-session-client.js +51 -0
  71. package/dist/products/dashboard-session-client.js.map +1 -1
  72. package/dist/products/ecosystem-versions.d.ts +19 -0
  73. package/dist/products/ecosystem-versions.d.ts.map +1 -0
  74. package/dist/products/ecosystem-versions.js +2 -0
  75. package/dist/products/ecosystem-versions.js.map +1 -0
  76. package/extensions/approvals.ts +3 -2
  77. package/extensions/dashboard-session-server.ts +32 -4
  78. package/extensions/delegate-chain.ts +2 -0
  79. package/extensions/delegation-ledger.ts +10 -6
  80. package/extensions/ecosystem-versions.ts +166 -0
  81. package/extensions/grants-command.ts +33 -3
  82. package/extensions/grants.ts +28 -2
  83. package/extensions/reload-environment.ts +16 -1
  84. package/extensions/run-delegation.ts +49 -3
  85. package/extensions/session-auto-mode.ts +132 -0
  86. package/extensions/session.ts +47 -37
  87. package/package.json +2 -2
  88. package/src/governance/approval-prompt.ts +62 -4
  89. package/src/governance/auto-mode-policy.ts +297 -0
  90. package/src/governance/execution-retention.ts +24 -0
  91. package/src/governance/ledger-events.ts +22 -1
  92. package/src/governance/ledger-report.ts +5 -3
  93. package/src/governance/ledger-v3-validation.ts +17 -3
  94. package/src/governance/ledger.ts +1 -0
  95. package/src/kernel/approval.ts +1 -1
  96. package/src/kernel/auto-mode.ts +56 -0
  97. package/src/kernel/delegate-types.ts +2 -0
  98. package/src/kernel/delegate.ts +2 -1
  99. package/src/kernel/env-names.ts +4 -0
  100. package/src/kernel/propagation.ts +11 -2
  101. package/src/products/dashboard-cli.ts +255 -53
  102. package/src/products/dashboard-display-controls.ts +52 -0
  103. package/src/products/dashboard-render.ts +2 -2
  104. package/src/products/dashboard-screen.ts +339 -0
  105. package/src/products/dashboard-session-client.ts +60 -1
  106. package/src/products/ecosystem-versions.ts +18 -0
@@ -0,0 +1,297 @@
1
+ /** Root-owned live Auto authority. Descendants receive read/admit access, never mutation access. */
2
+ import { randomBytes, randomUUID } from "node:crypto";
3
+ import { chmod, unlink } from "node:fs/promises";
4
+ import { unlinkSync } from "node:fs";
5
+ import { createServer, createConnection, type Socket } from "node:net";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import type {
9
+ AutoModeReader,
10
+ AutoModeRef,
11
+ AutoModeSnapshot,
12
+ AutoModeSource,
13
+ PendingApproval,
14
+ } from "../kernel/auto-mode.ts";
15
+
16
+ const aborted = () => new Error("Auto approval request aborted");
17
+ const unavailable = () => new Error("Auto policy owner is unavailable; no automatic permission was granted");
18
+ const validPending = (value: unknown): value is PendingApproval => {
19
+ if (!value || typeof value !== "object") return false;
20
+ const v = value as PendingApproval;
21
+ return [v.id, v.subject, v.capability].every((x) => typeof x === "string" && x.length > 0 && x.length <= 256);
22
+ };
23
+ export interface AutoModeAuthority extends AutoModeReader {
24
+ snapshot(): AutoModeSnapshot;
25
+ set(enabled: boolean, source: AutoModeSource): AutoModeSnapshot;
26
+ }
27
+
28
+ export async function createAutoModeAuthority(initial: {
29
+ enabled: boolean;
30
+ source: AutoModeSource;
31
+ }): Promise<AutoModeAuthority> {
32
+ const reference = {
33
+ socketPath: join(tmpdir(), `daddy-auto-${process.pid}-${randomBytes(8).toString("hex")}.sock`),
34
+ token: randomBytes(24).toString("hex"),
35
+ ownerId: randomBytes(16).toString("hex"),
36
+ };
37
+ let enabled = initial.enabled,
38
+ source = initial.source,
39
+ revision = 0,
40
+ closed = false;
41
+ const pending = new Map<string, PendingApproval>();
42
+ const waiters = new Set<() => void>();
43
+ const sockets = new Set<Socket>();
44
+ const assertOpen = () => {
45
+ if (closed) throw unavailable();
46
+ };
47
+ const snapshot = (): AutoModeSnapshot => {
48
+ assertOpen();
49
+ return {
50
+ enabled,
51
+ source,
52
+ revision,
53
+ ownerId: reference.ownerId,
54
+ pendingApprovals: [...pending.values()].map((v) => ({ ...v })),
55
+ };
56
+ };
57
+ const waitEnabled = async (signal: AbortSignal): Promise<void> => {
58
+ assertOpen();
59
+ if (signal.aborted) throw aborted();
60
+ if (enabled) return;
61
+ await new Promise<void>((resolve, reject) => {
62
+ const clean = () => {
63
+ waiters.delete(wake);
64
+ signal.removeEventListener("abort", cancel);
65
+ };
66
+ const wake = () => {
67
+ clean();
68
+ closed ? reject(unavailable()) : resolve();
69
+ };
70
+ const cancel = () => {
71
+ clean();
72
+ reject(aborted());
73
+ };
74
+ waiters.add(wake);
75
+ signal.addEventListener("abort", cancel, { once: true });
76
+ if (signal.aborted) cancel();
77
+ });
78
+ };
79
+ const trackPending = (approval: PendingApproval) => {
80
+ assertOpen();
81
+ if (!validPending(approval)) throw new Error("Invalid pending approval");
82
+ const key = randomUUID();
83
+ pending.set(key, { ...approval });
84
+ return () => {
85
+ pending.delete(key);
86
+ };
87
+ };
88
+ const server = createServer((socket) => {
89
+ sockets.add(socket);
90
+ socket.setEncoding("utf8");
91
+ socket.setTimeout(2000, () => socket.destroy());
92
+ const controller = new AbortController();
93
+ let input = "",
94
+ handled = false,
95
+ untrack: (() => void) | undefined;
96
+ socket.on("error", () => {});
97
+ socket.once("close", () => {
98
+ sockets.delete(socket);
99
+ controller.abort();
100
+ untrack?.();
101
+ });
102
+ const reply = (value: unknown) => {
103
+ if (!socket.destroyed) socket.end(JSON.stringify(value) + "\n");
104
+ };
105
+ socket.on("data", (chunk) => {
106
+ if (handled) {
107
+ socket.destroy();
108
+ return;
109
+ }
110
+ input += chunk;
111
+ if (input.length > 4096) {
112
+ socket.destroy();
113
+ return;
114
+ }
115
+ if (!input.includes("\n")) return;
116
+ handled = true;
117
+ try {
118
+ const request = JSON.parse(input.slice(0, input.indexOf("\n")));
119
+ if (request.token !== reference.token || request.ownerId !== reference.ownerId)
120
+ throw new Error("Unauthorized Auto policy request");
121
+ assertOpen();
122
+ if (request.action === "read") reply({ ok: true, value: snapshot() });
123
+ else if (request.action === "admit") reply({ ok: true, value: enabled });
124
+ else if (request.action === "wait") {
125
+ socket.setTimeout(0);
126
+ void waitEnabled(controller.signal)
127
+ .then(() => reply({ ok: true }))
128
+ .catch((error) => reply({ ok: false, error: String(error) }));
129
+ } else if (request.action === "pending" && validPending(request.approval)) {
130
+ socket.setTimeout(0);
131
+ untrack = trackPending(request.approval);
132
+ } else throw new Error("Unknown Auto policy action");
133
+ } catch (error) {
134
+ reply({ ok: false, error: String(error) });
135
+ }
136
+ });
137
+ });
138
+ await new Promise<void>((resolve, reject) => {
139
+ server.once("error", reject);
140
+ server.listen(reference.socketPath, resolve);
141
+ });
142
+ try {
143
+ await chmod(reference.socketPath, 0o600);
144
+ } catch (error) {
145
+ server.close();
146
+ await unlink(reference.socketPath).catch(() => {});
147
+ throw error;
148
+ }
149
+ server.unref();
150
+ const exit = () => {
151
+ try {
152
+ unlinkSync(reference.socketPath);
153
+ } catch {}
154
+ };
155
+ process.once("exit", exit);
156
+ return {
157
+ reference,
158
+ snapshot,
159
+ read: async () => snapshot(),
160
+ waitEnabled,
161
+ trackPending,
162
+ admit: async (signal) => {
163
+ assertOpen();
164
+ if (signal?.aborted) throw aborted();
165
+ return enabled;
166
+ },
167
+ set: (next, from) => {
168
+ assertOpen();
169
+ enabled = next;
170
+ source = from;
171
+ revision++;
172
+ if (enabled) for (const wake of [...waiters]) wake();
173
+ return snapshot();
174
+ },
175
+ close: async () => {
176
+ if (closed) return;
177
+ closed = true;
178
+ enabled = false;
179
+ for (const wake of [...waiters]) wake();
180
+ for (const socket of sockets) socket.destroy();
181
+ pending.clear();
182
+ process.off("exit", exit);
183
+ await new Promise<void>((resolve) => server.close(() => resolve()));
184
+ await unlink(reference.socketPath).catch((error) => {
185
+ if (error.code !== "ENOENT") throw error;
186
+ });
187
+ },
188
+ };
189
+ }
190
+
191
+ function request(ref: AutoModeRef, action: "read" | "admit" | "wait", signal?: AbortSignal): Promise<unknown> {
192
+ return new Promise((resolve, reject) => {
193
+ if (signal?.aborted) {
194
+ reject(aborted());
195
+ return;
196
+ }
197
+ const socket = createConnection(ref.socketPath);
198
+ let output = "",
199
+ settled = false;
200
+ const finish = (error?: Error, value?: unknown) => {
201
+ if (settled) return;
202
+ settled = true;
203
+ signal?.removeEventListener("abort", cancel);
204
+ socket.destroy();
205
+ error ? reject(error) : resolve(value);
206
+ };
207
+ const cancel = () => finish(aborted());
208
+ signal?.addEventListener("abort", cancel, { once: true });
209
+ socket.setEncoding("utf8");
210
+ socket.setTimeout(2000, () => finish(unavailable()));
211
+ socket.once("error", () => finish(unavailable()));
212
+ socket.once("connect", () => {
213
+ if (action === "wait") socket.setTimeout(0);
214
+ socket.write(JSON.stringify({ ...ref, action }) + "\n");
215
+ });
216
+ socket.on("data", (chunk) => {
217
+ output += chunk;
218
+ if (output.length > 256 * 1024) finish(new Error("Auto policy response too large"));
219
+ });
220
+ socket.once("end", () => {
221
+ try {
222
+ const reply = JSON.parse(output);
223
+ if (reply.ok !== true) throw new Error(reply.error ?? "Auto policy request failed");
224
+ finish(undefined, reply.value);
225
+ } catch (error) {
226
+ finish(error instanceof Error ? error : unavailable());
227
+ }
228
+ });
229
+ socket.once("close", () => {
230
+ if (!settled) finish(unavailable());
231
+ });
232
+ });
233
+ }
234
+
235
+ export function connectAutoMode(reference: AutoModeRef): AutoModeReader {
236
+ let closed = false;
237
+ const active = new Set<AbortController>();
238
+ const pending = new Set<Socket>();
239
+ const call = async (action: "read" | "admit" | "wait", signal?: AbortSignal) => {
240
+ if (closed) throw unavailable();
241
+ const controller = new AbortController();
242
+ active.add(controller);
243
+ const abort = () => controller.abort();
244
+ signal?.addEventListener("abort", abort, { once: true });
245
+ if (signal?.aborted) abort();
246
+ try {
247
+ return await request(reference, action, controller.signal);
248
+ } finally {
249
+ active.delete(controller);
250
+ signal?.removeEventListener("abort", abort);
251
+ }
252
+ };
253
+ return {
254
+ reference,
255
+ read: async () => {
256
+ const value = (await call("read")) as AutoModeSnapshot;
257
+ if (
258
+ !value ||
259
+ value.ownerId !== reference.ownerId ||
260
+ typeof value.enabled !== "boolean" ||
261
+ !["default", "environment", "session"].includes(value.source) ||
262
+ !Number.isSafeInteger(value.revision) ||
263
+ value.revision < 0 ||
264
+ !Array.isArray(value.pendingApprovals) ||
265
+ !value.pendingApprovals.every(validPending)
266
+ )
267
+ throw unavailable();
268
+ return value;
269
+ },
270
+ admit: async (signal) => {
271
+ const value = await call("admit", signal);
272
+ if (typeof value !== "boolean") throw unavailable();
273
+ return value;
274
+ },
275
+ waitEnabled: async (signal) => {
276
+ await call("wait", signal);
277
+ },
278
+ trackPending: (approval) => {
279
+ if (closed) throw unavailable();
280
+ if (!validPending(approval)) throw new Error("Invalid pending approval");
281
+ const socket = createConnection(reference.socketPath);
282
+ pending.add(socket);
283
+ socket.once("error", () => {});
284
+ socket.once("close", () => pending.delete(socket));
285
+ socket.once("connect", () => socket.write(JSON.stringify({ ...reference, action: "pending", approval }) + "\n"));
286
+ return () => {
287
+ pending.delete(socket);
288
+ socket.destroy();
289
+ };
290
+ },
291
+ close: async () => {
292
+ closed = true;
293
+ for (const controller of active) controller.abort();
294
+ for (const socket of pending) socket.destroy();
295
+ },
296
+ };
297
+ }
@@ -96,6 +96,9 @@ export const retentionConfigurationDigest = (configuration: unknown): string =>
96
96
  * A bounded, coalescing observation queue. No worker-control path awaits publication. A failed or stalled
97
97
  * archive never changes required ledger/lease semantics. `flush` is exclusively for external archival at
98
98
  * quiescence; it is NOT called by the spawner. Missing terminal publication means incomplete observation.
99
+ * Streams checkpoint at doubling byte milestones and once at finish: total stream blob bytes are below
100
+ * three times each stream's largest retained prefix, rather than one cumulative copy per chunk. A crash can lose the newest
101
+ * suffix since the last checkpoint; running manifests mark that lag. Existing archive files are untouched.
99
102
  */
100
103
  export function beginExecutionRetention(identity: RetentionIdentity, directory = process.env[ENV_EXECUTION_ARCHIVE]) {
101
104
  const archiveId = randomUUID();
@@ -143,6 +146,7 @@ export function beginExecutionRetention(identity: RetentionIdentity, directory =
143
146
  };
144
147
  const buffers = new Map<keyof typeof manifest.content, Buffer>();
145
148
  const written = new Set<string>();
149
+ const streamCheckpoints = new Map<"stdout" | "stderr", RetainedContent>();
146
150
  let total = 0;
147
151
  let dirty = false;
148
152
  let running: Promise<void> | undefined;
@@ -161,6 +165,9 @@ export function beginExecutionRetention(identity: RetentionIdentity, directory =
161
165
  const loss = (reason: string) => {
162
166
  if (!manifest.coverage.losses.includes(reason)) manifest.coverage.losses.push(reason);
163
167
  };
168
+ const clearLoss = (reason: string) => {
169
+ manifest.coverage.losses = manifest.coverage.losses.filter((value) => value !== reason);
170
+ };
164
171
  const release = () => {
165
172
  if (admitted && !released) {
166
173
  active--;
@@ -178,6 +185,13 @@ export function beginExecutionRetention(identity: RetentionIdentity, directory =
178
185
  const checkpoint: ExecutionRetentionManifest = JSON.parse(JSON.stringify(manifest));
179
186
  const contents = [...buffers];
180
187
  for (const [kind, bytes] of contents) {
188
+ const stream = kind === "stdout" || kind === "stderr" ? kind : undefined;
189
+ const previous = stream && streamCheckpoints.get(stream);
190
+ if (previous && checkpoint.state !== "terminal" && bytes.length < previous.bytes! * 2) {
191
+ checkpoint.content[kind] = previous;
192
+ if (bytes.length !== previous.bytes) checkpoint.coverage.losses.push(`${kind}-checkpoint-lagging`);
193
+ continue;
194
+ }
181
195
  const sha256 = digest(bytes);
182
196
  const name = `${kind}-${sha256}.bin`;
183
197
  if (!written.has(name)) {
@@ -185,6 +199,7 @@ export function beginExecutionRetention(identity: RetentionIdentity, directory =
185
199
  written.add(name);
186
200
  }
187
201
  checkpoint.content[kind] = { status: "retained", path: name, sha256, bytes: bytes.length };
202
+ if (stream) streamCheckpoints.set(stream, checkpoint.content[kind]);
188
203
  }
189
204
  await writeFile(
190
205
  join(root!, "manifest.pending"),
@@ -263,6 +278,15 @@ export function beginExecutionRetention(identity: RetentionIdentity, directory =
263
278
  manifest.native.branchLeafId = observation.branchLeafId;
264
279
  buffers.delete("session");
265
280
  if (captured.bytes && observation === captured.observation) buffers.set("session", captured.bytes);
281
+ if (buffers.has("session") && observation.status === "verified") {
282
+ clearLoss("native-session-content-unavailable");
283
+ if (manifest.coverage.losses.includes("native-session-read-failed")) {
284
+ clearLoss("native-session-read-failed");
285
+ loss("native-session-read-failed-earlier");
286
+ }
287
+ } else loss("native-session-content-unavailable");
288
+ if (observation.branchState === "observed") clearLoss("active-branch-unknown");
289
+ else loss("active-branch-unknown");
266
290
  if (observation.reason) loss(observation.reason);
267
291
  schedule();
268
292
  }
@@ -212,8 +212,9 @@ export interface SessionConfigEvent extends Omit<LedgerEventBase, "correlation">
212
212
  event: "session_config";
213
213
  episodeId: string;
214
214
  outcome: "kept" | "changed";
215
- trigger: "first-delegation" | "grants-models";
215
+ trigger: "first-delegation" | "grants-models" | "auto-mode";
216
216
  overrides: Record<string, { model: string; thinking: string }>;
217
+ autoMode?: { enabled: boolean; source: "default" | "environment" | "session"; revision: number };
217
218
  }
218
219
 
219
220
  export type RuntimeLedgerEvent =
@@ -244,6 +245,26 @@ export function buildSessionConfigEvent(args: {
244
245
  });
245
246
  }
246
247
 
248
+ /** Auto authorization changes are explicit session facts, never fabricated prompt approvals. */
249
+ export function buildAutoModeConfigEvent(args: {
250
+ episodeId: string;
251
+ enabled: boolean;
252
+ source: "default" | "environment" | "session";
253
+ revision: number;
254
+ now: Date;
255
+ }): SessionConfigEvent {
256
+ return assertLedgerV3Wire({
257
+ ledgerVersion: LEDGER_VERSION,
258
+ event: "session_config",
259
+ ts: args.now.toISOString(),
260
+ episodeId: args.episodeId,
261
+ outcome: "changed",
262
+ trigger: "auto-mode",
263
+ overrides: {},
264
+ autoMode: { enabled: args.enabled, source: args.source, revision: args.revision },
265
+ });
266
+ }
267
+
247
268
  export function buildEpisodeOutcomeEvent(args: {
248
269
  episodeId: string;
249
270
  commit: string;
@@ -211,8 +211,8 @@ export async function verifyLedger(path: string): Promise<LedgerReport> {
211
211
  executors: { herdr: 0, process: 0, unknown: 0 },
212
212
  definitions: [],
213
213
  approvals: {
214
- bySource: { prompt: 0, session: 0, persisted: 0, inherited: 0 },
215
- distinctBySource: { prompt: 0, session: 0, persisted: 0, inherited: 0 },
214
+ bySource: { prompt: 0, session: 0, persisted: 0, inherited: 0, auto: 0 },
215
+ distinctBySource: { prompt: 0, session: 0, persisted: 0, inherited: 0, auto: 0 },
216
216
  unattributed: 0,
217
217
  humanDenied: 0,
218
218
  humanDeniedPairs: 0,
@@ -243,13 +243,14 @@ export async function verifyLedger(path: string): Promise<LedgerReport> {
243
243
  let retired = 0;
244
244
  let escalationAttempts = 0;
245
245
  const executors = { herdr: 0, process: 0, unknown: 0 };
246
- const bySource: Record<ApprovalSource, number> = { prompt: 0, session: 0, persisted: 0, inherited: 0 };
246
+ const bySource: Record<ApprovalSource, number> = { prompt: 0, session: 0, persisted: 0, inherited: 0, auto: 0 };
247
247
  // `capability@subject` seen per source, so the report can state a bound as well as a raw count.
248
248
  const distinct: Record<ApprovalSource, Set<string>> = {
249
249
  prompt: new Set(),
250
250
  session: new Set(),
251
251
  persisted: new Set(),
252
252
  inherited: new Set(),
253
+ auto: new Set(),
253
254
  };
254
255
  let unattributed = 0;
255
256
  let humanDenied = 0;
@@ -386,6 +387,7 @@ export async function verifyLedger(path: string): Promise<LedgerReport> {
386
387
  session: distinct.session.size,
387
388
  persisted: distinct.persisted.size,
388
389
  inherited: distinct.inherited.size,
390
+ auto: distinct.auto.size,
389
391
  },
390
392
  unattributed,
391
393
  humanDenied,
@@ -21,7 +21,7 @@ export function isRetiredLedgerEvent(event: { event?: unknown }): boolean {
21
21
 
22
22
  const SHA256_RE = /^[a-f0-9]{64}$/i;
23
23
  const EXECUTORS = new Set(["process", "herdr"]);
24
- const APPROVAL_SOURCES = new Set(["prompt", "session", "persisted", "inherited"]);
24
+ const APPROVAL_SOURCES = new Set(["prompt", "session", "persisted", "inherited", "auto"]);
25
25
  const APPROVAL_SCOPES = new Set(["once", "session", "always"]);
26
26
  const GATE_OUTCOMES = new Set(["declined", "dismissed", "expired", "aborted", "no-ui", "error"]);
27
27
  const LEASE_OUTCOMES = new Set([
@@ -146,7 +146,7 @@ const FIELDS = {
146
146
  "outcome",
147
147
  "newCeiling",
148
148
  ]),
149
- session_config: new Set(["ledgerVersion", "event", "ts", "episodeId", "outcome", "trigger", "overrides"]),
149
+ session_config: new Set(["ledgerVersion", "event", "ts", "episodeId", "outcome", "trigger", "overrides", "autoMode"]),
150
150
  episode_outcome: new Set([
151
151
  "ledgerVersion",
152
152
  "event",
@@ -537,7 +537,7 @@ export function validateLedgerV3Event(event: LedgerV3Object): string | null {
537
537
  if (
538
538
  !isEpisodeId(event.episodeId) ||
539
539
  !["kept", "changed"].includes(String(event.outcome)) ||
540
- !["first-delegation", "grants-models"].includes(String(event.trigger)) ||
540
+ !["first-delegation", "grants-models", "auto-mode"].includes(String(event.trigger)) ||
541
541
  !isLedgerObject(event.overrides) ||
542
542
  !Object.entries(event.overrides).every(
543
543
  ([name, value]) =>
@@ -549,6 +549,20 @@ export function validateLedgerV3Event(event: LedgerV3Object): string | null {
549
549
  )
550
550
  )
551
551
  return "session config fields are invalid";
552
+ if (event.trigger === "auto-mode") {
553
+ const auto = event.autoMode;
554
+ if (
555
+ event.outcome !== "changed" ||
556
+ Object.keys(event.overrides).length !== 0 ||
557
+ !isLedgerObject(auto) ||
558
+ Object.keys(auto).length !== 3 ||
559
+ typeof auto.enabled !== "boolean" ||
560
+ !["default", "environment", "session"].includes(String(auto.source)) ||
561
+ !Number.isSafeInteger(auto.revision) ||
562
+ (auto.revision as number) < 0
563
+ )
564
+ return "auto mode config fields are invalid";
565
+ } else if (event.autoMode !== undefined) return "auto mode config requires its own trigger";
552
566
  return null;
553
567
  }
554
568
  if (kind === "episode_outcome") return validateEpisodeOutcome(event);
@@ -414,6 +414,7 @@ export {
414
414
  buildEpisodeCostGateEvent,
415
415
  buildEpisodeOutcomeEvent,
416
416
  buildSessionConfigEvent,
417
+ buildAutoModeConfigEvent,
417
418
  buildWorkspaceLeaseEvent,
418
419
  type CapabilityDecisionEvent,
419
420
  type EpisodeCostGateEvent,
@@ -18,7 +18,7 @@ export const APPROVAL_SCOPES = ["once", "session", "always"] as const;
18
18
  export type ApprovalScope = (typeof APPROVAL_SCOPES)[number];
19
19
 
20
20
  /** Where a yes came from, for the ledger. These call for different follow-ups, so they stay distinct. */
21
- export const APPROVAL_SOURCES = ["prompt", "session", "persisted", "inherited"] as const;
21
+ export const APPROVAL_SOURCES = ["prompt", "session", "persisted", "inherited", "auto"] as const;
22
22
  export type ApprovalSource = (typeof APPROVAL_SOURCES)[number];
23
23
 
24
24
  /** Which call site is asking. Determines the scopes offered — see `offeredScopes`. */
@@ -0,0 +1,56 @@
1
+ /** Live session permission policy. This does not widen a grant or convey stored approval. */
2
+ export type AutoModeSource = "default" | "environment" | "session";
3
+ export interface PendingApproval {
4
+ id: string;
5
+ subject: string;
6
+ capability: string;
7
+ }
8
+ export interface AutoModeSnapshot {
9
+ enabled: boolean;
10
+ source: AutoModeSource;
11
+ ownerId: string;
12
+ revision: number;
13
+ pendingApprovals: PendingApproval[];
14
+ }
15
+ export interface AutoModeRef {
16
+ socketPath: string;
17
+ token: string;
18
+ ownerId: string;
19
+ }
20
+ export interface AutoModeReader {
21
+ readonly reference: AutoModeRef;
22
+ read(): Promise<AutoModeSnapshot>;
23
+ /** Linearization point: only a current ON answer admits new work. No approval is banked. */
24
+ admit(signal?: AbortSignal): Promise<boolean>;
25
+ /** Notification only. A waiter must read again and later admit its own operation. */
26
+ waitEnabled(signal: AbortSignal): Promise<void>;
27
+ trackPending(approval: PendingApproval): () => void;
28
+ close(): Promise<void>;
29
+ }
30
+ export function parseAutoModeDefault(raw: string | undefined): { enabled: boolean; source: AutoModeSource } {
31
+ if (raw === undefined) return { enabled: false, source: "default" };
32
+ if (raw === "0" || raw === "1") return { enabled: raw === "1", source: "environment" };
33
+ throw new Error("PI_DADDY_AUTO_MODE must be literal 1 or 0 (unset defaults to OFF)");
34
+ }
35
+ export function parseAutoModeRef(raw: string): AutoModeRef {
36
+ let ref: AutoModeRef;
37
+ try {
38
+ ref = JSON.parse(raw);
39
+ } catch {
40
+ throw new Error("PI_DADDY_AUTO_MODE_REF is malformed");
41
+ }
42
+ if (
43
+ !ref ||
44
+ typeof ref !== "object" ||
45
+ Object.keys(ref).sort().join(",") !== "ownerId,socketPath,token" ||
46
+ typeof ref.socketPath !== "string" ||
47
+ !ref.socketPath.startsWith("/") ||
48
+ ref.socketPath.length > 107 ||
49
+ typeof ref.token !== "string" ||
50
+ !/^[a-f0-9]{48}$/.test(ref.token) ||
51
+ typeof ref.ownerId !== "string" ||
52
+ !/^[a-f0-9]{32}$/.test(ref.ownerId)
53
+ )
54
+ throw new Error("PI_DADDY_AUTO_MODE_REF is malformed");
55
+ return ref;
56
+ }
@@ -1,3 +1,4 @@
1
+ import type { AutoModeRef } from "./auto-mode.ts";
1
2
  /**
2
3
  * The shapes `planDelegation` consumes and produces. Split out of `./delegate.ts` only to stay under the
3
4
  * 400-line module ceiling this project enforces mechanically; `./delegate.ts` re-exports all three, so
@@ -58,6 +59,7 @@ export interface DelegationRequest {
58
59
  }
59
60
 
60
61
  export interface DelegationContext {
62
+ autoModeRef?: AutoModeRef;
61
63
  ownGrant: Capability[];
62
64
  /** Stable identity shared by the root session and every descendant. */
63
65
  episodeId?: string;
@@ -26,6 +26,7 @@ import {
26
26
  ENV_PARENT_ID,
27
27
  inheritableGrant,
28
28
  workspacePinEnv,
29
+ autoModeEnv,
29
30
  } from "./propagation.ts";
30
31
  import { DELEGATE_SUBJECT, inheritApprovals, type InheritableApproval } from "./approval.ts";
31
32
  import { explainDoubledNamespace, suggestForUnknown, unknownCapabilities, type Catalog } from "./catalog.ts";
@@ -421,7 +422,7 @@ export function planDelegation(request: DelegationRequest, ctx: DelegationContex
421
422
  // spelled once in `childEnv` and not here is a rule that does not hold on the path a delegated child
422
423
  // actually takes. Without this a routed grandchild inherits no pin and can route nowhere — fail-closed,
423
424
  // but wrong, and silently so.
424
- Object.assign(env, workspacePinEnv(ctx.workspacePin, inheritable));
425
+ Object.assign(env, workspacePinEnv(ctx.workspacePin, inheritable), autoModeEnv(ctx.autoModeRef));
425
426
  // Composition-supplied per-child environment (the activity timeline's observation identity today).
426
427
  // Never process-global grant state: a key in the governance namespace is a programming error in the
427
428
  // caller, refused loudly rather than letting a product widen what the child inherits.
@@ -20,6 +20,8 @@ export const ENV_MAX_DEPTH = "PI_DADDY_MAX_DEPTH";
20
20
  export const ENV_GATED = "PI_DADDY_GATED";
21
21
  export const ENV_LEDGER = "PI_DADDY_LEDGER";
22
22
  export const ENV_APPROVED = "PI_DADDY_APPROVED";
23
+ export const ENV_AUTO_MODE = "PI_DADDY_AUTO_MODE";
24
+ export const ENV_AUTO_MODE_REF = "PI_DADDY_AUTO_MODE_REF";
23
25
  export const ENV_FANOUT = "PI_DADDY_FANOUT";
24
26
  export const ENV_PARENT_ID = "PI_DADDY_PARENT_ID";
25
27
  export const ENV_EXECUTION_ID = "PI_DADDY_EXECUTION_ID";
@@ -86,6 +88,8 @@ export const GOVERNANCE_ENV_KEYS: readonly string[] = Object.freeze([
86
88
  ENV_GATED,
87
89
  ENV_LEDGER,
88
90
  ENV_APPROVED,
91
+ ENV_AUTO_MODE,
92
+ ENV_AUTO_MODE_REF,
89
93
  ENV_FANOUT,
90
94
  ENV_PARENT_ID,
91
95
  ENV_EXECUTION_ID,
@@ -25,7 +25,8 @@
25
25
  * interceptor and pi handed the child more than the parent held, the intersection clamps it back.
26
26
  */
27
27
 
28
- import { ENV_PUBLIC_EVIDENCE_DIR } from "./env-names.ts";
28
+ import { ENV_AUTO_MODE, ENV_AUTO_MODE_REF, ENV_PUBLIC_EVIDENCE_DIR } from "./env-names.ts";
29
+ import type { AutoModeRef } from "./auto-mode.ts";
29
30
  import type { Capability } from "./resolve.ts";
30
31
  import { attenuateWorkspacePin, formatWorkspacePin, type WorkspacePins } from "./workspace-pin.ts";
31
32
  import { WILDCARD } from "./pi-tools.ts";
@@ -87,6 +88,8 @@ export {
87
88
  * to give it.
88
89
  */
89
90
  export const GRANT_ENV_KEYS = [
91
+ ENV_AUTO_MODE,
92
+ ENV_AUTO_MODE_REF,
90
93
  ENV_PUBLIC_EVIDENCE_DIR,
91
94
  // Not governance state, but the same rule applies for a stronger reason: neither a credential the parent holds
92
95
  // nor the switch that points it at a third party is something a child inherits by being spawned (ADR-0077).
@@ -265,6 +268,7 @@ export function gatedFromEnv(raw: string | undefined): Capability[] {
265
268
  }
266
269
 
267
270
  export interface ChildEnvInput {
271
+ autoModeRef?: AutoModeRef;
268
272
  /** This session's own grant — becomes the child's inherited parent grant. */
269
273
  ownGrant: Capability[];
270
274
  /**
@@ -362,7 +366,7 @@ export function childEnv(input: ChildEnvInput): Record<string, string> {
362
366
  // `ENV_APPROVED` is: an omitted key does not overwrite, so a child would inherit the PARENT's unnarrowed pin
363
367
  // through the process-global publication path. An empty value parses back as "a pin was established and you
364
368
  // got nothing from it", which refuses at routing with a different message from "no pin exists".
365
- Object.assign(env, workspacePinEnv(input.workspacePin, inheritable));
369
+ Object.assign(env, workspacePinEnv(input.workspacePin, inheritable), autoModeEnv(input.autoModeRef));
366
370
  return env;
367
371
  }
368
372
 
@@ -427,3 +431,8 @@ export function observeToolNames(payload: unknown): string[] | null {
427
431
  }
428
432
  return null;
429
433
  }
434
+
435
+ /** A root preference is never copied; every descendant receives the same live owner reference. */
436
+ export function autoModeEnv(ref: AutoModeRef | undefined): Record<string, string> {
437
+ return { [ENV_AUTO_MODE]: "0", ...(ref ? { [ENV_AUTO_MODE_REF]: JSON.stringify(ref) } : {}) };
438
+ }