@topolo/cli 0.13.9 → 0.13.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -366,11 +366,11 @@ transient refresh failures.
366
366
  ## Topolo Agent Packs
367
367
 
368
368
  `topolo agent-packs` is the public entry point for portable business agent packs.
369
- The CLI bundles the Codex adapter; Python 3.11+ and the supported Codex desktop
370
- installation are required for native local switching. No separate pack CLI
371
- installation is needed. Claude and Cursor adapters are planned and currently
372
- refused before any mutation. MCP setup for those apps is separate from pack
373
- switching and does not establish pack adapter support.
369
+ The CLI bundles the Codex adapter and the qualified Claude Desktop Code preview.
370
+ Python 3.11+ and a supported native host are required for local operations. No
371
+ separate pack CLI installation is needed. The adapter README describes each
372
+ host's current qualification and live-operation requirements. Cursor pack
373
+ operations remain unsupported. MCP setup and pack adoption have separate proof.
374
374
 
375
375
  ```sh
376
376
  topolo agent-packs add founder --harness codex
@@ -378,6 +378,7 @@ topolo agent-packs add founder --group specialists
378
378
  topolo agent-packs add founder --agent strategy
379
379
  topolo agent-packs switch founder
380
380
  topolo agent-packs status
381
+ topolo agent-packs open --adapter-help
381
382
  ```
382
383
 
383
384
  A first add builds the bundled template into a fresh installed instance. Later
@@ -385,6 +386,9 @@ adds and switches resume that instance. `--group` selects the template's set;
385
386
  `--agent` selects an unambiguous logical agent. Use `list` to inspect installed
386
387
  names. Pack templates retain operating rules, rather than earlier work history.
387
388
  The underlying `topolo-agent-pack` adapter contains native switching and recovery.
389
+ `topolo agent-packs open` exposes that adapter's existing managed Codex desktop
390
+ route, including `--app` and `--codex-bin`. It uses the library's selected runtime
391
+ and stable desktop profile; it does not select a new native home.
388
392
 
389
393
  Cloud continuity uses explicit environment and authenticated context pins:
390
394
 
@@ -402,8 +406,45 @@ released in the selected environment first. The CLI does not create grants,
402
406
  choose another customer, invoke model work or silently publish agent versions.
403
407
  See the bundled adapter README for memory, artifact and recovery commands.
404
408
 
409
+ Native child helpers require an explicitly selected private profile reference
410
+ on the existing connection. Use `connect --private-profile` with a nonempty
411
+ `TOPOLO_CONFIG_PATH` pointing to the already authenticated protected profile.
412
+ The producer checks file ownership and modes, authenticates the selected
413
+ environment/context, and verifies the connection before recording public
414
+ profile metadata. Helpers build a child-only environment from that binding.
415
+ Credential contents and inherited token overrides are excluded from the pack.
416
+ Profile metadata does not establish native MCP configuration or runtime health.
417
+
418
+ Task-specific lane model intent is available through `lane models-template`
419
+ and `lane models-install`; `lane status` reads durable state. Native settings,
420
+ context, original-intake approval, operation outcome and retirement each require
421
+ their own current acknowledgement. See [the adapter guide](agent-packs/README.md)
422
+ for the closed inputs and lifecycle boundaries.
423
+
405
424
  The source quality gate covers the CLI dispatcher and the Python continuity
406
425
  adapter separately. `pnpm coverage` creates a local test environment under
407
426
  `node_modules/.cache`, installs the pinned test-only coverage tool and requires
408
427
  100% line and branch coverage for the cloud connection/recovery adapter.
409
428
  These tests use scoped fixtures and do not invoke models or mutate cloud data.
