feat(orchestrator): update a single egress secret in place (0081)
tracker-policy-pr / check-pr (pull_request) Successful in 13s
prd-number-check / require-numbered-prds (pull_request) Successful in 22s
lint / lint (push) Failing after 1m1s
test / unit (pull_request) Successful in 54s
test / integration-docker (pull_request) Successful in 1m6s
test / coverage (pull_request) Successful in 16s

Add a single-secret counterpart to reprovision_from_secret's restore-all:
update ONE egress token for a running bottle without a relaunch, for
refreshing a short-lived host credential (e.g. the Codex access token)
whose launch-time snapshot has expired.

- registry_store.store_agent_secret: per-key upsert (delete+insert of the
  one row), the counterpart of store_agent_secrets' replace-all.
- OrchestratorCore.update_agent_secret: set the in-memory token AND upsert
  the re-encrypted row under the bottle's env_var_secret, leaving other
  tokens untouched.
- POST /bottles/<id>/secret (cli-only) + client.update_agent_secret.

Refs #510, #512

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 18:21:40 -04:00
parent bf6b32381d
commit eb8ac7f011
8 changed files with 193 additions and 0 deletions
+25
View File
@@ -200,6 +200,31 @@ def dispatch( # pylint: disable=too-many-return-statements,too-many-branches
return 200, {"reprovisioned": True}
return 404, {"error": "no stored secrets for this bottle"}
if (
method == "POST"
and route.startswith("/bottles/")
and route.endswith("/secret")
):
# Update ONE egress token for a running bottle in place — the
# single-secret form of reprovision, for refreshing a short-lived host
# credential (e.g. the Codex access token) without a relaunch. cli-only
# (not in _GATEWAY_ROUTES): a bottle must never set its own tokens.
bottle_id = route[len("/bottles/") : -len("/secret")]
try:
data = _parse_json_object(body)
except ValueError as e:
return 400, {"error": f"invalid JSON: {e}"}
name = data.get("name")
value = data.get("value")
env_var_secret = data.get("env_var_secret")
if not all(isinstance(v, str) and v for v in (name, value, env_var_secret)):
return 400, {
"error": "name, value, and env_var_secret (non-empty strings) are required"
}
if orch.update_agent_secret(bottle_id, name, value, env_var_secret):
return 200, {"updated": True}
return 404, {"error": "no such bottle"}
if method == "DELETE" and route.startswith("/bottles/"):
bottle_id = route[len("/bottles/"):]
if orch.teardown_bottle(bottle_id):