@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,119 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { createAgentMuxClient, createJitsiAgentBridge, createResource } from '../src/index.js';
4
+
5
+ function stack(spec = {}) {
6
+ return createResource('AgentStack', { name: 'standup-bot', namespace: 'kradle-org-default' }, {
7
+ organizationRef: 'default',
8
+ baseAgent: 'claude-code',
9
+ adapter: 'claude-code',
10
+ runtimeIdentity: { serviceAccountRef: 'kradle' },
11
+ ...spec,
12
+ });
13
+ }
14
+
15
+ test('Jitsi agent bridge gates capability and prepares meeting context', async () => {
16
+ const emitted = [];
17
+ const bridge = createJitsiAgentBridge({
18
+ meetingController: {
19
+ async getMeeting(ref) {
20
+ assert.equal(ref, 'daily');
21
+ return {
22
+ metadata: { name: 'daily' },
23
+ spec: { roomId: 'daily-default', ttlMinutes: 30 },
24
+ status: { phase: 'Active', roomUrl: 'https://meet.example/daily-default' },
25
+ };
26
+ },
27
+ generateParticipantJwt(roomId, participant, ttlMinutes) {
28
+ assert.equal(roomId, 'daily-default');
29
+ assert.equal(participant.id, 'dispatch-1');
30
+ assert.equal(ttlMinutes, 30);
31
+ return 'kradle-jitsi.jwt.sig';
32
+ },
33
+ },
34
+ eventBus: { emit: (event) => emitted.push(event) },
35
+ now: () => new Date('2026-05-30T12:00:00Z'),
36
+ });
37
+
38
+ assert.equal(bridge.hasMeetingCapability(stack()), false);
39
+ assert.equal(bridge.hasMeetingCapability(stack({ jitsiCapability: true })), true);
40
+
41
+ const run = { metadata: { name: 'dispatch-1' }, spec: {}, status: {} };
42
+ const context = await bridge.prepareMeetingContext(run, 'daily', stack({
43
+ jitsiCapability: true,
44
+ jitsiConfig: { participantName: 'Standup Bot', role: 'observer', capabilities: { chat: 'readwrite', audio: 'listen' } },
45
+ }));
46
+ assert.equal(context.roomUrl, 'https://meet.example/daily-default');
47
+ assert.equal(context.jwt, 'kradle-jitsi.jwt.sig');
48
+ assert.equal(context.role, 'observer');
49
+ assert.equal(run.spec.meetingRef, 'daily');
50
+ assert.equal(run.spec.meetingContext.roomId, 'daily-default');
51
+ assert.equal(run.spec.meetingContext.jwt, undefined);
52
+ assert.deepEqual(run.spec.meetingContext.tokenRef, { runtimeOnly: true });
53
+
54
+ await bridge.onAgentJoined('dispatch-1', 'daily');
55
+ await bridge.onAgentLeft('dispatch-1', 'daily', 'completed');
56
+ assert.deepEqual(emitted.map((event) => event.type), ['agent-joined-meeting', 'agent-left-meeting', 'participant-left']);
57
+ });
58
+
59
+ test('Jitsi agent bridge can resolve meetings from resources and keeps generated JWT runtime-only', async () => {
60
+ const bridge = createJitsiAgentBridge({ now: () => new Date('2026-05-30T12:00:00Z') });
61
+ const run = { metadata: { name: 'dispatch-1' }, spec: {}, status: {} };
62
+ const context = await bridge.prepareMeetingContext(run, 'daily', stack({
63
+ jitsiCapability: true,
64
+ jitsiConfig: { role: 'participant', capabilities: { chat: 'readwrite', audio: 'listen' } },
65
+ }), {
66
+ resources: {
67
+ JitsiMeeting: [
68
+ createResource('JitsiMeeting', { name: 'daily', namespace: 'kradle-org-default' }, {
69
+ organizationRef: 'default',
70
+ providerRef: 'jitsi-prod',
71
+ roomId: 'daily-default',
72
+ ttlMinutes: 30,
73
+ }, {
74
+ phase: 'Active',
75
+ roomUrl: 'https://meet.example/daily-default',
76
+ }),
77
+ ],
78
+ },
79
+ });
80
+
81
+ assert.match(context.jwt, /^kradle-jitsi\./);
82
+ assert.equal(run.spec.meetingContext.jwt, undefined);
83
+ assert.equal(run.spec.meetingContext.role, 'participant');
84
+ });
85
+
86
+ test('Jitsi agent bridge builds sidecar specs and Agent Mux injects them only for meeting runs', () => {
87
+ const bridge = createJitsiAgentBridge({
88
+ sidecarImage: 'ghcr.io/a5c-ai/jitsi-agent-sidecar:test',
89
+ });
90
+ const sidecar = bridge.buildSidecarSpec('https://meet.example/daily-default', 'kradle-jitsi.jwt.sig', 'Standup Bot', {
91
+ roomId: 'daily-default',
92
+ role: 'observer',
93
+ capabilities: { audio: 'listen', chat: 'readwrite' },
94
+ });
95
+ assert.equal(sidecar.name, 'jitsi-agent-sidecar');
96
+ assert.equal(sidecar.image, 'ghcr.io/a5c-ai/jitsi-agent-sidecar:test');
97
+ assert.equal(sidecar.env.find((entry) => entry.name === 'JITSI_ROOM_URL').value, 'https://meet.example/daily-default');
98
+ assert.equal(sidecar.env.find((entry) => entry.name === 'JITSI_CHAT_MODE').value, 'readwrite');
99
+
100
+ const client = createAgentMuxClient();
101
+ const plainJob = client.createAgentJob({ adapter: 'claude-code', org: 'default' }).jobManifest;
102
+ assert.equal(plainJob.spec.template.spec.containers.length, 1);
103
+
104
+ const meetingJob = client.createAgentJob({
105
+ adapter: 'claude-code',
106
+ org: 'default',
107
+ meetingContext: {
108
+ roomUrl: 'https://meet.example/daily-default',
109
+ jwt: 'kradle-jitsi.jwt.sig',
110
+ roomId: 'daily-default',
111
+ participantName: 'Standup Bot',
112
+ role: 'observer',
113
+ capabilities: { audio: 'listen', chat: 'readwrite' },
114
+ },
115
+ }).jobManifest;
116
+ assert.equal(meetingJob.spec.template.spec.containers.length, 2);
117
+ assert.ok(meetingJob.spec.template.spec.containers[0].env.some((entry) => entry.name === 'JITSI_MEETING_ACTIVE' && entry.value === 'true'));
118
+ assert.ok(meetingJob.spec.template.spec.volumes.some((volume) => volume.name === 'agent-socket'));
119
+ });
@@ -0,0 +1,77 @@
1
+ import assert from 'node:assert/strict';
2
+ import { readFileSync } from 'node:fs';
3
+ import test from 'node:test';
4
+
5
+ const chart = readFileSync(new URL('../../charts/Chart.yaml', import.meta.url), 'utf8');
6
+ const values = readFileSync(new URL('../../charts/values.yaml', import.meta.url), 'utf8');
7
+ const networkPolicy = readFileSync(new URL('../../charts/templates/networkpolicy.yaml', import.meta.url), 'utf8');
8
+ const secrets = readFileSync(new URL('../../charts/templates/jitsi-secrets.yaml', import.meta.url), 'utf8');
9
+ const jitsiCrds = readFileSync(new URL('../../charts/crds/jitsi-resources.yaml', import.meta.url), 'utf8');
10
+ const agentCrds = readFileSync(new URL('../../charts/crds/agent-resources.yaml', import.meta.url), 'utf8');
11
+ const aggregatedCrds = readFileSync(new URL('../../charts/crds/aggregated-resources.yaml', import.meta.url), 'utf8');
12
+ const externalCrds = readFileSync(new URL('../../charts/crds/external-resources.yaml', import.meta.url), 'utf8');
13
+
14
+ test('Kradle chart declares the Jitsi Meet subchart dependency behind jitsi.install', () => {
15
+ assert.match(chart, /name:\s+jitsi-meet/);
16
+ assert.match(chart, /repository:\s+https:\/\/jitsi-contrib\.github\.io\/jitsi-helm\//);
17
+ assert.match(chart, /condition:\s+jitsi\.install/);
18
+ assert.match(chart, /alias:\s+jitsi-subchart/);
19
+ });
20
+
21
+ test('values expose internal and external Jitsi deployment settings', () => {
22
+ for (const term of [
23
+ 'jitsi:',
24
+ 'install: false',
25
+ 'external:',
26
+ 'publicURL: https://meet.kradle.local',
27
+ 'jwtAppSecret:',
28
+ 'web:',
29
+ 'prosody:',
30
+ 'type: jwt',
31
+ 'existingSecretName:',
32
+ 'jicofo:',
33
+ 'jvb:',
34
+ 'useNodeIP: true',
35
+ 'UDPPort: 10000',
36
+ 'udpPort: 10000',
37
+ 'jibri:',
38
+ 'recordingStorageClass:',
39
+ 'jitsi-subchart:',
40
+ ]) {
41
+ assert.ok(values.includes(term), `values.yaml should include ${term}`);
42
+ }
43
+ assert.match(values, /jitsi-subchart:[\s\S]*prosody:\n\s+<<: \*jitsiProsody[\s\S]*secretEnvs:\n\s+JWT_APP_SECRET: ""[\s\S]*jwt:\n\s+secret: ""\n\s+existingSecretName: ""/);
44
+ });
45
+
46
+ test('Jitsi CRDs are installed from a dedicated chart CRD file', () => {
47
+ const expectedKinds = ['JitsiMeetProvider', 'JitsiMeetingTemplate', 'JitsiMeeting', 'JitsiRecording'];
48
+ for (const kind of expectedKinds) {
49
+ assert.match(jitsiCrds, new RegExp(`kind:\\s+${kind}`));
50
+ }
51
+ assert.match(jitsiCrds, /required:\n\s+- organizationRef/);
52
+ assert.match(jitsiCrds, /name:\s+jitsimeetproviders\.kradle\.a5c\.ai/);
53
+ assert.match(jitsiCrds, /name:\s+jitsimeetings\.kradle\.a5c\.ai/);
54
+ });
55
+
56
+ test('Jitsi CRDs are not duplicated in non-Jitsi chart CRD bundles', () => {
57
+ for (const crdBundle of [agentCrds, aggregatedCrds, externalCrds]) {
58
+ assert.doesNotMatch(crdBundle, /name:\s+jitsi(?:meetproviders|meetingtemplates|meetings|recordings)\.kradle\.a5c\.ai/);
59
+ }
60
+ });
61
+
62
+ test('chart templates manage Jitsi secrets without committing literal credentials', () => {
63
+ assert.match(secrets, /jitsi-jwt/);
64
+ assert.match(secrets, /appSecret:\s+\{\{ \$jwtAppSecret \| quote \}\}/);
65
+ assert.match(secrets, /jitsi-webhook/);
66
+ assert.match(secrets, /webhookExistingSecret/);
67
+ assert.match(secrets, /webhookSecret:\s+\{\{ \.Values\.jitsi\.kradle\.webhookSecret \| quote \}\}/);
68
+ });
69
+
70
+ test('network policy opens JVB UDP media traffic only when Jitsi is installed', () => {
71
+ assert.match(networkPolicy, /if \.Values\.jitsi\.install/);
72
+ assert.match(networkPolicy, /jitsi-media/);
73
+ assert.match(networkPolicy, /component/);
74
+ assert.match(networkPolicy, /jvb/);
75
+ assert.match(networkPolicy, /protocol:\s+UDP/);
76
+ assert.match(networkPolicy, /port:\s+\{\{ \.Values\.jitsi\.jvb\.service\.udpPort \| default 10000 \}\}/);
77
+ });
@@ -0,0 +1,170 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { createJitsiMeetingController, createResource } from '../src/index.js';
4
+
5
+ function meeting(overrides = {}) {
6
+ return createResource('JitsiMeeting', { name: 'daily', namespace: 'kradle-org-default' }, {
7
+ organizationRef: 'default',
8
+ providerRef: 'jitsi-prod',
9
+ roomId: 'daily-default',
10
+ displayName: 'Daily',
11
+ ttlMinutes: 30,
12
+ ...overrides.spec,
13
+ }, {
14
+ phase: 'Scheduled',
15
+ participants: { current: [], total: 0, peak: 0 },
16
+ recording: { active: false, recordingId: null },
17
+ ...overrides.status,
18
+ });
19
+ }
20
+
21
+ test('Jitsi meeting controller validates required meeting resources and JWT claims', () => {
22
+ const controller = createJitsiMeetingController({ jwtSecret: 'test-secret', now: () => new Date('2026-05-30T12:00:00Z') });
23
+ const valid = meeting();
24
+
25
+ assert.equal(controller.validate(valid), valid);
26
+ assert.throws(() => controller.validate(meeting({ spec: { roomId: '' } })), /JitsiMeeting spec.roomId is required/);
27
+
28
+ const jwt = controller.generateParticipantJwt('daily-default', {
29
+ id: 'agent-run-1',
30
+ name: 'Standup Bot',
31
+ type: 'agent',
32
+ role: 'observer',
33
+ }, 15);
34
+ const [header, encoded, signature] = jwt.split('.');
35
+ assert.ok(header);
36
+ assert.ok(signature);
37
+ const claims = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'));
38
+ assert.equal(claims.aud, 'jitsi');
39
+ assert.equal(claims.iss, 'kradle');
40
+ assert.equal(claims.room, 'daily-default');
41
+ assert.equal(claims.context.user.id, 'agent-run-1');
42
+ assert.equal(claims.context.user.name, 'Standup Bot');
43
+ assert.equal(claims.context.user.type, 'agent');
44
+ assert.equal(claims.context.user.role, 'observer');
45
+ assert.equal(claims.exp, Math.floor(new Date('2026-05-30T12:15:00Z').getTime() / 1000));
46
+ });
47
+
48
+ test('Jitsi meeting controller delegates room lifecycle, recording, stats, and emits events', async () => {
49
+ const persisted = [];
50
+ const emitted = [];
51
+ const providerCalls = [];
52
+ const resources = [meeting({ status: { phase: 'Active', roomUrl: 'https://meet.example/daily-default' } })];
53
+ const providers = [
54
+ createResource('JitsiMeetProvider', { name: 'jitsi-prod', namespace: 'kradle-org-default' }, {
55
+ organizationRef: 'default',
56
+ endpoint: 'https://meet.example',
57
+ authMode: 'jwt',
58
+ defaultRoomTTL: 90,
59
+ defaultRoomConfig: { lobby: true },
60
+ }),
61
+ ];
62
+ const templates = [
63
+ createResource('JitsiMeetingTemplate', { name: 'standup', namespace: 'kradle-org-default' }, {
64
+ organizationRef: 'default',
65
+ providerRef: 'jitsi-prod',
66
+ displayName: 'Standup',
67
+ ttlMinutes: 45,
68
+ roomConfig: { startWithAudioMuted: true },
69
+ }),
70
+ ];
71
+ const recordings = [];
72
+ const controller = createJitsiMeetingController({
73
+ jwtSecret: 'test-secret',
74
+ providerClient: {
75
+ async createRoom(spec) {
76
+ providerCalls.push(['createRoom', spec.roomId, spec.ttlMinutes, spec.roomConfig]);
77
+ return { roomUrl: `https://meet.example/${spec.roomId}` };
78
+ },
79
+ async endRoom(roomId) { providerCalls.push(['endRoom', roomId]); return { ended: true }; },
80
+ async getRoom(roomId) { providerCalls.push(['getRoom', roomId]); return { phase: 'Active', participantCount: 2 }; },
81
+ async getStats(roomId) { providerCalls.push(['getStats', roomId]); return { participantCount: 2, active: true }; },
82
+ async startRecording(roomId) { providerCalls.push(['startRecording', roomId]); return { recordingId: 'rec-1' }; },
83
+ async stopRecording(roomId) { providerCalls.push(['stopRecording', roomId]); return { recordingId: 'rec-1', duration: 120 }; },
84
+ },
85
+ resourceGateway: {
86
+ async list(kind) { assert.equal(kind, 'JitsiMeeting'); return { items: resources }; },
87
+ async apply(resource) {
88
+ persisted.push(resource);
89
+ if (resource.kind === 'JitsiRecording') recordings.push(resource);
90
+ return { resource };
91
+ },
92
+ async get(kind, name) {
93
+ if (kind === 'JitsiMeetProvider') return providers.find((resource) => resource.metadata.name === name);
94
+ if (kind === 'JitsiMeetingTemplate') return templates.find((resource) => resource.metadata.name === name);
95
+ if (kind === 'JitsiRecording') return recordings.find((resource) => resource.metadata.name === name);
96
+ assert.equal(kind, 'JitsiMeeting');
97
+ return resources.find((resource) => resource.metadata.name === name);
98
+ },
99
+ },
100
+ eventBus: { emit(event) { emitted.push(event); } },
101
+ now: () => new Date('2026-05-30T12:00:00Z'),
102
+ });
103
+
104
+ const created = await controller.createRoom({ organizationRef: 'default', providerRef: 'jitsi-prod', templateRef: 'standup', roomId: 'daily-default', displayName: 'Daily', roomConfig: { startWithVideoMuted: true } });
105
+ assert.equal(created.status.phase, 'Active');
106
+ assert.equal(created.status.roomUrl, 'https://meet.example/daily-default');
107
+ assert.equal(created.spec.ttlMinutes, 45);
108
+ assert.deepEqual(created.spec.roomConfig, { lobby: true, startWithAudioMuted: true, startWithVideoMuted: true });
109
+ assert.equal(emitted.at(-1).type, 'meeting-created');
110
+
111
+ const active = await controller.listActiveMeetings('default');
112
+ assert.equal(active.length, 1);
113
+
114
+ const reconciled = await controller.reconcile(resources[0]);
115
+ assert.equal(reconciled.status.participants.total, 2);
116
+
117
+ assert.deepEqual(await controller.getMeetingStats('daily-default'), { participantCount: 2, active: true });
118
+ assert.equal((await controller.startRecording('daily')).status.recording.recordingId, 'rec-1');
119
+ assert.equal(recordings.at(-1).status.phase, 'Recording');
120
+ assert.equal(emitted.at(-1).type, 'recording-started');
121
+ assert.equal((await controller.stopRecording('daily')).status.recording.active, false);
122
+ assert.equal(recordings.at(-1).status.phase, 'Completed');
123
+ assert.equal(recordings.at(-1).status.duration, 120);
124
+ assert.equal((await controller.endRoom('daily-default')).status.phase, 'Ended');
125
+ assert.deepEqual(providerCalls.map(([name]) => name), ['createRoom', 'getRoom', 'getStats', 'startRecording', 'stopRecording', 'endRoom']);
126
+ assert.ok(persisted.length >= 4);
127
+ });
128
+
129
+ test('Jitsi meeting controller auto-dispatches template agents when autoJoin is enabled', async () => {
130
+ const calls = [];
131
+ const activeMeeting = meeting({
132
+ spec: {
133
+ templateRef: 'standup-template',
134
+ },
135
+ status: { phase: 'Active', roomUrl: 'https://meet.example/daily-default' },
136
+ });
137
+ const controller = createJitsiMeetingController({
138
+ resourceGateway: {
139
+ async list() { return { items: [activeMeeting] }; },
140
+ async get() { return activeMeeting; },
141
+ },
142
+ dispatchController: {
143
+ async createManualDispatch(input) {
144
+ calls.push(input);
145
+ return { error: false, run: { metadata: { name: `run-${calls.length}` } } };
146
+ },
147
+ },
148
+ });
149
+
150
+ const results = await controller.dispatchAutoJoinAgents('daily', {
151
+ resources: {
152
+ JitsiMeetingTemplate: [
153
+ createResource('JitsiMeetingTemplate', { name: 'standup-template', namespace: 'kradle-org-default' }, {
154
+ organizationRef: 'default',
155
+ providerRef: 'jitsi-prod',
156
+ displayName: 'Standup',
157
+ agentConfig: { autoJoin: true },
158
+ participants: { autoInvite: [{ type: 'agentStack', ref: 'standup-bot' }] },
159
+ }),
160
+ ],
161
+ },
162
+ repository: 'repo',
163
+ ref: 'main',
164
+ });
165
+
166
+ assert.equal(results.length, 1);
167
+ assert.equal(calls[0].agentStack, 'standup-bot');
168
+ assert.equal(calls[0].meetingRef, 'daily');
169
+ assert.equal(calls[0].taskKind, 'meeting');
170
+ });
@@ -0,0 +1,73 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { createResource, listResourceDefinitions, resourceSchemaForKind, storageClassForKind, validateResource } from '../src/index.js';
4
+
5
+ const JITSI_KINDS = {
6
+ JitsiMeetProvider: {
7
+ storage: 'etcd',
8
+ context: 'external-backends',
9
+ plural: 'jitsimeetproviders',
10
+ requiredSpec: ['organizationRef', 'endpoint', 'authMode'],
11
+ validSpec: { organizationRef: 'default', endpoint: 'https://meet.kradle.local', authMode: 'jwt' },
12
+ },
13
+ JitsiMeetingTemplate: {
14
+ storage: 'etcd',
15
+ context: 'agents',
16
+ plural: 'jitsimeetingtemplates',
17
+ requiredSpec: ['organizationRef', 'providerRef', 'displayName'],
18
+ validSpec: { organizationRef: 'default', providerRef: 'jitsi-prod', displayName: 'Daily Standup' },
19
+ },
20
+ JitsiMeeting: {
21
+ storage: 'postgres',
22
+ context: 'agents',
23
+ plural: 'jitsimeetings',
24
+ requiredSpec: ['organizationRef', 'providerRef', 'roomId'],
25
+ validSpec: { organizationRef: 'default', providerRef: 'jitsi-prod', roomId: 'standup-20260530-default' },
26
+ },
27
+ JitsiRecording: {
28
+ storage: 'postgres',
29
+ context: 'agents',
30
+ plural: 'jitsirecordings',
31
+ requiredSpec: ['organizationRef', 'meetingRef', 'providerRef'],
32
+ validSpec: { organizationRef: 'default', meetingRef: 'standup-2026-05-30', providerRef: 'jitsi-prod' },
33
+ },
34
+ };
35
+
36
+ test('Jitsi resource kinds are registered with #624 frozen storage contracts', () => {
37
+ const definitions = listResourceDefinitions();
38
+ for (const [kind, expected] of Object.entries(JITSI_KINDS)) {
39
+ const definition = definitions.find((entry) => entry.kind === kind);
40
+ assert.ok(definition, `${kind} must be present in listResourceDefinitions`);
41
+ assert.equal(definition.storage, expected.storage);
42
+ assert.equal(definition.context, expected.context);
43
+ assert.equal(definition.plural, expected.plural);
44
+ assert.deepEqual(definition.requiredSpec, expected.requiredSpec);
45
+ assert.equal(storageClassForKind(kind), expected.storage);
46
+ }
47
+ });
48
+
49
+ test('Jitsi resource schemas expose documented required spec fields', () => {
50
+ for (const [kind, expected] of Object.entries(JITSI_KINDS)) {
51
+ const schema = resourceSchemaForKind(kind);
52
+ assert.equal(schema.kind, kind);
53
+ assert.equal(schema.plural, expected.plural);
54
+ assert.equal(schema.storage, expected.storage);
55
+ assert.deepEqual(schema.required.spec, expected.requiredSpec);
56
+ }
57
+ });
58
+
59
+ test('Jitsi resources validate required org-scoped specs', () => {
60
+ for (const [kind, expected] of Object.entries(JITSI_KINDS)) {
61
+ const resource = createResource(kind, { name: kind.toLowerCase(), namespace: 'kradle-org-default' }, expected.validSpec);
62
+ assert.equal(resource.kind, kind);
63
+ assert.equal(validateResource(resource), resource);
64
+
65
+ for (const field of expected.requiredSpec) {
66
+ const invalid = createResource(kind, { name: `${kind.toLowerCase()}-missing-${field}` }, {
67
+ ...expected.validSpec,
68
+ [field]: '',
69
+ });
70
+ assert.throws(() => validateResource(invalid), new RegExp(`${kind} spec.${field} is required`));
71
+ }
72
+ }
73
+ });
@@ -0,0 +1,112 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { createJitsiSyncController } from '../src/index.js';
4
+
5
+ test('Jitsi sync controller normalizes room, participant, and recording events', async () => {
6
+ const persisted = [];
7
+ const emitted = [];
8
+ const controller = createJitsiSyncController({
9
+ persistFn: async (resource) => persisted.push(resource),
10
+ eventBus: { emit: (event) => emitted.push(event) },
11
+ now: () => new Date('2026-05-30T12:00:00Z'),
12
+ });
13
+
14
+ const normalized = controller.normalizeEvent({
15
+ type: 'participant_joined',
16
+ room: 'daily-default',
17
+ participant: { id: 'alice', name: 'Alice', type: 'user' },
18
+ providerRef: 'jitsi-prod',
19
+ timestamp: '2026-05-30T12:01:00Z',
20
+ });
21
+ assert.equal(normalized.eventType, 'participant-joined');
22
+ assert.equal(normalized.roomId, 'daily-default');
23
+ assert.equal(normalized.participant.id, 'alice');
24
+
25
+ const active = await controller.syncRoom('daily-default', {
26
+ eventType: 'room-created',
27
+ organizationRef: 'default',
28
+ providerRef: 'jitsi-prod',
29
+ roomUrl: 'https://meet.example/daily-default',
30
+ displayName: 'Daily',
31
+ });
32
+ assert.equal(active.kind, 'JitsiMeeting');
33
+ assert.equal(active.status.phase, 'Active');
34
+ assert.equal(emitted.at(-1).type, 'meeting-created');
35
+
36
+ await controller.syncParticipant('daily-default', {
37
+ eventType: 'participant-joined',
38
+ participant: { id: 'alice', name: 'Alice', type: 'user', audioMuted: false },
39
+ organizationRef: 'default',
40
+ });
41
+ const joinedAgain = await controller.syncParticipant('daily-default', {
42
+ eventType: 'participant-joined',
43
+ participant: { id: 'alice', name: 'Alice Updated', type: 'user' },
44
+ organizationRef: 'default',
45
+ });
46
+ assert.equal(joinedAgain.status.participants.current.length, 1, 'participant join must be idempotent by id');
47
+ assert.equal(joinedAgain.status.participants.total, 1);
48
+ assert.equal(joinedAgain.status.participants.peak, 1);
49
+ assert.equal(emitted.at(-1).type, 'participant-joined');
50
+
51
+ const left = await controller.syncParticipant('daily-default', {
52
+ eventType: 'participant-left',
53
+ participant: { id: 'alice', type: 'user' },
54
+ organizationRef: 'default',
55
+ });
56
+ assert.equal(left.status.participants.current.length, 0);
57
+ assert.equal(left.status.participants.total, 1);
58
+ assert.equal(left.status.participants.peak, 1);
59
+ assert.equal(emitted.at(-1).type, 'participant-left');
60
+
61
+ const recording = await controller.syncRecording('daily-default', {
62
+ eventType: 'recording-started',
63
+ recordingId: 'rec-1',
64
+ organizationRef: 'default',
65
+ providerRef: 'jitsi-prod',
66
+ meetingRef: 'daily-default',
67
+ });
68
+ assert.equal(recording.kind, 'JitsiRecording');
69
+ assert.equal(recording.status.phase, 'Recording');
70
+ assert.equal(emitted.at(-1).type, 'recording-started');
71
+
72
+ const stopped = await controller.syncRecording('daily-default', {
73
+ eventType: 'recording-stopped',
74
+ recordingId: 'rec-1',
75
+ duration: 120,
76
+ organizationRef: 'default',
77
+ });
78
+ assert.equal(stopped.status.phase, 'Completed');
79
+ assert.equal(stopped.status.duration, 120);
80
+
81
+ const ended = await controller.syncRoom('daily-default', { eventType: 'room-destroyed', organizationRef: 'default' });
82
+ assert.equal(ended.status.phase, 'Ended');
83
+ const duplicate = await controller.syncParticipant('daily-default', {
84
+ eventType: 'participant-joined',
85
+ deliveryId: 'delivery-1',
86
+ participant: { id: 'bob', name: 'Bob', type: 'user' },
87
+ organizationRef: 'default',
88
+ });
89
+ const duplicateAgain = await controller.syncParticipant('daily-default', {
90
+ eventType: 'participant-joined',
91
+ deliveryId: 'delivery-1',
92
+ participant: { id: 'charlie', name: 'Charlie', type: 'user' },
93
+ organizationRef: 'default',
94
+ });
95
+ assert.deepEqual(duplicateAgain.status.participants.current, duplicate.status.participants.current);
96
+ assert.ok(persisted.some((resource) => resource.kind === 'JitsiMeeting'));
97
+ assert.ok(persisted.some((resource) => resource.kind === 'JitsiRecording'));
98
+ });
99
+
100
+ test('Jitsi sync controller maintains monotonic org-scoped provider watermarks', async () => {
101
+ const persisted = [];
102
+ const controller = createJitsiSyncController({ persistFn: (resource) => persisted.push(resource) });
103
+
104
+ await controller.updateWatermark('jitsi-prod', '2026-05-30T12:00:00Z', { organizationRef: 'default' });
105
+ await controller.updateWatermark('jitsi-prod', '2026-05-30T11:00:00Z', { organizationRef: 'default' });
106
+ await controller.updateWatermark('jitsi-prod', '2026-05-30T12:05:00Z', { organizationRef: 'default' });
107
+
108
+ assert.equal(controller.getWatermark('jitsi-prod'), '2026-05-30T12:05:00Z');
109
+ assert.equal(persisted.filter((resource) => resource.kind === 'ExternalSyncState').length, 2);
110
+ assert.equal(persisted.at(-1).metadata.namespace, 'kradle-org-default');
111
+ assert.equal(persisted.at(-1).spec.organizationRef, 'default');
112
+ });