@a5c-ai/krate 5.0.1-staging.00fa5317c

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 (256) hide show
  1. package/Dockerfile +31 -0
  2. package/README.md +183 -0
  3. package/bin/krate-demo.mjs +23 -0
  4. package/bin/krate-server.mjs +14 -0
  5. package/dist/krate-controller-ui.json +3205 -0
  6. package/dist/krate-lifecycle.json +201 -0
  7. package/dist/krate-runtime-snapshot.json +3125 -0
  8. package/dist/krate-summary.json +724 -0
  9. package/docs/README.md +61 -0
  10. package/docs/agents/README.md +83 -0
  11. package/docs/agents/acceptance-test-matrix.md +193 -0
  12. package/docs/agents/agent-mux-adapter-contract.md +167 -0
  13. package/docs/agents/agent-mux-source-map.md +310 -0
  14. package/docs/agents/agent-run-memory-import-spec.md +256 -0
  15. package/docs/agents/agent-stack-management-spec.md +421 -0
  16. package/docs/agents/api-contract-spec.md +309 -0
  17. package/docs/agents/artifacts-writeback-spec.md +145 -0
  18. package/docs/agents/chart-packaging-spec.md +128 -0
  19. package/docs/agents/ci-orchestration-spec.md +140 -0
  20. package/docs/agents/context-assembly-spec.md +219 -0
  21. package/docs/agents/controller-reconciliation-spec.md +255 -0
  22. package/docs/agents/crd-schema-spec.md +315 -0
  23. package/docs/agents/decision-log-open-questions.md +169 -0
  24. package/docs/agents/developer-implementation-checklist.md +329 -0
  25. package/docs/agents/dispatching-design.md +262 -0
  26. package/docs/agents/gaps-agent-mux-to-krate-crds.md +298 -0
  27. package/docs/agents/glossary.md +66 -0
  28. package/docs/agents/implementation-blueprint.md +324 -0
  29. package/docs/agents/implementation-rollout-slices.md +251 -0
  30. package/docs/agents/memory-context-integration-spec.md +194 -0
  31. package/docs/agents/memory-ontology-schema-spec.md +253 -0
  32. package/docs/agents/memory-operations-runbook.md +121 -0
  33. package/docs/agents/mvp-vertical-slice-spec.md +146 -0
  34. package/docs/agents/observability-audit-spec.md +265 -0
  35. package/docs/agents/operator-runbook.md +174 -0
  36. package/docs/agents/org-memory-api-payload-examples.md +333 -0
  37. package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
  38. package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
  39. package/docs/agents/org-memory-ui-implementation-map.md +114 -0
  40. package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
  41. package/docs/agents/org-resource-model-delta-spec.md +111 -0
  42. package/docs/agents/org-route-resource-model-spec.md +183 -0
  43. package/docs/agents/org-scoping-namespace-spec.md +114 -0
  44. package/docs/agents/rbac-secrets-management-spec.md +406 -0
  45. package/docs/agents/repository-page-integration-spec.md +255 -0
  46. package/docs/agents/resource-contract-examples.md +808 -0
  47. package/docs/agents/resource-relationship-map.md +190 -0
  48. package/docs/agents/security-threat-model.md +188 -0
  49. package/docs/agents/shared-memory-company-brain-spec.md +358 -0
  50. package/docs/agents/storage-migration-spec.md +168 -0
  51. package/docs/agents/subagent-orchestration-spec.md +152 -0
  52. package/docs/agents/system-overview.md +88 -0
  53. package/docs/agents/tools-mcp-skills-spec.md +189 -0
  54. package/docs/agents/traceability-matrix.md +79 -0
  55. package/docs/agents/ui-flow-spec.md +211 -0
  56. package/docs/agents/ui-ux-system-spec.md +426 -0
  57. package/docs/agents/workspace-lifecycle-spec.md +166 -0
  58. package/docs/architecture-spec.md +78 -0
  59. package/docs/components/control-plane.md +78 -0
  60. package/docs/components/data-plane.md +69 -0
  61. package/docs/components/hooks-events.md +67 -0
  62. package/docs/components/identity-rbac-policy.md +73 -0
  63. package/docs/components/kubevela-oam.md +70 -0
  64. package/docs/components/operations-publishing.md +81 -0
  65. package/docs/components/runners-ci.md +66 -0
  66. package/docs/components/web-ui.md +94 -0
  67. package/docs/external/README.md +47 -0
  68. package/docs/external/bidirectional-sync-design.md +134 -0
  69. package/docs/external/cicd-interface.md +64 -0
  70. package/docs/external/external-backend-controllers.md +170 -0
  71. package/docs/external/external-backend-crds.md +234 -0
  72. package/docs/external/external-backend-ui-spec.md +151 -0
  73. package/docs/external/external-backend-ux-flows.md +115 -0
  74. package/docs/external/external-object-mapping.md +125 -0
  75. package/docs/external/git-forge-interface.md +68 -0
  76. package/docs/external/github-integration-design.md +151 -0
  77. package/docs/external/issue-tracking-interface.md +66 -0
  78. package/docs/external/provider-capability-manifests.md +204 -0
  79. package/docs/external/provider-catalog.md +139 -0
  80. package/docs/external/provider-rollout-testing.md +78 -0
  81. package/docs/external/research-results.md +48 -0
  82. package/docs/external/security-auth-permissions.md +81 -0
  83. package/docs/external/sync-state-machines.md +108 -0
  84. package/docs/external/unified-external-backend-model.md +107 -0
  85. package/docs/external/user-facing-changes.md +67 -0
  86. package/docs/gaps.md +161 -0
  87. package/docs/install.md +94 -0
  88. package/docs/krate-design.md +334 -0
  89. package/docs/local-minikube.md +55 -0
  90. package/docs/ontology/README.md +32 -0
  91. package/docs/ontology/bounded-contexts.md +29 -0
  92. package/docs/ontology/events-and-hooks.md +32 -0
  93. package/docs/ontology/oam-kubevela.md +32 -0
  94. package/docs/ontology/operations-and-release.md +25 -0
  95. package/docs/ontology/personas-and-actors.md +32 -0
  96. package/docs/ontology/policies-and-invariants.md +33 -0
  97. package/docs/ontology/problem-space.md +30 -0
  98. package/docs/ontology/resource-contracts.md +40 -0
  99. package/docs/ontology/resource-taxonomy.md +42 -0
  100. package/docs/ontology/runners-and-ci.md +29 -0
  101. package/docs/ontology/solution-space.md +24 -0
  102. package/docs/ontology/storage-and-data-boundaries.md +29 -0
  103. package/docs/ontology/validation-matrix.md +24 -0
  104. package/docs/ontology/web-ui-excellent-flows.md +32 -0
  105. package/docs/ontology/workflows.md +39 -0
  106. package/docs/ontology/world.md +35 -0
  107. package/docs/openapi.yaml +1275 -0
  108. package/docs/product-requirements.md +62 -0
  109. package/docs/roadmap-mvp.md +87 -0
  110. package/docs/system-requirements.md +90 -0
  111. package/docs/tests/README.md +53 -0
  112. package/docs/tests/agent-qa-plan.md +63 -0
  113. package/docs/tests/browser-ui-tests.md +62 -0
  114. package/docs/tests/ci-quality-gates.md +48 -0
  115. package/docs/tests/coverage-model.md +64 -0
  116. package/docs/tests/e2e-scenario-tests.md +53 -0
  117. package/docs/tests/fixtures-test-data.md +63 -0
  118. package/docs/tests/observability-reliability-tests.md +54 -0
  119. package/docs/tests/product-test-matrix.md +145 -0
  120. package/docs/tests/qa-adoption-roadmap.md +130 -0
  121. package/docs/tests/qa-automation-plan.md +101 -0
  122. package/docs/tests/security-compliance-tests.md +57 -0
  123. package/docs/tests/test-framework-tools.md +88 -0
  124. package/docs/tests/test-suite-layout.md +121 -0
  125. package/docs/tests/unit-integration-tests.md +48 -0
  126. package/docs/todo-kyverno +714 -0
  127. package/docs/todos.md +4 -0
  128. package/docs/user-stories.md +78 -0
  129. package/examples/minikube-demo.yaml +190 -0
  130. package/examples/oam-application.yaml +23 -0
  131. package/examples/policy-kyverno-pr-title.yaml +18 -0
  132. package/package.json +63 -0
  133. package/scripts/build.mjs +29 -0
  134. package/scripts/setup-minikube.mjs +65 -0
  135. package/scripts/smoke.mjs +37 -0
  136. package/scripts/validate-doc-coverage.mjs +152 -0
  137. package/scripts/validate-package.mjs +93 -0
  138. package/scripts/validate-ui.mjs +278 -0
  139. package/src/agent-adapter-controller.js +169 -0
  140. package/src/agent-approval-controller.js +170 -0
  141. package/src/agent-context-bundles.js +242 -0
  142. package/src/agent-dispatch-controller.js +209 -0
  143. package/src/agent-gateway-config-controller.js +147 -0
  144. package/src/agent-memory-controller.js +357 -0
  145. package/src/agent-memory-import.js +327 -0
  146. package/src/agent-memory-query.js +292 -0
  147. package/src/agent-memory-repository-source-controller.js +255 -0
  148. package/src/agent-mux-client.js +280 -0
  149. package/src/agent-permission-review.js +250 -0
  150. package/src/agent-project-controller.js +117 -0
  151. package/src/agent-provider-config-controller.js +150 -0
  152. package/src/agent-secret-config-grant-controller.js +282 -0
  153. package/src/agent-session-transcript-controller.js +189 -0
  154. package/src/agent-stack-controller.js +347 -0
  155. package/src/agent-subagent-controller.js +160 -0
  156. package/src/agent-transport-binding-controller.js +121 -0
  157. package/src/agent-trigger-controller.js +381 -0
  158. package/src/agent-workspace-controller.js +702 -0
  159. package/src/agent-writeback-controller.js +302 -0
  160. package/src/api-controller.js +541 -0
  161. package/src/argocd-gitops.js +43 -0
  162. package/src/async-controller.js +207 -0
  163. package/src/audit-controller.js +191 -0
  164. package/src/auth.js +307 -0
  165. package/src/component-catalog.js +41 -0
  166. package/src/control-plane.js +136 -0
  167. package/src/controller-client.js +72 -0
  168. package/src/controller-ui.js +617 -0
  169. package/src/data-plane.js +179 -0
  170. package/src/event-bus.js +61 -0
  171. package/src/external/conflict-controller.js +225 -0
  172. package/src/external/github/auth.js +96 -0
  173. package/src/external/github/cicd.js +180 -0
  174. package/src/external/github/git-forge.js +240 -0
  175. package/src/external/github/index.js +144 -0
  176. package/src/external/github/issue-tracking.js +163 -0
  177. package/src/external/provider-adapter.js +161 -0
  178. package/src/external/provider-resource-factory.js +161 -0
  179. package/src/external/sync-controller.js +235 -0
  180. package/src/external/webhook-controller.js +144 -0
  181. package/src/external/write-controller.js +283 -0
  182. package/src/gitea-backend.js +131 -0
  183. package/src/gitea-service.js +173 -0
  184. package/src/handoff.js +98 -0
  185. package/src/hooks-events.js +63 -0
  186. package/src/http-server.js +377 -0
  187. package/src/identity-policy.js +86 -0
  188. package/src/index.js +57 -0
  189. package/src/kubernetes-controller-async.js +511 -0
  190. package/src/kubernetes-controller.js +878 -0
  191. package/src/kubernetes-resource-gateway.js +48 -0
  192. package/src/notification-controller.js +178 -0
  193. package/src/operations.js +112 -0
  194. package/src/org-scoping.js +5 -0
  195. package/src/resource-model.js +221 -0
  196. package/src/runner-controller.js +272 -0
  197. package/src/runners-ci.js +48 -0
  198. package/src/runtime.js +196 -0
  199. package/src/snapshot-cache.js +157 -0
  200. package/src/web-ui.js +40 -0
  201. package/tests/agent-adapter-controller.test.js +361 -0
  202. package/tests/agent-approval-controller.test.js +173 -0
  203. package/tests/agent-context-bundles.test.js +278 -0
  204. package/tests/agent-dispatch-controller.test.js +315 -0
  205. package/tests/agent-gateway-config-controller.test.js +386 -0
  206. package/tests/agent-memory-controller.test.js +308 -0
  207. package/tests/agent-memory-import-snapshot.test.js +477 -0
  208. package/tests/agent-memory-query.test.js +404 -0
  209. package/tests/agent-memory-repository-source.test.js +514 -0
  210. package/tests/agent-mux-client.test.js +204 -0
  211. package/tests/agent-permission-review-v2.test.js +317 -0
  212. package/tests/agent-permission-review.test.js +209 -0
  213. package/tests/agent-project-controller.test.js +302 -0
  214. package/tests/agent-provider-config-controller.test.js +376 -0
  215. package/tests/agent-resources.test.js +228 -0
  216. package/tests/agent-secret-config-grant.test.js +231 -0
  217. package/tests/agent-session-transcript-controller.test.js +499 -0
  218. package/tests/agent-stack-controller.test.js +221 -0
  219. package/tests/agent-subagent-controller.test.js +201 -0
  220. package/tests/agent-transport-binding-controller.test.js +294 -0
  221. package/tests/agent-trigger-controller.test.js +211 -0
  222. package/tests/agent-trigger-routes.test.js +190 -0
  223. package/tests/agent-trigger-sources.test.js +245 -0
  224. package/tests/agent-workspace-controller.test.js +181 -0
  225. package/tests/agent-writeback.test.js +292 -0
  226. package/tests/approval-persistence.test.js +171 -0
  227. package/tests/async-controller.test.js +252 -0
  228. package/tests/audit-controller.test.js +227 -0
  229. package/tests/codespace-controller.test.js +318 -0
  230. package/tests/deployment.test.js +407 -0
  231. package/tests/e2e/lifecycle.test.js +117 -0
  232. package/tests/event-bus-integration.test.js +190 -0
  233. package/tests/external-github-forge.test.js +560 -0
  234. package/tests/external-github-issues-cicd.test.js +520 -0
  235. package/tests/external-integration.test.js +470 -0
  236. package/tests/external-persistence.test.js +340 -0
  237. package/tests/external-provider-adapter.test.js +365 -0
  238. package/tests/external-resource-model.test.js +215 -0
  239. package/tests/external-webhook-sync.test.js +287 -0
  240. package/tests/external-write-conflict.test.js +353 -0
  241. package/tests/gitea-service.test.js +253 -0
  242. package/tests/health-check-real.test.js +165 -0
  243. package/tests/integration/full-flow.test.js +266 -0
  244. package/tests/krate.test.js +756 -0
  245. package/tests/memory-search-wiring.test.js +270 -0
  246. package/tests/notification-controller.test.js +196 -0
  247. package/tests/notification-integration.test.js +179 -0
  248. package/tests/org-scoping.test.js +687 -0
  249. package/tests/runner-controller.test.js +327 -0
  250. package/tests/runner-integration.test.js +231 -0
  251. package/tests/session-cookie-hmac.test.js +151 -0
  252. package/tests/snapshot-performance.test.js +247 -0
  253. package/tests/sse-events.test.js +107 -0
  254. package/tests/webhook-trigger.test.js +198 -0
  255. package/tests/workspace-volumes.test.js +312 -0
  256. package/tests/writeback-persistence.test.js +207 -0
