@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,178 @@
1
+ import { giteaRepositoryIntegrationPlan } 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}/{repo}/issues', pullRequests: 'Gitea /repos/{owner}/{repo}/pulls' },
26
+ webhookUrl,
27
+ integrationPlan: giteaRepositoryIntegrationPlan({ owner, repo: repository, deployKeyTitle: 'krate-argocd', permission: 'write', branch, webhookUrl })
28
+ };
29
+ }
30
+
31
+ export function stableRepositoryStoreIndex(repositoryName, storeCount) {
32
+ let hash = 0;
33
+ for (const character of repositoryName) hash = (hash * 31 + character.charCodeAt(0)) >>> 0;
34
+ return hash % storeCount;
35
+ }
36
+
37
+ export class GiteaRepositoryStore {
38
+ constructor({ name = 'gitea-primary', receivePackReady = true, capacity = 1000 } = {}) {
39
+ this.name = name;
40
+ this.receivePackReady = receivePackReady;
41
+ this.capacity = capacity;
42
+ this.repositories = new Map();
43
+ this.objects = new Map();
44
+ this.searchIndex = new Map();
45
+ }
46
+
47
+ assign(repository) { this.repositories.set(repository.metadata.name, repository); return this; }
48
+ isReceivePackReady() { return this.receivePackReady; }
49
+
50
+ putObject(repository, object) {
51
+ const objects = this.objects.get(repository) || [];
52
+ objects.push(object);
53
+ this.objects.set(repository, objects);
54
+ return object;
55
+ }
56
+
57
+ index(repository, entry) {
58
+ const entries = this.searchIndex.get(repository) || [];
59
+ entries.push(entry);
60
+ this.searchIndex.set(repository, entries);
61
+ return entry;
62
+ }
63
+
64
+ snapshot() {
65
+ return {
66
+ name: this.name,
67
+ receivePackReady: this.receivePackReady,
68
+ capacity: this.capacity,
69
+ repositories: Object.fromEntries([...this.repositories.entries()].map(([name, resource]) => [name, clone(resource)])),
70
+ objects: Object.fromEntries([...this.objects.entries()].map(([repository, objects]) => [repository, clone(objects)])),
71
+ searchIndex: Object.fromEntries([...this.searchIndex.entries()].map(([repository, entries]) => [repository, clone(entries)]))
72
+ };
73
+ }
74
+
75
+ importSnapshot(snapshot = {}) {
76
+ this.receivePackReady = snapshot.receivePackReady ?? this.receivePackReady;
77
+ this.capacity = snapshot.capacity ?? this.capacity;
78
+ this.repositories = new Map(Object.entries(snapshot.repositories || {}).map(([name, resource]) => [name, clone(resource)]));
79
+ this.objects = new Map(Object.entries(snapshot.objects || {}).map(([repository, objects]) => [repository, clone(objects)]));
80
+ this.searchIndex = new Map(Object.entries(snapshot.searchIndex || {}).map(([repository, entries]) => [repository, clone(entries)]));
81
+ return this;
82
+ }
83
+ }
84
+
85
+ export class GiteaGitService {
86
+ constructor({ controlPlane, stores = [new GiteaRepositoryStore()], gitBackend = createDefaultGiteaGitBackend() }) {
87
+ if (!controlPlane) throw new Error('GiteaGitService requires a controlPlane');
88
+ this.controlPlane = controlPlane;
89
+ this.stores = stores;
90
+ this.gitBackend = gitBackend.type === 'gitea' ? gitBackend : createDefaultGiteaGitBackend(gitBackend);
91
+ this.integrationPlans = new Map();
92
+ }
93
+
94
+ snapshot() {
95
+ return {
96
+ apiVersion: 'krate.a5c.ai/v1alpha1',
97
+ kind: 'GiteaGitServiceSnapshot',
98
+ backend: clone(this.gitBackend),
99
+ integrationPlans: Object.fromEntries([...this.integrationPlans.entries()].map(([repository, plan]) => [repository, clone(plan)])),
100
+ stores: this.stores.map((store) => store.snapshot())
101
+ };
102
+ }
103
+
104
+ importSnapshot(snapshot = {}) {
105
+ if (snapshot.backend?.type === 'gitea') this.gitBackend = clone(snapshot.backend);
106
+ this.integrationPlans = new Map(Object.entries(snapshot.integrationPlans || {}).map(([repository, plan]) => [repository, clone(plan)]));
107
+ if (!Array.isArray(snapshot.stores)) return this;
108
+ this.stores = snapshot.stores.map((storeSnapshot) => new GiteaRepositoryStore({
109
+ name: storeSnapshot.name,
110
+ receivePackReady: storeSnapshot.receivePackReady,
111
+ capacity: storeSnapshot.capacity
112
+ }).importSnapshot(storeSnapshot));
113
+ return this;
114
+ }
115
+
116
+ route(repositoryName) {
117
+ const store = this.stores[stableRepositoryStoreIndex(repositoryName, this.stores.length)];
118
+ const owner = this.gitBackend.owner || 'krate';
119
+ 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` };
120
+ }
121
+
122
+ createRepository({ name, namespace = 'krate-org-default', organizationRef = 'default', visibility = 'private' }, user) {
123
+ const route = this.route(name);
124
+ const hosting = createGiteaRepositoryHosting({ backend: this.gitBackend, namespace, repository: name });
125
+ const repository = createResource('Repository', { name, namespace, labels: { gitBackend: 'gitea' } }, {
126
+ organizationRef,
127
+ visibility,
128
+ gitHosting: hosting,
129
+ storage: { mode: 'gitea', persistentVolumeClaim: 'krate-gitea-data', owner: hosting.owner, repository: name, httpUrl: hosting.httpUrl, sshUrl: hosting.sshUrl },
130
+ objectStorage: { lfs: true, artifacts: true },
131
+ search: { provider: 'zoekt', enabled: false }
132
+ }, { ready: true, route: { ...route, backend: 'gitea' }, gitHosting: { backend: 'gitea', httpUrl: hosting.httpUrl, sshUrl: hosting.sshUrl, integrationPlan: hosting.integrationPlan } });
133
+ const created = this.controlPlane.create(repository, user);
134
+ this.integrationPlans.set(name, hosting.integrationPlan);
135
+ this.stores.find((store) => store.name === route.store).assign(created);
136
+ return created;
137
+ }
138
+
139
+ receivePack({ repository, namespace = 'krate-org-default', ref, oldRev = '0'.repeat(40), newRev, actor }) {
140
+ const route = this.route(repository);
141
+ if (!route.receivePackReady) throw new Error(`receive-pack for ${repository} is not ready`);
142
+ const refPolicies = this.controlPlane.list('RefPolicy', { namespace }).items;
143
+ const branchProtections = this.controlPlane.list('BranchProtection', { namespace }).items;
144
+ const deniedByPolicy = refPolicies.find((policy) => (policy.spec.deny || []).some((prefix) => ref.startsWith(prefix)));
145
+ if (deniedByPolicy) throw new Error(`RefPolicy ${deniedByPolicy.metadata.name} denied ${ref}`);
146
+ const protectedBranch = branchProtections.find((policy) => (policy.spec.refs || []).includes(ref));
147
+ const isRepoAdmin = actor.groups?.includes('krate:repo-admins') || actor.groups?.includes('krate:platform-engineers');
148
+ if (protectedBranch && protectedBranch.spec.requirePullRequest && !isRepoAdmin) {
149
+ throw new Error(`BranchProtection ${protectedBranch.metadata.name} requires pull request for ${ref}`);
150
+ }
151
+ const event = { repository, namespace, ref, oldRev, newRev, actor: actor.name, backend: 'gitea', store: route.store, remoteUrl: route.httpUrl, at: new Date().toISOString() };
152
+ this.controlPlane.events.push({ type: 'git.receive-pack', resource: event, storage: 'gitea' });
153
+ return event;
154
+ }
155
+
156
+ uploadPack({ repository }) {
157
+ const route = this.route(repository);
158
+ return { repository, backend: 'gitea', store: route.store, remoteUrl: route.httpUrl, cacheable: true };
159
+ }
160
+
161
+ recordObject({ repository, namespace = 'default', key, size, mediaType = 'application/octet-stream' }) {
162
+ const route = this.route(repository);
163
+ const store = this.stores.find((candidate) => candidate.name === route.store);
164
+ const object = { repository, namespace, key, size, mediaType, store: route.store, storage: 'object-storage', at: new Date().toISOString() };
165
+ store.putObject(repository, object);
166
+ this.controlPlane.events.push({ type: 'git.object-recorded', resource: object, storage: 'object-storage' });
167
+ return object;
168
+ }
169
+
170
+ enqueueSearchIndex({ repository, namespace = 'default', commit, paths = [] }) {
171
+ const route = this.route(repository);
172
+ const store = this.stores.find((candidate) => candidate.name === route.store);
173
+ const entry = { repository, namespace, commit, paths, store: route.store, provider: 'zoekt', status: 'queued', at: new Date().toISOString() };
174
+ store.index(repository, entry);
175
+ this.controlPlane.events.push({ type: 'search.index-queued', resource: entry, storage: 'search-index' });
176
+ return entry;
177
+ }
178
+ }
@@ -0,0 +1,95 @@
1
+ export function createGiteaBackend({ baseUrl = 'http://krate-gitea-http:3000', token, fetchImpl = globalThis.fetch } = {}) {
2
+ if (!fetchImpl) throw new Error('Gitea backend requires a fetch implementation');
3
+ const root = baseUrl.replace(/\/$/, '');
4
+
5
+ async function request(method, path, body) {
6
+ const response = await fetchImpl(`${root}/api/v1${path}`, {
7
+ method,
8
+ headers: {
9
+ Accept: 'application/json',
10
+ 'Content-Type': 'application/json',
11
+ ...(token ? { Authorization: `token ${token}` } : {})
12
+ },
13
+ ...(body === undefined ? {} : { body: JSON.stringify(body) })
14
+ });
15
+ if (!response.ok) throw new Error(`Gitea ${method} ${path} failed with ${response.status}`);
16
+ return response.status === 204 ? null : response.json();
17
+ }
18
+
19
+ return {
20
+ role: 'gitea-backend',
21
+ baseUrl: root,
22
+ createOrganization({ name, fullName = name, description = '', visibility = 'private' }) {
23
+ return request('POST', '/orgs', { username: name, full_name: fullName, description, visibility });
24
+ },
25
+ createUser({ username, email, fullName = username, password, mustChangePassword = true }) {
26
+ return request('POST', '/admin/users', { username, email, full_name: fullName, password, must_change_password: mustChangePassword });
27
+ },
28
+ editUser({ username, email, fullName, active = true, admin = false }) {
29
+ return request('PATCH', `/admin/users/${encodeURIComponent(username)}`, { email, full_name: fullName, active, admin });
30
+ },
31
+ addUserSshKey({ title, key, readOnly = false }) {
32
+ return request('POST', '/user/keys', { title, key, read_only: readOnly });
33
+ },
34
+ createRepository({ owner, name, private: isPrivate = true, defaultBranch = 'main', description = '' }) {
35
+ const path = owner ? `/orgs/${encodeURIComponent(owner)}/repos` : '/user/repos';
36
+ return request('POST', path, { name, private: isPrivate, default_branch: defaultBranch, description, auto_init: false });
37
+ },
38
+ addDeployKey({ owner, repo, title, key, readOnly = true }) {
39
+ return request('POST', `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/keys`, { title, key, read_only: readOnly });
40
+ },
41
+ addCollaborator({ owner, repo, username, permission = 'read' }) {
42
+ return request('PUT', `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(username)}`, { permission });
43
+ },
44
+ addTeamRepository({ org, team, repo, owner = org, permission = 'read' }) {
45
+ return request('PUT', `/teams/${encodeURIComponent(team)}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, { permission });
46
+ },
47
+ createTeam({ org, name, permission = 'read', units = ['repo.code', 'repo.pulls', 'repo.issues'] }) {
48
+ return request('POST', `/orgs/${encodeURIComponent(org)}/teams`, { name, permission, units });
49
+ },
50
+ addTeamMember({ team, username }) {
51
+ return request('PUT', `/teams/${encodeURIComponent(team)}/members/${encodeURIComponent(username)}`);
52
+ },
53
+ protectBranch({ owner, repo, branch = 'main', approvals = 1, statusChecks = [] }) {
54
+ return request('POST', `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/branch_protections`, {
55
+ branch_name: branch,
56
+ enable_push: false,
57
+ enable_push_whitelist: true,
58
+ required_approvals: approvals,
59
+ enable_status_check: statusChecks.length > 0,
60
+ status_check_contexts: statusChecks
61
+ });
62
+ },
63
+ createIssue({ owner, repo, title, body = '', labels = [], assignees = [] }) {
64
+ return request('POST', `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues`, { title, body, labels, assignees });
65
+ },
66
+ createPullRequest({ owner, repo, title, head, base = 'main', body = '' }) {
67
+ return request('POST', `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls`, { title, head, base, body });
68
+ },
69
+ createWebhook({ owner, repo, url, events = ['push', 'pull_request'], secret }) {
70
+ return request('POST', `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/hooks`, {
71
+ type: 'gitea',
72
+ active: true,
73
+ events,
74
+ config: { url, content_type: 'json', ...(secret ? { secret } : {}) }
75
+ });
76
+ }
77
+ };
78
+ }
79
+
80
+ export function giteaRepositoryIntegrationPlan({ owner, repo, deployKeyTitle = 'krate-gitops', permission = 'write', branch = 'main', webhookUrl }) {
81
+ return {
82
+ backend: 'gitea',
83
+ operations: [
84
+ { action: 'createOrganization', owner },
85
+ { action: 'createRepository', owner, repo },
86
+ { action: 'ensureUserMappings', owner },
87
+ { action: 'addDeployKey', owner, repo, title: deployKeyTitle, readOnly: false },
88
+ { action: 'addUserSshKey', owner, repo, title: 'developer key' },
89
+ { action: 'addCollaborator', owner, repo, permission },
90
+ { action: 'addTeamRepository', owner, repo, team: 'maintainers', permission: 'admin' },
91
+ { action: 'protectBranch', owner, repo, branch },
92
+ { action: 'createWebhook', owner, repo, url: webhookUrl }
93
+ ]
94
+ };
95
+ }
package/src/handoff.js ADDED
@@ -0,0 +1,98 @@
1
+ export const HANDOFF_COMMANDS = Object.freeze({
2
+ build: 'npm run build',
3
+ demo: 'npm run demo',
4
+ serve: 'npm run serve',
5
+ check: 'npm run check',
6
+ test: 'npm test',
7
+ smoke: 'npm run smoke',
8
+ validateDocs: 'npm run validate:docs',
9
+ e2e: 'npm run e2e',
10
+ packageCheck: 'npm run package:check',
11
+ setupMinikube: 'npm run setup:minikube -- --dry-run',
12
+ dev: 'npm run dev',
13
+ uiBuild: 'npm run ui:build',
14
+ uiValidate: 'npm run ui:validate'
15
+ });
16
+
17
+ export const HANDOFF_DOCS = Object.freeze([
18
+ 'docs/README.md',
19
+ 'docs/product-requirements.md',
20
+ 'docs/system-requirements.md',
21
+ 'docs/architecture-spec.md',
22
+ 'docs/user-stories.md',
23
+ 'docs/roadmap-mvp.md',
24
+ 'docs/local-minikube.md',
25
+ 'docs/install.md',
26
+ 'docs/ontology/README.md'
27
+ ]);
28
+
29
+ export function createKrateHandoffSummary(demo, { packageInfo = {}, generatedAt = new Date().toISOString() } = {}) {
30
+ const smoke = demo.smoke || { ok: true, assertions: [] };
31
+ return {
32
+ project: 'Krate',
33
+ description: 'Kubernetes-native forge runtime with Argo CD and Krate-managed repository hosting',
34
+ package: {
35
+ name: packageInfo.name || 'krate',
36
+ version: packageInfo.version || '0.1.0',
37
+ private: packageInfo.private !== false
38
+ },
39
+ entrypoints: {
40
+ library: './src/index.js',
41
+ cli: './bin/krate-demo.mjs',
42
+ runtimeServer: './bin/krate-server.mjs',
43
+ webPreview: './public/index.html',
44
+ nextUi: './apps/web',
45
+ generatedSummary: './dist/krate-summary.json',
46
+ lifecycleSnapshot: './dist/krate-lifecycle.json',
47
+ helmChart: './charts/krate',
48
+ minikubeSetup: './scripts/setup-minikube.mjs'
49
+ },
50
+ commands: { ...HANDOFF_COMMANDS },
51
+ docs: [...HANDOFF_DOCS],
52
+ components: demo.components || [],
53
+ lifecycle: demo.lifecycle || null,
54
+ resources: Object.fromEntries(Object.entries(demo.resources).map(([key, value]) => [key, value?.kind || 'workflow'])),
55
+ storage: demo.controlPlane.storageReport(),
56
+ excellentFlows: demo.ui.dashboard.excellentFlows,
57
+ agents: demo.agents ? {
58
+ stacks: demo.agents.stacks?.count || 0,
59
+ runs: demo.agents.runs?.count || 0,
60
+ activeRuns: demo.agents.runs?.active?.length || 0,
61
+ rules: demo.agents.rules?.count || 0,
62
+ sessions: demo.agents.sessions?.count || 0,
63
+ workspaces: demo.agents.workspaces?.count || 0,
64
+ pendingApprovals: demo.agents.approvals?.pending?.length || 0
65
+ } : null,
66
+ operations: { chartPackage: demo.operations.chartPackage, localSetup: demo.operations.localSetup },
67
+ releaseGates: demo.operations.releaseGates,
68
+ smoke: {
69
+ ok: smoke.ok,
70
+ assertions: smoke.assertions.map(([name, passed]) => ({ name, passed }))
71
+ },
72
+ generatedAt
73
+ };
74
+ }
75
+
76
+ export function formatHandoffSummary(summary) {
77
+ const lines = [
78
+ `${summary.project} ${summary.package.version} — ${summary.description}`,
79
+ '',
80
+ 'Entrypoints:',
81
+ ...Object.entries(summary.entrypoints).map(([name, target]) => `- ${name}: ${target}`),
82
+ '',
83
+ 'Commands:',
84
+ ...Object.entries(summary.commands).map(([name, command]) => `- ${name}: ${command}`),
85
+ '',
86
+ 'Storage boundary:',
87
+ `- etcd: ${summary.storage.etcd.join(', ')}`,
88
+ `- postgres: ${summary.storage.postgres.join(', ')}`,
89
+ '',
90
+ 'Excellent flows:',
91
+ ...summary.excellentFlows.map((flow) => `- ${flow}`),
92
+ '',
93
+ `Smoke: ${summary.smoke.ok ? 'pass' : 'fail'}`
94
+ ];
95
+ return `${lines.join('\n')}\n`;
96
+ }
97
+
98
+
@@ -0,0 +1,63 @@
1
+ import { createHmac } from 'node:crypto';
2
+ import { createResource } from './resource-model.js';
3
+
4
+ export class WebhookBus {
5
+ constructor({ controlPlane, secret = 'krate-dev-secret' }) { this.controlPlane = controlPlane; this.secret = secret; }
6
+
7
+ subscribe({ name, namespace = 'krate-org-default', organizationRef = 'default', url, events = ['pullrequest.created'], mode = 'active' }, user) {
8
+ return this.controlPlane.create(createResource('WebhookSubscription', { name, namespace }, {
9
+ organizationRef, url, events, signing: { algorithm: 'hmac-sha256', secretRef: `${name}-secret` }, mode
10
+ }, { ready: true }), user);
11
+ }
12
+
13
+ sign(payload) { return createHmac('sha256', this.secret).update(JSON.stringify(payload)).digest('hex'); }
14
+
15
+ deliver({ subscriptionName, namespace = 'krate-org-default', organizationRef = 'default', eventType, payload, response = { status: 202, body: 'accepted' } }, user) {
16
+ const subscription = this.controlPlane.get('WebhookSubscription', namespace, subscriptionName);
17
+ if (!subscription) throw new Error(`WebhookSubscription ${subscriptionName} not found`);
18
+ if (!subscription.spec.events.includes(eventType)) throw new Error(`${subscriptionName} does not subscribe to ${eventType}`);
19
+ const delivery = createResource('WebhookDelivery', {
20
+ name: `${subscriptionName}-${Date.now()}-${this.controlPlane.list('WebhookDelivery', { namespace }).items.length + 1}`,
21
+ namespace,
22
+ labels: { subscription: subscriptionName, eventType }
23
+ }, {
24
+ organizationRef: subscription.spec.organizationRef || organizationRef,
25
+ subscription: subscriptionName,
26
+ url: subscription.spec.url,
27
+ eventType,
28
+ payload,
29
+ signature: this.sign(payload),
30
+ replayOf: payload.replayOf || null
31
+ }, {
32
+ phase: response.status >= 200 && response.status < 300 ? 'Delivered' : 'Failed',
33
+ response,
34
+ attempts: 1
35
+ });
36
+ return this.controlPlane.create(delivery, user);
37
+ }
38
+
39
+ replay(delivery, user) {
40
+ return this.deliver({
41
+ subscriptionName: delivery.spec.subscription,
42
+ namespace: delivery.metadata.namespace,
43
+ organizationRef: delivery.spec.organizationRef,
44
+ eventType: delivery.spec.eventType,
45
+ payload: { ...delivery.spec.payload, replayOf: delivery.metadata.name },
46
+ response: { status: 202, body: 'replayed' }
47
+ }, user);
48
+ }
49
+
50
+ inspect(delivery) {
51
+ return {
52
+ name: delivery.metadata.name,
53
+ subscription: delivery.spec.subscription,
54
+ eventType: delivery.spec.eventType,
55
+ phase: delivery.status.phase,
56
+ attempts: delivery.status.attempts,
57
+ response: delivery.status.response,
58
+ signature: delivery.spec.signature,
59
+ replayOf: delivery.spec.replayOf,
60
+ replayable: Boolean(delivery.spec.subscription && delivery.spec.eventType)
61
+ };
62
+ }
63
+ }
@@ -0,0 +1,151 @@
1
+ import { createServer } from 'node:http';
2
+ import { createKrateRuntime } from './runtime.js';
3
+ import { createControllerUiModel } from './controller-ui.js';
4
+ import { createKrateApiController } from './api-controller.js';
5
+ import { createKubernetesResourceGateway } from './kubernetes-resource-gateway.js';
6
+ import { orgNamespaceName } from './kubernetes-controller.js';
7
+
8
+ const jsonHeaders = { 'content-type': 'application/json; charset=utf-8' };
9
+
10
+ export function createKrateHttpHandler({ runtime = createKrateRuntime(), controller = createKrateApiController({ resourceGateway: createKubernetesResourceGateway() }) } = {}) {
11
+ return async function handleKrateRequest(request, response) {
12
+ try {
13
+ const url = new URL(request.url || '/', 'http://localhost');
14
+ if (request.method === 'GET' && url.pathname === '/healthz') return send(response, 200, { ok: true, project: 'Krate' });
15
+ if (request.method === 'GET' && url.pathname === '/api/controller') return send(response, 200, createControllerUiModel(await controller.snapshot(), { organization: url.searchParams.get('org') || undefined }));
16
+ if (url.pathname === '/api/orgs') {
17
+ if (request.method === 'GET') return send(response, 200, { organizations: createControllerUiModel(await controller.snapshot()).orgs });
18
+ if (request.method === 'POST') return send(response, 201, await controller.createOrganization(await readJson(request)));
19
+ }
20
+ const orgResourceCollectionMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/resources$/);
21
+ if (orgResourceCollectionMatch) {
22
+ const org = orgResourceCollectionMatch[1];
23
+ const scopedController = createKrateApiController({ namespace: orgNamespaceName(org) });
24
+ if (request.method === 'GET') return send(response, 200, await scopedController.listResource(url.searchParams.get('kind') || 'Repository'));
25
+ if (request.method === 'POST') return send(response, 201, await scopedController.applyResource(scopeResource(await readJson(request), org)));
26
+ }
27
+ const orgResourceMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/resources\/([^/]+)\/([^/]+)$/);
28
+ if (orgResourceMatch) {
29
+ const scopedController = createKrateApiController({ namespace: orgNamespaceName(orgResourceMatch[1]) });
30
+ if (request.method === 'GET') return send(response, 200, await scopedController.getResource(orgResourceMatch[2], orgResourceMatch[3]));
31
+ if (request.method === 'DELETE') return send(response, 200, await scopedController.deleteResource(orgResourceMatch[2], orgResourceMatch[3]));
32
+ }
33
+ const orgRepositoryCollectionMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/repositories$/);
34
+ if (orgRepositoryCollectionMatch) {
35
+ const org = orgRepositoryCollectionMatch[1];
36
+ const scopedController = createKrateApiController({ namespace: orgNamespaceName(org) });
37
+ if (request.method === 'GET') return send(response, 200, await scopedController.listResource('Repository'));
38
+ if (request.method === 'POST') return send(response, 201, await scopedController.createRepository({ ...(await readJson(request)), organizationRef: org }));
39
+ }
40
+ const orgRepositoryMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/repositories\/([^/]+)$/);
41
+ if (orgRepositoryMatch) {
42
+ const scopedController = createKrateApiController({ namespace: orgNamespaceName(orgRepositoryMatch[1]) });
43
+ if (request.method === 'GET') return send(response, 200, await scopedController.getResource('Repository', orgRepositoryMatch[2]));
44
+ if (request.method === 'DELETE') return send(response, 200, await scopedController.deleteResource('Repository', orgRepositoryMatch[2]));
45
+ }
46
+
47
+ const orgSnapshotMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/snapshot$/);
48
+ if (orgSnapshotMatch) {
49
+ ensureRuntimeOrg(runtime, orgSnapshotMatch[1]);
50
+ if (request.method === 'GET') return send(response, 200, runtime.snapshot());
51
+ if (request.method === 'POST') return send(response, 200, runtime.importSnapshot(await readJson(request)));
52
+ }
53
+ const runtimeResourceMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/runtime-resources\/([^/]+)$/);
54
+ if (request.method === 'GET' && runtimeResourceMatch) {
55
+ ensureRuntimeOrg(runtime, runtimeResourceMatch[1]);
56
+ return send(response, 200, runtime.controlPlane.list(runtimeResourceMatch[2], { namespace: orgNamespaceName(runtimeResourceMatch[1]) }));
57
+ }
58
+ const objectMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/repositories\/([^/]+)\/objects$/);
59
+ if (request.method === 'POST' && objectMatch) {
60
+ ensureRuntimeOrg(runtime, objectMatch[1]);
61
+ ensureRepository(runtime, objectMatch[2]);
62
+ return send(response, 201, runtime.git.recordObject({ organizationRef: objectMatch[1], repository: objectMatch[2], namespace: orgNamespaceName(objectMatch[1]), ...(await readJson(request)) }));
63
+ }
64
+ const searchIndexMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/repositories\/([^/]+)\/search-index$/);
65
+ if (request.method === 'POST' && searchIndexMatch) {
66
+ ensureRuntimeOrg(runtime, searchIndexMatch[1]);
67
+ ensureRepository(runtime, searchIndexMatch[2]);
68
+ return send(response, 202, runtime.git.enqueueSearchIndex({ organizationRef: searchIndexMatch[1], repository: searchIndexMatch[2], namespace: orgNamespaceName(searchIndexMatch[1]), ...(await readJson(request)) }));
69
+ }
70
+ const pullRequestCollectionMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/pullrequests$/);
71
+ if (request.method === 'POST' && pullRequestCollectionMatch) {
72
+ ensureRuntimeOrg(runtime, pullRequestCollectionMatch[1]);
73
+ return send(response, 201, runtime.createPullRequest({ ...(await readJson(request)), organizationRef: pullRequestCollectionMatch[1] }));
74
+ }
75
+ const reviewMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/pullrequests\/([^/]+)\/reviews$/);
76
+ if (request.method === 'POST' && reviewMatch) {
77
+ ensureRuntimeOrg(runtime, reviewMatch[1]);
78
+ return send(response, 201, runtime.addReview({ ...(await readJson(request)), pullRequest: reviewMatch[2] }));
79
+ }
80
+ const completeMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/pullrequests\/([^/]+)\/checks\/complete$/);
81
+ if (request.method === 'POST' && completeMatch) {
82
+ ensureRuntimeOrg(runtime, completeMatch[1]);
83
+ return send(response, 200, runtime.completePipeline({ pipeline: `pipeline-${completeMatch[2]}`, ...(await readJson(request)) }));
84
+ }
85
+ const mergeMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/pullrequests\/([^/]+)\/merge$/);
86
+ if (request.method === 'POST' && mergeMatch) {
87
+ ensureRuntimeOrg(runtime, mergeMatch[1]);
88
+ return send(response, 200, runtime.mergePullRequest({ ...(await readJson(request)), pullRequest: mergeMatch[2] }));
89
+ }
90
+ const agentApprovalDecideMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/agents\/approvals\/([^/]+)\/decide$/);
91
+ if (request.method === 'POST' && agentApprovalDecideMatch) {
92
+ const org = agentApprovalDecideMatch[1];
93
+ const approvalName = agentApprovalDecideMatch[2];
94
+ const body = await readJson(request);
95
+ const scopedController = createKrateApiController({ namespace: orgNamespaceName(org) });
96
+ const input = { approvalName, decidedBy: body.decidedBy || 'unknown', reason: body.reason || '' };
97
+ const result = body.decision === 'approve'
98
+ ? await scopedController.approveAgentAction(input)
99
+ : await scopedController.denyAgentAction(input);
100
+ return send(response, result.error ? 400 : 200, result);
101
+ }
102
+ const agentTriggerProcessMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/agents\/triggers\/process$/);
103
+ if (request.method === 'POST' && agentTriggerProcessMatch) {
104
+ const org = agentTriggerProcessMatch[1];
105
+ const scopedController = createKrateApiController({ namespace: orgNamespaceName(org) });
106
+ const body = await readJson(request);
107
+ const result = await scopedController.processWebhookEvent({ ...body, organizationRef: org });
108
+ return send(response, 200, result);
109
+ }
110
+ return send(response, 404, { error: 'not_found', method: request.method, path: url.pathname });
111
+ } catch (error) {
112
+ return send(response, 400, { error: 'bad_request', message: error.message });
113
+ }
114
+ };
115
+ }
116
+
117
+ export function createKrateHttpServer(options = {}) {
118
+ return createServer(createKrateHttpHandler(options));
119
+ }
120
+
121
+
122
+ function scopeResource(resource, org) {
123
+ const namespace = orgNamespaceName(org);
124
+ return {
125
+ ...resource,
126
+ metadata: { ...(resource.metadata || {}), namespace, labels: { ...(resource.metadata?.labels || {}), 'krate.a5c.ai/org': org, 'krate.a5c.ai/namespace': namespace } },
127
+ spec: { ...(resource.spec || {}), organizationRef: org }
128
+ };
129
+ }
130
+
131
+ function ensureRuntimeOrg(runtime, org) {
132
+ if (runtime.organizationRef !== org || runtime.namespace !== orgNamespaceName(org)) throw new Error(`Runtime is scoped to ${runtime.organizationRef}`);
133
+ }
134
+
135
+ function ensureRepository(runtime, repository) {
136
+ const existing = runtime.controlPlane.get('Repository', runtime.namespace, repository);
137
+ if (!existing) throw new Error(`Repository ${repository} not found`);
138
+ return existing;
139
+ }
140
+
141
+ async function readJson(request) {
142
+ const chunks = [];
143
+ for await (const chunk of request) chunks.push(chunk);
144
+ const text = Buffer.concat(chunks).toString('utf8');
145
+ return text ? JSON.parse(text) : {};
146
+ }
147
+
148
+ function send(response, status, body) {
149
+ response.writeHead(status, jsonHeaders);
150
+ response.end(JSON.stringify(body, null, 2));
151
+ }