@a5c-ai/kradle 5.0.1-staging.3abdf9534c25

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 (295) hide show
  1. package/Dockerfile +31 -0
  2. package/README.md +187 -0
  3. package/bin/kradle-demo.mjs +23 -0
  4. package/bin/kradle-server.mjs +14 -0
  5. package/dist/kradle-controller-ui.json +3482 -0
  6. package/dist/kradle-lifecycle.json +201 -0
  7. package/dist/kradle-runtime-snapshot.json +3125 -0
  8. package/dist/kradle-summary.json +724 -0
  9. package/docs/README.md +61 -0
  10. package/docs/agents/README.md +83 -0
  11. package/docs/agents/acceptance-test-matrix.md +193 -0
  12. package/docs/agents/agent-mux-adapter-contract.md +167 -0
  13. package/docs/agents/agent-mux-source-map.md +310 -0
  14. package/docs/agents/agent-run-memory-import-spec.md +256 -0
  15. package/docs/agents/agent-stack-management-spec.md +421 -0
  16. package/docs/agents/api-contract-spec.md +309 -0
  17. package/docs/agents/artifacts-writeback-spec.md +145 -0
  18. package/docs/agents/chart-packaging-spec.md +128 -0
  19. package/docs/agents/ci-orchestration-spec.md +140 -0
  20. package/docs/agents/context-assembly-spec.md +219 -0
  21. package/docs/agents/controller-reconciliation-spec.md +255 -0
  22. package/docs/agents/crd-schema-spec.md +315 -0
  23. package/docs/agents/decision-log-open-questions.md +169 -0
  24. package/docs/agents/developer-implementation-checklist.md +329 -0
  25. package/docs/agents/dispatching-design.md +262 -0
  26. package/docs/agents/gaps-agent-mux-to-kradle-crds.md +298 -0
  27. package/docs/agents/glossary.md +66 -0
  28. package/docs/agents/implementation-blueprint.md +324 -0
  29. package/docs/agents/implementation-rollout-slices.md +251 -0
  30. package/docs/agents/memory-context-integration-spec.md +194 -0
  31. package/docs/agents/memory-ontology-schema-spec.md +253 -0
  32. package/docs/agents/memory-operations-runbook.md +121 -0
  33. package/docs/agents/mvp-vertical-slice-spec.md +146 -0
  34. package/docs/agents/observability-audit-spec.md +265 -0
  35. package/docs/agents/operator-runbook.md +174 -0
  36. package/docs/agents/org-memory-api-payload-examples.md +333 -0
  37. package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
  38. package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
  39. package/docs/agents/org-memory-ui-implementation-map.md +114 -0
  40. package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
  41. package/docs/agents/org-resource-model-delta-spec.md +111 -0
  42. package/docs/agents/org-route-resource-model-spec.md +183 -0
  43. package/docs/agents/org-scoping-namespace-spec.md +114 -0
  44. package/docs/agents/rbac-secrets-management-spec.md +406 -0
  45. package/docs/agents/repository-page-integration-spec.md +255 -0
  46. package/docs/agents/resource-contract-examples.md +808 -0
  47. package/docs/agents/resource-relationship-map.md +190 -0
  48. package/docs/agents/security-threat-model.md +188 -0
  49. package/docs/agents/shared-memory-company-brain-spec.md +358 -0
  50. package/docs/agents/storage-migration-spec.md +168 -0
  51. package/docs/agents/subagent-orchestration-spec.md +152 -0
  52. package/docs/agents/system-overview.md +88 -0
  53. package/docs/agents/tools-mcp-skills-spec.md +189 -0
  54. package/docs/agents/traceability-matrix.md +79 -0
  55. package/docs/agents/ui-flow-spec.md +211 -0
  56. package/docs/agents/ui-ux-system-spec.md +426 -0
  57. package/docs/agents/workspace-lifecycle-spec.md +166 -0
  58. package/docs/architecture-spec.md +78 -0
  59. package/docs/architecture-v2.md +2759 -0
  60. package/docs/components/control-plane.md +78 -0
  61. package/docs/components/data-plane.md +69 -0
  62. package/docs/components/hooks-events.md +67 -0
  63. package/docs/components/identity-rbac-policy.md +73 -0
  64. package/docs/components/kubevela-oam.md +70 -0
  65. package/docs/components/operations-publishing.md +81 -0
  66. package/docs/components/runners-ci.md +66 -0
  67. package/docs/components/web-ui.md +94 -0
  68. package/docs/crd-behaviors-and-relationships.md +3926 -0
  69. package/docs/external/README.md +47 -0
  70. package/docs/external/bidirectional-sync-design.md +134 -0
  71. package/docs/external/cicd-interface.md +64 -0
  72. package/docs/external/external-backend-controllers.md +170 -0
  73. package/docs/external/external-backend-crds.md +234 -0
  74. package/docs/external/external-backend-ui-spec.md +151 -0
  75. package/docs/external/external-backend-ux-flows.md +115 -0
  76. package/docs/external/external-object-mapping.md +125 -0
  77. package/docs/external/git-forge-interface.md +68 -0
  78. package/docs/external/github-integration-design.md +151 -0
  79. package/docs/external/issue-tracking-interface.md +66 -0
  80. package/docs/external/provider-capability-manifests.md +204 -0
  81. package/docs/external/provider-catalog.md +139 -0
  82. package/docs/external/provider-rollout-testing.md +78 -0
  83. package/docs/external/research-results.md +48 -0
  84. package/docs/external/security-auth-permissions.md +81 -0
  85. package/docs/external/sync-state-machines.md +108 -0
  86. package/docs/external/unified-external-backend-model.md +107 -0
  87. package/docs/external/user-facing-changes.md +67 -0
  88. package/docs/gaps.md +161 -0
  89. package/docs/install.md +94 -0
  90. package/docs/integration-and-design-decisions.md +1530 -0
  91. package/docs/kradle-design.md +334 -0
  92. package/docs/local-minikube.md +55 -0
  93. package/docs/ontology/README.md +32 -0
  94. package/docs/ontology/bounded-contexts.md +29 -0
  95. package/docs/ontology/events-and-hooks.md +32 -0
  96. package/docs/ontology/oam-kubevela.md +32 -0
  97. package/docs/ontology/operations-and-release.md +25 -0
  98. package/docs/ontology/personas-and-actors.md +32 -0
  99. package/docs/ontology/policies-and-invariants.md +33 -0
  100. package/docs/ontology/problem-space.md +30 -0
  101. package/docs/ontology/resource-contracts.md +40 -0
  102. package/docs/ontology/resource-taxonomy.md +42 -0
  103. package/docs/ontology/runners-and-ci.md +29 -0
  104. package/docs/ontology/solution-space.md +24 -0
  105. package/docs/ontology/storage-and-data-boundaries.md +29 -0
  106. package/docs/ontology/validation-matrix.md +24 -0
  107. package/docs/ontology/web-ui-excellent-flows.md +32 -0
  108. package/docs/ontology/workflows.md +39 -0
  109. package/docs/ontology/world.md +35 -0
  110. package/docs/openapi.yaml +1291 -0
  111. package/docs/product-requirements.md +62 -0
  112. package/docs/requirements-v2.md +235 -0
  113. package/docs/roadmap-mvp.md +87 -0
  114. package/docs/sdk-api-reference.md +1108 -0
  115. package/docs/system-requirements.md +90 -0
  116. package/docs/system-spec-v2.md +1230 -0
  117. package/docs/tests/README.md +53 -0
  118. package/docs/tests/agent-qa-plan.md +63 -0
  119. package/docs/tests/browser-ui-tests.md +62 -0
  120. package/docs/tests/ci-quality-gates.md +48 -0
  121. package/docs/tests/coverage-model.md +64 -0
  122. package/docs/tests/e2e-scenario-tests.md +53 -0
  123. package/docs/tests/fixtures-test-data.md +63 -0
  124. package/docs/tests/observability-reliability-tests.md +54 -0
  125. package/docs/tests/product-test-matrix.md +145 -0
  126. package/docs/tests/qa-adoption-roadmap.md +130 -0
  127. package/docs/tests/qa-automation-plan.md +101 -0
  128. package/docs/tests/security-compliance-tests.md +57 -0
  129. package/docs/tests/test-framework-tools.md +88 -0
  130. package/docs/tests/test-suite-layout.md +121 -0
  131. package/docs/tests/unit-integration-tests.md +48 -0
  132. package/docs/todo-kyverno +714 -0
  133. package/docs/todos.md +4 -0
  134. package/docs/user-stories.md +78 -0
  135. package/docs/web-console-spec.md +533 -0
  136. package/examples/minikube-demo.yaml +190 -0
  137. package/examples/oam-application.yaml +23 -0
  138. package/examples/policy-kyverno-pr-title.yaml +18 -0
  139. package/package.json +66 -0
  140. package/scripts/build.mjs +29 -0
  141. package/scripts/setup-minikube.mjs +65 -0
  142. package/scripts/smoke.mjs +37 -0
  143. package/scripts/validate-doc-coverage.mjs +152 -0
  144. package/scripts/validate-package.mjs +95 -0
  145. package/scripts/validate-ui.mjs +305 -0
  146. package/src/agent-adapter-controller.js +169 -0
  147. package/src/agent-approval-controller.js +170 -0
  148. package/src/agent-context-bundles.js +242 -0
  149. package/src/agent-dispatch-controller.js +549 -0
  150. package/src/agent-gateway-config-controller.js +147 -0
  151. package/src/agent-identity-migration.js +115 -0
  152. package/src/agent-memory-controller.js +357 -0
  153. package/src/agent-memory-import.js +327 -0
  154. package/src/agent-memory-query.js +292 -0
  155. package/src/agent-memory-repository-source-controller.js +255 -0
  156. package/src/agent-mux-client.js +589 -0
  157. package/src/agent-permission-review.js +250 -0
  158. package/src/agent-persona-controller.js +135 -0
  159. package/src/agent-project-controller.js +117 -0
  160. package/src/agent-prompt-composition.js +55 -0
  161. package/src/agent-provider-config-controller.js +151 -0
  162. package/src/agent-secret-config-grant-controller.js +282 -0
  163. package/src/agent-session-transcript-controller.js +189 -0
  164. package/src/agent-stack-controller.js +421 -0
  165. package/src/agent-subagent-controller.js +160 -0
  166. package/src/agent-transport-binding-controller.js +121 -0
  167. package/src/agent-trigger-controller.js +387 -0
  168. package/src/agent-workspace-controller.js +702 -0
  169. package/src/agent-writeback-controller.js +302 -0
  170. package/src/api-controller.js +621 -0
  171. package/src/argocd-gitops.js +43 -0
  172. package/src/artifact-registry-controller.js +542 -0
  173. package/src/assistant-runtime.js +284 -0
  174. package/src/async-controller.js +207 -0
  175. package/src/audit-controller.js +191 -0
  176. package/src/auth.js +310 -0
  177. package/src/component-catalog.js +41 -0
  178. package/src/control-plane.js +136 -0
  179. package/src/controller-client.js +112 -0
  180. package/src/controller-ui.js +620 -0
  181. package/src/data-plane.js +179 -0
  182. package/src/event-bus.js +397 -0
  183. package/src/external/conflict-controller.js +225 -0
  184. package/src/external/github/auth.js +96 -0
  185. package/src/external/github/cicd.js +180 -0
  186. package/src/external/github/git-forge.js +240 -0
  187. package/src/external/github/index.js +144 -0
  188. package/src/external/github/issue-tracking.js +163 -0
  189. package/src/external/provider-adapter.js +161 -0
  190. package/src/external/provider-resource-factory.js +221 -0
  191. package/src/external/sync-controller.js +235 -0
  192. package/src/external/webhook-controller.js +144 -0
  193. package/src/external/write-controller.js +283 -0
  194. package/src/gitea-backend.js +131 -0
  195. package/src/gitea-service.js +173 -0
  196. package/src/handoff.js +98 -0
  197. package/src/health-probes.js +134 -0
  198. package/src/hooks-events.js +63 -0
  199. package/src/hooks-lifecycle.js +117 -0
  200. package/src/http-server.js +409 -0
  201. package/src/identity-policy.js +86 -0
  202. package/src/index.js +71 -0
  203. package/src/jitsi-agent-bridge.js +141 -0
  204. package/src/jitsi-meeting-controller.js +291 -0
  205. package/src/jitsi-sync-controller.js +198 -0
  206. package/src/kradle-inference-service-controller.js +246 -0
  207. package/src/kubernetes-controller-async.js +531 -0
  208. package/src/kubernetes-controller.js +904 -0
  209. package/src/kubernetes-resource-gateway.js +48 -0
  210. package/src/model-route-controller.js +364 -0
  211. package/src/notification-controller.js +178 -0
  212. package/src/operations.js +112 -0
  213. package/src/org-scoping.js +5 -0
  214. package/src/resource-model.js +282 -0
  215. package/src/runner-controller.js +272 -0
  216. package/src/runners-ci.js +48 -0
  217. package/src/runtime.js +196 -0
  218. package/src/snapshot-cache.js +157 -0
  219. package/src/virtual-model-controller.js +538 -0
  220. package/src/virtual-model-hook-bridge.js +200 -0
  221. package/src/web-ui.js +40 -0
  222. package/tests/agent-adapter-controller.test.js +361 -0
  223. package/tests/agent-approval-controller.test.js +173 -0
  224. package/tests/agent-context-bundles.test.js +278 -0
  225. package/tests/agent-dispatch-controller.test.js +679 -0
  226. package/tests/agent-gateway-config-controller.test.js +386 -0
  227. package/tests/agent-identity-migration.test.js +87 -0
  228. package/tests/agent-memory-controller.test.js +461 -0
  229. package/tests/agent-memory-import-snapshot.test.js +477 -0
  230. package/tests/agent-memory-query.test.js +404 -0
  231. package/tests/agent-memory-repository-source.test.js +514 -0
  232. package/tests/agent-mux-client.test.js +389 -0
  233. package/tests/agent-mux-integration.test.js +971 -0
  234. package/tests/agent-permission-review-v2.test.js +317 -0
  235. package/tests/agent-permission-review.test.js +209 -0
  236. package/tests/agent-persona-controller.test.js +127 -0
  237. package/tests/agent-project-controller.test.js +302 -0
  238. package/tests/agent-prompt-composition.test.js +76 -0
  239. package/tests/agent-provider-config-controller.test.js +376 -0
  240. package/tests/agent-resources.test.js +303 -0
  241. package/tests/agent-secret-config-grant.test.js +231 -0
  242. package/tests/agent-session-transcript-controller.test.js +499 -0
  243. package/tests/agent-stack-controller.test.js +283 -0
  244. package/tests/agent-subagent-controller.test.js +201 -0
  245. package/tests/agent-transport-binding-controller.test.js +294 -0
  246. package/tests/agent-trigger-controller.test.js +271 -0
  247. package/tests/agent-trigger-routes.test.js +190 -0
  248. package/tests/agent-trigger-sources.test.js +245 -0
  249. package/tests/agent-workspace-controller.test.js +181 -0
  250. package/tests/agent-writeback.test.js +292 -0
  251. package/tests/approval-persistence.test.js +171 -0
  252. package/tests/artifact-registry.test.js +511 -0
  253. package/tests/assistant-runtime.test.js +506 -0
  254. package/tests/async-controller.test.js +252 -0
  255. package/tests/audit-controller.test.js +227 -0
  256. package/tests/codespace-controller.test.js +318 -0
  257. package/tests/controller-client.test.js +133 -0
  258. package/tests/deployment.test.js +527 -0
  259. package/tests/e2e/lifecycle.test.js +120 -0
  260. package/tests/event-bus-integration.test.js +355 -0
  261. package/tests/external-github-forge.test.js +560 -0
  262. package/tests/external-github-issues-cicd.test.js +520 -0
  263. package/tests/external-integration.test.js +470 -0
  264. package/tests/external-persistence.test.js +415 -0
  265. package/tests/external-provider-adapter.test.js +365 -0
  266. package/tests/external-resource-model.test.js +223 -0
  267. package/tests/external-webhook-sync.test.js +287 -0
  268. package/tests/external-write-conflict.test.js +353 -0
  269. package/tests/gitea-service.test.js +253 -0
  270. package/tests/health-check-real.test.js +165 -0
  271. package/tests/health-probes.test.js +90 -0
  272. package/tests/hooks-lifecycle.test.js +364 -0
  273. package/tests/integration/full-flow.test.js +266 -0
  274. package/tests/jitsi-agent-bridge.test.js +119 -0
  275. package/tests/jitsi-helm-integration.test.js +77 -0
  276. package/tests/jitsi-meeting-controller.test.js +170 -0
  277. package/tests/jitsi-resource-model.test.js +73 -0
  278. package/tests/jitsi-sync-controller.test.js +112 -0
  279. package/tests/kradle-inference-service.test.js +689 -0
  280. package/tests/kradle.test.js +779 -0
  281. package/tests/memory-search-wiring.test.js +270 -0
  282. package/tests/model-route-controller.test.js +733 -0
  283. package/tests/notification-controller.test.js +196 -0
  284. package/tests/notification-integration.test.js +179 -0
  285. package/tests/org-scoping.test.js +687 -0
  286. package/tests/runner-controller.test.js +327 -0
  287. package/tests/runner-integration.test.js +231 -0
  288. package/tests/session-cookie-hmac.test.js +151 -0
  289. package/tests/snapshot-performance.test.js +315 -0
  290. package/tests/sse-events.test.js +107 -0
  291. package/tests/virtual-model-controller.test.js +877 -0
  292. package/tests/virtual-model-hook-bridge.test.js +384 -0
  293. package/tests/webhook-trigger.test.js +198 -0
  294. package/tests/workspace-volumes.test.js +312 -0
  295. package/tests/writeback-persistence.test.js +207 -0
