@a5c-ai/krate 5.0.1-staging.660d2b90f → 5.0.1-staging.69cb593ea

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 (118) hide show
  1. package/Dockerfile +31 -29
  2. package/bin/krate-demo.mjs +0 -0
  3. package/bin/krate-server.mjs +0 -0
  4. package/dist/krate-controller-ui.json +808 -10
  5. package/dist/krate-lifecycle.json +1 -1
  6. package/dist/krate-runtime-snapshot.json +223 -53
  7. package/dist/krate-summary.json +40 -3
  8. package/docs/agents/gaps-agent-mux-to-krate-crds.md +298 -0
  9. package/docs/architecture-v2.md +431 -0
  10. package/docs/openapi.yaml +1275 -0
  11. package/docs/requirements-v2.md +238 -0
  12. package/docs/sdk-api-reference.md +782 -0
  13. package/docs/system-spec-v2.md +352 -0
  14. package/docs/todos.md +4 -0
  15. package/docs/web-console-spec.md +433 -0
  16. package/package.json +1 -1
  17. package/scripts/validate-ui.mjs +305 -207
  18. package/src/agent-adapter-controller.js +169 -0
  19. package/src/agent-approval-controller.js +47 -0
  20. package/src/agent-dispatch-controller.js +130 -7
  21. package/src/agent-gateway-config-controller.js +147 -0
  22. package/src/agent-memory-controller.js +357 -0
  23. package/src/agent-memory-import.js +327 -0
  24. package/src/agent-memory-query.js +292 -0
  25. package/src/agent-memory-repository-source-controller.js +255 -0
  26. package/src/agent-mux-client.js +1 -1
  27. package/src/agent-permission-review.js +102 -14
  28. package/src/agent-project-controller.js +117 -0
  29. package/src/agent-provider-config-controller.js +150 -0
  30. package/src/agent-secret-config-grant-controller.js +282 -0
  31. package/src/agent-session-transcript-controller.js +189 -0
  32. package/src/agent-stack-controller.js +52 -1
  33. package/src/agent-subagent-controller.js +160 -0
  34. package/src/agent-transport-binding-controller.js +121 -0
  35. package/src/agent-trigger-controller.js +273 -0
  36. package/src/agent-workspace-controller.js +702 -0
  37. package/src/agent-writeback-controller.js +302 -0
  38. package/src/api-controller.js +338 -3
  39. package/src/async-controller.js +207 -0
  40. package/src/audit-controller.js +191 -0
  41. package/src/auth.js +48 -6
  42. package/src/controller-client.js +112 -38
  43. package/src/controller-ui.js +96 -16
  44. package/src/data-plane.js +3 -2
  45. package/src/event-bus.js +61 -0
  46. package/src/external/conflict-controller.js +225 -0
  47. package/src/external/github/auth.js +96 -0
  48. package/src/external/github/cicd.js +180 -0
  49. package/src/external/github/git-forge.js +240 -0
  50. package/src/external/github/index.js +144 -0
  51. package/src/external/github/issue-tracking.js +163 -0
  52. package/src/external/provider-adapter.js +161 -0
  53. package/src/external/provider-resource-factory.js +161 -0
  54. package/src/external/sync-controller.js +235 -0
  55. package/src/external/webhook-controller.js +144 -0
  56. package/src/external/write-controller.js +283 -0
  57. package/src/gitea-backend.js +36 -0
  58. package/src/gitea-service.js +173 -0
  59. package/src/http-server.js +226 -0
  60. package/src/index.js +27 -0
  61. package/src/kubernetes-controller-async.js +531 -0
  62. package/src/kubernetes-controller.js +156 -84
  63. package/src/notification-controller.js +178 -0
  64. package/src/org-scoping.js +5 -0
  65. package/src/resource-model.js +26 -8
  66. package/src/runner-controller.js +272 -0
  67. package/src/snapshot-cache.js +157 -0
  68. package/tests/agent-adapter-controller.test.js +361 -0
  69. package/tests/agent-dispatch-controller.test.js +139 -0
  70. package/tests/agent-gateway-config-controller.test.js +386 -0
  71. package/tests/agent-memory-controller.test.js +308 -0
  72. package/tests/agent-memory-import-snapshot.test.js +477 -0
  73. package/tests/agent-memory-query.test.js +404 -0
  74. package/tests/agent-memory-repository-source.test.js +514 -0
  75. package/tests/agent-permission-review-v2.test.js +317 -0
  76. package/tests/agent-project-controller.test.js +302 -0
  77. package/tests/agent-provider-config-controller.test.js +376 -0
  78. package/tests/agent-resources.test.js +35 -19
  79. package/tests/agent-secret-config-grant.test.js +231 -0
  80. package/tests/agent-session-transcript-controller.test.js +499 -0
  81. package/tests/agent-subagent-controller.test.js +201 -0
  82. package/tests/agent-transport-binding-controller.test.js +294 -0
  83. package/tests/agent-trigger-routes.test.js +190 -0
  84. package/tests/agent-trigger-sources.test.js +245 -0
  85. package/tests/agent-workspace-controller.test.js +181 -0
  86. package/tests/agent-writeback.test.js +292 -0
  87. package/tests/approval-persistence.test.js +171 -0
  88. package/tests/async-controller.test.js +252 -0
  89. package/tests/audit-controller.test.js +227 -0
  90. package/tests/codespace-controller.test.js +318 -0
  91. package/tests/controller-client.test.js +133 -0
  92. package/tests/deployment.test.js +43 -29
  93. package/tests/e2e/lifecycle.test.js +5 -2
  94. package/tests/event-bus-integration.test.js +190 -0
  95. package/tests/external-github-forge.test.js +560 -0
  96. package/tests/external-github-issues-cicd.test.js +520 -0
  97. package/tests/external-integration.test.js +470 -0
  98. package/tests/external-persistence.test.js +340 -0
  99. package/tests/external-provider-adapter.test.js +365 -0
  100. package/tests/external-resource-model.test.js +215 -0
  101. package/tests/external-webhook-sync.test.js +287 -0
  102. package/tests/external-write-conflict.test.js +353 -0
  103. package/tests/gitea-service.test.js +253 -0
  104. package/tests/health-check-real.test.js +165 -0
  105. package/tests/integration/full-flow.test.js +266 -0
  106. package/tests/krate.test.js +58 -6
  107. package/tests/memory-search-wiring.test.js +270 -0
  108. package/tests/notification-controller.test.js +196 -0
  109. package/tests/notification-integration.test.js +179 -0
  110. package/tests/org-scoping.test.js +687 -0
  111. package/tests/runner-controller.test.js +327 -0
  112. package/tests/runner-integration.test.js +231 -0
  113. package/tests/session-cookie-hmac.test.js +151 -0
  114. package/tests/snapshot-performance.test.js +315 -0
  115. package/tests/sse-events.test.js +107 -0
  116. package/tests/webhook-trigger.test.js +198 -0
  117. package/tests/workspace-volumes.test.js +312 -0
  118. package/tests/writeback-persistence.test.js +207 -0
