@agilno-tech/rivet 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +6 -0
  3. package/README.md +59 -0
  4. package/bin/cli.js +42 -0
  5. package/dist/agents/backend-django-agent/SKILL.md +35 -0
  6. package/dist/agents/backend-nestjs-agent/SKILL.md +36 -0
  7. package/dist/agents/boss-agent/SKILL.md +28 -0
  8. package/dist/agents/bounded-worker/SKILL.md +28 -0
  9. package/dist/agents/data-performance-agent/SKILL.md +32 -0
  10. package/dist/agents/delivery-ticketing-agent/SKILL.md +36 -0
  11. package/dist/agents/devops-agent/SKILL.md +34 -0
  12. package/dist/agents/engineering-manager/SKILL.md +28 -0
  13. package/dist/agents/frontend-nextjs-agent/SKILL.md +34 -0
  14. package/dist/agents/frontend-web-agent/SKILL.md +31 -0
  15. package/dist/agents/incident-response-agent/SKILL.md +42 -0
  16. package/dist/agents/jest-agent/SKILL.md +77 -0
  17. package/dist/agents/mobile-agent/SKILL.md +32 -0
  18. package/dist/agents/playwright-agent/SKILL.md +54 -0
  19. package/dist/agents/product-design-manager/SKILL.md +27 -0
  20. package/dist/agents/qa-agent/SKILL.md +29 -0
  21. package/dist/agents/quality-manager/SKILL.md +28 -0
  22. package/dist/agents/vitest-agent/SKILL.md +77 -0
  23. package/dist/governance/pre-push-rules.md +75 -0
  24. package/dist/governance/prompt-hygiene.md +22 -0
  25. package/dist/governance/review-checklist.md +24 -0
  26. package/dist/governance/safety-and-data.md +27 -0
  27. package/dist/governance/usage-rules.md +21 -0
  28. package/dist/mandatory/address-pr-feedback/SKILL.md +196 -0
  29. package/dist/mandatory/agentic-goal/SKILL.md +42 -0
  30. package/dist/mandatory/agentic-status/SKILL.md +74 -0
  31. package/dist/mandatory/apply-design/SKILL.md +228 -0
  32. package/dist/mandatory/check-ac/SKILL.md +124 -0
  33. package/dist/mandatory/create-pr-and-commit/SKILL.md +304 -0
  34. package/dist/mandatory/design/SKILL.md +312 -0
  35. package/dist/mandatory/feature-workflow/SKILL.md +87 -0
  36. package/dist/mandatory/hotfix/SKILL.md +195 -0
  37. package/dist/mandatory/pre-push/SKILL.md +272 -0
  38. package/dist/mandatory/project-context/SKILL.md +180 -0
  39. package/dist/mandatory/release-docs/SKILL.md +191 -0
  40. package/dist/mandatory/review-pr/SKILL.md +323 -0
  41. package/dist/mandatory/security-review/SKILL.md +157 -0
  42. package/dist/skills/api-contract/SKILL.md +77 -0
  43. package/dist/skills/backend-django/SKILL.md +27 -0
  44. package/dist/skills/backend-nestjs/SKILL.md +65 -0
  45. package/dist/skills/bug-ticket-creation/SKILL.md +43 -0
  46. package/dist/skills/cloudwatch-troubleshooting/SKILL.md +73 -0
  47. package/dist/skills/database-migration/SKILL.md +97 -0
  48. package/dist/skills/debugging/SKILL.md +32 -0
  49. package/dist/skills/devops-infra/SKILL.md +26 -0
  50. package/dist/skills/documentation/SKILL.md +24 -0
  51. package/dist/skills/frontend-nextjs/SKILL.md +27 -0
  52. package/dist/skills/incident-postmortem/SKILL.md +38 -0
  53. package/dist/skills/kubernetes-troubleshooting/SKILL.md +71 -0
  54. package/dist/skills/mobile-react-native/SKILL.md +37 -0
  55. package/dist/skills/postgres-analytics/SKILL.md +29 -0
  56. package/dist/skills/product-jira-ticketing/SKILL.md +28 -0
  57. package/dist/skills/qa-bug-analysis/SKILL.md +27 -0
  58. package/dist/skills/refactoring/SKILL.md +32 -0
  59. package/dist/skills/sprint-planning/SKILL.md +32 -0
  60. package/dist/skills/testing-quality/SKILL.md +62 -0
  61. package/dist/v2/protocols/agent-orchestration.md +49 -0
  62. package/dist/v2/protocols/delivery.md +45 -0
  63. package/dist/v2/protocols/design-authority.md +29 -0
  64. package/dist/v2/protocols/goal-graph.md +44 -0
  65. package/dist/v2/protocols/pattern-first-development.md +29 -0
  66. package/dist/v2/protocols/qa-evidence.md +29 -0
  67. package/dist/v2/protocols/security.md +29 -0
  68. package/dist/v2/schemas/event.schema.json +86 -0
  69. package/dist/v2/schemas/evidence.schema.json +94 -0
  70. package/dist/v2/schemas/feature-decomposition.schema.json +48 -0
  71. package/dist/v2/schemas/feature-plan.schema.json +95 -0
  72. package/dist/v2/schemas/goal-graph.schema.json +61 -0
  73. package/dist/v2/schemas/integration.schema.json +119 -0
  74. package/dist/v2/schemas/orchestration.schema.json +69 -0
  75. package/dist/v2/schemas/project.schema.json +209 -0
  76. package/dist/v2/schemas/providers.schema.json +142 -0
  77. package/dist/v2/schemas/quality.schema.json +50 -0
  78. package/dist/v2/schemas/work-action.schema.json +44 -0
  79. package/dist/v2/schemas/work-request.schema.json +253 -0
  80. package/dist/v2/templates/evidence/qa-bundle.json +194 -0
  81. package/dist/v2/templates/github-actions/rivet-deploy.yml +98 -0
  82. package/dist/v2/templates/harness/SKILL.md +90 -0
  83. package/dist/v2/templates/project/.rivet/orchestration.yaml +42 -0
  84. package/dist/v2/templates/project/.rivet/project.yaml +14 -0
  85. package/dist/v2/templates/project/.rivet/providers.yaml +8 -0
  86. package/dist/v2/templates/project/.rivet/quality.yaml +16 -0
  87. package/package.json +58 -0
  88. package/protocols/agent-orchestration.md +49 -0
  89. package/protocols/delivery.md +45 -0
  90. package/protocols/design-authority.md +29 -0
  91. package/protocols/goal-graph.md +44 -0
  92. package/protocols/pattern-first-development.md +29 -0
  93. package/protocols/qa-evidence.md +29 -0
  94. package/protocols/security.md +29 -0
  95. package/schemas/event.schema.json +86 -0
  96. package/schemas/evidence.schema.json +94 -0
  97. package/schemas/feature-decomposition.schema.json +48 -0
  98. package/schemas/feature-plan.schema.json +95 -0
  99. package/schemas/goal-graph.schema.json +61 -0
  100. package/schemas/integration.schema.json +119 -0
  101. package/schemas/orchestration.schema.json +69 -0
  102. package/schemas/project.schema.json +209 -0
  103. package/schemas/providers.schema.json +142 -0
  104. package/schemas/quality.schema.json +50 -0
  105. package/schemas/work-action.schema.json +44 -0
  106. package/schemas/work-request.schema.json +253 -0
  107. package/src/adapters/confluence.js +138 -0
  108. package/src/adapters/contract.js +657 -0
  109. package/src/adapters/factory.js +122 -0
  110. package/src/adapters/figma.js +178 -0
  111. package/src/adapters/fixtures.js +183 -0
  112. package/src/adapters/github.js +314 -0
  113. package/src/adapters/http.js +704 -0
  114. package/src/adapters/jira.js +195 -0
  115. package/src/adapters/linear.js +171 -0
  116. package/src/adapters/node-transport.js +59 -0
  117. package/src/cli/integration-setup-prompt.js +32 -0
  118. package/src/cli/interrupt.js +25 -0
  119. package/src/cli/main.js +596 -0
  120. package/src/cli/output.js +174 -0
  121. package/src/cli/parse-args.js +246 -0
  122. package/src/cli/project-discovery.js +70 -0
  123. package/src/cli/task-confirmation.js +38 -0
  124. package/src/cli/task-presentation.js +22 -0
  125. package/src/clients/claude.js +214 -0
  126. package/src/clients/codex.js +180 -0
  127. package/src/clients/compatibility.js +23 -0
  128. package/src/clients/contract.js +224 -0
  129. package/src/clients/fake.js +170 -0
  130. package/src/clients/process-runner.js +657 -0
  131. package/src/clients/result-contract.js +78 -0
  132. package/src/commands/delivery-publish.js +80 -0
  133. package/src/commands/delivery-remote.js +230 -0
  134. package/src/commands/delivery-review-update.js +39 -0
  135. package/src/commands/delivery-tracker.js +94 -0
  136. package/src/commands/delivery-transition.js +79 -0
  137. package/src/commands/delivery.js +223 -0
  138. package/src/commands/dependency-approval.js +21 -0
  139. package/src/commands/doctor.js +191 -0
  140. package/src/commands/evidence.js +44 -0
  141. package/src/commands/feature.js +212 -0
  142. package/src/commands/goals.js +231 -0
  143. package/src/commands/human-run.js +208 -0
  144. package/src/commands/human-task.js +278 -0
  145. package/src/commands/init.js +848 -0
  146. package/src/commands/install.js +814 -0
  147. package/src/commands/integration-setup.js +65 -0
  148. package/src/commands/integrations.js +54 -0
  149. package/src/commands/models.js +158 -0
  150. package/src/commands/orchestrate.js +61 -0
  151. package/src/commands/preflight.js +125 -0
  152. package/src/commands/protocols.js +228 -0
  153. package/src/commands/repositories.js +77 -0
  154. package/src/commands/setup-remote.js +41 -0
  155. package/src/commands/setup.js +214 -0
  156. package/src/commands/status.js +224 -0
  157. package/src/commands/support.js +44 -0
  158. package/src/commands/task-completion.js +104 -0
  159. package/src/commands/uninstall.js +234 -0
  160. package/src/commands/verify.js +171 -0
  161. package/src/commands/work.js +220 -0
  162. package/src/commands/worktrees.js +52 -0
  163. package/src/config/command-readiness.js +218 -0
  164. package/src/config/commands.js +206 -0
  165. package/src/config/defaults.js +55 -0
  166. package/src/config/load.js +230 -0
  167. package/src/config/validate.js +560 -0
  168. package/src/delivery/bitbucket-review.js +123 -0
  169. package/src/delivery/branch-publication.js +23 -0
  170. package/src/delivery/contract.js +379 -0
  171. package/src/delivery/github-deployment.js +132 -0
  172. package/src/delivery/github-review.js +5 -0
  173. package/src/delivery/github.js +382 -0
  174. package/src/delivery/gitlab-review.js +5 -0
  175. package/src/delivery/gitlab.js +395 -0
  176. package/src/delivery/prepare.js +90 -0
  177. package/src/delivery/publication-process.js +33 -0
  178. package/src/delivery/publication-transport.js +112 -0
  179. package/src/delivery/review-request.js +184 -0
  180. package/src/delivery/review-update.js +74 -0
  181. package/src/delivery/service.js +400 -0
  182. package/src/delivery/store.js +76 -0
  183. package/src/delivery/tracker-target.js +51 -0
  184. package/src/delivery/tracker-transition.js +158 -0
  185. package/src/delivery/tracker.js +164 -0
  186. package/src/discovery/git.js +85 -0
  187. package/src/discovery/portable.js +108 -0
  188. package/src/discovery/project.js +345 -0
  189. package/src/discovery/tools.js +145 -0
  190. package/src/evaluations/approval.js +20 -0
  191. package/src/evaluations/cost-policy.js +20 -0
  192. package/src/evaluations/harness-attempt.js +113 -0
  193. package/src/evaluations/live-runner.js +55 -0
  194. package/src/evaluations/profile.js +28 -0
  195. package/src/evaluations/report.js +11 -0
  196. package/src/evaluations/text-attempt.js +18 -0
  197. package/src/evidence/checksum.js +87 -0
  198. package/src/evidence/collect.js +727 -0
  199. package/src/evidence/validate.js +354 -0
  200. package/src/feature/accepted-integration.js +46 -0
  201. package/src/feature/actions.js +37 -0
  202. package/src/feature/client-profile.js +56 -0
  203. package/src/feature/decomposition-contract.js +58 -0
  204. package/src/feature/host-execution.js +633 -0
  205. package/src/feature/host-lock-recovery.js +96 -0
  206. package/src/feature/host-run-lock.js +32 -0
  207. package/src/feature/local-approval.js +183 -0
  208. package/src/feature/plan-contract.js +215 -0
  209. package/src/feature/planner.js +253 -0
  210. package/src/feature/run-store.js +241 -0
  211. package/src/feature/runtime-bridge.js +840 -0
  212. package/src/feature/verification-report.js +168 -0
  213. package/src/feature/workflow.js +375 -0
  214. package/src/git/client.js +509 -0
  215. package/src/git/integration-worktree.js +153 -0
  216. package/src/git/reconcile.js +222 -0
  217. package/src/git/reservations.js +450 -0
  218. package/src/git/worktrees.js +478 -0
  219. package/src/graph/completion.js +189 -0
  220. package/src/graph/fixtures.js +116 -0
  221. package/src/graph/reducer.js +124 -0
  222. package/src/graph/scheduler.js +252 -0
  223. package/src/graph/validate.js +106 -0
  224. package/src/install/managed.js +579 -0
  225. package/src/install/project-reference.js +33 -0
  226. package/src/install/project-runtime.js +142 -0
  227. package/src/install/runtime-integrity.cjs +141 -0
  228. package/src/integrations/capabilities.js +69 -0
  229. package/src/integrations/host-observation.js +26 -0
  230. package/src/integrations/registry.js +56 -0
  231. package/src/models/delegate.js +102 -0
  232. package/src/models/profiles.js +53 -0
  233. package/src/models/protocols.js +208 -0
  234. package/src/models/registry.js +83 -0
  235. package/src/models/transport.js +59 -0
  236. package/src/policy/approvals.js +251 -0
  237. package/src/policy/authority.js +282 -0
  238. package/src/policy/budget.js +149 -0
  239. package/src/policy/command-bootstrap.js +219 -0
  240. package/src/policy/commands.js +803 -0
  241. package/src/prompts/launch-contract.js +43 -0
  242. package/src/prompts/planning-contract.js +92 -0
  243. package/src/protocols/presentation.js +53 -0
  244. package/src/protocols/project.js +289 -0
  245. package/src/quality/runner.js +497 -0
  246. package/src/quality/traceability.js +215 -0
  247. package/src/repositories/identity.js +59 -0
  248. package/src/repositories/index.js +2 -0
  249. package/src/repositories/provider.js +210 -0
  250. package/src/runtime/application.js +353 -0
  251. package/src/runtime/dependency-directory.js +33 -0
  252. package/src/runtime/harness-discovery.js +121 -0
  253. package/src/runtime/heartbeat.js +47 -0
  254. package/src/runtime/instance-store.js +112 -0
  255. package/src/runtime/orchestrator.js +1133 -0
  256. package/src/runtime/portable-dependencies.js +94 -0
  257. package/src/runtime/recovery.js +190 -0
  258. package/src/runtime/retry.js +54 -0
  259. package/src/runtime/supervisor.js +166 -0
  260. package/src/runtime/worktree-bootstrap.js +169 -0
  261. package/src/state/event-store.js +333 -0
  262. package/src/state/lock.js +266 -0
  263. package/src/state/paths.js +274 -0
  264. package/src/state/redact.js +106 -0
  265. package/src/state/snapshot-store.js +310 -0
  266. package/src/status/public/app.js +96 -0
  267. package/src/status/public/index.html +37 -0
  268. package/src/status/public/styles.css +50 -0
  269. package/src/status/server.js +323 -0
  270. package/src/status/view-model.js +356 -0
  271. package/src/support/bundle.js +195 -0
  272. package/src/support/failure-report.js +96 -0
  273. package/src/work-request/contract.js +250 -0
  274. package/src/work-request/host.js +78 -0
  275. package/src/work-request/local.js +163 -0
  276. package/src/work-request/tracker.js +105 -0
  277. package/templates/evidence/qa-bundle.json +194 -0
  278. package/templates/github-actions/rivet-deploy.yml +98 -0
  279. package/templates/harness/SKILL.md +90 -0
  280. package/templates/project/.rivet/orchestration.yaml +42 -0
  281. package/templates/project/.rivet/project.yaml +14 -0
  282. package/templates/project/.rivet/providers.yaml +8 -0
  283. package/templates/project/.rivet/quality.yaml +16 -0
