@a5c-ai/krate 5.0.1-staging.00fa5317c

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 (256) 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 +3205 -0
  6. package/dist/krate-lifecycle.json +201 -0
  7. package/dist/krate-runtime-snapshot.json +3125 -0
  8. package/dist/krate-summary.json +724 -0
  9. package/docs/README.md +61 -0
  10. package/docs/agents/README.md +83 -0
  11. package/docs/agents/acceptance-test-matrix.md +193 -0
  12. package/docs/agents/agent-mux-adapter-contract.md +167 -0
  13. package/docs/agents/agent-mux-source-map.md +310 -0
  14. package/docs/agents/agent-run-memory-import-spec.md +256 -0
  15. package/docs/agents/agent-stack-management-spec.md +421 -0
  16. package/docs/agents/api-contract-spec.md +309 -0
  17. package/docs/agents/artifacts-writeback-spec.md +145 -0
  18. package/docs/agents/chart-packaging-spec.md +128 -0
  19. package/docs/agents/ci-orchestration-spec.md +140 -0
  20. package/docs/agents/context-assembly-spec.md +219 -0
  21. package/docs/agents/controller-reconciliation-spec.md +255 -0
  22. package/docs/agents/crd-schema-spec.md +315 -0
  23. package/docs/agents/decision-log-open-questions.md +169 -0
  24. package/docs/agents/developer-implementation-checklist.md +329 -0
  25. package/docs/agents/dispatching-design.md +262 -0
  26. package/docs/agents/gaps-agent-mux-to-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/openapi.yaml +1275 -0
  108. package/docs/product-requirements.md +62 -0
  109. package/docs/roadmap-mvp.md +87 -0
  110. package/docs/system-requirements.md +90 -0
  111. package/docs/tests/README.md +53 -0
  112. package/docs/tests/agent-qa-plan.md +63 -0
  113. package/docs/tests/browser-ui-tests.md +62 -0
  114. package/docs/tests/ci-quality-gates.md +48 -0
  115. package/docs/tests/coverage-model.md +64 -0
  116. package/docs/tests/e2e-scenario-tests.md +53 -0
  117. package/docs/tests/fixtures-test-data.md +63 -0
  118. package/docs/tests/observability-reliability-tests.md +54 -0
  119. package/docs/tests/product-test-matrix.md +145 -0
  120. package/docs/tests/qa-adoption-roadmap.md +130 -0
  121. package/docs/tests/qa-automation-plan.md +101 -0
  122. package/docs/tests/security-compliance-tests.md +57 -0
  123. package/docs/tests/test-framework-tools.md +88 -0
  124. package/docs/tests/test-suite-layout.md +121 -0
  125. package/docs/tests/unit-integration-tests.md +48 -0
  126. package/docs/todo-kyverno +714 -0
  127. package/docs/todos.md +4 -0
  128. package/docs/user-stories.md +78 -0
  129. package/examples/minikube-demo.yaml +190 -0
  130. package/examples/oam-application.yaml +23 -0
  131. package/examples/policy-kyverno-pr-title.yaml +18 -0
  132. package/package.json +63 -0
  133. package/scripts/build.mjs +29 -0
  134. package/scripts/setup-minikube.mjs +65 -0
  135. package/scripts/smoke.mjs +37 -0
  136. package/scripts/validate-doc-coverage.mjs +152 -0
  137. package/scripts/validate-package.mjs +93 -0
  138. package/scripts/validate-ui.mjs +278 -0
  139. package/src/agent-adapter-controller.js +169 -0
  140. package/src/agent-approval-controller.js +170 -0
  141. package/src/agent-context-bundles.js +242 -0
  142. package/src/agent-dispatch-controller.js +209 -0
  143. package/src/agent-gateway-config-controller.js +147 -0
  144. package/src/agent-memory-controller.js +357 -0
  145. package/src/agent-memory-import.js +327 -0
  146. package/src/agent-memory-query.js +292 -0
  147. package/src/agent-memory-repository-source-controller.js +255 -0
  148. package/src/agent-mux-client.js +280 -0
  149. package/src/agent-permission-review.js +250 -0
  150. package/src/agent-project-controller.js +117 -0
  151. package/src/agent-provider-config-controller.js +150 -0
  152. package/src/agent-secret-config-grant-controller.js +282 -0
  153. package/src/agent-session-transcript-controller.js +189 -0
  154. package/src/agent-stack-controller.js +347 -0
  155. package/src/agent-subagent-controller.js +160 -0
  156. package/src/agent-transport-binding-controller.js +121 -0
  157. package/src/agent-trigger-controller.js +381 -0
  158. package/src/agent-workspace-controller.js +702 -0
  159. package/src/agent-writeback-controller.js +302 -0
  160. package/src/api-controller.js +541 -0
  161. package/src/argocd-gitops.js +43 -0
  162. package/src/async-controller.js +207 -0
  163. package/src/audit-controller.js +191 -0
  164. package/src/auth.js +307 -0
  165. package/src/component-catalog.js +41 -0
  166. package/src/control-plane.js +136 -0
  167. package/src/controller-client.js +72 -0
  168. package/src/controller-ui.js +617 -0
  169. package/src/data-plane.js +179 -0
  170. package/src/event-bus.js +61 -0
  171. package/src/external/conflict-controller.js +225 -0
  172. package/src/external/github/auth.js +96 -0
  173. package/src/external/github/cicd.js +180 -0
  174. package/src/external/github/git-forge.js +240 -0
  175. package/src/external/github/index.js +144 -0
  176. package/src/external/github/issue-tracking.js +163 -0
  177. package/src/external/provider-adapter.js +161 -0
  178. package/src/external/provider-resource-factory.js +161 -0
  179. package/src/external/sync-controller.js +235 -0
  180. package/src/external/webhook-controller.js +144 -0
  181. package/src/external/write-controller.js +283 -0
  182. package/src/gitea-backend.js +131 -0
  183. package/src/gitea-service.js +173 -0
  184. package/src/handoff.js +98 -0
  185. package/src/hooks-events.js +63 -0
  186. package/src/http-server.js +377 -0
  187. package/src/identity-policy.js +86 -0
  188. package/src/index.js +57 -0
  189. package/src/kubernetes-controller-async.js +511 -0
  190. package/src/kubernetes-controller.js +878 -0
  191. package/src/kubernetes-resource-gateway.js +48 -0
  192. package/src/notification-controller.js +178 -0
  193. package/src/operations.js +112 -0
  194. package/src/org-scoping.js +5 -0
  195. package/src/resource-model.js +221 -0
  196. package/src/runner-controller.js +272 -0
  197. package/src/runners-ci.js +48 -0
  198. package/src/runtime.js +196 -0
  199. package/src/snapshot-cache.js +157 -0
  200. package/src/web-ui.js +40 -0
  201. package/tests/agent-adapter-controller.test.js +361 -0
  202. package/tests/agent-approval-controller.test.js +173 -0
  203. package/tests/agent-context-bundles.test.js +278 -0
  204. package/tests/agent-dispatch-controller.test.js +315 -0
  205. package/tests/agent-gateway-config-controller.test.js +386 -0
  206. package/tests/agent-memory-controller.test.js +308 -0
  207. package/tests/agent-memory-import-snapshot.test.js +477 -0
  208. package/tests/agent-memory-query.test.js +404 -0
  209. package/tests/agent-memory-repository-source.test.js +514 -0
  210. package/tests/agent-mux-client.test.js +204 -0
  211. package/tests/agent-permission-review-v2.test.js +317 -0
  212. package/tests/agent-permission-review.test.js +209 -0
  213. package/tests/agent-project-controller.test.js +302 -0
  214. package/tests/agent-provider-config-controller.test.js +376 -0
  215. package/tests/agent-resources.test.js +228 -0
  216. package/tests/agent-secret-config-grant.test.js +231 -0
  217. package/tests/agent-session-transcript-controller.test.js +499 -0
  218. package/tests/agent-stack-controller.test.js +221 -0
  219. package/tests/agent-subagent-controller.test.js +201 -0
  220. package/tests/agent-transport-binding-controller.test.js +294 -0
  221. package/tests/agent-trigger-controller.test.js +211 -0
  222. package/tests/agent-trigger-routes.test.js +190 -0
  223. package/tests/agent-trigger-sources.test.js +245 -0
  224. package/tests/agent-workspace-controller.test.js +181 -0
  225. package/tests/agent-writeback.test.js +292 -0
  226. package/tests/approval-persistence.test.js +171 -0
  227. package/tests/async-controller.test.js +252 -0
  228. package/tests/audit-controller.test.js +227 -0
  229. package/tests/codespace-controller.test.js +318 -0
  230. package/tests/deployment.test.js +407 -0
  231. package/tests/e2e/lifecycle.test.js +117 -0
  232. package/tests/event-bus-integration.test.js +190 -0
  233. package/tests/external-github-forge.test.js +560 -0
  234. package/tests/external-github-issues-cicd.test.js +520 -0
  235. package/tests/external-integration.test.js +470 -0
  236. package/tests/external-persistence.test.js +340 -0
  237. package/tests/external-provider-adapter.test.js +365 -0
  238. package/tests/external-resource-model.test.js +215 -0
  239. package/tests/external-webhook-sync.test.js +287 -0
  240. package/tests/external-write-conflict.test.js +353 -0
  241. package/tests/gitea-service.test.js +253 -0
  242. package/tests/health-check-real.test.js +165 -0
  243. package/tests/integration/full-flow.test.js +266 -0
  244. package/tests/krate.test.js +756 -0
  245. package/tests/memory-search-wiring.test.js +270 -0
  246. package/tests/notification-controller.test.js +196 -0
  247. package/tests/notification-integration.test.js +179 -0
  248. package/tests/org-scoping.test.js +687 -0
  249. package/tests/runner-controller.test.js +327 -0
  250. package/tests/runner-integration.test.js +231 -0
  251. package/tests/session-cookie-hmac.test.js +151 -0
  252. package/tests/snapshot-performance.test.js +247 -0
  253. package/tests/sse-events.test.js +107 -0
  254. package/tests/webhook-trigger.test.js +198 -0
  255. package/tests/workspace-volumes.test.js +312 -0
  256. package/tests/writeback-persistence.test.js +207 -0
