@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,538 @@
|
|
|
1
|
+
// KradleVirtualModel Controller
|
|
2
|
+
// Programmable model abstraction with declarative routing rules, JS hooks for
|
|
3
|
+
// request/response transformation, session lifecycle management, and
|
|
4
|
+
// observability injection.
|
|
5
|
+
|
|
6
|
+
import { globalEventBus } from './event-bus.js';
|
|
7
|
+
import vm from 'node:vm';
|
|
8
|
+
|
|
9
|
+
export const VIRTUAL_MODEL_CONTROLLER_BOUNDARY = {
|
|
10
|
+
role: 'virtual-model-controller',
|
|
11
|
+
scope: 'Programmable model abstraction with declarative routing rules, JS hooks, session lifecycle, and observability',
|
|
12
|
+
owns: ['virtual model validation', 'rule evaluation', 'hook execution', 'route resolution', 'session lifecycle', 'observability emission'],
|
|
13
|
+
delegatesTo: ['resource-model', 'model-route-controller', 'event-bus'],
|
|
14
|
+
mustNotOwn: ['secret values', 'gateway deployment', 'network policy', 'actual model invocation']
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const VALID_OPERATORS = ['eq', 'neq', 'gt', 'lt', 'gte', 'lte', 'in', 'contains', 'matches'];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Evaluate a single condition against a request context value.
|
|
21
|
+
* @param {{ field: string, operator: string, value: * }} condition
|
|
22
|
+
* @param {object} requestContext
|
|
23
|
+
* @returns {boolean}
|
|
24
|
+
*/
|
|
25
|
+
function evaluateCondition(condition, requestContext) {
|
|
26
|
+
const actual = requestContext[condition.field];
|
|
27
|
+
const expected = condition.value;
|
|
28
|
+
|
|
29
|
+
switch (condition.operator) {
|
|
30
|
+
case 'eq': return actual === expected;
|
|
31
|
+
case 'neq': return actual !== expected;
|
|
32
|
+
case 'gt': return actual > expected;
|
|
33
|
+
case 'lt': return actual < expected;
|
|
34
|
+
case 'gte': return actual >= expected;
|
|
35
|
+
case 'lte': return actual <= expected;
|
|
36
|
+
case 'in': return Array.isArray(expected) && expected.includes(actual);
|
|
37
|
+
case 'contains': return typeof actual === 'string' && actual.includes(expected);
|
|
38
|
+
case 'matches': {
|
|
39
|
+
try { return new RegExp(expected).test(String(actual)); }
|
|
40
|
+
catch { return false; }
|
|
41
|
+
}
|
|
42
|
+
default: return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Select a route via weighted random from the routes array.
|
|
48
|
+
* @param {Array<{ modelRouteRef: string, weight: number }>} routes
|
|
49
|
+
* @returns {string|null} modelRouteRef
|
|
50
|
+
*/
|
|
51
|
+
function weightedRandomSelect(routes) {
|
|
52
|
+
if (!Array.isArray(routes) || routes.length === 0) return null;
|
|
53
|
+
const totalWeight = routes.reduce((sum, r) => sum + (r.weight || 1), 0);
|
|
54
|
+
if (totalWeight <= 0) return routes[0].modelRouteRef || null;
|
|
55
|
+
let rand = Math.random() * totalWeight;
|
|
56
|
+
for (const route of routes) {
|
|
57
|
+
rand -= (route.weight || 1);
|
|
58
|
+
if (rand <= 0) return route.modelRouteRef;
|
|
59
|
+
}
|
|
60
|
+
return routes[routes.length - 1].modelRouteRef;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validate a KradleVirtualModel resource. Returns { valid, errors }.
|
|
65
|
+
* @param {object} resource
|
|
66
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
67
|
+
*/
|
|
68
|
+
export function validateVirtualModel(resource) {
|
|
69
|
+
const errors = [];
|
|
70
|
+
|
|
71
|
+
if (resource == null) {
|
|
72
|
+
errors.push('resource must not be null or undefined');
|
|
73
|
+
return { valid: false, errors };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!resource?.metadata?.name) {
|
|
77
|
+
errors.push('metadata.name is required');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const spec = resource?.spec || {};
|
|
81
|
+
|
|
82
|
+
if (!spec.organizationRef) {
|
|
83
|
+
errors.push('spec.organizationRef is required');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!spec.modelName) {
|
|
87
|
+
errors.push('spec.modelName is required');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!Array.isArray(spec.routes) || spec.routes.length === 0) {
|
|
91
|
+
errors.push('spec.routes is required and must be a non-empty array');
|
|
92
|
+
} else {
|
|
93
|
+
for (let i = 0; i < spec.routes.length; i++) {
|
|
94
|
+
const route = spec.routes[i];
|
|
95
|
+
if (!route.modelRouteRef) {
|
|
96
|
+
errors.push(`spec.routes[${i}].modelRouteRef is required`);
|
|
97
|
+
}
|
|
98
|
+
if (route.weight !== undefined && (typeof route.weight !== 'number' || route.weight < 0)) {
|
|
99
|
+
errors.push(`spec.routes[${i}].weight must be a non-negative number`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Validate rules if present
|
|
105
|
+
if (spec.rules && Array.isArray(spec.rules)) {
|
|
106
|
+
for (let i = 0; i < spec.rules.length; i++) {
|
|
107
|
+
const rule = spec.rules[i];
|
|
108
|
+
if (!rule.name) {
|
|
109
|
+
errors.push(`spec.rules[${i}].name is required`);
|
|
110
|
+
}
|
|
111
|
+
if (!Array.isArray(rule.conditions) || rule.conditions.length === 0) {
|
|
112
|
+
errors.push(`spec.rules[${i}].conditions must be a non-empty array`);
|
|
113
|
+
} else {
|
|
114
|
+
for (let j = 0; j < rule.conditions.length; j++) {
|
|
115
|
+
const cond = rule.conditions[j];
|
|
116
|
+
if (!cond.field) {
|
|
117
|
+
errors.push(`spec.rules[${i}].conditions[${j}].field is required`);
|
|
118
|
+
}
|
|
119
|
+
if (!cond.operator || !VALID_OPERATORS.includes(cond.operator)) {
|
|
120
|
+
errors.push(`spec.rules[${i}].conditions[${j}].operator must be one of: ${VALID_OPERATORS.join(', ')}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!rule.action || !rule.action.route) {
|
|
125
|
+
errors.push(`spec.rules[${i}].action.route is required`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Validate sessionConfig if present
|
|
131
|
+
if (spec.sessionConfig) {
|
|
132
|
+
if (spec.sessionConfig.maxTurns !== undefined && (typeof spec.sessionConfig.maxTurns !== 'number' || spec.sessionConfig.maxTurns < 1)) {
|
|
133
|
+
errors.push('spec.sessionConfig.maxTurns must be a positive number');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { valid: errors.length === 0, errors };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Factory that returns a KradleVirtualModel controller instance.
|
|
142
|
+
* @param {object} [options]
|
|
143
|
+
* @returns {object}
|
|
144
|
+
*/
|
|
145
|
+
export function createVirtualModelController(options = {}) {
|
|
146
|
+
const eventBus = options.eventBus || globalEventBus;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
role: 'virtual-model-controller',
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Validate a KradleVirtualModel resource.
|
|
153
|
+
* @param {object} resource
|
|
154
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
155
|
+
*/
|
|
156
|
+
validate(resource) {
|
|
157
|
+
return validateVirtualModel(resource);
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Evaluate rules against a request context.
|
|
162
|
+
* Returns the first matching rule's route, or null if none match.
|
|
163
|
+
*
|
|
164
|
+
* @param {Array<{ name: string, conditions: Array<{ field: string, operator: string, value: * }>, action: { route: string } }>} rules
|
|
165
|
+
* @param {object} requestContext
|
|
166
|
+
* @returns {{ matched: boolean, routeRef: string, ruleName: string }|null}
|
|
167
|
+
*/
|
|
168
|
+
evaluateRules(rules, requestContext) {
|
|
169
|
+
if (!Array.isArray(rules) || rules.length === 0) return null;
|
|
170
|
+
if (!requestContext || typeof requestContext !== 'object') return null;
|
|
171
|
+
|
|
172
|
+
for (const rule of rules) {
|
|
173
|
+
if (!Array.isArray(rule.conditions)) continue;
|
|
174
|
+
|
|
175
|
+
const allMatch = rule.conditions.every(cond => evaluateCondition(cond, requestContext));
|
|
176
|
+
if (allMatch) {
|
|
177
|
+
return {
|
|
178
|
+
matched: true,
|
|
179
|
+
routeRef: rule.action?.route || null,
|
|
180
|
+
ruleName: rule.name || 'unnamed'
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return null;
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Execute a JS hook body in a sandboxed Function constructor.
|
|
190
|
+
* Returns the result or null on error.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} hookType - e.g. 'routeSelect', 'requestTransform'
|
|
193
|
+
* @param {string} hookBody - JS function body as string
|
|
194
|
+
* @param {object} args - arguments to pass to the hook
|
|
195
|
+
* @param {object} context - execution context
|
|
196
|
+
* @returns {*} result or null on error
|
|
197
|
+
*/
|
|
198
|
+
executeHook(hookType, hookBody, args, context) {
|
|
199
|
+
if (!hookBody || typeof hookBody !== 'string') return null;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const safeArgs = JSON.parse(JSON.stringify(args || {}));
|
|
203
|
+
const safeCtx = JSON.parse(JSON.stringify(context || {}));
|
|
204
|
+
Object.setPrototypeOf(safeArgs, null);
|
|
205
|
+
Object.setPrototypeOf(safeCtx, null);
|
|
206
|
+
Object.freeze(safeArgs);
|
|
207
|
+
Object.freeze(safeCtx);
|
|
208
|
+
const sandbox = Object.create(null);
|
|
209
|
+
sandbox.args = safeArgs;
|
|
210
|
+
sandbox.context = safeCtx;
|
|
211
|
+
sandbox.result = null;
|
|
212
|
+
sandbox.JSON = JSON;
|
|
213
|
+
sandbox.Math = Math;
|
|
214
|
+
sandbox.Date = Date;
|
|
215
|
+
sandbox.parseInt = parseInt;
|
|
216
|
+
sandbox.parseFloat = parseFloat;
|
|
217
|
+
sandbox.isNaN = isNaN;
|
|
218
|
+
sandbox.isFinite = isFinite;
|
|
219
|
+
sandbox.encodeURIComponent = encodeURIComponent;
|
|
220
|
+
sandbox.decodeURIComponent = decodeURIComponent;
|
|
221
|
+
const script = new vm.Script(`result = (function(args, context) { "use strict"; ${hookBody} })(args, context);`);
|
|
222
|
+
const vmContext = vm.createContext(sandbox);
|
|
223
|
+
script.runInContext(vmContext, { timeout: 3000 });
|
|
224
|
+
return sandbox.result;
|
|
225
|
+
} catch (err) {
|
|
226
|
+
console.warn(`[virtual-model-controller] hook execution error (${hookType}): ${err.message}`);
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Resolve which route to use for a virtual model given a request context.
|
|
233
|
+
* Resolution order:
|
|
234
|
+
* 1. Evaluate declarative rules
|
|
235
|
+
* 2. Execute routeSelect hook
|
|
236
|
+
* 3. Weighted random from routes array
|
|
237
|
+
* 4. Try fallbackChain
|
|
238
|
+
*
|
|
239
|
+
* @param {object} virtualModel - KradleVirtualModel resource
|
|
240
|
+
* @param {object} requestContext
|
|
241
|
+
* @param {object[]} [resources] - cluster resources for route lookup
|
|
242
|
+
* @returns {{ routeRef: string|null, appliedRule?: string, appliedHook?: boolean, fallbackUsed?: boolean }}
|
|
243
|
+
*/
|
|
244
|
+
resolveRoute(virtualModel, requestContext, resources = []) {
|
|
245
|
+
const spec = virtualModel?.spec || {};
|
|
246
|
+
|
|
247
|
+
// Check if model is disabled
|
|
248
|
+
if (spec.enabled === false) {
|
|
249
|
+
return { routeRef: null };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 1. Evaluate declarative rules
|
|
253
|
+
if (spec.rules && Array.isArray(spec.rules)) {
|
|
254
|
+
const ruleResult = this.evaluateRules(spec.rules, requestContext);
|
|
255
|
+
if (ruleResult && ruleResult.matched) {
|
|
256
|
+
return {
|
|
257
|
+
routeRef: ruleResult.routeRef,
|
|
258
|
+
appliedRule: ruleResult.ruleName
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 2. Execute routeSelect hook
|
|
264
|
+
if (spec.hooks?.routeSelect) {
|
|
265
|
+
const hookResult = this.executeHook('routeSelect', spec.hooks.routeSelect, {
|
|
266
|
+
routes: spec.routes,
|
|
267
|
+
requestContext
|
|
268
|
+
}, { resources });
|
|
269
|
+
if (hookResult && typeof hookResult === 'string') {
|
|
270
|
+
return {
|
|
271
|
+
routeRef: hookResult,
|
|
272
|
+
appliedHook: true
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 3. Weighted random from routes
|
|
278
|
+
if (Array.isArray(spec.routes) && spec.routes.length > 0) {
|
|
279
|
+
const enabledRoutes = spec.routes.filter(r => r.modelRouteRef);
|
|
280
|
+
const routeRef = weightedRandomSelect(enabledRoutes);
|
|
281
|
+
if (routeRef) {
|
|
282
|
+
return { routeRef };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 4. Try fallbackChain
|
|
287
|
+
if (Array.isArray(spec.fallbackChain)) {
|
|
288
|
+
for (const fallbackRef of spec.fallbackChain) {
|
|
289
|
+
if (fallbackRef) {
|
|
290
|
+
return {
|
|
291
|
+
routeRef: fallbackRef,
|
|
292
|
+
fallbackUsed: true
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return { routeRef: null };
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Transform an outbound request using the requestTransform hook.
|
|
303
|
+
*
|
|
304
|
+
* @param {object} virtualModel
|
|
305
|
+
* @param {object} request
|
|
306
|
+
* @param {object} context
|
|
307
|
+
* @returns {object} modified request (or original on error)
|
|
308
|
+
*/
|
|
309
|
+
transformRequest(virtualModel, request, context) {
|
|
310
|
+
const hookBody = virtualModel?.spec?.hooks?.requestTransform;
|
|
311
|
+
if (!hookBody) return request;
|
|
312
|
+
|
|
313
|
+
const result = this.executeHook('requestTransform', hookBody, { request }, context);
|
|
314
|
+
if (result && typeof result === 'object') return result;
|
|
315
|
+
return request;
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Transform an inbound response using the responseTransform hook.
|
|
320
|
+
*
|
|
321
|
+
* @param {object} virtualModel
|
|
322
|
+
* @param {object} response
|
|
323
|
+
* @param {object} context
|
|
324
|
+
* @returns {object} modified response (or original on error)
|
|
325
|
+
*/
|
|
326
|
+
transformResponse(virtualModel, response, context) {
|
|
327
|
+
const hookBody = virtualModel?.spec?.hooks?.responseTransform;
|
|
328
|
+
if (!hookBody) return response;
|
|
329
|
+
|
|
330
|
+
const result = this.executeHook('responseTransform', hookBody, { response }, context);
|
|
331
|
+
if (result && typeof result === 'object') return result;
|
|
332
|
+
return response;
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Handle a session lifecycle event (turn started, turn ended, escalation, etc.).
|
|
337
|
+
*
|
|
338
|
+
* @param {object} virtualModel
|
|
339
|
+
* @param {string} event - event type ('turnStart', 'turnEnd', 'escalation')
|
|
340
|
+
* @param {object} session - current session state
|
|
341
|
+
* @returns {{ action: string, nextRoute?: string }}
|
|
342
|
+
*/
|
|
343
|
+
handleSessionEvent(virtualModel, event, session) {
|
|
344
|
+
const sessionConfig = virtualModel?.spec?.sessionConfig;
|
|
345
|
+
const hookBody = virtualModel?.spec?.hooks?.sessionLifecycle;
|
|
346
|
+
|
|
347
|
+
// Check maxTurns
|
|
348
|
+
if (sessionConfig?.enabled && sessionConfig?.maxTurns) {
|
|
349
|
+
const turnCount = session?.turnCount || 0;
|
|
350
|
+
if (turnCount >= sessionConfig.maxTurns) {
|
|
351
|
+
return { action: 'terminate' };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Check escalation threshold
|
|
356
|
+
if (sessionConfig?.enabled && sessionConfig?.escalationThreshold) {
|
|
357
|
+
const totalTokens = session?.totalTokens || 0;
|
|
358
|
+
if (totalTokens >= sessionConfig.escalationThreshold) {
|
|
359
|
+
return { action: 'escalate' };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Execute session lifecycle hook
|
|
364
|
+
if (hookBody) {
|
|
365
|
+
const result = this.executeHook('sessionLifecycle', hookBody, { event, session }, {
|
|
366
|
+
sessionConfig: sessionConfig || {}
|
|
367
|
+
});
|
|
368
|
+
if (result && typeof result === 'object' && result.action) {
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return { action: 'continue' };
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Emit observability data via the event bus.
|
|
378
|
+
*
|
|
379
|
+
* @param {object} virtualModel
|
|
380
|
+
* @param {string} event - event type
|
|
381
|
+
* @param {object} metrics - metric data
|
|
382
|
+
*/
|
|
383
|
+
emitObservability(virtualModel, event, metrics) {
|
|
384
|
+
const hookBody = virtualModel?.spec?.hooks?.observe;
|
|
385
|
+
|
|
386
|
+
// Execute observe hook (side-effect only)
|
|
387
|
+
if (hookBody) {
|
|
388
|
+
this.executeHook('observe', hookBody, { event, metrics }, {
|
|
389
|
+
modelName: virtualModel?.spec?.modelName
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Always emit to event bus
|
|
394
|
+
eventBus.emit({
|
|
395
|
+
type: 'virtual-model-observability',
|
|
396
|
+
kind: 'KradleVirtualModel',
|
|
397
|
+
name: virtualModel?.metadata?.name || 'unknown',
|
|
398
|
+
modelName: virtualModel?.spec?.modelName || 'unknown',
|
|
399
|
+
event,
|
|
400
|
+
metrics: metrics || {},
|
|
401
|
+
timestamp: new Date().toISOString()
|
|
402
|
+
});
|
|
403
|
+
},
|
|
404
|
+
|
|
405
|
+
// ── Agentic Lifecycle Hooks ─────────────────────────────────────────────
|
|
406
|
+
|
|
407
|
+
fireSessionStart(virtualModel, session) {
|
|
408
|
+
const hookBody = virtualModel?.spec?.hooks?.onSessionStart;
|
|
409
|
+
if (hookBody) this.executeHook('onSessionStart', hookBody, { session }, { modelName: virtualModel?.spec?.modelName });
|
|
410
|
+
eventBus.emit({ type: 'virtual-model-session-start', kind: 'KradleVirtualModel', name: virtualModel?.metadata?.name, modelName: virtualModel?.spec?.modelName, sessionId: session?.id, timestamp: new Date().toISOString() });
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
fireSessionEnd(virtualModel, session) {
|
|
414
|
+
const hookBody = virtualModel?.spec?.hooks?.onSessionEnd;
|
|
415
|
+
if (hookBody) this.executeHook('onSessionEnd', hookBody, { session }, { modelName: virtualModel?.spec?.modelName });
|
|
416
|
+
eventBus.emit({ type: 'virtual-model-session-end', kind: 'KradleVirtualModel', name: virtualModel?.metadata?.name, modelName: virtualModel?.spec?.modelName, sessionId: session?.id, turns: session?.turnCount, timestamp: new Date().toISOString() });
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
fireTurnEnd(virtualModel, turn, session) {
|
|
420
|
+
const hookBody = virtualModel?.spec?.hooks?.onTurnEnd;
|
|
421
|
+
if (hookBody) {
|
|
422
|
+
const result = this.executeHook('onTurnEnd', hookBody, { turn, session }, { modelName: virtualModel?.spec?.modelName });
|
|
423
|
+
if (result && typeof result === 'object') return result;
|
|
424
|
+
}
|
|
425
|
+
return { action: 'continue' };
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
firePreToolUse(virtualModel, toolCall, session) {
|
|
429
|
+
const hookBody = virtualModel?.spec?.hooks?.onPreToolUse;
|
|
430
|
+
if (hookBody) {
|
|
431
|
+
const result = this.executeHook('onPreToolUse', hookBody, { toolCall, session }, { modelName: virtualModel?.spec?.modelName });
|
|
432
|
+
if (result && typeof result === 'object') return { allow: result.allow !== false, modified: result.modified || null };
|
|
433
|
+
}
|
|
434
|
+
return { allow: true, modified: null };
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
firePostToolUse(virtualModel, toolCall, toolResult, session) {
|
|
438
|
+
const hookBody = virtualModel?.spec?.hooks?.onPostToolUse;
|
|
439
|
+
if (hookBody) {
|
|
440
|
+
const result = this.executeHook('onPostToolUse', hookBody, { toolCall, result: toolResult, session }, { modelName: virtualModel?.spec?.modelName });
|
|
441
|
+
if (result && typeof result === 'object') return { modified: result.modified || null };
|
|
442
|
+
}
|
|
443
|
+
return { modified: null };
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
fireUserPromptSubmit(virtualModel, prompt, session) {
|
|
447
|
+
const hookBody = virtualModel?.spec?.hooks?.onUserPromptSubmit;
|
|
448
|
+
if (hookBody) {
|
|
449
|
+
const result = this.executeHook('onUserPromptSubmit', hookBody, { prompt, session }, { modelName: virtualModel?.spec?.modelName });
|
|
450
|
+
if (result && typeof result === 'object') return { block: !!result.block, modified: result.modified || null };
|
|
451
|
+
}
|
|
452
|
+
return { block: false, modified: null };
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
fireError(virtualModel, error, session) {
|
|
456
|
+
const hookBody = virtualModel?.spec?.hooks?.onError;
|
|
457
|
+
if (hookBody) {
|
|
458
|
+
const result = this.executeHook('onError', hookBody, { error, session }, { modelName: virtualModel?.spec?.modelName });
|
|
459
|
+
if (result && typeof result === 'object') return { retry: !!result.retry, fallbackRoute: result.fallbackRoute || null };
|
|
460
|
+
}
|
|
461
|
+
return { retry: false, fallbackRoute: null };
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
fireCompact(virtualModel, summary, session) {
|
|
465
|
+
const hookBody = virtualModel?.spec?.hooks?.onCompact;
|
|
466
|
+
if (hookBody) {
|
|
467
|
+
const result = this.executeHook('onCompact', hookBody, { summary, session }, { modelName: virtualModel?.spec?.modelName });
|
|
468
|
+
if (result && typeof result === 'object') return { modified: result.modified || null };
|
|
469
|
+
}
|
|
470
|
+
return { modified: null };
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Reconcile a set of virtual models, resolving routes and producing conditions.
|
|
475
|
+
*
|
|
476
|
+
* @param {object[]} virtualModels - array of KradleVirtualModel resources
|
|
477
|
+
* @param {object[]} [resources] - all cluster resources
|
|
478
|
+
* @returns {{ conditions: object[], resolvedModels: object[] }}
|
|
479
|
+
*/
|
|
480
|
+
reconcileVirtualModels(virtualModels, resources = []) {
|
|
481
|
+
const conditions = [];
|
|
482
|
+
const resolvedModels = [];
|
|
483
|
+
|
|
484
|
+
for (const vm of virtualModels) {
|
|
485
|
+
const validation = validateVirtualModel(vm);
|
|
486
|
+
if (!validation.valid) {
|
|
487
|
+
conditions.push({
|
|
488
|
+
type: 'VirtualModelReady',
|
|
489
|
+
status: 'False',
|
|
490
|
+
name: vm?.metadata?.name,
|
|
491
|
+
reason: 'ValidationFailed',
|
|
492
|
+
message: validation.errors.join('; ')
|
|
493
|
+
});
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const spec = vm?.spec || {};
|
|
498
|
+
const routeRefs = (spec.routes || []).map(r => r.modelRouteRef).filter(Boolean);
|
|
499
|
+
|
|
500
|
+
// Check that referenced routes exist
|
|
501
|
+
const missingRoutes = routeRefs.filter(ref =>
|
|
502
|
+
!resources.some(r => r.kind === 'KradleModelRoute' && r.metadata?.name === ref)
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
if (missingRoutes.length > 0 && resources.length > 0) {
|
|
506
|
+
conditions.push({
|
|
507
|
+
type: 'VirtualModelReady',
|
|
508
|
+
status: 'False',
|
|
509
|
+
name: vm.metadata?.name,
|
|
510
|
+
reason: 'RoutesNotFound',
|
|
511
|
+
message: `Missing routes: ${missingRoutes.join(', ')}`
|
|
512
|
+
});
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
conditions.push({
|
|
517
|
+
type: 'VirtualModelReady',
|
|
518
|
+
status: 'True',
|
|
519
|
+
name: vm.metadata?.name,
|
|
520
|
+
reason: 'Reconciled',
|
|
521
|
+
message: `Virtual model "${vm.metadata?.name}" resolved with ${routeRefs.length} route(s)`
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
resolvedModels.push({
|
|
525
|
+
name: vm.metadata?.name,
|
|
526
|
+
modelName: spec.modelName,
|
|
527
|
+
routeCount: routeRefs.length,
|
|
528
|
+
rulesCount: spec.rules?.length || 0,
|
|
529
|
+
hooksEnabled: !!spec.hooks,
|
|
530
|
+
sessionEnabled: !!spec.sessionConfig?.enabled,
|
|
531
|
+
enabled: spec.enabled !== false
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return { conditions, resolvedModels };
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
}
|