@a5c-ai/kradle 5.0.1-staging.3abdf9534c25
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 +187 -0
- package/bin/kradle-demo.mjs +23 -0
- package/bin/kradle-server.mjs +14 -0
- package/dist/kradle-controller-ui.json +3482 -0
- package/dist/kradle-lifecycle.json +201 -0
- package/dist/kradle-runtime-snapshot.json +3125 -0
- package/dist/kradle-summary.json +724 -0
- package/docs/README.md +61 -0
- package/docs/agents/README.md +83 -0
- package/docs/agents/acceptance-test-matrix.md +193 -0
- package/docs/agents/agent-mux-adapter-contract.md +167 -0
- package/docs/agents/agent-mux-source-map.md +310 -0
- package/docs/agents/agent-run-memory-import-spec.md +256 -0
- package/docs/agents/agent-stack-management-spec.md +421 -0
- package/docs/agents/api-contract-spec.md +309 -0
- package/docs/agents/artifacts-writeback-spec.md +145 -0
- package/docs/agents/chart-packaging-spec.md +128 -0
- package/docs/agents/ci-orchestration-spec.md +140 -0
- package/docs/agents/context-assembly-spec.md +219 -0
- package/docs/agents/controller-reconciliation-spec.md +255 -0
- package/docs/agents/crd-schema-spec.md +315 -0
- package/docs/agents/decision-log-open-questions.md +169 -0
- package/docs/agents/developer-implementation-checklist.md +329 -0
- package/docs/agents/dispatching-design.md +262 -0
- package/docs/agents/gaps-agent-mux-to-kradle-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/architecture-v2.md +2759 -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/crd-behaviors-and-relationships.md +3926 -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/integration-and-design-decisions.md +1530 -0
- package/docs/kradle-design.md +334 -0
- package/docs/local-minikube.md +55 -0
- package/docs/ontology/README.md +32 -0
- package/docs/ontology/bounded-contexts.md +29 -0
- package/docs/ontology/events-and-hooks.md +32 -0
- package/docs/ontology/oam-kubevela.md +32 -0
- package/docs/ontology/operations-and-release.md +25 -0
- package/docs/ontology/personas-and-actors.md +32 -0
- package/docs/ontology/policies-and-invariants.md +33 -0
- package/docs/ontology/problem-space.md +30 -0
- package/docs/ontology/resource-contracts.md +40 -0
- package/docs/ontology/resource-taxonomy.md +42 -0
- package/docs/ontology/runners-and-ci.md +29 -0
- package/docs/ontology/solution-space.md +24 -0
- package/docs/ontology/storage-and-data-boundaries.md +29 -0
- package/docs/ontology/validation-matrix.md +24 -0
- package/docs/ontology/web-ui-excellent-flows.md +32 -0
- package/docs/ontology/workflows.md +39 -0
- package/docs/ontology/world.md +35 -0
- package/docs/openapi.yaml +1291 -0
- package/docs/product-requirements.md +62 -0
- package/docs/requirements-v2.md +235 -0
- package/docs/roadmap-mvp.md +87 -0
- package/docs/sdk-api-reference.md +1108 -0
- package/docs/system-requirements.md +90 -0
- package/docs/system-spec-v2.md +1230 -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/docs/web-console-spec.md +533 -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 +66 -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 +95 -0
- package/scripts/validate-ui.mjs +305 -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 +549 -0
- package/src/agent-gateway-config-controller.js +147 -0
- package/src/agent-identity-migration.js +115 -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 +589 -0
- package/src/agent-permission-review.js +250 -0
- package/src/agent-persona-controller.js +135 -0
- package/src/agent-project-controller.js +117 -0
- package/src/agent-prompt-composition.js +55 -0
- package/src/agent-provider-config-controller.js +151 -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 +421 -0
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +387 -0
- package/src/agent-workspace-controller.js +702 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +621 -0
- package/src/argocd-gitops.js +43 -0
- package/src/artifact-registry-controller.js +542 -0
- package/src/assistant-runtime.js +284 -0
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +310 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +112 -0
- package/src/controller-ui.js +620 -0
- package/src/data-plane.js +179 -0
- package/src/event-bus.js +397 -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 +221 -0
- package/src/external/sync-controller.js +235 -0
- package/src/external/webhook-controller.js +144 -0
- package/src/external/write-controller.js +283 -0
- package/src/gitea-backend.js +131 -0
- package/src/gitea-service.js +173 -0
- package/src/handoff.js +98 -0
- package/src/health-probes.js +134 -0
- package/src/hooks-events.js +63 -0
- package/src/hooks-lifecycle.js +117 -0
- package/src/http-server.js +409 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +71 -0
- package/src/jitsi-agent-bridge.js +141 -0
- package/src/jitsi-meeting-controller.js +291 -0
- package/src/jitsi-sync-controller.js +198 -0
- package/src/kradle-inference-service-controller.js +246 -0
- package/src/kubernetes-controller-async.js +531 -0
- package/src/kubernetes-controller.js +904 -0
- package/src/kubernetes-resource-gateway.js +48 -0
- package/src/model-route-controller.js +364 -0
- package/src/notification-controller.js +178 -0
- package/src/operations.js +112 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +282 -0
- package/src/runner-controller.js +272 -0
- package/src/runners-ci.js +48 -0
- package/src/runtime.js +196 -0
- package/src/snapshot-cache.js +157 -0
- package/src/virtual-model-controller.js +538 -0
- package/src/virtual-model-hook-bridge.js +200 -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 +679 -0
- package/tests/agent-gateway-config-controller.test.js +386 -0
- package/tests/agent-identity-migration.test.js +87 -0
- package/tests/agent-memory-controller.test.js +461 -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 +389 -0
- package/tests/agent-mux-integration.test.js +971 -0
- package/tests/agent-permission-review-v2.test.js +317 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-persona-controller.test.js +127 -0
- package/tests/agent-project-controller.test.js +302 -0
- package/tests/agent-prompt-composition.test.js +76 -0
- package/tests/agent-provider-config-controller.test.js +376 -0
- package/tests/agent-resources.test.js +303 -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 +283 -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 +271 -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/artifact-registry.test.js +511 -0
- package/tests/assistant-runtime.test.js +506 -0
- package/tests/async-controller.test.js +252 -0
- package/tests/audit-controller.test.js +227 -0
- package/tests/codespace-controller.test.js +318 -0
- package/tests/controller-client.test.js +133 -0
- package/tests/deployment.test.js +527 -0
- package/tests/e2e/lifecycle.test.js +120 -0
- package/tests/event-bus-integration.test.js +355 -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 +415 -0
- package/tests/external-provider-adapter.test.js +365 -0
- package/tests/external-resource-model.test.js +223 -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/health-probes.test.js +90 -0
- package/tests/hooks-lifecycle.test.js +364 -0
- package/tests/integration/full-flow.test.js +266 -0
- package/tests/jitsi-agent-bridge.test.js +119 -0
- package/tests/jitsi-helm-integration.test.js +77 -0
- package/tests/jitsi-meeting-controller.test.js +170 -0
- package/tests/jitsi-resource-model.test.js +73 -0
- package/tests/jitsi-sync-controller.test.js +112 -0
- package/tests/kradle-inference-service.test.js +689 -0
- package/tests/kradle.test.js +779 -0
- package/tests/memory-search-wiring.test.js +270 -0
- package/tests/model-route-controller.test.js +733 -0
- package/tests/notification-controller.test.js +196 -0
- package/tests/notification-integration.test.js +179 -0
- package/tests/org-scoping.test.js +687 -0
- package/tests/runner-controller.test.js +327 -0
- package/tests/runner-integration.test.js +231 -0
- package/tests/session-cookie-hmac.test.js +151 -0
- package/tests/snapshot-performance.test.js +315 -0
- package/tests/sse-events.test.js +107 -0
- package/tests/virtual-model-controller.test.js +877 -0
- package/tests/virtual-model-hook-bridge.test.js +384 -0
- package/tests/webhook-trigger.test.js +198 -0
- package/tests/workspace-volumes.test.js +312 -0
- package/tests/writeback-persistence.test.js +207 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createKubernetesResourceClient, repositoryManifest } from './kubernetes-controller.js';
|
|
2
|
+
|
|
3
|
+
export const KUBERNETES_RESOURCE_GATEWAY_BOUNDARY = {
|
|
4
|
+
role: 'kubernetes-resource-gateway',
|
|
5
|
+
scope: 'Application port translating API controller intent into Kubernetes resource-client operations',
|
|
6
|
+
owns: ['resource definitions', 'list/get/apply/delete/watch delegation', 'Repository manifest application', 'namespace scoping'],
|
|
7
|
+
delegatesTo: ['kubernetes-resource-client'],
|
|
8
|
+
mustNotOwn: ['HTTP routes', 'Next.js page flow decisions', 'forge DTO composition', 'Kubernetes reconciliation scheduling']
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function createKubernetesResourceGateway(options = {}) {
|
|
12
|
+
const resourceClient = options.resourceClient || options.kubernetesClient || createKubernetesResourceClient(options);
|
|
13
|
+
const namespace = options.namespace || resourceClient.namespace || process.env.KRADLE_NAMESPACE || 'kradle-system';
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
...KUBERNETES_RESOURCE_GATEWAY_BOUNDARY,
|
|
17
|
+
namespace,
|
|
18
|
+
resourceDefinitions: resourceClient.resourceDefinitions,
|
|
19
|
+
async snapshot() {
|
|
20
|
+
return resourceClient.snapshot();
|
|
21
|
+
},
|
|
22
|
+
async list(kindOrPlural) {
|
|
23
|
+
return resourceClient.listResource(kindOrPlural);
|
|
24
|
+
},
|
|
25
|
+
async get(kindOrPlural, name) {
|
|
26
|
+
return resourceClient.getResource(kindOrPlural, name);
|
|
27
|
+
},
|
|
28
|
+
async apply(resource) {
|
|
29
|
+
return resourceClient.applyResource(resource);
|
|
30
|
+
},
|
|
31
|
+
async delete(kindOrPlural, name) {
|
|
32
|
+
return resourceClient.deleteResource(kindOrPlural, name);
|
|
33
|
+
},
|
|
34
|
+
async createRepository(input) {
|
|
35
|
+
return resourceClient.applyResource(repositoryManifest(input, namespace));
|
|
36
|
+
},
|
|
37
|
+
async createOrganization(input) {
|
|
38
|
+
return resourceClient.createOrganization(input);
|
|
39
|
+
},
|
|
40
|
+
watch(resourcePath, handlers = {}) {
|
|
41
|
+
return resourceClient.watchResource(resourcePath, handlers);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
// KradleModelRoute Controller
|
|
2
|
+
// Unified model routing through Envoy AI Gateway — validation, endpoint resolution,
|
|
3
|
+
// catalog generation, and Envoy manifest generation for both internal (KServe) and
|
|
4
|
+
// external (cloud LLM) model routes.
|
|
5
|
+
|
|
6
|
+
export const ENVOY_AI_GATEWAY_API_GROUP = 'aigateway.envoyproxy.io';
|
|
7
|
+
export const ENVOY_AI_GATEWAY_API_VERSION = 'v1alpha1';
|
|
8
|
+
|
|
9
|
+
export const VALID_ROUTE_TYPES = ['internal', 'external'];
|
|
10
|
+
|
|
11
|
+
export const VALID_EXTERNAL_PROTOCOLS = ['openai', 'anthropic', 'bedrock', 'vertex', 'azure-openai', 'custom'];
|
|
12
|
+
|
|
13
|
+
export const MODEL_ROUTE_CONTROLLER_BOUNDARY = {
|
|
14
|
+
role: 'model-route-controller',
|
|
15
|
+
scope: 'Unified model routing through Envoy AI Gateway — validation, endpoint resolution, catalog generation, Envoy manifest generation',
|
|
16
|
+
owns: ['route validation', 'endpoint resolution', 'model catalog', 'envoy route manifests'],
|
|
17
|
+
delegatesTo: ['resource-model', 'kradle-inference-service-controller'],
|
|
18
|
+
mustNotOwn: ['secret values', 'gateway deployment', 'network policy']
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validate a KradleModelRoute resource. Returns { valid, errors }.
|
|
23
|
+
* @param {object} resource
|
|
24
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
25
|
+
*/
|
|
26
|
+
export function validateModelRoute(resource) {
|
|
27
|
+
const errors = [];
|
|
28
|
+
|
|
29
|
+
if (resource == null) {
|
|
30
|
+
errors.push('resource must not be null or undefined');
|
|
31
|
+
return { valid: false, errors };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!resource?.metadata?.name) {
|
|
35
|
+
errors.push('metadata.name is required');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const spec = resource?.spec || {};
|
|
39
|
+
|
|
40
|
+
if (!spec.organizationRef) {
|
|
41
|
+
errors.push('spec.organizationRef is required');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!spec.modelName) {
|
|
45
|
+
errors.push('spec.modelName is required');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!spec.routeType) {
|
|
49
|
+
errors.push('spec.routeType is required; valid values are: internal, external');
|
|
50
|
+
} else if (!VALID_ROUTE_TYPES.includes(spec.routeType)) {
|
|
51
|
+
errors.push(`spec.routeType "${spec.routeType}" is not valid; valid values are: ${VALID_ROUTE_TYPES.join(', ')}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Route-type-specific validation
|
|
55
|
+
if (spec.routeType === 'internal') {
|
|
56
|
+
if (!spec.inferenceServiceRef) {
|
|
57
|
+
errors.push('spec.inferenceServiceRef is required for internal routes');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (spec.routeType === 'external') {
|
|
62
|
+
if (!spec.external) {
|
|
63
|
+
errors.push('spec.external is required for external routes');
|
|
64
|
+
} else {
|
|
65
|
+
if (!spec.external.provider) {
|
|
66
|
+
errors.push('spec.external.provider is required for external routes');
|
|
67
|
+
}
|
|
68
|
+
if (!spec.external.endpoint) {
|
|
69
|
+
errors.push('spec.external.endpoint is required for external routes');
|
|
70
|
+
}
|
|
71
|
+
if (spec.external.protocol && !VALID_EXTERNAL_PROTOCOLS.includes(spec.external.protocol)) {
|
|
72
|
+
errors.push(`spec.external.protocol "${spec.external.protocol}" is not valid; valid values are: ${VALID_EXTERNAL_PROTOCOLS.join(', ')}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { valid: errors.length === 0, errors };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Factory that returns a KradleModelRoute controller instance.
|
|
82
|
+
* @param {object} [options]
|
|
83
|
+
* @returns {object}
|
|
84
|
+
*/
|
|
85
|
+
export function createModelRouteController(options = {}) {
|
|
86
|
+
return {
|
|
87
|
+
role: 'model-route-controller',
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validate a KradleModelRoute resource.
|
|
91
|
+
* @param {object} resource
|
|
92
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
93
|
+
*/
|
|
94
|
+
validate(resource) {
|
|
95
|
+
return validateModelRoute(resource);
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolve a route to its endpoint and connection details.
|
|
100
|
+
* For internal routes, looks up the InferenceService status.url.
|
|
101
|
+
* For external routes, uses spec.external fields.
|
|
102
|
+
*
|
|
103
|
+
* @param {object} route - KradleModelRoute resource
|
|
104
|
+
* @param {object[]} [resources] - all cluster resources to look up InferenceService
|
|
105
|
+
* @returns {{ endpoint: string, protocol: string, provider: string, modelId: string, authConfig: object|null }}
|
|
106
|
+
*/
|
|
107
|
+
resolveRoute(route, resources = []) {
|
|
108
|
+
const spec = route?.spec || {};
|
|
109
|
+
|
|
110
|
+
if (spec.routeType === 'internal') {
|
|
111
|
+
return resolveInternalRoute(route, resources);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (spec.routeType === 'external') {
|
|
115
|
+
return resolveExternalRoute(route);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
endpoint: null,
|
|
120
|
+
protocol: 'unknown',
|
|
121
|
+
provider: 'unknown',
|
|
122
|
+
modelId: spec.modelName || null,
|
|
123
|
+
authConfig: null
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Reconcile a set of routes, checking that each target is reachable.
|
|
129
|
+
* Returns conditions for each route indicating readiness.
|
|
130
|
+
*
|
|
131
|
+
* @param {object[]} routes - array of KradleModelRoute resources
|
|
132
|
+
* @param {object[]} [resources] - all cluster resources
|
|
133
|
+
* @returns {{ conditions: object[], resolvedRoutes: object[] }}
|
|
134
|
+
*/
|
|
135
|
+
reconcileRoutes(routes, resources = []) {
|
|
136
|
+
const conditions = [];
|
|
137
|
+
const resolvedRoutes = [];
|
|
138
|
+
|
|
139
|
+
for (const route of routes) {
|
|
140
|
+
const validation = validateModelRoute(route);
|
|
141
|
+
if (!validation.valid) {
|
|
142
|
+
conditions.push({
|
|
143
|
+
type: 'RouteReady',
|
|
144
|
+
status: 'False',
|
|
145
|
+
route: route.metadata?.name,
|
|
146
|
+
reason: 'ValidationFailed',
|
|
147
|
+
message: validation.errors.join('; ')
|
|
148
|
+
});
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const resolved = this.resolveRoute(route, resources);
|
|
153
|
+
if (!resolved.endpoint) {
|
|
154
|
+
conditions.push({
|
|
155
|
+
type: 'RouteReady',
|
|
156
|
+
status: 'False',
|
|
157
|
+
route: route.metadata?.name,
|
|
158
|
+
reason: 'EndpointNotFound',
|
|
159
|
+
message: `Could not resolve endpoint for route "${route.metadata?.name}"`
|
|
160
|
+
});
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
conditions.push({
|
|
165
|
+
type: 'RouteReady',
|
|
166
|
+
status: 'True',
|
|
167
|
+
route: route.metadata?.name,
|
|
168
|
+
reason: 'EndpointResolved',
|
|
169
|
+
message: `Route "${route.metadata?.name}" resolved to ${resolved.endpoint}`
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
resolvedRoutes.push({
|
|
173
|
+
name: route.metadata?.name,
|
|
174
|
+
modelName: route.spec?.modelName,
|
|
175
|
+
routeType: route.spec?.routeType,
|
|
176
|
+
...resolved
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { conditions, resolvedRoutes };
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Generate a unified model catalog merging internal and external routes.
|
|
185
|
+
*
|
|
186
|
+
* @param {object[]} routes - array of KradleModelRoute resources
|
|
187
|
+
* @param {object[]} [resources] - all cluster resources
|
|
188
|
+
* @returns {Array<{ name: string, provider: string, type: string, status: string, endpoint: string, protocol: string }>}
|
|
189
|
+
*/
|
|
190
|
+
listModelCatalog(routes, resources = []) {
|
|
191
|
+
const catalog = [];
|
|
192
|
+
|
|
193
|
+
for (const route of routes) {
|
|
194
|
+
const spec = route?.spec || {};
|
|
195
|
+
const validation = validateModelRoute(route);
|
|
196
|
+
const resolved = validation.valid ? this.resolveRoute(route, resources) : null;
|
|
197
|
+
|
|
198
|
+
catalog.push({
|
|
199
|
+
name: spec.modelName || route.metadata?.name || 'unknown',
|
|
200
|
+
provider: resolved?.provider || (spec.routeType === 'internal' ? 'kserve' : spec.external?.provider || 'unknown'),
|
|
201
|
+
type: spec.routeType || 'unknown',
|
|
202
|
+
status: resolved?.endpoint ? 'available' : 'unavailable',
|
|
203
|
+
endpoint: resolved?.endpoint || null,
|
|
204
|
+
protocol: resolved?.protocol || (spec.routeType === 'internal' ? 'v2' : spec.external?.protocol || 'unknown')
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return catalog;
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Generate an Envoy AI Gateway route configuration for a resolved route.
|
|
213
|
+
*
|
|
214
|
+
* @param {object} route - KradleModelRoute resource
|
|
215
|
+
* @param {{ endpoint: string, protocol: string, provider: string, modelId: string }} resolvedEndpoint
|
|
216
|
+
* @returns {object} Envoy AI Gateway route config
|
|
217
|
+
*/
|
|
218
|
+
generateEnvoyRouteManifest(route, resolvedEndpoint) {
|
|
219
|
+
const spec = route?.spec || {};
|
|
220
|
+
const metadata = route?.metadata || {};
|
|
221
|
+
|
|
222
|
+
const routeConfig = {
|
|
223
|
+
apiVersion: `${ENVOY_AI_GATEWAY_API_GROUP}/${ENVOY_AI_GATEWAY_API_VERSION}`,
|
|
224
|
+
kind: 'AIGatewayRoute',
|
|
225
|
+
metadata: {
|
|
226
|
+
name: `route-${metadata.name || 'unknown'}`,
|
|
227
|
+
namespace: metadata.namespace,
|
|
228
|
+
labels: {
|
|
229
|
+
'kradle.a5c.ai/managed': 'true',
|
|
230
|
+
'kradle.a5c.ai/org': spec.organizationRef || '',
|
|
231
|
+
'kradle.a5c.ai/model-route': metadata.name || ''
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
spec: {
|
|
235
|
+
modelName: spec.modelName,
|
|
236
|
+
targetRef: {
|
|
237
|
+
kind: spec.routeType === 'internal' ? 'Service' : 'ExternalBackend',
|
|
238
|
+
name: spec.routeType === 'internal'
|
|
239
|
+
? (spec.inferenceServiceRef || metadata.name)
|
|
240
|
+
: `ext-${resolvedEndpoint.provider || 'provider'}`
|
|
241
|
+
},
|
|
242
|
+
rules: [{
|
|
243
|
+
matches: [{
|
|
244
|
+
headers: [{
|
|
245
|
+
name: 'x-model-name',
|
|
246
|
+
value: spec.modelName
|
|
247
|
+
}]
|
|
248
|
+
}],
|
|
249
|
+
backendRefs: [{
|
|
250
|
+
name: resolvedEndpoint.endpoint,
|
|
251
|
+
protocol: resolvedEndpoint.protocol,
|
|
252
|
+
modelId: resolvedEndpoint.modelId || spec.modelName
|
|
253
|
+
}]
|
|
254
|
+
}]
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// Add rate limits if present
|
|
259
|
+
if (spec.rateLimits) {
|
|
260
|
+
routeConfig.spec.rateLimits = spec.rateLimits;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Add timeout if present
|
|
264
|
+
if (spec.timeout) {
|
|
265
|
+
routeConfig.spec.timeout = spec.timeout;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return routeConfig;
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Convert a KServe InferenceService into a KradleModelRoute spec.
|
|
273
|
+
* Bridge method for migrating existing services into the unified routing model.
|
|
274
|
+
*
|
|
275
|
+
* @param {object} inferenceService - KServe InferenceService resource
|
|
276
|
+
* @param {{ endpoint: string, protocol?: string }} resolvedEndpoint
|
|
277
|
+
* @returns {object} KradleModelRoute spec fields
|
|
278
|
+
*/
|
|
279
|
+
toModelRoute(inferenceService, resolvedEndpoint) {
|
|
280
|
+
const metadata = inferenceService?.metadata || {};
|
|
281
|
+
const spec = inferenceService?.spec || {};
|
|
282
|
+
const predictor = spec.predictor || {};
|
|
283
|
+
const modelFormat = predictor.modelFormat?.name || spec.modelFormat || 'custom';
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
apiVersion: 'kradle.a5c.ai/v1alpha1',
|
|
287
|
+
kind: 'KradleModelRoute',
|
|
288
|
+
metadata: {
|
|
289
|
+
name: `route-${metadata.name || 'unknown'}`,
|
|
290
|
+
namespace: metadata.namespace,
|
|
291
|
+
labels: {
|
|
292
|
+
'kradle.a5c.ai/managed': 'true',
|
|
293
|
+
'kradle.a5c.ai/source': 'inference-service',
|
|
294
|
+
...(metadata.labels || {})
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
spec: {
|
|
298
|
+
organizationRef: metadata.labels?.['kradle.a5c.ai/org'] || spec.organizationRef || 'default',
|
|
299
|
+
modelName: metadata.name || 'unknown',
|
|
300
|
+
routeType: 'internal',
|
|
301
|
+
inferenceServiceRef: metadata.name,
|
|
302
|
+
modelFormat,
|
|
303
|
+
endpoint: resolvedEndpoint.endpoint,
|
|
304
|
+
protocol: resolvedEndpoint.protocol || 'v2'
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
// Internal helpers
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
function resolveInternalRoute(route, resources) {
|
|
316
|
+
const spec = route?.spec || {};
|
|
317
|
+
const inferenceServiceRef = spec.inferenceServiceRef;
|
|
318
|
+
|
|
319
|
+
// Look up the InferenceService in the cluster resources
|
|
320
|
+
let endpoint = null;
|
|
321
|
+
let protocol = spec.protocol || 'v2';
|
|
322
|
+
|
|
323
|
+
if (inferenceServiceRef && Array.isArray(resources)) {
|
|
324
|
+
const isvc = resources.find(
|
|
325
|
+
r => (r.kind === 'InferenceService' || r.kind === 'KradleInferenceService') &&
|
|
326
|
+
r.metadata?.name === inferenceServiceRef
|
|
327
|
+
);
|
|
328
|
+
if (isvc) {
|
|
329
|
+
endpoint = isvc.status?.url || isvc.status?.address?.url || null;
|
|
330
|
+
if (!endpoint && isvc.metadata?.name) {
|
|
331
|
+
const ns = isvc.metadata.namespace || 'default';
|
|
332
|
+
endpoint = `http://${isvc.metadata.name}.${ns}.svc.cluster.local`;
|
|
333
|
+
}
|
|
334
|
+
protocol = isvc.spec?.inferenceProtocol || protocol;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Fallback: build cluster-internal URL from the reference name
|
|
339
|
+
if (!endpoint && inferenceServiceRef) {
|
|
340
|
+
const ns = route.metadata?.namespace || 'default';
|
|
341
|
+
endpoint = `http://${inferenceServiceRef}.${ns}.svc.cluster.local`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
endpoint,
|
|
346
|
+
protocol,
|
|
347
|
+
provider: 'kserve',
|
|
348
|
+
modelId: spec.modelName || inferenceServiceRef || null,
|
|
349
|
+
authConfig: spec.authConfig || null
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function resolveExternalRoute(route) {
|
|
354
|
+
const spec = route?.spec || {};
|
|
355
|
+
const ext = spec.external || {};
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
endpoint: ext.endpoint || null,
|
|
359
|
+
protocol: ext.protocol || 'openai',
|
|
360
|
+
provider: ext.provider || 'unknown',
|
|
361
|
+
modelId: ext.modelId || spec.modelName || null,
|
|
362
|
+
authConfig: ext.authConfig || null
|
|
363
|
+
};
|
|
364
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export const NOTIFICATION_CONTROLLER_BOUNDARY = {
|
|
4
|
+
role: 'notification-controller',
|
|
5
|
+
scope: 'User notification lifecycle: creation from events, querying, read state, and preferences',
|
|
6
|
+
owns: ['notification creation', 'notification listing', 'read state', 'user preferences'],
|
|
7
|
+
delegatesTo: [],
|
|
8
|
+
mustNotOwn: ['event dispatch', 'UI rendering', 'push delivery']
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const NOTIFICATION_TYPES = {
|
|
12
|
+
'run-complete': { severity: 'info' },
|
|
13
|
+
'approval-needed': { severity: 'warning' },
|
|
14
|
+
'conflict-detected': { severity: 'warning' },
|
|
15
|
+
'workspace-ready': { severity: 'info' },
|
|
16
|
+
'system': { severity: 'info' },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const DEFAULT_PREFERENCES = {
|
|
20
|
+
runs: true,
|
|
21
|
+
approvals: true,
|
|
22
|
+
conflicts: true,
|
|
23
|
+
workspaces: true,
|
|
24
|
+
sound: false,
|
|
25
|
+
desktop: false,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function createNotificationController() {
|
|
29
|
+
// Map of org -> notifications[]
|
|
30
|
+
const store = new Map();
|
|
31
|
+
// Map of userId -> preferences
|
|
32
|
+
const prefsStore = new Map();
|
|
33
|
+
|
|
34
|
+
function getOrgNotifications(org) {
|
|
35
|
+
if (!store.has(org)) store.set(org, []);
|
|
36
|
+
return store.get(org);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function mapEventToNotification(event) {
|
|
40
|
+
const { type, status, name, action, resourceRef, org = 'default' } = event || {};
|
|
41
|
+
|
|
42
|
+
let notifType = 'system';
|
|
43
|
+
let title = 'System event';
|
|
44
|
+
let message = '';
|
|
45
|
+
let severity = 'info';
|
|
46
|
+
|
|
47
|
+
if (type === 'AgentDispatchRun') {
|
|
48
|
+
notifType = 'run-complete';
|
|
49
|
+
const runName = name || event?.metadata?.name || 'Unknown';
|
|
50
|
+
if (status === 'completed') {
|
|
51
|
+
title = `Run ${runName} completed`;
|
|
52
|
+
message = `Agent dispatch run "${runName}" completed successfully.`;
|
|
53
|
+
severity = 'info';
|
|
54
|
+
} else if (status === 'failed') {
|
|
55
|
+
title = `Run ${runName} failed`;
|
|
56
|
+
message = `Agent dispatch run "${runName}" failed.`;
|
|
57
|
+
severity = 'error';
|
|
58
|
+
} else {
|
|
59
|
+
title = `Run ${runName} updated`;
|
|
60
|
+
message = `Agent dispatch run "${runName}" status: ${status || 'unknown'}.`;
|
|
61
|
+
severity = 'info';
|
|
62
|
+
}
|
|
63
|
+
} else if (type === 'AgentApproval' && status === 'pending') {
|
|
64
|
+
notifType = 'approval-needed';
|
|
65
|
+
const actionLabel = action || event?.spec?.action || 'unknown action';
|
|
66
|
+
title = `Approval needed for ${actionLabel}`;
|
|
67
|
+
message = `An agent is requesting approval for: ${actionLabel}.`;
|
|
68
|
+
severity = 'warning';
|
|
69
|
+
} else if (type === 'ExternalSyncConflict') {
|
|
70
|
+
notifType = 'conflict-detected';
|
|
71
|
+
const resource = resourceRef || event?.spec?.resourceRef || name || 'unknown resource';
|
|
72
|
+
title = `Conflict in ${resource}`;
|
|
73
|
+
message = `A sync conflict was detected in resource: ${resource}.`;
|
|
74
|
+
severity = 'warning';
|
|
75
|
+
} else if (type === 'KradleWorkspace' && event?.claimed) {
|
|
76
|
+
notifType = 'workspace-ready';
|
|
77
|
+
const wsName = name || event?.metadata?.name || 'Unknown';
|
|
78
|
+
const runRef = event?.claimedBy || event?.spec?.claimedBy || 'a run';
|
|
79
|
+
title = `Workspace ${wsName} claimed by ${runRef}`;
|
|
80
|
+
message = `Workspace "${wsName}" has been claimed by "${runRef}".`;
|
|
81
|
+
severity = 'info';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { notifType, title, message, severity, org };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
role: 'notification-controller',
|
|
89
|
+
|
|
90
|
+
createNotification(event) {
|
|
91
|
+
const { notifType, title, message, severity, org } = mapEventToNotification(event);
|
|
92
|
+
|
|
93
|
+
const notification = {
|
|
94
|
+
id: randomUUID(),
|
|
95
|
+
type: notifType,
|
|
96
|
+
title,
|
|
97
|
+
message,
|
|
98
|
+
severity,
|
|
99
|
+
resourceRef: event?.resourceRef || null,
|
|
100
|
+
createdAt: new Date().toISOString(),
|
|
101
|
+
read: false,
|
|
102
|
+
org,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const notifications = getOrgNotifications(org);
|
|
106
|
+
notifications.push(notification);
|
|
107
|
+
|
|
108
|
+
return notification;
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
listNotifications(org, opts = {}) {
|
|
112
|
+
const { unreadOnly = false, limit = 20, since = null } = opts;
|
|
113
|
+
let notifications = [...getOrgNotifications(org)];
|
|
114
|
+
|
|
115
|
+
// Filter by read state
|
|
116
|
+
if (unreadOnly) {
|
|
117
|
+
notifications = notifications.filter((n) => !n.read);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Filter by since timestamp
|
|
121
|
+
if (since) {
|
|
122
|
+
const sinceDate = new Date(since).getTime();
|
|
123
|
+
notifications = notifications.filter((n) => new Date(n.createdAt).getTime() > sinceDate);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Newest first
|
|
127
|
+
notifications.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
|
128
|
+
|
|
129
|
+
// Cap to limit
|
|
130
|
+
if (limit > 0) {
|
|
131
|
+
notifications = notifications.slice(0, limit);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return notifications;
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
markAsRead(notificationId) {
|
|
138
|
+
for (const notifications of store.values()) {
|
|
139
|
+
const notification = notifications.find((n) => n.id === notificationId);
|
|
140
|
+
if (notification) {
|
|
141
|
+
notification.read = true;
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
markAllAsRead(org) {
|
|
149
|
+
const notifications = getOrgNotifications(org);
|
|
150
|
+
let count = 0;
|
|
151
|
+
for (const notification of notifications) {
|
|
152
|
+
if (!notification.read) {
|
|
153
|
+
notification.read = true;
|
|
154
|
+
count++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return count;
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
getUnreadCount(org) {
|
|
161
|
+
return getOrgNotifications(org).filter((n) => !n.read).length;
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
getPreferences(userId) {
|
|
165
|
+
if (!prefsStore.has(userId)) {
|
|
166
|
+
return { ...DEFAULT_PREFERENCES };
|
|
167
|
+
}
|
|
168
|
+
return { ...DEFAULT_PREFERENCES, ...prefsStore.get(userId) };
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
updatePreferences(userId, prefs) {
|
|
172
|
+
const existing = prefsStore.get(userId) || {};
|
|
173
|
+
const merged = { ...DEFAULT_PREFERENCES, ...existing, ...prefs };
|
|
174
|
+
prefsStore.set(userId, merged);
|
|
175
|
+
return merged;
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|