Skip to the content.

Runtime Sandboxing

coding-ethos uses CEL and Go evaluators as the policy control plane: they decide whether a command, edit, lint capture, or Git action is allowed. Runtime sandboxing is the data plane: it constrains what an approved process can actually see and do.

The two layers are deliberately separate. CEL remains pure and deterministic. Go prepares facts, policy decides over those facts, and the sandbox runner enforces the declared runtime capabilities.

Non-Goal: LD_PRELOAD

LD_PRELOAD must not be used as a security boundary.

It is bypassed by statically linked binaries, Go and Rust programs that issue direct syscalls, child processes that scrub environment variables, and setuid or otherwise hardened executables that ignore loader injection. It is also fragile across libc variants and architectures. It may be useful for debugging or observability experiments, but not for coding-ethos enforcement.

Capability Model

Managed tools declare their runtime capabilities in toolcatalog. The initial model covers:

Current managed lint and formatting tools are offline by default. The first explicit network-capable catalog entry is gemini-check, which declares requires_network, the network capability tag, and an agent-network sandbox profile. Ordinary linters receive explicit no-network and no-git capability tags so MCP responses, traces, SARIF, and CEL policies can distinguish “capability denied by default” from “capability not documented.”

Consumer repositories can add required sandbox read/write mounts in repo_config.yaml:

sandbox:
  read_write_paths:
    - /opt/foundation
    - /opt/src/vllm
    - /scratch/lbox

These paths are additive to the managed tool’s catalog capability declaration. They are for required workspace or toolchain directories, not policy exceptions; .git write binds remain blocked even if a consumer lists them here.

CEL Policy Surface

Tool capabilities are exposed to CEL through tool_capabilities. Policies can now reason over declared runtime needs without reading host state or executing tools.

The first principle-local policies are:

These policies live with security-by-design in coding_ethos.yml, not in ad hoc Go checks or repo-local config. The Go layer supplies typed facts; CEL owns the contract.

Runtime Strategy

The first sandbox backend should be Bubblewrap (bwrap) rather than raw namespace syscalls. Bubblewrap gives a reviewed rootless wrapper for mount, PID, and network namespaces while keeping Go in charge of request construction, policy facts, traces, and normalized output.

The initial Go prototype lives in go/internal/sandbox. Managed lint capture can request it with coding-ethos-lint --managed-capture-tool <tool> --sandbox-mode required. The default remains off until the mount profile is proven across the full managed toolchain, because silently changing every developer lint invocation would make failures harder to attribute. In required mode, a missing Bubblewrap backend is a normalized runtime.sandbox_denial failure. In auto mode, the runner records the unavailable backend and falls back to unsandboxed execution.

The current mount profile is explicit and evidence-backed:

The target default profile for ordinary managed linters is:

Seccomp support is explicit: a catalog entry can declare a seccomp_profile and an optional compiled BPF profile path. When a profile path is present, Bubblewrap receives it through --seccomp using an inherited file descriptor. If required sandbox mode cannot open the profile, execution fails closed as runtime.sandbox_denial; advisory mode records degraded enforcement.

Resource controls are split by enforcement layer. Go wraps sandboxed managed tool execution in a hard timeout. Memory and CPU requests are applied through a delegated cgroup v2 hierarchy when requested by the tool capability model. The cgroup is prepared before process start, the Linux runner starts the child directly inside it using clone3 cgroup file-descriptor support, and the temporary cgroup directory is removed after the process exits. Required sandbox mode fails closed if no delegated writable cgroup hierarchy is available or if the limits cannot be applied; advisory mode keeps the degraded reason in sandbox evidence.

The first default managed-linter profile is intentionally conservative: no-network, no-git, lint-offline, 300 seconds, 2048 MB memory, 100% CPU, and deny-privilege seccomp metadata. The seccomp metadata is auditable even when no local BPF profile file has been installed yet.

Trace Contract

Sandbox execution must report declared capabilities, selected profile, backend, resource limits, and runtime denials into .coding-ethos traces. SARIF and MCP should preserve that evidence so agents see a policy-linked denial rather than raw kernel or wrapper noise.

The prototype records sandbox evidence under result.capture.sandbox in lint traces and under SARIF runs[].properties.sandbox, including capability tags, namespace isolation, cgroup state, timeout enforcement, and seccomp profile state. Required-mode denials also produce a blocking runtime.sandbox_denial finding grounded in security-by-design and one-path-for-critical-operations.

Unsupported platforms are explicit in the sandbox evidence. Required sandbox profiles fail closed when Linux namespace support or Bubblewrap is unavailable. Advisory auto mode records the degraded reason and falls back to the original command without claiming sandbox enforcement.

Generated GitHub and GitLab SARIF workflows default generated_config.ci.*.sandbox_mode to required and pass it to coding-ethos-run policy-lint. That lets CI enforce the sandbox while local developer workflows can remain explicit and recoverable with auto or off when a workstation lacks Bubblewrap support.