@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,531 @@
1
+ // Async kubectl helpers for parallel snapshot fetching.
2
+ //
3
+ // Exports:
4
+ // - runKubectlAsync -- Promise-based kubectl wrapper using child_process.spawn
5
+ // - getControllerSnapshotAsync -- parallel version of getControllerSnapshot
6
+ // - getPartialSnapshot -- query only a subset of resource kinds
7
+ // - watchResourceChanges -- lightweight watch that invalidates the snapshot cache
8
+
9
+ import { spawn } from 'node:child_process';
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { randomUUID } from 'node:crypto';
12
+ import {
13
+ KRADLE_API_GROUP,
14
+ KRADLE_API_VERSIONED_GROUP,
15
+ KUBEVELA_API_GROUP,
16
+ KYVERNO_API_GROUP,
17
+ KYVERNO_POLICIES_API_GROUP,
18
+ POLICY_REPORT_API_GROUP,
19
+ KRADLE_RESOURCES,
20
+ KRADLE_PLATFORM_NAMESPACE,
21
+ findResourceDefinition,
22
+ apiResourceName,
23
+ orgNamespaceName
24
+ } from './kubernetes-controller.js';
25
+ import { clearSnapshotCache, setOrgCache } from './snapshot-cache.js';
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // In-cluster kubectl args (same logic as the sync version, but using static
29
+ // node:fs imports instead of a lazy require helper).
30
+ // ---------------------------------------------------------------------------
31
+
32
+ function inClusterArgs(env) {
33
+ if (String(env.KRADLE_DISABLE_IN_CLUSTER_KUBECTL || '').toLowerCase() === 'true') return null;
34
+ if (env.KUBECONFIG) return null;
35
+ const host = env.KUBERNETES_SERVICE_HOST;
36
+ const port = env.KUBERNETES_SERVICE_PORT || '443';
37
+ if (!host) return null;
38
+ const serviceAccountDir = env.KRADLE_SERVICE_ACCOUNT_DIR || '/var/run/secrets/kubernetes.io/serviceaccount';
39
+ const tokenPath = env.KRADLE_SERVICE_ACCOUNT_TOKEN || `${serviceAccountDir}/token`;
40
+ const caPath = env.KRADLE_SERVICE_ACCOUNT_CA || `${serviceAccountDir}/ca.crt`;
41
+ if (!existsSync(tokenPath) || !existsSync(caPath)) return null;
42
+ const token = readFileSync(tokenPath, 'utf8').trim();
43
+ if (!token) return null;
44
+ return [
45
+ `--server=https://${host}:${port}`,
46
+ `--certificate-authority=${caPath}`,
47
+ `--token=${token}`
48
+ ];
49
+ }
50
+
51
+ function kubectlArgs(args, env) {
52
+ const extra = inClusterArgs(env);
53
+ return extra ? [...extra, ...args] : args;
54
+ }
55
+
56
+ function currentContextResult(env) {
57
+ if (!inClusterArgs(env)) return null;
58
+ return {
59
+ ok: true,
60
+ status: 0,
61
+ signal: null,
62
+ stdout: 'in-cluster\n',
63
+ stderr: '',
64
+ error: null,
65
+ command: 'kubectl config current-context'
66
+ };
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Low-level async kubectl runner
71
+ // ---------------------------------------------------------------------------
72
+
73
+ /**
74
+ * Run a kubectl command asynchronously using child_process.spawn.
75
+ * Returns a Promise that resolves to the same shape as runKubectl (sync).
76
+ *
77
+ * @param {string[]} args
78
+ * @param {object} [options]
79
+ * @param {string} [options.kubectl]
80
+ * @param {number} [options.timeoutMs]
81
+ * @param {object} [options.env]
82
+ * @param {string} [options.input]
83
+ * @param {boolean} [options.allowFailure]
84
+ * @returns {Promise<{ok: boolean, status: number|null, signal: string|null, stdout: string, stderr: string, error: string|null, command: string}>}
85
+ */
86
+ export function runKubectlAsync(args, options = {}) {
87
+ const kubectl = options.kubectl || process.env.KRADLE_KUBECTL || 'kubectl';
88
+ const timeoutMs = Number(options.timeoutMs || process.env.KRADLE_KUBECTL_TIMEOUT_MS || 3_000);
89
+ const env = options.env || process.env;
90
+ const command = `kubectl ${args.join(' ')}`;
91
+ const effectiveArgs = kubectlArgs(args, env);
92
+
93
+ return new Promise((resolve, reject) => {
94
+ let stdout = '';
95
+ let stderr = '';
96
+ let settled = false;
97
+
98
+ const child = spawn(kubectl, effectiveArgs, {
99
+ env,
100
+ windowsHide: true,
101
+ stdio: ['pipe', 'pipe', 'pipe']
102
+ });
103
+
104
+ if (options.input) {
105
+ child.stdin.write(options.input);
106
+ }
107
+ child.stdin.end();
108
+
109
+ child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
110
+ child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
111
+
112
+ const timer = setTimeout(() => {
113
+ if (settled) return;
114
+ settled = true;
115
+ try { child.kill('SIGTERM'); } catch { /* ignore */ }
116
+ const result = {
117
+ ok: false, status: null, signal: 'SIGTERM', stdout, stderr,
118
+ error: `kubectl timed out after ${timeoutMs}ms`, command
119
+ };
120
+ if (options.allowFailure) resolve(result);
121
+ else reject(new Error(result.error));
122
+ }, timeoutMs);
123
+
124
+ child.on('error', (error) => {
125
+ if (settled) return;
126
+ settled = true;
127
+ clearTimeout(timer);
128
+ const result = { ok: false, status: null, signal: null, stdout, stderr, error: error.message, command };
129
+ if (options.allowFailure) resolve(result);
130
+ else reject(error);
131
+ });
132
+
133
+ child.on('close', (code, signal) => {
134
+ if (settled) return;
135
+ settled = true;
136
+ clearTimeout(timer);
137
+ const ok = code === 0 && !signal;
138
+ const result = { ok, status: code, signal: signal || null, stdout, stderr, error: null, command };
139
+ if (!ok && !options.allowFailure) {
140
+ const detail = (stderr || stdout || '').trim();
141
+ reject(new Error(`${command}: ${detail || `exit ${code ?? 'unknown'}`}`));
142
+ } else {
143
+ resolve(result);
144
+ }
145
+ });
146
+ });
147
+ }
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // Parallel snapshot
151
+ // ---------------------------------------------------------------------------
152
+
153
+ /**
154
+ * Fetch the full controller snapshot with all resource kinds queried in
155
+ * parallel (Promise.all) instead of sequentially with spawnSync.
156
+ *
157
+ * Falls back to the synchronous getControllerSnapshot on any unexpected error.
158
+ *
159
+ * @param {object} [options]
160
+ * @returns {Promise<object>}
161
+ */
162
+ export async function getControllerSnapshotAsync(options = {}) {
163
+ const kubectl = options.kubectl || process.env.KRADLE_KUBECTL || 'kubectl';
164
+ const namespace = options.namespace || process.env.KRADLE_NAMESPACE || 'kradle-system';
165
+ const timeoutMs = Number(options.timeoutMs || process.env.KRADLE_KUBECTL_TIMEOUT_MS || 3_000);
166
+ const env = { ...process.env, ...(options.env || {}) };
167
+ const correlationId = randomUUID();
168
+
169
+ try {
170
+ // Phase 1: context + version in parallel
171
+ const inClusterContext = currentContextResult(env);
172
+ const [contextResult, versionResult] = await Promise.all([
173
+ inClusterContext || runKubectlAsync(['config', 'current-context'], { kubectl, timeoutMs, env, allowFailure: true }),
174
+ runKubectlAsync(['version', '--client=true', '-o', 'json'], { kubectl, timeoutMs, env, allowFailure: true })
175
+ ]);
176
+
177
+ if (!contextResult.ok || !versionResult.ok) {
178
+ return buildEmptySnapshot({ correlationId, namespace, kubectl, contextResult, versionResult, available: false });
179
+ }
180
+
181
+ // Phase 2: API service + CRD discovery in parallel
182
+ const [apiServiceResult, crdResult] = await Promise.all([
183
+ runKubectlAsync(['get', 'apiservice', KRADLE_API_VERSIONED_GROUP, '-o', 'json'], { kubectl, timeoutMs, env, allowFailure: true }),
184
+ runKubectlAsync(['get', 'crd', '-o', 'json'], { kubectl, timeoutMs, env, allowFailure: true })
185
+ ]);
186
+
187
+ if (!apiServiceResult.ok && !crdResult.ok) {
188
+ return buildEmptySnapshot({ correlationId, namespace, kubectl, contextResult, versionResult, available: true });
189
+ }
190
+
191
+ const KYVERNO_DISCOVERY_GROUPS = new Set([KYVERNO_API_GROUP, KYVERNO_POLICIES_API_GROUP, POLICY_REPORT_API_GROUP]);
192
+ const discoveredCrds = crdResult.ok
193
+ ? parseKubernetesList(crdResult.stdout).items.filter((crd) =>
194
+ [KRADLE_API_GROUP, KUBEVELA_API_GROUP].includes(crd.spec?.group) ||
195
+ KYVERNO_DISCOVERY_GROUPS.has(crd.spec?.group))
196
+ : [];
197
+ const discoveredPluralSet = new Set(
198
+ discoveredCrds.map((crd) => `${crd.spec?.group || KRADLE_API_GROUP}/${crd.spec?.names?.plural}`).filter(Boolean)
199
+ );
200
+
201
+ if (!apiServiceResult.ok && discoveredCrds.length === 0) {
202
+ return buildEmptySnapshot({ correlationId, namespace, kubectl, contextResult, versionResult, available: true, apiServiceResult });
203
+ }
204
+
205
+ // Phase 3: list all resources in parallel
206
+ const snapshotResources = KRADLE_RESOURCES.filter((d) => d.storage !== 'core');
207
+ const resources = Object.fromEntries(snapshotResources.map((d) => [d.kind, []]));
208
+
209
+ const platformScopedDefs = snapshotResources.filter((d) => d.platformScoped);
210
+ const orgScopedDefs = snapshotResources.filter((d) => !d.platformScoped);
211
+
212
+ // Fetch platform-scoped resources first so we can derive org namespaces
213
+ const platformResults = await Promise.all(
214
+ platformScopedDefs
215
+ .filter((d) => shouldListSnapshotDefinition(d, discoveredPluralSet))
216
+ .map(async (definition) => {
217
+ const resourceNamespace = definition.namespace || namespace;
218
+ const result = await runKubectlAsync(
219
+ ['get', apiResourceName(definition), ...namespaceArgs(definition, resourceNamespace), '-o', 'json', '--ignore-not-found'],
220
+ { kubectl, timeoutMs, env, allowFailure: true }
221
+ );
222
+ return { definition, result };
223
+ })
224
+ );
225
+
226
+ for (const { definition, result } of platformResults) {
227
+ resources[definition.kind] = result.ok ? parseKubernetesList(result.stdout).items : [];
228
+ }
229
+
230
+ const orgNamespaces = resolveOrgNamespaces(resources.Organization, resources.OrgNamespaceBinding, namespace);
231
+
232
+ // Fetch org-scoped resources in parallel
233
+ const orgResults = await Promise.all(
234
+ orgScopedDefs
235
+ .filter((d) => shouldListSnapshotDefinition(d, discoveredPluralSet))
236
+ .map(async (definition) => {
237
+ const namespaces = definition.namespaced === false
238
+ ? [null]
239
+ : [definition.namespace || null].filter(Boolean).concat(definition.namespace ? [] : orgNamespaces);
240
+
241
+ const itemArrays = await Promise.all(
242
+ namespaces.map(async (resourceNamespace) => {
243
+ const effectiveNamespace = resourceNamespace || namespace;
244
+ const result = await runKubectlAsync(
245
+ ['get', apiResourceName(definition), ...namespaceArgs(definition, effectiveNamespace), '-o', 'json', '--ignore-not-found'],
246
+ { kubectl, timeoutMs, env, allowFailure: true }
247
+ );
248
+ return result.ok ? parseKubernetesList(result.stdout).items : [];
249
+ })
250
+ );
251
+ return { definition, items: itemArrays.flat() };
252
+ })
253
+ );
254
+
255
+ for (const { definition, items } of orgResults) {
256
+ resources[definition.kind] = items;
257
+ }
258
+
259
+ const eventsResult = await runKubectlAsync(
260
+ ['get', 'events', '-n', namespace, '-o', 'json', '--ignore-not-found'],
261
+ { kubectl, timeoutMs, env, allowFailure: true }
262
+ );
263
+
264
+ return {
265
+ source: 'kubernetes',
266
+ mode: 'kubernetes-api',
267
+ namespace,
268
+ generatedAt: new Date().toISOString(),
269
+ correlationId,
270
+ kubectl: {
271
+ binary: kubectl,
272
+ context: contextResult.stdout.trim(),
273
+ clientVersion: safeJson(versionResult.stdout)?.clientVersion?.gitVersion || null,
274
+ available: true,
275
+ errors: []
276
+ },
277
+ apiService: apiServiceResult.ok ? safeJson(apiServiceResult.stdout) : null,
278
+ crds: discoveredCrds,
279
+ resources,
280
+ kyverno: emptyKyverno(),
281
+ events: eventsResult.ok ? parseKubernetesList(eventsResult.stdout).items : [],
282
+ permissions: [],
283
+ storage: storageBoundaries(),
284
+ commands: []
285
+ };
286
+ } catch (error) {
287
+ // Fallback to synchronous snapshot
288
+ const { getControllerSnapshot } = await import('./kubernetes-controller.js');
289
+ return getControllerSnapshot(options);
290
+ }
291
+ }
292
+
293
+ // ---------------------------------------------------------------------------
294
+ // Partial snapshot
295
+ // ---------------------------------------------------------------------------
296
+
297
+ /**
298
+ * Fetch only the requested resource kinds from Kubernetes.
299
+ * Useful for pages that only need a subset (e.g. only AgentStack, AgentSession).
300
+ *
301
+ * @param {string[]} kinds - Array of kind names or plurals to query
302
+ * @param {object} [options]
303
+ * @returns {Promise<object>} Snapshot with only the requested resource keys populated
304
+ */
305
+ export async function getPartialSnapshot(kinds = [], options = {}) {
306
+ if (!Array.isArray(kinds) || kinds.length === 0) {
307
+ return { source: 'kubernetes', mode: 'partial', resources: {}, generatedAt: new Date().toISOString() };
308
+ }
309
+
310
+ const kubectl = options.kubectl || process.env.KRADLE_KUBECTL || 'kubectl';
311
+ const namespace = options.namespace || process.env.KRADLE_NAMESPACE || 'kradle-system';
312
+ const timeoutMs = Number(options.timeoutMs || process.env.KRADLE_KUBECTL_TIMEOUT_MS || 3_000);
313
+ const env = { ...process.env, ...(options.env || {}) };
314
+
315
+ // Resolve requested kinds to definition objects (ignore unknown)
316
+ const requestedDefs = kinds
317
+ .map((k) => { try { return findResourceDefinition(k); } catch { return null; } })
318
+ .filter(Boolean);
319
+
320
+ const resources = Object.fromEntries(requestedDefs.map((d) => [d.kind, []]));
321
+
322
+ // Determine org namespaces only when necessary
323
+ const needsOrgNs = requestedDefs.some((d) => !d.platformScoped && d.namespaced !== false && !d.namespace);
324
+ let orgNamespaces = [namespace];
325
+
326
+ if (needsOrgNs) {
327
+ try {
328
+ const orgDef = findResourceDefinition('Organization');
329
+ const bindingDef = findResourceDefinition('OrgNamespaceBinding');
330
+ const [orgResult, bindingResult] = await Promise.all([
331
+ runKubectlAsync(['get', apiResourceName(orgDef), '-n', namespace, '-o', 'json', '--ignore-not-found'], { kubectl, timeoutMs, env, allowFailure: true }),
332
+ runKubectlAsync(['get', apiResourceName(bindingDef), '-n', namespace, '-o', 'json', '--ignore-not-found'], { kubectl, timeoutMs, env, allowFailure: true })
333
+ ]);
334
+ orgNamespaces = resolveOrgNamespaces(
335
+ orgResult.ok ? parseKubernetesList(orgResult.stdout).items : [],
336
+ bindingResult.ok ? parseKubernetesList(bindingResult.stdout).items : [],
337
+ namespace
338
+ );
339
+ } catch {
340
+ orgNamespaces = [namespace];
341
+ }
342
+ }
343
+
344
+ // Fetch all requested definitions in parallel
345
+ await Promise.all(requestedDefs.map(async (definition) => {
346
+ const namespaces = definition.namespaced === false
347
+ ? [null]
348
+ : definition.platformScoped
349
+ ? [definition.namespace || namespace]
350
+ : [definition.namespace || null].filter(Boolean).concat(definition.namespace ? [] : orgNamespaces);
351
+
352
+ const itemArrays = await Promise.all(
353
+ namespaces.map(async (resourceNamespace) => {
354
+ const effectiveNamespace = resourceNamespace || namespace;
355
+ const result = await runKubectlAsync(
356
+ ['get', apiResourceName(definition), ...namespaceArgs(definition, effectiveNamespace), '-o', 'json', '--ignore-not-found'],
357
+ { kubectl, timeoutMs, env, allowFailure: true }
358
+ );
359
+ return result.ok ? parseKubernetesList(result.stdout).items : [];
360
+ })
361
+ );
362
+ resources[definition.kind] = itemArrays.flat();
363
+ }));
364
+
365
+ return {
366
+ source: 'kubernetes',
367
+ mode: 'partial',
368
+ namespace,
369
+ generatedAt: new Date().toISOString(),
370
+ resources
371
+ };
372
+ }
373
+
374
+ // ---------------------------------------------------------------------------
375
+ // Watch resource changes
376
+ // ---------------------------------------------------------------------------
377
+
378
+ /**
379
+ * Watch key resource kinds with `kubectl get --watch -o json`.
380
+ * On any change event, the snapshot cache is cleared so the next read
381
+ * triggers a fresh fetch.
382
+ *
383
+ * @param {Function} callback - Called with (kind, item) on each watch event
384
+ * @param {object} [options]
385
+ * @param {string[]} [options.kinds] - Defaults to ['Organization', 'AgentStack', 'AgentSession']
386
+ * @returns {{ stop: () => void }} Cleanup handle
387
+ */
388
+ export function watchResourceChanges(callback, options = {}) {
389
+ const kubectl = options.kubectl || process.env.KRADLE_KUBECTL || 'kubectl';
390
+ const namespace = options.namespace || process.env.KRADLE_NAMESPACE || 'kradle-system';
391
+ const env = { ...process.env, ...(options.env || {}) };
392
+ const watchKinds = options.kinds || ['Organization', 'AgentStack', 'AgentSession'];
393
+
394
+ const children = [];
395
+
396
+ for (const kind of watchKinds) {
397
+ let definition;
398
+ try { definition = findResourceDefinition(kind); } catch { continue; }
399
+
400
+ const resourceNamespace = definition.namespace || namespace;
401
+ const args = [
402
+ 'get', apiResourceName(definition),
403
+ ...namespaceArgs(definition, resourceNamespace),
404
+ '--watch', '-o', 'json'
405
+ ];
406
+
407
+ const child = spawn(kubectl, kubectlArgs(args, env), {
408
+ env,
409
+ windowsHide: true,
410
+ stdio: ['ignore', 'pipe', 'pipe']
411
+ });
412
+
413
+ let buffer = '';
414
+ child.stdout.on('data', (chunk) => {
415
+ buffer += chunk.toString();
416
+ let newlineIdx;
417
+ while ((newlineIdx = buffer.indexOf('\n')) !== -1) {
418
+ const line = buffer.slice(0, newlineIdx).trim();
419
+ buffer = buffer.slice(newlineIdx + 1);
420
+ if (!line) continue;
421
+ const item = safeJson(line);
422
+ if (item) {
423
+ // Invalidate the snapshot cache so the next read fetches fresh data
424
+ clearSnapshotCache();
425
+ try { callback(kind, item); } catch { /* user callback errors are non-fatal */ }
426
+ }
427
+ }
428
+ });
429
+
430
+ children.push(child);
431
+ }
432
+
433
+ return {
434
+ stop() {
435
+ for (const child of children) {
436
+ try { child.kill('SIGTERM'); } catch { /* ignore */ }
437
+ }
438
+ children.length = 0;
439
+ }
440
+ };
441
+ }
442
+
443
+ // ---------------------------------------------------------------------------
444
+ // Private helpers
445
+ // ---------------------------------------------------------------------------
446
+
447
+ function namespaceArgs(definition, namespace) {
448
+ return definition.namespaced === false ? [] : ['-n', namespace];
449
+ }
450
+
451
+ function shouldListSnapshotDefinition(definition, discoveredPluralSet) {
452
+ const group = definition.group || KRADLE_API_GROUP;
453
+ if (discoveredPluralSet.has(`${group}/${definition.plural}`)) return true;
454
+ return group === KRADLE_API_GROUP;
455
+ }
456
+
457
+ function parseKubernetesList(stdout) {
458
+ const parsed = safeJson(stdout);
459
+ if (!parsed) return { items: [] };
460
+ if (Array.isArray(parsed.items)) return parsed;
461
+ if (parsed.kind && parsed.metadata) return { items: [parsed] };
462
+ return { items: [] };
463
+ }
464
+
465
+ function safeJson(text) {
466
+ try { return text ? JSON.parse(text) : null; }
467
+ catch { return null; }
468
+ }
469
+
470
+ function resolveOrgNamespaces(organizations = [], bindings = [], fallbackNamespace = KRADLE_PLATFORM_NAMESPACE) {
471
+ const namespaces = [...new Set([
472
+ ...organizations.map((o) => o.spec?.namespaceName || o.metadata?.labels?.['kradle.a5c.ai/namespace']).filter(Boolean),
473
+ ...bindings.map((b) => b.spec?.namespace || b.metadata?.labels?.['kradle.a5c.ai/namespace']).filter(Boolean)
474
+ ])];
475
+ if (namespaces.length) return namespaces;
476
+ const fallbackOrgs = new Set();
477
+ const adminOrg = process.env.KRADLE_ADMIN_ORG;
478
+ const defaultOrg = process.env.KRADLE_ORG || 'default';
479
+ if (adminOrg) fallbackOrgs.add(orgNamespaceName(adminOrg));
480
+ fallbackOrgs.add(orgNamespaceName(defaultOrg));
481
+ return fallbackOrgs.size ? [...fallbackOrgs] : [fallbackNamespace];
482
+ }
483
+
484
+ function storageBoundaries() {
485
+ return {
486
+ etcd: 'Kradle CRDs: Organization, Repository, SSHKey, RepositoryPermission, BranchProtection, RefPolicy, WebhookSubscription, RunnerPool, View, Selector',
487
+ kubevela: 'Kradle deployment CRDs: Application, ApplicationRevision, ComponentDefinition, WorkloadDefinition, TraitDefinition, ScopeDefinition, PolicyDefinition, Policy, WorkflowStepDefinition, Workflow, ResourceTracker',
488
+ postgres: 'Aggregated API resources: PullRequest, Issue, Review, Pipeline, Job, WebhookDelivery',
489
+ repositories: 'Repository backend Deployment, repository storage, and integration plans',
490
+ objects: 'Object storage referenced by Repository specs and Pipeline artifacts'
491
+ };
492
+ }
493
+
494
+ function emptyKyverno() {
495
+ return {
496
+ mode: 'auto',
497
+ detected: false,
498
+ resources: {},
499
+ reports: { policyReports: [], clusterPolicyReports: [], results: [], violations: [] },
500
+ permissions: [],
501
+ degraded: [],
502
+ controllers: []
503
+ };
504
+ }
505
+
506
+ function buildEmptySnapshot({ correlationId, namespace, kubectl, contextResult, versionResult, available, apiServiceResult }) {
507
+ return {
508
+ source: 'kubernetes',
509
+ mode: 'kubernetes-api',
510
+ namespace,
511
+ generatedAt: new Date().toISOString(),
512
+ correlationId,
513
+ kubectl: {
514
+ binary: kubectl,
515
+ context: contextResult?.ok ? contextResult.stdout.trim() : null,
516
+ clientVersion: versionResult?.ok ? safeJson(versionResult.stdout)?.clientVersion?.gitVersion || null : null,
517
+ available: Boolean(available),
518
+ errors: [contextResult, versionResult, apiServiceResult]
519
+ .filter((r) => r && !r.ok)
520
+ .map((r) => `${r.command}: ${(r.stderr || r.error || '').trim() || `exit ${r.status}`}`)
521
+ },
522
+ apiService: null,
523
+ crds: [],
524
+ resources: Object.fromEntries(KRADLE_RESOURCES.filter((d) => d.storage !== 'core').map((d) => [d.kind, []])),
525
+ kyverno: emptyKyverno(),
526
+ events: [],
527
+ permissions: [],
528
+ storage: storageBoundaries(),
529
+ commands: []
530
+ };
531
+ }