@@ -0,0 +1,173 @@
1
+ import { createGiteaBackend } from './gitea-backend.js';
2
+
3
+ /**
4
+ * Gitea service layer that wraps gitea-backend.js for repo code-browsing operations.
5
+ *
6
+ * Returns null when Gitea is not configured so call-sites can fall back to mock data.
7
+ *
8
+ * @param {{ giteaUrl?: string, token?: string, fetchImpl?: Function }} [options]
9
+ * @returns {GiteaService|null}
10
+ */
11
+ export function createGiteaService(options = {}) {
12
+ const giteaUrl = options.giteaUrl || process.env.KRATE_GITEA_HTTP_URL;
13
+ if (!giteaUrl) return null; // Gitea not configured — callers must fall back
14
+
15
+ const backend = createGiteaBackend({
16
+ baseUrl: giteaUrl,
17
+ token: options.token || process.env.KRATE_GITEA_TOKEN,
18
+ fetchImpl: options.fetchImpl || globalThis.fetch,
19
+ });
20
+
21
+ // Low-level fetch helper that bypasses the backend's opinionated error handling
22
+ // so service methods can gracefully return null on 404 rather than throwing.
23
+ const root = giteaUrl.replace(/\/$/, '');
24
+ const fetchImpl = options.fetchImpl || globalThis.fetch;
25
+ const token = options.token || process.env.KRATE_GITEA_TOKEN;
26
+
27
+ async function rawRequest(path) {
28
+ if (!fetchImpl) throw new Error('Gitea service requires a fetch implementation');
29
+ const response = await fetchImpl(`${root}/api/v1${path}`, {
30
+ method: 'GET',
31
+ headers: {
32
+ Accept: 'application/json',
33
+ ...(token ? { Authorization: `token ${token}` } : {}),
34
+ },
35
+ });
36
+ if (response.status === 404) return null;
37
+ if (!response.ok) {
38
+ throw new Error(`Gitea GET ${path} failed with ${response.status}`);
39
+ }
40
+ return response.json();
41
+ }
42
+
43
+ async function rawRawRequest(path) {
44
+ if (!fetchImpl) throw new Error('Gitea service requires a fetch implementation');
45
+ const response = await fetchImpl(`${root}/api/v1${path}`, {
46
+ method: 'GET',
47
+ headers: {
48
+ Accept: 'text/plain',
49
+ ...(token ? { Authorization: `token ${token}` } : {}),
50
+ },
51
+ });
52
+ if (response.status === 404) return null;
53
+ if (!response.ok) {
54
+ throw new Error(`Gitea GET ${path} failed with ${response.status}`);
55
+ }
56
+ return response.text();
57
+ }
58
+
59
+ return {
60
+ available: true,
61
+ baseUrl: root,
62
+
63
+ /**
64
+ * List tree entries for a repository path at a given ref.
65
+ * Uses GET /api/v1/repos/{owner}/{repo}/contents/{path}?ref={ref}
66
+ *
67
+ * @param {string} org
68
+ * @param {string} repo
69
+ * @param {string} ref — branch name, tag, or commit SHA
70
+ * @param {string} [path='']
71
+ * @returns {Promise<Array<{path:string,type:'blob'|'tree',size:number}>|null>}
72
+ */
73
+ async listTree(org, repo, ref, path = '') {
74
+ const encodedPath = path ? encodeURIComponent(path).replace(/%2F/g, '/') : '';
75
+ const apiPath = `/repos/${encodeURIComponent(org)}/${encodeURIComponent(repo)}/contents/${encodedPath}?ref=${encodeURIComponent(ref)}`;
76
+ const data = await rawRequest(apiPath);
77
+ if (data === null) return null;
78
+
79
+ // Gitea returns an array for directories, an object for files
80
+ const entries = Array.isArray(data) ? data : [data];
81
+ return entries.map((entry) => ({
82
+ path: entry.path || entry.name,
83
+ type: entry.type === 'dir' ? 'tree' : 'blob',
84
+ size: entry.size || 0,
85
+ sha: entry.sha,
86
+ name: entry.name,
87
+ }));
88
+ },
89
+
90
+ /**
91
+ * Get the raw text content of a file.
92
+ * Uses GET /api/v1/repos/{owner}/{repo}/raw/{filepath}?ref={ref}
93
+ *
94
+ * @param {string} org
95
+ * @param {string} repo
96
+ * @param {string} ref
97
+ * @param {string} path
98
+ * @returns {Promise<string|null>}
99
+ */
100
+ async getBlob(org, repo, ref, path) {
101
+ const encodedPath = path ? encodeURIComponent(path).replace(/%2F/g, '/') : '';
102
+ const apiPath = `/repos/${encodeURIComponent(org)}/${encodeURIComponent(repo)}/raw/${encodedPath}?ref=${encodeURIComponent(ref)}`;
103
+ return rawRawRequest(apiPath);
104
+ },
105
+
106
+ /**
107
+ * List branches for a repository.
108
+ * Uses GET /api/v1/repos/{owner}/{repo}/branches
109
+ *
110
+ * @param {string} org
111
+ * @param {string} repo
112
+ * @returns {Promise<Array<{name:string,sha:string,protected:boolean}>|null>}
113
+ */
114
+ async listBranches(org, repo) {
115
+ const data = await rawRequest(`/repos/${encodeURIComponent(org)}/${encodeURIComponent(repo)}/branches`);
116
+ if (data === null) return null;
117
+ return data.map((b) => ({
118
+ name: b.name,
119
+ sha: b.commit?.id || b.commit?.sha || '',
120
+ protected: b.protected || false,
121
+ }));
122
+ },
123
+
124
+ /**
125
+ * Get file metadata + base64-decoded content for a path.
126
+ * Uses GET /api/v1/repos/{owner}/{repo}/contents/{path}?ref={ref}
127
+ *
128
+ * @param {string} org
129
+ * @param {string} repo
130
+ * @param {string} ref
131
+ * @param {string} path
132
+ * @returns {Promise<{path:string,content:string,size:number,sha:string,encoding:string}|null>}
133
+ */
134
+ async getFileContent(org, repo, ref, path) {
135
+ const encodedPath = path ? encodeURIComponent(path).replace(/%2F/g, '/') : '';
136
+ const apiPath = `/repos/${encodeURIComponent(org)}/${encodeURIComponent(repo)}/contents/${encodedPath}?ref=${encodeURIComponent(ref)}`;
137
+ const data = await rawRequest(apiPath);
138
+ if (data === null || Array.isArray(data)) return null; // null = not found, array = directory
139
+
140
+ // Gitea returns content as base64 — decode it
141
+ const rawContent = data.content
142
+ ? Buffer.from(data.content.replace(/\n/g, ''), 'base64').toString('utf8')
143
+ : '';
144
+
145
+ return {
146
+ path: data.path || path,
147
+ content: rawContent,
148
+ size: data.size || Buffer.byteLength(rawContent, 'utf8'),
149
+ sha: data.sha,
150
+ encoding: 'utf-8',
151
+ lastCommit: data.last_commit_sha || null,
152
+ };
153
+ },
154
+
155
+ /**
156
+ * Create a repository inside an org.
157
+ * Delegates to the underlying gitea-backend.
158
+ *
159
+ * @param {string} org
160
+ * @param {string} name
161
+ * @param {{ private?: boolean, defaultBranch?: string, description?: string }} [repoOptions]
162
+ */
163
+ async createRepository(org, name, repoOptions = {}) {
164
+ return backend.createRepository({
165
+ owner: org,
166
+ name,
167
+ private: repoOptions.private ?? true,
168
+ defaultBranch: repoOptions.defaultBranch || 'main',
169
+ description: repoOptions.description || '',
170
+ });
171
+ },
172
+ };
173
+ }
@@ -4,6 +4,7 @@ import { createControllerUiModel } from './controller-ui.js';
4
4
  import { createKrateApiController } from './api-controller.js';
