@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.
- package/Dockerfile +29 -0
- package/README.md +183 -0
- package/bin/krate-demo.mjs +23 -0
- package/bin/krate-server.mjs +14 -0
- package/dist/krate-controller-ui.json +2407 -0
- package/dist/krate-lifecycle.json +201 -0
- package/dist/krate-runtime-snapshot.json +2955 -0
- package/dist/krate-summary.json +687 -0
- package/docs/README.md +61 -0
- package/docs/agents/README.md +83 -0
- package/docs/agents/acceptance-test-matrix.md +193 -0
- package/docs/agents/agent-mux-adapter-contract.md +167 -0
- package/docs/agents/agent-mux-source-map.md +310 -0
- package/docs/agents/agent-run-memory-import-spec.md +256 -0
- package/docs/agents/agent-stack-management-spec.md +421 -0
- package/docs/agents/api-contract-spec.md +309 -0
- package/docs/agents/artifacts-writeback-spec.md +145 -0
- package/docs/agents/chart-packaging-spec.md +128 -0
- package/docs/agents/ci-orchestration-spec.md +140 -0
- package/docs/agents/context-assembly-spec.md +219 -0
- package/docs/agents/controller-reconciliation-spec.md +255 -0
- package/docs/agents/crd-schema-spec.md +315 -0
- package/docs/agents/decision-log-open-questions.md +169 -0
- package/docs/agents/developer-implementation-checklist.md +329 -0
- package/docs/agents/dispatching-design.md +262 -0
- package/docs/agents/glossary.md +66 -0
- package/docs/agents/implementation-blueprint.md +324 -0
- package/docs/agents/implementation-rollout-slices.md +251 -0
- package/docs/agents/memory-context-integration-spec.md +194 -0
- package/docs/agents/memory-ontology-schema-spec.md +253 -0
- package/docs/agents/memory-operations-runbook.md +121 -0
- package/docs/agents/mvp-vertical-slice-spec.md +146 -0
- package/docs/agents/observability-audit-spec.md +265 -0
- package/docs/agents/operator-runbook.md +174 -0
- package/docs/agents/org-memory-api-payload-examples.md +333 -0
- package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
- package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
- package/docs/agents/org-memory-ui-implementation-map.md +114 -0
- package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
- package/docs/agents/org-resource-model-delta-spec.md +111 -0
- package/docs/agents/org-route-resource-model-spec.md +183 -0
- package/docs/agents/org-scoping-namespace-spec.md +114 -0
- package/docs/agents/rbac-secrets-management-spec.md +406 -0
- package/docs/agents/repository-page-integration-spec.md +255 -0
- package/docs/agents/resource-contract-examples.md +808 -0
- package/docs/agents/resource-relationship-map.md +190 -0
- package/docs/agents/security-threat-model.md +188 -0
- package/docs/agents/shared-memory-company-brain-spec.md +358 -0
- package/docs/agents/storage-migration-spec.md +168 -0
- package/docs/agents/subagent-orchestration-spec.md +152 -0
- package/docs/agents/system-overview.md +88 -0
- package/docs/agents/tools-mcp-skills-spec.md +189 -0
- package/docs/agents/traceability-matrix.md +79 -0
- package/docs/agents/ui-flow-spec.md +211 -0
- package/docs/agents/ui-ux-system-spec.md +426 -0
- package/docs/agents/workspace-lifecycle-spec.md +166 -0
- package/docs/architecture-spec.md +78 -0
- package/docs/components/control-plane.md +78 -0
- package/docs/components/data-plane.md +69 -0
- package/docs/components/hooks-events.md +67 -0
- package/docs/components/identity-rbac-policy.md +73 -0
- package/docs/components/kubevela-oam.md +70 -0
- package/docs/components/operations-publishing.md +81 -0
- package/docs/components/runners-ci.md +66 -0
- package/docs/components/web-ui.md +94 -0
- package/docs/external/README.md +47 -0
- package/docs/external/bidirectional-sync-design.md +134 -0
- package/docs/external/cicd-interface.md +64 -0
- package/docs/external/external-backend-controllers.md +170 -0
- package/docs/external/external-backend-crds.md +234 -0
- package/docs/external/external-backend-ui-spec.md +151 -0
- package/docs/external/external-backend-ux-flows.md +115 -0
- package/docs/external/external-object-mapping.md +125 -0
- package/docs/external/git-forge-interface.md +68 -0
- package/docs/external/github-integration-design.md +151 -0
- package/docs/external/issue-tracking-interface.md +66 -0
- package/docs/external/provider-capability-manifests.md +204 -0
- package/docs/external/provider-catalog.md +139 -0
- package/docs/external/provider-rollout-testing.md +78 -0
- package/docs/external/research-results.md +48 -0
- package/docs/external/security-auth-permissions.md +81 -0
- package/docs/external/sync-state-machines.md +108 -0
- package/docs/external/unified-external-backend-model.md +107 -0
- package/docs/external/user-facing-changes.md +67 -0
- package/docs/gaps.md +161 -0
- package/docs/install.md +94 -0
- package/docs/krate-design.md +334 -0
- package/docs/local-minikube.md +55 -0
- package/docs/ontology/README.md +32 -0
- package/docs/ontology/bounded-contexts.md +29 -0
- package/docs/ontology/events-and-hooks.md +32 -0
- package/docs/ontology/oam-kubevela.md +32 -0
- package/docs/ontology/operations-and-release.md +25 -0
- package/docs/ontology/personas-and-actors.md +32 -0
- package/docs/ontology/policies-and-invariants.md +33 -0
- package/docs/ontology/problem-space.md +30 -0
- package/docs/ontology/resource-contracts.md +40 -0
- package/docs/ontology/resource-taxonomy.md +42 -0
- package/docs/ontology/runners-and-ci.md +29 -0
- package/docs/ontology/solution-space.md +24 -0
- package/docs/ontology/storage-and-data-boundaries.md +29 -0
- package/docs/ontology/validation-matrix.md +24 -0
- package/docs/ontology/web-ui-excellent-flows.md +32 -0
- package/docs/ontology/workflows.md +39 -0
- package/docs/ontology/world.md +35 -0
- package/docs/product-requirements.md +62 -0
- package/docs/roadmap-mvp.md +87 -0
- package/docs/system-requirements.md +90 -0
- package/docs/tests/README.md +53 -0
- package/docs/tests/agent-qa-plan.md +63 -0
- package/docs/tests/browser-ui-tests.md +62 -0
- package/docs/tests/ci-quality-gates.md +48 -0
- package/docs/tests/coverage-model.md +64 -0
- package/docs/tests/e2e-scenario-tests.md +53 -0
- package/docs/tests/fixtures-test-data.md +63 -0
- package/docs/tests/observability-reliability-tests.md +54 -0
- package/docs/tests/product-test-matrix.md +145 -0
- package/docs/tests/qa-adoption-roadmap.md +130 -0
- package/docs/tests/qa-automation-plan.md +101 -0
- package/docs/tests/security-compliance-tests.md +57 -0
- package/docs/tests/test-framework-tools.md +88 -0
- package/docs/tests/test-suite-layout.md +121 -0
- package/docs/tests/unit-integration-tests.md +48 -0
- package/docs/todo-kyverno +714 -0
- package/docs/user-stories.md +78 -0
- package/examples/minikube-demo.yaml +190 -0
- package/examples/oam-application.yaml +23 -0
- package/examples/policy-kyverno-pr-title.yaml +18 -0
- package/package.json +63 -0
- package/scripts/build.mjs +29 -0
- package/scripts/setup-minikube.mjs +65 -0
- package/scripts/smoke.mjs +37 -0
- package/scripts/validate-doc-coverage.mjs +152 -0
- package/scripts/validate-package.mjs +93 -0
- package/scripts/validate-ui.mjs +207 -0
- package/src/agent-approval-controller.js +123 -0
- package/src/agent-context-bundles.js +242 -0
- package/src/agent-dispatch-controller.js +86 -0
- package/src/agent-mux-client.js +280 -0
- package/src/agent-permission-review.js +162 -0
- package/src/agent-stack-controller.js +296 -0
- package/src/agent-trigger-controller.js +108 -0
- package/src/api-controller.js +206 -0
- package/src/argocd-gitops.js +43 -0
- package/src/auth.js +265 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +38 -0
- package/src/controller-ui.js +538 -0
- package/src/data-plane.js +178 -0
- package/src/gitea-backend.js +95 -0
- package/src/handoff.js +98 -0
- package/src/hooks-events.js +63 -0
- package/src/http-server.js +151 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +30 -0
- package/src/kubernetes-controller.js +812 -0
- package/src/kubernetes-resource-gateway.js +48 -0
- package/src/operations.js +112 -0
- package/src/resource-model.js +203 -0
- package/src/runners-ci.js +48 -0
- package/src/runtime.js +196 -0
- package/src/web-ui.js +40 -0
- package/tests/agent-approval-controller.test.js +173 -0
- package/tests/agent-context-bundles.test.js +278 -0
- package/tests/agent-dispatch-controller.test.js +176 -0
- package/tests/agent-mux-client.test.js +204 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-resources.test.js +212 -0
- package/tests/agent-stack-controller.test.js +221 -0
- package/tests/agent-trigger-controller.test.js +211 -0
- package/tests/deployment.test.js +395 -0
- package/tests/e2e/lifecycle.test.js +117 -0
- 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
|
+
});
|