@a5c-ai/kradle 5.0.1-staging.3abdf9534c25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/Dockerfile +31 -0
  2. package/README.md +187 -0
  3. package/bin/kradle-demo.mjs +23 -0
  4. package/bin/kradle-server.mjs +14 -0
  5. package/dist/kradle-controller-ui.json +3482 -0
  6. package/dist/kradle-lifecycle.json +201 -0
  7. package/dist/kradle-runtime-snapshot.json +3125 -0
  8. package/dist/kradle-summary.json +724 -0
  9. package/docs/README.md +61 -0
  10. package/docs/agents/README.md +83 -0
  11. package/docs/agents/acceptance-test-matrix.md +193 -0
  12. package/docs/agents/agent-mux-adapter-contract.md +167 -0
  13. package/docs/agents/agent-mux-source-map.md +310 -0
  14. package/docs/agents/agent-run-memory-import-spec.md +256 -0
  15. package/docs/agents/agent-stack-management-spec.md +421 -0
  16. package/docs/agents/api-contract-spec.md +309 -0
  17. package/docs/agents/artifacts-writeback-spec.md +145 -0
  18. package/docs/agents/chart-packaging-spec.md +128 -0
  19. package/docs/agents/ci-orchestration-spec.md +140 -0
  20. package/docs/agents/context-assembly-spec.md +219 -0
  21. package/docs/agents/controller-reconciliation-spec.md +255 -0
  22. package/docs/agents/crd-schema-spec.md +315 -0
  23. package/docs/agents/decision-log-open-questions.md +169 -0
  24. package/docs/agents/developer-implementation-checklist.md +329 -0
  25. package/docs/agents/dispatching-design.md +262 -0
  26. package/docs/agents/gaps-agent-mux-to-kradle-crds.md +298 -0
  27. package/docs/agents/glossary.md +66 -0
  28. package/docs/agents/implementation-blueprint.md +324 -0
  29. package/docs/agents/implementation-rollout-slices.md +251 -0
  30. package/docs/agents/memory-context-integration-spec.md +194 -0
  31. package/docs/agents/memory-ontology-schema-spec.md +253 -0
  32. package/docs/agents/memory-operations-runbook.md +121 -0
  33. package/docs/agents/mvp-vertical-slice-spec.md +146 -0
  34. package/docs/agents/observability-audit-spec.md +265 -0
  35. package/docs/agents/operator-runbook.md +174 -0
  36. package/docs/agents/org-memory-api-payload-examples.md +333 -0
  37. package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
  38. package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
  39. package/docs/agents/org-memory-ui-implementation-map.md +114 -0
  40. package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
  41. package/docs/agents/org-resource-model-delta-spec.md +111 -0
  42. package/docs/agents/org-route-resource-model-spec.md +183 -0
  43. package/docs/agents/org-scoping-namespace-spec.md +114 -0
  44. package/docs/agents/rbac-secrets-management-spec.md +406 -0
  45. package/docs/agents/repository-page-integration-spec.md +255 -0
  46. package/docs/agents/resource-contract-examples.md +808 -0
  47. package/docs/agents/resource-relationship-map.md +190 -0
  48. package/docs/agents/security-threat-model.md +188 -0
  49. package/docs/agents/shared-memory-company-brain-spec.md +358 -0
  50. package/docs/agents/storage-migration-spec.md +168 -0
  51. package/docs/agents/subagent-orchestration-spec.md +152 -0
  52. package/docs/agents/system-overview.md +88 -0
  53. package/docs/agents/tools-mcp-skills-spec.md +189 -0
  54. package/docs/agents/traceability-matrix.md +79 -0
  55. package/docs/agents/ui-flow-spec.md +211 -0
  56. package/docs/agents/ui-ux-system-spec.md +426 -0
  57. package/docs/agents/workspace-lifecycle-spec.md +166 -0
  58. package/docs/architecture-spec.md +78 -0
  59. package/docs/architecture-v2.md +2759 -0
  60. package/docs/components/control-plane.md +78 -0
  61. package/docs/components/data-plane.md +69 -0
  62. package/docs/components/hooks-events.md +67 -0
  63. package/docs/components/identity-rbac-policy.md +73 -0
  64. package/docs/components/kubevela-oam.md +70 -0
  65. package/docs/components/operations-publishing.md +81 -0
  66. package/docs/components/runners-ci.md +66 -0
  67. package/docs/components/web-ui.md +94 -0
  68. package/docs/crd-behaviors-and-relationships.md +3926 -0
  69. package/docs/external/README.md +47 -0
  70. package/docs/external/bidirectional-sync-design.md +134 -0
  71. package/docs/external/cicd-interface.md +64 -0
  72. package/docs/external/external-backend-controllers.md +170 -0
  73. package/docs/external/external-backend-crds.md +234 -0
  74. package/docs/external/external-backend-ui-spec.md +151 -0
  75. package/docs/external/external-backend-ux-flows.md +115 -0
  76. package/docs/external/external-object-mapping.md +125 -0
  77. package/docs/external/git-forge-interface.md +68 -0
  78. package/docs/external/github-integration-design.md +151 -0
  79. package/docs/external/issue-tracking-interface.md +66 -0
  80. package/docs/external/provider-capability-manifests.md +204 -0
  81. package/docs/external/provider-catalog.md +139 -0
  82. package/docs/external/provider-rollout-testing.md +78 -0
  83. package/docs/external/research-results.md +48 -0
  84. package/docs/external/security-auth-permissions.md +81 -0
  85. package/docs/external/sync-state-machines.md +108 -0
  86. package/docs/external/unified-external-backend-model.md +107 -0
  87. package/docs/external/user-facing-changes.md +67 -0
  88. package/docs/gaps.md +161 -0
  89. package/docs/install.md +94 -0
  90. package/docs/integration-and-design-decisions.md +1530 -0
  91. package/docs/kradle-design.md +334 -0
  92. package/docs/local-minikube.md +55 -0
  93. package/docs/ontology/README.md +32 -0
  94. package/docs/ontology/bounded-contexts.md +29 -0
  95. package/docs/ontology/events-and-hooks.md +32 -0
  96. package/docs/ontology/oam-kubevela.md +32 -0
  97. package/docs/ontology/operations-and-release.md +25 -0
  98. package/docs/ontology/personas-and-actors.md +32 -0
  99. package/docs/ontology/policies-and-invariants.md +33 -0
  100. package/docs/ontology/problem-space.md +30 -0
  101. package/docs/ontology/resource-contracts.md +40 -0
  102. package/docs/ontology/resource-taxonomy.md +42 -0
  103. package/docs/ontology/runners-and-ci.md +29 -0
  104. package/docs/ontology/solution-space.md +24 -0
  105. package/docs/ontology/storage-and-data-boundaries.md +29 -0
  106. package/docs/ontology/validation-matrix.md +24 -0
  107. package/docs/ontology/web-ui-excellent-flows.md +32 -0
  108. package/docs/ontology/workflows.md +39 -0
  109. package/docs/ontology/world.md +35 -0
  110. package/docs/openapi.yaml +1291 -0
  111. package/docs/product-requirements.md +62 -0
  112. package/docs/requirements-v2.md +235 -0
  113. package/docs/roadmap-mvp.md +87 -0
  114. package/docs/sdk-api-reference.md +1108 -0
  115. package/docs/system-requirements.md +90 -0
  116. package/docs/system-spec-v2.md +1230 -0
  117. package/docs/tests/README.md +53 -0
  118. package/docs/tests/agent-qa-plan.md +63 -0
  119. package/docs/tests/browser-ui-tests.md +62 -0
  120. package/docs/tests/ci-quality-gates.md +48 -0
  121. package/docs/tests/coverage-model.md +64 -0
  122. package/docs/tests/e2e-scenario-tests.md +53 -0
  123. package/docs/tests/fixtures-test-data.md +63 -0
  124. package/docs/tests/observability-reliability-tests.md +54 -0
  125. package/docs/tests/product-test-matrix.md +145 -0
  126. package/docs/tests/qa-adoption-roadmap.md +130 -0
  127. package/docs/tests/qa-automation-plan.md +101 -0
  128. package/docs/tests/security-compliance-tests.md +57 -0
  129. package/docs/tests/test-framework-tools.md +88 -0
  130. package/docs/tests/test-suite-layout.md +121 -0
  131. package/docs/tests/unit-integration-tests.md +48 -0
  132. package/docs/todo-kyverno +714 -0
  133. package/docs/todos.md +4 -0
  134. package/docs/user-stories.md +78 -0
  135. package/docs/web-console-spec.md +533 -0
  136. package/examples/minikube-demo.yaml +190 -0
  137. package/examples/oam-application.yaml +23 -0
  138. package/examples/policy-kyverno-pr-title.yaml +18 -0
  139. package/package.json +66 -0
  140. package/scripts/build.mjs +29 -0
  141. package/scripts/setup-minikube.mjs +65 -0
  142. package/scripts/smoke.mjs +37 -0
  143. package/scripts/validate-doc-coverage.mjs +152 -0
  144. package/scripts/validate-package.mjs +95 -0
  145. package/scripts/validate-ui.mjs +305 -0
  146. package/src/agent-adapter-controller.js +169 -0
  147. package/src/agent-approval-controller.js +170 -0
  148. package/src/agent-context-bundles.js +242 -0
  149. package/src/agent-dispatch-controller.js +549 -0
  150. package/src/agent-gateway-config-controller.js +147 -0
  151. package/src/agent-identity-migration.js +115 -0
  152. package/src/agent-memory-controller.js +357 -0
  153. package/src/agent-memory-import.js +327 -0
  154. package/src/agent-memory-query.js +292 -0
  155. package/src/agent-memory-repository-source-controller.js +255 -0
  156. package/src/agent-mux-client.js +589 -0
  157. package/src/agent-permission-review.js +250 -0
  158. package/src/agent-persona-controller.js +135 -0
  159. package/src/agent-project-controller.js +117 -0
  160. package/src/agent-prompt-composition.js +55 -0
  161. package/src/agent-provider-config-controller.js +151 -0
  162. package/src/agent-secret-config-grant-controller.js +282 -0
  163. package/src/agent-session-transcript-controller.js +189 -0
  164. package/src/agent-stack-controller.js +421 -0
  165. package/src/agent-subagent-controller.js +160 -0
  166. package/src/agent-transport-binding-controller.js +121 -0
  167. package/src/agent-trigger-controller.js +387 -0
  168. package/src/agent-workspace-controller.js +702 -0
  169. package/src/agent-writeback-controller.js +302 -0
  170. package/src/api-controller.js +621 -0
  171. package/src/argocd-gitops.js +43 -0
  172. package/src/artifact-registry-controller.js +542 -0
  173. package/src/assistant-runtime.js +284 -0
  174. package/src/async-controller.js +207 -0
  175. package/src/audit-controller.js +191 -0
  176. package/src/auth.js +310 -0
  177. package/src/component-catalog.js +41 -0
  178. package/src/control-plane.js +136 -0
  179. package/src/controller-client.js +112 -0
  180. package/src/controller-ui.js +620 -0
  181. package/src/data-plane.js +179 -0
  182. package/src/event-bus.js +397 -0
  183. package/src/external/conflict-controller.js +225 -0
  184. package/src/external/github/auth.js +96 -0
  185. package/src/external/github/cicd.js +180 -0
  186. package/src/external/github/git-forge.js +240 -0
  187. package/src/external/github/index.js +144 -0
  188. package/src/external/github/issue-tracking.js +163 -0
  189. package/src/external/provider-adapter.js +161 -0
  190. package/src/external/provider-resource-factory.js +221 -0
  191. package/src/external/sync-controller.js +235 -0
  192. package/src/external/webhook-controller.js +144 -0
  193. package/src/external/write-controller.js +283 -0
  194. package/src/gitea-backend.js +131 -0
  195. package/src/gitea-service.js +173 -0
  196. package/src/handoff.js +98 -0
  197. package/src/health-probes.js +134 -0
  198. package/src/hooks-events.js +63 -0
  199. package/src/hooks-lifecycle.js +117 -0
  200. package/src/http-server.js +409 -0
  201. package/src/identity-policy.js +86 -0
  202. package/src/index.js +71 -0
  203. package/src/jitsi-agent-bridge.js +141 -0
  204. package/src/jitsi-meeting-controller.js +291 -0
  205. package/src/jitsi-sync-controller.js +198 -0
  206. package/src/kradle-inference-service-controller.js +246 -0
  207. package/src/kubernetes-controller-async.js +531 -0
  208. package/src/kubernetes-controller.js +904 -0
  209. package/src/kubernetes-resource-gateway.js +48 -0
  210. package/src/model-route-controller.js +364 -0
  211. package/src/notification-controller.js +178 -0
  212. package/src/operations.js +112 -0
  213. package/src/org-scoping.js +5 -0
  214. package/src/resource-model.js +282 -0
  215. package/src/runner-controller.js +272 -0
  216. package/src/runners-ci.js +48 -0
  217. package/src/runtime.js +196 -0
  218. package/src/snapshot-cache.js +157 -0
  219. package/src/virtual-model-controller.js +538 -0
  220. package/src/virtual-model-hook-bridge.js +200 -0
  221. package/src/web-ui.js +40 -0
  222. package/tests/agent-adapter-controller.test.js +361 -0
  223. package/tests/agent-approval-controller.test.js +173 -0
  224. package/tests/agent-context-bundles.test.js +278 -0
  225. package/tests/agent-dispatch-controller.test.js +679 -0
  226. package/tests/agent-gateway-config-controller.test.js +386 -0
  227. package/tests/agent-identity-migration.test.js +87 -0
  228. package/tests/agent-memory-controller.test.js +461 -0
  229. package/tests/agent-memory-import-snapshot.test.js +477 -0
  230. package/tests/agent-memory-query.test.js +404 -0
  231. package/tests/agent-memory-repository-source.test.js +514 -0
  232. package/tests/agent-mux-client.test.js +389 -0
  233. package/tests/agent-mux-integration.test.js +971 -0
  234. package/tests/agent-permission-review-v2.test.js +317 -0
  235. package/tests/agent-permission-review.test.js +209 -0
  236. package/tests/agent-persona-controller.test.js +127 -0
  237. package/tests/agent-project-controller.test.js +302 -0
  238. package/tests/agent-prompt-composition.test.js +76 -0
  239. package/tests/agent-provider-config-controller.test.js +376 -0
  240. package/tests/agent-resources.test.js +303 -0
  241. package/tests/agent-secret-config-grant.test.js +231 -0
  242. package/tests/agent-session-transcript-controller.test.js +499 -0
  243. package/tests/agent-stack-controller.test.js +283 -0
  244. package/tests/agent-subagent-controller.test.js +201 -0
  245. package/tests/agent-transport-binding-controller.test.js +294 -0
  246. package/tests/agent-trigger-controller.test.js +271 -0
  247. package/tests/agent-trigger-routes.test.js +190 -0
  248. package/tests/agent-trigger-sources.test.js +245 -0
  249. package/tests/agent-workspace-controller.test.js +181 -0
  250. package/tests/agent-writeback.test.js +292 -0
  251. package/tests/approval-persistence.test.js +171 -0
  252. package/tests/artifact-registry.test.js +511 -0
  253. package/tests/assistant-runtime.test.js +506 -0
  254. package/tests/async-controller.test.js +252 -0
  255. package/tests/audit-controller.test.js +227 -0
  256. package/tests/codespace-controller.test.js +318 -0
  257. package/tests/controller-client.test.js +133 -0
  258. package/tests/deployment.test.js +527 -0
  259. package/tests/e2e/lifecycle.test.js +120 -0
  260. package/tests/event-bus-integration.test.js +355 -0
  261. package/tests/external-github-forge.test.js +560 -0
  262. package/tests/external-github-issues-cicd.test.js +520 -0
  263. package/tests/external-integration.test.js +470 -0
  264. package/tests/external-persistence.test.js +415 -0
  265. package/tests/external-provider-adapter.test.js +365 -0
  266. package/tests/external-resource-model.test.js +223 -0
  267. package/tests/external-webhook-sync.test.js +287 -0
  268. package/tests/external-write-conflict.test.js +353 -0
  269. package/tests/gitea-service.test.js +253 -0
  270. package/tests/health-check-real.test.js +165 -0
  271. package/tests/health-probes.test.js +90 -0
  272. package/tests/hooks-lifecycle.test.js +364 -0
  273. package/tests/integration/full-flow.test.js +266 -0
  274. package/tests/jitsi-agent-bridge.test.js +119 -0
  275. package/tests/jitsi-helm-integration.test.js +77 -0
  276. package/tests/jitsi-meeting-controller.test.js +170 -0
  277. package/tests/jitsi-resource-model.test.js +73 -0
  278. package/tests/jitsi-sync-controller.test.js +112 -0
  279. package/tests/kradle-inference-service.test.js +689 -0
  280. package/tests/kradle.test.js +779 -0
  281. package/tests/memory-search-wiring.test.js +270 -0
  282. package/tests/model-route-controller.test.js +733 -0
  283. package/tests/notification-controller.test.js +196 -0
  284. package/tests/notification-integration.test.js +179 -0
  285. package/tests/org-scoping.test.js +687 -0
  286. package/tests/runner-controller.test.js +327 -0
  287. package/tests/runner-integration.test.js +231 -0
  288. package/tests/session-cookie-hmac.test.js +151 -0
  289. package/tests/snapshot-performance.test.js +315 -0
  290. package/tests/sse-events.test.js +107 -0
  291. package/tests/virtual-model-controller.test.js +877 -0
  292. package/tests/virtual-model-hook-bridge.test.js +384 -0
  293. package/tests/webhook-trigger.test.js +198 -0
  294. package/tests/workspace-volumes.test.js +312 -0
  295. package/tests/writeback-persistence.test.js +207 -0
