@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.
Files changed (295) hide show
  1. package/Dockerfile +31 -0
  2. package/README.md +187 -0
  3. package/bin/kradle-demo.mjs +23 -0
  4. package/bin/kradle-server.mjs +14 -0
  5. package/dist/kradle-controller-ui.json +3482 -0
  6. package/dist/kradle-lifecycle.json +201 -0
  7. package/dist/kradle-runtime-snapshot.json +3125 -0
  8. package/dist/kradle-summary.json +724 -0
  9. package/docs/README.md +61 -0
  10. package/docs/agents/README.md +83 -0
  11. package/docs/agents/acceptance-test-matrix.md +193 -0
  12. package/docs/agents/agent-mux-adapter-contract.md +167 -0
  13. package/docs/agents/agent-mux-source-map.md +310 -0
  14. package/docs/agents/agent-run-memory-import-spec.md +256 -0
  15. package/docs/agents/agent-stack-management-spec.md +421 -0
  16. package/docs/agents/api-contract-spec.md +309 -0
  17. package/docs/agents/artifacts-writeback-spec.md +145 -0
  18. package/docs/agents/chart-packaging-spec.md +128 -0
  19. package/docs/agents/ci-orchestration-spec.md +140 -0
  20. package/docs/agents/context-assembly-spec.md +219 -0
  21. package/docs/agents/controller-reconciliation-spec.md +255 -0
  22. package/docs/agents/crd-schema-spec.md +315 -0
  23. package/docs/agents/decision-log-open-questions.md +169 -0
  24. package/docs/agents/developer-implementation-checklist.md +329 -0
  25. package/docs/agents/dispatching-design.md +262 -0
  26. package/docs/agents/gaps-agent-mux-to-kradle-crds.md +298 -0
  27. package/docs/agents/glossary.md +66 -0
  28. package/docs/agents/implementation-blueprint.md +324 -0
  29. package/docs/agents/implementation-rollout-slices.md +251 -0
  30. package/docs/agents/memory-context-integration-spec.md +194 -0
  31. package/docs/agents/memory-ontology-schema-spec.md +253 -0
  32. package/docs/agents/memory-operations-runbook.md +121 -0
  33. package/docs/agents/mvp-vertical-slice-spec.md +146 -0
  34. package/docs/agents/observability-audit-spec.md +265 -0
  35. package/docs/agents/operator-runbook.md +174 -0
  36. package/docs/agents/org-memory-api-payload-examples.md +333 -0
  37. package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
  38. package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
  39. package/docs/agents/org-memory-ui-implementation-map.md +114 -0
  40. package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
  41. package/docs/agents/org-resource-model-delta-spec.md +111 -0
  42. package/docs/agents/org-route-resource-model-spec.md +183 -0
  43. package/docs/agents/org-scoping-namespace-spec.md +114 -0
  44. package/docs/agents/rbac-secrets-management-spec.md +406 -0
  45. package/docs/agents/repository-page-integration-spec.md +255 -0
  46. package/docs/agents/resource-contract-examples.md +808 -0
  47. package/docs/agents/resource-relationship-map.md +190 -0
  48. package/docs/agents/security-threat-model.md +188 -0
  49. package/docs/agents/shared-memory-company-brain-spec.md +358 -0
  50. package/docs/agents/storage-migration-spec.md +168 -0
  51. package/docs/agents/subagent-orchestration-spec.md +152 -0
  52. package/docs/agents/system-overview.md +88 -0
  53. package/docs/agents/tools-mcp-skills-spec.md +189 -0
  54. package/docs/agents/traceability-matrix.md +79 -0
  55. package/docs/agents/ui-flow-spec.md +211 -0
  56. package/docs/agents/ui-ux-system-spec.md +426 -0
  57. package/docs/agents/workspace-lifecycle-spec.md +166 -0
  58. package/docs/architecture-spec.md +78 -0
  59. package/docs/architecture-v2.md +2759 -0
  60. package/docs/components/control-plane.md +78 -0
  61. package/docs/components/data-plane.md +69 -0
  62. package/docs/components/hooks-events.md +67 -0
  63. package/docs/components/identity-rbac-policy.md +73 -0
  64. package/docs/components/kubevela-oam.md +70 -0
  65. package/docs/components/operations-publishing.md +81 -0
  66. package/docs/components/runners-ci.md +66 -0
  67. package/docs/components/web-ui.md +94 -0
  68. package/docs/crd-behaviors-and-relationships.md +3926 -0
  69. package/docs/external/README.md +47 -0
  70. package/docs/external/bidirectional-sync-design.md +134 -0
  71. package/docs/external/cicd-interface.md +64 -0
  72. package/docs/external/external-backend-controllers.md +170 -0
  73. package/docs/external/external-backend-crds.md +234 -0
  74. package/docs/external/external-backend-ui-spec.md +151 -0
  75. package/docs/external/external-backend-ux-flows.md +115 -0
  76. package/docs/external/external-object-mapping.md +125 -0
  77. package/docs/external/git-forge-interface.md +68 -0
  78. package/docs/external/github-integration-design.md +151 -0
  79. package/docs/external/issue-tracking-interface.md +66 -0
  80. package/docs/external/provider-capability-manifests.md +204 -0
  81. package/docs/external/provider-catalog.md +139 -0
  82. package/docs/external/provider-rollout-testing.md +78 -0
  83. package/docs/external/research-results.md +48 -0
  84. package/docs/external/security-auth-permissions.md +81 -0
  85. package/docs/external/sync-state-machines.md +108 -0
  86. package/docs/external/unified-external-backend-model.md +107 -0
  87. package/docs/external/user-facing-changes.md +67 -0
  88. package/docs/gaps.md +161 -0
  89. package/docs/install.md +94 -0
  90. package/docs/integration-and-design-decisions.md +1530 -0
  91. package/docs/kradle-design.md +334 -0
  92. package/docs/local-minikube.md +55 -0
  93. package/docs/ontology/README.md +32 -0
  94. package/docs/ontology/bounded-contexts.md +29 -0
  95. package/docs/ontology/events-and-hooks.md +32 -0
  96. package/docs/ontology/oam-kubevela.md +32 -0
  97. package/docs/ontology/operations-and-release.md +25 -0
  98. package/docs/ontology/personas-and-actors.md +32 -0
  99. package/docs/ontology/policies-and-invariants.md +33 -0
  100. package/docs/ontology/problem-space.md +30 -0
  101. package/docs/ontology/resource-contracts.md +40 -0
  102. package/docs/ontology/resource-taxonomy.md +42 -0
  103. package/docs/ontology/runners-and-ci.md +29 -0
  104. package/docs/ontology/solution-space.md +24 -0
  105. package/docs/ontology/storage-and-data-boundaries.md +29 -0
  106. package/docs/ontology/validation-matrix.md +24 -0
  107. package/docs/ontology/web-ui-excellent-flows.md +32 -0
  108. package/docs/ontology/workflows.md +39 -0
  109. package/docs/ontology/world.md +35 -0
  110. package/docs/openapi.yaml +1291 -0
  111. package/docs/product-requirements.md +62 -0
  112. package/docs/requirements-v2.md +235 -0
  113. package/docs/roadmap-mvp.md +87 -0
  114. package/docs/sdk-api-reference.md +1108 -0
  115. package/docs/system-requirements.md +90 -0
  116. package/docs/system-spec-v2.md +1230 -0
  117. package/docs/tests/README.md +53 -0
  118. package/docs/tests/agent-qa-plan.md +63 -0
  119. package/docs/tests/browser-ui-tests.md +62 -0
  120. package/docs/tests/ci-quality-gates.md +48 -0
  121. package/docs/tests/coverage-model.md +64 -0
  122. package/docs/tests/e2e-scenario-tests.md +53 -0
  123. package/docs/tests/fixtures-test-data.md +63 -0
  124. package/docs/tests/observability-reliability-tests.md +54 -0
  125. package/docs/tests/product-test-matrix.md +145 -0
  126. package/docs/tests/qa-adoption-roadmap.md +130 -0
  127. package/docs/tests/qa-automation-plan.md +101 -0
  128. package/docs/tests/security-compliance-tests.md +57 -0
  129. package/docs/tests/test-framework-tools.md +88 -0
  130. package/docs/tests/test-suite-layout.md +121 -0
  131. package/docs/tests/unit-integration-tests.md +48 -0
  132. package/docs/todo-kyverno +714 -0
  133. package/docs/todos.md +4 -0
  134. package/docs/user-stories.md +78 -0
  135. package/docs/web-console-spec.md +533 -0
  136. package/examples/minikube-demo.yaml +190 -0
  137. package/examples/oam-application.yaml +23 -0
  138. package/examples/policy-kyverno-pr-title.yaml +18 -0
  139. package/package.json +66 -0
  140. package/scripts/build.mjs +29 -0
  141. package/scripts/setup-minikube.mjs +65 -0
  142. package/scripts/smoke.mjs +37 -0
  143. package/scripts/validate-doc-coverage.mjs +152 -0
  144. package/scripts/validate-package.mjs +95 -0
  145. package/scripts/validate-ui.mjs +305 -0
  146. package/src/agent-adapter-controller.js +169 -0
  147. package/src/agent-approval-controller.js +170 -0
  148. package/src/agent-context-bundles.js +242 -0
  149. package/src/agent-dispatch-controller.js +549 -0
  150. package/src/agent-gateway-config-controller.js +147 -0
  151. package/src/agent-identity-migration.js +115 -0
  152. package/src/agent-memory-controller.js +357 -0
  153. package/src/agent-memory-import.js +327 -0
  154. package/src/agent-memory-query.js +292 -0
  155. package/src/agent-memory-repository-source-controller.js +255 -0
  156. package/src/agent-mux-client.js +589 -0
  157. package/src/agent-permission-review.js +250 -0
  158. package/src/agent-persona-controller.js +135 -0
  159. package/src/agent-project-controller.js +117 -0
  160. package/src/agent-prompt-composition.js +55 -0
  161. package/src/agent-provider-config-controller.js +151 -0
  162. package/src/agent-secret-config-grant-controller.js +282 -0
  163. package/src/agent-session-transcript-controller.js +189 -0
  164. package/src/agent-stack-controller.js +421 -0
  165. package/src/agent-subagent-controller.js +160 -0
  166. package/src/agent-transport-binding-controller.js +121 -0
  167. package/src/agent-trigger-controller.js +387 -0
  168. package/src/agent-workspace-controller.js +702 -0
  169. package/src/agent-writeback-controller.js +302 -0
  170. package/src/api-controller.js +621 -0
  171. package/src/argocd-gitops.js +43 -0
  172. package/src/artifact-registry-controller.js +542 -0
  173. package/src/assistant-runtime.js +284 -0
  174. package/src/async-controller.js +207 -0
  175. package/src/audit-controller.js +191 -0
  176. package/src/auth.js +310 -0
  177. package/src/component-catalog.js +41 -0
  178. package/src/control-plane.js +136 -0
  179. package/src/controller-client.js +112 -0
  180. package/src/controller-ui.js +620 -0
  181. package/src/data-plane.js +179 -0
  182. package/src/event-bus.js +397 -0
  183. package/src/external/conflict-controller.js +225 -0
  184. package/src/external/github/auth.js +96 -0
  185. package/src/external/github/cicd.js +180 -0
  186. package/src/external/github/git-forge.js +240 -0
  187. package/src/external/github/index.js +144 -0
  188. package/src/external/github/issue-tracking.js +163 -0
  189. package/src/external/provider-adapter.js +161 -0
  190. package/src/external/provider-resource-factory.js +221 -0
  191. package/src/external/sync-controller.js +235 -0
  192. package/src/external/webhook-controller.js +144 -0
  193. package/src/external/write-controller.js +283 -0
  194. package/src/gitea-backend.js +131 -0
  195. package/src/gitea-service.js +173 -0
  196. package/src/handoff.js +98 -0
  197. package/src/health-probes.js +134 -0
  198. package/src/hooks-events.js +63 -0
  199. package/src/hooks-lifecycle.js +117 -0
  200. package/src/http-server.js +409 -0
  201. package/src/identity-policy.js +86 -0
  202. package/src/index.js +71 -0
  203. package/src/jitsi-agent-bridge.js +141 -0
  204. package/src/jitsi-meeting-controller.js +291 -0
  205. package/src/jitsi-sync-controller.js +198 -0
  206. package/src/kradle-inference-service-controller.js +246 -0
  207. package/src/kubernetes-controller-async.js +531 -0
  208. package/src/kubernetes-controller.js +904 -0
  209. package/src/kubernetes-resource-gateway.js +48 -0
  210. package/src/model-route-controller.js +364 -0
  211. package/src/notification-controller.js +178 -0
  212. package/src/operations.js +112 -0
  213. package/src/org-scoping.js +5 -0
  214. package/src/resource-model.js +282 -0
  215. package/src/runner-controller.js +272 -0
  216. package/src/runners-ci.js +48 -0
  217. package/src/runtime.js +196 -0
  218. package/src/snapshot-cache.js +157 -0
  219. package/src/virtual-model-controller.js +538 -0
  220. package/src/virtual-model-hook-bridge.js +200 -0
  221. package/src/web-ui.js +40 -0
  222. package/tests/agent-adapter-controller.test.js +361 -0
  223. package/tests/agent-approval-controller.test.js +173 -0
  224. package/tests/agent-context-bundles.test.js +278 -0
  225. package/tests/agent-dispatch-controller.test.js +679 -0
  226. package/tests/agent-gateway-config-controller.test.js +386 -0
  227. package/tests/agent-identity-migration.test.js +87 -0
  228. package/tests/agent-memory-controller.test.js +461 -0
  229. package/tests/agent-memory-import-snapshot.test.js +477 -0
  230. package/tests/agent-memory-query.test.js +404 -0
  231. package/tests/agent-memory-repository-source.test.js +514 -0
  232. package/tests/agent-mux-client.test.js +389 -0
  233. package/tests/agent-mux-integration.test.js +971 -0
  234. package/tests/agent-permission-review-v2.test.js +317 -0
  235. package/tests/agent-permission-review.test.js +209 -0
  236. package/tests/agent-persona-controller.test.js +127 -0
  237. package/tests/agent-project-controller.test.js +302 -0
  238. package/tests/agent-prompt-composition.test.js +76 -0
  239. package/tests/agent-provider-config-controller.test.js +376 -0
  240. package/tests/agent-resources.test.js +303 -0
  241. package/tests/agent-secret-config-grant.test.js +231 -0
  242. package/tests/agent-session-transcript-controller.test.js +499 -0
  243. package/tests/agent-stack-controller.test.js +283 -0
  244. package/tests/agent-subagent-controller.test.js +201 -0
  245. package/tests/agent-transport-binding-controller.test.js +294 -0
  246. package/tests/agent-trigger-controller.test.js +271 -0
  247. package/tests/agent-trigger-routes.test.js +190 -0
  248. package/tests/agent-trigger-sources.test.js +245 -0
  249. package/tests/agent-workspace-controller.test.js +181 -0
  250. package/tests/agent-writeback.test.js +292 -0
  251. package/tests/approval-persistence.test.js +171 -0
  252. package/tests/artifact-registry.test.js +511 -0
  253. package/tests/assistant-runtime.test.js +506 -0
  254. package/tests/async-controller.test.js +252 -0
  255. package/tests/audit-controller.test.js +227 -0
  256. package/tests/codespace-controller.test.js +318 -0
  257. package/tests/controller-client.test.js +133 -0
  258. package/tests/deployment.test.js +527 -0
  259. package/tests/e2e/lifecycle.test.js +120 -0
  260. package/tests/event-bus-integration.test.js +355 -0
  261. package/tests/external-github-forge.test.js +560 -0
  262. package/tests/external-github-issues-cicd.test.js +520 -0
  263. package/tests/external-integration.test.js +470 -0
  264. package/tests/external-persistence.test.js +415 -0
  265. package/tests/external-provider-adapter.test.js +365 -0
  266. package/tests/external-resource-model.test.js +223 -0
  267. package/tests/external-webhook-sync.test.js +287 -0
  268. package/tests/external-write-conflict.test.js +353 -0
  269. package/tests/gitea-service.test.js +253 -0
  270. package/tests/health-check-real.test.js +165 -0
  271. package/tests/health-probes.test.js +90 -0
  272. package/tests/hooks-lifecycle.test.js +364 -0
  273. package/tests/integration/full-flow.test.js +266 -0
  274. package/tests/jitsi-agent-bridge.test.js +119 -0
  275. package/tests/jitsi-helm-integration.test.js +77 -0
  276. package/tests/jitsi-meeting-controller.test.js +170 -0
  277. package/tests/jitsi-resource-model.test.js +73 -0
  278. package/tests/jitsi-sync-controller.test.js +112 -0
  279. package/tests/kradle-inference-service.test.js +689 -0
  280. package/tests/kradle.test.js +779 -0
  281. package/tests/memory-search-wiring.test.js +270 -0
  282. package/tests/model-route-controller.test.js +733 -0
  283. package/tests/notification-controller.test.js +196 -0
  284. package/tests/notification-integration.test.js +179 -0
  285. package/tests/org-scoping.test.js +687 -0
  286. package/tests/runner-controller.test.js +327 -0
  287. package/tests/runner-integration.test.js +231 -0
  288. package/tests/session-cookie-hmac.test.js +151 -0
  289. package/tests/snapshot-performance.test.js +315 -0
  290. package/tests/sse-events.test.js +107 -0
  291. package/tests/virtual-model-controller.test.js +877 -0
  292. package/tests/virtual-model-hook-bridge.test.js +384 -0
  293. package/tests/webhook-trigger.test.js +198 -0
  294. package/tests/workspace-volumes.test.js +312 -0
  295. 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
+ }