429
+
430
+
431
+ Claude dynamic private startup currently refuses with
432
+ `native_private_query_prestart_contract_unqualified`. The installed desktop
433
+ contract has not qualified private query MCP configuration, exclusion of other
434
+ Topolo connections, and the owned shutdown lifecycle. Create, configure,
435
+ context start, fixture start, retirement, and positive permission forwarding
436
+ remain closed before native access. A valid private profile reference does not
437
+ lift this boundary. Source fixtures and descriptor reads do not establish
438
+ private MCP adoption or actual task completion.
439
+
440
+ Dynamic lane plans can explicitly select the `native-cli` continuity transport.
441
+ Its closed `remote-context --continuity-input` operation produces a bounded
442
+ public identity/context receipt with real response request IDs. The native
443
+ thread's own command or Bash result and charter reads must independently match
444
+ that receipt; an external shell result does not establish native context.
445
+
446
+ Current native startup, credential refresh authority and Bash result producers
447
+ remain unqualified. Native CLI effects therefore refuse with
448
+ `native_cli_startup_unqualified` before client/helper/native access. Private MCP
449
+ startup retains its existing refusal. Version 1 plans stay readable and do not
450
+ silently become version 2 CLI plans.
@@ -201,6 +201,18 @@ scrub conversations. Bundles are private files and are not encrypted.
201
201
 
202
202
  ## Desktop moving parts and recovery
203
203
 
