84 actor_type: str =
"human_operator",
85 grace_seconds: int |
None =
None,
86 secrets_path: str | Path |
None =
None,
87 trigger: str =
"manual",
89 """Start a two-phase credential rotation for ``slot``."""
91 backend = registry.backend
92 connect_func = getattr(backend,
"connect",
None)
93 transaction_func = getattr(backend,
"transaction",
None)
94 if connect_func
is None or transaction_func
is None:
95 raise TypeError(
"rotation requires a SQLite-compatible registry backend")
96 now = registry.clock.now_wall()
98 expires_at = wall_plus_seconds(now, effective_grace)
99 raw_secret = generate_secret()
100 new_key_id = str(uuid.uuid4())
101 new_block = block_from_secret(
103 phase=
"rotating_new",
104 hmac_key_id=new_key_id,
105 raw_secret=raw_secret,
109 with transaction_func(
"immediate")
as conn:
110 in_flight = conn.execute(
112 "SELECT 1 FROM agent_credentials WHERE slot = ? "
113 "AND rotation_phase IN ('rotating_old', 'rotating_new') "
114 "AND revoked_at IS NULL",
117 if in_flight
is not None:
119 active = conn.execute(
121 "SELECT hmac_key_id, secret_fingerprint FROM agent_credentials "
122 "WHERE slot = ? AND rotation_phase = 'active' AND revoked_at IS NULL",
126 raise ValueError(f
"no active credential for slot {slot!r}")
127 old_key_id = str(active[
"hmac_key_id"])
128 old_fingerprint = str(active[
"secret_fingerprint"])
131 "UPDATE agent_credentials SET rotation_phase = 'rotating_old', "
132 "expires_at = ? WHERE hmac_key_id = ?",
133 (expires_at, old_key_id),
137 "INSERT INTO agent_credentials("
138 "hmac_key_id, slot, secret_fingerprint, created_at, expires_at, "
140 ") VALUES (?, ?, ?, ?, NULL, 'rotating_new')",
141 (new_key_id, slot, new_block.secret_fingerprint, now),
147 secrets_path=secrets_path,
148 expires_at=expires_at,
150 operation = registry.execute_domain_command(
156 "old_key_id": old_key_id,
157 "new_key_id": new_key_id,
158 "old_key_fingerprint": old_fingerprint,
159 "new_key_fingerprint": new_block.secret_fingerprint,
160 "effective_grace_seconds": effective_grace,
161 "grace_source":
"cli_override"
162 if grace_seconds
is not None
163 else "global_default",
164 "expires_at": expires_at,
167 actor_type=actor_type,
177 actor_type: str =
"human_operator",
178 secrets_path: str | Path |
None =
None,
179 trigger: str =
"manual_finalize",
181 """Finalize a pending rotation for ``slot``."""
183 backend = registry.backend
184 transaction_func = getattr(backend,
"transaction",
None)
185 if transaction_func
is None:
186 raise TypeError(
"rotation requires a SQLite-compatible registry backend")
187 now = registry.clock.now_wall()
188 with transaction_func(
"immediate")
as conn:
191 "SELECT hmac_key_id, created_at FROM agent_credentials "
192 "WHERE slot = ? AND rotation_phase = 'rotating_old' "
193 "AND revoked_at IS NULL",
198 "SELECT hmac_key_id FROM agent_credentials "
199 "WHERE slot = ? AND rotation_phase = 'rotating_new' "
200 "AND revoked_at IS NULL",
203 if old
is None or new
is None:
204 raise ValueError(f
"no rotation in flight for slot {slot!r}")
205 old_key_id = str(old[
"hmac_key_id"])
206 new_key_id = str(new[
"hmac_key_id"])
209 "UPDATE agent_credentials SET revoked_at = ?, revoked_by = ? "
210 "WHERE hmac_key_id = ?",
211 (now, actor_slot, old_key_id),
215 "UPDATE agent_credentials SET rotation_phase = 'active' "
216 "WHERE hmac_key_id = ?",
221 operation = registry.execute_domain_command(
222 "auth.rotate_finalize",
227 "old_key_id": old_key_id,
228 "new_key_id": new_key_id,
230 "started_at": str(old[
"created_at"]),
232 "parent_operation_id": start_row,
234 actor_type=actor_type,
235 parent_operation_id=start_row,
265 new_block: SecretBlock,
266 secrets_path: str | Path |
None,
270 blocks = load_secrets(path)
271 slot_blocks = dict(blocks.get(slot, {}))
272 active = slot_blocks.get(
"active")
273 if active
is not None:
276 phase=
"rotating_old",
277 hmac_key_id=active.hmac_key_id,
278 secret_b64=active.secret_b64,
279 secret_fingerprint=active.secret_fingerprint,
280 created_at=active.created_at,
281 expires_at=expires_at,
283 slot_blocks.pop(
"active",
None)
284 slot_blocks[
"rotating_new"] = new_block
285 blocks[slot] = slot_blocks
286 write_secrets(blocks, path)
293 secrets_path: str | Path |
None,
296 blocks = load_secrets(path)
297 slot_blocks = dict(blocks.get(slot, {}))
298 new = slot_blocks.get(
"rotating_new")
304 hmac_key_id=new.hmac_key_id,
305 secret_b64=new.secret_b64,
306 secret_fingerprint=new.secret_fingerprint,
307 created_at=new.created_at,
310 slot_blocks.pop(
"rotating_new",
None)
311 slot_blocks.pop(
"rotating_old",
None)
312 blocks[slot] = slot_blocks
313 write_secrets(blocks, path)
335 if override
is not None:
336 return Path(override)
337 backend_path = getattr(backend,
"path",
None)
338 if isinstance(backend_path, Path)
and backend_path.name !=
":memory:":
340 backend_path.parent.name ==
"orchestration"
341 and backend_path.parent.parent.name ==
".option_c"
343 return backend_path.parent.parent /
"agent_secrets.json"
344 return backend_path.parent /
"agent_secrets.json"
345 return Path(
".option_c") /
"agent_secrets.json"
RotationResult finalize_rotation(Registry registry, *, str slot, str actor_slot, str actor_type="human_operator", str|Path|None secrets_path=None, str trigger="manual_finalize")
RotationResult rotate_secret(Registry registry, *, str slot, str actor_slot, str actor_type="human_operator", int|None grace_seconds=None, str|Path|None secrets_path=None, str trigger="manual")