@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,141 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ const DEFAULT_SIDECAR_IMAGE = 'ghcr.io/a5c-ai/jitsi-agent-sidecar:latest';
4
+ const SOCKET_PATH = '/tmp/jitsi-agent.sock';
5
+ const DEFAULT_TTL_MINUTES = 60;
6
+
7
+ function isoNow(now) {
8
+ return (typeof now === 'function' ? now() : new Date()).toISOString();
9
+ }
10
+
11
+ export function createJitsiAgentBridge(options = {}) {
12
+ const {
13
+ meetingController,
14
+ dispatchController,
15
+ eventBus,
16
+ sidecarImage = process.env.KRADLE_JITSI_AGENT_SIDECAR_IMAGE || DEFAULT_SIDECAR_IMAGE,
17
+ now = () => new Date(),
18
+ } = options;
19
+
20
+ async function resolveMeeting(meetingRef, resources = {}) {
21
+ const meeting = await meetingController?.getMeeting?.(meetingRef);
22
+ if (meeting) return meeting;
23
+ return (resources.JitsiMeeting || []).find((candidate) => (
24
+ candidate.metadata?.name === meetingRef || candidate.spec?.roomId === meetingRef
25
+ )) || null;
26
+ }
27
+
28
+ function generateParticipantJwt(roomId, participant, ttlMinutes) {
29
+ if (meetingController?.generateParticipantJwt) {
30
+ return meetingController.generateParticipantJwt(roomId, participant, ttlMinutes);
31
+ }
32
+ const exp = Math.floor(now().getTime() / 1000) + Math.max(1, Number(ttlMinutes) || DEFAULT_TTL_MINUTES) * 60;
33
+ const claims = {
34
+ aud: 'jitsi',
35
+ iss: 'kradle',
36
+ sub: participant.org || 'kradle',
37
+ room: roomId,
38
+ exp,
39
+ context: {
40
+ user: {
41
+ id: participant.id,
42
+ name: participant.name,
43
+ type: 'agent',
44
+ role: participant.role || 'observer',
45
+ avatar: participant.avatar || '',
46
+ },
47
+ },
48
+ };
49
+ const encoded = Buffer.from(JSON.stringify(claims)).toString('base64url');
50
+ const secret = process.env.KRADLE_JITSI_JWT_SECRET || process.env.JITSI_JWT_SECRET || 'dev-jitsi-secret';
51
+ const signature = crypto.createHmac('sha256', secret).update(encoded).digest('base64url');
52
+ return `kradle-jitsi.${encoded}.${signature}`;
53
+ }
54
+
55
+ return {
56
+ role: 'jitsi-agent-bridge',
57
+
58
+ hasMeetingCapability(stack) {
59
+ return stack?.spec?.jitsiCapability === true;
60
+ },
61
+
62
+ async prepareMeetingContext(dispatchRun, meetingRef, stack = {}, { resources = {} } = {}) {
63
+ if (!meetingRef) return null;
64
+ if (!this.hasMeetingCapability(stack)) return null;
65
+ const meeting = await resolveMeeting(meetingRef, resources);
66
+ if (!meeting || meeting.status?.phase !== 'Active') {
67
+ throw new Error(`Meeting ${meetingRef} is not active`);
68
+ }
69
+ const participantName = stack.spec?.jitsiConfig?.participantName || stack.metadata?.name || dispatchRun.metadata?.name;
70
+ const participant = {
71
+ id: dispatchRun.metadata?.name,
72
+ name: participantName,
73
+ type: 'agent',
74
+ role: stack.spec?.jitsiConfig?.role || 'observer',
75
+ };
76
+ const jwt = generateParticipantJwt(
77
+ meeting.spec.roomId,
78
+ participant,
79
+ meeting.spec.ttlMinutes || 120,
80
+ );
81
+ const context = {
82
+ roomUrl: meeting.status.roomUrl,
83
+ roomId: meeting.spec.roomId,
84
+ jwt,
85
+ participantName,
86
+ role: participant.role,
87
+ capabilities: stack.spec?.jitsiConfig?.capabilities || {},
88
+ };
89
+ dispatchRun.spec.meetingRef = meetingRef;
90
+ dispatchRun.spec.meetingContext = {
91
+ roomUrl: context.roomUrl,
92
+ roomId: context.roomId,
93
+ participantName: context.participantName,
94
+ role: context.role,
95
+ capabilities: context.capabilities,
96
+ tokenRef: { runtimeOnly: true },
97
+ };
98
+ return context;
99
+ },
100
+
101
+ buildSidecarSpec(meetingUrl, jwt, agentName, context = {}) {
102
+ const capabilities = context.capabilities || {};
103
+ return {
104
+ name: 'jitsi-agent-sidecar',
105
+ image: context.sidecarImage || sidecarImage,
106
+ env: [
107
+ { name: 'JITSI_ROOM_URL', value: meetingUrl },
108
+ { name: 'JITSI_JWT', value: jwt },
109
+ { name: 'JITSI_ROOM_ID', value: context.roomId || '' },
110
+ { name: 'JITSI_PARTICIPANT_NAME', value: agentName || 'Kradle Agent' },
111
+ { name: 'JITSI_PARTICIPANT_ROLE', value: context.role || 'observer' },
112
+ { name: 'JITSI_AUDIO_MODE', value: capabilities.audio || 'listen' },
113
+ { name: 'JITSI_CHAT_MODE', value: capabilities.chat || 'read' },
114
+ { name: 'AGENT_SOCKET_PATH', value: SOCKET_PATH },
115
+ ],
116
+ volumeMounts: [{ name: 'agent-socket', mountPath: '/tmp' }],
117
+ resources: {
118
+ requests: { cpu: '100m', memory: '256Mi' },
119
+ limits: { cpu: '500m', memory: '512Mi' },
120
+ },
121
+ };
122
+ },
123
+
124
+ async onAgentJoined(dispatchRunRef, meetingRef) {
125
+ const event = { type: 'agent-joined-meeting', dispatchRunRef, meetingRef, timestamp: isoNow(now) };
126
+ eventBus?.emit?.(event);
127
+ await dispatchController?.recordMeetingEvent?.(event);
128
+ return event;
129
+ },
130
+
131
+ async onAgentLeft(dispatchRunRef, meetingRef, reason = 'completed') {
132
+ const event = { type: 'agent-left-meeting', dispatchRunRef, meetingRef, reason, timestamp: isoNow(now) };
133
+ eventBus?.emit?.(event);
134
+ await dispatchController?.recordMeetingEvent?.(event);
135
+ const participantEvent = { type: 'participant-left', dispatchRunRef, meetingRef, participant: { id: dispatchRunRef, type: 'agent' }, reason, timestamp: event.timestamp };
136
+ eventBus?.emit?.(participantEvent);
137
+ await dispatchController?.recordMeetingEvent?.(participantEvent);
138
+ return event;
139
+ },
140
+ };
141
+ }
@@ -0,0 +1,291 @@
1
+ import crypto from 'node:crypto';
2
+ import { createResource, validateResource, clone } from './resource-model.js';
3
+
4
+ const DEFAULT_TTL_MINUTES = 60;
5
+
6
+ function isoNow(now) {
7
+ return (typeof now === 'function' ? now() : new Date()).toISOString();
8
+ }
9
+
10
+ function epochSeconds(now, ttlMinutes) {
11
+ return Math.floor((typeof now === 'function' ? now() : new Date()).getTime() / 1000) + ttlMinutes * 60;
12
+ }
13
+
14
+ function encodeJwtSegment(value) {
15
+ return Buffer.from(JSON.stringify(value)).toString('base64url');
16
+ }
17
+
18
+ function signJwt(claims, secret) {
19
+ const header = encodeJwtSegment({ alg: 'HS256', typ: 'JWT' });
20
+ const payload = encodeJwtSegment(claims);
21
+ const signature = crypto.createHmac('sha256', secret).update(`${header}.${payload}`).digest('base64url');
22
+ return `${header}.${payload}.${signature}`;
23
+ }
24
+
25
+ export function createJitsiMeetingController(options = {}) {
26
+ const {
27
+ providerClient = {},
28
+ resourceGateway = null,
29
+ eventBus = null,
30
+ dispatchController = null,
31
+ jwtConfig = {},
32
+ jwtSecret = process.env.KRADLE_JITSI_JWT_SECRET || process.env.JITSI_JWT_SECRET || 'dev-jitsi-secret',
33
+ now = () => new Date(),
34
+ } = options;
35
+
36
+ async function listMeetings() {
37
+ const result = await resourceGateway?.list?.('JitsiMeeting');
38
+ return result?.items || (Array.isArray(result) ? result : []);
39
+ }
40
+
41
+ async function getMeeting(nameOrRoomId) {
42
+ const direct = await resourceGateway?.get?.('JitsiMeeting', nameOrRoomId);
43
+ if (direct) return direct;
44
+ return (await listMeetings()).find((meeting) => meeting.metadata?.name === nameOrRoomId || meeting.spec?.roomId === nameOrRoomId) || null;
45
+ }
46
+
47
+ async function getResource(kind, name) {
48
+ if (!name) return null;
49
+ return resourceGateway?.get?.(kind, name) || null;
50
+ }
51
+
52
+ async function persist(resource) {
53
+ if (resourceGateway?.apply) {
54
+ const result = await resourceGateway.apply(resource);
55
+ return result?.resource || resource;
56
+ }
57
+ return resource;
58
+ }
59
+
60
+ function emit(type, resource, extra = {}) {
61
+ eventBus?.emit?.({ type, resource, timestamp: isoNow(now), ...extra });
62
+ }
63
+
64
+ return {
65
+ role: 'jitsi-meeting-controller',
66
+
67
+ validate(resource) {
68
+ if (resource?.kind !== 'JitsiMeeting') throw new Error('Jitsi meeting controller validates JitsiMeeting resources only');
69
+ return validateResource(resource);
70
+ },
71
+
72
+ async createRoom(meetingSpec = {}) {
73
+ const roomId = meetingSpec.roomId;
74
+ if (!roomId) throw new Error('createRoom requires roomId');
75
+ const provider = await getResource('JitsiMeetProvider', meetingSpec.providerRef);
76
+ const template = await getResource('JitsiMeetingTemplate', meetingSpec.templateRef);
77
+ const mergedSpec = {
78
+ ...meetingSpec,
79
+ endpoint: meetingSpec.endpoint || provider?.spec?.endpoint,
80
+ ttlMinutes: meetingSpec.ttlMinutes || template?.spec?.ttlMinutes || provider?.spec?.defaultRoomTTL || DEFAULT_TTL_MINUTES,
81
+ roomConfig: {
82
+ ...(provider?.spec?.defaultRoomConfig || {}),
83
+ ...(template?.spec?.roomConfig || {}),
84
+ ...(meetingSpec.roomConfig || {}),
85
+ },
86
+ };
87
+ const providerResult = await providerClient.createRoom?.(mergedSpec) || {};
88
+ const resource = createResource('JitsiMeeting', {
89
+ name: meetingSpec.name || roomId,
90
+ namespace: meetingSpec.namespace || `kradle-org-${meetingSpec.organizationRef || 'default'}`,
91
+ }, {
92
+ organizationRef: meetingSpec.organizationRef || 'default',
93
+ providerRef: meetingSpec.providerRef || 'default',
94
+ templateRef: meetingSpec.templateRef,
95
+ roomId,
96
+ displayName: meetingSpec.displayName || roomId,
97
+ dispatchRunRef: meetingSpec.dispatchRunRef,
98
+ ttlMinutes: mergedSpec.ttlMinutes,
99
+ participants: clone(meetingSpec.participants || { invited: [] }),
100
+ roomConfig: clone(mergedSpec.roomConfig || {}),
101
+ }, {
102
+ phase: 'Active',
103
+ roomUrl: providerResult.roomUrl || meetingSpec.roomUrl || `${mergedSpec.endpoint || 'https://meet.kradle.local'}/${roomId}`,
104
+ startedAt: isoNow(now),
105
+ endedAt: null,
106
+ duration: null,
107
+ participants: { current: [], total: 0, peak: 0 },
108
+ recording: { active: false, recordingId: null },
109
+ });
110
+ this.validate(resource);
111
+ const persisted = await persist(resource);
112
+ emit('meeting-created', persisted, { roomId });
113
+ return persisted;
114
+ },
115
+
116
+ async endRoom(roomId) {
117
+ await providerClient.endRoom?.(roomId);
118
+ const meeting = await getMeeting(roomId);
119
+ if (!meeting) throw new Error(`JitsiMeeting for room ${roomId} not found`);
120
+ const endedAt = isoNow(now);
121
+ const updated = {
122
+ ...meeting,
123
+ status: {
124
+ ...(meeting.status || {}),
125
+ phase: 'Ended',
126
+ endedAt,
127
+ },
128
+ };
129
+ const persisted = await persist(updated);
130
+ emit('meeting-ended', persisted, { roomId });
131
+ return persisted;
132
+ },
133
+
134
+ generateParticipantJwt(roomId, participant = {}, ttlMinutes = DEFAULT_TTL_MINUTES, providerJwtConfig = {}) {
135
+ const config = { ...jwtConfig, ...providerJwtConfig, ...(participant.jwtConfig || {}) };
136
+ const claims = {
137
+ aud: config.audience || 'jitsi',
138
+ iss: config.issuer || 'kradle',
139
+ sub: config.subject || participant.subject || participant.org || 'kradle',
140
+ room: roomId,
141
+ exp: epochSeconds(now, Math.max(1, Number(ttlMinutes) || DEFAULT_TTL_MINUTES)),
142
+ context: {
143
+ user: {
144
+ id: participant.id || participant.ref || participant.name || 'kradle-user',
145
+ name: participant.name || participant.displayName || participant.id || 'Kradle user',
146
+ type: participant.type || 'user',
147
+ role: participant.role || 'participant',
148
+ avatar: participant.avatar || '',
149
+ },
150
+ features: participant.features || {},
151
+ },
152
+ };
153
+ return signJwt(claims, config.secret || jwtSecret);
154
+ },
155
+
156
+ async reconcile(meeting) {
157
+ this.validate(meeting);
158
+ const state = await providerClient.getRoom?.(meeting.spec.roomId) || {};
159
+ const currentTotal = Number(state.participantCount ?? meeting.status?.participants?.current?.length ?? 0);
160
+ const updated = {
161
+ ...meeting,
162
+ status: {
163
+ ...(meeting.status || {}),
164
+ phase: state.phase || meeting.status?.phase || 'Active',
165
+ participants: {
166
+ ...(meeting.status?.participants || {}),
167
+ total: currentTotal,
168
+ peak: Math.max(Number(meeting.status?.participants?.peak || 0), currentTotal),
169
+ },
170
+ lastReconciledAt: isoNow(now),
171
+ },
172
+ };
173
+ return persist(updated);
174
+ },
175
+
176
+ async listActiveMeetings(organizationRef) {
177
+ return (await listMeetings()).filter((meeting) => (
178
+ meeting.spec?.organizationRef === organizationRef && meeting.status?.phase === 'Active'
179
+ ));
180
+ },
181
+
182
+ async getMeeting(nameOrRoomId) {
183
+ return getMeeting(nameOrRoomId);
184
+ },
185
+
186
+ async getMeetingStats(roomId) {
187
+ return providerClient.getStats?.(roomId) || { active: false, participantCount: 0 };
188
+ },
189
+
190
+ async dispatchAutoJoinAgents(meetingRef, { resources = {}, repository = 'default', ref = 'main', actor = 'jitsi-meeting-controller', namespace = null } = {}) {
191
+ const meeting = typeof meetingRef === 'string' ? await getMeeting(meetingRef) : meetingRef;
192
+ if (!meeting) throw new Error(`JitsiMeeting ${meetingRef} not found`);
193
+ const template = meeting.spec?.templateRef
194
+ ? (resources.JitsiMeetingTemplate || []).find((candidate) => candidate.metadata?.name === meeting.spec.templateRef)
195
+ : null;
196
+ const autoJoin = meeting.spec?.agentConfig?.autoJoin === true || template?.spec?.agentConfig?.autoJoin === true;
197
+ if (!autoJoin) return [];
198
+ const participants = [
199
+ ...(meeting.spec?.participants?.autoInvite || []),
200
+ ...(template?.spec?.participants?.autoInvite || []),
201
+ ];
202
+ const dispatched = [];
203
+ for (const participant of participants) {
204
+ if (!['agentStack', 'agentDefinition'].includes(participant.type)) continue;
205
+ const result = await dispatchController?.createManualDispatch?.({
206
+ repository,
207
+ ref,
208
+ actor,
209
+ namespace: namespace || meeting.metadata?.namespace || 'default',
210
+ organizationRef: meeting.spec?.organizationRef || 'default',
211
+ meetingRef: meeting.metadata?.name,
212
+ resources,
213
+ taskKind: participant.taskKind || 'meeting',
214
+ ...(participant.type === 'agentDefinition' ? { agentDefinition: participant.ref } : { agentStack: participant.ref }),
215
+ });
216
+ if (result) dispatched.push(result);
217
+ }
218
+ return dispatched;
219
+ },
220
+
221
+ async startRecording(meetingRef) {
222
+ const meeting = await getMeeting(meetingRef);
223
+ if (!meeting) throw new Error(`JitsiMeeting ${meetingRef} not found`);
224
+ const result = await providerClient.startRecording?.(meeting.spec.roomId) || {};
225
+ const recordingId = result.recordingId || `rec-${meeting.metadata.name}`;
226
+ const recording = createResource('JitsiRecording', {
227
+ name: recordingId,
228
+ namespace: meeting.metadata?.namespace,
229
+ }, {
230
+ organizationRef: meeting.spec.organizationRef,
231
+ meetingRef: meeting.metadata.name,
232
+ providerRef: meeting.spec.providerRef,
233
+ format: result.format || 'mp4',
234
+ storageRef: result.storageRef,
235
+ }, {
236
+ phase: 'Recording',
237
+ startedAt: isoNow(now),
238
+ transcript: { available: false },
239
+ });
240
+ const updated = {
241
+ ...meeting,
242
+ status: {
243
+ ...(meeting.status || {}),
244
+ recording: { active: true, recordingId },
245
+ },
246
+ };
247
+ await persist(recording);
248
+ const persisted = await persist(updated);
249
+ emit('recording-started', persisted, { recordingId });
250
+ return persisted;
251
+ },
252
+
253
+ async stopRecording(meetingRef) {
254
+ const meeting = await getMeeting(meetingRef);
255
+ if (!meeting) throw new Error(`JitsiMeeting ${meetingRef} not found`);
256
+ const result = await providerClient.stopRecording?.(meeting.spec.roomId) || {};
257
+ const recordingId = result.recordingId || meeting.status?.recording?.recordingId || null;
258
+ if (recordingId) {
259
+ const existingRecording = await getResource('JitsiRecording', recordingId);
260
+ const recording = existingRecording || createResource('JitsiRecording', {
261
+ name: recordingId,
262
+ namespace: meeting.metadata?.namespace,
263
+ }, {
264
+ organizationRef: meeting.spec.organizationRef,
265
+ meetingRef: meeting.metadata.name,
266
+ providerRef: meeting.spec.providerRef,
267
+ format: result.format || 'mp4',
268
+ storageRef: result.storageRef,
269
+ }, {});
270
+ await persist({
271
+ ...recording,
272
+ status: {
273
+ ...(recording.status || {}),
274
+ phase: 'Completed',
275
+ endedAt: isoNow(now),
276
+ duration: result.duration,
277
+ transcript: recording.status?.transcript || { available: false },
278
+ },
279
+ });
280
+ }
281
+ const updated = {
282
+ ...meeting,
283
+ status: {
284
+ ...(meeting.status || {}),
285
+ recording: { active: false, recordingId },
286
+ },
287
+ };
288
+ return persist(updated);
289
+ },
290
+ };
291
+ }
@@ -0,0 +1,198 @@
1
+ import { createResource, clone } from './resource-model.js';
2
+
3
+ const EVENT_ALIASES = {
4
+ room_created: 'room-created',
5
+ room_destroyed: 'room-destroyed',
6
+ participant_joined: 'participant-joined',
7
+ participant_left: 'participant-left',
8
+ recording_started: 'recording-started',
9
+ recording_stopped: 'recording-stopped',
10
+ };
11
+
12
+ function canonicalType(value) {
13
+ return EVENT_ALIASES[value] || String(value || '').replaceAll('_', '-');
14
+ }
15
+
16
+ function isoNow(now) {
17
+ return (typeof now === 'function' ? now() : new Date()).toISOString();
18
+ }
19
+
20
+ export function createJitsiSyncController(options = {}) {
21
+ const { persistFn, eventBus, now = () => new Date() } = options;
22
+ const meetings = new Map();
23
+ const recordings = new Map();
24
+ const watermarks = new Map();
25
+ const processedDeliveries = new Set();
26
+
27
+ async function persist(resource) {
28
+ if (typeof persistFn === 'function') await persistFn(resource);
29
+ return resource;
30
+ }
31
+
32
+ function emit(type, resource, extra = {}) {
33
+ eventBus?.emit?.({ type, resource, timestamp: isoNow(now), ...extra });
34
+ }
35
+
36
+ function meetingName(roomId, state = {}) {
37
+ return state.meetingRef || state.name || roomId;
38
+ }
39
+
40
+ function rememberDelivery(event = {}) {
41
+ if (!event.deliveryId) return false;
42
+ if (processedDeliveries.has(event.deliveryId)) return true;
43
+ processedDeliveries.add(event.deliveryId);
44
+ return false;
45
+ }
46
+
47
+ return {
48
+ role: 'jitsi-sync-controller',
49
+
50
+ normalizeEvent(rawWebhookPayload = {}) {
51
+ const eventType = canonicalType(rawWebhookPayload.eventType || rawWebhookPayload.type || rawWebhookPayload.event);
52
+ return {
53
+ deliveryId: rawWebhookPayload.deliveryId || rawWebhookPayload.id || null,
54
+ eventType,
55
+ providerRef: rawWebhookPayload.providerRef || rawWebhookPayload.provider || 'default',
56
+ organizationRef: rawWebhookPayload.organizationRef || rawWebhookPayload.org || 'default',
57
+ roomId: rawWebhookPayload.roomId || rawWebhookPayload.roomName || rawWebhookPayload.room,
58
+ meetingRef: rawWebhookPayload.meetingRef,
59
+ recordingId: rawWebhookPayload.recordingId,
60
+ participant: rawWebhookPayload.participant || rawWebhookPayload.user || {},
61
+ timestamp: rawWebhookPayload.timestamp || rawWebhookPayload.receivedAt || isoNow(now),
62
+ data: rawWebhookPayload,
63
+ };
64
+ },
65
+
66
+ async syncRoom(roomId, jitsiState = {}) {
67
+ if (rememberDelivery(jitsiState)) return meetings.get(roomId) || null;
68
+ const eventType = canonicalType(jitsiState.eventType || jitsiState.type || 'room-created');
69
+ const key = roomId;
70
+ const existing = meetings.get(key);
71
+ const phase = eventType === 'room-destroyed' ? 'Ended' : (jitsiState.phase || 'Active');
72
+ const resource = {
73
+ ...(existing || createResource('JitsiMeeting', {
74
+ name: meetingName(roomId, jitsiState),
75
+ namespace: jitsiState.namespace || `kradle-org-${jitsiState.organizationRef || 'default'}`,
76
+ }, {
77
+ organizationRef: jitsiState.organizationRef || 'default',
78
+ providerRef: jitsiState.providerRef || 'default',
79
+ roomId,
80
+ displayName: jitsiState.displayName || roomId,
81
+ ttlMinutes: jitsiState.ttlMinutes || 120,
82
+ participants: { invited: [] },
83
+ roomConfig: {},
84
+ }, {
85
+ participants: { current: [], total: 0, peak: 0 },
86
+ recording: { active: false, recordingId: null },
87
+ })),
88
+ };
89
+ resource.status = {
90
+ ...(resource.status || {}),
91
+ phase,
92
+ roomUrl: jitsiState.roomUrl || resource.status?.roomUrl || `https://meet.kradle.local/${roomId}`,
93
+ ...(phase === 'Active' && !resource.status?.startedAt ? { startedAt: jitsiState.timestamp || isoNow(now) } : {}),
94
+ ...(phase === 'Ended' ? { endedAt: jitsiState.timestamp || isoNow(now) } : {}),
95
+ };
96
+ meetings.set(key, resource);
97
+ await persist(resource);
98
+ emit(phase === 'Ended' ? 'meeting-ended' : 'meeting-created', resource, { roomId });
99
+ return resource;
100
+ },
101
+
102
+ async syncParticipant(roomId, participantEvent = {}) {
103
+ if (rememberDelivery(participantEvent)) return meetings.get(roomId) || null;
104
+ const eventType = canonicalType(participantEvent.eventType || participantEvent.type || 'participant-joined');
105
+ const meeting = meetings.get(roomId) || await this.syncRoom(roomId, participantEvent);
106
+ const participant = {
107
+ id: participantEvent.participant?.id || participantEvent.id || participantEvent.participant?.name,
108
+ name: participantEvent.participant?.name || participantEvent.name || participantEvent.participant?.id,
109
+ type: participantEvent.participant?.type || participantEvent.type || 'user',
110
+ joinedAt: participantEvent.timestamp || isoNow(now),
111
+ ...clone(participantEvent.participant || {}),
112
+ };
113
+ const existing = meeting.status?.participants?.current || [];
114
+ const current = eventType === 'participant-left'
115
+ ? existing.filter((item) => item.id !== participant.id)
116
+ : [...existing.filter((item) => item.id !== participant.id), participant];
117
+ const previousTotal = Number(meeting.status?.participants?.total || 0);
118
+ const total = eventType === 'participant-joined' && !existing.some((item) => item.id === participant.id)
119
+ ? previousTotal + 1
120
+ : previousTotal;
121
+ meeting.status = {
122
+ ...(meeting.status || {}),
123
+ participants: {
124
+ current,
125
+ total,
126
+ peak: Math.max(Number(meeting.status?.participants?.peak || 0), current.length, total),
127
+ },
128
+ };
129
+ meetings.set(roomId, meeting);
130
+ await persist(meeting);
131
+ emit(eventType, meeting, { roomId, participant });
132
+ return meeting;
133
+ },
134
+
135
+ async syncRecording(roomId, recordingEvent = {}) {
136
+ if (rememberDelivery(recordingEvent)) return recordings.get(recordingEvent.recordingId || `rec-${roomId}`) || null;
137
+ const eventType = canonicalType(recordingEvent.eventType || recordingEvent.type || 'recording-started');
138
+ const recordingId = recordingEvent.recordingId || `rec-${roomId}`;
139
+ const existing = recordings.get(recordingId);
140
+ const phase = eventType === 'recording-stopped' ? 'Completed' : 'Recording';
141
+ const resource = {
142
+ ...(existing || createResource('JitsiRecording', {
143
+ name: recordingId,
144
+ namespace: recordingEvent.namespace || `kradle-org-${recordingEvent.organizationRef || 'default'}`,
145
+ }, {
146
+ organizationRef: recordingEvent.organizationRef || 'default',
147
+ meetingRef: recordingEvent.meetingRef || roomId,
148
+ providerRef: recordingEvent.providerRef || 'default',
149
+ format: recordingEvent.format || 'mp4',
150
+ storageRef: recordingEvent.storageRef,
151
+ }, {
152
+ transcript: { available: false },
153
+ })),
154
+ };
155
+ resource.status = {
156
+ ...(resource.status || {}),
157
+ phase,
158
+ ...(phase === 'Recording' ? { startedAt: recordingEvent.timestamp || isoNow(now) } : {}),
159
+ ...(phase === 'Completed' ? { endedAt: recordingEvent.timestamp || isoNow(now), duration: recordingEvent.duration } : {}),
160
+ };
161
+ recordings.set(recordingId, resource);
162
+ const meeting = meetings.get(roomId);
163
+ if (meeting) {
164
+ meeting.status = {
165
+ ...(meeting.status || {}),
166
+ recording: { active: phase === 'Recording', recordingId: phase === 'Recording' ? recordingId : null },
167
+ };
168
+ await persist(meeting);
169
+ }
170
+ await persist(resource);
171
+ emit(eventType, resource, { roomId, recordingId });
172
+ return resource;
173
+ },
174
+
175
+ async updateWatermark(providerRef, timestamp, options = {}) {
176
+ const current = watermarks.get(providerRef);
177
+ if (!current || timestamp > current) {
178
+ watermarks.set(providerRef, timestamp);
179
+ await persist({
180
+ apiVersion: 'kradle.a5c.ai/v1alpha1',
181
+ kind: 'ExternalSyncState',
182
+ metadata: {
183
+ name: `jitsi-watermark-${providerRef}`,
184
+ namespace: options.namespace || `kradle-org-${options.organizationRef || 'default'}`,
185
+ labels: {},
186
+ annotations: {},
187
+ },
188
+ spec: { organizationRef: options.organizationRef || 'default', providerRef, resourceRef: 'jitsi', phase: 'Synced', watermark: timestamp },
189
+ status: { phase: 'Synced', lastSuccessfulSyncAt: isoNow(now) },
190
+ });
191
+ }
192
+ },
193
+
194
+ getWatermark(providerRef) {
195
+ return watermarks.get(providerRef) || null;
196
+ },
197
+ };
198
+ }