spens-acp
A shim that exposes agents running in the spens sandbox over the Agent Client Protocol (ACP).
Overview
spens is a sandbox for running AI coding agents (claude, codex, opencode, pi, …) inside isolated Docker containers with full network interception, egress policy, and audit logging — every LLM call, tool invocation, and file change is captured and reviewable.
spens-acp is the bridge between your editor and that sandbox. It speaks ACP — JSON-RPC 2.0 over stdio — on one side, and drives the spens CLI on the other. Any ACP-compatible client (Zed, VS Code with an ACP extension, …) can then use spens-sandboxed agents from its normal agent panel: the editor thinks it is talking to an ordinary ACP agent, while every prompt actually runs as a fully sandboxed, intercepted, auditable spens session.
What you get:
- Editor-native agent UX — chat, streamed output, tool activity, and cancellation from your editor's agent panel, for any agent spens supports.
- Sandboxing by default — each prompt runs in an ephemeral container with network policy, secret injection, and audit logs (see the spens docs).
- Continuous conversations — consecutive prompts in a thread behave like
one agent session (history is replayed as context), and sessions survive
editor restarts (
session/resume). - Full observability — everything spens captures (chat transcripts, HTTP
traffic, file changes) is still available via
spens log-viewer.
Quick start
Prerequisites
- Python 3.12+
- The spens CLI installed and on
$PATH, with Docker working (spens listshould print environments and agents) - An ACP-capable editor: Zed (native support) or VS Code with an ACP client extension
1. Install spens-acp
From a checkout of this repository:
# with uv (recommended) — installs the `spens-acp` command onto your PATH
uv tool install .
# or with pip
pip install .
For development:
uv sync # create .venv from uv.lock
uv run spens-acp --help
Verify the install (and that spens is reachable):
spens-acp --help
2. Configure the project you want to work on
For each project/workspace you want to drive through spens-acp, add
default_env and default_agent to .spens.config.json at the project root
(create the file if it does not exist):
{
"default_env": "python-3.12",
"default_agent": "pi"
}
default_env— the spens environment (container image + toolchain) to run in. Built-ins:node-20,node-22,node-24,python-3.11,python-3.12,python-3.13. Runspens listto see everything available.default_agent— the agent to run. Built-ins:claude,codex,opencode,pi.
The workspace config is the recommended way to configure spens-acp: one file
per project, and every editor/client that opens the project picks it up
automatically. (You can instead pass --env/--agent flags or
SPENS_ENV/SPENS_AGENT env vars per launch — see
Configuration — but then you must configure each
editor entry separately.)
.spens.config.jsonis also spens' own config file. Keys likedomain_rules,inject_headers,env, andaddition_capture_urlsconfigure the sandbox itself — see the spens docs. spens-acp only readsdefault_env/default_agentfrom it.
3. Smoke test (no editor needed)
Run one real prompt through the whole pipeline to confirm the wrapper, spens, and your project config all work:
cd /path/to/your/project
spens-acp --smoke-test "reply with hello"
It drives initialize / session/new / session/prompt against your real spens binary in the current directory and prints every session update to stderr. If this passes, the stack is healthy and any editor problem is between the editor and the wrapper (see Troubleshooting).
4. Zed
Zed has native ACP support. Agents are configured under External Agents:
- Open Zed's settings (
zed: open settings, orcmd-,). - Go to AI → General → External Agents (or run
agent: open settingsfrom the command palette and pick the External Agents page). - Click Add Agent → Add Custom Agent — Zed opens your
settings.jsonwith anagent_serversentry to fill in:
{
"agent_servers": {
"spens": {
"type": "custom",
"command": "spens-acp",
"args": [],
"env": {
"FIREWORKS_API_KEY": "fw-…"
}
}
}
}
Everything in the env block is passed to the spens-acp process (and
inherited by the spens sessions it launches). Use it for:
- Provider API keys —
FIREWORKS_API_KEY,ANTHROPIC_API_KEY, etc. The real key stays on the host: pair it with aninject_headersrule in the project's.spens.config.json, and spens' interceptor substitutes it into requests to the authorized domains only — the key itself never enters the sandbox (see the spens docs). - Any
SPENS_*option from the options table — e.g.SPENS_ENV/SPENS_AGENTto pin an env/agent for this editor entry instead of relying on the workspace config,SPENS_BINif the spens binary is not on Zed'sPATH, orSPENS_DEBUG=1while setting things up.
Zed picks up settings changes automatically — no restart needed. Then:
- Open your project folder in Zed (the folder containing
.spens.config.json). - Open the agent panel (
cmd-?on macOS,ctrl-?elsewhere). - Click + to start a new thread and select spens as the agent.
- Chat — each message runs one sandboxed spens session in the workspace.
If spens-acp is not on Zed's PATH (e.g. it lives in a venv), point
command at the interpreter instead:
"command": "python3", "args": ["-m", "spens_acp"]
5. VS Code
VS Code needs an ACP client extension — it does not ship with one. Install
ACP Client (formulahendry.acp-client) from the VS Code Marketplace
(alternatives exist, e.g. strato-space.acp-plugin, which uses the same
Zed-style agent_servers format).
Add the agent to your VS Code settings.json:
{
"acp.agents": {
"spens": {
"command": "spens-acp",
"args": [],
"env": {
"FIREWORKS_API_KEY": "fw-…"
}
}
}
}
The env block works exactly like Zed's — pass provider API keys (paired
with an inject_headers rule in the project's .spens.config.json) and any
SPENS_* options there.
Then:
- Open the ACP panel from the Activity Bar (ACP icon).
- Connect to the spens agent.
- Open your project folder and start chatting.
The extension spawns the agent process from the VS Code process itself — make sure
spens-acp(andspens) are on itsPATH. Launch VS Code from a shell where they are installed, or use an absolute path incommand.
Running it manually
spens-acp --env python-3.12 --agent pi
# or as a module:
python3 -m spens_acp --env python-3.12 --agent pi
# or via environment variables:
SPENS_ENV=python-3.12 SPENS_AGENT=pi spens-acp
This speaks JSON-RPC 2.0 on stdio — it is the interface editors spawn, not
meant for interactive use. Use --smoke-test for a human-readable run.
Configuration / options
Environment / agent resolution
Precedence (first match wins):
session/newparams (env/agent, top-level or in_meta)- Explicit argument —
--env/--agentorSPENS_ENV/SPENS_AGENT - Workspace config —
<workspace>/.spens.config.jsonkeysdefault_env/default_agent - Error —
"No environment/agent configured: pass --env/--agent or set default_env/default_agent in .spens.config.json"
All options
| Env var / flag | Default | Meaning |
|---|---|---|
SPENS_BIN |
spens |
Path to spens binary (see Windows shims) |
SPENS_ENV / --env |
— | Explicit environment |
SPENS_AGENT / --agent |
— | Explicit agent |
SPENS_CHANGES |
accept |
accept | reject → --accept-changes / --reject-changes |
SPENS_REBUILD |
auto |
auto (never pass flag) | always (pass --rebuild) |
SPENS_DIR |
<workspace>/.spens |
Override --spens-dir |
SPENS_CONFIG / --config |
— | Path passed to spens' --config: use this config file instead of the workspace's .spens.config.json (also forwarded to spens list for env/agent validation). One layer of surrounding quotes is stripped (some editors pass --config "C:\\path.json" with the quotes baked in), and the file's existence is checked at session setup |
SPENS_INCLUDE_AGENT_OUTPUT |
false |
Stream agent_output events as thought chunks |
SPENS_EMIT_SUMMARY |
true |
Emit final summary thought chunk (tokens / cost / files) |
SPENS_TOOL_RESULT_MAX |
2000 |
Truncate tool-result content in updates |
SPENS_HISTORY |
true |
Replay earlier turns of the ACP session as transcript context in each spens prompt (continuous-session behavior) |
SPENS_HISTORY_MAX_TURNS |
20 |
Maximum earlier turns included in the replayed transcript |
SPENS_HISTORY_MAX_CHARS |
24000 |
Character budget for the replayed transcript; oldest turns are dropped (whole turns) until it fits |
SPENS_RESUME |
true |
Persist session state to <spens-dir>/acp-sessions/ and support session/resume |
SPENS_POLL_INTERVAL |
0.25 |
File tailer poll interval (seconds) |
SPENS_LAUNCH_TIMEOUT |
60 |
Seconds to wait for spens --output background to print the session id |
SPENS_SESSION_DIR_TIMEOUT |
60 |
Seconds to wait for the session directory to appear after a confirmed launch |
SPENS_CANCEL_ON_EXIT |
true |
SIGTERM/SIGINT/EOF → spens cancel in-flight sessions |
SPENS_DEBUG |
— | Set to 1 for verbose stderr diagnostics (argv, events, state transitions) |
Windows: .cmd/.bat shims (prompts with quotes)
A prompt can contain any characters — quotes, &, |, %, … — and it
must reach the agent verbatim. On Windows this needs one extra step:
when spens resolves to a .cmd/.bat shim (npm shims, hand-written
wrappers), the OS executes the shim through cmd.exe, which re-parses
the command line with its own quoting rules. Python's argument escaping
(\") is invisible to cmd.exe, so a prompt containing quotes (or
& | < > ^) would be mangled or split into several arguments and spens
would fail with an argument-parsing error. There is no escaping that is
safe for cmd.exe (the "BatBadRe" problem), so spens-acp instead
resolves the shim to the interpreter command it wraps (e.g.
python -m spens, <python.exe> <script.py>, node <pkg>/bin/spens.js)
and spawns that directly — the whole argv list stays free of any shell,
so prompt content travels verbatim. One-line wrappers, npm cmd-shim
shims, py -3 launchers and call chains are understood; a shim that
cannot be parsed statically is spawned directly as before, with a
warning on stderr (set SPENS_BIN to the interpreter the shim runs in
that case). .exe launchers (pip/uv/pipx installs) never go through
cmd.exe and need no resolution.
Example workspace config
A real-world .spens.config.json (this repo's own), combining the
spens-acp keys with spens' sandbox policy keys:
{
"default_env": "python-3.12",
"default_agent": "pi",
"addition_capture_urls": ["*api.fireworks.ai*", "*api.anthropic.com*"],
"env": ["FIREWORKS_BASE_URL", "FIREWORKS_ACCOUNT_ID"],
"domain_rules": [
{ "pattern": "*pypi.org", "allow": ["GET", "HEAD", "POST"] },
{ "pattern": "*api.anthropic.com", "allow": ["GET", "POST", "PUT", "DELETE", "OPTIONS"] }
],
"inject_headers": [
{
"placeholder": "FIREWORKS_API_KEY",
"env_var": "FIREWORKS_API_KEY",
"for_domains": ["*api.fireworks.ai*"]
}
]
}
Only default_env / default_agent are read by spens-acp; the rest is
spens' own configuration (see the spens docs).
Continuous sessions (history replay)
spens itself is stateless — each prompt turn launches a fresh spens session.
To make an ACP session behave like one continuous conversation, the wrapper
records every completed exchange (user prompt + final assistant text from
captured.jsonl) and replays them as a compact transcript prefix on the next
prompt:
You are continuing an existing conversation. ...
User: <earlier prompt>
Assistant: <earlier reply>
User: <new prompt>
Only successful (end_turn) turns are recorded; failed or cancelled launches
are not replayed. The transcript is bounded by SPENS_HISTORY_MAX_TURNS and
SPENS_HISTORY_MAX_CHARS — oldest turns are dropped whole, with an
[... N earlier turn(s) omitted ...] marker — and an oversized single
exchange is mid-truncated with .... Disable the whole feature with
SPENS_HISTORY=0 (every prompt then launches spens with the bare user
message).
Resuming sessions (session/resume)
The wrapper process is itself stateless — when the client (editor) restarts
or reconnects, it spawns a fresh spens-acp with no memory. To let those
sessions continue, every ACP session's state (workspace cwd, resolved
env/agent, the replay-turn history, and the spens session ids already used)
is mirrored to a small JSON file:
<spens-dir>/acp-sessions/<sessionId>.json
Writes are atomic and best-effort — a failed write never fails the prompt it
serves, it only costs resumability. session/resume (which initialize
advertises via sessionCapabilities.resume) reloads that file in a fresh
process, re-validates the binary and env/agent, and restores the history so
the next prompt continues the conversation. The spens-id sequence continues
too, so a resumed turn never reuses a session directory already on disk.
Notes:
- A session can only be resumed in the workspace it was created in (the
cwdof the resume request must match); withSPENS_DIRpointing at a shared directory the state is found there instead. - The spens id is persisted before each launch, so even a crash mid-turn cannot make a later resume reuse the id.
- Disable with
SPENS_RESUME=0: no state is written, the capability is not advertised, and cross-process resume answersinvalid params(same-process resume of a live session still works). session/resumeis still flagged unstable in this SDK build, so the agent enablesuse_unstable_protocolon its router; unhandled unstable methods still answermethod not foundindividually.
Architecture
ACP client (Zed / VS Code / …)
│ JSON-RPC 2.0 over stdio (official agent-client-protocol SDK)
▼
spens-acp (Python; acp.Agent implementation)
│ subprocess (argv list, no shell) polling tailers
├──────────────────────────► spens CLI ──► <spens-dir>/sessions/<id>/
│ │ events.jsonl (lifecycle)
│ │ state.json (state machine)
│ │ traces/captured.jsonl (LLM traffic)
▼
session/update notifications ◄── decode + map
How it works
The wire protocol is implemented with the official
agent-client-protocol
SDK (acp.run_agent + AgentSideConnection), which provides the
cross-platform stdio transport (thread-based feeder on Windows,
connect_read_pipe on POSIX), JSON-RPC framing, request dispatch, and schema
validation. spens_acp.acp_server.SpensAgent implements the acp.Agent
protocol; internal update dataclasses from the decoders are converted to SDK
SessionUpdate types at the boundary.
Life of a prompt:
session/new— the agent generates ansessionId, records the workspacecwd, and resolves env/agent (session params → flags/env vars → workspace.spens.config.json), validating them againstspens list.session/prompt— the prompt is flattened to a string, earlier turns of the conversation are prepended as a transcript prefix (history replay), and one spens session is launched in yolo mode:spens <env> <agent> <workspace> "<prompt>" --output background --accept-changes --session-id <id>(spawned as an argv list, no shell).- While it runs, three concurrent watchers poll the session directory on
disk:
events.jsonl(lifecycle events → tool-call / plan / thought updates),state.json(state machine → terminal-state detection), andtraces/captured.jsonl(raw LLM API traffic → streamedagent_message_chunkupdates via provider-specific decoders). - Every decoded update is sent to the client as a
session/updatenotification; a framingspens <env>/<agent>tool call shows activity from the moment of launch. - Terminal state (
finished/canceled/error) → the tool call is closed, the exchange is recorded in the session history, andsession/promptreturns aPromptResponsewithstopReason(end_turn/cancelled; anerrorstate fails the request with a JSON-RPC error instead). session/cancel— callsspens cancel <id>; the in-flight prompt resolves withstopReason: "cancelled".
The wrapper is intentionally loose — it couples only to spens' documented CLI surface and on-disk session files. It never imports spens as a library, and degrades gracefully when spens emits records it does not recognise (skip + log, never crash).
ACP surface
| Method | Behaviour |
|---|---|
initialize |
Echoes the client's protocolVersion, advertises agentInfo, validates spens binary and explicit env/agent config |
session/new |
Agent generates sessionId (official protocol). Records workspace cwd, resolves env/agent from params, workspace config, or error |
session/resume |
Restores a session in a fresh wrapper process from persisted state; validates cwd/env/agent, continues the conversation and spens-id sequence |
session/prompt |
Flattens prompt → string, prepends replayed session history, launches one spens yolo session, streams session/update notifications, returns PromptResponse |
session/cancel |
Notification. Calls spens cancel <id>; in-flight session/prompt resolves with stopReason: "cancelled" |
Not implemented: session/set_mode, session/load, file-system
methods, permission requests → JSON-RPC method not found (-32601).
Provider format support
spens' interceptor captures LLM API traffic raw; the wrapper decodes it by request URL so the client sees real streamed assistant output:
| URL pattern | Decoder | Used by |
|---|---|---|
*/chat/completions |
OpenAI Chat Completions | opencode, pi, Fireworks, OpenRouter, … |
*/v1/messages (api.anthropic.com) |
Anthropic Messages | claude |
*/v1/responses |
OpenAI Responses | codex |
Unknown URLs degrade to the agent_output fallback rather than crashing.
Protocol notes
The implementation follows the official ACP schema (via the SDK); a few points worth knowing where it differs from a naive reading of the spec:
protocolVersionis an integer (u16); the agent echoes the client's integer back (legacy date strings are coerced to1on input).sessionIdinsession/newis generated by the agent, not supplied by the client.PromptResponsehas nomessagefield — final assistant text is streamed incrementally viaagent_message_chunkupdates; a fallback chunk ("Spens session finished…") is sent only when no text was decoded.stopReasonhas no"error"value — a spens session ending in theerrorstate failssession/promptwith a JSON-RPC error (-32603).- Error codes
-32000/-32002are reserved by ACP (authentication / resource-not-found) and clients discard the agent's text for them, so spens-acp uses-32603/-32602for its own errors. - No
"cancelled"tool-call status exists — the framing spens tool call is closed withfailedwhen a prompt is cancelled; overall cancellation is signalled viastopReason: "cancelled". - spens
env/agentselection travels insession/newparams — either as top-levelenv/agentkeys (spens-native clients) or in the official_metaextensibility field (official-SDK clients); both converge on the same handler.
stdout discipline
Only JSON-RPC ever goes to stdout (the SDK owns the stdio transport). All
wrapper logging, spens subprocess stdout/stderr, and decoder warnings go to
stderr. Key milestones (launch confirmed, session ended) are always logged to
stderr; set SPENS_DEBUG=1 for argv / event / state-transition detail.
Project layout
spens_acp/
__init__.py
__main__.py # python -m spens_acp entrypoint
main.py # env/flags → acp.run_agent() + signal handling + --smoke-test
acp_server.py # SpensAgent: acp.Agent implementation + session lifecycle
event_mapper.py # events.jsonl → internal updates (spec §7.3)
history.py # Per-session turn history → replayed transcript prefix
persist.py # Session state → <spens-dir>/acp-sessions/ (for session/resume)
launcher.py # argv builder + subprocess spawn + cancel
watcher.py # Polling JSONL tailer (partial-line safe, resume by offset)
decoder/
__init__.py # URL-based dispatch
chatcompletions.py # OpenAI Chat Completions (golden-verified)
anthropic.py # Anthropic Messages (spec-built + synthetic tests)
responses.py # OpenAI Responses (spec-built + synthetic tests)
common.py # Tool kind mapping, result dedup, truncation
session_map.py # ACP sessionId ↔ spens session id
config.py # Env/flag/workspace-config resolution
types.py # Internal update dataclasses + SDK conversion + prompt flattening
tests/
test_decoder_chatcompletions.py # Unit tests + golden fixture from example-data
test_decoder_anthropic.py # Synthetic SSE events
test_decoder_responses.py # Synthetic events
test_event_mapping_golden.py # Real example-data events.jsonl → ACP updates
test_history.py # History replay + truncation budgets
test_persist.py # Resume state files: paths, sanitize, round-trip
test_fake_spens_e2e.py # Full prompt + cancel + error lifecycle (no Docker)
test_protocol_e2e.py # Wire test: SDK client ↔ agent subprocess over stdio
test_acp_server.py # SpensAgent handlers + event mapping
test_config.py # Resolution + argv builder
test_session_map.py # ID sanitize, collision, truncation
test_prompt_flattening.py # SDK blocks + legacy dicts, image rejection
test_tool_kind.py # §7.5 mapping table
test_truncation.py # SPENS_TOOL_RESULT_MAX
test_watcher.py # Partial lines, growing files, late creation
Troubleshooting
First, split the problem in half — run one real prompt through the whole pipeline without any editor:
spens-acp --smoke-test "reply with hello" --env python-3.12 --agent pi
If the smoke test passes but your editor hangs, the problem is between the editor and the wrapper (see the log decision tree below); if the smoke test hangs or fails, its last line shows exactly where the wrapper stopped, with the session dir it was waiting for.
Log decision tree — every line below goes to stderr (SPENS_DEBUG=1
adds argv / event / state detail). In Zed, agent stderr lands in the Zed log
(zed: open log):
spens-acp 0.1.x starting; spens binary: …— no line at all means an old build is installed or the agent never startedsession '…' created (cwd=…, env=…, agent=…)— session/new OKprompt received for session '…'— the prompt request arrivedlaunching spens session '…' in <workspace>— right before spawnspens session '…' confirmed via …— launch confirmed (session dir on disk, or the id on stdout); stuck before this line means the session dir never appeared where expected (theSPENS_DEBUGlineexpecting session dir: …shows the exact path — compare it with where spens actually wrote the session)spens session '…' ended: state='…' -> …— terminal state reached
Prompt hangs forever — launch no longer waits for spens' pipes to close
(detached session children can hold them open after the CLI exits) and no
longer depends on spens' stdout at all (a piped Python CLI block-buffers
stdout, so the session-id line can be trapped until exit — or lost entirely
if spens daemonizes with os._exit()). Launch is confirmed by the session
directory appearing on disk, both pipes are drained for the launcher's
lifetime so a foreground spens can never freeze on a full pipe, and if
nothing confirms within SPENS_LAUNCH_TIMEOUT the launcher is killed with a
clear error. If the session directory never appears where the wrapper expects
it (<workspace>/.spens/sessions/<id>/, or SPENS_DIR), the prompt fails
after SPENS_SESSION_DIR_TIMEOUT with the exact path it was waiting for.
Windows: "Failed to launch spens: [Errno 22] Invalid argument" even though
the container started — an errno-only [Errno 22] on Windows is a write()
to a pipe whose reader is gone (the CRT reports broken pipes as plain
EINVAL). The reader in question is the editor's stderr drain: Zed reads
agent stderr as UTF-8, but a piped sys.stderr in Python ≤ 3.14 uses the
ANSI code page (cp1252, …). The first log line containing a non-ASCII
character (an em-dash or curly quote in replayed conversation history —
which only exists from the second turn on, or in a resumed session after an
editor restart) arrives as invalid UTF-8, Zed's reader stops and closes the
pipe, and the next log write inside the wrapper raises [Errno 22] —
typically inside launch(), right after the spens child (and its Docker
container) was already spawned. That is why the turn is reported failed
while the sandbox session runs to completion. spens-acp now reconfigures
stdio to UTF-8 (backslashreplace) at startup and makes every log write
best-effort, so neither the encoding mismatch nor a closed pipe can fail a
turn. On builds older than this fix, the workaround is PYTHONUTF8=1 in the
agent's env block (or plain-ASCII logs).
Windows: prompts that grew too long fail at spawn — follow-up turns
embed up to SPENS_HISTORY_MAX_CHARS of replayed transcript as one command
line argument, and Windows' CreateProcess limit is 32,767 characters for
the whole command line. spens-acp checks this before spawning and fails
fast with a message naming the knobs to lower (SPENS_HISTORY_MAX_CHARS,
SPENS_HISTORY_MAX_TURNS, SPENS_HISTORY=0).
Running tests
# All tests. The ACP SDK must be importable; if it is not installed
# into the environment (e.g. wheels were extracted manually), point
# PYTHONPATH at it:
PYTHONPATH=.local-packages/site-packages python -m unittest discover -s tests -v
test_protocol_e2e.py spawns the real agent entrypoint as a subprocess with
a fake spens executable and drives it through the SDK's
ClientSideConnection — the same path a real editor client takes (POSIX-only;
skipped on Windows).
Golden tests use the real example-data/ session capture shipped with this
repo (OpenAI Chat Completions). Synthetic fixtures cover Anthropic Messages
and OpenAI Responses until real captures are available.
License
Same as spens.
Release files for spens-acp 1.0.3
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_acp-1.0.3.tar.gz | 9.6 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| spens_acp-1.0.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 9.7 MB
Release files / spens_acp-1.0.3.tar.gz
| Download URL | spens_acp-1.0.3.tar.gz |
|---|---|
| Size | 9.6 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
52e4fcce366c74c4783bfa91710ea03d83d04d245b4451bfcf3074f86f1bd47b
|
|
BLAKE2b-256 checksum How to use checksums |
d18ab38111ae2f59b781b05b8cadd93ae7253fbcc55705c404e2d871928de96d
|
| 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_acp-1.0.3-py3-none-any.whl
| Download URL | spens_acp-1.0.3-py3-none-any.whl |
|---|---|
| Size | 52.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b95830dc7c1351794fef9dfe8d2e617c5182d8850992ff217a438a3eec077b41
|
|
BLAKE2b-256 checksum How to use checksums |
1f22fcc491d0b77028561ac8398dc79503e3615ca168f214a3a0d6e706998d33
|
| 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}
|