OpenShell¶
Backend¶
openshell
¶
OpenShell sandbox backend for agentic-ci.
OpenShellBackend(workdir='.', image=None, policy=None, extra_env=None, approval_mode=None, memory=None, cpu=None, gpu=None, *, harness, sandbox_profile=None, allow_host_setup=False)
¶
Bases: Backend
Runs an AI agent inside an OpenShell sandbox.
OpenShell provides security-focused sandboxing with network policy enforcement, filesystem isolation, and Landlock-based access control. Authentication is handled through an OpenShell provider created from one of agentic-ci's provider profiles (Vertex AI, Anthropic or OpenAI; none for a Claude subscription OAuth token). The gateway holds the real credential and injects a placeholder into the sandbox; the supervisor proxy swaps it for the real value on requests to the profile's endpoints from the agent binaries.
Unlike PodmanBackend, which bind-mounts the workdir so changes are
visible immediately on the host, OpenShellBackend copies the workdir
into the sandbox on setup() and copies it back after run() completes.
Only changes inside the workdir are reflected back to the host; files
written elsewhere in the sandbox (e.g. /tmp) are not retrieved. The
host's git control files (.git/config, hooks, info/) are restored
after the download, so the agent's git config never runs on the host.
sandbox_profile (see :mod:agentic_ci.sandbox_profile) is stored on
the backend. Its resources size the sandbox wherever the caller did
not pass memory, cpu or gpu explicitly, and its egress
presets open for the agent when the sandbox is created (the repo's
.agentic-ci/openshell-policy.yml is then ignored).
Its toolchains are provisioned after the workdir upload (see
:meth:_provision_toolchains): the host downloads and verifies each
archive and the sandbox extracts it under /sandbox/.local/toolchains;
:attr:toolchain_env then holds the variables the env script exports.
:meth:_set_egress_phase switches the egress of the setup shim between
the setup, validate and agent phases. The shim is an extra
layer, not isolation (see :data:sandbox.SANDBOX_SETUP_SHIM), so each
switch also kills the processes earlier execs left running and, for the
setup and validate phases, detaches the credential provider.
Its setup steps run in the sandbox under the setup phase when the
sandbox is created (:meth:_run_setup_phase), and
/sandbox/.agentic-ci/ENVIRONMENT.md tells the agent what it has
(:meth:_write_environment). After the agent, :meth:run deletes the
harness credential files, runs the validate commands under the
validate phase, moves the discard_before_download paths out of the
workdir (the next run moves them back) and only then downloads the
workdir. The host writes the results to
<run_dir>/sandbox-setup.json, sandbox-validation.json and
sandbox-discard.json and writes them again after the download, so
the agent cannot forge them.
Source code in src/agentic_ci/backends/openshell/__init__.py
Gateway¶
gateway
¶
OpenShell gateway lifecycle management.
is_running()
¶
Check if the OpenShell gateway is registered and healthy.
Source code in src/agentic_ci/backends/openshell/gateway.py
check_version()
¶
Raise RuntimeError unless the OpenShell CLI and gateway meet MIN_OPENSHELL_VERSION.
Source code in src/agentic_ci/backends/openshell/gateway.py
start()
¶
Start the OpenShell gateway with the podman driver.
Checks the OpenShell version first, then starts the podman API socket, generates TLS certificates for sandbox JWT auth, writes a gateway config, launches openshell-gateway in the background, registers it with the CLI, and blocks until the health endpoint responds.
If any step fails after processes have been spawned, cleanup is performed automatically to avoid orphaned processes.
Source code in src/agentic_ci/backends/openshell/gateway.py
stop()
¶
Terminate the gateway and podman service processes.
Deregisters the gateway from the CLI first, then discovers and kills
processes by port and socket rather than requiring stored handles, so
this works across process boundaries (e.g. a separate
agentic-ci stop invocation).
Source code in src/agentic_ci/backends/openshell/gateway.py
config_is_current()
¶
Return True when the on-disk gateway.toml matches the current environment.
The gateway only reads gateway.toml at startup, so a running gateway keeps using the driver images it was started with. Callers use this to decide whether an already-running gateway can be reused or must be restarted after the supervisor or sandbox runtime image changed.
Source code in src/agentic_ci/backends/openshell/gateway.py
Sandbox¶
sandbox
¶
OpenShell sandbox lifecycle management.
MainProcess(pid, start_time)
dataclass
¶
The sandbox's canonical main process (sleep infinity from :func:create).
start_time is field 22 of /proc/<pid>/stat (clock ticks after
boot). Together with the PID it identifies the process even if the agent
names another process sleep infinity or the PID is reused.
from_record(record)
classmethod
¶
Read a :meth:to_record mapping, or return None if it is not one.
Source code in src/agentic_ci/backends/openshell/sandbox.py
exists()
¶
create(image=None, policy_path=None, otel_port=None, workdir='.', approval_mode=None, auth_mode=None, memory=None, cpu=None, gpu=None, profile=None)
¶
Create a persistent sandbox with the CI provider attached, if auth_mode uses one.
The sandbox is created first, then the network policy is applied
via openshell policy update --wait to ensure the supervisor
has compiled and activated the rules before the agent starts.
memory, cpu and gpu size the sandbox. All three default to
None, which passes no resource flag and leaves the effective limits to
the compute driver and host configuration. An agent that exceeds a memory
limit is OOM-killed by the cgroup, and from inside the sandbox that looks
like the sandbox simply vanishing: the supervisor's log stops mid-line and
the next command reports sandbox is not ready.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
image
|
str | None
|
Sandbox image to create from. |
None
|
policy_path
|
str | None
|
Path to a network policy file to merge in. |
None
|
otel_port
|
int | None
|
Port for the OTEL collector on the host. |
None
|
workdir
|
str
|
Directory the policy file is resolved relative to. |
'.'
|
approval_mode
|
str | None
|
Enables agent policy proposals when set. |
None
|
memory
|
str | None
|
Memory limit, e.g. |
None
|
cpu
|
str | None
|
CPU limit, e.g. |
None
|
gpu
|
int | None
|
GPU count to request, e.g. |
None
|
profile
|
SandboxProfile | None
|
Sandbox profile whose agent-phase egress is added to the policy. None keeps the policy exactly as without profiles. |
None
|
Source code in src/agentic_ci/backends/openshell/sandbox.py
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | |
build_phase_policy(base_get_output, endpoints, *, park_agent=False)
¶
Build the policy for an egress phase from openshell policy get --base -o json.
Takes the .policy object, deep-copies it, and in network_policies:
- drops every rule named with :data:
PHASE_RULE_PREFIX(the previous phase's rules) or_provider_(composed by the server); - removes :data:
SANDBOX_SETUP_SHIMfrom every other rule'sbinaries, dropping a rule only when the shim was its only binary; - with park_agent (the setup and validate phases), parks every agent
binary path under :data:
PARKED_BINARY_PREFIXso no process matches the agent's rules while the shim's egress is open; without it (the agent phase), restores the parked paths; - adds one rule per endpoint, in order and de-duplicated, named
PHASE_RULE_PREFIX + indexand bound to the shim only; - sets
allow_encoded_slashon every L7 (rest) endpoint for a host in :data:ENCODED_SLASH_HOSTS, in the shim's rules and the agent's alike, sinceopenshell policy updatecannot set it and two endpoints for one host must agree on it.
One rule per endpoint mirrors the rules openshell policy update
creates and the shape the September 2026 spike verified, and keeps each
endpoint's options independent. Every other field (version,
filesystem_policy, landlock, process, rule names and endpoints,
and the credential bindings on user rules) is returned unchanged:
openshell policy set needs the whole object and refuses to drop the
static fields of a live sandbox.
Returns the raw policy dict for openshell policy set. Raises
ValueError when .policy or its network_policies is missing or
an endpoint is malformed; the message never includes policy content.
Source code in src/agentic_ci/backends/openshell/sandbox.py
apply_phase_policy(phase, endpoints)
¶
Switch the sandbox's setup-shim egress to phase.
Reads the base policy, builds the phase policy with
:func:build_phase_policy and applies the whole object with
openshell policy set --wait. openshell policy update is never
used here: it folds an endpoint whose host overlaps an existing rule into
that rule, where the shim could not be removed again.
phase is one of setup, validate or agent. setup and
validate also park the agent's rules (see :data:PARKED_BINARY_PREFIX),
so running an agent binary under the shim gains nothing. agent strips
every shim-bound rule, restores the agent's rules and takes no endpoints.
Any policy change closes every open proxied connection in the sandbox,
so switch only while nothing runs there. Nothing is applied when the
policy would not change, so switching to the phase already in effect is
free.
Raises RuntimeError when an openshell command fails or prints
unusable output. The message names the step and the failure class only;
stderr goes to the job log.
Source code in src/agentic_ci/backends/openshell/sandbox.py
389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 | |
python_exec_args(script, args)
¶
The argv that runs script with the sandbox's python, as the sandbox user, no login shell.
For in-sandbox helpers that manage the process themselves, such as the
setup and validate step runner (agentic_ci.backends.openshell.steps).
Source code in src/agentic_ci/backends/openshell/sandbox.py
exec_python(label, script, args, timeout=_PROCESS_EXEC_TIMEOUT_SECONDS)
¶
Run script with the sandbox image's /usr/bin/python3 (see :func:_exec_python).
For in-sandbox helpers outside this module, such as toolchain
installation (agentic_ci.backends.openshell.provision).
Source code in src/agentic_ci/backends/openshell/sandbox.py
find_main_process()
¶
Identify the sandbox's main process, right after :func:create.
Call it before anything else runs in the sandbox: the main process is the
only sandbox-user child of the supervisor, and it must be sleep
infinity. Waits up to :data:_MAIN_WAIT_SECONDS for it to appear.
Raises RuntimeError when there is none, more than one, or it is
something else.
Source code in src/agentic_ci/backends/openshell/sandbox.py
stop_leftover_processes(main, phase)
¶
Kill every sandbox-user process except main before phase opens.
Processes started by earlier execs (the agent, earlier setup or validate steps, and whatever they started, daemons included) are still running after their exec ends. The setup shim is not a boundary against them: once the setup or validate phase opens, any of them could run it. So every one of them is killed first; the main process (matched by PID and start time), the supervisor (root) and the killing exec itself survive.
Raises RuntimeError (a class and a count, never a process name or
command line) when a process is still alive afterwards, or when main is
not running: the record is then stale, nothing is killed, and the sandbox
must be recreated. The caller must not run the phase. Details go to the
job log.
Source code in src/agentic_ci/backends/openshell/sandbox.py
detach_provider(phase)
¶
Detach the CI provider from the sandbox for a setup or validate phase.
The rule OpenShell composes from a provider profile binds the API hosts
to the agent binaries and is not part of policy get --base, so
parking the agent's rules cannot remove it: a step running an agent
binary under the shim could still spend the credential. Detaching is the
only lever. --wait returns once the supervisor has installed the
change. Idempotent: detaching a detached provider succeeds at once.
Follow it with :func:wait_for_provider_env before any step runs.
Source code in src/agentic_ci/backends/openshell/sandbox.py
attach_provider(phase)
¶
provider_env_state(env_vars, timeout=_PROCESS_EXEC_TIMEOUT_SECONDS)
¶
Return ATTACHED or DETACHED as a fresh exec sees env_vars, or None.
ATTACHED when any of env_vars holds a provider placeholder. None
means the probe failed (a timeout, a refused exec, odd output) and says
nothing about the provider.
Source code in src/agentic_ci/backends/openshell/sandbox.py
wait_for_provider_env(env_vars, *, attached, phase)
¶
Wait until the supervisor has applied an attach or detach of the provider.
A new exec gets the provider's variables from the supervisor's current
provider snapshot, which OpenShell replaces together with the credential
bindings the proxy injects from. So a fresh exec in which one of
env_vars holds a provider placeholder proves the credential is injected
again, and one in which none does proves the proxy no longer resolves any
placeholder for it, including one issued before the detach. It proves
nothing about the network rule composed from the provider profile: the
supervisor reloads the policy right after the environment, in the same
poll, and a policy set --wait issued after the detach confirms it
(see OpenShellBackend._set_egress_phase). The probe needs no network.
A DETACHED result only counts when the probe has been seen to report
ATTACHED for this sandbox: call it with attached=False only after an
attached=True wait or an ATTACHED :func:provider_env_state.
Polls for up to :data:_PROVIDER_WAIT_SECONDS in total, probe execs
included, and raises RuntimeError when the state cannot be confirmed,
so the caller does not run the phase.
Source code in src/agentic_ci/backends/openshell/sandbox.py
upload(local_path, timeout=None)
¶
Upload a local path into the sandbox.
With timeout (seconds), subprocess.TimeoutExpired is raised when the
upload takes longer; without it the upload is not bounded.
Source code in src/agentic_ci/backends/openshell/sandbox.py
download(sandbox_path, local_dest)
¶
Download a path from the sandbox to a local destination.
exec_cmd(cmd)
¶
Run a command inside the sandbox. Returns the CompletedProcess.
exec_cmd_streaming(cmd)
¶
Run a command inside the sandbox with stdout piped. Returns a Popen.
Source code in src/agentic_ci/backends/openshell/sandbox.py
Policy¶
policy
¶
Policy resolution for OpenShell sandbox.
EGRESS_PHASES = ('setup', 'validate', 'agent')
module-attribute
¶
Sandbox phases with their own egress.
setup and validate endpoints are bound only to the setup shim and are
switched with a whole-policy openshell policy set (see
sandbox.apply_phase_policy). agent endpoints are bound to the agent
binaries when the sandbox is created.
ENCODED_SLASH_HOSTS = frozenset(host for preset in EGRESS_PRESETS.values() for host in preset.encoded_slash_hosts)
module-attribute
¶
Hosts whose L7 endpoints need allow_encoded_slash.
openshell policy update --add-endpoint has no option for it, so
sandbox sets it in the YAML policy it applies with openshell policy
set (see sandbox.build_phase_policy).
EgressPreset(endpoints, phases=_ALL_PHASES, encoded_slash_hosts=frozenset())
dataclass
¶
A named set of endpoints a sandbox profile can open.
endpoints use the openshell policy update --add-endpoint format.
phases lists the :data:EGRESS_PHASES the preset is open in, so a
later preset can be open only while setup runs. encoded_slash_hosts
are the preset's hosts whose clients send an encoded slash (%2F) in
the request path; their L7 endpoints get allow_encoded_slash (see
:data:ENCODED_SLASH_HOSTS).
phase_endpoints(profile, phase)
¶
Return the endpoints profile opens to the setup shim in phase.
phase is "setup" or "validate". The result holds the endpoints
of the profile's presets open in that phase, then the profile's raw
egress, de-duplicated (a raw endpoint for a preset host gives way to the
preset, see :func:_merge_endpoints). It never includes the
built-in defaults, the auth (LLM) endpoints or the OTel collector: those
stay bound to the agent binaries only. A raw endpoint whose host
overlaps an agent-only host (see :func:_agent_only_hosts: auth and
provider profile hosts, Google's OAuth endpoint, the collector), or that
carries a credential option, is left out as well; only their count is
logged.
Source code in src/agentic_ci/backends/openshell/policy.py
resolve_endpoints(flag_path=None, workdir='.', auth_mode=None, profile=None)
¶
Resolve the endpoint list to use for policy update.
Merges the built-in defaults and endpoints required by auth_mode with extra endpoints from, in priority order:
- Explicit
--policyflag path .agentic-ci/openshell-policy.ymlin workdir
With a sandbox profile, the repo policy file is ignored (egress then
comes from reviewed configuration only) and the endpoints of the
profile's presets open in the agent phase plus its raw egress are
added after the defaults and auth endpoints. An explicit --policy
flag still applies.
With a profile, the result is de-duplicated and every other endpoint for
a preset host gives way to the preset in place (see
:func:_merge_endpoints): with the pypi preset, the default
pypi.org:443:read-only becomes the preset's L7 read-only endpoint for
the agent too. Without a profile the list is built as before, with no
preset handling, even when an extra endpoint repeats a preset's text.
Returns a list of endpoint strings for openshell policy update --add-endpoint.
Source code in src/agentic_ci/backends/openshell/policy.py
Toolchain Installation¶
provision
¶
Toolchain installation inside the OpenShell sandbox.
The host resolves, downloads and verifies each toolchain archive
(:mod:agentic_ci.toolchains); :class:OpenShellInstaller uploads the
verified archive and extracts it inside the sandbox with
:data:INSTALL_SCRIPT, run by the image's own /usr/bin/python3
(tarfile and zipfile: the Hummingbird sandbox images ship python3,
tar, gzip and xz but no unzip). Nothing is ever extracted on the host.
The script opens the uploaded archive once (no symlink, a regular file),
copies it into a fresh private directory while hashing it, and refuses it
unless that copy has the expected sha256; everything after reads the copy,
so swapping the upload by name cannot slip an unverified archive in. It then
checks every member before it extracts anything: no absolute path, no
.., no member under a symlink, no duplicate, no device, FIFO or other
special file, and every symlink and hard link must resolve inside the target
directory (links are resolved through the archive's own symlinks). It
extracts into a temporary directory next to the target, writes a marker with
the sha256 and renames it into place, so a failed extraction leaves nothing
half-installed. For an archive with components (Rust), only the members
under <component_root>/<component>/ are extracted, links are refused
there, and each component's files are moved into the toolchain directory as
the tool's own installer would place them; a file two components both
provide is refused.
The marker only records what was installed; it is not an integrity check.
The toolchain directory is writable by the agent, so an earlier run on a
reused sandbox can plant a directory with a matching marker or change the
installed files. :class:OpenShellInstaller therefore skips a toolchain
only when the host's own record of this sandbox (kept in the saved sandbox
identity) names the same target and sha256 as the marker; otherwise it
installs again, which replaces the target directory. Files an earlier run
changed inside a recorded toolchain are not detected.
OpenShellInstaller(root=SANDBOX_TOOLCHAIN_ROOT, *, recorded=None)
¶
:class:agentic_ci.toolchains.Installer for the OpenShell sandbox.
recorded is the host's record of the toolchains this sandbox was given
(target directory to sha256). :meth:is_installed is True only when that
record and the in-sandbox marker both name the plan's sha256.
Source code in src/agentic_ci/backends/openshell/provision.py
Setup and Validate Steps¶
steps
¶
Sandbox profile steps inside the OpenShell sandbox: setup, validate and discard.
A profile's setup steps and validate commands are repository code.
They run inside the sandbox through the setup shim
(:data:sandbox.SANDBOX_SETUP_SHIM), so the egress the setup and validate
phases open reaches them and nothing else, and never on the CI host.
:func:run_step starts each one with :data:STEP_WRAPPER, run by the image's
own /usr/bin/python3 without a login shell, which:
- builds the step's environment from scratch: the variables agentic-ci passes
(a minimal
PATHandHOME, the toolchain variables and the profile'senv) and then the proxy and CA variables OpenShell sets for every process (:data:INHERITED_ENV), which win, so nothing else the exec carries reaches the step and the LLM env script is never sourced; - changes to the workdir and runs
SHIM /usr/bin/bash -c RUN; the shim puts the step in its own process group; - becomes a child subreaper (
PR_SET_CHILD_SUBREAPER), so what the step leaves behind (a background job, a double-forked daemon) is reparented to it rather than to the sandbox's init; - enforces the step's timeout inside the sandbox: SIGTERM to the shim (which
forwards it to the step's group) and every process below the wrapper, then
SIGKILL after :data:
KILL_GRACE_SECONDS, and exits 124; - once the step's shim exits, on success, failure or timeout, SIGKILLs and reaps every process still below it, so no step process outlives its step or keeps its output open.
The host reads the combined output and keeps only its end; :func:tail_text
turns it into at most :data:TAIL_LINES redacted lines for the records. A
step that fails or times out is recorded, never raised.
:func:wipe_files deletes harness credential files before validate commands
run. :func:discard_paths moves discard_before_download paths out of the
workdir (never through a symlink) before the workdir is downloaded, and
:func:restore_discarded moves them back before the next run's agent.
INHERITED_ENV = ('CURL_CA_BUNDLE', 'DENO_CERT', 'GIT_SSL_CAINFO', 'NODE_EXTRA_CA_CERTS', 'REQUESTS_CA_BUNDLE', 'SSL_CERT_DIR', 'SSL_CERT_FILE', 'ALL_PROXY', 'HTTPS_PROXY', 'HTTP_PROXY', 'NO_PROXY', 'all_proxy', 'https_proxy', 'http_proxy', 'no_proxy', 'OPENSHELL_SANDBOX', 'TERM', 'USER')
module-attribute
¶
Variables a step inherits from the exec when set: OpenShell's CA and proxy settings.
Without the CA variables a step would not trust the certificates OpenShell presents for the L7-inspected preset hosts. Nothing else the exec carries (the image's variables, a provider placeholder) is passed on.
DISCARD_STASH = '/sandbox/.agentic-ci/discarded'
module-attribute
¶
Where :func:discard_paths moves discarded paths, outside the workdir.
The next run() on the same sandbox moves them back (:func:restore_discarded)
before the agent starts, so a later agent run (after a classifier run or a
retry) still has what the setup steps installed.
StepRecord(name, status, rc=None, seconds=0.0, tail='', kind=None, reason='')
dataclass
¶
The outcome of one setup step or validate command, as the run records hold it.
status is passed, failed (a non-zero exit), timeout,
error (it could not be started) or not_run (its phase could not
be opened; reason says so). kind is set for validate commands
only. tail is the redacted end of the combined output.
from_dict(data)
classmethod
¶
Read a :meth:to_dict mapping back, or return None if it is not one.
Source code in src/agentic_ci/backends/openshell/steps.py
DiscardRecord(path, status)
dataclass
¶
One discard_before_download path and what happened to it.
status is removed, absent, refused (the path leaves the
workdir or crosses a symlink), failed or error (the removal did
not report back).
shown_name(name)
¶
A step name for a log line: the validated name, cut to a bounded length.
build_env(path, variables, profile_env)
¶
The environment a step runs with, apart from :data:INHERITED_ENV.
A minimal PATH with the toolchain directories (path) first,
HOME, LANG, the toolchain variables and the profile's
profile_env (which wins over a toolchain variable of the same name).
Profile validation rejects PATH, HOME and every
:data:INHERITED_ENV name, and the wrapper applies the inherited values
last anyway. No LLM, forge or other credential is ever in it.
Source code in src/agentic_ci/backends/openshell/steps.py
tail_text(data, secrets=(), *, truncated=False)
¶
The last :data:TAIL_LINES lines of data, redacted and cleaned for a record.
ANSI escapes and control characters are removed, every line is redacted
(see :func:agentic_ci.redact.redact, with the host's secrets) and then
cut to :data:TAIL_LINE_CHARS. With truncated, the first line is a
fragment of a longer one and is dropped, so half a secret cannot pass.
Source code in src/agentic_ci/backends/openshell/steps.py
run_step(name, run, timeout, *, env, cwd, secrets=(), kind=None)
¶
Run one setup step or validate command in the sandbox and record it.
env comes from :func:build_env; cwd is the sandbox workdir. The
step runs under the setup shim with timeout seconds, enforced inside
the sandbox; the host gives up on the exec :data:KILL_GRACE_SECONDS plus
a minute later. Never raises for a failing step: every outcome is a
:class:StepRecord.
Source code in src/agentic_ci/backends/openshell/steps.py
622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 | |
wipe_files(paths)
¶
Delete paths (absolute, normalized) in the sandbox and confirm they are gone.
Raises RuntimeError (a class, no path) when the wipe cannot be
confirmed, so the caller does not run validate commands.
Source code in src/agentic_ci/backends/openshell/steps.py
discard_paths(workdir, paths)
¶
Move each workdir-relative path in paths out of workdir inside the sandbox.
Each path goes to :data:DISCARD_STASH, which is outside the workdir and
never downloaded, and comes back at the next run (see
:func:restore_discarded); a symlink is removed itself, and without a
usable stash (a symlink planted there) paths are deleted instead. Every
path component is opened without following symlinks, so a symlink
anywhere in the path (planted by the agent or a step) makes that path
refused instead of reaching outside the workdir. .., absolute
paths and .git are refused. Failures are recorded, never raised.
Source code in src/agentic_ci/backends/openshell/steps.py
restore_discarded(workdir)
¶
Move the paths the previous run discarded back into workdir; return how many.
A path whose place in the workdir is taken, or whose parent directory is gone or crosses a symlink, is not restored; the stash is deleted either way. Returns None when the restore did not report back. Never raises.
Source code in src/agentic_ci/backends/openshell/steps.py
install_environment_files(uploaded, target, names)
¶
Move the uploaded directory's names into target in the sandbox; return success.
target is replaced if it is a symlink or not a directory, and each file is renamed into place, so a link the agent left there is replaced rather than written through.
Source code in src/agentic_ci/backends/openshell/steps.py
write_json(path, data)
¶
Write data as indented JSON to path atomically.
The temporary file gets a fresh random name (mkstemp), so nothing left
in the directory (a symlink the agent planted) is written through.
Source code in src/agentic_ci/backends/openshell/steps.py
ENVIRONMENT.md¶
environment
¶
ENVIRONMENT.md and environment.json: what the agent is told about its sandbox.
OpenShellBackend.setup() writes both to :data:SANDBOX_ENVIRONMENT_DIR
for every sandbox profile, before the agent starts: the provisioned
toolchains, the setup steps and their results, the validate commands to run,
the declared skips and the egress presets that are open. The directory is
outside the workdir, so neither file is downloaded back or committed.
Repo-controlled strings (step names, commands, skip text, output tails) are
kept as data: in ENVIRONMENT.md they only appear inside fenced code
blocks (with a fence longer than any backtick run they contain) or, for step
names (letters, digits, ., _ and - only), inside inline code, under
a note that says they are data. Every string and list is bounded, so a large
profile cannot produce a huge file.
build(profile, *, toolchain_results, toolchain_env, setup_records, workdir)
¶
The environment.json document (every list and string bounded).
Source code in src/agentic_ci/backends/openshell/environment.py
render_markdown(data)
¶
ENVIRONMENT.md for the :func:build document data.