gipper 0.3.0__tar.gz → 0.4.1__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 (79) hide show
  1. {gipper-0.3.0 → gipper-0.4.1}/AGENTS.md +1 -0
  2. {gipper-0.3.0 → gipper-0.4.1}/CHANGELOG.md +38 -0
  3. {gipper-0.3.0 → gipper-0.4.1}/PKG-INFO +23 -3
  4. {gipper-0.3.0 → gipper-0.4.1}/README.md +22 -2
  5. {gipper-0.3.0 → gipper-0.4.1}/ROADMAP.md +3 -0
  6. {gipper-0.3.0 → gipper-0.4.1}/SECURITY.md +4 -0
  7. {gipper-0.3.0 → gipper-0.4.1}/artifact.toml +5 -6
  8. {gipper-0.3.0 → gipper-0.4.1}/docs/clients.md +3 -1
  9. gipper-0.4.1/docs/custody.md +101 -0
  10. {gipper-0.3.0 → gipper-0.4.1}/docs/quickstart.md +15 -5
  11. {gipper-0.3.0 → gipper-0.4.1}/gipper/__init__.py +1 -1
  12. {gipper-0.3.0 → gipper-0.4.1}/gipper/cli.py +82 -0
  13. {gipper-0.3.0 → gipper-0.4.1}/gipper/config.py +2 -0
  14. gipper-0.4.1/gipper/custody.py +536 -0
  15. {gipper-0.3.0 → gipper-0.4.1}/gipper/job.py +10 -1
  16. {gipper-0.3.0 → gipper-0.4.1}/gipper/run.py +4 -2
  17. {gipper-0.3.0 → gipper-0.4.1}/gipper/ssh.py +36 -9
  18. {gipper-0.3.0 → gipper-0.4.1}/gipper.egg-info/PKG-INFO +23 -3
  19. {gipper-0.3.0 → gipper-0.4.1}/gipper.egg-info/SOURCES.txt +3 -0
  20. {gipper-0.3.0 → gipper-0.4.1}/pyproject.toml +1 -1
  21. {gipper-0.3.0 → gipper-0.4.1}/scripts/build_site.py +1 -0
  22. {gipper-0.3.0 → gipper-0.4.1}/scripts/check_dist.py +6 -0
  23. {gipper-0.3.0 → gipper-0.4.1}/site/index.html +3 -3
  24. gipper-0.4.1/tests/test_custody.py +238 -0
  25. {gipper-0.3.0 → gipper-0.4.1}/uv.lock +1 -1
  26. {gipper-0.3.0 → gipper-0.4.1}/.env.example +0 -0
  27. {gipper-0.3.0 → gipper-0.4.1}/CONTRIBUTING.md +0 -0
  28. {gipper-0.3.0 → gipper-0.4.1}/LICENSE +0 -0
  29. {gipper-0.3.0 → gipper-0.4.1}/MANIFEST.in +0 -0
  30. {gipper-0.3.0 → gipper-0.4.1}/RELEASING.md +0 -0
  31. {gipper-0.3.0 → gipper-0.4.1}/deploy/gipper-reap.service +0 -0
  32. {gipper-0.3.0 → gipper-0.4.1}/deploy/gipper-reap.timer +0 -0
  33. {gipper-0.3.0 → gipper-0.4.1}/docs/lessons.md +0 -0
  34. {gipper-0.3.0 → gipper-0.4.1}/docs/placement.md +0 -0
  35. {gipper-0.3.0 → gipper-0.4.1}/docs/sources/vast/README.md +0 -0
  36. {gipper-0.3.0 → gipper-0.4.1}/examples/smoke/hello.txt +0 -0
  37. {gipper-0.3.0 → gipper-0.4.1}/examples/smoke.toml +0 -0
  38. {gipper-0.3.0 → gipper-0.4.1}/examples/udp-echo.toml +0 -0
  39. {gipper-0.3.0 → gipper-0.4.1}/gipper/billing.py +0 -0
  40. {gipper-0.3.0 → gipper-0.4.1}/gipper/bootstrap.sh +0 -0
  41. {gipper-0.3.0 → gipper-0.4.1}/gipper/costs.py +0 -0
  42. {gipper-0.3.0 → gipper-0.4.1}/gipper/cuda_probe.py +0 -0
  43. {gipper-0.3.0 → gipper-0.4.1}/gipper/history.py +0 -0
  44. {gipper-0.3.0 → gipper-0.4.1}/gipper/http.py +0 -0
  45. {gipper-0.3.0 → gipper-0.4.1}/gipper/latency.py +0 -0
  46. {gipper-0.3.0 → gipper-0.4.1}/gipper/ledger.py +0 -0
  47. {gipper-0.3.0 → gipper-0.4.1}/gipper/lifecycle.py +0 -0
  48. {gipper-0.3.0 → gipper-0.4.1}/gipper/market.py +0 -0
  49. {gipper-0.3.0 → gipper-0.4.1}/gipper/network_probe.py +0 -0
  50. {gipper-0.3.0 → gipper-0.4.1}/gipper/placement.py +0 -0
  51. {gipper-0.3.0 → gipper-0.4.1}/gipper/providers/__init__.py +0 -0
  52. {gipper-0.3.0 → gipper-0.4.1}/gipper/providers/runpod.py +0 -0
  53. {gipper-0.3.0 → gipper-0.4.1}/gipper/providers/shadeform.py +0 -0
  54. {gipper-0.3.0 → gipper-0.4.1}/gipper/providers/vast.py +0 -0
  55. {gipper-0.3.0 → gipper-0.4.1}/gipper/reap.py +0 -0
  56. {gipper-0.3.0 → gipper-0.4.1}/gipper/runpod/__init__.py +0 -0
  57. {gipper-0.3.0 → gipper-0.4.1}/gipper/runpod/api.py +0 -0
  58. {gipper-0.3.0 → gipper-0.4.1}/gipper/runpod/run.py +0 -0
  59. {gipper-0.3.0 → gipper-0.4.1}/gipper.egg-info/dependency_links.txt +0 -0
  60. {gipper-0.3.0 → gipper-0.4.1}/gipper.egg-info/entry_points.txt +0 -0
  61. {gipper-0.3.0 → gipper-0.4.1}/gipper.egg-info/requires.txt +0 -0
  62. {gipper-0.3.0 → gipper-0.4.1}/gipper.egg-info/top_level.txt +0 -0
  63. {gipper-0.3.0 → gipper-0.4.1}/setup.cfg +0 -0
  64. {gipper-0.3.0 → gipper-0.4.1}/site/favicon.svg +0 -0
  65. {gipper-0.3.0 → gipper-0.4.1}/site/style.css +0 -0
  66. {gipper-0.3.0 → gipper-0.4.1}/tests/conftest.py +0 -0
  67. {gipper-0.3.0 → gipper-0.4.1}/tests/test_billing.py +0 -0
  68. {gipper-0.3.0 → gipper-0.4.1}/tests/test_compat.py +0 -0
  69. {gipper-0.3.0 → gipper-0.4.1}/tests/test_config.py +0 -0
  70. {gipper-0.3.0 → gipper-0.4.1}/tests/test_costs.py +0 -0
  71. {gipper-0.3.0 → gipper-0.4.1}/tests/test_cuda_probe.py +0 -0
  72. {gipper-0.3.0 → gipper-0.4.1}/tests/test_history.py +0 -0
  73. {gipper-0.3.0 → gipper-0.4.1}/tests/test_job.py +0 -0
  74. {gipper-0.3.0 → gipper-0.4.1}/tests/test_latency.py +0 -0
  75. {gipper-0.3.0 → gipper-0.4.1}/tests/test_lifecycle.py +0 -0
  76. {gipper-0.3.0 → gipper-0.4.1}/tests/test_market.py +0 -0
  77. {gipper-0.3.0 → gipper-0.4.1}/tests/test_network_probe.py +0 -0
  78. {gipper-0.3.0 → gipper-0.4.1}/tests/test_providers.py +0 -0
  79. {gipper-0.3.0 → gipper-0.4.1}/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,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.1 (2026-11-05)
