@a5c-ai/krate 5.0.1-staging.3a341c33c
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 +2455 -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/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-memory-controller.js +374 -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/agent-workspace-controller.js +208 -0
- package/src/api-controller.js +248 -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 +551 -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 +32 -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 +211 -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-memory-controller.test.js +308 -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 +228 -0
- package/tests/agent-stack-controller.test.js +221 -0
- package/tests/agent-trigger-controller.test.js +211 -0
- package/tests/agent-workspace-controller.test.js +215 -0
- package/tests/deployment.test.js +393 -0
- package/tests/e2e/lifecycle.test.js +117 -0
- package/tests/krate.test.js +727 -0
package/src/runtime.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { ControlPlane } from './control-plane.js';
|
|
2
|
+
import { GiteaGitService, GiteaRepositoryStore } from './data-plane.js';
|
|
3
|
+
import { WebhookBus } from './hooks-events.js';
|
|
4
|
+
import { createAdmissionPolicy, mapOidcIdentity } from './identity-policy.js';
|
|
5
|
+
import { createInviteResource, createTeamResource, createAuthProviderResources, mapLoginProfileToKrateIdentity } from './auth.js';
|
|
6
|
+
import { createResource, clone } from './resource-model.js';
|
|
7
|
+
import { RunnerScheduler } from './runners-ci.js';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_ORG = 'default';
|
|
10
|
+
const DEFAULT_NAMESPACE = 'krate-org-default';
|
|
11
|
+
|
|
12
|
+
export function createDefaultKrateUsers() {
|
|
13
|
+
return {
|
|
14
|
+
platformEngineer: mapOidcIdentity({ subject: 'user:platform', email: 'platform@example.com', groups: ['krate:platform-engineers'] }),
|
|
15
|
+
developer: mapOidcIdentity({ subject: 'user:dev', email: 'dev@example.com', groups: ['krate:developers'] }),
|
|
16
|
+
repoAdmin: mapOidcIdentity({ subject: 'user:admin', email: 'admin@example.com', groups: ['krate:repo-admins'] })
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class KrateRuntime {
|
|
21
|
+
constructor({ organizationRef = DEFAULT_ORG, namespace = DEFAULT_NAMESPACE, users = createDefaultKrateUsers(), controlPlane, git, runners, webhooks } = {}) {
|
|
22
|
+
this.organizationRef = organizationRef;
|
|
23
|
+
this.namespace = namespace;
|
|
24
|
+
this.users = users;
|
|
25
|
+
this.controlPlane = controlPlane || new ControlPlane();
|
|
26
|
+
this.controlPlane.addAdmissionPolicy(createAdmissionPolicy({
|
|
27
|
+
name: 'pr-title-required',
|
|
28
|
+
mode: 'enforce',
|
|
29
|
+
match: ({ resource }) => resource.kind === 'PullRequest',
|
|
30
|
+
validate: ({ resource }) => Boolean(resource.spec.title && resource.spec.title.length >= 8),
|
|
31
|
+
message: 'PullRequest spec.title must be descriptive'
|
|
32
|
+
}));
|
|
33
|
+
this.git = git || new GiteaGitService({ controlPlane: this.controlPlane, stores: [new GiteaRepositoryStore({ name: 'gitea-primary', receivePackReady: true })] });
|
|
34
|
+
this.runners = runners || new RunnerScheduler({ controlPlane: this.controlPlane });
|
|
35
|
+
this.webhooks = webhooks || new WebhookBus({ controlPlane: this.controlPlane });
|
|
36
|
+
this.seeded = false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static fromSnapshot(snapshot, options = {}) {
|
|
40
|
+
const runtime = new KrateRuntime(options);
|
|
41
|
+
runtime.importSnapshot(snapshot);
|
|
42
|
+
return runtime;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
bootstrap({ repository = 'krate-demo', webhookUrl = 'https://hooks.example.test/krate' } = {}) {
|
|
46
|
+
if (this.seeded) return this.snapshot();
|
|
47
|
+
const { platformEngineer, repoAdmin } = this.users;
|
|
48
|
+
this.git.createRepository({ name: repository, namespace: this.namespace, organizationRef: this.organizationRef }, repoAdmin);
|
|
49
|
+
this.controlPlane.create(createResource('BranchProtection', { name: 'main-protection', namespace: this.namespace }, { organizationRef: this.organizationRef, refs: ['refs/heads/main'], requirePullRequest: true, requiredChecks: ['test'], requiredApprovals: 1 }), repoAdmin);
|
|
50
|
+
this.controlPlane.create(createResource('RefPolicy', { name: 'deny-internal-refs', namespace: this.namespace }, { organizationRef: this.organizationRef, deny: ['refs/internal/'] }), repoAdmin);
|
|
51
|
+
this.runners.createRunnerPool({ name: 'trusted-linux', namespace: this.namespace, organizationRef: this.organizationRef, warmReplicas: 1, maxReplicas: 4 }, platformEngineer);
|
|
52
|
+
this.webhooks.subscribe({ name: 'chatops', namespace: this.namespace, organizationRef: this.organizationRef, url: webhookUrl, events: ['pullrequest.created', 'pullrequest.merged'] }, repoAdmin);
|
|
53
|
+
this.controlPlane.create(createTeamResource({ name: 'maintainers', organizationRef: this.organizationRef, displayName: 'Maintainers', members: ['admin'], maintainers: ['admin'], repositoryGrants: [{ repository, permission: 'admin' }], namespace: this.namespace }), platformEngineer);
|
|
54
|
+
const identity = mapLoginProfileToKrateIdentity({ provider: 'sso', subject: 'user:admin', email: 'admin@example.com', displayName: 'Admin', username: 'admin', teams: ['maintainers'], admin: true, namespace: this.namespace, organizationRef: this.organizationRef });
|
|
55
|
+
this.controlPlane.create(identity.user, platformEngineer);
|
|
56
|
+
this.controlPlane.create(identity.mapping, platformEngineer);
|
|
57
|
+
this.controlPlane.create(createInviteResource({ email: 'new-user@example.com', role: 'member', teams: ['maintainers'], invitedBy: 'admin', namespace: this.namespace, organizationRef: this.organizationRef }), repoAdmin);
|
|
58
|
+
for (const provider of createAuthProviderResources(undefined, this.namespace, this.organizationRef)) this.controlPlane.create(provider, platformEngineer);
|
|
59
|
+
this.seeded = true;
|
|
60
|
+
return this.snapshot();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
exportSnapshot() {
|
|
64
|
+
return {
|
|
65
|
+
apiVersion: 'krate.a5c.ai/v1alpha1',
|
|
66
|
+
kind: 'KrateRuntimeSnapshot',
|
|
67
|
+
namespace: this.namespace,
|
|
68
|
+
organizationRef: this.organizationRef,
|
|
69
|
+
seeded: this.seeded,
|
|
70
|
+
controlPlane: this.controlPlane.exportSnapshot(),
|
|
71
|
+
git: this.git.snapshot?.() || null
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
importSnapshot(snapshot) {
|
|
76
|
+
const controlPlaneSnapshot = snapshot?.controlPlane || snapshot;
|
|
77
|
+
this.controlPlane.importSnapshot(controlPlaneSnapshot);
|
|
78
|
+
if (snapshot?.git) this.git.importSnapshot(snapshot.git);
|
|
79
|
+
this.seeded = Boolean(snapshot?.seeded || this.controlPlane.list('Repository', { namespace: this.namespace }).items.length);
|
|
80
|
+
return this.snapshot();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
createRepository({ name, visibility = 'private' }, user = this.users.repoAdmin) {
|
|
84
|
+
return this.git.createRepository({ name, namespace: this.namespace, organizationRef: this.organizationRef, visibility }, user);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
createPullRequest({ repository, sourceRef = 'refs/heads/feature', targetRef = 'refs/heads/main', title, actor = this.users.developer, fork = false }) {
|
|
88
|
+
this.#requireRepository(repository);
|
|
89
|
+
const index = this.controlPlane.list('PullRequest', { namespace: this.namespace }).items.length + 1;
|
|
90
|
+
const name = `pr-${index}`;
|
|
91
|
+
const pullRequest = this.controlPlane.create(createResource('PullRequest', { name, namespace: this.namespace, labels: { repository } }, {
|
|
92
|
+
organizationRef: this.organizationRef,
|
|
93
|
+
repository,
|
|
94
|
+
sourceRef,
|
|
95
|
+
targetRef,
|
|
96
|
+
title,
|
|
97
|
+
author: actor.name,
|
|
98
|
+
checks: [],
|
|
99
|
+
requiredApprovals: this.#requiredApprovals(targetRef)
|
|
100
|
+
}, { phase: 'Open', approvals: 0, mergeable: false }), actor);
|
|
101
|
+
const pipelineRun = this.runners.startPipeline({ name: `pipeline-${name}`, namespace: this.namespace, organizationRef: this.organizationRef, repository, ref: `refs/pull/${index}/head`, actor, fork, steps: ['checkout', 'test'] }, actor);
|
|
102
|
+
this.webhooks.deliver({ subscriptionName: 'chatops', namespace: this.namespace, organizationRef: this.organizationRef, eventType: 'pullrequest.created', payload: { pullRequest: name, repository, title } }, this.users.repoAdmin);
|
|
103
|
+
return { pullRequest, pipeline: pipelineRun.pipeline, jobs: pipelineRun.jobs };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
completePipeline({ pipeline, phase = 'Succeeded', failedStep = null }, user = this.users.developer) {
|
|
107
|
+
const existing = this.controlPlane.get('Pipeline', this.namespace, pipeline);
|
|
108
|
+
if (!existing) throw new Error(`Pipeline ${pipeline} not found`);
|
|
109
|
+
const jobs = this.controlPlane.list('Job', { namespace: this.namespace, labels: { pipeline } }).items.map((job) => {
|
|
110
|
+
const jobPhase = failedStep && job.spec.step === failedStep ? 'Failed' : phase;
|
|
111
|
+
return this.controlPlane.patchStatus('Job', this.namespace, job.metadata.name, { phase: jobPhase, finishedAt: new Date().toISOString() }, user);
|
|
112
|
+
});
|
|
113
|
+
const pipelinePhase = jobs.some((job) => job.status.phase === 'Failed') ? 'Failed' : phase;
|
|
114
|
+
const updatedPipeline = this.controlPlane.patchStatus('Pipeline', this.namespace, pipeline, { phase: pipelinePhase, currentStep: null, finishedAt: new Date().toISOString() }, user);
|
|
115
|
+
this.#refreshPullRequestMergeability(existing.spec.repository);
|
|
116
|
+
return { pipeline: updatedPipeline, jobs };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
addReview({ pullRequest, decision = 'approved', body = 'Looks good', reviewer = this.users.repoAdmin }) {
|
|
120
|
+
const pr = this.#requirePullRequest(pullRequest);
|
|
121
|
+
const reviewCount = this.controlPlane.list('Review', { namespace: this.namespace, labels: { pullRequest } }).items.length + 1;
|
|
122
|
+
const review = this.controlPlane.create(createResource('Review', { name: `${pullRequest}-review-${reviewCount}`, namespace: this.namespace, labels: { pullRequest, decision } }, {
|
|
123
|
+
organizationRef: this.organizationRef,
|
|
124
|
+
pullRequest,
|
|
125
|
+
repository: pr.spec.repository,
|
|
126
|
+
reviewer: reviewer.name,
|
|
127
|
+
decision,
|
|
128
|
+
body
|
|
129
|
+
}, { phase: decision === 'approved' ? 'Approved' : 'ChangesRequested' }), reviewer);
|
|
130
|
+
this.#refreshPullRequestMergeability(pr.spec.repository);
|
|
131
|
+
return review;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
mergePullRequest({ pullRequest, actor = this.users.repoAdmin }) {
|
|
135
|
+
const pr = this.#requirePullRequest(pullRequest);
|
|
136
|
+
const refreshed = this.#refreshPullRequestMergeability(pr.spec.repository).find((candidate) => candidate.metadata.name === pullRequest) || pr;
|
|
137
|
+
if (!refreshed.status.mergeable) throw new Error(`PullRequest ${pullRequest} is not mergeable`);
|
|
138
|
+
const gitEvent = this.git.receivePack({ repository: refreshed.spec.repository, namespace: this.namespace, ref: refreshed.spec.targetRef, newRev: this.#syntheticRevision(pullRequest), actor });
|
|
139
|
+
const merged = this.controlPlane.patchStatus('PullRequest', this.namespace, pullRequest, { phase: 'Merged', mergeable: false, mergedAt: new Date().toISOString(), mergeCommit: gitEvent.newRev }, actor);
|
|
140
|
+
const delivery = this.webhooks.deliver({ subscriptionName: 'chatops', namespace: this.namespace, organizationRef: this.organizationRef, eventType: 'pullrequest.merged', payload: { pullRequest, repository: refreshed.spec.repository, mergeCommit: gitEvent.newRev } }, this.users.repoAdmin);
|
|
141
|
+
return { pullRequest: merged, gitEvent, delivery };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
snapshot() {
|
|
145
|
+
const kinds = ['Organization', 'User', 'Team', 'Invite', 'IdentityMapping', 'AuthProvider', 'Repository', 'SSHKey', 'RepositoryPermission', 'BranchProtection', 'RefPolicy', 'RunnerPool', 'PullRequest', 'Issue', 'Review', 'Pipeline', 'Job', 'WebhookSubscription', 'WebhookDelivery'];
|
|
146
|
+
const resources = Object.fromEntries(kinds.map((kind) => [kind, this.controlPlane.list(kind, { namespace: this.namespace }).items]));
|
|
147
|
+
return {
|
|
148
|
+
namespace: this.namespace,
|
|
149
|
+
export: this.exportSnapshot(),
|
|
150
|
+
storage: this.controlPlane.storageReport(),
|
|
151
|
+
resources,
|
|
152
|
+
events: clone(this.controlPlane.events),
|
|
153
|
+
auditLog: clone(this.controlPlane.auditLog)
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#requireRepository(repository) {
|
|
158
|
+
const existing = this.controlPlane.get('Repository', this.namespace, repository);
|
|
159
|
+
if (!existing) throw new Error(`Repository ${repository} not found`);
|
|
160
|
+
return existing;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#requirePullRequest(pullRequest) {
|
|
164
|
+
const existing = this.controlPlane.get('PullRequest', this.namespace, pullRequest);
|
|
165
|
+
if (!existing) throw new Error(`PullRequest ${pullRequest} not found`);
|
|
166
|
+
return existing;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#requiredApprovals(targetRef) {
|
|
170
|
+
const branchProtection = this.controlPlane.list('BranchProtection', { namespace: this.namespace }).items.find((policy) => (policy.spec.refs || []).includes(targetRef));
|
|
171
|
+
return branchProtection?.spec.requiredApprovals || 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#refreshPullRequestMergeability(repository) {
|
|
175
|
+
const pullRequests = this.controlPlane.list('PullRequest', { namespace: this.namespace, labels: { repository } }).items;
|
|
176
|
+
return pullRequests.map((pr) => {
|
|
177
|
+
if (pr.status.phase !== 'Open') return pr;
|
|
178
|
+
const reviews = this.controlPlane.list('Review', { namespace: this.namespace, labels: { pullRequest: pr.metadata.name } }).items;
|
|
179
|
+
const approvals = reviews.filter((review) => review.spec.decision === 'approved').length;
|
|
180
|
+
const pipelines = this.controlPlane.list('Pipeline', { namespace: this.namespace, labels: { repository } }).items.filter((pipeline) => pipeline.metadata.name === `pipeline-${pr.metadata.name}`);
|
|
181
|
+
const checksPassed = pipelines.length > 0 && pipelines.every((pipeline) => pipeline.status.phase === 'Succeeded');
|
|
182
|
+
return this.controlPlane.patchStatus('PullRequest', this.namespace, pr.metadata.name, { approvals, checksPassed, mergeable: approvals >= (pr.spec.requiredApprovals || 0) && checksPassed }, this.users.repoAdmin);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#syntheticRevision(seed) {
|
|
187
|
+
const source = Buffer.from(`${seed}:${Date.now()}`).toString('hex');
|
|
188
|
+
return source.padEnd(40, '0').slice(0, 40);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function createKrateRuntime(options = {}) {
|
|
193
|
+
const runtime = new KrateRuntime(options);
|
|
194
|
+
if (options.bootstrap !== false) runtime.bootstrap(options.bootstrapOptions);
|
|
195
|
+
return runtime;
|
|
196
|
+
}
|
package/src/web-ui.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createResource } from './resource-model.js';
|
|
2
|
+
import { toResourceYaml } from './identity-policy.js';
|
|
3
|
+
|
|
4
|
+
export function createPullRequestReviewModel({ pullRequest, changedFiles = [], pipelineRuns = [] }) {
|
|
5
|
+
return { layout: 'three-pane-review', panes: ['file-tree', 'diff-and-comments', 'conversation-ci'], keyboardShortcuts: ['j/k file navigation', 'n/p comment navigation', 'a add suggestion', 'm merge'], pullRequest, changedFiles, pipelineRuns, yaml: toResourceYaml(pullRequest) };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createFailingRunModel({ pipeline, jobs }) {
|
|
9
|
+
const failedJobs = jobs.filter((job) => job.status.phase === 'Failed');
|
|
10
|
+
return { layout: 'live-run-debugger', stream: 'sse', pipeline, failedJobs, actions: ['copy failure', 'find similar runs', 'rerun from step'], similarRunSelector: failedJobs.map((job) => job.metadata.labels).filter(Boolean) };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createRunnerPoolEditor(pool) {
|
|
14
|
+
return { layout: 'split-form-yaml', fields: ['image', 'resources', 'nodeSelector', 'warmReplicas', 'maxReplicas', 'trustTier', 'cache'], resource: pool, yaml: toResourceYaml(pool), saveModes: ['apply', 'copy kubectl', 'open platform-config PR'] };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createWebhookInspector({ subscription, deliveries }) {
|
|
18
|
+
return { layout: 'webhook-inspector', subscription, deliveries, columns: ['phase', 'latency', 'attempts', 'response', 'signature'], actions: ['send test delivery', 'inspect headers/body/response', 'replay'] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createPolicyRolloutModel(policy) {
|
|
22
|
+
return { layout: 'policy-authoring', modes: ['template', 'CEL/raw'], rollout: ['preview', 'audit', 'enforce'], policy, yaml: toResourceYaml(policy) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createTriageView({ name, namespace = 'krate-org-default', organizationRef = 'default', selector }) {
|
|
26
|
+
return createResource('View', { name, namespace, labels: { purpose: 'triage' } }, { organizationRef, selector, columns: ['kind', 'repository', 'priority', 'assignee', 'status'], shareable: true }, { saved: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createDashboard({ repositories, pullRequests, pipelines, runnerPools, webhookDeliveries }) {
|
|
30
|
+
return {
|
|
31
|
+
product: 'Krate',
|
|
32
|
+
principles: ['Kubernetes is the backend', 'CRDs are contracts', 'GitOps transparency'],
|
|
33
|
+
repositories,
|
|
34
|
+
pullRequests,
|
|
35
|
+
pipelines,
|
|
36
|
+
runnerPools,
|
|
37
|
+
webhookDeliveries,
|
|
38
|
+
excellentFlows: ['Open and review a PR', 'Debug a failing run', 'Configure a runner pool', 'Add a webhook and verify it works', 'Write a PR policy with audit-to-enforce rollout', 'Cross-repo triage with saved filters']
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createAgentApprovalController, createResource } from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
function makeApproval(name, dispatchRun, action, phase = 'Pending', extra = {}) {
|
|
6
|
+
const approval = createResource('AgentApproval', { name, namespace: 'krate-org-default' }, {
|
|
7
|
+
organizationRef: 'default',
|
|
8
|
+
dispatchRun,
|
|
9
|
+
action,
|
|
10
|
+
requestedBy: 'agent-stack-1',
|
|
11
|
+
...extra
|
|
12
|
+
});
|
|
13
|
+
approval.status = { phase, createdAt: new Date().toISOString(), ...extra.status };
|
|
14
|
+
return approval;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('Create approval request returns resource with phase=Pending', () => {
|
|
18
|
+
const controller = createAgentApprovalController();
|
|
19
|
+
const result = controller.createApprovalRequest({
|
|
20
|
+
dispatchRun: 'run-1',
|
|
21
|
+
action: 'tool-use',
|
|
22
|
+
requestedBy: 'agent-stack-1',
|
|
23
|
+
context: 'Needs filesystem write access',
|
|
24
|
+
namespace: 'krate-org-default',
|
|
25
|
+
organizationRef: 'default',
|
|
26
|
+
resources: {}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
assert.equal(result.error, false, 'Should succeed');
|
|
30
|
+
assert.equal(result.duplicate, false, 'Should not be a duplicate');
|
|
31
|
+
assert.ok(result.approval, 'Should return an approval resource');
|
|
32
|
+
assert.equal(result.approval.kind, 'AgentApproval');
|
|
33
|
+
assert.equal(result.approval.status.phase, 'Pending');
|
|
34
|
+
assert.equal(result.approval.spec.action, 'tool-use');
|
|
35
|
+
assert.equal(result.approval.spec.dispatchRun, 'run-1');
|
|
36
|
+
assert.equal(result.approval.spec.requestedBy, 'agent-stack-1');
|
|
37
|
+
assert.ok(result.approval.status.createdAt, 'Should have createdAt timestamp');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('Record approve sets phase=Approved and decidedBy', () => {
|
|
41
|
+
const controller = createAgentApprovalController();
|
|
42
|
+
const existing = makeApproval('approval-1', 'run-1', 'tool-use', 'Pending');
|
|
43
|
+
const result = controller.recordDecision({
|
|
44
|
+
approvalName: 'approval-1',
|
|
45
|
+
decision: 'approve',
|
|
46
|
+
decidedBy: 'owner',
|
|
47
|
+
reason: 'Looks safe',
|
|
48
|
+
resources: { AgentApproval: [existing] }
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
assert.equal(result.error, false, 'Should succeed');
|
|
52
|
+
assert.equal(result.approval.status.phase, 'Approved');
|
|
53
|
+
assert.equal(result.approval.status.decidedBy, 'owner');
|
|
54
|
+
assert.equal(result.approval.status.reason, 'Looks safe');
|
|
55
|
+
assert.ok(result.approval.status.decidedAt, 'Should have decidedAt timestamp');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('Record deny sets phase=Denied', () => {
|
|
59
|
+
const controller = createAgentApprovalController();
|
|
60
|
+
const existing = makeApproval('approval-2', 'run-1', 'secret-access', 'Pending');
|
|
61
|
+
const result = controller.recordDecision({
|
|
62
|
+
approvalName: 'approval-2',
|
|
63
|
+
decision: 'deny',
|
|
64
|
+
decidedBy: 'admin',
|
|
65
|
+
reason: 'Sensitive secrets not allowed',
|
|
66
|
+
resources: { AgentApproval: [existing] }
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
assert.equal(result.error, false, 'Should succeed');
|
|
70
|
+
assert.equal(result.approval.status.phase, 'Denied');
|
|
71
|
+
assert.equal(result.approval.status.decidedBy, 'admin');
|
|
72
|
+
assert.equal(result.approval.status.reason, 'Sensitive secrets not allowed');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('isActionApproved returns true after approval', () => {
|
|
76
|
+
const controller = createAgentApprovalController();
|
|
77
|
+
const approved = makeApproval('approval-3', 'run-2', 'write-back', 'Approved', { status: { decidedBy: 'owner', decidedAt: new Date().toISOString() } });
|
|
78
|
+
const result = controller.isActionApproved({
|
|
79
|
+
dispatchRun: 'run-2',
|
|
80
|
+
action: 'write-back',
|
|
81
|
+
resources: { AgentApproval: [approved] }
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
assert.equal(result.approved, true, 'Should be approved');
|
|
85
|
+
assert.ok(result.approval, 'Should return the approval resource');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('isActionApproved returns false when still pending', () => {
|
|
89
|
+
const controller = createAgentApprovalController();
|
|
90
|
+
const pending = makeApproval('approval-4', 'run-3', 'release', 'Pending');
|
|
91
|
+
const result = controller.isActionApproved({
|
|
92
|
+
dispatchRun: 'run-3',
|
|
93
|
+
action: 'release',
|
|
94
|
+
resources: { AgentApproval: [pending] }
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
assert.equal(result.approved, false, 'Should not be approved');
|
|
98
|
+
assert.equal(result.reason, 'Approval is still pending');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('Duplicate pending request returns existing approval', () => {
|
|
102
|
+
const controller = createAgentApprovalController();
|
|
103
|
+
const existing = makeApproval('approval-5', 'run-4', 'tool-use', 'Pending');
|
|
104
|
+
const result = controller.createApprovalRequest({
|
|
105
|
+
dispatchRun: 'run-4',
|
|
106
|
+
action: 'tool-use',
|
|
107
|
+
requestedBy: 'agent-stack-1',
|
|
108
|
+
resources: { AgentApproval: [existing] }
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
assert.equal(result.error, false, 'Should succeed');
|
|
112
|
+
assert.equal(result.duplicate, true, 'Should be flagged as duplicate');
|
|
113
|
+
assert.equal(result.approval.metadata.name, 'approval-5', 'Should return the existing approval');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('Invalid action type returns error', () => {
|
|
117
|
+
const controller = createAgentApprovalController();
|
|
118
|
+
const result = controller.createApprovalRequest({
|
|
119
|
+
dispatchRun: 'run-5',
|
|
120
|
+
action: 'invalid-action',
|
|
121
|
+
requestedBy: 'agent-stack-1',
|
|
122
|
+
resources: {}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
assert.equal(result.error, true, 'Should fail');
|
|
126
|
+
assert.equal(result.reason, 'invalid-action');
|
|
127
|
+
assert.ok(result.message.includes('invalid-action'), 'Message should mention the invalid action');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('Already decided approval returns error on re-decide', () => {
|
|
131
|
+
const controller = createAgentApprovalController();
|
|
132
|
+
const decided = makeApproval('approval-6', 'run-6', 'escalation', 'Approved', { status: { decidedBy: 'admin', decidedAt: new Date().toISOString() } });
|
|
133
|
+
const result = controller.recordDecision({
|
|
134
|
+
approvalName: 'approval-6',
|
|
135
|
+
decision: 'deny',
|
|
136
|
+
decidedBy: 'other-admin',
|
|
137
|
+
resources: { AgentApproval: [decided] }
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
assert.equal(result.error, true, 'Should fail');
|
|
141
|
+
assert.equal(result.reason, 'already-decided');
|
|
142
|
+
assert.ok(result.message.includes('already been decided'), 'Message should indicate already decided');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('listPendingApprovals filters by org and pending status', () => {
|
|
146
|
+
const controller = createAgentApprovalController();
|
|
147
|
+
const pending1 = makeApproval('p1', 'run-7', 'tool-use', 'Pending');
|
|
148
|
+
const pending2 = makeApproval('p2', 'run-8', 'release', 'Pending');
|
|
149
|
+
const approved = makeApproval('p3', 'run-9', 'write-back', 'Approved');
|
|
150
|
+
|
|
151
|
+
const result = controller.listPendingApprovals({
|
|
152
|
+
organizationRef: 'default',
|
|
153
|
+
resources: { AgentApproval: [pending1, pending2, approved] }
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
assert.equal(result.length, 2, 'Should return only pending approvals');
|
|
157
|
+
assert.ok(result.every(a => a.status.phase === 'Pending'), 'All should be Pending');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('listApprovalsForRun filters by dispatch run', () => {
|
|
161
|
+
const controller = createAgentApprovalController();
|
|
162
|
+
const a1 = makeApproval('r1', 'run-10', 'tool-use', 'Pending');
|
|
163
|
+
const a2 = makeApproval('r2', 'run-10', 'secret-access', 'Approved');
|
|
164
|
+
const a3 = makeApproval('r3', 'run-11', 'release', 'Pending');
|
|
165
|
+
|
|
166
|
+
const result = controller.listApprovalsForRun({
|
|
167
|
+
dispatchRun: 'run-10',
|
|
168
|
+
resources: { AgentApproval: [a1, a2, a3] }
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
assert.equal(result.length, 2, 'Should return approvals for run-10 only');
|
|
172
|
+
assert.ok(result.every(a => a.spec.dispatchRun === 'run-10'), 'All should belong to run-10');
|
|
173
|
+
});
|