pi-daddy 0.17.1 → 0.18.1
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 +72 -0
- package/README.md +54 -25
- package/dist/approval-prompt.d.ts +3 -1
- package/dist/approval-prompt.d.ts.map +1 -1
- package/dist/approval-prompt.js +1 -1
- package/dist/approval-prompt.js.map +1 -1
- package/dist/approval-store.d.ts.map +1 -1
- package/dist/approval-store.js +4 -1
- package/dist/approval-store.js.map +1 -1
- package/dist/approval.d.ts +20 -2
- package/dist/approval.d.ts.map +1 -1
- package/dist/approval.js +26 -6
- package/dist/approval.js.map +1 -1
- package/dist/capabilities.d.ts +15 -0
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +47 -0
- package/dist/capabilities.js.map +1 -1
- package/dist/chain.d.ts +6 -1
- package/dist/chain.d.ts.map +1 -1
- package/dist/chain.js +1 -1
- package/dist/chain.js.map +1 -1
- package/dist/check-runner.d.ts +61 -0
- package/dist/check-runner.d.ts.map +1 -0
- package/dist/check-runner.js +237 -0
- package/dist/check-runner.js.map +1 -0
- package/dist/correlation.d.ts +90 -0
- package/dist/correlation.d.ts.map +1 -0
- package/dist/correlation.js +183 -0
- package/dist/correlation.js.map +1 -0
- package/dist/delegate-types.d.ts +140 -0
- package/dist/delegate-types.d.ts.map +1 -0
- package/dist/delegate-types.js +8 -0
- package/dist/delegate-types.js.map +1 -0
- package/dist/delegate.d.ts +4 -128
- package/dist/delegate.d.ts.map +1 -1
- package/dist/delegate.js +70 -73
- package/dist/delegate.js.map +1 -1
- package/dist/delegation-approval.d.ts +36 -0
- package/dist/delegation-approval.d.ts.map +1 -0
- package/dist/delegation-approval.js +67 -0
- package/dist/delegation-approval.js.map +1 -0
- package/dist/git-identity.d.ts +13 -0
- package/dist/git-identity.d.ts.map +1 -0
- package/dist/git-identity.js +44 -0
- package/dist/git-identity.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/lease-record.d.ts +78 -0
- package/dist/lease-record.d.ts.map +1 -0
- package/dist/lease-record.js +52 -0
- package/dist/lease-record.js.map +1 -0
- package/dist/ledger-events.d.ts +87 -0
- package/dist/ledger-events.d.ts.map +1 -0
- package/dist/ledger-events.js +45 -0
- package/dist/ledger-events.js.map +1 -0
- package/dist/ledger-report.d.ts +19 -12
- package/dist/ledger-report.d.ts.map +1 -1
- package/dist/ledger-report.js +81 -1
- package/dist/ledger-report.js.map +1 -1
- package/dist/ledger.d.ts +44 -1
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +20 -2
- package/dist/ledger.js.map +1 -1
- package/dist/propagation.d.ts.map +1 -1
- package/dist/propagation.js +2 -1
- package/dist/propagation.js.map +1 -1
- package/dist/refusals.d.ts +16 -0
- package/dist/refusals.d.ts.map +1 -0
- package/dist/refusals.js +52 -0
- package/dist/refusals.js.map +1 -0
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +7 -1
- package/dist/resolve.js.map +1 -1
- package/dist/run-child.d.ts +4 -0
- package/dist/run-child.d.ts.map +1 -1
- package/dist/run-child.js +21 -1
- package/dist/run-child.js.map +1 -1
- package/dist/run-herdr.d.ts +7 -0
- package/dist/run-herdr.d.ts.map +1 -1
- package/dist/run-herdr.js +41 -12
- package/dist/run-herdr.js.map +1 -1
- package/dist/workspace-lease.d.ts +28 -0
- package/dist/workspace-lease.d.ts.map +1 -0
- package/dist/workspace-lease.js +276 -0
- package/dist/workspace-lease.js.map +1 -0
- package/dist/workspace.d.ts +32 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +79 -0
- package/dist/workspace.js.map +1 -0
- package/extensions/approval-banking.ts +66 -0
- package/extensions/approvals.ts +95 -7
- package/extensions/chain-approval-facts.ts +51 -0
- package/extensions/chain-ledger.ts +48 -0
- package/extensions/delegate-chain.ts +115 -81
- package/extensions/delegation.ts +81 -23
- package/extensions/execute-child.ts +289 -0
- package/extensions/fanout-outcome.ts +97 -0
- package/extensions/run-delegation.ts +130 -120
- package/extensions/session.ts +4 -0
- package/extensions/workspace-runtime.ts +165 -0
- package/package.json +17 -1
- package/src/approval-prompt.ts +4 -2
- package/src/approval-store.ts +4 -1
- package/src/approval.ts +47 -14
- package/src/capabilities.ts +53 -0
- package/src/chain.ts +3 -1
- package/src/check-runner.ts +341 -0
- package/src/correlation.ts +260 -0
- package/src/delegate-types.ts +144 -0
- package/src/delegate.ts +82 -180
- package/src/delegation-approval.ts +99 -0
- package/src/git-identity.ts +52 -0
- package/src/index.ts +50 -0
- package/src/lease-record.ts +119 -0
- package/src/ledger-events.ts +138 -0
- package/src/ledger-report.ts +90 -2
- package/src/ledger.ts +68 -3
- package/src/propagation.ts +2 -1
- package/src/refusals.ts +67 -0
- package/src/resolve.ts +7 -1
- package/src/run-child.ts +23 -1
- package/src/run-herdr.ts +41 -11
- package/src/workspace-lease.ts +303 -0
- package/src/workspace.ts +135 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { atomicMetadata, leasePaths, readMetadata, } from "./lease-record.js";
|
|
3
|
+
import { once } from "node:events";
|
|
4
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { GovernanceRefusal, refusal } from "./refusals.js";
|
|
8
|
+
export const ENV_WORKSPACE_LEASE_DIR = "PI_GRANTS_WORKSPACE_LEASE_DIR";
|
|
9
|
+
export function defaultWorkspaceLeaseDir(env = process.env) {
|
|
10
|
+
const agentDir = env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
11
|
+
return env[ENV_WORKSPACE_LEASE_DIR] ?? join(agentDir, "pi-daddy", "workspace-leases");
|
|
12
|
+
}
|
|
13
|
+
const LEASE_READY = "PI_DADDY_LEASE_READY";
|
|
14
|
+
// The lock holder also owns crash cleanup for the governed child. If the parent dies, stdin closes;
|
|
15
|
+
// the helper signals the attached process, or closes the herdr tab, before releasing flock. A raw
|
|
16
|
+
// descendant deliberately detached by bash remains ADR-0012's OS-containment boundary, not a lease
|
|
17
|
+
// guarantee. The process branch SIGTERMs, escalates to SIGKILL at +500ms, and releases at +750ms
|
|
18
|
+
// WITHOUT confirming death (R-101). The herdr branch retries `tab close` a BOUNDED number of times
|
|
19
|
+
// and then releases anyway, leaving a marker file: an unreleasable lock strands a worktree forever
|
|
20
|
+
// with no in-product recovery, which is strictly worse than a recorded failure to close (R-102).
|
|
21
|
+
const HELPER_SOURCE = `
|
|
22
|
+
import { execFile } from "node:child_process";
|
|
23
|
+
import { writeFileSync } from "node:fs";
|
|
24
|
+
let clean=false, target=null, buffered="";
|
|
25
|
+
process.stdout.write(${JSON.stringify(`${LEASE_READY}:`)}+process.pid+"\\n");
|
|
26
|
+
process.stdin.setEncoding("utf8");
|
|
27
|
+
process.stdin.on("data",chunk=>{buffered+=chunk;for(;;){const i=buffered.indexOf("\\n");if(i<0)break;const line=buffered.slice(0,i);buffered=buffered.slice(i+1);try{const value=JSON.parse(line);if(value.release)clean=true;else if(value.process_pid)target={process_pid:value.process_pid};else if(value.herdr_tab)target={herdr_tab:value.herdr_tab};}catch{}}});
|
|
28
|
+
process.stdin.on("end",()=>{if(clean||!target)return process.exit(0);if(target.process_pid){try{process.kill(target.process_pid,"SIGTERM")}catch{return process.exit(0)}setTimeout(()=>{try{process.kill(target.process_pid,"SIGKILL")}catch{}},500);return setTimeout(()=>process.exit(0),750);}let left=Number(process.env.PI_DADDY_LEASE_CLOSE_ATTEMPTS||10);const giveUp=()=>{try{if(process.env.PI_DADDY_LEASE_MARKER)writeFileSync(process.env.PI_DADDY_LEASE_MARKER,JSON.stringify({reason:"herdr-close-failed",herdr_tab:target.herdr_tab})+"\\n")}catch{}process.exit(0)};const close=()=>execFile("herdr",["tab","close",target.herdr_tab],error=>{if(!error)return process.exit(0);if(--left<=0)return giveUp();setTimeout(close,1000)});close();});
|
|
29
|
+
process.stdin.resume();`;
|
|
30
|
+
/**
|
|
31
|
+
* The ledger outcome for a release. ONE definition, exported, because two call sites had their own copies
|
|
32
|
+
* and the check runner's copy asserted `released` unconditionally — reproducing in the evidence path the
|
|
33
|
+
* exact defect this union was added to expose (R-100/R-103).
|
|
34
|
+
*/
|
|
35
|
+
export function leaseReleaseLedgerOutcome(outcome, reason) {
|
|
36
|
+
if (outcome === "retained")
|
|
37
|
+
return "retained";
|
|
38
|
+
if (outcome === "lost")
|
|
39
|
+
return "lost";
|
|
40
|
+
if (outcome === "released-unrecorded")
|
|
41
|
+
return "released-unrecorded";
|
|
42
|
+
if (outcome === "not-held")
|
|
43
|
+
return "uncontended";
|
|
44
|
+
// `released-superseded` is a clean handover as far as the ledger is concerned: the lock went back and the
|
|
45
|
+
// record is correct — it just belongs to somebody else now.
|
|
46
|
+
return reason === "timeout" ? "timeout" : "released";
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* `acquired` must mean the kernel actually excluded somebody. A read lease takes no lock (R-105), and an
|
|
50
|
+
* unreadable predecessor record is not proof of a crash (R-100) — so neither may be reported as one.
|
|
51
|
+
*/
|
|
52
|
+
export function leaseAcquisitionOutcome(access, recovered) {
|
|
53
|
+
if (access === "read")
|
|
54
|
+
return "uncontended";
|
|
55
|
+
return recovered === true ? "recovered" : "acquired";
|
|
56
|
+
}
|
|
57
|
+
export async function acquireWorkspaceLease(input) {
|
|
58
|
+
if (input.access === "read") {
|
|
59
|
+
return {
|
|
60
|
+
workspace: input.workspace,
|
|
61
|
+
access: "read",
|
|
62
|
+
ownerId: input.ownerId,
|
|
63
|
+
recovered: false,
|
|
64
|
+
attachProcess: () => { },
|
|
65
|
+
attachHerdrTab: () => { },
|
|
66
|
+
markRetained: async () => { },
|
|
67
|
+
readCloseFailure: async () => null,
|
|
68
|
+
lost: new Promise(() => { }),
|
|
69
|
+
// No kernel lock was ever taken, so there is no handover to claim (R-105, release side).
|
|
70
|
+
release: async () => "not-held",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (input.signal?.aborted) {
|
|
74
|
+
throw new GovernanceRefusal(refusal("WORKSPACE_WRITE_CONFLICT", `writer lease for ${input.workspace.workspaceId} was cancelled before acquisition`));
|
|
75
|
+
}
|
|
76
|
+
await mkdir(input.leaseDir, { recursive: true, mode: 0o700 });
|
|
77
|
+
const paths = leasePaths(input.leaseDir, input.workspace.root);
|
|
78
|
+
const { spawn } = await import("node:child_process");
|
|
79
|
+
const holder = spawn(input.flockCommand ?? "flock", [
|
|
80
|
+
"--exclusive", "--nonblock", "--conflict-exit-code", "73", paths.lock,
|
|
81
|
+
process.execPath, "--input-type=module", "-e", HELPER_SOURCE,
|
|
82
|
+
], {
|
|
83
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
84
|
+
env: { ...process.env, PI_DADDY_LEASE_MARKER: paths.marker, PI_DADDY_LEASE_CLOSE_ATTEMPTS: String(input.herdrCloseAttempts ?? 10) },
|
|
85
|
+
// Own process group, for two reasons. It lets teardown kill `flock` AND the helper holding the lock
|
|
86
|
+
// file descriptor as one unit even before the helper has reported its pid (R-99), and it stops a
|
|
87
|
+
// stray group signal — a terminal Ctrl-C, a closed window — from releasing a live writer's lock as
|
|
88
|
+
// a side effect.
|
|
89
|
+
detached: true,
|
|
90
|
+
});
|
|
91
|
+
const timeoutMs = input.acquisitionTimeoutMs ?? 2000;
|
|
92
|
+
let ready = false;
|
|
93
|
+
let helperPid;
|
|
94
|
+
let stderr = "";
|
|
95
|
+
/**
|
|
96
|
+
* Kill the whole holder GROUP — one signal, no ordering. (The pid-by-pid fallback below is the
|
|
97
|
+
* only path that kills in sequence, and only when the group is already reaped.) `flock` does not pass `--close`, so the helper inherits
|
|
98
|
+
* the lock file descriptor and holds the lock in its own right — killing only `flock` leaves the
|
|
99
|
+
* lock HELD by an orphan, and every later acquisition then reports WORKSPACE_WRITE_CONFLICT, which
|
|
100
|
+
* is the one message an operator would use to conclude another agent is writing. Measured in
|
|
101
|
+
* `docs/probes/g35-flock-fd-inheritance` (R-99).
|
|
102
|
+
*/
|
|
103
|
+
const hardKill = () => {
|
|
104
|
+
// The GROUP, so this works on the readiness-timeout path too — there `helperPid` is usually still
|
|
105
|
+
// unknown, and killing only the wrapper leaves a half-booted helper free to inherit the lock file
|
|
106
|
+
// descriptor and hold it forever. Fall back to individual pids if the group is already reaped.
|
|
107
|
+
if (holder.pid !== undefined) {
|
|
108
|
+
try {
|
|
109
|
+
process.kill(-holder.pid, "SIGKILL");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
catch { /* group gone */ }
|
|
113
|
+
}
|
|
114
|
+
if (Number.isInteger(helperPid)) {
|
|
115
|
+
try {
|
|
116
|
+
process.kill(helperPid, "SIGKILL");
|
|
117
|
+
}
|
|
118
|
+
catch { /* already gone */ }
|
|
119
|
+
}
|
|
120
|
+
holder.kill("SIGKILL");
|
|
121
|
+
};
|
|
122
|
+
holder.stderr?.on("data", (chunk) => { stderr += String(chunk); });
|
|
123
|
+
try {
|
|
124
|
+
await new Promise((resolveReady, rejectReady) => {
|
|
125
|
+
const timer = setTimeout(() => rejectReady(new GovernanceRefusal(refusal("WORKSPACE_LEASE_STALE", `writer lease helper did not become ready within ${timeoutMs}ms`))), timeoutMs);
|
|
126
|
+
const cleanup = () => {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
input.signal?.removeEventListener("abort", onAbort);
|
|
129
|
+
};
|
|
130
|
+
const onAbort = () => rejectReady(new GovernanceRefusal(refusal("WORKSPACE_WRITE_CONFLICT", `writer lease for ${input.workspace.workspaceId} was cancelled before acquisition`)));
|
|
131
|
+
input.signal?.addEventListener("abort", onAbort, { once: true });
|
|
132
|
+
holder.once("error", (error) => {
|
|
133
|
+
cleanup();
|
|
134
|
+
rejectReady(new GovernanceRefusal(refusal("WORKSPACE_LEASE_STALE", `workspace writer leases require a working flock command (${String(error)})`)));
|
|
135
|
+
});
|
|
136
|
+
holder.once("close", (code) => {
|
|
137
|
+
if (ready)
|
|
138
|
+
return;
|
|
139
|
+
cleanup();
|
|
140
|
+
const codeName = code === 73 ? "WORKSPACE_WRITE_CONFLICT" : "WORKSPACE_LEASE_STALE";
|
|
141
|
+
rejectReady(new GovernanceRefusal(refusal(codeName, code === 73
|
|
142
|
+
? `workspace ${input.workspace.workspaceId} already has an active pi-daddy-governed writer`
|
|
143
|
+
: `workspace lease helper exited before acquisition${stderr ? ` (${stderr.trim()})` : ""}`, { workspace_id: input.workspace.workspaceId, root: input.workspace.root })));
|
|
144
|
+
});
|
|
145
|
+
holder.stdout?.on("data", (chunk) => {
|
|
146
|
+
const output = String(chunk);
|
|
147
|
+
if (!output.includes(LEASE_READY) || ready)
|
|
148
|
+
return;
|
|
149
|
+
helperPid = Number(output.match(new RegExp(`${LEASE_READY}:(\\d+)`))?.[1]);
|
|
150
|
+
ready = true;
|
|
151
|
+
cleanup();
|
|
152
|
+
resolveReady();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
hardKill();
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
const previous = await readMetadata(paths.metadata);
|
|
161
|
+
// "malformed" is not "no prior owner": an unreadable record cannot prove a clean handover (R-100).
|
|
162
|
+
const recovered = previous === "malformed" ? "unknown" : previous?.state === "active";
|
|
163
|
+
const token = randomUUID();
|
|
164
|
+
const metadata = {
|
|
165
|
+
version: 1,
|
|
166
|
+
state: "active",
|
|
167
|
+
token,
|
|
168
|
+
owner_id: input.ownerId,
|
|
169
|
+
workspace_id: input.workspace.workspaceId,
|
|
170
|
+
root: input.workspace.root,
|
|
171
|
+
pid: Number.isInteger(helperPid) ? helperPid : (holder.pid ?? process.pid),
|
|
172
|
+
acquired_at: new Date().toISOString(),
|
|
173
|
+
};
|
|
174
|
+
try {
|
|
175
|
+
await atomicMetadata(paths.metadata, metadata);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
holder.stdin?.end();
|
|
179
|
+
throw new GovernanceRefusal(refusal("WORKSPACE_LEASE_STALE", `acquired the kernel writer lock but could not record its owner (${String(error)})`, { workspace_id: input.workspace.workspaceId, root: input.workspace.root }));
|
|
180
|
+
}
|
|
181
|
+
// TWO variables, deliberately. `releasing` goes up the instant release begins, because the handshake
|
|
182
|
+
// below closes the helper on purpose — and if `lost` could still fire during it, an intentional release
|
|
183
|
+
// would report itself as a lost lease. `settled` memoizes the ANSWER, so a second call cannot invent a
|
|
184
|
+
// clean handover. Collapsing these into one flag set at the end broke every check-runner test.
|
|
185
|
+
let releasing = false;
|
|
186
|
+
let settled;
|
|
187
|
+
let lose;
|
|
188
|
+
const lost = new Promise((resolveLost) => { lose = resolveLost; });
|
|
189
|
+
const lostError = () => new Error(`workspace writer lease helper exited for ${input.workspace.workspaceId}`);
|
|
190
|
+
holder.once("close", () => { if (!releasing)
|
|
191
|
+
lose(lostError()); });
|
|
192
|
+
if (holder.exitCode !== null || holder.signalCode !== null)
|
|
193
|
+
queueMicrotask(() => lose(lostError()));
|
|
194
|
+
const attach = (value) => {
|
|
195
|
+
if (releasing || holder.exitCode !== null || holder.signalCode !== null || !holder.stdin?.writable) {
|
|
196
|
+
throw new GovernanceRefusal(refusal("WORKSPACE_LEASE_STALE", `writer lease for ${input.workspace.workspaceId} was lost before child attachment`));
|
|
197
|
+
}
|
|
198
|
+
holder.stdin.write(`${JSON.stringify(value)}\n`);
|
|
199
|
+
};
|
|
200
|
+
return {
|
|
201
|
+
workspace: input.workspace,
|
|
202
|
+
access: "write",
|
|
203
|
+
ownerId: input.ownerId,
|
|
204
|
+
recovered,
|
|
205
|
+
attachProcess(pid) { attach({ process_pid: pid }); },
|
|
206
|
+
attachHerdrTab(tabId) { attach({ herdr_tab: tabId }); },
|
|
207
|
+
lost,
|
|
208
|
+
async markRetained(reason = "retained") {
|
|
209
|
+
// Deliberately does NOT touch the kernel lock or the helper: the pane may still be live. It only
|
|
210
|
+
// stops the record from looking like a crash.
|
|
211
|
+
const current = await readMetadata(paths.metadata);
|
|
212
|
+
if (current !== "malformed" && current?.token === token) {
|
|
213
|
+
await atomicMetadata(paths.metadata, {
|
|
214
|
+
...metadata, state: "released", released_at: new Date().toISOString(), release_reason: `retained:${reason}`,
|
|
215
|
+
}).catch(() => undefined);
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
async readCloseFailure() {
|
|
219
|
+
try {
|
|
220
|
+
return JSON.parse(await readFile(paths.marker, "utf8"));
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
async release(reason = "completed") {
|
|
227
|
+
// Memoized, not a boolean. A second call used to answer "released" whatever the first returned, so a
|
|
228
|
+
// retry — or a `finally` after an explicit release — read a clean handover that never happened.
|
|
229
|
+
if (settled)
|
|
230
|
+
return settled;
|
|
231
|
+
releasing = true;
|
|
232
|
+
// Already gone: this owner never released anything, and saying so is the caller's business to
|
|
233
|
+
// record. Throwing here discarded completed work and masked the real error (R-99).
|
|
234
|
+
if (holder.exitCode !== null || holder.signalCode !== null)
|
|
235
|
+
return (settled = "lost");
|
|
236
|
+
// Default FALSE: "recorded" must mean this owner actually wrote its own handover. Anything else —
|
|
237
|
+
// an unreadable record, or a successor's token in place of ours — means the lock goes back but the
|
|
238
|
+
// record does not say so, and the NEXT owner will report a recovery that never happened unless the
|
|
239
|
+
// caller ledgers it (R-100). Only the current token may mark the handover, so a stale lease object
|
|
240
|
+
// can never overwrite a live successor.
|
|
241
|
+
// Three outcomes, because they call for three different responses: we wrote our handover; a
|
|
242
|
+
// successor already owns the record and we correctly left it alone; or the record is unreadable or
|
|
243
|
+
// unwritable and somebody has to be told.
|
|
244
|
+
let how = "released-unrecorded";
|
|
245
|
+
const current = await readMetadata(paths.metadata);
|
|
246
|
+
if (current !== "malformed" && current?.token === token) {
|
|
247
|
+
try {
|
|
248
|
+
await atomicMetadata(paths.metadata, {
|
|
249
|
+
...metadata,
|
|
250
|
+
state: "released",
|
|
251
|
+
released_at: new Date().toISOString(),
|
|
252
|
+
release_reason: reason,
|
|
253
|
+
});
|
|
254
|
+
how = "released";
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
how = "released-unrecorded";
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else if (current !== "malformed" && current !== null) {
|
|
261
|
+
// A live successor's record. Declining to overwrite it is the token guard working, not a fault.
|
|
262
|
+
how = "released-superseded";
|
|
263
|
+
}
|
|
264
|
+
holder.stdin?.write(`${JSON.stringify({ release: true })}\n`);
|
|
265
|
+
holder.stdin?.end();
|
|
266
|
+
if (holder.exitCode === null && holder.signalCode === null) {
|
|
267
|
+
await Promise.race([once(holder, "close"), new Promise((resolveWait) => setTimeout(resolveWait, 1000))]);
|
|
268
|
+
}
|
|
269
|
+
// The helper holds the lock fd in its own right, so the wrapper alone is not enough (R-99).
|
|
270
|
+
if (holder.exitCode === null && holder.signalCode === null)
|
|
271
|
+
hardKill();
|
|
272
|
+
return (settled = how);
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
//# sourceMappingURL=workspace-lease.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-lease.js","sourceRoot":"","sources":["../src/workspace-lease.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const ENV_WORKSPACE_REGISTRY = "PI_GRANTS_WORKSPACE_REGISTRY";
|
|
2
|
+
export type WorkspaceAccess = "read" | "write";
|
|
3
|
+
export interface WorkspaceRegistryFile {
|
|
4
|
+
version: 1;
|
|
5
|
+
workspaces: Record<string, {
|
|
6
|
+
path: string;
|
|
7
|
+
}>;
|
|
8
|
+
}
|
|
9
|
+
export interface ValidatedWorkspace {
|
|
10
|
+
workspaceId: string;
|
|
11
|
+
/** Canonical Git worktree root and the child's validated initial CWD. Not a sandbox. */
|
|
12
|
+
root: string;
|
|
13
|
+
gitCommonDir: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function loadWorkspaceRegistry(path: string): Promise<WorkspaceRegistryFile>;
|
|
16
|
+
export declare function resolveWorkspace(registry: WorkspaceRegistryFile, workspaceId: string): Promise<ValidatedWorkspace>;
|
|
17
|
+
/**
|
|
18
|
+
* Canonicalize and validate the initial workspace against Git's registered worktree list.
|
|
19
|
+
* This prevents accidental misrouting. It does not constrain any path a child accesses after spawn.
|
|
20
|
+
*/
|
|
21
|
+
export declare function validateRegisteredWorkspace(input: {
|
|
22
|
+
workspaceId: string;
|
|
23
|
+
registeredRoot: string;
|
|
24
|
+
suppliedRoot?: string;
|
|
25
|
+
}): Promise<ValidatedWorkspace>;
|
|
26
|
+
/**
|
|
27
|
+
* The governed-writer lease lives in `./workspace-lease.ts` and is re-exported here so that
|
|
28
|
+
* "the workspace module" remains one import for callers. Split only to stay under the 400-line
|
|
29
|
+
* module ceiling this project enforces mechanically.
|
|
30
|
+
*/
|
|
31
|
+
export { ENV_WORKSPACE_LEASE_DIR, acquireWorkspaceLease, defaultWorkspaceLeaseDir, leaseAcquisitionOutcome, leaseReleaseLedgerOutcome, type LeaseReleaseOutcome, type WorkspaceLease, } from "./workspace-lease.ts";
|
|
32
|
+
//# sourceMappingURL=workspace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,sBAAsB,iCAAiC,CAAC;AAGrE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,OAAO,CAAC;AAE/C,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9C;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,wFAAwF;IACxF,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA6BxF;AAED,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,qBAAqB,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAUxH;AAED;;;GAGG;AACH,wBAAsB,2BAA2B,CAAC,KAAK,EAAE;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAyC9B;AAED;;;;GAIG;AACH,OAAO,EACL,uBAAuB,EACvB,qBAAqB,EACrB,wBAAwB,EACxB,uBAAuB,EACvB,yBAAyB,EACzB,KAAK,mBAAmB,EACxB,KAAK,cAAc,GACpB,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { once } from "node:events";
|
|
4
|
+
import { mkdir, readFile, realpath, rename, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { isAbsolute, join } from "node:path";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { GovernanceRefusal, refusal } from "./refusals.js";
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
export const ENV_WORKSPACE_REGISTRY = "PI_GRANTS_WORKSPACE_REGISTRY";
|
|
11
|
+
export async function loadWorkspaceRegistry(path) {
|
|
12
|
+
let parsed;
|
|
13
|
+
try {
|
|
14
|
+
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
throw new GovernanceRefusal(refusal("WORKSPACE_NOT_REGISTERED", `workspace registry ${path} could not be read (${String(error)})`, { registry_path: path }));
|
|
18
|
+
}
|
|
19
|
+
const file = parsed;
|
|
20
|
+
if (file?.version !== 1 || !file.workspaces || typeof file.workspaces !== "object" || Array.isArray(file.workspaces)) {
|
|
21
|
+
throw new GovernanceRefusal(refusal("WORKSPACE_NOT_REGISTERED", `workspace registry ${path} must contain {version:1, workspaces:{id:{path}}}`, { registry_path: path }));
|
|
22
|
+
}
|
|
23
|
+
for (const [id, value] of Object.entries(file.workspaces)) {
|
|
24
|
+
if (!id || !value || typeof value.path !== "string" || !isAbsolute(value.path)) {
|
|
25
|
+
throw new GovernanceRefusal(refusal("WORKSPACE_NOT_REGISTERED", `workspace registry entry ${JSON.stringify(id)} must name an absolute path`, { registry_path: path, workspace_id: id }));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { version: 1, workspaces: structuredClone(file.workspaces) };
|
|
29
|
+
}
|
|
30
|
+
export async function resolveWorkspace(registry, workspaceId) {
|
|
31
|
+
const registered = Object.hasOwn(registry.workspaces, workspaceId) ? registry.workspaces[workspaceId] : undefined;
|
|
32
|
+
if (!registered) {
|
|
33
|
+
throw new GovernanceRefusal(refusal("WORKSPACE_NOT_REGISTERED", `workspace ${JSON.stringify(workspaceId)} is not present in the operator-owned registry`, { workspace_id: workspaceId }));
|
|
34
|
+
}
|
|
35
|
+
return validateRegisteredWorkspace({ workspaceId, registeredRoot: registered.path });
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Canonicalize and validate the initial workspace against Git's registered worktree list.
|
|
39
|
+
* This prevents accidental misrouting. It does not constrain any path a child accesses after spawn.
|
|
40
|
+
*/
|
|
41
|
+
export async function validateRegisteredWorkspace(input) {
|
|
42
|
+
let registered;
|
|
43
|
+
let supplied;
|
|
44
|
+
try {
|
|
45
|
+
registered = await realpath(input.registeredRoot);
|
|
46
|
+
supplied = await realpath(input.suppliedRoot ?? input.registeredRoot);
|
|
47
|
+
if (!(await stat(registered)).isDirectory())
|
|
48
|
+
throw new Error("registered root is not a directory");
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
throw new GovernanceRefusal(refusal("WORKSPACE_NOT_REGISTERED", `workspace ${input.workspaceId} root could not be canonicalized (${String(error)})`, { workspace_id: input.workspaceId }));
|
|
52
|
+
}
|
|
53
|
+
if (registered !== supplied) {
|
|
54
|
+
throw new GovernanceRefusal(refusal("WORKSPACE_NOT_REGISTERED", `workspace ${input.workspaceId} resolved to ${supplied}, not its registered worktree ${registered}`, { workspace_id: input.workspaceId, supplied_root: supplied, registered_root: registered }));
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const top = (await execFileAsync("git", ["-C", registered, "rev-parse", "--show-toplevel"], { encoding: "utf8" })).stdout.trim();
|
|
58
|
+
const canonicalTop = await realpath(top);
|
|
59
|
+
if (canonicalTop !== registered)
|
|
60
|
+
throw new Error(`path is inside worktree ${canonicalTop}, not its root`);
|
|
61
|
+
const commonRaw = (await execFileAsync("git", ["-C", registered, "rev-parse", "--path-format=absolute", "--git-common-dir"], { encoding: "utf8" })).stdout.trim();
|
|
62
|
+
const gitCommonDir = await realpath(commonRaw);
|
|
63
|
+
const list = (await execFileAsync("git", ["-C", registered, "worktree", "list", "--porcelain"], { encoding: "utf8" })).stdout;
|
|
64
|
+
const registeredWorktrees = await Promise.all(list.split("\n").filter((line) => line.startsWith("worktree ")).map((line) => realpath(line.slice(9))));
|
|
65
|
+
if (!registeredWorktrees.includes(registered))
|
|
66
|
+
throw new Error("Git does not list this path as a worktree");
|
|
67
|
+
return { workspaceId: input.workspaceId, root: registered, gitCommonDir };
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
throw new GovernanceRefusal(refusal("WORKSPACE_NOT_REGISTERED", `workspace ${input.workspaceId} is not the registered Git worktree it claims to be (${String(error)})`, { workspace_id: input.workspaceId, registered_root: registered }));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The governed-writer lease lives in `./workspace-lease.ts` and is re-exported here so that
|
|
75
|
+
* "the workspace module" remains one import for callers. Split only to stay under the 400-line
|
|
76
|
+
* module ceiling this project enforces mechanically.
|
|
77
|
+
*/
|
|
78
|
+
export { ENV_WORKSPACE_LEASE_DIR, acquireWorkspaceLease, defaultWorkspaceLeaseDir, leaseAcquisitionOutcome, leaseReleaseLedgerOutcome, } from "./workspace-lease.js";
|
|
79
|
+
//# sourceMappingURL=workspace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace.js","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Taking back authority a gate banked for an operation that was then refused.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `./approvals.ts` only to stay under the 400-line module ceiling this project enforces
|
|
5
|
+
* mechanically; `./approvals.ts` re-exports both names.
|
|
6
|
+
*/
|
|
7
|
+
import { revokeApproval } from "../src/approval-store.ts";
|
|
8
|
+
import type { Capability } from "../src/resolve.ts";
|
|
9
|
+
import { snapshotOf } from "./approvals.ts";
|
|
10
|
+
import type { GrantsSession } from "./session.ts";
|
|
11
|
+
|
|
12
|
+
/** One piece of authority a gate banked, and where it lives, so it can be undone. */
|
|
13
|
+
export interface BankedApproval {
|
|
14
|
+
key: string;
|
|
15
|
+
capability: Capability;
|
|
16
|
+
subject: string;
|
|
17
|
+
/** True when a 30-day entry reached the on-disk store, which outlives the process. */
|
|
18
|
+
persisted: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* False when this call did not CREATE the authority — it joined a dialog another delegation had already
|
|
21
|
+
* opened, or the key was already standing from an earlier grant.
|
|
22
|
+
*
|
|
23
|
+
* Load-bearing: unbanking revoked by `capability@subject` with no ownership check, so one refused
|
|
24
|
+
* delegation could destroy an approval a live sibling was running under. Two reachable shapes — a
|
|
25
|
+
* joined gate (one human answer shared by concurrent waiters, each recording it as its own) and a
|
|
26
|
+
* legacy-then-bound sequence, where a bound call re-prompts over a plain key that already exists and
|
|
27
|
+
* then deletes both on refusal. Only what this call actually created may be taken back.
|
|
28
|
+
*/
|
|
29
|
+
owned: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Give back authority a gate banked for an operation that was then refused.
|
|
34
|
+
*
|
|
35
|
+
* Best-effort by necessity — the persisted store can be busy — but never silent: an approval that could
|
|
36
|
+
* not be revoked is exactly the one an operator needs to know is still standing (R-113).
|
|
37
|
+
*/
|
|
38
|
+
export async function unbankApprovals(
|
|
39
|
+
session: GrantsSession,
|
|
40
|
+
ctx: { ui: { notify: (message: string) => void } },
|
|
41
|
+
banked: BankedApproval[] | undefined,
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
if (!banked || banked.length === 0) return;
|
|
44
|
+
const stranded: string[] = [];
|
|
45
|
+
for (const entry of banked) {
|
|
46
|
+
// Never take back authority this call did not create — see `owned`.
|
|
47
|
+
if (!entry.owned) continue;
|
|
48
|
+
session.sessionApprovals.delete(entry.key);
|
|
49
|
+
session.sessionApprovalBindings.delete(entry.key);
|
|
50
|
+
if (!entry.persisted) continue;
|
|
51
|
+
const outcome = await revokeApproval(session.cwd, entry.key, (subject) => snapshotOf(session, subject), new Date());
|
|
52
|
+
// `"absent"` is NOT proof the entry is gone. `revokeApproval` reports it when the key is missing from
|
|
53
|
+
// the *valid* set, and `loadApprovals` excludes entries whose ceiling or body digest no longer
|
|
54
|
+
// matches — those are still physically in the file and revive the moment they validate again. So an
|
|
55
|
+
// entry that is merely stale reads as "nothing to revoke" and is silently left standing.
|
|
56
|
+
if (outcome !== "revoked") stranded.push(entry.key);
|
|
57
|
+
}
|
|
58
|
+
// Children inherit from the published set, so it must shrink with the session state, not after it.
|
|
59
|
+
session.publishChildEnv();
|
|
60
|
+
if (stranded.length > 0) {
|
|
61
|
+
ctx.ui.notify(
|
|
62
|
+
`grants: this delegation was refused, but a stored approval for ${stranded.join(", ")} could not be ` +
|
|
63
|
+
`revoked and still stands — remove it with \`/grants revoke\` if it was not intended.`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|