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,343 @@
1
+ "use strict";
2
+
3
+ const DEFAULT_LIMITS = Object.freeze({ maxBytes: 8_192, maxDepth: 8, maxNodes: 128 });
4
+ const LIMIT_CEILINGS = DEFAULT_LIMITS;
5
+ const NODE_STATUSES = new Set(["PASS", "FAIL", "INCOMPLETE"]);
6
+
7
+ function isPlainObject(value) {
8
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
9
+ const prototype = Object.getPrototypeOf(value);
10
+ return prototype === Object.prototype || prototype === null;
11
+ }
12
+
13
+ function safeFailure(code, details = {}, skipped = []) {
14
+ return {
15
+ status: "INCOMPLETE",
16
+ invoked: [],
17
+ skipped,
18
+ evidence: [],
19
+ error: { code, ...details },
20
+ };
21
+ }
22
+
23
+ function boundedClone(value, limits) {
24
+ let nodes = 0;
25
+
26
+ function visit(current, depth) {
27
+ nodes += 1;
28
+ if (nodes > limits.maxNodes || depth > limits.maxDepth) {
29
+ throw new RangeError("bounded value exceeds structural limits");
30
+ }
31
+ if (current === null || typeof current === "string" || typeof current === "boolean") {
32
+ return current;
33
+ }
34
+ if (typeof current === "number" && Number.isFinite(current)) return current;
35
+ if (Array.isArray(current)) return current.map((item) => visit(item, depth + 1));
36
+ if (!isPlainObject(current)) throw new TypeError("bounded value must contain JSON data only");
37
+
38
+ const result = {};
39
+ for (const key of Object.keys(current)) {
40
+ result[key] = visit(current[key], depth + 1);
41
+ }
42
+ return result;
43
+ }
44
+
45
+ const clone = visit(value, 0);
46
+ if (Buffer.byteLength(JSON.stringify(clone), "utf8") > limits.maxBytes) {
47
+ throw new RangeError("bounded value exceeds byte limit");
48
+ }
49
+ return clone;
50
+ }
51
+
52
+ function selectKeys(source, keys) {
53
+ const selected = {};
54
+ for (const key of keys) {
55
+ if (Object.hasOwn(source, key)) selected[key] = source[key];
56
+ }
57
+ return selected;
58
+ }
59
+
60
+ class SkillRuntime {
61
+ constructor(options = {}) {
62
+ const optionsValid = isPlainObject(options);
63
+ if (!optionsValid) options = {};
64
+ this.metadata = options.metadata;
65
+ this.handlers = Object.assign(Object.create(null), options.handlers || {});
66
+ this.capabilities = new Set(options.capabilities || []);
67
+ this.limits = { ...DEFAULT_LIMITS, ...(options.limits || {}) };
68
+ this.active = false;
69
+ this.nodes = new Map();
70
+ this.optionsError = optionsValid ? null : { code: "INVALID_OPTIONS", details: {} };
71
+ this.limitError = this._validateLimits(options.limits);
72
+ this.metadataError = this._indexMetadata();
73
+ }
74
+
75
+ async invoke(request = {}) {
76
+ if (this.active) return safeFailure("RECURSIVE_INVOCATION");
77
+ if (!isPlainObject(request)) return safeFailure("INVALID_REQUEST");
78
+ if (this.optionsError) return safeFailure(this.optionsError.code, this.optionsError.details);
79
+ if (this.limitError) return safeFailure("INVALID_LIMITS", this.limitError);
80
+ if (this.metadataError) return this._failure(this.metadataError.code, this.metadataError.details);
81
+
82
+ const requestedNodes = request.nodes;
83
+ if (!Array.isArray(requestedNodes) || requestedNodes.length === 0) {
84
+ return safeFailure("INVALID_REQUEST");
85
+ }
86
+
87
+ const requestedSet = new Set();
88
+ for (const nodeId of requestedNodes) {
89
+ if (requestedSet.has(nodeId)) {
90
+ return this._failure("DUPLICATE_INVOCATION", { nodeId });
91
+ }
92
+ requestedSet.add(nodeId);
93
+ }
94
+
95
+ const resolution = this._resolve(requestedNodes);
96
+ if (resolution.error) {
97
+ return this._failure(resolution.error.code, resolution.error.details);
98
+ }
99
+
100
+ const skipped = [...this.nodes.keys()]
101
+ .filter((nodeId) => !resolution.required.has(nodeId))
102
+ .map((nodeId) => ({ nodeId, reason: "NOT_REQUIRED" }));
103
+
104
+ for (const node of resolution.ordered) {
105
+ for (const capability of node.capabilities || []) {
106
+ if (!this.capabilities.has(capability)) {
107
+ return this._failure(
108
+ "MISSING_CAPABILITY",
109
+ { nodeId: node.id, capability },
110
+ skipped,
111
+ );
112
+ }
113
+ }
114
+ if (!Object.hasOwn(this.handlers, node.id) || typeof this.handlers[node.id] !== "function") {
115
+ return this._failure("MISSING_HANDLER", { nodeId: node.id }, skipped);
116
+ }
117
+ }
118
+
119
+ let inputs;
120
+ let context;
121
+ try {
122
+ inputs = boundedClone(request.inputs || {}, this.limits);
123
+ context = boundedClone(request.context || {}, this.limits);
124
+ } catch {
125
+ return this._failure("BOUNDED_INPUT_EXCEEDED", {}, skipped);
126
+ }
127
+
128
+ const preflightInvoked = resolution.ordered.map((node) => ({
129
+ nodeId: node.id,
130
+ status: "INCOMPLETE",
131
+ evidence: [],
132
+ }));
133
+ try {
134
+ boundedClone({
135
+ status: "INCOMPLETE",
136
+ invoked: preflightInvoked,
137
+ skipped,
138
+ evidence: preflightInvoked,
139
+ error: { code: "BOUNDED_OUTPUT_EXCEEDED" },
140
+ }, this.limits);
141
+ } catch {
142
+ return this._failure("BOUNDED_OUTPUT_EXCEEDED");
143
+ }
144
+
145
+ this.active = true;
146
+ const invoked = [];
147
+ const evidenceByNode = new Map();
148
+ try {
149
+ for (const node of resolution.ordered) {
150
+ const dependencyEvidence = (node.dependsOn || []).map((nodeId) => evidenceByNode.get(nodeId));
151
+ let handlerArguments;
152
+ try {
153
+ handlerArguments = {
154
+ nodeId: node.id,
155
+ inputs: boundedClone(selectKeys(inputs, node.inputKeys || []), this.limits),
156
+ context: boundedClone(selectKeys(context, node.contextKeys || []), this.limits),
157
+ dependencyEvidence: boundedClone(dependencyEvidence, this.limits),
158
+ };
159
+ } catch {
160
+ return this._boundedResult(
161
+ "INCOMPLETE",
162
+ invoked,
163
+ skipped,
164
+ { code: "INVALID_ARGUMENTS", nodeId: node.id },
165
+ );
166
+ }
167
+
168
+ let handlerResult;
169
+ try {
170
+ handlerResult = await this.handlers[node.id](handlerArguments);
171
+ } catch {
172
+ return this._boundedResult(
173
+ "INCOMPLETE",
174
+ invoked,
175
+ skipped,
176
+ { code: "HANDLER_FAILED", nodeId: node.id },
177
+ );
178
+ }
179
+
180
+ let result;
181
+ try {
182
+ result = boundedClone(handlerResult, this.limits);
183
+ } catch {
184
+ return this._boundedResult(
185
+ "INCOMPLETE",
186
+ invoked,
187
+ skipped,
188
+ { code: "INVALID_EVIDENCE", nodeId: node.id },
189
+ );
190
+ }
191
+
192
+ if (result && result.status === "INCOMPLETE"
193
+ && result.error?.code === "RECURSIVE_INVOCATION") {
194
+ return this._boundedResult(
195
+ "INCOMPLETE",
196
+ invoked,
197
+ skipped,
198
+ { code: "RECURSIVE_INVOCATION", nodeId: node.id },
199
+ );
200
+ }
201
+ if (!isPlainObject(result)
202
+ || !NODE_STATUSES.has(result.status)
203
+ || !Array.isArray(result.evidence)) {
204
+ return this._boundedResult(
205
+ "INCOMPLETE",
206
+ invoked,
207
+ skipped,
208
+ { code: "INVALID_EVIDENCE", nodeId: node.id },
209
+ );
210
+ }
211
+
212
+ let evidence;
213
+ try {
214
+ evidence = boundedClone(result.evidence, this.limits);
215
+ } catch {
216
+ return this._boundedResult(
217
+ "INCOMPLETE",
218
+ invoked,
219
+ skipped,
220
+ { code: "INVALID_EVIDENCE", nodeId: node.id },
221
+ );
222
+ }
223
+
224
+ const entry = { nodeId: node.id, status: result.status, evidence };
225
+ invoked.push(entry);
226
+ evidenceByNode.set(node.id, entry);
227
+ const bounded = this._boundedResult(result.status, invoked, skipped);
228
+ if (bounded.error?.code === "BOUNDED_OUTPUT_EXCEEDED") return bounded;
229
+ if (result.status !== "PASS") {
230
+ return bounded;
231
+ }
232
+ }
233
+ } finally {
234
+ this.active = false;
235
+ }
236
+
237
+ return this._boundedResult("PASS", invoked, skipped);
238
+ }
239
+
240
+ _boundedResult(status, invoked, skipped, error) {
241
+ const output = { status, invoked, skipped, evidence: invoked };
242
+ if (error) output.error = error;
243
+ try {
244
+ boundedClone(output, this.limits);
245
+ return output;
246
+ } catch {
247
+ if (invoked.length === 0 && error?.code) return safeFailure(error.code);
248
+ const compactInvoked = invoked.map(({ nodeId, status: invokedStatus }) => ({
249
+ nodeId,
250
+ status: invokedStatus,
251
+ evidence: [],
252
+ }));
253
+ const compactFailure = {
254
+ status: "INCOMPLETE",
255
+ invoked: compactInvoked,
256
+ skipped,
257
+ evidence: compactInvoked,
258
+ error: { code: "BOUNDED_OUTPUT_EXCEEDED" },
259
+ };
260
+ try {
261
+ boundedClone(compactFailure, this.limits);
262
+ return compactFailure;
263
+ } catch {
264
+ // Preflight normally makes this unreachable; retain a bounded fail-closed fallback.
265
+ }
266
+ return safeFailure("BOUNDED_OUTPUT_EXCEEDED");
267
+ }
268
+ }
269
+
270
+ _failure(code, details = {}, skipped = []) {
271
+ return this._boundedResult("INCOMPLETE", [], skipped, { code, ...details });
272
+ }
273
+
274
+ _validateLimits(overrides) {
275
+ if (overrides !== undefined && !isPlainObject(overrides)) {
276
+ return { limit: "limits" };
277
+ }
278
+ for (const [name, ceiling] of Object.entries(LIMIT_CEILINGS)) {
279
+ const value = this.limits[name];
280
+ if (!Number.isSafeInteger(value) || value <= 0 || value > ceiling) {
281
+ return { limit: name };
282
+ }
283
+ }
284
+ return null;
285
+ }
286
+
287
+ _indexMetadata() {
288
+ if (!isPlainObject(this.metadata)
289
+ || typeof this.metadata.id !== "string"
290
+ || this.metadata.id.length === 0
291
+ || !Array.isArray(this.metadata.nodes)) {
292
+ return { code: "INVALID_METADATA", details: {} };
293
+ }
294
+
295
+ for (const node of this.metadata.nodes) {
296
+ if (!isPlainObject(node) || typeof node.id !== "string" || node.id.length === 0) {
297
+ return { code: "INVALID_METADATA", details: {} };
298
+ }
299
+ if (this.nodes.has(node.id)) {
300
+ return { code: "DUPLICATE_NODE_METADATA", details: { nodeId: node.id } };
301
+ }
302
+ for (const field of ["dependsOn", "capabilities", "inputKeys", "contextKeys"]) {
303
+ if (node[field] !== undefined
304
+ && (!Array.isArray(node[field]) || node[field].some((item) => typeof item !== "string"))) {
305
+ return { code: "INVALID_METADATA", details: { nodeId: node.id, field } };
306
+ }
307
+ }
308
+ this.nodes.set(node.id, node);
309
+ }
310
+ return null;
311
+ }
312
+
313
+ _resolve(requestedNodes) {
314
+ const ordered = [];
315
+ const required = new Set();
316
+ const visiting = new Set();
317
+
318
+ const visit = (nodeId) => {
319
+ const node = this.nodes.get(nodeId);
320
+ if (!node) return { code: "UNKNOWN_NODE", details: { nodeId } };
321
+ if (visiting.has(nodeId)) return { code: "CYCLE", details: { nodeId } };
322
+ if (required.has(nodeId)) return null;
323
+
324
+ visiting.add(nodeId);
325
+ for (const dependencyId of node.dependsOn || []) {
326
+ const error = visit(dependencyId);
327
+ if (error) return error;
328
+ }
329
+ visiting.delete(nodeId);
330
+ required.add(nodeId);
331
+ ordered.push(node);
332
+ return null;
333
+ };
334
+
335
+ for (const nodeId of requestedNodes) {
336
+ const error = visit(nodeId);
337
+ if (error) return { error };
338
+ }
339
+ return { ordered, required };
340
+ }
341
+ }
342
+
343
+ module.exports = { DEFAULT_LIMITS, SkillRuntime };
@@ -0,0 +1,331 @@
1
+ 'use strict';
2
+
3
+ const { types } = require('node:util');
4
+ const { ContractValidationError, validateContractStructure } = require('@forge/contracts');
5
+
6
+ const TARGET_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;
7
+ const SECRET_PATTERNS = [
8
+ /gh[pousr]_[a-z0-9]{20,}/i,
9
+ /github_pat_[a-z0-9_]{20,}/i,
10
+ /sk_(?:live|test)_[a-z0-9]{16,}/i,
11
+ /sk-[a-z0-9]{16,}/i,
12
+ /AKIA[0-9A-Z]{16}/i,
13
+ /(?:api[_-]?key|token|secret|password)\s*[:=]\s*\S{8,}/i,
14
+ ];
15
+ const MAX_TARGETS = 32;
16
+ const MAX_TARGET_LENGTH = 128;
17
+ const PRIVATE_PATH_ROOTS = ['users', 'home', 'root'];
18
+ const MAX_PRIVATE_SCAN_LENGTH = 16_384;
19
+ const MONITOR_EVENT_ROW_FIELDS = Object.freeze([
20
+ 'event_id', 'monitor_id', 'sequence', 'content_hash', 'envelope_json',
21
+ 'artifact_digest', 'created_at',
22
+ ]);
23
+ const MONITOR_CURSOR_ROW_FIELDS = Object.freeze([
24
+ 'monitor_id', 'target', 'sequence', 'updated_at',
25
+ ]);
26
+ const MONITOR_OUTBOX_ROW_FIELDS = Object.freeze([
27
+ 'outbox_id', 'event_id', 'monitor_id', 'sequence', 'target', 'status',
28
+ 'attempts', 'next_attempt_at', 'created_at',
29
+ ]);
30
+ const MONITOR_TERMINAL_ROW_FIELDS = Object.freeze([
31
+ 'monitor_id', 'content_hash', 'envelope_json', 'owner_run_id', 'terminal_state',
32
+ 'last_sequence', 'evidence_digest', 'undelivered_cursor', 'created_at',
33
+ ]);
34
+ const MONITOR_TAIL_FIELDS = Object.freeze([
35
+ 'events', 'overflow', 'truncated_before_sequence',
36
+ ]);
37
+ const MONITOR_DELIVERY_STATE_FIELDS = Object.freeze([
38
+ 'cursors', 'outbox', 'terminal_receipt', 'overflow',
39
+ ]);
40
+
41
+ class MonitorStoreError extends Error {
42
+ constructor(code, message, options = {}) {
43
+ super(message, Object.hasOwn(options, 'cause') ? { cause: options.cause } : undefined);
44
+ this.name = 'MonitorStoreError';
45
+ this.code = code;
46
+ }
47
+ }
48
+
49
+ class MonitorStaleError extends MonitorStoreError {
50
+ constructor(code, message, options) {
51
+ super(code, message, options);
52
+ this.name = 'MonitorStaleError';
53
+ }
54
+ }
55
+
56
+ class MonitorConflictError extends MonitorStoreError {
57
+ constructor(code, message, options) {
58
+ super(code, message, options);
59
+ this.name = 'MonitorConflictError';
60
+ }
61
+ }
62
+
63
+ class MonitorTerminalError extends MonitorStoreError {
64
+ constructor(code, message, options) {
65
+ super(code, message, options);
66
+ this.name = 'MonitorTerminalError';
67
+ }
68
+ }
69
+
70
+ class MonitorUnavailableError extends MonitorStoreError {
71
+ constructor(code, message, options) {
72
+ super(code, message, options);
73
+ this.name = 'MonitorUnavailableError';
74
+ }
75
+ }
76
+
77
+ const MONITOR_CONFLICT_CODES = new Set([
78
+ 'MONITOR_EVENT_CONFLICT',
79
+ 'MONITOR_TARGET_SET_CONFLICT',
80
+ 'MONITOR_DELIVERY_CONFLICT',
81
+ 'MONITOR_RECEIPT_CONFLICT',
82
+ ]);
83
+
84
+ function publicMonitorError(error) {
85
+ if (error instanceof MonitorStoreError) return error;
86
+ const code = typeof error?.code === 'string' ? error.code : 'MONITOR_UNAVAILABLE';
87
+ const message = typeof error?.message === 'string' && error.message
88
+ ? error.message
89
+ : 'Monitor durability provider unavailable';
90
+ const options = { cause: error };
91
+ if (code.startsWith('MONITOR_STALE_')) return new MonitorStaleError(code, message, options);
92
+ if (MONITOR_CONFLICT_CODES.has(code)) return new MonitorConflictError(code, message, options);
93
+ if (code === 'MONITOR_TERMINAL') return new MonitorTerminalError(code, message, options);
94
+ return new MonitorUnavailableError(code, message, options);
95
+ }
96
+
97
+ async function callMonitorDriver(operation) {
98
+ try {
99
+ return await operation();
100
+ } catch (error) {
101
+ throw publicMonitorError(error);
102
+ }
103
+ }
104
+
105
+ function ordinaryDataDescriptors(value, prototype) {
106
+ if (
107
+ !value
108
+ || typeof value !== 'object'
109
+ || types.isProxy(value)
110
+ || Object.getPrototypeOf(value) !== prototype
111
+ || !Object.isExtensible(value)
112
+ ) return null;
113
+ const descriptors = Object.getOwnPropertyDescriptors(value);
114
+ if (Reflect.ownKeys(descriptors).some((key) => {
115
+ const descriptor = descriptors[key];
116
+ return typeof key !== 'string'
117
+ || !descriptor.enumerable
118
+ || !Object.hasOwn(descriptor, 'value');
119
+ })) return null;
120
+ return descriptors;
121
+ }
122
+
123
+ function hasExactFields(descriptors, fields) {
124
+ const keys = Object.keys(descriptors);
125
+ return keys.length === fields.length && fields.every(field => Object.hasOwn(descriptors, field));
126
+ }
127
+
128
+ function normalizeMonitorRow(value, fields) {
129
+ const descriptors = ordinaryDataDescriptors(value, null);
130
+ if (!descriptors || !hasExactFields(descriptors, fields) || Object.values(descriptors).some(({ value: field }) => (
131
+ field !== null
132
+ && typeof field !== 'string'
133
+ && !(typeof field === 'number' && Number.isFinite(field))
134
+ ))) return value;
135
+ return Object.defineProperties({}, descriptors);
136
+ }
137
+
138
+ function normalizeMonitorRows(value, fields) {
139
+ if (
140
+ !Array.isArray(value)
141
+ || types.isProxy(value)
142
+ || Object.getPrototypeOf(value) !== Array.prototype
143
+ || !Object.isExtensible(value)
144
+ ) return value;
145
+ const descriptors = Object.getOwnPropertyDescriptors(value);
146
+ const lengthDescriptor = descriptors.length;
147
+ if (
148
+ !lengthDescriptor
149
+ || !Object.hasOwn(lengthDescriptor, 'value')
150
+ || lengthDescriptor.value !== value.length
151
+ || lengthDescriptor.enumerable
152
+ || lengthDescriptor.configurable
153
+ || !lengthDescriptor.writable
154
+ ) return value;
155
+ const entryKeys = Reflect.ownKeys(descriptors).filter(key => key !== 'length');
156
+ if (entryKeys.length !== value.length || entryKeys.some((key) => {
157
+ const descriptor = descriptors[key];
158
+ const index = Number(key);
159
+ return typeof key !== 'string'
160
+ || !Number.isInteger(index)
161
+ || index < 0
162
+ || index >= 4_294_967_295
163
+ || index >= value.length
164
+ || String(index) !== key
165
+ || !descriptor.enumerable
166
+ || !descriptor.writable
167
+ || !descriptor.configurable
168
+ || !Object.hasOwn(descriptor, 'value');
169
+ })) return value;
170
+ return value.map(row => normalizeMonitorRow(row, fields));
171
+ }
172
+
173
+ function normalizeMonitorResult(value, expectedFields, fields) {
174
+ const descriptors = ordinaryDataDescriptors(value, Object.prototype);
175
+ if (!descriptors || !hasExactFields(descriptors, expectedFields)) return value;
176
+ for (const [field, normalize] of Object.entries(fields)) {
177
+ if (descriptors[field]) descriptors[field].value = normalize(descriptors[field].value);
178
+ }
179
+ return Object.defineProperties({}, descriptors);
180
+ }
181
+
182
+ async function readMonitorDriver(operation, normalize) {
183
+ return normalize(await callMonitorDriver(operation));
184
+ }
185
+
186
+ function hasNonWhitespacePathSegment(segment) {
187
+ return Boolean(segment && segment.trim());
188
+ }
189
+
190
+ function containsPrivateMonitorPath(value) {
191
+ if (typeof value !== 'string') return false;
192
+ if (value.length > MAX_PRIVATE_SCAN_LENGTH) return true;
193
+ const normalized = value.replaceAll('\\', '/').toLowerCase();
194
+ for (const root of PRIVATE_PATH_ROOTS) {
195
+ const marker = `/${root}/`;
196
+ let offset = 0;
197
+ while (true) {
198
+ const index = normalized.indexOf(marker, offset);
199
+ if (index < 0) break;
200
+ const segment = normalized.slice(index + marker.length).split('/')[0];
201
+ if (hasNonWhitespacePathSegment(segment)) return true;
202
+ offset = index + marker.length;
203
+ }
204
+ }
205
+ for (let code = 97; code <= 122; code += 1) {
206
+ const marker = `${String.fromCharCode(code)}:/users/`;
207
+ const index = normalized.indexOf(marker);
208
+ if (index >= 0 && hasNonWhitespacePathSegment(
209
+ normalized.slice(index + marker.length).split('/')[0],
210
+ )) return true;
211
+ }
212
+ return false;
213
+ }
214
+
215
+ function containsPrivateMonitorData(value) {
216
+ if (typeof value === 'string') {
217
+ return SECRET_PATTERNS.some(pattern => pattern.test(value)) || containsPrivateMonitorPath(value);
218
+ }
219
+ if (!value || typeof value !== 'object') return false;
220
+ return Object.entries(value).some(([key, nestedValue]) => (
221
+ SECRET_PATTERNS.some(pattern => pattern.test(key))
222
+ || containsPrivateMonitorPath(key)
223
+ || containsPrivateMonitorData(nestedValue)
224
+ ));
225
+ }
226
+
227
+ function assertEnvelope(envelope, schemaId) {
228
+ const result = validateContractStructure(envelope);
229
+ if (!result.ok || envelope?.schema_id !== schemaId) {
230
+ throw new ContractValidationError(result.ok
231
+ ? [{ path: '$.schema_id', code: 'UNEXPECTED_SCHEMA' }]
232
+ : result.errors);
233
+ }
234
+ const privacyEnvelope = schemaId === 'forge.memory.delivery-receipt.v1' && envelope?.payload
235
+ ? { ...envelope, payload: { ...envelope.payload, target: '' } }
236
+ : envelope;
237
+ if (containsPrivateMonitorData(privacyEnvelope)) {
238
+ throw new Error(`private content rejected from ${schemaId}`);
239
+ }
240
+ return envelope;
241
+ }
242
+
243
+ function assertTarget(target) {
244
+ if (typeof target !== 'string' || target.length === 0 || target.length > MAX_TARGET_LENGTH
245
+ || !TARGET_PATTERN.test(target) || SECRET_PATTERNS.some(pattern => pattern.test(target)) || containsPrivateMonitorPath(target)) {
246
+ throw new Error('invalid or private monitor delivery target');
247
+ }
248
+ return target;
249
+ }
250
+
251
+ function normalizeTargets(targets) {
252
+ if (!Array.isArray(targets) || targets.length === 0 || targets.length > MAX_TARGETS) {
253
+ throw new Error(`monitor delivery targets must contain 1-${MAX_TARGETS} entries`);
254
+ }
255
+ return [...new Set(targets.map(assertTarget))];
256
+ }
257
+
258
+ function assertMonitorId(monitorId) {
259
+ if (typeof monitorId !== 'string' || monitorId.length === 0 || monitorId.length > 128) {
260
+ throw new TypeError('monitorId must be a bounded non-empty string');
261
+ }
262
+ return monitorId;
263
+ }
264
+
265
+ function createMonitorStore(driver) {
266
+ if (!driver || typeof driver.appendMonitorEvent !== 'function') {
267
+ throw new Error('Monitor store requires a monitor-capable SQLite driver');
268
+ }
269
+ return {
270
+ async appendEvent(envelope, targets, config = {}) {
271
+ assertEnvelope(envelope, 'forge.memory.monitor-event.v1');
272
+ return callMonitorDriver(() => driver.appendMonitorEvent(envelope, normalizeTargets(targets), config));
273
+ },
274
+ async recordDeliveryReceipt(envelope, config = {}) {
275
+ assertEnvelope(envelope, 'forge.memory.delivery-receipt.v1');
276
+ assertTarget(envelope.payload.target);
277
+ return callMonitorDriver(() => driver.recordMonitorDeliveryReceipt(envelope, config));
278
+ },
279
+ async recordTerminalReceipt(envelope, config = {}) {
280
+ assertEnvelope(envelope, 'forge.memory.monitor-receipt.v1');
281
+ return callMonitorDriver(() => driver.recordMonitorTerminalReceipt(envelope, config));
282
+ },
283
+ getEvent(eventId, config = {}) {
284
+ if (typeof eventId !== 'string' || !eventId || eventId.length > 255) {
285
+ throw new TypeError('eventId must be a bounded non-empty string');
286
+ }
287
+ return readMonitorDriver(
288
+ () => driver.getMonitorEvent(eventId, config),
289
+ value => normalizeMonitorRow(value, MONITOR_EVENT_ROW_FIELDS),
290
+ );
291
+ },
292
+ readEventTail(monitorId, options = {}, config = {}) {
293
+ return readMonitorDriver(
294
+ () => driver.readMonitorEventTail(assertMonitorId(monitorId), options, config),
295
+ value => normalizeMonitorResult(value, MONITOR_TAIL_FIELDS, {
296
+ events: rows => normalizeMonitorRows(rows, MONITOR_EVENT_ROW_FIELDS),
297
+ }),
298
+ );
299
+ },
300
+ readDeliveryState(monitorId, options = {}, config = {}) {
301
+ return readMonitorDriver(
302
+ () => driver.readMonitorDeliveryState(assertMonitorId(monitorId), options, config),
303
+ value => normalizeMonitorResult(value, MONITOR_DELIVERY_STATE_FIELDS, {
304
+ cursors: rows => normalizeMonitorRows(rows, MONITOR_CURSOR_ROW_FIELDS),
305
+ outbox: rows => normalizeMonitorRows(rows, MONITOR_OUTBOX_ROW_FIELDS),
306
+ terminal_receipt: row => normalizeMonitorRow(row, MONITOR_TERMINAL_ROW_FIELDS),
307
+ }),
308
+ );
309
+ },
310
+ listEvents(monitorId, config = {}) {
311
+ return readMonitorDriver(
312
+ () => driver.listMonitorEvents(assertMonitorId(monitorId), config),
313
+ rows => normalizeMonitorRows(rows, MONITOR_EVENT_ROW_FIELDS),
314
+ );
315
+ },
316
+ };
317
+ }
318
+
319
+ module.exports = {
320
+ ...require('./src/authority-provider'),
321
+ ...require('./src/backend-registry'),
322
+ ...require('./src/feedback-intake'),
323
+ ...require('./src/pr-lifecycle-authority'),
324
+ ...require('./src/usage-evidence'),
325
+ MonitorConflictError,
326
+ MonitorStaleError,
327
+ MonitorStoreError,
328
+ MonitorTerminalError,
329
+ MonitorUnavailableError,
330
+ createMonitorStore,
331
+ };
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@forge/memory",
3
+ "version": "0.1.0-beta.6",
4
+ "description": "Forge Memory public backend boundary",
5
+ "main": "index.js",
6
+ "type": "commonjs",
7
+ "files": [
8
+ "index.js",
9
+ "src",
10
+ "!src/**/*.test.js"
11
+ ],
12
+ "scripts": {
13
+ "test": "bun test"
14
+ },
15
+ "dependencies": {
16
+ "@forge/contracts": "0.1.0-beta.6"
17
+ },
18
+ "engines": {
19
+ "node": ">=22.16.0"
20
+ },
21
+ "license": "MIT",
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/harshanandak/forge.git",
28
+ "directory": "packages/memory"
29
+ },
30
+ "homepage": "https://github.com/harshanandak/forge#readme",
31
+ "bugs": {
32
+ "url": "https://github.com/harshanandak/forge/issues"
33
+ }
34
+ }