@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,179 @@
1
+ import { giteaIssueSyncPlan, giteaRepositoryIntegrationPlan, orgMemoryRepositoryName } from './gitea-backend.js';
2
+ import { clone, createResource } from './resource-model.js';
3
+
4
+
5
+ export function createDefaultGiteaGitBackend({ baseUrl = 'http://krate-gitea-http:3000', sshDomain = 'krate-gitea-ssh', owner = 'krate', webhookBaseUrl = 'http://krate-webhook-worker' } = {}) {
6
+ return { type: 'gitea', baseUrl: baseUrl.replace(/\/$/, ''), sshDomain, owner, webhookBaseUrl: webhookBaseUrl.replace(/\/$/, '') };
7
+ }
8
+
9
+ export function createGiteaRepositoryHosting({ backend = createDefaultGiteaGitBackend(), namespace = 'default', repository, branch = 'main' }) {
10
+ const owner = backend.owner || namespace;
11
+ const httpUrl = `${backend.baseUrl}/${owner}/${repository}.git`;
12
+ const sshUrl = `ssh://git@${backend.sshDomain}/${owner}/${repository}.git`;
13
+ const webhookUrl = `${backend.webhookBaseUrl}/repositories/${namespace}/${repository}`;
14
+ return {
15
+ backend: 'gitea',
16
+ owner,
17
+ repository,
18
+ branch,
19
+ httpUrl,
20
+ sshUrl,
21
+ deployKeyTitle: 'krate-argocd',
22
+ organization: { kind: 'Organization', name: owner, delegatedTo: 'Gitea /api/v1/orgs' },
23
+ sshKeys: { kind: 'SSHKey', scopes: ['user', 'deploy', 'argocd'], delegatedTo: 'Gitea /api/v1/user/keys and /repos/{owner}/{repo}/keys' },
24
+ permissions: { kind: 'RepositoryPermission', defaultCollaborator: 'write', adminTeam: 'maintainers', delegatedTo: 'Gitea collaborators and team repository APIs' },
25
+ forgeRecords: { issues: `Gitea /repos/${owner}/${orgMemoryRepositoryName(namespace)}/issues`, pullRequests: 'Gitea /repos/{owner}/{repo}/pulls' },
26
+ issueSync: giteaIssueSyncPlan({ org: namespace, repositories: [repository] }),
27
+ webhookUrl,
28
+ integrationPlan: giteaRepositoryIntegrationPlan({ owner, repo: repository, deployKeyTitle: 'krate-argocd', permission: 'write', branch, webhookUrl })
29
+ };
30
+ }
31
+
32
+ export function stableRepositoryStoreIndex(repositoryName, storeCount) {
33
+ let hash = 0;
34
+ for (const character of repositoryName) hash = (hash * 31 + character.charCodeAt(0)) >>> 0;
35
+ return hash % storeCount;
36
+ }
37
+
38
+ export class GiteaRepositoryStore {
39
+ constructor({ name = 'gitea-primary', receivePackReady = true, capacity = 1000 } = {}) {
40
+ this.name = name;
41
+ this.receivePackReady = receivePackReady;
42
+ this.capacity = capacity;
43
+ this.repositories = new Map();
44
+ this.objects = new Map();
45
+ this.searchIndex = new Map();
46
+ }
47
+
48
+ assign(repository) { this.repositories.set(repository.metadata.name, repository); return this; }
49
+ isReceivePackReady() { return this.receivePackReady; }
50
+
51
+ putObject(repository, object) {
52
+ const objects = this.objects.get(repository) || [];
53
+ objects.push(object);
54
+ this.objects.set(repository, objects);
55
+ return object;
56
+ }
57
+
58
+ index(repository, entry) {
59
+ const entries = this.searchIndex.get(repository) || [];
60
+ entries.push(entry);
61
+ this.searchIndex.set(repository, entries);
62
+ return entry;
63
+ }
64
+
65
+ snapshot() {
66
+ return {
67
+ name: this.name,
68
+ receivePackReady: this.receivePackReady,
69
+ capacity: this.capacity,
70
+ repositories: Object.fromEntries([...this.repositories.entries()].map(([name, resource]) => [name, clone(resource)])),
71
+ objects: Object.fromEntries([...this.objects.entries()].map(([repository, objects]) => [repository, clone(objects)])),
72
+ searchIndex: Object.fromEntries([...this.searchIndex.entries()].map(([repository, entries]) => [repository, clone(entries)]))
73
+ };
74
+ }
75
+
76
+ importSnapshot(snapshot = {}) {
77
+ this.receivePackReady = snapshot.receivePackReady ?? this.receivePackReady;
78
+ this.capacity = snapshot.capacity ?? this.capacity;
79
+ this.repositories = new Map(Object.entries(snapshot.repositories || {}).map(([name, resource]) => [name, clone(resource)]));
80
+ this.objects = new Map(Object.entries(snapshot.objects || {}).map(([repository, objects]) => [repository, clone(objects)]));
81
+ this.searchIndex = new Map(Object.entries(snapshot.searchIndex || {}).map(([repository, entries]) => [repository, clone(entries)]));
82
+ return this;
83
+ }
84
+ }
85
+
86
+ export class GiteaGitService {
87
+ constructor({ controlPlane, stores = [new GiteaRepositoryStore()], gitBackend = createDefaultGiteaGitBackend() }) {
88
+ if (!controlPlane) throw new Error('GiteaGitService requires a controlPlane');
89
+ this.controlPlane = controlPlane;
90
+ this.stores = stores;
91
+ this.gitBackend = gitBackend.type === 'gitea' ? gitBackend : createDefaultGiteaGitBackend(gitBackend);
92
+ this.integrationPlans = new Map();
93
+ }
94
+
95
+ snapshot() {
96
+ return {
97
+ apiVersion: 'krate.a5c.ai/v1alpha1',
98
+ kind: 'GiteaGitServiceSnapshot',
99
+ backend: clone(this.gitBackend),
100
+ integrationPlans: Object.fromEntries([...this.integrationPlans.entries()].map(([repository, plan]) => [repository, clone(plan)])),
101
+ stores: this.stores.map((store) => store.snapshot())
102
+ };
103
+ }
104
+
105
+ importSnapshot(snapshot = {}) {
106
+ if (snapshot.backend?.type === 'gitea') this.gitBackend = clone(snapshot.backend);
107
+ this.integrationPlans = new Map(Object.entries(snapshot.integrationPlans || {}).map(([repository, plan]) => [repository, clone(plan)]));
108
+ if (!Array.isArray(snapshot.stores)) return this;
109
+ this.stores = snapshot.stores.map((storeSnapshot) => new GiteaRepositoryStore({
110
+ name: storeSnapshot.name,
111
+ receivePackReady: storeSnapshot.receivePackReady,
112
+ capacity: storeSnapshot.capacity
113
+ }).importSnapshot(storeSnapshot));
114
+ return this;
115
+ }
116
+
117
+ route(repositoryName) {
118
+ const store = this.stores[stableRepositoryStoreIndex(repositoryName, this.stores.length)];
119
+ const owner = this.gitBackend.owner || 'krate';
120
+ return { repositoryName, backend: 'gitea', owner, store: store.name, receivePackReady: store.isReceivePackReady(), httpUrl: `${this.gitBackend.baseUrl}/${owner}/${repositoryName}.git`, sshUrl: `ssh://git@${this.gitBackend.sshDomain}/${owner}/${repositoryName}.git` };
121
+ }
122
+
123
+ createRepository({ name, namespace = 'krate-org-default', organizationRef = 'default', visibility = 'private' }, user) {
124
+ const route = this.route(name);
125
+ const hosting = createGiteaRepositoryHosting({ backend: this.gitBackend, namespace, repository: name });
126
+ const repository = createResource('Repository', { name, namespace, labels: { gitBackend: 'gitea' } }, {
127
+ organizationRef,
128
+ visibility,
129
+ gitHosting: hosting,
130
+ storage: { mode: 'gitea', persistentVolumeClaim: 'krate-gitea-data', owner: hosting.owner, repository: name, httpUrl: hosting.httpUrl, sshUrl: hosting.sshUrl },
131
+ objectStorage: { lfs: true, artifacts: true },
132
+ search: { provider: 'zoekt', enabled: false }
133
+ }, { ready: true, route: { ...route, backend: 'gitea' }, gitHosting: { backend: 'gitea', httpUrl: hosting.httpUrl, sshUrl: hosting.sshUrl, integrationPlan: hosting.integrationPlan } });
134
+ const created = this.controlPlane.create(repository, user);
135
+ this.integrationPlans.set(name, hosting.integrationPlan);
136
+ this.stores.find((store) => store.name === route.store).assign(created);
137
+ return created;
138
+ }
139
+
140
+ receivePack({ repository, namespace = 'krate-org-default', ref, oldRev = '0'.repeat(40), newRev, actor }) {
141
+ const route = this.route(repository);
142
+ if (!route.receivePackReady) throw new Error(`receive-pack for ${repository} is not ready`);
143
+ const refPolicies = this.controlPlane.list('RefPolicy', { namespace }).items;
144
+ const branchProtections = this.controlPlane.list('BranchProtection', { namespace }).items;
145
+ const deniedByPolicy = refPolicies.find((policy) => (policy.spec.deny || []).some((prefix) => ref.startsWith(prefix)));
146
+ if (deniedByPolicy) throw new Error(`RefPolicy ${deniedByPolicy.metadata.name} denied ${ref}`);
147
+ const protectedBranch = branchProtections.find((policy) => (policy.spec.refs || []).includes(ref));
148
+ const isRepoAdmin = actor.groups?.includes('krate:repo-admins') || actor.groups?.includes('krate:platform-engineers');
149
+ if (protectedBranch && protectedBranch.spec.requirePullRequest && !isRepoAdmin) {
150
+ throw new Error(`BranchProtection ${protectedBranch.metadata.name} requires pull request for ${ref}`);
151
+ }
152
+ const event = { repository, namespace, ref, oldRev, newRev, actor: actor.name, backend: 'gitea', store: route.store, remoteUrl: route.httpUrl, at: new Date().toISOString() };
153
+ this.controlPlane.events.push({ type: 'git.receive-pack', resource: event, storage: 'gitea' });
154
+ return event;
155
+ }
156
+
157
+ uploadPack({ repository }) {
158
+ const route = this.route(repository);
159
+ return { repository, backend: 'gitea', store: route.store, remoteUrl: route.httpUrl, cacheable: true };
160
+ }
161
+
162
+ recordObject({ repository, namespace = 'default', key, size, mediaType = 'application/octet-stream' }) {
163
+ const route = this.route(repository);
164
+ const store = this.stores.find((candidate) => candidate.name === route.store);
165
+ const object = { repository, namespace, key, size, mediaType, store: route.store, storage: 'object-storage', at: new Date().toISOString() };
166
+ store.putObject(repository, object);
167
+ this.controlPlane.events.push({ type: 'git.object-recorded', resource: object, storage: 'object-storage' });
168
+ return object;
169
+ }
170
+
171
+ enqueueSearchIndex({ repository, namespace = 'default', commit, paths = [] }) {
172
+ const route = this.route(repository);
173
+ const store = this.stores.find((candidate) => candidate.name === route.store);
174
+ const entry = { repository, namespace, commit, paths, store: route.store, provider: 'zoekt', status: 'queued', at: new Date().toISOString() };
175
+ store.index(repository, entry);
176
+ this.controlPlane.events.push({ type: 'search.index-queued', resource: entry, storage: 'search-index' });
177
+ return entry;
178
+ }
179
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Module-level event bus for SSE real-event streaming.
3
+ * Provides a pub/sub mechanism for resource change events.
4
+ */
5
+
6
+ /**
7
+ * Creates a new event bus with subscribe, unsubscribe, and emit methods.
8
+ * @returns {{ subscribe: Function, unsubscribe: Function, emit: Function, emitResourceChange: Function }}
9
+ */
10
+ export function createEventBus() {
11
+ const listeners = new Set();
12
+
13
+ return {
14
+ /**
15
+ * Subscribe a listener function to receive emitted events.
16
+ * @param {Function} fn - listener receiving the event object
17
+ */
18
+ subscribe(fn) {
19
+ listeners.add(fn);
20
+ },
21
+
22
+ /**
23
+ * Remove a previously subscribed listener.
24
+ * @param {Function} fn - the listener to remove
25
+ */
26
+ unsubscribe(fn) {
27
+ listeners.delete(fn);
28
+ },
29
+
30
+ /**
31
+ * Emit an event to all current subscribers.
32
+ * @param {object} event - the event payload to broadcast
33
+ */
34
+ emit(event) {
35
+ for (const fn of listeners) {
36
+ fn(event);
37
+ }
38
+ },
39
+
40
+ /**
41
+ * Emit a resource-change event with kind, name, operation, and timestamp.
42
+ * @param {string} kind - resource kind (e.g. 'Repository')
43
+ * @param {string} name - resource name
44
+ * @param {string} operation - operation performed (e.g. 'apply', 'delete')
45
+ */
46
+ emitResourceChange(kind, name, operation) {
47
+ this.emit({
48
+ type: 'resource-change',
49
+ kind,
50
+ name,
51
+ operation,
52
+ timestamp: new Date().toISOString()
53
+ });
54
+ }
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Module-level singleton event bus shared across the HTTP server and API controller.
60
+ */
61
+ export const globalEventBus = createEventBus();
@@ -0,0 +1,225 @@
1
+ // External Conflict Controller — Slice 3.5
2
+ // Detects field divergence between local Krate state and external provider state,
3
+ // manages resolution workflows, and handles superseded conflict cleanup.
4
+
5
+ import { createResource, clone } from '../resource-model.js';
6
+
7
+ export const CONFLICT_CONTROLLER_BOUNDARY = {
8
+ role: 'external-conflict-controller',
9
+ scope: 'Field-level conflict detection and resolution for ExternalSyncConflict resources',
10
+ owns: ['conflict detection', 'resolution workflow', 'superseded cleanup', 'open conflict listing'],
11
+ delegatesTo: ['resource-model'],
12
+ mustNotOwn: ['write intent lifecycle', 'sync scheduling', 'external API client']
13
+ };
14
+
15
+ const VALID_STRATEGIES = ['prefer-external', 'prefer-krate', 'manual', 'ignore'];
16
+ const OPEN_PHASES = new Set(['Open']);
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Validation
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * Validate a conflict detection input object.
24
+ *
25
+ * @param {object} input
26
+ * @returns {{ valid: boolean, errors: string[] }}
27
+ */
28
+ export function validateConflict(input) {
29
+ const errors = [];
30
+ if (!input) {
31
+ return { valid: false, errors: ['input must not be null or undefined'] };
32
+ }
33
+ if (!input.resourceRef || typeof input.resourceRef !== 'string') {
34
+ errors.push('resourceRef is required and must be a non-empty string');
35
+ }
36
+ if (!input.fieldPath || typeof input.fieldPath !== 'string') {
37
+ errors.push('fieldPath is required and must be a non-empty string');
38
+ }
39
+ return { valid: errors.length === 0, errors };
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Controller factory
44
+ // ---------------------------------------------------------------------------
45
+
46
+ /**
47
+ * Create a ConflictController that manages ExternalSyncConflict resources.
48
+ *
49
+ * @param {{ persistFn?: (resource: object) => Promise<any> }} [opts]
50
+ * Optional persistFn is called (fire-and-forget) after conflict state changes.
51
+ * @returns {object}
52
+ */
53
+ export function createConflictController({ persistFn } = {}) {
54
+ /**
55
+ * Fire-and-forget persistence helper.
56
+ * @param {object} resource
57
+ */
58
+ function persist(resource) {
59
+ if (typeof persistFn === 'function') {
60
+ Promise.resolve(persistFn(resource)).catch(() => {});
61
+ }
62
+ }
63
+
64
+ return {
65
+ role: 'conflict-controller',
66
+
67
+ /**
68
+ * Detect a conflict between local and external field values.
69
+ * Returns { conflict: null } when values match (no conflict).
70
+ * Returns { conflict: ExternalSyncConflict } when values differ.
71
+ *
72
+ * @param {{ resourceRef, fieldPath, localValue, externalValue, namespace?, organizationRef? }} input
73
+ * @returns {{ conflict: object|null }}
74
+ */
75
+ detectConflict({
76
+ resourceRef,
77
+ fieldPath,
78
+ localValue,
79
+ externalValue,
80
+ namespace = 'default',
81
+ organizationRef = 'default'
82
+ }) {
83
+ const validation = validateConflict({ resourceRef, fieldPath });
84
+ if (!validation.valid) {
85
+ return { conflict: null, error: true, message: validation.errors.join('; ') };
86
+ }
87
+
88
+ // Values match — no conflict
89
+ if (localValue === externalValue) {
90
+ return { conflict: null };
91
+ }
92
+
93
+ const now = new Date().toISOString();
94
+ const conflictName = `conflict-${resourceRef.replace(/[^a-zA-Z0-9]/g, '-')}-${fieldPath.replace(/[^a-zA-Z0-9]/g, '-')}-${Date.now()}`;
95
+
96
+ const conflict = createResource('ExternalSyncConflict', { name: conflictName, namespace }, {
97
+ organizationRef,
98
+ resourceRef,
99
+ fieldPath,
100
+ localValue,
101
+ externalValue,
102
+ detectedAt: now
103
+ });
104
+ conflict.status = {
105
+ phase: 'Open',
106
+ detectedAt: now
107
+ };
108
+
109
+ persist(conflict);
110
+ return { conflict };
111
+ },
112
+
113
+ /**
114
+ * Resolve an Open conflict using the specified strategy.
115
+ *
116
+ * Strategies:
117
+ * - prefer-external: choose externalValue, phase → Resolved
118
+ * - prefer-krate: choose localValue, phase → Resolved
119
+ * - manual: choose resolvedValue, phase → Resolved (requires resolvedValue)
120
+ * - ignore: phase → Ignored (no value chosen)
121
+ *
122
+ * @param {{ conflictName, strategy, resolvedValue?, resources }} opts
123
+ * @returns {{ conflict: object, resolution: object } | { error: true, message: string }}
124
+ */
125
+ resolveConflict({ conflictName, strategy, resolvedValue, resources = {} }) {
126
+ if (!conflictName) {
127
+ return { error: true, reason: 'missing-name', message: 'conflictName is required' };
128
+ }
129
+ if (!strategy || !VALID_STRATEGIES.includes(strategy)) {
130
+ return {
131
+ error: true,
132
+ reason: 'invalid-strategy',
133
+ message: `strategy must be one of: ${VALID_STRATEGIES.join(', ')}`
134
+ };
135
+ }
136
+
137
+ const conflicts = resources.ExternalSyncConflict || [];
138
+ const found = conflicts.find((c) => c.metadata?.name === conflictName);
139
+ if (!found) {
140
+ return { error: true, reason: 'not-found', message: `ExternalSyncConflict not found: ${conflictName}` };
141
+ }
142
+ if (found.status?.phase !== 'Open') {
143
+ return { error: true, reason: 'invalid-phase', message: `Conflict is not Open: ${found.status?.phase}` };
144
+ }
145
+
146
+ const updated = clone(found);
147
+ const now = new Date().toISOString();
148
+ let chosenValue;
149
+ let newPhase;
150
+
151
+ if (strategy === 'ignore') {
152
+ newPhase = 'Ignored';
153
+ chosenValue = undefined;
154
+ } else {
155
+ newPhase = 'Resolved';
156
+ if (strategy === 'prefer-external') {
157
+ chosenValue = found.spec.externalValue;
158
+ } else if (strategy === 'prefer-krate') {
159
+ chosenValue = found.spec.localValue;
160
+ } else if (strategy === 'manual') {
161
+ if (resolvedValue === undefined) {
162
+ return { error: true, reason: 'missing-resolved-value', message: 'resolvedValue is required for manual strategy' };
163
+ }
164
+ chosenValue = resolvedValue;
165
+ }
166
+ }
167
+
168
+ updated.status = {
169
+ ...updated.status,
170
+ phase: newPhase,
171
+ resolvedAt: now,
172
+ strategy,
173
+ chosenValue
174
+ };
175
+
176
+ const resolution = { strategy, chosenValue };
177
+
178
+ persist(updated);
179
+ return { conflict: updated, resolution };
180
+ },
181
+
182
+ /**
183
+ * Mark all Open conflicts for a given (resourceRef, fieldPath) pair as Superseded.
184
+ * Used when a new sync event arrives that makes old conflicts irrelevant.
185
+ *
186
+ * @param {{ resourceRef, fieldPath, resources }} opts
187
+ * @returns {{ superseded: object[] }}
188
+ */
189
+ supersededCheck({ resourceRef, fieldPath, resources = {} }) {
190
+ const conflicts = resources.ExternalSyncConflict || [];
191
+ const now = new Date().toISOString();
192
+
193
+ const superseded = [];
194
+ for (const c of conflicts) {
195
+ if (
196
+ c.spec?.resourceRef === resourceRef &&
197
+ c.spec?.fieldPath === fieldPath &&
198
+ c.status?.phase === 'Open'
199
+ ) {
200
+ const updated = clone(c);
201
+ updated.status = {
202
+ ...updated.status,
203
+ phase: 'Superseded',
204
+ supersededAt: now
205
+ };
206
+ superseded.push(updated);
207
+ }
208
+ }
209
+
210
+ return { superseded };
211
+ },
212
+
213
+ /**
214
+ * Return all Open (non-resolved, non-ignored, non-superseded) conflicts.
215
+ *
216
+ * @param {{ resources }} opts
217
+ * @returns {{ conflicts: object[] }}
218
+ */
219
+ getOpenConflicts({ resources = {} } = {}) {
220
+ const conflicts = resources.ExternalSyncConflict || [];
221
+ const open = conflicts.filter((c) => OPEN_PHASES.has(c.status?.phase));
222
+ return { conflicts: open };
223
+ }
224
+ };
225
+ }
@@ -0,0 +1,96 @@
1
+ // GitHub App authentication helpers — Slice 3.3a
2
+ // Provides JWT signing (HMAC-SHA256 for test, RSA-SHA256 for production)
3
+ // and installation token exchange. No external dependencies; uses node:crypto.
4
+
5
+ import { createHmac, createSign } from 'node:crypto';
6
+
7
+ const GITHUB_API = 'https://api.github.com';
8
+
9
+ /**
10
+ * Encode a value as Base64url (RFC 4648 §5, no padding).
11
+ * @param {string|Buffer} data
12
+ * @returns {string}
13
+ */
14
+ function b64url(data) {
15
+ const buf = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
16
+ return buf.toString('base64url');
17
+ }
18
+
19
+ /**
20
+ * Create a GitHub App JWT.
21
+ *
22
+ * In production, pass a PEM-encoded RSA private key and the function will
23
+ * use RS256. For unit tests, pass any string key; if it does not look like
24
+ * a PEM file the function falls back to HS256 (HMAC-SHA256) so tests can
25
+ * run without real RSA keys.
26
+ *
27
+ * @param {{ appId: string, privateKey: string, expiresInSeconds?: number }} opts
28
+ * @returns {Promise<string>} A signed JWT string.
29
+ */
30
+ export async function createGitHubJwt({ appId, privateKey, expiresInSeconds = 600 } = {}) {
31
+ if (!appId) throw new Error('createGitHubJwt: appId is required');
32
+ if (!privateKey) throw new Error('createGitHubJwt: privateKey is required');
33
+
34
+ const now = Math.floor(Date.now() / 1000);
35
+ const isRsa = privateKey.includes('-----BEGIN');
36
+
37
+ const alg = isRsa ? 'RS256' : 'HS256';
38
+
39
+ const header = b64url(JSON.stringify({ alg, typ: 'JWT' }));
40
+ const payload = b64url(JSON.stringify({
41
+ iat: now,
42
+ exp: now + expiresInSeconds,
43
+ iss: appId
44
+ }));
45
+
46
+ const signingInput = `${header}.${payload}`;
47
+
48
+ let signature;
49
+ if (isRsa) {
50
+ const sign = createSign('RSA-SHA256');
51
+ sign.update(signingInput);
52
+ sign.end();
53
+ signature = sign.sign(privateKey, 'base64url');
54
+ } else {
55
+ // HMAC-SHA256 fallback (test mode only)
56
+ const hmac = createHmac('sha256', privateKey);
57
+ hmac.update(signingInput);
58
+ signature = hmac.digest('base64url');
59
+ }
60
+
61
+ return `${signingInput}.${signature}`;
62
+ }
63
+
64
+ /**
65
+ * Exchange a GitHub App JWT for an installation access token.
66
+ *
67
+ * @param {{ appJwt: string, installationId: string|number, fetchImpl?: Function }} opts
68
+ * @returns {Promise<{ token: string, expiresAt: string }>}
69
+ */
70
+ export async function exchangeInstallationToken({ appJwt, installationId, fetchImpl = globalThis.fetch } = {}) {
71
+ if (!appJwt) throw new Error('exchangeInstallationToken: appJwt is required');
72
+ if (!installationId) throw new Error('exchangeInstallationToken: installationId is required');
73
+ if (!fetchImpl) throw new Error('exchangeInstallationToken: a fetch implementation is required');
74
+
75
+ const url = `${GITHUB_API}/app/installations/${installationId}/access_tokens`;
76
+
77
+ const response = await fetchImpl(url, {
78
+ method: 'POST',
79
+ headers: {
80
+ Accept: 'application/vnd.github+json',
81
+ Authorization: `Bearer ${appJwt}`,
82
+ 'X-GitHub-Api-Version': '2022-11-28',
83
+ 'Content-Type': 'application/json'
84
+ }
85
+ });
86
+
87
+ if (!response.ok) {
88
+ throw new Error(`exchangeInstallationToken: GitHub API returned ${response.status} — authentication or token exchange failed`);
89
+ }
90
+
91
+ const data = await response.json();
92
+ return {
93
+ token: data.token,
94
+ expiresAt: data.expires_at
95
+ };
96
+ }