@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.
- package/Dockerfile +31 -0
- package/README.md +183 -0
- package/bin/krate-demo.mjs +23 -0
- package/bin/krate-server.mjs +14 -0
- package/dist/krate-controller-ui.json +3205 -0
- package/dist/krate-lifecycle.json +201 -0
- package/dist/krate-runtime-snapshot.json +3125 -0
- package/dist/krate-summary.json +724 -0
- package/docs/README.md +61 -0
- package/docs/agents/README.md +83 -0
- package/docs/agents/acceptance-test-matrix.md +193 -0
- package/docs/agents/agent-mux-adapter-contract.md +167 -0
- package/docs/agents/agent-mux-source-map.md +310 -0
- package/docs/agents/agent-run-memory-import-spec.md +256 -0
- package/docs/agents/agent-stack-management-spec.md +421 -0
- package/docs/agents/api-contract-spec.md +309 -0
- package/docs/agents/artifacts-writeback-spec.md +145 -0
- package/docs/agents/chart-packaging-spec.md +128 -0
- package/docs/agents/ci-orchestration-spec.md +140 -0
- package/docs/agents/context-assembly-spec.md +219 -0
- package/docs/agents/controller-reconciliation-spec.md +255 -0
- package/docs/agents/crd-schema-spec.md +315 -0
- package/docs/agents/decision-log-open-questions.md +169 -0
- package/docs/agents/developer-implementation-checklist.md +329 -0
- package/docs/agents/dispatching-design.md +262 -0
- package/docs/agents/gaps-agent-mux-to-krate-crds.md +298 -0
- package/docs/agents/glossary.md +66 -0
- package/docs/agents/implementation-blueprint.md +324 -0
- package/docs/agents/implementation-rollout-slices.md +251 -0
- package/docs/agents/memory-context-integration-spec.md +194 -0
- package/docs/agents/memory-ontology-schema-spec.md +253 -0
- package/docs/agents/memory-operations-runbook.md +121 -0
- package/docs/agents/mvp-vertical-slice-spec.md +146 -0
- package/docs/agents/observability-audit-spec.md +265 -0
- package/docs/agents/operator-runbook.md +174 -0
- package/docs/agents/org-memory-api-payload-examples.md +333 -0
- package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
- package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
- package/docs/agents/org-memory-ui-implementation-map.md +114 -0
- package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
- package/docs/agents/org-resource-model-delta-spec.md +111 -0
- package/docs/agents/org-route-resource-model-spec.md +183 -0
- package/docs/agents/org-scoping-namespace-spec.md +114 -0
- package/docs/agents/rbac-secrets-management-spec.md +406 -0
- package/docs/agents/repository-page-integration-spec.md +255 -0
- package/docs/agents/resource-contract-examples.md +808 -0
- package/docs/agents/resource-relationship-map.md +190 -0
- package/docs/agents/security-threat-model.md +188 -0
- package/docs/agents/shared-memory-company-brain-spec.md +358 -0
- package/docs/agents/storage-migration-spec.md +168 -0
- package/docs/agents/subagent-orchestration-spec.md +152 -0
- package/docs/agents/system-overview.md +88 -0
- package/docs/agents/tools-mcp-skills-spec.md +189 -0
- package/docs/agents/traceability-matrix.md +79 -0
- package/docs/agents/ui-flow-spec.md +211 -0
- package/docs/agents/ui-ux-system-spec.md +426 -0
- package/docs/agents/workspace-lifecycle-spec.md +166 -0
- package/docs/architecture-spec.md +78 -0
- package/docs/components/control-plane.md +78 -0
- package/docs/components/data-plane.md +69 -0
- package/docs/components/hooks-events.md +67 -0
- package/docs/components/identity-rbac-policy.md +73 -0
- package/docs/components/kubevela-oam.md +70 -0
- package/docs/components/operations-publishing.md +81 -0
- package/docs/components/runners-ci.md +66 -0
- package/docs/components/web-ui.md +94 -0
- package/docs/external/README.md +47 -0
- package/docs/external/bidirectional-sync-design.md +134 -0
- package/docs/external/cicd-interface.md +64 -0
- package/docs/external/external-backend-controllers.md +170 -0
- package/docs/external/external-backend-crds.md +234 -0
- package/docs/external/external-backend-ui-spec.md +151 -0
- package/docs/external/external-backend-ux-flows.md +115 -0
- package/docs/external/external-object-mapping.md +125 -0
- package/docs/external/git-forge-interface.md +68 -0
- package/docs/external/github-integration-design.md +151 -0
- package/docs/external/issue-tracking-interface.md +66 -0
- package/docs/external/provider-capability-manifests.md +204 -0
- package/docs/external/provider-catalog.md +139 -0
- package/docs/external/provider-rollout-testing.md +78 -0
- package/docs/external/research-results.md +48 -0
- package/docs/external/security-auth-permissions.md +81 -0
- package/docs/external/sync-state-machines.md +108 -0
- package/docs/external/unified-external-backend-model.md +107 -0
- package/docs/external/user-facing-changes.md +67 -0
- package/docs/gaps.md +161 -0
- package/docs/install.md +94 -0
- package/docs/krate-design.md +334 -0
- package/docs/local-minikube.md +55 -0
- package/docs/ontology/README.md +32 -0
- package/docs/ontology/bounded-contexts.md +29 -0
- package/docs/ontology/events-and-hooks.md +32 -0
- package/docs/ontology/oam-kubevela.md +32 -0
- package/docs/ontology/operations-and-release.md +25 -0
- package/docs/ontology/personas-and-actors.md +32 -0
- package/docs/ontology/policies-and-invariants.md +33 -0
- package/docs/ontology/problem-space.md +30 -0
- package/docs/ontology/resource-contracts.md +40 -0
- package/docs/ontology/resource-taxonomy.md +42 -0
- package/docs/ontology/runners-and-ci.md +29 -0
- package/docs/ontology/solution-space.md +24 -0
- package/docs/ontology/storage-and-data-boundaries.md +29 -0
- package/docs/ontology/validation-matrix.md +24 -0
- package/docs/ontology/web-ui-excellent-flows.md +32 -0
- package/docs/ontology/workflows.md +39 -0
- package/docs/ontology/world.md +35 -0
- package/docs/openapi.yaml +1275 -0
- package/docs/product-requirements.md +62 -0
- package/docs/roadmap-mvp.md +87 -0
- package/docs/system-requirements.md +90 -0
- package/docs/tests/README.md +53 -0
- package/docs/tests/agent-qa-plan.md +63 -0
- package/docs/tests/browser-ui-tests.md +62 -0
- package/docs/tests/ci-quality-gates.md +48 -0
- package/docs/tests/coverage-model.md +64 -0
- package/docs/tests/e2e-scenario-tests.md +53 -0
- package/docs/tests/fixtures-test-data.md +63 -0
- package/docs/tests/observability-reliability-tests.md +54 -0
- package/docs/tests/product-test-matrix.md +145 -0
- package/docs/tests/qa-adoption-roadmap.md +130 -0
- package/docs/tests/qa-automation-plan.md +101 -0
- package/docs/tests/security-compliance-tests.md +57 -0
- package/docs/tests/test-framework-tools.md +88 -0
- package/docs/tests/test-suite-layout.md +121 -0
- package/docs/tests/unit-integration-tests.md +48 -0
- package/docs/todo-kyverno +714 -0
- package/docs/todos.md +4 -0
- package/docs/user-stories.md +78 -0
- package/examples/minikube-demo.yaml +190 -0
- package/examples/oam-application.yaml +23 -0
- package/examples/policy-kyverno-pr-title.yaml +18 -0
- package/package.json +63 -0
- package/scripts/build.mjs +29 -0
- package/scripts/setup-minikube.mjs +65 -0
- package/scripts/smoke.mjs +37 -0
- package/scripts/validate-doc-coverage.mjs +152 -0
- package/scripts/validate-package.mjs +93 -0
- package/scripts/validate-ui.mjs +278 -0
- package/src/agent-adapter-controller.js +169 -0
- package/src/agent-approval-controller.js +170 -0
- package/src/agent-context-bundles.js +242 -0
- package/src/agent-dispatch-controller.js +209 -0
- package/src/agent-gateway-config-controller.js +147 -0
- package/src/agent-memory-controller.js +357 -0
- package/src/agent-memory-import.js +327 -0
- package/src/agent-memory-query.js +292 -0
- package/src/agent-memory-repository-source-controller.js +255 -0
- package/src/agent-mux-client.js +280 -0
- package/src/agent-permission-review.js +250 -0
- package/src/agent-project-controller.js +117 -0
- package/src/agent-provider-config-controller.js +150 -0
- package/src/agent-secret-config-grant-controller.js +282 -0
- package/src/agent-session-transcript-controller.js +189 -0
- package/src/agent-stack-controller.js +347 -0
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +381 -0
- package/src/agent-workspace-controller.js +702 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +541 -0
- package/src/argocd-gitops.js +43 -0
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +307 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +72 -0
- package/src/controller-ui.js +617 -0
- package/src/data-plane.js +179 -0
- package/src/event-bus.js +61 -0
- package/src/external/conflict-controller.js +225 -0
- package/src/external/github/auth.js +96 -0
- package/src/external/github/cicd.js +180 -0
- package/src/external/github/git-forge.js +240 -0
- package/src/external/github/index.js +144 -0
- package/src/external/github/issue-tracking.js +163 -0
- package/src/external/provider-adapter.js +161 -0
- package/src/external/provider-resource-factory.js +161 -0
- package/src/external/sync-controller.js +235 -0
- package/src/external/webhook-controller.js +144 -0
- package/src/external/write-controller.js +283 -0
- package/src/gitea-backend.js +131 -0
- package/src/gitea-service.js +173 -0
- package/src/handoff.js +98 -0
- package/src/hooks-events.js +63 -0
- package/src/http-server.js +377 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +57 -0
- package/src/kubernetes-controller-async.js +511 -0
- package/src/kubernetes-controller.js +878 -0
- package/src/kubernetes-resource-gateway.js +48 -0
- package/src/notification-controller.js +178 -0
- package/src/operations.js +112 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +221 -0
- package/src/runner-controller.js +272 -0
- package/src/runners-ci.js +48 -0
- package/src/runtime.js +196 -0
- package/src/snapshot-cache.js +157 -0
- package/src/web-ui.js +40 -0
- package/tests/agent-adapter-controller.test.js +361 -0
- package/tests/agent-approval-controller.test.js +173 -0
- package/tests/agent-context-bundles.test.js +278 -0
- package/tests/agent-dispatch-controller.test.js +315 -0
- package/tests/agent-gateway-config-controller.test.js +386 -0
- package/tests/agent-memory-controller.test.js +308 -0
- package/tests/agent-memory-import-snapshot.test.js +477 -0
- package/tests/agent-memory-query.test.js +404 -0
- package/tests/agent-memory-repository-source.test.js +514 -0
- package/tests/agent-mux-client.test.js +204 -0
- package/tests/agent-permission-review-v2.test.js +317 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-project-controller.test.js +302 -0
- package/tests/agent-provider-config-controller.test.js +376 -0
- package/tests/agent-resources.test.js +228 -0
- package/tests/agent-secret-config-grant.test.js +231 -0
- package/tests/agent-session-transcript-controller.test.js +499 -0
- package/tests/agent-stack-controller.test.js +221 -0
- package/tests/agent-subagent-controller.test.js +201 -0
- package/tests/agent-transport-binding-controller.test.js +294 -0
- package/tests/agent-trigger-controller.test.js +211 -0
- package/tests/agent-trigger-routes.test.js +190 -0
- package/tests/agent-trigger-sources.test.js +245 -0
- package/tests/agent-workspace-controller.test.js +181 -0
- package/tests/agent-writeback.test.js +292 -0
- package/tests/approval-persistence.test.js +171 -0
- package/tests/async-controller.test.js +252 -0
- package/tests/audit-controller.test.js +227 -0
- package/tests/codespace-controller.test.js +318 -0
- package/tests/deployment.test.js +407 -0
- package/tests/e2e/lifecycle.test.js +117 -0
- package/tests/event-bus-integration.test.js +190 -0
- package/tests/external-github-forge.test.js +560 -0
- package/tests/external-github-issues-cicd.test.js +520 -0
- package/tests/external-integration.test.js +470 -0
- package/tests/external-persistence.test.js +340 -0
- package/tests/external-provider-adapter.test.js +365 -0
- package/tests/external-resource-model.test.js +215 -0
- package/tests/external-webhook-sync.test.js +287 -0
- package/tests/external-write-conflict.test.js +353 -0
- package/tests/gitea-service.test.js +253 -0
- package/tests/health-check-real.test.js +165 -0
- package/tests/integration/full-flow.test.js +266 -0
- package/tests/krate.test.js +756 -0
- package/tests/memory-search-wiring.test.js +270 -0
- package/tests/notification-controller.test.js +196 -0
- package/tests/notification-integration.test.js +179 -0
- package/tests/org-scoping.test.js +687 -0
- package/tests/runner-controller.test.js +327 -0
- package/tests/runner-integration.test.js +231 -0
- package/tests/session-cookie-hmac.test.js +151 -0
- package/tests/snapshot-performance.test.js +247 -0
- package/tests/sse-events.test.js +107 -0
- package/tests/webhook-trigger.test.js +198 -0
- package/tests/workspace-volumes.test.js +312 -0
- package/tests/writeback-persistence.test.js +207 -0
|
@@ -0,0 +1,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
|
+
}
|