@a5c-ai/krate 5.0.1-staging.04a3db697
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 +3067 -0
- package/dist/krate-lifecycle.json +201 -0
- package/dist/krate-runtime-snapshot.json +2955 -0
- package/dist/krate-summary.json +722 -0
- package/docs/README.md +61 -0
- package/docs/agents/README.md +83 -0
- package/docs/agents/acceptance-test-matrix.md +193 -0
- package/docs/agents/agent-mux-adapter-contract.md +167 -0
- package/docs/agents/agent-mux-source-map.md +310 -0
- package/docs/agents/agent-run-memory-import-spec.md +256 -0
- package/docs/agents/agent-stack-management-spec.md +421 -0
- package/docs/agents/api-contract-spec.md +309 -0
- package/docs/agents/artifacts-writeback-spec.md +145 -0
- package/docs/agents/chart-packaging-spec.md +128 -0
- package/docs/agents/ci-orchestration-spec.md +140 -0
- package/docs/agents/context-assembly-spec.md +219 -0
- package/docs/agents/controller-reconciliation-spec.md +255 -0
- package/docs/agents/crd-schema-spec.md +315 -0
- package/docs/agents/decision-log-open-questions.md +169 -0
- package/docs/agents/developer-implementation-checklist.md +329 -0
- package/docs/agents/dispatching-design.md +262 -0
- package/docs/agents/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/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 +236 -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 +321 -0
- package/src/agent-workspace-controller.js +447 -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 +50 -0
- package/src/controller-ui.js +551 -0
- package/src/data-plane.js +178 -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 +95 -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 +55 -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/operations.js +112 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +221 -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/deployment.test.js +396 -0
- package/tests/e2e/lifecycle.test.js +117 -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 +727 -0
- package/tests/memory-search-wiring.test.js +270 -0
- package/tests/org-scoping.test.js +687 -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/workspace-volumes.test.js +312 -0
- package/tests/writeback-persistence.test.js +207 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
const requiredFiles = [
|
|
5
|
+
'Dockerfile',
|
|
6
|
+
'.dockerignore',
|
|
7
|
+
'.github/workflows/publish.yml',
|
|
8
|
+
'../web/app/page.jsx',
|
|
9
|
+
'../web/app/layout.jsx',
|
|
10
|
+
'../web/app/orgs/[org]/deployments/page.jsx',
|
|
11
|
+
'../web/app/globals.css',
|
|
12
|
+
'../web/next.config.mjs',
|
|
13
|
+
'../charts/Chart.yaml',
|
|
14
|
+
'../charts/values.yaml',
|
|
15
|
+
'../charts/crds/repositories.yaml',
|
|
16
|
+
'../charts/crds/aggregated-resources.yaml',
|
|
17
|
+
'../charts/crds/policy-resources.yaml',
|
|
18
|
+
'../charts/templates/apiservice.yaml',
|
|
19
|
+
'../charts/templates/deployments.yaml',
|
|
20
|
+
'../charts/templates/rbac.yaml',
|
|
21
|
+
'../charts/templates/serviceaccount.yaml',
|
|
22
|
+
'../charts/templates/services.yaml',
|
|
23
|
+
'../charts/templates/networkpolicy.yaml',
|
|
24
|
+
'../charts/templates/gitea.yaml',
|
|
25
|
+
'../charts/templates/ingress.yaml',
|
|
26
|
+
'../charts/templates/auth-secret.yaml',
|
|
27
|
+
'../charts/templates/argocd-application.yaml',
|
|
28
|
+
'../charts/templates/kubevela-application.yaml',
|
|
29
|
+
'../charts/templates/NOTES.txt',
|
|
30
|
+
'examples/minikube-demo.yaml',
|
|
31
|
+
'examples/policy-kyverno-pr-title.yaml',
|
|
32
|
+
'scripts/setup-minikube.mjs'
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const requiredKinds = ['Deployment', 'Service', 'ServiceAccount', 'ClusterRole', 'ClusterRoleBinding', 'NetworkPolicy', 'PersistentVolumeClaim'];
|
|
36
|
+
const requiredCrds = ['Organization', 'OrgNamespaceBinding', 'User', 'Team', 'Invite', 'IdentityMapping', 'AuthProvider', 'Repository', 'SSHKey', 'RepositoryPermission', 'BranchProtection', 'RefPolicy', 'PolicyProfile', 'PolicyTemplate', 'PolicyBinding', 'PolicyExceptionRequest', 'PullRequest', 'Issue', 'Review', 'Pipeline', 'Job', 'RunnerPool', 'WebhookSubscription', 'WebhookDelivery', 'View', 'Selector'];
|
|
37
|
+
const requiredExampleKinds = ['Pipeline', 'Application'];
|
|
38
|
+
const requiredValueTerms = ['externalDependencies', 'postgres', 'objectStorage', 'nats', 'arc', 'kyverno', 'gatekeeper', 'ingress', 'oidc', 'auth', 'github', 'sso', 'delegatedIdentity', 'autoscaling', 'targetCPUUtilizationPercentage', 'gitea', 'argocd', 'repoURL', 'syncPolicy', 'apiService', 'kubevela', 'vela-core'];
|
|
39
|
+
const missing = [];
|
|
40
|
+
for (const file of requiredFiles) if (!existsSync(file)) missing.push(`missing file: ${file}`);
|
|
41
|
+
const chartText = requiredFiles.filter((file) => file.startsWith('../charts/') && existsSync(file)).map((file) => readFileSync(file, 'utf8')).join('\n');
|
|
42
|
+
const exampleText = existsSync('examples/minikube-demo.yaml') ? readFileSync('examples/minikube-demo.yaml', 'utf8') : '';
|
|
43
|
+
const valuesText = existsSync('../charts/values.yaml') ? readFileSync('../charts/values.yaml', 'utf8') : '';
|
|
44
|
+
const argocdTemplate = ['../charts/templates/argocd-application.yaml', '../charts/templates/kubevela-application.yaml'].filter((file) => existsSync(file)).map((file) => readFileSync(file, 'utf8')).join('\n');
|
|
45
|
+
for (const kind of requiredKinds) if (!chartText.includes(`kind: ${kind}`)) missing.push(`chart missing kind: ${kind}`);
|
|
46
|
+
for (const kind of requiredCrds) if (!chartText.includes(`kind: ${kind}`) && !exampleText.includes(`kind: ${kind}`)) missing.push(`package missing resource kind: ${kind}`);
|
|
47
|
+
for (const kind of requiredExampleKinds) if (!exampleText.includes(`kind: ${kind}`)) missing.push(`example missing resource kind: ${kind}`);
|
|
48
|
+
for (const term of requiredValueTerms) if (!valuesText.includes(term)) missing.push(`values missing ${term}`);
|
|
49
|
+
if (valuesText.includes('`n')) missing.push('values contains escaped newline markers instead of YAML structure');
|
|
50
|
+
const requiredValuePatterns = [
|
|
51
|
+
[/^gitea:\s*$/m, 'values missing gitea block'],
|
|
52
|
+
[/^argocd:\s*$/m, 'values missing argocd block'],
|
|
53
|
+
[/^ repoURL:\s*\S+/m, 'values missing argocd repoURL value'],
|
|
54
|
+
[/^ syncPolicy:\s*$/m, 'values missing structured argocd syncPolicy block'],
|
|
55
|
+
[/^ automated:\s*true\s*$/m, 'values missing argocd syncPolicy.automated=true'],
|
|
56
|
+
[/^ prune:\s*true\s*$/m, 'values missing argocd syncPolicy.prune=true'],
|
|
57
|
+
[/^ selfHeal:\s*true\s*$/m, 'values missing argocd syncPolicy.selfHeal=true'],
|
|
58
|
+
[/^ syncOptions:\s*$/m, 'values missing argocd syncPolicy.syncOptions block'],
|
|
59
|
+
[/^ - CreateNamespace=true\s*$/m, 'values missing CreateNamespace sync option']
|
|
60
|
+
];
|
|
61
|
+
for (const [pattern, message] of requiredValuePatterns) if (!pattern.test(valuesText)) missing.push(message);
|
|
62
|
+
for (const requiredTemplateUse of ['.Values.argocd.syncPolicy.automated', '.Values.argocd.syncPolicy.prune', '.Values.argocd.syncPolicy.selfHeal', '.Values.argocd.syncPolicy.syncOptions']) {
|
|
63
|
+
if (!argocdTemplate.includes(requiredTemplateUse)) missing.push(`argocd template does not consume ${requiredTemplateUse}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const dockerignore = existsSync('.dockerignore') ? readFileSync('.dockerignore', 'utf8') : '';
|
|
67
|
+
for (const ignored of ['.a5c', 'node_modules', '**/.next', 'dist']) if (!dockerignore.includes(ignored)) missing.push(`dockerignore missing ${ignored}`);
|
|
68
|
+
|
|
69
|
+
const packageInfo = JSON.parse(readFileSync('package.json', 'utf8'));
|
|
70
|
+
for (const fileSet of ['bin', 'examples', 'scripts', 'src', 'tests', 'Dockerfile']) {
|
|
71
|
+
if (!packageInfo.files?.includes(fileSet)) missing.push(`package files missing ${fileSet}`);
|
|
72
|
+
}
|
|
73
|
+
for (const script of ['e2e', 'package:check', 'setup:minikube']) if (!packageInfo.scripts?.[script]) missing.push(`package script missing ${script}`);
|
|
74
|
+
if (missing.length) {
|
|
75
|
+
console.error(JSON.stringify({ status: 'failed', missing }, null, 2));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
80
|
+
const packOutput = execFileSync(npmBin, ['pack', '--dry-run', '--json'], { encoding: 'utf8', shell: process.platform === 'win32' });
|
|
81
|
+
const pack = JSON.parse(packOutput)[0];
|
|
82
|
+
const packedFiles = new Set(pack.files.map((file) => file.path));
|
|
83
|
+
for (const file of ['Dockerfile', 'examples/minikube-demo.yaml', 'scripts/setup-minikube.mjs', 'scripts/validate-package.mjs']) {
|
|
84
|
+
if (!packedFiles.has(file)) missing.push(`npm pack missing ${file}`);
|
|
85
|
+
}
|
|
86
|
+
if (missing.length) {
|
|
87
|
+
console.error(JSON.stringify({ status: 'failed', missing }, null, 2));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
console.log(JSON.stringify({ status: 'success', checkedFiles: requiredFiles.length, requiredKinds: requiredKinds.length, requiredCrds: requiredCrds.length, packedFiles: pack.files.length, packageSize: pack.size, requiredExampleKinds: requiredExampleKinds.length, requiredValueTerms: requiredValueTerms.length }, null, 2));
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createControllerUiModel } from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
const required = [
|
|
6
|
+
'apps/web/app/layout.jsx',
|
|
7
|
+
'apps/web/app/globals.css',
|
|
8
|
+
'apps/web/app/page.jsx',
|
|
9
|
+
'apps/web/app/ui-shell.jsx',
|
|
10
|
+
'apps/web/proxy.js',
|
|
11
|
+
'apps/web/app/components/code-editor.jsx',
|
|
12
|
+
'apps/web/app/components/resource-actions.jsx',
|
|
13
|
+
'apps/web/app/api/controller/route.js',
|
|
14
|
+
'apps/web/app/api/orgs/[org]/resources/route.js',
|
|
15
|
+
'apps/web/app/api/orgs/[org]/resources/[kind]/[name]/route.js',
|
|
16
|
+
'apps/web/app/api/orgs/[org]/repositories/route.js',
|
|
17
|
+
'apps/web/app/api/orgs/[org]/repositories/[name]/route.js',
|
|
18
|
+
'apps/web/app/api/orgs/[org]/policies/route.js',
|
|
19
|
+
'apps/web/app/api/orgs/[org]/policy-reports/route.js',
|
|
20
|
+
'apps/web/app/api/orgs/[org]/policy-exception-requests/route.js',
|
|
21
|
+
'apps/web/app/api/watch/[[...resource]]/route.js',
|
|
22
|
+
'apps/web/app/api/git-proxy/route.js',
|
|
23
|
+
'apps/web/app/api/auth/[provider]/route.js',
|
|
24
|
+
'apps/web/app/api/auth/callback/[provider]/route.js',
|
|
25
|
+
'apps/web/app/api/auth/logout/route.js',
|
|
26
|
+
'apps/web/app/api/auth/delegated/route.js',
|
|
27
|
+
'src/api-controller.js',
|
|
28
|
+
'src/kubernetes-resource-gateway.js',
|
|
29
|
+
'src/kubernetes-controller.js',
|
|
30
|
+
'src/controller-client.js',
|
|
31
|
+
'src/controller-ui.js',
|
|
32
|
+
'src/http-server.js'
|
|
33
|
+
];
|
|
34
|
+
function resolveRequiredFile(file) {
|
|
35
|
+
if (existsSync(file)) return file;
|
|
36
|
+
if (file.startsWith('apps/web/')) {
|
|
37
|
+
const packageRelative = path.join('..', 'web', file.slice('apps/web/'.length));
|
|
38
|
+
if (existsSync(packageRelative)) return packageRelative;
|
|
39
|
+
}
|
|
40
|
+
return file;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const files = Object.fromEntries(required.map((file) => [file, readFileSync(resolveRequiredFile(file), 'utf8')]));
|
|
44
|
+
const failures = [];
|
|
45
|
+
|
|
46
|
+
for (const [file, source] of Object.entries(files)) {
|
|
47
|
+
if (!source.trim()) failures.push(`${file} is empty`);
|
|
48
|
+
}
|
|
49
|
+
if (!/\.appBody\s*\{[\s\S]*?width:\s*100%;[\s\S]*?max-width:\s*none;/.test(files['apps/web/app/globals.css'])) {
|
|
50
|
+
failures.push('app shell body must use the full viewport width');
|
|
51
|
+
}
|
|
52
|
+
if (/\.appBody\s*\{[^}]*width:\s*min\(100%,\s*\d+px\)/.test(files['apps/web/app/globals.css'])) {
|
|
53
|
+
failures.push('app shell body is capped to a centered max width');
|
|
54
|
+
}
|
|
55
|
+
if (!/\.appTopbar,\s*\.appBody,\s*\.appContent,\s*\.routeMain\s*\{[\s\S]*?width:\s*100%;[\s\S]*?max-width:\s*none;[\s\S]*?margin-left:\s*0;[\s\S]*?margin-right:\s*0;/.test(files['apps/web/app/globals.css'])) {
|
|
56
|
+
failures.push('authenticated app shell must override centered layout caps at the final cascade layer');
|
|
57
|
+
}
|
|
58
|
+
if (!/\.loginMain\s*\{[\s\S]*?width:\s*100%;[\s\S]*?max-width:\s*none;[\s\S]*?margin:\s*0;/.test(files['apps/web/app/globals.css'])) {
|
|
59
|
+
failures.push('login shell must use the full viewport width');
|
|
60
|
+
}
|
|
61
|
+
if (/\.loginMain\s*\{[^}]*width:\s*min\([^;]*720px\)/.test(files['apps/web/app/globals.css'])) {
|
|
62
|
+
failures.push('login shell is capped to a centered card width');
|
|
63
|
+
}
|
|
64
|
+
if (!/\.loginCard\s*\{[\s\S]*?width:\s*min\(100%,\s*720px\)/.test(files['apps/web/app/globals.css'])) {
|
|
65
|
+
failures.push('login card must own the readable sign-in width cap');
|
|
66
|
+
}
|
|
67
|
+
for (const file of ['apps/web/app/api/controller/route.js', 'apps/web/app/api/watch/[[...resource]]/route.js', 'src/controller-client.js']) {
|
|
68
|
+
if (files[file].includes('createKrateUiDemoRuntime')) failures.push(`${file} imports or calls createKrateUiDemoRuntime`);
|
|
69
|
+
if (files[file].includes('createKrateRuntime()')) failures.push(`${file} creates an in-memory runtime fallback`);
|
|
70
|
+
}
|
|
71
|
+
for (const file of ['apps/web/app/page.jsx', 'apps/web/app/ui-shell.jsx']) {
|
|
72
|
+
if (files[file].includes('krate-demo')) failures.push(`${file} hardcodes krate-demo demo navigation`);
|
|
73
|
+
}
|
|
74
|
+
for (const [file, source] of Object.entries(files)) {
|
|
75
|
+
if (source.includes('sampleResource') || source.includes('exampleResource')) failures.push(`${file} synthesizes sample/example Krate resources`);
|
|
76
|
+
if (source.includes('new-repository') && file !== 'apps/web/app/components/resource-actions.jsx') failures.push(`${file} hardcodes synthetic Repository data`);
|
|
77
|
+
}
|
|
78
|
+
for (const token of ['export function proxy', 'NextResponse.redirect', 'KRATE_AUTH_COOKIE_NAME', 'krate_session', '/login', '/api/auth', 'matcher']) {
|
|
79
|
+
if (!files['apps/web/proxy.js'].includes(token)) failures.push(`web proxy missing ${token}`);
|
|
80
|
+
}
|
|
81
|
+
for (const token of ['spawnSync', 'spawn(', 'kubectl', 'getControllerSnapshot', 'listResource', 'getResource', 'applyResource', 'deleteResource', 'createRepository', 'watchResource', 'auth', 'can-i']) {
|
|
82
|
+
if (!files['src/kubernetes-controller.js'].includes(token)) failures.push(`kubernetes controller missing ${token}`);
|
|
83
|
+
}
|
|
84
|
+
for (const token of ['createKubernetesResourceGateway', 'controller.snapshot()', 'createControllerUiModel']) {
|
|
85
|
+
if (!files['src/controller-client.js'].includes(token)) failures.push(`controller client missing ${token}`);
|
|
86
|
+
}
|
|
87
|
+
for (const token of ['createKrateApiController', 'resourceGateway', 'withArchitecture', 'krate-api-controller', 'kubernetes-resource-gateway', 'kubernetes-resource-client', 'git-data-plane', 'never owns Kubernetes reconciliation loops', 'KRATE_API_CONTROLLER_BOUNDARY', 'listRepositoriesForForge', 'getRepositoryForgeView', 'krate-kubernetes-reconciler']) {
|
|
88
|
+
if (!files['src/api-controller.js'].includes(token)) failures.push(`api controller boundary missing ${token}`);
|
|
89
|
+
}
|
|
90
|
+
for (const token of ['createKubernetesResourceClient', 'repositoryManifest', 'async list', 'async get', 'async apply', 'async delete', 'watch(resourcePath', 'KUBERNETES_RESOURCE_GATEWAY_BOUNDARY', 'mustNotOwn']) {
|
|
91
|
+
if (!files['src/kubernetes-resource-gateway.js'].includes(token)) failures.push(`kubernetes resource gateway missing ${token}`);
|
|
92
|
+
}
|
|
93
|
+
for (const token of ['GET', 'POST', 'DELETE', 'controller.listResource', 'controller.getResource', 'controller.applyResource', 'controller.deleteResource', 'controller.createRepository']) {
|
|
94
|
+
const joinedRoutes = files['apps/web/app/api/orgs/[org]/resources/route.js'] + files['apps/web/app/api/orgs/[org]/resources/[kind]/[name]/route.js'] + files['apps/web/app/api/orgs/[org]/repositories/route.js'] + files['apps/web/app/api/orgs/[org]/repositories/[name]/route.js'];
|
|
95
|
+
if (!joinedRoutes.includes(token)) failures.push(`resource management routes missing ${token}`);
|
|
96
|
+
}
|
|
97
|
+
const policyRoutes = files['apps/web/app/api/orgs/[org]/policies/route.js'] + files['apps/web/app/api/orgs/[org]/policy-reports/route.js'] + files['apps/web/app/api/orgs/[org]/policy-exception-requests/route.js'];
|
|
98
|
+
for (const token of ['GET', 'POST', 'createKrateApiController', 'createControllerUiModel', 'policyEngine', 'controller.applyResource', 'PolicyBinding', 'PolicyExceptionRequest']) {
|
|
99
|
+
if (!policyRoutes.includes(token)) failures.push(`policy management routes missing ${token}`);
|
|
100
|
+
}
|
|
101
|
+
for (const token of ['--watch', 'text/event-stream', 'controller.watchResource', 'krate-error', 'request.signal']) {
|
|
102
|
+
if (!files['apps/web/app/api/watch/[[...resource]]/route.js'].includes(token)) failures.push(`watch route missing ${token}`);
|
|
103
|
+
}
|
|
104
|
+
for (const token of ['RepositoryManager', 'DeploymentManager', 'ResourceApplyPanel', '/api/orgs/${org}/repositories', '/api/orgs/${org}/resources', 'fetch(', 'Save changes', 'InviteReviewList', 'UserReviewList', 'PermissionReviewList', 'Mark accepted', 'Revoke invite', 'Disable user', 'Restore user', 'Revoke grant', 'SshKeyReviewList', 'Save SSH key', 'Revoke SSH key', 'Create deployment', 'Prepare deployment']) {
|
|
105
|
+
if (!(files['apps/web/app/components/resource-actions.jsx'] + files['apps/web/app/ui-shell.jsx']).includes(token)) failures.push(`UI management surface missing ${token}`);
|
|
106
|
+
}
|
|
107
|
+
for (const token of ['DegradedBanner', 'No repositories are available yet.', 'No resource selected yet.', 'Access checks', 'KRATE_CONTROLLER_URL', 'Krate repositories']) {
|
|
108
|
+
if (!(files['apps/web/app/ui-shell.jsx'] + files['apps/web/app/components/resource-actions.jsx']).includes(token)) failures.push(`truthful degraded/empty UI missing ${token}`);
|
|
109
|
+
}
|
|
110
|
+
for (const token of ['duplex', 'KRATE_GITEA_HTTP_URL', 'fetch(target', 'degraded']) {
|
|
111
|
+
if (!files['apps/web/app/api/git-proxy/route.js'].includes(token)) failures.push(`git proxy route missing ${token}`);
|
|
112
|
+
}
|
|
113
|
+
for (const token of ['orgResourceCollectionMatch', 'orgRepositoryCollectionMatch', 'orgNamespaceName', 'createKubernetesResourceGateway', 'scopedController.listResource', 'scopedController.getResource', 'scopedController.applyResource', 'scopedController.deleteResource']) {
|
|
114
|
+
if (!files['src/http-server.js'].includes(token)) failures.push(`http controller missing ${token}`);
|
|
115
|
+
}
|
|
116
|
+
const pageContracts = {
|
|
117
|
+
'apps/web/app/orgs/[org]/controller-api/page.jsx': 'ControllerApiPage',
|
|
118
|
+
'apps/web/app/orgs/[org]/repositories/page.jsx': 'RepositoriesPage',
|
|
119
|
+
'apps/web/app/orgs/[org]/inbox/page.jsx': 'InboxPage',
|
|
120
|
+
'apps/web/app/orgs/[org]/runs/page.jsx': 'RunsPage',
|
|
121
|
+
'apps/web/app/orgs/[org]/runners-ci/page.jsx': 'RunnersCiPage',
|
|
122
|
+
'apps/web/app/orgs/[org]/hooks-events/page.jsx': 'HooksEventsPage',
|
|
123
|
+
'apps/web/app/orgs/[org]/insights/page.jsx': 'InsightsPage',
|
|
124
|
+
'apps/web/app/orgs/[org]/operations-install/page.jsx': 'OperationsInstallPage',
|
|
125
|
+
'apps/web/app/orgs/[org]/advanced-plans/page.jsx': 'AdvancedPlansPage',
|
|
126
|
+
'apps/web/app/orgs/[org]/people/page.jsx': 'PeoplePage',
|
|
127
|
+
'apps/web/app/login/page.jsx': 'LoginPage',
|
|
128
|
+
'apps/web/app/logout/page.jsx': 'LogoutPage',
|
|
129
|
+
'apps/web/app/orgs/[org]/repositories/[repo]/code/page.jsx': 'RepositoryCodePage',
|
|
130
|
+
'apps/web/app/orgs/[org]/repositories/[repo]/pull-requests/page.jsx': 'RepositoryPullRequestsPage',
|
|
131
|
+
'apps/web/app/orgs/[org]/repositories/[repo]/issues/page.jsx': 'RepositoryIssuesPage',
|
|
132
|
+
'apps/web/app/orgs/[org]/repositories/[repo]/runs/page.jsx': 'RepositoryRunsPage',
|
|
133
|
+
'apps/web/app/orgs/[org]/repositories/[repo]/hooks/page.jsx': 'RepositoryHooksPage',
|
|
134
|
+
'apps/web/app/orgs/[org]/repositories/[repo]/settings/page.jsx': 'RepositorySettingsPage'
|
|
135
|
+
};
|
|
136
|
+
for (const file of Object.keys(pageContracts)) {
|
|
137
|
+
files[file] = readFileSync(resolveRequiredFile(file), 'utf8');
|
|
138
|
+
}
|
|
139
|
+
for (const [file, component] of Object.entries(pageContracts)) {
|
|
140
|
+
if (!files[file].includes(component)) failures.push(`${file} does not use dedicated ${component} route component`);
|
|
141
|
+
}
|
|
142
|
+
if (required.some((file) => file.includes('/pipelines'))) failures.push('legacy pipelines route is still required');
|
|
143
|
+
for (const token of ['ControllerApiPage', 'RepositoriesPage', 'InboxPage', 'RunsPage', 'RunnersCiPage', 'HooksEventsPage', 'InsightsPage', 'OperationsInstallPage', 'AdvancedPlansPage', 'PeoplePage', 'LoginPage', 'LogoutPage', 'RepositoryCodePage', 'RepositoryPullRequestsPage', 'RepositoryIssuesPage', 'RepositoryRunsPage', 'RepositoryHooksPage', 'RepositorySettingsPage']) {
|
|
144
|
+
if (!files['apps/web/app/ui-shell.jsx'].includes(token)) failures.push(`ui shell missing dedicated flow component ${token}`);
|
|
145
|
+
}
|
|
146
|
+
for (const token of ['Invite people', 'identity links', 'repository permissions', 'Access overview', 'Access readiness', 'Use workspace identity', 'Sign in to Krate', 'Repository home', 'Review inbox', 'Run debugger', 'Capacity designer', 'Automation inspector', 'Clone and refs', 'Repository settings map', 'Advanced architecture details', 'ResourceList', 'PlanCard', 'ForgeFlowRail', 'RepositoryCommandBar', 'breadcrumbs', 'Create → review → merge → deploy', 'Advanced resource details']) {
|
|
147
|
+
if (!files['apps/web/app/ui-shell.jsx'].includes(token)) failures.push(`ui shell missing forge UX affordance ${token}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const model = createControllerUiModel({
|
|
151
|
+
source: 'kubernetes',
|
|
152
|
+
namespace: 'krate-org-default',
|
|
153
|
+
generatedAt: 'test-time',
|
|
154
|
+
correlationId: 'validation',
|
|
155
|
+
kubectl: { available: true, context: 'kind-krate', clientVersion: 'v1.test', errors: [] },
|
|
156
|
+
apiService: { metadata: { name: 'v1alpha1.krate.a5c.ai' } },
|
|
157
|
+
crds: [{ metadata: { name: 'repositories.krate.a5c.ai' } }],
|
|
158
|
+
storage: { etcd: 'etcd', postgres: 'postgres', repositories: 'rwx', objects: 'object' },
|
|
159
|
+
commands: [],
|
|
160
|
+
permissions: [],
|
|
161
|
+
events: [],
|
|
162
|
+
resources: {
|
|
163
|
+
Organization: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'Organization', metadata: { name: 'default', namespace: 'krate-system' }, spec: { slug: 'default', namespaceName: 'krate-org-default', displayName: 'Default org' } }],
|
|
164
|
+
Repository: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'Repository', metadata: { name: 'live-repo', namespace: 'krate-org-default' }, spec: { organizationRef: 'default', visibility: 'internal', defaultBranch: 'main' }, status: { phase: 'Ready' } }],
|
|
165
|
+
User: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'User', metadata: { name: 'alice', namespace: 'krate-org-default' }, spec: { organizationRef: 'default', email: 'alice@example.com', username: 'alice' }, status: { phase: 'Active' } }],
|
|
166
|
+
RepositoryPermission: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'RepositoryPermission', metadata: { name: 'live-repo-alice', namespace: 'krate-org-default' }, spec: { organizationRef: 'default', repository: 'live-repo', subject: 'alice', subjectKind: 'user', permission: 'write' }, status: { phase: 'Synced' } }],
|
|
167
|
+
SSHKey: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'SSHKey', metadata: { name: 'alice-laptop', namespace: 'krate-org-default' }, spec: { organizationRef: 'default', owner: 'alice', title: 'laptop', scope: 'user', key: 'ssh-ed25519 AAAA' }, status: { phase: 'Synced' } }],
|
|
168
|
+
PolicyProfile: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'PolicyProfile', metadata: { name: 'default-profile', namespace: 'krate-org-default' }, spec: { organizationRef: 'default', displayName: 'Default profile', mode: 'audit' }, status: { phase: 'Ready' } }],
|
|
169
|
+
PolicyTemplate: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'PolicyTemplate', metadata: { name: 'require-pr-description', namespace: 'krate-org-default' }, spec: { organizationRef: 'default', displayName: 'Require PR description', targetKinds: ['PullRequest'], kyverno: { kind: 'ValidatingPolicy' } }, status: { phase: 'Ready' } }],
|
|
170
|
+
PolicyBinding: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'PolicyBinding', metadata: { name: 'require-pr-description-audit', namespace: 'krate-org-default' }, spec: { organizationRef: 'default', templateRef: 'require-pr-description', mode: 'audit' }, status: { phase: 'Bound' } }],
|
|
171
|
+
PolicyExceptionRequest: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'PolicyExceptionRequest', metadata: { name: 'temporary-bypass', namespace: 'krate-org-default' }, spec: { organizationRef: 'default', policyRef: { name: 'require-pr-description' }, justification: 'migration window', expiresAt: '2026-06-01T00:00:00Z' }, status: { phase: 'Requested' } }],
|
|
172
|
+
PullRequest: [],
|
|
173
|
+
Pipeline: [],
|
|
174
|
+
RunnerPool: [],
|
|
175
|
+
WebhookSubscription: []
|
|
176
|
+
},
|
|
177
|
+
kyverno: {
|
|
178
|
+
enabled: true,
|
|
179
|
+
detected: true,
|
|
180
|
+
mode: 'byo',
|
|
181
|
+
namespace: 'kyverno',
|
|
182
|
+
policyNamespace: 'krate-system',
|
|
183
|
+
health: 'ready',
|
|
184
|
+
degraded: [],
|
|
185
|
+
reports: {
|
|
186
|
+
policyReports: [{ metadata: { name: 'repo-policy', namespace: 'krate-org-default' }, results: [{ policy: 'require-pr-description', rule: 'description', result: 'fail', message: 'description required', resources: [{ kind: 'PullRequest', name: 'pr-1' }] }] }],
|
|
187
|
+
clusterPolicyReports: [],
|
|
188
|
+
results: [{ report: 'repo-policy', namespace: 'krate-org-default', policy: 'require-pr-description', rule: 'description', result: 'fail', message: 'description required', resource: { kind: 'PullRequest', name: 'pr-1' } }],
|
|
189
|
+
violations: [{ report: 'repo-policy', namespace: 'krate-org-default', policy: 'require-pr-description', rule: 'description', result: 'fail', message: 'description required', resource: { kind: 'PullRequest', name: 'pr-1' } }]
|
|
190
|
+
},
|
|
191
|
+
resources: { PolicyReport: [], ClusterPolicyReport: [], KyvernoPolicyException: [] }
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
if (model.controller.mode !== 'krate-workspace') failures.push('controller mode is not krate-workspace');
|
|
195
|
+
if (model.status !== 'ready') failures.push('model did not become ready for available Krate snapshot');
|
|
196
|
+
if (!model.resources.find((resource) => resource.kind === 'User')) failures.push('model missing User identity resource');
|
|
197
|
+
if (!model.identity || typeof model.identity.counts?.users !== 'number') failures.push('model missing identity admin projection');
|
|
198
|
+
if (model.identity.counts.sshKeys !== 1) failures.push('model missing SSH key identity projection');
|
|
199
|
+
if (!model.identity.reconciliation?.statuses?.some((status) => status.kind === 'SSHKey' && status.phase === 'Synced')) failures.push('model missing identity reconciliation projection');
|
|
200
|
+
if (!model.resources.find((resource) => resource.kind === 'Repository')?.action?.list?.includes('Open Repository records')) failures.push('repository model missing Krate action list command');
|
|
201
|
+
if (!model.controller.endpoints.some((endpoint) => endpoint.method === 'GET' && endpoint.path === '/api/orgs/:org/resources')) failures.push('model missing resource list endpoint');
|
|
202
|
+
if (!model.controller.endpoints.some((endpoint) => endpoint.method === 'POST' && endpoint.path === '/api/orgs/:org/resources')) failures.push('model missing resource apply endpoint');
|
|
203
|
+
if (!model.controller.endpoints.some((endpoint) => endpoint.method === 'GET' && endpoint.path === '/api/orgs/:org/policies')) failures.push('model missing policy center endpoint');
|
|
204
|
+
if (!model.controller.endpoints.some((endpoint) => endpoint.method === 'GET' && endpoint.path === '/api/orgs/:org/policy-exception-requests')) failures.push('model missing policy exception list endpoint');
|
|
205
|
+
if (model.controller.architecture?.apiController?.role !== 'krate-api-controller') failures.push('model missing API controller architecture boundary');
|
|
206
|
+
if (model.controller.architecture?.resourceGateway?.role !== 'krate-resource-gateway') failures.push('model missing resource gateway architecture boundary');
|
|
207
|
+
if (model.controller.architecture?.resourceClient?.role !== 'krate-resource-client') failures.push('model missing Krate client architecture boundary');
|
|
208
|
+
if (model.controller.architecture?.deliveryReconciler?.role !== 'krate-delivery-reconciler') failures.push('model missing delivery reconciler architecture boundary');
|
|
209
|
+
if (!model.controller.architecture?.apiController?.delegatesTo?.includes('krate-resource-gateway')) failures.push('API controller does not delegate to resource gateway');
|
|
210
|
+
if (model.resources.find((resource) => resource.kind === 'Repository')?.items?.[0]?.metadata?.name !== 'live-repo') failures.push('repository model missing live items');
|
|
211
|
+
if (!model.validation.some((item) => item.evidence.includes('/api/orgs/:org/repositories'))) failures.push('validation missing repository management evidence');
|
|
212
|
+
if (model.policyEngine.health !== 'ready') failures.push('policy engine did not project ready Kyverno health');
|
|
213
|
+
if (model.policyEngine.violations.length !== 1) failures.push('policy engine did not normalize Kyverno violations');
|
|
214
|
+
if (!model.policyEngine.exceptionRequests.some((request) => request.name === 'temporary-bypass')) failures.push('policy engine missing exception request projection');
|
|
215
|
+
const emptyModel = createControllerUiModel({ source: 'kubernetes', namespace: 'krate-org-default', kubectl: { available: true, context: 'kind-krate', errors: [] }, apiService: { metadata: { name: 'v1alpha1.krate.a5c.ai' } }, crds: [{ metadata: { name: 'repositories.krate.a5c.ai' } }], resources: { Repository: [] }, commands: [], events: [], permissions: [], storage: {} });
|
|
216
|
+
if (emptyModel.resources.find((resource) => resource.kind === 'Repository')?.yaml !== null) failures.push('empty Krate repository model synthesized plan');
|
|
217
|
+
|
|
218
|
+
if (failures.length) fail(failures);
|
|
219
|
+
console.log(JSON.stringify({
|
|
220
|
+
status: 'success',
|
|
221
|
+
checked: required,
|
|
222
|
+
contract: 'krate-task-led-controller-ui',
|
|
223
|
+
resources: model.metrics.resources,
|
|
224
|
+
endpoints: model.controller.endpoints.length,
|
|
225
|
+
validations: model.metrics.totalChecks
|
|
226
|
+
}, null, 2));
|
|
227
|
+
|
|
228
|
+
function fail(failures) {
|
|
229
|
+
console.error(JSON.stringify({ status: 'failed', failures }, null, 2));
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Agent Adapter Controller — Slice 1.2a / C4
|
|
2
|
+
// Manages AgentAdapter resources: validation, capabilities, transports, and health checks.
|
|
3
|
+
|
|
4
|
+
export const AGENT_ADAPTER_CONTROLLER_BOUNDARY = {
|
|
5
|
+
role: 'agent-adapter-controller',
|
|
6
|
+
scope: 'AgentAdapter lifecycle: validation, capabilities matrix, transport enumeration, real health checks',
|
|
7
|
+
owns: ['adapter validation', 'capabilities matrix', 'transport enumeration', 'health checks'],
|
|
8
|
+
delegatesTo: ['resource-model'],
|
|
9
|
+
mustNotOwn: ['secret values', 'dispatch execution', 'Agent Mux sessions']
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const HEALTH_CHECK_TIMEOUT_MS = 3000;
|
|
13
|
+
|
|
14
|
+
const VALID_TRANSPORTS = ['stdio', 'http', 'websocket', 'unix'];
|
|
15
|
+
const VALID_ADAPTER_TYPES = ['subprocess', 'remote', 'programmatic'];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validate an AgentAdapter resource. Returns { valid, errors }.
|
|
19
|
+
* @param {object} resource
|
|
20
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
21
|
+
*/
|
|
22
|
+
export function validateAgentAdapter(resource) {
|
|
23
|
+
const errors = [];
|
|
24
|
+
|
|
25
|
+
// Guard against null/undefined resource
|
|
26
|
+
if (resource == null) {
|
|
27
|
+
errors.push('resource must not be null or undefined');
|
|
28
|
+
return { valid: false, errors };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Validate metadata.name
|
|
32
|
+
if (!resource?.metadata?.name) {
|
|
33
|
+
errors.push('metadata.name is required');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const spec = resource?.spec || {};
|
|
37
|
+
|
|
38
|
+
// Validate adapterType
|
|
39
|
+
const adapterType = spec.adapterType;
|
|
40
|
+
if (!adapterType) {
|
|
41
|
+
errors.push(`spec.adapterType is required; valid types are: ${VALID_ADAPTER_TYPES.join(', ')}`);
|
|
42
|
+
} else if (!VALID_ADAPTER_TYPES.includes(adapterType)) {
|
|
43
|
+
errors.push(`spec.adapterType "${adapterType}" is not supported; valid types are: ${VALID_ADAPTER_TYPES.join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Validate transport
|
|
47
|
+
const transport = spec.transport;
|
|
48
|
+
if (!transport) {
|
|
49
|
+
errors.push(`spec.transport is required; valid transports are: ${VALID_TRANSPORTS.join(', ')}`);
|
|
50
|
+
} else if (!VALID_TRANSPORTS.includes(transport)) {
|
|
51
|
+
errors.push(`spec.transport "${transport}" is not supported; valid transports are: ${VALID_TRANSPORTS.join(', ')}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Validate capabilities — must be a non-empty array
|
|
55
|
+
const capabilities = spec.capabilities;
|
|
56
|
+
if (!Array.isArray(capabilities) || capabilities.length === 0) {
|
|
57
|
+
errors.push('spec.capabilities must be a non-empty array');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { valid: errors.length === 0, errors };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Perform an HTTP health check against the given URL.
|
|
65
|
+
* Returns { status: 'healthy'|'unhealthy', latencyMs, error? }.
|
|
66
|
+
* @param {string} url
|
|
67
|
+
* @param {Function} fetchFn - injectable fetch (defaults to globalThis.fetch)
|
|
68
|
+
* @returns {Promise<{ status: string, latencyMs: number, error?: string }>}
|
|
69
|
+
*/
|
|
70
|
+
async function performHttpHealthCheck(url, fetchFn) {
|
|
71
|
+
const fn = fetchFn || globalThis.fetch;
|
|
72
|
+
const start = Date.now();
|
|
73
|
+
try {
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
const timer = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT_MS);
|
|
76
|
+
let response;
|
|
77
|
+
try {
|
|
78
|
+
response = await fn(url, { signal: controller.signal });
|
|
79
|
+
} finally {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
}
|
|
82
|
+
const latencyMs = Date.now() - start;
|
|
83
|
+
if (response.ok) {
|
|
84
|
+
return { status: 'healthy', latencyMs };
|
|
85
|
+
}
|
|
86
|
+
return { status: 'unhealthy', latencyMs, error: `HTTP ${response.status}` };
|
|
87
|
+
} catch (err) {
|
|
88
|
+
const latencyMs = Date.now() - start;
|
|
89
|
+
return { status: 'unhealthy', latencyMs, error: err.message || String(err) };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Factory that returns an AgentAdapter controller instance.
|
|
95
|
+
* @param {object} [options]
|
|
96
|
+
* @param {Function} [options.fetch] - injectable fetch function (for testing)
|
|
97
|
+
*/
|
|
98
|
+
export function createAgentAdapterController(options = {}) {
|
|
99
|
+
const fetchFn = options.fetch || null;
|
|
100
|
+
return {
|
|
101
|
+
role: 'agent-adapter-controller',
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validate an AgentAdapter resource.
|
|
105
|
+
* @param {object} resource
|
|
106
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
107
|
+
*/
|
|
108
|
+
validate(resource) {
|
|
109
|
+
return validateAgentAdapter(resource);
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Return the capabilities matrix for an adapter.
|
|
114
|
+
* @param {object} resource
|
|
115
|
+
* @returns {{ adapterName: string, supported: string[] }}
|
|
116
|
+
*/
|
|
117
|
+
getCapabilities(resource) {
|
|
118
|
+
if (resource == null) {
|
|
119
|
+
throw new Error('resource must not be null or undefined');
|
|
120
|
+
}
|
|
121
|
+
const supported = Array.isArray(resource?.spec?.capabilities)
|
|
122
|
+
? [...resource.spec.capabilities]
|
|
123
|
+
: [];
|
|
124
|
+
return {
|
|
125
|
+
adapterName: resource?.metadata?.name,
|
|
126
|
+
supported
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Return the list of supported transport types.
|
|
132
|
+
* @returns {string[]}
|
|
133
|
+
*/
|
|
134
|
+
getSupportedTransports() {
|
|
135
|
+
return [...VALID_TRANSPORTS];
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Return the list of supported adapter types.
|
|
140
|
+
* @returns {string[]}
|
|
141
|
+
*/
|
|
142
|
+
getSupportedAdapterTypes() {
|
|
143
|
+
return [...VALID_ADAPTER_TYPES];
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Perform a health check for an AgentAdapter.
|
|
148
|
+
* If no healthEndpoint is configured in spec, returns { status: 'unknown', reason: 'no-endpoint' }.
|
|
149
|
+
* If a healthEndpoint is configured, performs a real HTTP GET with a 3s timeout.
|
|
150
|
+
* Returns { status: 'healthy'|'unhealthy', latencyMs, error? } or { status: 'unknown', reason }.
|
|
151
|
+
* @param {object} resource
|
|
152
|
+
* @returns {Promise<{ adapterName: string, status: string, latencyMs?: number, reason?: string, error?: string }>}
|
|
153
|
+
*/
|
|
154
|
+
async healthCheck(resource) {
|
|
155
|
+
if (resource == null) {
|
|
156
|
+
throw new Error('resource must not be null or undefined');
|
|
157
|
+
}
|
|
158
|
+
const adapterName = resource?.metadata?.name;
|
|
159
|
+
const endpoint = resource?.spec?.healthEndpoint;
|
|
160
|
+
|
|
161
|
+
if (!endpoint) {
|
|
162
|
+
return { adapterName, status: 'unknown', reason: 'no-endpoint' };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const checkResult = await performHttpHealthCheck(endpoint, fetchFn);
|
|
166
|
+
return { adapterName, ...checkResult };
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|