3232import secrets
3333import subprocess
3434import sys
35+ from contextlib import contextmanager
3536from datetime import datetime , timezone
3637from pathlib import Path
3738from typing import Any
@@ -174,12 +175,24 @@ def _read_fallback_secret_file() -> str | None:
174175 return None
175176
176177
177- def _write_fallback_secret_file (secret : str ) -> None :
178- FALLBACK_SECRET_PATH .parent .mkdir (parents = True , exist_ok = True )
179- fd = os .open (str (FALLBACK_SECRET_PATH ), os .O_WRONLY | os .O_CREAT | os .O_TRUNC , 0o600 )
178+ def _validate_repo_slug (repo : str ) -> None :
179+ if "|" in repo or not repo .strip ():
180+ raise GrantError ("grant repo must not contain pipe characters" )
181+
182+
183+ def _write_private_file (path : Path , content : str ) -> None :
184+ path .parent .mkdir (parents = True , exist_ok = True )
185+ path .unlink (missing_ok = True )
186+ flags = os .O_WRONLY | os .O_CREAT | os .O_EXCL
187+ if hasattr (os , "O_NOFOLLOW" ):
188+ flags |= os .O_NOFOLLOW
189+ fd = os .open (str (path ), flags , 0o600 )
180190 with os .fdopen (fd , "w" , encoding = "utf-8" ) as handle :
181- handle .write (secret )
182- handle .write ("\n " )
191+ handle .write (content )
192+
193+
194+ def _write_fallback_secret_file (secret : str ) -> None :
195+ _write_private_file (FALLBACK_SECRET_PATH , secret + "\n " )
183196
184197
185198def resolve_hmac_secret (allow_generate : bool = False ) -> bytes :
@@ -251,24 +264,29 @@ def _grant_ttl_ok(issued_raw: str) -> bool:
251264 return 0 <= age <= GRANT_TTL_SECONDS
252265
253266
254- def _lock_nonce_state () -> tuple [Any , dict [str , Any ]]:
267+ @contextmanager
268+ def _locked_nonce_state ():
255269 NONCE_STATE_PATH .parent .mkdir (parents = True , exist_ok = True )
256270 handle = open (NONCE_STATE_PATH , "a+" , encoding = "utf-8" )
257- fcntl .flock (handle .fileno (), fcntl .LOCK_EX )
258- handle .seek (0 )
259- raw = handle .read ()
260- if raw .strip ():
261- try :
262- state = json .loads (raw )
263- except json .JSONDecodeError :
271+ try :
272+ fcntl .flock (handle .fileno (), fcntl .LOCK_EX )
273+ handle .seek (0 )
274+ raw = handle .read ()
275+ if raw .strip ():
276+ try :
277+ state = json .loads (raw )
278+ except json .JSONDecodeError :
279+ state = {"nonces" : {}}
280+ else :
264281 state = {"nonces" : {}}
265- else :
266- state = {"nonces" : {}}
267- if "nonces" not in state or not isinstance (state ["nonces" ], dict ):
268- state ["nonces" ] = {}
269- if "reservations" not in state or not isinstance (state .get ("reservations" ), dict ):
270- state ["reservations" ] = {}
271- return handle , state
282+ if "nonces" not in state or not isinstance (state ["nonces" ], dict ):
283+ state ["nonces" ] = {}
284+ if "reservations" not in state or not isinstance (state .get ("reservations" ), dict ):
285+ state ["reservations" ] = {}
286+ yield handle , state
287+ finally :
288+ fcntl .flock (handle .fileno (), fcntl .LOCK_UN )
289+ handle .close ()
272290
273291
274292def _write_nonce_state (handle : Any , state : dict [str , Any ]) -> None :
@@ -318,18 +336,14 @@ def _reservation_blocks_verify(
318336 pr_number : str ,
319337 content_digest : str ,
320338) -> tuple [bool , str ]:
321- handle , state = _lock_nonce_state ()
322- try :
339+ with _locked_nonce_state () as (_handle , state ):
323340 _prune_nonce_state (state )
324341 entry = state .get ("reservations" , {}).get (nonce )
325342 if not entry :
326343 return True , ""
327344 if _reservation_matches (entry , repo , pr_number , content_digest ):
328345 return True , ""
329346 return False , "grant nonce reserved for a different append operation"
330- finally :
331- fcntl .flock (handle .fileno (), fcntl .LOCK_UN )
332- handle .close ()
333347
334348
335349def reserve_nonce_atomic (
@@ -339,8 +353,7 @@ def reserve_nonce_atomic(
339353 content_digest : str ,
340354) -> tuple [bool , str ]:
341355 """Reserve nonce before remote mutation. Idempotent for the same binding."""
342- handle , state = _lock_nonce_state ()
343- try :
356+ with _locked_nonce_state () as (handle , state ):
344357 _prune_nonce_state (state )
345358 if nonce in state ["nonces" ]:
346359 return False , "grant nonce already consumed (replay blocked)"
@@ -359,15 +372,11 @@ def reserve_nonce_atomic(
359372 }
360373 _write_nonce_state (handle , state )
361374 return True , ""
362- finally :
363- fcntl .flock (handle .fileno (), fcntl .LOCK_UN )
364- handle .close ()
365375
366376
367377def mark_remote_applied_atomic (nonce : str ) -> tuple [bool , str ]:
368378 """Mark remote PR body mutation successful; required before consume."""
369- handle , state = _lock_nonce_state ()
370- try :
379+ with _locked_nonce_state () as (handle , state ):
371380 _prune_nonce_state (state )
372381 reservations = state ["reservations" ]
373382 entry = reservations .get (nonce )
@@ -376,39 +385,27 @@ def mark_remote_applied_atomic(nonce: str) -> tuple[bool, str]:
376385 entry ["remote_applied" ] = True
377386 _write_nonce_state (handle , state )
378387 return True , ""
379- finally :
380- fcntl .flock (handle .fileno (), fcntl .LOCK_UN )
381- handle .close ()
382388
383389
384390def release_nonce_reservation_atomic (nonce : str ) -> None :
385391 """Drop an in-flight reservation when remote mutation did not succeed."""
386- handle , state = _lock_nonce_state ()
387- try :
392+ with _locked_nonce_state () as (handle , state ):
388393 _prune_nonce_state (state )
389394 entry = state .get ("reservations" , {}).get (nonce )
390395 if entry and not entry .get ("remote_applied" ):
391396 del state ["reservations" ][nonce ]
392397 _write_nonce_state (handle , state )
393- finally :
394- fcntl .flock (handle .fileno (), fcntl .LOCK_UN )
395- handle .close ()
396398
397399
398400def nonce_is_consumed (nonce : str ) -> bool :
399- handle , state = _lock_nonce_state ()
400- try :
401+ with _locked_nonce_state () as (_handle , state ):
401402 _prune_nonce_state (state )
402403 return nonce in state ["nonces" ]
403- finally :
404- fcntl .flock (handle .fileno (), fcntl .LOCK_UN )
405- handle .close ()
406404
407405
408406def consume_nonce_atomic (nonce : str , require_remote_applied : bool = True ) -> bool :
409407 """Mark nonce consumed once after remote success. Returns False if invalid."""
410- handle , state = _lock_nonce_state ()
411- try :
408+ with _locked_nonce_state () as (handle , state ):
412409 _prune_nonce_state (state )
413410 if nonce in state ["nonces" ]:
414411 return False
@@ -421,9 +418,6 @@ def consume_nonce_atomic(nonce: str, require_remote_applied: bool = True) -> boo
421418 del state ["reservations" ][nonce ]
422419 _write_nonce_state (handle , state )
423420 return True
424- finally :
425- fcntl .flock (handle .fileno (), fcntl .LOCK_UN )
426- handle .close ()
427421
428422
429423def verify_grant_fields (
@@ -434,8 +428,13 @@ def verify_grant_fields(
434428 action : str = DEFAULT_ACTION ,
435429 check_nonce_consumed : bool = True ,
436430) -> tuple [bool , str ]:
431+ try :
432+ _validate_repo_slug (repo )
433+ except GrantError as exc :
434+ return False , str (exc )
435+
437436 if fields .get ("marker" ) != GRANT_MARKER :
438- if " operator-grant-v1" in str ( fields ) :
437+ if fields . get ( "marker" ) == " operator-grant-v1" :
439438 return False , (
440439 "operator-grant-v1 is no longer accepted; re-run grant with matching "
441440 "--file or --message in an operator terminal"
@@ -602,13 +601,9 @@ def release_grant_for_append(
602601 if not nonce :
603602 return False , "grant missing grant-nonce"
604603 entry = None
605- handle , state = _lock_nonce_state ()
606- try :
604+ with _locked_nonce_state () as (_handle , state ):
607605 _prune_nonce_state (state )
608606 entry = state .get ("reservations" , {}).get (nonce )
609- finally :
610- fcntl .flock (handle .fileno (), fcntl .LOCK_UN )
611- handle .close ()
612607 if entry and not _reservation_matches (entry , repo , str (pr_number ), digest ):
613608 return False , "grant nonce reserved for a different append operation"
614609 release_nonce_reservation_atomic (nonce )
@@ -685,6 +680,7 @@ def mint_grant(
685680 message : str | None ,
686681 cwd : Path | None = None ,
687682) -> Path :
683+ _validate_repo_slug (repo )
688684 digest = content_digest_for_append (file_path , message , cwd = cwd )
689685 secret = resolve_hmac_secret (allow_generate = True )
690686 issued_at = _now_utc ().isoformat ().replace ("+00:00" , "Z" )
@@ -710,9 +706,7 @@ def mint_grant(
710706 f"grant-nonce={ nonce } \n "
711707 f"token={ token } \n "
712708 )
713- fd = os .open (str (ACK_PATH ), os .O_WRONLY | os .O_CREAT | os .O_TRUNC , 0o600 )
714- with os .fdopen (fd , "w" , encoding = "utf-8" ) as handle :
715- handle .write (body )
709+ _write_private_file (ACK_PATH , body )
716710 return ACK_PATH
717711
718712
@@ -908,8 +902,11 @@ def add_append_args(p: argparse.ArgumentParser) -> None:
908902 reconcile_p .set_defaults (func = _cmd_reconcile )
909903
910904 args = parser .parse_args (argv )
911- if args .command != "reconcile" and not args .file and not args .message :
912- parser .error ("provide --file or --message" )
905+ if args .command != "reconcile" :
906+ if not args .file and not args .message :
907+ parser .error ("provide --file or --message" )
908+ if args .file and args .message :
909+ parser .error ("provide --file or --message, not both" )
913910 return args .func (args )
914911
915912
0 commit comments