@@ -0,0 +1,702 @@
1
+ import { createResource, clone } from './resource-model.js';
2
+
3
+ export const AGENT_WORKSPACE_CONTROLLER_BOUNDARY = {
4
+ role: 'agent-workspace-controller',
5
+ scope: 'Volume-backed git workspace provisioning with PVC lifecycle, git ops, runner mount, reuse, codespace management, and workspace associations',
6
+ owns: ['workspace creation', 'PVC manifest generation', 'git command specs', 'mount specs', 'workspace reuse', 'codespace lifecycle', 'workspace associations', 'run history'],
7
+ delegatesTo: ['resource-model'],
8
+ mustNotOwn: ['git execution', 'Kubernetes API calls', 'secret values']
9
+ };
10
+
11
+ export function createAgentWorkspaceController() {
12
+ return {
13
+ role: 'agent-workspace-controller',
14
+
15
+ // --- Volume lifecycle ---
16
+
17
+ createWorkspace({ name, organizationRef, repository, volumeSpec = {}, branch, namespace = 'default' }) {
18
+ if (!organizationRef) {
19
+ return { error: true, reason: 'missing-org', message: 'organizationRef is required' };
20
+ }
21
+ if (!repository) {
22
+ return { error: true, reason: 'missing-repository', message: 'repository is required' };
23
+ }
24
+
25
+ const workspaceName = name || `ws-${repository.replace(/[^a-z0-9-]/gi, '-').toLowerCase()}-${Date.now()}`;
26
+ const pvcName = `krate-ws-${workspaceName}`;
27
+ const storageClassName = volumeSpec.storageClassName || 'standard';
28
+ const capacity = volumeSpec.capacity || '10Gi';
29
+ const accessModes = volumeSpec.accessModes || ['ReadWriteOnce'];
30
+
31
+ const workspace = createResource('KrateWorkspace', { name: workspaceName, namespace }, {
32
+ organizationRef,
33
+ repository,
34
+ volumeSpec: {
35
+ storageClassName,
36
+ capacity,
37
+ accessModes,
38
+ },
39
+ branch: branch || 'main',
40
+ pvcName,
41
+ });
42
+ workspace.status = {
43
+ phase: 'Pending',
44
+ volumeStatus: 'Pending',
45
+ createdAt: new Date().toISOString(),
46
+ };
47
+
48
+ const pvcManifest = {
49
+ apiVersion: 'v1',
50
+ kind: 'PersistentVolumeClaim',
51
+ metadata: {
52
+ name: pvcName,
53
+ namespace,
54
+ labels: {
55
+ 'krate.a5c.ai/workspace': workspaceName,
56
+ 'krate.a5c.ai/org': organizationRef,
57
+ },
58
+ },
59
+ spec: {
60
+ storageClassName,
61
+ accessModes,
62
+ resources: {
63
+ requests: { storage: capacity },
64
+ },
65
+ },
66
+ };
67
+
68
+ return { error: false, workspace, pvcManifest };
69
+ },
70
+
71
+ deleteWorkspace({ name, namespace = 'default', resources = {} }) {
72
+ if (!name) {
73
+ return { error: true, reason: 'missing-name', message: 'workspace name is required' };
74
+ }
75
+
76
+ const workspaces = resources.KrateWorkspace || [];
77
+ const workspace = workspaces.find((w) => w.metadata?.name === name);
78
+ if (!workspace) {
79
+ return { error: true, reason: 'not-found', message: `KrateWorkspace not found: ${name}` };
80
+ }
81
+
82
+ const pvcName = workspace.spec?.pvcName || `krate-ws-${name}`;
83
+ const updated = clone(workspace);
84
+ updated.status = {
85
+ ...updated.status,
86
+ phase: 'Terminating',
87
+ terminatingAt: new Date().toISOString(),
88
+ };
89
+
90
+ const pvcDeleteManifest = {
91
+ apiVersion: 'v1',
92
+ kind: 'PersistentVolumeClaim',
93
+ metadata: {
94
+ name: pvcName,
95
+ namespace: workspace.metadata?.namespace || namespace,
96
+ },
97
+ action: 'delete',
98
+ };
99
+
100
+ return { error: false, workspace: updated, pvcDeleteManifest };
101
+ },
102
+
103
+ getWorkspaceStatus({ name, resources = {} }) {
104
+ if (!name) {
105
+ return { error: true, reason: 'missing-name', message: 'workspace name is required' };
106
+ }
107
+
108
+ const workspaces = resources.KrateWorkspace || [];
109
+ const workspace = workspaces.find((w) => w.metadata?.name === name);
110
+ if (!workspace) {
111
+ return { error: true, reason: 'not-found', message: `KrateWorkspace not found: ${name}` };
112
+ }
113
+
114
+ return {
115
+ error: false,
116
+ name,
117
+ volumeStatus: workspace.status?.volumeStatus || 'Pending',
118
+ phase: workspace.status?.phase || 'Pending',
119
+ repository: workspace.spec?.repository,
120
+ branch: workspace.spec?.branch,
121
+ runRef: workspace.status?.runRef || null,
122
+ pvcName: workspace.spec?.pvcName,
123
+ capacity: workspace.spec?.volumeSpec?.capacity,
124
+ };
125
+ },
126
+
127
+ // --- Git operations (intent-based) ---
128
+
129
+ initializeWorkspace({ workspace, mountPath = '/workspace' }) {
130
+ if (!workspace) {
131
+ return { error: true, reason: 'missing-workspace', message: 'workspace resource is required' };
132
+ }
133
+
134
+ const repoUrl = workspace.spec?.repository || '';
135
+ const isSsh = repoUrl.startsWith('git@') || repoUrl.includes('ssh://');
136
+
137
+ const env = {};
138
+ if (isSsh) {
139
+ env.GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null';
140
+ }
141
+
142
+ return {
143
+ error: false,
144
+ commandSpec: {
145
+ command: 'git',
146
+ args: ['clone', repoUrl, mountPath],
147
+ env,
148
+ },
149
+ };
150
+ },
151
+
152
+ checkoutBranch({ workspace, branch }) {
153
+ if (!workspace) {
154
+ return { error: true, reason: 'missing-workspace', message: 'workspace resource is required' };
155
+ }
156
+ if (!branch) {
157
+ return { error: true, reason: 'missing-branch', message: 'branch is required' };
158
+ }
159
+
160
+ return {
161
+ error: false,
162
+ commandSpec: {
163
+ command: 'git',
164
+ args: ['checkout', branch],
165
+ cwd: '/workspace',
166
+ },
167
+ };
168
+ },
169
+
170
+ syncWorkspace({ workspace }) {
171
+ if (!workspace) {
172
+ return { error: true, reason: 'missing-workspace', message: 'workspace resource is required' };
173
+ }
174
+
175
+ const branch = workspace.spec?.branch || 'main';
176
+
177
+ return {
178
+ error: false,
179
+ commandSpecs: [
180
+ {
181
+ command: 'git',
182
+ args: ['fetch', 'origin'],
183
+ cwd: '/workspace',
184
+ },
185
+ {
186
+ command: 'git',
187
+ args: ['reset', '--hard', `origin/${branch}`],
188
+ cwd: '/workspace',
189
+ },
190
+ ],
191
+ };
192
+ },
193
+
194
+ // --- Runner mount spec ---
195
+
196
+ getMountSpec({ workspace }) {
197
+ if (!workspace) {
198
+ return { error: true, reason: 'missing-workspace', message: 'workspace resource is required' };
199
+ }
200
+
201
+ const pvcName = workspace.spec?.pvcName || `krate-ws-${workspace.metadata?.name}`;
202
+
203
+ return {
204
+ error: false,
205
+ volume: {
206
+ name: 'workspace',
207
+ persistentVolumeClaim: { claimName: pvcName },
208
+ },
209
+ volumeMount: {
210
+ name: 'workspace',
211
+ mountPath: '/workspace',
212
+ },
213
+ };
214
+ },
215
+
216
+ // --- Workspace reuse ---
217
+
218
+ findReusableWorkspace({ organizationRef, repository, branch, resources = {} }) {
219
+ const workspaces = resources.KrateWorkspace || [];
220
+ const match = workspaces.find((w) =>
221
+ w.spec?.organizationRef === organizationRef &&
222
+ w.spec?.repository === repository &&
223
+ (w.spec?.branch || 'main') === (branch || 'main') &&
224
+ w.status?.phase === 'Ready'
225
+ );
226
+
227
+ return match ? clone(match) : null;
228
+ },
229
+
230
+ claimWorkspace({ name, runRef, resources = {} }) {
231
+ if (!name) {
232
+ return { error: true, reason: 'missing-name', message: 'workspace name is required' };
233
+ }
234
+ if (!runRef) {
235
+ return { error: true, reason: 'missing-run-ref', message: 'runRef is required' };
236
+ }
237
+
238
+ const workspaces = resources.KrateWorkspace || [];
239
+ const workspace = workspaces.find((w) => w.metadata?.name === name);
240
+ if (!workspace) {
241
+ return { error: true, reason: 'not-found', message: `KrateWorkspace not found: ${name}` };
242
+ }
243
+
244
+ if (workspace.status?.phase === 'InUse') {
245
+ return { error: true, reason: 'already-in-use', message: `KrateWorkspace ${name} is already in use by ${workspace.status.runRef}` };
246
+ }
247
+
248
+ const updated = clone(workspace);
249
+ updated.status = {
250
+ ...updated.status,
251
+ phase: 'InUse',
252
+ runRef,
253
+ claimedAt: new Date().toISOString(),
254
+ };
255
+
256
+ return { error: false, workspace: updated };
257
+ },
258
+
259
+ releaseWorkspace({ name, resources = {} }) {
260
+ if (!name) {
261
+ return { error: true, reason: 'missing-name', message: 'workspace name is required' };
262
+ }
263
+
264
+ const workspaces = resources.KrateWorkspace || [];
265
+ const workspace = workspaces.find((w) => w.metadata?.name === name);
266
+ if (!workspace) {
267
+ return { error: true, reason: 'not-found', message: `KrateWorkspace not found: ${name}` };
268
+ }
269
+
270
+ if (workspace.status?.phase !== 'InUse') {
271
+ return { error: true, reason: 'not-in-use', message: `KrateWorkspace ${name} is not in use (current phase: ${workspace.status?.phase || 'Unknown'})` };
272
+ }
273
+
274
+ const updated = clone(workspace);
275
+ updated.status = {
276
+ ...updated.status,
277
+ phase: 'Ready',
278
+ runRef: undefined,
279
+ claimedAt: undefined,
280
+ releasedAt: new Date().toISOString(),
281
+ };
282
+
283
+ return { error: false, workspace: updated };
284
+ },
285
+
286
+ // --- Legacy compat helpers ---
287
+
288
+ provisionWorkspace({ repository, ref, branch, dispatchRun, policy, namespace = 'default', organizationRef = 'default' }) {
289
+ if (!repository) {
290
+ return { error: true, reason: 'missing-repository', message: 'repository is required' };
291
+ }
292
+ if (!dispatchRun) {
293
+ return { error: true, reason: 'missing-dispatch-run', message: 'dispatchRun is required' };
294
+ }
295
+
296
+ const result = this.createWorkspace({
297
+ organizationRef,
298
+ repository,
299
+ branch: branch || 'main',
300
+ namespace,
301
+ volumeSpec: {},
302
+ });
303
+
304
+ if (result.error) return result;
305
+
306
+ // Mark as InUse with the dispatch run
307
+ result.workspace.status.phase = 'InUse';
308
+ result.workspace.status.runRef = dispatchRun;
309
+ result.workspace.status.volumeStatus = 'Bound';
310
+
311
+ const runtimeName = `rt-${result.workspace.metadata.name}`;
312
+ const runtime = createResource('KrateWorkspaceRuntime', { name: runtimeName, namespace }, {
313
+ organizationRef,
314
+ workspaceRef: result.workspace.metadata.name,
315
+ status: 'provisioning'
316
+ });
317
+ runtime.status = { phase: 'Provisioning', createdAt: new Date().toISOString() };
318
+
319
+ return { error: false, workspace: result.workspace, runtime, pvcManifest: result.pvcManifest };
320
+ },
321
+
322
+ archiveWorkspace({ workspaceName, reason, resources = {} }) {
323
+ if (!workspaceName) {
324
+ return { error: true, reason: 'missing-workspace-name', message: 'workspaceName is required' };
325
+ }
326
+
327
+ const workspaces = resources.KrateWorkspace || [];
328
+ const workspace = workspaces.find((w) => w.metadata?.name === workspaceName);
329
+ if (!workspace) {
330
+ return { error: true, reason: 'not-found', message: `KrateWorkspace not found: ${workspaceName}` };
331
+ }
332
+
333
+ const now = new Date().toISOString();
334
+ const updated = clone(workspace);
335
+ updated.status = {
336
+ ...updated.status,
337
+ phase: 'Archived',
338
+ archivedAt: now,
339
+ archiveReason: reason || 'No reason provided'
340
+ };
341
+
342
+ return { error: false, workspace: updated };
343
+ },
344
+
345
+ recoverWorkspace({ workspaceName, resources = {} }) {
346
+ if (!workspaceName) {
347
+ return { error: true, reason: 'missing-workspace-name', message: 'workspaceName is required' };
348
+ }
349
+
350
+ const workspaces = resources.KrateWorkspace || [];
351
+ const workspace = workspaces.find((w) => w.metadata?.name === workspaceName);
352
+ if (!workspace) {
353
+ return { error: true, reason: 'not-found', message: `KrateWorkspace not found: ${workspaceName}` };
354
+ }
355
+
356
+ if (workspace.status?.phase !== 'Archived') {
357
+ return { error: true, reason: 'not-archived', message: `KrateWorkspace ${workspaceName} is not archived (current phase: ${workspace.status?.phase || 'Unknown'})` };
358
+ }
359
+
360
+ const updated = clone(workspace);
361
+ updated.status = {
362
+ ...updated.status,
363
+ phase: 'Active',
364
+ archivedAt: undefined,
365
+ archiveReason: undefined
366
+ };
367
+
368
+ return { error: false, workspace: updated };
369
+ },
370
+
371
+ bindSession({ workspaceName, sessionRef, agent, namespace = 'default', organizationRef = 'default', resources = {} }) {
372
+ if (!workspaceName) {
373
+ return { error: true, reason: 'missing-workspace-name', message: 'workspaceName is required' };
374
+ }
375
+ if (!sessionRef) {
376
+ return { error: true, reason: 'missing-session-ref', message: 'sessionRef is required' };
377
+ }
378
+
379
+ const workspaces = resources.KrateWorkspace || [];
380
+ const workspace = workspaces.find((w) => w.metadata?.name === workspaceName);
381
+ if (!workspace) {
382
+ return { error: true, reason: 'not-found', message: `KrateWorkspace not found: ${workspaceName}` };
383
+ }
384
+
385
+ const updated = clone(workspace);
386
+ if (!updated.status) updated.status = {};
387
+ if (!Array.isArray(updated.status.boundSessions)) updated.status.boundSessions = [];
388
+ updated.status.boundSessions.push({
389
+ sessionRef,
390
+ agent: agent || undefined,
391
+ boundAt: new Date().toISOString()
392
+ });
393
+
394
+ return { error: false, workspace: updated };
395
+ },
396
+
397
+ linkWorkItem({ workspaceName, workItemRef, workItemKind, namespace = 'default', organizationRef = 'default' }) {
398
+ if (!workspaceName) {
399
+ return { error: true, reason: 'missing-workspace-name', message: 'workspaceName is required' };
400
+ }
401
+ if (!workItemRef) {
402
+ return { error: true, reason: 'missing-work-item-ref', message: 'workItemRef is required' };
403
+ }
404
+
405
+ const linkName = `wiwl-${workspaceName}-${workItemRef}-${Date.now()}`;
406
+ const link = createResource('WorkItemWorkspaceLink', { name: linkName, namespace }, {
407
+ organizationRef,
408
+ workItemRef,
409
+ workItemKind: workItemKind || 'Issue',
410
+ workspace: workspaceName
411
+ });
412
+ link.status = { phase: 'Active', createdAt: new Date().toISOString() };
413
+
414
+ return { error: false, link };
415
+ },
416
+
417
+ linkWorkItemToSession({ workItemRef, workItemKind, sessionRef, namespace = 'default', organizationRef = 'default' }) {
418
+ if (!workItemRef) {
419
+ return { error: true, reason: 'missing-work-item-ref', message: 'workItemRef is required' };
420
+ }
421
+ if (!sessionRef) {
422
+ return { error: true, reason: 'missing-session-ref', message: 'sessionRef is required' };
423
+ }
424
+
425
+ const linkName = `wisl-${sessionRef}-${workItemRef}-${Date.now()}`;
426
+ const link = createResource('WorkItemSessionLink', { name: linkName, namespace }, {
427
+ organizationRef,
428
+ workItemRef,
429
+ workItemKind: workItemKind || 'Issue',
430
+ agentSession: sessionRef
431
+ });
432
+ link.status = { phase: 'Active', createdAt: new Date().toISOString() };
433
+
434
+ return { error: false, link };
435
+ },
436
+
437
+ listWorkspacesForRepo({ repository, resources = {} }) {
438
+ const workspaces = resources.KrateWorkspace || [];
439
+ return workspaces.filter((w) => w.spec?.repository === repository).map(clone);
440
+ },
441
+
442
+ listWorkspacesForRun({ dispatchRun, resources = {} }) {
443
+ const workspaces = resources.KrateWorkspace || [];
444
+ return workspaces.filter((w) => w.status?.runRef === dispatchRun).map(clone);
445
+ },
446
+
447
+ // --- Codespace lifecycle ---
448
+
449
+ launchCodespace(workspace, options = {}) {
450
+ if (!workspace) {
451
+ return { error: true, reason: 'missing-workspace', message: 'workspace is required' };
452
+ }
453
+
454
+ // Only one codespace per workspace
455
+ if (workspace.status?.codespace?.running) {
456
+ return { error: true, reason: 'codespace-already-running', message: `Codespace already running for workspace ${workspace.metadata?.name}` };
457
+ }
458
+
459
+ const wsName = workspace.metadata?.name || 'unknown';
460
+ const namespace = workspace.metadata?.namespace || 'default';
461
+ const orgRef = workspace.spec?.organizationRef || 'default';
462
+ const pvcName = workspace.spec?.pvcName || `krate-ws-${wsName}`;
463
+ const image = options.image || 'codercom/code-server:latest';
464
+ const cpuLimit = options.cpu || '1';
465
+ const memoryLimit = options.memory || '2Gi';
466
+ const port = options.port || 8080;
467
+ const passwordSecretRef = options.passwordSecretRef || null;
468
+
469
+ const podName = `codespace-${wsName}`;
470
+ const serviceName = `codespace-svc-${wsName}`;
471
+
472
+ const podSpec = {
473
+ apiVersion: 'v1',
474
+ kind: 'Pod',
475
+ metadata: {
476
+ name: podName,
477
+ namespace,
478
+ labels: {
479
+ 'krate.a5c.ai/workspace': wsName,
480
+ 'krate.a5c.ai/org': orgRef,
481
+ 'krate.a5c.ai/component': 'codespace',
482
+ },
483
+ },
484
+ spec: {
485
+ containers: [
486
+ {
487
+ name: 'code-server',
488
+ image,
489
+ ports: [{ containerPort: port, name: 'http' }],
490
+ resources: {
491
+ limits: { cpu: cpuLimit, memory: memoryLimit },
492
+ requests: { cpu: '250m', memory: '512Mi' },
493
+ },
494
+ env: [
495
+ { name: 'KRATE_WORKSPACE', value: wsName },
496
+ { name: 'KRATE_ORG', value: orgRef },
497
+ { name: 'GIT_AUTHOR_NAME', value: options.gitAuthorName || 'krate-agent' },
498
+ { name: 'GIT_AUTHOR_EMAIL', value: options.gitAuthorEmail || `agent@${orgRef}.krate.local` },
499
+ ],
500
+ volumeMounts: [
501
+ { name: 'workspace', mountPath: '/workspace' },
502
+ ],
503
+ },
504
+ ],
505
+ volumes: [
506
+ {
507
+ name: 'workspace',
508
+ persistentVolumeClaim: { claimName: pvcName },
509
+ },
510
+ ],
511
+ restartPolicy: 'Always',
512
+ },
513
+ };
514
+
515
+ if (passwordSecretRef) {
516
+ podSpec.spec.containers[0].env.push({
517
+ name: 'PASSWORD',
518
+ valueFrom: { secretKeyRef: passwordSecretRef },
519
+ });
520
+ }
521
+
522
+ const serviceSpec = {
523
+ apiVersion: 'v1',
524
+ kind: 'Service',
525
+ metadata: {
526
+ name: serviceName,
527
+ namespace,
528
+ labels: {
529
+ 'krate.a5c.ai/workspace': wsName,
530
+ 'krate.a5c.ai/org': orgRef,
531
+ 'krate.a5c.ai/component': 'codespace',
532
+ },
533
+ },
534
+ spec: {
535
+ selector: {
536
+ 'krate.a5c.ai/workspace': wsName,
537
+ 'krate.a5c.ai/component': 'codespace',
538
+ },
539
+ ports: [
540
+ { port, targetPort: port, protocol: 'TCP', name: 'http' },
541
+ ],
542
+ type: 'ClusterIP',
543
+ },
544
+ };
545
+
546
+ const codespaceUrl = `http://${serviceName}.${namespace}.svc.cluster.local:${port}`;
547
+
548
+ return { error: false, podSpec, serviceSpec, codespaceUrl };
549
+ },
550
+
551
+ stopCodespace(workspace) {
552
+ if (!workspace) {
553
+ return { error: true, reason: 'missing-workspace', message: 'workspace is required' };
554
+ }
555
+
556
+ const wsName = workspace.metadata?.name || 'unknown';
557
+ const namespace = workspace.metadata?.namespace || 'default';
558
+
559
+ const podDeleteManifest = {
560
+ apiVersion: 'v1',
561
+ kind: 'Pod',
562
+ metadata: { name: `codespace-${wsName}`, namespace },
563
+ action: 'delete',
564
+ };
565
+
566
+ const serviceDeleteManifest = {
567
+ apiVersion: 'v1',
568
+ kind: 'Service',
569
+ metadata: { name: `codespace-svc-${wsName}`, namespace },
570
+ action: 'delete',
571
+ };
572
+
573
+ return { error: false, podDeleteManifest, serviceDeleteManifest };
574
+ },
575
+
576
+ getCodespaceStatus(workspace, podStatus = null) {
577
+ if (!workspace) {
578
+ return { error: true, reason: 'missing-workspace', message: 'workspace is required' };
579
+ }
580
+
581
+ const wsName = workspace.metadata?.name || 'unknown';
582
+ const namespace = workspace.metadata?.namespace || 'default';
583
+ const running = podStatus?.phase === 'Running';
584
+ const port = 8080;
585
+ const serviceName = `codespace-svc-${wsName}`;
586
+ const url = running ? `http://${serviceName}.${namespace}.svc.cluster.local:${port}` : null;
587
+
588
+ return {
589
+ error: false,
590
+ running,
591
+ url,
592
+ port,
593
+ uptime: podStatus?.startTime ? new Date().toISOString() : null,
594
+ startTime: podStatus?.startTime || null,
595
+ connectedUsers: podStatus?.connectedUsers || 0,
596
+ phase: podStatus?.phase || 'Unknown',
597
+ };
598
+ },
599
+
600
+ // --- Workspace associations ---
601
+
602
+ addAssociation(workspace, ref) {
603
+ if (!workspace) {
604
+ return { error: true, reason: 'missing-workspace', message: 'workspace is required' };
605
+ }
606
+ if (!ref || !ref.kind || !ref.name) {
607
+ return { error: true, reason: 'invalid-ref', message: 'ref must have kind and name' };
608
+ }
609
+ const validKinds = ['AgentDispatchRun', 'User', 'AgentSession'];
610
+ if (!validKinds.includes(ref.kind)) {
611
+ return { error: true, reason: 'invalid-ref-kind', message: `ref.kind must be one of: ${validKinds.join(', ')}` };
612
+ }
613
+
614
+ const updated = clone(workspace);
615
+ if (!updated.spec) updated.spec = {};
616
+ if (!Array.isArray(updated.spec.associations)) updated.spec.associations = [];
617
+
618
+ // Prevent duplicates
619
+ const exists = updated.spec.associations.some(
620
+ (a) => a.kind === ref.kind && a.name === ref.name
621
+ );
622
+ if (exists) {
623
+ return { error: true, reason: 'duplicate-association', message: `Association ${ref.kind}/${ref.name} already exists` };
624
+ }
625
+
626
+ updated.spec.associations.push({
627
+ kind: ref.kind,
628
+ name: ref.name,
629
+ addedAt: new Date().toISOString(),
630
+ });
631
+
632
+ return { error: false, workspace: updated };
633
+ },
634
+
635
+ removeAssociation(workspace, ref) {
636
+ if (!workspace) {
637
+ return { error: true, reason: 'missing-workspace', message: 'workspace is required' };
638
+ }
639
+ if (!ref || !ref.kind || !ref.name) {
640
+ return { error: true, reason: 'invalid-ref', message: 'ref must have kind and name' };
641
+ }
642
+
643
+ const updated = clone(workspace);
644
+ if (!updated.spec) updated.spec = {};
645
+ if (!Array.isArray(updated.spec.associations)) updated.spec.associations = [];
646
+
647
+ const before = updated.spec.associations.length;
648
+ updated.spec.associations = updated.spec.associations.filter(
649
+ (a) => !(a.kind === ref.kind && a.name === ref.name)
650
+ );
651
+
652
+ if (updated.spec.associations.length === before) {
653
+ return { error: true, reason: 'not-found', message: `Association ${ref.kind}/${ref.name} not found` };
654
+ }
655
+
656
+ return { error: false, workspace: updated };
657
+ },
658
+
659
+ listAssociations(workspace) {
660
+ if (!workspace) {
661
+ return { error: true, reason: 'missing-workspace', message: 'workspace is required' };
662
+ }
663
+
664
+ const associations = workspace.spec?.associations || [];
665
+ return { error: false, associations: clone(associations) };
666
+ },
667
+
668
+ // --- Run history ---
669
+
670
+ getWorkspaceRuns(workspace, allRuns = []) {
671
+ if (!workspace) {
672
+ return { error: true, reason: 'missing-workspace', message: 'workspace is required' };
673
+ }
674
+
675
+ const wsName = workspace.metadata?.name;
676
+ const active = [];
677
+ const history = [];
678
+
679
+ for (const run of allRuns) {
680
+ const refersToWs =
681
+ run.status?.workspaceRef === wsName ||
682
+ run.spec?.workspaceRef === wsName ||
683
+ (workspace.spec?.associations || []).some(
684
+ (a) => a.kind === 'AgentDispatchRun' && a.name === run.metadata?.name
685
+ );
686
+
687
+ if (!refersToWs) continue;
688
+
689
+ const phase = run.status?.phase || 'Unknown';
690
+ const isActive = phase === 'Running' || phase === 'Queued' || phase === 'Pending' || phase === 'Dispatched';
691
+
692
+ if (isActive) {
693
+ active.push(clone(run));
694
+ } else {
695
+ history.push(clone(run));
696
+ }
697
+ }
698
+
699
+ return { error: false, active, history };
700
+ }
701
+ };
702
+ }