pi-daddy 0.18.1 → 0.20.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 +201 -0
- package/README.md +136 -23
- package/contracts/ledger/v2/README.md +59 -0
- package/contracts/ledger/v2/fixtures/capability-decision.json +95 -0
- package/contracts/ledger/v2/fixtures/check-receipt.json +40 -0
- package/contracts/ledger/v2/fixtures/child-lifecycle.json +42 -0
- package/contracts/ledger/v2/fixtures/workspace-lease.json +41 -0
- package/contracts/ledger/v2/ledger-event.schema.json +633 -0
- package/contracts/ledger/v3/README.md +36 -0
- package/contracts/ledger/v3/fixtures/capability-decision.json +98 -0
- package/contracts/ledger/v3/fixtures/check-receipt.json +42 -0
- package/contracts/ledger/v3/fixtures/child-lifecycle.json +45 -0
- package/contracts/ledger/v3/fixtures/workflow-fact.json +41 -0
- package/contracts/ledger/v3/fixtures/workspace-lease.json +43 -0
- package/contracts/ledger/v3/ledger-event.schema.json +930 -0
- package/dist/approval-prompt.d.ts +2 -1
- package/dist/approval-prompt.d.ts.map +1 -1
- package/dist/approval-prompt.js +9 -0
- package/dist/approval-prompt.js.map +1 -1
- package/dist/approval.d.ts +4 -2
- package/dist/approval.d.ts.map +1 -1
- package/dist/approval.js +4 -0
- package/dist/approval.js.map +1 -1
- package/dist/capabilities.d.ts +90 -0
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +114 -3
- package/dist/capabilities.js.map +1 -1
- package/dist/catalog.d.ts +15 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +54 -3
- package/dist/catalog.js.map +1 -1
- package/dist/check-runner.d.ts.map +1 -1
- package/dist/check-runner.js +19 -7
- package/dist/check-runner.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +21 -1
- package/dist/cli.js.map +1 -1
- package/dist/correlation.d.ts.map +1 -1
- package/dist/correlation.js +10 -0
- package/dist/correlation.js.map +1 -1
- package/dist/dashboard-cli.d.ts +18 -0
- package/dist/dashboard-cli.d.ts.map +1 -0
- package/dist/dashboard-cli.js +154 -0
- package/dist/dashboard-cli.js.map +1 -0
- package/dist/dashboard-handshake.d.ts +37 -0
- package/dist/dashboard-handshake.d.ts.map +1 -0
- package/dist/dashboard-handshake.js +127 -0
- package/dist/dashboard-handshake.js.map +1 -0
- package/dist/dashboard-herdr.d.ts +54 -0
- package/dist/dashboard-herdr.d.ts.map +1 -0
- package/dist/dashboard-herdr.js +286 -0
- package/dist/dashboard-herdr.js.map +1 -0
- package/dist/dashboard-projection.d.ts +73 -0
- package/dist/dashboard-projection.d.ts.map +1 -0
- package/dist/dashboard-projection.js +294 -0
- package/dist/dashboard-projection.js.map +1 -0
- package/dist/dashboard-render.d.ts +10 -0
- package/dist/dashboard-render.d.ts.map +1 -0
- package/dist/dashboard-render.js +208 -0
- package/dist/dashboard-render.js.map +1 -0
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +7 -1
- package/dist/definitions.js.map +1 -1
- package/dist/delegate-types.d.ts +6 -2
- package/dist/delegate-types.d.ts.map +1 -1
- package/dist/delegate-types.js.map +1 -1
- package/dist/delegate.d.ts.map +1 -1
- package/dist/delegate.js +34 -4
- package/dist/delegate.js.map +1 -1
- package/dist/delegation-approval.d.ts.map +1 -1
- package/dist/delegation-approval.js +37 -12
- package/dist/delegation-approval.js.map +1 -1
- package/dist/execution-id.d.ts +6 -0
- package/dist/execution-id.d.ts.map +1 -0
- package/dist/execution-id.js +13 -0
- package/dist/execution-id.js.map +1 -0
- package/dist/executor.d.ts +2 -1
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +1 -0
- package/dist/executor.js.map +1 -1
- package/dist/grant-env.d.ts +2 -0
- package/dist/grant-env.d.ts.map +1 -1
- package/dist/grant-env.js +26 -3
- package/dist/grant-env.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +13 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +35 -2
- package/dist/init.js.map +1 -1
- package/dist/lease-helper.d.ts +58 -0
- package/dist/lease-helper.d.ts.map +1 -0
- package/dist/lease-helper.js +94 -0
- package/dist/lease-helper.js.map +1 -0
- package/dist/lease-record.d.ts +15 -3
- package/dist/lease-record.d.ts.map +1 -1
- package/dist/lease-record.js.map +1 -1
- package/dist/ledger-events.d.ts +59 -15
- package/dist/ledger-events.d.ts.map +1 -1
- package/dist/ledger-events.js +67 -5
- package/dist/ledger-events.js.map +1 -1
- package/dist/ledger-identifiers.d.ts +7 -0
- package/dist/ledger-identifiers.d.ts.map +1 -0
- package/dist/ledger-identifiers.js +20 -0
- package/dist/ledger-identifiers.js.map +1 -0
- package/dist/ledger-report.d.ts +5 -2
- package/dist/ledger-report.d.ts.map +1 -1
- package/dist/ledger-report.js +38 -14
- package/dist/ledger-report.js.map +1 -1
- package/dist/ledger-v3-validation.d.ts +11 -0
- package/dist/ledger-v3-validation.d.ts.map +1 -0
- package/dist/ledger-v3-validation.js +265 -0
- package/dist/ledger-v3-validation.js.map +1 -0
- package/dist/ledger.d.ts +27 -9
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +38 -6
- package/dist/ledger.js.map +1 -1
- package/dist/propagation.d.ts +19 -1
- package/dist/propagation.d.ts.map +1 -1
- package/dist/propagation.js +23 -1
- package/dist/propagation.js.map +1 -1
- package/dist/refusals.d.ts +1 -1
- package/dist/refusals.d.ts.map +1 -1
- package/dist/refusals.js +1 -0
- package/dist/refusals.js.map +1 -1
- package/dist/resolve.d.ts +10 -0
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +27 -3
- package/dist/resolve.js.map +1 -1
- package/dist/routing-authority.d.ts +71 -0
- package/dist/routing-authority.d.ts.map +1 -0
- package/dist/routing-authority.js +100 -0
- package/dist/routing-authority.js.map +1 -0
- package/dist/run-child.d.ts +3 -1
- package/dist/run-child.d.ts.map +1 -1
- package/dist/run-child.js +30 -2
- package/dist/run-child.js.map +1 -1
- package/dist/run-herdr.d.ts +2 -0
- package/dist/run-herdr.d.ts.map +1 -1
- package/dist/run-herdr.js +8 -0
- package/dist/run-herdr.js.map +1 -1
- package/dist/skill-packages.d.ts +11 -5
- package/dist/skill-packages.d.ts.map +1 -1
- package/dist/skill-packages.js +20 -11
- package/dist/skill-packages.js.map +1 -1
- package/dist/workflow-fact-id.d.ts +4 -0
- package/dist/workflow-fact-id.d.ts.map +1 -0
- package/dist/workflow-fact-id.js +14 -0
- package/dist/workflow-fact-id.js.map +1 -0
- package/dist/workflow-facts.d.ts +34 -0
- package/dist/workflow-facts.d.ts.map +1 -0
- package/dist/workflow-facts.js +43 -0
- package/dist/workflow-facts.js.map +1 -0
- package/dist/workspace-lease.d.ts +15 -3
- package/dist/workspace-lease.d.ts.map +1 -1
- package/dist/workspace-lease.js +81 -24
- package/dist/workspace-lease.js.map +1 -1
- package/dist/workspace.d.ts +25 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +142 -5
- package/dist/workspace.js.map +1 -1
- package/extensions/chain-ledger.ts +4 -0
- package/extensions/chain-plan.ts +98 -0
- package/extensions/delegate-chain.ts +63 -122
- package/extensions/delegation-ledger.ts +60 -0
- package/extensions/delegation.ts +20 -14
- package/extensions/execute-child.ts +85 -21
- package/extensions/execution-occurrence.ts +20 -0
- package/extensions/grants-command.ts +33 -4
- package/extensions/grants.ts +50 -1
- package/extensions/init-command.ts +33 -2
- package/extensions/run-delegation.ts +36 -56
- package/extensions/session-report.ts +13 -20
- package/extensions/session.ts +16 -3
- package/extensions/workspace-runtime.ts +44 -4
- package/herdr-plugin/herdr-plugin.toml +18 -0
- package/package.json +22 -5
- package/src/approval-prompt.ts +2 -1
- package/src/approval.ts +4 -2
- package/src/capabilities.ts +120 -4
- package/src/catalog.ts +62 -4
- package/src/check-runner.ts +24 -8
- package/src/cli.ts +24 -1
- package/src/correlation.ts +11 -0
- package/src/dashboard-cli.ts +171 -0
- package/src/dashboard-handshake.ts +185 -0
- package/src/dashboard-herdr.ts +355 -0
- package/src/dashboard-projection.ts +390 -0
- package/src/dashboard-render.ts +254 -0
- package/src/definitions.ts +7 -1
- package/src/delegate-types.ts +6 -2
- package/src/delegate.ts +43 -4
- package/src/delegation-approval.ts +37 -12
- package/src/execution-id.ts +18 -0
- package/src/executor.ts +2 -1
- package/src/grant-env.ts +39 -7
- package/src/index.ts +26 -0
- package/src/init.ts +40 -2
- package/src/lease-helper.ts +97 -0
- package/src/lease-record.ts +15 -3
- package/src/ledger-events.ts +116 -28
- package/src/ledger-identifiers.ts +21 -0
- package/src/ledger-report.ts +40 -19
- package/src/ledger-v3-validation.ts +266 -0
- package/src/ledger.ts +75 -12
- package/src/propagation.ts +24 -1
- package/src/refusals.ts +1 -0
- package/src/resolve.ts +29 -3
- package/src/routing-authority.ts +121 -0
- package/src/run-child.ts +35 -3
- package/src/run-herdr.ts +9 -0
- package/src/skill-packages.ts +20 -13
- package/src/workflow-fact-id.ts +16 -0
- package/src/workflow-facts.ts +65 -0
- package/src/workspace-lease.ts +84 -24
- package/src/workspace.ts +179 -6
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { normaliseCorrelation, type CorrelationMetadata } from "./correlation.ts";
|
|
2
|
+
import { isExecutionId } from "./execution-id.ts";
|
|
3
|
+
import { REFUSAL_CODES } from "./refusals.ts";
|
|
4
|
+
import { isWorkflowFactId, isWorkflowIdentifier, workflowFactStateMatches } from "./workflow-fact-id.ts";
|
|
5
|
+
import { isLedgerCapabilityIdentifier, isLedgerDisplayIdentifier } from "./ledger-identifiers.ts";
|
|
6
|
+
|
|
7
|
+
export type LedgerV3Object = Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
const SHA256_RE = /^[a-f0-9]{64}$/i;
|
|
10
|
+
const EXECUTORS = new Set(["process", "herdr"]);
|
|
11
|
+
const APPROVAL_SOURCES = new Set(["prompt", "session", "persisted", "inherited"]);
|
|
12
|
+
const APPROVAL_SCOPES = new Set(["once", "session", "always"]);
|
|
13
|
+
const GATE_OUTCOMES = new Set(["declined", "dismissed", "no-ui", "error"]);
|
|
14
|
+
const LEASE_OUTCOMES = new Set([
|
|
15
|
+
"acquired", "uncontended", "refused", "released", "released-unrecorded", "lost", "retained", "timeout", "recovered",
|
|
16
|
+
]);
|
|
17
|
+
const PROCESS_SIGNALS = new Set([
|
|
18
|
+
"SIGABRT", "SIGALRM", "SIGBUS", "SIGCHLD", "SIGCONT", "SIGFPE", "SIGHUP", "SIGILL", "SIGINT", "SIGIO",
|
|
19
|
+
"SIGIOT", "SIGKILL", "SIGPIPE", "SIGPOLL", "SIGPROF", "SIGPWR", "SIGQUIT", "SIGSEGV", "SIGSTKFLT",
|
|
20
|
+
"SIGSTOP", "SIGSYS", "SIGTERM", "SIGTRAP", "SIGTSTP", "SIGTTIN", "SIGTTOU", "SIGUNUSED", "SIGURG",
|
|
21
|
+
"SIGUSR1", "SIGUSR2", "SIGVTALRM", "SIGWINCH", "SIGXCPU", "SIGXFSZ", "SIGBREAK", "SIGLOST", "SIGINFO",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const FIELDS = {
|
|
25
|
+
capability_decision: new Set([
|
|
26
|
+
"ledgerVersion", "event", "ts", "executionId", "parentExecutionId", "parentId", "childId", "depth",
|
|
27
|
+
"agentType", "requested", "parentGrant", "effective", "denied", "clipped", "gatedBlocked", "blocked",
|
|
28
|
+
"reason", "approved", "approvalSource", "approvalSources", "approvalScopes", "approvalExpiresAt",
|
|
29
|
+
"approvalUses", "approvalScope", "humanDenied", "gateOutcome", "definitionDigest", "executor", "taskFrom",
|
|
30
|
+
"taskFromExecutionId", "taskDigest", "correlation", "refusal",
|
|
31
|
+
]),
|
|
32
|
+
workspace_lease: new Set([
|
|
33
|
+
"ledgerVersion", "event", "ts", "executionId", "parentExecutionId", "childId", "workspaceId", "root",
|
|
34
|
+
"access", "outcome", "recovered", "releaseReason", "refusal", "correlation",
|
|
35
|
+
]),
|
|
36
|
+
child_lifecycle: new Set([
|
|
37
|
+
"ledgerVersion", "event", "ts", "executionId", "parentExecutionId", "childId", "state", "executor",
|
|
38
|
+
"exitCode", "signal", "timedOut", "aborted", "truncated", "reason", "deadlineAt", "herdrPaneId",
|
|
39
|
+
"herdrAgentName", "correlation",
|
|
40
|
+
]),
|
|
41
|
+
check_receipt: new Set([
|
|
42
|
+
"ledgerVersion", "event", "ts", "executionId", "parentExecutionId", "childId", "receiptId", "workspaceId",
|
|
43
|
+
"checkId", "treeSha", "correlation",
|
|
44
|
+
]),
|
|
45
|
+
workflow_fact: new Set([
|
|
46
|
+
"ledgerVersion", "event", "ts", "factId", "source", "provenance", "kind", "subject", "state", "correlation",
|
|
47
|
+
]),
|
|
48
|
+
} as const;
|
|
49
|
+
type EventKind = keyof typeof FIELDS;
|
|
50
|
+
|
|
51
|
+
export function isLedgerObject(value: unknown): value is LedgerV3Object {
|
|
52
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
53
|
+
const prototype = Object.getPrototypeOf(value);
|
|
54
|
+
return prototype === Object.prototype || prototype === null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function isNonEmptyString(value: unknown): value is string {
|
|
58
|
+
return typeof value === "string" && value.length > 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Ledger timestamp profile: JSON Schema `date-time`, narrowed to seconds 00-59 for JS date arithmetic. */
|
|
62
|
+
export function isTimestamp(value: unknown): value is string {
|
|
63
|
+
if (typeof value !== "string") return false;
|
|
64
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:[Zz]|([+-])(\d{2}):(\d{2}))$/.exec(value);
|
|
65
|
+
if (!match) return false;
|
|
66
|
+
const [, yearText, monthText, dayText, hourText, minuteText, secondText, , offsetHourText, offsetMinuteText] = match;
|
|
67
|
+
const year = Number(yearText);
|
|
68
|
+
const month = Number(monthText);
|
|
69
|
+
const day = Number(dayText);
|
|
70
|
+
const hour = Number(hourText);
|
|
71
|
+
const minute = Number(minuteText);
|
|
72
|
+
const second = Number(secondText);
|
|
73
|
+
if (month < 1 || month > 12 || hour > 23 || minute > 59 || second > 59) return false;
|
|
74
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
75
|
+
const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
76
|
+
if (day < 1 || day > days[month - 1]) return false;
|
|
77
|
+
return offsetHourText === undefined || (Number(offsetHourText) <= 23 && Number(offsetMinuteText) <= 59);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function isStringArray(value: unknown): value is string[] {
|
|
81
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function optional(event: LedgerV3Object, field: string, valid: (value: unknown) => boolean): boolean {
|
|
85
|
+
return !Object.hasOwn(event, field) || valid(event[field]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function stringMap(value: unknown, valid: (entry: unknown) => boolean): boolean {
|
|
89
|
+
return isLedgerObject(value) && Object.values(value).every(valid);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function capabilityMap(value: unknown, valid: (entry: unknown) => boolean): boolean {
|
|
93
|
+
return isLedgerObject(value) && Object.keys(value).every(isLedgerCapabilityIdentifier) && Object.values(value).every(valid);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function validCorrelation(value: unknown): boolean {
|
|
97
|
+
if (!isLedgerObject(value)) return false;
|
|
98
|
+
try {
|
|
99
|
+
const normalised = normaliseCorrelation(value as CorrelationMetadata);
|
|
100
|
+
// `normaliseCorrelation` drops nulls. The closed schema does not, so equality is part of validation.
|
|
101
|
+
return JSON.stringify(normalised) === JSON.stringify(value);
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function validRefusal(value: unknown): boolean {
|
|
108
|
+
if (!isLedgerObject(value) || Object.keys(value).some((key) => !["code", "message", "details"].includes(key))) return false;
|
|
109
|
+
if (!REFUSAL_CODES.includes(value.code as never) || typeof value.message !== "string") return false;
|
|
110
|
+
return optional(value, "details", (details) => isLedgerObject(details) && Object.values(details).every(
|
|
111
|
+
(entry) => entry === null || ["string", "number", "boolean"].includes(typeof entry),
|
|
112
|
+
));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function validDefinitionDigest(value: unknown): boolean {
|
|
116
|
+
return isLedgerObject(value) &&
|
|
117
|
+
Object.keys(value).every((key) => ["name", "source", "sha256"].includes(key)) &&
|
|
118
|
+
typeof value.name === "string" && typeof value.source === "string" && SHA256_RE.test(String(value.sha256));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function validApprovalUse(value: unknown): boolean {
|
|
122
|
+
return isLedgerObject(value) && Object.keys(value).every((key) => ["max", "remaining"].includes(key)) &&
|
|
123
|
+
Number.isInteger(value.max) && (value.max as number) >= 0 &&
|
|
124
|
+
Number.isInteger(value.remaining) && (value.remaining as number) >= 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function validateBase(event: LedgerV3Object): string | null {
|
|
128
|
+
if (!isTimestamp(event.ts)) return "ts must be an RFC 3339 timestamp";
|
|
129
|
+
if (!isExecutionId(event.executionId)) return "executionId is missing or invalid";
|
|
130
|
+
if (event.parentExecutionId !== null && !isExecutionId(event.parentExecutionId)) {
|
|
131
|
+
return "parentExecutionId must be an execution id or null";
|
|
132
|
+
}
|
|
133
|
+
if (event.parentExecutionId === event.executionId) return "an execution cannot be its own parent";
|
|
134
|
+
if (!isLedgerDisplayIdentifier(event.childId)) return "childId is missing or invalid";
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function validateCapabilityDecision(event: LedgerV3Object): string | null {
|
|
139
|
+
if (!isLedgerDisplayIdentifier(event.parentId) || !Number.isInteger(event.depth) || (event.depth as number) < 0) {
|
|
140
|
+
return "capability decision identity is invalid";
|
|
141
|
+
}
|
|
142
|
+
for (const field of ["requested", "parentGrant", "effective", "denied", "clipped", "gatedBlocked"]) {
|
|
143
|
+
if (!isStringArray(event[field]) || !event[field].every(isLedgerCapabilityIdentifier)) {
|
|
144
|
+
return `capability decision ${field} must contain capability identifiers`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (typeof event.blocked !== "boolean" || !EXECUTORS.has(String(event.executor)) || !SHA256_RE.test(String(event.taskDigest))) {
|
|
148
|
+
return "capability decision required fields are invalid";
|
|
149
|
+
}
|
|
150
|
+
if (!optional(event, "agentType", isLedgerDisplayIdentifier) ||
|
|
151
|
+
!optional(event, "reason", (value) => typeof value === "string") ||
|
|
152
|
+
!optional(event, "approved", (value) => isStringArray(value) && value.every(isLedgerCapabilityIdentifier)) ||
|
|
153
|
+
!optional(event, "approvalSource", (value) => APPROVAL_SOURCES.has(String(value))) ||
|
|
154
|
+
!optional(event, "approvalSources", (value) => capabilityMap(value, (entry) => APPROVAL_SOURCES.has(String(entry)))) ||
|
|
155
|
+
!optional(event, "approvalScopes", (value) => capabilityMap(value, (entry) => APPROVAL_SCOPES.has(String(entry)))) ||
|
|
156
|
+
!optional(event, "approvalExpiresAt", (value) => capabilityMap(value, isTimestamp)) ||
|
|
157
|
+
!optional(event, "approvalUses", (value) => capabilityMap(value, validApprovalUse)) ||
|
|
158
|
+
!optional(event, "approvalScope", (value) => APPROVAL_SCOPES.has(String(value))) ||
|
|
159
|
+
!optional(event, "humanDenied", (value) => value === true) ||
|
|
160
|
+
!optional(event, "gateOutcome", (value) => GATE_OUTCOMES.has(String(value))) ||
|
|
161
|
+
!optional(event, "definitionDigest", validDefinitionDigest) ||
|
|
162
|
+
!optional(event, "taskFrom", isLedgerDisplayIdentifier) ||
|
|
163
|
+
!optional(event, "taskFromExecutionId", isExecutionId) ||
|
|
164
|
+
!optional(event, "correlation", validCorrelation) ||
|
|
165
|
+
!optional(event, "refusal", validRefusal)) {
|
|
166
|
+
return "capability decision optional fields are invalid";
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function validateWorkspaceLease(event: LedgerV3Object): string | null {
|
|
172
|
+
if (!isLedgerDisplayIdentifier(event.workspaceId) || !isNonEmptyString(event.root) ||
|
|
173
|
+
!["read", "write"].includes(String(event.access)) || !LEASE_OUTCOMES.has(String(event.outcome))) {
|
|
174
|
+
return "workspace lease required fields are invalid";
|
|
175
|
+
}
|
|
176
|
+
if (!optional(event, "recovered", (value) => value === true || value === false || value === "unknown") ||
|
|
177
|
+
!optional(event, "releaseReason", (value) => typeof value === "string") ||
|
|
178
|
+
!optional(event, "refusal", validRefusal) || !optional(event, "correlation", validCorrelation)) {
|
|
179
|
+
return "workspace lease optional fields are invalid";
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function validateChildLifecycle(event: LedgerV3Object): string | null {
|
|
185
|
+
if (!["starting", "running", "completed", "failed"].includes(String(event.state)) || !EXECUTORS.has(String(event.executor))) {
|
|
186
|
+
return "child lifecycle required fields are invalid";
|
|
187
|
+
}
|
|
188
|
+
if (["starting", "running"].includes(String(event.state)) && !isTimestamp(event.deadlineAt)) {
|
|
189
|
+
return "deadlineAt is required and must be an RFC 3339 timestamp for a non-terminal lifecycle";
|
|
190
|
+
}
|
|
191
|
+
if (!optional(event, "deadlineAt", isTimestamp) ||
|
|
192
|
+
!optional(event, "exitCode", (value) => value === null || Number.isInteger(value)) ||
|
|
193
|
+
!optional(event, "signal", (value) => value === null || PROCESS_SIGNALS.has(String(value))) ||
|
|
194
|
+
!optional(event, "timedOut", (value) => value === true) ||
|
|
195
|
+
!optional(event, "aborted", (value) => value === true) ||
|
|
196
|
+
!optional(event, "truncated", (value) => value === true) ||
|
|
197
|
+
!optional(event, "reason", (value) => typeof value === "string") ||
|
|
198
|
+
!optional(event, "correlation", validCorrelation)) {
|
|
199
|
+
return "child lifecycle optional fields are invalid";
|
|
200
|
+
}
|
|
201
|
+
if (!optional(event, "herdrPaneId", isLedgerDisplayIdentifier) ||
|
|
202
|
+
!optional(event, "herdrAgentName", isLedgerDisplayIdentifier)) {
|
|
203
|
+
return "Herdr runtime identity is invalid";
|
|
204
|
+
}
|
|
205
|
+
if (Object.hasOwn(event, "herdrPaneId") !== Object.hasOwn(event, "herdrAgentName")) {
|
|
206
|
+
return "Herdr runtime identity must be paired";
|
|
207
|
+
}
|
|
208
|
+
if (Object.hasOwn(event, "herdrPaneId") && event.executor !== "herdr") {
|
|
209
|
+
return "Herdr runtime identity requires the Herdr executor";
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function validateCheckReceipt(event: LedgerV3Object): string | null {
|
|
215
|
+
if (!SHA256_RE.test(String(event.receiptId)) || !isLedgerDisplayIdentifier(event.workspaceId) ||
|
|
216
|
+
!isLedgerDisplayIdentifier(event.checkId) || !isNonEmptyString(event.treeSha) ||
|
|
217
|
+
!optional(event, "correlation", validCorrelation)) {
|
|
218
|
+
return "check receipt required fields are invalid";
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function validateWorkflowFact(event: LedgerV3Object): string | null {
|
|
224
|
+
if (!isTimestamp(event.ts) || !isWorkflowFactId(event.factId) ||
|
|
225
|
+
!isWorkflowIdentifier(event.source) || !isWorkflowIdentifier(event.subject) ||
|
|
226
|
+
!["planned", "observed", "controller_validated"].includes(String(event.provenance)) ||
|
|
227
|
+
!["workflow_phase", "inline_skill", "transition"].includes(String(event.kind)) ||
|
|
228
|
+
!["pending", "observed", "started", "completed", "blocked"].includes(String(event.state)) ||
|
|
229
|
+
!validCorrelation(event.correlation) ||
|
|
230
|
+
!isNonEmptyString((event.correlation as LedgerV3Object).run_id)) {
|
|
231
|
+
return "workflow fact required fields are invalid";
|
|
232
|
+
}
|
|
233
|
+
return workflowFactStateMatches(event.provenance, event.state)
|
|
234
|
+
? null
|
|
235
|
+
: "workflow fact provenance contradicts its state";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Exact, content-free runtime validation for one closed ledger v3 event. */
|
|
239
|
+
export function validateLedgerV3Event(event: LedgerV3Object): string | null {
|
|
240
|
+
if (event.ledgerVersion !== 3) return "unsupported ledger version";
|
|
241
|
+
const kind = event.event;
|
|
242
|
+
if (typeof kind !== "string" || !Object.hasOwn(FIELDS, kind)) return "unknown ledger event discriminator";
|
|
243
|
+
const allowed = FIELDS[kind as EventKind];
|
|
244
|
+
if (Object.keys(event).some((field) => !allowed.has(field))) return "ledger v3 contains an unsupported field";
|
|
245
|
+
if (kind === "workflow_fact") return validateWorkflowFact(event);
|
|
246
|
+
const base = validateBase(event);
|
|
247
|
+
if (base) return base;
|
|
248
|
+
if (kind === "capability_decision") return validateCapabilityDecision(event);
|
|
249
|
+
if (kind === "workspace_lease") return validateWorkspaceLease(event);
|
|
250
|
+
if (kind === "child_lifecycle") return validateChildLifecycle(event);
|
|
251
|
+
return validateCheckReceipt(event);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Ensure a public builder cannot emit bytes the closed v3 reader would reject. */
|
|
255
|
+
export function assertLedgerV3Wire<T extends object>(event: T): T {
|
|
256
|
+
let wire: unknown;
|
|
257
|
+
try {
|
|
258
|
+
wire = JSON.parse(JSON.stringify(event));
|
|
259
|
+
} catch (error) {
|
|
260
|
+
throw new TypeError(`invalid ledger v3 event: ${String(error)}`);
|
|
261
|
+
}
|
|
262
|
+
if (!isLedgerObject(wire)) throw new TypeError("invalid ledger v3 event: event is not a JSON object");
|
|
263
|
+
const invalid = validateLedgerV3Event(wire);
|
|
264
|
+
if (invalid) throw new TypeError(`invalid ledger v3 event: ${invalid}`);
|
|
265
|
+
return event;
|
|
266
|
+
}
|
package/src/ledger.ts
CHANGED
|
@@ -32,21 +32,33 @@ import type { CorrelationMetadata } from "./correlation.ts";
|
|
|
32
32
|
import type { StructuredRefusal } from "./refusals.ts";
|
|
33
33
|
// Type-only, so the cycle with ./ledger-events.ts is erased at runtime.
|
|
34
34
|
import type { RuntimeLedgerEvent } from "./ledger-events.ts";
|
|
35
|
+
import { assertExecutionId } from "./execution-id.ts";
|
|
36
|
+
import { assertLedgerV3Wire } from "./ledger-v3-validation.ts";
|
|
35
37
|
|
|
36
|
-
export const LEDGER_VERSION =
|
|
38
|
+
export const LEDGER_VERSION = 3 as const;
|
|
39
|
+
export const LEDGER_EVENT_KINDS = [
|
|
40
|
+
"capability_decision", "workspace_lease", "child_lifecycle", "check_receipt", "workflow_fact",
|
|
41
|
+
] as const;
|
|
42
|
+
export type LedgerEventKind = typeof LEDGER_EVENT_KINDS[number];
|
|
43
|
+
export const LEDGER_GATE_OUTCOMES = ["declined", "dismissed", "no-ui", "error"] as const;
|
|
44
|
+
export type LedgerGateOutcome = typeof LEDGER_GATE_OUTCOMES[number];
|
|
37
45
|
|
|
38
46
|
export interface LedgerEventBase {
|
|
39
|
-
/** Optional only on the legacy-compatible `GrantRecord` public type; every
|
|
47
|
+
/** Optional only on the legacy-compatible `GrantRecord` public type; every v3 event builder writes it. */
|
|
40
48
|
ledgerVersion?: typeof LEDGER_VERSION;
|
|
41
|
-
event?:
|
|
49
|
+
event?: LedgerEventKind;
|
|
42
50
|
ts: string;
|
|
51
|
+
/** Unique execution occurrence. Optional only for legacy, unversioned GrantRecord values. */
|
|
52
|
+
executionId?: string;
|
|
53
|
+
/** Explicit execution parent; null means the delegating session is not itself a governed child. */
|
|
54
|
+
parentExecutionId?: string | null;
|
|
43
55
|
childId?: string;
|
|
44
56
|
correlation?: CorrelationMetadata;
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
export interface GrantRecord extends LedgerEventBase {
|
|
48
60
|
/**
|
|
49
|
-
* Present only when a trusted `taskDigest` was supplied — that is what makes a line
|
|
61
|
+
* Present only when a trusted `taskDigest` was supplied — that is what makes a line v3. `buildRecord`
|
|
50
62
|
* does NOT always emit it, and the comment here used to say it did while the emission site 200 lines
|
|
51
63
|
* down said the opposite. Two comments asserting contradictory facts about one field is R-28's shape.
|
|
52
64
|
*/
|
|
@@ -104,7 +116,7 @@ export interface GrantRecord extends LedgerEventBase {
|
|
|
104
116
|
/** Present only when the source was a live prompt, and only when one scope covers the whole set. */
|
|
105
117
|
approvalScope?: ApprovalScope;
|
|
106
118
|
/** A human was asked and declined. Distinct from `denied`, which is an escalation attempt. */
|
|
107
|
-
humanDenied?:
|
|
119
|
+
humanDenied?: true;
|
|
108
120
|
/**
|
|
109
121
|
* WHY a gate went unsatisfied, when the answer was not a yes.
|
|
110
122
|
*
|
|
@@ -120,9 +132,10 @@ export interface GrantRecord extends LedgerEventBase {
|
|
|
120
132
|
* say *"nobody was there to ask"* and be believed, so it is recorded rather than inferred.
|
|
121
133
|
*
|
|
122
134
|
* **Privacy is unchanged**: this is a fixed five-member enum, not text — nothing model-authored, nothing
|
|
123
|
-
* a task could carry.
|
|
135
|
+
* a task could carry. The prompt has five outcomes, but `granted` is deliberately omitted from the
|
|
136
|
+
* four-member ledger enum because the approval source/scope fields already record a yes.
|
|
124
137
|
*/
|
|
125
|
-
gateOutcome?:
|
|
138
|
+
gateOutcome?: LedgerGateOutcome;
|
|
126
139
|
/**
|
|
127
140
|
* WHICH operator-authored instructions this child was given (ADR-0018).
|
|
128
141
|
*
|
|
@@ -158,6 +171,8 @@ export interface GrantRecord extends LedgerEventBase {
|
|
|
158
171
|
* `definitionDigest` names its instructions, and neither says where the TASK came from.
|
|
159
172
|
*/
|
|
160
173
|
taskFrom?: string;
|
|
174
|
+
/** Unique occurrence whose output composed this task; taskFrom remains the readable logical position. */
|
|
175
|
+
taskFromExecutionId?: string;
|
|
161
176
|
/** Trusted SHA-256 of the exact task; task text remains forbidden (ADR-0034). */
|
|
162
177
|
taskDigest?: string;
|
|
163
178
|
/** Stable machine-readable refusal accompanying `reason`. */
|
|
@@ -182,6 +197,10 @@ export interface LedgerOptions {
|
|
|
182
197
|
}
|
|
183
198
|
|
|
184
199
|
export function buildRecord(args: {
|
|
200
|
+
/** Required whenever taskDigest makes this an explicit v3 event. */
|
|
201
|
+
executionId?: string;
|
|
202
|
+
/** Required (including explicit null) whenever taskDigest makes this an explicit v3 event. */
|
|
203
|
+
parentExecutionId?: string | null;
|
|
185
204
|
parentId: string;
|
|
186
205
|
childId: string;
|
|
187
206
|
depth: number;
|
|
@@ -201,23 +220,42 @@ export function buildRecord(args: {
|
|
|
201
220
|
definitionDigest?: DefinitionDigest;
|
|
202
221
|
/** Where the child ran (ADR-0031). Required: the probe's answer survives nowhere else. */
|
|
203
222
|
executor: ExecutorKind;
|
|
204
|
-
/** The child whose output composed this task (ADR-0033).
|
|
223
|
+
/** The logical child whose output composed this task (ADR-0033). */
|
|
205
224
|
taskFrom?: string;
|
|
225
|
+
/** The unique occurrence whose output composed this task. */
|
|
226
|
+
taskFromExecutionId?: string;
|
|
206
227
|
taskDigest?: string;
|
|
207
228
|
correlation?: CorrelationMetadata;
|
|
208
229
|
refusal?: StructuredRefusal;
|
|
209
230
|
now: Date;
|
|
210
231
|
}): GrantRecord {
|
|
232
|
+
if (args.taskDigest !== undefined) {
|
|
233
|
+
if (!/^[a-f0-9]{64}$/i.test(args.taskDigest)) throw new TypeError("taskDigest must be a SHA-256 hex digest");
|
|
234
|
+
assertExecutionId(args.executionId);
|
|
235
|
+
if (args.parentExecutionId !== null) assertExecutionId(args.parentExecutionId, "parentExecutionId");
|
|
236
|
+
if (args.parentExecutionId === args.executionId) throw new TypeError("an execution cannot be its own parent");
|
|
237
|
+
if (args.taskFromExecutionId !== undefined) assertExecutionId(args.taskFromExecutionId, "taskFromExecutionId");
|
|
238
|
+
}
|
|
239
|
+
if (args.definitionDigest && !/^[a-f0-9]{64}$/i.test(args.definitionDigest.sha256)) {
|
|
240
|
+
throw new TypeError("definitionDigest.sha256 must be a SHA-256 hex digest");
|
|
241
|
+
}
|
|
211
242
|
// R-46: the scalar is a SUMMARY, emitted only when it cannot mislead. `buildRecord` derives it rather
|
|
212
243
|
// than accepting it, so a call site cannot supply one that disagrees with the map beside it.
|
|
213
244
|
const sources = args.approvalSources ?? {};
|
|
214
245
|
const distinct = [...new Set(Object.values(sources))];
|
|
215
246
|
const scopes = args.approvalScopes ?? {};
|
|
216
247
|
const distinctScopes = [...new Set(Object.values(scopes))];
|
|
217
|
-
|
|
218
|
-
// A trusted task digest is what distinguishes a
|
|
248
|
+
const record: GrantRecord = {
|
|
249
|
+
// A trusted task digest is what distinguishes a v3 capability event from the legacy construction API.
|
|
219
250
|
// Production delegation always supplies it; old TypeScript callers remain able to append legacy lines.
|
|
220
|
-
...(args.taskDigest
|
|
251
|
+
...(args.taskDigest !== undefined
|
|
252
|
+
? {
|
|
253
|
+
ledgerVersion: LEDGER_VERSION,
|
|
254
|
+
event: "capability_decision" as const,
|
|
255
|
+
executionId: args.executionId,
|
|
256
|
+
parentExecutionId: args.parentExecutionId,
|
|
257
|
+
}
|
|
258
|
+
: {}),
|
|
221
259
|
ts: args.now.toISOString(),
|
|
222
260
|
parentId: args.parentId,
|
|
223
261
|
childId: args.childId,
|
|
@@ -225,7 +263,8 @@ export function buildRecord(args: {
|
|
|
225
263
|
agentType: args.agentType,
|
|
226
264
|
executor: args.executor,
|
|
227
265
|
...(args.taskFrom ? { taskFrom: args.taskFrom } : {}),
|
|
228
|
-
...(args.
|
|
266
|
+
...(args.taskFromExecutionId ? { taskFromExecutionId: args.taskFromExecutionId } : {}),
|
|
267
|
+
...(args.taskDigest !== undefined ? { taskDigest: args.taskDigest } : {}),
|
|
229
268
|
...(args.correlation ? { correlation: structuredClone(args.correlation) } : {}),
|
|
230
269
|
...(args.refusal ? { refusal: structuredClone(args.refusal) } : {}),
|
|
231
270
|
requested: args.requested,
|
|
@@ -252,6 +291,8 @@ export function buildRecord(args: {
|
|
|
252
291
|
...(args.gateOutcome && args.gateOutcome !== "granted" ? { gateOutcome: args.gateOutcome } : {}),
|
|
253
292
|
...(args.definitionDigest ? { definitionDigest: args.definitionDigest } : {}),
|
|
254
293
|
};
|
|
294
|
+
if (args.taskDigest !== undefined) assertLedgerV3Wire(record);
|
|
295
|
+
return record;
|
|
255
296
|
}
|
|
256
297
|
|
|
257
298
|
// R-49: the lock moved to `src/file-lock.ts` so the approvals store could use the SAME one rather than
|
|
@@ -302,11 +343,33 @@ export function isEscalationAttempt(record: GrantRecord): boolean {
|
|
|
302
343
|
}
|
|
303
344
|
|
|
304
345
|
export {
|
|
346
|
+
CHILD_LIFECYCLE_STATES,
|
|
347
|
+
CHILD_PROCESS_SIGNALS,
|
|
348
|
+
WORKSPACE_ACCESSES,
|
|
349
|
+
WORKSPACE_LEASE_OUTCOMES,
|
|
350
|
+
WORKSPACE_RECOVERY_VALUES,
|
|
351
|
+
buildCheckReceiptLedgerEvent,
|
|
305
352
|
buildChildLifecycleEvent,
|
|
306
353
|
buildWorkspaceLeaseEvent,
|
|
354
|
+
type CapabilityDecisionEvent,
|
|
307
355
|
type ChildLifecycleEvent,
|
|
356
|
+
type ChildLifecycleState,
|
|
357
|
+
type ChildProcessSignal,
|
|
308
358
|
type CheckReceiptLedgerEvent,
|
|
309
359
|
type RuntimeLedgerEvent,
|
|
360
|
+
type WorkspaceAccess,
|
|
310
361
|
type WorkspaceLeaseEvent,
|
|
311
362
|
type WorkspaceLeaseOutcome,
|
|
363
|
+
type WorkspaceRecovery,
|
|
312
364
|
} from "./ledger-events.ts";
|
|
365
|
+
|
|
366
|
+
export {
|
|
367
|
+
WORKFLOW_FACT_KINDS,
|
|
368
|
+
WORKFLOW_FACT_PROVENANCE,
|
|
369
|
+
WORKFLOW_FACT_STATES,
|
|
370
|
+
buildWorkflowFactEvent,
|
|
371
|
+
type WorkflowFactEvent,
|
|
372
|
+
type WorkflowFactKind,
|
|
373
|
+
type WorkflowFactProvenance,
|
|
374
|
+
type WorkflowFactState,
|
|
375
|
+
} from "./workflow-facts.ts";
|
package/src/propagation.ts
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
import type { Capability } from "./resolve.ts";
|
|
29
29
|
import { WILDCARD } from "./pi-tools.ts";
|
|
30
|
+
import { WORKSPACE_WILDCARD } from "./resolve.ts";
|
|
30
31
|
import { inheritApprovals, type InheritableApproval } from "./approval.ts";
|
|
31
32
|
import { assertCapabilitiesArePropagatable } from "./capabilities.ts";
|
|
32
33
|
|
|
@@ -45,6 +46,8 @@ export const ENV_FANOUT = "PI_GRANTS_FANOUT";
|
|
|
45
46
|
* Without it every level restarts at `d0` and the ledger cannot be joined into a tree across processes.
|
|
46
47
|
*/
|
|
47
48
|
export const ENV_PARENT_ID = "PI_GRANTS_PARENT_ID";
|
|
49
|
+
/** Unique identity of this governed execution occurrence; unlike ENV_PARENT_ID it is never reused. */
|
|
50
|
+
export const ENV_EXECUTION_ID = "PI_GRANTS_EXECUTION_ID";
|
|
48
51
|
export const ENV_DEPTH = "PI_GRANTS_DEPTH";
|
|
49
52
|
export const ENV_MAX_DEPTH = "PI_GRANTS_MAX_DEPTH";
|
|
50
53
|
export const ENV_GATED = "PI_GRANTS_GATED";
|
|
@@ -67,6 +70,7 @@ export const GRANT_ENV_KEYS = [
|
|
|
67
70
|
ENV_APPROVED,
|
|
68
71
|
ENV_FANOUT,
|
|
69
72
|
ENV_PARENT_ID,
|
|
73
|
+
ENV_EXECUTION_ID,
|
|
70
74
|
] as const;
|
|
71
75
|
|
|
72
76
|
export const parseList = (raw: string | undefined): Capability[] =>
|
|
@@ -247,9 +251,28 @@ export interface ChildEnvInput {
|
|
|
247
251
|
* empty grant, so they can spawn nothing. That fails closed. It is also unreachable in normal flow,
|
|
248
252
|
* because a session's first provider request always precedes its first tool call.
|
|
249
253
|
*/
|
|
254
|
+
/**
|
|
255
|
+
* The part of a grant a child may inherit: everything but the wildcards that are HELD, never handed down.
|
|
256
|
+
*
|
|
257
|
+
* `tool:*` for R-26's reason — a root that handed it down let every descendant reacquire the full catalog.
|
|
258
|
+
* `workspace:*` for the same reason one namespace over (R-131): a descendant holding it could route anywhere
|
|
259
|
+
* the registry lists, which is the attenuation failure ADR-0035 exists to close. `agent:*` is deliberately
|
|
260
|
+
* NOT stripped — ADR-0023 decided that a session authorised for any definition passes that on, because
|
|
261
|
+
* definitions are ceilings rather than roots.
|
|
262
|
+
*
|
|
263
|
+
* **Exported, and there is exactly one spelling of this rule on purpose.** It shipped as a filter inline in
|
|
264
|
+
* `childEnv` and nowhere else, so a grant travelling the OTHER path — `delegate.ts` building a child's
|
|
265
|
+
* `PI_GRANTS_GRANT` from `result.effective` — carried `workspace:*` straight down. The test written beside
|
|
266
|
+
* that fix exercised `childEnv`, which is not the path a delegated child's grant travels, so it could not
|
|
267
|
+
* catch it. R-28's shape: two routes for one rule, with the guard on the quieter one. Both call this now.
|
|
268
|
+
*/
|
|
269
|
+
export function inheritableGrant(grant: readonly Capability[]): Capability[] {
|
|
270
|
+
return grant.filter((c) => c !== WILDCARD && c !== WORKSPACE_WILDCARD);
|
|
271
|
+
}
|
|
272
|
+
|
|
250
273
|
export function childEnv(input: ChildEnvInput): Record<string, string> {
|
|
251
274
|
if (input.governed === false) return {};
|
|
252
|
-
const inheritable = input.ownGrant
|
|
275
|
+
const inheritable = inheritableGrant(input.ownGrant);
|
|
253
276
|
const env: Record<string, string> = {
|
|
254
277
|
[ENV_GRANT]: (assertCapabilitiesArePropagatable(inheritable), inheritable.join(",")),
|
|
255
278
|
[ENV_DEPTH]: String(input.depth + 1),
|
package/src/refusals.ts
CHANGED
package/src/resolve.ts
CHANGED
|
@@ -76,6 +76,16 @@ import { isWellFormedCapability } from "./capabilities.ts";
|
|
|
76
76
|
* `tool:*` — authority to grant every tool — which made the safe configuration the laborious one.
|
|
77
77
|
*/
|
|
78
78
|
export const AGENT_WILDCARD: Capability = "agent:*";
|
|
79
|
+
/**
|
|
80
|
+
* `workspace:*` covers any `workspace:<id>` — ADR-0035, and the second namespace wildcard.
|
|
81
|
+
*
|
|
82
|
+
* Added as a deliberate edit, which is what the comment in `resolve()` below asks for: there is no
|
|
83
|
+
* generalised `<ns>:*` rule, so a namespace does not acquire a wildcard by existing. Unlike `agent:*` this
|
|
84
|
+
* one is **not inheritable** (`childEnv` strips it) — R-26's rule, because a root that handed
|
|
85
|
+
* `workspace:*` down would make routing attenuation meaningless below the root, which is the exact defect
|
|
86
|
+
* R-131 records.
|
|
87
|
+
*/
|
|
88
|
+
export const WORKSPACE_WILDCARD: Capability = "workspace:*";
|
|
79
89
|
|
|
80
90
|
export interface ResolveInput {
|
|
81
91
|
/** What the delegating agent asked to give the child. */
|
|
@@ -125,7 +135,10 @@ export function resolve(input: ResolveInput): ResolveResult {
|
|
|
125
135
|
const parent =
|
|
126
136
|
input.subsumption === false ? held : new Set(expandSubsumed(input.parentGrant));
|
|
127
137
|
/**
|
|
128
|
-
* `agent:*` covers any `agent:<name>` — ADR-0023
|
|
138
|
+
* `agent:*` covers any `agent:<name>` — ADR-0023. It was the only wildcard rule in this function when
|
|
139
|
+
* that was written; ADR-0035 added a third, so `covered()` now has `tool:*`, `agent:*` and `workspace:*`.
|
|
140
|
+
* There is still no GENERALISED `<ns>:*` rule: each is a deliberate edit, which is the property worth
|
|
141
|
+
* keeping. Corrected because the sentence sat eighty lines above "all THREE wildcards are excluded".
|
|
129
142
|
*
|
|
130
143
|
* `resolve` is otherwise exact-match plus subsumption, deliberately: `tool:*` works not because anything
|
|
131
144
|
* here understands it, but because `deriveOwnGrant` *enumerates* a session's observed tool names beside
|
|
@@ -150,13 +163,17 @@ export function resolve(input: ResolveInput): ResolveResult {
|
|
|
150
163
|
* spellings of one rule, and the enforcing one was wrong.
|
|
151
164
|
*/
|
|
152
165
|
const anyCapability = held.has(WILDCARD);
|
|
166
|
+
const anyWorkspace = held.has(WORKSPACE_WILDCARD);
|
|
153
167
|
// A malformed id is never covered — not by an exact hold, and above all not by a wildcard's PREFIX rule,
|
|
154
168
|
// which is how `agent:x,tool:bash` got admitted and then split into two capabilities in the child.
|
|
155
169
|
// Landing in `denied` is the right outcome rather than a throw: it fails closed AND records an
|
|
156
170
|
// escalation attempt, so the ledger shows the attempt instead of a clean line.
|
|
157
171
|
const covered = (c: Capability): boolean =>
|
|
158
172
|
isWellFormedCapability(c)
|
|
159
|
-
&& (parent.has(c) || anyCapability
|
|
173
|
+
&& (parent.has(c) || anyCapability
|
|
174
|
+
|| (anyDefinition && c.startsWith("agent:"))
|
|
175
|
+
|| (anyWorkspace && c.startsWith("workspace:")));
|
|
176
|
+
|
|
160
177
|
const ceiling = input.ceiling === undefined ? null : new Set(input.ceiling);
|
|
161
178
|
const gated = new Set(input.gated ?? []);
|
|
162
179
|
const approved = new Set(input.approved ?? []);
|
|
@@ -199,7 +216,16 @@ export function resolve(input: ResolveInput): ResolveResult {
|
|
|
199
216
|
// F9: capabilities covered by a WILDCARD are not "subsumed" — this field means "the grant is broader
|
|
200
217
|
// than its list suggests", which is the `bash`-covers-`grep` warning. A wildcard holder already knows
|
|
201
218
|
// its grant is broad; listing every id under it would bury the signal the field exists to carry.
|
|
202
|
-
|
|
219
|
+
//
|
|
220
|
+
// All THREE wildcards are excluded. `anyWorkspace` was missing, so a caller holding `workspace:*` saw
|
|
221
|
+
// its own `workspace:prod` reported as subsumed while the `agent:*` holder correctly saw nothing — the
|
|
222
|
+
// field contradicting its own rule, and a false "broader than it looks" flag in the ledger and in
|
|
223
|
+
// `/grants`. Every wildcard added to `covered()` above needs a line here; that is now three for three.
|
|
224
|
+
subsumedBy: effective.filter((c) =>
|
|
225
|
+
!held.has(c)
|
|
226
|
+
&& !anyCapability
|
|
227
|
+
&& !(anyDefinition && c.startsWith("agent:"))
|
|
228
|
+
&& !(anyWorkspace && c.startsWith("workspace:"))),
|
|
203
229
|
};
|
|
204
230
|
}
|
|
205
231
|
|