pi-daddy 0.37.0 → 0.38.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 +79 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/kernel/delegate-types.d.ts +9 -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 +6 -1
- 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 +3 -0
- package/dist/kernel/env-names.js.map +1 -1
- package/dist/kernel/propagation.d.ts +24 -1
- package/dist/kernel/propagation.d.ts.map +1 -1
- package/dist/kernel/propagation.js +28 -1
- package/dist/kernel/propagation.js.map +1 -1
- package/dist/kernel/workspace-pin.d.ts +56 -0
- package/dist/kernel/workspace-pin.d.ts.map +1 -0
- package/dist/kernel/workspace-pin.js +139 -0
- package/dist/kernel/workspace-pin.js.map +1 -0
- package/dist/kernel/workspace.d.ts +20 -1
- package/dist/kernel/workspace.d.ts.map +1 -1
- package/dist/kernel/workspace.js +32 -2
- package/dist/kernel/workspace.js.map +1 -1
- package/extensions/grants-command.ts +16 -1
- package/extensions/grants.ts +1 -0
- package/extensions/reload-environment.ts +25 -0
- package/extensions/run-delegation.ts +1 -0
- package/extensions/session-report.ts +7 -0
- package/extensions/session.ts +111 -0
- package/extensions/workspace-runtime.ts +16 -1
- package/package.json +1 -1
- package/src/index.ts +13 -0
- package/src/kernel/delegate-types.ts +9 -0
- package/src/kernel/delegate.ts +6 -0
- package/src/kernel/env-names.ts +3 -0
- package/src/kernel/propagation.ts +40 -0
- package/src/kernel/workspace-pin.ts +158 -0
- package/src/kernel/workspace.ts +34 -1
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The inherited destination pin (ADR-0042): what a workspace id MEANT when the authority was granted.
|
|
3
|
+
*
|
|
4
|
+
* **The escalation this closes, measured in `g37-registry-tamper`.** `workspace:<id>` attenuates the NAME, not
|
|
5
|
+
* the mutable id-to-path meaning. A child holding `workspace:staging` and `tool:write` — no `bash` — can rewrite
|
|
6
|
+
* the operator's registry so `staging` points at the `prod` worktree, route a grandchild there, and take an
|
|
7
|
+
* exclusive write lease on it. Every capability check passes, because the id it was granted is the id it used.
|
|
8
|
+
* File ownership cannot help: a governed child runs as the parent's uid, so the registry is as writable to it as
|
|
9
|
+
* to the operator.
|
|
10
|
+
*
|
|
11
|
+
* **The mechanism is authority, not metadata.** A root resolves each registered id to its canonical destination
|
|
12
|
+
* and records a digest of it. Descendants inherit only the entries their effective grant authorises, cannot mint
|
|
13
|
+
* an entry, and routing requires an exact match against the inherited digest. A rewritten registry therefore
|
|
14
|
+
* changes where an id points and NOT what an inherited id is allowed to mean, which is the whole property.
|
|
15
|
+
*
|
|
16
|
+
* **Why a digest rather than the path.** The path is already visible to the child through the registry it can
|
|
17
|
+
* read; pinning the digest keeps the wire small for a realistic registry and makes an equality check the only
|
|
18
|
+
* operation, so there is no path-comparison subtlety to get wrong. `validateRegisteredWorkspace` still does the
|
|
19
|
+
* canonicalisation; this only asks whether the answer changed.
|
|
20
|
+
*
|
|
21
|
+
* **Every failure refuses.** Missing, empty, malformed and mismatched all refuse, because the alternative is a
|
|
22
|
+
* mechanism a child can disable by corrupting one byte. ADR-0042 records that an earlier attempt was reverted
|
|
23
|
+
* after four failures, and that it failed "specifically because it was treated as a small patch".
|
|
24
|
+
*/
|
|
25
|
+
import { createHash } from "node:crypto";
|
|
26
|
+
import { isSafeWorkspaceId, workspaceCapability } from "./capabilities.js";
|
|
27
|
+
export { ENV_WORKSPACE_PIN } from "./env-names.js";
|
|
28
|
+
/**
|
|
29
|
+
* Half a SHA-256, hex. The comparison is equality against a value the operator's own root computed, not a
|
|
30
|
+
* signature, so this is sized to make accidental collision impossible rather than to resist an adversary who
|
|
31
|
+
* can already choose both sides.
|
|
32
|
+
*/
|
|
33
|
+
const DIGEST_HEX = 32;
|
|
34
|
+
/** A canonical destination, reduced to something an environment variable can carry. */
|
|
35
|
+
export function destinationDigest(canonicalRoot) {
|
|
36
|
+
return createHash("sha256").update(canonicalRoot, "utf8").digest("hex").slice(0, DIGEST_HEX);
|
|
37
|
+
}
|
|
38
|
+
/** `id:digest` pairs. Ids cannot contain `:` or `,` — `isSafeWorkspaceId` is the reason this is unambiguous. */
|
|
39
|
+
export function formatWorkspacePin(pins) {
|
|
40
|
+
return [...pins.entries()]
|
|
41
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
42
|
+
.map(([id, digest]) => `${id}:${digest}`)
|
|
43
|
+
.join(",");
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read a pin, refusing anything that is not exactly the shape this module writes.
|
|
47
|
+
*
|
|
48
|
+
* An absent variable is NOT the same as an empty one and both are distinguished by the caller: absent means no
|
|
49
|
+
* root ever established a pin, empty means a parent established one and this child inherited no entries. Both
|
|
50
|
+
* refuse at routing time; they need different messages to be diagnosable.
|
|
51
|
+
*/
|
|
52
|
+
export function parseWorkspacePin(raw) {
|
|
53
|
+
if (raw === undefined)
|
|
54
|
+
return { refusal: "no destination pin was inherited" };
|
|
55
|
+
const trimmed = raw.trim();
|
|
56
|
+
if (trimmed === "")
|
|
57
|
+
return { pins: new Map() };
|
|
58
|
+
const pins = new Map();
|
|
59
|
+
for (const entry of trimmed.split(",")) {
|
|
60
|
+
const at = entry.lastIndexOf(":");
|
|
61
|
+
if (at <= 0)
|
|
62
|
+
return { refusal: `destination pin entry ${JSON.stringify(entry)} is not id:digest` };
|
|
63
|
+
const id = entry.slice(0, at);
|
|
64
|
+
const digest = entry.slice(at + 1);
|
|
65
|
+
if (!isSafeWorkspaceId(id))
|
|
66
|
+
return { refusal: `destination pin names a malformed workspace id ${JSON.stringify(id)}` };
|
|
67
|
+
if (!/^[0-9a-f]{32}$/.test(digest))
|
|
68
|
+
return { refusal: `destination pin for ${id} is not a ${DIGEST_HEX}-character digest` };
|
|
69
|
+
// A duplicated id with two digests is ambiguous, and picking either would let a tamperer supply both.
|
|
70
|
+
if (pins.has(id) && pins.get(id) !== digest)
|
|
71
|
+
return { refusal: `destination pin names ${id} twice with different destinations` };
|
|
72
|
+
pins.set(id, digest);
|
|
73
|
+
}
|
|
74
|
+
return { pins };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The pin a child inherits: the parent's entries, narrowed to the workspaces the child's grant actually names.
|
|
78
|
+
*
|
|
79
|
+
* Narrowing here rather than passing the parent's whole map is the difference between a pin and a hint. A child
|
|
80
|
+
* that cannot see an entry cannot route to it even if it rewrites the registry to make the id resolve, because
|
|
81
|
+
* routing needs a pin entry and it has none to offer.
|
|
82
|
+
*/
|
|
83
|
+
export function attenuateWorkspacePin(pins, inheritable) {
|
|
84
|
+
const held = new Set(inheritable);
|
|
85
|
+
return new Map([...pins.entries()].filter(([id]) => held.has(workspaceCapability(id))));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Does the registry still resolve this id to what the grant meant?
|
|
89
|
+
*
|
|
90
|
+
* Returns a refusal reason, or `undefined` when the destination matches. A string rather than a thrown error
|
|
91
|
+
* because the one caller already owns the refusal code and the `details` this belongs in.
|
|
92
|
+
*/
|
|
93
|
+
export function checkPinnedDestination(input) {
|
|
94
|
+
if ("refusal" in input.pin)
|
|
95
|
+
return input.pin.refusal;
|
|
96
|
+
const expected = input.pin.pins.get(input.workspaceId);
|
|
97
|
+
if (expected === undefined)
|
|
98
|
+
return (`no destination pin was inherited for workspace ${input.workspaceId}` +
|
|
99
|
+
(input.pin.pins.size > 0 ? ` — pinned: ${[...input.pin.pins.keys()].sort().join(", ")}` : ""));
|
|
100
|
+
const actual = destinationDigest(input.canonicalRoot);
|
|
101
|
+
if (actual !== expected)
|
|
102
|
+
return (`workspace ${input.workspaceId} now resolves to ${input.canonicalRoot}, which is not the destination this ` +
|
|
103
|
+
`session was granted. The registry has been rewritten since the grant was established, so the id no longer ` +
|
|
104
|
+
`means what it meant when it was authorised (ADR-0042)`);
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Establish a root's pin by resolving every registered id to its canonical destination.
|
|
109
|
+
*
|
|
110
|
+
* **Only a session that inherited NO pin may call this**, and that rule is the mechanism. If a descendant could
|
|
111
|
+
* mint its own pin it would simply rewrite the registry and then re-establish, and the whole thing would be a
|
|
112
|
+
* comment. The caller enforces it because only the caller knows whether a pin was inherited; this function is
|
|
113
|
+
* the resolution, not the policy.
|
|
114
|
+
*
|
|
115
|
+
* An id whose destination cannot be canonicalised is LEFT OUT rather than pinned to a guess. Routing then
|
|
116
|
+
* refuses it for having no pin, which is the same direction as every other failure here.
|
|
117
|
+
*/
|
|
118
|
+
export async function establishWorkspacePin(registry, canonicalise, onSkipped) {
|
|
119
|
+
const pins = new Map();
|
|
120
|
+
for (const [id, entry] of Object.entries(registry.workspaces)) {
|
|
121
|
+
if (!isSafeWorkspaceId(id)) {
|
|
122
|
+
onSkipped?.(id, "its id is malformed");
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
pins.set(id, destinationDigest(await canonicalise(entry.path)));
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
// Rule 8, and `registeredWorkspaceIds` one file over carries the long version of why: this was a bare
|
|
130
|
+
// `catch {}`, so an unmounted worktree or a directory not yet created dropped a workspace silently and
|
|
131
|
+
// the operator met it later as "no destination pin was inherited", a message that names neither the
|
|
132
|
+
// directory nor the reason. Unresolvable still means no pin — routing must not invent one — but the
|
|
133
|
+
// caller is told which id and why.
|
|
134
|
+
onSkipped?.(id, `its destination could not be canonicalised (${String(error)})`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return pins;
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=workspace-pin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-pin.js","sourceRoot":"","sources":["../../src/kernel/workspace-pin.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ParsedPin } from "./workspace-pin.ts";
|
|
1
2
|
export { ENV_WORKSPACE_REGISTRY } from "./env-names.ts";
|
|
2
3
|
export type WorkspaceAccess = "read" | "write";
|
|
3
4
|
export interface WorkspaceRegistryFile {
|
|
@@ -44,11 +45,29 @@ export declare function loadWorkspaceRegistry(path: string): Promise<WorkspaceRe
|
|
|
44
45
|
* the old behaviour, which is why this is an optional parameter and not a changed return type.
|
|
45
46
|
*/
|
|
46
47
|
export declare function registeredWorkspaceIds(registryPath?: string | undefined, onRefusal?: (reason: string) => void): Promise<string[]>;
|
|
47
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Resolve an authorised id to a worktree, refusing if the id no longer means what the grant meant.
|
|
50
|
+
*
|
|
51
|
+
* **`pin` is REQUIRED, and it used to default to reading the environment.** That default was the last
|
|
52
|
+
* environment fallback in the routing path, and it was the wrong reassurance: after `publishChildEnv` the
|
|
53
|
+
* variable holds a session's CHILD's pin, so a two-argument call checked a session against its child's
|
|
54
|
+
* authority. It is dead in-tree — the one production caller always passes one — but this is a public export,
|
|
55
|
+
* so an embedder could reach it. Requiring the argument closes it permanently and makes "a session's own pin
|
|
56
|
+
* lives in memory" unbreakable rather than merely currently-true.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveWorkspace(registry: WorkspaceRegistryFile, workspaceId: string, pin: ParsedPin): Promise<ValidatedWorkspace>;
|
|
48
59
|
/**
|
|
49
60
|
* Canonicalize and validate the initial workspace against Git's registered worktree list.
|
|
50
61
|
* This prevents accidental misrouting. It does not constrain any path a child accesses after spawn.
|
|
51
62
|
*/
|
|
63
|
+
/**
|
|
64
|
+
* Canonicalise and verify a registered root as a git worktree.
|
|
65
|
+
*
|
|
66
|
+
* **This performs NO destination-pin check (ADR-0042), deliberately and dangerously.** It answers "is this
|
|
67
|
+
* path the worktree it claims to be", not "may this session route here" — `resolveWorkspace` is the one that
|
|
68
|
+
* asks the second question, and it is the only path production takes. It is a public export, so it is said
|
|
69
|
+
* here rather than left to be discovered: an embedder calling this directly routes unpinned.
|
|
70
|
+
*/
|
|
52
71
|
export declare function validateRegisteredWorkspace(input: {
|
|
53
72
|
workspaceId: string;
|
|
54
73
|
registeredRoot: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../src/kernel/workspace.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAIxD,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;IAC7C;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,wFAAwF;IACxF,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB;AAsBD,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAyFxF;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,sBAAsB,CAC1C,YAAY,qBAAsC,EAClD,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GACnC,OAAO,CAAC,MAAM,EAAE,CAAC,CAQnB;AAED,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,qBAAqB,EAC/B,WAAW,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../src/kernel/workspace.ts"],"names":[],"mappings":"AAWA,OAAO,EAA0B,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAIxD,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;IAC7C;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,wFAAwF;IACxF,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB;AAsBD,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAyFxF;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,sBAAsB,CAC1C,YAAY,qBAAsC,EAClD,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GACnC,OAAO,CAAC,MAAM,EAAE,CAAC,CAQnB;AAED;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,qBAAqB,EAC/B,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,SAAS,GACb,OAAO,CAAC,kBAAkB,CAAC,CA4B7B;AAED;;;GAGG;AACH;;;;;;;GAOG;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,CA0D9B"}
|
package/dist/kernel/workspace.js
CHANGED
|
@@ -9,6 +9,7 @@ import { promisify } from "node:util";
|
|
|
9
9
|
import { GovernanceRefusal, refusal } from "./refusals.js";
|
|
10
10
|
import { isSafeWorkspaceId, workspaceCapability } from "./capabilities.js";
|
|
11
11
|
import { ENV_WORKSPACE_REGISTRY } from "./env-names.js";
|
|
12
|
+
import { checkPinnedDestination } from "./workspace-pin.js";
|
|
12
13
|
export { ENV_WORKSPACE_REGISTRY } from "./env-names.js";
|
|
13
14
|
const execFileAsync = promisify(execFile);
|
|
14
15
|
/**
|
|
@@ -127,7 +128,17 @@ export async function registeredWorkspaceIds(registryPath = process.env[ENV_WORK
|
|
|
127
128
|
return [];
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Resolve an authorised id to a worktree, refusing if the id no longer means what the grant meant.
|
|
133
|
+
*
|
|
134
|
+
* **`pin` is REQUIRED, and it used to default to reading the environment.** That default was the last
|
|
135
|
+
* environment fallback in the routing path, and it was the wrong reassurance: after `publishChildEnv` the
|
|
136
|
+
* variable holds a session's CHILD's pin, so a two-argument call checked a session against its child's
|
|
137
|
+
* authority. It is dead in-tree — the one production caller always passes one — but this is a public export,
|
|
138
|
+
* so an embedder could reach it. Requiring the argument closes it permanently and makes "a session's own pin
|
|
139
|
+
* lives in memory" unbreakable rather than merely currently-true.
|
|
140
|
+
*/
|
|
141
|
+
export async function resolveWorkspace(registry, workspaceId, pin) {
|
|
131
142
|
const registered = Object.hasOwn(registry.workspaces, workspaceId) ? registry.workspaces[workspaceId] : undefined;
|
|
132
143
|
const known = Object.keys(registry.workspaces).sort();
|
|
133
144
|
if (!registered) {
|
|
@@ -135,12 +146,31 @@ export async function resolveWorkspace(registry, workspaceId) {
|
|
|
135
146
|
(registry.source ? ` ${registry.source}` : "") +
|
|
136
147
|
(known.length > 0 ? ` — it lists: ${known.join(", ")}` : " — it lists nothing"), { workspace_id: workspaceId, ...(registry.source ? { registry_path: registry.source } : {}) }));
|
|
137
148
|
}
|
|
138
|
-
|
|
149
|
+
const validated = await validateRegisteredWorkspace({ workspaceId, registeredRoot: registered.path });
|
|
150
|
+
// **ADR-0042, and it is checked HERE on purpose.** The capability check upstream asks whether this session may
|
|
151
|
+
// route to this NAME. This asks whether the name still points where it pointed when the name was granted —
|
|
152
|
+
// the question `g37-registry-tamper` showed nobody was asking. It runs after canonicalisation because the
|
|
153
|
+
// digest is of the canonical root; comparing the registry's raw string would be defeated by a symlink.
|
|
154
|
+
const mismatch = checkPinnedDestination({ workspaceId, canonicalRoot: validated.root, pin });
|
|
155
|
+
if (mismatch)
|
|
156
|
+
throw new GovernanceRefusal(refusal("WORKSPACE_NOT_AUTHORIZED", `workspace ${workspaceId} is not routable: ${mismatch}`, {
|
|
157
|
+
workspace_id: workspaceId,
|
|
158
|
+
resolved_root: validated.root,
|
|
159
|
+
}));
|
|
160
|
+
return validated;
|
|
139
161
|
}
|
|
140
162
|
/**
|
|
141
163
|
* Canonicalize and validate the initial workspace against Git's registered worktree list.
|
|
142
164
|
* This prevents accidental misrouting. It does not constrain any path a child accesses after spawn.
|
|
143
165
|
*/
|
|
166
|
+
/**
|
|
167
|
+
* Canonicalise and verify a registered root as a git worktree.
|
|
168
|
+
*
|
|
169
|
+
* **This performs NO destination-pin check (ADR-0042), deliberately and dangerously.** It answers "is this
|
|
170
|
+
* path the worktree it claims to be", not "may this session route here" — `resolveWorkspace` is the one that
|
|
171
|
+
* asks the second question, and it is the only path production takes. It is a public export, so it is said
|
|
172
|
+
* here rather than left to be discovered: an embedder calling this directly routes unpinned.
|
|
173
|
+
*/
|
|
144
174
|
export async function validateRegisteredWorkspace(input) {
|
|
145
175
|
let registered;
|
|
146
176
|
let supplied;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../src/kernel/workspace.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../src/kernel/workspace.ts"],"names":[],"mappings":"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"}
|
|
@@ -34,6 +34,8 @@ export interface GrantsCommandContext {
|
|
|
34
34
|
executor: ExecutorChoice;
|
|
35
35
|
/** ADR-0077: which advisor is in force, or why none is. Reported because an advisor sends task text out. */
|
|
36
36
|
advisor: { decider: string; refusal?: string };
|
|
37
|
+
/** ADR-0042: which ids this session pinned, so a routing refusal is discoverable before it happens. */
|
|
38
|
+
workspacePin?: ReadonlyMap<string, string>;
|
|
37
39
|
observed: boolean;
|
|
38
40
|
depth: number;
|
|
39
41
|
maxDepth: number;
|
|
@@ -92,6 +94,7 @@ export const grantsCommand = {
|
|
|
92
94
|
ownGrant,
|
|
93
95
|
executor,
|
|
94
96
|
advisor,
|
|
97
|
+
workspacePin,
|
|
95
98
|
observed,
|
|
96
99
|
depth,
|
|
97
100
|
maxDepth,
|
|
@@ -379,7 +382,19 @@ export const grantsCommand = {
|
|
|
379
382
|
`${catalog.byKind("skill").length} skill, ${catalog.byKind("agentType").length} agent-type, ` +
|
|
380
383
|
`${catalog.byKind("workspace").length} workspace`,
|
|
381
384
|
...(catalog.byKind("workspace").length > 0
|
|
382
|
-
? [
|
|
385
|
+
? [
|
|
386
|
+
` routable ${catalog.byKind("workspace").join(", ")} — held ones only are usable (ADR-0035)`,
|
|
387
|
+
// ADR-0042 made a destination pin a PRECONDITION for routing, and it had no operator surface at
|
|
388
|
+
// all: not here, not at session start, not in the README. An operator refused for want of a pin
|
|
389
|
+
// could not discover that the mechanism existed, let alone which ids it covered.
|
|
390
|
+
` pinned ${
|
|
391
|
+
workspacePin === undefined
|
|
392
|
+
? "(none — no workspace is routable this session)"
|
|
393
|
+
: workspacePin.size === 0
|
|
394
|
+
? "(none inherited — this session may route nowhere)"
|
|
395
|
+
: `${[...workspacePin.keys()].sort().join(", ")} — routing refuses if a registry entry moves`
|
|
396
|
+
}`,
|
|
397
|
+
]
|
|
383
398
|
: []),
|
|
384
399
|
// Rule 8's loud half again. The catalog fails soft on an unreadable registry, which is right, and used
|
|
385
400
|
// to discard the reason with it, which was not: one malformed entry removed every workspace from this
|
package/extensions/grants.ts
CHANGED
|
@@ -378,6 +378,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
378
378
|
decider: session.advisorSession.deciderName,
|
|
379
379
|
...(session.advisorSession.settings.refusal ? { refusal: session.advisorSession.settings.refusal } : {}),
|
|
380
380
|
},
|
|
381
|
+
...(session.workspacePin ? { workspacePin: session.workspacePin } : {}),
|
|
381
382
|
catalog: session.catalog,
|
|
382
383
|
definitions: session.definitions,
|
|
383
384
|
sessionApprovals: session.sessionApprovals,
|
|
@@ -5,6 +5,20 @@ export interface ReloadLifecycle {
|
|
|
5
5
|
root: Record<string, string | undefined>;
|
|
6
6
|
published?: Record<string, string | undefined>;
|
|
7
7
|
activityRootId?: string;
|
|
8
|
+
/**
|
|
9
|
+
* The destination pin this owner settled on (ADR-0042), which must survive an extension reload.
|
|
10
|
+
*
|
|
11
|
+
* **Here rather than on the session, because a reload builds a NEW session.** `session.pinSettled` closed
|
|
12
|
+
* the `/grants init` re-mint, which reuses one session object, and left the reload open: a fresh object has
|
|
13
|
+
* no flag, a root legitimately inherited nothing and sits at depth 0, so it minted a second time — against
|
|
14
|
+
* whatever the registry said by then, which a child with `tool:write` had had the whole session to rewrite.
|
|
15
|
+
* A security review reproduced it. That is the same rule-on-the-object, second-path-builds-another-object
|
|
16
|
+
* shape this package keeps finding, three times in this feature alone.
|
|
17
|
+
*
|
|
18
|
+
* The lifecycle is keyed by owner in a `WeakMap` and is already the thing that "recovers its root rather
|
|
19
|
+
* than its child publication", so it is where settled-ness belongs.
|
|
20
|
+
*/
|
|
21
|
+
workspacePin?: ReadonlyMap<string, string>;
|
|
8
22
|
}
|
|
9
23
|
type SessionOwner = object;
|
|
10
24
|
|
|
@@ -57,6 +71,17 @@ export function bindReloadLifecycle(
|
|
|
57
71
|
if (!holder.latestChildPublication || !same(current, holder.latestChildPublication.environment)) {
|
|
58
72
|
// No pi-daddy lifecycle published what is currently in process.env, so this is an explicit change to
|
|
59
73
|
// this owner's root rather than another bound session's child state.
|
|
74
|
+
// **An explicit root replacement is a NEW grant, so the settled pin must go with the old one — but only
|
|
75
|
+
// when the root ACTUALLY changed.** The condition above is broader than this comment used to claim: its
|
|
76
|
+
// first disjunct fires when nothing has published yet in the process, where nothing has been replaced at
|
|
77
|
+
// all. Deleting there re-opened the mint window on the next reload, which review reproduced. Comparing
|
|
78
|
+
// against the stored root is what makes "replacement" mean replacement.
|
|
79
|
+
//
|
|
80
|
+
// The widening this closes: a replacement was honoured for the grant, the depth and the approvals and
|
|
81
|
+
// silently ignored for the pin, so a session reconciled to depth 1 — believing itself a descendant —
|
|
82
|
+
// kept a root-minted pin naming a workspace the replacement root never granted. Deleting it makes the
|
|
83
|
+
// next `establishRootPin` re-settle from the new root, which for a descendant means minting nothing.
|
|
84
|
+
if (!same(current, existing.root)) delete existing.workspacePin;
|
|
60
85
|
existing.root = current;
|
|
61
86
|
}
|
|
62
87
|
return { lifecycle: existing, environment: withRoot(existing.root) };
|
|
@@ -328,6 +328,7 @@ export async function runOneDelegation(
|
|
|
328
328
|
if (plan.ok || shouldSeekApproval(plan.result)) {
|
|
329
329
|
try {
|
|
330
330
|
preparedWorkspace = await prepareDelegationWorkspace({
|
|
331
|
+
...(session.workspacePin ? { workspacePin: session.workspacePin } : {}),
|
|
331
332
|
spec: { ...spec.workspace, access: governedWorkspaceAccess(spec.workspace.access, plan.requested) },
|
|
332
333
|
correlation: spec.correlation,
|
|
333
334
|
childId: ids.childId,
|
|
@@ -67,6 +67,13 @@ export async function reportSessionStart(session: GrantsSession, ctx: SessionRep
|
|
|
67
67
|
// the moment somebody tries to delegate to it.
|
|
68
68
|
for (const reason of session.definitionSkips)
|
|
69
69
|
ctx.ui.notify(`grants: a definition was not loaded — ${reason}`, "warning");
|
|
70
|
+
// A registered workspace that could not be pinned is not routable this session, and the routing refusal an
|
|
71
|
+
// operator would otherwise meet names neither the directory nor the reason.
|
|
72
|
+
for (const skipped of session.workspaceSkips)
|
|
73
|
+
ctx.ui.notify(
|
|
74
|
+
`grants: workspace ${skipped} — it is registered but not routable this session (ADR-0042 destination pin)`,
|
|
75
|
+
"warning",
|
|
76
|
+
);
|
|
70
77
|
if (session.catalog.registryRefusal)
|
|
71
78
|
ctx.ui.notify(
|
|
72
79
|
`grants: workspace registry unreadable, no workspace is routable this session — ${session.catalog.registryRefusal}`,
|
package/extensions/session.ts
CHANGED
|
@@ -56,6 +56,16 @@ import { agentDir, projectSettingsPath } from "../src/kernel/project-paths.ts";
|
|
|
56
56
|
import { ENV_ALLOW_UNRESOLVED_MODELS } from "../src/kernel/model-preflight.ts";
|
|
57
57
|
import { beginExtensionLifecycle, rememberChildPublication, type ReloadLifecycle } from "./reload-environment.ts";
|
|
58
58
|
import { reconcileSessionEnvironment } from "./session-environment.ts";
|
|
59
|
+
import { realpath } from "node:fs/promises";
|
|
60
|
+
import {
|
|
61
|
+
establishWorkspacePin,
|
|
62
|
+
formatWorkspacePin,
|
|
63
|
+
parseWorkspacePin,
|
|
64
|
+
type WorkspacePins,
|
|
65
|
+
ENV_WORKSPACE_PIN,
|
|
66
|
+
} from "../src/kernel/workspace-pin.ts";
|
|
67
|
+
import { loadWorkspaceRegistry } from "../src/kernel/workspace.ts";
|
|
68
|
+
|
|
59
69
|
/**
|
|
60
70
|
* Run governed children in herdr panes instead of captured child processes.
|
|
61
71
|
*
|
|
@@ -196,6 +206,19 @@ export interface GrantsSession extends NativeSessionHost {
|
|
|
196
206
|
* capability.
|
|
197
207
|
*/
|
|
198
208
|
definitionSkips: string[];
|
|
209
|
+
/**
|
|
210
|
+
* This session's OWN destination pin (ADR-0042), captured once at start.
|
|
211
|
+
*
|
|
212
|
+
* **In memory, not re-read from the environment, and that is not a style choice.** `publishChildEnv` writes
|
|
213
|
+
* the CHILD's narrowed pin into `process.env` — that is how a child inherits anything here — so a session
|
|
214
|
+
* that re-read the variable at routing time would read its child's pin and lose its own. `ownGrant` has
|
|
215
|
+
* lived in memory for exactly this reason; the pin reached the same trap, and a test caught it.
|
|
216
|
+
*/
|
|
217
|
+
workspacePin?: WorkspacePins;
|
|
218
|
+
/** Whether this session has already settled its pin. Minting twice is how a reload laundered a tamper. */
|
|
219
|
+
pinSettled: boolean;
|
|
220
|
+
/** Registered workspaces this session could not pin, and why — reported at session start (rule 8). */
|
|
221
|
+
workspaceSkips: string[];
|
|
199
222
|
catalog: Catalog;
|
|
200
223
|
/**
|
|
201
224
|
* The in-flight catalog build, so `delegate` can wait for it instead of racing it.
|
|
@@ -264,7 +287,85 @@ export interface GrantsSession extends NativeSessionHost {
|
|
|
264
287
|
* selling point is "no restart"). Two copies of these three steps is how the two callers come to disagree
|
|
265
288
|
* about what loading means, so there is one.
|
|
266
289
|
*/
|
|
290
|
+
/**
|
|
291
|
+
* ADR-0042: a root records what each registered workspace id MEANT, once, before anything can rewrite it.
|
|
292
|
+
*
|
|
293
|
+
* **Only a session that inherited no pin establishes one.** A descendant that could mint its own would rewrite
|
|
294
|
+
* the registry, re-establish, and route anywhere — the mechanism would be a comment. So an inherited value is
|
|
295
|
+
* left exactly as it arrived, including an empty one, which says "your parent established a pin and gave you
|
|
296
|
+
* nothing from it" and refuses at routing with its own message.
|
|
297
|
+
*
|
|
298
|
+
* Failing to establish is not an error: a machine with no registry has no workspaces to route to, and the
|
|
299
|
+
* absent pin refuses anything that tries. That is the same direction as every other failure in this mechanism.
|
|
300
|
+
*/
|
|
301
|
+
async function establishRootPin(session: GrantsSession): Promise<void> {
|
|
302
|
+
if (session.pinSettled) return;
|
|
303
|
+
// **One assignment, at the end, on every path.** Review found the previous shape — assign at each `return`
|
|
304
|
+
// — missing two of five exits: the `catch` around an unreadable registry, which is precisely the state a
|
|
305
|
+
// child can create by truncating the file, and the no-registry return. A root that took either reached the
|
|
306
|
+
// RELOAD with the lifecycle still empty and minted over whatever the registry said by then, routing into
|
|
307
|
+
// prod. That is the checklist failing, so the checklist is gone: the body computes a value and the caller
|
|
308
|
+
// assigns both fields once.
|
|
309
|
+
// **Settled AFTER the value exists.** Setting the flag first meant a throw would leave the session marked
|
|
310
|
+
// settled with nothing settled — routing nowhere, which is safe, but with the LIFECYCLE unset, so the next
|
|
311
|
+
// reload would mint again. Review could construct no throw today; "currently unreachable" is exactly the
|
|
312
|
+
// property this feature has now been wrong about four times, and the ordering costs nothing.
|
|
313
|
+
const settled = await settleWorkspacePin(session);
|
|
314
|
+
session.pinSettled = true;
|
|
315
|
+
session.workspacePin = settled;
|
|
316
|
+
session.reloadLifecycle.workspacePin = settled;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* What this session's destination pin IS (ADR-0042). Every path returns a map; none writes anything.
|
|
321
|
+
*
|
|
322
|
+
* An empty map means "settled, and you may route nowhere", which is different from never having settled and
|
|
323
|
+
* is what every failure resolves to. The distinction that matters is not empty-versus-absent but
|
|
324
|
+
* settled-versus-not, and settling happens exactly once per owner.
|
|
325
|
+
*/
|
|
326
|
+
async function settleWorkspacePin(session: GrantsSession): Promise<WorkspacePins> {
|
|
327
|
+
// Settled by an EARLIER SESSION OBJECT for this same owner — an extension reload. Adopting rather than
|
|
328
|
+
// re-deriving is the point: a root may mint, but only once, and only from the registry as it stood before
|
|
329
|
+
// any child had a chance to rewrite it.
|
|
330
|
+
if (session.reloadLifecycle.workspacePin) return session.reloadLifecycle.workspacePin;
|
|
331
|
+
|
|
332
|
+
const raw = session.reloadLifecycle.root[ENV_WORKSPACE_PIN];
|
|
333
|
+
const inherited = parseWorkspacePin(raw);
|
|
334
|
+
// Inherited, so it is authority and is kept exactly as it arrived — including an empty one, which says
|
|
335
|
+
// "your parent established a pin and gave you none of it".
|
|
336
|
+
if ("pins" in inherited) return inherited.pins;
|
|
337
|
+
|
|
338
|
+
// **A DESCENDANT NEVER MINTS.** Review reproduced the escalation end to end across a real process boundary:
|
|
339
|
+
// `workspacePinEnv` OMITS the variable when a parent has no pin of its own — which happens whenever that
|
|
340
|
+
// parent's registry was unreadable at its start, a state any child with `tool:write` can arrange — and the
|
|
341
|
+
// child then read the absence as "I am a root", minted from the registry it had just rewritten, and routed
|
|
342
|
+
// to the prod worktree holding only `workspace:staging`. Depth already rides in the environment and already
|
|
343
|
+
// attenuates downward, so it is enough on its own.
|
|
344
|
+
//
|
|
345
|
+
// A MALFORMED value refuses for the same reason even at the root. The module header says missing, empty,
|
|
346
|
+
// malformed and mismatched all refuse; that was true of the routing check and false here, where a refusal
|
|
347
|
+
// fell through to minting. A tamperer who can corrupt one byte must not thereby earn a promotion.
|
|
348
|
+
if (session.depth > 0 || raw !== undefined) return new Map();
|
|
349
|
+
|
|
350
|
+
const registryPath = process.env[ENV_WORKSPACE_REGISTRY];
|
|
351
|
+
if (!registryPath) return new Map();
|
|
352
|
+
try {
|
|
353
|
+
const registry = await loadWorkspaceRegistry(registryPath);
|
|
354
|
+
return await establishWorkspacePin(registry, realpath, (id, reason) =>
|
|
355
|
+
session.workspaceSkips.push(`${id} — ${reason}`),
|
|
356
|
+
);
|
|
357
|
+
} catch {
|
|
358
|
+
// An unreadable registry is already reported by the catalog and at session start. It does NOT follow that
|
|
359
|
+
// nothing is blocked — an earlier draft of this comment claimed that and review measured it false, because
|
|
360
|
+
// a grant supplied through `PI_DADDY_GRANT` never passes the catalog and `workspace:` is exempt from the
|
|
361
|
+
// unknown check anyway. A session can genuinely hold `workspace:w1` and be refused for want of a pin.
|
|
362
|
+
// Returning an empty map SETTLES it, so a later reload cannot mint over a registry that has since changed.
|
|
363
|
+
return new Map();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
267
367
|
export async function loadProjectDefinitions(session: GrantsSession, cwd: string): Promise<void> {
|
|
368
|
+
await establishRootPin(session);
|
|
268
369
|
const skips: string[] = [];
|
|
269
370
|
session.definitions = await loadDefinitions(cwd, (_path, reason) => skips.push(reason));
|
|
270
371
|
session.definitionSkips = skips;
|
|
@@ -331,6 +432,9 @@ export function createGrantsSession(
|
|
|
331
432
|
maxDepth,
|
|
332
433
|
malformedBounds: bounds.malformed,
|
|
333
434
|
definitionSkips: [],
|
|
435
|
+
workspacePin: undefined,
|
|
436
|
+
pinSettled: false,
|
|
437
|
+
workspaceSkips: [],
|
|
334
438
|
// ADR-0012: `bash` is gated by DEFAULT — but only in a governed session. An ungoverned one
|
|
335
439
|
// (no PI_DADDY_GRANT) still blocks nothing, so "governance is opt-in" holds exactly where it always
|
|
336
440
|
// did. Inside a session the operator already chose to govern, handing a child `bash` hands it an
|
|
@@ -388,6 +492,10 @@ export function createGrantsSession(
|
|
|
388
492
|
extensionPath: session.extensionPath,
|
|
389
493
|
observerExtensionPath: session.observerExtensionPath,
|
|
390
494
|
childEnv: activityChildEnv(session.activity),
|
|
495
|
+
// ADR-0042: the pin this session holds, handed to the kernel so a delegated child inherits a narrowed
|
|
496
|
+
// one through the same builder `publishChildEnv` uses. Read here rather than in the kernel so there is
|
|
497
|
+
// one place that knows the environment is where a session's own pin lives.
|
|
498
|
+
...(session.workspacePin ? { workspacePin: session.workspacePin } : {}),
|
|
391
499
|
// ADR-0078: composition reads, the kernel decides. Called only for a mode that survived the gate.
|
|
392
500
|
// `options` is forwarded, and its absence is why the second decision point was dead in production: a
|
|
393
501
|
// one-parameter arrow is assignable to a two-parameter type, so the ids reached here and were discarded while
|
|
@@ -440,6 +548,9 @@ export function createGrantsSession(
|
|
|
440
548
|
gated: session.gated,
|
|
441
549
|
ledgerPath: session.ledgerPath,
|
|
442
550
|
approved: republishable(session),
|
|
551
|
+
// ADR-0042. The pin this session holds, which `childEnv` narrows to what the child's grant names. A
|
|
552
|
+
// session with no pin passes none, and its children can route nowhere — the fail-closed direction.
|
|
553
|
+
...(session.workspacePin ? { workspacePin: session.workspacePin } : {}),
|
|
443
554
|
// G7 / B-I8: an ungoverned session publishes nothing, so "governance is opt-in" holds for
|
|
444
555
|
// descendants too. Previously it exported its own observed tool surface as their grant.
|
|
445
556
|
governed: session.governed,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CorrelationMetadata } from "../src/kernel/correlation.ts";
|
|
2
|
+
import type { WorkspacePins } from "../src/kernel/workspace-pin.ts";
|
|
2
3
|
import type { Capability } from "../src/kernel/resolve.ts";
|
|
3
4
|
import { appendLedgerEvent, buildWorkspaceLeaseEvent } from "../src/governance/ledger.ts";
|
|
4
5
|
import { GovernanceRefusal, refusal, type StructuredRefusal } from "../src/kernel/refusals.ts";
|
|
@@ -72,6 +73,8 @@ export async function prepareDelegationWorkspace(input: {
|
|
|
72
73
|
parentExecutionId: string | null;
|
|
73
74
|
signal?: AbortSignal;
|
|
74
75
|
ledgerPath?: string;
|
|
76
|
+
/** The CALLING session's own destination pin (ADR-0042); the environment holds its child's, not its own. */
|
|
77
|
+
workspacePin?: WorkspacePins;
|
|
75
78
|
}): Promise<PreparedWorkspace> {
|
|
76
79
|
if (input.correlation?.workspace_id && input.correlation.workspace_id !== input.spec.workspace_id) {
|
|
77
80
|
throw new GovernanceRefusal(
|
|
@@ -90,7 +93,19 @@ export async function prepareDelegationWorkspace(input: {
|
|
|
90
93
|
}),
|
|
91
94
|
);
|
|
92
95
|
}
|
|
93
|
-
|
|
96
|
+
// ADR-0042. The pin comes from the caller's session, NOT from `process.env`: `publishChildEnv` writes the
|
|
97
|
+
// child's narrowed pin into the environment, so reading it back here would check this session's routing
|
|
98
|
+
// against its child's authority. `ownGrant` has always lived in memory for the same reason.
|
|
99
|
+
const workspace = await resolveWorkspace(
|
|
100
|
+
await loadWorkspaceRegistry(registryPath),
|
|
101
|
+
input.spec.workspace_id,
|
|
102
|
+
// **No environment fallback.** Review called the old `?? parseWorkspacePin(process.env[...])` a live read
|
|
103
|
+
// of a variable the session no longer owns — `publishChildEnv` writes its CHILD's pin there. No state
|
|
104
|
+
// could be constructed where it read a usable value, but "currently unreachable" is a weaker property
|
|
105
|
+
// than "cannot happen", and the whole rule here is that a session's own pin lives in memory. A caller
|
|
106
|
+
// with no pin routes nowhere, which is what a caller with no pin should do.
|
|
107
|
+
input.workspacePin ? { pins: input.workspacePin } : { pins: new Map() },
|
|
108
|
+
);
|
|
94
109
|
let lease: WorkspaceLease | undefined;
|
|
95
110
|
try {
|
|
96
111
|
lease = await acquireWorkspaceLease({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-daddy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.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",
|
package/src/index.ts
CHANGED
|
@@ -178,3 +178,16 @@ export * from "./products/dashboard-handshake.ts";
|
|
|
178
178
|
export * from "./products/dashboard-herdr.ts";
|
|
179
179
|
export * from "./products/activity-timeline.ts";
|
|
180
180
|
export { runDashboard, dashboardFrame, DASHBOARD_PROTOCOL_VERSION } from "./products/dashboard-cli.ts";
|
|
181
|
+
|
|
182
|
+
// ADR-0042. Exported because `resolveWorkspace` gained a destination-pin precondition in 0.38.0, and without
|
|
183
|
+
// these a public consumer has no supported way to satisfy it — the parameter defaults to reading an
|
|
184
|
+
// environment variable whose name and format were internal.
|
|
185
|
+
export {
|
|
186
|
+
destinationDigest,
|
|
187
|
+
establishWorkspacePin,
|
|
188
|
+
formatWorkspacePin,
|
|
189
|
+
parseWorkspacePin,
|
|
190
|
+
ENV_WORKSPACE_PIN,
|
|
191
|
+
type WorkspacePins,
|
|
192
|
+
type ParsedPin,
|
|
193
|
+
} from "./kernel/workspace-pin.ts";
|