bulkhead-runtime 0.1.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 (220) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +625 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +43 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/config/index.d.ts +29 -0
  8. package/dist/config/index.d.ts.map +1 -0
  9. package/dist/config/index.js +24 -0
  10. package/dist/config/index.js.map +1 -0
  11. package/dist/credentials/index.d.ts +4 -0
  12. package/dist/credentials/index.d.ts.map +1 -0
  13. package/dist/credentials/index.js +3 -0
  14. package/dist/credentials/index.js.map +1 -0
  15. package/dist/credentials/proxy.d.ts +3 -0
  16. package/dist/credentials/proxy.d.ts.map +1 -0
  17. package/dist/credentials/proxy.js +11 -0
  18. package/dist/credentials/proxy.js.map +1 -0
  19. package/dist/credentials/store.d.ts +7 -0
  20. package/dist/credentials/store.d.ts.map +1 -0
  21. package/dist/credentials/store.js +91 -0
  22. package/dist/credentials/store.js.map +1 -0
  23. package/dist/credentials/types.d.ts +14 -0
  24. package/dist/credentials/types.d.ts.map +1 -0
  25. package/dist/credentials/types.js +2 -0
  26. package/dist/credentials/types.js.map +1 -0
  27. package/dist/hooks/index.d.ts +3 -0
  28. package/dist/hooks/index.d.ts.map +1 -0
  29. package/dist/hooks/index.js +2 -0
  30. package/dist/hooks/index.js.map +1 -0
  31. package/dist/hooks/runner.d.ts +7 -0
  32. package/dist/hooks/runner.d.ts.map +1 -0
  33. package/dist/hooks/runner.js +20 -0
  34. package/dist/hooks/runner.js.map +1 -0
  35. package/dist/hooks/types.d.ts +39 -0
  36. package/dist/hooks/types.d.ts.map +1 -0
  37. package/dist/hooks/types.js +2 -0
  38. package/dist/hooks/types.js.map +1 -0
  39. package/dist/index.d.ts +14 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +21 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/infra/env.d.ts +2 -0
  44. package/dist/infra/env.d.ts.map +1 -0
  45. package/dist/infra/env.js +6 -0
  46. package/dist/infra/env.js.map +1 -0
  47. package/dist/infra/warning-filter.d.ts +8 -0
  48. package/dist/infra/warning-filter.d.ts.map +1 -0
  49. package/dist/infra/warning-filter.js +66 -0
  50. package/dist/infra/warning-filter.js.map +1 -0
  51. package/dist/logging/subsystem.d.ts +15 -0
  52. package/dist/logging/subsystem.d.ts.map +1 -0
  53. package/dist/logging/subsystem.js +57 -0
  54. package/dist/logging/subsystem.js.map +1 -0
  55. package/dist/memory/embeddings-debug.d.ts +2 -0
  56. package/dist/memory/embeddings-debug.d.ts.map +1 -0
  57. package/dist/memory/embeddings-debug.js +12 -0
  58. package/dist/memory/embeddings-debug.js.map +1 -0
  59. package/dist/memory/embeddings.d.ts +16 -0
  60. package/dist/memory/embeddings.d.ts.map +1 -0
  61. package/dist/memory/embeddings.js +180 -0
  62. package/dist/memory/embeddings.js.map +1 -0
  63. package/dist/memory/fs-utils.d.ts +12 -0
  64. package/dist/memory/fs-utils.d.ts.map +1 -0
  65. package/dist/memory/fs-utils.js +24 -0
  66. package/dist/memory/fs-utils.js.map +1 -0
  67. package/dist/memory/hybrid.d.ts +46 -0
  68. package/dist/memory/hybrid.d.ts.map +1 -0
  69. package/dist/memory/hybrid.js +81 -0
  70. package/dist/memory/hybrid.js.map +1 -0
  71. package/dist/memory/index.d.ts +12 -0
  72. package/dist/memory/index.d.ts.map +1 -0
  73. package/dist/memory/index.js +12 -0
  74. package/dist/memory/index.js.map +1 -0
  75. package/dist/memory/internal.d.ts +39 -0
  76. package/dist/memory/internal.d.ts.map +1 -0
  77. package/dist/memory/internal.js +292 -0
  78. package/dist/memory/internal.js.map +1 -0
  79. package/dist/memory/manager-search.d.ts +61 -0
  80. package/dist/memory/manager-search.d.ts.map +1 -0
  81. package/dist/memory/manager-search.js +102 -0
  82. package/dist/memory/manager-search.js.map +1 -0
  83. package/dist/memory/mmr.d.ts +63 -0
  84. package/dist/memory/mmr.d.ts.map +1 -0
  85. package/dist/memory/mmr.js +165 -0
  86. package/dist/memory/mmr.js.map +1 -0
  87. package/dist/memory/query-expansion.d.ts +42 -0
  88. package/dist/memory/query-expansion.d.ts.map +1 -0
  89. package/dist/memory/query-expansion.js +776 -0
  90. package/dist/memory/query-expansion.js.map +1 -0
  91. package/dist/memory/simple-manager.d.ts +23 -0
  92. package/dist/memory/simple-manager.d.ts.map +1 -0
  93. package/dist/memory/simple-manager.js +193 -0
  94. package/dist/memory/simple-manager.js.map +1 -0
  95. package/dist/memory/sqlite.d.ts +2 -0
  96. package/dist/memory/sqlite.d.ts.map +1 -0
  97. package/dist/memory/sqlite.js +16 -0
  98. package/dist/memory/sqlite.js.map +1 -0
  99. package/dist/memory/temporal-decay.d.ts +26 -0
  100. package/dist/memory/temporal-decay.d.ts.map +1 -0
  101. package/dist/memory/temporal-decay.js +120 -0
  102. package/dist/memory/temporal-decay.js.map +1 -0
  103. package/dist/memory/types.d.ts +95 -0
  104. package/dist/memory/types.d.ts.map +1 -0
  105. package/dist/memory/types.js +2 -0
  106. package/dist/memory/types.js.map +1 -0
  107. package/dist/package.json +36 -0
  108. package/dist/platform/index.d.ts +3 -0
  109. package/dist/platform/index.d.ts.map +1 -0
  110. package/dist/platform/index.js +2 -0
  111. package/dist/platform/index.js.map +1 -0
  112. package/dist/platform/platform.d.ts +3 -0
  113. package/dist/platform/platform.d.ts.map +1 -0
  114. package/dist/platform/platform.js +68 -0
  115. package/dist/platform/platform.js.map +1 -0
  116. package/dist/platform/types.d.ts +16 -0
  117. package/dist/platform/types.d.ts.map +1 -0
  118. package/dist/platform/types.js +2 -0
  119. package/dist/platform/types.js.map +1 -0
  120. package/dist/runtime/agent.d.ts +28 -0
  121. package/dist/runtime/agent.d.ts.map +1 -0
  122. package/dist/runtime/agent.js +102 -0
  123. package/dist/runtime/agent.js.map +1 -0
  124. package/dist/runtime/index.d.ts +2 -0
  125. package/dist/runtime/index.d.ts.map +1 -0
  126. package/dist/runtime/index.js +2 -0
  127. package/dist/runtime/index.js.map +1 -0
  128. package/dist/sandbox/cgroup.d.ts +17 -0
  129. package/dist/sandbox/cgroup.d.ts.map +1 -0
  130. package/dist/sandbox/cgroup.js +69 -0
  131. package/dist/sandbox/cgroup.js.map +1 -0
  132. package/dist/sandbox/index.d.ts +11 -0
  133. package/dist/sandbox/index.d.ts.map +1 -0
  134. package/dist/sandbox/index.js +9 -0
  135. package/dist/sandbox/index.js.map +1 -0
  136. package/dist/sandbox/ipc.d.ts +23 -0
  137. package/dist/sandbox/ipc.d.ts.map +1 -0
  138. package/dist/sandbox/ipc.js +138 -0
  139. package/dist/sandbox/ipc.js.map +1 -0
  140. package/dist/sandbox/manager.d.ts +5 -0
  141. package/dist/sandbox/manager.d.ts.map +1 -0
  142. package/dist/sandbox/manager.js +245 -0
  143. package/dist/sandbox/manager.js.map +1 -0
  144. package/dist/sandbox/namespace.d.ts +12 -0
  145. package/dist/sandbox/namespace.d.ts.map +1 -0
  146. package/dist/sandbox/namespace.js +120 -0
  147. package/dist/sandbox/namespace.js.map +1 -0
  148. package/dist/sandbox/proxy-tools.d.ts +14 -0
  149. package/dist/sandbox/proxy-tools.d.ts.map +1 -0
  150. package/dist/sandbox/proxy-tools.js +63 -0
  151. package/dist/sandbox/proxy-tools.js.map +1 -0
  152. package/dist/sandbox/rootfs.d.ts +15 -0
  153. package/dist/sandbox/rootfs.d.ts.map +1 -0
  154. package/dist/sandbox/rootfs.js +163 -0
  155. package/dist/sandbox/rootfs.js.map +1 -0
  156. package/dist/sandbox/seccomp.d.ts +13 -0
  157. package/dist/sandbox/seccomp.d.ts.map +1 -0
  158. package/dist/sandbox/seccomp.js +120 -0
  159. package/dist/sandbox/seccomp.js.map +1 -0
  160. package/dist/sandbox/types.d.ts +68 -0
  161. package/dist/sandbox/types.d.ts.map +1 -0
  162. package/dist/sandbox/types.js +8 -0
  163. package/dist/sandbox/types.js.map +1 -0
  164. package/dist/sandbox/worker.d.ts +12 -0
  165. package/dist/sandbox/worker.d.ts.map +1 -0
  166. package/dist/sandbox/worker.js +84 -0
  167. package/dist/sandbox/worker.js.map +1 -0
  168. package/dist/sessions/index.d.ts +2 -0
  169. package/dist/sessions/index.d.ts.map +1 -0
  170. package/dist/sessions/index.js +2 -0
  171. package/dist/sessions/index.js.map +1 -0
  172. package/dist/sessions/store.d.ts +17 -0
  173. package/dist/sessions/store.d.ts.map +1 -0
  174. package/dist/sessions/store.js +48 -0
  175. package/dist/sessions/store.js.map +1 -0
  176. package/dist/skills/enablement.d.ts +10 -0
  177. package/dist/skills/enablement.d.ts.map +1 -0
  178. package/dist/skills/enablement.js +52 -0
  179. package/dist/skills/enablement.js.map +1 -0
  180. package/dist/skills/index.d.ts +4 -0
  181. package/dist/skills/index.d.ts.map +1 -0
  182. package/dist/skills/index.js +4 -0
  183. package/dist/skills/index.js.map +1 -0
  184. package/dist/skills/loader.d.ts +8 -0
  185. package/dist/skills/loader.d.ts.map +1 -0
  186. package/dist/skills/loader.js +8 -0
  187. package/dist/skills/loader.js.map +1 -0
  188. package/dist/skills/registry.d.ts +19 -0
  189. package/dist/skills/registry.d.ts.map +1 -0
  190. package/dist/skills/registry.js +106 -0
  191. package/dist/skills/registry.js.map +1 -0
  192. package/dist/utils/boolean.d.ts +6 -0
  193. package/dist/utils/boolean.d.ts.map +1 -0
  194. package/dist/utils/boolean.js +28 -0
  195. package/dist/utils/boolean.js.map +1 -0
  196. package/dist/utils/run-with-concurrency.d.ts +12 -0
  197. package/dist/utils/run-with-concurrency.d.ts.map +1 -0
  198. package/dist/utils/run-with-concurrency.js +40 -0
  199. package/dist/utils/run-with-concurrency.js.map +1 -0
  200. package/dist/utils.d.ts +3 -0
  201. package/dist/utils.d.ts.map +1 -0
  202. package/dist/utils.js +38 -0
  203. package/dist/utils.js.map +1 -0
  204. package/dist/workspace/index.d.ts +3 -0
  205. package/dist/workspace/index.d.ts.map +1 -0
  206. package/dist/workspace/index.js +2 -0
  207. package/dist/workspace/index.js.map +1 -0
  208. package/dist/workspace/runner.d.ts +19 -0
  209. package/dist/workspace/runner.d.ts.map +1 -0
  210. package/dist/workspace/runner.js +158 -0
  211. package/dist/workspace/runner.js.map +1 -0
  212. package/dist/workspace/types.d.ts +36 -0
  213. package/dist/workspace/types.d.ts.map +1 -0
  214. package/dist/workspace/types.js +2 -0
  215. package/dist/workspace/types.js.map +1 -0
  216. package/dist/workspace/workspace.d.ts +12 -0
  217. package/dist/workspace/workspace.d.ts.map +1 -0
  218. package/dist/workspace/workspace.js +76 -0
  219. package/dist/workspace/workspace.js.map +1 -0
  220. package/package.json +53 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Peter Steinberger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,625 @@
