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.
- package/CHANGELOG.md +8 -0
- package/README.md +41 -11
- package/contracts/ledger-record/v1/governance-event.schema.json +65 -3
- package/dist/governance/approval-prompt.d.ts +4 -0
- package/dist/governance/approval-prompt.d.ts.map +1 -1
- package/dist/governance/approval-prompt.js +59 -4
- 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 +17 -3
- package/dist/governance/ledger-v3-validation.js.map +1 -1
- package/dist/governance/ledger.d.ts +1 -1
- package/dist/governance/ledger.d.ts.map +1 -1
- package/dist/governance/ledger.js +1 -1
- 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/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/grants-command.ts +33 -3
- package/extensions/grants.ts +28 -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 +62 -4
- 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 +17 -3
- package/src/governance/ledger.ts +1 -0
- 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
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { ENV_AUTO_MODE, ENV_AUTO_MODE_REF } from "../src/kernel/env-names.ts";
|
|
1
2
|
import type { OwnerCapacity } from "./session-capacity.ts";
|
|
2
3
|
import { GRANT_ENV_KEYS } from "../src/kernel/propagation.ts";
|
|
3
4
|
import type { BoundedReadCleanupError } from "../src/kernel/bounded-read.ts";
|
|
4
5
|
|
|
5
6
|
/** The root baseline and latest child publication for one real Pi session owner. */
|
|
6
7
|
export interface ReloadLifecycle {
|
|
8
|
+
autoMode?: import("./session-auto-mode.ts").SessionAutoLifecycle;
|
|
7
9
|
runtimeSnapshotUnsubscribe?: () => void;
|
|
8
10
|
runtimeSnapshotGeneration?: object;
|
|
9
11
|
runtimeSettlements?: Map<string, import("../src/governance/runtime-settlement.ts").RuntimeSettlement>;
|
|
@@ -59,7 +61,19 @@ function withRoot(root: Record<string, string | undefined>): NodeJS.ProcessEnv {
|
|
|
59
61
|
|
|
60
62
|
/** Factory-time state is provisional: ExtensionAPI is recreated before the stable session owner is known. */
|
|
61
63
|
export function beginExtensionLifecycle(): { environment: NodeJS.ProcessEnv; lifecycle: ReloadLifecycle } {
|
|
62
|
-
|
|
64
|
+
const root = snapshot();
|
|
65
|
+
const publication = state().latestChildPublication;
|
|
66
|
+
if (
|
|
67
|
+
publication?.environment[ENV_AUTO_MODE_REF] &&
|
|
68
|
+
root[ENV_AUTO_MODE_REF] === publication.environment[ENV_AUTO_MODE_REF]
|
|
69
|
+
) {
|
|
70
|
+
// Recognize our own published Auto reference independently of unrelated grant setting changes.
|
|
71
|
+
// A new SDK owner must not adopt another local owner; a real descendant has no local publication.
|
|
72
|
+
if (root[ENV_AUTO_MODE] === publication.environment[ENV_AUTO_MODE])
|
|
73
|
+
root[ENV_AUTO_MODE] = publication.lifecycle.root[ENV_AUTO_MODE];
|
|
74
|
+
root[ENV_AUTO_MODE_REF] = publication.lifecycle.root[ENV_AUTO_MODE_REF];
|
|
75
|
+
}
|
|
76
|
+
return { environment: withRoot(root), lifecycle: { root } };
|
|
63
77
|
}
|
|
64
78
|
|
|
65
79
|
/**
|
|
@@ -100,6 +114,7 @@ export function bindReloadLifecycle(
|
|
|
100
114
|
// Pending old-root operations keep THEIR original owner; they cannot publish/fault this replacement.
|
|
101
115
|
existing = {
|
|
102
116
|
root: current,
|
|
117
|
+
autoMode: existing.autoMode,
|
|
103
118
|
activityRootId: existing.activityRootId,
|
|
104
119
|
episodeId: existing.episodeId,
|
|
105
120
|
capacity: existing.capacity,
|
|
@@ -79,6 +79,13 @@ export interface GatedPlan {
|
|
|
79
79
|
approval?: ApprovalOutcome;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/** Discard provisional Auto facts when OFF or a policy error requires a fresh decision. */
|
|
83
|
+
function forgetAutoApprovals(outcome: ApprovalOutcome, capabilities: string[]): void {
|
|
84
|
+
outcome.approved = outcome.approved.filter((capability) => !capabilities.includes(capability));
|
|
85
|
+
for (const field of ["sources", "scopes", "recordedScopes", "bindings", "expiresAt", "uses"] as const)
|
|
86
|
+
for (const capability of capabilities) delete outcome[field][capability];
|
|
87
|
+
}
|
|
88
|
+
|
|
82
89
|
/**
|
|
83
90
|
* Plan a delegation and satisfy its gate as far as approvals allow.
|
|
84
91
|
*
|
|
@@ -143,12 +150,13 @@ export async function planWithApprovals(
|
|
|
143
150
|
plan.approvalBinding,
|
|
144
151
|
);
|
|
145
152
|
const outcome = approval;
|
|
146
|
-
|
|
147
|
-
|
|
153
|
+
while (outcome.approved.length > 0) {
|
|
154
|
+
const context = {
|
|
148
155
|
// The scope is the REAL one: a `once` approval still authorises this spawn, and
|
|
149
156
|
// `inheritApprovals` then keeps it from reaching the child. See ADR-0014. R-29 is what makes
|
|
150
157
|
// this safe under fan-out: a `once` is consumed by exactly one concurrent caller.
|
|
151
158
|
...(await session.delegationContext([
|
|
159
|
+
...(preApproved ?? []),
|
|
152
160
|
...republishable(session),
|
|
153
161
|
...outcome.approved.map((capability) => ({
|
|
154
162
|
capability,
|
|
@@ -167,7 +175,35 @@ export async function planWithApprovals(
|
|
|
167
175
|
})),
|
|
168
176
|
])),
|
|
169
177
|
...extra,
|
|
170
|
-
}
|
|
178
|
+
};
|
|
179
|
+
const automatic = outcome.approved.filter((capability) => outcome.sources[capability] === "auto");
|
|
180
|
+
// The owner serializes this check with OFF. Permission is admitted here, after context preparation;
|
|
181
|
+
// a later OFF does not cancel a delegation already admitted to its load-bearing ledger write.
|
|
182
|
+
if (automatic.length > 0 && !(await session.autoMode?.admit(signal))) {
|
|
183
|
+
const renewed = await obtainApprovals(
|
|
184
|
+
session,
|
|
185
|
+
automatic,
|
|
186
|
+
approvalSubject,
|
|
187
|
+
request.agent ? "definition" : "delegate",
|
|
188
|
+
ctx,
|
|
189
|
+
request.task,
|
|
190
|
+
signal,
|
|
191
|
+
plan.approvalBinding,
|
|
192
|
+
);
|
|
193
|
+
forgetAutoApprovals(outcome, automatic);
|
|
194
|
+
outcome.approved.push(...renewed.approved);
|
|
195
|
+
for (const field of ["sources", "scopes", "recordedScopes", "bindings", "expiresAt", "uses"] as const)
|
|
196
|
+
Object.assign(outcome[field], renewed[field]);
|
|
197
|
+
outcome.banked = [...(outcome.banked ?? []), ...(renewed.banked ?? [])];
|
|
198
|
+
outcome.humanDenied ||= renewed.humanDenied;
|
|
199
|
+
outcome.gateOutcome = renewed.gateOutcome;
|
|
200
|
+
outcome.refusalCode = renewed.refusalCode;
|
|
201
|
+
outcome.reason = renewed.reason;
|
|
202
|
+
if (automatic.some((capability) => !renewed.approved.includes(capability))) break;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
plan = observedPlan(context);
|
|
206
|
+
break;
|
|
171
207
|
}
|
|
172
208
|
if (!plan.ok && approval.reason) {
|
|
173
209
|
plan = {
|
|
@@ -178,6 +214,16 @@ export async function planWithApprovals(
|
|
|
178
214
|
}
|
|
179
215
|
} catch (error) {
|
|
180
216
|
const message = `grants: approval flow failed, denying (${String(error)})`;
|
|
217
|
+
if (approval) {
|
|
218
|
+
const failed = approval;
|
|
219
|
+
forgetAutoApprovals(
|
|
220
|
+
failed,
|
|
221
|
+
failed.approved.filter((capability) => failed.sources[capability] === "auto"),
|
|
222
|
+
);
|
|
223
|
+
approval.gateOutcome = "error";
|
|
224
|
+
approval.refusalCode = "APPROVAL_FLOW_FAILED";
|
|
225
|
+
approval.reason = message;
|
|
226
|
+
}
|
|
181
227
|
plan = { ...plan, reason: message, refusal: structuredRefusal("APPROVAL_FLOW_FAILED", message) };
|
|
182
228
|
}
|
|
183
229
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/** Session-owned Auto policy. Dashboard mutations never become banked or inherited approvals. */
|
|
2
|
+
import type { AutoModeReader, AutoModeSource } from "../src/kernel/auto-mode.ts";
|
|
3
|
+
import { parseAutoModeDefault, parseAutoModeRef } from "../src/kernel/auto-mode.ts";
|
|
4
|
+
import { ENV_AUTO_MODE, ENV_AUTO_MODE_REF } from "../src/kernel/env-names.ts";
|
|
5
|
+
import {
|
|
6
|
+
createAutoModeAuthority,
|
|
7
|
+
connectAutoMode,
|
|
8
|
+
type AutoModeAuthority,
|
|
9
|
+
} from "../src/governance/auto-mode-policy.ts";
|
|
10
|
+
import { appendLedgerEvent, buildAutoModeConfigEvent } from "../src/governance/ledger.ts";
|
|
11
|
+
import type { GrantsSession } from "./session.ts";
|
|
12
|
+
|
|
13
|
+
export interface SessionAutoLifecycle {
|
|
14
|
+
nativeSessionId: string;
|
|
15
|
+
reader: AutoModeReader;
|
|
16
|
+
authority?: AutoModeAuthority;
|
|
17
|
+
updates: Promise<unknown>;
|
|
18
|
+
}
|
|
19
|
+
const stale = () => new Error("Daddy Auto controls are unavailable for this session owner");
|
|
20
|
+
function current(session: GrantsSession): SessionAutoLifecycle {
|
|
21
|
+
const state = session.reloadLifecycle.autoMode;
|
|
22
|
+
if (!session.ownerBound || !state || session.autoMode !== state.reader) throw stale();
|
|
23
|
+
return state;
|
|
24
|
+
}
|
|
25
|
+
async function record(
|
|
26
|
+
session: GrantsSession,
|
|
27
|
+
enabled: boolean,
|
|
28
|
+
source: AutoModeSource,
|
|
29
|
+
revision: number,
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
if (!session.ledgerPath) return;
|
|
32
|
+
await appendLedgerEvent(
|
|
33
|
+
{ path: session.ledgerPath, strict: true },
|
|
34
|
+
buildAutoModeConfigEvent({
|
|
35
|
+
episodeId: session.episodeId,
|
|
36
|
+
enabled,
|
|
37
|
+
source,
|
|
38
|
+
revision,
|
|
39
|
+
now: new Date(),
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function initializeSessionAutoMode(
|
|
45
|
+
session: GrantsSession,
|
|
46
|
+
environment: NodeJS.ProcessEnv,
|
|
47
|
+
nativeSessionId: string,
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
const lifecycle = session.reloadLifecycle;
|
|
50
|
+
let state = lifecycle.autoMode;
|
|
51
|
+
if (session.autoMode && session.autoMode !== state?.reader) await session.autoMode.close();
|
|
52
|
+
if (state && state.nativeSessionId !== nativeSessionId) {
|
|
53
|
+
await state.reader.close();
|
|
54
|
+
delete lifecycle.autoMode;
|
|
55
|
+
state = undefined;
|
|
56
|
+
}
|
|
57
|
+
if (state) {
|
|
58
|
+
session.autoMode = state.reader;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const rawRef = environment[ENV_AUTO_MODE_REF];
|
|
62
|
+
if (rawRef !== undefined) {
|
|
63
|
+
// A descendant always consults its actual owner. A dead/malformed owner must not mint a new root.
|
|
64
|
+
const reader = connectAutoMode(parseAutoModeRef(rawRef));
|
|
65
|
+
try {
|
|
66
|
+
await reader.read();
|
|
67
|
+
} catch (error) {
|
|
68
|
+
await reader.close();
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
state = { nativeSessionId, reader, updates: Promise.resolve() };
|
|
72
|
+
} else {
|
|
73
|
+
const initial = parseAutoModeDefault(environment[ENV_AUTO_MODE]);
|
|
74
|
+
const authority = await createAutoModeAuthority({ enabled: false, source: initial.source });
|
|
75
|
+
try {
|
|
76
|
+
if (initial.enabled) {
|
|
77
|
+
await record(session, true, initial.source, 1);
|
|
78
|
+
authority.set(true, initial.source);
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
await authority.close();
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
state = { nativeSessionId, reader: authority, authority, updates: Promise.resolve() };
|
|
85
|
+
}
|
|
86
|
+
if (session.reloadLifecycle !== lifecycle) {
|
|
87
|
+
await state.reader.close();
|
|
88
|
+
throw stale();
|
|
89
|
+
}
|
|
90
|
+
lifecycle.autoMode = state;
|
|
91
|
+
session.autoMode = state.reader;
|
|
92
|
+
delete session.autoModeFailure;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function getDashboardControlState(session: GrantsSession) {
|
|
96
|
+
const state = current(session);
|
|
97
|
+
const value = await state.reader.read();
|
|
98
|
+
if (current(session) !== state) throw stale();
|
|
99
|
+
return { auto: { enabled: value.enabled, source: value.source }, pendingApprovals: value.pendingApprovals };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function setAutoApproval(session: GrantsSession, enabled: boolean, source: "dashboard" | "command") {
|
|
103
|
+
if (typeof enabled !== "boolean" || (source !== "dashboard" && source !== "command"))
|
|
104
|
+
throw new Error("Invalid Daddy Auto mode edit");
|
|
105
|
+
const state = current(session);
|
|
106
|
+
if (!state.authority) throw new Error("Only the owning root session may change Daddy Auto mode");
|
|
107
|
+
// Serialize operator edits. OFF remains effective even if its audit write fails; failed ON never enables.
|
|
108
|
+
const update = state.updates
|
|
109
|
+
.catch(() => {})
|
|
110
|
+
.then(async () => {
|
|
111
|
+
if (current(session) !== state) throw stale();
|
|
112
|
+
const before = state.authority!.snapshot();
|
|
113
|
+
if (enabled) {
|
|
114
|
+
await record(session, true, "session", before.revision + 1);
|
|
115
|
+
if (current(session) !== state) throw stale();
|
|
116
|
+
state.authority!.set(true, "session");
|
|
117
|
+
} else {
|
|
118
|
+
const after = state.authority!.set(false, "session");
|
|
119
|
+
await record(session, false, "session", after.revision);
|
|
120
|
+
}
|
|
121
|
+
return getDashboardControlState(session);
|
|
122
|
+
});
|
|
123
|
+
state.updates = update;
|
|
124
|
+
return update;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function closeSessionAutoMode(session: GrantsSession): Promise<void> {
|
|
128
|
+
const reader = session.autoMode;
|
|
129
|
+
delete session.autoMode;
|
|
130
|
+
if (reader && session.reloadLifecycle.autoMode?.reader === reader) delete session.reloadLifecycle.autoMode;
|
|
131
|
+
await reader?.close();
|
|
132
|
+
}
|
package/extensions/session.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { EcosystemVersions } from "../src/products/ecosystem-versions.ts";
|
|
2
|
+
import type { AutoModeReader } from "../src/kernel/auto-mode.ts";
|
|
1
3
|
/**
|
|
2
4
|
* The session object — the one place this extension's state is named.
|
|
3
5
|
*
|
|
@@ -141,6 +143,9 @@ export function activityChildEnv(activity: { rootId: string; path: string; taskI
|
|
|
141
143
|
}
|
|
142
144
|
/** Keep each child's pane after it finishes, for inspection. Off by default: fan-out would flood it. */
|
|
143
145
|
export interface GrantsSession extends NativeSessionHost {
|
|
146
|
+
ecosystemVersions?: () => EcosystemVersions;
|
|
147
|
+
autoMode?: AutoModeReader;
|
|
148
|
+
autoModeFailure?: string;
|
|
144
149
|
/** Reconciled from the actual owner root on reload; captured call owners stay immutable. */
|
|
145
150
|
publicEvidence?: PublicEvidenceOwner;
|
|
146
151
|
/** Legacy PI_GRANTS_* names adopted at construction (ADR-0076 PR 3b); the session-start warning names them. */
|
|
@@ -659,43 +664,47 @@ export function createGrantsSession(
|
|
|
659
664
|
discovery: Object.freeze({ ...discovery }),
|
|
660
665
|
catalog: emptyCatalog,
|
|
661
666
|
catalogReady: Promise.resolve(emptyCatalog),
|
|
662
|
-
delegationContext: async (approved?: InheritableApproval[]) =>
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
667
|
+
delegationContext: async (approved?: InheritableApproval[]) => {
|
|
668
|
+
if (session.autoModeFailure) throw new Error(session.autoModeFailure);
|
|
669
|
+
return {
|
|
670
|
+
autoModeRef: session.autoMode?.reference,
|
|
671
|
+
ownGrant: session.ownGrant,
|
|
672
|
+
episodeId: session.episodeId,
|
|
673
|
+
depth: session.depth,
|
|
674
|
+
maxDepth: session.maxDepth,
|
|
675
|
+
gated: session.gated,
|
|
676
|
+
ledgerPath: session.ledgerPath,
|
|
677
|
+
extensionPath: session.extensionPath,
|
|
678
|
+
observerExtensionPath: session.observerExtensionPath,
|
|
679
|
+
childEnv: activityChildEnv(session.activity),
|
|
680
|
+
// ADR-0042: the pin this session holds, handed to the kernel so a delegated child inherits a narrowed
|
|
681
|
+
// one through the same builder `publishChildEnv` uses. Read here rather than in the kernel so there is
|
|
682
|
+
// one place that knows the environment is where a session's own pin lives.
|
|
683
|
+
...(session.workspacePin ? { workspacePin: session.workspacePin } : {}),
|
|
684
|
+
// Context is staged only after its capability survives the ceiling, grant and gate.
|
|
685
|
+
stageHandoff: (granted, options) =>
|
|
686
|
+
createHandoffStager({
|
|
687
|
+
cwd: session.cwd,
|
|
688
|
+
forkRoot: join(agentDir(), "context-forks"),
|
|
689
|
+
...(session.parentSession ? { parentSession: session.parentSession } : {}),
|
|
690
|
+
})(granted, options),
|
|
691
|
+
catalog: await delegationCatalog(session),
|
|
692
|
+
// R-32: where each granted skill lives, so `planSpawn` can pass `--skill` for those and only those.
|
|
693
|
+
// Derived from the catalog's own `source`, so it cannot drift from what was discovered.
|
|
694
|
+
skillPaths: skillPathsFromCatalog(await delegationCatalog(session)),
|
|
695
|
+
// ADR-0016: operator-authored SKILL.md definitions, so `delegate({agent})` can name one.
|
|
696
|
+
definitions: session.definitions,
|
|
697
|
+
// The herdr executor drives the child after starting it, so its plan must NOT carry `--print`.
|
|
698
|
+
// Threaded through the plan rather than patched afterwards: the argv is what the ledger records, and
|
|
699
|
+
// an executor quietly rewriting it would make the record describe a spawn that did not happen.
|
|
700
|
+
//
|
|
701
|
+
// Read live off `session.executor` (ADR-0031) rather than a boolean captured in the factory: the probe
|
|
702
|
+
// has not run when this session object is built, so a captured value would plan `--print` for a session
|
|
703
|
+
// that turns out to use panes — and `runHerdrPane` refuses a plan containing `--print` by design.
|
|
704
|
+
interactive: false, // Both qualified backends use the same one-shot Pi JSON protocol.
|
|
705
|
+
...(approved ? { approved } : {}),
|
|
706
|
+
};
|
|
707
|
+
},
|
|
699
708
|
storeCwd,
|
|
700
709
|
...(grantStoreRefusal ? { grantStoreRefusal } : {}),
|
|
701
710
|
adoptGrant: (grant: Capability[], projectLedger?: string) => {
|
|
@@ -725,6 +734,7 @@ export function createGrantsSession(
|
|
|
725
734
|
},
|
|
726
735
|
publishChildEnv: () => {
|
|
727
736
|
const env = childEnv({
|
|
737
|
+
autoModeRef: session.autoMode?.reference,
|
|
728
738
|
ownGrant: session.ownGrant,
|
|
729
739
|
episodeId: session.episodeId,
|
|
730
740
|
depth: session.depth,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-daddy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"description": "Capability governance for pi sub-agents: spawn Agent Skills (SKILL.md) definitions whose allowed-tools becomes a grant that can only narrow going down a delegation tree, enforced by pi's own --tools allowlist, with an append-only ledger.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"typecheck": "tsc -p tsconfig.check.json",
|
|
54
54
|
"test": "node scripts/unit-tests.ts",
|
|
55
55
|
"test:integration": "node --test test-integration/*.it.ts",
|
|
56
|
-
"test:integration:ci": "node --test test-integration/approval.it.ts test-integration/governance.it.ts test-integration/activity.it.ts",
|
|
56
|
+
"test:integration:ci": "node --test test-integration/approval.it.ts test-integration/governance.it.ts test-integration/activity.it.ts test-integration/auto-chain.it.ts",
|
|
57
57
|
"contracts:generate": "node scripts/generate-ledger-record-contract.ts",
|
|
58
58
|
"prepack": "npm run build",
|
|
59
59
|
"test:smoke": "node scripts/smoke-installed.mjs",
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* no-op UI context whose `select` resolves undefined, so even a missed `hasUI` check would deny.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import type { AutoModeReader } from "../kernel/auto-mode.ts";
|
|
13
15
|
import { approvalKey, offeredScopes, type ApprovalPath, type ApprovalScope } from "../kernel/approval.ts";
|
|
14
16
|
import type { Capability } from "../kernel/resolve.ts";
|
|
15
17
|
|
|
@@ -72,6 +74,8 @@ export interface PromptOutcome {
|
|
|
72
74
|
* one. The rider's honest source is `session`, and this flag is how the caller can tell.
|
|
73
75
|
*/
|
|
74
76
|
joined?: boolean;
|
|
77
|
+
/** Provisional Auto permission; the admission caller must still check the live owner policy. */
|
|
78
|
+
source?: "auto";
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
export interface ApprovalGateOptions {
|
|
@@ -82,6 +86,7 @@ export interface ApprovalGateOptions {
|
|
|
82
86
|
mode: string;
|
|
83
87
|
/** Resolve only when this caller must open a new dialog; unused invalid settings cannot revoke authority. */
|
|
84
88
|
timeoutMs?: number | (() => number | undefined);
|
|
89
|
+
autoMode?: AutoModeReader;
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
export interface ApprovalGate {
|
|
@@ -106,6 +111,8 @@ function labelToScope(label: string, scopes: ApprovalScope[]): ApprovalScope | n
|
|
|
106
111
|
return scopes.find((s) => SCOPE_LABELS[s] === label) ?? null;
|
|
107
112
|
}
|
|
108
113
|
|
|
114
|
+
const autoApproval = (): PromptOutcome => ({ scope: "once", kind: "granted", source: "auto" });
|
|
115
|
+
|
|
109
116
|
const interrupted = (request: PromptRequest, kind: "expired" | "aborted"): PromptOutcome => ({
|
|
110
117
|
scope: null,
|
|
111
118
|
kind,
|
|
@@ -124,26 +131,61 @@ async function selectApproval(
|
|
|
124
131
|
const timeout = typeof options.timeoutMs === "function" ? options.timeoutMs() : options.timeoutMs;
|
|
125
132
|
const controller = new AbortController();
|
|
126
133
|
let outcome: PromptOutcome | undefined;
|
|
134
|
+
let settled = false;
|
|
127
135
|
let wake!: (value: PromptOutcome) => void;
|
|
128
136
|
const stopped = new Promise<PromptOutcome>((resolve) => {
|
|
129
137
|
wake = resolve;
|
|
130
138
|
});
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
133
|
-
|
|
139
|
+
const settle = (next: PromptOutcome) => {
|
|
140
|
+
if (settled) return;
|
|
141
|
+
settled = true;
|
|
142
|
+
outcome = next;
|
|
134
143
|
// Resolve our tagged outcome before a UI reacts to abort by returning undefined.
|
|
135
144
|
wake(outcome);
|
|
136
145
|
controller.abort();
|
|
137
146
|
};
|
|
147
|
+
const stop = (kind: "expired" | "aborted") => settle(interrupted(request, kind));
|
|
138
148
|
const abort = () => stop("aborted");
|
|
139
149
|
request.signal?.addEventListener("abort", abort, { once: true });
|
|
140
150
|
const timer = timeout === undefined ? undefined : setTimeout(() => stop("expired"), timeout);
|
|
151
|
+
let untrack: (() => void) | undefined;
|
|
141
152
|
try {
|
|
142
153
|
if (request.signal?.aborted) stop("aborted");
|
|
143
154
|
if (outcome) return outcome;
|
|
144
|
-
|
|
155
|
+
untrack = options.autoMode?.trackPending({
|
|
156
|
+
id: randomUUID(),
|
|
157
|
+
subject: request.subject,
|
|
158
|
+
capability: request.capability,
|
|
159
|
+
});
|
|
160
|
+
const selection = options.ui.select(title, labels, { signal: controller.signal }).then((chosen) => {
|
|
161
|
+
settled = true;
|
|
162
|
+
return chosen;
|
|
163
|
+
});
|
|
164
|
+
if (options.autoMode) {
|
|
165
|
+
const auto = options.autoMode;
|
|
166
|
+
void (async () => {
|
|
167
|
+
while (!controller.signal.aborted) {
|
|
168
|
+
await auto.waitEnabled(controller.signal);
|
|
169
|
+
// A notification is not authority: ON may already have changed back to OFF.
|
|
170
|
+
if (!controller.signal.aborted && (await auto.read()).enabled) {
|
|
171
|
+
settle(autoApproval());
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
})().catch((error) => {
|
|
176
|
+
if (!controller.signal.aborted)
|
|
177
|
+
settle({
|
|
178
|
+
scope: null,
|
|
179
|
+
kind: "error",
|
|
180
|
+
reason: `Auto permission policy unavailable, denying (${String(error)})`,
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
const chosen = await Promise.race([selection, stopped]);
|
|
145
185
|
return outcome ?? chosen;
|
|
146
186
|
} finally {
|
|
187
|
+
controller.abort();
|
|
188
|
+
untrack?.();
|
|
147
189
|
if (timer !== undefined) clearTimeout(timer);
|
|
148
190
|
request.signal?.removeEventListener("abort", abort);
|
|
149
191
|
}
|
|
@@ -211,6 +253,15 @@ export function createApprovalGate(
|
|
|
211
253
|
inFlight: InFlightApprovals = new Map(),
|
|
212
254
|
): ApprovalGate {
|
|
213
255
|
const ask = async (request: PromptRequest): Promise<PromptOutcome> => {
|
|
256
|
+
// A queued once waiter owns its own decision. Auto may have changed while it joined another dialog.
|
|
257
|
+
if (options.autoMode) {
|
|
258
|
+
try {
|
|
259
|
+
if ((await options.autoMode.read()).enabled)
|
|
260
|
+
return request.signal?.aborted ? interrupted(request, "aborted") : autoApproval();
|
|
261
|
+
} catch (error) {
|
|
262
|
+
return { scope: null, kind: "error", reason: `Auto permission policy unavailable, denying (${String(error)})` };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
214
265
|
const scopes = offeredScopes(request.path);
|
|
215
266
|
const title =
|
|
216
267
|
`grants: approve ${request.capability} for ${request.subject}?` +
|
|
@@ -251,6 +302,13 @@ export function createApprovalGate(
|
|
|
251
302
|
return {
|
|
252
303
|
async request(request: PromptRequest): Promise<PromptOutcome> {
|
|
253
304
|
if (request.signal?.aborted) return interrupted(request, "aborted");
|
|
305
|
+
try {
|
|
306
|
+
if (options.autoMode && (await options.autoMode.read()).enabled) {
|
|
307
|
+
return request.signal?.aborted ? interrupted(request, "aborted") : autoApproval();
|
|
308
|
+
}
|
|
309
|
+
} catch (error) {
|
|
310
|
+
return { scope: null, kind: "error", reason: `Auto permission policy unavailable, denying (${String(error)})` };
|
|
311
|
+
}
|
|
254
312
|
if (!options.hasUI) {
|
|
255
313
|
// Nobody was there to ask — distinct from a person declining, which is why this is its own kind
|
|
256
314
|
// rather than being folded into "declined" (see PromptOutcomeKind).
|