@topolo/cli 0.13.8 → 0.13.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -5
- package/agent-packs/README.md +98 -0
- package/agent-packs/coverage_changed.py +1 -1
- package/agent-packs/src/topolo_agent_pack/approvals.py +22 -0
- package/agent-packs/src/topolo_agent_pack/cli.py +37 -4
- package/agent-packs/src/topolo_agent_pack/execution.py +70 -2
- package/agent-packs/src/topolo_agent_pack/lanes.py +749 -0
- package/agent-packs/src/topolo_agent_pack/live.py +914 -16
- package/agent-packs/src/topolo_agent_pack/policy.py +6 -0
- package/agent-packs/src/topolo_agent_pack/task_models.py +200 -0
- package/agent-packs/src/topolo_agent_pack/topolo.py +42 -3
- package/dist/claude-pack-backend-RMSOPHVQ.js +2380 -0
- package/dist/index.js +270 -66
- package/package.json +5 -5
- package/dist/claude-pack-backend-3YJNKMYD.js +0 -1207
|
@@ -275,7 +275,7 @@ def request(lib,operation,params=None):
|
|
|
275
275
|
record=read_json(lib.root/'live-bridge.json',{})
|
|
276
276
|
if not record: raise PackError('This desktop profile has no live bridge. Open it through topolo-agent-pack first.')
|
|
277
277
|
with socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) as connection:
|
|
278
|
-
connection.settimeout(3 if operation=='status' else 60 if operation in {'permissions-configure','owned-roster-read','smoke-start'} else 20)
|
|
278
|
+
connection.settimeout(3 if operation=='status' else 240 if operation.startswith('lane-') else 60 if operation in {'permissions-configure','owned-roster-read','smoke-start'} else 20)
|
|
279
279
|
try: connection.connect(record['socket'])
|
|
280
280
|
except OSError as exc: raise PackError('Managed live bridge is unavailable: '+str(exc)) from exc
|
|
281
281
|
connection.sendall(canonical({'token':record['token'],'operation':operation,**({'params':params} if params is not None else {})})+b'\n')
|
|
@@ -315,8 +315,15 @@ class BackendProxy:
|
|
|
315
315
|
self.permission_batch=None
|
|
316
316
|
self._permission_configuration_inputs=None
|
|
317
317
|
self.smoke_busy=False
|
|
318
|
+
self.lane_busy=False
|
|
319
|
+
self.emit_native=True
|
|
320
|
+
self.lane_connection_binding=None
|
|
321
|
+
self.managed_transport=None
|
|
322
|
+
self._lane_configuration_record=None
|
|
318
323
|
|
|
319
324
|
async def start_child(self):
|
|
325
|
+
if self.lane_connection_binding:
|
|
326
|
+
raise PackError('Managed dynamic lanes require the existing native owner; no alternate app-server startup')
|
|
320
327
|
active=self.lib.active()
|
|
321
328
|
if not active.get('runtime'): raise PackError('No selected managed runtime')
|
|
322
329
|
self.runtime=active['runtime'];env=os.environ.copy()
|
|
@@ -359,6 +366,7 @@ class BackendProxy:
|
|
|
359
366
|
method=value.get('method');params=value.get('params') or {}
|
|
360
367
|
if self.approvals:
|
|
361
368
|
record=self.approvals.capture(value)
|
|
369
|
+
if record:self.dynamic_permission_event(record)
|
|
362
370
|
if record and record['sourceThreadId']!=record['intakeThreadId'] and record['notificationState']!='delivered':
|
|
363
371
|
self.schedule_notification(record)
|
|
364
372
|
if method=='turn/started': self.active_turns.add(params['threadId'])
|
|
@@ -370,7 +378,7 @@ class BackendProxy:
|
|
|
370
378
|
for row in self.approvals.records():
|
|
371
379
|
if row['nativeConnectionCurrent'] and row['intakeThreadId']==params['threadId'] and row['notificationState']!='delivered':
|
|
372
380
|
self.schedule_notification(row)
|
|
373
|
-
sys.stdout.buffer.write(line);sys.stdout.buffer.flush()
|
|
381
|
+
if self.emit_native:sys.stdout.buffer.write(line);sys.stdout.buffer.flush()
|
|
374
382
|
|
|
375
383
|
async def stop_child(self):
|
|
376
384
|
if self.child and self.child.returncode is None:
|
|
@@ -406,7 +414,12 @@ class BackendProxy:
|
|
|
406
414
|
if 'id' in value and 'method' in value:self.pending.add(str(value['id']))
|
|
407
415
|
if self.approvals and 'method' not in value and ('result' in value or 'error' in value):self.approvals.sending(value)
|
|
408
416
|
self.child.stdin.write(canonical(value)+b'\n');await self.child.stdin.drain()
|
|
409
|
-
if self.approvals and 'method' not in value and ('result' in value or 'error' in value):
|
|
417
|
+
if self.approvals and 'method' not in value and ('result' in value or 'error' in value):
|
|
418
|
+
entry=self.approvals.pending.get(str(value['id']))
|
|
419
|
+
self.approvals.settled(value)
|
|
420
|
+
decision=(value.get('result') or {}).get('decision')
|
|
421
|
+
if entry and decision in {'accept','decline','cancel'}:
|
|
422
|
+
self.dynamic_permission_event(entry['record'],'approved' if decision=='accept' else 'denied')
|
|
410
423
|
if self.execution and value.get('method')=='initialized':
|
|
411
424
|
await self.request_catalog()
|
|
412
425
|
|
|
@@ -417,6 +430,8 @@ class BackendProxy:
|
|
|
417
430
|
await self.child.stdin.drain()
|
|
418
431
|
|
|
419
432
|
async def call_native(self,method,params,before_dispatch=None):
|
|
433
|
+
if self.managed_transport:
|
|
434
|
+
return await self.call_managed_native(method,params,before_dispatch)
|
|
420
435
|
identity='topolo-relay-'+uuid.uuid4().hex
|
|
421
436
|
request={'id':identity,'method':method,'params':params}
|
|
422
437
|
if method in {'thread/resume','turn/start'}:
|
|
@@ -441,6 +456,49 @@ class BackendProxy:
|
|
|
441
456
|
raise PackError('Native intake relay refused; original request remains pending')
|
|
442
457
|
return reply['result']
|
|
443
458
|
|
|
459
|
+
async def call_managed_native(self,method,params,before_dispatch=None):
|
|
460
|
+
identity='topolo-managed-'+uuid.uuid4().hex
|
|
461
|
+
request={'id':identity,'method':method,'params':params}
|
|
462
|
+
intake=next((r for r in self.execution.roles.values() if r['key']==self.lane_connection_binding['intakeKey']),None)
|
|
463
|
+
intake_notification=method=='turn/start' and intake and params.get('threadId')==intake['threadId']
|
|
464
|
+
if method in {'thread/resume','turn/start'}:
|
|
465
|
+
await self.qualify_organization(params['threadId'])
|
|
466
|
+
if not intake_notification:request=self.execution.prepare(request)
|
|
467
|
+
elif set(params)!={'threadId','input'}:
|
|
468
|
+
raise PackError('Original intake native settings must remain unchanged')
|
|
469
|
+
if before_dispatch is not None:before_dispatch()
|
|
470
|
+
self.pending.add(identity)
|
|
471
|
+
try:
|
|
472
|
+
result=await self.managed_transport.evaluate('request',{'method':method,'params':request['params']})
|
|
473
|
+
reply=self.execution.reply({'id':identity,'result':result})
|
|
474
|
+
return reply['result']
|
|
475
|
+
finally:self.pending.discard(identity)
|
|
476
|
+
|
|
477
|
+
async def managed_native_events(self):
|
|
478
|
+
while not self.managed_transport.closed:
|
|
479
|
+
messages=await self.managed_transport.evaluate('next',{},events=True)
|
|
480
|
+
if not isinstance(messages,list):
|
|
481
|
+
raise PackError('Actual managed native event batch differs')
|
|
482
|
+
for value in messages:
|
|
483
|
+
if value.get('method')=='topolo/managed-request-unavailable':
|
|
484
|
+
raise PackError('Actual managed own-child request is unavailable; preserve custody')
|
|
485
|
+
record=self.approvals.capture(value)
|
|
486
|
+
if record:
|
|
487
|
+
self.dynamic_permission_event(record)
|
|
488
|
+
self.schedule_notification(record)
|
|
489
|
+
params=value.get('params') or {};method=value.get('method')
|
|
490
|
+
if method=='turn/started':self.active_turns.add(params['threadId'])
|
|
491
|
+
elif method=='turn/completed':self.active_turns.discard(params['threadId'])
|
|
492
|
+
elif method=='thread/status/changed':
|
|
493
|
+
if params.get('status',{}).get('type')=='active':self.active_turns.add(params['threadId'])
|
|
494
|
+
elif params.get('status',{}).get('type') in {'idle','notLoaded','systemError'}:
|
|
495
|
+
self.active_turns.discard(params['threadId'])
|
|
496
|
+
|
|
497
|
+
async def send_native_approval(self,entry,reply):
|
|
498
|
+
if self.managed_transport:
|
|
499
|
+
return await self.managed_transport.evaluate('approval',{'request':entry['message'],'reply':reply})
|
|
500
|
+
self.child.stdin.write(canonical(reply)+b'\n');await self.child.stdin.drain()
|
|
501
|
+
|
|
444
502
|
def smoke_current(self,roles,resuming=None):
|
|
445
503
|
if (not self.execution or self.permission_configuration or self.quiesced or not self.initialized or self.closed or
|
|
446
504
|
not self.child or self.child.returncode is not None or self.active_turns or self.pending or self.notifications or
|
|
@@ -557,20 +615,33 @@ class BackendProxy:
|
|
|
557
615
|
from . import topolo
|
|
558
616
|
consent=role['installationConsent']
|
|
559
617
|
agents=self.lib.load(consent['pack'])[0]['agents']
|
|
560
|
-
matches=[a for a in agents if '/'.join([a['pack'],a['set'],a['id']])==role['key'] and a['threadId']==role['threadId']]
|
|
618
|
+
matches=[a for a in agents if '/'.join([a['pack'],a['set'],a['id']])==role.get('sourceRoleKey',role['key']) and a['threadId']==role.get('sourceRoleNativeId',role['threadId'])]
|
|
561
619
|
if len(matches)!=1:
|
|
562
620
|
raise PackError('Current consent has no unique live owned role')
|
|
563
|
-
|
|
621
|
+
effective_consent=consent
|
|
622
|
+
if role.get('dynamicVerificationProfile')=='manual':
|
|
623
|
+
effective_consent={**consent,'profile':'manual','overrides':{}}
|
|
624
|
+
if role['settings']!=permission_settings(matches[0],role['settings'],effective_consent):
|
|
564
625
|
raise PackError('Effective native permission intent differs from the current consent profile')
|
|
565
626
|
if self.permission_batch and self.permission_batch['consent']==consent:
|
|
566
627
|
if time.monotonic()>=self.permission_batch['deadline']:
|
|
567
628
|
raise PackError('Qualified native permission batch expired; reconcile without replay')
|
|
568
629
|
role['organizationPolicyProof']=self.permission_batch['proof'];return
|
|
569
|
-
|
|
630
|
+
remote,proof=await asyncio.to_thread(topolo.organization_execution_policy,
|
|
570
631
|
topolo.Client(consent['environment'],consent['contextKey']),consent,agents,consent.get('agentWorkspaceId'))
|
|
571
632
|
from .permissions import authorize_native_scope
|
|
572
633
|
authorize_native_scope(proof)
|
|
634
|
+
if role.get('dynamicVerificationProfile')=='manual' and 'manual' not in remote['allowedProfiles']:
|
|
635
|
+
raise PackError('Fresh organization policy prohibits the child-only manual verification profile')
|
|
573
636
|
role['organizationPolicyProof']=proof
|
|
637
|
+
if role.get('dynamicLaneId'):
|
|
638
|
+
from .lanes import Registry
|
|
639
|
+
record=Registry(self.lib.root,'codex').read(role['dynamicLaneId'])
|
|
640
|
+
current=await self.dynamic_current(record['plan'])
|
|
641
|
+
configuring=self._lane_configuration_record
|
|
642
|
+
if configuring and configuring['nativeId']==thread_id and self.lane_busy:
|
|
643
|
+
self.execution.prepare_dynamic_configuration(configuring,current)
|
|
644
|
+
else:self.execution.qualify_dynamic(record,current)
|
|
574
645
|
|
|
575
646
|
def existing_runtime_intent(self):
|
|
576
647
|
"""Full existing composition only; never use a new composition as migration."""
|
|
@@ -588,7 +659,12 @@ class BackendProxy:
|
|
|
588
659
|
declared={(r['key'],r['threadId']) for r in document['roles']}
|
|
589
660
|
with closing(sqlite3.connect('file:'+str(pathlib.Path(self.runtime)/'home/state_5.sqlite')+'?mode=ro',uri=True)) as db:
|
|
590
661
|
native_ids=[r[0] for r in db.execute('SELECT id FROM threads')]
|
|
591
|
-
|
|
662
|
+
from .lanes import Registry
|
|
663
|
+
dynamic=[r for r in Registry(self.lib.root,'codex').records() if r['state']!='retired']
|
|
664
|
+
dynamic_ids={r['nativeId'] for r in dynamic}
|
|
665
|
+
if dynamic_ids & {tid for _,tid in declared} or len(dynamic_ids)!=len(dynamic):
|
|
666
|
+
raise PackError('Dynamic identity overlaps the fixed roster')
|
|
667
|
+
if len(native_ids)!=len(declared)+len(dynamic_ids) or set(native_ids)!={tid for _,tid in declared}|dynamic_ids:
|
|
592
668
|
raise PackError('Native runtime membership differs from full selected composition')
|
|
593
669
|
if identities(roster)!=declared:
|
|
594
670
|
raise PackError('Runtime roster differs from selected source membership')
|
|
@@ -773,7 +849,7 @@ class BackendProxy:
|
|
|
773
849
|
state=await self.call_native('thread/read',{'threadId':record['intakeThreadId'],'includeTurns':False})
|
|
774
850
|
if state.get('thread',{}).get('status',{}).get('type') not in {'idle','notLoaded'}:
|
|
775
851
|
self.approvals.notified(record['requestId'],False);return
|
|
776
|
-
if record['intakeThreadId'] not in self.execution.verified:
|
|
852
|
+
if not self.managed_transport and record['intakeThreadId'] not in self.execution.verified:
|
|
777
853
|
await self.call_native('thread/resume',{'threadId':record['intakeThreadId']})
|
|
778
854
|
self.approvals.live(record['key'],record['intakeThreadId'],record['fingerprint'])
|
|
779
855
|
# A lost turn/start reply cannot be retried on the next idle event.
|
|
@@ -789,8 +865,457 @@ class BackendProxy:
|
|
|
789
865
|
finally:self.notifications.discard(record['key'])
|
|
790
866
|
asyncio.create_task(deliver())
|
|
791
867
|
|
|
868
|
+
def dynamic_history(self, identity):
|
|
869
|
+
with closing(sqlite3.connect('file:'+str(pathlib.Path(self.runtime)/'home/state_5.sqlite')+'?mode=ro',uri=True)) as db:
|
|
870
|
+
row=db.execute('SELECT rollout_path FROM threads WHERE id=?',(identity,)).fetchone()
|
|
871
|
+
if not row:
|
|
872
|
+
raise PackError('Native transcript identity is missing')
|
|
873
|
+
path=pathlib.Path(row[0]);OwnedSmoke.owned(path)
|
|
874
|
+
if not path.resolve().is_relative_to(pathlib.Path(self.runtime).resolve()/'home'):
|
|
875
|
+
raise PackError('Native transcript is outside the owned home')
|
|
876
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
877
|
+
|
|
878
|
+
def dynamic_permission_event(self, approval, decision=None):
|
|
879
|
+
from . import lanes
|
|
880
|
+
role=self.execution.roles.get(approval['sourceThreadId']) if self.execution else None
|
|
881
|
+
if not role or not role.get('dynamicLaneId'):
|
|
882
|
+
return
|
|
883
|
+
store=lanes.Registry(self.lib.root,'codex');record=store.read(role['dynamicLaneId'])
|
|
884
|
+
if decision is None:
|
|
885
|
+
prior=record.get('permissionRequest')
|
|
886
|
+
if prior and prior['fingerprint']==approval['fingerprint'] and prior['requestId']==str(approval['requestId']):
|
|
887
|
+
return
|
|
888
|
+
changed=lanes.request_permission(record,approval['fingerprint'],str(approval['requestId']),approval['generation'])
|
|
889
|
+
changed['permissionRequest'].update(turnId=approval.get('turnId'),itemId=approval.get('itemId'))
|
|
890
|
+
else:
|
|
891
|
+
changed=lanes.record_decision(record,decision,approval['intakeKey'],approval['fingerprint'],approval['generation'])
|
|
892
|
+
store.write(changed,lanes.digest(record))
|
|
893
|
+
|
|
894
|
+
async def dynamic_outcome(self, record):
|
|
895
|
+
from . import lanes
|
|
896
|
+
if record['state']=='executed-outcome-verified':
|
|
897
|
+
return record
|
|
898
|
+
if record['state']!='permission-decision-forwarded':
|
|
899
|
+
raise PackError('Lane settlement needs a real native request, intake decision and operation outcome')
|
|
900
|
+
request=record['permissionRequest']
|
|
901
|
+
result=await self.call_native('thread/read',{'threadId':record['nativeId'],'includeTurns':True})
|
|
902
|
+
if result.get('thread',{}).get('id')!=record['nativeId']:
|
|
903
|
+
raise PackError('Native outcome thread identity differs')
|
|
904
|
+
turns=[t for t in result.get('thread',{}).get('turns',[]) if t.get('id')==request.get('turnId')]
|
|
905
|
+
if len(turns)!=1 or turns[0].get('status') not in {'completed','failed','interrupted'}:
|
|
906
|
+
raise PackError('Exact native permission turn has not reached a terminal outcome')
|
|
907
|
+
items=[i for i in turns[0].get('items',[]) if i.get('id')==request.get('itemId')]
|
|
908
|
+
if len(items)!=1 or items[0].get('type') not in {'commandExecution','fileChange'}:
|
|
909
|
+
raise PackError('Native permission operation outcome cannot be independently attributed')
|
|
910
|
+
item=items[0];status=item.get('status')
|
|
911
|
+
if record['permissionDecision']=='denied':
|
|
912
|
+
if status!='declined':
|
|
913
|
+
raise PackError('Denied native request has no declined operation ACK')
|
|
914
|
+
state='denied'
|
|
915
|
+
elif status=='completed' and (item['type']!='commandExecution' or item.get('exitCode')==0): state='completed'
|
|
916
|
+
elif status=='failed' or (item['type']=='commandExecution' and status=='completed' and item.get('exitCode')!=0): state='failed'
|
|
917
|
+
else: raise PackError('Native permission outcome is unresolved')
|
|
918
|
+
return lanes.record_outcome(record,{'nativeId':record['nativeId'],'requestFingerprint':request['fingerprint'],
|
|
919
|
+
'state':state,'verificationDigest':lanes.digest({'turn':turns[0],'item':item})})
|
|
920
|
+
|
|
921
|
+
def lane_idle(self):
|
|
922
|
+
if (not self.execution or not self.approvals or not self.initialized or self.quiesced or self.closed or
|
|
923
|
+
(not self.managed_transport and (not self.child or self.child.returncode is not None)) or
|
|
924
|
+
(self.managed_transport and not self.managed_transport.ready) or self.permission_configuration or self.smoke_busy):
|
|
925
|
+
raise PackError('Dynamic lanes require their current qualified native connection')
|
|
926
|
+
|
|
927
|
+
def dynamic_role(self, row):
|
|
928
|
+
"""Collect identity refs from the supported connection, never caller JSON."""
|
|
929
|
+
from . import topolo, lanes
|
|
930
|
+
consent=row.get('installationConsent')
|
|
931
|
+
if (not isinstance(consent,dict) or not isinstance(consent.get('contextKey'),str) or
|
|
932
|
+
not consent['contextKey'].startswith('org:') or consent['contextKey']!=row.get('contextKey') or
|
|
933
|
+
consent.get('environment') not in {'development','staging','production'}):
|
|
934
|
+
raise PackError('Dynamic lanes need an organization-bound source installation')
|
|
935
|
+
pack=row['key'].split('/')[0];logical='/'.join(row['key'].split('/')[1:])
|
|
936
|
+
cwd=pathlib.Path(row['cwd']);home=pathlib.Path(self.runtime).resolve()
|
|
937
|
+
if not cwd.resolve().is_relative_to(home):
|
|
938
|
+
raise PackError('Dynamic source workspace is outside the selected native runtime')
|
|
939
|
+
files=[cwd/'AGENTS.md',cwd/'topolo-connection.json',cwd/'.mcp.json']
|
|
940
|
+
for file in files: OwnedSmoke.owned(file)
|
|
941
|
+
hashes={str(f):hashlib.sha256(f.read_bytes()).hexdigest() for f in files}
|
|
942
|
+
descriptor=read_json(files[1]);binding=read_json(topolo.binding_path(self.lib,pack,consent['environment'],consent['contextKey']))
|
|
943
|
+
roles=[r for r in binding['input']['roles'] if r['logicalId']==logical]
|
|
944
|
+
identity=binding['identity']
|
|
945
|
+
if (len(roles)!=1 or identity['organizationId']!=consent['contextKey'][4:] or
|
|
946
|
+
any(descriptor.get(k)!=v for k,v in {'pack':pack,'role':logical,'environment':consent['environment'],
|
|
947
|
+
'contextKey':consent['contextKey'],'instanceId':binding['input']['instanceId'],
|
|
948
|
+
'agentWorkspaceId':binding['agentWorkspaceId'],'bytesWorkspaceId':binding['input']['bytesWorkspaceId'],
|
|
949
|
+
'agentId':roles[0]['agentId'],'agentVersionId':roles[0]['agentVersionId'],
|
|
950
|
+
'identity':{key:identity[key] for key in ['userId','organizationId']}}.items())):
|
|
951
|
+
raise PackError('Hosted role, Bytes and organization descriptors differ')
|
|
952
|
+
return {'key':row['key'],'harness':'codex','nativeId':row['threadId'],'cwd':row['cwd'],
|
|
953
|
+
'settings':row['settings'],'contextDigest':lanes.digest(hashes),'packId':pack,'packRevision':row['sourceRevision'],
|
|
954
|
+
'contextKey':consent['contextKey'],'organizationId':identity['organizationId'],'environment':consent['environment'],
|
|
955
|
+
'installationId':binding['input']['instanceId'],'AgentLogicalId':roles[0]['agentId'],
|
|
956
|
+
'AgentVersionId':roles[0]['agentVersionId'],'AgentWorkspaceId':binding['agentWorkspaceId'],
|
|
957
|
+
'BytesWorkspaceId':binding['input']['bytesWorkspaceId'],'privateMCPDescriptorHash':hashes[str(files[2])]}
|
|
958
|
+
|
|
959
|
+
def dynamic_model_inputs(self, source):
|
|
960
|
+
"""Explicit versioned pack/group/role model constraints; no inferred defaults."""
|
|
961
|
+
path=self.lib.root/'dynamic-models'/(source['packId']+'.json');OwnedSmoke.owned(path)
|
|
962
|
+
document=read_json(path)
|
|
963
|
+
if (set(document)!={'format','schemaVersion','sourceRevision','configuration','constraints','authorizedOverrides'} or
|
|
964
|
+
document['format']!='topolo-pack-dynamic-models' or document['schemaVersion']!=1 or
|
|
965
|
+
document['sourceRevision']!=source['packRevision']):
|
|
966
|
+
raise PackError('Versioned dynamic model configuration is missing for this exact source role')
|
|
967
|
+
from .task_models import role_constraints
|
|
968
|
+
document={**document,'effectiveConstraints':role_constraints(document['constraints'],source['key'])}
|
|
969
|
+
catalog={'harness':'codex','source':'native-account','connectionGeneration':self.approvals.generation,
|
|
970
|
+
'models':[{'model':m,'efforts':sorted(e)} for m,e in sorted(self.execution.catalog.items())]}
|
|
971
|
+
return document,catalog
|
|
972
|
+
|
|
973
|
+
async def dynamic_current(self, plan):
|
|
974
|
+
self.lane_idle()
|
|
975
|
+
source=self.execution.roles.get(plan['role']['nativeId']);intake=self.execution.roles.get(plan['intake']['nativeId'])
|
|
976
|
+
if not source or not intake or source.get('dynamicLaneId') or intake.get('dynamicLaneId'):
|
|
977
|
+
raise PackError('Dynamic source role or original intake is not currently owned')
|
|
978
|
+
await self.qualify_organization(source['threadId'])
|
|
979
|
+
self.lane_idle()
|
|
980
|
+
role=self.dynamic_role(source);route=self.dynamic_role(intake);proof=source['organizationPolicyProof']
|
|
981
|
+
consent=source['installationConsent']
|
|
982
|
+
ceiling={'policyRevision':proof['revision'],'policyDigest':proof['policyDigest'],
|
|
983
|
+
'profile':consent.get('overrides',{}).get(source['key'],consent['profile']),'intakeKey':source['intakeKey']}
|
|
984
|
+
document,catalog=self.dynamic_model_inputs(role)
|
|
985
|
+
from . import task_models,lanes
|
|
986
|
+
task=plan['resolution']['task']
|
|
987
|
+
resolution=task_models.resolve(document['configuration'],task,'codex',catalog,document['effectiveConstraints'],
|
|
988
|
+
override_authorized=lanes.digest(task) in document['authorizedOverrides'])
|
|
989
|
+
if resolution!=plan['resolution']:
|
|
990
|
+
raise PackError('Task profile, native catalog, intake override or configuration changed; no fallback')
|
|
991
|
+
return {'role':role,'intake':route,'ceiling':ceiling}
|
|
992
|
+
|
|
993
|
+
async def dynamic_native(self, identity, cwd):
|
|
994
|
+
result=await self.call_native('thread/read',{'threadId':identity,'includeTurns':False})
|
|
995
|
+
thread=result.get('thread',{})
|
|
996
|
+
with closing(sqlite3.connect('file:'+str(pathlib.Path(self.runtime)/'home/state_5.sqlite')+'?mode=ro',uri=True)) as db:
|
|
997
|
+
rows=db.execute('SELECT id,cwd,archived FROM threads WHERE id=?',(identity,)).fetchall()
|
|
998
|
+
if rows!=[(identity,cwd,0)] or thread.get('id')!=identity or thread.get('cwd')!=cwd:
|
|
999
|
+
raise PackError('Dynamic native descriptor is not the exact active owned runtime identity')
|
|
1000
|
+
return {'nativeId':identity,'cwd':cwd,'nativeParentId':None,'archived':False,'backend':'local',
|
|
1001
|
+
'foreignWorkspace':False,'busy':thread.get('status',{}).get('type') not in {'idle','notLoaded'},
|
|
1002
|
+
'pendingApprovals':sum(e['record']['sourceThreadId']==identity for e in self.approvals.pending.values())}
|
|
1003
|
+
|
|
1004
|
+
async def dynamic_context(self,operation,params):
|
|
1005
|
+
from . import lanes
|
|
1006
|
+
import shlex
|
|
1007
|
+
expected_fields={'laneId','operationId'} if operation=='lane-context-start' else {'laneId'}
|
|
1008
|
+
if set(params)!=expected_fields:
|
|
1009
|
+
raise PackError('Closed own-child context operation required')
|
|
1010
|
+
self.lane_idle();store=lanes.Registry(self.lib.root,'codex');record=store.read(params['laneId'])
|
|
1011
|
+
if not record or record['state']!='loaded-context-ACK':
|
|
1012
|
+
raise PackError('Actual native settings and instruction-source ACK required before bootstrap')
|
|
1013
|
+
current=await self.dynamic_current(record['plan'])
|
|
1014
|
+
if current!={k:record['plan'][k] for k in ['role','intake','ceiling']}:
|
|
1015
|
+
raise PackError('Current bootstrap source or policy differs')
|
|
1016
|
+
native=await self.dynamic_native(record['nativeId'],record['creationReceipt']['cwd'])
|
|
1017
|
+
if native['busy'] or native['pendingApprovals']:
|
|
1018
|
+
return {'state':'awaiting-own-native-idle','native':native}
|
|
1019
|
+
if operation=='lane-context-read':
|
|
1020
|
+
probe=record.get('contextProbe')
|
|
1021
|
+
if not probe or probe['state']!='native-delivered':
|
|
1022
|
+
raise PackError('Exact own bootstrap delivery is unresolved; no replay')
|
|
1023
|
+
result=await self.call_native('thread/read',{'threadId':record['nativeId'],'includeTurns':True})
|
|
1024
|
+
thread=result.get('thread',{});turns=[t for t in thread.get('turns',[]) if t.get('id')==probe['turnId']]
|
|
1025
|
+
if thread.get('id')!=record['nativeId'] or len(turns)!=1 or turns[0].get('status')!='completed' or turns[0].get('error'):
|
|
1026
|
+
raise PackError('Exact same-child bootstrap turn is not completed')
|
|
1027
|
+
reads=[]
|
|
1028
|
+
for item in turns[0].get('items',[]):
|
|
1029
|
+
if item.get('type')!='commandExecution' or item.get('status')!='completed' or item.get('exitCode')!=0:
|
|
1030
|
+
continue
|
|
1031
|
+
command=item.get('command','');args=shlex.split(command) if isinstance(command,str) else []
|
|
1032
|
+
exact=command==probe['command'] or (len(args)==3 and pathlib.Path(args[0]).name in {'sh','zsh','bash'} and args[1] in {'-c','-lc'} and args[2]==probe['command'])
|
|
1033
|
+
if not exact:
|
|
1034
|
+
continue
|
|
1035
|
+
try:value=json.loads(item.get('aggregatedOutput',''))
|
|
1036
|
+
except (ValueError,TypeError):continue
|
|
1037
|
+
if value==probe['expected'] and item.get('id'):
|
|
1038
|
+
reads.append(item['id'])
|
|
1039
|
+
if len(reads)!=1:
|
|
1040
|
+
raise PackError('Actual same-child native file-read command and exact metadata result are unproven')
|
|
1041
|
+
if any(hashlib.sha256(pathlib.Path(path).read_bytes()).hexdigest()!=sha for path,sha in probe['fileHashes'].items()):
|
|
1042
|
+
raise PackError('Own role context changed during native bootstrap')
|
|
1043
|
+
updated={**record,'contextProbe':{k:v for k,v in probe.items() if k not in {'text','command'}}|
|
|
1044
|
+
{'state':'native-file-read-ACK','nativeItemId':reads[0],'proofDigest':lanes.digest({'turnId':probe['turnId'],'itemId':reads[0],'expected':probe['expected']})}}
|
|
1045
|
+
return store.write(updated,lanes.digest(record))
|
|
1046
|
+
if record.get('contextProbe'):
|
|
1047
|
+
raise PackError('Owned native bootstrap is already reserved; no second turn')
|
|
1048
|
+
nonce=OwnedSmoke.operation_id(params['operationId']);cwd=pathlib.Path(record['creationReceipt']['cwd'])
|
|
1049
|
+
guide=cwd/'AGENTS.md';connection=cwd/'topolo-connection.json'
|
|
1050
|
+
for path in [guide,connection]:OwnedSmoke.owned(path)
|
|
1051
|
+
descriptor=read_json(connection);role=record['plan']['role'];principal=descriptor.get('identity',{}).get('userId')
|
|
1052
|
+
if not isinstance(principal,str) or not principal:
|
|
1053
|
+
raise PackError('Current connected role principal is unknown')
|
|
1054
|
+
expected={'operationId':nonce,'nativeId':record['nativeId'],'AgentLogicalId':role['AgentLogicalId'],'AgentVersionId':role['AgentVersionId'],
|
|
1055
|
+
'AgentWorkspaceId':role['AgentWorkspaceId'],'BytesWorkspaceId':role['BytesWorkspaceId'],'principalId':principal,
|
|
1056
|
+
'organizationId':role['organizationId'],'environment':role['environment'],'charterSHA256':hashlib.sha256(guide.read_bytes()).hexdigest()}
|
|
1057
|
+
script=("import pathlib,json,hashlib;d=json.loads(pathlib.Path("+repr(str(connection))+").read_text());"
|
|
1058
|
+
"v={'operationId':"+repr(nonce)+",'nativeId':"+repr(record['nativeId'])+",'AgentLogicalId':d['agentId'],'AgentVersionId':d['agentVersionId'],"
|
|
1059
|
+
"'AgentWorkspaceId':d['agentWorkspaceId'],'BytesWorkspaceId':d['bytesWorkspaceId'],'principalId':d['identity']['userId'],"
|
|
1060
|
+
"'organizationId':d['identity']['organizationId'],'environment':d['environment'],'charterSHA256':hashlib.sha256(pathlib.Path("+repr(str(guide))+").read_bytes()).hexdigest()};print(json.dumps(v))")
|
|
1061
|
+
command='python3 -c '+shlex.quote(script)
|
|
1062
|
+
text=('Bounded own-child context bootstrap '+nonce+'. Before role work, read only your own AGENTS.md and topolo-connection.json. '
|
|
1063
|
+
'Use the native exec_command tool to run exactly this read-only metadata command: '+command+'. '
|
|
1064
|
+
'It outputs only public role identities and a charter hash. Do not write files, access credentials, use the network, or perform other work. '
|
|
1065
|
+
'Return the same JSON object. This file-read proof does not verify private MCP connectivity.')
|
|
1066
|
+
probe={'operationId':nonce,'expected':expected,'command':command,'text':text,'state':'start-may-have-effects',
|
|
1067
|
+
'fileHashes':{str(p):hashlib.sha256(p.read_bytes()).hexdigest() for p in [guide,connection]},
|
|
1068
|
+
'generation':record['nativeConnectionGeneration'],'privateMCPVerified':False}
|
|
1069
|
+
held={**record,'contextProbe':probe};store.write(held,lanes.digest(record))
|
|
1070
|
+
try:
|
|
1071
|
+
self.execution.qualify_dynamic(record,current)
|
|
1072
|
+
result=await self.call_native('turn/start',{'threadId':record['nativeId'],'input':[{'type':'text','text':text,'text_elements':[]}]})
|
|
1073
|
+
turn_id=result.get('turn',{}).get('id')
|
|
1074
|
+
if not isinstance(turn_id,str) or not turn_id:
|
|
1075
|
+
raise PackError('Actual bootstrap turn acknowledgement is missing')
|
|
1076
|
+
updated={**held,'contextProbe':{**probe,'state':'native-delivered','turnId':turn_id}}
|
|
1077
|
+
store.write(updated,lanes.digest(held));return {'state':'own-context-bootstrap-delivered','turnId':turn_id,'operationId':nonce}
|
|
1078
|
+
except BaseException:
|
|
1079
|
+
store.write({**held,'contextProbe':{**probe,'state':'uncertain-effect-hold'}},lanes.digest(held));raise
|
|
1080
|
+
|
|
1081
|
+
async def dynamic_fixture(self,operation,params):
|
|
1082
|
+
from . import lanes
|
|
1083
|
+
expected={'laneId','operationId'} if operation=='lane-fixture-start' else {'laneId'}
|
|
1084
|
+
if set(params)!=expected:
|
|
1085
|
+
raise PackError('Closed own-lane fixture operation required')
|
|
1086
|
+
self.lane_idle();store=lanes.Registry(self.lib.root,'codex');record=store.read(params['laneId'])
|
|
1087
|
+
if not record or record['plan'].get('verificationProfile')!='manual':
|
|
1088
|
+
raise PackError('Actual routing proof requires the bound two-child manual verification plan')
|
|
1089
|
+
current=await self.dynamic_current(record['plan'])
|
|
1090
|
+
if current!={k:record['plan'][k] for k in ['role','intake','ceiling']}:
|
|
1091
|
+
raise PackError('Current source/intake/policy changed before own fixture verification')
|
|
1092
|
+
if record.get('contextProbe',{}).get('state')!='native-file-read-ACK':
|
|
1093
|
+
raise PackError('Actual own-child native file-read bootstrap is required before fixture work')
|
|
1094
|
+
if operation=='lane-fixture-read':
|
|
1095
|
+
fixture=record.get('fixtureVerification')
|
|
1096
|
+
if not fixture or fixture['state']!='native-delivered':
|
|
1097
|
+
raise PackError('Own native fixture delivery is unresolved; no replay')
|
|
1098
|
+
native=await self.dynamic_native(record['nativeId'],record['creationReceipt']['cwd'])
|
|
1099
|
+
if native['busy'] or native['pendingApprovals']:
|
|
1100
|
+
return {'state':record['state'],'nativeBusy':native['busy'],'pendingApprovals':native['pendingApprovals']}
|
|
1101
|
+
verified=await self.dynamic_outcome(record)
|
|
1102
|
+
if verified['outcome']['state']=='completed':
|
|
1103
|
+
path=pathlib.Path(fixture['path']);OwnedSmoke.owned(path)
|
|
1104
|
+
if read_json(path)!=fixture['expected']:
|
|
1105
|
+
raise PackError('Real native item result is not the exact own nonce-file bytes')
|
|
1106
|
+
verified['fixtureVerification']={k:v for k,v in fixture.items() if k!='text'}|{'state':'outcome-verified','fileSHA256':hashlib.sha256(path.read_bytes()).hexdigest()}
|
|
1107
|
+
elif verified['outcome']['state']=='denied':
|
|
1108
|
+
if pathlib.Path(fixture['path']).exists():
|
|
1109
|
+
raise PackError('Denied request has an unexpected fixture file; retain unresolved effects')
|
|
1110
|
+
settled=lanes.settle(verified,native);return store.write(settled,lanes.digest(record))
|
|
1111
|
+
nonce=OwnedSmoke.operation_id(params['operationId'])
|
|
1112
|
+
if record['state']!='loaded-context-ACK' or record.get('fixtureVerification'):
|
|
1113
|
+
raise PackError('Own fixture requires current loaded context and one fresh bounded operation')
|
|
1114
|
+
native=await self.dynamic_native(record['nativeId'],record['creationReceipt']['cwd'])
|
|
1115
|
+
if native['busy'] or native['pendingApprovals']:
|
|
1116
|
+
raise PackError('Own fixture child is not idle')
|
|
1117
|
+
role=record['plan']['role'];path=pathlib.Path(record['creationReceipt']['cwd'])/('.topolo-dynamic-'+record['laneId']+'-'+nonce+'.json')
|
|
1118
|
+
if path.exists() or path.is_symlink():
|
|
1119
|
+
raise PackError('Own fixture path already exists; never overwrite')
|
|
1120
|
+
body={'operationId':nonce,'laneId':record['laneId'],'nativeId':record['nativeId'],'AgentVersionId':role['AgentVersionId'],
|
|
1121
|
+
'AgentWorkspaceId':role['AgentWorkspaceId'],'BytesWorkspaceId':role['BytesWorkspaceId']}
|
|
1122
|
+
text=('Topolo bounded own-worker verification '+nonce+'. Read the already loaded role charter and connected Agent/Bytes identity. '
|
|
1123
|
+
'Use the native apply_patch/file-change tool to create only '+str(path)+' with exactly this JSON: '+json.dumps(body,separators=(',',':'))+'. '
|
|
1124
|
+
'Request native permission through the original intake; do not bypass policy or auto-approve. Read back only your fixture and report its nonce and status. '
|
|
1125
|
+
'Do not edit source, business content, other workspaces, credentials, global settings, provider resources or external messages.')
|
|
1126
|
+
fixture={'operationId':nonce,'state':'start-may-have-effects','path':str(path),'expected':body,'text':text,
|
|
1127
|
+
'generation':self.approvals.generation}
|
|
1128
|
+
prepared={**record,'fixtureVerification':fixture};store.write(prepared,lanes.digest(record))
|
|
1129
|
+
try:
|
|
1130
|
+
await self.call_native('thread/resume',{'threadId':record['nativeId']})
|
|
1131
|
+
result=await self.call_native('turn/start',{'threadId':record['nativeId'],'input':[{'type':'text','text':text,'text_elements':[]}]})
|
|
1132
|
+
turn=result.get('turn',{}).get('id')
|
|
1133
|
+
if not isinstance(turn,str) or not turn:
|
|
1134
|
+
raise PackError('Native fixture turn has no actual identity')
|
|
1135
|
+
latest=store.read(record['laneId']);updated={**latest,'fixtureVerification':{**fixture,'state':'native-delivered','turnId':turn}}
|
|
1136
|
+
return store.write(updated,lanes.digest(latest))
|
|
1137
|
+
except BaseException:
|
|
1138
|
+
latest=store.read(record['laneId']);store.write({**latest,'fixtureVerification':{**fixture,'state':'uncertain'}},lanes.digest(latest));raise
|
|
1139
|
+
|
|
1140
|
+
async def dynamic_create(self,params):
|
|
1141
|
+
"""Root-invoked supported ThreadStart on this exact owned native home.
|
|
1142
|
+
|
|
1143
|
+
No requested task or turn is started. A possible-effect creation is
|
|
1144
|
+
durably reserved before dispatch and never automatically replayed.
|
|
1145
|
+
"""
|
|
1146
|
+
from . import lanes
|
|
1147
|
+
if not self.lane_connection_binding or set(params)!={'laneId'} or params['laneId']!=self.lane_connection_binding['laneId']:
|
|
1148
|
+
raise PackError('Supported native creation requires the exact explicit owned Root lane connection')
|
|
1149
|
+
self.lane_idle();store=lanes.Registry(self.lib.root,'codex');planned=store.read_plan(params['laneId'])
|
|
1150
|
+
if store.read(params['laneId']) is not None:
|
|
1151
|
+
raise PackError('This lane already has an actual native identity')
|
|
1152
|
+
current=await self.dynamic_current(planned)
|
|
1153
|
+
if current!={k:planned[k] for k in ['role','intake','ceiling']}:
|
|
1154
|
+
raise PackError('Source, intake or policy changed before native creation')
|
|
1155
|
+
await asyncio.to_thread(managed_lane_frontend,self.lib,self.runtime,planned)
|
|
1156
|
+
# Independent fixed-ID reads before the single fresh worker is spent.
|
|
1157
|
+
for role in [current['role'],current['intake']]:await self.dynamic_native(role['nativeId'],role['cwd'])
|
|
1158
|
+
directory=store.directory/'creation';directory.mkdir(mode=0o700,exist_ok=True);store._owned(directory,True)
|
|
1159
|
+
marker=directory/(planned['laneId']+'.json')
|
|
1160
|
+
if marker.exists() or marker.is_symlink():
|
|
1161
|
+
raise PackError('Native creation already reserved; reconcile without replay')
|
|
1162
|
+
fd=os.open(marker,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
|
|
1163
|
+
phase={'format':'topolo-owned-native-creation','schemaVersion':1,'laneId':planned['laneId'],
|
|
1164
|
+
'planDigest':lanes.digest(planned),'state':'may-have-effects','nativeId':None,'generation':self.approvals.generation}
|
|
1165
|
+
with os.fdopen(fd,'wb') as stream:stream.write(canonical(phase)+b'\n');stream.flush();os.fsync(stream.fileno())
|
|
1166
|
+
spec=dict(planned['role']['settings'])
|
|
1167
|
+
if planned.get('verificationProfile')=='manual':
|
|
1168
|
+
from .permissions import apply_settings
|
|
1169
|
+
source=planned['role'];spec=apply_settings({'pack':source['packId'],'set':source['key'].split('/')[1],'id':source['key'].split('/')[2]},
|
|
1170
|
+
'codex',spec,{'profile':'manual','overrides':{},'intakeRoutes':{source['key']:planned['intake']['key']}})
|
|
1171
|
+
choice=planned['resolution']['resolved'];spec.update(model=choice['model'],model_reasoning_effort=choice['effort'])
|
|
1172
|
+
guide=pathlib.Path(planned['role']['cwd'])/'AGENTS.md';charter=guide.read_text()
|
|
1173
|
+
if len(charter.encode())>131072:
|
|
1174
|
+
raise PackError('Reviewed role charter exceeds bounded native creation context')
|
|
1175
|
+
try:
|
|
1176
|
+
result=await self.call_native('thread/start',{'cwd':planned['role']['cwd'],'model':choice['model'],
|
|
1177
|
+
'allowProviderModelFallback':False,
|
|
1178
|
+
'approvalPolicy':spec['approval_policy'],'sandbox':spec['sandbox_mode'],
|
|
1179
|
+
'config':{'model_reasoning_effort':choice['effort'],'sandbox_workspace_write':spec['sandbox_workspace_write'],'apps':spec['apps']},
|
|
1180
|
+
'developerInstructions':charter})
|
|
1181
|
+
thread=result.get('thread',{});identity=thread.get('id')
|
|
1182
|
+
if not isinstance(identity,str) or not identity or identity in planned['nativeIdsBeforeCreation']:
|
|
1183
|
+
raise PackError('Native ThreadStart has no proven fresh created identity')
|
|
1184
|
+
phase={**phase,'nativeId':identity};atomic_json(marker,phase)
|
|
1185
|
+
native=await self.dynamic_native(identity,planned['role']['cwd'])
|
|
1186
|
+
if native['busy'] or native['pendingApprovals'] or await self.dynamic_current(planned)!=current:
|
|
1187
|
+
raise PackError('Actual created child placement, idle state or current context differs')
|
|
1188
|
+
frontend=await asyncio.to_thread(managed_lane_frontend,self.lib,self.runtime,planned,identity)
|
|
1189
|
+
receipt={'format':'topolo-native-lane-created','schemaVersion':1,'state':'ready','planDigest':lanes.digest(planned),
|
|
1190
|
+
'harness':'codex','nativeId':identity,'logicalOriginThreadId':planned['logicalOriginThreadId'],'cwd':native['cwd'],
|
|
1191
|
+
'model':result.get('model'),'effort':result.get('reasoningEffort'),'nativeParentId':None,
|
|
1192
|
+
'fresh':True,'creationSource':'supported-native-host-tool'}
|
|
1193
|
+
if receipt['model']!=choice['model'] or receipt['effort']!=choice['effort']:
|
|
1194
|
+
raise PackError('Native creation model/effort tuple differs; no fallback or task dispatch')
|
|
1195
|
+
record=lanes.adopt(planned,receipt,native,current)
|
|
1196
|
+
record.update(frontendVisibility=frontend['state'],frontendProof=frontend)
|
|
1197
|
+
record=store.write(record,None)
|
|
1198
|
+
atomic_json(marker,{**phase,'state':'ready','receipt':receipt,'modelTurnsStarted':False,'nativeParentAvailability':'UNAVAILABLE'})
|
|
1199
|
+
return {'state':'created-adopted-unconfigured','receipt':receipt,'modelTurnsStarted':False,'nativeParentAvailability':'UNAVAILABLE'}
|
|
1200
|
+
except BaseException:
|
|
1201
|
+
atomic_json(marker,{**phase,'state':'uncertain-effect-hold','replayAuthorized':False});raise
|
|
1202
|
+
|
|
1203
|
+
async def dynamic_lane(self, operation, params):
|
|
1204
|
+
from . import lanes,task_models,policy
|
|
1205
|
+
self.lane_idle()
|
|
1206
|
+
if self.lane_busy:
|
|
1207
|
+
raise PackError('An owned dynamic lane operation is already active')
|
|
1208
|
+
self.lane_busy=True;store=lanes.Registry(self.lib.root,'codex');changed=False;record=None
|
|
1209
|
+
try:
|
|
1210
|
+
if operation=='lane-plan':
|
|
1211
|
+
if set(params)!={'request'}:
|
|
1212
|
+
raise PackError('Lane plan accepts only its exact intake request')
|
|
1213
|
+
request=params['request'];source=next((r for r in self.execution.roles.values() if r['key']==request['sourceRoleKey']),None)
|
|
1214
|
+
if not source or source.get('dynamicLaneId'):
|
|
1215
|
+
raise PackError('Exact fixed source role required')
|
|
1216
|
+
intake=next((r for r in self.execution.roles.values() if r['key']==source['intakeKey']),None)
|
|
1217
|
+
if not intake:
|
|
1218
|
+
raise PackError('Original native intake is missing')
|
|
1219
|
+
await self.qualify_organization(source['threadId'])
|
|
1220
|
+
self.execution.set_catalog(await self.read_catalog())
|
|
1221
|
+
role=self.dynamic_role(source);route=self.dynamic_role(intake);document,catalog=self.dynamic_model_inputs(role)
|
|
1222
|
+
choice=policy.dynamic_task_settings(document['configuration'],request['task'],'codex',catalog,
|
|
1223
|
+
document['effectiveConstraints'],override_authorized=lanes.digest(request['task']) in document['authorizedOverrides'])
|
|
1224
|
+
proof=source['organizationPolicyProof'];consent=source['installationConsent']
|
|
1225
|
+
if request.get('verificationProfile')=='manual':
|
|
1226
|
+
from . import topolo
|
|
1227
|
+
remote,_=await asyncio.to_thread(topolo.organization_execution_policy,topolo.Client(consent['environment'],consent['contextKey']),
|
|
1228
|
+
consent,self.lib.load(role['packId'])[0]['agents'],consent.get('agentWorkspaceId'))
|
|
1229
|
+
if 'manual' not in remote['allowedProfiles']:
|
|
1230
|
+
raise PackError('Fresh organization policy does not allow the narrower test profile')
|
|
1231
|
+
plan=lanes.plan(request,role,route,choice,{'policyRevision':proof['revision'],'policyDigest':proof['policyDigest'],
|
|
1232
|
+
'profile':consent.get('overrides',{}).get(source['key'],consent['profile']),'intakeKey':source['intakeKey']})
|
|
1233
|
+
with closing(sqlite3.connect('file:'+str(pathlib.Path(self.runtime)/'home/state_5.sqlite')+'?mode=ro',uri=True)) as db:
|
|
1234
|
+
plan['nativeIdsBeforeCreation']=sorted(r[0] for r in db.execute('SELECT id FROM threads'))
|
|
1235
|
+
return store.save_plan(plan)
|
|
1236
|
+
if operation=='lane-adopt':
|
|
1237
|
+
if set(params)!={'plan','receipt'}:
|
|
1238
|
+
raise PackError('Lane adoption requires exact plan and native creation receipt')
|
|
1239
|
+
plan=params['plan'];receipt=params['receipt']
|
|
1240
|
+
if store.read_plan(plan['laneId'])!=plan:
|
|
1241
|
+
raise PackError('Creation plan differs from the owned immutable native snapshot')
|
|
1242
|
+
if receipt['nativeId'] in plan.get('nativeIdsBeforeCreation',[]):
|
|
1243
|
+
raise PackError('Native lane was already present at planning; fresh creation not proven')
|
|
1244
|
+
if receipt['cwd']!=plan['role']['cwd']:
|
|
1245
|
+
raise PackError('Dynamic lane must use the reviewed source role context workspace')
|
|
1246
|
+
current=await self.dynamic_current(plan)
|
|
1247
|
+
native=await self.dynamic_native(receipt['nativeId'],receipt['cwd'])
|
|
1248
|
+
if native['busy'] or native['pendingApprovals']:
|
|
1249
|
+
raise PackError('Created native lane must be idle before adoption')
|
|
1250
|
+
record=lanes.adopt(plan,receipt,native,current)
|
|
1251
|
+
return store.write(record,None)
|
|
1252
|
+
if set(params)!={'laneId'}:
|
|
1253
|
+
raise PackError('Lane operation accepts only its exact registered identity')
|
|
1254
|
+
record=store.read(params['laneId'])
|
|
1255
|
+
if not record:
|
|
1256
|
+
raise PackError('No owned dynamic lane registration')
|
|
1257
|
+
current=await self.dynamic_current(record['plan'])
|
|
1258
|
+
if current!={k:record['plan'][k] for k in ['role','intake','ceiling']}:
|
|
1259
|
+
raise PackError('Dynamic source, intake or policy drift; no native mutation')
|
|
1260
|
+
native=await self.dynamic_native(record['nativeId'],record['creationReceipt']['cwd'])
|
|
1261
|
+
if operation=='lane-read':
|
|
1262
|
+
return {'record':record,'native':native,'proof':'current-own-native-read'}
|
|
1263
|
+
if native['busy'] or native['pendingApprovals']:
|
|
1264
|
+
raise PackError('Dynamic lane is busy or has a pending original approval')
|
|
1265
|
+
if operation=='lane-configure':
|
|
1266
|
+
self.execution.prepare_dynamic_configuration(record,current)
|
|
1267
|
+
self.approvals.register_dynamic(self.execution.roles[record['nativeId']])
|
|
1268
|
+
# Persist uncertainty before the first actual settings effect.
|
|
1269
|
+
before=lanes.digest(record);hold={**record,'state':'uncertain-effect-hold'}
|
|
1270
|
+
store.write(hold,before);changed=True
|
|
1271
|
+
self._lane_configuration_record=record
|
|
1272
|
+
result=await self.call_native('thread/resume',{'threadId':record['nativeId']})
|
|
1273
|
+
actual={'model':result.get('model'),'effort':result.get('reasoningEffort')}
|
|
1274
|
+
source=record['plan']['role'];document,catalog=self.dynamic_model_inputs(source)
|
|
1275
|
+
choice=task_models.acknowledge(record['plan']['resolution'],actual,configuration=document['configuration'],
|
|
1276
|
+
catalog=catalog,constraints=document['effectiveConstraints'])
|
|
1277
|
+
configured=lanes.acknowledge_settings(record,actual,choice,self.approvals.generation)
|
|
1278
|
+
guide=str(pathlib.Path(source['cwd'])/'AGENTS.md')
|
|
1279
|
+
if guide not in result.get('instructionSources',[]):
|
|
1280
|
+
raise PackError('Actual native loaded instruction sources omit the owned role charter')
|
|
1281
|
+
if await self.dynamic_current(record['plan'])!=current:
|
|
1282
|
+
raise PackError('Context or policy changed during native settings application')
|
|
1283
|
+
configured=lanes.acknowledge_context(configured,source['contextDigest'])
|
|
1284
|
+
self.execution.qualify_dynamic(configured,current)
|
|
1285
|
+
return store.write(configured,lanes.digest(hold))
|
|
1286
|
+
if operation=='lane-settle':
|
|
1287
|
+
verified=await self.dynamic_outcome(record)
|
|
1288
|
+
return store.write(lanes.settle(verified,native),lanes.digest(record))
|
|
1289
|
+
if operation=='lane-retire':
|
|
1290
|
+
if record['state']!='settled':
|
|
1291
|
+
raise PackError('Only a settled verified child can archive')
|
|
1292
|
+
# Archive through native API; preserve the durable possible-effect state.
|
|
1293
|
+
history=self.dynamic_history(record['nativeId'])
|
|
1294
|
+
hold={**record,'state':'uncertain-effect-hold'};store.write(hold,lanes.digest(record));changed=True
|
|
1295
|
+
await self.call_native('thread/archive',{'threadId':record['nativeId']})
|
|
1296
|
+
with closing(sqlite3.connect('file:'+str(pathlib.Path(self.runtime)/'home/state_5.sqlite')+'?mode=ro',uri=True)) as db:
|
|
1297
|
+
archived=db.execute('SELECT archived FROM threads WHERE id=?',(record['nativeId'],)).fetchone()
|
|
1298
|
+
if archived!=(1,):
|
|
1299
|
+
raise PackError('Native archive readback is unavailable; preserve uncertainty')
|
|
1300
|
+
if self.dynamic_history(record['nativeId'])!=history:
|
|
1301
|
+
raise PackError('Native archived transcript bytes differ; history preservation is unproven')
|
|
1302
|
+
retired=lanes.retire(record,{**native,'archived':True,'historyPreserved':True})
|
|
1303
|
+
self.approvals.retire_dynamic(record['nativeId']);self.execution.retire_dynamic(record['nativeId'])
|
|
1304
|
+
return store.write(retired,lanes.digest(hold))
|
|
1305
|
+
raise PackError('Unsupported closed lane operation')
|
|
1306
|
+
except BaseException:
|
|
1307
|
+
if changed and record and record['nativeId'] in self.execution.roles:
|
|
1308
|
+
self.execution.roles[record['nativeId']]['dynamicCurrentQualified']=False
|
|
1309
|
+
self.execution.verified.discard(record['nativeId'])
|
|
1310
|
+
raise
|
|
1311
|
+
finally:
|
|
1312
|
+
self._lane_configuration_record=None
|
|
1313
|
+
self.lane_busy=False
|
|
1314
|
+
|
|
792
1315
|
def status(self):
|
|
793
|
-
return {'pid':os.getpid(),'childPid':self.child.pid if self.child else None,'desktopPid':os.getppid(),'runtime':self.runtime,
|
|
1316
|
+
return {'pid':os.getpid(),'childPid':self.child.pid if self.child else None,'desktopPid':None if self.managed_transport else os.getppid(),'runtime':self.runtime,
|
|
1317
|
+
'nativeTransport':'existing-managed-window' if self.managed_transport else 'owned-stdio',
|
|
1318
|
+
'managedTargetId':self.managed_transport.target_id if self.managed_transport else None,
|
|
794
1319
|
'quiesced':self.quiesced,'initialized':self.initialized,
|
|
795
1320
|
'activeTurns':sorted(self.active_turns),'pendingRequests':len(self.pending),
|
|
796
1321
|
'executionPolicy':self.execution.status() if self.execution else {'state':'not-configured'},
|
|
@@ -799,17 +1324,53 @@ class BackendProxy:
|
|
|
799
1324
|
async def control(self,reader,writer):
|
|
800
1325
|
try:
|
|
801
1326
|
message=json.loads(await asyncio.wait_for(reader.readline(),5))
|
|
802
|
-
if not secrets.compare_digest(message.get('token',''),self.token):
|
|
1327
|
+
if not secrets.compare_digest(message.get('token',''),self.token):
|
|
1328
|
+
raise PackError('Live bridge authentication failed')
|
|
803
1329
|
op=message.get('operation')
|
|
1330
|
+
if self.lane_connection_binding:
|
|
1331
|
+
scoped=self.lane_connection_binding;params=message.get('params') or {}
|
|
1332
|
+
if op.startswith('lane-'):
|
|
1333
|
+
lane=params.get('request',params.get('plan',params)).get('laneId')
|
|
1334
|
+
if lane!=scoped['laneId']:
|
|
1335
|
+
raise PackError('Operation is outside the exact owned lane connection')
|
|
1336
|
+
if op=='lane-plan' and params['request']['sourceRoleKey']!=scoped['sourceRoleKey']:
|
|
1337
|
+
raise PackError('Source role differs from the connection binding')
|
|
1338
|
+
elif op in {'approval-details','approval-respond'}:
|
|
1339
|
+
entry=self.approvals.live(params['key'],params['intakeThreadId'],params['fingerprint'])
|
|
1340
|
+
role=self.execution.roles.get(entry['record']['sourceThreadId'],{})
|
|
1341
|
+
if role.get('dynamicLaneId')!=scoped['laneId']:
|
|
1342
|
+
raise PackError('Approval is outside the exact owned lane')
|
|
1343
|
+
elif op not in {'status','approval-list'}:
|
|
1344
|
+
raise PackError('Operation is outside the foreground lane connection')
|
|
804
1345
|
if op=='quiesce':
|
|
805
1346
|
if self.permission_configuration:
|
|
806
1347
|
raise PackError('Permission configuration in progress; preserve the owning native connection')
|
|
807
|
-
if self.active_turns:
|
|
808
|
-
|
|
1348
|
+
if self.active_turns:
|
|
1349
|
+
raise PackError('Outgoing agents have active turns. Wait for their current turns; Codex can stay open.')
|
|
1350
|
+
if self.pending:
|
|
1351
|
+
raise PackError('Backend requests are in flight. Retry the switch; Codex can stay open.')
|
|
809
1352
|
self.quiesced=True
|
|
810
1353
|
await self.stop_child()
|
|
811
1354
|
elif op=='resume':
|
|
812
|
-
if self.quiesced:
|
|
1355
|
+
if self.quiesced:
|
|
1356
|
+
raise PackError('Use the desktop reconnect action to resume a quiesced bridge')
|
|
1357
|
+
elif op in {'lane-context-start','lane-context-read'}:
|
|
1358
|
+
result=await self.dynamic_context(op,message.get('params') or {})
|
|
1359
|
+
elif op in {'lane-fixture-start','lane-fixture-read'}:
|
|
1360
|
+
result=await self.dynamic_fixture(op,message.get('params') or {})
|
|
1361
|
+
elif op=='lane-create':
|
|
1362
|
+
result=await self.dynamic_create(message.get('params') or {})
|
|
1363
|
+
elif op=='lane-close':
|
|
1364
|
+
from .lanes import Registry
|
|
1365
|
+
params=message.get('params') or {}
|
|
1366
|
+
if set(params)!={'laneId'} or not self.lane_connection_binding:
|
|
1367
|
+
raise PackError('Explicit foreground own-lane close required')
|
|
1368
|
+
record=Registry(self.lib.root,'codex').read(params['laneId'])
|
|
1369
|
+
if not record or record['state']!='retired' or self.active_turns or self.pending or self.notifications or self.approvals.pending:
|
|
1370
|
+
raise PackError('Own lane must be retired with no pending native work before normal connection closure')
|
|
1371
|
+
self.lane_close_event.set();result={'state':'normal-own-connection-close-requested'}
|
|
1372
|
+
elif op in {'lane-plan','lane-adopt','lane-configure','lane-read','lane-settle','lane-retire'}:
|
|
1373
|
+
result=await self.dynamic_lane(op,message.get('params') or {})
|
|
813
1374
|
elif op=='owned-roster-read':
|
|
814
1375
|
result=await self.owned_roster_read(message.get('params') or {})
|
|
815
1376
|
elif op in {'smoke-start','smoke-read'}:
|
|
@@ -836,10 +1397,15 @@ class BackendProxy:
|
|
|
836
1397
|
await self.qualify_organization(entry['record']['sourceThreadId'])
|
|
837
1398
|
self.approvals.live(params['key'],params['intakeThreadId'],params['fingerprint'])
|
|
838
1399
|
self.approvals.sending(reply)
|
|
839
|
-
|
|
1400
|
+
await self.send_native_approval(entry,reply);self.approvals.settled(reply)
|
|
1401
|
+
decision=reply['result'].get('decision')
|
|
1402
|
+
if decision in {'accept','decline','cancel'}:
|
|
1403
|
+
self.dynamic_permission_event(entry['record'],'approved' if decision=='accept' else 'denied')
|
|
840
1404
|
result={'state':'native-response-forwarded','operationEffectVerified':False}
|
|
841
|
-
elif op!='status':
|
|
842
|
-
|
|
1405
|
+
elif op!='status':
|
|
1406
|
+
raise PackError('Unknown live bridge operation')
|
|
1407
|
+
if op not in {'lane-context-start','lane-context-read','lane-fixture-start','lane-fixture-read','lane-create','lane-close','lane-plan','lane-adopt','lane-configure','lane-read','lane-settle','lane-retire','approval-list','approval-details','approval-respond','permissions-configure','owned-roster-read','smoke-start','smoke-read'}:
|
|
1408
|
+
result=self.status()
|
|
843
1409
|
except Exception as exc: result={'error':str(exc)}
|
|
844
1410
|
writer.write(canonical(result)+b'\n');await writer.drain();writer.close();await writer.wait_closed()
|
|
845
1411
|
|
|
@@ -869,6 +1435,250 @@ class BackendProxy:
|
|
|
869
1435
|
if record.get('pid')==os.getpid():
|
|
870
1436
|
(self.lib.root/'live-bridge.json').unlink(missing_ok=True);self.path.unlink(missing_ok=True)
|
|
871
1437
|
|
|
1438
|
+
class ManagedNativeTransport:
|
|
1439
|
+
"""One captured, current desktop manager owns every native lane effect."""
|
|
1440
|
+
def __init__(self,lib,runtime,lane_id):
|
|
1441
|
+
self.lib=lib;self.runtime=runtime;self.lane_id=lane_id;self.token=secrets.token_hex(32)
|
|
1442
|
+
self.ws=None;self.events_ws=None;self.ready=False;self.closed=False;self.subscriptions_closed=False;self.lock=asyncio.Lock()
|
|
1443
|
+
self.scope=None;self.target_id=None
|
|
1444
|
+
|
|
1445
|
+
async def open(self,planned):
|
|
1446
|
+
self.scope=await asyncio.to_thread(managed_lane_frontend,self.lib,self.runtime,planned)
|
|
1447
|
+
self.ws,target=await asyncio.to_thread(renderer,self.lib)
|
|
1448
|
+
self.events_ws,event_target=await asyncio.to_thread(renderer,self.lib)
|
|
1449
|
+
if target!=self.scope['targetId'] or event_target!=target:
|
|
1450
|
+
raise PackError('Existing managed renderer identity changed before connection')
|
|
1451
|
+
self.target_id=target
|
|
1452
|
+
await self.evaluate('open',{'homes':[self.scope['home']],'source':planned['role'],'intake':planned['intake'],
|
|
1453
|
+
'adapter':self.scope['capability']})
|
|
1454
|
+
self.ready=True
|
|
1455
|
+
|
|
1456
|
+
def expression(self,action,value):
|
|
1457
|
+
params={'action':action,'value':value,'laneId':self.lane_id,'token':self.token}
|
|
1458
|
+
return """(async()=>{
|
|
1459
|
+
const p=PARAMS,key=Symbol.for('topolo.agent-pack.managed-native.v1');
|
|
1460
|
+
let registry=window[key];
|
|
1461
|
+
if(p.action==='open'){
|
|
1462
|
+
window[key]??=new Map();registry=window[key];
|
|
1463
|
+
if(registry.has(p.laneId))throw new Error('Owned managed connection exists; no replay or takeover');
|
|
1464
|
+
const v=p.value,services=(await import(v.adapter.module))[v.adapter.export];
|
|
1465
|
+
const opened=await services?.appServerManagers?.open('local');
|
|
1466
|
+
if(opened?.status!=='ready')throw new Error('Existing managed native backend unavailable');
|
|
1467
|
+
const manager=opened.manager,home=await manager.getCodexHome();
|
|
1468
|
+
if(!v.homes.includes(home))throw new Error('Managed native home differs');
|
|
1469
|
+
const state={manager,home,source:v.source,intake:v.intake,token:p.token,childId:null,creationReserved:false,
|
|
1470
|
+
queue:[],waiters:[],subscriptions:[],closed:false};
|
|
1471
|
+
registry.set(p.laneId,state);
|
|
1472
|
+
const push=value=>{if(state.closed)return;if(state.queue.length>=1024){state.closed=true;return;}
|
|
1473
|
+
state.queue.push(value);state.waiters.splice(0).forEach(f=>f());};
|
|
1474
|
+
state.subscriptions.push(await manager.subscribe({type:'event',key:{hostId:'local'},event:'approval-request',listener:async event=>{
|
|
1475
|
+
if(event.conversationId!==state.childId)return;
|
|
1476
|
+
const conversation=await manager.getConversation(state.childId);
|
|
1477
|
+
const request=conversation?.requests?.find(r=>r.id===event.requestId);
|
|
1478
|
+
if(request&&request.params?.threadId===state.childId&&['item/fileChange/requestApproval','item/commandExecution/requestApproval'].includes(request.method))
|
|
1479
|
+
push(request);
|
|
1480
|
+
else push({method:'topolo/managed-request-unavailable',params:{threadId:state.childId}});
|
|
1481
|
+
}}));
|
|
1482
|
+
state.subscriptions.push(await manager.subscribe({type:'notification',key:{hostId:'local'},
|
|
1483
|
+
methods:['turn/started','turn/completed','thread/status/changed'],listener:notification=>{
|
|
1484
|
+
if([state.childId,state.intake.nativeId].includes(notification.params?.threadId))push(notification);
|
|
1485
|
+
}}));
|
|
1486
|
+
return {state:'managed-owning-connection-ready',home};
|
|
1487
|
+
}
|
|
1488
|
+
const s=registry?.get(p.laneId);
|
|
1489
|
+
if(p.action==='close'){
|
|
1490
|
+
if(s&&s.token!==p.token)throw new Error('Foreign managed native connection must be preserved');
|
|
1491
|
+
if(s){s.closed=true;s.waiters.splice(0).forEach(f=>f());
|
|
1492
|
+
for(const subscription of s.subscriptions)await subscription.unsubscribe();registry.delete(p.laneId);}
|
|
1493
|
+
return {state:'normal-own-managed-subscriptions-closed',nativeOwnerStopped:false};
|
|
1494
|
+
}
|
|
1495
|
+
if(!s||s.token!==p.token||s.closed)throw new Error('Original managed native connection unavailable; no fallback');
|
|
1496
|
+
if(await s.manager.getCodexHome()!==s.home)throw new Error('Captured managed native instance/home changed');
|
|
1497
|
+
if(p.action==='request'){
|
|
1498
|
+
const {method,params}=p.value;
|
|
1499
|
+
const allowed=['model/list','thread/read','thread/start','thread/resume','thread/archive','turn/start'];
|
|
1500
|
+
if(!allowed.includes(method))throw new Error('Unsupported closed managed native method');
|
|
1501
|
+
const id=params.threadId;
|
|
1502
|
+
if(method==='thread/start'){
|
|
1503
|
+
if(s.creationReserved||params.cwd!==s.source.cwd)throw new Error('Managed native creation is reserved or placement differs');
|
|
1504
|
+
s.creationReserved=true;
|
|
1505
|
+
}else if(method!=='model/list'){
|
|
1506
|
+
if(id!==s.childId&&!(method==='thread/read'&&[s.source.nativeId,s.intake.nativeId].includes(id))&&
|
|
1507
|
+
!(method==='turn/start'&&id===s.intake.nativeId))throw new Error('Managed native request is outside the own child/intake');
|
|
1508
|
+
if(method==='turn/start'&&id===s.intake.nativeId&&Object.keys(params).some(k=>!['threadId','input'].includes(k)))
|
|
1509
|
+
throw new Error('Original intake settings must remain unchanged');
|
|
1510
|
+
}
|
|
1511
|
+
const result=await s.manager.sendRequest(method,params);
|
|
1512
|
+
if(method==='thread/start'){
|
|
1513
|
+
const id=result?.thread?.id;
|
|
1514
|
+
if(typeof id!=='string'||[s.source.nativeId,s.intake.nativeId].includes(id))throw new Error('Fresh managed native identity unavailable');
|
|
1515
|
+
s.childId=id;
|
|
1516
|
+
}
|
|
1517
|
+
return result;
|
|
1518
|
+
}
|
|
1519
|
+
if(p.action==='approval'){
|
|
1520
|
+
const {request,reply}=p.value;
|
|
1521
|
+
if(request.params?.threadId!==s.childId||request.id!==reply.id||!['accept','decline','cancel'].includes(reply.result?.decision))
|
|
1522
|
+
throw new Error('Exact original own-child native approval required');
|
|
1523
|
+
const current=await s.manager.getConversation(s.childId);
|
|
1524
|
+
const actual=current?.requests?.find(r=>r.id===request.id);
|
|
1525
|
+
const canonical=v=>JSON.stringify(v,(_k,x)=>x&&typeof x==='object'&&!Array.isArray(x)?Object.fromEntries(Object.keys(x).sort().map(k=>[k,x[k]])):x);
|
|
1526
|
+
if(!actual||canonical(actual)!==canonical(request))throw new Error('Actual native pending request body changed');
|
|
1527
|
+
if(request.method==='item/fileChange/requestApproval')await s.manager.replyWithFileChangeApprovalDecision(s.childId,request.id,reply.result.decision);
|
|
1528
|
+
else if(request.method==='item/commandExecution/requestApproval')await s.manager.replyWithCommandExecutionApprovalDecision(s.childId,request.id,reply.result.decision);
|
|
1529
|
+
else throw new Error('Unsupported own-child native permission decision');
|
|
1530
|
+
const after=await s.manager.getConversation(s.childId);
|
|
1531
|
+
if(after?.requests?.some(r=>r.id===request.id))throw new Error('Native decision forwarding is not acknowledged; preserve uncertainty');
|
|
1532
|
+
return {state:'native-response-forwarded',operationEffectVerified:false};
|
|
1533
|
+
}
|
|
1534
|
+
if(p.action==='next'){
|
|
1535
|
+
if(!s.queue.length)await new Promise(resolve=>{const wake=()=>{clearTimeout(timer);resolve();};
|
|
1536
|
+
const timer=setTimeout(()=>{s.waiters=s.waiters.filter(f=>f!==wake);resolve();},10000);s.waiters.push(wake);});
|
|
1537
|
+
if(s.closed)throw new Error('Managed own native event custody was lost');
|
|
1538
|
+
return s.queue.splice(0);
|
|
1539
|
+
}
|
|
1540
|
+
throw new Error('Unsupported closed managed transport operation');
|
|
1541
|
+
})()""".replace('PARAMS',json.dumps(params),1)
|
|
1542
|
+
|
|
1543
|
+
async def evaluate(self,action,value,events=False):
|
|
1544
|
+
ws=self.events_ws if events else self.ws
|
|
1545
|
+
if not ws or self.closed:
|
|
1546
|
+
raise PackError('Owned managed transport is unavailable')
|
|
1547
|
+
if action!='close' and managed_backend_generation(self.lib,self.runtime)!=self.scope['proxyGeneration']:
|
|
1548
|
+
raise PackError('Original selected native execution owner changed; no retargeting')
|
|
1549
|
+
if events:
|
|
1550
|
+
return await asyncio.to_thread(ws.evaluate,self.expression(action,value))
|
|
1551
|
+
async with self.lock:return await asyncio.to_thread(ws.evaluate,self.expression(action,value))
|
|
1552
|
+
|
|
1553
|
+
async def close(self):
|
|
1554
|
+
try:
|
|
1555
|
+
if self.ws and not self.closed:
|
|
1556
|
+
result=await self.evaluate('close',{})
|
|
1557
|
+
self.subscriptions_closed=result.get('state')=='normal-own-managed-subscriptions-closed'
|
|
1558
|
+
finally:
|
|
1559
|
+
self.closed=True;self.ready=False
|
|
1560
|
+
for ws in [self.ws,self.events_ws]:
|
|
1561
|
+
if ws:
|
|
1562
|
+
ws.close()
|
|
1563
|
+
|
|
1564
|
+
|
|
1565
|
+
class OwnedLaneConnection:
|
|
1566
|
+
"""Explicit, foreground native stdio connection for one reviewed new lane.
|
|
1567
|
+
|
|
1568
|
+
The existing window manager owns creation, settings, turns and native approval
|
|
1569
|
+
responses. No replacement app-server or unrelated profile is started.
|
|
1570
|
+
"""
|
|
1571
|
+
def __init__(self,lib,connection_id,binding):
|
|
1572
|
+
from .storage import safe_id
|
|
1573
|
+
self.lib=lib;self.connection_id=safe_id(connection_id);self.binding=binding
|
|
1574
|
+
fields={'format','schemaVersion','laneId','runtime','binary','binaryDigest','protocolDigest','sourceRoleKey','intakeKey','transport'}
|
|
1575
|
+
if (not isinstance(binding,dict) or set(binding)!=fields or binding['format']!='topolo-owned-lane-connection' or
|
|
1576
|
+
binding['schemaVersion']!=1 or binding['transport']!='codex-managed-window' or binding['laneId']!=self.connection_id or
|
|
1577
|
+
str(lib.active().get('runtime'))!=binding['runtime'] or not pathlib.Path(binding['binary']).is_absolute() or
|
|
1578
|
+
'/intakes/' not in binding['intakeKey']):
|
|
1579
|
+
raise PackError('Explicit exact owned lane connection binding required')
|
|
1580
|
+
binary=pathlib.Path(binding['binary']);info=binary.lstat()
|
|
1581
|
+
if binary.is_symlink() or not binary.is_file() or info.st_uid not in {0,os.getuid()} or info.st_mode&0o022:
|
|
1582
|
+
raise PackError('Native binary must be a regular qualified installed executable')
|
|
1583
|
+
if hashlib.sha256(binary.read_bytes()).hexdigest()!=binding['binaryDigest']:
|
|
1584
|
+
raise PackError('Native binary differs from the reviewed connection binding')
|
|
1585
|
+
self.protocol=qualify_protocol(str(binary),lane_creation=True)
|
|
1586
|
+
from .lanes import digest
|
|
1587
|
+
if digest(self.protocol)!=binding['protocolDigest']:
|
|
1588
|
+
raise PackError('Native protocol differs from the reviewed connection binding')
|
|
1589
|
+
self.directory=lib.root/'dynamic-connections'/self.connection_id
|
|
1590
|
+
self.proxy=BackendProxy(lib,str(binary),['app-server']);self.proxy.emit_native=False
|
|
1591
|
+
self.proxy.lane_connection_binding=binding
|
|
1592
|
+
self.proxy.lane_close_event=asyncio.Event()
|
|
1593
|
+
|
|
1594
|
+
async def run(self):
|
|
1595
|
+
directory=self.directory
|
|
1596
|
+
active=self.lib.active();self.proxy.runtime=self.binding['runtime']
|
|
1597
|
+
self.proxy.execution=load_gate(self.proxy.runtime) or ExecutionGate(prepare_runtime(self.lib,active['components'],self.proxy.runtime))
|
|
1598
|
+
self.proxy.approvals=ApprovalRouter(pathlib.Path(self.proxy.runtime)/'approval-inbox',list(self.proxy.execution.roles.values()),uuid.uuid4().hex)
|
|
1599
|
+
source=[r for r in self.proxy.execution.roles.values() if r['key']==self.binding['sourceRoleKey']]
|
|
1600
|
+
intake=[r for r in self.proxy.execution.roles.values() if r['key']==self.binding['intakeKey']]
|
|
1601
|
+
if len(source)!=1 or len(intake)!=1 or source[0]['intakeKey']!=intake[0]['key']:
|
|
1602
|
+
raise PackError('Owned managed connection has no exact original source and intake route')
|
|
1603
|
+
planned={name:{'nativeId':row['threadId'],'cwd':row['cwd']} for name,row in [('role',source[0]),('intake',intake[0])]}
|
|
1604
|
+
transport=ManagedNativeTransport(self.lib,self.proxy.runtime,self.connection_id)
|
|
1605
|
+
self.proxy.managed_transport=transport;self.proxy.protocol=self.protocol
|
|
1606
|
+
server=None;stop=asyncio.Event();loop=asyncio.get_running_loop();event_task=None;created=False
|
|
1607
|
+
for sig in [signal.SIGINT,signal.SIGTERM]:loop.add_signal_handler(sig,stop.set)
|
|
1608
|
+
try:
|
|
1609
|
+
# A read-only preflight precedes durable custody for native subscriptions.
|
|
1610
|
+
preflight=await asyncio.to_thread(managed_lane_frontend,self.lib,self.proxy.runtime,planned)
|
|
1611
|
+
directory.parent.mkdir(mode=0o700,exist_ok=True);OwnedSmoke.owned(directory.parent,directory=True)
|
|
1612
|
+
directory.mkdir(mode=0o700);created=True;OwnedSmoke.owned(directory,directory=True)
|
|
1613
|
+
atomic_json(directory/'managed-custody.json',{'state':'native-subscriptions-may-follow','laneId':self.connection_id,
|
|
1614
|
+
'token':transport.token,'targetId':preflight['targetId'],'runtime':self.proxy.runtime,'binding':self.binding})
|
|
1615
|
+
await transport.open(planned)
|
|
1616
|
+
self.proxy.path=lane_socket_path(self.lib,self.connection_id)
|
|
1617
|
+
if self.proxy.path.exists() or self.proxy.path.is_symlink():
|
|
1618
|
+
raise PackError('Own managed control path is occupied; no takeover')
|
|
1619
|
+
self.proxy.initialized=True
|
|
1620
|
+
self.proxy.execution.set_catalog(await self.proxy.read_catalog());self.proxy.catalog_ready.set()
|
|
1621
|
+
server=await asyncio.start_unix_server(self.proxy.control,path=str(self.proxy.path),limit=1024*1024)
|
|
1622
|
+
self.proxy.path.chmod(0o600)
|
|
1623
|
+
atomic_json(directory/'connection.json',{'pid':os.getpid(),'socket':str(self.proxy.path),'token':self.proxy.token,
|
|
1624
|
+
'connectionId':self.connection_id,'generation':self.proxy.approvals.generation,'binding':self.binding,
|
|
1625
|
+
'nativeTransport':'existing-managed-window','managedTargetId':transport.target_id})
|
|
1626
|
+
print(json.dumps({'state':'owned-managed-native-lane-connection-ready','connectionId':self.connection_id,'pid':os.getpid(),
|
|
1627
|
+
'managedTargetId':transport.target_id,'modelTurnsStarted':False,'generation':self.proxy.approvals.generation}),flush=True)
|
|
1628
|
+
event_task=asyncio.create_task(self.proxy.managed_native_events())
|
|
1629
|
+
stopped=asyncio.create_task(stop.wait());normal=asyncio.create_task(self.proxy.lane_close_event.wait())
|
|
1630
|
+
done,pending=await asyncio.wait({stopped,normal,event_task},return_when=asyncio.FIRST_COMPLETED)
|
|
1631
|
+
for task in pending:task.cancel()
|
|
1632
|
+
if event_task in done:
|
|
1633
|
+
await event_task
|
|
1634
|
+
finally:
|
|
1635
|
+
await self.close_owned_controls(transport,server,event_task,created)
|
|
1636
|
+
|
|
1637
|
+
async def close_owned_controls(self,transport,server,event_task,created):
|
|
1638
|
+
directory=self.directory
|
|
1639
|
+
try:await transport.close()
|
|
1640
|
+
finally:
|
|
1641
|
+
if event_task:
|
|
1642
|
+
event_task.cancel()
|
|
1643
|
+
if server:
|
|
1644
|
+
server.close();await server.wait_closed()
|
|
1645
|
+
if created:
|
|
1646
|
+
pending=self.proxy.approvals.records()
|
|
1647
|
+
normal=self.proxy.lane_close_event.is_set() and transport.subscriptions_closed and not self.proxy.active_turns and not any(r.get('state')=='pending' for r in pending)
|
|
1648
|
+
atomic_json(directory/'closure.json',{'state':'closed' if normal else 'closed-with-unresolved-work',
|
|
1649
|
+
'normalLifecycleClosureVerified':normal,'nativeTransport':'existing-managed-window',
|
|
1650
|
+
'nativeOwnerStopped':False,'ownSubscriptionsClosed':transport.subscriptions_closed,
|
|
1651
|
+
'pendingNativeApprovals':sum(r.get('state')=='pending' for r in pending),'replayAuthorized':False})
|
|
1652
|
+
(directory/'connection.json').unlink(missing_ok=True)
|
|
1653
|
+
if server:
|
|
1654
|
+
self.proxy.path.unlink(missing_ok=True)
|
|
1655
|
+
|
|
1656
|
+
|
|
1657
|
+
def lane_socket_path(lib,connection_id):
|
|
1658
|
+
# A fixed short path fits macOS AF_UNIX even with a long library/lane name.
|
|
1659
|
+
# The authenticated socket is mode0600; an existing path is never taken over.
|
|
1660
|
+
identity=hashlib.sha256(canonical([str(lib.root),connection_id])).hexdigest()[:24]
|
|
1661
|
+
return pathlib.Path('/tmp')/('topolo-lane-'+str(os.getuid())+'-'+identity+'.sock')
|
|
1662
|
+
|
|
1663
|
+
|
|
1664
|
+
def request_lane(lib,connection_id,operation,params):
|
|
1665
|
+
from .storage import safe_id
|
|
1666
|
+
directory=lib.root/'dynamic-connections'/safe_id(connection_id)
|
|
1667
|
+
OwnedSmoke.owned(directory,directory=True);OwnedSmoke.owned(directory/'connection.json')
|
|
1668
|
+
record=read_json(directory/'connection.json')
|
|
1669
|
+
if record.get('connectionId')!=connection_id or record.get('socket')!=str(lane_socket_path(lib,connection_id)):
|
|
1670
|
+
raise PackError('Owned lane connection receipt differs')
|
|
1671
|
+
with socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) as connection:
|
|
1672
|
+
connection.settimeout(240);connection.connect(record['socket'])
|
|
1673
|
+
connection.sendall(canonical({'token':record['token'],'operation':operation,'params':params})+b'\n')
|
|
1674
|
+
with connection.makefile('rb') as stream:line=stream.readline(1024*1024)
|
|
1675
|
+
if not line:
|
|
1676
|
+
raise PackError('Owned lane connection closed without ACK; reconcile without replay')
|
|
1677
|
+
result=json.loads(line)
|
|
1678
|
+
if 'error' in result:
|
|
1679
|
+
raise PackError(result['error'])
|
|
1680
|
+
return result
|
|
1681
|
+
|
|
872
1682
|
def proxy_main():
|
|
873
1683
|
lib=Library(os.environ['CODEX_PACK_HOME']);real=os.environ['CODEX_PACK_REAL_CLI']
|
|
874
1684
|
args=sys.argv[1:]
|
|
@@ -976,6 +1786,94 @@ def thread_ids(runtime):
|
|
|
976
1786
|
def rendered_threads(ws):
|
|
977
1787
|
return ws.evaluate('Array.from(document.querySelectorAll("[data-app-action-sidebar-thread-row][data-app-action-sidebar-thread-host-id=local]")).map(e=>({id:e.getAttribute("data-app-action-sidebar-thread-id").replace(/^local:/,""),title:e.getAttribute("data-app-action-sidebar-thread-title")}))')
|
|
978
1788
|
|
|
1789
|
+
|
|
1790
|
+
def managed_backend_generation(lib,runtime):
|
|
1791
|
+
"""Authenticate the existing proxy; a manifest or symlink is not liveness."""
|
|
1792
|
+
active=lib.active()
|
|
1793
|
+
if active.get('runtime')!=str(runtime):
|
|
1794
|
+
raise PackError('Selected native runtime changed')
|
|
1795
|
+
status=request(lib,'status')
|
|
1796
|
+
if (not isinstance(status,dict) or status.get('runtime')!=str(runtime) or status.get('initialized') is not True or
|
|
1797
|
+
status.get('quiesced') is not False or any(type(status.get(k)) is not int or status[k]<1 for k in ['pid','childPid'])):
|
|
1798
|
+
raise PackError('Current authenticated selected backend is unavailable')
|
|
1799
|
+
return {k:status[k] for k in ['pid','childPid','runtime']}
|
|
1800
|
+
|
|
1801
|
+
|
|
1802
|
+
def managed_lane_frontend(lib,runtime,planned,native_id=None,app='/Applications/ChatGPT.app'):
|
|
1803
|
+
"""Use only the current managed window's exported native manager/catalog.
|
|
1804
|
+
|
|
1805
|
+
A separate selected-home app-server cannot assert managed visibility. The
|
|
1806
|
+
existing manager must already own the selected storage before any refresh.
|
|
1807
|
+
This function never reconnects, changes home, restarts or creates a worker.
|
|
1808
|
+
"""
|
|
1809
|
+
from . import lanes
|
|
1810
|
+
root=pathlib.Path(runtime);home=root/'home'
|
|
1811
|
+
if not home.is_dir():
|
|
1812
|
+
raise PackError('Selected owned native home is unavailable')
|
|
1813
|
+
generation=managed_backend_generation(lib,str(root))
|
|
1814
|
+
expected=[str(home)]
|
|
1815
|
+
layout=lib.root/'desktop-layout.json';stable=lib.root/'desktop-home';layout_hash=None
|
|
1816
|
+
if layout.exists():
|
|
1817
|
+
OwnedSmoke.owned(layout)
|
|
1818
|
+
if read_json(layout).get('owner') not in {'topolo-agent-pack','codex-pack'}:
|
|
1819
|
+
raise PackError('Stable app-host layout is not owned by this pack')
|
|
1820
|
+
OwnedSmoke.owned(stable,directory=True)
|
|
1821
|
+
expected.append(str(stable));layout_hash=hashlib.sha256(layout.read_bytes()).hexdigest()
|
|
1822
|
+
ws,target=renderer(lib)
|
|
1823
|
+
try:
|
|
1824
|
+
capability=adapter(app)
|
|
1825
|
+
params={'adapter':capability,'homes':expected,'configFile':str(home/'config.toml'),
|
|
1826
|
+
'roles':[planned['role'],planned['intake']],'nativeId':native_id}
|
|
1827
|
+
expression="""(async()=>{
|
|
1828
|
+
const p=PARAMS,services=(await import(p.adapter.module))[p.adapter.export];
|
|
1829
|
+
if(!services?.appServerManagers||!services.localThreadCatalog)throw new Error('Managed native manager/catalog unavailable; no fallback');
|
|
1830
|
+
const opened=await services.appServerManagers.open('local');
|
|
1831
|
+
if(opened?.status!=='ready'||!opened.manager)throw new Error('Existing managed local backend is unavailable');
|
|
1832
|
+
const manager=opened.manager,home=await manager.getCodexHome();
|
|
1833
|
+
if(typeof home!=='string'||!p.homes.includes(home))throw new Error('Existing managed backend is outside the selected owned home');
|
|
1834
|
+
const provenance=await manager.sendRequest('config/read',{cwd:p.roles[0].cwd,includeLayers:true});
|
|
1835
|
+
const users=provenance?.layers?.filter(layer=>layer.name?.type==='user'&&!layer.disabledReason);
|
|
1836
|
+
if(!Array.isArray(users)||!users.length||users.some(layer=>layer.name.file!==p.configFile||typeof layer.version!=='string'||!layer.version))
|
|
1837
|
+
throw new Error('Actual native execution-home config provenance differs');
|
|
1838
|
+
const configOrigins=users.map(layer=>({name:layer.name,version:layer.version}));
|
|
1839
|
+
for(const role of p.roles){const value=await manager.readThread(role.nativeId,{includeTurns:false}),thread=value?.thread;
|
|
1840
|
+
if(thread?.id!==role.nativeId||thread.cwd!==role.cwd||thread.archived===true)throw new Error('Managed backend cannot read exact fixed source/intake');}
|
|
1841
|
+
if(p.nativeId===null)return {home,configOrigins,sourceIds:p.roles.map(r=>r.nativeId),nativeId:null,state:'managed-preflight-read-verified'};
|
|
1842
|
+
if(p.roles.some(r=>r.nativeId===p.nativeId))throw new Error('Cannot register a fixed source/intake as a new lane');
|
|
1843
|
+
const value=await manager.readThread(p.nativeId,{includeTurns:false}),thread=value?.thread;
|
|
1844
|
+
if(thread?.id!==p.nativeId||thread.cwd!==p.roles[0].cwd||thread.archived===true)throw new Error('Managed native own-child read differs');
|
|
1845
|
+
let cursor=null,listed=false;
|
|
1846
|
+
for(let page=0;page<20;page++){
|
|
1847
|
+
const result=await manager.sendRequest('thread/list',{cursor,limit:100,archived:false,modelProviders:[],useStateDbOnly:true});
|
|
1848
|
+
if(!Array.isArray(result?.data))throw new Error('Managed native thread-list response unavailable');
|
|
1849
|
+
if(result.data.some(t=>t.id===p.nativeId)){listed=true;break;}
|
|
1850
|
+
cursor=result.nextCursor;if(cursor===null||cursor===undefined)break;
|
|
1851
|
+
}
|
|
1852
|
+
if(!listed)throw new Error('Exact own child is absent from the managed native list');
|
|
1853
|
+
const catalog=services.localThreadCatalog;
|
|
1854
|
+
await catalog.notifyThread({hostId:'local',threadId:p.nativeId},'upsert');
|
|
1855
|
+
await catalog.requestSync(['local'],'immediate');
|
|
1856
|
+
const entries=await catalog.readEntries([{hostId:'local',threadId:p.nativeId}]);
|
|
1857
|
+
if(!Array.isArray(entries)||entries.length!==1||entries[0].threadId!==p.nativeId||entries[0].hostId!=='local')
|
|
1858
|
+
throw new Error('Managed own-child catalog registration was not acknowledged');
|
|
1859
|
+
if(await manager.getCodexHome()!==home)throw new Error('Managed backend home changed during native registration');
|
|
1860
|
+
return {state:'managed-native-read-list-catalog-verified',home,configOrigins,nativeId:p.nativeId,sourceIds:p.roles.map(r=>r.nativeId),
|
|
1861
|
+
nativeRead:true,nativeList:true,catalogRead:true,rendered:!!document.querySelector('[data-app-action-sidebar-thread-row][data-app-action-sidebar-thread-id="local:'+CSS.escape(p.nativeId)+'"]')};
|
|
1862
|
+
})()""".replace('PARAMS',json.dumps(params),1)
|
|
1863
|
+
result=ws.evaluate(expression)
|
|
1864
|
+
if (not isinstance(result,dict) or result.get('home') not in expected or result.get('nativeId')!=native_id or
|
|
1865
|
+
result.get('sourceIds')!=[r['nativeId'] for r in params['roles']]):
|
|
1866
|
+
raise PackError('Exact existing managed backend acknowledgement differs')
|
|
1867
|
+
if native_id is not None and (result.get('state')!='managed-native-read-list-catalog-verified' or
|
|
1868
|
+
any(result.get(k) is not True for k in ['nativeRead','nativeList','catalogRead'])):
|
|
1869
|
+
raise PackError('Managed native child read/list/catalog proof is incomplete')
|
|
1870
|
+
if managed_backend_generation(lib,str(root))!=generation:
|
|
1871
|
+
raise PackError('Selected native backend generation changed during managed proof')
|
|
1872
|
+
if layout_hash is not None and hashlib.sha256(layout.read_bytes()).hexdigest()!=layout_hash:
|
|
1873
|
+
raise PackError('Stable owned app-host layout changed during managed proof')
|
|
1874
|
+
return {**result,'targetId':target,'selectedRuntime':str(root),'capability':capability,'proxyGeneration':generation}
|
|
1875
|
+
finally:ws.close()
|
|
1876
|
+
|
|
979
1877
|
def checkpoint_ui(lib,runtime):
|
|
980
1878
|
ws,_=renderer(lib)
|
|
981
1879
|
try:
|