@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,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 context assembly',
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. Assemble context bundle
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
- // 4. Create resources
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
- // 5. Try Agent Mux launch
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
+ }