pi-daddy 0.46.0 → 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.
- package/CHANGELOG.md +14 -0
- package/README.md +77 -11
- package/contracts/ledger-record/v1/governance-event.schema.json +67 -3
- package/dist/governance/approval-prompt.d.ts +8 -11
- package/dist/governance/approval-prompt.d.ts.map +1 -1
- package/dist/governance/approval-prompt.js +138 -24
- package/dist/governance/approval-prompt.js.map +1 -1
- package/dist/governance/auto-mode-policy.d.ts +11 -0
- package/dist/governance/auto-mode-policy.d.ts.map +1 -0
- package/dist/governance/auto-mode-policy.js +309 -0
- package/dist/governance/auto-mode-policy.js.map +1 -0
- package/dist/governance/execution-retention.d.ts +3 -0
- package/dist/governance/execution-retention.d.ts.map +1 -1
- package/dist/governance/execution-retention.js +30 -0
- package/dist/governance/execution-retention.js.map +1 -1
- package/dist/governance/ledger-events.d.ts +14 -1
- package/dist/governance/ledger-events.d.ts.map +1 -1
- package/dist/governance/ledger-events.js +13 -0
- package/dist/governance/ledger-events.js.map +1 -1
- package/dist/governance/ledger-report.d.ts.map +1 -1
- package/dist/governance/ledger-report.js +5 -3
- package/dist/governance/ledger-report.js.map +1 -1
- package/dist/governance/ledger-v3-validation.d.ts.map +1 -1
- package/dist/governance/ledger-v3-validation.js +18 -4
- package/dist/governance/ledger-v3-validation.js.map +1 -1
- package/dist/governance/ledger.d.ts +4 -5
- package/dist/governance/ledger.d.ts.map +1 -1
- package/dist/governance/ledger.js +2 -2
- package/dist/governance/ledger.js.map +1 -1
- package/dist/kernel/approval.d.ts +1 -1
- package/dist/kernel/approval.d.ts.map +1 -1
- package/dist/kernel/approval.js +1 -1
- package/dist/kernel/approval.js.map +1 -1
- package/dist/kernel/auto-mode.d.ts +35 -0
- package/dist/kernel/auto-mode.d.ts.map +1 -0
- package/dist/kernel/auto-mode.js +29 -0
- package/dist/kernel/auto-mode.js.map +1 -0
- package/dist/kernel/delegate-types.d.ts +2 -0
- package/dist/kernel/delegate-types.d.ts.map +1 -1
- package/dist/kernel/delegate-types.js.map +1 -1
- package/dist/kernel/delegate.d.ts.map +1 -1
- package/dist/kernel/delegate.js +2 -2
- package/dist/kernel/delegate.js.map +1 -1
- package/dist/kernel/env-names.d.ts +2 -0
- package/dist/kernel/env-names.d.ts.map +1 -1
- package/dist/kernel/env-names.js +4 -0
- package/dist/kernel/env-names.js.map +1 -1
- package/dist/kernel/propagation.d.ts +5 -1
- package/dist/kernel/propagation.d.ts.map +1 -1
- package/dist/kernel/propagation.js +8 -2
- package/dist/kernel/propagation.js.map +1 -1
- package/dist/products/dashboard-cli.d.ts +6 -0
- package/dist/products/dashboard-cli.d.ts.map +1 -1
- package/dist/products/dashboard-cli.js +248 -52
- package/dist/products/dashboard-cli.js.map +1 -1
- package/dist/products/dashboard-display-controls.d.ts +2 -1
- package/dist/products/dashboard-display-controls.d.ts.map +1 -1
- package/dist/products/dashboard-display-controls.js +61 -0
- package/dist/products/dashboard-display-controls.js.map +1 -1
- package/dist/products/dashboard-render.d.ts +2 -0
- package/dist/products/dashboard-render.d.ts.map +1 -1
- package/dist/products/dashboard-render.js +2 -2
- package/dist/products/dashboard-render.js.map +1 -1
- package/dist/products/dashboard-screen.d.ts +52 -0
- package/dist/products/dashboard-screen.d.ts.map +1 -0
- package/dist/products/dashboard-screen.js +262 -0
- package/dist/products/dashboard-screen.js.map +1 -0
- package/dist/products/dashboard-session-client.d.ts +34 -2
- package/dist/products/dashboard-session-client.d.ts.map +1 -1
- package/dist/products/dashboard-session-client.js +51 -0
- package/dist/products/dashboard-session-client.js.map +1 -1
- package/dist/products/ecosystem-versions.d.ts +19 -0
- package/dist/products/ecosystem-versions.d.ts.map +1 -0
- package/dist/products/ecosystem-versions.js +2 -0
- package/dist/products/ecosystem-versions.js.map +1 -0
- package/extensions/activity-timeline.ts +94 -31
- package/extensions/approvals.ts +3 -2
- package/extensions/dashboard-session-server.ts +32 -4
- package/extensions/delegate-chain.ts +2 -0
- package/extensions/delegation-ledger.ts +10 -6
- package/extensions/ecosystem-versions.ts +166 -0
- package/extensions/execution-controls.ts +42 -0
- package/extensions/grants-command.ts +37 -3
- package/extensions/grants.ts +29 -2
- package/extensions/reload-environment.ts +16 -1
- package/extensions/run-delegation.ts +49 -3
- package/extensions/session-auto-mode.ts +132 -0
- package/extensions/session.ts +47 -37
- package/package.json +2 -2
- package/src/governance/approval-prompt.ts +144 -29
- package/src/governance/auto-mode-policy.ts +297 -0
- package/src/governance/execution-retention.ts +24 -0
- package/src/governance/ledger-events.ts +22 -1
- package/src/governance/ledger-report.ts +5 -3
- package/src/governance/ledger-v3-validation.ts +18 -4
- package/src/governance/ledger.ts +4 -4
- package/src/kernel/approval.ts +1 -1
- package/src/kernel/auto-mode.ts +56 -0
- package/src/kernel/delegate-types.ts +2 -0
- package/src/kernel/delegate.ts +2 -1
- package/src/kernel/env-names.ts +4 -0
- package/src/kernel/propagation.ts +11 -2
- package/src/products/dashboard-cli.ts +255 -53
- package/src/products/dashboard-display-controls.ts +52 -0
- package/src/products/dashboard-render.ts +2 -2
- package/src/products/dashboard-screen.ts +339 -0
- package/src/products/dashboard-session-client.ts +60 -1
- 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,9 +21,9 @@ 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
|
-
const GATE_OUTCOMES = new Set(["declined", "dismissed", "no-ui", "error"]);
|
|
26
|
+
const GATE_OUTCOMES = new Set(["declined", "dismissed", "expired", "aborted", "no-ui", "error"]);
|
|
27
27
|
const LEASE_OUTCOMES = new Set([
|
|
28
28
|
"acquired",
|
|
29
29
|
"uncontended",
|
|
@@ -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);
|
package/src/governance/ledger.ts
CHANGED
|
@@ -44,7 +44,7 @@ export const LEDGER_EVENT_KINDS = [
|
|
|
44
44
|
"episode_outcome",
|
|
45
45
|
] as const;
|
|
46
46
|
export type LedgerEventKind = (typeof LEDGER_EVENT_KINDS)[number];
|
|
47
|
-
export const LEDGER_GATE_OUTCOMES = ["declined", "dismissed", "no-ui", "error"] as const;
|
|
47
|
+
export const LEDGER_GATE_OUTCOMES = ["declined", "dismissed", "expired", "aborted", "no-ui", "error"] as const;
|
|
48
48
|
export type LedgerGateOutcome = (typeof LEDGER_GATE_OUTCOMES)[number];
|
|
49
49
|
|
|
50
50
|
export interface LedgerEventBase {
|
|
@@ -137,9 +137,8 @@ export interface GrantRecord extends LedgerEventBase {
|
|
|
137
137
|
* The discriminant was already computed and thrown away. ADR-0026 leans on this vocabulary being able to
|
|
138
138
|
* say *"nobody was there to ask"* and be believed, so it is recorded rather than inferred.
|
|
139
139
|
*
|
|
140
|
-
* **Privacy is unchanged**: this is a fixed
|
|
141
|
-
*
|
|
142
|
-
* four-member ledger enum because the approval source/scope fields already record a yes.
|
|
140
|
+
* **Privacy is unchanged**: this is a fixed enum, not task-authored text. Explicit expiry and caller
|
|
141
|
+
* cancellation have their own outcomes; `granted` is omitted because approval source/scope records a yes.
|
|
143
142
|
*/
|
|
144
143
|
gateOutcome?: LedgerGateOutcome;
|
|
145
144
|
/**
|
|
@@ -415,6 +414,7 @@ export {
|
|
|
415
414
|
buildEpisodeCostGateEvent,
|
|
416
415
|
buildEpisodeOutcomeEvent,
|
|
417
416
|
buildSessionConfigEvent,
|
|
417
|
+
buildAutoModeConfigEvent,
|
|
418
418
|
buildWorkspaceLeaseEvent,
|
|
419
419
|
type CapabilityDecisionEvent,
|
|
420
420
|
type EpisodeCostGateEvent,
|
package/src/kernel/approval.ts
CHANGED
|
@@ -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;
|
package/src/kernel/delegate.ts
CHANGED
|
@@ -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.
|
package/src/kernel/env-names.ts
CHANGED
|
@@ -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
|
+
}
|