@a5c-ai/krate 5.0.1-staging.00fa5317c
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 +3205 -0
- package/dist/krate-lifecycle.json +201 -0
- package/dist/krate-runtime-snapshot.json +3125 -0
- package/dist/krate-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-krate-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/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/openapi.yaml +1275 -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/todos.md +4 -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 +278 -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 +209 -0
- package/src/agent-gateway-config-controller.js +147 -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 +280 -0
- package/src/agent-permission-review.js +250 -0
- package/src/agent-project-controller.js +117 -0
- package/src/agent-provider-config-controller.js +150 -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 +347 -0
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +381 -0
- package/src/agent-workspace-controller.js +702 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +541 -0
- package/src/argocd-gitops.js +43 -0
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +307 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +72 -0
- package/src/controller-ui.js +617 -0
- package/src/data-plane.js +179 -0
- package/src/event-bus.js +61 -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 +161 -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/hooks-events.js +63 -0
- package/src/http-server.js +377 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +57 -0
- package/src/kubernetes-controller-async.js +511 -0
- package/src/kubernetes-controller.js +878 -0
- package/src/kubernetes-resource-gateway.js +48 -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 +221 -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/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 +315 -0
- package/tests/agent-gateway-config-controller.test.js +386 -0
- package/tests/agent-memory-controller.test.js +308 -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 +204 -0
- package/tests/agent-permission-review-v2.test.js +317 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-project-controller.test.js +302 -0
- package/tests/agent-provider-config-controller.test.js +376 -0
- package/tests/agent-resources.test.js +228 -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 +221 -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 +211 -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/async-controller.test.js +252 -0
- package/tests/audit-controller.test.js +227 -0
- package/tests/codespace-controller.test.js +318 -0
- package/tests/deployment.test.js +407 -0
- package/tests/e2e/lifecycle.test.js +117 -0
- package/tests/event-bus-integration.test.js +190 -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 +340 -0
- package/tests/external-provider-adapter.test.js +365 -0
- package/tests/external-resource-model.test.js +215 -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/integration/full-flow.test.js +266 -0
- package/tests/krate.test.js +756 -0
- package/tests/memory-search-wiring.test.js +270 -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 +247 -0
- package/tests/sse-events.test.js +107 -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,250 @@
|
|
|
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
|
+
const VALID_APPROVAL_MODES = new Set(['yolo', 'prompt', 'deny']);
|
|
13
|
+
|
|
14
|
+
export function createPermissionReviewer(options = {}) {
|
|
15
|
+
return {
|
|
16
|
+
role: 'agent-permission-review',
|
|
17
|
+
|
|
18
|
+
reviewPermissions({ repository, ref, actor, agentStack, triggerSource, taskKind, runnerPool, toolRefs = [], skillRefs = [], mcpServerRefs = [], contextLabelRefs = [], workspacePolicyRef, isFork = false, resources = {} }) {
|
|
19
|
+
const reasons = [];
|
|
20
|
+
const grants = [];
|
|
21
|
+
const crossOrgDenials = [];
|
|
22
|
+
const untrustedForkWarnings = [];
|
|
23
|
+
|
|
24
|
+
// Step 1 — Resolve AgentStack
|
|
25
|
+
const stacks = resources.AgentStack || [];
|
|
26
|
+
const stack = stacks.find((s) => s.metadata?.name === agentStack);
|
|
27
|
+
if (!stack) {
|
|
28
|
+
return buildDecision({ decision: 'denied', reasons: [{ severity: 'error', message: `AgentStack not found: ${agentStack}` }], grants, capabilities: {}, actor, repository, ref, agentStack, taskKind, crossOrgDenials, untrustedForkWarnings });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Step 1a — Validate approvalMode
|
|
32
|
+
const approvalMode = stack.spec?.approvalMode;
|
|
33
|
+
if (approvalMode !== undefined && !VALID_APPROVAL_MODES.has(approvalMode)) {
|
|
34
|
+
reasons.push({ severity: 'error', message: `Invalid approvalMode '${approvalMode}': must be one of ${[...VALID_APPROVAL_MODES].join(', ')}` });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Step 1b — Deny mode blocks everything immediately
|
|
38
|
+
if (approvalMode === 'deny') {
|
|
39
|
+
reasons.push({ severity: 'error', message: `approvalMode is 'deny': all requests are blocked by policy` });
|
|
40
|
+
return buildDecision({ decision: 'denied', reasons, grants, capabilities: {}, actor, repository, ref, agentStack, taskKind, approvalMode, crossOrgDenials, untrustedForkWarnings });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Step 2 — Cross-org denial check
|
|
44
|
+
const agentOrg = stack.spec?.organizationRef;
|
|
45
|
+
if (agentOrg && repository) {
|
|
46
|
+
// repository format: '<org>/<repo>' or just '<repo>'
|
|
47
|
+
const repoParts = repository.split('/');
|
|
48
|
+
const repoOrg = repoParts.length >= 2 ? repoParts[0] : null;
|
|
49
|
+
if (repoOrg && repoOrg !== agentOrg) {
|
|
50
|
+
crossOrgDenials.push({ agentOrg, resourceOrg: repoOrg, resource: repository });
|
|
51
|
+
reasons.push({ severity: 'error', message: `Cross-org access denied: agent org '${agentOrg}' cannot access repository in org '${repoOrg}'` });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Step 3 — Expand capabilities from stack spec
|
|
56
|
+
const capabilities = {
|
|
57
|
+
toolRefs: clone(stack.spec?.toolPolicy ? [stack.spec.toolPolicy] : toolRefs),
|
|
58
|
+
mcpServerRefs: clone(stack.spec?.mcpServerRefs || mcpServerRefs),
|
|
59
|
+
skillRefs: clone(stack.spec?.skillRefs || skillRefs),
|
|
60
|
+
subagentRefs: clone(stack.spec?.subagentRefs || [])
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Step 4 — Untrusted fork detection
|
|
64
|
+
const isForkRef = isFork || /^refs\/pull\/\d+\//.test(ref);
|
|
65
|
+
if (isForkRef) {
|
|
66
|
+
const blockedKinds = ['AgentServiceAccount', 'AgentSecretGrant'];
|
|
67
|
+
untrustedForkWarnings.push({
|
|
68
|
+
ref,
|
|
69
|
+
isFork: true,
|
|
70
|
+
blockedKinds,
|
|
71
|
+
message: `Untrusted fork detected for ref '${ref}': privileged grants restricted`
|
|
72
|
+
});
|
|
73
|
+
reasons.push({ severity: 'warning', message: `Untrusted fork detected for ref '${ref}': privileged grants (${blockedKinds.join(', ')}) are not auto-approved` });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Step 5 — Check runtime identity (AgentServiceAccount)
|
|
77
|
+
const serviceAccountRef = stack.spec?.runtimeIdentity?.serviceAccountRef || stack.spec?.runtimeIdentity;
|
|
78
|
+
const serviceAccounts = resources.AgentServiceAccount || [];
|
|
79
|
+
const serviceAccount = serviceAccounts.find((sa) => sa.metadata?.name === serviceAccountRef);
|
|
80
|
+
if (!serviceAccount) {
|
|
81
|
+
reasons.push({ severity: 'error', message: `Missing AgentServiceAccount: ${serviceAccountRef}` });
|
|
82
|
+
} else {
|
|
83
|
+
const saGrant = { kind: 'AgentServiceAccount', name: serviceAccount.metadata.name, status: 'bound' };
|
|
84
|
+
if (isForkRef) {
|
|
85
|
+
saGrant.status = 'fork-restricted';
|
|
86
|
+
}
|
|
87
|
+
grants.push(saGrant);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Step 6 — Check role bindings
|
|
91
|
+
const roleBindings = resources.AgentRoleBinding || [];
|
|
92
|
+
const matchedBindings = roleBindings.filter((rb) => rb.spec?.subject === serviceAccountRef || rb.spec?.subject === agentStack);
|
|
93
|
+
for (const binding of matchedBindings) {
|
|
94
|
+
grants.push({ kind: 'AgentRoleBinding', name: binding.metadata.name, roleRef: binding.spec?.roleRef, scope: binding.spec?.scope, status: 'bound' });
|
|
95
|
+
}
|
|
96
|
+
if (matchedBindings.length === 0 && serviceAccount) {
|
|
97
|
+
reasons.push({ severity: 'warning', message: `No AgentRoleBinding found for subject: ${serviceAccountRef}` });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Step 7 — Check secret grants
|
|
101
|
+
const secretGrants = resources.AgentSecretGrant || [];
|
|
102
|
+
const neededSecrets = collectSecretNeeds(stack, capabilities, resources);
|
|
103
|
+
for (const need of neededSecrets) {
|
|
104
|
+
const match = secretGrants.find((sg) => {
|
|
105
|
+
if (sg.spec?.subject !== serviceAccountRef && sg.spec?.subject !== agentStack) return false;
|
|
106
|
+
if (need.purpose && sg.spec?.purpose !== need.purpose) return false;
|
|
107
|
+
if (sg.spec?.allowedRepositories && sg.spec.allowedRepositories.length > 0 && !sg.spec.allowedRepositories.includes(repository)) return false;
|
|
108
|
+
if (sg.spec?.allowedRefs && sg.spec.allowedRefs.length > 0 && !sg.spec.allowedRefs.includes(ref)) return false;
|
|
109
|
+
return true;
|
|
110
|
+
});
|
|
111
|
+
if (!match) {
|
|
112
|
+
reasons.push({ severity: 'error', message: `Missing AgentSecretGrant for ${need.description} (purpose: ${need.purpose})` });
|
|
113
|
+
} else {
|
|
114
|
+
const grantEntry = { kind: 'AgentSecretGrant', name: match.metadata.name, purpose: match.spec?.purpose, status: 'granted' };
|
|
115
|
+
if (isForkRef) {
|
|
116
|
+
grantEntry.status = 'fork-restricted';
|
|
117
|
+
} else if (match.spec?.requiredApproval) {
|
|
118
|
+
grantEntry.status = 'requires-approval';
|
|
119
|
+
grantEntry.requiredApproval = match.spec.requiredApproval;
|
|
120
|
+
reasons.push({ severity: 'info', message: `AgentSecretGrant ${match.metadata.name} requires approval: ${match.spec.requiredApproval}` });
|
|
121
|
+
}
|
|
122
|
+
grants.push(grantEntry);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Step 8 — Check config grants
|
|
127
|
+
const configGrants = resources.AgentConfigGrant || [];
|
|
128
|
+
const neededConfigs = collectConfigNeeds(stack, capabilities, resources);
|
|
129
|
+
for (const need of neededConfigs) {
|
|
130
|
+
const match = configGrants.find((cg) => {
|
|
131
|
+
if (cg.spec?.subject !== serviceAccountRef && cg.spec?.subject !== agentStack) return false;
|
|
132
|
+
if (need.purpose && cg.spec?.purpose !== need.purpose) return false;
|
|
133
|
+
return true;
|
|
134
|
+
});
|
|
135
|
+
if (!match) {
|
|
136
|
+
reasons.push({ severity: 'error', message: `Missing AgentConfigGrant for ${need.description} (purpose: ${need.purpose})` });
|
|
137
|
+
} else {
|
|
138
|
+
grants.push({ kind: 'AgentConfigGrant', name: match.metadata.name, purpose: match.spec?.purpose, status: 'granted' });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Step 9 — Workspace policy enforcement
|
|
143
|
+
if (workspacePolicyRef) {
|
|
144
|
+
const policies = resources.KrateWorkspacePolicy || [];
|
|
145
|
+
const policy = policies.find((p) => p.metadata?.name === workspacePolicyRef);
|
|
146
|
+
if (policy) {
|
|
147
|
+
// Check maxConcurrentSessions
|
|
148
|
+
if (policy.spec?.maxConcurrentSessions === 0) {
|
|
149
|
+
reasons.push({ severity: 'error', message: `Workspace policy '${workspacePolicyRef}' maxConcurrentSessions is 0: no sessions allowed` });
|
|
150
|
+
}
|
|
151
|
+
// Check deniedTools
|
|
152
|
+
const deniedTools = policy.spec?.deniedTools || [];
|
|
153
|
+
const requestedTools = capabilities.toolRefs;
|
|
154
|
+
for (const tool of requestedTools) {
|
|
155
|
+
if (deniedTools.includes(tool)) {
|
|
156
|
+
reasons.push({ severity: 'error', message: `Tool '${tool}' is denied by workspace policy '${workspacePolicyRef}'` });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Check allowedTools (if specified, only those tools are permitted)
|
|
160
|
+
const allowedTools = policy.spec?.allowedTools;
|
|
161
|
+
if (allowedTools && allowedTools.length > 0) {
|
|
162
|
+
for (const tool of requestedTools) {
|
|
163
|
+
if (!allowedTools.includes(tool)) {
|
|
164
|
+
reasons.push({ severity: 'error', message: `Tool '${tool}' is not in allowedTools for workspace policy '${workspacePolicyRef}'` });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Step 10 — Decision
|
|
172
|
+
const hasErrors = reasons.some((r) => r.severity === 'error');
|
|
173
|
+
const hasApprovals = grants.some((g) => g.status === 'requires-approval');
|
|
174
|
+
let decision;
|
|
175
|
+
if (hasErrors) {
|
|
176
|
+
decision = 'denied';
|
|
177
|
+
} else if (hasApprovals) {
|
|
178
|
+
decision = 'requires-approval';
|
|
179
|
+
} else {
|
|
180
|
+
decision = 'allowed';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return buildDecision({ decision, reasons, grants, capabilities, actor, repository, ref, agentStack, taskKind, approvalMode, crossOrgDenials, untrustedForkWarnings });
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
createPermissionSnapshot(reviewResult) {
|
|
187
|
+
const snapshot = clone(reviewResult);
|
|
188
|
+
snapshot.snapshotAt = new Date().toISOString();
|
|
189
|
+
snapshot.frozen = true;
|
|
190
|
+
snapshot.digest = reviewResult.digest;
|
|
191
|
+
return Object.freeze(snapshot);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function collectSecretNeeds(stack, capabilities, resources) {
|
|
197
|
+
const needs = [];
|
|
198
|
+
if (stack.spec?.adapter) {
|
|
199
|
+
needs.push({ description: `model provider for adapter ${stack.spec.adapter}`, purpose: 'model-provider' });
|
|
200
|
+
}
|
|
201
|
+
const mcpServers = resources.AgentMcpServer || [];
|
|
202
|
+
for (const ref of capabilities.mcpServerRefs) {
|
|
203
|
+
const server = mcpServers.find((s) => s.metadata?.name === ref);
|
|
204
|
+
if (server?.spec?.secretRef) {
|
|
205
|
+
needs.push({ description: `MCP server ${ref} secret`, purpose: `mcp-server:${ref}` });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return needs;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function collectConfigNeeds(stack, capabilities, resources) {
|
|
212
|
+
const needs = [];
|
|
213
|
+
const mcpServers = resources.AgentMcpServer || [];
|
|
214
|
+
for (const ref of capabilities.mcpServerRefs) {
|
|
215
|
+
const server = mcpServers.find((s) => s.metadata?.name === ref);
|
|
216
|
+
if (server?.spec?.configMapRef) {
|
|
217
|
+
needs.push({ description: `MCP server ${ref} config`, purpose: `mcp-server:${ref}` });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return needs;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function buildDecision({ decision, reasons, grants, capabilities, actor, repository, ref, agentStack, taskKind, approvalMode, crossOrgDenials = [], untrustedForkWarnings = [] }) {
|
|
224
|
+
const result = {
|
|
225
|
+
decision,
|
|
226
|
+
actor,
|
|
227
|
+
repository,
|
|
228
|
+
ref,
|
|
229
|
+
agentStack,
|
|
230
|
+
taskKind,
|
|
231
|
+
capabilities: clone(capabilities),
|
|
232
|
+
grants: clone(grants),
|
|
233
|
+
reasons: clone(reasons),
|
|
234
|
+
crossOrgDenials: clone(crossOrgDenials),
|
|
235
|
+
untrustedForkWarnings: clone(untrustedForkWarnings),
|
|
236
|
+
reviewedAt: new Date().toISOString()
|
|
237
|
+
};
|
|
238
|
+
if (approvalMode !== undefined) {
|
|
239
|
+
result.approvalMode = approvalMode;
|
|
240
|
+
}
|
|
241
|
+
result.digest = computeDigest(result);
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function computeDigest(result) {
|
|
246
|
+
const keys = Object.keys(result).filter((k) => k !== 'digest' && k !== 'reviewedAt').sort();
|
|
247
|
+
const canonical = {};
|
|
248
|
+
for (const key of keys) canonical[key] = result[key];
|
|
249
|
+
return createHash('sha256').update(JSON.stringify(canonical)).digest('hex');
|
|
250
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Agent Project Controller — Slice 1.2d
|
|
2
|
+
// Manages KrateProject resources: validation, workflow columns, board state, and issue assignment.
|
|
3
|
+
|
|
4
|
+
export const AGENT_PROJECT_CONTROLLER_BOUNDARY = {
|
|
5
|
+
role: 'agent-project-controller',
|
|
6
|
+
scope: 'KrateProject lifecycle: validation, workflow column definitions, board state management, issue assignment tracking',
|
|
7
|
+
owns: ['project validation', 'workflow columns', 'board state', 'default column resolution', 'issue assignment tracking'],
|
|
8
|
+
delegatesTo: ['resource-model'],
|
|
9
|
+
mustNotOwn: ['issue content', 'PR lifecycle', 'dispatch execution', 'Agent Mux sessions']
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const VALID_BOARD_STATES = ['active', 'archived'];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validate a KrateProject resource. Returns { valid, errors }.
|
|
16
|
+
* @param {object} resource
|
|
17
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
18
|
+
*/
|
|
19
|
+
export function validateAgentProject(resource) {
|
|
20
|
+
const errors = [];
|
|
21
|
+
|
|
22
|
+
// Guard against null/undefined resource
|
|
23
|
+
if (resource == null) {
|
|
24
|
+
errors.push('resource must not be null or undefined');
|
|
25
|
+
return { valid: false, errors };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Validate metadata.name
|
|
29
|
+
if (!resource?.metadata?.name) {
|
|
30
|
+
errors.push('metadata.name is required');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const spec = resource?.spec || {};
|
|
34
|
+
|
|
35
|
+
// Validate organizationRef
|
|
36
|
+
if (!spec.organizationRef) {
|
|
37
|
+
errors.push('spec.organizationRef is required');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Validate workflowColumns — must be a non-empty array
|
|
41
|
+
const cols = spec.workflowColumns;
|
|
42
|
+
if (!Array.isArray(cols) || cols.length === 0) {
|
|
43
|
+
errors.push('spec.workflowColumns must be a non-empty array');
|
|
44
|
+
} else {
|
|
45
|
+
// Check for duplicate column IDs
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
for (const col of cols) {
|
|
48
|
+
if (seen.has(col.id)) {
|
|
49
|
+
errors.push(`spec.workflowColumns contains duplicate column ID: "${col.id}"`);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
seen.add(col.id);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Validate boardState if explicitly set
|
|
57
|
+
const boardState = spec.boardState;
|
|
58
|
+
if (boardState != null && !VALID_BOARD_STATES.includes(boardState)) {
|
|
59
|
+
errors.push(`spec.boardState "${boardState}" is not supported; valid states are: ${VALID_BOARD_STATES.join(', ')}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { valid: errors.length === 0, errors };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Factory that returns a KrateProject controller instance.
|
|
67
|
+
*/
|
|
68
|
+
export function createAgentProjectController() {
|
|
69
|
+
return {
|
|
70
|
+
role: 'agent-project-controller',
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Validate a KrateProject resource.
|
|
74
|
+
* @param {object} resource
|
|
75
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
76
|
+
*/
|
|
77
|
+
validate(resource) {
|
|
78
|
+
return validateAgentProject(resource);
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Return the workflow columns for a project, in order.
|
|
83
|
+
* @param {object} resource
|
|
84
|
+
* @returns {Array<{ id: string, displayName: string, color: string, default?: boolean }>}
|
|
85
|
+
*/
|
|
86
|
+
getWorkflowColumns(resource) {
|
|
87
|
+
const cols = resource?.spec?.workflowColumns;
|
|
88
|
+
if (!Array.isArray(cols)) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
return [...cols];
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Return the default column — the one marked `default: true`, or the first column.
|
|
96
|
+
* @param {object} resource
|
|
97
|
+
* @returns {{ id: string, displayName: string, color: string, default?: boolean } | undefined}
|
|
98
|
+
*/
|
|
99
|
+
getDefaultColumn(resource) {
|
|
100
|
+
const cols = Array.isArray(resource?.spec?.workflowColumns)
|
|
101
|
+
? resource.spec.workflowColumns
|
|
102
|
+
: [];
|
|
103
|
+
const marked = cols.find((c) => c.default === true);
|
|
104
|
+
return marked ?? cols[0];
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Return the board state for a project.
|
|
109
|
+
* Defaults to 'active' when spec.boardState is not set.
|
|
110
|
+
* @param {object} resource
|
|
111
|
+
* @returns {string}
|
|
112
|
+
*/
|
|
113
|
+
getBoardState(resource) {
|
|
114
|
+
return resource?.spec?.boardState ?? 'active';
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Agent Provider Config Controller — Slice 1.2c
|
|
2
|
+
// Manages AgentProviderConfig resources: model provider config validation,
|
|
3
|
+
// endpoint resolution, credential ref validation, and feature flag management.
|
|
4
|
+
|
|
5
|
+
export const AGENT_PROVIDER_CONFIG_CONTROLLER_BOUNDARY = {
|
|
6
|
+
role: 'agent-provider-config-controller',
|
|
7
|
+
scope: 'AgentProviderConfig lifecycle: validation, endpoint resolution, credential ref validation, feature flags',
|
|
8
|
+
owns: ['provider config validation', 'endpoint resolution', 'credential ref validation', 'feature flag defaults', 'rate limit defaults'],
|
|
9
|
+
delegatesTo: ['resource-model'],
|
|
10
|
+
mustNotOwn: ['secret values', 'dispatch execution', 'Agent Mux sessions', 'adapter implementation']
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const VALID_PROVIDER_TYPES = ['anthropic', 'openai', 'azure-openai', 'google-vertex', 'foundry', 'custom'];
|
|
14
|
+
|
|
15
|
+
const DEFAULT_ENDPOINTS = Object.freeze({
|
|
16
|
+
anthropic: 'https://api.anthropic.com/v1',
|
|
17
|
+
openai: 'https://api.openai.com/v1',
|
|
18
|
+
'azure-openai': null, // requires explicit endpoint (tenant-specific)
|
|
19
|
+
'google-vertex': 'https://us-central1-aiplatform.googleapis.com/v1',
|
|
20
|
+
foundry: null, // requires explicit endpoint
|
|
21
|
+
custom: null // requires explicit endpoint
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const DEFAULT_FEATURE_FLAGS = Object.freeze({
|
|
25
|
+
streaming: true,
|
|
26
|
+
tool_use: true,
|
|
27
|
+
vision: false
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const DEFAULT_RATE_LIMITS = Object.freeze({
|
|
31
|
+
requestsPerMinute: 60,
|
|
32
|
+
tokensPerMinute: 100000
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Validate an AgentProviderConfig resource. Returns { valid, errors }.
|
|
37
|
+
* @param {object} resource
|
|
38
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
39
|
+
*/
|
|
40
|
+
export function validateAgentProviderConfig(resource) {
|
|
41
|
+
const errors = [];
|
|
42
|
+
|
|
43
|
+
// Guard against null/undefined resource
|
|
44
|
+
if (resource == null) {
|
|
45
|
+
errors.push('resource must not be null or undefined');
|
|
46
|
+
return { valid: false, errors };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Validate metadata.name
|
|
50
|
+
if (!resource?.metadata?.name) {
|
|
51
|
+
errors.push('metadata.name is required');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const spec = resource?.spec || {};
|
|
55
|
+
|
|
56
|
+
// Validate providerType
|
|
57
|
+
const providerType = spec.providerType;
|
|
58
|
+
if (!providerType) {
|
|
59
|
+
errors.push(`spec.providerType is required; valid types are: ${VALID_PROVIDER_TYPES.join(', ')}`);
|
|
60
|
+
} else if (!VALID_PROVIDER_TYPES.includes(providerType)) {
|
|
61
|
+
errors.push(`spec.providerType "${providerType}" is not supported; valid types are: ${VALID_PROVIDER_TYPES.join(', ')}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Validate credentialRef — always required for security
|
|
65
|
+
if (!spec.credentialRef) {
|
|
66
|
+
errors.push('spec.credentialRef is required; provide a Kubernetes Secret name for the API key');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { valid: errors.length === 0, errors };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Factory that returns an AgentProviderConfig controller instance.
|
|
74
|
+
*/
|
|
75
|
+
export function createAgentProviderConfigController() {
|
|
76
|
+
return {
|
|
77
|
+
role: 'agent-provider-config-controller',
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Validate an AgentProviderConfig resource.
|
|
81
|
+
* @param {object} resource
|
|
82
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
83
|
+
*/
|
|
84
|
+
validate(resource) {
|
|
85
|
+
return validateAgentProviderConfig(resource);
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Resolve the effective API endpoint for a provider config.
|
|
90
|
+
* Returns the explicit spec.endpoint if set, otherwise falls back to
|
|
91
|
+
* the well-known default for the provider type.
|
|
92
|
+
* @param {object} resource
|
|
93
|
+
* @returns {string|null}
|
|
94
|
+
*/
|
|
95
|
+
resolveEndpoint(resource) {
|
|
96
|
+
if (resource == null) {
|
|
97
|
+
throw new Error('resource must not be null or undefined');
|
|
98
|
+
}
|
|
99
|
+
const spec = resource?.spec || {};
|
|
100
|
+
|
|
101
|
+
// If an explicit endpoint is set in spec, prefer it
|
|
102
|
+
if (spec.endpoint) {
|
|
103
|
+
return spec.endpoint;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Fall back to the known default for the provider type
|
|
107
|
+
const providerType = spec.providerType;
|
|
108
|
+
return DEFAULT_ENDPOINTS[providerType] ?? null;
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Return the effective feature flags for a provider config.
|
|
113
|
+
* Merges spec.featureFlags with defaults; spec values take precedence.
|
|
114
|
+
* @param {object} resource
|
|
115
|
+
* @returns {{ streaming: boolean, tool_use: boolean, vision: boolean, [key: string]: boolean }}
|
|
116
|
+
*/
|
|
117
|
+
getFeatureFlags(resource) {
|
|
118
|
+
if (resource == null) {
|
|
119
|
+
throw new Error('resource must not be null or undefined');
|
|
120
|
+
}
|
|
121
|
+
const specFlags = resource?.spec?.featureFlags ?? {};
|
|
122
|
+
return { ...DEFAULT_FEATURE_FLAGS, ...specFlags };
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Return the effective rate limit configuration for a provider config.
|
|
127
|
+
* Merges spec.rateLimits with defaults; spec values take precedence.
|
|
128
|
+
* @param {object} resource
|
|
129
|
+
* @returns {{ requestsPerMinute: number, tokensPerMinute: number }}
|
|
130
|
+
*/
|
|
131
|
+
getRateLimits(resource) {
|
|
132
|
+
if (resource == null) {
|
|
133
|
+
throw new Error('resource must not be null or undefined');
|
|
134
|
+
}
|
|
135
|
+
const specLimits = resource?.spec?.rateLimits ?? {};
|
|
136
|
+
return {
|
|
137
|
+
requestsPerMinute: specLimits.requestsPerMinute ?? DEFAULT_RATE_LIMITS.requestsPerMinute,
|
|
138
|
+
tokensPerMinute: specLimits.tokensPerMinute ?? DEFAULT_RATE_LIMITS.tokensPerMinute
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Return the list of supported provider types.
|
|
144
|
+
* @returns {string[]}
|
|
145
|
+
*/
|
|
146
|
+
getSupportedProviderTypes() {
|
|
147
|
+
return [...VALID_PROVIDER_TYPES];
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|