@a5c-ai/krate 5.0.1-staging.3a341c33c
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 +31 -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 +2455 -0
- package/dist/krate-lifecycle.json +201 -0
- package/dist/krate-runtime-snapshot.json +2955 -0
- package/dist/krate-summary.json +722 -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-memory-controller.js +374 -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/agent-workspace-controller.js +208 -0
- package/src/api-controller.js +248 -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 +551 -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 +32 -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 +211 -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-memory-controller.test.js +308 -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 +228 -0
- package/tests/agent-stack-controller.test.js +221 -0
- package/tests/agent-trigger-controller.test.js +211 -0
- package/tests/agent-workspace-controller.test.js +215 -0
- package/tests/deployment.test.js +393 -0
- package/tests/e2e/lifecycle.test.js +117 -0
- package/tests/krate.test.js +727 -0
|
@@ -0,0 +1,209 @@
|
|
|
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
|
+
function makeStack(name, overrides = {}) {
|
|
7
|
+
return createResource('AgentStack', { name }, {
|
|
8
|
+
organizationRef: 'default',
|
|
9
|
+
baseAgent: 'claude-code',
|
|
10
|
+
adapter: 'babysitter',
|
|
11
|
+
runtimeIdentity: { serviceAccountRef: overrides.serviceAccountRef || 'sa-agent' },
|
|
12
|
+
...(overrides.spec || {})
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function makeServiceAccount(name) {
|
|
17
|
+
return createResource('AgentServiceAccount', { name }, {
|
|
18
|
+
organizationRef: 'default',
|
|
19
|
+
namespace: 'krate-agents',
|
|
20
|
+
serviceAccountName: name
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function makeRoleBinding(name, subject) {
|
|
25
|
+
return createResource('AgentRoleBinding', { name }, {
|
|
26
|
+
organizationRef: 'default',
|
|
27
|
+
subject,
|
|
28
|
+
roleRef: 'agent-role',
|
|
29
|
+
scope: 'namespace'
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeSecretGrant(name, subject, purpose, overrides = {}) {
|
|
34
|
+
return createResource('AgentSecretGrant', { name }, {
|
|
35
|
+
organizationRef: 'default',
|
|
36
|
+
subject,
|
|
37
|
+
secretRef: 'api-keys',
|
|
38
|
+
purpose,
|
|
39
|
+
...overrides
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function makeMcpServer(name, overrides = {}) {
|
|
44
|
+
return createResource('AgentMcpServer', { name }, {
|
|
45
|
+
organizationRef: 'default',
|
|
46
|
+
transport: 'stdio',
|
|
47
|
+
scope: 'workspace',
|
|
48
|
+
...overrides
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function makeModelProviderGrant(subject) {
|
|
53
|
+
return makeSecretGrant('sg-model', subject, 'model-provider');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const baseInput = {
|
|
57
|
+
repository: 'my-repo',
|
|
58
|
+
ref: 'refs/heads/main',
|
|
59
|
+
actor: 'user-1',
|
|
60
|
+
agentStack: 'test-stack',
|
|
61
|
+
triggerSource: 'manual',
|
|
62
|
+
taskKind: 'fix'
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
describe('agent permission review', () => {
|
|
66
|
+
it('fully granted stack returns allowed', () => {
|
|
67
|
+
const reviewer = createPermissionReviewer();
|
|
68
|
+
const resources = {
|
|
69
|
+
AgentStack: [makeStack('test-stack')],
|
|
70
|
+
AgentServiceAccount: [makeServiceAccount('sa-agent')],
|
|
71
|
+
AgentRoleBinding: [makeRoleBinding('rb-1', 'sa-agent')],
|
|
72
|
+
AgentSecretGrant: [makeModelProviderGrant('sa-agent')],
|
|
73
|
+
AgentConfigGrant: [],
|
|
74
|
+
AgentMcpServer: []
|
|
75
|
+
};
|
|
76
|
+
const result = reviewer.reviewPermissions({ ...baseInput, resources });
|
|
77
|
+
assert.equal(result.decision, 'allowed');
|
|
78
|
+
assert.ok(result.grants.length > 0, 'should have at least one grant');
|
|
79
|
+
assert.ok(result.grants.some((g) => g.kind === 'AgentServiceAccount' && g.status === 'bound'));
|
|
80
|
+
assert.ok(result.grants.some((g) => g.kind === 'AgentRoleBinding' && g.status === 'bound'));
|
|
81
|
+
assert.ok(typeof result.digest === 'string' && result.digest.length > 0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('missing service account returns denied with missing-runtime-identity reason', () => {
|
|
85
|
+
const reviewer = createPermissionReviewer();
|
|
86
|
+
const resources = {
|
|
87
|
+
AgentStack: [makeStack('test-stack', { serviceAccountRef: 'nonexistent-sa' })],
|
|
88
|
+
AgentServiceAccount: [],
|
|
89
|
+
AgentRoleBinding: [],
|
|
90
|
+
AgentSecretGrant: [],
|
|
91
|
+
AgentConfigGrant: [],
|
|
92
|
+
AgentMcpServer: []
|
|
93
|
+
};
|
|
94
|
+
const result = reviewer.reviewPermissions({ ...baseInput, resources });
|
|
95
|
+
assert.equal(result.decision, 'denied');
|
|
96
|
+
assert.ok(result.reasons.some((r) => r.severity === 'error' && r.message.includes('Missing AgentServiceAccount')));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('missing secret grant returns denied with missingGrants information', () => {
|
|
100
|
+
const reviewer = createPermissionReviewer();
|
|
101
|
+
const mcpServer = makeMcpServer('mcp-github', { secretRef: 'github-token' });
|
|
102
|
+
const stack = makeStack('test-stack', {
|
|
103
|
+
spec: {
|
|
104
|
+
organizationRef: 'default',
|
|
105
|
+
baseAgent: 'claude-code',
|
|
106
|
+
adapter: 'babysitter',
|
|
107
|
+
runtimeIdentity: { serviceAccountRef: 'sa-agent' },
|
|
108
|
+
mcpServerRefs: ['mcp-github']
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
const resources = {
|
|
112
|
+
AgentStack: [stack],
|
|
113
|
+
AgentServiceAccount: [makeServiceAccount('sa-agent')],
|
|
114
|
+
AgentRoleBinding: [makeRoleBinding('rb-1', 'sa-agent')],
|
|
115
|
+
AgentSecretGrant: [],
|
|
116
|
+
AgentConfigGrant: [],
|
|
117
|
+
AgentMcpServer: [mcpServer]
|
|
118
|
+
};
|
|
119
|
+
const result = reviewer.reviewPermissions({ ...baseInput, resources });
|
|
120
|
+
assert.equal(result.decision, 'denied');
|
|
121
|
+
assert.ok(result.reasons.some((r) => r.severity === 'error' && r.message.includes('Missing AgentSecretGrant')));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('grant requiring approval returns requires-approval', () => {
|
|
125
|
+
const reviewer = createPermissionReviewer();
|
|
126
|
+
const mcpServer = makeMcpServer('mcp-prod', { secretRef: 'prod-secret' });
|
|
127
|
+
const stack = makeStack('test-stack', {
|
|
128
|
+
spec: {
|
|
129
|
+
organizationRef: 'default',
|
|
130
|
+
baseAgent: 'claude-code',
|
|
131
|
+
adapter: 'babysitter',
|
|
132
|
+
runtimeIdentity: { serviceAccountRef: 'sa-agent' },
|
|
133
|
+
mcpServerRefs: ['mcp-prod']
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
const mcpGrant = makeSecretGrant('sg-prod', 'sa-agent', 'mcp-server:mcp-prod', {
|
|
137
|
+
requiredApproval: 'always'
|
|
138
|
+
});
|
|
139
|
+
const resources = {
|
|
140
|
+
AgentStack: [stack],
|
|
141
|
+
AgentServiceAccount: [makeServiceAccount('sa-agent')],
|
|
142
|
+
AgentRoleBinding: [makeRoleBinding('rb-1', 'sa-agent')],
|
|
143
|
+
AgentSecretGrant: [mcpGrant, makeModelProviderGrant('sa-agent')],
|
|
144
|
+
AgentConfigGrant: [],
|
|
145
|
+
AgentMcpServer: [mcpServer]
|
|
146
|
+
};
|
|
147
|
+
const result = reviewer.reviewPermissions({ ...baseInput, resources });
|
|
148
|
+
assert.equal(result.decision, 'requires-approval');
|
|
149
|
+
assert.ok(result.grants.some((g) => g.status === 'requires-approval' && g.requiredApproval === 'always'));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('empty capabilities stack returns allowed', () => {
|
|
153
|
+
const reviewer = createPermissionReviewer();
|
|
154
|
+
const stack = makeStack('test-stack');
|
|
155
|
+
const resources = {
|
|
156
|
+
AgentStack: [stack],
|
|
157
|
+
AgentServiceAccount: [makeServiceAccount('sa-agent')],
|
|
158
|
+
AgentRoleBinding: [makeRoleBinding('rb-1', 'sa-agent')],
|
|
159
|
+
AgentSecretGrant: [makeModelProviderGrant('sa-agent')],
|
|
160
|
+
AgentConfigGrant: [],
|
|
161
|
+
AgentMcpServer: []
|
|
162
|
+
};
|
|
163
|
+
const result = reviewer.reviewPermissions({
|
|
164
|
+
...baseInput,
|
|
165
|
+
toolRefs: [],
|
|
166
|
+
skillRefs: [],
|
|
167
|
+
mcpServerRefs: [],
|
|
168
|
+
contextLabelRefs: [],
|
|
169
|
+
resources
|
|
170
|
+
});
|
|
171
|
+
assert.equal(result.decision, 'allowed');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('same input produces deterministic digest', () => {
|
|
175
|
+
const reviewer = createPermissionReviewer();
|
|
176
|
+
const resources = {
|
|
177
|
+
AgentStack: [makeStack('test-stack')],
|
|
178
|
+
AgentServiceAccount: [makeServiceAccount('sa-agent')],
|
|
179
|
+
AgentRoleBinding: [makeRoleBinding('rb-1', 'sa-agent')],
|
|
180
|
+
AgentSecretGrant: [makeModelProviderGrant('sa-agent')],
|
|
181
|
+
AgentConfigGrant: [],
|
|
182
|
+
AgentMcpServer: []
|
|
183
|
+
};
|
|
184
|
+
const result1 = reviewer.reviewPermissions({ ...baseInput, resources });
|
|
185
|
+
const result2 = reviewer.reviewPermissions({ ...baseInput, resources });
|
|
186
|
+
assert.equal(result1.digest, result2.digest);
|
|
187
|
+
assert.ok(typeof result1.digest === 'string' && result1.digest.length === 64, 'digest should be a 64-char hex SHA-256');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('createPermissionSnapshot returns frozen object with timestamp', () => {
|
|
191
|
+
const reviewer = createPermissionReviewer();
|
|
192
|
+
const resources = {
|
|
193
|
+
AgentStack: [makeStack('test-stack')],
|
|
194
|
+
AgentServiceAccount: [makeServiceAccount('sa-agent')],
|
|
195
|
+
AgentRoleBinding: [makeRoleBinding('rb-1', 'sa-agent')],
|
|
196
|
+
AgentSecretGrant: [makeModelProviderGrant('sa-agent')],
|
|
197
|
+
AgentConfigGrant: [],
|
|
198
|
+
AgentMcpServer: []
|
|
199
|
+
};
|
|
200
|
+
const review = reviewer.reviewPermissions({ ...baseInput, resources });
|
|
201
|
+
const snapshot = reviewer.createPermissionSnapshot(review);
|
|
202
|
+
assert.ok(Object.isFrozen(snapshot), 'snapshot should be frozen');
|
|
203
|
+
assert.ok(typeof snapshot.snapshotAt === 'string' && snapshot.snapshotAt.length > 0, 'snapshot should have a timestamp');
|
|
204
|
+
assert.equal(snapshot.frozen, true);
|
|
205
|
+
assert.equal(snapshot.digest, review.digest);
|
|
206
|
+
assert.equal(snapshot.decision, review.decision);
|
|
207
|
+
assert.throws(() => { snapshot.decision = 'denied'; }, TypeError);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import {
|
|
4
|
+
CONFIG_KINDS,
|
|
5
|
+
AGGREGATED_KINDS,
|
|
6
|
+
ALL_KINDS,
|
|
7
|
+
RESOURCE_DEFINITIONS,
|
|
8
|
+
createResource,
|
|
9
|
+
validateResource,
|
|
10
|
+
resourceSchemaForKind,
|
|
11
|
+
storageClassForKind,
|
|
12
|
+
listResourceDefinitions
|
|
13
|
+
} from '../src/resource-model.js';
|
|
14
|
+
|
|
15
|
+
const AGENT_CONFIG_KINDS = [
|
|
16
|
+
'AgentStack',
|
|
17
|
+
'AgentSubagent',
|
|
18
|
+
'AgentToolProfile',
|
|
19
|
+
'AgentMcpServer',
|
|
20
|
+
'AgentSkill',
|
|
21
|
+
'AgentTriggerRule',
|
|
22
|
+
'AgentContextLabel',
|
|
23
|
+
'AgentWorkspacePolicy',
|
|
24
|
+
'AgentServiceAccount',
|
|
25
|
+
'AgentRoleBinding',
|
|
26
|
+
'AgentSecretGrant',
|
|
27
|
+
'AgentConfigGrant',
|
|
28
|
+
'AgentAdapter',
|
|
29
|
+
'AgentTransportBinding',
|
|
30
|
+
'AgentProviderConfig',
|
|
31
|
+
'AgentProject',
|
|
32
|
+
'AgentGatewayConfig',
|
|
33
|
+
'AgentMemoryRepository',
|
|
34
|
+
'AgentMemorySource',
|
|
35
|
+
'AgentMemoryOntology',
|
|
36
|
+
'AgentMemoryAssociation'
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const AGENT_AGGREGATED_KINDS = [
|
|
40
|
+
'AgentDispatchRun',
|
|
41
|
+
'AgentDispatchAttempt',
|
|
42
|
+
'AgentSession',
|
|
43
|
+
'AgentContextBundle',
|
|
44
|
+
'AgentArtifact',
|
|
45
|
+
'AgentApproval',
|
|
46
|
+
'AgentWorkspace',
|
|
47
|
+
'AgentTriggerExecution',
|
|
48
|
+
'AgentCapabilityRequirement',
|
|
49
|
+
'WorkItemSessionLink',
|
|
50
|
+
'WorkItemWorkspaceLink',
|
|
51
|
+
'AgentSessionTranscript',
|
|
52
|
+
'AgentSessionAttachment',
|
|
53
|
+
'AgentWorkspaceRuntime',
|
|
54
|
+
'AgentMemorySnapshot',
|
|
55
|
+
'AgentMemoryQuery',
|
|
56
|
+
'AgentMemoryUpdate',
|
|
57
|
+
'AgentRunMemoryImport'
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const ALL_AGENT_KINDS = [...AGENT_CONFIG_KINDS, ...AGENT_AGGREGATED_KINDS];
|
|
61
|
+
|
|
62
|
+
/** Minimal valid spec for each agent kind, satisfying requiredSpec. */
|
|
63
|
+
function minimalSpecForKind(kind) {
|
|
64
|
+
const specs = {
|
|
65
|
+
AgentStack: { organizationRef: 'default', baseAgent: 'claude-code', adapter: 'babysitter', runtimeIdentity: 'sa-agent' },
|
|
66
|
+
AgentSubagent: { organizationRef: 'default', rolePrompt: 'Code reviewer', taskKinds: ['review'] },
|
|
67
|
+
AgentToolProfile: { organizationRef: 'default', filesystemPolicy: 'read-write', approvalPolicyByTool: { shell: 'auto' } },
|
|
68
|
+
AgentMcpServer: { organizationRef: 'default', transport: 'stdio', scope: 'workspace' },
|
|
69
|
+
AgentSkill: { organizationRef: 'default', format: 'markdown', sourceRef: 'skills/debug.md' },
|
|
70
|
+
AgentTriggerRule: { organizationRef: 'default', sources: ['ci-failure'], agentStack: 'default-stack', taskKind: 'fix' },
|
|
71
|
+
AgentContextLabel: { organizationRef: 'default', promptFragment: 'Always run tests before committing', allowedSources: ['admin'] },
|
|
72
|
+
AgentWorkspacePolicy: { organizationRef: 'default', mode: 'worktree', retentionPolicy: '7d' },
|
|
73
|
+
AgentServiceAccount: { organizationRef: 'default', namespace: 'krate-agents', serviceAccountName: 'agent-runner' },
|
|
74
|
+
AgentRoleBinding: { organizationRef: 'default', subject: 'agent-runner', roleRef: 'agent-role', scope: 'namespace' },
|
|
75
|
+
AgentSecretGrant: { organizationRef: 'default', subject: 'agent-runner', secretRef: 'api-keys', purpose: 'API access' },
|
|
76
|
+
AgentConfigGrant: { organizationRef: 'default', subject: 'agent-runner', configMapRef: 'agent-config', purpose: 'Configuration' },
|
|
77
|
+
AgentDispatchRun: { organizationRef: 'default', repository: 'app', sourceRefs: ['refs/heads/main'], agentStack: 'default-stack', taskKind: 'fix' },
|
|
78
|
+
AgentDispatchAttempt: { organizationRef: 'default', agentDispatchRun: 'run-1', attemptReason: 'initial', agentStackSnapshot: { baseAgent: 'claude-code' } },
|
|
79
|
+
AgentSession: { organizationRef: 'default', agentMuxSessionId: 'sess-123', dispatchRun: 'run-1' },
|
|
80
|
+
AgentContextBundle: { organizationRef: 'default', dispatchRun: 'run-1', digest: 'sha256:abc', sources: ['repo-context'] },
|
|
81
|
+
AgentArtifact: { organizationRef: 'default', dispatchRun: 'run-1', kind: 'patch', digest: 'sha256:def' },
|
|
82
|
+
AgentApproval: { organizationRef: 'default', dispatchRun: 'run-1', action: 'write-back', requestedBy: 'agent-runner' },
|
|
83
|
+
AgentWorkspace: { organizationRef: 'default', repository: 'app', workspacePath: '/workspaces/app-1', ownership: 'agent-runner' },
|
|
84
|
+
AgentTriggerExecution: { organizationRef: 'default', triggerRule: 'on-ci-fail', sourceEvent: 'pipeline-failed', decision: 'dispatch' },
|
|
85
|
+
AgentCapabilityRequirement: { organizationRef: 'default', ownerRef: 'stack-1', requiredRoles: ['shell', 'git'] },
|
|
86
|
+
WorkItemSessionLink: { organizationRef: 'default', workItemRef: 'issue-1', agentSession: 'sess-123' },
|
|
87
|
+
WorkItemWorkspaceLink: { organizationRef: 'default', workItemRef: 'issue-1', workspace: 'ws-1' },
|
|
88
|
+
AgentAdapter: { organizationRef: 'default', adapterType: 'claude-code', transport: 'stdio' },
|
|
89
|
+
AgentTransportBinding: { organizationRef: 'default', adapterRef: 'claude-code', endpoint: 'https://agent.example.test', protocol: 'https' },
|
|
90
|
+
AgentProviderConfig: { organizationRef: 'default', provider: 'anthropic', authType: 'api-key' },
|
|
91
|
+
AgentProject: { organizationRef: 'default', displayName: 'Platform' },
|
|
92
|
+
AgentGatewayConfig: { organizationRef: 'default', gatewayUrl: 'https://mux.example.test' },
|
|
93
|
+
AgentSessionTranscript: { organizationRef: 'default', sessionRef: 'sess-123', messages: [{ role: 'user', content: 'hello' }] },
|
|
94
|
+
AgentSessionAttachment: { organizationRef: 'default', sessionRef: 'sess-123', sourceType: 'upload', digest: 'sha256:abc' },
|
|
95
|
+
AgentWorkspaceRuntime: { organizationRef: 'default', workspaceRef: 'ws-1', status: 'running' },
|
|
96
|
+
AgentMemoryRepository: { organizationRef: 'default', repositoryRef: 'memory-repo', defaultBranch: 'main', layoutProfile: 'standard' },
|
|
97
|
+
AgentMemorySource: { organizationRef: 'default', repositoryRef: 'memory-repo', appliesTo: 'team:platform', include: ['decisions/**', 'runbooks/**'] },
|
|
98
|
+
AgentMemoryOntology: { organizationRef: 'default', memoryRepository: 'memory-repo', ontologyPath: '.memory/ontology.yaml' },
|
|
99
|
+
AgentMemoryAssociation: { organizationRef: 'default', memoryRef: 'decision-001', targetRef: 'issue-42', relationship: 'informs' },
|
|
100
|
+
AgentMemorySnapshot: { organizationRef: 'default', memoryRepository: 'memory-repo', requestedRef: 'refs/heads/main', resolvedCommit: 'a'.repeat(40) },
|
|
101
|
+
AgentMemoryQuery: { organizationRef: 'default', snapshotRef: 'snap-1', requester: 'agent-runner', query: 'deployment patterns' },
|
|
102
|
+
AgentMemoryUpdate: { organizationRef: 'default', memoryRepository: 'memory-repo', sourceRun: 'run-1', changes: [{ path: 'decisions/001.md', op: 'add' }] },
|
|
103
|
+
AgentRunMemoryImport: { organizationRef: 'default', memoryRepository: 'memory-repo', source: 'babysitter-run-42', include: ['journal', 'effects'] }
|
|
104
|
+
};
|
|
105
|
+
return specs[kind];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
describe('agent resource set membership', () => {
|
|
109
|
+
for (const kind of AGENT_CONFIG_KINDS) {
|
|
110
|
+
it(`${kind} is in CONFIG_KINDS`, () => {
|
|
111
|
+
assert.ok(CONFIG_KINDS.has(kind), `${kind} should be in CONFIG_KINDS`);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const kind of AGENT_AGGREGATED_KINDS) {
|
|
116
|
+
it(`${kind} is in AGGREGATED_KINDS`, () => {
|
|
117
|
+
assert.ok(AGGREGATED_KINDS.has(kind), `${kind} should be in AGGREGATED_KINDS`);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const kind of ALL_AGENT_KINDS) {
|
|
122
|
+
it(`${kind} is in ALL_KINDS`, () => {
|
|
123
|
+
assert.ok(ALL_KINDS.has(kind), `${kind} should be in ALL_KINDS`);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('RESOURCE_DEFINITIONS for agent kinds', () => {
|
|
129
|
+
for (const kind of ALL_AGENT_KINDS) {
|
|
130
|
+
it(`${kind} has valid definition with storage, context, plural, purpose, requiredSpec`, () => {
|
|
131
|
+
const def = RESOURCE_DEFINITIONS[kind];
|
|
132
|
+
assert.ok(def, `${kind} should exist in RESOURCE_DEFINITIONS`);
|
|
133
|
+
assert.ok(['etcd', 'postgres'].includes(def.storage), `${kind} storage should be etcd or postgres`);
|
|
134
|
+
assert.ok(typeof def.context === 'string' && def.context.length > 0, `${kind} context should be a non-empty string`);
|
|
135
|
+
assert.ok(typeof def.plural === 'string' && def.plural.length > 0, `${kind} plural should be a non-empty string`);
|
|
136
|
+
assert.ok(typeof def.purpose === 'string' && def.purpose.length > 0, `${kind} purpose should be a non-empty string`);
|
|
137
|
+
assert.ok(Array.isArray(def.requiredSpec) && def.requiredSpec.length > 0, `${kind} requiredSpec should be a non-empty array`);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('createResource for agent kinds', () => {
|
|
143
|
+
for (const kind of ALL_AGENT_KINDS) {
|
|
144
|
+
it(`creates a valid ${kind} resource`, () => {
|
|
145
|
+
const spec = minimalSpecForKind(kind);
|
|
146
|
+
const resource = createResource(kind, { name: `test-${kind.toLowerCase()}` }, spec);
|
|
147
|
+
assert.equal(resource.apiVersion, 'krate.a5c.ai/v1alpha1');
|
|
148
|
+
assert.equal(resource.kind, kind);
|
|
149
|
+
assert.equal(resource.metadata.name, `test-${kind.toLowerCase()}`);
|
|
150
|
+
assert.equal(resource.metadata.namespace, 'default');
|
|
151
|
+
assert.ok(resource.metadata.labels !== undefined);
|
|
152
|
+
assert.ok(resource.metadata.annotations !== undefined);
|
|
153
|
+
assert.ok(typeof resource.spec === 'object');
|
|
154
|
+
assert.ok(typeof resource.status === 'object');
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('validateResource rejects missing required spec fields for agent kinds', () => {
|
|
160
|
+
for (const kind of ALL_AGENT_KINDS) {
|
|
161
|
+
it(`throws on empty spec for ${kind}`, () => {
|
|
162
|
+
const resource = {
|
|
163
|
+
apiVersion: 'krate.a5c.ai/v1alpha1',
|
|
164
|
+
kind,
|
|
165
|
+
metadata: { name: `invalid-${kind.toLowerCase()}` },
|
|
166
|
+
spec: {},
|
|
167
|
+
status: {}
|
|
168
|
+
};
|
|
169
|
+
const def = RESOURCE_DEFINITIONS[kind];
|
|
170
|
+
assert.throws(
|
|
171
|
+
() => validateResource(resource),
|
|
172
|
+
(err) => {
|
|
173
|
+
assert.ok(err.message.includes(`${kind} spec.${def.requiredSpec[0]} is required`));
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('resourceSchemaForKind for agent kinds', () => {
|
|
182
|
+
for (const kind of ALL_AGENT_KINDS) {
|
|
183
|
+
it(`returns correct schema for ${kind}`, () => {
|
|
184
|
+
const schema = resourceSchemaForKind(kind);
|
|
185
|
+
assert.equal(schema.apiVersion, 'krate.a5c.ai/v1alpha1');
|
|
186
|
+
assert.equal(schema.kind, kind);
|
|
187
|
+
assert.ok(typeof schema.plural === 'string');
|
|
188
|
+
assert.ok(['etcd', 'postgres'].includes(schema.storage));
|
|
189
|
+
assert.ok(Array.isArray(schema.required.metadata));
|
|
190
|
+
assert.ok(schema.required.metadata.includes('name'));
|
|
191
|
+
assert.ok(Array.isArray(schema.required.spec));
|
|
192
|
+
assert.ok(schema.required.spec.length > 0);
|
|
193
|
+
assert.deepEqual(schema.status, ['storage', 'phase', 'conditions']);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('storageClassForKind for agent kinds', () => {
|
|
199
|
+
for (const kind of AGENT_CONFIG_KINDS) {
|
|
200
|
+
it(`${kind} returns etcd`, () => {
|
|
201
|
+
assert.equal(storageClassForKind(kind), 'etcd');
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (const kind of AGENT_AGGREGATED_KINDS) {
|
|
206
|
+
it(`${kind} returns postgres`, () => {
|
|
207
|
+
assert.equal(storageClassForKind(kind), 'postgres');
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('kind set counts', () => {
|
|
213
|
+
it('CONFIG_KINDS has 41 members', () => {
|
|
214
|
+
assert.equal(CONFIG_KINDS.size, 41);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('AGGREGATED_KINDS has 24 members', () => {
|
|
218
|
+
assert.equal(AGGREGATED_KINDS.size, 24);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('ALL_KINDS has 65 members', () => {
|
|
222
|
+
assert.equal(ALL_KINDS.size, 65);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('listResourceDefinitions returns 65 definitions', () => {
|
|
226
|
+
assert.equal(listResourceDefinitions().length, 65);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createAgentStackController, 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 makeToolProfile(name) {
|
|
16
|
+
return createResource('AgentToolProfile', { name, namespace: 'krate-org-default' }, {
|
|
17
|
+
organizationRef: 'default',
|
|
18
|
+
filesystemPolicy: 'read-write',
|
|
19
|
+
approvalPolicyByTool: {}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeMcpServer(name) {
|
|
24
|
+
return createResource('AgentMcpServer', { name, namespace: 'krate-org-default' }, {
|
|
25
|
+
organizationRef: 'default',
|
|
26
|
+
transport: 'stdio',
|
|
27
|
+
scope: 'workspace'
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeSkill(name, overrides = {}) {
|
|
32
|
+
return createResource('AgentSkill', { name, namespace: 'krate-org-default' }, {
|
|
33
|
+
organizationRef: 'default',
|
|
34
|
+
format: 'markdown',
|
|
35
|
+
sourceRef: 'git://repo/skills/' + name,
|
|
36
|
+
...overrides
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function makeSubagent(name, overrides = {}) {
|
|
41
|
+
return createResource('AgentSubagent', { name, namespace: 'krate-org-default' }, {
|
|
42
|
+
organizationRef: 'default',
|
|
43
|
+
rolePrompt: 'Test subagent',
|
|
44
|
+
taskKinds: ['code-review'],
|
|
45
|
+
...overrides
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function makeContextLabel(name) {
|
|
50
|
+
return createResource('AgentContextLabel', { name, namespace: 'krate-org-default' }, {
|
|
51
|
+
organizationRef: 'default',
|
|
52
|
+
promptFragment: 'context for ' + name,
|
|
53
|
+
allowedSources: ['manual']
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function makeServiceAccount(name) {
|
|
58
|
+
return createResource('AgentServiceAccount', { name, namespace: 'krate-org-default' }, {
|
|
59
|
+
organizationRef: 'default',
|
|
60
|
+
namespace: 'krate-org-default',
|
|
61
|
+
serviceAccountName: name
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function makeRoleBinding(name, subject) {
|
|
66
|
+
return createResource('AgentRoleBinding', { name, namespace: 'krate-org-default' }, {
|
|
67
|
+
organizationRef: 'default',
|
|
68
|
+
subject,
|
|
69
|
+
roleRef: 'agent-developer',
|
|
70
|
+
scope: 'namespace'
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function makeSecretGrant(name, subject, purpose) {
|
|
75
|
+
return createResource('AgentSecretGrant', { name, namespace: 'krate-org-default' }, {
|
|
76
|
+
organizationRef: 'default',
|
|
77
|
+
subject,
|
|
78
|
+
secretRef: 'secret-' + purpose,
|
|
79
|
+
purpose
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
test('Stack with all refs present results in Ready=True', () => {
|
|
84
|
+
const controller = createAgentStackController();
|
|
85
|
+
const stack = makeStack('full-stack', {
|
|
86
|
+
toolPolicy: 'tool-profile-1',
|
|
87
|
+
mcpServerRefs: ['mcp-github'],
|
|
88
|
+
skillRefs: ['skill-review'],
|
|
89
|
+
subagentRefs: ['sub-linter'],
|
|
90
|
+
contextLabelRefs: ['ctx-security']
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const resources = {
|
|
94
|
+
AgentStack: [stack],
|
|
95
|
+
AgentToolProfile: [makeToolProfile('tool-profile-1')],
|
|
96
|
+
AgentMcpServer: [makeMcpServer('mcp-github')],
|
|
97
|
+
AgentSkill: [makeSkill('skill-review')],
|
|
98
|
+
AgentSubagent: [makeSubagent('sub-linter')],
|
|
99
|
+
AgentContextLabel: [makeContextLabel('ctx-security')],
|
|
100
|
+
AgentServiceAccount: [makeServiceAccount('sa-default')],
|
|
101
|
+
AgentRoleBinding: [makeRoleBinding('rb-1', 'sa-default')],
|
|
102
|
+
AgentSecretGrant: [makeSecretGrant('sg-model', 'sa-default', 'model-provider')]
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const result = controller.reconcileStack(stack, resources);
|
|
106
|
+
const readyCondition = result.conditions.find((c) => c.type === 'Ready');
|
|
107
|
+
assert.equal(readyCondition.status, 'True', 'Ready condition should be True when all refs are present');
|
|
108
|
+
assert.equal(result.validation, 'valid');
|
|
109
|
+
assert.equal(result.capabilities.tools.length, 1);
|
|
110
|
+
assert.equal(result.capabilities.mcpServers.length, 1);
|
|
111
|
+
assert.equal(result.capabilities.skills.length, 1);
|
|
112
|
+
assert.equal(result.capabilities.subagents.length, 1);
|
|
113
|
+
assert.equal(result.capabilities.contextLabels.length, 1);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('Stack with missing MCP server results in McpHealthy=False and Ready=False', () => {
|
|
117
|
+
const controller = createAgentStackController();
|
|
118
|
+
const stack = makeStack('missing-mcp-stack', {
|
|
119
|
+
mcpServerRefs: ['mcp-github', 'mcp-missing']
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const resources = {
|
|
123
|
+
AgentStack: [stack],
|
|
124
|
+
AgentMcpServer: [makeMcpServer('mcp-github')],
|
|
125
|
+
AgentServiceAccount: [makeServiceAccount('sa-default')],
|
|
126
|
+
AgentRoleBinding: [makeRoleBinding('rb-1', 'sa-default')],
|
|
127
|
+
AgentSecretGrant: [makeSecretGrant('sg-model', 'sa-default', 'model-provider')]
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const result = controller.reconcileStack(stack, resources);
|
|
131
|
+
const mcpCondition = result.conditions.find((c) => c.type === 'McpHealthy');
|
|
132
|
+
const readyCondition = result.conditions.find((c) => c.type === 'Ready');
|
|
133
|
+
assert.equal(mcpCondition.status, 'False', 'McpHealthy should be False when MCP server is missing');
|
|
134
|
+
assert.equal(readyCondition.status, 'False', 'Ready should be False when McpHealthy is False');
|
|
135
|
+
assert.ok(mcpCondition.message.includes('mcp-missing'));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('Stack with missing skill results in SkillsValidated=False and Ready=False', () => {
|
|
139
|
+
const controller = createAgentStackController();
|
|
140
|
+
const stack = makeStack('missing-skill-stack', {
|
|
141
|
+
skillRefs: ['skill-review', 'skill-ghost']
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const resources = {
|
|
145
|
+
AgentStack: [stack],
|
|
146
|
+
AgentSkill: [makeSkill('skill-review')],
|
|
147
|
+
AgentServiceAccount: [makeServiceAccount('sa-default')],
|
|
148
|
+
AgentRoleBinding: [makeRoleBinding('rb-1', 'sa-default')],
|
|
149
|
+
AgentSecretGrant: [makeSecretGrant('sg-model', 'sa-default', 'model-provider')]
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const result = controller.reconcileStack(stack, resources);
|
|
153
|
+
const skillsCondition = result.conditions.find((c) => c.type === 'SkillsValidated');
|
|
154
|
+
const readyCondition = result.conditions.find((c) => c.type === 'Ready');
|
|
155
|
+
assert.equal(skillsCondition.status, 'False', 'SkillsValidated should be False when skill is missing');
|
|
156
|
+
assert.equal(readyCondition.status, 'False', 'Ready should be False when SkillsValidated is False');
|
|
157
|
+
assert.ok(skillsCondition.message.includes('skill-ghost'));
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('Minimal stack with no capability refs results in Ready=True', () => {
|
|
161
|
+
const controller = createAgentStackController();
|
|
162
|
+
const stack = makeStack('minimal-stack');
|
|
163
|
+
|
|
164
|
+
const resources = {
|
|
165
|
+
AgentStack: [stack],
|
|
166
|
+
AgentServiceAccount: [makeServiceAccount('sa-default')],
|
|
167
|
+
AgentRoleBinding: [makeRoleBinding('rb-1', 'sa-default')],
|
|
168
|
+
AgentSecretGrant: [makeSecretGrant('sg-model', 'sa-default', 'model-provider')]
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const result = controller.reconcileStack(stack, resources);
|
|
172
|
+
const readyCondition = result.conditions.find((c) => c.type === 'Ready');
|
|
173
|
+
assert.equal(readyCondition.status, 'True', 'Ready should be True for minimal stack with identity present');
|
|
174
|
+
assert.equal(result.validation, 'valid');
|
|
175
|
+
assert.equal(result.capabilities.tools.length, 0);
|
|
176
|
+
assert.equal(result.capabilities.mcpServers.length, 0);
|
|
177
|
+
assert.equal(result.capabilities.skills.length, 0);
|
|
178
|
+
assert.equal(result.capabilities.subagents.length, 0);
|
|
179
|
+
assert.equal(result.capabilities.contextLabels.length, 0);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('listStackCapabilities returns correct normalized capability list', () => {
|
|
183
|
+
const controller = createAgentStackController();
|
|
184
|
+
const stack = makeStack('caps-stack', {
|
|
185
|
+
toolPolicy: 'tp-1',
|
|
186
|
+
mcpServerRefs: ['mcp-a', 'mcp-b'],
|
|
187
|
+
skillRefs: ['skill-x'],
|
|
188
|
+
subagentRefs: ['sub-y'],
|
|
189
|
+
contextLabelRefs: ['ctx-z']
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const resources = {
|
|
193
|
+
AgentToolProfile: [makeToolProfile('tp-1')],
|
|
194
|
+
AgentMcpServer: [makeMcpServer('mcp-a')],
|
|
195
|
+
AgentSkill: [makeSkill('skill-x')],
|
|
196
|
+
AgentSubagent: [makeSubagent('sub-y')],
|
|
197
|
+
AgentContextLabel: [makeContextLabel('ctx-z')]
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const capabilities = controller.listStackCapabilities(stack, resources);
|
|
201
|
+
assert.equal(capabilities.length, 6);
|
|
202
|
+
|
|
203
|
+
const tool = capabilities.find((c) => c.kind === 'tool');
|
|
204
|
+
assert.equal(tool.name, 'tp-1');
|
|
205
|
+
assert.equal(tool.status, 'resolved');
|
|
206
|
+
|
|
207
|
+
const mcpA = capabilities.find((c) => c.kind === 'mcp' && c.name === 'mcp-a');
|
|
208
|
+
assert.equal(mcpA.status, 'resolved');
|
|
209
|
+
|
|
210
|
+
const mcpB = capabilities.find((c) => c.kind === 'mcp' && c.name === 'mcp-b');
|
|
211
|
+
assert.equal(mcpB.status, 'missing');
|
|
212
|
+
|
|
213
|
+
const skill = capabilities.find((c) => c.kind === 'skill');
|
|
214
|
+
assert.equal(skill.status, 'resolved');
|
|
215
|
+
|
|
216
|
+
const subagent = capabilities.find((c) => c.kind === 'subagent');
|
|
217
|
+
assert.equal(subagent.status, 'resolved');
|
|
218
|
+
|
|
219
|
+
const ctxLabel = capabilities.find((c) => c.kind === 'contextLabel');
|
|
220
|
+
assert.equal(ctxLabel.status, 'resolved');
|
|
221
|
+
});
|