@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,204 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it } from 'node:test';
3
+ import { createAgentMuxClient, parseSseLines, AGENT_MUX_CLIENT_BOUNDARY } from '../src/agent-mux-client.js';
4
+
5
+ describe('AGENT_MUX_CLIENT_BOUNDARY', () => {
6
+ it('declares the expected role and scope', () => {
7
+ assert.equal(AGENT_MUX_CLIENT_BOUNDARY.role, 'agent-mux-client');
8
+ assert.ok(AGENT_MUX_CLIENT_BOUNDARY.scope.includes('HTTP/SSE'));
9
+ assert.ok(AGENT_MUX_CLIENT_BOUNDARY.owns.includes('gateway HTTP calls'));
10
+ assert.ok(AGENT_MUX_CLIENT_BOUNDARY.delegatesTo.includes('resource-model'));
11
+ });
12
+ });
13
+
14
+ describe('createAgentMuxClient — isAvailable', () => {
15
+ it('returns false when enabled is false', () => {
16
+ const client = createAgentMuxClient({ gateway: 'http://localhost:8080', enabled: false });
17
+ assert.equal(client.isAvailable(), false);
18
+ });
19
+
20
+ it('returns false when gateway is empty', () => {
21
+ const client = createAgentMuxClient({ gateway: '', enabled: true });
22
+ assert.equal(client.isAvailable(), false);
23
+ });
24
+
25
+ it('returns false when no options are provided', () => {
26
+ const client = createAgentMuxClient();
27
+ assert.equal(client.isAvailable(), false);
28
+ });
29
+
30
+ it('returns true when enabled is true and gateway is set', () => {
31
+ const client = createAgentMuxClient({ gateway: 'http://localhost:8080', enabled: true });
32
+ assert.equal(client.isAvailable(), true);
33
+ });
34
+ });
35
+
36
+ describe('createAgentMuxClient — queryCapabilities', () => {
37
+ it('returns null when client is unavailable', async () => {
38
+ const client = createAgentMuxClient({ enabled: false });
39
+ const result = await client.queryCapabilities('claude-code');
40
+ assert.equal(result, null);
41
+ });
42
+ });
43
+
44
+ describe('createAgentMuxClient — launchSession', () => {
45
+ it('returns null when client is unavailable', async () => {
46
+ const client = createAgentMuxClient({ enabled: false });
47
+ const result = await client.launchSession({
48
+ stack: { baseAgent: 'claude-code' },
49
+ contextBundle: {},
50
+ permissionSnapshot: {},
51
+ workspace: {}
52
+ });
53
+ assert.equal(result, null);
54
+ });
55
+ });
56
+
57
+ describe('parseSseLines', () => {
58
+ it('parses valid SSE data lines', () => {
59
+ const text = [
60
+ 'data: {"role":"assistant","content":"hello"}',
61
+ '',
62
+ 'data: {"role":"user","content":"world"}',
63
+ '',
64
+ ].join('\n');
65
+ const events = parseSseLines(text);
66
+ assert.equal(events.length, 2);
67
+ assert.equal(events[0].role, 'assistant');
68
+ assert.equal(events[0].content, 'hello');
69
+ assert.equal(events[1].role, 'user');
70
+ assert.equal(events[1].content, 'world');
71
+ });
72
+
73
+ it('handles multiple data lines in a single block', () => {
74
+ const text = [
75
+ 'data: {"seq":1}',
76
+ 'data: {"seq":2}',
77
+ '',
78
+ ].join('\n');
79
+ const events = parseSseLines(text);
80
+ assert.equal(events.length, 2);
81
+ assert.equal(events[0].seq, 1);
82
+ assert.equal(events[1].seq, 2);
83
+ });
84
+
85
+ it('skips malformed JSON gracefully', () => {
86
+ const text = [
87
+ 'data: {"valid":true}',
88
+ 'data: not-json',
89
+ 'data: {"also":"valid"}',
90
+ '',
91
+ ].join('\n');
92
+ const events = parseSseLines(text);
93
+ assert.equal(events.length, 2);
94
+ assert.equal(events[0].valid, true);
95
+ assert.equal(events[1].also, 'valid');
96
+ });
97
+
98
+ it('ignores non-data SSE lines', () => {
99
+ const text = [
100
+ 'event: message',
101
+ 'id: 42',
102
+ 'data: {"role":"assistant","content":"test"}',
103
+ 'retry: 3000',
104
+ '',
105
+ ].join('\n');
106
+ const events = parseSseLines(text);
107
+ assert.equal(events.length, 1);
108
+ assert.equal(events[0].role, 'assistant');
109
+ });
110
+
111
+ it('returns empty array for empty text', () => {
112
+ assert.deepEqual(parseSseLines(''), []);
113
+ });
114
+
115
+ it('returns empty array for text with no data lines', () => {
116
+ assert.deepEqual(parseSseLines('event: ping\nid: 1\n\n'), []);
117
+ });
118
+ });
119
+
120
+ describe('createAgentMuxClient — reconcileTranscript', () => {
121
+ it('creates a valid AgentSessionTranscript resource from events', () => {
122
+ const client = createAgentMuxClient({ gateway: 'http://localhost:8080', enabled: true });
123
+ const events = [
124
+ { role: 'user', content: 'Fix the bug', timestamp: '2026-01-01T00:00:00Z' },
125
+ { role: 'assistant', content: 'Looking at the code...', timestamp: '2026-01-01T00:00:01Z', usage: { inputTokens: 100, outputTokens: 50 } },
126
+ { role: 'assistant', content: 'Fixed it.', timestamp: '2026-01-01T00:00:02Z', usage: { inputTokens: 200, outputTokens: 80 } },
127
+ ];
128
+
129
+ const transcript = client.reconcileTranscript('sess-123', events, { namespace: 'krate-org-acme', organizationRef: 'acme' });
130
+
131
+ assert.equal(transcript.kind, 'AgentSessionTranscript');
132
+ assert.equal(transcript.apiVersion, 'krate.a5c.ai/v1alpha1');
133
+ assert.equal(transcript.metadata.name, 'transcript-sess-123');
134
+ assert.equal(transcript.metadata.namespace, 'krate-org-acme');
135
+ assert.equal(transcript.spec.organizationRef, 'acme');
136
+ assert.equal(transcript.spec.sessionRef, 'sess-123');
137
+ assert.equal(transcript.spec.messages.length, 3);
138
+ assert.equal(transcript.spec.messages[0].role, 'user');
139
+ assert.equal(transcript.spec.messages[0].content, 'Fix the bug');
140
+ assert.equal(transcript.spec.messages[1].role, 'assistant');
141
+ assert.equal(transcript.spec.cost.inputTokens, 300);
142
+ assert.equal(transcript.spec.cost.outputTokens, 130);
143
+ assert.equal(transcript.spec.cost.totalTokens, 430);
144
+ assert.equal(transcript.status.phase, 'Reconciled');
145
+ });
146
+
147
+ it('handles empty events array', () => {
148
+ const client = createAgentMuxClient({ gateway: 'http://localhost:8080', enabled: true });
149
+ const transcript = client.reconcileTranscript('sess-empty', [], { namespace: 'default', organizationRef: 'default' });
150
+
151
+ assert.equal(transcript.kind, 'AgentSessionTranscript');
152
+ assert.equal(transcript.spec.sessionRef, 'sess-empty');
153
+ assert.equal(transcript.spec.messages.length, 0);
154
+ assert.equal(transcript.spec.cost.inputTokens, 0);
155
+ assert.equal(transcript.spec.cost.outputTokens, 0);
156
+ assert.equal(transcript.spec.cost.totalTokens, 0);
157
+ });
158
+
159
+ it('handles events with toolUse and toolResult', () => {
160
+ const client = createAgentMuxClient({ gateway: 'http://localhost:8080', enabled: true });
161
+ const events = [
162
+ { role: 'assistant', content: 'Using tool...', toolUse: { name: 'read_file', input: { path: '/tmp/f' } }, timestamp: '2026-01-01T00:00:00Z' },
163
+ { role: 'tool', content: 'file contents', toolResult: { output: 'ok' }, timestamp: '2026-01-01T00:00:01Z' },
164
+ ];
165
+ const transcript = client.reconcileTranscript('sess-tools', events, { namespace: 'default', organizationRef: 'default' });
166
+
167
+ assert.equal(transcript.spec.messages.length, 2);
168
+ assert.deepEqual(transcript.spec.messages[0].toolUse, { name: 'read_file', input: { path: '/tmp/f' } });
169
+ assert.deepEqual(transcript.spec.messages[1].toolResult, { output: 'ok' });
170
+ });
171
+
172
+ it('handles events with non-string content', () => {
173
+ const client = createAgentMuxClient({ gateway: 'http://localhost:8080', enabled: true });
174
+ const events = [
175
+ { role: 'assistant', content: { type: 'structured', data: [1, 2, 3] }, timestamp: '2026-01-01T00:00:00Z' },
176
+ ];
177
+ const transcript = client.reconcileTranscript('sess-obj', events, { namespace: 'default', organizationRef: 'default' });
178
+
179
+ assert.equal(typeof transcript.spec.messages[0].content, 'string');
180
+ assert.ok(transcript.spec.messages[0].content.includes('"type":"structured"'));
181
+ });
182
+
183
+ it('skips null/non-object events gracefully', () => {
184
+ const client = createAgentMuxClient({ gateway: 'http://localhost:8080', enabled: true });
185
+ const events = [
186
+ null,
187
+ 'not-an-object',
188
+ 42,
189
+ { role: 'user', content: 'valid', timestamp: '2026-01-01T00:00:00Z' },
190
+ ];
191
+ const transcript = client.reconcileTranscript('sess-mixed', events, { namespace: 'default', organizationRef: 'default' });
192
+
193
+ assert.equal(transcript.spec.messages.length, 1);
194
+ assert.equal(transcript.spec.messages[0].role, 'user');
195
+ });
196
+
197
+ it('uses default namespace and organizationRef when not specified', () => {
198
+ const client = createAgentMuxClient({ gateway: 'http://localhost:8080', enabled: true });
199
+ const transcript = client.reconcileTranscript('sess-defaults', []);
200
+
201
+ assert.equal(transcript.metadata.namespace, 'default');
202
+ assert.equal(transcript.spec.organizationRef, 'default');
203
+ });
204
+ });
@@ -0,0 +1,317 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createPermissionReviewer } from '../src/agent-permission-review.js';
4
+ import { createResource } from '../src/resource-model.js';
5
+
6
+ // ---------- shared helpers ----------
7
+
8
+ function makeStack(name, specOverrides = {}) {
9
+ return createResource('AgentStack', { name }, {
10
+ organizationRef: 'org-a',
11
+ baseAgent: 'claude-code',
12
+ adapter: 'babysitter',
13
+ runtimeIdentity: { serviceAccountRef: 'sa-agent' },
14
+ ...specOverrides
15
+ });
16
+ }
17
+
18
+ function makeServiceAccount(name, orgRef = 'org-a') {
19
+ return createResource('AgentServiceAccount', { name }, {
20
+ organizationRef: orgRef,
21
+ namespace: 'krate-agents',
22
+ serviceAccountName: name
23
+ });
24
+ }
25
+
26
+ function makeRoleBinding(name, subject, orgRef = 'org-a') {
27
+ return createResource('AgentRoleBinding', { name }, {
28
+ organizationRef: orgRef,
29
+ subject,
30
+ roleRef: 'agent-role',
31
+ scope: 'namespace'
32
+ });
33
+ }
34
+
35
+ function makeSecretGrant(name, subject, purpose, overrides = {}) {
36
+ return createResource('AgentSecretGrant', { name }, {
37
+ organizationRef: 'org-a',
38
+ subject,
39
+ secretRef: 'api-keys',
40
+ purpose,
41
+ ...overrides
42
+ });
43
+ }
44
+
45
+ function makeWorkspacePolicy(name, specOverrides = {}) {
46
+ return createResource('KrateWorkspacePolicy', { name }, {
47
+ organizationRef: 'org-a',
48
+ mode: 'ephemeral',
49
+ retentionPolicy: 'delete-on-completion',
50
+ ...specOverrides
51
+ });
52
+ }
53
+
54
+ function fullyGrantedResources(stackOverrides = {}) {
55
+ return {
56
+ AgentStack: [makeStack('test-stack', stackOverrides)],
57
+ AgentServiceAccount: [makeServiceAccount('sa-agent')],
58
+ AgentRoleBinding: [makeRoleBinding('rb-1', 'sa-agent')],
59
+ AgentSecretGrant: [makeSecretGrant('sg-model', 'sa-agent', 'model-provider')],
60
+ AgentConfigGrant: [],
61
+ AgentMcpServer: [],
62
+ KrateWorkspacePolicy: []
63
+ };
64
+ }
65
+
66
+ const baseInput = {
67
+ repository: 'org-a/my-repo',
68
+ ref: 'refs/heads/main',
69
+ actor: 'user-1',
70
+ agentStack: 'test-stack',
71
+ triggerSource: 'manual',
72
+ taskKind: 'fix'
73
+ };
74
+
75
+ // ==========================================================
76
+ // Group 1 — Cross-org denial
77
+ // ==========================================================
78
+
79
+ describe('agent permission review v2 — cross-org denial', () => {
80
+ it('allows access when agent org matches repository org', () => {
81
+ const reviewer = createPermissionReviewer();
82
+ const resources = fullyGrantedResources({ organizationRef: 'org-a' });
83
+ const result = reviewer.reviewPermissions({
84
+ ...baseInput,
85
+ repository: 'org-a/my-repo',
86
+ resources
87
+ });
88
+ assert.equal(result.decision, 'allowed');
89
+ assert.ok(Array.isArray(result.crossOrgDenials), 'crossOrgDenials should be present');
90
+ assert.equal(result.crossOrgDenials.length, 0, 'no cross-org denials expected');
91
+ });
92
+
93
+ it('denies access and populates crossOrgDenials when agent org differs from repository org', () => {
94
+ const reviewer = createPermissionReviewer();
95
+ // stack is in org-a but repository is in org-b
96
+ const resources = fullyGrantedResources({ organizationRef: 'org-a' });
97
+ const result = reviewer.reviewPermissions({
98
+ ...baseInput,
99
+ repository: 'org-b/their-repo',
100
+ resources
101
+ });
102
+ assert.equal(result.decision, 'denied');
103
+ assert.ok(Array.isArray(result.crossOrgDenials), 'crossOrgDenials must be an array');
104
+ assert.ok(result.crossOrgDenials.length > 0, 'should have at least one cross-org denial entry');
105
+ const denial = result.crossOrgDenials[0];
106
+ assert.ok(denial.agentOrg, 'denial should include agentOrg');
107
+ assert.ok(denial.resourceOrg, 'denial should include resourceOrg');
108
+ });
109
+
110
+ it('cross-org denial error is reflected in the reasons array with severity error', () => {
111
+ const reviewer = createPermissionReviewer();
112
+ const resources = fullyGrantedResources({ organizationRef: 'org-a' });
113
+ const result = reviewer.reviewPermissions({
114
+ ...baseInput,
115
+ repository: 'org-z/external-repo',
116
+ resources
117
+ });
118
+ assert.ok(
119
+ result.reasons.some((r) => r.severity === 'error' && r.message.toLowerCase().includes('org')),
120
+ 'reasons should contain an error mentioning org mismatch'
121
+ );
122
+ });
123
+ });
124
+
125
+ // ==========================================================
126
+ // Group 2 — Approval mode validation
127
+ // ==========================================================
128
+
129
+ describe('agent permission review v2 — approval mode', () => {
130
+ it("approvalMode 'yolo' auto-approves and decision is allowed when permissions are otherwise valid", () => {
131
+ const reviewer = createPermissionReviewer();
132
+ const resources = fullyGrantedResources({ approvalMode: 'yolo' });
133
+ const result = reviewer.reviewPermissions({
134
+ ...baseInput,
135
+ resources
136
+ });
137
+ assert.equal(result.decision, 'allowed');
138
+ assert.equal(result.approvalMode, 'yolo');
139
+ });
140
+
141
+ it("approvalMode 'deny' blocks all requests and returns denied", () => {
142
+ const reviewer = createPermissionReviewer();
143
+ const resources = fullyGrantedResources({ approvalMode: 'deny' });
144
+ const result = reviewer.reviewPermissions({
145
+ ...baseInput,
146
+ resources
147
+ });
148
+ assert.equal(result.decision, 'denied');
149
+ assert.equal(result.approvalMode, 'deny');
150
+ assert.ok(
151
+ result.reasons.some((r) => r.severity === 'error' && r.message.toLowerCase().includes('deny')),
152
+ 'reason should mention deny mode'
153
+ );
154
+ });
155
+
156
+ it("approvalMode 'prompt' keeps requires-approval when grants need approval", () => {
157
+ const reviewer = createPermissionReviewer();
158
+ const mcpServer = createResource('AgentMcpServer', { name: 'mcp-prod' }, {
159
+ organizationRef: 'org-a',
160
+ transport: 'stdio',
161
+ scope: 'workspace',
162
+ secretRef: 'prod-secret'
163
+ });
164
+ const stack = makeStack('test-stack', {
165
+ approvalMode: 'prompt',
166
+ mcpServerRefs: ['mcp-prod']
167
+ });
168
+ const mcpGrant = makeSecretGrant('sg-prod', 'sa-agent', 'mcp-server:mcp-prod', {
169
+ requiredApproval: 'always'
170
+ });
171
+ const resources = {
172
+ AgentStack: [stack],
173
+ AgentServiceAccount: [makeServiceAccount('sa-agent')],
174
+ AgentRoleBinding: [makeRoleBinding('rb-1', 'sa-agent')],
175
+ AgentSecretGrant: [mcpGrant, makeSecretGrant('sg-model', 'sa-agent', 'model-provider')],
176
+ AgentConfigGrant: [],
177
+ AgentMcpServer: [mcpServer],
178
+ KrateWorkspacePolicy: []
179
+ };
180
+ const result = reviewer.reviewPermissions({ ...baseInput, resources });
181
+ assert.equal(result.decision, 'requires-approval');
182
+ assert.equal(result.approvalMode, 'prompt');
183
+ });
184
+
185
+ it('invalid approvalMode value causes denied with validation error', () => {
186
+ const reviewer = createPermissionReviewer();
187
+ const resources = fullyGrantedResources({ approvalMode: 'auto-accept-everything' });
188
+ const result = reviewer.reviewPermissions({
189
+ ...baseInput,
190
+ resources
191
+ });
192
+ assert.equal(result.decision, 'denied');
193
+ assert.ok(
194
+ result.reasons.some((r) => r.severity === 'error' && r.message.toLowerCase().includes('approvalmode')),
195
+ 'reasons should flag invalid approvalMode'
196
+ );
197
+ });
198
+ });
199
+
200
+ // ==========================================================
201
+ // Group 3 — Workspace policy enforcement
202
+ // ==========================================================
203
+
204
+ describe('agent permission review v2 — workspace policy', () => {
205
+ it('allowed when requested tool is in workspace policy allowedTools', () => {
206
+ const reviewer = createPermissionReviewer();
207
+ const policy = makeWorkspacePolicy('wp-1', { allowedTools: ['bash', 'read_file'] });
208
+ const resources = {
209
+ ...fullyGrantedResources(),
210
+ KrateWorkspacePolicy: [policy]
211
+ };
212
+ const result = reviewer.reviewPermissions({
213
+ ...baseInput,
214
+ workspacePolicyRef: 'wp-1',
215
+ toolRefs: ['bash'],
216
+ resources
217
+ });
218
+ assert.equal(result.decision, 'allowed');
219
+ });
220
+
221
+ it('denied when requested tool is in workspace policy deniedTools', () => {
222
+ const reviewer = createPermissionReviewer();
223
+ const policy = makeWorkspacePolicy('wp-1', { deniedTools: ['bash'] });
224
+ const resources = {
225
+ ...fullyGrantedResources(),
226
+ KrateWorkspacePolicy: [policy]
227
+ };
228
+ const result = reviewer.reviewPermissions({
229
+ ...baseInput,
230
+ workspacePolicyRef: 'wp-1',
231
+ toolRefs: ['bash'],
232
+ resources
233
+ });
234
+ assert.equal(result.decision, 'denied');
235
+ assert.ok(
236
+ result.reasons.some((r) => r.severity === 'error' && r.message.toLowerCase().includes('denied')),
237
+ 'reason should mention denied tool'
238
+ );
239
+ });
240
+
241
+ it('denied when maxConcurrentSessions is 0 (no sessions allowed by policy)', () => {
242
+ const reviewer = createPermissionReviewer();
243
+ const policy = makeWorkspacePolicy('wp-strict', { maxConcurrentSessions: 0 });
244
+ const resources = {
245
+ ...fullyGrantedResources(),
246
+ KrateWorkspacePolicy: [policy]
247
+ };
248
+ const result = reviewer.reviewPermissions({
249
+ ...baseInput,
250
+ workspacePolicyRef: 'wp-strict',
251
+ resources
252
+ });
253
+ assert.equal(result.decision, 'denied');
254
+ assert.ok(
255
+ result.reasons.some((r) => r.severity === 'error' && r.message.toLowerCase().includes('maxconcurrentsessions')),
256
+ 'reason should mention maxConcurrentSessions'
257
+ );
258
+ });
259
+ });
260
+
261
+ // ==========================================================
262
+ // Group 4 — Untrusted fork detection
263
+ // ==========================================================
264
+
265
+ describe('agent permission review v2 — untrusted fork detection', () => {
266
+ it('no untrustedForkWarnings when ref is from the canonical repository', () => {
267
+ const reviewer = createPermissionReviewer();
268
+ const resources = fullyGrantedResources();
269
+ const result = reviewer.reviewPermissions({
270
+ ...baseInput,
271
+ repository: 'org-a/my-repo',
272
+ ref: 'refs/heads/main',
273
+ resources
274
+ });
275
+ assert.ok(Array.isArray(result.untrustedForkWarnings), 'untrustedForkWarnings must be present');
276
+ assert.equal(result.untrustedForkWarnings.length, 0);
277
+ });
278
+
279
+ it('untrustedForkWarnings populated when ref indicates a fork (refs/pull/*/head from external fork)', () => {
280
+ const reviewer = createPermissionReviewer();
281
+ // A pull_request ref from a fork is commonly refs/pull/<n>/head where the head sha is from a fork
282
+ const resources = fullyGrantedResources();
283
+ const result = reviewer.reviewPermissions({
284
+ ...baseInput,
285
+ repository: 'org-a/my-repo',
286
+ ref: 'refs/pull/42/head',
287
+ isFork: true,
288
+ resources
289
+ });
290
+ assert.ok(Array.isArray(result.untrustedForkWarnings), 'untrustedForkWarnings must be present');
291
+ assert.ok(result.untrustedForkWarnings.length > 0, 'should have at least one fork warning');
292
+ });
293
+
294
+ it('privileged grants (ServiceAccount, Secret) are blocked for untrusted forks', () => {
295
+ const reviewer = createPermissionReviewer();
296
+ const resources = fullyGrantedResources();
297
+ const result = reviewer.reviewPermissions({
298
+ ...baseInput,
299
+ repository: 'org-a/my-repo',
300
+ ref: 'refs/pull/99/head',
301
+ isFork: true,
302
+ resources
303
+ });
304
+ // Privileged grants that existed should not be in the approved grants list
305
+ // or the decision should downgrade
306
+ const hasPrivilegedGrant = result.grants.some(
307
+ (g) => (g.kind === 'AgentServiceAccount' || g.kind === 'AgentSecretGrant') && g.status === 'granted'
308
+ );
309
+ // Either the grants are stripped or decision is denied/requires-approval
310
+ const isRestricted = !hasPrivilegedGrant || result.decision === 'denied' || result.decision === 'requires-approval';
311
+ assert.ok(isRestricted, 'privileged grants must not be silently approved for untrusted forks');
312
+ assert.ok(
313
+ result.untrustedForkWarnings.some((w) => w.blockedKinds && w.blockedKinds.length > 0),
314
+ 'fork warning should list blocked kinds'
315
+ );
316
+ });
317
+ });