@@ -0,0 +1,657 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import { claimApproval } from '../policy/approvals.js';
4
+ import { redactSecrets } from '../state/redact.js';
5
+
6
+ const PROVIDER = /^[a-z][a-z0-9-]{0,31}$/;
7
+ const CAPABILITY = /^[a-z][a-z0-9-]{0,63}$/;
8
+ const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:#/-]{0,255}$/;
9
+ const IDEMPOTENCY = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
10
+ const adapters = new WeakSet();
11
+ const governances = new WeakSet();
12
+ const idempotencyRegistries = new WeakMap();
13
+ const wireBodies = new WeakSet();
14
+ const conditionalCapabilities = new WeakMap();
15
+ const conditionalOperationResults = new WeakMap();
16
+
17
+ const MESSAGES = Object.freeze({
18
+ 'invalid-config': 'Provider adapter configuration is invalid.',
19
+ 'invalid-request': 'Provider request is invalid.',
20
+ 'invalid-envelope': 'Provider source envelope is invalid.',
21
+ 'invalid-adapter': 'Provider adapter contract is invalid.',
22
+ 'read-only': 'Provider adapter is read-only.',
23
+ 'remote': 'Provider rejected the request.',
24
+ 'transport': 'Provider transport failed.',
25
+ timeout: 'Provider request timed out.',
26
+ aborted: 'Provider request was cancelled.',
27
+ 'response-too-large': 'Provider response exceeded its safe limit.',
28
+ 'pagination-limit': 'Provider pagination exceeded its safe limit.',
29
+ 'state-conflict': 'Provider state changed before mutation.',
30
+ 'approval-required': 'A matching human approval is required.',
31
+ 'idempotency-reused': 'Provider idempotency key was already used.',
32
+ 'mutation-ambiguous': 'Provider mutation outcome is ambiguous.',
33
+ 'dns-unsafe': 'Provider hostname resolution is unsafe.',
34
+ });
35
+
36
+ export class ProviderAdapterError extends Error {
37
+ constructor(reason = 'invalid-request', options = {}) {
38
+ super(MESSAGES[reason] ?? MESSAGES['invalid-request']);
39
+ this.name = 'ProviderAdapterError';
40
+ this.code = `ERR_PROVIDER_${reason.replaceAll('-', '_').toUpperCase()}`;
41
+ this.safeMessage = this.message;
42
+ this.provider = typeof options.provider === 'string' && PROVIDER.test(options.provider) ? options.provider : undefined;
43
+ this.status = Number.isInteger(options.status) && options.status >= 100 && options.status <= 599 ? options.status : undefined;
44
+ this.retryClassification = ['none', 'transient', 'permanent', 'ambiguous'].includes(options.retryClassification)
45
+ ? options.retryClassification : 'permanent';
46
+ this.details = Object.freeze({ reason });
47
+ }
48
+ }
49
+
50
+ export function failProvider(reason, options) { throw new ProviderAdapterError(reason, options); }
51
+
52
+ export function captureRecord(input, allowed, required, reason = 'invalid-request') {
53
+ let array;
54
+ try { array = Array.isArray(input); } catch { failProvider(reason); }
55
+ if (!input || typeof input !== 'object' || array) failProvider(reason);
56
+ let keys;
57
+ try { keys = Reflect.ownKeys(input); } catch { failProvider(reason); }
58
+ if (keys.length > allowed.size || keys.some(key => typeof key !== 'string' || !allowed.has(key))) failProvider(reason);
59
+ const result = Object.create(null);
60
+ for (const key of keys) {
61
+ let descriptor;
62
+ let entry;
63
+ try {
64
+ descriptor = Object.getOwnPropertyDescriptor(input, key);
65
+ entry = input[key];
66
+ } catch { failProvider(reason); }
67
+ if (!descriptor?.enumerable) failProvider(reason);
68
+ result[key] = entry;
69
+ }
70
+ if (required.some(key => !Object.hasOwn(result, key))) failProvider(reason);
71
+ return result;
72
+ }
73
+
74
+ export function boundedString(value, maximum = 1024, pattern, reason = 'invalid-request') {
75
+ if (typeof value !== 'string' || value.length === 0 || value.length > maximum
76
+ || value.normalize('NFKC') !== value || /[\u0000-\u001f\u007f]/.test(value)
77
+ || (pattern && !pattern.test(value))) failProvider(reason);
78
+ return value;
79
+ }
80
+
81
+ export function boundedArray(input, maximum, convert, reason = 'invalid-request') {
82
+ let array;
83
+ let prototype;
84
+ let length;
85
+ try {
86
+ array = Array.isArray(input);
87
+ prototype = Object.getPrototypeOf(input);
88
+ length = input.length;
89
+ } catch { failProvider(reason); }
90
+ if (!array || prototype !== Array.prototype || !Number.isSafeInteger(length) || length > maximum) failProvider(reason);
91
+ const result = [];
92
+ for (let index = 0; index < length; index += 1) {
93
+ let descriptor;
94
+ let entry;
95
+ try {
96
+ descriptor = Object.getOwnPropertyDescriptor(input, String(index));
97
+ entry = input[index];
98
+ } catch { failProvider(reason); }
99
+ if (!descriptor?.enumerable) failProvider(reason);
100
+ try { result.push(convert(entry, index)); } catch { failProvider(reason); }
101
+ }
102
+ return Object.freeze(result);
103
+ }
104
+
105
+ function cloneJson(input, reason = 'invalid-request') {
106
+ const active = new WeakSet();
107
+ let nodes = 0;
108
+ let stringBytes = 0;
109
+ function clone(value, depth) {
110
+ if (depth > 20) failProvider(reason);
111
+ nodes += 1;
112
+ if (nodes > 20_000) failProvider(reason);
113
+ if (value === null || typeof value === 'boolean') return value;
114
+ if (typeof value === 'string') {
115
+ stringBytes += Buffer.byteLength(value);
116
+ if (value.length > 1_000_000 || stringBytes > 4 * 1024 * 1024 || value.includes('\0')) failProvider(reason);
117
+ return value;
118
+ }
119
+ if (typeof value === 'number') {
120
+ if (!Number.isFinite(value)) failProvider(reason);
121
+ return value;
122
+ }
123
+ if (!value || typeof value !== 'object' || active.has(value)) failProvider(reason);
124
+ active.add(value);
125
+ try {
126
+ if (Array.isArray(value)) {
127
+ if (Object.getPrototypeOf(value) !== Array.prototype || value.length > 10_000) failProvider(reason);
128
+ const result = [];
129
+ for (let index = 0; index < value.length; index += 1) {
130
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
131
+ if (!descriptor?.enumerable) failProvider(reason);
132
+ result.push(clone(value[index], depth + 1));
133
+ }
134
+ return Object.freeze(result);
135
+ }
136
+ const prototype = Object.getPrototypeOf(value);
137
+ if (prototype !== Object.prototype && prototype !== null) failProvider(reason);
138
+ const keys = Reflect.ownKeys(value);
139
+ if (keys.length > 10_000 || keys.some(key => typeof key !== 'string' || key.length > 256)) failProvider(reason);
140
+ const result = {};
141
+ for (const key of keys) {
142
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
143
+ if (!descriptor?.enumerable) failProvider(reason);
144
+ Object.defineProperty(result, key, {
145
+ value: clone(value[key], depth + 1), enumerable: true, configurable: false, writable: false,
146
+ });
147
+ }
148
+ return Object.freeze(result);
149
+ } finally { active.delete(value); }
150
+ }
151
+ try { return clone(input, 0); } catch { failProvider(reason); }
152
+ }
153
+
154
+ export function immutableRedactedJson(input, reason = 'invalid-request') {
155
+ try { return cloneJson(redactSecrets(input, { environment: {} }), reason); } catch { failProvider(reason); }
156
+ }
157
+
158
+ export function snapshotProviderJson(input) {
159
+ return immutableRedactedJson(cloneJson(input, 'invalid-request'), 'invalid-request');
160
+ }
161
+
162
+ function canonical(value) {
163
+ if (value === null || typeof value !== 'object') return JSON.stringify(value);
164
+ if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
165
+ return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
166
+ }
167
+
168
+ function payloadDigest(value) {
169
+ return createHash('sha256').update(canonical(cloneJson(value))).digest('hex');
170
+ }
171
+
172
+ function wireValue(write) {
173
+ const payload = cloneJson(write.payload);
174
+ let value;
175
+ if (write.provider === 'jira' && write.action === 'comment') value = { body: payload.body };
176
+ else if (write.provider === 'jira' && write.action === 'status') value = { transition: { id: payload.transitionId } };
177
+ else if (write.provider === 'confluence' && write.action === 'page-update') value = {
178
+ id: write.resourceId, status: 'current', title: payload.title,
179
+ body: { representation: 'storage', value: payload.body }, version: { number: payload.version },
180
+ };
181
+ else if (['jira', 'linear'].includes(write.provider) && write.action === 'tracker-transition') {
182
+ const jira = write.provider === 'jira';
183
+ const identifier = jira ? /^[1-9][0-9]{0,19}$/ : /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
184
+ const keys = jira ? ['transitionId'] : ['issueId', 'stateId'];
185
+ if (!payload || Object.keys(payload).length !== keys.length || !keys.every(key => Object.hasOwn(payload, key))
186
+ || !identifier.test(write.resourceId) || !identifier.test(write.expectedState)
187
+ || !/^[a-f0-9]{64}$/.test(write.idempotencyKey) || !Number.isFinite(Date.parse(write.expectedVersion))
188
+ || (jira ? !identifier.test(payload.transitionId) : payload.issueId !== write.resourceId || !identifier.test(payload.stateId)))
189
+ failProvider('invalid-request', { provider: write.provider });
190
+ value = jira ? { transition: { id: payload.transitionId } } : {
191
+ query: 'mutation RivetTrackerTransition($id: String!, $input: IssueUpdateInput!) { issueUpdate(id: $id, input: $input) { success issue { id } } }',
192
+ variables: { id: payload.issueId, input: { stateId: payload.stateId } },
193
+ };
194
+ }
195
+ else if (write.provider === 'linear' && write.action === 'delivery-comment') {
196
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
197
+ const lines = typeof payload?.body === 'string' ? payload.body.split('\n') : [];
198
+ const validUrlLine = (line, prefix) => {
199
+ if (typeof line !== 'string' || !line.startsWith(prefix)) return false;
200
+ const value = line.slice(prefix.length);
201
+ try {
202
+ const url = new URL(value);
203
+ return value.length <= 2048 && !/[\s<>`\[\]()]/.test(value)
204
+ && url.protocol === 'https:' && !url.username && !url.password && !url.search && !url.hash && url.href === value;
205
+ } catch { return false; }
206
+ };
207
+ if (!payload || Object.keys(payload).length !== 2 || !Object.hasOwn(payload, 'issueId') || !Object.hasOwn(payload, 'body')
208
+ || typeof payload.issueId !== 'string' || !uuid.test(payload.issueId) || payload.issueId !== write.resourceId
209
+ || write.expectedState !== 'merged' || !/^[a-f0-9]{40}$/.test(write.expectedVersion) || !/^[a-f0-9]{64}$/.test(write.idempotencyKey)
210
+ || ![5, 6].includes(lines.length) || lines[0] !== 'Rivet delivery update'
211
+ || lines[1] !== `Merged commit: ${write.expectedVersion}` || !validUrlLine(lines[2], 'Review: ')
212
+ || (lines.length === 6 && !validUrlLine(lines[3], 'Verified deployment: '))
213
+ || lines.at(-2) !== '' || lines.at(-1) !== `Rivet delivery operation: ${write.idempotencyKey}`)
214
+ failProvider('invalid-request', { provider: 'linear' });
215
+ value = {
216
+ query: 'mutation RivetDeliveryComment($input: CommentCreateInput!) { commentCreate(input: $input) { success comment { id } } }',
217
+ variables: { input: { issueId: payload.issueId, body: payload.body } },
218
+ };
219
+ }
220
+ else if (write.provider === 'github' && write.action === 'comment') value = { body: payload.body };
221
+ else if (write.provider === 'github' && write.action === 'pr') value = payload;
222
+ else if (['github', 'gitlab', 'bitbucket'].includes(write.provider) && write.action === 'review-update') {
223
+ const bodyKey = write.provider === 'github' ? 'body' : 'description';
224
+ if (!payload || Object.keys(payload).length !== 2 || !Object.hasOwn(payload, 'title') || !Object.hasOwn(payload, bodyKey)
225
+ || write.expectedState !== 'open' || !/^[a-f0-9]{40}$/.test(write.expectedVersion) || !/^[a-f0-9]{64}$/.test(write.idempotencyKey)
226
+ || typeof payload.title !== 'string' || !payload.title.trim() || Buffer.byteLength(payload.title) > 256 || /[\u0000-\u001f\u007f]/.test(payload.title)
227
+ || typeof payload[bodyKey] !== 'string' || Buffer.byteLength(payload[bodyKey]) > 32000 || /[\u0000-\u0008\u000b-\u001f\u007f]/.test(payload[bodyKey])) failProvider('invalid-request', { provider: write.provider });
228
+ value = payload;
229
+ }
230
+ else if (['github', 'gitlab', 'bitbucket'].includes(write.provider) && write.action === 'review-request') {
231
+ const github = write.provider === 'github', bitbucket = write.provider === 'bitbucket';
232
+ const keys = github ? ['title', 'body', 'head', 'base', 'draft', 'maintainer_can_modify']
233
+ : bitbucket ? ['title','description','source','destination','draft','close_source_branch'] : ['title', 'description', 'source_branch', 'target_branch', 'remove_source_branch', 'squash'];
234
+ const body = github ? payload?.body : payload?.description;
235
+ const source = github ? payload?.head : bitbucket ? payload?.source?.branch?.name : payload?.source_branch;
236
+ const target = github ? payload?.base : bitbucket ? payload?.destination?.branch?.name : payload?.target_branch;
237
+ const branch = value => typeof value === 'string' && /^[A-Za-z0-9_][A-Za-z0-9._/-]{0,254}$/.test(value)
238
+ && !value.includes('..') && !value.includes('//');
239
+ if (!payload || Object.keys(payload).length !== keys.length || !keys.every(key => Object.hasOwn(payload, key))
240
+ || write.expectedState !== 'absent' || !/^[a-f0-9]{40}$/.test(write.expectedVersion) || !/^[a-f0-9]{64}$/.test(write.idempotencyKey)
241
+ || typeof payload.title !== 'string' || payload.title.length < 1 || payload.title.length > 256 || /[\u0000-\u001f\u007f]/.test(payload.title)
242
+ || typeof body !== 'string' || body.length > 32100 || !body.endsWith(`\n\n<!-- rivet-review-operation:${write.idempotencyKey} -->`)
243
+ || !branch(source) || !branch(target) || source === target
244
+ || (github ? payload.draft !== false || payload.maintainer_can_modify !== false : bitbucket ? payload.draft !== false || payload.close_source_branch !== false || ![payload.source,payload.destination].every(part => part && Object.keys(part).length === 1 && part.branch && Object.keys(part.branch).length === 1 && Object.hasOwn(part.branch,'name')) : payload.remove_source_branch !== false || payload.squash !== false))
245
+ failProvider('invalid-request', { provider: write.provider });
246
+ value = payload;
247
+ }
248
+ else if (write.provider === 'github' && write.action === 'merge') {
249
+ if (!payload || Object.keys(payload).length !== 2
250
+ || !Object.hasOwn(payload, 'sha') || !Object.hasOwn(payload, 'merge_method')
251
+ || !/^[a-f0-9]{40}$/.test(payload.sha) || payload.sha !== write.expectedVersion
252
+ || !['merge', 'squash', 'rebase'].includes(payload.merge_method)) failProvider('invalid-request', { provider: 'github' });
253
+ value = { sha: payload.sha, merge_method: payload.merge_method };
254
+ }
255
+ else if (write.provider === 'github' && write.action === 'deploy') {
256
+ const keys = ['ref', 'auto_merge', 'task', 'environment', 'production_environment', 'transient_environment', 'payload'];
257
+ if (!payload || Object.keys(payload).length !== keys.length || !keys.every(key => Object.hasOwn(payload, key))
258
+ || !/^[a-f0-9]{40}$/.test(payload.ref) || payload.ref !== write.expectedVersion || write.expectedState !== 'merged'
259
+ || payload.auto_merge !== false || payload.task !== 'rivet-deploy' || payload.transient_environment !== false
260
+ || typeof payload.production_environment !== 'boolean'
261
+ || typeof payload.environment !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(payload.environment)
262
+ || !payload.payload || Object.keys(payload.payload).length !== 2
263
+ || !Object.hasOwn(payload.payload, 'rivetOperation') || !Object.hasOwn(payload.payload, 'workflow')
264
+ || payload.payload.rivetOperation !== write.idempotencyKey || !/^[a-f0-9]{64}$/.test(payload.payload.rivetOperation)
265
+ || typeof payload.payload.workflow !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,119}\.ya?ml$/.test(payload.payload.workflow))
266
+ failProvider('invalid-request', { provider: 'github' });
267
+ value = payload;
268
+ }
269
+ else if (write.provider === 'gitlab' && write.action === 'merge') {
270
+ const keys = ['sha', 'squash', 'auto_merge', 'should_remove_source_branch'];
271
+ if (!payload || Object.keys(payload).length !== keys.length || !keys.every(key => Object.hasOwn(payload, key))
272
+ || !/^[a-f0-9]{40}$/.test(payload.sha) || payload.sha !== write.expectedVersion
273
+ || payload.squash !== false || payload.auto_merge !== false || payload.should_remove_source_branch !== false)
274
+ failProvider('invalid-request', { provider: 'gitlab' });
275
+ value = { sha: payload.sha, squash: false, auto_merge: false, should_remove_source_branch: false };
276
+ }
277
+ else failProvider('invalid-request', { provider: write.provider });
278
+ const safe = cloneJson(value);
279
+ const redacted = redactSecrets(safe, { environment: {} });
280
+ if (canonical(safe) !== canonical(redacted)) failProvider('invalid-request', { provider: write.provider });
281
+ return safe;
282
+ }
283
+
284
+ export function createProviderWireBody(input) {
285
+ const value = captureRecord(input, new Set([
286
+ 'provider', 'action', 'resourceId', 'expectedState', 'expectedVersion', 'idempotencyKey', 'payload', 'payloadDigest', 'approval',
287
+ ]), ['provider', 'action', 'resourceId', 'expectedState', 'expectedVersion', 'idempotencyKey', 'payload']);
288
+ const write = {
289
+ provider: boundedString(value.provider, 32, PROVIDER), action: boundedString(value.action, 64, CAPABILITY),
290
+ resourceId: boundedString(value.resourceId, 256, SAFE_ID), expectedState: boundedString(value.expectedState, 256, SAFE_ID),
291
+ expectedVersion: boundedString(value.expectedVersion, 256), idempotencyKey: boundedString(value.idempotencyKey, 128, IDEMPOTENCY),
292
+ payload: cloneJson(value.payload),
293
+ };
294
+ const bytes = JSON.stringify(wireValue(write));
295
+ if (Buffer.byteLength(bytes) > 2 * 1024 * 1024) failProvider('invalid-request', { provider: write.provider });
296
+ const body = Object.freeze({ bytes, digest: createHash('sha256').update(bytes).digest('hex') });
297
+ wireBodies.add(body);
298
+ return body;
299
+ }
300
+
301
+ export function createProviderReadBody(input) {
302
+ const value = captureRecord(input, new Set(['provider', 'payload']), ['provider', 'payload'], 'invalid-request');
303
+ const provider = boundedString(value.provider, 32, PROVIDER);
304
+ if (provider !== 'linear') failProvider('invalid-request', { provider });
305
+ const payload = cloneJson(value.payload);
306
+ const redacted = redactSecrets(payload, { environment: {} });
307
+ if (canonical(payload) !== canonical(redacted)) failProvider('invalid-request', { provider });
308
+ const bytes = JSON.stringify(payload);
309
+ if (Buffer.byteLength(bytes) > 128 * 1024) failProvider('invalid-request', { provider });
310
+ const body = Object.freeze({ bytes, digest: createHash('sha256').update(bytes).digest('hex') });
311
+ wireBodies.add(body);
312
+ return body;
313
+ }
314
+
315
+ // Models have a separate bounded JSON body brand; they never borrow a tracker
316
+ // read/mutation action. Protocol modules construct only non-streaming text calls.
317
+ export function createModelJsonBody(input) {
318
+ const value = captureRecord(input, new Set(['provider', 'payload']), ['provider', 'payload'], 'invalid-request');
319
+ const provider = boundedString(value.provider, 32, PROVIDER);
320
+ if (!['anthropic', 'openai', 'gemini', 'ollama', 'openai-compatible'].includes(provider)) failProvider('invalid-request');
321
+ const payload = cloneJson(value.payload);
322
+ const redacted = redactSecrets(payload, { environment: {} });
323
+ if (canonical(payload) !== canonical(redacted)) failProvider('invalid-request', { provider });
324
+ const bytes = JSON.stringify(payload);
325
+ if (Buffer.byteLength(bytes) > 512 * 1024) failProvider('invalid-request', { provider });
326
+ const body = Object.freeze({ bytes, digest: createHash('sha256').update(bytes).digest('hex') });
327
+ wireBodies.add(body);
328
+ return body;
329
+ }
330
+
331
+ export function providerWireBytes(value) {
332
+ if (!wireBodies.has(value)) failProvider('invalid-request');
333
+ return value.bytes;
334
+ }
335
+
336
+ export function createProviderIdempotencyRegistry() {
337
+ const registry = Object.freeze(Object.create(null));
338
+ idempotencyRegistries.set(registry, new Map());
339
+ return registry;
340
+ }
341
+
342
+ export function createTrustedConditionalMutationCapability(input) {
343
+ const value = captureRecord(input, new Set(['provider', 'actions', 'execute']), ['provider', 'actions', 'execute'], 'invalid-config');
344
+ const provider = boundedString(value.provider, 32, PROVIDER, 'invalid-config');
345
+ const actionList = boundedArray(value.actions, 16, action => boundedString(action, 64, CAPABILITY, 'invalid-config'), 'invalid-config');
346
+ const actions = new Set(actionList);
347
+ if (typeof value.execute !== 'function' || actions.size !== actionList.length) failProvider('invalid-config', { provider });
348
+ const capability = Object.freeze(Object.create(null));
349
+ conditionalCapabilities.set(capability, { provider, actions, execute: value.execute });
350
+ return capability;
351
+ }
352
+
353
+ export function validateTrustedConditionalMutationCapability(capability, provider, requiredActions) {
354
+ const trusted = conditionalCapabilities.get(capability);
355
+ return Boolean(trusted && trusted.provider === provider && requiredActions.every(action => trusted.actions.has(action)));
356
+ }
357
+
358
+ export async function executeTrustedConditionalMutation(capability, input) {
359
+ const trusted = conditionalCapabilities.get(capability);
360
+ if (!trusted || trusted.provider !== input.provider || !trusted.actions.has(input.action)
361
+ || typeof input.dispatch !== 'function' || !wireBodies.has(input.wireBody)) failProvider('invalid-config', { provider: input.provider });
362
+ let operationCalls = 0;
363
+ let operationPromise;
364
+ let operationClosed = false;
365
+ let dispatchFailed = false;
366
+ let dispatchError;
367
+ const operation = Object.freeze({
368
+ run() {
369
+ operationCalls += 1;
370
+ if (operationClosed || operationCalls !== 1) {
371
+ const rejected = Promise.reject(new ProviderAdapterError('mutation-ambiguous', { provider: input.provider, retryClassification: 'ambiguous' }));
372
+ rejected.catch(() => {});
373
+ return rejected;
374
+ }
375
+ operationPromise = Promise.resolve().then(input.dispatch).then(result => {
376
+ const receipt = Object.freeze(Object.create(null));
377
+ conditionalOperationResults.set(receipt, { operation, result });
378
+ return receipt;
379
+ }, error => {
380
+ dispatchFailed = true;
381
+ dispatchError = error;
382
+ throw error;
383
+ });
384
+ return operationPromise;
385
+ },
386
+ });
387
+ const facts = Object.freeze({
388
+ provider: input.provider, action: input.action, resourceId: input.resourceId,
389
+ expectedState: input.expectedState, expectedVersion: input.expectedVersion,
390
+ idempotencyKey: input.idempotencyKey, wireBody: input.wireBody, operation,
391
+ });
392
+ let executorResult;
393
+ let executorFailed = false;
394
+ let executorError;
395
+ try { executorResult = await trusted.execute(facts); } catch (error) {
396
+ executorFailed = true;
397
+ executorError = error;
398
+ }
399
+ operationClosed = true;
400
+ if (operationCalls === 0 || !operationPromise) {
401
+ if (executorFailed && executorError instanceof ProviderAdapterError
402
+ && executorError.code === 'ERR_PROVIDER_STATE_CONFLICT') throw executorError;
403
+ failProvider('mutation-ambiguous', { provider: input.provider, retryClassification: 'ambiguous' });
404
+ }
405
+ let receipt;
406
+ try { receipt = await operationPromise; } catch {}
407
+ if (operationCalls !== 1) {
408
+ failProvider('mutation-ambiguous', { provider: input.provider, retryClassification: 'ambiguous' });
409
+ }
410
+ if (executorFailed) {
411
+ if (dispatchFailed && executorError === dispatchError) throw dispatchError;
412
+ failProvider('mutation-ambiguous', { provider: input.provider, retryClassification: 'ambiguous' });
413
+ }
414
+ const completed = conditionalOperationResults.get(executorResult);
415
+ if (!completed || executorResult !== receipt || completed.operation !== operation || dispatchFailed) {
416
+ failProvider('mutation-ambiguous', { provider: input.provider, retryClassification: 'ambiguous' });
417
+ }
418
+ return completed.result;
419
+ }
420
+
421
+ function capabilities(input, reason = 'invalid-envelope') {
422
+ const value = captureRecord(input, new Set(['read', 'write']), ['read', 'write'], reason);
423
+ const list = child => {
424
+ const result = boundedArray(child, 64, item => boundedString(item, 64, CAPABILITY, reason), reason);
425
+ if (new Set(result).size !== result.length) failProvider(reason);
426
+ return result;
427
+ };
428
+ return Object.freeze({ read: list(value.read), write: list(value.write) });
429
+ }
430
+
431
+ function timestamp(value, reason = 'invalid-envelope') {
432
+ boundedString(value, 32, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/, reason);
433
+ const milliseconds = Date.parse(value);
434
+ if (!Number.isSafeInteger(milliseconds) || new Date(milliseconds).toISOString() !== value) failProvider(reason);
435
+ return value;
436
+ }
437
+
438
+ export function providerTimestamp(clock, provider) {
439
+ if (typeof clock !== 'function') failProvider('invalid-config', { provider });
440
+ try { return timestamp(clock(), 'invalid-config'); } catch { failProvider('invalid-config', { provider }); }
441
+ }
442
+
443
+ export function isSensitiveQueryKey(value) {
444
+ if (typeof value !== 'string') return true;
445
+ const key = value.normalize('NFKC').replace(/[^a-z0-9]/gi, '').toLowerCase();
446
+ return key === 'sig' || key.includes('signature') || key.includes('secret') || key.includes('token')
447
+ || key.includes('credential') || key.includes('password') || key.includes('authorization')
448
+ || key === 'auth' || key.startsWith('auth') || key.startsWith('xamz') || key.includes('key');
449
+ }
450
+
451
+ function safeUrl(value, reason = 'invalid-envelope') {
452
+ boundedString(value, 2048, undefined, reason);
453
+ try {
454
+ const url = new URL(value);
455
+ if (url.protocol !== 'https:' || url.username || url.password || url.hash) failProvider(reason);
456
+ for (const key of [...url.searchParams.keys()]) if (isSensitiveQueryKey(key)) url.searchParams.delete(key);
457
+ return url.href;
458
+ } catch { failProvider(reason); }
459
+ }
460
+
461
+ export function sanitizeReferenceUrl(value) {
462
+ boundedString(value, 4096);
463
+ try {
464
+ const url = new URL(value);
465
+ if (url.protocol !== 'https:' || url.username || url.password || url.hash) failProvider('invalid-request');
466
+ for (const key of [...url.searchParams.keys()]) if (isSensitiveQueryKey(key)) url.searchParams.delete(key);
467
+ return url.href;
468
+ } catch { failProvider('invalid-request'); }
469
+ }
470
+
471
+ export function sanitizeProviderLink(value) {
472
+ boundedString(value, 4096);
473
+ try {
474
+ const relative = value.startsWith('/');
475
+ if (value.startsWith('//') || (!relative && !value.startsWith('https://'))) failProvider('invalid-request');
476
+ const url = new URL(value, 'https://provider.invalid');
477
+ if (url.protocol !== 'https:' || url.username || url.password || url.hash) failProvider('invalid-request');
478
+ for (const key of [...url.searchParams.keys()]) if (isSensitiveQueryKey(key)) url.searchParams.delete(key);
479
+ return relative ? `${url.pathname}${url.search}` : url.href;
480
+ } catch { failProvider('invalid-request'); }
481
+ }
482
+
483
+ export function createSourceEnvelope(input) {
484
+ const value = captureRecord(input, new Set([
485
+ 'provider', 'sourceId', 'sourceUrl', 'fetchedAt', 'fixtureSource', 'raw', 'normalized',
486
+ 'retryClassification', 'capabilities',
487
+ ]), [
488
+ 'provider', 'sourceId', 'sourceUrl', 'fetchedAt', 'fixtureSource', 'raw', 'normalized',
489
+ 'retryClassification', 'capabilities',
490
+ ], 'invalid-envelope');
491
+ const provider = boundedString(value.provider, 32, PROVIDER, 'invalid-envelope');
492
+ const sourceId = boundedString(value.sourceId, 256, SAFE_ID, 'invalid-envelope');
493
+ const sourceUrl = safeUrl(value.sourceUrl);
494
+ if (typeof value.fixtureSource !== 'boolean') failProvider('invalid-envelope');
495
+ if (!['none', 'transient', 'permanent'].includes(value.retryClassification)) failProvider('invalid-envelope');
496
+ const raw = immutableRedactedJson(value.raw, 'invalid-envelope');
497
+ const digest = createHash('sha256').update(canonical(raw)).digest('hex');
498
+ return Object.freeze({
499
+ version: 1, provider, fixtureSource: value.fixtureSource,
500
+ source: Object.freeze({ id: sourceId, url: sourceUrl }), fetchedAt: timestamp(value.fetchedAt), digest,
501
+ rawPayloadRef: Object.freeze({ kind: 'redacted-sha256', digest, redacted: true }),
502
+ normalized: immutableRedactedJson(value.normalized, 'invalid-envelope'),
503
+ retry: Object.freeze({ classification: value.retryClassification }),
504
+ capabilities: capabilities(value.capabilities),
505
+ });
506
+ }
507
+
508
+ export function createAdapter(input) {
509
+ const value = captureRecord(input, new Set([
510
+ 'provider', 'fixtureSource', 'capabilities', 'read', 'write',
511
+ ]), ['provider', 'fixtureSource', 'capabilities', 'read', 'write'], 'invalid-adapter');
512
+ const provider = boundedString(value.provider, 32, PROVIDER, 'invalid-adapter');
513
+ if (typeof value.fixtureSource !== 'boolean' || typeof value.read !== 'function' || typeof value.write !== 'function') failProvider('invalid-adapter');
514
+ const adapter = Object.freeze({
515
+ version: 1, provider, fixtureSource: value.fixtureSource,
516
+ capabilities: capabilities(value.capabilities, 'invalid-adapter'), read: value.read, write: value.write,
517
+ });
518
+ adapters.add(adapter);
519
+ return adapter;
520
+ }
521
+
522
+ export function validateAdapter(adapter) { return adapters.has(adapter); }
523
+
524
+ function snapshotWriteIdentity(input) {
525
+ const value = captureRecord(input, new Set([
526
+ 'provider', 'action', 'resourceId', 'expectedState', 'expectedVersion', 'idempotencyKey', 'payload', 'approval',
527
+ ]), [
528
+ 'provider', 'action', 'resourceId', 'expectedState', 'expectedVersion', 'idempotencyKey', 'payload', 'approval',
529
+ ]);
530
+ const result = {
531
+ provider: boundedString(value.provider, 32, PROVIDER),
532
+ action: boundedString(value.action, 64, CAPABILITY),
533
+ resourceId: boundedString(value.resourceId, 256, SAFE_ID),
534
+ expectedState: boundedString(value.expectedState, 256, SAFE_ID),
535
+ expectedVersion: boundedString(value.expectedVersion, 256),
536
+ idempotencyKey: boundedString(value.idempotencyKey, 128, IDEMPOTENCY),
537
+ payload: cloneJson(value.payload), approval: value.approval,
538
+ };
539
+ result.payloadDigest = payloadDigest(result.payload);
540
+ return Object.freeze(result);
541
+ }
542
+
543
+ export function providerWriteResource(input) {
544
+ const value = captureRecord(input, new Set([
545
+ 'provider', 'action', 'resourceId', 'expectedState', 'expectedVersion', 'idempotencyKey', 'payload', 'payloadDigest', 'approval',
546
+ ]), ['provider', 'action', 'resourceId', 'expectedState', 'expectedVersion', 'idempotencyKey', 'payload']);
547
+ const provider = boundedString(value.provider, 32, PROVIDER);
548
+ const action = boundedString(value.action, 64, CAPABILITY);
549
+ const resourceId = boundedString(value.resourceId, 256, SAFE_ID);
550
+ const expectedState = boundedString(value.expectedState, 256, SAFE_ID);
551
+ const expectedVersion = boundedString(value.expectedVersion, 256);
552
+ const key = boundedString(value.idempotencyKey, 128, IDEMPOTENCY);
553
+ const digest = payloadDigest(value.payload);
554
+ if (value.payloadDigest !== undefined && value.payloadDigest !== digest) failProvider('invalid-request');
555
+ const wire = createProviderWireBody({ provider, action, resourceId, expectedState, expectedVersion, idempotencyKey: key, payload: value.payload });
556
+ const tuple = { provider, action, resourceId, expectedState, expectedVersion, idempotencyKey: key, wireDigest: wire.digest };
557
+ return `provider-write-sha256:${createHash('sha256').update(canonical(tuple)).digest('hex')}`;
558
+ }
559
+
560
+ export function createProviderWriteGovernance(input, providerInput) {
561
+ const provider = boundedString(providerInput, 32, PROVIDER, 'invalid-config');
562
+ const value = captureRecord(input, new Set([
563
+ 'approvalRegistry', 'idempotencyRegistry', 'expectedApproverId', 'subjectId', 'now',
564
+ ]), ['approvalRegistry', 'idempotencyRegistry', 'expectedApproverId', 'subjectId', 'now'], 'invalid-config');
565
+ if (typeof value.now !== 'function') failProvider('invalid-config', { provider });
566
+ if (!idempotencyRegistries.has(value.idempotencyRegistry)) failProvider('invalid-config', { provider });
567
+ const governance = Object.freeze({
568
+ approvalRegistry: value.approvalRegistry,
569
+ idempotencyRegistry: value.idempotencyRegistry,
570
+ expectedApproverId: boundedString(value.expectedApproverId, 64, /^[a-z][a-z0-9-]*$/, 'invalid-config'),
571
+ subjectId: boundedString(value.subjectId, 64, /^[a-z][a-z0-9-]*$/, 'invalid-config'),
572
+ now: value.now,
573
+ });
574
+ governances.add(governance);
575
+ return governance;
576
+ }
577
+
578
+ function governanceNow(governance, provider) {
579
+ if (!governances.has(governance)) failProvider('approval-required', { provider });
580
+ let value;
581
+ try { value = governance.now(); } catch { failProvider('approval-required', { provider }); }
582
+ if (!Number.isSafeInteger(value) || value < 0) failProvider('approval-required', { provider });
583
+ return value;
584
+ }
585
+
586
+ export async function executeGovernedWrite(input, options) {
587
+ const write = snapshotWriteIdentity(input);
588
+ const provider = boundedString(options?.provider, 32, PROVIDER, 'invalid-config');
589
+ if (write.provider !== provider || !options.actions?.has(write.action)
590
+ || typeof options.preflight !== 'function'
591
+ || typeof options.prepare !== 'function' || typeof options.mutate !== 'function'
592
+ || !governances.has(options.governance)) failProvider('invalid-request', { provider });
593
+ const keys = idempotencyRegistries.get(options.governance.idempotencyRegistry);
594
+ if (!keys || keys.has(write.idempotencyKey)) failProvider('idempotency-reused', { provider });
595
+ const resource = providerWriteResource(write);
596
+ const reservation = { binding: resource, state: 'pending' };
597
+ keys.set(write.idempotencyKey, reservation);
598
+ let state;
599
+ let prepared;
600
+ try {
601
+ state = await options.preflight(write);
602
+ if (state?.state !== write.expectedState || state?.version !== write.expectedVersion) failProvider('state-conflict', { provider });
603
+ prepared = options.prepare(write, state);
604
+ if (prepared && typeof prepared.then === 'function') failProvider('invalid-config', { provider });
605
+ } catch (error) {
606
+ keys.delete(write.idempotencyKey);
607
+ if (error instanceof ProviderAdapterError) throw error;
608
+ failProvider('transport', { provider, retryClassification: 'transient' });
609
+ }
610
+ let claim;
611
+ try {
612
+ claim = claimApproval(write.approval, {
613
+ subjectId: options.governance.subjectId, action: 'provider.write', resource, policyId: 'authority.external-write',
614
+ }, {
615
+ registry: options.governance.approvalRegistry, expectedApproverId: options.governance.expectedApproverId,
616
+ requireHumanApprover: true, requireSingleUse: true, nowMs: governanceNow(options.governance, provider),
617
+ });
618
+ } catch {
619
+ keys.delete(write.idempotencyKey);
620
+ failProvider('approval-required', { provider });
621
+ }
622
+ if (!claim.valid) {
623
+ keys.delete(write.idempotencyKey);
624
+ failProvider('approval-required', { provider });
625
+ }
626
+ if (!claim.finalize()) {
627
+ claim.release();
628
+ keys.delete(write.idempotencyKey);
629
+ failProvider('approval-required', { provider });
630
+ }
631
+ reservation.state = 'dispatching';
632
+ try {
633
+ const result = await options.mutate(write, state, prepared);
634
+ if (!claim.publish()) failProvider('mutation-ambiguous', { provider, retryClassification: 'ambiguous' });
635
+ reservation.state = 'succeeded';
636
+ return Object.freeze({
637
+ version: 1, provider, action: write.action, resourceId: write.resourceId,
638
+ idempotencyKey: write.idempotencyKey, payloadDigest: write.payloadDigest,
639
+ status: 'written', result: immutableRedactedJson(result),
640
+ });
641
+ } catch (error) {
642
+ const definite = error instanceof ProviderAdapterError && (error.code === 'ERR_PROVIDER_STATE_CONFLICT'
643
+ || (error.code === 'ERR_PROVIDER_REMOTE' && Number.isInteger(error.status)
644
+ && error.status >= 400 && error.status < 500 && ![408, 425, 429].includes(error.status)));
645
+ if (definite) {
646
+ if (!claim.rollback()) failProvider('mutation-ambiguous', { provider, retryClassification: 'ambiguous' });
647
+ keys.delete(write.idempotencyKey);
648
+ throw error;
649
+ }
650
+ reservation.state = 'ambiguous';
651
+ failProvider('mutation-ambiguous', { provider, retryClassification: 'ambiguous' });
652
+ }
653
+ }
654
+
655
+ export function readOnlyWrite(provider) {
656
+ return function write() { return Promise.reject(new ProviderAdapterError('read-only', { provider })); };
657
+ }