pi-daddy 0.17.0 → 0.18.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 +154 -80
- 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/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 +68 -72
- 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/refusals.d.ts +16 -0
- package/dist/refusals.d.ts.map +1 -0
- package/dist/refusals.js +51 -0
- package/dist/refusals.js.map +1 -0
- 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/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 +80 -179
- 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/refusals.ts +66 -0
- 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
package/src/delegate.ts
CHANGED
|
@@ -1,24 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Governed delegation
|
|
3
|
-
*
|
|
4
|
-
* The `tool_call` interceptor can only *permit or refuse* a `pi-subagents` spawn, because that package's
|
|
5
|
-
* `Agent` tool has no `tools` parameter. When we do the spawning ourselves the grant becomes an argument,
|
|
6
|
-
* which is what "give them some tools but not others" actually requires.
|
|
7
|
-
*
|
|
8
|
-
* Two properties fall out of owning the spawn:
|
|
9
|
-
*
|
|
10
|
-
* 1. **No propagation race at all.** Each child receives its own explicit `env` object, so nothing is
|
|
11
|
-
* written to the shared `process.env`. The interceptor's constraint (only parent-level facts may be
|
|
12
|
-
* pushed, because the channel is global) does not apply here.
|
|
13
|
-
* 2. **Depth control by capability.** `tool:delegate` is itself a capability. Grant it and the child can
|
|
14
|
-
* sub-delegate; withhold it and the child is a leaf. No separate depth mechanism is required, though
|
|
15
|
-
* `maxDepth` remains as a cheap backstop.
|
|
2
|
+
* Governed delegation planning. Owning the spawn makes the grant an argument; each child receives its own
|
|
3
|
+
* environment, and `tool:delegate` determines whether it is a delegator or a leaf.
|
|
16
4
|
*/
|
|
17
5
|
|
|
18
6
|
import { planSpawn } from "./spawn.ts";
|
|
19
7
|
import { ceilingForDefinition, digestDefinition, type DefinitionDigest, type SkillDefinition } from "./definitions.ts";
|
|
20
|
-
import {
|
|
21
|
-
import { AGENT_WILDCARD } from "./resolve.ts";
|
|
8
|
+
import { assertNarrowing, type Capability, type ResolveResult } from "./resolve.ts";
|
|
22
9
|
import { DELEGATE_CAPABILITY, agentCapability, maySpawnDefinition, normaliseCapability } from "./capabilities.ts";
|
|
23
10
|
|
|
24
11
|
// Re-exported so the split stays internal: `delegate.ts` has been the import site for these since 0.6.0 and
|
|
@@ -26,128 +13,41 @@ import { DELEGATE_CAPABILITY, agentCapability, maySpawnDefinition, normaliseCapa
|
|
|
26
13
|
// charged to every caller for a line count they did not cause.
|
|
27
14
|
export { DELEGATE_CAPABILITY, agentCapability, maySpawnDefinition, normaliseCapability } from "./capabilities.ts";
|
|
28
15
|
import { ENV_APPROVED, ENV_DEPTH, ENV_FANOUT, ENV_GATED, ENV_GRANT, ENV_LEDGER, ENV_MAX_DEPTH, ENV_PARENT_ID } from "./propagation.ts";
|
|
29
|
-
import {
|
|
16
|
+
import { inheritApprovals, type InheritableApproval } from "./approval.ts";
|
|
30
17
|
import { suggestForUnknown, unknownCapabilities, type Catalog } from "./catalog.ts";
|
|
18
|
+
import { GovernanceRefusal, refusal, type RefusalCode, type StructuredRefusal } from "./refusals.ts";
|
|
19
|
+
import {
|
|
20
|
+
digestTask,
|
|
21
|
+
normaliseCorrelation,
|
|
22
|
+
type ApprovalBinding,
|
|
23
|
+
type CorrelationMetadata,
|
|
24
|
+
} from "./correlation.ts";
|
|
25
|
+
import { resolveDelegationApproval } from "./delegation-approval.ts";
|
|
26
|
+
import type { Delegation, DelegationContext, DelegationRequest } from "./delegate-types.ts";
|
|
27
|
+
export type { Delegation, DelegationContext, DelegationRequest } from "./delegate-types.ts";
|
|
31
28
|
|
|
32
|
-
export interface DelegationRequest {
|
|
33
|
-
task: string;
|
|
34
|
-
/**
|
|
35
|
-
* Capabilities the delegator wants the child to hold.
|
|
36
|
-
*
|
|
37
|
-
* Optional since ADR-0016: prefer `agent`, which names an operator-authored definition. This form
|
|
38
|
-
* lets the MODEL choose the capability set, which is the weaker arrangement — it is still bounded by
|
|
39
|
-
* the session grant (ADR-0008), so it cannot escalate, but nothing about it was reviewed by a human.
|
|
40
|
-
*/
|
|
41
|
-
tools?: string[];
|
|
42
|
-
/**
|
|
43
|
-
* Name of a `SKILL.md` definition to spawn (ADR-0016).
|
|
44
|
-
*
|
|
45
|
-
* When given, the definition's `allowed-tools` is the ceiling and its body is the child's system
|
|
46
|
-
* prompt. The model chooses only *which* definition and *what* task; the capability set is the
|
|
47
|
-
* operator's, written down in a file.
|
|
48
|
-
*/
|
|
49
|
-
agent?: string;
|
|
50
|
-
model?: string;
|
|
51
|
-
provider?: string;
|
|
52
|
-
thinking?: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface DelegationContext {
|
|
56
|
-
ownGrant: Capability[];
|
|
57
|
-
depth: number;
|
|
58
|
-
maxDepth: number;
|
|
59
|
-
gated: Capability[];
|
|
60
|
-
/**
|
|
61
|
-
* Approvals in force for this delegation, with subject and scope (ADR-0014).
|
|
62
|
-
*
|
|
63
|
-
* One source of truth for two different questions. The **gate check here** honours every entry,
|
|
64
|
-
* including `once` — that approval applies to *this* spawn, which is exactly what the human said yes
|
|
65
|
-
* to. What crosses to the CHILD is `inheritApprovals`, which drops `once` and keeps the subject, so
|
|
66
|
-
* the same list cannot silently authorise a subtree.
|
|
67
|
-
*/
|
|
68
|
-
approved?: InheritableApproval[];
|
|
69
|
-
ledgerPath?: string;
|
|
70
|
-
/** Path to this extension, so a child granted `tool:delegate` can delegate in turn. */
|
|
71
|
-
extensionPath?: string;
|
|
72
|
-
/** Live capability catalog. When supplied, capabilities absent from it are refused as unknown. */
|
|
73
|
-
catalog?: Catalog;
|
|
74
|
-
/**
|
|
75
|
-
* Absolute path per skill NAME, from the catalog's `source` field (R-32).
|
|
76
|
-
*
|
|
77
|
-
* Without it every granted `skill:` capability is unresolvable and the delegation is refused, which
|
|
78
|
-
* is the correct direction: a caller that cannot say where a skill lives cannot honestly grant it.
|
|
79
|
-
*/
|
|
80
|
-
skillPaths?: Record<string, string>;
|
|
81
|
-
/** Let the child load `AGENTS.md` / `CLAUDE.md`. Default false — see `planSpawn`. */
|
|
82
|
-
contextFiles?: boolean;
|
|
83
|
-
/** Known `SKILL.md` definitions by name, for `DelegationRequest.agent` (ADR-0016). */
|
|
84
|
-
definitions?: Map<string, SkillDefinition>;
|
|
85
|
-
/**
|
|
86
|
-
* Build an INTERACTIVE plan — no `--print` — for an executor that drives the child after starting it.
|
|
87
|
-
*
|
|
88
|
-
* `runHerdrPane` requires this: `--print` makes pi process the prompt and exit, so it never reaches the
|
|
89
|
-
* interactive readiness `herdr agent start` waits for and the agent is never detected. Default is the
|
|
90
|
-
* non-interactive plan, because a governed child should not sit waiting for a human by accident.
|
|
91
|
-
*/
|
|
92
|
-
interactive?: boolean;
|
|
93
|
-
/**
|
|
94
|
-
* Total descendants this session may still create (`src/fanout.ts`). Split among children by the caller.
|
|
95
|
-
*
|
|
96
|
-
* Omitted means unbounded, which is the pre-fan-out behaviour and correct for a single blocking
|
|
97
|
-
* delegation — the accident that used to bound cardinality to one.
|
|
98
|
-
*/
|
|
99
|
-
fanoutBudget?: number;
|
|
100
|
-
/** This session's ledger id, so a child's `parentId` names its real parent (F8). */
|
|
101
|
-
spawnId?: string;
|
|
102
|
-
/** Ledger id assigned to THIS child, distinguishing it from its siblings (F8). */
|
|
103
|
-
childSpawnId?: string;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export interface Delegation {
|
|
107
|
-
ok: boolean;
|
|
108
|
-
reason?: string;
|
|
109
|
-
args: string[];
|
|
110
|
-
/** Per-child environment — never merged into the parent's process.env. */
|
|
111
|
-
env: Record<string, string>;
|
|
112
|
-
effective: Capability[];
|
|
113
|
-
/**
|
|
114
|
-
* The result this plan was made from. **Required** (B-I3): while it was optional the extension
|
|
115
|
-
* guarded its ledger write with `if (ledgerPath && plan.result)`, silently dropping every refusal
|
|
116
|
-
* that returned before `resolve()` ran. The type is what keeps a new early exit auditable.
|
|
117
|
-
*/
|
|
118
|
-
result: ResolveResult;
|
|
119
|
-
childDepth: number;
|
|
120
|
-
/**
|
|
121
|
-
* The capabilities this delegation asked for, whatever route named them.
|
|
122
|
-
*
|
|
123
|
-
* Carried on the plan rather than re-derived by the caller (the B-I3 lesson): with `agent`, the
|
|
124
|
-
* request names a DEFINITION and the capabilities come from its `allowed-tools`, so a ledger that
|
|
125
|
-
* read the tool parameters would record an empty request for every definition spawn.
|
|
126
|
-
*/
|
|
127
|
-
requested: Capability[];
|
|
128
|
-
/** Ledger id for this child, if the caller assigned one (F8). */
|
|
129
|
-
childId?: string;
|
|
130
|
-
/**
|
|
131
|
-
* Which operator-authored instructions this spawn used (ADR-0018).
|
|
132
|
-
*
|
|
133
|
-
* Absent for a `tools:`-style delegation, which has no definition and therefore no instructions to
|
|
134
|
-
* identify — and absent on an ADR-0017 authorisation refusal, which is decided before the file is read.
|
|
135
|
-
*/
|
|
136
|
-
definitionDigest?: DefinitionDigest;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Plan a governed delegation. Pure: returns argv and env, spawns nothing.
|
|
141
|
-
*
|
|
142
|
-
* Fails closed on depth, on any requested capability the delegator does not hold, on gated capabilities
|
|
143
|
-
* without approval, and on a grant that cannot narrow (a universal capability slipping through).
|
|
144
|
-
*/
|
|
145
29
|
export function planDelegation(request: DelegationRequest, ctx: DelegationContext): Delegation {
|
|
146
30
|
const childDepth = ctx.depth + 1;
|
|
31
|
+
const denied = (plan: Delegation, code: RefusalCode): Delegation =>
|
|
32
|
+
plan.reason ? { ...plan, refusal: refusal(code, plan.reason) } : plan;
|
|
147
33
|
// G6 / B-I3: every refusal carries a result, including the four below that return before `resolve()`
|
|
148
34
|
// is ever called. The extension guarded its ledger write with `if (ledgerPath && plan.result)`, so
|
|
149
35
|
// those four governance decisions — disabled, too deep, no task, unknown capability — were never
|
|
150
36
|
// audited at all. An empty result is the honest record: nothing was resolved, and that is the fact.
|
|
37
|
+
// Bounded/whitelisted correlation is a REFUSAL, not an exception escaping the planner. It is reachable
|
|
38
|
+
// from a model-facing tool parameter on all three delegation tools, and throwing from here produced a
|
|
39
|
+
// governed refusal with no code and no ledger line at all — the ledger file was never even created
|
|
40
|
+
// (R-112). Caught here so it becomes an ordinary recorded decision.
|
|
41
|
+
let correlation: CorrelationMetadata | undefined;
|
|
42
|
+
let correlationRefused: StructuredRefusal | undefined;
|
|
43
|
+
try {
|
|
44
|
+
correlation = normaliseCorrelation(request.correlation);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
correlationRefused = error instanceof GovernanceRefusal
|
|
47
|
+
? { code: error.code, message: error.message, ...(error.details ? { details: error.details } : {}) }
|
|
48
|
+
: refusal("CORRELATION_INVALID", String(error instanceof Error ? error.message : error));
|
|
49
|
+
}
|
|
50
|
+
const taskDigest = digestTask(request.task ?? "");
|
|
151
51
|
const empty: Delegation = {
|
|
152
52
|
ok: false,
|
|
153
53
|
args: [],
|
|
@@ -155,14 +55,21 @@ export function planDelegation(request: DelegationRequest, ctx: DelegationContex
|
|
|
155
55
|
effective: [],
|
|
156
56
|
childDepth,
|
|
157
57
|
requested: [],
|
|
58
|
+
taskDigest,
|
|
59
|
+
...(correlation ? { correlation } : {}),
|
|
158
60
|
result: { effective: [], denied: [], clipped: [], gatedBlocked: [], universal: [], subsumedBy: [] },
|
|
159
61
|
};
|
|
160
62
|
|
|
161
|
-
if (
|
|
63
|
+
if (correlationRefused) {
|
|
64
|
+
return { ...empty, reason: correlationRefused.message, refusal: correlationRefused };
|
|
65
|
+
}
|
|
66
|
+
if (ctx.maxDepth <= 0) {
|
|
67
|
+
return denied({ ...empty, reason: "delegation is disabled (maxDepth 0)" }, "DEPTH_EXCEEDED");
|
|
68
|
+
}
|
|
162
69
|
if (childDepth > ctx.maxDepth) {
|
|
163
|
-
return { ...empty, reason: `delegation depth limit reached (${ctx.maxDepth})` };
|
|
70
|
+
return denied({ ...empty, reason: `delegation depth limit reached (${ctx.maxDepth})` }, "DEPTH_EXCEEDED");
|
|
164
71
|
}
|
|
165
|
-
if (!request.task?.trim()) return { ...empty, reason: "a delegation needs a task" };
|
|
72
|
+
if (!request.task?.trim()) return denied({ ...empty, reason: "a delegation needs a task" }, "TASK_MISSING");
|
|
166
73
|
|
|
167
74
|
// ADR-0016. A named definition replaces the model's tool list with an operator-authored ceiling.
|
|
168
75
|
let requested: Capability[];
|
|
@@ -179,12 +86,12 @@ export function planDelegation(request: DelegationRequest, ctx: DelegationContex
|
|
|
179
86
|
// here is simply an error.
|
|
180
87
|
if (!definition) {
|
|
181
88
|
const known = [...(ctx.definitions?.keys() ?? [])].sort();
|
|
182
|
-
return {
|
|
89
|
+
return denied({
|
|
183
90
|
...empty,
|
|
184
91
|
reason:
|
|
185
92
|
`unknown agent "${request.agent}"` +
|
|
186
93
|
(known.length > 0 ? ` — known definitions: ${known.join(", ")}` : " — no definitions were found"),
|
|
187
|
-
};
|
|
94
|
+
}, "UNKNOWN_DEFINITION");
|
|
188
95
|
}
|
|
189
96
|
|
|
190
97
|
// ADR-0017: authorisation comes BEFORE anything is said about the file. Which definitions this
|
|
@@ -198,7 +105,7 @@ export function planDelegation(request: DelegationRequest, ctx: DelegationContex
|
|
|
198
105
|
if (!maySpawnDefinition(ctx.ownGrant, definition.name)) {
|
|
199
106
|
const authorising = agentCapability(definition.name);
|
|
200
107
|
const held = ctx.ownGrant.filter((c) => c.startsWith("agent:")).sort();
|
|
201
|
-
return {
|
|
108
|
+
return denied({
|
|
202
109
|
...empty,
|
|
203
110
|
requested: [authorising],
|
|
204
111
|
result: { ...empty.result, denied: [authorising] },
|
|
@@ -208,7 +115,7 @@ export function planDelegation(request: DelegationRequest, ctx: DelegationContex
|
|
|
208
115
|
(held.length > 0
|
|
209
116
|
? `It may spawn: ${held.join(", ")}.`
|
|
210
117
|
: `It may spawn no definitions at all; add ${authorising} to its grant to allow this one.`),
|
|
211
|
-
};
|
|
118
|
+
}, "DEFINITION_NOT_AUTHORIZED");
|
|
212
119
|
}
|
|
213
120
|
|
|
214
121
|
// ADR-0018. Recorded from here on — after authorisation, because the digest is a fact about a file
|
|
@@ -224,21 +131,21 @@ export function planDelegation(request: DelegationRequest, ctx: DelegationContex
|
|
|
224
131
|
|
|
225
132
|
const ceiling = ceilingForDefinition(definition);
|
|
226
133
|
if (ceiling.undeclared) {
|
|
227
|
-
return {
|
|
134
|
+
return denied({
|
|
228
135
|
...empty,
|
|
229
136
|
reason:
|
|
230
137
|
`agent "${definition.name}" declares no \`allowed-tools\`, so it cannot be spawned — add one ` +
|
|
231
138
|
`to ${definition.source}. An undeclared capability set is treated as NONE, never as everything.`,
|
|
232
|
-
};
|
|
139
|
+
}, "UNDECLARED_TOOLS");
|
|
233
140
|
}
|
|
234
141
|
if (ceiling.patterns.length > 0) {
|
|
235
|
-
return {
|
|
142
|
+
return denied({
|
|
236
143
|
...empty,
|
|
237
144
|
reason:
|
|
238
145
|
`agent "${definition.name}" restricts a tool with a pattern (${ceiling.patterns.join(", ")}), ` +
|
|
239
146
|
`which pi's --tools cannot express — it matches whole tool names only. Granting the bare tool ` +
|
|
240
147
|
`would widen the declaration and dropping it would silently narrow, so neither is done.`,
|
|
241
|
-
};
|
|
148
|
+
}, "CEILING_PATTERNS_UNRESOLVED");
|
|
242
149
|
}
|
|
243
150
|
requested = ceiling.capabilities;
|
|
244
151
|
systemPrompt = definition.body;
|
|
@@ -262,59 +169,40 @@ export function planDelegation(request: DelegationRequest, ctx: DelegationContex
|
|
|
262
169
|
return s === null ? null : `${c} → did you mean ${s}?`;
|
|
263
170
|
})
|
|
264
171
|
.filter((h): h is string => h !== null);
|
|
265
|
-
return {
|
|
172
|
+
return denied({
|
|
266
173
|
...empty,
|
|
267
174
|
requested,
|
|
268
175
|
reason:
|
|
269
176
|
`unknown capabilit${unknown.length === 1 ? "y" : "ies"}: ${unknown.join(", ")} — not present in ` +
|
|
270
177
|
`this session's catalog (typo, or an uninstalled package?)` +
|
|
271
178
|
(hints.length > 0 ? ` — ${hints.join("; ")}` : ""),
|
|
272
|
-
};
|
|
179
|
+
}, "UNKNOWN_TOOL");
|
|
273
180
|
}
|
|
274
181
|
}
|
|
275
182
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const result = resolve({
|
|
183
|
+
const { result, approvalBinding, bindingMismatch } = resolveDelegationApproval({
|
|
184
|
+
task: request.task,
|
|
185
|
+
agent: request.agent,
|
|
186
|
+
boundWorkspaceId: request.boundWorkspaceId,
|
|
187
|
+
boundContextId: request.boundContextId,
|
|
282
188
|
requested,
|
|
283
189
|
parentGrant: ctx.ownGrant,
|
|
284
190
|
gated: ctx.gated,
|
|
285
|
-
approved:
|
|
191
|
+
approved: ctx.approved,
|
|
192
|
+
spawned,
|
|
193
|
+
definitionDigest,
|
|
194
|
+
correlation,
|
|
195
|
+
parentId: ctx.spawnId ?? `d${ctx.depth}`,
|
|
286
196
|
});
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* ADR-0024: gating `agent:<name>` asks a human before that definition runs.
|
|
290
|
-
*
|
|
291
|
-
* `gatedBlocked` is a filter over `requested`, and for a definition spawn `requested` is the definition's
|
|
292
|
-
* CEILING — so the id that authorises it was never a candidate, and `PI_GRANTS_GATED=agent:deploy` did
|
|
293
|
-
* nothing at all on the path an operator writing it means. It half-worked when some *other* definition
|
|
294
|
-
* passed the id down in its own `allowed-tools`, which is worse than not working (R-47, R-25's shape).
|
|
295
|
-
*
|
|
296
|
-
* Evaluated here rather than by adding the id to `requested`, and that is the load-bearing part: a
|
|
297
|
-
* capability in `requested` flows to `effective`, which becomes the CHILD's grant — so the child would
|
|
298
|
-
* hold `agent:deploy` and could spawn `deploy` itself without anyone being asked. This is the parent's
|
|
299
|
-
* authority to run the definition *now*, not something the child receives.
|
|
300
|
-
*
|
|
301
|
-
* `agent:*` in the gate covers every definition, so "ask me before any definition runs" is one variable.
|
|
302
|
-
*/
|
|
303
|
-
if (spawned) {
|
|
304
|
-
const authorising = agentCapability(spawned.name);
|
|
305
|
-
const gatedHere = ctx.gated.includes(authorising) || ctx.gated.includes(AGENT_WILDCARD);
|
|
306
|
-
if (gatedHere && !approvedCapabilities.includes(authorising)) {
|
|
307
|
-
result.gatedBlocked = [...result.gatedBlocked, authorising];
|
|
308
|
-
}
|
|
309
|
-
}
|
|
197
|
+
if (approvalBinding) Object.assign(empty, { approvalBinding });
|
|
310
198
|
|
|
311
199
|
if (result.denied.length > 0) {
|
|
312
|
-
return {
|
|
200
|
+
return denied({
|
|
313
201
|
...empty,
|
|
314
202
|
requested,
|
|
315
203
|
result,
|
|
316
204
|
reason: `cannot grant ${result.denied.join(", ")} — this session does not hold it (capability escalation blocked)`,
|
|
317
|
-
};
|
|
205
|
+
}, "CAPABILITY_ESCALATION");
|
|
318
206
|
}
|
|
319
207
|
// ADR-0011: narrowing is checked BEFORE the gate, and the order is load-bearing rather than
|
|
320
208
|
// stylistic. `assertNarrowing` refuses regardless of approval, so with the old order this returned
|
|
@@ -324,10 +212,20 @@ export function planDelegation(request: DelegationRequest, ctx: DelegationContex
|
|
|
324
212
|
try {
|
|
325
213
|
assertNarrowing(result);
|
|
326
214
|
} catch (error) {
|
|
327
|
-
|
|
215
|
+
// ADR-0011's narrowing invariant is the hardest rule this package enforces, and it was the one
|
|
216
|
+
// refusal an external controller could not identify by code (R-109).
|
|
217
|
+
return denied(
|
|
218
|
+
{ ...empty, requested, result, reason: String(error instanceof Error ? error.message : error) },
|
|
219
|
+
"NARROWING_VIOLATED",
|
|
220
|
+
);
|
|
328
221
|
}
|
|
329
222
|
if (result.gatedBlocked.length > 0) {
|
|
330
|
-
|
|
223
|
+
const code = bindingMismatch ? "APPROVAL_SCOPE_MISMATCH" : "GATED_UNAPPROVED";
|
|
224
|
+
const suffix = bindingMismatch ? " (an approval exists, but its task/workspace/context scope does not match)" : "";
|
|
225
|
+
return denied(
|
|
226
|
+
{ ...empty, requested, result, reason: `${result.gatedBlocked.join(", ")} requires explicit approval${suffix}` },
|
|
227
|
+
code,
|
|
228
|
+
);
|
|
331
229
|
}
|
|
332
230
|
|
|
333
231
|
const canSubDelegate = result.effective.includes(DELEGATE_CAPABILITY);
|
|
@@ -349,14 +247,14 @@ export function planDelegation(request: DelegationRequest, ctx: DelegationContex
|
|
|
349
247
|
// not contain. `unknownCapabilities` above catches names absent from the catalog entirely; this
|
|
350
248
|
// catches one that is known but whose path we could not resolve, which is a different fault.
|
|
351
249
|
if (plan.unresolvedSkills.length > 0) {
|
|
352
|
-
return {
|
|
250
|
+
return denied({
|
|
353
251
|
...empty,
|
|
354
252
|
requested,
|
|
355
253
|
result,
|
|
356
254
|
reason:
|
|
357
255
|
`cannot locate ${plan.unresolvedSkills.join(", ")} on disk — granted but unresolvable, so the ` +
|
|
358
256
|
`child would silently lack it`,
|
|
359
|
-
};
|
|
257
|
+
}, "DEFINITION_UNREADABLE");
|
|
360
258
|
}
|
|
361
259
|
|
|
362
260
|
// A child may only delegate further if it was granted the capability AND has the extension to do it.
|
|
@@ -394,6 +292,9 @@ export function planDelegation(request: DelegationRequest, ctx: DelegationContex
|
|
|
394
292
|
childDepth,
|
|
395
293
|
requested,
|
|
396
294
|
childId: ctx.childSpawnId,
|
|
295
|
+
taskDigest,
|
|
296
|
+
...(correlation ? { correlation } : {}),
|
|
297
|
+
...(approvalBinding ? { approvalBinding } : {}),
|
|
397
298
|
...(definitionDigest ? { definitionDigest } : {}),
|
|
398
299
|
};
|
|
399
300
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { DELEGATE_SUBJECT, type InheritableApproval } from "./approval.ts";
|
|
2
|
+
import { agentCapability } from "./capabilities.ts";
|
|
3
|
+
import {
|
|
4
|
+
approvalBindingsEqual,
|
|
5
|
+
buildApprovalBinding,
|
|
6
|
+
type ApprovalBinding,
|
|
7
|
+
type CorrelationMetadata,
|
|
8
|
+
} from "./correlation.ts";
|
|
9
|
+
import type { DefinitionDigest, SkillDefinition } from "./definitions.ts";
|
|
10
|
+
import { AGENT_WILDCARD, resolve, type Capability, type ResolveResult } from "./resolve.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the approval half of one delegation after its requested capability set is known.
|
|
14
|
+
*
|
|
15
|
+
* Kept as one function because the ordering is security-relevant: compute the unapproved and potential
|
|
16
|
+
* effective sets first, derive the exact binding from those trusted values, then decide which approvals
|
|
17
|
+
* match. An approval must never define the scope against which it is checked.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveDelegationApproval(input: {
|
|
20
|
+
task: string;
|
|
21
|
+
agent?: string;
|
|
22
|
+
requested: Capability[];
|
|
23
|
+
parentGrant: Capability[];
|
|
24
|
+
gated: Capability[];
|
|
25
|
+
approved?: InheritableApproval[];
|
|
26
|
+
spawned?: SkillDefinition;
|
|
27
|
+
definitionDigest?: DefinitionDigest;
|
|
28
|
+
/**
|
|
29
|
+
* Present iff this call is task-bound. Its VALUES never enter the binding — only its presence selects
|
|
30
|
+
* the exact-bound regime over the legacy subject-scoped one.
|
|
31
|
+
*/
|
|
32
|
+
correlation?: CorrelationMetadata;
|
|
33
|
+
/** Trusted: an id that was resolved against the operator registry and leased. Never a caller claim. */
|
|
34
|
+
boundWorkspaceId?: string;
|
|
35
|
+
/** Caller-declared label. Narrows the binding only; asserts nothing about enforcement. */
|
|
36
|
+
boundContextId?: string;
|
|
37
|
+
parentId: string;
|
|
38
|
+
}): { result: ResolveResult; approvalBinding?: ApprovalBinding; bindingMismatch: boolean } {
|
|
39
|
+
const subject = input.agent ?? DELEGATE_SUBJECT;
|
|
40
|
+
const unapproved = resolve({
|
|
41
|
+
requested: input.requested,
|
|
42
|
+
parentGrant: input.parentGrant,
|
|
43
|
+
gated: input.gated,
|
|
44
|
+
approved: [],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ADR-0024: a definition's authorising id is gated as the PARENT's authority to run it now. It never
|
|
48
|
+
// joins requested/effective, because that would hand the child authority to recursively spawn itself.
|
|
49
|
+
let authorisingCapability: Capability | undefined;
|
|
50
|
+
if (input.spawned) {
|
|
51
|
+
const authorising = agentCapability(input.spawned.name);
|
|
52
|
+
if (input.gated.includes(authorising) || input.gated.includes(AGENT_WILDCARD)) {
|
|
53
|
+
authorisingCapability = authorising;
|
|
54
|
+
unapproved.gatedBlocked = [...unapproved.gatedBlocked, authorising];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const potential = resolve({
|
|
59
|
+
requested: input.requested,
|
|
60
|
+
parentGrant: input.parentGrant,
|
|
61
|
+
gated: input.gated,
|
|
62
|
+
// If the definition also declares its own authorising `agent:<name>`, the same approval unblocks
|
|
63
|
+
// that requested child capability too; excluding it here would bind a smaller set than we provision.
|
|
64
|
+
approved: unapproved.gatedBlocked,
|
|
65
|
+
});
|
|
66
|
+
const approvalBinding = input.correlation
|
|
67
|
+
? buildApprovalBinding({
|
|
68
|
+
task: input.task,
|
|
69
|
+
requested: input.requested,
|
|
70
|
+
effective: potential.effective,
|
|
71
|
+
definitionSha256: input.definitionDigest?.sha256,
|
|
72
|
+
parentId: input.parentId,
|
|
73
|
+
workspaceId: input.boundWorkspaceId,
|
|
74
|
+
contextId: input.boundContextId,
|
|
75
|
+
})
|
|
76
|
+
: undefined;
|
|
77
|
+
|
|
78
|
+
const forSubject = (input.approved ?? []).filter((approval) => approval.subject === subject);
|
|
79
|
+
const approvedCapabilities = forSubject
|
|
80
|
+
.filter((approval) =>
|
|
81
|
+
approvalBinding
|
|
82
|
+
? approvalBindingsEqual(approval.binding, approvalBinding)
|
|
83
|
+
: approval.binding === undefined,
|
|
84
|
+
)
|
|
85
|
+
.map((approval) => approval.capability);
|
|
86
|
+
const bindingMismatch = approvalBinding !== undefined && forSubject.some(
|
|
87
|
+
(approval) => approval.binding !== undefined && !approvalBindingsEqual(approval.binding, approvalBinding),
|
|
88
|
+
);
|
|
89
|
+
const result = resolve({
|
|
90
|
+
requested: input.requested,
|
|
91
|
+
parentGrant: input.parentGrant,
|
|
92
|
+
gated: input.gated,
|
|
93
|
+
approved: approvedCapabilities,
|
|
94
|
+
});
|
|
95
|
+
if (authorisingCapability && !approvedCapabilities.includes(authorisingCapability)) {
|
|
96
|
+
result.gatedBlocked = [...result.gatedBlocked, authorisingCapability];
|
|
97
|
+
}
|
|
98
|
+
return { result, ...(approvalBinding ? { approvalBinding } : {}), bindingMismatch };
|
|
99
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { GovernanceRefusal, refusal } from "./refusals.ts";
|
|
7
|
+
import type { ValidatedWorkspace } from "./workspace.ts";
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
|
|
11
|
+
export interface GitCandidateIdentity {
|
|
12
|
+
headSha: string;
|
|
13
|
+
treeSha: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Compute HEAD plus NON-IGNORED tracked/untracked candidate content without changing the real index.
|
|
17
|
+
*
|
|
18
|
+
* NOT the exact working tree: `git add -A` honours `.gitignore`, `core.excludesFile` and
|
|
19
|
+
* `$GIT_DIR/info/exclude`, and records only a gitlink for a submodule. And it is not read-only — it writes
|
|
20
|
+
* blob objects into the real `.git/objects`. `docs/SPEC.md` retracted the word "exact"; this comment said
|
|
21
|
+
* it for one more round. */
|
|
22
|
+
export async function computeGitCandidateIdentity(workspace: ValidatedWorkspace): Promise<GitCandidateIdentity> {
|
|
23
|
+
const dir = await mkdtemp(join(tmpdir(), "pi-daddy-index-"));
|
|
24
|
+
const index = join(dir, "index");
|
|
25
|
+
const env: NodeJS.ProcessEnv = {
|
|
26
|
+
PATH: process.env.PATH,
|
|
27
|
+
HOME: process.env.HOME,
|
|
28
|
+
LANG: process.env.LANG,
|
|
29
|
+
LC_ALL: process.env.LC_ALL,
|
|
30
|
+
TMPDIR: process.env.TMPDIR,
|
|
31
|
+
GIT_INDEX_FILE: index,
|
|
32
|
+
};
|
|
33
|
+
const git = async (args: string[]) => (await execFileAsync("git", ["-C", workspace.root, ...args], {
|
|
34
|
+
env, encoding: "utf8", maxBuffer: 16 * 1024 * 1024,
|
|
35
|
+
})).stdout.trim();
|
|
36
|
+
try {
|
|
37
|
+
const headSha = await git(["rev-parse", "HEAD"]);
|
|
38
|
+
await git(["read-tree", "HEAD"]);
|
|
39
|
+
await git(["add", "-A"]);
|
|
40
|
+
const treeSha = await git(["write-tree"]);
|
|
41
|
+
if (!/^[a-f0-9]{40,64}$/i.test(headSha) || !/^[a-f0-9]{40,64}$/i.test(treeSha)) throw new Error("Git returned an invalid object id");
|
|
42
|
+
return { headSha, treeSha };
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new GovernanceRefusal(refusal(
|
|
45
|
+
"CHECK_IDENTITY_UNAVAILABLE",
|
|
46
|
+
`could not compute exact Git head/candidate-tree identity for workspace ${workspace.workspaceId} (${String(error)})`,
|
|
47
|
+
{ workspace_id: workspace.workspaceId },
|
|
48
|
+
));
|
|
49
|
+
} finally {
|
|
50
|
+
await rm(dir, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,11 +9,20 @@ export {
|
|
|
9
9
|
} from "./resolve.ts";
|
|
10
10
|
|
|
11
11
|
export {
|
|
12
|
+
appendLedgerEvent,
|
|
12
13
|
appendRecord,
|
|
14
|
+
buildChildLifecycleEvent,
|
|
13
15
|
buildRecord,
|
|
16
|
+
buildWorkspaceLeaseEvent,
|
|
14
17
|
isEscalationAttempt,
|
|
18
|
+
LEDGER_VERSION,
|
|
19
|
+
type CheckReceiptLedgerEvent,
|
|
20
|
+
type ChildLifecycleEvent,
|
|
15
21
|
type GrantRecord,
|
|
16
22
|
type LedgerOptions,
|
|
23
|
+
type RuntimeLedgerEvent,
|
|
24
|
+
type WorkspaceLeaseEvent,
|
|
25
|
+
type WorkspaceLeaseOutcome,
|
|
17
26
|
} from "./ledger.ts";
|
|
18
27
|
|
|
19
28
|
export { planSpawn, type SpawnPlan, type SpawnPlanInput } from "./spawn.ts";
|
|
@@ -46,6 +55,47 @@ export {
|
|
|
46
55
|
type SubjectLookup,
|
|
47
56
|
} from "./approval-store.ts";
|
|
48
57
|
|
|
58
|
+
export {
|
|
59
|
+
approvalBindingDigest,
|
|
60
|
+
approvalBindingsEqual,
|
|
61
|
+
buildApprovalBinding,
|
|
62
|
+
digestCapabilities,
|
|
63
|
+
digestTask,
|
|
64
|
+
isApprovalBinding,
|
|
65
|
+
normaliseCorrelation,
|
|
66
|
+
type ApprovalBinding,
|
|
67
|
+
type CorrelationMetadata,
|
|
68
|
+
type JsonValue,
|
|
69
|
+
} from "./correlation.ts";
|
|
70
|
+
|
|
71
|
+
export {
|
|
72
|
+
GovernanceRefusal,
|
|
73
|
+
REFUSAL_CODES,
|
|
74
|
+
refusal,
|
|
75
|
+
type RefusalCode,
|
|
76
|
+
type StructuredRefusal,
|
|
77
|
+
} from "./refusals.ts";
|
|
78
|
+
|
|
79
|
+
export {
|
|
80
|
+
acquireWorkspaceLease,
|
|
81
|
+
defaultWorkspaceLeaseDir,
|
|
82
|
+
loadWorkspaceRegistry,
|
|
83
|
+
resolveWorkspace,
|
|
84
|
+
validateRegisteredWorkspace,
|
|
85
|
+
type ValidatedWorkspace,
|
|
86
|
+
type WorkspaceAccess,
|
|
87
|
+
type WorkspaceLease,
|
|
88
|
+
type WorkspaceRegistryFile,
|
|
89
|
+
} from "./workspace.ts";
|
|
90
|
+
|
|
91
|
+
export {
|
|
92
|
+
buildCheckEnvironment,
|
|
93
|
+
runNamedCheck,
|
|
94
|
+
type CheckDefinition,
|
|
95
|
+
type CheckReceipt,
|
|
96
|
+
type CheckRegistry,
|
|
97
|
+
} from "./check-runner.ts";
|
|
98
|
+
|
|
49
99
|
export {
|
|
50
100
|
createApprovalGate,
|
|
51
101
|
createApprovalGateProvider,
|