@topolo/cli 0.13.10 → 0.13.12

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
@@ -436,3 +436,25 @@ context start, fixture start, retirement, and positive permission forwarding
436
436
  remain closed before native access. A valid private profile reference does not
437
437
  lift this boundary. Source fixtures and descriptor reads do not establish
438
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.
451
+
452
+ ### Preserving private pack runtime configuration
453
+
454
+ `topolo agent-packs --store /owned/AgentPacks runtime-adoption plan --manifest /review/adoption.json` produces a nonwriting configuration plan. Review its physical targets and digest, then use `runtime-adoption apply --manifest /review/adoption.json --plan-digest <sha256> --confirm` for one configuration-only attempt.
455
+
456
+ The closed owner manifest binds the actual consumer set and effective local Topolo transport origins, physical file preimages, shared aliases, private profile metadata, immutable CLI 0.13.11/MCP 0.14.26/SDK 0.23.21 targets and the separately pinned successor Python adapter. It must include every server in each physical config as either a targeted transport or an explicitly untouched server. Empty or incomplete manifests refuse. The command never installs skills, rebinds a home, changes existing instructions/permissions, starts a consumer or probes Auth.
457
+
458
+ A successful apply means configuration is prepared. It does not mean existing processes reloaded it, the private account is healthy, permissions were granted or either native harness completed a task. Starting or refreshing consumers is a separate owner-authorized step and can contact private Auth and renew the selected private profile. Public credential-store repair is not a blanket prerequisite for independent private profiles. See [the manifest and effect boundaries](agent-packs/README.md#private-runtime-adoption).
459
+
460
+ The protocol SDK pin addresses [GHSA-6qxp-vccf-f47h](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-6qxp-vccf-f47h). The CLI doctor's protocol transport is stdio. Separate HTTP OAuth consumers still need appropriate issuer binding for legacy credentials and bundled providers; this source update neither inspects nor migrates credentials.
@@ -669,3 +669,56 @@ context start, fixture start, retirement, and positive permission forwarding
669
669
  remain closed before native access. A valid private profile reference does not
670
670
  lift this boundary. Source fixtures and descriptor reads do not establish
671
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.
702
+
703
+ ## Private runtime adoption
704
+
705
+ The public CLI forwards `agent-packs runtime-adoption plan|apply` to the bundled successor adapter. `plan` performs no writes. `apply` requires the exact reviewed `--plan-digest` and `--confirm`; it never calls `prepare`, `bind`, `reconnect`, skill installation or a native/Auth client. The selected immutable public CLI/MCP cohort is separate from the successor Python adapter source. Do not edit an installed immutable runtime to add the successor hooks.
706
+
707
+ The manifest is a local owner-reviewed configuration scope, not native provenance, a permission grant or a health certificate. Its exact top-level fields are:
708
+
709
+ - `schemaVersion: 1`, a fresh UUID `operationId`, and the absolute physical `library`.
710
+ - `ownerGeneration: {path, sha256}`: the owner's current bounded nonsecret effective-transport/generation record, inside that library. The owner must establish its native currency separately.
711
+ - `consumers`: the complete unique existing native IDs in scope. Target rows must account for every ID; no new native IDs are created.
712
+ - `profile: {path, publicPath, metadata, environment, contextKey, principalId}`: absolute private and public credential-file references, explicit organization context, and metadata only. Files must be physically distinct; the private profile must be owned, single-link, regular and mode 0600. Neither profile's contents are read or hashed. A renewal that changes private file metadata invalidates the descriptor and requires owner reconciliation.
713
+ - `runtime: {node, cli, mcp, cohort, pythonSource, adapterFiles, osEnvironment}`. Each executable/entry and each `cohort` value is `{path, sha256}`. Cohort keys `cli`, `mcp`, `sdk` pin their package JSON; required target versions are 0.13.11, 0.14.26 and 0.23.21. CLI and MCP entries must be their pinned package's `dist/index.js`. `pythonSource` is the physical successor source directory; `adapterFiles` pins its `topolo_agent_pack/{cli,live,topolo}.py`. Verify the complete installed cohort independently before admitting its pins. `osEnvironment` permits only HOME/PATH and optional USER/LOGNAME/LANG/LC_ALL/TMPDIR, with no credential, service-origin, Node or installation overrides.
714
+ - `targets`: one row per unique physical configuration file, with `{path, before, aliases, format, rootKey, consumers, transports, untouchedServers}`. Supported formats are JSON `mcpServers` and simple TOML `mcp_servers` tables. Each transport is `{name, origin, consumers}` and must already exist as a local command; URL, inherited `env_vars`, installation-mode, ambiguous names or unsupported TOML layouts refuse. `origin` is the exact physical file. The complete physical server map must equal the targeted names plus `untouchedServers`. The owner must independently bind all effective native layers; a zero-table query or a guessed server name is insufficient.
715
+ - `launcher: {path, before}`: exactly the owned generated `launchers/topolo-agent-pack-backend`; its interpreter, owner and mode are preserved.
716
+ - `preserve`: nonsecret `{path, sha256}` references to existing roster, model/effort, permission, charter/context, routing and lifecycle records. Include every protected fixed and dynamic record in the approved scope. These may not overlap destinations. No transcript or credential contents belong in a manifest.
717
+
718
+ A physical `before` snapshot contains `device`, `inode`, `uid`, numeric permission `mode`, `bytes`, `mtimeNs`, `ctimeNs`, `parents` (absolute path/device/inode triples), and `sha256`. Profile `metadata` uses the same shape without `sha256`. Each alias is `{path, linkTarget, metadata}`, where `metadata` is the symlink's device/inode/uid/full mode/size/mtimeNs/ctimeNs tuple. The plan emits actual beforeimages for review; effect admission must independently establish the owner, complete source set, physical sharing and current generation. It cannot silently authorize another consumer sharing a file.
719
+
720
+ Only existing owned physical destinations inside the library can change. Shared desktop/backend config symlinks remain intact and their common file changes once. The generated private descriptor and isolated Python MCP launcher are operation-specific. The isolated MCP launcher validates successor bytes before import, then current profile/generation/preservation pins before replacing itself with the explicit Node/MCP pair and closed private environment. This retains the native host's PID, stdio and cancellation custody. Existing host agent names are retained as a closed wrapper argument. Managed default helper clients use the exact Node/CLI pair regardless of PATH. Unmanaged clients retain their ordinary behavior. No parent credential/service-origin/installation/Node environment is copied into those Topolo children. An intentional installation credential mode needs its own separately explicit contract.
721
+
722
+ Apply reserves `launchers/adoption-<UUID>.json` exclusively. The journal starts in `may-have-config-effects`, records each verified replacement, and becomes `configuration-prepared-native-adoption-unverified` only after all readbacks. Every write rechecks generation, source, profile metadata, preservation pins and destination custody. An interrupted or conflicting apply retains the journal and any uncertain temporary file; it never automatically replays or rolls back. Reconcile exact effects with the original owner.
723
+
724
+ No apply result proves native reload, identity, current RBAC, model/effort, loaded context, original-intake permission routing or a native tool result. Hosts may react to configuration changes; admit the actual affected owner/generation and normal request settlement before applying in a live host. Later ordinary startup can renew the private credential and load identity. A previous 429/403 or scoped recovery hold remains separately applicable and elapsed time grants no retry. Actual Codex and Claude same-query context, permission, nonce result, cancellation/settlement and own retirement require independent native evidence.
@@ -290,6 +290,9 @@ def parser():
290
290
  p.add_argument('--version',action='version',version=__version__)
291
291
  p.add_argument('--store',default=os.environ.get('CODEX_PACK_HOME',str(pathlib.Path.home()/'AgentPacks')))
292
292
  sub=p.add_subparsers(dest='command',required=True)
293
+ q=sub.add_parser('runtime-adoption',help='Configuration-only private runtime plan/apply; no consumer startup or authentication')
294
+ q.add_argument('operation',choices=['plan','apply']);q.add_argument('--manifest',required=True)
295
+ q.add_argument('--plan-digest');q.add_argument('--confirm',action='store_true')
293
296
  def selection(q):
294
297
  q.add_argument('--set',dest='sets',action='append');q.add_argument('--agent',dest='agents',action='append')
295
298
  def activation(q):
@@ -344,6 +347,7 @@ def parser():
344
347
  q.add_argument('--ownership',choices=['user','organization'],required=True);q.add_argument('--bytes-scope',choices=['private','org'],required=True);q.add_argument('--instance')
345
348
  q.add_argument('--reconcile-local-context',action='store_true',help='Explicitly reconcile a recorded partial context attachment; never replays cloud writes')
346
349
  q.add_argument('--private-profile',action='store_true',help='Bind the explicitly selected protected profile metadata for native child helpers')
350
+ if command=='remote-context':q.add_argument('--continuity-input',help='Closed native-cli continuity input; runtime authority must independently qualify before any client')
347
351
  if command=='remember':
348
352
  q.add_argument('--content',required=True);q.add_argument('--kind',choices=['instruction','knowledge','durable'],required=True)
349
353
  q.add_argument('--provenance',required=True);q.add_argument('--visibility',choices=['private','shared'],default='private')
@@ -373,7 +377,7 @@ def main(argv=None):
373
377
  elif command_args and command_args[0].startswith('--store='):command_args=command_args[1:]
374
378
  lane_command=bool(command_args and command_args[0]=='lane')
375
379
  cloud_command=bool(command_args and command_args[0] in {'connect','remote-context','remember','artifact','artifact-get','remote-recover'})
376
- if harness == 'claude' and not policy_command and not cloud_command and not lane_command:
380
+ if harness == 'claude' and not policy_command and not cloud_command and not lane_command and not (command_args and command_args[0]=='runtime-adoption'):
377
381
  from . import claude
378
382
  return claude.main(argv)
379
383
  if harness not in {'codex', 'claude'}:
@@ -382,10 +386,18 @@ def main(argv=None):
382
386
  args=parser().parse_args(argv)
383
387
  args.harness=harness
384
388
  try:
389
+ if args.command=='runtime-adoption':
390
+ if harness!='codex':
391
+ raise PackError('Runtime adoption is one explicit cross-host owner manifest, not a harness override')
392
+ emit(live.private_runtime_adoption(args.store,args.manifest,args.operation,args.plan_digest,args.confirm))
393
+ return 0
385
394
  lib=Library(args.store)
386
395
  if args.command in {'connect','remote-context','remember','artifact','artifact-get','remote-recover'}:
387
396
  from . import topolo
388
- with topolo.connection_lock(lib,args):result=topolo.dispatch(lib,args)
397
+ if args.command=='remote-context' and args.continuity_input:
398
+ result=topolo.native_cli_continuity_read(lib,args)
399
+ else:
400
+ with topolo.connection_lock(lib,args):result=topolo.dispatch(lib,args)
389
401
  emit(result)
390
402
  return 0
391
403
  if args.command=='lane' and args.operation=='serve':
@@ -9,6 +9,9 @@ import hashlib
9
9
  import os
10
10
  import pathlib
11
11
  import stat
12
+ import re
13
+ import uuid
14
+ import time
12
15
  from contextlib import contextmanager
13
16
 
14
17
  from .storage import PackError, atomic_json, canonical, read_json, safe_id
@@ -51,7 +54,7 @@ def _role(value, harness):
51
54
 
52
55
  def plan(request, role, intake, resolution, ceiling):
53
56
  if (not isinstance(request, dict) or not {'laneId','logicalOriginThreadId','sourceRoleKey','task'}<=set(request) or
54
- set(request)-{'laneId','logicalOriginThreadId','sourceRoleKey','task','verificationProfile'}):
57
+ set(request)-{'laneId','logicalOriginThreadId','sourceRoleKey','task','verificationProfile','continuityTransport','continuity'}):
55
58
  raise PackError('Dynamic lane request has unsupported fields')