4
+
5
+ ### Fixed
6
+
7
+ - Remove a stale version from the README status and refresh the PyPI package
8
+ description with the verified package-index install instructions.
9
+ - Reject stale numeric README status versions during distribution validation.
10
+ Pages continues to derive its version from package metadata.
11
+
12
+
13
+ ## 0.4.0 (2026-11-05)
14
+
15
+ A forensic trail of what reached each rented machine. When a provider reports an
16
+ incident, `gipper custody report` answers what to rotate and what can't be vouched
17
+ for. See [docs/custody.md](docs/custody.md).
18
+
19
+ ### Added
20
+
21
+ - **Every `Remote.put`, from jobs and client scripts alike, takes a manifest first**
22
+ of exactly what tar will send (path, size, sha256, mode), and writes a
23
+ `custody.upload` receipt before any byte moves. If the manifest can't be taken,
24
+ the upload doesn't happen.
25
+ - **Uploads are scanned** for sensitive file names and credential patterns. Findings
26
+ are recorded and printed, never the matched text. Provider keys and values from
27
+ `custody_secret_files` are always refused. `custody_on_sensitive` or a job's
28
+ `[custody]` table can refuse other findings, with an `allow` list for reviewed
29
+ false positives. Jobs are checked before a machine is rented.
30
+ - **`Remote.put_secrets`** places credentials deliberately, as a mode-600 env file
31
+ sent over stdin, recording names and keyed fingerprints only.
32
+ - **`custody.machine` receipts:** provider, machine, address, image, env names, and
33
+ hashes of the job file and command.
34
+ - **`gipper custody`:** `report` (rotate list, sensitive uploads, machines, gaps),
35
+ `show`, `find` (which machines received this file), `check` (was this exact value
36
+ ever placed on a machine; use it to confirm a rotation), and `declare` (record a
37
+ scp, a tunnel, or a machine gipper didn't rent).
38
+ - `report --check-providers` compares providers' bills with the ledger, to reveal
39
+ machines gipper never knew about.
40
+
3
41
  ## 0.3.0 (2026-11-04)
4
42
 
5
43
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gipper
3
- Version: 0.3.0
3
+ Version: 0.4.1
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
@@ -42,7 +42,7 @@ fees, and host failures. An append-only local ledger records runs and costs;
42
42
  future estimates learn from **your own** workloads and provider history. A fresh
43
43
  installation starts with stated defaults, not someone else's rental history.
44
44
 
45
- **Status: 0.3.0, alpha.** Linux client, CLI and Python API, no daemon. Runpod and
45
+ **Status: alpha.** Linux client, CLI and Python API, no daemon. Runpod and
46
46
  Vast have live-run validation; Shadeform is experimental and has not had a live
47
47
  smoke test. See the [roadmap](ROADMAP.md) for planned features and
48
48
  [SECURITY.md](SECURITY.md) for trust and billing limits. Licensed under [MIT](LICENSE).
@@ -54,7 +54,15 @@ gzip support. `ping` is optional for interactive placement. The Python runtime
54
54
  has no third-party dependencies. Remote machines need a Linux shell, `tar`,
55
55
  `timeout`, `curl`, and NVIDIA tools; the bootstrap installs `uv` if needed.
56
56
 
57
- Install from source (no PyPI release is assumed):
57
+ Install the CLI from [PyPI](https://pypi.org/project/gipper/):
58
+
59
+ ```sh
60
+ uv tool install gipper
61
+ gipper --help
62
+ ```
63
+
64
+ Alternatively, use `python -m pip install gipper` in a virtual environment.
65
+ To get the example jobs or install from source:
58
66
 
59
67
  ```sh
60
68
  git clone https://github.com/lavallee/gipper.git
@@ -174,6 +182,18 @@ compute_s = 900
174
182
  setup_download_gb = 5 # weights and wheels the job fetches itself
175
183
  ```
176
184
 
185
+ ## After a provider incident
186
+
187
+ Every upload is recorded before it leaves: a manifest of what was sent, sensitive-looking
188
+ files flagged, and provider keys refused outright. When a provider reports a problem, one
189
+ command gives you what to rotate, what to review, and what the trail can't vouch for:
190
+
191
+ ```
192
+ gipper custody report --provider runpod --since 2026-10-01 --check-providers
193
+ ```
194
+
195
+ See [docs/custody.md](docs/custody.md), including how to record what went around gipper.
196
+
177
197
  ## How gipper chooses
178
198
 
179
199
  Every offer is judged on the whole run, not its hourly price
@@ -15,7 +15,7 @@ fees, and host failures. An append-only local ledger records runs and costs;
15
15
  future estimates learn from **your own** workloads and provider history. A fresh
16
16
  installation starts with stated defaults, not someone else's rental history.
17
17
 
18
- **Status: 0.3.0, alpha.** Linux client, CLI and Python API, no daemon. Runpod and
18
+ **Status: alpha.** Linux client, CLI and Python API, no daemon. Runpod and
19
19
  Vast have live-run validation; Shadeform is experimental and has not had a live
20
20
  smoke test. See the [roadmap](ROADMAP.md) for planned features and
21
21
  [SECURITY.md](SECURITY.md) for trust and billing limits. Licensed under [MIT](LICENSE).
@@ -27,7 +27,15 @@ gzip support. `ping` is optional for interactive placement. The Python runtime
27
27
  has no third-party dependencies. Remote machines need a Linux shell, `tar`,
28
28
  `timeout`, `curl`, and NVIDIA tools; the bootstrap installs `uv` if needed.
29
29
 
30
- Install from source (no PyPI release is assumed):
30
+ Install the CLI from [PyPI](https://pypi.org/project/gipper/):
31
+
32
+ ```sh
33
+ uv tool install gipper
34
+ gipper --help
35
+ ```
36
+
37
+ Alternatively, use `python -m pip install gipper` in a virtual environment.
38
+ To get the example jobs or install from source:
31
39
 
32
40
  ```sh
33
41
  git clone https://github.com/lavallee/gipper.git
@@ -147,6 +155,18 @@ compute_s = 900
147
155
  setup_download_gb = 5 # weights and wheels the job fetches itself
148
156
  ```
149
157
 
158
+ ## After a provider incident
159
+
160
+ Every upload is recorded before it leaves: a manifest of what was sent, sensitive-looking
161
+ files flagged, and provider keys refused outright. When a provider reports a problem, one
162
+ command gives you what to rotate, what to review, and what the trail can't vouch for:
163
+
164
+ ```
165
+ gipper custody report --provider runpod --since 2026-10-01 --check-providers
166
+ ```
167
+
168
+ See [docs/custody.md](docs/custody.md), including how to record what went around gipper.
169
+
150
170
  ## How gipper chooses
151
171
 
152
172
  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-04"
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
- mode = "docs"
19
+ commit = true
21
20
  docs_dir = "docs"
22
- subpath = ""
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.
@@ -4,17 +4,27 @@ Gipper needs a Linux client, Python 3.12+, OpenSSH, and tar with gzip support.
4
4
  Provider accounts supply the GPUs; a missing API key disables that provider for
5
5
  rentals. Your application and dependencies remain part of your job.
6
6
 
7
- ## Install from source
7
+ ## Install
8
+
9
+ ```sh
10
+ uv tool install gipper
11
+ gipper --help
12
+ ```
13
+
14
+ Alternatively, use `python -m pip install gipper` in a virtual environment.
15
+ Install uv separately if using the uv commands. The Python runtime has no
16
+ third-party dependencies.
17
+
18
+ This walkthrough uses the example files from the repository:
8
19
 
9
20
  ```sh
10
21
  git clone https://github.com/lavallee/gipper.git
11
22
  cd gipper
12
- uv tool install .
13
- gipper --help
14
23
  ```
15
24
 
16
- Alternatively, use `pip install .` in a virtual environment. Install uv separately
17
- if using the uv commands. The Python runtime has no third-party dependencies.
25
+ For an installation from this source checkout, use `uv tool install .` instead
26
+ of the package-index command above. See [contributing](../CONTRIBUTING.md)
27
+ for development setup.
18
28
 
19
29
  ## Configure a provider
20
30
 
@@ -1,2 +1,2 @@
1
1
  """gipper: GPU jobs across cloud markets."""
2
- __version__ = "0.3.0"
2
+ __version__ = "0.4.1"
@@ -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