@cassiomc1/forgeloop 1.3.0 → 1.6.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/.github/copilot-instructions.md +1 -0
- package/AGENTS.md +1 -0
- package/CLAUDE.md +1 -0
- package/DOCS_INDEX.md +20 -8
- package/EXECUTION_STATE.md +60 -0
- package/LOOP_ENGINEERING.md +135 -5
- package/LOOP_SYSTEM_DESIGN.md +54 -1
- package/PROTOCOL_INTEGRATION.md +87 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +69 -9
- package/TERMINOLOGY.md +15 -0
- package/THIRD_PARTY_NOTICES.md +30 -0
- package/THREAT_MODEL.md +59 -1
- package/docs/ARTIFACT_REFERENCE.md +183 -0
- package/docs/CLI_REFERENCE.md +391 -6
- package/docs/CROSS_HARNESS_CONTINUITY.md +23 -0
- package/docs/DIAGNOSTIC_MODEL.md +181 -0
- package/docs/DOCUMENTATION_GUIDE.md +36 -13
- package/docs/EXECUTION_TRACE.md +76 -0
- package/docs/GETTING_STARTED.md +1 -0
- package/docs/MCP.md +159 -0
- package/docs/RECIPES.md +149 -0
- package/docs/RELEASE_CHECKLIST_1_4.md +38 -0
- package/docs/RELEASE_CHECKLIST_1_5_MCP.md +78 -0
- package/docs/TROUBLESHOOTING.md +217 -3
- package/docs/UNIVERSAL_INTEGRATION.md +48 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
- package/docs/diagrams/README.md +55 -0
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
- package/docs/diagrams/manifest.json +42 -0
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
- package/package.json +21 -8
- package/schemas/action.schema.json +100 -0
- package/schemas/approval.schema.json +51 -0
- package/schemas/capability-policy.schema.json +41 -0
- package/schemas/diagnostic-case.schema.json +85 -0
- package/schemas/execution-receipt.schema.json +16 -0
- package/schemas/hypothesis-disposition.schema.json +16 -0
- package/schemas/intervention.schema.json +27 -0
- package/schemas/policy-lock.schema.json +1 -0
- package/schemas/policy-snapshot.schema.json +2 -0
- package/schemas/task-recovery.schema.json +61 -0
- package/schemas/trajectory-evaluation.schema.json +64 -0
- package/schemas/trajectory-scenario.schema.json +42 -0
- package/src/cli.js +267 -347
- package/src/commands/action-authorize.js +41 -0
- package/src/commands/action-propose.js +10 -0
- package/src/commands/action-reconcile.js +10 -0
- package/src/commands/action-record.js +47 -0
- package/src/commands/action-show.js +10 -0
- package/src/commands/action-verify.js +10 -0
- package/src/commands/advance.js +7 -2
- package/src/commands/approval-request.js +64 -0
- package/src/commands/approval-resolve.js +10 -0
- package/src/commands/audit.js +5 -0
- package/src/commands/baseline.js +3 -3
- package/src/commands/eval.js +6 -0
- package/src/commands/history.js +18 -0
- package/src/commands/init.js +2 -2
- package/src/commands/inspect.js +55 -0
- package/src/commands/metrics.js +7 -0
- package/src/commands/next.js +8 -2
- package/src/commands/policy-discover.js +2 -2
- package/src/commands/progress.js +6 -2
- package/src/commands/record-diagnosis.js +37 -1
- package/src/commands/record-hypothesis-disposition.js +45 -0
- package/src/commands/record-intervention.js +35 -0
- package/src/commands/reflect.js +38 -0
- package/src/commands/report.js +9 -1
- package/src/commands/run-action.js +18 -0
- package/src/commands/status.js +17 -0
- package/src/commands/task-create.js +39 -1
- package/src/commands/task-list.js +14 -1
- package/src/commands/task-lock-status.js +2 -2
- package/src/commands/task-recover.js +202 -0
- package/src/commands/task-repair-legacy-recovery.js +417 -0
- package/src/commands/task-resume.js +172 -0
- package/src/commands/task-scope.js +23 -4
- package/src/commands/task-show.js +18 -4
- package/src/commands/trace.js +34 -0
- package/src/commands/validate-protocol.js +40 -15
- package/src/core/action-authorization.js +106 -0
- package/src/core/action-constants.js +86 -0
- package/src/core/action-execution.js +105 -0
- package/src/core/action-ledger-projection.js +302 -0
- package/src/core/action-model.js +581 -0
- package/src/core/action-readiness.js +141 -0
- package/src/core/action-reconciliation-policy.js +49 -0
- package/src/core/action-reconciliation.js +66 -0
- package/src/core/action-verification.js +111 -0
- package/src/core/actions.js +462 -0
- package/src/core/approvals.js +405 -0
- package/src/core/artifact-registry.js +60 -0
- package/src/core/audit.js +45 -4
- package/src/core/bundles.js +30 -0
- package/src/core/capability-policy.js +226 -0
- package/src/core/cli-command-definitions.js +260 -5
- package/src/core/command-executors.js +543 -0
- package/src/core/command-input.js +107 -0
- package/src/core/command-runtime.js +117 -0
- package/src/core/completion-artifacts.js +39 -15
- package/src/core/completion-ownership.js +88 -0
- package/src/core/completion-recovery-rebind.js +194 -0
- package/src/core/completion.js +70 -0
- package/src/core/continuity-reconciliation.js +24 -5
- package/src/core/diagnostic-model.js +396 -0
- package/src/core/diagnostic-projection.js +51 -0
- package/src/core/diagnostic-record.js +360 -0
- package/src/core/error-codes.js +461 -1
- package/src/core/events.js +171 -2
- package/src/core/execution-prerequisites.js +4 -1
- package/src/core/execution.js +26 -188
- package/src/core/failure-signature.js +70 -0
- package/src/core/failure-surface.js +57 -0
- package/src/core/filesystem.js +55 -6
- package/src/core/history.js +110 -0
- package/src/core/hypothesis-projection.js +85 -0
- package/src/core/information-gain-projection.js +283 -0
- package/src/core/information-gain.js +138 -0
- package/src/core/inspect.js +132 -7
- package/src/core/integration-invocation-policy.js +217 -0
- package/src/core/integration-limits.js +20 -0
- package/src/core/integration-resources.js +178 -0
- package/src/core/next-action-model.js +94 -0
- package/src/core/next-action.js +490 -3
- package/src/core/phase.js +42 -22
- package/src/core/policy-engine.js +113 -6
- package/src/core/preflight-consistency.js +31 -5
- package/src/core/preflight.js +19 -2
- package/src/core/prepared-execution.js +227 -0
- package/src/core/progress.js +41 -4
- package/src/core/project-root.js +21 -0
- package/src/core/protocol-info.js +61 -0
- package/src/core/protocol.js +14 -0
- package/src/core/receipt.js +1 -0
- package/src/core/reconcile-closure.js +35 -10
- package/src/core/recovery-history.js +116 -0
- package/src/core/reflection.js +305 -0
- package/src/core/resumability.js +57 -3
- package/src/core/schema-validation.js +9 -0
- package/src/core/strategy-analysis.js +97 -0
- package/src/core/task-claim-state.js +272 -0
- package/src/core/task-command.js +5 -1
- package/src/core/task-conflict-inspection.js +321 -0
- package/src/core/task-context.js +32 -29
- package/src/core/task-discovery.js +14 -1
- package/src/core/task-lock.js +216 -22
- package/src/core/task-paths.js +31 -2
- package/src/core/task-recovery-migration.js +192 -0
- package/src/core/task-recovery.js +205 -0
- package/src/core/task-scope.js +33 -1
- package/src/core/task-snapshot.js +53 -0
- package/src/core/templates.js +9 -0
- package/src/core/trace.js +548 -0
- package/src/core/trajectory-evaluation.js +71 -0
- package/src/core/trajectory-metrics.js +80 -0
- package/src/core/transaction.js +36 -2
- package/src/core/work-state.js +10 -5
- package/src/integration.js +47 -0
- package/docs/assets/forgeloop-flow.svg +0 -1
- package/docs/forgeloop-flow.mmd +0 -51
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { canonicalFingerprint } from "./artifacts.js";
|
|
4
|
+
import {
|
|
5
|
+
APPROVAL_AUTHORITY_KINDS,
|
|
6
|
+
APPROVAL_STATUSES,
|
|
7
|
+
ACTION_CAPABILITIES,
|
|
8
|
+
ACTION_EFFECT_CLASSES,
|
|
9
|
+
ACTION_PROVENANCE,
|
|
10
|
+
ACTION_STATES,
|
|
11
|
+
CAPABILITY_DECISIONS,
|
|
12
|
+
COMMIT_UNKNOWN_RESULT_CODES,
|
|
13
|
+
SIDE_EFFECTING_EFFECT_CLASSES,
|
|
14
|
+
} from "./action-constants.js";
|
|
15
|
+
import {
|
|
16
|
+
E_ACTION_EVIDENCE_INVALID,
|
|
17
|
+
E_ACTION_INVALID,
|
|
18
|
+
E_ACTION_IDEMPOTENCY_CONFLICT,
|
|
19
|
+
E_ACTION_IDEMPOTENCY_REQUIRED,
|
|
20
|
+
E_ACTION_STATE_MISMATCH,
|
|
21
|
+
E_ACTION_VERIFICATION_REQUIRED,
|
|
22
|
+
E_APPROVAL_INVALID,
|
|
23
|
+
E_POLICY_INVALID,
|
|
24
|
+
E_TRAJECTORY_SCENARIO_INVALID,
|
|
25
|
+
} from "./error-codes.js";
|
|
26
|
+
|
|
27
|
+
const SHA256_HEX_REGEX = /^[a-f0-9]{64}$/;
|
|
28
|
+
const ACTION_ID_REGEX = /^action-[A-Za-z0-9_-]+$/;
|
|
29
|
+
const APPROVAL_ID_REGEX = /^approval-[A-Za-z0-9_-]+$/;
|
|
30
|
+
const EVAL_ID_REGEX = /^eval-[A-Za-z0-9_-]+$/;
|
|
31
|
+
|
|
32
|
+
export const ACTION_TRANSITIONS = Object.freeze(
|
|
33
|
+
new Map([
|
|
34
|
+
["PROPOSED", Object.freeze(["AUTHORIZED", "CANCELLED"])],
|
|
35
|
+
["AUTHORIZED", Object.freeze(["STARTED", "CANCELLED"])],
|
|
36
|
+
["STARTED", Object.freeze(["COMMITTED", "FAILED", "COMMIT_UNKNOWN"])],
|
|
37
|
+
["COMMITTED", Object.freeze(["VERIFIED", "COMMIT_UNKNOWN"])],
|
|
38
|
+
["VERIFIED", Object.freeze([])],
|
|
39
|
+
["FAILED", Object.freeze([])],
|
|
40
|
+
["COMMIT_UNKNOWN", Object.freeze(["COMMITTED", "PROPOSED", "COMMIT_UNKNOWN"])],
|
|
41
|
+
["CANCELLED", Object.freeze([])],
|
|
42
|
+
]),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
export function actionRequiresIdempotency(effectClass) {
|
|
46
|
+
return SIDE_EFFECTING_EFFECT_CLASSES.includes(effectClass);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function actionIsTerminal(state) {
|
|
50
|
+
return state === "VERIFIED" || state === "FAILED" || state === "CANCELLED";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function canonicalActionIdentity(actionInput) {
|
|
54
|
+
return {
|
|
55
|
+
taskId: actionInput.taskId,
|
|
56
|
+
actionId: actionInput.actionId,
|
|
57
|
+
effectClass: actionInput.effectClass,
|
|
58
|
+
capability: actionInput.capability,
|
|
59
|
+
target: actionInput.target ?? null,
|
|
60
|
+
operation: actionInput.operation ?? null,
|
|
61
|
+
idempotencyKey: actionInput.idempotencyKey ?? null,
|
|
62
|
+
requiredForCompletion: Boolean(actionInput.requiredForCompletion),
|
|
63
|
+
requirement: actionInput.requirement ?? null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function canonicalActionFingerprint(actionInput) {
|
|
68
|
+
return canonicalFingerprint(canonicalActionIdentity(actionInput));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function sha256Hex(value) {
|
|
72
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function actionError(code, message) {
|
|
76
|
+
const error = new Error(message);
|
|
77
|
+
error.code = code;
|
|
78
|
+
return error;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function assertActionTransition(from, to) {
|
|
82
|
+
const targets = ACTION_TRANSITIONS.get(from);
|
|
83
|
+
if (!targets || !targets.includes(to)) {
|
|
84
|
+
throw actionError(
|
|
85
|
+
E_ACTION_STATE_MISMATCH,
|
|
86
|
+
`Invalid durable action transition ${String(from)} -> ${String(to)}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return to;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function assertEnum(value, allowed, label) {
|
|
93
|
+
if (!allowed.includes(value)) {
|
|
94
|
+
throw actionError(E_ACTION_INVALID, `${label} must be one of ${allowed.join(", ")}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function assertBoundedText(value, label, maxLength, { required = true } = {}) {
|
|
99
|
+
if (value === undefined || value === null) {
|
|
100
|
+
if (required) throw actionError(E_ACTION_INVALID, `${label} is required`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
104
|
+
throw actionError(E_ACTION_INVALID, `${label} must be a non-empty string`);
|
|
105
|
+
}
|
|
106
|
+
if (value.length > maxLength) {
|
|
107
|
+
throw actionError(E_ACTION_INVALID, `${label} must not exceed ${maxLength} characters`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function assertTimestamp(value, label, { required = false } = {}) {
|
|
112
|
+
if (value === undefined || value === null) {
|
|
113
|
+
if (required) throw actionError(E_ACTION_INVALID, `${label} is required`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (typeof value !== "string" || Number.isNaN(Date.parse(value))) {
|
|
117
|
+
throw actionError(E_ACTION_INVALID, `${label} must be an ISO timestamp string`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function validateActionArtifact(action) {
|
|
122
|
+
if (!action || typeof action !== "object" || Array.isArray(action)) {
|
|
123
|
+
throw actionError(E_ACTION_INVALID, "durable action artifact must be a JSON object");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const key of [
|
|
127
|
+
"schemaVersion",
|
|
128
|
+
"taskId",
|
|
129
|
+
"actionId",
|
|
130
|
+
"actionFingerprint",
|
|
131
|
+
"effectClass",
|
|
132
|
+
"capability",
|
|
133
|
+
"operation",
|
|
134
|
+
"target",
|
|
135
|
+
"idempotencyKey",
|
|
136
|
+
"requiredForCompletion",
|
|
137
|
+
"requirement",
|
|
138
|
+
"provenance",
|
|
139
|
+
"state",
|
|
140
|
+
"revision",
|
|
141
|
+
"createdAt",
|
|
142
|
+
"updatedAt",
|
|
143
|
+
]) {
|
|
144
|
+
if (!Object.prototype.hasOwnProperty.call(action, key)) {
|
|
145
|
+
throw actionError(E_ACTION_INVALID, `durable action artifact.${key} is required`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (action.schemaVersion !== 1) {
|
|
150
|
+
throw actionError(E_ACTION_INVALID, "durable action schemaVersion must be 1");
|
|
151
|
+
}
|
|
152
|
+
assertBoundedText(action.taskId, "taskId", 256);
|
|
153
|
+
assertBoundedText(action.actionId, "actionId", 256);
|
|
154
|
+
if (!ACTION_ID_REGEX.test(action.actionId)) {
|
|
155
|
+
throw actionError(E_ACTION_INVALID, "actionId must match action-[A-Za-z0-9_-]+");
|
|
156
|
+
}
|
|
157
|
+
if (typeof action.actionFingerprint !== "string" || !SHA256_HEX_REGEX.test(action.actionFingerprint)) {
|
|
158
|
+
throw actionError(E_ACTION_INVALID, "actionFingerprint must be a lowercase sha256 hex digest");
|
|
159
|
+
}
|
|
160
|
+
assertEnum(action.effectClass, ACTION_EFFECT_CLASSES, "effectClass");
|
|
161
|
+
assertEnum(action.capability, ACTION_CAPABILITIES, "capability");
|
|
162
|
+
assertBoundedText(action.operation, "operation", 512);
|
|
163
|
+
assertBoundedText(action.target, "target", 512);
|
|
164
|
+
|
|
165
|
+
if (actionRequiresIdempotency(action.effectClass)) {
|
|
166
|
+
assertBoundedText(action.idempotencyKey, "idempotencyKey", 512);
|
|
167
|
+
} else if (action.idempotencyKey !== null && action.idempotencyKey !== undefined) {
|
|
168
|
+
assertBoundedText(action.idempotencyKey, "idempotencyKey", 512);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (typeof action.requiredForCompletion !== "boolean") {
|
|
172
|
+
throw actionError(E_ACTION_INVALID, "requiredForCompletion must be a boolean");
|
|
173
|
+
}
|
|
174
|
+
assertBoundedText(action.requirement, "requirement", 256, { required: false });
|
|
175
|
+
assertEnum(action.provenance, ACTION_PROVENANCE, "provenance");
|
|
176
|
+
assertEnum(action.state, ACTION_STATES, "state");
|
|
177
|
+
|
|
178
|
+
if (action.state !== "PROPOSED" && !Number.isInteger(action.revision)) {
|
|
179
|
+
throw actionError(E_ACTION_INVALID, "revision must be an integer");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
assertTimestamp(action.createdAt, "createdAt", { required: true });
|
|
183
|
+
assertTimestamp(action.updatedAt, "updatedAt", { required: true });
|
|
184
|
+
|
|
185
|
+
if (action.lastEvidenceRef !== undefined && action.lastEvidenceRef !== null) {
|
|
186
|
+
assertBoundedText(action.lastEvidenceRef, "lastEvidenceRef", 256);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (action.lastReconciliationAt !== undefined && action.lastReconciliationAt !== null) {
|
|
190
|
+
assertTimestamp(action.lastReconciliationAt, "lastReconciliationAt", { required: true });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (action.commitResultCode !== undefined && action.commitResultCode !== null) {
|
|
194
|
+
assertEnum(
|
|
195
|
+
action.commitResultCode,
|
|
196
|
+
COMMIT_UNKNOWN_RESULT_CODES,
|
|
197
|
+
"commitResultCode",
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// VERIFIED is a trust claim: it must always carry canonical evidence.
|
|
202
|
+
if (action.state === "VERIFIED" && !action.lastEvidenceRef) {
|
|
203
|
+
throw actionError(
|
|
204
|
+
E_ACTION_VERIFICATION_REQUIRED,
|
|
205
|
+
"VERIFIED actions require a canonical verification evidence reference",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const allowedKeys = new Set([
|
|
210
|
+
"schemaVersion",
|
|
211
|
+
"taskId",
|
|
212
|
+
"actionId",
|
|
213
|
+
"actionFingerprint",
|
|
214
|
+
"effectClass",
|
|
215
|
+
"capability",
|
|
216
|
+
"operation",
|
|
217
|
+
"target",
|
|
218
|
+
"idempotencyKey",
|
|
219
|
+
"requiredForCompletion",
|
|
220
|
+
"requirement",
|
|
221
|
+
"provenance",
|
|
222
|
+
"state",
|
|
223
|
+
"revision",
|
|
224
|
+
"createdAt",
|
|
225
|
+
"updatedAt",
|
|
226
|
+
"lastEvidenceRef",
|
|
227
|
+
"lastReconciliationAt",
|
|
228
|
+
"commitResultCode",
|
|
229
|
+
]);
|
|
230
|
+
for (const key of Object.keys(action)) {
|
|
231
|
+
if (!allowedKeys.has(key)) {
|
|
232
|
+
throw actionError(E_ACTION_INVALID, `durable action artifact.${key} is not an allowed property`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const recomputed = canonicalActionFingerprint(action);
|
|
237
|
+
if (recomputed !== action.actionFingerprint) {
|
|
238
|
+
throw actionError(
|
|
239
|
+
E_ACTION_INVALID,
|
|
240
|
+
"actionFingerprint does not match the immutable identity fields",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return action;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function validateApprovalArtifact(approval) {
|
|
248
|
+
if (!approval || typeof approval !== "object" || Array.isArray(approval)) {
|
|
249
|
+
throw actionError(E_APPROVAL_INVALID, "approval artifact must be a JSON object");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const key of [
|
|
253
|
+
"schemaVersion",
|
|
254
|
+
"taskId",
|
|
255
|
+
"approvalId",
|
|
256
|
+
"actionId",
|
|
257
|
+
"actionFingerprint",
|
|
258
|
+
"contractFingerprint",
|
|
259
|
+
"taskRevision",
|
|
260
|
+
"capability",
|
|
261
|
+
"status",
|
|
262
|
+
"requestedAt",
|
|
263
|
+
]) {
|
|
264
|
+
if (!Object.prototype.hasOwnProperty.call(approval, key)) {
|
|
265
|
+
throw actionError(E_APPROVAL_INVALID, `approval artifact.${key} is required`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (approval.schemaVersion !== 1) {
|
|
270
|
+
throw actionError(E_APPROVAL_INVALID, "approval schemaVersion must be 1");
|
|
271
|
+
}
|
|
272
|
+
assertBoundedText(approval.taskId, "taskId", 256);
|
|
273
|
+
assertBoundedText(approval.approvalId, "approvalId", 256);
|
|
274
|
+
if (!APPROVAL_ID_REGEX.test(approval.approvalId)) {
|
|
275
|
+
throw actionError(E_APPROVAL_INVALID, "approvalId must match approval-[A-Za-z0-9_-]+");
|
|
276
|
+
}
|
|
277
|
+
assertBoundedText(approval.actionId, "actionId", 256);
|
|
278
|
+
if (typeof approval.actionFingerprint !== "string" || !SHA256_HEX_REGEX.test(approval.actionFingerprint)) {
|
|
279
|
+
throw actionError(E_APPROVAL_INVALID, "actionFingerprint must be a lowercase sha256 hex digest");
|
|
280
|
+
}
|
|
281
|
+
if (
|
|
282
|
+
typeof approval.contractFingerprint !== "string" ||
|
|
283
|
+
!SHA256_HEX_REGEX.test(approval.contractFingerprint)
|
|
284
|
+
) {
|
|
285
|
+
throw actionError(E_APPROVAL_INVALID, "contractFingerprint must be a lowercase sha256 hex digest");
|
|
286
|
+
}
|
|
287
|
+
if (!Number.isInteger(approval.taskRevision) || approval.taskRevision < 0) {
|
|
288
|
+
throw actionError(E_APPROVAL_INVALID, "taskRevision must be a non-negative integer");
|
|
289
|
+
}
|
|
290
|
+
assertEnum(approval.capability, ACTION_CAPABILITIES, "capability");
|
|
291
|
+
assertEnum(approval.status, APPROVAL_STATUSES, "status");
|
|
292
|
+
assertTimestamp(approval.requestedAt, "requestedAt", { required: true });
|
|
293
|
+
|
|
294
|
+
if (approval.status !== "PENDING") {
|
|
295
|
+
assertEnum(approval.decision, [approval.status], "decision");
|
|
296
|
+
assertTimestamp(approval.resolvedAt, "resolvedAt", { required: true });
|
|
297
|
+
assertEnum(approval.authorityKind, APPROVAL_AUTHORITY_KINDS, "authorityKind");
|
|
298
|
+
if (approval.authorityKind === "HOST_ATTESTED" && !approval.hostGrantRef) {
|
|
299
|
+
throw actionError(
|
|
300
|
+
E_APPROVAL_INVALID,
|
|
301
|
+
"HOST_ATTESTED approvals require a hostGrantRef supplied by the host boundary",
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const allowedKeys = new Set([
|
|
307
|
+
"schemaVersion",
|
|
308
|
+
"taskId",
|
|
309
|
+
"approvalId",
|
|
310
|
+
"actionId",
|
|
311
|
+
"actionFingerprint",
|
|
312
|
+
"contractFingerprint",
|
|
313
|
+
"taskRevision",
|
|
314
|
+
"capability",
|
|
315
|
+
"status",
|
|
316
|
+
"requestedAt",
|
|
317
|
+
"reason",
|
|
318
|
+
"decision",
|
|
319
|
+
"resolvedAt",
|
|
320
|
+
"authorityKind",
|
|
321
|
+
"hostGrantRef",
|
|
322
|
+
]);
|
|
323
|
+
for (const key of Object.keys(approval)) {
|
|
324
|
+
if (!allowedKeys.has(key)) {
|
|
325
|
+
throw actionError(E_APPROVAL_INVALID, `approval artifact.${key} is not an allowed property`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
assertBoundedText(approval.reason, "reason", 512, { required: false });
|
|
329
|
+
|
|
330
|
+
return approval;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function validateCapabilityPolicy(policy) {
|
|
334
|
+
if (!policy || typeof policy !== "object" || Array.isArray(policy)) {
|
|
335
|
+
throw actionError(E_POLICY_INVALID, "capability policy must be a JSON object");
|
|
336
|
+
}
|
|
337
|
+
if (policy.schemaVersion !== 1) {
|
|
338
|
+
throw actionError(E_POLICY_INVALID, "capability policy schemaVersion must be 1");
|
|
339
|
+
}
|
|
340
|
+
if (!["ALLOW", "DENY"].includes(policy.defaultDecision)) {
|
|
341
|
+
throw actionError(
|
|
342
|
+
E_POLICY_INVALID,
|
|
343
|
+
"defaultDecision must be ALLOW or DENY; REQUIRE_* defaults are not valid",
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
if (!Array.isArray(policy.rules)) {
|
|
347
|
+
throw actionError(E_POLICY_INVALID, "rules must be an array");
|
|
348
|
+
}
|
|
349
|
+
const seen = new Set();
|
|
350
|
+
for (const rule of policy.rules) {
|
|
351
|
+
if (!rule || typeof rule !== "object") {
|
|
352
|
+
throw actionError(E_POLICY_INVALID, "each rule must be a JSON object");
|
|
353
|
+
}
|
|
354
|
+
assertEnum(rule.capability, ACTION_CAPABILITIES, "rule.capability");
|
|
355
|
+
assertEnum(rule.decision, CAPABILITY_DECISIONS, "rule.decision");
|
|
356
|
+
if (seen.has(rule.capability)) {
|
|
357
|
+
throw actionError(
|
|
358
|
+
E_POLICY_INVALID,
|
|
359
|
+
`duplicate rule for capability ${rule.capability}`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
seen.add(rule.capability);
|
|
363
|
+
}
|
|
364
|
+
const allowedKeys = new Set(["schemaVersion", "defaultDecision", "rules"]);
|
|
365
|
+
for (const key of Object.keys(policy)) {
|
|
366
|
+
if (!allowedKeys.has(key)) {
|
|
367
|
+
throw actionError(E_POLICY_INVALID, `capability policy.${key} is not an allowed property`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return policy;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function assertActionIdFormat(actionId) {
|
|
374
|
+
if (typeof actionId !== "string" || !ACTION_ID_REGEX.test(actionId)) {
|
|
375
|
+
throw actionError(E_ACTION_INVALID, "actionId must match action-[A-Za-z0-9_-]+");
|
|
376
|
+
}
|
|
377
|
+
return actionId;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function assertApprovalIdFormat(approvalId) {
|
|
381
|
+
if (typeof approvalId !== "string" || !APPROVAL_ID_REGEX.test(approvalId)) {
|
|
382
|
+
throw actionError(E_APPROVAL_INVALID, "approvalId must match approval-[A-Za-z0-9_-]+");
|
|
383
|
+
}
|
|
384
|
+
return approvalId;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function assertEvaluationIdFormat(evaluationId) {
|
|
388
|
+
if (typeof evaluationId !== "string" || !EVAL_ID_REGEX.test(evaluationId)) {
|
|
389
|
+
throw actionError(E_TRAJECTORY_SCENARIO_INVALID, "evaluationId must match eval-[A-Za-z0-9_-]+");
|
|
390
|
+
}
|
|
391
|
+
return evaluationId;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const ACTION_EVENT_NAMES = new Set([
|
|
395
|
+
"ACTION_PROPOSED",
|
|
396
|
+
"ACTION_AUTHORIZED",
|
|
397
|
+
"ACTION_STARTED",
|
|
398
|
+
"ACTION_COMMIT_RECORDED",
|
|
399
|
+
"ACTION_VERIFIED",
|
|
400
|
+
"ACTION_FAILED",
|
|
401
|
+
"ACTION_COMMIT_UNKNOWN",
|
|
402
|
+
"ACTION_RECONCILED",
|
|
403
|
+
"ACTION_CANCELLED",
|
|
404
|
+
]);
|
|
405
|
+
|
|
406
|
+
const APPROVAL_EVENT_NAMES = new Set(["APPROVAL_REQUESTED", "APPROVAL_RESOLVED"]);
|
|
407
|
+
|
|
408
|
+
function assertBoundedEventText(details, key, maxLength, { required = true } = {}) {
|
|
409
|
+
const value = details[key];
|
|
410
|
+
if (value === undefined || value === null) {
|
|
411
|
+
if (required) throw actionError(E_ACTION_EVIDENCE_INVALID, `event details.${key} is required`);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maxLength) {
|
|
415
|
+
throw actionError(
|
|
416
|
+
E_ACTION_EVIDENCE_INVALID,
|
|
417
|
+
`event details.${key} must be a non-empty string of at most ${maxLength} characters`,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Mandatory details for a modern ACTION_AUTHORIZED event. Legacy events that
|
|
424
|
+
* omit these fields remain ledger-readable but are not trusted authorization
|
|
425
|
+
* evidence for new required-action completion.
|
|
426
|
+
*/
|
|
427
|
+
export function assertActionAuthorizationDetails(details, { legacyAllowed = false } = {}) {
|
|
428
|
+
const required = legacyAllowed
|
|
429
|
+
? []
|
|
430
|
+
: [
|
|
431
|
+
"capabilityDecision",
|
|
432
|
+
"actionFingerprint",
|
|
433
|
+
"capabilityPolicyFingerprint",
|
|
434
|
+
"policyLockDigest",
|
|
435
|
+
"taskPolicyDigest",
|
|
436
|
+
];
|
|
437
|
+
for (const key of required) {
|
|
438
|
+
assertBoundedEventText(details, key, 512);
|
|
439
|
+
}
|
|
440
|
+
if (details.capabilityDecision !== undefined) {
|
|
441
|
+
if (!["ALLOW", "REQUIRE_AUTHORITY", "REQUIRE_APPROVAL"].includes(details.capabilityDecision)) {
|
|
442
|
+
throw actionError(
|
|
443
|
+
E_ACTION_EVIDENCE_INVALID,
|
|
444
|
+
"capabilityDecision must be ALLOW, REQUIRE_AUTHORITY, or REQUIRE_APPROVAL",
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
if (
|
|
448
|
+
typeof details.capabilityPolicyFingerprint !== "string" ||
|
|
449
|
+
!SHA256_HEX_REGEX.test(details.capabilityPolicyFingerprint)
|
|
450
|
+
) {
|
|
451
|
+
throw actionError(
|
|
452
|
+
E_ACTION_EVIDENCE_INVALID,
|
|
453
|
+
"capabilityPolicyFingerprint must bind ACTION_AUTHORIZED to the exact project policy",
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
if (typeof details.policyLockDigest !== "string" || !details.policyLockDigest.startsWith("sha256:")) {
|
|
457
|
+
throw actionError(
|
|
458
|
+
E_ACTION_EVIDENCE_INVALID,
|
|
459
|
+
"policyLockDigest must bind ACTION_AUTHORIZED to the persisted policy lock",
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
if (typeof details.taskPolicyDigest !== "string" || !details.taskPolicyDigest.startsWith("sha256:")) {
|
|
463
|
+
throw actionError(
|
|
464
|
+
E_ACTION_EVIDENCE_INVALID,
|
|
465
|
+
"taskPolicyDigest must bind ACTION_AUTHORIZED to the task policy snapshot",
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
if (details.capabilityDecision === "REQUIRE_AUTHORITY") {
|
|
469
|
+
if (details.authorityKind !== "HOST_ATTESTED") {
|
|
470
|
+
throw actionError(
|
|
471
|
+
E_ACTION_EVIDENCE_INVALID,
|
|
472
|
+
"REQUIRE_AUTHORIZATION decisions require authorityKind=HOST_ATTESTED",
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
assertBoundedEventText(details, "authorityRef", 256);
|
|
476
|
+
}
|
|
477
|
+
if (details.capabilityDecision === "REQUIRE_APPROVAL") {
|
|
478
|
+
assertBoundedEventText(details, "approvalId", 256);
|
|
479
|
+
if (typeof details.approvalFingerprint !== "string") {
|
|
480
|
+
throw actionError(
|
|
481
|
+
E_ACTION_EVIDENCE_INVALID,
|
|
482
|
+
"REQUIRE_APPROVAL authorizations require approvalFingerprint",
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
if (details.authorityKind !== "HOST_ATTESTED") {
|
|
486
|
+
throw actionError(
|
|
487
|
+
E_ACTION_EVIDENCE_INVALID,
|
|
488
|
+
"REQUIRE_APPROVAL authorizations require authorityKind=HOST_ATTESTED",
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
assertBoundedEventText(details, "authorityRef", 256);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Mandatory details for a modern ACTION_VERIFIED event: canonical evidence
|
|
499
|
+
* reference and kind are unconditional.
|
|
500
|
+
*/
|
|
501
|
+
export function assertActionVerificationDetails(details) {
|
|
502
|
+
assertBoundedEventText(details, "evidenceRef", 256);
|
|
503
|
+
assertBoundedEventText(details, "evidenceKind", 64);
|
|
504
|
+
assertTimestamp(details.verifiedAt, "verifiedAt", { required: true });
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export function assertActionEventDetails(event) { const details = event.details;
|
|
509
|
+
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
|
510
|
+
throw actionError(E_ACTION_EVIDENCE_INVALID, `${event.event} requires structured details`);
|
|
511
|
+
}
|
|
512
|
+
assertBoundedEventText(details, "actionId", 256);
|
|
513
|
+
if (
|
|
514
|
+
typeof details.actionFingerprint !== "string" ||
|
|
515
|
+
!SHA256_HEX_REGEX.test(details.actionFingerprint)
|
|
516
|
+
) {
|
|
517
|
+
throw actionError(
|
|
518
|
+
E_ACTION_EVIDENCE_INVALID,
|
|
519
|
+
`${event.event} details.actionFingerprint must be a lowercase sha256 hex digest`,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
if (event.fingerprint && event.fingerprint !== details.actionFingerprint) {
|
|
523
|
+
throw actionError(
|
|
524
|
+
E_ACTION_EVIDENCE_INVALID,
|
|
525
|
+
`${event.event} fingerprint does not match details.actionFingerprint`,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
if (event.event === "ACTION_RECONCILED") {
|
|
529
|
+
if (!["COMMITTED", "NOT_COMMITTED", "UNKNOWN"].includes(details.outcome)) {
|
|
530
|
+
throw actionError(
|
|
531
|
+
E_ACTION_EVIDENCE_INVALID,
|
|
532
|
+
"ACTION_RECONCILED details.outcome must be COMMITTED, NOT_COMMITTED, or UNKNOWN",
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export function assertApprovalEventDetails(event) {
|
|
540
|
+
const details = event.details;
|
|
541
|
+
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
|
542
|
+
throw actionError(E_APPROVAL_INVALID, `${event.event} requires structured details`);
|
|
543
|
+
}
|
|
544
|
+
assertBoundedEventText(details, "approvalId", 256);
|
|
545
|
+
assertBoundedEventText(details, "actionId", 256);
|
|
546
|
+
if (
|
|
547
|
+
typeof details.actionFingerprint !== "string" ||
|
|
548
|
+
!SHA256_HEX_REGEX.test(details.actionFingerprint)
|
|
549
|
+
) {
|
|
550
|
+
throw actionError(
|
|
551
|
+
E_APPROVAL_INVALID,
|
|
552
|
+
`${event.event} details.actionFingerprint must be a lowercase sha256 hex digest`,
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
if (event.event === "APPROVAL_RESOLVED") {
|
|
556
|
+
if (!["APPROVED", "REJECTED"].includes(details.decision)) {
|
|
557
|
+
throw actionError(E_APPROVAL_INVALID, "APPROVAL_RESOLVED details.decision must be APPROVED or REJECTED");
|
|
558
|
+
}
|
|
559
|
+
if (!APPROVAL_AUTHORITY_KINDS.includes(details.authorityKind)) {
|
|
560
|
+
throw actionError(E_APPROVAL_INVALID, "APPROVAL_RESOLVED details.authorityKind is invalid");
|
|
561
|
+
}
|
|
562
|
+
if (details.authorityKind === "HOST_ATTESTED" && !details.hostGrantRef) {
|
|
563
|
+
throw actionError(
|
|
564
|
+
E_APPROVAL_INVALID,
|
|
565
|
+
"HOST_ATTESTED approval resolution requires details.hostGrantRef",
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export function isActionEventName(eventName) {
|
|
573
|
+
return ACTION_EVENT_NAMES.has(eventName);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export function isApprovalEventName(eventName) {
|
|
577
|
+
return APPROVAL_EVENT_NAMES.has(eventName);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export { E_ACTION_IDEMPOTENCY_CONFLICT, E_ACTION_IDEMPOTENCY_REQUIRED };
|
|
581
|
+
|