Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
c51c4b6
docs: ADR for a three-state sandbox network policy
KB-syntheticsciences Aug 10, 2026
a887206
docs: mark ADR 0002's forward decisions apart from its measurements
KB-syntheticsciences Aug 10, 2026
74d9388
feat(sandbox): allowlist egress proxy and loopback shim
KB-syntheticsciences Aug 10, 2026
e55ce20
feat(sandbox): three-state network policy with a socket egress route
KB-syntheticsciences Aug 10, 2026
6d8d9a3
fix(sandbox): close the egress-as-write-escape gap, correct stale com…
KB-syntheticsciences Aug 10, 2026
c602246
fix(sandbox): normalize egress through dedupe before the over-broad c…
KB-syntheticsciences Aug 10, 2026
05cb7db
feat(sandbox): bridge loopback to the egress socket inside the namespace
KB-syntheticsciences Aug 10, 2026
d2633b6
fix(sandbox): shim must not touch CLI middleware, resolve entry struc…
KB-syntheticsciences Aug 10, 2026
4ebae67
fix(sandbox): bind the shim's launcher explicitly, don't rely on its …
KB-syntheticsciences Aug 10, 2026
efc101e
fix(sandbox): bind the interpreter too, and stop shadowing writable o…
KB-syntheticsciences Aug 10, 2026
412cc8d
fix(sandbox): bundle the dev egress shim so it resolves nothing from …
KB-syntheticsciences Aug 10, 2026
312dccd
fix(sandbox): classify the shim bundle's imports against the real bui…
KB-syntheticsciences Aug 10, 2026
d368520
feat(sandbox): egress proxy lifecycle and allowlist config
KB-syntheticsciences Aug 10, 2026
f048be8
fix(sandbox): wire the egress proxy into every wrapArgv/plan caller
KB-syntheticsciences Aug 10, 2026
ca6edf8
fix(sandbox): honour write backpressure, unlatch proxy start, cut spa…
KB-syntheticsciences Aug 10, 2026
3bfc983
fix(sandbox): one upstream per client, and none left behind
KB-syntheticsciences Aug 10, 2026
de016f5
fix(sandbox): bound what one client can make the host allocate
KB-syntheticsciences Aug 10, 2026
e509136
fix(sandbox): wire allowHosts, widen the CLI network enum, unify egre…
KB-syntheticsciences Aug 10, 2026
fce582b
test(sandbox): prove egress is bounded and is the only route
KB-syntheticsciences Aug 10, 2026
00159ab
fix(sandbox): give curl headroom under the outer test timeout
KB-syntheticsciences Aug 10, 2026
30934e8
fix(sandbox): ro-bind the egress socket so a sandboxed process can't …
KB-syntheticsciences Aug 10, 2026
c0a7b15
fix(sandbox): let the settings GUI represent allowlist, add allowHost…
KB-syntheticsciences Aug 10, 2026
c7f1dd0
docs(sandbox): correct macOS/allowlist claims across ADR, docs, and f…
KB-syntheticsciences Aug 10, 2026
dfbe20e
feat(sandbox): macOS seatbelt support for network:"allowlist"
KB-syntheticsciences Aug 11, 2026
ec76353
fix(sandbox): darwin proxy must listen on TCP directly, not bridge to…
KB-syntheticsciences Aug 11, 2026
4ed60ed
fix(sandbox): fix round 1 — match the ADR's seatbelt reference shape,…
KB-syntheticsciences Aug 11, 2026
e584696
docs(sandbox): record seatbeltProfile's network-bind/inbound residual…
KB-syntheticsciences Aug 11, 2026
8d0f5f4
docs: Windows sandbox design — AppContainer plus a broker
KB-syntheticsciences Aug 11, 2026
6758a29
test(sandbox): first-ever macOS CI leg for seatbelt network:"allowlist"
KB-syntheticsciences Aug 11, 2026
3ae4849
fix: make the shim readiness cap a deadline, and pin platform in tests
KB-syntheticsciences Aug 11, 2026
17ba33a
Merge branch 'main' into feat/sandbox-network-policy
KB-syntheticsciences Aug 11, 2026
d0527a6
test: poll on the asserted predicate in the allowHosts round-trip
KB-syntheticsciences Aug 11, 2026
96cf4e2
test: pip install under network "allowlist", live, on both backends
KB-syntheticsciences Aug 11, 2026
2cfdec4
docs: carry the execution spec onto the install branch, corrected
KB-syntheticsciences Aug 11, 2026
5f004d1
feat(package): refuse shell installers in the bash tool
KB-syntheticsciences Aug 11, 2026
25a5270
feat(package): inject the package capability contract into every request
KB-syntheticsciences Aug 11, 2026
89e6a37
feat(package): environment manifest, derived directory, and per-env lock
KB-syntheticsciences Aug 11, 2026
8dbbb5d
feat(package): PEP 508 subset parser, canonical pattern, credential r…
KB-syntheticsciences Aug 11, 2026
4f27830
fix(test): accept both npm pack --json shapes
KB-syntheticsciences Aug 12, 2026
6818dfe
feat(package): installer ladder, sandboxed install, failure translation
KB-syntheticsciences Aug 12, 2026
933a882
feat(package): the package_install tool and its approval card
KB-syntheticsciences Aug 12, 2026
3d698e6
feat(package): bind kernels to environments, restart on non-additive …
KB-syntheticsciences Aug 12, 2026
b4ea2cf
feat(package): report the real environment inventory to the agent
KB-syntheticsciences Aug 12, 2026
28701e2
feat(package): async dispatch with reconcile-on-restart
KB-syntheticsciences Aug 12, 2026
e0bf9dc
feat(package): R environment support via install.packages
KB-syntheticsciences Aug 12, 2026
1e27421
test(package): the merge gate, stated as an executable assertion
KB-syntheticsciences Aug 12, 2026
c41e02d
ci: configure a git identity for the sandbox job
KB-syntheticsciences Aug 12, 2026
5dabeef
test(package): wait for a spawned pid to actually be gone before clai…
KB-syntheticsciences Aug 12, 2026
4c16818
test(package): spawn a portable binary for the dead-pid claim
KB-syntheticsciences Aug 12, 2026
3f129f0
docs: add a Windows AppContainer probe for the four unverified claims
KB-syntheticsciences Aug 12, 2026
be3eb94
docs: measure the Windows AppContainer design — the core claim was wrong
KB-syntheticsciences Aug 12, 2026
882cbd6
refactor(package): name the CRAN index, and assert it is allowlisted
KB-syntheticsciences Aug 12, 2026
9247098
fix(package): the first install must not strip the host's scientific …
KB-syntheticsciences Aug 12, 2026
00de92f
feat(package): show what the installer is doing while it does it
KB-syntheticsciences Aug 12, 2026
a403c2a
perf(package): share the wheel cache across environments
KB-syntheticsciences Aug 12, 2026
53b6895
test(package): exercise source builds and a real interrupt
KB-syntheticsciences Aug 12, 2026
6417b78
test(package): verify R for the first time, in CI
KB-syntheticsciences Aug 12, 2026
448c2b7
fix(package): give the async install path a way to report its outcome
KB-syntheticsciences Aug 12, 2026
23aae97
fix(package): decide restarts from what the kernel sees, not what the…
KB-syntheticsciences Aug 12, 2026
8eebad5
fix(compute): SSH remote jobs work again under the allowlist default
KB-syntheticsciences Aug 12, 2026
46d5f60
feat(sandbox): Windows AppContainer composition, without claiming it …
KB-syntheticsciences Aug 12, 2026
449b64f
docs: probe writes an untruncatable report, plus a Windows runbook
KB-syntheticsciences Aug 12, 2026
a0a40c8
fix(package,cli): three bugs a real Windows run found
KB-syntheticsciences Aug 12, 2026
6df1a89
feat(sandbox): the Windows AppContainer launcher
KB-syntheticsciences Aug 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,70 @@ jobs:
shell: bash
working-directory: backend/cli

