@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,2759 @@
1
+ # Kradle Architecture Specification v2
2
+
3
+ > Exhaustive architecture reference derived from implementation source code.
4
+ > Source: `packages/kradle/core/src/`, `packages/kradle/sdk/src/`, `packages/kradle/web/`, `packages/kradle/cli/`
5
+
6
+ ---
7
+
8
+ ## 1. System Overview
9
+
10
+ Kradle is a Kubernetes-native Git forge runtime built as a monorepo with four packages:
11
+
12
+ | Package | NPM Name | Role | Path | Dependencies |
13
+ |---------|-----------|------|------|--------------|
14
+ | **core** | `@a5c-ai/kradle` | Resource model, controllers, HTTP API server | `packages/kradle/core/` | Zero external (Node.js built-ins only) |
15
+ | **sdk** | `@a5c-ai/kradle-sdk` | Client SDK re-exporting core helpers for web/CLI consumers | `packages/kradle/sdk/` | Re-exports from core |
16
+ | **cli** | `@a5c-ai/kradle-cli` | CLI entrypoint and MCP server mode | `packages/kradle/cli/` | Imports from core |
17
+ | **web** | `@a5c-ai/kradle-web` | Next.js 16 + React 19 web console | `packages/kradle/web/` | Imports from sdk |
18
+
19
+ **Design principles:**
20
+ - Pure ESM JavaScript (Node 20+), zero external runtime dependencies in core
21
+ - Kubernetes-first: all resources are K8s API objects (CRDs or aggregated)
22
+ - CRD-driven: 76 CustomResourceDefinitions under `kradle.a5c.ai/v1alpha1`
23
+ - Controller pattern: each domain has a controller with explicit boundary declarations
24
+ - Intent-based: controllers produce manifests/specs but never execute kubectl directly
25
+
26
+ ```mermaid
27
+ graph TB
28
+ subgraph "Browser"
29
+ WEB[Web Console<br/>Next.js 16 + React 19]
30
+ end
31
+
32
+ subgraph "SDK Layer"
33
+ SDK_INDEX[sdk/src/index.js<br/>65+ re-exports]
34
+ ATLAS[sdk/src/atlas-graph-client.js<br/>Stack layer catalog]
35
+ end
36
+
37
+ subgraph "Core Controllers"
38
+ HTTP[http-server.js<br/>Node HTTP handler]
39
+ API[api-controller.js<br/>Facade/orchestrator]
40
+ STACK[agent-stack-controller.js]
41
+ DISPATCH[agent-dispatch-controller.js]
42
+ TRIGGER[agent-trigger-controller.js]
43
+ WORKSPACE[agent-workspace-controller.js]
44
+ APPROVAL[agent-approval-controller.js]
45
+ MEMORY[agent-memory-controller.js]
46
+ MEMQ[agent-memory-query.js]
47
+ PERM[agent-permission-review.js]
48
+ AUDIT[audit-controller.js]
49
+ NOTIFY[notification-controller.js]
50
+ RUNNER[runner-controller.js]
51
+ EVENTBUS[event-bus.js]
52
+ CACHE[snapshot-cache.js]
53
+ end
54
+
55
+ subgraph "Resource Layer"
56
+ GATEWAY[kubernetes-resource-gateway.js]
57
+ CLIENT[kubernetes-controller.js<br/>kubectl spawning]
58
+ RESMODEL[resource-model.js<br/>76 kinds]
59
+ end
60
+
61
+ subgraph "External Subsystem"
62
+ WEBHOOK[external/webhook-controller.js]
63
+ SYNC[external/sync-controller.js]
64
+ CONFLICT[external/conflict-controller.js]
65
+ WRITE[external/write-controller.js]
66
+ GITHUB_ADAPTER[external/github/]
67
+ end
68
+
69
+ subgraph "Infrastructure"
70
+ K8S[Kubernetes API<br/>etcd CRD storage]
71
+ PG[PostgreSQL<br/>Aggregated storage]
72
+ GITEA[Gitea<br/>Git hosting]
73
+ end
74
+
75
+ WEB --> SDK_INDEX
76
+ SDK_INDEX --> API
77
+ HTTP --> API
78
+ API --> GATEWAY
79
+ API --> DISPATCH
80
+ API --> TRIGGER
81
+ API --> APPROVAL
82
+ API --> MEMORY
83
+ API --> WORKSPACE
84
+ API --> SYNC
85
+ API --> WEBHOOK
86
+ API --> CONFLICT
87
+ API --> WRITE
88
+ DISPATCH --> PERM
89
+ DISPATCH --> STACK
90
+ DISPATCH --> WORKSPACE
91
+ DISPATCH --> APPROVAL
92
+ DISPATCH --> MEMORY
93
+ GATEWAY --> CLIENT
94
+ CLIENT --> K8S
95
+ CLIENT --> PG
96
+ GATEWAY --> GITEA
97
+ EVENTBUS --> HTTP
98
+ ```
99
+
100
+ ---
101
+
102
+ ## 2. Package Dependency Graph
103
+
104
+ ### 2.1 Import Hierarchy (Strict)
105
+
106
+ ```
107
+ web → sdk → core
108
+ cli → core
109
+ ```
110
+
111
+ The web package NEVER imports directly from core. The SDK acts as the public API surface.
112
+
113
+ ### 2.2 Core Internal Dependencies
114
+
115
+ ```mermaid
116
+ graph LR
117
+ HTTP[http-server] --> API[api-controller]
118
+ HTTP --> CTRL_UI[controller-ui]
119
+ HTTP --> GATEWAY[kubernetes-resource-gateway]
120
+ HTTP --> EVENTBUS[event-bus]
121
+ API --> GATEWAY
122
+ API --> DISPATCH[agent-dispatch-controller]
123
+ API --> TRIGGER[agent-trigger-controller]
124
+ API --> APPROVAL[agent-approval-controller]
125
+ API --> WORKSPACE[agent-workspace-controller]
126
+ API --> MEMORY_CTRL[agent-memory-controller]
127
+ API --> PERM[agent-permission-review]
128
+ API --> SYNC[external/sync-controller]
129
+ API --> WEBHOOK_CTRL[external/webhook-controller]
130
+ API --> WRITE_CTRL[external/write-controller]
131
+ API --> CONFLICT_CTRL[external/conflict-controller]
132
+ DISPATCH --> PERM
133
+ DISPATCH --> STACK[agent-stack-controller]
134
+ DISPATCH --> CONTEXT[agent-context-bundles]
135
+ DISPATCH --> MUX[agent-mux-client]
136
+ DISPATCH --> MEMORY_CTRL
137
+ DISPATCH --> APPROVAL
138
+ DISPATCH --> WORKSPACE
139
+ GATEWAY --> CLIENT[kubernetes-controller]
140
+ CLIENT --> RESMODEL[resource-model]
141
+ STACK --> PERM
142
+ TRIGGER --> DISPATCH
143
+ ```
144
+
145
+ ### 2.3 Circular Dependency Prevention
146
+
147
+ - Controllers only import `resource-model.js` and their declared `delegatesTo` modules
148
+ - Every controller has a `BOUNDARY` constant declaring what it owns and what it must not own
149
+ - The api-controller is the only fan-out point that imports multiple controllers
150
+ - No controller imports the api-controller (prevents upward dependency)
151
+
152
+ ---
153
+
154
+ ## 3. Request Lifecycle
155
+
156
+ ### 3.1 From Browser Click to Kubectl Apply
157
+
158
+ ```mermaid
159
+ sequenceDiagram
160
+ participant Browser
161
+ participant NextJS as Next.js App Router
162
+ participant WebAPI as Web API Route
163
+ participant SDK as fetchControllerUiModel()
164
+ participant HTTP as Kradle HTTP Server
165
+ participant API as createKradleApiController
166
+ participant Gateway as KubernetesResourceGateway
167
+ participant Client as KubernetesResourceClient
168
+ participant Kubectl as kubectl process
169
+
170
+ Browser->>NextJS: Page load / navigation
171
+ NextJS->>WebAPI: GET /api/orgs/[org]/repositories
172
+ WebAPI->>SDK: fetchControllerUiModel({ baseUrl, org })
173
+ SDK->>HTTP: GET /api/controller?org=acme
174
+ HTTP->>API: controller.snapshot()
175
+ API->>Gateway: resourceGateway.snapshot()
176
+ Gateway->>Client: resourceClient.snapshot()
177
+ Client->>Kubectl: spawnSync('kubectl', ['get', ...])
178
+ Kubectl-->>Client: JSON stdout
179
+ Client-->>Gateway: parsed resources
180
+ Gateway-->>API: snapshot object
181
+ API-->>HTTP: withArchitecture(snapshot)
182
+ HTTP->>HTTP: createControllerUiModel(snapshot, { organization })
183
+ HTTP-->>SDK: JSON response (UI model)
184
+ SDK-->>WebAPI: structured data
185
+ WebAPI-->>NextJS: props
186
+ NextJS-->>Browser: rendered HTML
187
+ ```
188
+
189
+ ### 3.2 Function Call Chain (Exact)
190
+
191
+ 1. `createKradleHttpHandler()` receives Node.js `IncomingMessage`
192
+ 2. URL parsed: `new URL(request.url, 'http://localhost')`
193
+ 3. Route matching via regex: `/^\/api\/orgs\/([^/]+)\/resources$/`
194
+ 4. Org extracted from URL path segment
195
+ 5. `createKradleApiController({ namespace: orgNamespaceName(org) })` instantiated per-request
196
+ 6. Controller method called (e.g., `listResource`, `applyResource`)
197
+ 7. Cross-org admission check in `applyResource()`: verifies `spec.organizationRef` matches namespace
198
+ 8. `resourceGateway.apply(resource)` delegates to kubectl
199
+ 9. `clearSnapshotCache()` invalidates stale data
200
+ 10. `globalEventBus.emitResourceChange(kind, name, operation)` broadcasts SSE
201
+ 11. JSON response written via `send(response, status, body)`
202
+
203
+ ---
204
+
205
+ ## 4. Snapshot Pipeline
206
+
207
+ ### 4.1 `getControllerSnapshot()` Step by Step
208
+
209
+ Source: `packages/kradle/core/src/kubernetes-controller.js` lines 352-497
210
+
211
+ ```mermaid
212
+ flowchart TD
213
+ A[Start: getControllerSnapshot] --> B{kubectl config current-context}
214
+ B -->|Fail| C[Return degraded snapshot]
215
+ B -->|OK| D{kubectl version --client=true}
216
+ D -->|Fail| C
217
+ D -->|OK| E[kubectl get apiservice v1alpha1.kradle.a5c.ai]
218
+ E --> F[kubectl get crd -o json]
219
+ F --> G{Filter CRDs by group}
220
+ G --> H[kradle.a5c.ai CRDs]
221
+ G --> I[core.oam.dev CRDs]
222
+ G --> J[kyverno.io CRDs]
223
+ H --> K[Build discoveredPluralSet]
224
+ K --> L[List platform-scoped resources]
225
+ L --> M[Determine org namespaces]
226
+ M --> N[List org-scoped resources per namespace]
227
+ N --> O[Get events in platform namespace]
228
+ O --> P[Run SubjectAccessReview for each CRD]
229
+ P --> Q[Discover Kyverno controllers]
230
+ Q --> R[Return full snapshot object]
231
+ ```
232
+
233
+ ### 4.2 Org Namespace Discovery
234
+
235
+ ```javascript
236
+ function organizationNamespaces(organizations, bindings, fallbackNamespace) {
237
+ // 1. Extract namespaceName from Organization specs
238
+ // 2. Extract namespace from OrgNamespaceBinding specs
239
+ // 3. Deduplicate with Set
240
+ // 4. Fallback: KRADLE_ADMIN_ORG, KRADLE_ORG, or 'default'
241
+ }
242
+ ```
243
+
244
+ ### 4.3 In-Cluster Detection
245
+
246
+ Source: `inClusterKubectlConfig()` at line 724
247
+
248
+ When running inside a Kubernetes pod:
249
+ - Checks `KUBERNETES_SERVICE_HOST` and `KUBERNETES_SERVICE_PORT`
250
+ - Reads `/var/run/secrets/kubernetes.io/serviceaccount/token`
251
+ - Reads `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`
252
+ - Adds `--server`, `--certificate-authority`, `--token` args to all kubectl calls
253
+
254
+ ### 4.4 kubectl Execution Model
255
+
256
+ ```javascript
257
+ function runKubectl(args, options) {
258
+ // Uses spawnSync (synchronous) for snapshot queries
259
+ // Timeout: KRADLE_KUBECTL_TIMEOUT_MS (default 3000ms)
260
+ // Max buffer: KRADLE_KUBECTL_MAX_BUFFER_BYTES (default 32MB)
261
+ // windowsHide: true (prevents console flash on Windows)
262
+ // encoding: 'utf8'
263
+ }
264
+ ```
265
+
266
+ ### 4.5 Environment Variables Affecting Snapshot
267
+
268
+ | Variable | Default | Purpose |
269
+ |----------|---------|---------|
270
+ | `KRADLE_KUBECTL` | `kubectl` | Path to kubectl binary |
271
+ | `KRADLE_NAMESPACE` | `kradle-system` | Platform namespace |
272
+ | `KRADLE_KUBECTL_TIMEOUT_MS` | `3000` | kubectl spawn timeout |
273
+ | `KRADLE_KUBECTL_MAX_BUFFER_BYTES` | `33554432` | Max stdout buffer (32MB) |
274
+ | `KRADLE_DISABLE_IN_CLUSTER_KUBECTL` | `false` | Skip in-cluster detection |
275
+ | `KUBECONFIG` | (none) | If set, disables in-cluster mode |
276
+ | `KUBERNETES_SERVICE_HOST` | (none) | In-cluster API server host |
277
+ | `KUBERNETES_SERVICE_PORT` | `443` | In-cluster API server port |
278
+ | `KRADLE_SERVICE_ACCOUNT_DIR` | `/var/run/secrets/kubernetes.io/serviceaccount` | SA mount path |
279
+ | `KRADLE_ORG` | `default` | Fallback org for namespace discovery |
280
+ | `KRADLE_ADMIN_ORG` | (none) | Admin org for namespace discovery |
281
+
282
+ ---
283
+
284
+ ## 5. Stale-While-Revalidate Cache
285
+
286
+ Source: `packages/kradle/core/src/snapshot-cache.js`
287
+
288
+ ### 5.1 Cache Architecture
289
+
290
+ ```mermaid
291
+ graph LR
292
+ subgraph "Per-Org Cache Map"
293
+ A["orgCacheMap (Map)"]
294
+ A --> B["'' → {data, timestamp, revalidating}"]
295
+ A --> C["'acme' → {data, timestamp, revalidating}"]
296
+ A --> D["'beta' → {data, timestamp, revalidating}"]
297
+ end
298
+ subgraph "Legacy Single-Org"
299
+ E["snapshotCache = {data, timestamp, org}"]
300
+ end
301
+ A -.->|sync| E
302
+ ```
303
+
304
+ ### 5.2 TTL Configuration
305
+
306
+ ```javascript
307
+ export const CACHE_TTL_MS = Number(process.env.KRADLE_SNAPSHOT_CACHE_TTL_MS || 30_000);
308
+ ```
309
+
310
+ ### 5.3 staleWhileRevalidate Algorithm
311
+
312
+ ```javascript
313
+ async function staleWhileRevalidate(org, revalidateFn, swrOptions = {}) {
314
+ const ttlMs = swrOptions.ttlMs ?? CACHE_TTL_MS; // Fresh window: 30s
315
+ const staleMs = swrOptions.staleMs ?? ttlMs * 5; // Max stale: 150s
316
+
317
+ // CASE 1: Fresh (< 30s old) → return immediately
318
+ // CASE 2: Stale but usable (30s-150s) → return immediately, revalidate in background
319
+ // CASE 3: Stale and revalidating → return stale data (another caller is refreshing)
320
+ // CASE 4: No cache or too old (>150s) → block on revalidation
321
+ }
322
+ ```
323
+
324
+ ### 5.4 Cache Invalidation Triggers
325
+
326
+ `clearSnapshotCache()` is called on:
327
+ - `applyResource()` success
328
+ - `applyResourceForOrg()` success
329
+ - `deleteResource()` success
330
+ - `deleteResourceForOrg()` success
331
+
332
+ ---
333
+
334
+ ## 6. Authentication Flow
335
+
336
+ ### 6.1 Complete OAuth Flow
337
+
338
+ Source: `packages/kradle/core/src/auth.js`
339
+
340
+ ```mermaid
341
+ sequenceDiagram
342
+ participant User
343
+ participant Browser
344
+ participant LoginPage as /login page
345
+ participant AuthRoute as /api/auth/github
346
+ participant GitHub as GitHub OAuth
347
+ participant CallbackRoute as /api/auth/callback/github
348
+ participant AuthModule as auth.js
349
+ participant K8s as Kubernetes
350
+
351
+ User->>Browser: Click "Sign in with GitHub"
352
+ Browser->>LoginPage: Navigate
353
+ LoginPage->>AuthRoute: GET /api/auth/github
354
+ AuthRoute->>AuthModule: buildAuthorizationRedirect({ provider, requestUrl })
355
+ AuthModule-->>AuthRoute: { url, state, redirectUri }
356
+ AuthRoute->>Browser: 302 Redirect to GitHub
357
+
358
+ Browser->>GitHub: GET /login/oauth/authorize?client_id=...&redirect_uri=...&scope=read:user+user:email&state=...
359
+ GitHub->>User: Show authorization prompt
360
+ User->>GitHub: Authorize
361
+ GitHub->>Browser: 302 Redirect to /api/auth/callback/github?code=ABC&state=...
362
+
363
+ Browser->>CallbackRoute: GET /api/auth/callback/github?code=ABC&state=...
364
+ CallbackRoute->>AuthModule: exchangeOAuthCodeForProfile({ provider, code, requestUrl })
365
+ AuthModule->>GitHub: POST /login/oauth/access_token (code + client_secret)
366
+ GitHub-->>AuthModule: { access_token: "gho_..." }
367
+ AuthModule->>GitHub: GET /user (Authorization: Bearer gho_...)
368
+ GitHub-->>AuthModule: { login, id, email, name }
369
+ AuthModule->>AuthModule: normalizeProviderProfile(provider, profile)
370
+ AuthModule-->>CallbackRoute: { provider, subject, email, displayName, username, groups, admin }
371
+
372
+ CallbackRoute->>AuthModule: registerLoginProfile({ controller, namespace, profile })
373
+ AuthModule->>K8s: applyResource(User)
374
+ AuthModule->>K8s: applyResource(IdentityMapping)
375
+ CallbackRoute->>AuthModule: createSessionCookie(config, profile, { secret })
376
+ AuthModule-->>CallbackRoute: "kradle_session=base64url.hmac; Path=/; HttpOnly; SameSite=Lax"
377
+ CallbackRoute->>Browser: Set-Cookie + 302 to /orgs/[org]
378
+ ```
379
+
380
+ ### 6.2 Session Cookie Structure
381
+
382
+ ```
383
+ kradle_session = base64url(payload) . hmac_sha256_base64url(payload, secret)
384
+ ```
385
+
386
+ Payload JSON:
387
+ ```json
388
+ { "provider": "github", "subject": "12345", "user": "octocat" }
389
+ ```
390
+
391
+ ### 6.3 Session Verification
392
+
393
+ ```javascript
394
+ function parseSessionCookie(config, cookieValue, options) {
395
+ // 1. Split on first '.' → [payload, signature]
396
+ // 2. If signed + no secret: reject (null)
397
+ // 3. If unsigned + secret configured: reject (null)
398
+ // 4. If signed + secret: compute expected HMAC, timingSafeEqual
399
+ // 5. If match: decode base64url → JSON.parse → extract user/subject/provider
400
+ // 6. Return { cookieName, provider, subject, user } or null
401
+ }
402
+ ```
403
+
404
+ ### 6.4 Delegated Identity (Proxy Auth)
405
+
406
+ Headers examined:
407
+ - `x-forwarded-user` (configurable via `KRADLE_AUTH_DELEGATED_USER_HEADER`)
408
+ - `x-forwarded-groups` (configurable via `KRADLE_AUTH_DELEGATED_GROUPS_HEADER`)
409
+ - `x-forwarded-email` (configurable via `KRADLE_AUTH_DELEGATED_EMAIL_HEADER`)
410
+
411
+ Local development auto-login:
412
+ - Active when `NODE_ENV !== 'production'` (or `KRADLE_AUTH_DELEGATED_LOCAL_DEVELOPMENT=true`)
413
+ - Default user: `KRADLE_AUTH_DELEGATED_LOCAL_USER` or `'local-developer'`
414
+ - Default groups: `KRADLE_AUTH_DELEGATED_LOCAL_GROUPS` or `'kradle:repo-admins'`
415
+
416
+ ### 6.5 Admin Detection
417
+
418
+ Admin status is derived from group membership:
419
+ ```javascript
420
+ admin: groups.includes('kradle:platform-engineers') || groups.includes('kradle:repo-admins')
421
+ ```
422
+
423
+ ### 6.6 All Auth Environment Variables
424
+
425
+ | Variable | Default | Purpose |
426
+ |----------|---------|---------|
427
+ | `KRADLE_AUTH_COOKIE_NAME` | `kradle_session` | Cookie name |
428
+ | `KRADLE_SESSION_SECRET` | `''` | HMAC signing secret |
429
+ | `KRADLE_AUTH_GITHUB_ENABLED` | `true` | Enable GitHub provider |
430
+ | `KRADLE_AUTH_GITHUB_CLIENT_ID` | `''` | OAuth client ID |
431
+ | `KRADLE_AUTH_GITHUB_CLIENT_SECRET` | `''` | OAuth client secret |
432
+ | `KRADLE_AUTH_GITHUB_AUTHORIZATION_URL` | `https://github.com/login/oauth/authorize` | Auth endpoint |
433
+ | `KRADLE_AUTH_GITHUB_TOKEN_URL` | `https://github.com/login/oauth/access_token` | Token endpoint |
434
+ | `KRADLE_AUTH_GITHUB_USERINFO_URL` | `https://api.github.com/user` | Profile endpoint |
435
+ | `KRADLE_AUTH_GITHUB_SCOPES` | `read:user user:email` | OAuth scopes |
436
+ | `KRADLE_AUTH_SSO_ENABLED` | `false` | Enable OIDC provider |
437
+ | `KRADLE_AUTH_SSO_PROVIDER_NAME` | `Workspace SSO` | Display label |
438
+ | `KRADLE_AUTH_SSO_ISSUER_URL` | `''` | OIDC issuer |
439
+ | `KRADLE_AUTH_SSO_CLIENT_ID` | `''` | OIDC client ID |
440
+ | `KRADLE_AUTH_SSO_CLIENT_SECRET` | `''` | OIDC client secret |
441
+ | `KRADLE_AUTH_SSO_AUTHORIZATION_URL` | `''` | OIDC auth endpoint |
442
+ | `KRADLE_AUTH_SSO_TOKEN_URL` | `''` | OIDC token endpoint |
443
+ | `KRADLE_AUTH_SSO_USERINFO_URL` | `''` | OIDC profile endpoint |
444
+ | `KRADLE_AUTH_SSO_SCOPES` | `openid profile email groups` | OIDC scopes |
445
+ | `KRADLE_AUTH_DELEGATED_IDENTITY_ENABLED` | `false` | Enable proxy auth |
446
+ | `KRADLE_AUTH_DELEGATED_USER_HEADER` | `x-forwarded-user` | User header |
447
+ | `KRADLE_AUTH_DELEGATED_GROUPS_HEADER` | `x-forwarded-groups` | Groups header |
448
+ | `KRADLE_AUTH_DELEGATED_EMAIL_HEADER` | `x-forwarded-email` | Email header |
449
+ | `KRADLE_AUTH_DELEGATED_LOCAL_DEVELOPMENT` | auto | Enable local dev fallback |
450
+ | `KRADLE_AUTH_DELEGATED_LOCAL_USER` | `local-developer` | Dev username |
451
+ | `KRADLE_AUTH_DELEGATED_LOCAL_EMAIL` | `''` | Dev email |
452
+ | `KRADLE_AUTH_DELEGATED_LOCAL_GROUPS` | `kradle:repo-admins` | Dev groups |
453
+ | `KRADLE_ADMIN_ORG` | (none) | Bootstrap admin org |
454
+ | `KRADLE_ADMIN_USERNAME` | (none) | Bootstrap admin user |
455
+
456
+ ---
457
+
458
+ ## 7. Resource Lifecycle
459
+
460
+ ### 7.1 From UI Form Submit to SSE Update
461
+
462
+ ```mermaid
463
+ sequenceDiagram
464
+ participant UI as Web Console
465
+ participant Route as API Route
466
+ participant HTTP as HTTP Handler
467
+ participant API as ApiController
468
+ participant Gateway as ResourceGateway
469
+ participant Client as KubernetesClient
470
+ participant Kubectl as kubectl
471
+ participant K8s as Kubernetes API
472
+ participant Cache as SnapshotCache
473
+ participant Bus as EventBus
474
+ participant SSE as SSE Stream
475
+
476
+ UI->>Route: POST fetch('/api/orgs/acme/resources', { body: resource })
477
+ Route->>HTTP: Forward request
478
+ HTTP->>HTTP: scopeResource(resource, org)
479
+ Note over HTTP: Add namespace, labels, organizationRef
480
+ HTTP->>API: scopedController.applyResource(scopedResource)
481
+ API->>API: Cross-org admission check
482
+ Note over API: Verify spec.organizationRef matches namespace
483
+ API->>Gateway: resourceGateway.apply(resource)
484
+ Gateway->>Client: resourceClient.applyResource(resource)
485
+ Client->>Client: withOrgScope(resource, { namespace })
486
+ Client->>Client: ensureNamespace(targetNs)
487
+ Client->>Kubectl: spawnSync(['apply', '-f', '-', '-o', 'json'], { input: JSON })
488
+ Kubectl->>K8s: kubectl apply -f -
489
+ K8s-->>Kubectl: Applied resource JSON
490
+ Kubectl-->>Client: stdout
491
+ Client-->>Gateway: { operation: 'apply', resource }
492
+ Gateway-->>API: result
493
+ API->>Cache: clearSnapshotCache()
494
+ API->>Bus: globalEventBus.emitResourceChange(kind, name, 'apply')
495
+ Bus->>SSE: writer(event) → response.write('data: {...}\n\n')
496
+ SSE->>UI: EventSource message received
497
+ UI->>UI: Re-fetch / optimistic update
498
+ API-->>HTTP: { operation, resource }
499
+ HTTP-->>Route: 201 JSON
500
+ Route-->>UI: Success response
501
+ ```
502
+
503
+ ### 7.2 scopeResource Function
504
+
505
+ ```javascript
506
+ function scopeResource(resource, org) {
507
+ const namespace = orgNamespaceName(org); // 'kradle-org-acme'
508
+ return {
509
+ ...resource,
510
+ metadata: {
511
+ ...(resource.metadata || {}),
512
+ namespace,
513
+ labels: {
514
+ ...(resource.metadata?.labels || {}),
515
+ 'kradle.a5c.ai/org': org,
516
+ 'kradle.a5c.ai/namespace': namespace
517
+ }
518
+ },
519
+ spec: { ...(resource.spec || {}), organizationRef: org }
520
+ };
521
+ }
522
+ ```
523
+
524
+ ### 7.3 Cross-Org Admission
525
+
526
+ In `applyResource()`:
527
+ ```javascript
528
+ const resourceOrg = resource.spec?.organizationRef;
529
+ const resourceNs = resource.metadata?.namespace;
530
+ if (resourceOrg) {
531
+ const expectedNs = orgNamespaceName(resourceOrg);
532
+ if (resourceNs && resourceNs !== expectedNs) {
533
+ throw new Error(`Cross-org namespace mismatch`);
534
+ }
535
+ }
536
+ ```
537
+
538
+ In `deleteResourceForOrg()`:
539
+ ```javascript
540
+ // Verify existing resource namespace matches org
541
+ if (!resourceNs || resourceNs !== orgNs) {
542
+ throw new Error(`Cross-org denial`);
543
+ }
544
+ ```
545
+
546
+ ---
547
+
548
+ ## 8. Agent Dispatch Lifecycle
549
+
550
+ ### 8.1 Complete Flow (K8s Job-Based)
551
+
552
+ Source: `packages/kradle/core/src/agent-dispatch-controller.js`
553
+
554
+ Agents are dispatched as Kubernetes `batch/v1` Jobs (not subprocesses or direct
555
+ Agent Mux HTTP calls). Each dispatch creates a Job manifest via `createAgentJob()`,
556
+ submits it to Kubernetes via `submitAgentJob()`, and waits for the agent pod to
557
+ POST its result to the callback endpoint.
558
+
559
+ ```mermaid
560
+ sequenceDiagram
561
+ participant UI as Dispatch Button
562
+ participant HTTP as HTTP Handler
563
+ participant API as ApiController
564
+ participant Dispatch as DispatchController
565
+ participant Perm as PermissionReviewer
566
+ participant Stack as StackController
567
+ participant Memory as MemoryController
568
+ participant Approval as ApprovalController
569
+ participant Workspace as WorkspaceController
570
+ participant Context as ContextBundler
571
+ participant Hooks as HooksLifecycleEmitter
572
+ participant K8s as Kubernetes Jobs API
573
+ participant Pod as Agent Pod (Job)
574
+ participant Callback as Callback Endpoint
575
+
576
+ UI->>HTTP: POST /api/orgs/:org/agents/dispatch
577
+ HTTP->>API: controller.dispatchAgent(input)
578
+ API->>API: snapshot = await this.snapshot()
579
+ API->>Dispatch: createManualDispatch({ ...input, resources: snapshot.resources })
580
+
581
+ Dispatch->>Dispatch: 1. resolveStack(agentStack, resources)
582
+ Note over Dispatch: Translates AgentStack CRD → execution config (model, prompt, tools, transport)
583
+ alt Stack not found
584
+ Dispatch-->>API: { error: true, reason: 'stack-not-found' }
585
+ end
586
+
587
+ Dispatch->>Perm: 2. reviewPermissions({ repository, ref, actor, agentStack, resources })
588
+ Note over Perm: Check cross-org, fork, SA, roles, secrets, configs
589
+ alt Permission denied
590
+ Dispatch-->>API: { error: true, reason: 'permission-denied' }
591
+ end
592
+
593
+ Dispatch->>Hooks: emit('RUN_CREATED', { runId, org, stackRef })
594
+
595
+ Dispatch->>Memory: 3. Memory snapshot (if AgentMemoryRepository exists)
596
+ Memory-->>Dispatch: memorySnapshot resource
597
+
598
+ alt Requires approval
599
+ Dispatch->>Dispatch: Create AgentDispatchRun (phase: AwaitingApproval)
600
+ Dispatch->>Approval: createApprovalRequest({ dispatchRun, action: 'secret-access' })
601
+ Dispatch->>Hooks: emit('APPROVAL_REQUESTED', { dispatchRun, action })
602
+ Dispatch-->>API: { run, approval, awaitingApproval: true }
603
+ end
604
+
605
+ Dispatch->>Workspace: 4. findReusableWorkspace({ org, repo, branch })
606
+ alt Reusable found
607
+ Workspace-->>Dispatch: claimWorkspace result
608
+ else No reusable
609
+ Dispatch->>Workspace: createWorkspace({ org, repo, branch })
610
+ Workspace-->>Dispatch: { workspace, pvcManifest }
611
+ end
612
+
613
+ Dispatch->>Context: 5. assembleContextBundle({ stack, repository, ref })
614
+ Context-->>Dispatch: contextBundle resource
615
+
616
+ Dispatch->>Dispatch: 6. Create AgentDispatchRun + AgentDispatchAttempt
617
+ Dispatch->>Dispatch: checkBudget({ org, model, estimatedTokens })
618
+ Note over Dispatch: Enforced via activeDeadlineSeconds + model-based cost tracking
619
+
620
+ Dispatch->>Dispatch: 7. createAgentJob(run, executionConfig)
621
+ Note over Dispatch: Generates batch/v1 Job manifest with image, command, env, volumes, resources, deadline
622
+ Dispatch->>K8s: submitAgentJob(jobManifest)
623
+ K8s-->>Dispatch: Job created (phase: Pending)
624
+ Dispatch->>Dispatch: run.status.phase = 'Running'
625
+ Dispatch->>Hooks: emit('STEP_STARTED', { runId, jobName })
626
+
627
+ K8s->>Pod: Schedule agent pod (workspace PVC mounted at /workspace)
628
+ Note over Pod: Env: AGENT_MUX_TRANSPORT, TRANSPORT_MUX_CODEC injected by resolveTransport()
629
+ Pod->>Pod: Execute agent session
630
+ Pod->>Callback: POST /api/orgs/:org/agents/runs/:name/callback (result)
631
+ Callback->>Dispatch: persistSessionEvent(runId, result)
632
+ Dispatch->>Dispatch: run.status.phase = 'Completed' | 'Failed'
633
+ Dispatch->>Hooks: emit('RUN_COMPLETED' | 'RUN_FAILED', { runId, result })
634
+
635
+ Dispatch-->>API: { run, attempt, contextBundle, workspace, jobName }
636
+ ```
637
+
638
+ ### 8.2 K8s Job Manifest Structure
639
+
640
+ `createAgentJob(run, executionConfig)` produces a `batch/v1` Job manifest with
641
+ the following structure:
642
+
643
+ ```javascript
644
+ {
645
+ apiVersion: 'batch/v1',
646
+ kind: 'Job',
647
+ metadata: {
648
+ name: `agent-${run.metadata.name}`,
649
+ namespace: run.metadata.namespace,
650
+ labels: {
651
+ 'kradle.a5c.ai/org': org,
652
+ 'kradle.a5c.ai/dispatch-run': run.metadata.name,
653
+ 'kradle.a5c.ai/agent-job': 'true'
654
+ }
655
+ },
656
+ spec: {
657
+ activeDeadlineSeconds: budgetDeadline, // budget enforcement
658
+ backoffLimit: 0, // no automatic retry (kradle handles retries)
659
+ template: {
660
+ spec: {
661
+ serviceAccountName: executionConfig.serviceAccountName,
662
+ restartPolicy: 'Never',
663
+ containers: [{
664
+ name: 'agent',
665
+ image: executionConfig.agentImage,
666
+ command: executionConfig.command,
667
+ env: [
668
+ { name: 'AGENT_MUX_TRANSPORT', value: resolvedTransport.transport },
669
+ { name: 'TRANSPORT_MUX_CODEC', value: resolvedTransport.codec },
670
+ { name: 'KRADLE_CALLBACK_URL', value: callbackUrl },
671
+ { name: 'KRADLE_RUN_ID', value: run.metadata.name },
672
+ // ...stack-specific env vars
673
+ ],
674
+ resources: executionConfig.resourceRequests,
675
+ volumeMounts: [{ name: 'workspace', mountPath: '/workspace' }]
676
+ }],
677
+ volumes: [{
678
+ name: 'workspace',
679
+ persistentVolumeClaim: { claimName: workspace.spec.pvcName }
680
+ }]
681
+ }
682
+ }
683
+ }
684
+ }
685
+ ```
686
+
687
+ ### 8.3 Transport Resolution and Codec Injection
688
+
689
+ `resolveTransport(stack, resources)` reads the `AgentTransportBinding` referenced
690
+ by the stack's adapter and produces the environment variables injected into the Job:
691
+
692
+ | Variable | Source | Example |
693
+ |----------|--------|---------|
694
+ | `AGENT_MUX_TRANSPORT` | `binding.spec.protocol` | `'websocket'` |
695
+ | `TRANSPORT_MUX_CODEC` | `binding.spec.codec ?? 'json'` | `'json'` |
696
+
697
+ The agent pod reads these variables at startup to configure its message framing
698
+ and connection lifecycle. No agent code changes are needed when switching transports.
699
+
700
+ ### 8.4 Budget Enforcement
701
+
702
+ `checkBudget({ org, model, estimatedTokens })` runs before Job creation:
703
+
704
+ 1. Load `AgentProviderConfig` for the stack's model/provider
705
+ 2. Compute `estimateCost(model, estimatedTokens)` using model-based rate tables
706
+ 3. Compare against `org.spec.budgetLimitUsd` or a default ceiling
707
+ 4. If over budget: return `{ allowed: false, reason: 'budget-exceeded' }` — dispatch aborted
708
+ 5. If within budget: set `activeDeadlineSeconds` on the Job spec proportional to the
709
+ remaining budget at the model's token rate
710
+
711
+ This ensures agent pods are automatically terminated by Kubernetes if they run
712
+ longer than the budget allows, even without an explicit callback.
713
+
714
+ ### 8.5 Callback-Based Result Collection
715
+
716
+ The agent pod calls `POST /api/orgs/:org/agents/runs/:name/callback` when the
717
+ session completes:
718
+
719
+ ```
720
+ POST /api/orgs/{org}/agents/runs/{name}/callback
721
+ Authorization: Bearer <kradle-run-token>
722
+ Content-Type: application/json
723
+
724
+ {
725
+ "phase": "Succeeded" | "Failed",
726
+ "exitCode": 0 | 1,
727
+ "artifacts": [...],
728
+ "costUsd": 0.042,
729
+ "errorMessage": "..." // present on failure
730
+ }
731
+ ```
732
+
733
+ `persistSessionEvent(runId, result)` applies the callback payload to the
734
+ `AgentDispatchRun` and `AgentSession` resources, then emits the appropriate
735
+ hooks lifecycle event.
736
+
737
+ ### 8.6 Hooks Lifecycle Events
738
+
739
+ `createHooksLifecycleEmitter(bus)` wraps the internal event bus and emits
740
+ 9 lifecycle events:
741
+
742
+ | Event | Trigger |
743
+ |-------|---------|
744
+ | `RUN_CREATED` | `createManualDispatch()` called |
745
+ | `RUN_QUEUED` | Run enters `Queued` phase (budget check pending) |
746
+ | `RUN_STARTED` | Job submitted to K8s, pod scheduled |
747
+ | `STEP_STARTED` | Agent pod reports a tool-use or reasoning step |
748
+ | `STEP_COMPLETED` | Agent pod reports step completion |
749
+ | `APPROVAL_REQUESTED` | `createApprovalRequest()` called |
750
+ | `APPROVAL_GRANTED` | `recordDecision()` with `Approved` |
751
+ | `APPROVAL_DENIED` | `recordDecision()` with `Denied` |
752
+ | `RUN_COMPLETED` | Callback received with `phase: Succeeded` |
753
+ | `RUN_FAILED` | Callback received with `phase: Failed`, or Job deadline exceeded |
754
+
755
+ These events flow to registered `WebhookSubscription` endpoints and the
756
+ notification system.
757
+
758
+ ### 8.7 Permission Review Steps
759
+
760
+ 1. Resolve AgentStack from resources via `resolveStack()`
761
+ 2. Validate approvalMode (yolo/prompt/deny)
762
+ 3. Cross-org denial: agent org vs repository org
763
+ 4. Expand capabilities from stack spec (tools, MCP, skills, subagents)
764
+ 5. Untrusted fork detection (`refs/pull/\d+/`)
765
+ 6. Check AgentServiceAccount binding
766
+ 7. Check AgentRoleBinding for subject
767
+ 8. Check AgentSecretGrant for agent
768
+ 9. Check AgentConfigGrant for agent
769
+ 10. Compute decision: `allowed`, `requires-approval`, or `denied`
770
+
771
+ ### 8.8 Decision Matrix
772
+
773
+ | approvalMode | Errors | Fork | Decision |
774
+ |-------------|--------|------|----------|
775
+ | `deny` | any | any | `denied` |
776
+ | `yolo` | none | false | `allowed` |
777
+ | `yolo` | none | true | `allowed` (warnings only) |
778
+ | `prompt` | none | false | `requires-approval` |
779
+ | `prompt` | none | true | `requires-approval` |
780
+ | any | has errors | any | `denied` |
781
+
782
+ ---
783
+
784
+ ## 9. External Sync Pipeline
785
+
786
+ ### 9.1 Complete Flow
787
+
788
+ ```mermaid
789
+ sequenceDiagram
790
+ participant Ext as External Provider (GitHub)
791
+ participant Ingress as POST /api/orgs/:org/agents/webhooks/ingest
792
+ participant HTTP as HTTP Handler
793
+ participant Normalize as normalizeWebhookEvent()
794
+ participant API as ApiController
795
+ participant Trigger as TriggerController
796
+ participant Webhook as WebhookController
797
+ participant Sync as SyncController
798
+ participant Conflict as ConflictController
799
+ participant Write as WriteController
800
+ participant K8s as Kubernetes
801
+
802
+ Ext->>Ingress: POST with X-Hub-Signature-256 header
803
+ Ingress->>HTTP: Match /api/orgs/:org/agents/webhooks/ingest
804
+ HTTP->>Normalize: normalizeWebhookEvent(body, org)
805
+ Note over Normalize: Pattern match: workflow_run, PR, comment, label, push
806
+ Normalize-->>HTTP: Canonical event { type, source, repository, ref, actor, payload }
807
+ HTTP->>API: processWebhookEvent({ event, organizationRef, namespace })
808
+ API->>API: snapshot()
809
+ API->>Trigger: createAgentTriggerController({ dispatchController })
810
+ API->>Trigger: processEvent({ event, resources, namespace, organizationRef })
811
+
812
+ Trigger->>Trigger: evaluateEvent — match event type against each rule's sources
813
+ loop For each matching rule
814
+ Trigger->>Trigger: Dedup check (existing TriggerExecution with same eventUid)
815
+ alt Not duplicate
816
+ Trigger->>Trigger: createTriggerExecution (phase: Dispatching)
817
+ Trigger->>Dispatch: createManualDispatch(...)
818
+ end
819
+ end
820
+ Trigger-->>API: { processed, dispatched, skipped, executions }
821
+ ```
822
+
823
+ ### 9.2 Webhook Event Normalization
824
+
825
+ Source: `normalizeWebhookEvent()` in `http-server.js`
826
+
827
+ | GitHub Action/Shape | Kradle Event Type | Source Kind |
828
+ |--------------------|-----------------|-------------|
829
+ | `completed` + `workflow_run.conclusion=failure` | `ci-failure` | Pipeline |
830
+ | `opened` + `pull_request` | `pr-opened` | PullRequest |
831
+ | `created` + `comment` | `comment` | Issue/PullRequest |
832
+ | `labeled` | `label-added` | Issue/PullRequest |
833
+ | `opened` + `issue` (no PR) | `issue-created` | Issue |
834
+ | `ref` + `commits` | `push` | Repository |
835
+ | (fallback) | `webhook` | WebhookDelivery |
836
+
837
+ ### 9.3 HMAC Verification
838
+
839
+ Source: `external/webhook-controller.js`
840
+
841
+ ```javascript
842
+ verifyHmacSignature(body, signature) {
843
+ // 1. Reject if no signature header
844
+ // 2. Reject if not prefixed with 'sha256='
845
+ // 3. Compute expected: 'sha256=' + createHmac('sha256', secret).update(body).digest('hex')
846
+ // 4. timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
847
+ // 5. Return { valid: true/false, reason }
848
+ }
849
+ ```
850
+
851
+ ### 9.4 Sync Controller Ownership Modes
852
+
853
+ | Mode | Kradle Writes | External Writes |
854
+ |------|-------------|-----------------|
855
+ | `bidirectional` | Allowed | Allowed |
856
+ | `external-owned` | Blocked | Allowed |
857
+ | `kradle-owned` | Allowed | Blocked |
858
+
859
+ ### 9.5 Watermark Tracking
860
+
861
+ - Per-binding watermark stored as ISO timestamp
862
+ - Only advances forward (new timestamp must be > current)
863
+ - Persisted as `ExternalSyncWatermark` CRD resource
864
+
865
+ ---
866
+
867
+ ## 10. Memory Query Pipeline
868
+
869
+ ### 10.1 From Search Form to Results
870
+
871
+ ```mermaid
872
+ sequenceDiagram
873
+ participant UI as Memory Search Form
874
+ participant HTTP as HTTP Handler
875
+ participant API as ApiController
876
+ participant Memory as MemoryController
877
+ participant QueryEngine as queryMemory()
878
+
879
+ UI->>HTTP: POST /api/orgs/:org/agents/memory/query { query, mode, kinds, depth }
880
+ HTTP->>API: queryAgentMemory({ query, mode, ... })
881
+ API->>Memory: queryMemory({ query, mode, organizationRef })
882
+ Memory->>QueryEngine: queryMemory({ records, documents, edges, query, mode })
883
+
884
+ alt mode = 'graph-only'
885
+ QueryEngine->>QueryEngine: queryGraph({ records, edges, query, kinds, depth })
886
+ Note over QueryEngine: buildAdjacency → filter by nodeKind → score → follow edges → sort
887
+ else mode = 'grep-only'
888
+ QueryEngine->>QueryEngine: queryGrep({ documents, query, paths, context })
889
+ Note over QueryEngine: filter by glob → line-by-line search → extract context
890
+ else mode = 'graph-and-grep'
891
+ QueryEngine->>QueryEngine: queryGraph + queryGrep (both)
892
+ end
893
+
894
+ QueryEngine-->>Memory: { graph, grep, stats }
895
+ Memory-->>API: result
896
+ API-->>HTTP: JSON response
897
+ HTTP-->>UI: { graph: { matches, totalMatches }, grep: { excerpts, totalMatches } }
898
+ ```
899
+
900
+ ### 10.2 Graph Scoring Algorithm
901
+
902
+ ```javascript
903
+ function scoreRecord(record, lowerQuery) {
904
+ const id = String(record.id || '').toLowerCase();
905
+ const attrs = JSON.stringify(record.attributes || {}).toLowerCase();
906
+ if (id.includes(lowerQuery)) return 2; // ID match: higher priority
907
+ if (attrs.includes(lowerQuery)) return 1; // Attribute match
908
+ return 0; // No match
909
+ }
910
+ ```
911
+
912
+ ### 10.3 Edge Traversal (BFS)
913
+
914
+ ```javascript
915
+ function followEdges(startId, adjacency, maxDepth) {
916
+ // BFS from startId up to maxDepth hops
917
+ // visited Set prevents cycles
918
+ // Returns flat array of all encountered edges
919
+ }
920
+ ```
921
+
922
+ ### 10.4 Grep Highlighting
923
+
924
+ Match output format:
925
+ ```javascript
926
+ {
927
+ path: 'docs/design.md',
928
+ lineNumber: 42,
929
+ line: 'The agent memory stores knowledge graphs...',
930
+ highlighted: 'The agent **memory** stores knowledge graphs...',
931
+ context: '...\nThe agent memory stores knowledge graphs...\n...',
932
+ contextStart: 41,
933
+ contextEnd: 43
934
+ }
935
+ ```
936
+
937
+ ---
938
+
939
+ ## 11. Workspace Provisioning
940
+
941
+ ### 11.1 PVC-Based Provisioning
942
+
943
+ Source: `packages/kradle/core/src/agent-workspace-controller.js`
944
+
945
+ ```mermaid
946
+ flowchart TD
947
+ A[Dispatch trigger] --> B{Find reusable workspace?}
948
+ B -->|Yes: same repo+branch+Ready| C[claimWorkspace]
949
+ B -->|No| D[createWorkspace]
950
+
951
+ C --> E[Mark phase=InUse, set runRef]
952
+ D --> F[Generate workspace name]
953
+ F --> G[Generate PVC manifest]
954
+ G --> H[Create KradleWorkspace resource]
955
+
956
+ E --> I[getMountSpec]
957
+ H --> I
958
+
959
+ I --> J[Return { volume, volumeMount }]
960
+ J --> K[Attach to AgentDispatchRun.spec.mountSpec]
961
+ ```
962
+
963
+ ### 11.2 PVC Manifest Structure
964
+
965
+ ```javascript
966
+ {
967
+ apiVersion: 'v1',
968
+ kind: 'PersistentVolumeClaim',
969
+ metadata: {
970
+ name: 'kradle-ws-<workspace-name>',
971
+ namespace: '<org-namespace>',
972
+ labels: {
973
+ 'kradle.a5c.ai/workspace': '<workspace-name>',
974
+ 'kradle.a5c.ai/org': '<org>'
975
+ }
976
+ },
977
+ spec: {
978
+ storageClassName: 'standard', // configurable via volumeSpec.storageClassName
979
+ accessModes: ['ReadWriteOnce'], // configurable
980
+ resources: { requests: { storage: '10Gi' } } // configurable via volumeSpec.capacity
981
+ }
982
+ }
983
+ ```
984
+
985
+ ### 11.3 Codespace Pod Spec
986
+
987
+ When `launchCodespace()` is called:
988
+ - Image: `codercom/code-server:latest` (configurable)
989
+ - CPU: 1 core limit, 250m request
990
+ - Memory: 2Gi limit, 512Mi request
991
+ - Port: 8080
992
+ - Volume: PVC mount at `/workspace`
993
+ - Env: `KRADLE_WORKSPACE`, `KRADLE_ORG`, `GIT_AUTHOR_NAME`, `GIT_AUTHOR_EMAIL`
994
+ - Service: ClusterIP on port 8080
995
+ - URL pattern: `http://codespace-svc-<ws>.<namespace>.svc.cluster.local:8080`
996
+
997
+ ### 11.4 Workspace Phase Transitions
998
+
999
+ ```
1000
+ Pending → Ready → InUse → Ready (release)
1001
+ → Archived (archive)
1002
+ → Terminating (delete)
1003
+ Archived → Active (recover)
1004
+ ```
1005
+
1006
+ ---
1007
+
1008
+ ## 12. Notification Pipeline
1009
+
1010
+ Source: `packages/kradle/core/src/notification-controller.js`
1011
+
1012
+ ### 12.1 Event-to-Notification Mapping
1013
+
1014
+ | Source Event Type | Notification Type | Severity |
1015
+ |-------------------|------------------|----------|
1016
+ | `AgentDispatchRun` (completed) | `run-complete` | info |
1017
+ | `AgentDispatchRun` (failed) | `run-complete` | error |
1018
+ | `AgentApproval` (pending) | `approval-needed` | warning |
1019
+ | `ExternalSyncConflict` | `conflict-detected` | warning |
1020
+ | `KradleWorkspace` (claimed) | `workspace-ready` | info |
1021
+ | (default) | `system` | info |
1022
+
1023
+ ### 12.2 Notification Delivery Flow
1024
+
1025
+ ```mermaid
1026
+ sequenceDiagram
1027
+ participant Controller as Any Controller
1028
+ participant NotifCtrl as NotificationController
1029
+ participant Store as In-Memory Store (Map)
1030
+ participant Bus as EventBus
1031
+ participant SSE as SSE Stream
1032
+ participant Bell as NotificationBell
1033
+
1034
+ Controller->>NotifCtrl: createNotification(event)
1035
+ NotifCtrl->>NotifCtrl: mapEventToNotification(event)
1036
+ NotifCtrl->>Store: store.get(org).push(notification)
1037
+ NotifCtrl->>Bus: emit({ type: 'notification', ... })
1038
+ Bus->>SSE: Forward to all subscribers
1039
+ SSE->>Bell: EventSource receives
1040
+ Bell->>Bell: Increment unread count badge
1041
+ ```
1042
+
1043
+ ### 12.3 User Preferences
1044
+
1045
+ Default preferences:
1046
+ ```javascript
1047
+ { runs: true, approvals: true, conflicts: true, workspaces: true, sound: false, desktop: false }
1048
+ ```
1049
+
1050
+ ---
1051
+
1052
+ ## 13. Event Bus and SSE Streaming
1053
+
1054
+ ### 13.1 Event Bus Implementation
1055
+
1056
+ Source: `packages/kradle/core/src/event-bus.js`
1057
+
1058
+ - Uses a `Set<Function>` for listeners (O(1) add/remove)
1059
+ - `emit(event)` iterates all listeners synchronously
1060
+ - `emitResourceChange(kind, name, operation)` adds timestamp
1061
+ - Global singleton: `globalEventBus`
1062
+
1063
+ ### 13.2 SSE Endpoint
1064
+
1065
+ Route: `GET /api/orgs/:org/agents/events/stream`
1066
+
1067
+ Response headers:
1068
+ ```
1069
+ Content-Type: text/event-stream
1070
+ Cache-Control: no-cache
1071
+ Connection: keep-alive
1072
+ X-Accel-Buffering: no
1073
+ ```
1074
+
1075
+ Protocol:
1076
+ 1. Initial: `data: {"type":"connected"}\n\n`
1077
+ 2. Every 30s: `data: {"type":"heartbeat"}\n\n`
1078
+ 3. On resource change: `data: {"type":"resource-change","kind":"...","name":"...","operation":"apply","timestamp":"..."}\n\n`
1079
+ 4. On client disconnect: `clearInterval(heartbeat)`, `globalEventBus.unsubscribe(writer)`
1080
+
1081
+ ---
1082
+
1083
+ ## 14. Async Utilities
1084
+
1085
+ Source: `packages/kradle/core/src/async-controller.js`
1086
+
1087
+ ### 14.1 Event Batcher
1088
+
1089
+ ```javascript
1090
+ createEventBatcher(handler, { maxBatchSize: 50, flushIntervalMs: 1000 })
1091
+ ```
1092
+
1093
+ Behavior:
1094
+ - Accumulates events in array
1095
+ - Flushes when `batch.length >= maxBatchSize` (fire-and-forget)
1096
+ - Flushes on timer (setTimeout) when batch has items but below threshold
1097
+ - `flush()` forces immediate flush (awaitable)
1098
+ - `stop()` clears timer and buffer
1099
+
1100
+ ### 14.2 Retry Policy
1101
+
1102
+ ```javascript
1103
+ createRetryPolicy({ maxRetries: 3, baseDelayMs: 1000, maxDelayMs: 30000, jitter: true })
1104
+ ```
1105
+
1106
+ Delay formula: `min(baseDelayMs * 2^attempt, maxDelayMs)` with optional full-jitter `[0, capped]`
1107
+
1108
+ ### 14.3 Delivery Queue
1109
+
1110
+ ```javascript
1111
+ createDeliveryQueue(processor, { concurrency: 5, retryPolicy })
1112
+ ```
1113
+
1114
+ - In-memory ordered queue
1115
+ - Up to `concurrency` items processed in parallel
1116
+ - Each item retried per retryPolicy on failure
1117
+ - `drain()` returns Promise that resolves when queue is empty and all active items complete
1118
+ - `stop()` clears queue and resolves all drain waiters
1119
+
1120
+ ### 14.4 Checkpointer
1121
+
1122
+ ```javascript
1123
+ createCheckpointer(storage = new Map())
1124
+ ```
1125
+
1126
+ Simple key-value store: `save(key, value)`, `load(key)`, `clear(key)`, `listKeys()`
1127
+
1128
+ ---
1129
+
1130
+ ## 15. Controller Boundary Declarations
1131
+
1132
+ Every controller exports a frozen boundary object. This serves as both documentation and runtime introspection.
1133
+
1134
+ | Controller | Source File | Role | Owns | Must Not Own |
1135
+ |-----------|-------------|------|------|--------------|
1136
+ | KubernetesResourceClient | `kubernetes-controller.js` | kubectl execution | command exec, API discovery, access checks, watch streams | HTTP routes, pages, forge DTOs |
1137
+ | KradleKubernetesReconciler | `kubernetes-controller.js` | Resource reconciliation | repo status, identity projection, hosting intent, policy sync | HTTP routes, pages, API DTOs |
1138
+ | KubernetesResourceGateway | `kubernetes-resource-gateway.js` | API port delegation | resource definitions, CRUD delegation, namespace scoping | HTTP routes, page flows, reconciliation |
1139
+ | KradleApiController | `api-controller.js` | HTTP facade | validation, DTOs, errors, workflow affordances, UI snapshots | kubectl execution, reconciliation loops |
1140
+ | AgentStackController | `agent-stack-controller.js` | Stack readiness | capability resolution, conditions, readiness, MCP health | secrets, dispatch execution, Mux sessions |
1141
+ | AgentDispatchController | `agent-dispatch-controller.js` | Dispatch orchestration | dispatch creation, attempt lifecycle, session binding, workspace | secrets, UI rendering |
1142
+ | AgentWorkspaceController | `agent-workspace-controller.js` | Workspace provisioning | workspace creation, PVC gen, git specs, mount specs, reuse, codespace | git execution, K8s API, secrets |
1143
+ | AgentTriggerController | `agent-trigger-controller.js` | Event routing | normalization, rule matching, trigger records, dispatch initiation | event sourcing, webhook delivery |
1144
+ | AgentApprovalController | `agent-approval-controller.js` | Approval gates | approval creation, decision recording, lookup, dedup | secrets, agent execution, UI |
1145
+ | AgentMemoryQuery | `agent-memory-query.js` | Query execution | graph traversal, filtering, scoring, grep, context extraction | persistence, HTTP, K8s, secrets |
1146
+ | WebhookController | `external/webhook-controller.js` | Inbound webhooks | HMAC validation, delivery records, dedup, event queue | resource persistence, ownership |
1147
+ | SyncController | `external/sync-controller.js` | External sync | normalization, upsert, watermarks, ownership, tombstones | HMAC, webhook delivery |
1148
+ | ConflictController | `external/conflict-controller.js` | Conflict detection | detection, resolution, superseded cleanup | write intent, sync scheduling |
1149
+ | WriteController | `external/write-controller.js` | Write intents | creation, approval gate, retry, idempotency | conflict resolution, sync state |
1150
+ | AuditController | `audit-controller.js` | Audit log | event recording, streaming, replay, metrics | identity, storage, git |
1151
+ | RunnerController | `runner-controller.js` | Runner pools | pool validation, lifecycle, scheduling, pod specs, capacity | K8s API calls, actual pod creation |
1152
+ | NotificationController | `notification-controller.js` | Notifications | creation, listing, read state, preferences | event dispatch, UI rendering, push |
1153
+ | PermissionReviewer | `agent-permission-review.js` | Permission review | capability expansion, grant resolution, snapshot creation | secrets, K8s API, runtime execution |
1154
+
1155
+ ---
1156
+
1157
+ ## 16. Concurrency Model
1158
+
1159
+ ### 16.1 Single-Threaded Event Loop
1160
+
1161
+ Kradle core runs on Node.js's single-threaded event loop:
1162
+ - All kubectl calls use `spawnSync` (blocking) during snapshot collection
1163
+ - API request handling is async (Node HTTP server)
1164
+ - Background revalidation uses `Promise.resolve().then(...)` (microtask)
1165
+ - No worker threads or clustering in the core package
1166
+
1167
+ ### 16.2 Concurrent Access Patterns
1168
+
1169
+ | Pattern | Mechanism |
1170
+ |---------|-----------|
1171
+ | Multiple orgs cached | Per-org Map entries, independent TTLs |
1172
+ | SSE connections | Set of listener functions, one per connection |
1173
+ | Background revalidation | `revalidating` flag prevents thundering herd |
1174
+ | Event bus | Synchronous iteration over Set (no races) |
1175
+ | Audit store | Append-only array, seq counter |
1176
+ | Notification store | Per-org array, no locking needed |
1177
+
1178
+ ---
1179
+
1180
+ ## 17. Error Handling Strategy
1181
+
1182
+ ### 17.1 HTTP Layer
1183
+
1184
+ ```javascript
1185
+ try {
1186
+ // Route matching and handler execution
1187
+ } catch (error) {
1188
+ return send(response, 400, { error: 'bad_request', message: error.message });
1189
+ }
1190
+ ```
1191
+
1192
+ All unhandled errors in route handlers become 400 responses.
1193
+
1194
+ ### 17.2 Controller Layer
1195
+
1196
+ Controllers return error objects instead of throwing:
1197
+ ```javascript
1198
+ { error: true, reason: 'stack-not-found', message: 'AgentStack not found' }
1199
+ ```
1200
+
1201
+ ### 17.3 kubectl Layer
1202
+
1203
+ - `allowFailure: true` — returns `{ ok: false }`, caller decides
1204
+ - `allowFailure: false` — throws Error with `commandFailure()` message
1205
+
1206
+ ### 17.4 Audit Event Failures
1207
+
1208
+ ```javascript
1209
+ function emitAuditEvent(resource, operation) {
1210
+ try { ... } catch { /* Audit failures must not crash apply operations */ }
1211
+ }
1212
+ ```
1213
+
1214
+ ### 17.5 Background Revalidation Failures
1215
+
1216
+ ```javascript
1217
+ try { const fresh = await revalidateFn(); ... }
1218
+ catch { orgCacheMap.set(key, { ...current, revalidating: false }); }
1219
+ ```
1220
+
1221
+ ---
1222
+
1223
+ ## 18. Deployment Architecture
1224
+
1225
+ ### 18.1 Container Topology
1226
+
1227
+ | Container | Port | Role |
1228
+ |-----------|------|------|
1229
+ | api | 3080 | HTTP API server (`kradle serve`) |
1230
+ | controllers | — | Background reconciliation (future) |
1231
+ | web | 3000 | Next.js web console |
1232
+ | webhook-worker | — | Inbound webhook processing |
1233
+
1234
+ ### 18.2 CRD Management
1235
+
1236
+ - 76 CRDs under `kradle.a5c.ai/v1alpha1`
1237
+ - All use `x-kubernetes-preserve-unknown-fields: true`
1238
+ - All namespaced
1239
+ - Platform resources (Organization, OrgNamespaceBinding) in `kradle-system`
1240
+ - Org resources in `kradle-org-<slug>` namespaces
1241
+
1242
+ ### 18.3 Infrastructure Requirements
1243
+
1244
+ | Component | Purpose |
1245
+ |-----------|---------|
1246
+ | AKS (or compatible K8s) | Container orchestration |
1247
+ | ACR (or registry) | Image storage |
1248
+ | cert-manager | TLS provisioning |
1249
+ | nginx ingress | HTTP routing |
1250
+ | PostgreSQL | Aggregated resource storage |
1251
+ | Gitea | Git hosting backend |
1252
+ | Kyverno (optional) | Policy engine |
1253
+ | KubeVela (optional) | Application delivery |
1254
+
1255
+ ---
1256
+
1257
+ ## 19. Data Storage Boundaries
1258
+
1259
+ | Storage Backend | Resource Count | Access Pattern |
1260
+ |----------------|---------------|----------------|
1261
+ | etcd (CRDs) | 44 CONFIG kinds | kubectl get/apply/delete |
1262
+ | PostgreSQL | 32 AGGREGATED kinds | In-memory during dev, runtime queries |
1263
+ | Gitea | Repository content | HTTP API, SSH |
1264
+ | In-memory | Notifications, audit, runners | Per-process, non-persistent |
1265
+ | Snapshot cache | Derived views | Stale-while-revalidate |
1266
+
1267
+ ---
1268
+
1269
+ ## 20. Configuration Reference
1270
+
1271
+ ### 20.1 Core Server
1272
+
1273
+ | Variable | Default | Purpose |
1274
+ |----------|---------|---------|
1275
+ | `KRADLE_NAMESPACE` | `kradle-system` | Platform namespace |
1276
+ | `KRADLE_ORG` | `default` | Default organization |
1277
+ | `KRADLE_SNAPSHOT_CACHE_TTL_MS` | `30000` | Cache freshness TTL |
1278
+ | `KRADLE_GITEA_HTTP_URL` | (none) | Gitea API base URL |
1279
+
1280
+ ### 20.2 External Integrations
1281
+
1282
+ | Variable | Default | Purpose |
1283
+ |----------|---------|---------|
1284
+ | `KRADLE_KYVERNO_MODE` | auto | Kyverno integration mode |
1285
+ | `KRADLE_KYVERNO_ENABLED` | (none) | Enable BYO Kyverno |
1286
+ | `KRADLE_KYVERNO_NAMESPACE` | `kyverno` | Kyverno deployment namespace |
1287
+ | `KRADLE_KYVERNO_POLICY_NAMESPACE` | platform ns | Policy storage namespace |
1288
+ | `KRADLE_KUBEVELA_NAMESPACE` | `vela-system` | KubeVela system namespace |
1289
+
1290
+ ### 20.3 Runtime Identity
1291
+
1292
+ | Variable | Default | Purpose |
1293
+ |----------|---------|---------|
1294
+ | `KRADLE_SERVICE_ACCOUNT_DIR` | `/var/run/secrets/kubernetes.io/serviceaccount` | SA mount |
1295
+ | `KRADLE_SERVICE_ACCOUNT_TOKEN` | `<SA_DIR>/token` | Token file path |
1296
+ | `KRADLE_SERVICE_ACCOUNT_CA` | `<SA_DIR>/ca.crt` | CA cert path |
1297
+
1298
+ ---
1299
+
1300
+ ## 21. Resource Reconciliation Deep Dive
1301
+
1302
+ > Source: `packages/kradle/core/src/kubernetes-controller.js`
1303
+
1304
+ ### 21.1 KRADLE_RESOURCES Array
1305
+
1306
+ The `KRADLE_RESOURCES` array (exported at module level) defines every resource the Kradle control plane manages. Each entry carries the following fields:
1307
+
1308
+ | Field | Type | Meaning |
1309
+ |-------|------|---------|
1310
+ | `kind` | string | PascalCase K8s kind (e.g. `'Organization'`) |
1311
+ | `plural` | string | Lowercase plural used in kubectl (e.g. `'organizations'`) |
1312
+ | `group` | string? | API group. Defaults to `kradle.a5c.ai` when absent. KubeVela uses `core.oam.dev`, core K8s uses `''`. |
1313
+ | `namespaced` | boolean | Whether the resource lives in a namespace (`true`) or is cluster-scoped (`false`) |
1314
+ | `namespace` | string? | Fixed namespace override (e.g. `'kradle-system'` for platform resources, `'vela-system'` for KubeVela defs) |
1315
+ | `storage` | string | Backend store: `'etcd'`, `'postgres'`, `'kubevela'`, `'kyverno'`, `'kyverno-reports'`, or `'core'` |
1316
+ | `platformScoped` | boolean? | When `true`, listed only from the platform namespace — not from per-org namespaces |
1317
+
1318
+ **Resource categories and counts:**
1319
+
1320
+ | Storage | Count | Examples |
1321
+ |---------|-------|----------|
1322
+ | etcd (Kradle CRDs) | 46 | Organization, User, Team, Repository, AgentStack, AgentSubagent, AgentToolProfile, AgentMcpServer, AgentSkill, AgentTriggerRule, AgentContextLabel, KradleWorkspacePolicy, AgentServiceAccount, AgentRoleBinding, AgentSecretGrant, AgentConfigGrant, AgentAdapter, AgentTransportBinding, AgentProviderConfig, KradleProject, AgentGatewayConfig, AgentMemoryRepository, AgentMemorySource, AgentMemoryOntology, AgentMemoryAssociation, ExternalBackendProvider, ExternalBackendBinding, ExternalBackendSyncPolicy |
1323
+ | postgres (aggregated) | 13 | PullRequest, Issue, Review, Pipeline, Job, WebhookDelivery, AgentDispatchRun, AgentDispatchAttempt, AgentSession, AgentContextBundle, KradleArtifact, AgentApproval, KradleWorkspace, AgentTriggerExecution, KradleWorkspaceRuntime, AgentSessionTranscript |
1324
+ | kubevela | 11 | KubeVelaApplication, KubeVelaApplicationRevision, KubeVelaComponentDefinition, KubeVelaWorkloadDefinition, KubeVelaTraitDefinition, KubeVelaScopeDefinition, KubeVelaPolicyDefinition, KubeVelaPolicy, KubeVelaWorkflowStepDefinition, KubeVelaWorkflow, KubeVelaResourceTracker |
1325
+ | kyverno / kyverno-reports | 10 | KyvernoPolicy, KyvernoClusterPolicy, KyvernoValidatingPolicy, KyvernoMutatingPolicy, KyvernoGeneratingPolicy, KyvernoDeletingPolicy, KyvernoImageValidatingPolicy, KyvernoPolicyException, PolicyReport, ClusterPolicyReport |
1326
+ | core (K8s built-in) | 2 | Secret, ConfigMap — excluded from snapshot, accessed on-demand |
1327
+
1328
+ **Platform-scoped definitions** (only listed from `kradle-system`):
1329
+ - `Organization` (namespace: `KRADLE_PLATFORM_NAMESPACE`)
1330
+ - `OrgNamespaceBinding` (namespace: `KRADLE_PLATFORM_NAMESPACE`)
1331
+
1332
+ ### 21.2 getControllerSnapshot() — Step-by-Step
1333
+
1334
+ `getControllerSnapshot(options)` is the synchronous entrypoint (uses `spawnSync`) that produces the full cluster state snapshot.
1335
+
1336
+ #### Step 1: currentContextResult()
1337
+
1338
+ ```javascript
1339
+ function currentContextResult(options) {
1340
+ const inCluster = inClusterKubectlConfig(options.env);
1341
+ if (inCluster) return { ok: true, stdout: `${inCluster.context}\n`, ... };
1342
+ return runKubectl(['config', 'current-context'], { ...options, allowFailure: true });
1343
+ }
1344
+ ```
1345
+
1346
+ Checks for in-cluster mode first (via `KUBERNETES_SERVICE_HOST` + service account files at `/var/run/secrets/kubernetes.io/serviceaccount/`). If found, returns synthetic result with context `'in-cluster'`. Otherwise runs `kubectl config current-context`.
1347
+
1348
+ **Failure mode:** Returns `{ ok: false }` — snapshot proceeds to build a degraded response with `kubectl.available: false`.
1349
+
1350
+ #### Step 2: versionResult
1351
+
1352
+ ```javascript
1353
+ runKubectl(['version', '--client=true', '-o', 'json'], { allowFailure: true })
1354
+ ```
1355
+
1356
+ Extracts `clientVersion.gitVersion` from JSON output. If both context and version fail, the snapshot is returned early with empty resource maps and `kubectl.available: false`.
1357
+
1358
+ #### Step 3: CRD Discovery Loop
1359
+
1360
+ ```javascript
1361
+ const crdResult = runKubectl(['get', 'crd', '-o', 'json'], { allowFailure: true });
1362
+ const discoveredCrds = crdResult.ok
1363
+ ? parseKubernetesList(crdResult.stdout).items.filter((crd) =>
1364
+ [KRADLE_API_GROUP, KUBEVELA_API_GROUP].includes(crd.spec?.group) ||
1365
+ KYVERNO_DISCOVERY_GROUPS.has(crd.spec?.group))
1366
+ : [];
1367
+ const discoveredPluralSet = new Set(
1368
+ discoveredCrds.map((crd) => `${crd.spec?.group}/${crd.spec?.names?.plural}`)
1369
+ );
1370
+ ```
1371
+
1372
+ Queries ALL cluster CRDs, then filters to only those belonging to `kradle.a5c.ai`, `core.oam.dev`, `kyverno.io`, `policies.kyverno.io`, or `wgpolicyk8s.io`. The resulting `discoveredPluralSet` is used to decide which resources to actually query — avoids 404s for uninstalled CRDs.
1373
+
1374
+ #### Step 4: platformScopedDefinitions vs orgScopedDefinitions
1375
+
1376
+ ```javascript
1377
+ const platformScopedDefinitions = snapshotResources.filter((d) => d.platformScoped);
1378
+ const orgScopedDefinitions = snapshotResources.filter((d) => !d.platformScoped);
1379
+ ```
1380
+
1381
+ Platform-scoped resources (Organization, OrgNamespaceBinding) are listed first from their fixed namespace. This is required because org namespaces are derived from the Organization/OrgNamespaceBinding resources.
1382
+
1383
+ #### Step 5: organizationNamespaces() — Fallback Chain
1384
+
1385
+ ```javascript
1386
+ function organizationNamespaces(organizations, bindings, fallbackNamespace) {
1387
+ // 1. Extract spec.namespaceName from Organization items
1388
+ // 2. Extract spec.namespace from OrgNamespaceBinding items
1389
+ // 3. Deduplicate into a Set
1390
+ // If non-empty → return those namespaces
1391
+ // 4. Fallback: KRADLE_ADMIN_ORG → orgNamespaceName(adminOrg)
1392
+ // 5. Fallback: KRADLE_ORG || 'default' → orgNamespaceName(defaultOrg)
1393
+ // 6. Final fallback: platformNamespace itself
1394
+ }
1395
+ ```
1396
+
1397
+ The `orgNamespaceName(org)` function generates `kradle-org-${slug}` from the org slug.
1398
+
1399
+ #### Step 6: Parallel Org-Scoped Resource Listing
1400
+
1401
+ For each org-scoped definition:
1402
+ 1. Skip if `shouldListSnapshotDefinition()` returns false (CRD not discovered and not kradle.a5c.ai group)
1403
+ 2. Compute target namespaces: fixed namespace → use it alone; otherwise → all org namespaces
1404
+ 3. For each namespace, run `kubectl get <plural>.<group> -n <ns> -o json --ignore-not-found`
1405
+ 4. Flatten all items into `resources[definition.kind]`
1406
+
1407
+ #### Step 7: Event Collection
1408
+
1409
+ ```javascript
1410
+ runKubectl(['get', 'events', '-n', namespace, '-o', 'json', '--ignore-not-found'], { allowFailure: true })
1411
+ ```
1412
+
1413
+ Collects Kubernetes events from the platform namespace only (not org namespaces).
1414
+
1415
+ #### Step 8: Permission Matrix (canI Checks)
1416
+
1417
+ ```javascript
1418
+ const permissions = await Promise.all(
1419
+ snapshotResources
1420
+ .filter((d) => discoveredPluralSet.has(`${d.group || KRADLE_API_GROUP}/${d.plural}`))
1421
+ .map(async (d) => ({
1422
+ kind: d.kind,
1423
+ plural: d.plural,
1424
+ verbs: Object.fromEntries(
1425
+ ['get', 'list', 'watch', 'create', 'update', 'patch', 'delete']
1426
+ .map((verb) => [verb, canI(verb, d, { kubectl, namespace, timeoutMs, env })])
1427
+ )
1428
+ }))
1429
+ );
1430
+ ```
1431
+
1432
+ For every discovered CRD, runs `kubectl auth can-i <verb> <plural>.<group> -n <ns>` for all 7 standard verbs. Result is `true`/`false` per verb.
1433
+
1434
+ #### Step 9: Kyverno Discovery
1435
+
1436
+ `discoverKyverno()` is called with the discoveredPluralSet. It:
1437
+ 1. Filters `KYVERNO_RESOURCES` to only those with discovered CRDs
1438
+ 2. Lists each Kyverno resource from the Kyverno policy namespace
1439
+ 3. Lists Kyverno controller deployments from the kyverno namespace
1440
+ 4. Runs `canI` checks for Kyverno resources
1441
+ 5. Extracts policy reports and violations
1442
+ 6. Reports degraded conditions if CRDs exist but controllers are missing
1443
+
1444
+ #### Step 10: Return Shape
1445
+
1446
+ ```typescript
1447
+ {
1448
+ source: 'kubernetes',
1449
+ mode: 'kubernetes-api',
1450
+ namespace: string, // Platform namespace
1451
+ generatedAt: string, // ISO timestamp
1452
+ correlationId: string, // UUID for request correlation
1453
+ kubectl: {
1454
+ binary: string, // kubectl path
1455
+ context: string | null, // Current context name
1456
+ clientVersion: string | null, // e.g. 'v1.28.3'
1457
+ available: boolean, // true if both context + version succeeded
1458
+ errors: string[] // Command failure messages
1459
+ },
1460
+ apiService: object | null, // Raw APIService JSON for kradle API
1461
+ crds: object[], // Discovered Kradle/KubeVela/Kyverno CRDs
1462
+ resources: Record<string, object[]>, // Map: kind → items array
1463
+ kyverno: object, // Full Kyverno discovery state
1464
+ events: object[], // K8s events from platform namespace
1465
+ permissions: object[], // Per-kind verb permission map
1466
+ storage: object, // Storage boundary descriptions
1467
+ commands: object[] // Generated kubectl commands per kind
1468
+ }
1469
+ ```
1470
+
1471
+ ---
1472
+
1473
+ ## 22. Controller-UI Model Construction
1474
+
1475
+ > Source: `packages/kradle/core/src/controller-ui.js`
1476
+
1477
+ ### 22.1 createControllerUiModel(source, options)
1478
+
1479
+ Transforms a raw Kubernetes snapshot into a UI-ready model consumed by the web console.
1480
+
1481
+ **Parameters:**
1482
+ - `source` — Raw snapshot object (from `getControllerSnapshot()`) or a controller with `.snapshot()` method
1483
+ - `options.organization` / `options.org` — Requested org slug
1484
+
1485
+ **Pipeline:**
1486
+
1487
+ ```
1488
+ normalizeSnapshot(source)
1489
+ → ensureOrganizations(snapshot.resources.Organization)
1490
+ → resolve activeOrg (by slug match or first)
1491
+ → filterResourceItemsForOrg() per kind
1492
+ → assemble domain views (agent, delivery, policy, identity)
1493
+ → compute metrics
1494
+ → format events
1495
+ → build validation checks
1496
+ → return full model
1497
+ ```
1498
+
1499
+ ### 22.2 Organization Resolution
1500
+
1501
+ ```javascript
1502
+ function ensureOrganizations(organizations, platformNamespace) {
1503
+ if (organizations.length) return organizations.map((org) => ({
1504
+ name: org.metadata?.name,
1505
+ slug: org.spec?.slug || org.metadata?.name,
1506
+ displayName: org.spec?.displayName || slug,
1507
+ namespace: org.spec?.namespaceName || orgNamespaceName(slug),
1508
+ platformNamespace
1509
+ }));
1510
+ // Fallback: synthesize a 'default' org
1511
+ return [{ name: 'default', slug: 'default', displayName: 'Default org',
1512
+ namespace: 'kradle-org-default', platformNamespace }];
1513
+ }
1514
+ ```
1515
+
1516
+ Active org is selected by matching `requestedOrg` against slug or name, falling back to `organizations[0]`.
1517
+
1518
+ ### 22.3 Resource Filtering by Org
1519
+
1520
+ ```javascript
1521
+ function filterResourceItemsForOrg(definition, items, org) {
1522
+ if (definition.kind === 'Organization') → filter by spec.slug match
1523
+ if (definition.kind === 'OrgNamespaceBinding') → filterByOrg (label/ref match)
1524
+ if (definition.namespace && !== orgNamespaceName(org)) → return all (system-level)
1525
+ default → filterByOrg (label/ref match)
1526
+ }
1527
+
1528
+ function filterByOrg(items, org) {
1529
+ const orgNamespace = orgNamespaceName(org);
1530
+ return items.filter((item) =>
1531
+ item.spec?.organizationRef === org ||
1532
+ item.metadata?.labels?.['kradle.a5c.ai/org'] === org ||
1533
+ item.metadata?.namespace === orgNamespace
1534
+ );
1535
+ }
1536
+ ```
1537
+
1538
+ ### 22.4 Agent View Assembly
1539
+
1540
+ The `agentView` object is constructed from 14+ filtered resource arrays:
1541
+
1542
+ ```javascript
1543
+ const agentView = {
1544
+ org: activeOrg?.slug,
1545
+ stacks: { count, items: AgentStack[] },
1546
+ runs: { count, items: AgentDispatchRun[], active: [...non-terminal] },
1547
+ rules: { count, items: AgentTriggerRule[] },
1548
+ sessions: { count, items: AgentSession[] },
1549
+ workspaces: { count, items: KradleWorkspace[] },
1550
+ approvals: { count, items: AgentApproval[], pending: [...phase=Pending] },
1551
+ adapters: { count, items: AgentAdapter[] },
1552
+ providers: { count, items: AgentProviderConfig[] },
1553
+ projects: { count, items: KradleProject[] },
1554
+ gateway: AgentGatewayConfig | null,
1555
+ transcripts: { count, items: AgentSessionTranscript[] },
1556
+ memoryRepositories: { count, items: AgentMemoryRepository[] },
1557
+ memorySnapshots: { count, items: AgentMemorySnapshot[] },
1558
+ memoryImports: { count, items: AgentRunMemoryImport[], pending: [...] }
1559
+ };
1560
+ ```
1561
+
1562
+ ### 22.5 Delivery View (KubeVela)
1563
+
1564
+ `createDeliveryView()` assembles:
1565
+ - `installed` — boolean (any KubeVela definitions present)
1566
+ - `counts` — applications, releases, components, workloads, traits, scopes, policies, automations, managedResources
1567
+ - `capabilityCatalog` — names of installed component/trait/scope/policy/workflow-step definitions
1568
+ - `applications[]` — enriched with services, workflow status, releases, managed resources, YAML
1569
+ - `runtime` — releases, automations, policies, managedResources summaries
1570
+
1571
+ ### 22.6 Policy Engine View
1572
+
1573
+ `createPolicyEngineView()` produces:
1574
+ - `engine: 'kyverno'`, `mode`, `health` (disabled/ready/degraded)
1575
+ - `profiles`, `templates`, `bindings`, `exceptionRequests` — summarized via `policySummary()`
1576
+ - `kyvernoResources` — count per Kyverno kind
1577
+ - `controllers[]` — deployment health (name, ready, replicas)
1578
+ - `reports` — policyReports count, clusterPolicyReports count, results array
1579
+ - `violations[]` — filtered results with fail/error/warn status
1580
+
1581
+ ### 22.7 Identity View
1582
+
1583
+ `createIdentityView()` produces a fully-expanded view of:
1584
+ - `counts` — users, teams, pendingInvites, mappings, repositoryGrants, sshKeys
1585
+ - `providers[]` — name, label, type, enabled, phase
1586
+ - `users[]` — with email, teams, admin flag, disabled state
1587
+ - `teams[]` — with members, maintainers, repositoryGrants
1588
+ - `invites[]` — with email, role, teams, phase, expiresAt
1589
+ - `mappings[]` — with provider, subject, workspace/repository identity
1590
+ - `permissions[]` — with repository, subject, permission level, revoked state
1591
+ - `sshKeys[]` — with owner, scope, repository, revoked
1592
+ - `reconciliation` — counts, phases, statuses, nextActions (human-readable intents)
1593
+
1594
+ ### 22.8 Metrics
1595
+
1596
+ ```javascript
1597
+ metrics: {
1598
+ components, resources, events, auditEntries,
1599
+ users, teams, invites, repositories, pullRequests, issues, projects,
1600
+ pipelines, jobs, runnerPools, webhookDeliveries,
1601
+ policyViolations, policyBindings, deployments, releases,
1602
+ agentStacks, agentRuns, agentSessions,
1603
+ greenChecks, totalChecks
1604
+ }
1605
+ ```
1606
+
1607
+ ### 22.9 Events Formatting
1608
+
1609
+ Last 8 events are formatted as:
1610
+ ```javascript
1611
+ { type, storage: 'kubernetes', resource: 'Kind/namespace/name', actor, allowed: true, message }
1612
+ ```
1613
+
1614
+ ---
1615
+
1616
+ ## 23. HTTP Server Route Handlers
1617
+
1618
+ > Source: `packages/kradle/core/src/http-server.js`
1619
+
1620
+ ### 23.1 Server Factory
1621
+
1622
+ ```javascript
1623
+ export function createKradleHttpServer(options) {
1624
+ return createServer(createKradleHttpHandler(options));
1625
+ }
1626
+
1627
+ export function createKradleHttpHandler({ runtime, controller }) {
1628
+ return async function handleKradleRequest(request, response) { ... };
1629
+ }
1630
+ ```
1631
+
1632
+ All routes use regex pattern matching against `url.pathname`. JSON responses via `send(response, status, body)` with `content-type: application/json; charset=utf-8`.
1633
+
1634
+ ### 23.2 Route Table
1635
+
1636
+ | Method | Pattern | Handler |
1637
+ |--------|---------|---------|
1638
+ | GET | `/healthz` | Returns `{ ok: true, project: 'Kradle' }` |
1639
+ | GET | `/api/controller?org=:org` | Full UI model via `createControllerUiModel(snapshot, { organization })` |
1640
+ | GET/POST | `/api/orgs` | List orgs / create organization |
1641
+ | GET/POST | `/api/orgs/:org/resources` | List resources by kind (query `?kind=`) / apply resource |
1642
+ | GET/DELETE | `/api/orgs/:org/resources/:kind/:name` | Get or delete specific resource |
1643
+ | GET/POST | `/api/orgs/:org/repositories` | List / create repositories |
1644
+ | GET/DELETE | `/api/orgs/:org/repositories/:name` | Get / delete specific repository |
1645
+ | GET/POST | `/api/orgs/:org/snapshot` | Get runtime snapshot / import snapshot |
1646
+ | GET | `/api/orgs/:org/runtime-resources/:kind` | List runtime resources by kind |
1647
+ | POST | `/api/orgs/:org/repositories/:name/objects` | Record git object |
1648
+ | POST | `/api/orgs/:org/repositories/:name/search-index` | Enqueue search index |
1649
+ | POST | `/api/orgs/:org/pullrequests` | Create pull request |
1650
+ | POST | `/api/orgs/:org/pullrequests/:name/reviews` | Add review |
1651
+ | POST | `/api/orgs/:org/pullrequests/:name/checks/complete` | Complete pipeline check |
1652
+ | POST | `/api/orgs/:org/pullrequests/:name/merge` | Merge pull request |
1653
+ | POST | `/api/orgs/:org/agents/approvals/:name/decide` | Approve/deny agent approval |
1654
+ | POST | `/api/orgs/:org/agents/webhooks/ingest` | Webhook ingestion (GitHub/Gitea normalization) |
1655
+ | POST | `/api/orgs/:org/agents/events/pipeline-failure` | Pipeline failure event |
1656
+ | POST | `/api/orgs/:org/agents/events/comment` | Comment event |
1657
+ | POST | `/api/orgs/:org/agents/events/label` | Label event |
1658
+ | POST | `/api/orgs/:org/agents/triggers/process` | Evaluate event against trigger rules |
1659
+ | POST | `/api/orgs/:org/agents/memory/query` | Memory graph+grep search |
1660
+ | GET/POST | `/api/orgs/:org/secrets` | List / create secrets (AgentSecretGrant) |
1661
+ | DELETE | `/api/orgs/:org/secrets/:name` | Delete secret grant |
1662
+ | GET/POST | `/api/orgs/:org/secret-grants` | List / create secret grants |
1663
+ | POST | `/api/orgs/:org/external/sync` | Trigger external sync for binding |
1664
+ | POST | `/api/orgs/:org/external/conflicts/:name/resolve` | Resolve external sync conflict |
1665
+ | POST | `/api/orgs/:org/external/write-intents/:name/approve` | Approve write intent |
1666
+ | POST | `/api/orgs/:org/external/write-intents/:name/cancel` | Cancel write intent |
1667
+ | GET | `/api/orgs/:org/agents/events/stream` | **SSE endpoint** |
1668
+
1669
+ ### 23.3 SSE Endpoint Implementation
1670
+
1671
+ ```javascript
1672
+ const sseMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/agents\/events\/stream$/);
1673
+ if (request.method === 'GET' && sseMatch) {
1674
+ response.writeHead(200, {
1675
+ 'Content-Type': 'text/event-stream',
1676
+ 'Cache-Control': 'no-cache',
1677
+ 'Connection': 'keep-alive',
1678
+ 'X-Accel-Buffering': 'no', // Disable nginx buffering
1679
+ });
1680
+ response.write('data: {"type":"connected"}\n\n');
1681
+
1682
+ const writer = (event) => {
1683
+ response.write(`data: ${JSON.stringify(event)}\n\n`);
1684
+ };
1685
+ globalEventBus.subscribe(writer);
1686
+
1687
+ const interval = setInterval(() => {
1688
+ response.write('data: {"type":"heartbeat"}\n\n');
1689
+ }, 30000); // 30-second heartbeat
1690
+
1691
+ request.on('close', () => {
1692
+ clearInterval(interval);
1693
+ globalEventBus.unsubscribe(writer);
1694
+ });
1695
+ }
1696
+ ```
1697
+
1698
+ **Key behaviors:**
1699
+ - Sends `{"type":"connected"}` immediately on connection
1700
+ - Subscribes a writer function to `globalEventBus`
1701
+ - Sends heartbeat every 30 seconds to keep connection alive through proxies
1702
+ - On client disconnect: clears interval and unsubscribes from event bus
1703
+ - No CORS headers (handled by proxy or web framework)
1704
+
1705
+ ### 23.4 Webhook Event Normalization
1706
+
1707
+ `normalizeWebhookEvent(body, org)` maps raw GitHub/Gitea payloads:
1708
+
1709
+ | Condition | Normalized Type |
1710
+ |-----------|-----------------|
1711
+ | `action='completed'` + `workflow_run.conclusion='failure'` | `ci-failure` |
1712
+ | `action='opened'` + `pull_request` present | `pr-opened` |
1713
+ | `action='created'` + `comment` present | `comment` |
1714
+ | `action='labeled'` | `label-added` |
1715
+ | `action='opened'` + `issue` (no PR) | `issue-created` |
1716
+ | `ref` + `commits` present | `push` |
1717
+ | fallback | `webhook` |
1718
+
1719
+ ### 23.5 Error Handling
1720
+
1721
+ All routes are wrapped in a try/catch. Unhandled errors return:
1722
+ ```json
1723
+ { "error": "bad_request", "message": "<error.message>" }
1724
+ ```
1725
+ with status 400. Unmatched routes return 404:
1726
+ ```json
1727
+ { "error": "not_found", "method": "GET", "path": "/unknown" }
1728
+ ```
1729
+
1730
+ ---
1731
+
1732
+ ## 24. Async Snapshot Architecture
1733
+
1734
+ > Source: `packages/kradle/core/src/kubernetes-controller-async.js`
1735
+
1736
+ ### 24.1 runKubectlAsync(args, options)
1737
+
1738
+ Promise-based kubectl wrapper using `child_process.spawn`. Returns the same shape as the sync `runKubectl`:
1739
+
1740
+ ```typescript
1741
+ {
1742
+ ok: boolean,
1743
+ status: number | null,
1744
+ signal: string | null,
1745
+ stdout: string,
1746
+ stderr: string,
1747
+ error: string | null,
1748
+ command: string // Reconstructed command string for diagnostics
1749
+ }
1750
+ ```
1751
+
1752
+ **Timeout handling:**
1753
+ - Timer fires after `timeoutMs` (default 3000ms from `KRADLE_KUBECTL_TIMEOUT_MS`)
1754
+ - Sends SIGTERM to the child process
1755
+ - If `allowFailure: true` → resolves with `{ ok: false, error: 'kubectl timed out...' }`
1756
+ - If `allowFailure: false` → rejects with Error
1757
+
1758
+ **stdin:** If `options.input` is provided, writes to child stdin then closes it.
1759
+
1760
+ ### 24.2 getControllerSnapshotAsync(options) — Parallel Execution Strategy
1761
+
1762
+ Three-phase parallel execution:
1763
+
1764
+ **Phase 1:** Context + version in parallel
1765
+ ```javascript
1766
+ const [contextResult, versionResult] = await Promise.all([
1767
+ inClusterContext || runKubectlAsync(['config', 'current-context'], ...),
1768
+ runKubectlAsync(['version', '--client=true', '-o', 'json'], ...)
1769
+ ]);
1770
+ ```
1771
+
1772
+ **Phase 2:** API service + CRD discovery in parallel
1773
+ ```javascript
1774
+ const [apiServiceResult, crdResult] = await Promise.all([
1775
+ runKubectlAsync(['get', 'apiservice', KRADLE_API_VERSIONED_GROUP, ...]),
1776
+ runKubectlAsync(['get', 'crd', '-o', 'json'], ...)
1777
+ ]);
1778
+ ```
1779
+
1780
+ **Phase 3:** All resource kinds in parallel
1781
+ ```javascript
1782
+ // First: platform-scoped (to discover org namespaces)
1783
+ const platformResults = await Promise.all(
1784
+ platformScopedDefs.map((definition) => runKubectlAsync([...]))
1785
+ );
1786
+ // Derive org namespaces from results
1787
+ const orgNamespaces = resolveOrgNamespaces(resources.Organization, ...);
1788
+
1789
+ // Then: all org-scoped resources in parallel (each definition across all namespaces)
1790
+ const orgResults = await Promise.all(
1791
+ orgScopedDefs.map(async (definition) => {
1792
+ const itemArrays = await Promise.all(
1793
+ namespaces.map((ns) => runKubectlAsync([...]))
1794
+ );
1795
+ return { definition, items: itemArrays.flat() };
1796
+ })
1797
+ );
1798
+ ```
1799
+
1800
+ **Error fallback:** On any unexpected error, imports and falls back to the synchronous `getControllerSnapshot()`:
1801
+ ```javascript
1802
+ catch (error) {
1803
+ const { getControllerSnapshot } = await import('./kubernetes-controller.js');
1804
+ return getControllerSnapshot(options);
1805
+ }
1806
+ ```
1807
+
1808
+ ### 24.3 getPartialSnapshot(kinds, options)
1809
+
1810
+ Fetches only the requested resource kinds. Used by pages that need a subset (e.g. only `AgentStack` + `AgentSession`).
1811
+
1812
+ ```javascript
1813
+ export async function getPartialSnapshot(kinds = [], options = {}) {
1814
+ // 1. Resolve each kind string to a KRADLE_RESOURCES definition (skip unknown)
1815
+ // 2. If any org-scoped kind is needed, pre-fetch Organization + OrgNamespaceBinding
1816
+ // to compute orgNamespaces
1817
+ // 3. Fetch all requested definitions in parallel (across all applicable namespaces)
1818
+ // 4. Return { source: 'kubernetes', mode: 'partial', namespace, generatedAt, resources }
1819
+ }
1820
+ ```
1821
+
1822
+ Return shape is minimal — no kubectl metadata, no permissions, no events. Just `resources: Record<kind, items[]>`.
1823
+
1824
+ ### 24.4 watchResourceChanges(callback, options)
1825
+
1826
+ Lightweight watch that invalidates the snapshot cache on any change.
1827
+
1828
+ ```javascript
1829
+ export function watchResourceChanges(callback, options = {}) {
1830
+ const watchKinds = options.kinds || ['Organization', 'AgentStack', 'AgentSession'];
1831
+ const children = []; // Array of spawned child processes
1832
+
1833
+ for (const kind of watchKinds) {
1834
+ const child = spawn(kubectl, [...args, '--watch', '-o', 'json'], ...);
1835
+ let buffer = '';
1836
+ child.stdout.on('data', (chunk) => {
1837
+ buffer += chunk.toString();
1838
+ // Parse newline-delimited JSON objects
1839
+ while ((newlineIdx = buffer.indexOf('\n')) !== -1) {
1840
+ const item = safeJson(line);
1841
+ if (item) {
1842
+ clearSnapshotCache(); // Invalidate ALL cached snapshots
1843
+ callback(kind, item); // User callback (errors swallowed)
1844
+ }
1845
+ }
1846
+ });
1847
+ children.push(child);
1848
+ }
1849
+
1850
+ return { stop() { children.forEach(c => c.kill('SIGTERM')); } };
1851
+ }
1852
+ ```
1853
+
1854
+ **Key behaviors:**
1855
+ - Default watched kinds: Organization, AgentStack, AgentSession
1856
+ - Uses `--watch -o json` for streaming JSON from kubectl
1857
+ - Parses newline-delimited JSON (not JSON array)
1858
+ - On any valid object: calls `clearSnapshotCache()` (see §25)
1859
+ - Returns `{ stop }` cleanup handle for graceful shutdown
1860
+
1861
+ ### 24.5 Differences from Sync Version
1862
+
1863
+ | Aspect | Sync (`getControllerSnapshot`) | Async (`getControllerSnapshotAsync`) |
1864
+ |--------|------|------|
1865
+ | Process execution | `spawnSync` | `spawn` + Promise |
1866
+ | Resource listing | Sequential loop | `Promise.all` parallel |
1867
+ | Permission checks | Inline `canI` per resource | Skipped (returns `[]`) |
1868
+ | Kyverno discovery | Full `discoverKyverno()` | Returns `emptyKyverno()` stub |
1869
+ | Error recovery | Throws or returns degraded | Falls back to sync version |
1870
+ | Event collection | Included | Included (async) |
1871
+
1872
+ ---
1873
+
1874
+ ## 25. Snapshot Cache Architecture
1875
+
1876
+ > Source: `packages/kradle/core/src/snapshot-cache.js`
1877
+
1878
+ ### 25.1 Data Structures
1879
+
1880
+ ```javascript
1881
+ // Per-org cache: Map<string, CacheEntry>
1882
+ const orgCacheMap = new Map();
1883
+
1884
+ // CacheEntry shape:
1885
+ { data: object, timestamp: number, revalidating: boolean }
1886
+
1887
+ // Legacy single-org cache (backward compatibility with controller-client.js):
1888
+ let snapshotCache = { data: null, timestamp: 0, org: null };
1889
+ ```
1890
+
1891
+ ### 25.2 Constants
1892
+
1893
+ ```javascript
1894
+ export const CACHE_TTL_MS = Number(process.env.KRADLE_SNAPSHOT_CACHE_TTL_MS || 30_000);
1895
+ ```
1896
+
1897
+ Default: 30 seconds. Configurable via environment.
1898
+
1899
+ ### 25.3 staleWhileRevalidate(org, revalidateFn, swrOptions)
1900
+
1901
+ Full algorithm:
1902
+
1903
+ ```javascript
1904
+ export async function staleWhileRevalidate(org, revalidateFn, swrOptions = {}) {
1905
+ const ttlMs = swrOptions.ttlMs ?? CACHE_TTL_MS; // Fresh window (default 30s)
1906
+ const staleMs = swrOptions.staleMs ?? ttlMs * 5; // Max staleness (default 150s)
1907
+ const entry = orgCacheMap.get(org ?? '');
1908
+ const now = Date.now();
1909
+
1910
+ const isFresh = entry?.data && (now - entry.timestamp) < ttlMs;
1911
+ const isStale = entry?.data && (now - entry.timestamp) < staleMs;
1912
+
1913
+ // Case 1: Fresh — return immediately, no revalidation
1914
+ if (isFresh) return entry.data;
1915
+
1916
+ // Case 2: Stale + not already revalidating — return stale, background refresh
1917
+ if (isStale && !entry.revalidating) {
1918
+ orgCacheMap.set(key, { ...entry, revalidating: true });
1919
+ Promise.resolve().then(async () => {
1920
+ try {
1921
+ const fresh = await revalidateFn();
1922
+ setOrgCache(fresh, org); // Updates both orgCacheMap and legacy cache
1923
+ } catch {
1924
+ // Clear revalidating flag so future requests can retry
1925
+ orgCacheMap.set(key, { ...current, revalidating: false });
1926
+ }
1927
+ });
1928
+ return entry.data; // Return stale immediately
1929
+ }
1930
+
1931
+ // Case 3: Stale + already revalidating — return stale (another caller is refreshing)
1932
+ if (isStale && entry.revalidating) return entry.data;
1933
+
1934
+ // Case 4: No usable cache — block on revalidation
1935
+ const fresh = await revalidateFn();
1936
+ setOrgCache(fresh, org);
1937
+ return fresh;
1938
+ }
1939
+ ```
1940
+
1941
+ ### 25.4 Cache API Summary
1942
+
1943
+ | Function | Behavior |
1944
+ |----------|----------|
1945
+ | `getOrgCache(org)` | Returns `CacheEntry` or `null` |
1946
+ | `setOrgCache(data, org)` | Stores entry with `Date.now()` timestamp, clears `revalidating` |
1947
+ | `clearOrgCache(org)` | Removes single org entry; clears legacy if matching |
1948
+ | `clearSnapshotCache()` | Clears ALL orgs + legacy cache |
1949
+ | `isCacheFresh(org, ttlMs?)` | `(Date.now() - entry.timestamp) < ttlMs` |
1950
+ | `cachedOrgs()` | Returns `[...orgCacheMap.keys()]` for introspection |
1951
+ | `setSnapshotCache(data, org)` | Legacy API: updates both stores |
1952
+ | `getSnapshotCache()` | Legacy API: returns `{ data, timestamp, org }` |
1953
+
1954
+ ### 25.5 Background Revalidation
1955
+
1956
+ The revalidation promise is fire-and-forget (`Promise.resolve().then(async () => {...})`). On error:
1957
+ - The `revalidating` flag is cleared so the next caller can try again
1958
+ - The stale data remains available until `staleMs` expires
1959
+ - No retries — the next request triggers a new revalidation attempt
1960
+
1961
+ ---
1962
+
1963
+ ## 26. Auth System Deep Dive
1964
+
1965
+ > Source: `packages/kradle/core/src/auth.js`
1966
+
1967
+ ### 26.1 createAuthProviderConfig(env)
1968
+
1969
+ Parses environment variables into a provider configuration object:
1970
+
1971
+ ```javascript
1972
+ return {
1973
+ session: { cookieName: env.KRADLE_AUTH_COOKIE_NAME || 'kradle_session' },
1974
+ delegatedIdentity: {
1975
+ enabled: env.KRADLE_AUTH_DELEGATED_IDENTITY_ENABLED === 'true',
1976
+ userHeader: env.KRADLE_AUTH_DELEGATED_USER_HEADER || 'x-forwarded-user',
1977
+ groupsHeader: env.KRADLE_AUTH_DELEGATED_GROUPS_HEADER || 'x-forwarded-groups',
1978
+ emailHeader: env.KRADLE_AUTH_DELEGATED_EMAIL_HEADER || 'x-forwarded-email',
1979
+ localDevelopment: {
1980
+ enabled: delegatedLocalDevelopmentEnabled(env), // true unless NODE_ENV=production
1981
+ user: env.KRADLE_AUTH_DELEGATED_LOCAL_USER || 'local-developer',
1982
+ email: env.KRADLE_AUTH_DELEGATED_LOCAL_EMAIL || '',
1983
+ groups: env.KRADLE_AUTH_DELEGATED_LOCAL_GROUPS || 'kradle:repo-admins'
1984
+ }
1985
+ },
1986
+ providers: {
1987
+ github: { id: 'github', label: 'GitHub', type: 'github', enabled, clientId, clientSecret, authorizationUrl, tokenUrl, userInfoUrl, scopes },
1988
+ sso: { id: 'sso', label: '<configurable>', type: 'oidc', enabled, issuerUrl, clientId, clientSecret, authorizationUrl, tokenUrl, userInfoUrl, scopes }
1989
+ }
1990
+ };
1991
+ ```
1992
+
1993
+ **Provider enablement:**
1994
+ - GitHub: enabled unless `KRADLE_AUTH_GITHUB_ENABLED=false`
1995
+ - SSO: enabled only when `KRADLE_AUTH_SSO_ENABLED=true`
1996
+
1997
+ ### 26.2 listEnabledAuthProviders(config)
1998
+
1999
+ ```javascript
2000
+ Object.values(config.providers).filter((p) => p.enabled && p.clientId && p.authorizationUrl)
2001
+ ```
2002
+
2003
+ Returns only providers that are both enabled AND have credentials configured.
2004
+
2005
+ ### 26.3 buildAuthorizationRedirect({ provider, requestUrl, state })
2006
+
2007
+ 1. Validates provider is enabled with clientId and authorizationUrl
2008
+ 2. Constructs `redirectUri` = `${protocol}://${host}/api/auth/callback/${provider.id}`
2009
+ 3. Builds authorization URL with query params: `response_type=code`, `client_id`, `redirect_uri`, `scope`, `state`
2010
+ 4. Returns `{ url, state, redirectUri }`
2011
+
2012
+ State token generation: `Math.random().toString(36).slice(2) + Date.now().toString(36)`
2013
+
2014
+ ### 26.4 exchangeOAuthCodeForProfile({ provider, code, requestUrl, fetchImpl })
2015
+
2016
+ 1. POSTs to `provider.tokenUrl` with `grant_type=authorization_code`, `code`, `redirect_uri`, `client_id`, `client_secret`
2017
+ 2. Extracts `access_token` from JSON response
2018
+ 3. GETs `provider.userInfoUrl` with `Authorization: Bearer <token>`
2019
+ 4. Normalizes profile via `normalizeProviderProfile(provider, profile)`
2020
+
2021
+ **Profile normalization:**
2022
+ - GitHub: extracts `login`, `id`, `email`, `name`; groups = `[]`
2023
+ - OIDC/SSO: extracts `sub`, `email`, `preferred_username`, `groups` (comma-split if string)
2024
+ - Admin detection: groups include `kradle:platform-engineers` or `kradle:repo-admins`
2025
+
2026
+ ### 26.5 registerLoginProfile({ controller, namespace, profile })
2027
+
2028
+ 1. Determines org from `KRADLE_ADMIN_ORG || KRADLE_ORG || 'default'`
2029
+ 2. Detects bootstrap admin: compares profile username/email against `KRADLE_ADMIN_USERNAME`
2030
+ 3. Calls `mapLoginProfileToKradleIdentity()` to produce User + IdentityMapping resources
2031
+ 4. Applies both via `controller.applyResource()`
2032
+ 5. Returns `{ identity, user, mapping, userResult, mappingResult }`
2033
+
2034
+ ### 26.6 createSessionCookie(config, profile, options)
2035
+
2036
+ ```javascript
2037
+ // 1. Build JSON payload
2038
+ const payload = Buffer.from(JSON.stringify({
2039
+ provider: profile.provider,
2040
+ subject: profile.subject,
2041
+ user: profile.username || profile.email
2042
+ })).toString('base64url');
2043
+
2044
+ // 2. Sign if KRADLE_SESSION_SECRET is set
2045
+ if (secret) {
2046
+ const signature = createHmac('sha256', secret).update(payload).digest('base64url');
2047
+ value = `${payload}.${signature}`;
2048
+ } else {
2049
+ value = payload; // Unsigned (development mode)
2050
+ }
2051
+
2052
+ // 3. Return Set-Cookie header value
2053
+ return `${cookieName}=${value}; Path=/; HttpOnly; SameSite=Lax`;
2054
+ ```
2055
+
2056
+ ### 26.7 parseSessionCookie(config, cookieValue, options)
2057
+
2058
+ ```javascript
2059
+ // 1. Detect signature presence (indexOf('.'))
2060
+ // 2. Reject: signed cookie + no secret, or unsigned cookie + secret configured
2061
+ // 3. If signed: extract payload + signature, verify with HMAC-SHA256 + timingSafeEqual
2062
+ // 4. Decode: JSON.parse(Buffer.from(payload, 'base64url'))
2063
+ // 5. Return { cookieName, provider, subject, user } or null on any failure
2064
+ ```
2065
+
2066
+ **Security properties:**
2067
+ - Constant-time comparison via `timingSafeEqual`
2068
+ - Rejects mismatched length buffers before comparison
2069
+ - Silent failure (returns null) — no error messages leaked
2070
+
2071
+ ### 26.8 mapLoginProfileToKradleIdentity(profile)
2072
+
2073
+ Creates two Kradle CRD resources:
2074
+
2075
+ **User resource:**
2076
+ ```javascript
2077
+ createResource('User', { name: userName, namespace, labels: { role } }, {
2078
+ organizationRef, displayName, email, username, teams, admin, disabled: false
2079
+ }, { phase: 'Active', lastLoginProvider, groups })
2080
+ ```
2081
+
2082
+ **IdentityMapping resource:**
2083
+ ```javascript
2084
+ createResource('IdentityMapping', { name: `${provider}-${userName}`, namespace }, {
2085
+ organizationRef, user, provider, subject, email,
2086
+ workspaceIdentity: { name, uid, groups }, // From mapOidcIdentity()
2087
+ repositoryIdentity: { username, email }
2088
+ }, { phase: 'Synced' })
2089
+ ```
2090
+
2091
+ ### 26.9 profileFromDelegatedHeaders(headers, config, options)
2092
+
2093
+ For reverse-proxy authentication (e.g. OAuth2 Proxy, Authelia):
2094
+ 1. Reads user from configured header (`x-forwarded-user` by default)
2095
+ 2. Falls back to local development profile if no header and localhost request
2096
+ 3. Reads email from email header
2097
+ 4. Reads groups from groups header (comma-separated string → array)
2098
+ 5. Admin detection: same group check as OAuth flow
2099
+ 6. Returns profile with `delegatedIdentitySource: 'proxy-header' | 'local-development'`
2100
+
2101
+ ### 26.10 normalizeName(value)
2102
+
2103
+ ```javascript
2104
+ String(value || 'user').toLowerCase()
2105
+ .replace(/[^a-z0-9-]+/g, '-') // Non-alphanumeric → dash
2106
+ .replace(/^-+|-+$/g, '') // Trim leading/trailing dashes
2107
+ .slice(0, 63) // K8s name length limit
2108
+ || 'user' // Fallback if empty after normalization
2109
+ ```
2110
+
2111
+ ### 26.11 KRADLE_SESSION_SECRET Flow
2112
+
2113
+ | Scenario | Cookie Format | Verification |
2114
+ |----------|---------------|--------------|
2115
+ | No secret (dev) | `base64url(json)` | Accepts any base64url payload |
2116
+ | Secret set (prod) | `base64url(json).hmac_sha256_base64url` | Rejects unsigned, verifies HMAC |
2117
+ | Signed cookie + no secret | — | Rejected (returns null) |
2118
+ | Unsigned cookie + secret | — | Rejected (returns null) |
2119
+
2120
+ ---
2121
+
2122
+ ## 27. Event Bus Architecture
2123
+
2124
+ > Source: `packages/kradle/core/src/event-bus.js`
2125
+
2126
+ ### 27.1 createEventBus() — Factory
2127
+
2128
+ ```javascript
2129
+ export function createEventBus() {
2130
+ const listeners = new Set();
2131
+ return { subscribe(fn), unsubscribe(fn), emit(event), emitResourceChange(kind, name, operation) };
2132
+ }
2133
+ ```
2134
+
2135
+ ### 27.2 globalEventBus — Module-Level Singleton
2136
+
2137
+ ```javascript
2138
+ export const globalEventBus = createEventBus();
2139
+ ```
2140
+
2141
+ Shared across the entire Node.js process. Imported by:
2142
+ - `http-server.js` — SSE endpoint subscribes writer functions
2143
+ - `api-controller.js` — emits after `applyResource()` / `deleteResource()`
2144
+
2145
+ ### 27.3 Listener Management
2146
+
2147
+ - `subscribe(fn)` — adds to `Set<Function>` (deduplication via reference equality)
2148
+ - `unsubscribe(fn)` — removes from Set
2149
+
2150
+ ### 27.4 emit(event) — Synchronous Broadcast
2151
+
2152
+ ```javascript
2153
+ emit(event) {
2154
+ for (const fn of listeners) {
2155
+ fn(event); // Synchronous invocation — no error boundary
2156
+ }
2157
+ }
2158
+ ```
2159
+
2160
+ All subscribers are called synchronously in iteration order. A throwing subscriber would propagate to the emitter.
2161
+
2162
+ ### 27.5 emitResourceChange(kind, name, operation)
2163
+
2164
+ Convenience wrapper producing structured events:
2165
+
2166
+ ```javascript
2167
+ {
2168
+ type: 'resource-change',
2169
+ kind: string, // e.g. 'Repository', 'AgentDispatchRun'
2170
+ name: string, // Resource metadata.name
2171
+ operation: string, // 'apply' | 'delete'
2172
+ timestamp: string // ISO 8601
2173
+ }
2174
+ ```
2175
+
2176
+ ### 27.6 Integration Flow
2177
+
2178
+ ```
2179
+ api-controller.applyResource()
2180
+ → globalEventBus.emitResourceChange('Repository', 'my-repo', 'apply')
2181
+ → SSE writer in http-server.js
2182
+ → response.write('data: {"type":"resource-change",...}\n\n')
2183
+ → Browser EventSource receives event
2184
+ ```
2185
+
2186
+ ### 27.7 Memory Model
2187
+
2188
+ - `listeners` is a `Set` — no persistence, no durability
2189
+ - Events are fire-and-forget — if no subscribers exist, events are dropped
2190
+ - No event replay or history — late subscribers miss past events
2191
+ - No backpressure — slow subscribers block the emit loop
2192
+
2193
+ ---
2194
+
2195
+ ## 28. Gitea Service Layer
2196
+
2197
+ > Source: `packages/kradle/core/src/gitea-service.js`, `packages/kradle/core/src/gitea-backend.js`
2198
+
2199
+ ### 28.1 createGiteaService(options) — High-Level Service
2200
+
2201
+ ```javascript
2202
+ export function createGiteaService(options = {}) {
2203
+ const giteaUrl = options.giteaUrl || process.env.KRADLE_GITEA_HTTP_URL;
2204
+ if (!giteaUrl) return null; // Callers must check and fall back to mock
2205
+ const backend = createGiteaBackend({ baseUrl: giteaUrl, token, fetchImpl });
2206
+ return { available: true, baseUrl, listTree, getBlob, listBranches, getFileContent, createRepository };
2207
+ }
2208
+ ```
2209
+
2210
+ **Returns `null`** when `KRADLE_GITEA_HTTP_URL` is not set — this is the availability check. Web routes that need tree/blob data try Gitea first, then fall back to mock responses.
2211
+
2212
+ ### 28.2 Service Methods
2213
+
2214
+ | Method | Gitea API Endpoint | Returns |
2215
+ |--------|-------------------|---------|
2216
+ | `listTree(org, repo, ref, path)` | `GET /api/v1/repos/{owner}/{repo}/contents/{path}?ref={ref}` | `[{ path, type: 'blob'|'tree', size, sha, name }]` or `null` (404) |
2217
+ | `getBlob(org, repo, ref, path)` | `GET /api/v1/repos/{owner}/{repo}/raw/{path}?ref={ref}` | Raw text content or `null` (404) |
2218
+ | `listBranches(org, repo)` | `GET /api/v1/repos/{owner}/{repo}/branches` | `[{ name, sha, protected }]` or `null` |
2219
+ | `getFileContent(org, repo, ref, path)` | `GET /api/v1/repos/{owner}/{repo}/contents/{path}?ref={ref}` | `{ path, content, size, sha, encoding, lastCommit }` or `null` |
2220
+ | `createRepository(org, name, opts)` | Delegates to `backend.createRepository()` | Gitea API response |
2221
+
2222
+ **Error handling:**
2223
+ - 404 → returns `null` (graceful degradation)
2224
+ - Other non-OK status → throws `Error('Gitea GET <path> failed with <status>')`
2225
+
2226
+ ### 28.3 createGiteaBackend(options) — Low-Level HTTP Client
2227
+
2228
+ ```javascript
2229
+ export function createGiteaBackend({ baseUrl, token, fetchImpl }) {
2230
+ async function request(method, path, body) {
2231
+ const response = await fetchImpl(`${root}/api/v1${path}`, { method, headers: {...}, body? });
2232
+ if (!response.ok) throw new Error(`Gitea ${method} ${path} failed with ${response.status}`);
2233
+ return response.status === 204 ? null : response.json();
2234
+ }
2235
+ return { role: 'gitea-backend', baseUrl, ...methods };
2236
+ }
2237
+ ```
2238
+
2239
+ **Backend methods (all use `request()` internally):**
2240
+
2241
+ | Method | HTTP | Gitea API Path |
2242
+ |--------|------|----------------|
2243
+ | `createOrganization({ name, fullName, description, visibility })` | POST | `/orgs` |
2244
+ | `createUser({ username, email, fullName, password, mustChangePassword })` | POST | `/admin/users` |
2245
+ | `editUser({ username, email, fullName, active, admin })` | PATCH | `/admin/users/{username}` |
2246
+ | `addUserSshKey({ title, key, readOnly })` | POST | `/user/keys` |
2247
+ | `createRepository({ owner, name, private, defaultBranch, description })` | POST | `/orgs/{owner}/repos` or `/user/repos` |
2248
+ | `addDeployKey({ owner, repo, title, key, readOnly })` | POST | `/repos/{owner}/{repo}/keys` |
2249
+ | `addCollaborator({ owner, repo, username, permission })` | PUT | `/repos/{owner}/{repo}/collaborators/{username}` |
2250
+ | `addTeamRepository({ org, team, repo, owner, permission })` | PUT | `/teams/{team}/repos/{owner}/{repo}` |
2251
+ | `createTeam({ org, name, permission, units })` | POST | `/orgs/{org}/teams` |
2252
+ | `addTeamMember({ team, username })` | PUT | `/teams/{team}/members/{username}` |
2253
+ | `protectBranch({ owner, repo, branch, approvals, statusChecks })` | POST | `/repos/{owner}/{repo}/branch_protections` |
2254
+ | `createIssue({ owner, repo, title, body, labels, assignees })` | POST | `/repos/{owner}/{repo}/issues` |
2255
+ | `createPullRequest({ owner, repo, title, head, base, body })` | POST | `/repos/{owner}/{repo}/pulls` |
2256
+ | `createWebhook({ owner, repo, url, events, secret })` | POST | `/repos/{owner}/{repo}/hooks` |
2257
+
2258
+ ### 28.4 Authentication
2259
+
2260
+ All requests include `Authorization: token <token>` header when `token` is provided (from `KRADLE_GITEA_TOKEN` environment variable).
2261
+
2262
+ ---
2263
+
2264
+ ## 29. Notification Controller
2265
+
2266
+ > Source: `packages/kradle/core/src/notification-controller.js`
2267
+
2268
+ ### 29.1 Event-to-Notification Mapping
2269
+
2270
+ | Event Type | Event Status/Condition | Notification Type | Severity |
2271
+ |------------|----------------------|-------------------|----------|
2272
+ | `AgentDispatchRun` | `status='completed'` | `run-complete` | info |
2273
+ | `AgentDispatchRun` | `status='failed'` | `run-complete` | error |
2274
+ | `AgentDispatchRun` | other status | `run-complete` | info |
2275
+ | `AgentApproval` | `status='pending'` | `approval-needed` | warning |
2276
+ | `ExternalSyncConflict` | any | `conflict-detected` | warning |
2277
+ | `KradleWorkspace` | `claimed=true` | `workspace-ready` | info |
2278
+ | fallback | any | `system` | info |
2279
+
2280
+ ### 29.2 Notification Shape
2281
+
2282
+ ```javascript
2283
+ {
2284
+ id: string, // crypto.randomUUID()
2285
+ type: string, // 'run-complete' | 'approval-needed' | 'conflict-detected' | 'workspace-ready' | 'system'
2286
+ title: string, // Human-readable title
2287
+ message: string, // Detailed message
2288
+ severity: string, // 'info' | 'warning' | 'error'
2289
+ resourceRef: any, // Optional reference to the triggering resource
2290
+ createdAt: string, // ISO 8601 timestamp
2291
+ read: boolean, // Read state (default false)
2292
+ org: string // Organization slug
2293
+ }
2294
+ ```
2295
+
2296
+ ### 29.3 Storage Model
2297
+
2298
+ ```javascript
2299
+ const store = new Map(); // org → notifications[] (in-memory, no persistence)
2300
+ const prefsStore = new Map(); // userId → preferences object
2301
+ ```
2302
+
2303
+ ### 29.4 API Methods
2304
+
2305
+ | Method | Signature | Behavior |
2306
+ |--------|-----------|----------|
2307
+ | `createNotification(event)` | `(object) → notification` | Maps event → notification, pushes to org store |
2308
+ | `listNotifications(org, opts)` | `(string, { unreadOnly?, limit?, since? })` | Sort by createdAt desc, apply filters, cap to limit (default 20) |
2309
+ | `markAsRead(notificationId)` | `(string) → boolean` | Scans all org stores, sets `read=true`, returns success |
2310
+ | `markAllAsRead(org)` | `(string) → number` | Marks all unread in org, returns count |
2311
+ | `getUnreadCount(org)` | `(string) → number` | `.filter(n => !n.read).length` |
2312
+ | `getPreferences(userId)` | `(string) → prefs` | Returns merged defaults + stored prefs |
2313
+ | `updatePreferences(userId, prefs)` | `(string, object) → prefs` | Deep-merge into stored prefs |
2314
+
2315
+ ### 29.5 Default Preferences
2316
+
2317
+ ```javascript
2318
+ { runs: true, approvals: true, conflicts: true, workspaces: true, sound: false, desktop: false }
2319
+ ```
2320
+
2321
+ ---
2322
+
2323
+ ## 30. Runner Controller
2324
+
2325
+ > Source: `packages/kradle/core/src/runner-controller.js`
2326
+
2327
+ ### 30.1 validateRunnerPool(resource)
2328
+
2329
+ Validates a RunnerPool resource:
2330
+
2331
+ | Check | Error Reason | Message |
2332
+ |-------|--------------|---------|
2333
+ | resource is null/undefined | `missing-resource` | resource is required |
2334
+ | no `metadata.name` | `missing-name` | metadata.name is required |
2335
+ | no `spec.organizationRef` | `missing-org` | spec.organizationRef is required |
2336
+ | `warmReplicas` not non-negative int | `invalid-min-replicas` | must be non-negative integer |
2337
+ | `maxReplicas` not positive int | `invalid-max-replicas` | must be positive integer |
2338
+ | `warmReplicas > maxReplicas` | `replicas-conflict` | must not exceed maxReplicas |
2339
+
2340
+ Returns `{ valid: true, name, organizationRef, warmReplicas, maxReplicas, image, labels }` on success.
2341
+
2342
+ ### 30.2 getPoolStatus(pool)
2343
+
2344
+ ```javascript
2345
+ return {
2346
+ poolName,
2347
+ idle: runners.filter(status === 'Idle').length,
2348
+ active: runners.filter(status === 'Running').length,
2349
+ terminating: runners.filter(status === 'Terminating').length,
2350
+ total: poolRunners.length,
2351
+ desired: pool.spec.warmReplicas,
2352
+ maxReplicas: pool.spec.maxReplicas,
2353
+ phase: total === 0 ? 'Empty' : active > 0 ? 'Active' : 'Idle',
2354
+ scaling: total < desired ? 'ScalingUp' : total > max ? 'ScalingDown' : 'Stable'
2355
+ };
2356
+ ```
2357
+
2358
+ ### 30.3 getCapacity(pool)
2359
+
2360
+ ```javascript
2361
+ return {
2362
+ poolName,
2363
+ maxReplicas,
2364
+ used: runners.filter(status === 'Running').length,
2365
+ available: Math.max(0, maxReplicas - used),
2366
+ utilizationPct: Math.round((used / maxReplicas) * 100)
2367
+ };
2368
+ ```
2369
+
2370
+ ### 30.4 createRunner(pool, runRef)
2371
+
2372
+ 1. Generates runner ID: `runner-${poolName}-${Date.now()}-${random5chars}`
2373
+ 2. Determines initial status: `'Running'` if runRef provided, `'Idle'` otherwise
2374
+ 3. Calls `generatePodSpec()` to build the K8s Pod manifest
2375
+ 4. Stores in in-memory `runners` Map
2376
+ 5. If runRef → stores in `jobAssignments` Map
2377
+
2378
+ ### 30.5 generatePodSpec({ runnerId, pool }, workspace)
2379
+
2380
+ Produces a complete K8s Pod manifest:
2381
+
2382
+ ```javascript
2383
+ {
2384
+ apiVersion: 'v1',
2385
+ kind: 'Pod',
2386
+ metadata: {
2387
+ name: `runner-${runnerId}`,
2388
+ namespace: pool.metadata.namespace || 'kradle-org-default',
2389
+ labels: {
2390
+ 'kradle.a5c.ai/runner': runnerId,
2391
+ 'kradle.a5c.ai/pool': poolName,
2392
+ 'kradle.a5c.ai/org': organizationRef
2393
+ }
2394
+ },
2395
+ spec: {
2396
+ serviceAccountName: spec.serviceAccount || 'kradle-runner',
2397
+ restartPolicy: 'Never',
2398
+ containers: [{
2399
+ name: 'runner',
2400
+ image: spec.image || 'ubuntu:24.04',
2401
+ env: [
2402
+ { name: 'KRADLE_ORG', value: organizationRef },
2403
+ { name: 'KRADLE_RUN_ID', value: runId },
2404
+ { name: 'KRADLE_WORKSPACE_PATH', value: '/workspace' }
2405
+ ],
2406
+ volumeMounts: workspace ? [{ name: 'workspace', mountPath: '/workspace' }] : [],
2407
+ resources: {
2408
+ limits: spec.resourceLimits || { cpu: '2', memory: '4Gi' },
2409
+ requests: spec.resourceRequests || { cpu: '500m', memory: '1Gi' }
2410
+ }
2411
+ }],
2412
+ volumes: workspace ? [{
2413
+ name: 'workspace',
2414
+ persistentVolumeClaim: { claimName: `kradle-ws-${runId}` }
2415
+ }] : []
2416
+ }
2417
+ }
2418
+ ```
2419
+
2420
+ ### 30.6 scheduleJob(pool, job)
2421
+
2422
+ 1. Check if job already assigned → return existing runner (reused=true)
2423
+ 2. Find idle runner in pool → assign it (status → Running)
2424
+ 3. Check capacity → if none available, return `{ error: true, reason: 'no-capacity' }`
2425
+ 4. Create new runner via `createRunner(pool, jobRef)`
2426
+
2427
+ ### 30.7 terminateRunner(runnerId)
2428
+
2429
+ 1. Look up runner in Map
2430
+ 2. Remove job assignment if any
2431
+ 3. Set status to `'Terminating'`, record `terminatedAt`
2432
+ 4. Remove from runners Map
2433
+
2434
+ ---
2435
+
2436
+ ## 31. External Backend Pipeline
2437
+
2438
+ > Source: `packages/kradle/core/src/external/`
2439
+
2440
+ ### 31.1 Provider Registration (provider-resource-factory.js)
2441
+
2442
+ ```javascript
2443
+ export function createDefaultProviderRegistry() {
2444
+ const registry = createProviderRegistry(); // Map<type, adapter>
2445
+ registry.register('github', buildGitHubAdapterDescriptor());
2446
+ return registry;
2447
+ }
2448
+ ```
2449
+
2450
+ The GitHub adapter descriptor exposes factory methods (`createForge`, `createIssueTracker`, `createCicd`) for credential-bound instances, plus stub credential-free interfaces.
2451
+
2452
+ **Provider Registry API:**
2453
+ - `register(type, adapter)` — stores adapter by type key
2454
+ - `get(type)` → adapter or null
2455
+ - `list()` → `[...adapters.keys()]`
2456
+
2457
+ **Adapter validation contract** (from `provider-adapter.js`):
2458
+ - Required: `descriptor()`, `health()`, `normalizeWebhook(payload)`, `verifyWebhook(request)`
2459
+ - At least one of: `issueTracking`, `cicd`, `gitForge`
2460
+
2461
+ ### 31.2 Webhook Ingestion (webhook-controller.js)
2462
+
2463
+ ```javascript
2464
+ export function createWebhookController({ secret }) {
2465
+ const deliveries = new Map(); // deliveryId → record
2466
+ const subscribers = []; // event handlers
2467
+ return { verifyHmacSignature, createDeliveryRecord, recordDelivery, isDuplicate, onEvent, processDelivery };
2468
+ }
2469
+ ```
2470
+
2471
+ **verifyHmacSignature(body, signature):**
2472
+ - Requires `sha256=` prefix
2473
+ - Computes `sha256=` + HMAC-SHA256(secret, body).hex()
2474
+ - Constant-time comparison via `timingSafeEqual` on the full strings (not just digests)
2475
+ - Returns `{ valid: boolean, reason: string|null }`
2476
+
2477
+ **processDelivery({ deliveryId, eventType, payload, rawBody }):**
2478
+ 1. Dedup check: `if (isDuplicate(deliveryId)) → { queued: 0, duplicate: true }`
2479
+ 2. Create delivery record with timestamp
2480
+ 3. Store in `deliveries` Map
2481
+ 4. Emit to all subscribers
2482
+ 5. Return `{ queued: subscriberCount, duplicate: false, deliveryId }`
2483
+
2484
+ ### 31.3 Event Normalization (sync-controller.js)
2485
+
2486
+ ```javascript
2487
+ normalizeEvent(rawEvent) → {
2488
+ eventType,
2489
+ action: rawEvent.action || 'unknown',
2490
+ nativeId,
2491
+ providerRef,
2492
+ resourceKind,
2493
+ data: rawEvent.data || {},
2494
+ receivedAt: rawEvent.receivedAt || now,
2495
+ canonicalAt: now
2496
+ }
2497
+ ```
2498
+
2499
+ ### 31.4 Resource Upsert (sync-controller.js)
2500
+
2501
+ ```javascript
2502
+ upsertResource({ kind, localName, namespace, spec, externalEnvelope }) → resource
2503
+ ```
2504
+
2505
+ The `externalEnvelope` contains:
2506
+ - `nativeId` — provider's identifier (e.g. GitHub issue number)
2507
+ - `url` — canonical URL on the provider
2508
+ - `etag` — version marker for conflict detection
2509
+ - `providerRef` — which ExternalBackendProvider this came from
2510
+
2511
+ On upsert:
2512
+ - `firstSyncedAt` is preserved from existing resource
2513
+ - `lastSyncedAt` is always updated to now
2514
+ - Stored in internal `resources` Map keyed by `${namespace}/${kind}/${localName}`
2515
+ - Fire-and-forget `persistFn(resource)` called
2516
+
2517
+ ### 31.5 Watermark Tracking (sync-controller.js)
2518
+
2519
+ ```javascript
2520
+ updateWatermark(bindingRef, timestamp)
2521
+ // Only advances if new timestamp > current (monotonic)
2522
+ // Persists as ExternalSyncWatermark CRD resource
2523
+
2524
+ getWatermark(bindingRef) → string | null
2525
+ ```
2526
+
2527
+ Per-binding state stored in `watermarks` Map. Prevents re-processing of already-synced events.
2528
+
2529
+ ### 31.6 Ownership Modes (sync-controller.js)
2530
+
2531
+ ```javascript
2532
+ applyOwnershipMode({ ownershipMode, operation, origin }) → { allowed, reason }
2533
+ ```
2534
+
2535
+ | Mode | Kradle Write | External Write |
2536
+ |------|-------------|----------------|
2537
+ | `bidirectional` | allowed | allowed |
2538
+ | `external-owned` | **blocked** | allowed |
2539
+ | `kradle-owned` | allowed | **blocked** |
2540
+ | unknown | blocked | blocked |
2541
+
2542
+ ### 31.7 Conflict Detection (conflict-controller.js)
2543
+
2544
+ ```javascript
2545
+ detectConflict({ resourceRef, fieldPath, localValue, externalValue, namespace, organizationRef })
2546
+ ```
2547
+
2548
+ - If `localValue === externalValue` → `{ conflict: null }` (no conflict)
2549
+ - If different → creates `ExternalSyncConflict` resource with phase `'Open'`
2550
+ - Conflict name: `conflict-${resourceRef}-${fieldPath}-${timestamp}`
2551
+
2552
+ ### 31.8 Conflict Resolution (conflict-controller.js)
2553
+
2554
+ ```javascript
2555
+ resolveConflict({ conflictName, strategy, resolvedValue, resources })
2556
+ ```
2557
+
2558
+ | Strategy | Chosen Value | New Phase |
2559
+ |----------|-------------|-----------|
2560
+ | `prefer-external` | `spec.externalValue` | `Resolved` |
2561
+ | `prefer-kradle` | `spec.localValue` | `Resolved` |
2562
+ | `manual` | `resolvedValue` param (required) | `Resolved` |
2563
+ | `ignore` | undefined | `Ignored` |
2564
+
2565
+ **Superseded check:** `supersededCheck({ resourceRef, fieldPath, resources })` marks all Open conflicts for the same field as `'Superseded'` when a new sync event arrives.
2566
+
2567
+ ### 31.9 Write Intents (write-controller.js)
2568
+
2569
+ **Phase lifecycle:**
2570
+
2571
+ ```
2572
+ PendingApproval → ReadyToSend → Sending → Succeeded
2573
+ ↘ Retrying → Sending (loop, up to maxRetries)
2574
+ ↘ Failed
2575
+ PendingApproval → Rejected
2576
+ ```
2577
+
2578
+ **createWriteIntent({ interfaceKey, operation, payload, resourceRef, requiresApproval, maxRetries }):**
2579
+ - Generates idempotency key via `getIdempotencyKey()`
2580
+ - Initial phase: `PendingApproval` if `requiresApproval`, else `ReadyToSend`
2581
+
2582
+ **approveWriteIntent({ intentName, approvedBy, resources }):**
2583
+ - Validates current phase is `PendingApproval`
2584
+ - Transitions to `ReadyToSend` with approver + timestamp
2585
+
2586
+ **executeWriteIntent({ intentName, resources, executor, onPhaseChange }):**
2587
+ - Validates current phase is `ReadyToSend`
2588
+ - Calls `executor()` (an async function that performs the external API call)
2589
+ - On success: phase → `Succeeded` with `externalResult`
2590
+ - On failure: retries up to `maxRetries`, phase cycles through `Retrying`
2591
+ - After exhausting retries: phase → `Failed` with `lastError`
2592
+
2593
+ ### 31.10 Idempotency Key Generation (write-controller.js)
2594
+
2595
+ ```javascript
2596
+ export function getIdempotencyKey({ interfaceKey, operation, resourceRef, payload }) {
2597
+ const canonical = JSON.stringify({ interfaceKey, operation, resourceRef, payload }, sortedKeys);
2598
+ // djb2 hash algorithm
2599
+ let hash = 5381;
2600
+ for (let i = 0; i < canonical.length; i++) {
2601
+ hash = ((hash << 5) + hash) ^ canonical.charCodeAt(i);
2602
+ hash = hash >>> 0; // Keep 32-bit unsigned
2603
+ }
2604
+ return `idem-${interfaceKey}-${operation}-${hash.toString(16)}`;
2605
+ }
2606
+ ```
2607
+
2608
+ Deterministic — same inputs always produce the same key. Used to prevent duplicate write operations.
2609
+
2610
+ ### 31.11 Persistence Callbacks
2611
+
2612
+ All controllers (sync, conflict, write) accept an optional `persistFn`:
2613
+
2614
+ ```javascript
2615
+ function persist(resource) {
2616
+ if (typeof persistFn === 'function') {
2617
+ Promise.resolve(persistFn(resource)).catch(() => {}); // Fire-and-forget
2618
+ }
2619
+ }
2620
+ ```
2621
+
2622
+ The persistFn is called with a fully-formed K8s-style CRD resource. Errors are swallowed — the caller wires monitoring separately if needed.
2623
+
2624
+ ### 31.12 GitHub Adapter (external/github/)
2625
+
2626
+ **auth.js — JWT Signing and Token Exchange:**
2627
+
2628
+ ```javascript
2629
+ // createGitHubJwt({ appId, privateKey, expiresInSeconds })
2630
+ // - RS256 for PEM keys (production)
2631
+ // - HS256 fallback for non-PEM keys (test mode)
2632
+ // - Returns: header.payload.signature (base64url encoded)
2633
+
2634
+ // exchangeInstallationToken({ appJwt, installationId, fetchImpl })
2635
+ // - POST https://api.github.com/app/installations/{id}/access_tokens
2636
+ // - Returns: { token, expiresAt }
2637
+ ```
2638
+
2639
+ **git-forge.js — GitHubGitForge class:**
2640
+
2641
+ | Method | GitHub API |
2642
+ |--------|-----------|
2643
+ | `listRepositories()` | GET `/installation/repositories` |
2644
+ | `getPullRequest({ repo, pullNumber })` | GET `/repos/{owner}/{repo}/pulls/{number}` |
2645
+ | `createPullRequest({ repo, title, head, base, body })` | POST `/repos/{owner}/{repo}/pulls` |
2646
+ | `mergePullRequest({ repo, pullNumber, mergeMethod, commitTitle })` | PUT `/repos/{owner}/{repo}/pulls/{number}/merge` |
2647
+ | `listRefs({ repo })` | GET `/repos/{owner}/{repo}/branches` + `/tags` (parallel) |
2648
+ | `syncDeployKeys({ repo, desiredKeys })` | GET+DELETE+POST `/repos/{owner}/{repo}/keys` |
2649
+ | `syncBranchProtection({ repo, branch, ... })` | PUT `/repos/{owner}/{repo}/branches/{branch}/protection` |
2650
+
2651
+ **issue-tracking.js — GitHubIssueTracking class:**
2652
+
2653
+ | Method | GitHub API |
2654
+ |--------|-----------|
2655
+ | `listIssues({ repo, state })` | GET `/repos/{owner}/{repo}/issues?state={state}` |
2656
+ | `createIssue({ repo, title, body, labels })` | POST `/repos/{owner}/{repo}/issues` |
2657
+ | `updateIssue({ repo, issueNumber, title, body, labels })` | PATCH `/repos/{owner}/{repo}/issues/{number}` |
2658
+ | `closeIssue({ repo, issueNumber })` | PATCH `/repos/{owner}/{repo}/issues/{number}` (state: closed) |
2659
+ | `listComments({ repo, issueNumber })` | GET `/repos/{owner}/{repo}/issues/{number}/comments` |
2660
+ | `createComment({ repo, issueNumber, body })` | POST `/repos/{owner}/{repo}/issues/{number}/comments` |
2661
+
2662
+ **cicd.js — GitHubCicd class:**
2663
+
2664
+ | Method | GitHub API |
2665
+ |--------|-----------|
2666
+ | `listWorkflowRuns({ repo, workflowId? })` | GET `/repos/{owner}/{repo}/actions/runs` or `/actions/workflows/{id}/runs` |
2667
+ | `listJobs({ repo, runId })` | GET `/repos/{owner}/{repo}/actions/runs/{id}/jobs` |
2668
+ | `rerunWorkflow({ repo, runId })` | POST `/repos/{owner}/{repo}/actions/runs/{id}/rerun` |
2669
+ | `cancelWorkflow({ repo, runId })` | POST `/repos/{owner}/{repo}/actions/runs/{id}/cancel` |
2670
+ | `createCheck({ repo, name, headSha, status, conclusion, detailsUrl, output })` | POST `/repos/{owner}/{repo}/check-runs` |
2671
+ | `updateCheck({ repo, checkRunId, status, conclusion, output })` | PATCH `/repos/{owner}/{repo}/check-runs/{id}` |
2672
+
2673
+ All GitHub classes use `X-GitHub-Api-Version: 2022-11-28` header and Bearer token auth.
2674
+
2675
+ ---
2676
+
2677
+ ## 32. KServe Model Inference Pipeline
2678
+
2679
+ ### KradleInferenceService Controller (`kradle-inference-service-controller.js`)
2680
+
2681
+ - `KradleInferenceService` wraps KServe `InferenceService` CRDs under `serving.kserve.io/v1beta1`
2682
+ - Endpoint discovery from K8s status after KServe readiness
2683
+ - V1 and V2 inference protocol support (`/v1/models/{name}:predict`, `/v2/models/{name}/infer`)
2684
+ - `toProviderConfig` creates an `AgentProviderConfig` with `type: 'kserve'` for agent stack integration
2685
+ - Agent stacks can reference on-cluster models alongside cloud LLMs via the provider bridge
2686
+ - Supported model frameworks: `sklearn`, `xgboost`, `lightgbm`, `tensorflow`, `pytorch`, `onnx`, `triton`, `huggingface`, `custom`
2687
+
2688
+ **Boundary:**
2689
+ ```javascript
2690
+ export const KRADLE_INFERENCE_SERVICE_CONTROLLER_BOUNDARY = {
2691
+ role: 'kradle-inference-service-controller',
2692
+ owns: ['inference service validation', 'KServe manifest generation', 'endpoint resolution',
2693
+ 'provider config bridge', 'inference protocol handling', 'health checks'],
2694
+ delegatesTo: ['resource-model', 'agent-provider-config-controller'],
2695
+ };
2696
+ ```
2697
+
2698
+ **Key exports:** `createInferenceServiceController`, `KRADLE_INFERENCE_SERVICE_CONTROLLER_BOUNDARY`, `SUPPORTED_MODEL_FORMATS`, `INFERENCE_PROTOCOLS`, `KSERVE_API_GROUP`, `KSERVE_API_VERSION`
2699
+
2700
+ ---
2701
+
2702
+ ## 33. Artifact Registry System
2703
+
2704
+ ### Artifact Registry Controller (`artifact-registry-controller.js`)
2705
+
2706
+ Five resource kinds:
2707
+
2708
+ | Kind | Description |
2709
+ |------|-------------|
2710
+ | `ArtifactRegistry` | Top-level registry scoped to an org |
2711
+ | `ArtifactFeed` | Named feed within a registry (e.g. `npm`, `pip`, `docker`) |
2712
+ | `ArtifactAccessPolicy` | Per-feed access control (read/write/admin per subject) |
2713
+ | `ArtifactVersion` | Immutable published artifact version record |
2714
+ | `ArtifactDownload` | Audit record for each artifact download |
2715
+
2716
+ - Supports `npm`, `pip`, `docker`, and `generic` artifact types
2717
+ - Internal storage backend plus external integrations (`s3`, `azure-blob`, `gcs`)
2718
+ - External backend modes: `read-only`, `read-write`, `mirror` (e.g. GitHub Packages proxy)
2719
+ - Protocol-specific install command generation per feed type
2720
+ - Access policy enforcement: `read`, `write`, `admin` permissions per feed per subject
2721
+
2722
+ **Boundary:**
2723
+ ```javascript
2724
+ export const ARTIFACT_REGISTRY_CONTROLLER_BOUNDARY = {
2725
+ role: 'artifact-registry-controller',
2726
+ owns: ['registry validation', 'feed management', 'access policy enforcement',
2727
+ 'artifact publish', 'artifact version listing', 'artifact deletion', 'download audit'],
2728
+ delegatesTo: ['resource-model', 'external-backend-binding'],
2729
+ mustNotOwn: ['secret values', 'blob storage I/O', 'network transport'],
2730
+ };
2731
+ ```
2732
+
2733
+ **Key exports:** `createArtifactRegistryController`, `ARTIFACT_REGISTRY_CONTROLLER_BOUNDARY`
2734
+
2735
+ ---
2736
+
2737
+ ## 34. Assistant Agent
2738
+
2739
+ ### Assistant Runtime (`assistant-runtime.js`)
2740
+
2741
+ - In-process runtime using the Anthropic API (no K8s Job dispatch)
2742
+ - Chat sessions with message history persisted via `globalThis` session store
2743
+ - `createAssistantRuntime` returns: `chat`, `generate`, `listSessions`, `clearSession`
2744
+ - Structured generation endpoint (`generate`) for dynamic content and tool-augmented agentic calls
2745
+ - Default `assistant` AgentStack deployed via the Helm chart (`values.yaml` default stacks)
2746
+ - Stack selector allows different `AgentStack` CRDs per conversation context
2747
+ - `callModel` handles HTTP to Anthropic API with support for tool definitions and response format constraints
2748
+
2749
+ **Boundary:**
2750
+ ```javascript
2751
+ export const ASSISTANT_RUNTIME_BOUNDARY = {
2752
+ role: 'assistant-runtime',
2753
+ owns: ['chat sessions', 'message history', 'model API calls', 'structured agentic calls', 'session lifecycle'],
2754
+ delegatesTo: ['resource-model', 'agent-stack-controller', 'agent-mux-client'],
2755
+ mustNotOwn: ['secret values', 'K8s Job dispatch', 'resource persistence'],
2756
+ };
2757
+ ```
2758
+
2759
+ **Key exports:** `createAssistantRuntime`, `ASSISTANT_RUNTIME_BOUNDARY`, `defaultAssistantConfig`, `defaultSystemPrompt`, `callModel`