@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,589 @@
1
+ import http from 'node:http';
2
+ import https from 'node:https';
3
+ import { URL } from 'node:url';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { createResource } from './resource-model.js';
6
+
7
+ export const AGENT_MUX_CLIENT_BOUNDARY = {
8
+ role: 'agent-mux-client',
9
+ scope: 'HTTP/SSE adapter for Agent Mux gateway — capabilities, sessions, events, transcripts, K8s Job dispatch',
10
+ owns: ['gateway HTTP calls', 'SSE event streaming', 'transcript reconciliation', 'K8s Job manifest generation', 'Job lifecycle management'],
11
+ delegatesTo: ['resource-model', 'kubernetes-resource-gateway'],
12
+ mustNotOwn: ['secret values', 'permission review', 'resource persistence']
13
+ };
14
+
15
+ /** Known agent adapter names for job dispatch. */
16
+ const KNOWN_ADAPTERS = new Set([
17
+ 'claude-code', 'codex', 'gemini-cli', 'aider', 'goose',
18
+ 'amp', 'roo-code', 'kilo-code', 'cline', 'cursor',
19
+ ]);
20
+
21
+ const JITSI_SOCKET_PATH = '/tmp/jitsi-agent.sock';
22
+
23
+ function jitsiResourceProfile(audioMode = 'listen') {
24
+ if (audioMode === 'both') {
25
+ return {
26
+ requests: { cpu: '500m', memory: '512Mi' },
27
+ limits: { cpu: '2000m', memory: '2Gi' },
28
+ };
29
+ }
30
+ if (audioMode === 'speak') {
31
+ return {
32
+ requests: { cpu: '500m', memory: '512Mi' },
33
+ limits: { cpu: '2000m', memory: '2Gi' },
34
+ };
35
+ }
36
+ if (audioMode === 'listen') {
37
+ return {
38
+ requests: { cpu: '200m', memory: '256Mi' },
39
+ limits: { cpu: '1000m', memory: '1Gi' },
40
+ };
41
+ }
42
+ return {
43
+ requests: { cpu: '50m', memory: '128Mi' },
44
+ limits: { cpu: '200m', memory: '256Mi' },
45
+ };
46
+ }
47
+
48
+ function createJitsiSidecarContainer(jitsi = {}) {
49
+ const capabilities = jitsi.capabilities || {};
50
+ const tts = jitsi.tts || {};
51
+ const stt = jitsi.stt || {};
52
+ const vad = jitsi.vad || {};
53
+ const audioMode = capabilities.audio || jitsi.audioMode || 'listen';
54
+ const chatMode = capabilities.chat || jitsi.chatMode || 'read';
55
+ const screenshareMode = capabilities.screenshare || jitsi.screenshareMode || 'none';
56
+ const env = [
57
+ { name: 'JITSI_ROOM_URL', value: jitsi.roomUrl || '' },
58
+ { name: 'JITSI_JWT', value: jitsi.jwt || '' },
59
+ { name: 'JITSI_ROOM_ID', value: jitsi.roomId || '' },
60
+ { name: 'JITSI_PARTICIPANT_NAME', value: jitsi.participantName || jitsi.stackName || 'Kradle Agent' },
61
+ { name: 'JITSI_PARTICIPANT_ROLE', value: jitsi.role || 'observer' },
62
+ { name: 'JITSI_GOODBYE_MESSAGE', value: jitsi.goodbyeMessage || 'Kradle agent is leaving the meeting.' },
63
+ { name: 'JITSI_AUDIO_MODE', value: audioMode },
64
+ { name: 'JITSI_CHAT_MODE', value: chatMode },
65
+ { name: 'JITSI_SCREENSHARE_MODE', value: screenshareMode },
66
+ { name: 'AGENT_SOCKET_PATH', value: JITSI_SOCKET_PATH },
67
+ ];
68
+ if (tts.provider) env.push({ name: 'JITSI_TTS_PROVIDER', value: tts.provider });
69
+ if (tts.voice) env.push({ name: 'JITSI_TTS_VOICE', value: tts.voice });
70
+ if (tts.speed) env.push({ name: 'JITSI_TTS_SPEED', value: String(tts.speed) });
71
+ if (stt.provider) env.push({ name: 'JITSI_STT_PROVIDER', value: stt.provider });
72
+ if (vad.provider) env.push({ name: 'JITSI_VAD_PROVIDER', value: vad.provider });
73
+ if (jitsi.goodbyeMessage) env.push({ name: 'JITSI_GOODBYE_MESSAGE', value: jitsi.goodbyeMessage });
74
+
75
+ return {
76
+ name: 'jitsi-agent-sidecar',
77
+ image: jitsi.sidecarImage || 'kradle/jitsi-agent-sidecar:latest',
78
+ env,
79
+ resources: jitsi.resources || jitsiResourceProfile(audioMode),
80
+ volumeMounts: [{ name: 'agent-socket', mountPath: '/tmp' }],
81
+ lifecycle: {
82
+ preStop: {
83
+ exec: {
84
+ command: ['node', 'bin/graceful-leave.mjs'],
85
+ },
86
+ },
87
+ },
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Internal HTTP request helper. Zero external deps — uses node:http / node:https.
93
+ * @param {string} url
94
+ * @param {{ method?: string, body?: object, headers?: Record<string,string>, timeout?: number }} options
95
+ * @returns {Promise<{ status: number, body: any }>}
96
+ */
97
+ function httpRequest(url, { method = 'GET', body, headers = {}, timeout = 30000 } = {}) {
98
+ return new Promise((resolve, reject) => {
99
+ const parsed = new URL(url);
100
+ const transport = parsed.protocol === 'https:' ? https : http;
101
+ const opts = {
102
+ hostname: parsed.hostname,
103
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
104
+ path: parsed.pathname + parsed.search,
105
+ method,
106
+ headers: { 'Accept': 'application/json', ...headers },
107
+ timeout,
108
+ };
109
+ if (body) {
110
+ const payload = JSON.stringify(body);
111
+ opts.headers['Content-Type'] = 'application/json';
112
+ opts.headers['Content-Length'] = Buffer.byteLength(payload);
113
+ }
114
+ const req = transport.request(opts, (res) => {
115
+ const chunks = [];
116
+ res.on('data', chunk => chunks.push(chunk));
117
+ res.on('end', () => {
118
+ const raw = Buffer.concat(chunks).toString();
119
+ try {
120
+ resolve({ status: res.statusCode, body: JSON.parse(raw) });
121
+ } catch {
122
+ resolve({ status: res.statusCode, body: raw });
123
+ }
124
+ });
125
+ });
126
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
127
+ req.on('error', reject);
128
+ if (body) req.write(JSON.stringify(body));
129
+ req.end();
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Parse SSE text into an array of parsed JSON data payloads.
135
+ * Each `data: {...}` line is extracted; malformed JSON is silently skipped.
136
+ * @param {string} text
137
+ * @returns {object[]}
138
+ */
139
+ export function parseSseLines(text) {
140
+ const events = [];
141
+ for (const block of text.split('\n\n')) {
142
+ for (const line of block.split('\n')) {
143
+ if (line.startsWith('data: ')) {
144
+ try { events.push(JSON.parse(line.slice(6))); } catch { /* skip malformed */ }
145
+ }
146
+ }
147
+ }
148
+ return events;
149
+ }
150
+
151
+ /**
152
+ * Map provider name to codec identifier.
153
+ * @param {string} provider
154
+ * @returns {'anthropic'|'openai'|'google'}
155
+ */
156
+ function deriveCodec(provider) {
157
+ const map = { anthropic: 'anthropic', openai: 'openai', google: 'google', gemini: 'google' };
158
+ return map[provider] || 'anthropic';
159
+ }
160
+
161
+ /**
162
+ * @param {{ gateway?: string, enabled?: boolean, resourceGateway?: object }} options
163
+ */
164
+ export function createAgentMuxClient(options = {}) {
165
+ const { gateway = '', enabled = false, resourceGateway = null } = options;
166
+
167
+ return {
168
+ role: 'agent-mux-client',
169
+
170
+ isAvailable() {
171
+ return enabled && !!gateway;
172
+ },
173
+
174
+ /**
175
+ * Resolve the transport config for a stack from its AgentTransportBinding.
176
+ * Defaults to 'stdio' for local subprocess adapters when no binding is found.
177
+ *
178
+ * @param {object} stack - AgentStack resource or plain spec object
179
+ * @param {object[]} transportBindings - Array of AgentTransportBinding resources
180
+ * @returns {{ protocol: string, endpoint: string, codec: string }}
181
+ */
182
+ resolveTransport(stack, transportBindings = []) {
183
+ const adapterName = stack?.spec?.adapter || stack?.spec?.baseAgent || 'claude-code';
184
+ const provider = stack?.spec?.provider || 'anthropic';
185
+ const binding = (transportBindings || []).find(b => b.spec?.adapterRef === adapterName);
186
+
187
+ if (binding) {
188
+ const protocol = binding.spec.protocol || 'http';
189
+ const endpoint = binding.spec.endpoint || '';
190
+ const codec = deriveCodec(provider);
191
+ return { protocol, endpoint, codec };
192
+ }
193
+
194
+ return { protocol: 'stdio', endpoint: '', codec: deriveCodec(provider) };
195
+ },
196
+
197
+ /**
198
+ * Query adapter capabilities from the gateway.
199
+ * GET {gateway}/api/v1/agents/{adapter}/capabilities
200
+ * @param {string} adapter
201
+ * @returns {Promise<object|null>}
202
+ */
203
+ async queryCapabilities(adapter) {
204
+ if (!this.isAvailable()) return null;
205
+ try {
206
+ const { status, body } = await httpRequest(`${gateway}/api/v1/agents/${encodeURIComponent(adapter)}/capabilities`);
207
+ if (status >= 200 && status < 300) return body;
208
+ return null;
209
+ } catch {
210
+ return null;
211
+ }
212
+ },
213
+
214
+ /**
215
+ * Launch a new agent session through the gateway.
216
+ * POST {gateway}/api/v1/sessions
217
+ * @param {{ stack: object, contextBundle?: object, permissionSnapshot?: object, workspace?: object }} params
218
+ * @returns {Promise<{ runId: string, sessionId: string }|null>}
219
+ */
220
+ async launchSession({ stack, contextBundle, permissionSnapshot, workspace }) {
221
+ if (!this.isAvailable()) return null;
222
+ try {
223
+ const payload = {
224
+ agent: stack?.baseAgent,
225
+ model: stack?.model,
226
+ prompt: contextBundle?.prompt,
227
+ systemPrompt: contextBundle?.systemPrompt,
228
+ attachments: contextBundle?.attachments,
229
+ workspace: workspace?.mountPath || '/workspace',
230
+ };
231
+ const { status, body } = await httpRequest(`${gateway}/api/v1/sessions`, { method: 'POST', body: payload });
232
+ if (status >= 200 && status < 300 && body?.runId && body?.sessionId) {
233
+ return { runId: body.runId, sessionId: body.sessionId };
234
+ }
235
+ return null;
236
+ } catch {
237
+ return null;
238
+ }
239
+ },
240
+
241
+ /**
242
+ * Get session status from the gateway.
243
+ * GET {gateway}/api/v1/sessions/{sessionId}
244
+ * @param {string} sessionId
245
+ * @returns {Promise<object|null>}
246
+ */
247
+ async getSessionStatus(sessionId) {
248
+ if (!this.isAvailable()) return null;
249
+ try {
250
+ const { status, body } = await httpRequest(`${gateway}/api/v1/sessions/${encodeURIComponent(sessionId)}`);
251
+ if (status >= 200 && status < 300) return body;
252
+ return null;
253
+ } catch {
254
+ return null;
255
+ }
256
+ },
257
+
258
+ /**
259
+ * Subscribe to SSE events for a run. Reconnects with exponential backoff (1s, 2s, 4s... max 30s).
260
+ * GET {gateway}/api/v1/runs/{runId}/events (Accept: text/event-stream)
261
+ * @param {string} runId
262
+ * @param {(event: object) => void} callback
263
+ * @returns {{ abort: () => void }}
264
+ */
265
+ subscribeToEvents(runId, callback) {
266
+ let aborted = false;
267
+ let currentReq = null;
268
+ let backoff = 1000;
269
+
270
+ const connect = () => {
271
+ if (aborted) return;
272
+ try {
273
+ const parsed = new URL(`${gateway}/api/v1/runs/${encodeURIComponent(runId)}/events`);
274
+ const transport = parsed.protocol === 'https:' ? https : http;
275
+ const opts = {
276
+ hostname: parsed.hostname,
277
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
278
+ path: parsed.pathname + parsed.search,
279
+ method: 'GET',
280
+ headers: { 'Accept': 'text/event-stream' },
281
+ };
282
+ currentReq = transport.request(opts, (res) => {
283
+ if (aborted) return;
284
+ // Reset backoff on successful connection
285
+ backoff = 1000;
286
+ let buffer = '';
287
+ res.on('data', (chunk) => {
288
+ if (aborted) return;
289
+ buffer += chunk.toString();
290
+ // Process complete SSE blocks (separated by double newlines)
291
+ const parts = buffer.split('\n\n');
292
+ // Keep the last part as it may be incomplete
293
+ buffer = parts.pop() || '';
294
+ for (const block of parts) {
295
+ for (const line of block.split('\n')) {
296
+ if (line.startsWith('data: ')) {
297
+ try {
298
+ callback(JSON.parse(line.slice(6)));
299
+ } catch { /* skip malformed */ }
300
+ }
301
+ }
302
+ }
303
+ });
304
+ res.on('end', () => {
305
+ if (!aborted) reconnect();
306
+ });
307
+ res.on('error', () => {
308
+ if (!aborted) reconnect();
309
+ });
310
+ });
311
+ currentReq.on('error', () => {
312
+ if (!aborted) reconnect();
313
+ });
314
+ currentReq.end();
315
+ } catch {
316
+ if (!aborted) reconnect();
317
+ }
318
+ };
319
+
320
+ const reconnect = () => {
321
+ if (aborted) return;
322
+ const delay = backoff;
323
+ backoff = Math.min(backoff * 2, 30000);
324
+ setTimeout(connect, delay);
325
+ };
326
+
327
+ connect();
328
+
329
+ return {
330
+ abort() {
331
+ aborted = true;
332
+ if (currentReq) {
333
+ currentReq.destroy();
334
+ currentReq = null;
335
+ }
336
+ }
337
+ };
338
+ },
339
+
340
+ /**
341
+ * Generate a Kubernetes Job manifest to run an agent as an isolated Job
342
+ * instead of a subprocess of the API server.
343
+ *
344
+ * @param {{ adapter: string, provider?: string, model?: string, workspace?: { pvcName?: string }, prompt?: { system?: string, task?: string }, env?: Record<string,string>, org: string, runId?: string, stackName?: string, budget?: { maxDurationSeconds?: number }, resources?: object, image?: string, serviceAccount?: string, callbackUrl?: string, jitsi?: object, meetingContext?: object }} config
345
+ * @returns {{ jobManifest: object, jobName: string }}
346
+ */
347
+ createAgentJob(config = {}) {
348
+ const {
349
+ adapter,
350
+ provider = 'anthropic',
351
+ model,
352
+ org,
353
+ runId = randomUUID(),
354
+ stackName,
355
+ budget,
356
+ image,
357
+ serviceAccount,
358
+ callbackUrl,
359
+ prompt,
360
+ env = {},
361
+ workspace,
362
+ resources: resourceLimits,
363
+ transportBindings = [],
364
+ jitsi,
365
+ meetingContext = null,
366
+ } = config;
367
+
368
+ // Validate adapter
369
+ if (!adapter || typeof adapter !== 'string') {
370
+ throw new Error('createAgentJob requires a valid adapter name');
371
+ }
372
+ if (!KNOWN_ADAPTERS.has(adapter)) {
373
+ throw new Error(`Unknown adapter: ${adapter}. Known adapters: ${[...KNOWN_ADAPTERS].join(', ')}`);
374
+ }
375
+ if (!org) {
376
+ throw new Error('createAgentJob requires an org');
377
+ }
378
+
379
+ const jobName = `kradle-agent-${runId}`;
380
+ const pvcName = workspace?.pvcName;
381
+
382
+ const transportConfig = this.resolveTransport(
383
+ { spec: { adapter, provider } },
384
+ transportBindings
385
+ );
386
+ const jitsiContext = meetingContext || jitsi || null;
387
+
388
+ const containerEnv = [
389
+ { name: 'KRADLE_ORG', value: org },
390
+ { name: 'KRADLE_RUN_ID', value: runId },
391
+ { name: 'KRADLE_WORKSPACE_PATH', value: '/workspace' },
392
+ { name: 'AGENT_MUX_TRANSPORT', value: transportConfig.protocol },
393
+ { name: 'TRANSPORT_MUX_CODEC', value: transportConfig.codec },
394
+ ...(transportConfig.endpoint ? [{ name: 'AGENT_MUX_TRANSPORT_ENDPOINT', value: transportConfig.endpoint }] : []),
395
+ ...(callbackUrl ? [{ name: 'KRADLE_CALLBACK_URL', value: callbackUrl }] : []),
396
+ ...(prompt?.system ? [{ name: 'AGENT_SYSTEM_PROMPT', value: prompt.system }] : []),
397
+ ...(prompt?.task ? [{ name: 'AGENT_TASK', value: prompt.task }] : []),
398
+ ...(jitsiContext ? [
399
+ { name: 'JITSI_AGENT_SOCKET', value: JITSI_SOCKET_PATH },
400
+ { name: 'JITSI_MEETING_ACTIVE', value: 'true' },
401
+ ] : []),
402
+ ...Object.entries(env).map(([name, value]) => ({ name, value: String(value) })),
403
+ ];
404
+ const volumes = pvcName ? [{ name: 'workspace', persistentVolumeClaim: { claimName: pvcName } }] : [];
405
+ const agentVolumeMounts = pvcName ? [{ name: 'workspace', mountPath: '/workspace' }] : [];
406
+ if (jitsiContext) {
407
+ agentVolumeMounts.push({ name: 'agent-socket', mountPath: '/tmp' });
408
+ volumes.push({ name: 'agent-socket', emptyDir: {} });
409
+ }
410
+ const containers = [{
411
+ name: 'agent',
412
+ image: image || 'ghcr.io/a5c-ai/agent-mux:latest',
413
+ command: ['node', 'dist/cli/index.js', 'launch', adapter, provider],
414
+ args: model ? ['--model', model] : [],
415
+ env: containerEnv,
416
+ resources: resourceLimits || {
417
+ requests: { cpu: '500m', memory: '1Gi' },
418
+ limits: { cpu: '2', memory: '4Gi' },
419
+ },
420
+ volumeMounts: agentVolumeMounts,
421
+ }];
422
+
423
+ if (jitsiContext) {
424
+ containers.push(createJitsiSidecarContainer({ ...jitsiContext, stackName }));
425
+ }
426
+
427
+ const jobManifest = {
428
+ apiVersion: 'batch/v1',
429
+ kind: 'Job',
430
+ metadata: {
431
+ name: jobName,
432
+ namespace: `kradle-org-${org}`,
433
+ labels: {
434
+ 'kradle.a5c.ai/component': 'agent-run',
435
+ 'kradle.a5c.ai/run': runId,
436
+ ...(stackName ? { 'kradle.a5c.ai/stack': stackName } : {}),
437
+ 'kradle.a5c.ai/org': org,
438
+ },
439
+ },
440
+ spec: {
441
+ backoffLimit: 0,
442
+ activeDeadlineSeconds: budget?.maxDurationSeconds || 3600,
443
+ template: {
444
+ metadata: {
445
+ labels: {
446
+ 'kradle.a5c.ai/component': 'agent-run',
447
+ 'kradle.a5c.ai/run': runId,
448
+ },
449
+ },
450
+ spec: {
451
+ restartPolicy: 'Never',
452
+ serviceAccountName: serviceAccount || 'kradle',
453
+ containers,
454
+ volumes,
455
+ },
456
+ },
457
+ },
458
+ };
459
+
460
+ return { jobManifest, jobName };
461
+ },
462
+
463
+ /**
464
+ * Submit a Job manifest to Kubernetes via the resource gateway.
465
+ *
466
+ * @param {object} jobManifest - Full K8s Job manifest
467
+ * @returns {Promise<{ jobName: string, namespace: string, submitted: boolean }>}
468
+ */
469
+ async submitAgentJob(jobManifest) {
470
+ if (!resourceGateway) {
471
+ throw new Error('submitAgentJob requires a resourceGateway');
472
+ }
473
+ const jobName = jobManifest?.metadata?.name;
474
+ const namespace = jobManifest?.metadata?.namespace;
475
+ await resourceGateway.apply(jobManifest);
476
+ return { jobName, namespace, submitted: true };
477
+ },
478
+
479
+ /**
480
+ * Get the status of a submitted K8s Job.
481
+ *
482
+ * @param {string} jobName
483
+ * @param {string} namespace
484
+ * @returns {Promise<{ active: number, succeeded: number, failed: number, startTime: string|null, completionTime: string|null, conditions: object[] }>}
485
+ */
486
+ async getJobStatus(jobName, namespace) {
487
+ if (!resourceGateway) {
488
+ throw new Error('getJobStatus requires a resourceGateway');
489
+ }
490
+ const job = await resourceGateway.get('Job', jobName);
491
+ if (!job) {
492
+ return { active: 0, succeeded: 0, failed: 0, startTime: null, completionTime: null, conditions: [] };
493
+ }
494
+ const status = job.status || {};
495
+ return {
496
+ active: status.active || 0,
497
+ succeeded: status.succeeded || 0,
498
+ failed: status.failed || 0,
499
+ startTime: status.startTime || null,
500
+ completionTime: status.completionTime || null,
501
+ conditions: status.conditions || [],
502
+ };
503
+ },
504
+
505
+ /**
506
+ * Retrieve logs from a K8s Job's pod.
507
+ *
508
+ * @param {string} jobName
509
+ * @param {string} namespace
510
+ * @returns {Promise<string>}
511
+ */
512
+ async getJobLogs(jobName, namespace) {
513
+ if (!resourceGateway) {
514
+ throw new Error('getJobLogs requires a resourceGateway');
515
+ }
516
+ // Use the resource gateway's log retrieval (reads pod logs for the job)
517
+ if (typeof resourceGateway.getLogs === 'function') {
518
+ return resourceGateway.getLogs('Job', jobName, namespace);
519
+ }
520
+ // Fallback: return empty string if gateway doesn't support logs
521
+ return '';
522
+ },
523
+
524
+ /**
525
+ * Delete a completed K8s Job and its pods.
526
+ *
527
+ * @param {string} jobName
528
+ * @param {string} namespace
529
+ * @returns {Promise<{ deleted: boolean }>}
530
+ */
531
+ async deleteJob(jobName, namespace) {
532
+ if (!resourceGateway) {
533
+ throw new Error('deleteJob requires a resourceGateway');
534
+ }
535
+ await resourceGateway.delete('Job', jobName);
536
+ return { deleted: true };
537
+ },
538
+
539
+ /**
540
+ * Reconcile SSE events into an AgentSessionTranscript resource.
541
+ * Parses events by role, computes cost, creates the resource via createResource().
542
+ * @param {string} sessionId
543
+ * @param {object[]} events
544
+ * @param {{ namespace?: string, organizationRef?: string }} options
545
+ * @returns {object} AgentSessionTranscript resource
546
+ */
547
+ reconcileTranscript(sessionId, events, { namespace = 'default', organizationRef = 'default' } = {}) {
548
+ const messages = [];
549
+ let totalInputTokens = 0;
550
+ let totalOutputTokens = 0;
551
+
552
+ for (const event of events) {
553
+ if (!event || typeof event !== 'object') continue;
554
+ const role = event.role || 'unknown';
555
+ const content = event.content || event.text || event.message || '';
556
+ const node = {
557
+ role,
558
+ content: typeof content === 'string' ? content : JSON.stringify(content),
559
+ timestamp: event.timestamp || new Date().toISOString(),
560
+ };
561
+ if (event.toolUse) node.toolUse = event.toolUse;
562
+ if (event.toolResult) node.toolResult = event.toolResult;
563
+ messages.push(node);
564
+
565
+ // Accumulate token usage if present
566
+ if (event.usage) {
567
+ totalInputTokens += event.usage.inputTokens || 0;
568
+ totalOutputTokens += event.usage.outputTokens || 0;
569
+ }
570
+ }
571
+
572
+ return createResource(
573
+ 'AgentSessionTranscript',
574
+ { name: `transcript-${sessionId}`, namespace },
575
+ {
576
+ organizationRef,
577
+ sessionRef: sessionId,
578
+ messages,
579
+ cost: {
580
+ inputTokens: totalInputTokens,
581
+ outputTokens: totalOutputTokens,
582
+ totalTokens: totalInputTokens + totalOutputTokens,
583
+ },
584
+ },
585
+ { phase: 'Reconciled', reconciledAt: new Date().toISOString() }
586
+ );
587
+ },
588
+ };
589
+ }