@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,48 @@
1
+ import { createKubernetesResourceClient, repositoryManifest } from './kubernetes-controller.js';
2
+
3
+ export const KUBERNETES_RESOURCE_GATEWAY_BOUNDARY = {
4
+ role: 'kubernetes-resource-gateway',
5
+ scope: 'Application port translating API controller intent into Kubernetes resource-client operations',
6
+ owns: ['resource definitions', 'list/get/apply/delete/watch delegation', 'Repository manifest application', 'namespace scoping'],
7
+ delegatesTo: ['kubernetes-resource-client'],
8
+ mustNotOwn: ['HTTP routes', 'Next.js page flow decisions', 'forge DTO composition', 'Kubernetes reconciliation scheduling']
9
+ };
10
+
11
+ export function createKubernetesResourceGateway(options = {}) {
12
+ const resourceClient = options.resourceClient || options.kubernetesClient || createKubernetesResourceClient(options);
13
+ const namespace = options.namespace || resourceClient.namespace || process.env.KRADLE_NAMESPACE || 'kradle-system';
14
+
15
+ return {
16
+ ...KUBERNETES_RESOURCE_GATEWAY_BOUNDARY,
17
+ namespace,
18
+ resourceDefinitions: resourceClient.resourceDefinitions,
19
+ async snapshot() {
20
+ return resourceClient.snapshot();
21
+ },
22
+ async list(kindOrPlural) {
23
+ return resourceClient.listResource(kindOrPlural);
24
+ },
25
+ async get(kindOrPlural, name) {
26
+ return resourceClient.getResource(kindOrPlural, name);
27
+ },
28
+ async apply(resource) {
29
+ return resourceClient.applyResource(resource);
30
+ },
31
+ async delete(kindOrPlural, name) {
32
+ return resourceClient.deleteResource(kindOrPlural, name);
33
+ },
34
+ async createRepository(input) {
35
+ return resourceClient.applyResource(repositoryManifest(input, namespace));
36
+ },
37
+ async createOrganization(input) {
38
+ return resourceClient.createOrganization(input);
39
+ },
40
+ watch(resourcePath, handlers = {}) {
41
+ return resourceClient.watchResource(resourcePath, handlers);
42
+ }
43
+ };
44
+ }
45
+
46
+
47
+
48
+
@@ -0,0 +1,364 @@
1
+ // KradleModelRoute Controller
2
+ // Unified model routing through Envoy AI Gateway — validation, endpoint resolution,
3
+ // catalog generation, and Envoy manifest generation for both internal (KServe) and
4
+ // external (cloud LLM) model routes.
5
+
6
+ export const ENVOY_AI_GATEWAY_API_GROUP = 'aigateway.envoyproxy.io';
7
+ export const ENVOY_AI_GATEWAY_API_VERSION = 'v1alpha1';
8
+
9
+ export const VALID_ROUTE_TYPES = ['internal', 'external'];
10
+
11
+ export const VALID_EXTERNAL_PROTOCOLS = ['openai', 'anthropic', 'bedrock', 'vertex', 'azure-openai', 'custom'];
12
+
13
+ export const MODEL_ROUTE_CONTROLLER_BOUNDARY = {
14
+ role: 'model-route-controller',
15
+ scope: 'Unified model routing through Envoy AI Gateway — validation, endpoint resolution, catalog generation, Envoy manifest generation',
16
+ owns: ['route validation', 'endpoint resolution', 'model catalog', 'envoy route manifests'],
17
+ delegatesTo: ['resource-model', 'kradle-inference-service-controller'],
18
+ mustNotOwn: ['secret values', 'gateway deployment', 'network policy']
19
+ };
20
+
21
+ /**
22
+ * Validate a KradleModelRoute resource. Returns { valid, errors }.
23
+ * @param {object} resource
24
+ * @returns {{ valid: boolean, errors: string[] }}
25
+ */
26
+ export function validateModelRoute(resource) {
27
+ const errors = [];
28
+
29
+ if (resource == null) {
30
+ errors.push('resource must not be null or undefined');
31
+ return { valid: false, errors };
32
+ }
33
+
34
+ if (!resource?.metadata?.name) {
35
+ errors.push('metadata.name is required');
36
+ }
37
+
38
+ const spec = resource?.spec || {};
39
+
40
+ if (!spec.organizationRef) {
41
+ errors.push('spec.organizationRef is required');
42
+ }
43
+
44
+ if (!spec.modelName) {
45
+ errors.push('spec.modelName is required');
46
+ }
47
+
48
+ if (!spec.routeType) {
49
+ errors.push('spec.routeType is required; valid values are: internal, external');
50
+ } else if (!VALID_ROUTE_TYPES.includes(spec.routeType)) {
51
+ errors.push(`spec.routeType "${spec.routeType}" is not valid; valid values are: ${VALID_ROUTE_TYPES.join(', ')}`);
52
+ }
53
+
54
+ // Route-type-specific validation
55
+ if (spec.routeType === 'internal') {
56
+ if (!spec.inferenceServiceRef) {
57
+ errors.push('spec.inferenceServiceRef is required for internal routes');
58
+ }
59
+ }
60
+
61
+ if (spec.routeType === 'external') {
62
+ if (!spec.external) {
63
+ errors.push('spec.external is required for external routes');
64
+ } else {
65
+ if (!spec.external.provider) {
66
+ errors.push('spec.external.provider is required for external routes');
67
+ }
68
+ if (!spec.external.endpoint) {
69
+ errors.push('spec.external.endpoint is required for external routes');
70
+ }
71
+ if (spec.external.protocol && !VALID_EXTERNAL_PROTOCOLS.includes(spec.external.protocol)) {
72
+ errors.push(`spec.external.protocol "${spec.external.protocol}" is not valid; valid values are: ${VALID_EXTERNAL_PROTOCOLS.join(', ')}`);
73
+ }
74
+ }
75
+ }
76
+
77
+ return { valid: errors.length === 0, errors };
78
+ }
79
+
80
+ /**
81
+ * Factory that returns a KradleModelRoute controller instance.
82
+ * @param {object} [options]
83
+ * @returns {object}
84
+ */
85
+ export function createModelRouteController(options = {}) {
86
+ return {
87
+ role: 'model-route-controller',
88
+
89
+ /**
90
+ * Validate a KradleModelRoute resource.
91
+ * @param {object} resource
92
+ * @returns {{ valid: boolean, errors: string[] }}
93
+ */
94
+ validate(resource) {
95
+ return validateModelRoute(resource);
96
+ },
97
+
98
+ /**
99
+ * Resolve a route to its endpoint and connection details.
100
+ * For internal routes, looks up the InferenceService status.url.
101
+ * For external routes, uses spec.external fields.
102
+ *
103
+ * @param {object} route - KradleModelRoute resource
104
+ * @param {object[]} [resources] - all cluster resources to look up InferenceService
105
+ * @returns {{ endpoint: string, protocol: string, provider: string, modelId: string, authConfig: object|null }}
106
+ */
107
+ resolveRoute(route, resources = []) {
108
+ const spec = route?.spec || {};
109
+
110
+ if (spec.routeType === 'internal') {
111
+ return resolveInternalRoute(route, resources);
112
+ }
113
+
114
+ if (spec.routeType === 'external') {
115
+ return resolveExternalRoute(route);
116
+ }
117
+
118
+ return {
119
+ endpoint: null,
120
+ protocol: 'unknown',
121
+ provider: 'unknown',
122
+ modelId: spec.modelName || null,
123
+ authConfig: null
124
+ };
125
+ },
126
+
127
+ /**
128
+ * Reconcile a set of routes, checking that each target is reachable.
129
+ * Returns conditions for each route indicating readiness.
130
+ *
131
+ * @param {object[]} routes - array of KradleModelRoute resources
132
+ * @param {object[]} [resources] - all cluster resources
133
+ * @returns {{ conditions: object[], resolvedRoutes: object[] }}
134
+ */
135
+ reconcileRoutes(routes, resources = []) {
136
+ const conditions = [];
137
+ const resolvedRoutes = [];
138
+
139
+ for (const route of routes) {
140
+ const validation = validateModelRoute(route);
141
+ if (!validation.valid) {
142
+ conditions.push({
143
+ type: 'RouteReady',
144
+ status: 'False',
145
+ route: route.metadata?.name,
146
+ reason: 'ValidationFailed',
147
+ message: validation.errors.join('; ')
148
+ });
149
+ continue;
150
+ }
151
+
152
+ const resolved = this.resolveRoute(route, resources);
153
+ if (!resolved.endpoint) {
154
+ conditions.push({
155
+ type: 'RouteReady',
156
+ status: 'False',
157
+ route: route.metadata?.name,
158
+ reason: 'EndpointNotFound',
159
+ message: `Could not resolve endpoint for route "${route.metadata?.name}"`
160
+ });
161
+ continue;
162
+ }
163
+
164
+ conditions.push({
165
+ type: 'RouteReady',
166
+ status: 'True',
167
+ route: route.metadata?.name,
168
+ reason: 'EndpointResolved',
169
+ message: `Route "${route.metadata?.name}" resolved to ${resolved.endpoint}`
170
+ });
171
+
172
+ resolvedRoutes.push({
173
+ name: route.metadata?.name,
174
+ modelName: route.spec?.modelName,
175
+ routeType: route.spec?.routeType,
176
+ ...resolved
177
+ });
178
+ }
179
+
180
+ return { conditions, resolvedRoutes };
181
+ },
182
+
183
+ /**
184
+ * Generate a unified model catalog merging internal and external routes.
185
+ *
186
+ * @param {object[]} routes - array of KradleModelRoute resources
187
+ * @param {object[]} [resources] - all cluster resources
188
+ * @returns {Array<{ name: string, provider: string, type: string, status: string, endpoint: string, protocol: string }>}
189
+ */
190
+ listModelCatalog(routes, resources = []) {
191
+ const catalog = [];
192
+
193
+ for (const route of routes) {
194
+ const spec = route?.spec || {};
195
+ const validation = validateModelRoute(route);
196
+ const resolved = validation.valid ? this.resolveRoute(route, resources) : null;
197
+
198
+ catalog.push({
199
+ name: spec.modelName || route.metadata?.name || 'unknown',
200
+ provider: resolved?.provider || (spec.routeType === 'internal' ? 'kserve' : spec.external?.provider || 'unknown'),
201
+ type: spec.routeType || 'unknown',
202
+ status: resolved?.endpoint ? 'available' : 'unavailable',
203
+ endpoint: resolved?.endpoint || null,
204
+ protocol: resolved?.protocol || (spec.routeType === 'internal' ? 'v2' : spec.external?.protocol || 'unknown')
205
+ });
206
+ }
207
+
208
+ return catalog;
209
+ },
210
+
211
+ /**
212
+ * Generate an Envoy AI Gateway route configuration for a resolved route.
213
+ *
214
+ * @param {object} route - KradleModelRoute resource
215
+ * @param {{ endpoint: string, protocol: string, provider: string, modelId: string }} resolvedEndpoint
216
+ * @returns {object} Envoy AI Gateway route config
217
+ */
218
+ generateEnvoyRouteManifest(route, resolvedEndpoint) {
219
+ const spec = route?.spec || {};
220
+ const metadata = route?.metadata || {};
221
+
222
+ const routeConfig = {
223
+ apiVersion: `${ENVOY_AI_GATEWAY_API_GROUP}/${ENVOY_AI_GATEWAY_API_VERSION}`,
224
+ kind: 'AIGatewayRoute',
225
+ metadata: {
226
+ name: `route-${metadata.name || 'unknown'}`,
227
+ namespace: metadata.namespace,
228
+ labels: {
229
+ 'kradle.a5c.ai/managed': 'true',
230
+ 'kradle.a5c.ai/org': spec.organizationRef || '',
231
+ 'kradle.a5c.ai/model-route': metadata.name || ''
232
+ }
233
+ },
234
+ spec: {
235
+ modelName: spec.modelName,
236
+ targetRef: {
237
+ kind: spec.routeType === 'internal' ? 'Service' : 'ExternalBackend',
238
+ name: spec.routeType === 'internal'
239
+ ? (spec.inferenceServiceRef || metadata.name)
240
+ : `ext-${resolvedEndpoint.provider || 'provider'}`
241
+ },
242
+ rules: [{
243
+ matches: [{
244
+ headers: [{
245
+ name: 'x-model-name',
246
+ value: spec.modelName
247
+ }]
248
+ }],
249
+ backendRefs: [{
250
+ name: resolvedEndpoint.endpoint,
251
+ protocol: resolvedEndpoint.protocol,
252
+ modelId: resolvedEndpoint.modelId || spec.modelName
253
+ }]
254
+ }]
255
+ }
256
+ };
257
+
258
+ // Add rate limits if present
259
+ if (spec.rateLimits) {
260
+ routeConfig.spec.rateLimits = spec.rateLimits;
261
+ }
262
+
263
+ // Add timeout if present
264
+ if (spec.timeout) {
265
+ routeConfig.spec.timeout = spec.timeout;
266
+ }
267
+
268
+ return routeConfig;
269
+ },
270
+
271
+ /**
272
+ * Convert a KServe InferenceService into a KradleModelRoute spec.
273
+ * Bridge method for migrating existing services into the unified routing model.
274
+ *
275
+ * @param {object} inferenceService - KServe InferenceService resource
276
+ * @param {{ endpoint: string, protocol?: string }} resolvedEndpoint
277
+ * @returns {object} KradleModelRoute spec fields
278
+ */
279
+ toModelRoute(inferenceService, resolvedEndpoint) {
280
+ const metadata = inferenceService?.metadata || {};
281
+ const spec = inferenceService?.spec || {};
282
+ const predictor = spec.predictor || {};
283
+ const modelFormat = predictor.modelFormat?.name || spec.modelFormat || 'custom';
284
+
285
+ return {
286
+ apiVersion: 'kradle.a5c.ai/v1alpha1',
287
+ kind: 'KradleModelRoute',
288
+ metadata: {
289
+ name: `route-${metadata.name || 'unknown'}`,
290
+ namespace: metadata.namespace,
291
+ labels: {
292
+ 'kradle.a5c.ai/managed': 'true',
293
+ 'kradle.a5c.ai/source': 'inference-service',
294
+ ...(metadata.labels || {})
295
+ }
296
+ },
297
+ spec: {
298
+ organizationRef: metadata.labels?.['kradle.a5c.ai/org'] || spec.organizationRef || 'default',
299
+ modelName: metadata.name || 'unknown',
300
+ routeType: 'internal',
301
+ inferenceServiceRef: metadata.name,
302
+ modelFormat,
303
+ endpoint: resolvedEndpoint.endpoint,
304
+ protocol: resolvedEndpoint.protocol || 'v2'
305
+ }
306
+ };
307
+ }
308
+ };
309
+ }
310
+
311
+ // ---------------------------------------------------------------------------
312
+ // Internal helpers
313
+ // ---------------------------------------------------------------------------
314
+
315
+ function resolveInternalRoute(route, resources) {
316
+ const spec = route?.spec || {};
317
+ const inferenceServiceRef = spec.inferenceServiceRef;
318
+
319
+ // Look up the InferenceService in the cluster resources
320
+ let endpoint = null;
321
+ let protocol = spec.protocol || 'v2';
322
+
323
+ if (inferenceServiceRef && Array.isArray(resources)) {
324
+ const isvc = resources.find(
325
+ r => (r.kind === 'InferenceService' || r.kind === 'KradleInferenceService') &&
326
+ r.metadata?.name === inferenceServiceRef
327
+ );
328
+ if (isvc) {
329
+ endpoint = isvc.status?.url || isvc.status?.address?.url || null;
330
+ if (!endpoint && isvc.metadata?.name) {
331
+ const ns = isvc.metadata.namespace || 'default';
332
+ endpoint = `http://${isvc.metadata.name}.${ns}.svc.cluster.local`;
333
+ }
334
+ protocol = isvc.spec?.inferenceProtocol || protocol;
335
+ }
336
+ }
337
+
338
+ // Fallback: build cluster-internal URL from the reference name
339
+ if (!endpoint && inferenceServiceRef) {
340
+ const ns = route.metadata?.namespace || 'default';
341
+ endpoint = `http://${inferenceServiceRef}.${ns}.svc.cluster.local`;
342
+ }
343
+
344
+ return {
345
+ endpoint,
346
+ protocol,
347
+ provider: 'kserve',
348
+ modelId: spec.modelName || inferenceServiceRef || null,
349
+ authConfig: spec.authConfig || null
350
+ };
351
+ }
352
+
353
+ function resolveExternalRoute(route) {
354
+ const spec = route?.spec || {};
355
+ const ext = spec.external || {};
356
+
357
+ return {
358
+ endpoint: ext.endpoint || null,
359
+ protocol: ext.protocol || 'openai',
360
+ provider: ext.provider || 'unknown',
361
+ modelId: ext.modelId || spec.modelName || null,
362
+ authConfig: ext.authConfig || null
363
+ };
364
+ }
@@ -0,0 +1,178 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ export const NOTIFICATION_CONTROLLER_BOUNDARY = {
4
+ role: 'notification-controller',
5
+ scope: 'User notification lifecycle: creation from events, querying, read state, and preferences',
6
+ owns: ['notification creation', 'notification listing', 'read state', 'user preferences'],
7
+ delegatesTo: [],
8
+ mustNotOwn: ['event dispatch', 'UI rendering', 'push delivery']
9
+ };
10
+
11
+ const NOTIFICATION_TYPES = {
12
+ 'run-complete': { severity: 'info' },
13
+ 'approval-needed': { severity: 'warning' },
14
+ 'conflict-detected': { severity: 'warning' },
15
+ 'workspace-ready': { severity: 'info' },
16
+ 'system': { severity: 'info' },
17
+ };
18
+
19
+ const DEFAULT_PREFERENCES = {
20
+ runs: true,
21
+ approvals: true,
22
+ conflicts: true,
23
+ workspaces: true,
24
+ sound: false,
25
+ desktop: false,
26
+ };
27
+
28
+ export function createNotificationController() {
29
+ // Map of org -> notifications[]
30
+ const store = new Map();
31
+ // Map of userId -> preferences
32
+ const prefsStore = new Map();
33
+
34
+ function getOrgNotifications(org) {
35
+ if (!store.has(org)) store.set(org, []);
36
+ return store.get(org);
37
+ }
38
+
39
+ function mapEventToNotification(event) {
40
+ const { type, status, name, action, resourceRef, org = 'default' } = event || {};
41
+
42
+ let notifType = 'system';
43
+ let title = 'System event';
44
+ let message = '';
45
+ let severity = 'info';
46
+
47
+ if (type === 'AgentDispatchRun') {
48
+ notifType = 'run-complete';
49
+ const runName = name || event?.metadata?.name || 'Unknown';
50
+ if (status === 'completed') {
51
+ title = `Run ${runName} completed`;
52
+ message = `Agent dispatch run "${runName}" completed successfully.`;
53
+ severity = 'info';
54
+ } else if (status === 'failed') {
55
+ title = `Run ${runName} failed`;
56
+ message = `Agent dispatch run "${runName}" failed.`;
57
+ severity = 'error';
58
+ } else {
59
+ title = `Run ${runName} updated`;
60
+ message = `Agent dispatch run "${runName}" status: ${status || 'unknown'}.`;
61
+ severity = 'info';
62
+ }
63
+ } else if (type === 'AgentApproval' && status === 'pending') {
64
+ notifType = 'approval-needed';
65
+ const actionLabel = action || event?.spec?.action || 'unknown action';
66
+ title = `Approval needed for ${actionLabel}`;
67
+ message = `An agent is requesting approval for: ${actionLabel}.`;
68
+ severity = 'warning';
69
+ } else if (type === 'ExternalSyncConflict') {
70
+ notifType = 'conflict-detected';
71
+ const resource = resourceRef || event?.spec?.resourceRef || name || 'unknown resource';
72
+ title = `Conflict in ${resource}`;
73
+ message = `A sync conflict was detected in resource: ${resource}.`;
74
+ severity = 'warning';
75
+ } else if (type === 'KradleWorkspace' && event?.claimed) {
76
+ notifType = 'workspace-ready';
77
+ const wsName = name || event?.metadata?.name || 'Unknown';
78
+ const runRef = event?.claimedBy || event?.spec?.claimedBy || 'a run';
79
+ title = `Workspace ${wsName} claimed by ${runRef}`;
80
+ message = `Workspace "${wsName}" has been claimed by "${runRef}".`;
81
+ severity = 'info';
82
+ }
83
+
84
+ return { notifType, title, message, severity, org };
85
+ }
86
+
87
+ return {
88
+ role: 'notification-controller',
89
+
90
+ createNotification(event) {
91
+ const { notifType, title, message, severity, org } = mapEventToNotification(event);
92
+
93
+ const notification = {
94
+ id: randomUUID(),
95
+ type: notifType,
96
+ title,
97
+ message,
98
+ severity,
99
+ resourceRef: event?.resourceRef || null,
100
+ createdAt: new Date().toISOString(),
101
+ read: false,
102
+ org,
103
+ };
104
+
105
+ const notifications = getOrgNotifications(org);
106
+ notifications.push(notification);
107
+
108
+ return notification;
109
+ },
110
+
111
+ listNotifications(org, opts = {}) {
112
+ const { unreadOnly = false, limit = 20, since = null } = opts;
113
+ let notifications = [...getOrgNotifications(org)];
114
+
115
+ // Filter by read state
116
+ if (unreadOnly) {
117
+ notifications = notifications.filter((n) => !n.read);
118
+ }
119
+
120
+ // Filter by since timestamp
121
+ if (since) {
122
+ const sinceDate = new Date(since).getTime();
123
+ notifications = notifications.filter((n) => new Date(n.createdAt).getTime() > sinceDate);
124
+ }
125
+
126
+ // Newest first
127
+ notifications.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
128
+
129
+ // Cap to limit
130
+ if (limit > 0) {
131
+ notifications = notifications.slice(0, limit);
132
+ }
133
+
134
+ return notifications;
135
+ },
136
+
137
+ markAsRead(notificationId) {
138
+ for (const notifications of store.values()) {
139
+ const notification = notifications.find((n) => n.id === notificationId);
140
+ if (notification) {
141
+ notification.read = true;
142
+ return true;
143
+ }
144
+ }
145
+ return false;
146
+ },
147
+
148
+ markAllAsRead(org) {
149
+ const notifications = getOrgNotifications(org);
150
+ let count = 0;
151
+ for (const notification of notifications) {
152
+ if (!notification.read) {
153
+ notification.read = true;
154
+ count++;
155
+ }
156
+ }
157
+ return count;
158
+ },
159
+
160
+ getUnreadCount(org) {
161
+ return getOrgNotifications(org).filter((n) => !n.read).length;
162
+ },
163
+
164
+ getPreferences(userId) {
165
+ if (!prefsStore.has(userId)) {
166
+ return { ...DEFAULT_PREFERENCES };
167
+ }
168
+ return { ...DEFAULT_PREFERENCES, ...prefsStore.get(userId) };
169
+ },
170
+
171
+ updatePreferences(userId, prefs) {
172
+ const existing = prefsStore.get(userId) || {};
173
+ const merged = { ...DEFAULT_PREFERENCES, ...existing, ...prefs };
174
+ prefsStore.set(userId, merged);
175
+ return merged;
176
+ },
177
+ };
178
+ }