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
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
|
|
|
@@ -43,21 +45,22 @@ export interface PromptRequest {
|
|
|
43
45
|
signal?: AbortSignal;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
/** Only declined means a human chose Deny. Expiry, cancellation and dismissal grant nothing. */
|
|
49
|
+
export const PROMPT_OUTCOME_KINDS = [
|
|
50
|
+
"granted",
|
|
51
|
+
"declined",
|
|
52
|
+
"dismissed",
|
|
53
|
+
"expired",
|
|
54
|
+
"aborted",
|
|
55
|
+
"no-ui",
|
|
56
|
+
"error",
|
|
57
|
+
] as const;
|
|
55
58
|
export type PromptOutcomeKind = (typeof PROMPT_OUTCOME_KINDS)[number];
|
|
56
59
|
|
|
57
60
|
export interface PromptOutcome {
|
|
58
61
|
/** The scope the human chose, or null for any form of no. */
|
|
59
62
|
scope: ApprovalScope | null;
|
|
60
|
-
/** Which
|
|
63
|
+
/** Which outcome this was. Set on every return path. */
|
|
61
64
|
kind: PromptOutcomeKind;
|
|
62
65
|
/** Why, when the answer was no. */
|
|
63
66
|
reason?: string;
|
|
@@ -71,6 +74,8 @@ export interface PromptOutcome {
|
|
|
71
74
|
* one. The rider's honest source is `session`, and this flag is how the caller can tell.
|
|
72
75
|
*/
|
|
73
76
|
joined?: boolean;
|
|
77
|
+
/** Provisional Auto permission; the admission caller must still check the live owner policy. */
|
|
78
|
+
source?: "auto";
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
export interface ApprovalGateOptions {
|
|
@@ -81,23 +86,21 @@ export interface ApprovalGateOptions {
|
|
|
81
86
|
mode: string;
|
|
82
87
|
/** Resolve only when this caller must open a new dialog; unused invalid settings cannot revoke authority. */
|
|
83
88
|
timeoutMs?: number | (() => number | undefined);
|
|
89
|
+
autoMode?: AutoModeReader;
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
export interface ApprovalGate {
|
|
87
93
|
request(request: PromptRequest): Promise<PromptOutcome>;
|
|
88
94
|
}
|
|
89
95
|
|
|
90
|
-
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
91
|
-
|
|
92
96
|
/**
|
|
93
97
|
* Read `PI_DADDY_APPROVAL_TIMEOUT`, in SECONDS, into the milliseconds pi expects.
|
|
94
98
|
*
|
|
95
|
-
*
|
|
99
|
+
* Absent or literal `0` waits for the human without a deadline. Other values must be canonical whole seconds
|
|
96
100
|
* within Node's signed 32-bit millisecond timer limit; malformed settings refuse a needed prompt.
|
|
97
101
|
*/
|
|
98
102
|
export function timeoutMsFromEnv(raw: string | undefined): number | undefined {
|
|
99
|
-
if (raw === undefined) return
|
|
100
|
-
if (raw === "0") return undefined;
|
|
103
|
+
if (raw === undefined || raw === "0") return undefined;
|
|
101
104
|
if (!/^[1-9][0-9]*$/.test(raw) || Number(raw) > 2_147_483)
|
|
102
105
|
throw new Error(
|
|
103
106
|
"PI_DADDY_APPROVAL_TIMEOUT must be literal 0 or whole seconds 1..2147483 without signs, padding or units",
|
|
@@ -108,6 +111,101 @@ function labelToScope(label: string, scopes: ApprovalScope[]): ApprovalScope | n
|
|
|
108
111
|
return scopes.find((s) => SCOPE_LABELS[s] === label) ?? null;
|
|
109
112
|
}
|
|
110
113
|
|
|
114
|
+
const autoApproval = (): PromptOutcome => ({ scope: "once", kind: "granted", source: "auto" });
|
|
115
|
+
|
|
116
|
+
const interrupted = (request: PromptRequest, kind: "expired" | "aborted"): PromptOutcome => ({
|
|
117
|
+
scope: null,
|
|
118
|
+
kind,
|
|
119
|
+
reason:
|
|
120
|
+
`approval for ${request.capability} ${kind === "expired" ? "expired before an answer" : "was canceled"}. ` +
|
|
121
|
+
"No permission was granted. Retry the delegation to open a new approval prompt.",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
/** Own the deadline so Pi's undefined dismissal cannot erase whether our timer or cancellation fired. */
|
|
125
|
+
async function selectApproval(
|
|
126
|
+
options: ApprovalGateOptions,
|
|
127
|
+
request: PromptRequest,
|
|
128
|
+
title: string,
|
|
129
|
+
labels: string[],
|
|
130
|
+
): Promise<string | undefined | PromptOutcome> {
|
|
131
|
+
const timeout = typeof options.timeoutMs === "function" ? options.timeoutMs() : options.timeoutMs;
|
|
132
|
+
const controller = new AbortController();
|
|
133
|
+
let outcome: PromptOutcome | undefined;
|
|
134
|
+
let settled = false;
|
|
135
|
+
let wake!: (value: PromptOutcome) => void;
|
|
136
|
+
const stopped = new Promise<PromptOutcome>((resolve) => {
|
|
137
|
+
wake = resolve;
|
|
138
|
+
});
|
|
139
|
+
const settle = (next: PromptOutcome) => {
|
|
140
|
+
if (settled) return;
|
|
141
|
+
settled = true;
|
|
142
|
+
outcome = next;
|
|
143
|
+
// Resolve our tagged outcome before a UI reacts to abort by returning undefined.
|
|
144
|
+
wake(outcome);
|
|
145
|
+
controller.abort();
|
|
146
|
+
};
|
|
147
|
+
const stop = (kind: "expired" | "aborted") => settle(interrupted(request, kind));
|
|
148
|
+
const abort = () => stop("aborted");
|
|
149
|
+
request.signal?.addEventListener("abort", abort, { once: true });
|
|
150
|
+
const timer = timeout === undefined ? undefined : setTimeout(() => stop("expired"), timeout);
|
|
151
|
+
let untrack: (() => void) | undefined;
|
|
152
|
+
try {
|
|
153
|
+
if (request.signal?.aborted) stop("aborted");
|
|
154
|
+
if (outcome) return outcome;
|
|
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]);
|
|
185
|
+
return outcome ?? chosen;
|
|
186
|
+
} finally {
|
|
187
|
+
controller.abort();
|
|
188
|
+
untrack?.();
|
|
189
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
190
|
+
request.signal?.removeEventListener("abort", abort);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function joinApproval(pending: Promise<PromptOutcome>, request: PromptRequest): Promise<PromptOutcome> {
|
|
195
|
+
if (!request.signal) return pending;
|
|
196
|
+
let abort!: () => void;
|
|
197
|
+
const stopped = new Promise<PromptOutcome>((resolve) => {
|
|
198
|
+
abort = () => resolve(interrupted(request, "aborted"));
|
|
199
|
+
request.signal!.addEventListener("abort", abort, { once: true });
|
|
200
|
+
if (request.signal!.aborted) abort();
|
|
201
|
+
});
|
|
202
|
+
try {
|
|
203
|
+
return await Promise.race([pending, stopped]);
|
|
204
|
+
} finally {
|
|
205
|
+
request.signal.removeEventListener("abort", abort);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
111
209
|
/**
|
|
112
210
|
* The single-flight queue: one dialog per approval key at a time.
|
|
113
211
|
*
|
|
@@ -155,29 +253,41 @@ export function createApprovalGate(
|
|
|
155
253
|
inFlight: InFlightApprovals = new Map(),
|
|
156
254
|
): ApprovalGate {
|
|
157
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
|
+
}
|
|
158
265
|
const scopes = offeredScopes(request.path);
|
|
159
266
|
const title =
|
|
160
267
|
`grants: approve ${request.capability} for ${request.subject}?` +
|
|
161
268
|
(request.task ? `\n task: ${request.task}` : "");
|
|
162
269
|
|
|
163
|
-
let chosen: string | undefined;
|
|
270
|
+
let chosen: string | undefined | PromptOutcome;
|
|
164
271
|
try {
|
|
165
|
-
chosen = await options
|
|
166
|
-
timeout: typeof options.timeoutMs === "function" ? options.timeoutMs() : options.timeoutMs,
|
|
167
|
-
signal: request.signal,
|
|
168
|
-
});
|
|
272
|
+
chosen = await selectApproval(options, request, title, [DENY_LABEL, ...scopes.map((s) => SCOPE_LABELS[s])]);
|
|
169
273
|
} catch (error) {
|
|
170
274
|
// A governance layer that errors must deny, not permit. This is a dialog malfunction, not a
|
|
171
275
|
// person's answer — `kind: "error"` keeps it out of "declined".
|
|
172
276
|
return { scope: null, kind: "error", reason: `approval dialog failed, denying (${String(error)})` };
|
|
173
277
|
}
|
|
174
278
|
|
|
175
|
-
if (request.signal?.aborted)
|
|
176
|
-
return { scope: null, kind: "dismissed", reason: `approval for ${request.capability} was canceled` };
|
|
279
|
+
if (request.signal?.aborted) return interrupted(request, "aborted");
|
|
177
280
|
|
|
178
|
-
|
|
281
|
+
if (typeof chosen === "object") return chosen;
|
|
282
|
+
// Only an ordinary UI dismissal reaches undefined; our deadline and abort carry explicit outcomes.
|
|
179
283
|
if (chosen === undefined) {
|
|
180
|
-
return {
|
|
284
|
+
return {
|
|
285
|
+
scope: null,
|
|
286
|
+
kind: "dismissed",
|
|
287
|
+
reason:
|
|
288
|
+
`approval for ${request.capability} was dismissed. No permission was granted. ` +
|
|
289
|
+
"Retry the delegation to open a new approval prompt.",
|
|
290
|
+
};
|
|
181
291
|
}
|
|
182
292
|
if (chosen === DENY_LABEL) {
|
|
183
293
|
return { scope: null, kind: "declined", reason: `${request.capability} was denied by a human` };
|
|
@@ -191,8 +301,14 @@ export function createApprovalGate(
|
|
|
191
301
|
|
|
192
302
|
return {
|
|
193
303
|
async request(request: PromptRequest): Promise<PromptOutcome> {
|
|
194
|
-
if (request.signal?.aborted)
|
|
195
|
-
|
|
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
|
+
}
|
|
196
312
|
if (!options.hasUI) {
|
|
197
313
|
// Nobody was there to ask — distinct from a person declining, which is why this is its own kind
|
|
198
314
|
// rather than being folded into "declined" (see PromptOutcomeKind).
|
|
@@ -215,9 +331,8 @@ export function createApprovalGate(
|
|
|
215
331
|
for (;;) {
|
|
216
332
|
const existing = inFlight.get(key);
|
|
217
333
|
if (!existing) break;
|
|
218
|
-
const outcome = await existing;
|
|
219
|
-
if (request.signal?.aborted)
|
|
220
|
-
return { scope: null, kind: "dismissed", reason: `approval for ${request.capability} was canceled` };
|
|
334
|
+
const outcome = await joinApproval(existing, request);
|
|
335
|
+
if (request.signal?.aborted) return interrupted(request, "aborted");
|
|
221
336
|
// **`joined` marks the rider, and the ledger depends on it (R-66).** Sharing a non-`once` outcome
|
|
222
337
|
// is correct — the human authorised the capability for the session, not for one child — but the
|
|
223
338
|
// caller then stamped `approvalSource: "prompt"` on every one of them, so a fan-out of eight wrote
|