bulkhead-runtime 0.1.0 → 2026.4.5-beta.2

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 (199) hide show
  1. package/README.md +344 -262
  2. package/dist/cli.js +5 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config/index.d.ts +28 -0
  5. package/dist/config/index.d.ts.map +1 -1
  6. package/dist/config/index.js +9 -6
  7. package/dist/config/index.js.map +1 -1
  8. package/dist/credentials/store.d.ts.map +1 -1
  9. package/dist/credentials/store.js +39 -15
  10. package/dist/credentials/store.js.map +1 -1
  11. package/dist/index.d.ts +18 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +38 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/infra/warning-filter.js +1 -1
  16. package/dist/infra/warning-filter.js.map +1 -1
  17. package/dist/logging/subsystem.d.ts +15 -1
  18. package/dist/logging/subsystem.d.ts.map +1 -1
  19. package/dist/logging/subsystem.js +310 -45
  20. package/dist/logging/subsystem.js.map +1 -1
  21. package/dist/memory/embedding-batch.d.ts +38 -0
  22. package/dist/memory/embedding-batch.d.ts.map +1 -0
  23. package/dist/memory/embedding-batch.js +253 -0
  24. package/dist/memory/embedding-batch.js.map +1 -0
  25. package/dist/memory/embedding-cache.d.ts +16 -0
  26. package/dist/memory/embedding-cache.d.ts.map +1 -0
  27. package/dist/memory/embedding-cache.js +113 -0
  28. package/dist/memory/embedding-cache.js.map +1 -0
  29. package/dist/memory/embeddings-debug.js +1 -1
  30. package/dist/memory/embeddings.d.ts +1 -0
  31. package/dist/memory/embeddings.d.ts.map +1 -1
  32. package/dist/memory/embeddings.js +115 -92
  33. package/dist/memory/embeddings.js.map +1 -1
  34. package/dist/memory/file-indexer.d.ts +26 -0
  35. package/dist/memory/file-indexer.d.ts.map +1 -0
  36. package/dist/memory/file-indexer.js +258 -0
  37. package/dist/memory/file-indexer.js.map +1 -0
  38. package/dist/memory/hybrid.d.ts.map +1 -1
  39. package/dist/memory/hybrid.js +6 -2
  40. package/dist/memory/hybrid.js.map +1 -1
  41. package/dist/memory/index.d.ts +5 -0
  42. package/dist/memory/index.d.ts.map +1 -1
  43. package/dist/memory/index.js +5 -2
  44. package/dist/memory/index.js.map +1 -1
  45. package/dist/memory/session-indexer.d.ts +41 -0
  46. package/dist/memory/session-indexer.d.ts.map +1 -0
  47. package/dist/memory/session-indexer.js +367 -0
  48. package/dist/memory/session-indexer.js.map +1 -0
  49. package/dist/memory/simple-manager.d.ts +6 -0
  50. package/dist/memory/simple-manager.d.ts.map +1 -1
  51. package/dist/memory/simple-manager.js +35 -12
  52. package/dist/memory/simple-manager.js.map +1 -1
  53. package/dist/memory/ssrf.d.ts +18 -0
  54. package/dist/memory/ssrf.d.ts.map +1 -0
  55. package/dist/memory/ssrf.js +305 -0
  56. package/dist/memory/ssrf.js.map +1 -0
  57. package/dist/package.json +8 -5
  58. package/dist/platform/platform.d.ts.map +1 -1
  59. package/dist/platform/platform.js +30 -7
  60. package/dist/platform/platform.js.map +1 -1
  61. package/dist/platform/types.d.ts +2 -0
  62. package/dist/platform/types.d.ts.map +1 -1
  63. package/dist/runtime/agent.d.ts +8 -0
  64. package/dist/runtime/agent.d.ts.map +1 -1
  65. package/dist/runtime/agent.js +194 -46
  66. package/dist/runtime/agent.js.map +1 -1
  67. package/dist/runtime/api-key-rotation.d.ts +26 -0
  68. package/dist/runtime/api-key-rotation.d.ts.map +1 -0
  69. package/dist/runtime/api-key-rotation.js +174 -0
  70. package/dist/runtime/api-key-rotation.js.map +1 -0
  71. package/dist/runtime/context-guard.d.ts +32 -0
  72. package/dist/runtime/context-guard.d.ts.map +1 -0
  73. package/dist/runtime/context-guard.js +61 -0
  74. package/dist/runtime/context-guard.js.map +1 -0
  75. package/dist/runtime/failover-error.d.ts +62 -0
  76. package/dist/runtime/failover-error.d.ts.map +1 -0
  77. package/dist/runtime/failover-error.js +733 -0
  78. package/dist/runtime/failover-error.js.map +1 -0
  79. package/dist/runtime/failover-policy.d.ts +5 -0
  80. package/dist/runtime/failover-policy.d.ts.map +1 -0
  81. package/dist/runtime/failover-policy.js +18 -0
  82. package/dist/runtime/failover-policy.js.map +1 -0
  83. package/dist/runtime/index.d.ts +11 -0
  84. package/dist/runtime/index.d.ts.map +1 -1
  85. package/dist/runtime/index.js +11 -0
  86. package/dist/runtime/index.js.map +1 -1
  87. package/dist/runtime/memory-flush.d.ts +24 -0
  88. package/dist/runtime/memory-flush.d.ts.map +1 -0
  89. package/dist/runtime/memory-flush.js +64 -0
  90. package/dist/runtime/memory-flush.js.map +1 -0
  91. package/dist/runtime/memory-tools.d.ts +14 -0
  92. package/dist/runtime/memory-tools.d.ts.map +1 -0
  93. package/dist/runtime/memory-tools.js +58 -0
  94. package/dist/runtime/memory-tools.js.map +1 -0
  95. package/dist/runtime/model-fallback.d.ts +56 -0
  96. package/dist/runtime/model-fallback.d.ts.map +1 -0
  97. package/dist/runtime/model-fallback.js +301 -0
  98. package/dist/runtime/model-fallback.js.map +1 -0
  99. package/dist/runtime/model-fallback.types.d.ts +14 -0
  100. package/dist/runtime/model-fallback.types.d.ts.map +1 -0
  101. package/dist/runtime/model-fallback.types.js +3 -0
  102. package/dist/runtime/model-fallback.types.js.map +1 -0
  103. package/dist/runtime/retry.d.ts +24 -0
  104. package/dist/runtime/retry.d.ts.map +1 -0
  105. package/dist/runtime/retry.js +102 -0
  106. package/dist/runtime/retry.js.map +1 -0
  107. package/dist/runtime/session-pruning.d.ts +22 -0
  108. package/dist/runtime/session-pruning.d.ts.map +1 -0
  109. package/dist/runtime/session-pruning.js +118 -0
  110. package/dist/runtime/session-pruning.js.map +1 -0
  111. package/dist/runtime/stream-adapters.d.ts +11 -0
  112. package/dist/runtime/stream-adapters.d.ts.map +1 -0
  113. package/dist/runtime/stream-adapters.js +46 -0
  114. package/dist/runtime/stream-adapters.js.map +1 -0
  115. package/dist/runtime/subagent.d.ts +83 -0
  116. package/dist/runtime/subagent.d.ts.map +1 -0
  117. package/dist/runtime/subagent.js +190 -0
  118. package/dist/runtime/subagent.js.map +1 -0
  119. package/dist/runtime/tool-result-truncation.d.ts +25 -0
  120. package/dist/runtime/tool-result-truncation.d.ts.map +1 -0
  121. package/dist/runtime/tool-result-truncation.js +115 -0
  122. package/dist/runtime/tool-result-truncation.js.map +1 -0
  123. package/dist/sandbox/cgroup.d.ts +4 -1
  124. package/dist/sandbox/cgroup.d.ts.map +1 -1
  125. package/dist/sandbox/cgroup.js +28 -15
  126. package/dist/sandbox/cgroup.js.map +1 -1
  127. package/dist/sandbox/index.d.ts +2 -1
  128. package/dist/sandbox/index.d.ts.map +1 -1
  129. package/dist/sandbox/index.js +2 -1
  130. package/dist/sandbox/index.js.map +1 -1
  131. package/dist/sandbox/ipc.d.ts +4 -1
  132. package/dist/sandbox/ipc.d.ts.map +1 -1
  133. package/dist/sandbox/ipc.js +33 -17
  134. package/dist/sandbox/ipc.js.map +1 -1
  135. package/dist/sandbox/manager.d.ts +1 -2
  136. package/dist/sandbox/manager.d.ts.map +1 -1
  137. package/dist/sandbox/manager.js +136 -130
  138. package/dist/sandbox/manager.js.map +1 -1
  139. package/dist/sandbox/namespace.d.ts +1 -1
  140. package/dist/sandbox/namespace.d.ts.map +1 -1
  141. package/dist/sandbox/namespace.js +36 -37
  142. package/dist/sandbox/namespace.js.map +1 -1
  143. package/dist/sandbox/rootfs.d.ts +6 -1
  144. package/dist/sandbox/rootfs.d.ts.map +1 -1
  145. package/dist/sandbox/rootfs.js +114 -30
  146. package/dist/sandbox/rootfs.js.map +1 -1
  147. package/dist/sandbox/seccomp-apply.d.ts +9 -0
  148. package/dist/sandbox/seccomp-apply.d.ts.map +1 -0
  149. package/dist/sandbox/seccomp-apply.js +227 -0
  150. package/dist/sandbox/seccomp-apply.js.map +1 -0
  151. package/dist/sandbox/seccomp.js +3 -3
  152. package/dist/sandbox/seccomp.js.map +1 -1
  153. package/dist/sandbox/types.d.ts +1 -3
  154. package/dist/sandbox/types.d.ts.map +1 -1
  155. package/dist/sandbox/types.js.map +1 -1
  156. package/dist/sandbox/worker.d.ts +3 -0
  157. package/dist/sandbox/worker.d.ts.map +1 -1
  158. package/dist/sandbox/worker.js +84 -17
  159. package/dist/sandbox/worker.js.map +1 -1
  160. package/dist/sessions/index.d.ts +1 -0
  161. package/dist/sessions/index.d.ts.map +1 -1
  162. package/dist/sessions/index.js +1 -0
  163. package/dist/sessions/index.js.map +1 -1
  164. package/dist/sessions/store.d.ts +2 -2
  165. package/dist/sessions/store.d.ts.map +1 -1
  166. package/dist/sessions/store.js +49 -27
  167. package/dist/sessions/store.js.map +1 -1
  168. package/dist/sessions/transcript-events.d.ts +11 -0
  169. package/dist/sessions/transcript-events.d.ts.map +1 -0
  170. package/dist/sessions/transcript-events.js +40 -0
  171. package/dist/sessions/transcript-events.js.map +1 -0
  172. package/dist/shared/agent-session.d.ts +10 -0
  173. package/dist/shared/agent-session.d.ts.map +1 -0
  174. package/dist/shared/agent-session.js +33 -0
  175. package/dist/shared/agent-session.js.map +1 -0
  176. package/dist/shared/constants.d.ts +6 -0
  177. package/dist/shared/constants.d.ts.map +1 -0
  178. package/dist/shared/constants.js +11 -0
  179. package/dist/shared/constants.js.map +1 -0
  180. package/dist/shared/fs.d.ts +7 -0
  181. package/dist/shared/fs.d.ts.map +1 -0
  182. package/dist/shared/fs.js +14 -0
  183. package/dist/shared/fs.js.map +1 -0
  184. package/dist/shared/index.d.ts +4 -0
  185. package/dist/shared/index.d.ts.map +1 -0
  186. package/dist/shared/index.js +4 -0
  187. package/dist/shared/index.js.map +1 -0
  188. package/dist/skills/enablement.d.ts.map +1 -1
  189. package/dist/skills/enablement.js +2 -2
  190. package/dist/skills/enablement.js.map +1 -1
  191. package/dist/workspace/runner.d.ts.map +1 -1
  192. package/dist/workspace/runner.js +436 -105
  193. package/dist/workspace/runner.js.map +1 -1
  194. package/dist/workspace/types.d.ts +1 -0
  195. package/dist/workspace/types.d.ts.map +1 -1
  196. package/dist/workspace/workspace.d.ts.map +1 -1
  197. package/dist/workspace/workspace.js +12 -3
  198. package/dist/workspace/workspace.js.map +1 -1
  199. package/package.json +1 -1
