@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.
Files changed (148) hide show
  1. package/README.md +18 -4
  2. package/agent-packs/README.md +469 -1
  3. package/agent-packs/coverage_changed.py +1 -1
  4. package/agent-packs/docs/selected-artifacts.md +47 -0
  5. package/agent-packs/documentation/cloud-task-state.md +56 -0
  6. package/agent-packs/domain-contexts/README.md +25 -0
  7. package/agent-packs/domain-contexts/cicd.json +8 -0
  8. package/agent-packs/gui/feed-adapter.js +4 -0
  9. package/agent-packs/gui/index.html +9 -0
  10. package/agent-packs/gui/studio.css +14 -0
  11. package/agent-packs/gui/studio.js +121 -0
  12. package/agent-packs/gui/studio_server.py +355 -0
  13. package/agent-packs/roles/cicd/CLAUDE.md +24 -0
  14. package/agent-packs/specializations/cicd--cicd.json +5 -0
  15. package/agent-packs/specializations/cicd--pipeline-engineering.json +5 -0
  16. package/agent-packs/specializations/cicd--promotion-deployment.json +5 -0
  17. package/agent-packs/specializations/cicd--queue-recovery.json +5 -0
  18. package/agent-packs/specializations/cicd--release-verification.json +5 -0
  19. package/agent-packs/specializations/cicd--runner-capacity.json +5 -0
  20. package/agent-packs/specializations/intakes--engineering-02.json +5 -0
  21. package/agent-packs/specializations/intakes--engineering-03.json +5 -0
  22. package/agent-packs/specializations/intakes--engineering.json +5 -0
  23. package/agent-packs/specializations/intakes--operator-01.json +5 -0
  24. package/agent-packs/specializations/intakes--operator-02.json +5 -0
  25. package/agent-packs/specializations/operator-skills--audio-and-narration-specialist.json +5 -0
  26. package/agent-packs/specializations/operator-skills--brand-and-copywriting-specialist.json +5 -0
  27. package/agent-packs/specializations/operator-skills--capability-research-specialist.json +5 -0
  28. package/agent-packs/specializations/operator-skills--data-analysis-and-visualization.json +5 -0
  29. package/agent-packs/specializations/operator-skills--financial-evidence-specialist.json +5 -0
  30. package/agent-packs/specializations/operator-skills--operating-template-specialist.json +5 -0
  31. package/agent-packs/specializations/operator-skills--operator-skill-library.json +5 -0
  32. package/agent-packs/specializations/operator-skills--operator-skills.json +5 -0
  33. package/agent-packs/specializations/operator-skills--operator-workflow-evaluation.json +5 -0
  34. package/agent-packs/specializations/operator-skills--privacy-and-agreement-review.json +5 -0
  35. package/agent-packs/specializations/operator-skills--storytelling-specialist.json +5 -0
  36. package/agent-packs/specializations/operator-skills--ux-and-accessibility-specialist.json +5 -0
  37. package/agent-packs/specializations/operator-skills--video-production-specialist.json +5 -0
  38. package/agent-packs/specializations/operator-skills--visual-design-specialist.json +5 -0
  39. package/agent-packs/specializations/operator-skills--workflow-integration-specialist.json +5 -0
  40. package/agent-packs/specializations/operators--access-and-privacy.json +5 -0
  41. package/agent-packs/specializations/operators--accounting-evidence-review.json +5 -0
  42. package/agent-packs/specializations/operators--agreement-operations.json +5 -0
  43. package/agent-packs/specializations/operators--books-operator.json +5 -0
  44. package/agent-packs/specializations/operators--brand-operator.json +5 -0
  45. package/agent-packs/specializations/operators--commerce-operations.json +5 -0
  46. package/agent-packs/specializations/operators--communications-operator.json +5 -0
  47. package/agent-packs/specializations/operators--content-and-assets-operator.json +5 -0
  48. package/agent-packs/specializations/operators--customer-operations.json +5 -0
  49. package/agent-packs/specializations/operators--design-operator.json +5 -0
  50. package/agent-packs/specializations/operators--design-production-02.json +5 -0
  51. package/agent-packs/specializations/operators--design-production-03.json +5 -0
  52. package/agent-packs/specializations/operators--design-production-04.json +5 -0
  53. package/agent-packs/specializations/operators--device-and-feed-operator.json +5 -0
  54. package/agent-packs/specializations/operators--director-operator.json +5 -0
  55. package/agent-packs/specializations/operators--experience-review.json +5 -0
  56. package/agent-packs/specializations/operators--growth-operations.json +5 -0
  57. package/agent-packs/specializations/operators--learn-operator.json +5 -0
  58. package/agent-packs/specializations/operators--localization-operator.json +5 -0
  59. package/agent-packs/specializations/operators--operators.json +5 -0
  60. package/agent-packs/specializations/operators--pay-operator.json +5 -0
  61. package/agent-packs/specializations/operators--research-and-intelligence.json +5 -0
  62. package/agent-packs/specializations/operators--roadmap-operator.json +5 -0
  63. package/agent-packs/specializations/operators--social-content-operator.json +5 -0
  64. package/agent-packs/specializations/operators--talent-operations.json +5 -0
  65. package/agent-packs/specializations/operators--web-operator.json +5 -0
  66. package/agent-packs/specializations/operators--workflow-operator.json +5 -0
  67. package/agent-packs/specializations/specialists--adversarial-qa.json +5 -0
  68. package/agent-packs/specializations/specialists--backlog-dispatch.json +5 -0
  69. package/agent-packs/specializations/specialists--build-01.json +5 -0
  70. package/agent-packs/specializations/specialists--build-02.json +5 -0
  71. package/agent-packs/specializations/specialists--build-03.json +5 -0
  72. package/agent-packs/specializations/specialists--developer-environment-steward.json +5 -0
  73. package/agent-packs/specializations/specialists--documentation-knowledge-steward.json +5 -0
  74. package/agent-packs/specializations/specialists--execution-readiness.json +5 -0
  75. package/agent-packs/specializations/specialists--incident-triage.json +5 -0
  76. package/agent-packs/specializations/specialists--platform-package-steward.json +5 -0
  77. package/agent-packs/specializations/specialists--product-direction-01.json +5 -0
  78. package/agent-packs/specializations/specialists--product-direction-02.json +5 -0
  79. package/agent-packs/specializations/specialists--release-recovery.json +5 -0
  80. package/agent-packs/specializations/specialists--release-watch.json +5 -0
  81. package/agent-packs/specializations/specialists--roadmap-planning-01.json +5 -0
  82. package/agent-packs/specializations/specialists--roadmap-planning-02.json +5 -0
  83. package/agent-packs/specializations/specialists--security-permissions-review.json +5 -0
  84. package/agent-packs/specializations/specialists--system-architecture-01.json +5 -0
  85. package/agent-packs/specializations/specialists--system-architecture-02.json +5 -0
  86. package/agent-packs/specializations/specialists--translation.json +5 -0
  87. package/agent-packs/specializations/specialists--ux-consistency-review.json +5 -0
  88. package/agent-packs/specializations/specialists--workflow-improvement.json +5 -0
  89. package/agent-packs/specializations/specialists--worktree-retirement.json +5 -0
  90. package/agent-packs/src/topolo_agent_pack/cicd.py +101 -0
  91. package/agent-packs/src/topolo_agent_pack/claude.py +22 -7
  92. package/agent-packs/src/topolo_agent_pack/claude_activation.py +157 -0
  93. package/agent-packs/src/topolo_agent_pack/claude_completion_recovery.py +216 -0
  94. package/agent-packs/src/topolo_agent_pack/claude_desktop.py +181 -0
  95. package/agent-packs/src/topolo_agent_pack/claude_dispatch.py +240 -0
  96. package/agent-packs/src/topolo_agent_pack/claude_prompt.py +74 -0
  97. package/agent-packs/src/topolo_agent_pack/claude_runner.py +207 -0
  98. package/agent-packs/src/topolo_agent_pack/claude_service.py +397 -0
  99. package/agent-packs/src/topolo_agent_pack/cli.py +188 -18
  100. package/agent-packs/src/topolo_agent_pack/closed_ledger_fence.py +118 -0
  101. package/agent-packs/src/topolo_agent_pack/cloud_attach.py +128 -0
  102. package/agent-packs/src/topolo_agent_pack/cloud_cutover.py +134 -0
  103. package/agent-packs/src/topolo_agent_pack/cloud_delta.py +162 -0
  104. package/agent-packs/src/topolo_agent_pack/cloud_migrate.py +142 -0
  105. package/agent-packs/src/topolo_agent_pack/cloud_recovery.py +42 -0
  106. package/agent-packs/src/topolo_agent_pack/cloud_work.py +126 -0
  107. package/agent-packs/src/topolo_agent_pack/coordinator_activity.py +27 -0
  108. package/agent-packs/src/topolo_agent_pack/coordinator_context.py +91 -0
  109. package/agent-packs/src/topolo_agent_pack/coordinator_refresh.py +116 -0
  110. package/agent-packs/src/topolo_agent_pack/desktop_acceptance.py +45 -0
  111. package/agent-packs/src/topolo_agent_pack/desktop_launcher.py +114 -0
  112. package/agent-packs/src/topolo_agent_pack/domain_context.py +38 -0
  113. package/agent-packs/src/topolo_agent_pack/execution.py +104 -7
  114. package/agent-packs/src/topolo_agent_pack/fresh.py +1 -1
  115. package/agent-packs/src/topolo_agent_pack/frontdoor.py +23 -0
  116. package/agent-packs/src/topolo_agent_pack/linked_task_context.py +80 -0
  117. package/agent-packs/src/topolo_agent_pack/live.py +285 -18
  118. package/agent-packs/src/topolo_agent_pack/native.py +160 -4
  119. package/agent-packs/src/topolo_agent_pack/policy.py +8 -0
  120. package/agent-packs/src/topolo_agent_pack/review_context.py +33 -0
  121. package/agent-packs/src/topolo_agent_pack/role_catalog.py +137 -0
  122. package/agent-packs/src/topolo_agent_pack/role_context.py +63 -0
  123. package/agent-packs/src/topolo_agent_pack/role_refresh.py +50 -0
  124. package/agent-packs/src/topolo_agent_pack/task_models.py +11 -7
  125. package/agent-packs/src/topolo_agent_pack/throughput.py +149 -0
  126. package/agent-packs/src/topolo_agent_pack/topolo.py +7 -1
  127. package/agent-packs/src/topolo_agent_pack/work.py +322 -0
  128. package/agent-packs/src/topolo_agent_pack/work_adoption.py +122 -0
  129. package/agent-packs/src/topolo_agent_pack/work_artifacts.py +84 -0
  130. package/agent-packs/src/topolo_agent_pack/work_context.py +77 -0
  131. package/agent-packs/src/topolo_agent_pack/work_gui.py +51 -0
  132. package/agent-packs/src/topolo_agent_pack/work_native.py +414 -0
  133. package/agent-packs/src/topolo_agent_pack/work_wait.py +151 -0
  134. package/agent-packs/templates/topolo-coordinators/AGENTS.md +7 -0
  135. package/agent-packs/templates/topolo-coordinators/agents/intakes/cicd/AGENTS.md +6 -0
  136. package/agent-packs/templates/topolo-coordinators/agents/intakes/cicd/config.json +4 -0
  137. package/agent-packs/templates/topolo-coordinators/agents/intakes/engineering/AGENTS.md +3 -0
  138. package/agent-packs/templates/topolo-coordinators/agents/intakes/engineering/config.json +4 -0
  139. package/agent-packs/templates/topolo-coordinators/agents/intakes/operator-01/AGENTS.md +3 -0
  140. package/agent-packs/templates/topolo-coordinators/agents/intakes/operator-01/config.json +4 -0
  141. package/agent-packs/templates/topolo-coordinators/pack.json +56 -0
  142. package/dist/chunk-NYDIAYVY.js +144 -0
  143. package/dist/{claude-pack-backend-YEVPAOID.js → chunk-UY5LEU5C.js} +1659 -21
  144. package/dist/claude-desktop-profile-JCQKSY3K.js +13 -0
  145. package/dist/claude-pack-activation-Q25MCEPU.js +506 -0
  146. package/dist/claude-pack-backend-HUTKGIDS.js +90 -0
  147. package/dist/index.js +408 -164
  148. package/package.json +8 -6
@@ -275,7 +275,7 @@ def request(lib,operation,params=None):
275
275
  record=read_json(lib.root/'live-bridge.json',{})
276
276
  if not record: raise PackError('This desktop profile has no live bridge. Open it through topolo-agent-pack first.')
277
277
  with socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) as connection:
278
- connection.settimeout(3 if operation=='status' else 240 if operation.startswith('lane-') else 60 if operation in {'permissions-configure','owned-roster-read','smoke-start'} else 20)
278
+ connection.settimeout(3 if operation=='status' else 240 if operation.startswith('lane-') or operation in {'work-submit','work-respond','work-re-review'} else 60 if operation in {'permissions-configure','owned-roster-read','smoke-start'} else 20)
279
279
  try: connection.connect(record['socket'])
280
280
  except OSError as exc: raise PackError('Managed live bridge is unavailable: '+str(exc)) from exc
281
281
  connection.sendall(canonical({'token':record['token'],'operation':operation,**({'params':params} if params is not None else {})})+b'\n')
@@ -310,6 +310,7 @@ class BackendProxy:
310
310
  self.token=secrets.token_hex(32);self.path=socket_path(lib)
311
311
  self.execution=None;self.catalog_ready=asyncio.Event();self.catalog_id='topolo-catalog-'+uuid.uuid4().hex
312
312
  self.catalog_rows=[];self.catalog_cursors=set()
313
+ self.work=None;self.work_startup_error=None;self.work_initialization=None
313
314
  self.approvals=None;self.native_calls={};self.notifications=set()
314
315
  self.permission_configuration=False
315
316
  self.permission_batch=None
@@ -328,6 +329,15 @@ class BackendProxy:
328
329
  if not active.get('runtime'): raise PackError('No selected managed runtime')
329
330
  self.runtime=active['runtime'];env=os.environ.copy()
330
331
  self.execution=load_gate(self.runtime);self.catalog_ready.clear()
332
+ self.work=None;self.work_startup_error=None;initialize_work=False
333
+ if (self.lib.root/'work/config.json').is_file():
334
+ if (self.lib.root/'work/cutover.json').exists() and not (self.lib.root/'work/cloud.json').exists():
335
+ # A closed legacy ledger cannot initialize a broker. Native policy
336
+ # and history reads remain available; model work stays fenced.
337
+ self.work_startup_error='Legacy task authority is fenced; attach this installation to the existing cloud authority before dispatch'
338
+ else:
339
+ initialize_work=True
340
+ self.work_startup_error='Task dispatcher initialization is pending; native task execution is unavailable'
331
341
  self.approvals=ApprovalRouter(pathlib.Path(self.runtime)/'approval-inbox',
332
342
  list(self.execution.roles.values()),uuid.uuid4().hex) if self.execution else None
333
343
  self.catalog_rows=[];self.catalog_cursors=set()
@@ -335,6 +345,18 @@ class BackendProxy:
335
345
  env['CODEX_HOME']=str(pathlib.Path(self.runtime)/'home');env['CODEX_CLI_PATH']=self.real
336
346
  self.child=await asyncio.create_subprocess_exec(self.real,*self.args,env=env,stdin=asyncio.subprocess.PIPE,stdout=asyncio.subprocess.PIPE,limit=64*1024*1024)
337
347
  self.output_task=asyncio.create_task(self.output())
348
+ if initialize_work:self.work_initialization=asyncio.create_task(self.initialize_work())
349
+
350
+ async def initialize_work(self):
351
+ # Native organization-policy bootstrap must not depend on cloud latency.
352
+ # The explicit admission error remains until every broker check passes.
353
+ try:
354
+ from .work_native import Broker
355
+ broker=Broker(self);await broker.initialize()
356
+ self.work=broker;self.work_startup_error=None
357
+ if self.initialized:self.work.launch(self.work.resume_ready())
358
+ except Exception as exc:
359
+ self.work=None;self.work_startup_error=str(exc) or 'Task dispatcher initialization failed'
338
360
 
339
361
  async def output(self):
340
362
  while line:=await self.child.stdout.readline():
@@ -362,15 +384,24 @@ class BackendProxy:
362
384
  {**self.execution.status(),'protocol':self.protocol})
363
385
  if 'id' in value and ('result' in value or 'error' in value):
364
386
  self.pending.discard(str(value['id']))
365
- if 'result' in value and isinstance(value['result'],dict) and 'userAgent' in value['result']: self.initialized=True
387
+ if 'result' in value and isinstance(value['result'],dict) and 'userAgent' in value['result']:
388
+ self.initialized=True
389
+ if self.work:self.work.launch(self.work.resume_ready())
366
390
  method=value.get('method');params=value.get('params') or {}
391
+ if method=='account/updated' and self.execution:
392
+ self.execution.catalog={};self.catalog_ready.clear()
367
393
  if self.approvals:
368
394
  record=self.approvals.capture(value)
369
395
  if record:self.dynamic_permission_event(record)
370
396
  if record and record['sourceThreadId']!=record['intakeThreadId'] and record['notificationState']!='delivered':
371
397
  self.schedule_notification(record)
398
+ if method in {'turn/started','turn/completed','thread/status/changed'} and getattr(self,'work',None):
399
+ self.work.launch(self.work.notify())
400
+ if method=='item/completed' and getattr(self,'work',None):self.work.launch(self.work.progress(params))
372
401
  if method=='turn/started': self.active_turns.add(params['threadId'])
373
402
  if method=='turn/completed': self.active_turns.discard(params['threadId'])
403
+ if method=='turn/completed' and getattr(self,'work',None):
404
+ self.work.completed(params['threadId'],params['turn'])
374
405
  if method=='thread/status/changed':
375
406
  if params.get('status',{}).get('type')=='active':self.active_turns.add(params['threadId'])
376
407
  elif params.get('status',{}).get('type') in {'idle','notLoaded','systemError'}:self.active_turns.discard(params['threadId'])
@@ -378,9 +409,20 @@ class BackendProxy:
378
409
  for row in self.approvals.records():
379
410
  if row['nativeConnectionCurrent'] and row['intakeThreadId']==params['threadId'] and row['notificationState']!='delivered':
380
411
  self.schedule_notification(row)
412
+ if method in {'turn/started','turn/completed','thread/status/changed'} and getattr(self,'work',None):
413
+ from .coordinator_activity import record as record_activity
414
+ try:
415
+ record_activity(self.lib.root,{'runtime':self.runtime,'activeTurns':sorted(self.active_turns)})
416
+ self.activity_error=None
417
+ except Exception as exc:self.activity_error=str(exc)
381
418
  if self.emit_native:sys.stdout.buffer.write(line);sys.stdout.buffer.flush()
