@topolo/cli 0.13.5 → 0.13.7
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.
|
@@ -327,6 +327,9 @@ def parser():
|
|
|
327
327
|
q.add_argument('operation',choices=['list','details','respond']);q.add_argument('key',nargs='?')
|
|
328
328
|
q.add_argument('--intake');q.add_argument('--fingerprint');q.add_argument('--response')
|
|
329
329
|
q.add_argument('--human-decision-recorded',action='store_true')
|
|
330
|
+
q=sub.add_parser('smoke',help='Start or read one closed, bound native acceptance case; never arbitrary prompts')
|
|
331
|
+
q.add_argument('operation',choices=['start','read']);q.add_argument('identity')
|
|
332
|
+
q.add_argument('--case',choices=sorted(live.SMOKE_CASES));q.add_argument('--operation-id')
|
|
330
333
|
for command in ['connect','remote-context','remember','artifact','artifact-get','remote-recover']:
|
|
331
334
|
q=sub.add_parser(command,help='Use public Topolo Agent continuity and Bytes artifact contracts')
|
|
332
335
|
q.add_argument('name');q.add_argument('--environment',choices=['development','staging','production'],required=True)
|
|
@@ -380,7 +383,17 @@ def main(argv=None):
|
|
|
380
383
|
emit(result)
|
|
381
384
|
return 0
|
|
382
385
|
with lib.lock():
|
|
383
|
-
if args.command=='
|
|
386
|
+
if args.command=='smoke':
|
|
387
|
+
if args.operation=='start':
|
|
388
|
+
if not args.case or not args.operation_id:
|
|
389
|
+
raise PackError('Exact smoke case and operation ID required')
|
|
390
|
+
params={'key':args.identity,'caseId':args.case,'operationId':live.OwnedSmoke.operation_id(args.operation_id)}
|
|
391
|
+
else:
|
|
392
|
+
if args.case or args.operation_id:
|
|
393
|
+
raise PackError('Smoke read accepts only its existing operation ID')
|
|
394
|
+
params={'operationId':live.OwnedSmoke.operation_id(args.identity)}
|
|
395
|
+
result=live.request(lib,'smoke-'+args.operation,params)
|
|
396
|
+
elif args.command=='approvals':
|
|
384
397
|
if args.operation!='list' and (not args.key or not args.intake or not args.fingerprint):
|
|
385
398
|
raise PackError('Exact request key, --intake and --fingerprint required')
|
|
386
399
|
params={'key':args.key,'intakeThreadId':args.intake,'fingerprint':args.fingerprint}
|
|
@@ -23,11 +23,240 @@ import time
|
|
|
23
23
|
import urllib.request
|
|
24
24
|
import urllib.parse
|
|
25
25
|
import uuid
|
|
26
|
+
import datetime
|
|
26
27
|
|
|
27
28
|
from .storage import Library,PackError,atomic_json,canonical,read_json
|
|
28
29
|
from .execution import ExecutionGate, load_gate, prepare_runtime, qualify_protocol
|
|
29
30
|
from .approvals import ApprovalRouter
|
|
30
31
|
|
|
32
|
+
SMOKE_PLAN = '6504d4e44c2456ae3e761de6c2e57b086b0e49229a7ee6a1ac782684ad4e40c6'
|
|
33
|
+
SMOKE_CASES = {'stg-context-memory-bytes': ('staging', 6), 'stg-peer-ack': ('staging', 6),
|
|
34
|
+
'prod-role-context-ack': ('production', 140)}
|
|
35
|
+
|
|
36
|
+
class OwnedSmoke:
|
|
37
|
+
"""One finite native verification operation. No caller text, paths or RPC.
|
|
38
|
+
|
|
39
|
+
Both native hosts use this store. A durable exclusive reservation covers
|
|
40
|
+
budget checks and dispatch; an uncertain send retains it across restart.
|
|
41
|
+
Binding/fixture references are supplied by the effect owner, never invented.
|
|
42
|
+
"""
|
|
43
|
+
def __init__(self, lib, harness):
|
|
44
|
+
if harness not in {'codex', 'claude'}:
|
|
45
|
+
raise PackError('Unknown smoke harness')
|
|
46
|
+
self.lib=lib;self.harness=harness;self.root=lib.root/'smoke-operations'
|
|
47
|
+
self.root.mkdir(mode=0o700,exist_ok=True)
|
|
48
|
+
self.owned(self.root,directory=True)
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def owned(path,directory=False):
|
|
52
|
+
stat=path.lstat()
|
|
53
|
+
if path.is_symlink() or stat.st_uid!=os.getuid() or stat.st_mode&0o022 or (not directory and not path.is_file()):
|
|
54
|
+
raise PackError('Unowned smoke record')
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def operation_id(value):
|
|
58
|
+
try: valid=isinstance(value,str) and str(uuid.UUID(value))==value
|
|
59
|
+
except ValueError: valid=False
|
|
60
|
+
if not valid:
|
|
61
|
+
raise PackError('Exact smoke UUID required')
|
|
62
|
+
return value
|
|
63
|
+
|
|
64
|
+
def path(self,operation):
|
|
65
|
+
return self.root/(self.operation_id(operation)+'.json')
|
|
66
|
+
|
|
67
|
+
def read(self,operation):
|
|
68
|
+
path=self.path(operation);self.owned(path)
|
|
69
|
+
if path.stat().st_size>1048576:
|
|
70
|
+
raise PackError('Smoke record exceeds bounded transport')
|
|
71
|
+
record=read_json(path)
|
|
72
|
+
if record.get('operationId')!=operation or record.get('harness')!=self.harness or record.get('planSHA256')!=SMOKE_PLAN:
|
|
73
|
+
raise PackError('Smoke operation custody differs')
|
|
74
|
+
return record
|
|
75
|
+
|
|
76
|
+
def binding(self,key,case):
|
|
77
|
+
if case not in SMOKE_CASES or not isinstance(key,str) or not re.fullmatch(r'[a-z0-9-]+/[a-z0-9-]+/[a-z0-9-]+',key):
|
|
78
|
+
raise PackError('Finite owned smoke case and role required')
|
|
79
|
+
path=self.lib.root/'smoke-bindings'/(self.harness+'.json');self.owned(path.parent,directory=True);self.owned(path)
|
|
80
|
+
if path.stat().st_size>131072:
|
|
81
|
+
raise PackError('Smoke binding exceeds bounded inventory')
|
|
82
|
+
binding=read_json(path);now=time.time()
|
|
83
|
+
if (set(binding)!={'format','planSHA256','planId','issuedAt','expiresAt','roles'} or
|
|
84
|
+
binding['format']!='topolo-owned-native-smoke-v1' or binding['planSHA256']!=SMOKE_PLAN or
|
|
85
|
+
not isinstance(binding['issuedAt'],(int,float)) or not isinstance(binding['expiresAt'],(int,float)) or
|
|
86
|
+
not binding['issuedAt']<=now<binding['expiresAt']<=binding['issuedAt']+900):
|
|
87
|
+
raise PackError('Current bounded smoke binding required')
|
|
88
|
+
self.operation_id(binding['planId'])
|
|
89
|
+
rows=binding['roles']
|
|
90
|
+
if (not isinstance(rows,list) or not 1<=len(rows)<=70 or
|
|
91
|
+
len({r['key'] for r in rows})!=len(rows) or len({r['nativeId'] for r in rows})!=len(rows)):
|
|
92
|
+
raise PackError('Unique complete smoke roster required')
|
|
93
|
+
matches=[r for r in rows if r['key']==key]
|
|
94
|
+
if len(matches)!=1:
|
|
95
|
+
raise PackError('Smoke role is outside the owned binding')
|
|
96
|
+
row=matches[0]
|
|
97
|
+
fields={'key','nativeId','cwd','title','environment','contextKey','principalId','organizationId','instanceId',
|
|
98
|
+
'logicalId','agentId','agentVersionId','agentWorkspaceId','bytesWorkspaceId',
|
|
99
|
+
'contextMarkerId','contextMarkerHash','artifactMemoryId','artifactHash','fixtureExpiresAt'}
|
|
100
|
+
if set(row)!=fields or row['environment']!=SMOKE_CASES[case][0]:
|
|
101
|
+
raise PackError('Exact smoke descriptor or environment differs')
|
|
102
|
+
for field in fields-{'key','cwd','title','environment','contextMarkerHash','artifactHash','fixtureExpiresAt'}:
|
|
103
|
+
if not isinstance(row[field],str) or not re.fullmatch(r'[A-Za-z0-9_:/.-]{1,160}',row[field]):
|
|
104
|
+
raise PackError('Invalid smoke identity reference')
|
|
105
|
+
if (not pathlib.Path(row['cwd']).is_absolute() or not isinstance(row['title'],str) or not 1<=len(row['title'])<=200 or
|
|
106
|
+
any(not re.fullmatch(r'[a-f0-9]{64}',row[f]) for f in ['contextMarkerHash','artifactHash']) or
|
|
107
|
+
row['logicalId']!='/'.join(key.split('/')[1:]) or row['contextKey']!='org:'+row['organizationId']):
|
|
108
|
+
raise PackError('Smoke role/context references differ')
|
|
109
|
+
if case.startswith('stg-') and not now<row['fixtureExpiresAt']<=binding['expiresAt']:
|
|
110
|
+
raise PackError('Bounded staging fixture expiry required')
|
|
111
|
+
from . import topolo
|
|
112
|
+
pack=key.split('/')[0]
|
|
113
|
+
connection=read_json(topolo.binding_path(self.lib,pack,row['environment'],row['contextKey']))
|
|
114
|
+
role=[r for r in connection['input']['roles'] if r['logicalId']==row['logicalId']]
|
|
115
|
+
expected={'instanceId':row['instanceId'],'bytesWorkspaceId':row['bytesWorkspaceId']}
|
|
116
|
+
if (connection['identity']!={'userId':row['principalId'],'organizationId':row['organizationId']} or
|
|
117
|
+
connection['agentWorkspaceId']!=row['agentWorkspaceId'] or
|
|
118
|
+
any(connection['input'].get(k)!=v for k,v in expected.items()) or len(role)!=1 or
|
|
119
|
+
any(role[0].get(k)!=row[k] for k in ['agentId','agentVersionId'])):
|
|
120
|
+
raise PackError('Smoke hosted connection differs')
|
|
121
|
+
local=pathlib.Path(row['cwd'])/'topolo-connection.json';self.owned(local)
|
|
122
|
+
descriptor=read_json(local)
|
|
123
|
+
if any(descriptor.get(k)!=row[k] for k in ['environment','contextKey','instanceId','agentWorkspaceId','bytesWorkspaceId']):
|
|
124
|
+
raise PackError('Current native hosted descriptor differs')
|
|
125
|
+
return binding,row,hashlib.sha256(canonical(binding)).hexdigest()
|
|
126
|
+
|
|
127
|
+
def prepare(self,key,case,operation,native_identity):
|
|
128
|
+
path=self.path(operation)
|
|
129
|
+
if path.exists():
|
|
130
|
+
raise PackError('Smoke operation already exists; read without replay')
|
|
131
|
+
lock=self.root/'.reservation'
|
|
132
|
+
try:fd=os.open(lock,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
|
|
133
|
+
except FileExistsError: raise PackError('Native smoke reservation remains; reconcile without replay')
|
|
134
|
+
with os.fdopen(fd,'wb') as stream:stream.write(canonical({'operationId':operation,'harness':self.harness}));stream.flush();os.fsync(stream.fileno())
|
|
135
|
+
try:
|
|
136
|
+
binding,row,digest=self.binding(key,case)
|
|
137
|
+
history=[]
|
|
138
|
+
for previous in self.root.glob('*.json'):
|
|
139
|
+
self.owned(previous);history.append(read_json(previous))
|
|
140
|
+
active=[r for r in history if r.get('state') not in {'completed','preflight-refused','native-refused'}]
|
|
141
|
+
if active:
|
|
142
|
+
raise PackError('Unsettled native smoke operation remains; no replay')
|
|
143
|
+
used=[r for r in history if r.get('planSHA256')==SMOKE_PLAN and r.get('caseId')==case and r.get('state')!='preflight-refused']
|
|
144
|
+
same_role=lambda r:r.get('harness')==self.harness and (r.get('key')==key or
|
|
145
|
+
(case.startswith('stg-') and r.get('key','').split('/')[1:2]==key.split('/')[1:2]))
|
|
146
|
+
if len(used)>=SMOKE_CASES[case][1] or any(same_role(r) for r in used):
|
|
147
|
+
raise PackError('Finite smoke case budget or role already consumed')
|
|
148
|
+
authority=self.authority(key,row)
|
|
149
|
+
if self.binding(key,case)[2]!=digest:
|
|
150
|
+
raise PackError('Smoke binding changed during qualification')
|
|
151
|
+
record={'operationId':operation,'harness':self.harness,'planSHA256':SMOKE_PLAN,'planId':binding['planId'],
|
|
152
|
+
'caseId':case,'key':key,'nativeIdentity':native_identity,'bindingHash':digest,'row':row,
|
|
153
|
+
'bindingExpiresAt':binding['expiresAt'],'authority':authority,
|
|
154
|
+
'dispatchExpiresAt':min(binding['expiresAt'],row['fixtureExpiresAt']) if case.startswith('stg-') else binding['expiresAt'],
|
|
155
|
+
'state':'prepared','atUTC':datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
156
|
+
'nativeId':None,'hostedUsageVerified':False,'operationEffectVerified':False}
|
|
157
|
+
context={};prefix={}
|
|
158
|
+
for member in binding['roles']:
|
|
159
|
+
charter=pathlib.Path(member['cwd'])/('AGENTS.md' if self.harness=='codex' else 'CLAUDE.md');self.owned(charter)
|
|
160
|
+
for file in pathlib.Path(member['cwd']).rglob('*.md'):
|
|
161
|
+
self.owned(file);context[str(file)]=hashlib.sha256(file.read_bytes()).hexdigest()
|
|
162
|
+
if self.harness=='codex':
|
|
163
|
+
runtime=pathlib.Path(self.lib.active()['runtime'])
|
|
164
|
+
files=[p for folder in ['sessions','archived_sessions'] for p in (runtime/'home'/folder).rglob('*.jsonl')]
|
|
165
|
+
else:
|
|
166
|
+
state=read_json(self.lib.root/'claude/state.json')
|
|
167
|
+
ids={r['nativeId'] for r in binding['roles']}
|
|
168
|
+
files=[pathlib.Path(a['transcript']) for a in state['agents'] if a.get('desktopSessionId') in ids]
|
|
169
|
+
for file in files:
|
|
170
|
+
self.owned(file);raw=file.read_bytes();prefix[str(file)]={'bytes':len(raw),'sha256':hashlib.sha256(raw).hexdigest()}
|
|
171
|
+
record.update(contextFiles=context,contextRoots=[r['cwd'] for r in binding['roles']],transcriptPrefixes=prefix)
|
|
172
|
+
atomic_json(path,record)
|
|
173
|
+
refs={k:row[k] for k in ['principalId','organizationId','instanceId','logicalId','agentId','agentVersionId',
|
|
174
|
+
'agentWorkspaceId','bytesWorkspaceId','contextMarkerId','contextMarkerHash','artifactMemoryId','artifactHash','fixtureExpiresAt']}
|
|
175
|
+
task=('Perform the explicit expiring STG test-memory/Bytes roundtrip only; test provenance is this operation marker. '
|
|
176
|
+
if case=='stg-context-memory-bytes' else 'Read and acknowledge only the bound peer test-memory/Bytes references. '
|
|
177
|
+
if case=='stg-peer-ack' else 'Read only your hosted role/context and the bound verification memory/Bytes artifact. ')
|
|
178
|
+
text=('Topolo finite native acceptance case '+case+'. Operation '+operation+'. '+task+
|
|
179
|
+
'Use the published Topolo Agent and Bytes tools in '+row['environment']+' / '+row['contextKey']+
|
|
180
|
+
', for exactly '+key+'. Retrieve actual hosted context and the referenced Bytes content; local or CLI-proxy evidence is insufficient. '+
|
|
181
|
+
'Exact references: '+canonical(refs).decode()+'. No business work, spawn, additional messages, permission changes or unrelated writes. '+
|
|
182
|
+
'If any reference/tool is unavailable report the exact blocker. Return one concise JSON ACK with operationId, logicalId, agentVersionId, '+
|
|
183
|
+
'contextMarkerHash, artifactHash and status; target at most 256 output tokens.')
|
|
184
|
+
return {'record':record,'text':text,'roster':[{k:r[k] for k in ['key','nativeId','cwd','title']} for r in binding['roles']]}
|
|
185
|
+
except BaseException:
|
|
186
|
+
lock.unlink();raise
|
|
187
|
+
|
|
188
|
+
def authority(self,key,row):
|
|
189
|
+
"""Fresh public authority, with local consent re-read after blocking I/O."""
|
|
190
|
+
from . import topolo,permissions
|
|
191
|
+
pack=key.split('/')[0]
|
|
192
|
+
manifest,_=self.lib.load(pack);consent=permissions.load(self.lib,pack,manifest['agents'])
|
|
193
|
+
if not consent or consent.get('profile')!='full-access' or consent.get('contextKey')!=row['contextKey']:
|
|
194
|
+
raise PackError('Smoke needs the existing exact full-access consent')
|
|
195
|
+
original=canonical([manifest,consent])
|
|
196
|
+
client=topolo.Client(row['environment'],row['contextKey'])
|
|
197
|
+
if topolo.identity(client)!={'userId':row['principalId'],'organizationId':row['organizationId']}:
|
|
198
|
+
raise PackError('Fresh smoke principal differs')
|
|
199
|
+
ceiling,proof=topolo.organization_execution_policy(client,consent,manifest['agents'],row['agentWorkspaceId'])
|
|
200
|
+
permissions.authorize_native_scope(proof)
|
|
201
|
+
current,_=self.lib.load(pack)
|
|
202
|
+
if canonical([current,permissions.load(self.lib,pack,current['agents'])])!=original:
|
|
203
|
+
raise PackError('Smoke consent or manifest changed during authority qualification')
|
|
204
|
+
return {'localHash':hashlib.sha256(original).hexdigest(),'policyHash':hashlib.sha256(canonical([ceiling,proof])).hexdigest()}
|
|
205
|
+
|
|
206
|
+
def arm(self,operation,native_identity):
|
|
207
|
+
"""Admit only the original prepared operation; never reset its budget."""
|
|
208
|
+
record=self.read(operation)
|
|
209
|
+
lock=self.root/'.reservation';self.owned(lock)
|
|
210
|
+
if (record['state']!='prepared' or record['nativeIdentity']!=native_identity or
|
|
211
|
+
read_json(lock)!={'operationId':operation,'harness':self.harness}):
|
|
212
|
+
raise PackError('Original prepared smoke reservation required; no replay')
|
|
213
|
+
binding,row,digest=self.binding(record['key'],record['caseId'])
|
|
214
|
+
if digest!=record['bindingHash'] or row!=record['row']:
|
|
215
|
+
raise PackError('Original smoke binding changed before dispatch')
|
|
216
|
+
if self.authority(record['key'],row)!=record['authority']:
|
|
217
|
+
raise PackError('Original smoke authority changed before dispatch')
|
|
218
|
+
# The public qualification can block. Re-read local binding/expiry,
|
|
219
|
+
# descriptors and original context after it, before the durable arm.
|
|
220
|
+
if self.binding(record['key'],record['caseId'])[2]!=record['bindingHash']:
|
|
221
|
+
raise PackError('Original smoke binding changed during final qualification')
|
|
222
|
+
self.verify(operation)
|
|
223
|
+
self.mark(operation,'start-may-have-effects')
|
|
224
|
+
return {'operationId':operation,'nativeIdentity':native_identity,'bindingHash':digest,
|
|
225
|
+
'expiresAt':record['dispatchExpiresAt'],'state':'start-may-have-effects'}
|
|
226
|
+
|
|
227
|
+
def mark(self,operation,state,**evidence):
|
|
228
|
+
record=self.read(operation)
|
|
229
|
+
allowed={'prepared':{'prepared','resume-may-have-effects','start-may-have-effects','preflight-refused'},
|
|
230
|
+
'resume-may-have-effects':{'start-may-have-effects','uncertain'},
|
|
231
|
+
'start-may-have-effects':{'native-delivered','native-refused','uncertain'},
|
|
232
|
+
'native-delivered':{'completed','uncertain'}}
|
|
233
|
+
if state not in allowed.get(record['state'],set()):
|
|
234
|
+
raise PackError('Smoke phase cannot replay or regress')
|
|
235
|
+
if set(evidence)-{'nativeId','nativeStatus','ack','historyPreserved','historyCount','historyHash'} or len(canonical(evidence))>8192:
|
|
236
|
+
raise PackError('Bounded native smoke evidence required')
|
|
237
|
+
record.update(state=state,**evidence);atomic_json(self.path(operation),record)
|
|
238
|
+
if state in {'native-delivered','native-refused','preflight-refused'}:
|
|
239
|
+
lock=self.root/'.reservation';self.owned(lock)
|
|
240
|
+
if read_json(lock).get('operationId')!=operation:
|
|
241
|
+
raise PackError('Native smoke reservation custody differs')
|
|
242
|
+
lock.unlink()
|
|
243
|
+
return record
|
|
244
|
+
|
|
245
|
+
def verify(self,operation):
|
|
246
|
+
record=self.read(operation)
|
|
247
|
+
if {str(p) for root in record['contextRoots'] for p in pathlib.Path(root).rglob('*.md')}!=set(record['contextFiles']):
|
|
248
|
+
raise PackError('Native smoke context inventory changed')
|
|
249
|
+
for filename,digest in record['contextFiles'].items():
|
|
250
|
+
path=pathlib.Path(filename);self.owned(path)
|
|
251
|
+
if hashlib.sha256(path.read_bytes()).hexdigest()!=digest:
|
|
252
|
+
raise PackError('Native smoke context changed')
|
|
253
|
+
for filename,before in record['transcriptPrefixes'].items():
|
|
254
|
+
path=pathlib.Path(filename);self.owned(path)
|
|
255
|
+
with path.open('rb') as stream: prefix=stream.read(before['bytes'])
|
|
256
|
+
if hashlib.sha256(prefix).hexdigest()!=before['sha256']:
|
|
257
|
+
raise PackError('Native smoke original transcript prefix changed')
|
|
258
|
+
return {'contextPreserved':True,'originalTranscriptPrefixesPreserved':True}
|
|
259
|
+
|
|
31
260
|
def permission_settings(agent, current, consent):
|
|
32
261
|
from .policy import settings
|
|
33
262
|
wanted=settings(agent,'codex',consent)['nativeSettings']
|
|
@@ -46,7 +275,7 @@ def request(lib,operation,params=None):
|
|
|
46
275
|
record=read_json(lib.root/'live-bridge.json',{})
|
|
47
276
|
if not record: raise PackError('This desktop profile has no live bridge. Open it through topolo-agent-pack first.')
|
|
48
277
|
with socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) as connection:
|
|
49
|
-
connection.settimeout(3 if operation=='status' else 60 if operation in {'permissions-configure','owned-roster-read'} else 20)
|
|
278
|
+
connection.settimeout(3 if operation=='status' else 60 if operation in {'permissions-configure','owned-roster-read','smoke-start'} else 20)
|
|
50
279
|
try: connection.connect(record['socket'])
|
|
51
280
|
except OSError as exc: raise PackError('Managed live bridge is unavailable: '+str(exc)) from exc
|
|
52
281
|
connection.sendall(canonical({'token':record['token'],'operation':operation,**({'params':params} if params is not None else {})})+b'\n')
|
|
@@ -85,6 +314,7 @@ class BackendProxy:
|
|
|
85
314
|
self.permission_configuration=False
|
|
86
315
|
self.permission_batch=None
|
|
87
316
|
self._permission_configuration_inputs=None
|
|
317
|
+
self.smoke_busy=False
|
|
88
318
|
|
|
89
319
|
async def start_child(self):
|
|
90
320
|
active=self.lib.active()
|
|
@@ -152,6 +382,9 @@ class BackendProxy:
|
|
|
152
382
|
if self.output_task: await self.output_task
|
|
153
383
|
|
|
154
384
|
async def forward(self,value):
|
|
385
|
+
if self.smoke_busy and value.get('method') in {'thread/resume','turn/start'}:
|
|
386
|
+
sys.stdout.buffer.write(canonical({'id':value.get('id'),'error':{'code':-32000,
|
|
387
|
+
'message':'Owned native smoke dispatch is being qualified'}})+b'\n');sys.stdout.buffer.flush();return
|
|
155
388
|
if self.permission_configuration and value.get('method') in {'thread/resume','turn/start'}:
|
|
156
389
|
sys.stdout.buffer.write(canonical({'id':value.get('id'),'error':{'code':-32000,'message':'Owned permission configuration in progress'}})+b'\n')
|
|
157
390
|
sys.stdout.buffer.flush();return
|
|
@@ -183,7 +416,7 @@ class BackendProxy:
|
|
|
183
416
|
'params':{'includeHidden':True,**({'cursor':cursor} if cursor else {})}})+b'\n')
|
|
184
417
|
await self.child.stdin.drain()
|
|
185
418
|
|
|
186
|
-
async def call_native(self,method,params):
|
|
419
|
+
async def call_native(self,method,params,before_dispatch=None):
|
|
187
420
|
identity='topolo-relay-'+uuid.uuid4().hex
|
|
188
421
|
request={'id':identity,'method':method,'params':params}
|
|
189
422
|
if method in {'thread/resume','turn/start'}:
|
|
@@ -196,6 +429,7 @@ class BackendProxy:
|
|
|
196
429
|
raise PackError('No owned permission configuration snapshot; native settings were not sent')
|
|
197
430
|
self.configuration_current(*self._permission_configuration_inputs)
|
|
198
431
|
request=self.execution.prepare(request)
|
|
432
|
+
if before_dispatch is not None: before_dispatch()
|
|
199
433
|
future=asyncio.get_running_loop().create_future();self.native_calls[identity]=future;self.pending.add(identity)
|
|
200
434
|
try:
|
|
201
435
|
self.child.stdin.write(canonical(request)+b'\n');await self.child.stdin.drain()
|
|
@@ -207,6 +441,111 @@ class BackendProxy:
|
|
|
207
441
|
raise PackError('Native intake relay refused; original request remains pending')
|
|
208
442
|
return reply['result']
|
|
209
443
|
|
|
444
|
+
def smoke_current(self,roles,resuming=None):
|
|
445
|
+
if (not self.execution or self.permission_configuration or self.quiesced or not self.initialized or self.closed or
|
|
446
|
+
not self.child or self.child.returncode is not None or self.active_turns or self.pending or self.notifications or
|
|
447
|
+
(self.approvals and self.approvals.pending)):
|
|
448
|
+
raise PackError('Smoke requires an idle current configured native connection')
|
|
449
|
+
current=self.existing_runtime_intent()['roles']
|
|
450
|
+
effective=[{k:r.get(k) for k in original} for original in current for r in [self.execution.roles.get(original['threadId'],{})]]
|
|
451
|
+
if canonical(current)!=canonical(roles) or canonical(current)!=canonical(effective):
|
|
452
|
+
raise PackError('Smoke execution/source roster changed')
|
|
453
|
+
verified=set(self.execution.roles)
|
|
454
|
+
if resuming is not None:
|
|
455
|
+
if list(self.execution.pending.values())!=[resuming]:
|
|
456
|
+
raise PackError('Owned smoke resume custody differs')
|
|
457
|
+
verified.discard(resuming)
|
|
458
|
+
if self.execution.verified!=verified:
|
|
459
|
+
raise PackError('Smoke requires all owned native permission/settings ACKs on this connection')
|
|
460
|
+
for role in roles:
|
|
461
|
+
spec=role['settings']
|
|
462
|
+
if (spec['sandbox_mode']!='danger-full-access' or spec['approval_policy']!='never' or
|
|
463
|
+
not role.get('installationConsent') or spec['model_reasoning_effort'] not in self.execution.catalog.get(spec['model'],set())):
|
|
464
|
+
raise PackError('Smoke native full-access policy or catalog is not qualified')
|
|
465
|
+
|
|
466
|
+
async def owned_smoke(self,operation,params):
|
|
467
|
+
wanted={'operationId'} if operation=='smoke-read' else {'operationId','key','caseId'}
|
|
468
|
+
if operation not in {'smoke-start','smoke-read'} or set(params)!=wanted:
|
|
469
|
+
raise PackError('Only closed smoke start/read fields are supported')
|
|
470
|
+
store=OwnedSmoke(self.lib,'codex');op=store.operation_id(params['operationId'])
|
|
471
|
+
if operation=='smoke-read':
|
|
472
|
+
record=store.read(op)
|
|
473
|
+
if record['state']!='native-delivered': return self.smoke_result(record)
|
|
474
|
+
if self.smoke_busy or not self.execution or not self.child or self.quiesced:
|
|
475
|
+
raise PackError('Existing native smoke connection unavailable; no replay')
|
|
476
|
+
identity=hashlib.sha256(canonical([self.runtime,self.child.pid,self.protocol])).hexdigest()
|
|
477
|
+
if record['nativeIdentity']!=identity: return {**self.smoke_result(record),'connectionCurrent':False}
|
|
478
|
+
row=record['row'];role=self.execution.roles.get(row['nativeId'])
|
|
479
|
+
if not role or role['key']!=record['key'] or role['cwd']!=row['cwd']:
|
|
480
|
+
raise PackError('Native smoke read ownership differs')
|
|
481
|
+
native=await self.call_native('thread/read',{'threadId':row['nativeId'],'includeTurns':True})
|
|
482
|
+
thread=native['thread'];turns=thread.get('turns',[])
|
|
483
|
+
matches=[t for t in turns if t.get('id')==record['nativeId']]
|
|
484
|
+
if thread['id']!=row['nativeId'] or thread['cwd']!=row['cwd'] or len(matches)!=1:
|
|
485
|
+
raise PackError('Exact native smoke turn is unavailable')
|
|
486
|
+
turn=matches[0]
|
|
487
|
+
if turn.get('status') not in {'completed','failed','interrupted'}: return self.smoke_result(record)
|
|
488
|
+
prefix=turns[:record['historyCount']]
|
|
489
|
+
preserved=hashlib.sha256(canonical(prefix)).hexdigest()==record['historyHash']
|
|
490
|
+
texts=[i.get('text','') for i in turn.get('items',[]) if i.get('type')=='agentMessage']
|
|
491
|
+
ack=None
|
|
492
|
+
if texts and len(texts[-1])<=2048:
|
|
493
|
+
try: ack=json.loads(texts[-1])
|
|
494
|
+
except ValueError: pass
|
|
495
|
+
expected={'operationId':op,'logicalId':row['logicalId'],'agentVersionId':row['agentVersionId'],
|
|
496
|
+
'contextMarkerHash':row['contextMarkerHash'],'artifactHash':row['artifactHash']}
|
|
497
|
+
valid=(preserved and isinstance(ack,dict) and set(ack)==set(expected)|{'status'} and
|
|
498
|
+
all(ack[k]==v for k,v in expected.items()) and ack['status'] in {'ok','blocked'} and turn.get('status')=='completed')
|
|
499
|
+
store.verify(op)
|
|
500
|
+
if not valid:
|
|
501
|
+
record=store.mark(op,'uncertain',nativeStatus=turn.get('status'),historyPreserved=preserved)
|
|
502
|
+
else: record=store.mark(op,'completed',nativeStatus=turn.get('status'),ack=ack,historyPreserved=True)
|
|
503
|
+
return self.smoke_result(record)
|
|
504
|
+
if self.smoke_busy:
|
|
505
|
+
raise PackError('Native smoke dispatch already reserved')
|
|
506
|
+
self.smoke_busy=True;prepared=False
|
|
507
|
+
try:
|
|
508
|
+
roles=self.existing_runtime_intent()['roles'];self.smoke_current(roles)
|
|
509
|
+
self.execution.set_catalog(await self.read_catalog());self.smoke_current(roles)
|
|
510
|
+
binding,row,_=store.binding(params['key'],params['caseId'])
|
|
511
|
+
if {(r['key'],r['nativeId'],r['cwd']) for r in binding['roles']}!={(r['key'],r['threadId'],r['cwd']) for r in roles}:
|
|
512
|
+
raise PackError('Smoke binding must cover the complete existing roster')
|
|
513
|
+
identity=hashlib.sha256(canonical([self.runtime,self.child.pid,self.protocol])).hexdigest()
|
|
514
|
+
plan=await asyncio.to_thread(store.prepare,params['key'],params['caseId'],op,identity);prepared=True
|
|
515
|
+
self.smoke_current(roles)
|
|
516
|
+
native=await self.call_native('thread/read',{'threadId':row['nativeId'],'includeTurns':True})
|
|
517
|
+
thread=native['thread'];history=thread.get('turns',[])
|
|
518
|
+
if (thread.get('id')!=row['nativeId'] or thread.get('cwd')!=row['cwd'] or thread.get('name')!=row['title'] or
|
|
519
|
+
thread.get('status',{}).get('type') not in {'idle','notLoaded'} or not isinstance(history,list)):
|
|
520
|
+
raise PackError('Native smoke descriptor is not the exact idle role')
|
|
521
|
+
record=store.read(op);record.update(historyCount=len(history),historyHash=hashlib.sha256(canonical(history)).hexdigest())
|
|
522
|
+
atomic_json(store.path(op),record)
|
|
523
|
+
def guard(resuming=None):
|
|
524
|
+
self.smoke_current(roles,resuming)
|
|
525
|
+
store.verify(op)
|
|
526
|
+
if store.binding(params['key'],params['caseId'])[2]!=record['bindingHash']:
|
|
527
|
+
raise PackError('Smoke binding changed before native dispatch')
|
|
528
|
+
guard();store.mark(op,'resume-may-have-effects')
|
|
529
|
+
await self.call_native('thread/resume',{'threadId':row['nativeId']},before_dispatch=lambda:guard(row['nativeId']))
|
|
530
|
+
guard();store.mark(op,'start-may-have-effects')
|
|
531
|
+
result=await self.call_native('turn/start',{'threadId':row['nativeId'],
|
|
532
|
+
'input':[{'type':'text','text':plan['text'],'text_elements':[]}]},before_dispatch=guard)
|
|
533
|
+
turn=result.get('turn',{})
|
|
534
|
+
if not isinstance(turn.get('id'),str) or not turn['id']:
|
|
535
|
+
raise PackError('Native smoke turn identity missing')
|
|
536
|
+
return self.smoke_result(store.mark(op,'native-delivered',nativeId=turn['id']))
|
|
537
|
+
except BaseException:
|
|
538
|
+
if prepared:
|
|
539
|
+
phase=store.read(op)['state']
|
|
540
|
+
store.mark(op,'preflight-refused' if phase=='prepared' else 'uncertain')
|
|
541
|
+
raise
|
|
542
|
+
finally:self.smoke_busy=False
|
|
543
|
+
|
|
544
|
+
@staticmethod
|
|
545
|
+
def smoke_result(record):
|
|
546
|
+
return {k:record[k] for k in ['operationId','harness','caseId','key','state','nativeId',
|
|
547
|
+
'hostedUsageVerified','operationEffectVerified', 'nativeStatus','historyPreserved','ack'] if k in record}
|
|
548
|
+
|
|
210
549
|
async def qualify_organization(self,thread_id):
|
|
211
550
|
role=self.execution.roles.get(thread_id) if self.execution else None
|
|
212
551
|
if role and not self.permission_configuration and (pathlib.Path(self.runtime)/'permissions-operation.json').exists():
|
|
@@ -473,6 +812,8 @@ class BackendProxy:
|
|
|
473
812
|
if self.quiesced: raise PackError('Use the desktop reconnect action to resume a quiesced bridge')
|
|
474
813
|
elif op=='owned-roster-read':
|
|
475
814
|
result=await self.owned_roster_read(message.get('params') or {})
|
|
815
|
+
elif op in {'smoke-start','smoke-read'}:
|
|
816
|
+
result=await self.owned_smoke(op,message.get('params') or {})
|
|
476
817
|
elif op=='permissions-configure':
|
|
477
818
|
result=await self.configure_permissions((message.get('params') or {})['pack'])
|
|
478
819
|
elif op in {'approval-list','approval-details','approval-respond'}:
|
|
@@ -498,7 +839,7 @@ class BackendProxy:
|
|
|
498
839
|
self.child.stdin.write(canonical(reply)+b'\n');await self.child.stdin.drain();self.approvals.settled(reply)
|
|
499
840
|
result={'state':'native-response-forwarded','operationEffectVerified':False}
|
|
500
841
|
elif op!='status': raise PackError('Unknown live bridge operation')
|
|
501
|
-
if op not in {'approval-list','approval-details','approval-respond','permissions-configure','owned-roster-read'}:result=self.status()
|
|
842
|
+
if op not in {'approval-list','approval-details','approval-respond','permissions-configure','owned-roster-read','smoke-start','smoke-read'}:result=self.status()
|
|
502
843
|
except Exception as exc: result={'error':str(exc)}
|
|
503
844
|
writer.write(canonical(result)+b'\n');await writer.drain();writer.close();await writer.wait_closed()
|
|
504
845
|
|
|
@@ -270,7 +270,8 @@ async function nativeClaudeApprovalControl(manager, descriptor, token, net, fs,
|
|
|
270
270
|
if (Object.keys(request).sort().join(",") !== "identity,operation,pid,token" || request.token !== token || request.pid !== descriptor.pid || request.identity !== descriptor.identity)
|
|
271
271
|
throw new Error("Native approval control authentication or process identity differs.");
|
|
272
272
|
const op = request.operation;
|
|
273
|
-
|
|
273
|
+
const fields = op?.kind === "smoke-start" ? ["kind", "key", "caseId", "operationId"] : op?.kind === "smoke-read" ? ["kind", "operationId"] : ["kind", "key", "intake", "fingerprint", "decision", "humanDecisionRecorded"];
|
|
274
|
+
if (!op || !["list", "details", "respond", "smoke-start", "smoke-read"].includes(op.kind) || Object.keys(op).some((k) => !fields.includes(k)))
|
|
274
275
|
throw new Error("Unsupported native control operation.");
|
|
275
276
|
const result = await invoke(op);
|
|
276
277
|
const reply = JSON.stringify({ result }) + "\n";
|
|
@@ -315,6 +316,136 @@ async function nativeClaudeApprovalControl(manager, descriptor, token, net, fs,
|
|
|
315
316
|
manager[key] = control;
|
|
316
317
|
return { state: "native-approval-control-installed", ...descriptor };
|
|
317
318
|
}
|
|
319
|
+
async function nativeClaudeOwnedSmokeOperation(manager, members, operation, helper, fingerprint, nativeIdentity, qualifyNative = () => {
|
|
320
|
+
}) {
|
|
321
|
+
const uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
322
|
+
const fields = operation?.kind === "smoke-start" ? ["kind", "key", "caseId", "operationId"] : ["kind", "operationId"];
|
|
323
|
+
if (!operation || !["smoke-start", "smoke-read"].includes(operation.kind) || Object.keys(operation).sort().join() !== fields.sort().join() || !uuid.test(operation.operationId) || operation.kind === "smoke-start" && (!/^[a-z0-9-]+\/[a-z0-9-]+\/[a-z0-9-]+$/.test(operation.key) || !["stg-context-memory-bytes", "stg-peer-ack", "prod-role-context-ack"].includes(operation.caseId)))
|
|
324
|
+
throw new Error("Exact closed native smoke operation required.");
|
|
325
|
+
if (!members.length || members.length > 70 || new Set(members.map((m) => m.key)).size !== members.length || new Set(members.map((m) => m.sessionId)).size !== members.length) throw new Error("Unique native smoke roster required.");
|
|
326
|
+
const owner = /* @__PURE__ */ Symbol.for("topolo.agent-pack.owned-smoke.v1");
|
|
327
|
+
const rosterIdentity = fingerprint(members);
|
|
328
|
+
const summary = (record) => Object.fromEntries([
|
|
329
|
+
"operationId",
|
|
330
|
+
"harness",
|
|
331
|
+
"caseId",
|
|
332
|
+
"key",
|
|
333
|
+
"state",
|
|
334
|
+
"nativeId",
|
|
335
|
+
"hostedUsageVerified",
|
|
336
|
+
"operationEffectVerified",
|
|
337
|
+
"nativeStatus",
|
|
338
|
+
"historyPreserved",
|
|
339
|
+
"ack"
|
|
340
|
+
].filter((k) => k in record).map((k) => [k, record[k]]));
|
|
341
|
+
const check = (member2, session) => {
|
|
342
|
+
if (!session || session.sessionId !== member2.sessionId || session.cwd !== member2.cwd || session.title !== member2.title || session.isArchived || session.backend?.kind !== "local" || session.backend.remoteTarget || session.worktreePath || session.worktreeLazy || session.spawnedFrom || session.isRunning || session.starting || session.pendingToolPermissions?.length || manager.hasLosableWork(member2.sessionId) || session.model !== member2.model || session.effort !== member2.effort || session.permissionMode !== member2.permissionMode || member2.permissionMode !== "bypassPermissions" || !member2.permissionConsent || !manager.getAvailableCodeModels().includes(member2.model)) throw new Error("Exact idle native smoke ownership/settings unavailable.");
|
|
343
|
+
};
|
|
344
|
+
const current = () => {
|
|
345
|
+
qualifyNative();
|
|
346
|
+
if (manager[/* @__PURE__ */ Symbol.for("topolo.agent-pack.native-operation.v1")] || fingerprint(members) !== rosterIdentity)
|
|
347
|
+
throw new Error("Unresolved native configuration or changed smoke roster remains.");
|
|
348
|
+
for (const member2 of members) check(member2, manager.getSession(member2.sessionId));
|
|
349
|
+
};
|
|
350
|
+
const ready = async () => {
|
|
351
|
+
current();
|
|
352
|
+
for (const member2 of members) {
|
|
353
|
+
const descriptor = await manager.getSessionWhenLoaded(member2.sessionId, "topolo-owned-smoke", { warnOnMiss: false });
|
|
354
|
+
check(member2, descriptor);
|
|
355
|
+
check(member2, manager.getSession(member2.sessionId));
|
|
356
|
+
}
|
|
357
|
+
current();
|
|
358
|
+
};
|
|
359
|
+
if (operation.kind === "smoke-read") {
|
|
360
|
+
const record = await helper("read", { operation: operation.operationId });
|
|
361
|
+
if (record.state !== "native-delivered") return summary(record);
|
|
362
|
+
if (record.nativeIdentity !== nativeIdentity) return { ...summary(record), connectionCurrent: false };
|
|
363
|
+
const member2 = members.find((m) => m.key === record.key && m.sessionId === record.row.nativeId && m.cwd === record.row.cwd);
|
|
364
|
+
if (!member2) throw new Error("Native smoke read custody differs.");
|
|
365
|
+
const session = await manager.getSession(member2.sessionId);
|
|
366
|
+
if (session?.isRunning || session?.starting || manager[owner]) return summary(record);
|
|
367
|
+
check(member2, session);
|
|
368
|
+
const history = await manager.getTranscript(member2.sessionId);
|
|
369
|
+
if (!Array.isArray(history)) throw new Error("Qualified native smoke transcript API unavailable.");
|
|
370
|
+
const same = fingerprint(history.slice(0, record.historyCount)) === record.historyHash;
|
|
371
|
+
const markers = history.map((m, index) => m.uuid === record.operationId ? index : -1).filter((i) => i >= 0);
|
|
372
|
+
const marker = markers[0] ?? -1;
|
|
373
|
+
const user = markers.length === 1 && marker === record.historyCount && history[marker]?.type === "user" ? marker : -1;
|
|
374
|
+
const nextUser = history.findIndex((m, index) => index > user && m.type === "user");
|
|
375
|
+
const assistants = user < 0 ? [] : history.slice(user + 1, nextUser < 0 ? history.length : nextUser).filter((m) => m.type === "assistant");
|
|
376
|
+
const text = assistants.at(-1)?.message?.content?.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
377
|
+
let ack;
|
|
378
|
+
if (typeof text === "string" && text.length <= 2048) {
|
|
379
|
+
try {
|
|
380
|
+
ack = JSON.parse(text);
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const expected = {
|
|
385
|
+
operationId: record.operationId,
|
|
386
|
+
logicalId: record.row.logicalId,
|
|
387
|
+
agentVersionId: record.row.agentVersionId,
|
|
388
|
+
contextMarkerHash: record.row.contextMarkerHash,
|
|
389
|
+
artifactHash: record.row.artifactHash
|
|
390
|
+
};
|
|
391
|
+
const valid = same && Number.isSafeInteger(record.historyCount) && record.historyCount >= 0 && ack && typeof ack === "object" && !Array.isArray(ack) && Object.keys(ack).sort().join() === [...Object.keys(expected), "status"].sort().join() && Object.entries(expected).every(([k, v]) => ack[k] === v) && ["ok", "blocked"].includes(ack.status);
|
|
392
|
+
await helper("verify", { operation: operation.operationId });
|
|
393
|
+
return summary(await helper("mark", {
|
|
394
|
+
operation: operation.operationId,
|
|
395
|
+
state: "uncertain",
|
|
396
|
+
nativeStatus: "terminal-evidence-unavailable",
|
|
397
|
+
historyPreserved: same,
|
|
398
|
+
...valid ? { ack } : {}
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
if (manager[owner]) throw new Error("Native smoke dispatch already reserved.");
|
|
402
|
+
const member = members.find((m) => m.key === operation.key);
|
|
403
|
+
if (!member) throw new Error("Smoke role is not natively owned.");
|
|
404
|
+
manager[owner] = operation.operationId;
|
|
405
|
+
let prepared = false;
|
|
406
|
+
try {
|
|
407
|
+
await ready();
|
|
408
|
+
const plan = await helper("prepare", {
|
|
409
|
+
key: operation.key,
|
|
410
|
+
case: operation.caseId,
|
|
411
|
+
operation: operation.operationId,
|
|
412
|
+
native_identity: nativeIdentity
|
|
413
|
+
});
|
|
414
|
+
prepared = true;
|
|
415
|
+
const actual = plan.roster.map((r) => [r.key, r.nativeId, r.cwd, r.title]).sort();
|
|
416
|
+
const wanted = members.map((m) => [m.key, m.sessionId, m.cwd, m.title]).sort();
|
|
417
|
+
if (JSON.stringify(actual) !== JSON.stringify(wanted)) throw new Error("Smoke binding must match the complete owned native roster.");
|
|
418
|
+
await ready();
|
|
419
|
+
await helper("verify", { operation: operation.operationId });
|
|
420
|
+
const history = await manager.getTranscript(member.sessionId);
|
|
421
|
+
if (!Array.isArray(history)) throw new Error("Qualified native smoke transcript API unavailable.");
|
|
422
|
+
await helper("mark", { operation: operation.operationId, state: "prepared", historyCount: history.length, historyHash: fingerprint(history) });
|
|
423
|
+
await ready();
|
|
424
|
+
const admission = await helper("arm", { operation: operation.operationId, native_identity: nativeIdentity });
|
|
425
|
+
current();
|
|
426
|
+
if (admission.operationId !== operation.operationId || admission.nativeIdentity !== nativeIdentity || admission.state !== "start-may-have-effects" || admission.bindingHash !== plan.record.bindingHash || !/^[a-f0-9]{64}$/.test(admission.bindingHash) || !Number.isFinite(admission.expiresAt) || admission.expiresAt !== plan.record.dispatchExpiresAt || Date.now() >= admission.expiresAt * 1e3)
|
|
427
|
+
throw new Error("Original smoke final dispatch admission expired or differs.");
|
|
428
|
+
const ack = await manager.sendMessage(
|
|
429
|
+
member.sessionId,
|
|
430
|
+
plan.text,
|
|
431
|
+
void 0,
|
|
432
|
+
{ messageUuid: operation.operationId, initiator: "agent", origin: { kind: "task-notification" } }
|
|
433
|
+
);
|
|
434
|
+
return summary(await helper("mark", {
|
|
435
|
+
operation: operation.operationId,
|
|
436
|
+
state: ack?.delivered === true ? "native-delivered" : ack?.delivered === false ? "native-refused" : "uncertain",
|
|
437
|
+
nativeId: operation.operationId
|
|
438
|
+
}));
|
|
439
|
+
} catch (error) {
|
|
440
|
+
if (prepared) {
|
|
441
|
+
const record = await helper("read", { operation: operation.operationId });
|
|
442
|
+
await helper("mark", { operation: operation.operationId, state: record.state === "prepared" ? "preflight-refused" : "uncertain" });
|
|
443
|
+
}
|
|
444
|
+
throw error;
|
|
445
|
+
} finally {
|
|
446
|
+
delete manager[owner];
|
|
447
|
+
}
|
|
448
|
+
}
|
|
318
449
|
async function nativeClaudeApprovalOperation(manager, routes, operation, fingerprint, checkpoint, history = []) {
|
|
319
450
|
const symbol = /* @__PURE__ */ Symbol.for("topolo.agent-pack.approvals.v1");
|
|
320
451
|
if (!routes.length || routes.length > 100 || new Set(routes.map((r) => r.sessionId)).size !== routes.length || new Set(routes.map((r) => r.key)).size !== routes.length) throw new Error("Unique owned approval roster required.");
|
|
@@ -496,13 +627,13 @@ async function nativeClaudeApprovalOperation(manager, routes, operation, fingerp
|
|
|
496
627
|
|
|
497
628
|
// src/lib/claude-pack-backend.ts
|
|
498
629
|
var CLAUDE_NATIVE_QUALIFIER = {
|
|
499
|
-
version: "2.
|
|
500
|
-
manager: "index.chunk-
|
|
501
|
-
sidebar: "index.chunk-
|
|
630
|
+
version: "2.37337.1",
|
|
631
|
+
manager: "index.chunk-BolytlRP.js",
|
|
632
|
+
sidebar: "index.chunk-BoQoqlAs.js",
|
|
502
633
|
hashes: {
|
|
503
|
-
"index.chunk-
|
|
504
|
-
"index.chunk-
|
|
505
|
-
"index.chunk-
|
|
634
|
+
"index.chunk-BolytlRP.js": "e387edfb06a34a59d99b54c04365f6e32d0108e8710124b4d1a628eea726d030",
|
|
635
|
+
"index.chunk-BoQoqlAs.js": "5c07a7e836d98dcd6afcfc2a62619e71d173a136ecb9af847716ea377b58b1df",
|
|
636
|
+
"index.chunk-BWk6pVUt.js": "f7d74e113f34761a675816fe2abf65b3ddc190327ee7623d9c822f7b59cceaca"
|
|
506
637
|
}
|
|
507
638
|
};
|
|
508
639
|
function validateInspectorEndpoint(endpoint) {
|
|
@@ -615,6 +746,7 @@ function prepareClaudeNativePlan(store, pack, policyPath, mode = "configure") {
|
|
|
615
746
|
contextInventories,
|
|
616
747
|
approvalRoutes,
|
|
617
748
|
installationConsent: policy.installationConsent,
|
|
749
|
+
libraryRoot: root,
|
|
618
750
|
sourceRevision: policy.sourceRevision,
|
|
619
751
|
mode,
|
|
620
752
|
activeBefore: active
|
|
@@ -630,6 +762,9 @@ function qualifyPermissionConsent(policy, key, records, full) {
|
|
|
630
762
|
function nativePackExpression(pid, plan, journal, apply) {
|
|
631
763
|
if (!Number.isSafeInteger(pid) || pid < 1) throw new Error("Explicit Claude main-process PID required.");
|
|
632
764
|
const qualifier = CLAUDE_NATIVE_QUALIFIER;
|
|
765
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
766
|
+
const smokeSource = [resolve(here, "../../agent-packs/src"), resolve(here, "../agent-packs/src")].find(existsSync);
|
|
767
|
+
if (!smokeSource) throw new Error("Bundled owned smoke source unavailable.");
|
|
633
768
|
return `(async () => {
|
|
634
769
|
const expected = ${JSON.stringify({ pid, qualifier, journal, apply })};
|
|
635
770
|
if (process.pid !== expected.pid || typeof process.getBuiltinModule !== 'function') throw new Error('Claude process identity or Node runtime differs.');
|
|
@@ -659,14 +794,24 @@ function nativePackExpression(pid, plan, journal, apply) {
|
|
|
659
794
|
throw new Error('Context or transcript changed during configuration: ' + file);
|
|
660
795
|
};
|
|
661
796
|
const result = await (${nativeClaudePackOperation.toString()})(manager,
|
|
662
|
-
value => transport.
|
|
797
|
+
value => transport.P(value, transport.M),
|
|
663
798
|
{ members: ${JSON.stringify(plan.members)}, outgoing: ${JSON.stringify(plan.outgoing)},
|
|
664
799
|
operationId: ${JSON.stringify(plan.operationId)}, mode: ${JSON.stringify(plan.recovery ? "recover" : plan.mode)},
|
|
665
800
|
noEffects: ${JSON.stringify(plan.noEffects)},
|
|
666
801
|
deadline: Date.now() + 45000 }, expected.apply, checkpoint, verifyContext);
|
|
667
802
|
${plan.approvalRoutes.length ? `const routes = ${JSON.stringify(plan.approvalRoutes)};
|
|
668
803
|
const fingerprint = value => crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
669
|
-
|
|
804
|
+
const qualifyNative = () => {
|
|
805
|
+
if (process.pid !== expected.pid || load('electron').app.getVersion() !== expected.qualifier.version) throw new Error('Native process changed.');
|
|
806
|
+
for (const [file, hash] of Object.entries(expected.qualifier.hashes))
|
|
807
|
+
if (crypto.createHash('sha256').update(fs.readFileSync(base + file)).digest('hex') !== hash) throw new Error('Native module changed.');
|
|
808
|
+
};
|
|
809
|
+
const smokeIdentity = fingerprint({pid: expected.pid, qualifier: expected.qualifier, routes});
|
|
810
|
+
const invoke = op => ['smoke-start', 'smoke-read'].includes(op.kind) ?
|
|
811
|
+
(${nativeClaudeOwnedSmokeOperation.toString()})(manager, ${JSON.stringify(plan.members)}, op,
|
|
812
|
+
(action, payload) => (${runClaudeSmokeHelper.toString()})(${JSON.stringify(plan.libraryRoot)}, action, payload,
|
|
813
|
+
${JSON.stringify(realpathSync(smokeSource))}, load('node:child_process').spawn), fingerprint, smokeIdentity, qualifyNative) :
|
|
814
|
+
(${nativeClaudeApprovalOperation.toString()})(manager, routes, op, fingerprint,
|
|
670
815
|
row => checkpoint({kind: 'approval', ...row}),
|
|
671
816
|
fs.readFileSync(expected.journal, 'utf8').split('\\n').filter(Boolean).map(line => JSON.parse(line)));
|
|
672
817
|
if (expected.apply) {
|
|
@@ -676,11 +821,6 @@ function nativePackExpression(pid, plan, journal, apply) {
|
|
|
676
821
|
const stat = fs.lstatSync(control.secretPath);
|
|
677
822
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== process.getuid() || (stat.mode & 63)) throw new Error('Unowned native control secret.');
|
|
678
823
|
const secret = JSON.parse(fs.readFileSync(control.secretPath, 'utf8'));
|
|
679
|
-
const qualifyNative = () => {
|
|
680
|
-
if (process.pid !== expected.pid || load('electron').app.getVersion() !== expected.qualifier.version) throw new Error('Native process changed.');
|
|
681
|
-
for (const [file, hash] of Object.entries(expected.qualifier.hashes))
|
|
682
|
-
if (crypto.createHash('sha256').update(fs.readFileSync(base + file)).digest('hex') !== hash) throw new Error('Native module changed.');
|
|
683
|
-
};
|
|
684
824
|
result.approvalRouting.control = await (${nativeClaudeApprovalControl.toString()})(manager,
|
|
685
825
|
{socket: control.socket, pid: expected.pid, identity: fingerprint(routes)}, secret.token,
|
|
686
826
|
load('node:net'), fs, invoke, qualifyNative, checkpoint);
|
|
@@ -935,6 +1075,47 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
|
|
|
935
1075
|
await release();
|
|
936
1076
|
}
|
|
937
1077
|
}
|
|
1078
|
+
function runClaudeSmokeHelper(root, action, payload, source, launch = spawn) {
|
|
1079
|
+
if (!["prepare", "arm", "mark", "read", "verify"].includes(action) || typeof root !== "string" || typeof source !== "string")
|
|
1080
|
+
return Promise.reject(new Error("Closed owned smoke helper required."));
|
|
1081
|
+
const script = 'import sys,json\nsys.path.insert(0,sys.argv[3])\nfrom topolo_agent_pack.storage import Library\nfrom topolo_agent_pack.live import OwnedSmoke\ns=OwnedSmoke(Library(sys.argv[1]),"claude")\np=json.loads(sys.stdin.read(16384))\na=sys.argv[2]\nr=s.prepare(**p) if a=="prepare" else s.arm(**p) if a=="arm" else s.mark(**p) if a=="mark" else s.verify(p["operation"]) if a=="verify" else s.read(p["operation"])\nprint(json.dumps(r))';
|
|
1082
|
+
return new Promise((done, fail) => {
|
|
1083
|
+
const child = launch("python3", ["-I", "-B", "-c", script, root, action, source], { stdio: ["pipe", "pipe", "pipe"] });
|
|
1084
|
+
let output = "";
|
|
1085
|
+
let settled = false;
|
|
1086
|
+
const finish = (error, value) => {
|
|
1087
|
+
if (settled) return;
|
|
1088
|
+
settled = true;
|
|
1089
|
+
clearTimeout(timer);
|
|
1090
|
+
error ? fail(error) : done(value);
|
|
1091
|
+
};
|
|
1092
|
+
const timer = setTimeout(() => {
|
|
1093
|
+
child.kill("SIGTERM");
|
|
1094
|
+
finish(new Error("Owned smoke helper timed out; reconcile without replay."));
|
|
1095
|
+
}, 6e4);
|
|
1096
|
+
child.stdout.setEncoding("utf8");
|
|
1097
|
+
child.stderr.resume();
|
|
1098
|
+
child.stdout.on("data", (data) => {
|
|
1099
|
+
output += data;
|
|
1100
|
+
if (Buffer.byteLength(output) > 1048576) {
|
|
1101
|
+
child.kill("SIGTERM");
|
|
1102
|
+
finish(new Error("Owned smoke evidence exceeds bounded transport."));
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
child.on("error", () => finish(new Error("Bundled owned smoke helper unavailable.")));
|
|
1106
|
+
child.on("close", (code) => {
|
|
1107
|
+
if (settled) return;
|
|
1108
|
+
if (code !== 0) return finish(new Error("Owned smoke qualification refused; preserve its journal."));
|
|
1109
|
+
try {
|
|
1110
|
+
finish(void 0, JSON.parse(output));
|
|
1111
|
+
} catch {
|
|
1112
|
+
finish(new Error("Invalid owned smoke helper result."));
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
child.stdin.on("error", () => finish(new Error("Owned smoke helper input failed; reconcile without replay.")));
|
|
1116
|
+
child.stdin.end(JSON.stringify(payload));
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
938
1119
|
function requestClaudeApprovalControl(connection, operation, token, connect = createConnection) {
|
|
939
1120
|
const parent = lstatSync(dirname(connection.socket));
|
|
940
1121
|
const socket = lstatSync(connection.socket);
|
|
@@ -951,7 +1132,7 @@ function requestClaudeApprovalControl(connection, operation, token, connect = cr
|
|
|
951
1132
|
error ? fail(error) : done(result);
|
|
952
1133
|
};
|
|
953
1134
|
client.setEncoding("utf8");
|
|
954
|
-
client.setTimeout(1e4, () => finish(new Error("Native approval response timed out; reconcile without replay.")));
|
|
1135
|
+
client.setTimeout(operation.kind === "smoke-start" ? 6e4 : 1e4, () => finish(new Error("Native approval response timed out; reconcile without replay.")));
|
|
955
1136
|
client.on("connect", () => client.write(JSON.stringify({ token, pid: connection.pid, identity: connection.identity, operation }) + "\n"));
|
|
956
1137
|
client.on("data", (chunk) => {
|
|
957
1138
|
reply += chunk;
|
|
@@ -985,6 +1166,15 @@ async function operateClaudeApprovals(options, request = requestClaudeApprovalCo
|
|
|
985
1166
|
if (!secretPath.startsWith(realpathSync(privateDirectory) + sep) || !stat.isFile() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || stat.mode & 63) throw new Error("Unowned native control secret.");
|
|
986
1167
|
const secret = JSON.parse(readFileSync(secretPath, "utf8"));
|
|
987
1168
|
if (!/^[a-f0-9]{64}$/.test(secret.token) || !/^[a-f0-9]{64}$/.test(connection.identity)) throw new Error("Native control identity is invalid.");
|
|
1169
|
+
if (options.kind === "smoke-start" || options.kind === "smoke-read") return request(
|
|
1170
|
+
connection,
|
|
1171
|
+
{
|
|
1172
|
+
kind: options.kind,
|
|
1173
|
+
operationId: options.operationId,
|
|
1174
|
+
...options.kind === "smoke-start" ? { key: options.key, caseId: options.caseId } : {}
|
|
1175
|
+
},
|
|
1176
|
+
secret.token
|
|
1177
|
+
);
|
|
988
1178
|
return request(connection, {
|
|
989
1179
|
kind: options.kind,
|
|
990
1180
|
key: options.key,
|
|
@@ -994,6 +1184,12 @@ async function operateClaudeApprovals(options, request = requestClaudeApprovalCo
|
|
|
994
1184
|
humanDecisionRecorded: options.humanDecisionRecorded
|
|
995
1185
|
}, secret.token);
|
|
996
1186
|
}
|
|
1187
|
+
async function operateClaudeSmoke(options, request = requestClaudeApprovalControl) {
|
|
1188
|
+
const fields = options.kind === "smoke-start" ? ["store", "kind", "operationId", "key", "caseId"] : ["store", "kind", "operationId"];
|
|
1189
|
+
if (!["smoke-start", "smoke-read"].includes(options.kind) || Object.keys(options).some((k) => !fields.includes(k)) || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(options.operationId) || options.kind === "smoke-start" && (!/^[a-z0-9-]+\/[a-z0-9-]+\/[a-z0-9-]+$/.test(options.key ?? "") || !["stg-context-memory-bytes", "stg-peer-ack", "prod-role-context-ack"].includes(options.caseId ?? "")))
|
|
1190
|
+
throw new Error("Exact closed native smoke fields required.");
|
|
1191
|
+
return operateClaudeApprovals(options, request);
|
|
1192
|
+
}
|
|
997
1193
|
export {
|
|
998
1194
|
CLAUDE_NATIVE_QUALIFIER,
|
|
999
1195
|
configureClaudePack,
|
|
@@ -1001,8 +1197,10 @@ export {
|
|
|
1001
1197
|
lockClaudeLibrary,
|
|
1002
1198
|
nativePackExpression,
|
|
1003
1199
|
operateClaudeApprovals,
|
|
1200
|
+
operateClaudeSmoke,
|
|
1004
1201
|
prepareClaudeNativePlan,
|
|
1005
1202
|
requestClaudeApprovalControl,
|
|
1203
|
+
runClaudeSmokeHelper,
|
|
1006
1204
|
snapshotClaudeMarkdown,
|
|
1007
1205
|
validateInspectorEndpoint,
|
|
1008
1206
|
verifyClaudeOrganizationPolicy
|
package/dist/index.js
CHANGED
|
@@ -8,9 +8,43 @@ import { fileURLToPath } from "url";
|
|
|
8
8
|
import { homedir } from "os";
|
|
9
9
|
function registerAgentPacksCommand(program2) {
|
|
10
10
|
const packs = program2.command("agent-packs").description("Install and operate Topolo Agent Packs in supported local hosts").option("--store <path>", "Portable pack library (default ~/AgentPacks)");
|
|
11
|
+
packs.command("smoke").description("Start or read one closed, bound native acceptance case").argument("<operation>", "start or read").argument("<identity>", "Owned role key for start, existing operation UUID for read").option("--case <id>", "Finite compiled acceptance case").option("--operation-id <id>", "Unique native operation UUID for start").option("--harness <name>", "codex or claude", "codex").allowExcessArguments(false).action(async (operation, identity, options) => {
|
|
12
|
+
const uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
13
|
+
if (!["start", "read"].includes(operation) || !["codex", "claude"].includes(options.harness) || (operation === "start" ? !/^[a-z0-9-]+\/[a-z0-9-]+\/[a-z0-9-]+$/.test(identity) || !["stg-context-memory-bytes", "stg-peer-ack", "prod-role-context-ack"].includes(options.case ?? "") || !uuid.test(options.operationId ?? "") : !uuid.test(identity) || options.case !== void 0 || options.operationId !== void 0))
|
|
14
|
+
throw new Error("Exact closed smoke operation, role/case and UUID required.");
|
|
15
|
+
const store = packs.opts().store ?? resolve(homedir(), "AgentPacks");
|
|
16
|
+
if (options.harness === "claude") {
|
|
17
|
+
const { operateClaudeSmoke } = await import("./claude-pack-backend-3YJNKMYD.js");
|
|
18
|
+
console.log(JSON.stringify(await operateClaudeSmoke({
|
|
19
|
+
store,
|
|
20
|
+
kind: "smoke-" + operation,
|
|
21
|
+
operationId: operation === "start" ? options.operationId : identity,
|
|
22
|
+
...operation === "start" ? { key: identity, caseId: options.case } : {}
|
|
23
|
+
}), null, 2));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const adapter = [resolve(here, "../agent-packs/topolo-agent-pack"), resolve(here, "../../agent-packs/topolo-agent-pack")].find(existsSync);
|
|
28
|
+
if (!adapter) throw new Error("Installed bundled pack adapter unavailable.");
|
|
29
|
+
const result = spawnSync(
|
|
30
|
+
"python3",
|
|
31
|
+
[
|
|
32
|
+
adapter,
|
|
33
|
+
"--store",
|
|
34
|
+
store,
|
|
35
|
+
"smoke",
|
|
36
|
+
operation,
|
|
37
|
+
identity,
|
|
38
|
+
...operation === "start" ? ["--case", options.case, "--operation-id", options.operationId] : []
|
|
39
|
+
],
|
|
40
|
+
{ stdio: "inherit", env: { ...process.env, PYTHONDONTWRITEBYTECODE: "1", TOPOLO_AGENT_PACK_PUBLIC_CLI: "1" } }
|
|
41
|
+
);
|
|
42
|
+
if (result.error) throw new Error("Python 3.11 or later is required for local packs.");
|
|
43
|
+
if (result.status !== 0) process.exitCode = result.status ?? 2;
|
|
44
|
+
});
|
|
11
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) => {
|
|
12
46
|
if (options.harness !== "claude") throw new Error("Native configure is currently qualified only for Claude Desktop.");
|
|
13
|
-
const { configureClaudePack } = await import("./claude-pack-backend-
|
|
47
|
+
const { configureClaudePack } = await import("./claude-pack-backend-3YJNKMYD.js");
|
|
14
48
|
const result = await configureClaudePack({
|
|
15
49
|
store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
|
|
16
50
|
pack,
|
|
@@ -62,7 +96,7 @@ function registerAgentPacksCommand(program2) {
|
|
|
62
96
|
throw new Error("Only a one-request once/deny response is supported.");
|
|
63
97
|
decision = response.decision;
|
|
64
98
|
}
|
|
65
|
-
const { operateClaudeApprovals } = await import("./claude-pack-backend-
|
|
99
|
+
const { operateClaudeApprovals } = await import("./claude-pack-backend-3YJNKMYD.js");
|
|
66
100
|
console.log(JSON.stringify(await operateClaudeApprovals({
|
|
67
101
|
store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
|
|
68
102
|
kind: args[0],
|
|
@@ -77,7 +111,7 @@ function registerAgentPacksCommand(program2) {
|
|
|
77
111
|
if (options.policy || options.endpoint || options.pid || options.apply) {
|
|
78
112
|
if (options.harness !== "claude" || !options.policy || !options.endpoint || !options.pid || args.length !== 1)
|
|
79
113
|
throw new Error("Native switch/recovery requires one Claude pack, --policy, --endpoint and --pid.");
|
|
80
|
-
const { configureClaudePack } = await import("./claude-pack-backend-
|
|
114
|
+
const { configureClaudePack } = await import("./claude-pack-backend-3YJNKMYD.js");
|
|
81
115
|
const result2 = await configureClaudePack({
|
|
82
116
|
store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
|
|
83
117
|
pack: args[0],
|
|
@@ -124,7 +158,7 @@ import { platform as platform2 } from "os";
|
|
|
124
158
|
import { createInterface } from "readline/promises";
|
|
125
159
|
import { stdin as input } from "process";
|
|
126
160
|
|
|
127
|
-
// node_modules/@topolo/sdk/dist/index.js
|
|
161
|
+
// node_modules/.pnpm/@topolo+sdk@0.23.19/node_modules/@topolo/sdk/dist/index.js
|
|
128
162
|
function record(value) {
|
|
129
163
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
130
164
|
}
|
|
@@ -3734,10 +3768,10 @@ async function pollUntilApproved(params) {
|
|
|
3734
3768
|
throw new Error("Device code expired. Run `topolo auth login` again.");
|
|
3735
3769
|
}
|
|
3736
3770
|
|
|
3737
|
-
// node_modules/@topolo/sdk/dist/node/credential-store.js
|
|
3771
|
+
// node_modules/.pnpm/@topolo+sdk@0.23.19/node_modules/@topolo/sdk/dist/node/credential-store.js
|
|
3738
3772
|
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, lstat, readdir as readdir2, rename as rename2, link } from "fs/promises";
|
|
3739
|
-
import { existsSync as existsSync2, lstatSync, readdirSync, readFileSync as readFileSync2, openSync, writeFileSync, fsyncSync, closeSync, renameSync, unlinkSync, rmdirSync } from "fs";
|
|
3740
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
3773
|
+
import { existsSync as existsSync2, mkdirSync, lstatSync, readdirSync, readFileSync as readFileSync2, openSync, writeFileSync, fsyncSync, closeSync, renameSync, unlinkSync, rmdirSync, linkSync } from "fs";
|
|
3774
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
3741
3775
|
import { homedir as homedir2, platform } from "os";
|
|
3742
3776
|
import { dirname as dirname2, join as join3 } from "path";
|
|
3743
3777
|
import { createHash, randomUUID } from "crypto";
|
|
@@ -4608,20 +4642,33 @@ var ConfigOwnershipError = class extends TopoloSdkError2 {
|
|
|
4608
4642
|
super("state_conflict", "Credential configuration ownership cannot be verified. Retain the unresolved operation instead of starting another renewal.");
|
|
4609
4643
|
}
|
|
4610
4644
|
};
|
|
4645
|
+
function lockEntryExists(path) {
|
|
4646
|
+
try {
|
|
4647
|
+
lstatSync(path);
|
|
4648
|
+
return true;
|
|
4649
|
+
} catch (error) {
|
|
4650
|
+
if (error.code === "ENOENT") return false;
|
|
4651
|
+
throw new ConfigOwnershipError();
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4611
4654
|
async function withConfigWriteLock(operation) {
|
|
4612
4655
|
const lockPath = `${configPath()}.lock`;
|
|
4613
4656
|
await mkdir2(dirname2(lockPath), { recursive: true });
|
|
4614
4657
|
const startedAt = performance.now();
|
|
4615
4658
|
while (true) {
|
|
4616
4659
|
try {
|
|
4660
|
+
if (lockEntryExists(lockPath + ".reconcile")) throw Object.assign(new Error(), { code: "EEXIST" });
|
|
4617
4661
|
await mkdir2(lockPath, { mode: 448 });
|
|
4662
|
+
if (lockEntryExists(lockPath + ".reconcile")) {
|
|
4663
|
+
throw new ConfigOwnershipError();
|
|
4664
|
+
}
|
|
4618
4665
|
break;
|
|
4619
4666
|
} catch (error) {
|
|
4620
4667
|
if (error.code !== "EEXIST") throw error;
|
|
4621
4668
|
if (performance.now() - startedAt >= CONFIG_LOCK_TIMEOUT_MS) {
|
|
4622
4669
|
throw new TopoloSdkError2("state_conflict", "Credential configuration is still held or unresolved. This waiter did not dispatch a renewal.");
|
|
4623
4670
|
}
|
|
4624
|
-
await new Promise((
|
|
4671
|
+
await new Promise((resolve22) => setTimeout(resolve22, CONFIG_LOCK_RETRY_MS));
|
|
4625
4672
|
}
|
|
4626
4673
|
}
|
|
4627
4674
|
const directory = await lstat(lockPath);
|
|
@@ -4632,7 +4679,7 @@ async function withConfigWriteLock(operation) {
|
|
|
4632
4679
|
await writeFile2(markerPath, marker, { flag: "wx", mode: 384 });
|
|
4633
4680
|
const assertDirectory = async () => {
|
|
4634
4681
|
const current = await lstat(lockPath);
|
|
4635
|
-
if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== directory.dev || current.ino !== directory.ino)
|
|
4682
|
+
if (lockEntryExists(lockPath + ".reconcile") || !current.isDirectory() || current.isSymbolicLink() || current.dev !== directory.dev || current.ino !== directory.ino)
|
|
4636
4683
|
throw new ConfigOwnershipError();
|
|
4637
4684
|
};
|
|
4638
4685
|
const assertOwned = async () => {
|
|
@@ -4649,7 +4696,7 @@ async function withConfigWriteLock(operation) {
|
|
|
4649
4696
|
};
|
|
4650
4697
|
const assertDirectorySync = () => {
|
|
4651
4698
|
const current = lstatSync(lockPath);
|
|
4652
|
-
if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== directory.dev || current.ino !== directory.ino)
|
|
4699
|
+
if (lockEntryExists(lockPath + ".reconcile") || !current.isDirectory() || current.isSymbolicLink() || current.dev !== directory.dev || current.ino !== directory.ino)
|
|
4653
4700
|
throw new ConfigOwnershipError();
|
|
4654
4701
|
};
|
|
4655
4702
|
const assertOwnedSync = () => {
|
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.7",
|
|
5
|
+
"topoloPublishedVersion": "0.13.7",
|
|
6
6
|
"description": "Public command-line interface for Topolo customer organizations, developers, and agents.",
|
|
7
7
|
"license": "UNLICENSED",
|
|
8
8
|
"author": "Topolo",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
34
|
-
"@topolo/mcp": "0.14.
|
|
35
|
-
"@topolo/sdk": "0.23.
|
|
34
|
+
"@topolo/mcp": "0.14.24",
|
|
35
|
+
"@topolo/sdk": "0.23.19",
|
|
36
36
|
"commander": "^12.1.0",
|
|
37
37
|
"undici": "6.29.0"
|
|
38
38
|
},
|