@@ -0,0 +1,170 @@
1
+ import { createResource, clone } from './resource-model.js';
2
+
3
+ export const AGENT_APPROVAL_CONTROLLER_BOUNDARY = {
4
+ role: 'agent-approval-controller',
5
+ scope: 'Human gate lifecycle for agent tool-use, secret-access, write-back, release, and escalation actions',
6
+ owns: ['approval creation', 'decision recording', 'approval lookup', 'duplicate detection'],
7
+ delegatesTo: ['resource-model'],
8
+ mustNotOwn: ['secret values', 'agent execution', 'UI rendering']
9
+ };
10
+
11
+ const VALID_ACTIONS = new Set(['tool-use', 'secret-access', 'write-back', 'release', 'escalation']);
12
+
13
+ export function createAgentApprovalController() {
14
+ return {
15
+ role: 'agent-approval-controller',
16
+
17
+ createApprovalRequest({ dispatchRun, action, requestedBy, context, namespace = 'default', organizationRef = 'default', resources = {} }) {
18
+ if (!action || !VALID_ACTIONS.has(action)) {
19
+ return { error: true, reason: 'invalid-action', message: `Invalid action type: ${action}. Must be one of: ${[...VALID_ACTIONS].join(', ')}` };
20
+ }
21
+ if (!dispatchRun) {
22
+ return { error: true, reason: 'missing-dispatch-run', message: 'dispatchRun is required' };
23
+ }
24
+ if (!requestedBy) {
25
+ return { error: true, reason: 'missing-requested-by', message: 'requestedBy is required' };
26
+ }
27
+
28
+ // Check for duplicate pending approval
29
+ const existing = (resources.AgentApproval || []).find(
30
+ (a) => a.spec?.dispatchRun === dispatchRun && a.spec?.action === action && (!a.status?.phase || a.status.phase === 'Pending')
31
+ );
32
+ if (existing) {
33
+ return { error: false, approval: clone(existing), duplicate: true };
34
+ }
35
+
36
+ const now = new Date().toISOString();
37
+ const approvalName = `approval-${dispatchRun}-${action}-${Date.now()}`;
38
+
39
+ const approval = createResource('AgentApproval', { name: approvalName, namespace }, {
40
+ organizationRef,
41
+ dispatchRun,
42
+ action,
43
+ requestedBy,
44
+ description: context || `Agent requests permission to perform: ${action}`,
45
+ requestedAt: now
46
+ });
47
+ approval.status = { phase: 'Pending', createdAt: now };
48
+
49
+ return { error: false, approval, duplicate: false };
50
+ },
51
+
52
+ recordDecision({ approvalName, decision, decidedBy, reason, namespace = 'default', organizationRef = 'default', resources = {} }) {
53
+ if (!approvalName) {
54
+ return { error: true, reason: 'missing-approval-name', message: 'approvalName is required' };
55
+ }
56
+ if (!decision || (decision !== 'approve' && decision !== 'deny')) {
57
+ return { error: true, reason: 'invalid-decision', message: `Invalid decision: ${decision}. Must be 'approve' or 'deny'` };
58
+ }
59
+ if (!decidedBy) {
60
+ return { error: true, reason: 'missing-decided-by', message: 'decidedBy is required' };
61
+ }
62
+
63
+ const approvals = resources.AgentApproval || [];
64
+ const approval = approvals.find((a) => a.metadata?.name === approvalName);
65
+ if (!approval) {
66
+ return { error: true, reason: 'not-found', message: `AgentApproval not found: ${approvalName}` };
67
+ }
68
+
69
+ if (approval.status?.phase && approval.status.phase !== 'Pending') {
70
+ return { error: true, reason: 'already-decided', message: `AgentApproval ${approvalName} has already been decided: ${approval.status.phase}` };
71
+ }
72
+
73
+ const now = new Date().toISOString();
74
+ const phase = decision === 'approve' ? 'Approved' : 'Denied';
75
+
76
+ const updated = clone(approval);
77
+ updated.status = {
78
+ ...updated.status,
79
+ phase,
80
+ decidedBy,
81
+ decidedAt: now,
82
+ reason: reason || undefined
83
+ };
84
+
85
+ return { error: false, approval: updated };
86
+ },
87
+
88
+ isActionApproved({ dispatchRun, action, resources = {} }) {
89
+ const approvals = resources.AgentApproval || [];
90
+ const match = approvals.find(
91
+ (a) => a.spec?.dispatchRun === dispatchRun && a.spec?.action === action
92
+ );
93
+
94
+ if (!match) {
95
+ return { approved: false, approval: null, reason: 'No approval request found' };
96
+ }
97
+
98
+ if (match.status?.phase === 'Approved') {
99
+ return { approved: true, approval: clone(match), reason: match.status.reason || 'Approved' };
100
+ }
101
+
102
+ if (match.status?.phase === 'Denied') {
103
+ return { approved: false, approval: clone(match), reason: match.status.reason || 'Denied' };
104
+ }
105
+
106
+ return { approved: false, approval: clone(match), reason: 'Approval is still pending' };
107
+ },
108
+
109
+ listPendingApprovals({ organizationRef, resources = {} }) {
110
+ const approvals = resources.AgentApproval || [];
111
+ return approvals.filter((a) => {
112
+ const matchesOrg = !organizationRef || a.spec?.organizationRef === organizationRef;
113
+ const isPending = !a.status?.phase || a.status.phase === 'Pending';
114
+ return matchesOrg && isPending;
115
+ }).map(clone);
116
+ },
117
+
118
+ listApprovalsForRun({ dispatchRun, resources = {} }) {
119
+ const approvals = resources.AgentApproval || [];
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) };
168
+ }
169
+ };
170
+ }
@@ -0,0 +1,242 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createResource, clone } from './resource-model.js';
3
+
4
+ export const AGENT_CONTEXT_BUNDLES_BOUNDARY = {
5
+ role: 'agent-context-bundles',
6
+ scope: 'Context assembly, redaction, and digest computation for agent dispatch',
7
+ owns: ['prompt layer collection', 'redaction patterns', 'size enforcement', 'digest computation'],
8
+ delegatesTo: ['resource-model'],
9
+ mustNotOwn: ['secret values', 'runtime execution', 'Agent Mux sessions']
10
+ };
11
+
12
+ const PROMPT_LAYER_MAX = 64 * 1024; // 64 KiB
13
+ const BUNDLE_MAX = 750 * 1024; // 750 KiB
14
+ const MAX_ATTACHMENTS = 32;
15
+
16
+ // Redaction patterns (ordered by priority)
17
+ const REDACTION_PATTERNS = [
18
+ { kind: 'secret-key', pattern: /(?:API_KEY|API_SECRET|SECRET_KEY|ACCESS_KEY|PRIVATE_KEY|AUTH_TOKEN|PASSWORD|PASSWD|CREDENTIALS?)\s*[=:]\s*['"]?([^\s'"}{,\]]+)/gi },
19
+ { kind: 'provider-token', pattern: /\b(sk-[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{36,}|gho_[a-zA-Z0-9]{36,}|glpat-[a-zA-Z0-9\-_]{20,}|xoxb-[a-zA-Z0-9\-]+|xoxp-[a-zA-Z0-9\-]+)\b/g },
20
+ { kind: 'bearer-token', pattern: /Bearer\s+[a-zA-Z0-9\-._~+\/]+=*/gi },
21
+ { kind: 'private-key', pattern: /-----BEGIN\s+(?:RSA\s+|EC\s+|DSA\s+|OPENSSH\s+)?PRIVATE\s+KEY-----[\s\S]*?-----END\s+(?:RSA\s+|EC\s+|DSA\s+|OPENSSH\s+)?PRIVATE\s+KEY-----/g },
22
+ { kind: 'base64-credential', pattern: /\b[A-Za-z0-9+\/]{40,}={0,2}\b/g },
23
+ ];
24
+
25
+ function sha256(data) {
26
+ return createHash('sha256').update(data).digest('hex');
27
+ }
28
+
29
+ function truncateToLimit(text, limit) {
30
+ if (typeof text !== 'string') return '';
31
+ if (text.length <= limit) return text;
32
+ return text.slice(0, limit);
33
+ }
34
+
35
+ function applyRedactions(text) {
36
+ if (typeof text !== 'string' || text.length === 0) return { text, counts: {} };
37
+ const counts = {};
38
+ let redacted = text;
39
+ for (const { kind, pattern } of REDACTION_PATTERNS) {
40
+ const fresh = new RegExp(pattern.source, pattern.flags);
41
+ const before = redacted;
42
+ redacted = redacted.replace(fresh, (match) => {
43
+ counts[kind] = (counts[kind] || 0) + 1;
44
+ return `[REDACTED:${kind}]`;
45
+ });
46
+ }
47
+ return { text: redacted, counts };
48
+ }
49
+
50
+ function mergeRedactionCounts(target, source) {
51
+ for (const [kind, count] of Object.entries(source)) {
52
+ target[kind] = (target[kind] || 0) + count;
53
+ }
54
+ }
55
+
56
+ export function createRedactionManifest(redactionCounts) {
57
+ const total = Object.values(redactionCounts).reduce((sum, n) => sum + n, 0);
58
+ return { total, byKind: clone(redactionCounts) };
59
+ }
60
+
61
+ export function assembleContextBundle({ stack, repository, ref, sourceRefs = [], contextLabels = [], redactionPolicy, resources = {} }) {
62
+ const namespace = stack?.metadata?.namespace || 'default';
63
+ const organizationRef = stack?.spec?.organizationRef || 'default';
64
+ const allRedactionCounts = {};
65
+
66
+ // Step 1 — Collect prompt layers
67
+ const rawSystem = stack?.spec?.prompt?.system || '';
68
+ const rawDeveloper = stack?.spec?.prompt?.developer || '';
69
+ const rawTask = stack?.spec?.prompt?.task || '';
70
+
71
+ // Collect skill fragments
72
+ const skillFragments = [];
73
+ const skillRefs = stack?.spec?.skillRefs || [];
74
+ const agentSkills = resources.AgentSkill || [];
75
+ for (const skillRef of skillRefs) {
76
+ const skill = agentSkills.find((s) => s.metadata?.name === skillRef);
77
+ if (skill?.spec?.promptFragment) {
78
+ skillFragments.push({ name: skillRef, content: skill.spec.promptFragment });
79
+ }
80
+ }
81
+
82
+ // Collect label fragments
83
+ const labelFragments = [];
84
+ const agentContextLabels = resources.AgentContextLabel || [];
85
+ for (const labelRef of contextLabels) {
86
+ const label = agentContextLabels.find((l) => l.metadata?.name === labelRef);
87
+ if (label?.spec?.promptFragment) {
88
+ labelFragments.push({ name: labelRef, content: label.spec.promptFragment });
89
+ }
90
+ }
91
+
92
+ // Step 2 — Truncate each layer and compute digests
93
+ const system = truncateToLimit(rawSystem, PROMPT_LAYER_MAX);
94
+ const developer = truncateToLimit(rawDeveloper, PROMPT_LAYER_MAX);
95
+ const task = truncateToLimit(rawTask, PROMPT_LAYER_MAX);
96
+
97
+ // Apply redaction to prompt layers
98
+ const systemRedacted = applyRedactions(system);
99
+ mergeRedactionCounts(allRedactionCounts, systemRedacted.counts);
100
+ const developerRedacted = applyRedactions(developer);
101
+ mergeRedactionCounts(allRedactionCounts, developerRedacted.counts);
102
+ const taskRedacted = applyRedactions(task);
103
+ mergeRedactionCounts(allRedactionCounts, taskRedacted.counts);
104
+
105
+ const systemDigest = sha256(systemRedacted.text);
106
+ const developerDigest = sha256(developerRedacted.text);
107
+ const taskDigest = sha256(taskRedacted.text);
108
+
109
+ const skillLayerDigests = [];
110
+ const redactedSkillFragments = [];
111
+ for (const frag of skillFragments) {
112
+ const truncated = truncateToLimit(frag.content, PROMPT_LAYER_MAX);
113
+ const redacted = applyRedactions(truncated);
114
+ mergeRedactionCounts(allRedactionCounts, redacted.counts);
115
+ const digest = sha256(redacted.text);
116
+ skillLayerDigests.push({ role: `skill:${frag.name}`, digest, sizeBytes: redacted.text.length });
117
+ redactedSkillFragments.push({ name: frag.name, content: redacted.text });
118
+ }
119
+
120
+ const labelLayerDigests = [];
121
+ const redactedLabelFragments = [];
122
+ for (const frag of labelFragments) {
123
+ const truncated = truncateToLimit(frag.content, PROMPT_LAYER_MAX);
124
+ const redacted = applyRedactions(truncated);
125
+ mergeRedactionCounts(allRedactionCounts, redacted.counts);
126
+ const digest = sha256(redacted.text);
127
+ labelLayerDigests.push({ role: `label:${frag.name}`, digest, sizeBytes: redacted.text.length });
128
+ redactedLabelFragments.push({ name: frag.name, content: redacted.text });
129
+ }
130
+
131
+ // Step 3 — Collect sources from sourceRefs
132
+ const sources = [];
133
+ const limitedSourceRefs = sourceRefs.slice(0, MAX_ATTACHMENTS);
134
+ for (const srcRef of limitedSourceRefs) {
135
+ const content = truncateToLimit(srcRef.content || '', PROMPT_LAYER_MAX);
136
+ const redacted = applyRedactions(content);
137
+ mergeRedactionCounts(allRedactionCounts, redacted.counts);
138
+ sources.push({
139
+ kind: srcRef.kind || 'unknown',
140
+ ref: srcRef.ref || '',
141
+ content: redacted.text,
142
+ digest: sha256(redacted.text)
143
+ });
144
+ }
145
+
146
+ // Step 5 — Enforce total size
147
+ let wasTruncated = false;
148
+ const TRUNC_MARKER = '[...truncated at 750KiB limit]';
149
+ const TRUNC_MARKER_LEN = TRUNC_MARKER.length;
150
+
151
+ const computeTotalSize = () =>
152
+ systemRedacted.text.length + developerRedacted.text.length + taskRedacted.text.length +
153
+ redactedSkillFragments.reduce((s, f) => s + f.content.length, 0) +
154
+ redactedLabelFragments.reduce((s, f) => s + f.content.length, 0) +
155
+ sources.reduce((s, src) => s + src.content.length, 0);
156
+
157
+ let totalSize = computeTotalSize();
158
+
159
+ if (totalSize > BUNDLE_MAX) {
160
+ wasTruncated = true;
161
+ // Build a list of truncatable items (sources first, then labels, then skills)
162
+ // sorted by descending size so we cut the largest first
163
+ const truncatableItems = [
164
+ ...sources.map((s, i) => ({ type: 'source', index: i })),
165
+ ...redactedLabelFragments.map((f, i) => ({ type: 'label', index: i })),
166
+ ...redactedSkillFragments.map((f, i) => ({ type: 'skill', index: i }))
167
+ ];
168
+
169
+ const getContent = (t) => {
170
+ if (t.type === 'source') return sources[t.index].content;
171
+ if (t.type === 'label') return redactedLabelFragments[t.index].content;
172
+ return redactedSkillFragments[t.index].content;
173
+ };
174
+
175
+ truncatableItems.sort((a, b) => getContent(b).length - getContent(a).length);
176
+
177
+ for (const target of truncatableItems) {
178
+ totalSize = computeTotalSize();
179
+ if (totalSize <= BUNDLE_MAX) break;
180
+
181
+ const currentContent = getContent(target);
182
+ const excess = totalSize - BUNDLE_MAX;
183
+ // We need to remove at least `excess` bytes, but also account for the marker we add
184
+ const cutAmount = excess + TRUNC_MARKER_LEN;
185
+ const newLen = Math.max(0, currentContent.length - cutAmount);
186
+ const truncatedContent = currentContent.slice(0, newLen) + TRUNC_MARKER;
187
+
188
+ if (target.type === 'source') {
189
+ sources[target.index].content = truncatedContent;
190
+ sources[target.index].digest = sha256(truncatedContent);
191
+ } else if (target.type === 'label') {
192
+ redactedLabelFragments[target.index].content = truncatedContent;
193
+ labelLayerDigests[target.index].digest = sha256(truncatedContent);
194
+ labelLayerDigests[target.index].sizeBytes = truncatedContent.length;
195
+ } else if (target.type === 'skill') {
196
+ redactedSkillFragments[target.index].content = truncatedContent;
197
+ skillLayerDigests[target.index].digest = sha256(truncatedContent);
198
+ skillLayerDigests[target.index].sizeBytes = truncatedContent.length;
199
+ }
200
+ }
201
+ }
202
+
203
+ // Step 6 — Compute bundle digest
204
+ const promptLayers = [
205
+ { role: 'system', digest: systemDigest, sizeBytes: systemRedacted.text.length },
206
+ { role: 'developer', digest: developerDigest, sizeBytes: developerRedacted.text.length },
207
+ { role: 'task', digest: taskDigest, sizeBytes: taskRedacted.text.length },
208
+ ...skillLayerDigests,
209
+ ...labelLayerDigests,
210
+ ];
211
+
212
+ const digestPayload = JSON.stringify({
213
+ promptLayers,
214
+ sources: sources.map((s) => ({ kind: s.kind, ref: s.ref, digest: s.digest }))
215
+ });
216
+ const digest = sha256(digestPayload);
217
+
218
+ const redactionManifest = createRedactionManifest(allRedactionCounts);
219
+
220
+ // Step 7 — Build the resource
221
+ const resource = createResource('AgentContextBundle', { name: `bundle-${digest.slice(0, 12)}`, namespace }, {
222
+ organizationRef,
223
+ dispatchRun: '',
224
+ digest,
225
+ sources: limitedSourceRefs.map((s) => ({ kind: s.kind || 'unknown', ref: s.ref || '' })),
226
+ promptLayers,
227
+ redactions: redactionManifest,
228
+ limits: { maxBytes: BUNDLE_MAX, truncated: wasTruncated },
229
+ });
230
+
231
+ // Store actual text content in _content (in-memory only, not part of resource spec)
232
+ resource._content = {
233
+ system: systemRedacted.text,
234
+ developer: developerRedacted.text,
235
+ task: taskRedacted.text,
236
+ skillFragments: redactedSkillFragments,
237
+ labelFragments: redactedLabelFragments,
238
+ sources
239
+ };
240
+
241
+ return resource;
242
+ }
@@ -0,0 +1,209 @@
1
+ import { createPermissionReviewer } from './agent-permission-review.js';
2
+ import { createAgentStackController } from './agent-stack-controller.js';
3
+ import { assembleContextBundle } from './agent-context-bundles.js';
4
+ import { createResource, clone } from './resource-model.js';
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';
9
+
10
+ export const AGENT_DISPATCH_CONTROLLER_BOUNDARY = {
11
+ role: 'agent-dispatch-controller',
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'],
15
+ mustNotOwn: ['secret values', 'UI rendering']
16
+ };
17
+
18
+ export function createAgentDispatchController(options = {}) {
19
+ const permissionReviewer = options.permissionReviewer || createPermissionReviewer();
20
+ const stackController = options.stackController || createAgentStackController();
21
+ const agentMuxClient = options.agentMuxClient || createAgentMuxClient();
22
+ const memoryController = options.memoryController || createAgentMemoryController();
23
+ const approvalController = options.approvalController || createAgentApprovalController();
24
+ const workspaceController = options.workspaceController || createAgentWorkspaceController();
25
+
26
+ return {
27
+ role: 'agent-dispatch-controller',
28
+
29
+ async createManualDispatch({ repository, ref, sourceRefs = [], agentStack, taskKind, actor, namespace = 'default', organizationRef = 'default', resources = {} }) {
30
+ // 1. Find stack
31
+ const stack = (resources.AgentStack || []).find(s => s.metadata?.name === agentStack);
32
+ if (!stack) return { error: true, reason: 'stack-not-found', message: `AgentStack '${agentStack}' not found` };
33
+
34
+ // 2. Permission review
35
+ const review = permissionReviewer.reviewPermissions({ repository, ref, actor, agentStack, resources });
36
+ if (review.decision === 'denied') {
37
+ return { error: true, reason: 'permission-denied', message: 'Dispatch denied by permission review', review };
38
+ }
39
+ const permissionSnapshot = permissionReviewer.createPermissionSnapshot(review);
40
+
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
134
+ const contextBundle = assembleContextBundle({ stack, repository, ref, sourceRefs, contextLabels: [], resources });
135
+
136
+ // 7. Create resources
137
+ const now = new Date().toISOString();
138
+ const runName = `dispatch-${Date.now()}`;
139
+
140
+ const run = createResource('AgentDispatchRun', { name: runName, namespace }, {
141
+ organizationRef,
142
+ repository,
143
+ sourceRefs: clone(sourceRefs),
144
+ agentStack,
145
+ taskKind: taskKind || 'diagnostic',
146
+ contextBundleRef: contextBundle.metadata.name,
147
+ });
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
+ }
163
+
164
+ const attempt = createResource('AgentDispatchAttempt', { name: `${runName}-attempt-1`, namespace }, {
165
+ organizationRef,
166
+ agentDispatchRun: runName,
167
+ attemptReason: 'initial',
168
+ agentStackSnapshot: clone(stack.spec),
169
+ contextBundleDigest: contextBundle.spec.digest,
170
+ });
171
+ attempt.status = { permissionSnapshot, queueEnteredAt: now };
172
+
173
+ // 7. Try Agent Mux launch
174
+ let transcript = null;
175
+ if (agentMuxClient.isAvailable()) {
176
+ try {
177
+ const session = await agentMuxClient.launchSession({ stack, contextBundle, permissionSnapshot });
178
+ if (session && session.runId) {
179
+ attempt.status.agentMuxRunId = session.runId;
180
+ attempt.status.agentMuxSessionId = session.sessionId;
181
+ run.status.phase = 'Running';
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;
193
+ } else {
194
+ run.status.phase = 'Queued';
195
+ run.status.conditions = [{ type: 'AgentMuxBound', status: 'False', reason: 'LaunchFailed', message: 'Agent Mux launch returned no session' }];
196
+ }
197
+ } catch {
198
+ run.status.phase = 'Queued';
199
+ run.status.conditions = [{ type: 'AgentMuxBound', status: 'False', reason: 'LaunchFailed' }];
200
+ }
201
+ } else {
202
+ run.status.phase = 'Queued';
203
+ run.status.conditions = [{ type: 'AgentMuxBound', status: 'False', reason: 'Unavailable', message: 'Agent Mux gateway not configured' }];
204
+ }
205
+
206
+ return { error: false, run, attempt, contextBundle, permissionSnapshot, memorySnapshot, transcript, workspace: workspaceResult, mountSpec };
207
+ }
208
+ };
209
+ }