@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.
- package/Dockerfile +31 -29
- package/bin/krate-demo.mjs +0 -0
- package/bin/krate-server.mjs +0 -0
- package/dist/krate-controller-ui.json +808 -10
- package/dist/krate-lifecycle.json +1 -1
- package/dist/krate-runtime-snapshot.json +223 -53
- package/dist/krate-summary.json +40 -3
- package/docs/agents/gaps-agent-mux-to-krate-crds.md +298 -0
- package/docs/architecture-v2.md +431 -0
- package/docs/openapi.yaml +1275 -0
- package/docs/requirements-v2.md +238 -0
- package/docs/sdk-api-reference.md +782 -0
- package/docs/system-spec-v2.md +352 -0
- package/docs/todos.md +4 -0
- package/docs/web-console-spec.md +433 -0
- package/package.json +1 -1
- package/scripts/validate-ui.mjs +305 -207
- package/src/agent-adapter-controller.js +169 -0
- package/src/agent-approval-controller.js +47 -0
- package/src/agent-dispatch-controller.js +130 -7
- package/src/agent-gateway-config-controller.js +147 -0
- package/src/agent-memory-controller.js +357 -0
- package/src/agent-memory-import.js +327 -0
- package/src/agent-memory-query.js +292 -0
- package/src/agent-memory-repository-source-controller.js +255 -0
- package/src/agent-mux-client.js +1 -1
- package/src/agent-permission-review.js +102 -14
- package/src/agent-project-controller.js +117 -0
- package/src/agent-provider-config-controller.js +150 -0
- package/src/agent-secret-config-grant-controller.js +282 -0
- package/src/agent-session-transcript-controller.js +189 -0
- package/src/agent-stack-controller.js +52 -1
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +273 -0
- package/src/agent-workspace-controller.js +702 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +338 -3
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +48 -6
- package/src/controller-client.js +112 -38
- package/src/controller-ui.js +96 -16
- package/src/data-plane.js +3 -2
- package/src/event-bus.js +61 -0
- package/src/external/conflict-controller.js +225 -0
- package/src/external/github/auth.js +96 -0
- package/src/external/github/cicd.js +180 -0
- package/src/external/github/git-forge.js +240 -0
- package/src/external/github/index.js +144 -0
- package/src/external/github/issue-tracking.js +163 -0
- package/src/external/provider-adapter.js +161 -0
- package/src/external/provider-resource-factory.js +161 -0
- package/src/external/sync-controller.js +235 -0
- package/src/external/webhook-controller.js +144 -0
- package/src/external/write-controller.js +283 -0
- package/src/gitea-backend.js +36 -0
- package/src/gitea-service.js +173 -0
- package/src/http-server.js +226 -0
- package/src/index.js +27 -0
- package/src/kubernetes-controller-async.js +531 -0
- package/src/kubernetes-controller.js +156 -84
- package/src/notification-controller.js +178 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +26 -8
- package/src/runner-controller.js +272 -0
- package/src/snapshot-cache.js +157 -0
- package/tests/agent-adapter-controller.test.js +361 -0
- package/tests/agent-dispatch-controller.test.js +139 -0
- package/tests/agent-gateway-config-controller.test.js +386 -0
- package/tests/agent-memory-controller.test.js +308 -0
- package/tests/agent-memory-import-snapshot.test.js +477 -0
- package/tests/agent-memory-query.test.js +404 -0
- package/tests/agent-memory-repository-source.test.js +514 -0
- package/tests/agent-permission-review-v2.test.js +317 -0
- package/tests/agent-project-controller.test.js +302 -0
- package/tests/agent-provider-config-controller.test.js +376 -0
- package/tests/agent-resources.test.js +35 -19
- package/tests/agent-secret-config-grant.test.js +231 -0
- package/tests/agent-session-transcript-controller.test.js +499 -0
- package/tests/agent-subagent-controller.test.js +201 -0
- package/tests/agent-transport-binding-controller.test.js +294 -0
- package/tests/agent-trigger-routes.test.js +190 -0
- package/tests/agent-trigger-sources.test.js +245 -0
- package/tests/agent-workspace-controller.test.js +181 -0
- package/tests/agent-writeback.test.js +292 -0
- package/tests/approval-persistence.test.js +171 -0
- package/tests/async-controller.test.js +252 -0
- package/tests/audit-controller.test.js +227 -0
- package/tests/codespace-controller.test.js +318 -0
- package/tests/controller-client.test.js +133 -0
- package/tests/deployment.test.js +43 -29
- package/tests/e2e/lifecycle.test.js +5 -2
- package/tests/event-bus-integration.test.js +190 -0
- package/tests/external-github-forge.test.js +560 -0
- package/tests/external-github-issues-cicd.test.js +520 -0
- package/tests/external-integration.test.js +470 -0
- package/tests/external-persistence.test.js +340 -0
- package/tests/external-provider-adapter.test.js +365 -0
- package/tests/external-resource-model.test.js +215 -0
- package/tests/external-webhook-sync.test.js +287 -0
- package/tests/external-write-conflict.test.js +353 -0
- package/tests/gitea-service.test.js +253 -0
- package/tests/health-check-real.test.js +165 -0
- package/tests/integration/full-flow.test.js +266 -0
- package/tests/krate.test.js +58 -6
- package/tests/memory-search-wiring.test.js +270 -0
- package/tests/notification-controller.test.js +196 -0
- package/tests/notification-integration.test.js +179 -0
- package/tests/org-scoping.test.js +687 -0
- package/tests/runner-controller.test.js +327 -0
- package/tests/runner-integration.test.js +231 -0
- package/tests/session-cookie-hmac.test.js +151 -0
- package/tests/snapshot-performance.test.js +315 -0
- package/tests/sse-events.test.js +107 -0
- package/tests/webhook-trigger.test.js +198 -0
- package/tests/workspace-volumes.test.js +312 -0
- 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
|
+
}
|
package/src/http-server.js
CHANGED
|
@@ -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';
|