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.
Files changed (148) hide show
  1. spens_ai-1.2.5/.gitconfig +2 -0
  2. spens_ai-1.2.5/.gitignore +9 -0
  3. spens_ai-1.2.5/.gitlab-ci.yml +270 -0
  4. spens_ai-1.2.5/.spens.config.json +31 -0
  5. spens_ai-1.2.5/.spens.config.ocr.json +23 -0
  6. spens_ai-1.2.5/ARCHITECTURE.md +83 -0
  7. spens_ai-1.2.5/CHANGELOG.md +154 -0
  8. spens_ai-1.2.5/CONTRIBUTING.md +88 -0
  9. spens_ai-1.2.5/PKG-INFO +522 -0
  10. spens_ai-1.2.5/README.md +507 -0
  11. spens_ai-1.2.5/agent.md +204 -0
  12. spens_ai-1.2.5/config_example/.spens.config.json +18 -0
  13. spens_ai-1.2.5/docs/.gitignore +27 -0
  14. spens_ai-1.2.5/docs/README.md +45 -0
  15. spens_ai-1.2.5/docs/app/(home)/layout.tsx +6 -0
  16. spens_ai-1.2.5/docs/app/(home)/page.tsx +182 -0
  17. spens_ai-1.2.5/docs/app/api/search/route.ts +10 -0
  18. spens_ai-1.2.5/docs/app/docs/[[...slug]]/page.tsx +71 -0
  19. spens_ai-1.2.5/docs/app/docs/layout.tsx +10 -0
  20. spens_ai-1.2.5/docs/app/global.css +75 -0
  21. spens_ai-1.2.5/docs/app/layout.tsx +27 -0
  22. spens_ai-1.2.5/docs/app/llms-full.txt/route.ts +7 -0
  23. spens_ai-1.2.5/docs/app/llms.mdx/docs/[[...slug]]/route.ts +24 -0
  24. spens_ai-1.2.5/docs/app/llms.txt/route.ts +7 -0
  25. spens_ai-1.2.5/docs/app/og/docs/[...slug]/route.tsx +25 -0
  26. spens_ai-1.2.5/docs/components/mdx.tsx +15 -0
  27. spens_ai-1.2.5/docs/components/site-footer.tsx +25 -0
  28. spens_ai-1.2.5/docs/content/docs/cli.mdx +103 -0
  29. spens_ai-1.2.5/docs/content/docs/comparisons.mdx +112 -0
  30. spens_ai-1.2.5/docs/content/docs/config.mdx +96 -0
  31. spens_ai-1.2.5/docs/content/docs/custom-agents.mdx +76 -0
  32. spens_ai-1.2.5/docs/content/docs/custom-environments.mdx +52 -0
  33. spens_ai-1.2.5/docs/content/docs/examples/spens-in-ci-cd.mdx +123 -0
  34. spens_ai-1.2.5/docs/content/docs/examples/spens-with-playwright.mdx +114 -0
  35. spens_ai-1.2.5/docs/content/docs/getting-started.mdx +121 -0
  36. spens_ai-1.2.5/docs/content/docs/log-viewer.mdx +63 -0
  37. spens_ai-1.2.5/docs/content/docs/meta.json +21 -0
  38. spens_ai-1.2.5/docs/content/docs/pricing.mdx +41 -0
  39. spens_ai-1.2.5/docs/content/docs/spens-acp.mdx +167 -0
  40. spens_ai-1.2.5/docs/content/docs/using-spens-with-ide.mdx +122 -0
  41. spens_ai-1.2.5/docs/content/docs/what-is-spens.mdx +43 -0
  42. spens_ai-1.2.5/docs/lib/cn.ts +1 -0
  43. spens_ai-1.2.5/docs/lib/layout.shared.tsx +44 -0
  44. spens_ai-1.2.5/docs/lib/shared.ts +29 -0
  45. spens_ai-1.2.5/docs/lib/source.ts +31 -0
  46. spens_ai-1.2.5/docs/next.config.mjs +12 -0
  47. spens_ai-1.2.5/docs/package-lock.json +5886 -0
  48. spens_ai-1.2.5/docs/package.json +31 -0
  49. spens_ai-1.2.5/docs/postcss.config.mjs +7 -0
  50. spens_ai-1.2.5/docs/public/images/background.png +0 -0
  51. spens_ai-1.2.5/docs/public/images/header.png +0 -0
  52. spens_ai-1.2.5/docs/public/images/refwd.png +0 -0
  53. spens_ai-1.2.5/docs/public/images/spens_demo.gif +0 -0
  54. spens_ai-1.2.5/docs/tsconfig.json +34 -0
  55. spens_ai-1.2.5/landing_page_copy.md +120 -0
  56. spens_ai-1.2.5/license.md +7 -0
  57. spens_ai-1.2.5/logo.png +0 -0
  58. spens_ai-1.2.5/nono-override-ocr.json +52 -0
  59. spens_ai-1.2.5/nono-override.json +39 -0
  60. spens_ai-1.2.5/ocr.json +17 -0
  61. spens_ai-1.2.5/prompts/code-review.md +23 -0
  62. spens_ai-1.2.5/pyproject.toml +76 -0
  63. spens_ai-1.2.5/specs/docs.md +271 -0
  64. spens_ai-1.2.5/specs/enforced_egress.md +170 -0
  65. spens_ai-1.2.5/specs/init.md +161 -0
  66. spens_ai-1.2.5/specs/migrate_to_mtimproxy.md +17 -0
  67. spens_ai-1.2.5/specs/specfication.md +57 -0
  68. spens_ai-1.2.5/specs/spens_non_interactive.md +360 -0
  69. spens_ai-1.2.5/spens/__init__.py +1 -0
  70. spens_ai-1.2.5/spens/__main__.py +4 -0
  71. spens_ai-1.2.5/spens/background.py +132 -0
  72. spens_ai-1.2.5/spens/builder.py +747 -0
  73. spens_ai-1.2.5/spens/cli.py +294 -0
  74. spens_ai-1.2.5/spens/config.py +39 -0
  75. spens_ai-1.2.5/spens/control.py +309 -0
  76. spens_ai-1.2.5/spens/data/dns_forwarder.py +440 -0
  77. spens_ai-1.2.5/spens/data/init_templates/providers/anthropic.json +22 -0
  78. spens_ai-1.2.5/spens/data/init_templates/providers/fireworks.json +17 -0
  79. spens_ai-1.2.5/spens/data/init_templates/providers/openai.json +17 -0
  80. spens_ai-1.2.5/spens/data/init_templates/providers/openrouter.json +17 -0
  81. spens_ai-1.2.5/spens/data/init_templates/stacks/dotnet.json +16 -0
  82. spens_ai-1.2.5/spens/data/init_templates/stacks/go.json +16 -0
  83. spens_ai-1.2.5/spens/data/init_templates/stacks/java.json +13 -0
  84. spens_ai-1.2.5/spens/data/init_templates/stacks/node.json +16 -0
  85. spens_ai-1.2.5/spens/data/init_templates/stacks/python.json +14 -0
  86. spens_ai-1.2.5/spens/data/init_templates/stacks/rust.json +13 -0
  87. spens_ai-1.2.5/spens/data/mitmproxy_addon.py +490 -0
  88. spens_ai-1.2.5/spens/data/mitmproxy_helpers.py +327 -0
  89. spens_ai-1.2.5/spens/data/pricing/README.md +29 -0
  90. spens_ai-1.2.5/spens/data/pricing/anthropic.json +1273 -0
  91. spens_ai-1.2.5/spens/data/pricing/bedrock.json +5862 -0
  92. spens_ai-1.2.5/spens/data/pricing/fireworks-ai.json +1330 -0
  93. spens_ai-1.2.5/spens/data/pricing/google.json +2126 -0
  94. spens_ai-1.2.5/spens/data/pricing/openai.json +3293 -0
  95. spens_ai-1.2.5/spens/data/pricing/openrouter.json +7604 -0
  96. spens_ai-1.2.5/spens/data/templates/agents/claude.json +31 -0
  97. spens_ai-1.2.5/spens/data/templates/agents/codex.json +23 -0
  98. spens_ai-1.2.5/spens/data/templates/agents/opencode.json +32 -0
  99. spens_ai-1.2.5/spens/data/templates/agents/pi.json +21 -0
  100. spens_ai-1.2.5/spens/data/templates/environments/node-20.json +17 -0
  101. spens_ai-1.2.5/spens/data/templates/environments/node-22.json +17 -0
  102. spens_ai-1.2.5/spens/data/templates/environments/node-24.json +17 -0
  103. spens_ai-1.2.5/spens/data/templates/environments/python-3.11.json +18 -0
  104. spens_ai-1.2.5/spens/data/templates/environments/python-3.12.json +18 -0
  105. spens_ai-1.2.5/spens/data/templates/environments/python-3.13.json +18 -0
  106. spens_ai-1.2.5/spens/events.py +196 -0
  107. spens_ai-1.2.5/spens/init.py +476 -0
  108. spens_ai-1.2.5/spens/pricing.py +763 -0
  109. spens_ai-1.2.5/spens/runner.py +1143 -0
  110. spens_ai-1.2.5/spens/sessions.py +165 -0
  111. spens_ai-1.2.5/spens/sinks/__init__.py +34 -0
  112. spens_ai-1.2.5/spens/sinks/file.py +174 -0
  113. spens_ai-1.2.5/spens/sinks/jsonl.py +27 -0
  114. spens_ai-1.2.5/spens/sinks/tty.py +77 -0
  115. spens_ai-1.2.5/spens/static/app.js +529 -0
  116. spens_ai-1.2.5/spens/static/header.png +0 -0
  117. spens_ai-1.2.5/spens/static/index.html +30 -0
  118. spens_ai-1.2.5/spens/static/style.css +103 -0
  119. spens_ai-1.2.5/spens/summarizer.py +507 -0
  120. spens_ai-1.2.5/spens/templates.py +117 -0
  121. spens_ai-1.2.5/spens/ui.py +368 -0
  122. spens_ai-1.2.5/spens/viewer.py +1026 -0
  123. spens_ai-1.2.5/spens_demo.gif +0 -0
  124. spens_ai-1.2.5/tests/__init__.py +0 -0
  125. spens_ai-1.2.5/tests/e2e/runner.py +51 -0
  126. spens_ai-1.2.5/tests/e2e/workspace/.spens.config.json +17 -0
  127. spens_ai-1.2.5/tests/e2e/workspace/test.txt +1 -0
  128. spens_ai-1.2.5/tests/unit/__init__.py +0 -0
  129. spens_ai-1.2.5/tests/unit/conftest.py +70 -0
  130. spens_ai-1.2.5/tests/unit/test_builder.py +705 -0
  131. spens_ai-1.2.5/tests/unit/test_cli.py +221 -0
  132. spens_ai-1.2.5/tests/unit/test_config.py +74 -0
  133. spens_ai-1.2.5/tests/unit/test_control.py +240 -0
  134. spens_ai-1.2.5/tests/unit/test_dns_forwarder.py +459 -0
  135. spens_ai-1.2.5/tests/unit/test_events.py +155 -0
  136. spens_ai-1.2.5/tests/unit/test_init.py +365 -0
  137. spens_ai-1.2.5/tests/unit/test_mitmproxy_helpers.py +696 -0
  138. spens_ai-1.2.5/tests/unit/test_pricing.py +558 -0
  139. spens_ai-1.2.5/tests/unit/test_runner.py +1006 -0
  140. spens_ai-1.2.5/tests/unit/test_sessions.py +174 -0
  141. spens_ai-1.2.5/tests/unit/test_sinks_file.py +198 -0
  142. spens_ai-1.2.5/tests/unit/test_sinks_jsonl.py +87 -0
  143. spens_ai-1.2.5/tests/unit/test_sinks_tty.py +183 -0
  144. spens_ai-1.2.5/tests/unit/test_summarizer.py +668 -0
  145. spens_ai-1.2.5/tests/unit/test_templates.py +132 -0
  146. spens_ai-1.2.5/tests/unit/test_ui.py +146 -0
  147. spens_ai-1.2.5/tests/unit/test_viewer.py +503 -0
  148. spens_ai-1.2.5/uv.lock +145 -0
@@ -0,0 +1,2 @@
1
+ [safe]
2
+ directory = /workspace
@@ -0,0 +1,9 @@
1
+ __pycache__
2
+ .ruff_cache
3
+ .venv
4
+ testing
5
+ .spens
6
+ .pytest_cache
7
+ .uv-cache/
8
+ dist
9
+ feedback.md
@@ -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`.