@topolo/cli 0.13.17 → 0.13.21
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 +18 -4
- package/agent-packs/README.md +469 -1
- package/agent-packs/coverage_changed.py +1 -1
- package/agent-packs/docs/selected-artifacts.md +47 -0
- package/agent-packs/documentation/cloud-task-state.md +56 -0
- package/agent-packs/domain-contexts/README.md +25 -0
- package/agent-packs/domain-contexts/cicd.json +8 -0
- package/agent-packs/gui/feed-adapter.js +4 -0
- package/agent-packs/gui/index.html +9 -0
- package/agent-packs/gui/studio.css +14 -0
- package/agent-packs/gui/studio.js +121 -0
- package/agent-packs/gui/studio_server.py +355 -0
- package/agent-packs/roles/cicd/CLAUDE.md +24 -0
- package/agent-packs/specializations/cicd--cicd.json +5 -0
- package/agent-packs/specializations/cicd--pipeline-engineering.json +5 -0
- package/agent-packs/specializations/cicd--promotion-deployment.json +5 -0
- package/agent-packs/specializations/cicd--queue-recovery.json +5 -0
- package/agent-packs/specializations/cicd--release-verification.json +5 -0
- package/agent-packs/specializations/cicd--runner-capacity.json +5 -0
- package/agent-packs/specializations/intakes--engineering-02.json +5 -0
- package/agent-packs/specializations/intakes--engineering-03.json +5 -0
- package/agent-packs/specializations/intakes--engineering.json +5 -0
- package/agent-packs/specializations/intakes--operator-01.json +5 -0
- package/agent-packs/specializations/intakes--operator-02.json +5 -0
- package/agent-packs/specializations/operator-skills--audio-and-narration-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--brand-and-copywriting-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--capability-research-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--data-analysis-and-visualization.json +5 -0
- package/agent-packs/specializations/operator-skills--financial-evidence-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--operating-template-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--operator-skill-library.json +5 -0
- package/agent-packs/specializations/operator-skills--operator-skills.json +5 -0
- package/agent-packs/specializations/operator-skills--operator-workflow-evaluation.json +5 -0
- package/agent-packs/specializations/operator-skills--privacy-and-agreement-review.json +5 -0
- package/agent-packs/specializations/operator-skills--storytelling-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--ux-and-accessibility-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--video-production-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--visual-design-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--workflow-integration-specialist.json +5 -0
- package/agent-packs/specializations/operators--access-and-privacy.json +5 -0
- package/agent-packs/specializations/operators--accounting-evidence-review.json +5 -0
- package/agent-packs/specializations/operators--agreement-operations.json +5 -0
- package/agent-packs/specializations/operators--books-operator.json +5 -0
- package/agent-packs/specializations/operators--brand-operator.json +5 -0
- package/agent-packs/specializations/operators--commerce-operations.json +5 -0
- package/agent-packs/specializations/operators--communications-operator.json +5 -0
- package/agent-packs/specializations/operators--content-and-assets-operator.json +5 -0
- package/agent-packs/specializations/operators--customer-operations.json +5 -0
- package/agent-packs/specializations/operators--design-operator.json +5 -0
- package/agent-packs/specializations/operators--design-production-02.json +5 -0
- package/agent-packs/specializations/operators--design-production-03.json +5 -0
- package/agent-packs/specializations/operators--design-production-04.json +5 -0
- package/agent-packs/specializations/operators--device-and-feed-operator.json +5 -0
- package/agent-packs/specializations/operators--director-operator.json +5 -0
- package/agent-packs/specializations/operators--experience-review.json +5 -0
- package/agent-packs/specializations/operators--growth-operations.json +5 -0
- package/agent-packs/specializations/operators--learn-operator.json +5 -0
- package/agent-packs/specializations/operators--localization-operator.json +5 -0
- package/agent-packs/specializations/operators--operators.json +5 -0
- package/agent-packs/specializations/operators--pay-operator.json +5 -0
- package/agent-packs/specializations/operators--research-and-intelligence.json +5 -0
- package/agent-packs/specializations/operators--roadmap-operator.json +5 -0
- package/agent-packs/specializations/operators--social-content-operator.json +5 -0
- package/agent-packs/specializations/operators--talent-operations.json +5 -0
- package/agent-packs/specializations/operators--web-operator.json +5 -0
- package/agent-packs/specializations/operators--workflow-operator.json +5 -0
- package/agent-packs/specializations/specialists--adversarial-qa.json +5 -0
- package/agent-packs/specializations/specialists--backlog-dispatch.json +5 -0
- package/agent-packs/specializations/specialists--build-01.json +5 -0
- package/agent-packs/specializations/specialists--build-02.json +5 -0
- package/agent-packs/specializations/specialists--build-03.json +5 -0
- package/agent-packs/specializations/specialists--developer-environment-steward.json +5 -0
- package/agent-packs/specializations/specialists--documentation-knowledge-steward.json +5 -0
- package/agent-packs/specializations/specialists--execution-readiness.json +5 -0
- package/agent-packs/specializations/specialists--incident-triage.json +5 -0
- package/agent-packs/specializations/specialists--platform-package-steward.json +5 -0
- package/agent-packs/specializations/specialists--product-direction-01.json +5 -0
- package/agent-packs/specializations/specialists--product-direction-02.json +5 -0
- package/agent-packs/specializations/specialists--release-recovery.json +5 -0
- package/agent-packs/specializations/specialists--release-watch.json +5 -0
- package/agent-packs/specializations/specialists--roadmap-planning-01.json +5 -0
- package/agent-packs/specializations/specialists--roadmap-planning-02.json +5 -0
- package/agent-packs/specializations/specialists--security-permissions-review.json +5 -0
- package/agent-packs/specializations/specialists--system-architecture-01.json +5 -0
- package/agent-packs/specializations/specialists--system-architecture-02.json +5 -0
- package/agent-packs/specializations/specialists--translation.json +5 -0
- package/agent-packs/specializations/specialists--ux-consistency-review.json +5 -0
- package/agent-packs/specializations/specialists--workflow-improvement.json +5 -0
- package/agent-packs/specializations/specialists--worktree-retirement.json +5 -0
- package/agent-packs/src/topolo_agent_pack/cicd.py +101 -0
- package/agent-packs/src/topolo_agent_pack/claude.py +22 -7
- package/agent-packs/src/topolo_agent_pack/claude_activation.py +157 -0
- package/agent-packs/src/topolo_agent_pack/claude_completion_recovery.py +216 -0
- package/agent-packs/src/topolo_agent_pack/claude_desktop.py +181 -0
- package/agent-packs/src/topolo_agent_pack/claude_dispatch.py +240 -0
- package/agent-packs/src/topolo_agent_pack/claude_prompt.py +74 -0
- package/agent-packs/src/topolo_agent_pack/claude_runner.py +207 -0
- package/agent-packs/src/topolo_agent_pack/claude_service.py +397 -0
- package/agent-packs/src/topolo_agent_pack/cli.py +188 -18
- package/agent-packs/src/topolo_agent_pack/closed_ledger_fence.py +118 -0
- package/agent-packs/src/topolo_agent_pack/cloud_attach.py +128 -0
- package/agent-packs/src/topolo_agent_pack/cloud_cutover.py +134 -0
- package/agent-packs/src/topolo_agent_pack/cloud_delta.py +162 -0
- package/agent-packs/src/topolo_agent_pack/cloud_migrate.py +142 -0
- package/agent-packs/src/topolo_agent_pack/cloud_recovery.py +42 -0
- package/agent-packs/src/topolo_agent_pack/cloud_work.py +126 -0
- package/agent-packs/src/topolo_agent_pack/coordinator_activity.py +27 -0
- package/agent-packs/src/topolo_agent_pack/coordinator_context.py +91 -0
- package/agent-packs/src/topolo_agent_pack/coordinator_refresh.py +116 -0
- package/agent-packs/src/topolo_agent_pack/desktop_acceptance.py +45 -0
- package/agent-packs/src/topolo_agent_pack/desktop_launcher.py +114 -0
- package/agent-packs/src/topolo_agent_pack/domain_context.py +38 -0
- package/agent-packs/src/topolo_agent_pack/execution.py +104 -7
- package/agent-packs/src/topolo_agent_pack/fresh.py +1 -1
- package/agent-packs/src/topolo_agent_pack/frontdoor.py +23 -0
- package/agent-packs/src/topolo_agent_pack/linked_task_context.py +80 -0
- package/agent-packs/src/topolo_agent_pack/live.py +285 -18
- package/agent-packs/src/topolo_agent_pack/native.py +160 -4
- package/agent-packs/src/topolo_agent_pack/policy.py +8 -0
- package/agent-packs/src/topolo_agent_pack/review_context.py +33 -0
- package/agent-packs/src/topolo_agent_pack/role_catalog.py +137 -0
- package/agent-packs/src/topolo_agent_pack/role_context.py +63 -0
- package/agent-packs/src/topolo_agent_pack/role_refresh.py +50 -0
- package/agent-packs/src/topolo_agent_pack/task_models.py +11 -7
- package/agent-packs/src/topolo_agent_pack/throughput.py +149 -0
- package/agent-packs/src/topolo_agent_pack/topolo.py +7 -1
- package/agent-packs/src/topolo_agent_pack/work.py +322 -0
- package/agent-packs/src/topolo_agent_pack/work_adoption.py +122 -0
- package/agent-packs/src/topolo_agent_pack/work_artifacts.py +84 -0
- package/agent-packs/src/topolo_agent_pack/work_context.py +77 -0
- package/agent-packs/src/topolo_agent_pack/work_gui.py +51 -0
- package/agent-packs/src/topolo_agent_pack/work_native.py +414 -0
- package/agent-packs/src/topolo_agent_pack/work_wait.py +151 -0
- package/agent-packs/templates/topolo-coordinators/AGENTS.md +7 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/cicd/AGENTS.md +6 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/cicd/config.json +4 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/engineering/AGENTS.md +3 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/engineering/config.json +4 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/operator-01/AGENTS.md +3 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/operator-01/config.json +4 -0
- package/agent-packs/templates/topolo-coordinators/pack.json +56 -0
- package/dist/chunk-NYDIAYVY.js +144 -0
- package/dist/{claude-pack-backend-YEVPAOID.js → chunk-UY5LEU5C.js} +1659 -21
- package/dist/claude-desktop-profile-JCQKSY3K.js +13 -0
- package/dist/claude-pack-activation-Q25MCEPU.js +506 -0
- package/dist/claude-pack-backend-HUTKGIDS.js +90 -0
- package/dist/index.js +408 -164
- package/package.json +8 -6
|
@@ -589,7 +589,7 @@ def materialize(library, components, runtime, mappings=None):
|
|
|
589
589
|
# Generate the dual desktop/server section mapping from the selected composition.
|
|
590
590
|
catalog_conn = sqlite3.connect(home / 'state_5.sqlite')
|
|
591
591
|
import uuid
|
|
592
|
-
groups = {}; pins = []; titles = {}; descriptions = {}
|
|
592
|
+
groups = {}; pins = []; titles = {}; descriptions = {}
|
|
593
593
|
for agent in chosen:
|
|
594
594
|
groupkey = (agent['pack'],agent['set'])
|
|
595
595
|
if groupkey not in groups:
|
|
@@ -610,9 +610,10 @@ def materialize(library, components, runtime, mappings=None):
|
|
|
610
610
|
('complete',None,int(time.time()),int(time.time())))
|
|
611
611
|
catalog_conn.commit(); catalog_conn.close()
|
|
612
612
|
for manifest in manifests:
|
|
613
|
-
|
|
613
|
+
descriptions.update(manifest['ui'].get('descriptions',{}))
|
|
614
614
|
groups_list = [g for (_,sid),g in groups.items() if sid!='intakes']
|
|
615
|
-
|
|
615
|
+
# Portable source-account IDs are not destination authentication.
|
|
616
|
+
sections = {}
|
|
616
617
|
gs={'thread-titles':{'titles':titles,'order':list(titles)},'pinned-thread-ids':[k.rsplit(':',1)[1] for k in pins],
|
|
617
618
|
'thread-workspace-root-hints':{a['threadId']:remap(thread_sources[a['threadId']][1]['cwd'],mapping) for a in chosen},
|
|
618
619
|
'electron-persisted-atom-state':{'sidebar-custom-sections-v3':sections,'unified-sidebar-pinned-order-v1':pins,'thread-descriptions-v1':descriptions,'flat-project-sidebar-preferences-v1':{'initialized':True,'mode':'project','chatSortMode':'updated_at','projectSortMode':'updated_at'}}}
|
|
@@ -640,6 +641,161 @@ def materialize(library, components, runtime, mappings=None):
|
|
|
640
641
|
from .topolo import attach_runtime
|
|
641
642
|
attach_runtime(library,runtime)
|
|
642
643
|
result={'threads':len(thread_sources),'agents':len(chosen),'historyOffsetsRebased':True,'activeGoalsPaused':sorted(set(paused_goals)),
|
|
643
|
-
'home':str(home),'userData':str(runtime/'desktop-data'),'dependencies':dependencies,'mappings':mapping
|
|
644
|
+
'home':str(home),'userData':str(runtime/'desktop-data'),'dependencies':dependencies,'mappings':mapping,
|
|
645
|
+
'sidebar':{'state':'pending-account-read','frontendVerified':False}}
|
|
644
646
|
atomic_json(runtime/'verification.json',result)
|
|
645
647
|
return result
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def sidebar_binding(runtime, account_response):
|
|
651
|
+
"""Bind the existing roster to a native account/read result, never pack IDs.
|
|
652
|
+
|
|
653
|
+
The caller owns the selected idle runtime or current managed generation.
|
|
654
|
+
Authentication and provider configuration are not read or changed here.
|
|
655
|
+
"""
|
|
656
|
+
from .storage import read_json
|
|
657
|
+
import uuid
|
|
658
|
+
runtime = pathlib.Path(runtime)
|
|
659
|
+
if (not isinstance(account_response, dict) or
|
|
660
|
+
type(account_response.get('requiresOpenaiAuth')) is not bool):
|
|
661
|
+
raise PackError('Native account/read response unavailable')
|
|
662
|
+
account = account_response.get('account')
|
|
663
|
+
routing = account_response.get('workspaceRouting')
|
|
664
|
+
account_id = None
|
|
665
|
+
if account is None:
|
|
666
|
+
state = 'pending-sign-in'
|
|
667
|
+
elif not isinstance(account, dict) or account.get('type') not in {'chatgpt', 'apiKey', 'amazonBedrock'}:
|
|
668
|
+
raise PackError('Unknown native account type')
|
|
669
|
+
elif account['type'] != 'chatgpt':
|
|
670
|
+
state = 'pending-chatgpt-account'
|
|
671
|
+
elif routing is None:
|
|
672
|
+
state = 'pending-account-identity'
|
|
673
|
+
elif (not isinstance(routing, dict) or not isinstance(routing.get('chatgptAccountId'), str) or
|
|
674
|
+
not re.fullmatch(r'[A-Za-z0-9_-]{1,200}', routing['chatgptAccountId'])):
|
|
675
|
+
raise PackError('Native workspace account identity unavailable')
|
|
676
|
+
else:
|
|
677
|
+
account_id = routing['chatgptAccountId']
|
|
678
|
+
state = 'configured-rendering-unverified'
|
|
679
|
+
agents = read_json(runtime / 'agents.json')['agents']
|
|
680
|
+
groups = {}; pins = []
|
|
681
|
+
conn = ro(runtime / 'home/state_5.sqlite')
|
|
682
|
+
try:
|
|
683
|
+
for agent in agents:
|
|
684
|
+
row = conn.execute('SELECT thread_section_id FROM threads WHERE id=?', (agent['threadId'],)).fetchone()
|
|
685
|
+
if not row or not row[0]:
|
|
686
|
+
raise PackError('Native sidebar roster membership unavailable')
|
|
687
|
+
key = 'codex:thread:local:' + agent['threadId']
|
|
688
|
+
if agent.get('pinned'):
|
|
689
|
+
pins.append(key)
|
|
690
|
+
continue
|
|
691
|
+
groupkey = (agent['pack'], agent['set'])
|
|
692
|
+
if groupkey not in groups:
|
|
693
|
+
groups[groupkey] = {'id':str(uuid.uuid5(uuid.NAMESPACE_URL, 'codex-pack-desktop:' + '/'.join(groupkey))),
|
|
694
|
+
'name':agent['sectionName'], 'hostSectionIds':{'local':row[0]},
|
|
695
|
+
'itemKeys':[], 'appearance':None}
|
|
696
|
+
if groups[groupkey]['hostSectionIds']['local'] != row[0]:
|
|
697
|
+
raise PackError('Native sidebar group membership conflicts')
|
|
698
|
+
groups[groupkey]['itemKeys'].append(key)
|
|
699
|
+
finally:
|
|
700
|
+
conn.close()
|
|
701
|
+
layout = {'sections':list(groups.values()),
|
|
702
|
+
'sectionOrder':['pinned'] + [g['id'] for g in groups.values()] + ['chats','threads'],
|
|
703
|
+
'collapsedSectionIds':[]}
|
|
704
|
+
return {'state':state, 'accountId':account_id, 'sections':{account_id:layout} if account_id else {},
|
|
705
|
+
'pins':pins, 'agents':len(agents), 'frontendVerified':False, 'sectionsRenderedVerified':False}
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
class SidebarJournal:
|
|
709
|
+
"""Presentation steps use the existing library transaction/recovery store."""
|
|
710
|
+
def __init__(self, lib, runtime, context, before, expected, report, parent=None):
|
|
711
|
+
import uuid
|
|
712
|
+
from .storage import read_json
|
|
713
|
+
self.lib=lib;self.runtime=pathlib.Path(runtime).resolve()
|
|
714
|
+
if not self.runtime.is_relative_to(lib.root.resolve()):
|
|
715
|
+
raise PackError('Sidebar runtime is outside the owned library')
|
|
716
|
+
self.parent=parent is not None;self.dispatched=False
|
|
717
|
+
if parent is None:
|
|
718
|
+
if lib.pending():raise PackError('Pending operation requires original-context recovery')
|
|
719
|
+
transaction=uuid.uuid4().hex
|
|
720
|
+
self.path=lib.root/'journals'/(transaction+'.json')
|
|
721
|
+
self.document={'id':transaction,'phase':'sidebar-binding','operation':'sidebar-bind',
|
|
722
|
+
'before':lib.active(),'targetRuntime':str(self.runtime),'createdAt':now()}
|
|
723
|
+
else:
|
|
724
|
+
self.path,self.document=parent
|
|
725
|
+
self.path=pathlib.Path(self.path)
|
|
726
|
+
if (self.path.parent!=lib.root/'journals' or read_json(self.path)!=self.document or
|
|
727
|
+
str(self.runtime) not in {self.document.get('targetRuntime'),self.document.get('before',{}).get('runtime'),self.document.get('after',{}).get('runtime')} or
|
|
728
|
+
self.document.get('phase') in {'complete','rolled-back'} or
|
|
729
|
+
any(path!=self.path for path in lib.pending())):
|
|
730
|
+
raise PackError('Original presentation transaction changed')
|
|
731
|
+
if self.document.get('sidebar',{}).get('state') in {'UNKNOWN','may-have-effects'}:
|
|
732
|
+
raise PackError('Original sidebar effects require reconciliation before another write')
|
|
733
|
+
self.document['sidebar']={'state':'may-have-effects','runtime':str(self.runtime),
|
|
734
|
+
'context':context,'before':before,'expected':expected,'report':report}
|
|
735
|
+
atomic_json(self.path,self.document)
|
|
736
|
+
|
|
737
|
+
def complete(self):
|
|
738
|
+
self.document['sidebar']['state']='complete'
|
|
739
|
+
if not self.parent:self.document['phase']='complete'
|
|
740
|
+
atomic_json(self.path,self.document)
|
|
741
|
+
|
|
742
|
+
def failed(self,error):
|
|
743
|
+
self.document['sidebar']['state']='UNKNOWN' if self.dispatched else 'not-executed'
|
|
744
|
+
if not self.dispatched and not self.parent:self.document['phase']='rolled-back'
|
|
745
|
+
self.document['sidebar']['errorClass']=type(error).__name__
|
|
746
|
+
try:atomic_json(self.path,self.document)
|
|
747
|
+
except Exception as journal_error:
|
|
748
|
+
# The original may-have-effects entry is already durable. Preserve
|
|
749
|
+
# the primary failure; never turn a failed status write into success.
|
|
750
|
+
error.add_note('Sidebar uncertainty journal update failed: '+type(journal_error).__name__)
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def bind_sidebar(runtime, account_response, *, lib, guard, journal=None, context=None):
|
|
754
|
+
"""Write only under an existing/new journal and a current native guard."""
|
|
755
|
+
from .storage import read_json
|
|
756
|
+
binding=sidebar_binding(runtime,account_response)
|
|
757
|
+
path=pathlib.Path(runtime)/'home/.codex-global-state.json'
|
|
758
|
+
before=read_json(path);gs=copy.deepcopy(before)
|
|
759
|
+
atoms=gs.setdefault('electron-persisted-atom-state',{})
|
|
760
|
+
if binding['accountId']:
|
|
761
|
+
sections=dict(atoms.get('sidebar-custom-sections-v3',{}));sections.update(binding['sections'])
|
|
762
|
+
atoms['sidebar-custom-sections-v3']=sections
|
|
763
|
+
atoms['unified-sidebar-pinned-order-v1']=binding['pins']
|
|
764
|
+
report={k:v for k,v in binding.items() if k not in {'sections','pins'}}
|
|
765
|
+
current=guard()
|
|
766
|
+
mutation=SidebarJournal(lib,runtime,context or current,before,gs,report,parent=journal)
|
|
767
|
+
try:
|
|
768
|
+
if read_json(path)!=before:raise PackError('Sidebar file changed before write')
|
|
769
|
+
if guard()!=current:raise PackError('Sidebar native context changed before write')
|
|
770
|
+
mutation.dispatched=True;atomic_json(path,gs)
|
|
771
|
+
if read_json(path)!=gs or guard()!=current:raise PackError('Sidebar file or native context changed after write')
|
|
772
|
+
report_path=pathlib.Path(runtime)/'sidebar-binding.json';atomic_json(report_path,report)
|
|
773
|
+
if read_json(report_path)!=report or guard()!=current:raise PackError('Sidebar report/context read-back differs')
|
|
774
|
+
mutation.complete();return report
|
|
775
|
+
except BaseException as error:
|
|
776
|
+
mutation.failed(error);raise
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def reconcile_sidebar(lib,path,document,current,actual,guard):
|
|
780
|
+
"""Reconcile exact observed presentation; never replay a presentation setter."""
|
|
781
|
+
from .storage import read_json
|
|
782
|
+
entry=document.get('sidebar')
|
|
783
|
+
targets={document.get('targetRuntime'),document.get('before',{}).get('runtime'),document.get('after',{}).get('runtime')}
|
|
784
|
+
if not entry or entry['runtime'] not in targets or actual!=entry['expected']:
|
|
785
|
+
raise PackError('Sidebar effects UNKNOWN; original destination presentation needs reconciliation')
|
|
786
|
+
original=entry['context']
|
|
787
|
+
# A new read-only connection is allowed; it never impersonates the original
|
|
788
|
+
# process generation. Account/home/config/binary/roster scope must agree.
|
|
789
|
+
stable=lambda value:{k:v for k,v in value.items() if k not in {'generation','readerPID'}}
|
|
790
|
+
if stable(current)!=stable(original) or guard()!=current:
|
|
791
|
+
raise PackError('Sidebar recovery owner/account/home/config changed')
|
|
792
|
+
report_path=pathlib.Path(entry['runtime'])/'sidebar-binding.json'
|
|
793
|
+
# Completing a missing local report follows exact authoritative readback;
|
|
794
|
+
# neither offline state nor the managed desktop setter is replayed.
|
|
795
|
+
atomic_json(report_path,entry['report'])
|
|
796
|
+
if read_json(report_path)!=entry['report'] or guard()!=current:
|
|
797
|
+
raise PackError('Sidebar reconciliation report/context changed')
|
|
798
|
+
entry['state']='complete';entry['reconciledCurrentContext']=current
|
|
799
|
+
if document.get('operation')=='sidebar-bind':document['phase']='complete'
|
|
800
|
+
atomic_json(path,document)
|
|
801
|
+
return entry['report']
|
|
@@ -59,6 +59,14 @@ def settings(agent, harness, consent=None):
|
|
|
59
59
|
spec = PROFILES[profile]
|
|
60
60
|
model = MODELS[harness][spec['tier']]
|
|
61
61
|
if harness == 'codex':
|
|
62
|
+
if 'modelPreset' in agent:
|
|
63
|
+
preset = agent['modelPreset']
|
|
64
|
+
if (not isinstance(preset, dict) or set(preset) != {'model', 'reasoningEffort'} or
|
|
65
|
+
not isinstance(preset['model'], str) or not preset['model'] or
|
|
66
|
+
preset['model'].endswith('-latest') or preset['reasoningEffort'] not in {'low', 'medium', 'high'}):
|
|
67
|
+
raise PackError('Invalid explicit fresh-role model preset')
|
|
68
|
+
model = preset['model']
|
|
69
|
+
spec = {**spec, 'effort': preset['reasoningEffort']}
|
|
62
70
|
native = {'model': model, 'model_reasoning_effort': spec['effort'],
|
|
63
71
|
'approval_policy': 'on-request', 'approvals_reviewer': 'user',
|
|
64
72
|
'sandbox_mode': spec['localAccess'],
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Selected canonical lifecycle evidence for one independent task review."""
|
|
2
|
+
import copy
|
|
3
|
+
from .storage import atomic_json
|
|
4
|
+
from .work import require
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def materialize(store, task, cwd):
|
|
8
|
+
current = store.get(task['id'])
|
|
9
|
+
require(current == task, 'Review task changed before workflow context materialization')
|
|
10
|
+
workers = [w for w in store.workers() if w['taskId'] == task['id']
|
|
11
|
+
and w.get('attempt', 1) == task.get('attempt', 1)]
|
|
12
|
+
require(len(workers) <= 32 and any(w['phase'] == 'execute' and w['id'] == task['result']['workerId']
|
|
13
|
+
and w['state'] == 'archived' and w.get('nativeArchived') is True
|
|
14
|
+
and w.get('checkpoint') == task['result']['checkpoint'] for w in workers),
|
|
15
|
+
'Review requires the exact archived execution checkpoint')
|
|
16
|
+
fields = ['id', 'phase', 'role', 'state', 'nativeId', 'turnId', 'resolution', 'roleSource',
|
|
17
|
+
'checkpoint', 'reviewGeneration', 'nativeArchived', 'nativeRetirement', 'createdAt']
|
|
18
|
+
selected = [{k: copy.deepcopy(w[k]) for k in fields if k in w} for w in workers]
|
|
19
|
+
started = min(w['createdAt'] for w in workers)
|
|
20
|
+
events = [{k: e[k] for k in ['sequence', 'kind', 'at']}
|
|
21
|
+
for e in store.events(task['id']) if e['at'] >= started]
|
|
22
|
+
require(len(events) <= 512, 'Selected review workflow events exceed boundary')
|
|
23
|
+
packet = {'format': 'topolo-task-review-workflow-v1', 'taskId': task['id'],
|
|
24
|
+
'attempt': task.get('attempt', 1), 'revision': task['revision'],
|
|
25
|
+
'cloudReference': {k: store.binding[k] for k in
|
|
26
|
+
['organizationId', 'workspaceId', 'documentId', 'datasetId']},
|
|
27
|
+
'route': copy.deepcopy(task['route']), 'workers': selected, 'events': events,
|
|
28
|
+
'evidenceContract': {'workspace': str(cwd.resolve()),
|
|
29
|
+
'readOutputExample': str(cwd.resolve() / 'evidence/retained-page-0.json'),
|
|
30
|
+
'instructions': 'Create an evidence directory in this reviewer workspace and save newly performed read-only command output there before citing it. Return absolute paths to existing nonempty regular files inside this workspace. Existing inspected context files may also be cited. Never cite /tmp, /private/tmp, another worker directory, a symlink or a path containing .. . Do not copy external temporary files to bypass this boundary, invent evidence or claim a read that was not performed. Each declared file is validated before any evidence upload; one external path rejects the entire declared evidence list.'},
|
|
31
|
+
'boundary': 'Canonical cloud lifecycle records for this task attempt only. A specialization role and its task type are separate fields. Event persistence proves recorded progress, not rendered browser delivery. The current reviewer is intentionally not archived until its result is checkpointed; the coordinator must verify that archival and live Activity delivery afterward.'}
|
|
32
|
+
atomic_json(cwd / 'context/workflow-evidence.json', packet)
|
|
33
|
+
return packet
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Specialization metadata independent of native chats and captured histories."""
|
|
2
|
+
import copy,hashlib,json,pathlib
|
|
3
|
+
from .storage import Library,atomic_json,canonical,read_json,component_key
|
|
4
|
+
from .work import require
|
|
5
|
+
from .task_models import default_configuration
|
|
6
|
+
from .cicd import DISCIPLINES
|
|
7
|
+
from .domain_context import references
|
|
8
|
+
|
|
9
|
+
COORDINATORS={'engineering':'engineering','operations':'operator-01','cicd':'cicd'}
|
|
10
|
+
REVIEWS={'engineering':'adversarial-qa','operations':'workflow-integration-specialist','cicd':'release-verification'}
|
|
11
|
+
|
|
12
|
+
def digest(value):
|
|
13
|
+
return hashlib.sha256(canonical(value)).hexdigest()
|
|
14
|
+
|
|
15
|
+
def install(lib,pack,agent_workspace,bytes_workspace,max_workers=4,client=None):
|
|
16
|
+
manifest,revision=lib.load(pack)
|
|
17
|
+
require(len(manifest['agents'])==3 and {(a['set'],a['id']) for a in manifest['agents']}=={('intakes',v) for v in COORDINATORS.values()},'A fresh three-coordinator pack is required; no specialist native chats')
|
|
18
|
+
require(type(max_workers) is int and 1<=max_workers<=8,'Invalid dispatch capacity')
|
|
19
|
+
root=lib.root/'work';root.mkdir(exist_ok=True,mode=0o700)
|
|
20
|
+
require(not (root/'config.json').exists(),'Existing task installation must be reconciled before replacement')
|
|
21
|
+
if client is None:
|
|
22
|
+
from .topolo import Client
|
|
23
|
+
client=Client('production','org:org_topolo_platform')
|
|
24
|
+
rows=client.action('app_topolo_agent.agents.list',{},agent_workspace)['agents'];roles=[];refs={};seen=set()
|
|
25
|
+
for agent in rows:
|
|
26
|
+
config=json.loads(agent['config']) if isinstance(agent['config'],str) else agent['config'];spec=config.get('specialization',{})
|
|
27
|
+
if spec.get('format')!='topolo-role-specialization-v1':continue
|
|
28
|
+
logical=spec.get('logicalId','');parts=logical.split('/')
|
|
29
|
+
require(len(parts)==2,'Invalid logical specialization identity')
|
|
30
|
+
group,identifier=parts
|
|
31
|
+
if group=='intakes' and identifier not in COORDINATORS.values():continue
|
|
32
|
+
require(group in {'intakes','specialists','operators','operator-skills'} and logical not in seen,'Unknown or duplicate specialization')
|
|
33
|
+
instructions=spec.get('instructions')
|
|
34
|
+
require(agent['organizationId']=='org_topolo_platform' and agent['workspaceId']==agent_workspace and isinstance(instructions,str) and 100<=len(instructions)<=16000 and config.get('purpose')==instructions,'Unqualified specialization definition')
|
|
35
|
+
seen.add(logical);role={'pack':pack,'set':group,'id':identifier,'title':identifier.replace('-',' ').title()};key=component_key(role)
|
|
36
|
+
roles.append(role);refs[key]={'agentId':agent['id'],'workspaceId':agent_workspace,'logicalId':logical,'instructionsSHA256':hashlib.sha256(instructions.encode()).hexdigest()}
|
|
37
|
+
domain=references(config,logical)
|
|
38
|
+
if domain:refs[key]['domainContextRefs']=domain
|
|
39
|
+
channels={}
|
|
40
|
+
for channel,intake in COORDINATORS.items():
|
|
41
|
+
eligible=[r for r in roles if (r['set']=='intakes' and r['id']==intake) or
|
|
42
|
+
(channel=='engineering' and r['set']=='specialists' and r['id'] not in DISCIPLINES) or
|
|
43
|
+
(channel=='operations' and r['set'] in {'operators','operator-skills'}) or
|
|
44
|
+
(channel=='cicd' and r['set']=='specialists' and r['id'] in DISCIPLINES)]
|
|
45
|
+
require(sum(r['set']=='intakes' for r in eligible)==1 and sum(r['id']==REVIEWS[channel] for r in eligible)==1,'Channel specialization or independent reviewer is missing')
|
|
46
|
+
require(len({r['id'] for r in eligible})==len(eligible),'Ambiguous discipline names in channel')
|
|
47
|
+
channels[channel]={'intakeRole':intake,'reviewRole':REVIEWS[channel],'roleKeys':[component_key(r) for r in eligible]}
|
|
48
|
+
configuration=default_configuration();ceiling={'allowedModels':sorted({model for harness in configuration['harnesses'].values() for model in harness['tiers'].values()}),'allowedEfforts':['low','medium','high']}
|
|
49
|
+
constraints={'pack':ceiling,'groups':{r['set']:copy.deepcopy(ceiling) for r in roles},'roles':{component_key(r):copy.deepcopy(ceiling) for r in roles}}
|
|
50
|
+
registry={'format':'topolo-role-bindings-v1','organizationId':'org_topolo_platform','roles':refs}
|
|
51
|
+
catalog={'format':'topolo-task-role-catalog-v1','pack':pack,'nativePackRevision':revision,'roles':roles,'bindings':refs,'channels':channels,'configuration':configuration,'constraints':constraints}
|
|
52
|
+
atomic_json(root/'role-catalog.json',catalog);atomic_json(root/'role-bindings.json',registry)
|
|
53
|
+
atomic_json(root/'context-authority.json',{'format':'topolo-task-context-authority-v1','organizationId':'org_topolo_platform','workspaceId':bytes_workspace,'scope':'org'})
|
|
54
|
+
atomic_json(root/'config.json',{'format':'topolo-task-dispatch-v2','pack':pack,'catalogDigest':digest(catalog),'maxWorkers':max_workers,'frontdoorDispatch':True,'requireCloudAuthority':True})
|
|
55
|
+
return {'state':'three-coordinator-dispatch-prepared','nativeCoordinators':3,'specializationReferences':len(refs),'specialistNativeChatsCreated':0,'instructionsCached':False,'cloudAuthorityRequired':True,'nativeActivationVerified':False}
|
|
56
|
+
|
|
57
|
+
def configuration(owner,saved,channel):
|
|
58
|
+
require(set(saved)=={'format','pack','catalogDigest','maxWorkers','frontdoorDispatch','requireCloudAuthority'} and saved['frontdoorDispatch'] is True and saved['requireCloudAuthority'] is True,'Unqualified source-independent task configuration')
|
|
59
|
+
require(channel in COORDINATORS,'Unknown task channel')
|
|
60
|
+
root=owner.lib.root/'work';catalog=read_json(root/'role-catalog.json')
|
|
61
|
+
require(catalog.get('format')=='topolo-task-role-catalog-v1' and catalog.get('pack')==saved['pack'] and digest(catalog)==saved['catalogDigest'],'Specialization catalog changed; review before dispatch')
|
|
62
|
+
registry=read_json(root/'role-bindings.json');require(registry.get('organizationId')=='org_topolo_platform' and registry.get('roles')==catalog['bindings'],'Role reference registry differs from the reviewed catalog')
|
|
63
|
+
native,revision=owner.lib.load(saved['pack']);require(revision==catalog['nativePackRevision'] and len(native['agents'])==3,'Native coordinator roster changed')
|
|
64
|
+
selected=catalog['channels'][channel];expected=saved['pack']+'/intakes/'+selected['intakeRole']
|
|
65
|
+
matches=[c for c in owner.lib.active()['components'] if c['key']==expected]
|
|
66
|
+
require(len(matches)==1 and owner.execution.roles.get(matches[0]['threadId'],{}).get('key')==expected,'Channel has no current owned coordinator')
|
|
67
|
+
require(type(saved['maxWorkers']) is int and 1<=saved['maxWorkers']<=8,'Invalid native capacity')
|
|
68
|
+
manifest={'packId':saved['pack'],'sourceIndependent':True,'agents':[r for r in catalog['roles'] if component_key(r) in selected['roleKeys']]}
|
|
69
|
+
return {'pack':saved['pack'],'revision':revision,'maxWorkers':saved['maxWorkers'],'catalogOnly':True,**selected},manifest,{'configuration':catalog['configuration'],'constraints':catalog['constraints']}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def repair_model_ceilings(lib,expected_catalog_digest,expected_native,client=None,native_call=None,live_request=None):
|
|
73
|
+
with Library(lib.root).lock():
|
|
74
|
+
return _repair_model_ceilings_locked(lib,expected_catalog_digest,expected_native,client,native_call,live_request)
|
|
75
|
+
|
|
76
|
+
def _repair_model_ceilings_locked(lib,expected_catalog_digest,expected_native,client,native_call,live_request):
|
|
77
|
+
"""Add only the three already-configured Claude IDs to stale GPT-only ceilings.
|
|
78
|
+
|
|
79
|
+
No task, role definition, native session, permission or model preference is
|
|
80
|
+
changed. A digest mismatch fences dispatch throughout metadata publication.
|
|
81
|
+
"""
|
|
82
|
+
from .claude_desktop import call
|
|
83
|
+
from .live import request
|
|
84
|
+
from .cloud_work import CloudStore
|
|
85
|
+
native_call=native_call or call;live_request=live_request or request
|
|
86
|
+
root=lib.root/'work';journal=root/'model-ceiling-repair.json'
|
|
87
|
+
require(not journal.exists() and not journal.is_symlink(),'Existing model ceiling repair requires reconciliation; no replay')
|
|
88
|
+
saved=read_json(root/'config.json');catalog=read_json(root/'role-catalog.json');registry=read_json(root/'role-bindings.json')
|
|
89
|
+
require(saved.get('format')=='topolo-task-dispatch-v2' and saved.get('requireCloudAuthority') is True and saved.get('catalogDigest')==expected_catalog_digest==digest(catalog) and catalog.get('format')=='topolo-task-role-catalog-v1' and catalog.get('pack')==saved.get('pack') and registry.get('roles')==catalog.get('bindings'),'Expected source-independent catalog beforeimage changed')
|
|
90
|
+
native_pack,revision=lib.load(saved['pack'])
|
|
91
|
+
require(revision==catalog['nativePackRevision'] and len(native_pack['agents'])==3,'Original three-coordinator pack changed')
|
|
92
|
+
connection=read_json(root/'claude-frontdoors.json')['nativeConnection']
|
|
93
|
+
require(isinstance(expected_native,dict) and set(expected_native)=={'harness','pid','identity','models','workers','codeIdentity','qualified'} and expected_native['harness']=='claude-desktop' and expected_native['qualified'] is True and expected_native['workers']==0 and type(expected_native['pid']) is int and expected_native['pid']>1 and expected_native['pid']==connection['pid'] and expected_native['identity']==connection['identity'] and isinstance(expected_native['models'],list) and expected_native['models'] and all(isinstance(v,str) and v for v in expected_native['models']) and len(expected_native['models'])==len(set(expected_native['models'])),'Exact qualified idle native model catalog required')
|
|
94
|
+
configured=catalog['configuration']['harnesses'];claude=configured['claude']['tiers'];codex=set(configured['codex']['tiers'].values())
|
|
95
|
+
require(set(claude)=={'economy','standard','deep'} and len(set(claude.values()))==3 and all(isinstance(v,str) and v.startswith('claude-') and not v.endswith('-latest') and v in expected_native['models'] for v in claude.values()),'Configured three Claude tiers are not in the current native catalog')
|
|
96
|
+
reviewed=copy.deepcopy(catalog);constraints=reviewed['constraints']
|
|
97
|
+
require(set(constraints)=={'pack','groups','roles'} and set(constraints['roles'])==set(catalog['bindings']) and set(constraints['groups'])=={r['set'] for r in catalog['roles']},'Exact catalog role/group constraint coverage required')
|
|
98
|
+
for layer in [constraints['pack'],*constraints['groups'].values(),*constraints['roles'].values()]:
|
|
99
|
+
require(isinstance(layer,dict) and set(layer)=={'allowedModels','allowedEfforts'} and set(layer['allowedModels'])==codex and len(layer['allowedModels'])==len(codex) and layer['allowedEfforts']==['low','medium','high'],'Only unchanged stale GPT-only model ceilings may be repaired')
|
|
100
|
+
layer['allowedModels']=sorted(codex|set(claude.values()))
|
|
101
|
+
require(digest(reviewed)!=expected_catalog_digest,'Catalog has no stale model ceilings to repair')
|
|
102
|
+
if client is None:
|
|
103
|
+
from .topolo import Client
|
|
104
|
+
client=Client('production','org:org_topolo_platform')
|
|
105
|
+
store=CloudStore(lib.root,client=client)
|
|
106
|
+
def quiescent():
|
|
107
|
+
require(native_call(connection,{'kind':'status'})==expected_native,'Current native model catalog or idle connection changed')
|
|
108
|
+
status=live_request(lib,'status',{})
|
|
109
|
+
require(status.get('harness')=='claude' and status.get('cloudDispatchReady') is True and status.get('desktopPid')==expected_native['pid'] and status.get('activeTurns')==[],'Owning Claude dispatcher is not quiescent')
|
|
110
|
+
version,state=store.load()
|
|
111
|
+
for row in state['tables']['workers']:
|
|
112
|
+
w=json.loads(row[2])
|
|
113
|
+
require(w['state']=='archived' and w.get('nativeArchived') is True or w['state']=='failed' and not w.get('turnId') and (not w.get('nativeId') or w.get('nativeArchived') is True),'Existing cloud worker custody is unresolved')
|
|
114
|
+
for row in state['tables']['tasks']:
|
|
115
|
+
t=json.loads(row[2])
|
|
116
|
+
require(t.get('ownerInstallationId')!=store.binding['installationId'] or t['state'] not in {'queued','routing','executing','reviewing'},'Runnable owned cloud work must settle before model ceiling repair')
|
|
117
|
+
return {'versionId':version,'stateSHA256':digest(state),'datasetId':state['datasetId']}
|
|
118
|
+
cloud=quiescent()
|
|
119
|
+
held={**saved,'catalogDigest':'0'*64};after={**saved,'catalogDigest':digest(reviewed)}
|
|
120
|
+
record={'format':'topolo-model-ceiling-repair-v1','state':'prepared','harness':'claude','native':expected_native,'cloud':cloud,'before':{'config':saved,'catalog':catalog},'after':{'config':after,'catalog':reviewed},'addedModels':sorted(claude.values()),'cloudTaskStateChanged':False,'nativeSessionsCreated':0}
|
|
121
|
+
require(read_json(root/'config.json')==saved and read_json(root/'role-catalog.json')==catalog and read_json(root/'role-bindings.json')==registry,'Catalog metadata changed before publication')
|
|
122
|
+
atomic_json(journal,record)
|
|
123
|
+
try:
|
|
124
|
+
atomic_json(root/'config.json',held)
|
|
125
|
+
atomic_json(root/'role-catalog.json',reviewed)
|
|
126
|
+
require(quiescent()==cloud,'Protected cloud/native beforeimage changed during repair')
|
|
127
|
+
require(read_json(root/'config.json')==held and read_json(root/'role-catalog.json')==reviewed and read_json(root/'role-bindings.json')==registry,'Protected catalog metadata changed during repair')
|
|
128
|
+
atomic_json(root/'config.json',after)
|
|
129
|
+
require(read_json(root/'config.json')==after and read_json(root/'role-catalog.json')==reviewed,'Model ceiling publication readback differs')
|
|
130
|
+
atomic_json(journal,{**record,'state':'verified'})
|
|
131
|
+
except Exception:
|
|
132
|
+
# Retain a digest fence after partial publication; never roll back cloud
|
|
133
|
+
# work, rewrite a foreign catalog, or silently resume from an old copy.
|
|
134
|
+
if read_json(root/'config.json') in (held,after):
|
|
135
|
+
atomic_json(root/'config.json',held)
|
|
136
|
+
raise
|
|
137
|
+
return {'state':'reviewed-model-ceilings-repaired','catalogDigest':after['catalogDigest'],'addedModels':sorted(claude.values()),'roles':len(constraints['roles']),'cloudTaskStateChanged':False,'nativeSessionsCreated':0,'existingRoleSettingsChanged':False}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Fetch exactly one Agent specialization. Never load a pack export or memory."""
|
|
2
|
+
import hashlib,json,pathlib
|
|
3
|
+
from .storage import read_json,atomic_json,canonical
|
|
4
|
+
from .work import require
|
|
5
|
+
from .domain_context import references,materialize as materialize_domain
|
|
6
|
+
|
|
7
|
+
def materialize(root,key,cwd,client=None,*,harness='codex'):
|
|
8
|
+
require(harness in {'codex','claude'},'Unsupported role instruction harness')
|
|
9
|
+
require(read_json(pathlib.Path(root)/'role-refresh.json',{}).get('state') in {None,'verified'},'Role metadata refresh is incomplete; reconcile before native startup')
|
|
10
|
+
registry=read_json(pathlib.Path(root)/'role-bindings.json')
|
|
11
|
+
require(registry.get('format')=='topolo-role-bindings-v1' and registry.get('organizationId')=='org_topolo_platform','Unqualified Agent role bindings')
|
|
12
|
+
ref=registry['roles'].get(key)
|
|
13
|
+
require(isinstance(ref,dict) and set(ref) in [{'agentId','workspaceId','logicalId','instructionsSHA256'},{'agentId','workspaceId','logicalId','instructionsSHA256','domainContextRefs'}],'Selected role has no qualified Agent definition')
|
|
14
|
+
if client is None:
|
|
15
|
+
from .topolo import Client
|
|
16
|
+
client=Client('production','org:org_topolo_platform')
|
|
17
|
+
result=client.action('app_topolo_agent.agents.get',{'id':ref['agentId']},ref['workspaceId'])
|
|
18
|
+
agent=result['agent']
|
|
19
|
+
require(agent['id']==ref['agentId'] and agent['workspaceId']==ref['workspaceId'] and agent['organizationId']==registry['organizationId'],'Agent specialization identity differs')
|
|
20
|
+
config=json.loads(agent['config']) if isinstance(agent['config'],str) else agent['config']
|
|
21
|
+
spec=config.get('specialization',{})
|
|
22
|
+
require(set(spec)=={'format','logicalId','instructions'} and spec['format']=='topolo-role-specialization-v1' and spec['logicalId']==ref['logicalId'],'Agent specialization is missing or belongs to another role')
|
|
23
|
+
instructions=spec['instructions']
|
|
24
|
+
require(isinstance(instructions,str) and 100<=len(instructions)<=16000 and config.get('purpose')==instructions,'Agent specialization body is unqualified')
|
|
25
|
+
digest=hashlib.sha256(instructions.encode()).hexdigest()
|
|
26
|
+
require(digest==ref['instructionsSHA256'],'Agent role changed; review its new definition before dispatch')
|
|
27
|
+
refs=references(config,spec['logicalId'])
|
|
28
|
+
require(refs==ref.get('domainContextRefs',[]),'Reviewed operating reference binding changed')
|
|
29
|
+
cwd=pathlib.Path(cwd);require(cwd.is_dir() and not cwd.is_symlink(),'New worker directory required')
|
|
30
|
+
filename='CLAUDE.md' if harness=='claude' else 'AGENTS.md'
|
|
31
|
+
target=cwd/filename
|
|
32
|
+
require(not target.is_symlink(),'Role instruction file cannot be a symlink')
|
|
33
|
+
domain=materialize_domain(refs,cwd,client)
|
|
34
|
+
target.write_text(instructions+'\n')
|
|
35
|
+
receipt={'source':'topolo-agent-definition','agentId':agent['id'],'workspaceId':agent['workspaceId'],'logicalId':spec['logicalId'],'instructionsSHA256':digest,'instructionBytes':len(instructions.encode()),'action':'app_topolo_agent.agents.get','rolesLoaded':1,'wholePackExportLoaded':False,'memoryLoaded':False}
|
|
36
|
+
if domain:receipt['domainContextRefs']=domain
|
|
37
|
+
atomic_json(cwd/'role-source.json',receipt)
|
|
38
|
+
return receipt
|
|
39
|
+
|
|
40
|
+
def bind(root,manifests,workspace,client=None):
|
|
41
|
+
"""Installation discovery only; registry contains references, never bodies."""
|
|
42
|
+
if client is None:
|
|
43
|
+
from .topolo import Client
|
|
44
|
+
client=Client('production','org:org_topolo_platform')
|
|
45
|
+
agents=client.action('app_topolo_agent.agents.list',{},workspace)['agents']
|
|
46
|
+
roles={}
|
|
47
|
+
for manifest in manifests:
|
|
48
|
+
for role in manifest['agents']:
|
|
49
|
+
logical=role['set']+'/'+role['id'];matches=[]
|
|
50
|
+
for agent in agents:
|
|
51
|
+
config=json.loads(agent['config']) if isinstance(agent['config'],str) else agent['config']
|
|
52
|
+
spec=config.get('specialization',{})
|
|
53
|
+
if spec.get('format')=='topolo-role-specialization-v1' and spec.get('logicalId')==logical:
|
|
54
|
+
require(agent['organizationId']=='org_topolo_platform' and agent['workspaceId']==workspace and config.get('purpose')==spec.get('instructions'),'Agent specialization discovery differs')
|
|
55
|
+
matches.append((agent,spec))
|
|
56
|
+
require(len(matches)==1,'Missing or ambiguous Agent specialization: '+logical)
|
|
57
|
+
agent,spec=matches[0];key=role['pack']+'/'+logical
|
|
58
|
+
roles[key]={'agentId':agent['id'],'workspaceId':workspace,'logicalId':logical,'instructionsSHA256':hashlib.sha256(spec['instructions'].encode()).hexdigest()}
|
|
59
|
+
refs=references(json.loads(agent['config']) if isinstance(agent['config'],str) else agent['config'],logical)
|
|
60
|
+
if refs:roles[key]['domainContextRefs']=refs
|
|
61
|
+
registry={'format':'topolo-role-bindings-v1','organizationId':'org_topolo_platform','roles':roles}
|
|
62
|
+
atomic_json(pathlib.Path(root)/'role-bindings.json',registry)
|
|
63
|
+
return {'state':'agent-role-references-bound','roles':len(roles),'instructionsCached':False,'dispatchLookup':'one Agent id per selected worker'}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Explicit reviewed metadata refresh for the same inactive cloud installation."""
|
|
2
|
+
import copy,hashlib,json,os,pathlib
|
|
3
|
+
from .storage import read_json,atomic_json,canonical
|
|
4
|
+
from .work import require
|
|
5
|
+
from .domain_context import references
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def refresh_refs(lib,expected_catalog_digest,expected_bindings,reviewed_bindings,client=None):
|
|
9
|
+
root=lib.root/'work';journal=root/'role-refresh.json'
|
|
10
|
+
require(not journal.exists(),'Role metadata refresh already recorded; reconcile its existing journal')
|
|
11
|
+
saved=read_json(root/'config.json');catalog=read_json(root/'role-catalog.json');registry=read_json(root/'role-bindings.json')
|
|
12
|
+
digest=lambda value:hashlib.sha256(canonical(value)).hexdigest()
|
|
13
|
+
require(saved.get('catalogDigest')==expected_catalog_digest==digest(catalog) and registry.get('roles')==catalog.get('bindings')==expected_bindings,'Expected role metadata changed')
|
|
14
|
+
require(not lib.active().get('components'),'Native owner must be stopped before role metadata refresh')
|
|
15
|
+
for record,field in [(root/'claude-frontdoors.json','desktopPid'),(lib.root/'live-bridge.json','pid')]:
|
|
16
|
+
owner=read_json(record,{})
|
|
17
|
+
if owner:
|
|
18
|
+
pid=owner.get(field);require(type(pid) is int and pid>1,'Native owner identity is unverified')
|
|
19
|
+
try:os.kill(pid,0)
|
|
20
|
+
except ProcessLookupError:pass
|
|
21
|
+
else:require(False,'Native owner must be stopped before role metadata refresh')
|
|
22
|
+
if client is None:
|
|
23
|
+
from .topolo import Client
|
|
24
|
+
client=Client('production','org:org_topolo_platform')
|
|
25
|
+
binding=read_json(root/'cloud.json')
|
|
26
|
+
require(binding.get('format')=='topolo-cloud-work-binding-v1' and binding.get('organizationId')=='org_topolo_platform','Inactive cloud installation binding required')
|
|
27
|
+
response=client.action('app_topolo_docs.documents.get',{'documentId':binding['documentId']},binding['workspaceId']);doc=response['document'];versions=[v for v in response['versions'] if v['id']==doc['currentVersionId']]
|
|
28
|
+
require(doc['id']==binding['documentId'] and doc['workspaceId']==binding['workspaceId'] and doc['organizationId']==binding['organizationId'] and len(versions)==1,'Inactive cloud installation identity differs')
|
|
29
|
+
state=json.loads(versions[0]['bodyMarkdown'])
|
|
30
|
+
require(state.get('format')=='topolo-cloud-work-state-v1' and state.get('datasetId')==binding['datasetId'] and state.get('activated') is False,'Role refresh requires the existing inactive prepared dataset')
|
|
31
|
+
for row in state['tables']['workers']:
|
|
32
|
+
worker=json.loads(row[2])
|
|
33
|
+
require(worker['state']=='archived' and worker.get('nativeArchived') is True or worker['state']=='failed' and not worker.get('turnId') and (not worker.get('nativeId') or worker.get('nativeArchived') is True),'Existing native worker closure is unresolved before role refresh')
|
|
34
|
+
required={key for key,ref in expected_bindings.items() if ref['logicalId']=='intakes/cicd' or ref['logicalId'] in {'specialists/'+v for v in ['queue-recovery','runner-capacity','pipeline-engineering','promotion-deployment','release-verification']}}
|
|
35
|
+
require(len(required)==6 and isinstance(reviewed_bindings,dict) and set(reviewed_bindings)==required,'Exactly the six existing CI/CD roles must be reviewed')
|
|
36
|
+
updated=copy.deepcopy(expected_bindings)
|
|
37
|
+
for key,ref in reviewed_bindings.items():
|
|
38
|
+
previous=expected_bindings[key]
|
|
39
|
+
require(set(ref)=={'agentId','workspaceId','logicalId','instructionsSHA256','domainContextRefs'} and all(ref[field]==previous[field] for field in ['agentId','workspaceId','logicalId']),'Reviewed role identity changed')
|
|
40
|
+
agent=client.action('app_topolo_agent.agents.get',{'id':ref['agentId']},ref['workspaceId'])['agent'];config=json.loads(agent['config']) if isinstance(agent['config'],str) else agent['config'];spec=config.get('specialization',{})
|
|
41
|
+
require(agent['id']==ref['agentId'] and agent['workspaceId']==ref['workspaceId'] and agent['organizationId']=='org_topolo_platform' and spec.get('format')=='topolo-role-specialization-v1' and spec.get('logicalId')==ref['logicalId'] and isinstance(spec.get('instructions'),str) and config.get('purpose')==spec['instructions'] and hashlib.sha256(spec['instructions'].encode()).hexdigest()==ref['instructionsSHA256'] and references(config,ref['logicalId'])==ref['domainContextRefs'],'Reviewed live Agent definition differs')
|
|
42
|
+
updated[key]=copy.deepcopy(ref)
|
|
43
|
+
new_catalog={**catalog,'bindings':updated};new_registry={**registry,'roles':updated};new_saved={**saved,'catalogDigest':digest(new_catalog)}
|
|
44
|
+
record={'format':'topolo-role-refresh-v1','state':'prepared','datasetId':binding['datasetId'],'cloudVersionId':doc['currentVersionId'],'oldCatalogDigest':expected_catalog_digest,'newCatalogDigest':new_saved['catalogDigest'],'before':{'catalog':catalog,'registry':registry,'config':saved},'after':{'catalog':new_catalog,'registry':new_registry,'config':new_saved}}
|
|
45
|
+
atomic_json(journal,record)
|
|
46
|
+
# Partial writes remain a visible administrative hold, never a fallback.
|
|
47
|
+
atomic_json(root/'role-catalog.json',new_catalog);atomic_json(root/'role-bindings.json',new_registry);atomic_json(root/'config.json',new_saved)
|
|
48
|
+
require(read_json(root/'role-catalog.json')==new_catalog and read_json(root/'role-bindings.json')==new_registry and read_json(root/'config.json')==new_saved,'Role metadata refresh readback differs')
|
|
49
|
+
atomic_json(journal,{**record,'state':'verified'})
|
|
50
|
+
return {'state':'reviewed-role-metadata-refreshed','roles':6,'catalogDigest':new_saved['catalogDigest'],'datasetId':binding['datasetId'],'cloudTaskStateChanged':False,'nativeOwnersStarted':False}
|
|
@@ -16,23 +16,27 @@ def digest(value):
|
|
|
16
16
|
def default_configuration():
|
|
17
17
|
"""Explicit template for a new pack configuration; existing roles are unchanged."""
|
|
18
18
|
return {
|
|
19
|
-
'format': 'topolo-task-model-configuration', 'schemaVersion': 1, 'revision': '
|
|
19
|
+
'format': 'topolo-task-model-configuration', 'schemaVersion': 1, 'revision': '2',
|
|
20
20
|
'rules': [
|
|
21
|
-
{'id': '
|
|
21
|
+
{'id': 'trivial', 'complexity': ['trivial'], 'risk': ['low'], 'profile': 'trivial',
|
|
22
|
+
'types': ['format', 'extract-fields', 'reference-lookup', 'status-read']},
|
|
23
|
+
{'id': 'routine', 'complexity': ['routine'], 'risk': ['low', 'normal'], 'profile': 'routine'},
|
|
22
24
|
{'id': 'deep', 'complexity': ['complex'], 'risk': ['low', 'normal', 'security-critical'], 'profile': 'deep'},
|
|
23
25
|
{'id': 'security', 'complexity': ['routine', 'standard'], 'risk': ['security-critical'], 'profile': 'deep'},
|
|
24
26
|
{'id': 'standard', 'complexity': ['standard'], 'risk': ['low', 'normal'], 'profile': 'standard'},
|
|
25
27
|
],
|
|
26
28
|
'harnesses': {
|
|
27
29
|
'codex': {
|
|
28
|
-
'tiers': {'standard': 'gpt-6.1-sol', 'deep': 'gpt-6-astra'},
|
|
29
|
-
'profiles': {'
|
|
30
|
+
'tiers': {'standard': 'gpt-6.1-sol', 'deep': 'gpt-6-astra', 'economy': 'gpt-6-luna'},
|
|
31
|
+
'profiles': {'trivial': {'tier': 'economy', 'efforts': ['low']},
|
|
32
|
+
'routine': {'tier': 'standard', 'efforts': ['low', 'medium']},
|
|
30
33
|
'standard': {'tier': 'standard', 'efforts': ['medium']},
|
|
31
|
-
'deep': {'tier': '
|
|
34
|
+
'deep': {'tier': 'standard', 'efforts': ['high']}},
|
|
32
35
|
},
|
|
33
36
|
'claude': {
|
|
34
37
|
'tiers': {'economy': 'claude-haiku-5-5', 'standard': 'claude-sonnet-5-5', 'deep': 'claude-opus-5-5'},
|
|
35
|
-
'profiles': {'
|
|
38
|
+
'profiles': {'trivial': {'tier': 'economy', 'efforts': ['low']},
|
|
39
|
+
'routine': {'tier': 'standard', 'efforts': ['low', 'medium']},
|
|
36
40
|
'standard': {'tier': 'standard', 'efforts': ['medium']},
|
|
37
41
|
'deep': {'tier': 'deep', 'efforts': ['high']}},
|
|
38
42
|
},
|
|
@@ -80,7 +84,7 @@ def _selection(configuration, task, harness, constraints, *, override_authorized
|
|
|
80
84
|
if (not isinstance(task, dict) or not {'type', 'complexity', 'risk'} <= set(task) or
|
|
81
85
|
set(task) - {'type', 'complexity', 'risk', 'override'} or
|
|
82
86
|
not isinstance(task['type'], str) or not task['type'] or len(task['type']) > 120 or
|
|
83
|
-
task['complexity'] not in {'routine', 'standard', 'complex'} or
|
|
87
|
+
task['complexity'] not in {'trivial', 'routine', 'standard', 'complex'} or
|
|
84
88
|
task['risk'] not in {'low', 'normal', 'security-critical'}):
|
|
85
89
|
raise PackError('An intake must supply an exact task type, complexity and risk')
|
|
86
90
|
rules = configuration.get('rules')
|