# Task 7 gave macOS a seatbelt profile for network:"allowlist" — an SBPL
# profile plus an authenticated loopback proxy, built and unit-tested
# entirely from Linux with the platform injected, because no Mac exists on
# this project. `sandbox-exec` (macOS) and `bwrap --unshare-net` (Linux)
# are unrelated OS-level mechanisms underneath the same `Sandbox` API, so a
# green Linux run says nothing about whether seatbelt actually confines a
# real process the way the profile text claims — only this leg's macOS run
# does. See test/sandbox/egress-live-seatbelt.test.ts's doc comment for
# exactly what a red run here would mean.
sandbox:
name: Sandbox (${{ matrix.os }})
runs-on: ${{ matrix.os }}
# Raised from 20: test/package/ now runs real pip installs through the
# sandbox against real pypi, which the sandbox suite alone never did.
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: ./.github/actions/setup-bun
# Same step the `test` job already has. The sandbox job never needed it
# until test/package/ joined it: those tests create a real project with
# `tmpdir({ git: true })`, and `git commit` exits 128 on a runner with no
# global identity configured.
- name: Configure git for tests
run: |
git config --global user.email "ci@openscience.dev"
git config --global user.name "OpenScience CI"
git config --global init.defaultBranch main
- name: Install and verify Linux sandbox
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install --yes bubblewrap
# Ubuntu 24.04's host-wide AppArmor policy blocks unprivileged user
# namespaces on the hosted runner before bubblewrap can apply our
# stricter per-process profile. This runner is disposable; enable
# user namespaces for the job, then prove the sandbox can start.
if [[ -e /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]]; then
echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
fi
bwrap --ro-bind / / --dev /dev --proc /proc --unshare-pid --die-with-parent -- true
# R is the one backend with no verification anywhere: no runner has
# Rscript by default, and neither does any development machine on this
# project, so its two live tests skip everywhere and it ships on faith.
# r-base-core is the minimal package that provides Rscript. Linux only —
# `brew install r` on the macOS leg costs several minutes for a backend
# whose only platform-specific surface (the sandbox wrapper) is already
# covered there by the Python tests.
- name: Install R so the R installer tests actually run
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get install --yes --no-install-recommends r-base-core
Rscript -e 'cat("Rscript", as.character(getRversion()), "\n")'
# test/package/ carries the merge gate: a governed install under
# network "allowlist", plus the assertion that the shell route to the
# same install is refused. Both legs run it, so the gate is a fact on
# Linux and macOS rather than a claim about one of them.
- run: bun test test/sandbox/ test/package/
shell: bash
working-directory: backend/cli

