spens-ai 1.2.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spens_ai-1.2.5/.gitconfig +2 -0
- spens_ai-1.2.5/.gitignore +9 -0
- spens_ai-1.2.5/.gitlab-ci.yml +270 -0
- spens_ai-1.2.5/.spens.config.json +31 -0
- spens_ai-1.2.5/.spens.config.ocr.json +23 -0
- spens_ai-1.2.5/ARCHITECTURE.md +83 -0
- spens_ai-1.2.5/CHANGELOG.md +154 -0
- spens_ai-1.2.5/CONTRIBUTING.md +88 -0
- spens_ai-1.2.5/PKG-INFO +522 -0
- spens_ai-1.2.5/README.md +507 -0
- spens_ai-1.2.5/agent.md +204 -0
- spens_ai-1.2.5/config_example/.spens.config.json +18 -0
- spens_ai-1.2.5/docs/.gitignore +27 -0
- spens_ai-1.2.5/docs/README.md +45 -0
- spens_ai-1.2.5/docs/app/(home)/layout.tsx +6 -0
- spens_ai-1.2.5/docs/app/(home)/page.tsx +182 -0
- spens_ai-1.2.5/docs/app/api/search/route.ts +10 -0
- spens_ai-1.2.5/docs/app/docs/[[...slug]]/page.tsx +71 -0
- spens_ai-1.2.5/docs/app/docs/layout.tsx +10 -0
- spens_ai-1.2.5/docs/app/global.css +75 -0
- spens_ai-1.2.5/docs/app/layout.tsx +27 -0
- spens_ai-1.2.5/docs/app/llms-full.txt/route.ts +7 -0
- spens_ai-1.2.5/docs/app/llms.mdx/docs/[[...slug]]/route.ts +24 -0
- spens_ai-1.2.5/docs/app/llms.txt/route.ts +7 -0
- spens_ai-1.2.5/docs/app/og/docs/[...slug]/route.tsx +25 -0
- spens_ai-1.2.5/docs/components/mdx.tsx +15 -0
- spens_ai-1.2.5/docs/components/site-footer.tsx +25 -0
- spens_ai-1.2.5/docs/content/docs/cli.mdx +103 -0
- spens_ai-1.2.5/docs/content/docs/comparisons.mdx +112 -0
- spens_ai-1.2.5/docs/content/docs/config.mdx +96 -0
- spens_ai-1.2.5/docs/content/docs/custom-agents.mdx +76 -0
- spens_ai-1.2.5/docs/content/docs/custom-environments.mdx +52 -0
- spens_ai-1.2.5/docs/content/docs/examples/spens-in-ci-cd.mdx +123 -0
- spens_ai-1.2.5/docs/content/docs/examples/spens-with-playwright.mdx +114 -0
- spens_ai-1.2.5/docs/content/docs/getting-started.mdx +121 -0
- spens_ai-1.2.5/docs/content/docs/log-viewer.mdx +63 -0
- spens_ai-1.2.5/docs/content/docs/meta.json +21 -0
- spens_ai-1.2.5/docs/content/docs/pricing.mdx +41 -0
- spens_ai-1.2.5/docs/content/docs/spens-acp.mdx +167 -0
- spens_ai-1.2.5/docs/content/docs/using-spens-with-ide.mdx +122 -0
- spens_ai-1.2.5/docs/content/docs/what-is-spens.mdx +43 -0
- spens_ai-1.2.5/docs/lib/cn.ts +1 -0
- spens_ai-1.2.5/docs/lib/layout.shared.tsx +44 -0
- spens_ai-1.2.5/docs/lib/shared.ts +29 -0
- spens_ai-1.2.5/docs/lib/source.ts +31 -0
- spens_ai-1.2.5/docs/next.config.mjs +12 -0
- spens_ai-1.2.5/docs/package-lock.json +5886 -0
- spens_ai-1.2.5/docs/package.json +31 -0
- spens_ai-1.2.5/docs/postcss.config.mjs +7 -0
- spens_ai-1.2.5/docs/public/images/background.png +0 -0
- spens_ai-1.2.5/docs/public/images/header.png +0 -0
- spens_ai-1.2.5/docs/public/images/refwd.png +0 -0
- spens_ai-1.2.5/docs/public/images/spens_demo.gif +0 -0
- spens_ai-1.2.5/docs/tsconfig.json +34 -0
- spens_ai-1.2.5/landing_page_copy.md +120 -0
- spens_ai-1.2.5/license.md +7 -0
- spens_ai-1.2.5/logo.png +0 -0
- spens_ai-1.2.5/nono-override-ocr.json +52 -0
- spens_ai-1.2.5/nono-override.json +39 -0
- spens_ai-1.2.5/ocr.json +17 -0
- spens_ai-1.2.5/prompts/code-review.md +23 -0
- spens_ai-1.2.5/pyproject.toml +76 -0
- spens_ai-1.2.5/specs/docs.md +271 -0
- spens_ai-1.2.5/specs/enforced_egress.md +170 -0
- spens_ai-1.2.5/specs/init.md +161 -0
- spens_ai-1.2.5/specs/migrate_to_mtimproxy.md +17 -0
- spens_ai-1.2.5/specs/specfication.md +57 -0
- spens_ai-1.2.5/specs/spens_non_interactive.md +360 -0
- spens_ai-1.2.5/spens/__init__.py +1 -0
- spens_ai-1.2.5/spens/__main__.py +4 -0
- spens_ai-1.2.5/spens/background.py +132 -0
- spens_ai-1.2.5/spens/builder.py +747 -0
- spens_ai-1.2.5/spens/cli.py +294 -0
- spens_ai-1.2.5/spens/config.py +39 -0
- spens_ai-1.2.5/spens/control.py +309 -0
- spens_ai-1.2.5/spens/data/dns_forwarder.py +440 -0
- spens_ai-1.2.5/spens/data/init_templates/providers/anthropic.json +22 -0
- spens_ai-1.2.5/spens/data/init_templates/providers/fireworks.json +17 -0
- spens_ai-1.2.5/spens/data/init_templates/providers/openai.json +17 -0
- spens_ai-1.2.5/spens/data/init_templates/providers/openrouter.json +17 -0
- spens_ai-1.2.5/spens/data/init_templates/stacks/dotnet.json +16 -0
- spens_ai-1.2.5/spens/data/init_templates/stacks/go.json +16 -0
- spens_ai-1.2.5/spens/data/init_templates/stacks/java.json +13 -0
- spens_ai-1.2.5/spens/data/init_templates/stacks/node.json +16 -0
- spens_ai-1.2.5/spens/data/init_templates/stacks/python.json +14 -0
- spens_ai-1.2.5/spens/data/init_templates/stacks/rust.json +13 -0
- spens_ai-1.2.5/spens/data/mitmproxy_addon.py +490 -0
- spens_ai-1.2.5/spens/data/mitmproxy_helpers.py +327 -0
- spens_ai-1.2.5/spens/data/pricing/README.md +29 -0
- spens_ai-1.2.5/spens/data/pricing/anthropic.json +1273 -0
- spens_ai-1.2.5/spens/data/pricing/bedrock.json +5862 -0
- spens_ai-1.2.5/spens/data/pricing/fireworks-ai.json +1330 -0
- spens_ai-1.2.5/spens/data/pricing/google.json +2126 -0
- spens_ai-1.2.5/spens/data/pricing/openai.json +3293 -0
- spens_ai-1.2.5/spens/data/pricing/openrouter.json +7604 -0
- spens_ai-1.2.5/spens/data/templates/agents/claude.json +31 -0
- spens_ai-1.2.5/spens/data/templates/agents/codex.json +23 -0
- spens_ai-1.2.5/spens/data/templates/agents/opencode.json +32 -0
- spens_ai-1.2.5/spens/data/templates/agents/pi.json +21 -0
- spens_ai-1.2.5/spens/data/templates/environments/node-20.json +17 -0
- spens_ai-1.2.5/spens/data/templates/environments/node-22.json +17 -0
- spens_ai-1.2.5/spens/data/templates/environments/node-24.json +17 -0
- spens_ai-1.2.5/spens/data/templates/environments/python-3.11.json +18 -0
- spens_ai-1.2.5/spens/data/templates/environments/python-3.12.json +18 -0
- spens_ai-1.2.5/spens/data/templates/environments/python-3.13.json +18 -0
- spens_ai-1.2.5/spens/events.py +196 -0
- spens_ai-1.2.5/spens/init.py +476 -0
- spens_ai-1.2.5/spens/pricing.py +763 -0
- spens_ai-1.2.5/spens/runner.py +1143 -0
- spens_ai-1.2.5/spens/sessions.py +165 -0
- spens_ai-1.2.5/spens/sinks/__init__.py +34 -0
- spens_ai-1.2.5/spens/sinks/file.py +174 -0
- spens_ai-1.2.5/spens/sinks/jsonl.py +27 -0
- spens_ai-1.2.5/spens/sinks/tty.py +77 -0
- spens_ai-1.2.5/spens/static/app.js +529 -0
- spens_ai-1.2.5/spens/static/header.png +0 -0
- spens_ai-1.2.5/spens/static/index.html +30 -0
- spens_ai-1.2.5/spens/static/style.css +103 -0
- spens_ai-1.2.5/spens/summarizer.py +507 -0
- spens_ai-1.2.5/spens/templates.py +117 -0
- spens_ai-1.2.5/spens/ui.py +368 -0
- spens_ai-1.2.5/spens/viewer.py +1026 -0
- spens_ai-1.2.5/spens_demo.gif +0 -0
- spens_ai-1.2.5/tests/__init__.py +0 -0
- spens_ai-1.2.5/tests/e2e/runner.py +51 -0
- spens_ai-1.2.5/tests/e2e/workspace/.spens.config.json +17 -0
- spens_ai-1.2.5/tests/e2e/workspace/test.txt +1 -0
- spens_ai-1.2.5/tests/unit/__init__.py +0 -0
- spens_ai-1.2.5/tests/unit/conftest.py +70 -0
- spens_ai-1.2.5/tests/unit/test_builder.py +705 -0
- spens_ai-1.2.5/tests/unit/test_cli.py +221 -0
- spens_ai-1.2.5/tests/unit/test_config.py +74 -0
- spens_ai-1.2.5/tests/unit/test_control.py +240 -0
- spens_ai-1.2.5/tests/unit/test_dns_forwarder.py +459 -0
- spens_ai-1.2.5/tests/unit/test_events.py +155 -0
- spens_ai-1.2.5/tests/unit/test_init.py +365 -0
- spens_ai-1.2.5/tests/unit/test_mitmproxy_helpers.py +696 -0
- spens_ai-1.2.5/tests/unit/test_pricing.py +558 -0
- spens_ai-1.2.5/tests/unit/test_runner.py +1006 -0
- spens_ai-1.2.5/tests/unit/test_sessions.py +174 -0
- spens_ai-1.2.5/tests/unit/test_sinks_file.py +198 -0
- spens_ai-1.2.5/tests/unit/test_sinks_jsonl.py +87 -0
- spens_ai-1.2.5/tests/unit/test_sinks_tty.py +183 -0
- spens_ai-1.2.5/tests/unit/test_summarizer.py +668 -0
- spens_ai-1.2.5/tests/unit/test_templates.py +132 -0
- spens_ai-1.2.5/tests/unit/test_ui.py +146 -0
- spens_ai-1.2.5/tests/unit/test_viewer.py +503 -0
- spens_ai-1.2.5/uv.lock +145 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# GitLab CI/CD configuration for spens
|
|
2
|
+
#
|
|
3
|
+
# test - ruff + pytest across supported Python versions
|
|
4
|
+
# version - bump version, update CHANGELOG, and create a git tag (default branch)
|
|
5
|
+
# build - sdist + wheel (tags and default branch)
|
|
6
|
+
# publish - upload to this project's GitLab PyPI registry and to the public PyPI (tags)
|
|
7
|
+
# release - cut a GitLab Release with the artifacts attached (tags)
|
|
8
|
+
# pages - build the docs site and publish it as GitLab Pages (tags)
|
|
9
|
+
|
|
10
|
+
stages:
|
|
11
|
+
- test
|
|
12
|
+
- version
|
|
13
|
+
- build
|
|
14
|
+
- publish
|
|
15
|
+
- release
|
|
16
|
+
|
|
17
|
+
workflow:
|
|
18
|
+
rules:
|
|
19
|
+
- if: $CI_COMMIT_TAG
|
|
20
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
21
|
+
# Don't run a duplicate branch pipeline when an MR is already open
|
|
22
|
+
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
|
|
23
|
+
when: never
|
|
24
|
+
- if: $CI_COMMIT_BRANCH
|
|
25
|
+
|
|
26
|
+
variables:
|
|
27
|
+
UV_VERSION: "0.9"
|
|
28
|
+
PYTHON_VERSION: "3.12"
|
|
29
|
+
UV_CACHE_DIR: .uv-cache
|
|
30
|
+
# Hardlinks don't work across CI volume mounts; copying silences the warning
|
|
31
|
+
UV_LINK_MODE: copy
|
|
32
|
+
UV_PUBLISH_URL: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi
|
|
33
|
+
UV_PUBLISH_USERNAME: gitlab-ci-token
|
|
34
|
+
UV_PUBLISH_PASSWORD: ${CI_JOB_TOKEN}
|
|
35
|
+
|
|
36
|
+
default:
|
|
37
|
+
image: ghcr.io/astral-sh/uv:$UV_VERSION-python$PYTHON_VERSION-bookworm-slim
|
|
38
|
+
interruptible: true
|
|
39
|
+
retry:
|
|
40
|
+
max: 2
|
|
41
|
+
when:
|
|
42
|
+
- runner_system_failure
|
|
43
|
+
- stuck_or_timeout_failure
|
|
44
|
+
|
|
45
|
+
# Only jobs that resolve dependencies need the uv cache.
|
|
46
|
+
.uv-cached:
|
|
47
|
+
cache:
|
|
48
|
+
key:
|
|
49
|
+
files:
|
|
50
|
+
- uv.lock
|
|
51
|
+
paths:
|
|
52
|
+
- $UV_CACHE_DIR
|
|
53
|
+
after_script:
|
|
54
|
+
- uv cache prune --ci
|
|
55
|
+
|
|
56
|
+
lint:
|
|
57
|
+
extends: .uv-cached
|
|
58
|
+
stage: test
|
|
59
|
+
allow_failure: true
|
|
60
|
+
script:
|
|
61
|
+
- uv sync --extra dev
|
|
62
|
+
- |
|
|
63
|
+
status=0
|
|
64
|
+
uv run ruff check --output-format=gitlab --output-file=code-quality.json . || status=$?
|
|
65
|
+
uv run ruff format --check . || status=1
|
|
66
|
+
exit $status
|
|
67
|
+
artifacts:
|
|
68
|
+
when: always
|
|
69
|
+
reports:
|
|
70
|
+
codequality: code-quality.json
|
|
71
|
+
|
|
72
|
+
test:
|
|
73
|
+
extends: .uv-cached
|
|
74
|
+
stage: test
|
|
75
|
+
parallel:
|
|
76
|
+
matrix:
|
|
77
|
+
- PYTHON_VERSION: ["3.11", "3.12", "3.13"]
|
|
78
|
+
script:
|
|
79
|
+
- uv sync --extra dev
|
|
80
|
+
- uv run pytest tests/unit -v --junitxml=report.xml
|
|
81
|
+
artifacts:
|
|
82
|
+
when: always
|
|
83
|
+
reports:
|
|
84
|
+
junit: report.xml
|
|
85
|
+
|
|
86
|
+
# Python Semantic Release — analyzes conventional commits on main,
|
|
87
|
+
# bumps version in pyproject.toml, updates CHANGELOG.md, commits,
|
|
88
|
+
# creates a git tag, and pushes back to GitLab. The new tag then
|
|
89
|
+
# triggers the build/publish/release jobs.
|
|
90
|
+
semantic-release:
|
|
91
|
+
stage: version
|
|
92
|
+
image: python:3.12-slim
|
|
93
|
+
rules:
|
|
94
|
+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
95
|
+
before_script:
|
|
96
|
+
- apt-get update && apt-get install -y --no-install-recommends git
|
|
97
|
+
- pip install --quiet python-semantic-release "uv~=${UV_VERSION}"
|
|
98
|
+
script:
|
|
99
|
+
- |
|
|
100
|
+
# GitLab checks out a detached HEAD; PSR needs to be on a branch
|
|
101
|
+
git checkout "${CI_COMMIT_BRANCH}" || git checkout -B "${CI_COMMIT_BRANCH}"
|
|
102
|
+
git config --global user.email "semantic-release-bot@gitlab.com"
|
|
103
|
+
git config --global user.name "Semantic Release Bot"
|
|
104
|
+
semantic-release version
|
|
105
|
+
variables:
|
|
106
|
+
# Full history is required for PSR to analyse all commits
|
|
107
|
+
GIT_DEPTH: "0"
|
|
108
|
+
# PSR expects GITLAB_TOKEN with api + write_repository scopes.
|
|
109
|
+
# Re-use the existing RELEASE_PLEASE_TOKEN project variable.
|
|
110
|
+
GITLAB_TOKEN: $RELEASE_PLEASE_TOKEN
|
|
111
|
+
GIT_COMMITTER_NAME: "semantic-release-bot"
|
|
112
|
+
GIT_COMMITTER_EMAIL: "semantic-release-bot@gitlab.com"
|
|
113
|
+
|
|
114
|
+
build:
|
|
115
|
+
stage: build
|
|
116
|
+
variables:
|
|
117
|
+
UV_CACHE_DIR: /tmp/uv-cache
|
|
118
|
+
rules:
|
|
119
|
+
- if: $CI_COMMIT_TAG
|
|
120
|
+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
121
|
+
script:
|
|
122
|
+
- uv build
|
|
123
|
+
- ls -l dist/
|
|
124
|
+
artifacts:
|
|
125
|
+
paths:
|
|
126
|
+
- dist/
|
|
127
|
+
expire_in: 1 week
|
|
128
|
+
|
|
129
|
+
# Publish to this project's GitLab PyPI registry using the built-in job token.
|
|
130
|
+
# Install with:
|
|
131
|
+
# uv add spens-ai --index-url https://__token__:<token>@gitlab.com/api/v4/projects/<id>/packages/pypi/simple
|
|
132
|
+
publish:pypi:gitlab:
|
|
133
|
+
extends: .uv-cached
|
|
134
|
+
stage: publish
|
|
135
|
+
interruptible: false
|
|
136
|
+
needs:
|
|
137
|
+
- job: build
|
|
138
|
+
artifacts: true
|
|
139
|
+
rules:
|
|
140
|
+
- if: $CI_COMMIT_TAG
|
|
141
|
+
script:
|
|
142
|
+
- |
|
|
143
|
+
set -eu
|
|
144
|
+
version="${CI_COMMIT_TAG#v}"
|
|
145
|
+
if ! ls dist/ | grep -q -- "-${version}"; then
|
|
146
|
+
echo "dist/ does not contain version ${version} — is the tag out of sync with pyproject.toml?"
|
|
147
|
+
ls -l dist/
|
|
148
|
+
exit 1
|
|
149
|
+
fi
|
|
150
|
+
- uv publish dist/*
|
|
151
|
+
|
|
152
|
+
# Publish to the public PyPI (https://pypi.org/project/spens-ai/). The
|
|
153
|
+
# distribution is named spens-ai because "spens" is already taken on PyPI.
|
|
154
|
+
# Requires a masked, protected PYPI_API_TOKEN CI/CD variable holding an API
|
|
155
|
+
# token created at pypi.org. Without it the job is skipped and only the
|
|
156
|
+
# GitLab registry publish above happens.
|
|
157
|
+
publish:pypi:public:
|
|
158
|
+
stage: publish
|
|
159
|
+
interruptible: false
|
|
160
|
+
needs:
|
|
161
|
+
- job: build
|
|
162
|
+
artifacts: true
|
|
163
|
+
rules:
|
|
164
|
+
- if: $CI_COMMIT_TAG && $PYPI_API_TOKEN
|
|
165
|
+
variables:
|
|
166
|
+
UV_PUBLISH_URL: "https://upload.pypi.org/legacy/"
|
|
167
|
+
UV_PUBLISH_USERNAME: "__token__"
|
|
168
|
+
UV_PUBLISH_PASSWORD: "$PYPI_API_TOKEN"
|
|
169
|
+
script:
|
|
170
|
+
- uv publish dist/*
|
|
171
|
+
|
|
172
|
+
# Attach the sdist and wheel to a GitLab Release. glab uploads them to the
|
|
173
|
+
# generic package registry and links them in one step.
|
|
174
|
+
release:gitlab:
|
|
175
|
+
stage: release
|
|
176
|
+
image: registry.gitlab.com/gitlab-org/cli:latest
|
|
177
|
+
interruptible: false
|
|
178
|
+
needs:
|
|
179
|
+
- job: build
|
|
180
|
+
artifacts: true
|
|
181
|
+
- job: publish:pypi:gitlab
|
|
182
|
+
- job: publish:pypi:public
|
|
183
|
+
optional: true
|
|
184
|
+
rules:
|
|
185
|
+
- if: $CI_COMMIT_TAG
|
|
186
|
+
variables:
|
|
187
|
+
GLAB_ENABLE_CI_AUTOLOGIN: "true"
|
|
188
|
+
script:
|
|
189
|
+
- |
|
|
190
|
+
glab release create "$CI_COMMIT_TAG" dist/* \
|
|
191
|
+
--name "spens ${CI_COMMIT_TAG}" \
|
|
192
|
+
--notes "Release ${CI_COMMIT_TAG}. The package is published to the project's GitLab PyPI registry and to PyPI as spens-ai; the sdist and wheel are attached below as assets." \
|
|
193
|
+
--use-package-registry \
|
|
194
|
+
--package-name spens-ai \
|
|
195
|
+
--ref "$CI_COMMIT_SHA"
|
|
196
|
+
|
|
197
|
+
# Build the fumadocs documentation site (docs/) as a static export and
|
|
198
|
+
# publish it as GitLab Pages. The special `pages` job name makes GitLab
|
|
199
|
+
# publish the `public/` artifact automatically once the job succeeds.
|
|
200
|
+
pages:
|
|
201
|
+
stage: release
|
|
202
|
+
image: node:22-bookworm-slim
|
|
203
|
+
interruptible: false
|
|
204
|
+
rules:
|
|
205
|
+
- if: $CI_COMMIT_TAG
|
|
206
|
+
cache:
|
|
207
|
+
key:
|
|
208
|
+
files:
|
|
209
|
+
- docs/package-lock.json
|
|
210
|
+
paths:
|
|
211
|
+
- docs/.npm-cache
|
|
212
|
+
variables:
|
|
213
|
+
npm_config_cache: "$CI_PROJECT_DIR/docs/.npm-cache"
|
|
214
|
+
script:
|
|
215
|
+
- cd docs
|
|
216
|
+
- npm ci
|
|
217
|
+
- npm run build
|
|
218
|
+
- rm -rf "$CI_PROJECT_DIR/public"
|
|
219
|
+
- mv out "$CI_PROJECT_DIR/public"
|
|
220
|
+
artifacts:
|
|
221
|
+
paths:
|
|
222
|
+
- public
|
|
223
|
+
|
|
224
|
+
spens-review:
|
|
225
|
+
tags: [landlock]
|
|
226
|
+
stage: test
|
|
227
|
+
allow_failure: true
|
|
228
|
+
rules:
|
|
229
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
230
|
+
image: docker:27
|
|
231
|
+
services:
|
|
232
|
+
- docker:27-dind
|
|
233
|
+
variables:
|
|
234
|
+
# How the job container talks to the dind daemon (service alias is "docker")
|
|
235
|
+
DOCKER_HOST: tcp://docker:2376
|
|
236
|
+
DOCKER_TLS_CERTDIR: "/certs"
|
|
237
|
+
GITLAB_TOKEN: $RELEASE_PLEASE_TOKEN
|
|
238
|
+
|
|
239
|
+
before_script:
|
|
240
|
+
# gettext provides envsubst for prompt variable substitution
|
|
241
|
+
- apk add --no-cache curl jq git gettext
|
|
242
|
+
# Install uv
|
|
243
|
+
- curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
244
|
+
- export PATH="/root/.local/bin:$PATH"
|
|
245
|
+
# Wait for the dind daemon to be ready before spens tries to use it
|
|
246
|
+
- until docker info > /dev/null 2>&1; do sleep 1; done
|
|
247
|
+
- git fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
|
|
248
|
+
script:
|
|
249
|
+
- uv tool install spens-ai
|
|
250
|
+
--extra-index-url "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/api/v4/projects/refwd1%2Fspens/packages/pypi/simple"
|
|
251
|
+
- export SPENS_PROMPT="$(envsubst < prompts/code-review.md)"
|
|
252
|
+
# Fail here, not later: an empty SPENS_PROMPT (broken envsubst, missing
|
|
253
|
+
# prompt file) would otherwise surface as a confusing spens usage error.
|
|
254
|
+
- test -n "$SPENS_PROMPT"
|
|
255
|
+
- spens ocr.json pi . "$SPENS_PROMPT" --config ./.spens.config.ocr.json --accept-changes --output jsonl
|
|
256
|
+
|
|
257
|
+
- |
|
|
258
|
+
jq -n --rawfile body feedback.md \
|
|
259
|
+
'{body: ("## Spens Review\n\n" + $body)}' > payload.json
|
|
260
|
+
curl --fail --silent --show-error \
|
|
261
|
+
--header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
|
|
262
|
+
--header "Content-Type: application/json" \
|
|
263
|
+
--request POST \
|
|
264
|
+
--data-binary "@payload.json" \
|
|
265
|
+
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/notes"
|
|
266
|
+
|
|
267
|
+
artifacts:
|
|
268
|
+
when: always
|
|
269
|
+
paths:
|
|
270
|
+
- feedback.md
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"addition_capture_urls": ["*opencode.ai*", "*api.fireworks.ai*", "*192.168.8.10*", "*api.anthropic.com*"],
|
|
3
|
+
"exclude_capture_urls": ["*models.opencode.ai*"],
|
|
4
|
+
"nono_override": "nono-override.json",
|
|
5
|
+
"env": [],
|
|
6
|
+
"default_env": "python-3.13",
|
|
7
|
+
"default_agent": "pi",
|
|
8
|
+
"domain_rules": [
|
|
9
|
+
{"pattern": "*github.com", "allow": ["GET"]},
|
|
10
|
+
{"pattern": "*anthropic.com", "allow": ["GET", "POST"]},
|
|
11
|
+
{"pattern": "*release-assets.githubusercontent.com", "allow": ["GET"]},
|
|
12
|
+
{"pattern": "*pi.dev", "allow": ["GET"]},
|
|
13
|
+
{"pattern": "*npmjs.org", "allow": ["GET"]},
|
|
14
|
+
{"pattern": "*pypi.org", "allow": ["GET"]},
|
|
15
|
+
{"pattern": "*files.pythonhosted.org", "allow": ["GET"]},
|
|
16
|
+
{"pattern": "*platform.claude.com", "allow": ["GET"]},
|
|
17
|
+
{"pattern": "*api.anthropic.com", "allow": ["GET", "POST", "PUT", "DELETE", "OPTIONS"]},
|
|
18
|
+
{"pattern": "*opencode.ai", "allow": ["GET", "POST", "PUT", "DELETE", "OPTIONS"]},
|
|
19
|
+
{"pattern": "*api.fireworks.ai", "allow": ["GET", "POST", "PUT", "DELETE", "OPTIONS"]},
|
|
20
|
+
{"pattern": "bootstrap.pypa.io", "allow": ["GET"]},
|
|
21
|
+
{"pattern": "www.fumadocs.dev", "allow": ["GET"]}
|
|
22
|
+
|
|
23
|
+
],
|
|
24
|
+
"inject_headers": [
|
|
25
|
+
{
|
|
26
|
+
"placeholder": "FIREWORKS_API_KEY",
|
|
27
|
+
"env_var": "FIREWORKS_API_KEY",
|
|
28
|
+
"for_domains": ["*api.fireworks.ai*"]
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"addition_capture_urls": ["*api.fireworks.ai*"],
|
|
3
|
+
"exclude_capture_urls": ["*models.opencode.ai*"],
|
|
4
|
+
"nono_override": "nono-override-ocr.json",
|
|
5
|
+
"env": ["OCR_LLM_URL", "OCR_LLM_MODEL"],
|
|
6
|
+
"default_env": "python-3.12",
|
|
7
|
+
"default_agent": "pi",
|
|
8
|
+
"domain_rules": [
|
|
9
|
+
{"pattern": "*api.fireworks.ai", "allow": ["GET", "POST", "PUT", "DELETE", "OPTIONS"]}
|
|
10
|
+
],
|
|
11
|
+
"inject_headers": [
|
|
12
|
+
{
|
|
13
|
+
"placeholder": "OCR_LLM_TOKEN",
|
|
14
|
+
"env_var": "OCR_LLM_TOKEN",
|
|
15
|
+
"for_domains": ["*api.fireworks.ai*"]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"placeholder": "FIREWORKS_API_KEY",
|
|
19
|
+
"env_var": "FIREWORKS_API_KEY",
|
|
20
|
+
"for_domains": ["*api.fireworks.ai*"]
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Spens Architecture
|
|
2
|
+
|
|
3
|
+
This document explains, in technical terms, how spens runs a session from start to finish. For user-facing documentation, see the [README](README.md). For development setup, see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
4
|
+
|
|
5
|
+
## The three building blocks
|
|
6
|
+
|
|
7
|
+
Spens combines three tools:
|
|
8
|
+
|
|
9
|
+
- **Docker** - isolated, ephemeral runtime environments
|
|
10
|
+
- **[nono.sh](https://nono.sh/)** - a sandboxed shell with filesystem access control and audit logging; the agent runs as a non-root user inside it
|
|
11
|
+
- **[mitmproxy](https://www.mitmproxy.org/)** - an SSL-intercepting proxy that captures and decodes LLM API traffic
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
A spens session involves three cooperating pieces: a CLI process on the host, an interceptor container, and an agent container. The CLI orchestrates everything: it loads templates and config, builds the images, creates the networks and containers, and collects the results.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
host (spens CLI)
|
|
19
|
+
|
|
|
20
|
+
| docker network: internal (no internet)
|
|
21
|
+
| +---------------------------+ +---------------------------+
|
|
22
|
+
| | interceptor container | | agent container |
|
|
23
|
+
| | - mitmproxy + addon |<-------| nono sandbox |
|
|
24
|
+
| | - DNS forwarder | proxy | - unprivileged user |
|
|
25
|
+
| | - traces/ volume | :9090 | - agent binary |
|
|
26
|
+
| +---------------------------+ +---------------------------+
|
|
27
|
+
| docker network: egress (internet only for the interceptor)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Step by step
|
|
31
|
+
|
|
32
|
+
1. **Load inputs.** The CLI resolves the environment and agent templates (built-in, local `templates/`, or a direct `.json` file), and reads `.spens.config.json` from the workspace if present.
|
|
33
|
+
2. **Generate the agent image.** `builder.py` writes a Dockerfile from the templates: base image, system packages, the unprivileged agent user, the nono.sh installation, the agent's install command, and any static config files. It also generates a combined nono profile (extending the environment's and agent's base profiles, or copying the user's `nono_override` file) and an entrypoint script, and builds the image with Docker. The generated profile is baked into the image twice: as `spens-profile.json` (the active profile, which may be overridden by a user-supplied `nono_override` or bind-mount) and as `spens-base.json` (the generated base profile, never overridden). This lets a custom profile safely declare `"extends": ["spens-base"]` to inherit Spens internals without recreating them.
|
|
34
|
+
3. **Generate the interceptor image.** A second Dockerfile installs mitmproxy and copies in the spens addon, its helpers, and a DNS forwarder. The addon's runtime config (capture URLs, domain rules, inject rules) is generated from `.spens.config.json` and baked into the image.
|
|
35
|
+
4. **Create the networks.** Two Docker networks are created per session: an internal network (`docker network create --internal`) holding the agent and the interceptor, and an egress network giving the interceptor (and only the interceptor) internet access. Docker drops all routed egress from the internal network, so the agent's only reachable neighbor is the interceptor.
|
|
36
|
+
5. **Start the interceptor.** The interceptor container runs mitmdump with the spens addon on port 9090, plus a DNS forwarder on port 53. It generates its CA certificate on first run, before it starts listening. The session's traces directory is bind-mounted into it.
|
|
37
|
+
6. **Start the agent.** The agent container joins the internal network, points its `/etc/resolv.conf` at the interceptor's DNS forwarder, and receives proxy environment variables (`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`) plus the mitmproxy CA certificate. Its entrypoint waits for readiness markers from the interceptor, runs any `pre_sandbox_commands`, then starts the agent under `nono run` with the combined profile, the workspace mounted read-write at `/workspace`, and the allowed folders granted.
|
|
38
|
+
7. **Capture.** Every byte the agent sends flows through the interceptor: LLM API calls are decoded (including streaming responses) and written to `traces/captured.jsonl`; every request and response is logged to `traces/request_log.jsonl`; DNS queries are logged to `traces/dns_log.jsonl`. Domain rules are enforced fail-closed on both plain requests and `CONNECT` tunnel targets. Placeholders are swapped for real secrets only on matching hostnames, and real secret values are masked before anything is written to disk. nono writes its audit ledger and rollback snapshots to `nono-audit/`.
|
|
39
|
+
8. **Summarize.** When the agent exits, the CLI computes a session summary (tokens, cost, models, file changes) from the captured traces and writes `session_summary.json`. Costing runs on the host, after the container exits, per the rules in [Cost estimation](README.md#cost-estimation). The end-of-session prompt asks whether to keep the agent's workspace changes or roll them back via nono's rollback snapshots.
|
|
40
|
+
9. **Persist.** Everything is written under `<workspace>/.spens/sessions/<session-id>/` (or the `--spens-dir` path): `events.jsonl`, `state.json`, `traces/`, `nono-audit/`, and `session_summary.json`. The log viewer and the status/cancel/attach commands are thin readers over these files.
|
|
41
|
+
|
|
42
|
+
## Session directory layout
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
<workspace>/.spens/sessions/<session-id>/
|
|
46
|
+
events.jsonl machine-readable event stream for the session
|
|
47
|
+
state.json current session state (running, finished, canceled, error)
|
|
48
|
+
session_summary.json tokens, cost, models, file changes, pricing sources
|
|
49
|
+
traces/
|
|
50
|
+
captured.jsonl decoded LLM API calls (requests, responses, streamed chunks)
|
|
51
|
+
request_log.jsonl every HTTP request/response and CONNECT decision
|
|
52
|
+
dns_log.jsonl every DNS query the agent made
|
|
53
|
+
nono-audit/
|
|
54
|
+
audit/ nono audit events and the integrity ledger
|
|
55
|
+
rollbacks/ rollback snapshots and per-file change records
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## The interceptor
|
|
59
|
+
|
|
60
|
+
The interceptor is a Python container running two processes:
|
|
61
|
+
|
|
62
|
+
- **mitmdump with the spens addon** (`spens/data/mitmproxy_addon.py`). It intercepts HTTPS by terminating TLS with its own CA certificate (which the agent trusts), then re-originating the connection upstream. The addon implements: LLM traffic capture for the OpenAI Chat Completions, OpenAI Responses, and Anthropic Messages APIs (including SSE streaming); domain rule enforcement with a fail-closed policy; placeholder-to-secret substitution scoped by hostname; secret masking in logs; and the request log. `rawtcp` forwarding is disabled and HTTP/2 is disabled, so non-HTTP protocols inside a `CONNECT` tunnel fail closed inside the proxy instead of being blindly forwarded.
|
|
63
|
+
- **The DNS forwarder** (`spens/data/dns_forwarder.py`). Docker's embedded DNS is special-cased past the egress filtering of `--internal` networks, so the agent's `/etc/resolv.conf` is pointed at this forwarder instead. Every query is logged to `traces/dns_log.jsonl`, and resolution only works for the proxy path, closing the last uncaptured channel.
|
|
64
|
+
|
|
65
|
+
Domain policy semantics live in `spens/data/mitmproxy_helpers.py` (`is_allowed`, `host_allowed`, `domain_matches`) and are shared by the HTTP hooks and the DNS forwarder, so the two channels cannot drift apart. Patterns are matched against the hostname only, at a label boundary: a pattern for `anthropic.com` matches `anthropic.com` and `api.anthropic.com`, but not `api.anthropic.com.attacker.example` or `evil-anthropic.com`. When no rules are configured at all there is no policy to enforce, so HTTP egress is allow-all (see [Default network policy](README.md#default-network-policy) in the README).
|
|
66
|
+
|
|
67
|
+
## The agent sandbox
|
|
68
|
+
|
|
69
|
+
Inside the agent container, the agent never runs as root. It runs as an unprivileged user (uid 1000) under the nono sandbox, which adds filesystem access control on top: the workspace is allowed, a small set of agent folders is allowed, and everything else is off limits. nono records an audit ledger with an integrity chain (Merkle root) and takes rollback snapshots of the workspace, which is what powers the accept/reject decision at the end of a session. Because bind-mount permissions are enforced by numeric uid, the agent can only write to the workspace if the host directory is owned by (or writable by) uid 1000.
|
|
70
|
+
|
|
71
|
+
## Policy masking
|
|
72
|
+
|
|
73
|
+
The workspace is mounted read-write into the agent, and the workspace is also where spens reads its own trust-sensitive inputs (`.spens.config.json`, `templates/`, and any `nono_override` file). Left visible and writable, an untrusted agent - or a cloned malicious repo - could rewrite the policy the *next* session honors: downgrade `egress` to `legacy`, clear the domain rules, add `pre_sandbox_commands`, or define an agent template whose `configuration` block mounts `~/.ssh` from the host.
|
|
74
|
+
|
|
75
|
+
By the time the agent starts, those inputs have already been consumed and baked into the built images and generated configs, so the agent never needs to see them. Each input is overlaid with a read-only empty mask inside the agent container: `/dev/null` for the config and override files, an empty hardened tmpfs (`noexec,nosuid,nodev`) for `templates/`. The host copies are untouched; the agent can neither read them nor persist a poisoned policy for a later run (anything it writes to a masked path is ephemeral and discarded when the container exits).
|
|
76
|
+
|
|
77
|
+
This blocks tampering *across* sessions. It does not vet a config that is malicious the first time you run against an untrusted repo - review `.spens.config.json`, `templates/`, and any `nono_override` in a repo you don't trust before running spens on it.
|
|
78
|
+
|
|
79
|
+
## Non-interactive and background sessions
|
|
80
|
+
|
|
81
|
+
Every session, in every mode, writes the same `events.jsonl` and `state.json`. The `--output` flag only changes what is rendered to stdout: `tty` renders a rich-based live view, `jsonl` streams the raw events, and `background` prints the session id and detaches. Detached sessions are launched by a child process that owns the docker run; `spens status`, `spens cancel`, and `spens attach` read and write the same `state.json` and `events.jsonl`, and the first writer of a terminal state (`finished`, `canceled`, `error`) wins, so an external cancel can never be overwritten by the running session.
|
|
82
|
+
|
|
83
|
+
Yolo mode (a prompt passed on the CLI) is delivered to the container as the `SPENS_YOLO_PROMPT` environment variable. The entrypoint runs the agent template's `yolo_command` with `{prompt}` filled in, instead of the interactive agent, so the image is not rebuilt per prompt. If the template has no `yolo_command`, passing a prompt is an error.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
<!-- version list -->
|
|
4
|
+
|
|
5
|
+
## v1.2.5 (2026-09-24)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Force re-release
|
|
10
|
+
([`002c06c`](https://gitlab.com/refwd1/spens/-/commit/002c06c386ca214300395a8a88dcd5a1313d8a1a))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## v1.2.4 (2026-09-24)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- Offical pypi package
|
|
18
|
+
([`378ea2b`](https://gitlab.com/refwd1/spens/-/commit/378ea2bf5a45d44d1919ff5b5aa6b3d84ef978f0))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## v1.2.3 (2026-09-23)
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
- Cleaned up ci/cd. Generated example docs
|
|
26
|
+
([`b397fc3`](https://gitlab.com/refwd1/spens/-/commit/b397fc3bad103e09b539c1c0e4fd40fecf36c247))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## v1.2.2 (2026-09-23)
|
|
30
|
+
|
|
31
|
+
### Bug Fixes
|
|
32
|
+
|
|
33
|
+
- Fixed codex and opencode so they work now
|
|
34
|
+
([`dda74ff`](https://gitlab.com/refwd1/spens/-/commit/dda74ff6b71db33cbc66db2db69ef80769c718a7))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## v1.2.1 (2026-09-23)
|
|
38
|
+
|
|
39
|
+
### Bug Fixes
|
|
40
|
+
|
|
41
|
+
- Spelling errors
|
|
42
|
+
([`8c81081`](https://gitlab.com/refwd1/spens/-/commit/8c810818f573218c602e815cbbdf4403d45ec6b6))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## v1.2.0 (2026-09-22)
|
|
46
|
+
|
|
47
|
+
### Features
|
|
48
|
+
|
|
49
|
+
- **docs**: Added inital docs
|
|
50
|
+
([`061aa59`](https://gitlab.com/refwd1/spens/-/commit/061aa59a733f22cd5f0c42042ef32eae7a543494))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## v1.1.2 (2026-09-22)
|
|
54
|
+
|
|
55
|
+
### Bug Fixes
|
|
56
|
+
|
|
57
|
+
- Changed inheretence model for base profile.
|
|
58
|
+
([`d7f8547`](https://gitlab.com/refwd1/spens/-/commit/d7f8547ee7940808167cefc8ecb4170dc54303ec))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## v1.1.1 (2026-09-22)
|
|
62
|
+
|
|
63
|
+
### Bug Fixes
|
|
64
|
+
|
|
65
|
+
- Set jsonnl mode
|
|
66
|
+
([`e40350a`](https://gitlab.com/refwd1/spens/-/commit/e40350af26913c30fc37ddf7e1734da5c74e8f73))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
## v1.1.0 (2026-09-21)
|
|
70
|
+
|
|
71
|
+
### Features
|
|
72
|
+
|
|
73
|
+
- **config**: Added ability to change spens.config.json file to support different runners, added OCR
|
|
74
|
+
to the review chain
|
|
75
|
+
([`93e5bfe`](https://gitlab.com/refwd1/spens/-/commit/93e5bfe84530a33110aa265cdc60b744d1468846))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
## v1.0.2 (2026-09-21)
|
|
79
|
+
|
|
80
|
+
### Bug Fixes
|
|
81
|
+
|
|
82
|
+
- Fixing release
|
|
83
|
+
([`32c199d`](https://gitlab.com/refwd1/spens/-/commit/32c199d8eec274891181b2a0d12ee811ee2e8fe8))
|
|
84
|
+
|
|
85
|
+
- Hopefulyl fixing the release for real
|
|
86
|
+
([`5575aa3`](https://gitlab.com/refwd1/spens/-/commit/5575aa3860e3721a431143b95854d17c031904bd))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
## v1.0.1 (2026-09-21)
|
|
90
|
+
|
|
91
|
+
### Bug Fixes
|
|
92
|
+
|
|
93
|
+
- Release
|
|
94
|
+
([`e69da54`](https://gitlab.com/refwd1/spens/-/commit/e69da54f91729fe3469dad3d064a7fda679952b3))
|
|
95
|
+
|
|
96
|
+
- Updated lock file
|
|
97
|
+
([`7398188`](https://gitlab.com/refwd1/spens/-/commit/7398188778f625379148c9b65513e28af9989480))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
## v1.0.0 (2026-09-21)
|
|
101
|
+
|
|
102
|
+
### Bug Fixes
|
|
103
|
+
|
|
104
|
+
- Fix git issue with semantic-release
|
|
105
|
+
([`ec2be98`](https://gitlab.com/refwd1/spens/-/commit/ec2be986a02ed5d79cb64ef4d34919d1ab79b5de))
|
|
106
|
+
|
|
107
|
+
- Fixing semantic release
|
|
108
|
+
([`d8d4f08`](https://gitlab.com/refwd1/spens/-/commit/d8d4f0873c45696a9bd70c93db9f3832b636cd07))
|
|
109
|
+
|
|
110
|
+
### Features
|
|
111
|
+
|
|
112
|
+
- **release**: Enabled release please
|
|
113
|
+
([`9af2698`](https://gitlab.com/refwd1/spens/-/commit/9af26983a20bbfe2b7275fb95e889caf2226b7ab))
|
|
114
|
+
|
|
115
|
+
- **release**: Switched to semantic release
|
|
116
|
+
([`7674e2b`](https://gitlab.com/refwd1/spens/-/commit/7674e2b6f593d9b765064e47b9e3d875f9c6cfc5))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
## v0.4.3 (2026-09-20)
|
|
120
|
+
|
|
121
|
+
### Bug Fixes
|
|
122
|
+
|
|
123
|
+
- Fixing broken script
|
|
124
|
+
([`0ad7144`](https://gitlab.com/refwd1/spens/-/commit/0ad7144ecbe97be53897f58ed1134752a0c44268))
|
|
125
|
+
|
|
126
|
+
- Ruff formating
|
|
127
|
+
([`3a99d2b`](https://gitlab.com/refwd1/spens/-/commit/3a99d2b416cb62526ad5d3c1fc0c5cf5aaf3730d))
|
|
128
|
+
|
|
129
|
+
- Tweaks
|
|
130
|
+
([`7d50829`](https://gitlab.com/refwd1/spens/-/commit/7d50829386e9bdaf5ef29abd59394d7c5e7c3454))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
## v0.4.2 (2026-09-20)
|
|
134
|
+
|
|
135
|
+
### Bug Fixes
|
|
136
|
+
|
|
137
|
+
- Continuing to tweak gitlab
|
|
138
|
+
([`6a1ae15`](https://gitlab.com/refwd1/spens/-/commit/6a1ae154e1dde8169c168b4717bbd072c9457d51))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
## v0.4.0 (2026-09-20)
|
|
142
|
+
|
|
143
|
+
### Bug Fixes
|
|
144
|
+
|
|
145
|
+
- Fixed ci/cd issue
|
|
146
|
+
([`b5158e5`](https://gitlab.com/refwd1/spens/-/commit/b5158e5e60b47ccf34a27514453a684e17015ca9))
|
|
147
|
+
|
|
148
|
+
- Fixed viewerd bugs with custom sessions
|
|
149
|
+
([`9e6e894`](https://gitlab.com/refwd1/spens/-/commit/9e6e8947356fefa72ba46da7547b3cc3d6605440))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
## v0.3.0 (2026-09-20)
|
|
153
|
+
|
|
154
|
+
- Initial Release
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Contributing to Spens
|
|
2
|
+
|
|
3
|
+
This document covers development setup, tests, and the project layout. For using spens, see the [README](README.md). For how spens works internally, see [ARCHITECTURE.md](ARCHITECTURE.md).
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Python 3.11, 3.12, or 3.13
|
|
8
|
+
- [uv](https://docs.astral.sh/uv/) for dependency management (the repo ships a `uv.lock`)
|
|
9
|
+
- Docker, for running sessions and the end-to-end tests (on macOS, [OrbStack](https://orbstack.dev) is required)
|
|
10
|
+
- Git
|
|
11
|
+
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
git clone https://gitlab.com/refwd1/spens.git
|
|
16
|
+
cd spens
|
|
17
|
+
uv sync --extra dev
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This creates a `.venv` with spens installed in editable mode, plus the dev tools (pytest and ruff).
|
|
21
|
+
|
|
22
|
+
Run the CLI from the checkout:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv run spens list
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Unit tests
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv run pytest tests/unit -v
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Unit tests do not need Docker. They cover config generation, template loading, builder output, the pricing table, the summarizer, and the pure helper functions used by the interceptor addon.
|
|
35
|
+
|
|
36
|
+
## Lint and format
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv run ruff check .
|
|
40
|
+
uv run ruff format --check .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Ruff is configured in `pyproject.toml` with a line length of 120. CI runs both, so run them before opening a merge request.
|
|
44
|
+
|
|
45
|
+
## End-to-end tests
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uv run python tests/e2e/runner.py
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Requires Docker and an `ANTHROPIC_API_KEY` in the environment. It runs each built-in agent in each built-in environment against `tests/e2e/workspace` and reports pass/fail per combination.
|
|
52
|
+
|
|
53
|
+
## CI and releases
|
|
54
|
+
|
|
55
|
+
`.gitlab-ci.yml` runs ruff and pytest (on Python 3.11, 3.12, and 3.13) on every push. On tags it builds the sdist and wheel, publishes them to the project's GitLab PyPI registry and to the public PyPI (as `spens-ai`, when the `PYPI_API_TOKEN` CI/CD variable is set), and creates a GitLab release with the artifacts attached.
|
|
56
|
+
|
|
57
|
+
## Project layout
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
spens/
|
|
61
|
+
cli.py CLI entry point and argument parsing
|
|
62
|
+
init.py spens init questionnaire and config generation
|
|
63
|
+
config.py .spens.config.json loader
|
|
64
|
+
templates.py Environment and agent template loading
|
|
65
|
+
builder.py Dockerfile, nono profile, and entrypoint generation
|
|
66
|
+
runner.py Session orchestration (interceptor + agent containers)
|
|
67
|
+
background.py Detached (background) session launching
|
|
68
|
+
control.py status / cancel / attach commands
|
|
69
|
+
sessions.py Session directory and trace loading helpers
|
|
70
|
+
events.py Session event stream (events.jsonl)
|
|
71
|
+
sinks/ Output rendering (tty, jsonl, background)
|
|
72
|
+
viewer.py Log viewer web server and trace parser
|
|
73
|
+
summarizer.py Session summary stats and cost estimation
|
|
74
|
+
pricing.py Model pricing data (fetch, cache, lookup)
|
|
75
|
+
ui.py rich-based terminal rendering
|
|
76
|
+
static/ Log viewer frontend (html, css, js)
|
|
77
|
+
data/
|
|
78
|
+
mitmproxy_addon.py Interceptor addon (LLM capture, domain filtering, key injection)
|
|
79
|
+
mitmproxy_helpers.py Pure helpers for the interceptor addon (unit-testable without mitmproxy)
|
|
80
|
+
dns_forwarder.py Logging DNS forwarder
|
|
81
|
+
pricing/ Vendored pricing snapshots (offline fallback)
|
|
82
|
+
templates/ Built-in environment and agent templates
|
|
83
|
+
init_templates/ Partial configs used by spens init
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
Spens is MIT licensed. See `license.md`.
|