@a-dray/aglib 0.1.0
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/LICENSE +21 -0
- package/README.md +172 -0
- package/dist/agent.d.ts +158 -0
- package/dist/agent.js +2 -0
- package/dist/agent.js.map +1 -0
- package/dist/content.d.ts +45 -0
- package/dist/content.js +11 -0
- package/dist/content.js.map +1 -0
- package/dist/harness/adapters/acp/index.d.ts +92 -0
- package/dist/harness/adapters/acp/index.js +479 -0
- package/dist/harness/adapters/acp/index.js.map +1 -0
- package/dist/harness/adapters/acp/rpc.d.ts +23 -0
- package/dist/harness/adapters/acp/rpc.js +103 -0
- package/dist/harness/adapters/acp/rpc.js.map +1 -0
- package/dist/harness/adapters/native/compaction.d.ts +16 -0
- package/dist/harness/adapters/native/compaction.js +55 -0
- package/dist/harness/adapters/native/compaction.js.map +1 -0
- package/dist/harness/adapters/native/index.d.ts +2 -0
- package/dist/harness/adapters/native/index.js +2 -0
- package/dist/harness/adapters/native/index.js.map +1 -0
- package/dist/harness/adapters/native/loop.d.ts +25 -0
- package/dist/harness/adapters/native/loop.js +153 -0
- package/dist/harness/adapters/native/loop.js.map +1 -0
- package/dist/harness/harness.d.ts +129 -0
- package/dist/harness/harness.js +2 -0
- package/dist/harness/harness.js.map +1 -0
- package/dist/harness/index.d.ts +3 -0
- package/dist/harness/index.js +2 -0
- package/dist/harness/index.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +4 -0
- package/dist/json.js +2 -0
- package/dist/json.js.map +1 -0
- package/dist/model/adapters/anthropic/index.d.ts +34 -0
- package/dist/model/adapters/anthropic/index.js +271 -0
- package/dist/model/adapters/anthropic/index.js.map +1 -0
- package/dist/model/adapters/fake/index.d.ts +27 -0
- package/dist/model/adapters/fake/index.js +37 -0
- package/dist/model/adapters/fake/index.js.map +1 -0
- package/dist/model/adapters/openai-compatible/index.d.ts +29 -0
- package/dist/model/adapters/openai-compatible/index.js +226 -0
- package/dist/model/adapters/openai-compatible/index.js.map +1 -0
- package/dist/model/conformance.d.ts +210 -0
- package/dist/model/conformance.js +427 -0
- package/dist/model/conformance.js.map +1 -0
- package/dist/model/index.d.ts +2 -0
- package/dist/model/index.js +2 -0
- package/dist/model/index.js.map +1 -0
- package/dist/model/model.d.ts +85 -0
- package/dist/model/model.js +8 -0
- package/dist/model/model.js.map +1 -0
- package/dist/result.d.ts +23 -0
- package/dist/result.js +3 -0
- package/dist/result.js.map +1 -0
- package/dist/run.d.ts +10 -0
- package/dist/run.js +477 -0
- package/dist/run.js.map +1 -0
- package/dist/sandbox/adapters/docker.d.ts +22 -0
- package/dist/sandbox/adapters/docker.js +443 -0
- package/dist/sandbox/adapters/docker.js.map +1 -0
- package/dist/sandbox/adapters/local.d.ts +11 -0
- package/dist/sandbox/adapters/local.js +284 -0
- package/dist/sandbox/adapters/local.js.map +1 -0
- package/dist/sandbox/conformance.d.ts +73 -0
- package/dist/sandbox/conformance.js +309 -0
- package/dist/sandbox/conformance.js.map +1 -0
- package/dist/sandbox/index.d.ts +1 -0
- package/dist/sandbox/index.js +2 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/sandbox.d.ts +172 -0
- package/dist/sandbox/sandbox.js +2 -0
- package/dist/sandbox/sandbox.js.map +1 -0
- package/dist/session/entry.d.ts +196 -0
- package/dist/session/entry.js +2 -0
- package/dist/session/entry.js.map +1 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +2 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/log.d.ts +20 -0
- package/dist/session/log.js +24 -0
- package/dist/session/log.js.map +1 -0
- package/dist/session/messages.d.ts +42 -0
- package/dist/session/messages.js +99 -0
- package/dist/session/messages.js.map +1 -0
- package/dist/store/adapters/sqlite.d.ts +20 -0
- package/dist/store/adapters/sqlite.js +365 -0
- package/dist/store/adapters/sqlite.js.map +1 -0
- package/dist/store/conformance.d.ts +66 -0
- package/dist/store/conformance.js +580 -0
- package/dist/store/conformance.js.map +1 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.js +2 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/store.d.ts +244 -0
- package/dist/store/store.js +2 -0
- package/dist/store/store.js.map +1 -0
- package/dist/tools/execute.d.ts +24 -0
- package/dist/tools/execute.js +105 -0
- package/dist/tools/execute.js.map +1 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/tool.d.ts +106 -0
- package/dist/tools/tool.js +34 -0
- package/dist/tools/tool.js.map +1 -0
- package/package.json +119 -0
package/dist/run.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Agent, AgentRun, Arrival, RunAgentOptions, RunResult } from \"./agent.js\";\nimport type { Failure } from \"./result.js\";\nimport type { Update } from \"./harness/harness.js\";\nimport type { Delivery, Store, StoreConflict, StoreError } from \"./store/store.js\";\nimport type { Content } from \"./content.js\";\nimport type { Entry, Stored, Usage } from \"./session/entry.js\";\nimport { err } from \"./result.js\";\nimport { createLog } from \"./session/log.js\";\nimport { toMessages } from \"./session/messages.js\";\nimport { createExecutor } from \"./tools/execute.js\";\n\n/**\n * Start an agent on a session.\n *\n * Without a store the log lives in memory and the session is ephemeral; with\n * one, every commit is durable before the next turn is built. The agent\n * definition is identical either way — durability is a composition choice, not\n * a different program.\n */\nexport function runAgent(options: RunAgentOptions): AgentRun {\n // A claim names its own session; a caller sending into a new one names none.\n const claim = options.claim;\n const sessionId = claim?.sessionId ?? options.sessionId ?? crypto.randomUUID();\n const controller = new AbortController();\n // Composed rather than forwarded. `addEventListener(\"abort\", …)` never fires\n // on a signal that has already aborted, so a caller who gave up before\n // calling got a full run anyway; and a listener added per run to a signal\n // that outlives it is a leak the run cannot clean up.\n const signal = options.signal\n ? AbortSignal.any([options.signal, controller.signal])\n : controller.signal;\n\n const updates = createChannel<Update>();\n const result = execute();\n\n return {\n result,\n cancel: () => controller.abort(),\n [Symbol.asyncIterator]: () => updates.iterator(),\n };\n\n async function execute(): Promise<RunResult> {\n // What this activation consumed. Summed from the `assistant` entries it\n // commits rather than reported by the harness, so one answer serves a loop\n // we own and a loop we do not — and so a run that failed still says what it\n // burned on the way there.\n const usage: Usage = {};\n\n try {\n const { agent, store } = options;\n const restored = store ? await open(store, sessionId, agent, options.key) : undefined;\n if (restored && !restored.ok) {\n return {\n status: \"failed\", error: restored.error,\n seq: positionOf(restored.error, 0), usage,\n };\n }\n // The store's own position, not the last entry's. They agree only while\n // nothing has ever been folded away, and the store is the one that knows.\n const log = createLog(restored?.value.entries ?? [], restored?.value.seq ?? 0);\n const stopping = (error: Failure): RunResult => ({ status: \"failed\", error, seq: log.seq, usage });\n\n // A claim was read at a position. If the session has moved since, this\n // worker lost, and the deliveries it was carrying are still where it\n // found them.\n if (claim && claim.seq !== log.seq) {\n return stopping({ code: \"conflict\", message: `Session is at ${log.seq}, not ${claim.seq}`, retryable: true });\n }\n\n // A ceiling on money needs something that can count it. Refused here\n // rather than silently ignored, because a limit that is declared and not\n // enforced is the most expensive kind of nothing.\n\n // The activation still open on this log, if one is. It decides three\n // things below: whether there is anything to resume, which run a\n // resumption continues, and which run a fresh activation supersedes.\n const interrupted = openRunId(log.entries);\n\n // What this run is adding, then what was waiting. A claim's deliveries\n // are named so the log records who each came from; anything the worker\n // supplies itself leads, because the only thing that ever does is\n // orientation for a harness with nowhere else to put it.\n const arrivals: readonly Arrival[] = [\n ...arrivalsOf(options.input),\n ...(claim?.pending ?? []).map((delivery): Arrival => ({\n input: delivery.input,\n ...(delivery.from ? { from: delivery.from } : {}),\n })),\n ];\n\n // A resumption continues the run it was handed, under that run's id. A\n // fresh id would leave the log holding a `run.started` nothing closes and\n // a `run.finished` nothing opened — a pair that only the store's\n // type-based rule saves, and that anything grouping entries by run reads\n // as broken. A resumption without an interrupted run is refused below.\n const runId = arrivals.length || !interrupted ? crypto.randomUUID() : interrupted;\n\n // Deliveries ride the next commit, which is the one that records the work\n // that produced them. They used to be held back to the run's final entry\n // so that a failed run handed nothing to anyone — a stronger promise than\n // any caller needs, and it was being paid for with a lie: a `send` tool\n // returned \"Delivered\" and interrupted the recipient many turns before\n // the delivery existed, and never at all if a later turn failed.\n const deliveries: Delivery[] = [];\n\n // The ceiling has one owner, and it is here rather than in a harness.\n // Counting committed entries is the only way to bound a harness that owns\n // its own loop — which is most of them — so a limit declared on the agent\n // holds for every one instead of only for ours.\n const limits = agent.limits;\n let stopped: Failure | undefined;\n let turns = 0;\n let toolCalls = 0;\n\n function exceeded(): Failure | undefined {\n if (!limits) return undefined;\n if (limits.maxTurns !== undefined && turns >= limits.maxTurns) {\n return { code: \"turn-limit\", message: `Stopped after ${turns} turns.`, retryable: false };\n }\n if (limits.maxToolCalls !== undefined && toolCalls >= limits.maxToolCalls) {\n return { code: \"tool-call-limit\", message: `Stopped after ${toolCalls} tool calls.`, retryable: false };\n }\n // Checked where work is recorded rather than on a timer: a deadline\n // stops the next piece of work, it does not interrupt one mid-flight.\n if (limits.deadline !== undefined && Date.now() >= Date.parse(limits.deadline)) {\n return { code: \"deadline\", message: `Stopped at the deadline of ${limits.deadline}.`, retryable: false };\n }\n return undefined;\n }\n\n // How many deliveries the next write takes off this session's own queue:\n // what the claim was carrying, and afterwards whatever `drain` folded in.\n // Cleared by the write that lands, because the input is gone once the\n // entries carrying it are durable, and not before.\n let take = claim?.pending.length ?? 0;\n\n async function commit(entries: readonly Entry[], enqueue?: readonly Delivery[]): Promise<void> {\n const outgoing = [...deliveries, ...(enqueue ?? [])];\n deliveries.length = 0;\n if (!entries.length && !outgoing.length) return;\n // Counted before the write, not after: the tokens were consumed whether\n // or not this entry wins the compare-and-swap, and a run that reports\n // nothing after paying for a generation is the opposite of what\n // `RunResult.usage` promises.\n for (const entry of entries) {\n if (entry.type === \"assistant\" && entry.usage) accumulate(usage, entry.usage);\n }\n\n if (store) {\n const written = await store.append({\n sessionId, expectedSeq: log.seq, entries,\n ...(outgoing.length ? { enqueue: outgoing } : {}),\n ...(take ? { takePending: take } : {}),\n });\n if (!written.ok) throw new CommitFailed(written.error, positionOf(written.error, log.seq));\n take = 0;\n }\n for (const stored of log.append(entries)) updates.push({ type: \"entry\", entry: stored });\n\n if (!stopped) {\n for (const entry of entries) {\n if (entry.type === \"assistant\") turns += 1;\n // `tool.finished`, not `tool.started`: the start is committed before\n // the batch executes, so counting it there cancelled the very batch\n // that reached the ceiling — `maxToolCalls: 1` ran no tools at all,\n // and said it had stopped after one.\n if (entry.type === \"tool.finished\") toolCalls += 1;\n }\n stopped = exceeded();\n if (stopped) controller.abort();\n }\n }\n\n /**\n * What the agent wants delivered as this run ends.\n *\n * A callback that throws must not cost the log its terminal entry: the\n * run would stay open, and an open run nothing closes is claimed by\n * `interrupted` every window for ever — the trap the no-recovery path above\n * exists to close. So the throw is held, the run is ended without the\n * deliveries, and it is re-thrown once the log is safe.\n */\n let reportFailure: unknown;\n async function reporting(run: {\n runId: string; outcome: \"completed\" | \"failed\" | \"cancelled\"; output: Content;\n }): Promise<readonly Delivery[]> {\n if (!agent.finished) return [];\n try {\n return await agent.finished({ sessionId, ...run });\n } catch (error) {\n reportFailure = error;\n return [];\n }\n }\n\n // Input that arrived while this activation was running, taken from the\n // front of the queue and committed as part of it. `takePending` removes\n // exactly what was committed, in the same write, so losing the position\n // loses the turn and never the messages.\n async function drain(): Promise<number> {\n if (!store) return 0;\n // Nothing is folded into an activation that is over. Draining here\n // committed the waiting deliveries and took them off the queue, and the\n // loop then ended — leaving a peer's message answered by nobody and\n // invisible to `next` (nothing pending) and to `interrupted` (no open run),\n // which is the trap this whole path exists to close.\n if (signal.aborted) return 0;\n const current = await store.read({ sessionId, afterSeq: log.seq });\n if (!current.ok || !current.value.pending.length) return 0;\n const waiting = current.value.pending;\n take = waiting.length;\n await commit(waiting.map((delivery): Entry => ({\n type: \"run.started\", runId, input: delivery.input,\n ...(delivery.from ? { from: delivery.from } : {}),\n })));\n return waiting.length;\n }\n\n const executor = agent.tools?.length\n ? createExecutor({\n tools: agent.tools,\n ...(agent.decide ? { decide: agent.decide } : {}),\n sessionId, runId,\n enqueue: (delivery) => deliveries.push(delivery),\n report: (callId, data) => updates.push({ type: \"tool.progress\", callId, data }),\n })\n : undefined;\n\n if (!arrivals.length) {\n // An open run is what a resumption continues. Without one there is\n // nothing half-done here: a closed log would otherwise take a second\n // `run.finished` with no beginning to match it.\n if (!interrupted) {\n return stopping({\n code: \"nothing-to-resume\",\n message: \"An activation with no input continues an interrupted run, and this session has none open.\",\n retryable: false,\n });\n }\n if (agent.harness.recovery !== \"history\") {\n // Asked to continue a log this harness cannot read. The interrupted\n // run is ended rather than left open, because nothing else will\n // continue it either — the harness is a property of the session — and\n // a run that stays open is handed back by `interrupted` every window,\n // for ever. A worker that provisions anything before it runs pays for\n // that each time round.\n const failure: Failure = {\n code: \"no-recovery\",\n message: `The ${agent.harness.id} harness cannot restart from history, so this activation cannot be continued.`,\n retryable: false,\n };\n // Whoever was waiting on it is told, in the write that ends it. A\n // parent expecting a child that can now never report is the same dead\n // end one layer up.\n const told = await reporting({ runId: interrupted, outcome: \"failed\", output: \"\" });\n await commit([{ type: \"run.finished\", runId: interrupted, outcome: \"failed\", error: failure }], told);\n return stopping(failure);\n }\n }\n\n // An interrupted predecessor is ended before a new activation begins over\n // it, so one session never holds two open runs. Only on a claim: the\n // store established there that nothing else is running, where a bare\n // `sessionId` establishes nothing and the run may still be live.\n if (arrivals.length && interrupted && claim) {\n await commit([{\n type: \"run.finished\", runId: interrupted, outcome: \"cancelled\",\n error: {\n code: \"superseded\",\n message: \"This activation was interrupted, and a later one began over it.\",\n retryable: false,\n },\n }]);\n }\n\n // Nothing opens a resumption. A `run.started` recording input nobody sent\n // would be a second beginning for one conversation; the loop reads the\n // history it was handed instead, sees the call that already ran, and\n // carries on rather than asking for it again.\n await commit(arrivals.map((arrival): Entry => ({\n type: \"run.started\", runId, input: arrival.input,\n ...(arrival.from ? { from: arrival.from } : {}),\n })));\n\n // Every ceiling is checked where work is recorded, and a resumption\n // records nothing before its first turn — so a deadline that had already\n // passed was never seen. Checked once here too, before the harness is\n // handed anything.\n stopped ??= exceeded();\n\n const outcome = stopped\n ? { status: \"failed\" as const, error: stopped }\n : await agent.harness.run({\n sessionId, runId,\n instructions: agent.instructions,\n history: () => toMessages({\n instructions: agent.instructions,\n entries: log.entries,\n ...(options.context ? { context: options.context } : {}),\n }),\n entries: () => log.entries,\n ...(options.context ? { context: options.context } : {}),\n ...(executor ? { tools: executor } : {}),\n commit: (entries) => commit(entries),\n ...(store ? { drain } : {}),\n emit: (update) => updates.push(update),\n signal,\n });\n\n // A harness stopped by the ceiling reports a cancellation, because that is\n // all it saw. The run knows why it was cancelled and says so instead.\n const settled = stopped && outcome.status === \"cancelled\"\n ? { status: \"failed\" as const, error: stopped }\n : outcome;\n\n const output = settled.status === \"completed\" ? settled.output : \"\";\n const finished = await reporting({ runId, outcome: settled.status, output });\n\n await commit(\n [{\n type: \"run.finished\", runId,\n outcome: settled.status === \"completed\" ? \"completed\" : settled.status === \"cancelled\" ? \"cancelled\" : \"failed\",\n ...(settled.status === \"failed\" ? { error: settled.error } : {}),\n }],\n finished,\n );\n\n if (settled.status === \"completed\") {\n return { status: \"completed\", output: settled.output, seq: log.seq, usage };\n }\n if (settled.status === \"cancelled\") return { status: \"cancelled\", seq: log.seq, usage };\n return stopping(settled.error);\n } catch (error) {\n if (error instanceof CommitFailed) {\n return { status: \"failed\", error: error.failure, seq: error.seq, usage };\n }\n throw error;\n } finally {\n updates.close();\n }\n }\n}\n\nclass CommitFailed extends Error {\n constructor(readonly failure: Failure, readonly seq: number) {\n super(failure.message);\n }\n}\n\n/**\n * Where the session actually is after a refused write. A conflict knows; every\n * other failure leaves the caller with the position it already had. Reporting\n * zero told a caller at forty that it was at the beginning.\n *\n * The `typeof` is not defensive noise: a store may report a conflict, and only\n * a `StoreConflict` carries the position. Casting on the code alone produced a\n * `RunResult.seq` of `undefined` under a type that says `number`.\n */\nfunction positionOf(error: StoreError, fallback: number): number {\n const actual = (error as StoreConflict).actualSeq;\n return error.code === \"conflict\" && typeof actual === \"number\" ? actual : fallback;\n}\n\nconst isArrival = (value: unknown): value is Arrival =>\n typeof value === \"object\" && value !== null && !Array.isArray(value) && \"input\" in value;\n\n/**\n * One arrival, or several.\n *\n * The elements decide it, because the outer shape cannot: an arrival names its\n * `input`, a content part names its `type`, and nothing is both. Reading every\n * array as a batch is what turned one image-and-text message into one entry per\n * part, each holding a part where whole content belongs.\n */\nfunction arrivalsOf(input: RunAgentOptions[\"input\"]): readonly Arrival[] {\n if (input === undefined) return [];\n // Nothing sent is nothing sent. Falling through committed a `run.started`\n // whose input was an empty array of content parts.\n if (Array.isArray(input) && input.length === 0) return [];\n if (Array.isArray(input) && input.length > 0 && input.every(isArrival)) return input;\n if (isArrival(input)) return [input];\n return [{ input: input as Content }];\n}\n\n/**\n * The activation still open on this log, if one is.\n *\n * `run.started` opens and `run.finished` closes, which is the same rule the\n * store maintains its claim from — read here from the entries rather than asked\n * of the store, because the log is the only thing that knows.\n */\nfunction openRunId(entries: readonly Stored[]): string | undefined {\n let open: string | undefined;\n for (const entry of entries) {\n if (entry.type === \"run.started\") open = entry.runId;\n if (entry.type === \"run.finished\") open = undefined;\n }\n return open;\n}\n\n/** Token counts add; unknown stays unknown rather than becoming a zero. */\nfunction accumulate(total: Usage, turn: Usage): void {\n for (const key of [\"inputTokens\", \"outputTokens\", \"cacheReadTokens\", \"cacheWriteTokens\"] as const) {\n if (turn[key] !== undefined) total[key] = (total[key] ?? 0) + turn[key]!;\n }\n}\n\nasync function open(store: Store, sessionId: string, agent: Agent, key?: string) {\n const existing = await store.read({ sessionId });\n if (existing.ok) {\n // `version` is documented as the boundary past which an agent is no longer\n // compatible with an existing session. Nothing was comparing it, so the\n // boundary held for exactly as long as nobody crossed it.\n const stored = existing.value.agent;\n if (stored.id !== agent.id || stored.version !== agent.version) {\n return err<StoreError>({\n code: \"conflict\",\n message: `Session ${sessionId} belongs to ${stored.id}@${stored.version}, not ${agent.id}@${agent.version}`,\n retryable: false,\n });\n }\n return existing;\n }\n if (existing.error.code !== \"not-found\") return existing;\n const created = await store.create({\n sessionId, agent: { id: agent.id, version: agent.version }, ...(key ? { key } : {}),\n });\n if (!created.ok) return created;\n return store.read({ sessionId });\n}\n\n/**\n * A bounded fan-out queue for ephemeral updates.\n *\n * One buffer of recent updates, which every listener starts from and which\n * never grows past its limit. That single rule gives three things:\n *\n * **A run nobody drains costs a fixed amount.** A headless worker used to\n * accumulate every delta of an entire activation, because the queue only ever\n * grew. Dropping the oldest is safe precisely because updates are not recovery\n * state and the committed log is.\n *\n * **Arriving late loses nothing that matters.** The first entry commits before\n * `runAgent` returns, so a caller that iterates immediately would otherwise\n * never see its own run start. A UI reconnecting mid-run gets recent history\n * for the same reason, without asking the store for it.\n *\n * **Everyone sees everything.** Each listener drains its own copy, so an\n * application can watch a run for telemetry and for a UI at once instead of the\n * two stealing events from each other.\n */\nfunction createChannel<T>(limit = 1_024) {\n interface Listener { buffer: T[]; wake?: () => void }\n const listeners = new Set<Listener>();\n const replay: T[] = [];\n let closed = false;\n\n const bounded = (buffer: T[], value: T) => {\n if (buffer.length >= limit) buffer.shift();\n buffer.push(value);\n };\n\n return {\n push(value: T) {\n bounded(replay, value);\n for (const listener of listeners) {\n bounded(listener.buffer, value);\n listener.wake?.();\n }\n },\n close() {\n closed = true;\n for (const listener of listeners) listener.wake?.();\n },\n iterator(): AsyncIterator<T> {\n const listener: Listener = { buffer: [...replay] };\n listeners.add(listener);\n const done = (): IteratorResult<T> => {\n listeners.delete(listener);\n return { value: undefined, done: true };\n };\n return {\n async next(): Promise<IteratorResult<T>> {\n for (;;) {\n const value = listener.buffer.shift();\n if (value !== undefined) return { value, done: false };\n if (closed) return done();\n await new Promise<void>((resolve) => { listener.wake = resolve; });\n }\n },\n async return(): Promise<IteratorResult<T>> { return done(); },\n };\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SandboxProvider } from "../sandbox.js";
|
|
2
|
+
export interface DockerSandboxProviderOptions {
|
|
3
|
+
/** The image every container is made from. */
|
|
4
|
+
image: string;
|
|
5
|
+
/**
|
|
6
|
+
* How long a container lives before it reaps itself, in seconds. Defaults to
|
|
7
|
+
* an hour. It exists because a process that died without closing would
|
|
8
|
+
* otherwise leave a machine that outlives the laptop's next reboot.
|
|
9
|
+
*/
|
|
10
|
+
lifetimeSeconds?: number;
|
|
11
|
+
/** Where relative paths resolve, and the containers' working directory. Defaults to `/home/user`. */
|
|
12
|
+
root?: string;
|
|
13
|
+
/** Deadline for one `docker` call, in milliseconds. Composed with the caller's signal. */
|
|
14
|
+
requestTimeoutMs?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Containers from the local daemon.
|
|
18
|
+
*
|
|
19
|
+
* Each runs nothing but a sleep long enough to serve a session and no longer,
|
|
20
|
+
* so the container reaps itself on the same ceiling a hosted provider enforces.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createDockerSandboxProvider(options: DockerSandboxProviderOptions): SandboxProvider;
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A machine on this computer, in a container.
|
|
3
|
+
*
|
|
4
|
+
* The port says isolation is requested and either enforced or refused, and
|
|
5
|
+
* until this existed nothing in the package could answer the first half: the
|
|
6
|
+
* local provider runs with the host's authority and says so, which makes the
|
|
7
|
+
* contract honest and leaves it unimplementable. A caller that wanted the thing
|
|
8
|
+
* the port describes had to go and write it, and every one of them wrote the
|
|
9
|
+
* same file.
|
|
10
|
+
*
|
|
11
|
+
* Docker rather than a hosted provider because it needs no account, no vendor
|
|
12
|
+
* SDK and no network: the daemon is on the machine, the client is a program,
|
|
13
|
+
* and the whole adapter is argument arrays. That keeps the package's one
|
|
14
|
+
* dependency one, and it means the isolation a caller asks for is testable on a
|
|
15
|
+
* laptop rather than only on somebody's bill.
|
|
16
|
+
*
|
|
17
|
+
* A container is also what makes a remote agent reachable. The host answers on
|
|
18
|
+
* `host.docker.internal` from inside, so a process started in here can call the
|
|
19
|
+
* application that started it without a tunnel to the open internet.
|
|
20
|
+
*/
|
|
21
|
+
import { spawn as spawnProcess } from "node:child_process";
|
|
22
|
+
import { Readable } from "node:stream";
|
|
23
|
+
import { err, ok } from "../../result.js";
|
|
24
|
+
/** Docker's own client. Its absence is the one failure worth naming plainly. */
|
|
25
|
+
const DOCKER = "docker";
|
|
26
|
+
/** The address the host answers on from inside a container. Docker Desktop
|
|
27
|
+
* publishes it; `host-gateway` is what makes it true on a plain Linux daemon
|
|
28
|
+
* too, so one URL works either place. Not exported: nothing composes it yet,
|
|
29
|
+
* and a name with no consumer is surface with a cost. */
|
|
30
|
+
const HOST_ADDRESS = "host.docker.internal";
|
|
31
|
+
/**
|
|
32
|
+
* A command may produce useful output; no command may consume the host by
|
|
33
|
+
* streaming forever. Collected output past this — the two streams together, and
|
|
34
|
+
* a file read through `cat` with them — is refused rather than truncated
|
|
35
|
+
* silently, because a caller that asked for a file and got most of one has no
|
|
36
|
+
* way to tell.
|
|
37
|
+
*
|
|
38
|
+
* Refusing ends this client and not the command: what `docker exec` started
|
|
39
|
+
* keeps running in the container, where the container's own lifetime reaps it.
|
|
40
|
+
*/
|
|
41
|
+
const MAX_OUTPUT_BYTES = 1_000_000;
|
|
42
|
+
/** Everything this adapter reports. `retryable` follows the code rather than
|
|
43
|
+
* being decided call by call: a daemon that is not there may be there in a
|
|
44
|
+
* moment, and a denied path never will be. */
|
|
45
|
+
function failure(code, message) {
|
|
46
|
+
return err({ code, message, retryable: code === "unavailable" || code === "timeout" });
|
|
47
|
+
}
|
|
48
|
+
/** The one place an abort becomes a code: a deadline reads `timeout`, and
|
|
49
|
+
* anything else is the caller changing its mind. */
|
|
50
|
+
function aborted(signal, reason) {
|
|
51
|
+
const cause = (signal?.aborted ? signal.reason : undefined) ?? reason;
|
|
52
|
+
const timedOut = typeof cause === "object" && cause !== null && cause.name === "TimeoutError";
|
|
53
|
+
return timedOut
|
|
54
|
+
? failure("timeout", "Sandbox operation timed out")
|
|
55
|
+
: failure("cancelled", "Sandbox operation cancelled");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* One `docker` invocation.
|
|
59
|
+
*
|
|
60
|
+
* Nothing goes through a shell, so a path or an environment value is an
|
|
61
|
+
* argument and never something to quote. `node:child_process` rather than
|
|
62
|
+
* `Bun.spawn` because there is no reason for this adapter to be less portable
|
|
63
|
+
* than the daemon it talks to.
|
|
64
|
+
*
|
|
65
|
+
* An abort or a deadline ends this client only. A command it started is still
|
|
66
|
+
* the container's, and runs there until the container's lifetime reaps it.
|
|
67
|
+
*/
|
|
68
|
+
function docker(input) {
|
|
69
|
+
return new Promise((settle) => {
|
|
70
|
+
const deadline = AbortSignal.timeout(input.timeoutMs);
|
|
71
|
+
const signal = input.signal ? AbortSignal.any([input.signal, deadline]) : deadline;
|
|
72
|
+
let child;
|
|
73
|
+
try {
|
|
74
|
+
child = spawnProcess(DOCKER, [...input.argv], {
|
|
75
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
76
|
+
...(input.environment ? { env: input.environment } : {}),
|
|
77
|
+
signal,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
settle(failure("unavailable", error instanceof Error ? error.message : String(error)));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const stdout = [];
|
|
85
|
+
const stderr = [];
|
|
86
|
+
let bytes = 0;
|
|
87
|
+
let exceeded = false;
|
|
88
|
+
const collect = (into, chunk) => {
|
|
89
|
+
if (exceeded)
|
|
90
|
+
return;
|
|
91
|
+
const remaining = MAX_OUTPUT_BYTES - bytes;
|
|
92
|
+
if (remaining > 0) {
|
|
93
|
+
const kept = chunk.subarray(0, remaining);
|
|
94
|
+
into.push(kept);
|
|
95
|
+
bytes += kept.byteLength;
|
|
96
|
+
}
|
|
97
|
+
if (bytes >= MAX_OUTPUT_BYTES) {
|
|
98
|
+
exceeded = true;
|
|
99
|
+
child.kill();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
child.stdout.on("data", (chunk) => collect(stdout, chunk));
|
|
103
|
+
child.stderr.on("data", (chunk) => collect(stderr, chunk));
|
|
104
|
+
child.stdin.on("error", () => { });
|
|
105
|
+
child.stdin.end(input.stdin ?? "");
|
|
106
|
+
child.on("error", (error) => {
|
|
107
|
+
if (signal.aborted) {
|
|
108
|
+
settle(aborted(input.signal, signal.reason));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
settle(failure("unavailable", error.code === "ENOENT" ? "docker is not on PATH" : error.message));
|
|
112
|
+
});
|
|
113
|
+
child.on("close", (code) => {
|
|
114
|
+
if (signal.aborted) {
|
|
115
|
+
settle(aborted(input.signal, signal.reason));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (exceeded) {
|
|
119
|
+
settle(failure("failed", `Output exceeded ${MAX_OUTPUT_BYTES} bytes`));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
settle(ok({
|
|
123
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
124
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
125
|
+
code: code ?? 1,
|
|
126
|
+
}));
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/** The client failing means the daemon is unreachable or the container is gone;
|
|
131
|
+
* either way the sandbox is not there to be used. */
|
|
132
|
+
const unavailable = (ran) => failure("unavailable", ran.stderr.trim() || ran.stdout.trim() || `docker exited ${ran.code}`);
|
|
133
|
+
/** What the daemon writes when the container is not there to run anything in.
|
|
134
|
+
* It writes it to the same stderr the command has, so a match is a question to
|
|
135
|
+
* put to the daemon and never an answer on its own. */
|
|
136
|
+
const looksGone = (stderr) => /No such container|is not running/i.test(stderr);
|
|
137
|
+
/** Asks the daemon whether a container is there and running. */
|
|
138
|
+
const inspectRunning = (id, config, signal) => docker({
|
|
139
|
+
argv: ["inspect", "--format", "{{.State.Running}}", id],
|
|
140
|
+
timeoutMs: config.requestTimeoutMs,
|
|
141
|
+
...(signal ? { signal } : {}),
|
|
142
|
+
});
|
|
143
|
+
/** How that answer reads. A daemon that could not be asked is not a container
|
|
144
|
+
* that is running. */
|
|
145
|
+
const running = (state) => state.ok && state.value.code === 0 && state.value.stdout.trim() === "true";
|
|
146
|
+
/**
|
|
147
|
+
* Resolves `path` against `root`, and answers only when it stayed inside.
|
|
148
|
+
*
|
|
149
|
+
* POSIX by hand: these are paths in the container, not on the calling machine,
|
|
150
|
+
* so `node:path` would resolve them against the wrong filesystem's rules.
|
|
151
|
+
*
|
|
152
|
+
* Lexical, and only lexical: a symlink under the root that points out of it is
|
|
153
|
+
* followed by the filesystem this cannot see. Confining paths to the root is a
|
|
154
|
+
* guard against slips, and the container is the boundary that holds.
|
|
155
|
+
*/
|
|
156
|
+
function within(root, path) {
|
|
157
|
+
const base = root.replace(/\/+$/, "");
|
|
158
|
+
const segments = [];
|
|
159
|
+
for (const segment of (path.startsWith("/") ? path : `${base}/${path}`).split("/")) {
|
|
160
|
+
if (segment === "" || segment === ".")
|
|
161
|
+
continue;
|
|
162
|
+
if (segment === "..") {
|
|
163
|
+
if (segments.pop() === undefined)
|
|
164
|
+
return undefined;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
segments.push(segment);
|
|
168
|
+
}
|
|
169
|
+
const resolved = `/${segments.join("/")}`;
|
|
170
|
+
return resolved === base || resolved.startsWith(`${base}/`) ? resolved : undefined;
|
|
171
|
+
}
|
|
172
|
+
/** Single-quoted for `/bin/sh`, so a directory name with a space or a quote in
|
|
173
|
+
* it is a directory name and not the start of another command. */
|
|
174
|
+
const quoted = (value) => `'${value.split("'").join("'\\''")}'`;
|
|
175
|
+
/**
|
|
176
|
+
* The `--env NAME` flags for an environment, or a refusal.
|
|
177
|
+
*
|
|
178
|
+
* Named rather than valued: the value travels in this client's own environment,
|
|
179
|
+
* so nothing a model typed is ever pasted into an argument or published in the
|
|
180
|
+
* host's process table, where an agent's credentials would be readable by
|
|
181
|
+
* anything on the machine. A name that is itself an assignment would defeat
|
|
182
|
+
* that — `docker` would take it whole and set a variable the caller never
|
|
183
|
+
* named, dropping the value it did — so it is refused rather than passed.
|
|
184
|
+
*/
|
|
185
|
+
function envFlags(env) {
|
|
186
|
+
const flags = [];
|
|
187
|
+
for (const name of Object.keys(env ?? {})) {
|
|
188
|
+
if (name === "" || name.includes("=")) {
|
|
189
|
+
return failure("failed", `'${name}' is not the name of an environment variable`);
|
|
190
|
+
}
|
|
191
|
+
flags.push("--env", name);
|
|
192
|
+
}
|
|
193
|
+
return ok(flags);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* @param base Set for every call, under each call's own. Carries `create`'s
|
|
197
|
+
* `env` and its `secrets` together, because to a container they are the same
|
|
198
|
+
* thing — see the `secrets: "plain"` this reports.
|
|
199
|
+
*/
|
|
200
|
+
function dockerSandbox(id, config, base) {
|
|
201
|
+
let closed = false;
|
|
202
|
+
const gone = () => failure("unavailable", "Sandbox is closed");
|
|
203
|
+
const call = (argv, request, stdin, environment) => docker({
|
|
204
|
+
argv,
|
|
205
|
+
timeoutMs: config.requestTimeoutMs,
|
|
206
|
+
...(request.signal ? { signal: request.signal } : {}),
|
|
207
|
+
...(stdin === undefined ? {} : { stdin }),
|
|
208
|
+
...(environment ? { environment } : {}),
|
|
209
|
+
});
|
|
210
|
+
return {
|
|
211
|
+
id,
|
|
212
|
+
root: config.root,
|
|
213
|
+
// The container is the containment: a shell the model talked into
|
|
214
|
+
// misbehaving holds nothing this process has.
|
|
215
|
+
isolation: "container",
|
|
216
|
+
// A container has no egress this adapter controls, so a value it is given
|
|
217
|
+
// is a value it holds. The names travel as `--env NAME` and the values in
|
|
218
|
+
// this client's own environment, so nothing is published in the host's
|
|
219
|
+
// process table — but inside the box it is readable, and that is what
|
|
220
|
+
// "plain" says.
|
|
221
|
+
secrets: "plain",
|
|
222
|
+
async exec(request) {
|
|
223
|
+
if (closed)
|
|
224
|
+
return gone();
|
|
225
|
+
const cwd = request.cwd ? within(config.root, request.cwd) : config.root;
|
|
226
|
+
if (!cwd)
|
|
227
|
+
return failure("denied", "Working directory is outside the sandbox root");
|
|
228
|
+
if (request.signal?.aborted)
|
|
229
|
+
return aborted(request.signal);
|
|
230
|
+
const environment = { ...base, ...request.env };
|
|
231
|
+
const named = envFlags(environment);
|
|
232
|
+
if (!named.ok)
|
|
233
|
+
return err(named.error);
|
|
234
|
+
// The directory is made rather than required — `docker exec -w` refuses
|
|
235
|
+
// one that is not there yet, and a command's cwd is the model's to name.
|
|
236
|
+
// The command follows on its own line, exactly as written, so there is no
|
|
237
|
+
// second round of quoting for it to be mangled by.
|
|
238
|
+
const script = `mkdir -p ${quoted(cwd)} && cd ${quoted(cwd)} || exit 1\n${request.command}`;
|
|
239
|
+
const ran = await call(["exec", ...named.value, "--interactive", id, "/bin/sh", "-c", script], request, undefined, named.value.length ? { ...process.env, ...environment } : undefined);
|
|
240
|
+
if (!ran.ok)
|
|
241
|
+
return err(ran.error);
|
|
242
|
+
// A command's own non-zero exit is an outcome. Docker's own 125/126/127
|
|
243
|
+
// are indistinguishable from a command that exited the same way, so only
|
|
244
|
+
// a container that is gone is a failure — and the daemon is asked whether
|
|
245
|
+
// it is, because the text saying so arrives on the stderr the command
|
|
246
|
+
// writes to: a command that printed it could otherwise report its own
|
|
247
|
+
// sandbox as gone, and be believed and retried.
|
|
248
|
+
if (looksGone(ran.value.stderr) && !running(await inspectRunning(id, config, request.signal))) {
|
|
249
|
+
return unavailable(ran.value);
|
|
250
|
+
}
|
|
251
|
+
return ok({ stdout: ran.value.stdout, stderr: ran.value.stderr, exitCode: ran.value.code });
|
|
252
|
+
},
|
|
253
|
+
async spawn(request) {
|
|
254
|
+
if (closed)
|
|
255
|
+
return gone();
|
|
256
|
+
const cwd = request.cwd ? within(config.root, request.cwd) : config.root;
|
|
257
|
+
if (!cwd)
|
|
258
|
+
return failure("denied", "Working directory is outside the sandbox root");
|
|
259
|
+
const [program, ...rest] = request.command;
|
|
260
|
+
if (!program)
|
|
261
|
+
return failure("failed", "spawn was given no command");
|
|
262
|
+
const environment = { ...base, ...request.env };
|
|
263
|
+
const named = envFlags(environment);
|
|
264
|
+
if (!named.ok)
|
|
265
|
+
return err(named.error);
|
|
266
|
+
let child;
|
|
267
|
+
try {
|
|
268
|
+
child = spawnProcess(DOCKER,
|
|
269
|
+
// `--workdir` where `exec` makes the directory first: docker refuses
|
|
270
|
+
// to start a process in a path that is not there, and refuses it as
|
|
271
|
+
// the process's own exit rather than as this call's result. A caller
|
|
272
|
+
// that means to start something somewhere new makes it with `exec`.
|
|
273
|
+
["exec", "--interactive", ...named.value, "--workdir", cwd, id, program, ...rest], {
|
|
274
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
275
|
+
...(named.value.length ? { env: { ...process.env, ...environment } } : {}),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
return failure("unavailable", error instanceof Error ? error.message : String(error));
|
|
280
|
+
}
|
|
281
|
+
child.stdin.on("error", () => { });
|
|
282
|
+
// A launch that fails does so after this call returns, and an `error`
|
|
283
|
+
// event with no listener is an uncaught exception that takes the host
|
|
284
|
+
// process down. No `close` follows it either, so `exited` would never
|
|
285
|
+
// settle. Waiting for the launch makes it the typed failure the port
|
|
286
|
+
// already has a place for, and the listener stays attached afterwards so
|
|
287
|
+
// that a later one is not fatal either.
|
|
288
|
+
const failed = await new Promise((settle) => {
|
|
289
|
+
child.once("spawn", () => settle(undefined));
|
|
290
|
+
child.once("error", (error) => settle({
|
|
291
|
+
code: "unavailable",
|
|
292
|
+
message: error.code === "ENOENT" ? "docker is not on PATH" : error.message,
|
|
293
|
+
retryable: true,
|
|
294
|
+
}));
|
|
295
|
+
});
|
|
296
|
+
if (failed)
|
|
297
|
+
return err(failed);
|
|
298
|
+
return ok({
|
|
299
|
+
write: (chunk) => { child.stdin.write(chunk); },
|
|
300
|
+
stdout: Readable.toWeb(child.stdout),
|
|
301
|
+
stderr: Readable.toWeb(child.stderr),
|
|
302
|
+
exited: new Promise((settle) => { child.on("close", (code) => settle(code ?? 1)); }),
|
|
303
|
+
kill: () => { child.kill(); },
|
|
304
|
+
});
|
|
305
|
+
},
|
|
306
|
+
async readFile(request) {
|
|
307
|
+
if (closed)
|
|
308
|
+
return gone();
|
|
309
|
+
const path = within(config.root, request.path);
|
|
310
|
+
if (!path)
|
|
311
|
+
return failure("denied", "Path is outside the sandbox root");
|
|
312
|
+
if (request.signal?.aborted)
|
|
313
|
+
return aborted(request.signal);
|
|
314
|
+
const ran = await call(["exec", id, "cat", path], request);
|
|
315
|
+
if (!ran.ok)
|
|
316
|
+
return err(ran.error);
|
|
317
|
+
if (ran.value.code !== 0) {
|
|
318
|
+
return /No such file or directory/i.test(ran.value.stderr)
|
|
319
|
+
? failure("not-found", `No file at ${request.path}`)
|
|
320
|
+
: failure("failed", ran.value.stderr.trim() || `cat exited ${ran.value.code}`);
|
|
321
|
+
}
|
|
322
|
+
return ok(ran.value.stdout);
|
|
323
|
+
},
|
|
324
|
+
async writeFile(request) {
|
|
325
|
+
if (closed)
|
|
326
|
+
return gone();
|
|
327
|
+
const path = within(config.root, request.path);
|
|
328
|
+
if (!path)
|
|
329
|
+
return failure("denied", "Path is outside the sandbox root");
|
|
330
|
+
if (request.signal?.aborted)
|
|
331
|
+
return aborted(request.signal);
|
|
332
|
+
const ran = await call(["exec", "--interactive", id, "/bin/sh", "-c", 'mkdir -p "$(dirname "$1")" && cat > "$1"', "sh", path], request, request.content);
|
|
333
|
+
if (!ran.ok)
|
|
334
|
+
return err(ran.error);
|
|
335
|
+
if (ran.value.code !== 0) {
|
|
336
|
+
return failure("failed", ran.value.stderr.trim() || `write exited ${ran.value.code}`);
|
|
337
|
+
}
|
|
338
|
+
return ok(undefined);
|
|
339
|
+
},
|
|
340
|
+
async close() {
|
|
341
|
+
if (closed)
|
|
342
|
+
return ok(undefined);
|
|
343
|
+
const ran = await docker({ argv: ["rm", "--force", id], timeoutMs: config.requestTimeoutMs });
|
|
344
|
+
if (!ran.ok)
|
|
345
|
+
return err(ran.error);
|
|
346
|
+
// A container already gone is what closing asked for.
|
|
347
|
+
if (ran.value.code !== 0 && !/No such container/i.test(ran.value.stderr))
|
|
348
|
+
return unavailable(ran.value);
|
|
349
|
+
// Marked only once the machine is actually gone. Marking it before the
|
|
350
|
+
// daemon answered would tell a caller retrying a reported refusal that
|
|
351
|
+
// the container it is still paying for had been released.
|
|
352
|
+
closed = true;
|
|
353
|
+
return ok(undefined);
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Containers from the local daemon.
|
|
359
|
+
*
|
|
360
|
+
* Each runs nothing but a sleep long enough to serve a session and no longer,
|
|
361
|
+
* so the container reaps itself on the same ceiling a hosted provider enforces.
|
|
362
|
+
*/
|
|
363
|
+
export function createDockerSandboxProvider(options) {
|
|
364
|
+
const config = {
|
|
365
|
+
root: "/home/user",
|
|
366
|
+
lifetimeSeconds: 60 * 60,
|
|
367
|
+
requestTimeoutMs: 60_000,
|
|
368
|
+
...options,
|
|
369
|
+
};
|
|
370
|
+
return {
|
|
371
|
+
id: "docker",
|
|
372
|
+
async create(request) {
|
|
373
|
+
// Merged here and not later: to a container these are one environment,
|
|
374
|
+
// and keeping two would be two names for what every call has to combine
|
|
375
|
+
// anyway. What the caller gains by having said `secrets` is the honest
|
|
376
|
+
// `"plain"` this sandbox reports back.
|
|
377
|
+
const base = {
|
|
378
|
+
...request.env,
|
|
379
|
+
...Object.fromEntries((request.secrets ?? []).map((secret) => [secret.env, secret.value])),
|
|
380
|
+
};
|
|
381
|
+
// Refused rather than ignored. A caller that named an allowlist and got
|
|
382
|
+
// an unrestricted container would have no way to find that out.
|
|
383
|
+
if (request.network.mode === "web-allowlist") {
|
|
384
|
+
return failure("unsupported", "Docker is not configured for a web allowlist; use deny-all or unrestricted.");
|
|
385
|
+
}
|
|
386
|
+
if (request.signal?.aborted)
|
|
387
|
+
return aborted(request.signal);
|
|
388
|
+
// Reattaching must find a machine, not make one: a container that has
|
|
389
|
+
// stopped is one whose files are already gone.
|
|
390
|
+
if (request.reconnect !== undefined) {
|
|
391
|
+
const found = await inspectRunning(request.reconnect, config, request.signal);
|
|
392
|
+
// A daemon that could not be asked is not the same answer as a machine
|
|
393
|
+
// that is not there, and only the second one is `not-found`.
|
|
394
|
+
if (!found.ok)
|
|
395
|
+
return err(found.error);
|
|
396
|
+
if (!running(found))
|
|
397
|
+
return failure("not-found", `No sandbox ${request.reconnect}`);
|
|
398
|
+
return ok(dockerSandbox(request.reconnect, config, base));
|
|
399
|
+
}
|
|
400
|
+
const ran = await docker({
|
|
401
|
+
argv: [
|
|
402
|
+
"run", "--detach", "--rm",
|
|
403
|
+
// What the host answers to from inside. Docker Desktop resolves it
|
|
404
|
+
// anyway; the mapping is what makes a plain Linux daemon agree.
|
|
405
|
+
"--add-host", `${HOST_ADDRESS}:host-gateway`,
|
|
406
|
+
...(request.network.mode === "deny-all" ? ["--network", "none"] : []),
|
|
407
|
+
"--workdir", config.root,
|
|
408
|
+
"--env", `HOME=${config.root}`,
|
|
409
|
+
config.image,
|
|
410
|
+
"sleep", String(config.lifetimeSeconds),
|
|
411
|
+
],
|
|
412
|
+
timeoutMs: config.requestTimeoutMs,
|
|
413
|
+
...(request.signal ? { signal: request.signal } : {}),
|
|
414
|
+
});
|
|
415
|
+
if (!ran.ok)
|
|
416
|
+
return err(ran.error);
|
|
417
|
+
if (ran.value.code !== 0)
|
|
418
|
+
return unavailable(ran.value);
|
|
419
|
+
const id = ran.value.stdout.trim();
|
|
420
|
+
if (!id)
|
|
421
|
+
return failure("failed", "docker run named no container");
|
|
422
|
+
// `docker run --detach` answers as soon as the container is created, and
|
|
423
|
+
// every relative path is resolved against the root. One command from
|
|
424
|
+
// inside is what says the machine is there to run them.
|
|
425
|
+
const prepared = await docker({
|
|
426
|
+
argv: ["exec", id, "mkdir", "-p", config.root],
|
|
427
|
+
timeoutMs: config.requestTimeoutMs,
|
|
428
|
+
...(request.signal ? { signal: request.signal } : {}),
|
|
429
|
+
});
|
|
430
|
+
if (!prepared.ok || prepared.value.code !== 0) {
|
|
431
|
+
// The container exists whether or not it can be used, and this is the
|
|
432
|
+
// last place its id is held: not removing it here leaks a machine that
|
|
433
|
+
// nothing can reach and only its own lifetime ends.
|
|
434
|
+
await docker({ argv: ["rm", "--force", id], timeoutMs: config.requestTimeoutMs });
|
|
435
|
+
return prepared.ok
|
|
436
|
+
? failure("failed", prepared.value.stderr.trim() || `preparing ${config.root} exited ${prepared.value.code}`)
|
|
437
|
+
: err(prepared.error);
|
|
438
|
+
}
|
|
439
|
+
return ok(dockerSandbox(id, config, base));
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
//# sourceMappingURL=docker.js.map
|