5
5
  import { createKubernetesResourceGateway } from './kubernetes-resource-gateway.js';
6
6
  import { orgNamespaceName } from './kubernetes-controller.js';
7
+ import { globalEventBus } from './event-bus.js';
7
8
 
8
9
  const jsonHeaders = { 'content-type': 'application/json; charset=utf-8' };
9
10
 
@@ -99,6 +100,50 @@ export function createKrateHttpHandler({ runtime = createKrateRuntime(), control
99
100
  : await scopedController.denyAgentAction(input);
100
101
  return send(response, result.error ? 400 : 200, result);
101
102
  }
103
+ // Agent webhook ingress
104
+ const webhookIngestMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/agents\/webhooks\/ingest$/);
105
+ if (request.method === 'POST' && webhookIngestMatch) {
106
+ const org = webhookIngestMatch[1];
107
+ const body = await readJson(request);
108
+ const event = normalizeWebhookEvent(body, org);
109
+ const scopedController = createKrateApiController({ namespace: orgNamespaceName(org) });
110
+ const result = await scopedController.processWebhookEvent({ event, organizationRef: org, namespace: orgNamespaceName(org) });
111
+ return send(response, 200, result);
112
+ }
113
+
114
+ // Pipeline failure event
115
+ const pipelineFailMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/agents\/events\/pipeline-failure$/);
116
+ if (request.method === 'POST' && pipelineFailMatch) {
117
+ const org = pipelineFailMatch[1];
118
+ const body = await readJson(request);
119
+ const event = { type: 'ci-failure', source: { kind: 'Pipeline', name: body.name || 'unknown', namespace: body.namespace }, repository: body.repository || '', ref: body.ref || 'main', actor: body.actor || 'system', payload: body };
120
+ const scopedController = createKrateApiController({ namespace: orgNamespaceName(org) });
121
+ const result = await scopedController.processWebhookEvent({ event, organizationRef: org, namespace: orgNamespaceName(org) });
122
+ return send(response, 200, result);
123
+ }
124
+
125
+ // Comment event
126
+ const commentMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/agents\/events\/comment$/);
127
+ if (request.method === 'POST' && commentMatch) {
128
+ const org = commentMatch[1];
129
+ const body = await readJson(request);
130
+ const event = { type: 'comment', source: { kind: body.kind || 'Issue', name: body.name || 'unknown' }, repository: body.repository || '', ref: body.ref || 'main', actor: body.actor || 'system', payload: { body: body.body || '' } };
131
+ const scopedController = createKrateApiController({ namespace: orgNamespaceName(org) });
132
+ const result = await scopedController.processWebhookEvent({ event, organizationRef: org, namespace: orgNamespaceName(org) });
133
+ return send(response, 200, result);
134
+ }
135
+
136
+ // Label event
137
+ const labelMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/agents\/events\/label$/);
138
+ if (request.method === 'POST' && labelMatch) {
139
+ const org = labelMatch[1];
140
+ const body = await readJson(request);
141
+ const event = { type: 'label-added', source: { kind: body.kind || 'Issue', name: body.name || 'unknown' }, repository: body.repository || '', ref: body.ref || 'main', actor: body.actor || 'system', payload: { label: body.label || '' } };
142
+ const scopedController = createKrateApiController({ namespace: orgNamespaceName(org) });
143
+ const result = await scopedController.processWebhookEvent({ event, organizationRef: org, namespace: orgNamespaceName(org) });
144
+ return send(response, 200, result);
145
+ }
146
+
102
147
  const agentTriggerProcessMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/agents\/triggers\/process$/);
