@a5c-ai/kradle 5.0.1-staging.3abdf9534c25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +31 -0
- package/README.md +187 -0
- package/bin/kradle-demo.mjs +23 -0
- package/bin/kradle-server.mjs +14 -0
- package/dist/kradle-controller-ui.json +3482 -0
- package/dist/kradle-lifecycle.json +201 -0
- package/dist/kradle-runtime-snapshot.json +3125 -0
- package/dist/kradle-summary.json +724 -0
- package/docs/README.md +61 -0
- package/docs/agents/README.md +83 -0
- package/docs/agents/acceptance-test-matrix.md +193 -0
- package/docs/agents/agent-mux-adapter-contract.md +167 -0
- package/docs/agents/agent-mux-source-map.md +310 -0
- package/docs/agents/agent-run-memory-import-spec.md +256 -0
- package/docs/agents/agent-stack-management-spec.md +421 -0
- package/docs/agents/api-contract-spec.md +309 -0
- package/docs/agents/artifacts-writeback-spec.md +145 -0
- package/docs/agents/chart-packaging-spec.md +128 -0
- package/docs/agents/ci-orchestration-spec.md +140 -0
- package/docs/agents/context-assembly-spec.md +219 -0
- package/docs/agents/controller-reconciliation-spec.md +255 -0
- package/docs/agents/crd-schema-spec.md +315 -0
- package/docs/agents/decision-log-open-questions.md +169 -0
- package/docs/agents/developer-implementation-checklist.md +329 -0
- package/docs/agents/dispatching-design.md +262 -0
- package/docs/agents/gaps-agent-mux-to-kradle-crds.md +298 -0
- package/docs/agents/glossary.md +66 -0
- package/docs/agents/implementation-blueprint.md +324 -0
- package/docs/agents/implementation-rollout-slices.md +251 -0
- package/docs/agents/memory-context-integration-spec.md +194 -0
- package/docs/agents/memory-ontology-schema-spec.md +253 -0
- package/docs/agents/memory-operations-runbook.md +121 -0
- package/docs/agents/mvp-vertical-slice-spec.md +146 -0
- package/docs/agents/observability-audit-spec.md +265 -0
- package/docs/agents/operator-runbook.md +174 -0
- package/docs/agents/org-memory-api-payload-examples.md +333 -0
- package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
- package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
- package/docs/agents/org-memory-ui-implementation-map.md +114 -0
- package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
- package/docs/agents/org-resource-model-delta-spec.md +111 -0
- package/docs/agents/org-route-resource-model-spec.md +183 -0
- package/docs/agents/org-scoping-namespace-spec.md +114 -0
- package/docs/agents/rbac-secrets-management-spec.md +406 -0
- package/docs/agents/repository-page-integration-spec.md +255 -0
- package/docs/agents/resource-contract-examples.md +808 -0
- package/docs/agents/resource-relationship-map.md +190 -0
- package/docs/agents/security-threat-model.md +188 -0
- package/docs/agents/shared-memory-company-brain-spec.md +358 -0
- package/docs/agents/storage-migration-spec.md +168 -0
- package/docs/agents/subagent-orchestration-spec.md +152 -0
- package/docs/agents/system-overview.md +88 -0
- package/docs/agents/tools-mcp-skills-spec.md +189 -0
- package/docs/agents/traceability-matrix.md +79 -0
- package/docs/agents/ui-flow-spec.md +211 -0
- package/docs/agents/ui-ux-system-spec.md +426 -0
- package/docs/agents/workspace-lifecycle-spec.md +166 -0
- package/docs/architecture-spec.md +78 -0
- package/docs/architecture-v2.md +2759 -0
- package/docs/components/control-plane.md +78 -0
- package/docs/components/data-plane.md +69 -0
- package/docs/components/hooks-events.md +67 -0
- package/docs/components/identity-rbac-policy.md +73 -0
- package/docs/components/kubevela-oam.md +70 -0
- package/docs/components/operations-publishing.md +81 -0
- package/docs/components/runners-ci.md +66 -0
- package/docs/components/web-ui.md +94 -0
- package/docs/crd-behaviors-and-relationships.md +3926 -0
- package/docs/external/README.md +47 -0
- package/docs/external/bidirectional-sync-design.md +134 -0
- package/docs/external/cicd-interface.md +64 -0
- package/docs/external/external-backend-controllers.md +170 -0
- package/docs/external/external-backend-crds.md +234 -0
- package/docs/external/external-backend-ui-spec.md +151 -0
- package/docs/external/external-backend-ux-flows.md +115 -0
- package/docs/external/external-object-mapping.md +125 -0
- package/docs/external/git-forge-interface.md +68 -0
- package/docs/external/github-integration-design.md +151 -0
- package/docs/external/issue-tracking-interface.md +66 -0
- package/docs/external/provider-capability-manifests.md +204 -0
- package/docs/external/provider-catalog.md +139 -0
- package/docs/external/provider-rollout-testing.md +78 -0
- package/docs/external/research-results.md +48 -0
- package/docs/external/security-auth-permissions.md +81 -0
- package/docs/external/sync-state-machines.md +108 -0
- package/docs/external/unified-external-backend-model.md +107 -0
- package/docs/external/user-facing-changes.md +67 -0
- package/docs/gaps.md +161 -0
- package/docs/install.md +94 -0
- package/docs/integration-and-design-decisions.md +1530 -0
- package/docs/kradle-design.md +334 -0
- package/docs/local-minikube.md +55 -0
- package/docs/ontology/README.md +32 -0
- package/docs/ontology/bounded-contexts.md +29 -0
- package/docs/ontology/events-and-hooks.md +32 -0
- package/docs/ontology/oam-kubevela.md +32 -0
- package/docs/ontology/operations-and-release.md +25 -0
- package/docs/ontology/personas-and-actors.md +32 -0
- package/docs/ontology/policies-and-invariants.md +33 -0
- package/docs/ontology/problem-space.md +30 -0
- package/docs/ontology/resource-contracts.md +40 -0
- package/docs/ontology/resource-taxonomy.md +42 -0
- package/docs/ontology/runners-and-ci.md +29 -0
- package/docs/ontology/solution-space.md +24 -0
- package/docs/ontology/storage-and-data-boundaries.md +29 -0
- package/docs/ontology/validation-matrix.md +24 -0
- package/docs/ontology/web-ui-excellent-flows.md +32 -0
- package/docs/ontology/workflows.md +39 -0
- package/docs/ontology/world.md +35 -0
- package/docs/openapi.yaml +1291 -0
- package/docs/product-requirements.md +62 -0
- package/docs/requirements-v2.md +235 -0
- package/docs/roadmap-mvp.md +87 -0
- package/docs/sdk-api-reference.md +1108 -0
- package/docs/system-requirements.md +90 -0
- package/docs/system-spec-v2.md +1230 -0
- package/docs/tests/README.md +53 -0
- package/docs/tests/agent-qa-plan.md +63 -0
- package/docs/tests/browser-ui-tests.md +62 -0
- package/docs/tests/ci-quality-gates.md +48 -0
- package/docs/tests/coverage-model.md +64 -0
- package/docs/tests/e2e-scenario-tests.md +53 -0
- package/docs/tests/fixtures-test-data.md +63 -0
- package/docs/tests/observability-reliability-tests.md +54 -0
- package/docs/tests/product-test-matrix.md +145 -0
- package/docs/tests/qa-adoption-roadmap.md +130 -0
- package/docs/tests/qa-automation-plan.md +101 -0
- package/docs/tests/security-compliance-tests.md +57 -0
- package/docs/tests/test-framework-tools.md +88 -0
- package/docs/tests/test-suite-layout.md +121 -0
- package/docs/tests/unit-integration-tests.md +48 -0
- package/docs/todo-kyverno +714 -0
- package/docs/todos.md +4 -0
- package/docs/user-stories.md +78 -0
- package/docs/web-console-spec.md +533 -0
- package/examples/minikube-demo.yaml +190 -0
- package/examples/oam-application.yaml +23 -0
- package/examples/policy-kyverno-pr-title.yaml +18 -0
- package/package.json +66 -0
- package/scripts/build.mjs +29 -0
- package/scripts/setup-minikube.mjs +65 -0
- package/scripts/smoke.mjs +37 -0
- package/scripts/validate-doc-coverage.mjs +152 -0
- package/scripts/validate-package.mjs +95 -0
- package/scripts/validate-ui.mjs +305 -0
- package/src/agent-adapter-controller.js +169 -0
- package/src/agent-approval-controller.js +170 -0
- package/src/agent-context-bundles.js +242 -0
- package/src/agent-dispatch-controller.js +549 -0
- package/src/agent-gateway-config-controller.js +147 -0
- package/src/agent-identity-migration.js +115 -0
- package/src/agent-memory-controller.js +357 -0
- package/src/agent-memory-import.js +327 -0
- package/src/agent-memory-query.js +292 -0
- package/src/agent-memory-repository-source-controller.js +255 -0
- package/src/agent-mux-client.js +589 -0
- package/src/agent-permission-review.js +250 -0
- package/src/agent-persona-controller.js +135 -0
- package/src/agent-project-controller.js +117 -0
- package/src/agent-prompt-composition.js +55 -0
- package/src/agent-provider-config-controller.js +151 -0
- package/src/agent-secret-config-grant-controller.js +282 -0
- package/src/agent-session-transcript-controller.js +189 -0
- package/src/agent-stack-controller.js +421 -0
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +387 -0
- package/src/agent-workspace-controller.js +702 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +621 -0
- package/src/argocd-gitops.js +43 -0
- package/src/artifact-registry-controller.js +542 -0
- package/src/assistant-runtime.js +284 -0
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +310 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +112 -0
- package/src/controller-ui.js +620 -0
- package/src/data-plane.js +179 -0
- package/src/event-bus.js +397 -0
- package/src/external/conflict-controller.js +225 -0
- package/src/external/github/auth.js +96 -0
- package/src/external/github/cicd.js +180 -0
- package/src/external/github/git-forge.js +240 -0
- package/src/external/github/index.js +144 -0
- package/src/external/github/issue-tracking.js +163 -0
- package/src/external/provider-adapter.js +161 -0
- package/src/external/provider-resource-factory.js +221 -0
- package/src/external/sync-controller.js +235 -0
- package/src/external/webhook-controller.js +144 -0
- package/src/external/write-controller.js +283 -0
- package/src/gitea-backend.js +131 -0
- package/src/gitea-service.js +173 -0
- package/src/handoff.js +98 -0
- package/src/health-probes.js +134 -0
- package/src/hooks-events.js +63 -0
- package/src/hooks-lifecycle.js +117 -0
- package/src/http-server.js +409 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +71 -0
- package/src/jitsi-agent-bridge.js +141 -0
- package/src/jitsi-meeting-controller.js +291 -0
- package/src/jitsi-sync-controller.js +198 -0
- package/src/kradle-inference-service-controller.js +246 -0
- package/src/kubernetes-controller-async.js +531 -0
- package/src/kubernetes-controller.js +904 -0
- package/src/kubernetes-resource-gateway.js +48 -0
- package/src/model-route-controller.js +364 -0
- package/src/notification-controller.js +178 -0
- package/src/operations.js +112 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +282 -0
- package/src/runner-controller.js +272 -0
- package/src/runners-ci.js +48 -0
- package/src/runtime.js +196 -0
- package/src/snapshot-cache.js +157 -0
- package/src/virtual-model-controller.js +538 -0
- package/src/virtual-model-hook-bridge.js +200 -0
- package/src/web-ui.js +40 -0
- package/tests/agent-adapter-controller.test.js +361 -0
- package/tests/agent-approval-controller.test.js +173 -0
- package/tests/agent-context-bundles.test.js +278 -0
- package/tests/agent-dispatch-controller.test.js +679 -0
- package/tests/agent-gateway-config-controller.test.js +386 -0
- package/tests/agent-identity-migration.test.js +87 -0
- package/tests/agent-memory-controller.test.js +461 -0
- package/tests/agent-memory-import-snapshot.test.js +477 -0
- package/tests/agent-memory-query.test.js +404 -0
- package/tests/agent-memory-repository-source.test.js +514 -0
- package/tests/agent-mux-client.test.js +389 -0
- package/tests/agent-mux-integration.test.js +971 -0
- package/tests/agent-permission-review-v2.test.js +317 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-persona-controller.test.js +127 -0
- package/tests/agent-project-controller.test.js +302 -0
- package/tests/agent-prompt-composition.test.js +76 -0
- package/tests/agent-provider-config-controller.test.js +376 -0
- package/tests/agent-resources.test.js +303 -0
- package/tests/agent-secret-config-grant.test.js +231 -0
- package/tests/agent-session-transcript-controller.test.js +499 -0
- package/tests/agent-stack-controller.test.js +283 -0
- package/tests/agent-subagent-controller.test.js +201 -0
- package/tests/agent-transport-binding-controller.test.js +294 -0
- package/tests/agent-trigger-controller.test.js +271 -0
- package/tests/agent-trigger-routes.test.js +190 -0
- package/tests/agent-trigger-sources.test.js +245 -0
- package/tests/agent-workspace-controller.test.js +181 -0
- package/tests/agent-writeback.test.js +292 -0
- package/tests/approval-persistence.test.js +171 -0
- package/tests/artifact-registry.test.js +511 -0
- package/tests/assistant-runtime.test.js +506 -0
- package/tests/async-controller.test.js +252 -0
- package/tests/audit-controller.test.js +227 -0
- package/tests/codespace-controller.test.js +318 -0
- package/tests/controller-client.test.js +133 -0
- package/tests/deployment.test.js +527 -0
- package/tests/e2e/lifecycle.test.js +120 -0
- package/tests/event-bus-integration.test.js +355 -0
- package/tests/external-github-forge.test.js +560 -0
- package/tests/external-github-issues-cicd.test.js +520 -0
- package/tests/external-integration.test.js +470 -0
- package/tests/external-persistence.test.js +415 -0
- package/tests/external-provider-adapter.test.js +365 -0
- package/tests/external-resource-model.test.js +223 -0
- package/tests/external-webhook-sync.test.js +287 -0
- package/tests/external-write-conflict.test.js +353 -0
- package/tests/gitea-service.test.js +253 -0
- package/tests/health-check-real.test.js +165 -0
- package/tests/health-probes.test.js +90 -0
- package/tests/hooks-lifecycle.test.js +364 -0
- package/tests/integration/full-flow.test.js +266 -0
- package/tests/jitsi-agent-bridge.test.js +119 -0
- package/tests/jitsi-helm-integration.test.js +77 -0
- package/tests/jitsi-meeting-controller.test.js +170 -0
- package/tests/jitsi-resource-model.test.js +73 -0
- package/tests/jitsi-sync-controller.test.js +112 -0
- package/tests/kradle-inference-service.test.js +689 -0
- package/tests/kradle.test.js +779 -0
- package/tests/memory-search-wiring.test.js +270 -0
- package/tests/model-route-controller.test.js +733 -0
- package/tests/notification-controller.test.js +196 -0
- package/tests/notification-integration.test.js +179 -0
- package/tests/org-scoping.test.js +687 -0
- package/tests/runner-controller.test.js +327 -0
- package/tests/runner-integration.test.js +231 -0
- package/tests/session-cookie-hmac.test.js +151 -0
- package/tests/snapshot-performance.test.js +315 -0
- package/tests/sse-events.test.js +107 -0
- package/tests/virtual-model-controller.test.js +877 -0
- package/tests/virtual-model-hook-bridge.test.js +384 -0
- package/tests/webhook-trigger.test.js +198 -0
- package/tests/workspace-volumes.test.js +312 -0
- package/tests/writeback-persistence.test.js +207 -0
|
@@ -0,0 +1,2759 @@
|
|
|
1
|
+
# Kradle Architecture Specification v2
|
|
2
|
+
|
|
3
|
+
> Exhaustive architecture reference derived from implementation source code.
|
|
4
|
+
> Source: `packages/kradle/core/src/`, `packages/kradle/sdk/src/`, `packages/kradle/web/`, `packages/kradle/cli/`
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. System Overview
|
|
9
|
+
|
|
10
|
+
Kradle is a Kubernetes-native Git forge runtime built as a monorepo with four packages:
|
|
11
|
+
|
|
12
|
+
| Package | NPM Name | Role | Path | Dependencies |
|
|
13
|
+
|---------|-----------|------|------|--------------|
|
|
14
|
+
| **core** | `@a5c-ai/kradle` | Resource model, controllers, HTTP API server | `packages/kradle/core/` | Zero external (Node.js built-ins only) |
|
|
15
|
+
| **sdk** | `@a5c-ai/kradle-sdk` | Client SDK re-exporting core helpers for web/CLI consumers | `packages/kradle/sdk/` | Re-exports from core |
|
|
16
|
+
| **cli** | `@a5c-ai/kradle-cli` | CLI entrypoint and MCP server mode | `packages/kradle/cli/` | Imports from core |
|
|
17
|
+
| **web** | `@a5c-ai/kradle-web` | Next.js 16 + React 19 web console | `packages/kradle/web/` | Imports from sdk |
|
|
18
|
+
|
|
19
|
+
**Design principles:**
|
|
20
|
+
- Pure ESM JavaScript (Node 20+), zero external runtime dependencies in core
|
|
21
|
+
- Kubernetes-first: all resources are K8s API objects (CRDs or aggregated)
|
|
22
|
+
- CRD-driven: 76 CustomResourceDefinitions under `kradle.a5c.ai/v1alpha1`
|
|
23
|
+
- Controller pattern: each domain has a controller with explicit boundary declarations
|
|
24
|
+
- Intent-based: controllers produce manifests/specs but never execute kubectl directly
|
|
25
|
+
|
|
26
|
+
```mermaid
|
|
27
|
+
graph TB
|
|
28
|
+
subgraph "Browser"
|
|
29
|
+
WEB[Web Console<br/>Next.js 16 + React 19]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
subgraph "SDK Layer"
|
|
33
|
+
SDK_INDEX[sdk/src/index.js<br/>65+ re-exports]
|
|
34
|
+
ATLAS[sdk/src/atlas-graph-client.js<br/>Stack layer catalog]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
subgraph "Core Controllers"
|
|
38
|
+
HTTP[http-server.js<br/>Node HTTP handler]
|
|
39
|
+
API[api-controller.js<br/>Facade/orchestrator]
|
|
40
|
+
STACK[agent-stack-controller.js]
|
|
41
|
+
DISPATCH[agent-dispatch-controller.js]
|
|
42
|
+
TRIGGER[agent-trigger-controller.js]
|
|
43
|
+
WORKSPACE[agent-workspace-controller.js]
|
|
44
|
+
APPROVAL[agent-approval-controller.js]
|
|
45
|
+
MEMORY[agent-memory-controller.js]
|
|
46
|
+
MEMQ[agent-memory-query.js]
|
|
47
|
+
PERM[agent-permission-review.js]
|
|
48
|
+
AUDIT[audit-controller.js]
|
|
49
|
+
NOTIFY[notification-controller.js]
|
|
50
|
+
RUNNER[runner-controller.js]
|
|
51
|
+
EVENTBUS[event-bus.js]
|
|
52
|
+
CACHE[snapshot-cache.js]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
subgraph "Resource Layer"
|
|
56
|
+
GATEWAY[kubernetes-resource-gateway.js]
|
|
57
|
+
CLIENT[kubernetes-controller.js<br/>kubectl spawning]
|
|
58
|
+
RESMODEL[resource-model.js<br/>76 kinds]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
subgraph "External Subsystem"
|
|
62
|
+
WEBHOOK[external/webhook-controller.js]
|
|
63
|
+
SYNC[external/sync-controller.js]
|
|
64
|
+
CONFLICT[external/conflict-controller.js]
|
|
65
|
+
WRITE[external/write-controller.js]
|
|
66
|
+
GITHUB_ADAPTER[external/github/]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
subgraph "Infrastructure"
|
|
70
|
+
K8S[Kubernetes API<br/>etcd CRD storage]
|
|
71
|
+
PG[PostgreSQL<br/>Aggregated storage]
|
|
72
|
+
GITEA[Gitea<br/>Git hosting]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
WEB --> SDK_INDEX
|
|
76
|
+
SDK_INDEX --> API
|
|
77
|
+
HTTP --> API
|
|
78
|
+
API --> GATEWAY
|
|
79
|
+
API --> DISPATCH
|
|
80
|
+
API --> TRIGGER
|
|
81
|
+
API --> APPROVAL
|
|
82
|
+
API --> MEMORY
|
|
83
|
+
API --> WORKSPACE
|
|
84
|
+
API --> SYNC
|
|
85
|
+
API --> WEBHOOK
|
|
86
|
+
API --> CONFLICT
|
|
87
|
+
API --> WRITE
|
|
88
|
+
DISPATCH --> PERM
|
|
89
|
+
DISPATCH --> STACK
|
|
90
|
+
DISPATCH --> WORKSPACE
|
|
91
|
+
DISPATCH --> APPROVAL
|
|
92
|
+
DISPATCH --> MEMORY
|
|
93
|
+
GATEWAY --> CLIENT
|
|
94
|
+
CLIENT --> K8S
|
|
95
|
+
CLIENT --> PG
|
|
96
|
+
GATEWAY --> GITEA
|
|
97
|
+
EVENTBUS --> HTTP
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 2. Package Dependency Graph
|
|
103
|
+
|
|
104
|
+
### 2.1 Import Hierarchy (Strict)
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
web → sdk → core
|
|
108
|
+
cli → core
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The web package NEVER imports directly from core. The SDK acts as the public API surface.
|
|
112
|
+
|
|
113
|
+
### 2.2 Core Internal Dependencies
|
|
114
|
+
|
|
115
|
+
```mermaid
|
|
116
|
+
graph LR
|
|
117
|
+
HTTP[http-server] --> API[api-controller]
|
|
118
|
+
HTTP --> CTRL_UI[controller-ui]
|
|
119
|
+
HTTP --> GATEWAY[kubernetes-resource-gateway]
|
|
120
|
+
HTTP --> EVENTBUS[event-bus]
|
|
121
|
+
API --> GATEWAY
|
|
122
|
+
API --> DISPATCH[agent-dispatch-controller]
|
|
123
|
+
API --> TRIGGER[agent-trigger-controller]
|
|
124
|
+
API --> APPROVAL[agent-approval-controller]
|
|
125
|
+
API --> WORKSPACE[agent-workspace-controller]
|
|
126
|
+
API --> MEMORY_CTRL[agent-memory-controller]
|
|
127
|
+
API --> PERM[agent-permission-review]
|
|
128
|
+
API --> SYNC[external/sync-controller]
|
|
129
|
+
API --> WEBHOOK_CTRL[external/webhook-controller]
|
|
130
|
+
API --> WRITE_CTRL[external/write-controller]
|
|
131
|
+
API --> CONFLICT_CTRL[external/conflict-controller]
|
|
132
|
+
DISPATCH --> PERM
|
|
133
|
+
DISPATCH --> STACK[agent-stack-controller]
|
|
134
|
+
DISPATCH --> CONTEXT[agent-context-bundles]
|
|
135
|
+
DISPATCH --> MUX[agent-mux-client]
|
|
136
|
+
DISPATCH --> MEMORY_CTRL
|
|
137
|
+
DISPATCH --> APPROVAL
|
|
138
|
+
DISPATCH --> WORKSPACE
|
|
139
|
+
GATEWAY --> CLIENT[kubernetes-controller]
|
|
140
|
+
CLIENT --> RESMODEL[resource-model]
|
|
141
|
+
STACK --> PERM
|
|
142
|
+
TRIGGER --> DISPATCH
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 2.3 Circular Dependency Prevention
|
|
146
|
+
|
|
147
|
+
- Controllers only import `resource-model.js` and their declared `delegatesTo` modules
|
|
148
|
+
- Every controller has a `BOUNDARY` constant declaring what it owns and what it must not own
|
|
149
|
+
- The api-controller is the only fan-out point that imports multiple controllers
|
|
150
|
+
- No controller imports the api-controller (prevents upward dependency)
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 3. Request Lifecycle
|
|
155
|
+
|
|
156
|
+
### 3.1 From Browser Click to Kubectl Apply
|
|
157
|
+
|
|
158
|
+
```mermaid
|
|
159
|
+
sequenceDiagram
|
|
160
|
+
participant Browser
|
|
161
|
+
participant NextJS as Next.js App Router
|
|
162
|
+
participant WebAPI as Web API Route
|
|
163
|
+
participant SDK as fetchControllerUiModel()
|
|
164
|
+
participant HTTP as Kradle HTTP Server
|
|
165
|
+
participant API as createKradleApiController
|
|
166
|
+
participant Gateway as KubernetesResourceGateway
|
|
167
|
+
participant Client as KubernetesResourceClient
|
|
168
|
+
participant Kubectl as kubectl process
|
|
169
|
+
|
|
170
|
+
Browser->>NextJS: Page load / navigation
|
|
171
|
+
NextJS->>WebAPI: GET /api/orgs/[org]/repositories
|
|
172
|
+
WebAPI->>SDK: fetchControllerUiModel({ baseUrl, org })
|
|
173
|
+
SDK->>HTTP: GET /api/controller?org=acme
|
|
174
|
+
HTTP->>API: controller.snapshot()
|
|
175
|
+
API->>Gateway: resourceGateway.snapshot()
|
|
176
|
+
Gateway->>Client: resourceClient.snapshot()
|
|
177
|
+
Client->>Kubectl: spawnSync('kubectl', ['get', ...])
|
|
178
|
+
Kubectl-->>Client: JSON stdout
|
|
179
|
+
Client-->>Gateway: parsed resources
|
|
180
|
+
Gateway-->>API: snapshot object
|
|
181
|
+
API-->>HTTP: withArchitecture(snapshot)
|
|
182
|
+
HTTP->>HTTP: createControllerUiModel(snapshot, { organization })
|
|
183
|
+
HTTP-->>SDK: JSON response (UI model)
|
|
184
|
+
SDK-->>WebAPI: structured data
|
|
185
|
+
WebAPI-->>NextJS: props
|
|
186
|
+
NextJS-->>Browser: rendered HTML
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### 3.2 Function Call Chain (Exact)
|
|
190
|
+
|
|
191
|
+
1. `createKradleHttpHandler()` receives Node.js `IncomingMessage`
|
|
192
|
+
2. URL parsed: `new URL(request.url, 'http://localhost')`
|
|
193
|
+
3. Route matching via regex: `/^\/api\/orgs\/([^/]+)\/resources$/`
|
|
194
|
+
4. Org extracted from URL path segment
|
|
195
|
+
5. `createKradleApiController({ namespace: orgNamespaceName(org) })` instantiated per-request
|
|
196
|
+
6. Controller method called (e.g., `listResource`, `applyResource`)
|
|
197
|
+
7. Cross-org admission check in `applyResource()`: verifies `spec.organizationRef` matches namespace
|
|
198
|
+
8. `resourceGateway.apply(resource)` delegates to kubectl
|
|
199
|
+
9. `clearSnapshotCache()` invalidates stale data
|
|
200
|
+
10. `globalEventBus.emitResourceChange(kind, name, operation)` broadcasts SSE
|
|
201
|
+
11. JSON response written via `send(response, status, body)`
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 4. Snapshot Pipeline
|
|
206
|
+
|
|
207
|
+
### 4.1 `getControllerSnapshot()` Step by Step
|
|
208
|
+
|
|
209
|
+
Source: `packages/kradle/core/src/kubernetes-controller.js` lines 352-497
|
|
210
|
+
|
|
211
|
+
```mermaid
|
|
212
|
+
flowchart TD
|
|
213
|
+
A[Start: getControllerSnapshot] --> B{kubectl config current-context}
|
|
214
|
+
B -->|Fail| C[Return degraded snapshot]
|
|
215
|
+
B -->|OK| D{kubectl version --client=true}
|
|
216
|
+
D -->|Fail| C
|
|
217
|
+
D -->|OK| E[kubectl get apiservice v1alpha1.kradle.a5c.ai]
|
|
218
|
+
E --> F[kubectl get crd -o json]
|
|
219
|
+
F --> G{Filter CRDs by group}
|
|
220
|
+
G --> H[kradle.a5c.ai CRDs]
|
|
221
|
+
G --> I[core.oam.dev CRDs]
|
|
222
|
+
G --> J[kyverno.io CRDs]
|
|
223
|
+
H --> K[Build discoveredPluralSet]
|
|
224
|
+
K --> L[List platform-scoped resources]
|
|
225
|
+
L --> M[Determine org namespaces]
|
|
226
|
+
M --> N[List org-scoped resources per namespace]
|
|
227
|
+
N --> O[Get events in platform namespace]
|
|
228
|
+
O --> P[Run SubjectAccessReview for each CRD]
|
|
229
|
+
P --> Q[Discover Kyverno controllers]
|
|
230
|
+
Q --> R[Return full snapshot object]
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 4.2 Org Namespace Discovery
|
|
234
|
+
|
|
235
|
+
```javascript
|
|
236
|
+
function organizationNamespaces(organizations, bindings, fallbackNamespace) {
|
|
237
|
+
// 1. Extract namespaceName from Organization specs
|
|
238
|
+
// 2. Extract namespace from OrgNamespaceBinding specs
|
|
239
|
+
// 3. Deduplicate with Set
|
|
240
|
+
// 4. Fallback: KRADLE_ADMIN_ORG, KRADLE_ORG, or 'default'
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### 4.3 In-Cluster Detection
|
|
245
|
+
|
|
246
|
+
Source: `inClusterKubectlConfig()` at line 724
|
|
247
|
+
|
|
248
|
+
When running inside a Kubernetes pod:
|
|
249
|
+
- Checks `KUBERNETES_SERVICE_HOST` and `KUBERNETES_SERVICE_PORT`
|
|
250
|
+
- Reads `/var/run/secrets/kubernetes.io/serviceaccount/token`
|
|
251
|
+
- Reads `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`
|
|
252
|
+
- Adds `--server`, `--certificate-authority`, `--token` args to all kubectl calls
|
|
253
|
+
|
|
254
|
+
### 4.4 kubectl Execution Model
|
|
255
|
+
|
|
256
|
+
```javascript
|
|
257
|
+
function runKubectl(args, options) {
|
|
258
|
+
// Uses spawnSync (synchronous) for snapshot queries
|
|
259
|
+
// Timeout: KRADLE_KUBECTL_TIMEOUT_MS (default 3000ms)
|
|
260
|
+
// Max buffer: KRADLE_KUBECTL_MAX_BUFFER_BYTES (default 32MB)
|
|
261
|
+
// windowsHide: true (prevents console flash on Windows)
|
|
262
|
+
// encoding: 'utf8'
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### 4.5 Environment Variables Affecting Snapshot
|
|
267
|
+
|
|
268
|
+
| Variable | Default | Purpose |
|
|
269
|
+
|----------|---------|---------|
|
|
270
|
+
| `KRADLE_KUBECTL` | `kubectl` | Path to kubectl binary |
|
|
271
|
+
| `KRADLE_NAMESPACE` | `kradle-system` | Platform namespace |
|
|
272
|
+
| `KRADLE_KUBECTL_TIMEOUT_MS` | `3000` | kubectl spawn timeout |
|
|
273
|
+
| `KRADLE_KUBECTL_MAX_BUFFER_BYTES` | `33554432` | Max stdout buffer (32MB) |
|
|
274
|
+
| `KRADLE_DISABLE_IN_CLUSTER_KUBECTL` | `false` | Skip in-cluster detection |
|
|
275
|
+
| `KUBECONFIG` | (none) | If set, disables in-cluster mode |
|
|
276
|
+
| `KUBERNETES_SERVICE_HOST` | (none) | In-cluster API server host |
|
|
277
|
+
| `KUBERNETES_SERVICE_PORT` | `443` | In-cluster API server port |
|
|
278
|
+
| `KRADLE_SERVICE_ACCOUNT_DIR` | `/var/run/secrets/kubernetes.io/serviceaccount` | SA mount path |
|
|
279
|
+
| `KRADLE_ORG` | `default` | Fallback org for namespace discovery |
|
|
280
|
+
| `KRADLE_ADMIN_ORG` | (none) | Admin org for namespace discovery |
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## 5. Stale-While-Revalidate Cache
|
|
285
|
+
|
|
286
|
+
Source: `packages/kradle/core/src/snapshot-cache.js`
|
|
287
|
+
|
|
288
|
+
### 5.1 Cache Architecture
|
|
289
|
+
|
|
290
|
+
```mermaid
|
|
291
|
+
graph LR
|
|
292
|
+
subgraph "Per-Org Cache Map"
|
|
293
|
+
A["orgCacheMap (Map)"]
|
|
294
|
+
A --> B["'' → {data, timestamp, revalidating}"]
|
|
295
|
+
A --> C["'acme' → {data, timestamp, revalidating}"]
|
|
296
|
+
A --> D["'beta' → {data, timestamp, revalidating}"]
|
|
297
|
+
end
|
|
298
|
+
subgraph "Legacy Single-Org"
|
|
299
|
+
E["snapshotCache = {data, timestamp, org}"]
|
|
300
|
+
end
|
|
301
|
+
A -.->|sync| E
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### 5.2 TTL Configuration
|
|
305
|
+
|
|
306
|
+
```javascript
|
|
307
|
+
export const CACHE_TTL_MS = Number(process.env.KRADLE_SNAPSHOT_CACHE_TTL_MS || 30_000);
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### 5.3 staleWhileRevalidate Algorithm
|
|
311
|
+
|
|
312
|
+
```javascript
|
|
313
|
+
async function staleWhileRevalidate(org, revalidateFn, swrOptions = {}) {
|
|
314
|
+
const ttlMs = swrOptions.ttlMs ?? CACHE_TTL_MS; // Fresh window: 30s
|
|
315
|
+
const staleMs = swrOptions.staleMs ?? ttlMs * 5; // Max stale: 150s
|
|
316
|
+
|
|
317
|
+
// CASE 1: Fresh (< 30s old) → return immediately
|
|
318
|
+
// CASE 2: Stale but usable (30s-150s) → return immediately, revalidate in background
|
|
319
|
+
// CASE 3: Stale and revalidating → return stale data (another caller is refreshing)
|
|
320
|
+
// CASE 4: No cache or too old (>150s) → block on revalidation
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### 5.4 Cache Invalidation Triggers
|
|
325
|
+
|
|
326
|
+
`clearSnapshotCache()` is called on:
|
|
327
|
+
- `applyResource()` success
|
|
328
|
+
- `applyResourceForOrg()` success
|
|
329
|
+
- `deleteResource()` success
|
|
330
|
+
- `deleteResourceForOrg()` success
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## 6. Authentication Flow
|
|
335
|
+
|
|
336
|
+
### 6.1 Complete OAuth Flow
|
|
337
|
+
|
|
338
|
+
Source: `packages/kradle/core/src/auth.js`
|
|
339
|
+
|
|
340
|
+
```mermaid
|
|
341
|
+
sequenceDiagram
|
|
342
|
+
participant User
|
|
343
|
+
participant Browser
|
|
344
|
+
participant LoginPage as /login page
|
|
345
|
+
participant AuthRoute as /api/auth/github
|
|
346
|
+
participant GitHub as GitHub OAuth
|
|
347
|
+
participant CallbackRoute as /api/auth/callback/github
|
|
348
|
+
participant AuthModule as auth.js
|
|
349
|
+
participant K8s as Kubernetes
|
|
350
|
+
|
|
351
|
+
User->>Browser: Click "Sign in with GitHub"
|
|
352
|
+
Browser->>LoginPage: Navigate
|
|
353
|
+
LoginPage->>AuthRoute: GET /api/auth/github
|
|
354
|
+
AuthRoute->>AuthModule: buildAuthorizationRedirect({ provider, requestUrl })
|
|
355
|
+
AuthModule-->>AuthRoute: { url, state, redirectUri }
|
|
356
|
+
AuthRoute->>Browser: 302 Redirect to GitHub
|
|
357
|
+
|
|
358
|
+
Browser->>GitHub: GET /login/oauth/authorize?client_id=...&redirect_uri=...&scope=read:user+user:email&state=...
|
|
359
|
+
GitHub->>User: Show authorization prompt
|
|
360
|
+
User->>GitHub: Authorize
|
|
361
|
+
GitHub->>Browser: 302 Redirect to /api/auth/callback/github?code=ABC&state=...
|
|
362
|
+
|
|
363
|
+
Browser->>CallbackRoute: GET /api/auth/callback/github?code=ABC&state=...
|
|
364
|
+
CallbackRoute->>AuthModule: exchangeOAuthCodeForProfile({ provider, code, requestUrl })
|
|
365
|
+
AuthModule->>GitHub: POST /login/oauth/access_token (code + client_secret)
|
|
366
|
+
GitHub-->>AuthModule: { access_token: "gho_..." }
|
|
367
|
+
AuthModule->>GitHub: GET /user (Authorization: Bearer gho_...)
|
|
368
|
+
GitHub-->>AuthModule: { login, id, email, name }
|
|
369
|
+
AuthModule->>AuthModule: normalizeProviderProfile(provider, profile)
|
|
370
|
+
AuthModule-->>CallbackRoute: { provider, subject, email, displayName, username, groups, admin }
|
|
371
|
+
|
|
372
|
+
CallbackRoute->>AuthModule: registerLoginProfile({ controller, namespace, profile })
|
|
373
|
+
AuthModule->>K8s: applyResource(User)
|
|
374
|
+
AuthModule->>K8s: applyResource(IdentityMapping)
|
|
375
|
+
CallbackRoute->>AuthModule: createSessionCookie(config, profile, { secret })
|
|
376
|
+
AuthModule-->>CallbackRoute: "kradle_session=base64url.hmac; Path=/; HttpOnly; SameSite=Lax"
|
|
377
|
+
CallbackRoute->>Browser: Set-Cookie + 302 to /orgs/[org]
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### 6.2 Session Cookie Structure
|
|
381
|
+
|
|
382
|
+
```
|
|
383
|
+
kradle_session = base64url(payload) . hmac_sha256_base64url(payload, secret)
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Payload JSON:
|
|
387
|
+
```json
|
|
388
|
+
{ "provider": "github", "subject": "12345", "user": "octocat" }
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### 6.3 Session Verification
|
|
392
|
+
|
|
393
|
+
```javascript
|
|
394
|
+
function parseSessionCookie(config, cookieValue, options) {
|
|
395
|
+
// 1. Split on first '.' → [payload, signature]
|
|
396
|
+
// 2. If signed + no secret: reject (null)
|
|
397
|
+
// 3. If unsigned + secret configured: reject (null)
|
|
398
|
+
// 4. If signed + secret: compute expected HMAC, timingSafeEqual
|
|
399
|
+
// 5. If match: decode base64url → JSON.parse → extract user/subject/provider
|
|
400
|
+
// 6. Return { cookieName, provider, subject, user } or null
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### 6.4 Delegated Identity (Proxy Auth)
|
|
405
|
+
|
|
406
|
+
Headers examined:
|
|
407
|
+
- `x-forwarded-user` (configurable via `KRADLE_AUTH_DELEGATED_USER_HEADER`)
|
|
408
|
+
- `x-forwarded-groups` (configurable via `KRADLE_AUTH_DELEGATED_GROUPS_HEADER`)
|
|
409
|
+
- `x-forwarded-email` (configurable via `KRADLE_AUTH_DELEGATED_EMAIL_HEADER`)
|
|
410
|
+
|
|
411
|
+
Local development auto-login:
|
|
412
|
+
- Active when `NODE_ENV !== 'production'` (or `KRADLE_AUTH_DELEGATED_LOCAL_DEVELOPMENT=true`)
|
|
413
|
+
- Default user: `KRADLE_AUTH_DELEGATED_LOCAL_USER` or `'local-developer'`
|
|
414
|
+
- Default groups: `KRADLE_AUTH_DELEGATED_LOCAL_GROUPS` or `'kradle:repo-admins'`
|
|
415
|
+
|
|
416
|
+
### 6.5 Admin Detection
|
|
417
|
+
|
|
418
|
+
Admin status is derived from group membership:
|
|
419
|
+
```javascript
|
|
420
|
+
admin: groups.includes('kradle:platform-engineers') || groups.includes('kradle:repo-admins')
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### 6.6 All Auth Environment Variables
|
|
424
|
+
|
|
425
|
+
| Variable | Default | Purpose |
|
|
426
|
+
|----------|---------|---------|
|
|
427
|
+
| `KRADLE_AUTH_COOKIE_NAME` | `kradle_session` | Cookie name |
|
|
428
|
+
| `KRADLE_SESSION_SECRET` | `''` | HMAC signing secret |
|
|
429
|
+
| `KRADLE_AUTH_GITHUB_ENABLED` | `true` | Enable GitHub provider |
|
|
430
|
+
| `KRADLE_AUTH_GITHUB_CLIENT_ID` | `''` | OAuth client ID |
|
|
431
|
+
| `KRADLE_AUTH_GITHUB_CLIENT_SECRET` | `''` | OAuth client secret |
|
|
432
|
+
| `KRADLE_AUTH_GITHUB_AUTHORIZATION_URL` | `https://github.com/login/oauth/authorize` | Auth endpoint |
|
|
433
|
+
| `KRADLE_AUTH_GITHUB_TOKEN_URL` | `https://github.com/login/oauth/access_token` | Token endpoint |
|
|
434
|
+
| `KRADLE_AUTH_GITHUB_USERINFO_URL` | `https://api.github.com/user` | Profile endpoint |
|
|
435
|
+
| `KRADLE_AUTH_GITHUB_SCOPES` | `read:user user:email` | OAuth scopes |
|
|
436
|
+
| `KRADLE_AUTH_SSO_ENABLED` | `false` | Enable OIDC provider |
|
|
437
|
+
| `KRADLE_AUTH_SSO_PROVIDER_NAME` | `Workspace SSO` | Display label |
|
|
438
|
+
| `KRADLE_AUTH_SSO_ISSUER_URL` | `''` | OIDC issuer |
|
|
439
|
+
| `KRADLE_AUTH_SSO_CLIENT_ID` | `''` | OIDC client ID |
|
|
440
|
+
| `KRADLE_AUTH_SSO_CLIENT_SECRET` | `''` | OIDC client secret |
|
|
441
|
+
| `KRADLE_AUTH_SSO_AUTHORIZATION_URL` | `''` | OIDC auth endpoint |
|
|
442
|
+
| `KRADLE_AUTH_SSO_TOKEN_URL` | `''` | OIDC token endpoint |
|
|
443
|
+
| `KRADLE_AUTH_SSO_USERINFO_URL` | `''` | OIDC profile endpoint |
|
|
444
|
+
| `KRADLE_AUTH_SSO_SCOPES` | `openid profile email groups` | OIDC scopes |
|
|
445
|
+
| `KRADLE_AUTH_DELEGATED_IDENTITY_ENABLED` | `false` | Enable proxy auth |
|
|
446
|
+
| `KRADLE_AUTH_DELEGATED_USER_HEADER` | `x-forwarded-user` | User header |
|
|
447
|
+
| `KRADLE_AUTH_DELEGATED_GROUPS_HEADER` | `x-forwarded-groups` | Groups header |
|
|
448
|
+
| `KRADLE_AUTH_DELEGATED_EMAIL_HEADER` | `x-forwarded-email` | Email header |
|
|
449
|
+
| `KRADLE_AUTH_DELEGATED_LOCAL_DEVELOPMENT` | auto | Enable local dev fallback |
|
|
450
|
+
| `KRADLE_AUTH_DELEGATED_LOCAL_USER` | `local-developer` | Dev username |
|
|
451
|
+
| `KRADLE_AUTH_DELEGATED_LOCAL_EMAIL` | `''` | Dev email |
|
|
452
|
+
| `KRADLE_AUTH_DELEGATED_LOCAL_GROUPS` | `kradle:repo-admins` | Dev groups |
|
|
453
|
+
| `KRADLE_ADMIN_ORG` | (none) | Bootstrap admin org |
|
|
454
|
+
| `KRADLE_ADMIN_USERNAME` | (none) | Bootstrap admin user |
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## 7. Resource Lifecycle
|
|
459
|
+
|
|
460
|
+
### 7.1 From UI Form Submit to SSE Update
|
|
461
|
+
|
|
462
|
+
```mermaid
|
|
463
|
+
sequenceDiagram
|
|
464
|
+
participant UI as Web Console
|
|
465
|
+
participant Route as API Route
|
|
466
|
+
participant HTTP as HTTP Handler
|
|
467
|
+
participant API as ApiController
|
|
468
|
+
participant Gateway as ResourceGateway
|
|
469
|
+
participant Client as KubernetesClient
|
|
470
|
+
participant Kubectl as kubectl
|
|
471
|
+
participant K8s as Kubernetes API
|
|
472
|
+
participant Cache as SnapshotCache
|
|
473
|
+
participant Bus as EventBus
|
|
474
|
+
participant SSE as SSE Stream
|
|
475
|
+
|
|
476
|
+
UI->>Route: POST fetch('/api/orgs/acme/resources', { body: resource })
|
|
477
|
+
Route->>HTTP: Forward request
|
|
478
|
+
HTTP->>HTTP: scopeResource(resource, org)
|
|
479
|
+
Note over HTTP: Add namespace, labels, organizationRef
|
|
480
|
+
HTTP->>API: scopedController.applyResource(scopedResource)
|
|
481
|
+
API->>API: Cross-org admission check
|
|
482
|
+
Note over API: Verify spec.organizationRef matches namespace
|
|
483
|
+
API->>Gateway: resourceGateway.apply(resource)
|
|
484
|
+
Gateway->>Client: resourceClient.applyResource(resource)
|
|
485
|
+
Client->>Client: withOrgScope(resource, { namespace })
|
|
486
|
+
Client->>Client: ensureNamespace(targetNs)
|
|
487
|
+
Client->>Kubectl: spawnSync(['apply', '-f', '-', '-o', 'json'], { input: JSON })
|
|
488
|
+
Kubectl->>K8s: kubectl apply -f -
|
|
489
|
+
K8s-->>Kubectl: Applied resource JSON
|
|
490
|
+
Kubectl-->>Client: stdout
|
|
491
|
+
Client-->>Gateway: { operation: 'apply', resource }
|
|
492
|
+
Gateway-->>API: result
|
|
493
|
+
API->>Cache: clearSnapshotCache()
|
|
494
|
+
API->>Bus: globalEventBus.emitResourceChange(kind, name, 'apply')
|
|
495
|
+
Bus->>SSE: writer(event) → response.write('data: {...}\n\n')
|
|
496
|
+
SSE->>UI: EventSource message received
|
|
497
|
+
UI->>UI: Re-fetch / optimistic update
|
|
498
|
+
API-->>HTTP: { operation, resource }
|
|
499
|
+
HTTP-->>Route: 201 JSON
|
|
500
|
+
Route-->>UI: Success response
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### 7.2 scopeResource Function
|
|
504
|
+
|
|
505
|
+
```javascript
|
|
506
|
+
function scopeResource(resource, org) {
|
|
507
|
+
const namespace = orgNamespaceName(org); // 'kradle-org-acme'
|
|
508
|
+
return {
|
|
509
|
+
...resource,
|
|
510
|
+
metadata: {
|
|
511
|
+
...(resource.metadata || {}),
|
|
512
|
+
namespace,
|
|
513
|
+
labels: {
|
|
514
|
+
...(resource.metadata?.labels || {}),
|
|
515
|
+
'kradle.a5c.ai/org': org,
|
|
516
|
+
'kradle.a5c.ai/namespace': namespace
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
spec: { ...(resource.spec || {}), organizationRef: org }
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### 7.3 Cross-Org Admission
|
|
525
|
+
|
|
526
|
+
In `applyResource()`:
|
|
527
|
+
```javascript
|
|
528
|
+
const resourceOrg = resource.spec?.organizationRef;
|
|
529
|
+
const resourceNs = resource.metadata?.namespace;
|
|
530
|
+
if (resourceOrg) {
|
|
531
|
+
const expectedNs = orgNamespaceName(resourceOrg);
|
|
532
|
+
if (resourceNs && resourceNs !== expectedNs) {
|
|
533
|
+
throw new Error(`Cross-org namespace mismatch`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
In `deleteResourceForOrg()`:
|
|
539
|
+
```javascript
|
|
540
|
+
// Verify existing resource namespace matches org
|
|
541
|
+
if (!resourceNs || resourceNs !== orgNs) {
|
|
542
|
+
throw new Error(`Cross-org denial`);
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
## 8. Agent Dispatch Lifecycle
|
|
549
|
+
|
|
550
|
+
### 8.1 Complete Flow (K8s Job-Based)
|
|
551
|
+
|
|
552
|
+
Source: `packages/kradle/core/src/agent-dispatch-controller.js`
|
|
553
|
+
|
|
554
|
+
Agents are dispatched as Kubernetes `batch/v1` Jobs (not subprocesses or direct
|
|
555
|
+
Agent Mux HTTP calls). Each dispatch creates a Job manifest via `createAgentJob()`,
|
|
556
|
+
submits it to Kubernetes via `submitAgentJob()`, and waits for the agent pod to
|
|
557
|
+
POST its result to the callback endpoint.
|
|
558
|
+
|
|
559
|
+
```mermaid
|
|
560
|
+
sequenceDiagram
|
|
561
|
+
participant UI as Dispatch Button
|
|
562
|
+
participant HTTP as HTTP Handler
|
|
563
|
+
participant API as ApiController
|
|
564
|
+
participant Dispatch as DispatchController
|
|
565
|
+
participant Perm as PermissionReviewer
|
|
566
|
+
participant Stack as StackController
|
|
567
|
+
participant Memory as MemoryController
|
|
568
|
+
participant Approval as ApprovalController
|
|
569
|
+
participant Workspace as WorkspaceController
|
|
570
|
+
participant Context as ContextBundler
|
|
571
|
+
participant Hooks as HooksLifecycleEmitter
|
|
572
|
+
participant K8s as Kubernetes Jobs API
|
|
573
|
+
participant Pod as Agent Pod (Job)
|
|
574
|
+
participant Callback as Callback Endpoint
|
|
575
|
+
|
|
576
|
+
UI->>HTTP: POST /api/orgs/:org/agents/dispatch
|
|
577
|
+
HTTP->>API: controller.dispatchAgent(input)
|
|
578
|
+
API->>API: snapshot = await this.snapshot()
|
|
579
|
+
API->>Dispatch: createManualDispatch({ ...input, resources: snapshot.resources })
|
|
580
|
+
|
|
581
|
+
Dispatch->>Dispatch: 1. resolveStack(agentStack, resources)
|
|
582
|
+
Note over Dispatch: Translates AgentStack CRD → execution config (model, prompt, tools, transport)
|
|
583
|
+
alt Stack not found
|
|
584
|
+
Dispatch-->>API: { error: true, reason: 'stack-not-found' }
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
Dispatch->>Perm: 2. reviewPermissions({ repository, ref, actor, agentStack, resources })
|
|
588
|
+
Note over Perm: Check cross-org, fork, SA, roles, secrets, configs
|
|
589
|
+
alt Permission denied
|
|
590
|
+
Dispatch-->>API: { error: true, reason: 'permission-denied' }
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
Dispatch->>Hooks: emit('RUN_CREATED', { runId, org, stackRef })
|
|
594
|
+
|
|
595
|
+
Dispatch->>Memory: 3. Memory snapshot (if AgentMemoryRepository exists)
|
|
596
|
+
Memory-->>Dispatch: memorySnapshot resource
|
|
597
|
+
|
|
598
|
+
alt Requires approval
|
|
599
|
+
Dispatch->>Dispatch: Create AgentDispatchRun (phase: AwaitingApproval)
|
|
600
|
+
Dispatch->>Approval: createApprovalRequest({ dispatchRun, action: 'secret-access' })
|
|
601
|
+
Dispatch->>Hooks: emit('APPROVAL_REQUESTED', { dispatchRun, action })
|
|
602
|
+
Dispatch-->>API: { run, approval, awaitingApproval: true }
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
Dispatch->>Workspace: 4. findReusableWorkspace({ org, repo, branch })
|
|
606
|
+
alt Reusable found
|
|
607
|
+
Workspace-->>Dispatch: claimWorkspace result
|
|
608
|
+
else No reusable
|
|
609
|
+
Dispatch->>Workspace: createWorkspace({ org, repo, branch })
|
|
610
|
+
Workspace-->>Dispatch: { workspace, pvcManifest }
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
Dispatch->>Context: 5. assembleContextBundle({ stack, repository, ref })
|
|
614
|
+
Context-->>Dispatch: contextBundle resource
|
|
615
|
+
|
|
616
|
+
Dispatch->>Dispatch: 6. Create AgentDispatchRun + AgentDispatchAttempt
|
|
617
|
+
Dispatch->>Dispatch: checkBudget({ org, model, estimatedTokens })
|
|
618
|
+
Note over Dispatch: Enforced via activeDeadlineSeconds + model-based cost tracking
|
|
619
|
+
|
|
620
|
+
Dispatch->>Dispatch: 7. createAgentJob(run, executionConfig)
|
|
621
|
+
Note over Dispatch: Generates batch/v1 Job manifest with image, command, env, volumes, resources, deadline
|
|
622
|
+
Dispatch->>K8s: submitAgentJob(jobManifest)
|
|
623
|
+
K8s-->>Dispatch: Job created (phase: Pending)
|
|
624
|
+
Dispatch->>Dispatch: run.status.phase = 'Running'
|
|
625
|
+
Dispatch->>Hooks: emit('STEP_STARTED', { runId, jobName })
|
|
626
|
+
|
|
627
|
+
K8s->>Pod: Schedule agent pod (workspace PVC mounted at /workspace)
|
|
628
|
+
Note over Pod: Env: AGENT_MUX_TRANSPORT, TRANSPORT_MUX_CODEC injected by resolveTransport()
|
|
629
|
+
Pod->>Pod: Execute agent session
|
|
630
|
+
Pod->>Callback: POST /api/orgs/:org/agents/runs/:name/callback (result)
|
|
631
|
+
Callback->>Dispatch: persistSessionEvent(runId, result)
|
|
632
|
+
Dispatch->>Dispatch: run.status.phase = 'Completed' | 'Failed'
|
|
633
|
+
Dispatch->>Hooks: emit('RUN_COMPLETED' | 'RUN_FAILED', { runId, result })
|
|
634
|
+
|
|
635
|
+
Dispatch-->>API: { run, attempt, contextBundle, workspace, jobName }
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### 8.2 K8s Job Manifest Structure
|
|
639
|
+
|
|
640
|
+
`createAgentJob(run, executionConfig)` produces a `batch/v1` Job manifest with
|
|
641
|
+
the following structure:
|
|
642
|
+
|
|
643
|
+
```javascript
|
|
644
|
+
{
|
|
645
|
+
apiVersion: 'batch/v1',
|
|
646
|
+
kind: 'Job',
|
|
647
|
+
metadata: {
|
|
648
|
+
name: `agent-${run.metadata.name}`,
|
|
649
|
+
namespace: run.metadata.namespace,
|
|
650
|
+
labels: {
|
|
651
|
+
'kradle.a5c.ai/org': org,
|
|
652
|
+
'kradle.a5c.ai/dispatch-run': run.metadata.name,
|
|
653
|
+
'kradle.a5c.ai/agent-job': 'true'
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
spec: {
|
|
657
|
+
activeDeadlineSeconds: budgetDeadline, // budget enforcement
|
|
658
|
+
backoffLimit: 0, // no automatic retry (kradle handles retries)
|
|
659
|
+
template: {
|
|
660
|
+
spec: {
|
|
661
|
+
serviceAccountName: executionConfig.serviceAccountName,
|
|
662
|
+
restartPolicy: 'Never',
|
|
663
|
+
containers: [{
|
|
664
|
+
name: 'agent',
|
|
665
|
+
image: executionConfig.agentImage,
|
|
666
|
+
command: executionConfig.command,
|
|
667
|
+
env: [
|
|
668
|
+
{ name: 'AGENT_MUX_TRANSPORT', value: resolvedTransport.transport },
|
|
669
|
+
{ name: 'TRANSPORT_MUX_CODEC', value: resolvedTransport.codec },
|
|
670
|
+
{ name: 'KRADLE_CALLBACK_URL', value: callbackUrl },
|
|
671
|
+
{ name: 'KRADLE_RUN_ID', value: run.metadata.name },
|
|
672
|
+
// ...stack-specific env vars
|
|
673
|
+
],
|
|
674
|
+
resources: executionConfig.resourceRequests,
|
|
675
|
+
volumeMounts: [{ name: 'workspace', mountPath: '/workspace' }]
|
|
676
|
+
}],
|
|
677
|
+
volumes: [{
|
|
678
|
+
name: 'workspace',
|
|
679
|
+
persistentVolumeClaim: { claimName: workspace.spec.pvcName }
|
|
680
|
+
}]
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
### 8.3 Transport Resolution and Codec Injection
|
|
688
|
+
|
|
689
|
+
`resolveTransport(stack, resources)` reads the `AgentTransportBinding` referenced
|
|
690
|
+
by the stack's adapter and produces the environment variables injected into the Job:
|
|
691
|
+
|
|
692
|
+
| Variable | Source | Example |
|
|
693
|
+
|----------|--------|---------|
|
|
694
|
+
| `AGENT_MUX_TRANSPORT` | `binding.spec.protocol` | `'websocket'` |
|
|
695
|
+
| `TRANSPORT_MUX_CODEC` | `binding.spec.codec ?? 'json'` | `'json'` |
|
|
696
|
+
|
|
697
|
+
The agent pod reads these variables at startup to configure its message framing
|
|
698
|
+
and connection lifecycle. No agent code changes are needed when switching transports.
|
|
699
|
+
|
|
700
|
+
### 8.4 Budget Enforcement
|
|
701
|
+
|
|
702
|
+
`checkBudget({ org, model, estimatedTokens })` runs before Job creation:
|
|
703
|
+
|
|
704
|
+
1. Load `AgentProviderConfig` for the stack's model/provider
|
|
705
|
+
2. Compute `estimateCost(model, estimatedTokens)` using model-based rate tables
|
|
706
|
+
3. Compare against `org.spec.budgetLimitUsd` or a default ceiling
|
|
707
|
+
4. If over budget: return `{ allowed: false, reason: 'budget-exceeded' }` — dispatch aborted
|
|
708
|
+
5. If within budget: set `activeDeadlineSeconds` on the Job spec proportional to the
|
|
709
|
+
remaining budget at the model's token rate
|
|
710
|
+
|
|
711
|
+
This ensures agent pods are automatically terminated by Kubernetes if they run
|
|
712
|
+
longer than the budget allows, even without an explicit callback.
|
|
713
|
+
|
|
714
|
+
### 8.5 Callback-Based Result Collection
|
|
715
|
+
|
|
716
|
+
The agent pod calls `POST /api/orgs/:org/agents/runs/:name/callback` when the
|
|
717
|
+
session completes:
|
|
718
|
+
|
|
719
|
+
```
|
|
720
|
+
POST /api/orgs/{org}/agents/runs/{name}/callback
|
|
721
|
+
Authorization: Bearer <kradle-run-token>
|
|
722
|
+
Content-Type: application/json
|
|
723
|
+
|
|
724
|
+
{
|
|
725
|
+
"phase": "Succeeded" | "Failed",
|
|
726
|
+
"exitCode": 0 | 1,
|
|
727
|
+
"artifacts": [...],
|
|
728
|
+
"costUsd": 0.042,
|
|
729
|
+
"errorMessage": "..." // present on failure
|
|
730
|
+
}
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
`persistSessionEvent(runId, result)` applies the callback payload to the
|
|
734
|
+
`AgentDispatchRun` and `AgentSession` resources, then emits the appropriate
|
|
735
|
+
hooks lifecycle event.
|
|
736
|
+
|
|
737
|
+
### 8.6 Hooks Lifecycle Events
|
|
738
|
+
|
|
739
|
+
`createHooksLifecycleEmitter(bus)` wraps the internal event bus and emits
|
|
740
|
+
9 lifecycle events:
|
|
741
|
+
|
|
742
|
+
| Event | Trigger |
|
|
743
|
+
|-------|---------|
|
|
744
|
+
| `RUN_CREATED` | `createManualDispatch()` called |
|
|
745
|
+
| `RUN_QUEUED` | Run enters `Queued` phase (budget check pending) |
|
|
746
|
+
| `RUN_STARTED` | Job submitted to K8s, pod scheduled |
|
|
747
|
+
| `STEP_STARTED` | Agent pod reports a tool-use or reasoning step |
|
|
748
|
+
| `STEP_COMPLETED` | Agent pod reports step completion |
|
|
749
|
+
| `APPROVAL_REQUESTED` | `createApprovalRequest()` called |
|
|
750
|
+
| `APPROVAL_GRANTED` | `recordDecision()` with `Approved` |
|
|
751
|
+
| `APPROVAL_DENIED` | `recordDecision()` with `Denied` |
|
|
752
|
+
| `RUN_COMPLETED` | Callback received with `phase: Succeeded` |
|
|
753
|
+
| `RUN_FAILED` | Callback received with `phase: Failed`, or Job deadline exceeded |
|
|
754
|
+
|
|
755
|
+
These events flow to registered `WebhookSubscription` endpoints and the
|
|
756
|
+
notification system.
|
|
757
|
+
|
|
758
|
+
### 8.7 Permission Review Steps
|
|
759
|
+
|
|
760
|
+
1. Resolve AgentStack from resources via `resolveStack()`
|
|
761
|
+
2. Validate approvalMode (yolo/prompt/deny)
|
|
762
|
+
3. Cross-org denial: agent org vs repository org
|
|
763
|
+
4. Expand capabilities from stack spec (tools, MCP, skills, subagents)
|
|
764
|
+
5. Untrusted fork detection (`refs/pull/\d+/`)
|
|
765
|
+
6. Check AgentServiceAccount binding
|
|
766
|
+
7. Check AgentRoleBinding for subject
|
|
767
|
+
8. Check AgentSecretGrant for agent
|
|
768
|
+
9. Check AgentConfigGrant for agent
|
|
769
|
+
10. Compute decision: `allowed`, `requires-approval`, or `denied`
|
|
770
|
+
|
|
771
|
+
### 8.8 Decision Matrix
|
|
772
|
+
|
|
773
|
+
| approvalMode | Errors | Fork | Decision |
|
|
774
|
+
|-------------|--------|------|----------|
|
|
775
|
+
| `deny` | any | any | `denied` |
|
|
776
|
+
| `yolo` | none | false | `allowed` |
|
|
777
|
+
| `yolo` | none | true | `allowed` (warnings only) |
|
|
778
|
+
| `prompt` | none | false | `requires-approval` |
|
|
779
|
+
| `prompt` | none | true | `requires-approval` |
|
|
780
|
+
| any | has errors | any | `denied` |
|
|
781
|
+
|
|
782
|
+
---
|
|
783
|
+
|
|
784
|
+
## 9. External Sync Pipeline
|
|
785
|
+
|
|
786
|
+
### 9.1 Complete Flow
|
|
787
|
+
|
|
788
|
+
```mermaid
|
|
789
|
+
sequenceDiagram
|
|
790
|
+
participant Ext as External Provider (GitHub)
|
|
791
|
+
participant Ingress as POST /api/orgs/:org/agents/webhooks/ingest
|
|
792
|
+
participant HTTP as HTTP Handler
|
|
793
|
+
participant Normalize as normalizeWebhookEvent()
|
|
794
|
+
participant API as ApiController
|
|
795
|
+
participant Trigger as TriggerController
|
|
796
|
+
participant Webhook as WebhookController
|
|
797
|
+
participant Sync as SyncController
|
|
798
|
+
participant Conflict as ConflictController
|
|
799
|
+
participant Write as WriteController
|
|
800
|
+
participant K8s as Kubernetes
|
|
801
|
+
|
|
802
|
+
Ext->>Ingress: POST with X-Hub-Signature-256 header
|
|
803
|
+
Ingress->>HTTP: Match /api/orgs/:org/agents/webhooks/ingest
|
|
804
|
+
HTTP->>Normalize: normalizeWebhookEvent(body, org)
|
|
805
|
+
Note over Normalize: Pattern match: workflow_run, PR, comment, label, push
|
|
806
|
+
Normalize-->>HTTP: Canonical event { type, source, repository, ref, actor, payload }
|
|
807
|
+
HTTP->>API: processWebhookEvent({ event, organizationRef, namespace })
|
|
808
|
+
API->>API: snapshot()
|
|
809
|
+
API->>Trigger: createAgentTriggerController({ dispatchController })
|
|
810
|
+
API->>Trigger: processEvent({ event, resources, namespace, organizationRef })
|
|
811
|
+
|
|
812
|
+
Trigger->>Trigger: evaluateEvent — match event type against each rule's sources
|
|
813
|
+
loop For each matching rule
|
|
814
|
+
Trigger->>Trigger: Dedup check (existing TriggerExecution with same eventUid)
|
|
815
|
+
alt Not duplicate
|
|
816
|
+
Trigger->>Trigger: createTriggerExecution (phase: Dispatching)
|
|
817
|
+
Trigger->>Dispatch: createManualDispatch(...)
|
|
818
|
+
end
|
|
819
|
+
end
|
|
820
|
+
Trigger-->>API: { processed, dispatched, skipped, executions }
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
### 9.2 Webhook Event Normalization
|
|
824
|
+
|
|
825
|
+
Source: `normalizeWebhookEvent()` in `http-server.js`
|
|
826
|
+
|
|
827
|
+
| GitHub Action/Shape | Kradle Event Type | Source Kind |
|
|
828
|
+
|--------------------|-----------------|-------------|
|
|
829
|
+
| `completed` + `workflow_run.conclusion=failure` | `ci-failure` | Pipeline |
|
|
830
|
+
| `opened` + `pull_request` | `pr-opened` | PullRequest |
|
|
831
|
+
| `created` + `comment` | `comment` | Issue/PullRequest |
|
|
832
|
+
| `labeled` | `label-added` | Issue/PullRequest |
|
|
833
|
+
| `opened` + `issue` (no PR) | `issue-created` | Issue |
|
|
834
|
+
| `ref` + `commits` | `push` | Repository |
|
|
835
|
+
| (fallback) | `webhook` | WebhookDelivery |
|
|
836
|
+
|
|
837
|
+
### 9.3 HMAC Verification
|
|
838
|
+
|
|
839
|
+
Source: `external/webhook-controller.js`
|
|
840
|
+
|
|
841
|
+
```javascript
|
|
842
|
+
verifyHmacSignature(body, signature) {
|
|
843
|
+
// 1. Reject if no signature header
|
|
844
|
+
// 2. Reject if not prefixed with 'sha256='
|
|
845
|
+
// 3. Compute expected: 'sha256=' + createHmac('sha256', secret).update(body).digest('hex')
|
|
846
|
+
// 4. timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
|
|
847
|
+
// 5. Return { valid: true/false, reason }
|
|
848
|
+
}
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
### 9.4 Sync Controller Ownership Modes
|
|
852
|
+
|
|
853
|
+
| Mode | Kradle Writes | External Writes |
|
|
854
|
+
|------|-------------|-----------------|
|
|
855
|
+
| `bidirectional` | Allowed | Allowed |
|
|
856
|
+
| `external-owned` | Blocked | Allowed |
|
|
857
|
+
| `kradle-owned` | Allowed | Blocked |
|
|
858
|
+
|
|
859
|
+
### 9.5 Watermark Tracking
|
|
860
|
+
|
|
861
|
+
- Per-binding watermark stored as ISO timestamp
|
|
862
|
+
- Only advances forward (new timestamp must be > current)
|
|
863
|
+
- Persisted as `ExternalSyncWatermark` CRD resource
|
|
864
|
+
|
|
865
|
+
---
|
|
866
|
+
|
|
867
|
+
## 10. Memory Query Pipeline
|
|
868
|
+
|
|
869
|
+
### 10.1 From Search Form to Results
|
|
870
|
+
|
|
871
|
+
```mermaid
|
|
872
|
+
sequenceDiagram
|
|
873
|
+
participant UI as Memory Search Form
|
|
874
|
+
participant HTTP as HTTP Handler
|
|
875
|
+
participant API as ApiController
|
|
876
|
+
participant Memory as MemoryController
|
|
877
|
+
participant QueryEngine as queryMemory()
|
|
878
|
+
|
|
879
|
+
UI->>HTTP: POST /api/orgs/:org/agents/memory/query { query, mode, kinds, depth }
|
|
880
|
+
HTTP->>API: queryAgentMemory({ query, mode, ... })
|
|
881
|
+
API->>Memory: queryMemory({ query, mode, organizationRef })
|
|
882
|
+
Memory->>QueryEngine: queryMemory({ records, documents, edges, query, mode })
|
|
883
|
+
|
|
884
|
+
alt mode = 'graph-only'
|
|
885
|
+
QueryEngine->>QueryEngine: queryGraph({ records, edges, query, kinds, depth })
|
|
886
|
+
Note over QueryEngine: buildAdjacency → filter by nodeKind → score → follow edges → sort
|
|
887
|
+
else mode = 'grep-only'
|
|
888
|
+
QueryEngine->>QueryEngine: queryGrep({ documents, query, paths, context })
|
|
889
|
+
Note over QueryEngine: filter by glob → line-by-line search → extract context
|
|
890
|
+
else mode = 'graph-and-grep'
|
|
891
|
+
QueryEngine->>QueryEngine: queryGraph + queryGrep (both)
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
QueryEngine-->>Memory: { graph, grep, stats }
|
|
895
|
+
Memory-->>API: result
|
|
896
|
+
API-->>HTTP: JSON response
|
|
897
|
+
HTTP-->>UI: { graph: { matches, totalMatches }, grep: { excerpts, totalMatches } }
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
### 10.2 Graph Scoring Algorithm
|
|
901
|
+
|
|
902
|
+
```javascript
|
|
903
|
+
function scoreRecord(record, lowerQuery) {
|
|
904
|
+
const id = String(record.id || '').toLowerCase();
|
|
905
|
+
const attrs = JSON.stringify(record.attributes || {}).toLowerCase();
|
|
906
|
+
if (id.includes(lowerQuery)) return 2; // ID match: higher priority
|
|
907
|
+
if (attrs.includes(lowerQuery)) return 1; // Attribute match
|
|
908
|
+
return 0; // No match
|
|
909
|
+
}
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
### 10.3 Edge Traversal (BFS)
|
|
913
|
+
|
|
914
|
+
```javascript
|
|
915
|
+
function followEdges(startId, adjacency, maxDepth) {
|
|
916
|
+
// BFS from startId up to maxDepth hops
|
|
917
|
+
// visited Set prevents cycles
|
|
918
|
+
// Returns flat array of all encountered edges
|
|
919
|
+
}
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
### 10.4 Grep Highlighting
|
|
923
|
+
|
|
924
|
+
Match output format:
|
|
925
|
+
```javascript
|
|
926
|
+
{
|
|
927
|
+
path: 'docs/design.md',
|
|
928
|
+
lineNumber: 42,
|
|
929
|
+
line: 'The agent memory stores knowledge graphs...',
|
|
930
|
+
highlighted: 'The agent **memory** stores knowledge graphs...',
|
|
931
|
+
context: '...\nThe agent memory stores knowledge graphs...\n...',
|
|
932
|
+
contextStart: 41,
|
|
933
|
+
contextEnd: 43
|
|
934
|
+
}
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
---
|
|
938
|
+
|
|
939
|
+
## 11. Workspace Provisioning
|
|
940
|
+
|
|
941
|
+
### 11.1 PVC-Based Provisioning
|
|
942
|
+
|
|
943
|
+
Source: `packages/kradle/core/src/agent-workspace-controller.js`
|
|
944
|
+
|
|
945
|
+
```mermaid
|
|
946
|
+
flowchart TD
|
|
947
|
+
A[Dispatch trigger] --> B{Find reusable workspace?}
|
|
948
|
+
B -->|Yes: same repo+branch+Ready| C[claimWorkspace]
|
|
949
|
+
B -->|No| D[createWorkspace]
|
|
950
|
+
|
|
951
|
+
C --> E[Mark phase=InUse, set runRef]
|
|
952
|
+
D --> F[Generate workspace name]
|
|
953
|
+
F --> G[Generate PVC manifest]
|
|
954
|
+
G --> H[Create KradleWorkspace resource]
|
|
955
|
+
|
|
956
|
+
E --> I[getMountSpec]
|
|
957
|
+
H --> I
|
|
958
|
+
|
|
959
|
+
I --> J[Return { volume, volumeMount }]
|
|
960
|
+
J --> K[Attach to AgentDispatchRun.spec.mountSpec]
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
### 11.2 PVC Manifest Structure
|
|
964
|
+
|
|
965
|
+
```javascript
|
|
966
|
+
{
|
|
967
|
+
apiVersion: 'v1',
|
|
968
|
+
kind: 'PersistentVolumeClaim',
|
|
969
|
+
metadata: {
|
|
970
|
+
name: 'kradle-ws-<workspace-name>',
|
|
971
|
+
namespace: '<org-namespace>',
|
|
972
|
+
labels: {
|
|
973
|
+
'kradle.a5c.ai/workspace': '<workspace-name>',
|
|
974
|
+
'kradle.a5c.ai/org': '<org>'
|
|
975
|
+
}
|
|
976
|
+
},
|
|
977
|
+
spec: {
|
|
978
|
+
storageClassName: 'standard', // configurable via volumeSpec.storageClassName
|
|
979
|
+
accessModes: ['ReadWriteOnce'], // configurable
|
|
980
|
+
resources: { requests: { storage: '10Gi' } } // configurable via volumeSpec.capacity
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
### 11.3 Codespace Pod Spec
|
|
986
|
+
|
|
987
|
+
When `launchCodespace()` is called:
|
|
988
|
+
- Image: `codercom/code-server:latest` (configurable)
|
|
989
|
+
- CPU: 1 core limit, 250m request
|
|
990
|
+
- Memory: 2Gi limit, 512Mi request
|
|
991
|
+
- Port: 8080
|
|
992
|
+
- Volume: PVC mount at `/workspace`
|
|
993
|
+
- Env: `KRADLE_WORKSPACE`, `KRADLE_ORG`, `GIT_AUTHOR_NAME`, `GIT_AUTHOR_EMAIL`
|
|
994
|
+
- Service: ClusterIP on port 8080
|
|
995
|
+
- URL pattern: `http://codespace-svc-<ws>.<namespace>.svc.cluster.local:8080`
|
|
996
|
+
|
|
997
|
+
### 11.4 Workspace Phase Transitions
|
|
998
|
+
|
|
999
|
+
```
|
|
1000
|
+
Pending → Ready → InUse → Ready (release)
|
|
1001
|
+
→ Archived (archive)
|
|
1002
|
+
→ Terminating (delete)
|
|
1003
|
+
Archived → Active (recover)
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
---
|
|
1007
|
+
|
|
1008
|
+
## 12. Notification Pipeline
|
|
1009
|
+
|
|
1010
|
+
Source: `packages/kradle/core/src/notification-controller.js`
|
|
1011
|
+
|
|
1012
|
+
### 12.1 Event-to-Notification Mapping
|
|
1013
|
+
|
|
1014
|
+
| Source Event Type | Notification Type | Severity |
|
|
1015
|
+
|-------------------|------------------|----------|
|
|
1016
|
+
| `AgentDispatchRun` (completed) | `run-complete` | info |
|
|
1017
|
+
| `AgentDispatchRun` (failed) | `run-complete` | error |
|
|
1018
|
+
| `AgentApproval` (pending) | `approval-needed` | warning |
|
|
1019
|
+
| `ExternalSyncConflict` | `conflict-detected` | warning |
|
|
1020
|
+
| `KradleWorkspace` (claimed) | `workspace-ready` | info |
|
|
1021
|
+
| (default) | `system` | info |
|
|
1022
|
+
|
|
1023
|
+
### 12.2 Notification Delivery Flow
|
|
1024
|
+
|
|
1025
|
+
```mermaid
|
|
1026
|
+
sequenceDiagram
|
|
1027
|
+
participant Controller as Any Controller
|
|
1028
|
+
participant NotifCtrl as NotificationController
|
|
1029
|
+
participant Store as In-Memory Store (Map)
|
|
1030
|
+
participant Bus as EventBus
|
|
1031
|
+
participant SSE as SSE Stream
|
|
1032
|
+
participant Bell as NotificationBell
|
|
1033
|
+
|
|
1034
|
+
Controller->>NotifCtrl: createNotification(event)
|
|
1035
|
+
NotifCtrl->>NotifCtrl: mapEventToNotification(event)
|
|
1036
|
+
NotifCtrl->>Store: store.get(org).push(notification)
|
|
1037
|
+
NotifCtrl->>Bus: emit({ type: 'notification', ... })
|
|
1038
|
+
Bus->>SSE: Forward to all subscribers
|
|
1039
|
+
SSE->>Bell: EventSource receives
|
|
1040
|
+
Bell->>Bell: Increment unread count badge
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
### 12.3 User Preferences
|
|
1044
|
+
|
|
1045
|
+
Default preferences:
|
|
1046
|
+
```javascript
|
|
1047
|
+
{ runs: true, approvals: true, conflicts: true, workspaces: true, sound: false, desktop: false }
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
---
|
|
1051
|
+
|
|
1052
|
+
## 13. Event Bus and SSE Streaming
|
|
1053
|
+
|
|
1054
|
+
### 13.1 Event Bus Implementation
|
|
1055
|
+
|
|
1056
|
+
Source: `packages/kradle/core/src/event-bus.js`
|
|
1057
|
+
|
|
1058
|
+
- Uses a `Set<Function>` for listeners (O(1) add/remove)
|
|
1059
|
+
- `emit(event)` iterates all listeners synchronously
|
|
1060
|
+
- `emitResourceChange(kind, name, operation)` adds timestamp
|
|
1061
|
+
- Global singleton: `globalEventBus`
|
|
1062
|
+
|
|
1063
|
+
### 13.2 SSE Endpoint
|
|
1064
|
+
|
|
1065
|
+
Route: `GET /api/orgs/:org/agents/events/stream`
|
|
1066
|
+
|
|
1067
|
+
Response headers:
|
|
1068
|
+
```
|
|
1069
|
+
Content-Type: text/event-stream
|
|
1070
|
+
Cache-Control: no-cache
|
|
1071
|
+
Connection: keep-alive
|
|
1072
|
+
X-Accel-Buffering: no
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
Protocol:
|
|
1076
|
+
1. Initial: `data: {"type":"connected"}\n\n`
|
|
1077
|
+
2. Every 30s: `data: {"type":"heartbeat"}\n\n`
|
|
1078
|
+
3. On resource change: `data: {"type":"resource-change","kind":"...","name":"...","operation":"apply","timestamp":"..."}\n\n`
|
|
1079
|
+
4. On client disconnect: `clearInterval(heartbeat)`, `globalEventBus.unsubscribe(writer)`
|
|
1080
|
+
|
|
1081
|
+
---
|
|
1082
|
+
|
|
1083
|
+
## 14. Async Utilities
|
|
1084
|
+
|
|
1085
|
+
Source: `packages/kradle/core/src/async-controller.js`
|
|
1086
|
+
|
|
1087
|
+
### 14.1 Event Batcher
|
|
1088
|
+
|
|
1089
|
+
```javascript
|
|
1090
|
+
createEventBatcher(handler, { maxBatchSize: 50, flushIntervalMs: 1000 })
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
Behavior:
|
|
1094
|
+
- Accumulates events in array
|
|
1095
|
+
- Flushes when `batch.length >= maxBatchSize` (fire-and-forget)
|
|
1096
|
+
- Flushes on timer (setTimeout) when batch has items but below threshold
|
|
1097
|
+
- `flush()` forces immediate flush (awaitable)
|
|
1098
|
+
- `stop()` clears timer and buffer
|
|
1099
|
+
|
|
1100
|
+
### 14.2 Retry Policy
|
|
1101
|
+
|
|
1102
|
+
```javascript
|
|
1103
|
+
createRetryPolicy({ maxRetries: 3, baseDelayMs: 1000, maxDelayMs: 30000, jitter: true })
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1106
|
+
Delay formula: `min(baseDelayMs * 2^attempt, maxDelayMs)` with optional full-jitter `[0, capped]`
|
|
1107
|
+
|
|
1108
|
+
### 14.3 Delivery Queue
|
|
1109
|
+
|
|
1110
|
+
```javascript
|
|
1111
|
+
createDeliveryQueue(processor, { concurrency: 5, retryPolicy })
|
|
1112
|
+
```
|
|
1113
|
+
|
|
1114
|
+
- In-memory ordered queue
|
|
1115
|
+
- Up to `concurrency` items processed in parallel
|
|
1116
|
+
- Each item retried per retryPolicy on failure
|
|
1117
|
+
- `drain()` returns Promise that resolves when queue is empty and all active items complete
|
|
1118
|
+
- `stop()` clears queue and resolves all drain waiters
|
|
1119
|
+
|
|
1120
|
+
### 14.4 Checkpointer
|
|
1121
|
+
|
|
1122
|
+
```javascript
|
|
1123
|
+
createCheckpointer(storage = new Map())
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
Simple key-value store: `save(key, value)`, `load(key)`, `clear(key)`, `listKeys()`
|
|
1127
|
+
|
|
1128
|
+
---
|
|
1129
|
+
|
|
1130
|
+
## 15. Controller Boundary Declarations
|
|
1131
|
+
|
|
1132
|
+
Every controller exports a frozen boundary object. This serves as both documentation and runtime introspection.
|
|
1133
|
+
|
|
1134
|
+
| Controller | Source File | Role | Owns | Must Not Own |
|
|
1135
|
+
|-----------|-------------|------|------|--------------|
|
|
1136
|
+
| KubernetesResourceClient | `kubernetes-controller.js` | kubectl execution | command exec, API discovery, access checks, watch streams | HTTP routes, pages, forge DTOs |
|
|
1137
|
+
| KradleKubernetesReconciler | `kubernetes-controller.js` | Resource reconciliation | repo status, identity projection, hosting intent, policy sync | HTTP routes, pages, API DTOs |
|
|
1138
|
+
| KubernetesResourceGateway | `kubernetes-resource-gateway.js` | API port delegation | resource definitions, CRUD delegation, namespace scoping | HTTP routes, page flows, reconciliation |
|
|
1139
|
+
| KradleApiController | `api-controller.js` | HTTP facade | validation, DTOs, errors, workflow affordances, UI snapshots | kubectl execution, reconciliation loops |
|
|
1140
|
+
| AgentStackController | `agent-stack-controller.js` | Stack readiness | capability resolution, conditions, readiness, MCP health | secrets, dispatch execution, Mux sessions |
|
|
1141
|
+
| AgentDispatchController | `agent-dispatch-controller.js` | Dispatch orchestration | dispatch creation, attempt lifecycle, session binding, workspace | secrets, UI rendering |
|
|
1142
|
+
| AgentWorkspaceController | `agent-workspace-controller.js` | Workspace provisioning | workspace creation, PVC gen, git specs, mount specs, reuse, codespace | git execution, K8s API, secrets |
|
|
1143
|
+
| AgentTriggerController | `agent-trigger-controller.js` | Event routing | normalization, rule matching, trigger records, dispatch initiation | event sourcing, webhook delivery |
|
|
1144
|
+
| AgentApprovalController | `agent-approval-controller.js` | Approval gates | approval creation, decision recording, lookup, dedup | secrets, agent execution, UI |
|
|
1145
|
+
| AgentMemoryQuery | `agent-memory-query.js` | Query execution | graph traversal, filtering, scoring, grep, context extraction | persistence, HTTP, K8s, secrets |
|
|
1146
|
+
| WebhookController | `external/webhook-controller.js` | Inbound webhooks | HMAC validation, delivery records, dedup, event queue | resource persistence, ownership |
|
|
1147
|
+
| SyncController | `external/sync-controller.js` | External sync | normalization, upsert, watermarks, ownership, tombstones | HMAC, webhook delivery |
|
|
1148
|
+
| ConflictController | `external/conflict-controller.js` | Conflict detection | detection, resolution, superseded cleanup | write intent, sync scheduling |
|
|
1149
|
+
| WriteController | `external/write-controller.js` | Write intents | creation, approval gate, retry, idempotency | conflict resolution, sync state |
|
|
1150
|
+
| AuditController | `audit-controller.js` | Audit log | event recording, streaming, replay, metrics | identity, storage, git |
|
|
1151
|
+
| RunnerController | `runner-controller.js` | Runner pools | pool validation, lifecycle, scheduling, pod specs, capacity | K8s API calls, actual pod creation |
|
|
1152
|
+
| NotificationController | `notification-controller.js` | Notifications | creation, listing, read state, preferences | event dispatch, UI rendering, push |
|
|
1153
|
+
| PermissionReviewer | `agent-permission-review.js` | Permission review | capability expansion, grant resolution, snapshot creation | secrets, K8s API, runtime execution |
|
|
1154
|
+
|
|
1155
|
+
---
|
|
1156
|
+
|
|
1157
|
+
## 16. Concurrency Model
|
|
1158
|
+
|
|
1159
|
+
### 16.1 Single-Threaded Event Loop
|
|
1160
|
+
|
|
1161
|
+
Kradle core runs on Node.js's single-threaded event loop:
|
|
1162
|
+
- All kubectl calls use `spawnSync` (blocking) during snapshot collection
|
|
1163
|
+
- API request handling is async (Node HTTP server)
|
|
1164
|
+
- Background revalidation uses `Promise.resolve().then(...)` (microtask)
|
|
1165
|
+
- No worker threads or clustering in the core package
|
|
1166
|
+
|
|
1167
|
+
### 16.2 Concurrent Access Patterns
|
|
1168
|
+
|
|
1169
|
+
| Pattern | Mechanism |
|
|
1170
|
+
|---------|-----------|
|
|
1171
|
+
| Multiple orgs cached | Per-org Map entries, independent TTLs |
|
|
1172
|
+
| SSE connections | Set of listener functions, one per connection |
|
|
1173
|
+
| Background revalidation | `revalidating` flag prevents thundering herd |
|
|
1174
|
+
| Event bus | Synchronous iteration over Set (no races) |
|
|
1175
|
+
| Audit store | Append-only array, seq counter |
|
|
1176
|
+
| Notification store | Per-org array, no locking needed |
|
|
1177
|
+
|
|
1178
|
+
---
|
|
1179
|
+
|
|
1180
|
+
## 17. Error Handling Strategy
|
|
1181
|
+
|
|
1182
|
+
### 17.1 HTTP Layer
|
|
1183
|
+
|
|
1184
|
+
```javascript
|
|
1185
|
+
try {
|
|
1186
|
+
// Route matching and handler execution
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
return send(response, 400, { error: 'bad_request', message: error.message });
|
|
1189
|
+
}
|
|
1190
|
+
```
|
|
1191
|
+
|
|
1192
|
+
All unhandled errors in route handlers become 400 responses.
|
|
1193
|
+
|
|
1194
|
+
### 17.2 Controller Layer
|
|
1195
|
+
|
|
1196
|
+
Controllers return error objects instead of throwing:
|
|
1197
|
+
```javascript
|
|
1198
|
+
{ error: true, reason: 'stack-not-found', message: 'AgentStack not found' }
|
|
1199
|
+
```
|
|
1200
|
+
|
|
1201
|
+
### 17.3 kubectl Layer
|
|
1202
|
+
|
|
1203
|
+
- `allowFailure: true` — returns `{ ok: false }`, caller decides
|
|
1204
|
+
- `allowFailure: false` — throws Error with `commandFailure()` message
|
|
1205
|
+
|
|
1206
|
+
### 17.4 Audit Event Failures
|
|
1207
|
+
|
|
1208
|
+
```javascript
|
|
1209
|
+
function emitAuditEvent(resource, operation) {
|
|
1210
|
+
try { ... } catch { /* Audit failures must not crash apply operations */ }
|
|
1211
|
+
}
|
|
1212
|
+
```
|
|
1213
|
+
|
|
1214
|
+
### 17.5 Background Revalidation Failures
|
|
1215
|
+
|
|
1216
|
+
```javascript
|
|
1217
|
+
try { const fresh = await revalidateFn(); ... }
|
|
1218
|
+
catch { orgCacheMap.set(key, { ...current, revalidating: false }); }
|
|
1219
|
+
```
|
|
1220
|
+
|
|
1221
|
+
---
|
|
1222
|
+
|
|
1223
|
+
## 18. Deployment Architecture
|
|
1224
|
+
|
|
1225
|
+
### 18.1 Container Topology
|
|
1226
|
+
|
|
1227
|
+
| Container | Port | Role |
|
|
1228
|
+
|-----------|------|------|
|
|
1229
|
+
| api | 3080 | HTTP API server (`kradle serve`) |
|
|
1230
|
+
| controllers | — | Background reconciliation (future) |
|
|
1231
|
+
| web | 3000 | Next.js web console |
|
|
1232
|
+
| webhook-worker | — | Inbound webhook processing |
|
|
1233
|
+
|
|
1234
|
+
### 18.2 CRD Management
|
|
1235
|
+
|
|
1236
|
+
- 76 CRDs under `kradle.a5c.ai/v1alpha1`
|
|
1237
|
+
- All use `x-kubernetes-preserve-unknown-fields: true`
|
|
1238
|
+
- All namespaced
|
|
1239
|
+
- Platform resources (Organization, OrgNamespaceBinding) in `kradle-system`
|
|
1240
|
+
- Org resources in `kradle-org-<slug>` namespaces
|
|
1241
|
+
|
|
1242
|
+
### 18.3 Infrastructure Requirements
|
|
1243
|
+
|
|
1244
|
+
| Component | Purpose |
|
|
1245
|
+
|-----------|---------|
|
|
1246
|
+
| AKS (or compatible K8s) | Container orchestration |
|
|
1247
|
+
| ACR (or registry) | Image storage |
|
|
1248
|
+
| cert-manager | TLS provisioning |
|
|
1249
|
+
| nginx ingress | HTTP routing |
|
|
1250
|
+
| PostgreSQL | Aggregated resource storage |
|
|
1251
|
+
| Gitea | Git hosting backend |
|
|
1252
|
+
| Kyverno (optional) | Policy engine |
|
|
1253
|
+
| KubeVela (optional) | Application delivery |
|
|
1254
|
+
|
|
1255
|
+
---
|
|
1256
|
+
|
|
1257
|
+
## 19. Data Storage Boundaries
|
|
1258
|
+
|
|
1259
|
+
| Storage Backend | Resource Count | Access Pattern |
|
|
1260
|
+
|----------------|---------------|----------------|
|
|
1261
|
+
| etcd (CRDs) | 44 CONFIG kinds | kubectl get/apply/delete |
|
|
1262
|
+
| PostgreSQL | 32 AGGREGATED kinds | In-memory during dev, runtime queries |
|
|
1263
|
+
| Gitea | Repository content | HTTP API, SSH |
|
|
1264
|
+
| In-memory | Notifications, audit, runners | Per-process, non-persistent |
|
|
1265
|
+
| Snapshot cache | Derived views | Stale-while-revalidate |
|
|
1266
|
+
|
|
1267
|
+
---
|
|
1268
|
+
|
|
1269
|
+
## 20. Configuration Reference
|
|
1270
|
+
|
|
1271
|
+
### 20.1 Core Server
|
|
1272
|
+
|
|
1273
|
+
| Variable | Default | Purpose |
|
|
1274
|
+
|----------|---------|---------|
|
|
1275
|
+
| `KRADLE_NAMESPACE` | `kradle-system` | Platform namespace |
|
|
1276
|
+
| `KRADLE_ORG` | `default` | Default organization |
|
|
1277
|
+
| `KRADLE_SNAPSHOT_CACHE_TTL_MS` | `30000` | Cache freshness TTL |
|
|
1278
|
+
| `KRADLE_GITEA_HTTP_URL` | (none) | Gitea API base URL |
|
|
1279
|
+
|
|
1280
|
+
### 20.2 External Integrations
|
|
1281
|
+
|
|
1282
|
+
| Variable | Default | Purpose |
|
|
1283
|
+
|----------|---------|---------|
|
|
1284
|
+
| `KRADLE_KYVERNO_MODE` | auto | Kyverno integration mode |
|
|
1285
|
+
| `KRADLE_KYVERNO_ENABLED` | (none) | Enable BYO Kyverno |
|
|
1286
|
+
| `KRADLE_KYVERNO_NAMESPACE` | `kyverno` | Kyverno deployment namespace |
|
|
1287
|
+
| `KRADLE_KYVERNO_POLICY_NAMESPACE` | platform ns | Policy storage namespace |
|
|
1288
|
+
| `KRADLE_KUBEVELA_NAMESPACE` | `vela-system` | KubeVela system namespace |
|
|
1289
|
+
|
|
1290
|
+
### 20.3 Runtime Identity
|
|
1291
|
+
|
|
1292
|
+
| Variable | Default | Purpose |
|
|
1293
|
+
|----------|---------|---------|
|
|
1294
|
+
| `KRADLE_SERVICE_ACCOUNT_DIR` | `/var/run/secrets/kubernetes.io/serviceaccount` | SA mount |
|
|
1295
|
+
| `KRADLE_SERVICE_ACCOUNT_TOKEN` | `<SA_DIR>/token` | Token file path |
|
|
1296
|
+
| `KRADLE_SERVICE_ACCOUNT_CA` | `<SA_DIR>/ca.crt` | CA cert path |
|
|
1297
|
+
|
|
1298
|
+
---
|
|
1299
|
+
|
|
1300
|
+
## 21. Resource Reconciliation Deep Dive
|
|
1301
|
+
|
|
1302
|
+
> Source: `packages/kradle/core/src/kubernetes-controller.js`
|
|
1303
|
+
|
|
1304
|
+
### 21.1 KRADLE_RESOURCES Array
|
|
1305
|
+
|
|
1306
|
+
The `KRADLE_RESOURCES` array (exported at module level) defines every resource the Kradle control plane manages. Each entry carries the following fields:
|
|
1307
|
+
|
|
1308
|
+
| Field | Type | Meaning |
|
|
1309
|
+
|-------|------|---------|
|
|
1310
|
+
| `kind` | string | PascalCase K8s kind (e.g. `'Organization'`) |
|
|
1311
|
+
| `plural` | string | Lowercase plural used in kubectl (e.g. `'organizations'`) |
|
|
1312
|
+
| `group` | string? | API group. Defaults to `kradle.a5c.ai` when absent. KubeVela uses `core.oam.dev`, core K8s uses `''`. |
|
|
1313
|
+
| `namespaced` | boolean | Whether the resource lives in a namespace (`true`) or is cluster-scoped (`false`) |
|
|
1314
|
+
| `namespace` | string? | Fixed namespace override (e.g. `'kradle-system'` for platform resources, `'vela-system'` for KubeVela defs) |
|
|
1315
|
+
| `storage` | string | Backend store: `'etcd'`, `'postgres'`, `'kubevela'`, `'kyverno'`, `'kyverno-reports'`, or `'core'` |
|
|
1316
|
+
| `platformScoped` | boolean? | When `true`, listed only from the platform namespace — not from per-org namespaces |
|
|
1317
|
+
|
|
1318
|
+
**Resource categories and counts:**
|
|
1319
|
+
|
|
1320
|
+
| Storage | Count | Examples |
|
|
1321
|
+
|---------|-------|----------|
|
|
1322
|
+
| etcd (Kradle CRDs) | 46 | Organization, User, Team, Repository, AgentStack, AgentSubagent, AgentToolProfile, AgentMcpServer, AgentSkill, AgentTriggerRule, AgentContextLabel, KradleWorkspacePolicy, AgentServiceAccount, AgentRoleBinding, AgentSecretGrant, AgentConfigGrant, AgentAdapter, AgentTransportBinding, AgentProviderConfig, KradleProject, AgentGatewayConfig, AgentMemoryRepository, AgentMemorySource, AgentMemoryOntology, AgentMemoryAssociation, ExternalBackendProvider, ExternalBackendBinding, ExternalBackendSyncPolicy |
|
|
1323
|
+
| postgres (aggregated) | 13 | PullRequest, Issue, Review, Pipeline, Job, WebhookDelivery, AgentDispatchRun, AgentDispatchAttempt, AgentSession, AgentContextBundle, KradleArtifact, AgentApproval, KradleWorkspace, AgentTriggerExecution, KradleWorkspaceRuntime, AgentSessionTranscript |
|
|
1324
|
+
| kubevela | 11 | KubeVelaApplication, KubeVelaApplicationRevision, KubeVelaComponentDefinition, KubeVelaWorkloadDefinition, KubeVelaTraitDefinition, KubeVelaScopeDefinition, KubeVelaPolicyDefinition, KubeVelaPolicy, KubeVelaWorkflowStepDefinition, KubeVelaWorkflow, KubeVelaResourceTracker |
|
|
1325
|
+
| kyverno / kyverno-reports | 10 | KyvernoPolicy, KyvernoClusterPolicy, KyvernoValidatingPolicy, KyvernoMutatingPolicy, KyvernoGeneratingPolicy, KyvernoDeletingPolicy, KyvernoImageValidatingPolicy, KyvernoPolicyException, PolicyReport, ClusterPolicyReport |
|
|
1326
|
+
| core (K8s built-in) | 2 | Secret, ConfigMap — excluded from snapshot, accessed on-demand |
|
|
1327
|
+
|
|
1328
|
+
**Platform-scoped definitions** (only listed from `kradle-system`):
|
|
1329
|
+
- `Organization` (namespace: `KRADLE_PLATFORM_NAMESPACE`)
|
|
1330
|
+
- `OrgNamespaceBinding` (namespace: `KRADLE_PLATFORM_NAMESPACE`)
|
|
1331
|
+
|
|
1332
|
+
### 21.2 getControllerSnapshot() — Step-by-Step
|
|
1333
|
+
|
|
1334
|
+
`getControllerSnapshot(options)` is the synchronous entrypoint (uses `spawnSync`) that produces the full cluster state snapshot.
|
|
1335
|
+
|
|
1336
|
+
#### Step 1: currentContextResult()
|
|
1337
|
+
|
|
1338
|
+
```javascript
|
|
1339
|
+
function currentContextResult(options) {
|
|
1340
|
+
const inCluster = inClusterKubectlConfig(options.env);
|
|
1341
|
+
if (inCluster) return { ok: true, stdout: `${inCluster.context}\n`, ... };
|
|
1342
|
+
return runKubectl(['config', 'current-context'], { ...options, allowFailure: true });
|
|
1343
|
+
}
|
|
1344
|
+
```
|
|
1345
|
+
|
|
1346
|
+
Checks for in-cluster mode first (via `KUBERNETES_SERVICE_HOST` + service account files at `/var/run/secrets/kubernetes.io/serviceaccount/`). If found, returns synthetic result with context `'in-cluster'`. Otherwise runs `kubectl config current-context`.
|
|
1347
|
+
|
|
1348
|
+
**Failure mode:** Returns `{ ok: false }` — snapshot proceeds to build a degraded response with `kubectl.available: false`.
|
|
1349
|
+
|
|
1350
|
+
#### Step 2: versionResult
|
|
1351
|
+
|
|
1352
|
+
```javascript
|
|
1353
|
+
runKubectl(['version', '--client=true', '-o', 'json'], { allowFailure: true })
|
|
1354
|
+
```
|
|
1355
|
+
|
|
1356
|
+
Extracts `clientVersion.gitVersion` from JSON output. If both context and version fail, the snapshot is returned early with empty resource maps and `kubectl.available: false`.
|
|
1357
|
+
|
|
1358
|
+
#### Step 3: CRD Discovery Loop
|
|
1359
|
+
|
|
1360
|
+
```javascript
|
|
1361
|
+
const crdResult = runKubectl(['get', 'crd', '-o', 'json'], { allowFailure: true });
|
|
1362
|
+
const discoveredCrds = crdResult.ok
|
|
1363
|
+
? parseKubernetesList(crdResult.stdout).items.filter((crd) =>
|
|
1364
|
+
[KRADLE_API_GROUP, KUBEVELA_API_GROUP].includes(crd.spec?.group) ||
|
|
1365
|
+
KYVERNO_DISCOVERY_GROUPS.has(crd.spec?.group))
|
|
1366
|
+
: [];
|
|
1367
|
+
const discoveredPluralSet = new Set(
|
|
1368
|
+
discoveredCrds.map((crd) => `${crd.spec?.group}/${crd.spec?.names?.plural}`)
|
|
1369
|
+
);
|
|
1370
|
+
```
|
|
1371
|
+
|
|
1372
|
+
Queries ALL cluster CRDs, then filters to only those belonging to `kradle.a5c.ai`, `core.oam.dev`, `kyverno.io`, `policies.kyverno.io`, or `wgpolicyk8s.io`. The resulting `discoveredPluralSet` is used to decide which resources to actually query — avoids 404s for uninstalled CRDs.
|
|
1373
|
+
|
|
1374
|
+
#### Step 4: platformScopedDefinitions vs orgScopedDefinitions
|
|
1375
|
+
|
|
1376
|
+
```javascript
|
|
1377
|
+
const platformScopedDefinitions = snapshotResources.filter((d) => d.platformScoped);
|
|
1378
|
+
const orgScopedDefinitions = snapshotResources.filter((d) => !d.platformScoped);
|
|
1379
|
+
```
|
|
1380
|
+
|
|
1381
|
+
Platform-scoped resources (Organization, OrgNamespaceBinding) are listed first from their fixed namespace. This is required because org namespaces are derived from the Organization/OrgNamespaceBinding resources.
|
|
1382
|
+
|
|
1383
|
+
#### Step 5: organizationNamespaces() — Fallback Chain
|
|
1384
|
+
|
|
1385
|
+
```javascript
|
|
1386
|
+
function organizationNamespaces(organizations, bindings, fallbackNamespace) {
|
|
1387
|
+
// 1. Extract spec.namespaceName from Organization items
|
|
1388
|
+
// 2. Extract spec.namespace from OrgNamespaceBinding items
|
|
1389
|
+
// 3. Deduplicate into a Set
|
|
1390
|
+
// If non-empty → return those namespaces
|
|
1391
|
+
// 4. Fallback: KRADLE_ADMIN_ORG → orgNamespaceName(adminOrg)
|
|
1392
|
+
// 5. Fallback: KRADLE_ORG || 'default' → orgNamespaceName(defaultOrg)
|
|
1393
|
+
// 6. Final fallback: platformNamespace itself
|
|
1394
|
+
}
|
|
1395
|
+
```
|
|
1396
|
+
|
|
1397
|
+
The `orgNamespaceName(org)` function generates `kradle-org-${slug}` from the org slug.
|
|
1398
|
+
|
|
1399
|
+
#### Step 6: Parallel Org-Scoped Resource Listing
|
|
1400
|
+
|
|
1401
|
+
For each org-scoped definition:
|
|
1402
|
+
1. Skip if `shouldListSnapshotDefinition()` returns false (CRD not discovered and not kradle.a5c.ai group)
|
|
1403
|
+
2. Compute target namespaces: fixed namespace → use it alone; otherwise → all org namespaces
|
|
1404
|
+
3. For each namespace, run `kubectl get <plural>.<group> -n <ns> -o json --ignore-not-found`
|
|
1405
|
+
4. Flatten all items into `resources[definition.kind]`
|
|
1406
|
+
|
|
1407
|
+
#### Step 7: Event Collection
|
|
1408
|
+
|
|
1409
|
+
```javascript
|
|
1410
|
+
runKubectl(['get', 'events', '-n', namespace, '-o', 'json', '--ignore-not-found'], { allowFailure: true })
|
|
1411
|
+
```
|
|
1412
|
+
|
|
1413
|
+
Collects Kubernetes events from the platform namespace only (not org namespaces).
|
|
1414
|
+
|
|
1415
|
+
#### Step 8: Permission Matrix (canI Checks)
|
|
1416
|
+
|
|
1417
|
+
```javascript
|
|
1418
|
+
const permissions = await Promise.all(
|
|
1419
|
+
snapshotResources
|
|
1420
|
+
.filter((d) => discoveredPluralSet.has(`${d.group || KRADLE_API_GROUP}/${d.plural}`))
|
|
1421
|
+
.map(async (d) => ({
|
|
1422
|
+
kind: d.kind,
|
|
1423
|
+
plural: d.plural,
|
|
1424
|
+
verbs: Object.fromEntries(
|
|
1425
|
+
['get', 'list', 'watch', 'create', 'update', 'patch', 'delete']
|
|
1426
|
+
.map((verb) => [verb, canI(verb, d, { kubectl, namespace, timeoutMs, env })])
|
|
1427
|
+
)
|
|
1428
|
+
}))
|
|
1429
|
+
);
|
|
1430
|
+
```
|
|
1431
|
+
|
|
1432
|
+
For every discovered CRD, runs `kubectl auth can-i <verb> <plural>.<group> -n <ns>` for all 7 standard verbs. Result is `true`/`false` per verb.
|
|
1433
|
+
|
|
1434
|
+
#### Step 9: Kyverno Discovery
|
|
1435
|
+
|
|
1436
|
+
`discoverKyverno()` is called with the discoveredPluralSet. It:
|
|
1437
|
+
1. Filters `KYVERNO_RESOURCES` to only those with discovered CRDs
|
|
1438
|
+
2. Lists each Kyverno resource from the Kyverno policy namespace
|
|
1439
|
+
3. Lists Kyverno controller deployments from the kyverno namespace
|
|
1440
|
+
4. Runs `canI` checks for Kyverno resources
|
|
1441
|
+
5. Extracts policy reports and violations
|
|
1442
|
+
6. Reports degraded conditions if CRDs exist but controllers are missing
|
|
1443
|
+
|
|
1444
|
+
#### Step 10: Return Shape
|
|
1445
|
+
|
|
1446
|
+
```typescript
|
|
1447
|
+
{
|
|
1448
|
+
source: 'kubernetes',
|
|
1449
|
+
mode: 'kubernetes-api',
|
|
1450
|
+
namespace: string, // Platform namespace
|
|
1451
|
+
generatedAt: string, // ISO timestamp
|
|
1452
|
+
correlationId: string, // UUID for request correlation
|
|
1453
|
+
kubectl: {
|
|
1454
|
+
binary: string, // kubectl path
|
|
1455
|
+
context: string | null, // Current context name
|
|
1456
|
+
clientVersion: string | null, // e.g. 'v1.28.3'
|
|
1457
|
+
available: boolean, // true if both context + version succeeded
|
|
1458
|
+
errors: string[] // Command failure messages
|
|
1459
|
+
},
|
|
1460
|
+
apiService: object | null, // Raw APIService JSON for kradle API
|
|
1461
|
+
crds: object[], // Discovered Kradle/KubeVela/Kyverno CRDs
|
|
1462
|
+
resources: Record<string, object[]>, // Map: kind → items array
|
|
1463
|
+
kyverno: object, // Full Kyverno discovery state
|
|
1464
|
+
events: object[], // K8s events from platform namespace
|
|
1465
|
+
permissions: object[], // Per-kind verb permission map
|
|
1466
|
+
storage: object, // Storage boundary descriptions
|
|
1467
|
+
commands: object[] // Generated kubectl commands per kind
|
|
1468
|
+
}
|
|
1469
|
+
```
|
|
1470
|
+
|
|
1471
|
+
---
|
|
1472
|
+
|
|
1473
|
+
## 22. Controller-UI Model Construction
|
|
1474
|
+
|
|
1475
|
+
> Source: `packages/kradle/core/src/controller-ui.js`
|
|
1476
|
+
|
|
1477
|
+
### 22.1 createControllerUiModel(source, options)
|
|
1478
|
+
|
|
1479
|
+
Transforms a raw Kubernetes snapshot into a UI-ready model consumed by the web console.
|
|
1480
|
+
|
|
1481
|
+
**Parameters:**
|
|
1482
|
+
- `source` — Raw snapshot object (from `getControllerSnapshot()`) or a controller with `.snapshot()` method
|
|
1483
|
+
- `options.organization` / `options.org` — Requested org slug
|
|
1484
|
+
|
|
1485
|
+
**Pipeline:**
|
|
1486
|
+
|
|
1487
|
+
```
|
|
1488
|
+
normalizeSnapshot(source)
|
|
1489
|
+
→ ensureOrganizations(snapshot.resources.Organization)
|
|
1490
|
+
→ resolve activeOrg (by slug match or first)
|
|
1491
|
+
→ filterResourceItemsForOrg() per kind
|
|
1492
|
+
→ assemble domain views (agent, delivery, policy, identity)
|
|
1493
|
+
→ compute metrics
|
|
1494
|
+
→ format events
|
|
1495
|
+
→ build validation checks
|
|
1496
|
+
→ return full model
|
|
1497
|
+
```
|
|
1498
|
+
|
|
1499
|
+
### 22.2 Organization Resolution
|
|
1500
|
+
|
|
1501
|
+
```javascript
|
|
1502
|
+
function ensureOrganizations(organizations, platformNamespace) {
|
|
1503
|
+
if (organizations.length) return organizations.map((org) => ({
|
|
1504
|
+
name: org.metadata?.name,
|
|
1505
|
+
slug: org.spec?.slug || org.metadata?.name,
|
|
1506
|
+
displayName: org.spec?.displayName || slug,
|
|
1507
|
+
namespace: org.spec?.namespaceName || orgNamespaceName(slug),
|
|
1508
|
+
platformNamespace
|
|
1509
|
+
}));
|
|
1510
|
+
// Fallback: synthesize a 'default' org
|
|
1511
|
+
return [{ name: 'default', slug: 'default', displayName: 'Default org',
|
|
1512
|
+
namespace: 'kradle-org-default', platformNamespace }];
|
|
1513
|
+
}
|
|
1514
|
+
```
|
|
1515
|
+
|
|
1516
|
+
Active org is selected by matching `requestedOrg` against slug or name, falling back to `organizations[0]`.
|
|
1517
|
+
|
|
1518
|
+
### 22.3 Resource Filtering by Org
|
|
1519
|
+
|
|
1520
|
+
```javascript
|
|
1521
|
+
function filterResourceItemsForOrg(definition, items, org) {
|
|
1522
|
+
if (definition.kind === 'Organization') → filter by spec.slug match
|
|
1523
|
+
if (definition.kind === 'OrgNamespaceBinding') → filterByOrg (label/ref match)
|
|
1524
|
+
if (definition.namespace && !== orgNamespaceName(org)) → return all (system-level)
|
|
1525
|
+
default → filterByOrg (label/ref match)
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function filterByOrg(items, org) {
|
|
1529
|
+
const orgNamespace = orgNamespaceName(org);
|
|
1530
|
+
return items.filter((item) =>
|
|
1531
|
+
item.spec?.organizationRef === org ||
|
|
1532
|
+
item.metadata?.labels?.['kradle.a5c.ai/org'] === org ||
|
|
1533
|
+
item.metadata?.namespace === orgNamespace
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
```
|
|
1537
|
+
|
|
1538
|
+
### 22.4 Agent View Assembly
|
|
1539
|
+
|
|
1540
|
+
The `agentView` object is constructed from 14+ filtered resource arrays:
|
|
1541
|
+
|
|
1542
|
+
```javascript
|
|
1543
|
+
const agentView = {
|
|
1544
|
+
org: activeOrg?.slug,
|
|
1545
|
+
stacks: { count, items: AgentStack[] },
|
|
1546
|
+
runs: { count, items: AgentDispatchRun[], active: [...non-terminal] },
|
|
1547
|
+
rules: { count, items: AgentTriggerRule[] },
|
|
1548
|
+
sessions: { count, items: AgentSession[] },
|
|
1549
|
+
workspaces: { count, items: KradleWorkspace[] },
|
|
1550
|
+
approvals: { count, items: AgentApproval[], pending: [...phase=Pending] },
|
|
1551
|
+
adapters: { count, items: AgentAdapter[] },
|
|
1552
|
+
providers: { count, items: AgentProviderConfig[] },
|
|
1553
|
+
projects: { count, items: KradleProject[] },
|
|
1554
|
+
gateway: AgentGatewayConfig | null,
|
|
1555
|
+
transcripts: { count, items: AgentSessionTranscript[] },
|
|
1556
|
+
memoryRepositories: { count, items: AgentMemoryRepository[] },
|
|
1557
|
+
memorySnapshots: { count, items: AgentMemorySnapshot[] },
|
|
1558
|
+
memoryImports: { count, items: AgentRunMemoryImport[], pending: [...] }
|
|
1559
|
+
};
|
|
1560
|
+
```
|
|
1561
|
+
|
|
1562
|
+
### 22.5 Delivery View (KubeVela)
|
|
1563
|
+
|
|
1564
|
+
`createDeliveryView()` assembles:
|
|
1565
|
+
- `installed` — boolean (any KubeVela definitions present)
|
|
1566
|
+
- `counts` — applications, releases, components, workloads, traits, scopes, policies, automations, managedResources
|
|
1567
|
+
- `capabilityCatalog` — names of installed component/trait/scope/policy/workflow-step definitions
|
|
1568
|
+
- `applications[]` — enriched with services, workflow status, releases, managed resources, YAML
|
|
1569
|
+
- `runtime` — releases, automations, policies, managedResources summaries
|
|
1570
|
+
|
|
1571
|
+
### 22.6 Policy Engine View
|
|
1572
|
+
|
|
1573
|
+
`createPolicyEngineView()` produces:
|
|
1574
|
+
- `engine: 'kyverno'`, `mode`, `health` (disabled/ready/degraded)
|
|
1575
|
+
- `profiles`, `templates`, `bindings`, `exceptionRequests` — summarized via `policySummary()`
|
|
1576
|
+
- `kyvernoResources` — count per Kyverno kind
|
|
1577
|
+
- `controllers[]` — deployment health (name, ready, replicas)
|
|
1578
|
+
- `reports` — policyReports count, clusterPolicyReports count, results array
|
|
1579
|
+
- `violations[]` — filtered results with fail/error/warn status
|
|
1580
|
+
|
|
1581
|
+
### 22.7 Identity View
|
|
1582
|
+
|
|
1583
|
+
`createIdentityView()` produces a fully-expanded view of:
|
|
1584
|
+
- `counts` — users, teams, pendingInvites, mappings, repositoryGrants, sshKeys
|
|
1585
|
+
- `providers[]` — name, label, type, enabled, phase
|
|
1586
|
+
- `users[]` — with email, teams, admin flag, disabled state
|
|
1587
|
+
- `teams[]` — with members, maintainers, repositoryGrants
|
|
1588
|
+
- `invites[]` — with email, role, teams, phase, expiresAt
|
|
1589
|
+
- `mappings[]` — with provider, subject, workspace/repository identity
|
|
1590
|
+
- `permissions[]` — with repository, subject, permission level, revoked state
|
|
1591
|
+
- `sshKeys[]` — with owner, scope, repository, revoked
|
|
1592
|
+
- `reconciliation` — counts, phases, statuses, nextActions (human-readable intents)
|
|
1593
|
+
|
|
1594
|
+
### 22.8 Metrics
|
|
1595
|
+
|
|
1596
|
+
```javascript
|
|
1597
|
+
metrics: {
|
|
1598
|
+
components, resources, events, auditEntries,
|
|
1599
|
+
users, teams, invites, repositories, pullRequests, issues, projects,
|
|
1600
|
+
pipelines, jobs, runnerPools, webhookDeliveries,
|
|
1601
|
+
policyViolations, policyBindings, deployments, releases,
|
|
1602
|
+
agentStacks, agentRuns, agentSessions,
|
|
1603
|
+
greenChecks, totalChecks
|
|
1604
|
+
}
|
|
1605
|
+
```
|
|
1606
|
+
|
|
1607
|
+
### 22.9 Events Formatting
|
|
1608
|
+
|
|
1609
|
+
Last 8 events are formatted as:
|
|
1610
|
+
```javascript
|
|
1611
|
+
{ type, storage: 'kubernetes', resource: 'Kind/namespace/name', actor, allowed: true, message }
|
|
1612
|
+
```
|
|
1613
|
+
|
|
1614
|
+
---
|
|
1615
|
+
|
|
1616
|
+
## 23. HTTP Server Route Handlers
|
|
1617
|
+
|
|
1618
|
+
> Source: `packages/kradle/core/src/http-server.js`
|
|
1619
|
+
|
|
1620
|
+
### 23.1 Server Factory
|
|
1621
|
+
|
|
1622
|
+
```javascript
|
|
1623
|
+
export function createKradleHttpServer(options) {
|
|
1624
|
+
return createServer(createKradleHttpHandler(options));
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
export function createKradleHttpHandler({ runtime, controller }) {
|
|
1628
|
+
return async function handleKradleRequest(request, response) { ... };
|
|
1629
|
+
}
|
|
1630
|
+
```
|
|
1631
|
+
|
|
1632
|
+
All routes use regex pattern matching against `url.pathname`. JSON responses via `send(response, status, body)` with `content-type: application/json; charset=utf-8`.
|
|
1633
|
+
|
|
1634
|
+
### 23.2 Route Table
|
|
1635
|
+
|
|
1636
|
+
| Method | Pattern | Handler |
|
|
1637
|
+
|--------|---------|---------|
|
|
1638
|
+
| GET | `/healthz` | Returns `{ ok: true, project: 'Kradle' }` |
|
|
1639
|
+
| GET | `/api/controller?org=:org` | Full UI model via `createControllerUiModel(snapshot, { organization })` |
|
|
1640
|
+
| GET/POST | `/api/orgs` | List orgs / create organization |
|
|
1641
|
+
| GET/POST | `/api/orgs/:org/resources` | List resources by kind (query `?kind=`) / apply resource |
|
|
1642
|
+
| GET/DELETE | `/api/orgs/:org/resources/:kind/:name` | Get or delete specific resource |
|
|
1643
|
+
| GET/POST | `/api/orgs/:org/repositories` | List / create repositories |
|
|
1644
|
+
| GET/DELETE | `/api/orgs/:org/repositories/:name` | Get / delete specific repository |
|
|
1645
|
+
| GET/POST | `/api/orgs/:org/snapshot` | Get runtime snapshot / import snapshot |
|
|
1646
|
+
| GET | `/api/orgs/:org/runtime-resources/:kind` | List runtime resources by kind |
|
|
1647
|
+
| POST | `/api/orgs/:org/repositories/:name/objects` | Record git object |
|
|
1648
|
+
| POST | `/api/orgs/:org/repositories/:name/search-index` | Enqueue search index |
|
|
1649
|
+
| POST | `/api/orgs/:org/pullrequests` | Create pull request |
|
|
1650
|
+
| POST | `/api/orgs/:org/pullrequests/:name/reviews` | Add review |
|
|
1651
|
+
| POST | `/api/orgs/:org/pullrequests/:name/checks/complete` | Complete pipeline check |
|
|
1652
|
+
| POST | `/api/orgs/:org/pullrequests/:name/merge` | Merge pull request |
|
|
1653
|
+
| POST | `/api/orgs/:org/agents/approvals/:name/decide` | Approve/deny agent approval |
|
|
1654
|
+
| POST | `/api/orgs/:org/agents/webhooks/ingest` | Webhook ingestion (GitHub/Gitea normalization) |
|
|
1655
|
+
| POST | `/api/orgs/:org/agents/events/pipeline-failure` | Pipeline failure event |
|
|
1656
|
+
| POST | `/api/orgs/:org/agents/events/comment` | Comment event |
|
|
1657
|
+
| POST | `/api/orgs/:org/agents/events/label` | Label event |
|
|
1658
|
+
| POST | `/api/orgs/:org/agents/triggers/process` | Evaluate event against trigger rules |
|
|
1659
|
+
| POST | `/api/orgs/:org/agents/memory/query` | Memory graph+grep search |
|
|
1660
|
+
| GET/POST | `/api/orgs/:org/secrets` | List / create secrets (AgentSecretGrant) |
|
|
1661
|
+
| DELETE | `/api/orgs/:org/secrets/:name` | Delete secret grant |
|
|
1662
|
+
| GET/POST | `/api/orgs/:org/secret-grants` | List / create secret grants |
|
|
1663
|
+
| POST | `/api/orgs/:org/external/sync` | Trigger external sync for binding |
|
|
1664
|
+
| POST | `/api/orgs/:org/external/conflicts/:name/resolve` | Resolve external sync conflict |
|
|
1665
|
+
| POST | `/api/orgs/:org/external/write-intents/:name/approve` | Approve write intent |
|
|
1666
|
+
| POST | `/api/orgs/:org/external/write-intents/:name/cancel` | Cancel write intent |
|
|
1667
|
+
| GET | `/api/orgs/:org/agents/events/stream` | **SSE endpoint** |
|
|
1668
|
+
|
|
1669
|
+
### 23.3 SSE Endpoint Implementation
|
|
1670
|
+
|
|
1671
|
+
```javascript
|
|
1672
|
+
const sseMatch = url.pathname.match(/^\/api\/orgs\/([^/]+)\/agents\/events\/stream$/);
|
|
1673
|
+
if (request.method === 'GET' && sseMatch) {
|
|
1674
|
+
response.writeHead(200, {
|
|
1675
|
+
'Content-Type': 'text/event-stream',
|
|
1676
|
+
'Cache-Control': 'no-cache',
|
|
1677
|
+
'Connection': 'keep-alive',
|
|
1678
|
+
'X-Accel-Buffering': 'no', // Disable nginx buffering
|
|
1679
|
+
});
|
|
1680
|
+
response.write('data: {"type":"connected"}\n\n');
|
|
1681
|
+
|
|
1682
|
+
const writer = (event) => {
|
|
1683
|
+
response.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
1684
|
+
};
|
|
1685
|
+
globalEventBus.subscribe(writer);
|
|
1686
|
+
|
|
1687
|
+
const interval = setInterval(() => {
|
|
1688
|
+
response.write('data: {"type":"heartbeat"}\n\n');
|
|
1689
|
+
}, 30000); // 30-second heartbeat
|
|
1690
|
+
|
|
1691
|
+
request.on('close', () => {
|
|
1692
|
+
clearInterval(interval);
|
|
1693
|
+
globalEventBus.unsubscribe(writer);
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
```
|
|
1697
|
+
|
|
1698
|
+
**Key behaviors:**
|
|
1699
|
+
- Sends `{"type":"connected"}` immediately on connection
|
|
1700
|
+
- Subscribes a writer function to `globalEventBus`
|
|
1701
|
+
- Sends heartbeat every 30 seconds to keep connection alive through proxies
|
|
1702
|
+
- On client disconnect: clears interval and unsubscribes from event bus
|
|
1703
|
+
- No CORS headers (handled by proxy or web framework)
|
|
1704
|
+
|
|
1705
|
+
### 23.4 Webhook Event Normalization
|
|
1706
|
+
|
|
1707
|
+
`normalizeWebhookEvent(body, org)` maps raw GitHub/Gitea payloads:
|
|
1708
|
+
|
|
1709
|
+
| Condition | Normalized Type |
|
|
1710
|
+
|-----------|-----------------|
|
|
1711
|
+
| `action='completed'` + `workflow_run.conclusion='failure'` | `ci-failure` |
|
|
1712
|
+
| `action='opened'` + `pull_request` present | `pr-opened` |
|
|
1713
|
+
| `action='created'` + `comment` present | `comment` |
|
|
1714
|
+
| `action='labeled'` | `label-added` |
|
|
1715
|
+
| `action='opened'` + `issue` (no PR) | `issue-created` |
|
|
1716
|
+
| `ref` + `commits` present | `push` |
|
|
1717
|
+
| fallback | `webhook` |
|
|
1718
|
+
|
|
1719
|
+
### 23.5 Error Handling
|
|
1720
|
+
|
|
1721
|
+
All routes are wrapped in a try/catch. Unhandled errors return:
|
|
1722
|
+
```json
|
|
1723
|
+
{ "error": "bad_request", "message": "<error.message>" }
|
|
1724
|
+
```
|
|
1725
|
+
with status 400. Unmatched routes return 404:
|
|
1726
|
+
```json
|
|
1727
|
+
{ "error": "not_found", "method": "GET", "path": "/unknown" }
|
|
1728
|
+
```
|
|
1729
|
+
|
|
1730
|
+
---
|
|
1731
|
+
|
|
1732
|
+
## 24. Async Snapshot Architecture
|
|
1733
|
+
|
|
1734
|
+
> Source: `packages/kradle/core/src/kubernetes-controller-async.js`
|
|
1735
|
+
|
|
1736
|
+
### 24.1 runKubectlAsync(args, options)
|
|
1737
|
+
|
|
1738
|
+
Promise-based kubectl wrapper using `child_process.spawn`. Returns the same shape as the sync `runKubectl`:
|
|
1739
|
+
|
|
1740
|
+
```typescript
|
|
1741
|
+
{
|
|
1742
|
+
ok: boolean,
|
|
1743
|
+
status: number | null,
|
|
1744
|
+
signal: string | null,
|
|
1745
|
+
stdout: string,
|
|
1746
|
+
stderr: string,
|
|
1747
|
+
error: string | null,
|
|
1748
|
+
command: string // Reconstructed command string for diagnostics
|
|
1749
|
+
}
|
|
1750
|
+
```
|
|
1751
|
+
|
|
1752
|
+
**Timeout handling:**
|
|
1753
|
+
- Timer fires after `timeoutMs` (default 3000ms from `KRADLE_KUBECTL_TIMEOUT_MS`)
|
|
1754
|
+
- Sends SIGTERM to the child process
|
|
1755
|
+
- If `allowFailure: true` → resolves with `{ ok: false, error: 'kubectl timed out...' }`
|
|
1756
|
+
- If `allowFailure: false` → rejects with Error
|
|
1757
|
+
|
|
1758
|
+
**stdin:** If `options.input` is provided, writes to child stdin then closes it.
|
|
1759
|
+
|
|
1760
|
+
### 24.2 getControllerSnapshotAsync(options) — Parallel Execution Strategy
|
|
1761
|
+
|
|
1762
|
+
Three-phase parallel execution:
|
|
1763
|
+
|
|
1764
|
+
**Phase 1:** Context + version in parallel
|
|
1765
|
+
```javascript
|
|
1766
|
+
const [contextResult, versionResult] = await Promise.all([
|
|
1767
|
+
inClusterContext || runKubectlAsync(['config', 'current-context'], ...),
|
|
1768
|
+
runKubectlAsync(['version', '--client=true', '-o', 'json'], ...)
|
|
1769
|
+
]);
|
|
1770
|
+
```
|
|
1771
|
+
|
|
1772
|
+
**Phase 2:** API service + CRD discovery in parallel
|
|
1773
|
+
```javascript
|
|
1774
|
+
const [apiServiceResult, crdResult] = await Promise.all([
|
|
1775
|
+
runKubectlAsync(['get', 'apiservice', KRADLE_API_VERSIONED_GROUP, ...]),
|
|
1776
|
+
runKubectlAsync(['get', 'crd', '-o', 'json'], ...)
|
|
1777
|
+
]);
|
|
1778
|
+
```
|
|
1779
|
+
|
|
1780
|
+
**Phase 3:** All resource kinds in parallel
|
|
1781
|
+
```javascript
|
|
1782
|
+
// First: platform-scoped (to discover org namespaces)
|
|
1783
|
+
const platformResults = await Promise.all(
|
|
1784
|
+
platformScopedDefs.map((definition) => runKubectlAsync([...]))
|
|
1785
|
+
);
|
|
1786
|
+
// Derive org namespaces from results
|
|
1787
|
+
const orgNamespaces = resolveOrgNamespaces(resources.Organization, ...);
|
|
1788
|
+
|
|
1789
|
+
// Then: all org-scoped resources in parallel (each definition across all namespaces)
|
|
1790
|
+
const orgResults = await Promise.all(
|
|
1791
|
+
orgScopedDefs.map(async (definition) => {
|
|
1792
|
+
const itemArrays = await Promise.all(
|
|
1793
|
+
namespaces.map((ns) => runKubectlAsync([...]))
|
|
1794
|
+
);
|
|
1795
|
+
return { definition, items: itemArrays.flat() };
|
|
1796
|
+
})
|
|
1797
|
+
);
|
|
1798
|
+
```
|
|
1799
|
+
|
|
1800
|
+
**Error fallback:** On any unexpected error, imports and falls back to the synchronous `getControllerSnapshot()`:
|
|
1801
|
+
```javascript
|
|
1802
|
+
catch (error) {
|
|
1803
|
+
const { getControllerSnapshot } = await import('./kubernetes-controller.js');
|
|
1804
|
+
return getControllerSnapshot(options);
|
|
1805
|
+
}
|
|
1806
|
+
```
|
|
1807
|
+
|
|
1808
|
+
### 24.3 getPartialSnapshot(kinds, options)
|
|
1809
|
+
|
|
1810
|
+
Fetches only the requested resource kinds. Used by pages that need a subset (e.g. only `AgentStack` + `AgentSession`).
|
|
1811
|
+
|
|
1812
|
+
```javascript
|
|
1813
|
+
export async function getPartialSnapshot(kinds = [], options = {}) {
|
|
1814
|
+
// 1. Resolve each kind string to a KRADLE_RESOURCES definition (skip unknown)
|
|
1815
|
+
// 2. If any org-scoped kind is needed, pre-fetch Organization + OrgNamespaceBinding
|
|
1816
|
+
// to compute orgNamespaces
|
|
1817
|
+
// 3. Fetch all requested definitions in parallel (across all applicable namespaces)
|
|
1818
|
+
// 4. Return { source: 'kubernetes', mode: 'partial', namespace, generatedAt, resources }
|
|
1819
|
+
}
|
|
1820
|
+
```
|
|
1821
|
+
|
|
1822
|
+
Return shape is minimal — no kubectl metadata, no permissions, no events. Just `resources: Record<kind, items[]>`.
|
|
1823
|
+
|
|
1824
|
+
### 24.4 watchResourceChanges(callback, options)
|
|
1825
|
+
|
|
1826
|
+
Lightweight watch that invalidates the snapshot cache on any change.
|
|
1827
|
+
|
|
1828
|
+
```javascript
|
|
1829
|
+
export function watchResourceChanges(callback, options = {}) {
|
|
1830
|
+
const watchKinds = options.kinds || ['Organization', 'AgentStack', 'AgentSession'];
|
|
1831
|
+
const children = []; // Array of spawned child processes
|
|
1832
|
+
|
|
1833
|
+
for (const kind of watchKinds) {
|
|
1834
|
+
const child = spawn(kubectl, [...args, '--watch', '-o', 'json'], ...);
|
|
1835
|
+
let buffer = '';
|
|
1836
|
+
child.stdout.on('data', (chunk) => {
|
|
1837
|
+
buffer += chunk.toString();
|
|
1838
|
+
// Parse newline-delimited JSON objects
|
|
1839
|
+
while ((newlineIdx = buffer.indexOf('\n')) !== -1) {
|
|
1840
|
+
const item = safeJson(line);
|
|
1841
|
+
if (item) {
|
|
1842
|
+
clearSnapshotCache(); // Invalidate ALL cached snapshots
|
|
1843
|
+
callback(kind, item); // User callback (errors swallowed)
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
});
|
|
1847
|
+
children.push(child);
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
return { stop() { children.forEach(c => c.kill('SIGTERM')); } };
|
|
1851
|
+
}
|
|
1852
|
+
```
|
|
1853
|
+
|
|
1854
|
+
**Key behaviors:**
|
|
1855
|
+
- Default watched kinds: Organization, AgentStack, AgentSession
|
|
1856
|
+
- Uses `--watch -o json` for streaming JSON from kubectl
|
|
1857
|
+
- Parses newline-delimited JSON (not JSON array)
|
|
1858
|
+
- On any valid object: calls `clearSnapshotCache()` (see §25)
|
|
1859
|
+
- Returns `{ stop }` cleanup handle for graceful shutdown
|
|
1860
|
+
|
|
1861
|
+
### 24.5 Differences from Sync Version
|
|
1862
|
+
|
|
1863
|
+
| Aspect | Sync (`getControllerSnapshot`) | Async (`getControllerSnapshotAsync`) |
|
|
1864
|
+
|--------|------|------|
|
|
1865
|
+
| Process execution | `spawnSync` | `spawn` + Promise |
|
|
1866
|
+
| Resource listing | Sequential loop | `Promise.all` parallel |
|
|
1867
|
+
| Permission checks | Inline `canI` per resource | Skipped (returns `[]`) |
|
|
1868
|
+
| Kyverno discovery | Full `discoverKyverno()` | Returns `emptyKyverno()` stub |
|
|
1869
|
+
| Error recovery | Throws or returns degraded | Falls back to sync version |
|
|
1870
|
+
| Event collection | Included | Included (async) |
|
|
1871
|
+
|
|
1872
|
+
---
|
|
1873
|
+
|
|
1874
|
+
## 25. Snapshot Cache Architecture
|
|
1875
|
+
|
|
1876
|
+
> Source: `packages/kradle/core/src/snapshot-cache.js`
|
|
1877
|
+
|
|
1878
|
+
### 25.1 Data Structures
|
|
1879
|
+
|
|
1880
|
+
```javascript
|
|
1881
|
+
// Per-org cache: Map<string, CacheEntry>
|
|
1882
|
+
const orgCacheMap = new Map();
|
|
1883
|
+
|
|
1884
|
+
// CacheEntry shape:
|
|
1885
|
+
{ data: object, timestamp: number, revalidating: boolean }
|
|
1886
|
+
|
|
1887
|
+
// Legacy single-org cache (backward compatibility with controller-client.js):
|
|
1888
|
+
let snapshotCache = { data: null, timestamp: 0, org: null };
|
|
1889
|
+
```
|
|
1890
|
+
|
|
1891
|
+
### 25.2 Constants
|
|
1892
|
+
|
|
1893
|
+
```javascript
|
|
1894
|
+
export const CACHE_TTL_MS = Number(process.env.KRADLE_SNAPSHOT_CACHE_TTL_MS || 30_000);
|
|
1895
|
+
```
|
|
1896
|
+
|
|
1897
|
+
Default: 30 seconds. Configurable via environment.
|
|
1898
|
+
|
|
1899
|
+
### 25.3 staleWhileRevalidate(org, revalidateFn, swrOptions)
|
|
1900
|
+
|
|
1901
|
+
Full algorithm:
|
|
1902
|
+
|
|
1903
|
+
```javascript
|
|
1904
|
+
export async function staleWhileRevalidate(org, revalidateFn, swrOptions = {}) {
|
|
1905
|
+
const ttlMs = swrOptions.ttlMs ?? CACHE_TTL_MS; // Fresh window (default 30s)
|
|
1906
|
+
const staleMs = swrOptions.staleMs ?? ttlMs * 5; // Max staleness (default 150s)
|
|
1907
|
+
const entry = orgCacheMap.get(org ?? '');
|
|
1908
|
+
const now = Date.now();
|
|
1909
|
+
|
|
1910
|
+
const isFresh = entry?.data && (now - entry.timestamp) < ttlMs;
|
|
1911
|
+
const isStale = entry?.data && (now - entry.timestamp) < staleMs;
|
|
1912
|
+
|
|
1913
|
+
// Case 1: Fresh — return immediately, no revalidation
|
|
1914
|
+
if (isFresh) return entry.data;
|
|
1915
|
+
|
|
1916
|
+
// Case 2: Stale + not already revalidating — return stale, background refresh
|
|
1917
|
+
if (isStale && !entry.revalidating) {
|
|
1918
|
+
orgCacheMap.set(key, { ...entry, revalidating: true });
|
|
1919
|
+
Promise.resolve().then(async () => {
|
|
1920
|
+
try {
|
|
1921
|
+
const fresh = await revalidateFn();
|
|
1922
|
+
setOrgCache(fresh, org); // Updates both orgCacheMap and legacy cache
|
|
1923
|
+
} catch {
|
|
1924
|
+
// Clear revalidating flag so future requests can retry
|
|
1925
|
+
orgCacheMap.set(key, { ...current, revalidating: false });
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
return entry.data; // Return stale immediately
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
// Case 3: Stale + already revalidating — return stale (another caller is refreshing)
|
|
1932
|
+
if (isStale && entry.revalidating) return entry.data;
|
|
1933
|
+
|
|
1934
|
+
// Case 4: No usable cache — block on revalidation
|
|
1935
|
+
const fresh = await revalidateFn();
|
|
1936
|
+
setOrgCache(fresh, org);
|
|
1937
|
+
return fresh;
|
|
1938
|
+
}
|
|
1939
|
+
```
|
|
1940
|
+
|
|
1941
|
+
### 25.4 Cache API Summary
|
|
1942
|
+
|
|
1943
|
+
| Function | Behavior |
|
|
1944
|
+
|----------|----------|
|
|
1945
|
+
| `getOrgCache(org)` | Returns `CacheEntry` or `null` |
|
|
1946
|
+
| `setOrgCache(data, org)` | Stores entry with `Date.now()` timestamp, clears `revalidating` |
|
|
1947
|
+
| `clearOrgCache(org)` | Removes single org entry; clears legacy if matching |
|
|
1948
|
+
| `clearSnapshotCache()` | Clears ALL orgs + legacy cache |
|
|
1949
|
+
| `isCacheFresh(org, ttlMs?)` | `(Date.now() - entry.timestamp) < ttlMs` |
|
|
1950
|
+
| `cachedOrgs()` | Returns `[...orgCacheMap.keys()]` for introspection |
|
|
1951
|
+
| `setSnapshotCache(data, org)` | Legacy API: updates both stores |
|
|
1952
|
+
| `getSnapshotCache()` | Legacy API: returns `{ data, timestamp, org }` |
|
|
1953
|
+
|
|
1954
|
+
### 25.5 Background Revalidation
|
|
1955
|
+
|
|
1956
|
+
The revalidation promise is fire-and-forget (`Promise.resolve().then(async () => {...})`). On error:
|
|
1957
|
+
- The `revalidating` flag is cleared so the next caller can try again
|
|
1958
|
+
- The stale data remains available until `staleMs` expires
|
|
1959
|
+
- No retries — the next request triggers a new revalidation attempt
|
|
1960
|
+
|
|
1961
|
+
---
|
|
1962
|
+
|
|
1963
|
+
## 26. Auth System Deep Dive
|
|
1964
|
+
|
|
1965
|
+
> Source: `packages/kradle/core/src/auth.js`
|
|
1966
|
+
|
|
1967
|
+
### 26.1 createAuthProviderConfig(env)
|
|
1968
|
+
|
|
1969
|
+
Parses environment variables into a provider configuration object:
|
|
1970
|
+
|
|
1971
|
+
```javascript
|
|
1972
|
+
return {
|
|
1973
|
+
session: { cookieName: env.KRADLE_AUTH_COOKIE_NAME || 'kradle_session' },
|
|
1974
|
+
delegatedIdentity: {
|
|
1975
|
+
enabled: env.KRADLE_AUTH_DELEGATED_IDENTITY_ENABLED === 'true',
|
|
1976
|
+
userHeader: env.KRADLE_AUTH_DELEGATED_USER_HEADER || 'x-forwarded-user',
|
|
1977
|
+
groupsHeader: env.KRADLE_AUTH_DELEGATED_GROUPS_HEADER || 'x-forwarded-groups',
|
|
1978
|
+
emailHeader: env.KRADLE_AUTH_DELEGATED_EMAIL_HEADER || 'x-forwarded-email',
|
|
1979
|
+
localDevelopment: {
|
|
1980
|
+
enabled: delegatedLocalDevelopmentEnabled(env), // true unless NODE_ENV=production
|
|
1981
|
+
user: env.KRADLE_AUTH_DELEGATED_LOCAL_USER || 'local-developer',
|
|
1982
|
+
email: env.KRADLE_AUTH_DELEGATED_LOCAL_EMAIL || '',
|
|
1983
|
+
groups: env.KRADLE_AUTH_DELEGATED_LOCAL_GROUPS || 'kradle:repo-admins'
|
|
1984
|
+
}
|
|
1985
|
+
},
|
|
1986
|
+
providers: {
|
|
1987
|
+
github: { id: 'github', label: 'GitHub', type: 'github', enabled, clientId, clientSecret, authorizationUrl, tokenUrl, userInfoUrl, scopes },
|
|
1988
|
+
sso: { id: 'sso', label: '<configurable>', type: 'oidc', enabled, issuerUrl, clientId, clientSecret, authorizationUrl, tokenUrl, userInfoUrl, scopes }
|
|
1989
|
+
}
|
|
1990
|
+
};
|
|
1991
|
+
```
|
|
1992
|
+
|
|
1993
|
+
**Provider enablement:**
|
|
1994
|
+
- GitHub: enabled unless `KRADLE_AUTH_GITHUB_ENABLED=false`
|
|
1995
|
+
- SSO: enabled only when `KRADLE_AUTH_SSO_ENABLED=true`
|
|
1996
|
+
|
|
1997
|
+
### 26.2 listEnabledAuthProviders(config)
|
|
1998
|
+
|
|
1999
|
+
```javascript
|
|
2000
|
+
Object.values(config.providers).filter((p) => p.enabled && p.clientId && p.authorizationUrl)
|
|
2001
|
+
```
|
|
2002
|
+
|
|
2003
|
+
Returns only providers that are both enabled AND have credentials configured.
|
|
2004
|
+
|
|
2005
|
+
### 26.3 buildAuthorizationRedirect({ provider, requestUrl, state })
|
|
2006
|
+
|
|
2007
|
+
1. Validates provider is enabled with clientId and authorizationUrl
|
|
2008
|
+
2. Constructs `redirectUri` = `${protocol}://${host}/api/auth/callback/${provider.id}`
|
|
2009
|
+
3. Builds authorization URL with query params: `response_type=code`, `client_id`, `redirect_uri`, `scope`, `state`
|
|
2010
|
+
4. Returns `{ url, state, redirectUri }`
|
|
2011
|
+
|
|
2012
|
+
State token generation: `Math.random().toString(36).slice(2) + Date.now().toString(36)`
|
|
2013
|
+
|
|
2014
|
+
### 26.4 exchangeOAuthCodeForProfile({ provider, code, requestUrl, fetchImpl })
|
|
2015
|
+
|
|
2016
|
+
1. POSTs to `provider.tokenUrl` with `grant_type=authorization_code`, `code`, `redirect_uri`, `client_id`, `client_secret`
|
|
2017
|
+
2. Extracts `access_token` from JSON response
|
|
2018
|
+
3. GETs `provider.userInfoUrl` with `Authorization: Bearer <token>`
|
|
2019
|
+
4. Normalizes profile via `normalizeProviderProfile(provider, profile)`
|
|
2020
|
+
|
|
2021
|
+
**Profile normalization:**
|
|
2022
|
+
- GitHub: extracts `login`, `id`, `email`, `name`; groups = `[]`
|
|
2023
|
+
- OIDC/SSO: extracts `sub`, `email`, `preferred_username`, `groups` (comma-split if string)
|
|
2024
|
+
- Admin detection: groups include `kradle:platform-engineers` or `kradle:repo-admins`
|
|
2025
|
+
|
|
2026
|
+
### 26.5 registerLoginProfile({ controller, namespace, profile })
|
|
2027
|
+
|
|
2028
|
+
1. Determines org from `KRADLE_ADMIN_ORG || KRADLE_ORG || 'default'`
|
|
2029
|
+
2. Detects bootstrap admin: compares profile username/email against `KRADLE_ADMIN_USERNAME`
|
|
2030
|
+
3. Calls `mapLoginProfileToKradleIdentity()` to produce User + IdentityMapping resources
|
|
2031
|
+
4. Applies both via `controller.applyResource()`
|
|
2032
|
+
5. Returns `{ identity, user, mapping, userResult, mappingResult }`
|
|
2033
|
+
|
|
2034
|
+
### 26.6 createSessionCookie(config, profile, options)
|
|
2035
|
+
|
|
2036
|
+
```javascript
|
|
2037
|
+
// 1. Build JSON payload
|
|
2038
|
+
const payload = Buffer.from(JSON.stringify({
|
|
2039
|
+
provider: profile.provider,
|
|
2040
|
+
subject: profile.subject,
|
|
2041
|
+
user: profile.username || profile.email
|
|
2042
|
+
})).toString('base64url');
|
|
2043
|
+
|
|
2044
|
+
// 2. Sign if KRADLE_SESSION_SECRET is set
|
|
2045
|
+
if (secret) {
|
|
2046
|
+
const signature = createHmac('sha256', secret).update(payload).digest('base64url');
|
|
2047
|
+
value = `${payload}.${signature}`;
|
|
2048
|
+
} else {
|
|
2049
|
+
value = payload; // Unsigned (development mode)
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
// 3. Return Set-Cookie header value
|
|
2053
|
+
return `${cookieName}=${value}; Path=/; HttpOnly; SameSite=Lax`;
|
|
2054
|
+
```
|
|
2055
|
+
|
|
2056
|
+
### 26.7 parseSessionCookie(config, cookieValue, options)
|
|
2057
|
+
|
|
2058
|
+
```javascript
|
|
2059
|
+
// 1. Detect signature presence (indexOf('.'))
|
|
2060
|
+
// 2. Reject: signed cookie + no secret, or unsigned cookie + secret configured
|
|
2061
|
+
// 3. If signed: extract payload + signature, verify with HMAC-SHA256 + timingSafeEqual
|
|
2062
|
+
// 4. Decode: JSON.parse(Buffer.from(payload, 'base64url'))
|
|
2063
|
+
// 5. Return { cookieName, provider, subject, user } or null on any failure
|
|
2064
|
+
```
|
|
2065
|
+
|
|
2066
|
+
**Security properties:**
|
|
2067
|
+
- Constant-time comparison via `timingSafeEqual`
|
|
2068
|
+
- Rejects mismatched length buffers before comparison
|
|
2069
|
+
- Silent failure (returns null) — no error messages leaked
|
|
2070
|
+
|
|
2071
|
+
### 26.8 mapLoginProfileToKradleIdentity(profile)
|
|
2072
|
+
|
|
2073
|
+
Creates two Kradle CRD resources:
|
|
2074
|
+
|
|
2075
|
+
**User resource:**
|
|
2076
|
+
```javascript
|
|
2077
|
+
createResource('User', { name: userName, namespace, labels: { role } }, {
|
|
2078
|
+
organizationRef, displayName, email, username, teams, admin, disabled: false
|
|
2079
|
+
}, { phase: 'Active', lastLoginProvider, groups })
|
|
2080
|
+
```
|
|
2081
|
+
|
|
2082
|
+
**IdentityMapping resource:**
|
|
2083
|
+
```javascript
|
|
2084
|
+
createResource('IdentityMapping', { name: `${provider}-${userName}`, namespace }, {
|
|
2085
|
+
organizationRef, user, provider, subject, email,
|
|
2086
|
+
workspaceIdentity: { name, uid, groups }, // From mapOidcIdentity()
|
|
2087
|
+
repositoryIdentity: { username, email }
|
|
2088
|
+
}, { phase: 'Synced' })
|
|
2089
|
+
```
|
|
2090
|
+
|
|
2091
|
+
### 26.9 profileFromDelegatedHeaders(headers, config, options)
|
|
2092
|
+
|
|
2093
|
+
For reverse-proxy authentication (e.g. OAuth2 Proxy, Authelia):
|
|
2094
|
+
1. Reads user from configured header (`x-forwarded-user` by default)
|
|
2095
|
+
2. Falls back to local development profile if no header and localhost request
|
|
2096
|
+
3. Reads email from email header
|
|
2097
|
+
4. Reads groups from groups header (comma-separated string → array)
|
|
2098
|
+
5. Admin detection: same group check as OAuth flow
|
|
2099
|
+
6. Returns profile with `delegatedIdentitySource: 'proxy-header' | 'local-development'`
|
|
2100
|
+
|
|
2101
|
+
### 26.10 normalizeName(value)
|
|
2102
|
+
|
|
2103
|
+
```javascript
|
|
2104
|
+
String(value || 'user').toLowerCase()
|
|
2105
|
+
.replace(/[^a-z0-9-]+/g, '-') // Non-alphanumeric → dash
|
|
2106
|
+
.replace(/^-+|-+$/g, '') // Trim leading/trailing dashes
|
|
2107
|
+
.slice(0, 63) // K8s name length limit
|
|
2108
|
+
|| 'user' // Fallback if empty after normalization
|
|
2109
|
+
```
|
|
2110
|
+
|
|
2111
|
+
### 26.11 KRADLE_SESSION_SECRET Flow
|
|
2112
|
+
|
|
2113
|
+
| Scenario | Cookie Format | Verification |
|
|
2114
|
+
|----------|---------------|--------------|
|
|
2115
|
+
| No secret (dev) | `base64url(json)` | Accepts any base64url payload |
|
|
2116
|
+
| Secret set (prod) | `base64url(json).hmac_sha256_base64url` | Rejects unsigned, verifies HMAC |
|
|
2117
|
+
| Signed cookie + no secret | — | Rejected (returns null) |
|
|
2118
|
+
| Unsigned cookie + secret | — | Rejected (returns null) |
|
|
2119
|
+
|
|
2120
|
+
---
|
|
2121
|
+
|
|
2122
|
+
## 27. Event Bus Architecture
|
|
2123
|
+
|
|
2124
|
+
> Source: `packages/kradle/core/src/event-bus.js`
|
|
2125
|
+
|
|
2126
|
+
### 27.1 createEventBus() — Factory
|
|
2127
|
+
|
|
2128
|
+
```javascript
|
|
2129
|
+
export function createEventBus() {
|
|
2130
|
+
const listeners = new Set();
|
|
2131
|
+
return { subscribe(fn), unsubscribe(fn), emit(event), emitResourceChange(kind, name, operation) };
|
|
2132
|
+
}
|
|
2133
|
+
```
|
|
2134
|
+
|
|
2135
|
+
### 27.2 globalEventBus — Module-Level Singleton
|
|
2136
|
+
|
|
2137
|
+
```javascript
|
|
2138
|
+
export const globalEventBus = createEventBus();
|
|
2139
|
+
```
|
|
2140
|
+
|
|
2141
|
+
Shared across the entire Node.js process. Imported by:
|
|
2142
|
+
- `http-server.js` — SSE endpoint subscribes writer functions
|
|
2143
|
+
- `api-controller.js` — emits after `applyResource()` / `deleteResource()`
|
|
2144
|
+
|
|
2145
|
+
### 27.3 Listener Management
|
|
2146
|
+
|
|
2147
|
+
- `subscribe(fn)` — adds to `Set<Function>` (deduplication via reference equality)
|
|
2148
|
+
- `unsubscribe(fn)` — removes from Set
|
|
2149
|
+
|
|
2150
|
+
### 27.4 emit(event) — Synchronous Broadcast
|
|
2151
|
+
|
|
2152
|
+
```javascript
|
|
2153
|
+
emit(event) {
|
|
2154
|
+
for (const fn of listeners) {
|
|
2155
|
+
fn(event); // Synchronous invocation — no error boundary
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
```
|
|
2159
|
+
|
|
2160
|
+
All subscribers are called synchronously in iteration order. A throwing subscriber would propagate to the emitter.
|
|
2161
|
+
|
|
2162
|
+
### 27.5 emitResourceChange(kind, name, operation)
|
|
2163
|
+
|
|
2164
|
+
Convenience wrapper producing structured events:
|
|
2165
|
+
|
|
2166
|
+
```javascript
|
|
2167
|
+
{
|
|
2168
|
+
type: 'resource-change',
|
|
2169
|
+
kind: string, // e.g. 'Repository', 'AgentDispatchRun'
|
|
2170
|
+
name: string, // Resource metadata.name
|
|
2171
|
+
operation: string, // 'apply' | 'delete'
|
|
2172
|
+
timestamp: string // ISO 8601
|
|
2173
|
+
}
|
|
2174
|
+
```
|
|
2175
|
+
|
|
2176
|
+
### 27.6 Integration Flow
|
|
2177
|
+
|
|
2178
|
+
```
|
|
2179
|
+
api-controller.applyResource()
|
|
2180
|
+
→ globalEventBus.emitResourceChange('Repository', 'my-repo', 'apply')
|
|
2181
|
+
→ SSE writer in http-server.js
|
|
2182
|
+
→ response.write('data: {"type":"resource-change",...}\n\n')
|
|
2183
|
+
→ Browser EventSource receives event
|
|
2184
|
+
```
|
|
2185
|
+
|
|
2186
|
+
### 27.7 Memory Model
|
|
2187
|
+
|
|
2188
|
+
- `listeners` is a `Set` — no persistence, no durability
|
|
2189
|
+
- Events are fire-and-forget — if no subscribers exist, events are dropped
|
|
2190
|
+
- No event replay or history — late subscribers miss past events
|
|
2191
|
+
- No backpressure — slow subscribers block the emit loop
|
|
2192
|
+
|
|
2193
|
+
---
|
|
2194
|
+
|
|
2195
|
+
## 28. Gitea Service Layer
|
|
2196
|
+
|
|
2197
|
+
> Source: `packages/kradle/core/src/gitea-service.js`, `packages/kradle/core/src/gitea-backend.js`
|
|
2198
|
+
|
|
2199
|
+
### 28.1 createGiteaService(options) — High-Level Service
|
|
2200
|
+
|
|
2201
|
+
```javascript
|
|
2202
|
+
export function createGiteaService(options = {}) {
|
|
2203
|
+
const giteaUrl = options.giteaUrl || process.env.KRADLE_GITEA_HTTP_URL;
|
|
2204
|
+
if (!giteaUrl) return null; // Callers must check and fall back to mock
|
|
2205
|
+
const backend = createGiteaBackend({ baseUrl: giteaUrl, token, fetchImpl });
|
|
2206
|
+
return { available: true, baseUrl, listTree, getBlob, listBranches, getFileContent, createRepository };
|
|
2207
|
+
}
|
|
2208
|
+
```
|
|
2209
|
+
|
|
2210
|
+
**Returns `null`** when `KRADLE_GITEA_HTTP_URL` is not set — this is the availability check. Web routes that need tree/blob data try Gitea first, then fall back to mock responses.
|
|
2211
|
+
|
|
2212
|
+
### 28.2 Service Methods
|
|
2213
|
+
|
|
2214
|
+
| Method | Gitea API Endpoint | Returns |
|
|
2215
|
+
|--------|-------------------|---------|
|
|
2216
|
+
| `listTree(org, repo, ref, path)` | `GET /api/v1/repos/{owner}/{repo}/contents/{path}?ref={ref}` | `[{ path, type: 'blob'|'tree', size, sha, name }]` or `null` (404) |
|
|
2217
|
+
| `getBlob(org, repo, ref, path)` | `GET /api/v1/repos/{owner}/{repo}/raw/{path}?ref={ref}` | Raw text content or `null` (404) |
|
|
2218
|
+
| `listBranches(org, repo)` | `GET /api/v1/repos/{owner}/{repo}/branches` | `[{ name, sha, protected }]` or `null` |
|
|
2219
|
+
| `getFileContent(org, repo, ref, path)` | `GET /api/v1/repos/{owner}/{repo}/contents/{path}?ref={ref}` | `{ path, content, size, sha, encoding, lastCommit }` or `null` |
|
|
2220
|
+
| `createRepository(org, name, opts)` | Delegates to `backend.createRepository()` | Gitea API response |
|
|
2221
|
+
|
|
2222
|
+
**Error handling:**
|
|
2223
|
+
- 404 → returns `null` (graceful degradation)
|
|
2224
|
+
- Other non-OK status → throws `Error('Gitea GET <path> failed with <status>')`
|
|
2225
|
+
|
|
2226
|
+
### 28.3 createGiteaBackend(options) — Low-Level HTTP Client
|
|
2227
|
+
|
|
2228
|
+
```javascript
|
|
2229
|
+
export function createGiteaBackend({ baseUrl, token, fetchImpl }) {
|
|
2230
|
+
async function request(method, path, body) {
|
|
2231
|
+
const response = await fetchImpl(`${root}/api/v1${path}`, { method, headers: {...}, body? });
|
|
2232
|
+
if (!response.ok) throw new Error(`Gitea ${method} ${path} failed with ${response.status}`);
|
|
2233
|
+
return response.status === 204 ? null : response.json();
|
|
2234
|
+
}
|
|
2235
|
+
return { role: 'gitea-backend', baseUrl, ...methods };
|
|
2236
|
+
}
|
|
2237
|
+
```
|
|
2238
|
+
|
|
2239
|
+
**Backend methods (all use `request()` internally):**
|
|
2240
|
+
|
|
2241
|
+
| Method | HTTP | Gitea API Path |
|
|
2242
|
+
|--------|------|----------------|
|
|
2243
|
+
| `createOrganization({ name, fullName, description, visibility })` | POST | `/orgs` |
|
|
2244
|
+
| `createUser({ username, email, fullName, password, mustChangePassword })` | POST | `/admin/users` |
|
|
2245
|
+
| `editUser({ username, email, fullName, active, admin })` | PATCH | `/admin/users/{username}` |
|
|
2246
|
+
| `addUserSshKey({ title, key, readOnly })` | POST | `/user/keys` |
|
|
2247
|
+
| `createRepository({ owner, name, private, defaultBranch, description })` | POST | `/orgs/{owner}/repos` or `/user/repos` |
|
|
2248
|
+
| `addDeployKey({ owner, repo, title, key, readOnly })` | POST | `/repos/{owner}/{repo}/keys` |
|
|
2249
|
+
| `addCollaborator({ owner, repo, username, permission })` | PUT | `/repos/{owner}/{repo}/collaborators/{username}` |
|
|
2250
|
+
| `addTeamRepository({ org, team, repo, owner, permission })` | PUT | `/teams/{team}/repos/{owner}/{repo}` |
|
|
2251
|
+
| `createTeam({ org, name, permission, units })` | POST | `/orgs/{org}/teams` |
|
|
2252
|
+
| `addTeamMember({ team, username })` | PUT | `/teams/{team}/members/{username}` |
|
|
2253
|
+
| `protectBranch({ owner, repo, branch, approvals, statusChecks })` | POST | `/repos/{owner}/{repo}/branch_protections` |
|
|
2254
|
+
| `createIssue({ owner, repo, title, body, labels, assignees })` | POST | `/repos/{owner}/{repo}/issues` |
|
|
2255
|
+
| `createPullRequest({ owner, repo, title, head, base, body })` | POST | `/repos/{owner}/{repo}/pulls` |
|
|
2256
|
+
| `createWebhook({ owner, repo, url, events, secret })` | POST | `/repos/{owner}/{repo}/hooks` |
|
|
2257
|
+
|
|
2258
|
+
### 28.4 Authentication
|
|
2259
|
+
|
|
2260
|
+
All requests include `Authorization: token <token>` header when `token` is provided (from `KRADLE_GITEA_TOKEN` environment variable).
|
|
2261
|
+
|
|
2262
|
+
---
|
|
2263
|
+
|
|
2264
|
+
## 29. Notification Controller
|
|
2265
|
+
|
|
2266
|
+
> Source: `packages/kradle/core/src/notification-controller.js`
|
|
2267
|
+
|
|
2268
|
+
### 29.1 Event-to-Notification Mapping
|
|
2269
|
+
|
|
2270
|
+
| Event Type | Event Status/Condition | Notification Type | Severity |
|
|
2271
|
+
|------------|----------------------|-------------------|----------|
|
|
2272
|
+
| `AgentDispatchRun` | `status='completed'` | `run-complete` | info |
|
|
2273
|
+
| `AgentDispatchRun` | `status='failed'` | `run-complete` | error |
|
|
2274
|
+
| `AgentDispatchRun` | other status | `run-complete` | info |
|
|
2275
|
+
| `AgentApproval` | `status='pending'` | `approval-needed` | warning |
|
|
2276
|
+
| `ExternalSyncConflict` | any | `conflict-detected` | warning |
|
|
2277
|
+
| `KradleWorkspace` | `claimed=true` | `workspace-ready` | info |
|
|
2278
|
+
| fallback | any | `system` | info |
|
|
2279
|
+
|
|
2280
|
+
### 29.2 Notification Shape
|
|
2281
|
+
|
|
2282
|
+
```javascript
|
|
2283
|
+
{
|
|
2284
|
+
id: string, // crypto.randomUUID()
|
|
2285
|
+
type: string, // 'run-complete' | 'approval-needed' | 'conflict-detected' | 'workspace-ready' | 'system'
|
|
2286
|
+
title: string, // Human-readable title
|
|
2287
|
+
message: string, // Detailed message
|
|
2288
|
+
severity: string, // 'info' | 'warning' | 'error'
|
|
2289
|
+
resourceRef: any, // Optional reference to the triggering resource
|
|
2290
|
+
createdAt: string, // ISO 8601 timestamp
|
|
2291
|
+
read: boolean, // Read state (default false)
|
|
2292
|
+
org: string // Organization slug
|
|
2293
|
+
}
|
|
2294
|
+
```
|
|
2295
|
+
|
|
2296
|
+
### 29.3 Storage Model
|
|
2297
|
+
|
|
2298
|
+
```javascript
|
|
2299
|
+
const store = new Map(); // org → notifications[] (in-memory, no persistence)
|
|
2300
|
+
const prefsStore = new Map(); // userId → preferences object
|
|
2301
|
+
```
|
|
2302
|
+
|
|
2303
|
+
### 29.4 API Methods
|
|
2304
|
+
|
|
2305
|
+
| Method | Signature | Behavior |
|
|
2306
|
+
|--------|-----------|----------|
|
|
2307
|
+
| `createNotification(event)` | `(object) → notification` | Maps event → notification, pushes to org store |
|
|
2308
|
+
| `listNotifications(org, opts)` | `(string, { unreadOnly?, limit?, since? })` | Sort by createdAt desc, apply filters, cap to limit (default 20) |
|
|
2309
|
+
| `markAsRead(notificationId)` | `(string) → boolean` | Scans all org stores, sets `read=true`, returns success |
|
|
2310
|
+
| `markAllAsRead(org)` | `(string) → number` | Marks all unread in org, returns count |
|
|
2311
|
+
| `getUnreadCount(org)` | `(string) → number` | `.filter(n => !n.read).length` |
|
|
2312
|
+
| `getPreferences(userId)` | `(string) → prefs` | Returns merged defaults + stored prefs |
|
|
2313
|
+
| `updatePreferences(userId, prefs)` | `(string, object) → prefs` | Deep-merge into stored prefs |
|
|
2314
|
+
|
|
2315
|
+
### 29.5 Default Preferences
|
|
2316
|
+
|
|
2317
|
+
```javascript
|
|
2318
|
+
{ runs: true, approvals: true, conflicts: true, workspaces: true, sound: false, desktop: false }
|
|
2319
|
+
```
|
|
2320
|
+
|
|
2321
|
+
---
|
|
2322
|
+
|
|
2323
|
+
## 30. Runner Controller
|
|
2324
|
+
|
|
2325
|
+
> Source: `packages/kradle/core/src/runner-controller.js`
|
|
2326
|
+
|
|
2327
|
+
### 30.1 validateRunnerPool(resource)
|
|
2328
|
+
|
|
2329
|
+
Validates a RunnerPool resource:
|
|
2330
|
+
|
|
2331
|
+
| Check | Error Reason | Message |
|
|
2332
|
+
|-------|--------------|---------|
|
|
2333
|
+
| resource is null/undefined | `missing-resource` | resource is required |
|
|
2334
|
+
| no `metadata.name` | `missing-name` | metadata.name is required |
|
|
2335
|
+
| no `spec.organizationRef` | `missing-org` | spec.organizationRef is required |
|
|
2336
|
+
| `warmReplicas` not non-negative int | `invalid-min-replicas` | must be non-negative integer |
|
|
2337
|
+
| `maxReplicas` not positive int | `invalid-max-replicas` | must be positive integer |
|
|
2338
|
+
| `warmReplicas > maxReplicas` | `replicas-conflict` | must not exceed maxReplicas |
|
|
2339
|
+
|
|
2340
|
+
Returns `{ valid: true, name, organizationRef, warmReplicas, maxReplicas, image, labels }` on success.
|
|
2341
|
+
|
|
2342
|
+
### 30.2 getPoolStatus(pool)
|
|
2343
|
+
|
|
2344
|
+
```javascript
|
|
2345
|
+
return {
|
|
2346
|
+
poolName,
|
|
2347
|
+
idle: runners.filter(status === 'Idle').length,
|
|
2348
|
+
active: runners.filter(status === 'Running').length,
|
|
2349
|
+
terminating: runners.filter(status === 'Terminating').length,
|
|
2350
|
+
total: poolRunners.length,
|
|
2351
|
+
desired: pool.spec.warmReplicas,
|
|
2352
|
+
maxReplicas: pool.spec.maxReplicas,
|
|
2353
|
+
phase: total === 0 ? 'Empty' : active > 0 ? 'Active' : 'Idle',
|
|
2354
|
+
scaling: total < desired ? 'ScalingUp' : total > max ? 'ScalingDown' : 'Stable'
|
|
2355
|
+
};
|
|
2356
|
+
```
|
|
2357
|
+
|
|
2358
|
+
### 30.3 getCapacity(pool)
|
|
2359
|
+
|
|
2360
|
+
```javascript
|
|
2361
|
+
return {
|
|
2362
|
+
poolName,
|
|
2363
|
+
maxReplicas,
|
|
2364
|
+
used: runners.filter(status === 'Running').length,
|
|
2365
|
+
available: Math.max(0, maxReplicas - used),
|
|
2366
|
+
utilizationPct: Math.round((used / maxReplicas) * 100)
|
|
2367
|
+
};
|
|
2368
|
+
```
|
|
2369
|
+
|
|
2370
|
+
### 30.4 createRunner(pool, runRef)
|
|
2371
|
+
|
|
2372
|
+
1. Generates runner ID: `runner-${poolName}-${Date.now()}-${random5chars}`
|
|
2373
|
+
2. Determines initial status: `'Running'` if runRef provided, `'Idle'` otherwise
|
|
2374
|
+
3. Calls `generatePodSpec()` to build the K8s Pod manifest
|
|
2375
|
+
4. Stores in in-memory `runners` Map
|
|
2376
|
+
5. If runRef → stores in `jobAssignments` Map
|
|
2377
|
+
|
|
2378
|
+
### 30.5 generatePodSpec({ runnerId, pool }, workspace)
|
|
2379
|
+
|
|
2380
|
+
Produces a complete K8s Pod manifest:
|
|
2381
|
+
|
|
2382
|
+
```javascript
|
|
2383
|
+
{
|
|
2384
|
+
apiVersion: 'v1',
|
|
2385
|
+
kind: 'Pod',
|
|
2386
|
+
metadata: {
|
|
2387
|
+
name: `runner-${runnerId}`,
|
|
2388
|
+
namespace: pool.metadata.namespace || 'kradle-org-default',
|
|
2389
|
+
labels: {
|
|
2390
|
+
'kradle.a5c.ai/runner': runnerId,
|
|
2391
|
+
'kradle.a5c.ai/pool': poolName,
|
|
2392
|
+
'kradle.a5c.ai/org': organizationRef
|
|
2393
|
+
}
|
|
2394
|
+
},
|
|
2395
|
+
spec: {
|
|
2396
|
+
serviceAccountName: spec.serviceAccount || 'kradle-runner',
|
|
2397
|
+
restartPolicy: 'Never',
|
|
2398
|
+
containers: [{
|
|
2399
|
+
name: 'runner',
|
|
2400
|
+
image: spec.image || 'ubuntu:24.04',
|
|
2401
|
+
env: [
|
|
2402
|
+
{ name: 'KRADLE_ORG', value: organizationRef },
|
|
2403
|
+
{ name: 'KRADLE_RUN_ID', value: runId },
|
|
2404
|
+
{ name: 'KRADLE_WORKSPACE_PATH', value: '/workspace' }
|
|
2405
|
+
],
|
|
2406
|
+
volumeMounts: workspace ? [{ name: 'workspace', mountPath: '/workspace' }] : [],
|
|
2407
|
+
resources: {
|
|
2408
|
+
limits: spec.resourceLimits || { cpu: '2', memory: '4Gi' },
|
|
2409
|
+
requests: spec.resourceRequests || { cpu: '500m', memory: '1Gi' }
|
|
2410
|
+
}
|
|
2411
|
+
}],
|
|
2412
|
+
volumes: workspace ? [{
|
|
2413
|
+
name: 'workspace',
|
|
2414
|
+
persistentVolumeClaim: { claimName: `kradle-ws-${runId}` }
|
|
2415
|
+
}] : []
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
```
|
|
2419
|
+
|
|
2420
|
+
### 30.6 scheduleJob(pool, job)
|
|
2421
|
+
|
|
2422
|
+
1. Check if job already assigned → return existing runner (reused=true)
|
|
2423
|
+
2. Find idle runner in pool → assign it (status → Running)
|
|
2424
|
+
3. Check capacity → if none available, return `{ error: true, reason: 'no-capacity' }`
|
|
2425
|
+
4. Create new runner via `createRunner(pool, jobRef)`
|
|
2426
|
+
|
|
2427
|
+
### 30.7 terminateRunner(runnerId)
|
|
2428
|
+
|
|
2429
|
+
1. Look up runner in Map
|
|
2430
|
+
2. Remove job assignment if any
|
|
2431
|
+
3. Set status to `'Terminating'`, record `terminatedAt`
|
|
2432
|
+
4. Remove from runners Map
|
|
2433
|
+
|
|
2434
|
+
---
|
|
2435
|
+
|
|
2436
|
+
## 31. External Backend Pipeline
|
|
2437
|
+
|
|
2438
|
+
> Source: `packages/kradle/core/src/external/`
|
|
2439
|
+
|
|
2440
|
+
### 31.1 Provider Registration (provider-resource-factory.js)
|
|
2441
|
+
|
|
2442
|
+
```javascript
|
|
2443
|
+
export function createDefaultProviderRegistry() {
|
|
2444
|
+
const registry = createProviderRegistry(); // Map<type, adapter>
|
|
2445
|
+
registry.register('github', buildGitHubAdapterDescriptor());
|
|
2446
|
+
return registry;
|
|
2447
|
+
}
|
|
2448
|
+
```
|
|
2449
|
+
|
|
2450
|
+
The GitHub adapter descriptor exposes factory methods (`createForge`, `createIssueTracker`, `createCicd`) for credential-bound instances, plus stub credential-free interfaces.
|
|
2451
|
+
|
|
2452
|
+
**Provider Registry API:**
|
|
2453
|
+
- `register(type, adapter)` — stores adapter by type key
|
|
2454
|
+
- `get(type)` → adapter or null
|
|
2455
|
+
- `list()` → `[...adapters.keys()]`
|
|
2456
|
+
|
|
2457
|
+
**Adapter validation contract** (from `provider-adapter.js`):
|
|
2458
|
+
- Required: `descriptor()`, `health()`, `normalizeWebhook(payload)`, `verifyWebhook(request)`
|
|
2459
|
+
- At least one of: `issueTracking`, `cicd`, `gitForge`
|
|
2460
|
+
|
|
2461
|
+
### 31.2 Webhook Ingestion (webhook-controller.js)
|
|
2462
|
+
|
|
2463
|
+
```javascript
|
|
2464
|
+
export function createWebhookController({ secret }) {
|
|
2465
|
+
const deliveries = new Map(); // deliveryId → record
|
|
2466
|
+
const subscribers = []; // event handlers
|
|
2467
|
+
return { verifyHmacSignature, createDeliveryRecord, recordDelivery, isDuplicate, onEvent, processDelivery };
|
|
2468
|
+
}
|
|
2469
|
+
```
|
|
2470
|
+
|
|
2471
|
+
**verifyHmacSignature(body, signature):**
|
|
2472
|
+
- Requires `sha256=` prefix
|
|
2473
|
+
- Computes `sha256=` + HMAC-SHA256(secret, body).hex()
|
|
2474
|
+
- Constant-time comparison via `timingSafeEqual` on the full strings (not just digests)
|
|
2475
|
+
- Returns `{ valid: boolean, reason: string|null }`
|
|
2476
|
+
|
|
2477
|
+
**processDelivery({ deliveryId, eventType, payload, rawBody }):**
|
|
2478
|
+
1. Dedup check: `if (isDuplicate(deliveryId)) → { queued: 0, duplicate: true }`
|
|
2479
|
+
2. Create delivery record with timestamp
|
|
2480
|
+
3. Store in `deliveries` Map
|
|
2481
|
+
4. Emit to all subscribers
|
|
2482
|
+
5. Return `{ queued: subscriberCount, duplicate: false, deliveryId }`
|
|
2483
|
+
|
|
2484
|
+
### 31.3 Event Normalization (sync-controller.js)
|
|
2485
|
+
|
|
2486
|
+
```javascript
|
|
2487
|
+
normalizeEvent(rawEvent) → {
|
|
2488
|
+
eventType,
|
|
2489
|
+
action: rawEvent.action || 'unknown',
|
|
2490
|
+
nativeId,
|
|
2491
|
+
providerRef,
|
|
2492
|
+
resourceKind,
|
|
2493
|
+
data: rawEvent.data || {},
|
|
2494
|
+
receivedAt: rawEvent.receivedAt || now,
|
|
2495
|
+
canonicalAt: now
|
|
2496
|
+
}
|
|
2497
|
+
```
|
|
2498
|
+
|
|
2499
|
+
### 31.4 Resource Upsert (sync-controller.js)
|
|
2500
|
+
|
|
2501
|
+
```javascript
|
|
2502
|
+
upsertResource({ kind, localName, namespace, spec, externalEnvelope }) → resource
|
|
2503
|
+
```
|
|
2504
|
+
|
|
2505
|
+
The `externalEnvelope` contains:
|
|
2506
|
+
- `nativeId` — provider's identifier (e.g. GitHub issue number)
|
|
2507
|
+
- `url` — canonical URL on the provider
|
|
2508
|
+
- `etag` — version marker for conflict detection
|
|
2509
|
+
- `providerRef` — which ExternalBackendProvider this came from
|
|
2510
|
+
|
|
2511
|
+
On upsert:
|
|
2512
|
+
- `firstSyncedAt` is preserved from existing resource
|
|
2513
|
+
- `lastSyncedAt` is always updated to now
|
|
2514
|
+
- Stored in internal `resources` Map keyed by `${namespace}/${kind}/${localName}`
|
|
2515
|
+
- Fire-and-forget `persistFn(resource)` called
|
|
2516
|
+
|
|
2517
|
+
### 31.5 Watermark Tracking (sync-controller.js)
|
|
2518
|
+
|
|
2519
|
+
```javascript
|
|
2520
|
+
updateWatermark(bindingRef, timestamp)
|
|
2521
|
+
// Only advances if new timestamp > current (monotonic)
|
|
2522
|
+
// Persists as ExternalSyncWatermark CRD resource
|
|
2523
|
+
|
|
2524
|
+
getWatermark(bindingRef) → string | null
|
|
2525
|
+
```
|
|
2526
|
+
|
|
2527
|
+
Per-binding state stored in `watermarks` Map. Prevents re-processing of already-synced events.
|
|
2528
|
+
|
|
2529
|
+
### 31.6 Ownership Modes (sync-controller.js)
|
|
2530
|
+
|
|
2531
|
+
```javascript
|
|
2532
|
+
applyOwnershipMode({ ownershipMode, operation, origin }) → { allowed, reason }
|
|
2533
|
+
```
|
|
2534
|
+
|
|
2535
|
+
| Mode | Kradle Write | External Write |
|
|
2536
|
+
|------|-------------|----------------|
|
|
2537
|
+
| `bidirectional` | allowed | allowed |
|
|
2538
|
+
| `external-owned` | **blocked** | allowed |
|
|
2539
|
+
| `kradle-owned` | allowed | **blocked** |
|
|
2540
|
+
| unknown | blocked | blocked |
|
|
2541
|
+
|
|
2542
|
+
### 31.7 Conflict Detection (conflict-controller.js)
|
|
2543
|
+
|
|
2544
|
+
```javascript
|
|
2545
|
+
detectConflict({ resourceRef, fieldPath, localValue, externalValue, namespace, organizationRef })
|
|
2546
|
+
```
|
|
2547
|
+
|
|
2548
|
+
- If `localValue === externalValue` → `{ conflict: null }` (no conflict)
|
|
2549
|
+
- If different → creates `ExternalSyncConflict` resource with phase `'Open'`
|
|
2550
|
+
- Conflict name: `conflict-${resourceRef}-${fieldPath}-${timestamp}`
|
|
2551
|
+
|
|
2552
|
+
### 31.8 Conflict Resolution (conflict-controller.js)
|
|
2553
|
+
|
|
2554
|
+
```javascript
|
|
2555
|
+
resolveConflict({ conflictName, strategy, resolvedValue, resources })
|
|
2556
|
+
```
|
|
2557
|
+
|
|
2558
|
+
| Strategy | Chosen Value | New Phase |
|
|
2559
|
+
|----------|-------------|-----------|
|
|
2560
|
+
| `prefer-external` | `spec.externalValue` | `Resolved` |
|
|
2561
|
+
| `prefer-kradle` | `spec.localValue` | `Resolved` |
|
|
2562
|
+
| `manual` | `resolvedValue` param (required) | `Resolved` |
|
|
2563
|
+
| `ignore` | undefined | `Ignored` |
|
|
2564
|
+
|
|
2565
|
+
**Superseded check:** `supersededCheck({ resourceRef, fieldPath, resources })` marks all Open conflicts for the same field as `'Superseded'` when a new sync event arrives.
|
|
2566
|
+
|
|
2567
|
+
### 31.9 Write Intents (write-controller.js)
|
|
2568
|
+
|
|
2569
|
+
**Phase lifecycle:**
|
|
2570
|
+
|
|
2571
|
+
```
|
|
2572
|
+
PendingApproval → ReadyToSend → Sending → Succeeded
|
|
2573
|
+
↘ Retrying → Sending (loop, up to maxRetries)
|
|
2574
|
+
↘ Failed
|
|
2575
|
+
PendingApproval → Rejected
|
|
2576
|
+
```
|
|
2577
|
+
|
|
2578
|
+
**createWriteIntent({ interfaceKey, operation, payload, resourceRef, requiresApproval, maxRetries }):**
|
|
2579
|
+
- Generates idempotency key via `getIdempotencyKey()`
|
|
2580
|
+
- Initial phase: `PendingApproval` if `requiresApproval`, else `ReadyToSend`
|
|
2581
|
+
|
|
2582
|
+
**approveWriteIntent({ intentName, approvedBy, resources }):**
|
|
2583
|
+
- Validates current phase is `PendingApproval`
|
|
2584
|
+
- Transitions to `ReadyToSend` with approver + timestamp
|
|
2585
|
+
|
|
2586
|
+
**executeWriteIntent({ intentName, resources, executor, onPhaseChange }):**
|
|
2587
|
+
- Validates current phase is `ReadyToSend`
|
|
2588
|
+
- Calls `executor()` (an async function that performs the external API call)
|
|
2589
|
+
- On success: phase → `Succeeded` with `externalResult`
|
|
2590
|
+
- On failure: retries up to `maxRetries`, phase cycles through `Retrying`
|
|
2591
|
+
- After exhausting retries: phase → `Failed` with `lastError`
|
|
2592
|
+
|
|
2593
|
+
### 31.10 Idempotency Key Generation (write-controller.js)
|
|
2594
|
+
|
|
2595
|
+
```javascript
|
|
2596
|
+
export function getIdempotencyKey({ interfaceKey, operation, resourceRef, payload }) {
|
|
2597
|
+
const canonical = JSON.stringify({ interfaceKey, operation, resourceRef, payload }, sortedKeys);
|
|
2598
|
+
// djb2 hash algorithm
|
|
2599
|
+
let hash = 5381;
|
|
2600
|
+
for (let i = 0; i < canonical.length; i++) {
|
|
2601
|
+
hash = ((hash << 5) + hash) ^ canonical.charCodeAt(i);
|
|
2602
|
+
hash = hash >>> 0; // Keep 32-bit unsigned
|
|
2603
|
+
}
|
|
2604
|
+
return `idem-${interfaceKey}-${operation}-${hash.toString(16)}`;
|
|
2605
|
+
}
|
|
2606
|
+
```
|
|
2607
|
+
|
|
2608
|
+
Deterministic — same inputs always produce the same key. Used to prevent duplicate write operations.
|
|
2609
|
+
|
|
2610
|
+
### 31.11 Persistence Callbacks
|
|
2611
|
+
|
|
2612
|
+
All controllers (sync, conflict, write) accept an optional `persistFn`:
|
|
2613
|
+
|
|
2614
|
+
```javascript
|
|
2615
|
+
function persist(resource) {
|
|
2616
|
+
if (typeof persistFn === 'function') {
|
|
2617
|
+
Promise.resolve(persistFn(resource)).catch(() => {}); // Fire-and-forget
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
```
|
|
2621
|
+
|
|
2622
|
+
The persistFn is called with a fully-formed K8s-style CRD resource. Errors are swallowed — the caller wires monitoring separately if needed.
|
|
2623
|
+
|
|
2624
|
+
### 31.12 GitHub Adapter (external/github/)
|
|
2625
|
+
|
|
2626
|
+
**auth.js — JWT Signing and Token Exchange:**
|
|
2627
|
+
|
|
2628
|
+
```javascript
|
|
2629
|
+
// createGitHubJwt({ appId, privateKey, expiresInSeconds })
|
|
2630
|
+
// - RS256 for PEM keys (production)
|
|
2631
|
+
// - HS256 fallback for non-PEM keys (test mode)
|
|
2632
|
+
// - Returns: header.payload.signature (base64url encoded)
|
|
2633
|
+
|
|
2634
|
+
// exchangeInstallationToken({ appJwt, installationId, fetchImpl })
|
|
2635
|
+
// - POST https://api.github.com/app/installations/{id}/access_tokens
|
|
2636
|
+
// - Returns: { token, expiresAt }
|
|
2637
|
+
```
|
|
2638
|
+
|
|
2639
|
+
**git-forge.js — GitHubGitForge class:**
|
|
2640
|
+
|
|
2641
|
+
| Method | GitHub API |
|
|
2642
|
+
|--------|-----------|
|
|
2643
|
+
| `listRepositories()` | GET `/installation/repositories` |
|
|
2644
|
+
| `getPullRequest({ repo, pullNumber })` | GET `/repos/{owner}/{repo}/pulls/{number}` |
|
|
2645
|
+
| `createPullRequest({ repo, title, head, base, body })` | POST `/repos/{owner}/{repo}/pulls` |
|
|
2646
|
+
| `mergePullRequest({ repo, pullNumber, mergeMethod, commitTitle })` | PUT `/repos/{owner}/{repo}/pulls/{number}/merge` |
|
|
2647
|
+
| `listRefs({ repo })` | GET `/repos/{owner}/{repo}/branches` + `/tags` (parallel) |
|
|
2648
|
+
| `syncDeployKeys({ repo, desiredKeys })` | GET+DELETE+POST `/repos/{owner}/{repo}/keys` |
|
|
2649
|
+
| `syncBranchProtection({ repo, branch, ... })` | PUT `/repos/{owner}/{repo}/branches/{branch}/protection` |
|
|
2650
|
+
|
|
2651
|
+
**issue-tracking.js — GitHubIssueTracking class:**
|
|
2652
|
+
|
|
2653
|
+
| Method | GitHub API |
|
|
2654
|
+
|--------|-----------|
|
|
2655
|
+
| `listIssues({ repo, state })` | GET `/repos/{owner}/{repo}/issues?state={state}` |
|
|
2656
|
+
| `createIssue({ repo, title, body, labels })` | POST `/repos/{owner}/{repo}/issues` |
|
|
2657
|
+
| `updateIssue({ repo, issueNumber, title, body, labels })` | PATCH `/repos/{owner}/{repo}/issues/{number}` |
|
|
2658
|
+
| `closeIssue({ repo, issueNumber })` | PATCH `/repos/{owner}/{repo}/issues/{number}` (state: closed) |
|
|
2659
|
+
| `listComments({ repo, issueNumber })` | GET `/repos/{owner}/{repo}/issues/{number}/comments` |
|
|
2660
|
+
| `createComment({ repo, issueNumber, body })` | POST `/repos/{owner}/{repo}/issues/{number}/comments` |
|
|
2661
|
+
|
|
2662
|
+
**cicd.js — GitHubCicd class:**
|
|
2663
|
+
|
|
2664
|
+
| Method | GitHub API |
|
|
2665
|
+
|--------|-----------|
|
|
2666
|
+
| `listWorkflowRuns({ repo, workflowId? })` | GET `/repos/{owner}/{repo}/actions/runs` or `/actions/workflows/{id}/runs` |
|
|
2667
|
+
| `listJobs({ repo, runId })` | GET `/repos/{owner}/{repo}/actions/runs/{id}/jobs` |
|
|
2668
|
+
| `rerunWorkflow({ repo, runId })` | POST `/repos/{owner}/{repo}/actions/runs/{id}/rerun` |
|
|
2669
|
+
| `cancelWorkflow({ repo, runId })` | POST `/repos/{owner}/{repo}/actions/runs/{id}/cancel` |
|
|
2670
|
+
| `createCheck({ repo, name, headSha, status, conclusion, detailsUrl, output })` | POST `/repos/{owner}/{repo}/check-runs` |
|
|
2671
|
+
| `updateCheck({ repo, checkRunId, status, conclusion, output })` | PATCH `/repos/{owner}/{repo}/check-runs/{id}` |
|
|
2672
|
+
|
|
2673
|
+
All GitHub classes use `X-GitHub-Api-Version: 2022-11-28` header and Bearer token auth.
|
|
2674
|
+
|
|
2675
|
+
---
|
|
2676
|
+
|
|
2677
|
+
## 32. KServe Model Inference Pipeline
|
|
2678
|
+
|
|
2679
|
+
### KradleInferenceService Controller (`kradle-inference-service-controller.js`)
|
|
2680
|
+
|
|
2681
|
+
- `KradleInferenceService` wraps KServe `InferenceService` CRDs under `serving.kserve.io/v1beta1`
|
|
2682
|
+
- Endpoint discovery from K8s status after KServe readiness
|
|
2683
|
+
- V1 and V2 inference protocol support (`/v1/models/{name}:predict`, `/v2/models/{name}/infer`)
|
|
2684
|
+
- `toProviderConfig` creates an `AgentProviderConfig` with `type: 'kserve'` for agent stack integration
|
|
2685
|
+
- Agent stacks can reference on-cluster models alongside cloud LLMs via the provider bridge
|
|
2686
|
+
- Supported model frameworks: `sklearn`, `xgboost`, `lightgbm`, `tensorflow`, `pytorch`, `onnx`, `triton`, `huggingface`, `custom`
|
|
2687
|
+
|
|
2688
|
+
**Boundary:**
|
|
2689
|
+
```javascript
|
|
2690
|
+
export const KRADLE_INFERENCE_SERVICE_CONTROLLER_BOUNDARY = {
|
|
2691
|
+
role: 'kradle-inference-service-controller',
|
|
2692
|
+
owns: ['inference service validation', 'KServe manifest generation', 'endpoint resolution',
|
|
2693
|
+
'provider config bridge', 'inference protocol handling', 'health checks'],
|
|
2694
|
+
delegatesTo: ['resource-model', 'agent-provider-config-controller'],
|
|
2695
|
+
};
|
|
2696
|
+
```
|
|
2697
|
+
|
|
2698
|
+
**Key exports:** `createInferenceServiceController`, `KRADLE_INFERENCE_SERVICE_CONTROLLER_BOUNDARY`, `SUPPORTED_MODEL_FORMATS`, `INFERENCE_PROTOCOLS`, `KSERVE_API_GROUP`, `KSERVE_API_VERSION`
|
|
2699
|
+
|
|
2700
|
+
---
|
|
2701
|
+
|
|
2702
|
+
## 33. Artifact Registry System
|
|
2703
|
+
|
|
2704
|
+
### Artifact Registry Controller (`artifact-registry-controller.js`)
|
|
2705
|
+
|
|
2706
|
+
Five resource kinds:
|
|
2707
|
+
|
|
2708
|
+
| Kind | Description |
|
|
2709
|
+
|------|-------------|
|
|
2710
|
+
| `ArtifactRegistry` | Top-level registry scoped to an org |
|
|
2711
|
+
| `ArtifactFeed` | Named feed within a registry (e.g. `npm`, `pip`, `docker`) |
|
|
2712
|
+
| `ArtifactAccessPolicy` | Per-feed access control (read/write/admin per subject) |
|
|
2713
|
+
| `ArtifactVersion` | Immutable published artifact version record |
|
|
2714
|
+
| `ArtifactDownload` | Audit record for each artifact download |
|
|
2715
|
+
|
|
2716
|
+
- Supports `npm`, `pip`, `docker`, and `generic` artifact types
|
|
2717
|
+
- Internal storage backend plus external integrations (`s3`, `azure-blob`, `gcs`)
|
|
2718
|
+
- External backend modes: `read-only`, `read-write`, `mirror` (e.g. GitHub Packages proxy)
|
|
2719
|
+
- Protocol-specific install command generation per feed type
|
|
2720
|
+
- Access policy enforcement: `read`, `write`, `admin` permissions per feed per subject
|
|
2721
|
+
|
|
2722
|
+
**Boundary:**
|
|
2723
|
+
```javascript
|
|
2724
|
+
export const ARTIFACT_REGISTRY_CONTROLLER_BOUNDARY = {
|
|
2725
|
+
role: 'artifact-registry-controller',
|
|
2726
|
+
owns: ['registry validation', 'feed management', 'access policy enforcement',
|
|
2727
|
+
'artifact publish', 'artifact version listing', 'artifact deletion', 'download audit'],
|
|
2728
|
+
delegatesTo: ['resource-model', 'external-backend-binding'],
|
|
2729
|
+
mustNotOwn: ['secret values', 'blob storage I/O', 'network transport'],
|
|
2730
|
+
};
|
|
2731
|
+
```
|
|
2732
|
+
|
|
2733
|
+
**Key exports:** `createArtifactRegistryController`, `ARTIFACT_REGISTRY_CONTROLLER_BOUNDARY`
|
|
2734
|
+
|
|
2735
|
+
---
|
|
2736
|
+
|
|
2737
|
+
## 34. Assistant Agent
|
|
2738
|
+
|
|
2739
|
+
### Assistant Runtime (`assistant-runtime.js`)
|
|
2740
|
+
|
|
2741
|
+
- In-process runtime using the Anthropic API (no K8s Job dispatch)
|
|
2742
|
+
- Chat sessions with message history persisted via `globalThis` session store
|
|
2743
|
+
- `createAssistantRuntime` returns: `chat`, `generate`, `listSessions`, `clearSession`
|
|
2744
|
+
- Structured generation endpoint (`generate`) for dynamic content and tool-augmented agentic calls
|
|
2745
|
+
- Default `assistant` AgentStack deployed via the Helm chart (`values.yaml` default stacks)
|
|
2746
|
+
- Stack selector allows different `AgentStack` CRDs per conversation context
|
|
2747
|
+
- `callModel` handles HTTP to Anthropic API with support for tool definitions and response format constraints
|
|
2748
|
+
|
|
2749
|
+
**Boundary:**
|
|
2750
|
+
```javascript
|
|
2751
|
+
export const ASSISTANT_RUNTIME_BOUNDARY = {
|
|
2752
|
+
role: 'assistant-runtime',
|
|
2753
|
+
owns: ['chat sessions', 'message history', 'model API calls', 'structured agentic calls', 'session lifecycle'],
|
|
2754
|
+
delegatesTo: ['resource-model', 'agent-stack-controller', 'agent-mux-client'],
|
|
2755
|
+
mustNotOwn: ['secret values', 'K8s Job dispatch', 'resource persistence'],
|
|
2756
|
+
};
|
|
2757
|
+
```
|
|
2758
|
+
|
|
2759
|
+
**Key exports:** `createAssistantRuntime`, `ASSISTANT_RUNTIME_BOUNDARY`, `defaultAssistantConfig`, `defaultSystemPrompt`, `callModel`
|