Skip to content

Refresh on expiry, and push to providers that hold the value - #39

Merged
czpython merged 1 commit into
mainfrom
dru-461-refresh-on-expiry
Sep 7, 2026
Merged

Refresh on expiry, and push to providers that hold the value#39
czpython merged 1 commit into
mainfrom
dru-461-refresh-on-expiry

Conversation

@czpython

@czpython czpython commented Sep 6, 2026

Copy link
Copy Markdown
Owner

What changes

The exchange refreshes an issuer-backed secret on expiry for a provider that holds the value, and pushes it.

  • The exchange runs a timer. Every five seconds it looks at the active hosts on providers that hold the value. For each issuer-backed entry it fetches a fresh value when less than a minute of the pushed one remains, and hands it to the seam's push_secret. The first push happens at first sight of the host, since the boot value came from the API process.
  • RefreshableSecret keeps its margin and its retry wait. A push that fails is logged with the host and the service, and waits like a fetch that fails. The same value goes again after the wait, without a new fetch.
  • On restart the timer starts from the database. Nothing is written back.
  • Proxy providers are not visited. Their value lives in the exchange and refreshes on request as before.
  • On docker-sbx a push is a rewritten value file. sbx runs the cat command at each use: for a custom secret by default, and for a service secret when registered with --refresh on-demand, which sbx secret set now passes. Without that flag sbx caches the output for 55 minutes.
  • The exchange process logs at INFO, so its log shows each push. On docker-sbx the exchange service needs the api's environment and its workspace root mount, and the deploy doc says so.

Where this differs from the ticket

  • The exchange calls push_secret, not put_secret again. put_secret takes the placeholder, and the host row keeps only a fingerprint of it, by design. A provider that holds the value knows its own registration, so the push takes the box, the service name, and the value.
  • The push is a file rewrite, not a new registration. Probes on the bed showed that sbx reads the file at each use, and that a service secret registered after the sandbox started never applies, so a new registration would not help.

Names

New names, open to change: push_secret on the seam, Secrets.push, RefreshableSecret.pushed, RefreshableSecret.unpushed, RefreshableSecret.retry_later, push_on_expiry, push_held, TICK, SbxInjection.write_value.

Gates

uv run ruff check, uv run ruff format --check, uv run pyright, and uv run pytest are green.

Acceptance

docker-sbx on the KVM bed, with an anthropic secret from an issuer stub at a 70 second lifetime and a static github secret:

  • The API process fetched the boot value once.
  • claude -p answered ok four times across three lifetimes, over 224 seconds, with no manual step.
  • The exchange log shows 23 pushes for 23 refreshes. No push failed and the timer never died.
  • The value file was rewritten during the run. The token is nowhere in the exchange log and nowhere in the box.
  • After DELETE /hosts, the sandbox scope listed no secret.

11 of 11 checks passed on this commit.

Review

The adversarial review reported four findings. All four are applied, in their smallest form:

  • One host's error stopped the pass for the hosts after it. Each host now has its own error boundary, and the pass runs the hosts side by side, so one slow issuer delays no other host. That covers the second finding too, which asked for concurrency.
  • The file rewrite truncated the live value for a moment, and a failed write lost the old value. The value is now staged in a 0600 file in the same directory and replaced whole.
  • A push that raced a teardown recreated the sandbox's directory with a plaintext value that nothing would clean. Only put_secret creates the directory now. A push into a removed one fails as a provider error, which the timer logs and drops once the row is gone. The review suggested the host row lock for this. It is not needed once a push cannot recreate the directory.

@czpython
czpython force-pushed the dru-461-refresh-on-expiry branch from 79000b9 to e787d85 Compare September 6, 2026 16:15
@czpython
czpython force-pushed the dru-460-docker-sbx-secrets branch from 082831d to c51e946 Compare September 7, 2026 05:27
Base automatically changed from dru-460-docker-sbx-secrets to main September 7, 2026 05:29
@czpython
czpython force-pushed the dru-461-refresh-on-expiry branch 3 times, most recently from 3bf8602 to 0cd8887 Compare September 7, 2026 05:49
The exchange refreshes an issuer-backed secret when a request asks for it.
On docker-sbx no request ever asks, since sbx does the swap, so the boot
value expired after an hour and the box started to fail.

The exchange now runs a timer. Every five seconds it looks at the active
hosts on providers that hold the value. For each issuer-backed entry it
fetches a fresh value when less than a minute of the pushed one remains,
and hands it to the seam's push_secret. A push that fails is logged with
the host and the service, and waits like a fetch that fails. The same
value goes again after the wait. On restart the timer starts from the
database, and nothing is written back. Proxy providers are not visited.

push_secret takes the box, the service name, and the value. It takes no
placeholder, since the host row keeps only a fingerprint of it. On
docker-sbx a push is a rewritten value file: sbx runs the cat command at
each use, for a service secret once registered with --refresh on-demand.
Without that flag sbx caches the output for 55 minutes.

Each host gets its own error boundary in a pass, and the hosts go side
by side, so one host in trouble or one slow issuer costs no other host
its value. A value file is replaced whole through a staged 0600 file,
so sbx never reads a half-written one. Only put_secret makes the
sandbox's directory, so a push that races a teardown brings nothing
back.

The exchange process logs at INFO, so its log shows each push.
@czpython
czpython force-pushed the dru-461-refresh-on-expiry branch from 0cd8887 to 10404df Compare September 7, 2026 05:52
@czpython
czpython merged commit e6d0530 into main Sep 7, 2026
6 checks passed
@czpython
czpython deleted the dru-461-refresh-on-expiry branch September 7, 2026 05:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant