@topolo/cli 0.13.8 → 0.13.10
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 +34 -5
- package/agent-packs/README.md +98 -0
- package/agent-packs/coverage_changed.py +1 -1
- package/agent-packs/src/topolo_agent_pack/approvals.py +22 -0
- package/agent-packs/src/topolo_agent_pack/cli.py +37 -4
- package/agent-packs/src/topolo_agent_pack/execution.py +70 -2
- package/agent-packs/src/topolo_agent_pack/lanes.py +749 -0
- package/agent-packs/src/topolo_agent_pack/live.py +914 -16
- package/agent-packs/src/topolo_agent_pack/policy.py +6 -0
- package/agent-packs/src/topolo_agent_pack/task_models.py +200 -0
- package/agent-packs/src/topolo_agent_pack/topolo.py +42 -3
- package/dist/claude-pack-backend-RMSOPHVQ.js +2380 -0
- package/dist/index.js +270 -66
- package/package.json +5 -5
- package/dist/claude-pack-backend-3YJNKMYD.js +0 -1207
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
|
|
370
|
-
|
|
371
|
-
installation is needed.
|
|
372
|
-
|
|
373
|
-
|
|
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,33 @@ 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.
|
package/agent-packs/README.md
CHANGED
|
@@ -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,89 @@ 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.
|
|
@@ -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,7 @@ 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')
|
|
342
347
|
if command=='remember':
|
|
343
348
|
q.add_argument('--content',required=True);q.add_argument('--kind',choices=['instruction','knowledge','durable'],required=True)
|
|
344
349
|
q.add_argument('--provenance',required=True);q.add_argument('--visibility',choices=['private','shared'],default='private')
|
|
@@ -366,8 +371,9 @@ def main(argv=None):
|
|
|
366
371
|
command_args=list(argv)
|
|
367
372
|
if command_args and command_args[0]=='--store':command_args=command_args[2:]
|
|
368
373
|
elif command_args and command_args[0].startswith('--store='):command_args=command_args[1:]
|
|
374
|
+
lane_command=bool(command_args and command_args[0]=='lane')
|
|
369
375
|
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:
|
|
376
|
+
if harness == 'claude' and not policy_command and not cloud_command and not lane_command:
|
|
371
377
|
from . import claude
|
|
372
378
|
return claude.main(argv)
|
|
373
379
|
if harness not in {'codex', 'claude'}:
|
|
@@ -382,8 +388,35 @@ def main(argv=None):
|
|
|
382
388
|
with topolo.connection_lock(lib,args):result=topolo.dispatch(lib,args)
|
|
383
389
|
emit(result)
|
|
384
390
|
return 0
|
|
391
|
+
if args.command=='lane' and args.operation=='serve':
|
|
392
|
+
if harness!='codex' or not args.connection or not args.input:
|
|
393
|
+
raise PackError('Explicit Codex connection ID and reviewed input binding required')
|
|
394
|
+
import asyncio
|
|
395
|
+
asyncio.run(live.OwnedLaneConnection(lib,args.connection,read_json(args.input)).run());return 0
|
|
385
396
|
with lib.lock():
|
|
386
|
-
if args.command=='
|
|
397
|
+
if args.command=='lane':
|
|
398
|
+
from .lanes import Registry
|
|
399
|
+
params=read_json(args.input) if args.input else {}
|
|
400
|
+
if not isinstance(params,dict):
|
|
401
|
+
raise PackError('Closed native lane input object required')
|
|
402
|
+
if args.operation in {'models-template','models-install'}:
|
|
403
|
+
from . import lanes
|
|
404
|
+
if args.operation=='models-template':
|
|
405
|
+
if set(params)!={'pack'}:
|
|
406
|
+
raise PackError('Exact source pack required')
|
|
407
|
+
manifest,revision=lib.load(params['pack']);result=lanes.model_configuration_template(manifest,revision)
|
|
408
|
+
else:
|
|
409
|
+
if set(params)!={'pack','configuration','beforeDigest'}:
|
|
410
|
+
raise PackError('Exact model configuration and beforeimage required')
|
|
411
|
+
result=lanes.install_model_configuration(lib,params['pack'],params['configuration'],params['beforeDigest'])
|
|
412
|
+
elif args.operation=='status':
|
|
413
|
+
if params:
|
|
414
|
+
raise PackError('Lane status accepts no overrides')
|
|
415
|
+
result={'harness':harness,'lanes':Registry(lib.root,harness).records(), 'proof':'durable intent; native connection must requalify'}
|
|
416
|
+
elif harness!='codex':
|
|
417
|
+
raise PackError('Use the native Claude lane adapter with its qualified connection')
|
|
418
|
+
else: result=(live.request_lane(lib,args.connection,'lane-'+args.operation,params) if args.connection else live.request(lib,'lane-'+args.operation,params))
|
|
419
|
+
elif args.command=='smoke':
|
|
387
420
|
if args.operation=='start':
|
|
388
421
|
if not args.case or not args.operation_id:
|
|
389
422
|
raise PackError('Exact smoke case and operation ID required')
|
|
@@ -401,7 +434,7 @@ def main(argv=None):
|
|
|
401
434
|
if not args.response or not args.human_decision_recorded:
|
|
402
435
|
raise PackError('Response file and recorded human decision required')
|
|
403
436
|
params.update(result=read_json(args.response),humanDecisionRecorded=True)
|
|
404
|
-
result=live.request(lib,'approval-'+args.operation,params)
|
|
437
|
+
result=live.request_lane(lib,args.connection,'approval-'+args.operation,params) if args.connection else live.request(lib,'approval-'+args.operation,params)
|
|
405
438
|
elif args.command=='permissions':
|
|
406
439
|
from . import permissions, topolo
|
|
407
440
|
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'}
|
|
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)
|