@a5c-ai/krate 5.0.1-staging.f672fe79b

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 (174) hide show
  1. package/Dockerfile +29 -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 +2407 -0
  6. package/dist/krate-lifecycle.json +201 -0
  7. package/dist/krate-runtime-snapshot.json +2955 -0
  8. package/dist/krate-summary.json +687 -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/glossary.md +66 -0
  27. package/docs/agents/implementation-blueprint.md +324 -0
  28. package/docs/agents/implementation-rollout-slices.md +251 -0
  29. package/docs/agents/memory-context-integration-spec.md +194 -0
  30. package/docs/agents/memory-ontology-schema-spec.md +253 -0
  31. package/docs/agents/memory-operations-runbook.md +121 -0
  32. package/docs/agents/mvp-vertical-slice-spec.md +146 -0
  33. package/docs/agents/observability-audit-spec.md +265 -0
  34. package/docs/agents/operator-runbook.md +174 -0
  35. package/docs/agents/org-memory-api-payload-examples.md +333 -0
  36. package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
  37. package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
  38. package/docs/agents/org-memory-ui-implementation-map.md +114 -0
  39. package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
  40. package/docs/agents/org-resource-model-delta-spec.md +111 -0
  41. package/docs/agents/org-route-resource-model-spec.md +183 -0
  42. package/docs/agents/org-scoping-namespace-spec.md +114 -0
  43. package/docs/agents/rbac-secrets-management-spec.md +406 -0
  44. package/docs/agents/repository-page-integration-spec.md +255 -0
  45. package/docs/agents/resource-contract-examples.md +808 -0
  46. package/docs/agents/resource-relationship-map.md +190 -0
  47. package/docs/agents/security-threat-model.md +188 -0
  48. package/docs/agents/shared-memory-company-brain-spec.md +358 -0
  49. package/docs/agents/storage-migration-spec.md +168 -0
  50. package/docs/agents/subagent-orchestration-spec.md +152 -0
  51. package/docs/agents/system-overview.md +88 -0
  52. package/docs/agents/tools-mcp-skills-spec.md +189 -0
  53. package/docs/agents/traceability-matrix.md +79 -0
  54. package/docs/agents/ui-flow-spec.md +211 -0
  55. package/docs/agents/ui-ux-system-spec.md +426 -0
  56. package/docs/agents/workspace-lifecycle-spec.md +166 -0
  57. package/docs/architecture-spec.md +78 -0
  58. package/docs/components/control-plane.md +78 -0
  59. package/docs/components/data-plane.md +69 -0
  60. package/docs/components/hooks-events.md +67 -0
  61. package/docs/components/identity-rbac-policy.md +73 -0
  62. package/docs/components/kubevela-oam.md +70 -0
  63. package/docs/components/operations-publishing.md +81 -0
  64. package/docs/components/runners-ci.md +66 -0
  65. package/docs/components/web-ui.md +94 -0
  66. package/docs/external/README.md +47 -0
  67. package/docs/external/bidirectional-sync-design.md +134 -0
  68. package/docs/external/cicd-interface.md +64 -0
  69. package/docs/external/external-backend-controllers.md +170 -0
  70. package/docs/external/external-backend-crds.md +234 -0
  71. package/docs/external/external-backend-ui-spec.md +151 -0
  72. package/docs/external/external-backend-ux-flows.md +115 -0
  73. package/docs/external/external-object-mapping.md +125 -0
  74. package/docs/external/git-forge-interface.md +68 -0
  75. package/docs/external/github-integration-design.md +151 -0
  76. package/docs/external/issue-tracking-interface.md +66 -0
  77. package/docs/external/provider-capability-manifests.md +204 -0
  78. package/docs/external/provider-catalog.md +139 -0
  79. package/docs/external/provider-rollout-testing.md +78 -0
  80. package/docs/external/research-results.md +48 -0
  81. package/docs/external/security-auth-permissions.md +81 -0
  82. package/docs/external/sync-state-machines.md +108 -0
  83. package/docs/external/unified-external-backend-model.md +107 -0
  84. package/docs/external/user-facing-changes.md +67 -0
  85. package/docs/gaps.md +161 -0
  86. package/docs/install.md +94 -0
  87. package/docs/krate-design.md +334 -0
  88. package/docs/local-minikube.md +55 -0
  89. package/docs/ontology/README.md +32 -0
  90. package/docs/ontology/bounded-contexts.md +29 -0
  91. package/docs/ontology/events-and-hooks.md +32 -0
  92. package/docs/ontology/oam-kubevela.md +32 -0
  93. package/docs/ontology/operations-and-release.md +25 -0
  94. package/docs/ontology/personas-and-actors.md +32 -0
  95. package/docs/ontology/policies-and-invariants.md +33 -0
  96. package/docs/ontology/problem-space.md +30 -0
  97. package/docs/ontology/resource-contracts.md +40 -0
  98. package/docs/ontology/resource-taxonomy.md +42 -0
  99. package/docs/ontology/runners-and-ci.md +29 -0
  100. package/docs/ontology/solution-space.md +24 -0
  101. package/docs/ontology/storage-and-data-boundaries.md +29 -0
  102. package/docs/ontology/validation-matrix.md +24 -0
  103. package/docs/ontology/web-ui-excellent-flows.md +32 -0
  104. package/docs/ontology/workflows.md +39 -0
  105. package/docs/ontology/world.md +35 -0
  106. package/docs/product-requirements.md +62 -0
  107. package/docs/roadmap-mvp.md +87 -0
  108. package/docs/system-requirements.md +90 -0
  109. package/docs/tests/README.md +53 -0
  110. package/docs/tests/agent-qa-plan.md +63 -0
  111. package/docs/tests/browser-ui-tests.md +62 -0
  112. package/docs/tests/ci-quality-gates.md +48 -0
  113. package/docs/tests/coverage-model.md +64 -0
  114. package/docs/tests/e2e-scenario-tests.md +53 -0
  115. package/docs/tests/fixtures-test-data.md +63 -0
  116. package/docs/tests/observability-reliability-tests.md +54 -0
  117. package/docs/tests/product-test-matrix.md +145 -0
  118. package/docs/tests/qa-adoption-roadmap.md +130 -0
  119. package/docs/tests/qa-automation-plan.md +101 -0
  120. package/docs/tests/security-compliance-tests.md +57 -0
  121. package/docs/tests/test-framework-tools.md +88 -0
  122. package/docs/tests/test-suite-layout.md +121 -0
  123. package/docs/tests/unit-integration-tests.md +48 -0
  124. package/docs/todo-kyverno +714 -0
  125. package/docs/user-stories.md +78 -0
  126. package/examples/minikube-demo.yaml +190 -0
  127. package/examples/oam-application.yaml +23 -0
  128. package/examples/policy-kyverno-pr-title.yaml +18 -0
  129. package/package.json +63 -0
  130. package/scripts/build.mjs +29 -0
  131. package/scripts/setup-minikube.mjs +65 -0
  132. package/scripts/smoke.mjs +37 -0
  133. package/scripts/validate-doc-coverage.mjs +152 -0
  134. package/scripts/validate-package.mjs +93 -0
  135. package/scripts/validate-ui.mjs +207 -0
  136. package/src/agent-approval-controller.js +123 -0
  137. package/src/agent-context-bundles.js +242 -0
  138. package/src/agent-dispatch-controller.js +86 -0
  139. package/src/agent-mux-client.js +280 -0
  140. package/src/agent-permission-review.js +162 -0
  141. package/src/agent-stack-controller.js +296 -0
  142. package/src/agent-trigger-controller.js +108 -0
  143. package/src/api-controller.js +206 -0
  144. package/src/argocd-gitops.js +43 -0
  145. package/src/auth.js +265 -0
  146. package/src/component-catalog.js +41 -0
  147. package/src/control-plane.js +136 -0
  148. package/src/controller-client.js +38 -0
  149. package/src/controller-ui.js +538 -0
  150. package/src/data-plane.js +178 -0
  151. package/src/gitea-backend.js +95 -0
  152. package/src/handoff.js +98 -0
  153. package/src/hooks-events.js +63 -0
  154. package/src/http-server.js +151 -0
  155. package/src/identity-policy.js +86 -0
  156. package/src/index.js +30 -0
  157. package/src/kubernetes-controller.js +812 -0
  158. package/src/kubernetes-resource-gateway.js +48 -0
  159. package/src/operations.js +112 -0
  160. package/src/resource-model.js +203 -0
  161. package/src/runners-ci.js +48 -0
  162. package/src/runtime.js +196 -0
  163. package/src/web-ui.js +40 -0
  164. package/tests/agent-approval-controller.test.js +173 -0
  165. package/tests/agent-context-bundles.test.js +278 -0
  166. package/tests/agent-dispatch-controller.test.js +176 -0
  167. package/tests/agent-mux-client.test.js +204 -0
  168. package/tests/agent-permission-review.test.js +209 -0
  169. package/tests/agent-resources.test.js +212 -0
  170. package/tests/agent-stack-controller.test.js +221 -0
  171. package/tests/agent-trigger-controller.test.js +211 -0
  172. package/tests/deployment.test.js +395 -0
  173. package/tests/e2e/lifecycle.test.js +117 -0
  174. package/tests/krate.test.js +727 -0