103
148
  if (request.method === 'POST' && agentTriggerProcessMatch) {
104
149
  const org = agentTriggerProcessMatch[1];
@@ -107,6 +152,162 @@ export function createKrateHttpHandler({ runtime = createKrateRuntime(), control
107
152
  const result = await scopedController.processWebhookEvent({ ...body, organizationRef: org });
108
153
  return send(response, 200, result);
109
154
  }
155
+ const memoryQueryMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/agents\/memory\/query$/);
156
+ if (request.method === 'POST' && memoryQueryMatch) {
157
+ const org = memoryQueryMatch[1];
158
+ const body = await readJson(request);
159
+ const scopedController = createKrateApiController({ namespace: orgNamespaceName(org) });
160
+ const result = await scopedController.queryAgentMemory({ ...body, organizationRef: org });
161
+ return send(response, 200, result);
162
+ }
163
+ // --- Secrets management routes ---
164
+ const orgSecretsMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/secrets$/);
165
+ if (orgSecretsMatch) {
166
+ const org = orgSecretsMatch[1];
167
+ const scopedController = createKrateApiController({ namespace: orgNamespaceName(org) });
168
+ if (request.method === 'GET') {
169
+ const result = await scopedController.listResourceForOrg(org, 'AgentSecretGrant');
170
+ const items = Array.isArray(result?.items) ? result.items : [];
171
+ return send(response, 200, {
172
+ secrets: items.map((item) => ({
173
+ name: item.spec?.secretName || item.spec?.secretRef || item.metadata?.name,
174
+ type: item.spec?.type || 'Opaque',
175
+ createdAt: item.status?.createdAt || item.metadata?.creationTimestamp || null,
176
+ namespace: orgNamespaceName(org),
177
+ grants: []
178
+ }))
179
+ });
180
+ }
181
+ if (request.method === 'POST') {
182
+ const body = await readJson(request);
183
+ const secretResource = {
184
+ apiVersion: 'krate.a5c.ai/v1alpha1',
185
+ kind: 'AgentSecretGrant',
186
+ metadata: { name: body.name, namespace: orgNamespaceName(org) },
187
+ spec: {
188
+ organizationRef: org,
189
+ secretName: body.name,
190
+ secretRef: body.name,
191
+ grantedTo: body.grantedTo || 'system',
192
+ subject: body.grantedTo || 'system',
193
+ permissions: body.permissions || ['read'],
194
+ purpose: 'read',
195
+ data: body.data || {}
196
+ }
197
+ };
198
+ const result = await scopedController.applyResourceForOrg(org, secretResource);
199
+ return send(response, 201, result);
200
+ }
201
+ }
202
+
203
+ const orgSecretMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/secrets\/([^/]+)$/);
204
+ if (orgSecretMatch) {
205
+ const org = orgSecretMatch[1];
206
+ const secretName = orgSecretMatch[2];
207
+ const scopedController = createKrateApiController({ namespace: orgNamespaceName(org) });
208
+ if (request.method === 'DELETE') {
209
+ const result = await scopedController.deleteResourceForOrg(org, 'AgentSecretGrant', secretName);
210
+ return send(response, 200, result);
211
+ }
212
+ }
213
+
214
+ const orgSecretGrantsMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/secret-grants$/);
215
+ if (orgSecretGrantsMatch) {
216
+ const org = orgSecretGrantsMatch[1];
217
+ const scopedController = createKrateApiController({ namespace: orgNamespaceName(org) });
218
+ if (request.method === 'GET') {
219
+ return send(response, 200, await scopedController.listResourceForOrg(org, 'AgentSecretGrant'));
220
+ }
221
+ if (request.method === 'POST') {
222
+ const body = await readJson(request);
223
+ const grant = {
224
+ apiVersion: 'krate.a5c.ai/v1alpha1',
225
+ kind: 'AgentSecretGrant',
226
+ metadata: { name: body.name || `grant-${Date.now()}`, namespace: orgNamespaceName(org) },
227
+ spec: {
228
+ organizationRef: org,
229
+ secretName: body.secretName,
230
+ secretRef: body.secretName,
231
+ grantedTo: body.grantedTo,
232
+ subject: body.grantedTo,
233
+ permissions: body.permissions || ['read'],
234
+ purpose: (body.permissions || ['read']).join(',')
235
+ }
236
+ };
237
+ return send(response, 201, await scopedController.applyResourceForOrg(org, grant));
238
+ }
239
+ }
240
+
241
+ // --- External integration routes ---
242
+ const externalSyncMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/external\/sync$/);
243
+ if (request.method === 'POST' && externalSyncMatch) {
244
+ const org = externalSyncMatch[1];
245
+ const body = await readJson(request);
246
+ const scopedController = createKrateApiController({ namespace: orgNamespaceName(org) });
247
+ const bindingName = body.bindingName || '';
248
+ const result = await scopedController.syncExternalBinding(bindingName, {
249
+ kind: body.kind,
250
+ localName: body.localName,
251
+ namespace: orgNamespaceName(org),
252
+ spec: body.spec || {},
253
+ externalEnvelope: body.externalEnvelope || { nativeId: '', url: '', etag: '', providerRef: '' },
254
+ watermark: body.watermark
255
+ });
256
+ return send(response, 200, result);
257
+ }
258
+
259
+ const externalConflictResolveMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/external\/conflicts\/([^/]+)\/resolve$/);
260
+ if (request.method === 'POST' && externalConflictResolveMatch) {
261
+ const org = externalConflictResolveMatch[1];
262
+ const conflictName = externalConflictResolveMatch[2];
263
+ const body = await readJson(request);
264
+ const scopedController = createKrateApiController({ namespace: orgNamespaceName(org) });
265
+ const result = await scopedController.resolveExternalConflict({ conflictName, strategy: body.strategy, resolvedValue: body.resolvedValue, resources: body.resources || {} });
266
+ return send(response, 200, result);
267
+ }
268
+
269
+ const externalWriteIntentApproveMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/external\/write-intents\/([^/]+)\/approve$/);
270
+ if (request.method === 'POST' && externalWriteIntentApproveMatch) {
271
+ const org = externalWriteIntentApproveMatch[1];
272
+ const intentName = externalWriteIntentApproveMatch[2];
273
+ const body = await readJson(request);
274
+ const scopedController = createKrateApiController({ namespace: orgNamespaceName(org) });
275
+ const result = await scopedController.approveExternalWriteIntent({ intentName, approvedBy: body.approvedBy || 'unknown', resources: body.resources || {} });
276
+ return send(response, 200, result);
277
+ }
278
+
279
+ const externalWriteIntentCancelMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/external\/write-intents\/([^/]+)\/cancel$/);
280
+ if (request.method === 'POST' && externalWriteIntentCancelMatch) {
281
+ const org = externalWriteIntentCancelMatch[1];
282
+ const intentName = externalWriteIntentCancelMatch[2];
283
+ const body = await readJson(request);
284
+ const scopedController = createKrateApiController({ namespace: orgNamespaceName(org) });
285
+ const result = await scopedController.cancelExternalWriteIntent({ intentName, cancelledBy: body.cancelledBy || 'unknown', resources: body.resources || {} });
286
+ return send(response, 200, result);
287
+ }
288
+
289
+ const sseMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/agents\/events\/stream$/);
290
+ if (request.method === 'GET' && sseMatch) {
291
+ response.writeHead(200, {
292
+ 'Content-Type': 'text/event-stream',
293
+ 'Cache-Control': 'no-cache',
294
+ 'Connection': 'keep-alive',
295
+ 'X-Accel-Buffering': 'no',
296
+ });
297
+ response.write('data: {"type":"connected"}\n\n');
298
+ const writer = (event) => {
299
+ response.write(`data: ${JSON.stringify(event)}\n\n`);
300
+ };
301
+ globalEventBus.subscribe(writer);
302
+ const interval = setInterval(() => {
303
+ response.write('data: {"type":"heartbeat"}\n\n');
304
+ }, 30000);
305
+ request.on('close', () => {
306
+ clearInterval(interval);
307
+ globalEventBus.unsubscribe(writer);
308
+ });
309
+ return;
310
+ }
110
311
  return send(response, 404, { error: 'not_found', method: request.method, path: url.pathname });
