feat(orchestrator): update a single egress secret in place (0081)
prd-number-check / require-numbered-prds (pull_request) Successful in 8s
tracker-policy-pr / check-pr (pull_request) Successful in 22s
test / unit (pull_request) Successful in 58s
lint / lint (push) Successful in 1m7s
test / integration-docker (pull_request) Successful in 1m12s
test / coverage (pull_request) Successful in 46s

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 f9d0e15f13
8 changed files with 197 additions and 0 deletions
+29
View File
@@ -200,6 +200,35 @@ 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 isinstance(name, str) or not name
or not isinstance(value, str) or not value
or not isinstance(env_var_secret, str) or not 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):