@kb-labs/adapters 0.5.0

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 (276) hide show
  1. package/.cursorrules +32 -0
  2. package/.github/workflows/ci.yml +13 -0
  3. package/.github/workflows/deploy.yml +28 -0
  4. package/.github/workflows/docker-build.yml +25 -0
  5. package/.github/workflows/drift-check.yml +10 -0
  6. package/.github/workflows/profiles-validate.yml +16 -0
  7. package/.github/workflows/release.yml +8 -0
  8. package/.kb/devkit/agents/devkit-maintainer/context.globs +15 -0
  9. package/.kb/devkit/agents/devkit-maintainer/permissions.yml +17 -0
  10. package/.kb/devkit/agents/devkit-maintainer/prompt.md +28 -0
  11. package/.kb/devkit/agents/devkit-maintainer/runbook.md +31 -0
  12. package/.kb/devkit/agents/docs-crafter/prompt.md +24 -0
  13. package/.kb/devkit/agents/docs-crafter/runbook.md +18 -0
  14. package/.kb/devkit/agents/release-manager/context.globs +7 -0
  15. package/.kb/devkit/agents/release-manager/prompt.md +27 -0
  16. package/.kb/devkit/agents/release-manager/runbook.md +17 -0
  17. package/.kb/devkit/agents/test-generator/context.globs +7 -0
  18. package/.kb/devkit/agents/test-generator/prompt.md +27 -0
  19. package/.kb/devkit/agents/test-generator/runbook.md +18 -0
  20. package/CONTRIBUTING.md +90 -0
  21. package/IMPLEMENTATION_COMPLETE.md +416 -0
  22. package/LICENSE +186 -0
  23. package/README-TEMPLATE.md +179 -0
  24. package/README.md +306 -0
  25. package/docs/DOCUMENTATION.md +74 -0
  26. package/docs/adr/0000-template.md +49 -0
  27. package/docs/adr/0001-architecture-and-repository-layout.md +33 -0
  28. package/docs/adr/0002-plugins-and-extensibility.md +46 -0
  29. package/docs/adr/0003-package-and-module-boundaries.md +37 -0
  30. package/docs/adr/0004-versioning-and-release-policy.md +38 -0
  31. package/docs/adr/0005-use-devkit-for-shared-tooling.md +48 -0
  32. package/docs/adr/0006-adopt-devkit-sync.md +47 -0
  33. package/docs/adr/0007-drift-kit-check.md +72 -0
  34. package/docs/adr/0008-devkit-sync-wrapper-strategy.md +67 -0
  35. package/docs/naming-convention.md +272 -0
  36. package/eslint.config.js +27 -0
  37. package/kb-labs.config.json +5 -0
  38. package/package.json +84 -0
  39. package/package.json.bin +25 -0
  40. package/package.json.lib +30 -0
  41. package/packages/adapters-analytics-duckdb/package.json +54 -0
  42. package/packages/adapters-analytics-duckdb/scripts/migrate-from-jsonl.mjs +253 -0
  43. package/packages/adapters-analytics-duckdb/src/index.ts +380 -0
  44. package/packages/adapters-analytics-duckdb/src/manifest.ts +36 -0
  45. package/packages/adapters-analytics-duckdb/src/schema.ts +161 -0
  46. package/packages/adapters-analytics-duckdb/tsconfig.build.json +15 -0
  47. package/packages/adapters-analytics-duckdb/tsconfig.json +9 -0
  48. package/packages/adapters-analytics-duckdb/tsup.config.ts +9 -0
  49. package/packages/adapters-analytics-file/README.md +32 -0
  50. package/packages/adapters-analytics-file/eslint.config.js +27 -0
  51. package/packages/adapters-analytics-file/package.json +50 -0
  52. package/packages/adapters-analytics-file/src/__tests__/daily-stats.spec.ts +287 -0
  53. package/packages/adapters-analytics-file/src/__tests__/scoped-analytics.test.ts +233 -0
  54. package/packages/adapters-analytics-file/src/index.test.ts +214 -0
  55. package/packages/adapters-analytics-file/src/index.ts +830 -0
  56. package/packages/adapters-analytics-file/src/manifest.ts +45 -0
  57. package/packages/adapters-analytics-file/tsconfig.build.json +15 -0
  58. package/packages/adapters-analytics-file/tsconfig.json +9 -0
  59. package/packages/adapters-analytics-file/tsup.config.ts +9 -0
  60. package/packages/adapters-analytics-sqlite/package.json +55 -0
  61. package/packages/adapters-analytics-sqlite/scripts/migrate-from-jsonl.mjs +194 -0
  62. package/packages/adapters-analytics-sqlite/src/index.ts +460 -0
  63. package/packages/adapters-analytics-sqlite/src/manifest.ts +41 -0
  64. package/packages/adapters-analytics-sqlite/tsconfig.build.json +15 -0
  65. package/packages/adapters-analytics-sqlite/tsconfig.json +9 -0
  66. package/packages/adapters-analytics-sqlite/tsup.config.ts +9 -0
  67. package/packages/adapters-environment-docker/README.md +28 -0
  68. package/packages/adapters-environment-docker/eslint.config.js +5 -0
  69. package/packages/adapters-environment-docker/package.json +49 -0
  70. package/packages/adapters-environment-docker/src/index.test.ts +138 -0
  71. package/packages/adapters-environment-docker/src/index.ts +439 -0
  72. package/packages/adapters-environment-docker/src/manifest.ts +65 -0
  73. package/packages/adapters-environment-docker/tsconfig.build.json +15 -0
  74. package/packages/adapters-environment-docker/tsconfig.json +16 -0
  75. package/packages/adapters-environment-docker/tsup.config.ts +9 -0
  76. package/packages/adapters-eventbus-cache/README.md +242 -0
  77. package/packages/adapters-eventbus-cache/eslint.config.js +27 -0
  78. package/packages/adapters-eventbus-cache/package.json +46 -0
  79. package/packages/adapters-eventbus-cache/src/index.test.ts +235 -0
  80. package/packages/adapters-eventbus-cache/src/index.ts +215 -0
  81. package/packages/adapters-eventbus-cache/src/manifest.ts +50 -0
  82. package/packages/adapters-eventbus-cache/src/types.ts +58 -0
  83. package/packages/adapters-eventbus-cache/tsconfig.build.json +15 -0
  84. package/packages/adapters-eventbus-cache/tsconfig.json +9 -0
  85. package/packages/adapters-eventbus-cache/tsup.config.ts +9 -0
  86. package/packages/adapters-fs/README.md +171 -0
  87. package/packages/adapters-fs/allowed.txt +1 -0
  88. package/packages/adapters-fs/conflict.txt +1 -0
  89. package/packages/adapters-fs/dest.txt +1 -0
  90. package/packages/adapters-fs/eslint.config.js +27 -0
  91. package/packages/adapters-fs/exists.txt +1 -0
  92. package/packages/adapters-fs/not-allowed.txt +1 -0
  93. package/packages/adapters-fs/other.txt +1 -0
  94. package/packages/adapters-fs/package.json +55 -0
  95. package/packages/adapters-fs/public/file1.txt +1 -0
  96. package/packages/adapters-fs/public/file2.txt +1 -0
  97. package/packages/adapters-fs/secret.txt +1 -0
  98. package/packages/adapters-fs/secrets/key.txt +1 -0
  99. package/packages/adapters-fs/src/index.test.ts +243 -0
  100. package/packages/adapters-fs/src/index.ts +258 -0
  101. package/packages/adapters-fs/src/manifest.ts +35 -0
  102. package/packages/adapters-fs/src/secure-storage.test.ts +380 -0
  103. package/packages/adapters-fs/src/secure-storage.ts +268 -0
  104. package/packages/adapters-fs/test.json +1 -0
  105. package/packages/adapters-fs/test.txt +1 -0
  106. package/packages/adapters-fs/test.xyz +1 -0
  107. package/packages/adapters-fs/test1.txt +1 -0
  108. package/packages/adapters-fs/test2.txt +1 -0
  109. package/packages/adapters-fs/tsconfig.build.json +15 -0
  110. package/packages/adapters-fs/tsconfig.json +9 -0
  111. package/packages/adapters-fs/tsup.config.ts +8 -0
  112. package/packages/adapters-fs/vitest.config.ts +19 -0
  113. package/packages/adapters-log-ringbuffer/README.md +228 -0
  114. package/packages/adapters-log-ringbuffer/eslint.config.js +27 -0
  115. package/packages/adapters-log-ringbuffer/package.json +47 -0
  116. package/packages/adapters-log-ringbuffer/src/__tests__/ring-buffer.test.ts +450 -0
  117. package/packages/adapters-log-ringbuffer/src/index.ts +212 -0
  118. package/packages/adapters-log-ringbuffer/src/manifest.ts +30 -0
  119. package/packages/adapters-log-ringbuffer/tsconfig.build.json +15 -0
  120. package/packages/adapters-log-ringbuffer/tsconfig.json +9 -0
  121. package/packages/adapters-log-ringbuffer/tsup.config.ts +9 -0
  122. package/packages/adapters-log-ringbuffer/vitest.config.ts +14 -0
  123. package/packages/adapters-log-sqlite/README.md +396 -0
  124. package/packages/adapters-log-sqlite/eslint.config.js +27 -0
  125. package/packages/adapters-log-sqlite/package.json +49 -0
  126. package/packages/adapters-log-sqlite/src/__tests__/log-persistence.test.ts +718 -0
  127. package/packages/adapters-log-sqlite/src/index.ts +1068 -0
  128. package/packages/adapters-log-sqlite/src/manifest.ts +36 -0
  129. package/packages/adapters-log-sqlite/src/schema.sql +46 -0
  130. package/packages/adapters-log-sqlite/tsconfig.build.json +15 -0
  131. package/packages/adapters-log-sqlite/tsconfig.json +9 -0
  132. package/packages/adapters-log-sqlite/tsup.config.ts +9 -0
  133. package/packages/adapters-log-sqlite/vitest.config.ts +15 -0
  134. package/packages/adapters-mongodb/README.md +147 -0
  135. package/packages/adapters-mongodb/eslint.config.js +27 -0
  136. package/packages/adapters-mongodb/package.json +53 -0
  137. package/packages/adapters-mongodb/src/index.ts +428 -0
  138. package/packages/adapters-mongodb/src/manifest.ts +45 -0
  139. package/packages/adapters-mongodb/src/secure-document.ts +231 -0
  140. package/packages/adapters-mongodb/tsconfig.build.json +15 -0
  141. package/packages/adapters-mongodb/tsconfig.json +9 -0
  142. package/packages/adapters-mongodb/tsup.config.ts +8 -0
  143. package/packages/adapters-openai/README.md +151 -0
  144. package/packages/adapters-openai/embeddings.ts +37 -0
  145. package/packages/adapters-openai/eslint.config.js +26 -0
  146. package/packages/adapters-openai/index.ts +22 -0
  147. package/packages/adapters-openai/package.json +57 -0
  148. package/packages/adapters-openai/src/embeddings-manifest.ts +45 -0
  149. package/packages/adapters-openai/src/embeddings.ts +104 -0
  150. package/packages/adapters-openai/src/index.ts +13 -0
  151. package/packages/adapters-openai/src/llm.ts +304 -0
  152. package/packages/adapters-openai/src/manifest.ts +47 -0
  153. package/packages/adapters-openai/tsconfig.build.json +15 -0
  154. package/packages/adapters-openai/tsconfig.json +9 -0
  155. package/packages/adapters-openai/tsup.config.ts +8 -0
  156. package/packages/adapters-pino/README.md +152 -0
  157. package/packages/adapters-pino/eslint.config.js +27 -0
  158. package/packages/adapters-pino/package.json +49 -0
  159. package/packages/adapters-pino/src/index.test.ts +44 -0
  160. package/packages/adapters-pino/src/index.ts +322 -0
  161. package/packages/adapters-pino/src/log-ring-buffer.ts +142 -0
  162. package/packages/adapters-pino/src/manifest.ts +49 -0
  163. package/packages/adapters-pino/tsconfig.build.json +15 -0
  164. package/packages/adapters-pino/tsconfig.json +9 -0
  165. package/packages/adapters-pino/tsup.config.ts +9 -0
  166. package/packages/adapters-pino-http/README.md +141 -0
  167. package/packages/adapters-pino-http/eslint.config.js +27 -0
  168. package/packages/adapters-pino-http/package.json +46 -0
  169. package/packages/adapters-pino-http/src/index.ts +229 -0
  170. package/packages/adapters-pino-http/tsconfig.build.json +15 -0
  171. package/packages/adapters-pino-http/tsconfig.json +9 -0
  172. package/packages/adapters-pino-http/tsup.config.ts +9 -0
  173. package/packages/adapters-qdrant/README.md +166 -0
  174. package/packages/adapters-qdrant/eslint.config.js +27 -0
  175. package/packages/adapters-qdrant/package.json +49 -0
  176. package/packages/adapters-qdrant/src/index.ts +490 -0
  177. package/packages/adapters-qdrant/src/manifest.ts +54 -0
  178. package/packages/adapters-qdrant/src/retry.ts +204 -0
  179. package/packages/adapters-qdrant/tsconfig.build.json +15 -0
  180. package/packages/adapters-qdrant/tsconfig.json +9 -0
  181. package/packages/adapters-qdrant/tsup.config.ts +9 -0
  182. package/packages/adapters-redis/README.md +159 -0
  183. package/packages/adapters-redis/eslint.config.js +27 -0
  184. package/packages/adapters-redis/package.json +49 -0
  185. package/packages/adapters-redis/src/index.ts +164 -0
  186. package/packages/adapters-redis/src/manifest.ts +49 -0
  187. package/packages/adapters-redis/tsconfig.build.json +15 -0
  188. package/packages/adapters-redis/tsconfig.json +9 -0
  189. package/packages/adapters-redis/tsup.config.ts +9 -0
  190. package/packages/adapters-snapshot-localfs/README.md +10 -0
  191. package/packages/adapters-snapshot-localfs/eslint.config.js +2 -0
  192. package/packages/adapters-snapshot-localfs/package.json +46 -0
  193. package/packages/adapters-snapshot-localfs/src/index.test.ts +40 -0
  194. package/packages/adapters-snapshot-localfs/src/index.ts +292 -0
  195. package/packages/adapters-snapshot-localfs/src/manifest.ts +32 -0
  196. package/packages/adapters-snapshot-localfs/tsconfig.build.json +15 -0
  197. package/packages/adapters-snapshot-localfs/tsconfig.json +16 -0
  198. package/packages/adapters-snapshot-localfs/tsup.config.ts +11 -0
  199. package/packages/adapters-sqlite/README.md +163 -0
  200. package/packages/adapters-sqlite/eslint.config.js +27 -0
  201. package/packages/adapters-sqlite/package.json +54 -0
  202. package/packages/adapters-sqlite/src/index.test.ts +245 -0
  203. package/packages/adapters-sqlite/src/index.ts +382 -0
  204. package/packages/adapters-sqlite/src/manifest.ts +47 -0
  205. package/packages/adapters-sqlite/src/secure-sql.test.ts +290 -0
  206. package/packages/adapters-sqlite/src/secure-sql.ts +281 -0
  207. package/packages/adapters-sqlite/tsconfig.build.json +15 -0
  208. package/packages/adapters-sqlite/tsconfig.json +9 -0
  209. package/packages/adapters-sqlite/tsup.config.ts +8 -0
  210. package/packages/adapters-sqlite/vitest.config.ts +19 -0
  211. package/packages/adapters-transport/README.md +170 -0
  212. package/packages/adapters-transport/eslint.config.js +27 -0
  213. package/packages/adapters-transport/package.json +49 -0
  214. package/packages/adapters-transport/src/__tests__/unix-socket-server.test.ts +550 -0
  215. package/packages/adapters-transport/src/index.ts +101 -0
  216. package/packages/adapters-transport/src/ipc-transport.ts +228 -0
  217. package/packages/adapters-transport/src/transport.ts +224 -0
  218. package/packages/adapters-transport/src/types.ts +92 -0
  219. package/packages/adapters-transport/src/unix-socket-server.ts +193 -0
  220. package/packages/adapters-transport/src/unix-socket-transport.ts +280 -0
  221. package/packages/adapters-transport/tsconfig.build.json +15 -0
  222. package/packages/adapters-transport/tsconfig.json +9 -0
  223. package/packages/adapters-transport/tsup.config.ts +9 -0
  224. package/packages/adapters-vibeproxy/README.md +159 -0
  225. package/packages/adapters-vibeproxy/eslint.config.js +27 -0
  226. package/packages/adapters-vibeproxy/package.json +51 -0
  227. package/packages/adapters-vibeproxy/src/index.ts +13 -0
  228. package/packages/adapters-vibeproxy/src/llm.ts +437 -0
  229. package/packages/adapters-vibeproxy/src/manifest.ts +51 -0
  230. package/packages/adapters-vibeproxy/tsconfig.build.json +15 -0
  231. package/packages/adapters-vibeproxy/tsconfig.json +9 -0
  232. package/packages/adapters-vibeproxy/tsup.config.ts +8 -0
  233. package/packages/adapters-workspace-agent/package.json +46 -0
  234. package/packages/adapters-workspace-agent/src/__tests__/adapter.test.ts +212 -0
  235. package/packages/adapters-workspace-agent/src/index.ts +220 -0
  236. package/packages/adapters-workspace-agent/src/manifest.ts +36 -0
  237. package/packages/adapters-workspace-agent/tsconfig.build.json +15 -0
  238. package/packages/adapters-workspace-agent/tsconfig.json +16 -0
  239. package/packages/adapters-workspace-agent/tsup.config.ts +11 -0
  240. package/packages/adapters-workspace-localfs/README.md +9 -0
  241. package/packages/adapters-workspace-localfs/eslint.config.js +2 -0
  242. package/packages/adapters-workspace-localfs/package.json +46 -0
  243. package/packages/adapters-workspace-localfs/src/index.test.ts +27 -0
  244. package/packages/adapters-workspace-localfs/src/index.ts +172 -0
  245. package/packages/adapters-workspace-localfs/src/manifest.ts +32 -0
  246. package/packages/adapters-workspace-localfs/tsconfig.build.json +15 -0
  247. package/packages/adapters-workspace-localfs/tsconfig.json +16 -0
  248. package/packages/adapters-workspace-localfs/tsup.config.ts +11 -0
  249. package/packages/adapters-workspace-worktree/README.md +9 -0
  250. package/packages/adapters-workspace-worktree/eslint.config.js +2 -0
  251. package/packages/adapters-workspace-worktree/package.json +46 -0
  252. package/packages/adapters-workspace-worktree/src/index.test.ts +38 -0
  253. package/packages/adapters-workspace-worktree/src/index.ts +245 -0
  254. package/packages/adapters-workspace-worktree/src/manifest.ts +38 -0
  255. package/packages/adapters-workspace-worktree/tsconfig.build.json +15 -0
  256. package/packages/adapters-workspace-worktree/tsconfig.json +16 -0
  257. package/packages/adapters-workspace-worktree/tsup.config.ts +11 -0
  258. package/pnpm-workspace.yaml +2800 -0
  259. package/prettierrc.json +1 -0
  260. package/scripts/devkit-sync.mjs +37 -0
  261. package/scripts/hooks/post-push +9 -0
  262. package/scripts/hooks/pre-commit +9 -0
  263. package/scripts/hooks/pre-push +9 -0
  264. package/test-integration.ts +242 -0
  265. package/test.txt +1 -0
  266. package/tsconfig.base.json +6 -0
  267. package/tsconfig.build.json +15 -0
  268. package/tsconfig.json +9 -0
  269. package/tsconfig.paths.json +26 -0
  270. package/tsconfig.tools.json +17 -0
  271. package/tsup.config.bin.ts +34 -0
  272. package/tsup.config.cli.ts +41 -0
  273. package/tsup.config.dual.ts +46 -0
  274. package/tsup.config.ts +36 -0
  275. package/tsup.external.json +103 -0
  276. package/vitest.config.ts +2 -0
