@topolo/cli 0.13.19 → 0.13.22
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/agent-packs/README.md +421 -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 +181 -16
- 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 +103 -6
- 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/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/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 +152 -73
- package/package.json +5 -5
|
@@ -96,7 +96,7 @@ def maps(values):
|
|
|
96
96
|
result[old]=new
|
|
97
97
|
return result
|
|
98
98
|
|
|
99
|
-
def verify_backend(lib, runtime, cli):
|
|
99
|
+
def verify_backend(lib, runtime, cli, presentation_journal=None, reconcile=None):
|
|
100
100
|
"""Read back through the installed native protocol; do not load/resume or start turns."""
|
|
101
101
|
if not cli: raise PackError('Native Codex binary not found; supply --codex-bin.')
|
|
102
102
|
cli=pathlib.Path(cli).expanduser().resolve()
|
|
@@ -129,9 +129,24 @@ def verify_backend(lib, runtime, cli):
|
|
|
129
129
|
buffer+=chunk
|
|
130
130
|
raise PackError('Native read-back timed out: '+method)
|
|
131
131
|
checked=[]
|
|
132
|
+
account=None;sidebar=None
|
|
133
|
+
binary_hash=hashlib.sha256(cli.read_bytes()).hexdigest()
|
|
134
|
+
def current_context():
|
|
135
|
+
config=request('config/read',{'cwd':str(home),'includeLayers':True})
|
|
136
|
+
origins=[{'name':layer['name'],'version':layer['version']} for layer in config.get('layers',[])
|
|
137
|
+
if layer.get('name',{}).get('type')=='user' and not layer.get('disabledReason')]
|
|
138
|
+
if not origins or any(row['name'].get('file')!=str(home/'config.toml') or not isinstance(row['version'],str) or not row['version'] for row in origins):
|
|
139
|
+
raise PackError('Native verification execution-home provenance differs')
|
|
140
|
+
actual=request('account/read',{'refreshToken':False})
|
|
141
|
+
native.sidebar_binding(runtime,actual) # Validate the native schema without effects.
|
|
142
|
+
return {'kind':'offline','home':str(home),'binary':str(cli),'binarySHA256':binary_hash,
|
|
143
|
+
'configOrigins':origins,'account':{'type':(actual.get('account') or {}).get('type'),
|
|
144
|
+
'accountId':(actual.get('workspaceRouting') or {}).get('chatgptAccountId'),
|
|
145
|
+
'requiresOpenaiAuth':actual['requiresOpenaiAuth']},'readerPID':proc.pid},actual
|
|
132
146
|
try:
|
|
133
147
|
init=request('initialize',{'clientInfo':{'name':'topolo_agent_pack','version':__version__},'capabilities':{'experimentalApi':True}})
|
|
134
148
|
proc.stdin.write(canonical({'method':'initialized'})+b'\n');proc.stdin.flush()
|
|
149
|
+
initial_context,account=current_context()
|
|
135
150
|
agents=read_json(pathlib.Path(runtime)/'agents.json')['agents']
|
|
136
151
|
for a in agents:
|
|
137
152
|
tid=a['threadId']; result=request('thread/read',{'threadId':tid,'includeTurns':False})
|
|
@@ -141,13 +156,23 @@ def verify_backend(lib, runtime, cli):
|
|
|
141
156
|
turns=request('thread/turns/list',{'threadId':tid,'limit':1,'itemsView':'notLoaded'})
|
|
142
157
|
items=request('thread/items/list',{'threadId':tid,'limit':1})
|
|
143
158
|
checked.append({'threadId':tid,'cwdVerified':True,'historyReadable':True,'sampledTurns':len(turns['data']),'sampledItems':len(items['data'])})
|
|
159
|
+
# The same original backend is still open here, after all history reads.
|
|
160
|
+
def guard():
|
|
161
|
+
context,actual=current_context()
|
|
162
|
+
if context!=initial_context:raise PackError('Native account/home/config changed during verification')
|
|
163
|
+
return context
|
|
164
|
+
if reconcile is not None:
|
|
165
|
+
path,document=reconcile
|
|
166
|
+
sidebar=native.reconcile_sidebar(lib,path,document,guard(),read_json(home/'.codex-global-state.json'),guard)
|
|
167
|
+
else:
|
|
168
|
+
sidebar=native.bind_sidebar(runtime,account,lib=lib,guard=guard,journal=presentation_journal,context=initial_context)
|
|
144
169
|
finally:
|
|
145
170
|
proc.terminate()
|
|
146
171
|
try: proc.wait(timeout=5)
|
|
147
172
|
except subprocess.TimeoutExpired: proc.kill(); proc.wait()
|
|
148
173
|
sel.close(); proc.stdin.close(); proc.stdout.close(); stderr.close()
|
|
149
|
-
report={'boundary':'native backend read-back; desktop rendering not verified','agents':checked,'modelTurnsStarted':False}
|
|
150
|
-
atomic_json(pathlib.Path(runtime)/'backend-proof.json',report)
|
|
174
|
+
report={'boundary':'native backend read-back; desktop rendering not verified','agents':checked,'modelTurnsStarted':False,'sidebar':sidebar}
|
|
175
|
+
if reconcile is None:atomic_json(pathlib.Path(runtime)/'backend-proof.json',report)
|
|
151
176
|
return report
|
|
152
177
|
|
|
153
178
|
def desktop_open(runtime, app, user_data, codex_bin=None):
|
|
@@ -165,14 +190,20 @@ def desktop_open(runtime, app, user_data, codex_bin=None):
|
|
|
165
190
|
return {'state':'managed-window-launch-requested','app':str(app),'frontendVerified':False,'note':'One-time managed window launch. Future changes reconnect this same window. Authenticate locally if prompted.'}
|
|
166
191
|
|
|
167
192
|
def apply_plan(lib,plan,args):
|
|
193
|
+
if lib.pending(): raise PackError('An incomplete operation needs recovery. Run topolo-agent-pack recover before another mutation.')
|
|
168
194
|
if plan['noChange']:
|
|
169
195
|
active=lib.active()
|
|
196
|
+
if live.available(lib):
|
|
197
|
+
sidebar=live.bind_sidebar(lib,active['runtime'],args.app)
|
|
198
|
+
else:
|
|
199
|
+
native.assert_idle(pathlib.Path(active['runtime'])/'home')
|
|
200
|
+
sidebar=verify_backend(lib,active['runtime'],args.codex_bin)['sidebar']
|
|
170
201
|
if not args.no_open and not live.available(lib):
|
|
171
202
|
native.assert_idle(pathlib.Path(active['runtime'])/'home')
|
|
172
203
|
active['desktop']=desktop_open(active['runtime'],args.app,lib.root/'desktop-data',args.codex_bin)
|
|
173
204
|
active['state']=active['desktop']['state'];atomic_json(lib.root/'active.json',active)
|
|
174
|
-
return {'state':active['state'],'active':active}
|
|
175
|
-
return {'state':'unchanged','active':active}
|
|
205
|
+
return {'state':active['state'],'active':active,'sidebarBinding':sidebar,'sidebar':sidebar,'sectionsRenderedVerified':False}
|
|
206
|
+
return {'state':'unchanged','active':active,'sidebar':sidebar,'sidebarBinding':sidebar,'sectionsRenderedVerified':False}
|
|
176
207
|
if lib.pending(): raise PackError('An incomplete operation needs recovery. Run topolo-agent-pack recover before another mutation.')
|
|
177
208
|
before=plan['before']; old_runtime=pathlib.Path(before['runtime']) if before.get('runtime') else None
|
|
178
209
|
bridge=live.available(lib)
|
|
@@ -238,7 +269,7 @@ def apply_plan(lib,plan,args):
|
|
|
238
269
|
journal['phase']='staged';atomic_json(jpath,journal)
|
|
239
270
|
if old_runtime and old_runtime!=runtime and (old_runtime/'home/auth.json').exists():
|
|
240
271
|
shutil.copyfile(old_runtime/'home/auth.json',runtime/'home/auth.json');(runtime/'home/auth.json').chmod(0o600)
|
|
241
|
-
backend=verify_backend(lib,runtime,args.codex_bin)
|
|
272
|
+
backend=verify_backend(lib,runtime,args.codex_bin,presentation_journal=(jpath,journal))
|
|
242
273
|
if not plan.get('resumeRuntime'): sync_tree(runtime)
|
|
243
274
|
if old_runtime: native.assert_idle(old_runtime/'home')
|
|
244
275
|
if lib.active()!=before: raise PackError('Active composition changed concurrently; refusing to overwrite it.')
|
|
@@ -247,16 +278,16 @@ def apply_plan(lib,plan,args):
|
|
|
247
278
|
atomic_json(lib.root/'active.json',after)
|
|
248
279
|
journal['phase']='committed';atomic_json(jpath,journal)
|
|
249
280
|
if bridge or not args.no_open:
|
|
250
|
-
after['desktop']=live.reconnect(lib,runtime,old_runtime,args.app) if bridge else desktop_open(runtime,args.app,lib.root/'desktop-data',args.codex_bin)
|
|
281
|
+
after['desktop']=live.reconnect(lib,runtime,old_runtime,args.app,presentation_journal=(jpath,journal)) if bridge else desktop_open(runtime,args.app,lib.root/'desktop-data',args.codex_bin)
|
|
251
282
|
after['state']=after['desktop']['state'];atomic_json(lib.root/'active.json',after)
|
|
252
283
|
journal['after']=after;journal['phase']='complete';atomic_json(jpath,journal)
|
|
253
284
|
return {'state':after['state'],'agents':proof['agents'],'totalHistoryThreads':proof['threads'],'runtime':str(runtime),'checkpoint':journal.get('checkpoint'),
|
|
254
|
-
'desktop':after['desktop'],'backend':backend,'dependencies':proof['dependencies'],'activeGoalsPaused':proof['activeGoalsPaused']}
|
|
285
|
+
'desktop':after['desktop'],'backend':backend,'sidebarBinding':after['desktop'].get('sidebarBinding',backend['sidebar']),'sectionsRenderedVerified':False,'dependencies':proof['dependencies'],'activeGoalsPaused':proof['activeGoalsPaused']}
|
|
255
286
|
except Exception as exc:
|
|
256
287
|
journal['failure']=str(exc);atomic_json(jpath,journal)
|
|
257
|
-
if bridge and live.available(lib) and live.available(lib).get('quiesced'):
|
|
288
|
+
if bridge and journal.get('sidebar',{}).get('state') not in {'UNKNOWN','may-have-effects'} and live.available(lib) and live.available(lib).get('quiesced'):
|
|
258
289
|
try:
|
|
259
|
-
current=lib.active();journal['reconnectAfterFailure']=live.reconnect(lib,current['runtime'],old_runtime,args.app);atomic_json(jpath,journal)
|
|
290
|
+
current=lib.active();journal['reconnectAfterFailure']=live.reconnect(lib,current['runtime'],old_runtime,args.app,presentation_journal=(jpath,journal));atomic_json(jpath,journal)
|
|
260
291
|
except Exception as reconnect_error:
|
|
261
292
|
journal['reconnectFailure']=str(reconnect_error);atomic_json(jpath,journal)
|
|
262
293
|
raise
|
|
@@ -265,11 +296,24 @@ def recover(lib):
|
|
|
265
296
|
result=[]
|
|
266
297
|
for path in sorted(lib.pending()):
|
|
267
298
|
j=read_json(path); active=lib.active(); before=j['before']; after=j.get('after')
|
|
299
|
+
entry=j.get('sidebar')
|
|
300
|
+
if entry and entry.get('state') not in {'complete','not-executed'}:
|
|
301
|
+
context=entry['context'];runtime=entry['runtime']
|
|
302
|
+
if context['kind']=='managed':
|
|
303
|
+
live.bind_sidebar(lib,runtime,context.get('app',j.get('desktopApp','/Applications/ChatGPT.app')),reconcile=(path,j))
|
|
304
|
+
else:
|
|
305
|
+
binary=pathlib.Path(context['binary'])
|
|
306
|
+
if hashlib.sha256(binary.read_bytes()).hexdigest()!=context['binarySHA256']:
|
|
307
|
+
raise PackError('Original sidebar recovery binary changed')
|
|
308
|
+
native.assert_idle(pathlib.Path(runtime)/'home')
|
|
309
|
+
verify_backend(lib,runtime,str(binary),reconcile=(path,j))
|
|
310
|
+
if j.get('operation')=='sidebar-bind':
|
|
311
|
+
result.append({'transaction':j['id'],'phase':j['phase'],'action':'Exact current presentation reconciled; no presentation replay.'});continue
|
|
268
312
|
previous=(active.get('runtime')==before.get('runtime') and active.get('transaction')==before.get('transaction') and active.get('components')==before.get('components'))
|
|
269
313
|
committed=bool(after and active.get('transaction')==j['id'] and active.get('runtime')==after['runtime'] and active.get('components')==after.get('components'))
|
|
270
314
|
if not previous and not committed:raise PackError('Recovery found a concurrent active-pointer change; no state overwritten. Inspect '+str(path))
|
|
271
315
|
if j.get('liveBridge'):
|
|
272
|
-
j['desktopRecovery']=live.reconnect(lib,active['runtime'],before.get('runtime'),j.get('desktopApp','/Applications/ChatGPT.app'))
|
|
316
|
+
j['desktopRecovery']=live.reconnect(lib,active['runtime'],before.get('runtime'),j.get('desktopApp','/Applications/ChatGPT.app'),presentation_journal=(path,j))
|
|
273
317
|
active['desktop']=j['desktopRecovery'];active['state']=active['desktop']['state'];atomic_json(lib.root/'active.json',active)
|
|
274
318
|
if previous:
|
|
275
319
|
j['phase']='rolled-back';j['recovery']='Previous active pointer retained; staged files kept for inspection.'
|
|
@@ -290,6 +334,8 @@ def parser():
|
|
|
290
334
|
p.add_argument('--version',action='version',version=__version__)
|
|
291
335
|
p.add_argument('--store',default=os.environ.get('CODEX_PACK_HOME',str(pathlib.Path.home()/'AgentPacks')))
|
|
292
336
|
sub=p.add_subparsers(dest='command',required=True)
|
|
337
|
+
q=sub.add_parser('desktop-launcher',help='Install the owned same-profile Agent Pack Dock launcher without opening it')
|
|
338
|
+
q.add_argument('--app',default='/Applications/ChatGPT.app');q.add_argument('--codex-bin',default=find_codex())
|
|
293
339
|
q=sub.add_parser('runtime-adoption',help='Configuration-only private runtime plan/apply; no consumer startup or authentication')
|
|
294
340
|
q.add_argument('operation',choices=['plan','apply']);q.add_argument('--manifest',required=True)
|
|
295
341
|
q.add_argument('--plan-digest');q.add_argument('--confirm',action='store_true')
|
|
@@ -337,6 +383,33 @@ def parser():
|
|
|
337
383
|
q=sub.add_parser('smoke',help='Start or read one closed, bound native acceptance case; never arbitrary prompts')
|
|
338
384
|
q.add_argument('operation',choices=['start','read']);q.add_argument('identity')
|
|
339
385
|
q.add_argument('--case',choices=sorted(live.SMOKE_CASES));q.add_argument('--operation-id')
|
|
386
|
+
q=sub.add_parser('work',help='Durable task dispatch and checkpoint reads on the owning connection')
|
|
387
|
+
q.add_argument('operation',choices=['submit','respond','re-review','list','read','retained','adoption-plan','adopt','wait','cutover','cutover-status','claude-hook','claude-worker-hook','bind-claude','serve-claude'])
|
|
388
|
+
q.add_argument('--input',help='Exact task request JSON')
|
|
389
|
+
q=sub.add_parser('work-recover',help='Recover verified cloud evidence to a new directory without creating or adopting work')
|
|
390
|
+
q.add_argument('--reference',required=True);q.add_argument('--destination',required=True)
|
|
391
|
+
q=sub.add_parser('work-cloud-cutover',help='Conditionally activate the existing cloud dataset from its authenticated idle native owner')
|
|
392
|
+
q.add_argument('--source-store',required=True);q.add_argument('--input',required=True)
|
|
393
|
+
q=sub.add_parser('work-closed-ledger-fence',help='Permanently fence a closed local task ledger without stopping unrelated native work')
|
|
394
|
+
q.add_argument('--input',required=True)
|
|
395
|
+
q=sub.add_parser('work-cloud-prepare-delta',help='Publish only selected closed legacy task context, final outputs and declared evidence')
|
|
396
|
+
q.add_argument('--source-store',required=True);q.add_argument('--input',required=True)
|
|
397
|
+
q=sub.add_parser('work-install',help='Prepare a three-coordinator installation from cloud specialization metadata; requires cloud authority before execution')
|
|
398
|
+
q.add_argument('--pack',required=True);q.add_argument('--agent-workspace',required=True);q.add_argument('--bytes-workspace',required=True);q.add_argument('--max-workers',type=int,default=4)
|
|
399
|
+
q=sub.add_parser('work-cloud-attach',help='Attach a clean installation to existing activated cloud task authority without dispatch')
|
|
400
|
+
q.add_argument('--input',required=True)
|
|
401
|
+
q=sub.add_parser('work-cloud-migrate',help='Plan or apply exact existing fenced Codex installation attachment without adopting cloud tasks')
|
|
402
|
+
q.add_argument('--input',required=True);q.add_argument('--apply',action='store_true')
|
|
403
|
+
q=sub.add_parser('work-renew-coordinators',help='Renew only the three canonical conversation definitions on an idle existing cloud installation')
|
|
404
|
+
q.add_argument('--input',required=True)
|
|
405
|
+
q=sub.add_parser('work-recover-claude-completion',help='Recover one exact already-delivered Claude completion without resending or starting a dispatcher')
|
|
406
|
+
q.add_argument('--input',required=True)
|
|
407
|
+
q=sub.add_parser('work-repair-model-ceilings',help='Repair stale Claude model ceilings from the existing qualified three-tier catalog only')
|
|
408
|
+
q.add_argument('--input',required=True)
|
|
409
|
+
q=sub.add_parser('work-refresh-role-refs',help='Refresh reviewed references only on the same stopped inactive cloud installation')
|
|
410
|
+
q.add_argument('--input',required=True)
|
|
411
|
+
q=sub.add_parser('work-bind-roles',help='Discover individual Agent definitions and bind metadata-only dispatch references')
|
|
412
|
+
q.add_argument('--pack',action='append',required=True);q.add_argument('--agent-workspace',required=True);q.add_argument('--bytes-workspace')
|
|
340
413
|
for command in ['connect','remote-context','remember','artifact','artifact-get','remote-recover']:
|
|
341
414
|
q=sub.add_parser(command,help='Use public Topolo Agent continuity and Bytes artifact contracts')
|
|
342
415
|
q.add_argument('name');q.add_argument('--environment',choices=['development','staging','production'],required=True)
|
|
@@ -371,13 +444,14 @@ def main(argv=None):
|
|
|
371
444
|
parser().error('--harness requires codex or claude')
|
|
372
445
|
harness = argv[at + 1]
|
|
373
446
|
del argv[at:at + 2]
|
|
374
|
-
policy_command = 'policy' in argv or 'permissions' in argv
|
|
375
447
|
command_args=list(argv)
|
|
376
448
|
if command_args and command_args[0]=='--store':command_args=command_args[2:]
|
|
377
449
|
elif command_args and command_args[0].startswith('--store='):command_args=command_args[1:]
|
|
450
|
+
policy_command=bool(command_args and command_args[0] in {'policy','permissions'})
|
|
378
451
|
lane_command=bool(command_args and command_args[0]=='lane')
|
|
379
|
-
|
|
380
|
-
|
|
452
|
+
work_command=bool(command_args and command_args[0]=='work')
|
|
453
|
+
cloud_command=bool(command_args and command_args[0] in {'connect','remote-context','remember','artifact','artifact-get','remote-recover','work-recover','work-cloud-cutover','work-closed-ledger-fence','work-cloud-prepare-delta','work-bind-roles','work-install','work-refresh-role-refs','work-repair-model-ceilings','work-recover-claude-completion','work-cloud-attach','work-cloud-migrate','work-renew-coordinators'})
|
|
454
|
+
if harness == 'claude' and not policy_command and not cloud_command and not lane_command and not work_command and not (command_args and command_args[0]=='runtime-adoption'):
|
|
381
455
|
from . import claude
|
|
382
456
|
return claude.main(argv)
|
|
383
457
|
if harness not in {'codex', 'claude'}:
|
|
@@ -391,7 +465,62 @@ def main(argv=None):
|
|
|
391
465
|
raise PackError('Runtime adoption is one explicit cross-host owner manifest, not a harness override')
|
|
392
466
|
emit(live.private_runtime_adoption(args.store,args.manifest,args.operation,args.plan_digest,args.confirm))
|
|
393
467
|
return 0
|
|
468
|
+
if args.command=='work-recover':
|
|
469
|
+
from .cloud_recovery import recover as recover_cloud
|
|
470
|
+
emit(recover_cloud(read_json(args.reference),args.destination))
|
|
471
|
+
return 0
|
|
472
|
+
if args.command=='work-cloud-cutover':
|
|
473
|
+
from .cloud_cutover import run
|
|
474
|
+
emit(run(args.source_store,args.store,read_json(args.input)));return 0
|
|
475
|
+
if args.command=='work-closed-ledger-fence':
|
|
476
|
+
from .closed_ledger_fence import run
|
|
477
|
+
emit(run(args.store,read_json(args.input)));return 0
|
|
478
|
+
if args.command=='work-cloud-prepare-delta':
|
|
479
|
+
from .cloud_delta import run
|
|
480
|
+
emit(run(args.source_store,args.store,read_json(args.input)));return 0
|
|
481
|
+
if args.command=='work' and args.operation=='claude-worker-hook':
|
|
482
|
+
if harness!='claude' or not args.input:raise PackError('Exact Claude worker hook binding required')
|
|
483
|
+
from .claude_runner import hook_main
|
|
484
|
+
return hook_main(args.input)
|
|
394
485
|
lib=Library(args.store)
|
|
486
|
+
if args.command=='work-install':
|
|
487
|
+
from .role_catalog import install
|
|
488
|
+
emit(install(lib,args.pack,args.agent_workspace,args.bytes_workspace,args.max_workers))
|
|
489
|
+
return 0
|
|
490
|
+
if args.command=='work-cloud-attach':
|
|
491
|
+
from .cloud_attach import attach
|
|
492
|
+
emit(attach(lib,read_json(args.input)));return 0
|
|
493
|
+
if args.command=='work-renew-coordinators':
|
|
494
|
+
from .coordinator_refresh import renew
|
|
495
|
+
emit(renew(lib,read_json(args.input)));return 0
|
|
496
|
+
if args.command=='work-cloud-migrate':
|
|
497
|
+
if harness!='codex':raise PackError('Existing fenced Codex installation migration required')
|
|
498
|
+
from .cloud_migrate import plan,apply
|
|
499
|
+
value=read_json(args.input)
|
|
500
|
+
emit(apply(lib,value) if args.apply else plan(lib,value));return 0
|
|
501
|
+
if args.command=='work-recover-claude-completion':
|
|
502
|
+
if harness!='claude':raise PackError('Claude-only production native completion recovery required')
|
|
503
|
+
from .claude_completion_recovery import recover_completed
|
|
504
|
+
value=read_json(args.input)
|
|
505
|
+
if not isinstance(value,dict):raise PackError('Closed native completion recovery object required')
|
|
506
|
+
emit(recover_completed(lib,value));return 0
|
|
507
|
+
if args.command=='work-repair-model-ceilings':
|
|
508
|
+
from .role_catalog import repair_model_ceilings
|
|
509
|
+
value=read_json(args.input)
|
|
510
|
+
if not isinstance(value,dict) or set(value)!={'expectedCatalogDigest','expectedNative'}:raise PackError('Exact reviewed model ceiling repair input required')
|
|
511
|
+
emit(repair_model_ceilings(lib,value['expectedCatalogDigest'],value['expectedNative']));return 0
|
|
512
|
+
if args.command=='work-refresh-role-refs':
|
|
513
|
+
from .role_refresh import refresh_refs
|
|
514
|
+
value=read_json(args.input)
|
|
515
|
+
if not isinstance(value,dict) or set(value)!={'expectedCatalogDigest','expectedBindings','reviewedBindings'}:raise PackError('Exact reviewed role reference input required')
|
|
516
|
+
emit(refresh_refs(lib,value['expectedCatalogDigest'],value['expectedBindings'],value['reviewedBindings']));return 0
|
|
517
|
+
if args.command=='work-bind-roles':
|
|
518
|
+
from .role_context import bind
|
|
519
|
+
result=bind(lib.root/'work',[lib.load(name)[0] for name in args.pack],args.agent_workspace)
|
|
520
|
+
if args.bytes_workspace:
|
|
521
|
+
atomic_json(lib.root/'work/context-authority.json',{'format':'topolo-task-context-authority-v1','organizationId':'org_topolo_platform','workspaceId':args.bytes_workspace,'scope':'org'})
|
|
522
|
+
emit(result)
|
|
523
|
+
return 0
|
|
395
524
|
if args.command in {'connect','remote-context','remember','artifact','artifact-get','remote-recover'}:
|
|
396
525
|
from . import topolo
|
|
397
526
|
if args.command=='remote-context' and args.continuity_input:
|
|
@@ -405,6 +534,29 @@ def main(argv=None):
|
|
|
405
534
|
raise PackError('Explicit Codex connection ID and reviewed input binding required')
|
|
406
535
|
import asyncio
|
|
407
536
|
asyncio.run(live.OwnedLaneConnection(lib,args.connection,read_json(args.input)).run());return 0
|
|
537
|
+
# Live requests are serialized by their authenticated owning service, which
|
|
538
|
+
# has its own owner lock. The setup lock must never span that service's
|
|
539
|
+
# lifetime or prevent its workers from reading/responding through the CLI.
|
|
540
|
+
if args.command=='work' and args.operation!='bind-claude':
|
|
541
|
+
if args.operation in {'claude-hook','serve-claude'}:
|
|
542
|
+
if harness!='claude':
|
|
543
|
+
raise PackError('This operation requires the Claude owning connection')
|
|
544
|
+
if args.operation=='claude-hook':
|
|
545
|
+
from .claude_prompt import main as hook
|
|
546
|
+
return hook(lib.root)
|
|
547
|
+
from .claude_service import Service
|
|
548
|
+
import asyncio
|
|
549
|
+
asyncio.run(Service(lib).run());return 0
|
|
550
|
+
if harness=='claude':
|
|
551
|
+
try:status=live.request(lib,'status',{})
|
|
552
|
+
except (PackError,OSError) as exc:raise PackError('Claude cloud dispatcher is not active') from exc
|
|
553
|
+
if status.get('harness')!='claude' or status.get('cloudDispatchReady') is not True:
|
|
554
|
+
raise PackError('Claude cloud dispatcher is not active')
|
|
555
|
+
params=read_json(args.input) if args.input else {}
|
|
556
|
+
if args.operation=='wait':
|
|
557
|
+
from .work_wait import wait
|
|
558
|
+
emit(wait(lib,params));return 0
|
|
559
|
+
emit(live.request(lib,'work-'+args.operation,params));return 0
|
|
408
560
|
with lib.lock():
|
|
409
561
|
if args.command=='lane':
|
|
410
562
|
from .lanes import Registry
|
|
@@ -433,6 +585,13 @@ def main(argv=None):
|
|
|
433
585
|
elif harness!='codex':
|
|
434
586
|
raise PackError('Use the native Claude lane adapter with its qualified connection')
|
|
435
587
|
else: result=(live.request_lane(lib,args.connection,'lane-'+args.operation,params) if args.connection else live.request(lib,'lane-'+args.operation,params))
|
|
588
|
+
elif args.command=='work':
|
|
589
|
+
if harness!='claude':
|
|
590
|
+
raise PackError('This operation requires the Claude owning connection')
|
|
591
|
+
if not args.input:
|
|
592
|
+
raise PackError('Exact native coordinator binding required')
|
|
593
|
+
from .claude_service import install
|
|
594
|
+
emit(install(lib.root,read_json(args.input)));return 0
|
|
436
595
|
elif args.command=='smoke':
|
|
437
596
|
if args.operation=='start':
|
|
438
597
|
if not args.case or not args.operation_id:
|
|
@@ -514,17 +673,23 @@ def main(argv=None):
|
|
|
514
673
|
blueprint.build(lib,args.source,template,args.codex_bin)
|
|
515
674
|
result=apply_plan(lib,make_plan(lib,args),args)
|
|
516
675
|
elif args.command=='recover': result=recover(lib)
|
|
676
|
+
elif args.command=='desktop-launcher':
|
|
677
|
+
from .desktop_launcher import install
|
|
678
|
+
result=install(lib,args.app,args.codex_bin)
|
|
517
679
|
elif args.command=='open':
|
|
518
680
|
active=lib.active()
|
|
519
681
|
if not active.get('runtime'):raise PackError('Select a pack first with switch NAME')
|
|
520
|
-
|
|
682
|
+
bridge=live.available(lib)
|
|
683
|
+
if bridge:
|
|
684
|
+
from .desktop_launcher import restore
|
|
685
|
+
result=restore(lib,bridge,args.app)
|
|
521
686
|
else:
|
|
522
687
|
native.assert_idle(pathlib.Path(active['runtime'])/'home')
|
|
523
688
|
result=desktop_open(active['runtime'],args.app,lib.root/'desktop-data',args.codex_bin)
|
|
524
689
|
active['desktop']=result;active['state']=result['state'];atomic_json(lib.root/'active.json',active)
|
|
525
690
|
elif args.command=='status':
|
|
526
691
|
active=lib.active();pids=native.busy_writers(pathlib.Path(active['runtime'])/'home') if active.get('runtime') else []
|
|
527
|
-
result={'store':str(lib.root),'active':active,'liveBridge':live.available(lib),'runtimeDatabaseHolders':pids,'recoveryRequired':[str(p) for p in lib.pending()],'frontendVerified':active.get('desktop',{}).get('frontendVerified',False)}
|
|
692
|
+
result={'store':str(lib.root),'active':active,'liveBridge':live.available(lib),'runtimeDatabaseHolders':pids,'recoveryRequired':[str(p) for p in lib.pending()],'frontendVerified':active.get('desktop',{}).get('frontendVerified',False),'sidebarBinding':read_json(pathlib.Path(active['runtime'])/'sidebar-binding.json',{}) if active.get('runtime') else None,'sectionsRenderedVerified':False}
|
|
528
693
|
elif args.command=='doctor':
|
|
529
694
|
checked=[]
|
|
530
695
|
if args.name:
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Fixed-schema closed-ledger migration barrier; unrelated native turns keep running."""
|
|
2
|
+
import hashlib,json,os,pathlib,sqlite3,uuid
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
from .storage import atomic_json,canonical,read_json,sync_directory
|
|
5
|
+
from .work import require
|
|
6
|
+
from .cloud_cutover import snapshot,digest,birth
|
|
7
|
+
|
|
8
|
+
CAPABILITY='topolo-closed-ledger-fence-v1'
|
|
9
|
+
META='_topolo_closed_ledger_fence'
|
|
10
|
+
TABLES={'tasks':('id','input_hash','body'),'workers':('id','task_id','body'),'events':('sequence','task_id','body'),'responses':('id','input_hash','task_id'),'retained':('id','body'),'sqlite_sequence':('name','seq')}
|
|
11
|
+
META_SQL='CREATE TABLE '+META+' (operation_id TEXT PRIMARY KEY, body TEXT NOT NULL)'
|
|
12
|
+
TABLE_SQL={'tasks':'CREATE TABLE tasks(id TEXT PRIMARY KEY, input_hash TEXT NOT NULL, body TEXT NOT NULL)','workers':'CREATE TABLE workers(id TEXT PRIMARY KEY,task_id TEXT NOT NULL,body TEXT NOT NULL)','events':'CREATE TABLE events(sequence INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL, body TEXT NOT NULL)','responses':'CREATE TABLE responses(id TEXT PRIMARY KEY,input_hash TEXT NOT NULL,task_id TEXT NOT NULL)','retained':'CREATE TABLE retained(id TEXT PRIMARY KEY,body TEXT NOT NULL)','sqlite_sequence':'CREATE TABLE sqlite_sequence(name,seq)'}
|
|
13
|
+
|
|
14
|
+
def triggers():
|
|
15
|
+
result={'_topolo_fence_'+table+'_'+op.lower():"CREATE TRIGGER _topolo_fence_"+table+'_'+op.lower()+' BEFORE '+op+' ON '+table+" BEGIN SELECT RAISE(ABORT, 'legacy task ledger is permanently cloud fenced'); END" for table in TABLES if table!='sqlite_sequence' for op in ['INSERT','UPDATE','DELETE']}
|
|
16
|
+
for op in ['INSERT','UPDATE','DELETE']:
|
|
17
|
+
name='_topolo_fence_receipt_'+op.lower()
|
|
18
|
+
result[name]='CREATE TRIGGER '+name+' BEFORE '+op+' ON '+META+(' WHEN EXISTS(SELECT 1 FROM '+META+')' if op=='INSERT' else '')+" BEGIN SELECT RAISE(ABORT, 'immutable closed ledger fence receipt'); END"
|
|
19
|
+
return result
|
|
20
|
+
|
|
21
|
+
def code_digest():
|
|
22
|
+
base=pathlib.Path(__file__).parent
|
|
23
|
+
return digest({name:hashlib.sha256((base/name).read_bytes()).hexdigest() for name in ['closed_ledger_fence.py','cloud_cutover.py']})
|
|
24
|
+
|
|
25
|
+
def owned(path,directory=False):
|
|
26
|
+
path=pathlib.Path(path);st=path.lstat()
|
|
27
|
+
require(path.is_absolute() and path.resolve()==path and not path.is_symlink() and st.st_uid==os.getuid() and not st.st_mode&0o022 and (path.is_dir() if directory else path.is_file() and st.st_nlink==1),'Closed ledger path ownership differs')
|
|
28
|
+
return {'path':str(path),'device':st.st_dev,'inode':st.st_ino,'uid':st.st_uid,'mode':st.st_mode&0o777}
|
|
29
|
+
|
|
30
|
+
def identity(root):
|
|
31
|
+
return {name:owned(root/suffix,directory=name!='database') for name,suffix in [('root','.'),('work','work'),('database','work/tasks.sqlite')]}
|
|
32
|
+
|
|
33
|
+
def schema(db,fenced):
|
|
34
|
+
tables={row[0] for row in db.execute("SELECT name FROM sqlite_master WHERE type='table'")}
|
|
35
|
+
require(tables==set(TABLES)|({META} if fenced else set()),'Closed ledger schema table inventory differs')
|
|
36
|
+
for table,columns in {**TABLES,**({META:('operation_id','body')} if fenced else {})}.items():
|
|
37
|
+
require(tuple(row[1] for row in db.execute('PRAGMA table_info('+table+')'))==columns,'Closed ledger schema columns differ')
|
|
38
|
+
require(dict(db.execute("SELECT name,sql FROM sqlite_master WHERE type='table'"))=={**TABLE_SQL,**({META:META_SQL} if fenced else {})},'Closed ledger schema definitions differ')
|
|
39
|
+
actual={row[0]:row[1] for row in db.execute("SELECT name,sql FROM sqlite_master WHERE type='trigger'")}
|
|
40
|
+
require(actual==(triggers() if fenced else {}),'Closed ledger fixed write guards differ')
|
|
41
|
+
return digest([list(row) for row in db.execute('SELECT type,name,tbl_name,sql FROM sqlite_master ORDER BY type,name')])
|
|
42
|
+
|
|
43
|
+
def owner(root,observe=None):
|
|
44
|
+
from . import live
|
|
45
|
+
bridge=root/'live-bridge.json';owned(bridge)
|
|
46
|
+
before=read_json(bridge)
|
|
47
|
+
require(set(before)=={'pid','socket','token'},'Native owner bridge fields differ')
|
|
48
|
+
result=(observe or (lambda r:live.request(SimpleNamespace(root=r),'status')))(root)
|
|
49
|
+
require(read_json(bridge)==before and result['pid']==before['pid'] and isinstance(result['activeTurns'],list) and all(isinstance(x,str) for x in result['activeTurns']) and isinstance(result['runtime'],str),'Authenticated native roster changed')
|
|
50
|
+
return {'pid':result['pid'],'birth':birth(result['pid']),'runtime':result['runtime'],'tokenDigest':hashlib.sha256(before['token'].encode()).hexdigest()},set(result['activeTurns'])
|
|
51
|
+
|
|
52
|
+
def disjoint(state,active):
|
|
53
|
+
workers={json.loads(row[2]).get('nativeId') for row in state['tables']['workers']}
|
|
54
|
+
require(not workers.intersection(active),'A ledger worker still owns an active native turn')
|
|
55
|
+
|
|
56
|
+
def target(request):
|
|
57
|
+
root=pathlib.Path(request['destinationRoot']);owned(root,True);owned(root/'work',True);owned(root/'work/cloud.json')
|
|
58
|
+
binding=read_json(root/'work/cloud.json')
|
|
59
|
+
require(binding['format']=='topolo-cloud-work-binding-v1' and binding['organizationId']=='org_topolo_platform' and all(binding[k]==request[k] for k in ['documentId','datasetId']),'Existing target cloud authority differs')
|
|
60
|
+
return digest(binding)
|
|
61
|
+
|
|
62
|
+
def validate_request(root,request):
|
|
63
|
+
require(set(request)=={'operationId','sourceDigest','proxyPid','runtime','destinationRoot','documentId','datasetId','expectedCurrentVersionId'},'Exact closed ledger migration selectors required')
|
|
64
|
+
require(all(isinstance(request[k],str) and bool(request[k]) for k in ['operationId','sourceDigest','runtime','destinationRoot','documentId','datasetId','expectedCurrentVersionId']) and type(request['proxyPid']) is int and request['proxyPid']>0,'Closed ledger selector types differ')
|
|
65
|
+
require(len(request['sourceDigest'])==64 and all(c in '0123456789abcdef' for c in request['sourceDigest']),'Reviewed snapshot digest is invalid')
|
|
66
|
+
require(pathlib.Path(request['runtime']).is_absolute() and pathlib.Path(request['runtime']).resolve()==pathlib.Path(request['runtime']) and pathlib.Path(request['runtime']).is_relative_to(root/'runtimes'),'Native runtime is outside the source installation')
|
|
67
|
+
require(str(uuid.UUID(request['operationId']))==request['operationId'] and pathlib.Path(request['destinationRoot']).resolve()!=root,'Distinct migration operation and target required')
|
|
68
|
+
|
|
69
|
+
def verify(root,observe=None):
|
|
70
|
+
root=pathlib.Path(root).resolve();ident=identity(root);journal=root/'work/cutover.json';owned(journal)
|
|
71
|
+
saved=read_json(journal)
|
|
72
|
+
require(saved['format']==CAPABILITY,'Closed ledger capability differs')
|
|
73
|
+
native,active=owner(root,observe)
|
|
74
|
+
db=sqlite3.connect('file:'+str(root/'work/tasks.sqlite')+'?mode=ro',uri=True)
|
|
75
|
+
try:
|
|
76
|
+
db.execute('BEGIN');rows=db.execute('SELECT operation_id,body FROM '+META).fetchall()
|
|
77
|
+
require(len(rows)==1,'Closed ledger durable receipt is ambiguous')
|
|
78
|
+
record=json.loads(rows[0][1]);require(rows[0][0]==saved['operationId']==record['operationId'],'Closed ledger operation identity differs')
|
|
79
|
+
require(record['format']==CAPABILITY and record['state']=='ledger-fenced' and record['root']==str(root) and record['databaseIdentity']==ident and record['codeDigest']==code_digest() and record['nativeOwner']==native,'Closed ledger ownership or executing source changed')
|
|
80
|
+
require(saved==record or saved=={'format':CAPABILITY,'operationId':record['operationId'],'state':'prepared','request':record['request']},'Closed ledger file receipt changed')
|
|
81
|
+
require(target(record['request'])==record['targetBindingDigest'] and schema(db,True)==record['schemaDigest'],'Closed ledger target or schema changed')
|
|
82
|
+
state=snapshot(db);disjoint(state,active)
|
|
83
|
+
require(digest(state)==record['sourceDigest']==digest(record['snapshot']),'Closed ledger source checkpoint changed')
|
|
84
|
+
finally:db.close()
|
|
85
|
+
require(identity(root)==ident,'Closed ledger file identity changed during observation')
|
|
86
|
+
return record
|
|
87
|
+
|
|
88
|
+
def run(root,request,observe=None):
|
|
89
|
+
root=pathlib.Path(root).resolve();validate_request(root,request);ident=identity(root);binding=target(request)
|
|
90
|
+
journal=root/'work/cutover.json'
|
|
91
|
+
if journal.exists():
|
|
92
|
+
record=verify(root,observe)
|
|
93
|
+
require(record['request']==request,'Existing ledger fence request differs; no replay')
|
|
94
|
+
return record
|
|
95
|
+
native,active=owner(root,observe)
|
|
96
|
+
require(native['pid']==request['proxyPid'] and native['runtime']==request['runtime'],'Reviewed native owner differs')
|
|
97
|
+
prepared={'format':CAPABILITY,'operationId':request['operationId'],'state':'prepared','request':request}
|
|
98
|
+
fd=os.open(journal,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
|
|
99
|
+
with os.fdopen(fd,'wb') as file:file.write(canonical(prepared));file.flush();os.fsync(file.fileno())
|
|
100
|
+
sync_directory(journal.parent)
|
|
101
|
+
db=sqlite3.connect(root/'work/tasks.sqlite',timeout=10)
|
|
102
|
+
try:
|
|
103
|
+
db.execute('BEGIN IMMEDIATE');require(identity(root)==ident and read_json(journal)==prepared,'Source identity changed before fence')
|
|
104
|
+
current,active=owner(root,observe);require(current==native,'Native owner changed while awaiting ledger admission')
|
|
105
|
+
schema(db,False);state=snapshot(db);disjoint(state,active)
|
|
106
|
+
require(digest(state)==request['sourceDigest'],'Reviewed closed ledger snapshot changed')
|
|
107
|
+
db.execute(META_SQL)
|
|
108
|
+
for sql in triggers().values():db.execute(sql)
|
|
109
|
+
record={'format':CAPABILITY,'operationId':request['operationId'],'state':'ledger-fenced','root':str(root),'request':request,'databaseIdentity':ident,'nativeOwner':native,'codeDigest':code_digest(),'targetBindingDigest':binding,'schemaDigest':schema(db,True),'sourceDigest':digest(state),'snapshot':state,'unrelatedNativeTurnsPreserved':True}
|
|
110
|
+
require(len(canonical(record))<=900000,'Closed ledger receipt exceeds bound')
|
|
111
|
+
db.execute('INSERT INTO '+META+' VALUES (?,?)',(request['operationId'],canonical(record).decode()));db.commit()
|
|
112
|
+
except BaseException:db.rollback();raise
|
|
113
|
+
finally:db.close()
|
|
114
|
+
# A lost file acknowledgement is recovered from the immutable native DB row;
|
|
115
|
+
# another call can only verify that row, never execute the fence again.
|
|
116
|
+
actual=verify(root,observe);require(actual==record,'Committed ledger fence differs')
|
|
117
|
+
atomic_json(journal,record)
|
|
118
|
+
return record
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Attach a clean installation to existing cloud authority; never dispatch work."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import types
|
|
6
|
+
|
|
7
|
+
from . import permissions, topolo
|
|
8
|
+
from .cloud_work import CloudStore
|
|
9
|
+
from .role_catalog import COORDINATORS, digest
|
|
10
|
+
from .storage import atomic_json, canonical, read_json, sync_directory
|
|
11
|
+
from .work import require
|
|
12
|
+
|
|
13
|
+
FIELDS = {'pack', 'workspaceId', 'documentId', 'datasetId', 'installationId',
|
|
14
|
+
'expectedCurrentVersionId', 'expectedCatalogDigest'}
|
|
15
|
+
METADATA = {'config.json', 'role-catalog.json', 'role-bindings.json', 'context-authority.json',
|
|
16
|
+
'cloud-attach.json', 'cloud.json'}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def owned(path, directory=False):
|
|
20
|
+
stat = path.lstat()
|
|
21
|
+
require(not path.is_symlink() and stat.st_uid == os.getuid() and not stat.st_mode & 0o077
|
|
22
|
+
and (path.is_dir() if directory else path.is_file()), 'Private owned installation metadata required')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def attach(lib, request, client=None):
|
|
26
|
+
require(isinstance(request, dict) and set(request) == FIELDS
|
|
27
|
+
and all(isinstance(v, str) and 1 <= len(v) <= 240 for v in request.values())
|
|
28
|
+
and re.fullmatch(r'[a-z0-9][a-z0-9-]{0,119}', request['installationId']),
|
|
29
|
+
'Exact cloud attachment selectors required')
|
|
30
|
+
with lib.lock():
|
|
31
|
+
return attach_locked(lib, request, client)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def clean(lib, root):
|
|
35
|
+
owned(root, True)
|
|
36
|
+
require(not lib.active().get('components') and not lib.pending(), 'Clean stopped installation required')
|
|
37
|
+
require({p.name for p in root.iterdir()} <= METADATA, 'Local tasks, pending writes or unknown metadata prohibit attachment')
|
|
38
|
+
for path in root.iterdir():
|
|
39
|
+
owned(path)
|
|
40
|
+
for path in [lib.root / 'live-bridge.json', root / 'claude-frontdoors.json']:
|
|
41
|
+
require(not path.exists(), 'Existing native owner metadata prohibits attachment')
|
|
42
|
+
for name in ['startup-operation.json', 'native-operation.json', 'active.json', 'state.json']:
|
|
43
|
+
require(not (lib.root / 'claude' / name).exists(), 'Existing Claude preparation or startup prohibits fresh attachment')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def attach_locked(lib, request, client):
|
|
47
|
+
root = lib.root / 'work'
|
|
48
|
+
clean(lib, root)
|
|
49
|
+
saved = read_json(root / 'config.json')
|
|
50
|
+
catalog = read_json(root / 'role-catalog.json')
|
|
51
|
+
registry = read_json(root / 'role-bindings.json')
|
|
52
|
+
manifest, revision = lib.load(request['pack'])
|
|
53
|
+
agents = manifest['agents']
|
|
54
|
+
require(set(saved) == {'format', 'pack', 'catalogDigest', 'maxWorkers', 'frontdoorDispatch', 'requireCloudAuthority'}
|
|
55
|
+
and saved['format'] == 'topolo-task-dispatch-v2' and saved['pack'] == request['pack']
|
|
56
|
+
and saved['frontdoorDispatch'] is True and saved['requireCloudAuthority'] is True
|
|
57
|
+
and type(saved['maxWorkers']) is int and 1 <= saved['maxWorkers'] <= 8,
|
|
58
|
+
'Qualified three-coordinator cloud configuration required')
|
|
59
|
+
require(len(agents) == 3 and {(a['set'], a['id']) for a in agents} == {('intakes', v) for v in COORDINATORS.values()}
|
|
60
|
+
and catalog['pack'] == request['pack'] and catalog['nativePackRevision'] == revision
|
|
61
|
+
and catalog['format'] == 'topolo-task-role-catalog-v1'
|
|
62
|
+
and saved['catalogDigest'] == request['expectedCatalogDigest'] == digest(catalog)
|
|
63
|
+
and registry.get('organizationId') == 'org_topolo_platform' and registry.get('roles') == catalog['bindings']
|
|
64
|
+
and set(catalog['channels']) == set(COORDINATORS), 'Reviewed role catalog differs')
|
|
65
|
+
owned(lib.root / 'permissions' / (request['pack'] + '.json'))
|
|
66
|
+
consent = permissions.load(lib, request['pack'], agents)
|
|
67
|
+
require(consent and consent.get('sourceRevision') == revision and consent.get('environment') == 'production'
|
|
68
|
+
and consent['contextKey'] == 'org:org_topolo_platform'
|
|
69
|
+
and consent['intakeRoutes'] == {request['pack'] + '/intakes/' + a['id']: request['pack'] + '/intakes/' + a['id'] for a in agents}
|
|
70
|
+
and all(ref.get('workspaceId') == consent.get('agentWorkspaceId') for ref in registry['roles'].values()),
|
|
71
|
+
'Current production installation consent required')
|
|
72
|
+
if client is None:
|
|
73
|
+
client = topolo.Client('production', 'org:org_topolo_platform')
|
|
74
|
+
require(client.environment == 'production' and client.context == consent['contextKey'], 'Production cloud reader context differs')
|
|
75
|
+
actor = client.run(['whoami'])
|
|
76
|
+
require(actor.get('organization', {}).get('id') == 'org_topolo_platform'
|
|
77
|
+
and actor.get('user', {}).get('id') == consent['principalId'], 'Current production identity differs')
|
|
78
|
+
_, policy = topolo.organization_execution_policy(client, consent, agents, consent.get('agentWorkspaceId'))
|
|
79
|
+
permissions.authorize_native_scope(policy)
|
|
80
|
+
binding = {'format': 'topolo-cloud-work-binding-v1', 'organizationId': 'org_topolo_platform',
|
|
81
|
+
**{k: request[k] for k in ['workspaceId', 'documentId', 'datasetId', 'installationId']}}
|
|
82
|
+
# Reuse the exact current-only authenticated reader before publishing a binding.
|
|
83
|
+
version, state = CloudStore.load(types.SimpleNamespace(binding=binding, client=client))
|
|
84
|
+
journal_path, binding_path = root / 'cloud-attach.json', root / 'cloud.json'
|
|
85
|
+
journal = read_json(journal_path, {})
|
|
86
|
+
pins = {'catalog': digest(catalog), 'registry': digest(registry), 'config': digest(saved), 'consent': digest(consent), 'revision': revision}
|
|
87
|
+
if journal:
|
|
88
|
+
require(set(journal) == {'format', 'state', 'request', 'binding', 'pins', 'cloudVersion', 'cloudDigest', 'verifiedCloudVersion', 'verifiedCloudDigest'}
|
|
89
|
+
and journal['format'] == 'topolo-cloud-attach-v1' and journal['state'] in {'prepared', 'verified'}
|
|
90
|
+
and journal['request'] == request and journal['binding'] == binding and journal['pins'] == pins,
|
|
91
|
+
'Existing cloud attachment requires its original exact selectors and metadata')
|
|
92
|
+
require(binding_path.exists() and read_json(binding_path) == binding, 'Attachment write is uncertain; no automatic replay')
|
|
93
|
+
else:
|
|
94
|
+
require(not binding_path.exists(), 'Existing unverified cloud binding cannot be replaced')
|
|
95
|
+
verified = journal.get('state') == 'verified'
|
|
96
|
+
require(journal or version == request['expectedCurrentVersionId'], 'Expected current cloud version changed')
|
|
97
|
+
if not verified:
|
|
98
|
+
for table in ['tasks', 'workers']:
|
|
99
|
+
for row in state['tables'][table]:
|
|
100
|
+
body = json.loads(row[2])
|
|
101
|
+
require(request['installationId'] not in [body.get('ownerInstallationId'), body.get('installationId'),
|
|
102
|
+
body.get('nativePlan', {}).get('installationId')], 'Installation identity already owns recorded work')
|
|
103
|
+
# Awaited identity/policy/Docs reads must not race installation metadata changes.
|
|
104
|
+
require(read_json(root / 'config.json') == saved and read_json(root / 'role-catalog.json') == catalog
|
|
105
|
+
and read_json(root / 'role-bindings.json') == registry
|
|
106
|
+
and permissions.load(lib, request['pack'], agents) == consent
|
|
107
|
+
and lib.load(request['pack'])[1] == revision and not lib.active().get('components') and not lib.pending(),
|
|
108
|
+
'Installation metadata changed during attachment')
|
|
109
|
+
clean(lib, root)
|
|
110
|
+
if verified:
|
|
111
|
+
return {'state': 'cloud-authority-attached', 'alreadyApplied': True, 'binding': binding, 'tasksCreated': 0, 'workersAdopted': 0}
|
|
112
|
+
resumed = bool(journal)
|
|
113
|
+
if not journal:
|
|
114
|
+
journal = {'format': 'topolo-cloud-attach-v1', 'state': 'prepared', 'request': request, 'binding': binding,
|
|
115
|
+
'pins': pins, 'cloudVersion': version, 'cloudDigest': digest(state),
|
|
116
|
+
'verifiedCloudVersion': None, 'verifiedCloudDigest': None}
|
|
117
|
+
# The library lock serializes the exclusive journal reservation.
|
|
118
|
+
with journal_path.open('x', encoding='utf8') as stream:
|
|
119
|
+
os.chmod(journal_path, 0o600)
|
|
120
|
+
stream.write(canonical(journal).decode()); stream.flush(); os.fsync(stream.fileno())
|
|
121
|
+
sync_directory(root)
|
|
122
|
+
atomic_json(binding_path, binding)
|
|
123
|
+
require(read_json(binding_path) == binding, 'Cloud binding readback differs; reconcile without replay')
|
|
124
|
+
verified_record = {**journal, 'state': 'verified', 'verifiedCloudVersion': version, 'verifiedCloudDigest': digest(state)}
|
|
125
|
+
atomic_json(journal_path, verified_record)
|
|
126
|
+
require(read_json(journal_path) == verified_record, 'Cloud attachment acknowledgement is uncertain')
|
|
127
|
+
return {'state': 'cloud-authority-attached', 'alreadyApplied': False, 'binding': binding,
|
|
128
|
+
'tasksCreated': 0, 'workersAdopted': 0, 'cloudTaskStateChanged': False, 'reconciled': resumed}
|