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,72 @@
1
+ "use strict";
2
+
3
+ const {
4
+ CONTRACTS,
5
+ EXTENSION_ID_MAX_LENGTH,
6
+ EXTENSION_ID_PATTERN,
7
+ PAYLOAD_FIELDS,
8
+ } = require("./definitions.js");
9
+
10
+ function generateJsonSchema(schemaId) {
11
+ const definition = Object.hasOwn(CONTRACTS, schemaId) ? CONTRACTS[schemaId] : undefined;
12
+ if (!definition) throw new TypeError(`Unsupported schema_id: ${schemaId}`);
13
+ const payloadProperties = {};
14
+ for (const field of [...definition.required, ...definition.optional]) payloadProperties[field] = structuredClone(PAYLOAD_FIELDS[schemaId][field]);
15
+ return {
16
+ $schema: "https://json-schema.org/draft/2020-12/schema",
17
+ $id: schemaId,
18
+ title: definition.name,
19
+ type: "object",
20
+ required: ["schema_id", "schema_version", "object_id", "created_at", "producer", "capabilities_used", "provenance", "content_hash", "payload", "extensions"],
21
+ properties: {
22
+ schema_id: { const: schemaId },
23
+ schema_version: { const: 1 },
24
+ object_id: { type: "string", format: "uuid" },
25
+ created_at: { type: "string", format: "date-time" },
26
+ producer: {
27
+ type: "object",
28
+ required: ["product_id", "product_version", "instance_id"],
29
+ properties: { product_id: { type: "string", minLength: 1 }, product_version: { type: "string", minLength: 1 }, instance_id: { type: "string", minLength: 1 } },
30
+ additionalProperties: false,
31
+ },
32
+ capabilities_used: {
33
+ type: "array",
34
+ items: {
35
+ type: "object",
36
+ required: ["capability_id", "manifest_digest"],
37
+ properties: { capability_id: { type: "string", minLength: 1 }, manifest_digest: { type: "string", pattern: "^[0-9a-f]{64}$" } },
38
+ additionalProperties: false,
39
+ },
40
+ },
41
+ provenance: {
42
+ type: "object",
43
+ required: ["source_kind", "actor_class", "actor_id"],
44
+ properties: { source_kind: { type: "string", minLength: 1 }, actor_class: { type: "string", minLength: 1 }, actor_id: { type: "string", minLength: 1 } },
45
+ additionalProperties: false,
46
+ },
47
+ content_hash: { type: "string", pattern: "^[0-9a-f]{64}$" },
48
+ payload: { type: "object", required: definition.required, properties: payloadProperties, additionalProperties: false },
49
+ extensions: {
50
+ type: "object",
51
+ propertyNames: { pattern: EXTENSION_ID_PATTERN, maxLength: EXTENSION_ID_MAX_LENGTH },
52
+ additionalProperties: {
53
+ type: "object",
54
+ required: ["impact", "schema_version", "value"],
55
+ properties: {
56
+ impact: { const: "advisory" },
57
+ schema_version: { type: "integer", minimum: 1 },
58
+ value: {},
59
+ },
60
+ additionalProperties: false,
61
+ },
62
+ },
63
+ },
64
+ additionalProperties: false,
65
+ };
66
+ }
67
+
68
+ function supportedSchemaVersions(schemaId) {
69
+ return Object.hasOwn(CONTRACTS, schemaId) ? [1] : [];
70
+ }
71
+
72
+ module.exports = { generateJsonSchema, supportedSchemaVersions };
@@ -0,0 +1,305 @@
1
+ "use strict";
2
+
3
+ const { canonicalize, computeContentHash, preflightCanonicalValue } = require("./canonical.js");
4
+ const {
5
+ CONTRACTS,
6
+ EXTENSION_ID_MAX_LENGTH,
7
+ EXTENSION_ID_PATTERN,
8
+ PAYLOAD_FIELDS,
9
+ } = require("./definitions.js");
10
+
11
+ const ENVELOPE_FIELDS = Object.freeze([
12
+ "schema_id", "schema_version", "object_id", "created_at", "producer",
13
+ "capabilities_used", "provenance", "content_hash", "payload", "extensions",
14
+ ]);
15
+ 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;
16
+ const SHA256 = /^[0-9a-f]{64}$/;
17
+ const RFC3339_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
18
+ const LIVE_EVIDENCE_FIELDS = Object.freeze({
19
+ "forge.memory.work-packet.v1": ["issueRevision", "workflowConfigRevision", "capabilityManifestDigest", "exactHead"],
20
+ "forge.memory.context-packet.v1": ["workPacketHash", "privacyScopeHash", "redactionPolicyRevision", "allowedDisclosureClasses"],
21
+ "forge.memory.claim-request.v1": ["issueRevision", "actorId"],
22
+ "forge.memory.lease-receipt.v1": ["issueRevision", "actorId", "leaseEpoch", "observedAt"],
23
+ "forge.memory.capability-manifest.v1": ["providerId", "configRevision", "observedAt"],
24
+ "forge.memory.run-receipt.v1": ["packetHash", "workflowConfigRevision", "capabilityManifestDigest", "exactHead"],
25
+ "forge.memory.feedback-report.v1": ["consentEventId", "redactionPolicyRevision"],
26
+ "forge.memory.structured-error.v1": ["parentObjectHash"],
27
+ "forge.memory.monitor-event.v1": ["monitorId", "subjectRevision"],
28
+ "forge.memory.delivery-receipt.v1": ["eventId", "target"],
29
+ "forge.memory.monitor-receipt.v1": ["monitorId", "ownerRunId"],
30
+ });
31
+ const EXTENSION_ID = new RegExp(EXTENSION_ID_PATTERN);
32
+ const SECRET_PATTERN = /(?:gh[pousr]_[A-Za-z0-9]{20,}|sk_(?:live|test)_[A-Za-z0-9]{16,}|sk-[A-Za-z0-9]{16,}|AKIA[0-9A-Z]{16}|(?:api[_-]?key|token|secret|password)\s*[:=]\s*\S{8,})/i;
33
+ const ABSOLUTE_USER_PATH = /(?:[A-Za-z]:\\Users\\[^\\\s]+|\/(?:Users|home)\/[^/\s]+\/)/i;
34
+ const BOUNDED_LIMITS = Object.freeze({ maxDepth: 8, maxItems: 128, maxProperties: 64, maxBytes: 16_384 });
35
+
36
+ function error(errors, path, code) {
37
+ errors.push({ path, code });
38
+ }
39
+
40
+ function validateEnvelope(value, options = {}) {
41
+ const errors = [];
42
+ try {
43
+ preflightCanonicalValue(value);
44
+ } catch {
45
+ return { ok: false, errors: [{ path: "$", code: "NON_CANONICAL_VALUE" }] };
46
+ }
47
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
48
+ return { ok: false, errors: [{ path: "$", code: "INVALID_ENVELOPE" }] };
49
+ }
50
+ for (const field of ENVELOPE_FIELDS) {
51
+ if (!Object.hasOwn(value, field)) error(errors, `$.${field}`, "MISSING_REQUIRED");
52
+ }
53
+ if (errors.length > 0) return { ok: false, errors };
54
+ if (!Object.hasOwn(CONTRACTS, value.schema_id)) error(errors, "$.schema_id", "UNSUPPORTED_SCHEMA");
55
+ if (value.schema_version !== 1) error(errors, "$.schema_version", "UNSUPPORTED_VERSION");
56
+ if (typeof value.schema_id !== "string" || value.schema_id.length === 0) error(errors, "$.schema_id", "INVALID_TYPE");
57
+ if (!UUID.test(value.object_id)) error(errors, "$.object_id", "INVALID_UUID");
58
+ if (typeof value.created_at !== "string" || !RFC3339_UTC.test(value.created_at) || Number.isNaN(Date.parse(value.created_at))) error(errors, "$.created_at", "INVALID_TIMESTAMP");
59
+ if (!value.producer || typeof value.producer !== "object" || Array.isArray(value.producer)) error(errors, "$.producer", "INVALID_TYPE");
60
+ else for (const field of ["product_id", "product_version", "instance_id"]) if (typeof value.producer[field] !== "string" || value.producer[field].length === 0) error(errors, `$.producer.${field}`, "MISSING_REQUIRED");
61
+ if (!Array.isArray(value.capabilities_used)) error(errors, "$.capabilities_used", "INVALID_TYPE");
62
+ else {
63
+ const keys = value.capabilities_used.map((item) => `${item?.capability_id ?? ""}:${item?.manifest_digest ?? ""}`);
64
+ if (value.capabilities_used.some((item) => !item || typeof item !== "object" || Array.isArray(item) || typeof item.capability_id !== "string" || item.capability_id.length === 0 || !SHA256.test(item.manifest_digest))) {
65
+ error(errors, "$.capabilities_used", "INVALID_CAPABILITY_BINDING");
66
+ }
67
+ if (keys.join("\0") !== [...keys].sort().join("\0")) error(errors, "$.capabilities_used", "NOT_SORTED");
68
+ }
69
+ if (!value.provenance || typeof value.provenance !== "object" || Array.isArray(value.provenance)) error(errors, "$.provenance", "INVALID_TYPE");
70
+ else for (const field of ["source_kind", "actor_class", "actor_id"]) if (typeof value.provenance[field] !== "string" || value.provenance[field].length === 0) error(errors, `$.provenance.${field}`, "MISSING_REQUIRED");
71
+ if (!SHA256.test(value.content_hash)) error(errors, "$.content_hash", "INVALID_CONTENT_HASH");
72
+ if (!value.payload || typeof value.payload !== "object" || Array.isArray(value.payload)) error(errors, "$.payload", "INVALID_TYPE");
73
+ if (!value.extensions || typeof value.extensions !== "object" || Array.isArray(value.extensions)) error(errors, "$.extensions", "INVALID_TYPE");
74
+ if (options.verifyHash !== false && SHA256.test(value.content_hash)) {
75
+ try {
76
+ if (computeContentHash(value) !== value.content_hash) error(errors, "$.content_hash", "CONTENT_HASH_MISMATCH");
77
+ } catch {
78
+ error(errors, "$", "NON_CANONICAL_VALUE");
79
+ }
80
+ }
81
+ return { ok: errors.length === 0, errors };
82
+ }
83
+
84
+ function validateExtensions(extensions, errors) {
85
+ if (!extensions || typeof extensions !== "object" || Array.isArray(extensions)) return;
86
+ for (const [extensionId, extension] of Object.entries(extensions)) {
87
+ const path = `$.extensions[${JSON.stringify(extensionId)}]`;
88
+ if (extensionId.length > EXTENSION_ID_MAX_LENGTH || !EXTENSION_ID.test(extensionId)) error(errors, path, "INVALID_EXTENSION_ID");
89
+ if (!extension || typeof extension !== "object" || Array.isArray(extension)) {
90
+ error(errors, path, "INVALID_EXTENSION");
91
+ continue;
92
+ }
93
+ if (extension.impact === "consequential") error(errors, path, "UNKNOWN_CONSEQUENTIAL_EXTENSION");
94
+ const allowed = new Set(["impact", "schema_version", "value"]);
95
+ const hasExtra = Object.keys(extension).some((field) => !allowed.has(field));
96
+ if (extension.impact !== "advisory" || !Number.isInteger(extension.schema_version) || extension.schema_version < 1 || !Object.hasOwn(extension, "value") || hasExtra) error(errors, path, "INVALID_EXTENSION");
97
+ }
98
+ }
99
+
100
+ function compare(errors, actual, expected, path, code) {
101
+ if (expected !== undefined && actual !== expected) error(errors, path, code);
102
+ }
103
+
104
+ function isType(value, type) {
105
+ if (type === "integer") return Number.isInteger(value);
106
+ if (type === "array") return Array.isArray(value);
107
+ if (type === "object") return Boolean(value) && typeof value === "object" && !Array.isArray(value);
108
+ return typeof value === type;
109
+ }
110
+
111
+ function validateShape(value, shape, path, errors) {
112
+ if (!isType(value, shape.type)) {
113
+ error(errors, path, "INVALID_TYPE");
114
+ return;
115
+ }
116
+ if (shape.enum && !shape.enum.includes(value)) error(errors, path, "INVALID_ENUM");
117
+ if (shape.minLength !== undefined && value.length < shape.minLength) error(errors, path, "INVALID_LENGTH");
118
+ if (shape.maxLength !== undefined && value.length > shape.maxLength) error(errors, path, "BOUNDED_VALUE_TOO_LARGE");
119
+ if (shape.minimum !== undefined && value < shape.minimum) error(errors, path, "OUT_OF_RANGE");
120
+ if (shape.pattern && !new RegExp(shape.pattern).test(value)) error(errors, path, "INVALID_FORMAT");
121
+ if (shape.not?.pattern && new RegExp(shape.not.pattern, "i").test(value)) error(errors, path, "PRIVACY_PATTERN_REJECTED");
122
+ if (shape.format === "date-time" && (!RFC3339_UTC.test(value) || Number.isNaN(Date.parse(value)))) error(errors, path, "INVALID_TIMESTAMP");
123
+ if (shape.type === "array" && shape.maxItems !== undefined && value.length > shape.maxItems) error(errors, path, "BOUNDED_VALUE_TOO_MANY_ITEMS");
124
+ if (shape.type === "array" && shape.items) value.forEach((item, index) => validateShape(item, shape.items, `${path}[${index}]`, errors));
125
+ if (shape.type === "object") {
126
+ if (shape.maxProperties !== undefined && Object.keys(value).length > shape.maxProperties) error(errors, path, "BOUNDED_VALUE_TOO_MANY_PROPERTIES");
127
+ for (const field of shape.required ?? []) {
128
+ if (!Object.hasOwn(value, field)) error(errors, `${path}.${field}`, "MISSING_REQUIRED");
129
+ }
130
+ for (const [field, fieldShape] of Object.entries(shape.properties ?? {})) {
131
+ if (Object.hasOwn(value, field)) validateShape(value[field], fieldShape, `${path}.${field}`, errors);
132
+ }
133
+ if (shape.additionalProperties === false) {
134
+ const allowed = new Set(Object.keys(shape.properties ?? {}));
135
+ for (const field of Object.keys(value)) if (!allowed.has(field)) error(errors, `${path}.${field}`, "UNKNOWN_FIELD");
136
+ }
137
+ }
138
+ }
139
+
140
+ function inspectPrivacyString(value, path, errors) {
141
+ if (SECRET_PATTERN.test(value)) error(errors, path, "PRIVACY_SECRET_PATTERN");
142
+ if (ABSOLUTE_USER_PATH.test(value)) error(errors, path, "PRIVACY_ABSOLUTE_PATH");
143
+ }
144
+
145
+ function inspectBoundedValue(value, path, errors, depth = 0) {
146
+ if (depth > BOUNDED_LIMITS.maxDepth) {
147
+ error(errors, path, "BOUNDED_VALUE_TOO_DEEP");
148
+ return;
149
+ }
150
+ if (typeof value === "string") {
151
+ inspectPrivacyString(value, path, errors);
152
+ return;
153
+ }
154
+ if (Array.isArray(value)) {
155
+ if (value.length > BOUNDED_LIMITS.maxItems) error(errors, path, "BOUNDED_VALUE_TOO_MANY_ITEMS");
156
+ value.forEach((item, index) => inspectBoundedValue(item, `${path}[${index}]`, errors, depth + 1));
157
+ return;
158
+ }
159
+ if (!value || typeof value !== "object") return;
160
+ const entries = Object.entries(value);
161
+ if (entries.length > BOUNDED_LIMITS.maxProperties) error(errors, path, "BOUNDED_VALUE_TOO_MANY_PROPERTIES");
162
+ entries.forEach(([field, item]) => inspectBoundedValue(item, `${path}.${field}`, errors, depth + 1));
163
+ }
164
+
165
+ function validateBoundedPrivacy(value, errors) {
166
+ const targets = [];
167
+ if (value?.schema_id === "forge.memory.feedback-report.v1") {
168
+ targets.push([value.payload?.redacted_reproduction_steps, "$.payload.redacted_reproduction_steps"]);
169
+ if (value.payload?.proposed_fix !== undefined) targets.push([value.payload.proposed_fix, "$.payload.proposed_fix"]);
170
+ if (value.payload?.return_channel !== undefined) targets.push([value.payload.return_channel, "$.payload.return_channel"]);
171
+ } else if (value?.schema_id === "forge.memory.context-packet.v1") {
172
+ if (value.payload?.references !== undefined) targets.push([value.payload.references, "$.payload.references"]);
173
+ if (value.payload?.summaries !== undefined) targets.push([value.payload.summaries, "$.payload.summaries"]);
174
+ } else if (value?.schema_id === "forge.memory.monitor-event.v1" && value.payload?.bounded_payload !== undefined) {
175
+ targets.push([value.payload.bounded_payload, "$.payload.bounded_payload"]);
176
+ } else if (value?.schema_id === "forge.memory.monitor-receipt.v1") {
177
+ targets.push([value.payload?.process_cleanup, "$.payload.process_cleanup"], [value.payload?.lease_cleanup, "$.payload.lease_cleanup"]);
178
+ } else if (value?.schema_id === "forge.memory.structured-error.v1") {
179
+ targets.push([value.payload?.safe_details, "$.payload.safe_details"]);
180
+ } else if (value?.schema_id === "forge.memory.run-receipt.v1") {
181
+ targets.push(
182
+ [value.payload?.validation, "$.payload.validation"],
183
+ [value.payload?.cleanup, "$.payload.cleanup"],
184
+ );
185
+ if (value.payload?.structured_error !== undefined) targets.push([value.payload.structured_error, "$.payload.structured_error"]);
186
+ }
187
+ for (const [target, path] of targets) {
188
+ if (target === undefined) continue;
189
+ inspectBoundedValue(target, path, errors);
190
+ try {
191
+ canonicalize(target, { maxDepth: 64, maxNodes: 100_000, maxBytes: BOUNDED_LIMITS.maxBytes });
192
+ } catch (failure) {
193
+ if (failure.code === "CANONICAL_BYTE_LIMIT") error(errors, path, "BOUNDED_VALUE_TOO_LARGE");
194
+ }
195
+ }
196
+ }
197
+
198
+ function isStale(expiresAt, observedAt) {
199
+ const expires = Date.parse(expiresAt);
200
+ const observed = Date.parse(observedAt);
201
+ return Number.isNaN(expires) || Number.isNaN(observed) || expires <= observed;
202
+ }
203
+
204
+ function validateConsequentialEvidence(value, expected, errors) {
205
+ if (!value?.payload) return;
206
+ if (value.schema_id === "forge.memory.run-receipt.v1" && value.payload.status === "NOT_EXECUTED") {
207
+ error(errors, "$.payload.status", "NOT_EXECUTED_NO_TRANSITION");
208
+ }
209
+ const requiredEvidence = LIVE_EVIDENCE_FIELDS[value.schema_id] ?? [];
210
+ for (const field of requiredEvidence) {
211
+ if (!expected || !Object.hasOwn(expected, field)) error(errors, `$.live_expected.${field}`, "MISSING_LIVE_EVIDENCE");
212
+ }
213
+ if (!expected) return;
214
+ const payload = value.payload;
215
+ if (value.schema_id === "forge.memory.work-packet.v1") {
216
+ compare(errors, payload.expected_issue_revision, expected.issueRevision, "$.payload.expected_issue_revision", "STALE_ISSUE_REVISION");
217
+ compare(errors, payload.workflow_config_revision, expected.workflowConfigRevision, "$.payload.workflow_config_revision", "STALE_WORKFLOW_CONFIG");
218
+ compare(errors, payload.capability_manifest_digest, expected.capabilityManifestDigest, "$.payload.capability_manifest_digest", "WRONG_CAPABILITY_DIGEST");
219
+ compare(errors, payload.target_head, expected.exactHead, "$.payload.target_head", "STALE_EXACT_HEAD");
220
+ } else if (value.schema_id === "forge.memory.context-packet.v1") {
221
+ compare(errors, payload.work_packet_hash, expected.workPacketHash, "$.payload.work_packet_hash", "STALE_WORK_PACKET");
222
+ compare(errors, payload.privacy_scope_hash, expected.privacyScopeHash, "$.payload.privacy_scope_hash", "STALE_PRIVACY_SCOPE");
223
+ compare(errors, payload.redaction_policy_revision, expected.redactionPolicyRevision, "$.payload.redaction_policy_revision", "STALE_REDACTION_POLICY");
224
+ if (expected.allowedDisclosureClasses && !expected.allowedDisclosureClasses.includes(payload.disclosure_class)) {
225
+ error(errors, "$.payload.disclosure_class", "DISCLOSURE_NOT_ALLOWED");
226
+ }
227
+ } else if (value.schema_id === "forge.memory.run-receipt.v1") {
228
+ compare(errors, payload.packet_hash, expected.packetHash, "$.payload.packet_hash", "STALE_WORK_PACKET");
229
+ compare(errors, payload.workflow_config_revision, expected.workflowConfigRevision, "$.payload.workflow_config_revision", "STALE_WORKFLOW_CONFIG");
230
+ compare(errors, payload.manifest_digest, expected.capabilityManifestDigest, "$.payload.manifest_digest", "WRONG_CAPABILITY_DIGEST");
231
+ compare(errors, payload.exact_head, expected.exactHead, "$.payload.exact_head", "STALE_EXACT_HEAD");
232
+ compare(errors, payload.lease_epoch, expected.leaseEpoch, "$.payload.lease_epoch", "STALE_LEASE_EPOCH");
233
+ } else if (value.schema_id === "forge.memory.claim-request.v1") {
234
+ compare(errors, payload.expected_issue_revision, expected.issueRevision, "$.payload.expected_issue_revision", "STALE_ISSUE_REVISION");
235
+ compare(errors, payload.actor_id, expected.actorId, "$.payload.actor_id", "STALE_ACTOR");
236
+ } else if (value.schema_id === "forge.memory.lease-receipt.v1") {
237
+ compare(errors, payload.issue_revision, expected.issueRevision, "$.payload.issue_revision", "STALE_ISSUE_REVISION");
238
+ compare(errors, payload.actor_id, expected.actorId, "$.payload.actor_id", "STALE_ACTOR");
239
+ compare(errors, payload.lease_epoch, expected.leaseEpoch, "$.payload.lease_epoch", "STALE_LEASE_EPOCH");
240
+ if (isStale(payload.expires_at, expected.observedAt)) error(errors, "$.payload.expires_at", "STALE_LEASE");
241
+ } else if (value.schema_id === "forge.memory.capability-manifest.v1") {
242
+ compare(errors, payload.provider_id, expected.providerId, "$.payload.provider_id", "STALE_PROVIDER");
243
+ compare(errors, payload.config_revision, expected.configRevision, "$.payload.config_revision", "STALE_CAPABILITY_CONFIG");
244
+ if (isStale(payload.expires_at, expected.observedAt)) error(errors, "$.payload.expires_at", "STALE_CAPABILITY_MANIFEST");
245
+ } else if (value.schema_id === "forge.memory.feedback-report.v1") {
246
+ compare(errors, payload.consent_event_id, expected.consentEventId, "$.payload.consent_event_id", "STALE_CONSENT");
247
+ compare(errors, payload.redaction_policy_revision, expected.redactionPolicyRevision, "$.payload.redaction_policy_revision", "STALE_REDACTION_POLICY");
248
+ } else if (value.schema_id === "forge.memory.structured-error.v1") {
249
+ compare(errors, payload.parent_object_hash, expected.parentObjectHash, "$.payload.parent_object_hash", "STALE_PARENT_OBJECT");
250
+ } else if (value.schema_id === "forge.memory.monitor-event.v1") {
251
+ compare(errors, payload.monitor_id, expected.monitorId, "$.payload.monitor_id", "STALE_MONITOR");
252
+ compare(errors, payload.subject_revision, expected.subjectRevision, "$.payload.subject_revision", "STALE_SUBJECT");
253
+ } else if (value.schema_id === "forge.memory.delivery-receipt.v1") {
254
+ compare(errors, payload.event_id, expected.eventId, "$.payload.event_id", "STALE_EVENT");
255
+ compare(errors, payload.target, expected.target, "$.payload.target", "STALE_DELIVERY_TARGET");
256
+ } else if (value.schema_id === "forge.memory.monitor-receipt.v1") {
257
+ compare(errors, payload.monitor_id, expected.monitorId, "$.payload.monitor_id", "STALE_MONITOR");
258
+ compare(errors, payload.owner_run_id, expected.ownerRunId, "$.payload.owner_run_id", "STALE_OWNER_RUN");
259
+ }
260
+ }
261
+
262
+ function validateContractStructure(value, options = {}) {
263
+ const envelope = validateEnvelope(value, options);
264
+ if (envelope.errors.some((item) => item.code === "NON_CANONICAL_VALUE")) return envelope;
265
+ const errors = [...envelope.errors];
266
+ const definition = Object.hasOwn(CONTRACTS, value?.schema_id) ? CONTRACTS[value.schema_id] : undefined;
267
+ if (definition && value?.payload && typeof value.payload === "object" && !Array.isArray(value.payload)) {
268
+ for (const field of definition.required) {
269
+ if (!Object.hasOwn(value.payload, field)) error(errors, `$.payload.${field}`, "MISSING_REQUIRED");
270
+ }
271
+ const allowed = new Set([...definition.required, ...definition.optional]);
272
+ for (const field of Object.keys(value.payload)) {
273
+ if (!allowed.has(field)) error(errors, `$.payload.${field}`, "UNKNOWN_PAYLOAD_FIELD");
274
+ else validateShape(value.payload[field], PAYLOAD_FIELDS[value.schema_id][field], `$.payload.${field}`, errors);
275
+ }
276
+ }
277
+ validateExtensions(value?.extensions, errors);
278
+ if (!errors.some((item) => item.code === "NON_CANONICAL_VALUE")) validateBoundedPrivacy(value, errors);
279
+ return { ok: errors.length === 0, errors };
280
+ }
281
+
282
+ function validateContract(value, options = {}) {
283
+ const structural = validateContractStructure(value, options);
284
+ if (structural.errors.some((item) => item.code === "NON_CANONICAL_VALUE")) return structural;
285
+ const errors = [...structural.errors];
286
+ validateConsequentialEvidence(value, options.expected, errors);
287
+ return { ok: errors.length === 0, errors };
288
+ }
289
+
290
+ class ContractValidationError extends Error {
291
+ constructor(errors) {
292
+ super("Contract validation failed");
293
+ this.name = "ContractValidationError";
294
+ this.errors = errors;
295
+ }
296
+ }
297
+
298
+ function parseContract(json, options = {}) {
299
+ const value = JSON.parse(json);
300
+ const result = validateContract(value, options);
301
+ if (!result.ok) throw new ContractValidationError(result.errors);
302
+ return value;
303
+ }
304
+
305
+ module.exports = { ContractValidationError, ENVELOPE_FIELDS, parseContract, validateContract, validateContractStructure, validateEnvelope };
package/scripts/test.js CHANGED
@@ -569,7 +569,7 @@ async function runTestExecutionPlan(plan, deps = {}) {
569
569
  if (status !== 0) return status;
570
570
  }
571
571
 
572
- if (plan.runE2E) {
572
+ if (!plan.runFullSuite && plan.runE2E) {
573
573
  console.log(' Extra: running affected e2e tests');
574
574
  const status = await runCommand(bunCommand, ['test', '--timeout', '15000', 'test/e2e/'], laneOptions, spawn);
575
575
  if (signal) return signalExitCode(signal);