bulkhead-runtime 0.1.0 → 2026.4.5-beta.1

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 +337 -234
  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 +245 -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 +341 -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 +316 -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 +132 -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 +353 -106
  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,14 @@
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
+ *Built on production-hardened internals from [OpenClaw](https://github.com/nicepkg/openclaw) failover, SSRF protection, embedding pipelines, session indexing, and more.*
10
+
11
+ ![Features](docs/assets/features.png)
25
12
 
26
13
  ---
27
14
 
@@ -36,10 +23,9 @@ import { createPlatform } from "bulkhead-runtime";
36
23
 
37
24
  const platform = createPlatform({
38
25
  stateDir: "/var/bulkhead-runtime",
39
- credentialPassphrase: process.env.CREDENTIAL_KEY,
26
+ credentialPassphrase: process.env.BULKHEAD_CREDENTIAL_KEY,
40
27
  });
41
28
 
42
- // Create isolated workspaces — one per user, team, or tenant
43
29
  const workspace = await platform.createWorkspace("user-42", {
44
30
  provider: "anthropic",
45
31
  model: "claude-sonnet-4-20250514",
@@ -49,27 +35,27 @@ const result = await workspace.run({
49
35
  message: "Refactor the auth module to use JWT",
50
36
  sessionId: "project-alpha",
51
37
  });
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
38
  ```
56
39
 
57
40
  > **Requires:** Linux + Node.js 22.12+
58
41
  >
59
42
  > **macOS / Windows dev:**
43
+ >
60
44
  > ```bash
61
45
  > git clone https://github.com/tonga54/bulkhead-runtime.git && cd bulkhead-runtime
62
46
  > docker compose run dev bash
63
- > pnpm test # 88 tests, all green
47
+ > pnpm test # 279 tests, all green
64
48
  > ```
65
49
 
66
50
  ---
67
51
 
68
52
  ## Use Cases
69
53
 
70
- <table>
71
- <tr>
72
- <td width="50%">
54
+
55
+ | |
56
+ | --- |
57
+ | |
58
+
73
59
 
74
60
  **One agent per customer in your SaaS**
75
61
 
@@ -86,9 +72,6 @@ app.post("/api/agent", async (req, res) => {
86
72
  });
87
73
  ```
88
74
 
89
- </td>
90
- <td width="50%">
91
-
92
75
  **Per-team agents inside your company**
93
76
 
94
77
  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.
@@ -107,11 +90,6 @@ await ops.credentials.store("pagerduty", { token: "pd_..." });
107
90
  await data.credentials.store("gcp", { key: "..." });
108
91
  ```
109
92
 
110
- </td>
111
- </tr>
112
- <tr>
113
- <td width="50%">
114
-
115
93
  **Client-isolated agents in consulting / agencies**
116
94
 
117
95
  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.
@@ -126,13 +104,9 @@ await acme.run({
126
104
  sessionId: "daily-ops",
127
105
  });
128
106
 
129
- // Client offboarded — clean removal
130
107
  await platform.deleteWorkspace("client-acme");
131
108
  ```
132
109
 
133
- </td>
134
- <td width="50%">
135
-
136
110
  **Ephemeral agents for CI / PR review / task runners**
137
111
 
138
112
  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.
@@ -151,53 +125,13 @@ const result = await ws.run({
151
125
  await platform.deleteWorkspace(jobId);
152
126
  ```
153
127
 
154
- </td>
155
- </tr>
156
- </table>
157
-
158
128
  **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
129
 
160
130
  ---
161
131
 
162
132
  ## How It Works
163
133
 
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
- ```
134
+ ![Architecture](docs/assets/architecture.png)
201
135
 
202
136
  > Agent A cannot see Agent B's files, memory, credentials, or processes. Not by policy. **By kernel enforcement.**
203
137
 
@@ -205,17 +139,23 @@ graph TB
205
139
 
206
140
  ## Why Bulkhead Over Alternatives
207
141
 
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** |
142
+
143
+ | | Docker per user | E2B / Cloud | **Bulkhead Runtime** |
144
+ | -------------------------------- | ------------------ | -------------------- | --------------------------------------------------------- |
145
+ | **Isolation mechanism** | Container per user | Cloud VM per session | **Linux namespaces** |
146
+ | **Credential security** | DIY | Not built-in | **AES-256-GCM, never exposed to agent** |
147
+ | **Persistent memory** | DIY | DIY | **SQLite + vector embeddings per tenant** |
148
+ | **Embedding cache + batch** | DIY | DIY | **SQLite cache, batch with retry** |
149
+ | **SSRF protection** | DIY | DIY | **DNS-resolve-and-pin, private-IP blocking, fail-closed** |
150
+ | **Model fallback** | DIY | DIY | **Automatic multi-model fallback chains** |
151
+ | **API key rotation** | DIY | DIY | **Multi-key with rate-limit rotation** |
152
+ | **Parallel subagents** | DIY | DIY | **Built-in with depth limiting** |
153
+ | **Skills with secret injection** | DIY | DIY | **Credentials injected server-side** |
154
+ | **Infrastructure** | Docker daemon | Cloud API + billing | **Single npm package** |
155
+ | **Cold start** | ~2s | ~5-10s | **~50ms** |
156
+ | **Embeddable in your app** | No | No | **Yes — it's a library** |
157
+ | **License** | — | Proprietary | **MIT** |
158
+
219
159
 
220
160
  ---
221
161
 
@@ -249,10 +189,9 @@ import { createPlatform } from "bulkhead-runtime";
249
189
 
250
190
  const platform = createPlatform({
251
191
  stateDir: "/var/bulkhead-runtime",
252
- credentialPassphrase: process.env.CREDENTIAL_KEY,
192
+ credentialPassphrase: process.env.BULKHEAD_CREDENTIAL_KEY,
253
193
  });
254
194
 
255
- // Each workspace is a universe unto itself
256
195
  const alice = await platform.createWorkspace("alice", {
257
196
  provider: "anthropic",
258
197
  model: "claude-sonnet-4-20250514",
@@ -268,33 +207,7 @@ const bob = await platform.createWorkspace("bob", {
268
207
 
269
208
  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
209
 
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
- ```
210
+ ![Execution Flow](docs/assets/execution-flow.png)
298
211
 
299
212
  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
213
 
@@ -305,33 +218,11 @@ The agent gets coding tools (bash, file read/write/edit) because the mount names
305
218
  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
219
 
307
220
  ```typescript
308
- // Store encrypted credentials for Alice
309
221
  await alice.credentials.store("github", { token: "ghp_alice_secret" });
310
222
  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
223
  ```
317
224
 
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
- ```
225
+ ![Credential Flow](docs/assets/credential-flow.png)
335
226
 
336
227
  System environment variables (`PATH`, `HOME`, `NODE_ENV`) are protected from credential key collision. Skill IDs are validated against prototype pollution.
337
228
 
@@ -342,24 +233,13 @@ System environment variables (`PATH`, `HOME`, `NODE_ENV`) are protected from cre
342
233
  Skills are registered globally and **enabled per workspace**. Each workspace gets exactly the capabilities it needs — nothing more.
343
234
 
344
235
  ```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
236
  const frontend = await platform.createWorkspace("team-frontend");
356
237
  const backend = await platform.createWorkspace("team-backend");
357
238
 
358
239
  frontend.skills.enable("github-issues");
359
240
  backend.skills.enable("github-issues");
360
- backend.skills.enable("db-migration"); // only backend gets this
241
+ backend.skills.enable("db-migration");
361
242
 
362
- // Store different credentials per workspace
363
243
  await frontend.credentials.store("github", { token: "ghp_frontend_token" });
364
244
  await backend.credentials.store("github", { token: "ghp_backend_token" });
365
245
  await backend.credentials.store("database", { url: "postgres://prod:5432/app" });
@@ -368,7 +248,7 @@ await backend.credentials.store("database", { url: "postgres://prod:5432/app" })
368
248
  ```javascript
369
249
  // skills/github-issues/execute.js
370
250
  const params = JSON.parse(await readStdin());
371
- const token = process.env.token; // Injected from encrypted store — never over IPC
251
+ const token = process.env.token;
372
252
  const res = await fetch(`https://api.github.com/repos/${params.repo}/issues`, {
373
253
  headers: { Authorization: `Bearer ${token}` },
374
254
  });
@@ -411,17 +291,19 @@ await runtime.run({
411
291
  // Agent searches memory → finds preferences → scaffolds TypeScript project
412
292
  ```
413
293
 
414
- **Search engine under the hood:**
294
+ **Hybrid search engine under the hood:**
295
+
415
296
 
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 |
297
+ | Stage | Algorithm |
298
+ | ------------------- | ---------------------------------------------------- |
299
+ | **Vector search** | Cosine similarity against stored embeddings |
300
+ | **Keyword search** | SQLite FTS5 with BM25 ranking |
301
+ | **Fusion** | Weighted merge of vector + keyword scores |
302
+ | **Temporal decay** | Exponential time-based score attenuation |
303
+ | **Diversity** | MMR (Maximal Marginal Relevance) re-ranking |
423
304
  | **Query expansion** | 7-language keyword extraction (EN/ES/PT/ZH/JA/KO/AR) |
424
305
 
306
+
425
307
  Works without any embedding API key — falls back to FTS5 keyword search.
426
308
 
427
309
  ---
@@ -447,23 +329,34 @@ Sessions are per-workspace, stored as JSONL transcripts with async locking.
447
329
 
448
330
  ## Subagent Orchestration
449
331
 
450
- A tool can spawn a child agent. The parent blocks until the child finishes.
332
+ Agents can spawn sub-agents for parallel or complex work. Sub-agents run concurrently with controlled parallelism and depth limiting.
451
333
 
452
334
  ```typescript
453
335
  const result = await runtime.run({
454
336
  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
- }],
337
+ enableSubagents: true,
338
+ });
339
+ ```
340
+
341
+ Or programmatically:
342
+
343
+ ```typescript
344
+ import { spawnSubagentsParallel } from "bulkhead-runtime";
345
+
346
+ const results = await spawnSubagentsParallel({
347
+ tasks: [
348
+ { id: "security", task: "Audit for SQL injection and XSS", label: "Security" },
349
+ { id: "perf", task: "Profile hot paths and suggest optimizations", label: "Performance" },
350
+ { id: "docs", task: "Check all public APIs have JSDoc", label: "Documentation" },
351
+ ],
352
+ maxConcurrent: 3,
353
+ run: async (task) => {
354
+ const r = await runtime.run({
355
+ message: task.task,
356
+ systemPrompt: `You are a ${task.label} expert.`,
357
+ });
358
+ return r.response;
359
+ },
467
360
  });
468
361
  ```
469
362
 
@@ -485,47 +378,212 @@ workspace.hooks.register("after_agent_end", async ({ sessionId, result }) => {
485
378
 
486
379
  ---
487
380
 
381
+ ## Model Fallback & API Key Rotation
382
+
383
+ 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.
384
+
385
+ ```typescript
386
+ const result = await runtime.run({
387
+ message: "Analyze this codebase",
388
+ provider: "anthropic",
389
+ model: "claude-sonnet-4-20250514",
390
+
391
+ fallbacks: ["openai/gpt-4o", "google/gemini-2.5-flash"],
392
+
393
+ apiKeys: [process.env.ANTHROPIC_KEY_1!, process.env.ANTHROPIC_KEY_2!],
394
+ });
395
+ ```
396
+
397
+ Or set keys via environment variables:
398
+
399
+ ```bash
400
+ ANTHROPIC_API_KEY=sk-ant-...
401
+ ANTHROPIC_API_KEYS=sk-ant-key1,sk-ant-key2,sk-ant-key3
402
+ ANTHROPIC_API_KEY_1=sk-ant-...
403
+ ANTHROPIC_API_KEY_2=sk-ant-...
404
+ ```
405
+
406
+ ---
407
+
408
+ ## Context Window Guards
409
+
410
+ Prevents silent failures from models with insufficient context windows.
411
+
412
+ ```typescript
413
+ import { resolveContextWindowInfo, evaluateContextWindowGuard } from "bulkhead-runtime";
414
+
415
+ const info = resolveContextWindowInfo({
416
+ modelContextWindow: model.contextWindow,
417
+ configContextTokens: 16_000,
418
+ });
419
+
420
+ const guard = evaluateContextWindowGuard({ info });
421
+ // guard.shouldWarn → true if below 32,000 tokens
422
+ // guard.shouldBlock → true if below 16,000 tokens
423
+ ```
424
+
425
+ The runtime applies these guards automatically before every agent execution.
426
+
427
+ ---
428
+
429
+ ## Retry with Compaction
430
+
431
+ Transient errors (rate limits, timeouts, context overflow) trigger automatic retry with exponential backoff.
432
+
433
+ ```typescript
434
+ const result = await runtime.run({
435
+ message: "Refactor the auth module",
436
+ maxRetries: 3,
437
+ });
438
+ // On context overflow → SDK compaction reduces history
439
+ // On 429/5xx → exponential backoff + jitter
440
+ ```
441
+
442
+ ---
443
+
444
+ ## Embedding Pipeline
445
+
446
+ ### Embedding Cache
447
+
448
+ Embeddings are cached in SQLite to avoid re-embedding unchanged content. Enabled by default.
449
+
450
+ ```typescript
451
+ const memory = createSimpleMemoryManager({
452
+ dbDir: "/var/data/memory",
453
+ embeddingProvider: createEmbeddingProvider({ provider: "openai", apiKey: "..." }),
454
+ enableEmbeddingCache: true,
455
+ maxCacheEntries: 50_000,
456
+ });
457
+ ```
458
+
459
+ ### Batch Embedding with Retry
460
+
461
+ ```typescript
462
+ import { embedBatchWithRetry } from "bulkhead-runtime";
463
+
464
+ const result = await embedBatchWithRetry(
465
+ ["text 1", "text 2", "text 3"],
466
+ {
467
+ provider: embeddingProvider,
468
+ cache: memory.embeddingCache ?? undefined,
469
+ batchSize: 100,
470
+ concurrency: 2,
471
+ retryAttempts: 3,
472
+ },
473
+ );
474
+ // result: { embeddings: [...], cached: 150, computed: 50, errors: 0 }
475
+ ```
476
+
477
+ ### SSRF Protection
478
+
479
+ 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.
480
+
481
+ ```typescript
482
+ import { validateUrl, buildBaseUrlPolicy } from "bulkhead-runtime";
483
+
484
+ const provider = createEmbeddingProvider({
485
+ provider: "openai",
486
+ apiKey: "...",
487
+ enableSsrf: true,
488
+ });
489
+
490
+ await validateUrl("https://api.openai.com/v1/embeddings"); // OK
491
+ await validateUrl("http://169.254.1.1/steal"); // throws SSRF error
492
+ ```
493
+
494
+ ---
495
+
496
+ ## File-based Memory Indexing
497
+
498
+ Automatically watches `MEMORY.md` and `memory/` directory for changes and re-indexes them into the memory system.
499
+
500
+ ```typescript
501
+ import { createFileIndexer } from "bulkhead-runtime";
502
+
503
+ const indexer = createFileIndexer({
504
+ workspaceDir: "/path/to/workspace",
505
+ memory,
506
+ watchPaths: ["docs/"],
507
+ debounceMs: 2000,
508
+ });
509
+
510
+ indexer.start();
511
+ indexer.stop();
512
+ ```
513
+
514
+ ---
515
+
516
+ ## Session Transcript Indexing
517
+
518
+ 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.
519
+
520
+ ```typescript
521
+ import { createSessionIndexer } from "bulkhead-runtime";
522
+
523
+ const indexer = createSessionIndexer({
524
+ sessionsDir: path.join(stateDir, "sessions"),
525
+ memory,
526
+ deltaBytes: 4096,
527
+ deltaMessages: 10,
528
+ });
529
+
530
+ await indexer.indexAllSessions();
531
+ indexer.onTranscriptUpdate(sessionFile);
532
+ ```
533
+
534
+ ---
535
+
536
+ ## Structured Logging
537
+
538
+ Structured JSON logging with file output, rotation, and configurable levels.
539
+
540
+ ```typescript
541
+ import { configureLogger, createSubsystemLogger } from "bulkhead-runtime";
542
+
543
+ configureLogger({
544
+ level: "debug",
545
+ file: "/var/log/bulkhead-runtime.log",
546
+ maxFileBytes: 10 * 1024 * 1024,
547
+ json: true,
548
+ });
549
+
550
+ const log = createSubsystemLogger("my-module");
551
+ log.info("agent started", { userId: "alice", model: "claude-sonnet-4-20250514" });
552
+ ```
553
+
554
+ Or via environment:
555
+
556
+ ```bash
557
+ BULKHEAD_LOG_LEVEL=debug
558
+ BULKHEAD_LOG_FILE=/var/log/bulkhead-runtime.log
559
+ ```
560
+
561
+ ---
562
+
488
563
  ## Security Architecture
489
564
 
490
565
  ### 5 Layers of Sandbox Isolation
491
566
 
492
567
  All layers are **fail-closed** — if any layer can't be applied, the sandbox refuses to start.
493
568
 
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
- ```
569
+ ![Security Layers](docs/assets/security-layers.png)
514
570
 
515
571
  ### Defense in Depth
516
572
 
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. |
573
+
574
+ | Defense | Mechanism |
575
+ | ----------------------------- | ---------------------------------------------------------------------------------------------------- |
576
+ | **Env allowlist** | Only `PATH`, `HOME`, `NODE_ENV` + the single API key the agent needs. Everything else dropped. |
577
+ | **Credential proxy** | Secrets decrypted server-side, injected into skill execution. Never sent over IPC. |
578
+ | **Path traversal blocklist** | `/proc`, `/sys`, `/home/`, `/etc/shadow`, `/run/docker.sock`, and more are blocked from bind mounts. |
579
+ | **Symlink rejection** | `additionalBinds` sources must not be symlinks (prevents TOCTOU attacks). |
580
+ | **IPC rate limiting** | 200 calls/sec per method. Prevents resource exhaustion from rogue agents. |
581
+ | **IPC buffer limit** | 50 MB max. Peer stops on overflow to prevent memory exhaustion. |
582
+ | **Prototype pollution guard** | `__proto__`, `constructor`, `prototype` rejected as skill/credential IDs. |
583
+ | **Stdout interception** | IPC uses a dedicated fd. All other stdout is redirected to stderr. |
584
+ | **Sensitive path validation** | `workspaceDir`, `projectDir`, `nodeExecutable`, `additionalBinds` all validated. |
585
+ | **Atomic writes** | Config, credentials, sessions, skill state — all use tmp+rename pattern. |
586
+
529
587
 
530
588
  ---
531
589
 
@@ -535,27 +593,31 @@ block-beta
535
593
 
536
594
  Any provider supported by [pi-ai](https://github.com/nicepkg/pi-ai):
537
595
 
538
- | Provider | Example Model |
539
- |:---|:---|
596
+
597
+ | Provider | Example Model |
598
+ | ------------- | -------------------------- |
540
599
  | **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` |
600
+ | **Google** | `gemini-2.5-flash` |
601
+ | **OpenAI** | `gpt-4o` |
602
+ | **Groq** | `llama-3.3-70b-versatile` |
603
+ | **Cerebras** | `llama-3.3-70b` |
604
+ | **Mistral** | `mistral-large-latest` |
605
+ | **xAI** | `grok-3` |
606
+
547
607
 
548
608
  ### Embedding Providers
549
609
 
550
610
  Optional — keyword search works without any API key.
551
611
 
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** |
612
+
613
+ | Provider | Default Model | Local |
614
+ | ----------- | ------------------------ | ------- |
615
+ | **OpenAI** | `text-embedding-3-small` | |
616
+ | **Gemini** | `gemini-embedding-001` | |
617
+ | **Voyage** | `voyage-3-lite` | |
618
+ | **Mistral** | `mistral-embed` | |
619
+ | **Ollama** | `nomic-embed-text` | **Yes** |
620
+
559
621
 
560
622
  ---
561
623
 
@@ -571,23 +633,40 @@ src/
571
633
  │ ├── cgroup.ts cgroups v2 resource limits (fail-closed)
572
634
  │ ├── rootfs.ts Minimal rootfs with bind mounts
573
635
  │ ├── ipc.ts Bidirectional JSON-RPC 2.0 over stdio
574
- │ ├── seccomp.ts BPF syscall filter profiles (prepared)
636
+ │ ├── seccomp.ts BPF syscall filter profiles
637
+ │ ├── seccomp-apply.ts seccomp-BPF application via C helper
575
638
  │ ├── proxy-tools.ts memory/skill tools proxied to host via IPC
576
639
  │ └── worker.ts Agent entry point inside sandbox
577
640
  ├── credentials/ AES-256-GCM encrypted store + credential proxy
578
641
  ├── skills/ Global registry + per-workspace enablement
579
642
  ├── runtime/ createRuntime() — single-user mode
643
+ │ ├── failover-error.ts Error classification (ported from OpenClaw)
644
+ │ ├── model-fallback.ts Fallback chains + cooldown tracking
645
+ │ ├── context-guard.ts Context window guards (16K/32K thresholds)
646
+ │ ├── retry.ts Exponential backoff with jitter + retryAfterMs
647
+ │ ├── api-key-rotation.ts Per-provider key rotation
648
+ │ ├── subagent.ts Parallel execution + lifecycle + registry
649
+ │ ├── session-pruning.ts Trim old tool results in context
650
+ │ ├── tool-result-truncation.ts Truncate oversized tool output
651
+ │ ├── memory-flush.ts Pre-compaction memory save
652
+ │ └── stream-adapters.ts Per-provider stream configuration
580
653
  ├── memory/ Hybrid search engine
581
654
  │ ├── hybrid.ts Vector + FTS5 fusion scoring
582
655
  │ ├── mmr.ts Maximal Marginal Relevance re-ranking
583
656
  │ ├── temporal-decay.ts Exponential time-based scoring
584
- └── query-expansion.ts 7-language keyword expansion
657
+ ├── query-expansion.ts 7-language keyword expansion
658
+ │ ├── embedding-cache.ts SQLite embedding cache
659
+ │ ├── embedding-batch.ts Batch embedding with retry
660
+ │ ├── file-indexer.ts File-based memory indexing with fs.watch
661
+ │ ├── session-indexer.ts Session transcript indexing
662
+ │ └── ssrf.ts SSRF protection for HTTP calls
585
663
  ├── hooks/ 6 lifecycle hook points
664
+ ├── logging/ Structured JSON logging with file output
586
665
  ├── sessions/ File-based store with async locking
587
666
  └── config/ Configuration loading
588
667
  ```
589
668
 
590
- **59 source files** · **3 runtime deps** · Sandbox, crypto, and IPC use **zero external deps** — all Node.js built-ins.
669
+ **78 source files** · **3 runtime deps** · Sandbox, crypto, IPC, logging, and SSRF use **zero external deps** — all Node.js built-ins.
591
670
 
592
671
  ---
593
672
 
@@ -614,6 +693,30 @@ Every file is workspace-scoped. No shared state between tenants.
614
693
 
615
694
  ---
616
695
 
696
+ ## Built on OpenClaw
697
+
698
+ Bulkhead Runtime stands on the shoulders of [OpenClaw](https://github.com/nicepkg/openclaw). Several production-critical subsystems were ported directly from OpenClaw's codebase:
699
+
700
+
701
+ | Subsystem | Origin |
702
+ | ----------------------------------------- | ------------------------------------------------------------------------- |
703
+ | **Model fallback & error classification** | `src/agents/model-fallback.ts`, `failover-error.ts`, `failover-policy.ts` |
704
+ | **API key rotation** | `src/agents/api-key-rotation.ts`, `live-auth-keys.ts` |
705
+ | **Context window guards** | `src/agents/context-window-guard.ts` |
706
+ | **Retry with backoff** | `src/infra/retry.ts` |
707
+ | **SSRF protection** | `src/infra/net/ssrf.ts`, `fetch-guard.ts` |
708
+ | **Embedding cache & batch** | `extensions/memory-core/src/memory/manager-embedding-ops.ts` |
709
+ | **File indexer** | `extensions/memory-core/src/memory/manager-sync-ops.ts` |
710
+ | **Session transcript indexer** | `extensions/memory-core/src/memory/manager-sync-ops.ts` |
711
+ | **Subagent orchestration** | `src/agents/subagent-*.ts` |
712
+ | **Structured logging** | `src/logging/logger.ts`, `subsystem.ts`, `levels.ts` |
713
+ | **Transcript events** | `src/sessions/transcript-events.ts` |
714
+
715
+
716
+ OpenClaw solved these problems in production. We extracted, adapted, and integrated them into Bulkhead's multi-tenant architecture. Full credit where it's due.
717
+
718
+ ---
719
+
617
720
  ## Requirements
618
721
 
619
722
  - **Linux** — bare metal, VM, or container with `--privileged`
@@ -622,4 +725,4 @@ Every file is workspace-scoped. No shared state between tenants.
622
725
 
623
726
  ## License
624
727
 
625
- [MIT](LICENSE)
728
+ [MIT](LICENSE)