204
+ The existing desktop entry is exposed through the public CLI:
205
+
206
+ ```sh
207
+ topolo agent-packs --store /absolute/AgentPacks open --adapter-help
208
+ topolo agent-packs --store /absolute/AgentPacks open \
209
+ --app /Applications/ChatGPT.app --codex-bin /absolute/qualified/codex
210
+ ```
211
+
212
+ The second command performs the normal managed desktop open/reconnect operation.
213
+ It retains the selected runtime and stable desktop profile described below.
214
+ Its usual ownership, idle-work and recovery checks still apply.
215
+
204
216
  ```
205
217
  objects/ immutable content addressed by SHA-256
206
218
  packs/ immutable captures and revision heads
@@ -571,3 +583,119 @@ exact PID/roster identity and current module qualification. No independent
571
583
  daemon, polling loop, arbitrary evaluation endpoint or UI pack fix is used.
572
584
  Audit failures refuse further broker operation. Cached reports and source
573
585
  fixtures are not live native settings or shared Agent/Bytes acceptance proof.
586
+
587
+
588
+ ## Private profiles for native child helpers
589
+
590
+ A native helper uses an explicit private credential profile from the existing
591
+ Topolo connection binding. First authenticate and qualify that profile through
592
+ the supported CLI. Add `--private-profile` to the normal `connect` request and
593
+ select its protected store for that process with `TOPOLO_CONFIG_PATH`:
594
+
595
+ ```sh
596
+ TOPOLO_CONFIG_PATH=/absolute/private-profile/config.json \
597
+ topolo --env production agent-packs connect founder --harness claude \
598
+ --environment production --context-key org:DISCOVERED_ORGANIZATION_ID \
599
+ --agent-workspace DISCOVERED_AGENT_WORKSPACE \
600
+ --bytes-workspace DISCOVERED_BYTES_WORKSPACE \
601
+ --ownership user --bytes-scope private --roles roles.json \
602
+ --private-profile --confirm
603
+ ```
604
+
605
+ The profile must be a regular, same-owner, single-link file with mode `0600` in
606
+ an owned `0700` directory. Credential contents are never copied into a pack or
607
+ hashed for this reference. The producer checks the selected client environment
608
+ and context, obtains the actual principal, and verifies the connection read-back
609
+ before recording `privateCredentialProfile` metadata on the existing binding.
610
+ Requalifying that private connection requires the explicit flag again.
611
+
612
+ Before launching a helper, the native adapter checks the current binding,
613
+ installation consent, source revision, original intake, public role descriptors,
614
+ and Agent/Bytes workspace references. It constructs only the child process's
615
+ OS runtime environment plus the bound `TOPOLO_CONFIG_PATH`, `TOPOLO_ENV` and
616
+ `TOPOLO_CONTEXT`. Host token overrides and unrelated environment variables are
617
+ excluded. Missing or stale private metadata refuses before client startup.
618
+ The native app's environment and the other sessions remain unchanged.
619
+
620
+ This private helper reference and an authenticated CLI are separate from the
621
+ native child's applied MCP configuration. Native creation and activation still
622
+ require the qualified host startup contract, actual same-process MCP status and
623
+ principal proof. A profile path, descriptor hash or helper success supplies none
624
+ of those acknowledgements by itself.
625
+
626
+ ## Dynamic lane task-model intent
627
+
628
+ A lane has one immutable task resolution and its original intake route. The
629
+ versioned configuration includes explicit pack, group and role model/effort
630
+ constraints. `models-template` takes a JSON input containing only the installed
631
+ `pack` name and returns a reviewable configuration tied to its current source
632
+ revision. `models-install` takes exactly `pack`, `configuration` and
633
+ `beforeDigest`; use `null` only for the first installation.
634
+
635
+ ```sh
636
+ topolo agent-packs lane models-template --input /absolute/pack-name.json
637
+ topolo agent-packs lane models-install --input /absolute/reviewed-model-install.json
638
+ topolo agent-packs lane status
639
+ ```
640
+
641
+ Installation records intent and preserves existing role settings. Resolution
642
+ intersects that configuration with the current native account catalog; no
643
+ unavailable model or effort falls back to a parent setting. For Claude, a
644
+ native model-ID observation is only an intent input. The actual fresh process
645
+ must acknowledge the exact model and effort before work. Explicit task overrides
646
+ also require their recorded intake authorization.
647
+
648
+ Each later operation rechecks current source, intake, context and policy. Native
649
+ creation/adoption, settings acknowledgement, own-file context bootstrap, original
650
+ intake permission decision, native operation outcome, fixture read-back,
651
+ settlement, preserved-history retirement and own-control closure are distinct
652
+ states. `lane status` reads durable state; `lane read` performs the qualified
653
+ current native read. Codex uses an explicit foreground `lane serve` connection
654
+ to the existing managed window. Its child creation and approval responses stay
655
+ on that captured native owner, without another app-server process.
656
+
657
+ A lost acknowledgement retains its possible-effect state and blocks replay.
658
+ Retirement requires the actual own child to be idle with no pending approvals,
659
+ and verifies native history preservation. Closing a connection removes only its
660
+ owned controls and subscriptions. Existing fixed lanes and their original
661
+ intake settings are preserved throughout.
662
+
663
+
664
+ Claude dynamic private startup currently refuses with
665
+ `native_private_query_prestart_contract_unqualified`. The installed desktop
666
+ contract has not qualified private query MCP configuration, exclusion of other
667
+ Topolo connections, and the owned shutdown lifecycle. Create, configure,
668
+ context start, fixture start, retirement, and positive permission forwarding
669
+ remain closed before native access. A valid private profile reference does not
670
+ lift this boundary. Source fixtures and descriptor reads do not establish
671
+ private MCP adoption or actual task completion.
672
+
673
+ ### Explicit native CLI continuity
674
+
675
+ Version 2 dynamic plans may name `continuityTransport: native-cli`; version 1
676
+ plans retain their existing behavior. The original intake relation is logical
677
+ routing. It does not invent native parentage for a peer session.
678
+
679
+ The existing public `remote-context` command accepts `--continuity-input` with
680
+ one owned, closed input file. The operation uses an absolute pinned Node/CLI,
681
+ selected profile metadata and an isolated child environment. Its receipt projects
682
+ public principal, organization, Agent/version, Agent/Bytes scope, source digests,
683
+ nonce and actual successful response request IDs. Raw stderr, response bodies,
684
+ credentials and environment dumps are excluded. Missing or ambiguous response
685
+ metadata is incomplete evidence.
686
+
687
+ A receipt itself cannot acknowledge native context. Verification requires the
688
+ same owning native query, its actual model/effort acknowledgement, exact command,
689
+ turn/tool identity, successful completion/exit and matching result within the
690
+ original request history boundary, plus genuine reads of the own charter/context.
691
+ Only the exact bootstrap command may receive bootstrap permission through the
692
+ original intake. Other work also requires verified context, current policy,
693
+ actual tool outcome and separate settlement. Approval forwarding alone is not
694
+ operation completion.
695
+
696
+ Current startup provenance, profile refresh authority and native Bash completion
697
+ contracts are unavailable. Effects stay closed with
698
+ `native_cli_startup_unqualified`; an unsupported result contract stays
699
+ `native_cli_tool_result_unqualified`. Input references, hashes or JSON flags do
700
+ not grant admission. Private MCP is a separate unsupported transport and is never
701
+ a fallback. Source fixtures do not activate either native harness.
@@ -15,7 +15,7 @@ from coverage.parser import PythonParser
15
15
 
16
16
  RELEASE_BASE = '6a13ada56844712aae58738ba01dbbdf90718bef'
17
17
  SOURCE = pathlib.Path('agent-packs/src/topolo_agent_pack')
18
- MODULES = {'topolo', 'claude', 'policy', 'permissions', 'approvals', 'execution', 'live', 'cli'}
18
+ MODULES = {'topolo', 'claude', 'policy', 'permissions', 'approvals', 'execution', 'live', 'cli', 'lanes', 'task_models'}
19
19
 
20
20
 
21
21
  class CoverageRefused(RuntimeError):
@@ -29,6 +29,28 @@ class ApprovalRouter:
29
29
  intake.get('contextKey') != role['contextKey']):
30
30
  raise PackError('Approval route has no same-context owned intake')
31
31
 
32
+ def register_dynamic(self, role):
33
+ """Append one route without replacing the connection or pending requests."""
34
+ intake = self.by_key.get(role.get('intakeKey'))
35
+ if (not role.get('dynamicLaneId') or not intake or '/intakes/' not in intake['key'] or
36
+ role.get('contextKey') != intake.get('contextKey') or
37
+ role.get('dynamicIntakeNativeId') != intake['threadId']):
38
+ raise PackError('Dynamic approval route has no exact same-context original intake')
39
+ old = self.roles.get(role['threadId'])
40
+ if old and old.get('dynamicPlanDigest') != role.get('dynamicPlanDigest'):
41
+ raise PackError('Dynamic approval identity conflicts with a current role')
42
+ if role['key'] in self.by_key and self.by_key[role['key']]['threadId'] != role['threadId']:
43
+ raise PackError('Dynamic approval role key is already owned')
44
+ self.roles[role['threadId']] = copy.deepcopy(role)
45
+ self.by_key[role['key']] = self.roles[role['threadId']]
46
+
47
+ def retire_dynamic(self, thread_id):
48
+ role = self.roles.get(thread_id)
49
+ if (not role or not role.get('dynamicLaneId') or
50
+ any(entry['record']['sourceThreadId'] == thread_id for entry in self.pending.values())):
51
+ raise PackError('Dynamic lane retains a pending native approval or is not an owned child')
52
+ self.roles.pop(thread_id); self.by_key.pop(role['key'])
53
+
32
54
  def capture(self, message):
33
55
  if message.get('method') not in METHODS or 'id' not in message:
34
56
  return None
@@ -325,8 +325,12 @@ def parser():
325
325
  q.add_argument('--apply',action='store_true',help='Configure the existing idle owned Codex backend; no new app server or model turn')
326
326
  q=sub.add_parser('approvals',help='Read and relay exact live native decisions through the owning intake')
327
327
  q.add_argument('operation',choices=['list','details','respond']);q.add_argument('key',nargs='?')
328
- q.add_argument('--intake');q.add_argument('--fingerprint');q.add_argument('--response')
328
+ q.add_argument('--connection');q.add_argument('--intake');q.add_argument('--fingerprint');q.add_argument('--response')
329
329
  q.add_argument('--human-decision-recorded',action='store_true')
330
+ q=sub.add_parser('lane',help='Plan, adopt and verify one pack-managed native child through its original intake')
331
+ q.add_argument('operation',choices=['plan','adopt','configure','status','read','settle','retire','serve','close','create','context-start','context-read','fixture-start','fixture-read','models-template','models-install'])
332
+ q.add_argument('--connection',help='Exact explicit foreground owned lane connection ID')
333
+ q.add_argument('--input',help='Closed operation JSON; never arbitrary native RPC or task prompts')
330
334
  q=sub.add_parser('smoke',help='Start or read one closed, bound native acceptance case; never arbitrary prompts')
331
335
  q.add_argument('operation',choices=['start','read']);q.add_argument('identity')
332
336
  q.add_argument('--case',choices=sorted(live.SMOKE_CASES));q.add_argument('--operation-id')
@@ -339,6 +343,8 @@ def parser():
339
343
  q.add_argument('--roles',required=True);q.add_argument('--agent-workspace',required=True);q.add_argument('--bytes-workspace',required=True)
340
344
  q.add_argument('--ownership',choices=['user','organization'],required=True);q.add_argument('--bytes-scope',choices=['private','org'],required=True);q.add_argument('--instance')
341
345
  q.add_argument('--reconcile-local-context',action='store_true',help='Explicitly reconcile a recorded partial context attachment; never replays cloud writes')
346
+ q.add_argument('--private-profile',action='store_true',help='Bind the explicitly selected protected profile metadata for native child helpers')
347
+ if command=='remote-context':q.add_argument('--continuity-input',help='Closed native-cli continuity input; runtime authority must independently qualify before any client')
342
348
  if command=='remember':
343
349
  q.add_argument('--content',required=True);q.add_argument('--kind',choices=['instruction','knowledge','durable'],required=True)
344
350
  q.add_argument('--provenance',required=True);q.add_argument('--visibility',choices=['private','shared'],default='private')
@@ -366,8 +372,9 @@ def main(argv=None):
366
372
  command_args=list(argv)
367
373
  if command_args and command_args[0]=='--store':command_args=command_args[2:]
368
374
  elif command_args and command_args[0].startswith('--store='):command_args=command_args[1:]
375
+ lane_command=bool(command_args and command_args[0]=='lane')
369
376
  cloud_command=bool(command_args and command_args[0] in {'connect','remote-context','remember','artifact','artifact-get','remote-recover'})
370
- if harness == 'claude' and not policy_command and not cloud_command:
377
+ if harness == 'claude' and not policy_command and not cloud_command and not lane_command:
371
378
  from . import claude
372
379
  return claude.main(argv)
373
380
  if harness not in {'codex', 'claude'}:
@@ -379,11 +386,41 @@ def main(argv=None):
379
386
  lib=Library(args.store)
380
387
  if args.command in {'connect','remote-context','remember','artifact','artifact-get','remote-recover'}:
381
388
  from . import topolo
382
- with topolo.connection_lock(lib,args):result=topolo.dispatch(lib,args)
389
+ if args.command=='remote-context' and args.continuity_input:
390
+ result=topolo.native_cli_continuity_read(lib,args)
391
+ else:
392
+ with topolo.connection_lock(lib,args):result=topolo.dispatch(lib,args)
383
393
  emit(result)
384
394
  return 0
395
+ if args.command=='lane' and args.operation=='serve':
396
+ if harness!='codex' or not args.connection or not args.input:
397
+ raise PackError('Explicit Codex connection ID and reviewed input binding required')
398
+ import asyncio
399
+ asyncio.run(live.OwnedLaneConnection(lib,args.connection,read_json(args.input)).run());return 0
385
400
  with lib.lock():
386
- if args.command=='smoke':
401
+ if args.command=='lane':
402
+ from .lanes import Registry
403
+ params=read_json(args.input) if args.input else {}
404
+ if not isinstance(params,dict):
405
+ raise PackError('Closed native lane input object required')
406
+ if args.operation in {'models-template','models-install'}:
407
+ from . import lanes
408
+ if args.operation=='models-template':
409
+ if set(params)!={'pack'}:
410
+ raise PackError('Exact source pack required')
411
+ manifest,revision=lib.load(params['pack']);result=lanes.model_configuration_template(manifest,revision)
412
+ else:
413
+ if set(params)!={'pack','configuration','beforeDigest'}:
414
+ raise PackError('Exact model configuration and beforeimage required')
415
+ result=lanes.install_model_configuration(lib,params['pack'],params['configuration'],params['beforeDigest'])
416
+ elif args.operation=='status':
417
+ if params:
418
+ raise PackError('Lane status accepts no overrides')
419
+ result={'harness':harness,'lanes':Registry(lib.root,harness).records(), 'proof':'durable intent; native connection must requalify'}
420
+ elif harness!='codex':
421
+ raise PackError('Use the native Claude lane adapter with its qualified connection')
422
+ else: result=(live.request_lane(lib,args.connection,'lane-'+args.operation,params) if args.connection else live.request(lib,'lane-'+args.operation,params))
423
+ elif args.command=='smoke':
387
424
  if args.operation=='start':
388
425
  if not args.case or not args.operation_id:
389
426
  raise PackError('Exact smoke case and operation ID required')
@@ -401,7 +438,7 @@ def main(argv=None):
401
438
  if not args.response or not args.human_decision_recorded:
402
439
  raise PackError('Response file and recorded human decision required')
403
440
  params.update(result=read_json(args.response),humanDecisionRecorded=True)
404
- result=live.request(lib,'approval-'+args.operation,params)
441
+ result=live.request_lane(lib,args.connection,'approval-'+args.operation,params) if args.connection else live.request(lib,'approval-'+args.operation,params)
405
442
  elif args.command=='permissions':
406
443
  from . import permissions, topolo
407
444
  if args.apply and harness!='codex':
@@ -62,7 +62,9 @@ class ExecutionGate:
62
62
  required = {'model', 'model_reasoning_effort', 'approval_policy', 'approvals_reviewer',
63
63
  'sandbox_mode', 'sandbox_workspace_write', 'apps'}
64
64
  if (set(spec) != required or not isinstance(spec['model'], str) or not spec['model'] or
65
- spec['model_reasoning_effort'] not in {'medium', 'high'} or
65
+ (spec['model_reasoning_effort'] not in {'medium', 'high'} and not
66
+ (role.get('dynamicLaneId') and role.get('dynamicModelResolution', {}).get('resolved') ==
67
+ {'model': spec['model'], 'effort': spec['model_reasoning_effort']})) or
66
68
  spec['approval_policy'] not in {'on-request', 'never'} or spec['approvals_reviewer'] != 'user' or
67
69
  spec['sandbox_mode'] not in {'read-only', 'workspace-write', 'danger-full-access'} or
68
70
  (spec['sandbox_mode'] == 'danger-full-access') != (spec['approval_policy'] == 'never') or
@@ -74,6 +76,59 @@ class ExecutionGate:
74
76
  (spec['apps'] != {'_default': {'default_tools_approval_mode': 'approve'}} or not role.get('contextKey') or not role.get('intakeKey')))):
75
77
  raise PackError('Unqualified role execution settings')
76
78
  self.roles[tid] = copy.deepcopy(role)
79
+ if role.get('dynamicLaneId'):
80
+ # Persisted intent cannot reinstate native proof after reconnect.
81
+ self.roles[tid]['dynamicCurrentQualified'] = False
82
+ self.roles[tid]['dynamicContextVerified'] = False
83
+
84
+ def register_dynamic(self, record):
85
+ from .lanes import execution_role
86
+ row = execution_role(record)
87
+ parent = self.roles.get(row['sourceRoleNativeId'])
88
+ intake = self.roles.get(row['dynamicIntakeNativeId'])
89
+ if (not parent or not intake or parent['key'] != row['sourceRoleKey'] or
90
+ intake['key'] != row['intakeKey'] or parent.get('intakeKey') != row['intakeKey'] or
91
+ parent.get('contextKey') != row['contextKey'] or intake.get('contextKey') != row['contextKey'] or
92
+ parent['sourceRevision'] != row['sourceRevision']):
93
+ raise PackError('Dynamic execution has no current same-context source role and original intake')
94
+ existing = self.roles.get(row['threadId'])
95
+ if existing is not None:
96
+ if existing.get('dynamicPlanDigest') != row['dynamicPlanDigest']:
97
+ raise PackError('Dynamic native identity conflicts with an existing managed role')
98
+ return copy.deepcopy(existing)
99
+ row['installationConsent'] = copy.deepcopy(parent.get('installationConsent'))
100
+ candidate = ExecutionGate({'format': 'topolo-codex-runtime-execution', 'schemaVersion': 1, 'roles': list({r['threadId']: r for r in [parent, intake, row]}.values())})
101
+ choice = row['dynamicModelResolution']['resolved']
102
+ if choice['effort'] not in self.catalog.get(choice['model'], set()):
103
+ raise PackError('Dynamic task model is unavailable in the current native account catalog')
104
+ self.roles[row['threadId']] = candidate.roles[row['threadId']]
105
+ self.verified.discard(row['threadId'])
106
+ return copy.deepcopy(row)
107
+
108
+ def prepare_dynamic_configuration(self, record, current):
109
+ if record['state'] != 'adopted-unconfigured' or current != {
110
+ 'role': record['plan']['role'], 'intake': record['plan']['intake'], 'ceiling': record['plan']['ceiling']}:
111
+ raise PackError('Dynamic configuration requires fresh adopted source and policy inputs')
112
+ self.register_dynamic(record)
113
+ self.roles[record['nativeId']]['dynamicCurrentQualified'] = True
114
+ # Only resume is allowed until the native settings and loaded context ACK.
115
+ self.roles[record['nativeId']]['dynamicContextVerified'] = False
116
+
117
+ def qualify_dynamic(self, record, current):
118
+ role = self.roles.get(record['nativeId'])
119
+ plan = record['plan']
120
+ if (not role or role.get('dynamicPlanDigest') != record['planDigest'] or
121
+ current != {'role': plan['role'], 'intake': plan['intake'], 'ceiling': plan['ceiling']} or
122
+ record.get('nativeSettingsACK') != plan['resolution']['resolved']):
123
+ raise PackError('Dynamic role, intake or policy input is stale or unqualified')
124
+ role['dynamicCurrentQualified'] = True
125
+ role['dynamicContextVerified'] = record.get('contextACK') == plan['role']['contextDigest']
126
+
127
+ def retire_dynamic(self, thread_id):
128
+ role = self.roles.get(thread_id)
129
+ if (not role or not role.get('dynamicLaneId') or thread_id in self.pending.values()):
130
+ raise PackError('Only an idle owned dynamic execution row may retire')
131
+ self.roles.pop(thread_id); self.verified.discard(thread_id)
77
132
 
78
133
  def set_catalog(self, result):
79
134
  self.catalog = {row['model']: {e['reasoningEffort'] for e in row['supportedReasoningEfforts']}
@@ -90,17 +145,24 @@ class ExecutionGate:
90
145
  def prepare(self, message):
91
146
  value = copy.deepcopy(message); method = value.get('method'); params = value.get('params') or {}
92
147
  tid = params.get('threadId'); role = self.roles.get(tid)
148
+ if role and role.get('dynamicLaneId') and method == 'thread/settings/update':
149
+ raise PackError('Dynamic settings changes require a new reviewed task resolution and native ACK')
93
150
  if not role or method not in {'thread/resume', 'turn/start'}:
94
151
  return value
95
152
  if 'id' not in value or str(value['id']) in self.pending:
96
153
  raise PackError('Managed execution requires a unique request identity')
97
154
  spec = role['settings']
155
+ if role.get('dynamicLaneId') and not role.get('dynamicCurrentQualified'):
156
+ raise PackError('Dynamic lane needs fresh source, intake and policy qualification on this connection')
157
+ if role.get('dynamicLaneId') and not role.get('dynamicContextVerified') and method == 'turn/start':
158
+ raise PackError('Dynamic lane needs actual loaded role context before task execution')
98
159
  if spec['model_reasoning_effort'] not in self.catalog.get(spec['model'], set()):
99
160
  raise PackError('Role model/effort is absent from the native account catalog; no fallback selected')
100
161
  if any(params.get(key) is not None for key in ['permissions', 'path', 'history', 'collaborationMode', 'environments', 'runtimeWorkspaceRoots']):
101
162
  raise PackError('Frontend supplied unreviewed permission, identity or execution overrides')
102
163
  wanted = {'cwd': role['cwd'], 'model': spec['model'], 'approvalPolicy': spec['approval_policy'],
103
164
  'approvalsReviewer': spec['approvals_reviewer']}
165
+ if role.get('dynamicLaneId') and method=='thread/resume':wanted['allowProviderModelFallback']=False
104
166
  if method == 'thread/resume':
105
167
  wanted['sandbox'] = spec['sandbox_mode']
106
168
  wanted['config'] = {'model_reasoning_effort': spec['model_reasoning_effort'],
@@ -148,12 +210,18 @@ def load_gate(runtime):
148
210
  return ExecutionGate(read_json(path)) if path.exists() else None
149
211
 
150
212
 
151
- def qualify_protocol(binary, run=subprocess.run):
213
+ def qualify_protocol(binary, run=subprocess.run, *, lane_creation=False):
152
214
  """Generate schema offline from this executable; never initialize an app server."""
153
215
  required = {'ThreadResumeParams': {'threadId', 'model', 'approvalPolicy', 'approvalsReviewer', 'sandbox', 'config'},
154
216
  'ThreadResumeResponse': {'thread', 'model', 'cwd', 'reasoningEffort', 'approvalPolicy', 'approvalsReviewer', 'sandbox'},
155
217
  'TurnStartParams': {'threadId', 'model', 'effort', 'approvalPolicy', 'approvalsReviewer', 'sandboxPolicy'},
156
218
  'ModelListParams': {'includeHidden', 'cursor'}, 'ModelListResponse': {'data', 'nextCursor'}}
219
+ if lane_creation:
220
+ required.update({'ThreadStartParams':{'cwd','model','config','developerInstructions','approvalPolicy','sandbox','allowProviderModelFallback'},
221
+ 'ThreadStartResponse':{'thread','model','cwd','reasoningEffort','instructionSources'},
222
+ 'ThreadReadParams':{'threadId','includeTurns'},'ThreadReadResponse':{'thread'},'ThreadArchiveParams':{'threadId'},
223
+ 'ConfigReadParams':{'cwd','includeLayers'},'ConfigReadResponse':{'config','layers','origins'}})
224
+ required['ThreadResumeParams'].add('allowProviderModelFallback')
157
225
  with tempfile.TemporaryDirectory(prefix='topolo-codex-protocol-') as temp:
158
226
  result = run([binary, 'app-server', 'generate-json-schema', '--experimental', '--out', temp],
159
227
  capture_output=True, timeout=20, check=False)