gipper 0.3.0__tar.gz → 0.4.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.
- {gipper-0.3.0 → gipper-0.4.0}/AGENTS.md +1 -0
- {gipper-0.3.0 → gipper-0.4.0}/CHANGELOG.md +28 -0
- {gipper-0.3.0 → gipper-0.4.0}/PKG-INFO +13 -1
- {gipper-0.3.0 → gipper-0.4.0}/README.md +12 -0
- {gipper-0.3.0 → gipper-0.4.0}/ROADMAP.md +3 -0
- {gipper-0.3.0 → gipper-0.4.0}/SECURITY.md +4 -0
- {gipper-0.3.0 → gipper-0.4.0}/artifact.toml +5 -6
- {gipper-0.3.0 → gipper-0.4.0}/docs/clients.md +3 -1
- gipper-0.4.0/docs/custody.md +101 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/__init__.py +1 -1
- {gipper-0.3.0 → gipper-0.4.0}/gipper/cli.py +82 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/config.py +2 -0
- gipper-0.4.0/gipper/custody.py +536 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/job.py +10 -1
- {gipper-0.3.0 → gipper-0.4.0}/gipper/run.py +4 -2
- {gipper-0.3.0 → gipper-0.4.0}/gipper/ssh.py +36 -9
- {gipper-0.3.0 → gipper-0.4.0}/gipper.egg-info/PKG-INFO +13 -1
- {gipper-0.3.0 → gipper-0.4.0}/gipper.egg-info/SOURCES.txt +3 -0
- {gipper-0.3.0 → gipper-0.4.0}/pyproject.toml +1 -1
- {gipper-0.3.0 → gipper-0.4.0}/scripts/build_site.py +1 -0
- {gipper-0.3.0 → gipper-0.4.0}/site/index.html +1 -1
- gipper-0.4.0/tests/test_custody.py +238 -0
- {gipper-0.3.0 → gipper-0.4.0}/uv.lock +1 -1
- {gipper-0.3.0 → gipper-0.4.0}/.env.example +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/CONTRIBUTING.md +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/LICENSE +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/MANIFEST.in +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/RELEASING.md +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/deploy/gipper-reap.service +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/deploy/gipper-reap.timer +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/docs/lessons.md +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/docs/placement.md +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/docs/quickstart.md +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/docs/sources/vast/README.md +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/examples/smoke/hello.txt +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/examples/smoke.toml +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/examples/udp-echo.toml +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/billing.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/bootstrap.sh +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/costs.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/cuda_probe.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/history.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/http.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/latency.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/ledger.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/lifecycle.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/market.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/network_probe.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/placement.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/providers/__init__.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/providers/runpod.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/providers/shadeform.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/providers/vast.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/reap.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/runpod/__init__.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/runpod/api.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper/runpod/run.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper.egg-info/dependency_links.txt +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper.egg-info/entry_points.txt +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper.egg-info/requires.txt +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/gipper.egg-info/top_level.txt +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/scripts/check_dist.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/setup.cfg +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/site/favicon.svg +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/site/style.css +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/tests/conftest.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/tests/test_billing.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/tests/test_compat.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/tests/test_config.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/tests/test_costs.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/tests/test_cuda_probe.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/tests/test_history.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/tests/test_job.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/tests/test_latency.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/tests/test_lifecycle.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/tests/test_market.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/tests/test_network_probe.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/tests/test_providers.py +0 -0
- {gipper-0.3.0 → gipper-0.4.0}/tests/test_reap.py +0 -0
|
@@ -54,6 +54,7 @@ working *on* gipper.
|
|
|
54
54
|
| `costs.py` | the whole-run estimate and score for one offer |
|
|
55
55
|
| `history.py` | memory, read from the ledger: failure rates, ready times, link speeds, compute times, billing factors |
|
|
56
56
|
| `run.py` | the run: plan, rent, preflight, upload, detached job, log, checkpoints, collect, teardown |
|
|
57
|
+
| `custody.py` | what reached each machine: manifests, scanning, the incident report |
|
|
57
58
|
| `lifecycle.py` | verified teardown |
|
|
58
59
|
| `reap.py` | the 15-minute sweep for forgotten machines |
|
|
59
60
|
| `billing.py` | providers' bills → `spend.billed`; spend for the daily cap |
|
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 (2026-11-05)
|
|
4
|
+
|
|
5
|
+
A forensic trail of what reached each rented machine. When a provider reports an
|
|
6
|
+
incident, `gipper custody report` answers what to rotate and what can't be vouched
|
|
7
|
+
for. See [docs/custody.md](docs/custody.md).
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Every `Remote.put`, from jobs and client scripts alike, takes a manifest first**
|
|
12
|
+
of exactly what tar will send (path, size, sha256, mode), and writes a
|
|
13
|
+
`custody.upload` receipt before any byte moves. If the manifest can't be taken,
|
|
14
|
+
the upload doesn't happen.
|
|
15
|
+
- **Uploads are scanned** for sensitive file names and credential patterns. Findings
|
|
16
|
+
are recorded and printed, never the matched text. Provider keys and values from
|
|
17
|
+
`custody_secret_files` are always refused. `custody_on_sensitive` or a job's
|
|
18
|
+
`[custody]` table can refuse other findings, with an `allow` list for reviewed
|
|
19
|
+
false positives. Jobs are checked before a machine is rented.
|
|
20
|
+
- **`Remote.put_secrets`** places credentials deliberately, as a mode-600 env file
|
|
21
|
+
sent over stdin, recording names and keyed fingerprints only.
|
|
22
|
+
- **`custody.machine` receipts:** provider, machine, address, image, env names, and
|
|
23
|
+
hashes of the job file and command.
|
|
24
|
+
- **`gipper custody`:** `report` (rotate list, sensitive uploads, machines, gaps),
|
|
25
|
+
`show`, `find` (which machines received this file), `check` (was this exact value
|
|
26
|
+
ever placed on a machine; use it to confirm a rotation), and `declare` (record a
|
|
27
|
+
scp, a tunnel, or a machine gipper didn't rent).
|
|
28
|
+
- `report --check-providers` compares providers' bills with the ledger, to reveal
|
|
29
|
+
machines gipper never knew about.
|
|
30
|
+
|
|
3
31
|
## 0.3.0 (2026-11-04)
|
|
4
32
|
|
|
5
33
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gipper
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Run GPU jobs across cloud markets with cost estimates, verified teardown, and an append-only ledger
|
|
5
5
|
Author: Marc Lavallee
|
|
6
6
|
License-Expression: MIT
|
|
@@ -174,6 +174,18 @@ compute_s = 900
|
|
|
174
174
|
setup_download_gb = 5 # weights and wheels the job fetches itself
|
|
175
175
|
```
|
|
176
176
|
|
|
177
|
+
## After a provider incident
|
|
178
|
+
|
|
179
|
+
Every upload is recorded before it leaves: a manifest of what was sent, sensitive-looking
|
|
180
|
+
files flagged, and provider keys refused outright. When a provider reports a problem, one
|
|
181
|
+
command gives you what to rotate, what to review, and what the trail can't vouch for:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
gipper custody report --provider runpod --since 2026-10-01 --check-providers
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
See [docs/custody.md](docs/custody.md), including how to record what went around gipper.
|
|
188
|
+
|
|
177
189
|
## How gipper chooses
|
|
178
190
|
|
|
179
191
|
Every offer is judged on the whole run, not its hourly price
|
|
@@ -147,6 +147,18 @@ compute_s = 900
|
|
|
147
147
|
setup_download_gb = 5 # weights and wheels the job fetches itself
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
+
## After a provider incident
|
|
151
|
+
|
|
152
|
+
Every upload is recorded before it leaves: a manifest of what was sent, sensitive-looking
|
|
153
|
+
files flagged, and provider keys refused outright. When a provider reports a problem, one
|
|
154
|
+
command gives you what to rotate, what to review, and what the trail can't vouch for:
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
gipper custody report --provider runpod --since 2026-10-01 --check-providers
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
See [docs/custody.md](docs/custody.md), including how to record what went around gipper.
|
|
161
|
+
|
|
150
162
|
## How gipper chooses
|
|
151
163
|
|
|
152
164
|
Every offer is judged on the whole run, not its hourly price
|
|
@@ -9,4 +9,7 @@ Potential next steps, without delivery commitments:
|
|
|
9
9
|
- Coordinate owned GPUs and rented capacity through requests and leases.
|
|
10
10
|
- Add priorities and pre-emption with explicit resource ownership.
|
|
11
11
|
|
|
12
|
+
- Extend the custody trail to outputs brought back from a machine, and look inside
|
|
13
|
+
uploaded archives.
|
|
14
|
+
|
|
12
15
|
These features are not implemented by the current CLI.
|
|
@@ -31,6 +31,10 @@ no promised response time.
|
|
|
31
31
|
or a mode-600 dotenv file. Known provider key values are redacted from API
|
|
32
32
|
errors and receipts, but arbitrary secrets printed by jobs are not detected.
|
|
33
33
|
The `[env]` name check is a guard, not a general secret detector.
|
|
34
|
+
- `gipper custody report` lists what reached rented machines, for use after a
|
|
35
|
+
provider incident. It covers what passed through gipper or was declared; see
|
|
36
|
+
[docs/custody.md](docs/custody.md) for its limits. Custody manifests hold file
|
|
37
|
+
paths and hashes, and fingerprints are keyed hashes, never secret values.
|
|
34
38
|
- Receipts include paths, provider resource identifiers, host addresses, and
|
|
35
39
|
commands; logs and collected files can contain sensitive job data. Restrict
|
|
36
40
|
filesystem permissions and review everything before sharing it.
|
|
@@ -6,20 +6,19 @@ kind = "reference-guide"
|
|
|
6
6
|
form = "collection"
|
|
7
7
|
status = "live"
|
|
8
8
|
created = "2026-11-04"
|
|
9
|
-
updated = "2026-11-
|
|
9
|
+
updated = "2026-11-05"
|
|
10
10
|
|
|
11
|
-
# Raw HTML is owned by site/; scripts/build_site.py renders public Markdown guides.
|
|
12
11
|
[build]
|
|
13
|
-
site = "docs"
|
|
14
12
|
commands = ["uv run --group site python scripts/build_site.py"]
|
|
13
|
+
site = "docs"
|
|
15
14
|
|
|
16
15
|
[deploy]
|
|
17
16
|
target = "github-pages"
|
|
18
17
|
|
|
19
18
|
[deploy.github-pages]
|
|
20
|
-
|
|
19
|
+
commit = true
|
|
21
20
|
docs_dir = "docs"
|
|
22
|
-
|
|
21
|
+
mode = "docs"
|
|
23
22
|
own_root = true
|
|
24
|
-
commit = true
|
|
25
23
|
push = false
|
|
24
|
+
subpath = ""
|
|
@@ -40,7 +40,7 @@ These names are stable within 0.x:
|
|
|
40
40
|
| `gipper.providers.runpod.Runpod().api()` | the raw Runpod client |
|
|
41
41
|
| `gipper.ledger.read(since=, kinds=, job=)` / `.write(kind, payload, job=)` | receipts |
|
|
42
42
|
| `gipper.history.History.load()` | memory: `.failure(p, machine)`, `.ready_s`, `.mbps`, `.compute_s(workload, gpu)`, `.billing_factor(p)` |
|
|
43
|
-
| `gipper.ssh.Remote(host, port, user)` | `.run`, `.put`, `.get` over gipper's key |
|
|
43
|
+
| `gipper.ssh.Remote(host, port, user)` | `.run`, `.put`, `.put_secrets`, `.get` over gipper's key. `.put` records a custody manifest first and can raise `gipper.custody.Refused` |
|
|
44
44
|
|
|
45
45
|
Kept for scripts written before 0.2: `gipper.runpod.api.Api` and
|
|
46
46
|
`gipper.runpod.run._wait_ready`. Any other `gipper.runpod.run` name raises an
|
|
@@ -57,6 +57,8 @@ instead of silently patching nothing.
|
|
|
57
57
|
- `spend` (computed at teardown) and `spend.billed` (the provider's bill, from
|
|
58
58
|
`reconcile`; the latest one per machine counts). `gipper.billing.spend_since(t)`
|
|
59
59
|
combines them by machine.
|
|
60
|
+
- `custody.machine`, `custody.upload`, `custody.uploaded`, `custody.secret`,
|
|
61
|
+
`custody.note`: what reached a machine ([custody.md](custody.md)).
|
|
60
62
|
- Receipts without `provider` predate 2026-10-26 and are Runpod's.
|
|
61
63
|
|
|
62
64
|
An editable install imports code directly from its checkout. Use an isolated
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Custody: what reached each machine
|
|
2
|
+
|
|
3
|
+
When a provider tells you a machine hosting your work may have been compromised, you
|
|
4
|
+
need three answers quickly: what was on it, which credentials to rotate, and what you
|
|
5
|
+
can't be sure about. gipper keeps that trail as it works.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
gipper custody report --provider runpod --since 2026-10-01 --check-providers
|
|
9
|
+
gipper custody show POD_ID # every file sent to one machine
|
|
10
|
+
gipper custody find ./data/private.csv # which machines received this file
|
|
11
|
+
gipper custody check ./secrets.env # was this exact value ever placed on a machine
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`report` has four sections:
|
|
15
|
+
|
|
16
|
+
- **Rotate:** credentials placed on machines, with how many machines and distinct values.
|
|
17
|
+
- **Review:** uploaded files that look sensitive.
|
|
18
|
+
- **Machines:** each rental with its machine ID, job, uploads and env names.
|
|
19
|
+
- **Gaps:** what the report can't vouch for. Read this section before relying on the others.
|
|
20
|
+
|
|
21
|
+
Add `--json` for the whole report as data. `--pod`, `--machine` and `--until` narrow it.
|
|
22
|
+
|
|
23
|
+
## What is recorded
|
|
24
|
+
|
|
25
|
+
**Each upload.** `Remote.put` reads the tar stream it is about to send, so exclude rules
|
|
26
|
+
apply exactly as they will on the wire. It stores a manifest (path, size, sha256, mode)
|
|
27
|
+
under `~/.local/share/gipper/custody` with owner-only permissions, then writes a
|
|
28
|
+
`custody.upload` receipt. Only then does anything leave. If the manifest can't be taken,
|
|
29
|
+
the upload doesn't happen. A `custody.uploaded` receipt records the outcome. An upload
|
|
30
|
+
with no outcome, or a failed one, may still have delivered bytes, so treat it as arrived.
|
|
31
|
+
|
|
32
|
+
**Each machine.** `custody.machine` records provider, machine ID, address, image, env
|
|
33
|
+
names, and hashes of the job file and command.
|
|
34
|
+
|
|
35
|
+
**Sensitive material.** Uploads are checked for file names like `.env`, `id_ed25519`
|
|
36
|
+
and `*.pem`, and content patterns like private keys and common API tokens. Findings keep
|
|
37
|
+
the path and rule, never the matched text. Files over 2 MB are hashed but not scanned,
|
|
38
|
+
and archives are not opened. Both appear under Gaps.
|
|
39
|
+
|
|
40
|
+
**Credentials.** Values are never recorded. A fingerprint is a keyed hash, with the key
|
|
41
|
+
kept beside the ledger, so `check` can recognise a value without the ledger being
|
|
42
|
+
usable to guess it. After you rotate, `check` on the new values should report that none
|
|
43
|
+
has been seen on a machine.
|
|
44
|
+
|
|
45
|
+
## Placing a credential on a machine
|
|
46
|
+
|
|
47
|
+
Don't upload credential files. Do this instead, which records the names:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
remote.put_secrets({"SERVICE_TOKEN": token}, "/root/job/service.env")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The file is mode 600, and the values travel over stdin rather than the command line. It
|
|
54
|
+
is receipted before sending, so a failed write still counts as exposed.
|
|
55
|
+
|
|
56
|
+
## Recording what went around gipper
|
|
57
|
+
|
|
58
|
+
gipper can't see a `scp` you ran by hand, a tunnel, or a machine created in a provider's
|
|
59
|
+
console. Declare them, and they join the report:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
gipper custody declare --provider runpod --pod POD_ID \
|
|
63
|
+
--secret SERVICE_TOKEN --from-env ./secrets.env --path /root/job/service.env --how scp
|
|
64
|
+
gipper custody declare --provider runpod --pod POD_ID --file ./bundle --path /root/job/bundle --how scp
|
|
65
|
+
gipper custody declare --provider runpod --pod POD_ID --tunnel "reverse tunnel to local port 8917"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Use `--at` for something that happened earlier, and `--confidence inferred` when you
|
|
69
|
+
worked it out rather than saw it. `report --check-providers` compares providers' bills
|
|
70
|
+
with the ledger and lists machines gipper has never heard of. This call is read-only.
|
|
71
|
+
|
|
72
|
+
## Refusing uploads
|
|
73
|
+
|
|
74
|
+
```toml
|
|
75
|
+
# ~/.config/gipper/config.toml
|
|
76
|
+
custody_on_sensitive = "refuse" # default "warn": record it and say so
|
|
77
|
+
custody_secret_files = ["~/.config/app/.env"] # these values must never be uploaded
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```toml
|
|
81
|
+
# in a job
|
|
82
|
+
[custody]
|
|
83
|
+
on_sensitive = "refuse"
|
|
84
|
+
allow = ["tests/fixtures/*.pem"] # reviewed false positives
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Provider keys and values from `custody_secret_files` are always refused, even when a
|
|
88
|
+
path is allowed. Jobs are checked before renting, so a refusal doesn't cost a machine.
|
|
89
|
+
|
|
90
|
+
## Limits
|
|
91
|
+
|
|
92
|
+
- It records what passed through gipper or was declared. Commands run on a machine, and
|
|
93
|
+
anything a job downloaded for itself, are not recorded. The command is kept as a hash.
|
|
94
|
+
- Outputs brought back from a machine have no manifest yet.
|
|
95
|
+
- Scanning catches common shapes and known values, not every secret. A clean scan is not
|
|
96
|
+
proof that a file holds none.
|
|
97
|
+
- Providers may add credentials of their own to a machine, such as a machine-scoped API
|
|
98
|
+
key. Check your provider's console for keys that outlived their machines.
|
|
99
|
+
- The ledger is append-only by convention and isn't tamper-proof. Treat it as your own
|
|
100
|
+
record, not as evidence against an attacker who had access to this machine.
|
|
101
|
+
- A trail only exists from the release that adds it. Earlier rentals show under Gaps.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""gipper: GPU jobs across cloud markets."""
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.4.0"
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
gipper pod cost [--since YYYY-MM-DD] spend per job: billed where known, else computed
|
|
11
11
|
gipper pod account balance per provider
|
|
12
12
|
gipper ledger [--since YYYY-MM-DD] [--job ID] raw receipts
|
|
13
|
+
gipper custody report [--provider P] [--since D] [--check-providers] what reached rented machines
|
|
14
|
+
gipper custody find FILE|SHA256|PATH-PART which machines received this
|
|
15
|
+
gipper custody check ENV_FILE was this exact value ever placed on a machine
|
|
16
|
+
gipper custody declare --provider P --pod ID --secret NAME [...] record what went around gipper
|
|
13
17
|
|
|
14
18
|
Providers: runpod, vast, shadeform (keys RUNPOD_API_KEY, VAST_API_KEY,
|
|
15
19
|
SHADEFORM_API_KEY in the environment or ~/.config/gipper/.env). Vast and Shadeform list offers without a key.
|
|
@@ -166,6 +170,51 @@ def _pod(args):
|
|
|
166
170
|
print(f"total computed ${computed:.3f}; billed where known, else computed: ${billed_total:.3f}")
|
|
167
171
|
|
|
168
172
|
|
|
173
|
+
def _custody(a):
|
|
174
|
+
from gipper import custody
|
|
175
|
+
if a.cmd == "report":
|
|
176
|
+
rep = custody.report(provider=a.provider, pod=a.pod, machine=a.machine, since=a.since, until=a.until,
|
|
177
|
+
check_providers=a.check_providers)
|
|
178
|
+
if a.json:
|
|
179
|
+
print(json.dumps(rep, indent=1, default=str))
|
|
180
|
+
else:
|
|
181
|
+
custody.show(rep)
|
|
182
|
+
elif a.cmd == "show":
|
|
183
|
+
found = [m for m in custody.collect().values() if m["pod"] == a.pod]
|
|
184
|
+
if not found:
|
|
185
|
+
raise SystemExit(f"{a.pod}: not in the ledger")
|
|
186
|
+
for m in found:
|
|
187
|
+
print(f"{m['provider']}:{m['pod']} machine {m['machine'] or '?'} {m['name'] or '?'}")
|
|
188
|
+
for placed in m["secrets"]:
|
|
189
|
+
names = ", ".join(s["name"] for s in placed["secrets"])
|
|
190
|
+
print(f" secrets {placed['at']} {names} -> {placed.get('path') or '?'} {placed.get('how') or ''}".rstrip())
|
|
191
|
+
for u in m["uploads"]:
|
|
192
|
+
state = "refused" if u.get("refused") else "declared" if u.get("declared") else {True: "sent", False: "failed", None: "outcome unknown"}[u.get("ok")]
|
|
193
|
+
print(f" upload {u['at']} {u['src']} -> {u['dst']} [{state}] manifest {u['manifest']}")
|
|
194
|
+
flagged = {f["path"]: f["rule"] for f in u.get("findings") or []}
|
|
195
|
+
for e in custody.load_manifest(u["manifest"])["entries"]:
|
|
196
|
+
if e["type"] != "dir":
|
|
197
|
+
print(f" {(e.get('sha256') or '')[:16]:16s} {e['size']:>12d} {e['path']}" + (f" !{flagged[e['path']]}" if e["path"] in flagged else ""))
|
|
198
|
+
for n in m["notes"]:
|
|
199
|
+
print(f" note {n['at']} {n.get('tunnel') or ''} {n.get('note') or ''}".rstrip())
|
|
200
|
+
elif a.cmd == "find":
|
|
201
|
+
hits = custody.find(a.target)
|
|
202
|
+
for h in hits:
|
|
203
|
+
print(f"{h['at']} {h['machine']:32s} {h['path']} (from {h['src']}; {'sent' if h['arrived'] else 'outcome unknown' if h['arrived'] is None else 'failed'})")
|
|
204
|
+
print(f"{len(hits)} match(es)")
|
|
205
|
+
elif a.cmd == "check":
|
|
206
|
+
rows = custody.check(a.env_file)
|
|
207
|
+
for r in rows:
|
|
208
|
+
print(f"{r['name']:28s} " + (f"EXPOSED on {', '.join(r['exposed_on'])}" if r["exposed_on"] else "not seen on any machine"))
|
|
209
|
+
elif a.cmd == "declare":
|
|
210
|
+
written = custody.declare(provider=a.provider, pod=a.pod, secrets=a.secret, from_env=a.from_env, path=a.path, files=a.file,
|
|
211
|
+
tunnel=a.tunnel, note=a.note, how=a.how, confidence=a.confidence, machine=a.machine,
|
|
212
|
+
host=a.host, port=a.port, at=a.at)
|
|
213
|
+
if not written:
|
|
214
|
+
raise SystemExit("nothing to declare: give --secret, --file, --tunnel or --note")
|
|
215
|
+
print(f"{len(written)} receipt(s) written for {a.provider}:{a.pod}")
|
|
216
|
+
|
|
217
|
+
|
|
169
218
|
def main(argv=None):
|
|
170
219
|
sys.stdout.reconfigure(line_buffering=True) # live output when redirected to a file
|
|
171
220
|
signal.signal(signal.SIGPIPE, signal.SIG_DFL) # `gipper pod hosts | head` ends quietly
|
|
@@ -201,9 +250,42 @@ def main(argv=None):
|
|
|
201
250
|
lg = sub.add_parser("ledger", help="raw receipts")
|
|
202
251
|
lg.add_argument("--since")
|
|
203
252
|
lg.add_argument("--job")
|
|
253
|
+
cu = sub.add_parser("custody", help="what reached rented machines: the incident trail")
|
|
254
|
+
cs = cu.add_subparsers(dest="cmd", required=True)
|
|
255
|
+
rp = cs.add_parser("report", help="rotate list, sensitive uploads, machines and gaps")
|
|
256
|
+
rp.add_argument("--provider", choices=PROVIDERS)
|
|
257
|
+
rp.add_argument("--pod")
|
|
258
|
+
rp.add_argument("--machine")
|
|
259
|
+
rp.add_argument("--since", help="machines in use on or after this date")
|
|
260
|
+
rp.add_argument("--until", help="machines in use on or before this date")
|
|
261
|
+
rp.add_argument("--check-providers", action="store_true", help="also ask providers' bills for machines the ledger doesn't know (read-only)")
|
|
262
|
+
rp.add_argument("--json", action="store_true")
|
|
263
|
+
sh = cs.add_parser("show", help="every file sent to one machine")
|
|
264
|
+
sh.add_argument("pod")
|
|
265
|
+
fd = cs.add_parser("find", help="which machines received a file")
|
|
266
|
+
fd.add_argument("target", help="local file, sha256, or part of a path")
|
|
267
|
+
ck = cs.add_parser("check", help="has each value in this file been placed on a machine (values are not shown)")
|
|
268
|
+
ck.add_argument("env_file")
|
|
269
|
+
dc = cs.add_parser("declare", help="record what reached a machine outside gipper: scp, a tunnel, a machine it didn't rent")
|
|
270
|
+
dc.add_argument("--provider", required=True)
|
|
271
|
+
dc.add_argument("--pod", required=True)
|
|
272
|
+
dc.add_argument("--machine")
|
|
273
|
+
dc.add_argument("--host")
|
|
274
|
+
dc.add_argument("--port", type=int)
|
|
275
|
+
dc.add_argument("--secret", action="append", default=[], help="credential name, repeatable")
|
|
276
|
+
dc.add_argument("--from-env", help="read those names' values from this file, to fingerprint them")
|
|
277
|
+
dc.add_argument("--path", help="where it landed on the machine")
|
|
278
|
+
dc.add_argument("--file", action="append", default=[], help="local file or directory sent, repeatable: takes a manifest")
|
|
279
|
+
dc.add_argument("--tunnel", help="e.g. \"reverse tunnel to local port 8917\"")
|
|
280
|
+
dc.add_argument("--how", default="", help="e.g. scp")
|
|
281
|
+
dc.add_argument("--note", default="")
|
|
282
|
+
dc.add_argument("--confidence", choices=["direct", "inferred"], default="direct")
|
|
283
|
+
dc.add_argument("--at", help="when it happened, if not now (ISO time)")
|
|
204
284
|
a = ap.parse_args(argv)
|
|
205
285
|
if a.group == "pod":
|
|
206
286
|
return _pod(a)
|
|
287
|
+
if a.group == "custody":
|
|
288
|
+
return _custody(a)
|
|
207
289
|
if a.group == "ledger":
|
|
208
290
|
from gipper import ledger
|
|
209
291
|
for rec in ledger.read(since=a.since, job=a.job):
|
|
@@ -35,6 +35,8 @@ DEFAULTS = {
|
|
|
35
35
|
"hour_value_usd": 3.0,
|
|
36
36
|
"failure_cost_usd": 1.0,
|
|
37
37
|
"history_days": 90, # how far back the ledger counts as memory
|
|
38
|
+
"custody_on_sensitive": "warn", # warn: record and say so; refuse: stop the upload (see docs/custody.md)
|
|
39
|
+
"custody_secret_files": [], # env or JSON files whose values must never be uploaded
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
|