@a5c-ai/krate 5.0.1-staging.04a3db697

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 (246) hide show
  1. package/Dockerfile +31 -0
  2. package/README.md +183 -0
  3. package/bin/krate-demo.mjs +23 -0
  4. package/bin/krate-server.mjs +14 -0
  5. package/dist/krate-controller-ui.json +3067 -0
  6. package/dist/krate-lifecycle.json +201 -0
  7. package/dist/krate-runtime-snapshot.json +2955 -0
  8. package/dist/krate-summary.json +722 -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-krate-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/components/control-plane.md +78 -0
  60. package/docs/components/data-plane.md +69 -0
  61. package/docs/components/hooks-events.md +67 -0
  62. package/docs/components/identity-rbac-policy.md +73 -0
  63. package/docs/components/kubevela-oam.md +70 -0
  64. package/docs/components/operations-publishing.md +81 -0
  65. package/docs/components/runners-ci.md +66 -0
  66. package/docs/components/web-ui.md +94 -0
  67. package/docs/external/README.md +47 -0
  68. package/docs/external/bidirectional-sync-design.md +134 -0
  69. package/docs/external/cicd-interface.md +64 -0
  70. package/docs/external/external-backend-controllers.md +170 -0
  71. package/docs/external/external-backend-crds.md +234 -0
  72. package/docs/external/external-backend-ui-spec.md +151 -0
  73. package/docs/external/external-backend-ux-flows.md +115 -0
  74. package/docs/external/external-object-mapping.md +125 -0
  75. package/docs/external/git-forge-interface.md +68 -0
  76. package/docs/external/github-integration-design.md +151 -0
  77. package/docs/external/issue-tracking-interface.md +66 -0
  78. package/docs/external/provider-capability-manifests.md +204 -0
  79. package/docs/external/provider-catalog.md +139 -0
  80. package/docs/external/provider-rollout-testing.md +78 -0
  81. package/docs/external/research-results.md +48 -0
  82. package/docs/external/security-auth-permissions.md +81 -0
  83. package/docs/external/sync-state-machines.md +108 -0
  84. package/docs/external/unified-external-backend-model.md +107 -0
  85. package/docs/external/user-facing-changes.md +67 -0
  86. package/docs/gaps.md +161 -0
  87. package/docs/install.md +94 -0
  88. package/docs/krate-design.md +334 -0
  89. package/docs/local-minikube.md +55 -0
  90. package/docs/ontology/README.md +32 -0
  91. package/docs/ontology/bounded-contexts.md +29 -0
  92. package/docs/ontology/events-and-hooks.md +32 -0
  93. package/docs/ontology/oam-kubevela.md +32 -0
  94. package/docs/ontology/operations-and-release.md +25 -0
  95. package/docs/ontology/personas-and-actors.md +32 -0
  96. package/docs/ontology/policies-and-invariants.md +33 -0
  97. package/docs/ontology/problem-space.md +30 -0
  98. package/docs/ontology/resource-contracts.md +40 -0
  99. package/docs/ontology/resource-taxonomy.md +42 -0
  100. package/docs/ontology/runners-and-ci.md +29 -0
  101. package/docs/ontology/solution-space.md +24 -0
  102. package/docs/ontology/storage-and-data-boundaries.md +29 -0
  103. package/docs/ontology/validation-matrix.md +24 -0
  104. package/docs/ontology/web-ui-excellent-flows.md +32 -0
  105. package/docs/ontology/workflows.md +39 -0
  106. package/docs/ontology/world.md +35 -0
  107. package/docs/product-requirements.md +62 -0
  108. package/docs/roadmap-mvp.md +87 -0
  109. package/docs/system-requirements.md +90 -0
  110. package/docs/tests/README.md +53 -0
  111. package/docs/tests/agent-qa-plan.md +63 -0
  112. package/docs/tests/browser-ui-tests.md +62 -0
  113. package/docs/tests/ci-quality-gates.md +48 -0
  114. package/docs/tests/coverage-model.md +64 -0
  115. package/docs/tests/e2e-scenario-tests.md +53 -0
  116. package/docs/tests/fixtures-test-data.md +63 -0
  117. package/docs/tests/observability-reliability-tests.md +54 -0
  118. package/docs/tests/product-test-matrix.md +145 -0
  119. package/docs/tests/qa-adoption-roadmap.md +130 -0
  120. package/docs/tests/qa-automation-plan.md +101 -0
  121. package/docs/tests/security-compliance-tests.md +57 -0
  122. package/docs/tests/test-framework-tools.md +88 -0
  123. package/docs/tests/test-suite-layout.md +121 -0
  124. package/docs/tests/unit-integration-tests.md +48 -0
  125. package/docs/todo-kyverno +714 -0
  126. package/docs/todos.md +4 -0
  127. package/docs/user-stories.md +78 -0
  128. package/examples/minikube-demo.yaml +190 -0
  129. package/examples/oam-application.yaml +23 -0
  130. package/examples/policy-kyverno-pr-title.yaml +18 -0
  131. package/package.json +63 -0
  132. package/scripts/build.mjs +29 -0
  133. package/scripts/setup-minikube.mjs +65 -0
  134. package/scripts/smoke.mjs +37 -0
  135. package/scripts/validate-doc-coverage.mjs +152 -0
  136. package/scripts/validate-package.mjs +93 -0
  137. package/scripts/validate-ui.mjs +236 -0
  138. package/src/agent-adapter-controller.js +169 -0
  139. package/src/agent-approval-controller.js +170 -0
  140. package/src/agent-context-bundles.js +242 -0
  141. package/src/agent-dispatch-controller.js +209 -0
  142. package/src/agent-gateway-config-controller.js +147 -0
  143. package/src/agent-memory-controller.js +357 -0
  144. package/src/agent-memory-import.js +327 -0
  145. package/src/agent-memory-query.js +292 -0
  146. package/src/agent-memory-repository-source-controller.js +255 -0
  147. package/src/agent-mux-client.js +280 -0
  148. package/src/agent-permission-review.js +250 -0
  149. package/src/agent-project-controller.js +117 -0
  150. package/src/agent-provider-config-controller.js +150 -0
  151. package/src/agent-secret-config-grant-controller.js +282 -0
  152. package/src/agent-session-transcript-controller.js +189 -0
  153. package/src/agent-stack-controller.js +347 -0
  154. package/src/agent-subagent-controller.js +160 -0
  155. package/src/agent-transport-binding-controller.js +121 -0
  156. package/src/agent-trigger-controller.js +321 -0
  157. package/src/agent-workspace-controller.js +447 -0
  158. package/src/agent-writeback-controller.js +302 -0
  159. package/src/api-controller.js +541 -0
  160. package/src/argocd-gitops.js +43 -0
  161. package/src/async-controller.js +207 -0
  162. package/src/audit-controller.js +191 -0
  163. package/src/auth.js +307 -0
  164. package/src/component-catalog.js +41 -0
  165. package/src/control-plane.js +136 -0
  166. package/src/controller-client.js +50 -0
  167. package/src/controller-ui.js +551 -0
  168. package/src/data-plane.js +178 -0
  169. package/src/event-bus.js +61 -0
  170. package/src/external/conflict-controller.js +225 -0
  171. package/src/external/github/auth.js +96 -0
  172. package/src/external/github/cicd.js +180 -0
  173. package/src/external/github/git-forge.js +240 -0
  174. package/src/external/github/index.js +144 -0
  175. package/src/external/github/issue-tracking.js +163 -0
  176. package/src/external/provider-adapter.js +161 -0
  177. package/src/external/provider-resource-factory.js +161 -0
  178. package/src/external/sync-controller.js +235 -0
  179. package/src/external/webhook-controller.js +144 -0
  180. package/src/external/write-controller.js +283 -0
  181. package/src/gitea-backend.js +95 -0
  182. package/src/gitea-service.js +173 -0
  183. package/src/handoff.js +98 -0
  184. package/src/hooks-events.js +63 -0
  185. package/src/http-server.js +377 -0
  186. package/src/identity-policy.js +86 -0
  187. package/src/index.js +55 -0
  188. package/src/kubernetes-controller-async.js +511 -0
  189. package/src/kubernetes-controller.js +878 -0
  190. package/src/kubernetes-resource-gateway.js +48 -0
  191. package/src/operations.js +112 -0
  192. package/src/org-scoping.js +5 -0
  193. package/src/resource-model.js +221 -0
  194. package/src/runners-ci.js +48 -0
  195. package/src/runtime.js +196 -0
  196. package/src/snapshot-cache.js +157 -0
  197. package/src/web-ui.js +40 -0
  198. package/tests/agent-adapter-controller.test.js +361 -0
  199. package/tests/agent-approval-controller.test.js +173 -0
  200. package/tests/agent-context-bundles.test.js +278 -0
  201. package/tests/agent-dispatch-controller.test.js +315 -0
  202. package/tests/agent-gateway-config-controller.test.js +386 -0
  203. package/tests/agent-memory-controller.test.js +308 -0
  204. package/tests/agent-memory-import-snapshot.test.js +477 -0
  205. package/tests/agent-memory-query.test.js +404 -0
  206. package/tests/agent-memory-repository-source.test.js +514 -0
  207. package/tests/agent-mux-client.test.js +204 -0
  208. package/tests/agent-permission-review-v2.test.js +317 -0
  209. package/tests/agent-permission-review.test.js +209 -0
  210. package/tests/agent-project-controller.test.js +302 -0
  211. package/tests/agent-provider-config-controller.test.js +376 -0
  212. package/tests/agent-resources.test.js +228 -0
  213. package/tests/agent-secret-config-grant.test.js +231 -0
  214. package/tests/agent-session-transcript-controller.test.js +499 -0
  215. package/tests/agent-stack-controller.test.js +221 -0
  216. package/tests/agent-subagent-controller.test.js +201 -0
  217. package/tests/agent-transport-binding-controller.test.js +294 -0
  218. package/tests/agent-trigger-controller.test.js +211 -0
  219. package/tests/agent-trigger-routes.test.js +190 -0
  220. package/tests/agent-trigger-sources.test.js +245 -0
  221. package/tests/agent-workspace-controller.test.js +181 -0
  222. package/tests/agent-writeback.test.js +292 -0
  223. package/tests/approval-persistence.test.js +171 -0
  224. package/tests/async-controller.test.js +252 -0
  225. package/tests/audit-controller.test.js +227 -0
  226. package/tests/deployment.test.js +396 -0
  227. package/tests/e2e/lifecycle.test.js +117 -0
  228. package/tests/external-github-forge.test.js +560 -0
  229. package/tests/external-github-issues-cicd.test.js +520 -0
  230. package/tests/external-integration.test.js +470 -0
  231. package/tests/external-persistence.test.js +340 -0
  232. package/tests/external-provider-adapter.test.js +365 -0
  233. package/tests/external-resource-model.test.js +215 -0
  234. package/tests/external-webhook-sync.test.js +287 -0
  235. package/tests/external-write-conflict.test.js +353 -0
  236. package/tests/gitea-service.test.js +253 -0
  237. package/tests/health-check-real.test.js +165 -0
  238. package/tests/integration/full-flow.test.js +266 -0
  239. package/tests/krate.test.js +727 -0
  240. package/tests/memory-search-wiring.test.js +270 -0
  241. package/tests/org-scoping.test.js +687 -0
  242. package/tests/session-cookie-hmac.test.js +151 -0
  243. package/tests/snapshot-performance.test.js +247 -0
  244. package/tests/sse-events.test.js +107 -0
  245. package/tests/workspace-volumes.test.js +312 -0
  246. package/tests/writeback-persistence.test.js +207 -0