@@ -0,0 +1,144 @@
1
+ // External Webhook Controller — Slice 3.4
2
+ //
3
+ // Handles inbound webhook delivery:
4
+ // - HMAC-SHA256 signature verification
5
+ // - Delivery record creation and persistence (in-memory store)
6
+ // - Deduplication by deliveryId
7
+ // - Async event queue with subscriber support
8
+
9
+ import { createHmac, timingSafeEqual } from 'node:crypto';
10
+
11
+ export const WEBHOOK_CONTROLLER_BOUNDARY = {
12
+ role: 'webhook-controller',
13
+ scope: 'Inbound webhook delivery — HMAC verification, dedup, async event queue',
14
+ owns: ['HMAC validation', 'delivery records', 'dedup index', 'event queue'],
15
+ delegatesTo: ['sync-controller'],
16
+ mustNotOwn: ['resource persistence', 'ownership arbitration']
17
+ };
18
+
19
+ /**
20
+ * Create a webhook controller that handles inbound webhook delivery.
21
+ *
22
+ * @param {{ secret: string }} options
23
+ * @returns {object}
24
+ */
25
+ export function createWebhookController(options = {}) {
26
+ const { secret } = options;
27
+
28
+ /** @type {Map<string, object>} deliveryId → delivery record */
29
+ const deliveries = new Map();
30
+
31
+ /** @type {Function[]} event subscribers */
32
+ const subscribers = [];
33
+
34
+ return {
35
+ /**
36
+ * Verify an HMAC-SHA256 signature against a request body.
37
+ *
38
+ * @param {string} body Raw request body string
39
+ * @param {string|null} signature Signature header value (e.g. "sha256=abc…")
40
+ * @returns {{ valid: boolean, reason: string|null }}
41
+ */
42
+ verifyHmacSignature(body, signature) {
43
+ if (!signature) {
44
+ return { valid: false, reason: 'missing signature header' };
45
+ }
46
+
47
+ if (!signature.startsWith('sha256=')) {
48
+ return { valid: false, reason: 'invalid signature format — must start with sha256=' };
49
+ }
50
+
51
+ const expected = 'sha256=' + createHmac('sha256', secret).update(body).digest('hex');
52
+
53
+ try {
54
+ const expectedBuf = Buffer.from(expected, 'utf8');
55
+ const actualBuf = Buffer.from(signature, 'utf8');
56
+ if (expectedBuf.length !== actualBuf.length) {
57
+ return { valid: false, reason: 'signature mismatch' };
58
+ }
59
+ const match = timingSafeEqual(expectedBuf, actualBuf);
60
+ if (!match) {
61
+ return { valid: false, reason: 'signature mismatch — HMAC does not match' };
62
+ }
63
+ return { valid: true, reason: null };
64
+ } catch {
65
+ return { valid: false, reason: 'signature verification error — invalid signature bytes' };
66
+ }
67
+ },
68
+
69
+ /**
70
+ * Create a delivery record for a received webhook payload.
71
+ *
72
+ * @param {{ deliveryId: string, eventType: string, payload: object, rawBody: string }} params
73
+ * @returns {{ deliveryId: string, eventType: string, timestamp: string, payload: object, rawBody: string, status: string }}
74
+ */
75
+ createDeliveryRecord({ deliveryId, eventType, payload, rawBody }) {
76
+ return {
77
+ deliveryId,
78
+ eventType,
79
+ timestamp: new Date().toISOString(),
80
+ payload,
81
+ rawBody,
82
+ status: 'received'
83
+ };
84
+ },
85
+
86
+ /**
87
+ * Persist a delivery record into the in-memory dedup store.
88
+ *
89
+ * @param {{ deliveryId: string }} record
90
+ */
91
+ recordDelivery(record) {
92
+ deliveries.set(record.deliveryId, record);
93
+ },
94
+
95
+ /**
96
+ * Check whether a deliveryId has already been processed.
97
+ *
98
+ * @param {string} deliveryId
99
+ * @returns {boolean}
100
+ */
101
+ isDuplicate(deliveryId) {
102
+ return deliveries.has(deliveryId);
103
+ },
104
+
105
+ /**
106
+ * Register a subscriber that will be called for each queued normalized event.
107
+ *
108
+ * @param {Function} handler fn(event) → void
109
+ */
110
+ onEvent(handler) {
111
+ subscribers.push(handler);
112
+ },
113
+
114
+ /**
115
+ * Process a delivery: create a record, check dedup, emit to queue.
116
+ *
117
+ * @param {{ deliveryId: string, eventType: string, payload: object, rawBody: string }} params
118
+ * @returns {{ queued: number, duplicate: boolean, deliveryId: string }}
119
+ */
120
+ processDelivery({ deliveryId, eventType, payload, rawBody }) {
121
+ if (this.isDuplicate(deliveryId)) {
122
+ return { queued: 0, duplicate: true, deliveryId };
123
+ }
124
+
125
+ const record = this.createDeliveryRecord({ deliveryId, eventType, payload, rawBody });
126
+ this.recordDelivery(record);
127
+
128
+ const normalizedEvent = {
129
+ deliveryId,
130
+ eventType,
131
+ payload,
132
+ receivedAt: record.timestamp
133
+ };
134
+
135
+ let queued = 0;
136
+ for (const subscriber of subscribers) {
137
+ subscriber(normalizedEvent);
138
+ queued++;
139
+ }
140
+
141
+ return { queued, duplicate: false, deliveryId };
142
+ }
143
+ };
144
+ }
@@ -0,0 +1,283 @@
1
+ // External Write Controller — Slice 3.5
2
+ // Manages WriteIntent lifecycle: creation, approval enforcement, idempotency,
3
+ // retry logic, and confirmation recording.
4
+
5
+ import { createResource, clone } from '../resource-model.js';
6
+
7
+ export const WRITE_CONTROLLER_BOUNDARY = {
8
+ role: 'external-write-controller',
9
+ scope: 'WriteIntent lifecycle — creation, approval, rejection, execution with retry',
10
+ owns: ['WriteIntent creation', 'approval gate', 'retry logic', 'idempotency key'],
11
+ delegatesTo: ['resource-model'],
12
+ mustNotOwn: ['conflict resolution', 'sync state', 'external API client']
13
+ };
14
+
15
+ const VALID_PHASES = ['PendingApproval', 'ReadyToSend', 'Sending', 'Retrying', 'Succeeded', 'Failed', 'Rejected'];
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Idempotency key
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /**
22
+ * Generate a deterministic idempotency key for a write operation.
23
+ * The key is stable for the same (interfaceKey, operation, resourceRef, payload).
24
+ *
25
+ * @param {{ interfaceKey: string, operation: string, resourceRef: string, payload: object }} input
26
+ * @returns {string}
27
+ */
28
+ export function getIdempotencyKey({ interfaceKey, operation, resourceRef, payload }) {
29
+ const canonical = JSON.stringify({ interfaceKey, operation, resourceRef, payload }, Object.keys({ interfaceKey, operation, resourceRef, payload }).sort());
30
+ // Simple deterministic hash using djb2-style accumulation (no external deps)
31
+ let hash = 5381;
32
+ for (let i = 0; i < canonical.length; i++) {
33
+ hash = ((hash << 5) + hash) ^ canonical.charCodeAt(i);
34
+ hash = hash >>> 0; // keep 32-bit unsigned
35
+ }
36
+ return `idem-${interfaceKey}-${operation}-${hash.toString(16)}`;
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Validation
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /**
44
+ * Validate a WriteIntent input object.
45
+ *
46
+ * @param {object} input
47
+ * @returns {{ valid: boolean, errors: string[] }}
48
+ */
49
+ export function validateWriteIntent(input) {
50
+ const errors = [];
51
+ if (!input) {
52
+ return { valid: false, errors: ['input must not be null or undefined'] };
53
+ }
54
+ if (!input.interfaceKey || typeof input.interfaceKey !== 'string') {
55
+ errors.push('interfaceKey is required and must be a non-empty string');
56
+ }
57
+ if (!input.operation || typeof input.operation !== 'string') {
58
+ errors.push('operation is required and must be a non-empty string');
59
+ }
60
+ if (!input.resourceRef || typeof input.resourceRef !== 'string') {
61
+ errors.push('resourceRef is required and must be a non-empty string');
62
+ }
63
+ return { valid: errors.length === 0, errors };
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Controller factory
68
+ // ---------------------------------------------------------------------------
69
+
70
+ /**
71
+ * Create a WriteController that manages ExternalWriteIntent resources.
72
+ *
73
+ * @param {{ persistFn?: (resource: object) => Promise<any> }} [opts]
74
+ * Optional persistFn is called (fire-and-forget) after intent state changes.
75
+ * @returns {object}
76
+ */
77
+ export function createWriteController({ persistFn } = {}) {
78
+ /**
79
+ * Fire-and-forget persistence helper.
80
+ * @param {object} resource
81
+ */
82
+ function persist(resource) {
83
+ if (typeof persistFn === 'function') {
84
+ Promise.resolve(persistFn(resource)).catch(() => {});
85
+ }
86
+ }
87
+
88
+ return {
89
+ role: 'write-controller',
90
+
91
+ /**
92
+ * Create a new ExternalWriteIntent resource.
93
+ * If requiresApproval is true, phase starts as PendingApproval.
94
+ * If requiresApproval is false, phase starts as ReadyToSend.
95
+ *
96
+ * @param {{ interfaceKey, operation, payload, resourceRef, requiresApproval?, maxRetries?, namespace?, organizationRef? }} input
97
+ * @returns {{ intent: object } | { error: true, message: string }}
98
+ */
99
+ createWriteIntent({
100
+ interfaceKey,
101
+ operation,
102
+ payload = {},
103
+ resourceRef,
104
+ requiresApproval = false,
105
+ maxRetries = 3,
106
+ namespace = 'default',
107
+ organizationRef = 'default'
108
+ }) {
109
+ const validation = validateWriteIntent({ interfaceKey, operation, resourceRef });
110
+ if (!validation.valid) {
111
+ return { error: true, message: validation.errors.join('; ') };
112
+ }
113
+
114
+ const idempotencyKey = getIdempotencyKey({ interfaceKey, operation, resourceRef, payload });
115
+ const now = new Date().toISOString();
116
+ const intentName = `write-intent-${idempotencyKey}-${Date.now()}`;
117
+ const phase = requiresApproval ? 'PendingApproval' : 'ReadyToSend';
118
+
119
+ const intent = createResource('ExternalWriteIntent', { name: intentName, namespace }, {
120
+ organizationRef,
121
+ interfaceKey,
122
+ operation,
123
+ payload,
124
+ resourceRef,
125
+ requiresApproval,
126
+ maxRetries,
127
+ idempotencyKey
128
+ });
129
+ intent.status = {
130
+ phase,
131
+ retryCount: 0,
132
+ createdAt: now
133
+ };
134
+
135
+ persist(intent);
136
+ return { intent };
137
+ },
138
+
139
+ /**
140
+ * Approve a PendingApproval intent, transitioning it to ReadyToSend.
141
+ *
142
+ * @param {{ intentName, approvedBy, resources }} opts
143
+ * @returns {{ intent: object } | { error: true, reason: string, message: string }}
144
+ */
145
+ approveWriteIntent({ intentName, approvedBy, resources = {} }) {
146
+ if (!intentName) {
147
+ return { error: true, reason: 'missing-name', message: 'intentName is required' };
148
+ }
149
+ if (!approvedBy) {
150
+ return { error: true, reason: 'missing-approver', message: 'approvedBy is required' };
151
+ }
152
+
153
+ const intents = resources.ExternalWriteIntent || [];
154
+ const found = intents.find((i) => i.metadata?.name === intentName);
155
+ if (!found) {
156
+ return { error: true, reason: 'not-found', message: `ExternalWriteIntent not found: ${intentName}` };
157
+ }
158
+ if (found.status?.phase !== 'PendingApproval') {
159
+ return { error: true, reason: 'invalid-phase', message: `Intent is not in PendingApproval: ${found.status?.phase}` };
160
+ }
161
+
162
+ const updated = clone(found);
163
+ updated.status = {
164
+ ...updated.status,
165
+ phase: 'ReadyToSend',
166
+ approvedBy,
167
+ approvedAt: new Date().toISOString()
168
+ };
169
+
170
+ persist(updated);
171
+ return { intent: updated };
172
+ },
173
+
174
+ /**
175
+ * Reject a PendingApproval intent, transitioning it to Rejected.
176
+ *
177
+ * @param {{ intentName, rejectedBy, reason, resources }} opts
178
+ * @returns {{ intent: object } | { error: true, reason: string, message: string }}
179
+ */
180
+ rejectWriteIntent({ intentName, rejectedBy, reason = '', resources = {} }) {
181
+ if (!intentName) {
182
+ return { error: true, reason: 'missing-name', message: 'intentName is required' };
183
+ }
184
+ if (!rejectedBy) {
185
+ return { error: true, reason: 'missing-rejecter', message: 'rejectedBy is required' };
186
+ }
187
+
188
+ const intents = resources.ExternalWriteIntent || [];
189
+ const found = intents.find((i) => i.metadata?.name === intentName);
190
+ if (!found) {
191
+ return { error: true, reason: 'not-found', message: `ExternalWriteIntent not found: ${intentName}` };
192
+ }
193
+ if (found.status?.phase !== 'PendingApproval') {
194
+ return { error: true, reason: 'invalid-phase', message: `Intent is not in PendingApproval: ${found.status?.phase}` };
195
+ }
196
+
197
+ const updated = clone(found);
198
+ updated.status = {
199
+ ...updated.status,
200
+ phase: 'Rejected',
201
+ rejectedBy,
202
+ rejectedAt: new Date().toISOString(),
203
+ rejectionReason: reason
204
+ };
205
+
206
+ persist(updated);
207
+ return { intent: updated };
208
+ },
209
+
210
+ /**
211
+ * Execute a WriteIntent by calling the provided executor function.
212
+ * Handles Sending → Succeeded / Retrying → Failed lifecycle.
213
+ *
214
+ * @param {{ intentName, resources, executor, onPhaseChange? }} opts
215
+ * @returns {Promise<{ intent: object } | { error: true, intent: object, message: string }>}
216
+ */
217
+ async executeWriteIntent({ intentName, resources = {}, executor, onPhaseChange }) {
218
+ if (!intentName) {
219
+ return { error: true, message: 'intentName is required' };
220
+ }
221
+ if (typeof executor !== 'function') {
222
+ return { error: true, message: 'executor must be a function' };
223
+ }
224
+
225
+ const intents = resources.ExternalWriteIntent || [];
226
+ const found = intents.find((i) => i.metadata?.name === intentName);
227
+ if (!found) {
228
+ return { error: true, message: `ExternalWriteIntent not found: ${intentName}` };
229
+ }
230
+ if (found.status?.phase !== 'ReadyToSend') {
231
+ return { error: true, message: `Intent is not ReadyToSend: ${found.status?.phase}` };
232
+ }
233
+
234
+ const maxRetries = found.spec?.maxRetries ?? 3;
235
+ let intent = clone(found);
236
+
237
+ // Transition to Sending
238
+ intent.status = { ...intent.status, phase: 'Sending', sendingAt: new Date().toISOString() };
239
+ if (onPhaseChange) onPhaseChange('Sending');
240
+
241
+ let lastError = null;
242
+ let attempt = 0;
243
+
244
+ while (attempt <= maxRetries) {
245
+ try {
246
+ const externalResult = await executor();
247
+ intent.status = {
248
+ ...intent.status,
249
+ phase: 'Succeeded',
250
+ succeededAt: new Date().toISOString(),
251
+ externalResult,
252
+ retryCount: attempt
253
+ };
254
+ if (onPhaseChange) onPhaseChange('Succeeded');
255
+ return { intent };
256
+ } catch (err) {
257
+ lastError = err;
258
+ attempt++;
259
+ if (attempt <= maxRetries) {
260
+ intent.status = {
261
+ ...intent.status,
262
+ phase: 'Retrying',
263
+ retryCount: attempt,
264
+ lastError: err.message
265
+ };
266
+ if (onPhaseChange) onPhaseChange('Retrying');
267
+ }
268
+ }
269
+ }
270
+
271
+ // Exhausted retries
272
+ intent.status = {
273
+ ...intent.status,
274
+ phase: 'Failed',
275
+ failedAt: new Date().toISOString(),
276
+ retryCount: attempt - 1,
277
+ lastError: lastError?.message ?? 'unknown error'
278
+ };
279
+ if (onPhaseChange) onPhaseChange('Failed');
280
+ return { error: true, intent, message: `Execution failed after ${attempt - 1} retries: ${lastError?.message}` };
281
+ }
282
+ };
283
+ }
@@ -0,0 +1,131 @@
1
+ export function createGiteaBackend({ baseUrl = 'http://kradle-gitea-http:3000', token, fetchImpl = globalThis.fetch } = {}) {
2
+ if (!fetchImpl) throw new Error('Gitea backend requires a fetch implementation');
3
+ const root = baseUrl.replace(/\/$/, '');
4
+
5
+ async function request(method, path, body) {
6
+ const response = await fetchImpl(`${root}/api/v1${path}`, {
7
+ method,
8
+ headers: {
9
+ Accept: 'application/json',
10
+ 'Content-Type': 'application/json',
11
+ ...(token ? { Authorization: `token ${token}` } : {})
12
+ },
13
+ ...(body === undefined ? {} : { body: JSON.stringify(body) })
14
+ });
15
+ if (!response.ok) throw new Error(`Gitea ${method} ${path} failed with ${response.status}`);
16
+ return response.status === 204 ? null : response.json();
17
+ }
18
+
19
+ return {
20
+ role: 'gitea-backend',
21
+ baseUrl: root,
22
+ createOrganization({ name, fullName = name, description = '', visibility = 'private' }) {
23
+ return request('POST', '/orgs', { username: name, full_name: fullName, description, visibility });
24
+ },
25
+ createUser({ username, email, fullName = username, password, mustChangePassword = true }) {
26
+ return request('POST', '/admin/users', { username, email, full_name: fullName, password, must_change_password: mustChangePassword });
27
+ },
28
+ editUser({ username, email, fullName, active = true, admin = false }) {
29
+ return request('PATCH', `/admin/users/${encodeURIComponent(username)}`, { email, full_name: fullName, active, admin });
30
+ },
31
+ addUserSshKey({ title, key, readOnly = false }) {
32
+ return request('POST', '/user/keys', { title, key, read_only: readOnly });
33
+ },
34
+ createRepository({ owner, name, private: isPrivate = true, defaultBranch = 'main', description = '' }) {
35
+ const path = owner ? `/orgs/${encodeURIComponent(owner)}/repos` : '/user/repos';
36
+ return request('POST', path, { name, private: isPrivate, default_branch: defaultBranch, description, auto_init: false });
37
+ },
38
+ addDeployKey({ owner, repo, title, key, readOnly = true }) {
39
+ return request('POST', `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/keys`, { title, key, read_only: readOnly });
40
+ },
41
+ addCollaborator({ owner, repo, username, permission = 'read' }) {
42
+ return request('PUT', `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(username)}`, { permission });
43
+ },
44
+ addTeamRepository({ org, team, repo, owner = org, permission = 'read' }) {
45
+ return request('PUT', `/teams/${encodeURIComponent(team)}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, { permission });
46
+ },
47
+ createTeam({ org, name, permission = 'read', units = ['repo.code', 'repo.pulls', 'repo.issues'] }) {
48
+ return request('POST', `/orgs/${encodeURIComponent(org)}/teams`, { name, permission, units });
49
+ },
50
+ addTeamMember({ team, username }) {
51
+ return request('PUT', `/teams/${encodeURIComponent(team)}/members/${encodeURIComponent(username)}`);
52
+ },
53
+ protectBranch({ owner, repo, branch = 'main', approvals = 1, statusChecks = [] }) {
54
+ return request('POST', `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/branch_protections`, {
55
+ branch_name: branch,
56
+ enable_push: false,
57
+ enable_push_whitelist: true,
58
+ required_approvals: approvals,
59
+ enable_status_check: statusChecks.length > 0,
60
+ status_check_contexts: statusChecks
61
+ });
62
+ },
63
+ createIssue({ owner, repo, title, body = '', labels = [], assignees = [] }) {
64
+ return request('POST', `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues`, { title, body, labels, assignees });
65
+ },
66
+ createPullRequest({ owner, repo, title, head, base = 'main', body = '' }) {
67
+ return request('POST', `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls`, { title, head, base, body });
68
+ },
69
+ createWebhook({ owner, repo, url, events = ['push', 'pull_request'], secret }) {
70
+ return request('POST', `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/hooks`, {
71
+ type: 'gitea',
72
+ active: true,
73
+ events,
74
+ config: { url, content_type: 'json', ...(secret ? { secret } : {}) }
75
+ });
76
+ }
77
+ };
78
+ }
79
+
80
+ export function orgMemoryRepositoryName(org = 'default') {
81
+ return `_${String(org || 'default').replace(/[^a-zA-Z0-9.-]+/g, '-')}_`;
82
+ }
83
+
84
+ export function giteaIssueSyncPlan({ org = 'default', project = null, issue = null, repositories = [] } = {}) {
85
+ const owner = org;
86
+ const repo = orgMemoryRepositoryName(org);
87
+ const issueName = issue?.metadata?.name || issue?.name || '<issue>';
88
+ return {
89
+ backend: 'gitea',
90
+ owner,
91
+ repo,
92
+ issue: issueName,
93
+ project,
94
+ repositoryRefs: repositories,
95
+ metadataKeys: ['kradle.a5c.ai/project', 'kradle.a5c.ai/repositories'],
96
+ actions: [
97
+ { action: 'ensureOrgMemoryRepository', owner, repo },
98
+ { action: 'syncIssue', owner, repo, issue: issueName },
99
+ { action: 'writeIssueRepositoryMetadata', issue: issueName, repositories }
100
+ ]
101
+ };
102
+ }
103
+
104
+ export function githubProjectIssueSyncPlan({ org = 'default', project = null, issue = null, repositories = [] } = {}) {
105
+ return {
106
+ backend: 'github',
107
+ owner: org,
108
+ project,
109
+ issue: issue?.metadata?.name || issue?.name || '<issue>',
110
+ repositoryRefs: repositories,
111
+ metadataKeys: ['project item fields', 'kradle repositories field'],
112
+ actions: ['syncProjectItem', 'syncIssueMetadata', 'syncRepositoryLinks']
113
+ };
114
+ }
115
+
116
+ export function giteaRepositoryIntegrationPlan({ owner, repo, deployKeyTitle = 'kradle-gitops', permission = 'write', branch = 'main', webhookUrl }) {
117
+ return {
118
+ backend: 'gitea',
119
+ operations: [
120
+ { action: 'createOrganization', owner },
121
+ { action: 'createRepository', owner, repo },
122
+ { action: 'ensureUserMappings', owner },
123
+ { action: 'addDeployKey', owner, repo, title: deployKeyTitle, readOnly: false },
124
+ { action: 'addUserSshKey', owner, repo, title: 'developer key' },
125
+ { action: 'addCollaborator', owner, repo, permission },
126
+ { action: 'addTeamRepository', owner, repo, team: 'maintainers', permission: 'admin' },
127
+ { action: 'protectBranch', owner, repo, branch },
128
+ { action: 'createWebhook', owner, repo, url: webhookUrl }
129
+ ]
130
+ };
131
+ }