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.
- package/README.md +337 -234
- package/dist/cli.js +5 -1
- package/dist/cli.js.map +1 -1
- package/dist/config/index.d.ts +28 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +9 -6
- package/dist/config/index.js.map +1 -1
- package/dist/credentials/store.d.ts.map +1 -1
- package/dist/credentials/store.js +39 -15
- package/dist/credentials/store.js.map +1 -1
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -1
- package/dist/index.js.map +1 -1
- package/dist/infra/warning-filter.js +1 -1
- package/dist/infra/warning-filter.js.map +1 -1
- package/dist/logging/subsystem.d.ts +15 -1
- package/dist/logging/subsystem.d.ts.map +1 -1
- package/dist/logging/subsystem.js +310 -45
- package/dist/logging/subsystem.js.map +1 -1
- package/dist/memory/embedding-batch.d.ts +38 -0
- package/dist/memory/embedding-batch.d.ts.map +1 -0
- package/dist/memory/embedding-batch.js +253 -0
- package/dist/memory/embedding-batch.js.map +1 -0
- package/dist/memory/embedding-cache.d.ts +16 -0
- package/dist/memory/embedding-cache.d.ts.map +1 -0
- package/dist/memory/embedding-cache.js +113 -0
- package/dist/memory/embedding-cache.js.map +1 -0
- package/dist/memory/embeddings-debug.js +1 -1
- package/dist/memory/embeddings.d.ts +1 -0
- package/dist/memory/embeddings.d.ts.map +1 -1
- package/dist/memory/embeddings.js +115 -92
- package/dist/memory/embeddings.js.map +1 -1
- package/dist/memory/file-indexer.d.ts +26 -0
- package/dist/memory/file-indexer.d.ts.map +1 -0
- package/dist/memory/file-indexer.js +245 -0
- package/dist/memory/file-indexer.js.map +1 -0
- package/dist/memory/hybrid.d.ts.map +1 -1
- package/dist/memory/hybrid.js +6 -2
- package/dist/memory/hybrid.js.map +1 -1
- package/dist/memory/index.d.ts +5 -0
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +5 -2
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/session-indexer.d.ts +41 -0
- package/dist/memory/session-indexer.d.ts.map +1 -0
- package/dist/memory/session-indexer.js +341 -0
- package/dist/memory/session-indexer.js.map +1 -0
- package/dist/memory/simple-manager.d.ts +6 -0
- package/dist/memory/simple-manager.d.ts.map +1 -1
- package/dist/memory/simple-manager.js +35 -12
- package/dist/memory/simple-manager.js.map +1 -1
- package/dist/memory/ssrf.d.ts +18 -0
- package/dist/memory/ssrf.d.ts.map +1 -0
- package/dist/memory/ssrf.js +316 -0
- package/dist/memory/ssrf.js.map +1 -0
- package/dist/package.json +8 -5
- package/dist/platform/platform.d.ts.map +1 -1
- package/dist/platform/platform.js +30 -7
- package/dist/platform/platform.js.map +1 -1
- package/dist/platform/types.d.ts +2 -0
- package/dist/platform/types.d.ts.map +1 -1
- package/dist/runtime/agent.d.ts +8 -0
- package/dist/runtime/agent.d.ts.map +1 -1
- package/dist/runtime/agent.js +194 -46
- package/dist/runtime/agent.js.map +1 -1
- package/dist/runtime/api-key-rotation.d.ts +26 -0
- package/dist/runtime/api-key-rotation.d.ts.map +1 -0
- package/dist/runtime/api-key-rotation.js +174 -0
- package/dist/runtime/api-key-rotation.js.map +1 -0
- package/dist/runtime/context-guard.d.ts +32 -0
- package/dist/runtime/context-guard.d.ts.map +1 -0
- package/dist/runtime/context-guard.js +61 -0
- package/dist/runtime/context-guard.js.map +1 -0
- package/dist/runtime/failover-error.d.ts +62 -0
- package/dist/runtime/failover-error.d.ts.map +1 -0
- package/dist/runtime/failover-error.js +733 -0
- package/dist/runtime/failover-error.js.map +1 -0
- package/dist/runtime/failover-policy.d.ts +5 -0
- package/dist/runtime/failover-policy.d.ts.map +1 -0
- package/dist/runtime/failover-policy.js +18 -0
- package/dist/runtime/failover-policy.js.map +1 -0
- package/dist/runtime/index.d.ts +11 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +11 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/memory-flush.d.ts +24 -0
- package/dist/runtime/memory-flush.d.ts.map +1 -0
- package/dist/runtime/memory-flush.js +64 -0
- package/dist/runtime/memory-flush.js.map +1 -0
- package/dist/runtime/memory-tools.d.ts +14 -0
- package/dist/runtime/memory-tools.d.ts.map +1 -0
- package/dist/runtime/memory-tools.js +58 -0
- package/dist/runtime/memory-tools.js.map +1 -0
- package/dist/runtime/model-fallback.d.ts +56 -0
- package/dist/runtime/model-fallback.d.ts.map +1 -0
- package/dist/runtime/model-fallback.js +301 -0
- package/dist/runtime/model-fallback.js.map +1 -0
- package/dist/runtime/model-fallback.types.d.ts +14 -0
- package/dist/runtime/model-fallback.types.d.ts.map +1 -0
- package/dist/runtime/model-fallback.types.js +3 -0
- package/dist/runtime/model-fallback.types.js.map +1 -0
- package/dist/runtime/retry.d.ts +24 -0
- package/dist/runtime/retry.d.ts.map +1 -0
- package/dist/runtime/retry.js +102 -0
- package/dist/runtime/retry.js.map +1 -0
- package/dist/runtime/session-pruning.d.ts +22 -0
- package/dist/runtime/session-pruning.d.ts.map +1 -0
- package/dist/runtime/session-pruning.js +118 -0
- package/dist/runtime/session-pruning.js.map +1 -0
- package/dist/runtime/stream-adapters.d.ts +11 -0
- package/dist/runtime/stream-adapters.d.ts.map +1 -0
- package/dist/runtime/stream-adapters.js +46 -0
- package/dist/runtime/stream-adapters.js.map +1 -0
- package/dist/runtime/subagent.d.ts +83 -0
- package/dist/runtime/subagent.d.ts.map +1 -0
- package/dist/runtime/subagent.js +190 -0
- package/dist/runtime/subagent.js.map +1 -0
- package/dist/runtime/tool-result-truncation.d.ts +25 -0
- package/dist/runtime/tool-result-truncation.d.ts.map +1 -0
- package/dist/runtime/tool-result-truncation.js +115 -0
- package/dist/runtime/tool-result-truncation.js.map +1 -0
- package/dist/sandbox/cgroup.d.ts +4 -1
- package/dist/sandbox/cgroup.d.ts.map +1 -1
- package/dist/sandbox/cgroup.js +28 -15
- package/dist/sandbox/cgroup.js.map +1 -1
- package/dist/sandbox/index.d.ts +2 -1
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/index.js +2 -1
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sandbox/ipc.d.ts +4 -1
- package/dist/sandbox/ipc.d.ts.map +1 -1
- package/dist/sandbox/ipc.js +33 -17
- package/dist/sandbox/ipc.js.map +1 -1
- package/dist/sandbox/manager.d.ts +1 -2
- package/dist/sandbox/manager.d.ts.map +1 -1
- package/dist/sandbox/manager.js +132 -130
- package/dist/sandbox/manager.js.map +1 -1
- package/dist/sandbox/namespace.d.ts +1 -1
- package/dist/sandbox/namespace.d.ts.map +1 -1
- package/dist/sandbox/namespace.js +36 -37
- package/dist/sandbox/namespace.js.map +1 -1
- package/dist/sandbox/rootfs.d.ts +6 -1
- package/dist/sandbox/rootfs.d.ts.map +1 -1
- package/dist/sandbox/rootfs.js +114 -30
- package/dist/sandbox/rootfs.js.map +1 -1
- package/dist/sandbox/seccomp-apply.d.ts +9 -0
- package/dist/sandbox/seccomp-apply.d.ts.map +1 -0
- package/dist/sandbox/seccomp-apply.js +227 -0
- package/dist/sandbox/seccomp-apply.js.map +1 -0
- package/dist/sandbox/seccomp.js +3 -3
- package/dist/sandbox/seccomp.js.map +1 -1
- package/dist/sandbox/types.d.ts +1 -3
- package/dist/sandbox/types.d.ts.map +1 -1
- package/dist/sandbox/types.js.map +1 -1
- package/dist/sandbox/worker.d.ts +3 -0
- package/dist/sandbox/worker.d.ts.map +1 -1
- package/dist/sandbox/worker.js +84 -17
- package/dist/sandbox/worker.js.map +1 -1
- package/dist/sessions/index.d.ts +1 -0
- package/dist/sessions/index.d.ts.map +1 -1
- package/dist/sessions/index.js +1 -0
- package/dist/sessions/index.js.map +1 -1
- package/dist/sessions/store.d.ts +2 -2
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +49 -27
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/transcript-events.d.ts +11 -0
- package/dist/sessions/transcript-events.d.ts.map +1 -0
- package/dist/sessions/transcript-events.js +40 -0
- package/dist/sessions/transcript-events.js.map +1 -0
- package/dist/shared/agent-session.d.ts +10 -0
- package/dist/shared/agent-session.d.ts.map +1 -0
- package/dist/shared/agent-session.js +33 -0
- package/dist/shared/agent-session.js.map +1 -0
- package/dist/shared/constants.d.ts +6 -0
- package/dist/shared/constants.d.ts.map +1 -0
- package/dist/shared/constants.js +11 -0
- package/dist/shared/constants.js.map +1 -0
- package/dist/shared/fs.d.ts +7 -0
- package/dist/shared/fs.d.ts.map +1 -0
- package/dist/shared/fs.js +14 -0
- package/dist/shared/fs.js.map +1 -0
- package/dist/shared/index.d.ts +4 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/index.js +4 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/skills/enablement.d.ts.map +1 -1
- package/dist/skills/enablement.js +2 -2
- package/dist/skills/enablement.js.map +1 -1
- package/dist/workspace/runner.d.ts.map +1 -1
- package/dist/workspace/runner.js +353 -106
- package/dist/workspace/runner.js.map +1 -1
- package/dist/workspace/types.d.ts +1 -0
- package/dist/workspace/types.d.ts.map +1 -1
- package/dist/workspace/workspace.d.ts.map +1 -1
- package/dist/workspace/workspace.js +12 -3
- package/dist/workspace/workspace.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,27 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
+

