156 lines
9.0 KiB
Python
156 lines
9.0 KiB
Python
"""CLI routing for per-conversation workers; legacy recovery remains explicit."""
|
|
from .issue_queue import (Queue, Delivery, GitLease, STOP, DIRECTORY as LEGACY_DIRECTORY,
|
|
configuration, main_project, git, validate_delivery_storage)
|
|
from .issue_workers import WorkerQueue, DIRECTORY
|
|
from .issue_claims import Claims
|
|
from .issue_tracker import Tracker
|
|
from .permissions import Permissions
|
|
from .transaction import InstallError, safe_path
|
|
|
|
|
|
def validate_storage(root, worker):
|
|
for directory in (DIRECTORY, LEGACY_DIRECTORY):
|
|
if git(root,'ls-files','--',directory): raise InstallError('worker/queue state must not be Git-tracked')
|
|
paths=[STOP]
|
|
if worker.owner: paths += [worker._path(name) for name in ('state.json','stop','write.lock')]
|
|
for path in paths:
|
|
safe_path(root,path)
|
|
git(root,'check-ignore','--quiet','--',path)
|
|
|
|
|
|
def validate_checkpoint_storage(root, repository, branch, record, stage):
|
|
if record and record.get('commit') and (stage == 'waiting_user' or
|
|
(record.get('stage') in ('ready_to_merge', 'merging') and
|
|
stage not in ('ready_to_merge', 'merging'))):
|
|
# Invalidation writes the accepted record commit, not the requested new commit.
|
|
validate_delivery_storage(root, Delivery(root, repository, branch),
|
|
record['number'], record['commit'])
|
|
|
|
|
|
|
|
CONTROL_PERMISSIONS = ('manage_claims', 'update_issue_labels')
|
|
|
|
|
|
def scope_issue(state, requested=None):
|
|
current = state.get('current')
|
|
scope = state.get('issue_scope')
|
|
if current and scope is not None and current['number'] != scope:
|
|
raise InstallError('current issue is outside worker scope')
|
|
if requested is not None and scope is not None and requested != scope:
|
|
raise InstallError('requested issue is outside worker scope')
|
|
return current['number'] if current else (scope if scope is not None else requested)
|
|
|
|
|
|
def bind_authorization(worker):
|
|
def check(state):
|
|
def before_write():
|
|
# Local checks only: never recurse into claims.ensure from a transport hook.
|
|
config, _, repository, branch = configuration(worker.root)
|
|
if repository != state['repository'] or branch != state['integration_branch']:
|
|
raise InstallError('permission repository or integration branch changed')
|
|
return Permissions(worker.root, repository, branch, worker.owner).require(
|
|
config.get('permissions', {}), scope_issue(state), CONTROL_PERMISSIONS)
|
|
effective = before_write()
|
|
worker.tracker.before_write = before_write
|
|
worker.claims.before_write = before_write
|
|
return effective
|
|
worker.authorization = check
|
|
|
|
|
|
def permission_dispatch(root, args, config, repository, branch):
|
|
permissions = Permissions(root, repository, branch, args.owner)
|
|
action = args.action.split('-', 1)[1]
|
|
if action == 'status': return dict(record=permissions.status(), project_permissions=config.get('permissions', {}))
|
|
if action == 'revoke': return permissions.revoke(args.issue, args.evidence, apply=args.apply)
|
|
if action == 'grant':
|
|
allow = args.allow.split(',') if args.allow else []
|
|
deny = args.deny.split(',') if args.deny else []
|
|
return permissions.grant(args.issue, allow, deny, args.evidence, apply=args.apply)
|
|
capabilities = args.capability.split(',')
|
|
effective = permissions.require(config.get('permissions', {}), args.issue, capabilities)
|
|
return dict(status='allowed', owner=args.owner, issue=args.issue, capabilities=capabilities,
|
|
permissions=effective)
|
|
|
|
|
|
def dispatch(root,args):
|
|
root=main_project(root)
|
|
worker=WorkerQueue(root,owner=args.owner)
|
|
action=args.action.split('-',1)[1]
|
|
delivery_action=args.action.startswith('delivery-')
|
|
permission_action=args.action.startswith('permissions-')
|
|
if action=='status' and not delivery_action and not permission_action: return worker.status()
|
|
if action in ('stop','legacy-stop'):
|
|
validate_storage(root,worker)
|
|
return Queue(root).stop() if action=='legacy-stop' else worker.stop()
|
|
config,settings,repository,branch=configuration(root)
|
|
if permission_action: return permission_dispatch(root,args,config,repository,branch)
|
|
if delivery_action:
|
|
delivery=Delivery(root,repository,branch)
|
|
if action=='status':
|
|
if args.issue is not None: return delivery.status(args.issue)
|
|
safe_path(root,'.local/agent/delivery/write.lock')
|
|
numbers=sorted(int(p.stem) for p in (root/'.local/agent/delivery').glob('*.json') if p.stem.isdigit())
|
|
return dict(status='inventory',tasks=[delivery.status(n) for n in numbers])
|
|
validate_delivery_storage(root,delivery,args.issue,args.commit if action=='record' else None)
|
|
if action=='record': return delivery.record(args.issue,args.commit,args.stage,args.evidence)
|
|
old=Queue(root).status()
|
|
tasks=WorkerQueue(root).status()['workers']
|
|
if any(s.get('current',{}).get('number')==args.issue for s in [old]+tasks if s.get('current')):
|
|
raise InstallError('current issue must use queue-finish with its owner')
|
|
if delivery.status(args.issue)['record'] is None: raise InstallError('missing delivery record')
|
|
if action == 'transfer' and not args.apply:
|
|
return dict(status='preview', project=str(root), repository=repository, integration_branch=branch,
|
|
source_owner=args.owner, target_owner=args.target_owner,
|
|
effect='transfer the stopped source worker current issue to the target owner; retain task checkpoints',
|
|
needs=['explicit user transfer', 'source stopped and shared resources released',
|
|
'Git/API write access', 'manage_claims and update_issue_labels permissions'])
|
|
if action == 'start' and not args.apply:
|
|
return dict(status='preview',project=str(root),repository=repository,integration_branch=branch,
|
|
label='ai-ready',effect='per-issue remote claims, compatibility gate and stage labels; may trigger branch CI',
|
|
needs=['explicit user start','Git/API write access','manage_claims and update_issue_labels permissions'])
|
|
if not delivery_action:
|
|
state = worker.status()
|
|
if action not in ('start','transfer') and not state['enabled']: raise InstallError('queue is stopped')
|
|
requested = getattr(args, 'issue', None) if action in ('start','claim','retry') else None
|
|
issue = scope_issue(state, requested)
|
|
Permissions(root, repository, branch, args.owner).require(config.get('permissions', {}), issue, CONTROL_PERMISSIONS)
|
|
validate_storage(root,worker)
|
|
if action == 'checkpoint':
|
|
# Reject unsafe known local targets before any tracker or remote access.
|
|
validate_checkpoint_storage(root, repository, branch, worker.status().get('current'), args.stage)
|
|
if action == 'retry':
|
|
delivery = Delivery(root, repository, branch)
|
|
proof = delivery.status(args.issue)['record']
|
|
if proof:
|
|
# Retry clears acceptance on the existing proof commit, without changing revision.
|
|
validate_delivery_storage(root, delivery, args.issue, proof['commit'])
|
|
credentials=config.get('environment',{}).get('secrets')
|
|
if credentials in (None,'','.local/secrets/access.json','external-controlled-storage'): credentials=None
|
|
worker.tracker=Tracker(settings['kind'],repository,root,credentials_path=credentials)
|
|
worker.lease,worker.branch,worker.claims=GitLease(root,repository),branch,Claims(root,repository)
|
|
if delivery_action:
|
|
if worker.claims.get('issues/'+str(args.issue)):
|
|
raise InstallError('remote claimed issue must use owner queue-finish')
|
|
return delivery.check(args.issue,args.commit,worker.tracker,worker.lease)
|
|
bind_authorization(worker)
|
|
if action=='start': return worker.start(issue=getattr(args,'issue',None))
|
|
if action=='transfer': return worker.transfer(args.target_owner,args.evidence)
|
|
if action=='next': return worker.next()
|
|
if action=='claim': return worker.claim(args.issue)
|
|
if action=='guard': return worker.guard()
|
|
if action=='cleanup-guard': return worker.cleanup_guard()
|
|
if action=='checkpoint':
|
|
# The remote stage/commit can be newer when a prior label sync was interrupted.
|
|
record = worker._owned(worker.status())
|
|
validate_checkpoint_storage(root, repository, branch, record, args.stage)
|
|
return worker.checkpoint(args.stage,args.branch,args.worktree,args.commit)
|
|
if action=='resource-acquire': return worker.resource(args.resource,True)
|
|
if action=='resource-release': return worker.resource(args.resource,False)
|
|
if action=='block': return worker.block(args.reason)
|
|
if action=='retry': return worker.retry(args.issue)
|
|
if action=='finish':
|
|
current=worker.status()['current']
|
|
if current: validate_delivery_storage(root,Delivery(root,repository,branch),current['number'])
|
|
return worker.finish(args.commit,args.evidence)
|
|
raise InstallError('unknown worker action')
|