@a5c-ai/kradle 5.0.1-staging.3abdf9534c25
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 +187 -0
- package/bin/kradle-demo.mjs +23 -0
- package/bin/kradle-server.mjs +14 -0
- package/dist/kradle-controller-ui.json +3482 -0
- package/dist/kradle-lifecycle.json +201 -0
- package/dist/kradle-runtime-snapshot.json +3125 -0
- package/dist/kradle-summary.json +724 -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/gaps-agent-mux-to-kradle-crds.md +298 -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/architecture-v2.md +2759 -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/crd-behaviors-and-relationships.md +3926 -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/integration-and-design-decisions.md +1530 -0
- package/docs/kradle-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/openapi.yaml +1291 -0
- package/docs/product-requirements.md +62 -0
- package/docs/requirements-v2.md +235 -0
- package/docs/roadmap-mvp.md +87 -0
- package/docs/sdk-api-reference.md +1108 -0
- package/docs/system-requirements.md +90 -0
- package/docs/system-spec-v2.md +1230 -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/todos.md +4 -0
- package/docs/user-stories.md +78 -0
- package/docs/web-console-spec.md +533 -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 +66 -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 +95 -0
- package/scripts/validate-ui.mjs +305 -0
- package/src/agent-adapter-controller.js +169 -0
- package/src/agent-approval-controller.js +170 -0
- package/src/agent-context-bundles.js +242 -0
- package/src/agent-dispatch-controller.js +549 -0
- package/src/agent-gateway-config-controller.js +147 -0
- package/src/agent-identity-migration.js +115 -0
- package/src/agent-memory-controller.js +357 -0
- package/src/agent-memory-import.js +327 -0
- package/src/agent-memory-query.js +292 -0
- package/src/agent-memory-repository-source-controller.js +255 -0
- package/src/agent-mux-client.js +589 -0
- package/src/agent-permission-review.js +250 -0
- package/src/agent-persona-controller.js +135 -0
- package/src/agent-project-controller.js +117 -0
- package/src/agent-prompt-composition.js +55 -0
- package/src/agent-provider-config-controller.js +151 -0
- package/src/agent-secret-config-grant-controller.js +282 -0
- package/src/agent-session-transcript-controller.js +189 -0
- package/src/agent-stack-controller.js +421 -0
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +387 -0
- package/src/agent-workspace-controller.js +702 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +621 -0
- package/src/argocd-gitops.js +43 -0
- package/src/artifact-registry-controller.js +542 -0
- package/src/assistant-runtime.js +284 -0
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +310 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +112 -0
- package/src/controller-ui.js +620 -0
- package/src/data-plane.js +179 -0
- package/src/event-bus.js +397 -0
- package/src/external/conflict-controller.js +225 -0
- package/src/external/github/auth.js +96 -0
- package/src/external/github/cicd.js +180 -0
- package/src/external/github/git-forge.js +240 -0
- package/src/external/github/index.js +144 -0
- package/src/external/github/issue-tracking.js +163 -0
- package/src/external/provider-adapter.js +161 -0
- package/src/external/provider-resource-factory.js +221 -0
- package/src/external/sync-controller.js +235 -0
- package/src/external/webhook-controller.js +144 -0
- package/src/external/write-controller.js +283 -0
- package/src/gitea-backend.js +131 -0
- package/src/gitea-service.js +173 -0
- package/src/handoff.js +98 -0
- package/src/health-probes.js +134 -0
- package/src/hooks-events.js +63 -0
- package/src/hooks-lifecycle.js +117 -0
- package/src/http-server.js +409 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +71 -0
- package/src/jitsi-agent-bridge.js +141 -0
- package/src/jitsi-meeting-controller.js +291 -0
- package/src/jitsi-sync-controller.js +198 -0
- package/src/kradle-inference-service-controller.js +246 -0
- package/src/kubernetes-controller-async.js +531 -0
- package/src/kubernetes-controller.js +904 -0
- package/src/kubernetes-resource-gateway.js +48 -0
- package/src/model-route-controller.js +364 -0
- package/src/notification-controller.js +178 -0
- package/src/operations.js +112 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +282 -0
- package/src/runner-controller.js +272 -0
- package/src/runners-ci.js +48 -0
- package/src/runtime.js +196 -0
- package/src/snapshot-cache.js +157 -0
- package/src/virtual-model-controller.js +538 -0
- package/src/virtual-model-hook-bridge.js +200 -0
- package/src/web-ui.js +40 -0
- package/tests/agent-adapter-controller.test.js +361 -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 +679 -0
- package/tests/agent-gateway-config-controller.test.js +386 -0
- package/tests/agent-identity-migration.test.js +87 -0
- package/tests/agent-memory-controller.test.js +461 -0
- package/tests/agent-memory-import-snapshot.test.js +477 -0
- package/tests/agent-memory-query.test.js +404 -0
- package/tests/agent-memory-repository-source.test.js +514 -0
- package/tests/agent-mux-client.test.js +389 -0
- package/tests/agent-mux-integration.test.js +971 -0
- package/tests/agent-permission-review-v2.test.js +317 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-persona-controller.test.js +127 -0
- package/tests/agent-project-controller.test.js +302 -0
- package/tests/agent-prompt-composition.test.js +76 -0
- package/tests/agent-provider-config-controller.test.js +376 -0
- package/tests/agent-resources.test.js +303 -0
- package/tests/agent-secret-config-grant.test.js +231 -0
- package/tests/agent-session-transcript-controller.test.js +499 -0
- package/tests/agent-stack-controller.test.js +283 -0
- package/tests/agent-subagent-controller.test.js +201 -0
- package/tests/agent-transport-binding-controller.test.js +294 -0
- package/tests/agent-trigger-controller.test.js +271 -0
- package/tests/agent-trigger-routes.test.js +190 -0
- package/tests/agent-trigger-sources.test.js +245 -0
- package/tests/agent-workspace-controller.test.js +181 -0
- package/tests/agent-writeback.test.js +292 -0
- package/tests/approval-persistence.test.js +171 -0
- package/tests/artifact-registry.test.js +511 -0
- package/tests/assistant-runtime.test.js +506 -0
- package/tests/async-controller.test.js +252 -0
- package/tests/audit-controller.test.js +227 -0
- package/tests/codespace-controller.test.js +318 -0
- package/tests/controller-client.test.js +133 -0
- package/tests/deployment.test.js +527 -0
- package/tests/e2e/lifecycle.test.js +120 -0
- package/tests/event-bus-integration.test.js +355 -0
- package/tests/external-github-forge.test.js +560 -0
- package/tests/external-github-issues-cicd.test.js +520 -0
- package/tests/external-integration.test.js +470 -0
- package/tests/external-persistence.test.js +415 -0
- package/tests/external-provider-adapter.test.js +365 -0
- package/tests/external-resource-model.test.js +223 -0
- package/tests/external-webhook-sync.test.js +287 -0
- package/tests/external-write-conflict.test.js +353 -0
- package/tests/gitea-service.test.js +253 -0
- package/tests/health-check-real.test.js +165 -0
- package/tests/health-probes.test.js +90 -0
- package/tests/hooks-lifecycle.test.js +364 -0
- package/tests/integration/full-flow.test.js +266 -0
- package/tests/jitsi-agent-bridge.test.js +119 -0
- package/tests/jitsi-helm-integration.test.js +77 -0
- package/tests/jitsi-meeting-controller.test.js +170 -0
- package/tests/jitsi-resource-model.test.js +73 -0
- package/tests/jitsi-sync-controller.test.js +112 -0
- package/tests/kradle-inference-service.test.js +689 -0
- package/tests/kradle.test.js +779 -0
- package/tests/memory-search-wiring.test.js +270 -0
- package/tests/model-route-controller.test.js +733 -0
- package/tests/notification-controller.test.js +196 -0
- package/tests/notification-integration.test.js +179 -0
- package/tests/org-scoping.test.js +687 -0
- package/tests/runner-controller.test.js +327 -0
- package/tests/runner-integration.test.js +231 -0
- package/tests/session-cookie-hmac.test.js +151 -0
- package/tests/snapshot-performance.test.js +315 -0
- package/tests/sse-events.test.js +107 -0
- package/tests/virtual-model-controller.test.js +877 -0
- package/tests/virtual-model-hook-bridge.test.js +384 -0
- package/tests/webhook-trigger.test.js +198 -0
- package/tests/workspace-volumes.test.js +312 -0
- package/tests/writeback-persistence.test.js +207 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createPermissionReviewer } from '../src/agent-permission-review.js';
|
|
4
|
+
import { createResource } from '../src/resource-model.js';
|
|
5
|
+
|
|
6
|
+
// ---------- shared helpers ----------
|
|
7
|
+
|
|
8
|
+
function makeStack(name, specOverrides = {}) {
|
|
9
|
+
return createResource('AgentStack', { name }, {
|
|
10
|
+
organizationRef: 'org-a',
|
|
11
|
+
baseAgent: 'claude-code',
|
|
12
|
+
adapter: 'babysitter',
|
|
13
|
+
runtimeIdentity: { serviceAccountRef: 'sa-agent' },
|
|
14
|
+
...specOverrides
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function makeServiceAccount(name, orgRef = 'org-a') {
|
|
19
|
+
return createResource('AgentServiceAccount', { name }, {
|
|
20
|
+
organizationRef: orgRef,
|
|
21
|
+
namespace: 'kradle-agents',
|
|
22
|
+
serviceAccountName: name
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeRoleBinding(name, subject, orgRef = 'org-a') {
|
|
27
|
+
return createResource('AgentRoleBinding', { name }, {
|
|
28
|
+
organizationRef: orgRef,
|
|
29
|
+
subject,
|
|
30
|
+
roleRef: 'agent-role',
|
|
31
|
+
scope: 'namespace'
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeSecretGrant(name, subject, purpose, overrides = {}) {
|
|
36
|
+
return createResource('AgentSecretGrant', { name }, {
|
|
37
|
+
organizationRef: 'org-a',
|
|
38
|
+
subject,
|
|
39
|
+
secretRef: 'api-keys',
|
|
40
|
+
purpose,
|
|
41
|
+
...overrides
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function makeWorkspacePolicy(name, specOverrides = {}) {
|
|
46
|
+
return createResource('KradleWorkspacePolicy', { name }, {
|
|
47
|
+
organizationRef: 'org-a',
|
|
48
|
+
mode: 'ephemeral',
|
|
49
|
+
retentionPolicy: 'delete-on-completion',
|
|
50
|
+
...specOverrides
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function fullyGrantedResources(stackOverrides = {}) {
|
|
55
|
+
return {
|
|
56
|
+
AgentStack: [makeStack('test-stack', stackOverrides)],
|
|
57
|
+
AgentServiceAccount: [makeServiceAccount('sa-agent')],
|
|
58
|
+
AgentRoleBinding: [makeRoleBinding('rb-1', 'sa-agent')],
|
|
59
|
+
AgentSecretGrant: [makeSecretGrant('sg-model', 'sa-agent', 'model-provider')],
|
|
60
|
+
AgentConfigGrant: [],
|
|
61
|
+
AgentMcpServer: [],
|
|
62
|
+
KradleWorkspacePolicy: []
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const baseInput = {
|
|
67
|
+
repository: 'org-a/my-repo',
|
|
68
|
+
ref: 'refs/heads/main',
|
|
69
|
+
actor: 'user-1',
|
|
70
|
+
agentStack: 'test-stack',
|
|
71
|
+
triggerSource: 'manual',
|
|
72
|
+
taskKind: 'fix'
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// ==========================================================
|
|
76
|
+
// Group 1 — Cross-org denial
|
|
77
|
+
// ==========================================================
|
|
78
|
+
|
|
79
|
+
describe('agent permission review v2 — cross-org denial', () => {
|
|
80
|
+
it('allows access when agent org matches repository org', () => {
|
|
81
|
+
const reviewer = createPermissionReviewer();
|
|
82
|
+
const resources = fullyGrantedResources({ organizationRef: 'org-a' });
|
|
83
|
+
const result = reviewer.reviewPermissions({
|
|
84
|
+
...baseInput,
|
|
85
|
+
repository: 'org-a/my-repo',
|
|
86
|
+
resources
|
|
87
|
+
});
|
|
88
|
+
assert.equal(result.decision, 'allowed');
|
|
89
|
+
assert.ok(Array.isArray(result.crossOrgDenials), 'crossOrgDenials should be present');
|
|
90
|
+
assert.equal(result.crossOrgDenials.length, 0, 'no cross-org denials expected');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('denies access and populates crossOrgDenials when agent org differs from repository org', () => {
|
|
94
|
+
const reviewer = createPermissionReviewer();
|
|
95
|
+
// stack is in org-a but repository is in org-b
|
|
96
|
+
const resources = fullyGrantedResources({ organizationRef: 'org-a' });
|
|
97
|
+
const result = reviewer.reviewPermissions({
|
|
98
|
+
...baseInput,
|
|
99
|
+
repository: 'org-b/their-repo',
|
|
100
|
+
resources
|
|
101
|
+
});
|
|
102
|
+
assert.equal(result.decision, 'denied');
|
|
103
|
+
assert.ok(Array.isArray(result.crossOrgDenials), 'crossOrgDenials must be an array');
|
|
104
|
+
assert.ok(result.crossOrgDenials.length > 0, 'should have at least one cross-org denial entry');
|
|
105
|
+
const denial = result.crossOrgDenials[0];
|
|
106
|
+
assert.ok(denial.agentOrg, 'denial should include agentOrg');
|
|
107
|
+
assert.ok(denial.resourceOrg, 'denial should include resourceOrg');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('cross-org denial error is reflected in the reasons array with severity error', () => {
|
|
111
|
+
const reviewer = createPermissionReviewer();
|
|
112
|
+
const resources = fullyGrantedResources({ organizationRef: 'org-a' });
|
|
113
|
+
const result = reviewer.reviewPermissions({
|
|
114
|
+
...baseInput,
|
|
115
|
+
repository: 'org-z/external-repo',
|
|
116
|
+
resources
|
|
117
|
+
});
|
|
118
|
+
assert.ok(
|
|
119
|
+
result.reasons.some((r) => r.severity === 'error' && r.message.toLowerCase().includes('org')),
|
|
120
|
+
'reasons should contain an error mentioning org mismatch'
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ==========================================================
|
|
126
|
+
// Group 2 — Approval mode validation
|
|
127
|
+
// ==========================================================
|
|
128
|
+
|
|
129
|
+
describe('agent permission review v2 — approval mode', () => {
|
|
130
|
+
it("approvalMode 'yolo' auto-approves and decision is allowed when permissions are otherwise valid", () => {
|
|
131
|
+
const reviewer = createPermissionReviewer();
|
|
132
|
+
const resources = fullyGrantedResources({ approvalMode: 'yolo' });
|
|
133
|
+
const result = reviewer.reviewPermissions({
|
|
134
|
+
...baseInput,
|
|
135
|
+
resources
|
|
136
|
+
});
|
|
137
|
+
assert.equal(result.decision, 'allowed');
|
|
138
|
+
assert.equal(result.approvalMode, 'yolo');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("approvalMode 'deny' blocks all requests and returns denied", () => {
|
|
142
|
+
const reviewer = createPermissionReviewer();
|
|
143
|
+
const resources = fullyGrantedResources({ approvalMode: 'deny' });
|
|
144
|
+
const result = reviewer.reviewPermissions({
|
|
145
|
+
...baseInput,
|
|
146
|
+
resources
|
|
147
|
+
});
|
|
148
|
+
assert.equal(result.decision, 'denied');
|
|
149
|
+
assert.equal(result.approvalMode, 'deny');
|
|
150
|
+
assert.ok(
|
|
151
|
+
result.reasons.some((r) => r.severity === 'error' && r.message.toLowerCase().includes('deny')),
|
|
152
|
+
'reason should mention deny mode'
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("approvalMode 'prompt' keeps requires-approval when grants need approval", () => {
|
|
157
|
+
const reviewer = createPermissionReviewer();
|
|
158
|
+
const mcpServer = createResource('AgentMcpServer', { name: 'mcp-prod' }, {
|
|
159
|
+
organizationRef: 'org-a',
|
|
160
|
+
transport: 'stdio',
|
|
161
|
+
scope: 'workspace',
|
|
162
|
+
secretRef: 'prod-secret'
|
|
163
|
+
});
|
|
164
|
+
const stack = makeStack('test-stack', {
|
|
165
|
+
approvalMode: 'prompt',
|
|
166
|
+
mcpServerRefs: ['mcp-prod']
|
|
167
|
+
});
|
|
168
|
+
const mcpGrant = makeSecretGrant('sg-prod', 'sa-agent', 'mcp-server:mcp-prod', {
|
|
169
|
+
requiredApproval: 'always'
|
|
170
|
+
});
|
|
171
|
+
const resources = {
|
|
172
|
+
AgentStack: [stack],
|
|
173
|
+
AgentServiceAccount: [makeServiceAccount('sa-agent')],
|
|
174
|
+
AgentRoleBinding: [makeRoleBinding('rb-1', 'sa-agent')],
|
|
175
|
+
AgentSecretGrant: [mcpGrant, makeSecretGrant('sg-model', 'sa-agent', 'model-provider')],
|
|
176
|
+
AgentConfigGrant: [],
|
|
177
|
+
AgentMcpServer: [mcpServer],
|
|
178
|
+
KradleWorkspacePolicy: []
|
|
179
|
+
};
|
|
180
|
+
const result = reviewer.reviewPermissions({ ...baseInput, resources });
|
|
181
|
+
assert.equal(result.decision, 'requires-approval');
|
|
182
|
+
assert.equal(result.approvalMode, 'prompt');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('invalid approvalMode value causes denied with validation error', () => {
|
|
186
|
+
const reviewer = createPermissionReviewer();
|
|
187
|
+
const resources = fullyGrantedResources({ approvalMode: 'auto-accept-everything' });
|
|
188
|
+
const result = reviewer.reviewPermissions({
|
|
189
|
+
...baseInput,
|
|
190
|
+
resources
|
|
191
|
+
});
|
|
192
|
+
assert.equal(result.decision, 'denied');
|
|
193
|
+
assert.ok(
|
|
194
|
+
result.reasons.some((r) => r.severity === 'error' && r.message.toLowerCase().includes('approvalmode')),
|
|
195
|
+
'reasons should flag invalid approvalMode'
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ==========================================================
|
|
201
|
+
// Group 3 — Workspace policy enforcement
|
|
202
|
+
// ==========================================================
|
|
203
|
+
|
|
204
|
+
describe('agent permission review v2 — workspace policy', () => {
|
|
205
|
+
it('allowed when requested tool is in workspace policy allowedTools', () => {
|
|
206
|
+
const reviewer = createPermissionReviewer();
|
|
207
|
+
const policy = makeWorkspacePolicy('wp-1', { allowedTools: ['bash', 'read_file'] });
|
|
208
|
+
const resources = {
|
|
209
|
+
...fullyGrantedResources(),
|
|
210
|
+
KradleWorkspacePolicy: [policy]
|
|
211
|
+
};
|
|
212
|
+
const result = reviewer.reviewPermissions({
|
|
213
|
+
...baseInput,
|
|
214
|
+
workspacePolicyRef: 'wp-1',
|
|
215
|
+
toolRefs: ['bash'],
|
|
216
|
+
resources
|
|
217
|
+
});
|
|
218
|
+
assert.equal(result.decision, 'allowed');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('denied when requested tool is in workspace policy deniedTools', () => {
|
|
222
|
+
const reviewer = createPermissionReviewer();
|
|
223
|
+
const policy = makeWorkspacePolicy('wp-1', { deniedTools: ['bash'] });
|
|
224
|
+
const resources = {
|
|
225
|
+
...fullyGrantedResources(),
|
|
226
|
+
KradleWorkspacePolicy: [policy]
|
|
227
|
+
};
|
|
228
|
+
const result = reviewer.reviewPermissions({
|
|
229
|
+
...baseInput,
|
|
230
|
+
workspacePolicyRef: 'wp-1',
|
|
231
|
+
toolRefs: ['bash'],
|
|
232
|
+
resources
|
|
233
|
+
});
|
|
234
|
+
assert.equal(result.decision, 'denied');
|
|
235
|
+
assert.ok(
|
|
236
|
+
result.reasons.some((r) => r.severity === 'error' && r.message.toLowerCase().includes('denied')),
|
|
237
|
+
'reason should mention denied tool'
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('denied when maxConcurrentSessions is 0 (no sessions allowed by policy)', () => {
|
|
242
|
+
const reviewer = createPermissionReviewer();
|
|
243
|
+
const policy = makeWorkspacePolicy('wp-strict', { maxConcurrentSessions: 0 });
|
|
244
|
+
const resources = {
|
|
245
|
+
...fullyGrantedResources(),
|
|
246
|
+
KradleWorkspacePolicy: [policy]
|
|
247
|
+
};
|
|
248
|
+
const result = reviewer.reviewPermissions({
|
|
249
|
+
...baseInput,
|
|
250
|
+
workspacePolicyRef: 'wp-strict',
|
|
251
|
+
resources
|
|
252
|
+
});
|
|
253
|
+
assert.equal(result.decision, 'denied');
|
|
254
|
+
assert.ok(
|
|
255
|
+
result.reasons.some((r) => r.severity === 'error' && r.message.toLowerCase().includes('maxconcurrentsessions')),
|
|
256
|
+
'reason should mention maxConcurrentSessions'
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ==========================================================
|
|
262
|
+
// Group 4 — Untrusted fork detection
|
|
263
|
+
// ==========================================================
|
|
264
|
+
|
|
265
|
+
describe('agent permission review v2 — untrusted fork detection', () => {
|
|
266
|
+
it('no untrustedForkWarnings when ref is from the canonical repository', () => {
|
|
267
|
+
const reviewer = createPermissionReviewer();
|
|
268
|
+
const resources = fullyGrantedResources();
|
|
269
|
+
const result = reviewer.reviewPermissions({
|
|
270
|
+
...baseInput,
|
|
271
|
+
repository: 'org-a/my-repo',
|
|
272
|
+
ref: 'refs/heads/main',
|
|
273
|
+
resources
|
|
274
|
+
});
|
|
275
|
+
assert.ok(Array.isArray(result.untrustedForkWarnings), 'untrustedForkWarnings must be present');
|
|
276
|
+
assert.equal(result.untrustedForkWarnings.length, 0);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('untrustedForkWarnings populated when ref indicates a fork (refs/pull/*/head from external fork)', () => {
|
|
280
|
+
const reviewer = createPermissionReviewer();
|
|
281
|
+
// A pull_request ref from a fork is commonly refs/pull/<n>/head where the head sha is from a fork
|
|
282
|
+
const resources = fullyGrantedResources();
|
|
283
|
+
const result = reviewer.reviewPermissions({
|
|
284
|
+
...baseInput,
|
|
285
|
+
repository: 'org-a/my-repo',
|
|
286
|
+
ref: 'refs/pull/42/head',
|
|
287
|
+
isFork: true,
|
|
288
|
+
resources
|
|
289
|
+
});
|
|
290
|
+
assert.ok(Array.isArray(result.untrustedForkWarnings), 'untrustedForkWarnings must be present');
|
|
291
|
+
assert.ok(result.untrustedForkWarnings.length > 0, 'should have at least one fork warning');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('privileged grants (ServiceAccount, Secret) are blocked for untrusted forks', () => {
|
|
295
|
+
const reviewer = createPermissionReviewer();
|
|
296
|
+
const resources = fullyGrantedResources();
|
|
297
|
+
const result = reviewer.reviewPermissions({
|
|
298
|
+
...baseInput,
|
|
299
|
+
repository: 'org-a/my-repo',
|
|
300
|
+
ref: 'refs/pull/99/head',
|
|
301
|
+
isFork: true,
|
|
302
|
+
resources
|
|
303
|
+
});
|
|
304
|
+
// Privileged grants that existed should not be in the approved grants list
|
|
305
|
+
// or the decision should downgrade
|
|
306
|
+
const hasPrivilegedGrant = result.grants.some(
|
|
307
|
+
(g) => (g.kind === 'AgentServiceAccount' || g.kind === 'AgentSecretGrant') && g.status === 'granted'
|
|
308
|
+
);
|
|
309
|
+
// Either the grants are stripped or decision is denied/requires-approval
|
|
310
|
+
const isRestricted = !hasPrivilegedGrant || result.decision === 'denied' || result.decision === 'requires-approval';
|
|
311
|
+
assert.ok(isRestricted, 'privileged grants must not be silently approved for untrusted forks');
|
|
312
|
+
assert.ok(
|
|
313
|
+
result.untrustedForkWarnings.some((w) => w.blockedKinds && w.blockedKinds.length > 0),
|
|
314
|
+
'fork warning should list blocked kinds'
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
@@ -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: 'kradle-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,127 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createAgentPersonaController, createResource } from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
function persona(name = 'aria', spec = {}) {
|
|
6
|
+
return createResource('AgentPersona', { name, namespace: 'kradle-org-default' }, {
|
|
7
|
+
organizationRef: 'default',
|
|
8
|
+
displayName: 'Aria',
|
|
9
|
+
soul: { ref: 'aria-soul' },
|
|
10
|
+
appearance: { ref: 'aria-appearance' },
|
|
11
|
+
voiceProfile: { ref: 'aria-voice' },
|
|
12
|
+
...spec
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function soul(name = 'aria-soul') {
|
|
17
|
+
return createResource('AgentSoul', { name, namespace: 'kradle-org-default' }, {
|
|
18
|
+
organizationRef: 'default',
|
|
19
|
+
personaRef: 'aria',
|
|
20
|
+
content: 'You are Aria.'
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function appearance(name = 'aria-appearance') {
|
|
25
|
+
return createResource('AgentAppearance', { name, namespace: 'kradle-org-default' }, {
|
|
26
|
+
organizationRef: 'default',
|
|
27
|
+
personaRef: 'aria',
|
|
28
|
+
avatar: { type: 'initials', fallbackInitials: 'AR' }
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function voice(name = 'aria-voice') {
|
|
33
|
+
return createResource('AgentVoiceProfile', { name, namespace: 'kradle-org-default' }, {
|
|
34
|
+
organizationRef: 'default',
|
|
35
|
+
personaRef: 'aria',
|
|
36
|
+
ttsProvider: 'openai',
|
|
37
|
+
ttsConfig: { voice: 'nova' }
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function stack(name = 'review-stack') {
|
|
42
|
+
return createResource('AgentStack', { name, namespace: 'kradle-org-default' }, {
|
|
43
|
+
organizationRef: 'default',
|
|
44
|
+
baseAgent: 'claude-code',
|
|
45
|
+
adapter: 'claude-code',
|
|
46
|
+
runtimeIdentity: { serviceAccountRef: 'sa-default' }
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function definition(name = 'aria-reviewer', spec = {}) {
|
|
51
|
+
return createResource('AgentDefinition', { name, namespace: 'kradle-org-default' }, {
|
|
52
|
+
organizationRef: 'default',
|
|
53
|
+
personaRef: 'aria',
|
|
54
|
+
stackRef: 'review-stack',
|
|
55
|
+
roleContext: 'Review pull requests for security issues.',
|
|
56
|
+
...spec
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test('resolveAgentDefinition resolves persona, stack, and referenced soul appearance voice', () => {
|
|
61
|
+
const controller = createAgentPersonaController();
|
|
62
|
+
const resources = {
|
|
63
|
+
AgentDefinition: [definition()],
|
|
64
|
+
AgentPersona: [persona()],
|
|
65
|
+
AgentSoul: [soul()],
|
|
66
|
+
AgentAppearance: [appearance()],
|
|
67
|
+
AgentVoiceProfile: [voice()],
|
|
68
|
+
AgentStack: [stack()]
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const resolved = controller.resolveAgentDefinition('aria-reviewer', { resources, organizationRef: 'default' });
|
|
72
|
+
|
|
73
|
+
assert.equal(resolved.error, false);
|
|
74
|
+
assert.equal(resolved.definition.metadata.name, 'aria-reviewer');
|
|
75
|
+
assert.equal(resolved.persona.metadata.name, 'aria');
|
|
76
|
+
assert.equal(resolved.soul.spec.content, 'You are Aria.');
|
|
77
|
+
assert.equal(resolved.appearance.spec.avatar.fallbackInitials, 'AR');
|
|
78
|
+
assert.equal(resolved.voiceProfile.spec.ttsProvider, 'openai');
|
|
79
|
+
assert.equal(resolved.stack.metadata.name, 'review-stack');
|
|
80
|
+
assert.deepEqual(resolved.errors, []);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('resolveAgentDefinition supports inline soul, appearance, and voice profile data', () => {
|
|
84
|
+
const controller = createAgentPersonaController();
|
|
85
|
+
const resources = {
|
|
86
|
+
AgentDefinition: [definition()],
|
|
87
|
+
AgentPersona: [persona('aria', {
|
|
88
|
+
soul: { inline: 'Inline soul' },
|
|
89
|
+
appearance: { inline: { avatar: { type: 'emoji', emoji: 'A' } } },
|
|
90
|
+
voiceProfile: { inline: { ttsProvider: 'local-piper', ttsConfig: { voice: 'alto' } } }
|
|
91
|
+
})],
|
|
92
|
+
AgentStack: [stack()]
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const resolved = controller.resolveAgentDefinition('aria-reviewer', { resources, organizationRef: 'default' });
|
|
96
|
+
|
|
97
|
+
assert.equal(resolved.error, false);
|
|
98
|
+
assert.equal(resolved.soul.spec.content, 'Inline soul');
|
|
99
|
+
assert.equal(resolved.appearance.spec.avatar.type, 'emoji');
|
|
100
|
+
assert.equal(resolved.voiceProfile.spec.ttsProvider, 'local-piper');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('resolveAgentDefinition reports missing referenced resources', () => {
|
|
104
|
+
const controller = createAgentPersonaController();
|
|
105
|
+
const resources = {
|
|
106
|
+
AgentDefinition: [definition()],
|
|
107
|
+
AgentPersona: [persona()],
|
|
108
|
+
AgentStack: []
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const resolved = controller.resolveAgentDefinition('aria-reviewer', { resources, organizationRef: 'default' });
|
|
112
|
+
|
|
113
|
+
assert.equal(resolved.error, true);
|
|
114
|
+
assert.ok(resolved.errors.some((error) => error.includes('AgentSoul not found: aria-soul')));
|
|
115
|
+
assert.ok(resolved.errors.some((error) => error.includes('AgentAppearance not found: aria-appearance')));
|
|
116
|
+
assert.ok(resolved.errors.some((error) => error.includes('AgentVoiceProfile not found: aria-voice')));
|
|
117
|
+
assert.ok(resolved.errors.some((error) => error.includes('AgentStack not found: review-stack')));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('validateAgentDefinition requires personaRef and stackRef', () => {
|
|
121
|
+
const controller = createAgentPersonaController();
|
|
122
|
+
const result = controller.validateAgentDefinition({ kind: 'AgentDefinition', spec: { organizationRef: 'default' } });
|
|
123
|
+
|
|
124
|
+
assert.equal(result.valid, false);
|
|
125
|
+
assert.ok(result.errors.includes('spec.personaRef is required'));
|
|
126
|
+
assert.ok(result.errors.includes('spec.stackRef is required'));
|
|
127
|
+
});
|