@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,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,207 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createControllerUiModel } from '../src/index.js';
|
|
3
|
+
|
|
4
|
+
const required = [
|
|
5
|
+
'apps/web/app/layout.jsx',
|
|
6
|
+
'apps/web/app/page.jsx',
|
|
7
|
+
'apps/web/app/ui-shell.jsx',
|
|
8
|
+
'apps/web/proxy.js',
|
|
9
|
+
'apps/web/app/components/code-editor.jsx',
|
|
10
|
+
'apps/web/app/components/resource-actions.jsx',
|
|
11
|
+
'apps/web/app/api/controller/route.js',
|
|
12
|
+
'apps/web/app/api/orgs/[org]/resources/route.js',
|
|
13
|
+
'apps/web/app/api/orgs/[org]/resources/[kind]/[name]/route.js',
|
|
14
|
+
'apps/web/app/api/orgs/[org]/repositories/route.js',
|
|
15
|
+
'apps/web/app/api/orgs/[org]/repositories/[name]/route.js',
|
|
16
|
+
'apps/web/app/api/orgs/[org]/policies/route.js',
|
|
17
|
+
'apps/web/app/api/orgs/[org]/policy-reports/route.js',
|
|
18
|
+
'apps/web/app/api/orgs/[org]/policy-exception-requests/route.js',
|
|
19
|
+
'apps/web/app/api/watch/[[...resource]]/route.js',
|
|
20
|
+
'apps/web/app/api/git-proxy/route.js',
|
|
21
|
+
'apps/web/app/api/auth/[provider]/route.js',
|
|
22
|
+
'apps/web/app/api/auth/callback/[provider]/route.js',
|
|
23
|
+
'apps/web/app/api/auth/logout/route.js',
|
|
24
|
+
'apps/web/app/api/auth/delegated/route.js',
|
|
25
|
+
'src/api-controller.js',
|
|
26
|
+
'src/kubernetes-resource-gateway.js',
|
|
27
|
+
'src/kubernetes-controller.js',
|
|
28
|
+
'src/controller-client.js',
|
|
29
|
+
'src/controller-ui.js',
|
|
30
|
+
'src/http-server.js'
|
|
31
|
+
];
|
|
32
|
+
const files = Object.fromEntries(required.map((file) => [file, readFileSync(file, 'utf8')]));
|
|
33
|
+
const failures = [];
|
|
34
|
+
|
|
35
|
+
for (const [file, source] of Object.entries(files)) {
|
|
36
|
+
if (!source.trim()) failures.push(`${file} is empty`);
|
|
37
|
+
}
|
|
38
|
+
for (const file of ['apps/web/app/api/controller/route.js', 'apps/web/app/api/watch/[[...resource]]/route.js', 'src/controller-client.js']) {
|
|
39
|
+
if (files[file].includes('createKrateUiDemoRuntime')) failures.push(`${file} imports or calls createKrateUiDemoRuntime`);
|
|
40
|
+
if (files[file].includes('createKrateRuntime()')) failures.push(`${file} creates an in-memory runtime fallback`);
|
|
41
|
+
}
|
|
42
|
+
for (const file of ['apps/web/app/page.jsx', 'apps/web/app/ui-shell.jsx']) {
|
|
43
|
+
if (files[file].includes('krate-demo')) failures.push(`${file} hardcodes krate-demo demo navigation`);
|
|
44
|
+
}
|
|
45
|
+
for (const [file, source] of Object.entries(files)) {
|
|
46
|
+
if (source.includes('sampleResource') || source.includes('exampleResource')) failures.push(`${file} synthesizes sample/example Krate resources`);
|
|
47
|
+
if (source.includes('new-repository') && file !== 'apps/web/app/components/resource-actions.jsx') failures.push(`${file} hardcodes synthetic Repository data`);
|
|
48
|
+
}
|
|
49
|
+
for (const token of ['export function proxy', 'NextResponse.redirect', 'KRATE_AUTH_COOKIE_NAME', 'krate_session', '/login', '/api/auth', 'matcher']) {
|
|
50
|
+
if (!files['apps/web/proxy.js'].includes(token)) failures.push(`web proxy missing ${token}`);
|
|
51
|
+
}
|
|
52
|
+
for (const token of ['spawnSync', 'spawn(', 'kubectl', 'getControllerSnapshot', 'listResource', 'getResource', 'applyResource', 'deleteResource', 'createRepository', 'watchResource', 'auth', 'can-i']) {
|
|
53
|
+
if (!files['src/kubernetes-controller.js'].includes(token)) failures.push(`kubernetes controller missing ${token}`);
|
|
54
|
+
}
|
|
55
|
+
for (const token of ['createKubernetesResourceGateway', 'controller.snapshot()', 'createControllerUiModel']) {
|
|
56
|
+
if (!files['src/controller-client.js'].includes(token)) failures.push(`controller client missing ${token}`);
|
|
57
|
+
}
|
|
58
|
+
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']) {
|
|
59
|
+
if (!files['src/api-controller.js'].includes(token)) failures.push(`api controller boundary missing ${token}`);
|
|
60
|
+
}
|
|
61
|
+
for (const token of ['createKubernetesResourceClient', 'repositoryManifest', 'async list', 'async get', 'async apply', 'async delete', 'watch(resourcePath', 'KUBERNETES_RESOURCE_GATEWAY_BOUNDARY', 'mustNotOwn']) {
|
|
62
|
+
if (!files['src/kubernetes-resource-gateway.js'].includes(token)) failures.push(`kubernetes resource gateway missing ${token}`);
|
|
63
|
+
}
|
|
64
|
+
for (const token of ['GET', 'POST', 'DELETE', 'controller.listResource', 'controller.getResource', 'controller.applyResource', 'controller.deleteResource', 'controller.createRepository']) {
|
|
65
|
+
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'];
|
|
66
|
+
if (!joinedRoutes.includes(token)) failures.push(`resource management routes missing ${token}`);
|
|
67
|
+
}
|
|
68
|
+
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'];
|
|
69
|
+
for (const token of ['GET', 'POST', 'createKrateApiController', 'createControllerUiModel', 'policyEngine', 'controller.applyResource', 'PolicyBinding', 'PolicyExceptionRequest']) {
|
|
70
|
+
if (!policyRoutes.includes(token)) failures.push(`policy management routes missing ${token}`);
|
|
71
|
+
}
|
|
72
|
+
for (const token of ['--watch', 'text/event-stream', 'controller.watchResource', 'krate-error', 'request.signal']) {
|
|
73
|
+
if (!files['apps/web/app/api/watch/[[...resource]]/route.js'].includes(token)) failures.push(`watch route missing ${token}`);
|
|
74
|
+
}
|
|
75
|
+
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']) {
|
|
76
|
+
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}`);
|
|
77
|
+
}
|
|
78
|
+
for (const token of ['DegradedBanner', 'No repositories are available yet.', 'No resource selected yet.', 'Access checks', 'KRATE_CONTROLLER_URL', 'Krate repositories']) {
|
|
79
|
+
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}`);
|
|
80
|
+
}
|
|
81
|
+
for (const token of ['duplex', 'KRATE_GITEA_HTTP_URL', 'fetch(target', 'degraded']) {
|
|
82
|
+
if (!files['apps/web/app/api/git-proxy/route.js'].includes(token)) failures.push(`git proxy route missing ${token}`);
|
|
83
|
+
}
|
|
84
|
+
for (const token of ['orgResourceCollectionMatch', 'orgRepositoryCollectionMatch', 'orgNamespaceName', 'createKubernetesResourceGateway', 'scopedController.listResource', 'scopedController.getResource', 'scopedController.applyResource', 'scopedController.deleteResource']) {
|
|
85
|
+
if (!files['src/http-server.js'].includes(token)) failures.push(`http controller missing ${token}`);
|
|
86
|
+
}
|
|
87
|
+
const pageContracts = {
|
|
88
|
+
'apps/web/app/orgs/[org]/controller-api/page.jsx': 'ControllerApiPage',
|
|
89
|
+
'apps/web/app/orgs/[org]/repositories/page.jsx': 'RepositoriesPage',
|
|
90
|
+
'apps/web/app/orgs/[org]/inbox/page.jsx': 'InboxPage',
|
|
91
|
+
'apps/web/app/orgs/[org]/runs/page.jsx': 'RunsPage',
|
|
92
|
+
'apps/web/app/orgs/[org]/runners-ci/page.jsx': 'RunnersCiPage',
|
|
93
|
+
'apps/web/app/orgs/[org]/hooks-events/page.jsx': 'HooksEventsPage',
|
|
94
|
+
'apps/web/app/orgs/[org]/insights/page.jsx': 'InsightsPage',
|
|
95
|
+
'apps/web/app/orgs/[org]/operations-install/page.jsx': 'OperationsInstallPage',
|
|
96
|
+
'apps/web/app/orgs/[org]/advanced-plans/page.jsx': 'AdvancedPlansPage',
|
|
97
|
+
'apps/web/app/orgs/[org]/people/page.jsx': 'PeoplePage',
|
|
98
|
+
'apps/web/app/login/page.jsx': 'LoginPage',
|
|
99
|
+
'apps/web/app/logout/page.jsx': 'LogoutPage',
|
|
100
|
+
'apps/web/app/orgs/[org]/repositories/[repo]/code/page.jsx': 'RepositoryCodePage',
|
|
101
|
+
'apps/web/app/orgs/[org]/repositories/[repo]/pull-requests/page.jsx': 'RepositoryPullRequestsPage',
|
|
102
|
+
'apps/web/app/orgs/[org]/repositories/[repo]/issues/page.jsx': 'RepositoryIssuesPage',
|
|
103
|
+
'apps/web/app/orgs/[org]/repositories/[repo]/runs/page.jsx': 'RepositoryRunsPage',
|
|
104
|
+
'apps/web/app/orgs/[org]/repositories/[repo]/hooks/page.jsx': 'RepositoryHooksPage',
|
|
105
|
+
'apps/web/app/orgs/[org]/repositories/[repo]/settings/page.jsx': 'RepositorySettingsPage'
|
|
106
|
+
};
|
|
107
|
+
for (const file of Object.keys(pageContracts)) {
|
|
108
|
+
files[file] = readFileSync(file, 'utf8');
|
|
109
|
+
}
|
|
110
|
+
for (const [file, component] of Object.entries(pageContracts)) {
|
|
111
|
+
if (!files[file].includes(component)) failures.push(`${file} does not use dedicated ${component} route component`);
|
|
112
|
+
}
|
|
113
|
+
if (required.some((file) => file.includes('/pipelines'))) failures.push('legacy pipelines route is still required');
|
|
114
|
+
for (const token of ['ControllerApiPage', 'RepositoriesPage', 'InboxPage', 'RunsPage', 'RunnersCiPage', 'HooksEventsPage', 'InsightsPage', 'OperationsInstallPage', 'AdvancedPlansPage', 'PeoplePage', 'LoginPage', 'LogoutPage', 'RepositoryCodePage', 'RepositoryPullRequestsPage', 'RepositoryIssuesPage', 'RepositoryRunsPage', 'RepositoryHooksPage', 'RepositorySettingsPage']) {
|
|
115
|
+
if (!files['apps/web/app/ui-shell.jsx'].includes(token)) failures.push(`ui shell missing dedicated flow component ${token}`);
|
|
116
|
+
}
|
|
117
|
+
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']) {
|
|
118
|
+
if (!files['apps/web/app/ui-shell.jsx'].includes(token)) failures.push(`ui shell missing forge UX affordance ${token}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const model = createControllerUiModel({
|
|
122
|
+
source: 'kubernetes',
|
|
123
|
+
namespace: 'krate-org-default',
|
|
124
|
+
generatedAt: 'test-time',
|
|
125
|
+
correlationId: 'validation',
|
|
126
|
+
kubectl: { available: true, context: 'kind-krate', clientVersion: 'v1.test', errors: [] },
|
|
127
|
+
apiService: { metadata: { name: 'v1alpha1.krate.a5c.ai' } },
|
|
128
|
+
crds: [{ metadata: { name: 'repositories.krate.a5c.ai' } }],
|
|
129
|
+
storage: { etcd: 'etcd', postgres: 'postgres', repositories: 'rwx', objects: 'object' },
|
|
130
|
+
commands: [],
|
|
131
|
+
permissions: [],
|
|
132
|
+
events: [],
|
|
133
|
+
resources: {
|
|
134
|
+
Organization: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'Organization', metadata: { name: 'default', namespace: 'krate-system' }, spec: { slug: 'default', namespaceName: 'krate-org-default', displayName: 'Default org' } }],
|
|
135
|
+
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' } }],
|
|
136
|
+
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' } }],
|
|
137
|
+
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' } }],
|
|
138
|
+
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' } }],
|
|
139
|
+
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' } }],
|
|
140
|
+
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' } }],
|
|
141
|
+
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' } }],
|
|
142
|
+
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' } }],
|
|
143
|
+
PullRequest: [],
|
|
144
|
+
Pipeline: [],
|
|
145
|
+
RunnerPool: [],
|
|
146
|
+
WebhookSubscription: []
|
|
147
|
+
},
|
|
148
|
+
kyverno: {
|
|
149
|
+
enabled: true,
|
|
150
|
+
detected: true,
|
|
151
|
+
mode: 'byo',
|
|
152
|
+
namespace: 'kyverno',
|
|
153
|
+
policyNamespace: 'krate-system',
|
|
154
|
+
health: 'ready',
|
|
155
|
+
degraded: [],
|
|
156
|
+
reports: {
|
|
157
|
+
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' }] }] }],
|
|
158
|
+
clusterPolicyReports: [],
|
|
159
|
+
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' } }],
|
|
160
|
+
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' } }]
|
|
161
|
+
},
|
|
162
|
+
resources: { PolicyReport: [], ClusterPolicyReport: [], KyvernoPolicyException: [] }
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
if (model.controller.mode !== 'krate-workspace') failures.push('controller mode is not krate-workspace');
|
|
166
|
+
if (model.status !== 'ready') failures.push('model did not become ready for available Krate snapshot');
|
|
167
|
+
if (!model.resources.find((resource) => resource.kind === 'User')) failures.push('model missing User identity resource');
|
|
168
|
+
if (!model.identity || typeof model.identity.counts?.users !== 'number') failures.push('model missing identity admin projection');
|
|
169
|
+
if (model.identity.counts.sshKeys !== 1) failures.push('model missing SSH key identity projection');
|
|
170
|
+
if (!model.identity.reconciliation?.statuses?.some((status) => status.kind === 'SSHKey' && status.phase === 'Synced')) failures.push('model missing identity reconciliation projection');
|
|
171
|
+
if (!model.resources.find((resource) => resource.kind === 'Repository')?.action?.list?.includes('Open Repository records')) failures.push('repository model missing Krate action list command');
|
|
172
|
+
if (!model.controller.endpoints.some((endpoint) => endpoint.method === 'GET' && endpoint.path === '/api/orgs/:org/resources')) failures.push('model missing resource list endpoint');
|
|
173
|
+
if (!model.controller.endpoints.some((endpoint) => endpoint.method === 'POST' && endpoint.path === '/api/orgs/:org/resources')) failures.push('model missing resource apply endpoint');
|
|
174
|
+
if (!model.controller.endpoints.some((endpoint) => endpoint.method === 'GET' && endpoint.path === '/api/orgs/:org/policies')) failures.push('model missing policy center endpoint');
|
|
175
|
+
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');
|
|
176
|
+
if (model.controller.architecture?.apiController?.role !== 'krate-api-controller') failures.push('model missing API controller architecture boundary');
|
|
177
|
+
if (model.controller.architecture?.resourceGateway?.role !== 'krate-resource-gateway') failures.push('model missing resource gateway architecture boundary');
|
|
178
|
+
if (model.controller.architecture?.resourceClient?.role !== 'krate-resource-client') failures.push('model missing Krate client architecture boundary');
|
|
179
|
+
if (model.controller.architecture?.deliveryReconciler?.role !== 'krate-delivery-reconciler') failures.push('model missing delivery reconciler architecture boundary');
|
|
180
|
+
if (!model.controller.architecture?.apiController?.delegatesTo?.includes('krate-resource-gateway')) failures.push('API controller does not delegate to resource gateway');
|
|
181
|
+
if (model.resources.find((resource) => resource.kind === 'Repository')?.items?.[0]?.metadata?.name !== 'live-repo') failures.push('repository model missing live items');
|
|
182
|
+
if (!model.validation.some((item) => item.evidence.includes('/api/orgs/:org/repositories'))) failures.push('validation missing repository management evidence');
|
|
183
|
+
if (model.policyEngine.health !== 'ready') failures.push('policy engine did not project ready Kyverno health');
|
|
184
|
+
if (model.policyEngine.violations.length !== 1) failures.push('policy engine did not normalize Kyverno violations');
|
|
185
|
+
if (!model.policyEngine.exceptionRequests.some((request) => request.name === 'temporary-bypass')) failures.push('policy engine missing exception request projection');
|
|
186
|
+
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: {} });
|
|
187
|
+
if (emptyModel.resources.find((resource) => resource.kind === 'Repository')?.yaml !== null) failures.push('empty Krate repository model synthesized plan');
|
|
188
|
+
|
|
189
|
+
if (failures.length) fail(failures);
|
|
190
|
+
console.log(JSON.stringify({
|
|
191
|
+
status: 'success',
|
|
192
|
+
checked: required,
|
|
193
|
+
contract: 'krate-task-led-controller-ui',
|
|
194
|
+
resources: model.metrics.resources,
|
|
195
|
+
endpoints: model.controller.endpoints.length,
|
|
196
|
+
validations: model.metrics.totalChecks
|
|
197
|
+
}, null, 2));
|
|
198
|
+
|
|
199
|
+
function fail(failures) {
|
|
200
|
+
console.error(JSON.stringify({ status: 'failed', failures }, null, 2));
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createResource, clone } from './resource-model.js';
|
|
2
|
+
|
|
3
|
+
export const AGENT_APPROVAL_CONTROLLER_BOUNDARY = {
|
|
4
|
+
role: 'agent-approval-controller',
|
|
5
|
+
scope: 'Human gate lifecycle for agent tool-use, secret-access, write-back, release, and escalation actions',
|
|
6
|
+
owns: ['approval creation', 'decision recording', 'approval lookup', 'duplicate detection'],
|
|
7
|
+
delegatesTo: ['resource-model'],
|
|
8
|
+
mustNotOwn: ['secret values', 'agent execution', 'UI rendering']
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const VALID_ACTIONS = new Set(['tool-use', 'secret-access', 'write-back', 'release', 'escalation']);
|
|
12
|
+
|
|
13
|
+
export function createAgentApprovalController() {
|
|
14
|
+
return {
|
|
15
|
+
role: 'agent-approval-controller',
|
|
16
|
+
|
|
17
|
+
createApprovalRequest({ dispatchRun, action, requestedBy, context, namespace = 'default', organizationRef = 'default', resources = {} }) {
|
|
18
|
+
if (!action || !VALID_ACTIONS.has(action)) {
|
|
19
|
+
return { error: true, reason: 'invalid-action', message: `Invalid action type: ${action}. Must be one of: ${[...VALID_ACTIONS].join(', ')}` };
|
|
20
|
+
}
|
|
21
|
+
if (!dispatchRun) {
|
|
22
|
+
return { error: true, reason: 'missing-dispatch-run', message: 'dispatchRun is required' };
|
|
23
|
+
}
|
|
24
|
+
if (!requestedBy) {
|
|
25
|
+
return { error: true, reason: 'missing-requested-by', message: 'requestedBy is required' };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check for duplicate pending approval
|
|
29
|
+
const existing = (resources.AgentApproval || []).find(
|
|
30
|
+
(a) => a.spec?.dispatchRun === dispatchRun && a.spec?.action === action && (!a.status?.phase || a.status.phase === 'Pending')
|
|
31
|
+
);
|
|
32
|
+
if (existing) {
|
|
33
|
+
return { error: false, approval: clone(existing), duplicate: true };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const now = new Date().toISOString();
|
|
37
|
+
const approvalName = `approval-${dispatchRun}-${action}-${Date.now()}`;
|
|
38
|
+
|
|
39
|
+
const approval = createResource('AgentApproval', { name: approvalName, namespace }, {
|
|
40
|
+
organizationRef,
|
|
41
|
+
dispatchRun,
|
|
42
|
+
action,
|
|
43
|
+
requestedBy,
|
|
44
|
+
description: context || `Agent requests permission to perform: ${action}`,
|
|
45
|
+
requestedAt: now
|
|
46
|
+
});
|
|
47
|
+
approval.status = { phase: 'Pending', createdAt: now };
|
|
48
|
+
|
|
49
|
+
return { error: false, approval, duplicate: false };
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
recordDecision({ approvalName, decision, decidedBy, reason, namespace = 'default', organizationRef = 'default', resources = {} }) {
|
|
53
|
+
if (!approvalName) {
|
|
54
|
+
return { error: true, reason: 'missing-approval-name', message: 'approvalName is required' };
|
|
55
|
+
}
|
|
56
|
+
if (!decision || (decision !== 'approve' && decision !== 'deny')) {
|
|
57
|
+
return { error: true, reason: 'invalid-decision', message: `Invalid decision: ${decision}. Must be 'approve' or 'deny'` };
|
|
58
|
+
}
|
|
59
|
+
if (!decidedBy) {
|
|
60
|
+
return { error: true, reason: 'missing-decided-by', message: 'decidedBy is required' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const approvals = resources.AgentApproval || [];
|
|
64
|
+
const approval = approvals.find((a) => a.metadata?.name === approvalName);
|
|
65
|
+
if (!approval) {
|
|
66
|
+
return { error: true, reason: 'not-found', message: `AgentApproval not found: ${approvalName}` };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (approval.status?.phase && approval.status.phase !== 'Pending') {
|
|
70
|
+
return { error: true, reason: 'already-decided', message: `AgentApproval ${approvalName} has already been decided: ${approval.status.phase}` };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const now = new Date().toISOString();
|
|
74
|
+
const phase = decision === 'approve' ? 'Approved' : 'Denied';
|
|
75
|
+
|
|
76
|
+
const updated = clone(approval);
|
|
77
|
+
updated.status = {
|
|
78
|
+
...updated.status,
|
|
79
|
+
phase,
|
|
80
|
+
decidedBy,
|
|
81
|
+
decidedAt: now,
|
|
82
|
+
reason: reason || undefined
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return { error: false, approval: updated };
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
isActionApproved({ dispatchRun, action, resources = {} }) {
|
|
89
|
+
const approvals = resources.AgentApproval || [];
|
|
90
|
+
const match = approvals.find(
|
|
91
|
+
(a) => a.spec?.dispatchRun === dispatchRun && a.spec?.action === action
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (!match) {
|
|
95
|
+
return { approved: false, approval: null, reason: 'No approval request found' };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (match.status?.phase === 'Approved') {
|
|
99
|
+
return { approved: true, approval: clone(match), reason: match.status.reason || 'Approved' };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (match.status?.phase === 'Denied') {
|
|
103
|
+
return { approved: false, approval: clone(match), reason: match.status.reason || 'Denied' };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { approved: false, approval: clone(match), reason: 'Approval is still pending' };
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
listPendingApprovals({ organizationRef, resources = {} }) {
|
|
110
|
+
const approvals = resources.AgentApproval || [];
|
|
111
|
+
return approvals.filter((a) => {
|
|
112
|
+
const matchesOrg = !organizationRef || a.spec?.organizationRef === organizationRef;
|
|
113
|
+
const isPending = !a.status?.phase || a.status.phase === 'Pending';
|
|
114
|
+
return matchesOrg && isPending;
|
|
115
|
+
}).map(clone);
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
listApprovalsForRun({ dispatchRun, resources = {} }) {
|
|
119
|
+
const approvals = resources.AgentApproval || [];
|
|
120
|
+
return approvals.filter((a) => a.spec?.dispatchRun === dispatchRun).map(clone);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|