@@ -0,0 +1,284 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import http from 'node:http';
3
+ import https from 'node:https';
4
+ import { URL } from 'node:url';
5
+
6
+ export const ASSISTANT_RUNTIME_BOUNDARY = {
7
+ role: 'assistant-runtime',
8
+ scope: 'In-process agent runtime for chat sessions and structured agentic calls via AgentStack CRDs',
9
+ owns: ['chat sessions', 'message history', 'model API calls', 'structured agentic calls', 'session lifecycle'],
10
+ delegatesTo: ['resource-model', 'agent-stack-controller', 'agent-mux-client'],
11
+ mustNotOwn: ['secret values', 'K8s Job dispatch', 'resource persistence'],
12
+ };
13
+
14
+ /**
15
+ * Default assistant stack configuration used when no CRD is found.
16
+ * @returns {object}
17
+ */
18
+ export function defaultAssistantConfig() {
19
+ return {
20
+ baseAgent: 'kradle-assistant',
21
+ provider: process.env.KRADLE_ASSISTANT_PROVIDER || 'anthropic',
22
+ model: process.env.KRADLE_ASSISTANT_MODEL || 'claude-sonnet-4-6',
23
+ systemPrompt: `You are the Kradle Assistant — an AI agent embedded in the Kradle Kubernetes-native forge. You help users manage repositories, agent stacks, workspaces, deployments, and policies. You have access to kradle MCP tools for resource management and Atlas graph for knowledge queries.`,
24
+ approvalMode: 'prompt',
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Default system prompt for the assistant.
30
+ * @returns {string}
31
+ */
32
+ export function defaultSystemPrompt() {
33
+ return `You are the Kradle Assistant — an AI agent embedded in the Kradle Kubernetes-native forge platform.
34
+
35
+ You help users with:
36
+ - Managing repositories, branches, and pull requests
37
+ - Configuring agent stacks and dispatching agent runs
38
+ - Managing workspaces and codespaces
39
+ - Querying the Atlas knowledge graph
40
+ - Configuring external provider integrations
41
+ - Managing secrets, policies, and permissions
42
+
43
+ You have access to MCP tools for direct resource management. Be concise and helpful.`;
44
+ }
45
+
46
+ /**
47
+ * Call a model via the Anthropic Messages API.
48
+ * Uses node:http/node:https — zero external deps.
49
+ *
50
+ * @param {{ provider: string, model: string, messages: object[], tools?: object[], maxTokens?: number, responseFormat?: string, apiKey?: string, fetchImpl?: Function }} params
51
+ * @returns {Promise<{ content: string, usage: object, stopReason?: string, toolCalls?: object[] }>}
52
+ */
53
+ export async function callModel({ provider, model, messages, tools, maxTokens, responseFormat, apiKey, baseUrl, fetchImpl } = {}) {
54
+ const resolvedProvider = provider || process.env.KRADLE_ASSISTANT_PROVIDER || 'anthropic';
55
+
56
+ if (resolvedProvider === 'anthropic') {
57
+ return callAnthropicModel({ model, messages, tools, maxTokens, apiKey, fetchImpl });
58
+ }
59
+
60
+ return callOpenAICompatibleModel({ provider: resolvedProvider, model, messages, tools, maxTokens, apiKey, baseUrl, fetchImpl });
61
+ }
62
+
63
+ async function callAnthropicModel({ model, messages, tools, maxTokens, apiKey, fetchImpl }) {
64
+ const resolvedKey = apiKey || process.env.ANTHROPIC_API_KEY || process.env.KRADLE_ASSISTANT_API_KEY;
65
+ if (!resolvedKey) {
66
+ return { content: 'Anthropic API key not configured. Set ANTHROPIC_API_KEY or KRADLE_ASSISTANT_API_KEY.', usage: {} };
67
+ }
68
+
69
+ const systemMessage = messages.find(m => m.role === 'system');
70
+ const nonSystemMessages = messages.filter(m => m.role !== 'system');
71
+
72
+ const body = {
73
+ model: model || 'claude-sonnet-4-6',
74
+ max_tokens: maxTokens || 4096,
75
+ messages: nonSystemMessages,
76
+ ...(systemMessage ? { system: systemMessage.content } : {}),
77
+ ...(tools?.length ? { tools } : {}),
78
+ };
79
+
80
+ const doFetch = fetchImpl || globalThis.fetch;
81
+
82
+ try {
83
+ const res = await doFetch('https://api.anthropic.com/v1/messages', {
84
+ method: 'POST',
85
+ headers: {
86
+ 'Content-Type': 'application/json',
87
+ 'x-api-key': resolvedKey,
88
+ 'anthropic-version': '2023-06-01',
89
+ },
90
+ body: JSON.stringify(body),
91
+ });
92
+ const data = await res.json();
93
+ if (!res.ok) return { content: data.error?.message || 'Model API error', usage: {} };
94
+ return {
95
+ content: data.content?.map(b => b.text || '').join('') || '',
96
+ usage: data.usage || {},
97
+ stopReason: data.stop_reason,
98
+ toolCalls: data.content?.filter(b => b.type === 'tool_use') || [],
99
+ };
100
+ } catch (err) {
101
+ return { content: `Error calling model: ${err.message}`, usage: {} };
102
+ }
103
+ }
104
+
105
+ async function callOpenAICompatibleModel({ provider, model, messages, tools, maxTokens, apiKey, baseUrl, fetchImpl }) {
106
+ const resolvedKey = apiKey || process.env.AZURE_API_KEY || process.env.OPENAI_API_KEY || process.env.KRADLE_ASSISTANT_API_KEY;
107
+ const resolvedBase = baseUrl || process.env.KRADLE_ASSISTANT_BASE_URL || process.env.AGENT_MUX_API_BASE;
108
+
109
+ if (!resolvedKey) {
110
+ return { content: `API key not configured for ${provider}. Set AZURE_API_KEY, OPENAI_API_KEY, or KRADLE_ASSISTANT_API_KEY.`, usage: {} };
111
+ }
112
+ if (!resolvedBase) {
113
+ return { content: `Base URL not configured for ${provider}. Set KRADLE_ASSISTANT_BASE_URL or AGENT_MUX_API_BASE.`, usage: {} };
114
+ }
115
+
116
+ const endpoint = resolvedBase.replace(/\/$/, '') + '/openai/deployments/' + (model || 'gpt-5.5') + '/chat/completions?api-version=2024-12-01-preview';
117
+
118
+ const body = {
119
+ messages,
120
+ max_tokens: maxTokens || 4096,
121
+ ...(tools?.length ? { tools: tools.map(t => ({ type: 'function', function: t })) } : {}),
122
+ };
123
+
124
+ const doFetch = fetchImpl || globalThis.fetch;
125
+
126
+ try {
127
+ const res = await doFetch(endpoint, {
128
+ method: 'POST',
129
+ headers: {
130
+ 'Content-Type': 'application/json',
131
+ 'api-key': resolvedKey,
132
+ },
133
+ body: JSON.stringify(body),
134
+ });
135
+ const data = await res.json();
136
+ if (!res.ok) return { content: data.error?.message || `${provider} API error: ${res.status}`, usage: {} };
137
+ const choice = data.choices?.[0]?.message;
138
+ return {
139
+ content: choice?.content || '',
140
+ usage: data.usage ? { input_tokens: data.usage.prompt_tokens, output_tokens: data.usage.completion_tokens } : {},
141
+ stopReason: data.choices?.[0]?.finish_reason,
142
+ toolCalls: choice?.tool_calls || [],
143
+ };
144
+ } catch (err) {
145
+ return { content: `Error calling ${provider} model: ${err.message}`, usage: {} };
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Create an in-process assistant runtime that reads config from AgentStack CRDs,
151
+ * manages chat sessions with message history, and supports structured agentic calls.
152
+ *
153
+ * @param {{ stackName?: string, apiKey?: string, fetchImpl?: Function }} options
154
+ * @returns {object} Assistant runtime object
155
+ */
156
+ export function createAssistantRuntime(options = {}) {
157
+ const stackName = options.stackName || 'assistant';
158
+ const sessions = new Map();
159
+
160
+ return {
161
+ role: 'assistant-runtime',
162
+
163
+ /**
164
+ * Resolve stack config from a controller's AgentStack CRD, falling back to defaults.
165
+ * @param {object} controller - Object with getResource(kind, name) method
166
+ * @param {string} [stackNameOverride]
167
+ * @returns {Promise<object>}
168
+ */
169
+ async resolveConfig(controller, stackNameOverride) {
170
+ const name = stackNameOverride || stackName;
171
+ if (!controller) return defaultAssistantConfig();
172
+ try {
173
+ const result = await controller.getResource('AgentStack', name);
174
+ return result.resource?.spec || defaultAssistantConfig();
175
+ } catch {
176
+ return defaultAssistantConfig();
177
+ }
178
+ },
179
+
180
+ /**
181
+ * Create a new chat session.
182
+ * @param {string} [sessionId] - Optional session ID; auto-generated if omitted.
183
+ * @param {string} [stackRef] - Stack name reference; defaults to the runtime's stackName.
184
+ * @returns {object} Session object with id, messages, createdAt, stackRef, status.
185
+ */
186
+ createSession(sessionId, stackRef) {
187
+ const session = {
188
+ id: sessionId || randomUUID(),
189
+ messages: [],
190
+ createdAt: new Date().toISOString(),
191
+ stackRef: stackRef || stackName,
192
+ status: 'active',
193
+ };
194
+ sessions.set(session.id, session);
195
+ return session;
196
+ },
197
+
198
+ /**
199
+ * Send a message in a session and receive the assistant's response.
200
+ * @param {string} sessionId
201
+ * @param {string} message
202
+ * @param {{ controller?: object, tools?: object[], maxTokens?: number }} opts
203
+ * @returns {Promise<object>} Model response with content, usage, stopReason, toolCalls.
204
+ */
205
+ async chat(sessionId, message, opts = {}) {
206
+ const session = sessions.get(sessionId);
207
+ if (!session) throw new Error(`Session ${sessionId} not found`);
208
+
209
+ session.messages.push({ role: 'user', content: message, timestamp: new Date().toISOString() });
210
+
211
+ const config = await this.resolveConfig(opts.controller, session.stackRef);
212
+ const systemPromptText = config.systemPrompt || defaultSystemPrompt();
213
+
214
+ const response = await callModel({
215
+ provider: config.provider || 'anthropic',
216
+ model: config.model || 'claude-sonnet-4-20250514',
217
+ messages: [
218
+ { role: 'system', content: systemPromptText },
219
+ ...session.messages.map(m => ({ role: m.role, content: m.content })),
220
+ ],
221
+ tools: opts.tools || [],
222
+ maxTokens: opts.maxTokens || 4096,
223
+ apiKey: options.apiKey,
224
+ fetchImpl: options.fetchImpl,
225
+ });
226
+
227
+ session.messages.push({ role: 'assistant', content: response.content, timestamp: new Date().toISOString() });
228
+ return response;
229
+ },
230
+
231
+ /**
232
+ * Get a session by ID.
233
+ * @param {string} sessionId
234
+ * @returns {object|null}
235
+ */
236
+ getSession(sessionId) {
237
+ return sessions.get(sessionId) || null;
238
+ },
239
+
240
+ /**
241
+ * List all active sessions.
242
+ * @returns {object[]}
243
+ */
244
+ listSessions() {
245
+ return [...sessions.values()];
246
+ },
247
+
248
+ /**
249
+ * Delete a session by ID.
250
+ * @param {string} sessionId
251
+ * @returns {boolean}
252
+ */
253
+ deleteSession(sessionId) {
254
+ return sessions.delete(sessionId);
255
+ },
256
+
257
+ /**
258
+ * Structured agentic call (non-chat, single-shot).
259
+ * @param {string|object} task - Task description or structured task object.
260
+ * @param {{ controller?: object, stackRef?: string, systemPrompt?: string, tools?: object[], maxTokens?: number, responseFormat?: string }} opts
261
+ * @returns {Promise<object>} Model response.
262
+ */
263
+ async structuredCall(task, opts = {}) {
264
+ const config = await this.resolveConfig(opts.controller, opts.stackRef);
265
+ const systemPromptText = opts.systemPrompt || config.systemPrompt || defaultSystemPrompt();
266
+
267
+ const response = await callModel({
268
+ provider: config.provider || 'anthropic',
269
+ model: config.model || 'claude-sonnet-4-20250514',
270
+ messages: [
271
+ { role: 'system', content: systemPromptText },
272
+ { role: 'user', content: typeof task === 'string' ? task : JSON.stringify(task) },
273
+ ],
274
+ tools: opts.tools || [],
275
+ maxTokens: opts.maxTokens || 8192,
276
+ responseFormat: opts.responseFormat,
277
+ apiKey: options.apiKey,
278
+ fetchImpl: options.fetchImpl,
279
+ });
280
+
281
+ return response;
282
+ },
283
+ };
284
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Async controller utilities for event batching, retry policies, delivery queues,
3
+ * and checkpoint persistence.
4
+ */
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Event batcher
8
+ // ---------------------------------------------------------------------------
9
+
10
+ /**
11
+ * Accumulates events and flushes them in batches either when the batch is full
12
+ * or when the flush interval expires.
13
+ *
14
+ * @param {(events: any[]) => void | Promise<void>} handler - Called with each flushed batch.
15
+ * @param {{ maxBatchSize?: number, flushIntervalMs?: number }} [options]
16
+ * @returns {{ push(event: any): void, flush(): Promise<void>, stop(): void }}
17
+ */
18
+ export function createEventBatcher(handler, { maxBatchSize = 50, flushIntervalMs = 1000 } = {}) {
19
+ let batch = [];
20
+ let timer = null;
21
+
22
+ function scheduleFlush() {
23
+ if (timer !== null) return;
24
+ timer = setTimeout(async () => {
25
+ timer = null;
26
+ await flushNow();
27
+ }, flushIntervalMs);
28
+ }
29
+
30
+ async function flushNow() {
31
+ if (batch.length === 0) return;
32
+ const toFlush = batch;
33
+ batch = [];
34
+ await handler(toFlush);
35
+ }
36
+
37
+ return {
38
+ push(event) {
39
+ batch.push(event);
40
+ if (batch.length >= maxBatchSize) {
41
+ if (timer !== null) {
42
+ clearTimeout(timer);
43
+ timer = null;
44
+ }
45
+ // Fire-and-forget the synchronous portion; handler may return a Promise
46
+ const toFlush = batch;
47
+ batch = [];
48
+ Promise.resolve(handler(toFlush)).catch(() => {});
49
+ } else {
50
+ scheduleFlush();
51
+ }
52
+ },
53
+ async flush() {
54
+ if (timer !== null) {
55
+ clearTimeout(timer);
56
+ timer = null;
57
+ }
58
+ await flushNow();
59
+ },
60
+ stop() {
61
+ if (timer !== null) {
62
+ clearTimeout(timer);
63
+ timer = null;
64
+ }
65
+ batch = [];
66
+ },
67
+ };
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Retry policy
72
+ // ---------------------------------------------------------------------------
73
+
74
+ /**
75
+ * Creates a retry policy with exponential backoff and optional jitter.
76
+ *
77
+ * @param {{ maxRetries?: number, baseDelayMs?: number, maxDelayMs?: number, jitter?: boolean }} [options]
78
+ * @returns {{ shouldRetry(attempt: number, error: any): boolean, getDelay(attempt: number): number }}
79
+ */
80
+ export function createRetryPolicy({ maxRetries = 3, baseDelayMs = 1000, maxDelayMs = 30000, jitter = true } = {}) {
81
+ return {
82
+ /**
83
+ * Returns true if another attempt should be made.
84
+ * @param {number} attempt - 0-based number of the attempt that just failed.
85
+ */
86
+ shouldRetry(attempt, _error) {
87
+ return attempt < maxRetries;
88
+ },
89
+ /**
90
+ * Returns the delay in ms to wait before the next attempt.
91
+ * @param {number} attempt - 0-based number of the attempt that just failed.
92
+ */
93
+ getDelay(attempt) {
94
+ const exponential = baseDelayMs * Math.pow(2, attempt);
95
+ const capped = Math.min(exponential, maxDelayMs);
96
+ if (!jitter) return capped;
97
+ // Full-jitter: random value in [0, capped]
98
+ return Math.floor(Math.random() * (capped + 1));
99
+ },
100
+ };
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Delivery queue
105
+ // ---------------------------------------------------------------------------
106
+
107
+ /**
108
+ * In-memory ordered queue with configurable concurrency and optional retry support.
109
+ *
110
+ * @param {(item: any) => Promise<void>} processor - Called for each dequeued item.
111
+ * @param {{ concurrency?: number, retryPolicy?: ReturnType<typeof createRetryPolicy> }} [options]
112
+ * @returns {{ enqueue(item: any): void, drain(): Promise<void>, size(): number, stop(): void }}
113
+ */
114
+ export function createDeliveryQueue(processor, { concurrency = 5, retryPolicy } = {}) {
115
+ const queue = [];
116
+ let active = 0;
117
+ let stopped = false;
118
+ /** @type {Array<() => void>} */
119
+ let drainResolvers = [];
120
+
121
+ function checkDrain() {
122
+ if (active === 0 && queue.length === 0) {
123
+ for (const resolve of drainResolvers) resolve();
124
+ drainResolvers = [];
125
+ }
126
+ }
127
+
128
+ async function processItem(item) {
129
+ let attempt = 0;
130
+ while (true) {
131
+ try {
132
+ await processor(item);
133
+ return;
134
+ } catch (err) {
135
+ if (retryPolicy && retryPolicy.shouldRetry(attempt, err)) {
136
+ const delay = retryPolicy.getDelay(attempt);
137
+ attempt++;
138
+ if (delay > 0) await new Promise((r) => setTimeout(r, delay));
139
+ } else {
140
+ // Swallow the error; callers can handle via processor rejections externally
141
+ return;
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ function tick() {
148
+ while (!stopped && queue.length > 0 && active < concurrency) {
149
+ const item = queue.shift();
150
+ active++;
151
+ processItem(item).finally(() => {
152
+ active--;
153
+ tick();
154
+ checkDrain();
155
+ });
156
+ }
157
+ if (!stopped) checkDrain();
158
+ }
159
+
160
+ return {
161
+ enqueue(item) {
162
+ if (stopped) return;
163
+ queue.push(item);
164
+ tick();
165
+ },
166
+ drain() {
167
+ if (active === 0 && queue.length === 0) return Promise.resolve();
168
+ return new Promise((resolve) => drainResolvers.push(resolve));
169
+ },
170
+ size() {
171
+ return queue.length + active;
172
+ },
173
+ stop() {
174
+ stopped = true;
175
+ queue.length = 0;
176
+ for (const resolve of drainResolvers) resolve();
177
+ drainResolvers = [];
178
+ },
179
+ };
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Checkpointer
184
+ // ---------------------------------------------------------------------------
185
+
186
+ /**
187
+ * Simple key-value checkpoint persistence backed by any Map-like storage.
188
+ *
189
+ * @param {Map<string, any>} [storage]
190
+ * @returns {{ save(key: string, value: any): void, load(key: string): any, clear(key: string): void, listKeys(): string[] }}
191
+ */
192
+ export function createCheckpointer(storage = new Map()) {
193
+ return {
194
+ save(key, value) {
195
+ storage.set(key, value);
196
+ },
197
+ load(key) {
198
+ return storage.has(key) ? storage.get(key) : undefined;
199
+ },
200
+ clear(key) {
201
+ storage.delete(key);
202
+ },
203
+ listKeys() {
204
+ return Array.from(storage.keys());
205
+ },
206
+ };
207
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Audit Controller — Org-scoped audit log, event streaming, smart polling with
3
+ * exponential backoff, replay on reconnect, and metrics aggregation.
4
+ *
5
+ * @module audit-controller
6
+ */
7
+
8
+ export const AUDIT_CONTROLLER_BOUNDARY = {
9
+ role: 'audit-controller',
10
+ scope: 'Org-scoped audit log — event recording, streaming, replay, metrics',
11
+ owns: ['audit events', 'event streaming', 'event polling', 'audit metrics'],
12
+ delegatesTo: [],
13
+ mustNotOwn: ['identity management', 'resource storage', 'git operations'],
14
+ };
15
+
16
+ // ─── AuditController ─────────────────────────────────────────────────────────
17
+
18
+ /**
19
+ * Create an in-memory audit controller.
20
+ *
21
+ * @returns {{
22
+ * log: Function,
23
+ * query: Function,
24
+ * getStream: Function,
25
+ * getMetrics: Function,
26
+ * }}
27
+ */
28
+ export function createAuditController() {
29
+ /** @type {Array<AuditEvent>} */
30
+ const store = [];
31
+ let seq = 0;
32
+
33
+ return {
34
+ role: 'audit-controller',
35
+
36
+ /**
37
+ * Record an audit event.
38
+ *
39
+ * @param {{ org: string, actor?: string, action: string, resource?: object, timestamp?: string }} params
40
+ * @returns {AuditEvent}
41
+ */
42
+ log({ org, actor = 'system', action, resource = {}, timestamp } = {}) {
43
+ if (!org || typeof org !== 'string') {
44
+ throw new Error('audit.log: org is required');
45
+ }
46
+ if (!action || typeof action !== 'string') {
47
+ throw new Error('audit.log: action is required');
48
+ }
49
+
50
+ const event = {
51
+ id: ++seq,
52
+ org,
53
+ actor,
54
+ action,
55
+ resource: Object.assign({}, resource),
56
+ timestamp: timestamp || new Date().toISOString(),
57
+ };
58
+
59
+ store.push(event);
60
+ return Object.assign({}, event);
61
+ },
62
+
63
+ /**
64
+ * Query audit events with filtering and pagination.
65
+ *
66
+ * @param {{ org?: string, action?: string, since?: string, until?: string, limit?: number, offset?: number }} params
67
+ * @returns {{ events: AuditEvent[], total: number }}
68
+ */
69
+ query({ org, action, since, until, limit, offset = 0 } = {}) {
70
+ let filtered = store.slice();
71
+
72
+ if (org) filtered = filtered.filter(e => e.org === org);
73
+ if (action) filtered = filtered.filter(e => e.action === action);
74
+
75
+ if (since) {
76
+ const sinceMs = new Date(since).getTime();
77
+ filtered = filtered.filter(e => new Date(e.timestamp).getTime() >= sinceMs);
78
+ }
79
+ if (until) {
80
+ const untilMs = new Date(until).getTime();
81
+ filtered = filtered.filter(e => new Date(e.timestamp).getTime() <= untilMs);
82
+ }
83
+
84
+ // reverse chronological
85
+ filtered = filtered.slice().sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
86
+
87
+ const total = filtered.length;
88
+ filtered = filtered.slice(offset);
89
+ if (limit != null) filtered = filtered.slice(0, limit);
90
+
91
+ return { events: filtered.map(e => Object.assign({}, e)), total };
92
+ },
93
+
94
+ /**
95
+ * Return all events after the given sequence number for a given org (event replay).
96
+ *
97
+ * @param {{ org?: string, afterSeq: number }} params
98
+ * @returns {{ events: AuditEvent[], lastSeq: number }}
99
+ */
100
+ getStream({ org, afterSeq = 0 } = {}) {
101
+ let filtered = store.filter(e => e.id > afterSeq);
102
+ if (org) filtered = filtered.filter(e => e.org === org);
103
+ // chronological order for stream replay
104
+ filtered = filtered.slice().sort((a, b) => a.id - b.id);
105
+ return {
106
+ events: filtered.map(e => Object.assign({}, e)),
107
+ lastSeq: filtered.length > 0 ? filtered[filtered.length - 1].id : afterSeq,
108
+ };
109
+ },
110
+
111
+ /**
112
+ * Aggregate audit metrics for an org.
113
+ *
114
+ * @param {{ org?: string }} params
115
+ * @returns {{ byAction: object, byOrg: object, byHour: object, total: number }}
116
+ */
117
+ getMetrics({ org } = {}) {
118
+ let events = store.slice();
119
+ if (org) events = events.filter(e => e.org === org);
120
+
121
+ const byAction = {};
122
+ const byOrg = {};
123
+ const byHour = {};
124
+
125
+ for (const event of events) {
126
+ byAction[event.action] = (byAction[event.action] || 0) + 1;
127
+ byOrg[event.org] = (byOrg[event.org] || 0) + 1;
128
+ // hour key: "2026-05-13T10" (drop minutes/seconds)
129
+ const hourKey = event.timestamp.slice(0, 13);
130
+ byHour[hourKey] = (byHour[hourKey] || 0) + 1;
131
+ }
132
+
133
+ return { byAction, byOrg, byHour, total: events.length };
134
+ },
135
+ };
136
+ }
137
+
138
+ // ─── EventPoller ─────────────────────────────────────────────────────────────
139
+
140
+ /**
141
+ * Create a smart event poller with exponential backoff.
142
+ *
143
+ * When polls return no new events the backoff interval doubles (up to maxBackoff).
144
+ * When new events arrive the backoff resets to initialBackoff.
145
+ *
146
+ * @param {{ controller: object, org?: string, initialBackoff?: number, maxBackoff?: number }} options
147
+ * @returns {{ poll: Function, getBackoff: Function, reset: Function }}
148
+ */
149
+ export function createEventPoller({ controller, org, initialBackoff = 1000, maxBackoff = 30000 } = {}) {
150
+ let lastSeq = 0;
151
+ let currentBackoff = initialBackoff;
152
+
153
+ return {
154
+ /**
155
+ * Poll for new events. Updates backoff state.
156
+ * @returns {{ events: AuditEvent[], lastSeq: number }}
157
+ */
158
+ poll() {
159
+ const result = controller.getStream({ org, afterSeq: lastSeq });
160
+ if (result.events.length > 0) {
161
+ // New events — reset backoff and advance cursor
162
+ lastSeq = result.lastSeq;
163
+ currentBackoff = initialBackoff;
164
+ } else {
165
+ // No new events — double the backoff, capped at maxBackoff
166
+ currentBackoff = Math.min(currentBackoff * 2, maxBackoff);
167
+ }
168
+ return result;
169
+ },
170
+
171
+ /**
172
+ * Get the current backoff interval in milliseconds.
173
+ * @returns {number}
174
+ */
175
+ getBackoff() {
176
+ return currentBackoff;
177
+ },
178
+
179
+ /**
180
+ * Reset the poller cursor and backoff to their initial states.
181
+ */
182
+ reset() {
183
+ lastSeq = 0;
184
+ currentBackoff = initialBackoff;
185
+ },
186
+ };
187
+ }
188
+
189
+ /**
190
+ * @typedef {{ id: number, org: string, actor: string, action: string, resource: object, timestamp: string }} AuditEvent
191
+ */