382
419
 
383
420
  async def stop_child(self):
421
+ if self.work_initialization:
422
+ self.work_initialization.cancel()
423
+ try:await self.work_initialization
424
+ except asyncio.CancelledError:pass
425
+ self.work_initialization=None
384
426
  if self.child and self.child.returncode is None:
385
427
  self.child.terminate()
386
428
  try: await asyncio.wait_for(self.child.wait(),5)
@@ -390,13 +432,13 @@ class BackendProxy:
390
432
  if self.output_task: await self.output_task
391
433
 
392
434
  async def forward(self,value):
393
- if self.smoke_busy and value.get('method') in {'thread/resume','turn/start'}:
435
+ if self.smoke_busy and value.get('method') in {'thread/resume','turn/start','turn/steer'}:
394
436
  sys.stdout.buffer.write(canonical({'id':value.get('id'),'error':{'code':-32000,
395
437
  'message':'Owned native smoke dispatch is being qualified'}})+b'\n');sys.stdout.buffer.flush();return
396
- if self.permission_configuration and value.get('method') in {'thread/resume','turn/start'}:
438
+ if self.permission_configuration and value.get('method') in {'thread/resume','turn/start','turn/steer'}:
397
439
  sys.stdout.buffer.write(canonical({'id':value.get('id'),'error':{'code':-32000,'message':'Owned permission configuration in progress'}})+b'\n')
398
440
  sys.stdout.buffer.flush();return
399
- if value.get('method') in {'thread/resume','turn/start'} and (pathlib.Path(self.runtime)/'permissions-operation.json').exists():
441
+ if value.get('method') in {'thread/resume','turn/start','turn/steer'} and (pathlib.Path(self.runtime)/'permissions-operation.json').exists():
400
442
  sys.stdout.buffer.write(canonical({'id':value.get('id'),'error':{'code':-32000,
401
443
  'message':'Unresolved native permission operation requires reconciliation before execution'}})+b'\n')
402
444
  sys.stdout.buffer.flush();return
@@ -404,11 +446,16 @@ class BackendProxy:
404
446
  if 'id' in value:
405
447
  sys.stdout.buffer.write(canonical({'id':value['id'],'error':{'code':-32000,'message':'Pack switch in progress; reconnecting backend'}})+b'\n');sys.stdout.buffer.flush()
406
448
  return
407
- if self.execution and value.get('method') in {'thread/resume','turn/start'} and (value.get('params') or {}).get('threadId') in self.execution.roles:
449
+ if self.execution and value.get('method') in {'thread/resume','turn/start','turn/steer'} and (value.get('params') or {}).get('threadId') in self.execution.roles:
408
450
  try:
451
+ if value.get('method') in {'turn/start','turn/steer'} and self.work_startup_error:
452
+ raise PackError(self.work_startup_error)
409
453
  await self.qualify_organization((value.get('params') or {})['threadId'])
410
- await asyncio.wait_for(self.catalog_ready.wait(),10)
454
+ await self.ensure_catalog()
455
+ value=await self.qualify_resume_path(value)
411
456
  value=self.execution.prepare(value)
457
+ if value.get('method') in {'turn/start','turn/steer'} and getattr(self,'work',None):
458
+ value=await self.work.frontdoor(value)
412
459
  except (PackError,asyncio.TimeoutError) as exc:
413
460
  sys.stdout.buffer.write(canonical({'id':value.get('id'),'error':{'code':-32000,'message':str(exc) or 'Native model catalog unavailable'}})+b'\n');sys.stdout.buffer.flush();return
414
461
  if 'id' in value and 'method' in value:self.pending.add(str(value['id']))
@@ -429,6 +476,32 @@ class BackendProxy:
429
476
  'params':{'includeHidden':True,**({'cursor':cursor} if cursor else {})}})+b'\n')
430
477
  await self.child.stdin.drain()
431
478
 
479
+ async def ensure_catalog(self):
480
+ # Some desktop clients do not send the optional initialized notification
481
+ # that primes our private catalog request. Qualify on the owning native
482
+ # connection before resume instead of waiting for an event never sent.
483
+ if not self.catalog_ready.is_set():
484
+ result=await self.read_catalog()
485
+ self.execution.set_catalog(result)
486
+ self.catalog_ready.set()
487
+ # Preserve the native event-loop readiness fence: queued approvals or
488
+ # work notifications must be observed before a configuration effect.
489
+ await asyncio.wait_for(self.catalog_ready.wait(),10)
490
+
491
+ async def qualify_resume_path(self,value):
492
+ params=value.get('params') or {}
493
+ if value.get('method')!='thread/resume' or params.get('path') is None:return value
494
+ role=self.execution.roles[params['threadId']]
495
+ result=await self.call_native('thread/read',{'threadId':params['threadId'],'includeTurns':False})
496
+ thread=result.get('thread',{})
497
+ if (thread.get('id')!=params['threadId'] or thread.get('cwd')!=role['cwd'] or
498
+ not isinstance(thread.get('path'),str) or params['path']!=thread['path']):
499
+ raise PackError('Frontend resume path differs from the native owned thread')
500
+ # Resume by verified identity. Never allow a caller path to select a
501
+ # different history or change the reviewed execution settings.
502
+ value={**value,'params':{k:v for k,v in params.items() if k!='path'}}
503
+ return value
504
+
432
505
  async def call_native(self,method,params,before_dispatch=None):
433
506
  if self.managed_transport:
434
507
  return await self.call_managed_native(method,params,before_dispatch)
@@ -438,7 +511,7 @@ class BackendProxy:
438
511
  if not self.permission_configuration and (pathlib.Path(self.runtime)/'permissions-operation.json').exists():
439
512
  raise PackError('Unresolved native permission operation requires reconciliation before execution')
440
513
  await self.qualify_organization(params['threadId'])
441
- await asyncio.wait_for(self.catalog_ready.wait(),10)
514
+ await self.ensure_catalog()
442
515
  if self.permission_configuration:
443
516
  if self._permission_configuration_inputs is None:
444
517
  raise PackError('No owned permission configuration snapshot; native settings were not sent')
@@ -621,7 +694,7 @@ class BackendProxy:
621
694
  from . import topolo
622
695
  consent=role['installationConsent']
