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.
- package/README.md +344 -262
- 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 +258 -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 +367 -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 +305 -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 +136 -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 +436 -105
- 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,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
+

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

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

|
|
73
53
|
|
|
74
|
-
|
|
54
|
+
### SaaS -- one agent per customer
|
|
75
55
|
|
|
76
|
-
|
|
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
|
-
|
|
90
|
-
<td width="50%">
|
|
91
|
-
|
|
92
|
-
**Per-team agents inside your company**
|
|
69
|
+
### Teams -- per-team agents, per-team secrets
|
|
93
70
|
|
|
94
|
-
|
|
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
|
-
|
|
111
|
-
</tr>
|
|
112
|
-
<tr>
|
|
113
|
-
<td width="50%">
|
|
84
|
+
### Consulting -- client isolation, clean offboarding
|
|
114
85
|
|
|
115
|
-
|
|
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.
|
|
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
|
|
93
|
+
// Client offboarded -- clean wipe
|
|
130
94
|
await platform.deleteWorkspace("client-acme");
|
|
131
95
|
```
|
|
132
96
|
|
|
133
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+

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

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

|
|
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");
|
|
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;
|
|
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
|
-
**
|
|
273
|
+
**Hybrid search engine under the hood:**
|
|
415
274
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
|
419
|
-
| **
|
|
420
|
-
| **
|
|
421
|
-
| **
|
|
422
|
-
| **
|
|
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
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
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
|
+

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