pi-daddy 0.19.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -0
- package/README.md +105 -36
- 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/check-runner.d.ts.map +1 -1
- package/dist/check-runner.js +14 -0
- package/dist/check-runner.js.map +1 -1
- package/dist/cli.js +0 -0
- 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/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 +4 -1
- package/dist/delegate.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/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/ledger-events.d.ts +27 -4
- package/dist/ledger-events.d.ts.map +1 -1
- package/dist/ledger-events.js +30 -9
- 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 +18 -5
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +29 -7
- package/dist/ledger.js.map +1 -1
- package/dist/propagation.d.ts +3 -1
- package/dist/propagation.d.ts.map +1 -1
- package/dist/propagation.js +3 -0
- package/dist/propagation.js.map +1 -1
- 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/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/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 +13 -13
- package/extensions/execute-child.ts +85 -21
- package/extensions/execution-occurrence.ts +20 -0
- package/extensions/grants-command.ts +22 -3
- package/extensions/grants.ts +44 -0
- package/extensions/run-delegation.ts +36 -56
- package/extensions/session.ts +7 -2
- package/extensions/workspace-runtime.ts +10 -0
- package/herdr-plugin/herdr-plugin.toml +18 -0
- package/package.json +16 -4
- package/src/check-runner.ts +16 -0
- 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/delegate-types.ts +6 -2
- package/src/delegate.ts +4 -2
- package/src/execution-id.ts +18 -0
- package/src/index.ts +25 -0
- package/src/ledger-events.ts +54 -10
- 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 +49 -10
- package/src/propagation.ts +3 -0
- package/src/run-child.ts +35 -3
- package/src/run-herdr.ts +9 -0
- package/src/workflow-fact-id.ts +16 -0
- package/src/workflow-facts.ts +65 -0
|
@@ -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,27 +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;
|
|
37
39
|
export const LEDGER_EVENT_KINDS = [
|
|
38
|
-
"capability_decision", "workspace_lease", "child_lifecycle", "check_receipt",
|
|
40
|
+
"capability_decision", "workspace_lease", "child_lifecycle", "check_receipt", "workflow_fact",
|
|
39
41
|
] as const;
|
|
40
42
|
export type LedgerEventKind = typeof LEDGER_EVENT_KINDS[number];
|
|
41
43
|
export const LEDGER_GATE_OUTCOMES = ["declined", "dismissed", "no-ui", "error"] as const;
|
|
42
44
|
export type LedgerGateOutcome = typeof LEDGER_GATE_OUTCOMES[number];
|
|
43
45
|
|
|
44
46
|
export interface LedgerEventBase {
|
|
45
|
-
/** 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. */
|
|
46
48
|
ledgerVersion?: typeof LEDGER_VERSION;
|
|
47
49
|
event?: LedgerEventKind;
|
|
48
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;
|
|
49
55
|
childId?: string;
|
|
50
56
|
correlation?: CorrelationMetadata;
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
export interface GrantRecord extends LedgerEventBase {
|
|
54
60
|
/**
|
|
55
|
-
* 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`
|
|
56
62
|
* does NOT always emit it, and the comment here used to say it did while the emission site 200 lines
|
|
57
63
|
* down said the opposite. Two comments asserting contradictory facts about one field is R-28's shape.
|
|
58
64
|
*/
|
|
@@ -165,6 +171,8 @@ export interface GrantRecord extends LedgerEventBase {
|
|
|
165
171
|
* `definitionDigest` names its instructions, and neither says where the TASK came from.
|
|
166
172
|
*/
|
|
167
173
|
taskFrom?: string;
|
|
174
|
+
/** Unique occurrence whose output composed this task; taskFrom remains the readable logical position. */
|
|
175
|
+
taskFromExecutionId?: string;
|
|
168
176
|
/** Trusted SHA-256 of the exact task; task text remains forbidden (ADR-0034). */
|
|
169
177
|
taskDigest?: string;
|
|
170
178
|
/** Stable machine-readable refusal accompanying `reason`. */
|
|
@@ -189,6 +197,10 @@ export interface LedgerOptions {
|
|
|
189
197
|
}
|
|
190
198
|
|
|
191
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;
|
|
192
204
|
parentId: string;
|
|
193
205
|
childId: string;
|
|
194
206
|
depth: number;
|
|
@@ -208,15 +220,21 @@ export function buildRecord(args: {
|
|
|
208
220
|
definitionDigest?: DefinitionDigest;
|
|
209
221
|
/** Where the child ran (ADR-0031). Required: the probe's answer survives nowhere else. */
|
|
210
222
|
executor: ExecutorKind;
|
|
211
|
-
/** The child whose output composed this task (ADR-0033).
|
|
223
|
+
/** The logical child whose output composed this task (ADR-0033). */
|
|
212
224
|
taskFrom?: string;
|
|
225
|
+
/** The unique occurrence whose output composed this task. */
|
|
226
|
+
taskFromExecutionId?: string;
|
|
213
227
|
taskDigest?: string;
|
|
214
228
|
correlation?: CorrelationMetadata;
|
|
215
229
|
refusal?: StructuredRefusal;
|
|
216
230
|
now: Date;
|
|
217
231
|
}): GrantRecord {
|
|
218
|
-
if (args.taskDigest !== undefined
|
|
219
|
-
throw new TypeError("taskDigest must be a SHA-256 hex digest");
|
|
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");
|
|
220
238
|
}
|
|
221
239
|
if (args.definitionDigest && !/^[a-f0-9]{64}$/i.test(args.definitionDigest.sha256)) {
|
|
222
240
|
throw new TypeError("definitionDigest.sha256 must be a SHA-256 hex digest");
|
|
@@ -227,10 +245,17 @@ export function buildRecord(args: {
|
|
|
227
245
|
const distinct = [...new Set(Object.values(sources))];
|
|
228
246
|
const scopes = args.approvalScopes ?? {};
|
|
229
247
|
const distinctScopes = [...new Set(Object.values(scopes))];
|
|
230
|
-
|
|
231
|
-
// 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.
|
|
232
250
|
// Production delegation always supplies it; old TypeScript callers remain able to append legacy lines.
|
|
233
|
-
...(args.taskDigest !== undefined
|
|
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
|
+
: {}),
|
|
234
259
|
ts: args.now.toISOString(),
|
|
235
260
|
parentId: args.parentId,
|
|
236
261
|
childId: args.childId,
|
|
@@ -238,6 +263,7 @@ export function buildRecord(args: {
|
|
|
238
263
|
agentType: args.agentType,
|
|
239
264
|
executor: args.executor,
|
|
240
265
|
...(args.taskFrom ? { taskFrom: args.taskFrom } : {}),
|
|
266
|
+
...(args.taskFromExecutionId ? { taskFromExecutionId: args.taskFromExecutionId } : {}),
|
|
241
267
|
...(args.taskDigest !== undefined ? { taskDigest: args.taskDigest } : {}),
|
|
242
268
|
...(args.correlation ? { correlation: structuredClone(args.correlation) } : {}),
|
|
243
269
|
...(args.refusal ? { refusal: structuredClone(args.refusal) } : {}),
|
|
@@ -265,6 +291,8 @@ export function buildRecord(args: {
|
|
|
265
291
|
...(args.gateOutcome && args.gateOutcome !== "granted" ? { gateOutcome: args.gateOutcome } : {}),
|
|
266
292
|
...(args.definitionDigest ? { definitionDigest: args.definitionDigest } : {}),
|
|
267
293
|
};
|
|
294
|
+
if (args.taskDigest !== undefined) assertLedgerV3Wire(record);
|
|
295
|
+
return record;
|
|
268
296
|
}
|
|
269
297
|
|
|
270
298
|
// R-49: the lock moved to `src/file-lock.ts` so the approvals store could use the SAME one rather than
|
|
@@ -334,3 +362,14 @@ export {
|
|
|
334
362
|
type WorkspaceLeaseOutcome,
|
|
335
363
|
type WorkspaceRecovery,
|
|
336
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
|
@@ -46,6 +46,8 @@ export const ENV_FANOUT = "PI_GRANTS_FANOUT";
|
|
|
46
46
|
* Without it every level restarts at `d0` and the ledger cannot be joined into a tree across processes.
|
|
47
47
|
*/
|
|
48
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";
|
|
49
51
|
export const ENV_DEPTH = "PI_GRANTS_DEPTH";
|
|
50
52
|
export const ENV_MAX_DEPTH = "PI_GRANTS_MAX_DEPTH";
|
|
51
53
|
export const ENV_GATED = "PI_GRANTS_GATED";
|
|
@@ -68,6 +70,7 @@ export const GRANT_ENV_KEYS = [
|
|
|
68
70
|
ENV_APPROVED,
|
|
69
71
|
ENV_FANOUT,
|
|
70
72
|
ENV_PARENT_ID,
|
|
73
|
+
ENV_EXECUTION_ID,
|
|
71
74
|
] as const;
|
|
72
75
|
|
|
73
76
|
export const parseList = (raw: string | undefined): Capability[] =>
|
package/src/run-child.ts
CHANGED
|
@@ -13,6 +13,15 @@ import { spawn } from "node:child_process";
|
|
|
13
13
|
import { StringDecoder } from "node:string_decoder";
|
|
14
14
|
import { parseBound } from "./propagation.ts";
|
|
15
15
|
|
|
16
|
+
interface RunChildTestControl { hardDeadlineAtAfterSpawn(): number }
|
|
17
|
+
const TEST_CONTROL = Symbol.for("pi-daddy.internal.run-child-test-control");
|
|
18
|
+
|
|
19
|
+
function currentRunChildTestControl(): RunChildTestControl | undefined {
|
|
20
|
+
if (process.env.NODE_TEST_CONTEXT !== "child-v8") return undefined;
|
|
21
|
+
const storage = (globalThis as Record<symbol, { getStore(): RunChildTestControl | undefined }>)[TEST_CONTROL];
|
|
22
|
+
return storage?.getStore();
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
/**
|
|
17
26
|
* The longest prefix of `text` that fits in `budget` BYTES, never splitting a character.
|
|
18
27
|
*
|
|
@@ -64,9 +73,11 @@ export interface ChildRunRequest {
|
|
|
64
73
|
signal?: AbortSignal;
|
|
65
74
|
/** Hard cap on captured output. Beyond it the child is killed and the result flagged. */
|
|
66
75
|
maxOutputBytes?: number;
|
|
67
|
-
/**
|
|
76
|
+
/** Soft wall-clock cap. On expiry: SIGTERM, then SIGKILL after `killGraceMs`. */
|
|
68
77
|
timeoutMs?: number;
|
|
69
78
|
killGraceMs?: number;
|
|
79
|
+
/** Absolute epoch-millisecond hard cap: SIGKILL fires here even when the SIGTERM grace has not elapsed. */
|
|
80
|
+
hardDeadlineAt?: number;
|
|
70
81
|
/**
|
|
71
82
|
* Called with each chunk as it arrives, for the parent's progress display (ADR-0032).
|
|
72
83
|
*
|
|
@@ -106,9 +117,11 @@ export const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
|
106
117
|
export const DEFAULT_KILL_GRACE_MS = 5000;
|
|
107
118
|
|
|
108
119
|
export function runChild(request: ChildRunRequest): Promise<ChildRunResult> {
|
|
120
|
+
const testControl = currentRunChildTestControl();
|
|
109
121
|
const maxOutputBytes = request.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
110
122
|
const timeoutMs = request.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
111
123
|
const killGraceMs = request.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
124
|
+
const hardDeadlineAt = request.hardDeadlineAt;
|
|
112
125
|
|
|
113
126
|
// A-R3: checked BEFORE spawning. `AbortSignal` does not replay, so a listener attached after an
|
|
114
127
|
// `await` cannot observe an abort that already happened — and the child would then run to completion
|
|
@@ -160,6 +173,16 @@ export function runChild(request: ChildRunRequest): Promise<ChildRunResult> {
|
|
|
160
173
|
timers.push(setTimeout(() => child.kill("SIGKILL"), killGraceMs));
|
|
161
174
|
};
|
|
162
175
|
|
|
176
|
+
// A timer phase may run before libuv delivers an exit that the OS already observed, especially after the
|
|
177
|
+
// controller event loop was blocked across the deadline. Give pending child-process events one turn to
|
|
178
|
+
// publish exitCode/signalCode before deciding that a governed PID is still live.
|
|
179
|
+
const controlIfRunning = (control: () => void) => {
|
|
180
|
+
setImmediate(() => {
|
|
181
|
+
if (done || child.exitCode !== null || child.signalCode !== null) return;
|
|
182
|
+
control();
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
|
|
163
186
|
/** How much of `text` the progress display has already been shown. */
|
|
164
187
|
let emittedUpTo = 0;
|
|
165
188
|
|
|
@@ -225,11 +248,20 @@ export function runChild(request: ChildRunRequest): Promise<ChildRunResult> {
|
|
|
225
248
|
child.stderr?.on("data", capture);
|
|
226
249
|
|
|
227
250
|
timers.push(
|
|
228
|
-
setTimeout(() => {
|
|
251
|
+
setTimeout(() => controlIfRunning(() => {
|
|
229
252
|
timedOut = true;
|
|
230
253
|
stop();
|
|
231
|
-
}, timeoutMs),
|
|
254
|
+
}), timeoutMs),
|
|
232
255
|
);
|
|
256
|
+
const activeHardDeadlineAt = testControl?.hardDeadlineAtAfterSpawn() ?? hardDeadlineAt;
|
|
257
|
+
if (activeHardDeadlineAt !== undefined) {
|
|
258
|
+
// Independent of the soft timer and measured from the recorded epoch, not from `spawn`: neither a
|
|
259
|
+
// delayed soft callback nor spawn/setup time may start a fresh grace period beyond the lifecycle bound.
|
|
260
|
+
timers.push(setTimeout(() => controlIfRunning(() => {
|
|
261
|
+
timedOut = true;
|
|
262
|
+
child.kill("SIGKILL");
|
|
263
|
+
}), Math.max(0, activeHardDeadlineAt - Date.now())));
|
|
264
|
+
}
|
|
233
265
|
|
|
234
266
|
const onAbort = () => {
|
|
235
267
|
aborted = true;
|
package/src/run-herdr.ts
CHANGED
|
@@ -98,6 +98,8 @@ export interface HerdrRunRequest {
|
|
|
98
98
|
* name a human can switch to **while the child is alive**, which is the whole point of the pane surviving.
|
|
99
99
|
*/
|
|
100
100
|
onPane?: (paneId: string, agentName: string) => void;
|
|
101
|
+
/** Called only after agent start and prompt both succeed; pane creation alone is still `starting`. */
|
|
102
|
+
onRunning?: (paneId: string, agentName: string) => void;
|
|
101
103
|
/** Security hook for attaching a writer lease to the tab. Unlike display callbacks, errors fail the run. */
|
|
102
104
|
onTab?: (tabId: string) => void;
|
|
103
105
|
/** Close even a settled pane before releasing a writer lease; no post-lease prompt may remain live. */
|
|
@@ -300,6 +302,13 @@ export async function runHerdrPane(request: HerdrRunRequest): Promise<ChildRunRe
|
|
|
300
302
|
|
|
301
303
|
const prompted = parseReply(await exec(["agent", "prompt", agentName, request.prompt]));
|
|
302
304
|
if (prompted.error) return { ...empty, spawnError: `herdr agent prompt failed: ${prompted.error}` };
|
|
305
|
+
if (request.onRunning) {
|
|
306
|
+
try {
|
|
307
|
+
request.onRunning(paneId, agentName);
|
|
308
|
+
} catch {
|
|
309
|
+
/* display/observation only */
|
|
310
|
+
}
|
|
311
|
+
}
|
|
303
312
|
|
|
304
313
|
const settled = await waitForSettled(exec, { ...request, name: agentName }, before, deadline, maxOutputBytes);
|
|
305
314
|
if (settled.aborted || settled.timedOut) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const WORKFLOW_FACT_ID_RE = /^fact:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
2
|
+
const WORKFLOW_IDENTIFIER_RE = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;
|
|
3
|
+
|
|
4
|
+
export function isWorkflowFactId(value: unknown): value is string {
|
|
5
|
+
return typeof value === "string" && WORKFLOW_FACT_ID_RE.test(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isWorkflowIdentifier(value: unknown): value is string {
|
|
9
|
+
return typeof value === "string" && WORKFLOW_IDENTIFIER_RE.test(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function workflowFactStateMatches(provenance: unknown, state: unknown): boolean {
|
|
13
|
+
return (provenance === "planned" && state === "pending") ||
|
|
14
|
+
(provenance === "observed" && state === "observed") ||
|
|
15
|
+
(provenance === "controller_validated" && ["started", "completed", "blocked"].includes(String(state)));
|
|
16
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { LEDGER_VERSION } from "./ledger.ts";
|
|
2
|
+
import { normaliseCorrelation, type CorrelationMetadata } from "./correlation.ts";
|
|
3
|
+
import { isWorkflowFactId, isWorkflowIdentifier, workflowFactStateMatches } from "./workflow-fact-id.ts";
|
|
4
|
+
import { assertLedgerV3Wire } from "./ledger-v3-validation.ts";
|
|
5
|
+
|
|
6
|
+
export const WORKFLOW_FACT_PROVENANCE = ["planned", "observed", "controller_validated"] as const;
|
|
7
|
+
export type WorkflowFactProvenance = typeof WORKFLOW_FACT_PROVENANCE[number];
|
|
8
|
+
export const WORKFLOW_FACT_KINDS = ["workflow_phase", "inline_skill", "transition"] as const;
|
|
9
|
+
export type WorkflowFactKind = typeof WORKFLOW_FACT_KINDS[number];
|
|
10
|
+
export const WORKFLOW_FACT_STATES = ["pending", "observed", "started", "completed", "blocked"] as const;
|
|
11
|
+
export type WorkflowFactState = typeof WORKFLOW_FACT_STATES[number];
|
|
12
|
+
|
|
13
|
+
export interface WorkflowFactEvent {
|
|
14
|
+
ledgerVersion: typeof LEDGER_VERSION;
|
|
15
|
+
event: "workflow_fact";
|
|
16
|
+
ts: string;
|
|
17
|
+
factId: string;
|
|
18
|
+
/** Package/controller identifier, not free-form display text. */
|
|
19
|
+
source: string;
|
|
20
|
+
provenance: WorkflowFactProvenance;
|
|
21
|
+
kind: WorkflowFactKind;
|
|
22
|
+
/** Phase, skill, or transition identifier. Never task text. */
|
|
23
|
+
subject: string;
|
|
24
|
+
state: WorkflowFactState;
|
|
25
|
+
/** run_id is required by the builder; other values remain non-authoritative labels. */
|
|
26
|
+
correlation: CorrelationMetadata;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildWorkflowFactEvent(args: {
|
|
30
|
+
factId: string;
|
|
31
|
+
source: string;
|
|
32
|
+
provenance: WorkflowFactProvenance;
|
|
33
|
+
kind: WorkflowFactKind;
|
|
34
|
+
subject: string;
|
|
35
|
+
state: WorkflowFactState;
|
|
36
|
+
correlation: CorrelationMetadata;
|
|
37
|
+
now: Date;
|
|
38
|
+
}): WorkflowFactEvent {
|
|
39
|
+
if (!isWorkflowFactId(args.factId)) throw new TypeError("factId must be a pi-daddy workflow fact id");
|
|
40
|
+
if (!isWorkflowIdentifier(args.source)) throw new TypeError("workflow fact source must be an identifier");
|
|
41
|
+
if (!WORKFLOW_FACT_PROVENANCE.includes(args.provenance)) throw new TypeError("workflow fact provenance is not recognised");
|
|
42
|
+
if (!WORKFLOW_FACT_KINDS.includes(args.kind)) throw new TypeError("workflow fact kind is not recognised");
|
|
43
|
+
if (!isWorkflowIdentifier(args.subject)) throw new TypeError("workflow fact subject must be an identifier, not task text");
|
|
44
|
+
if (!WORKFLOW_FACT_STATES.includes(args.state)) throw new TypeError("workflow fact state is not recognised");
|
|
45
|
+
const correlation = normaliseCorrelation(args.correlation);
|
|
46
|
+
if (!correlation?.run_id) throw new TypeError("workflow facts require correlation.run_id");
|
|
47
|
+
if (!workflowFactStateMatches(args.provenance, args.state)) {
|
|
48
|
+
const expected = args.provenance === "planned" ? "pending" : args.provenance === "observed"
|
|
49
|
+
? "observed" : "started, completed, or blocked";
|
|
50
|
+
throw new TypeError(`${args.provenance.replaceAll("_", "-")} workflow facts must use ${expected}`);
|
|
51
|
+
}
|
|
52
|
+
const event: WorkflowFactEvent = {
|
|
53
|
+
ledgerVersion: LEDGER_VERSION,
|
|
54
|
+
event: "workflow_fact",
|
|
55
|
+
ts: args.now.toISOString(),
|
|
56
|
+
factId: args.factId,
|
|
57
|
+
source: args.source,
|
|
58
|
+
provenance: args.provenance,
|
|
59
|
+
kind: args.kind,
|
|
60
|
+
subject: args.subject,
|
|
61
|
+
state: args.state,
|
|
62
|
+
correlation,
|
|
63
|
+
};
|
|
64
|
+
return assertLedgerV3Wire(event);
|
|
65
|
+
}
|