pi-daddy 0.21.0 → 0.22.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 +25 -1
- package/README.md +14 -9
- package/contracts/ledger/v3/fixtures/capability-decision.json +3 -4
- package/contracts/ledger/v3/fixtures/check-receipt.json +3 -4
- package/contracts/ledger/v3/fixtures/child-lifecycle.json +3 -4
- package/contracts/ledger/v3/fixtures/workflow-fact.json +3 -4
- package/contracts/ledger/v3/fixtures/workspace-lease.json +3 -4
- package/contracts/ledger/v3/ledger-event.schema.json +38 -13
- package/dist/check-runner.d.ts.map +1 -1
- package/dist/check-runner.js +161 -149
- package/dist/check-runner.js.map +1 -1
- package/dist/correlation.d.ts +15 -4
- package/dist/correlation.d.ts.map +1 -1
- package/dist/correlation.js +40 -2
- package/dist/correlation.js.map +1 -1
- package/dist/delegation-approval.d.ts +2 -2
- package/dist/delegation-approval.d.ts.map +1 -1
- package/dist/delegation-approval.js +2 -0
- package/dist/delegation-approval.js.map +1 -1
- package/dist/finalization.d.ts +15 -0
- package/dist/finalization.d.ts.map +1 -0
- package/dist/finalization.js +65 -0
- package/dist/finalization.js.map +1 -0
- package/dist/git-identity.d.ts.map +1 -1
- package/dist/git-identity.js +18 -15
- package/dist/git-identity.js.map +1 -1
- package/dist/grant-store.d.ts +13 -0
- package/dist/grant-store.d.ts.map +1 -1
- package/dist/grant-store.js +48 -25
- package/dist/grant-store.js.map +1 -1
- package/dist/herdr-poll.js +1 -1
- package/dist/herdr-poll.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +2 -6
- package/dist/init.js.map +1 -1
- package/dist/model-preflight.d.ts +8 -0
- package/dist/model-preflight.d.ts.map +1 -0
- package/dist/model-preflight.js +20 -0
- package/dist/model-preflight.js.map +1 -0
- package/dist/refusals.d.ts +1 -1
- package/dist/refusals.d.ts.map +1 -1
- package/dist/refusals.js +2 -0
- package/dist/refusals.js.map +1 -1
- package/dist/run-child.d.ts +1 -1
- package/dist/run-child.d.ts.map +1 -1
- package/dist/run-child.js +2 -2
- package/dist/run-child.js.map +1 -1
- package/dist/run-herdr.d.ts.map +1 -1
- package/dist/run-herdr.js +6 -5
- package/dist/run-herdr.js.map +1 -1
- package/extensions/chain-plan.ts +16 -0
- package/extensions/correlation-shape.ts +39 -0
- package/extensions/delegate-chain.ts +9 -14
- package/extensions/delegation.ts +3 -25
- package/extensions/execute-child.ts +8 -1
- package/extensions/grant-store-refusal.ts +36 -0
- package/extensions/grants.ts +6 -0
- package/extensions/run-delegation.ts +21 -2
- package/extensions/session.ts +15 -5
- package/extensions/stored-grant-session.ts +33 -0
- package/package.json +3 -1
- package/src/check-runner.ts +56 -44
- package/src/correlation.ts +55 -6
- package/src/delegation-approval.ts +4 -2
- package/src/finalization.ts +79 -0
- package/src/git-identity.ts +20 -16
- package/src/grant-store.ts +55 -25
- package/src/herdr-poll.ts +1 -1
- package/src/init.ts +5 -5
- package/src/model-preflight.ts +32 -0
- package/src/refusals.ts +2 -0
- package/src/run-child.ts +2 -2
- package/src/run-herdr.ts +6 -4
package/src/grant-store.ts
CHANGED
|
@@ -52,6 +52,12 @@ export interface StoredGrant {
|
|
|
52
52
|
projectLedger: boolean;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
export type GrantStoreRefusalReason = "malformed" | "unsupported-version" | "unreadable" | "wrong-cwd";
|
|
56
|
+
export type StoredGrantState =
|
|
57
|
+
| { state: "absent" }
|
|
58
|
+
| { state: "valid"; stored: StoredGrant }
|
|
59
|
+
| { state: "refuse"; reason: GrantStoreRefusalReason };
|
|
60
|
+
|
|
55
61
|
export interface SaveGrantOptions {
|
|
56
62
|
projectLedger?: boolean;
|
|
57
63
|
}
|
|
@@ -84,26 +90,36 @@ export function grantStorePath(cwd: string): string {
|
|
|
84
90
|
* treats absent and invalid stores alike (R-175), so do not call that wider behavior fail-closed. The `cwd`
|
|
85
91
|
* check is R-27's: a copied file describes a directory nobody authorised it for.
|
|
86
92
|
*/
|
|
87
|
-
export function
|
|
93
|
+
export function parseStoredGrantState(text: string, cwd: string): StoredGrantState {
|
|
94
|
+
let parsed: { version?: unknown; cwd?: unknown; grant?: unknown; projectLedger?: unknown };
|
|
88
95
|
try {
|
|
89
|
-
|
|
90
|
-
version?: unknown;
|
|
91
|
-
cwd?: unknown;
|
|
92
|
-
grant?: unknown;
|
|
93
|
-
projectLedger?: unknown;
|
|
94
|
-
};
|
|
95
|
-
if (parsed.version !== 1 && parsed.version !== 2) return null;
|
|
96
|
-
if (parsed.cwd !== cwd) return null;
|
|
97
|
-
if (!Array.isArray(parsed.grant)) return null;
|
|
98
|
-
if (!parsed.grant.every((c) => typeof c === "string" && c.length > 0)) return null;
|
|
99
|
-
if (parsed.version === 2 && parsed.projectLedger !== true) return null;
|
|
100
|
-
return {
|
|
101
|
-
grant: parsed.grant as Capability[],
|
|
102
|
-
projectLedger: parsed.version === 2,
|
|
103
|
-
};
|
|
96
|
+
parsed = JSON.parse(text) as typeof parsed;
|
|
104
97
|
} catch {
|
|
105
|
-
return
|
|
98
|
+
return { state: "refuse", reason: "malformed" };
|
|
99
|
+
}
|
|
100
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
101
|
+
return { state: "refuse", reason: "malformed" };
|
|
102
|
+
}
|
|
103
|
+
if (parsed.version !== 1 && parsed.version !== 2) {
|
|
104
|
+
return { state: "refuse", reason: "unsupported-version" };
|
|
105
|
+
}
|
|
106
|
+
if (parsed.cwd !== cwd) return { state: "refuse", reason: "wrong-cwd" };
|
|
107
|
+
if (!Array.isArray(parsed.grant)) return { state: "refuse", reason: "malformed" };
|
|
108
|
+
if (!parsed.grant.every((c) => typeof c === "string" && c.length > 0)) {
|
|
109
|
+
return { state: "refuse", reason: "malformed" };
|
|
110
|
+
}
|
|
111
|
+
if (parsed.version === 2 && parsed.projectLedger !== true) {
|
|
112
|
+
return { state: "refuse", reason: "malformed" };
|
|
106
113
|
}
|
|
114
|
+
return {
|
|
115
|
+
state: "valid",
|
|
116
|
+
stored: { grant: parsed.grant as Capability[], projectLedger: parsed.version === 2 },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function parseStoredGrant(text: string, cwd: string): StoredGrant | null {
|
|
121
|
+
const result = parseStoredGrantState(text, cwd);
|
|
122
|
+
return result.state === "valid" ? result.stored : null;
|
|
107
123
|
}
|
|
108
124
|
|
|
109
125
|
/** Compatibility view for callers that need only the capability ceiling. */
|
|
@@ -120,27 +136,41 @@ export function parseGrantFile(text: string, cwd: string): Capability[] | null {
|
|
|
120
136
|
* decision it exists to inform, so the store would silently fail to grant delegation — the exact class of
|
|
121
137
|
* defect R-38 and R-39 were.
|
|
122
138
|
*/
|
|
123
|
-
export function
|
|
139
|
+
export function loadStoredGrantStateSync(cwd: string): StoredGrantState {
|
|
124
140
|
try {
|
|
125
|
-
return
|
|
126
|
-
} catch {
|
|
127
|
-
return
|
|
141
|
+
return parseStoredGrantState(readFileSync(grantStorePath(cwd), "utf8"), cwd);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return (error as NodeJS.ErrnoException).code === "ENOENT"
|
|
144
|
+
? { state: "absent" }
|
|
145
|
+
: { state: "refuse", reason: "unreadable" };
|
|
128
146
|
}
|
|
129
147
|
}
|
|
130
148
|
|
|
149
|
+
export function loadStoredGrantSync(cwd: string): StoredGrant | null {
|
|
150
|
+
const result = loadStoredGrantStateSync(cwd);
|
|
151
|
+
return result.state === "valid" ? result.stored : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
131
154
|
export function loadGrantSync(cwd: string): Capability[] | null {
|
|
132
155
|
return loadStoredGrantSync(cwd)?.grant ?? null;
|
|
133
156
|
}
|
|
134
157
|
|
|
135
158
|
/** Async twin, for callers that already have one. Same parser, so they cannot disagree. */
|
|
136
|
-
export async function
|
|
159
|
+
export async function loadStoredGrantState(cwd: string): Promise<StoredGrantState> {
|
|
137
160
|
try {
|
|
138
|
-
return
|
|
139
|
-
} catch {
|
|
140
|
-
return
|
|
161
|
+
return parseStoredGrantState(await readFile(grantStorePath(cwd), "utf8"), cwd);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return (error as NodeJS.ErrnoException).code === "ENOENT"
|
|
164
|
+
? { state: "absent" }
|
|
165
|
+
: { state: "refuse", reason: "unreadable" };
|
|
141
166
|
}
|
|
142
167
|
}
|
|
143
168
|
|
|
169
|
+
export async function loadStoredGrant(cwd: string): Promise<StoredGrant | null> {
|
|
170
|
+
const result = await loadStoredGrantState(cwd);
|
|
171
|
+
return result.state === "valid" ? result.stored : null;
|
|
172
|
+
}
|
|
173
|
+
|
|
144
174
|
export async function loadGrant(cwd: string): Promise<Capability[] | null> {
|
|
145
175
|
return (await loadStoredGrant(cwd))?.grant ?? null;
|
|
146
176
|
}
|
package/src/herdr-poll.ts
CHANGED
|
@@ -67,7 +67,7 @@ export async function waitForSettled(
|
|
|
67
67
|
): Promise<{ status?: string; timedOut?: boolean; aborted?: boolean; spawnError?: string }> {
|
|
68
68
|
const interval = request.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
69
69
|
// ADR-0032. The pane is read on every poll so the parent can show what the child is doing, rather than the
|
|
70
|
-
// one word `delegate` for up to
|
|
70
|
+
// one word `delegate` for up to twenty minutes by default.
|
|
71
71
|
//
|
|
72
72
|
// No cross-poll state is kept, deliberately: the previous design remembered what it had reported so it could
|
|
73
73
|
// send a diff, and that is what broke — see `tailLines`. A snapshot needs no memory.
|
package/src/init.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { mkdir, open, rm, writeFile } from "node:fs/promises";
|
|
|
26
26
|
import { join } from "node:path";
|
|
27
27
|
import { agentCapability, workspaceCapability } from "./capabilities.ts";
|
|
28
28
|
import { ceilingForDefinition } from "./definitions.ts";
|
|
29
|
+
import { runWithFinalizers } from "./finalization.ts";
|
|
29
30
|
import { ALWAYS_LIVE, assertGrantIsWritable, isLiveByDefault, renderGrantEnv, type GrantEnvSkill } from "./grant-env.ts";
|
|
30
31
|
import { PI_BUILTIN_TOOLS } from "./pi-tools.ts";
|
|
31
32
|
import type { Capability } from "./resolve.ts";
|
|
@@ -332,11 +333,10 @@ async function createUnlessPresent(path: string, content: string, outcome: InitO
|
|
|
332
333
|
try {
|
|
333
334
|
await mkdir(join(path, ".."), { recursive: true });
|
|
334
335
|
const handle = await open(path, "wx");
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
336
|
+
await runWithFinalizers(
|
|
337
|
+
() => handle.writeFile(content, "utf8"),
|
|
338
|
+
[{ label: "new-file handle cleanup failed", run: () => handle.close() }],
|
|
339
|
+
);
|
|
340
340
|
outcome.written.push(path);
|
|
341
341
|
} catch (error) {
|
|
342
342
|
if ((error as { code?: string }).code === "EEXIST") outcome.kept.push(path);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { refusal, type StructuredRefusal } from "./refusals.ts";
|
|
2
|
+
|
|
3
|
+
export const ENV_ALLOW_UNRESOLVED_MODELS = "PI_GRANTS_ALLOW_UNRESOLVED_MODELS";
|
|
4
|
+
|
|
5
|
+
export interface ModelCatalogue {
|
|
6
|
+
find(provider: string, modelId: string): unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Resolve an explicit provider/id against pi's own session catalogue without probing credentials or network. */
|
|
10
|
+
export function preflightModel(
|
|
11
|
+
model: string | undefined,
|
|
12
|
+
catalogue: ModelCatalogue,
|
|
13
|
+
cache: Map<string, boolean>,
|
|
14
|
+
allowUnresolved: boolean,
|
|
15
|
+
): StructuredRefusal | undefined {
|
|
16
|
+
if (model === undefined || allowUnresolved) return undefined;
|
|
17
|
+
const slash = model.indexOf("/");
|
|
18
|
+
const provider = slash > 0 ? model.slice(0, slash) : "";
|
|
19
|
+
const id = slash > 0 ? model.slice(slash + 1) : "";
|
|
20
|
+
let resolved = cache.get(model);
|
|
21
|
+
if (resolved === undefined) {
|
|
22
|
+
resolved = provider.length > 0 && id.length > 0 && catalogue.find(provider, id) !== undefined;
|
|
23
|
+
cache.set(model, resolved);
|
|
24
|
+
}
|
|
25
|
+
if (resolved) return undefined;
|
|
26
|
+
return refusal(
|
|
27
|
+
"MODEL_UNRESOLVED",
|
|
28
|
+
`grants: model ${model || "(empty)"} is not in pi's session catalogue; use provider/id from /model, ` +
|
|
29
|
+
`or set ${ENV_ALLOW_UNRESOLVED_MODELS}=1 to let pi attempt custom resolution`,
|
|
30
|
+
{ model: model || "(empty)" },
|
|
31
|
+
);
|
|
32
|
+
}
|
package/src/refusals.ts
CHANGED
|
@@ -11,6 +11,8 @@ export const REFUSAL_CODES = [
|
|
|
11
11
|
"DEPTH_EXCEEDED",
|
|
12
12
|
"FANOUT_EXCEEDED",
|
|
13
13
|
"EXECUTOR_UNAVAILABLE",
|
|
14
|
+
"MODEL_UNRESOLVED",
|
|
15
|
+
"GRANT_STORE_INVALID",
|
|
14
16
|
// Execution-phase outcomes. Without these, an external controller could distinguish a policy refusal
|
|
15
17
|
// from an internal error but not a lost writer lease from a user pressing stop (R-103).
|
|
16
18
|
"CHILD_TIMED_OUT",
|
package/src/run-child.ts
CHANGED
|
@@ -111,8 +111,8 @@ export interface ChildRunResult {
|
|
|
111
111
|
|
|
112
112
|
/** 1 MiB. A delegation returns a summary; anything larger is a runaway, not an answer. */
|
|
113
113
|
export const DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
114
|
-
/**
|
|
115
|
-
export const DEFAULT_TIMEOUT_MS =
|
|
114
|
+
/** 20 minutes. Long-running sub-agent work often exceeded ten; a hang remains bounded. */
|
|
115
|
+
export const DEFAULT_TIMEOUT_MS = 20 * 60 * 1000;
|
|
116
116
|
/** Grace between SIGTERM and SIGKILL. A child that ignores SIGTERM must not make the timeout advisory. */
|
|
117
117
|
export const DEFAULT_KILL_GRACE_MS = 5000;
|
|
118
118
|
|
package/src/run-herdr.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { rm } from "node:fs/promises";
|
|
25
|
+
import { runWithFinalizers } from "./finalization.ts";
|
|
25
26
|
import type { ChildRunResult } from "./run-child.ts";
|
|
26
27
|
import { DEFAULT_MAX_OUTPUT_BYTES, DEFAULT_TIMEOUT_MS } from "./run-child.ts";
|
|
27
28
|
import { MAX_OPEN_PANES, markPaneSettled, trackPane, trimOpenPanes, untrackPane } from "./pane-reaper.ts";
|
|
@@ -293,7 +294,7 @@ export async function runHerdrPane(request: HerdrRunRequest): Promise<ChildRunRe
|
|
|
293
294
|
// early `return` from a failed start or prompt — leaves it false, so `cleanup` closes the tab.
|
|
294
295
|
let settledCleanly = false;
|
|
295
296
|
|
|
296
|
-
|
|
297
|
+
return runWithFinalizers(async () => {
|
|
297
298
|
const started = await startAgent(exec, agentName, paneId, effectiveArgs, deadline);
|
|
298
299
|
if (started.error) return { ...empty, spawnError: `herdr agent start failed: ${started.error}` };
|
|
299
300
|
|
|
@@ -352,9 +353,10 @@ export async function runHerdrPane(request: HerdrRunRequest): Promise<ChildRunRe
|
|
|
352
353
|
timedOut: false,
|
|
353
354
|
aborted: false,
|
|
354
355
|
};
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
|
|
356
|
+
}, [{
|
|
357
|
+
label: "herdr pane finalizer failed",
|
|
358
|
+
run: () => cleanup(settledCleanly),
|
|
359
|
+
}]);
|
|
358
360
|
}
|
|
359
361
|
|
|
360
362
|
/**
|