623
696
  agents=self.lib.load(consent['pack'])[0]['agents']
624
- matches=[a for a in agents if '/'.join([a['pack'],a['set'],a['id']])==role.get('sourceRoleKey',role['key']) and a['threadId']==role.get('sourceRoleNativeId',role['threadId'])]
697
+ matches=[a for a in agents if '/'.join([a['pack'],a['set'],a['id']])==role.get('sourcePermissionRoleKey',role.get('sourceRoleKey',role['key'])) and a['threadId']==role.get('sourceRoleNativeId',role['threadId'])]
625
698
  if len(matches)!=1:
626
699
  raise PackError('Current consent has no unique live owned role')
627
700
  effective_consent=consent
@@ -663,6 +736,9 @@ class BackendProxy:
663
736
  declared={(r['key'],r['threadId']) for r in document['roles']}
664
737
  with closing(sqlite3.connect('file:'+str(pathlib.Path(self.runtime)/'home/state_5.sqlite')+'?mode=ro',uri=True)) as db:
665
738
  native_ids=[r[0] for r in db.execute('SELECT id FROM threads')]
739
+ if getattr(self,'work',None):
740
+ task_ids=self.work.native_ids
741
+ native_ids=[tid for tid in native_ids if tid not in task_ids]
666
742
  from .lanes import Registry
667
743
  dynamic=[r for r in Registry(self.lib.root,'codex').records() if r['state']!='retired']
668
744
  dynamic_ids={r['nativeId'] for r in dynamic}
@@ -1332,7 +1408,11 @@ class BackendProxy:
1332
1408
  'nativeTransport':'existing-managed-window' if self.managed_transport else 'owned-stdio',
1333
1409
  'managedTargetId':self.managed_transport.target_id if self.managed_transport else None,
1334
1410
  'quiesced':self.quiesced,'initialized':self.initialized,
1335
- 'activeTurns':sorted(self.active_turns),'pendingRequests':len(self.pending),
1411
+ 'activeTurns':sorted(self.active_turns),'pendingRequests':len(self.pending),'activityError':getattr(self,'activity_error',None),'workStartupError':getattr(self,'work_startup_error',None),
1412
+ 'backendHealth':{'childReturnCode':getattr(self.child,'returncode',None),
1413
+ 'responseReaderDone':self.output_task.done() if self.output_task else None,
1414
+ 'catalogReady':self.catalog_ready.is_set(),
1415
+ 'catalogModels':len(self.execution.catalog) if self.execution else 0},
1336
1416
  'executionPolicy':self.execution.status() if self.execution else {'state':'not-configured'},
1337
1417
  'executionProtocol':self.protocol if self.execution else None}
1338
1418
 
@@ -1364,8 +1444,10 @@ class BackendProxy:
1364
1444
  raise PackError('Outgoing agents have active turns. Wait for their current turns; Codex can stay open.')
1365
1445
  if self.pending:
1366
1446
  raise PackError('Backend requests are in flight. Retry the switch; Codex can stay open.')
1367
- self.quiesced=True
1368
- await self.stop_child()
1447
+ if getattr(self,'work',None):await self.work.quiesce()
1448
+ else:
1449
+ self.quiesced=True
1450
+ await self.stop_child()
1369
1451
  elif op=='resume':
1370
1452
  if self.quiesced:
1371
1453
  raise PackError('Use the desktop reconnect action to resume a quiesced bridge')
@@ -1386,8 +1468,48 @@ class BackendProxy:
1386
1468
  self.lane_close_event.set();result={'state':'normal-own-connection-close-requested'}
1387
1469
  elif op in {'lane-plan','lane-adopt','lane-configure','lane-read','lane-settle','lane-retire'}:
1388
1470
  result=await self.dynamic_lane(op,message.get('params') or {})
1471
+ elif op=='desktop-acceptance':
1472
+ from .desktop_acceptance import run
1473
+ result=await run(self,message.get('params') or {})
1389
1474
  elif op=='owned-roster-read':
1390
1475
  result=await self.owned_roster_read(message.get('params') or {})
1476
+ elif op.startswith('work-'):
1477
+ if not self.work:
1478
+ raise PackError(self.work_startup_error or 'Task dispatch is not installed on this owning connection')
1479
+ params=message.get('params') or {}
1480
+ if self.quiesced and op not in {'work-list','work-read','work-retained','work-watch','work-cutover-status'}:
1481
+ raise PackError('Native task admission is quiesced for cutover')
1482
+ if op=='work-cutover' and not params:
1483
+ result=await self.work.quiesce(cutover=True)
1484
+ elif op=='work-cutover-status' and not params:
1485
+ if not self.quiesced:raise PackError('Native cutover is not quiesced')
1486
+ from .cloud_cutover import closure
1487
+ if self.work.cutover_closure!=closure():raise PackError('Executing native cutover source changed')
1488
+ if self.work.cutover_record is None or read_json(self.work.store.root/'cutover.json')!=self.work.cutover_record:raise PackError('Native cutover acknowledgement changed')
1489
+ result=self.work.cutover_record
1490
+ elif op=='work-submit':
1491
+ result=await self.work.submit(params)
1492
+ elif op=='work-adoption-plan':
1493
+ result=await self.work.adoption_plan(params)
1494
+ elif op=='work-adopt':
1495
+ result=await self.work.adopt_retained(params)
1496
+ elif op=='work-respond':
1497
+ result=await self.work.respond(params)
1498
+ elif op=='work-re-review':
1499
+ result=await self.work.re_review(params)
1500
+ elif op=='work-list' and not params:
1501
+ result=await self.work.snapshot()
1502
+ elif op=='work-read' and set(params)=={'taskId'}:
1503
+ result=await self.work.read(params['taskId'])
1504
+ elif op=='work-retained':
1505
+ result=await self.work.state_call('retained_page',params)
1506
+ elif op=='work-reconcile-unstarted':
1507
+ result=await self.work.reconcile_unstarted(params)
1508
+ elif op=='work-reconcile-route':
1509
+ result=await self.work.reconcile_route(params)
1510
+ elif op=='work-watch' and not params:
1511
+ await self.work.watch(reader,writer);return
1512
+ else:raise PackError('Unsupported bounded work operation')
1391
1513
  elif op in {'smoke-start','smoke-read'}:
1392
1514
  result=await self.owned_smoke(op,message.get('params') or {})
1393
1515
  elif op=='permissions-configure':