@@ -0,0 +1,812 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+
5
+ export const KRATE_API_GROUP = 'krate.a5c.ai';
6
+ export const KRATE_API_VERSION = 'v1alpha1';
7
+ export const KRATE_API_VERSIONED_GROUP = `${KRATE_API_VERSION}.${KRATE_API_GROUP}`;
8
+ export const KUBEVELA_API_GROUP = 'core.oam.dev';
9
+ export const KYVERNO_API_GROUP = 'kyverno.io';
10
+ export const KYVERNO_POLICIES_API_GROUP = 'policies.kyverno.io';
11
+ export const POLICY_REPORT_API_GROUP = 'wgpolicyk8s.io';
12
+ export const KRATE_ORG_LABEL = 'krate.a5c.ai/org';
13
+ export const KRATE_ORG_NAMESPACE_LABEL = 'krate.a5c.ai/namespace';
14
+ export const KRATE_PLATFORM_NAMESPACE = process.env.KRATE_NAMESPACE || 'krate-system';
15
+
16
+ export const KUBERNETES_RESOURCE_CLIENT_BOUNDARY = {
17
+ role: 'kubernetes-resource-client',
18
+ scope: 'workspace API discovery, delegated access checks, list/get/apply/delete/watch, and command normalization',
19
+ owns: ['workspace command execution', 'Krate API resource discovery', 'delegated access checks', 'workspace watch streams'],
20
+ mustNotOwn: ['HTTP route orchestration', 'Next.js page flows', 'forge DTO composition', 'business workflow decisions']
21
+ };
22
+
23
+ export const KRATE_KUBERNETES_RECONCILER_BOUNDARY = {
24
+ role: 'krate-kubernetes-reconciler',
25
+ scope: 'reconcile Kubernetes-style Krate resources into status, repository hosting intent, policy projection, and data-plane sync intent',
26
+ owns: ['Repository status projection', 'identity and access status projection', 'repository hosting intent', 'policy sync intent', 'degraded condition reporting'],
27
+ delegatesTo: ['kubernetes-resource-gateway', 'git-data-plane'],
28
+ mustNotOwn: ['HTTP routes', 'web page navigation', 'API DTO shaping', 'browser form behavior']
29
+ };
30
+
31
+ export const KRATE_RESOURCES = [
32
+ { kind: 'Organization', plural: 'organizations', namespaced: true, namespace: KRATE_PLATFORM_NAMESPACE, storage: 'etcd', platformScoped: true },
33
+ { kind: 'OrgNamespaceBinding', plural: 'orgnamespacebindings', namespaced: true, namespace: KRATE_PLATFORM_NAMESPACE, storage: 'etcd', platformScoped: true },
34
+ { kind: 'User', plural: 'users', namespaced: true, storage: 'etcd' },
35
+ { kind: 'Team', plural: 'teams', namespaced: true, storage: 'etcd' },
36
+ { kind: 'Invite', plural: 'invites', namespaced: true, storage: 'etcd' },
37
+ { kind: 'IdentityMapping', plural: 'identitymappings', namespaced: true, storage: 'etcd' },
38
+ { kind: 'AuthProvider', plural: 'authproviders', namespaced: true, storage: 'etcd' },
39
+ { kind: 'Repository', plural: 'repositories', namespaced: true, storage: 'etcd' },
40
+ { kind: 'SSHKey', plural: 'sshkeys', namespaced: true, storage: 'etcd' },
41
+ { kind: 'RepositoryPermission', plural: 'repositorypermissions', namespaced: true, storage: 'etcd' },
42
+ { kind: 'BranchProtection', plural: 'branchprotections', namespaced: true, storage: 'etcd' },
43
+ { kind: 'RefPolicy', plural: 'refpolicies', namespaced: true, storage: 'etcd' },
44
+ { kind: 'PolicyProfile', plural: 'policyprofiles', namespaced: true, storage: 'etcd' },
45
+ { kind: 'PolicyTemplate', plural: 'policytemplates', namespaced: true, storage: 'etcd' },
46
+ { kind: 'PolicyBinding', plural: 'policybindings', namespaced: true, storage: 'etcd' },
47
+ { kind: 'PolicyExceptionRequest', plural: 'policyexceptionrequests', namespaced: true, storage: 'etcd' },
48
+ { kind: 'WebhookSubscription', plural: 'webhooksubscriptions', namespaced: true, storage: 'etcd' },
49
+ { kind: 'RunnerPool', plural: 'runnerpools', namespaced: true, storage: 'etcd' },
50
+ { kind: 'PullRequest', plural: 'pullrequests', namespaced: true, storage: 'postgres' },
51
+ { kind: 'Issue', plural: 'issues', namespaced: true, storage: 'postgres' },
52
+ { kind: 'Review', plural: 'reviews', namespaced: true, storage: 'postgres' },
53
+ { kind: 'Pipeline', plural: 'pipelines', namespaced: true, storage: 'postgres' },
54
+ { kind: 'Job', plural: 'jobs', namespaced: true, storage: 'postgres' },
55
+ { kind: 'WebhookDelivery', plural: 'webhookdeliveries', namespaced: true, storage: 'postgres' },
56
+ { kind: 'KubeVelaApplication', plural: 'applications', group: KUBEVELA_API_GROUP, namespaced: true, storage: 'kubevela' },
57
+ { kind: 'KubeVelaApplicationRevision', plural: 'applicationrevisions', group: KUBEVELA_API_GROUP, namespaced: true, storage: 'kubevela' },
58
+ { kind: 'KubeVelaComponentDefinition', plural: 'componentdefinitions', group: KUBEVELA_API_GROUP, namespaced: true, namespace: process.env.KRATE_KUBEVELA_NAMESPACE || 'vela-system', storage: 'kubevela' },
59
+ { kind: 'KubeVelaWorkloadDefinition', plural: 'workloaddefinitions', group: KUBEVELA_API_GROUP, namespaced: true, namespace: process.env.KRATE_KUBEVELA_NAMESPACE || 'vela-system', storage: 'kubevela' },
60
+ { kind: 'KubeVelaTraitDefinition', plural: 'traitdefinitions', group: KUBEVELA_API_GROUP, namespaced: true, namespace: process.env.KRATE_KUBEVELA_NAMESPACE || 'vela-system', storage: 'kubevela' },
61
+ { kind: 'KubeVelaScopeDefinition', plural: 'scopedefinitions', group: KUBEVELA_API_GROUP, namespaced: true, namespace: process.env.KRATE_KUBEVELA_NAMESPACE || 'vela-system', storage: 'kubevela' },
62
+ { kind: 'KubeVelaPolicyDefinition', plural: 'policydefinitions', group: KUBEVELA_API_GROUP, namespaced: true, namespace: process.env.KRATE_KUBEVELA_NAMESPACE || 'vela-system', storage: 'kubevela' },
63
+ { kind: 'KubeVelaPolicy', plural: 'policies', group: KUBEVELA_API_GROUP, namespaced: true, storage: 'kubevela' },
64
+ { kind: 'KubeVelaWorkflowStepDefinition', plural: 'workflowstepdefinitions', group: KUBEVELA_API_GROUP, namespaced: true, namespace: process.env.KRATE_KUBEVELA_NAMESPACE || 'vela-system', storage: 'kubevela' },
65
+ { kind: 'KubeVelaWorkflow', plural: 'workflows', group: KUBEVELA_API_GROUP, namespaced: true, storage: 'kubevela' },
66
+ { kind: 'KubeVelaResourceTracker', plural: 'resourcetrackers', group: KUBEVELA_API_GROUP, namespaced: false, storage: 'kubevela' },
67
+ { kind: 'View', plural: 'views', namespaced: true, storage: 'etcd' },
68
+ { kind: 'Selector', plural: 'selectors', namespaced: true, storage: 'etcd' }
69
+ ];
70
+
71
+ export const KYVERNO_RESOURCES = [
72
+ { kind: 'KyvernoPolicy', plural: 'policies', group: KYVERNO_API_GROUP, namespaced: true, storage: 'kyverno', namespace: process.env.KRATE_KYVERNO_POLICY_NAMESPACE || process.env.KRATE_NAMESPACE || 'krate-system' },
73
+ { kind: 'KyvernoClusterPolicy', plural: 'clusterpolicies', group: KYVERNO_API_GROUP, namespaced: false, storage: 'kyverno' },
74
+ { kind: 'KyvernoValidatingPolicy', plural: 'validatingpolicies', group: KYVERNO_POLICIES_API_GROUP, namespaced: false, storage: 'kyverno' },
75
+ { kind: 'KyvernoMutatingPolicy', plural: 'mutatingpolicies', group: KYVERNO_POLICIES_API_GROUP, namespaced: false, storage: 'kyverno' },
76
+ { kind: 'KyvernoGeneratingPolicy', plural: 'generatingpolicies', group: KYVERNO_POLICIES_API_GROUP, namespaced: false, storage: 'kyverno' },
77
+ { kind: 'KyvernoDeletingPolicy', plural: 'deletingpolicies', group: KYVERNO_POLICIES_API_GROUP, namespaced: false, storage: 'kyverno' },
78
+ { kind: 'KyvernoImageValidatingPolicy', plural: 'imagevalidatingpolicies', group: KYVERNO_POLICIES_API_GROUP, namespaced: false, storage: 'kyverno' },
79
+ { kind: 'KyvernoPolicyException', plural: 'policyexceptions', group: KYVERNO_POLICIES_API_GROUP, namespaced: true, storage: 'kyverno', namespace: process.env.KRATE_KYVERNO_POLICY_NAMESPACE || process.env.KRATE_NAMESPACE || 'krate-system' },
80
+ { kind: 'PolicyReport', plural: 'policyreports', group: POLICY_REPORT_API_GROUP, namespaced: true, storage: 'kyverno-reports' },
81
+ { kind: 'ClusterPolicyReport', plural: 'clusterpolicyreports', group: POLICY_REPORT_API_GROUP, namespaced: false, storage: 'kyverno-reports' }
82
+ ];
83
+
84
+ const KYVERNO_DISCOVERY_GROUPS = new Set([KYVERNO_API_GROUP, KYVERNO_POLICIES_API_GROUP, POLICY_REPORT_API_GROUP]);
85
+
86
+ export function createKubernetesResourceClient(options = {}) {
87
+ const kubectl = options.kubectl || process.env.KRATE_KUBECTL || 'kubectl';
88
+ const namespace = options.namespace || process.env.KRATE_NAMESPACE || 'krate-system';
89
+ const timeoutMs = Number(options.timeoutMs || process.env.KRATE_KUBECTL_TIMEOUT_MS || 3_000);
90
+ const env = { ...process.env, ...(options.env || {}) };
91
+
92
+ return {
93
+ ...KUBERNETES_RESOURCE_CLIENT_BOUNDARY,
94
+ namespace,
95
+ kubectl,
96
+ resourceDefinitions: KRATE_RESOURCES,
97
+ async snapshot() {
98
+ return getControllerSnapshot({ kubectl, namespace, timeoutMs, env });
99
+ },
100
+ async listResource(kindOrPlural) {
101
+ return listResource(kindOrPlural, { kubectl, namespace, timeoutMs, env });
102
+ },
103
+ async getResource(kindOrPlural, name) {
104
+ return getResource(kindOrPlural, name, { kubectl, namespace, timeoutMs, env });
105
+ },
106
+ async applyResource(resource) {
107
+ return applyResource(resource, { kubectl, namespace, timeoutMs, env });
108
+ },
109
+ async deleteResource(kindOrPlural, name) {
110
+ return deleteResource(kindOrPlural, name, { kubectl, namespace, timeoutMs, env });
111
+ },
112
+ async createRepository(input) {
113
+ return applyResource(repositoryManifest(input, namespace), { kubectl, namespace, timeoutMs, env });
114
+ },
115
+ async createOrganization(input) {
116
+ return createOrganization(input, { kubectl, namespace, timeoutMs, env });
117
+ },
118
+ watchResource(resourcePath, handlers = {}) {
119
+ return watchResource(resourcePath, { kubectl, namespace, env }, handlers);
120
+ }
121
+ };
122
+ }
123
+
124
+ export function createKubernetesController(options = {}) {
125
+ return createKubernetesResourceClient(options);
126
+ }
127
+ export function createKrateKubernetesReconciler(options = {}) {
128
+ const namespace = options.namespace || process.env.KRATE_NAMESPACE || 'krate-system';
129
+ return {
130
+ ...KRATE_KUBERNETES_RECONCILER_BOUNDARY,
131
+ namespace,
132
+ describeReconciliationScope() {
133
+ return {
134
+ ...KRATE_KUBERNETES_RECONCILER_BOUNDARY,
135
+ namespace,
136
+ resources: KRATE_RESOURCES.map(({ kind, plural, storage }) => ({ kind, plural, storage }))
137
+ };
138
+ },
139
+ reconcileRepository(resource = {}) {
140
+ const name = resource.metadata?.name || 'unknown-repository';
141
+ return {
142
+ kind: 'RepositoryReconciliationPlan',
143
+ namespace: resource.metadata?.namespace || namespace,
144
+ name,
145
+ desiredStatus: {
146
+ phase: resource.status?.phase || 'Reconciling',
147
+ gitBackend: resource.status?.gitBackend || 'gitea',
148
+ conditions: [
149
+ { type: 'ResourceObserved', status: 'True', reason: 'KubernetesResourceWatch' },
150
+ { type: 'DataPlaneSyncPlanned', status: 'True', reason: 'RepositoryBackendProjection' }
151
+ ]
152
+ },
153
+ syncIntents: [
154
+ { target: 'git-data-plane', action: 'ensure-gitea-repository', repository: name },
155
+ { target: 'policy-controller', action: 'compile-ref-policy', repository: name }
156
+ ]
157
+ };
158
+ },
159
+ reconcileIdentityAccess(resource = {}, context = {}) {
160
+ return identityAccessReconciliationPlan(resource, { namespace, ...context });
161
+ },
162
+ reconcileIdentityAccessResources(resources = {}, context = {}) {
163
+ const items = ['User', 'Team', 'Invite', 'IdentityMapping', 'RepositoryPermission', 'SSHKey']
164
+ .flatMap((kind) => (resources[kind] || []).map((resource) => identityAccessReconciliationPlan(resource, { namespace, ...context })));
165
+ return {
166
+ kind: 'IdentityAccessReconciliationPlan',
167
+ namespace,
168
+ desiredStatuses: items.map(({ kind, name, desiredStatus }) => ({ kind, name, phase: desiredStatus.phase, conditions: desiredStatus.conditions })),
169
+ syncIntents: items.flatMap((item) => item.syncIntents),
170
+ counts: items.reduce((counts, item) => ({ ...counts, [item.kind]: (counts[item.kind] || 0) + 1 }), {})
171
+ };
172
+ }
173
+ };
174
+ }
175
+ export function identityAccessReconciliationPlan(resource = {}, options = {}) {
176
+ const kind = resource.kind || 'Unknown';
177
+ const name = resource.metadata?.name || `unknown-${kind.toLowerCase()}`;
178
+ const namespace = resource.metadata?.namespace || options.namespace || process.env.KRATE_NAMESPACE || 'krate-system';
179
+ const spec = resource.spec || {};
180
+ const baseCondition = { type: `${kind}Observed`, status: 'True', reason: 'KrateResourceWatch' };
181
+ const base = {
182
+ kind,
183
+ namespace,
184
+ name,
185
+ desiredStatus: { phase: resource.status?.phase || 'Reconciling', conditions: [baseCondition] },
186
+ syncIntents: []
187
+ };
188
+ if (kind === 'User') {
189
+ const disabled = Boolean(spec.disabled);
190
+ return {
191
+ ...base,
192
+ desiredStatus: {
193
+ phase: disabled ? 'Disabled' : 'Active',
194
+ repositoryIdentity: spec.username || name,
195
+ groups: ['krate:users', spec.admin ? 'krate:platform-engineers' : 'krate:developers', ...(spec.teams || []).map((team) => `team:${team}`)],
196
+ conditions: [baseCondition, { type: 'WorkspaceIdentityProjected', status: 'True', reason: disabled ? 'UserDisabled' : 'UserActive' }, { type: 'RepositoryIdentityProjected', status: disabled ? 'False' : 'True', reason: disabled ? 'RepositoryAccessSuspended' : 'RepositoryAccountPlanned' }]
197
+ },
198
+ syncIntents: [
199
+ { target: 'workspace-identity', action: disabled ? 'suspend-user' : 'ensure-user', user: name },
200
+ { target: 'repository-access', action: disabled ? 'suspend-repository-user' : 'ensure-repository-user', user: name, repositoryIdentity: spec.username || name }
201
+ ]
202
+ };
203
+ }
204
+ if (kind === 'Team') {
205
+ return {
206
+ ...base,
207
+ desiredStatus: { phase: 'Active', memberCount: (spec.members || []).length, maintainerCount: (spec.maintainers || []).length, conditions: [baseCondition, { type: 'TeamMembershipProjected', status: 'True', reason: 'MembersAndMaintainersPlanned' }, { type: 'RepositoryGrantsProjected', status: 'True', reason: 'TeamGrantProjectionPlanned' }] },
208
+ syncIntents: [
209
+ { target: 'workspace-identity', action: 'sync-team-membership', team: name, members: spec.members || [], maintainers: spec.maintainers || [] },
210
+ ...(spec.repositoryGrants || []).map((grant) => ({ target: 'repository-access', action: 'sync-team-repository-grant', team: name, repository: grant.repository, permission: grant.permission }))
211
+ ]
212
+ };
213
+ }
214
+ if (kind === 'Invite') {
215
+ const phase = resource.status?.phase || 'Pending';
216
+ return {
217
+ ...base,
218
+ desiredStatus: { phase, expiresAt: spec.expiresAt || '', conditions: [baseCondition, { type: 'InviteLifecycleTracked', status: 'True', reason: phase === 'Pending' ? 'InvitePending' : `Invite${phase}` }] },
219
+ syncIntents: [{ target: 'workspace-identity', action: phase === 'Pending' ? 'send-invite' : 'close-invite', invite: name, email: spec.email, phase }]
220
+ };
221
+ }
222
+ if (kind === 'IdentityMapping') {
223
+ const complete = Boolean(spec.user && spec.provider && spec.subject);
224
+ return {
225
+ ...base,
226
+ desiredStatus: { phase: complete ? 'Synced' : 'Pending', workspaceIdentity: spec.workspaceIdentity?.name || spec.subject || '', repositoryIdentity: spec.repositoryIdentity?.username || spec.user || '', conditions: [baseCondition, { type: 'WorkspaceIdentityProjected', status: complete ? 'True' : 'False', reason: complete ? 'SubjectLinked' : 'MissingSubject' }, { type: 'RepositoryIdentityProjected', status: complete ? 'True' : 'False', reason: complete ? 'RepositoryAccountLinked' : 'MissingRepositoryIdentity' }] },
227
+ syncIntents: [
228
+ { target: 'workspace-identity', action: 'link-identity', user: spec.user, provider: spec.provider, subject: spec.subject },
229
+ { target: 'repository-access', action: 'link-repository-identity', user: spec.user, repositoryIdentity: spec.repositoryIdentity?.username || spec.user }
230
+ ]
231
+ };
232
+ }
233
+ if (kind === 'RepositoryPermission') {
234
+ const revoked = Boolean(spec.revoked || resource.status?.phase === 'Revoked');
235
+ return {
236
+ ...base,
237
+ desiredStatus: { phase: revoked ? 'Revoked' : 'Synced', repository: spec.repository, subject: spec.subject, permission: spec.permission || 'read', conditions: [baseCondition, { type: 'RepositoryPermissionProjected', status: revoked ? 'False' : 'True', reason: revoked ? 'GrantRevoked' : 'GrantSynced' }] },
238
+ syncIntents: [{ target: 'repository-access', action: revoked ? 'revoke-repository-permission' : 'sync-repository-permission', repository: spec.repository, subject: spec.subject, subjectKind: spec.subjectKind || 'user', permission: spec.permission || 'read' }]
239
+ };
240
+ }
241
+ if (kind === 'SSHKey') {
242
+ const fingerprint = spec.key ? `sha256:${createHash('sha256').update(spec.key).digest('base64url').slice(0, 32)}` : '';
243
+ const revoked = Boolean(spec.revoked || resource.status?.phase === 'Revoked');
244
+ return {
245
+ ...base,
246
+ desiredStatus: { phase: revoked ? 'Revoked' : 'Synced', scope: spec.scope, fingerprint, conditions: [baseCondition, { type: 'SSHKeyProjected', status: revoked ? 'False' : 'True', reason: revoked ? 'KeyRevoked' : 'KeySynced' }] },
247
+ syncIntents: [{ target: 'repository-access', action: revoked ? 'revoke-ssh-key' : 'sync-ssh-key', name, scope: spec.scope, owner: spec.owner || spec.user, fingerprint }]
248
+ };
249
+ }
250
+ return base;
251
+ }
252
+
253
+
254
+ export function orgNamespaceName(org) {
255
+ const slug = normalizeOrgSlug(org);
256
+ if (!slug) throw new Error('organization is required');
257
+ return `krate-org-${slug}`;
258
+ }
259
+
260
+ export function normalizeOrgSlug(value) {
261
+ return String(value || '').trim().toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 63);
262
+ }
263
+
264
+ export function resolveResourceOrg(resource = {}, options = {}) {
265
+ const organizationRef = resource.spec?.organizationRef || resource.metadata?.labels?.[KRATE_ORG_LABEL] || options.organization || options.org;
266
+ const org = normalizeOrgSlug(organizationRef);
267
+ if (!org && resource.kind !== 'Organization' && resource.kind !== 'OrgNamespaceBinding') throw new Error(`${resource.kind || 'Resource'} requires spec.organizationRef`);
268
+ const namespaceName = resource.kind === 'Organization'
269
+ ? (resource.metadata?.namespace || options.platformNamespace || KRATE_PLATFORM_NAMESPACE)
270
+ : (resource.spec?.namespaceName || resource.spec?.namespace || resource.metadata?.labels?.[KRATE_ORG_NAMESPACE_LABEL] || (org ? orgNamespaceName(org) : resource.metadata?.namespace || options.platformNamespace || KRATE_PLATFORM_NAMESPACE));
271
+ return { org, namespace: namespaceName };
272
+ }
273
+
274
+ export function withOrgScope(resource, options = {}) {
275
+ if (!resource || typeof resource !== 'object') throw new Error('resource object is required');
276
+ const platformNamespace = options.platformNamespace || options.namespace || KRATE_PLATFORM_NAMESPACE;
277
+ if (resource.kind === 'Organization') {
278
+ const org = normalizeOrgSlug(resource.spec?.slug || resource.metadata?.name);
279
+ if (!org) throw new Error('Organization requires metadata.name or spec.slug');
280
+ const orgNamespace = resource.spec?.namespaceName || orgNamespaceName(org);
281
+ return {
282
+ apiVersion: resource.apiVersion || `${KRATE_API_GROUP}/${KRATE_API_VERSION}`,
283
+ ...resource,
284
+ metadata: { ...(resource.metadata || {}), name: org, namespace: resource.metadata?.namespace || platformNamespace, labels: { ...(resource.metadata?.labels || {}), [KRATE_ORG_LABEL]: org, [KRATE_ORG_NAMESPACE_LABEL]: orgNamespace } },
285
+ spec: { ...(resource.spec || {}), slug: org, namespaceName: orgNamespace }
286
+ };
287
+ }
288
+ if (resource.kind === 'OrgNamespaceBinding') {
289
+ const org = normalizeOrgSlug(resource.spec?.organizationRef || resource.metadata?.labels?.[KRATE_ORG_LABEL] || resource.metadata?.name);
290
+ if (!org) throw new Error('OrgNamespaceBinding requires spec.organizationRef');
291
+ const orgNamespace = resource.spec?.namespace || orgNamespaceName(org);
292
+ return {
293
+ apiVersion: resource.apiVersion || `${KRATE_API_GROUP}/${KRATE_API_VERSION}`,
294
+ ...resource,
295
+ metadata: { ...(resource.metadata || {}), name: resource.metadata?.name || org, namespace: resource.metadata?.namespace || platformNamespace, labels: { ...(resource.metadata?.labels || {}), [KRATE_ORG_LABEL]: org, [KRATE_ORG_NAMESPACE_LABEL]: orgNamespace } },
296
+ spec: { createNamespace: true, ...(resource.spec || {}), organizationRef: org, namespace: orgNamespace, labels: { ...(resource.spec?.labels || {}), [KRATE_ORG_LABEL]: org } }
297
+ };
298
+ }
299
+ const { org, namespace } = resolveResourceOrg(resource, options);
300
+ if (resource.metadata?.namespace && resource.metadata.namespace !== namespace) throw new Error(`${resource.kind} namespace ${resource.metadata.namespace} does not match organization ${org} namespace ${namespace}`);
301
+ if (resource.metadata?.labels?.[KRATE_ORG_LABEL] && resource.metadata.labels[KRATE_ORG_LABEL] !== org) throw new Error(`${resource.kind} org label does not match spec.organizationRef`);
302
+ return {
303
+ apiVersion: resource.apiVersion || `${KRATE_API_GROUP}/${KRATE_API_VERSION}`,
304
+ ...resource,
305
+ metadata: { ...(resource.metadata || {}), namespace, labels: { ...(resource.metadata?.labels || {}), [KRATE_ORG_LABEL]: org, [KRATE_ORG_NAMESPACE_LABEL]: namespace } },
306
+ spec: { ...(resource.spec || {}), organizationRef: org }
307
+ };
308
+ }
309
+
310
+ export async function getControllerSnapshot(options = {}) {
311
+ const kubectl = options.kubectl || process.env.KRATE_KUBECTL || 'kubectl';
312
+ const namespace = options.namespace || process.env.KRATE_NAMESPACE || 'krate-system';
313
+ const timeoutMs = Number(options.timeoutMs || process.env.KRATE_KUBECTL_TIMEOUT_MS || 3_000);
314
+ const env = { ...process.env, ...(options.env || {}) };
315
+ const correlationId = randomUUID();
316
+ const contextResult = currentContextResult({ kubectl, timeoutMs, env });
317
+ const versionResult = runKubectl(['version', '--client=true', '-o', 'json'], { kubectl, timeoutMs, env, allowFailure: true });
318
+ if (!contextResult.ok || !versionResult.ok) {
319
+ const failed = [contextResult, versionResult].filter((result) => !result.ok);
320
+ return {
321
+ source: 'kubernetes',
322
+ mode: 'kubernetes-api',
323
+ namespace,
324
+ generatedAt: new Date().toISOString(),
325
+ correlationId,
326
+ kubectl: {
327
+ binary: kubectl,
328
+ context: contextResult.ok ? contextResult.stdout.trim() : null,
329
+ clientVersion: versionResult.ok ? safeJson(versionResult.stdout)?.clientVersion?.gitVersion || null : null,
330
+ available: false,
331
+ errors: failed.map((result) => commandFailure(result)).filter(Boolean)
332
+ },
333
+ apiService: null,
334
+ crds: [],
335
+ resources: Object.fromEntries(KRATE_RESOURCES.map((definition) => [definition.kind, []])),
336
+ kyverno: emptyKyvernoDiscovery(namespace, env),
337
+ events: [],
338
+ permissions: [],
339
+ storage: storageBoundaries(),
340
+ commands: controllerCommands(namespace)
341
+ };
342
+ }
343
+ const apiServiceResult = runKubectl(['get', 'apiservice', KRATE_API_VERSIONED_GROUP, '-o', 'json'], { kubectl, timeoutMs, env, allowFailure: true });
344
+ const crdResult = runKubectl(['get', 'crd', '-o', 'json'], { kubectl, timeoutMs, env, allowFailure: true });
345
+ if (!apiServiceResult.ok && !crdResult.ok) {
346
+ const failed = [apiServiceResult, crdResult].filter((result) => !result.ok);
347
+ return {
348
+ source: 'kubernetes',
349
+ mode: 'kubernetes-api',
350
+ namespace,
351
+ generatedAt: new Date().toISOString(),
352
+ correlationId,
353
+ kubectl: {
354
+ binary: kubectl,
355
+ context: contextResult.stdout.trim(),
356
+ clientVersion: safeJson(versionResult.stdout)?.clientVersion?.gitVersion || null,
357
+ available: true,
358
+ errors: failed.map((result) => commandFailure(result)).filter(Boolean)
359
+ },
360
+ apiService: null,
361
+ crds: [],
362
+ resources: Object.fromEntries(KRATE_RESOURCES.map((definition) => [definition.kind, []])),
363
+ kyverno: emptyKyvernoDiscovery(namespace, env),
364
+ events: [],
365
+ permissions: [],
366
+ storage: storageBoundaries(),
367
+ commands: controllerCommands(namespace)
368
+ };
369
+ }
370
+ const discoveredCrds = crdResult.ok ? parseKubernetesList(crdResult.stdout).items.filter((crd) => [KRATE_API_GROUP, KUBEVELA_API_GROUP].includes(crd.spec?.group) || KYVERNO_DISCOVERY_GROUPS.has(crd.spec?.group)) : [];
371
+ const discoveredPluralSet = new Set(discoveredCrds.map((crd) => `${crd.spec?.group || KRATE_API_GROUP}/${crd.spec?.names?.plural}`).filter(Boolean));
372
+ if (!apiServiceResult.ok && discoveredCrds.length === 0) {
373
+ return {
374
+ source: 'kubernetes',
375
+ mode: 'kubernetes-api',
376
+ namespace,
377
+ generatedAt: new Date().toISOString(),
378
+ correlationId,
379
+ kubectl: {
380
+ binary: kubectl,
381
+ context: contextResult.stdout.trim(),
382
+ clientVersion: safeJson(versionResult.stdout)?.clientVersion?.gitVersion || null,
383
+ available: true,
384
+ errors: [commandFailure(apiServiceResult)].filter(Boolean)
385
+ },
386
+ apiService: null,
387
+ crds: [],
388
+ resources: Object.fromEntries(KRATE_RESOURCES.map((definition) => [definition.kind, []])),
389
+ kyverno: emptyKyvernoDiscovery(namespace, env),
390
+ events: [],
391
+ permissions: [],
392
+ storage: storageBoundaries(),
393
+ commands: controllerCommands(namespace)
394
+ };
395
+ }
396
+ const kyverno = discoverKyverno({ kubectl, namespace, timeoutMs, env, discoveredPluralSet });
397
+ const resources = Object.fromEntries(KRATE_RESOURCES.map((definition) => [definition.kind, []]));
398
+ const listResults = [];
399
+ const platformScopedDefinitions = KRATE_RESOURCES.filter((definition) => definition.platformScoped);
400
+ const orgScopedDefinitions = KRATE_RESOURCES.filter((definition) => !definition.platformScoped);
401
+
402
+ for (const definition of platformScopedDefinitions) {
403
+ if (!discoveredPluralSet.has(`${definition.group || KRATE_API_GROUP}/${definition.plural}`)) continue;
404
+ const resourceNamespace = definition.namespace || namespace;
405
+ const result = runKubectl(['get', apiResourceName(definition), ...namespaceArgs(definition, resourceNamespace), '-o', 'json', '--ignore-not-found'], { kubectl, timeoutMs, env, allowFailure: true });
406
+ listResults.push({ definition, result });
407
+ resources[definition.kind] = result.ok ? parseKubernetesList(result.stdout).items : [];
408
+ }
409
+
410
+ const orgNamespaces = organizationNamespaces(resources.Organization, resources.OrgNamespaceBinding, namespace);
411
+ for (const definition of orgScopedDefinitions) {
412
+ if (!discoveredPluralSet.has(`${definition.group || KRATE_API_GROUP}/${definition.plural}`)) continue;
413
+ const namespaces = definition.namespaced === false ? [null] : [definition.namespace || null].filter(Boolean).concat(definition.namespace ? [] : orgNamespaces);
414
+ resources[definition.kind] = namespaces.flatMap((resourceNamespace) => {
415
+ const effectiveNamespace = resourceNamespace || namespace;
416
+ const result = runKubectl(['get', apiResourceName(definition), ...namespaceArgs(definition, effectiveNamespace), '-o', 'json', '--ignore-not-found'], { kubectl, timeoutMs, env, allowFailure: true });
417
+ listResults.push({ definition, result });
418
+ return result.ok ? parseKubernetesList(result.stdout).items : [];
419
+ });
420
+ }
421
+
422
+ const eventsResult = runKubectl(['get', 'events', '-n', namespace, '-o', 'json', '--ignore-not-found'], { kubectl, timeoutMs, env, allowFailure: true });
423
+ const permissions = await Promise.all(KRATE_RESOURCES.filter((definition) => discoveredPluralSet.has(`${definition.group || KRATE_API_GROUP}/${definition.plural}`)).map(async (definition) => ({
424
+ kind: definition.kind,
425
+ plural: definition.plural,
426
+ verbs: Object.fromEntries(['get', 'list', 'watch', 'create', 'update', 'patch', 'delete'].map((verb) => [verb, canI(verb, definition, { kubectl, namespace: definition.namespace || namespace, timeoutMs, env })]))
427
+ })));
428
+ const unavailable = [contextResult, versionResult, ...listResults.map((item) => item.result)].filter((result) => !result.ok);
429
+
430
+ return {
431
+ source: 'kubernetes',
432
+ mode: 'kubernetes-api',
433
+ namespace,
434
+ generatedAt: new Date().toISOString(),
435
+ correlationId,
436
+ kubectl: {
437
+ binary: kubectl,
438
+ context: contextResult.ok ? contextResult.stdout.trim() : null,
439
+ clientVersion: versionResult.ok ? safeJson(versionResult.stdout)?.clientVersion?.gitVersion || null : null,
440
+ available: contextResult.ok && versionResult.ok,
441
+ errors: unavailable.map((result) => commandFailure(result)).filter(Boolean)
442
+ },
443
+ apiService: apiServiceResult.ok ? safeJson(apiServiceResult.stdout) : null,
444
+ crds: discoveredCrds,
445
+ resources,
446
+ kyverno,
447
+ events: eventsResult.ok ? parseKubernetesList(eventsResult.stdout).items : [],
448
+ permissions,
449
+ storage: storageBoundaries(),
450
+ commands: controllerCommands(namespace)
451
+ };
452
+ }
453
+
454
+ export async function listResource(kindOrPlural, options = {}) {
455
+ const namespace = options.namespace || process.env.KRATE_NAMESPACE || 'krate-system';
456
+ const definition = findResourceDefinition(kindOrPlural);
457
+ const resourceNamespace = definition.namespace || namespace;
458
+ const args = ['get', apiResourceName(definition), ...namespaceArgs(definition, resourceNamespace), '-o', 'json', '--ignore-not-found'];
459
+ const result = runKubectl(args, {
460
+ kubectl: options.kubectl || process.env.KRATE_KUBECTL || 'kubectl',
461
+ timeoutMs: Number(options.timeoutMs || process.env.KRATE_KUBECTL_TIMEOUT_MS || 3_000),
462
+ env: { ...process.env, ...(options.env || {}) },
463
+ allowFailure: false
464
+ });
465
+ return { operation: 'list', kind: definition.kind, command: `kubectl ${args.join(' ')}`, items: parseKubernetesList(result.stdout).items, stderr: result.stderr.trim() };
466
+ }
467
+
468
+ export async function getResource(kindOrPlural, name, options = {}) {
469
+ if (!name) throw new Error('resource name is required');
470
+ const namespace = options.namespace || process.env.KRATE_NAMESPACE || 'krate-system';
471
+ const definition = findResourceDefinition(kindOrPlural);
472
+ const resourceNamespace = definition.namespace || namespace;
473
+ const args = ['get', apiResourceName(definition), name, ...namespaceArgs(definition, resourceNamespace), '-o', 'json'];
474
+ const result = runKubectl(args, {
475
+ kubectl: options.kubectl || process.env.KRATE_KUBECTL || 'kubectl',
476
+ timeoutMs: Number(options.timeoutMs || process.env.KRATE_KUBECTL_TIMEOUT_MS || 3_000),
477
+ env: { ...process.env, ...(options.env || {}) },
478
+ allowFailure: false
479
+ });
480
+ return { operation: 'get', kind: definition.kind, name, command: `kubectl ${args.join(' ')}`, resource: safeJson(result.stdout), stderr: result.stderr.trim() };
481
+ }
482
+ export async function applyResource(resource, options = {}) {
483
+ if (!resource || typeof resource !== 'object') throw new Error('resource object is required');
484
+ if (!resource.kind) throw new Error('resource.kind is required');
485
+ const namespace = options.namespace || process.env.KRATE_NAMESPACE || 'krate-system';
486
+ const manifest = withOrgScope(resource, { namespace });
487
+ const result = runKubectl(['apply', '-f', '-', '-o', 'json'], {
488
+ kubectl: options.kubectl || process.env.KRATE_KUBECTL || 'kubectl',
489
+ timeoutMs: Number(options.timeoutMs || process.env.KRATE_KUBECTL_TIMEOUT_MS || 3_000),
490
+ env: { ...process.env, ...(options.env || {}) },
491
+ input: JSON.stringify(manifest),
492
+ allowFailure: false
493
+ });
494
+ return { operation: 'apply', command: 'kubectl apply -f -', resource: safeJson(result.stdout) || manifest, stderr: result.stderr.trim() };
495
+ }
496
+
497
+ export async function deleteResource(kindOrPlural, name, options = {}) {
498
+ if (!name) throw new Error('resource name is required');
499
+ const namespace = options.namespace || process.env.KRATE_NAMESPACE || 'krate-system';
500
+ const definition = findResourceDefinition(kindOrPlural);
501
+ const resourceNamespace = definition.namespace || namespace;
502
+ const args = ['delete', apiResourceName(definition), name, ...namespaceArgs(definition, resourceNamespace), '-o', 'json', '--ignore-not-found'];
503
+ const result = runKubectl(args, {
504
+ kubectl: options.kubectl || process.env.KRATE_KUBECTL || 'kubectl',
505
+ timeoutMs: Number(options.timeoutMs || process.env.KRATE_KUBECTL_TIMEOUT_MS || 3_000),
506
+ env: { ...process.env, ...(options.env || {}) },
507
+ allowFailure: false
508
+ });
509
+ return { operation: 'delete', command: `kubectl ${args.join(' ')}`, resource: safeJson(result.stdout), stdout: result.stdout.trim(), stderr: result.stderr.trim() };
510
+ }
511
+
512
+ export async function createOrganization(input = {}, options = {}) {
513
+ const namespace = options.namespace || process.env.KRATE_NAMESPACE || KRATE_PLATFORM_NAMESPACE;
514
+ const org = normalizeOrgSlug(input.slug || input.name || input.metadata?.name || input.spec?.slug);
515
+ if (!org) throw new Error('organization name is required');
516
+ const orgNamespace = input.namespaceName || input.namespace || input.spec?.namespaceName || orgNamespaceName(org);
517
+ const displayName = input.displayName || input.fullName || input.spec?.displayName || org;
518
+ const namespaceManifest = {
519
+ apiVersion: 'v1',
520
+ kind: 'Namespace',
521
+ metadata: { name: orgNamespace, labels: { [KRATE_ORG_LABEL]: org, [KRATE_ORG_NAMESPACE_LABEL]: orgNamespace, ...(input.labels || {}) } }
522
+ };
523
+ const organization = withOrgScope({
524
+ apiVersion: `${KRATE_API_GROUP}/${KRATE_API_VERSION}`,
525
+ kind: 'Organization',
526
+ metadata: { name: org },
527
+ spec: { displayName, slug: org, namespaceName: orgNamespace, ...(input.spec || {}) }
528
+ }, { namespace });
529
+ const binding = withOrgScope({
530
+ apiVersion: `${KRATE_API_GROUP}/${KRATE_API_VERSION}`,
531
+ kind: 'OrgNamespaceBinding',
532
+ metadata: { name: org },
533
+ spec: { organizationRef: org, namespace: orgNamespace, createNamespace: true, labels: input.labels || {} }
534
+ }, { namespace });
535
+ const applyOptions = {
536
+ kubectl: options.kubectl || process.env.KRATE_KUBECTL || 'kubectl',
537
+ timeoutMs: Number(options.timeoutMs || process.env.KRATE_KUBECTL_TIMEOUT_MS || 3_000),
538
+ env: { ...process.env, ...(options.env || {}) },
539
+ allowFailure: false
540
+ };
541
+ const namespaceResult = runKubectl(['apply', '-f', '-', '-o', 'json'], { ...applyOptions, input: JSON.stringify(namespaceManifest) });
542
+ const organizationResult = await applyResource(organization, { ...applyOptions, namespace });
543
+ const bindingResult = await applyResource(binding, { ...applyOptions, namespace });
544
+ return {
545
+ operation: 'create-organization',
546
+ organization: organizationResult.resource,
547
+ namespace: safeJson(namespaceResult.stdout) || namespaceManifest,
548
+ binding: bindingResult.resource,
549
+ command: 'kubectl apply -f -'
550
+ };
551
+ }
552
+
553
+ export function repositoryManifest(input = {}, namespace = process.env.KRATE_NAMESPACE || 'krate-system') {
554
+ const name = String(input.name || input.metadata?.name || '').trim();
555
+ const organizationRef = normalizeOrgSlug(input.organizationRef || input.org || input.spec?.organizationRef || input.metadata?.labels?.[KRATE_ORG_LABEL]);
556
+ if (!name) throw new Error('repository name is required');
557
+ if (!organizationRef) throw new Error('repository organization is required');
558
+ return withOrgScope({
559
+ apiVersion: `${KRATE_API_GROUP}/${KRATE_API_VERSION}`,
560
+ kind: 'Repository',
561
+ metadata: { name, labels: input.labels || {} },
562
+ spec: {
563
+ organizationRef,
564
+ visibility: input.visibility || input.spec?.visibility || 'internal',
565
+ defaultBranch: input.defaultBranch || input.spec?.defaultBranch || 'main'
566
+ }
567
+ }, { namespace });
568
+ }
569
+
570
+ export function watchResource(resourcePath = 'orgs/default/repositories', options = {}, handlers = {}) {
571
+ const namespace = options.namespace || process.env.KRATE_NAMESPACE || 'krate-system';
572
+ const kubectl = options.kubectl || process.env.KRATE_KUBECTL || 'kubectl';
573
+ const parts = String(resourcePath || '').split('/').filter(Boolean);
574
+ if (parts[0] !== 'orgs' || !parts[1] || !parts[2]) throw new Error('watch requires /api/watch/orgs/{org}/{resource}');
575
+ const org = normalizeOrgSlug(parts[1]);
576
+ const [pluralOrKind, name] = parts.slice(2);
577
+ const definition = findResourceDefinition(pluralOrKind || 'repositories');
578
+ const resourceNamespace = definition.namespace || (definition.platformScoped ? namespace : orgNamespaceName(org));
579
+ const args = ['get', apiResourceName(definition), ...(name ? [name] : []), ...namespaceArgs(definition, resourceNamespace), '--watch', '-o', 'json'];
580
+ const env = { ...process.env, ...(options.env || {}) };
581
+ const child = spawn(kubectl, kubectlInvocationArgs(args, env), { env, windowsHide: true });
582
+ child.stdout.on('data', (chunk) => handlers.stdout?.(chunk));
583
+ child.stderr.on('data', (chunk) => handlers.stderr?.(chunk));
584
+ child.on('error', (error) => handlers.error?.(error));
585
+ child.on('close', (code) => handlers.close?.(code));
586
+ return { child, command: `kubectl ${args.join(' ')}` };
587
+ }
588
+
589
+ export function findResourceDefinition(kindOrPlural) {
590
+ const normalized = String(kindOrPlural || '').toLowerCase();
591
+ const definition = KRATE_RESOURCES.find((item) => item.kind.toLowerCase() === normalized || item.plural.toLowerCase() === normalized);
592
+ if (!definition) throw new Error(`Unsupported Krate resource ${kindOrPlural}`);
593
+ return definition;
594
+ }
595
+
596
+ export function apiResourceName(definition) {
597
+ return `${definition.plural}.${definition.group || KRATE_API_GROUP}`;
598
+ }
599
+
600
+ export function withKrateDefaults(resource, namespace) {
601
+ if (resource?.kind && resource.kind !== 'Organization' && resource.kind !== 'OrgNamespaceBinding') return withOrgScope(resource, { namespace });
602
+ return {
603
+ apiVersion: resource.apiVersion || `${KRATE_API_GROUP}/${KRATE_API_VERSION}`,
604
+ ...resource,
605
+ metadata: {
606
+ ...(resource.metadata || {}),
607
+ namespace: resource.metadata?.namespace || namespace
608
+ }
609
+ };
610
+ }
611
+
612
+ function canI(verb, definition, options) {
613
+ const result = runKubectl(['auth', 'can-i', verb, apiResourceName(definition), ...namespaceArgs(definition, options.namespace)], { ...options, allowFailure: true });
614
+ return result.ok && result.stdout.trim().toLowerCase() === 'yes';
615
+ }
616
+
617
+ export function runKubectl(args, options = {}) {
618
+ const env = options.env || process.env;
619
+ const result = spawnSync(options.kubectl || 'kubectl', kubectlInvocationArgs(args, env), {
620
+ input: options.input,
621
+ encoding: 'utf8',
622
+ timeout: options.timeoutMs || 3_000,
623
+ env,
624
+ windowsHide: true,
625
+ maxBuffer: Number(env.KRATE_KUBECTL_MAX_BUFFER_BYTES || 32 * 1024 * 1024)
626
+ });
627
+ const normalized = {
628
+ ok: result.status === 0 && !result.error,
629
+ status: result.status,
630
+ signal: result.signal,
631
+ stdout: result.stdout || '',
632
+ stderr: result.stderr || '',
633
+ error: result.error ? result.error.message : null,
634
+ command: `kubectl ${args.join(' ')}`
635
+ };
636
+ if (!normalized.ok && !options.allowFailure) {
637
+ throw new Error(commandFailure(normalized) || `kubectl failed: ${normalized.command}`);
638
+ }
639
+ return normalized;
640
+ }
641
+
642
+
643
+ function currentContextResult(options = {}) {
644
+ const inCluster = inClusterKubectlConfig(options.env || process.env);
645
+ if (inCluster) {
646
+ return {
647
+ ok: true,
648
+ status: 0,
649
+ signal: null,
650
+ stdout: `${inCluster.context}\n`,
651
+ stderr: '',
652
+ error: null,
653
+ command: 'kubectl config current-context'
654
+ };
655
+ }
656
+ return runKubectl(['config', 'current-context'], { ...options, allowFailure: true });
657
+ }
658
+
659
+ function kubectlInvocationArgs(args, env = process.env) {
660
+ const inCluster = inClusterKubectlConfig(env);
661
+ return inCluster ? [...inCluster.args, ...args] : args;
662
+ }
663
+
664
+ function inClusterKubectlConfig(env = process.env) {
665
+ if (String(env.KRATE_DISABLE_IN_CLUSTER_KUBECTL || '').toLowerCase() === 'true') return null;
666
+ if (env.KUBECONFIG) return null;
667
+ const host = env.KUBERNETES_SERVICE_HOST;
668
+ const port = env.KUBERNETES_SERVICE_PORT || '443';
669
+ const serviceAccountDir = env.KRATE_SERVICE_ACCOUNT_DIR || '/var/run/secrets/kubernetes.io/serviceaccount';
670
+ const tokenPath = env.KRATE_SERVICE_ACCOUNT_TOKEN || `${serviceAccountDir}/token`;
671
+ const certificateAuthorityPath = env.KRATE_SERVICE_ACCOUNT_CA || `${serviceAccountDir}/ca.crt`;
672
+ if (!host || !port || !existsSync(tokenPath) || !existsSync(certificateAuthorityPath)) return null;
673
+ const token = readFileSync(tokenPath, 'utf8').trim();
674
+ if (!token) return null;
675
+ return {
676
+ context: 'in-cluster',
677
+ args: [
678
+ `--server=https://${host}:${port}`,
679
+ `--certificate-authority=${certificateAuthorityPath}`,
680
+ `--token=${token}`
681
+ ]
682
+ };
683
+ }
684
+
685
+
686
+ function kyvernoMode(env = process.env) {
687
+ const configured = env.KRATE_KYVERNO_MODE || env.KRATE_EXTERNAL_KYVERNO_MODE;
688
+ if (configured) return configured;
689
+ if (String(env.KRATE_KYVERNO_ENABLED || '').toLowerCase() === 'true') return 'byo';
690
+ if (String(env.KRATE_KYVERNO_DISCOVER_EXISTING || '').toLowerCase() !== 'false') return 'auto';
691
+ return 'disabled';
692
+ }
693
+
694
+ function emptyKyvernoDiscovery(namespace = KRATE_PLATFORM_NAMESPACE, env = process.env) {
695
+ return {
696
+ mode: kyvernoMode(env),
697
+ namespace: env.KRATE_KYVERNO_NAMESPACE || env.KRATE_EXTERNAL_KYVERNO_NAMESPACE || 'kyverno',
698
+ policyNamespace: env.KRATE_KYVERNO_POLICY_NAMESPACE || namespace,
699
+ requireForEnforceMode: String(env.KRATE_KYVERNO_REQUIRE_FOR_ENFORCE_MODE || 'true') !== 'false',
700
+ detected: false,
701
+ controllers: [],
702
+ resources: Object.fromEntries(KYVERNO_RESOURCES.map((definition) => [definition.kind, []])),
703
+ reports: { policyReports: [], clusterPolicyReports: [], results: [], violations: [] },
704
+ permissions: [],
705
+ degraded: []
706
+ };
707
+ }
708
+
709
+ function discoverKyverno({ kubectl, namespace, timeoutMs, env, discoveredPluralSet }) {
710
+ const discovery = emptyKyvernoDiscovery(namespace, env);
711
+ const availableDefinitions = KYVERNO_RESOURCES.filter((definition) => discoveredPluralSet.has(`${definition.group || KRATE_API_GROUP}/${definition.plural}`));
712
+ discovery.detected = availableDefinitions.length > 0;
713
+ if (discovery.detected && discovery.mode === 'auto') discovery.mode = 'byo';
714
+ if (!discovery.detected) {
715
+ if (discovery.mode !== 'disabled') discovery.degraded.push('Kyverno CRDs are not installed or are not readable by the Krate service account.');
716
+ return discovery;
717
+ }
718
+
719
+ const listResults = [];
720
+ for (const definition of availableDefinitions) {
721
+ const resourceNamespace = definition.namespaced === false ? null : definition.namespace || (definition.kind === 'PolicyReport' ? namespace : discovery.policyNamespace);
722
+ const result = runKubectl(['get', apiResourceName(definition), ...namespaceArgs(definition, resourceNamespace), '-o', 'json', '--ignore-not-found'], { kubectl, timeoutMs, env, allowFailure: true });
723
+ listResults.push({ definition, result });
724
+ discovery.resources[definition.kind] = result.ok ? parseKubernetesList(result.stdout).items : [];
725
+ }
726
+
727
+ const deploymentsResult = runKubectl(['get', 'deploy', '-n', discovery.namespace, '-o', 'json', '--ignore-not-found'], { kubectl, timeoutMs, env, allowFailure: true });
728
+ discovery.controllers = deploymentsResult.ok ? parseKubernetesList(deploymentsResult.stdout).items.filter((deployment) => String(deployment.metadata?.name || '').includes('kyverno') || deployment.metadata?.labels?.['app.kubernetes.io/part-of'] === 'kyverno').map((deployment) => ({ name: deployment.metadata?.name, namespace: deployment.metadata?.namespace || discovery.namespace, ready: Number(deployment.status?.readyReplicas || 0) >= Number(deployment.spec?.replicas || 1), readyReplicas: deployment.status?.readyReplicas || 0, replicas: deployment.spec?.replicas || 0 })) : [];
729
+
730
+ discovery.permissions = availableDefinitions.map((definition) => {
731
+ const resourceNamespace = definition.namespaced === false ? null : definition.namespace || (definition.kind === 'PolicyReport' ? namespace : discovery.policyNamespace);
732
+ return {
733
+ kind: definition.kind,
734
+ plural: definition.plural,
735
+ apiResource: apiResourceName(definition),
736
+ verbs: Object.fromEntries(['get', 'list', 'watch', 'create', 'patch'].map((verb) => [verb, canI(verb, definition, { kubectl, namespace: resourceNamespace || namespace, timeoutMs, env })]))
737
+ };
738
+ });
739
+
740
+ const policyReports = discovery.resources.PolicyReport || [];
741
+ const clusterPolicyReports = discovery.resources.ClusterPolicyReport || [];
742
+ const results = [...policyReports, ...clusterPolicyReports].flatMap((report) => (report.results || []).map((result) => ({ report: report.metadata?.name, namespace: report.metadata?.namespace || '', policy: result.policy, rule: result.rule, result: result.result, severity: result.severity || result.properties?.severity || 'medium', message: result.message || '', resource: result.resources?.[0] || report.scope || null })));
743
+ discovery.reports = { policyReports, clusterPolicyReports, results, violations: results.filter((result) => ['fail', 'error', 'warn'].includes(String(result.result || '').toLowerCase())) };
744
+ discovery.degraded.push(...listResults.filter(({ result }) => !result.ok).map(({ definition, result }) => `${definition.kind}: ${commandFailure(result)}`));
745
+ if (discovery.mode !== 'disabled' && discovery.controllers.length === 0) discovery.degraded.push(`No Kyverno controller deployments were found in namespace ${discovery.namespace}.`);
746
+ return discovery;
747
+ }
748
+ function organizationNamespaces(organizations = [], bindings = [], fallbackNamespace = KRATE_PLATFORM_NAMESPACE) {
749
+ const namespaces = [...new Set([
750
+ ...organizations.map((org) => org.spec?.namespaceName || org.metadata?.labels?.[KRATE_ORG_NAMESPACE_LABEL]).filter(Boolean),
751
+ ...bindings.map((binding) => binding.spec?.namespace || binding.metadata?.labels?.[KRATE_ORG_NAMESPACE_LABEL]).filter(Boolean)
752
+ ])];
753
+ return namespaces.length ? namespaces : [fallbackNamespace];
754
+ }
755
+
756
+ function parseKubernetesList(stdout) {
757
+ const parsed = safeJson(stdout);
758
+ if (!parsed) return { items: [] };
759
+ if (Array.isArray(parsed.items)) return parsed;
760
+ if (parsed.kind && parsed.metadata) return { items: [parsed] };
761
+ return { items: [] };
762
+ }
763
+
764
+ function safeJson(text) {
765
+ try {
766
+ return text ? JSON.parse(text) : null;
767
+ } catch {
768
+ return null;
769
+ }
770
+ }
771
+
772
+ function commandFailure(result) {
773
+ if (!result || result.ok) return null;
774
+ const detail = (result.stderr || result.error || result.stdout || '').trim();
775
+ return `${result.command}: ${detail || `exit ${result.status ?? 'unknown'}`}`;
776
+ }
777
+
778
+ function namespaceArgs(definition, namespace) {
779
+ return definition.namespaced === false ? [] : ['-n', namespace];
780
+ }
781
+
782
+ function commandString(args) {
783
+ return args.join(' ');
784
+ }
785
+
786
+ function storageBoundaries() {
787
+ return {
788
+ etcd: 'Krate CRDs: Organization, Repository, SSHKey, RepositoryPermission, BranchProtection, RefPolicy, WebhookSubscription, RunnerPool, View, Selector',
789
+ kubevela: 'Krate deployment CRDs: Application, ApplicationRevision, ComponentDefinition, WorkloadDefinition, TraitDefinition, ScopeDefinition, PolicyDefinition, Policy, WorkflowStepDefinition, Workflow, ResourceTracker',
790
+ postgres: 'Aggregated API resources: PullRequest, Issue, Review, Pipeline, Job, WebhookDelivery',
791
+ repositories: 'Repository backend Deployment, repository storage, and integration plans',
792
+ objects: 'Object storage referenced by Repository specs and Pipeline artifacts'
793
+ };
794
+ }
795
+
796
+ function controllerCommands(namespace) {
797
+ return KRATE_RESOURCES.map((definition) => {
798
+ const resourceNamespace = definition.namespace || namespace;
799
+ return {
800
+ kind: definition.kind,
801
+ list: commandString(['kubectl', 'get', apiResourceName(definition), ...namespaceArgs(definition, resourceNamespace)]),
802
+ watch: commandString(['kubectl', 'get', apiResourceName(definition), ...namespaceArgs(definition, resourceNamespace), '--watch', '-o', 'json']),
803
+ apply: 'kubectl apply -f -',
804
+ delete: commandString(['kubectl', 'delete', apiResourceName(definition), '<name>', ...namespaceArgs(definition, resourceNamespace)])
805
+ };
806
+ });
807
+ }
808
+
809
+
810
+
811
+
812
+