@elie-laloum/outpost 1.1.2 → 1.1.4
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/CHANGELOG.md +18 -0
- package/README.md +19 -191
- package/dist/cli/scaffold.constants.js +1 -0
- package/dist/cli/scaffold.constants.js.map +1 -1
- package/dist/infrastructure/binary-process.d.ts +3 -0
- package/dist/infrastructure/binary-process.js +53 -0
- package/dist/infrastructure/binary-process.js.map +1 -0
- package/dist/infrastructure/binary-process.types.d.ts +5 -0
- package/dist/infrastructure/binary-process.types.js +2 -0
- package/dist/infrastructure/binary-process.types.js.map +1 -0
- package/dist/infrastructure/transfer-copy.d.ts +3 -0
- package/dist/infrastructure/transfer-copy.js +51 -0
- package/dist/infrastructure/transfer-copy.js.map +1 -0
- package/dist/providers/container-archive.d.ts +3 -0
- package/dist/providers/container-archive.js +27 -0
- package/dist/providers/container-archive.js.map +1 -0
- package/dist/providers/container-command.js +3 -3
- package/dist/providers/container-command.js.map +1 -1
- package/dist/providers/container-files.d.ts +2 -1
- package/dist/providers/container-files.js +100 -11
- package/dist/providers/container-files.js.map +1 -1
- package/dist/providers/container.js +11 -2
- package/dist/providers/container.js.map +1 -1
- package/package.json +9 -6
- package/README.fr.md +0 -251
- package/ROADMAP.md +0 -34
- package/docs/api.md +0 -147
- package/docs/architecture.fr.md +0 -37
- package/docs/architecture.md +0 -57
- package/docs/migration-1.1.fr.md +0 -44
- package/docs/migration-1.1.md +0 -38
- package/docs/operations.md +0 -101
- package/docs/providers.md +0 -119
- package/docs/workflows.md +0 -101
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.4
|
|
4
|
+
|
|
5
|
+
- Wait for noninteractive container commands and preserve their real exit status; reuse the runtime terminal for interactive commands.
|
|
6
|
+
- Stream binary archives through the running Docker/Podman container so uploads and conversation capture/restore see the same tmpfs home as agents.
|
|
7
|
+
- Preserve ordinary file modes and links, reject unsafe download destinations, and support GNU tar and bsdtar hosts.
|
|
8
|
+
- Create a private writable agent home in generated images, including when used without the sandbox tmpfs mount.
|
|
9
|
+
- Add real container, pseudo-terminal, cancellation, binary integrity and native conversation regression checks.
|
|
10
|
+
- Add English/French account-connection guides for Claude Code and Codex, plus seven progressive cookbooks.
|
|
11
|
+
|
|
12
|
+
## 1.1.3
|
|
13
|
+
|
|
14
|
+
- Replace the flat documentation with an Astro Starlight site in English and French, with English as the default language.
|
|
15
|
+
- Organize usage into focused guides and generate checked reference pages for every public API export and supporting contract.
|
|
16
|
+
- Validate translations, TypeScript examples, rendered links, navigation and search assets in CI.
|
|
17
|
+
- Deploy one GitHub Pages site after successful stable releases, protecting it from older release deployments.
|
|
18
|
+
- Move the French getting-started content and roadmap into the documentation, retain the root changelog and remove obsolete migration and contribution files.
|
|
19
|
+
- Keep documentation tooling out of the published library archive.
|
|
20
|
+
|
|
3
21
|
## 1.1.2
|
|
4
22
|
|
|
5
23
|
- Match npm package repository metadata to the GitHub build origin for provenance verification. GitLab remains the canonical source repository.
|
package/README.md
CHANGED
|
@@ -1,226 +1,54 @@
|
|
|
1
1
|
# Outpost
|
|
2
2
|
|
|
3
|
-
[
|
|
3
|
+
[Documentation](https://elie-laloum.github.io/outpost/) · [Français](https://elie-laloum.github.io/outpost/fr/) · [API](https://elie-laloum.github.io/outpost/reference/) · [Changelog](CHANGELOG.md)
|
|
4
4
|
|
|
5
|
-
Outpost is a TypeScript library for running coding agents in reusable sandboxes, managing their Git workspaces and composing typed workflows.
|
|
5
|
+
Outpost is a TypeScript library for running coding agents in reusable sandboxes, managing their Git workspaces and composing typed workflows. Claude Code and Codex adapters work with Docker, Podman, Vercel, Daytona or explicit host execution.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Get started
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- Node.js **24 or later**, Git, and an existing repository with at least one commit.
|
|
12
|
-
- Docker or Podman for local isolation; the appropriate account and optional SDK for a cloud sandbox.
|
|
13
|
-
- An authenticated coding agent or its API key. Git hosting credentials are not model credentials.
|
|
14
|
-
- ESM imports. TypeScript declarations ship with the package; Node can execute the generated `.ts`/`.mts` scripts directly.
|
|
15
|
-
|
|
16
|
-
## Install and run
|
|
17
|
-
|
|
18
|
-
Build from the source checkout:
|
|
19
|
-
|
|
20
|
-
```sh
|
|
21
|
-
npm ci
|
|
22
|
-
npm run build
|
|
23
|
-
npm pack
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
Install the resulting tarball in your target repository, or use the package from [GitHub Packages](https://github.com/elie-laloum/outpost/packages). Registry configuration is documented in [Operations](docs/operations.md).
|
|
9
|
+
Requires Node.js **24+**, Git, an existing repository with a commit, and the credentials of your chosen agent. This example uses Docker.
|
|
27
10
|
|
|
28
11
|
```sh
|
|
29
|
-
npm install --save-dev
|
|
12
|
+
npm install --save-dev @elie-laloum/outpost
|
|
30
13
|
npx outpost init --yes --agent codex --provider docker --template blank --build
|
|
31
14
|
```
|
|
32
15
|
|
|
33
|
-
Copy `.outpost/.env.example` to `.outpost/.env
|
|
16
|
+
Copy `.outpost/.env.example` to `.outpost/.env` and declare `OPENAI_API_KEY`. An empty declaration inherits the matching process variable. Then run the generated script:
|
|
34
17
|
|
|
35
18
|
```sh
|
|
36
|
-
node .outpost/run.mts "Add validation
|
|
19
|
+
node .outpost/run.mts "Add validation, run tests and commit the change"
|
|
37
20
|
```
|
|
38
21
|
|
|
39
|
-
|
|
22
|
+
Use `run.ts` for a project with `"type": "module"`; initialization prints the exact command.
|
|
40
23
|
|
|
41
|
-
##
|
|
24
|
+
## Use the library
|
|
42
25
|
|
|
43
26
|
```ts
|
|
44
27
|
import { dispatch, codex } from "@elie-laloum/outpost";
|
|
45
|
-
import { docker } from "@elie-laloum/outpost/providers/docker";
|
|
46
28
|
|
|
47
29
|
const result = await dispatch({
|
|
48
30
|
agent: codex(),
|
|
49
|
-
provider: docker(),
|
|
50
31
|
branch: { mode: "integrate" },
|
|
51
|
-
brief: { text: "Fix the failing tests, verify
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
console.log(result.branch, result.commits, result.conversation);
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
`dispatch` provisions a sandbox, executes the agent, collects changes and native conversation files, integrates commits when requested, and closes owned resources. Uncommitted work is retained. Failure paths preserve separate workspaces for inspection.
|
|
58
|
-
|
|
59
|
-
| Branch policy | Behavior |
|
|
60
|
-
| ------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
61
|
-
| `{ mode: "current" }` | Uses the current checkout. This is the mounted/host default. |
|
|
62
|
-
| `{ mode: "named", name: "feature/validation", from: "main" }` | Creates or reuses a managed worktree for that branch. |
|
|
63
|
-
| `{ mode: "integrate", from: "main" }` | Creates a temporary branch and merges its commits into the original host branch. |
|
|
64
|
-
|
|
65
|
-
Remote providers require `named` or `integrate`. `from` is optional. Integration refuses a changed host branch and retains the workspace on conflict. A branch checked out elsewhere produces an actionable error.
|
|
66
|
-
|
|
67
|
-
## Keep a sandbox warm
|
|
68
|
-
|
|
69
|
-
```ts
|
|
70
|
-
import { createSandbox, claude } from "@elie-laloum/outpost";
|
|
71
|
-
|
|
72
|
-
await using sandbox = await createSandbox({
|
|
73
|
-
agent: claude({ model: "sonnet", reasoning: "high" }),
|
|
74
|
-
branch: { mode: "named", name: "feature/settings" },
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const first = await sandbox.dispatch({
|
|
78
|
-
brief: { text: "Implement settings validation." },
|
|
79
|
-
});
|
|
80
|
-
const tests = await sandbox.command({ executable: "npm", arguments: ["test"] });
|
|
81
|
-
if (tests.status !== 0)
|
|
82
|
-
await first.resume({ brief: { text: "Fix the failing tests." } });
|
|
83
|
-
await sandbox.attach();
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Commands return nonzero statuses; agent dispatches throw on agent failure. `AbortSignal`, command deadlines and idle watchdogs stop the current operation while leaving a warm sandbox usable. A sandbox accepts one active operation at a time. Use separate sandboxes for parallel work.
|
|
87
|
-
|
|
88
|
-
The default agent is optional for a warm sandbox. Pass `agent` to individual `sandbox.dispatch` or `sandbox.attach` calls to switch between Codex and Claude, or change models, without recreating the environment. Each job receives only its selected adapter's variables.
|
|
89
|
-
|
|
90
|
-
## Own the workspace separately
|
|
91
|
-
|
|
92
|
-
```ts
|
|
93
|
-
import { openWorkspace, codex, claude } from "@elie-laloum/outpost";
|
|
94
|
-
|
|
95
|
-
await using workspace = await openWorkspace({
|
|
96
|
-
branch: { mode: "named", name: "feature/shared-work" },
|
|
97
|
-
copies: [".env.test"],
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
await workspace.dispatch({
|
|
101
|
-
agent: codex(),
|
|
102
|
-
brief: { text: "Implement the feature and commit." },
|
|
103
|
-
});
|
|
104
|
-
await workspace.dispatch({
|
|
105
|
-
agent: claude(),
|
|
106
|
-
brief: { text: "Review the implementation, test and commit fixes." },
|
|
107
|
-
});
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
A supplied workspace outlives each sandbox. Its owner closes it. Closing a dirty workspace returns `retainedDirectory`; closing a clean managed workspace removes the worktree. Named branches remain. `workspace.sandbox()` and `workspace.attach()` provide the other lifecycles. Warm integration workspaces can call `sandbox.workspace.integrate()` explicitly.
|
|
111
|
-
|
|
112
|
-
## Prompts and iteration
|
|
113
|
-
|
|
114
|
-
Provide exactly one `brief`: literal `text`, or a `file` with optional primitive `values`. Files are reread each pass; inline text is never expanded.
|
|
115
|
-
|
|
116
|
-
```ts
|
|
117
|
-
const result = await dispatch({
|
|
118
|
-
agent: codex(),
|
|
119
|
-
brief: { file: ".outpost/brief.md", values: { OBJECTIVE: "Fix validation" } },
|
|
120
|
-
passes: 5,
|
|
121
|
-
until: ["<outpost>done</outpost>"],
|
|
122
|
-
idleMs: 600_000,
|
|
123
|
-
settleMs: 60_000,
|
|
32
|
+
brief: { text: "Fix the failing tests, verify and commit." },
|
|
124
33
|
});
|
|
34
|
+
console.log(result.branch, result.commits);
|
|
125
35
|
```
|
|
126
36
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
Completion markers stop the loop. If the process hangs after a marker, the completion grace period resets on subsequent output and then stops that command. Every turn includes its duration, native transcript path when available and raw token counts (input, cache read, cache creation, output); the result includes aggregate usage and the matched completion marker.
|
|
37
|
+
Learn about [sandboxes](https://elie-laloum.github.io/outpost/sandboxes/lifecycle/), [workflows](https://elie-laloum.github.io/outpost/workflows/graph/), [providers](https://elie-laloum.github.io/outpost/providers/overview/) and [recovery](https://elie-laloum.github.io/outpost/operations/recovery/) in the English/French documentation. The [roadmap](https://elie-laloum.github.io/outpost/project/roadmap/) describes future work.
|
|
130
38
|
|
|
131
|
-
##
|
|
132
|
-
|
|
133
|
-
```ts
|
|
134
|
-
import { response } from "@elie-laloum/outpost";
|
|
135
|
-
|
|
136
|
-
const result = await dispatch({
|
|
137
|
-
agent: codex(),
|
|
138
|
-
brief: {
|
|
139
|
-
text: "Analyze the repository. Return <report>JSON with an ok boolean</report>.",
|
|
140
|
-
},
|
|
141
|
-
response: response.json({
|
|
142
|
-
tag: "report",
|
|
143
|
-
repairs: 2,
|
|
144
|
-
schema(input) {
|
|
145
|
-
if (
|
|
146
|
-
!input ||
|
|
147
|
-
typeof input !== "object" ||
|
|
148
|
-
!("ok" in input) ||
|
|
149
|
-
typeof input.ok !== "boolean"
|
|
150
|
-
) {
|
|
151
|
-
throw new Error("Expected an ok boolean");
|
|
152
|
-
}
|
|
153
|
-
return { ok: input.ok };
|
|
154
|
-
},
|
|
155
|
-
}),
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
console.log(result.value.ok);
|
|
159
|
-
await result.resume({ brief: { text: "Explain the result." } });
|
|
160
|
-
await result.fork({ brief: { text: "Explore a different solution." } });
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
`response.text` extracts tagged text. `response.json` accepts a function or a Standard Schema validator, including asynchronous validation. Structured responses and conversation continuations require one pass. Repairs resume the same conversation. `ResponseError.recovery` exposes the conversation, commits and recovery paths. `recoveryDetails(error)` also retrieves recovery metadata for other failures without replacing cancellation reasons.
|
|
164
|
-
|
|
165
|
-
Native conversation capture is enabled by default. Transcripts are saved to the host agent's own storage and working-directory fields are rewritten for host resumption. Claude child transcripts are copied on a best-effort basis. Set `saveConversations: false` on an adapter to opt out. Forks create a new conversation; use a separate workspace when filesystem isolation is also required.
|
|
166
|
-
|
|
167
|
-
Each cold `dispatch({ passes })` provisions a fresh sandbox per pass. `sandbox.dispatch({ passes })` deliberately reuses its environment. Relative brief filenames resolve from the caller’s working directory. Cold result `resume`/`fork` accepts new branch/provider/hook settings; warm result methods retain the sandbox’s settings. See [1.1 migration notes](docs/migration-1.1.md).
|
|
168
|
-
|
|
169
|
-
## Typed workflows
|
|
170
|
-
|
|
171
|
-
```ts
|
|
172
|
-
import { task, workflow } from "@elie-laloum/outpost";
|
|
173
|
-
|
|
174
|
-
const inspect = task({
|
|
175
|
-
key: "inspect",
|
|
176
|
-
perform: async () => ({ ready: true }),
|
|
177
|
-
});
|
|
178
|
-
const implement = task({
|
|
179
|
-
key: "implement",
|
|
180
|
-
after: [inspect],
|
|
181
|
-
retry: { attempts: 2, delayMs: 500 },
|
|
182
|
-
perform: async (context) => context.value(inspect).ready,
|
|
183
|
-
});
|
|
184
|
-
const result = await workflow("delivery", [inspect, implement]).start({
|
|
185
|
-
concurrency: 2,
|
|
186
|
-
});
|
|
187
|
-
result.unwrap();
|
|
188
|
-
console.log(result.value(implement));
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
The graph validates dependencies and cycles before running. Results are typed and scoped to one execution. Conditions, cooperative cancellation, retries, concurrency limits, observer events and Mermaid diagrams are built in. `agentTask`, `commandTask` and `isolatedTask` connect workflows to sandbox operations. [Workflow guide](docs/workflows.md).
|
|
192
|
-
|
|
193
|
-
## Issue campaigns
|
|
194
|
-
|
|
195
|
-
`campaign({ agent, provider, backlog })` loads a backlog, validates a typed plan, allocates a branch per issue and bounds parallel implementation. Review shares each issue sandbox. A merge phase validates the combined work; issue closure occurs only after integration into the host branch. The backlog is refreshed for the next cycle. GitHub and Beads connectors expose list, detail and close operations; custom trackers implement `Backlog`.
|
|
196
|
-
|
|
197
|
-
[Campaign configuration and starter examples](docs/workflows.md#issue-campaigns).
|
|
198
|
-
|
|
199
|
-
## Configuration and extension
|
|
200
|
-
|
|
201
|
-
Provider settings cover environment variables, file/directory mounts, networks, resource limits, devices, extra groups and SELinux labels. Hooks run at workspace readiness and after sandbox creation. The two post-creation hook groups run concurrently. Host commands run in order; sandbox commands run concurrently and cancel their siblings on failure. [Complete API](docs/api.md) and [provider reference](docs/providers.md).
|
|
202
|
-
|
|
203
|
-
Outpost reads only `.outpost/.env`. Nonempty file values win; empty declarations inherit matching process values. The repository-root `.env` is not imported. Explicit provider and adapter variables override those values. The same variable cannot be declared by both provider and adapter. Logs default to `.outpost/logs`, with `false`, `"stdout"`, custom files and verbose modes available.
|
|
204
|
-
|
|
205
|
-
Implement `AgentAdapter` to add an agent. Implement `SandboxProvider` directly or use `mountedProvider`/`remoteProvider` to add an execution backend. Domain contracts do not depend on a specific cloud SDK. [Architecture](docs/architecture.md).
|
|
206
|
-
|
|
207
|
-
## Verification and releases
|
|
39
|
+
## Development
|
|
208
40
|
|
|
209
41
|
```sh
|
|
210
42
|
npm ci
|
|
211
43
|
npm run check
|
|
212
44
|
npm run coverage
|
|
213
45
|
npm run test:package
|
|
46
|
+
npm ci --prefix docs
|
|
47
|
+
npm run docs:sync
|
|
48
|
+
npm run docs:build
|
|
49
|
+
npm run docs:test
|
|
214
50
|
```
|
|
215
51
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
Version tags are created on GitLab and mirrored. The release workflow validates the tag, runs CI, publishes GitHub Packages and attaches the installable tarball to a GitHub release. Optional npm publication uses trusted publishing when enabled. [Release procedure and registry setup](docs/operations.md).
|
|
219
|
-
|
|
220
|
-
## Execution boundary
|
|
221
|
-
|
|
222
|
-
Docker and Podman mount the selected worktree and Git metadata. Agents can edit that repository and its Git state. This protects unrelated host paths but is not a boundary against a hostile agent attacking the shared repository. Mount only what the task needs. `local()` runs directly on your machine without isolation. Cloud providers synchronize via Git bundles and file transfers; concurrent host changes cause a recovery error instead of being overwritten.
|
|
223
|
-
|
|
224
|
-
Logs, conversation files and recovery files can contain private prompts or source. Runtime folders and `.env` are excluded from version control. See [Security](SECURITY.md) for the precise boundaries.
|
|
52
|
+
[GitLab](https://gitlab.elielaloum.com/elielaloum/outpost) is the canonical repository. [GitHub](https://github.com/elie-laloum/outpost) is the mirror running tests, package releases and GitHub Pages. Documentation is validated on main and deployed only after a successful stable release.
|
|
225
53
|
|
|
226
|
-
|
|
54
|
+
See [SECURITY.md](SECURITY.md) for execution boundaries and [LICENSE](LICENSE) for the MIT license.
|
|
@@ -5,6 +5,7 @@ ARG AGENT_GID=1000
|
|
|
5
5
|
RUN apt-get update && apt-get install -y --no-install-recommends git gh openssh-client ca-certificates curl procps util-linux python3 && rm -rf /var/lib/apt/lists/*
|
|
6
6
|
RUN npm install -g --allow-scripts=@anthropic-ai/claude-code @openai/codex@${agentVersions.codex} @anthropic-ai/claude-code@${agentVersions.claude}
|
|
7
7
|
RUN groupmod -o -g "$AGENT_GID" node && usermod -o -u "$AGENT_UID" -g "$AGENT_GID" node
|
|
8
|
+
RUN mkdir -p /home/agent && chown "$AGENT_UID:$AGENT_GID" /home/agent && chmod 700 /home/agent
|
|
8
9
|
ENV HOME=/home/agent
|
|
9
10
|
USER $AGENT_UID:$AGENT_GID
|
|
10
11
|
WORKDIR /workspace
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scaffold.constants.js","sourceRoot":"","sources":["../../src/cli/scaffold.constants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEzD,MAAM,CAAC,MAAM,WAAW,GAAG;;;;6EAIkD,aAAa,CAAC,KAAK,8BAA8B,aAAa,CAAC,MAAM
|
|
1
|
+
{"version":3,"file":"scaffold.constants.js","sourceRoot":"","sources":["../../src/cli/scaffold.constants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEzD,MAAM,CAAC,MAAM,WAAW,GAAG;;;;6EAIkD,aAAa,CAAC,KAAK,8BAA8B,aAAa,CAAC,MAAM;;;;;;CAMjJ,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAsB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;AACtE,MAAM,CAAC,MAAM,kBAAkB,GAAsB;IACnD,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,SAAS;IACT,OAAO;CACR,CAAC;AACF,MAAM,CAAC,MAAM,kBAAkB,GAAsB;IACnD,OAAO;IACP,SAAS;IACT,QAAQ;IACR,MAAM;IACN,aAAa;CACd,CAAC;AACF,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IAClD,QAAQ;IACR,OAAO;IACP,QAAQ;CACT,CAAC;AACF,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IAClD,KAAK;IACL,MAAM;IACN,MAAM;IACN,KAAK;CACN,CAAC;AACF,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAC1B,CAAC,WAAW,EAAE,MAAM,CAAC;IACrB,CAAC,UAAU,EAAE,KAAK,CAAC;IACnB,CAAC,WAAW,EAAE,KAAK,CAAC;CACZ,CAAC;AACX,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,MAAM,EAAE,EAAE;IACV,MAAM,EAAE,EAAE;IACV,KAAK,EAAE,EAAE;IACT,MAAM,EAAE,CAAC,iBAAiB,CAAC;IAC3B,OAAO,EAAE,CAAC,cAAc,CAAC;CACjB,CAAC","sourcesContent":["import { agentVersions } from \"../providers/versions.ts\";\n\nexport const imageRecipe = `FROM node:24-bookworm-slim\nARG AGENT_UID=1000\nARG AGENT_GID=1000\nRUN apt-get update && apt-get install -y --no-install-recommends git gh openssh-client ca-certificates curl procps util-linux python3 && rm -rf /var/lib/apt/lists/*\nRUN npm install -g --allow-scripts=@anthropic-ai/claude-code @openai/codex@${agentVersions.codex} @anthropic-ai/claude-code@${agentVersions.claude}\nRUN groupmod -o -g \"$AGENT_GID\" node && usermod -o -u \"$AGENT_UID\" -g \"$AGENT_GID\" node\nRUN mkdir -p /home/agent && chown \"$AGENT_UID:$AGENT_GID\" /home/agent && chmod 700 /home/agent\nENV HOME=/home/agent\nUSER $AGENT_UID:$AGENT_GID\nWORKDIR /workspace\n`;\n\nexport const supportedAgents: readonly string[] = [\"codex\", \"claude\"];\nexport const supportedProviders: readonly string[] = [\n \"docker\",\n \"podman\",\n \"vercel\",\n \"daytona\",\n \"local\",\n];\nexport const supportedTemplates: readonly string[] = [\n \"blank\",\n \"iterate\",\n \"review\",\n \"plan\",\n \"plan-review\",\n];\nexport const supportedTrackers: readonly string[] = [\n \"github\",\n \"beads\",\n \"custom\",\n];\nexport const supportedManagers: readonly string[] = [\n \"npm\",\n \"pnpm\",\n \"yarn\",\n \"bun\",\n];\nexport const managerLocks = [\n [\"pnpm-lock.yaml\", \"pnpm\"],\n [\"yarn.lock\", \"yarn\"],\n [\"bun.lock\", \"bun\"],\n [\"bun.lockb\", \"bun\"],\n] as const;\nexport const providerPackages = {\n docker: [],\n podman: [],\n local: [],\n vercel: [\"@vercel/sandbox\"],\n daytona: [\"@daytona/sdk\"],\n} as const;\n"]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { pipeline } from "node:stream/promises";
|
|
3
|
+
import { OutpostError } from "../domain/errors.js";
|
|
4
|
+
import { processDefaults } from "./process.constants.js";
|
|
5
|
+
import { transfer } from "./transfer.js";
|
|
6
|
+
export function binaryExecutor(streams) {
|
|
7
|
+
return (command) => transfer(command, async (signal) => {
|
|
8
|
+
signal.throwIfAborted();
|
|
9
|
+
const child = spawn(command.executable, [...(command.arguments ?? [])], {
|
|
10
|
+
cwd: command.directory,
|
|
11
|
+
env: { ...process.env, ...command.variables },
|
|
12
|
+
windowsHide: true,
|
|
13
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
14
|
+
signal,
|
|
15
|
+
killSignal: "SIGKILL",
|
|
16
|
+
});
|
|
17
|
+
let stderr = "";
|
|
18
|
+
child.stderr.on("data", (data) => {
|
|
19
|
+
stderr = (stderr + data.toString("utf8")).slice(-processDefaults.retainBytes);
|
|
20
|
+
});
|
|
21
|
+
const completion = new Promise((resolve, reject) => {
|
|
22
|
+
child.on("error", reject);
|
|
23
|
+
child.on("close", (status) => {
|
|
24
|
+
if (status !== 0)
|
|
25
|
+
reject(new OutpostError("process", `${command.executable} exited with status ${status}`, { status, stderr }));
|
|
26
|
+
else
|
|
27
|
+
resolve({ status: 0, stdout: "", stderr });
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
const pending = [completion];
|
|
31
|
+
if (streams.input)
|
|
32
|
+
pending.push(pipeline(streams.input, child.stdin, { signal }));
|
|
33
|
+
else
|
|
34
|
+
child.stdin.end();
|
|
35
|
+
if (streams.output)
|
|
36
|
+
pending.push(pipeline(child.stdout, streams.output, { signal }));
|
|
37
|
+
else
|
|
38
|
+
child.stdout.resume();
|
|
39
|
+
try {
|
|
40
|
+
await Promise.all(pending);
|
|
41
|
+
return await completion;
|
|
42
|
+
}
|
|
43
|
+
catch (cause) {
|
|
44
|
+
child.kill("SIGKILL");
|
|
45
|
+
streams.input?.destroy();
|
|
46
|
+
streams.output?.destroy();
|
|
47
|
+
await Promise.allSettled(pending);
|
|
48
|
+
signal.throwIfAborted();
|
|
49
|
+
throw cause;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=binary-process.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binary-process.js","sourceRoot":"","sources":["../../src/infrastructure/binary-process.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"node:child_process\";\nimport { pipeline } from \"node:stream/promises\";\nimport type { CommandResult } from \"../domain/command.types.ts\";\nimport { OutpostError } from \"../domain/errors.ts\";\nimport type { BinaryStreams } from \"./binary-process.types.ts\";\nimport { processDefaults } from \"./process.constants.ts\";\nimport type { Executor } from \"./process.types.ts\";\nimport { transfer } from \"./transfer.ts\";\n\nexport function binaryExecutor(streams: BinaryStreams): Executor {\n return (command) =>\n transfer(command, async (signal) => {\n signal.throwIfAborted();\n const child = spawn(command.executable, [...(command.arguments ?? [])], {\n cwd: command.directory,\n env: { ...process.env, ...command.variables },\n windowsHide: true,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n signal,\n killSignal: \"SIGKILL\",\n });\n let stderr = \"\";\n child.stderr.on(\"data\", (data: Buffer) => {\n stderr = (stderr + data.toString(\"utf8\")).slice(\n -processDefaults.retainBytes,\n );\n });\n const completion = new Promise<CommandResult>((resolve, reject) => {\n child.on(\"error\", reject);\n child.on(\"close\", (status) => {\n if (status !== 0)\n reject(\n new OutpostError(\n \"process\",\n `${command.executable} exited with status ${status}`,\n { status, stderr },\n ),\n );\n else resolve({ status: 0, stdout: \"\", stderr });\n });\n });\n const pending: Promise<unknown>[] = [completion];\n if (streams.input)\n pending.push(pipeline(streams.input, child.stdin, { signal }));\n else child.stdin.end();\n if (streams.output)\n pending.push(pipeline(child.stdout, streams.output, { signal }));\n else child.stdout.resume();\n try {\n await Promise.all(pending);\n return await completion;\n } catch (cause) {\n child.kill(\"SIGKILL\");\n streams.input?.destroy();\n streams.output?.destroy();\n await Promise.allSettled(pending);\n signal.throwIfAborted();\n throw cause;\n }\n });\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binary-process.types.js","sourceRoot":"","sources":["../../src/infrastructure/binary-process.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { Readable, Writable } from \"node:stream\";\n\nexport interface BinaryStreams {\n readonly input?: Readable;\n readonly output?: Writable;\n}\n"]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { chmod, copyFile, rm, lstat, mkdir, readdir, readlink, symlink, } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, parse, resolve } from "node:path";
|
|
3
|
+
import { OutpostError } from "../domain/errors.js";
|
|
4
|
+
export async function transferDestination(destination) {
|
|
5
|
+
const absolute = resolve(destination);
|
|
6
|
+
for (let path = absolute; path !== parse(path).root; path = dirname(path)) {
|
|
7
|
+
const info = await lstat(path).catch((cause) => {
|
|
8
|
+
if (cause.code === "ENOENT")
|
|
9
|
+
return undefined;
|
|
10
|
+
throw cause;
|
|
11
|
+
});
|
|
12
|
+
if (info?.isSymbolicLink())
|
|
13
|
+
throw new OutpostError("provider", "Transfer destination traverses a symlink", { destination });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function copyTransfer(source, destination, signal) {
|
|
17
|
+
signal.throwIfAborted();
|
|
18
|
+
await transferDestination(destination);
|
|
19
|
+
const info = await lstat(source);
|
|
20
|
+
if (info.isDirectory()) {
|
|
21
|
+
await mkdir(destination, { recursive: true });
|
|
22
|
+
for (const name of await readdir(source))
|
|
23
|
+
await copyTransfer(join(source, name), join(destination, name), signal);
|
|
24
|
+
await chmod(destination, info.mode & 0o777);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
28
|
+
if (info.isSymbolicLink()) {
|
|
29
|
+
await symlink(await readlink(source), destination);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (!info.isFile())
|
|
33
|
+
throw new OutpostError("provider", "Unsupported transfer file type", {
|
|
34
|
+
source,
|
|
35
|
+
});
|
|
36
|
+
await copyFile(source, destination);
|
|
37
|
+
await chmod(destination, info.mode & 0o777);
|
|
38
|
+
}
|
|
39
|
+
export async function removeTransferDirectory(root) {
|
|
40
|
+
async function writable(path) {
|
|
41
|
+
const info = await lstat(path);
|
|
42
|
+
if (!info.isDirectory() || info.isSymbolicLink())
|
|
43
|
+
return;
|
|
44
|
+
await chmod(path, 0o700);
|
|
45
|
+
for (const name of await readdir(path))
|
|
46
|
+
await writable(join(path, name));
|
|
47
|
+
}
|
|
48
|
+
await writable(root);
|
|
49
|
+
await rm(root, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=transfer-copy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transfer-copy.js","sourceRoot":"","sources":["../../src/infrastructure/transfer-copy.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n chmod,\n copyFile,\n rm,\n lstat,\n mkdir,\n readdir,\n readlink,\n symlink,\n} from \"node:fs/promises\";\nimport { dirname, join, parse, resolve } from \"node:path\";\nimport { OutpostError } from \"../domain/errors.ts\";\n\nexport async function transferDestination(destination: string): Promise<void> {\n const absolute = resolve(destination);\n for (let path = absolute; path !== parse(path).root; path = dirname(path)) {\n const info = await lstat(path).catch((cause: NodeJS.ErrnoException) => {\n if (cause.code === \"ENOENT\") return undefined;\n throw cause;\n });\n if (info?.isSymbolicLink())\n throw new OutpostError(\n \"provider\",\n \"Transfer destination traverses a symlink\",\n { destination },\n );\n }\n}\n\nexport async function copyTransfer(\n source: string,\n destination: string,\n signal: AbortSignal,\n): Promise<void> {\n signal.throwIfAborted();\n await transferDestination(destination);\n const info = await lstat(source);\n if (info.isDirectory()) {\n await mkdir(destination, { recursive: true });\n for (const name of await readdir(source))\n await copyTransfer(join(source, name), join(destination, name), signal);\n await chmod(destination, info.mode & 0o777);\n return;\n }\n await mkdir(dirname(destination), { recursive: true });\n if (info.isSymbolicLink()) {\n await symlink(await readlink(source), destination);\n return;\n }\n if (!info.isFile())\n throw new OutpostError(\"provider\", \"Unsupported transfer file type\", {\n source,\n });\n await copyFile(source, destination);\n await chmod(destination, info.mode & 0o777);\n}\n\nexport async function removeTransferDirectory(root: string): Promise<void> {\n async function writable(path: string): Promise<void> {\n const info = await lstat(path);\n if (!info.isDirectory() || info.isSymbolicLink()) return;\n await chmod(path, 0o700);\n for (const name of await readdir(path)) await writable(join(path, name));\n }\n await writable(root);\n await rm(root, { recursive: true, force: true });\n}\n"]}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Command } from "../domain/command.types.ts";
|
|
2
|
+
import type { ContainerRuntime } from "./container.types.ts";
|
|
3
|
+
export declare function containerArchive(runtime: ContainerRuntime, local: Command, remote: Command, upload: boolean, signal: AbortSignal): Promise<void>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { PassThrough } from "node:stream";
|
|
2
|
+
import { binaryExecutor } from "../infrastructure/binary-process.js";
|
|
3
|
+
import { containerCommand } from "./container-command.js";
|
|
4
|
+
export async function containerArchive(runtime, local, remote, upload, signal) {
|
|
5
|
+
const pipe = new PassThrough();
|
|
6
|
+
const stop = new AbortController();
|
|
7
|
+
const combined = AbortSignal.any([signal, stop.signal]);
|
|
8
|
+
const host = binaryExecutor(upload ? { output: pipe } : { input: pipe });
|
|
9
|
+
const guest = containerCommand({
|
|
10
|
+
...runtime,
|
|
11
|
+
executor: binaryExecutor(upload ? { input: pipe } : { output: pipe }),
|
|
12
|
+
});
|
|
13
|
+
const pending = [
|
|
14
|
+
host({ ...local, signal: combined }),
|
|
15
|
+
guest({ ...remote, signal: combined }),
|
|
16
|
+
];
|
|
17
|
+
try {
|
|
18
|
+
await Promise.all(pending);
|
|
19
|
+
}
|
|
20
|
+
catch (cause) {
|
|
21
|
+
stop.abort(cause);
|
|
22
|
+
pipe.destroy();
|
|
23
|
+
await Promise.allSettled(pending);
|
|
24
|
+
throw cause;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=container-archive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"container-archive.js","sourceRoot":"","sources":["../../src/providers/container-archive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG1D,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,OAAyB,EACzB,KAAc,EACd,MAAe,EACf,MAAe,EACf,MAAmB;IAEnB,MAAM,IAAI,GAAG,IAAI,WAAW,EAAE,CAAC;IAC/B,MAAM,IAAI,GAAG,IAAI,eAAe,EAAE,CAAC;IACnC,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IACxD,MAAM,IAAI,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACzE,MAAM,KAAK,GAAG,gBAAgB,CAAC;QAC7B,GAAG,OAAO;QACV,QAAQ,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;KACtE,CAAC,CAAC;IACH,MAAM,OAAO,GAAG;QACd,IAAI,CAAC,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;QACpC,KAAK,CAAC,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;KACvC,CAAC;IACF,IAAI,CAAC;QACH,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAClB,IAAI,CAAC,OAAO,EAAE,CAAC;QACf,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QAClC,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC","sourcesContent":["import { PassThrough } from \"node:stream\";\nimport type { Command } from \"../domain/command.types.ts\";\nimport { binaryExecutor } from \"../infrastructure/binary-process.ts\";\nimport { containerCommand } from \"./container-command.ts\";\nimport type { ContainerRuntime } from \"./container.types.ts\";\n\nexport async function containerArchive(\n runtime: ContainerRuntime,\n local: Command,\n remote: Command,\n upload: boolean,\n signal: AbortSignal,\n): Promise<void> {\n const pipe = new PassThrough();\n const stop = new AbortController();\n const combined = AbortSignal.any([signal, stop.signal]);\n const host = binaryExecutor(upload ? { output: pipe } : { input: pipe });\n const guest = containerCommand({\n ...runtime,\n executor: binaryExecutor(upload ? { input: pipe } : { output: pipe }),\n });\n const pending = [\n host({ ...local, signal: combined }),\n guest({ ...remote, signal: combined }),\n ];\n try {\n await Promise.all(pending);\n } catch (cause) {\n stop.abort(cause);\n pipe.destroy();\n await Promise.allSettled(pending);\n throw cause;\n }\n}\n"]}
|
|
@@ -37,9 +37,9 @@ export function containerCommand(runtime) {
|
|
|
37
37
|
executable: engine,
|
|
38
38
|
arguments: [
|
|
39
39
|
...flags,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
...(command.interactive && process.stdin.isTTY
|
|
41
|
+
? []
|
|
42
|
+
: ["setsid", "--wait"]),
|
|
43
43
|
"sh",
|
|
44
44
|
"-c",
|
|
45
45
|
wrapper,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"container-command.js","sourceRoot":"","sources":["../../src/providers/container-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAE9D,OAAO,EAAE,KAAK,EAAE,MAAM,8BAA8B,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG7D,MAAM,UAAU,gBAAgB,CAC9B,OAAyB;IAEzB,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC;IAC9E,OAAO,KAAK,EAAE,OAAO,EAAE,EAAE;QACvB,IAAI,QAAQ,EAAE;YACZ,MAAM,IAAI,YAAY,CAAC,UAAU,EAAE,6BAA6B,CAAC,CAAC;QACpE,OAAO,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC;QACjC,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC;QACxB,MAAM,OAAO,GAAG,gBAAgB,EAAE,MAAM,CAAC;QACzC,MAAM,SAAS,GAAG,EAAE,GAAG,GAAG,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;QACnD,SAAS,CACP,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CACnC,0BAA0B,CAAC,IAAI,CAAC,GAAG,CAAC,CACrC,EACD,yBAAyB,CAC1B,CAAC;QACF,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CACpC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;YAClD,iBAAiB,KAAK,EAAE;YACxB,KAAK;SACN,CAAC,CACH,CAAC;QACF,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC;aACnC,GAAG,CACF,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CACb,UAAU,GAAG,oBAAoB,KAAK,0BAA0B,KAAK,GAAG,CAC3E;aACA,IAAI,CAAC,GAAG,CAAC,CAAC;QACb,MAAM,KAAK,GAAG;YACZ,MAAM;YACN,OAAO,CAAC,WAAW,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI;YACzD,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9C,WAAW;YACX,OAAO,CAAC,SAAS,IAAI,IAAI;YACzB,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;YAC5D,IAAI;SACL,CAAC;QACF,MAAM,OAAO,GAAG,GAAG,OAAO,cAAc,KAAK,CAAC,OAAO,CAAC,eAAe,KAAK,CAAC,OAAO,GAAG,SAAS,CAAC,yBAAyB,CAAC;QACzH,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,GAAG,OAAO,CAAC;QACzD,IAAI,WAAW,GAAG,KAAK,CAAC;QACxB,IAAI,CAAC;YACH,OAAO,MAAM,QAAQ,CAAC;gBACpB,GAAG,UAAU;gBACb,UAAU,EAAE,MAAM;gBAClB,SAAS,EAAE;oBACT,GAAG,KAAK;oBACR,
|
|
1
|
+
{"version":3,"file":"container-command.js","sourceRoot":"","sources":["../../src/providers/container-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { invariant, OutpostError } from \"../domain/errors.ts\";\nimport type { SandboxLease } from \"../domain/sandbox.types.ts\";\nimport { quote } from \"../infrastructure/process.ts\";\nimport { containerDefaults } from \"./container.constants.ts\";\nimport type { ContainerRuntime } from \"./container.types.ts\";\n\nexport function containerCommand(\n runtime: ContainerRuntime,\n): SandboxLease[\"invoke\"] {\n const { engine, config, executor, root, name, env, call, isClosed } = runtime;\n return async (command) => {\n if (isClosed())\n throw new OutpostError(\"provider\", \"Container sandbox is closed\");\n command.signal?.throwIfAborted();\n const id = randomUUID();\n const pidFile = `/tmp/outpost-${id}.pid`;\n const variables = { ...env, ...command.variables };\n invariant(\n Object.keys(variables).every((key) =>\n /^[A-Za-z_][A-Za-z0-9_]*$/.test(key),\n ),\n \"Invalid environment key\",\n );\n const environment = Object.fromEntries(\n Object.entries(variables).map(([, value], index) => [\n `OUTPOST_VALUE_${index}`,\n value,\n ]),\n );\n const exports = Object.keys(variables)\n .map(\n (key, index) =>\n `export ${key}=\"$OUTPOST_VALUE_${index}\"; unset OUTPOST_VALUE_${index};`,\n )\n .join(\" \");\n const flags = [\n \"exec\",\n command.interactive && process.stdin.isTTY ? \"-it\" : \"-i\",\n ...(command.elevated ? [\"--user\", \"0:0\"] : []),\n \"--workdir\",\n command.directory ?? root,\n ...Object.keys(environment).flatMap((key) => [\"--env\", key]),\n name,\n ];\n const wrapper = `${exports} echo $$ > ${quote(pidFile)}; test ! -f ${quote(pidFile + \".cancel\")} || exit 130; exec \"$@\"`;\n const { directory: _directory, ...invocation } = command;\n let interrupted = false;\n try {\n return await executor({\n ...invocation,\n executable: engine,\n arguments: [\n ...flags,\n ...(command.interactive && process.stdin.isTTY\n ? []\n : [\"setsid\", \"--wait\"]),\n \"sh\",\n \"-c\",\n wrapper,\n \"outpost\",\n command.executable,\n ...(command.arguments ?? []),\n ],\n variables: environment,\n retain:\n command.retain ?? config.retain ?? containerDefaults.retainBytes,\n });\n } catch (cause) {\n interrupted = true;\n try {\n await call([\n \"exec\",\n ...(command.elevated ? [\"--user\", \"0:0\"] : []),\n name,\n \"sh\",\n \"-c\",\n `touch ${quote(pidFile + \".cancel\")}; if [ -f ${quote(pidFile)} ]; then p=$(cat ${quote(pidFile)}); kill -TERM -\"$p\" 2>/dev/null || true; sleep 0.1; kill -KILL -\"$p\" 2>/dev/null || true; fi`,\n ]);\n } catch (cleanup) {\n throw new AggregateError(\n [cause, cleanup],\n \"Command cancellation could not be confirmed\",\n );\n }\n throw cause;\n } finally {\n if (!isClosed() && !interrupted)\n await call([\"exec\", name, \"rm\", \"-f\", pidFile]).catch(() => undefined);\n }\n };\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { SandboxLease } from "../domain/sandbox.types.ts";
|
|
2
|
+
import { containerArchive } from "./container-archive.ts";
|
|
2
3
|
import type { ContainerRuntime } from "./container.types.ts";
|
|
3
|
-
export declare function containerFiles(
|
|
4
|
+
export declare function containerFiles(runtime: ContainerRuntime, archive?: typeof containerArchive): Pick<SandboxLease, "upload" | "download">;
|