@a5c-ai/krate 5.0.1-staging.00fa5317c

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 (256) hide show
  1. package/Dockerfile +31 -0
  2. package/README.md +183 -0
  3. package/bin/krate-demo.mjs +23 -0
  4. package/bin/krate-server.mjs +14 -0
  5. package/dist/krate-controller-ui.json +3205 -0
  6. package/dist/krate-lifecycle.json +201 -0
  7. package/dist/krate-runtime-snapshot.json +3125 -0
  8. package/dist/krate-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-krate-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/components/control-plane.md +78 -0
  60. package/docs/components/data-plane.md +69 -0
  61. package/docs/components/hooks-events.md +67 -0
  62. package/docs/components/identity-rbac-policy.md +73 -0
  63. package/docs/components/kubevela-oam.md +70 -0
  64. package/docs/components/operations-publishing.md +81 -0
  65. package/docs/components/runners-ci.md +66 -0
  66. package/docs/components/web-ui.md +94 -0
  67. package/docs/external/README.md +47 -0
  68. package/docs/external/bidirectional-sync-design.md +134 -0
  69. package/docs/external/cicd-interface.md +64 -0
  70. package/docs/external/external-backend-controllers.md +170 -0
  71. package/docs/external/external-backend-crds.md +234 -0
  72. package/docs/external/external-backend-ui-spec.md +151 -0
  73. package/docs/external/external-backend-ux-flows.md +115 -0
  74. package/docs/external/external-object-mapping.md +125 -0
  75. package/docs/external/git-forge-interface.md +68 -0
  76. package/docs/external/github-integration-design.md +151 -0
  77. package/docs/external/issue-tracking-interface.md +66 -0
  78. package/docs/external/provider-capability-manifests.md +204 -0
  79. package/docs/external/provider-catalog.md +139 -0
  80. package/docs/external/provider-rollout-testing.md +78 -0
  81. package/docs/external/research-results.md +48 -0
  82. package/docs/external/security-auth-permissions.md +81 -0
  83. package/docs/external/sync-state-machines.md +108 -0
  84. package/docs/external/unified-external-backend-model.md +107 -0
  85. package/docs/external/user-facing-changes.md +67 -0
  86. package/docs/gaps.md +161 -0
  87. package/docs/install.md +94 -0
  88. package/docs/krate-design.md +334 -0
  89. package/docs/local-minikube.md +55 -0
  90. package/docs/ontology/README.md +32 -0
  91. package/docs/ontology/bounded-contexts.md +29 -0
  92. package/docs/ontology/events-and-hooks.md +32 -0
  93. package/docs/ontology/oam-kubevela.md +32 -0
  94. package/docs/ontology/operations-and-release.md +25 -0
  95. package/docs/ontology/personas-and-actors.md +32 -0
  96. package/docs/ontology/policies-and-invariants.md +33 -0
  97. package/docs/ontology/problem-space.md +30 -0
  98. package/docs/ontology/resource-contracts.md +40 -0
  99. package/docs/ontology/resource-taxonomy.md +42 -0
  100. package/docs/ontology/runners-and-ci.md +29 -0
  101. package/docs/ontology/solution-space.md +24 -0
  102. package/docs/ontology/storage-and-data-boundaries.md +29 -0
  103. package/docs/ontology/validation-matrix.md +24 -0
  104. package/docs/ontology/web-ui-excellent-flows.md +32 -0
  105. package/docs/ontology/workflows.md +39 -0
  106. package/docs/ontology/world.md +35 -0
  107. package/docs/openapi.yaml +1275 -0
  108. package/docs/product-requirements.md +62 -0
  109. package/docs/roadmap-mvp.md +87 -0
  110. package/docs/system-requirements.md +90 -0
  111. package/docs/tests/README.md +53 -0
  112. package/docs/tests/agent-qa-plan.md +63 -0
  113. package/docs/tests/browser-ui-tests.md +62 -0
  114. package/docs/tests/ci-quality-gates.md +48 -0
  115. package/docs/tests/coverage-model.md +64 -0
  116. package/docs/tests/e2e-scenario-tests.md +53 -0
  117. package/docs/tests/fixtures-test-data.md +63 -0
  118. package/docs/tests/observability-reliability-tests.md +54 -0
  119. package/docs/tests/product-test-matrix.md +145 -0
  120. package/docs/tests/qa-adoption-roadmap.md +130 -0
  121. package/docs/tests/qa-automation-plan.md +101 -0
  122. package/docs/tests/security-compliance-tests.md +57 -0
  123. package/docs/tests/test-framework-tools.md +88 -0
  124. package/docs/tests/test-suite-layout.md +121 -0
  125. package/docs/tests/unit-integration-tests.md +48 -0
  126. package/docs/todo-kyverno +714 -0
  127. package/docs/todos.md +4 -0
  128. package/docs/user-stories.md +78 -0
  129. package/examples/minikube-demo.yaml +190 -0
  130. package/examples/oam-application.yaml +23 -0
  131. package/examples/policy-kyverno-pr-title.yaml +18 -0
  132. package/package.json +63 -0
  133. package/scripts/build.mjs +29 -0
  134. package/scripts/setup-minikube.mjs +65 -0
  135. package/scripts/smoke.mjs +37 -0
  136. package/scripts/validate-doc-coverage.mjs +152 -0
  137. package/scripts/validate-package.mjs +93 -0
  138. package/scripts/validate-ui.mjs +278 -0
  139. package/src/agent-adapter-controller.js +169 -0
  140. package/src/agent-approval-controller.js +170 -0
  141. package/src/agent-context-bundles.js +242 -0
  142. package/src/agent-dispatch-controller.js +209 -0
  143. package/src/agent-gateway-config-controller.js +147 -0
  144. package/src/agent-memory-controller.js +357 -0
  145. package/src/agent-memory-import.js +327 -0
  146. package/src/agent-memory-query.js +292 -0
  147. package/src/agent-memory-repository-source-controller.js +255 -0
  148. package/src/agent-mux-client.js +280 -0
  149. package/src/agent-permission-review.js +250 -0
  150. package/src/agent-project-controller.js +117 -0
  151. package/src/agent-provider-config-controller.js +150 -0
  152. package/src/agent-secret-config-grant-controller.js +282 -0
  153. package/src/agent-session-transcript-controller.js +189 -0
  154. package/src/agent-stack-controller.js +347 -0
  155. package/src/agent-subagent-controller.js +160 -0
  156. package/src/agent-transport-binding-controller.js +121 -0
  157. package/src/agent-trigger-controller.js +381 -0
  158. package/src/agent-workspace-controller.js +702 -0
  159. package/src/agent-writeback-controller.js +302 -0
  160. package/src/api-controller.js +541 -0
  161. package/src/argocd-gitops.js +43 -0
  162. package/src/async-controller.js +207 -0
  163. package/src/audit-controller.js +191 -0
  164. package/src/auth.js +307 -0
  165. package/src/component-catalog.js +41 -0
  166. package/src/control-plane.js +136 -0
  167. package/src/controller-client.js +72 -0
  168. package/src/controller-ui.js +617 -0
  169. package/src/data-plane.js +179 -0
  170. package/src/event-bus.js +61 -0
  171. package/src/external/conflict-controller.js +225 -0
  172. package/src/external/github/auth.js +96 -0
  173. package/src/external/github/cicd.js +180 -0
  174. package/src/external/github/git-forge.js +240 -0
  175. package/src/external/github/index.js +144 -0
  176. package/src/external/github/issue-tracking.js +163 -0
  177. package/src/external/provider-adapter.js +161 -0
  178. package/src/external/provider-resource-factory.js +161 -0
  179. package/src/external/sync-controller.js +235 -0
  180. package/src/external/webhook-controller.js +144 -0
  181. package/src/external/write-controller.js +283 -0
  182. package/src/gitea-backend.js +131 -0
  183. package/src/gitea-service.js +173 -0
  184. package/src/handoff.js +98 -0
  185. package/src/hooks-events.js +63 -0
  186. package/src/http-server.js +377 -0
  187. package/src/identity-policy.js +86 -0
  188. package/src/index.js +57 -0
  189. package/src/kubernetes-controller-async.js +511 -0
  190. package/src/kubernetes-controller.js +878 -0
  191. package/src/kubernetes-resource-gateway.js +48 -0
  192. package/src/notification-controller.js +178 -0
  193. package/src/operations.js +112 -0
  194. package/src/org-scoping.js +5 -0
  195. package/src/resource-model.js +221 -0
  196. package/src/runner-controller.js +272 -0
  197. package/src/runners-ci.js +48 -0
  198. package/src/runtime.js +196 -0
  199. package/src/snapshot-cache.js +157 -0
  200. package/src/web-ui.js +40 -0
  201. package/tests/agent-adapter-controller.test.js +361 -0
  202. package/tests/agent-approval-controller.test.js +173 -0
  203. package/tests/agent-context-bundles.test.js +278 -0
  204. package/tests/agent-dispatch-controller.test.js +315 -0
  205. package/tests/agent-gateway-config-controller.test.js +386 -0
  206. package/tests/agent-memory-controller.test.js +308 -0
  207. package/tests/agent-memory-import-snapshot.test.js +477 -0
  208. package/tests/agent-memory-query.test.js +404 -0
  209. package/tests/agent-memory-repository-source.test.js +514 -0
  210. package/tests/agent-mux-client.test.js +204 -0
  211. package/tests/agent-permission-review-v2.test.js +317 -0
  212. package/tests/agent-permission-review.test.js +209 -0
  213. package/tests/agent-project-controller.test.js +302 -0
  214. package/tests/agent-provider-config-controller.test.js +376 -0
  215. package/tests/agent-resources.test.js +228 -0
  216. package/tests/agent-secret-config-grant.test.js +231 -0
  217. package/tests/agent-session-transcript-controller.test.js +499 -0
  218. package/tests/agent-stack-controller.test.js +221 -0
  219. package/tests/agent-subagent-controller.test.js +201 -0
  220. package/tests/agent-transport-binding-controller.test.js +294 -0
  221. package/tests/agent-trigger-controller.test.js +211 -0
  222. package/tests/agent-trigger-routes.test.js +190 -0
  223. package/tests/agent-trigger-sources.test.js +245 -0
  224. package/tests/agent-workspace-controller.test.js +181 -0
  225. package/tests/agent-writeback.test.js +292 -0
  226. package/tests/approval-persistence.test.js +171 -0
  227. package/tests/async-controller.test.js +252 -0
  228. package/tests/audit-controller.test.js +227 -0
  229. package/tests/codespace-controller.test.js +318 -0
  230. package/tests/deployment.test.js +407 -0
  231. package/tests/e2e/lifecycle.test.js +117 -0
  232. package/tests/event-bus-integration.test.js +190 -0
  233. package/tests/external-github-forge.test.js +560 -0
  234. package/tests/external-github-issues-cicd.test.js +520 -0
  235. package/tests/external-integration.test.js +470 -0
  236. package/tests/external-persistence.test.js +340 -0
  237. package/tests/external-provider-adapter.test.js +365 -0
  238. package/tests/external-resource-model.test.js +215 -0
  239. package/tests/external-webhook-sync.test.js +287 -0
  240. package/tests/external-write-conflict.test.js +353 -0
  241. package/tests/gitea-service.test.js +253 -0
  242. package/tests/health-check-real.test.js +165 -0
  243. package/tests/integration/full-flow.test.js +266 -0
  244. package/tests/krate.test.js +756 -0
  245. package/tests/memory-search-wiring.test.js +270 -0
  246. package/tests/notification-controller.test.js +196 -0
  247. package/tests/notification-integration.test.js +179 -0
  248. package/tests/org-scoping.test.js +687 -0
  249. package/tests/runner-controller.test.js +327 -0
  250. package/tests/runner-integration.test.js +231 -0
  251. package/tests/session-cookie-hmac.test.js +151 -0
  252. package/tests/snapshot-performance.test.js +247 -0
  253. package/tests/sse-events.test.js +107 -0
  254. package/tests/webhook-trigger.test.js +198 -0
  255. package/tests/workspace-volumes.test.js +312 -0
  256. package/tests/writeback-persistence.test.js +207 -0
