@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,549 @@
1
+ import { createPermissionReviewer } from './agent-permission-review.js';
2
+ import { createAgentStackController } from './agent-stack-controller.js';
3
+ import { createAgentPersonaController } from './agent-persona-controller.js';
4
+ import { composeAgentPrompt } from './agent-prompt-composition.js';
5
+ import { legacyAgentStackIdentityWarning } from './agent-identity-migration.js';
6
+ import { assembleContextBundle } from './agent-context-bundles.js';
7
+ import { createResource, clone } from './resource-model.js';
8
+ import { createAgentMuxClient } from './agent-mux-client.js';
9
+ import { createAgentMemoryController } from './agent-memory-controller.js';
10
+ import { createAgentApprovalController } from './agent-approval-controller.js';
11
+ import { createAgentWorkspaceController } from './agent-workspace-controller.js';
12
+ import { createHooksLifecycleEmitter } from './hooks-lifecycle.js';
13
+ import { createJitsiAgentBridge } from './jitsi-agent-bridge.js';
14
+
15
+ const MODEL_RATES = {
16
+ 'claude-sonnet-4-20250514': { inputPer1k: 0.003, outputPer1k: 0.015 },
17
+ 'claude-opus-4-20250514': { inputPer1k: 0.015, outputPer1k: 0.075 },
18
+ 'claude-haiku-4-20250514': { inputPer1k: 0.0008, outputPer1k: 0.004 },
19
+ 'gpt-4o': { inputPer1k: 0.005, outputPer1k: 0.015 },
20
+ 'gpt-4o-mini': { inputPer1k: 0.00015, outputPer1k: 0.0006 },
21
+ 'gemini-1.5-pro': { inputPer1k: 0.00125, outputPer1k: 0.005 },
22
+ };
23
+ const DEFAULT_RATE = { inputPer1k: 0.003, outputPer1k: 0.015 };
24
+
25
+ function estimateCost(event) {
26
+ if (!event?.usage) return 0;
27
+ const model = event.model || 'claude-sonnet-4-20250514';
28
+ const rate = MODEL_RATES[model] || DEFAULT_RATE;
29
+ const inputCost = ((event.usage.inputTokens || 0) / 1000) * rate.inputPer1k;
30
+ const outputCost = ((event.usage.outputTokens || 0) / 1000) * rate.outputPer1k;
31
+ return inputCost + outputCost;
32
+ }
33
+
34
+ export const AGENT_DISPATCH_CONTROLLER_BOUNDARY = {
35
+ role: 'agent-dispatch-controller',
36
+ scope: 'Manual dispatch orchestration with permission gating, context assembly, workspace provisioning, stack resolution, K8s Job dispatch, and event persistence',
37
+ owns: ['dispatch creation', 'attempt lifecycle', 'K8s Job dispatch binding', 'workspace provisioning', 'stack resolution', 'session event persistence'],
38
+ delegatesTo: ['agent-permission-review', 'agent-stack-controller', 'agent-context-bundles', 'agent-mux-client', 'agent-memory-controller', 'agent-approval-controller', 'agent-workspace-controller'],
39
+ mustNotOwn: ['secret values', 'UI rendering']
40
+ };
41
+
42
+ export function createAgentDispatchController(options = {}) {
43
+ const permissionReviewer = options.permissionReviewer || createPermissionReviewer();
44
+ const stackController = options.stackController || createAgentStackController();
45
+ const personaController = options.personaController || createAgentPersonaController();
46
+ const agentMuxClient = options.agentMuxClient || createAgentMuxClient();
47
+ const memoryController = options.memoryController || createAgentMemoryController();
48
+ const approvalController = options.approvalController || createAgentApprovalController();
49
+ const workspaceController = options.workspaceController || createAgentWorkspaceController();
50
+ const eventBus = options.eventBus || null;
51
+ const lifecycleEmitter = options.lifecycleEmitter || (eventBus ? createHooksLifecycleEmitter(eventBus) : null);
52
+ const hookBridge = options.hookBridge || null;
53
+ const jitsiAgentBridge = options.jitsiAgentBridge || createJitsiAgentBridge({
54
+ meetingController: options.jitsiMeetingController,
55
+ eventBus,
56
+ });
57
+ const logger = options.logger || console;
58
+
59
+ return {
60
+ role: 'agent-dispatch-controller',
61
+
62
+ /**
63
+ * Resolve an AgentStack CRD into a concrete execution config for agent-mux.
64
+ *
65
+ * @param {object} stack - AgentStack resource
66
+ * @param {{ organizationRef?: string }} opts
67
+ * @returns {{ adapter: string, provider: string, model: string, prompt: object, mcpServers: string[], skills: string[], approvalMode: string, env: Record<string,string> }}
68
+ */
69
+ resolveStack(stack, { organizationRef = 'default', identity = null } = {}) {
70
+ if (!stack || !stack.spec) {
71
+ throw new Error('resolveStack requires a valid AgentStack resource with spec');
72
+ }
73
+ const spec = stack.spec;
74
+
75
+ // Merge flat mcpServerRefs with structured externalTools.mcpServerRefs (deduplicated)
76
+ const mergedMcpServers = [
77
+ ...(spec.mcpServerRefs || []),
78
+ ...(spec.externalTools?.mcpServerRefs || [])
79
+ ].filter((v, i, a) => a.indexOf(v) === i);
80
+
81
+ const memoryConfig = {};
82
+ if (spec.memoryRepositoryRefs && spec.memoryRepositoryRefs.length > 0) {
83
+ memoryConfig.memoryRepositoryRefs = clone(spec.memoryRepositoryRefs);
84
+ }
85
+ if (spec.memorySnapshotRef) {
86
+ memoryConfig.memorySnapshotRef = spec.memorySnapshotRef;
87
+ }
88
+
89
+ const prompt = identity
90
+ ? composeAgentPrompt({ ...identity, stack })
91
+ : {
92
+ system: spec.systemPrompt || null,
93
+ developer: spec.developerPrompt || null,
94
+ task: spec.taskPrompt || null,
95
+ };
96
+
97
+ return {
98
+ adapter: spec.adapter || spec.baseAgent || 'claude-code',
99
+ provider: spec.provider || 'anthropic',
100
+ model: spec.model || 'claude-sonnet-4-20250514',
101
+ prompt,
102
+ mcpServers: clone(mergedMcpServers),
103
+ skills: clone(spec.skillRefs || []),
104
+ approvalMode: spec.approvalMode || 'prompt',
105
+ env: {
106
+ KRADLE_ORG: organizationRef,
107
+ KRADLE_STACK_NAME: stack.metadata?.name || 'unknown',
108
+ },
109
+ memoryConfig: Object.keys(memoryConfig).length > 0 ? memoryConfig : null,
110
+ };
111
+ },
112
+
113
+ /**
114
+ * Check if the run has exceeded its token or cost budget.
115
+ *
116
+ * @param {object} run - AgentDispatchRun resource
117
+ * @param {object} event - SSE event with optional usage field
118
+ * @returns {{ exceeded: boolean, reason?: string, current?: number, limit?: number, totalTokens?: number, totalCost?: number }}
119
+ */
120
+ checkBudget(run, event) {
121
+ const maxTokens = run.spec?.budget?.maxTokens ?? Infinity;
122
+ const maxCostUsd = run.spec?.budget?.maxCostUsd ?? Infinity;
123
+ const currentTokens = run.status?.tokenUsage?.totalTokens || 0;
124
+ const eventTokens = event?.usage?.totalTokens
125
+ || ((event?.usage?.inputTokens || 0) + (event?.usage?.outputTokens || 0));
126
+ const totalTokens = currentTokens + eventTokens;
127
+ const currentCost = run.status?.costUsd || 0;
128
+ const totalCost = currentCost + estimateCost(event);
129
+
130
+ if (totalTokens > maxTokens) {
131
+ return { exceeded: true, reason: 'token_limit', current: totalTokens, limit: maxTokens };
132
+ }
133
+ if (totalCost > maxCostUsd) {
134
+ return { exceeded: true, reason: 'cost_limit', current: totalCost, limit: maxCostUsd };
135
+ }
136
+ return { exceeded: false, totalTokens, totalCost };
137
+ },
138
+
139
+ /**
140
+ * Persist a session event from an agent-mux session.
141
+ * Appends to the transcript, updates the dispatch attempt status,
142
+ * marks the run as Completed/Failed on terminal events, and emits to event bus.
143
+ *
144
+ * @param {object} event - The SSE event object
145
+ * @param {object} run - AgentDispatchRun resource (mutated in place)
146
+ * @param {object} attempt - AgentDispatchAttempt resource (mutated in place)
147
+ * @param {{ namespace?: string, organizationRef?: string, transcript?: object }} opts
148
+ * @returns {{ run: object, attempt: object, transcript: object, notification: object|null }}
149
+ */
150
+ persistSessionEvent(event, run, attempt, { namespace = 'default', organizationRef = 'default', transcript = null } = {}) {
151
+ if (!event || typeof event !== 'object') {
152
+ return { run, attempt, transcript, notification: null };
153
+ }
154
+
155
+ // Budget enforcement — check before persisting
156
+ const budgetCheck = this.checkBudget(run, event);
157
+ if (budgetCheck.exceeded) {
158
+ const now = new Date().toISOString();
159
+ run.status.phase = 'Failed';
160
+ run.status.failedAt = now;
161
+ run.status.failureReason = 'budget_exceeded';
162
+ run.status.budgetExceeded = { reason: budgetCheck.reason, current: budgetCheck.current, limit: budgetCheck.limit };
163
+ attempt.status.failedAt = now;
164
+ attempt.status.failureReason = 'budget_exceeded';
165
+ if (eventBus) {
166
+ eventBus.emit({ type: 'run-complete', status: 'failed', name: run.metadata?.name, reason: 'budget_exceeded', timestamp: now });
167
+ }
168
+ const budgetNotification = { type: 'run-complete', status: 'failed', name: run.metadata?.name, reason: 'budget_exceeded', timestamp: now };
169
+ return { run, attempt, transcript, notification: budgetNotification };
170
+ }
171
+
172
+ const now = new Date().toISOString();
173
+
174
+ // 1. Append to AgentSessionTranscript
175
+ if (!transcript) {
176
+ const sessionRef = attempt?.status?.agentMuxSessionId || 'unknown';
177
+ transcript = createResource('AgentSessionTranscript', { name: `transcript-${sessionRef}`, namespace }, {
178
+ organizationRef,
179
+ sessionRef,
180
+ messages: [],
181
+ cost: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
182
+ }, { phase: 'Streaming', startedAt: now });
183
+ }
184
+
185
+ const role = event.role || 'system';
186
+ const content = event.content || event.text || event.message || '';
187
+ const message = {
188
+ role,
189
+ content: typeof content === 'string' ? content : JSON.stringify(content),
190
+ timestamp: event.timestamp || now,
191
+ };
192
+ if (event.toolUse) message.toolUse = event.toolUse;
193
+ if (event.toolResult) message.toolResult = event.toolResult;
194
+ transcript.spec.messages.push(message);
195
+
196
+ // Accumulate token usage
197
+ if (event.usage) {
198
+ transcript.spec.cost.inputTokens += event.usage.inputTokens || 0;
199
+ transcript.spec.cost.outputTokens += event.usage.outputTokens || 0;
200
+ transcript.spec.cost.totalTokens = transcript.spec.cost.inputTokens + transcript.spec.cost.outputTokens;
201
+
202
+ if (!run.status.tokenUsage) run.status.tokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
203
+ run.status.tokenUsage.inputTokens = (run.status.tokenUsage.inputTokens || 0) + (event.usage.inputTokens || 0);
204
+ run.status.tokenUsage.outputTokens = (run.status.tokenUsage.outputTokens || 0) + (event.usage.outputTokens || 0);
205
+ run.status.tokenUsage.totalTokens = run.status.tokenUsage.inputTokens + run.status.tokenUsage.outputTokens;
206
+ run.status.costUsd = (run.status.costUsd || 0) + estimateCost(event);
207
+ }
208
+
209
+ // 2. Update AgentDispatchAttempt status
210
+ attempt.status.lastEventAt = now;
211
+ attempt.status.eventCount = (attempt.status.eventCount || 0) + 1;
212
+
213
+ // 3. Terminal event handling
214
+ let notification = null;
215
+ if (event.type === 'completion') {
216
+ run.status.phase = 'Completed';
217
+ run.status.completedAt = now;
218
+ attempt.status.completedAt = now;
219
+ transcript.status.phase = 'Reconciled';
220
+ transcript.status.reconciledAt = now;
221
+
222
+ notification = {
223
+ type: 'run-complete',
224
+ status: 'completed',
225
+ name: run.metadata?.name,
226
+ org: organizationRef,
227
+ timestamp: now,
228
+ };
229
+
230
+ if (lifecycleEmitter) {
231
+ lifecycleEmitter.emitRunCompleted(run, { phase: 'Completed' });
232
+ lifecycleEmitter.emitSessionEnded({ sessionId: attempt.status?.agentMuxSessionId, runId: attempt.status?.agentMuxRunId });
233
+ }
234
+ } else if (event.type === 'error') {
235
+ run.status.phase = 'Failed';
236
+ run.status.failedAt = now;
237
+ run.status.failureReason = event.error || event.message || 'Unknown error';
238
+ attempt.status.failedAt = now;
239
+ attempt.status.failureReason = event.error || event.message || 'Unknown error';
240
+ transcript.status.phase = 'Failed';
241
+
242
+ notification = {
243
+ type: 'run-complete',
244
+ status: 'failed',
245
+ name: run.metadata?.name,
246
+ org: organizationRef,
247
+ timestamp: now,
248
+ };
249
+
250
+ if (lifecycleEmitter) {
251
+ lifecycleEmitter.emitRunCompleted(run, { phase: 'Failed' });
252
+ lifecycleEmitter.emitSessionEnded({ sessionId: attempt.status?.agentMuxSessionId, runId: attempt.status?.agentMuxRunId });
253
+ }
254
+ }
255
+
256
+ // 5. Emit to event bus for SSE broadcast
257
+ if (eventBus) {
258
+ eventBus.emit({
259
+ type: 'session-event',
260
+ runName: run.metadata?.name,
261
+ eventType: event.type || 'message',
262
+ timestamp: now,
263
+ });
264
+ if (notification) {
265
+ eventBus.emit(notification);
266
+ }
267
+ }
268
+
269
+ return { run, attempt, transcript, notification };
270
+ },
271
+
272
+ async createManualDispatch({ repository, ref, sourceRefs = [], agentDefinition, agentStack, meetingRef, taskKind, actor, namespace = 'default', organizationRef = 'default', resources = {}, callbackUrl } = {}, options = {}) {
273
+ let identity = null;
274
+ if (agentDefinition) {
275
+ const resolved = personaController.resolveAgentDefinition(agentDefinition, { resources, organizationRef });
276
+ if (resolved.error) {
277
+ return { error: true, reason: 'agent-definition-invalid', message: resolved.errors.join('; '), errors: resolved.errors };
278
+ }
279
+ identity = {
280
+ definition: resolved.definition,
281
+ persona: resolved.persona,
282
+ soul: resolved.soul,
283
+ appearance: resolved.appearance,
284
+ voiceProfile: resolved.voiceProfile,
285
+ };
286
+ agentStack = resolved.stack.metadata?.name;
287
+ }
288
+
289
+ if (!agentStack) {
290
+ return { error: true, reason: 'dispatch-target-required', message: 'Dispatch requires agentDefinition or agentStack' };
291
+ }
292
+
293
+ // 0. Virtual model PreModelResolution hook — can redirect to a different model/stack
294
+ if (hookBridge) {
295
+ const virtualModels = resources.KradleVirtualModel || [];
296
+ const matchedVm = hookBridge.matchVirtualModel(agentStack, virtualModels);
297
+ if (matchedVm) {
298
+ const preResult = hookBridge.handleHook('VirtualModel.PreModelResolution', {
299
+ modelId: agentStack, repository, actor, taskKind
300
+ }, matchedVm);
301
+ if (preResult.decision === 'deny') {
302
+ return { error: true, reason: 'virtual-model-denied', message: preResult.message || 'Blocked by virtual model policy' };
303
+ }
304
+ if (preResult.decision === 'modify' && preResult.modifiedInput?.modelId) {
305
+ agentStack = preResult.modifiedInput.modelId;
306
+ }
307
+ }
308
+ }
309
+
310
+ // 1. Find stack
311
+ const stack = (resources.AgentStack || []).find(s => s.metadata?.name === agentStack);
312
+ if (!stack) return { error: true, reason: 'stack-not-found', message: `AgentStack '${agentStack}' not found` };
313
+ if (meetingRef && !jitsiAgentBridge.hasMeetingCapability(stack)) {
314
+ return { error: true, reason: 'meeting-not-supported', message: `AgentStack '${agentStack}' is not Jitsi-capable` };
315
+ }
316
+ const legacyWarning = legacyAgentStackIdentityWarning(stack);
317
+ if (legacyWarning && typeof logger?.warn === 'function') logger.warn(legacyWarning);
318
+
319
+ // 2. Permission review
320
+ const review = permissionReviewer.reviewPermissions({ repository, ref, actor, agentStack, resources });
321
+ if (review.decision === 'denied') {
322
+ return { error: true, reason: 'permission-denied', message: 'Dispatch denied by permission review', review };
323
+ }
324
+ const permissionSnapshot = permissionReviewer.createPermissionSnapshot(review);
325
+
326
+ // 3. Memory snapshot — use stack-scoped memoryRepositoryRefs if present, else fall back to all repos
327
+ let memorySnapshot = null;
328
+ const allMemoryRepos = resources.AgentMemoryRepository || [];
329
+ const stackMemoryRefs = stack.spec?.memoryRepositoryRefs || [];
330
+ const scopedMemoryRepos = stackMemoryRefs.length > 0
331
+ ? allMemoryRepos.filter((r) => stackMemoryRefs.includes(r.metadata?.name))
332
+ : allMemoryRepos;
333
+ if (scopedMemoryRepos.length > 0) {
334
+ const memRepo = scopedMemoryRepos[0];
335
+ const timeTravel = memoryController.resolveTimeTravel({ mode: 'current', commits: [] });
336
+ memorySnapshot = memoryController.createMemorySnapshot({
337
+ memoryRepository: memRepo.metadata.name,
338
+ requestedRef: ref,
339
+ resolvedCommit: timeTravel.resolvedCommit || ref,
340
+ queryManifest: {},
341
+ selectedRecords: [],
342
+ selectedDocuments: [],
343
+ ontologyDigest: '',
344
+ namespace,
345
+ organizationRef,
346
+ });
347
+ }
348
+
349
+ // 4. Approval gate — if review requires approval, create approval and return early
350
+ if (review.decision === 'requires-approval') {
351
+ const now = new Date().toISOString();
352
+ const runName = `dispatch-${Date.now()}`;
353
+
354
+ const run = createResource('AgentDispatchRun', { name: runName, namespace }, {
355
+ organizationRef,
356
+ repository,
357
+ sourceRefs: clone(sourceRefs),
358
+ ...(agentDefinition ? { agentDefinition } : {}),
359
+ agentStack,
360
+ taskKind: taskKind || 'diagnostic',
361
+ contextBundleRef: null,
362
+ });
363
+ run.status = { phase: 'AwaitingApproval', queuedAt: now };
364
+ if (memorySnapshot) {
365
+ run.spec.memorySnapshotRef = memorySnapshot.metadata.name;
366
+ }
367
+
368
+ const approvalResult = approvalController.createApprovalRequest({
369
+ dispatchRun: runName,
370
+ action: 'secret-access',
371
+ requestedBy: actor,
372
+ context: `Dispatch requires approval for agent stack: ${agentStack}`,
373
+ namespace,
374
+ organizationRef,
375
+ resources,
376
+ });
377
+
378
+ return {
379
+ error: false,
380
+ run,
381
+ approval: approvalResult.error ? null : approvalResult.approval,
382
+ awaitingApproval: true,
383
+ memorySnapshot,
384
+ permissionSnapshot,
385
+ review,
386
+ warnings: legacyWarning ? [legacyWarning] : [],
387
+ };
388
+ }
389
+
390
+ // 5. Workspace provisioning — reuse or create
391
+ let workspaceResult = null;
392
+ let mountSpec = null;
393
+ const branch = ref || 'main';
394
+
395
+ const reusable = workspaceController.findReusableWorkspace({
396
+ organizationRef, repository, branch, resources,
397
+ });
398
+
399
+ if (reusable) {
400
+ const claimResult = workspaceController.claimWorkspace({
401
+ name: reusable.metadata.name,
402
+ runRef: `dispatch-pending`,
403
+ resources,
404
+ });
405
+ if (!claimResult.error) {
406
+ workspaceResult = { workspace: claimResult.workspace, reused: true };
407
+ const mount = workspaceController.getMountSpec({ workspace: claimResult.workspace });
408
+ if (!mount.error) mountSpec = { volume: mount.volume, volumeMount: mount.volumeMount };
409
+ }
410
+ }
411
+
412
+ if (!workspaceResult) {
413
+ const createResult = workspaceController.createWorkspace({
414
+ organizationRef, repository, branch, namespace,
415
+ volumeSpec: {},
416
+ });
417
+ if (!createResult.error) {
418
+ workspaceResult = { workspace: createResult.workspace, pvcManifest: createResult.pvcManifest, reused: false };
419
+ const mount = workspaceController.getMountSpec({ workspace: createResult.workspace });
420
+ if (!mount.error) mountSpec = { volume: mount.volume, volumeMount: mount.volumeMount };
421
+ }
422
+ }
423
+
424
+ // 6. Assemble context bundle
425
+ const contextBundle = assembleContextBundle({ stack, repository, ref, sourceRefs, contextLabels: [], resources });
426
+
427
+ // 7. Create resources
428
+ const now = new Date().toISOString();
429
+ const runName = `dispatch-${Date.now()}`;
430
+
431
+ const run = createResource('AgentDispatchRun', { name: runName, namespace }, {
432
+ organizationRef,
433
+ repository,
434
+ sourceRefs: clone(sourceRefs),
435
+ ...(agentDefinition ? { agentDefinition } : {}),
436
+ agentStack,
437
+ taskKind: taskKind || 'diagnostic',
438
+ contextBundleRef: contextBundle.metadata.name,
439
+ });
440
+ run.status = { phase: 'Pending', queuedAt: now };
441
+ if (memorySnapshot) {
442
+ run.spec.memorySnapshotRef = memorySnapshot.metadata.name;
443
+ }
444
+ if (workspaceResult) {
445
+ run.spec.workspaceRef = workspaceResult.workspace.metadata.name;
446
+ }
447
+ if (mountSpec) {
448
+ run.spec.mountSpec = mountSpec;
449
+ }
450
+ let runtimeMeetingContext = null;
451
+ if (meetingRef) {
452
+ try {
453
+ runtimeMeetingContext = await jitsiAgentBridge.prepareMeetingContext(run, meetingRef, stack, { resources, organizationRef, namespace });
454
+ } catch (err) {
455
+ return { error: true, reason: 'meeting-unavailable', message: err.message || `Meeting ${meetingRef} is not available` };
456
+ }
457
+ }
458
+
459
+ if (lifecycleEmitter) {
460
+ lifecycleEmitter.emitRunCreated(run);
461
+ if (workspaceResult) {
462
+ lifecycleEmitter.emitWorkspaceProvisioned(workspaceResult.workspace);
463
+ }
464
+ }
465
+
466
+ // Update workspace runRef to actual dispatch name
467
+ if (workspaceResult) {
468
+ workspaceResult.workspace.status.runRef = runName;
469
+ }
470
+
471
+ const attempt = createResource('AgentDispatchAttempt', { name: `${runName}-attempt-1`, namespace }, {
472
+ organizationRef,
473
+ agentDispatchRun: runName,
474
+ attemptReason: 'initial',
475
+ agentStackSnapshot: clone(stack.spec),
476
+ ...(identity?.definition ? { agentDefinitionSnapshot: clone(identity.definition.spec) } : {}),
477
+ ...(identity?.persona ? { agentPersonaSnapshot: clone(identity.persona.spec) } : {}),
478
+ contextBundleDigest: contextBundle.spec.digest,
479
+ });
480
+ attempt.status = { permissionSnapshot, queueEnteredAt: now };
481
+
482
+ // 7. Dispatch as K8s Job
483
+ let transcript = null;
484
+ let jobResult = null;
485
+
486
+ const executionConfig = this.resolveStack(stack, { organizationRef, identity });
487
+ const pvcName = workspaceResult?.workspace?.spec?.volumeClaimName || workspaceResult?.pvcManifest?.metadata?.name || null;
488
+ const resolvedCallbackUrl = callbackUrl || process.env.KRADLE_CALLBACK_URL || null;
489
+
490
+ try {
491
+ // Inject memory config env vars into the Job when memory repos are scoped
492
+ const jobEnv = { ...executionConfig.env };
493
+ if (executionConfig.memoryConfig) {
494
+ if (executionConfig.memoryConfig.memoryRepositoryRefs) {
495
+ jobEnv.KRADLE_MEMORY_REPOS = executionConfig.memoryConfig.memoryRepositoryRefs.join(',');
496
+ }
497
+ if (executionConfig.memoryConfig.memorySnapshotRef) {
498
+ jobEnv.KRADLE_MEMORY_SNAPSHOT = executionConfig.memoryConfig.memorySnapshotRef;
499
+ }
500
+ }
501
+
502
+ const { jobManifest, jobName } = agentMuxClient.createAgentJob({
503
+ adapter: executionConfig.adapter,
504
+ provider: executionConfig.provider,
505
+ model: executionConfig.model,
506
+ org: organizationRef,
507
+ runId: runName,
508
+ stackName: stack.metadata?.name,
509
+ budget: stack.spec?.budget,
510
+ image: stack.spec?.image,
511
+ serviceAccount: stack.spec?.runtimeIdentity?.serviceAccountRef,
512
+ callbackUrl: resolvedCallbackUrl,
513
+ prompt: executionConfig.prompt,
514
+ env: jobEnv,
515
+ workspace: pvcName ? { pvcName } : undefined,
516
+ resources: stack.spec?.resources,
517
+ meetingContext: runtimeMeetingContext || run.spec.meetingContext,
518
+ });
519
+
520
+ attempt.status.jobName = jobName;
521
+ attempt.status.jobNamespace = jobManifest.metadata.namespace;
522
+ run.spec.jobRef = jobName;
523
+
524
+ try {
525
+ const submitResult = await agentMuxClient.submitAgentJob(jobManifest);
526
+ run.status.phase = 'Running';
527
+ attempt.status.startedAt = now;
528
+ attempt.status.jobSubmitted = true;
529
+ jobResult = { jobName: submitResult.jobName, namespace: submitResult.namespace, submitted: true };
530
+
531
+ if (lifecycleEmitter) {
532
+ lifecycleEmitter.emitSessionStarted({ sessionId: jobName, runId: runName });
533
+ lifecycleEmitter.emitStepStarted(run, 'launch');
534
+ }
535
+ } catch {
536
+ // Job manifest was generated but submission failed — queue for retry
537
+ run.status.phase = 'Queued';
538
+ run.status.conditions = [{ type: 'JobSubmitted', status: 'False', reason: 'SubmitFailed', message: 'K8s Job submission failed' }];
539
+ jobResult = { jobName, namespace: jobManifest.metadata.namespace, submitted: false };
540
+ }
541
+ } catch (err) {
542
+ run.status.phase = 'Queued';
543
+ run.status.conditions = [{ type: 'JobSubmitted', status: 'False', reason: 'ManifestFailed', message: err.message || 'Job manifest generation failed' }];
544
+ }
545
+
546
+ return { error: false, run, attempt, contextBundle, permissionSnapshot, memorySnapshot, transcript, workspace: workspaceResult, mountSpec, jobResult, executionConfig, identity, warnings: legacyWarning ? [legacyWarning] : [] };
547
+ }
548
+ };
549
+ }
@@ -0,0 +1,147 @@
1
+ // Agent Gateway Config Controller — Slice 1.2e
2
+ // Manages AgentGatewayConfig resources: gateway name, endpoint URL, feature flags,
3
+ // connection pool settings, and TLS configuration reference.
4
+
5
+ export const AGENT_GATEWAY_CONFIG_CONTROLLER_BOUNDARY = {
6
+ role: 'agent-gateway-config-controller',
7
+ scope: 'AgentGatewayConfig lifecycle: validation, endpoint resolution, feature flags, connection pool, TLS config ref',
8
+ owns: ['gateway config validation', 'endpoint URL', 'feature flags', 'connection pool defaults', 'TLS config ref'],
9
+ delegatesTo: ['resource-model'],
10
+ mustNotOwn: ['secret values', 'dispatch execution', 'Agent Mux sessions', 'adapter implementation']
11
+ };
12
+
13
+ const DEFAULT_FEATURE_FLAGS = Object.freeze({
14
+ streaming: true,
15
+ reconnect: true,
16
+ healthCheck: true
17
+ });
18
+
19
+ const DEFAULT_POOL_SETTINGS = Object.freeze({
20
+ maxConnections: 10,
21
+ timeoutMs: 30000
22
+ });
23
+
24
+ /**
25
+ * Validate an AgentGatewayConfig resource. Returns { valid, errors }.
26
+ * @param {object} resource
27
+ * @returns {{ valid: boolean, errors: string[] }}
28
+ */
29
+ export function validateAgentGatewayConfig(resource) {
30
+ const errors = [];
31
+
32
+ // Guard against null/undefined resource
33
+ if (resource == null) {
34
+ errors.push('resource must not be null or undefined');
35
+ return { valid: false, errors };
36
+ }
37
+
38
+ // Validate metadata.name
39
+ if (!resource?.metadata?.name) {
40
+ errors.push('metadata.name is required');
41
+ }
42
+
43
+ const spec = resource?.spec || {};
44
+
45
+ // Validate organizationRef
46
+ if (!spec.organizationRef) {
47
+ errors.push('spec.organizationRef is required');
48
+ }
49
+
50
+ // Validate endpoint URL
51
+ if (!spec.endpointUrl) {
52
+ errors.push('spec.endpointUrl is required; provide the Agent Mux gateway endpoint URL');
53
+ } else {
54
+ // Basic URL validation: must start with http:// or https:// or ws:// or wss://
55
+ const validProtocols = ['http://', 'https://', 'ws://', 'wss://'];
56
+ const hasValidProtocol = validProtocols.some((p) => spec.endpointUrl.startsWith(p));
57
+ if (!hasValidProtocol) {
58
+ errors.push(`spec.endpointUrl must start with one of: ${validProtocols.join(', ')}`);
59
+ }
60
+ }
61
+
62
+ // Validate connectionPool if provided
63
+ const pool = spec.connectionPool;
64
+ if (pool != null) {
65
+ if (pool.maxConnections != null && (!Number.isInteger(pool.maxConnections) || pool.maxConnections < 1)) {
66
+ errors.push('spec.connectionPool.maxConnections must be a positive integer');
67
+ }
68
+ if (pool.timeoutMs != null && (!Number.isFinite(pool.timeoutMs) || pool.timeoutMs < 0)) {
69
+ errors.push('spec.connectionPool.timeoutMs must be a non-negative number');
70
+ }
71
+ }
72
+
73
+ return { valid: errors.length === 0, errors };
74
+ }
75
+
76
+ /**
77
+ * Factory that returns an AgentGatewayConfig controller instance.
78
+ */
79
+ export function createAgentGatewayConfigController() {
80
+ return {
81
+ role: 'agent-gateway-config-controller',
82
+
83
+ /**
84
+ * Validate an AgentGatewayConfig resource.
85
+ * @param {object} resource
86
+ * @returns {{ valid: boolean, errors: string[] }}
87
+ */
88
+ validate(resource) {
89
+ return validateAgentGatewayConfig(resource);
90
+ },
91
+
92
+ /**
93
+ * Return the effective endpoint URL for the gateway config.
94
+ * @param {object} resource
95
+ * @returns {string}
96
+ */
97
+ getEndpointUrl(resource) {
98
+ if (resource == null) {
99
+ throw new Error('resource must not be null or undefined');
100
+ }
101
+ return resource?.spec?.endpointUrl ?? null;
102
+ },
103
+
104
+ /**
105
+ * Return the effective feature flags for a gateway config.
106
+ * Merges spec.featureFlags with defaults; spec values take precedence.
107
+ * @param {object} resource
108
+ * @returns {{ streaming: boolean, reconnect: boolean, healthCheck: boolean, [key: string]: boolean }}
109
+ */
110
+ getFeatureFlags(resource) {
111
+ if (resource == null) {
112
+ throw new Error('resource must not be null or undefined');
113
+ }
114
+ const specFlags = resource?.spec?.featureFlags ?? {};
115
+ return { ...DEFAULT_FEATURE_FLAGS, ...specFlags };
116
+ },
117
+
118
+ /**
119
+ * Return the effective connection pool settings for a gateway config.
120
+ * Merges spec.connectionPool with defaults; spec values take precedence.
121
+ * @param {object} resource
122
+ * @returns {{ maxConnections: number, timeoutMs: number }}
123
+ */
124
+ getConnectionPool(resource) {
125
+ if (resource == null) {
126
+ throw new Error('resource must not be null or undefined');
127
+ }
128
+ const specPool = resource?.spec?.connectionPool ?? {};
129
+ return {
130
+ maxConnections: specPool.maxConnections ?? DEFAULT_POOL_SETTINGS.maxConnections,
131
+ timeoutMs: specPool.timeoutMs ?? DEFAULT_POOL_SETTINGS.timeoutMs
132
+ };
133
+ },
134
+
135
+ /**
136
+ * Return the TLS configuration reference from the spec, or null if not set.
137
+ * @param {object} resource
138
+ * @returns {string|null}
139
+ */
140
+ getTlsConfigRef(resource) {
141
+ if (resource == null) {
142
+ throw new Error('resource must not be null or undefined');
143
+ }
144
+ return resource?.spec?.tlsConfigRef ?? null;
145
+ }
146
+ };
147
+ }