1
+ <p align="center">
2
+ <br />
3
+ <img src="https://img.shields.io/badge/%E2%96%88%E2%96%88%E2%96%88_BULKHEAD-RUNTIME_%E2%96%88%E2%96%88%E2%96%88-000?style=for-the-badge&labelColor=0d1117&color=00ff41" alt="Bulkhead Runtime" />
4
+ <br /><br />
5
+ <strong>Watertight isolation for multi-tenant AI agents.</strong>
6
+ <br /><br />
7
+ <a href="https://www.npmjs.com/package/bulkhead-runtime"><img src="https://img.shields.io/npm/v/bulkhead-runtime?style=flat-square&color=00ff41&labelColor=0d1117" alt="npm" /></a>
8
+ <img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square&labelColor=0d1117" alt="MIT" />
9
+ <img src="https://img.shields.io/badge/node-%3E%3D22.12-brightgreen?style=flat-square&labelColor=0d1117&logo=node.js&logoColor=white" alt="node" />
10
+ <img src="https://img.shields.io/badge/runtime_deps-3-00ff41?style=flat-square&labelColor=0d1117" alt="deps" />
11
+ <img src="https://img.shields.io/badge/tests-88_passing-00ff41?style=flat-square&labelColor=0d1117" alt="tests" />
12
+ <img src="https://img.shields.io/badge/sandbox-5_isolation_layers-ff6b6b?style=flat-square&labelColor=0d1117" alt="isolation" />
13
+ <img src="https://img.shields.io/badge/crypto-AES--256--GCM-blueviolet?style=flat-square&labelColor=0d1117" alt="crypto" />
14
+ </p>
15
+
16
+ <br />
17
+
18
+ <p align="center">
19
+ Run 1,000 AI agents on a single Linux box.<br />
20
+ Each in its own OS namespace. Each with private memory, encrypted credentials, and an isolated filesystem.<br />
21
+ <b>No Docker. No cloud. One <code>npm install</code>.</b>
22
+ </p>
23
+
24
+ <br />
25
+
26
+ ---
27
+
28
+ ## Quick Start
29
+
30
+ ```bash
31
+ npm install bulkhead-runtime
32
+ ```
33
+
34
+ ```typescript
35
+ import { createPlatform } from "bulkhead-runtime";
36
+
37
+ const platform = createPlatform({
38
+ stateDir: "/var/bulkhead-runtime",
39
+ credentialPassphrase: process.env.CREDENTIAL_KEY,
40
+ });
41
+
42
+ // Create isolated workspaces — one per user, team, or tenant
43
+ const workspace = await platform.createWorkspace("user-42", {
44
+ provider: "anthropic",
45
+ model: "claude-sonnet-4-20250514",
46
+ });
47
+
48
+ const result = await workspace.run({
49
+ message: "Refactor the auth module to use JWT",
50
+ sessionId: "project-alpha",
51
+ });
52
+ // Runs in a Linux namespace sandbox.
53
+ // Private memory, encrypted credentials, isolated filesystem.
54
+ // No other workspace can see this agent's data. Ever.
55
+ ```
56
+
57
+ > **Requires:** Linux + Node.js 22.12+
58
+ >
59
+ > **macOS / Windows dev:**
60
+ > ```bash
61
+ > git clone https://github.com/tonga54/bulkhead-runtime.git && cd bulkhead-runtime
62
+ > docker compose run dev bash
63
+ > pnpm test # 88 tests, all green
64
+ > ```
65
+
66
+ ---
67
+
68
+ ## Use Cases
69
+
70
+ <table>
71
+ <tr>
72
+ <td width="50%">
73
+
74
+ **One agent per customer in your SaaS**
75
+
76
+ Your platform gives each customer an AI agent. Each agent accesses that customer's repos, APIs, and databases — with their own credentials. Customer A's agent can never see Customer B's tokens, data, or conversation history.
77
+
78
+ ```typescript
79
+ app.post("/api/agent", async (req, res) => {
80
+ const ws = await platform.getWorkspace(req.org.id);
81
+ const result = await ws.run({
82
+ message: req.body.message,
83
+ sessionId: req.body.threadId,
84
+ });
85
+ res.json({ response: result.response });
86
+ });
87
+ ```
88
+
89
+ </td>
90
+ <td width="50%">
91
+
92
+ **Per-team agents inside your company**
93
+
94
+ Engineering, ops, and data each get their own agent. Each team's agent connects to their own tools — different GitHub orgs, different databases, different cloud accounts. No credential leaks between teams.
95
+
96
+ ```typescript
97
+ const eng = await platform.createWorkspace("engineering");
98
+ const ops = await platform.createWorkspace("ops");
99
+ const data = await platform.createWorkspace("data-team");
100
+
101
+ eng.skills.enable("github-pr");
102
+ ops.skills.enable("pagerduty");
103
+ data.skills.enable("bigquery");
104
+
105
+ await eng.credentials.store("github", { token: "ghp_eng..." });
106
+ await ops.credentials.store("pagerduty", { token: "pd_..." });
107
+ await data.credentials.store("gcp", { key: "..." });
108
+ ```
109
+
110
+ </td>
111
+ </tr>
112
+ <tr>
113
+ <td width="50%">
114
+
115
+ **Client-isolated agents in consulting / agencies**
116
+
117
+ Each client project gets its own workspace. The agent knows that client's stack, their conventions, their infra. When you offboard a client, `deleteWorkspace()` wipes everything — memory, credentials, sessions.
118
+
119
+ ```typescript
120
+ const acme = await platform.createWorkspace("client-acme");
121
+ await acme.credentials.store("aws", { key: "...", secret: "..." });
122
+ await acme.credentials.store("jira", { token: "..." });
123
+
124
+ await acme.run({
125
+ message: "Check the staging deploy and open a Jira ticket if it failed",
126
+ sessionId: "daily-ops",
127
+ });
128
+
129
+ // Client offboarded — clean removal
130
+ await platform.deleteWorkspace("client-acme");
131
+ ```
132
+
133
+ </td>
134
+ <td width="50%">
135
+
136
+ **Ephemeral agents for CI / PR review / task runners**
137
+
138
+ Spin up a workspace per job, per PR, or per deploy. The agent runs, does its thing, and the workspace is destroyed. No state leaks between runs.
139
+
140
+ ```typescript
141
+ const jobId = `deploy-${Date.now()}`;
142
+ const ws = await platform.createWorkspace(jobId);
143
+
144
+ await ws.credentials.store("k8s", { kubeconfig: "..." });
145
+ ws.skills.enable("kubectl");
146
+
147
+ const result = await ws.run({
148
+ message: "Roll out v2.3.1 to staging, run smoke tests, report status",
149
+ });
150
+
151
+ await platform.deleteWorkspace(jobId);
152
+ ```
153
+
154
+ </td>
155
+ </tr>
156
+ </table>
157
+
158
+ **The common thread:** you have multiple tenants (users, teams, clients, jobs) and each one needs an AI agent with its own secrets, tools, and memory — on the same server, without any cross-contamination.
159
+
160
+ ---
161
+
162
+ ## How It Works
163
+
164
+ ```mermaid
165
+ graph TB
166
+ subgraph HOST["HOST PROCESS"]
167
+ subgraph WA["Workspace A"]
168
+ WA_mem["memory.db"]
169
+ WA_cred["creds.enc"]
170
+ WA_sess["sessions/"]
171
+ WA_skill["skills[]"]
172
+ end
173
+ subgraph WB["Workspace B"]
174
+ WB_mem["memory.db"]
175
+ WB_cred["creds.enc"]
176
+ WB_sess["sessions/"]
177
+ WB_skill["skills[]"]
178
+ end
179
+ subgraph WN["Workspace N"]
180
+ WN_mem["memory.db"]
181
+ WN_cred["creds.enc"]
182
+ WN_sess["sessions/"]
183
+ WN_skill["skills[]"]
184
+ end
185
+
186
+ subgraph SA["Sandbox A — user ns · mount ns · pid ns · net ns · cgroup v2"]
187
+ AgentA["Agent A"]
188
+ end
189
+ subgraph SB["Sandbox B — user ns · mount ns · pid ns · net ns · cgroup v2"]
190
+ AgentB["Agent B"]
191
+ end
192
+ subgraph SN["Sandbox N — user ns · mount ns · pid ns · net ns · cgroup v2"]
193
+ AgentN["Agent N"]
194
+ end
195
+ end
196
+
197
+ WA -->|"JSON-RPC IPC"| SA
198
+ WB -->|"JSON-RPC IPC"| SB
199
+ WN -->|"JSON-RPC IPC"| SN
200
+ ```
201
+
202
+ > Agent A cannot see Agent B's files, memory, credentials, or processes. Not by policy. **By kernel enforcement.**
203
+
204
+ ---
205
+
206
+ ## Why Bulkhead Over Alternatives
207
+
208
+ | | Docker per user | E2B / Cloud | **Bulkhead Runtime** |
209
+ |:---|:---:|:---:|:---:|
210
+ | **Isolation mechanism** | Container per user | Cloud VM per session | **Linux namespaces** |
211
+ | **Credential security** | DIY | Not built-in | **AES-256-GCM, never exposed to agent** |
212
+ | **Persistent memory** | DIY | DIY | **SQLite + vector embeddings per tenant** |
213
+ | **Skills with secret injection** | DIY | DIY | **Credentials injected server-side** |
214
+ | **Per-workspace skill config** | DIY | DIY | **Enable/disable per tenant** |
215
+ | **Infrastructure** | Docker daemon | Cloud API + billing | **Single npm package** |
216
+ | **Cold start** | ~2s | ~5-10s | **~50ms** |
217
+ | **Embeddable in your app** | No | No | **Yes — it's a library** |
218
+ | **License** | — | Proprietary | **MIT** |
219
+
220
+ ---
221
+
222
+ ## Single-User Mode
223
+
224
+ The fastest path for prototyping or single-agent use. No platform needed.
225
+
226
+ ```typescript
227
+ import { createRuntime } from "bulkhead-runtime";
228
+
229
+ const runtime = await createRuntime({
230
+ provider: "anthropic",
231
+ model: "claude-sonnet-4-20250514",
232
+ });
233
+
234
+ const result = await runtime.run({
235
+ message: "Find all TODO comments and create a summary",
236
+ });
237
+ ```
238
+
239
+ The agent runs inside a Linux namespace sandbox with full coding tools (read, write, edit, bash, grep, find, ls) and autonomous memory (`memory_store`, `memory_search`).
240
+
241
+ ---
242
+
243
+ ## Multi-Tenant Isolation
244
+
245
+ This is the core of Bulkhead Runtime. Each user gets a **workspace** — a fully isolated environment with its own memory, encrypted credentials, enabled skills, and session history. Workspaces are physically separated at the OS level.
246
+
247
+ ```typescript
248
+ import { createPlatform } from "bulkhead-runtime";
249
+
250
+ const platform = createPlatform({
251
+ stateDir: "/var/bulkhead-runtime",
252
+ credentialPassphrase: process.env.CREDENTIAL_KEY,
253
+ });
254
+
255
+ // Each workspace is a universe unto itself
256
+ const alice = await platform.createWorkspace("alice", {
257
+ provider: "anthropic",
258
+ model: "claude-sonnet-4-20250514",
259
+ });
260
+
261
+ const bob = await platform.createWorkspace("bob", {
262
+ provider: "google",
263
+ model: "gemini-2.5-flash",
264
+ });
265
+ ```
266
+
267
+ ### What "isolated" actually means
268
+
269
+ When `workspace.run()` executes, Bulkhead spawns a **child process** with 5 layers of kernel isolation. The agent **never runs in your application's process**.
270
+
271
+ ```mermaid
272
+ sequenceDiagram
273
+ participant App as Your App
274
+ participant Host as Host Process
275
+ participant Sandbox as Sandbox (isolated)
276
+
277
+ App->>Host: workspace.run({ message })
278
+ Host->>Host: Load session history
279
+ Host->>Host: Resolve enabled skills
280
+ Host->>Host: Detect sandbox capabilities
281
+ Host->>Sandbox: Spawn child process (unshare)
282
+ activate Sandbox
283
+ Note over Sandbox: Enter user namespace<br/>pivot_root → new filesystem<br/>PID namespace (own procs)<br/>Network ns (loopback only)<br/>cgroup v2 (mem/cpu/pid cap)
284
+ Sandbox->>Host: IPC: memory.search
285
+ Host-->>Sandbox: search results
286
+ Sandbox->>Host: IPC: memory.store
287
+ Host-->>Sandbox: store confirmation
288
+ Sandbox->>Host: IPC: skill.execute
289
+ Host->>Host: Decrypt credentials (AES-256-GCM)
290
+ Host-->>Sandbox: skill output (no credentials)
291
+ Sandbox-->>Host: Agent response
292
+ deactivate Sandbox
293
+ Host->>Host: Kill child process
294
+ Host->>Host: Fire lifecycle hooks
295
+ Host->>Host: Update session store
296
+ Host-->>App: { response, session }
297
+ ```
298
+
299
+ The agent gets coding tools (bash, file read/write/edit) because the mount namespace restricts its entire filesystem view. **It literally cannot see anything outside its sandbox.**
300
+
301
+ ---
302
+
303
+ ## Credential Security
304
+
305
+ Credentials are **AES-256-GCM encrypted** at rest. PBKDF2 key derivation with 100k iterations (SHA-512). The agent **never** sees raw secrets — not through tools, not through IPC, not through environment variables.
306
+
307
+ ```typescript
308
+ // Store encrypted credentials for Alice
309
+ await alice.credentials.store("github", { token: "ghp_alice_secret" });
310
+ await alice.credentials.store("openai", { apiKey: "sk-..." });
311
+
312
+ // When the agent uses a skill that needs credentials:
313
+ // 1. Host decrypts credentials server-side
314
+ // 2. Injects them as env vars into the skill process
315
+ // 3. Agent receives the skill's output — never the credential itself
316
+ ```
317
+
318
+ ```mermaid
319
+ sequenceDiagram
320
+ participant Agent as Agent (sandboxed)
321
+ participant Host as Host Process
322
+ participant Skill as Skill Script
323
+
324
+ Agent->>Host: skill_execute({ skillId: "github-issues" })
325
+ Host->>Host: Resolve skill directory
326
+ Host->>Host: Decrypt credentials (AES-256-GCM)
327
+ Host->>Skill: Spawn script with credentials as env vars
328
+ activate Skill
329
+ Skill->>Skill: process.env.token → use API
330
+ Skill-->>Host: stdout → JSON result
331
+ deactivate Skill
332
+ Host-->>Agent: Result string (no credentials)
333
+ Note over Agent: ✗ Never sees the token<br/>✗ Cannot read credential file<br/>✗ Cannot intercept env vars
334
+ ```
335
+
336
+ System environment variables (`PATH`, `HOME`, `NODE_ENV`) are protected from credential key collision. Skill IDs are validated against prototype pollution.
337
+
338
+ ---
339
+
340
+ ## Per-Workspace Skills
341
+
342
+ Skills are registered globally and **enabled per workspace**. Each workspace gets exactly the capabilities it needs — nothing more.
343
+
344
+ ```typescript
345
+ // Register skills globally
346
+ // skills/
347
+ // github-issues/
348
+ // SKILL.md ← LLM reads this to understand the skill
349
+ // execute.js ← Runs with credentials injected as env vars
350
+ // db-migration/
351
+ // SKILL.md
352
+ // execute.sh
353
+
354
+ // Enable different skills per workspace
355
+ const frontend = await platform.createWorkspace("team-frontend");
356
+ const backend = await platform.createWorkspace("team-backend");
357
+
358
+ frontend.skills.enable("github-issues");
359
+ backend.skills.enable("github-issues");
360
+ backend.skills.enable("db-migration"); // only backend gets this
361
+
362
+ // Store different credentials per workspace
363
+ await frontend.credentials.store("github", { token: "ghp_frontend_token" });
364
+ await backend.credentials.store("github", { token: "ghp_backend_token" });
365
+ await backend.credentials.store("database", { url: "postgres://prod:5432/app" });
366
+ ```
367
+
368
+ ```javascript
369
+ // skills/github-issues/execute.js
370
+ const params = JSON.parse(await readStdin());
371
+ const token = process.env.token; // Injected from encrypted store — never over IPC
372
+ const res = await fetch(`https://api.github.com/repos/${params.repo}/issues`, {
373
+ headers: { Authorization: `Bearer ${token}` },
374
+ });
375
+ console.log(JSON.stringify(await res.json()));
376
+ ```
377
+
378
+ Skills run with a minimal env (`PATH`, `HOME`, `NODE_ENV` + credentials), 30s timeout, and 10 MB stdout/stderr cap.
379
+
380
+ ---
381
+
382
+ ## Memory Isolation
383
+
384
+ Each workspace has its own SQLite database. Memories never cross workspace boundaries — not by access control, **by physical separation**.
385
+
386
+ ```typescript
387
+ await alice.memory.store("Project uses React and TypeScript");
388
+ await bob.memory.store("Project uses Vue and Python");
389
+
390
+ const search = await alice.memory.search("framework");
391
+ // → "React and TypeScript"
392
+ // Bob's data doesn't exist in Alice's universe.
393
+ ```
394
+
395
+ ### Autonomous Agent Memory
396
+
397
+ The agent decides what to remember. Memory persists across sessions, across restarts.
398
+
399
+ ```typescript
400
+ // Session 1
401
+ await runtime.run({
402
+ message: "My name is Juan, I work in fintech, I prefer TypeScript",
403
+ sessionId: "onboarding",
404
+ });
405
+
406
+ // Session 2 — different session, memory persists
407
+ await runtime.run({
408
+ message: "Set up a new project for me",
409
+ sessionId: "new-project",
410
+ });
411
+ // Agent searches memory → finds preferences → scaffolds TypeScript project
412
+ ```
413
+
414
+ **Search engine under the hood:**
415
+
416
+ | Stage | Algorithm |
417
+ |:---|:---|
418
+ | **Vector search** | Cosine similarity against stored embeddings |
419
+ | **Keyword search** | SQLite FTS5 with BM25 ranking |
420
+ | **Fusion** | Weighted merge of vector + keyword scores |
421
+ | **Temporal decay** | Exponential time-based score attenuation |
422
+ | **Diversity** | MMR (Maximal Marginal Relevance) re-ranking |
423
+ | **Query expansion** | 7-language keyword extraction (EN/ES/PT/ZH/JA/KO/AR) |
424
+
425
+ Works without any embedding API key — falls back to FTS5 keyword search.
426
+
427
+ ---
428
+
429
+ ## Session Continuity
430
+
431
+ ```typescript
432
+ await workspace.run({
433
+ message: "Create a REST API for user management",
434
+ sessionId: "api-project",
435
+ });
436
+
437
+ // Later — agent sees the full conversation history
438
+ await workspace.run({
439
+ message: "Add input validation to those endpoints",
440
+ sessionId: "api-project",
441
+ });
442
+ ```
443
+
444
+ Sessions are per-workspace, stored as JSONL transcripts with async locking.
445
+
446
+ ---
447
+
448
+ ## Subagent Orchestration
449
+
450
+ A tool can spawn a child agent. The parent blocks until the child finishes.
451
+
452
+ ```typescript
453
+ const result = await runtime.run({
454
+ message: "Review this PR for security and performance",
455
+ tools: [{
456
+ name: "specialist",
457
+ description: "Delegate a subtask to a specialist agent",
458
+ parameters: { task: { type: "string" }, role: { type: "string" } },
459
+ async execute(_id, params) {
460
+ const r = await runtime.run({
461
+ message: params.task,
462
+ systemPrompt: `You are a ${params.role} expert.`,
463
+ });
464
+ return { resultForAssistant: r.response };
465
+ },
466
+ }],
467
+ });
468
+ ```
469
+
470
+ ---
471
+
472
+ ## Lifecycle Hooks
473
+
474
+ ```typescript
475
+ workspace.hooks.register("before_tool_call", async ({ toolName, input }) => {
476
+ await auditLog.write({ tool: toolName, input, timestamp: Date.now() });
477
+ });
478
+
479
+ workspace.hooks.register("after_agent_end", async ({ sessionId, result }) => {
480
+ await billing.recordUsage(workspace.id, sessionId);
481
+ });
482
+ ```
483
+
484
+ 6 hook points: `session_start` · `session_end` · `before_agent_start` · `after_agent_end` · `before_tool_call` · `after_tool_call`
485
+
486
+ ---
487
+
488
+ ## Security Architecture
489
+
490
+ ### 5 Layers of Sandbox Isolation
491
+
492
+ All layers are **fail-closed** — if any layer can't be applied, the sandbox refuses to start.
493
+
494
+ ```mermaid
495
+ block-beta
496
+ columns 1
497
+ block:L1["Layer 1 — User Namespace · unprivileged creation via unshare(2)"]
498
+ columns 1
499
+ block:L2["Layer 2 — Mount Namespace · pivot_root → new root · old root unmounted"]
500
+ columns 1
501
+ block:L3["Layer 3 — PID Namespace · agent only sees its own processes"]
502
+ columns 1
503
+ block:L4["Layer 4 — Network Namespace · loopback only · no external access"]
504
+ columns 1
505
+ L5["Layer 5 — cgroups v2 · memory.max · pids.max · cpu.weight"]
506
+ end
507
+ end
508
+ end
509
+ end
510
+ AGENT["Agent process — can only see its own isolated world"]
511
+
512
+ L1 --> AGENT
513
+ ```
514
+
515
+ ### Defense in Depth
516
+
517
+ | Defense | Mechanism |
518
+ |:---|:---|
519
+ | **Env allowlist** | Only `PATH`, `HOME`, `NODE_ENV` + the single API key the agent needs. Everything else dropped. |
520
+ | **Credential proxy** | Secrets decrypted server-side, injected into skill execution. Never sent over IPC. |
521
+ | **Path traversal blocklist** | `/proc`, `/sys`, `/home/`, `/etc/shadow`, `/run/docker.sock`, and more are blocked from bind mounts. |
522
+ | **Symlink rejection** | `additionalBinds` sources must not be symlinks (prevents TOCTOU attacks). |
523
+ | **IPC rate limiting** | 200 calls/sec per method. Prevents resource exhaustion from rogue agents. |
524
+ | **IPC buffer limit** | 50 MB max. Peer stops on overflow to prevent memory exhaustion. |
525
+ | **Prototype pollution guard** | `__proto__`, `constructor`, `prototype` rejected as skill/credential IDs. |
526
+ | **Stdout interception** | IPC uses a dedicated fd. All other stdout is redirected to stderr. |
527
+ | **Sensitive path validation** | `workspaceDir`, `projectDir`, `nodeExecutable`, `additionalBinds` all validated. |
528
+ | **Atomic writes** | Config, credentials, sessions, skill state — all use tmp+rename pattern. |
529
+
530
+ ---
531
+
532
+ ## Provider Support
533
+
534
+ ### LLM Providers
535
+
536
+ Any provider supported by [pi-ai](https://github.com/nicepkg/pi-ai):
537
+
538
+ | Provider | Example Model |
539
+ |:---|:---|
540
+ | **Anthropic** | `claude-sonnet-4-20250514` |
541
+ | **Google** | `gemini-2.5-flash` |
542
+ | **OpenAI** | `gpt-4o` |
543
+ | **Groq** | `llama-3.3-70b-versatile` |
544
+ | **Cerebras** | `llama-3.3-70b` |
545
+ | **Mistral** | `mistral-large-latest` |
546
+ | **xAI** | `grok-3` |
547
+
548
+ ### Embedding Providers
549
+
550
+ Optional — keyword search works without any API key.
551
+
552
+ | Provider | Default Model | Local |
553
+ |:---|:---|:---:|
554
+ | **OpenAI** | `text-embedding-3-small` | |
555
+ | **Gemini** | `gemini-embedding-001` | |
556
+ | **Voyage** | `voyage-3-lite` | |
557
+ | **Mistral** | `mistral-embed` | |
558
+ | **Ollama** | `nomic-embed-text` | **Yes** |
559
+
560
+ ---
561
+
562
+ ## Architecture
563
+
564
+ ```
565
+ src/
566
+ ├── platform/ createPlatform() — workspace CRUD, skill registry
567
+ ├── workspace/ createWorkspace() — scoped memory, sessions, runner
568
+ │ └── runner.ts Out-of-process agent execution via sandbox + IPC
569
+ ├── sandbox/ Linux namespace sandbox — zero external deps
570
+ │ ├── namespace.ts unshare(2) + pivot_root
571
+ │ ├── cgroup.ts cgroups v2 resource limits (fail-closed)
572
+ │ ├── rootfs.ts Minimal rootfs with bind mounts
573
+ │ ├── ipc.ts Bidirectional JSON-RPC 2.0 over stdio
574
+ │ ├── seccomp.ts BPF syscall filter profiles (prepared)
575
+ │ ├── proxy-tools.ts memory/skill tools proxied to host via IPC
576
+ │ └── worker.ts Agent entry point inside sandbox
577
+ ├── credentials/ AES-256-GCM encrypted store + credential proxy
578
+ ├── skills/ Global registry + per-workspace enablement
579
+ ├── runtime/ createRuntime() — single-user mode
580
+ ├── memory/ Hybrid search engine
581
+ │ ├── hybrid.ts Vector + FTS5 fusion scoring
582
+ │ ├── mmr.ts Maximal Marginal Relevance re-ranking
583
+ │ ├── temporal-decay.ts Exponential time-based scoring
584
+ │ └── query-expansion.ts 7-language keyword expansion
585
+ ├── hooks/ 6 lifecycle hook points
586
+ ├── sessions/ File-based store with async locking
587
+ └── config/ Configuration loading
588
+ ```
589
+
590
+ **59 source files** · **3 runtime deps** · Sandbox, crypto, and IPC use **zero external deps** — all Node.js built-ins.
591
+
592
+ ---
593
+
594
+ ## State Directory
595
+
596
+ ```
597
+ <stateDir>/
598
+ ├── skills/ # Global skill catalog
599
+ │ └── <skill-id>/
600
+ │ ├── SKILL.md # LLM-readable skill description
601
+ │ └── execute.js # Runs with injected credentials
602
+ └── workspaces/
603
+ └── <userId>/
604
+ ├── config.json # Workspace config (no secrets)
605
+ ├── credentials.enc.json # AES-256-GCM encrypted credentials
606
+ ├── enabled-skills.json # Which skills this workspace can use
607
+ ├── sessions.json # Session index
608
+ ├── sessions/ # JSONL transcripts
609
+ └── memory/
610
+ └── memory.db # SQLite (vectors + FTS5)
611
+ ```
612
+
613
+ Every file is workspace-scoped. No shared state between tenants.
614
+
615
+ ---
616
+
617
+ ## Requirements
618
+
619
+ - **Linux** — bare metal, VM, or container with `--privileged`
620
+ - **Node.js 22.12+** — uses `node:sqlite` built-in
621
+ - **macOS / Windows** — `docker compose run dev` for development
622
+
623
+ ## License
624
+
625
+ [MIT](LICENSE)
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ import { createRuntime } from "./runtime/index.js";
3
+ async function main() {
4
+ const args = process.argv.slice(2);
5
+ function getFlag(flag) {
6
+ const i = args.indexOf(flag);
7
+ if (i === -1 || i + 1 >= args.length)
8
+ return undefined;
9
+ return args[i + 1];
10
+ }
11
+ const message = getFlag("--message") ?? getFlag("-m");
12
+ const sessionId = getFlag("--session");
13
+ const model = getFlag("--model");
14
+ const provider = getFlag("--provider");
15
+ const apiKey = getFlag("--api-key");
16
+ const workspaceDir = getFlag("--workspace");
17
+ const systemPrompt = getFlag("--system-prompt");
18
+ if (!message) {
19
+ console.error("Usage: openclaw --message <message> [--session <id>] [--model <model>] [--provider <provider>]");
20
+ process.exit(1);
21
+ }
22
+ const runtime = await createRuntime({
23
+ model,
24
+ provider,
25
+ apiKey,
26
+ workspaceDir,
27
+ });
28
+ const result = await runtime.run({
29
+ message,
30
+ sessionId,
31
+ model,
32
+ provider,
33
+ apiKey,
34
+ workspaceDir,
35
+ systemPrompt,
36
+ });
37
+ console.log(result.response);
38
+ }
39
+ main().catch((err) => {
40
+ console.error(err);
41
+ process.exit(1);
42
+ });
43
+ //# sourceMappingURL=cli.js.map