|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/bulkhead-runtime)      
|
|
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
|
+

|
|
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.
|
|
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 #
|
|
47
|
+
> pnpm test # 279 tests, all green
|
|
64
48
|
> ```
|
|
65
49
|
|
|
66
50
|
---
|
|
67
51
|
|
|
68
52
|
## Use Cases
|
|
69
53
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
+

|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
|
211
|
-
| **
|
|
212
|
-
| **
|
|
213
|
-
| **
|
|
214
|
-
| **
|
|
215
|
-
| **
|
|
216
|
-
| **
|
|
217
|
-
| **
|
|
218
|
-
| **
|
|
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.
|
|
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
|
-
|
|
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
|
+

|
|
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
|
-
|
|
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
|
+

|
|
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");
|
|
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;
|
|
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
|
-
**
|
|
294
|
+
**Hybrid search engine under the hood:**
|
|
295
|
+
|
|
415
296
|
|
|
416
|
-
| Stage
|
|
417
|
-
|
|
418
|
-
| **Vector search**
|
|
419
|
-
| **Keyword search**
|
|
420
|
-
| **Fusion**
|
|
421
|
-
| **Temporal decay**
|
|
422
|
-
| **Diversity**
|
|
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
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
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
|
+

|
|
514
570
|
|
|
515
571
|
### Defense in Depth
|
|
516
572
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
|
520
|
-
| **
|
|
521
|
-
| **
|
|
522
|
-
| **
|
|
523
|
-
| **
|
|
524
|
-
| **IPC
|
|
525
|
-
| **
|
|
526
|
-
| **
|
|
527
|
-
| **
|
|
528
|
-
| **
|
|
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
|
-
|
|
539
|
-
|
|
596
|
+
|
|
597
|
+
| Provider | Example Model |
|
|
598
|
+
| ------------- | -------------------------- |
|
|
540
599
|
| **Anthropic** | `claude-sonnet-4-20250514` |
|
|
541
|
-
| **Google**
|
|
542
|
-
| **OpenAI**
|
|
543
|
-
| **Groq**
|
|
544
|
-
| **Cerebras**
|
|
545
|
-
| **Mistral**
|
|
546
|
-
| **xAI**
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
|
555
|
-
| **
|
|
556
|
-
| **
|
|
557
|
-
| **
|
|
558
|
-
| **
|
|
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
|
|
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
|
-
│
|
|
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
|
-
**
|
|
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)
|