@agimon-ai/doompi-sandbox 0.0.1-alpha.37
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 +221 -0
- package/dist/_virtual/_rolldown/runtime.cjs +1 -0
- package/dist/adapters/brokerHost.cjs +2 -0
- package/dist/adapters/brokerHost.cjs.map +1 -0
- package/dist/adapters/brokerHost.d.cts +16 -0
- package/dist/adapters/brokerHost.d.cts.map +1 -0
- package/dist/adapters/brokerHost.d.mts +16 -0
- package/dist/adapters/brokerHost.d.mts.map +1 -0
- package/dist/adapters/brokerHost.mjs +2 -0
- package/dist/adapters/brokerHost.mjs.map +1 -0
- package/dist/adapters/brokerServer.cjs +2 -0
- package/dist/adapters/brokerServer.cjs.map +1 -0
- package/dist/adapters/brokerServer.mjs +2 -0
- package/dist/adapters/brokerServer.mjs.map +1 -0
- package/dist/adapters/devcontainer.cjs +2 -0
- package/dist/adapters/devcontainer.cjs.map +1 -0
- package/dist/adapters/devcontainer.mjs +2 -0
- package/dist/adapters/devcontainer.mjs.map +1 -0
- package/dist/adapters/engineProcess.cjs +2 -0
- package/dist/adapters/engineProcess.cjs.map +1 -0
- package/dist/adapters/engineProcess.mjs +2 -0
- package/dist/adapters/engineProcess.mjs.map +1 -0
- package/dist/adapters/harness.cjs +2 -0
- package/dist/adapters/harness.cjs.map +1 -0
- package/dist/adapters/harness.d.cts +26 -0
- package/dist/adapters/harness.d.cts.map +1 -0
- package/dist/adapters/harness.d.mts +26 -0
- package/dist/adapters/harness.d.mts.map +1 -0
- package/dist/adapters/harness.mjs +2 -0
- package/dist/adapters/harness.mjs.map +1 -0
- package/dist/adapters/loginPorts.cjs +2 -0
- package/dist/adapters/loginPorts.cjs.map +1 -0
- package/dist/adapters/loginPorts.mjs +2 -0
- package/dist/adapters/loginPorts.mjs.map +1 -0
- package/dist/adapters/pi/extension.cjs +2 -0
- package/dist/adapters/pi/extension.cjs.map +1 -0
- package/dist/adapters/pi/extension.d.cts +9 -0
- package/dist/adapters/pi/extension.d.cts.map +1 -0
- package/dist/adapters/pi/extension.d.mts +9 -0
- package/dist/adapters/pi/extension.d.mts.map +1 -0
- package/dist/adapters/pi/extension.mjs +2 -0
- package/dist/adapters/pi/extension.mjs.map +1 -0
- package/dist/adapters/sandboxImageTag.cjs +2 -0
- package/dist/adapters/sandboxImageTag.cjs.map +1 -0
- package/dist/adapters/sandboxImageTag.mjs +2 -0
- package/dist/adapters/sandboxImageTag.mjs.map +1 -0
- package/dist/commands/doomSandboxCommand.cjs +2 -0
- package/dist/commands/doomSandboxCommand.cjs.map +1 -0
- package/dist/commands/doomSandboxCommand.mjs +2 -0
- package/dist/commands/doomSandboxCommand.mjs.map +1 -0
- package/dist/container/index.cjs +2 -0
- package/dist/container/index.cjs.map +1 -0
- package/dist/container/index.d.cts +6 -0
- package/dist/container/index.d.cts.map +1 -0
- package/dist/container/index.d.mts +6 -0
- package/dist/container/index.d.mts.map +1 -0
- package/dist/container/index.mjs +2 -0
- package/dist/container/index.mjs.map +1 -0
- package/dist/extensions/pi.cjs +1 -0
- package/dist/extensions/pi.d.cts +2 -0
- package/dist/extensions/pi.d.mts +2 -0
- package/dist/extensions/pi.mjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +1 -0
- package/dist/sandboxHarness.cjs +1 -0
- package/dist/sandboxHarness.d.cts +2 -0
- package/dist/sandboxHarness.d.mts +2 -0
- package/dist/sandboxHarness.mjs +1 -0
- package/dist/services/brokerProviders.cjs +2 -0
- package/dist/services/brokerProviders.cjs.map +1 -0
- package/dist/services/brokerProviders.mjs +2 -0
- package/dist/services/brokerProviders.mjs.map +1 -0
- package/dist/services/brokerRoutes.cjs +2 -0
- package/dist/services/brokerRoutes.cjs.map +1 -0
- package/dist/services/brokerRoutes.mjs +2 -0
- package/dist/services/brokerRoutes.mjs.map +1 -0
- package/dist/services/devcontainer.cjs +3 -0
- package/dist/services/devcontainer.cjs.map +1 -0
- package/dist/services/devcontainer.mjs +3 -0
- package/dist/services/devcontainer.mjs.map +1 -0
- package/dist/services/extensionService.cjs +2 -0
- package/dist/services/extensionService.cjs.map +1 -0
- package/dist/services/extensionService.d.cts +10 -0
- package/dist/services/extensionService.d.cts.map +1 -0
- package/dist/services/extensionService.d.mts +10 -0
- package/dist/services/extensionService.d.mts.map +1 -0
- package/dist/services/extensionService.mjs +2 -0
- package/dist/services/extensionService.mjs.map +1 -0
- package/dist/services/oauthCallback.cjs +2 -0
- package/dist/services/oauthCallback.cjs.map +1 -0
- package/dist/services/oauthCallback.mjs +2 -0
- package/dist/services/oauthCallback.mjs.map +1 -0
- package/dist/services/runFlags.cjs +2 -0
- package/dist/services/runFlags.cjs.map +1 -0
- package/dist/services/runFlags.mjs +2 -0
- package/dist/services/runFlags.mjs.map +1 -0
- package/dist/services/sandboxBridge.cjs +3 -0
- package/dist/services/sandboxBridge.cjs.map +1 -0
- package/dist/services/sandboxBridge.mjs +3 -0
- package/dist/services/sandboxBridge.mjs.map +1 -0
- package/dist/services/sandboxEnvironment.cjs +2 -0
- package/dist/services/sandboxEnvironment.cjs.map +1 -0
- package/dist/services/sandboxEnvironment.mjs +2 -0
- package/dist/services/sandboxEnvironment.mjs.map +1 -0
- package/dist/services/sandboxImage.cjs +3 -0
- package/dist/services/sandboxImage.cjs.map +1 -0
- package/dist/services/sandboxImage.mjs +3 -0
- package/dist/services/sandboxImage.mjs.map +1 -0
- package/dist/services/sandboxPlan.cjs +2 -0
- package/dist/services/sandboxPlan.cjs.map +1 -0
- package/dist/services/sandboxPlan.mjs +2 -0
- package/dist/services/sandboxPlan.mjs.map +1 -0
- package/dist/types/extension.d.cts +15 -0
- package/dist/types/extension.d.cts.map +1 -0
- package/dist/types/extension.d.mts +15 -0
- package/dist/types/extension.d.mts.map +1 -0
- package/dist/types/sandboxHarness.d.cts +38 -0
- package/dist/types/sandboxHarness.d.cts.map +1 -0
- package/dist/types/sandboxHarness.d.mts +38 -0
- package/dist/types/sandboxHarness.d.mts.map +1 -0
- package/llms.txt +11 -0
- package/package.json +94 -0
- package/src/prompts/doompi-use-sandbox/SKILL.md +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vuong Ngo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# @agimon-ai/doompi-sandbox
|
|
2
|
+
|
|
3
|
+
Container sandbox for DoomPi launches: the agent, extensions, and tools run inside Docker or Podman while the terminal stays on the host
|
|
4
|
+
|
|
5
|
+
This package is a composable [DoomPi](https://www.npmjs.com/package/@agimon-ai/doompi) subsystem. Use it with the distribution or install it independently in [Pi](https://www.npmjs.com/package/@earendil-works/pi-coding-agent).
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Node.js 22.19.0 or newer
|
|
10
|
+
- `@earendil-works/pi-coding-agent` 0.84.2
|
|
11
|
+
- One of `docker`, `podman`, `nerdctl`, or `finch` on the host for sandboxed launches
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pi install npm:@agimon-ai/doompi-sandbox
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The package declares its Pi extension entry, so Pi loads it after installation. DoomPi users can include the package through their normal profile and domain composition instead.
|
|
20
|
+
|
|
21
|
+
## Enabling it
|
|
22
|
+
|
|
23
|
+
This layer is still in development, so `doompi init` does not add it to any mode. Opt in by hand:
|
|
24
|
+
name it as a layer in `.doom/modes.yaml`, then list that layer on the mode you want it in.
|
|
25
|
+
|
|
26
|
+
```yaml
|
|
27
|
+
layers:
|
|
28
|
+
sandbox:
|
|
29
|
+
packages:
|
|
30
|
+
- '@agimon-ai/doompi-sandbox'
|
|
31
|
+
|
|
32
|
+
majorMode:
|
|
33
|
+
copilot:
|
|
34
|
+
description: General-purpose coding mode.
|
|
35
|
+
layers: [team, ask-user, task, sandbox]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`--sandbox` resolves the provider from the selected mode, so a mode without this layer reports that
|
|
39
|
+
no sandbox harness is available and refuses to launch rather than running unsandboxed.
|
|
40
|
+
|
|
41
|
+
## Sandboxed launches
|
|
42
|
+
|
|
43
|
+
With this layer in the selected major mode, `doompi --sandbox` moves the whole session into a
|
|
44
|
+
disposable Linux container instead of running Pi on the host:
|
|
45
|
+
|
|
46
|
+
1. The harness resolves this package's `./sandbox-harness` export and delegates the launch.
|
|
47
|
+
2. The layer detects the first available engine in the order `docker`, `podman`, `nerdctl`,
|
|
48
|
+
`finch` (override with `DOOMPI_SANDBOX_ENGINE`), and builds the `doompi-sandbox:v<version>`
|
|
49
|
+
image on first use. The image installs the DoomPi distribution from the registry, so the
|
|
50
|
+
container never runs the host's platform-specific packages.
|
|
51
|
+
3. It starts `docker run --rm` with the repository bind-mounted at its host path, an isolated
|
|
52
|
+
home volume, and a volume shadowing the repository's `.pi` package store. Inside, the
|
|
53
|
+
`doompi` launcher replays the same major mode, domains, profile, and Pi arguments.
|
|
54
|
+
|
|
55
|
+
The terminal stays attached, so the session looks like a host launch while bash, file edits,
|
|
56
|
+
extensions, MCP servers, and skills all execute inside the container. `DOOMPI_SANDBOX=1` marks
|
|
57
|
+
every sandboxed process; nesting is refused.
|
|
58
|
+
|
|
59
|
+
Only an allowlisted environment enters the container: terminal and locale variables, proxy
|
|
60
|
+
settings, and `DOOMPI_PRESET`. Everything else a shell accumulates stays on the host.
|
|
61
|
+
|
|
62
|
+
## Engine and runtime selection
|
|
63
|
+
|
|
64
|
+
Every supported engine takes docker's `run` syntax. On macOS any docker-compatible VM manager
|
|
65
|
+
(Docker Desktop, OrbStack, colima, podman machine) works without configuration.
|
|
66
|
+
|
|
67
|
+
`DOOMPI_SANDBOX_RUN_FLAGS` passes extra options straight to the engine, which is how you select a
|
|
68
|
+
different isolation runtime without the layer having to know about it:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
DOOMPI_SANDBOX_RUN_FLAGS=--runtime=runsc doompi --sandbox
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Options must be self-contained (`--flag` or `--flag=value`). A separated value such as
|
|
75
|
+
`--runtime runsc` is refused, because a bare word cannot be told apart from an image name and
|
|
76
|
+
would silently launch a different container.
|
|
77
|
+
|
|
78
|
+
A stronger runtime that boots a VM rather than sharing the host kernel, such as Kata or
|
|
79
|
+
Firecracker, is untested here. Their filesystem passthrough is unlikely to carry the broker's
|
|
80
|
+
bind-mounted unix socket, so expect to turn brokering off or move it to a port first.
|
|
81
|
+
|
|
82
|
+
## Workspace dev containers
|
|
83
|
+
|
|
84
|
+
A repository with a `.devcontainer/devcontainer.json` (or a root `.devcontainer.json`) uses that
|
|
85
|
+
container instead of the built-in image, because a workspace that describes its own container is
|
|
86
|
+
describing the toolchain its agent needs. The Dev Containers CLI brings it up, so the file decides
|
|
87
|
+
the image, features, mounts, run arguments and lifecycle hooks in full.
|
|
88
|
+
|
|
89
|
+
**This mode is not an isolation boundary.** The configuration is author-controlled, and a
|
|
90
|
+
devcontainer that mounts your home directory or the docker socket removes the containment this
|
|
91
|
+
layer otherwise provides. The launch says so on every run. Set `DOOMPI_SANDBOX_DEVCONTAINER=0` to
|
|
92
|
+
ignore the file and use the built-in image, which is a boundary.
|
|
93
|
+
|
|
94
|
+
What still applies: the environment allowlist and the credential broker. The container receives the
|
|
95
|
+
session token rather than any real key, and reaches the broker over the host gateway.
|
|
96
|
+
|
|
97
|
+
Notes on this mode:
|
|
98
|
+
|
|
99
|
+
- DoomPi is installed into the container on first use with `npm install -g`, since a project's
|
|
100
|
+
container has no reason to carry it. The CLI reuses the container, so that cost is paid once for
|
|
101
|
+
its lifetime. A container without `npm` is reported rather than silently degraded.
|
|
102
|
+
- The session attaches through the engine rather than `devcontainer exec`, which allocates no
|
|
103
|
+
terminal and would break the full-screen TUI.
|
|
104
|
+
- OAuth callback ports cannot be published into a container this layer did not create, so `/login`
|
|
105
|
+
needs the ports declared as `appPort` in the devcontainer configuration.
|
|
106
|
+
|
|
107
|
+
## Signing in from inside the sandbox
|
|
108
|
+
|
|
109
|
+
Subscription logins are not brokered, so `/login` runs inside the container. Two things make its
|
|
110
|
+
browser callback reachable:
|
|
111
|
+
|
|
112
|
+
- The launch publishes Pi's fixed callback ports back onto host loopback: 1455 (OpenAI Codex),
|
|
113
|
+
1456 (Radius) and 53692 (Anthropic).
|
|
114
|
+
- `PI_OAUTH_CALLBACK_HOST=0.0.0.0` makes the in-container server bind every interface. A published
|
|
115
|
+
port reaches the container's external interface, never its loopback, so Pi's default bind would
|
|
116
|
+
refuse the connection. The redirect the provider sees is unchanged, still `localhost:<port>`.
|
|
117
|
+
|
|
118
|
+
Credentials land in the per-repository home volume, so a login survives later runs against the
|
|
119
|
+
same repository.
|
|
120
|
+
|
|
121
|
+
Ports already held by another process are skipped rather than failing the launch, and the run says
|
|
122
|
+
so. A second concurrent sandbox therefore starts normally but cannot complete a login. Providers
|
|
123
|
+
that bind an ephemeral callback port instead of a fixed one, OpenRouter among them, cannot be
|
|
124
|
+
published ahead of the flow and are not covered.
|
|
125
|
+
|
|
126
|
+
## Terminal behaviour
|
|
127
|
+
|
|
128
|
+
The session is Pi's own TUI running inside the container, attached straight to your terminal: the
|
|
129
|
+
launch passes `-i`, adds `-t` when the host session has one, and inherits stdio. Nothing proxies or
|
|
130
|
+
re-renders frames, so rendering, keybindings, mouse and every extension's custom panel behave
|
|
131
|
+
exactly as they do unsandboxed.
|
|
132
|
+
|
|
133
|
+
Host integrations are the exception, because the process is not on your host:
|
|
134
|
+
|
|
135
|
+
| Feature | In a sandboxed session |
|
|
136
|
+
| --------------------- | ---------------------------------------------------------------------------- |
|
|
137
|
+
| External editor | Works. `nano` is installed and Pi falls back to it, editing the mounted file |
|
|
138
|
+
| Opening a browser | Not available; Pi prints the URL, which is how OAuth login proceeds |
|
|
139
|
+
| Clipboard integration | Not available; your terminal's own copy and paste still work |
|
|
140
|
+
| Desktop notifications | Not available |
|
|
141
|
+
|
|
142
|
+
A host `EDITOR` or `VISUAL` is deliberately not forwarded. Those commonly name a desktop
|
|
143
|
+
application, which would resolve to a binary the container does not have and would fail instead of
|
|
144
|
+
falling back to the editor that is there.
|
|
145
|
+
|
|
146
|
+
## Provider credential broker
|
|
147
|
+
|
|
148
|
+
The container never receives a provider API key. For each brokered provider the host holds a key
|
|
149
|
+
for, the launch:
|
|
150
|
+
|
|
151
|
+
1. Starts a broker on the host and grants the container exactly one route to it.
|
|
152
|
+
2. Replaces the credential variable with a random per-session token.
|
|
153
|
+
3. Redirects the provider's base URL at a loopback bridge inside the container, which forwards
|
|
154
|
+
raw bytes to the broker.
|
|
155
|
+
|
|
156
|
+
How the container reaches the broker depends on the engine:
|
|
157
|
+
|
|
158
|
+
| Host | Transport | Why |
|
|
159
|
+
| ----------------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
160
|
+
| Linux | Unix socket, bind-mounted, owner-only | Container and host share a kernel, so no port is needed |
|
|
161
|
+
| macOS, Windows, any VM engine | Loopback TCP through `host.docker.internal` | A container in a virtual machine cannot connect to a mounted host socket (ENOTSUP) |
|
|
162
|
+
|
|
163
|
+
On the TCP path the broker binds `127.0.0.1` on an ephemeral port, so it is never exposed beyond
|
|
164
|
+
the host, and the launch passes `--add-host` so the gateway name resolves on every engine. Another
|
|
165
|
+
local process could reach that port, and the session token is what stops it being useful.
|
|
166
|
+
|
|
167
|
+
The broker validates the token, swaps in the real key, and streams the provider response back.
|
|
168
|
+
Credentials for providers it cannot carry are withheld from the container rather than passed
|
|
169
|
+
through. Turn the whole mechanism off with `DOOMPI_SANDBOX_BROKER=0`.
|
|
170
|
+
|
|
171
|
+
## In-session command
|
|
172
|
+
|
|
173
|
+
```text
|
|
174
|
+
/doom-sandbox
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Reports whether the current session runs inside the sandbox container or directly on the host.
|
|
178
|
+
|
|
179
|
+
## Current limits
|
|
180
|
+
|
|
181
|
+
- The broker carries a curated provider list. OAuth subscription logins are not brokered, so they
|
|
182
|
+
are performed inside the sandbox (see below) and stored in that container's home volume.
|
|
183
|
+
- A host `*_BASE_URL` override is dropped rather than used as the broker's upstream.
|
|
184
|
+
- Compositions that declare local workspace packages cannot load their platform-specific
|
|
185
|
+
dependencies inside the Linux container; use registry-installed layers for sandboxed work.
|
|
186
|
+
- `dpi --sandbox` runs the harness rather than the synchronized fast path. That path loads Pi
|
|
187
|
+
in-process against synchronized settings, and a fresh container has none to load, so the
|
|
188
|
+
session is composed from the repository the way a first run is.
|
|
189
|
+
- Container network access follows the engine's defaults and is not restricted yet.
|
|
190
|
+
|
|
191
|
+
## Public API
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
import { DefaultSandboxExtensionService, activateSandboxExtension } from '@agimon-ai/doompi-sandbox';
|
|
195
|
+
import { launchSandbox } from '@agimon-ai/doompi-sandbox/sandbox-harness';
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The Pi host entry is also available at:
|
|
199
|
+
|
|
200
|
+
```text
|
|
201
|
+
@agimon-ai/doompi-sandbox/extensions/pi
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The service layer is host-neutral. The Pi entrypoint owns only command registration and its runtime-scoped installation guard, and the sandbox harness entry owns container provisioning for the DoomPi launcher.
|
|
205
|
+
|
|
206
|
+
## Development
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
pnpm build
|
|
210
|
+
pnpm typecheck
|
|
211
|
+
pnpm test
|
|
212
|
+
pnpm lint
|
|
213
|
+
pnpm exec vibe-lint check .
|
|
214
|
+
npm pack --dry-run
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Maintained by [Agimon](https://agimon.ai/about).
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,s)=>(s=n==null?{}:e(i(n)),o(r||!n||!n.__esModule||!a.call(n,`default`)?t(s,`default`,{value:n,enumerable:!0}):s,n));exports.__toESM=s;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../services/brokerRoutes.cjs"),n=require("./brokerServer.cjs");let r=require("node:crypto"),i=require("node:fs");i=e.__toESM(i,1);let a=require("node:os");a=e.__toESM(a,1);let o=require("node:path");o=e.__toESM(o,1);async function s(e){let i=t.resolveBrokeredCredentials(e.environment);if(i.length===0)return;let a=(0,r.randomBytes)(32).toString(`base64url`),o=new Map(i.map(e=>[e.route.provider,e])),s=n.createBrokerServer({credentials:o,token:a,onDenied:e.onDenied}),{endpoint:d,dispose:f}=(e.platform??process.platform)===`linux`&&e.forceLoopback!==!0?await c(s,e.socketDirectory):await l(s);return{endpoint:d,token:a,providers:i.map(e=>e.route.provider),withheldEnv:i.map(e=>e.envName),stop:()=>u(s,f)}}async function c(e,t){let n=t??i.default.mkdtempSync(o.default.join(a.default.tmpdir(),`doompi-broker-`));i.default.mkdirSync(n,{recursive:!0}),i.default.chmodSync(n,448);let r=o.default.join(n,`broker.sock`);return i.default.rmSync(r,{force:!0}),await new Promise((t,n)=>{e.once(`error`,n),e.listen(r,()=>t())}),{endpoint:{transport:`unix`,socketDirectory:n},dispose:()=>t?i.default.rmSync(r,{force:!0}):i.default.rmSync(n,{recursive:!0,force:!0})}}async function l(e){await new Promise((t,n)=>{e.once(`error`,n),e.listen(0,`127.0.0.1`,()=>t())});let t=e.address();if(t===null||typeof t==`string`)throw Error(`The broker did not report a listening port.`);return{endpoint:{transport:`tcp`,port:t.port},dispose:()=>void 0}}async function u(e,t){await new Promise(t=>{e.close(()=>t()),e.closeAllConnections()}),t()}exports.startBroker=s;
|
|
2
|
+
//# sourceMappingURL=brokerHost.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brokerHost.cjs","names":["resolveBrokeredCredentials","randomBytes","createBrokerServer","fs","path","os"],"sources":["../../src/adapters/brokerHost.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport fs from 'node:fs';\nimport type http from 'node:http';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { resolveBrokeredCredentials } from '../services/brokerRoutes.ts';\nimport type { BrokerEndpoint } from '../types/sandboxHarness.ts';\nimport { createBrokerServer } from './brokerServer.ts';\n\nconst SOCKET_DIRECTORY_PREFIX = 'doompi-broker-';\nconst SOCKET_FILE_NAME = 'broker.sock';\nconst OWNER_ONLY_DIRECTORY = 0o700;\nconst TOKEN_BYTES = 32;\nconst LOOPBACK = '127.0.0.1';\nconst LINUX_PLATFORM = 'linux';\nconst EPHEMERAL_PORT = 0;\n\nexport interface RunningBroker {\n /** How a container reaches this broker. */\n endpoint: BrokerEndpoint;\n /** Secret the container presents instead of any real credential. */\n token: string;\n /** Pi provider names the session may reach. */\n providers: string[];\n /** Host environment variables whose value the container receives as the token. */\n withheldEnv: string[];\n stop(): Promise<void>;\n}\n\nexport interface StartBrokerOptions {\n environment: Readonly<Record<string, string | undefined>>;\n onDenied?: (reason: string) => void;\n /** Seam for tests; defaults to this host's platform. */\n platform?: string;\n /**\n * Fixed directory to hold the socket instead of a fresh temporary one.\n *\n * A reused container keeps the mounts it was created with, so a dev\n * container has to find the socket at the same path on every launch.\n */\n socketDirectory?: string;\n /** Forces the loopback transport even where a socket would work. */\n forceLoopback?: boolean;\n}\n\n/**\n * Starts the host-side provider broker for one sandboxed session.\n *\n * Answers undefined when the host holds no brokerable credential, which keeps\n * a session that authenticates some other way on the unbrokered path instead\n * of failing it.\n */\nexport async function startBroker(options: StartBrokerOptions): Promise<RunningBroker | undefined> {\n const resolved = resolveBrokeredCredentials(options.environment);\n if (resolved.length === 0) return undefined;\n\n const token = randomBytes(TOKEN_BYTES).toString('base64url');\n const credentials = new Map(resolved.map((credential) => [credential.route.provider, credential]));\n const server = createBrokerServer({ credentials, token, onDenied: options.onDenied });\n const platform = options.platform ?? process.platform;\n\n const useSocket = platform === LINUX_PLATFORM && options.forceLoopback !== true;\n const { endpoint, dispose } = useSocket\n ? await listenOnSocket(server, options.socketDirectory)\n : await listenOnLoopback(server);\n\n return {\n endpoint,\n token,\n providers: resolved.map((credential) => credential.route.provider),\n withheldEnv: resolved.map((credential) => credential.envName),\n stop: () => stopBroker(server, dispose),\n };\n}\n\n/**\n * Native Linux shares a kernel with the container, so a bind-mounted socket\n * needs no port and is reachable as an ordinary file.\n */\nasync function listenOnSocket(\n server: http.Server,\n fixedDirectory?: string,\n): Promise<{ endpoint: BrokerEndpoint; dispose: () => void }> {\n const socketDirectory = fixedDirectory ?? fs.mkdtempSync(path.join(os.tmpdir(), SOCKET_DIRECTORY_PREFIX));\n fs.mkdirSync(socketDirectory, { recursive: true });\n fs.chmodSync(socketDirectory, OWNER_ONLY_DIRECTORY);\n const socketPath = path.join(socketDirectory, SOCKET_FILE_NAME);\n // A previous session that died without closing leaves the node behind, and\n // bind fails on an existing path.\n fs.rmSync(socketPath, { force: true });\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(socketPath, () => resolve());\n });\n return {\n endpoint: { transport: 'unix', socketDirectory },\n // A fixed directory outlives the session that borrowed it.\n dispose: () =>\n fixedDirectory\n ? fs.rmSync(socketPath, { force: true })\n : fs.rmSync(socketDirectory, { recursive: true, force: true }),\n };\n}\n\n/**\n * Everywhere else the container runs in its own virtual machine, which cannot\n * connect to a host unix socket even when the file is shared through: the\n * connect fails with ENOTSUP. Loopback keeps the broker off the network while\n * still being reachable through the engine's host gateway, and the session\n * token is what stops another local process from using it.\n */\nasync function listenOnLoopback(server: http.Server): Promise<{ endpoint: BrokerEndpoint; dispose: () => void }> {\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(EPHEMERAL_PORT, LOOPBACK, () => resolve());\n });\n const address = server.address();\n if (address === null || typeof address === 'string') throw new Error('The broker did not report a listening port.');\n return { endpoint: { transport: 'tcp', port: address.port }, dispose: () => undefined };\n}\n\nasync function stopBroker(server: http.Server, dispose: () => void): Promise<void> {\n await new Promise<void>((resolve) => {\n server.close(() => resolve());\n // An in-flight provider stream would otherwise hold the listener open past\n // the session that owns it.\n server.closeAllConnections();\n });\n dispose();\n}\n"],"mappings":"wRAoDA,eAAsB,EAAY,EAAiE,CACjG,IAAM,EAAWA,EAAAA,2BAA2B,EAAQ,WAAW,EAC/D,GAAI,EAAS,SAAW,EAAG,OAE3B,IAAM,GAAA,EAAQC,EAAAA,YAAAA,CAAY,EAAW,CAAC,CAAC,SAAS,WAAW,EACrD,EAAc,IAAI,IAAI,EAAS,IAAK,GAAe,CAAC,EAAW,MAAM,SAAU,CAAU,CAAC,CAAC,EAC3F,EAASC,EAAAA,mBAAmB,CAAE,cAAa,QAAO,SAAU,EAAQ,QAAS,CAAC,EAI9E,CAAE,WAAU,YAHD,EAAQ,UAAY,QAAQ,YAEd,SAAkB,EAAQ,gBAAkB,GAEvE,MAAM,EAAe,EAAQ,EAAQ,eAAe,EACpD,MAAM,EAAiB,CAAM,EAEjC,MAAO,CACL,WACA,QACA,UAAW,EAAS,IAAK,GAAe,EAAW,MAAM,QAAQ,EACjE,YAAa,EAAS,IAAK,GAAe,EAAW,OAAO,EAC5D,SAAY,EAAW,EAAQ,CAAO,CACxC,CACF,CAMA,eAAe,EACb,EACA,EAC4D,CAC5D,IAAM,EAAkB,GAAkBC,EAAAA,QAAG,YAAYC,EAAAA,QAAK,KAAKC,EAAAA,QAAG,OAAO,EAAG,gBAAuB,CAAC,EACxG,EAAA,QAAG,UAAU,EAAiB,CAAE,UAAW,EAAK,CAAC,EACjD,EAAA,QAAG,UAAU,EAAiB,GAAoB,EAClD,IAAM,EAAaD,EAAAA,QAAK,KAAK,EAAiB,aAAgB,EAQ9D,OALA,EAAA,QAAG,OAAO,EAAY,CAAE,MAAO,EAAK,CAAC,EACrC,MAAM,IAAI,SAAe,EAAS,IAAW,CAC3C,EAAO,KAAK,QAAS,CAAM,EAC3B,EAAO,OAAO,MAAkB,EAAQ,CAAC,CAC3C,CAAC,EACM,CACL,SAAU,CAAE,UAAW,OAAQ,iBAAgB,EAE/C,YACE,EACID,EAAAA,QAAG,OAAO,EAAY,CAAE,MAAO,EAAK,CAAC,EACrCA,EAAAA,QAAG,OAAO,EAAiB,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,CACnE,CACF,CASA,eAAe,EAAiB,EAAiF,CAC/G,MAAM,IAAI,SAAe,EAAS,IAAW,CAC3C,EAAO,KAAK,QAAS,CAAM,EAC3B,EAAO,OAAO,EAAgB,gBAAgB,EAAQ,CAAC,CACzD,CAAC,EACD,IAAM,EAAU,EAAO,QAAQ,EAC/B,GAAI,IAAY,MAAQ,OAAO,GAAY,SAAU,MAAU,MAAM,6CAA6C,EAClH,MAAO,CAAE,SAAU,CAAE,UAAW,MAAO,KAAM,EAAQ,IAAK,EAAG,YAAe,IAAA,EAAU,CACxF,CAEA,eAAe,EAAW,EAAqB,EAAoC,CACjF,MAAM,IAAI,QAAe,GAAY,CACnC,EAAO,UAAY,EAAQ,CAAC,EAG5B,EAAO,oBAAoB,CAC7B,CAAC,EACD,EAAQ,CACV"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BrokerEndpoint } from "../types/sandboxHarness.cjs";
|
|
2
|
+
//#region src/adapters/brokerHost.d.ts
|
|
3
|
+
interface RunningBroker {
|
|
4
|
+
/** How a container reaches this broker. */
|
|
5
|
+
endpoint: BrokerEndpoint;
|
|
6
|
+
/** Secret the container presents instead of any real credential. */
|
|
7
|
+
token: string;
|
|
8
|
+
/** Pi provider names the session may reach. */
|
|
9
|
+
providers: string[];
|
|
10
|
+
/** Host environment variables whose value the container receives as the token. */
|
|
11
|
+
withheldEnv: string[];
|
|
12
|
+
stop(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { RunningBroker };
|
|
16
|
+
//# sourceMappingURL=brokerHost.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brokerHost.d.cts","names":[],"sources":["../../src/adapters/brokerHost.ts"],"mappings":";;UAiBiB;;EAEf,UAAU;;EAEV;;EAEA;;EAEA;EACA,QAAQ"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BrokerEndpoint } from "../types/sandboxHarness.mjs";
|
|
2
|
+
//#region src/adapters/brokerHost.d.ts
|
|
3
|
+
interface RunningBroker {
|
|
4
|
+
/** How a container reaches this broker. */
|
|
5
|
+
endpoint: BrokerEndpoint;
|
|
6
|
+
/** Secret the container presents instead of any real credential. */
|
|
7
|
+
token: string;
|
|
8
|
+
/** Pi provider names the session may reach. */
|
|
9
|
+
providers: string[];
|
|
10
|
+
/** Host environment variables whose value the container receives as the token. */
|
|
11
|
+
withheldEnv: string[];
|
|
12
|
+
stop(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { RunningBroker };
|
|
16
|
+
//# sourceMappingURL=brokerHost.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brokerHost.d.mts","names":[],"sources":["../../src/adapters/brokerHost.ts"],"mappings":";;UAiBiB;;EAEf,UAAU;;EAEV;;EAEA;;EAEA;EACA,QAAQ"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{resolveBrokeredCredentials as e}from"../services/brokerRoutes.mjs";import{createBrokerServer as t}from"./brokerServer.mjs";import{randomBytes as n}from"node:crypto";import r from"node:fs";import i from"node:os";import a from"node:path";async function o(r){let i=e(r.environment);if(i.length===0)return;let a=n(32).toString(`base64url`),o=new Map(i.map(e=>[e.route.provider,e])),u=t({credentials:o,token:a,onDenied:r.onDenied}),{endpoint:d,dispose:f}=(r.platform??process.platform)===`linux`&&r.forceLoopback!==!0?await s(u,r.socketDirectory):await c(u);return{endpoint:d,token:a,providers:i.map(e=>e.route.provider),withheldEnv:i.map(e=>e.envName),stop:()=>l(u,f)}}async function s(e,t){let n=t??r.mkdtempSync(a.join(i.tmpdir(),`doompi-broker-`));r.mkdirSync(n,{recursive:!0}),r.chmodSync(n,448);let o=a.join(n,`broker.sock`);return r.rmSync(o,{force:!0}),await new Promise((t,n)=>{e.once(`error`,n),e.listen(o,()=>t())}),{endpoint:{transport:`unix`,socketDirectory:n},dispose:()=>t?r.rmSync(o,{force:!0}):r.rmSync(n,{recursive:!0,force:!0})}}async function c(e){await new Promise((t,n)=>{e.once(`error`,n),e.listen(0,`127.0.0.1`,()=>t())});let t=e.address();if(t===null||typeof t==`string`)throw Error(`The broker did not report a listening port.`);return{endpoint:{transport:`tcp`,port:t.port},dispose:()=>void 0}}async function l(e,t){await new Promise(t=>{e.close(()=>t()),e.closeAllConnections()}),t()}export{o as startBroker};
|
|
2
|
+
//# sourceMappingURL=brokerHost.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brokerHost.mjs","names":[],"sources":["../../src/adapters/brokerHost.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport fs from 'node:fs';\nimport type http from 'node:http';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { resolveBrokeredCredentials } from '../services/brokerRoutes.ts';\nimport type { BrokerEndpoint } from '../types/sandboxHarness.ts';\nimport { createBrokerServer } from './brokerServer.ts';\n\nconst SOCKET_DIRECTORY_PREFIX = 'doompi-broker-';\nconst SOCKET_FILE_NAME = 'broker.sock';\nconst OWNER_ONLY_DIRECTORY = 0o700;\nconst TOKEN_BYTES = 32;\nconst LOOPBACK = '127.0.0.1';\nconst LINUX_PLATFORM = 'linux';\nconst EPHEMERAL_PORT = 0;\n\nexport interface RunningBroker {\n /** How a container reaches this broker. */\n endpoint: BrokerEndpoint;\n /** Secret the container presents instead of any real credential. */\n token: string;\n /** Pi provider names the session may reach. */\n providers: string[];\n /** Host environment variables whose value the container receives as the token. */\n withheldEnv: string[];\n stop(): Promise<void>;\n}\n\nexport interface StartBrokerOptions {\n environment: Readonly<Record<string, string | undefined>>;\n onDenied?: (reason: string) => void;\n /** Seam for tests; defaults to this host's platform. */\n platform?: string;\n /**\n * Fixed directory to hold the socket instead of a fresh temporary one.\n *\n * A reused container keeps the mounts it was created with, so a dev\n * container has to find the socket at the same path on every launch.\n */\n socketDirectory?: string;\n /** Forces the loopback transport even where a socket would work. */\n forceLoopback?: boolean;\n}\n\n/**\n * Starts the host-side provider broker for one sandboxed session.\n *\n * Answers undefined when the host holds no brokerable credential, which keeps\n * a session that authenticates some other way on the unbrokered path instead\n * of failing it.\n */\nexport async function startBroker(options: StartBrokerOptions): Promise<RunningBroker | undefined> {\n const resolved = resolveBrokeredCredentials(options.environment);\n if (resolved.length === 0) return undefined;\n\n const token = randomBytes(TOKEN_BYTES).toString('base64url');\n const credentials = new Map(resolved.map((credential) => [credential.route.provider, credential]));\n const server = createBrokerServer({ credentials, token, onDenied: options.onDenied });\n const platform = options.platform ?? process.platform;\n\n const useSocket = platform === LINUX_PLATFORM && options.forceLoopback !== true;\n const { endpoint, dispose } = useSocket\n ? await listenOnSocket(server, options.socketDirectory)\n : await listenOnLoopback(server);\n\n return {\n endpoint,\n token,\n providers: resolved.map((credential) => credential.route.provider),\n withheldEnv: resolved.map((credential) => credential.envName),\n stop: () => stopBroker(server, dispose),\n };\n}\n\n/**\n * Native Linux shares a kernel with the container, so a bind-mounted socket\n * needs no port and is reachable as an ordinary file.\n */\nasync function listenOnSocket(\n server: http.Server,\n fixedDirectory?: string,\n): Promise<{ endpoint: BrokerEndpoint; dispose: () => void }> {\n const socketDirectory = fixedDirectory ?? fs.mkdtempSync(path.join(os.tmpdir(), SOCKET_DIRECTORY_PREFIX));\n fs.mkdirSync(socketDirectory, { recursive: true });\n fs.chmodSync(socketDirectory, OWNER_ONLY_DIRECTORY);\n const socketPath = path.join(socketDirectory, SOCKET_FILE_NAME);\n // A previous session that died without closing leaves the node behind, and\n // bind fails on an existing path.\n fs.rmSync(socketPath, { force: true });\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(socketPath, () => resolve());\n });\n return {\n endpoint: { transport: 'unix', socketDirectory },\n // A fixed directory outlives the session that borrowed it.\n dispose: () =>\n fixedDirectory\n ? fs.rmSync(socketPath, { force: true })\n : fs.rmSync(socketDirectory, { recursive: true, force: true }),\n };\n}\n\n/**\n * Everywhere else the container runs in its own virtual machine, which cannot\n * connect to a host unix socket even when the file is shared through: the\n * connect fails with ENOTSUP. Loopback keeps the broker off the network while\n * still being reachable through the engine's host gateway, and the session\n * token is what stops another local process from using it.\n */\nasync function listenOnLoopback(server: http.Server): Promise<{ endpoint: BrokerEndpoint; dispose: () => void }> {\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(EPHEMERAL_PORT, LOOPBACK, () => resolve());\n });\n const address = server.address();\n if (address === null || typeof address === 'string') throw new Error('The broker did not report a listening port.');\n return { endpoint: { transport: 'tcp', port: address.port }, dispose: () => undefined };\n}\n\nasync function stopBroker(server: http.Server, dispose: () => void): Promise<void> {\n await new Promise<void>((resolve) => {\n server.close(() => resolve());\n // An in-flight provider stream would otherwise hold the listener open past\n // the session that owns it.\n server.closeAllConnections();\n });\n dispose();\n}\n"],"mappings":"mPAoDA,eAAsB,EAAY,EAAiE,CACjG,IAAM,EAAW,EAA2B,EAAQ,WAAW,EAC/D,GAAI,EAAS,SAAW,EAAG,OAE3B,IAAM,EAAQ,EAAY,EAAW,CAAC,CAAC,SAAS,WAAW,EACrD,EAAc,IAAI,IAAI,EAAS,IAAK,GAAe,CAAC,EAAW,MAAM,SAAU,CAAU,CAAC,CAAC,EAC3F,EAAS,EAAmB,CAAE,cAAa,QAAO,SAAU,EAAQ,QAAS,CAAC,EAI9E,CAAE,WAAU,YAHD,EAAQ,UAAY,QAAQ,YAEd,SAAkB,EAAQ,gBAAkB,GAEvE,MAAM,EAAe,EAAQ,EAAQ,eAAe,EACpD,MAAM,EAAiB,CAAM,EAEjC,MAAO,CACL,WACA,QACA,UAAW,EAAS,IAAK,GAAe,EAAW,MAAM,QAAQ,EACjE,YAAa,EAAS,IAAK,GAAe,EAAW,OAAO,EAC5D,SAAY,EAAW,EAAQ,CAAO,CACxC,CACF,CAMA,eAAe,EACb,EACA,EAC4D,CAC5D,IAAM,EAAkB,GAAkB,EAAG,YAAY,EAAK,KAAK,EAAG,OAAO,EAAG,gBAAuB,CAAC,EACxG,EAAG,UAAU,EAAiB,CAAE,UAAW,EAAK,CAAC,EACjD,EAAG,UAAU,EAAiB,GAAoB,EAClD,IAAM,EAAa,EAAK,KAAK,EAAiB,aAAgB,EAQ9D,OALA,EAAG,OAAO,EAAY,CAAE,MAAO,EAAK,CAAC,EACrC,MAAM,IAAI,SAAe,EAAS,IAAW,CAC3C,EAAO,KAAK,QAAS,CAAM,EAC3B,EAAO,OAAO,MAAkB,EAAQ,CAAC,CAC3C,CAAC,EACM,CACL,SAAU,CAAE,UAAW,OAAQ,iBAAgB,EAE/C,YACE,EACI,EAAG,OAAO,EAAY,CAAE,MAAO,EAAK,CAAC,EACrC,EAAG,OAAO,EAAiB,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,CACnE,CACF,CASA,eAAe,EAAiB,EAAiF,CAC/G,MAAM,IAAI,SAAe,EAAS,IAAW,CAC3C,EAAO,KAAK,QAAS,CAAM,EAC3B,EAAO,OAAO,EAAgB,gBAAgB,EAAQ,CAAC,CACzD,CAAC,EACD,IAAM,EAAU,EAAO,QAAQ,EAC/B,GAAI,IAAY,MAAQ,OAAO,GAAY,SAAU,MAAU,MAAM,6CAA6C,EAClH,MAAO,CAAE,SAAU,CAAE,UAAW,MAAO,KAAM,EAAQ,IAAK,EAAG,YAAe,IAAA,EAAU,CACxF,CAEA,eAAe,EAAW,EAAqB,EAAoC,CACjF,MAAM,IAAI,QAAe,GAAY,CACnC,EAAO,UAAY,EAAQ,CAAC,EAG5B,EAAO,oBAAoB,CAC7B,CAAC,EACD,EAAQ,CACV"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../_virtual/_rolldown/runtime.cjs");let t=require("node:crypto"),n=require("node:http");n=e.__toESM(n,1);let r=require("node:https");r=e.__toESM(r,1);const i=[`x-api-key`,`authorization`,`x-goog-api-key`],a=`bearer `,o=new Set([`connection`,`keep-alive`,`proxy-authenticate`,`proxy-authorization`,`te`,`trailer`,`transfer-encoding`,`upgrade`,`host`,`content-length`]),s=/^\/([^/?]+)(.*)$/;function c(e,n){let r=Buffer.from(e),i=Buffer.from(n);return r.length===i.length&&(0,t.timingSafeEqual)(r,i)}function l(e){return e.toLowerCase().startsWith(a)?e.slice(7):e}function u(e,t){return e.toLowerCase().startsWith(a)?`Bearer ${t}`:t}function d(e,t,n,r){let a={},s=!1;for(let[t,d]of Object.entries(e)){if(d===void 0||o.has(t))continue;let e=Array.isArray(d)?d[0]:d;if(i.includes(t)&&e!==void 0){if(!c(l(e),n))return;a[t]=u(e,r),s=!0;continue}a[t]=d}let d=t.get(`key`);if(d!==null){if(!c(d,n))return;t.set(`key`,r),s=!0}return s?a:void 0}function f(e,t,n){e.writeHead(t,{"content-type":`application/json`}),e.end(JSON.stringify({error:{type:`doompi_broker`,message:n}}))}function p(e){let t=e.requestUpstream??r.default.request;return n.default.createServer((n,r)=>{let i=s.exec(n.url??``),a=i?.[1],o=a?e.credentials.get(a):void 0;if(!i||!o){e.onDenied?.(`unroutable provider path ${n.url??``}`),f(r,404,`Unknown provider for this sandbox session.`);return}let c=new URL(`${o.route.upstream}${i[2]||``}`),l=d(n.headers,c.searchParams,e.token,o.value);if(!l){e.onDenied?.(`missing or invalid session token for ${o.route.provider}`),f(r,401,`This sandbox session did not present its broker token.`);return}let u=t(c,{method:n.method,headers:{...l,host:c.host}},e=>{r.writeHead(e.statusCode??502,e.headers),e.pipe(r)});u.on(`error`,t=>{e.onDenied?.(`upstream ${o.route.provider} failed: ${t.message}`),r.headersSent?r.destroy():f(r,502,`The provider could not be reached from the host.`)}),n.on(`aborted`,()=>u.destroy()),n.pipe(u)})}exports.createBrokerServer=p;
|
|
2
|
+
//# sourceMappingURL=brokerServer.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brokerServer.cjs","names":["timingSafeEqual","https","http"],"sources":["../../src/adapters/brokerServer.ts"],"sourcesContent":["import { timingSafeEqual } from 'node:crypto';\nimport http from 'node:http';\nimport https from 'node:https';\nimport type { ResolvedCredential } from '../services/brokerRoutes.ts';\n\n/** Headers a provider SDK may carry its credential in. */\nconst CREDENTIAL_HEADERS = ['x-api-key', 'authorization', 'x-goog-api-key'] as const;\n/** Query parameter some Google clients use instead of a header. */\nconst CREDENTIAL_QUERY = 'key';\nconst BEARER_PREFIX = 'bearer ';\n/** Connection-scoped headers that must not cross to the upstream request. */\nconst HOP_BY_HOP = new Set([\n 'connection',\n 'keep-alive',\n 'proxy-authenticate',\n 'proxy-authorization',\n 'te',\n 'trailer',\n 'transfer-encoding',\n 'upgrade',\n 'host',\n 'content-length',\n]);\nconst PATH_PATTERN = /^\\/([^/?]+)(.*)$/;\n\nexport interface BrokerServerOptions {\n /** Providers this session may reach, keyed by Pi provider name. */\n credentials: ReadonlyMap<string, ResolvedCredential>;\n /** Secret the container presents in place of a real credential. */\n token: string;\n /** Receives one line per rejected request, for host-side visibility. */\n onDenied?: (reason: string) => void;\n /** Seam for tests; defaults to the real https transport. */\n requestUpstream?: typeof https.request;\n}\n\nfunction matchesToken(candidate: string, token: string): boolean {\n const left = Buffer.from(candidate);\n const right = Buffer.from(token);\n return left.length === right.length && timingSafeEqual(left, right);\n}\n\nfunction credentialValue(headerValue: string): string {\n return headerValue.toLowerCase().startsWith(BEARER_PREFIX) ? headerValue.slice(BEARER_PREFIX.length) : headerValue;\n}\n\nfunction withCredential(headerValue: string, realKey: string): string {\n return headerValue.toLowerCase().startsWith(BEARER_PREFIX) ? `Bearer ${realKey}` : realKey;\n}\n\n/**\n * Rewrites the request's credential, proving the caller holds the session token.\n *\n * Which header carries the key differs per provider SDK, so every candidate is\n * checked rather than assumed. Returns undefined when nothing presented the\n * token, which is what makes an unauthenticated caller indistinguishable from a\n * misrouted one.\n */\nfunction swapCredential(\n headers: http.IncomingHttpHeaders,\n search: URLSearchParams,\n token: string,\n realKey: string,\n): http.OutgoingHttpHeaders | undefined {\n const forwarded: http.OutgoingHttpHeaders = {};\n let authenticated = false;\n\n for (const [name, value] of Object.entries(headers)) {\n if (value === undefined || HOP_BY_HOP.has(name)) continue;\n const single = Array.isArray(value) ? value[0] : value;\n if ((CREDENTIAL_HEADERS as readonly string[]).includes(name) && single !== undefined) {\n if (!matchesToken(credentialValue(single), token)) return undefined;\n forwarded[name] = withCredential(single, realKey);\n authenticated = true;\n continue;\n }\n forwarded[name] = value;\n }\n\n const queryKey = search.get(CREDENTIAL_QUERY);\n if (queryKey !== null) {\n if (!matchesToken(queryKey, token)) return undefined;\n search.set(CREDENTIAL_QUERY, realKey);\n authenticated = true;\n }\n\n return authenticated ? forwarded : undefined;\n}\n\nfunction reject(response: http.ServerResponse, status: number, message: string): void {\n response.writeHead(status, { 'content-type': 'application/json' });\n response.end(JSON.stringify({ error: { type: 'doompi_broker', message } }));\n}\n\n/**\n * Terminates provider calls from a sandboxed session on the host.\n *\n * The container never holds a real credential: it presents the session token,\n * and only a request that proves possession of it is forwarded upstream with\n * the host's key attached. Bodies stream in both directions so token-by-token\n * responses are not buffered.\n */\nexport function createBrokerServer(options: BrokerServerOptions): http.Server {\n const requestUpstream = options.requestUpstream ?? https.request;\n\n return http.createServer((request, response) => {\n const match = PATH_PATTERN.exec(request.url ?? '');\n const provider = match?.[1];\n const credential = provider ? options.credentials.get(provider) : undefined;\n if (!match || !credential) {\n options.onDenied?.(`unroutable provider path ${request.url ?? ''}`);\n reject(response, 404, 'Unknown provider for this sandbox session.');\n return;\n }\n\n const target = new URL(`${credential.route.upstream}${match[2] || ''}`);\n const headers = swapCredential(request.headers, target.searchParams, options.token, credential.value);\n if (!headers) {\n options.onDenied?.(`missing or invalid session token for ${credential.route.provider}`);\n reject(response, 401, 'This sandbox session did not present its broker token.');\n return;\n }\n\n const upstream = requestUpstream(\n target,\n { method: request.method, headers: { ...headers, host: target.host } },\n (upstreamResponse) => {\n response.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);\n upstreamResponse.pipe(response);\n },\n );\n upstream.on('error', (error: Error) => {\n options.onDenied?.(`upstream ${credential.route.provider} failed: ${error.message}`);\n if (!response.headersSent) reject(response, 502, 'The provider could not be reached from the host.');\n else response.destroy();\n });\n request.on('aborted', () => upstream.destroy());\n request.pipe(upstream);\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{timingSafeEqual as e}from"node:crypto";import t from"node:http";import n from"node:https";const r=[`x-api-key`,`authorization`,`x-goog-api-key`],i=`bearer `,a=new Set([`connection`,`keep-alive`,`proxy-authenticate`,`proxy-authorization`,`te`,`trailer`,`transfer-encoding`,`upgrade`,`host`,`content-length`]),o=/^\/([^/?]+)(.*)$/;function s(t,n){let r=Buffer.from(t),i=Buffer.from(n);return r.length===i.length&&e(r,i)}function c(e){return e.toLowerCase().startsWith(i)?e.slice(7):e}function l(e,t){return e.toLowerCase().startsWith(i)?`Bearer ${t}`:t}function u(e,t,n,i){let o={},u=!1;for(let[t,d]of Object.entries(e)){if(d===void 0||a.has(t))continue;let e=Array.isArray(d)?d[0]:d;if(r.includes(t)&&e!==void 0){if(!s(c(e),n))return;o[t]=l(e,i),u=!0;continue}o[t]=d}let d=t.get(`key`);if(d!==null){if(!s(d,n))return;t.set(`key`,i),u=!0}return u?o:void 0}function d(e,t,n){e.writeHead(t,{"content-type":`application/json`}),e.end(JSON.stringify({error:{type:`doompi_broker`,message:n}}))}function f(e){let r=e.requestUpstream??n.request;return t.createServer((t,n)=>{let i=o.exec(t.url??``),a=i?.[1],s=a?e.credentials.get(a):void 0;if(!i||!s){e.onDenied?.(`unroutable provider path ${t.url??``}`),d(n,404,`Unknown provider for this sandbox session.`);return}let c=new URL(`${s.route.upstream}${i[2]||``}`),l=u(t.headers,c.searchParams,e.token,s.value);if(!l){e.onDenied?.(`missing or invalid session token for ${s.route.provider}`),d(n,401,`This sandbox session did not present its broker token.`);return}let f=r(c,{method:t.method,headers:{...l,host:c.host}},e=>{n.writeHead(e.statusCode??502,e.headers),e.pipe(n)});f.on(`error`,t=>{e.onDenied?.(`upstream ${s.route.provider} failed: ${t.message}`),n.headersSent?n.destroy():d(n,502,`The provider could not be reached from the host.`)}),t.on(`aborted`,()=>f.destroy()),t.pipe(f)})}export{f as createBrokerServer};
|
|
2
|
+
//# sourceMappingURL=brokerServer.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brokerServer.mjs","names":[],"sources":["../../src/adapters/brokerServer.ts"],"sourcesContent":["import { timingSafeEqual } from 'node:crypto';\nimport http from 'node:http';\nimport https from 'node:https';\nimport type { ResolvedCredential } from '../services/brokerRoutes.ts';\n\n/** Headers a provider SDK may carry its credential in. */\nconst CREDENTIAL_HEADERS = ['x-api-key', 'authorization', 'x-goog-api-key'] as const;\n/** Query parameter some Google clients use instead of a header. */\nconst CREDENTIAL_QUERY = 'key';\nconst BEARER_PREFIX = 'bearer ';\n/** Connection-scoped headers that must not cross to the upstream request. */\nconst HOP_BY_HOP = new Set([\n 'connection',\n 'keep-alive',\n 'proxy-authenticate',\n 'proxy-authorization',\n 'te',\n 'trailer',\n 'transfer-encoding',\n 'upgrade',\n 'host',\n 'content-length',\n]);\nconst PATH_PATTERN = /^\\/([^/?]+)(.*)$/;\n\nexport interface BrokerServerOptions {\n /** Providers this session may reach, keyed by Pi provider name. */\n credentials: ReadonlyMap<string, ResolvedCredential>;\n /** Secret the container presents in place of a real credential. */\n token: string;\n /** Receives one line per rejected request, for host-side visibility. */\n onDenied?: (reason: string) => void;\n /** Seam for tests; defaults to the real https transport. */\n requestUpstream?: typeof https.request;\n}\n\nfunction matchesToken(candidate: string, token: string): boolean {\n const left = Buffer.from(candidate);\n const right = Buffer.from(token);\n return left.length === right.length && timingSafeEqual(left, right);\n}\n\nfunction credentialValue(headerValue: string): string {\n return headerValue.toLowerCase().startsWith(BEARER_PREFIX) ? headerValue.slice(BEARER_PREFIX.length) : headerValue;\n}\n\nfunction withCredential(headerValue: string, realKey: string): string {\n return headerValue.toLowerCase().startsWith(BEARER_PREFIX) ? `Bearer ${realKey}` : realKey;\n}\n\n/**\n * Rewrites the request's credential, proving the caller holds the session token.\n *\n * Which header carries the key differs per provider SDK, so every candidate is\n * checked rather than assumed. Returns undefined when nothing presented the\n * token, which is what makes an unauthenticated caller indistinguishable from a\n * misrouted one.\n */\nfunction swapCredential(\n headers: http.IncomingHttpHeaders,\n search: URLSearchParams,\n token: string,\n realKey: string,\n): http.OutgoingHttpHeaders | undefined {\n const forwarded: http.OutgoingHttpHeaders = {};\n let authenticated = false;\n\n for (const [name, value] of Object.entries(headers)) {\n if (value === undefined || HOP_BY_HOP.has(name)) continue;\n const single = Array.isArray(value) ? value[0] : value;\n if ((CREDENTIAL_HEADERS as readonly string[]).includes(name) && single !== undefined) {\n if (!matchesToken(credentialValue(single), token)) return undefined;\n forwarded[name] = withCredential(single, realKey);\n authenticated = true;\n continue;\n }\n forwarded[name] = value;\n }\n\n const queryKey = search.get(CREDENTIAL_QUERY);\n if (queryKey !== null) {\n if (!matchesToken(queryKey, token)) return undefined;\n search.set(CREDENTIAL_QUERY, realKey);\n authenticated = true;\n }\n\n return authenticated ? forwarded : undefined;\n}\n\nfunction reject(response: http.ServerResponse, status: number, message: string): void {\n response.writeHead(status, { 'content-type': 'application/json' });\n response.end(JSON.stringify({ error: { type: 'doompi_broker', message } }));\n}\n\n/**\n * Terminates provider calls from a sandboxed session on the host.\n *\n * The container never holds a real credential: it presents the session token,\n * and only a request that proves possession of it is forwarded upstream with\n * the host's key attached. Bodies stream in both directions so token-by-token\n * responses are not buffered.\n */\nexport function createBrokerServer(options: BrokerServerOptions): http.Server {\n const requestUpstream = options.requestUpstream ?? https.request;\n\n return http.createServer((request, response) => {\n const match = PATH_PATTERN.exec(request.url ?? '');\n const provider = match?.[1];\n const credential = provider ? options.credentials.get(provider) : undefined;\n if (!match || !credential) {\n options.onDenied?.(`unroutable provider path ${request.url ?? ''}`);\n reject(response, 404, 'Unknown provider for this sandbox session.');\n return;\n }\n\n const target = new URL(`${credential.route.upstream}${match[2] || ''}`);\n const headers = swapCredential(request.headers, target.searchParams, options.token, credential.value);\n if (!headers) {\n options.onDenied?.(`missing or invalid session token for ${credential.route.provider}`);\n reject(response, 401, 'This sandbox session did not present its broker token.');\n return;\n }\n\n const upstream = requestUpstream(\n target,\n { method: request.method, headers: { ...headers, host: target.host } },\n (upstreamResponse) => {\n response.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);\n upstreamResponse.pipe(response);\n },\n );\n upstream.on('error', (error: Error) => {\n options.onDenied?.(`upstream ${credential.route.provider} failed: ${error.message}`);\n if (!response.headersSent) reject(response, 502, 'The provider could not be reached from the host.');\n else response.destroy();\n });\n request.on('aborted', () => upstream.destroy());\n request.pipe(upstream);\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../services/devcontainer.cjs");let n=require("node:fs");n=e.__toESM(n,1);let r=require("node:path");r=e.__toESM(r,1);const i=`devcontainer`;function a(e){for(let i of t.DEVCONTAINER_CONFIG_PATHS){let t=r.default.join(e,i);if(n.default.existsSync(t))return t}}async function o(e){return(await e.capture(i,[`--version`]))?.exitCode===0?{command:i,prefix:[]}:{command:`npx`,prefix:[`-y`,t.DEVCONTAINER_CLI_PACKAGE]}}async function s(e){let n=await o(e.runner);e.onProgress?.(`starting the workspace dev container`);let r=t.devcontainerUpArgs({repoRoot:e.repoRoot,socketDirectory:e.socketDirectory,socketTarget:e.socketDirectory?`/run/doompi`:void 0}),i=await e.runner.capture(n.command,[...n.prefix,...r]);if(!i)throw Error(`The Dev Containers CLI could not be started. Install @devcontainers/cli, or make npx available.`);let{containerId:a,remoteWorkspaceFolder:s,error:c}=t.parseDevcontainerUp(i.stdout);if(!a)throw Error(`The dev container did not start: ${c??`exit ${i.exitCode}`}`);let l=await e.runner.run(e.engine,[`exec`,a,...t.bootstrapCommand(e.version)]);if(l!==0)throw Error(`DoomPi could not be installed in the dev container (exit ${l}). Add it to the dev container, or unset the dev container preference to use the built-in image.`);return e.onProgress?.(`attaching to dev container ${a.slice(0,12)}`),e.runner.run(e.engine,t.devcontainerExecArgs({containerId:a,cwd:s?t.containerWorkspacePath(e.repoRoot,e.cwd,s):e.cwd,environment:e.environment,hasTty:e.hasTty,command:[`doompi`,...s?t.mapForwardArgs(e.forwardArgs,e.repoRoot,s):e.forwardArgs]}))}exports.findDevcontainerConfig=a,exports.resolveDevcontainerCli=o,exports.runDevcontainerSession=s;
|
|
2
|
+
//# sourceMappingURL=devcontainer.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devcontainer.cjs","names":["DEVCONTAINER_CONFIG_PATHS","path","fs","DEVCONTAINER_CLI_PACKAGE","devcontainerUpArgs","parseDevcontainerUp","bootstrapCommand","devcontainerExecArgs","containerWorkspacePath","mapForwardArgs"],"sources":["../../src/adapters/devcontainer.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport {\n bootstrapCommand,\n containerWorkspacePath,\n mapForwardArgs,\n DEVCONTAINER_CLI_PACKAGE,\n DEVCONTAINER_CONFIG_PATHS,\n devcontainerExecArgs,\n devcontainerUpArgs,\n parseDevcontainerUp,\n} from '../services/devcontainer.ts';\nimport type { EngineProcessRunner, SandboxEngine } from '../types/sandboxHarness.ts';\n\nconst DEVCONTAINER_BINARY = 'devcontainer';\nconst NPX_BINARY = 'npx';\nconst LAUNCHER_BINARY = 'doompi';\nconst BROKER_MOUNT_DIRECTORY = '/run/doompi';\n\n/** Locates the workspace's own dev container configuration, if it has one. */\nexport function findDevcontainerConfig(repoRoot: string): string | undefined {\n for (const relative of DEVCONTAINER_CONFIG_PATHS) {\n const candidate = path.join(repoRoot, relative);\n if (fs.existsSync(candidate)) return candidate;\n }\n return undefined;\n}\n\n/**\n * Resolves how to invoke the Dev Containers CLI.\n *\n * A local install is preferred so a launch does not depend on the network;\n * npx is the fallback, pinned so the tool cannot change underneath a session.\n */\nexport async function resolveDevcontainerCli(\n runner: EngineProcessRunner,\n): Promise<{ command: string; prefix: string[] }> {\n const probe = await runner.capture(DEVCONTAINER_BINARY, ['--version']);\n if (probe?.exitCode === 0) return { command: DEVCONTAINER_BINARY, prefix: [] };\n return { command: NPX_BINARY, prefix: ['-y', DEVCONTAINER_CLI_PACKAGE] };\n}\n\nexport interface DevcontainerSessionOptions {\n repoRoot: string;\n cwd: string;\n forwardArgs: readonly string[];\n environment: Readonly<Record<string, string>>;\n engine: SandboxEngine;\n runner: EngineProcessRunner;\n version: string;\n hasTty: boolean;\n /** Host directory holding the broker socket, when the broker uses one. */\n socketDirectory?: string;\n onProgress?: (message: string) => void;\n}\n\n/**\n * Runs the session inside the workspace's own dev container.\n *\n * The container is brought up by the Dev Containers CLI, so the file decides\n * the image, features, mounts and lifecycle hooks: the session gets the\n * project's real toolchain, and this layer's isolation guarantees do not\n * apply beyond the environment it passes in.\n */\nexport async function runDevcontainerSession(options: DevcontainerSessionOptions): Promise<number> {\n const cli = await resolveDevcontainerCli(options.runner);\n options.onProgress?.('starting the workspace dev container');\n\n const upArgs = devcontainerUpArgs({\n repoRoot: options.repoRoot,\n socketDirectory: options.socketDirectory,\n socketTarget: options.socketDirectory ? BROKER_MOUNT_DIRECTORY : undefined,\n });\n const up = await options.runner.capture(cli.command, [...cli.prefix, ...upArgs]);\n if (!up) {\n throw new Error('The Dev Containers CLI could not be started. Install @devcontainers/cli, or make npx available.');\n }\n const { containerId, remoteWorkspaceFolder, error } = parseDevcontainerUp(up.stdout);\n if (!containerId) throw new Error(`The dev container did not start: ${error ?? `exit ${up.exitCode}`}`);\n\n const bootstrap = await options.runner.run(options.engine, [\n 'exec',\n containerId,\n ...bootstrapCommand(options.version),\n ]);\n if (bootstrap !== 0) {\n throw new Error(\n `DoomPi could not be installed in the dev container (exit ${bootstrap}). ` +\n 'Add it to the dev container, or unset the dev container preference to use the built-in image.',\n );\n }\n\n options.onProgress?.(`attaching to dev container ${containerId.slice(0, 12)}`);\n return options.runner.run(\n options.engine,\n devcontainerExecArgs({\n containerId,\n cwd: remoteWorkspaceFolder\n ? containerWorkspacePath(options.repoRoot, options.cwd, remoteWorkspaceFolder)\n : options.cwd,\n environment: options.environment,\n hasTty: options.hasTty,\n command: [\n LAUNCHER_BINARY,\n ...(remoteWorkspaceFolder\n ? mapForwardArgs(options.forwardArgs, options.repoRoot, remoteWorkspaceFolder)\n : options.forwardArgs),\n ],\n }),\n );\n}\n"],"mappings":"qLAcA,MAAM,EAAsB,eAM5B,SAAgB,EAAuB,EAAsC,CAC3E,IAAK,IAAM,KAAYA,EAAAA,0BAA2B,CAChD,IAAM,EAAYC,EAAAA,QAAK,KAAK,EAAU,CAAQ,EAC9C,GAAIC,EAAAA,QAAG,WAAW,CAAS,EAAG,OAAO,CACvC,CAEF,CAQA,eAAsB,EACpB,EACgD,CAGhD,OADI,MADgB,EAAO,QAAQ,EAAqB,CAAC,WAAW,CAAC,EAAA,EAC1D,WAAa,EAAU,CAAE,QAAS,EAAqB,OAAQ,CAAC,CAAE,EACtE,CAAE,QAAS,MAAY,OAAQ,CAAC,KAAMC,EAAAA,wBAAwB,CAAE,CACzE,CAwBA,eAAsB,EAAuB,EAAsD,CACjG,IAAM,EAAM,MAAM,EAAuB,EAAQ,MAAM,EACvD,EAAQ,aAAa,sCAAsC,EAE3D,IAAM,EAASC,EAAAA,mBAAmB,CAChC,SAAU,EAAQ,SAClB,gBAAiB,EAAQ,gBACzB,aAAc,EAAQ,gBAAkB,cAAyB,IAAA,EACnE,CAAC,EACK,EAAK,MAAM,EAAQ,OAAO,QAAQ,EAAI,QAAS,CAAC,GAAG,EAAI,OAAQ,GAAG,CAAM,CAAC,EAC/E,GAAI,CAAC,EACH,MAAU,MAAM,iGAAiG,EAEnH,GAAM,CAAE,cAAa,wBAAuB,SAAUC,EAAAA,oBAAoB,EAAG,MAAM,EACnF,GAAI,CAAC,EAAa,MAAU,MAAM,oCAAoC,GAAS,QAAQ,EAAG,YAAY,EAEtG,IAAM,EAAY,MAAM,EAAQ,OAAO,IAAI,EAAQ,OAAQ,CACzD,OACA,EACA,GAAGC,EAAAA,iBAAiB,EAAQ,OAAO,CACrC,CAAC,EACD,GAAI,IAAc,EAChB,MAAU,MACR,4DAA4D,EAAU,iGAExE,EAIF,OADA,EAAQ,aAAa,8BAA8B,EAAY,MAAM,EAAG,EAAE,GAAG,EACtE,EAAQ,OAAO,IACpB,EAAQ,OACRC,EAAAA,qBAAqB,CACnB,cACA,IAAK,EACDC,EAAAA,uBAAuB,EAAQ,SAAU,EAAQ,IAAK,CAAqB,EAC3E,EAAQ,IACZ,YAAa,EAAQ,YACrB,OAAQ,EAAQ,OAChB,QAAS,CACP,SACA,GAAI,EACAC,EAAAA,eAAe,EAAQ,YAAa,EAAQ,SAAU,CAAqB,EAC3E,EAAQ,WACd,CACF,CAAC,CACH,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{DEVCONTAINER_CLI_PACKAGE as e,DEVCONTAINER_CONFIG_PATHS as t,bootstrapCommand as n,containerWorkspacePath as r,devcontainerExecArgs as i,devcontainerUpArgs as a,mapForwardArgs as o,parseDevcontainerUp as s}from"../services/devcontainer.mjs";import c from"node:fs";import l from"node:path";const u=`devcontainer`;function d(e){for(let n of t){let t=l.join(e,n);if(c.existsSync(t))return t}}async function f(t){return(await t.capture(u,[`--version`]))?.exitCode===0?{command:u,prefix:[]}:{command:`npx`,prefix:[`-y`,e]}}async function p(e){let t=await f(e.runner);e.onProgress?.(`starting the workspace dev container`);let c=a({repoRoot:e.repoRoot,socketDirectory:e.socketDirectory,socketTarget:e.socketDirectory?`/run/doompi`:void 0}),l=await e.runner.capture(t.command,[...t.prefix,...c]);if(!l)throw Error(`The Dev Containers CLI could not be started. Install @devcontainers/cli, or make npx available.`);let{containerId:u,remoteWorkspaceFolder:d,error:p}=s(l.stdout);if(!u)throw Error(`The dev container did not start: ${p??`exit ${l.exitCode}`}`);let m=await e.runner.run(e.engine,[`exec`,u,...n(e.version)]);if(m!==0)throw Error(`DoomPi could not be installed in the dev container (exit ${m}). Add it to the dev container, or unset the dev container preference to use the built-in image.`);return e.onProgress?.(`attaching to dev container ${u.slice(0,12)}`),e.runner.run(e.engine,i({containerId:u,cwd:d?r(e.repoRoot,e.cwd,d):e.cwd,environment:e.environment,hasTty:e.hasTty,command:[`doompi`,...d?o(e.forwardArgs,e.repoRoot,d):e.forwardArgs]}))}export{d as findDevcontainerConfig,f as resolveDevcontainerCli,p as runDevcontainerSession};
|
|
2
|
+
//# sourceMappingURL=devcontainer.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devcontainer.mjs","names":[],"sources":["../../src/adapters/devcontainer.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport {\n bootstrapCommand,\n containerWorkspacePath,\n mapForwardArgs,\n DEVCONTAINER_CLI_PACKAGE,\n DEVCONTAINER_CONFIG_PATHS,\n devcontainerExecArgs,\n devcontainerUpArgs,\n parseDevcontainerUp,\n} from '../services/devcontainer.ts';\nimport type { EngineProcessRunner, SandboxEngine } from '../types/sandboxHarness.ts';\n\nconst DEVCONTAINER_BINARY = 'devcontainer';\nconst NPX_BINARY = 'npx';\nconst LAUNCHER_BINARY = 'doompi';\nconst BROKER_MOUNT_DIRECTORY = '/run/doompi';\n\n/** Locates the workspace's own dev container configuration, if it has one. */\nexport function findDevcontainerConfig(repoRoot: string): string | undefined {\n for (const relative of DEVCONTAINER_CONFIG_PATHS) {\n const candidate = path.join(repoRoot, relative);\n if (fs.existsSync(candidate)) return candidate;\n }\n return undefined;\n}\n\n/**\n * Resolves how to invoke the Dev Containers CLI.\n *\n * A local install is preferred so a launch does not depend on the network;\n * npx is the fallback, pinned so the tool cannot change underneath a session.\n */\nexport async function resolveDevcontainerCli(\n runner: EngineProcessRunner,\n): Promise<{ command: string; prefix: string[] }> {\n const probe = await runner.capture(DEVCONTAINER_BINARY, ['--version']);\n if (probe?.exitCode === 0) return { command: DEVCONTAINER_BINARY, prefix: [] };\n return { command: NPX_BINARY, prefix: ['-y', DEVCONTAINER_CLI_PACKAGE] };\n}\n\nexport interface DevcontainerSessionOptions {\n repoRoot: string;\n cwd: string;\n forwardArgs: readonly string[];\n environment: Readonly<Record<string, string>>;\n engine: SandboxEngine;\n runner: EngineProcessRunner;\n version: string;\n hasTty: boolean;\n /** Host directory holding the broker socket, when the broker uses one. */\n socketDirectory?: string;\n onProgress?: (message: string) => void;\n}\n\n/**\n * Runs the session inside the workspace's own dev container.\n *\n * The container is brought up by the Dev Containers CLI, so the file decides\n * the image, features, mounts and lifecycle hooks: the session gets the\n * project's real toolchain, and this layer's isolation guarantees do not\n * apply beyond the environment it passes in.\n */\nexport async function runDevcontainerSession(options: DevcontainerSessionOptions): Promise<number> {\n const cli = await resolveDevcontainerCli(options.runner);\n options.onProgress?.('starting the workspace dev container');\n\n const upArgs = devcontainerUpArgs({\n repoRoot: options.repoRoot,\n socketDirectory: options.socketDirectory,\n socketTarget: options.socketDirectory ? BROKER_MOUNT_DIRECTORY : undefined,\n });\n const up = await options.runner.capture(cli.command, [...cli.prefix, ...upArgs]);\n if (!up) {\n throw new Error('The Dev Containers CLI could not be started. Install @devcontainers/cli, or make npx available.');\n }\n const { containerId, remoteWorkspaceFolder, error } = parseDevcontainerUp(up.stdout);\n if (!containerId) throw new Error(`The dev container did not start: ${error ?? `exit ${up.exitCode}`}`);\n\n const bootstrap = await options.runner.run(options.engine, [\n 'exec',\n containerId,\n ...bootstrapCommand(options.version),\n ]);\n if (bootstrap !== 0) {\n throw new Error(\n `DoomPi could not be installed in the dev container (exit ${bootstrap}). ` +\n 'Add it to the dev container, or unset the dev container preference to use the built-in image.',\n );\n }\n\n options.onProgress?.(`attaching to dev container ${containerId.slice(0, 12)}`);\n return options.runner.run(\n options.engine,\n devcontainerExecArgs({\n containerId,\n cwd: remoteWorkspaceFolder\n ? containerWorkspacePath(options.repoRoot, options.cwd, remoteWorkspaceFolder)\n : options.cwd,\n environment: options.environment,\n hasTty: options.hasTty,\n command: [\n LAUNCHER_BINARY,\n ...(remoteWorkspaceFolder\n ? mapForwardArgs(options.forwardArgs, options.repoRoot, remoteWorkspaceFolder)\n : options.forwardArgs),\n ],\n }),\n );\n}\n"],"mappings":"wSAcA,MAAM,EAAsB,eAM5B,SAAgB,EAAuB,EAAsC,CAC3E,IAAK,IAAM,KAAY,EAA2B,CAChD,IAAM,EAAY,EAAK,KAAK,EAAU,CAAQ,EAC9C,GAAI,EAAG,WAAW,CAAS,EAAG,OAAO,CACvC,CAEF,CAQA,eAAsB,EACpB,EACgD,CAGhD,OADI,MADgB,EAAO,QAAQ,EAAqB,CAAC,WAAW,CAAC,EAAA,EAC1D,WAAa,EAAU,CAAE,QAAS,EAAqB,OAAQ,CAAC,CAAE,EACtE,CAAE,QAAS,MAAY,OAAQ,CAAC,KAAM,CAAwB,CAAE,CACzE,CAwBA,eAAsB,EAAuB,EAAsD,CACjG,IAAM,EAAM,MAAM,EAAuB,EAAQ,MAAM,EACvD,EAAQ,aAAa,sCAAsC,EAE3D,IAAM,EAAS,EAAmB,CAChC,SAAU,EAAQ,SAClB,gBAAiB,EAAQ,gBACzB,aAAc,EAAQ,gBAAkB,cAAyB,IAAA,EACnE,CAAC,EACK,EAAK,MAAM,EAAQ,OAAO,QAAQ,EAAI,QAAS,CAAC,GAAG,EAAI,OAAQ,GAAG,CAAM,CAAC,EAC/E,GAAI,CAAC,EACH,MAAU,MAAM,iGAAiG,EAEnH,GAAM,CAAE,cAAa,wBAAuB,SAAU,EAAoB,EAAG,MAAM,EACnF,GAAI,CAAC,EAAa,MAAU,MAAM,oCAAoC,GAAS,QAAQ,EAAG,YAAY,EAEtG,IAAM,EAAY,MAAM,EAAQ,OAAO,IAAI,EAAQ,OAAQ,CACzD,OACA,EACA,GAAG,EAAiB,EAAQ,OAAO,CACrC,CAAC,EACD,GAAI,IAAc,EAChB,MAAU,MACR,4DAA4D,EAAU,iGAExE,EAIF,OADA,EAAQ,aAAa,8BAA8B,EAAY,MAAM,EAAG,EAAE,GAAG,EACtE,EAAQ,OAAO,IACpB,EAAQ,OACR,EAAqB,CACnB,cACA,IAAK,EACD,EAAuB,EAAQ,SAAU,EAAQ,IAAK,CAAqB,EAC3E,EAAQ,IACZ,YAAa,EAAQ,YACrB,OAAQ,EAAQ,OAChB,QAAS,CACP,SACA,GAAI,EACA,EAAe,EAAQ,YAAa,EAAQ,SAAU,CAAqB,EAC3E,EAAQ,WACd,CACF,CAAC,CACH,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
let e=require("node:child_process");const t=`inherit`,n=`pipe`,r=`ignore`,i=[`SIGINT`,`SIGTERM`,`SIGHUP`];var a=class{run(r,a,o={}){return new Promise((s,c)=>{let l=(0,e.spawn)(r,a,{stdio:o.input===void 0?t:[n,t,t]});o.input!==void 0&&l.stdin?.end(o.input);let u=i.map(e=>{let t=()=>{l.kill(e)};return process.on(e,t),{signal:e,forward:t}}),d=()=>{for(let{signal:e,forward:t}of u)process.off(e,t)};l.once(`error`,e=>{d(),c(e)}),l.once(`exit`,(e,t)=>{d(),s(e??+!!t)})})}capture(t,i){return new Promise(a=>{let o=(0,e.spawn)(t,i,{stdio:[r,n,r]}),s=``;o.stdout?.on(`data`,e=>{s+=e.toString()}),o.once(`error`,()=>{a(void 0)}),o.once(`exit`,e=>{a({exitCode:e??1,stdout:s})})})}};exports.SpawnEngineProcessRunner=a;
|
|
2
|
+
//# sourceMappingURL=engineProcess.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engineProcess.cjs","names":["spawn"],"sources":["../../src/adapters/engineProcess.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport type { EngineCaptureResult, EngineProcessRunner, EngineRunOptions } from '../types/sandboxHarness.ts';\n\nconst INHERIT = 'inherit';\nconst PIPE = 'pipe';\nconst IGNORE = 'ignore';\nconst FORWARDED_SIGNALS: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGHUP'];\n\n/** Spawns the container engine, keeping the session's terminal attached. */\nexport class SpawnEngineProcessRunner implements EngineProcessRunner {\n run(command: string, args: string[], options: EngineRunOptions = {}): Promise<number> {\n return new Promise((resolve, reject) => {\n const child = spawn(command, args, {\n stdio: options.input === undefined ? INHERIT : [PIPE, INHERIT, INHERIT],\n });\n if (options.input !== undefined) child.stdin?.end(options.input);\n\n // With a shared terminal the engine sees Ctrl-C itself; forwarding covers\n // detached automation runs, and a duplicate signal is harmless.\n const forwarders = FORWARDED_SIGNALS.map((signal) => {\n const forward = (): void => {\n child.kill(signal);\n };\n process.on(signal, forward);\n return { signal, forward };\n });\n const cleanup = (): void => {\n for (const { signal, forward } of forwarders) process.off(signal, forward);\n };\n\n child.once('error', (error) => {\n cleanup();\n reject(error);\n });\n child.once('exit', (code, signal) => {\n cleanup();\n resolve(code ?? (signal ? 1 : 0));\n });\n });\n }\n\n capture(command: string, args: string[]): Promise<EngineCaptureResult | undefined> {\n return new Promise((resolve) => {\n const child = spawn(command, args, { stdio: [IGNORE, PIPE, IGNORE] });\n let stdout = '';\n child.stdout?.on('data', (chunk: Buffer | string) => {\n stdout += chunk.toString();\n });\n child.once('error', () => {\n resolve(undefined);\n });\n child.once('exit', (code) => {\n resolve({ exitCode: code ?? 1, stdout });\n });\n });\n }\n}\n"],"mappings":"oCAGA,MAAM,EAAU,UACV,EAAO,OACP,EAAS,SACT,EAAsC,CAAC,SAAU,UAAW,QAAQ,EAG1E,IAAa,EAAb,KAAqE,CACnE,IAAI,EAAiB,EAAgB,EAA4B,CAAC,EAAoB,CACpF,OAAO,IAAI,SAAS,EAAS,IAAW,CACtC,IAAM,GAAA,EAAQA,EAAAA,MAAAA,CAAM,EAAS,EAAM,CACjC,MAAO,EAAQ,QAAU,IAAA,GAAY,EAAU,CAAC,EAAM,EAAS,CAAO,CACxE,CAAC,EACG,EAAQ,QAAU,IAAA,IAAW,EAAM,OAAO,IAAI,EAAQ,KAAK,EAI/D,IAAM,EAAa,EAAkB,IAAK,GAAW,CACnD,IAAM,MAAsB,CAC1B,EAAM,KAAK,CAAM,CACnB,EAEA,OADA,QAAQ,GAAG,EAAQ,CAAO,EACnB,CAAE,SAAQ,SAAQ,CAC3B,CAAC,EACK,MAAsB,CAC1B,IAAK,GAAM,CAAE,SAAQ,aAAa,EAAY,QAAQ,IAAI,EAAQ,CAAO,CAC3E,EAEA,EAAM,KAAK,QAAU,GAAU,CAC7B,EAAQ,EACR,EAAO,CAAK,CACd,CAAC,EACD,EAAM,KAAK,QAAS,EAAM,IAAW,CACnC,EAAQ,EACR,EAAQ,GAAS,IAAe,CAClC,CAAC,CACH,CAAC,CACH,CAEA,QAAQ,EAAiB,EAA0D,CACjF,OAAO,IAAI,QAAS,GAAY,CAC9B,IAAM,GAAA,EAAQA,EAAAA,MAAAA,CAAM,EAAS,EAAM,CAAE,MAAO,CAAC,EAAQ,EAAM,CAAM,CAAE,CAAC,EAChE,EAAS,GACb,EAAM,QAAQ,GAAG,OAAS,GAA2B,CACnD,GAAU,EAAM,SAAS,CAC3B,CAAC,EACD,EAAM,KAAK,YAAe,CACxB,EAAQ,IAAA,EAAS,CACnB,CAAC,EACD,EAAM,KAAK,OAAS,GAAS,CAC3B,EAAQ,CAAE,SAAU,GAAQ,EAAG,QAAO,CAAC,CACzC,CAAC,CACH,CAAC,CACH,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{spawn as e}from"node:child_process";const t=`inherit`,n=`pipe`,r=`ignore`,i=[`SIGINT`,`SIGTERM`,`SIGHUP`];var a=class{run(r,a,o={}){return new Promise((s,c)=>{let l=e(r,a,{stdio:o.input===void 0?t:[n,t,t]});o.input!==void 0&&l.stdin?.end(o.input);let u=i.map(e=>{let t=()=>{l.kill(e)};return process.on(e,t),{signal:e,forward:t}}),d=()=>{for(let{signal:e,forward:t}of u)process.off(e,t)};l.once(`error`,e=>{d(),c(e)}),l.once(`exit`,(e,t)=>{d(),s(e??+!!t)})})}capture(t,i){return new Promise(a=>{let o=e(t,i,{stdio:[r,n,r]}),s=``;o.stdout?.on(`data`,e=>{s+=e.toString()}),o.once(`error`,()=>{a(void 0)}),o.once(`exit`,e=>{a({exitCode:e??1,stdout:s})})})}};export{a as SpawnEngineProcessRunner};
|
|
2
|
+
//# sourceMappingURL=engineProcess.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engineProcess.mjs","names":[],"sources":["../../src/adapters/engineProcess.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport type { EngineCaptureResult, EngineProcessRunner, EngineRunOptions } from '../types/sandboxHarness.ts';\n\nconst INHERIT = 'inherit';\nconst PIPE = 'pipe';\nconst IGNORE = 'ignore';\nconst FORWARDED_SIGNALS: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGHUP'];\n\n/** Spawns the container engine, keeping the session's terminal attached. */\nexport class SpawnEngineProcessRunner implements EngineProcessRunner {\n run(command: string, args: string[], options: EngineRunOptions = {}): Promise<number> {\n return new Promise((resolve, reject) => {\n const child = spawn(command, args, {\n stdio: options.input === undefined ? INHERIT : [PIPE, INHERIT, INHERIT],\n });\n if (options.input !== undefined) child.stdin?.end(options.input);\n\n // With a shared terminal the engine sees Ctrl-C itself; forwarding covers\n // detached automation runs, and a duplicate signal is harmless.\n const forwarders = FORWARDED_SIGNALS.map((signal) => {\n const forward = (): void => {\n child.kill(signal);\n };\n process.on(signal, forward);\n return { signal, forward };\n });\n const cleanup = (): void => {\n for (const { signal, forward } of forwarders) process.off(signal, forward);\n };\n\n child.once('error', (error) => {\n cleanup();\n reject(error);\n });\n child.once('exit', (code, signal) => {\n cleanup();\n resolve(code ?? (signal ? 1 : 0));\n });\n });\n }\n\n capture(command: string, args: string[]): Promise<EngineCaptureResult | undefined> {\n return new Promise((resolve) => {\n const child = spawn(command, args, { stdio: [IGNORE, PIPE, IGNORE] });\n let stdout = '';\n child.stdout?.on('data', (chunk: Buffer | string) => {\n stdout += chunk.toString();\n });\n child.once('error', () => {\n resolve(undefined);\n });\n child.once('exit', (code) => {\n resolve({ exitCode: code ?? 1, stdout });\n });\n });\n }\n}\n"],"mappings":"2CAGA,MAAM,EAAU,UACV,EAAO,OACP,EAAS,SACT,EAAsC,CAAC,SAAU,UAAW,QAAQ,EAG1E,IAAa,EAAb,KAAqE,CACnE,IAAI,EAAiB,EAAgB,EAA4B,CAAC,EAAoB,CACpF,OAAO,IAAI,SAAS,EAAS,IAAW,CACtC,IAAM,EAAQ,EAAM,EAAS,EAAM,CACjC,MAAO,EAAQ,QAAU,IAAA,GAAY,EAAU,CAAC,EAAM,EAAS,CAAO,CACxE,CAAC,EACG,EAAQ,QAAU,IAAA,IAAW,EAAM,OAAO,IAAI,EAAQ,KAAK,EAI/D,IAAM,EAAa,EAAkB,IAAK,GAAW,CACnD,IAAM,MAAsB,CAC1B,EAAM,KAAK,CAAM,CACnB,EAEA,OADA,QAAQ,GAAG,EAAQ,CAAO,EACnB,CAAE,SAAQ,SAAQ,CAC3B,CAAC,EACK,MAAsB,CAC1B,IAAK,GAAM,CAAE,SAAQ,aAAa,EAAY,QAAQ,IAAI,EAAQ,CAAO,CAC3E,EAEA,EAAM,KAAK,QAAU,GAAU,CAC7B,EAAQ,EACR,EAAO,CAAK,CACd,CAAC,EACD,EAAM,KAAK,QAAS,EAAM,IAAW,CACnC,EAAQ,EACR,EAAQ,GAAS,IAAe,CAClC,CAAC,CACH,CAAC,CACH,CAEA,QAAQ,EAAiB,EAA0D,CACjF,OAAO,IAAI,QAAS,GAAY,CAC9B,IAAM,EAAQ,EAAM,EAAS,EAAM,CAAE,MAAO,CAAC,EAAQ,EAAM,CAAM,CAAE,CAAC,EAChE,EAAS,GACb,EAAM,QAAQ,GAAG,OAAS,GAA2B,CACnD,GAAU,EAAM,SAAS,CAC3B,CAAC,EACD,EAAM,KAAK,YAAe,CACxB,EAAQ,IAAA,EAAS,CACnB,CAAC,EACD,EAAM,KAAK,OAAS,GAAS,CAC3B,EAAQ,CAAE,SAAU,GAAQ,EAAG,QAAO,CAAC,CACzC,CAAC,CACH,CAAC,CACH,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../services/sandboxBridge.cjs");require("../services/devcontainer.cjs");const n=require("./devcontainer.cjs"),r=require("../services/oauthCallback.cjs"),i=require("./loginPorts.cjs"),a=require("./brokerHost.cjs"),o=require("../services/sandboxPlan.cjs"),s=require("../services/runFlags.cjs"),c=require("../services/sandboxImage.cjs"),l=require("./sandboxImageTag.cjs"),u=require("./engineProcess.cjs");let d=require("node:crypto"),f=require("node:fs");f=e.__toESM(f,1);let p=require("node:module"),m=require("node:os");m=e.__toESM(m,1);let h=require("node:path");h=e.__toESM(h,1);const g=`DOOMPI_SANDBOX_ENGINE`,_=`DOOMPI_SANDBOX_RUN_FLAGS`,v=[`docker`,`podman`,`nerdctl`,`finch`],y=h.default.join(`.doom`,`modes.yaml`),b=/^\s*(?:-|name:)\s*["']?\.{1,2}\//m,x=(0,p.createRequire)(require("url").pathToFileURL(__filename).href);function S(){return x(`@agimon-ai/doompi-sandbox/package.json`).version}function C(e){return h.default.join(m.default.tmpdir(),`${w(e)}-broker`)}function w(e){return`doompi-sandbox-${(0,d.createHash)(`sha256`).update(e).digest(`hex`).slice(0,12)}`}async function T(e,t){let n=t[g]?.trim();if(n){if(v.includes(n))return n;throw Error(`${g} must be one of: ${v.join(`, `)}.`)}for(let t of v)if((await e.capture(t,[`--version`]))?.exitCode===0)return t;throw Error(`No container engine found. Install one of ${v.join(`, `)}, or set ${g}.`)}async function E(e,n,r,i){let a=l.sandboxImageTag(r);if((await e.capture(n,[`image`,`inspect`,a]))?.exitCode===0)return a;i?.(`building ${a} (first run for this DoomPi version)`);let o=f.default.mkdtempSync(h.default.join(m.default.tmpdir(),`doompi-sandbox-build-`));try{f.default.writeFileSync(h.default.join(o,`Dockerfile`),c.sandboxDockerfile()),f.default.writeFileSync(h.default.join(o,t.BRIDGE_FILE_NAME),t.sandboxBridgeSource());let i=await e.run(n,[`build`,`-t`,a,`--build-arg`,`DOOMPI_VERSION=${r}`,o]);if(i!==0)throw Error(`Building the sandbox image failed with exit code ${i}.`)}finally{f.default.rmSync(o,{recursive:!0,force:!0})}return a}function D(e,t){let n;try{n=f.default.readFileSync(h.default.join(e,y),`utf8`)}catch{return}b.test(n)&&t?.(`modes.yaml declares local workspace packages; their platform-specific dependencies must be installed inside the container before they can load`)}function O(e={}){let t=e.runner??new u.SpawnEngineProcessRunner,c=e.startBroker??a.startBroker,l=e.loginPorts??i.availableLoginPorts;return{async launchSandbox(i){let a=await T(t,i.environment),u=s.parseRunFlags(i.environment[_]);s.assertRunFlags(u,_),i.onProgress?.(`using ${a}${u.length>0?` with ${u.join(` `)}`:``}`);let d=e.version??S(),f=i.environment.DOOMPI_SANDBOX_DEVCONTAINER===`0`?void 0:n.findDevcontainerConfig(i.repoRoot),p=f?void 0:await E(t,a,d,i.onProgress);f?(i.onProgress?.(`using the workspace dev container from ${h.default.relative(i.repoRoot,f)}`),i.onProgress?.(`its configuration owns the mounts and run arguments, so this is not an isolation boundary`)):D(i.repoRoot,i.onProgress);let m=i.environment.DOOMPI_SANDBOX_BROKER===`0`?void 0:await c({environment:i.environment,onDenied:e=>i.onProgress?.(`broker refused a call: ${e}`),...f?{socketDirectory:C(i.repoRoot)}:{}});m&&i.onProgress?.(`brokering ${m.providers.join(`, `)}; provider keys stay on the host`);let g=f?[]:await l();!f&&g.length<r.OAUTH_CALLBACK_PORTS.length&&i.onProgress?.(`some OAuth callback ports are already taken; /login inside this sandbox may not complete`);try{let r=process.stdin.isTTY===!0&&process.stdout.isTTY===!0;if(f)return await n.runDevcontainerSession({repoRoot:i.repoRoot,cwd:i.cwd,forwardArgs:i.forwardArgs,environment:o.containerEnvironment(i.environment,m),engine:a,runner:t,version:d,hasTty:r,...m?.endpoint.transport===`unix`?{socketDirectory:m.endpoint.socketDirectory}:{},...i.onProgress?{onProgress:i.onProgress}:{}});let s={hasTty:r,platform:process.platform,userId:process.getuid?.(),groupId:process.getgid?.(),repoKey:w(i.repoRoot),version:d,...e.hostFacts},c=o.buildSandboxPlan({repoRoot:i.repoRoot,cwd:i.cwd,forwardArgs:i.forwardArgs,environment:i.environment,engine:a,host:s,imageTag:p,runFlags:u,loginPorts:g,broker:m});return i.onProgress?.(`starting contained session in ${p}`),await t.run(a,c.runArgs)}finally{await m?.stop()}}}}function k(e){return O().launchSandbox(e)}exports.createSandboxLauncher=O,exports.launchSandbox=k;
|
|
2
|
+
//# sourceMappingURL=harness.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness.cjs","names":["path","require","createRequire","os","createHash","sandboxImageTag","fs","sandboxDockerfile","BRIDGE_FILE_NAME","sandboxBridgeSource","SpawnEngineProcessRunner","startBroker","availableLoginPorts","parseRunFlags","findDevcontainerConfig","OAUTH_CALLBACK_PORTS","runDevcontainerSession","containerEnvironment","buildSandboxPlan"],"sources":["../../src/adapters/harness.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport { createRequire } from 'node:module';\nimport os from 'node:os';\nimport path from 'node:path';\nimport type { SandboxLaunchRequest } from '@agimon-ai/doompi-extension-contracts/sandbox-harness';\nimport { findDevcontainerConfig, runDevcontainerSession } from './devcontainer.ts';\nimport { DEVCONTAINER_DISABLED_ENV } from '../services/devcontainer.ts';\nimport { availableLoginPorts } from './loginPorts.ts';\nimport { startBroker, type RunningBroker } from './brokerHost.ts';\nimport { BRIDGE_FILE_NAME, sandboxBridgeSource } from '../services/sandboxBridge.ts';\nimport { buildSandboxPlan, containerEnvironment } from '../services/sandboxPlan.ts';\nimport { OAUTH_CALLBACK_PORTS } from '../services/oauthCallback.ts';\nimport { assertRunFlags, parseRunFlags } from '../services/runFlags.ts';\nimport { sandboxDockerfile } from '../services/sandboxImage.ts';\nimport { sandboxImageTag } from './sandboxImageTag.ts';\nimport type { EngineProcessRunner, SandboxEngine, SandboxHostFacts } from '../types/sandboxHarness.ts';\nimport { SpawnEngineProcessRunner } from './engineProcess.ts';\n\nconst ENGINE_ENV = 'DOOMPI_SANDBOX_ENGINE';\nconst BROKER_DISABLED_ENV = 'DOOMPI_SANDBOX_BROKER';\nconst RUN_FLAGS_ENV = 'DOOMPI_SANDBOX_RUN_FLAGS';\nconst DISABLED_VALUE = '0';\n// Every entry takes docker's run syntax, which is what the plan emits.\nconst ENGINES: SandboxEngine[] = ['docker', 'podman', 'nerdctl', 'finch'];\nconst DOCKERFILE_NAME = 'Dockerfile';\nconst MODES_FILE = path.join('.doom', 'modes.yaml');\nconst LOCAL_PACKAGE_PATTERN = /^\\s*(?:-|name:)\\s*[\"']?\\.{1,2}\\//m;\nconst REPO_KEY_LENGTH = 12;\n\n// Self-reference instead of a relative path: the file depth differs between\n// running from src and from the built dist tree.\nconst require = createRequire(import.meta.url);\nconst SELF_MANIFEST = '@agimon-ai/doompi-sandbox/package.json';\n\nexport interface SandboxLauncherDependencies {\n runner?: EngineProcessRunner;\n /** Seam for tests; defaults to probing the real OAuth callback ports. */\n loginPorts?: () => Promise<number[]>;\n /** Seam for tests; defaults to the real host broker. */\n startBroker?: (options: {\n environment: Readonly<Record<string, string | undefined>>;\n onDenied?: (reason: string) => void;\n }) => Promise<RunningBroker | undefined>;\n /** Distribution version override; defaults to this package's own version. */\n version?: string;\n hostFacts?: Partial<SandboxHostFacts>;\n}\n\nfunction distributionVersion(): string {\n return (require(SELF_MANIFEST) as { version: string }).version;\n}\n\n/** Fixed per-repository directory a reused dev container can keep mounted. */\nfunction brokerDirectory(repoRoot: string): string {\n return path.join(os.tmpdir(), `${repoKey(repoRoot)}-broker`);\n}\n\nfunction repoKey(repoRoot: string): string {\n return `doompi-sandbox-${createHash('sha256').update(repoRoot).digest('hex').slice(0, REPO_KEY_LENGTH)}`;\n}\n\nasync function detectEngine(\n runner: EngineProcessRunner,\n environment: Readonly<Record<string, string | undefined>>,\n): Promise<SandboxEngine> {\n const configured = environment[ENGINE_ENV]?.trim();\n if (configured) {\n if ((ENGINES as string[]).includes(configured)) return configured as SandboxEngine;\n throw new Error(`${ENGINE_ENV} must be one of: ${ENGINES.join(', ')}.`);\n }\n for (const engine of ENGINES) {\n const probe = await runner.capture(engine, ['--version']);\n if (probe?.exitCode === 0) return engine;\n }\n throw new Error(`No container engine found. Install one of ${ENGINES.join(', ')}, or set ${ENGINE_ENV}.`);\n}\n\nasync function ensureImage(\n runner: EngineProcessRunner,\n engine: SandboxEngine,\n version: string,\n onProgress: ((message: string) => void) | undefined,\n): Promise<string> {\n const tag = sandboxImageTag(version);\n const inspected = await runner.capture(engine, ['image', 'inspect', tag]);\n if (inspected?.exitCode === 0) return tag;\n\n onProgress?.(`building ${tag} (first run for this DoomPi version)`);\n const context = fs.mkdtempSync(path.join(os.tmpdir(), 'doompi-sandbox-build-'));\n try {\n fs.writeFileSync(path.join(context, DOCKERFILE_NAME), sandboxDockerfile());\n fs.writeFileSync(path.join(context, BRIDGE_FILE_NAME), sandboxBridgeSource());\n const exitCode = await runner.run(engine, [\n 'build',\n '-t',\n tag,\n '--build-arg',\n `DOOMPI_VERSION=${version}`,\n context,\n ]);\n if (exitCode !== 0) throw new Error(`Building the sandbox image failed with exit code ${exitCode}.`);\n } finally {\n fs.rmSync(context, { recursive: true, force: true });\n }\n return tag;\n}\n\nfunction warnAboutLocalPackages(repoRoot: string, onProgress: ((message: string) => void) | undefined): void {\n let modes: string;\n try {\n modes = fs.readFileSync(path.join(repoRoot, MODES_FILE), 'utf8');\n } catch {\n return;\n }\n if (LOCAL_PACKAGE_PATTERN.test(modes)) {\n onProgress?.(\n 'modes.yaml declares local workspace packages; their platform-specific dependencies ' +\n 'must be installed inside the container before they can load',\n );\n }\n}\n\n/** Builds the launcher with injectable seams for tests. */\nexport function createSandboxLauncher(dependencies: SandboxLauncherDependencies = {}) {\n const runner = dependencies.runner ?? new SpawnEngineProcessRunner();\n const beginBroker = dependencies.startBroker ?? startBroker;\n const resolveLoginPorts = dependencies.loginPorts ?? availableLoginPorts;\n return {\n async launchSandbox(request: SandboxLaunchRequest): Promise<number> {\n const engine = await detectEngine(runner, request.environment);\n const runFlags = parseRunFlags(request.environment[RUN_FLAGS_ENV]);\n assertRunFlags(runFlags, RUN_FLAGS_ENV);\n request.onProgress?.(`using ${engine}${runFlags.length > 0 ? ` with ${runFlags.join(' ')}` : ''}`);\n\n const version = dependencies.version ?? distributionVersion();\n // A workspace that describes its own container is describing the\n // toolchain its agent needs, so that container wins over the built-in\n // image. Nothing here is built or pulled until that choice is made.\n const devcontainer =\n request.environment[DEVCONTAINER_DISABLED_ENV] === DISABLED_VALUE\n ? undefined\n : findDevcontainerConfig(request.repoRoot);\n const imageTag = devcontainer ? undefined : await ensureImage(runner, engine, version, request.onProgress);\n if (devcontainer) {\n request.onProgress?.(`using the workspace dev container from ${path.relative(request.repoRoot, devcontainer)}`);\n request.onProgress?.(\n 'its configuration owns the mounts and run arguments, so this is not an isolation boundary',\n );\n } else {\n warnAboutLocalPackages(request.repoRoot, request.onProgress);\n }\n\n const broker =\n request.environment[BROKER_DISABLED_ENV] === DISABLED_VALUE\n ? undefined\n : await beginBroker({\n environment: request.environment,\n onDenied: (reason) => request.onProgress?.(`broker refused a call: ${reason}`),\n // A reused dev container keeps the mounts it was created with,\n // so the socket has to be at the same path on every launch.\n ...(devcontainer ? { socketDirectory: brokerDirectory(request.repoRoot) } : {}),\n });\n if (broker) {\n request.onProgress?.(`brokering ${broker.providers.join(', ')}; provider keys stay on the host`);\n }\n\n const loginPorts = devcontainer ? [] : await resolveLoginPorts();\n if (!devcontainer && loginPorts.length < OAUTH_CALLBACK_PORTS.length) {\n request.onProgress?.(\n 'some OAuth callback ports are already taken; /login inside this sandbox may not complete',\n );\n }\n\n try {\n const hasTty = process.stdin.isTTY === true && process.stdout.isTTY === true;\n if (devcontainer) {\n return await runDevcontainerSession({\n repoRoot: request.repoRoot,\n cwd: request.cwd,\n forwardArgs: request.forwardArgs,\n environment: containerEnvironment(request.environment, broker),\n engine,\n runner,\n version,\n hasTty,\n ...(broker?.endpoint.transport === 'unix' ? { socketDirectory: broker.endpoint.socketDirectory } : {}),\n ...(request.onProgress ? { onProgress: request.onProgress } : {}),\n });\n }\n\n const host: SandboxHostFacts = {\n hasTty,\n platform: process.platform,\n userId: process.getuid?.(),\n groupId: process.getgid?.(),\n repoKey: repoKey(request.repoRoot),\n version,\n ...dependencies.hostFacts,\n };\n const plan = buildSandboxPlan({\n repoRoot: request.repoRoot,\n cwd: request.cwd,\n forwardArgs: request.forwardArgs,\n environment: request.environment,\n engine,\n host,\n imageTag: imageTag as string,\n runFlags,\n loginPorts,\n broker,\n });\n request.onProgress?.(`starting contained session in ${imageTag}`);\n return await runner.run(engine, plan.runArgs);\n } finally {\n await broker?.stop();\n }\n },\n };\n}\n\n/** Contract entry the DoomPi harness resolves for `--sandbox`. */\nexport function launchSandbox(request: SandboxLaunchRequest): Promise<number> {\n return createSandboxLauncher().launchSandbox(request);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { EngineProcessRunner, SandboxHostFacts } from "../types/sandboxHarness.cjs";
|
|
2
|
+
import { RunningBroker } from "./brokerHost.cjs";
|
|
3
|
+
import { SandboxLaunchRequest } from "@agimon-ai/doompi-extension-contracts/sandbox-harness";
|
|
4
|
+
//#region src/adapters/harness.d.ts
|
|
5
|
+
interface SandboxLauncherDependencies {
|
|
6
|
+
runner?: EngineProcessRunner;
|
|
7
|
+
/** Seam for tests; defaults to probing the real OAuth callback ports. */
|
|
8
|
+
loginPorts?: () => Promise<number[]>;
|
|
9
|
+
/** Seam for tests; defaults to the real host broker. */
|
|
10
|
+
startBroker?: (options: {
|
|
11
|
+
environment: Readonly<Record<string, string | undefined>>;
|
|
12
|
+
onDenied?: (reason: string) => void;
|
|
13
|
+
}) => Promise<RunningBroker | undefined>;
|
|
14
|
+
/** Distribution version override; defaults to this package's own version. */
|
|
15
|
+
version?: string;
|
|
16
|
+
hostFacts?: Partial<SandboxHostFacts>;
|
|
17
|
+
}
|
|
18
|
+
/** Builds the launcher with injectable seams for tests. */
|
|
19
|
+
declare function createSandboxLauncher(dependencies?: SandboxLauncherDependencies): {
|
|
20
|
+
launchSandbox(request: SandboxLaunchRequest): Promise<number>;
|
|
21
|
+
};
|
|
22
|
+
/** Contract entry the DoomPi harness resolves for `--sandbox`. */
|
|
23
|
+
declare function launchSandbox(request: SandboxLaunchRequest): Promise<number>;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { SandboxLauncherDependencies, createSandboxLauncher, launchSandbox };
|
|
26
|
+
//# sourceMappingURL=harness.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness.d.cts","names":[],"sources":["../../src/adapters/harness.ts"],"mappings":";;;;UAmCiB;EACf,SAAS;;EAET,mBAAmB;;EAEnB,eAAe;IACb,aAAa,SAAS;IACtB,YAAY;QACR,QAAQ;;EAEd;EACA,YAAY,QAAQ;;;iBA8EN,sBAAsB,eAAc;EAK1C,cAAa,SAAU,uBAAuB;;;iBA6FxC,cAAc,SAAS,uBAAuB"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { EngineProcessRunner, SandboxHostFacts } from "../types/sandboxHarness.mjs";
|
|
2
|
+
import { RunningBroker } from "./brokerHost.mjs";
|
|
3
|
+
import { SandboxLaunchRequest } from "@agimon-ai/doompi-extension-contracts/sandbox-harness";
|
|
4
|
+
//#region src/adapters/harness.d.ts
|
|
5
|
+
interface SandboxLauncherDependencies {
|
|
6
|
+
runner?: EngineProcessRunner;
|
|
7
|
+
/** Seam for tests; defaults to probing the real OAuth callback ports. */
|
|
8
|
+
loginPorts?: () => Promise<number[]>;
|
|
9
|
+
/** Seam for tests; defaults to the real host broker. */
|
|
10
|
+
startBroker?: (options: {
|
|
11
|
+
environment: Readonly<Record<string, string | undefined>>;
|
|
12
|
+
onDenied?: (reason: string) => void;
|
|
13
|
+
}) => Promise<RunningBroker | undefined>;
|
|
14
|
+
/** Distribution version override; defaults to this package's own version. */
|
|
15
|
+
version?: string;
|
|
16
|
+
hostFacts?: Partial<SandboxHostFacts>;
|
|
17
|
+
}
|
|
18
|
+
/** Builds the launcher with injectable seams for tests. */
|
|
19
|
+
declare function createSandboxLauncher(dependencies?: SandboxLauncherDependencies): {
|
|
20
|
+
launchSandbox(request: SandboxLaunchRequest): Promise<number>;
|
|
21
|
+
};
|
|
22
|
+
/** Contract entry the DoomPi harness resolves for `--sandbox`. */
|
|
23
|
+
declare function launchSandbox(request: SandboxLaunchRequest): Promise<number>;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { SandboxLauncherDependencies, createSandboxLauncher, launchSandbox };
|
|
26
|
+
//# sourceMappingURL=harness.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness.d.mts","names":[],"sources":["../../src/adapters/harness.ts"],"mappings":";;;;UAmCiB;EACf,SAAS;;EAET,mBAAmB;;EAEnB,eAAe;IACb,aAAa,SAAS;IACtB,YAAY;QACR,QAAQ;;EAEd;EACA,YAAY,QAAQ;;;iBA8EN,sBAAsB,eAAc;EAK1C,cAAa,SAAU,uBAAuB;;;iBA6FxC,cAAc,SAAS,uBAAuB"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{BRIDGE_FILE_NAME as e,sandboxBridgeSource as t}from"../services/sandboxBridge.mjs";import"../services/devcontainer.mjs";import{findDevcontainerConfig as n,runDevcontainerSession as r}from"./devcontainer.mjs";import{OAUTH_CALLBACK_PORTS as i}from"../services/oauthCallback.mjs";import{availableLoginPorts as a}from"./loginPorts.mjs";import{startBroker as o}from"./brokerHost.mjs";import{buildSandboxPlan as s,containerEnvironment as c}from"../services/sandboxPlan.mjs";import{assertRunFlags as l,parseRunFlags as u}from"../services/runFlags.mjs";import{sandboxDockerfile as d}from"../services/sandboxImage.mjs";import{sandboxImageTag as f}from"./sandboxImageTag.mjs";import{SpawnEngineProcessRunner as p}from"./engineProcess.mjs";import{createRequire as m}from"node:module";import{createHash as h}from"node:crypto";import g from"node:fs";import _ from"node:os";import v from"node:path";const y=`DOOMPI_SANDBOX_ENGINE`,b=`DOOMPI_SANDBOX_RUN_FLAGS`,x=[`docker`,`podman`,`nerdctl`,`finch`],S=v.join(`.doom`,`modes.yaml`),C=/^\s*(?:-|name:)\s*["']?\.{1,2}\//m,w=m(import.meta.url);function T(){return w("@agimon-ai/doompi-sandbox/package.json").version}function E(e){return v.join(_.tmpdir(),`${D(e)}-broker`)}function D(e){return`doompi-sandbox-${h(`sha256`).update(e).digest(`hex`).slice(0,12)}`}async function O(e,t){let n=t[y]?.trim();if(n){if(x.includes(n))return n;throw Error(`${y} must be one of: ${x.join(`, `)}.`)}for(let t of x)if((await e.capture(t,[`--version`]))?.exitCode===0)return t;throw Error(`No container engine found. Install one of ${x.join(`, `)}, or set ${y}.`)}async function k(n,r,i,a){let o=f(i);if((await n.capture(r,[`image`,`inspect`,o]))?.exitCode===0)return o;a?.(`building ${o} (first run for this DoomPi version)`);let s=g.mkdtempSync(v.join(_.tmpdir(),`doompi-sandbox-build-`));try{g.writeFileSync(v.join(s,`Dockerfile`),d()),g.writeFileSync(v.join(s,e),t());let a=await n.run(r,[`build`,`-t`,o,`--build-arg`,`DOOMPI_VERSION=${i}`,s]);if(a!==0)throw Error(`Building the sandbox image failed with exit code ${a}.`)}finally{g.rmSync(s,{recursive:!0,force:!0})}return o}function A(e,t){let n;try{n=g.readFileSync(v.join(e,S),`utf8`)}catch{return}C.test(n)&&t?.(`modes.yaml declares local workspace packages; their platform-specific dependencies must be installed inside the container before they can load`)}function j(e={}){let t=e.runner??new p,d=e.startBroker??o,f=e.loginPorts??a;return{async launchSandbox(a){let o=await O(t,a.environment),p=u(a.environment[b]);l(p,b),a.onProgress?.(`using ${o}${p.length>0?` with ${p.join(` `)}`:``}`);let m=e.version??T(),h=a.environment.DOOMPI_SANDBOX_DEVCONTAINER===`0`?void 0:n(a.repoRoot),g=h?void 0:await k(t,o,m,a.onProgress);h?(a.onProgress?.(`using the workspace dev container from ${v.relative(a.repoRoot,h)}`),a.onProgress?.(`its configuration owns the mounts and run arguments, so this is not an isolation boundary`)):A(a.repoRoot,a.onProgress);let _=a.environment.DOOMPI_SANDBOX_BROKER===`0`?void 0:await d({environment:a.environment,onDenied:e=>a.onProgress?.(`broker refused a call: ${e}`),...h?{socketDirectory:E(a.repoRoot)}:{}});_&&a.onProgress?.(`brokering ${_.providers.join(`, `)}; provider keys stay on the host`);let y=h?[]:await f();!h&&y.length<i.length&&a.onProgress?.(`some OAuth callback ports are already taken; /login inside this sandbox may not complete`);try{let n=process.stdin.isTTY===!0&&process.stdout.isTTY===!0;if(h)return await r({repoRoot:a.repoRoot,cwd:a.cwd,forwardArgs:a.forwardArgs,environment:c(a.environment,_),engine:o,runner:t,version:m,hasTty:n,..._?.endpoint.transport===`unix`?{socketDirectory:_.endpoint.socketDirectory}:{},...a.onProgress?{onProgress:a.onProgress}:{}});let i={hasTty:n,platform:process.platform,userId:process.getuid?.(),groupId:process.getgid?.(),repoKey:D(a.repoRoot),version:m,...e.hostFacts},l=s({repoRoot:a.repoRoot,cwd:a.cwd,forwardArgs:a.forwardArgs,environment:a.environment,engine:o,host:i,imageTag:g,runFlags:p,loginPorts:y,broker:_});return a.onProgress?.(`starting contained session in ${g}`),await t.run(o,l.runArgs)}finally{await _?.stop()}}}}function M(e){return j().launchSandbox(e)}export{j as createSandboxLauncher,M as launchSandbox};
|
|
2
|
+
//# sourceMappingURL=harness.mjs.map
|