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,166 @@
|
|
|
1
|
+
/** Report loaded package generations separately from the manifests currently installed on disk. */
|
|
2
|
+
import {
|
|
3
|
+
DefaultPackageManager,
|
|
4
|
+
SettingsManager,
|
|
5
|
+
VERSION,
|
|
6
|
+
getPackageDir,
|
|
7
|
+
type ExtensionAPI,
|
|
8
|
+
type ExtensionContext,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { join, resolve } from "node:path";
|
|
11
|
+
import { readBoundedTextSync } from "../src/kernel/bounded-read-sync.ts";
|
|
12
|
+
import { agentDir } from "../src/kernel/project-paths.ts";
|
|
13
|
+
import type { EcosystemPackageId, EcosystemVersionRow, EcosystemVersions } from "../src/products/ecosystem-versions.ts";
|
|
14
|
+
|
|
15
|
+
const CHANNEL = "pi-daddy:ecosystem-versions:v1";
|
|
16
|
+
const packages = [
|
|
17
|
+
{ id: "pi", label: "Pi", names: ["@earendil-works/pi-coding-agent"] },
|
|
18
|
+
{ id: "pi-daddy", label: "pi-daddy", names: ["pi-daddy"] },
|
|
19
|
+
{ id: "principal-pi-skills", label: "Principal", names: ["principal-pi-skills"] },
|
|
20
|
+
{ id: "skill-harness", label: "Harness", names: ["skill-harness", "skill-harness-monorepo"] },
|
|
21
|
+
] as const;
|
|
22
|
+
interface LoadedPackage {
|
|
23
|
+
id: EcosystemPackageId;
|
|
24
|
+
version: string | null;
|
|
25
|
+
root: string;
|
|
26
|
+
}
|
|
27
|
+
const versionPattern = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
28
|
+
function manifest(root: string, names: readonly string[]): string | null {
|
|
29
|
+
try {
|
|
30
|
+
const value = JSON.parse(readBoundedTextSync(join(root, "package.json"), { maxBytes: 128 * 1024 }).text);
|
|
31
|
+
return names.includes(value.name) && typeof value.version === "string" && versionPattern.test(value.version)
|
|
32
|
+
? value.version
|
|
33
|
+
: null;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export interface VersionInventoryPorts {
|
|
39
|
+
hostVersion: string;
|
|
40
|
+
hostRoot: string;
|
|
41
|
+
ownRoot: string;
|
|
42
|
+
readVersion(root: string, names: readonly string[]): string | null;
|
|
43
|
+
configured(ctx: ExtensionContext): Array<{ source: string; scope: "user" | "project"; installedPath?: string }>;
|
|
44
|
+
}
|
|
45
|
+
const defaultPorts = (ownRoot: string): VersionInventoryPorts => ({
|
|
46
|
+
hostVersion: VERSION,
|
|
47
|
+
hostRoot: getPackageDir(),
|
|
48
|
+
ownRoot,
|
|
49
|
+
readVersion: manifest,
|
|
50
|
+
configured: (ctx) =>
|
|
51
|
+
new DefaultPackageManager({
|
|
52
|
+
cwd: ctx.cwd,
|
|
53
|
+
agentDir: agentDir(),
|
|
54
|
+
settingsManager: SettingsManager.create(ctx.cwd, agentDir(), {
|
|
55
|
+
projectTrusted: ctx.isProjectTrusted?.() === true,
|
|
56
|
+
}),
|
|
57
|
+
}).listConfiguredPackages(),
|
|
58
|
+
});
|
|
59
|
+
/** Every package answers synchronously on Pi's native event bus; old packages stay explicitly unreported. */
|
|
60
|
+
export function createEcosystemVersionInventory(
|
|
61
|
+
pi: Pick<ExtensionAPI, "events">,
|
|
62
|
+
ownRoot: string,
|
|
63
|
+
ports = defaultPorts(ownRoot),
|
|
64
|
+
) {
|
|
65
|
+
const own: LoadedPackage = {
|
|
66
|
+
id: "pi-daddy",
|
|
67
|
+
root: ports.ownRoot,
|
|
68
|
+
version: ports.readVersion(ports.ownRoot, ["pi-daddy"]),
|
|
69
|
+
};
|
|
70
|
+
return (ctx: ExtensionContext): EcosystemVersions => {
|
|
71
|
+
const reports: LoadedPackage[] = [own, { id: "pi", root: ports.hostRoot, version: ports.hostVersion }];
|
|
72
|
+
pi.events?.emit(CHANNEL, {
|
|
73
|
+
report: (value: unknown) => {
|
|
74
|
+
if (!value || typeof value !== "object") return;
|
|
75
|
+
const item = value as LoadedPackage;
|
|
76
|
+
if (
|
|
77
|
+
!["principal-pi-skills", "skill-harness"].includes(item.id) ||
|
|
78
|
+
typeof item.root !== "string" ||
|
|
79
|
+
!item.root.startsWith("/")
|
|
80
|
+
)
|
|
81
|
+
return;
|
|
82
|
+
if (typeof item.version !== "string" || !versionPattern.test(item.version)) return;
|
|
83
|
+
reports.push({ id: item.id, root: item.root, version: item.version });
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
let configured: ReturnType<VersionInventoryPorts["configured"]> = [],
|
|
87
|
+
note: string | undefined;
|
|
88
|
+
try {
|
|
89
|
+
configured = ports.configured(ctx);
|
|
90
|
+
} catch {
|
|
91
|
+
note = "Configured package sources unavailable; loaded reports remain visible.";
|
|
92
|
+
}
|
|
93
|
+
const rows: EcosystemVersionRow[] = packages.map((pkg) => {
|
|
94
|
+
const loaded = reports.filter((item) => item.id === pkg.id);
|
|
95
|
+
const sources = configured.filter(
|
|
96
|
+
(item) =>
|
|
97
|
+
item.source === `npm:${pkg.id}` ||
|
|
98
|
+
item.source.startsWith(`npm:${pkg.id}@`) ||
|
|
99
|
+
(item.installedPath !== undefined && ports.readVersion(item.installedPath, pkg.names) !== null) ||
|
|
100
|
+
loaded.some((x) => item.installedPath && resolve(item.installedPath) === resolve(x.root)),
|
|
101
|
+
);
|
|
102
|
+
const source = sources.length === 1 ? sources[0] : undefined;
|
|
103
|
+
const root = loaded.length === 1 ? loaded[0].root : loaded.length === 0 ? source?.installedPath : undefined;
|
|
104
|
+
const installedVersion = root ? ports.readVersion(root, pkg.names) : null;
|
|
105
|
+
const loadedVersion = loaded.length === 1 ? loaded[0].version : null;
|
|
106
|
+
const observedRoots = new Set([
|
|
107
|
+
...loaded.map((item) => resolve(item.root)),
|
|
108
|
+
...sources.flatMap((item) => (item.installedPath ? [resolve(item.installedPath)] : [])),
|
|
109
|
+
]);
|
|
110
|
+
const multiple =
|
|
111
|
+
loaded.length > 1 ||
|
|
112
|
+
sources.length > 1 ||
|
|
113
|
+
observedRoots.size > 1 ||
|
|
114
|
+
(loaded.length > 0 && sources.some((item) => !item.installedPath));
|
|
115
|
+
const commands =
|
|
116
|
+
pkg.id === "pi"
|
|
117
|
+
? ["pi update"]
|
|
118
|
+
: source?.source.startsWith("npm:") && !multiple
|
|
119
|
+
? [`pi install${source.scope === "project" ? " --local" : ""} npm:${pkg.id}@<version>`]
|
|
120
|
+
: ["pi list", "pi config"];
|
|
121
|
+
if (pkg.id === "principal-pi-skills" && root && !multiple) {
|
|
122
|
+
const installer = "'" + join(root, "scripts/install-agents.mjs").replaceAll("'", "'\\''") + "'";
|
|
123
|
+
commands.push(`node ${installer} install`, `node ${installer} check`);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
id: pkg.id,
|
|
127
|
+
label: pkg.label,
|
|
128
|
+
loadedVersion,
|
|
129
|
+
installedVersion,
|
|
130
|
+
source: multiple ? null : (source?.source ?? (pkg.id === "pi" ? "host runtime" : null)),
|
|
131
|
+
path: root ?? null,
|
|
132
|
+
state: multiple
|
|
133
|
+
? "multiple"
|
|
134
|
+
: loadedVersion && installedVersion
|
|
135
|
+
? loadedVersion === installedVersion
|
|
136
|
+
? "current"
|
|
137
|
+
: "reload-required"
|
|
138
|
+
: !loadedVersion && installedVersion
|
|
139
|
+
? "not-reported"
|
|
140
|
+
: "unavailable",
|
|
141
|
+
commands,
|
|
142
|
+
note: multiple
|
|
143
|
+
? "Multiple loaded or configured sources; inspect with pi list before changing versions."
|
|
144
|
+
: !loadedVersion
|
|
145
|
+
? "This package did not report its loaded generation. Installed bytes alone do not establish the running version."
|
|
146
|
+
: pkg.id === "pi"
|
|
147
|
+
? "Restart Pi after changing its version."
|
|
148
|
+
: "Use an explicit npm version to replace a pin; pi update keeps pinned versions. Reload Pi after installation." +
|
|
149
|
+
(pkg.id === "principal-pi-skills"
|
|
150
|
+
? " Agent installer commands are optional, for legacy installed agent definitions."
|
|
151
|
+
: ""),
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
return { rows, checkedAt: new Date().toISOString(), note };
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
export function bindEcosystemVersions(
|
|
158
|
+
pi: ExtensionAPI,
|
|
159
|
+
session: { ecosystemVersions?: () => EcosystemVersions },
|
|
160
|
+
ownRoot: string,
|
|
161
|
+
): void {
|
|
162
|
+
const read = createEcosystemVersionInventory(pi, ownRoot);
|
|
163
|
+
pi.on("session_start", (_event, ctx) => {
|
|
164
|
+
session.ecosystemVersions = () => read(ctx);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Operator-facing execution settings; display never changes admission, deadlines or retention. */
|
|
2
|
+
import { timeoutFromEnv, idleTimeoutFromEnv } from "../src/kernel/run-child.ts";
|
|
3
|
+
import { timeoutMsFromEnv } from "../src/governance/approval-prompt.ts";
|
|
4
|
+
import {
|
|
5
|
+
ENV_APPROVAL_TIMEOUT,
|
|
6
|
+
ENV_CHILD_TIMEOUT,
|
|
7
|
+
ENV_CHILD_IDLE_TIMEOUT,
|
|
8
|
+
ENV_EXECUTION_ARCHIVE,
|
|
9
|
+
} from "../src/kernel/env-names.ts";
|
|
10
|
+
|
|
11
|
+
export function renderExecutionControls(
|
|
12
|
+
nativeSessionRoot: string | undefined,
|
|
13
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
14
|
+
): string[] {
|
|
15
|
+
let approval: string;
|
|
16
|
+
try {
|
|
17
|
+
const timeout = timeoutMsFromEnv(env[ENV_APPROVAL_TIMEOUT]);
|
|
18
|
+
approval = timeout === undefined ? "wait for human; no deadline" : `${timeout / 1000}s configured deadline`;
|
|
19
|
+
} catch {
|
|
20
|
+
approval = `${ENV_APPROVAL_TIMEOUT} invalid; a needed new prompt will refuse`;
|
|
21
|
+
}
|
|
22
|
+
const source = (key: string) => (env[key] === undefined ? "default" : `from ${key}; zero/invalid uses default`);
|
|
23
|
+
return [
|
|
24
|
+
` child wall ${timeoutFromEnv(env[ENV_CHILD_TIMEOUT]) / 1000}s (${source(ENV_CHILD_TIMEOUT)})`,
|
|
25
|
+
` child idle ${idleTimeoutFromEnv(env[ENV_CHILD_IDLE_TIMEOUT]) / 1000}s (${source(ENV_CHILD_IDLE_TIMEOUT)})`,
|
|
26
|
+
" process safety controls; no time/token budget is added to the model prompt",
|
|
27
|
+
` approval ${approval}`,
|
|
28
|
+
` transcripts ${
|
|
29
|
+
nativeSessionRoot === undefined
|
|
30
|
+
? "off"
|
|
31
|
+
: nativeSessionRoot
|
|
32
|
+
? `opted in at ${JSON.stringify(nativeSessionRoot)}; destination checked at dispatch`
|
|
33
|
+
: "opted in but native session root is missing"
|
|
34
|
+
}`,
|
|
35
|
+
` diagnostics ${
|
|
36
|
+
env[ENV_EXECUTION_ARCHIVE]
|
|
37
|
+
? `configured at ${JSON.stringify(env[ENV_EXECUTION_ARCHIVE])}; check each result's retention status`
|
|
38
|
+
: "off"
|
|
39
|
+
}`,
|
|
40
|
+
" local diagnostic retention is separate from JEV/LoRA consent",
|
|
41
|
+
];
|
|
42
|
+
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* being whatever happened to be in scope.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { renderExecutionControls } from "./execution-controls.ts";
|
|
15
16
|
import type { Capability } from "../src/kernel/resolve.ts";
|
|
16
17
|
import type { ExecutorChoice } from "../src/executors/executor.ts";
|
|
17
18
|
import type { Catalog } from "../src/kernel/catalog.ts";
|
|
@@ -25,6 +26,11 @@ import type { ResolvedDefinitionRuntime } from "./definition-runtime.ts";
|
|
|
25
26
|
export interface GrantsCommandContext {
|
|
26
27
|
cwd: string;
|
|
27
28
|
governed: boolean;
|
|
29
|
+
/** Host opt-in captured by this session, not a later environment reread. */
|
|
30
|
+
nativeSessionRoot?: string;
|
|
31
|
+
autoModeStatus?: string;
|
|
32
|
+
/** Explicit owner mutation; the command parser itself holds no permission authority. */
|
|
33
|
+
changeAutoMode?: (enabled: boolean) => Promise<string>;
|
|
28
34
|
ownGrant: Capability[];
|
|
29
35
|
/**
|
|
30
36
|
* Which executor this session settled on, and why — ADR-0031.
|
|
@@ -100,12 +106,13 @@ const KNOWN_SUBCOMMANDS: readonly string[] = [
|
|
|
100
106
|
"revoke",
|
|
101
107
|
"workspaces",
|
|
102
108
|
"models",
|
|
109
|
+
"auto",
|
|
103
110
|
];
|
|
104
111
|
|
|
105
112
|
export const grantsCommand = {
|
|
106
113
|
description:
|
|
107
114
|
"Show this session's capability grant, delegation depth, and known agent-type ceilings; " +
|
|
108
|
-
"/grants init | /grants dashboard | /grants approvals | /grants revoke | /grants ledger",
|
|
115
|
+
"/grants init | /grants dashboard | /grants auto [on|off] | /grants approvals | /grants revoke | /grants ledger",
|
|
109
116
|
handler: async (args: string, ctx: any) => {
|
|
110
117
|
// Everything this command may see, named in one place. Previously these were whatever happened to be in
|
|
111
118
|
// the enclosing closure — which is how a diagnostic came to disagree with the enforcer (R-28).
|
|
@@ -129,7 +136,32 @@ export const grantsCommand = {
|
|
|
129
136
|
runtimeFor,
|
|
130
137
|
} = ctx.grants as GrantsCommandContext;
|
|
131
138
|
|
|
132
|
-
const
|
|
139
|
+
const words = args.trim().split(/\s+/).filter(Boolean);
|
|
140
|
+
const [sub, target] = words;
|
|
141
|
+
if (sub === "auto") {
|
|
142
|
+
if (words.length > 2 || (target !== undefined && target !== "on" && target !== "off")) {
|
|
143
|
+
ctx.ui.notify("grants: use /grants auto [on|off]; no permission setting changed.", "warning");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (target === undefined) {
|
|
147
|
+
ctx.ui.notify(`grants: Daddy permission Auto ${ctx.grants.autoModeStatus ?? "unavailable"}`, "info");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (!ctx.grants.changeAutoMode) {
|
|
151
|
+
ctx.ui.notify("grants: Daddy Auto controls are unavailable for this session owner.", "warning");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const status = await ctx.grants.changeAutoMode(target === "on");
|
|
156
|
+
ctx.ui.notify(`grants: Daddy permission Auto ${status}; admitted work continues.`, "info");
|
|
157
|
+
} catch (error) {
|
|
158
|
+
ctx.ui.notify(
|
|
159
|
+
`grants: Auto change not confirmed: ${String(error)}. Read /grants auto for current state.`,
|
|
160
|
+
"error",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
133
165
|
|
|
134
166
|
if (sub === "init") {
|
|
135
167
|
await ctx.grants.runInit();
|
|
@@ -261,7 +293,7 @@ export const grantsCommand = {
|
|
|
261
293
|
if (attributed > 0 || unattributed > 0) {
|
|
262
294
|
lines.push(
|
|
263
295
|
` approvals ${bySource.prompt} prompt · ${bySource.persisted} persisted · ${bySource.session} session · ` +
|
|
264
|
-
`${bySource.inherited} inherited`,
|
|
296
|
+
`${bySource.inherited} inherited · ${bySource.auto} auto`,
|
|
265
297
|
);
|
|
266
298
|
// **Two numbers, because one of them would lie.** Counting `persisted` RECORDS as prompts avoided
|
|
267
299
|
// overstates the layer twentyfold in the obvious case: precedence is inherited → session →
|
|
@@ -407,6 +439,8 @@ export const grantsCommand = {
|
|
|
407
439
|
// ADR-0031, and its probe is only defensible because this line exists. Placed next to `holding` so the
|
|
408
440
|
// two facts about what a spawn will be sit together.
|
|
409
441
|
` executor ${executor.disclosure}`,
|
|
442
|
+
...renderExecutionControls(ctx.grants.nativeSessionRoot),
|
|
443
|
+
` Auto ${ctx.grants.autoModeStatus ?? "OFF (default)"}`,
|
|
410
444
|
` depth ${depth} of max ${maxDepth}${maxDepth <= 0 ? " (spawning disabled)" : ""}`,
|
|
411
445
|
` ledger ${ledgerPath || "(not recording — set PI_DADDY_LEDGER)"}`,
|
|
412
446
|
` approvals ${sessionApprovals.size} this session, ${valid.size} persisted` +
|
package/extensions/grants.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { bindEcosystemVersions } from "./ecosystem-versions.ts";
|
|
2
|
+
import {
|
|
3
|
+
initializeSessionAutoMode,
|
|
4
|
+
closeSessionAutoMode,
|
|
5
|
+
getDashboardControlState,
|
|
6
|
+
setAutoApproval,
|
|
7
|
+
} from "./session-auto-mode.ts";
|
|
1
8
|
/**
|
|
2
9
|
* pi-daddy — the extension entry point pi loads.
|
|
3
10
|
*
|
|
@@ -79,6 +86,7 @@ export default function (pi: ExtensionAPI, trustedSession?: GrantsSession) {
|
|
|
79
86
|
})();
|
|
80
87
|
const observerExtensionPath = fileURLToPath(new URL("./activity-timeline.ts", import.meta.url));
|
|
81
88
|
const session = trustedSession ?? createGrantsSession(extensionPath, undefined, observerExtensionPath);
|
|
89
|
+
bindEcosystemVersions(pi, session, fileURLToPath(new URL("../", import.meta.url)));
|
|
82
90
|
session.currentThinking = typeof pi.getThinkingLevel === "function" ? () => pi.getThinkingLevel() : undefined;
|
|
83
91
|
registerActivityTimeline(pi, session);
|
|
84
92
|
const runtimeSnapshot = registerRuntimeSnapshot(pi, session);
|
|
@@ -131,6 +139,13 @@ export default function (pi: ExtensionAPI, trustedSession?: GrantsSession) {
|
|
|
131
139
|
// this absolute identity verbatim, and resolve(abs) remains abs at every deeper session start.
|
|
132
140
|
if (session.ledgerPath) session.ledgerPath = resolve(ctx.cwd, session.ledgerPath);
|
|
133
141
|
try {
|
|
142
|
+
try {
|
|
143
|
+
const nativeId = (owner as { getSessionId?: () => string }).getSessionId?.() ?? session.episodeId;
|
|
144
|
+
await initializeSessionAutoMode(session, reload.environment, nativeId);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
session.autoModeFailure = `Daddy Auto mode unavailable: ${String(error)}`;
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
134
149
|
// ADR-0076 PR 3d: a pre-format project ledger is imported once into the record envelope, bodies verbatim, each
|
|
135
150
|
// record marked with its source line; the old file is left untouched. Happens before any spawn can append.
|
|
136
151
|
if (session.governed) {
|
|
@@ -319,6 +334,10 @@ export default function (pi: ExtensionAPI, trustedSession?: GrantsSession) {
|
|
|
319
334
|
return undefined;
|
|
320
335
|
});
|
|
321
336
|
|
|
337
|
+
pi.on("session_shutdown", async (event) => {
|
|
338
|
+
if (event.reason !== "reload") await closeSessionAutoMode(session);
|
|
339
|
+
});
|
|
340
|
+
|
|
322
341
|
/**
|
|
323
342
|
* Reap the herdr panes this agent run opened — ADR-0032.
|
|
324
343
|
*
|
|
@@ -433,8 +452,8 @@ export default function (pi: ExtensionAPI, trustedSession?: GrantsSession) {
|
|
|
433
452
|
// Built per invocation and spelled out field by field, rather than passing the session whole: what a
|
|
434
453
|
// read-only diagnostic may see is a decision, and `GrantsCommandContext` is where it is recorded.
|
|
435
454
|
handler: async (args, ctx) => {
|
|
436
|
-
//
|
|
437
|
-
if (args.trim() !== "revoke --all") await session.ensureDefinitions?.();
|
|
455
|
+
// Auto controls and emergency revoke-all need no definition acquisition; OFF must remain reachable.
|
|
456
|
+
if (args.trim() !== "revoke --all" && args.trim().split(/\s+/)[0] !== "auto") await session.ensureDefinitions?.();
|
|
438
457
|
return grantsCommand.handler(args, {
|
|
439
458
|
...ctx,
|
|
440
459
|
grants: {
|
|
@@ -462,6 +481,14 @@ export default function (pi: ExtensionAPI, trustedSession?: GrantsSession) {
|
|
|
462
481
|
},
|
|
463
482
|
}
|
|
464
483
|
: {}),
|
|
484
|
+
nativeSessionRoot: session.nativeSessionRoot,
|
|
485
|
+
changeAutoMode: async (enabled: boolean) => {
|
|
486
|
+
const { auto } = await setAutoApproval(session, enabled, "command");
|
|
487
|
+
return `${auto.enabled ? "ON" : "OFF"} (${auto.source}; current session permissions)`;
|
|
488
|
+
},
|
|
489
|
+
autoModeStatus: await getDashboardControlState(session)
|
|
490
|
+
.then(({ auto }) => `${auto.enabled ? "ON" : "OFF"} (${auto.source}; current session permissions)`)
|
|
491
|
+
.catch((error) => `unavailable: ${String(error)}`),
|
|
465
492
|
catalog: session.catalog,
|
|
466
493
|
definitions: session.definitions,
|
|
467
494
|
sessionApprovals: session.sessionApprovals,
|
|
@@ -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
|
+
}
|