@@ -0,0 +1,381 @@
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 (sourceType === 'cron') {
194
+ const cronResult = validateCronExpression(spec.cronExpression);
195
+ if (!cronResult.valid) errors.push(`cronExpression: ${cronResult.error}`);
196
+ } else if (sourceType === 'webhook') {
197
+ const webhookResult = validateWebhookTrigger(spec.webhookTrigger);
198
+ if (!webhookResult.valid) errors.push(`webhookTrigger: ${webhookResult.error}`);
199
+ } else if (sourceType === 'comment') {
200
+ const commentResult = validateCommentTrigger(spec.commentTrigger);
201
+ if (!commentResult.valid) errors.push(`commentTrigger: ${commentResult.error}`);
202
+ } else if (sourceType === 'label') {
203
+ const labelResult = validateLabelTrigger(spec.labelTrigger);
204
+ if (!labelResult.valid) errors.push(`labelTrigger: ${labelResult.error}`);
205
+ } else if (sourceType === 'event') {
206
+ if (!Array.isArray(spec.sources) || spec.sources.length === 0) {
207
+ errors.push('sources: must be a non-empty array');
208
+ }
209
+ } else {
210
+ errors.push('spec must include at least one of: cronExpression, webhookTrigger, commentTrigger, labelTrigger, or sources');
211
+ }
212
+
213
+ return { valid: errors.length === 0, errors };
214
+ }
215
+
216
+ export const AGENT_TRIGGER_CONTROLLER_BOUNDARY = {
217
+ role: 'agent-trigger-controller',
218
+ scope: 'Event normalization, rule matching, deduplication, and dispatch creation',
219
+ owns: ['event normalization', 'rule matching', 'trigger execution records', 'dispatch initiation'],
220
+ delegatesTo: ['agent-dispatch-controller', 'resource-model'],
221
+ mustNotOwn: ['event sourcing', 'webhook delivery', 'secret values']
222
+ };
223
+
224
+ export function createAgentTriggerController(options = {}) {
225
+ const dispatchController = options.dispatchController;
226
+
227
+ return {
228
+ role: 'agent-trigger-controller',
229
+
230
+ matchRule(rule, event) {
231
+ // 1. Check event type is in rule.spec.sources
232
+ const sources = rule.spec?.sources || [];
233
+ if (!sources.includes(event.type)) return { matches: false, reason: `Event type '${event.type}' not in rule sources [${sources.join(', ')}]` };
234
+ // 2. Check repository scope (if rule has spec.repository, must match)
235
+ if (rule.spec?.repository && rule.spec.repository !== event.repository) return { matches: false, reason: `Repository '${event.repository}' does not match rule scope '${rule.spec.repository}'` };
236
+ // 3. Check actor filter (if rule has spec.allowedActors)
237
+ if (rule.spec?.allowedActors?.length > 0 && !rule.spec.allowedActors.includes(event.actor)) return { matches: false, reason: `Actor '${event.actor}' not in allowed actors` };
238
+ return { matches: true, reason: 'All conditions met' };
239
+ },
240
+
241
+ evaluateEvent({ event, resources }) {
242
+ const rules = resources.AgentTriggerRule || [];
243
+ const executions = resources.AgentTriggerExecution || [];
244
+ const eventUid = `${event.type}:${event.source?.kind}:${event.source?.name}`;
245
+
246
+ return rules.map(rule => {
247
+ const match = this.matchRule(rule, event);
248
+ const isDuplicate = executions.some(ex =>
249
+ ex.spec?.triggerRule === rule.metadata?.name &&
250
+ ex.spec?.sourceEvent === eventUid &&
251
+ ex.status?.phase !== 'Failed'
252
+ );
253
+ return { rule, matches: match.matches, reason: match.reason, isDuplicate };
254
+ });
255
+ },
256
+
257
+ createTriggerExecution({ rule, event, decision, reason, namespace = 'default', organizationRef = 'default' }) {
258
+ const eventUid = `${event.type}:${event.source?.kind}:${event.source?.name}`;
259
+ const name = `trigger-exec-${rule.metadata?.name}-${Date.now()}`;
260
+ const execution = createResource('AgentTriggerExecution', { name, namespace }, {
261
+ organizationRef,
262
+ triggerRule: rule.metadata?.name,
263
+ sourceEvent: eventUid,
264
+ decision,
265
+ });
266
+ execution.status = { phase: decision, reason, evaluatedAt: new Date().toISOString() };
267
+ return execution;
268
+ },
269
+
270
+ /**
271
+ * Evaluate a normalized inbound webhook event against a set of AgentTriggerRule resources.
272
+ *
273
+ * A rule matches when ALL of:
274
+ * 1. rule.spec.enabled !== false
275
+ * 2. rule.spec.webhookTrigger.events includes event.eventType (or is absent/['*'])
276
+ * 3. rule.spec.webhookTrigger.repository (if set) equals event.repository
277
+ * 4. rule.spec.webhookTrigger.action (if set) equals event.action
278
+ *
279
+ * Duplicate rule names are deduplicated (first occurrence wins).
280
+ *
281
+ * @param {{ eventType: string, repository?: string, ref?: string, action?: string, provider?: string }} event
282
+ * @param {object[]} [rules] Array of AgentTriggerRule resources
283
+ * @returns {{ matchingRules: object[], dispatchIntents: object[] }}
284
+ */
285
+ evaluateWebhookEvent(event, rules) {
286
+ if (!rules || rules.length === 0) {
287
+ return { matchingRules: [], dispatchIntents: [] };
288
+ }
289
+
290
+ const seen = new Set();
291
+ const matchingRules = [];
292
+ const dispatchIntents = [];
293
+
294
+ for (const rule of rules) {
295
+ const ruleName = rule.metadata?.name;
296
+
297
+ // Deduplication
298
+ if (seen.has(ruleName)) continue;
299
+ seen.add(ruleName);
300
+
301
+ // 1. Enabled check
302
+ if (rule.spec?.enabled === false) continue;
303
+
304
+ const wh = rule.spec?.webhookTrigger;
305
+ // Rule must have a webhookTrigger spec to be considered
306
+ if (!wh) continue;
307
+
308
+ // 2. Event type match
309
+ const events = wh.events;
310
+ if (events && !(events.includes('*') || events.includes(event.eventType))) continue;
311
+
312
+ // 3. Repository filter
313
+ if (wh.repository && wh.repository !== event.repository) continue;
314
+
315
+ // 4. Action filter
316
+ if (wh.action && wh.action !== event.action) continue;
317
+
318
+ matchingRules.push(rule);
319
+ dispatchIntents.push({
320
+ rule,
321
+ event,
322
+ agentStack: rule.spec.agentStack,
323
+ taskKind: rule.spec.taskKind || 'diagnostic',
324
+ });
325
+ }
326
+
327
+ return { matchingRules, dispatchIntents };
328
+ },
329
+
330
+ async processEvent({ event, resources, namespace = 'default', organizationRef = 'default' }) {
331
+ const evaluations = this.evaluateEvent({ event, resources });
332
+ const executions = [];
333
+ let dispatched = 0;
334
+ let skipped = 0;
335
+
336
+ for (const { rule, matches, reason, isDuplicate } of evaluations) {
337
+ if (!matches) {
338
+ executions.push(this.createTriggerExecution({ rule, event, decision: 'Skipped', reason, namespace, organizationRef }));
339
+ skipped++;
340
+ continue;
341
+ }
342
+ if (isDuplicate) {
343
+ executions.push(this.createTriggerExecution({ rule, event, decision: 'Deduplicated', reason: 'Already dispatched for this event', namespace, organizationRef }));
344
+ skipped++;
345
+ continue;
346
+ }
347
+
348
+ const execution = this.createTriggerExecution({ rule, event, decision: 'Dispatching', reason, namespace, organizationRef });
349
+
350
+ if (dispatchController) {
351
+ const result = await dispatchController.createManualDispatch({
352
+ repository: event.repository,
353
+ ref: event.ref,
354
+ sourceRefs: [event.source],
355
+ agentStack: rule.spec?.agentStack,
356
+ taskKind: rule.spec?.taskKind || 'diagnostic',
357
+ actor: event.actor,
358
+ namespace,
359
+ organizationRef,
360
+ resources,
361
+ });
362
+ if (result.error) {
363
+ execution.status.phase = 'Failed';
364
+ execution.status.reason = result.message;
365
+ } else {
366
+ execution.status.phase = 'Dispatched';
367
+ execution.status.dispatchRunRef = result.run?.metadata?.name;
368
+ }
369
+ } else {
370
+ execution.status.phase = 'Dispatched';
371
+ execution.status.reason = 'No dispatch controller configured (dry-run)';
372
+ }
373
+
374
+ executions.push(execution);
375
+ dispatched++;
376
+ }
377
+
378
+ return { processed: evaluations.length, dispatched, skipped, executions };
379
+ },
380
+ };
381
+ }