@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,121 @@
1
+ // Agent Transport Binding Controller — Slice 1.2b
2
+ // Manages AgentTransportBinding resources: connection config validation,
3
+ // health status tracking, and reconnect policy enforcement.
4
+
5
+ export const AGENT_TRANSPORT_BINDING_CONTROLLER_BOUNDARY = {
6
+ role: 'agent-transport-binding-controller',
7
+ scope: 'AgentTransportBinding lifecycle: validation, connection status tracking, reconnect policy enforcement',
8
+ owns: ['binding validation', 'connection status tracking', 'reconnect policy enforcement'],
9
+ delegatesTo: ['resource-model', 'agent-adapter-controller'],
10
+ mustNotOwn: ['secret values', 'dispatch execution', 'Agent Mux sessions', 'adapter implementation']
11
+ };
12
+
13
+ const VALID_PROTOCOLS = ['stdio', 'http', 'websocket', 'unix'];
14
+
15
+ const DEFAULT_RECONNECT_POLICY = Object.freeze({
16
+ maxRetries: 3,
17
+ backoffMs: 1000,
18
+ maxBackoffMs: 30000
19
+ });
20
+
21
+ /**
22
+ * Validate an AgentTransportBinding resource. Returns { valid, errors }.
23
+ * @param {object} resource
24
+ * @returns {{ valid: boolean, errors: string[] }}
25
+ */
26
+ export function validateAgentTransportBinding(resource) {
27
+ const errors = [];
28
+
29
+ // Guard against null/undefined resource
30
+ if (resource == null) {
31
+ errors.push('resource must not be null or undefined');
32
+ return { valid: false, errors };
33
+ }
34
+
35
+ // Validate metadata.name
36
+ if (!resource?.metadata?.name) {
37
+ errors.push('metadata.name is required');
38
+ }
39
+
40
+ const spec = resource?.spec || {};
41
+
42
+ // Validate adapterRef
43
+ if (!spec.adapterRef) {
44
+ errors.push('spec.adapterRef is required');
45
+ }
46
+
47
+ // Validate endpoint
48
+ if (!spec.endpoint) {
49
+ errors.push('spec.endpoint is required');
50
+ }
51
+
52
+ // Validate protocol
53
+ const protocol = spec.protocol;
54
+ if (!protocol) {
55
+ errors.push(`spec.protocol is required; valid protocols are: ${VALID_PROTOCOLS.join(', ')}`);
56
+ } else if (!VALID_PROTOCOLS.includes(protocol)) {
57
+ errors.push(`spec.protocol "${protocol}" is not supported; valid protocols are: ${VALID_PROTOCOLS.join(', ')}`);
58
+ }
59
+
60
+ return { valid: errors.length === 0, errors };
61
+ }
62
+
63
+ /**
64
+ * Factory that returns an AgentTransportBinding controller instance.
65
+ */
66
+ export function createAgentTransportBindingController() {
67
+ return {
68
+ role: 'agent-transport-binding-controller',
69
+
70
+ /**
71
+ * Validate an AgentTransportBinding resource.
72
+ * @param {object} resource
73
+ * @returns {{ valid: boolean, errors: string[] }}
74
+ */
75
+ validate(resource) {
76
+ return validateAgentTransportBinding(resource);
77
+ },
78
+
79
+ /**
80
+ * Return the current connection status for a transport binding.
81
+ * Reads from resource.status.connectionStatus when available,
82
+ * otherwise returns 'unknown'.
83
+ * @param {object} resource
84
+ * @returns {{ bindingName: string, connectionStatus: string }}
85
+ */
86
+ getConnectionStatus(resource) {
87
+ if (resource == null) {
88
+ throw new Error('resource must not be null or undefined');
89
+ }
90
+ const bindingName = resource?.metadata?.name;
91
+ const connectionStatus = resource?.status?.connectionStatus ?? 'unknown';
92
+ return { bindingName, connectionStatus };
93
+ },
94
+
95
+ /**
96
+ * Return the reconnect policy for a transport binding.
97
+ * Merges spec.reconnectPolicy values with defaults.
98
+ * @param {object} resource
99
+ * @returns {{ maxRetries: number, backoffMs: number, maxBackoffMs: number }}
100
+ */
101
+ getReconnectPolicy(resource) {
102
+ if (resource == null) {
103
+ throw new Error('resource must not be null or undefined');
104
+ }
105
+ const specPolicy = resource?.spec?.reconnectPolicy ?? {};
106
+ return {
107
+ maxRetries: specPolicy.maxRetries ?? DEFAULT_RECONNECT_POLICY.maxRetries,
108
+ backoffMs: specPolicy.backoffMs ?? DEFAULT_RECONNECT_POLICY.backoffMs,
109
+ maxBackoffMs: specPolicy.maxBackoffMs ?? DEFAULT_RECONNECT_POLICY.maxBackoffMs
110
+ };
111
+ },
112
+
113
+ /**
114
+ * Return the list of supported protocol types.
115
+ * @returns {string[]}
116
+ */
117
+ getSupportedProtocols() {
118
+ return [...VALID_PROTOCOLS];
119
+ }
120
+ };
121
+ }
@@ -0,0 +1,387 @@
1
+ import { createResource, clone } from './resource-model.js';
2
+
3
+ // ── Cron validation helpers ───────────────────────────────────────────────────
4
+
5
+ /**
6
+ * Validate a 5-field cron expression (minute hour dom month dow).
7
+ * Each field must be a non-empty string composed only of digits, '*', '/', '-', and ','.
8
+ * @param {string} expr
9
+ * @returns {{ valid: boolean, error?: string }}
10
+ */
11
+ export function validateCronExpression(expr) {
12
+ if (typeof expr !== 'string' || expr.trim() === '') {
13
+ return { valid: false, error: 'Cron expression must be a non-empty string' };
14
+ }
15
+ const fields = expr.trim().split(/\s+/);
16
+ if (fields.length !== 5) {
17
+ return { valid: false, error: `Cron expression must have exactly 5 fields (got ${fields.length})` };
18
+ }
19
+ // Each field: digits, *, /, -, , only
20
+ const fieldPattern = /^(\*|(\d+|\*)(\/\d+)?)(-(\d+|\*)(\/\d+)?)?(,(\*|(\d+|\*)(\/\d+)?)(-(\d+|\*)(\/\d+)?)?)*$/;
21
+ // Simpler but robust: allow only [0-9*/,-] characters and at least one valid character
22
+ const validChars = /^[0-9*/,\-]+$/;
23
+ for (let i = 0; i < fields.length; i++) {
24
+ if (!validChars.test(fields[i])) {
25
+ return { valid: false, error: `Invalid character in cron field ${i + 1}: "${fields[i]}"` };
26
+ }
27
+ }
28
+ return { valid: true };
29
+ }
30
+
31
+ /**
32
+ * Calculate the next run date/time after `fromDate` for a valid cron expression.
33
+ * Uses a lightweight iterative approach (no external deps) — minute-level precision.
34
+ * Returns null if the expression is invalid.
35
+ * @param {string} cronExpr
36
+ * @param {Date} [fromDate]
37
+ * @returns {Date|null}
38
+ */
39
+ export function calculateNextRun(cronExpr, fromDate) {
40
+ const validation = validateCronExpression(cronExpr);
41
+ if (!validation.valid) return null;
42
+
43
+ const fields = cronExpr.trim().split(/\s+/);
44
+ const [minuteF, hourF, domF, monthF, dowF] = fields;
45
+
46
+ function matchesField(value, fieldStr, min, max) {
47
+ if (fieldStr === '*') return true;
48
+ const parts = fieldStr.split(',');
49
+ return parts.some(part => {
50
+ if (part.includes('/')) {
51
+ const [range, step] = part.split('/');
52
+ const stepNum = parseInt(step, 10);
53
+ const start = range === '*' ? min : parseInt(range.split('-')[0], 10);
54
+ const end = range === '*' ? max : (range.includes('-') ? parseInt(range.split('-')[1], 10) : max);
55
+ if (isNaN(stepNum)) return false;
56
+ for (let v = start; v <= end; v += stepNum) {
57
+ if (v === value) return true;
58
+ }
59
+ return false;
60
+ }
61
+ if (part.includes('-')) {
62
+ const [lo, hi] = part.split('-').map(Number);
63
+ return value >= lo && value <= hi;
64
+ }
65
+ return parseInt(part, 10) === value;
66
+ });
67
+ }
68
+
69
+ // Start from the next minute after fromDate
70
+ const base = fromDate ? new Date(fromDate) : new Date();
71
+ base.setSeconds(0, 0);
72
+ base.setMinutes(base.getMinutes() + 1);
73
+
74
+ // Iterate up to 366 days * 24 * 60 = ~527,040 minutes
75
+ const MAX_ITER = 527040;
76
+ const candidate = new Date(base);
77
+ for (let i = 0; i < MAX_ITER; i++) {
78
+ const min = candidate.getUTCMinutes();
79
+ const hour = candidate.getUTCHours();
80
+ const dom = candidate.getUTCDate();
81
+ const month = candidate.getUTCMonth() + 1; // 1-12
82
+ const dow = candidate.getUTCDay(); // 0-6
83
+
84
+ if (
85
+ matchesField(month, monthF, 1, 12) &&
86
+ matchesField(dom, domF, 1, 31) &&
87
+ matchesField(dow, dowF, 0, 6) &&
88
+ matchesField(hour, hourF, 0, 23) &&
89
+ matchesField(min, minuteF, 0, 59)
90
+ ) {
91
+ return new Date(candidate);
92
+ }
93
+ candidate.setMinutes(candidate.getMinutes() + 1);
94
+ }
95
+
96
+ return null; // No match found within a year
97
+ }
98
+
99
+ // ── Webhook trigger validation ────────────────────────────────────────────────
100
+
101
+ /**
102
+ * Validate a webhook trigger configuration.
103
+ * @param {{ url: string, secretRef?: string }} config
104
+ * @returns {{ valid: boolean, error?: string }}
105
+ */
106
+ export function validateWebhookTrigger(config) {
107
+ if (!config || typeof config !== 'object') {
108
+ return { valid: false, error: 'Webhook trigger config must be an object' };
109
+ }
110
+ if (!config.url || typeof config.url !== 'string' || config.url.trim() === '') {
111
+ return { valid: false, error: 'Webhook trigger config must include a non-empty url' };
112
+ }
113
+ // Only http/https urls are allowed
114
+ try {
115
+ const parsed = new URL(config.url);
116
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
117
+ return { valid: false, error: `Webhook url scheme must be http or https (got "${parsed.protocol.replace(':', '')}")` };
118
+ }
119
+ } catch {
120
+ return { valid: false, error: `Webhook url is not a valid URL: "${config.url}"` };
121
+ }
122
+ return { valid: true };
123
+ }
124
+
125
+ // ── Comment trigger validation ────────────────────────────────────────────────
126
+
127
+ /**
128
+ * Validate a comment-based trigger configuration.
129
+ * @param {{ pattern: string, repos?: string[] }} config
130
+ * @returns {{ valid: boolean, error?: string }}
131
+ */
132
+ export function validateCommentTrigger(config) {
133
+ if (!config || typeof config !== 'object') {
134
+ return { valid: false, error: 'Comment trigger config must be an object' };
135
+ }
136
+ if (!('pattern' in config) || typeof config.pattern !== 'string' || config.pattern.trim() === '') {
137
+ return { valid: false, error: 'Comment trigger config must include a non-empty pattern string' };
138
+ }
139
+ return { valid: true };
140
+ }
141
+
142
+ // ── Label trigger validation ──────────────────────────────────────────────────
143
+
144
+ const VALID_LABEL_ACTIONS = ['labeled', 'unlabeled'];
145
+
146
+ /**
147
+ * Validate a label-based trigger configuration.
148
+ * @param {{ labels: string[], action?: string }} config
149
+ * @returns {{ valid: boolean, error?: string }}
150
+ */
151
+ export function validateLabelTrigger(config) {
152
+ if (!config || typeof config !== 'object') {
153
+ return { valid: false, error: 'Label trigger config must be an object' };
154
+ }
155
+ if (!Array.isArray(config.labels) || config.labels.length === 0) {
156
+ return { valid: false, error: 'Label trigger config must include a non-empty labels array' };
157
+ }
158
+ if (config.action !== undefined && !VALID_LABEL_ACTIONS.includes(config.action)) {
159
+ return { valid: false, error: `Label trigger action must be one of: ${VALID_LABEL_ACTIONS.join(', ')} (got "${config.action}")` };
160
+ }
161
+ return { valid: true };
162
+ }
163
+
164
+ // ── Source type detection ─────────────────────────────────────────────────────
165
+
166
+ /**
167
+ * Determine the source type for a trigger rule based on its spec.
168
+ * @param {{ spec?: object }} rule
169
+ * @returns {'cron'|'webhook'|'comment'|'label'|'event'|'unknown'}
170
+ */
171
+ export function getTriggerSourceType(rule) {
172
+ const spec = rule?.spec || {};
173
+ if (spec.cronExpression !== undefined) return 'cron';
174
+ if (spec.webhookTrigger !== undefined) return 'webhook';
175
+ if (spec.commentTrigger !== undefined) return 'comment';
176
+ if (spec.labelTrigger !== undefined) return 'label';
177
+ if (spec.sources !== undefined) return 'event';
178
+ return 'unknown';
179
+ }
180
+
181
+ // ── Trigger rule validation ───────────────────────────────────────────────────
182
+
183
+ /**
184
+ * Validate an AgentTriggerRule resource, including source-specific sub-configs.
185
+ * @param {object} rule
186
+ * @returns {{ valid: boolean, errors: string[] }}
187
+ */
188
+ export function validateTriggerRule(rule) {
189
+ const errors = [];
190
+ const spec = rule?.spec || {};
191
+ const sourceType = getTriggerSourceType(rule);
192
+
193
+ if (!spec.agentStack && !spec.agentDefinition) {
194
+ errors.push('target: must include agentStack or agentDefinition');
195
+ }
196
+
197
+ if (sourceType === 'cron') {
198
+ const cronResult = validateCronExpression(spec.cronExpression);
199
+ if (!cronResult.valid) errors.push(`cronExpression: ${cronResult.error}`);
200
+ } else if (sourceType === 'webhook') {
201
+ const webhookResult = validateWebhookTrigger(spec.webhookTrigger);
202
+ if (!webhookResult.valid) errors.push(`webhookTrigger: ${webhookResult.error}`);
203
+ } else if (sourceType === 'comment') {
204
+ const commentResult = validateCommentTrigger(spec.commentTrigger);
205
+ if (!commentResult.valid) errors.push(`commentTrigger: ${commentResult.error}`);
206
+ } else if (sourceType === 'label') {
207
+ const labelResult = validateLabelTrigger(spec.labelTrigger);
208
+ if (!labelResult.valid) errors.push(`labelTrigger: ${labelResult.error}`);
209
+ } else if (sourceType === 'event') {
210
+ if (!Array.isArray(spec.sources) || spec.sources.length === 0) {
211
+ errors.push('sources: must be a non-empty array');
212
+ }
213
+ } else {
214
+ errors.push('spec must include at least one of: cronExpression, webhookTrigger, commentTrigger, labelTrigger, or sources');
215
+ }
216
+
217
+ return { valid: errors.length === 0, errors };
218
+ }
219
+
220
+ export const AGENT_TRIGGER_CONTROLLER_BOUNDARY = {
221
+ role: 'agent-trigger-controller',
222
+ scope: 'Event normalization, rule matching, deduplication, and dispatch creation',
223
+ owns: ['event normalization', 'rule matching', 'trigger execution records', 'dispatch initiation'],
224
+ delegatesTo: ['agent-dispatch-controller', 'resource-model'],
225
+ mustNotOwn: ['event sourcing', 'webhook delivery', 'secret values']
226
+ };
227
+
228
+ export function createAgentTriggerController(options = {}) {
229
+ const dispatchController = options.dispatchController;
230
+
231
+ return {
232
+ role: 'agent-trigger-controller',
233
+
234
+ matchRule(rule, event) {
235
+ // 1. Check event type is in rule.spec.sources
236
+ const sources = rule.spec?.sources || [];
237
+ if (!sources.includes(event.type)) return { matches: false, reason: `Event type '${event.type}' not in rule sources [${sources.join(', ')}]` };
238
+ // 2. Check repository scope (if rule has spec.repository, must match)
239
+ if (rule.spec?.repository && rule.spec.repository !== event.repository) return { matches: false, reason: `Repository '${event.repository}' does not match rule scope '${rule.spec.repository}'` };
240
+ // 3. Check actor filter (if rule has spec.allowedActors)
241
+ if (rule.spec?.allowedActors?.length > 0 && !rule.spec.allowedActors.includes(event.actor)) return { matches: false, reason: `Actor '${event.actor}' not in allowed actors` };
242
+ return { matches: true, reason: 'All conditions met' };
243
+ },
244
+
245
+ evaluateEvent({ event, resources }) {
246
+ const rules = resources.AgentTriggerRule || [];
247
+ const executions = resources.AgentTriggerExecution || [];
248
+ const eventUid = `${event.type}:${event.source?.kind}:${event.source?.name}`;
249
+
250
+ return rules.map(rule => {
251
+ const match = this.matchRule(rule, event);
252
+ const isDuplicate = executions.some(ex =>
253
+ ex.spec?.triggerRule === rule.metadata?.name &&
254
+ ex.spec?.sourceEvent === eventUid &&
255
+ ex.status?.phase !== 'Failed'
256
+ );
257
+ return { rule, matches: match.matches, reason: match.reason, isDuplicate };
258
+ });
259
+ },
260
+
261
+ createTriggerExecution({ rule, event, decision, reason, namespace = 'default', organizationRef = 'default' }) {
262
+ const eventUid = `${event.type}:${event.source?.kind}:${event.source?.name}`;
263
+ const name = `trigger-exec-${rule.metadata?.name}-${Date.now()}`;
264
+ const execution = createResource('AgentTriggerExecution', { name, namespace }, {
265
+ organizationRef,
266
+ triggerRule: rule.metadata?.name,
267
+ sourceEvent: eventUid,
268
+ decision,
269
+ });
270
+ execution.status = { phase: decision, reason, evaluatedAt: new Date().toISOString() };
271
+ return execution;
272
+ },
273
+
274
+ /**
275
+ * Evaluate a normalized inbound webhook event against a set of AgentTriggerRule resources.
276
+ *
277
+ * A rule matches when ALL of:
278
+ * 1. rule.spec.enabled !== false
279
+ * 2. rule.spec.webhookTrigger.events includes event.eventType (or is absent/['*'])
280
+ * 3. rule.spec.webhookTrigger.repository (if set) equals event.repository
281
+ * 4. rule.spec.webhookTrigger.action (if set) equals event.action
282
+ *
283
+ * Duplicate rule names are deduplicated (first occurrence wins).
284
+ *
285
+ * @param {{ eventType: string, repository?: string, ref?: string, action?: string, provider?: string }} event
286
+ * @param {object[]} [rules] Array of AgentTriggerRule resources
287
+ * @returns {{ matchingRules: object[], dispatchIntents: object[] }}
288
+ */
289
+ evaluateWebhookEvent(event, rules) {
290
+ if (!rules || rules.length === 0) {
291
+ return { matchingRules: [], dispatchIntents: [] };
292
+ }
293
+
294
+ const seen = new Set();
295
+ const matchingRules = [];
296
+ const dispatchIntents = [];
297
+
298
+ for (const rule of rules) {
299
+ const ruleName = rule.metadata?.name;
300
+
301
+ // Deduplication
302
+ if (seen.has(ruleName)) continue;
303
+ seen.add(ruleName);
304
+
305
+ // 1. Enabled check
306
+ if (rule.spec?.enabled === false) continue;
307
+
308
+ const wh = rule.spec?.webhookTrigger;
309
+ // Rule must have a webhookTrigger spec to be considered
310
+ if (!wh) continue;
311
+
312
+ // 2. Event type match
313
+ const events = wh.events;
314
+ if (events && !(events.includes('*') || events.includes(event.eventType))) continue;
315
+
316
+ // 3. Repository filter
317
+ if (wh.repository && wh.repository !== event.repository) continue;
318
+
319
+ // 4. Action filter
320
+ if (wh.action && wh.action !== event.action) continue;
321
+
322
+ matchingRules.push(rule);
323
+ dispatchIntents.push({
324
+ rule,
325
+ event,
326
+ agentDefinition: rule.spec.agentDefinition,
327
+ agentStack: rule.spec.agentStack,
328
+ taskKind: rule.spec.taskKind || 'diagnostic',
329
+ });
330
+ }
331
+
332
+ return { matchingRules, dispatchIntents };
333
+ },
334
+
335
+ async processEvent({ event, resources, namespace = 'default', organizationRef = 'default' }) {
336
+ const evaluations = this.evaluateEvent({ event, resources });
337
+ const executions = [];
338
+ let dispatched = 0;
339
+ let skipped = 0;
340
+
341
+ for (const { rule, matches, reason, isDuplicate } of evaluations) {
342
+ if (!matches) {
343
+ executions.push(this.createTriggerExecution({ rule, event, decision: 'Skipped', reason, namespace, organizationRef }));
344
+ skipped++;
345
+ continue;
346
+ }
347
+ if (isDuplicate) {
348
+ executions.push(this.createTriggerExecution({ rule, event, decision: 'Deduplicated', reason: 'Already dispatched for this event', namespace, organizationRef }));
349
+ skipped++;
350
+ continue;
351
+ }
352
+
353
+ const execution = this.createTriggerExecution({ rule, event, decision: 'Dispatching', reason, namespace, organizationRef });
354
+
355
+ if (dispatchController) {
356
+ const result = await dispatchController.createManualDispatch({
357
+ repository: event.repository,
358
+ ref: event.ref,
359
+ sourceRefs: [event.source],
360
+ agentDefinition: rule.spec?.agentDefinition,
361
+ agentStack: rule.spec?.agentStack,
362
+ taskKind: rule.spec?.taskKind || 'diagnostic',
363
+ actor: event.actor,
364
+ namespace,
365
+ organizationRef,
366
+ resources,
367
+ });
368
+ if (result.error) {
369
+ execution.status.phase = 'Failed';
370
+ execution.status.reason = result.message;
371
+ } else {
372
+ execution.status.phase = 'Dispatched';
373
+ execution.status.dispatchRunRef = result.run?.metadata?.name;
374
+ }
375
+ } else {
376
+ execution.status.phase = 'Dispatched';
377
+ execution.status.reason = 'No dispatch controller configured (dry-run)';
378
+ }
379
+
380
+ executions.push(execution);
381
+ dispatched++;
382
+ }
383
+
384
+ return { processed: evaluations.length, dispatched, skipped, executions };
385
+ },
386
+ };
387
+ }