@topolo/cli 0.13.10 → 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 +12 -0
- package/agent-packs/README.md +30 -0
- package/agent-packs/src/topolo_agent_pack/cli.py +5 -1
- package/agent-packs/src/topolo_agent_pack/lanes.py +158 -5
- package/agent-packs/src/topolo_agent_pack/live.py +70 -2
- package/agent-packs/src/topolo_agent_pack/topolo.py +130 -5
- package/dist/{claude-pack-backend-RMSOPHVQ.js → claude-pack-backend-YEVPAOID.js} +76 -31
- package/dist/index.js +7 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -436,3 +436,15 @@ 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.
|
package/agent-packs/README.md
CHANGED
|
@@ -669,3 +669,33 @@ 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.
|
|
@@ -344,6 +344,7 @@ def parser():
|
|
|
344
344
|
q.add_argument('--ownership',choices=['user','organization'],required=True);q.add_argument('--bytes-scope',choices=['private','org'],required=True);q.add_argument('--instance')
|
|
345
345
|
q.add_argument('--reconcile-local-context',action='store_true',help='Explicitly reconcile a recorded partial context attachment; never replays cloud writes')
|
|
346
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')
|
|
347
348
|
if command=='remember':
|
|
348
349
|
q.add_argument('--content',required=True);q.add_argument('--kind',choices=['instruction','knowledge','durable'],required=True)
|
|
349
350
|
q.add_argument('--provenance',required=True);q.add_argument('--visibility',choices=['private','shared'],default='private')
|
|
@@ -385,7 +386,10 @@ def main(argv=None):
|
|
|
385
386
|
lib=Library(args.store)
|
|
386
387
|
if args.command in {'connect','remote-context','remember','artifact','artifact-get','remote-recover'}:
|
|
387
388
|
from . import topolo
|
|
388
|
-
|
|
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)
|
|
389
393
|
emit(result)
|
|
390
394
|
return 0
|
|
391
395
|
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
|
-
|
|
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':
|
|
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')
|
|
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')
|
|
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'})
|
|
@@ -606,6 +606,12 @@ class BackendProxy:
|
|
|
606
606
|
|
|
607
607
|
async def qualify_organization(self,thread_id):
|
|
608
608
|
role=self.execution.roles.get(thread_id) if self.execution else None
|
|
609
|
+
record=None
|
|
610
|
+
if role and role.get('dynamicLaneId'):
|
|
611
|
+
from . import lanes
|
|
612
|
+
record=lanes.Registry(self.lib.root,'codex').read(role['dynamicLaneId'])
|
|
613
|
+
if record['plan'].get('continuityTransport')=='native-cli':
|
|
614
|
+
lanes.native_cli_admission(record['plan']['continuity'])
|
|
609
615
|
if role and not self.permission_configuration and (pathlib.Path(self.runtime)/'permissions-operation.json').exists():
|
|
610
616
|
raise PackError('Unresolved native permission operation requires reconciliation before owned execution or grants')
|
|
611
617
|
if not role or not role.get('installationConsent'):
|
|
@@ -635,8 +641,6 @@ class BackendProxy:
|
|
|
635
641
|
raise PackError('Fresh organization policy prohibits the child-only manual verification profile')
|
|
636
642
|
role['organizationPolicyProof']=proof
|
|
637
643
|
if role.get('dynamicLaneId'):
|
|
638
|
-
from .lanes import Registry
|
|
639
|
-
record=Registry(self.lib.root,'codex').read(role['dynamicLaneId'])
|
|
640
644
|
current=await self.dynamic_current(record['plan'])
|
|
641
645
|
configuring=self._lane_configuration_record
|
|
642
646
|
if configuring and configuring['nativeId']==thread_id and self.lane_busy:
|
|
@@ -1010,6 +1014,8 @@ class BackendProxy:
|
|
|
1010
1014
|
self.lane_idle();store=lanes.Registry(self.lib.root,'codex');record=store.read(params['laneId'])
|
|
1011
1015
|
if not record or record['state']!='loaded-context-ACK':
|
|
1012
1016
|
raise PackError('Actual native settings and instruction-source ACK required before bootstrap')
|
|
1017
|
+
if record['plan'].get('continuityTransport')=='native-cli':
|
|
1018
|
+
lanes.native_cli_admission(record['plan']['continuity'])
|
|
1013
1019
|
current=await self.dynamic_current(record['plan'])
|
|
1014
1020
|
if current!={k:record['plan'][k] for k in ['role','intake','ceiling']}:
|
|
1015
1021
|
raise PackError('Current bootstrap source or policy differs')
|
|
@@ -1086,6 +1092,8 @@ class BackendProxy:
|
|
|
1086
1092
|
self.lane_idle();store=lanes.Registry(self.lib.root,'codex');record=store.read(params['laneId'])
|
|
1087
1093
|
if not record or record['plan'].get('verificationProfile')!='manual':
|
|
1088
1094
|
raise PackError('Actual routing proof requires the bound two-child manual verification plan')
|
|
1095
|
+
if record['plan'].get('continuityTransport')=='native-cli':
|
|
1096
|
+
lanes.native_cli_admission(record['plan']['continuity'])
|
|
1089
1097
|
current=await self.dynamic_current(record['plan'])
|
|
1090
1098
|
if current!={k:record['plan'][k] for k in ['role','intake','ceiling']}:
|
|
1091
1099
|
raise PackError('Current source/intake/policy changed before own fixture verification')
|
|
@@ -1149,6 +1157,8 @@ class BackendProxy:
|
|
|
1149
1157
|
self.lane_idle();store=lanes.Registry(self.lib.root,'codex');planned=store.read_plan(params['laneId'])
|
|
1150
1158
|
if store.read(params['laneId']) is not None:
|
|
1151
1159
|
raise PackError('This lane already has an actual native identity')
|
|
1160
|
+
if planned.get('continuityTransport')=='native-cli':
|
|
1161
|
+
lanes.native_cli_admission(planned['continuity'])
|
|
1152
1162
|
current=await self.dynamic_current(planned)
|
|
1153
1163
|
if current!={k:planned[k] for k in ['role','intake','ceiling']}:
|
|
1154
1164
|
raise PackError('Source, intake or policy changed before native creation')
|
|
@@ -1202,6 +1212,9 @@ class BackendProxy:
|
|
|
1202
1212
|
|
|
1203
1213
|
async def dynamic_lane(self, operation, params):
|
|
1204
1214
|
from . import lanes,task_models,policy
|
|
1215
|
+
requested=params.get('request',params.get('plan',{})) if isinstance(params,dict) else {}
|
|
1216
|
+
if isinstance(requested,dict) and (requested.get('continuityTransport')=='native-cli' or requested.get('schemaVersion')==2 or 'continuity' in requested):
|
|
1217
|
+
lanes.native_cli_admission(requested.get('continuity'))
|
|
1205
1218
|
self.lane_idle()
|
|
1206
1219
|
if self.lane_busy:
|
|
1207
1220
|
raise PackError('An owned dynamic lane operation is already active')
|
|
@@ -1254,6 +1267,8 @@ class BackendProxy:
|
|
|
1254
1267
|
record=store.read(params['laneId'])
|
|
1255
1268
|
if not record:
|
|
1256
1269
|
raise PackError('No owned dynamic lane registration')
|
|
1270
|
+
if record['plan'].get('continuityTransport')=='native-cli':
|
|
1271
|
+
lanes.native_cli_admission(record['plan']['continuity'])
|
|
1257
1272
|
current=await self.dynamic_current(record['plan'])
|
|
1258
1273
|
if current!={k:record['plan'][k] for k in ['role','intake','ceiling']}:
|
|
1259
1274
|
raise PackError('Dynamic source, intake or policy drift; no native mutation')
|
|
@@ -1973,3 +1988,56 @@ def bind(lib,runtime):
|
|
|
1973
1988
|
if backup.exists():raise PackError('Cannot preserve existing desktop-home file: '+name)
|
|
1974
1989
|
dest.rename(backup)
|
|
1975
1990
|
dest.symlink_to(expected)
|
|
1991
|
+
|
|
1992
|
+
|
|
1993
|
+
def compile_native_cli_command(value,store,pack,input_path,environment):
|
|
1994
|
+
"""Compile a fixed native tool command; no shell/model text is accepted."""
|
|
1995
|
+
import shlex
|
|
1996
|
+
from . import lanes
|
|
1997
|
+
value=lanes.native_cli_input(value)
|
|
1998
|
+
allowed={'HOME','USER','LOGNAME','PATH','LANG','LC_ALL','TMPDIR','TOPOLO_CONFIG_PATH','TOPOLO_ENV','TOPOLO_CONTEXT'}
|
|
1999
|
+
if (not isinstance(environment,dict) or not set(environment)<=allowed or
|
|
2000
|
+
any(not isinstance(v,str) or len(v)>4096 or any(ord(c)<32 for c in v) for v in environment.values()) or
|
|
2001
|
+
not environment.get('TOPOLO_CONFIG_PATH') or not pathlib.Path(environment['TOPOLO_CONFIG_PATH']).is_absolute() or
|
|
2002
|
+
environment.get('TOPOLO_ENV')!=value['context']['environment'] or environment.get('TOPOLO_CONTEXT')!=value['context']['contextKey']):
|
|
2003
|
+
raise PackError('Exact child-only native-cli environment required; inherited credentials are forbidden')
|
|
2004
|
+
if any(not pathlib.Path(p).is_absolute() for p in [store,input_path]) or pack!=value['sourceRoleKey'].split('/')[0]:
|
|
2005
|
+
raise PackError('Exact owned store, pack and input references required')
|
|
2006
|
+
argv=['/usr/bin/env','-i',*[k+'='+environment[k] for k in sorted(environment)],value['cli']['nodeExecutable'],value['cli']['entry'],
|
|
2007
|
+
'agent-packs','--store',str(store),'remote-context',pack,'--environment',value['context']['environment'],
|
|
2008
|
+
'--context-key',value['context']['contextKey'],'--role',value['context']['logicalId'],'--continuity-input',str(input_path)]
|
|
2009
|
+
return shlex.join(argv)
|
|
2010
|
+
|
|
2011
|
+
|
|
2012
|
+
def native_cli_codex_tool_proof(value,command,result,turn_id,cwd,guide_command,guide_digest):
|
|
2013
|
+
"""Verify actual owned Codex tool DTOs; printed/foreign receipts do not ACK."""
|
|
2014
|
+
import shlex
|
|
2015
|
+
from . import lanes
|
|
2016
|
+
value=lanes.native_cli_input(value);lanes.native_cli_hash(guide_digest)
|
|
2017
|
+
if guide_digest!=value['source']['charterDigest']:
|
|
2018
|
+
raise PackError('Native-cli actual charter read differs from the pinned receipt source')
|
|
2019
|
+
thread=result.get('thread',{});turns=[t for t in thread.get('turns',[]) if t.get('id')==turn_id]
|
|
2020
|
+
if (thread.get('id')!=value['nativeId'] or thread.get('cwd')!=str(cwd) or len(turns)!=1 or
|
|
2021
|
+
turns[0].get('status')!='completed' or turns[0].get('error')):
|
|
2022
|
+
raise PackError('Exact same-child native-cli turn/cwd completion required')
|
|
2023
|
+
receipts=[];reads=[]
|
|
2024
|
+
for item in turns[0].get('items',[]):
|
|
2025
|
+
if item.get('type')!='commandExecution' or item.get('status')!='completed' or type(item.get('exitCode'))is not int or item['exitCode']!=0 or not item.get('id'):
|
|
2026
|
+
continue
|
|
2027
|
+
actual=item.get('command');args=shlex.split(actual) if isinstance(actual,str) else []
|
|
2028
|
+
unwrapped=args[2] if len(args)==3 and args[0] in {'/bin/sh','/bin/bash','/bin/zsh'} and args[1]=='-c' else actual
|
|
2029
|
+
if unwrapped==guide_command and hashlib.sha256(item.get('aggregatedOutput','').encode()).hexdigest()==guide_digest:
|
|
2030
|
+
reads.append(item['id'])
|
|
2031
|
+
if unwrapped!=command:
|
|
2032
|
+
continue
|
|
2033
|
+
try:
|
|
2034
|
+
receipt=json.loads(item.get('aggregatedOutput',''))
|
|
2035
|
+
except (ValueError,TypeError):
|
|
2036
|
+
continue
|
|
2037
|
+
receipt=lanes.native_cli_receipt(value,receipt);receipts.append((item['id'],receipt))
|
|
2038
|
+
if len(receipts)!=1 or len(reads)!=1 or receipts[0][0]==reads[0]:
|
|
2039
|
+
raise PackError('Actual native-cli command/receipt and own charter read require unique successful tool results')
|
|
2040
|
+
identity,receipt=receipts[0]
|
|
2041
|
+
proof={'harness':'codex','nativeId':value['nativeId'],'generation':value['nativeConnectionGeneration'],'turnId':turn_id,
|
|
2042
|
+
'itemId':identity,'commandDigest':lanes.digest(command),'exitCode':0,'receiptDigest':lanes.digest(receipt)}
|
|
2043
|
+
return {'receipt':receipt,'proof':proof,'nativeCharterReadItemId':reads[0]}
|
|
@@ -6,21 +6,23 @@ class ConnectionSourceRefused(PackError):
|
|
|
6
6
|
pass
|
|
7
7
|
|
|
8
8
|
class Client:
|
|
9
|
-
def __init__(self,environment,context,binary=None):
|
|
9
|
+
def __init__(self,environment,context,binary=None,env=None,node=None):
|
|
10
10
|
if environment not in {'development','staging','production'}:
|
|
11
11
|
raise PackError('Explicit Topolo environment required')
|
|
12
12
|
if not context or any(c in context for c in '\n\r'):
|
|
13
13
|
raise PackError('Explicit Topolo context required')
|
|
14
|
-
|
|
14
|
+
selected=os.environ if env is None else env
|
|
15
|
+
if any(selected.get(k) for k in ['TOPOLO_API_KEY','TOPOLO_ACCESS_TOKEN','TOPOLO_'+environment.upper()+'_API_KEY','TOPOLO_'+environment.upper()+'_ACCESS_TOKEN']):
|
|
15
16
|
raise PackError('Pinned connections require the CLI protected account context, not injected credentials')
|
|
16
17
|
self.environment=environment;self.context=context
|
|
17
18
|
self.binary=binary or shutil.which('topolo')
|
|
18
19
|
if not self.binary:
|
|
19
20
|
raise PackError('Install the public @topolo/cli and authenticate before connecting')
|
|
20
|
-
self.env={**
|
|
21
|
-
self.
|
|
21
|
+
self.env={**selected,'TOPOLO_CONTEXT':context}
|
|
22
|
+
self.command=[node,self.binary] if node else [self.binary]
|
|
23
|
+
self.env['PATH']=(str(pathlib.Path(node).parent)+os.pathsep+'/usr/bin:/bin') if node else str(pathlib.Path(self.binary).parent)+os.pathsep+self.env.get('PATH','')
|
|
22
24
|
def run(self,args):
|
|
23
|
-
p=subprocess.run([self.
|
|
25
|
+
p=subprocess.run([*self.command,'--env',self.environment,*args,'--json'],env=self.env,capture_output=True,text=True,timeout=90)
|
|
24
26
|
if p.returncode:
|
|
25
27
|
raise PackError('Topolo command failed (exit '+str(p.returncode)+'); inspect the published action, account permissions and environment. No fallback was used.')
|
|
26
28
|
try:value=json.loads(p.stdout)
|
|
@@ -599,6 +601,8 @@ def connection_lock(lib,args):
|
|
|
599
601
|
yield
|
|
600
602
|
|
|
601
603
|
def dispatch(lib,args):
|
|
604
|
+
if args.command=='remote-context' and getattr(args,'continuity_input',None):
|
|
605
|
+
return native_cli_continuity_read(lib,args)
|
|
602
606
|
client=Client(args.environment,args.context_key)
|
|
603
607
|
if args.command=='connect':return connect(lib,args,client)
|
|
604
608
|
if args.command=='remote-context':return remote_context(lib,args,client)[1]
|
|
@@ -610,3 +614,124 @@ def dispatch(lib,args):
|
|
|
610
614
|
reference=read_json(args.reference)
|
|
611
615
|
return read_artifact(client,binding,reference['key'],reference,args.destination)
|
|
612
616
|
raise PackError('Unsupported Topolo command')
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def native_cli_response(stdout,stderr,service):
|
|
621
|
+
"""Preserve only canonical public response metadata; never echo raw stderr."""
|
|
622
|
+
from . import lanes
|
|
623
|
+
if len(stdout.encode())>262144 or len(stderr.encode())>65536:
|
|
624
|
+
raise PackError('native_cli_request_metadata_unavailable: bounded public response required')
|
|
625
|
+
try:
|
|
626
|
+
body=json.loads(stdout)
|
|
627
|
+
except ValueError as error:
|
|
628
|
+
raise PackError('native_cli_request_metadata_unavailable: invalid public JSON') from error
|
|
629
|
+
if not isinstance(body,dict) or body.get('error'):
|
|
630
|
+
raise PackError('native_cli_request_metadata_unavailable: unsuccessful public response')
|
|
631
|
+
events=[];seen=set()
|
|
632
|
+
pattern=r'^\[topolo\] ← (\d{3}) (GET|POST) (auth|agent) (/[^\s?#\\]+) \([0-9.]+ms, req=([A-Za-z0-9][A-Za-z0-9._:-]{0,127})\)$'
|
|
633
|
+
for line in stderr.splitlines():
|
|
634
|
+
match=re.fullmatch(pattern,line)
|
|
635
|
+
if not match:
|
|
636
|
+
continue
|
|
637
|
+
status,method,target,path,request_id=match.groups()
|
|
638
|
+
if request_id=='unavailable' or request_id in seen or not 200<=int(status)<300:
|
|
639
|
+
raise PackError('native_cli_request_metadata_unavailable: ambiguous or unsuccessful response request IDs')
|
|
640
|
+
seen.add(request_id);events.append({'requestId':request_id,'method':method,'service':target,'path':path,'status':int(status)})
|
|
641
|
+
if not 1<=len(events)<=8 or service not in {event['service'] for event in events}:
|
|
642
|
+
raise PackError('native_cli_request_metadata_unavailable: actual canonical server response IDs required')
|
|
643
|
+
return body.get('data',body),events
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def native_cli_command_response(client,args,service):
|
|
647
|
+
env={**client.env,'TOPOLO_DEBUG':'1'}
|
|
648
|
+
result=subprocess.run([*client.command,'--env',client.environment,*args,'--json'],env=env,
|
|
649
|
+
capture_output=True,text=True,timeout=90)
|
|
650
|
+
if result.returncode:
|
|
651
|
+
raise PackError('native_cli_request_metadata_unavailable: public command refused; no fallback or retry')
|
|
652
|
+
return native_cli_response(result.stdout,result.stderr,service)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def native_cli_selected_environment(value,binding,host_env):
|
|
656
|
+
from . import lanes
|
|
657
|
+
from types import SimpleNamespace
|
|
658
|
+
context=value['context'];profile=binding.get('privateCredentialProfile')
|
|
659
|
+
lanes.native_cli_closed(profile,{'format','schemaVersion','configPath','environment','contextKey','identity'},'private profile reference')
|
|
660
|
+
if (profile['format']!='topolo-private-credential-profile' or profile['schemaVersion']!=1 or
|
|
661
|
+
profile['environment']!=context['environment'] or profile['contextKey']!=context['contextKey'] or
|
|
662
|
+
profile['identity']!={'userId':context['principalId'],'organizationId':context['organizationId']}):
|
|
663
|
+
raise PackError('Exact native-cli approved profile/principal/context reference required')
|
|
664
|
+
selected=SimpleNamespace(env={'TOPOLO_CONFIG_PATH':profile['configPath']},environment=context['environment'],context=context['contextKey'])
|
|
665
|
+
args=SimpleNamespace(private_profile=True,environment=context['environment'],context_key=context['contextKey'])
|
|
666
|
+
private_profile_reference(selected,args)
|
|
667
|
+
for path,sha in [(value['cli']['nodeExecutable'],value['cli']['nodeDigest']),(value['cli']['entry'],value['cli']['entryDigest'])]:
|
|
668
|
+
p=pathlib.Path(path);info=p.lstat()
|
|
669
|
+
if (p.resolve()!=p or not stat.S_ISREG(info.st_mode) or info.st_uid not in {0,os.getuid()} or info.st_mode&0o022 or
|
|
670
|
+
not info.st_mode&0o111 or hashlib.sha256(p.read_bytes()).hexdigest()!=sha):
|
|
671
|
+
raise PackError('Native-cli executable source/provenance changed')
|
|
672
|
+
allowed={'HOME','USER','LOGNAME','PATH','LANG','LC_ALL','TMPDIR'}
|
|
673
|
+
env={k:v for k,v in host_env.items() if k in allowed and isinstance(v,str)}
|
|
674
|
+
env['PATH']=str(pathlib.Path(value['cli']['nodeExecutable']).parent)+os.pathsep+'/usr/bin:/bin'
|
|
675
|
+
env.update(TOPOLO_CONFIG_PATH=profile['configPath'],TOPOLO_ENV=context['environment'],TOPOLO_CONTEXT=context['contextKey'])
|
|
676
|
+
return env
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def native_cli_continuity_read(lib,args,*,_admit=None,_client_factory=None,_response=None):
|
|
680
|
+
"""Native command producer. No current runtime authority producer exists.
|
|
681
|
+
|
|
682
|
+
Injected callbacks are internal hermetic seams, never CLI/JSON options.
|
|
683
|
+
Receipt output is public response evidence, not native-query attribution.
|
|
684
|
+
"""
|
|
685
|
+
from . import lanes
|
|
686
|
+
path=pathlib.Path(args.continuity_input);info=path.lstat()
|
|
687
|
+
if (path.is_symlink() or not stat.S_ISREG(info.st_mode) or info.st_uid!=os.getuid() or info.st_nlink!=1 or
|
|
688
|
+
info.st_mode&0o077 or info.st_size>65536):
|
|
689
|
+
raise PackError('Exact owned bounded native-cli input required')
|
|
690
|
+
value=lanes.native_cli_input(read_json(path))
|
|
691
|
+
(lanes.native_cli_admission if _admit is None else _admit)(value)
|
|
692
|
+
context=value['context']
|
|
693
|
+
if (args.environment!=context['environment'] or args.context_key!=context['contextKey'] or args.role!=context['logicalId'] or
|
|
694
|
+
value['sourceRoleKey']!=args.name+'/'+args.role):
|
|
695
|
+
raise PackError('Native-cli command differs from the exact source/operation')
|
|
696
|
+
file=binding_path(lib,args.name,args.environment,args.context_key);info=file.lstat()
|
|
697
|
+
if (file.is_symlink() or not stat.S_ISREG(info.st_mode) or info.st_uid!=os.getuid() or info.st_nlink!=1 or
|
|
698
|
+
hashlib.sha256(file.read_bytes()).hexdigest()!=value['source']['bindingDigest']):
|
|
699
|
+
raise PackError('Native-cli current owned binding source differs')
|
|
700
|
+
binding=read_json(file);roles=[r for r in binding['input']['roles'] if r['logicalId']==args.role]
|
|
701
|
+
if (len(roles)!=1 or binding['identity']!={'userId':context['principalId'],'organizationId':context['organizationId']} or
|
|
702
|
+
binding['packRevision']!=value['source']['packRevision'] or binding['input']['instanceId']!=context['installationId'] or
|
|
703
|
+
binding['agentWorkspaceId']!=context['agentWorkspaceId'] or binding['input']['bytesWorkspaceId']!=context['bytesWorkspaceId'] or
|
|
704
|
+
binding['input']['bytesScope']!=context['bytesScope'] or roles[0]['agentId']!=context['agentId'] or roles[0]['agentVersionId']!=context['agentVersionId']):
|
|
705
|
+
raise PackError('Native-cli source principal/Agent/Bytes binding differs')
|
|
706
|
+
env=native_cli_selected_environment(value,binding,os.environ)
|
|
707
|
+
client=(Client if _client_factory is None else _client_factory)(args.environment,args.context_key,
|
|
708
|
+
binary=value['cli']['entry'],env=env,node=value['cli']['nodeExecutable'])
|
|
709
|
+
call=native_cli_command_response if _response is None else _response
|
|
710
|
+
identity,identity_events=call(client,['whoami'],'auth')
|
|
711
|
+
if (identity.get('user',{}).get('id')!=context['principalId'] or (identity.get('organization')or{}).get('id')!=context['organizationId']):
|
|
712
|
+
raise PackError('Actual native-cli public identity differs')
|
|
713
|
+
with tempfile.TemporaryDirectory(prefix='topolo-native-cli-context-') as directory:
|
|
714
|
+
input_file=pathlib.Path(directory)/'input.json';atomic_json(input_file,{'instanceId':context['installationId'],'logicalId':context['logicalId']})
|
|
715
|
+
envelope,context_events=call(client,['actions','call','app_topolo_agent.agent_packs.context','--detail','--data','@'+str(input_file),
|
|
716
|
+
'--resource-type','workspace','--resource-id',context['agentWorkspaceId']],'agent')
|
|
717
|
+
result=envelope.get('data',envelope)
|
|
718
|
+
actual=result.get('context',{})
|
|
719
|
+
expected={'organizationId':context['organizationId'],'workspaceId':context['agentWorkspaceId'],'instanceId':context['installationId'],
|
|
720
|
+
'logicalId':context['logicalId'],'agentId':context['agentId'],'agentVersionId':context['agentVersionId'],
|
|
721
|
+
'artifacts':{'appId':'app_topolo_bytes','resourceType':'workspace','resourceId':context['bytesWorkspaceId'],'scope':context['bytesScope']}}
|
|
722
|
+
if envelope.get('success') is False or any(actual.get(k)!=v for k,v in expected.items()):
|
|
723
|
+
raise PackError('Actual native-cli public Agent/Bytes context differs')
|
|
724
|
+
# Recheck mutable public binding, binary and stat-only profile after awaited reads.
|
|
725
|
+
if hashlib.sha256(file.read_bytes()).hexdigest()!=value['source']['bindingDigest']:
|
|
726
|
+
raise PackError('Native-cli binding changed during response; no receipt ACK')
|
|
727
|
+
native_cli_selected_environment(value,binding,os.environ)
|
|
728
|
+
(lanes.native_cli_admission if _admit is None else _admit)(value)
|
|
729
|
+
responses={}
|
|
730
|
+
for kind,body,events in [('identity',{'userId':context['principalId'],'organizationId':context['organizationId']},identity_events),
|
|
731
|
+
('context',expected,context_events)]:
|
|
732
|
+
responses[kind]={'requestEvents':events,'bodyProjectionDigest':lanes.digest(body),'actionId':None if kind=='identity' else 'app_topolo_agent.agent_packs.context',
|
|
733
|
+
'resourceType':None if kind=='identity' else 'workspace','resourceId':None if kind=='identity' else context['agentWorkspaceId']}
|
|
734
|
+
receipt={k:value[k] for k in lanes.NATIVE_CLI_RECEIPT_FIELDS-{'format','schemaVersion','responses','claim'}}
|
|
735
|
+
receipt.update(format='topolo-native-cli-continuity-receipt',schemaVersion=1,responses=responses,
|
|
736
|
+
claim='PUBLIC_IDENTITY_CONTEXT_RESPONSE_ONLY_NATIVE_ATTRIBUTION_REQUIRED')
|
|
737
|
+
return lanes.native_cli_receipt(value,receipt)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/lib/claude-pack-backend.ts
|
|
4
|
-
import { createHash, randomBytes, randomUUID } from "crypto";
|
|
4
|
+
import { createHash as createHash2, randomBytes, randomUUID } from "crypto";
|
|
5
5
|
import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
6
6
|
import * as filesystem from "fs";
|
|
7
7
|
import * as cryptography from "crypto";
|
|
@@ -13,6 +13,7 @@ import { createConnection } from "net";
|
|
|
13
13
|
import { WebSocket } from "undici";
|
|
14
14
|
|
|
15
15
|
// src/lib/claude-native-pack.ts
|
|
16
|
+
import { createHash } from "crypto";
|
|
16
17
|
async function nativeClaudePackOperation(manager, sidebar, plan, apply, checkpoint, verifyContext = () => {
|
|
17
18
|
}) {
|
|
18
19
|
const fenceKey = /* @__PURE__ */ Symbol.for("topolo.agent-pack.native-operation.v1");
|
|
@@ -849,7 +850,7 @@ async function nativeClaudeApprovalOperation(manager, routes, operation, fingerp
|
|
|
849
850
|
const session = await manager.getSession(route.sessionId);
|
|
850
851
|
if (!session || session.sessionId !== route.sessionId || session.cwd !== route.cwd || session.title !== route.title || session.isArchived)
|
|
851
852
|
throw new Error("Native approval session identity differs: " + route.key);
|
|
852
|
-
if (!descriptor || descriptor.sessionId !== route.sessionId || descriptor.cwd !== route.cwd || descriptor.title !== route.title || descriptor.isArchived || descriptor.backend?.kind !== "local" || descriptor.backend.remoteTarget || descriptor.worktreePath || descriptor.worktreeLazy || (route.dynamic ? descriptor.spawnedFrom?.sessionId !== route.dynamic.nativeParentId || descriptor.spawnedFrom?.sideSession !== true || route.dynamic.nativeParentId !== route.intakeSessionId || !/^[a-f0-9]{64}$/.test(route.dynamic.planDigest) : descriptor.spawnedFrom))
|
|
853
|
+
if (!descriptor || descriptor.sessionId !== route.sessionId || descriptor.cwd !== route.cwd || descriptor.title !== route.title || descriptor.isArchived || descriptor.backend?.kind !== "local" || descriptor.backend.remoteTarget || descriptor.worktreePath || descriptor.worktreeLazy || (route.dynamic ? (route.dynamic.continuityTransport === "native-cli" ? Boolean(descriptor.spawnedFrom) || route.dynamic.nativeParentId !== null : descriptor.spawnedFrom?.sessionId !== route.dynamic.nativeParentId || descriptor.spawnedFrom?.sideSession !== true || route.dynamic.nativeParentId !== route.intakeSessionId) || !/^[a-f0-9]{64}$/.test(route.dynamic.planDigest) : descriptor.spawnedFrom))
|
|
853
854
|
throw new Error("Native approval session descriptor identity differs: " + route.key);
|
|
854
855
|
return session;
|
|
855
856
|
};
|
|
@@ -993,11 +994,30 @@ async function nativeClaudeApprovalOperation(manager, routes, operation, fingerp
|
|
|
993
994
|
if (operation.kind === "list") return { requests: [...state.entries.values()].map((e) => ({ ...e.record })) };
|
|
994
995
|
const entry = state.entries.get(operation.key);
|
|
995
996
|
if (!entry || entry.record.state !== "pending" || entry.record.intakeSessionId !== operation.intake || entry.record.fingerprint !== operation.fingerprint) throw new Error("Exact pending request and owning intake required.");
|
|
996
|
-
|
|
997
|
-
|
|
997
|
+
let cliFinalGuard;
|
|
998
|
+
if (operation.kind === "respond" && operation.decision === "once" && byId.get(entry.record.sourceSessionId)?.dynamic) {
|
|
999
|
+
const route = byId.get(entry.record.sourceSessionId);
|
|
1000
|
+
if (route.dynamic?.continuityTransport === "native-cli") {
|
|
1001
|
+
const producer = state.dynamicLifecycle?.get(entry.record.sourceSessionId)?.authorizeNativeCLI;
|
|
1002
|
+
if (typeof producer !== "function") throw Object.assign(new Error("native_cli_startup_unqualified: actual native CLI admission producer unavailable."), { code: "native_cli_startup_unqualified" });
|
|
1003
|
+
const admission = await producer(entry.record, entry.request, operation);
|
|
1004
|
+
if (!admission || !["bootstrap", "work"].includes(admission.phase) || admission.nativeId !== entry.record.sourceSessionId || admission.intakeNativeId !== entry.record.intakeSessionId || admission.fingerprint !== entry.record.fingerprint || admission.generation !== route.dynamic.generation || typeof route.dynamic.generation !== "string" || !route.dynamic.generation || !admission.query || typeof admission.verifyCurrent !== "function" || typeof admission.model !== "string" || !admission.model || typeof admission.effort !== "string" || !admission.effort)
|
|
1005
|
+
throw new Error("Exact current native CLI query/intake admission differs.");
|
|
1006
|
+
const raw = await manager.getSessionWhenLoaded(entry.record.sourceSessionId, "topolo-native-cli-permission", { warnOnMiss: false });
|
|
1007
|
+
if (raw?.query !== admission.query || typeof manager.readRunningModel !== "function" || typeof manager.getEffort !== "function" || await manager.readRunningModel(raw, raw.query) !== admission.model || await manager.getEffort(raw.sessionId) !== admission.effort)
|
|
1008
|
+
throw new Error("Actual same-query native model/effort acknowledgement differs.");
|
|
1009
|
+
cliFinalGuard = () => {
|
|
1010
|
+
if (state.dynamicLifecycle?.get(entry.record.sourceSessionId)?.authorizeNativeCLI !== producer || raw.query !== admission.query || admission.verifyCurrent() !== void 0) throw new Error("Current native CLI admission changed before dispatch.");
|
|
1011
|
+
};
|
|
1012
|
+
if (admission.phase === "bootstrap") {
|
|
1013
|
+
if (entry.request.toolName !== "Bash" || entry.request.input?.command !== admission.command || admission.commandDigest !== fingerprint(admission.command))
|
|
1014
|
+
throw new Error("Only the exact native CLI bootstrap command may precede context acknowledgement.");
|
|
1015
|
+
} else if (!/^[a-f0-9]{64}$/.test(admission.contextReceiptDigest ?? "")) throw new Error("Native CLI work requires verified same-query context receipt.");
|
|
1016
|
+
} else throw Object.assign(
|
|
998
1017
|
new Error("native_private_query_prestart_contract_unqualified: dynamic private query authorization remains closed."),
|
|
999
1018
|
{ code: "native_private_query_prestart_contract_unqualified" }
|
|
1000
1019
|
);
|
|
1020
|
+
}
|
|
1001
1021
|
const pending = await current(entry);
|
|
1002
1022
|
if (manager[symbol] !== state || state.entries.get(operation.key) !== entry || entry.record.state !== "pending" || state.auditFailed)
|
|
1003
1023
|
throw new Error("Exact pending request changed during native identity validation; reconcile without replay.");
|
|
@@ -1005,6 +1025,7 @@ async function nativeClaudeApprovalOperation(manager, routes, operation, fingerp
|
|
|
1005
1025
|
if (operation.kind !== "respond" || operation.humanDecisionRecorded !== true || !["once", "deny"].includes(operation.decision ?? ""))
|
|
1006
1026
|
throw new Error("Recorded human one-request decision required; never auto-approve.");
|
|
1007
1027
|
if (operation.decision !== "deny" && entry.record.cardOnly) throw new Error("Claude requires the original card; intake cannot impersonate its origin.");
|
|
1028
|
+
cliFinalGuard?.();
|
|
1008
1029
|
entry.record.state = "response-may-have-effects";
|
|
1009
1030
|
checkpoint({ ...entry.record });
|
|
1010
1031
|
await manager.respondToToolPermission(pending.requestId, operation.decision, void 0, "elsewhere");
|
|
@@ -1019,17 +1040,26 @@ async function nativeClaudeApprovalOperation(manager, routes, operation, fingerp
|
|
|
1019
1040
|
}
|
|
1020
1041
|
async function nativeClaudeAppendDynamicRoute(manager, route, verify, checkpoint, lifecycle) {
|
|
1021
1042
|
const state = manager[/* @__PURE__ */ Symbol.for("topolo.agent-pack.approvals.v1")];
|
|
1022
|
-
if (!state || state.auditFailed || !state.byId || !state.byKey || !route.dynamic || !/^[a-f0-9]{64}$/.test(route.dynamic.planDigest) || route.dynamic.nativeParentId !== route.intakeSessionId)
|
|
1043
|
+
if (!state || state.auditFailed || !state.byId || !state.byKey || !route.dynamic || !/^[a-f0-9]{64}$/.test(route.dynamic.planDigest) || (route.dynamic.continuityTransport === "native-cli" ? route.dynamic.nativeParentId !== null : route.dynamic.nativeParentId !== route.intakeSessionId))
|
|
1023
1044
|
throw new Error("Current qualified original approval listener required.");
|
|
1045
|
+
let cliAdmission;
|
|
1046
|
+
if (route.dynamic.continuityTransport === "native-cli") {
|
|
1047
|
+
if (typeof lifecycle?.authorizeNativeCLI !== "function") throw Object.assign(new Error("native_cli_startup_unqualified: actual native CLI admission producer unavailable."), { code: "native_cli_startup_unqualified" });
|
|
1048
|
+
cliAdmission = await lifecycle.authorizeNativeCLI(null, null, { kind: "route-append" });
|
|
1049
|
+
}
|
|
1024
1050
|
const intake = state.byId.get(route.intakeSessionId);
|
|
1025
1051
|
if (!intake || intake.key !== route.intakeKey || intake.contextKey !== route.contextKey || !intake.key.includes("/intakes/"))
|
|
1026
1052
|
throw new Error("Dynamic approval route has no exact same-context original intake.");
|
|
1027
1053
|
await verify();
|
|
1028
1054
|
const raw = await manager.getSessionWhenLoaded(route.sessionId, "topolo-dynamic-route", { warnOnMiss: false });
|
|
1029
1055
|
const dto = await manager.getSession(route.sessionId);
|
|
1030
|
-
if (!raw || !dto || raw.sessionId !== route.sessionId || raw.cwd !== route.cwd || raw.title !== route.title || dto.sessionId !== route.sessionId || dto.cwd !== route.cwd || dto.title !== route.title || raw.isArchived || dto.isArchived || raw.backend?.kind !== "local" || raw.backend.remoteTarget || raw.worktreePath || raw.worktreeLazy || raw.spawnedFrom?.sessionId !== route.intakeSessionId || raw.spawnedFrom?.sideSession !== true)
|
|
1056
|
+
if (!raw || !dto || raw.sessionId !== route.sessionId || raw.cwd !== route.cwd || raw.title !== route.title || dto.sessionId !== route.sessionId || dto.cwd !== route.cwd || dto.title !== route.title || raw.isArchived || dto.isArchived || raw.backend?.kind !== "local" || raw.backend.remoteTarget || raw.worktreePath || raw.worktreeLazy || (route.dynamic.continuityTransport === "native-cli" ? Boolean(raw.spawnedFrom) : raw.spawnedFrom?.sessionId !== route.intakeSessionId || raw.spawnedFrom?.sideSession !== true))
|
|
1031
1057
|
throw new Error("Actual dynamic child descriptor differs before route append.");
|
|
1058
|
+
if (route.dynamic.continuityTransport === "native-cli" && (!cliAdmission?.query || raw.query !== cliAdmission.query || cliAdmission.nativeId !== route.sessionId || cliAdmission.intakeNativeId !== route.intakeSessionId || cliAdmission.generation !== route.dynamic.generation || typeof route.dynamic.generation !== "string" || !route.dynamic.generation || typeof cliAdmission.verifyCurrent !== "function" || typeof cliAdmission.model !== "string" || !cliAdmission.model || typeof cliAdmission.effort !== "string" || !cliAdmission.effort || typeof manager.readRunningModel !== "function" || typeof manager.getEffort !== "function" || await manager.readRunningModel(raw, raw.query) !== cliAdmission.model || await manager.getEffort(raw.sessionId) !== cliAdmission.effort))
|
|
1059
|
+
throw new Error("Actual native CLI route query/model/effort admission differs.");
|
|
1032
1060
|
await verify();
|
|
1061
|
+
if (route.dynamic.continuityTransport === "native-cli" && (raw.query !== cliAdmission.query || cliAdmission.verifyCurrent() !== void 0))
|
|
1062
|
+
throw new Error("Current native CLI route admission changed before append.");
|
|
1033
1063
|
if (manager[/* @__PURE__ */ Symbol.for("topolo.agent-pack.approvals.v1")] !== state || state.auditFailed)
|
|
1034
1064
|
throw new Error("Original native permission connection changed.");
|
|
1035
1065
|
const old = state.byId.get(route.sessionId);
|
|
@@ -1188,8 +1218,8 @@ function prepareClaudeNativePlan(store, pack, policyPath, mode = "configure") {
|
|
|
1188
1218
|
captureMarkdown(cwd);
|
|
1189
1219
|
const charter = resolve(cwd, "CLAUDE.md");
|
|
1190
1220
|
const transcript = realpathSync(record.transcript);
|
|
1191
|
-
context[charter] =
|
|
1192
|
-
context[transcript] =
|
|
1221
|
+
context[charter] = createHash2("sha256").update(readFileSync(charter)).digest("hex");
|
|
1222
|
+
context[transcript] = createHash2("sha256").update(readFileSync(transcript)).digest("hex");
|
|
1193
1223
|
roleContextFiles[record.desktopSessionId] = [charter, transcript];
|
|
1194
1224
|
members.push({
|
|
1195
1225
|
key: record.key,
|
|
@@ -1207,7 +1237,7 @@ function prepareClaudeNativePlan(store, pack, policyPath, mode = "configure") {
|
|
|
1207
1237
|
});
|
|
1208
1238
|
}
|
|
1209
1239
|
for (const file of [resolve(root, "claude/state.json"), realpathSync(policyPath)])
|
|
1210
|
-
context[file] =
|
|
1240
|
+
context[file] = createHash2("sha256").update(readFileSync(file)).digest("hex");
|
|
1211
1241
|
const activePath = resolve(root, "claude/active.json");
|
|
1212
1242
|
const active = existsSync(activePath) ? JSON.parse(readFileSync(activePath, "utf8")) : null;
|
|
1213
1243
|
const target = new Set(members.map((m) => m.key));
|
|
@@ -1224,7 +1254,7 @@ function prepareClaudeNativePlan(store, pack, policyPath, mode = "configure") {
|
|
|
1224
1254
|
if (!cwd.startsWith(resolve(root, "claude/workspaces", record.pack) + sep)) throw new Error("Outgoing workspace is outside the owned pack.");
|
|
1225
1255
|
captureMarkdown(cwd);
|
|
1226
1256
|
for (const file of [resolve(cwd, "CLAUDE.md"), realpathSync(record.transcript)])
|
|
1227
|
-
context[file] =
|
|
1257
|
+
context[file] = createHash2("sha256").update(readFileSync(file)).digest("hex");
|
|
1228
1258
|
outgoing.push({ key, sessionId: record.desktopSessionId, cwd, title: record.title });
|
|
1229
1259
|
}
|
|
1230
1260
|
} else if (mode === "switch") throw new Error("Configure and verify an initial active roster before switching.");
|
|
@@ -1528,7 +1558,7 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
|
|
|
1528
1558
|
if (!journal.startsWith(realpathSync(directory) + sep)) throw new Error("Recovery journal is outside the owned library.");
|
|
1529
1559
|
const lines = readFileSync(journal, "utf8").split("\n").filter(Boolean);
|
|
1530
1560
|
const line = lines[0];
|
|
1531
|
-
if (
|
|
1561
|
+
if (createHash2("sha256").update(line).digest("hex") !== marker.planHash) throw new Error("Recovery plan identity changed.");
|
|
1532
1562
|
const record = JSON.parse(line);
|
|
1533
1563
|
if (record.pack !== options.pack || record.operationId !== marker.operationId || !record.apply) throw new Error("Recovery pack or operation identity differs.");
|
|
1534
1564
|
plan = { ...record, approvalRoutes: record.approvalRoutes ?? [], recovery: true, noEffects: marker.format === 2 && marker.dispatchState === "not-dispatched" && !lines.map((line2) => JSON.parse(line2)).some((r) => r.kind === "attempt") };
|
|
@@ -1542,7 +1572,7 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
|
|
|
1542
1572
|
throw new Error("Unowned native approval connection.");
|
|
1543
1573
|
const connection = JSON.parse(readFileSync(connectionPath, "utf8"));
|
|
1544
1574
|
if (connection.pid === options.pid && connection.pack === options.pack && JSON.stringify(connection.routes) === JSON.stringify(plan.approvalRoutes) && JSON.stringify(connection.installationConsent) === JSON.stringify(plan.installationConsent)) {
|
|
1545
|
-
if (typeof connection.secretPath !== "string" || typeof connection.socket !== "string" || !resolve(connection.secretPath).startsWith(realpathSync(resolve(root, "claude/private")) + sep) || realpathSync(connection.secretPath) !== resolve(connection.secretPath) || dirname(resolve(connection.socket)) !== resolve("/tmp", "topolo-claude-pack-" +
|
|
1575
|
+
if (typeof connection.secretPath !== "string" || typeof connection.socket !== "string" || !resolve(connection.secretPath).startsWith(realpathSync(resolve(root, "claude/private")) + sep) || realpathSync(connection.secretPath) !== resolve(connection.secretPath) || dirname(resolve(connection.socket)) !== resolve("/tmp", "topolo-claude-pack-" + createHash2("sha256").update(root).digest("hex").slice(0, 16)))
|
|
1546
1576
|
throw new Error("Native approval connection is outside owned control paths.");
|
|
1547
1577
|
const secret = lstatSync(connection.secretPath);
|
|
1548
1578
|
if (!secret.isFile() || secret.isSymbolicLink() || typeof process.getuid !== "function" || secret.uid !== process.getuid() || secret.mode & 63)
|
|
@@ -1553,7 +1583,7 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
|
|
|
1553
1583
|
if (!plan.control) {
|
|
1554
1584
|
const directory2 = resolve(root, "claude/private");
|
|
1555
1585
|
mkdirSync(directory2, { recursive: true, mode: 448 });
|
|
1556
|
-
const socketDirectory = resolve("/tmp", "topolo-claude-pack-" +
|
|
1586
|
+
const socketDirectory = resolve("/tmp", "topolo-claude-pack-" + createHash2("sha256").update(root).digest("hex").slice(0, 16));
|
|
1557
1587
|
mkdirSync(socketDirectory, { recursive: true, mode: 448 });
|
|
1558
1588
|
for (const path of [directory2, socketDirectory]) {
|
|
1559
1589
|
const stat = lstatSync(path);
|
|
@@ -1568,7 +1598,7 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
|
|
|
1568
1598
|
journal = resolve(directory, operationId + ".jsonl");
|
|
1569
1599
|
const record = { kind: "plan", pack: options.pack, ...plan, apply: options.apply };
|
|
1570
1600
|
durableRecord(journal, record, "wx");
|
|
1571
|
-
marker = { format: 2, dispatchState: "not-dispatched", operationId, journal, planHash:
|
|
1601
|
+
marker = { format: 2, dispatchState: "not-dispatched", operationId, journal, planHash: createHash2("sha256").update(JSON.stringify(record)).digest("hex") };
|
|
1572
1602
|
if (options.apply) durableRecord(markerPath, marker, "wx");
|
|
1573
1603
|
syncDirectory(directory);
|
|
1574
1604
|
syncDirectory(resolve(root, "claude"));
|
|
@@ -1837,8 +1867,9 @@ function prepareClaudeDynamicLane(store, pack, policyPath, laneId) {
|
|
|
1837
1867
|
if (lstatSync(file).size > 131072) throw new Error("Dynamic lane record exceeds its schema bound.");
|
|
1838
1868
|
const record = JSON.parse(readFileSync(file, "utf8"));
|
|
1839
1869
|
const canonical = (value) => JSON.stringify(value, (_key, item) => item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, item[key]])) : item);
|
|
1840
|
-
const digest = (value) =>
|
|
1841
|
-
|
|
1870
|
+
const digest = (value) => createHash2("sha256").update(canonical(value)).digest("hex");
|
|
1871
|
+
const nativeCLI = record.schemaVersion === 2 && record.plan?.schemaVersion === 2 && record.plan.continuityTransport === "native-cli";
|
|
1872
|
+
if (record.format !== "topolo-dynamic-lane" || !(record.schemaVersion === 1 || nativeCLI) || record.harness !== "claude" || record.laneId !== laneId || record.planDigest !== digest(record.plan) || record.creationReceiptDigest !== digest(record.creationReceipt) || typeof record.nativeTitle !== "string" || !record.nativeTitle || record.nativeTitle.length > 256 || record.plan.role.packId !== pack || (nativeCLI ? record.plan.nativeParentId !== null : record.plan.nativeParentId !== record.plan.intake.nativeId))
|
|
1842
1873
|
throw new Error("Durable dynamic child identity or plan digest differs.");
|
|
1843
1874
|
const stateFile = owned(resolve(root, "claude/state.json"));
|
|
1844
1875
|
const state = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
@@ -1847,8 +1878,8 @@ function prepareClaudeDynamicLane(store, pack, policyPath, laneId) {
|
|
|
1847
1878
|
if (state.harness !== "claude" || policy.format !== "topolo-agent-execution-policy" || policy.pack !== pack || policy.sourceRevision !== record.plan.role.packRevision || !policy.installationConsent)
|
|
1848
1879
|
throw new Error("Dynamic child source policy or organization-bound installation is stale.");
|
|
1849
1880
|
const context = {
|
|
1850
|
-
[stateFile]:
|
|
1851
|
-
[policyFile]:
|
|
1881
|
+
[stateFile]: createHash2("sha256").update(readFileSync(stateFile)).digest("hex"),
|
|
1882
|
+
[policyFile]: createHash2("sha256").update(readFileSync(policyFile)).digest("hex")
|
|
1852
1883
|
};
|
|
1853
1884
|
const inventories = {};
|
|
1854
1885
|
const fixed = (descriptor) => {
|
|
@@ -1861,7 +1892,7 @@ function prepareClaudeDynamicLane(store, pack, policyPath, laneId) {
|
|
|
1861
1892
|
throw new Error("Dynamic source workspace or current fixed settings differ.");
|
|
1862
1893
|
const files = [resolve(cwd, "CLAUDE.md"), resolve(cwd, "topolo-connection.json"), resolve(cwd, ".mcp.json")];
|
|
1863
1894
|
const hashes = {};
|
|
1864
|
-
for (const f of files) hashes[owned(f)] =
|
|
1895
|
+
for (const f of files) hashes[owned(f)] = createHash2("sha256").update(readFileSync(f)).digest("hex");
|
|
1865
1896
|
const connection = JSON.parse(readFileSync(files[1], "utf8"));
|
|
1866
1897
|
const expected = {
|
|
1867
1898
|
environment: descriptor.environment,
|
|
@@ -1898,6 +1929,7 @@ function prepareClaudeDynamicLane(store, pack, policyPath, laneId) {
|
|
|
1898
1929
|
planDigest: record.planDigest,
|
|
1899
1930
|
nativeId: record.nativeId,
|
|
1900
1931
|
cwd: record.creationReceipt.cwd,
|
|
1932
|
+
...nativeCLI ? { continuityTransport: "native-cli", continuity: record.plan.continuity } : {},
|
|
1901
1933
|
title: record.nativeTitle,
|
|
1902
1934
|
source,
|
|
1903
1935
|
intake,
|
|
@@ -1981,7 +2013,7 @@ function prepareClaudePrivateHelperConnection(store, pack, policyPath, sourceRol
|
|
|
1981
2013
|
const policyBytes = readFileSync(policyFile), policy = JSON.parse(policyBytes.toString("utf8")), consent = policy.installationConsent;
|
|
1982
2014
|
if (!consent || !["development", "staging", "production"].includes(consent.environment) || typeof consent.contextKey !== "string")
|
|
1983
2015
|
throw new Error("Existing approved per-environment private connection required.");
|
|
1984
|
-
const bindingFile = resolve(root, "connections", pack, consent.environment,
|
|
2016
|
+
const bindingFile = resolve(root, "connections", pack, consent.environment, createHash2("sha256").update(consent.contextKey).digest("hex") + ".json");
|
|
1985
2017
|
const bindingBytes = readFileSync(bindingFile), binding = JSON.parse(bindingBytes.toString("utf8"));
|
|
1986
2018
|
const connection = {
|
|
1987
2019
|
format: "topolo-private-helper-connection",
|
|
@@ -1991,9 +2023,9 @@ function prepareClaudePrivateHelperConnection(store, pack, policyPath, sourceRol
|
|
|
1991
2023
|
sourceRoleKey,
|
|
1992
2024
|
policyFile,
|
|
1993
2025
|
bindingFile,
|
|
1994
|
-
policyDigest:
|
|
1995
|
-
bindingDigest:
|
|
1996
|
-
stateDigest:
|
|
2026
|
+
policyDigest: createHash2("sha256").update(policyBytes).digest("hex"),
|
|
2027
|
+
bindingDigest: createHash2("sha256").update(bindingBytes).digest("hex"),
|
|
2028
|
+
stateDigest: createHash2("sha256").update(readFileSync(resolve(root, "claude/state.json"))).digest("hex"),
|
|
1997
2029
|
profile: binding.privateCredentialProfile
|
|
1998
2030
|
};
|
|
1999
2031
|
privateClaudeHelperEnvironment(connection, filesystem, cryptography, paths, process.env);
|
|
@@ -2025,6 +2057,11 @@ function runClaudeLaneHelper(root, pack, policyPath, action, payload, source, co
|
|
|
2025
2057
|
return Promise.reject(new Error("Closed typed native lifecycle helper required."));
|
|
2026
2058
|
const encoded = JSON.stringify(payload);
|
|
2027
2059
|
if (Buffer.byteLength(encoded) > 2097152) return Promise.reject(new Error("Own-child native transcript exceeds bounded transport; no broad history read."));
|
|
2060
|
+
try {
|
|
2061
|
+
requireClaudePrivateQueryEffectQualified("helper", payload);
|
|
2062
|
+
} catch (error) {
|
|
2063
|
+
return Promise.reject(error);
|
|
2064
|
+
}
|
|
2028
2065
|
const selectedRole = payload.sourceRoleKey ?? payload.request?.sourceRoleKey ?? payload.plan?.role?.key ?? payload.operation?.record?.plan?.role?.key;
|
|
2029
2066
|
if (!connection || root !== connection.root || pack !== connection.pack || policyPath !== connection.policyFile || selectedRole !== void 0 && selectedRole !== connection.sourceRoleKey)
|
|
2030
2067
|
return Promise.reject(new Error("Private helper arguments differ from the exact bound source connection."));
|
|
@@ -2072,7 +2109,14 @@ function runClaudeLaneHelper(root, pack, policyPath, action, payload, source, co
|
|
|
2072
2109
|
child.stdin.end(encoded);
|
|
2073
2110
|
});
|
|
2074
2111
|
}
|
|
2075
|
-
function requireClaudePrivateQueryEffectQualified(operation) {
|
|
2112
|
+
function requireClaudePrivateQueryEffectQualified(operation, input) {
|
|
2113
|
+
const candidates = [input, input?.request, input?.plan, input?.record?.plan, input?.operation?.record?.plan];
|
|
2114
|
+
const transports = candidates.map((value) => value?.continuityTransport).filter((value) => value !== void 0);
|
|
2115
|
+
if (transports.some((value) => !["native-cli", "private-mcp"].includes(value)) || new Set(transports).size > 1)
|
|
2116
|
+
throw new Error("Closed explicit continuity transport required; no fallback.");
|
|
2117
|
+
const transport = transports[0];
|
|
2118
|
+
if (transport === "native-cli" || candidates.some((value) => value?.schemaVersion === 2))
|
|
2119
|
+
throw Object.assign(new Error("native_cli_startup_unqualified: current startup, profile refresh authority and native result producers are unavailable."), { code: "native_cli_startup_unqualified" });
|
|
2076
2120
|
if (["create", "configure", "context-start", "fixture-start", "retire", "approval-once"].includes(operation))
|
|
2077
2121
|
throw Object.assign(
|
|
2078
2122
|
new Error("native_private_query_prestart_contract_unqualified: private native query startup, exclusion and owned lifecycle are not qualified."),
|
|
@@ -2082,12 +2126,12 @@ function requireClaudePrivateQueryEffectQualified(operation) {
|
|
|
2082
2126
|
function claudeLaneExpression(options, source, journal, prepared) {
|
|
2083
2127
|
if (!Number.isSafeInteger(options.pid) || options.pid < 1 || !["plan", "create", "create-read", "adopt", "read", "configure", "context-start", "context-read", "fixture-start", "fixture-read", "retire"].includes(options.operation))
|
|
2084
2128
|
throw new Error("Closed typed Claude lane operation required.");
|
|
2085
|
-
requireClaudePrivateQueryEffectQualified(options.operation);
|
|
2129
|
+
requireClaudePrivateQueryEffectQualified(options.operation, options.input);
|
|
2086
2130
|
if (!options.privateConnection) throw new Error("Explicit approved private helper connection required before native access.");
|
|
2087
2131
|
return `(async()=>{
|
|
2088
2132
|
const expected=${JSON.stringify({ ...options, qualifier: CLAUDE_NATIVE_QUALIFIER, source, journal })};
|
|
2089
2133
|
const requireClaudePrivateQueryEffectQualified=(${requireClaudePrivateQueryEffectQualified.toString()});
|
|
2090
|
-
requireClaudePrivateQueryEffectQualified(expected.operation);
|
|
2134
|
+
requireClaudePrivateQueryEffectQualified(expected.operation,expected.input);
|
|
2091
2135
|
if(process.pid!==expected.pid || typeof process.getBuiltinModule!=='function')throw new Error('Native process identity changed.');
|
|
2092
2136
|
const load=process.getBuiltinModule('module').createRequire(process.resourcesPath+'/app.asar/package.json');
|
|
2093
2137
|
const fs=load('node:fs'),crypto=load('node:crypto'),path=load('node:path');
|
|
@@ -2203,7 +2247,7 @@ function claudeLaneExpression(options, source, journal, prepared) {
|
|
|
2203
2247
|
if(!secretInfo.isFile()||secretInfo.isSymbolicLink()||secretInfo.uid!==process.getuid()||secretInfo.mode&63)throw new Error('Unowned private native control secret.');
|
|
2204
2248
|
const secret=JSON.parse(fs.readFileSync(control.secretPath,'utf8'));
|
|
2205
2249
|
const ownedOperation=async op=>{
|
|
2206
|
-
requireClaudePrivateQueryEffectQualified(op.kind==='approval-respond'&&op.decision==='once'?'approval-once':op.kind);
|
|
2250
|
+
requireClaudePrivateQueryEffectQualified(op.kind==='approval-respond'&&op.decision==='once'?'approval-once':op.kind,{continuityTransport:nativePlan.continuityTransport});
|
|
2207
2251
|
qualify();const current=await helper('read',{laneId:nativePlan.laneId});
|
|
2208
2252
|
const scopedPlan={...nativePlan,lifecycleState:current.state,deadline:Date.now()+200000};
|
|
2209
2253
|
if(op.kind.startsWith('approval-')){
|
|
@@ -2270,7 +2314,7 @@ function claudeLaneExpression(options, source, journal, prepared) {
|
|
|
2270
2314
|
})()`;
|
|
2271
2315
|
}
|
|
2272
2316
|
async function operateClaudeLane(options, evaluate = evaluateClaudeMain) {
|
|
2273
|
-
requireClaudePrivateQueryEffectQualified(options.operation);
|
|
2317
|
+
requireClaudePrivateQueryEffectQualified(options.operation, options.input);
|
|
2274
2318
|
validateInspectorEndpoint(options.endpoint);
|
|
2275
2319
|
const root = realpathSync(options.store);
|
|
2276
2320
|
const policy = realpathSync(options.policy);
|
|
@@ -2285,6 +2329,7 @@ async function operateClaudeLane(options, evaluate = evaluateClaudeMain) {
|
|
|
2285
2329
|
const journal = resolve(directory, randomUUID() + ".jsonl");
|
|
2286
2330
|
durableRecord(journal, { operation: options.operation, pid: options.pid, at: Date.now() }, "wx");
|
|
2287
2331
|
const prepared = ["plan", "create", "create-read", "adopt"].includes(options.operation) ? void 0 : prepareClaudeDynamicLane(root, options.pack, policy, options.input.laneId);
|
|
2332
|
+
if (prepared?.record.plan.continuityTransport === "native-cli") requireClaudePrivateQueryEffectQualified(options.operation, prepared.record.plan);
|
|
2288
2333
|
let sourceRoleKey = prepared?.record.plan.role.key ?? (options.operation === "plan" ? options.input.request?.sourceRoleKey : options.input.plan?.role?.key);
|
|
2289
2334
|
if (["create", "create-read"].includes(options.operation)) {
|
|
2290
2335
|
if (typeof options.input.laneId !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(options.input.laneId)) throw new Error("Exact existing creation lane required.");
|
|
@@ -2307,8 +2352,8 @@ async function operateClaudeLane(options, evaluate = evaluateClaudeMain) {
|
|
|
2307
2352
|
mkdirSync(directory2, { mode: 448 });
|
|
2308
2353
|
const secretPath = resolve(directory2, "secret.json");
|
|
2309
2354
|
durableRecord(secretPath, { token: randomBytes(32).toString("hex") }, "wx");
|
|
2310
|
-
const identity =
|
|
2311
|
-
const socketDirectory = resolve("/tmp", "topolo-claude-" + process.getuid?.() + "-" +
|
|
2355
|
+
const identity = createHash2("sha256").update(JSON.stringify({ pid: options.pid, laneId, planDigest: prepared.record.planDigest, qualifier: CLAUDE_NATIVE_QUALIFIER })).digest("hex");
|
|
2356
|
+
const socketDirectory = resolve("/tmp", "topolo-claude-" + process.getuid?.() + "-" + createHash2("sha256").update(root + "\0" + laneId).digest("hex").slice(0, 24));
|
|
2312
2357
|
mkdirSync(socketDirectory, { mode: 448 });
|
|
2313
2358
|
const socket = resolve(socketDirectory, "control.sock");
|
|
2314
2359
|
control = { socket, secretPath, identity };
|
|
@@ -2335,7 +2380,7 @@ async function operateClaudeLane(options, evaluate = evaluateClaudeMain) {
|
|
|
2335
2380
|
}
|
|
2336
2381
|
}
|
|
2337
2382
|
function operateClaudeLaneControl(options, request = requestClaudeApprovalControl) {
|
|
2338
|
-
requireClaudePrivateQueryEffectQualified(options.operation.kind === "approval-respond" && options.operation.decision === "once" ? "approval-once" : options.operation.kind);
|
|
2383
|
+
requireClaudePrivateQueryEffectQualified(options.operation.kind === "approval-respond" && options.operation.decision === "once" ? "approval-once" : options.operation.kind, options.operation);
|
|
2339
2384
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.connection)) throw new Error("Exact owned native lane connection ID required.");
|
|
2340
2385
|
const root = realpathSync(options.store);
|
|
2341
2386
|
const directory = resolve(root, "claude/dynamic-controls", options.connection);
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ function registerAgentPacksCommand(program2) {
|
|
|
14
14
|
throw new Error("Exact closed smoke operation, role/case and UUID required.");
|
|
15
15
|
const store = packs.opts().store ?? resolve(homedir(), "AgentPacks");
|
|
16
16
|
if (options.harness === "claude") {
|
|
17
|
-
const { operateClaudeSmoke } = await import("./claude-pack-backend-
|
|
17
|
+
const { operateClaudeSmoke } = await import("./claude-pack-backend-YEVPAOID.js");
|
|
18
18
|
console.log(JSON.stringify(await operateClaudeSmoke({
|
|
19
19
|
store,
|
|
20
20
|
kind: "smoke-" + operation,
|
|
@@ -44,7 +44,7 @@ function registerAgentPacksCommand(program2) {
|
|
|
44
44
|
});
|
|
45
45
|
packs.command("configure").description("Qualify and configure an adopted Claude roster through its live native backend").argument("<pack>").option("--harness <name>", "Host adapter", "claude").requiredOption("--policy <path>", "Reviewed execution policy JSON").requiredOption("--endpoint <url>", "Explicit loopback main-process inspector endpoint, enabled separately with authorization").requiredOption("--pid <pid>", "Expected Claude main-process PID").option("--apply", "Apply native groups, pins, models, effort and permission modes; otherwise preflight only").action(async (pack, options) => {
|
|
46
46
|
if (options.harness !== "claude") throw new Error("Native configure is currently qualified only for Claude Desktop.");
|
|
47
|
-
const { configureClaudePack } = await import("./claude-pack-backend-
|
|
47
|
+
const { configureClaudePack } = await import("./claude-pack-backend-YEVPAOID.js");
|
|
48
48
|
const result = await configureClaudePack({
|
|
49
49
|
store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
|
|
50
50
|
pack,
|
|
@@ -92,7 +92,7 @@ function registerAgentPacksCommand(program2) {
|
|
|
92
92
|
throw new Error("Closed existing Claude lane control operation required.");
|
|
93
93
|
const input3 = options.input ? JSON.parse(readFileSync(options.input, "utf8")) : {};
|
|
94
94
|
if (!input3 || typeof input3 !== "object" || Array.isArray(input3) || Object.keys(input3).some((k) => k !== "operationId")) throw new Error("Only an exact bootstrap/fixture operation UUID may be supplied.");
|
|
95
|
-
const { operateClaudeLaneControl } = await import("./claude-pack-backend-
|
|
95
|
+
const { operateClaudeLaneControl } = await import("./claude-pack-backend-YEVPAOID.js");
|
|
96
96
|
console.log(JSON.stringify(await operateClaudeLaneControl({
|
|
97
97
|
store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
|
|
98
98
|
connection: options.connection,
|
|
@@ -104,7 +104,7 @@ function registerAgentPacksCommand(program2) {
|
|
|
104
104
|
throw new Error("Closed Claude lane operation requires --input, --pack, --policy, --endpoint and --pid.");
|
|
105
105
|
const input2 = JSON.parse(readFileSync(options.input, "utf8"));
|
|
106
106
|
if (!input2 || typeof input2 !== "object" || Array.isArray(input2)) throw new Error("Closed native lane input object required.");
|
|
107
|
-
const { operateClaudeLane } = await import("./claude-pack-backend-
|
|
107
|
+
const { operateClaudeLane } = await import("./claude-pack-backend-YEVPAOID.js");
|
|
108
108
|
console.log(JSON.stringify(await operateClaudeLane({
|
|
109
109
|
store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
|
|
110
110
|
pack: options.pack,
|
|
@@ -130,7 +130,7 @@ function registerAgentPacksCommand(program2) {
|
|
|
130
130
|
decision = response.decision;
|
|
131
131
|
}
|
|
132
132
|
if (options.connection) {
|
|
133
|
-
const { operateClaudeLaneControl } = await import("./claude-pack-backend-
|
|
133
|
+
const { operateClaudeLaneControl } = await import("./claude-pack-backend-YEVPAOID.js");
|
|
134
134
|
console.log(JSON.stringify(await operateClaudeLaneControl({
|
|
135
135
|
store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
|
|
136
136
|
connection: options.connection,
|
|
@@ -142,7 +142,7 @@ function registerAgentPacksCommand(program2) {
|
|
|
142
142
|
}), null, 2));
|
|
143
143
|
return;
|
|
144
144
|
}
|
|
145
|
-
const { operateClaudeApprovals } = await import("./claude-pack-backend-
|
|
145
|
+
const { operateClaudeApprovals } = await import("./claude-pack-backend-YEVPAOID.js");
|
|
146
146
|
console.log(JSON.stringify(await operateClaudeApprovals({
|
|
147
147
|
store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
|
|
148
148
|
kind: args[0],
|
|
@@ -157,7 +157,7 @@ function registerAgentPacksCommand(program2) {
|
|
|
157
157
|
if (name !== "lane" && (options.policy || options.endpoint || options.pid || options.apply)) {
|
|
158
158
|
if (options.harness !== "claude" || !options.policy || !options.endpoint || !options.pid || args.length !== 1)
|
|
159
159
|
throw new Error("Native switch/recovery requires one Claude pack, --policy, --endpoint and --pid.");
|
|
160
|
-
const { configureClaudePack } = await import("./claude-pack-backend-
|
|
160
|
+
const { configureClaudePack } = await import("./claude-pack-backend-YEVPAOID.js");
|
|
161
161
|
const result2 = await configureClaudePack({
|
|
162
162
|
store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
|
|
163
163
|
pack: args[0],
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/cli",
|
|
3
3
|
"packageManager": "pnpm@10.17.1",
|
|
4
|
-
"version": "0.13.
|
|
5
|
-
"topoloPublishedVersion": "0.13.
|
|
4
|
+
"version": "0.13.11",
|
|
5
|
+
"topoloPublishedVersion": "0.13.11",
|
|
6
6
|
"description": "Public command-line interface for Topolo customer organizations, developers, and agents.",
|
|
7
7
|
"license": "UNLICENSED",
|
|
8
8
|
"author": "Topolo",
|