@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
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Docs-selected task state; local SQLite exists only inside a disposable operation.
|
|
2
|
+
|
|
3
|
+
Requires the published conditional-version and current-only-read contracts.
|
|
4
|
+
No network failure falls back to a local ledger, and startup never adopts workers.
|
|
5
|
+
"""
|
|
6
|
+
import copy,hashlib,json,pathlib,sqlite3,tempfile,uuid
|
|
7
|
+
from .storage import PackError,read_json,canonical
|
|
8
|
+
from .work import Store,require
|
|
9
|
+
|
|
10
|
+
TABLES={'tasks':('id','input_hash','body'),'workers':('id','task_id','body'),'events':('sequence','task_id','body'),'responses':('id','input_hash','task_id')}
|
|
11
|
+
READS={'get','list','worker','workers','events','retained','retained_page'}
|
|
12
|
+
WRITES={'submit','update','reserve','worker_update','respond','re_review','progress','adopt_ready','adopt_retained'}
|
|
13
|
+
|
|
14
|
+
class CloudStore:
|
|
15
|
+
cloud=True
|
|
16
|
+
def __init__(self,root,client=None,inventory=None):
|
|
17
|
+
self.root=pathlib.Path(root)/'work'
|
|
18
|
+
self.binding=read_json(self.root/'cloud.json')
|
|
19
|
+
renewal=self.root/'coordinator-renewal.json'
|
|
20
|
+
if renewal.exists() or renewal.is_symlink():
|
|
21
|
+
from .coordinator_refresh import verified_local
|
|
22
|
+
from types import SimpleNamespace
|
|
23
|
+
verified_local(SimpleNamespace(root=self.root.parent))
|
|
24
|
+
if (self.root/'cloud-migration.json').exists():
|
|
25
|
+
from .cloud_migrate import verified
|
|
26
|
+
verified(self.root.parent)
|
|
27
|
+
attachment_path=self.root/'cloud-attach.json'
|
|
28
|
+
if attachment_path.exists() or attachment_path.is_symlink():
|
|
29
|
+
require(not attachment_path.is_symlink(),'Cloud attachment journal cannot be a symlink')
|
|
30
|
+
attachment=read_json(attachment_path)
|
|
31
|
+
require(isinstance(attachment,dict) and attachment.get('format')=='topolo-cloud-attach-v1' and attachment.get('state')=='verified' and attachment.get('binding')==self.binding,'Cloud attachment is incomplete; reconcile before execution')
|
|
32
|
+
require(set(self.binding)=={'format','organizationId','workspaceId','documentId','datasetId','installationId'},'Invalid cloud work binding')
|
|
33
|
+
require(self.binding['format']=='topolo-cloud-work-binding-v1' and self.binding['organizationId']=='org_topolo_platform','Cloud work organization differs')
|
|
34
|
+
if client is None:
|
|
35
|
+
from .topolo import Client
|
|
36
|
+
client=Client('production','org:org_topolo_platform')
|
|
37
|
+
require(all(isinstance(self.binding[k],str) and 1<=len(self.binding[k])<=240 for k in ['workspaceId','documentId','datasetId','installationId']),'Cloud work binding identity is missing')
|
|
38
|
+
self.client=client;self.inventory=inventory
|
|
39
|
+
def recover(self):
|
|
40
|
+
# Another machine's running or UNKNOWN worker retains its owner. A fresh
|
|
41
|
+
# process is not a lease, checkpoint adoption, or execution authorization.
|
|
42
|
+
return None
|
|
43
|
+
def load(self):
|
|
44
|
+
r=self.client.action('app_topolo_docs.documents.get',{'documentId':self.binding['documentId'],'includeHistory':False},self.binding['workspaceId'])
|
|
45
|
+
doc=r['document'];rows=[v for v in r['versions'] if v['id']==doc['currentVersionId']]
|
|
46
|
+
require(doc['id']==self.binding['documentId'] and doc['workspaceId']==self.binding['workspaceId'] and doc['organizationId']==self.binding['organizationId'] and len(rows)==1,'Cloud task-state readback differs')
|
|
47
|
+
value=json.loads(rows[0]['bodyMarkdown'])
|
|
48
|
+
require(value['format']=='topolo-cloud-work-state-v1' and value['datasetId']==self.binding['datasetId'] and type(value['generation']) is int and set(value['tables'])==set(TABLES),'Cloud work state is unqualified')
|
|
49
|
+
require(value.get('activated') is True,'Cloud task-state migration has not been activated; no execution authority')
|
|
50
|
+
return doc['currentVersionId'],value
|
|
51
|
+
def retained_rows(self,ref):
|
|
52
|
+
if self.inventory is not None:
|
|
53
|
+
require(hashlib.sha256(canonical(self.inventory)).hexdigest()==ref['sha256'],'Retained inventory cache differs from cloud')
|
|
54
|
+
return self.inventory
|
|
55
|
+
from .topolo import read_artifact
|
|
56
|
+
with tempfile.TemporaryDirectory(prefix='topolo-work-inventory-') as tmp:
|
|
57
|
+
path=pathlib.Path(tmp)/'inventory.json'
|
|
58
|
+
read_artifact(self.client,{'input':{'bytesScope':ref['scope'],'bytesWorkspaceId':ref['workspaceId']}},ref['key'],ref,path)
|
|
59
|
+
value=json.loads(path.read_text())
|
|
60
|
+
require(hashlib.sha256(canonical(value)).hexdigest()==ref['sha256'],'Cloud retained inventory digest differs')
|
|
61
|
+
self.inventory=value;return value
|
|
62
|
+
def snapshot(self):
|
|
63
|
+
_,value=self.load()
|
|
64
|
+
return {'tasks':[json.loads(r[2]) for r in reversed(value['tables']['tasks'])],
|
|
65
|
+
'workers':[json.loads(r[2]) for r in value['tables']['workers']],
|
|
66
|
+
'events':[{'sequence':r[0],**json.loads(r[2])} for r in value['tables']['events']],
|
|
67
|
+
'retainedCount':value['inventory']['count'],'authority':'topolo-docs','generation':value['generation']}
|
|
68
|
+
def adopt_ready(self,tid,revision):
|
|
69
|
+
return self.invoke('adopt_ready',(tid,revision,self.binding['installationId']),{})
|
|
70
|
+
def __getattr__(self,name):
|
|
71
|
+
if name not in READS|WRITES:
|
|
72
|
+
raise AttributeError(name)
|
|
73
|
+
return lambda *args,**kwargs:self.invoke(name,args,kwargs)
|
|
74
|
+
def invoke(self,name,args,kwargs):
|
|
75
|
+
version,state=self.load()
|
|
76
|
+
with tempfile.TemporaryDirectory(prefix='topolo-work-operation-') as tmp:
|
|
77
|
+
store=Store(tmp)
|
|
78
|
+
with store.db() as db:
|
|
79
|
+
for table,columns in TABLES.items():
|
|
80
|
+
db.executemany('INSERT INTO '+table+' VALUES ('+','.join('?' for _ in columns)+')',state['tables'][table])
|
|
81
|
+
if name in {'retained','retained_page','adopt_retained'}:
|
|
82
|
+
db.executemany('INSERT INTO retained VALUES (?,?)',[(row[0],row[1]) for row in self.retained_rows(state['inventory'])])
|
|
83
|
+
if name in WRITES and name not in {'submit','adopt_ready','adopt_retained'}:
|
|
84
|
+
task_id=store.worker(args[0])['taskId'] if name in {'worker_update','progress'} else args[0]['taskId'] if name in {'respond','re_review'} else args[0]
|
|
85
|
+
if name!='respond':require(store.get(task_id).get('ownerInstallationId')==self.binding['installationId'],'Task belongs to another installation; explicit checkpoint adoption is required')
|
|
86
|
+
if name=='adopt_retained':
|
|
87
|
+
parent=store.get(args[0]['continuationOwnerTaskId'])
|
|
88
|
+
if parent.get('ownerInstallationId')!=self.binding['installationId']:
|
|
89
|
+
from .work_adoption import verify_parent
|
|
90
|
+
workers=[w for w in store.workers() if w['taskId']==parent['id']]
|
|
91
|
+
proof=verify_parent(parent,workers,args[0],self.root,self.client,self.binding['installationId'])
|
|
92
|
+
kwargs={**kwargs,'parent_proof':proof}
|
|
93
|
+
result=getattr(store,name)(*args,**kwargs)
|
|
94
|
+
if name in READS:
|
|
95
|
+
return result
|
|
96
|
+
if name in {'submit','adopt_retained'} and result[1]:
|
|
97
|
+
task=result[0];task['ownerInstallationId']=self.binding['installationId'];task['resumeAfterCheckpoint']=True
|
|
98
|
+
with store.db() as db:
|
|
99
|
+
db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(task),task['id']))
|
|
100
|
+
if name=='respond' and result[1] and result[0]['state']=='queued':
|
|
101
|
+
task=result[0];task['ownerInstallationId']=self.binding['installationId'];task['resumeAfterCheckpoint']=True
|
|
102
|
+
with store.db() as db:
|
|
103
|
+
db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(task),task['id']))
|
|
104
|
+
with store.db() as db:
|
|
105
|
+
tables={table:db.execute('SELECT * FROM '+table+' ORDER BY '+('sequence' if table=='events' else 'rowid')).fetchall() for table in TABLES}
|
|
106
|
+
if tables=={k:[tuple(row) for row in rows] for k,rows in state['tables'].items()}:
|
|
107
|
+
return result
|
|
108
|
+
operation=str(uuid.uuid4());next_state={**copy.deepcopy(state),'tables':tables,'generation':state['generation']+1,'operation':{'id':operation,'method':name,'installationId':self.binding['installationId']}}
|
|
109
|
+
body=canonical(next_state).decode();require(len(body)<=900000,'Active cloud ledger requires archival before accepting more state; no local fallback')
|
|
110
|
+
try:
|
|
111
|
+
self.client.action('app_topolo_docs.documents.versions.create',{'documentId':self.binding['documentId'],'bodyMarkdown':body,'expectedCurrentVersionId':version},self.binding['workspaceId'],write=True)
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
# Reconcile a lost successful ACK once. A competing generation
|
|
114
|
+
# or unavailable read is never permission to repeat the write.
|
|
115
|
+
try:_,actual=self.load()
|
|
116
|
+
except Exception:
|
|
117
|
+
raise PackError('Cloud task write is uncertain; no local commit or external replay was authorized') from exc
|
|
118
|
+
if actual.get('operation',{}).get('id')!=operation:
|
|
119
|
+
raise PackError('Cloud state changed or write was refused; reread and reconcile without replay') from exc
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
def open_store(root):
|
|
123
|
+
config=read_json(pathlib.Path(root)/'work/config.json',{})
|
|
124
|
+
cloud=(pathlib.Path(root)/'work/cloud.json').is_file()
|
|
125
|
+
require(((config.get('authority')=='topolo-docs' or config.get('requireCloudAuthority') is True)==cloud),'Cloud authority binding is incomplete; no local fallback')
|
|
126
|
+
return CloudStore(root) if cloud else Store(root)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Durable native coordinator observations, separate from task completion."""
|
|
2
|
+
import time,fcntl
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from .storage import read_json,atomic_json
|
|
5
|
+
|
|
6
|
+
def record(root,status):
|
|
7
|
+
roster=read_json(Path(status['runtime'])/'agents.json',{}).get('agents',[])
|
|
8
|
+
active=set(status['activeTurns'])
|
|
9
|
+
work=Path(root)/'work';work.mkdir(exist_ok=True)
|
|
10
|
+
with (work/'coordinator-observations.lock').open('a') as lock:
|
|
11
|
+
fcntl.flock(lock,fcntl.LOCK_EX)
|
|
12
|
+
path=work/'coordinator-observations.json'
|
|
13
|
+
record=read_json(path,{'states':{},'events':[]})
|
|
14
|
+
if read_json(work/'config.json',{}).get('frontdoorDispatch') is True:
|
|
15
|
+
return record['events']
|
|
16
|
+
for role in roster:
|
|
17
|
+
if role.get('set')!='intakes':continue
|
|
18
|
+
tid=role['threadId'];state='active' if tid in active else 'idle'
|
|
19
|
+
previous=record['states'].get(tid)
|
|
20
|
+
# Do not invent past starts or completion from the first idle observation.
|
|
21
|
+
if previous!=state and (state=='active' or previous is not None):
|
|
22
|
+
record['events'].append({'id':'native-observation:'+tid+':'+str(len(record['events'])+1),
|
|
23
|
+
'at':time.time(),'threadId':tid,'owner':role.get('title',role['id']),'state':state,
|
|
24
|
+
'summary':('Native coordinator execution is active outside the task broker. Existing work has not been dispatched again; task progress and outcome remain unverified.' if state=='active' else 'Native coordinator is now idle. This observation does not verify task completion or deployment.')})
|
|
25
|
+
record['states'][tid]=state
|
|
26
|
+
atomic_json(path,record)
|
|
27
|
+
return record['events']
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Bounded conversational context and typed coordinator broker-tool boundary."""
|
|
2
|
+
import copy,json,pathlib,shlex,sys,uuid,re
|
|
3
|
+
from .storage import canonical,atomic_json,read_json,PackError
|
|
4
|
+
from .work import require
|
|
5
|
+
|
|
6
|
+
CONTRACT='''Stay in the human-facing conversation. Answer questions, clarify scope, inspect current task status and explain results directly using canonical cloud evidence. A status question, follow-up or clarification is not a new assignment and must not create a task. For an actual new assignment, first reconcile the selected existing tasks; continue the original task when appropriate instead of duplicating it. Submit a new bounded assignment only through the installed task broker, using the supplied native-message dispatch identity exactly once. Delegate implementation, infrastructure, operational effects and bounded diagnostic execution to task-based specialists; do not perform them in this coordinator. Preserve task IDs, owners, holds, acceptance and uncertain effects. Use the existing broker read/wait tools to follow progress and inspect the final result, then explain the actual outcome here. A queue receipt is not completion and a GUI link is not a substitute for answering the human. Native model, effort, permissions and specialist-selection gates remain in force. Never invent a completed result, resend uncertain work, resume archived workers, or use another agent mechanism outside the task broker.'''
|
|
7
|
+
|
|
8
|
+
OPERATIONS={'list','read','retained','submit','respond','re-review','wait'}
|
|
9
|
+
|
|
10
|
+
def dispatch_id(thread,message):
|
|
11
|
+
require(isinstance(message,str) and 1<=len(message)<=200,'Native human message identity is missing; no new assignment can be dispatched')
|
|
12
|
+
return str(uuid.uuid5(uuid.NAMESPACE_URL,'topolo-frontdoor:'+thread+':'+message))
|
|
13
|
+
|
|
14
|
+
def payload_valid(value,operation,identity,channel):
|
|
15
|
+
require(isinstance(value,dict),'Typed broker request object required')
|
|
16
|
+
if operation=='read':require(set(value)=={'taskId'} and isinstance(value['taskId'],str) and 1<=len(value['taskId'])<=200,'Exact existing task identity required')
|
|
17
|
+
elif operation=='wait':require(set(value)=={'taskId','afterRevision','afterSequence','timeoutSeconds'} and isinstance(value['taskId'],str) and type(value['afterRevision']) is int and value['afterRevision']>=0 and type(value['afterSequence']) is int and value['afterSequence']>=0 and type(value['timeoutSeconds']) is int and 1<=value['timeoutSeconds']<=30,'Bounded existing task wait required')
|
|
18
|
+
elif operation=='retained':require(set(value)<={'cursor','limit'} and type(value.get('cursor',0)) is int and value.get('cursor',0)>=0 and type(value.get('limit',50)) is int and 1<=value.get('limit',50)<=50,'Bounded retained query required')
|
|
19
|
+
elif operation in {'submit','respond','re-review'}:
|
|
20
|
+
expected={'submit':{'requestId','objective','channel','canonicalClaimId'},'respond':{'requestId','taskId','revision','intent','note'},'re-review':{'requestId','taskId','revision','reviewWorkerId','reviewCheckpoint','note'}}[operation]
|
|
21
|
+
require(set(value)==expected and value['requestId']==identity,'Use this exact native-message dispatch identity; no invented request')
|
|
22
|
+
if operation=='submit':require(value['channel']==channel and isinstance(value['objective'],str) and 1<=len(value['objective'])<=16000 and value['canonicalClaimId'] is None,'Bounded new assignment required; existing claims need their supported adoption authority')
|
|
23
|
+
else:require(isinstance(value['taskId'],str) and type(value['revision']) is int and value['revision']>0 and isinstance(value['note'],str) and 1<=len(value['note'])<=8000,'Existing task revision and human response required')
|
|
24
|
+
else:require(operation=='list' and value=={},'Unsupported coordinator broker operation')
|
|
25
|
+
return value
|
|
26
|
+
|
|
27
|
+
def context(root,channel,thread,message,snapshot,harness):
|
|
28
|
+
require(harness in {'codex','claude'} and channel in {'engineering','operations','cicd'},'Qualified coordinator channel/harness required')
|
|
29
|
+
require(snapshot.get('authority')=='topolo-docs' and isinstance(snapshot.get('tasks'),list) and isinstance(snapshot.get('workers'),list),'Canonical cloud work snapshot required')
|
|
30
|
+
rows=[t for t in snapshot['tasks'] if t.get('channel')==channel][:20]
|
|
31
|
+
selected=[]
|
|
32
|
+
for task in rows:
|
|
33
|
+
workers=[w for w in snapshot['workers'] if w['taskId']==task['id']]
|
|
34
|
+
selected.append({k:task.get(k) for k in ['id','revision','state','phase','attempt','ownerInstallationId']}|{'objective':str(task.get('objective',''))[:500],'summary':str(task.get('summary',''))[:800],'workers':[{'phase':w['phase'],'state':w['state'],'nativeArchived':w.get('nativeArchived') is True} for w in workers[-8:]]})
|
|
35
|
+
adapter=pathlib.Path(__file__).resolve().parents[2]/'topolo-agent-pack'
|
|
36
|
+
require(adapter.is_file() and not adapter.is_symlink(),'Qualified bundled broker command is unavailable')
|
|
37
|
+
command=[sys.executable,str(adapter),'--store',str(pathlib.Path(root).resolve()),'--harness',harness,'work']
|
|
38
|
+
return {'format':'topolo-coordinator-conversation-v1','contract':CONTRACT,'channel':channel,'dispatchRequestId':dispatch_id(thread,message),'commands':{'list':shlex.join(command+['list']),'requestPrefix':shlex.join(command),'requestPath':'context/coordinator-request.json','wait':'Write {taskId, afterRevision, afterSequence, timeoutSeconds: 30} for an existing task, then wait --input <absolute request path>. This event-driven read returns changed or terminal state and results; do not create another task or poll in a background loop.','read':"Write {taskId} to the request file, then run the requestPrefix plus read --input <absolute request path>.",'dispatch':"Only for an actual assignment: write {requestId: dispatchRequestId, objective: exact bounded assignment, channel, canonicalClaimId: null}, then submit --input <absolute request path>. For existing work use read/respond instead of creating a duplicate.",'allowedOperations':sorted(OPERATIONS),'respond':"For a human-authorized continuation/change/hold of existing work, first read that task and use its returned latest task.revision. Write exactly {requestId: dispatchRequestId, taskId, revision: latest task.revision, intent: 'continue' | 'request_changes' | 'hold', note: exact human instruction}, then respond --input <absolute request path>. This updates the same task; never submit a replacement or reuse a stale revision. A refusal or uncertain response is not permission to replay.",'re-review':"Only for an explicitly requested fresh review of the same failed review, first read the task. Write exactly {requestId: dispatchRequestId, taskId, revision: latest task.revision, reviewWorkerId: rejected reviewer worker ID, reviewCheckpoint: rejected review checkpoint sha256, note: exact review correction}, then re-review --input <absolute request path>. Preserve the original route/execution, attempt, rejection and worker history; do not rerun execution.",'requestTemplates':{'respond':{'requestId':dispatch_id(thread,message),'taskId':'<existing task ID from read>','revision':'<latest task.revision integer from read>','intent':'continue','note':'<exact human continuation/change/hold instruction>'},'re-review':{'requestId':dispatch_id(thread,message),'taskId':'<existing task ID from read>','revision':'<latest task.revision integer from read>','reviewWorkerId':'<rejected reviewer worker ID from read>','reviewCheckpoint':'<rejected review checkpoint sha256 from read>','note':'<exact human review correction>'}}},'tasks':selected,'matchingTasks':sum(t.get('channel')==channel for t in snapshot['tasks']),'retainedCount':snapshot.get('retainedCount'),'evidenceBoundary':'Canonical recorded state only; queued is not running, retainedCount counts historical retained claims rather than active workloads, a model turn ending is not delivery, and no work is submitted by this context read.'}
|
|
39
|
+
|
|
40
|
+
def augment(value,selected):
|
|
41
|
+
require(isinstance(value.get('params',{}).get('input'),list) and bool(value['params']['input']),'Native coordinator input is missing')
|
|
42
|
+
result=copy.deepcopy(value);result['params']['input'].append({'type':'text','text':'Topolo coordinator context (platform data; the original human message above is unchanged):\n'+canonical(selected).decode()});return result
|
|
43
|
+
|
|
44
|
+
def prepare(root,role,event,snapshot,harness='claude'):
|
|
45
|
+
selected=context(root,role['channel'],'claude:'+event['session_id'],event['prompt_id'],snapshot,harness)
|
|
46
|
+
cwd=pathlib.Path(role['cwd']);require(cwd.is_absolute() and cwd.is_dir() and cwd.resolve()==cwd,'Owned coordinator workspace required')
|
|
47
|
+
directory=cwd/'context';require(not directory.is_symlink(),'Coordinator context cannot be a symlink');directory.mkdir(mode=0o700,exist_ok=True)
|
|
48
|
+
atomic_json(directory/'coordinator-session.json',{'format':'topolo-coordinator-session-v1','sessionId':event['session_id'],'dispatchRequestId':selected['dispatchRequestId'],'channel':role['channel'],'commandPrefix':shlex.split(selected['commands']['requestPrefix'])})
|
|
49
|
+
return selected
|
|
50
|
+
|
|
51
|
+
def tool_allowed(root,role,event):
|
|
52
|
+
"""Allow reads and exact broker transport, never direct engineering effects."""
|
|
53
|
+
cwd=pathlib.Path(role['cwd']);session=cwd/'context/coordinator-session.json'
|
|
54
|
+
require(cwd.is_absolute() and cwd.is_dir() and cwd.resolve()==cwd and not (cwd/'context').is_symlink() and not session.is_symlink(),'Owned coordinator context required')
|
|
55
|
+
record=read_json(session,{})
|
|
56
|
+
require(record.get('format')=='topolo-coordinator-session-v1' and record.get('sessionId')==event['session_id'] and record.get('channel')==role['channel'],'Current native prompt context is missing')
|
|
57
|
+
expected=[sys.executable,str(pathlib.Path(__file__).resolve().parents[2]/'topolo-agent-pack'),'--store',str(pathlib.Path(root).resolve()),'--harness','claude','work']
|
|
58
|
+
require(record.get('commandPrefix')==expected,'Current bundled broker command changed')
|
|
59
|
+
require(event.get('permission_mode')=='bypassPermissions','Coordinator native permissions differ')
|
|
60
|
+
request_path=cwd/'context/coordinator-request.json';tool=event.get('tool_name');value=event.get('tool_input') or {}
|
|
61
|
+
if tool=='Read':
|
|
62
|
+
path=pathlib.Path(value.get('file_path',''));return path.is_absolute() and path.resolve()==path and path.is_relative_to(cwd) and path.is_file()
|
|
63
|
+
if tool=='Write':
|
|
64
|
+
require(value.get('file_path')==str(request_path) and not request_path.is_symlink(),'Only the typed coordinator request file can be written')
|
|
65
|
+
body=value.get('content');require(isinstance(body,str) and len(body.encode())<=20000,'Bounded broker request required');payload=json.loads(body)
|
|
66
|
+
for operation in OPERATIONS:
|
|
67
|
+
try:payload_valid(payload,operation,record['dispatchRequestId'],role['channel']);return True
|
|
68
|
+
except PackError:pass
|
|
69
|
+
return False
|
|
70
|
+
if tool=='Bash':
|
|
71
|
+
require(set(value)<={'command','description','timeout','run_in_background'} and value.get('run_in_background') is not True,'Foreground typed broker command required')
|
|
72
|
+
args=shlex.split(value.get('command',''));prefix=record['commandPrefix']
|
|
73
|
+
require(args[:len(prefix)]==prefix,'Only the installed typed broker transport is allowed')
|
|
74
|
+
suffix=args[len(prefix):];require(bool(suffix) and suffix[0] in OPERATIONS,'Unsupported broker operation')
|
|
75
|
+
if suffix==['list']:return True
|
|
76
|
+
require(len(suffix)==3 and suffix[1]=='--input' and suffix[2]==str(request_path) and request_path.is_file() and request_path.resolve()==request_path,'Exact owned broker request path required')
|
|
77
|
+
require(request_path.stat().st_size<=20000,'Bounded broker request required')
|
|
78
|
+
payload_valid(read_json(request_path),suffix[0],record['dispatchRequestId'],role['channel']);return True
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def charter(logical,previous):
|
|
83
|
+
"""Authored canonical Agent update; installation still requires governed CAS."""
|
|
84
|
+
summaries={'intakes/engineering':'Coordinate engineering conversation and existing tasks; delegate implementation and bounded diagnostics through the task broker.',
|
|
85
|
+
'intakes/operator-01':'Coordinate operational conversation and existing tasks; delegate bounded business operations to the owning specialist through the task broker.',
|
|
86
|
+
'intakes/cicd':'Coordinate CI/CD conversation, existing releases and decisions. Delegate bounded diagnostics, engineering and release effects through the task broker; never SSH, edit source or deploy directly.'}
|
|
87
|
+
require(logical in summaries and isinstance(previous,str) and previous.count('## Specialization')==1,'Existing canonical coordinator definition required')
|
|
88
|
+
body=re.sub(r'## Specialization\n.*?(?=\n## |\Z)','## Specialization\n'+summaries[logical]+'\n\n'+CONTRACT+'\n',previous,flags=re.S)
|
|
89
|
+
body=body.replace('Capture exact request, attachments and desired outcome in the existing task. Check its request identity before creating anything. Select one discipline and task complexity/risk; dispatch promptly, then return idle. A separate task worker owns execution and progress. Surface decisions in the GUI; do not occupy this coordinator with implementation.', 'Read the selected cloud task context first. Answer status, follow-ups and clarification in this conversation. For actual assignments use the supplied deterministic request identity and the existing broker; reuse the original task for continuation. Follow its real result through read/wait before reporting the outcome. Specialists own implementation and operational effects.')
|
|
90
|
+
body=body.replace('Execute only the bounded assignment, then return its checkpoint and stop.', 'Report verified task outcomes and next actions in this conversation; specialists checkpoint and stop after their bounded work.')
|
|
91
|
+
return body
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Reviewed instruction-only renewal; task state and native identities never change."""
|
|
2
|
+
import copy,fcntl,hashlib,json,os,pathlib
|
|
3
|
+
from contextlib import ExitStack
|
|
4
|
+
from .storage import read_json,atomic_json,PackError
|
|
5
|
+
from .work import require
|
|
6
|
+
from .cloud_work import CloudStore
|
|
7
|
+
from .cloud_cutover import digest
|
|
8
|
+
|
|
9
|
+
FORMAT='topolo-coordinator-renewal-v1'
|
|
10
|
+
LOGICAL={'intakes/engineering','intakes/operator-01','intakes/cicd'}
|
|
11
|
+
|
|
12
|
+
def local(lib):
|
|
13
|
+
names=['config.json','role-bindings.json','cloud.json','cutover.json','cloud-migration.json','role-catalog.json']
|
|
14
|
+
from .closed_ledger_fence import owned
|
|
15
|
+
paths=[lib.root/'work'/name for name in names if (lib.root/'work'/name).exists() or (lib.root/'work'/name).is_symlink()]
|
|
16
|
+
for path in paths:owned(path)
|
|
17
|
+
return {path.name:read_json(path) for path in paths}
|
|
18
|
+
|
|
19
|
+
def verified_local(lib):
|
|
20
|
+
path=lib.root/'work/coordinator-renewal.json'
|
|
21
|
+
from .closed_ledger_fence import owned
|
|
22
|
+
owned(path)
|
|
23
|
+
record=read_json(path)
|
|
24
|
+
require(record.get('format')==FORMAT and record.get('state')=='verified'
|
|
25
|
+
and record.get('root')==str(lib.root.resolve()),
|
|
26
|
+
'Coordinator renewal is incomplete or belongs to another authority')
|
|
27
|
+
current=local(lib)
|
|
28
|
+
require(current==record['after'],'Renewed coordinator metadata changed')
|
|
29
|
+
return record
|
|
30
|
+
|
|
31
|
+
def effective(lib,migration):
|
|
32
|
+
path=lib.root/'work/coordinator-renewal.json'
|
|
33
|
+
if not path.exists():return migration['afterMetadataDigest'],migration['request']['expectedTargetCatalogDigest']
|
|
34
|
+
record=verified_local(lib)
|
|
35
|
+
require(record.get('originalMigrationDigest')==digest(migration)
|
|
36
|
+
and record.get('binding')==migration['binding'],'Coordinator renewal changed migration authority')
|
|
37
|
+
return record['afterMetadataDigest'],record['targetCatalogDigest']
|
|
38
|
+
|
|
39
|
+
def renew(lib,request,client=None):
|
|
40
|
+
require(isinstance(request,dict) and set(request)=={'expectedLocalDigest','reviewedBindings','expectedTargetCatalogDigest'},
|
|
41
|
+
'Exact coordinator renewal selectors required')
|
|
42
|
+
with lib.lock(),ExitStack() as stack:
|
|
43
|
+
for name in ['claude-service-start.lock','claude-service-owner.lock']:
|
|
44
|
+
path=lib.root/'work'/name
|
|
45
|
+
require(not path.is_symlink(),'Coordinator owner lock cannot be a symlink')
|
|
46
|
+
stream=stack.enter_context(path.open('a'))
|
|
47
|
+
try:fcntl.flock(stream.fileno(),fcntl.LOCK_EX|fcntl.LOCK_NB)
|
|
48
|
+
except BlockingIOError:raise PackError('Coordinator dispatcher owner lock is held')
|
|
49
|
+
bridge=read_json(lib.root/'live-bridge.json',{})
|
|
50
|
+
if bridge:
|
|
51
|
+
pid=bridge.get('pid');require(type(pid) is int and pid>1,'Native dispatcher owner is unverified')
|
|
52
|
+
try:os.kill(pid,0)
|
|
53
|
+
except ProcessLookupError:pass
|
|
54
|
+
else:require(False,'Stop the idle owning dispatcher before coordinator renewal')
|
|
55
|
+
journal=lib.root/'work/coordinator-renewal.json'
|
|
56
|
+
require(not journal.exists() and not journal.is_symlink(),'Existing coordinator renewal requires reconciliation, not replay')
|
|
57
|
+
before=local(lib);require(digest(before)==request['expectedLocalDigest'],'Reviewed coordinator preimage changed')
|
|
58
|
+
binding=before['cloud.json'];registry=before['role-bindings.json'];refs=request['reviewedBindings']
|
|
59
|
+
old={k:v for k,v in registry['roles'].items() if v['logicalId'] in LOGICAL}
|
|
60
|
+
require(len(old)==3 and isinstance(refs,dict) and set(refs)==set(old),'Exactly the three existing coordinators are required')
|
|
61
|
+
if client is None:
|
|
62
|
+
from .topolo import Client
|
|
63
|
+
client=Client('production','org:org_topolo_platform')
|
|
64
|
+
require(client.environment=='production' and client.context=='org:org_topolo_platform','Production organization context required')
|
|
65
|
+
from .coordinator_context import CONTRACT
|
|
66
|
+
for key,ref in refs.items():
|
|
67
|
+
prior=old[key]
|
|
68
|
+
require(set(ref)==set(prior) and {k:v for k,v in ref.items() if k!='instructionsSHA256'}=={k:v for k,v in prior.items() if k!='instructionsSHA256'},
|
|
69
|
+
'Coordinator identity or domain context cannot change')
|
|
70
|
+
agent=client.action('app_topolo_agent.agents.get',{'id':ref['agentId']},ref['workspaceId'])['agent']
|
|
71
|
+
config=json.loads(agent['config']) if isinstance(agent['config'],str) else agent['config'];spec=config['specialization']
|
|
72
|
+
require(agent['id']==ref['agentId'] and agent['workspaceId']==ref['workspaceId']
|
|
73
|
+
and agent['organizationId']==binding['organizationId']=='org_topolo_platform'
|
|
74
|
+
and spec['format']=='topolo-role-specialization-v1' and spec['logicalId']==ref['logicalId']
|
|
75
|
+
and config.get('domainContextRefs',[])==ref.get('domainContextRefs',[])
|
|
76
|
+
and config['purpose']==spec['instructions'] and CONTRACT in spec['instructions']
|
|
77
|
+
and hashlib.sha256(spec['instructions'].encode()).hexdigest()==ref['instructionsSHA256'],
|
|
78
|
+
'Live coordinator conversation instructions differ')
|
|
79
|
+
from types import SimpleNamespace
|
|
80
|
+
version,state=CloudStore.load(SimpleNamespace(binding=binding,client=client))
|
|
81
|
+
for row in state['tables']['workers']:
|
|
82
|
+
w=json.loads(row[2])
|
|
83
|
+
require(w['state']=='archived' and w.get('nativeArchived') is True or
|
|
84
|
+
w['state']=='failed' and not w.get('turnId') and (not w.get('nativeId') or w.get('nativeArchived') is True),
|
|
85
|
+
'Native worker closure is unresolved; preserve its instructions')
|
|
86
|
+
after=copy.deepcopy(before);after['role-bindings.json']['roles'].update(copy.deepcopy(refs))
|
|
87
|
+
migration=before.get('cloud-migration.json')
|
|
88
|
+
if 'role-catalog.json' in before:
|
|
89
|
+
catalog=before['role-catalog.json'];require(registry['roles']==catalog['bindings'] and before['config.json']['catalogDigest']==digest(catalog)==request['expectedTargetCatalogDigest'],
|
|
90
|
+
'Canonical role catalog preimage differs')
|
|
91
|
+
after['role-catalog.json']['bindings'].update(copy.deepcopy(refs))
|
|
92
|
+
target_digest=digest(after['role-catalog.json']);after['config.json']['catalogDigest']=target_digest
|
|
93
|
+
else:
|
|
94
|
+
require(migration is not None,'Existing migrated installation required')
|
|
95
|
+
target=pathlib.Path(before['cutover.json']['request']['destinationRoot'])
|
|
96
|
+
catalog=read_json(target/'work/role-catalog.json');target_digest=digest(catalog)
|
|
97
|
+
require(target_digest==request['expectedTargetCatalogDigest'],'Renewed canonical catalog differs')
|
|
98
|
+
canonical={r['logicalId']:r for r in catalog['bindings'].values()}
|
|
99
|
+
require(all(canonical.get(ref['logicalId'])==ref for ref in refs.values()),'Coordinator references differ from canonical renewal')
|
|
100
|
+
require(local(lib)==before,'Coordinator metadata changed during validation')
|
|
101
|
+
record={'format':FORMAT,'state':'prepared','root':str(lib.root.resolve()),'before':before,'after':after,
|
|
102
|
+
'binding':binding,'cloudVersionId':version,'targetCatalogDigest':target_digest,
|
|
103
|
+
'originalMigrationDigest':digest(migration) if migration else None,'cloudTaskStateChanged':False}
|
|
104
|
+
atomic_json(journal,record)
|
|
105
|
+
for name in ['role-bindings.json','role-catalog.json','config.json']:
|
|
106
|
+
if name in after:atomic_json(lib.root/'work'/name,after[name])
|
|
107
|
+
require(local(lib)==after,'Coordinator renewal readback differs')
|
|
108
|
+
if migration:
|
|
109
|
+
from .cloud_migrate import metadata
|
|
110
|
+
record['afterMetadataDigest']=digest(metadata(lib))
|
|
111
|
+
atomic_json(journal,{**record,'state':'verified'})
|
|
112
|
+
if migration:
|
|
113
|
+
from .cloud_migrate import verified
|
|
114
|
+
verified(lib.root)
|
|
115
|
+
return {'state':'coordinator-instructions-renewed','coordinators':3,'catalogDigest':target_digest,
|
|
116
|
+
'tasksCreated':0,'nativeSessionsCreated':0,'cloudTaskStateChanged':False}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""One fixed no-tool turn through the owning proxy; never replays delivery."""
|
|
2
|
+
import pathlib
|
|
3
|
+
import uuid
|
|
4
|
+
from .storage import PackError,atomic_json,read_json
|
|
5
|
+
|
|
6
|
+
async def run(owner,params):
|
|
7
|
+
if not isinstance(params,dict) or set(params)!={'operationId','key'}:
|
|
8
|
+
raise PackError('Exact desktop acceptance identity required')
|
|
9
|
+
try:valid=str(uuid.UUID(params['operationId']))==params['operationId']
|
|
10
|
+
except (ValueError,TypeError,AttributeError):valid=False
|
|
11
|
+
if not valid:
|
|
12
|
+
raise PackError('Acceptance UUID required')
|
|
13
|
+
path=pathlib.Path(owner.runtime)/'desktop-acceptance'/(params['operationId']+'.json')
|
|
14
|
+
rows=[r for r in owner.execution.roles.values() if r['key']==params['key']]
|
|
15
|
+
if len(rows)!=1:
|
|
16
|
+
raise PackError('Owned acceptance role missing')
|
|
17
|
+
role=rows[0];tid=role['threadId']
|
|
18
|
+
if path.exists():
|
|
19
|
+
record=read_json(path)
|
|
20
|
+
if record['key']!=params['key'] or record['threadId']!=tid:
|
|
21
|
+
raise PackError('Acceptance identity changed')
|
|
22
|
+
if not record.get('turnId'):return record
|
|
23
|
+
native=await owner.call_native('thread/read',{'threadId':tid,'includeTurns':True})
|
|
24
|
+
matches=[t for t in native['thread'].get('turns',[]) if t['id']==record['turnId']]
|
|
25
|
+
if len(matches)!=1:
|
|
26
|
+
raise PackError('Exact acceptance turn missing')
|
|
27
|
+
turn=matches[0];items=turn.get('items',[])
|
|
28
|
+
text='\n'.join(i.get('text','') for i in items if i.get('type')=='agentMessage' and i.get('phase')!='commentary')
|
|
29
|
+
record.update(nativeStatus=turn['status'],reply=text)
|
|
30
|
+
record['verified']=turn['status']=='completed' and text.strip().rstrip('.')=='TOPOLO_DESKTOP_ACCEPTED' and all(i.get('type') in {'userMessage','agentMessage','reasoning','tokenUsage'} for i in items)
|
|
31
|
+
atomic_json(path,record);return record
|
|
32
|
+
if owner.active_turns or owner.pending or owner.quiesced:
|
|
33
|
+
raise PackError('Acceptance requires an idle owner')
|
|
34
|
+
await owner.call_native('thread/resume',{'threadId':tid})
|
|
35
|
+
spec=role['settings'];record={**params,'threadId':tid,'state':'reserved','verified':False,'settings':spec}
|
|
36
|
+
atomic_json(path,record)
|
|
37
|
+
# Save uncertainty before sending. A lost ACK cannot create another turn.
|
|
38
|
+
record['state']='delivery-uncertain';atomic_json(path,record)
|
|
39
|
+
roots=[role['cwd']]
|
|
40
|
+
home=getattr(owner.execution,'desktop_home',None)
|
|
41
|
+
if home is not None:
|
|
42
|
+
matches=list((pathlib.Path(home)/'visualizations').glob('*/*/*/'+tid))
|
|
43
|
+
if len(matches)==1:roots.append(str(matches[0]))
|
|
44
|
+
result=await owner.call_native('turn/start',{'threadId':tid,'input':[{'type':'text','text':'Transport acceptance test only. Do not use tools, read files, perform work, contact anyone or resume any task. Reply exactly TOPOLO_DESKTOP_ACCEPTED.'}],'environments':[{'environmentId':'local','cwd':role['cwd'],'runtimeWorkspaceRoots':roots}], 'runtimeWorkspaceRoots':roots, 'collaborationMode':{'mode':'default','settings':{'model':spec['model'],'reasoning_effort':spec['model_reasoning_effort'],'developer_instructions':None}}})
|
|
45
|
+
record.update(state='native-accepted',turnId=result['turn']['id']);atomic_json(path,record);return record
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""An owned Dock launcher for the selected Agent Pack, separate from personal apps."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import plistlib
|
|
7
|
+
import shlex
|
|
8
|
+
import re
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
|
|
13
|
+
from .storage import PackError, atomic_json, read_json
|
|
14
|
+
from .work import require
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _digest(path):
|
|
18
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _files(root):
|
|
22
|
+
result = {}
|
|
23
|
+
for path in sorted(root.rglob('*')):
|
|
24
|
+
metadata = path.lstat()
|
|
25
|
+
require(not path.is_symlink() and metadata.st_uid == os.getuid()
|
|
26
|
+
and not metadata.st_mode & 0o022, 'Desktop launcher contains an unowned or writable entry')
|
|
27
|
+
if path.is_file():
|
|
28
|
+
result[str(path.relative_to(root))] = _digest(path)
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def install(lib, app, codex_bin, run=None):
|
|
33
|
+
"""Caller owns Library.lock. Compilation/configuration does not open any app."""
|
|
34
|
+
require(sys.platform == 'darwin', 'Managed Dock launchers require macOS')
|
|
35
|
+
run = run or subprocess.run
|
|
36
|
+
root = lib.root.resolve(); folder = root / 'launchers'
|
|
37
|
+
folder.mkdir(exist_ok=True, mode=0o700)
|
|
38
|
+
target = folder / 'Topolo Agent Pack.app'; shim = folder / 'topolo-agent-pack-open'
|
|
39
|
+
record = folder / 'desktop-launcher.json'
|
|
40
|
+
require(not folder.is_symlink() and folder.stat().st_uid == os.getuid()
|
|
41
|
+
and not folder.stat().st_mode & 0o022, 'Unowned desktop launcher directory')
|
|
42
|
+
app = Path(app).expanduser().resolve(); native = Path(codex_bin).expanduser().resolve()
|
|
43
|
+
require(app.is_dir() and native.is_file(), 'Existing native application and CLI are required')
|
|
44
|
+
adapter = Path(__file__).resolve().parents[2] / 'topolo-agent-pack'
|
|
45
|
+
require(adapter.is_file(), 'Installed public pack adapter is unavailable')
|
|
46
|
+
argv = [sys.executable, str(adapter), '--store', str(root), 'open', '--app', str(app), '--codex-bin', str(native)]
|
|
47
|
+
source = '#!' + sys.executable + '\nimport os\nos.execv(' + repr(sys.executable) + ',' + repr(argv) + ')\n'
|
|
48
|
+
script = ('on openPack()\ntry\ndo shell script ' + json.dumps(shlex.join([str(shim)])) +
|
|
49
|
+
'\non error messageText number errorNumber\ndisplay alert "Topolo Agent Pack" message messageText as warning\nend try\nend openPack\non run\nmy openPack()\nend run\non reopen\nmy openPack()\nend reopen\n')
|
|
50
|
+
script_digest = hashlib.sha256(script.encode()).hexdigest()
|
|
51
|
+
previous = read_json(record, {})
|
|
52
|
+
if previous:
|
|
53
|
+
require(set(previous) == {'format', 'store', 'app', 'scriptSHA256', 'appFiles', 'shimSHA256'}
|
|
54
|
+
and previous['format'] == 'topolo-managed-desktop-launcher-v1'
|
|
55
|
+
and previous['store'] == str(root) and previous['app'] == str(target)
|
|
56
|
+
and previous['scriptSHA256'] == script_digest
|
|
57
|
+
and not target.is_symlink() and target.is_dir() and _files(target) == previous['appFiles']
|
|
58
|
+
and not shim.is_symlink() and shim.is_file() and _digest(shim) == previous['shimSHA256'],
|
|
59
|
+
'Existing managed Dock launcher changed; reconcile before replacement')
|
|
60
|
+
else:
|
|
61
|
+
require(not target.exists() and not target.is_symlink() and not shim.exists() and not shim.is_symlink(),
|
|
62
|
+
'Desktop launcher destination is not an owned installation')
|
|
63
|
+
with tempfile.TemporaryDirectory(prefix='.desktop-launcher-', dir=folder) as temporary:
|
|
64
|
+
temporary = Path(temporary); apple = temporary / 'launcher.applescript'; apple.write_text(script)
|
|
65
|
+
built = temporary / target.name
|
|
66
|
+
run(['/usr/bin/osacompile', '-s', '-o', str(built), str(apple)], check=True, timeout=30)
|
|
67
|
+
info = built / 'Contents/Info.plist'
|
|
68
|
+
value = plistlib.loads(info.read_bytes())
|
|
69
|
+
value.update(CFBundleIdentifier='io.topolo.agent-pack.' + hashlib.sha256(str(root).encode()).hexdigest()[:16],
|
|
70
|
+
CFBundleName='Topolo Agent Pack', CFBundleDisplayName='Topolo Agent Pack')
|
|
71
|
+
info.write_bytes(plistlib.dumps(value))
|
|
72
|
+
run(['/usr/bin/codesign', '--force', '--sign', '-', str(built)], check=True, timeout=30)
|
|
73
|
+
run(['/usr/bin/codesign', '--verify', '--deep', '--strict', str(built)], check=True, timeout=15)
|
|
74
|
+
_files(built)
|
|
75
|
+
os.rename(built, target)
|
|
76
|
+
# Refuse unjournaled partial publication instead of silently replacing it.
|
|
77
|
+
temporary = shim.with_name(shim.name + '.new')
|
|
78
|
+
require(not temporary.exists() and not temporary.is_symlink(), 'Desktop launcher publication is unresolved')
|
|
79
|
+
with temporary.open('x') as stream:
|
|
80
|
+
os.chmod(temporary, 0o700); stream.write(source); stream.flush(); os.fsync(stream.fileno())
|
|
81
|
+
os.replace(temporary, shim)
|
|
82
|
+
value = {'format': 'topolo-managed-desktop-launcher-v1', 'store': str(root), 'app': str(target),
|
|
83
|
+
'scriptSHA256': script_digest, 'appFiles': _files(target), 'shimSHA256': _digest(shim)}
|
|
84
|
+
atomic_json(record, value)
|
|
85
|
+
require(read_json(record) == value and _files(target) == value['appFiles'], 'Desktop launcher readback differs')
|
|
86
|
+
return {'state': 'managed-desktop-launcher-installed', 'app': str(target), 'store': str(root),
|
|
87
|
+
'appOpened': False, 'dockPreferencesChanged': False}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def restore(lib, bridge, app, run=None):
|
|
91
|
+
"""Activate only the current managed PID; never prepare or restart its backend."""
|
|
92
|
+
require(sys.platform == 'darwin', 'Managed window restoration requires macOS')
|
|
93
|
+
run = run or subprocess.run
|
|
94
|
+
pid = bridge.get('desktopPid'); expected_runtime = lib.active().get('runtime')
|
|
95
|
+
require(type(pid) is int and pid > 1 and bridge.get('runtime') == expected_runtime
|
|
96
|
+
and bridge.get('nativeTransport') == 'owned-stdio', 'Exact managed Desktop owner is required')
|
|
97
|
+
os.kill(pid, 0)
|
|
98
|
+
command = run(['/bin/ps', '-p', str(pid), '-o', 'command='], check=True, capture_output=True,
|
|
99
|
+
text=True, timeout=5).stdout.strip()
|
|
100
|
+
bundle = Path(app).expanduser().resolve()
|
|
101
|
+
executable_name = plistlib.loads((bundle / 'Contents/Info.plist').read_bytes())['CFBundleExecutable']
|
|
102
|
+
require(isinstance(executable_name, str) and Path(executable_name).name == executable_name,
|
|
103
|
+
'Native application executable identity differs')
|
|
104
|
+
executable = str(bundle / 'Contents/MacOS' / executable_name)
|
|
105
|
+
flag = '--user-data-dir=' + str(lib.root / 'desktop-data')
|
|
106
|
+
require(command.startswith(executable + ' ') and re.search(r'(?:^|\s)' + re.escape(flag) + r'(?:\s|$)', command),
|
|
107
|
+
'Managed Desktop process does not own this library profile')
|
|
108
|
+
# AppKit activation targets this PID. Electron's normal activate handler
|
|
109
|
+
# restores its primary window; a same-bundle personal process is untouched.
|
|
110
|
+
script = ('ObjC.import("AppKit"); const app=$.NSRunningApplication.runningApplicationWithProcessIdentifier(' +
|
|
111
|
+
str(pid) + '); if(!app || !app.activateWithOptions($.NSApplicationActivateAllWindows|$.NSApplicationActivateIgnoringOtherApps)) throw Error("Managed window activation refused");')
|
|
112
|
+
run(['/usr/bin/osascript', '-l', 'JavaScript', '-e', script], check=True, timeout=15)
|
|
113
|
+
return {'state': 'managed-window-restoration-requested', 'desktopPid': pid,
|
|
114
|
+
'runtime': expected_runtime, 'backendRestarted': False, 'frontendVerified': False}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Selected immutable operating references, separate from task history."""
|
|
2
|
+
import hashlib,pathlib
|
|
3
|
+
from .storage import atomic_json
|
|
4
|
+
from .work import require
|
|
5
|
+
from .cicd import DISCIPLINES
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def references(config,logical):
|
|
9
|
+
selected=logical=='intakes/cicd' or logical in {'specialists/'+name for name in DISCIPLINES}
|
|
10
|
+
refs=config.get('domainContextRefs',[])
|
|
11
|
+
require(isinstance(refs,list) and len(refs)==(1 if selected else 0),'CI/CD requires one reviewed release-controller domain reference')
|
|
12
|
+
for ref in refs:
|
|
13
|
+
require(isinstance(ref,dict) and set(ref)=={'workspaceId','documentId','versionId','bodySHA256','heading','sectionSHA256'} and all(isinstance(v,str) and v for v in ref.values()),'Invalid domain context reference')
|
|
14
|
+
require(ref['heading']=='CI/CD operating bootstrap' and all(len(ref[k])==64 and all(c in '0123456789abcdef' for c in ref[k]) for k in ['bodySHA256','sectionSHA256']),'Unqualified release-controller bootstrap reference')
|
|
15
|
+
return refs
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def materialize(refs,cwd,client):
|
|
19
|
+
receipts=[]
|
|
20
|
+
for ref in refs:
|
|
21
|
+
result=client.action('app_topolo_docs.documents.get',{'documentId':ref['documentId']},ref['workspaceId'])
|
|
22
|
+
doc=result['document'];require(isinstance(result['versions'],list) and len(result['versions'])<=128,'Operating reference history exceeds the bounded reader')
|
|
23
|
+
versions=[v for v in result['versions'] if v['id']==ref['versionId']]
|
|
24
|
+
require(doc['id']==ref['documentId'] and doc['workspaceId']==ref['workspaceId'] and doc['organizationId']=='org_topolo_platform' and len(versions)==1,'Pinned operating reference is unavailable')
|
|
25
|
+
body=versions[0]['bodyMarkdown']
|
|
26
|
+
require(isinstance(body,str) and len(body.encode())<=1000000,'Operating reference body exceeds the bounded reader')
|
|
27
|
+
require(hashlib.sha256(body.encode()).hexdigest()==ref['bodySHA256'],'Pinned operating reference changed')
|
|
28
|
+
marker='## '+ref['heading']+'\n'
|
|
29
|
+
require(body.count(marker)==1,'Operating bootstrap heading is missing or ambiguous')
|
|
30
|
+
section=body.split(marker,1)[1].split('\n## ',1)[0].strip()+'\n'
|
|
31
|
+
require(100<=len(section.encode())<=12000 and hashlib.sha256(section.encode()).hexdigest()==ref['sectionSHA256'],'Operating bootstrap section differs')
|
|
32
|
+
require(all(term in section for term in ['Topolo-io/TopoloReleaseController','topolo-box-submit','"op":"intent-status"','"op":"job-diagnostic"','hosts','read-only']),'Operating bootstrap lacks controller access and custody boundaries')
|
|
33
|
+
target=pathlib.Path(cwd)/'context';require(not target.is_symlink(),'Domain context directory cannot be a symlink');target.mkdir(exist_ok=True,mode=0o700)
|
|
34
|
+
path=target/'release-controller.md';require(not path.is_symlink(),'Domain context file cannot be a symlink')
|
|
35
|
+
path.write_text('# Verified CI/CD operating reference\n\n'+marker+section);path.chmod(0o600)
|
|
36
|
+
receipts.append(dict(ref))
|
|
37
|
+
if receipts:atomic_json(pathlib.Path(cwd)/'context/domain-source.json',{'format':'topolo-selected-domain-context-v1','references':receipts,'taskHistoryLoaded':False,'wholeDocumentLoadedIntoContext':False})
|
|
38
|
+
return receipts
|