@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
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { clone } from './resource-model.js';
|
|
3
|
+
|
|
4
|
+
export const AGENT_PERMISSION_REVIEW_BOUNDARY = {
|
|
5
|
+
role: 'agent-permission-review',
|
|
6
|
+
scope: 'Deterministic permission review for agent dispatch decisions',
|
|
7
|
+
owns: ['capability expansion', 'grant resolution', 'permission snapshot creation'],
|
|
8
|
+
delegatesTo: ['resource-model'],
|
|
9
|
+
mustNotOwn: ['secret values', 'native K8s API calls', 'runtime execution']
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function createPermissionReviewer(options = {}) {
|
|
13
|
+
return {
|
|
14
|
+
role: 'agent-permission-review',
|
|
15
|
+
|
|
16
|
+
reviewPermissions({ repository, ref, actor, agentStack, triggerSource, taskKind, runnerPool, toolRefs = [], skillRefs = [], mcpServerRefs = [], contextLabelRefs = [], resources = {} }) {
|
|
17
|
+
const reasons = [];
|
|
18
|
+
const grants = [];
|
|
19
|
+
|
|
20
|
+
// Step 1 — Resolve AgentStack
|
|
21
|
+
const stacks = resources.AgentStack || [];
|
|
22
|
+
const stack = stacks.find((s) => s.metadata?.name === agentStack);
|
|
23
|
+
if (!stack) {
|
|
24
|
+
return buildDecision({ decision: 'denied', reasons: [{ severity: 'error', message: `AgentStack not found: ${agentStack}` }], grants, capabilities: {}, actor, repository, ref, agentStack, taskKind });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Step 2 — Expand capabilities from stack spec
|
|
28
|
+
const capabilities = {
|
|
29
|
+
toolRefs: clone(stack.spec?.toolPolicy ? [stack.spec.toolPolicy] : toolRefs),
|
|
30
|
+
mcpServerRefs: clone(stack.spec?.mcpServerRefs || mcpServerRefs),
|
|
31
|
+
skillRefs: clone(stack.spec?.skillRefs || skillRefs),
|
|
32
|
+
subagentRefs: clone(stack.spec?.subagentRefs || [])
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Step 3 — Check runtime identity (AgentServiceAccount)
|
|
36
|
+
const serviceAccountRef = stack.spec?.runtimeIdentity?.serviceAccountRef || stack.spec?.runtimeIdentity;
|
|
37
|
+
const serviceAccounts = resources.AgentServiceAccount || [];
|
|
38
|
+
const serviceAccount = serviceAccounts.find((sa) => sa.metadata?.name === serviceAccountRef);
|
|
39
|
+
if (!serviceAccount) {
|
|
40
|
+
reasons.push({ severity: 'error', message: `Missing AgentServiceAccount: ${serviceAccountRef}` });
|
|
41
|
+
} else {
|
|
42
|
+
grants.push({ kind: 'AgentServiceAccount', name: serviceAccount.metadata.name, status: 'bound' });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Step 4 — Check role bindings
|
|
46
|
+
const roleBindings = resources.AgentRoleBinding || [];
|
|
47
|
+
const matchedBindings = roleBindings.filter((rb) => rb.spec?.subject === serviceAccountRef || rb.spec?.subject === agentStack);
|
|
48
|
+
for (const binding of matchedBindings) {
|
|
49
|
+
grants.push({ kind: 'AgentRoleBinding', name: binding.metadata.name, roleRef: binding.spec?.roleRef, scope: binding.spec?.scope, status: 'bound' });
|
|
50
|
+
}
|
|
51
|
+
if (matchedBindings.length === 0 && serviceAccount) {
|
|
52
|
+
reasons.push({ severity: 'warning', message: `No AgentRoleBinding found for subject: ${serviceAccountRef}` });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Step 5 — Check secret grants
|
|
56
|
+
const secretGrants = resources.AgentSecretGrant || [];
|
|
57
|
+
const neededSecrets = collectSecretNeeds(stack, capabilities, resources);
|
|
58
|
+
for (const need of neededSecrets) {
|
|
59
|
+
const match = secretGrants.find((sg) => {
|
|
60
|
+
if (sg.spec?.subject !== serviceAccountRef && sg.spec?.subject !== agentStack) return false;
|
|
61
|
+
if (need.purpose && sg.spec?.purpose !== need.purpose) return false;
|
|
62
|
+
if (sg.spec?.allowedRepositories && sg.spec.allowedRepositories.length > 0 && !sg.spec.allowedRepositories.includes(repository)) return false;
|
|
63
|
+
if (sg.spec?.allowedRefs && sg.spec.allowedRefs.length > 0 && !sg.spec.allowedRefs.includes(ref)) return false;
|
|
64
|
+
return true;
|
|
65
|
+
});
|
|
66
|
+
if (!match) {
|
|
67
|
+
reasons.push({ severity: 'error', message: `Missing AgentSecretGrant for ${need.description} (purpose: ${need.purpose})` });
|
|
68
|
+
} else {
|
|
69
|
+
const grantEntry = { kind: 'AgentSecretGrant', name: match.metadata.name, purpose: match.spec?.purpose, status: 'granted' };
|
|
70
|
+
if (match.spec?.requiredApproval) {
|
|
71
|
+
grantEntry.status = 'requires-approval';
|
|
72
|
+
grantEntry.requiredApproval = match.spec.requiredApproval;
|
|
73
|
+
reasons.push({ severity: 'info', message: `AgentSecretGrant ${match.metadata.name} requires approval: ${match.spec.requiredApproval}` });
|
|
74
|
+
}
|
|
75
|
+
grants.push(grantEntry);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Step 6 — Check config grants
|
|
80
|
+
const configGrants = resources.AgentConfigGrant || [];
|
|
81
|
+
const neededConfigs = collectConfigNeeds(stack, capabilities, resources);
|
|
82
|
+
for (const need of neededConfigs) {
|
|
83
|
+
const match = configGrants.find((cg) => {
|
|
84
|
+
if (cg.spec?.subject !== serviceAccountRef && cg.spec?.subject !== agentStack) return false;
|
|
85
|
+
if (need.purpose && cg.spec?.purpose !== need.purpose) return false;
|
|
86
|
+
return true;
|
|
87
|
+
});
|
|
88
|
+
if (!match) {
|
|
89
|
+
reasons.push({ severity: 'error', message: `Missing AgentConfigGrant for ${need.description} (purpose: ${need.purpose})` });
|
|
90
|
+
} else {
|
|
91
|
+
grants.push({ kind: 'AgentConfigGrant', name: match.metadata.name, purpose: match.spec?.purpose, status: 'granted' });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Step 7 — Decision
|
|
96
|
+
const hasErrors = reasons.some((r) => r.severity === 'error');
|
|
97
|
+
const hasApprovals = grants.some((g) => g.status === 'requires-approval');
|
|
98
|
+
const decision = hasErrors ? 'denied' : hasApprovals ? 'requires-approval' : 'allowed';
|
|
99
|
+
|
|
100
|
+
return buildDecision({ decision, reasons, grants, capabilities, actor, repository, ref, agentStack, taskKind });
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
createPermissionSnapshot(reviewResult) {
|
|
104
|
+
const snapshot = clone(reviewResult);
|
|
105
|
+
snapshot.snapshotAt = new Date().toISOString();
|
|
106
|
+
snapshot.frozen = true;
|
|
107
|
+
snapshot.digest = reviewResult.digest;
|
|
108
|
+
return Object.freeze(snapshot);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function collectSecretNeeds(stack, capabilities, resources) {
|
|
114
|
+
const needs = [];
|
|
115
|
+
if (stack.spec?.adapter) {
|
|
116
|
+
needs.push({ description: `model provider for adapter ${stack.spec.adapter}`, purpose: 'model-provider' });
|
|
117
|
+
}
|
|
118
|
+
const mcpServers = resources.AgentMcpServer || [];
|
|
119
|
+
for (const ref of capabilities.mcpServerRefs) {
|
|
120
|
+
const server = mcpServers.find((s) => s.metadata?.name === ref);
|
|
121
|
+
if (server?.spec?.secretRef) {
|
|
122
|
+
needs.push({ description: `MCP server ${ref} secret`, purpose: `mcp-server:${ref}` });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return needs;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function collectConfigNeeds(stack, capabilities, resources) {
|
|
129
|
+
const needs = [];
|
|
130
|
+
const mcpServers = resources.AgentMcpServer || [];
|
|
131
|
+
for (const ref of capabilities.mcpServerRefs) {
|
|
132
|
+
const server = mcpServers.find((s) => s.metadata?.name === ref);
|
|
133
|
+
if (server?.spec?.configMapRef) {
|
|
134
|
+
needs.push({ description: `MCP server ${ref} config`, purpose: `mcp-server:${ref}` });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return needs;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function buildDecision({ decision, reasons, grants, capabilities, actor, repository, ref, agentStack, taskKind }) {
|
|
141
|
+
const result = {
|
|
142
|
+
decision,
|
|
143
|
+
actor,
|
|
144
|
+
repository,
|
|
145
|
+
ref,
|
|
146
|
+
agentStack,
|
|
147
|
+
taskKind,
|
|
148
|
+
capabilities: clone(capabilities),
|
|
149
|
+
grants: clone(grants),
|
|
150
|
+
reasons: clone(reasons),
|
|
151
|
+
reviewedAt: new Date().toISOString()
|
|
152
|
+
};
|
|
153
|
+
result.digest = computeDigest(result);
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function computeDigest(result) {
|
|
158
|
+
const keys = Object.keys(result).filter((k) => k !== 'digest').sort();
|
|
159
|
+
const canonical = {};
|
|
160
|
+
for (const key of keys) canonical[key] = result[key];
|
|
161
|
+
return createHash('sha256').update(JSON.stringify(canonical)).digest('hex');
|
|
162
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { createPermissionReviewer } from './agent-permission-review.js';
|
|
2
|
+
import { clone } from './resource-model.js';
|
|
3
|
+
|
|
4
|
+
export const AGENT_STACK_CONTROLLER_BOUNDARY = {
|
|
5
|
+
role: 'agent-stack-controller',
|
|
6
|
+
scope: 'Stack readiness reconciliation with capability resolution and condition management',
|
|
7
|
+
owns: ['capability resolution', 'stack conditions', 'readiness computation'],
|
|
8
|
+
delegatesTo: ['agent-permission-review', 'resource-model'],
|
|
9
|
+
mustNotOwn: ['secret values', 'dispatch execution', 'Agent Mux sessions']
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function createAgentStackController(options = {}) {
|
|
13
|
+
const permissionReviewer = options.permissionReviewer || createPermissionReviewer();
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
role: 'agent-stack-controller',
|
|
17
|
+
|
|
18
|
+
reconcileStack(stack, resources = {}) {
|
|
19
|
+
const spec = stack?.spec || {};
|
|
20
|
+
const conditions = [];
|
|
21
|
+
const missing = [];
|
|
22
|
+
|
|
23
|
+
// --- Resolve capability refs from stack spec ---
|
|
24
|
+
const resolvedTools = [];
|
|
25
|
+
const resolvedMcpServers = [];
|
|
26
|
+
const resolvedSkills = [];
|
|
27
|
+
const resolvedSubagents = [];
|
|
28
|
+
const resolvedContextLabels = [];
|
|
29
|
+
|
|
30
|
+
// toolPolicyRef
|
|
31
|
+
const toolPolicyRef = spec.toolPolicy || spec.toolPolicyRef || null;
|
|
32
|
+
let toolPolicyFound = true;
|
|
33
|
+
if (toolPolicyRef) {
|
|
34
|
+
const profiles = resources.AgentToolProfile || [];
|
|
35
|
+
const profile = profiles.find((p) => p.metadata?.name === toolPolicyRef);
|
|
36
|
+
if (profile) {
|
|
37
|
+
resolvedTools.push(profile.metadata.name);
|
|
38
|
+
} else {
|
|
39
|
+
toolPolicyFound = false;
|
|
40
|
+
missing.push(`AgentToolProfile/${toolPolicyRef}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// mcpServerRefs
|
|
45
|
+
const mcpServerRefs = spec.mcpServerRefs || [];
|
|
46
|
+
let allMcpFound = true;
|
|
47
|
+
for (const ref of mcpServerRefs) {
|
|
48
|
+
const servers = resources.AgentMcpServer || [];
|
|
49
|
+
const server = servers.find((s) => s.metadata?.name === ref);
|
|
50
|
+
if (server) {
|
|
51
|
+
resolvedMcpServers.push(server.metadata.name);
|
|
52
|
+
} else {
|
|
53
|
+
allMcpFound = false;
|
|
54
|
+
missing.push(`AgentMcpServer/${ref}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// skillRefs
|
|
59
|
+
const skillRefs = spec.skillRefs || [];
|
|
60
|
+
let allSkillsFound = true;
|
|
61
|
+
let allSkillsValid = true;
|
|
62
|
+
for (const ref of skillRefs) {
|
|
63
|
+
const skills = resources.AgentSkill || [];
|
|
64
|
+
const skill = skills.find((s) => s.metadata?.name === ref);
|
|
65
|
+
if (skill) {
|
|
66
|
+
resolvedSkills.push(skill.metadata.name);
|
|
67
|
+
if (!skill.spec?.format || !skill.spec?.sourceRef) {
|
|
68
|
+
allSkillsValid = false;
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
allSkillsFound = false;
|
|
72
|
+
allSkillsValid = false;
|
|
73
|
+
missing.push(`AgentSkill/${ref}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// subagentRefs
|
|
78
|
+
const subagentRefs = spec.subagentRefs || [];
|
|
79
|
+
let allSubagentsFound = true;
|
|
80
|
+
let allSubagentsValid = true;
|
|
81
|
+
for (const ref of subagentRefs) {
|
|
82
|
+
const subagents = resources.AgentSubagent || [];
|
|
83
|
+
const subagent = subagents.find((s) => s.metadata?.name === ref);
|
|
84
|
+
if (subagent) {
|
|
85
|
+
resolvedSubagents.push(subagent.metadata.name);
|
|
86
|
+
if (!subagent.spec?.taskKinds || subagent.spec.taskKinds.length === 0) {
|
|
87
|
+
allSubagentsValid = false;
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
allSubagentsFound = false;
|
|
91
|
+
allSubagentsValid = false;
|
|
92
|
+
missing.push(`AgentSubagent/${ref}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// contextLabelRefs
|
|
97
|
+
const contextLabelRefs = spec.contextLabelRefs || [];
|
|
98
|
+
let allContextLabelsFound = true;
|
|
99
|
+
for (const ref of contextLabelRefs) {
|
|
100
|
+
const labels = resources.AgentContextLabel || [];
|
|
101
|
+
const label = labels.find((l) => l.metadata?.name === ref);
|
|
102
|
+
if (label) {
|
|
103
|
+
resolvedContextLabels.push(label.metadata.name);
|
|
104
|
+
} else {
|
|
105
|
+
allContextLabelsFound = false;
|
|
106
|
+
missing.push(`AgentContextLabel/${ref}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- Build conditions ---
|
|
111
|
+
const allRefsFound = missing.length === 0;
|
|
112
|
+
conditions.push({
|
|
113
|
+
type: 'CapabilitiesResolved',
|
|
114
|
+
status: allRefsFound ? 'True' : 'False',
|
|
115
|
+
reason: allRefsFound ? 'AllRefsResolved' : 'MissingRefs',
|
|
116
|
+
message: allRefsFound ? 'All capability references resolved' : `Missing: ${missing.join(', ')}`
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
conditions.push({
|
|
120
|
+
type: 'ToolsAdmitted',
|
|
121
|
+
status: (toolPolicyFound || !toolPolicyRef) ? 'True' : 'False',
|
|
122
|
+
reason: !toolPolicyRef ? 'NoToolPolicyRef' : toolPolicyFound ? 'ToolPolicyResolved' : 'ToolPolicyMissing',
|
|
123
|
+
message: !toolPolicyRef ? 'No tool policy reference set' : toolPolicyFound ? 'Tool policy resolved' : `AgentToolProfile/${toolPolicyRef} not found`
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
conditions.push({
|
|
127
|
+
type: 'McpHealthy',
|
|
128
|
+
status: allMcpFound ? 'True' : 'False',
|
|
129
|
+
reason: allMcpFound ? 'AllMcpServersExist' : 'MissingMcpServers',
|
|
130
|
+
message: allMcpFound ? 'All MCP servers exist (health check deferred)' : `Missing MCP servers: ${mcpServerRefs.filter((ref) => !resolvedMcpServers.includes(ref)).join(', ')}`
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
conditions.push({
|
|
134
|
+
type: 'SkillsValidated',
|
|
135
|
+
status: (allSkillsFound && allSkillsValid) ? 'True' : 'False',
|
|
136
|
+
reason: !allSkillsFound ? 'MissingSkills' : !allSkillsValid ? 'InvalidSkillFormat' : 'AllSkillsValid',
|
|
137
|
+
message: !allSkillsFound ? `Missing skills: ${skillRefs.filter((ref) => !resolvedSkills.includes(ref)).join(', ')}` : !allSkillsValid ? 'Some skills have invalid format or missing sourceRef' : 'All skills validated'
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
conditions.push({
|
|
141
|
+
type: 'SubagentsValid',
|
|
142
|
+
status: (allSubagentsFound && allSubagentsValid) ? 'True' : 'False',
|
|
143
|
+
reason: !allSubagentsFound ? 'MissingSubagents' : !allSubagentsValid ? 'InvalidSubagentTaskKinds' : 'AllSubagentsValid',
|
|
144
|
+
message: !allSubagentsFound ? `Missing subagents: ${subagentRefs.filter((ref) => !resolvedSubagents.includes(ref)).join(', ')}` : !allSubagentsValid ? 'Some subagents have invalid or empty taskKinds' : 'All subagents validated'
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
conditions.push({
|
|
148
|
+
type: 'ContextLabelsValid',
|
|
149
|
+
status: allContextLabelsFound ? 'True' : 'False',
|
|
150
|
+
reason: allContextLabelsFound ? 'AllContextLabelsExist' : 'MissingContextLabels',
|
|
151
|
+
message: allContextLabelsFound ? 'All context labels exist' : `Missing context labels: ${contextLabelRefs.filter((ref) => !resolvedContextLabels.includes(ref)).join(', ')}`
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// --- Permission review conditions via permissionReviewer ---
|
|
155
|
+
const serviceAccountRef = spec.runtimeIdentity?.serviceAccountRef || spec.runtimeIdentity;
|
|
156
|
+
const serviceAccounts = resources.AgentServiceAccount || [];
|
|
157
|
+
const serviceAccount = serviceAccounts.find((sa) => sa.metadata?.name === serviceAccountRef);
|
|
158
|
+
const runtimeIdentityReady = Boolean(serviceAccount);
|
|
159
|
+
|
|
160
|
+
conditions.push({
|
|
161
|
+
type: 'RuntimeIdentityReady',
|
|
162
|
+
status: runtimeIdentityReady ? 'True' : 'False',
|
|
163
|
+
reason: runtimeIdentityReady ? 'ServiceAccountBound' : 'MissingServiceAccount',
|
|
164
|
+
message: runtimeIdentityReady ? `AgentServiceAccount ${serviceAccountRef} bound` : `AgentServiceAccount ${serviceAccountRef || 'undefined'} not found`
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Run permission review for roles, secrets, config
|
|
168
|
+
const permissionReview = permissionReviewer.reviewPermissions({
|
|
169
|
+
repository: stack?.metadata?.labels?.repository || 'unknown',
|
|
170
|
+
ref: stack?.metadata?.labels?.ref || 'main',
|
|
171
|
+
actor: stack?.metadata?.labels?.actor || 'system',
|
|
172
|
+
agentStack: stack?.metadata?.name,
|
|
173
|
+
triggerSource: 'reconciliation',
|
|
174
|
+
taskKind: spec.taskKind || 'general',
|
|
175
|
+
resources
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const rolesAdmitted = !permissionReview.reasons.some((r) => r.severity === 'error' && r.message.includes('AgentRoleBinding'));
|
|
179
|
+
const secretsAdmitted = !permissionReview.reasons.some((r) => r.severity === 'error' && r.message.includes('AgentSecretGrant'));
|
|
180
|
+
const configAdmitted = !permissionReview.reasons.some((r) => r.severity === 'error' && r.message.includes('AgentConfigGrant'));
|
|
181
|
+
|
|
182
|
+
conditions.push({
|
|
183
|
+
type: 'RolesAdmitted',
|
|
184
|
+
status: rolesAdmitted ? 'True' : 'False',
|
|
185
|
+
reason: rolesAdmitted ? 'RoleBindingsResolved' : 'MissingRoleBindings',
|
|
186
|
+
message: rolesAdmitted ? 'Role bindings satisfied' : 'Missing required AgentRoleBinding resources'
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
conditions.push({
|
|
190
|
+
type: 'SecretsAdmitted',
|
|
191
|
+
status: secretsAdmitted ? 'True' : 'False',
|
|
192
|
+
reason: secretsAdmitted ? 'SecretGrantsResolved' : 'MissingSecretGrants',
|
|
193
|
+
message: secretsAdmitted ? 'Secret grants satisfied' : 'Missing required AgentSecretGrant resources'
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
conditions.push({
|
|
197
|
+
type: 'ConfigAdmitted',
|
|
198
|
+
status: configAdmitted ? 'True' : 'False',
|
|
199
|
+
reason: configAdmitted ? 'ConfigGrantsResolved' : 'MissingConfigGrants',
|
|
200
|
+
message: configAdmitted ? 'Config grants satisfied' : 'Missing required AgentConfigGrant resources'
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// --- Ready condition: true only if ALL other conditions are true ---
|
|
204
|
+
const allTrue = conditions.every((c) => c.status === 'True');
|
|
205
|
+
const hasErrors = conditions.some((c) => c.status === 'False');
|
|
206
|
+
|
|
207
|
+
conditions.push({
|
|
208
|
+
type: 'Ready',
|
|
209
|
+
status: allTrue ? 'True' : 'False',
|
|
210
|
+
reason: allTrue ? 'StackReady' : 'StackNotReady',
|
|
211
|
+
message: allTrue ? 'All conditions met' : `Failing conditions: ${conditions.filter((c) => c.status === 'False').map((c) => c.type).join(', ')}`
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
conditions: clone(conditions),
|
|
216
|
+
capabilities: {
|
|
217
|
+
tools: clone(resolvedTools),
|
|
218
|
+
mcpServers: clone(resolvedMcpServers),
|
|
219
|
+
skills: clone(resolvedSkills),
|
|
220
|
+
subagents: clone(resolvedSubagents),
|
|
221
|
+
contextLabels: clone(resolvedContextLabels)
|
|
222
|
+
},
|
|
223
|
+
validation: allTrue ? 'valid' : hasErrors ? 'invalid' : 'warning',
|
|
224
|
+
permissionDecision: permissionReview.decision
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
listStackCapabilities(stack, resources = {}) {
|
|
229
|
+
const spec = stack?.spec || {};
|
|
230
|
+
const capabilities = [];
|
|
231
|
+
|
|
232
|
+
// Tools
|
|
233
|
+
const toolPolicyRef = spec.toolPolicy || spec.toolPolicyRef || null;
|
|
234
|
+
if (toolPolicyRef) {
|
|
235
|
+
const profiles = resources.AgentToolProfile || [];
|
|
236
|
+
const profile = profiles.find((p) => p.metadata?.name === toolPolicyRef);
|
|
237
|
+
capabilities.push({
|
|
238
|
+
kind: 'tool',
|
|
239
|
+
name: toolPolicyRef,
|
|
240
|
+
status: profile ? 'resolved' : 'missing',
|
|
241
|
+
ref: toolPolicyRef
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// MCP Servers
|
|
246
|
+
for (const ref of spec.mcpServerRefs || []) {
|
|
247
|
+
const servers = resources.AgentMcpServer || [];
|
|
248
|
+
const server = servers.find((s) => s.metadata?.name === ref);
|
|
249
|
+
capabilities.push({
|
|
250
|
+
kind: 'mcp',
|
|
251
|
+
name: ref,
|
|
252
|
+
status: server ? 'resolved' : 'missing',
|
|
253
|
+
ref
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Skills
|
|
258
|
+
for (const ref of spec.skillRefs || []) {
|
|
259
|
+
const skills = resources.AgentSkill || [];
|
|
260
|
+
const skill = skills.find((s) => s.metadata?.name === ref);
|
|
261
|
+
capabilities.push({
|
|
262
|
+
kind: 'skill',
|
|
263
|
+
name: ref,
|
|
264
|
+
status: skill ? 'resolved' : 'missing',
|
|
265
|
+
ref
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Subagents
|
|
270
|
+
for (const ref of spec.subagentRefs || []) {
|
|
271
|
+
const subagents = resources.AgentSubagent || [];
|
|
272
|
+
const subagent = subagents.find((s) => s.metadata?.name === ref);
|
|
273
|
+
capabilities.push({
|
|
274
|
+
kind: 'subagent',
|
|
275
|
+
name: ref,
|
|
276
|
+
status: subagent ? 'resolved' : 'missing',
|
|
277
|
+
ref
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Context Labels
|
|
282
|
+
for (const ref of spec.contextLabelRefs || []) {
|
|
283
|
+
const labels = resources.AgentContextLabel || [];
|
|
284
|
+
const label = labels.find((l) => l.metadata?.name === ref);
|
|
285
|
+
capabilities.push({
|
|
286
|
+
kind: 'contextLabel',
|
|
287
|
+
name: ref,
|
|
288
|
+
status: label ? 'resolved' : 'missing',
|
|
289
|
+
ref
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return capabilities;
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createResource, clone } from './resource-model.js';
|
|
2
|
+
|
|
3
|
+
export const AGENT_TRIGGER_CONTROLLER_BOUNDARY = {
|
|
4
|
+
role: 'agent-trigger-controller',
|
|
5
|
+
scope: 'Event normalization, rule matching, deduplication, and dispatch creation',
|
|
6
|
+
owns: ['event normalization', 'rule matching', 'trigger execution records', 'dispatch initiation'],
|
|
7
|
+
delegatesTo: ['agent-dispatch-controller', 'resource-model'],
|
|
8
|
+
mustNotOwn: ['event sourcing', 'webhook delivery', 'secret values']
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function createAgentTriggerController(options = {}) {
|
|
12
|
+
const dispatchController = options.dispatchController;
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
role: 'agent-trigger-controller',
|
|
16
|
+
|
|
17
|
+
matchRule(rule, event) {
|
|
18
|
+
// 1. Check event type is in rule.spec.sources
|
|
19
|
+
const sources = rule.spec?.sources || [];
|
|
20
|
+
if (!sources.includes(event.type)) return { matches: false, reason: `Event type '${event.type}' not in rule sources [${sources.join(', ')}]` };
|
|
21
|
+
// 2. Check repository scope (if rule has spec.repository, must match)
|
|
22
|
+
if (rule.spec?.repository && rule.spec.repository !== event.repository) return { matches: false, reason: `Repository '${event.repository}' does not match rule scope '${rule.spec.repository}'` };
|
|
23
|
+
// 3. Check actor filter (if rule has spec.allowedActors)
|
|
24
|
+
if (rule.spec?.allowedActors?.length > 0 && !rule.spec.allowedActors.includes(event.actor)) return { matches: false, reason: `Actor '${event.actor}' not in allowed actors` };
|
|
25
|
+
return { matches: true, reason: 'All conditions met' };
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
evaluateEvent({ event, resources }) {
|
|
29
|
+
const rules = resources.AgentTriggerRule || [];
|
|
30
|
+
const executions = resources.AgentTriggerExecution || [];
|
|
31
|
+
const eventUid = `${event.type}:${event.source?.kind}:${event.source?.name}`;
|
|
32
|
+
|
|
33
|
+
return rules.map(rule => {
|
|
34
|
+
const match = this.matchRule(rule, event);
|
|
35
|
+
const isDuplicate = executions.some(ex =>
|
|
36
|
+
ex.spec?.triggerRule === rule.metadata?.name &&
|
|
37
|
+
ex.spec?.sourceEvent === eventUid &&
|
|
38
|
+
ex.status?.phase !== 'Failed'
|
|
39
|
+
);
|
|
40
|
+
return { rule, matches: match.matches, reason: match.reason, isDuplicate };
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
createTriggerExecution({ rule, event, decision, reason, namespace = 'default', organizationRef = 'default' }) {
|
|
45
|
+
const eventUid = `${event.type}:${event.source?.kind}:${event.source?.name}`;
|
|
46
|
+
const name = `trigger-exec-${rule.metadata?.name}-${Date.now()}`;
|
|
47
|
+
const execution = createResource('AgentTriggerExecution', { name, namespace }, {
|
|
48
|
+
organizationRef,
|
|
49
|
+
triggerRule: rule.metadata?.name,
|
|
50
|
+
sourceEvent: eventUid,
|
|
51
|
+
decision,
|
|
52
|
+
});
|
|
53
|
+
execution.status = { phase: decision, reason, evaluatedAt: new Date().toISOString() };
|
|
54
|
+
return execution;
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async processEvent({ event, resources, namespace = 'default', organizationRef = 'default' }) {
|
|
58
|
+
const evaluations = this.evaluateEvent({ event, resources });
|
|
59
|
+
const executions = [];
|
|
60
|
+
let dispatched = 0;
|
|
61
|
+
let skipped = 0;
|
|
62
|
+
|
|
63
|
+
for (const { rule, matches, reason, isDuplicate } of evaluations) {
|
|
64
|
+
if (!matches) {
|
|
65
|
+
executions.push(this.createTriggerExecution({ rule, event, decision: 'Skipped', reason, namespace, organizationRef }));
|
|
66
|
+
skipped++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (isDuplicate) {
|
|
70
|
+
executions.push(this.createTriggerExecution({ rule, event, decision: 'Deduplicated', reason: 'Already dispatched for this event', namespace, organizationRef }));
|
|
71
|
+
skipped++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const execution = this.createTriggerExecution({ rule, event, decision: 'Dispatching', reason, namespace, organizationRef });
|
|
76
|
+
|
|
77
|
+
if (dispatchController) {
|
|
78
|
+
const result = await dispatchController.createManualDispatch({
|
|
79
|
+
repository: event.repository,
|
|
80
|
+
ref: event.ref,
|
|
81
|
+
sourceRefs: [event.source],
|
|
82
|
+
agentStack: rule.spec?.agentStack,
|
|
83
|
+
taskKind: rule.spec?.taskKind || 'diagnostic',
|
|
84
|
+
actor: event.actor,
|
|
85
|
+
namespace,
|
|
86
|
+
organizationRef,
|
|
87
|
+
resources,
|
|
88
|
+
});
|
|
89
|
+
if (result.error) {
|
|
90
|
+
execution.status.phase = 'Failed';
|
|
91
|
+
execution.status.reason = result.message;
|
|
92
|
+
} else {
|
|
93
|
+
execution.status.phase = 'Dispatched';
|
|
94
|
+
execution.status.dispatchRunRef = result.run?.metadata?.name;
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
execution.status.phase = 'Dispatched';
|
|
98
|
+
execution.status.reason = 'No dispatch controller configured (dry-run)';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
executions.push(execution);
|
|
102
|
+
dispatched++;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { processed: evaluations.length, dispatched, skipped, executions };
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|