stablemate-saddlebag 2.0.0__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.
- stablemate_saddlebag-2.0.0/.gitignore +264 -0
- stablemate_saddlebag-2.0.0/LICENSE +21 -0
- stablemate_saddlebag-2.0.0/PKG-INFO +299 -0
- stablemate_saddlebag-2.0.0/README.md +271 -0
- stablemate_saddlebag-2.0.0/pyproject.toml +60 -0
- stablemate_saddlebag-2.0.0/saddlebag/README.md +13 -0
- stablemate_saddlebag-2.0.0/saddlebag/__init__.py +7 -0
- stablemate_saddlebag-2.0.0/saddlebag/browser.py +190 -0
- stablemate_saddlebag-2.0.0/saddlebag/catalogue.py +95 -0
- stablemate_saddlebag-2.0.0/saddlebag/cli.py +361 -0
- stablemate_saddlebag-2.0.0/saddlebag/custody.py +102 -0
- stablemate_saddlebag-2.0.0/saddlebag/daemon.py +88 -0
- stablemate_saddlebag-2.0.0/saddlebag/keyring_store.py +167 -0
- stablemate_saddlebag-2.0.0/saddlebag/models.py +73 -0
- stablemate_saddlebag-2.0.0/saddlebag/preference.py +59 -0
- stablemate_saddlebag-2.0.0/saddlebag/protocol.py +175 -0
- stablemate_saddlebag-2.0.0/saddlebag/route.py +75 -0
- stablemate_saddlebag-2.0.0/saddlebag/store.py +41 -0
- stablemate_saddlebag-2.0.0/saddlebag/totp.py +44 -0
- stablemate_saddlebag-2.0.0/saddlebag/vault.py +117 -0
- stablemate_saddlebag-2.0.0/saddlebag/verbs.py +233 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
# Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
# poetry.lock
|
|
109
|
+
# poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
# pdm.lock
|
|
116
|
+
# pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
# pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# Redis
|
|
135
|
+
*.rdb
|
|
136
|
+
*.aof
|
|
137
|
+
*.pid
|
|
138
|
+
|
|
139
|
+
# RabbitMQ
|
|
140
|
+
mnesia/
|
|
141
|
+
rabbitmq/
|
|
142
|
+
rabbitmq-data/
|
|
143
|
+
|
|
144
|
+
# ActiveMQ
|
|
145
|
+
activemq-data/
|
|
146
|
+
|
|
147
|
+
# SageMath parsed files
|
|
148
|
+
*.sage.py
|
|
149
|
+
|
|
150
|
+
# Environments
|
|
151
|
+
.env
|
|
152
|
+
.envrc
|
|
153
|
+
.venv
|
|
154
|
+
env/
|
|
155
|
+
venv/
|
|
156
|
+
ENV/
|
|
157
|
+
env.bak/
|
|
158
|
+
venv.bak/
|
|
159
|
+
|
|
160
|
+
# Spyder project settings
|
|
161
|
+
.spyderproject
|
|
162
|
+
.spyproject
|
|
163
|
+
|
|
164
|
+
# Rope project settings
|
|
165
|
+
.ropeproject
|
|
166
|
+
|
|
167
|
+
# mkdocs documentation
|
|
168
|
+
/site
|
|
169
|
+
|
|
170
|
+
# mypy
|
|
171
|
+
.mypy_cache/
|
|
172
|
+
.dmypy.json
|
|
173
|
+
dmypy.json
|
|
174
|
+
|
|
175
|
+
# Pyre type checker
|
|
176
|
+
.pyre/
|
|
177
|
+
|
|
178
|
+
# pytype static type analyzer
|
|
179
|
+
.pytype/
|
|
180
|
+
|
|
181
|
+
# Cython debug symbols
|
|
182
|
+
cython_debug/
|
|
183
|
+
|
|
184
|
+
# PyCharm
|
|
185
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
186
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
188
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
189
|
+
# .idea/
|
|
190
|
+
|
|
191
|
+
# Abstra
|
|
192
|
+
# Abstra is an AI-powered process automation framework.
|
|
193
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
194
|
+
# Learn more at https://abstra.io/docs
|
|
195
|
+
.abstra/
|
|
196
|
+
|
|
197
|
+
# Visual Studio Code
|
|
198
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
199
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
200
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
201
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
202
|
+
# .vscode/
|
|
203
|
+
# Temporary file for partial code execution
|
|
204
|
+
tempCodeRunnerFile.py
|
|
205
|
+
|
|
206
|
+
# Ruff stuff:
|
|
207
|
+
.ruff_cache/
|
|
208
|
+
|
|
209
|
+
# PyPI configuration file
|
|
210
|
+
.pypirc
|
|
211
|
+
|
|
212
|
+
# Marimo
|
|
213
|
+
marimo/_static/
|
|
214
|
+
marimo/_lsp/
|
|
215
|
+
__marimo__/
|
|
216
|
+
|
|
217
|
+
# Streamlit
|
|
218
|
+
.streamlit/secrets.toml
|
|
219
|
+
|
|
220
|
+
.playwright-mcp
|
|
221
|
+
|
|
222
|
+
.agents/*
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# A benchmark round's built repo, its run artifacts and its scorecard. Untracked for
|
|
226
|
+
# three separate reasons, any one of which is sufficient — it is large (a whole app per
|
|
227
|
+
# round), it is derived (re-runnable from the task module), and the built repo is its OWN
|
|
228
|
+
# git repo, which is the whole point: nesting one working tree inside another is only safe
|
|
229
|
+
# while the outer one cannot see it. Paddock's work and result roots live outside the tree
|
|
230
|
+
# entirely (`~/.local/share/stablemate/paddock`) for the same reason; this entry stays as
|
|
231
|
+
# the backstop for a round pointed at the repo by hand.
|
|
232
|
+
/data/
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# Internal planning notes: maintainer paths, private-project references and
|
|
236
|
+
# retired-engine prose live here by nature. The directory stays (kept by .gitkeep)
|
|
237
|
+
# but its contents never ship.
|
|
238
|
+
docs/plans/*
|
|
239
|
+
!docs/plans/.gitkeep
|
|
240
|
+
progress.md
|
|
241
|
+
docs/specs
|
|
242
|
+
.opencode/opencode-loop
|
|
243
|
+
|
|
244
|
+
docs/backload.md
|
|
245
|
+
|
|
246
|
+
# >>> farrier: QA evidence (generated) >>>
|
|
247
|
+
**/qa/**/steps/
|
|
248
|
+
**/qa/**/asserts/
|
|
249
|
+
**/qa/**/traces/
|
|
250
|
+
**/qa/**/videos/
|
|
251
|
+
**/qa/**/screenshots/
|
|
252
|
+
**/qa/qa-run.ndjson
|
|
253
|
+
**/qa/run-manifest.json
|
|
254
|
+
**/qa/qa-session.json
|
|
255
|
+
# <<< farrier: QA evidence <<<
|
|
256
|
+
|
|
257
|
+
.agents/runs/
|
|
258
|
+
.agents/worktrees/
|
|
259
|
+
.agents/workflows/
|
|
260
|
+
.agents/operator/
|
|
261
|
+
.agents/local.compose.yaml
|
|
262
|
+
.agents/agents-context.json
|
|
263
|
+
.agents/agents-context.*.json
|
|
264
|
+
.agents/agents.mk
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gabriel Côté
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: stablemate-saddlebag
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: A Vault-backed daemon that fills secrets into browsers and commands without printing them.
|
|
5
|
+
Project-URL: Homepage, https://github.com/GabrielCpp/stablemate/tree/main/saddlebag
|
|
6
|
+
Project-URL: Repository, https://github.com/GabrielCpp/stablemate
|
|
7
|
+
Project-URL: Issues, https://github.com/GabrielCpp/stablemate/issues
|
|
8
|
+
Author: Gabriel Côté
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,credentials,secrets,testing,vault,workflow
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Security
|
|
18
|
+
Classifier: Topic :: Software Development :: Testing
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: hvac>=2.0
|
|
22
|
+
Requires-Dist: keyring>=25
|
|
23
|
+
Requires-Dist: platformdirs>=4.0
|
|
24
|
+
Requires-Dist: websockets>=13.0
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == 'test'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# saddlebag
|
|
30
|
+
|
|
31
|
+
> Carry the right credentials for every ride.
|
|
32
|
+
|
|
33
|
+
`saddlebag` holds the secrets a QA run needs and puts each one where it is consumed, so
|
|
34
|
+
that an agent driving a `workhorse` workflow never holds a secret in its context. A
|
|
35
|
+
secret leaves the store in exactly two ways. `fill` types it into a field of a browser
|
|
36
|
+
over the Chrome DevTools Protocol. `exec` runs a command with it set in that command's
|
|
37
|
+
process environment. Neither hands the value back to whoever ran them.
|
|
38
|
+
|
|
39
|
+
One daemon holds the HashiCorp Vault token, and every other command is a thin client
|
|
40
|
+
over a unix socket. That is what lets the same tool serve a run on the host and a run
|
|
41
|
+
in a container: the container sees the socket and nothing else.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Position in the ecosystem
|
|
46
|
+
|
|
47
|
+
| Tool | Job |
|
|
48
|
+
|---|---|
|
|
49
|
+
| **ostler** | Tends the knowledge graph: epics, stories, seeds, specs |
|
|
50
|
+
| **farrier** | Fits the shared prompt library onto each repo |
|
|
51
|
+
| **workhorse** | Runs the workflow graph unattended |
|
|
52
|
+
| **saddlebag** | Carries secrets and fills them where they are consumed |
|
|
53
|
+
|
|
54
|
+
Ostler owns the spec of what a test needs and which environment it runs against.
|
|
55
|
+
Saddlebag owns the secret material that satisfies it. Ostler's stack bring-up asks
|
|
56
|
+
saddlebag for the variables a stack needs, as described under
|
|
57
|
+
[The ostler stack](#the-ostler-stack).
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
Saddlebag publishes to PyPI as `stablemate-saddlebag`, because the name `saddlebag` is
|
|
64
|
+
unavailable. The command and the import package stay `saddlebag`.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
uv tool install stablemate-saddlebag
|
|
68
|
+
pipx install stablemate-saddlebag
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The `stablemate` package installs it along with the other tools (`uv tool install stablemate`).
|
|
72
|
+
From a checkout of the [stablemate](https://github.com/GabrielCpp/stablemate) workspace:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
uv run --package stablemate-saddlebag saddlebag --help
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Requires Python ≥ 3.12 and a reachable HashiCorp Vault with a KV v2 mount named
|
|
79
|
+
`secret`. For a developer machine, `vault server -dev` is enough.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Sixty seconds
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# 1. Start the daemon. The token arrives on stdin and stays in the daemon's memory.
|
|
87
|
+
export VAULT_ADDR=http://127.0.0.1:8200
|
|
88
|
+
printf '%s' "$VAULT_TOKEN_FROM_YOUR_LOGIN" | saddlebag serve
|
|
89
|
+
|
|
90
|
+
# 2. In another shell: describe a secret, then give it its value on stdin.
|
|
91
|
+
saddlebag secret create normal_login \
|
|
92
|
+
--username gab \
|
|
93
|
+
--description "Log in as a normal user" \
|
|
94
|
+
--var LOGIN_PASSWORD \
|
|
95
|
+
--tag environment=local --tag role=user
|
|
96
|
+
printf '%s' "$PASSWORD" | saddlebag secret set normal_login
|
|
97
|
+
|
|
98
|
+
# 3. An agent finds it by tag and reads the description. No value is printed.
|
|
99
|
+
saddlebag list --tag environment=local
|
|
100
|
+
|
|
101
|
+
# 4. Deliver it. Into a browser field, or into a command's environment.
|
|
102
|
+
saddlebag fill normal_login --field username --selector "#login_field" --url-contains example.com
|
|
103
|
+
saddlebag fill normal_login --field password --selector "#password" --url-contains example.com
|
|
104
|
+
saddlebag exec --tag environment=local -- docker compose up
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The browser the fourth step talks to is one you started with a debugger port:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
chromium --remote-debugging-port=9222 --user-data-dir=/tmp/qa-profile
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Saddlebag connects only to a DevTools endpoint on loopback and refuses any other
|
|
114
|
+
host. That port is unauthenticated and grants total control of the browser.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## The daemon
|
|
119
|
+
|
|
120
|
+
`saddlebag serve` is the one process that talks to Vault. It reads the token from
|
|
121
|
+
stdin when piped, else from the OS keyring where `saddlebag serve --keep-token` stored
|
|
122
|
+
it, else from `saddlebag/vault-token` under the stablemate state dir when that file is
|
|
123
|
+
mode 0600. It refuses to start if `VAULT_TOKEN` is set in its environment, and listens
|
|
124
|
+
on a unix socket created with owner-only permissions. Every other verb connects to
|
|
125
|
+
that socket, sends one request and reads one reply.
|
|
126
|
+
|
|
127
|
+
| | |
|
|
128
|
+
|---|---|
|
|
129
|
+
| Backend | `$SADDLEBAG_BACKEND` (`keyring` or `daemon`), else `backend` under `[saddlebag]` in the shared stablemate `config.toml`, else a daemon that answers on the socket, else the OS keyring |
|
|
130
|
+
| Socket path | `$SADDLEBAG_SOCKET`, else `$XDG_RUNTIME_DIR/saddlebag.sock`, else the OS runtime dir |
|
|
131
|
+
| Vault address | `$VAULT_ADDR` or `--vault-addr` |
|
|
132
|
+
| Browser | `--cdp`, default `http://127.0.0.1:9222` |
|
|
133
|
+
|
|
134
|
+
saddlebag never puts the token in an environment variable, because a process of the
|
|
135
|
+
same user can read `/proc/<pid>/environ`. The kept token lives in the OS keyring. The
|
|
136
|
+
filed token is the operator's choice, and `doctor` warns about it because any file the
|
|
137
|
+
user owns is readable by their other processes. For the same reason the operator's own
|
|
138
|
+
Vault login must not leave `~/.vault-token` behind, and `doctor` warns when that file
|
|
139
|
+
exists.
|
|
140
|
+
|
|
141
|
+
### A run in a container
|
|
142
|
+
|
|
143
|
+
Mount the socket and name it. The client inside the container talks to the daemon
|
|
144
|
+
on the host, which is also where the browser and its debug port live.
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
docker run \
|
|
148
|
+
-v "$SADDLEBAG_SOCKET:/run/saddlebag.sock" \
|
|
149
|
+
-e SADDLEBAG_SOCKET=/run/saddlebag.sock \
|
|
150
|
+
...
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Secrets and tags
|
|
156
|
+
|
|
157
|
+
A **secret** has a name, an optional username, a description an agent reads to choose
|
|
158
|
+
between candidates, an optional `var` naming the environment variable `exec` fills it
|
|
159
|
+
into, and a set of `key=value` tags. It holds up to two values: a `password` and a
|
|
160
|
+
`totp` enrolment seed. The username is metadata and may print. The two values never do.
|
|
161
|
+
|
|
162
|
+
```jsonc
|
|
163
|
+
{
|
|
164
|
+
"name": "normal_login",
|
|
165
|
+
"username": "gab",
|
|
166
|
+
"description": "Log in as a normal user",
|
|
167
|
+
"var": "LOGIN_PASSWORD",
|
|
168
|
+
"tags": [["environment", "local"], ["role", "user"]],
|
|
169
|
+
"fields": ["password"]
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Tags are the whole query vocabulary. `list --tag environment=local --tag role=admin`
|
|
174
|
+
returns the secrets carrying both pairs. `tags` lists the distinct pairs in use. The
|
|
175
|
+
convention the ostler integration relies on is that a secret for a QA environment
|
|
176
|
+
carries `environment=<ostler environment node id>`.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Commands
|
|
181
|
+
|
|
182
|
+
| | |
|
|
183
|
+
|---|---|
|
|
184
|
+
| `serve` | run the daemon; token from stdin, the OS keyring or a 0600 file; `--keep-token` stores one |
|
|
185
|
+
| `list` `tags` | query by tag; neither can emit a value |
|
|
186
|
+
| `secret create` `set` `unset` `remove` `tag` `untag` | manage a secret; `set` reads its value from stdin |
|
|
187
|
+
| `fill` | type a secret's username, password or current TOTP code into a browser field |
|
|
188
|
+
| `exec` | run a command with matching secrets in its environment |
|
|
189
|
+
| `doctor` | backend and route in use, store reachable, secrets with no value, orphan keyring items, stray token file, and on the daemon route where its token came from |
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
saddlebag fill NAME --field username|password|totp --selector S [--url-contains X]
|
|
193
|
+
saddlebag exec [--tag k=v]... [--env VAR=NAME[.field]]... -- cmd args...
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`exec` fills each tag-matched secret's `var` with its password. An explicit
|
|
197
|
+
`--env VAR=NAME.field` binding may name any field, including the username and the
|
|
198
|
+
current TOTP code. A secret matched by tag that has no `var` is an error naming it,
|
|
199
|
+
not a silent skip.
|
|
200
|
+
|
|
201
|
+
**What each verb guarantees.** `fill` reports a character count, a selector and a URL.
|
|
202
|
+
The value is typed through `Input.insertText` and never interpolated into an evaluated
|
|
203
|
+
expression. A TOTP seed leaves the store only as a computed six-digit code, and only
|
|
204
|
+
by being typed. `exec` is as private as the command it runs: a command that prints its
|
|
205
|
+
own environment prints the secret. What `exec` guarantees is that the value is never
|
|
206
|
+
in the agent's transcript by default and never on disk. The agent's permission layer
|
|
207
|
+
decides which commands it may run under `exec`.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## The ostler stack
|
|
212
|
+
|
|
213
|
+
A repository's `docs/services/ops/stack.yml` names one secret source for its compose
|
|
214
|
+
stack. With saddlebag as that source, ostler asks `saddlebag list` for the variable
|
|
215
|
+
names the matching secrets fill, checks them against the `${VAR}` references in the
|
|
216
|
+
rendered compose file, and runs every `docker compose` command under `saddlebag exec`.
|
|
217
|
+
No `stack.env` file is written.
|
|
218
|
+
|
|
219
|
+
```yaml
|
|
220
|
+
secrets:
|
|
221
|
+
saddlebag: environment=local
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The value is one or more space-separated tag pairs. The ostler README documents the
|
|
225
|
+
other two sources and the required-name check.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Where secrets live
|
|
230
|
+
|
|
231
|
+
A command picks its route in this order: `SADDLEBAG_BACKEND`, then the `backend` key
|
|
232
|
+
of the `[saddlebag]` table in the shared stablemate `config.toml`, then a daemon that
|
|
233
|
+
answers on the socket, else the OS keyring in this process. A set `SADDLEBAG_SOCKET`
|
|
234
|
+
always means the daemon, and nothing listening there is an error, never a silent
|
|
235
|
+
fall-back to the keyring.
|
|
236
|
+
|
|
237
|
+
Without a daemon, values live in the OS keyring, one item per secret and field under
|
|
238
|
+
the service `stablemate-saddlebag`, and their metadata in `saddlebag/catalogue.json`
|
|
239
|
+
under the stablemate state dir, owner-only. Behind the daemon, values and metadata both
|
|
240
|
+
live in Vault under `secret/saddlebag/<name>`: the values as the secret's data, the
|
|
241
|
+
metadata as its custom metadata. Saddlebag implements no encryption and keeps no
|
|
242
|
+
plaintext fallback. A locked or absent keyring stops the command with a line that says
|
|
243
|
+
what to do, and if Vault is unreachable, the daemon does not start.
|
|
244
|
+
|
|
245
|
+
Over SSH the keyring is usually locked. Feed your login password to
|
|
246
|
+
`gnome-keyring-daemon --unlock` to open it, or set `SADDLEBAG_BACKEND=daemon`. On
|
|
247
|
+
macOS the Keychain trusts the interpreter that stored an item, so a reinstalled
|
|
248
|
+
interpreter prompts once per item; `doctor` prints the interpreter path, which
|
|
249
|
+
`security add-generic-password -T <path>` takes to pre-authorise it. The full account,
|
|
250
|
+
including what the socket and the keyring do and do not protect against, is in
|
|
251
|
+
[docs/SECURITY.md](https://github.com/GabrielCpp/stablemate/blob/main/saddlebag/docs/SECURITY.md).
|
|
252
|
+
Driving saddlebag from a workflow node is in
|
|
253
|
+
[docs/INTEGRATION.md](https://github.com/GabrielCpp/stablemate/blob/main/saddlebag/docs/INTEGRATION.md).
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Upgrading from 1.x
|
|
258
|
+
|
|
259
|
+
Version 2.0 removed the 1.x OS keyring backend, the SQLite pool, credential leases,
|
|
260
|
+
AI selection, the `env` command family and the YAML manifest. Nothing is migrated.
|
|
261
|
+
Credentials in a 1.x pool and 1.x keyring items under the service `saddlebag` are left
|
|
262
|
+
in place and orphaned; the keyring store of 2.x reads only items under
|
|
263
|
+
`stablemate-saddlebag`, which it wrote itself. Create each secret you still need with
|
|
264
|
+
`secret create` and `secret set`.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Package layout
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
saddlebag/
|
|
272
|
+
├── __init__.py
|
|
273
|
+
├── cli.py # argparse front end: build a request, route it, print the reply; `exec`
|
|
274
|
+
├── route.py # how a command reaches the counter: the daemon's socket, or in-process over the keyring
|
|
275
|
+
├── verbs.py # the verb handlers over one store, used in-process or behind the daemon
|
|
276
|
+
├── store.py # the SecretStore contract every verb talks to, and StoreUnavailable
|
|
277
|
+
├── custody.py # the Vault token's three sources and the refusal
|
|
278
|
+
├── daemon.py # `serve`: the socket accept loop over the verbs
|
|
279
|
+
├── protocol.py # the socket path, newline-JSON framing and the client's send()
|
|
280
|
+
├── vault.py # Vault KV v2: values in the secret's data, metadata in its custom metadata
|
|
281
|
+
├── keyring_store.py # the OS keyring door and the shelf: values in the keyring, records in the catalogue
|
|
282
|
+
├── catalogue.py # the keyring shelf's records, in one owner-only JSON file
|
|
283
|
+
├── models.py # Secret and tag parsing; no field of it carries a value
|
|
284
|
+
├── browser.py # CDP target discovery and Input.insertText; only a length comes back
|
|
285
|
+
└── totp.py # RFC 6238, computed in-process so the seed never leaves
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Name
|
|
289
|
+
|
|
290
|
+
A **saddlebag** is the kit a horse carries on a ride: the right tools, ready when
|
|
291
|
+
needed, returned to the stable when the ride is done. It fits the stablemate
|
|
292
|
+
vocabulary. Ostler tends the stable, farrier fits the gear, workhorse does the
|
|
293
|
+
riding, and saddlebag carries what the journey needs.
|
|
294
|
+
|
|
295
|
+
## Map
|
|
296
|
+
|
|
297
|
+
- `saddlebag/`: the package behind the `saddlebag` command. [Package layout](#package-layout) says what each module owns.
|
|
298
|
+
- `docs/`: the security model and the workflow integration.
|
|
299
|
+
- `tests/`: the suite, run from the repo root with `uv run --package stablemate-saddlebag pytest saddlebag/tests`.
|