test:
name: Test
runs-on: ubuntu-latest
Expand Down
42 changes: 33 additions & 9 deletions backend/cli/src/cli/cmd/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,39 @@ function printStatus(config?: Config.Sandbox) {
const enabled = config?.enabled === true

UI.println(`${S.TEXT_NORMAL_BOLD}Execution sandbox${S.TEXT_NORMAL}`)
// Three states, not two. "enabled" describes the CONFIG; whether anything is
// actually confined depends on a backend existing. Keying the sentence off
// `enabled` alone told a Windows user "agent shell commands are confined to
// the workspace" on a machine where `Sandbox.backend()` is "none" and nothing
// confines anything — a false statement about a security property, which is
// the worst kind of wrong thing for this command to print.
const effect = !enabled
? "run with full user authority"
: d.available
? "are confined to the workspace"
: "are NOT confined here: no backend on this platform"
UI.println(
` status ${enabled ? `${S.TEXT_SUCCESS_BOLD}enabled` : `${S.TEXT_DIM}disabled`}${S.TEXT_NORMAL}` +
`${S.TEXT_DIM} (agent shell commands${enabled ? " are confined to the workspace" : " run with full user authority"})${S.TEXT_NORMAL}`,
`${S.TEXT_DIM} (agent shell commands ${effect})${S.TEXT_NORMAL}`,
)
UI.println(` platform ${d.platform}`)
UI.println(
` backend ${
d.available
? `${S.TEXT_SUCCESS}${d.backend}${S.TEXT_NORMAL} ${S.TEXT_DIM}(${d.tool})${S.TEXT_NORMAL}`
: `${S.TEXT_WARNING}unavailable${S.TEXT_NORMAL} ${S.TEXT_DIM} ${d.reason}${S.TEXT_NORMAL}`
: `${S.TEXT_WARNING}unavailable${S.TEXT_NORMAL} ${S.TEXT_DIM}- ${d.reason}${S.TEXT_NORMAL}`
}`,
)
if (enabled) {
UI.println(` network ${config?.network ?? "deny"}`)
UI.println(` network ${config?.network ?? "allowlist"}`)
UI.println(` on missing backend ${config?.onUnavailable ?? "error"}`)
if (config?.allowWrite?.length) UI.println(` extra writable ${config.allowWrite.join(", ")}`)
if (config?.allowHosts?.length) UI.println(` extra hosts ${config.allowHosts.join(", ")}`)
}
if (enabled && !d.available) {
UI.println("")
UI.println(
` ${S.TEXT_WARNING_BOLD}Note:${S.TEXT_NORMAL} sandbox is on but no backend exists here ` +
` ${S.TEXT_WARNING_BOLD}Note:${S.TEXT_NORMAL} sandbox is on but no backend exists here - ` +
`commands run per "${config?.onUnavailable ?? "error"}". It takes effect on machines with a backend.`,
)
}
Expand Down Expand Up @@ -67,14 +79,19 @@ const EnableCommand = cmd({
builder: (yargs: Argv) =>
yargs
.option("network", {
choices: ["allow", "deny"] as const,
describe: "allow or deny network egress from sandboxed commands (default: deny)",
choices: ["deny", "allowlist", "allow"] as const,
describe: "network egress from sandboxed commands: deny, allowlist (default), or allow",
})
.option("allow", {
type: "string",
array: true,
describe: "extra absolute path the sandbox may write to (repeatable)",
})
.option("allow-host", {
type: "string",
array: true,
describe: "extra host the sandbox may reach when network is 'allowlist' (repeatable)",
})
.option("on-unavailable", {
choices: ["warn", "error", "allow"] as const,
describe: "what to do when no backend exists on a machine (default: error)",
Expand All @@ -84,10 +101,12 @@ const EnableCommand = cmd({
directory: process.cwd(),
async fn() {
const patch: Partial<Config.Sandbox> = { enabled: true }
if (args.network) patch.network = args.network as "allow" | "deny"
if (args.network) patch.network = args.network
if (args["on-unavailable"]) patch.onUnavailable = args["on-unavailable"] as "warn" | "error" | "allow"
const allow = args.allow as string[] | undefined
if (allow?.length) patch.allowWrite = allow
const allowHosts = args["allow-host"] as string[] | undefined
if (allowHosts?.length) patch.allowHosts = allowHosts
await Config.setSandbox(patch)
UI.empty()
UI.println(`${S.TEXT_SUCCESS_BOLD}Sandbox enabled${S.TEXT_NORMAL} ${S.TEXT_DIM}(global config)${S.TEXT_NORMAL}`)
Expand Down Expand Up @@ -125,22 +144,27 @@ const TestCommand = cmd({
const result = await Sandbox.selfTest()
if (!result.available) {
const d = Sandbox.describe()
UI.println(`${S.TEXT_WARNING}No sandbox backend available${S.TEXT_NORMAL} ${d.reason}.`)
UI.println(`${S.TEXT_WARNING}No sandbox backend available${S.TEXT_NORMAL} - ${d.reason}.`)
UI.println(`${S.TEXT_DIM}Nothing to test here.${S.TEXT_NORMAL}`)
return
}
UI.println(
`${S.TEXT_NORMAL_BOLD}Sandbox self-test${S.TEXT_NORMAL} ${S.TEXT_DIM}(${result.backend})${S.TEXT_NORMAL}`,
)
for (const c of result.checks) {
// The glyphs below are only reachable when a backend EXISTS, so they
// cannot print on Windows today, where the command exits above. Anything
// printed on a backend-less machine must stay ASCII: a Windows console
// decodes our UTF-8 as its OEM code page, and an em dash arrived as
// "\u0393\u00c7\u00f6" in a real run. Keep that rule if a Windows backend lands.
const mark = c.skipped ? `${S.TEXT_DIM}– skip` : c.pass ? `${S.TEXT_SUCCESS}✓ pass` : `${S.TEXT_DANGER}✗ FAIL`
UI.println(` ${mark}${S.TEXT_NORMAL} ${c.name}${c.detail ? ` ${S.TEXT_DIM}(${c.detail})${S.TEXT_NORMAL}` : ""}`)
}
UI.empty()
UI.println(
result.ok
? `${S.TEXT_SUCCESS_BOLD}Containment verified.${S.TEXT_NORMAL}`
: `${S.TEXT_DANGER_BOLD}Containment FAILED do not rely on the sandbox until this passes.${S.TEXT_NORMAL}`,
: `${S.TEXT_DANGER_BOLD}Containment FAILED - do not rely on the sandbox until this passes.${S.TEXT_NORMAL}`,
)
},
})
Expand Down
87 changes: 75 additions & 12 deletions backend/cli/src/compute/jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { OpenScience } from "../openscience"
import { Shell } from "../shell/shell"
import { Instance } from "../project/instance"
import { Sandbox } from "../sandbox/sandbox"
import { EgressRuntime } from "../sandbox/egress-runtime"
import { Filesystem } from "../util/filesystem"
import { ProvenanceEnvelope } from "../science/provenance/envelope"
import { ExecutionAuthority } from "../project/execution"
Expand Down Expand Up @@ -169,6 +170,10 @@ export namespace ComputeJobs {
recovery_attempts: z.number().int().nonnegative().optional(),
recovery_retry_at: z.string().optional(),
session_id: z.string().startsWith("ses_").optional(),
// Persists the whole Decision, including its own sandbox.network — a
// second copy of the persisted enum below `sandbox.network` carries; see
// the comment on ExecutionAuthority.Decision for the downgrade cost of
// widening either one.
authority: ExecutionAuthority.Decision.optional(),
scope: z
.object({
Expand All @@ -180,8 +185,11 @@ export namespace ComputeJobs {
.object({
requested: z.boolean(),
enforced: z.boolean(),
backend: z.enum(["seatbelt", "bubblewrap", "none"]),
network: z.enum(["allow", "deny"]),
backend: z.enum(["seatbelt", "bubblewrap", "appcontainer", "none"]),
// Persisted — widening this costs an older binary its ability to
// read a newer record. `authority.sandbox.network` above is the same
// enum persisted a second time; see ExecutionAuthority.Decision.
network: z.enum(["deny", "allowlist", "allow"]),
warning: z.string().optional(),
})
.optional(),
Expand Down Expand Up @@ -246,6 +254,9 @@ export namespace ComputeJobs {
type Launch = {
argv: string[]
sandbox?: Job["sandbox"]
/** Proxy variables `execute` must merge into the spawned process's env
* for the loopback shim to be reachable — see `Sandbox.Wrapped.env`. */
env?: Record<string, string>
}

const active = new Map<string, Runtime>()
Expand Down Expand Up @@ -675,6 +686,25 @@ export namespace ComputeJobs {
}
}

/**
* The network policy that actually applies to an SSH transport.
*
* "allowlist" is relaxed to "allow" because that policy is HTTP-only: it
* severs the network namespace and offers one HTTP proxy socket, which ssh
* cannot use at all — it does not read HTTP_PROXY and needs ProxyCommand or
* SOCKS. Applying it to ssh is not bounded egress, it is denial with a
* confusing error, and it buys nothing: the job's command runs on the remote
* machine, so the process being confined is a transport rather than the
* workload.
*
* An explicit "deny" is left alone. That is a user saying no network, not a
* default they never chose, and honouring it is the difference between
* relaxing a default and overriding an instruction.
*/
export function transportNetwork(requested: "deny" | "allowlist" | "allow") {
return requested === "allowlist" ? ("allow" as const) : requested
}

async function launch(
job: Job,
host: Host | undefined,
Expand All @@ -683,35 +713,66 @@ export namespace ComputeJobs {
): Promise<Launch> {
const spec = command(job, host)
if (host) {
// The ssh CLIENT is what gets wrapped here; the job's command runs on the
// remote machine. Two consequences.
//
// Network containment of this process buys nothing — the code being
// confined is a transport, not the workload — and under "allowlist" it
// actively breaks the feature: that policy severs the network namespace
// and offers one HTTP proxy socket as the only route out, which ssh
// cannot use. It reads HTTP_PROXY not at all and needs ProxyCommand or
// SOCKS. So an allowlist remote job did not fail closed with a useful
// message, it failed with an opaque connection error on the default
// policy.
//
// Filesystem containment still matters and is kept: ssh reads keys and
// can write locally. Only the network dimension is relaxed, and the value
// reported below is the one actually applied, not the one requested —
// reporting "allowlist" for a process running unconfined would be worse
// than the original bug.
const network = transportNetwork(authority.sandbox.network)
const relaxed = { ...authority.sandbox, network }
const egress = await EgressRuntime.egressFor(relaxed)
const planned = Sandbox.wrapArgv({
file: spec.argv[0]!,
args: spec.argv.slice(1),
workspace: authority.writable,
unreadable: OpenScience.kernelSensitivePaths(),
options: authority.sandbox,
options: { ...relaxed, egress },
})
const note =
network === authority.sandbox.network
? planned.warning
: [
planned.warning,
"network left unconfined for the ssh transport: the allowlist proxy is HTTP-only and ssh cannot use it. The job's own command runs on the remote host, outside this sandbox either way.",
]
.filter(Boolean)
.join(" ")
return {
argv: [planned.file, ...planned.args],
sandbox: {
requested: authority.sandbox.enabled,
enforced: planned.sandboxed,
backend: planned.backend,
network: authority.sandbox.network,
warning: planned.warning,
network,
warning: note,
},
env: planned.env,
}
}

await fs.mkdir(logsOf(scope.root), { recursive: true })
await fs.writeFile(exitOf(scope.root, job.id), "", { mode: 0o600 })
const wrapped = `(${job.command}\n); code=$?; printf %s "$code" > ${quote(exitOf(scope.root, job.id))}; exit "$code"`
const egress = await EgressRuntime.egressFor(authority.sandbox)
const planned = Sandbox.wrapArgv({
file: Shell.acceptable(),
args: ["-lc", wrapped],
workspace: authority.writable,
extraWritable: [exitOf(scope.root, job.id)],
unreadable: OpenScience.kernelSensitivePaths(),
options: authority.sandbox,
options: { ...authority.sandbox, egress },
})
return {
argv: [planned.file, ...planned.args],
Expand All @@ -722,6 +783,7 @@ export namespace ComputeJobs {
network: authority.sandbox.network,
warning: planned.warning,
},
env: planned.env,
}
}

Expand All @@ -730,16 +792,17 @@ export namespace ComputeJobs {
cwd: string,
authority: ExecutionAuthority.Decision,
): Promise<string | undefined> {
const egress = await EgressRuntime.egressFor(authority.sandbox)
const planned = Sandbox.wrapArgv({
file: argv[0]!,
args: argv.slice(1),
workspace: authority.writable,
unreadable: OpenScience.kernelSensitivePaths(),
options: authority.sandbox,
options: { ...authority.sandbox, egress },
})
const proc = Bun.spawn([planned.file, ...planned.args], {
cwd,
env: await OpenScience.subprocessEnv(process.env),
env: { ...(await OpenScience.subprocessEnv(process.env)), ...(planned.env ?? {}) },
stdin: "ignore",
stdout: "pipe",
stderr: "ignore",
Expand Down Expand Up @@ -1103,7 +1166,7 @@ export namespace ComputeJobs {
await fs.mkdir(logsOf(scope.root), { recursive: true })
const log = path.join(logsOf(scope.root), `${job.id}.log`)
const output = await fs.open(log, "a", 0o600)
const env = await OpenScience.subprocessEnv(process.env)
const env = { ...(await OpenScience.subprocessEnv(process.env)), ...(launch.env ?? {}) }
const queued = (await read(scope.root)).find((item) => item.id === job.id)
if (queued?.status === "cancelled") {
await output.close()
Expand Down Expand Up @@ -1828,12 +1891,12 @@ export namespace ComputeJobs {
args: spec.argv.slice(1),
workspace: job.authority.writable,
unreadable: OpenScience.kernelSensitivePaths(),
options: job.authority.sandbox,
options: { ...job.authority.sandbox, egress: await EgressRuntime.egressFor(job.authority.sandbox) },
})
: { file: spec.argv[0]!, args: spec.argv.slice(1) }
: { file: spec.argv[0]!, args: spec.argv.slice(1), env: undefined }
const proc = spawn(planned.file, planned.args, {
cwd: job.authority?.workspace,
env: await OpenScience.subprocessEnv(process.env),
env: { ...(await OpenScience.subprocessEnv(process.env)), ...(planned.env ?? {}) },
windowsHide: true,
stdio: "ignore",
})
Expand Down
Loading