111
312
  } catch (error) {
112
313
  return send(response, 400, { error: 'bad_request', message: error.message });
@@ -149,3 +350,28 @@ function send(response, status, body) {
149
350
  response.writeHead(status, jsonHeaders);
150
351
  response.end(JSON.stringify(body, null, 2));
151
352
  }
353
+
354
+ export function normalizeWebhookEvent(body, org) {
355
+ // GitHub/Gitea webhook normalization
356
+ if (body.action === 'completed' && body.workflow_run?.conclusion === 'failure') {
357
+ return { type: 'ci-failure', source: { kind: 'Pipeline', name: body.workflow_run?.name || 'unknown' }, repository: body.repository?.full_name || '', ref: body.workflow_run?.head_branch || 'main', actor: body.sender?.login || 'system', payload: body };
358
+ }
359
+ if (body.action === 'opened' && body.pull_request) {
360
+ return { type: 'pr-opened', source: { kind: 'PullRequest', name: String(body.pull_request.number) }, repository: body.repository?.full_name || '', ref: body.pull_request?.head?.ref || 'main', actor: body.sender?.login || 'system', payload: body };
361
+ }
362
+ if (body.action === 'created' && body.comment) {
363
+ const kind = body.issue?.pull_request ? 'PullRequest' : 'Issue';
364
+ return { type: 'comment', source: { kind, name: String(body.issue?.number || 'unknown') }, repository: body.repository?.full_name || '', ref: 'main', actor: body.sender?.login || body.comment?.user?.login || 'system', payload: { body: body.comment?.body || '' } };
365
+ }
366
+ if (body.action === 'labeled') {
367
+ return { type: 'label-added', source: { kind: body.pull_request ? 'PullRequest' : 'Issue', name: String(body.issue?.number || body.pull_request?.number || 'unknown') }, repository: body.repository?.full_name || '', ref: 'main', actor: body.sender?.login || 'system', payload: { label: body.label?.name || '' } };
368
+ }
369
+ if (body.action === 'opened' && body.issue && !body.pull_request) {
370
+ return { type: 'issue-created', source: { kind: 'Issue', name: String(body.issue.number) }, repository: body.repository?.full_name || '', ref: 'main', actor: body.sender?.login || 'system', payload: body };
371
+ }
372
+ if (body.ref && body.commits) {
373
+ return { type: 'push', source: { kind: 'Repository', name: body.repository?.full_name || '' }, repository: body.repository?.full_name || '', ref: body.ref?.replace('refs/heads/', '') || 'main', actor: body.sender?.login || body.pusher?.name || 'system', payload: body };
374
+ }
375
+ // Generic fallback
376
+ return { type: 'webhook', source: { kind: 'WebhookDelivery', name: 'unknown' }, repository: body.repository?.full_name || '', ref: 'main', actor: body.sender?.login || 'system', payload: body };
377
+ }
package/src/index.js CHANGED
@@ -20,6 +20,7 @@ export * from './api-controller.js';
20
20
  export * from './kubernetes-controller.js';
21
21
  export * from './kubernetes-resource-gateway.js';
22
22
  export * from './gitea-backend.js';
23
+ export * from './gitea-service.js';
23
24
  export * from './argocd-gitops.js';
24
25
  export * from './agent-permission-review.js';
25
26
  export * from './agent-stack-controller.js';
@@ -28,3 +29,29 @@ export * from './agent-mux-client.js';
28
29
  export * from './agent-dispatch-controller.js';
29
30
  export * from './agent-approval-controller.js';
30
31
  export * from './agent-trigger-controller.js';
32
+ export * from './agent-workspace-controller.js';
33
+ export * from './agent-memory-controller.js';
34
+ export * from './agent-memory-query.js';
35
+ export * from './agent-memory-repository-source-controller.js';
36
+ export * from './agent-adapter-controller.js';
37
+ export * from './agent-transport-binding-controller.js';
38
+ export * from './agent-provider-config-controller.js';
39
+ export * from './agent-project-controller.js';
40
+ export * from './agent-gateway-config-controller.js';
41
+ export * from './agent-session-transcript-controller.js';
42
+ export * from './agent-writeback-controller.js';
43
+ export * from './agent-memory-import.js';
44
+ export * from './agent-subagent-controller.js';
45
+ export * from './external/provider-adapter.js';
46
+ export * from './external/provider-resource-factory.js';
47
+ export * from './external/github/index.js';
48
+ export * from './external/webhook-controller.js';
49
+ export * from './external/sync-controller.js';
50
+ export * from './external/write-controller.js';
51
+ export * from './external/conflict-controller.js';
52
+ export * from './audit-controller.js';
53
+ export * from './async-controller.js';
54
+ export * from './event-bus.js';
55
+ export * from './agent-secret-config-grant-controller.js';
56
+ export * from './runner-controller.js';
57
+ export * from './notification-controller.js';