Spens
Spens is a command line tool for running AI coding agents securely, with full observability of everything they do.
Why Spens Exists
If you work across many codebases and experiment with multiple agents and LLMs, you need a way to run them without polluting your machine, creating conflicts or having an agent blow away your home directory. Spens combines three tools to create reproducible, observable agent runtimes.
What spens gives you
Coding agents are useful, but risky to run directly on your own machine. They execute shell commands, change files, and make network requests. Spens runs every agent session in a contained environment and records everything that happens, so you can let agents work and still know exactly what they did.
- Security. The agent runs in a sandbox, isolated from your machine. It runs as an unprivileged user and cannot touch your home directory or your system files.
- Observability. Every LLM API call, tool invocation, HTTP request, and file change is captured. After a session you can review exactly what the agent did, line by line, in a local web viewer.
- Network control. You decide which domains the agent may contact and which HTTP methods it may use. Non-HTTP protocols are always blocked. With domain rules configured, any host that is not explicitly allowed is blocked.
- Secret handling. Your API keys never enter the agent's container. The agent only sees a placeholder, which is swapped for the real key only for requests to approved domains.
- Confidence in autonomous runs. Give the agent a prompt and let it run unattended ("yolo mode"), then review the full log and accept or roll back its file changes.
Spens works with existing coding agents out of the box and can be easily configured to support more: Codex, Claude Code, opencode, and pi.
Note: spens is not designed to be 100% secure as it uses docker, however it is secure enough for most agent workloads and can be run with gvisor to harden the docker instance.
Installation
Prerequisites
- Python 3.11 or newer
- Docker, installed and running. On macOS, OrbStack is required
Docker is only needed when you run a session. It is not needed to install the package.
Install with pip
The package is published to PyPI as spens-ai (the spens name is already taken there):
pip install spens-ai
It is also published to the GitLab package registry of the spens project: https://gitlab.com/refwd1/spens. The project is public, so no login or token is needed.
pip install spens-ai --extra-index-url https://gitlab.com/api/v4/projects/refwd1%2Fspens/packages/pypi/simple
Install with uv
uv tool install spens-ai
or from the GitLab registry:
uv tool install spens-ai --index https://gitlab.com/api/v4/projects/refwd1%2Fspens/packages/pypi/simple
uv tool install installs spens into its own environment and puts the spens command on your PATH. The distribution is named spens-ai, but the command and the Python package are still spens.
Verify the install
spens list
This prints the available environments and agents.
Quick start
Three steps: configure the workspace, run an agent, review the session.
1. Configure the workspace
Run this in your project directory:
cd my-project
spens init
spens init asks a short set of questions:
- What type of stack is used (node, python, dotnet, rust, go, java, manual)
- Do you want to allow public GET access from the internet
- Do you want to allow access to common URLs for tooling (package registries and similar, based on your stack)
- What model provider you are using (fireworks, anthropic, openrouter, openai, manual; you can pick several)
- Do you want to inject API keys for your provider (say no if you are using configurations or subscriptions instead of API keys)
- Do you want to configure common environment variables for the provider
It then writes a .spens.config.json file with the matching domain rules, key injection rules, and capture URLs, and adds .spens to your .gitignore.
It can also run without prompts:
spens init --yes --stack node --provider anthropic
Make sure your provider API key is set in your host environment (for example ANTHROPIC_API_KEY). Spens gives the agent a placeholder value and injects the real key only for approved domains, so the real key never enters the container.
If you skip this step and provide no config, the network policy fails closed: with no domain rules configured, every HTTP request is denied. See Default network policy.
2. Run an agent
spens node-24 pi .
The format is: spens <environment> <agent> <workspace>.
- The first run builds the Docker images for the environment, the agent, and the interceptor. This takes a few minutes. Later runs reuse the cached images.
- The current directory (
.) is mounted as the workspace. - The pi agent starts in an interactive session.
- When the session ends, you choose whether to keep the agent's file changes or roll the workspace back to its pre-session state.
You can also give the agent a prompt and let it run unattended (yolo mode). The agent executes the prompt and exits:
spens node-24 pi . "explain what this repository does"
3. Review the session
spens log-viewer
This opens a local web UI at http://127.0.0.1:7331 with four tabs:
- Chat transcript - every LLM call, decoded: request messages, responses, tool calls, reasoning, and token usage
- Audit log - session metadata, executable identity, tracked paths, audit events, and the integrity chain
- Other HTTP - all non-LLM HTTP requests and responses captured during the session
- File changes - every file the agent changed, with diffs and rollback information
The session summary includes an estimated cost. See Cost estimation.
Out of the box environments and agents
Run spens list to print everything that is available.
Environments
| Environment | Base image |
|---|---|
node-20 |
node:20.18.0-bookworm |
node-22 |
node:22.16.0-bookworm |
node-24 |
node:24.18.0-trixie |
python-3.11 |
python:3.11-bookworm |
python-3.12 |
python:3.12-bookworm |
python-3.13 |
python:3.13-bookworm |
Agents
| Agent | Tool | Notes |
|---|---|---|
codex |
OpenAI Codex | Forced to HTTP-only mode via config.toml so its traffic can be intercepted. Mounts ~/.codex/settings.json from the host; a file at .spens/.codex/settings.json overrides it |
claude |
Claude Code | Mounts ~/.claude/settings.json from the host; a file at .spens/.claude/settings.json overrides it |
opencode |
opencode | Mounts ~/.config/opencode/config.json and ~/.local/state/opencode/models.json from the host; files under .spens/.opencode override them |
pi |
pi.dev | Mounts ~/.pi/agent/settings.json and models.json from the host; files under .spens/.pi/agent override them |
Any environment can be combined with any agent.
Cost estimation
Every session summary includes an estimated cost, computed from the token usage in the captured LLM traces.
Spens prices tokens with the Portkey pricing dataset, covering six providers: anthropic, openai, google, fireworks-ai, openrouter, and bedrock. A model listed by more than one provider is priced from the first provider in that order that lists it, so a direct provider's rate wins over a reseller's.
You normally do not need to do anything. After each session, spens fetches the dataset on the host, caches it under ~/.cache/spens/pricing for 24 hours, and prices the session from it. For each model, the price is resolved from the first source that lists it:
- the live Portkey dataset (cached for 24 hours)
- the vendored snapshot shipped inside the package at
spens/data/pricing, used when the fetch fails or you are offline - a small built-in price table
- nothing - a model that no source lists is costed at $0 rather than guessed
Each session summary records which source each provider's numbers came from, so an archived cost figure can still be explained later.
Use spens pricing to inspect or refresh the catalog on demand:
spens pricing # where each provider's prices are coming from
spens pricing --refresh # re-fetch every provider into the local cache
spens pricing --model claude-sonnet-4 # resolved price for one model id
spens pricing --model gemini-2.5-pro --tokens 400000
The full option reference is in spens pricing.
To keep costing entirely offline, set SPENS_PRICING_OFFLINE=1 in your environment or put "pricing": {"live_fetch": false} in .spens.config.json.
Advanced configuration
Spens runs every session as two Docker containers - the agent container and an interceptor container - on an isolated internal Docker network, so all agent traffic, including DNS, is forced through the interceptor where it is filtered, logged, and decoded. For the full picture, including image generation, the interceptor internals, the nono sandbox, and policy masking, see ARCHITECTURE.md.
Custom environments and agents
There are two ways to add your own templates:
- Put JSON template files in a
templates/directory inside your workspace. Local templates take precedence over built-in templates with the same name. - Pass a
.jsonfile directly as the environment or agent argument. The path is resolved relative to the current directory or the workspace.
Example: a custom Go environment
Create templates/go-1.24.json in your workspace:
{
"template_type": "environment",
"name": "go-1.24",
"base-image": "golang:1.24-bookworm",
"nono-command": "curl -fsSL https://nono.sh/install.sh | sh",
"packages": ["curl", "ca-certificates", "git"],
"package_manager": "apt",
"agent_user": {
"name": "spens",
"uid": 1000,
"home": "/home/spens",
"create": true
}
}
Then run it by name (picked up from templates/) or by file path:
spens go-1.24 pi .
spens go-1.24.json pi .
See Custom environment parameters for the full field reference.
Example: a custom agent
Create templates/my-agent.json:
{
"template_type": "agent",
"name": "my-agent",
"installation_command": "curl -fsSL https://example.com/install | sh",
"dependencies": [],
"nono_base_config": "nolabs-ai/my-agent",
"relocate_binary": {
"from": "~/.local/bin/my-agent",
"to": "/usr/local/bin/my-agent",
"cleanup": ["~/.my-agent", "~/.local/bin/my-agent"]
},
"config_files": {
"~/.my-agent/config.toml": "setting = true\n"
},
"configuration": [
{
"source": "~/.config/my-agent",
"destination": "~/.config/my-agent",
"include": ["config.json", "*.toml"],
"exclude": ["secrets.*"]
}
],
"allow_folders": ["~/.cache/my-agent"],
"env": ["MY_AGENT_CERT=/certs/mitmproxy-ca-cert.pem"],
"packages": ["ripgrep"],
"yolo_command": "my-agent run --yes {prompt}"
}
Then run it:
spens node-24 my-agent .
See Custom agent parameters for the full field reference.
Command line arguments
Run a session
spens <environment> <agent> <workspace> [prompt] [options]
| Argument or option | Description |
|---|---|
environment |
Environment template name (e.g. node-24), or a path to a .json template file |
agent |
Agent template name (e.g. pi), or a path to a .json template file |
workspace |
Path to the directory to mount as the workspace |
prompt |
Optional fourth argument. Runs the agent non-interactively (yolo mode): the agent executes the prompt and exits. The agent template must define a yolo_command |
--rebuild |
Rebuild the Docker images from scratch, without cache |
--spens-dir PATH |
Directory where session state is stored. Default: <workspace>/.spens |
--config PATH |
Use a different spens config file for the session. Default: <workspace>/.spens.config.json. Relative paths resolve against the current directory. The file must exist when given; it is an error otherwise |
--accept-changes |
Auto-accept the agent's workspace changes when the session ends (skips the review prompt) |
--reject-changes |
Roll the workspace back to its pre-session state after the agent exits |
--output MODE |
Output mode: tty (default, pretty terminal output), jsonl (stream JSON events to stdout), or background (print the session id and exit; the session runs detached) |
--session-id ID |
Use a specific session id instead of a random one. Must match ^[a-z0-9][a-z0-9-]{0,31}$; reusing an existing id is an error |
--bypass-config-mounts |
Do not mount any of the agent template's configuration files into the agent container. The agent starts with no host configuration/credentials and must log in (or be configured) from inside the session; that state is ephemeral and lost when the container exits |
Notes:
jsonlandbackgroundmodes require a prompt and exactly one of--accept-changes/--reject-changes. Without an explicit decision, the end-of-session change review would block a non-interactive session forever.- Every session writes a machine-readable event stream to
<session_dir>/events.jsonland its current state to<session_dir>/state.json, regardless of output mode.
Session lifecycle commands
spens status <session-id> [--spens-dir PATH] # print session state and exit code
spens cancel <session-id> [--spens-dir PATH] # stop the session and mark it canceled
spens attach <session-id> [--spens-dir PATH] # follow the session event stream until it ends
These work for every spens session, regardless of output mode. Terminal states are finished, canceled, and error.
spens init
spens init [options]
| Option | Description |
|---|---|
--yes, -y |
Skip all prompts and use defaults for unanswered questions. Requires --stack and --provider |
--stack |
Stack used: node, python, dotnet, rust, go, java, manual |
--provider |
Model provider(s): fireworks, anthropic, openrouter, openai, manual. Comma-separated for multiple (e.g. openai,fireworks) |
--public-get / --no-public-get |
Allow or deny public GET access (default: deny) |
--tooling-urls / --no-tooling-urls |
Allow or deny the stack's common tooling URLs (default: allow) |
--inject-api-keys / --no-inject-api-keys |
Inject the provider's API keys for approved domains (default: allow). Say no when using configurations or subscriptions instead of API keys |
--inject-env / --no-inject-env |
Configure the provider's common env vars: forward non-secret vars such as base URLs (default: allow) |
--output PATH |
Write the config somewhere other than .spens.config.json |
--force |
Overwrite an existing config without asking |
--dry-run |
Print the generated config to stdout without writing any file |
spens list
spens list
Prints all available environments and agents, including local templates from the workspace templates/ directory.
spens log-viewer
spens log-viewer [workspace] [options]
| Option | Description |
|---|---|
--port PORT |
Port for the web server (default: 7331) |
--host HOST |
Host to bind (default: 127.0.0.1) |
--no-browser |
Do not open a browser automatically |
--spens-dir PATH |
Read sessions from a specific spens directory instead of searching for .spens near the workspace |
spens pricing
spens pricing [options]
| Option | Description |
|---|---|
--refresh |
Re-fetch every provider's pricing dataset into the local cache |
--vendor |
Re-fetch and also update the snapshots shipped in spens/data/pricing (needs a writable checkout) |
--provider NAME |
Limit to one provider (repeatable). Providers: anthropic, openai, google, fireworks-ai, openrouter, bedrock |
--model ID |
Show the resolved price for one model id |
--tokens N |
Context size used to pick a banded model's price with --model |
--offline |
Do not fetch; report what cached and vendored data provide |
How costs are computed is described in Cost estimation.
Spens config parameters
Spens reads an optional .spens.config.json from the workspace root. spens init generates one for you; you can also write it by hand. To use a different config file for a single session, pass --config PATH (e.g. spens node-24 pi . --config .spens.config.ocr.json). A full example:
{
"egress": "enforced",
"nono_override": "profile_override.json",
"additional_capture_urls": ["*opencode.ai*"],
"exclude_capture_urls": ["*models.opencode.ai*"],
"env": ["OPENAI_BASE_URL"],
"pre_sandbox_commands": ["npm install"],
"domain_rules": [
{"pattern": "*api.github.com*", "allow": ["GET"]},
{"pattern": "*pypi.org*", "allow": ["*"]},
{"pattern": "*api.anthropic.com*", "allow": ["GET", "POST"]}
],
"inject_headers": [
{
"placeholder": "ANTHROPIC_API_KEY",
"env_var": "ANTHROPIC_API_KEY",
"for_domains": ["*api.anthropic.com*"]
}
],
"pricing": {"live_fetch": false}
}
Extending the generated nono profile
When you use nono_override, the generated profile is replaced at runtime. The generated profile is still preserved inside the image as spens-base.json, so your override can extend it rather than recreating all the Spens internals. For example, a profile_override.json that only adds an extra allowed folder:
{
"extends": ["spens-base"],
"allow_folders": ["/tmp/custom-cache"]
}
This inherits the proxy environment variables, cache sanitization, placeholder injection, and any base-pack extensions that Spens generates, and layers your own rules on top.
Default network policy
Two mechanisms control the agent's network access, and both fail closed:
- Non-HTTP protocols always fail closed. The agent's Docker network has no route to the internet except through the interceptor, and the interceptor refuses tunnels that do not speak HTTP. SSH, raw sockets, and other non-HTTP protocols cannot get out, regardless of configuration.
- HTTP/HTTPS filtering depends on
domain_rules. Ifdomain_rulesis omitted or empty, every domain is denied (fail closed). Traffic is still routed through the interceptor, so any request that is allowed by an explicit rule is captured and logged. Once any rule is configured, the policy stays fail closed: a hostname that matches no rule is blocked, and HTTPS tunnels to unmatched hosts are refused before they are established.
If you want allow-by-default HTTP egress, add a wildcard rule such as {"pattern": "*", "allow": ["*"]} to domain_rules. spens init generates a ruleset for your stack and provider; answering manual for both the stack and the provider produces an empty ruleset, which means deny-all.
When several rules match a hostname, the most specific one governs the request, regardless of the order the rules are declared in. For example, with a public-GET catch-all {"pattern": "*", "allow": ["GET"]} plus {"pattern": "*api.anthropic.com*", "allow": ["GET", "POST"]}, POST requests to api.anthropic.com are allowed while every other host is still limited to GET. Specificity is the number of domain labels in the pattern, so api.anthropic.com overrides anthropic.com, which overrides *. If two matching rules are equally specific (e.g. *.anthropic.com and anthropic.com), the first one declared wins.
A rule whose allow key is missing or is not a list is malformed and never widens permissions: it is skipped entirely, so the next most specific well-formed matching rule governs instead, and the request is denied if none does. An explicitly empty allow list ([]) is well-formed and denies every method on that host without falling back.
Field reference
| Field | Description |
|---|---|
egress |
"enforced" (default) or "legacy". Enforced: the agent runs on an isolated internal Docker network whose only neighbor is the interceptor, so all egress, including DNS, is forced through it and non-proxied protocols fail closed. Legacy: the agent shares the interceptor's network namespace and proxy env vars are advisory only. Not recommended; kept for migration |
nono_override |
Path to a custom nono profile JSON file, replacing the auto-generated combined profile. The generated profile is always preserved inside the image as spens-base.json, so your override can use "extends": ["spens-base"] to layer your own rules on top of Spens internals (proxy env vars, cache sanitization, placeholders, base-pack extensions) instead of recreating them |
additional_capture_urls |
Glob-style URL patterns to additionally capture in the interceptor. The old typo addition_capture_urls is still accepted for backward compatibility |
exclude_capture_urls |
Glob-style URL patterns to skip in the interceptor |
env |
Environment variable names to forward from the host to the agent container with their real values. Use for non-secret variables only |
pre_sandbox_commands |
Shell commands to run inside the container before the sandbox starts the agent (e.g. npm install). They run unsandboxed as the unprivileged agent user, and their network traffic goes through the interceptor |
domain_rules |
Per-domain method restrictions. Each rule has a glob pattern (matched against the request hostname only) and an allow list of HTTP methods, or ["*"] for all methods. When multiple rules match, the most specific pattern governs the request (see Default network policy). Fails closed: a hostname that matches no rule is blocked, and omitted or empty domain_rules means deny-all. A rule with a missing or non-list allow is skipped (it never defaults to allow-all) |
inject_headers |
Secret substitution rules. See How inject_headers works |
pricing |
Costing options. {"live_fetch": false} stops spens fetching the pricing dataset at the end of a session; prices then come from the vendored snapshot and the built-in table |
An example lives in config_example/.spens.config.json.
How inject_headers works
inject_headers rules keep real secrets out of the agent container. Each rule has three parts:
placeholder- the literal string the agent is given as its "key"env_var- the host environment variable that holds the real secretfor_domains- the hostnames the secret may be substituted on
A worked example. Suppose your host environment has ANTHROPIC_API_KEY=sk-ant-secret123 and your config contains the rule from the example above. At runtime:
- The agent container is started with
ANTHROPIC_API_KEY=ANTHROPIC_API_KEY. The variable name is real, but its value is the literal placeholder string. The agent reads it and sends it in its API requests, but it holds no secret. - The interceptor container is started with the real value of
ANTHROPIC_API_KEYfrom your host. - When the agent sends a request whose hostname matches
for_domains(here,api.anthropic.com), the interceptor replaces every occurrence of the placeholder string with the real secret before forwarding the request upstream. - On any other hostname, the placeholder is left untouched, so the real secret can never be attached to a request to a destination you did not authorize.
- Real secret values are masked out of the request log and trace files before anything is written to disk.
placeholder and env_var do not have to share a name. If your host stores the secret under ANTHROPIC_KEY, write "placeholder": "ANTHROPIC_API_KEY", "env_var": "ANTHROPIC_KEY" - the agent still sees ANTHROPIC_API_KEY, and the interceptor reads the real value from ANTHROPIC_KEY. spens init generates rules where both names are the provider's conventional variable name.
A rule without for_domains (or with an empty list) authorizes no hostnames, so its placeholder is never substituted.
Custom environment parameters
An environment template is a JSON file with "template_type": "environment". It defines the base Docker image, system packages, and the nono.sh installation.
| Field | Required | Description |
|---|---|---|
template_type |
yes | Must be "environment" |
name |
yes | Name used on the command line |
base-image |
yes | Docker base image for the environment |
nono-command |
yes | Shell command that installs the nono shell in that image |
packages |
no | Additional system packages to install |
package_manager |
no | Package manager used to install packages: apt (default) or apk |
nono_base_config |
no | nono.sh base profile to pull and extend. Omit to use only the agent's profile |
agent_user |
no | The unprivileged user the agent runs as: {"name", "uid", "home", "create"}. Defaults to a created spens user (uid 1000, home /home/spens). The node environments reuse the image's existing node user. Paths in agent templates starting with ~ expand to this user's home |
Custom agent parameters
An agent template is a JSON file with "template_type": "agent". It defines how to install and configure a specific coding agent.
| Field | Required | Description |
|---|---|---|
template_type |
yes | Must be "agent" |
name |
yes | Name used on the command line. It is also the command used to start the agent |
installation_command |
yes | Shell command that installs the agent binary. Runs as the unprivileged agent user, installing into its home |
dependencies |
no | Shell commands to run before installation (e.g. installing prerequisites). Runs as root during build, with HOME pinned to the agent user's home |
nono_base_config |
no | nono.sh base profile to pull and extend |
relocate_binary |
no | Moves the installed binary to a standard location and cleans up install artifacts: {"from", "to", "cleanup"}. cleanup is a path or list of paths to remove |
config_files |
no | Static files written into the image at build time. A map of container path to file content |
configuration |
no | Ordered list of host config mounts. Each entry has a source, a destination, an optional include glob list, and an optional exclude glob list. Only existing sources and matching files are mounted. source accepts the {workspace} token (the resolved workspace directory) and the {spens_dir} token (the session's spens directory). destination paths starting with ~ expand to the agent user's home. When multiple entries target the same container file, the entry listed first wins |
allow_folders |
no | Directories the agent is allowed to access |
env |
no | Environment variables to set in the container, in KEY=value format |
packages |
no | Additional system packages to install in the image |
yolo_command |
no | Command template with a {prompt} placeholder, used to run the agent non-interactively when a prompt is passed on the CLI. The placeholder is filled at runtime, so the image is not rebuilt per prompt. If omitted, passing a prompt is an error |
The built-in agents use these yolo commands:
| Agent | Yolo command |
|---|---|
opencode |
opencode run --auto {prompt} |
codex |
codex exec --sandbox danger-full-access {prompt} |
claude |
claude -p {prompt} --dangerously-skip-permissions |
pi |
pi -p {prompt} |
Contributing
For development setup, running the tests, linting, and the project layout, see CONTRIBUTING.md.
Architecture
For a technical explanation of how spens works - the two-container setup, network enforcement, the interceptor, the nono sandbox, policy masking, and the session data model - see ARCHITECTURE.md.
License
Spens is MIT licensed. See license.md.
Release files for spens-ai 1.2.7
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| spens_ai-1.2.7.tar.gz | 21.9 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| spens_ai-1.2.7-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 22.5 MB
Release files / spens_ai-1.2.7.tar.gz
| Download URL | spens_ai-1.2.7.tar.gz |
|---|---|
| Size | 21.9 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
89794a10189d520aeef7eaafa134b2875cebf72a7612a8f70bed8d6dfe1d2551
|
|
BLAKE2b-256 checksum How to use checksums |
ddb4ecea7392e533e47daf2c77cab618e9309717498e86f1aa566b13fe7a5f2f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / spens_ai-1.2.7-py3-none-any.whl
| Download URL | spens_ai-1.2.7-py3-none-any.whl |
|---|---|
| Size | 556.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b35c61632bd057e8e6e7d7827103645393a430caf7acabc0836e18ea677b9307
|
|
BLAKE2b-256 checksum How to use checksums |
b3fac2b9dfb330d17aa189f9d4ca72f14cd1a0cc1589ea5faa3cc94402489d53
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|