56
59
  if request.get('verificationProfile') not in {None,'manual'}:
57
60
  raise PackError('Only a separately bound narrower manual verification profile is supported')
@@ -68,7 +71,12 @@ def plan(request, role, intake, resolution, ceiling):
68
71
  raise PackError('Dynamic lane has no exact current policy ceiling and intake route')
69
72
  for key in ['policyRevision', 'policyDigest', 'profile']:
70
73
  _identity(ceiling[key], key)
71
- return {'format': 'topolo-dynamic-lane-plan', 'schemaVersion': 1,
74
+ explicit_cli='continuityTransport' in request or 'continuity' in request
75
+ if explicit_cli:
76
+ if request.get('continuityTransport')!='native-cli' or set(request.get('continuity',{}))!={'cli','admissionReferences'}:
77
+ raise PackError('Explicit closed native-cli continuity transport required; no fallback')
78
+ native_cli_configuration(request['continuity'])
79
+ result={'format': 'topolo-dynamic-lane-plan', 'schemaVersion': 2 if explicit_cli else 1,
72
80
  'laneId': request['laneId'], 'harness': harness,
73
81
  'logicalOriginThreadId': request['logicalOriginThreadId'],
74
82
  'role': copy.deepcopy(role), 'intake': copy.deepcopy(intake),
@@ -78,6 +86,10 @@ def plan(request, role, intake, resolution, ceiling):
78
86
  'nativeParentId': intake['nativeId'] if harness == 'claude' else None,
79
87
  'nativeParentAvailability': 'actual-harness-intake' if harness == 'claude' else 'UNAVAILABLE',
80
88
  'state': 'planned', 'executionAuthorized': False}
89
+ if explicit_cli:
90
+ result.update(continuityTransport='native-cli',continuity=copy.deepcopy(request['continuity']),
91
+ nativeParentId=None,nativeParentAvailability='actual-native-peer')
92
+ return result
81
93
 
82
94
 
83
95
  def adopt(planned, receipt, native, current):
@@ -110,7 +122,7 @@ def adopt(planned, receipt, native, current):
110
122
  _identity(native.get('title'),'actual child title')
111
123
  if not pathlib.Path(receipt['cwd']).is_absolute():
112
124
  raise PackError('Created lane requires its exact absolute owned workspace')
113
- return {'format': 'topolo-dynamic-lane', 'schemaVersion': 1,
125
+ return {'format': 'topolo-dynamic-lane', 'schemaVersion': planned['schemaVersion'],
114
126
  'laneId': planned['laneId'], 'harness': planned['harness'], 'nativeId': receipt['nativeId'],
115
127
  'plan': copy.deepcopy(planned), 'planDigest': digest(planned),
116
128
  'creationReceipt': copy.deepcopy(receipt), 'creationReceiptDigest': digest(receipt),
@@ -139,6 +151,8 @@ def acknowledge_context(record, context_digest):
139
151
 
140
152
 
141
153
  def request_permission(record, request_fingerprint, request_id, generation):
154
+ if record['plan'].get('continuityTransport')=='native-cli' and record.get('nativeCliContext',{}).get('state')!='native-cli-context-ACK':
155
+ raise PackError('Native-cli work requires verified same-query public context receipt; bootstrap is separately scoped')
142
156
  if record['state'] != 'loaded-context-ACK' or generation != record['nativeConnectionGeneration']:
143
157
  raise PackError('Dynamic permission request has no current configured connection')
144
158
  _identity(request_fingerprint, 'native request fingerprint'); _identity(request_id, 'native request identity')
@@ -263,7 +277,7 @@ class Registry:
263
277
  if path.stat().st_size > 131072:
264
278
  raise PackError('Dynamic lane record exceeds its bounded schema')
265
279
  record = read_json(path)
266
- if (record.get('format') != 'topolo-dynamic-lane' or record.get('schemaVersion') != 1 or
280
+ if (record.get('format') != 'topolo-dynamic-lane' or record.get('schemaVersion') not in {1,2} or not native_cli_record_version(record) or
267
281
  record.get('laneId') != lane_id or record.get('harness') != self.harness or
268
282
  record.get('state') not in STATES or
269
283
  digest(record.get('plan')) != record.get('planDigest') or
@@ -297,7 +311,7 @@ class Registry:
297
311
 
298
312
  def write(self, record, before_digest):
299
313
  lane_id = record['laneId']
300
- if (record.get('format') != 'topolo-dynamic-lane' or record.get('schemaVersion') != 1 or
314
+ if (record.get('format') != 'topolo-dynamic-lane' or record.get('schemaVersion') not in {1,2} or not native_cli_record_version(record) or
301
315
  record['harness'] != self.harness or record['state'] not in STATES or
302
316
  digest(record['plan']) != record['planDigest'] or
303
317
  digest(record['creationReceipt']) != record['creationReceiptDigest']):
@@ -647,6 +661,14 @@ class ClaudeLifecycle:
647
661
  def dispatch(self,action,payload):
648
662
  if not isinstance(payload,dict):
649
663
  raise PackError('Closed native helper object required')
664
+ candidates=[payload.get('request'),payload.get('plan'),payload.get('operation',{}).get('record',{}).get('plan') if isinstance(payload.get('operation'),dict) and isinstance(payload.get('operation',{}).get('record',{}),dict) else None]
665
+ for candidate in candidates:
666
+ if isinstance(candidate,dict) and (candidate.get('continuityTransport')=='native-cli' or candidate.get('schemaVersion')==2 or 'continuity' in candidate):
667
+ native_cli_admission(candidate.get('continuity'))
668
+ if isinstance(payload.get('laneId'),str):
669
+ existing=self.store.read(payload['laneId'])
670
+ if existing and existing['plan'].get('continuityTransport')=='native-cli':
671
+ native_cli_admission(existing['plan']['continuity'])
650
672
  base={'models'}
651
673
  shapes={
652
674
  'peek':{'sourceRoleKey'},'plan':{'request','nativeIds'},'adopt':{'plan','receipt','native'},
@@ -747,3 +769,134 @@ def install_model_configuration(lib,pack,document,before_digest):
747
769
  raise PackError('Dynamic configuration readback differs')
748
770
  return {'state':'dynamic-task-model-intent-installed','pack':pack,'configurationDigest':digest(document),
749
771
  'nativeAvailabilityVerified':False,'existingRoleSettingsChanged':False,'modelTurnsStarted':False}
772
+
773
+
774
+ # Explicit native-query CLI continuity. These validators do not grant authority.
775
+ NATIVE_CLI_CONTEXT_FIELDS={'principalId','organizationId','contextKey','installationId','logicalId','agentId',
776
+ 'agentVersionId','agentWorkspaceId','bytesWorkspaceId','environment','bytesScope'}
777
+ NATIVE_CLI_SOURCE_FIELDS={'packRevision','planDigest','bindingDigest','policyDigest','charterDigest','contextDigest'}
778
+ NATIVE_CLI_CLI_FIELDS={'nodeExecutable','nodeDigest','entry','entryDigest','version'}
779
+ NATIVE_CLI_AUTHORITY_FIELDS={'startupEvidencePath','startupEvidenceDigest','profileAuthorityPath','profileAuthorityDigest','expiresAt'}
780
+ NATIVE_CLI_INPUT_FIELDS={'format','schemaVersion','operationId','nonce','transport','harness','laneId','nativeId',
781
+ 'sourceRoleKey','intakeKey','intakeNativeId','nativeConnectionGeneration','context','source','cli','admissionReferences','deadline'}
782
+ NATIVE_CLI_RECEIPT_FIELDS={'format','schemaVersion','operationId','nonce','transport','harness','laneId','nativeId',
783
+ 'nativeConnectionGeneration','context','source','cli','responses','claim'}
784
+
785
+
786
+ def native_cli_closed(value,fields,label):
787
+ if not isinstance(value,dict) or set(value)!=fields:
788
+ raise PackError('Closed native-cli '+label+' required')
789
+
790
+
791
+ def native_cli_hash(value):
792
+ if not isinstance(value,str) or not re.fullmatch('[a-f0-9]{64}',value):
793
+ raise PackError('Exact native-cli source digest required')
794
+
795
+
796
+ def native_cli_configuration(value):
797
+ native_cli_closed(value,{'cli','admissionReferences'},'configuration')
798
+ cli=value['cli'];refs=value['admissionReferences']
799
+ native_cli_closed(cli,NATIVE_CLI_CLI_FIELDS,'CLI identity')
800
+ native_cli_closed(refs,NATIVE_CLI_AUTHORITY_FIELDS,'authority references')
801
+ for path in [cli['nodeExecutable'],cli['entry'],refs['startupEvidencePath'],refs['profileAuthorityPath']]:
802
+ if not isinstance(path,str) or len(path)>4096 or not pathlib.Path(path).is_absolute() or any(ord(c)<32 for c in path):
803
+ raise PackError('Exact absolute native-cli path reference required')
804
+ for value in [cli['nodeDigest'],cli['entryDigest'],refs['startupEvidenceDigest'],refs['profileAuthorityDigest']]:
805
+ native_cli_hash(value)
806
+ if not isinstance(cli['version'],str) or not re.fullmatch(r'0\.13\.\d+',cli['version']):
807
+ raise PackError('Immutable native-cli version required')
808
+ if type(refs['expiresAt']) is not int or refs['expiresAt']<=time.time():
809
+ raise PackError('Current native-cli authority reference required; reference alone is not authority')
810
+
811
+
812
+ def native_cli_input(value):
813
+ native_cli_closed(value,NATIVE_CLI_INPUT_FIELDS,'input')
814
+ if (value['format']!='topolo-native-cli-continuity-input' or type(value['schemaVersion']) is not int or value['schemaVersion']!=1 or
815
+ value['transport']!='native-cli' or value['harness'] not in {'codex','claude'}):
816
+ raise PackError('Explicit versioned native-cli input required')
817
+ try:
818
+ if str(uuid.UUID(value['operationId']))!=value['operationId']:
819
+ raise ValueError()
820
+ except (ValueError,TypeError,AttributeError) as error:
821
+ raise PackError('Exact native-cli operation UUID required') from error
822
+ native_cli_hash(value['nonce']);safe_id(value['laneId'])
823
+ for key in ['nativeId','sourceRoleKey','intakeKey','intakeNativeId','nativeConnectionGeneration']:
824
+ _identity(value[key],key)
825
+ if '/intakes/' not in value['intakeKey'] or value['nativeId']==value['intakeNativeId']:
826
+ raise PackError('Exact independent child and original intake required')
827
+ context=value['context'];source=value['source']
828
+ native_cli_closed(context,NATIVE_CLI_CONTEXT_FIELDS,'public context');native_cli_closed(source,NATIVE_CLI_SOURCE_FIELDS,'source')
829
+ for entry in context.values():
830
+ _identity(entry,'public context')
831
+ if (context['environment'] not in {'production','staging','development'} or context['bytesScope'] not in {'private','org'} or
832
+ context['contextKey']!='org:'+context['organizationId']):
833
+ raise PackError('Exact native-cli environment, organization and Bytes scope required')
834
+ _identity(source['packRevision'],'pack revision')
835
+ for key in NATIVE_CLI_SOURCE_FIELDS-{'packRevision'}:
836
+ native_cli_hash(source[key])
837
+ native_cli_configuration({'cli':value['cli'],'admissionReferences':value['admissionReferences']})
838
+ if type(value['deadline']) is not int or value['deadline']<=time.time() or value['deadline']>value['admissionReferences']['expiresAt']:
839
+ raise PackError('Current bounded native-cli operation deadline required')
840
+ return copy.deepcopy(value)
841
+
842
+
843
+ def native_cli_admission(_value):
844
+ # No supported current startup/refresh authority producer is installed.
845
+ # Never turn hashes, a caller boolean or JSON evidence references into one.
846
+ raise PackError('native_cli_startup_unqualified: current startup, profile refresh authority and native result contracts are unavailable')
847
+
848
+
849
+ def native_cli_receipt(expected,receipt):
850
+ expected=native_cli_input(expected);native_cli_closed(receipt,NATIVE_CLI_RECEIPT_FIELDS,'receipt')
851
+ if (receipt['format']!='topolo-native-cli-continuity-receipt' or type(receipt['schemaVersion']) is not int or receipt['schemaVersion']!=1 or
852
+ receipt['claim']!='PUBLIC_IDENTITY_CONTEXT_RESPONSE_ONLY_NATIVE_ATTRIBUTION_REQUIRED'):
853
+ raise PackError('Native-cli public receipt format differs')
854
+ for key in NATIVE_CLI_RECEIPT_FIELDS-{'format','schemaVersion','responses','claim'}:
855
+ if receipt[key]!=expected[key]:
856
+ raise PackError('Native-cli receipt identity/source/operation differs')
857
+ native_cli_closed(receipt['responses'],{'identity','context'},'responses');seen=set()
858
+ for kind,response in receipt['responses'].items():
859
+ native_cli_closed(response,{'requestEvents','bodyProjectionDigest','actionId','resourceType','resourceId'},'response evidence')
860
+ native_cli_hash(response['bodyProjectionDigest']);events=response['requestEvents']
861
+ if not isinstance(events,list) or not 1<=len(events)<=8:
862
+ raise PackError('Actual bounded native-cli response request IDs required')
863
+ services=set()
864
+ for event in events:
865
+ native_cli_closed(event,{'requestId','method','service','path','status'},'request event')
866
+ identity=event['requestId'];path=event['path']
867
+ if (not isinstance(identity,str) or identity=='unavailable' or not re.fullmatch('[A-Za-z0-9][A-Za-z0-9._:-]{0,127}',identity) or
868
+ identity in seen or event['method'] not in {'GET','POST'} or event['service'] not in {'auth','agent'} or
869
+ type(event['status']) is not int or not 200<=event['status']<300 or not isinstance(path,str) or
870
+ not path.startswith('/') or len(path)>1024 or any(c in path for c in ['?','#','\\','\n','\r'])):
871
+ raise PackError('Actual canonical successful native-cli response evidence required')
872
+ seen.add(identity);services.add(event['service'])
873
+ wanted=(None,None,None) if kind=='identity' else ('app_topolo_agent.agent_packs.context','workspace',expected['context']['agentWorkspaceId'])
874
+ if (response['actionId'],response['resourceType'],response['resourceId'])!=wanted or ('auth' if kind=='identity' else 'agent') not in services:
875
+ raise PackError('Native-cli response action/resource attribution differs')
876
+ return copy.deepcopy(receipt)
877
+
878
+
879
+ def native_cli_context_ack(record,input,receipt,proof):
880
+ receipt=native_cli_receipt(input,receipt)
881
+ if (record.get('schemaVersion')!=2 or record['plan'].get('continuityTransport')!='native-cli' or
882
+ record['nativeId']!=input['nativeId'] or record['nativeConnectionGeneration']!=input['nativeConnectionGeneration'] or
883
+ record['state']!='loaded-context-ACK' or record['planDigest']!=input['source']['planDigest']):
884
+ raise PackError('Exact configured native-cli lane required before context ACK')
885
+ native_cli_closed(proof,{'harness','nativeId','generation','turnId','itemId','commandDigest','exitCode','receiptDigest'},'native tool attribution')
886
+ if (proof['harness']!=input['harness'] or proof['nativeId']!=input['nativeId'] or proof['generation']!=input['nativeConnectionGeneration'] or
887
+ proof['exitCode']!=0 or type(proof['exitCode'])is not int or proof['receiptDigest']!=digest(receipt)):
888
+ raise PackError('Actual same-query successful native tool attribution differs')
889
+ for key in ['turnId','itemId']:
890
+ _identity(proof[key],key)
891
+ native_cli_hash(proof['commandDigest'])
892
+ return {**record,'nativeCliContext':{'state':'native-cli-context-ACK','operationId':input['operationId'],
893
+ 'nonce':input['nonce'],'receiptDigest':digest(receipt),'proof':copy.deepcopy(proof)}}
894
+
895
+
896
+ def native_cli_record_version(record):
897
+ planned=record.get('plan',{})
898
+ if record.get('schemaVersion')==1:
899
+ return planned.get('schemaVersion')==1 and 'continuityTransport' not in planned
900
+ return (planned.get('schemaVersion')==2 and planned.get('continuityTransport')=='native-cli' and
901
+ planned.get('nativeParentId') is None and isinstance(planned.get('continuity'),dict) and
902
+ set(planned['continuity'])=={'cli','admissionReferences'})