@aspectlylabs/mikka 0.0.230 → 0.0.232
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/agents/native-container.mjs +7 -23
- package/cli.js +717 -2127
- package/client.js +4 -7
- package/computer/proxy.mjs +1 -17
- package/harness/cli.mjs +1 -1
- package/harness/install.mjs +2 -2
- package/http-link.js +692 -994
- package/login.js +51 -56
- package/main.js +364 -1583
- package/mikka-release-manifest.json +34 -54
- package/mikka.js +717 -2127
- package/npm-shrinkwrap.json +2 -54
- package/package.json +2 -3
- package/paths.js +4 -7
- package/sandbox.js +0 -115
- package/sbom.cdx.json +7 -171
- package/agents/native-egress.mjs +0 -51
- package/agents/run-container-entry.mjs +0 -22
- package/harness/Dockerfile +0 -19
- package/run-sandbox-worker.mjs +0 -360
package/run-sandbox-worker.mjs
DELETED
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
// This trusted host process owns exactly one SandboxManager and one CLI.
|
|
2
|
-
// Only its parent's private IPC channel can configure it or answer a request.
|
|
3
|
-
import { spawn } from "node:child_process"
|
|
4
|
-
import { randomUUID } from "node:crypto"
|
|
5
|
-
import { isIP, Socket } from "node:net"
|
|
6
|
-
import { isAbsolute } from "node:path"
|
|
7
|
-
import { fileURLToPath } from "node:url"
|
|
8
|
-
|
|
9
|
-
const AUTH_LIMIT = 1_048_576
|
|
10
|
-
const MAX_PENDING_NETWORK = 128
|
|
11
|
-
const NETWORK_DECISION_MS = 5 * 60_000
|
|
12
|
-
|
|
13
|
-
export function assertRunSandboxDependencies(dependencies, platform = process.platform) {
|
|
14
|
-
if (dependencies.errors.length) {
|
|
15
|
-
throw new Error(`sandbox dependencies unavailable: ${dependencies.errors.join("; ")}`)
|
|
16
|
-
}
|
|
17
|
-
// Official 0.0.80 reports a missing apply-seccomp helper as only a warning.
|
|
18
|
-
// That would leave host Unix sockets reachable, so this launcher refuses to
|
|
19
|
-
// initialize or wrap. Old/unknown Linux dependency warnings also fail closed.
|
|
20
|
-
if (platform === "linux" && dependencies.warnings.length) {
|
|
21
|
-
throw new Error(`sandbox isolation dependencies unsafe: ${dependencies.warnings.join("; ")}`)
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Runs INSIDE the official sandbox. Linux bwrap deliberately closes extra
|
|
26
|
-
// inherited fds, so a framed stdin prefix carries the token across that
|
|
27
|
-
// boundary. It is converted to a token-only fd3 pipe before the real CLI starts.
|
|
28
|
-
async function authBridge() {
|
|
29
|
-
let token
|
|
30
|
-
let child
|
|
31
|
-
let headerBytes = 0
|
|
32
|
-
let tokenBytes = 0
|
|
33
|
-
const header = Buffer.alloc(4)
|
|
34
|
-
let complete = false
|
|
35
|
-
const fail = () => {
|
|
36
|
-
token?.fill(0)
|
|
37
|
-
process.stderr.write("run sandbox auth pipe was incomplete or invalid\n")
|
|
38
|
-
process.exit(1)
|
|
39
|
-
}
|
|
40
|
-
process.stdin.on("error", fail)
|
|
41
|
-
process.stdin.once("end", () => { if (!complete) fail() })
|
|
42
|
-
const read = (chunk) => {
|
|
43
|
-
let offset = 0
|
|
44
|
-
if (headerBytes < 4) {
|
|
45
|
-
const count = Math.min(4 - headerBytes, chunk.length)
|
|
46
|
-
chunk.copy(header, headerBytes, 0, count)
|
|
47
|
-
headerBytes += count
|
|
48
|
-
offset += count
|
|
49
|
-
if (headerBytes === 4) {
|
|
50
|
-
const length = header.readUInt32BE()
|
|
51
|
-
if (length < 1 || length > AUTH_LIMIT) return fail()
|
|
52
|
-
token = Buffer.alloc(length)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
if (token) {
|
|
56
|
-
const count = Math.min(token.length - tokenBytes, chunk.length - offset)
|
|
57
|
-
chunk.copy(token, tokenBytes, offset, offset + count)
|
|
58
|
-
chunk.fill(0, offset, offset + count)
|
|
59
|
-
tokenBytes += count
|
|
60
|
-
offset += count
|
|
61
|
-
}
|
|
62
|
-
if (!token || tokenBytes !== token.length) return
|
|
63
|
-
complete = true
|
|
64
|
-
process.stdin.pause()
|
|
65
|
-
process.stdin.off("data", read)
|
|
66
|
-
if (offset < chunk.length) process.stdin.unshift(chunk.subarray(offset))
|
|
67
|
-
child = spawn(process.argv[3], process.argv.slice(4), {
|
|
68
|
-
env: { ...process.env, CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR: "3" },
|
|
69
|
-
stdio: ["pipe", "pipe", "pipe", "pipe"],
|
|
70
|
-
})
|
|
71
|
-
child.once("error", () => fail())
|
|
72
|
-
child.stdin.on("error", () => undefined)
|
|
73
|
-
child.stdio[3].on("error", fail)
|
|
74
|
-
const payload = token
|
|
75
|
-
child.stdio[3].end(payload, () => { payload.fill(0); token = undefined })
|
|
76
|
-
// unshift above preserves protocol bytes delivered in the frame's last
|
|
77
|
-
// chunk. pipe handles subsequent data and backpressure without parsing it.
|
|
78
|
-
process.stdin.pipe(child.stdin)
|
|
79
|
-
child.stdout.pipe(process.stdout, { end: false })
|
|
80
|
-
child.stderr.pipe(process.stderr, { end: false })
|
|
81
|
-
child.once("close", async (code, signal) => {
|
|
82
|
-
token?.fill(0)
|
|
83
|
-
await Promise.all([process.stdout, process.stderr].map((stream) =>
|
|
84
|
-
new Promise((resolve) => stream.write("", resolve))))
|
|
85
|
-
process.exit(code ?? (signal === "SIGINT" ? 130 : 143))
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
process.stdin.on("data", read)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async function runWorker() {
|
|
92
|
-
const { NetworkConfigSchema, SandboxManager, SandboxRuntimeConfigSchema } = await import("@anthropic-ai/sandbox-runtime")
|
|
93
|
-
|
|
94
|
-
let configured = false
|
|
95
|
-
let active = false
|
|
96
|
-
let stopping = false
|
|
97
|
-
let cli
|
|
98
|
-
let initialize
|
|
99
|
-
let wrapping
|
|
100
|
-
let nextRequest = 0
|
|
101
|
-
let stderr = ""
|
|
102
|
-
let correlation
|
|
103
|
-
let initTimer
|
|
104
|
-
const pending = new Map()
|
|
105
|
-
const lifetime = new AbortController()
|
|
106
|
-
|
|
107
|
-
// Intentionally kept equivalent to the parent's exported matcher. Both sides
|
|
108
|
-
// independently apply the immutable maximum before a permission callback runs.
|
|
109
|
-
function canonicalHost(host) {
|
|
110
|
-
// eslint-disable-next-line no-control-regex -- Reject control bytes in a destination.
|
|
111
|
-
if (!host || /[\s\x00-\x1f\x7f%\\/@?#*]/u.test(host)) return
|
|
112
|
-
const bare = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host
|
|
113
|
-
if (isIP(bare) === 6) return new URL(`http://[${bare}]`).hostname.slice(1, -1)
|
|
114
|
-
if (bare.includes(":")) return
|
|
115
|
-
try { return new URL(`http://${bare}`).hostname.toLowerCase() } catch { return undefined }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function matchesDomain(request, domains) {
|
|
119
|
-
const host = canonicalHost(request.host)
|
|
120
|
-
if (!host || !Number.isInteger(request.port) || request.port < 1 || request.port > 65535) return false
|
|
121
|
-
return domains.some((entry) => {
|
|
122
|
-
if (entry === "*") return true
|
|
123
|
-
const parts = /^(\[[^\]]+\]|[^:]+?)(?::([1-9][0-9]{0,4}))?$/u.exec(entry)
|
|
124
|
-
if (!parts || (parts[2] !== undefined &&
|
|
125
|
-
(Number(parts[2]) > 65535 || Number(parts[2]) !== request.port))) return false
|
|
126
|
-
const wildcard = parts[1].startsWith("*.")
|
|
127
|
-
const pattern = canonicalHost(wildcard ? parts[1].slice(2) : parts[1])
|
|
128
|
-
if (!pattern) return false
|
|
129
|
-
return wildcard ? !isIP(host) && pattern.includes(".") && host.endsWith(`.${pattern}`) : host === pattern
|
|
130
|
-
})
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function quote(arg) {
|
|
134
|
-
return /^[A-Za-z0-9_@%+=:,./-]+$/u.test(arg) ? arg : `'${arg.replaceAll("'", `'"'"'`)}'`
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function send(message) {
|
|
138
|
-
if (!process.connected) return
|
|
139
|
-
process.send(message, (error) => { if (error) void stop(0) })
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function denyPending() {
|
|
143
|
-
active = false
|
|
144
|
-
lifetime.abort()
|
|
145
|
-
for (const request of pending.values()) {
|
|
146
|
-
clearTimeout(request.timer)
|
|
147
|
-
request.resolve(false)
|
|
148
|
-
}
|
|
149
|
-
pending.clear()
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function groupSignal(signal) {
|
|
153
|
-
try { process.kill(-process.pid, signal) } catch (error) {
|
|
154
|
-
if (error.code !== "ESRCH") throw error
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async function stop(graceMs = 0, outcome = { code: null, signal: "SIGTERM" }) {
|
|
159
|
-
denyPending()
|
|
160
|
-
if (stopping) return
|
|
161
|
-
stopping = true
|
|
162
|
-
clearTimeout(initTimer)
|
|
163
|
-
// The worker catches TERM; CLI descendants and runtime helpers share its
|
|
164
|
-
// group. A final parent group kill also covers a descendant that ignores it.
|
|
165
|
-
const watchdog = setTimeout(() => {
|
|
166
|
-
process.stderr.write("run sandbox teardown timed out; terminating process group\n")
|
|
167
|
-
groupSignal("SIGKILL")
|
|
168
|
-
}, Math.max(0, Math.min(graceMs, 30_000)) + 4000)
|
|
169
|
-
try {
|
|
170
|
-
groupSignal("SIGTERM")
|
|
171
|
-
if (cli && cli.exitCode === null && cli.signalCode === null) {
|
|
172
|
-
// Linux bwrap creates a new session for its PID namespace. Address the
|
|
173
|
-
// owned wrapper child directly as well as our group; --die-with-parent
|
|
174
|
-
// and --unshare-pid end its nested workload when that wrapper dies.
|
|
175
|
-
cli.kill(graceMs > 0 ? "SIGTERM" : "SIGKILL")
|
|
176
|
-
await new Promise((resolve) => {
|
|
177
|
-
const timer = setTimeout(() => { cli.kill("SIGKILL") }, Math.min(graceMs, 30_000))
|
|
178
|
-
cli.once("exit", () => { clearTimeout(timer); resolve() })
|
|
179
|
-
})
|
|
180
|
-
}
|
|
181
|
-
await initialize?.catch(() => undefined)
|
|
182
|
-
await wrapping?.catch(() => undefined)
|
|
183
|
-
// Official reset closes this launcher's established proxy connections and
|
|
184
|
-
// removes only its own ephemeral sockets/mountpoints/CA/masked artifacts.
|
|
185
|
-
// No worktree, run directory, logs or caller-owned data are removed here.
|
|
186
|
-
await SandboxManager.reset()
|
|
187
|
-
if (correlation) {
|
|
188
|
-
const annotated = SandboxManager.annotateStderrWithSandboxFailures(correlation, stderr)
|
|
189
|
-
if (annotated.length > stderr.length) process.stderr.write(annotated.slice(stderr.length))
|
|
190
|
-
}
|
|
191
|
-
if (process.connected) await new Promise((resolve) => {
|
|
192
|
-
process.send({ type: "finished", ...outcome }, () => resolve())
|
|
193
|
-
})
|
|
194
|
-
// process.exit does not wait for writes; flush the bridged output first.
|
|
195
|
-
await Promise.all([process.stdout, process.stderr].map((stream) =>
|
|
196
|
-
new Promise((resolve) => stream.write("", resolve))))
|
|
197
|
-
process.exit(outcome.code ?? (outcome.signal === "SIGINT" ? 130 : 143))
|
|
198
|
-
} catch (error) {
|
|
199
|
-
process.stderr.write(`run sandbox teardown failed: ${error.message}\n`)
|
|
200
|
-
// A failed reset cannot be followed by a successful-looking exit or a
|
|
201
|
-
// lingering live proxy. Group kill is fail-closed; parent also enforces it.
|
|
202
|
-
groupSignal("SIGKILL")
|
|
203
|
-
} finally {
|
|
204
|
-
clearTimeout(watchdog)
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
async function start(message) {
|
|
209
|
-
try {
|
|
210
|
-
if (typeof message.command !== "string" || !message.command || message.command.includes("\0") ||
|
|
211
|
-
!Array.isArray(message.args) || message.args.some((arg) => typeof arg !== "string" || arg.includes("\0")) ||
|
|
212
|
-
typeof message.cwd !== "string" || !isAbsolute(message.cwd) ||
|
|
213
|
-
!message.env || typeof message.env !== "object" || Array.isArray(message.env) ||
|
|
214
|
-
Object.entries(message.env).some(([key, value]) => key.includes("=") || key.includes("\0") ||
|
|
215
|
-
(value !== undefined && (typeof value !== "string" || value.includes("\0")))) ||
|
|
216
|
-
!Array.isArray(message.maxAllowDomains) || !Array.isArray(message.modelApiDomains) ||
|
|
217
|
-
typeof message.hasAuthToken !== "boolean") {
|
|
218
|
-
throw new Error("invalid run sandbox launch configuration")
|
|
219
|
-
}
|
|
220
|
-
const maximum = Object.freeze([...message.maxAllowDomains])
|
|
221
|
-
NetworkConfigSchema.parse({ allowedDomains: maximum.filter((entry) => entry !== "*"), deniedDomains: [] })
|
|
222
|
-
NetworkConfigSchema.parse({ allowedDomains: [...message.modelApiDomains], deniedDomains: [] })
|
|
223
|
-
const config = SandboxRuntimeConfigSchema.parse({
|
|
224
|
-
// Every connection, even a model API connection, must consult the live
|
|
225
|
-
// parent closure. No configured allow or command-id allow bypasses it.
|
|
226
|
-
network: {
|
|
227
|
-
allowedDomains: [], deniedDomains: [], strictAllowlist: false,
|
|
228
|
-
allowLocalBinding: false, allowAllUnixSockets: false, allowUnixSockets: [],
|
|
229
|
-
},
|
|
230
|
-
filesystem: message.policy,
|
|
231
|
-
enableWeakerNetworkIsolation: true,
|
|
232
|
-
enableWeakerNestedSandbox: false,
|
|
233
|
-
allowNestedUserNamespaces: false,
|
|
234
|
-
})
|
|
235
|
-
if (!SandboxManager.isSupportedPlatform()) throw new Error("sandbox platform unsupported")
|
|
236
|
-
const dependencies = SandboxManager.checkDependencies()
|
|
237
|
-
assertRunSandboxDependencies(dependencies)
|
|
238
|
-
if (stopping || !process.connected) return
|
|
239
|
-
active = true
|
|
240
|
-
initialize = SandboxManager.initialize(config, async (request) => {
|
|
241
|
-
if (!active || stopping || !process.connected || pending.size >= MAX_PENDING_NETWORK || !matchesDomain(request, maximum)) return false
|
|
242
|
-
const id = ++nextRequest
|
|
243
|
-
return new Promise((resolve) => {
|
|
244
|
-
const timer = setTimeout(() => {
|
|
245
|
-
if (pending.delete(id)) resolve(false)
|
|
246
|
-
}, NETWORK_DECISION_MS)
|
|
247
|
-
pending.set(id, { resolve, timer })
|
|
248
|
-
send({ type: "network", id, host: request.host, port: request.port })
|
|
249
|
-
})
|
|
250
|
-
})
|
|
251
|
-
await initialize
|
|
252
|
-
if (!active || stopping || !process.connected) return
|
|
253
|
-
const argv = message.hasAuthToken
|
|
254
|
-
? [process.execPath, fileURLToPath(import.meta.url), "--auth-bridge", message.command, ...message.args]
|
|
255
|
-
: [message.command, ...message.args]
|
|
256
|
-
const command = argv.map(quote).join(" ")
|
|
257
|
-
// Correlation is only for diagnostics, never network/run authority.
|
|
258
|
-
correlation = randomUUID()
|
|
259
|
-
wrapping = SandboxManager.wrapWithSandbox(command, "/bin/sh", undefined, lifetime.signal, { commandId: correlation })
|
|
260
|
-
const wrapped = await wrapping
|
|
261
|
-
if (!active || stopping || !process.connected) return
|
|
262
|
-
const env = { ...message.env }
|
|
263
|
-
// Node's private IPC setup must not be inherited by the untrusted CLI.
|
|
264
|
-
delete env.NODE_CHANNEL_FD
|
|
265
|
-
delete env.NODE_CHANNEL_SERIALIZATION_MODE
|
|
266
|
-
if (message.hasAuthToken === true) {
|
|
267
|
-
env.CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR = "3"
|
|
268
|
-
delete env.CLAUDE_CODE_OAUTH_TOKEN
|
|
269
|
-
}
|
|
270
|
-
cli = spawn("/bin/sh", ["-c", `exec ${wrapped}`], {
|
|
271
|
-
cwd: message.cwd,
|
|
272
|
-
env,
|
|
273
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
274
|
-
// Stay in the launcher's group, so even an abrupt worker failure cannot
|
|
275
|
-
// orphan an independently detached CLI before its pid can be reported.
|
|
276
|
-
detached: false,
|
|
277
|
-
})
|
|
278
|
-
cli.stdin.on("error", () => undefined)
|
|
279
|
-
if (message.hasAuthToken) {
|
|
280
|
-
// The token arrives on a separate host pipe. Frame it before forwarding
|
|
281
|
-
// any protocol input, so neither packet boundaries nor early SDK writes
|
|
282
|
-
// can mix credentials with the first CLI message.
|
|
283
|
-
const source = new Socket({ fd: 3, readable: true, writable: false })
|
|
284
|
-
const chunks = []
|
|
285
|
-
let length = 0
|
|
286
|
-
source.on("data", (data) => {
|
|
287
|
-
length += data.length
|
|
288
|
-
if (length > AUTH_LIMIT) {
|
|
289
|
-
data.fill(0)
|
|
290
|
-
source.destroy(new Error("auth pipe payload too large"))
|
|
291
|
-
} else chunks.push(data)
|
|
292
|
-
})
|
|
293
|
-
source.once("error", () => {
|
|
294
|
-
for (const chunk of chunks) chunk.fill(0)
|
|
295
|
-
process.stderr.write("run sandbox auth pipe failed\n")
|
|
296
|
-
void stop(0, { code: 1, signal: null })
|
|
297
|
-
})
|
|
298
|
-
source.once("end", () => {
|
|
299
|
-
if (!length || stopping) {
|
|
300
|
-
for (const chunk of chunks) chunk.fill(0)
|
|
301
|
-
void stop(0, { code: 1, signal: null })
|
|
302
|
-
return
|
|
303
|
-
}
|
|
304
|
-
const payload = Buffer.concat(chunks)
|
|
305
|
-
for (const chunk of chunks) chunk.fill(0)
|
|
306
|
-
const header = Buffer.alloc(4)
|
|
307
|
-
header.writeUInt32BE(payload.length)
|
|
308
|
-
cli.stdin.write(header)
|
|
309
|
-
cli.stdin.write(payload, () => payload.fill(0))
|
|
310
|
-
process.stdin.pipe(cli.stdin)
|
|
311
|
-
})
|
|
312
|
-
} else process.stdin.pipe(cli.stdin)
|
|
313
|
-
cli.stdout.pipe(process.stdout, { end: false })
|
|
314
|
-
cli.stderr.on("data", (data) => { stderr = (stderr + data.toString()).slice(-16_000) })
|
|
315
|
-
cli.stderr.pipe(process.stderr, { end: false })
|
|
316
|
-
cli.once("error", (error) => {
|
|
317
|
-
process.stderr.write(`sandboxed CLI launch failed: ${error.message}\n`)
|
|
318
|
-
void stop(0, { code: 1, signal: null })
|
|
319
|
-
})
|
|
320
|
-
cli.once("close", (code, signal) => { void stop(0, { code, signal }) })
|
|
321
|
-
} catch (error) {
|
|
322
|
-
process.stderr.write(`run sandbox launch failed: ${error.message}\n`)
|
|
323
|
-
await stop(0, { code: 1, signal: null })
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
process.on("message", (message) => {
|
|
328
|
-
if (!message || typeof message !== "object") return
|
|
329
|
-
if (message.type === "stop") {
|
|
330
|
-
void stop(Number.isFinite(message.graceMs) ? Math.max(0, message.graceMs) : 0)
|
|
331
|
-
} else if (message.type === "init" && !configured && !stopping) {
|
|
332
|
-
configured = true
|
|
333
|
-
clearTimeout(initTimer)
|
|
334
|
-
void start(message)
|
|
335
|
-
} else if (message.type === "decision" && Number.isSafeInteger(message.id)) {
|
|
336
|
-
const request = pending.get(message.id)
|
|
337
|
-
if (!request) return
|
|
338
|
-
pending.delete(message.id)
|
|
339
|
-
clearTimeout(request.timer)
|
|
340
|
-
request.resolve(message.allow === true && active && !stopping && process.connected)
|
|
341
|
-
}
|
|
342
|
-
})
|
|
343
|
-
process.on("disconnect", () => { void stop(0) })
|
|
344
|
-
process.on("SIGTERM", () => { void stop(0) })
|
|
345
|
-
process.on("SIGINT", () => { void stop(0, { code: null, signal: "SIGINT" }) })
|
|
346
|
-
process.stdout.on("error", () => { void stop(0) })
|
|
347
|
-
process.stderr.on("error", () => { void stop(0) })
|
|
348
|
-
initTimer = setTimeout(() => {
|
|
349
|
-
process.stderr.write("run sandbox launch configuration was not received\n")
|
|
350
|
-
void stop(0, { code: 1, signal: null })
|
|
351
|
-
}, 10_000)
|
|
352
|
-
if (!process.connected) void stop(0, { code: 1, signal: null })
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Importing the pure dependency check for source-only tests does not start a
|
|
356
|
-
// launcher, touch resources or initialize the official manager.
|
|
357
|
-
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
358
|
-
if (process.argv[2] === "--auth-bridge") await authBridge()
|
|
359
|
-
else await runWorker()
|
|
360
|
-
}
|