package/README.md CHANGED
@@ -1,27 +1,12 @@
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 />
1
+ ![Bulkhead Runtime](docs/assets/hero-banner.png)
2
+
3
+ [![npm](https://img.shields.io/npm/v/bulkhead-runtime?style=flat-square&color=00ff41&labelColor=0d1117)](https://www.npmjs.com/package/bulkhead-runtime) ![MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square&labelColor=0d1117) ![node](https://img.shields.io/badge/node-%3E%3D22.12-brightgreen?style=flat-square&labelColor=0d1117&logo=node.js&logoColor=white) ![deps](https://img.shields.io/badge/runtime_deps-3-00ff41?style=flat-square&labelColor=0d1117) ![tests](https://img.shields.io/badge/tests-279_passing-00ff41?style=flat-square&labelColor=0d1117) ![isolation](https://img.shields.io/badge/sandbox-5_isolation_layers-ff6b6b?style=flat-square&labelColor=0d1117) ![crypto](https://img.shields.io/badge/crypto-AES--256--GCM-blueviolet?style=flat-square&labelColor=0d1117)
4
+
5
+ **Run 1,000 AI agents on a single Linux box.**
6
+ Each in its own OS namespace. Each with private memory, encrypted credentials, and an isolated filesystem.
7
+ **No Docker. No cloud. One `npm install`.**
8
+
9
+ ![Features](docs/assets/features.png)
25
10
 
26
11
  ---
27
12
 
@@ -36,10 +21,9 @@ import { createPlatform } from "bulkhead-runtime";
36
21
 
37
22
  const platform = createPlatform({
38
23
  stateDir: "/var/bulkhead-runtime",
39
- credentialPassphrase: process.env.CREDENTIAL_KEY,
24
+ credentialPassphrase: process.env.BULKHEAD_CREDENTIAL_KEY,
40
25
  });
41
26
 
42
- // Create isolated workspaces — one per user, team, or tenant
43
27
  const workspace = await platform.createWorkspace("user-42", {
44
28
  provider: "anthropic",
45
29
  model: "claude-sonnet-4-20250514",
@@ -49,31 +33,27 @@ const result = await workspace.run({
49
33
  message: "Refactor the auth module to use JWT",
50
34
  sessionId: "project-alpha",
51
35
  });
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
36
  ```
56
37
 
57
38
  > **Requires:** Linux + Node.js 22.12+
58
39
  >
59
40
  > **macOS / Windows dev:**
41
+ >
60
42
  > ```bash
61
43
  > git clone https://github.com/tonga54/bulkhead-runtime.git && cd bulkhead-runtime
62
44
  > docker compose run dev bash
63
- > pnpm test # 88 tests, all green
45
+ > pnpm test # 279 tests, all green
64
46
  > ```
65
47
 
66
48
  ---
67
49
 
68
50
  ## Use Cases
69
51
 
70
- <table>
71
- <tr>
72
- <td width="50%">
52
+ ![Use Cases](docs/assets/use-cases.png)
73
53
 
74
- **One agent per customer in your SaaS**
54
+ ### SaaS -- one agent per customer
75
55
 
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.
56
+ Customer A's agent can never see Customer B's tokens, data, or conversation history.
77
57
 
78
58
  ```typescript
79
59
  app.post("/api/agent", async (req, res) => {
@@ -86,118 +66,51 @@ app.post("/api/agent", async (req, res) => {
86
66
  });
87
67
  ```
88
68
 
89
- </td>
90
- <td width="50%">
91
-
92
- **Per-team agents inside your company**
69
+ ### Teams -- per-team agents, per-team secrets
93
70
 
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.
71
+ Different GitHub orgs, different databases, different cloud accounts. No credential leaks between teams.
95
72
 
96
73
  ```typescript
97
74
  const eng = await platform.createWorkspace("engineering");
98
75
  const ops = await platform.createWorkspace("ops");
99
- const data = await platform.createWorkspace("data-team");
100
76
 
101
77
  eng.skills.enable("github-pr");
102
78
  ops.skills.enable("pagerduty");
103
- data.skills.enable("bigquery");
104
79
 
105
80
  await eng.credentials.store("github", { token: "ghp_eng..." });
106
81
  await ops.credentials.store("pagerduty", { token: "pd_..." });
107
- await data.credentials.store("gcp", { key: "..." });
108
82
  ```
109
83
 
110
- </td>
111
- </tr>
112
- <tr>
113
- <td width="50%">
84
+ ### Consulting -- client isolation, clean offboarding
114
85
 
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.
86
+ One `deleteWorkspace()` wipes everything -- memory, credentials, sessions. Gone.
118
87
 
119
88
  ```typescript
120
89
  const acme = await platform.createWorkspace("client-acme");
121
90
  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
- });
91
+ await acme.run({ message: "Check staging and open a Jira ticket if it failed" });
128
92
 
129
- // Client offboarded clean removal
93
+ // Client offboarded -- clean wipe
130
94
  await platform.deleteWorkspace("client-acme");
131
95
  ```
132
96
 
133
- </td>
134
- <td width="50%">
135
-
136
- **Ephemeral agents for CI / PR review / task runners**
97
+ ### CI/CD -- ephemeral agents, zero state leaks
137
98
 
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.
99
+ Spin up per job, per PR, per deploy. The workspace is destroyed after. No state leaks between runs.
139
100
 
140
101
  ```typescript
141
- const jobId = `deploy-${Date.now()}`;
142
- const ws = await platform.createWorkspace(jobId);
143
-
102
+ const ws = await platform.createWorkspace(`deploy-${Date.now()}`);
144
103
  await ws.credentials.store("k8s", { kubeconfig: "..." });
145
104
  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);
105
+ const result = await ws.run({ message: "Roll out v2.3.1 to staging, run smoke tests" });
106
+ await platform.deleteWorkspace(ws.userId);
152
107
  ```
153
108
 
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
109
  ---
161
110
 
162
111
  ## How It Works
163
112
 
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
- ```
113
+ ![Architecture](docs/assets/architecture.png)
201
114
 
202
115
  > Agent A cannot see Agent B's files, memory, credentials, or processes. Not by policy. **By kernel enforcement.**
203
116
 
@@ -205,17 +118,23 @@ graph TB
205
118
 
206
119
  ## Why Bulkhead Over Alternatives
207
120
 
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** |
121
+
122
+ | | Docker per user | E2B / Cloud | **Bulkhead Runtime** |
123
+ | -------------------------------- | ------------------ | -------------------- | --------------------------------------------------------- |
124
+ | **Isolation mechanism** | Container per user | Cloud VM per session | **Linux namespaces** |
125
+ | **Credential security** | DIY | Not built-in | **AES-256-GCM, never exposed to agent** |
126
+ | **Persistent memory** | DIY | DIY | **SQLite + vector embeddings per tenant** |
127
+ | **Embedding cache + batch** | DIY | DIY | **SQLite cache, batch with retry** |
128
+ | **SSRF protection** | DIY | DIY | **DNS-validated, private-IP blocking, fail-closed** |
129
+ | **Model fallback** | DIY | DIY | **Automatic multi-model fallback chains** |
130
+ | **API key rotation** | DIY | DIY | **Multi-key with rate-limit rotation** |
131
+ | **Parallel subagents** | DIY | DIY | **Built-in with depth limiting** |
132
+ | **Skills with secret injection** | DIY | DIY | **Credentials injected server-side** |
133
+ | **Infrastructure** | Docker daemon | Cloud API + billing | **Single npm package** |
134
+ | **Cold start** | ~2s | ~5-10s | **~50ms** |
135
+ | **Embeddable in your app** | No | No | **Yes — it's a library** |
136
+ | **License** | — | Proprietary | **MIT** |
137
+
219
138
 
220
139
  ---
221
140
 
@@ -249,10 +168,9 @@ import { createPlatform } from "bulkhead-runtime";
249
168
 
250
169
  const platform = createPlatform({
251
170
  stateDir: "/var/bulkhead-runtime",
252
- credentialPassphrase: process.env.CREDENTIAL_KEY,
171
+ credentialPassphrase: process.env.BULKHEAD_CREDENTIAL_KEY,
253
172
  });
254
173
 
255
- // Each workspace is a universe unto itself
256
174
  const alice = await platform.createWorkspace("alice", {
257
175
  provider: "anthropic",
258
176
  model: "claude-sonnet-4-20250514",
@@ -268,33 +186,7 @@ const bob = await platform.createWorkspace("bob", {
268
186
 
269
187
  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
188
 
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
- ```
189
+ ![Execution Flow](docs/assets/execution-flow.png)
298
190
 
299
191
  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
192
 
@@ -305,33 +197,11 @@ The agent gets coding tools (bash, file read/write/edit) because the mount names
305
197
  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
198
 
307
199
  ```typescript
308
- // Store encrypted credentials for Alice
309
200
  await alice.credentials.store("github", { token: "ghp_alice_secret" });
310
201
  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
202
  ```
317
203
 
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
- ```
204
+ ![Credential Flow](docs/assets/credential-flow.png)
335
205
 
336
206
  System environment variables (`PATH`, `HOME`, `NODE_ENV`) are protected from credential key collision. Skill IDs are validated against prototype pollution.
337
207
 
@@ -342,24 +212,13 @@ System environment variables (`PATH`, `HOME`, `NODE_ENV`) are protected from cre
342
212
  Skills are registered globally and **enabled per workspace**. Each workspace gets exactly the capabilities it needs — nothing more.
343
213
 
344
214
  ```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
215
  const frontend = await platform.createWorkspace("team-frontend");
356
216
  const backend = await platform.createWorkspace("team-backend");
357
217
 
358
218
  frontend.skills.enable("github-issues");
359
219
  backend.skills.enable("github-issues");
360
- backend.skills.enable("db-migration"); // only backend gets this
220
+ backend.skills.enable("db-migration");
361
221
 
362
- // Store different credentials per workspace
363
222
  await frontend.credentials.store("github", { token: "ghp_frontend_token" });
364
223
  await backend.credentials.store("github", { token: "ghp_backend_token" });
365
224
  await backend.credentials.store("database", { url: "postgres://prod:5432/app" });
@@ -368,7 +227,7 @@ await backend.credentials.store("database", { url: "postgres://prod:5432/app" })
368
227
  ```javascript
369
228
  // skills/github-issues/execute.js
370
229
  const params = JSON.parse(await readStdin());
371
- const token = process.env.token; // Injected from encrypted store — never over IPC
230
+ const token = process.env.token;
372
231
  const res = await fetch(`https://api.github.com/repos/${params.repo}/issues`, {
373
232
  headers: { Authorization: `Bearer ${token}` },
374
233
  });
@@ -411,17 +270,19 @@ await runtime.run({
411
270
  // Agent searches memory → finds preferences → scaffolds TypeScript project
412
271
  ```
413
272
 
414
- **Search engine under the hood:**
273
+ **Hybrid search engine under the hood:**
415
274
 
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 |
275
+
276
+ | Stage | Algorithm |
277
+ | ------------------- | ---------------------------------------------------- |
278
+ | **Vector search** | Cosine similarity against stored embeddings |
279
+ | **Keyword search** | SQLite FTS5 with BM25 ranking |
280
+ | **Fusion** | Weighted merge of vector + keyword scores |
281
+ | **Temporal decay** | Exponential time-based score attenuation |
282
+ | **Diversity** | MMR (Maximal Marginal Relevance) re-ranking |
423
283
  | **Query expansion** | 7-language keyword extraction (EN/ES/PT/ZH/JA/KO/AR) |
424
284
 
285
+
425
286
  Works without any embedding API key — falls back to FTS5 keyword search.
426
287
 
427
288
  ---
@@ -447,23 +308,34 @@ Sessions are per-workspace, stored as JSONL transcripts with async locking.
447
308
 
448
309
  ## Subagent Orchestration
449
310
 
450
- A tool can spawn a child agent. The parent blocks until the child finishes.
311
+ Agents can spawn sub-agents for parallel or complex work. Sub-agents run concurrently with controlled parallelism and depth limiting.
451
312
 
452
313
  ```typescript
453
314
  const result = await runtime.run({
454
315
  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
- }],
316
+ enableSubagents: true,
317
+ });
318
+ ```
319
+
320
+ Or programmatically:
321
+
322
+ ```typescript
323
+ import { spawnSubagentsParallel } from "bulkhead-runtime";
324
+
325
+ const results = await spawnSubagentsParallel({
326
+ tasks: [
327
+ { id: "security", task: "Audit for SQL injection and XSS", label: "Security" },
328
+ { id: "perf", task: "Profile hot paths and suggest optimizations", label: "Performance" },
329
+ { id: "docs", task: "Check all public APIs have JSDoc", label: "Documentation" },
330
+ ],
331
+ maxConcurrent: 3,
332
+ run: async (task) => {
333
+ const r = await runtime.run({
334
+ message: task.task,
335
+ systemPrompt: `You are a ${task.label} expert.`,
336
+ });
337
+ return r.response;
338
+ },
467
339
  });
468
340
  ```
469
341
 
@@ -485,47 +357,212 @@ workspace.hooks.register("after_agent_end", async ({ sessionId, result }) => {
485
357
 
486
358
  ---
487
359
 
360
+ ## Model Fallback & API Key Rotation
361
+
362
+ When a model fails, Bulkhead automatically falls back to the next candidate. The error classification engine — ported from [OpenClaw](https://github.com/nicepkg/openclaw)'s battle-tested failover system — covers rate limits, billing, auth, overload, timeout, model not found, context overflow, and provider-specific patterns (Bedrock, Groq, Azure, Ollama, Mistral, etc.). Providers in cooldown are skipped automatically. When a key is rate-limited, it rotates to the next one.
363
+
364
+ ```typescript
365
+ const result = await runtime.run({
366
+ message: "Analyze this codebase",
367
+ provider: "anthropic",
368
+ model: "claude-sonnet-4-20250514",
369
+
370
+ fallbacks: ["openai/gpt-4o", "google/gemini-2.5-flash"],
371
+
372
+ apiKeys: [process.env.ANTHROPIC_KEY_1!, process.env.ANTHROPIC_KEY_2!],
373
+ });
374
+ ```
375
+
376
+ Or set keys via environment variables:
377
+
378
+ ```bash
379
+ ANTHROPIC_API_KEY=sk-ant-...
380
+ ANTHROPIC_API_KEYS=sk-ant-key1,sk-ant-key2,sk-ant-key3
381
+ ANTHROPIC_API_KEY_1=sk-ant-...
382
+ ANTHROPIC_API_KEY_2=sk-ant-...
383
+ ```
384
+
385
+ ---
386
+
387
+ ## Context Window Guards
388
+
389
+ Prevents silent failures from models with insufficient context windows.
390
+
391
+ ```typescript
392
+ import { resolveContextWindowInfo, evaluateContextWindowGuard } from "bulkhead-runtime";
393
+
394
+ const info = resolveContextWindowInfo({
395
+ modelContextWindow: model.contextWindow,
396
+ configContextTokens: 16_000,
397
+ });
398
+
399
+ const guard = evaluateContextWindowGuard({ info });
400
+ // guard.shouldWarn → true if below 32,000 tokens
401
+ // guard.shouldBlock → true if below 16,000 tokens
402
+ ```
403
+
404
+ The runtime applies these guards automatically before every agent execution.
405
+
406
+ ---
407
+
408
+ ## Retry with Compaction
409
+
410
+ Transient errors (rate limits, timeouts, context overflow) trigger automatic retry with exponential backoff.
411
+
412
+ ```typescript
413
+ const result = await runtime.run({
414
+ message: "Refactor the auth module",
415
+ maxRetries: 3,
416
+ });
417
+ // On context overflow → SDK compaction reduces history
418
+ // On 429/5xx → exponential backoff + jitter
419
+ ```
420
+
421
+ ---
422
+
423
+ ## Embedding Pipeline
424
+
425
+ ### Embedding Cache
426
+
427
+ Embeddings are cached in SQLite to avoid re-embedding unchanged content. Enabled by default.
428
+
429
+ ```typescript
430
+ const memory = createSimpleMemoryManager({
431
+ dbDir: "/var/data/memory",
432
+ embeddingProvider: createEmbeddingProvider({ provider: "openai", apiKey: "..." }),
433
+ enableEmbeddingCache: true,
434
+ maxCacheEntries: 50_000,
435
+ });
436
+ ```
437
+
438
+ ### Batch Embedding with Retry
439
+
440
+ ```typescript
441
+ import { embedBatchWithRetry } from "bulkhead-runtime";
442
+
443
+ const result = await embedBatchWithRetry(
444
+ ["text 1", "text 2", "text 3"],
445
+ {
446
+ provider: embeddingProvider,
447
+ cache: memory.embeddingCache ?? undefined,
448
+ batchSize: 100,
449
+ concurrency: 2,
450
+ retryAttempts: 3,
451
+ },
452
+ );
453
+ // result: { embeddings: [...], cached: 150, computed: 50, errors: 0 }
454
+ ```
455
+
456
+ ### SSRF Protection
457
+
458
+ All embedding provider HTTP calls are protected against Server-Side Request Forgery by default. The SSRF engine — ported from [OpenClaw](https://github.com/nicepkg/openclaw) — resolves DNS and pins IPs before connecting, blocks private/link-local ranges, and enforces a hostname allowlist. Fail-closed by default.
459
+
460
+ ```typescript
461
+ import { validateUrl, buildBaseUrlPolicy } from "bulkhead-runtime";
462
+
463
+ const provider = createEmbeddingProvider({
464
+ provider: "openai",
465
+ apiKey: "...",
466
+ enableSsrf: true,
467
+ });
468
+
469
+ await validateUrl("https://api.openai.com/v1/embeddings"); // OK
470
+ await validateUrl("http://169.254.1.1/steal"); // throws SSRF error
471
+ ```
472
+
473
+ ---
474
+
475
+ ## File-based Memory Indexing
476
+
477
+ Automatically watches `MEMORY.md` and `memory/` directory for changes and re-indexes them into the memory system.
478
+
479
+ ```typescript
480
+ import { createFileIndexer } from "bulkhead-runtime";
481
+
482
+ const indexer = createFileIndexer({
483
+ workspaceDir: "/path/to/workspace",
484
+ memory,
485
+ watchPaths: ["docs/"],
486
+ debounceMs: 2000,
487
+ });
488
+
489
+ indexer.start();
490
+ indexer.stop();
491
+ ```
492
+
493
+ ---
494
+
495
+ ## Session Transcript Indexing
496
+
497
+ Indexes session transcripts into memory for cross-session search. Supports post-compaction re-indexing. Ported from [OpenClaw](https://github.com/nicepkg/openclaw)'s memory-core extension.
498
+
499
+ ```typescript
500
+ import { createSessionIndexer } from "bulkhead-runtime";
501
+
502
+ const indexer = createSessionIndexer({
503
+ sessionsDir: path.join(stateDir, "sessions"),
504
+ memory,
505
+ deltaBytes: 4096,
506
+ deltaMessages: 10,
507
+ });
508
+
509
+ await indexer.indexAllSessions();
510
+ indexer.onTranscriptUpdate(sessionFile);
511
+ ```
512
+
513
+ ---
514
+
515
+ ## Structured Logging
516
+
517
+ Structured JSON logging with file output, rotation, and configurable levels.
518
+
519
+ ```typescript
520
+ import { configureLogger, createSubsystemLogger } from "bulkhead-runtime";
521
+
522
+ configureLogger({
523
+ level: "debug",
524
+ file: "/var/log/bulkhead-runtime.log",
525
+ maxFileBytes: 10 * 1024 * 1024,
526
+ json: true,
527
+ });
528
+
529
+ const log = createSubsystemLogger("my-module");
530
+ log.info("agent started", { userId: "alice", model: "claude-sonnet-4-20250514" });
531
+ ```
532
+
533
+ Or via environment:
534
+
535
+ ```bash
536
+ BULKHEAD_LOG_LEVEL=debug
537
+ BULKHEAD_LOG_FILE=/var/log/bulkhead-runtime.log
538
+ ```
539
+
540
+ ---
541
+
488
542
  ## Security Architecture
489
543
 
490
544
  ### 5 Layers of Sandbox Isolation
491
545
 
492
546
  All layers are **fail-closed** — if any layer can't be applied, the sandbox refuses to start.
493
547
 
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
- ```
548
+ ![Security Layers](docs/assets/security-layers.png)
514
549
 
515
550
  ### Defense in Depth
516
551
 
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. |
552
+
553
+ | Defense | Mechanism |
554
+ | ----------------------------- | ---------------------------------------------------------------------------------------------------- |
555
+ | **Env allowlist** | Only `PATH`, `HOME`, `NODE_ENV` + the single API key the agent needs. Everything else dropped. |
556
+ | **Credential proxy** | Secrets decrypted server-side, injected into skill execution. Never sent over IPC. |
557
+ | **Path traversal blocklist** | `/proc`, `/sys`, `/home/`, `/etc/shadow`, `/run/docker.sock`, and more are blocked from bind mounts. |
558
+ | **Symlink rejection** | `additionalBinds` sources must not be symlinks (prevents TOCTOU attacks). |
559
+ | **IPC rate limiting** | 200 calls/sec per method. Prevents resource exhaustion from rogue agents. |
560
+ | **IPC buffer limit** | 50 MB max. Peer stops on overflow to prevent memory exhaustion. |
561
+ | **Prototype pollution guard** | `__proto__`, `constructor`, `prototype` rejected as skill/credential IDs. |
562
+ | **Stdout interception** | IPC uses a dedicated fd. All other stdout is redirected to stderr. |
563
+ | **Sensitive path validation** | `workspaceDir`, `projectDir`, `nodeExecutable`, `additionalBinds` all validated. |
564
+ | **Atomic writes** | Config, credentials, sessions, skill state — all use tmp+rename pattern. |
565
+
529
566
 
530
567
  ---
531
568
 
@@ -535,27 +572,31 @@ block-beta
535
572
 
536
573
  Any provider supported by [pi-ai](https://github.com/nicepkg/pi-ai):
537
574
 
538
- | Provider | Example Model |
539
- |:---|:---|
575
+
576
+ | Provider | Example Model |
577
+ | ------------- | -------------------------- |
540
578
  | **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` |
579
+ | **Google** | `gemini-2.5-flash` |
580
+ | **OpenAI** | `gpt-4o` |
581
+ | **Groq** | `llama-3.3-70b-versatile` |
582
+ | **Cerebras** | `llama-3.3-70b` |
583
+ | **Mistral** | `mistral-large-latest` |
584
+ | **xAI** | `grok-3` |
585
+
547
586
 
548
587
  ### Embedding Providers
549
588
 
550
589
  Optional — keyword search works without any API key.
551
590
 
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** |
591
+
592
+ | Provider | Default Model | Local |
593
+ | ----------- | ------------------------ | ------- |
594
+ | **OpenAI** | `text-embedding-3-small` | |
595
+ | **Gemini** | `gemini-embedding-001` | |
596
+ | **Voyage** | `voyage-3-lite` | |
597
+ | **Mistral** | `mistral-embed` | |
598
+ | **Ollama** | `nomic-embed-text` | **Yes** |
599
+
559
600
 
560
601
  ---
561
602
 
@@ -571,23 +612,40 @@ src/
571
612
  │ ├── cgroup.ts cgroups v2 resource limits (fail-closed)
572
613
  │ ├── rootfs.ts Minimal rootfs with bind mounts
573
614
  │ ├── ipc.ts Bidirectional JSON-RPC 2.0 over stdio
574
- │ ├── seccomp.ts BPF syscall filter profiles (prepared)
615
+ │ ├── seccomp.ts BPF syscall filter profiles
616
+ │ ├── seccomp-apply.ts seccomp-BPF application via C helper
575
617
  │ ├── proxy-tools.ts memory/skill tools proxied to host via IPC
576
618
  │ └── worker.ts Agent entry point inside sandbox
577
619
  ├── credentials/ AES-256-GCM encrypted store + credential proxy
578
620
  ├── skills/ Global registry + per-workspace enablement
579
621
  ├── runtime/ createRuntime() — single-user mode
622
+ │ ├── failover-error.ts Error classification (ported from OpenClaw)
623
+ │ ├── model-fallback.ts Fallback chains + cooldown tracking
624
+ │ ├── context-guard.ts Context window guards (16K/32K thresholds)
625
+ │ ├── retry.ts Exponential backoff with jitter + retryAfterMs
626
+ │ ├── api-key-rotation.ts Per-provider key rotation
627
+ │ ├── subagent.ts Parallel execution + lifecycle + registry
628
+ │ ├── session-pruning.ts Trim old tool results in context
629
+ │ ├── tool-result-truncation.ts Truncate oversized tool output
630
+ │ ├── memory-flush.ts Pre-compaction memory save
631
+ │ └── stream-adapters.ts Per-provider stream configuration
580
632
  ├── memory/ Hybrid search engine
581
633
  │ ├── hybrid.ts Vector + FTS5 fusion scoring
582
634
  │ ├── mmr.ts Maximal Marginal Relevance re-ranking
583
635
  │ ├── temporal-decay.ts Exponential time-based scoring
584
- └── query-expansion.ts 7-language keyword expansion
636
+ ├── query-expansion.ts 7-language keyword expansion
637
+ │ ├── embedding-cache.ts SQLite embedding cache
638
+ │ ├── embedding-batch.ts Batch embedding with retry
639
+ │ ├── file-indexer.ts File-based memory indexing with fs.watch
640
+ │ ├── session-indexer.ts Session transcript indexing
641
+ │ └── ssrf.ts SSRF protection for HTTP calls
585
642
  ├── hooks/ 6 lifecycle hook points
643
+ ├── logging/ Structured JSON logging with file output
586
644
  ├── sessions/ File-based store with async locking
587
645
  └── config/ Configuration loading
588
646
  ```
589
647
 
590
- **59 source files** · **3 runtime deps** · Sandbox, crypto, and IPC use **zero external deps** — all Node.js built-ins.
648
+ **78 source files** · **3 runtime deps** · Sandbox, crypto, IPC, logging, and SSRF use **zero external deps** — all Node.js built-ins.
591
649
 
592
650
  ---
593
651
 
@@ -614,6 +672,30 @@ Every file is workspace-scoped. No shared state between tenants.
614
672
 
615
673
  ---
616
674
 
675
+ ## Built on OpenClaw
676
+
677
+ Bulkhead Runtime stands on the shoulders of [OpenClaw](https://github.com/nicepkg/openclaw). Several production-critical subsystems were ported directly from OpenClaw's codebase:
678
+
679
+
680
+ | Subsystem | Origin |
681
+ | ----------------------------------------- | ------------------------------------------------------------------------- |
682
+ | **Model fallback & error classification** | `src/agents/model-fallback.ts`, `failover-error.ts`, `failover-policy.ts` |
683
+ | **API key rotation** | `src/agents/api-key-rotation.ts`, `live-auth-keys.ts` |
684
+ | **Context window guards** | `src/agents/context-window-guard.ts` |
685
+ | **Retry with backoff** | `src/infra/retry.ts` |
686
+ | **SSRF protection** | `src/infra/net/ssrf.ts`, `fetch-guard.ts` |
687
+ | **Embedding cache & batch** | `extensions/memory-core/src/memory/manager-embedding-ops.ts` |
688
+ | **File indexer** | `extensions/memory-core/src/memory/manager-sync-ops.ts` |
689
+ | **Session transcript indexer** | `extensions/memory-core/src/memory/manager-sync-ops.ts` |
690
+ | **Subagent orchestration** | `src/agents/subagent-*.ts` |
691
+ | **Structured logging** | `src/logging/logger.ts`, `subsystem.ts`, `levels.ts` |
692
+ | **Transcript events** | `src/sessions/transcript-events.ts` |
693
+
694
+
695
+ OpenClaw solved these problems in production. We extracted, adapted, and integrated them into Bulkhead's multi-tenant architecture. Full credit where it's due.
696
+
697
+ ---
698
+
617
699
  ## Requirements
618
700
 
619
701
  - **Linux** — bare metal, VM, or container with `--privileged`
@@ -622,4 +704,4 @@ Every file is workspace-scoped. No shared state between tenants.
622
704
 
623
705
  ## License
624
706
 
625
- [MIT](LICENSE)
707
+ [MIT](LICENSE)