forge-workflow 0.1.0-beta.6 → 0.1.0-beta.7

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.
Files changed (156) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/docs/guides/MIGRATION.md +3 -3
  3. package/docs/reference/RELEASE.md +3 -3
  4. package/lib/commands/merge.js +2 -2
  5. package/lib/commands/push.js +13 -4
  6. package/lib/commands/shepherd.js +1 -1
  7. package/lib/commands/validate.js +13 -1
  8. package/lib/kernel/migrations.js +1 -1
  9. package/lib/kernel/sqlite-driver.js +1 -1
  10. package/lib/memory/router.js +1 -1
  11. package/lib/memory/usage-evidence.js +1 -1
  12. package/lib/pr-monitor/flow-monitor.js +2 -2
  13. package/lib/pr-monitor/reconcile-executor.js +1 -1
  14. package/lib/project-memory.js +1 -1
  15. package/lib/validation-receipt.js +190 -0
  16. package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  17. package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  18. package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  19. package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  20. package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  21. package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  22. package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  23. package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  24. package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  25. package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  26. package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  27. package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  28. package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  29. package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  30. package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  31. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  32. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  33. package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  34. package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  35. package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  36. package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  37. package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  38. package/node_modules/@forge/contracts/index.js +32 -0
  39. package/node_modules/@forge/contracts/package.json +35 -0
  40. package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  41. package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  42. package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  43. package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  44. package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  45. package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  46. package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  47. package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  48. package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  49. package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  50. package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  51. package/node_modules/@forge/contracts/src/baseline.js +23 -0
  52. package/node_modules/@forge/contracts/src/canonical.js +151 -0
  53. package/node_modules/@forge/contracts/src/definitions.js +176 -0
  54. package/node_modules/@forge/contracts/src/identity.js +42 -0
  55. package/node_modules/@forge/contracts/src/schema.js +72 -0
  56. package/node_modules/@forge/contracts/src/validate.js +305 -0
  57. package/node_modules/@forge/flow/index.js +114 -0
  58. package/node_modules/@forge/flow/package.json +34 -0
  59. package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
  60. package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
  61. package/node_modules/@forge/flow/src/executor.js +279 -0
  62. package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
  63. package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
  64. package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
  65. package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
  66. package/node_modules/@forge/memory/index.js +331 -0
  67. package/node_modules/@forge/memory/package.json +34 -0
  68. package/node_modules/@forge/memory/src/authority-provider.js +67 -0
  69. package/node_modules/@forge/memory/src/backend-registry.js +166 -0
  70. package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
  71. package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
  72. package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
  73. package/package.json +9 -1
  74. package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  75. package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  76. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  77. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  78. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  79. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  80. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  81. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  82. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  83. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  84. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  85. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  86. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  87. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  88. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  89. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  90. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  91. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  92. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  93. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  94. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  95. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  96. package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
  97. package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
  98. package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  99. package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  100. package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  101. package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  102. package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  103. package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  104. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  105. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  106. package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  107. package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  108. package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  109. package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
  110. package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
  111. package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
  112. package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
  113. package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
  114. package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
  115. package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  116. package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  117. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  118. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  119. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  120. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  121. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  122. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  123. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  124. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  125. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  126. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  127. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  128. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  129. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  130. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  131. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  132. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  133. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  134. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  135. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  136. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  137. package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
  138. package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
  139. package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  140. package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  141. package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  142. package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  143. package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  144. package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  145. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  146. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  147. package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  148. package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  149. package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  150. package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
  151. package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
  152. package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
  153. package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
  154. package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
  155. package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
  156. package/scripts/test.js +1 -1
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+
3
+ const {
4
+ canonicalize,
5
+ computeContentHash,
6
+ semanticIdentity,
7
+ validateContract,
8
+ validateContractStructure,
9
+ } = require("@forge/contracts");
10
+
11
+ const TERMINAL_STATUSES = new Set(["PASS", "FAIL", "INCOMPLETE"]);
12
+ const MAX_ACCEPTED_PACKETS = 128;
13
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
14
+ const RFC3339_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
15
+
16
+ class FlowExecutionError extends Error {
17
+ constructor(code, message, details = []) {
18
+ super(message);
19
+ this.name = "FlowExecutionError";
20
+ this.code = code;
21
+ this.details = details;
22
+ }
23
+ }
24
+
25
+ function requiredString(value, name) {
26
+ if (typeof value !== "string" || value.length === 0) {
27
+ throw new FlowExecutionError("INVALID_EXECUTION_CONTEXT", `${name} must be a non-empty string`);
28
+ }
29
+ return value;
30
+ }
31
+
32
+ function assertPacket(workPacket, expected) {
33
+ if (!expected || typeof expected !== "object" || Array.isArray(expected)) {
34
+ throw new FlowExecutionError("INPUT_INVALID", "WorkPacket validation failed", [
35
+ { path: "$.live_expected", code: "MISSING_LIVE_EVIDENCE" },
36
+ ]);
37
+ }
38
+ const validation = validateContract(workPacket, { expected });
39
+ if (workPacket?.schema_id !== "forge.memory.work-packet.v1" || !validation.ok) {
40
+ throw new FlowExecutionError("INPUT_INVALID", "WorkPacket validation failed", validation.errors);
41
+ }
42
+ }
43
+
44
+ function validateExecutionContext(executionContext) {
45
+ try {
46
+ for (const field of ["objectId", "runId", "attemptId", "startedAt", "endedAt", "producerInstanceId"]) {
47
+ requiredString(executionContext[field], field);
48
+ }
49
+ if (!UUID.test(executionContext.objectId)) throw new TypeError("objectId is invalid");
50
+ for (const field of ["startedAt", "endedAt"]) {
51
+ const value = executionContext[field];
52
+ if (!RFC3339_UTC.test(value) || Number.isNaN(Date.parse(value))) throw new TypeError(`${field} is invalid`);
53
+ }
54
+ if (Date.parse(executionContext.endedAt) < Date.parse(executionContext.startedAt)) {
55
+ throw new TypeError("endedAt precedes startedAt");
56
+ }
57
+ const leaseEpoch = executionContext.expected?.leaseEpoch;
58
+ if (leaseEpoch !== undefined && (!Number.isInteger(leaseEpoch) || leaseEpoch < 1)) {
59
+ throw new TypeError("leaseEpoch is invalid");
60
+ }
61
+ } catch (error) {
62
+ throw new FlowExecutionError("INVALID_EXECUTION_CONTEXT", `Invalid execution context: ${error.message}`);
63
+ }
64
+ }
65
+
66
+ function incompleteProviderResult(code) {
67
+ return {
68
+ status: "INCOMPLETE",
69
+ executor: { product_id: "forge-flow", mode: "injected-provider" },
70
+ evidenceRefs: [],
71
+ validation: { status: "INCOMPLETE", code },
72
+ cleanup: { status: "UNKNOWN" },
73
+ mutationsAttempted: [],
74
+ mutationsAuthorized: [],
75
+ };
76
+ }
77
+
78
+ function isRecord(value) {
79
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
80
+ }
81
+
82
+ function isStringArray(value) {
83
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
84
+ }
85
+
86
+ function isOptionalNonNegativeInteger(value) {
87
+ return value === undefined || (Number.isInteger(value) && value >= 0);
88
+ }
89
+
90
+ function normalizeProviderResult(result, providerFailed) {
91
+ if (providerFailed) return incompleteProviderResult("PROVIDER_FAILURE");
92
+ try {
93
+ const cloned = structuredClone(result);
94
+ canonicalize(cloned, { maxBytes: 16_384, maxDepth: 8, maxNodes: 128 });
95
+ const valid = isRecord(cloned)
96
+ && TERMINAL_STATUSES.has(cloned.status)
97
+ && isRecord(cloned.executor)
98
+ && Array.isArray(cloned.evidenceRefs)
99
+ && cloned.evidenceRefs.every(isRecord)
100
+ && isRecord(cloned.validation)
101
+ && isRecord(cloned.cleanup)
102
+ && isStringArray(cloned.mutationsAttempted)
103
+ && isStringArray(cloned.mutationsAuthorized)
104
+ && (cloned.tokens === undefined || isRecord(cloned.tokens))
105
+ && isOptionalNonNegativeInteger(cloned.retries)
106
+ && isOptionalNonNegativeInteger(cloned.corrections)
107
+ && isOptionalNonNegativeInteger(cloned.activeTimeMs)
108
+ && isOptionalNonNegativeInteger(cloned.passiveTimeMs);
109
+ if (!valid) return incompleteProviderResult("INVALID_PROVIDER_RESULT");
110
+ return cloned;
111
+ } catch {
112
+ return incompleteProviderResult("INVALID_PROVIDER_RESULT");
113
+ }
114
+ }
115
+
116
+ function authorizeAndClassify(result, allowedMutations) {
117
+ const allowed = new Set(allowedMutations);
118
+ const attempted = new Set(result.mutationsAttempted);
119
+ const authorized = new Set(result.mutationsAuthorized);
120
+ const unauthorized = [...attempted, ...authorized].filter((mutation) => !allowed.has(mutation));
121
+ if (unauthorized.length > 0) {
122
+ result.status = "FAIL";
123
+ result.validation = {
124
+ status: "REJECTED",
125
+ code: "UNAUTHORIZED_MUTATION",
126
+ unauthorized_count: unauthorized.length,
127
+ };
128
+ result.mutationsAuthorized = result.mutationsAuthorized.filter((mutation) => allowed.has(mutation));
129
+ return;
130
+ }
131
+ const inconsistent = [...attempted].some((mutation) => !authorized.has(mutation))
132
+ || [...authorized].some((mutation) => !attempted.has(mutation));
133
+ if (inconsistent) {
134
+ result.status = "FAIL";
135
+ result.validation = { status: "REJECTED", code: "INCONSISTENT_AUTHORIZATION" };
136
+ return;
137
+ }
138
+ const incompletePass = result.status === "PASS"
139
+ && (result.evidenceRefs.length === 0 || result.validation.status !== "PASS");
140
+ const incompleteFail = result.status === "FAIL"
141
+ && (result.evidenceRefs.length === 0 || result.validation.status !== "FAIL");
142
+ if (incompletePass || incompleteFail) {
143
+ result.status = "INCOMPLETE";
144
+ result.validation = { status: "INCOMPLETE", code: "INCOMPLETE_EVIDENCE" };
145
+ }
146
+ }
147
+
148
+ function optionalInteger(target, field, value, minimum = 0) {
149
+ if (value === undefined) return;
150
+ if (!Number.isInteger(value) || value < minimum) {
151
+ throw new FlowExecutionError("OUTPUT_INVALID", `Provider result ${field} is invalid`);
152
+ }
153
+ target[field] = value;
154
+ }
155
+
156
+ function buildReceipt(workPacket, executionContext, result) {
157
+ const startedAt = requiredString(executionContext.startedAt, "startedAt");
158
+ const endedAt = requiredString(executionContext.endedAt, "endedAt");
159
+ if (Date.parse(endedAt) < Date.parse(startedAt)) {
160
+ throw new FlowExecutionError("INVALID_EXECUTION_CONTEXT", "endedAt must not precede startedAt");
161
+ }
162
+ const producerInstanceId = requiredString(executionContext.producerInstanceId, "producerInstanceId");
163
+ const payload = {
164
+ packet_hash: workPacket.content_hash,
165
+ run_id: requiredString(executionContext.runId, "runId"),
166
+ attempt_id: requiredString(executionContext.attemptId, "attemptId"),
167
+ exact_head: workPacket.payload.target_head,
168
+ packet_revision: workPacket.payload.packet_revision,
169
+ manifest_digest: workPacket.payload.capability_manifest_digest,
170
+ workflow_config_revision: workPacket.payload.workflow_config_revision,
171
+ status: result.status,
172
+ executor: result.executor,
173
+ started_at: startedAt,
174
+ ended_at: endedAt,
175
+ evidence_refs: result.evidenceRefs,
176
+ validation: result.validation,
177
+ cleanup: result.cleanup,
178
+ mutations_attempted: result.mutationsAttempted,
179
+ mutations_authorized: result.mutationsAuthorized,
180
+ };
181
+ optionalInteger(payload, "lease_epoch", executionContext.expected.leaseEpoch, 1);
182
+ optionalInteger(payload, "retries", result.retries);
183
+ optionalInteger(payload, "corrections", result.corrections);
184
+ optionalInteger(payload, "active_time_ms", result.activeTimeMs);
185
+ optionalInteger(payload, "passive_time_ms", result.passiveTimeMs);
186
+ if (result.tokens !== undefined) payload.tokens = structuredClone(result.tokens);
187
+
188
+ const receipt = {
189
+ schema_id: "forge.memory.run-receipt.v1",
190
+ schema_version: 1,
191
+ object_id: requiredString(executionContext.objectId, "objectId"),
192
+ created_at: endedAt,
193
+ producer: { product_id: "forge-flow", product_version: "0.1.0-beta.6", instance_id: producerInstanceId },
194
+ capabilities_used: [],
195
+ provenance: { source_kind: "execution", actor_class: "system", actor_id: producerInstanceId },
196
+ payload,
197
+ extensions: {},
198
+ };
199
+ receipt.content_hash = computeContentHash(receipt);
200
+
201
+ const structural = validateContractStructure(receipt);
202
+ const consequential = validateContract(receipt, {
203
+ expected: {
204
+ packetHash: workPacket.content_hash,
205
+ workflowConfigRevision: workPacket.payload.workflow_config_revision,
206
+ capabilityManifestDigest: workPacket.payload.capability_manifest_digest,
207
+ exactHead: workPacket.payload.target_head,
208
+ ...(executionContext.expected.leaseEpoch === undefined ? {} : { leaseEpoch: executionContext.expected.leaseEpoch }),
209
+ },
210
+ });
211
+ if (!structural.ok || !consequential.ok) {
212
+ throw new FlowExecutionError("OUTPUT_INVALID", "RunReceipt validation failed", consequential.errors);
213
+ }
214
+ return receipt;
215
+ }
216
+
217
+ function createWorkPacketExecutor({ run, onReceipt = () => {}, maxAcceptedPackets = MAX_ACCEPTED_PACKETS } = {}) {
218
+ if (typeof run !== "function") throw new TypeError("run must be a function");
219
+ if (typeof onReceipt !== "function") throw new TypeError("onReceipt must be a function");
220
+ if (!Number.isSafeInteger(maxAcceptedPackets)
221
+ || maxAcceptedPackets < 1
222
+ || maxAcceptedPackets > MAX_ACCEPTED_PACKETS) {
223
+ throw new TypeError(`maxAcceptedPackets must be an integer from 1 to ${MAX_ACCEPTED_PACKETS}`);
224
+ }
225
+ const acceptedPackets = new Map();
226
+
227
+ return Object.freeze({
228
+ execute(workPacket, executionContext = {}) {
229
+ assertPacket(workPacket, executionContext.expected);
230
+ validateExecutionContext(executionContext);
231
+ const identity = semanticIdentity(workPacket);
232
+ const accepted = acceptedPackets.get(identity);
233
+ if (accepted) {
234
+ if (accepted.packetHash !== workPacket.content_hash) {
235
+ throw new FlowExecutionError("IDENTITY_CONFLICT", "WorkPacket identity conflict");
236
+ }
237
+ return structuredClone(accepted.receipt);
238
+ }
239
+ if (acceptedPackets.size >= maxAcceptedPackets) {
240
+ throw new FlowExecutionError("CAPACITY_EXCEEDED", "WorkPacket idempotency capacity exceeded");
241
+ }
242
+
243
+ let providerResult;
244
+ let providerFailed = false;
245
+ try {
246
+ providerResult = run(structuredClone(workPacket));
247
+ if (providerResult && typeof providerResult.then === "function") {
248
+ throw new TypeError("run must be synchronous");
249
+ }
250
+ } catch {
251
+ providerFailed = true;
252
+ }
253
+ let result = normalizeProviderResult(providerResult, providerFailed);
254
+ authorizeAndClassify(result, workPacket.payload.allowed_mutations);
255
+ let receipt;
256
+ try {
257
+ receipt = buildReceipt(workPacket, executionContext, result);
258
+ } catch (error) {
259
+ if (!(error instanceof FlowExecutionError) || error.code !== "OUTPUT_INVALID") throw error;
260
+ result = incompleteProviderResult("INVALID_PROVIDER_RESULT");
261
+ receipt = buildReceipt(workPacket, executionContext, result);
262
+ }
263
+ acceptedPackets.set(identity, { packetHash: workPacket.content_hash, receipt: structuredClone(receipt) });
264
+ try {
265
+ const observation = onReceipt(structuredClone(receipt));
266
+ if (observation !== null
267
+ && (typeof observation === "object" || typeof observation === "function")
268
+ && typeof observation.then === "function") {
269
+ Promise.resolve(observation).catch(() => {});
270
+ }
271
+ } catch {
272
+ // Receipt observation is secondary and cannot change the accepted execution result.
273
+ }
274
+ return receipt;
275
+ },
276
+ });
277
+ }
278
+
279
+ module.exports = { FlowExecutionError, createWorkPacketExecutor };
@@ -0,0 +1,419 @@
1
+ "use strict";
2
+
3
+ const { types } = require("node:util");
4
+ const {
5
+ canonicalize,
6
+ computeContentHash,
7
+ validateContractStructure,
8
+ } = require("@forge/contracts");
9
+
10
+ const MAX_EVIDENCE_HISTORY = 128;
11
+ const MAX_INPUT_BYTES = 1_048_576;
12
+ const MAX_MONITOR_ID_LENGTH = 128;
13
+ const SHA256 = /^[0-9a-f]{64}$/;
14
+ const TARGET = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;
15
+
16
+ class MonitorDurabilityError extends Error {
17
+ constructor(code, message, details = {}) {
18
+ super(message, Object.hasOwn(details, "cause") ? { cause: details.cause } : undefined);
19
+ this.name = "MonitorDurabilityError";
20
+ this.code = code;
21
+ Object.assign(this, details);
22
+ }
23
+ }
24
+
25
+ function fail(code, message, details) {
26
+ throw new MonitorDurabilityError(code, message, details);
27
+ }
28
+
29
+ function ownValue(object, field) {
30
+ if (!object || typeof object !== "object" || types.isProxy(object)) {
31
+ fail("INVALID_CONFIGURATION", "Monitor durability configuration must be a plain object");
32
+ }
33
+ const descriptor = Object.getOwnPropertyDescriptor(object, field);
34
+ if (!descriptor || !Object.hasOwn(descriptor, "value")) {
35
+ fail("INVALID_CONFIGURATION", `Monitor durability configuration requires ${field}`);
36
+ }
37
+ return descriptor.value;
38
+ }
39
+
40
+ function assertMethod(object, field) {
41
+ const method = ownValue(object, field);
42
+ if (typeof method !== "function") {
43
+ fail("INVALID_CONFIGURATION", `Monitor store requires ${field}()`);
44
+ }
45
+ return method.bind(object);
46
+ }
47
+
48
+ function snapshotCanonical(value, label) {
49
+ try {
50
+ return JSON.parse(canonicalize(value, {
51
+ maxDepth: 64,
52
+ maxNodes: 100_000,
53
+ maxBytes: MAX_INPUT_BYTES,
54
+ }));
55
+ } catch {
56
+ fail("INPUT_INVALID", `${label} must be bounded canonical data`);
57
+ }
58
+ }
59
+
60
+ function immutableSnapshot(value, label) {
61
+ const snapshot = snapshotCanonical(value, label);
62
+ const freeze = (current) => {
63
+ if (!current || typeof current !== "object" || Object.isFrozen(current)) return current;
64
+ Object.freeze(current);
65
+ for (const child of Object.values(current)) freeze(child);
66
+ return current;
67
+ };
68
+ return freeze(snapshot);
69
+ }
70
+
71
+ function assertMonitorId(value) {
72
+ const snapshot = snapshotCanonical(value, "monitorId");
73
+ if (typeof snapshot !== "string" || snapshot.length === 0 || snapshot.length > MAX_MONITOR_ID_LENGTH) {
74
+ fail("INPUT_INVALID", "monitorId must be a bounded canonical string");
75
+ }
76
+ return snapshot;
77
+ }
78
+
79
+ function assertEnvelope(value, schemaId) {
80
+ const snapshot = snapshotCanonical(value, schemaId);
81
+ let validation;
82
+ try {
83
+ validation = validateContractStructure(snapshot);
84
+ } catch {
85
+ fail("INPUT_INVALID", `${schemaId} structure validation failed`);
86
+ }
87
+ if (!validation?.ok) {
88
+ fail("INPUT_INVALID", `${schemaId} structure validation failed`, {
89
+ errors: validation?.errors,
90
+ });
91
+ }
92
+ const computedHash = computeContentHash(snapshot);
93
+ if (
94
+ !snapshot ||
95
+ typeof snapshot !== "object" ||
96
+ snapshot.schema_id !== schemaId ||
97
+ !snapshot.payload ||
98
+ typeof snapshot.payload !== "object" ||
99
+ snapshot.content_hash !== computedHash
100
+ ) {
101
+ fail("INPUT_INVALID", `${schemaId} identity validation failed`, {
102
+ actualHash: snapshot?.content_hash,
103
+ computedHash,
104
+ });
105
+ }
106
+ if (schemaId === "forge.memory.monitor-event.v1" || schemaId === "forge.memory.monitor-receipt.v1") {
107
+ assertMonitorId(snapshot.payload.monitor_id);
108
+ }
109
+ return snapshot;
110
+ }
111
+
112
+ function assertTargets(value) {
113
+ const snapshot = snapshotCanonical(value, "deliveryTargets");
114
+ if (
115
+ !Array.isArray(snapshot) ||
116
+ snapshot.length === 0 ||
117
+ snapshot.length > 32 ||
118
+ snapshot.some((target) => (
119
+ typeof target !== "string" ||
120
+ target.length === 0 ||
121
+ target.length > 128 ||
122
+ !TARGET.test(target)
123
+ ))
124
+ ) {
125
+ fail("INVALID_CONFIGURATION", "deliveryTargets must contain 1-32 entries");
126
+ }
127
+ return Object.freeze([...new Set(snapshot)]);
128
+ }
129
+
130
+ function readProviderCode(error) {
131
+ if (
132
+ (typeof error !== "object" && typeof error !== "function") ||
133
+ error === null ||
134
+ types.isProxy(error)
135
+ ) {
136
+ return "";
137
+ }
138
+ let descriptor;
139
+ try {
140
+ descriptor = Object.getOwnPropertyDescriptor(error, "code");
141
+ } catch {
142
+ return "";
143
+ }
144
+ return descriptor && Object.hasOwn(descriptor, "value") && typeof descriptor.value === "string"
145
+ ? descriptor.value
146
+ : "";
147
+ }
148
+
149
+ function mapProviderFailure(error) {
150
+ const code = readProviderCode(error);
151
+ if ([
152
+ "MONITOR_EVENT_CONFLICT",
153
+ "MONITOR_TARGET_SET_CONFLICT",
154
+ "MONITOR_DELIVERY_CONFLICT",
155
+ "MONITOR_RECEIPT_CONFLICT",
156
+ ].includes(code)) {
157
+ return new MonitorDurabilityError("IDENTITY_CONFLICT", "Monitor identity/content conflict");
158
+ }
159
+ if (code === "MONITOR_STALE_CURSOR") {
160
+ return new MonitorDurabilityError("STALE_CURSOR", "Monitor delivery cursor is stale");
161
+ }
162
+ if (code === "MONITOR_STALE_TERMINAL") {
163
+ return new MonitorDurabilityError(
164
+ "INCOMPLETE_TERMINAL_EVIDENCE",
165
+ "Monitor terminal evidence is stale",
166
+ );
167
+ }
168
+ if (code === "MONITOR_TERMINAL") {
169
+ return new MonitorDurabilityError("TERMINAL_FENCED", "Monitor is already terminal");
170
+ }
171
+ return new MonitorDurabilityError("PROVIDER_UNAVAILABLE", "Monitor durability provider unavailable");
172
+ }
173
+
174
+ async function providerCall(operation) {
175
+ try {
176
+ return await operation();
177
+ } catch (error) {
178
+ throw mapProviderFailure(error);
179
+ }
180
+ }
181
+
182
+ function assertRows(
183
+ value,
184
+ monitorId,
185
+ limit = MAX_EVIDENCE_HISTORY,
186
+ code = "INCOMPLETE_TERMINAL_EVIDENCE",
187
+ ) {
188
+ if (!Array.isArray(value) || value.length > limit) {
189
+ fail(code, "Monitor event history is unavailable or exceeds its bound");
190
+ }
191
+ const rows = value.map((row) => {
192
+ if (
193
+ !row ||
194
+ typeof row !== "object" ||
195
+ row.monitor_id !== monitorId ||
196
+ !Number.isSafeInteger(row.sequence) ||
197
+ row.sequence < 0 ||
198
+ typeof row.event_id !== "string" ||
199
+ !SHA256.test(row.content_hash)
200
+ ) {
201
+ fail(code, "Monitor event history is incomplete");
202
+ }
203
+ return row;
204
+ });
205
+ for (let index = 1; index < rows.length; index += 1) {
206
+ if (rows[index].sequence <= rows[index - 1].sequence) {
207
+ fail(code, "Monitor event history is not monotonic");
208
+ }
209
+ }
210
+ return rows;
211
+ }
212
+
213
+ function assertEvent(value, monitorId, eventId) {
214
+ const snapshot = snapshotCanonical(value, "monitor event");
215
+ if (
216
+ !snapshot ||
217
+ typeof snapshot !== "object" ||
218
+ snapshot.monitor_id !== monitorId ||
219
+ snapshot.event_id !== eventId
220
+ ) {
221
+ fail("INPUT_INVALID", "Delivery receipt references an unknown monitor event");
222
+ }
223
+ const [event] = assertRows([snapshot], monitorId, 1, "INPUT_INVALID");
224
+ return event;
225
+ }
226
+
227
+ function assertTail(value, monitorId, maxHistory) {
228
+ const snapshot = snapshotCanonical(value, "monitor event tail");
229
+ if (
230
+ !snapshot ||
231
+ typeof snapshot !== "object" ||
232
+ Array.isArray(snapshot) ||
233
+ typeof snapshot.overflow !== "boolean"
234
+ ) {
235
+ fail("INCOMPLETE_TERMINAL_EVIDENCE", "Monitor event tail is incomplete");
236
+ }
237
+ const rows = assertRows(snapshot.events, monitorId, maxHistory);
238
+ const expectedTruncation = snapshot.overflow && rows.length > 0 ? rows[0].sequence : null;
239
+ if (
240
+ (snapshot.overflow && rows.length !== maxHistory) ||
241
+ snapshot.truncated_before_sequence !== expectedTruncation
242
+ ) {
243
+ fail("INCOMPLETE_TERMINAL_EVIDENCE", "Monitor event tail overflow metadata is inconsistent");
244
+ }
245
+ return rows;
246
+ }
247
+
248
+ function assertDeliveryState(value, monitorId) {
249
+ const snapshot = snapshotCanonical(value, "monitor delivery state");
250
+ if (
251
+ !snapshot ||
252
+ typeof snapshot !== "object" ||
253
+ Array.isArray(snapshot) ||
254
+ !Array.isArray(snapshot.cursors) ||
255
+ !Array.isArray(snapshot.outbox) ||
256
+ !snapshot.overflow ||
257
+ typeof snapshot.overflow.cursors !== "boolean" ||
258
+ typeof snapshot.overflow.outbox !== "boolean"
259
+ ) {
260
+ fail("PROVIDER_UNAVAILABLE", "Monitor delivery state is incomplete");
261
+ }
262
+ if (snapshot.overflow.cursors) {
263
+ fail("PROVIDER_UNAVAILABLE", "Monitor delivery cursor state exceeds its bound");
264
+ }
265
+ const cursors = snapshot.cursors.map((cursor) => {
266
+ if (
267
+ !cursor ||
268
+ typeof cursor !== "object" ||
269
+ cursor.monitor_id !== monitorId ||
270
+ typeof cursor.target !== "string" ||
271
+ !Number.isSafeInteger(cursor.sequence) ||
272
+ cursor.sequence < 0
273
+ ) {
274
+ fail("PROVIDER_UNAVAILABLE", "Monitor delivery cursor state is incomplete");
275
+ }
276
+ return cursor;
277
+ });
278
+ const terminal = snapshot.terminal_receipt;
279
+ if (
280
+ terminal !== null && (
281
+ !terminal ||
282
+ typeof terminal !== "object" ||
283
+ terminal.monitor_id !== monitorId ||
284
+ !SHA256.test(terminal.content_hash) ||
285
+ typeof terminal.envelope_json !== "string"
286
+ )
287
+ ) {
288
+ fail("PROVIDER_UNAVAILABLE", "Monitor terminal state is incomplete");
289
+ }
290
+ return { cursors, terminal };
291
+ }
292
+
293
+ function assertHistoryWidth(config) {
294
+ const maxHistory = Object.hasOwn(config, "maxHistory") ? config.maxHistory : MAX_EVIDENCE_HISTORY;
295
+ if (!Number.isSafeInteger(maxHistory) || maxHistory < 1 || maxHistory > MAX_EVIDENCE_HISTORY) {
296
+ fail("INPUT_INVALID", `maxHistory must be an integer from 1 to ${MAX_EVIDENCE_HISTORY}`);
297
+ }
298
+ return maxHistory;
299
+ }
300
+
301
+ function assertTerminalEvidence(receipt, rows, maxHistory) {
302
+ const terminalState = receipt.payload.terminal_state;
303
+ if (terminalState !== "PASS" && terminalState !== "FAIL") return;
304
+ const evidenceRows = rows.slice(-maxHistory);
305
+ const digest = computeContentHash({ evidence_hashes: evidenceRows.map((row) => row.content_hash) });
306
+ if (rows.length === 0) {
307
+ if (terminalState === "FAIL" && receipt.payload.last_sequence === 0 && receipt.payload.evidence_digest === digest) {
308
+ return;
309
+ }
310
+ fail("INCOMPLETE_TERMINAL_EVIDENCE", `${terminalState} receipt does not cover durable monitor events`);
311
+ }
312
+ if (rows.at(-1).sequence !== receipt.payload.last_sequence) {
313
+ fail("INCOMPLETE_TERMINAL_EVIDENCE", `${terminalState} receipt does not cover durable monitor events`);
314
+ }
315
+ if (digest !== receipt.payload.evidence_digest) {
316
+ fail("INCOMPLETE_TERMINAL_EVIDENCE", `${terminalState} receipt evidence digest is incomplete`);
317
+ }
318
+ }
319
+
320
+ function createMonitorDurabilityBridge(options) {
321
+ const store = ownValue(options, "store");
322
+ const appendEvent = assertMethod(store, "appendEvent");
323
+ const recordDeliveryReceipt = assertMethod(store, "recordDeliveryReceipt");
324
+ const recordTerminalReceipt = assertMethod(store, "recordTerminalReceipt");
325
+ const getEvent = assertMethod(store, "getEvent");
326
+ const readEventTail = assertMethod(store, "readEventTail");
327
+ const readDeliveryState = assertMethod(store, "readDeliveryState");
328
+ const targets = assertTargets(ownValue(options, "deliveryTargets"));
329
+ const deliver = ownValue(options, "deliver");
330
+ if (typeof deliver !== "function") {
331
+ fail("INVALID_CONFIGURATION", "Monitor durability bridge requires deliver()");
332
+ }
333
+
334
+ return Object.freeze({
335
+ async persistEvent(event, config = {}) {
336
+ const validatedEvent = assertEnvelope(event, "forge.memory.monitor-event.v1");
337
+ const persistenceEvent = immutableSnapshot(validatedEvent, "monitor persistence event");
338
+ const deliveryEvent = immutableSnapshot(validatedEvent, "monitor delivery event");
339
+ const safeConfig = immutableSnapshot(config, "monitor persistence config");
340
+ const persistence = await providerCall(() => appendEvent(persistenceEvent, targets, safeConfig));
341
+ try {
342
+ const delivery = await deliver(deliveryEvent, targets);
343
+ return Object.freeze({ persistence, delivery });
344
+ } catch (error) {
345
+ fail("DELIVERY_FAILED", "Monitor delivery failed after durable persistence", {
346
+ persisted: true,
347
+ eventId: persistenceEvent.payload.event_id,
348
+ cause: error,
349
+ });
350
+ }
351
+ },
352
+
353
+ async acknowledgeDelivery(monitorId, receipt, config = {}) {
354
+ const safeMonitorId = assertMonitorId(monitorId);
355
+ const safeReceipt = immutableSnapshot(
356
+ assertEnvelope(receipt, "forge.memory.delivery-receipt.v1"),
357
+ "monitor delivery receipt",
358
+ );
359
+ const safeConfig = immutableSnapshot(config, "monitor acknowledgement config");
360
+ if (!targets.includes(safeReceipt.payload.target)) {
361
+ fail("INPUT_INVALID", "Delivery receipt target is outside the monitor targets");
362
+ }
363
+ const event = assertEvent(
364
+ await providerCall(() => getEvent(safeReceipt.payload.event_id, safeConfig)),
365
+ safeMonitorId,
366
+ safeReceipt.payload.event_id,
367
+ );
368
+ const deliveryState = assertDeliveryState(
369
+ await providerCall(() => readDeliveryState(
370
+ safeMonitorId,
371
+ { limit: MAX_EVIDENCE_HISTORY },
372
+ safeConfig,
373
+ )),
374
+ safeMonitorId,
375
+ );
376
+ const cursor = deliveryState.cursors.find(({ target }) => target === safeReceipt.payload.target);
377
+ if (cursor && cursor.sequence > event.sequence) {
378
+ fail("STALE_CURSOR", "Monitor delivery cursor is stale");
379
+ }
380
+ const persistence = await providerCall(() => recordDeliveryReceipt(safeReceipt, safeConfig));
381
+ return Object.freeze({ persistence, sequence: event.sequence });
382
+ },
383
+
384
+ async recordTerminalReceipt(receipt, config = {}) {
385
+ const safeReceipt = immutableSnapshot(
386
+ assertEnvelope(receipt, "forge.memory.monitor-receipt.v1"),
387
+ "monitor terminal receipt",
388
+ );
389
+ const safeConfig = immutableSnapshot(config, "monitor terminal config");
390
+ const maxHistory = assertHistoryWidth(safeConfig);
391
+ const monitorId = safeReceipt.payload.monitor_id;
392
+ const deliveryState = assertDeliveryState(
393
+ await providerCall(() => readDeliveryState(
394
+ monitorId,
395
+ { limit: MAX_EVIDENCE_HISTORY },
396
+ safeConfig,
397
+ )),
398
+ monitorId,
399
+ );
400
+ if (deliveryState.terminal) {
401
+ if (deliveryState.terminal.content_hash !== safeReceipt.content_hash) {
402
+ fail("IDENTITY_CONFLICT", "Monitor identity/content conflict");
403
+ }
404
+ const persistence = await providerCall(() => recordTerminalReceipt(safeReceipt, safeConfig));
405
+ return Object.freeze({ persistence });
406
+ }
407
+ const rows = assertTail(
408
+ await providerCall(() => readEventTail(monitorId, { limit: maxHistory }, safeConfig)),
409
+ monitorId,
410
+ maxHistory,
411
+ );
412
+ assertTerminalEvidence(safeReceipt, rows, maxHistory);
413
+ const persistence = await providerCall(() => recordTerminalReceipt(safeReceipt, safeConfig));
414
+ return Object.freeze({ persistence });
415
+ },
416
+ });
417
+ }
418
+
419
+ module.exports = { MonitorDurabilityError, createMonitorDurabilityBridge };