@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,169 @@
|
|
|
1
|
+
// Agent Adapter Controller — Slice 1.2a / C4
|
|
2
|
+
// Manages AgentAdapter resources: validation, capabilities, transports, and health checks.
|
|
3
|
+
|
|
4
|
+
export const AGENT_ADAPTER_CONTROLLER_BOUNDARY = {
|
|
5
|
+
role: 'agent-adapter-controller',
|
|
6
|
+
scope: 'AgentAdapter lifecycle: validation, capabilities matrix, transport enumeration, real health checks',
|
|
7
|
+
owns: ['adapter validation', 'capabilities matrix', 'transport enumeration', 'health checks'],
|
|
8
|
+
delegatesTo: ['resource-model'],
|
|
9
|
+
mustNotOwn: ['secret values', 'dispatch execution', 'Agent Mux sessions']
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const HEALTH_CHECK_TIMEOUT_MS = 3000;
|
|
13
|
+
|
|
14
|
+
const VALID_TRANSPORTS = ['stdio', 'http', 'websocket', 'unix'];
|
|
15
|
+
const VALID_ADAPTER_TYPES = ['subprocess', 'remote', 'programmatic'];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validate an AgentAdapter resource. Returns { valid, errors }.
|
|
19
|
+
* @param {object} resource
|
|
20
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
21
|
+
*/
|
|
22
|
+
export function validateAgentAdapter(resource) {
|
|
23
|
+
const errors = [];
|
|
24
|
+
|
|
25
|
+
// Guard against null/undefined resource
|
|
26
|
+
if (resource == null) {
|
|
27
|
+
errors.push('resource must not be null or undefined');
|
|
28
|
+
return { valid: false, errors };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Validate metadata.name
|
|
32
|
+
if (!resource?.metadata?.name) {
|
|
33
|
+
errors.push('metadata.name is required');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const spec = resource?.spec || {};
|
|
37
|
+
|
|
38
|
+
// Validate adapterType
|
|
39
|
+
const adapterType = spec.adapterType;
|
|
40
|
+
if (!adapterType) {
|
|
41
|
+
errors.push(`spec.adapterType is required; valid types are: ${VALID_ADAPTER_TYPES.join(', ')}`);
|
|
42
|
+
} else if (!VALID_ADAPTER_TYPES.includes(adapterType)) {
|
|
43
|
+
errors.push(`spec.adapterType "${adapterType}" is not supported; valid types are: ${VALID_ADAPTER_TYPES.join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Validate transport
|
|
47
|
+
const transport = spec.transport;
|
|
48
|
+
if (!transport) {
|
|
49
|
+
errors.push(`spec.transport is required; valid transports are: ${VALID_TRANSPORTS.join(', ')}`);
|
|
50
|
+
} else if (!VALID_TRANSPORTS.includes(transport)) {
|
|
51
|
+
errors.push(`spec.transport "${transport}" is not supported; valid transports are: ${VALID_TRANSPORTS.join(', ')}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Validate capabilities — must be a non-empty array
|
|
55
|
+
const capabilities = spec.capabilities;
|
|
56
|
+
if (!Array.isArray(capabilities) || capabilities.length === 0) {
|
|
57
|
+
errors.push('spec.capabilities must be a non-empty array');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { valid: errors.length === 0, errors };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Perform an HTTP health check against the given URL.
|
|
65
|
+
* Returns { status: 'healthy'|'unhealthy', latencyMs, error? }.
|
|
66
|
+
* @param {string} url
|
|
67
|
+
* @param {Function} fetchFn - injectable fetch (defaults to globalThis.fetch)
|
|
68
|
+
* @returns {Promise<{ status: string, latencyMs: number, error?: string }>}
|
|
69
|
+
*/
|
|
70
|
+
async function performHttpHealthCheck(url, fetchFn) {
|
|
71
|
+
const fn = fetchFn || globalThis.fetch;
|
|
72
|
+
const start = Date.now();
|
|
73
|
+
try {
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
const timer = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT_MS);
|
|
76
|
+
let response;
|
|
77
|
+
try {
|
|
78
|
+
response = await fn(url, { signal: controller.signal });
|
|
79
|
+
} finally {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
}
|
|
82
|
+
const latencyMs = Date.now() - start;
|
|
83
|
+
if (response.ok) {
|
|
84
|
+
return { status: 'healthy', latencyMs };
|
|
85
|
+
}
|
|
86
|
+
return { status: 'unhealthy', latencyMs, error: `HTTP ${response.status}` };
|
|
87
|
+
} catch (err) {
|
|
88
|
+
const latencyMs = Date.now() - start;
|
|
89
|
+
return { status: 'unhealthy', latencyMs, error: err.message || String(err) };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Factory that returns an AgentAdapter controller instance.
|
|
95
|
+
* @param {object} [options]
|
|
96
|
+
* @param {Function} [options.fetch] - injectable fetch function (for testing)
|
|
97
|
+
*/
|
|
98
|
+
export function createAgentAdapterController(options = {}) {
|
|
99
|
+
const fetchFn = options.fetch || null;
|
|
100
|
+
return {
|
|
101
|
+
role: 'agent-adapter-controller',
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validate an AgentAdapter resource.
|
|
105
|
+
* @param {object} resource
|
|
106
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
107
|
+
*/
|
|
108
|
+
validate(resource) {
|
|
109
|
+
return validateAgentAdapter(resource);
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Return the capabilities matrix for an adapter.
|
|
114
|
+
* @param {object} resource
|
|
115
|
+
* @returns {{ adapterName: string, supported: string[] }}
|
|
116
|
+
*/
|
|
117
|
+
getCapabilities(resource) {
|
|
118
|
+
if (resource == null) {
|
|
119
|
+
throw new Error('resource must not be null or undefined');
|
|
120
|
+
}
|
|
121
|
+
const supported = Array.isArray(resource?.spec?.capabilities)
|
|
122
|
+
? [...resource.spec.capabilities]
|
|
123
|
+
: [];
|
|
124
|
+
return {
|
|
125
|
+
adapterName: resource?.metadata?.name,
|
|
126
|
+
supported
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Return the list of supported transport types.
|
|
132
|
+
* @returns {string[]}
|
|
133
|
+
*/
|
|
134
|
+
getSupportedTransports() {
|
|
135
|
+
return [...VALID_TRANSPORTS];
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Return the list of supported adapter types.
|
|
140
|
+
* @returns {string[]}
|
|
141
|
+
*/
|
|
142
|
+
getSupportedAdapterTypes() {
|
|
143
|
+
return [...VALID_ADAPTER_TYPES];
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Perform a health check for an AgentAdapter.
|
|
148
|
+
* If no healthEndpoint is configured in spec, returns { status: 'unknown', reason: 'no-endpoint' }.
|
|
149
|
+
* If a healthEndpoint is configured, performs a real HTTP GET with a 3s timeout.
|
|
150
|
+
* Returns { status: 'healthy'|'unhealthy', latencyMs, error? } or { status: 'unknown', reason }.
|
|
151
|
+
* @param {object} resource
|
|
152
|
+
* @returns {Promise<{ adapterName: string, status: string, latencyMs?: number, reason?: string, error?: string }>}
|
|
153
|
+
*/
|
|
154
|
+
async healthCheck(resource) {
|
|
155
|
+
if (resource == null) {
|
|
156
|
+
throw new Error('resource must not be null or undefined');
|
|
157
|
+
}
|
|
158
|
+
const adapterName = resource?.metadata?.name;
|
|
159
|
+
const endpoint = resource?.spec?.healthEndpoint;
|
|
160
|
+
|
|
161
|
+
if (!endpoint) {
|
|
162
|
+
return { adapterName, status: 'unknown', reason: 'no-endpoint' };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const checkResult = await performHttpHealthCheck(endpoint, fetchFn);
|
|
166
|
+
return { adapterName, ...checkResult };
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -118,6 +118,53 @@ export function createAgentApprovalController() {
|
|
|
118
118
|
listApprovalsForRun({ dispatchRun, resources = {} }) {
|
|
119
119
|
const approvals = resources.AgentApproval || [];
|
|
120
120
|
return approvals.filter((a) => a.spec?.dispatchRun === dispatchRun).map(clone);
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// -----------------------------------------------------------------------
|
|
124
|
+
// B1: Persistence
|
|
125
|
+
// -----------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
async persistApproval({ approval, applyResource }) {
|
|
128
|
+
if (!approval) {
|
|
129
|
+
return { error: true, reason: 'missing-approval', message: 'approval is required' };
|
|
130
|
+
}
|
|
131
|
+
if (typeof applyResource !== 'function') {
|
|
132
|
+
return { error: true, reason: 'missing-apply-resource', message: 'applyResource function is required' };
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const applyResult = await applyResource(approval);
|
|
136
|
+
return { error: false, approval, applyResult };
|
|
137
|
+
} catch (err) {
|
|
138
|
+
return { error: true, reason: 'persist-failed', message: err?.message || 'applyResource failed' };
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// -----------------------------------------------------------------------
|
|
143
|
+
// B1: Enforcement gate
|
|
144
|
+
// -----------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
enforceApproval({ dispatchRun, action, resources = {} }) {
|
|
147
|
+
const approvals = resources.AgentApproval || [];
|
|
148
|
+
const match = approvals.find(
|
|
149
|
+
(a) => a.spec?.dispatchRun === dispatchRun && a.spec?.action === action
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (!match) {
|
|
153
|
+
return { allowed: false, reason: 'no-approval-found', message: `No approval found for dispatchRun=${dispatchRun} action=${action}`, approval: null };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const phase = match.status?.phase;
|
|
157
|
+
|
|
158
|
+
if (phase === 'Approved') {
|
|
159
|
+
return { allowed: true, approval: clone(match), reason: 'approved' };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (phase === 'Denied') {
|
|
163
|
+
return { allowed: false, reason: 'approval-denied', message: `Approval for dispatchRun=${dispatchRun} action=${action} was denied`, approval: clone(match) };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Pending or unknown
|
|
167
|
+
return { allowed: false, reason: 'approval-pending', message: `Approval for dispatchRun=${dispatchRun} action=${action} is still pending`, approval: clone(match) };
|
|
121
168
|
}
|
|
122
169
|
};
|
|
123
170
|
}
|
|
@@ -3,12 +3,15 @@ import { createAgentStackController } from './agent-stack-controller.js';
|
|
|
3
3
|
import { assembleContextBundle } from './agent-context-bundles.js';
|
|
4
4
|
import { createResource, clone } from './resource-model.js';
|
|
5
5
|
import { createAgentMuxClient } from './agent-mux-client.js';
|
|
6
|
+
import { createAgentMemoryController } from './agent-memory-controller.js';
|
|
7
|
+
import { createAgentApprovalController } from './agent-approval-controller.js';
|
|
8
|
+
import { createAgentWorkspaceController } from './agent-workspace-controller.js';
|
|
6
9
|
|
|
7
10
|
export const AGENT_DISPATCH_CONTROLLER_BOUNDARY = {
|
|
8
11
|
role: 'agent-dispatch-controller',
|
|
9
|
-
scope: 'Manual dispatch orchestration with permission gating and
|
|
10
|
-
owns: ['dispatch creation', 'attempt lifecycle', 'Agent Mux session binding'],
|
|
11
|
-
delegatesTo: ['agent-permission-review', 'agent-stack-controller', 'agent-context-bundles', 'agent-mux-client'],
|
|
12
|
+
scope: 'Manual dispatch orchestration with permission gating, context assembly, and workspace provisioning',
|
|
13
|
+
owns: ['dispatch creation', 'attempt lifecycle', 'Agent Mux session binding', 'workspace provisioning'],
|
|
14
|
+
delegatesTo: ['agent-permission-review', 'agent-stack-controller', 'agent-context-bundles', 'agent-mux-client', 'agent-memory-controller', 'agent-approval-controller', 'agent-workspace-controller'],
|
|
12
15
|
mustNotOwn: ['secret values', 'UI rendering']
|
|
13
16
|
};
|
|
14
17
|
|
|
@@ -16,6 +19,9 @@ export function createAgentDispatchController(options = {}) {
|
|
|
16
19
|
const permissionReviewer = options.permissionReviewer || createPermissionReviewer();
|
|
17
20
|
const stackController = options.stackController || createAgentStackController();
|
|
18
21
|
const agentMuxClient = options.agentMuxClient || createAgentMuxClient();
|
|
22
|
+
const memoryController = options.memoryController || createAgentMemoryController();
|
|
23
|
+
const approvalController = options.approvalController || createAgentApprovalController();
|
|
24
|
+
const workspaceController = options.workspaceController || createAgentWorkspaceController();
|
|
19
25
|
|
|
20
26
|
return {
|
|
21
27
|
role: 'agent-dispatch-controller',
|
|
@@ -32,10 +38,102 @@ export function createAgentDispatchController(options = {}) {
|
|
|
32
38
|
}
|
|
33
39
|
const permissionSnapshot = permissionReviewer.createPermissionSnapshot(review);
|
|
34
40
|
|
|
35
|
-
// 3.
|
|
41
|
+
// 3. Memory snapshot — create if any AgentMemoryRepository exists in resources
|
|
42
|
+
let memorySnapshot = null;
|
|
43
|
+
const memoryRepos = resources.AgentMemoryRepository || [];
|
|
44
|
+
if (memoryRepos.length > 0) {
|
|
45
|
+
const memRepo = memoryRepos[0];
|
|
46
|
+
const timeTravel = memoryController.resolveTimeTravel({ mode: 'current', commits: [] });
|
|
47
|
+
memorySnapshot = memoryController.createMemorySnapshot({
|
|
48
|
+
memoryRepository: memRepo.metadata.name,
|
|
49
|
+
requestedRef: ref,
|
|
50
|
+
resolvedCommit: timeTravel.resolvedCommit || ref,
|
|
51
|
+
queryManifest: {},
|
|
52
|
+
selectedRecords: [],
|
|
53
|
+
selectedDocuments: [],
|
|
54
|
+
ontologyDigest: '',
|
|
55
|
+
namespace,
|
|
56
|
+
organizationRef,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 4. Approval gate — if review requires approval, create approval and return early
|
|
61
|
+
if (review.decision === 'requires-approval') {
|
|
62
|
+
const now = new Date().toISOString();
|
|
63
|
+
const runName = `dispatch-${Date.now()}`;
|
|
64
|
+
|
|
65
|
+
const run = createResource('AgentDispatchRun', { name: runName, namespace }, {
|
|
66
|
+
organizationRef,
|
|
67
|
+
repository,
|
|
68
|
+
sourceRefs: clone(sourceRefs),
|
|
69
|
+
agentStack,
|
|
70
|
+
taskKind: taskKind || 'diagnostic',
|
|
71
|
+
contextBundleRef: null,
|
|
72
|
+
});
|
|
73
|
+
run.status = { phase: 'AwaitingApproval', queuedAt: now };
|
|
74
|
+
if (memorySnapshot) {
|
|
75
|
+
run.spec.memorySnapshotRef = memorySnapshot.metadata.name;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const approvalResult = approvalController.createApprovalRequest({
|
|
79
|
+
dispatchRun: runName,
|
|
80
|
+
action: 'secret-access',
|
|
81
|
+
requestedBy: actor,
|
|
82
|
+
context: `Dispatch requires approval for agent stack: ${agentStack}`,
|
|
83
|
+
namespace,
|
|
84
|
+
organizationRef,
|
|
85
|
+
resources,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
error: false,
|
|
90
|
+
run,
|
|
91
|
+
approval: approvalResult.error ? null : approvalResult.approval,
|
|
92
|
+
awaitingApproval: true,
|
|
93
|
+
memorySnapshot,
|
|
94
|
+
permissionSnapshot,
|
|
95
|
+
review,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 5. Workspace provisioning — reuse or create
|
|
100
|
+
let workspaceResult = null;
|
|
101
|
+
let mountSpec = null;
|
|
102
|
+
const branch = ref || 'main';
|
|
103
|
+
|
|
104
|
+
const reusable = workspaceController.findReusableWorkspace({
|
|
105
|
+
organizationRef, repository, branch, resources,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (reusable) {
|
|
109
|
+
const claimResult = workspaceController.claimWorkspace({
|
|
110
|
+
name: reusable.metadata.name,
|
|
111
|
+
runRef: `dispatch-pending`,
|
|
112
|
+
resources,
|
|
113
|
+
});
|
|
114
|
+
if (!claimResult.error) {
|
|
115
|
+
workspaceResult = { workspace: claimResult.workspace, reused: true };
|
|
116
|
+
const mount = workspaceController.getMountSpec({ workspace: claimResult.workspace });
|
|
117
|
+
if (!mount.error) mountSpec = { volume: mount.volume, volumeMount: mount.volumeMount };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!workspaceResult) {
|
|
122
|
+
const createResult = workspaceController.createWorkspace({
|
|
123
|
+
organizationRef, repository, branch, namespace,
|
|
124
|
+
volumeSpec: {},
|
|
125
|
+
});
|
|
126
|
+
if (!createResult.error) {
|
|
127
|
+
workspaceResult = { workspace: createResult.workspace, pvcManifest: createResult.pvcManifest, reused: false };
|
|
128
|
+
const mount = workspaceController.getMountSpec({ workspace: createResult.workspace });
|
|
129
|
+
if (!mount.error) mountSpec = { volume: mount.volume, volumeMount: mount.volumeMount };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 6. Assemble context bundle
|
|
36
134
|
const contextBundle = assembleContextBundle({ stack, repository, ref, sourceRefs, contextLabels: [], resources });
|
|
37
135
|
|
|
38
|
-
//
|
|
136
|
+
// 7. Create resources
|
|
39
137
|
const now = new Date().toISOString();
|
|
40
138
|
const runName = `dispatch-${Date.now()}`;
|
|
41
139
|
|
|
@@ -48,6 +146,20 @@ export function createAgentDispatchController(options = {}) {
|
|
|
48
146
|
contextBundleRef: contextBundle.metadata.name,
|
|
49
147
|
});
|
|
50
148
|
run.status = { phase: 'Pending', queuedAt: now };
|
|
149
|
+
if (memorySnapshot) {
|
|
150
|
+
run.spec.memorySnapshotRef = memorySnapshot.metadata.name;
|
|
151
|
+
}
|
|
152
|
+
if (workspaceResult) {
|
|
153
|
+
run.spec.workspaceRef = workspaceResult.workspace.metadata.name;
|
|
154
|
+
}
|
|
155
|
+
if (mountSpec) {
|
|
156
|
+
run.spec.mountSpec = mountSpec;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Update workspace runRef to actual dispatch name
|
|
160
|
+
if (workspaceResult) {
|
|
161
|
+
workspaceResult.workspace.status.runRef = runName;
|
|
162
|
+
}
|
|
51
163
|
|
|
52
164
|
const attempt = createResource('AgentDispatchAttempt', { name: `${runName}-attempt-1`, namespace }, {
|
|
53
165
|
organizationRef,
|
|
@@ -58,7 +170,8 @@ export function createAgentDispatchController(options = {}) {
|
|
|
58
170
|
});
|
|
59
171
|
attempt.status = { permissionSnapshot, queueEnteredAt: now };
|
|
60
172
|
|
|
61
|
-
//
|
|
173
|
+
// 7. Try Agent Mux launch
|
|
174
|
+
let transcript = null;
|
|
62
175
|
if (agentMuxClient.isAvailable()) {
|
|
63
176
|
try {
|
|
64
177
|
const session = await agentMuxClient.launchSession({ stack, contextBundle, permissionSnapshot });
|
|
@@ -67,6 +180,16 @@ export function createAgentDispatchController(options = {}) {
|
|
|
67
180
|
attempt.status.agentMuxSessionId = session.sessionId;
|
|
68
181
|
run.status.phase = 'Running';
|
|
69
182
|
attempt.status.startedAt = now;
|
|
183
|
+
|
|
184
|
+
// 8. After successful launch — start SSE subscription + create initial transcript
|
|
185
|
+
const collectedEvents = [];
|
|
186
|
+
const subscription = agentMuxClient.subscribeToEvents(session.runId, (event) => {
|
|
187
|
+
collectedEvents.push(event);
|
|
188
|
+
});
|
|
189
|
+
run.status.sseSubscription = { runId: session.runId, active: true };
|
|
190
|
+
|
|
191
|
+
transcript = agentMuxClient.reconcileTranscript(session.sessionId, collectedEvents, { namespace, organizationRef });
|
|
192
|
+
run.status.transcriptRef = transcript.metadata.name;
|
|
70
193
|
} else {
|
|
71
194
|
run.status.phase = 'Queued';
|
|
72
195
|
run.status.conditions = [{ type: 'AgentMuxBound', status: 'False', reason: 'LaunchFailed', message: 'Agent Mux launch returned no session' }];
|
|
@@ -80,7 +203,7 @@ export function createAgentDispatchController(options = {}) {
|
|
|
80
203
|
run.status.conditions = [{ type: 'AgentMuxBound', status: 'False', reason: 'Unavailable', message: 'Agent Mux gateway not configured' }];
|
|
81
204
|
}
|
|
82
205
|
|
|
83
|
-
return { error: false, run, attempt, contextBundle, permissionSnapshot };
|
|
206
|
+
return { error: false, run, attempt, contextBundle, permissionSnapshot, memorySnapshot, transcript, workspace: workspaceResult, mountSpec };
|
|
84
207
|
}
|
|
85
208
|
};
|
|
86
209
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Agent Gateway Config Controller — Slice 1.2e
|
|
2
|
+
// Manages AgentGatewayConfig resources: gateway name, endpoint URL, feature flags,
|
|
3
|
+
// connection pool settings, and TLS configuration reference.
|
|
4
|
+
|
|
5
|
+
export const AGENT_GATEWAY_CONFIG_CONTROLLER_BOUNDARY = {
|
|
6
|
+
role: 'agent-gateway-config-controller',
|
|
7
|
+
scope: 'AgentGatewayConfig lifecycle: validation, endpoint resolution, feature flags, connection pool, TLS config ref',
|
|
8
|
+
owns: ['gateway config validation', 'endpoint URL', 'feature flags', 'connection pool defaults', 'TLS config ref'],
|
|
9
|
+
delegatesTo: ['resource-model'],
|
|
10
|
+
mustNotOwn: ['secret values', 'dispatch execution', 'Agent Mux sessions', 'adapter implementation']
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const DEFAULT_FEATURE_FLAGS = Object.freeze({
|
|
14
|
+
streaming: true,
|
|
15
|
+
reconnect: true,
|
|
16
|
+
healthCheck: true
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const DEFAULT_POOL_SETTINGS = Object.freeze({
|
|
20
|
+
maxConnections: 10,
|
|
21
|
+
timeoutMs: 30000
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validate an AgentGatewayConfig resource. Returns { valid, errors }.
|
|
26
|
+
* @param {object} resource
|
|
27
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
28
|
+
*/
|
|
29
|
+
export function validateAgentGatewayConfig(resource) {
|
|
30
|
+
const errors = [];
|
|
31
|
+
|
|
32
|
+
// Guard against null/undefined resource
|
|
33
|
+
if (resource == null) {
|
|
34
|
+
errors.push('resource must not be null or undefined');
|
|
35
|
+
return { valid: false, errors };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Validate metadata.name
|
|
39
|
+
if (!resource?.metadata?.name) {
|
|
40
|
+
errors.push('metadata.name is required');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const spec = resource?.spec || {};
|
|
44
|
+
|
|
45
|
+
// Validate organizationRef
|
|
46
|
+
if (!spec.organizationRef) {
|
|
47
|
+
errors.push('spec.organizationRef is required');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Validate endpoint URL
|
|
51
|
+
if (!spec.endpointUrl) {
|
|
52
|
+
errors.push('spec.endpointUrl is required; provide the Agent Mux gateway endpoint URL');
|
|
53
|
+
} else {
|
|
54
|
+
// Basic URL validation: must start with http:// or https:// or ws:// or wss://
|
|
55
|
+
const validProtocols = ['http://', 'https://', 'ws://', 'wss://'];
|
|
56
|
+
const hasValidProtocol = validProtocols.some((p) => spec.endpointUrl.startsWith(p));
|
|
57
|
+
if (!hasValidProtocol) {
|
|
58
|
+
errors.push(`spec.endpointUrl must start with one of: ${validProtocols.join(', ')}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Validate connectionPool if provided
|
|
63
|
+
const pool = spec.connectionPool;
|
|
64
|
+
if (pool != null) {
|
|
65
|
+
if (pool.maxConnections != null && (!Number.isInteger(pool.maxConnections) || pool.maxConnections < 1)) {
|
|
66
|
+
errors.push('spec.connectionPool.maxConnections must be a positive integer');
|
|
67
|
+
}
|
|
68
|
+
if (pool.timeoutMs != null && (!Number.isFinite(pool.timeoutMs) || pool.timeoutMs < 0)) {
|
|
69
|
+
errors.push('spec.connectionPool.timeoutMs must be a non-negative number');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { valid: errors.length === 0, errors };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Factory that returns an AgentGatewayConfig controller instance.
|
|
78
|
+
*/
|
|
79
|
+
export function createAgentGatewayConfigController() {
|
|
80
|
+
return {
|
|
81
|
+
role: 'agent-gateway-config-controller',
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validate an AgentGatewayConfig resource.
|
|
85
|
+
* @param {object} resource
|
|
86
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
87
|
+
*/
|
|
88
|
+
validate(resource) {
|
|
89
|
+
return validateAgentGatewayConfig(resource);
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Return the effective endpoint URL for the gateway config.
|
|
94
|
+
* @param {object} resource
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
getEndpointUrl(resource) {
|
|
98
|
+
if (resource == null) {
|
|
99
|
+
throw new Error('resource must not be null or undefined');
|
|
100
|
+
}
|
|
101
|
+
return resource?.spec?.endpointUrl ?? null;
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Return the effective feature flags for a gateway config.
|
|
106
|
+
* Merges spec.featureFlags with defaults; spec values take precedence.
|
|
107
|
+
* @param {object} resource
|
|
108
|
+
* @returns {{ streaming: boolean, reconnect: boolean, healthCheck: boolean, [key: string]: boolean }}
|
|
109
|
+
*/
|
|
110
|
+
getFeatureFlags(resource) {
|
|
111
|
+
if (resource == null) {
|
|
112
|
+
throw new Error('resource must not be null or undefined');
|
|
113
|
+
}
|
|
114
|
+
const specFlags = resource?.spec?.featureFlags ?? {};
|
|
115
|
+
return { ...DEFAULT_FEATURE_FLAGS, ...specFlags };
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Return the effective connection pool settings for a gateway config.
|
|
120
|
+
* Merges spec.connectionPool with defaults; spec values take precedence.
|
|
121
|
+
* @param {object} resource
|
|
122
|
+
* @returns {{ maxConnections: number, timeoutMs: number }}
|
|
123
|
+
*/
|
|
124
|
+
getConnectionPool(resource) {
|
|
125
|
+
if (resource == null) {
|
|
126
|
+
throw new Error('resource must not be null or undefined');
|
|
127
|
+
}
|
|
128
|
+
const specPool = resource?.spec?.connectionPool ?? {};
|
|
129
|
+
return {
|
|
130
|
+
maxConnections: specPool.maxConnections ?? DEFAULT_POOL_SETTINGS.maxConnections,
|
|
131
|
+
timeoutMs: specPool.timeoutMs ?? DEFAULT_POOL_SETTINGS.timeoutMs
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Return the TLS configuration reference from the spec, or null if not set.
|
|
137
|
+
* @param {object} resource
|
|
138
|
+
* @returns {string|null}
|
|
139
|
+
*/
|
|
140
|
+
getTlsConfigRef(resource) {
|
|
141
|
+
if (resource == null) {
|
|
142
|
+
throw new Error('resource must not be null or undefined');
|
|
143
|
+
}
|
|
144
|
+
return resource?.spec?.tlsConfigRef ?? null;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|