@a5c-ai/krate 5.0.1-staging.f672fe79b

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 (174) hide show
  1. package/Dockerfile +29 -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 +2407 -0
  6. package/dist/krate-lifecycle.json +201 -0
  7. package/dist/krate-runtime-snapshot.json +2955 -0
  8. package/dist/krate-summary.json +687 -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/glossary.md +66 -0
  27. package/docs/agents/implementation-blueprint.md +324 -0
  28. package/docs/agents/implementation-rollout-slices.md +251 -0
  29. package/docs/agents/memory-context-integration-spec.md +194 -0
  30. package/docs/agents/memory-ontology-schema-spec.md +253 -0
  31. package/docs/agents/memory-operations-runbook.md +121 -0
  32. package/docs/agents/mvp-vertical-slice-spec.md +146 -0
  33. package/docs/agents/observability-audit-spec.md +265 -0
  34. package/docs/agents/operator-runbook.md +174 -0
  35. package/docs/agents/org-memory-api-payload-examples.md +333 -0
  36. package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
  37. package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
  38. package/docs/agents/org-memory-ui-implementation-map.md +114 -0
  39. package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
  40. package/docs/agents/org-resource-model-delta-spec.md +111 -0
  41. package/docs/agents/org-route-resource-model-spec.md +183 -0
  42. package/docs/agents/org-scoping-namespace-spec.md +114 -0
  43. package/docs/agents/rbac-secrets-management-spec.md +406 -0
  44. package/docs/agents/repository-page-integration-spec.md +255 -0
  45. package/docs/agents/resource-contract-examples.md +808 -0
  46. package/docs/agents/resource-relationship-map.md +190 -0
  47. package/docs/agents/security-threat-model.md +188 -0
  48. package/docs/agents/shared-memory-company-brain-spec.md +358 -0
  49. package/docs/agents/storage-migration-spec.md +168 -0
  50. package/docs/agents/subagent-orchestration-spec.md +152 -0
  51. package/docs/agents/system-overview.md +88 -0
  52. package/docs/agents/tools-mcp-skills-spec.md +189 -0
  53. package/docs/agents/traceability-matrix.md +79 -0
  54. package/docs/agents/ui-flow-spec.md +211 -0
  55. package/docs/agents/ui-ux-system-spec.md +426 -0
  56. package/docs/agents/workspace-lifecycle-spec.md +166 -0
  57. package/docs/architecture-spec.md +78 -0
  58. package/docs/components/control-plane.md +78 -0
  59. package/docs/components/data-plane.md +69 -0
  60. package/docs/components/hooks-events.md +67 -0
  61. package/docs/components/identity-rbac-policy.md +73 -0
  62. package/docs/components/kubevela-oam.md +70 -0
  63. package/docs/components/operations-publishing.md +81 -0
  64. package/docs/components/runners-ci.md +66 -0
  65. package/docs/components/web-ui.md +94 -0
  66. package/docs/external/README.md +47 -0
  67. package/docs/external/bidirectional-sync-design.md +134 -0
  68. package/docs/external/cicd-interface.md +64 -0
  69. package/docs/external/external-backend-controllers.md +170 -0
  70. package/docs/external/external-backend-crds.md +234 -0
  71. package/docs/external/external-backend-ui-spec.md +151 -0
  72. package/docs/external/external-backend-ux-flows.md +115 -0
  73. package/docs/external/external-object-mapping.md +125 -0
  74. package/docs/external/git-forge-interface.md +68 -0
  75. package/docs/external/github-integration-design.md +151 -0
  76. package/docs/external/issue-tracking-interface.md +66 -0
  77. package/docs/external/provider-capability-manifests.md +204 -0
  78. package/docs/external/provider-catalog.md +139 -0
  79. package/docs/external/provider-rollout-testing.md +78 -0
  80. package/docs/external/research-results.md +48 -0
  81. package/docs/external/security-auth-permissions.md +81 -0
  82. package/docs/external/sync-state-machines.md +108 -0
  83. package/docs/external/unified-external-backend-model.md +107 -0
  84. package/docs/external/user-facing-changes.md +67 -0
  85. package/docs/gaps.md +161 -0
  86. package/docs/install.md +94 -0
  87. package/docs/krate-design.md +334 -0
  88. package/docs/local-minikube.md +55 -0
  89. package/docs/ontology/README.md +32 -0
  90. package/docs/ontology/bounded-contexts.md +29 -0
  91. package/docs/ontology/events-and-hooks.md +32 -0
  92. package/docs/ontology/oam-kubevela.md +32 -0
  93. package/docs/ontology/operations-and-release.md +25 -0
  94. package/docs/ontology/personas-and-actors.md +32 -0
  95. package/docs/ontology/policies-and-invariants.md +33 -0
  96. package/docs/ontology/problem-space.md +30 -0
  97. package/docs/ontology/resource-contracts.md +40 -0
  98. package/docs/ontology/resource-taxonomy.md +42 -0
  99. package/docs/ontology/runners-and-ci.md +29 -0
  100. package/docs/ontology/solution-space.md +24 -0
  101. package/docs/ontology/storage-and-data-boundaries.md +29 -0
  102. package/docs/ontology/validation-matrix.md +24 -0
  103. package/docs/ontology/web-ui-excellent-flows.md +32 -0
  104. package/docs/ontology/workflows.md +39 -0
  105. package/docs/ontology/world.md +35 -0
  106. package/docs/product-requirements.md +62 -0
  107. package/docs/roadmap-mvp.md +87 -0
  108. package/docs/system-requirements.md +90 -0
  109. package/docs/tests/README.md +53 -0
  110. package/docs/tests/agent-qa-plan.md +63 -0
  111. package/docs/tests/browser-ui-tests.md +62 -0
  112. package/docs/tests/ci-quality-gates.md +48 -0
  113. package/docs/tests/coverage-model.md +64 -0
  114. package/docs/tests/e2e-scenario-tests.md +53 -0
  115. package/docs/tests/fixtures-test-data.md +63 -0
  116. package/docs/tests/observability-reliability-tests.md +54 -0
  117. package/docs/tests/product-test-matrix.md +145 -0
  118. package/docs/tests/qa-adoption-roadmap.md +130 -0
  119. package/docs/tests/qa-automation-plan.md +101 -0
  120. package/docs/tests/security-compliance-tests.md +57 -0
  121. package/docs/tests/test-framework-tools.md +88 -0
  122. package/docs/tests/test-suite-layout.md +121 -0
  123. package/docs/tests/unit-integration-tests.md +48 -0
  124. package/docs/todo-kyverno +714 -0
  125. package/docs/user-stories.md +78 -0
  126. package/examples/minikube-demo.yaml +190 -0
  127. package/examples/oam-application.yaml +23 -0
  128. package/examples/policy-kyverno-pr-title.yaml +18 -0
  129. package/package.json +63 -0
  130. package/scripts/build.mjs +29 -0
  131. package/scripts/setup-minikube.mjs +65 -0
  132. package/scripts/smoke.mjs +37 -0
  133. package/scripts/validate-doc-coverage.mjs +152 -0
  134. package/scripts/validate-package.mjs +93 -0
  135. package/scripts/validate-ui.mjs +207 -0
  136. package/src/agent-approval-controller.js +123 -0
  137. package/src/agent-context-bundles.js +242 -0
  138. package/src/agent-dispatch-controller.js +86 -0
  139. package/src/agent-mux-client.js +280 -0
  140. package/src/agent-permission-review.js +162 -0
  141. package/src/agent-stack-controller.js +296 -0
  142. package/src/agent-trigger-controller.js +108 -0
  143. package/src/api-controller.js +206 -0
  144. package/src/argocd-gitops.js +43 -0
  145. package/src/auth.js +265 -0
  146. package/src/component-catalog.js +41 -0
  147. package/src/control-plane.js +136 -0
  148. package/src/controller-client.js +38 -0
  149. package/src/controller-ui.js +538 -0
  150. package/src/data-plane.js +178 -0
  151. package/src/gitea-backend.js +95 -0
  152. package/src/handoff.js +98 -0
  153. package/src/hooks-events.js +63 -0
  154. package/src/http-server.js +151 -0
  155. package/src/identity-policy.js +86 -0
  156. package/src/index.js +30 -0
  157. package/src/kubernetes-controller.js +812 -0
  158. package/src/kubernetes-resource-gateway.js +48 -0
  159. package/src/operations.js +112 -0
  160. package/src/resource-model.js +203 -0
  161. package/src/runners-ci.js +48 -0
  162. package/src/runtime.js +196 -0
  163. package/src/web-ui.js +40 -0
  164. package/tests/agent-approval-controller.test.js +173 -0
  165. package/tests/agent-context-bundles.test.js +278 -0
  166. package/tests/agent-dispatch-controller.test.js +176 -0
  167. package/tests/agent-mux-client.test.js +204 -0
  168. package/tests/agent-permission-review.test.js +209 -0
  169. package/tests/agent-resources.test.js +212 -0
  170. package/tests/agent-stack-controller.test.js +221 -0
  171. package/tests/agent-trigger-controller.test.js +211 -0
  172. package/tests/deployment.test.js +395 -0
  173. package/tests/e2e/lifecycle.test.js +117 -0
  174. package/tests/krate.test.js +727 -0