@@ -1419,7 +1541,7 @@ class BackendProxy:
1419
1541
  result={'state':'native-response-forwarded','operationEffectVerified':False}
1420
1542
  elif op!='status':
1421
1543
  raise PackError('Unknown live bridge operation')
1422
- if op not in {'lane-context-start','lane-context-read','lane-fixture-start','lane-fixture-read','lane-create','lane-close','lane-plan','lane-adopt','lane-configure','lane-read','lane-settle','lane-retire','approval-list','approval-details','approval-respond','permissions-configure','owned-roster-read','smoke-start','smoke-read'}:
1544
+ if not op.startswith('work-') and op not in {'lane-context-start','lane-context-read','lane-fixture-start','lane-fixture-read','lane-create','lane-close','lane-plan','lane-adopt','lane-configure','lane-read','lane-settle','lane-retire','approval-list','approval-details','approval-respond','permissions-configure','owned-roster-read','desktop-acceptance','smoke-start','smoke-read'}:
1423
1545
  result=self.status()
1424
1546
  except Exception as exc: result={'error':str(exc)}
1425
1547
  writer.write(canonical(result)+b'\n');await writer.drain();writer.close();await writer.wait_closed()
@@ -1889,6 +2011,93 @@ def managed_lane_frontend(lib,runtime,planned,native_id=None,app='/Applications/
1889
2011
  return {**result,'targetId':target,'selectedRuntime':str(root),'capability':capability,'proxyGeneration':generation}
1890
2012
  finally:ws.close()
1891
2013
 
2014
+ def bind_sidebar(lib,runtime,app='/Applications/ChatGPT.app',presentation_journal=None,reconcile=None,reconnect_atoms=None):
2015
+ """Existing managed manager, genuine account, guarded journaled presentation."""
2016
+ from . import native
2017
+ root=pathlib.Path(runtime);home=root/'home'
2018
+ generation=managed_backend_generation(lib,str(root))
2019
+ homes=[str(home)];layout=lib.root/'desktop-layout.json';stable=lib.root/'desktop-home'
2020
+ layout_hash=None
2021
+ if layout.exists():
2022
+ OwnedSmoke.owned(layout);OwnedSmoke.owned(stable,directory=True)
2023
+ if read_json(layout).get('owner') not in {'topolo-agent-pack','codex-pack'}:
2024
+ raise PackError('Stable sidebar layout owner differs')
2025
+ selected=lib.root/'selected-runtime'
2026
+ if not selected.is_symlink() or selected.resolve()!=root.resolve():
2027
+ raise PackError('Stable sidebar selected runtime differs')
2028
+ for name in ['config.toml','state_5.sqlite','sessions']:
2029
+ target=stable/name
2030
+ if not target.is_symlink() or os.readlink(target)!=str(pathlib.Path('..')/'selected-runtime'/'home'/name):
2031
+ raise PackError('Stable sidebar execution route differs')
2032
+ homes.append(str(stable));layout_hash=hashlib.sha256(layout.read_bytes()).hexdigest()
2033
+ capability=adapter(app);ws,_=renderer(lib)
2034
+ try:
2035
+ params={'adapter':capability,'homes':homes,'configFile':str(home/'config.toml'),'cwd':str(home)}
2036
+ expression="""(async()=>{
2037
+ const p=PARAMS,services=(await import(p.adapter.module))[p.adapter.export];
2038
+ const opened=await services.appServerManagers.open('local');
2039
+ if(opened?.status!=='ready'||!opened.manager)throw new Error('Existing managed backend unavailable');
2040
+ const manager=opened.manager,home=await manager.getCodexHome();
2041
+ if(typeof home!=='string'||!p.homes.includes(home))throw new Error('Selected sidebar home changed');
2042
+ const config=await manager.sendRequest('config/read',{cwd:p.cwd,includeLayers:true});
2043
+ const origins=config?.layers?.filter(layer=>layer.name?.type==='user'&&!layer.disabledReason);
2044
+ if(!Array.isArray(origins)||!origins.length||origins.some(layer=>layer.name.file!==p.configFile||typeof layer.version!=='string'||!layer.version))throw new Error('Selected sidebar config provenance changed');
2045
+ const a=await manager.sendRequest('account/read',{refreshToken:false});
2046
+ if(await manager.getCodexHome()!==home)throw new Error('Selected sidebar home changed');
2047
+ return {requiresOpenaiAuth:a.requiresOpenaiAuth,account:a.account?{type:a.account.type}:null,
2048
+ workspaceRouting:a.workspaceRouting?{chatgptAccountId:a.workspaceRouting.chatgptAccountId}:null,
2049
+ _nativeContext:{home,configOrigins:origins.map(layer=>({name:layer.name,version:layer.version}))}};
2050
+ })()""".replace('PARAMS',json.dumps(params),1)
2051
+ account=ws.evaluate(expression);binding=native.sidebar_binding(root,account)
2052
+ origin=account.get('_nativeContext') if isinstance(account,dict) else None
2053
+ if not isinstance(origin,dict) or origin.get('home') not in homes or not origin.get('configOrigins'):
2054
+ raise PackError('Actual managed sidebar provenance missing')
2055
+ context={'kind':'managed','app':app,'home':origin['home'],'configOrigins':origin['configOrigins'],
2056
+ 'account':{'type':(account.get('account') or {}).get('type'),'accountId':binding['accountId'],
2057
+ 'requiresOpenaiAuth':account['requiresOpenaiAuth']},'generation':generation,'layoutSHA256':layout_hash}
2058
+ def guard():
2059
+ if ws.evaluate(expression)!=account:raise PackError('Selected sidebar account/home/config changed')
2060
+ if managed_backend_generation(lib,str(root))!=generation:raise PackError('Selected sidebar backend changed')
2061
+ if layout_hash is not None:
2062
+ if hashlib.sha256(layout.read_bytes()).hexdigest()!=layout_hash or (lib.root/'selected-runtime').resolve()!=root.resolve():
2063
+ raise PackError('Stable sidebar route/layout changed')
2064
+ for name in ['config.toml','state_5.sqlite','sessions']:
2065
+ target=stable/name
2066
+ if not target.is_symlink() or os.readlink(target)!=str(pathlib.Path('..')/'selected-runtime'/'home'/name):
2067
+ raise PackError('Stable sidebar route changed')
2068
+ return context
2069
+ guard()
2070
+ current=fetch(ws,'get-global-state',{'key':'electron-persisted-atom-state'}).get('value') or {}
2071
+ if reconcile is not None:
2072
+ path,document=reconcile
2073
+ return native.reconcile_sidebar(lib,path,document,guard(),current,guard)
2074
+ sections=dict(current.get('sidebar-custom-sections-v3',{}));sections.update(binding['sections'])
2075
+ # set-global-state replaces the whole value for its key. Reconnect must
2076
+ # not send a filtered container early: that would erase live account
2077
+ # presentation. Merge ordinary runtime atoms only inside this guarded,
2078
+ # journaled write; destination sections/pins come exclusively from the
2079
+ # fresh managed account, never the captured/offline runtime account.
2080
+ ordinary={k:v for k,v in (reconnect_atoms or {}).items()
2081
+ if k not in {'sidebar-custom-sections-v3','unified-sidebar-pinned-order-v1'}}
2082
+ value={**current,**ordinary,'sidebar-custom-sections-v3':sections,'unified-sidebar-pinned-order-v1':binding['pins']}
2083
+ report={k:v for k,v in binding.items() if k not in {'sections','pins'}}
2084
+ # There is no qualified identity-bound/CAS setter here. Journal the
2085
+ # unavoidable mid-dispatch uncertainty rather than claiming atomicity.
2086
+ mutation=native.SidebarJournal(lib,root,context,current,value,report,parent=presentation_journal)
2087
+ try:
2088
+ guard() # AFTER awaited settings read and durable intent, BEFORE setter.
2089
+ mutation.dispatched=True
2090
+ if not fetch(ws,'set-global-state',{'key':'electron-persisted-atom-state','value':value}).get('success'):
2091
+ raise PackError('Desktop refused sidebar binding')
2092
+ actual=fetch(ws,'get-global-state',{'key':'electron-persisted-atom-state'}).get('value')
2093
+ if actual!=value:raise PackError('Desktop sidebar read-back differs')
2094
+ guard();atomic_json(root/'sidebar-binding.json',report)
2095
+ if read_json(root/'sidebar-binding.json')!=report:raise PackError('Sidebar report read-back differs')
2096
+ guard();mutation.complete();return report
2097
+ except BaseException as error:
2098
+ mutation.failed(error);raise
2099
+ finally:ws.close()
2100
+
1892
2101
  def checkpoint_ui(lib,runtime):
1893
2102
  ws,_=renderer(lib)
1894
2103
  try:
@@ -1907,15 +2116,26 @@ def reconcile(ws,capability,incoming,outgoing):
1907
2116
  params=json.dumps({'adapter':capability,'incoming':incoming,'outgoing':outgoing})
1908
2117
  return ws.evaluate('''(async()=>{const p=PARAMS;const c=(await import(p.adapter.module))[p.adapter.export]?.localThreadCatalog;if(!c)throw new Error('Catalog capability unavailable');for(const threadId of p.outgoing)await c.removeMissingEntry({hostId:'local',threadId});for(const threadId of p.incoming)await c.notifyThread({hostId:'local',threadId},'upsert');await c.invalidateSource('local');await c.requestSync(['local'],'immediate');let proof;const deadline=Date.now()+15000;do{const entries=await c.readEntries(p.incoming.map(threadId=>({hostId:'local',threadId})));const missing=await c.readEntries(p.outgoing.map(threadId=>({hostId:'local',threadId})));proof={incoming:entries.map(e=>e.threadId),titles:entries.map(e=>e.displayTitle),outgoing:missing.map(e=>e.threadId),count:await c.readThreadCount('local')};if(proof.incoming.length===p.incoming.length&&!proof.outgoing.length&&proof.count===p.incoming.length)return proof;await new Promise(r=>setTimeout(r,50))}while(Date.now()<deadline);return proof})()'''.replace('PARAMS',params,1))
1909
2118
 
1910
- def reconnect(lib,runtime,previous_runtime=None,app='/Applications/ChatGPT.app'):
2119
+ def reconnect(lib,runtime,previous_runtime=None,app='/Applications/ChatGPT.app',presentation_journal=None):
1911
2120
  ws,target_id=renderer(lib)
1912
2121
  try:
2122
+ if presentation_journal is None:
2123
+ raise PackError('Reconnect presentation requires its original transaction journal')
2124
+ jpath,jdocument=presentation_journal
2125
+ targets={jdocument.get('targetRuntime'),jdocument.get('before',{}).get('runtime'),jdocument.get('after',{}).get('runtime')}
2126
+ if pathlib.Path(jpath).parent!=lib.root/'journals' or read_json(jpath)!=jdocument or str(runtime) not in targets or jdocument.get('phase') in {'complete','rolled-back'}:
2127
+ raise PackError('Reconnect presentation transaction changed')
2128
+ if jdocument.get('sidebar',{}).get('state') in {'UNKNOWN','may-have-effects'}:
2129
+ raise PackError('Pending sidebar effects require original owner reconciliation')
1913
2130
  current_bridge=available(lib)
1914
2131
  if current_bridge and not current_bridge['quiesced']:quiesce(lib)
1915
2132
  capability=adapter(app);bind(lib,runtime)
1916
2133
  gs=read_json(pathlib.Path(runtime)/'home'/'.codex-global-state.json')
1917
- current=fetch(ws,'get-global-state',{'key':'electron-persisted-atom-state'}).get('value') or {}
1918
- updates={**gs,'electron-persisted-atom-state':{**current,**gs.get('electron-persisted-atom-state',{})}}
2134
+ # Defer the entire atom container. The native API replaces a key's
2135
+ # value; removing nested presentation keys here would still erase them.
2136
+ # Its ordinary atoms join the post-restart guarded binder below.
2137
+ runtime_atoms=gs.get('electron-persisted-atom-state',{})
2138
+ updates={key:value for key,value in gs.items() if key!='electron-persisted-atom-state'}
1919
2139
  for key,value in updates.items():
1920
2140
  result=fetch(ws,'set-global-state',{'key':key,'value':value})
1921
2141
  if not result.get('success'): raise PackError('Desktop refused state update: '+key)
@@ -1927,6 +2147,7 @@ def reconnect(lib,runtime,previous_runtime=None,app='/Applications/ChatGPT.app')
1927
2147
  if bridge and not bridge['quiesced'] and bridge['runtime']==str(runtime) and bridge['initialized']:break
1928
2148
  time.sleep(0.05)
1929
2149
  else:raise PackError('Managed backend did not reconnect; run topolo-agent-pack recover')
2150
+ sidebar_binding=bind_sidebar(lib,runtime,app,presentation_journal=presentation_journal,reconnect_atoms=runtime_atoms)
1930
2151
  proof=reconcile(ws,capability,incoming,outgoing)
1931
2152
  if set(proof['incoming'])!=set(incoming) or proof['outgoing'] or proof['count']!=len(incoming):raise PackError('Sidebar catalog did not match selected runtime; run recover')
1932
2153
  # Soft reload clears thread/composer caches while retaining this exact
@@ -1949,9 +2170,42 @@ def reconnect(lib,runtime,previous_runtime=None,app='/Applications/ChatGPT.app')
1949
2170
  ws.close();time.sleep(0.05)
1950
2171
  except (OSError,PackError):time.sleep(0.05)
1951
2172
  else:raise PackError('Desktop did not render after reconnect; run recover')
1952
- return {'state':'same-window-verified','app':str(app),'targetId':target_id,'desktopPid':bridge['desktopPid'],'frontendVerified':True,'windowClosed':False,'catalog':proof,'renderedTitles':sidebar,'renderedThreadIds':sorted(shown),'selectedThreadId':selected}
2173
+ return {'state':'same-window-verified','app':str(app),'targetId':target_id,'desktopPid':bridge['desktopPid'],'frontendVerified':True,'windowClosed':False,'catalog':proof,'renderedTitles':sidebar,'renderedThreadIds':sorted(shown),'selectedThreadId':selected,'sidebarBinding':sidebar_binding,'sectionsRenderedVerified':False}
1953
2174
  finally:ws.close()
1954
2175
 
2176
+ def reconcile_desktop_catalog(lib,home,runtime):
2177
+ """Drop obsolete derived local catalog rows before an offline cold launch.
2178
+
2179
+ Native transcripts and the timeline ledger are never changed. A complete
2180
+ SQLite backup makes cache repair recoverable; cloud/remote hosts stay intact.
2181
+ """
2182
+ from . import native
2183
+ path=home/'sqlite/codex-dev.db'
2184
+ if not path.exists():return
2185
+ if path.is_symlink() or path.stat().st_uid!=os.getuid():
2186
+ raise PackError('Unowned desktop catalog')
2187
+ native.assert_idle(home)
2188
+ state=pathlib.Path(runtime)/'home/state_5.sqlite'
2189
+ with closing(sqlite3.connect('file:'+str(state)+'?mode=ro',uri=True)) as db:
2190
+ active={r[0] for r in db.execute('SELECT id FROM threads WHERE archived=0')}
2191
+ with closing(sqlite3.connect(path)) as db:
2192
+ tables={r[0] for r in db.execute("SELECT name FROM sqlite_master WHERE type='table'")}
2193
+ if 'local_thread_catalog' not in tables:
2194
+ return
2195
+ stale=[r[0] for r in db.execute("SELECT thread_id FROM local_thread_catalog WHERE host_id='local'") if r[0] not in active]
2196
+ if not stale:
2197
+ return
2198
+ folder=lib.root/'catalog-recovery';folder.mkdir(exist_ok=True,mode=0o700)
2199
+ backup=folder/(uuid.uuid4().hex+'.sqlite')
2200
+ with closing(sqlite3.connect(backup)) as saved:db.backup(saved)
2201
+ backup.chmod(0o600)
2202
+ with db:
2203
+ db.executemany("DELETE FROM local_thread_catalog WHERE host_id='local' AND thread_id=?",[(v,) for v in stale])
2204
+ for table in ['local_thread_catalog_sync_state','local_thread_catalog_scan_checkpoints','local_thread_catalog_scan_entries']:
2205
+ if table in tables:db.execute('DELETE FROM '+table+" WHERE host_id='local'")
2206
+ if 'local_thread_catalog_metadata' in tables:db.execute('UPDATE local_thread_catalog_metadata SET catalog_revision=catalog_revision+1 WHERE id=1')
2207
+ atomic_json(backup.with_suffix('.json'),{'boundary':'derived desktop catalog only','runtime':str(runtime),'removedIds':stale,'retainedActiveIds':sorted(active),'backup':str(backup),'nativeHistoryChanged':False})
2208
+
1955
2209
  def prepare(lib,real):
1956
2210
  home=lib.root/'desktop-home'
1957
2211
  if home.exists() and not (lib.root/'desktop-layout.json').exists():raise PackError('Existing desktop-home is unowned; choose a separate pack library')
@@ -1962,7 +2216,20 @@ def prepare(lib,real):
1962
2216
  home.mkdir(exist_ok=True,mode=0o700)
1963
2217
  atomic_json(lib.root/'desktop-layout.json',{'owner':'topolo-agent-pack','schemaVersion':1})
1964
2218
  bind(lib,lib.active()['runtime'])
1965
- if not (home/'.codex-global-state.json').exists():atomic_json(home/'.codex-global-state.json',read_json(pathlib.Path(lib.active()['runtime'])/'home/.codex-global-state.json'))
2219
+ reconcile_desktop_catalog(lib,home,lib.active()['runtime'])
2220
+ # The control home outlives a selected runtime. Its cached sidebar must
2221
+ # follow that runtime on every cold launch, rather than only its first one.
2222
+ state_path=home/'.codex-global-state.json'
2223
+ current=read_json(state_path,{})
2224
+ runtime=str(lib.active()['runtime'])
2225
+ if current.get('topolo-selected-runtime')==runtime:return executable,home
2226
+ selected=read_json(pathlib.Path(lib.active()['runtime'])/'home/.codex-global-state.json')
2227
+ for key in ['pinned-thread-ids','thread-titles','thread-workspace-root-hints']:
2228
+ current[key]=selected.get(key,{} if key!='pinned-thread-ids' else [])
2229
+ current['electron-persisted-atom-state']={**current.get('electron-persisted-atom-state',{}),**selected.get('electron-persisted-atom-state',{})}
2230
+ current.pop('app-server-migrated-pinned-thread-ids-by-host',None)
2231
+ current['topolo-selected-runtime']=runtime
2232
+ atomic_json(state_path,current)
1966
2233
  return executable,home
1967
2234
 
1968
2235
  def bind(lib,runtime):