@@ -0,0 +1,138 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { DockerEnvironmentAdapter } from './index.js';
3
+
4
+ describe('DockerEnvironmentAdapter', () => {
5
+ it('creates environment via docker run with expected args', async () => {
6
+ const execDocker = vi.fn(async (args: string[]) => {
7
+ if (args[0] === 'run') {
8
+ return 'container-123\n';
9
+ }
10
+ throw new Error(`Unexpected command: ${args.join(' ')}`);
11
+ });
12
+
13
+ const adapter = new DockerEnvironmentAdapter({
14
+ defaultImage: 'node:20-alpine',
15
+ network: 'kb-net',
16
+ workspace: { cwd: '/repo' },
17
+ execDocker,
18
+ });
19
+
20
+ const env = await adapter.create({
21
+ runId: 'RUN_MAIN',
22
+ templateId: 'node-dev',
23
+ env: { A: '1', B: '2' },
24
+ command: ['sleep', '300'],
25
+ });
26
+
27
+ expect(env.environmentId).toBe('container-123');
28
+ expect(env.provider).toBe('docker-cli');
29
+ expect(env.status).toBe('ready');
30
+ expect(env.metadata?.image).toBe('node:20-alpine');
31
+
32
+ const runArgs = execDocker.mock.calls[0]?.[0] as string[];
33
+ expect(runArgs[0]).toBe('run');
34
+ expect(runArgs).toContain('-d');
35
+ expect(runArgs).toContain('--rm');
36
+ expect(runArgs).toContain('--network');
37
+ expect(runArgs).toContain('kb-net');
38
+ expect(runArgs).toContain('-e');
39
+ expect(runArgs).toContain('A=1');
40
+ expect(runArgs).toContain('B=2');
41
+ expect(runArgs).toContain('-v');
42
+ expect(runArgs).toContain('/repo:/workspace');
43
+ expect(runArgs).toContain('-w');
44
+ expect(runArgs).toContain('/workspace');
45
+ expect(runArgs).toContain('node:20-alpine');
46
+ expect(runArgs.slice(-2)).toEqual(['sleep', '300']);
47
+ });
48
+
49
+ it('maps docker running state to ready', async () => {
50
+ const execDocker = vi.fn(async (args: string[]) => {
51
+ if (args[0] === 'inspect') {
52
+ return 'running\n';
53
+ }
54
+ throw new Error(`Unexpected command: ${args.join(' ')}`);
55
+ });
56
+
57
+ const adapter = new DockerEnvironmentAdapter({ execDocker });
58
+ const status = await adapter.getStatus('env-1');
59
+
60
+ expect(status.environmentId).toBe('env-1');
61
+ expect(status.status).toBe('ready');
62
+ expect(status.reason).toBe('running');
63
+ });
64
+
65
+ it('uses runtimeCommand when request command is omitted', async () => {
66
+ const execDocker = vi.fn(async (args: string[]) => {
67
+ if (args[0] === 'run') {
68
+ return 'container-keepalive\n';
69
+ }
70
+ throw new Error(`Unexpected command: ${args.join(' ')}`);
71
+ });
72
+
73
+ const adapter = new DockerEnvironmentAdapter({
74
+ execDocker,
75
+ runtimeCommand: ['node', '/app/dist/cli.cjs'],
76
+ });
77
+ await adapter.create({ runId: 'keepalive' });
78
+
79
+ const runArgs = execDocker.mock.calls[0]?.[0] as string[];
80
+ expect(runArgs.slice(-2)).toEqual(['node', '/app/dist/cli.cjs']);
81
+ });
82
+
83
+ it('uses image default when no command and no runtimeCommand', async () => {
84
+ const execDocker = vi.fn(async (args: string[]) => {
85
+ if (args[0] === 'run') {
86
+ return 'container-default\n';
87
+ }
88
+ throw new Error(`Unexpected command: ${args.join(' ')}`);
89
+ });
90
+
91
+ const adapter = new DockerEnvironmentAdapter({ execDocker });
92
+ await adapter.create({ runId: 'keepalive' });
93
+
94
+ const runArgs = execDocker.mock.calls[0]?.[0] as string[];
95
+ // Last arg is the image, no command appended
96
+ expect(runArgs[runArgs.length - 1]).toBe('node:20-alpine');
97
+ });
98
+
99
+ it('returns terminated for missing containers on status', async () => {
100
+ const execDocker = vi.fn(async () => {
101
+ throw new Error('Error response from daemon: No such container: env-missing');
102
+ });
103
+
104
+ const adapter = new DockerEnvironmentAdapter({ execDocker });
105
+ const status = await adapter.getStatus('env-missing');
106
+
107
+ expect(status.status).toBe('terminated');
108
+ expect(status.reason).toBe('container_not_found');
109
+ });
110
+
111
+ it('destroy is idempotent for missing containers', async () => {
112
+ const execDocker = vi.fn(async () => {
113
+ throw new Error('No such container: env-missing');
114
+ });
115
+
116
+ const adapter = new DockerEnvironmentAdapter({ execDocker });
117
+ await expect(adapter.destroy('env-missing')).resolves.toBeUndefined();
118
+ });
119
+
120
+ it('renews lease with requested ttl', async () => {
121
+ const adapter = new DockerEnvironmentAdapter();
122
+ const ttlMs = 45_000;
123
+ const lease = await adapter.renewLease('env-1', ttlMs);
124
+
125
+ const expiresAt = new Date(lease.expiresAt).getTime();
126
+ const acquiredAt = new Date(lease.acquiredAt).getTime();
127
+ expect(expiresAt - acquiredAt).toBe(ttlMs);
128
+ });
129
+
130
+ it('returns provider capabilities', () => {
131
+ const adapter = new DockerEnvironmentAdapter();
132
+ const capabilities = adapter.getCapabilities();
133
+
134
+ expect(capabilities.supportsLeaseRenewal).toBe(true);
135
+ expect(capabilities.supportsSnapshots).toBe(false);
136
+ expect(capabilities.custom?.provider).toBe('docker-cli');
137
+ });
138
+ });
@@ -0,0 +1,439 @@
1
+ /**
2
+ * @module @kb-labs/adapters-environment-docker
3
+ * Docker CLI adapter implementing IEnvironmentProvider.
4
+ */
5
+
6
+ import { execFile } from 'node:child_process';
7
+ import { promisify } from 'node:util';
8
+ import { randomUUID } from 'node:crypto';
9
+ import type {
10
+ IEnvironmentProvider,
11
+ CreateEnvironmentRequest,
12
+ ReserveEnvironmentRequest,
13
+ ReservedEnvironment,
14
+ StartEnvironmentRequest,
15
+ EnvironmentDescriptor,
16
+ EnvironmentStatusResult,
17
+ EnvironmentLease,
18
+ EnvironmentProviderCapabilities,
19
+ EnvironmentStatus,
20
+ } from '@kb-labs/core-platform';
21
+
22
+ export { manifest } from './manifest.js';
23
+
24
+ const execFileAsync = promisify(execFile);
25
+
26
+ interface WorkspaceContext {
27
+ cwd?: string;
28
+ }
29
+
30
+ export type DockerExec = (args: string[]) => Promise<string>;
31
+
32
+ /**
33
+ * Gateway coordination config for container runtime.
34
+ * The adapter owns all transport-specific setup — callers know nothing about Gateway.
35
+ */
36
+ export interface GatewayConfig {
37
+ /** WebSocket URL the runtime-server connects to, e.g. ws://kb-gateway:4000 */
38
+ wsUrl: string;
39
+ /** JWT secret for signing per-container access tokens */
40
+ jwtSecret: string;
41
+ /** Internal secret for dispatch endpoint */
42
+ dispatchSecret: string;
43
+ /** Dispatch endpoint URL, e.g. http://localhost:4000/internal/dispatch */
44
+ dispatchUrl: string;
45
+ }
46
+
47
+ /**
48
+ * Docker provider configuration.
49
+ */
50
+ export interface DockerEnvironmentAdapterConfig {
51
+ dockerBinary?: string;
52
+ defaultImage?: string;
53
+ network?: string;
54
+ autoRemove?: boolean;
55
+ defaultTtlMs?: number;
56
+ mountWorkspace?: boolean;
57
+ workspaceMountPath?: string;
58
+ workspace?: WorkspaceContext;
59
+ /**
60
+ * Optional command executor override (primarily for tests).
61
+ * Receives argv for docker binary and must resolve stdout.
62
+ */
63
+ execDocker?: DockerExec;
64
+ /**
65
+ * Command to run inside the container to start the runtime server.
66
+ * E.g. ['node', '/app/dist/cli.cjs']
67
+ */
68
+ runtimeCommand?: string[];
69
+ /**
70
+ * Extra host mappings passed as --add-host flags.
71
+ * E.g. ['host.docker.internal:host-gateway'] for Linux dev environments
72
+ * where host.docker.internal is not automatically available.
73
+ */
74
+ extraHosts?: string[];
75
+ /**
76
+ * Gateway coordination config.
77
+ * When present, the adapter mints a JWT per container and injects
78
+ * GATEWAY_WS_URL, RUNTIME_HOST_ID, GATEWAY_TOKEN, GATEWAY_ALLOW_WS into env.
79
+ * Callers never see these details.
80
+ */
81
+ gateway?: GatewayConfig;
82
+ }
83
+
84
+ /**
85
+ * Docker CLI implementation of IEnvironmentProvider.
86
+ */
87
+ export class DockerEnvironmentAdapter implements IEnvironmentProvider {
88
+ private readonly dockerBinary: string;
89
+ private readonly defaultImage: string;
90
+ private readonly network?: string;
91
+ private readonly autoRemove: boolean;
92
+ private readonly defaultTtlMs: number;
93
+ private readonly mountWorkspace: boolean;
94
+ private readonly workspaceMountPath: string;
95
+ private readonly workspaceCwd?: string;
96
+ private readonly dockerExec: DockerExec;
97
+ private readonly runtimeCommand?: string[];
98
+ private readonly extraHosts: string[];
99
+ private readonly gateway?: GatewayConfig;
100
+
101
+ // reserved environments pending start(): environmentId → reservation metadata
102
+ private readonly reserved = new Map<string, { runId: string; ttlMs: number; createdAt: Date }>();
103
+ // active environments: environmentId → Docker containerId (for getStatus/destroy lookup)
104
+ private readonly active = new Map<string, string>();
105
+
106
+ constructor(private readonly config: DockerEnvironmentAdapterConfig = {}) {
107
+ this.dockerBinary = config.dockerBinary ?? 'docker';
108
+ this.defaultImage = config.defaultImage ?? 'node:20-alpine';
109
+ this.network = config.network;
110
+ this.autoRemove = config.autoRemove ?? true;
111
+ this.defaultTtlMs = config.defaultTtlMs ?? 60 * 60 * 1000;
112
+ this.mountWorkspace = config.mountWorkspace ?? true;
113
+ this.workspaceMountPath = config.workspaceMountPath ?? '/workspace';
114
+ this.workspaceCwd = config.workspace?.cwd;
115
+ this.dockerExec = config.execDocker ?? this.defaultExecDocker.bind(this);
116
+ this.runtimeCommand = config.runtimeCommand;
117
+ this.extraHosts = config.extraHosts ?? [];
118
+ this.gateway = config.gateway;
119
+ }
120
+
121
+ /**
122
+ * Phase 1: Reserve an environmentId without starting the container.
123
+ * The adapter generates a stable runtimeHostId used as environmentId.
124
+ * When gateway config is present, this id will be used for JWT and RUNTIME_HOST_ID.
125
+ */
126
+ async reserve(request: ReserveEnvironmentRequest): Promise<ReservedEnvironment> {
127
+ const runId = request.runId ?? randomUUID();
128
+ const environmentId = `runtime-${runId}`;
129
+ const ttlMs = request.ttlMs ?? this.defaultTtlMs;
130
+
131
+ this.reserved.set(environmentId, { runId, ttlMs, createdAt: new Date() });
132
+
133
+ return { environmentId };
134
+ }
135
+
136
+ /**
137
+ * Phase 2: Start the container for a previously reserved environmentId.
138
+ * Injects all gateway coordination env vars (JWT, WS URL, etc.) internally.
139
+ */
140
+ async start(environmentId: string, request: StartEnvironmentRequest): Promise<EnvironmentDescriptor> {
141
+ const reservation = this.reserved.get(environmentId);
142
+ if (!reservation) {
143
+ throw new Error(`No reservation found for environmentId: ${environmentId}. Call reserve() first.`);
144
+ }
145
+ this.reserved.delete(environmentId);
146
+
147
+ const { runId, ttlMs } = reservation;
148
+ const image = request.image ?? this.defaultImage;
149
+ const containerName = this.buildContainerName(runId);
150
+ const args = ['run', '-d'];
151
+
152
+ if (this.autoRemove) {
153
+ args.push('--rm');
154
+ }
155
+
156
+ args.push('--name', containerName);
157
+ args.push('--label', `kb.run_id=${runId}`);
158
+ args.push('--label', `kb.environment_id=${environmentId}`);
159
+
160
+ if (this.network) {
161
+ args.push('--network', this.network);
162
+ }
163
+
164
+ for (const host of this.extraHosts) {
165
+ args.push('--add-host', host);
166
+ }
167
+
168
+ // Adapter-owned env vars: gateway coordination
169
+ const namespaceId = (request.metadata?.namespaceId as string | undefined) ?? 'default';
170
+ const gatewayEnv = await this.buildGatewayEnv(environmentId, namespaceId);
171
+ for (const [key, value] of Object.entries(gatewayEnv)) {
172
+ args.push('-e', `${key}=${value}`);
173
+ }
174
+
175
+ // Caller-supplied extra env vars (merged after gateway env)
176
+ for (const [key, value] of Object.entries(request.env ?? {})) {
177
+ args.push('-e', `${key}=${value}`);
178
+ }
179
+
180
+ const workspacePath = request.workspacePath ?? this.workspaceCwd;
181
+ if (workspacePath && this.mountWorkspace) {
182
+ args.push('-v', `${workspacePath}:${this.workspaceMountPath}`);
183
+ args.push('-w', this.workspaceMountPath);
184
+ }
185
+
186
+ args.push(image);
187
+
188
+ const command = this.runtimeCommand;
189
+ if (command && command.length > 0) {
190
+ args.push(...command);
191
+ }
192
+
193
+ const containerId = (await this.execDocker(args)).trim();
194
+
195
+ // Register containerId for getStatus/destroy lookup by environmentId
196
+ this.active.set(environmentId, containerId);
197
+
198
+ const now = new Date();
199
+ const lease = this.buildLease(now, ttlMs, runId);
200
+
201
+ return {
202
+ environmentId,
203
+ provider: 'docker-cli',
204
+ status: 'ready',
205
+ createdAt: now.toISOString(),
206
+ updatedAt: now.toISOString(),
207
+ lease,
208
+ metadata: {
209
+ image,
210
+ containerName,
211
+ containerId,
212
+ },
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Single-phase create (legacy / simple use cases without gateway).
218
+ */
219
+ async create(request: CreateEnvironmentRequest): Promise<EnvironmentDescriptor> {
220
+ const image = request.image ?? this.defaultImage;
221
+ const ttlMs = request.ttlMs ?? this.defaultTtlMs;
222
+ const runId = request.runId ?? 'run';
223
+
224
+ const containerName = this.buildContainerName(runId);
225
+ const args = ['run', '-d'];
226
+
227
+ if (this.autoRemove) {
228
+ args.push('--rm');
229
+ }
230
+
231
+ args.push('--name', containerName);
232
+ args.push('--label', `kb.run_id=${runId}`);
233
+ if (request.templateId) {
234
+ args.push('--label', `kb.template_id=${request.templateId}`);
235
+ }
236
+
237
+ if (this.network) {
238
+ args.push('--network', this.network);
239
+ }
240
+
241
+ for (const host of this.extraHosts) {
242
+ args.push('--add-host', host);
243
+ }
244
+
245
+ for (const [key, value] of Object.entries(request.env ?? {})) {
246
+ args.push('-e', `${key}=${value}`);
247
+ }
248
+
249
+ const workspacePath = request.workspacePath ?? this.workspaceCwd;
250
+ if (workspacePath && this.mountWorkspace) {
251
+ args.push('-v', `${workspacePath}:${this.workspaceMountPath}`);
252
+ args.push('-w', this.workspaceMountPath);
253
+ }
254
+
255
+ args.push(image);
256
+
257
+ if (request.command && request.command.length > 0) {
258
+ args.push(...request.command);
259
+ } else if (this.runtimeCommand && this.runtimeCommand.length > 0) {
260
+ args.push(...this.runtimeCommand);
261
+ }
262
+
263
+ const containerId = (await this.execDocker(args)).trim();
264
+
265
+ const now = new Date();
266
+ const lease = this.buildLease(now, ttlMs, runId);
267
+
268
+ return {
269
+ environmentId: containerId,
270
+ provider: 'docker-cli',
271
+ status: 'ready',
272
+ createdAt: now.toISOString(),
273
+ updatedAt: now.toISOString(),
274
+ lease,
275
+ metadata: {
276
+ image,
277
+ containerName,
278
+ templateId: request.templateId,
279
+ },
280
+ };
281
+ }
282
+
283
+ async getStatus(environmentId: string): Promise<EnvironmentStatusResult> {
284
+ const now = new Date().toISOString();
285
+ // Resolve logical environmentId → actual Docker container ID (from start())
286
+ const dockerRef = this.active.get(environmentId) ?? environmentId;
287
+
288
+ try {
289
+ const state = (await this.execDocker([
290
+ 'inspect',
291
+ '-f',
292
+ '{{.State.Status}}',
293
+ dockerRef,
294
+ ])).trim();
295
+
296
+ return {
297
+ environmentId,
298
+ status: this.mapDockerStateToStatus(state),
299
+ updatedAt: now,
300
+ reason: state,
301
+ };
302
+ } catch (error) {
303
+ if (this.isMissingContainerError(error)) {
304
+ return {
305
+ environmentId,
306
+ status: 'terminated',
307
+ updatedAt: now,
308
+ reason: 'container_not_found',
309
+ };
310
+ }
311
+ throw error;
312
+ }
313
+ }
314
+
315
+ async destroy(environmentId: string): Promise<void> {
316
+ // Clean up any pending reservation
317
+ this.reserved.delete(environmentId);
318
+ // Resolve logical environmentId → actual Docker container ID (from start())
319
+ const dockerRef = this.active.get(environmentId) ?? environmentId;
320
+ this.active.delete(environmentId);
321
+
322
+ try {
323
+ await this.execDocker(['rm', '-f', dockerRef]);
324
+ } catch (error) {
325
+ if (this.isMissingContainerError(error)) {
326
+ return;
327
+ }
328
+ throw error;
329
+ }
330
+ }
331
+
332
+ async renewLease(environmentId: string, ttlMs: number): Promise<EnvironmentLease> {
333
+ const now = new Date();
334
+ return this.buildLease(now, ttlMs, environmentId);
335
+ }
336
+
337
+ getCapabilities(): EnvironmentProviderCapabilities {
338
+ return {
339
+ supportsLeaseRenewal: true,
340
+ supportsExecProbe: false,
341
+ supportsLogs: false,
342
+ supportsSnapshots: false,
343
+ custom: {
344
+ provider: 'docker-cli',
345
+ supportsTwoPhaseProvisioning: true,
346
+ },
347
+ };
348
+ }
349
+
350
+ /**
351
+ * Build gateway coordination env vars for a container.
352
+ * Mints a short-lived JWT using the environmentId as hostId.
353
+ * Returns empty object when gateway is not configured.
354
+ */
355
+ private async buildGatewayEnv(environmentId: string, namespaceId: string): Promise<Record<string, string>> {
356
+ if (!this.gateway) {
357
+ return {};
358
+ }
359
+
360
+ const { signAccessToken } = await import('@kb-labs/gateway-auth');
361
+ const { token } = await signAccessToken(
362
+ { hostId: environmentId, namespaceId, type: 'machine' as const, tier: 'free' as const },
363
+ { secret: this.gateway.jwtSecret },
364
+ );
365
+
366
+ return {
367
+ GATEWAY_WS_URL: this.gateway.wsUrl,
368
+ RUNTIME_HOST_ID: environmentId,
369
+ GATEWAY_TOKEN: token,
370
+ // Allow ws:// on Docker bridge networks (trusted, orchestrated network)
371
+ GATEWAY_ALLOW_WS: '1',
372
+ };
373
+ }
374
+
375
+ private async execDocker(args: string[]): Promise<string> {
376
+ return this.dockerExec(args);
377
+ }
378
+
379
+ private async defaultExecDocker(args: string[]): Promise<string> {
380
+ try {
381
+ const { stdout } = await execFileAsync(this.dockerBinary, args, {
382
+ encoding: 'utf8',
383
+ });
384
+ return stdout;
385
+ } catch (error) {
386
+ const details = error instanceof Error ? error.message : String(error);
387
+ throw new Error(`Docker command failed: ${this.dockerBinary} ${args.join(' ')} :: ${details}`);
388
+ }
389
+ }
390
+
391
+ private buildLease(now: Date, ttlMs: number, owner?: string): EnvironmentLease {
392
+ return {
393
+ leaseId: randomUUID(),
394
+ acquiredAt: now.toISOString(),
395
+ expiresAt: new Date(now.getTime() + ttlMs).toISOString(),
396
+ owner,
397
+ };
398
+ }
399
+
400
+ private buildContainerName(runId: string): string {
401
+ const normalizedRunId = runId.toLowerCase().replace(/[^a-z0-9_-]/g, '-').slice(0, 32);
402
+ const entropy = randomUUID().slice(0, 8);
403
+ return `kb-env-${normalizedRunId}-${Date.now().toString(36)}-${entropy}`;
404
+ }
405
+
406
+ private mapDockerStateToStatus(state: string): EnvironmentStatus {
407
+ switch (state) {
408
+ case 'running':
409
+ return 'ready';
410
+ case 'created':
411
+ case 'restarting':
412
+ case 'paused':
413
+ return 'provisioning';
414
+ case 'exited':
415
+ case 'dead':
416
+ return 'terminated';
417
+ default:
418
+ return 'degraded';
419
+ }
420
+ }
421
+
422
+ private isMissingContainerError(error: unknown): boolean {
423
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
424
+ return (
425
+ message.includes('no such container') ||
426
+ message.includes('no such object') ||
427
+ message.includes('container not found')
428
+ );
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Create Docker environment adapter.
434
+ */
435
+ export function createAdapter(config?: DockerEnvironmentAdapterConfig): DockerEnvironmentAdapter {
436
+ return new DockerEnvironmentAdapter(config);
437
+ }
438
+
439
+ export default createAdapter;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @module @kb-labs/adapters-environment-docker/manifest
3
+ * Adapter manifest for Docker-based environment provider.
4
+ */
5
+
6
+ import type { AdapterManifest } from '@kb-labs/core-platform';
7
+
8
+ /**
9
+ * Adapter manifest for Docker environment provider.
10
+ */
11
+ export const manifest: AdapterManifest = {
12
+ manifestVersion: '1.0.0',
13
+ id: 'docker-environment-provider',
14
+ name: 'Docker Environment Provider',
15
+ version: '0.1.0',
16
+ description: 'Long-lived environment provider using local Docker CLI',
17
+ author: 'KB Labs Team',
18
+ license: 'KBPL-1.1',
19
+ type: 'core',
20
+ implements: 'IEnvironmentProvider',
21
+ contexts: ['workspace'],
22
+ capabilities: {
23
+ custom: {
24
+ docker: true,
25
+ leaseRenewal: true,
26
+ longLivedEnvironments: true,
27
+ },
28
+ },
29
+ configSchema: {
30
+ dockerBinary: {
31
+ type: 'string',
32
+ default: 'docker',
33
+ description: 'Path to Docker CLI binary',
34
+ },
35
+ defaultImage: {
36
+ type: 'string',
37
+ default: 'node:20-alpine',
38
+ description: 'Default image used for created environments',
39
+ },
40
+ network: {
41
+ type: 'string',
42
+ description: 'Optional Docker network for created containers',
43
+ },
44
+ autoRemove: {
45
+ type: 'boolean',
46
+ default: true,
47
+ description: 'Use --rm for containers',
48
+ },
49
+ defaultTtlMs: {
50
+ type: 'number',
51
+ default: 3600000,
52
+ description: 'Default environment lease TTL in milliseconds',
53
+ },
54
+ mountWorkspace: {
55
+ type: 'boolean',
56
+ default: true,
57
+ description: 'Mount workspace path into container',
58
+ },
59
+ workspaceMountPath: {
60
+ type: 'string',
61
+ default: '/workspace',
62
+ description: 'Container path used for workspace mount',
63
+ },
64
+ },
65
+ };
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "baseUrl": ".",
6
+ "paths": {}
7
+ },
8
+ "include": [
9
+ "src/**/*"
10
+ ],
11
+ "exclude": [
12
+ "dist",
13
+ "node_modules"
14
+ ]
15
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "@kb-labs/devkit/tsconfig/node.json",
4
+ "compilerOptions": {
5
+ "baseUrl": ".",
6
+ "paths": {
7
+ "@kb-labs/core-platform": [
8
+ "../../../kb-labs-core/packages/core-platform/src/index.ts"
9
+ ],
10
+ "@kb-labs/core-platform/*": [
11
+ "../../../kb-labs-core/packages/core-platform/src/*"
12
+ ]
13
+ }
14
+ },
15
+ "include": ["src"]
16
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'tsup';
2
+ import nodePreset from '@kb-labs/devkit/tsup/node';
3
+
4
+ export default defineConfig({
5
+ ...nodePreset,
6
+ tsconfig: 'tsconfig.build.json',
7
+ entry: ['src/index.ts', 'src/manifest.ts'],
8
+ dts: true,
9
+ });