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.
Files changed (79) hide show
  1. {gipper-0.3.0 → gipper-0.4.0}/AGENTS.md +1 -0
  2. {gipper-0.3.0 → gipper-0.4.0}/CHANGELOG.md +28 -0
  3. {gipper-0.3.0 → gipper-0.4.0}/PKG-INFO +13 -1
  4. {gipper-0.3.0 → gipper-0.4.0}/README.md +12 -0
  5. {gipper-0.3.0 → gipper-0.4.0}/ROADMAP.md +3 -0
  6. {gipper-0.3.0 → gipper-0.4.0}/SECURITY.md +4 -0
  7. {gipper-0.3.0 → gipper-0.4.0}/artifact.toml +5 -6
  8. {gipper-0.3.0 → gipper-0.4.0}/docs/clients.md +3 -1
  9. gipper-0.4.0/docs/custody.md +101 -0
  10. {gipper-0.3.0 → gipper-0.4.0}/gipper/__init__.py +1 -1
  11. {gipper-0.3.0 → gipper-0.4.0}/gipper/cli.py +82 -0
  12. {gipper-0.3.0 → gipper-0.4.0}/gipper/config.py +2 -0
  13. gipper-0.4.0/gipper/custody.py +536 -0
  14. {gipper-0.3.0 → gipper-0.4.0}/gipper/job.py +10 -1
  15. {gipper-0.3.0 → gipper-0.4.0}/gipper/run.py +4 -2
  16. {gipper-0.3.0 → gipper-0.4.0}/gipper/ssh.py +36 -9
  17. {gipper-0.3.0 → gipper-0.4.0}/gipper.egg-info/PKG-INFO +13 -1
  18. {gipper-0.3.0 → gipper-0.4.0}/gipper.egg-info/SOURCES.txt +3 -0
  19. {gipper-0.3.0 → gipper-0.4.0}/pyproject.toml +1 -1
  20. {gipper-0.3.0 → gipper-0.4.0}/scripts/build_site.py +1 -0
  21. {gipper-0.3.0 → gipper-0.4.0}/site/index.html +1 -1
  22. gipper-0.4.0/tests/test_custody.py +238 -0
  23. {gipper-0.3.0 → gipper-0.4.0}/uv.lock +1 -1
  24. {gipper-0.3.0 → gipper-0.4.0}/.env.example +0 -0
  25. {gipper-0.3.0 → gipper-0.4.0}/CONTRIBUTING.md +0 -0
  26. {gipper-0.3.0 → gipper-0.4.0}/LICENSE +0 -0
  27. {gipper-0.3.0 → gipper-0.4.0}/MANIFEST.in +0 -0
  28. {gipper-0.3.0 → gipper-0.4.0}/RELEASING.md +0 -0
  29. {gipper-0.3.0 → gipper-0.4.0}/deploy/gipper-reap.service +0 -0
  30. {gipper-0.3.0 → gipper-0.4.0}/deploy/gipper-reap.timer +0 -0
  31. {gipper-0.3.0 → gipper-0.4.0}/docs/lessons.md +0 -0
  32. {gipper-0.3.0 → gipper-0.4.0}/docs/placement.md +0 -0
  33. {gipper-0.3.0 → gipper-0.4.0}/docs/quickstart.md +0 -0
  34. {gipper-0.3.0 → gipper-0.4.0}/docs/sources/vast/README.md +0 -0
  35. {gipper-0.3.0 → gipper-0.4.0}/examples/smoke/hello.txt +0 -0
  36. {gipper-0.3.0 → gipper-0.4.0}/examples/smoke.toml +0 -0
  37. {gipper-0.3.0 → gipper-0.4.0}/examples/udp-echo.toml +0 -0
  38. {gipper-0.3.0 → gipper-0.4.0}/gipper/billing.py +0 -0
  39. {gipper-0.3.0 → gipper-0.4.0}/gipper/bootstrap.sh +0 -0
  40. {gipper-0.3.0 → gipper-0.4.0}/gipper/costs.py +0 -0
  41. {gipper-0.3.0 → gipper-0.4.0}/gipper/cuda_probe.py +0 -0
  42. {gipper-0.3.0 → gipper-0.4.0}/gipper/history.py +0 -0
  43. {gipper-0.3.0 → gipper-0.4.0}/gipper/http.py +0 -0
  44. {gipper-0.3.0 → gipper-0.4.0}/gipper/latency.py +0 -0
  45. {gipper-0.3.0 → gipper-0.4.0}/gipper/ledger.py +0 -0
  46. {gipper-0.3.0 → gipper-0.4.0}/gipper/lifecycle.py +0 -0
  47. {gipper-0.3.0 → gipper-0.4.0}/gipper/market.py +0 -0
  48. {gipper-0.3.0 → gipper-0.4.0}/gipper/network_probe.py +0 -0
  49. {gipper-0.3.0 → gipper-0.4.0}/gipper/placement.py +0 -0
  50. {gipper-0.3.0 → gipper-0.4.0}/gipper/providers/__init__.py +0 -0
  51. {gipper-0.3.0 → gipper-0.4.0}/gipper/providers/runpod.py +0 -0
  52. {gipper-0.3.0 → gipper-0.4.0}/gipper/providers/shadeform.py +0 -0
  53. {gipper-0.3.0 → gipper-0.4.0}/gipper/providers/vast.py +0 -0
  54. {gipper-0.3.0 → gipper-0.4.0}/gipper/reap.py +0 -0
  55. {gipper-0.3.0 → gipper-0.4.0}/gipper/runpod/__init__.py +0 -0
  56. {gipper-0.3.0 → gipper-0.4.0}/gipper/runpod/api.py +0 -0
  57. {gipper-0.3.0 → gipper-0.4.0}/gipper/runpod/run.py +0 -0
  58. {gipper-0.3.0 → gipper-0.4.0}/gipper.egg-info/dependency_links.txt +0 -0
  59. {gipper-0.3.0 → gipper-0.4.0}/gipper.egg-info/entry_points.txt +0 -0
  60. {gipper-0.3.0 → gipper-0.4.0}/gipper.egg-info/requires.txt +0 -0
  61. {gipper-0.3.0 → gipper-0.4.0}/gipper.egg-info/top_level.txt +0 -0
  62. {gipper-0.3.0 → gipper-0.4.0}/scripts/check_dist.py +0 -0
  63. {gipper-0.3.0 → gipper-0.4.0}/setup.cfg +0 -0
  64. {gipper-0.3.0 → gipper-0.4.0}/site/favicon.svg +0 -0
  65. {gipper-0.3.0 → gipper-0.4.0}/site/style.css +0 -0
  66. {gipper-0.3.0 → gipper-0.4.0}/tests/conftest.py +0 -0
  67. {gipper-0.3.0 → gipper-0.4.0}/tests/test_billing.py +0 -0
  68. {gipper-0.3.0 → gipper-0.4.0}/tests/test_compat.py +0 -0
  69. {gipper-0.3.0 → gipper-0.4.0}/tests/test_config.py +0 -0
  70. {gipper-0.3.0 → gipper-0.4.0}/tests/test_costs.py +0 -0
  71. {gipper-0.3.0 → gipper-0.4.0}/tests/test_cuda_probe.py +0 -0
  72. {gipper-0.3.0 → gipper-0.4.0}/tests/test_history.py +0 -0
  73. {gipper-0.3.0 → gipper-0.4.0}/tests/test_job.py +0 -0
  74. {gipper-0.3.0 → gipper-0.4.0}/tests/test_latency.py +0 -0
  75. {gipper-0.3.0 → gipper-0.4.0}/tests/test_lifecycle.py +0 -0
  76. {gipper-0.3.0 → gipper-0.4.0}/tests/test_market.py +0 -0
  77. {gipper-0.3.0 → gipper-0.4.0}/tests/test_network_probe.py +0 -0
  78. {gipper-0.3.0 → gipper-0.4.0}/tests/test_providers.py +0 -0
  79. {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.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-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.
@@ -1,2 +1,2 @@
1
1
  """gipper: GPU jobs across cloud markets."""
2
- __version__ = "0.3.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