@topolo/cli 0.13.6 → 0.13.8
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/.pnpm/@topolo+sdk@0.23.
|
|
161
|
+
// node_modules/.pnpm/@topolo+sdk@0.23.20/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,11 @@ async function pollUntilApproved(params) {
|
|
|
3734
3768
|
throw new Error("Device code expired. Run `topolo auth login` again.");
|
|
3735
3769
|
}
|
|
3736
3770
|
|
|
3737
|
-
// node_modules/.pnpm/@topolo+sdk@0.23.
|
|
3738
|
-
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, lstat, readdir as readdir2
|
|
3771
|
+
// node_modules/.pnpm/@topolo+sdk@0.23.20/node_modules/@topolo/sdk/dist/node/credential-store.js
|
|
3772
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, lstat, readdir as readdir2 } from "fs/promises";
|
|
3739
3773
|
import { existsSync as existsSync2, mkdirSync, lstatSync, readdirSync, readFileSync as readFileSync2, openSync, writeFileSync, fsyncSync, closeSync, renameSync, unlinkSync, rmdirSync, linkSync } from "fs";
|
|
3740
3774
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
3775
|
+
import { constants, fstatSync, readSync } from "fs";
|
|
3741
3776
|
import { homedir as homedir2, platform } from "os";
|
|
3742
3777
|
import { dirname as dirname2, join as join3 } from "path";
|
|
3743
3778
|
import { createHash, randomUUID } from "crypto";
|
|
@@ -4603,6 +4638,7 @@ async function resolveCredential(environment = selectedEnvironment()) {
|
|
|
4603
4638
|
var REFRESH_SKEW_SECONDS = 60;
|
|
4604
4639
|
var CONFIG_LOCK_RETRY_MS = 25;
|
|
4605
4640
|
var CONFIG_LOCK_TIMEOUT_MS = 5e3;
|
|
4641
|
+
var CONFIG_LOCK_DEAD_OWNER_MIN_AGE_MS = 2e3;
|
|
4606
4642
|
var ConfigOwnershipError = class extends TopoloSdkError2 {
|
|
4607
4643
|
constructor() {
|
|
4608
4644
|
super("state_conflict", "Credential configuration ownership cannot be verified. Retain the unresolved operation instead of starting another renewal.");
|
|
@@ -4617,32 +4653,145 @@ function lockEntryExists(path) {
|
|
|
4617
4653
|
throw new ConfigOwnershipError();
|
|
4618
4654
|
}
|
|
4619
4655
|
}
|
|
4656
|
+
function lockIdentity(entry) {
|
|
4657
|
+
return [entry.dev, entry.ino, entry.uid, entry.mode, entry.nlink, entry.size, entry.mtimeMs, entry.ctimeMs];
|
|
4658
|
+
}
|
|
4659
|
+
function privateLockDirectory(path) {
|
|
4660
|
+
const entry = lstatSync(path);
|
|
4661
|
+
if (typeof process.getuid !== "function" || !entry.isDirectory() || entry.isSymbolicLink() || entry.uid !== process.getuid() || (entry.mode & 511) !== 448) throw new ConfigOwnershipError();
|
|
4662
|
+
return entry;
|
|
4663
|
+
}
|
|
4664
|
+
function readPrivateLockMarker(path) {
|
|
4665
|
+
const before = lstatSync(path);
|
|
4666
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || before.size > 512 || before.uid !== process.getuid?.() || (before.mode & 511) !== 384) throw new ConfigOwnershipError();
|
|
4667
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
4668
|
+
try {
|
|
4669
|
+
if (JSON.stringify(lockIdentity(fstatSync(fd))) !== JSON.stringify(lockIdentity(before))) throw new ConfigOwnershipError();
|
|
4670
|
+
const bytes = Buffer.alloc(513);
|
|
4671
|
+
const count = readSync(fd, bytes, 0, bytes.length, 0);
|
|
4672
|
+
if (count !== before.size || count > 512 || JSON.stringify(lockIdentity(fstatSync(fd))) !== JSON.stringify(lockIdentity(before)) || JSON.stringify(lockIdentity(lstatSync(path))) !== JSON.stringify(lockIdentity(before))) throw new ConfigOwnershipError();
|
|
4673
|
+
return bytes.subarray(0, count).toString("utf8");
|
|
4674
|
+
} finally {
|
|
4675
|
+
closeSync(fd);
|
|
4676
|
+
}
|
|
4677
|
+
}
|
|
4678
|
+
var OWNER_MARKER_NAME = /^owner-[a-f0-9-]{36}\.json$/;
|
|
4679
|
+
function readOwnerMarker(lockPath, name) {
|
|
4680
|
+
const bytes = readPrivateLockMarker(join3(lockPath, name));
|
|
4681
|
+
const owner = JSON.parse(bytes);
|
|
4682
|
+
if (owner.version !== 1 || Object.keys(owner).sort().join(",") !== "identity,pid,version" || name !== "owner-" + owner.identity + ".json" || !Number.isSafeInteger(owner.pid) || owner.pid <= 0) throw new ConfigOwnershipError();
|
|
4683
|
+
return { bytes, pid: owner.pid };
|
|
4684
|
+
}
|
|
4685
|
+
function writePrivateLockRecord(path, bytes) {
|
|
4686
|
+
const fd = openSync(path, "wx", 384);
|
|
4687
|
+
try {
|
|
4688
|
+
writeFileSync(fd, bytes);
|
|
4689
|
+
fsyncSync(fd);
|
|
4690
|
+
} finally {
|
|
4691
|
+
closeSync(fd);
|
|
4692
|
+
}
|
|
4693
|
+
}
|
|
4694
|
+
function deadOwnerMarker(lockPath, ignoreOwnFence = false) {
|
|
4695
|
+
try {
|
|
4696
|
+
if (!ignoreOwnFence && lockEntryExists(lockPath + ".reconcile")) return null;
|
|
4697
|
+
const directory = privateLockDirectory(lockPath);
|
|
4698
|
+
const entries = readdirSync(lockPath);
|
|
4699
|
+
if (entries.length !== 1 || !OWNER_MARKER_NAME.test(entries[0])) return null;
|
|
4700
|
+
const name = entries[0];
|
|
4701
|
+
const { bytes, pid } = readOwnerMarker(lockPath, name);
|
|
4702
|
+
if (Date.now() - lstatSync(join3(lockPath, name)).mtimeMs < CONFIG_LOCK_DEAD_OWNER_MIN_AGE_MS) return null;
|
|
4703
|
+
try {
|
|
4704
|
+
process.kill(pid, 0);
|
|
4705
|
+
return null;
|
|
4706
|
+
} catch (error) {
|
|
4707
|
+
if (error.code !== "ESRCH") return null;
|
|
4708
|
+
}
|
|
4709
|
+
return { directory, name, bytes, pid };
|
|
4710
|
+
} catch {
|
|
4711
|
+
return null;
|
|
4712
|
+
}
|
|
4713
|
+
}
|
|
4714
|
+
function reclaimDeadOwnerLock(lockPath, markerName, marker) {
|
|
4715
|
+
const observed = deadOwnerMarker(lockPath);
|
|
4716
|
+
if (!observed) return false;
|
|
4717
|
+
const fence = lockPath + ".reconcile";
|
|
4718
|
+
try {
|
|
4719
|
+
mkdirSync(fence, { mode: 448 });
|
|
4720
|
+
} catch {
|
|
4721
|
+
return false;
|
|
4722
|
+
}
|
|
4723
|
+
const fenceMarker = join3(fence, markerName);
|
|
4724
|
+
const quarantine = join3(dirname2(lockPath), ".credential-reclaim-" + randomUUID2());
|
|
4725
|
+
let captured;
|
|
4726
|
+
let reclaimed = false;
|
|
4727
|
+
try {
|
|
4728
|
+
writePrivateLockRecord(fenceMarker, marker);
|
|
4729
|
+
const current = deadOwnerMarker(lockPath, true);
|
|
4730
|
+
if (!current || current.name !== observed.name || current.bytes !== observed.bytes || current.directory.dev !== observed.directory.dev || current.directory.ino !== observed.directory.ino) return false;
|
|
4731
|
+
mkdirSync(quarantine, { mode: 448 });
|
|
4732
|
+
writePrivateLockRecord(join3(quarantine, "receipt.json"), JSON.stringify({
|
|
4733
|
+
schemaVersion: 1,
|
|
4734
|
+
disposition: "DEAD_OWNER_RECLAIMED",
|
|
4735
|
+
lockPath,
|
|
4736
|
+
deadOwnerMarker: current.name,
|
|
4737
|
+
deadOwnerPid: current.pid,
|
|
4738
|
+
deadOwnerEvidence: "ESRCH",
|
|
4739
|
+
reclaimedByPid: process.pid,
|
|
4740
|
+
reclaimedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4741
|
+
configCredentialOrOAuthEffectsByThisReclaim: "none"
|
|
4742
|
+
}, null, 2));
|
|
4743
|
+
captured = join3(quarantine, current.name);
|
|
4744
|
+
renameSync(join3(lockPath, current.name), captured);
|
|
4745
|
+
const directory = privateLockDirectory(lockPath);
|
|
4746
|
+
if (directory.dev !== observed.directory.dev || directory.ino !== observed.directory.ino || readdirSync(lockPath).length) throw new ConfigOwnershipError();
|
|
4747
|
+
writePrivateLockRecord(join3(lockPath, markerName), marker);
|
|
4748
|
+
reclaimed = true;
|
|
4749
|
+
} catch {
|
|
4750
|
+
if (captured && !reclaimed) {
|
|
4751
|
+
try {
|
|
4752
|
+
linkSync(captured, join3(lockPath, observed.name));
|
|
4753
|
+
} catch {
|
|
4754
|
+
}
|
|
4755
|
+
}
|
|
4756
|
+
} finally {
|
|
4757
|
+
try {
|
|
4758
|
+
if (readPrivateLockMarker(fenceMarker) !== marker || readdirSync(fence).length !== 1) throw new ConfigOwnershipError();
|
|
4759
|
+
unlinkSync(fenceMarker);
|
|
4760
|
+
rmdirSync(fence);
|
|
4761
|
+
} catch {
|
|
4762
|
+
reclaimed = false;
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4765
|
+
return reclaimed;
|
|
4766
|
+
}
|
|
4767
|
+
var RELEASE_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
4620
4768
|
async function withConfigWriteLock(operation) {
|
|
4621
4769
|
const lockPath = `${configPath()}.lock`;
|
|
4622
4770
|
await mkdir2(dirname2(lockPath), { recursive: true });
|
|
4623
4771
|
const startedAt = performance.now();
|
|
4772
|
+
const identity = randomUUID2();
|
|
4773
|
+
const markerName = "owner-" + identity + ".json";
|
|
4774
|
+
const markerPath = join3(lockPath, markerName);
|
|
4775
|
+
const marker = JSON.stringify({ version: 1, identity, pid: process.pid });
|
|
4624
4776
|
while (true) {
|
|
4625
4777
|
try {
|
|
4626
4778
|
if (lockEntryExists(lockPath + ".reconcile")) throw Object.assign(new Error(), { code: "EEXIST" });
|
|
4627
|
-
|
|
4779
|
+
mkdirSync(lockPath, { mode: 448 });
|
|
4628
4780
|
if (lockEntryExists(lockPath + ".reconcile")) {
|
|
4629
4781
|
throw new ConfigOwnershipError();
|
|
4630
4782
|
}
|
|
4783
|
+
writeFileSync(markerPath, marker, { flag: "wx", mode: 384 });
|
|
4631
4784
|
break;
|
|
4632
4785
|
} catch (error) {
|
|
4633
4786
|
if (error.code !== "EEXIST") throw error;
|
|
4787
|
+
if (reclaimDeadOwnerLock(lockPath, markerName, marker)) break;
|
|
4634
4788
|
if (performance.now() - startedAt >= CONFIG_LOCK_TIMEOUT_MS) {
|
|
4635
4789
|
throw new TopoloSdkError2("state_conflict", "Credential configuration is still held or unresolved. This waiter did not dispatch a renewal.");
|
|
4636
4790
|
}
|
|
4637
4791
|
await new Promise((resolve22) => setTimeout(resolve22, CONFIG_LOCK_RETRY_MS));
|
|
4638
4792
|
}
|
|
4639
4793
|
}
|
|
4640
|
-
const directory =
|
|
4641
|
-
const identity = randomUUID2();
|
|
4642
|
-
const markerName = "owner-" + identity + ".json";
|
|
4643
|
-
const markerPath = join3(lockPath, markerName);
|
|
4644
|
-
const marker = JSON.stringify({ version: 1, identity, pid: process.pid });
|
|
4645
|
-
await writeFile2(markerPath, marker, { flag: "wx", mode: 384 });
|
|
4794
|
+
const directory = lstatSync(lockPath);
|
|
4646
4795
|
const assertDirectory = async () => {
|
|
4647
4796
|
const current = await lstat(lockPath);
|
|
4648
4797
|
if (lockEntryExists(lockPath + ".reconcile") || !current.isDirectory() || current.isSymbolicLink() || current.dev !== directory.dev || current.ino !== directory.ino)
|
|
@@ -4666,10 +4815,14 @@ async function withConfigWriteLock(operation) {
|
|
|
4666
4815
|
throw new ConfigOwnershipError();
|
|
4667
4816
|
};
|
|
4668
4817
|
const assertOwnedSync = () => {
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4818
|
+
try {
|
|
4819
|
+
assertDirectorySync();
|
|
4820
|
+
const entries = readdirSync(lockPath);
|
|
4821
|
+
const entry = lstatSync(markerPath);
|
|
4822
|
+
if (entries.length !== 1 || entries[0] !== markerName || !entry.isFile() || entry.isSymbolicLink() || readFileSync2(markerPath, "utf8") !== marker) throw new ConfigOwnershipError();
|
|
4823
|
+
} catch {
|
|
4824
|
+
throw new ConfigOwnershipError();
|
|
4825
|
+
}
|
|
4673
4826
|
};
|
|
4674
4827
|
const persist = async (update) => {
|
|
4675
4828
|
const path = configPath();
|
|
@@ -4698,14 +4851,14 @@ async function withConfigWriteLock(operation) {
|
|
|
4698
4851
|
if (temporaryOwned) unlinkSync(temporary);
|
|
4699
4852
|
}
|
|
4700
4853
|
};
|
|
4701
|
-
const release =
|
|
4702
|
-
|
|
4854
|
+
const release = () => {
|
|
4855
|
+
assertOwnedSync();
|
|
4703
4856
|
const quarantine = join3(dirname2(lockPath), ".credential-release-" + randomUUID2());
|
|
4704
|
-
|
|
4857
|
+
mkdirSync(quarantine, { mode: 448 });
|
|
4705
4858
|
const captured = join3(quarantine, markerName);
|
|
4706
4859
|
try {
|
|
4707
|
-
|
|
4708
|
-
|
|
4860
|
+
assertOwnedSync();
|
|
4861
|
+
renameSync(markerPath, captured);
|
|
4709
4862
|
assertDirectorySync();
|
|
4710
4863
|
const entry = lstatSync(captured);
|
|
4711
4864
|
if (!entry.isFile() || entry.isSymbolicLink() || readFileSync2(captured, "utf8") !== marker || readdirSync(lockPath).length) throw new ConfigOwnershipError();
|
|
@@ -4717,20 +4870,40 @@ async function withConfigWriteLock(operation) {
|
|
|
4717
4870
|
} catch {
|
|
4718
4871
|
if (existsSync2(captured)) {
|
|
4719
4872
|
try {
|
|
4720
|
-
|
|
4873
|
+
linkSync(captured, markerPath);
|
|
4721
4874
|
} catch {
|
|
4722
4875
|
}
|
|
4723
4876
|
}
|
|
4724
4877
|
throw new ConfigOwnershipError();
|
|
4725
4878
|
}
|
|
4726
4879
|
};
|
|
4880
|
+
let held = true;
|
|
4881
|
+
const releaseOnce = () => {
|
|
4882
|
+
if (!held) return;
|
|
4883
|
+
held = false;
|
|
4884
|
+
process.off("exit", onExit);
|
|
4885
|
+
for (const signal of RELEASE_SIGNALS) process.off(signal, onSignal);
|
|
4886
|
+
release();
|
|
4887
|
+
};
|
|
4888
|
+
const onExit = () => {
|
|
4889
|
+
try {
|
|
4890
|
+
releaseOnce();
|
|
4891
|
+
} catch {
|
|
4892
|
+
}
|
|
4893
|
+
};
|
|
4894
|
+
const onSignal = (signal) => {
|
|
4895
|
+
onExit();
|
|
4896
|
+
if (process.listenerCount(signal) === 0) process.kill(process.pid, signal);
|
|
4897
|
+
};
|
|
4898
|
+
process.on("exit", onExit);
|
|
4899
|
+
for (const signal of RELEASE_SIGNALS) process.on(signal, onSignal);
|
|
4727
4900
|
try {
|
|
4728
4901
|
await assertOwned();
|
|
4729
4902
|
const result = await operation(assertOwned, persist);
|
|
4730
4903
|
await assertOwned();
|
|
4731
4904
|
return result;
|
|
4732
4905
|
} finally {
|
|
4733
|
-
|
|
4906
|
+
releaseOnce();
|
|
4734
4907
|
}
|
|
4735
4908
|
}
|
|
4736
4909
|
function renewalFailure(error) {
|
|
@@ -6623,6 +6796,7 @@ async function readGooseRegistrations(path) {
|
|
|
6623
6796
|
continue;
|
|
6624
6797
|
}
|
|
6625
6798
|
if (inExtensions && /^\S/.test(rawLine) && rawLine.trim()) {
|
|
6799
|
+
if (current?.command) registrations.push(current);
|
|
6626
6800
|
inExtensions = false;
|
|
6627
6801
|
inTopolo = false;
|
|
6628
6802
|
current = void 0;
|
|
@@ -6630,6 +6804,7 @@ async function readGooseRegistrations(path) {
|
|
|
6630
6804
|
if (!inExtensions) continue;
|
|
6631
6805
|
const extension = rawLine.match(/^ ([A-Za-z0-9_-]+):\s*$/);
|
|
6632
6806
|
if (extension) {
|
|
6807
|
+
if (current?.command) registrations.push(current);
|
|
6633
6808
|
inTopolo = true;
|
|
6634
6809
|
current = isPublicRegistrationName(extension[1]) ? { name: extension[1], command: "", args: [], env: {} } : void 0;
|
|
6635
6810
|
continue;
|
|
@@ -6807,7 +6982,7 @@ function message(error) {
|
|
|
6807
6982
|
|
|
6808
6983
|
// src/commands/agent-config.ts
|
|
6809
6984
|
import { existsSync as existsSync5 } from "fs";
|
|
6810
|
-
import { mkdir as mkdir3, readFile as readFile5, rename as
|
|
6985
|
+
import { mkdir as mkdir3, readFile as readFile5, rename as rename2, writeFile as writeFile3 } from "fs/promises";
|
|
6811
6986
|
import { dirname as dirname5 } from "path";
|
|
6812
6987
|
async function installAgentSkill(agent, source, homeDir, projectDir) {
|
|
6813
6988
|
const path = agentSkillPath(agent, homeDir, projectDir);
|
|
@@ -6843,6 +7018,7 @@ async function unregisterAgentConfig(agent, homeDir, name, projectDir) {
|
|
|
6843
7018
|
function registrationEnvironment(agent, registration) {
|
|
6844
7019
|
return {
|
|
6845
7020
|
TOPOLO_AGENT_NAME: AGENT_NAME_FOR[agent],
|
|
7021
|
+
...registration.configPath ? { TOPOLO_CONFIG_PATH: registration.configPath } : {},
|
|
6846
7022
|
...registration.installationId ? { TOPOLO_AGENT_INSTALLATION_ID: registration.installationId } : {},
|
|
6847
7023
|
...registration.context ? { TOPOLO_CONTEXT: registration.context, TOPOLO_ENV: registration.environment } : {}
|
|
6848
7024
|
};
|
|
@@ -6974,7 +7150,7 @@ async function atomicWrite(path, contents) {
|
|
|
6974
7150
|
await mkdir3(dirname5(path), { recursive: true });
|
|
6975
7151
|
const temporaryPath = path + ".topolo-" + process.pid + ".tmp";
|
|
6976
7152
|
await writeFile3(temporaryPath, contents, { encoding: "utf8", mode: 384 });
|
|
6977
|
-
await
|
|
7153
|
+
await rename2(temporaryPath, path);
|
|
6978
7154
|
}
|
|
6979
7155
|
function withoutMcpServerSections(text2, name) {
|
|
6980
7156
|
const section = "[mcp_servers." + name + "]";
|
|
@@ -7038,7 +7214,14 @@ function resolveMcpRegistration(options) {
|
|
|
7038
7214
|
if (installationId && /[\r\n]/.test(installationId)) {
|
|
7039
7215
|
throw new Error("Installation id must be a single-line identifier.");
|
|
7040
7216
|
}
|
|
7041
|
-
|
|
7217
|
+
const configPath2 = process.env.TOPOLO_CONFIG_PATH;
|
|
7218
|
+
return {
|
|
7219
|
+
name,
|
|
7220
|
+
...context ? { context } : {},
|
|
7221
|
+
environment,
|
|
7222
|
+
...installationId ? { installationId } : {},
|
|
7223
|
+
...configPath2 ? { configPath: configPath2 } : {}
|
|
7224
|
+
};
|
|
7042
7225
|
}
|
|
7043
7226
|
async function installForAgent(agent, sourceDir, mcp, opts) {
|
|
7044
7227
|
const source = await readFile6(join7(sourceDir, "topolo", "SKILL.md"), "utf8");
|
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.8",
|
|
5
|
+
"topoloPublishedVersion": "0.13.8",
|
|
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.25",
|
|
35
|
+
"@topolo/sdk": "0.23.20",
|
|
36
36
|
"commander": "^12.1.0",
|
|
37
37
|
"undici": "6.29.0"
|
|
38
38
|
},
|