@@ -0,0 +1,511 @@
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
+ KRATE_API_GROUP,
14
+ KRATE_API_VERSIONED_GROUP,
15
+ KUBEVELA_API_GROUP,
16
+ KYVERNO_API_GROUP,
17
+ KYVERNO_POLICIES_API_GROUP,
18
+ POLICY_REPORT_API_GROUP,
19
+ KRATE_RESOURCES,
20
+ KRATE_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.KRATE_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.KRATE_SERVICE_ACCOUNT_DIR || '/var/run/secrets/kubernetes.io/serviceaccount';
39
+ const tokenPath = env.KRATE_SERVICE_ACCOUNT_TOKEN || `${serviceAccountDir}/token`;
40
+ const caPath = env.KRATE_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
+ // ---------------------------------------------------------------------------
57
+ // Low-level async kubectl runner
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /**
61
+ * Run a kubectl command asynchronously using child_process.spawn.
62
+ * Returns a Promise that resolves to the same shape as runKubectl (sync).
63
+ *
64
+ * @param {string[]} args
65
+ * @param {object} [options]
66
+ * @param {string} [options.kubectl]
67
+ * @param {number} [options.timeoutMs]
68
+ * @param {object} [options.env]
69
+ * @param {string} [options.input]
70
+ * @param {boolean} [options.allowFailure]
71
+ * @returns {Promise<{ok: boolean, status: number|null, signal: string|null, stdout: string, stderr: string, error: string|null, command: string}>}
72
+ */
73
+ export function runKubectlAsync(args, options = {}) {
74
+ const kubectl = options.kubectl || process.env.KRATE_KUBECTL || 'kubectl';
75
+ const timeoutMs = Number(options.timeoutMs || process.env.KRATE_KUBECTL_TIMEOUT_MS || 3_000);
76
+ const env = options.env || process.env;
77
+ const command = `kubectl ${args.join(' ')}`;
78
+ const effectiveArgs = kubectlArgs(args, env);
79
+
80
+ return new Promise((resolve, reject) => {
81
+ let stdout = '';
82
+ let stderr = '';
83
+ let settled = false;
84
+
85
+ const child = spawn(kubectl, effectiveArgs, {
86
+ env,
87
+ windowsHide: true,
88
+ stdio: ['pipe', 'pipe', 'pipe']
89
+ });
90
+
91
+ if (options.input) {
92
+ child.stdin.write(options.input);
93
+ }
94
+ child.stdin.end();
95
+
96
+ child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
97
+ child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
98
+
99
+ const timer = setTimeout(() => {
100
+ if (settled) return;
101
+ settled = true;
102
+ try { child.kill('SIGTERM'); } catch { /* ignore */ }
103
+ const result = {
104
+ ok: false, status: null, signal: 'SIGTERM', stdout, stderr,
105
+ error: `kubectl timed out after ${timeoutMs}ms`, command
106
+ };
107
+ if (options.allowFailure) resolve(result);
108
+ else reject(new Error(result.error));
109
+ }, timeoutMs);
110
+
111
+ child.on('error', (error) => {
112
+ if (settled) return;
113
+ settled = true;
114
+ clearTimeout(timer);
115
+ const result = { ok: false, status: null, signal: null, stdout, stderr, error: error.message, command };
116
+ if (options.allowFailure) resolve(result);
117
+ else reject(error);
118
+ });
119
+
120
+ child.on('close', (code, signal) => {
121
+ if (settled) return;
122
+ settled = true;
123
+ clearTimeout(timer);
124
+ const ok = code === 0 && !signal;
125
+ const result = { ok, status: code, signal: signal || null, stdout, stderr, error: null, command };
126
+ if (!ok && !options.allowFailure) {
127
+ const detail = (stderr || stdout || '').trim();
128
+ reject(new Error(`${command}: ${detail || `exit ${code ?? 'unknown'}`}`));
129
+ } else {
130
+ resolve(result);
131
+ }
132
+ });
133
+ });
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Parallel snapshot
138
+ // ---------------------------------------------------------------------------
139
+
140
+ /**
141
+ * Fetch the full controller snapshot with all resource kinds queried in
142
+ * parallel (Promise.all) instead of sequentially with spawnSync.
143
+ *
144
+ * Falls back to the synchronous getControllerSnapshot on any unexpected error.
145
+ *
146
+ * @param {object} [options]
147
+ * @returns {Promise<object>}
148
+ */
149
+ export async function getControllerSnapshotAsync(options = {}) {
150
+ const kubectl = options.kubectl || process.env.KRATE_KUBECTL || 'kubectl';
151
+ const namespace = options.namespace || process.env.KRATE_NAMESPACE || 'krate-system';
152
+ const timeoutMs = Number(options.timeoutMs || process.env.KRATE_KUBECTL_TIMEOUT_MS || 3_000);
153
+ const env = { ...process.env, ...(options.env || {}) };
154
+ const correlationId = randomUUID();
155
+
156
+ try {
157
+ // Phase 1: context + version in parallel
158
+ const [contextResult, versionResult] = await Promise.all([
159
+ runKubectlAsync(['config', 'current-context'], { kubectl, timeoutMs, env, allowFailure: true }),
160
+ runKubectlAsync(['version', '--client=true', '-o', 'json'], { kubectl, timeoutMs, env, allowFailure: true })
161
+ ]);
162
+
163
+ if (!contextResult.ok || !versionResult.ok) {
164
+ return buildEmptySnapshot({ correlationId, namespace, kubectl, contextResult, versionResult, available: false });
165
+ }
166
+
167
+ // Phase 2: API service + CRD discovery in parallel
168
+ const [apiServiceResult, crdResult] = await Promise.all([
169
+ runKubectlAsync(['get', 'apiservice', KRATE_API_VERSIONED_GROUP, '-o', 'json'], { kubectl, timeoutMs, env, allowFailure: true }),
170
+ runKubectlAsync(['get', 'crd', '-o', 'json'], { kubectl, timeoutMs, env, allowFailure: true })
171
+ ]);
172
+
173
+ if (!apiServiceResult.ok && !crdResult.ok) {
174
+ return buildEmptySnapshot({ correlationId, namespace, kubectl, contextResult, versionResult, available: true });
175
+ }
176
+
177
+ const KYVERNO_DISCOVERY_GROUPS = new Set([KYVERNO_API_GROUP, KYVERNO_POLICIES_API_GROUP, POLICY_REPORT_API_GROUP]);
178
+ const discoveredCrds = crdResult.ok
179
+ ? parseKubernetesList(crdResult.stdout).items.filter((crd) =>
180
+ [KRATE_API_GROUP, KUBEVELA_API_GROUP].includes(crd.spec?.group) ||
181
+ KYVERNO_DISCOVERY_GROUPS.has(crd.spec?.group))
182
+ : [];
183
+ const discoveredPluralSet = new Set(
184
+ discoveredCrds.map((crd) => `${crd.spec?.group || KRATE_API_GROUP}/${crd.spec?.names?.plural}`).filter(Boolean)
185
+ );
186
+
187
+ if (!apiServiceResult.ok && discoveredCrds.length === 0) {
188
+ return buildEmptySnapshot({ correlationId, namespace, kubectl, contextResult, versionResult, available: true, apiServiceResult });
189
+ }
190
+
191
+ // Phase 3: list all resources in parallel
192
+ const snapshotResources = KRATE_RESOURCES.filter((d) => d.storage !== 'core');
193
+ const resources = Object.fromEntries(snapshotResources.map((d) => [d.kind, []]));
194
+
195
+ const platformScopedDefs = snapshotResources.filter((d) => d.platformScoped);
196
+ const orgScopedDefs = snapshotResources.filter((d) => !d.platformScoped);
197
+
198
+ // Fetch platform-scoped resources first so we can derive org namespaces
199
+ const platformResults = await Promise.all(
200
+ platformScopedDefs
201
+ .filter((d) => discoveredPluralSet.has(`${d.group || KRATE_API_GROUP}/${d.plural}`))
202
+ .map(async (definition) => {
203
+ const resourceNamespace = definition.namespace || namespace;
204
+ const result = await runKubectlAsync(
205
+ ['get', apiResourceName(definition), ...namespaceArgs(definition, resourceNamespace), '-o', 'json', '--ignore-not-found'],
206
+ { kubectl, timeoutMs, env, allowFailure: true }
207
+ );
208
+ return { definition, result };
209
+ })
210
+ );
211
+
212
+ for (const { definition, result } of platformResults) {
213
+ resources[definition.kind] = result.ok ? parseKubernetesList(result.stdout).items : [];
214
+ }
215
+
216
+ const orgNamespaces = resolveOrgNamespaces(resources.Organization, resources.OrgNamespaceBinding, namespace);
217
+
218
+ // Fetch org-scoped resources in parallel
219
+ const orgResults = await Promise.all(
220
+ orgScopedDefs
221
+ .filter((d) => discoveredPluralSet.has(`${d.group || KRATE_API_GROUP}/${d.plural}`))
222
+ .map(async (definition) => {
223
+ const namespaces = definition.namespaced === false
224
+ ? [null]
225
+ : [definition.namespace || null].filter(Boolean).concat(definition.namespace ? [] : orgNamespaces);
226
+
227
+ const itemArrays = await Promise.all(
228
+ namespaces.map(async (resourceNamespace) => {
229
+ const effectiveNamespace = resourceNamespace || namespace;
230
+ const result = await runKubectlAsync(
231
+ ['get', apiResourceName(definition), ...namespaceArgs(definition, effectiveNamespace), '-o', 'json', '--ignore-not-found'],
232
+ { kubectl, timeoutMs, env, allowFailure: true }
233
+ );
234
+ return result.ok ? parseKubernetesList(result.stdout).items : [];
235
+ })
236
+ );
237
+ return { definition, items: itemArrays.flat() };
238
+ })
239
+ );
240
+
241
+ for (const { definition, items } of orgResults) {
242
+ resources[definition.kind] = items;
243
+ }
244
+
245
+ const eventsResult = await runKubectlAsync(
246
+ ['get', 'events', '-n', namespace, '-o', 'json', '--ignore-not-found'],
247
+ { kubectl, timeoutMs, env, allowFailure: true }
248
+ );
249
+
250
+ return {
251
+ source: 'kubernetes',
252
+ mode: 'kubernetes-api',
253
+ namespace,
254
+ generatedAt: new Date().toISOString(),
255
+ correlationId,
256
+ kubectl: {
257
+ binary: kubectl,
258
+ context: contextResult.stdout.trim(),
259
+ clientVersion: safeJson(versionResult.stdout)?.clientVersion?.gitVersion || null,
260
+ available: true,
261
+ errors: []
262
+ },
263
+ apiService: apiServiceResult.ok ? safeJson(apiServiceResult.stdout) : null,
264
+ crds: discoveredCrds,
265
+ resources,
266
+ kyverno: emptyKyverno(),
267
+ events: eventsResult.ok ? parseKubernetesList(eventsResult.stdout).items : [],
268
+ permissions: [],
269
+ storage: storageBoundaries(),
270
+ commands: []
271
+ };
272
+ } catch (error) {
273
+ // Fallback to synchronous snapshot
274
+ const { getControllerSnapshot } = await import('./kubernetes-controller.js');
275
+ return getControllerSnapshot(options);
276
+ }
277
+ }
278
+
279
+ // ---------------------------------------------------------------------------
280
+ // Partial snapshot
281
+ // ---------------------------------------------------------------------------
282
+
283
+ /**
284
+ * Fetch only the requested resource kinds from Kubernetes.
285
+ * Useful for pages that only need a subset (e.g. only AgentStack, AgentSession).
286
+ *
287
+ * @param {string[]} kinds - Array of kind names or plurals to query
288
+ * @param {object} [options]
289
+ * @returns {Promise<object>} Snapshot with only the requested resource keys populated
290
+ */
291
+ export async function getPartialSnapshot(kinds = [], options = {}) {
292
+ if (!Array.isArray(kinds) || kinds.length === 0) {
293
+ return { source: 'kubernetes', mode: 'partial', resources: {}, generatedAt: new Date().toISOString() };
294
+ }
295
+
296
+ const kubectl = options.kubectl || process.env.KRATE_KUBECTL || 'kubectl';
297
+ const namespace = options.namespace || process.env.KRATE_NAMESPACE || 'krate-system';
298
+ const timeoutMs = Number(options.timeoutMs || process.env.KRATE_KUBECTL_TIMEOUT_MS || 3_000);
299
+ const env = { ...process.env, ...(options.env || {}) };
300
+
301
+ // Resolve requested kinds to definition objects (ignore unknown)
302
+ const requestedDefs = kinds
303
+ .map((k) => { try { return findResourceDefinition(k); } catch { return null; } })
304
+ .filter(Boolean);
305
+
306
+ const resources = Object.fromEntries(requestedDefs.map((d) => [d.kind, []]));
307
+
308
+ // Determine org namespaces only when necessary
309
+ const needsOrgNs = requestedDefs.some((d) => !d.platformScoped && d.namespaced !== false && !d.namespace);
310
+ let orgNamespaces = [namespace];
311
+
312
+ if (needsOrgNs) {
313
+ try {
314
+ const orgDef = findResourceDefinition('Organization');
315
+ const bindingDef = findResourceDefinition('OrgNamespaceBinding');
316
+ const [orgResult, bindingResult] = await Promise.all([
317
+ runKubectlAsync(['get', apiResourceName(orgDef), '-n', namespace, '-o', 'json', '--ignore-not-found'], { kubectl, timeoutMs, env, allowFailure: true }),
318
+ runKubectlAsync(['get', apiResourceName(bindingDef), '-n', namespace, '-o', 'json', '--ignore-not-found'], { kubectl, timeoutMs, env, allowFailure: true })
319
+ ]);
320
+ orgNamespaces = resolveOrgNamespaces(
321
+ orgResult.ok ? parseKubernetesList(orgResult.stdout).items : [],
322
+ bindingResult.ok ? parseKubernetesList(bindingResult.stdout).items : [],
323
+ namespace
324
+ );
325
+ } catch {
326
+ orgNamespaces = [namespace];
327
+ }
328
+ }
329
+
330
+ // Fetch all requested definitions in parallel
331
+ await Promise.all(requestedDefs.map(async (definition) => {
332
+ const namespaces = definition.namespaced === false
333
+ ? [null]
334
+ : definition.platformScoped
335
+ ? [definition.namespace || namespace]
336
+ : [definition.namespace || null].filter(Boolean).concat(definition.namespace ? [] : orgNamespaces);
337
+
338
+ const itemArrays = await Promise.all(
339
+ namespaces.map(async (resourceNamespace) => {
340
+ const effectiveNamespace = resourceNamespace || namespace;
341
+ const result = await runKubectlAsync(
342
+ ['get', apiResourceName(definition), ...namespaceArgs(definition, effectiveNamespace), '-o', 'json', '--ignore-not-found'],
343
+ { kubectl, timeoutMs, env, allowFailure: true }
344
+ );
345
+ return result.ok ? parseKubernetesList(result.stdout).items : [];
346
+ })
347
+ );
348
+ resources[definition.kind] = itemArrays.flat();
349
+ }));
350
+
351
+ return {
352
+ source: 'kubernetes',
353
+ mode: 'partial',
354
+ namespace,
355
+ generatedAt: new Date().toISOString(),
356
+ resources
357
+ };
358
+ }
359
+
360
+ // ---------------------------------------------------------------------------
361
+ // Watch resource changes
362
+ // ---------------------------------------------------------------------------
363
+
364
+ /**
365
+ * Watch key resource kinds with `kubectl get --watch -o json`.
366
+ * On any change event, the snapshot cache is cleared so the next read
367
+ * triggers a fresh fetch.
368
+ *
369
+ * @param {Function} callback - Called with (kind, item) on each watch event
370
+ * @param {object} [options]
371
+ * @param {string[]} [options.kinds] - Defaults to ['Organization', 'AgentStack', 'AgentSession']
372
+ * @returns {{ stop: () => void }} Cleanup handle
373
+ */
374
+ export function watchResourceChanges(callback, options = {}) {
375
+ const kubectl = options.kubectl || process.env.KRATE_KUBECTL || 'kubectl';
376
+ const namespace = options.namespace || process.env.KRATE_NAMESPACE || 'krate-system';
377
+ const env = { ...process.env, ...(options.env || {}) };
378
+ const watchKinds = options.kinds || ['Organization', 'AgentStack', 'AgentSession'];
379
+
380
+ const children = [];
381
+
382
+ for (const kind of watchKinds) {
383
+ let definition;
384
+ try { definition = findResourceDefinition(kind); } catch { continue; }
385
+
386
+ const resourceNamespace = definition.namespace || namespace;
387
+ const args = [
388
+ 'get', apiResourceName(definition),
389
+ ...namespaceArgs(definition, resourceNamespace),
390
+ '--watch', '-o', 'json'
391
+ ];
392
+
393
+ const child = spawn(kubectl, kubectlArgs(args, env), {
394
+ env,
395
+ windowsHide: true,
396
+ stdio: ['ignore', 'pipe', 'pipe']
397
+ });
398
+
399
+ let buffer = '';
400
+ child.stdout.on('data', (chunk) => {
401
+ buffer += chunk.toString();
402
+ let newlineIdx;
403
+ while ((newlineIdx = buffer.indexOf('\n')) !== -1) {
404
+ const line = buffer.slice(0, newlineIdx).trim();
405
+ buffer = buffer.slice(newlineIdx + 1);
406
+ if (!line) continue;
407
+ const item = safeJson(line);
408
+ if (item) {
409
+ // Invalidate the snapshot cache so the next read fetches fresh data
410
+ clearSnapshotCache();
411
+ try { callback(kind, item); } catch { /* user callback errors are non-fatal */ }
412
+ }
413
+ }
414
+ });
415
+
416
+ children.push(child);
417
+ }
418
+
419
+ return {
420
+ stop() {
421
+ for (const child of children) {
422
+ try { child.kill('SIGTERM'); } catch { /* ignore */ }
423
+ }
424
+ children.length = 0;
425
+ }
426
+ };
427
+ }
428
+
429
+ // ---------------------------------------------------------------------------
430
+ // Private helpers
431
+ // ---------------------------------------------------------------------------
432
+
433
+ function namespaceArgs(definition, namespace) {
434
+ return definition.namespaced === false ? [] : ['-n', namespace];
435
+ }
436
+
437
+ function parseKubernetesList(stdout) {
438
+ const parsed = safeJson(stdout);
439
+ if (!parsed) return { items: [] };
440
+ if (Array.isArray(parsed.items)) return parsed;
441
+ if (parsed.kind && parsed.metadata) return { items: [parsed] };
442
+ return { items: [] };
443
+ }
444
+
445
+ function safeJson(text) {
446
+ try { return text ? JSON.parse(text) : null; }
447
+ catch { return null; }
448
+ }
449
+
450
+ function resolveOrgNamespaces(organizations = [], bindings = [], fallbackNamespace = KRATE_PLATFORM_NAMESPACE) {
451
+ const namespaces = [...new Set([
452
+ ...organizations.map((o) => o.spec?.namespaceName || o.metadata?.labels?.['krate.a5c.ai/namespace']).filter(Boolean),
453
+ ...bindings.map((b) => b.spec?.namespace || b.metadata?.labels?.['krate.a5c.ai/namespace']).filter(Boolean)
454
+ ])];
455
+ if (namespaces.length) return namespaces;
456
+ const fallbackOrgs = new Set();
457
+ const adminOrg = process.env.KRATE_ADMIN_ORG;
458
+ const defaultOrg = process.env.KRATE_ORG || 'default';
459
+ if (adminOrg) fallbackOrgs.add(orgNamespaceName(adminOrg));
460
+ fallbackOrgs.add(orgNamespaceName(defaultOrg));
461
+ return fallbackOrgs.size ? [...fallbackOrgs] : [fallbackNamespace];
462
+ }
463
+
464
+ function storageBoundaries() {
465
+ return {
466
+ etcd: 'Krate CRDs: Organization, Repository, SSHKey, RepositoryPermission, BranchProtection, RefPolicy, WebhookSubscription, RunnerPool, View, Selector',
467
+ kubevela: 'Krate deployment CRDs: Application, ApplicationRevision, ComponentDefinition, WorkloadDefinition, TraitDefinition, ScopeDefinition, PolicyDefinition, Policy, WorkflowStepDefinition, Workflow, ResourceTracker',
468
+ postgres: 'Aggregated API resources: PullRequest, Issue, Review, Pipeline, Job, WebhookDelivery',
469
+ repositories: 'Repository backend Deployment, repository storage, and integration plans',
470
+ objects: 'Object storage referenced by Repository specs and Pipeline artifacts'
471
+ };
472
+ }
473
+
474
+ function emptyKyverno() {
475
+ return {
476
+ mode: 'auto',
477
+ detected: false,
478
+ resources: {},
479
+ reports: { policyReports: [], clusterPolicyReports: [], results: [], violations: [] },
480
+ permissions: [],
481
+ degraded: [],
482
+ controllers: []
483
+ };
484
+ }
485
+
486
+ function buildEmptySnapshot({ correlationId, namespace, kubectl, contextResult, versionResult, available, apiServiceResult }) {
487
+ return {
488
+ source: 'kubernetes',
489
+ mode: 'kubernetes-api',
490
+ namespace,
491
+ generatedAt: new Date().toISOString(),
492
+ correlationId,
493
+ kubectl: {
494
+ binary: kubectl,
495
+ context: contextResult?.ok ? contextResult.stdout.trim() : null,
496
+ clientVersion: versionResult?.ok ? safeJson(versionResult.stdout)?.clientVersion?.gitVersion || null : null,
497
+ available: Boolean(available),
498
+ errors: [contextResult, versionResult, apiServiceResult]
499
+ .filter((r) => r && !r.ok)
500
+ .map((r) => `${r.command}: ${(r.stderr || r.error || '').trim() || `exit ${r.status}`}`)
501
+ },
502
+ apiService: null,
503
+ crds: [],
504
+ resources: Object.fromEntries(KRATE_RESOURCES.filter((d) => d.storage !== 'core').map((d) => [d.kind, []])),
505
+ kyverno: emptyKyverno(),
506
+ events: [],
507
+ permissions: [],
508
+ storage: storageBoundaries(),
509
+ commands: []
510
+ };
511
+ }