package/src/web-ui.js ADDED
@@ -0,0 +1,40 @@
1
+ import { createResource } from './resource-model.js';
2
+ import { toResourceYaml } from './identity-policy.js';
3
+
4
+ export function createPullRequestReviewModel({ pullRequest, changedFiles = [], pipelineRuns = [] }) {
5
+ return { layout: 'three-pane-review', panes: ['file-tree', 'diff-and-comments', 'conversation-ci'], keyboardShortcuts: ['j/k file navigation', 'n/p comment navigation', 'a add suggestion', 'm merge'], pullRequest, changedFiles, pipelineRuns, yaml: toResourceYaml(pullRequest) };
6
+ }
7
+
8
+ export function createFailingRunModel({ pipeline, jobs }) {
9
+ const failedJobs = jobs.filter((job) => job.status.phase === 'Failed');
10
+ return { layout: 'live-run-debugger', stream: 'sse', pipeline, failedJobs, actions: ['copy failure', 'find similar runs', 'rerun from step'], similarRunSelector: failedJobs.map((job) => job.metadata.labels).filter(Boolean) };
11
+ }
12
+
13
+ export function createRunnerPoolEditor(pool) {
14
+ return { layout: 'split-form-yaml', fields: ['image', 'resources', 'nodeSelector', 'warmReplicas', 'maxReplicas', 'trustTier', 'cache'], resource: pool, yaml: toResourceYaml(pool), saveModes: ['apply', 'copy kubectl', 'open platform-config PR'] };
15
+ }
16
+
17
+ export function createWebhookInspector({ subscription, deliveries }) {
18
+ return { layout: 'webhook-inspector', subscription, deliveries, columns: ['phase', 'latency', 'attempts', 'response', 'signature'], actions: ['send test delivery', 'inspect headers/body/response', 'replay'] };
19
+ }
20
+
21
+ export function createPolicyRolloutModel(policy) {
22
+ return { layout: 'policy-authoring', modes: ['template', 'CEL/raw'], rollout: ['preview', 'audit', 'enforce'], policy, yaml: toResourceYaml(policy) };
23
+ }
24
+
25
+ export function createTriageView({ name, namespace = 'krate-org-default', organizationRef = 'default', selector }) {
26
+ return createResource('View', { name, namespace, labels: { purpose: 'triage' } }, { organizationRef, selector, columns: ['kind', 'repository', 'priority', 'assignee', 'status'], shareable: true }, { saved: true });
27
+ }
28
+
29
+ export function createDashboard({ repositories, pullRequests, pipelines, runnerPools, webhookDeliveries }) {
30
+ return {
31
+ product: 'Krate',
32
+ principles: ['Kubernetes is the backend', 'CRDs are contracts', 'GitOps transparency'],
33
+ repositories,
34
+ pullRequests,
35
+ pipelines,
36
+ runnerPools,
37
+ webhookDeliveries,
38
+ excellentFlows: ['Open and review a PR', 'Debug a failing run', 'Configure a runner pool', 'Add a webhook and verify it works', 'Write a PR policy with audit-to-enforce rollout', 'Cross-repo triage with saved filters']
39
+ };
40
+ }
@@ -0,0 +1,173 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { createAgentApprovalController, createResource } from '../src/index.js';
4
+
5
+ function makeApproval(name, dispatchRun, action, phase = 'Pending', extra = {}) {
6
+ const approval = createResource('AgentApproval', { name, namespace: 'krate-org-default' }, {
7
+ organizationRef: 'default',
8
+ dispatchRun,
9
+ action,
10
+ requestedBy: 'agent-stack-1',
11
+ ...extra
12
+ });
13
+ approval.status = { phase, createdAt: new Date().toISOString(), ...extra.status };
14
+ return approval;
15
+ }
16
+
17
+ test('Create approval request returns resource with phase=Pending', () => {
18
+ const controller = createAgentApprovalController();
19
+ const result = controller.createApprovalRequest({
20
+ dispatchRun: 'run-1',
21
+ action: 'tool-use',
22
+ requestedBy: 'agent-stack-1',
23
+ context: 'Needs filesystem write access',
24
+ namespace: 'krate-org-default',
25
+ organizationRef: 'default',
26
+ resources: {}
27
+ });
28
+
29
+ assert.equal(result.error, false, 'Should succeed');
30
+ assert.equal(result.duplicate, false, 'Should not be a duplicate');
31
+ assert.ok(result.approval, 'Should return an approval resource');
32
+ assert.equal(result.approval.kind, 'AgentApproval');
33
+ assert.equal(result.approval.status.phase, 'Pending');
34
+ assert.equal(result.approval.spec.action, 'tool-use');
35
+ assert.equal(result.approval.spec.dispatchRun, 'run-1');
36
+ assert.equal(result.approval.spec.requestedBy, 'agent-stack-1');
37
+ assert.ok(result.approval.status.createdAt, 'Should have createdAt timestamp');
38
+ });
39
+
40
+ test('Record approve sets phase=Approved and decidedBy', () => {
41
+ const controller = createAgentApprovalController();
42
+ const existing = makeApproval('approval-1', 'run-1', 'tool-use', 'Pending');
43
+ const result = controller.recordDecision({
44
+ approvalName: 'approval-1',
45
+ decision: 'approve',
46
+ decidedBy: 'owner',
47
+ reason: 'Looks safe',
48
+ resources: { AgentApproval: [existing] }
49
+ });
50
+
51
+ assert.equal(result.error, false, 'Should succeed');
52
+ assert.equal(result.approval.status.phase, 'Approved');
53
+ assert.equal(result.approval.status.decidedBy, 'owner');
54
+ assert.equal(result.approval.status.reason, 'Looks safe');
55
+ assert.ok(result.approval.status.decidedAt, 'Should have decidedAt timestamp');
56
+ });
57
+
58
+ test('Record deny sets phase=Denied', () => {
59
+ const controller = createAgentApprovalController();
60
+ const existing = makeApproval('approval-2', 'run-1', 'secret-access', 'Pending');
61
+ const result = controller.recordDecision({
62
+ approvalName: 'approval-2',
63
+ decision: 'deny',
64
+ decidedBy: 'admin',
65
+ reason: 'Sensitive secrets not allowed',
66
+ resources: { AgentApproval: [existing] }
67
+ });
68
+
69
+ assert.equal(result.error, false, 'Should succeed');
70
+ assert.equal(result.approval.status.phase, 'Denied');
71
+ assert.equal(result.approval.status.decidedBy, 'admin');
72
+ assert.equal(result.approval.status.reason, 'Sensitive secrets not allowed');
73
+ });
74
+
75
+ test('isActionApproved returns true after approval', () => {
76
+ const controller = createAgentApprovalController();
77
+ const approved = makeApproval('approval-3', 'run-2', 'write-back', 'Approved', { status: { decidedBy: 'owner', decidedAt: new Date().toISOString() } });
78
+ const result = controller.isActionApproved({
79
+ dispatchRun: 'run-2',
80
+ action: 'write-back',
81
+ resources: { AgentApproval: [approved] }
82
+ });
83
+
84
+ assert.equal(result.approved, true, 'Should be approved');
85
+ assert.ok(result.approval, 'Should return the approval resource');
86
+ });
87
+
88
+ test('isActionApproved returns false when still pending', () => {
89
+ const controller = createAgentApprovalController();
90
+ const pending = makeApproval('approval-4', 'run-3', 'release', 'Pending');
91
+ const result = controller.isActionApproved({
92
+ dispatchRun: 'run-3',
93
+ action: 'release',
94
+ resources: { AgentApproval: [pending] }
95
+ });
96
+
97
+ assert.equal(result.approved, false, 'Should not be approved');
98
+ assert.equal(result.reason, 'Approval is still pending');
99
+ });
100
+
101
+ test('Duplicate pending request returns existing approval', () => {
102
+ const controller = createAgentApprovalController();
103
+ const existing = makeApproval('approval-5', 'run-4', 'tool-use', 'Pending');
104
+ const result = controller.createApprovalRequest({
105
+ dispatchRun: 'run-4',
106
+ action: 'tool-use',
107
+ requestedBy: 'agent-stack-1',
108
+ resources: { AgentApproval: [existing] }
109
+ });
110
+
111
+ assert.equal(result.error, false, 'Should succeed');
112
+ assert.equal(result.duplicate, true, 'Should be flagged as duplicate');
113
+ assert.equal(result.approval.metadata.name, 'approval-5', 'Should return the existing approval');
114
+ });
115
+
116
+ test('Invalid action type returns error', () => {
117
+ const controller = createAgentApprovalController();
118
+ const result = controller.createApprovalRequest({
119
+ dispatchRun: 'run-5',
120
+ action: 'invalid-action',
121
+ requestedBy: 'agent-stack-1',
122
+ resources: {}
123
+ });
124
+
125
+ assert.equal(result.error, true, 'Should fail');
126
+ assert.equal(result.reason, 'invalid-action');
127
+ assert.ok(result.message.includes('invalid-action'), 'Message should mention the invalid action');
128
+ });
129
+
130
+ test('Already decided approval returns error on re-decide', () => {
131
+ const controller = createAgentApprovalController();
132
+ const decided = makeApproval('approval-6', 'run-6', 'escalation', 'Approved', { status: { decidedBy: 'admin', decidedAt: new Date().toISOString() } });
133
+ const result = controller.recordDecision({
134
+ approvalName: 'approval-6',
135
+ decision: 'deny',
136
+ decidedBy: 'other-admin',
137
+ resources: { AgentApproval: [decided] }
138
+ });
139
+
140
+ assert.equal(result.error, true, 'Should fail');
141
+ assert.equal(result.reason, 'already-decided');
142
+ assert.ok(result.message.includes('already been decided'), 'Message should indicate already decided');
143
+ });
144
+
145
+ test('listPendingApprovals filters by org and pending status', () => {
146
+ const controller = createAgentApprovalController();
147
+ const pending1 = makeApproval('p1', 'run-7', 'tool-use', 'Pending');
148
+ const pending2 = makeApproval('p2', 'run-8', 'release', 'Pending');
149
+ const approved = makeApproval('p3', 'run-9', 'write-back', 'Approved');
150
+
151
+ const result = controller.listPendingApprovals({
152
+ organizationRef: 'default',
153
+ resources: { AgentApproval: [pending1, pending2, approved] }
154
+ });
155
+
156
+ assert.equal(result.length, 2, 'Should return only pending approvals');
157
+ assert.ok(result.every(a => a.status.phase === 'Pending'), 'All should be Pending');
158
+ });
159
+
160
+ test('listApprovalsForRun filters by dispatch run', () => {
161
+ const controller = createAgentApprovalController();
162
+ const a1 = makeApproval('r1', 'run-10', 'tool-use', 'Pending');
163
+ const a2 = makeApproval('r2', 'run-10', 'secret-access', 'Approved');
164
+ const a3 = makeApproval('r3', 'run-11', 'release', 'Pending');
165
+
166
+ const result = controller.listApprovalsForRun({
167
+ dispatchRun: 'run-10',
168
+ resources: { AgentApproval: [a1, a2, a3] }
169
+ });
170
+
171
+ assert.equal(result.length, 2, 'Should return approvals for run-10 only');
172
+ assert.ok(result.every(a => a.spec.dispatchRun === 'run-10'), 'All should belong to run-10');
173
+ });
@@ -0,0 +1,278 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { assembleContextBundle, createRedactionManifest } from '../src/agent-context-bundles.js';
4
+ import { createResource } from '../src/resource-model.js';
5
+
6
+ function makeStack(name, promptOverrides = {}, extraSpec = {}) {
7
+ return createResource('AgentStack', { name }, {
8
+ organizationRef: 'default',
9
+ baseAgent: 'claude-code',
10
+ adapter: 'babysitter',
11
+ runtimeIdentity: { serviceAccountRef: 'sa-agent' },
12
+ prompt: {
13
+ system: promptOverrides.system || '',
14
+ developer: promptOverrides.developer || '',
15
+ task: promptOverrides.task || '',
16
+ },
17
+ ...extraSpec
18
+ });
19
+ }
20
+
21
+ function makeSkill(name, promptFragment) {
22
+ return createResource('AgentSkill', { name }, {
23
+ organizationRef: 'default',
24
+ format: 'markdown',
25
+ sourceRef: `skills/${name}`,
26
+ promptFragment
27
+ });
28
+ }
29
+
30
+ function makeContextLabel(name, promptFragment) {
31
+ return createResource('AgentContextLabel', { name }, {
32
+ organizationRef: 'default',
33
+ promptFragment,
34
+ allowedSources: ['manual']
35
+ });
36
+ }
37
+
38
+ describe('agent context bundles', () => {
39
+ it('basic assembly with system/developer/task prompt', () => {
40
+ const stack = makeStack('test-stack', {
41
+ system: 'You are a security reviewer.',
42
+ developer: 'Focus on OWASP top 10.',
43
+ task: 'Review the authentication module.'
44
+ });
45
+ const bundle = assembleContextBundle({ stack, repository: 'my-repo', ref: 'refs/heads/main' });
46
+
47
+ assert.equal(bundle.kind, 'AgentContextBundle');
48
+ assert.equal(bundle.apiVersion, 'krate.a5c.ai/v1alpha1');
49
+ assert.ok(bundle.metadata.name.startsWith('bundle-'));
50
+ assert.equal(bundle.spec.organizationRef, 'default');
51
+ assert.equal(bundle.spec.dispatchRun, '');
52
+ assert.ok(typeof bundle.spec.digest === 'string' && bundle.spec.digest.length === 64);
53
+
54
+ // Prompt layers present
55
+ const layers = bundle.spec.promptLayers;
56
+ assert.equal(layers[0].role, 'system');
57
+ assert.equal(layers[1].role, 'developer');
58
+ assert.equal(layers[2].role, 'task');
59
+ assert.ok(layers[0].sizeBytes > 0);
60
+ assert.ok(layers[1].sizeBytes > 0);
61
+ assert.ok(layers[2].sizeBytes > 0);
62
+
63
+ // _content present (in-memory, not persisted)
64
+ assert.equal(bundle._content.system, 'You are a security reviewer.');
65
+ assert.equal(bundle._content.developer, 'Focus on OWASP top 10.');
66
+ assert.equal(bundle._content.task, 'Review the authentication module.');
67
+ });
68
+
69
+ it('redaction catches API_KEY=xxx patterns', () => {
70
+ const stack = makeStack('test-stack', {
71
+ system: 'config: API_KEY=sk_live_abc123xyz',
72
+ developer: '',
73
+ task: ''
74
+ });
75
+ const bundle = assembleContextBundle({ stack });
76
+ assert.ok(bundle._content.system.includes('[REDACTED:secret-key]'));
77
+ assert.ok(!bundle._content.system.includes('sk_live_abc123xyz'));
78
+ assert.ok(bundle.spec.redactions.total > 0);
79
+ assert.ok(bundle.spec.redactions.byKind['secret-key'] > 0);
80
+ });
81
+
82
+ it('redaction catches provider tokens (sk-xxx)', () => {
83
+ const stack = makeStack('test-stack', {
84
+ system: '',
85
+ developer: 'Use key: sk-abcdefghijklmnopqrstuvwxyz1234567890',
86
+ task: ''
87
+ });
88
+ const bundle = assembleContextBundle({ stack });
89
+ assert.ok(bundle._content.developer.includes('[REDACTED:provider-token]'));
90
+ assert.ok(!bundle._content.developer.includes('sk-abcdefghijklmnopqrstuvwxyz1234567890'));
91
+ assert.ok(bundle.spec.redactions.byKind['provider-token'] > 0);
92
+ });
93
+
94
+ it('redaction catches Bearer tokens', () => {
95
+ const stack = makeStack('test-stack', {
96
+ system: 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.test',
97
+ developer: '',
98
+ task: ''
99
+ });
100
+ const bundle = assembleContextBundle({ stack });
101
+ assert.ok(bundle._content.system.includes('[REDACTED:bearer-token]'));
102
+ assert.ok(!bundle._content.system.includes('eyJhbGciOiJIUzI1NiJ9'));
103
+ assert.ok(bundle.spec.redactions.byKind['bearer-token'] > 0);
104
+ });
105
+
106
+ it('redaction catches private keys', () => {
107
+ const privateKey = '-----BEGIN RSA PRIVATE KEY-----\nMIIBogIBAAJBALRpYJk...\n-----END RSA PRIVATE KEY-----';
108
+ const stack = makeStack('test-stack', {
109
+ system: `Here is a key: ${privateKey}`,
110
+ developer: '',
111
+ task: ''
112
+ });
113
+ const bundle = assembleContextBundle({ stack });
114
+ assert.ok(bundle._content.system.includes('[REDACTED:private-key]'));
115
+ assert.ok(!bundle._content.system.includes('MIIBogIBAAJBALRpYJk'));
116
+ assert.ok(bundle.spec.redactions.byKind['private-key'] > 0);
117
+ });
118
+
119
+ it('size truncation at 750 KiB', () => {
120
+ // Create a bundle that exceeds 750 KiB via many source attachments.
121
+ // Each source is truncated to 64 KiB per-layer, so we need enough sources
122
+ // to exceed 750 KiB total. 750/64 ~= 12, so 13 sources at 64 KiB each should exceed.
123
+ // Use content with spaces to avoid base64-credential pattern matching.
124
+ const chunk = 'the quick brown fox jumps.\n';
125
+ const bigContent = chunk.repeat(Math.ceil((64 * 1024) / chunk.length)).slice(0, 64 * 1024);
126
+ const stack = makeStack('test-stack', {
127
+ system: 'small system prompt',
128
+ developer: '',
129
+ task: ''
130
+ });
131
+ const sourceRefs = [];
132
+ for (let i = 0; i < 15; i++) {
133
+ sourceRefs.push({ kind: 'file', ref: `big-file-${i}.txt`, content: bigContent });
134
+ }
135
+ const bundle = assembleContextBundle({ stack, sourceRefs });
136
+
137
+ assert.equal(bundle.spec.limits.truncated, true);
138
+ assert.equal(bundle.spec.limits.maxBytes, 750 * 1024);
139
+
140
+ // Verify total content is at or near the limit
141
+ const allContentLen = bundle._content.system.length +
142
+ bundle._content.developer.length +
143
+ bundle._content.task.length +
144
+ bundle._content.sources.reduce((s, src) => s + src.content.length, 0);
145
+ assert.ok(allContentLen <= 750 * 1024 + 100, `total size ${allContentLen} should be near 750KiB limit`);
146
+ });
147
+
148
+ it('digest is deterministic (same input produces same digest)', () => {
149
+ const stack = makeStack('test-stack', {
150
+ system: 'Deterministic test system prompt.',
151
+ developer: 'Deterministic test developer prompt.',
152
+ task: 'Deterministic test task prompt.'
153
+ });
154
+ const sourceRefs = [{ kind: 'pr-body', ref: '#42', content: 'PR description here' }];
155
+
156
+ const bundle1 = assembleContextBundle({ stack, sourceRefs });
157
+ const bundle2 = assembleContextBundle({ stack, sourceRefs });
158
+
159
+ assert.equal(bundle1.spec.digest, bundle2.spec.digest);
160
+ assert.ok(typeof bundle1.spec.digest === 'string' && bundle1.spec.digest.length === 64);
161
+ });
162
+
163
+ it('redaction manifest has counts only (no leaked values)', () => {
164
+ const stack = makeStack('test-stack', {
165
+ system: 'SECRET_KEY = "my-super-secret-value123" and PASSWORD=hunter2',
166
+ developer: '',
167
+ task: ''
168
+ });
169
+ const bundle = assembleContextBundle({ stack });
170
+ const manifest = bundle.spec.redactions;
171
+
172
+ assert.ok(typeof manifest.total === 'number' && manifest.total >= 2);
173
+ assert.ok(typeof manifest.byKind === 'object');
174
+ // Ensure the manifest only has counts, no string values that could leak secrets
175
+ for (const [kind, count] of Object.entries(manifest.byKind)) {
176
+ assert.ok(typeof kind === 'string');
177
+ assert.ok(typeof count === 'number');
178
+ }
179
+ // Verify no secret values in the serialized manifest
180
+ const serialized = JSON.stringify(manifest);
181
+ assert.ok(!serialized.includes('my-super-secret-value123'));
182
+ assert.ok(!serialized.includes('hunter2'));
183
+ });
184
+
185
+ it('empty inputs handled gracefully', () => {
186
+ const stack = makeStack('test-stack', { system: '', developer: '', task: '' });
187
+ const bundle = assembleContextBundle({ stack });
188
+
189
+ assert.equal(bundle.kind, 'AgentContextBundle');
190
+ assert.ok(typeof bundle.spec.digest === 'string' && bundle.spec.digest.length === 64);
191
+ assert.equal(bundle.spec.promptLayers.length, 3);
192
+ assert.equal(bundle.spec.promptLayers[0].sizeBytes, 0);
193
+ assert.equal(bundle.spec.promptLayers[1].sizeBytes, 0);
194
+ assert.equal(bundle.spec.promptLayers[2].sizeBytes, 0);
195
+ assert.equal(bundle.spec.redactions.total, 0);
196
+ assert.equal(bundle.spec.limits.truncated, false);
197
+ assert.deepEqual(bundle.spec.sources, []);
198
+ assert.equal(bundle._content.system, '');
199
+ assert.equal(bundle._content.developer, '');
200
+ assert.equal(bundle._content.task, '');
201
+ });
202
+
203
+ it('assembles skill and label fragments from resources', () => {
204
+ const stack = makeStack('test-stack', {
205
+ system: 'System prompt',
206
+ developer: '',
207
+ task: ''
208
+ }, { skillRefs: ['skill-review', 'skill-test'] });
209
+ const resources = {
210
+ AgentSkill: [
211
+ makeSkill('skill-review', 'Review all changed files for security issues.'),
212
+ makeSkill('skill-test', 'Run unit tests before proceeding.'),
213
+ makeSkill('skill-unused', 'This should not appear.')
214
+ ],
215
+ AgentContextLabel: [
216
+ makeContextLabel('label-prod', 'This is a production environment.')
217
+ ]
218
+ };
219
+ const bundle = assembleContextBundle({
220
+ stack,
221
+ contextLabels: ['label-prod'],
222
+ resources
223
+ });
224
+
225
+ // Should have system + developer + task + 2 skills + 1 label = 6 layers
226
+ assert.equal(bundle.spec.promptLayers.length, 6);
227
+ assert.equal(bundle.spec.promptLayers[3].role, 'skill:skill-review');
228
+ assert.equal(bundle.spec.promptLayers[4].role, 'skill:skill-test');
229
+ assert.equal(bundle.spec.promptLayers[5].role, 'label:label-prod');
230
+ assert.ok(bundle._content.skillFragments.length === 2);
231
+ assert.ok(bundle._content.labelFragments.length === 1);
232
+ });
233
+
234
+ it('createRedactionManifest returns correct structure', () => {
235
+ const manifest = createRedactionManifest({ 'secret-key': 3, 'provider-token': 1, 'bearer-token': 2 });
236
+ assert.equal(manifest.total, 6);
237
+ assert.deepEqual(manifest.byKind, { 'secret-key': 3, 'provider-token': 1, 'bearer-token': 2 });
238
+ });
239
+
240
+ it('createRedactionManifest with empty counts', () => {
241
+ const manifest = createRedactionManifest({});
242
+ assert.equal(manifest.total, 0);
243
+ assert.deepEqual(manifest.byKind, {});
244
+ });
245
+
246
+ it('source refs are limited to MAX_ATTACHMENTS (32)', () => {
247
+ const stack = makeStack('test-stack', { system: '', developer: '', task: '' });
248
+ const sourceRefs = [];
249
+ for (let i = 0; i < 40; i++) {
250
+ sourceRefs.push({ kind: 'file', ref: `file-${i}.txt`, content: `content ${i}` });
251
+ }
252
+ const bundle = assembleContextBundle({ stack, sourceRefs });
253
+ assert.ok(bundle.spec.sources.length <= 32, 'should cap at 32 sources');
254
+ assert.ok(bundle._content.sources.length <= 32);
255
+ });
256
+
257
+ it('prompt layers are truncated to 64 KiB each', () => {
258
+ const oversized = 'a'.repeat(80 * 1024); // 80 KiB
259
+ const stack = makeStack('test-stack', {
260
+ system: oversized,
261
+ developer: '',
262
+ task: ''
263
+ });
264
+ const bundle = assembleContextBundle({ stack });
265
+ assert.ok(bundle._content.system.length <= 64 * 1024, 'system prompt should be truncated to 64 KiB');
266
+ });
267
+
268
+ it('redaction in source content', () => {
269
+ const stack = makeStack('test-stack', { system: '', developer: '', task: '' });
270
+ const sourceRefs = [
271
+ { kind: 'pipeline-log', ref: 'run-123', content: 'Error: API_KEY=leaked_value_here failed auth' }
272
+ ];
273
+ const bundle = assembleContextBundle({ stack, sourceRefs });
274
+ assert.ok(bundle._content.sources[0].content.includes('[REDACTED:secret-key]'));
275
+ assert.ok(!bundle._content.sources[0].content.includes('leaked_value_here'));
276
+ assert.ok(bundle.spec.redactions.total > 0);
277
+ });
278
+ });
@@ -0,0 +1,176 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { createAgentDispatchController, createAgentMuxClient, createResource } from '../src/index.js';
4
+
5
+ function makeStack(name, spec = {}) {
6
+ return createResource('AgentStack', { name, namespace: 'krate-org-default' }, {
7
+ organizationRef: 'default',
8
+ baseAgent: 'claude-code',
9
+ adapter: 'anthropic',
10
+ runtimeIdentity: { serviceAccountRef: 'sa-default' },
11
+ ...spec
12
+ });
13
+ }
14
+
15
+ function makeServiceAccount(name) {
16
+ return createResource('AgentServiceAccount', { name, namespace: 'krate-org-default' }, {
17
+ organizationRef: 'default',
18
+ namespace: 'krate-org-default',
19
+ serviceAccountName: name
20
+ });
21
+ }
22
+
23
+ function makeRoleBinding(name, subject) {
24
+ return createResource('AgentRoleBinding', { name, namespace: 'krate-org-default' }, {
25
+ organizationRef: 'default',
26
+ subject,
27
+ roleRef: 'agent-developer',
28
+ scope: 'namespace'
29
+ });
30
+ }
31
+
32
+ function makeSecretGrant(name, subject, purpose) {
33
+ return createResource('AgentSecretGrant', { name, namespace: 'krate-org-default' }, {
34
+ organizationRef: 'default',
35
+ subject,
36
+ secretRef: 'secret-' + purpose,
37
+ purpose
38
+ });
39
+ }
40
+
41
+ function buildValidResources(stackName) {
42
+ return {
43
+ AgentStack: [makeStack(stackName)],
44
+ AgentServiceAccount: [makeServiceAccount('sa-default')],
45
+ AgentRoleBinding: [makeRoleBinding('rb-1', 'sa-default')],
46
+ AgentSecretGrant: [makeSecretGrant('sg-model', 'sa-default', 'model-provider')]
47
+ };
48
+ }
49
+
50
+ test('Successful dispatch with Agent Mux available', async () => {
51
+ // Use a mock mux client that returns a session without making real HTTP calls
52
+ const muxClient = {
53
+ role: 'agent-mux-client',
54
+ isAvailable() { return true; },
55
+ async launchSession() { return { runId: `amux-${Date.now()}`, sessionId: `session-${Date.now()}` }; },
56
+ };
57
+ const resources = buildValidResources('dispatch-stack');
58
+ const controller = createAgentDispatchController({ agentMuxClient: muxClient });
59
+
60
+ const result = await controller.createManualDispatch({
61
+ repository: 'test-repo',
62
+ ref: 'main',
63
+ agentStack: 'dispatch-stack',
64
+ actor: 'test-user',
65
+ namespace: 'krate-org-default',
66
+ organizationRef: 'default',
67
+ resources
68
+ });
69
+
70
+ assert.equal(result.error, false, 'Dispatch should succeed');
71
+ assert.ok(result.run, 'Result should include run resource');
72
+ assert.ok(result.attempt, 'Result should include attempt resource');
73
+ assert.ok(result.contextBundle, 'Result should include contextBundle resource');
74
+ assert.ok(result.permissionSnapshot, 'Result should include permissionSnapshot');
75
+ assert.equal(result.run.kind, 'AgentDispatchRun');
76
+ assert.equal(result.attempt.kind, 'AgentDispatchAttempt');
77
+ assert.equal(result.run.status.phase, 'Running', 'Run phase should be Running when mux is available');
78
+ assert.ok(result.attempt.status.agentMuxRunId, 'Attempt should have agentMuxRunId');
79
+ assert.ok(result.attempt.status.agentMuxSessionId, 'Attempt should have agentMuxSessionId');
80
+ assert.ok(result.attempt.status.startedAt, 'Attempt should have startedAt timestamp');
81
+ });
82
+
83
+ test('Dispatch with Agent Mux unavailable', async () => {
84
+ const muxClient = createAgentMuxClient({ gateway: '', enabled: false });
85
+ const resources = buildValidResources('dispatch-stack');
86
+ const controller = createAgentDispatchController({ agentMuxClient: muxClient });
87
+
88
+ const result = await controller.createManualDispatch({
89
+ repository: 'test-repo',
90
+ ref: 'main',
91
+ agentStack: 'dispatch-stack',
92
+ actor: 'test-user',
93
+ namespace: 'krate-org-default',
94
+ organizationRef: 'default',
95
+ resources
96
+ });
97
+
98
+ assert.equal(result.error, false, 'Dispatch should still succeed (queued)');
99
+ assert.equal(result.run.status.phase, 'Queued', 'Run phase should be Queued when mux is unavailable');
100
+ assert.ok(result.run.status.conditions, 'Run should have conditions');
101
+ const muxCondition = result.run.status.conditions.find(c => c.type === 'AgentMuxBound');
102
+ assert.ok(muxCondition, 'Should have AgentMuxBound condition');
103
+ assert.equal(muxCondition.status, 'False', 'AgentMuxBound should be False');
104
+ assert.equal(muxCondition.reason, 'Unavailable');
105
+ });
106
+
107
+ test('Dispatch denied by permission review', async () => {
108
+ const resources = {
109
+ AgentStack: [makeStack('denied-stack', { runtimeIdentity: { serviceAccountRef: 'sa-missing' } })],
110
+ // No service account, no role binding, no secret grant — permission review will deny
111
+ };
112
+ const controller = createAgentDispatchController();
113
+
114
+ const result = await controller.createManualDispatch({
115
+ repository: 'test-repo',
116
+ ref: 'main',
117
+ agentStack: 'denied-stack',
118
+ actor: 'test-user',
119
+ namespace: 'krate-org-default',
120
+ organizationRef: 'default',
121
+ resources
122
+ });
123
+
124
+ assert.equal(result.error, true, 'Dispatch should be denied');
125
+ assert.equal(result.reason, 'permission-denied', 'Reason should be permission-denied');
126
+ assert.ok(result.review, 'Result should include the review details');
127
+ assert.equal(result.review.decision, 'denied');
128
+ });
129
+
130
+ test('Stack not found', async () => {
131
+ const resources = buildValidResources('existing-stack');
132
+ const controller = createAgentDispatchController();
133
+
134
+ const result = await controller.createManualDispatch({
135
+ repository: 'test-repo',
136
+ ref: 'main',
137
+ agentStack: 'nonexistent-stack',
138
+ actor: 'test-user',
139
+ namespace: 'krate-org-default',
140
+ organizationRef: 'default',
141
+ resources
142
+ });
143
+
144
+ assert.equal(result.error, true, 'Dispatch should fail');
145
+ assert.equal(result.reason, 'stack-not-found', 'Reason should be stack-not-found');
146
+ assert.ok(result.message.includes('nonexistent-stack'), 'Message should name the missing stack');
147
+ });
148
+
149
+ test('Context bundle referenced correctly', async () => {
150
+ const muxClient = createAgentMuxClient({ gateway: '', enabled: false });
151
+ const resources = buildValidResources('ref-stack');
152
+ const controller = createAgentDispatchController({ agentMuxClient: muxClient });
153
+
154
+ const result = await controller.createManualDispatch({
155
+ repository: 'test-repo',
156
+ ref: 'main',
157
+ agentStack: 'ref-stack',
158
+ actor: 'test-user',
159
+ namespace: 'krate-org-default',
160
+ organizationRef: 'default',
161
+ resources
162
+ });
163
+
164
+ assert.equal(result.error, false, 'Dispatch should succeed');
165
+ assert.ok(result.contextBundle.spec.digest, 'Context bundle should have a digest');
166
+ assert.equal(
167
+ result.attempt.spec.contextBundleDigest,
168
+ result.contextBundle.spec.digest,
169
+ 'Attempt contextBundleDigest should match context bundle digest'
170
+ );
171
+ assert.equal(
172
+ result.run.spec.contextBundleRef,
173
+ result.contextBundle.metadata.name,
174
+ 'Run contextBundleRef should match context bundle name'
175
+ );
176
+ });