@topolo/cli 0.13.8 → 0.13.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -5
- package/agent-packs/README.md +98 -0
- package/agent-packs/coverage_changed.py +1 -1
- package/agent-packs/src/topolo_agent_pack/approvals.py +22 -0
- package/agent-packs/src/topolo_agent_pack/cli.py +37 -4
- package/agent-packs/src/topolo_agent_pack/execution.py +70 -2
- package/agent-packs/src/topolo_agent_pack/lanes.py +749 -0
- package/agent-packs/src/topolo_agent_pack/live.py +914 -16
- package/agent-packs/src/topolo_agent_pack/policy.py +6 -0
- package/agent-packs/src/topolo_agent_pack/task_models.py +200 -0
- package/agent-packs/src/topolo_agent_pack/topolo.py +42 -3
- package/dist/claude-pack-backend-RMSOPHVQ.js +2380 -0
- package/dist/index.js +270 -66
- package/package.json +5 -5
- package/dist/claude-pack-backend-3YJNKMYD.js +0 -1207
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
"""Bounded dynamic-lane intent and durable, fenced lifecycle records.
|
|
2
|
+
|
|
3
|
+
Native creation belongs to the host. A creation receipt is independently read
|
|
4
|
+
back before adoption; a registry entry is never a permission or execution ACK.
|
|
5
|
+
"""
|
|
6
|
+
import copy
|
|
7
|
+
import fcntl
|
|
8
|
+
import hashlib
|
|
9
|
+
import os
|
|
10
|
+
import pathlib
|
|
11
|
+
import stat
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
|
|
14
|
+
from .storage import PackError, atomic_json, canonical, read_json, safe_id
|
|
15
|
+
from . import task_models
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
STATES = {'planned', 'adopted-unconfigured', 'configured-settings-ACK', 'loaded-context-ACK',
|
|
19
|
+
'permission-request-pending', 'permission-decision-forwarded', 'executed-outcome-verified',
|
|
20
|
+
'settled', 'retired', 'uncertain-effect-hold'}
|
|
21
|
+
IDENTITY_FIELDS = ('packId', 'packRevision', 'contextKey', 'organizationId', 'environment', 'installationId')
|
|
22
|
+
ROLE_FIELDS = {'key', 'harness', 'nativeId', 'cwd', 'contextDigest', 'settings',
|
|
23
|
+
'AgentLogicalId', 'AgentVersionId', 'AgentWorkspaceId', 'BytesWorkspaceId',
|
|
24
|
+
'privateMCPDescriptorHash', *IDENTITY_FIELDS}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def digest(value):
|
|
28
|
+
return hashlib.sha256(canonical(value)).hexdigest()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _identity(value, label):
|
|
32
|
+
if not isinstance(value, str) or not value or len(value) > 256 or any(ord(c) < 32 for c in value):
|
|
33
|
+
raise PackError('Missing or invalid exact ' + label)
|
|
34
|
+
return value
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _role(value, harness):
|
|
38
|
+
if not isinstance(value, dict) or set(value) != ROLE_FIELDS or value['harness'] != harness:
|
|
39
|
+
raise PackError('Current owned role descriptor is missing or unsupported')
|
|
40
|
+
for key in ROLE_FIELDS - {'settings'}:
|
|
41
|
+
_identity(value[key], key)
|
|
42
|
+
if not pathlib.Path(value['cwd']).is_absolute() or not isinstance(value['settings'], dict):
|
|
43
|
+
raise PackError('Role workspace and settings are unqualified')
|
|
44
|
+
settings = value['settings']
|
|
45
|
+
expected = ({'model', 'model_reasoning_effort', 'approval_policy', 'approvals_reviewer',
|
|
46
|
+
'sandbox_mode', 'sandbox_workspace_write', 'apps'} if harness == 'codex'
|
|
47
|
+
else {'model', 'effortLevel', 'permissions'})
|
|
48
|
+
if set(settings) != expected:
|
|
49
|
+
raise PackError('Role settings contain unsupported or credential-bearing fields')
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def plan(request, role, intake, resolution, ceiling):
|
|
53
|
+
if (not isinstance(request, dict) or not {'laneId','logicalOriginThreadId','sourceRoleKey','task'}<=set(request) or
|
|
54
|
+
set(request)-{'laneId','logicalOriginThreadId','sourceRoleKey','task','verificationProfile'}):
|
|
55
|
+
raise PackError('Dynamic lane request has unsupported fields')
|
|
56
|
+
if request.get('verificationProfile') not in {None,'manual'}:
|
|
57
|
+
raise PackError('Only a separately bound narrower manual verification profile is supported')
|
|
58
|
+
safe_id(request['laneId']); _identity(request['logicalOriginThreadId'], 'logical origin')
|
|
59
|
+
harness = resolution['harness']; _role(role, harness); _role(intake, harness)
|
|
60
|
+
if request['sourceRoleKey'] != role['key'] or '/intakes/' not in intake['key']:
|
|
61
|
+
raise PackError('Dynamic lane has no exact source role and original intake')
|
|
62
|
+
if any(role[key] != intake[key] for key in IDENTITY_FIELDS):
|
|
63
|
+
raise PackError('Dynamic lane intake is outside the role pack, installation or organization context')
|
|
64
|
+
if resolution['task'] != request['task'] or resolution.get('nativeACK') is not None:
|
|
65
|
+
raise PackError('Dynamic task resolution does not match the creation request')
|
|
66
|
+
if (not isinstance(ceiling, dict) or set(ceiling) != {'policyRevision', 'policyDigest', 'profile', 'intakeKey'} or
|
|
67
|
+
ceiling['intakeKey'] != intake['key']):
|
|
68
|
+
raise PackError('Dynamic lane has no exact current policy ceiling and intake route')
|
|
69
|
+
for key in ['policyRevision', 'policyDigest', 'profile']:
|
|
70
|
+
_identity(ceiling[key], key)
|
|
71
|
+
return {'format': 'topolo-dynamic-lane-plan', 'schemaVersion': 1,
|
|
72
|
+
'laneId': request['laneId'], 'harness': harness,
|
|
73
|
+
'logicalOriginThreadId': request['logicalOriginThreadId'],
|
|
74
|
+
'role': copy.deepcopy(role), 'intake': copy.deepcopy(intake),
|
|
75
|
+
'resolution': copy.deepcopy(resolution), 'ceiling': copy.deepcopy(ceiling),
|
|
76
|
+
'verificationProfile': request.get('verificationProfile'),
|
|
77
|
+
'verificationProfileAuthority': 'Backlog bound two-fresh-worker verification only' if request.get('verificationProfile') else None,
|
|
78
|
+
'nativeParentId': intake['nativeId'] if harness == 'claude' else None,
|
|
79
|
+
'nativeParentAvailability': 'actual-harness-intake' if harness == 'claude' else 'UNAVAILABLE',
|
|
80
|
+
'state': 'planned', 'executionAuthorized': False}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def adopt(planned, receipt, native, current):
|
|
84
|
+
"""Adopt one actual created identity after current source/native readback.
|
|
85
|
+
|
|
86
|
+
``current`` is collected by the owned connection, never trusted from the
|
|
87
|
+
caller's receipt. Codex logical origin is deliberately not native ancestry.
|
|
88
|
+
"""
|
|
89
|
+
if current != {'role': planned['role'], 'intake': planned['intake'], 'ceiling': planned['ceiling']}:
|
|
90
|
+
raise PackError('Role context, intake, installation or policy changed before adoption')
|
|
91
|
+
fields = {'format', 'schemaVersion', 'state', 'planDigest', 'harness', 'nativeId',
|
|
92
|
+
'logicalOriginThreadId', 'cwd', 'model', 'effort', 'nativeParentId', 'fresh', 'creationSource'}
|
|
93
|
+
if (not isinstance(receipt, dict) or set(receipt) != fields or
|
|
94
|
+
receipt['format'] != 'topolo-native-lane-created' or receipt['schemaVersion'] != 1 or
|
|
95
|
+
receipt['state'] != 'ready' or receipt['creationSource'] != 'supported-native-host-tool' or
|
|
96
|
+
receipt['fresh'] is not True or receipt['planDigest'] != digest(planned) or
|
|
97
|
+
receipt['harness'] != planned['harness'] or
|
|
98
|
+
receipt['logicalOriginThreadId'] != planned['logicalOriginThreadId']):
|
|
99
|
+
raise PackError('Created native identity has no matching fresh host receipt')
|
|
100
|
+
_identity(receipt['nativeId'], 'created native identity')
|
|
101
|
+
if receipt['nativeId'] in {planned['role']['nativeId'], planned['intake']['nativeId']}:
|
|
102
|
+
raise PackError('A dynamic lane cannot adopt its source role or intake')
|
|
103
|
+
expected = {'nativeId': receipt['nativeId'], 'cwd': receipt['cwd'], 'nativeParentId': planned['nativeParentId']}
|
|
104
|
+
if (not isinstance(native, dict) or any(native.get(k) != v for k, v in expected.items()) or
|
|
105
|
+
receipt['nativeParentId'] != planned['nativeParentId'] or
|
|
106
|
+
any(k in native and native[k] != receipt[k] for k in ['model','effort']) or native.get('archived') is not False or
|
|
107
|
+
native.get('backend') != 'local' or native.get('foreignWorkspace') is not False):
|
|
108
|
+
raise PackError('Actual native child ownership or parent descriptor does not match')
|
|
109
|
+
if planned['harness']=='claude':
|
|
110
|
+
_identity(native.get('title'),'actual child title')
|
|
111
|
+
if not pathlib.Path(receipt['cwd']).is_absolute():
|
|
112
|
+
raise PackError('Created lane requires its exact absolute owned workspace')
|
|
113
|
+
return {'format': 'topolo-dynamic-lane', 'schemaVersion': 1,
|
|
114
|
+
'laneId': planned['laneId'], 'harness': planned['harness'], 'nativeId': receipt['nativeId'],
|
|
115
|
+
'plan': copy.deepcopy(planned), 'planDigest': digest(planned),
|
|
116
|
+
'creationReceipt': copy.deepcopy(receipt), 'creationReceiptDigest': digest(receipt),
|
|
117
|
+
'nativeTitle': native.get('title'),
|
|
118
|
+
'frontendVisibility': 'UNVERIFIED_MANAGED_APP_READ_REQUIRED' if planned['harness']=='codex' else 'native-manager-read-verified',
|
|
119
|
+
'state': 'adopted-unconfigured', 'nativeSettingsACK': None, 'contextACK': None,
|
|
120
|
+
'permissionRequest': None, 'permissionDecision': None, 'outcome': None,
|
|
121
|
+
'nativeConnectionGeneration': None, 'executionAuthorized': False}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def acknowledge_settings(record, native, resolution, generation):
|
|
125
|
+
if record['state'] != 'adopted-unconfigured' or resolution['nativeACK'] != native:
|
|
126
|
+
raise PackError('Dynamic lane settings need their exact native model and effort ACK')
|
|
127
|
+
if ({**resolution, 'nativeACK': None} != record['plan']['resolution'] or
|
|
128
|
+
native != record['plan']['resolution']['resolved']):
|
|
129
|
+
raise PackError('Dynamic lane model choice changed after creation planning')
|
|
130
|
+
_identity(generation, 'native connection generation')
|
|
131
|
+
return {**copy.deepcopy(record), 'state': 'configured-settings-ACK',
|
|
132
|
+
'nativeSettingsACK': copy.deepcopy(native), 'nativeConnectionGeneration': generation}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def acknowledge_context(record, context_digest):
|
|
136
|
+
if record['state'] != 'configured-settings-ACK' or context_digest != record['plan']['role']['contextDigest']:
|
|
137
|
+
raise PackError('Dynamic lane needs actual loaded role context acknowledgement')
|
|
138
|
+
return {**copy.deepcopy(record), 'state': 'loaded-context-ACK', 'contextACK': context_digest}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def request_permission(record, request_fingerprint, request_id, generation):
|
|
142
|
+
if record['state'] != 'loaded-context-ACK' or generation != record['nativeConnectionGeneration']:
|
|
143
|
+
raise PackError('Dynamic permission request has no current configured connection')
|
|
144
|
+
_identity(request_fingerprint, 'native request fingerprint'); _identity(request_id, 'native request identity')
|
|
145
|
+
return {**copy.deepcopy(record), 'state': 'permission-request-pending',
|
|
146
|
+
'permissionRequest': {'fingerprint': request_fingerprint, 'requestId': request_id,
|
|
147
|
+
'generation': generation, 'intakeKey': record['plan']['intake']['key']}}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def record_decision(record, decision, intake_key, fingerprint, generation):
|
|
151
|
+
request = record.get('permissionRequest')
|
|
152
|
+
if (record['state'] != 'permission-request-pending' or not request or
|
|
153
|
+
intake_key != request['intakeKey'] or fingerprint != request['fingerprint'] or
|
|
154
|
+
generation != request['generation'] or decision not in {'approved', 'denied'}):
|
|
155
|
+
raise PackError('Dynamic permission decision differs from the original intake request')
|
|
156
|
+
return {**copy.deepcopy(record), 'state': 'permission-decision-forwarded',
|
|
157
|
+
'permissionDecision': decision, 'executionAuthorized': decision == 'approved'}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def record_outcome(record, native_outcome):
|
|
161
|
+
if (record['state'] != 'permission-decision-forwarded' or not isinstance(native_outcome, dict) or
|
|
162
|
+
set(native_outcome) != {'nativeId', 'requestFingerprint', 'state', 'verificationDigest'} or
|
|
163
|
+
native_outcome['nativeId'] != record['nativeId'] or
|
|
164
|
+
native_outcome['requestFingerprint'] != record['permissionRequest']['fingerprint'] or
|
|
165
|
+
native_outcome['state'] not in {'completed', 'denied', 'failed'} or
|
|
166
|
+
(record['permissionDecision'] == 'denied' and native_outcome['state'] != 'denied')):
|
|
167
|
+
raise PackError('Forwarded approval is not an attributable native operation outcome')
|
|
168
|
+
_identity(native_outcome['verificationDigest'], 'operation verification digest')
|
|
169
|
+
return {**copy.deepcopy(record), 'state': 'executed-outcome-verified',
|
|
170
|
+
'outcome': copy.deepcopy(native_outcome), 'executionAuthorized': False}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def settle(record, native):
|
|
174
|
+
if (record['state'] != 'executed-outcome-verified' or not isinstance(native, dict) or native.get('nativeId') != record['nativeId'] or
|
|
175
|
+
native.get('busy') is not False or native.get('pendingApprovals') != 0):
|
|
176
|
+
raise PackError('Dynamic lane has active work or pending native approval')
|
|
177
|
+
return {**copy.deepcopy(record), 'state': 'settled', 'executionAuthorized': False}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def retire(record, native):
|
|
181
|
+
if (record['state'] != 'settled' or not isinstance(native, dict) or native.get('nativeId') != record['nativeId'] or
|
|
182
|
+
native.get('busy') is not False or native.get('pendingApprovals') != 0 or
|
|
183
|
+
native.get('archived') is not True or native.get('historyPreserved') is not True):
|
|
184
|
+
raise PackError('Only the settled own lane with actual preserved-history archive ACK may retire')
|
|
185
|
+
return {**copy.deepcopy(record), 'state': 'retired', 'executionAuthorized': False}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def execution_role(record):
|
|
189
|
+
"""A gate row, retaining the source role key instead of inventing a new grant."""
|
|
190
|
+
if record['state'] not in {'adopted-unconfigured', 'configured-settings-ACK', 'loaded-context-ACK', 'permission-request-pending',
|
|
191
|
+
'permission-decision-forwarded', 'executed-outcome-verified', 'settled'}:
|
|
192
|
+
raise PackError('Dynamic lane is not configured on a qualified native connection')
|
|
193
|
+
plan = record['plan']; source = plan['role']; settings = copy.deepcopy(source['settings'])
|
|
194
|
+
choice = record['nativeSettingsACK'] or (plan['resolution']['resolved'] if record['state'] == 'adopted-unconfigured' else None)
|
|
195
|
+
if not choice or choice != plan['resolution']['resolved']:
|
|
196
|
+
raise PackError('Dynamic native settings have no exact model resolution acknowledgement')
|
|
197
|
+
if plan.get('verificationProfile')=='manual':
|
|
198
|
+
from . import permissions
|
|
199
|
+
agent={'pack':source['packId'],'set':source['key'].split('/')[1],'id':source['key'].split('/')[2]}
|
|
200
|
+
settings=permissions.apply_settings(agent,record['harness'],settings,
|
|
201
|
+
{'profile':'manual','overrides':{},'intakeRoutes':{source['key']:plan['intake']['key']}})
|
|
202
|
+
settings['model'] = choice['model']
|
|
203
|
+
settings['model_reasoning_effort' if record['harness'] == 'codex' else 'effortLevel'] = choice['effort']
|
|
204
|
+
return {'key': source['key'] + '/dynamic/' + record['laneId'], 'threadId': record['nativeId'],
|
|
205
|
+
'sourceRoleKey': source['key'], 'sourceRevision': source['packRevision'],
|
|
206
|
+
'sourceRoleNativeId': source['nativeId'], 'dynamicIntakeNativeId': plan['intake']['nativeId'],
|
|
207
|
+
'cwd': record['creationReceipt']['cwd'], 'intakeKey': plan['intake']['key'],
|
|
208
|
+
'contextKey': source['contextKey'], 'installationConsent': None,
|
|
209
|
+
'settings': settings, 'dynamicLaneId': record['laneId'],
|
|
210
|
+
'dynamicPlanDigest': record['planDigest'], 'dynamicContextDigest': source['contextDigest'],
|
|
211
|
+
'dynamicConnectionGeneration': record['nativeConnectionGeneration'],
|
|
212
|
+
'dynamicModelResolution': copy.deepcopy(plan['resolution']), 'dynamicVerificationProfile': plan.get('verificationProfile'), 'dynamicCeiling': copy.deepcopy(plan['ceiling'])}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class Registry:
|
|
216
|
+
def __init__(self, root, harness):
|
|
217
|
+
if harness not in {'codex', 'claude'}:
|
|
218
|
+
raise PackError('Unsupported native lane harness')
|
|
219
|
+
self.root = pathlib.Path(root); self.harness = harness
|
|
220
|
+
self.directory = self.root / 'dynamic-lanes' / harness
|
|
221
|
+
|
|
222
|
+
def save_plan(self, planned):
|
|
223
|
+
lane_id=planned['laneId']
|
|
224
|
+
if planned.get('format')!='topolo-dynamic-lane-plan' or planned.get('harness')!=self.harness:
|
|
225
|
+
raise PackError('Unqualified native lane plan')
|
|
226
|
+
with self.mutation():
|
|
227
|
+
directory=self.directory/'plans'
|
|
228
|
+
if not directory.exists():
|
|
229
|
+
directory.mkdir(mode=0o700)
|
|
230
|
+
self._owned(directory,True)
|
|
231
|
+
path=directory/(safe_id(lane_id)+'.json')
|
|
232
|
+
# A plan is a one-use immutable native snapshot, never a caller assertion.
|
|
233
|
+
fd=os.open(path,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
|
|
234
|
+
with os.fdopen(fd,'wb') as stream:
|
|
235
|
+
stream.write(canonical(planned)+b'\n');stream.flush();os.fsync(stream.fileno())
|
|
236
|
+
return copy.deepcopy(planned)
|
|
237
|
+
|
|
238
|
+
def read_plan(self, lane_id):
|
|
239
|
+
directory=self.directory/'plans';self._owned(directory,True)
|
|
240
|
+
path=directory/(safe_id(lane_id)+'.json');self._owned(path)
|
|
241
|
+
if path.stat().st_size>131072:
|
|
242
|
+
raise PackError('Native lane plan exceeds its schema bound')
|
|
243
|
+
return read_json(path)
|
|
244
|
+
|
|
245
|
+
def path(self, lane_id):
|
|
246
|
+
return self.directory / (safe_id(lane_id) + '.json')
|
|
247
|
+
|
|
248
|
+
def _owned(self, path, directory=False):
|
|
249
|
+
info = path.lstat()
|
|
250
|
+
if (stat.S_ISLNK(info.st_mode) or info.st_uid != os.getuid() or
|
|
251
|
+
(not directory and (not stat.S_ISREG(info.st_mode) or info.st_nlink != 1)) or
|
|
252
|
+
(directory and not stat.S_ISDIR(info.st_mode))):
|
|
253
|
+
raise PackError('Dynamic registry requires regular exclusively owned paths')
|
|
254
|
+
|
|
255
|
+
def read(self, lane_id):
|
|
256
|
+
path = self.path(lane_id)
|
|
257
|
+
if not path.exists():
|
|
258
|
+
if path.is_symlink():
|
|
259
|
+
raise PackError('Dynamic registry refuses a symlink')
|
|
260
|
+
return None
|
|
261
|
+
self._owned(self.root, True); self._owned(self.directory.parent, True); self._owned(self.directory, True)
|
|
262
|
+
self._owned(path)
|
|
263
|
+
if path.stat().st_size > 131072:
|
|
264
|
+
raise PackError('Dynamic lane record exceeds its bounded schema')
|
|
265
|
+
record = read_json(path)
|
|
266
|
+
if (record.get('format') != 'topolo-dynamic-lane' or record.get('schemaVersion') != 1 or
|
|
267
|
+
record.get('laneId') != lane_id or record.get('harness') != self.harness or
|
|
268
|
+
record.get('state') not in STATES or
|
|
269
|
+
digest(record.get('plan')) != record.get('planDigest') or
|
|
270
|
+
digest(record.get('creationReceipt')) != record.get('creationReceiptDigest')):
|
|
271
|
+
raise PackError('Dynamic registry identity or lifecycle schema differs')
|
|
272
|
+
return record
|
|
273
|
+
|
|
274
|
+
def records(self):
|
|
275
|
+
if not self.directory.exists():
|
|
276
|
+
return []
|
|
277
|
+
self._owned(self.directory, True)
|
|
278
|
+
return [self.read(p.stem) for p in sorted(self.directory.glob('*.json'))]
|
|
279
|
+
|
|
280
|
+
@contextmanager
|
|
281
|
+
def mutation(self):
|
|
282
|
+
self._owned(self.root, True)
|
|
283
|
+
for path in [self.directory.parent, self.directory]:
|
|
284
|
+
if not path.exists():
|
|
285
|
+
path.mkdir(mode=0o700)
|
|
286
|
+
self._owned(path, True)
|
|
287
|
+
path = self.directory / 'registry.lock'
|
|
288
|
+
fd = os.open(path, os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, 0o600)
|
|
289
|
+
try:
|
|
290
|
+
self._owned(path)
|
|
291
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
292
|
+
yield
|
|
293
|
+
except BlockingIOError as exc:
|
|
294
|
+
raise PackError('An owned dynamic registry writer is already active') from exc
|
|
295
|
+
finally:
|
|
296
|
+
fcntl.flock(fd, fcntl.LOCK_UN); os.close(fd)
|
|
297
|
+
|
|
298
|
+
def write(self, record, before_digest):
|
|
299
|
+
lane_id = record['laneId']
|
|
300
|
+
if (record.get('format') != 'topolo-dynamic-lane' or record.get('schemaVersion') != 1 or
|
|
301
|
+
record['harness'] != self.harness or record['state'] not in STATES or
|
|
302
|
+
digest(record['plan']) != record['planDigest'] or
|
|
303
|
+
digest(record['creationReceipt']) != record['creationReceiptDigest']):
|
|
304
|
+
raise PackError('Cannot write an unqualified dynamic lifecycle identity')
|
|
305
|
+
with self.mutation():
|
|
306
|
+
current = self.read(lane_id)
|
|
307
|
+
actual = digest(current) if current is not None else None
|
|
308
|
+
if actual != before_digest:
|
|
309
|
+
raise PackError('Dynamic lane beforeimage changed; no overwrite or replay')
|
|
310
|
+
others = [row for row in self.records() if row['laneId'] != lane_id]
|
|
311
|
+
if any(row['nativeId'] == record['nativeId'] for row in others):
|
|
312
|
+
raise PackError('Native identity is already registered to a different dynamic lane')
|
|
313
|
+
if current and (current['nativeId'] != record['nativeId'] or current['planDigest'] != record['planDigest']):
|
|
314
|
+
raise PackError('Dynamic registration conflicts with its durable owned identity')
|
|
315
|
+
atomic_json(self.path(lane_id), record)
|
|
316
|
+
if self.read(lane_id) != record:
|
|
317
|
+
raise PackError('Dynamic registry write acknowledgement differs; reconcile without replay')
|
|
318
|
+
return copy.deepcopy(record)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class ClaudeLifecycle:
|
|
322
|
+
"""Typed helper for one new native child; no host effects or cached grants."""
|
|
323
|
+
def __init__(self,lib,pack,policy_path):
|
|
324
|
+
self.lib=lib;self.pack=safe_id(pack);self.policy_path=pathlib.Path(policy_path)
|
|
325
|
+
self.store=Registry(lib.root,'claude')
|
|
326
|
+
|
|
327
|
+
def current(self,source_key,models):
|
|
328
|
+
from . import permissions,topolo
|
|
329
|
+
for path in [self.lib.root/'claude/state.json',self.policy_path]:self.store._owned(path)
|
|
330
|
+
state=read_json(self.lib.root/'claude/state.json');policy=read_json(self.policy_path)
|
|
331
|
+
manifest,revision=self.lib.load(self.pack);consent=permissions.load(self.lib,self.pack,manifest['agents'])
|
|
332
|
+
if (state.get('harness')!='claude' or policy.get('format')!='topolo-agent-execution-policy' or
|
|
333
|
+
policy.get('pack')!=self.pack or policy.get('sourceRevision')!=revision or
|
|
334
|
+
not consent or policy.get('installationConsent')!=consent):
|
|
335
|
+
raise PackError('Current adopted Claude source and installation policy differ')
|
|
336
|
+
intake_key=consent['intakeRoutes'].get(source_key)
|
|
337
|
+
if not intake_key or '/intakes/' not in intake_key:
|
|
338
|
+
raise PackError('Original same-context intake is unavailable')
|
|
339
|
+
remote,proof=topolo.organization_execution_policy(topolo.Client(consent['environment'],consent['contextKey']),
|
|
340
|
+
consent,manifest['agents'],consent.get('agentWorkspaceId'))
|
|
341
|
+
permissions.authorize_native_scope(proof)
|
|
342
|
+
self._manual_profile_allowed='manual' in remote['allowedProfiles']
|
|
343
|
+
binding_file=topolo.binding_path(self.lib,self.pack,consent['environment'],consent['contextKey']);self.store._owned(binding_file)
|
|
344
|
+
binding=read_json(binding_file)
|
|
345
|
+
def role(key):
|
|
346
|
+
rows=[r for r in state['agents'] if r['key']==key and r['pack']==self.pack and r['sourceRevision']==revision and r['importState']=='desktop-adopted']
|
|
347
|
+
entries=[r for r in policy['agents'] if r['key']==key]
|
|
348
|
+
if len(rows)!=1 or len(entries)!=1:
|
|
349
|
+
raise PackError('Exact currently adopted source role missing')
|
|
350
|
+
row=rows[0];cwd=pathlib.Path(row['workspace'])
|
|
351
|
+
if not cwd.resolve().is_relative_to((self.lib.root/'claude/workspaces'/self.pack).resolve()):
|
|
352
|
+
raise PackError('Role context is outside the owned pack')
|
|
353
|
+
files=[cwd/'CLAUDE.md',cwd/'topolo-connection.json',cwd/'.mcp.json']
|
|
354
|
+
for file in files:self.store._owned(file)
|
|
355
|
+
hashes={str(f):hashlib.sha256(f.read_bytes()).hexdigest() for f in files}
|
|
356
|
+
descriptor=read_json(files[1]);logical='/'.join(key.split('/')[1:]);refs=[r for r in binding['input']['roles'] if r['logicalId']==logical]
|
|
357
|
+
if len(refs)!=1:
|
|
358
|
+
raise PackError('Source Agent role has no unique connection')
|
|
359
|
+
expected={'harness':'claude','pack':self.pack,'packRevision':revision,'role':logical,'nativeThreadId':row['desktopSessionId'],
|
|
360
|
+
'environment':consent['environment'],'contextKey':consent['contextKey'],'instanceId':binding['input']['instanceId'],
|
|
361
|
+
'agentId':refs[0]['agentId'],'agentVersionId':refs[0]['agentVersionId'],'agentWorkspaceId':binding['agentWorkspaceId'],
|
|
362
|
+
'bytesWorkspaceId':binding['input']['bytesWorkspaceId'],'identity':binding['identity']}
|
|
363
|
+
if any(descriptor.get(k)!=v for k,v in expected.items()) or binding['identity']['organizationId']!=consent['contextKey'][4:]:
|
|
364
|
+
raise PackError('Current Agent/Bytes/organization/native role descriptor differs')
|
|
365
|
+
return {'key':key,'harness':'claude','nativeId':row['desktopSessionId'],'cwd':str(cwd),
|
|
366
|
+
'contextDigest':digest(hashes),'settings':entries[0]['harnesses']['claude']['nativeSettings'],
|
|
367
|
+
'AgentLogicalId':refs[0]['agentId'],'AgentVersionId':refs[0]['agentVersionId'],'AgentWorkspaceId':binding['agentWorkspaceId'],
|
|
368
|
+
'BytesWorkspaceId':binding['input']['bytesWorkspaceId'],'privateMCPDescriptorHash':hashes[str(files[2])],
|
|
369
|
+
'packId':self.pack,'packRevision':revision,'contextKey':consent['contextKey'],'organizationId':binding['identity']['organizationId'],
|
|
370
|
+
'environment':consent['environment'],'installationId':binding['input']['instanceId']}
|
|
371
|
+
source=role(source_key);intake=role(intake_key)
|
|
372
|
+
config_path=self.lib.root/'dynamic-models'/(self.pack+'.json');self.store._owned(config_path);config=read_json(config_path)
|
|
373
|
+
if (set(config)!={'format','schemaVersion','sourceRevision','configuration','constraints','authorizedOverrides'} or
|
|
374
|
+
config['format']!='topolo-pack-dynamic-models' or config['schemaVersion']!=1 or config['sourceRevision']!=revision):
|
|
375
|
+
raise PackError('Explicit current versioned pack task-model configuration required')
|
|
376
|
+
constraints=task_models.role_constraints(config['constraints'],source_key)
|
|
377
|
+
ceiling={'policyRevision':proof['revision'],'policyDigest':proof['policyDigest'],
|
|
378
|
+
'profile':consent.get('overrides',{}).get(source_key,consent['profile']),'intakeKey':intake_key}
|
|
379
|
+
return {'role':source,'intake':intake,'ceiling':ceiling},config,constraints
|
|
380
|
+
|
|
381
|
+
def qualified(self,record,models):
|
|
382
|
+
current,config,constraints=self.current(record['plan']['role']['key'],models)
|
|
383
|
+
if record['plan'].get('verificationProfile')=='manual' and not self._manual_profile_allowed:
|
|
384
|
+
raise PackError('Fresh organization policy prohibits the narrower child-only manual test profile')
|
|
385
|
+
task=record['plan']['resolution']['task']
|
|
386
|
+
choice=task_models.resolve_intent(config['configuration'],task,'claude',models,constraints,
|
|
387
|
+
override_authorized=digest(task) in config['authorizedOverrides'])
|
|
388
|
+
if current!={k:record['plan'][k] for k in ['role','intake','ceiling']} or choice!=record['plan']['resolution']:
|
|
389
|
+
raise PackError('Dynamic task/source/intake/configuration/native model observation is stale')
|
|
390
|
+
return current,config,constraints
|
|
391
|
+
|
|
392
|
+
def prepare(self,request,models,native_ids):
|
|
393
|
+
if not isinstance(native_ids,list) or len(native_ids)>1000 or len(set(native_ids))!=len(native_ids):
|
|
394
|
+
raise PackError('Exact native ID-only precreation inventory required')
|
|
395
|
+
current,config,constraints=self.current(request['sourceRoleKey'],models)
|
|
396
|
+
choice=task_models.resolve_intent(config['configuration'],request['task'],'claude',models,constraints,
|
|
397
|
+
override_authorized=digest(request['task']) in config['authorizedOverrides'])
|
|
398
|
+
if request.get('verificationProfile')=='manual' and not self._manual_profile_allowed:
|
|
399
|
+
raise PackError('Fresh organization policy does not allow the bound manual verification profile')
|
|
400
|
+
if any(r['nativeId'] not in native_ids for r in [current['role'],current['intake']]):
|
|
401
|
+
raise PackError('Source role and intake are not in the actual native precreation inventory')
|
|
402
|
+
planned=plan(request,current['role'],current['intake'],choice,current['ceiling'])
|
|
403
|
+
planned['nativeIdsBeforeCreation']=sorted(native_ids)
|
|
404
|
+
return self.store.save_plan(planned)
|
|
405
|
+
|
|
406
|
+
def adopt(self,planned,receipt,native,models):
|
|
407
|
+
if self.store.read_plan(planned['laneId'])!=planned or receipt['nativeId'] in planned['nativeIdsBeforeCreation']:
|
|
408
|
+
raise PackError('Fresh host creation differs from the immutable owned native snapshot')
|
|
409
|
+
if receipt['cwd']!=planned['role']['cwd'] or native.get('busy') is not False or native.get('pendingApprovals')!=0:
|
|
410
|
+
raise PackError('Fresh child is outside its reviewed role workspace or not idle')
|
|
411
|
+
current,_,_=self.qualified({'plan':planned},models)
|
|
412
|
+
return self.store.write(adopt(planned,receipt,native,current),None)
|
|
413
|
+
|
|
414
|
+
def creation(self,lane_id,models,begin=False,generation=None,native_id=None):
|
|
415
|
+
planned=self.store.read_plan(lane_id)
|
|
416
|
+
self.qualified({'plan':planned},models)
|
|
417
|
+
with self.store.mutation():
|
|
418
|
+
directory=self.store.directory/'creation'
|
|
419
|
+
if not directory.exists():
|
|
420
|
+
directory.mkdir(mode=0o700)
|
|
421
|
+
self.store._owned(directory,True)
|
|
422
|
+
path=directory/(safe_id(lane_id)+'.json')
|
|
423
|
+
if begin:
|
|
424
|
+
if self.store.read(lane_id) or path.exists() or path.is_symlink():
|
|
425
|
+
raise PackError('Native creation is already reserved; reconcile without replay')
|
|
426
|
+
_identity(generation,'creation generation')
|
|
427
|
+
record={'format':'topolo-owned-native-creation','schemaVersion':1,'laneId':lane_id,
|
|
428
|
+
'planDigest':digest(planned),'generation':generation,'state':'may-have-effects','nativeId':None}
|
|
429
|
+
fd=os.open(path,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
|
|
430
|
+
with os.fdopen(fd,'wb') as stream:
|
|
431
|
+
stream.write(canonical(record)+b'\n');stream.flush();os.fsync(stream.fileno())
|
|
432
|
+
else:
|
|
433
|
+
self.store._owned(path)
|
|
434
|
+
record=read_json(path)
|
|
435
|
+
if (set(record)!={'format','schemaVersion','laneId','planDigest','generation','state','nativeId'} or
|
|
436
|
+
record['format']!='topolo-owned-native-creation' or record['schemaVersion']!=1 or
|
|
437
|
+
record['laneId']!=lane_id or record['planDigest']!=digest(planned) or
|
|
438
|
+
record['state'] not in {'may-have-effects','created-initializing'}):
|
|
439
|
+
raise PackError('Exact own native creation marker differs')
|
|
440
|
+
if native_id is not None:
|
|
441
|
+
_identity(native_id,'actual created native identity')
|
|
442
|
+
if (record['state']!='may-have-effects' or record['nativeId'] is not None or
|
|
443
|
+
generation!=record['generation'] or native_id in planned['nativeIdsBeforeCreation']):
|
|
444
|
+
raise PackError('Native creation identity/generation conflicts; no replay')
|
|
445
|
+
record={**record,'state':'created-initializing','nativeId':native_id}
|
|
446
|
+
atomic_json(path,record)
|
|
447
|
+
if read_json(path)!=record:
|
|
448
|
+
raise PackError('Native creation marker readback differs')
|
|
449
|
+
return {'plan':planned,'creation':record}
|
|
450
|
+
|
|
451
|
+
def configuration_begin(self,lane_id,models,generation):
|
|
452
|
+
record=self.store.read(lane_id)
|
|
453
|
+
if not record or record['state']!='adopted-unconfigured':
|
|
454
|
+
raise PackError('Fresh adopted child required; no retry after possible effects')
|
|
455
|
+
self.qualified(record,models)
|
|
456
|
+
hold={**record,'state':'uncertain-effect-hold','configurationGeneration':generation}
|
|
457
|
+
self.store.write(hold,digest(record))
|
|
458
|
+
return {'record':record,'holdDigest':digest(hold),'generation':generation}
|
|
459
|
+
|
|
460
|
+
def configuration_ack(self,operation,models,native):
|
|
461
|
+
record=operation['record'];held=self.store.read(record['laneId'])
|
|
462
|
+
if digest(held)!=operation['holdDigest'] or held.get('configurationGeneration')!=operation['generation']:
|
|
463
|
+
raise PackError('Child settings effect owner/beforeimage differs')
|
|
464
|
+
_,config,constraints=self.qualified(record,models)
|
|
465
|
+
if native.get('nativeId')!=record['nativeId'] or native.get('settingsACK') is not True or native.get('busy') is not False:
|
|
466
|
+
raise PackError('Actual idle native child settings ACK is missing')
|
|
467
|
+
expected_mode='default' if record['plan'].get('verificationProfile')=='manual' else record['plan']['role']['settings']['permissions']['defaultMode']
|
|
468
|
+
if native.get('permissionMode')!=expected_mode:
|
|
469
|
+
raise PackError('Actual child native permission mode differs from the bound intent')
|
|
470
|
+
actual={'model':native['model'],'effort':native['effort']}
|
|
471
|
+
resolution=task_models.acknowledge(record['plan']['resolution'],actual,configuration=config['configuration'],catalog=models,constraints=constraints)
|
|
472
|
+
updated=acknowledge_settings(record,actual,resolution,operation['generation'])
|
|
473
|
+
updated['nativeProcessSettingsProof']={'generation':operation['generation'],'model':native['model'],'effort':native['effort'],
|
|
474
|
+
'classification':'exact live child process tuple; not a general effort capability catalogue'}
|
|
475
|
+
return self.store.write(updated,digest(held))
|
|
476
|
+
|
|
477
|
+
def context_prepare(self,lane_id,nonce,models):
|
|
478
|
+
import uuid
|
|
479
|
+
if str(uuid.UUID(nonce))!=nonce:
|
|
480
|
+
raise PackError('Exact context probe UUID required')
|
|
481
|
+
record=self.store.read(lane_id)
|
|
482
|
+
if not record or record['state']!='configured-settings-ACK' or record.get('contextProbe'):
|
|
483
|
+
raise PackError('Context probe requires a current configured child and one fresh operation')
|
|
484
|
+
self.qualified(record,models);source=record['plan']['role'];cwd=pathlib.Path(source['cwd'])
|
|
485
|
+
files=[cwd/'CLAUDE.md',cwd/'topolo-connection.json'];hashes={str(p):hashlib.sha256(p.read_bytes()).hexdigest() for p in files}
|
|
486
|
+
expected={'operationId':nonce,'nativeId':record['nativeId'],'AgentLogicalId':source['AgentLogicalId'],
|
|
487
|
+
'AgentVersionId':source['AgentVersionId'],'AgentWorkspaceId':source['AgentWorkspaceId'],'BytesWorkspaceId':source['BytesWorkspaceId']}
|
|
488
|
+
text=('Topolo bounded context bootstrap '+nonce+'. Read only your own '+str(files[0])+' and '+str(files[1])+'. '
|
|
489
|
+
'Use the Read tool on each file. Load the role charter and current connected Agent/Bytes identity. '
|
|
490
|
+
'Do not read credentials, MCP configuration, other sessions or business records. Do not write or run commands. '
|
|
491
|
+
'Return one JSON object with exactly operationId, nativeId, AgentLogicalId, AgentVersionId, AgentWorkspaceId, BytesWorkspaceId. '
|
|
492
|
+
'operationId is '+nonce+'; nativeId is '+record['nativeId']+'. Obtain the four identity references from the actual connection file. '
|
|
493
|
+
'Do not start assigned task work until this context acknowledgement has been checked through the original intake.')
|
|
494
|
+
probe={'operationId':nonce,'state':'prepared','expected':expected,'fileHashes':hashes,'text':text,'generation':record['nativeConnectionGeneration']}
|
|
495
|
+
updated={**record,'contextProbe':probe};self.store.write(updated,digest(record));return probe
|
|
496
|
+
|
|
497
|
+
def context_mark(self,lane_id,nonce,state,models,history_count=None,history_digest=None):
|
|
498
|
+
record=self.store.read(lane_id);self.qualified(record,models);probe=record.get('contextProbe')
|
|
499
|
+
transitions={'prepared':{'start-may-have-effects'},'start-may-have-effects':{'native-delivered','uncertain','native-refused'}}
|
|
500
|
+
if not probe or probe['operationId']!=nonce or state not in transitions.get(probe['state'],set()):
|
|
501
|
+
raise PackError('Context probe custody differs or delivery cannot be replayed')
|
|
502
|
+
update={**probe,'state':state}
|
|
503
|
+
if state=='start-may-have-effects':
|
|
504
|
+
if type(history_count)is not int or history_count<0 or not isinstance(history_digest,str) or len(history_digest)!=64:
|
|
505
|
+
raise PackError('Exact own-child native history prefix required before bootstrap')
|
|
506
|
+
update.update(historyCount=history_count,historyDigest=history_digest)
|
|
507
|
+
result={**record,'contextProbe':update};return self.store.write(result,digest(record))
|
|
508
|
+
|
|
509
|
+
def context_ack(self,lane_id,nonce,models,messages):
|
|
510
|
+
record=self.store.read(lane_id);self.qualified(record,models);probe=record.get('contextProbe')
|
|
511
|
+
if (not probe or probe['operationId']!=nonce or probe['state']!='native-delivered' or
|
|
512
|
+
not isinstance(messages,list) or digest(messages[:probe['historyCount']])!=probe['historyDigest']):
|
|
513
|
+
raise PackError('Exact native child bootstrap history is missing or changed')
|
|
514
|
+
indices=[i for i,m in enumerate(messages) if m.get('uuid')==nonce and m.get('type')=='user']
|
|
515
|
+
if len(indices)!=1 or indices[0]!=probe['historyCount'] or messages[indices[0]].get('message',{}).get('content')!=probe['text']:
|
|
516
|
+
raise PackError('Real bootstrap native request identity or text differs')
|
|
517
|
+
tail=messages[indices[0]+1:];stop=next((i for i,m in enumerate(tail) if m.get('type')=='user' and isinstance(m.get('message',{}).get('content'),str)),len(tail));tail=tail[:stop]
|
|
518
|
+
reads={};results=set();assistant=None
|
|
519
|
+
for message in tail:
|
|
520
|
+
blocks=message.get('message',{}).get('content',[])
|
|
521
|
+
if not isinstance(blocks,list):
|
|
522
|
+
continue
|
|
523
|
+
for block in blocks:
|
|
524
|
+
if block.get('type')=='tool_use' and block.get('name')=='Read' and block.get('input',{}).get('file_path') in probe['fileHashes']:
|
|
525
|
+
reads[block['id']]=block['input']['file_path']
|
|
526
|
+
if block.get('type')=='tool_result' and block.get('is_error') is not True:
|
|
527
|
+
results.add(block.get('tool_use_id'))
|
|
528
|
+
if message.get('type')=='assistant':
|
|
529
|
+
assistant=message
|
|
530
|
+
if {path for identity,path in reads.items() if identity in results}!=set(probe['fileHashes']) or not assistant:
|
|
531
|
+
raise PackError('Same-child native file reads have no matching successful tool results')
|
|
532
|
+
text=''.join(b.get('text','') for b in assistant['message']['content'] if b.get('type')=='text')
|
|
533
|
+
if len(text)>2048:
|
|
534
|
+
raise PackError('Context bootstrap reply exceeds its closed schema')
|
|
535
|
+
try:actual=__import__('json').loads(text)
|
|
536
|
+
except ValueError as error:raise PackError('Context bootstrap has no exact structured reply') from error
|
|
537
|
+
if actual!=probe['expected'] or not assistant.get('uuid'):
|
|
538
|
+
raise PackError('Same-child context attestation differs from the actual connected role')
|
|
539
|
+
if any(hashlib.sha256(pathlib.Path(file).read_bytes()).hexdigest()!=sha for file,sha in probe['fileHashes'].items()):
|
|
540
|
+
raise PackError('Role context changed during actual bootstrap')
|
|
541
|
+
updated=acknowledge_context(record,record['plan']['role']['contextDigest'])
|
|
542
|
+
updated['contextProbe']={k:v for k,v in probe.items() if k!='text'}|{'state':'loaded-context-ACK','replyMessageId':assistant['uuid'],
|
|
543
|
+
'proofDigest':digest({'requestMessageId':nonce,'replyMessageId':assistant['uuid'],'readToolUseIds':sorted(reads),'attestation':actual})}
|
|
544
|
+
return self.store.write(updated,digest(record))
|
|
545
|
+
|
|
546
|
+
def permission_request(self,lane_id,models,request):
|
|
547
|
+
record=self.store.read(lane_id);self.qualified(record,models)
|
|
548
|
+
required={'nativeId','fingerprint','requestId','generation','intakeKey','intakeNativeId','toolUseId'}
|
|
549
|
+
if (not isinstance(request,dict) or set(request)!=required or request['nativeId']!=record['nativeId'] or
|
|
550
|
+
request['intakeKey']!=record['plan']['intake']['key'] or request['intakeNativeId']!=record['plan']['intake']['nativeId']):
|
|
551
|
+
raise PackError('Native permission request is outside the original child/intake scope')
|
|
552
|
+
prior=record.get('permissionRequest')
|
|
553
|
+
if prior and prior['requestId']==request['requestId'] and prior['fingerprint']==request['fingerprint']:
|
|
554
|
+
return record
|
|
555
|
+
_identity(request['toolUseId'],'native approved tool-use identity')
|
|
556
|
+
updated=request_permission(record,request['fingerprint'],request['requestId'],request['generation'])
|
|
557
|
+
updated['permissionRequest']['toolUseId']=request['toolUseId']
|
|
558
|
+
return self.store.write(updated,digest(record))
|
|
559
|
+
|
|
560
|
+
def permission_decision(self,lane_id,models,decision,intake_key,fingerprint,generation):
|
|
561
|
+
record=self.store.read(lane_id);self.qualified(record,models)
|
|
562
|
+
updated=record_decision(record,decision,intake_key,fingerprint,generation)
|
|
563
|
+
return self.store.write(updated,digest(record))
|
|
564
|
+
|
|
565
|
+
def fixture_plan(self,lane_id,nonce,models):
|
|
566
|
+
import uuid
|
|
567
|
+
if str(uuid.UUID(nonce))!=nonce:
|
|
568
|
+
raise PackError('Exact fixture verification UUID required')
|
|
569
|
+
record=self.store.read(lane_id);self.qualified(record,models)
|
|
570
|
+
if record['state']!='loaded-context-ACK' or record.get('fixtureVerification'):
|
|
571
|
+
raise PackError('Own fixture requires verified loaded context and one fresh bounded operation')
|
|
572
|
+
role=record['plan']['role'];path=pathlib.Path(record['creationReceipt']['cwd'])/('.topolo-dynamic-'+lane_id+'-'+nonce+'.json')
|
|
573
|
+
if path.exists() or path.is_symlink():
|
|
574
|
+
raise PackError('Own native fixture target already exists; never overwrite')
|
|
575
|
+
expected={'operationId':nonce,'laneId':lane_id,'nativeId':record['nativeId'],'AgentVersionId':role['AgentVersionId'],
|
|
576
|
+
'AgentWorkspaceId':role['AgentWorkspaceId'],'BytesWorkspaceId':role['BytesWorkspaceId']}
|
|
577
|
+
text=('Topolo bounded own-worker verification '+nonce+'. You have acknowledged the current role and Agent/Bytes context. '
|
|
578
|
+
'Write exactly one new fixture file '+str(path)+' with the JSON object '+__import__('json').dumps(expected,separators=(',',':'))+'. '
|
|
579
|
+
'Use the native Write tool for this file. Request any required native permission through your original intake; do not auto-approve or bypass policy. '
|
|
580
|
+
'Read back only that file and return one JSON object with operationId and status (ok, denied or failed). '
|
|
581
|
+
'No other source, business content, credentials, external messages, provider or policy changes are authorized.')
|
|
582
|
+
fixture={'operationId':nonce,'path':str(path),'expected':expected,'text':text,'state':'prepared','generation':record['nativeConnectionGeneration']}
|
|
583
|
+
updated={**record,'fixtureVerification':fixture};self.store.write(updated,digest(record));return fixture
|
|
584
|
+
|
|
585
|
+
def fixture_mark(self,lane_id,nonce,state,models):
|
|
586
|
+
record=self.store.read(lane_id);self.qualified(record,models);fixture=record.get('fixtureVerification')
|
|
587
|
+
transitions={'prepared':{'start-may-have-effects'},'start-may-have-effects':{'native-delivered','uncertain','native-refused'}}
|
|
588
|
+
if not fixture or fixture['operationId']!=nonce or state not in transitions.get(fixture['state'],set()):
|
|
589
|
+
raise PackError('Own fixture delivery identity differs or cannot be replayed')
|
|
590
|
+
result={**record,'fixtureVerification':{**fixture,'state':state}};return self.store.write(result,digest(record))
|
|
591
|
+
|
|
592
|
+
def fixture_outcome(self,lane_id,models,messages,native):
|
|
593
|
+
record=self.store.read(lane_id);self.qualified(record,models);fixture=record.get('fixtureVerification');request=record.get('permissionRequest')
|
|
594
|
+
if (record['state']!='permission-decision-forwarded' or not fixture or fixture['state']!='native-delivered' or not request or
|
|
595
|
+
request['generation']!=record['nativeConnectionGeneration'] or native.get('nativeId')!=record['nativeId'] or
|
|
596
|
+
native.get('busy') is not False or native.get('pendingApprovals')!=0 or not isinstance(messages,list)):
|
|
597
|
+
raise PackError('Native request/decision/idle child/fixture outcome are not all attributable')
|
|
598
|
+
tool_uses=[];tool_results=[]
|
|
599
|
+
for message in messages:
|
|
600
|
+
blocks=message.get('message',{}).get('content',[])
|
|
601
|
+
if not isinstance(blocks,list):
|
|
602
|
+
continue
|
|
603
|
+
for block in blocks:
|
|
604
|
+
if block.get('type')=='tool_use' and block.get('id')==request['toolUseId']:
|
|
605
|
+
tool_uses.append((message,block))
|
|
606
|
+
if block.get('type')=='tool_result' and block.get('tool_use_id')==request['toolUseId']:
|
|
607
|
+
tool_results.append((message,block))
|
|
608
|
+
if len(tool_uses)!=1 or len(tool_results)!=1 or tool_uses[0][1].get('name')!='Write' or tool_uses[0][1].get('input',{}).get('file_path')!=fixture['path']:
|
|
609
|
+
raise PackError('Exact native approved Write/tool-result identity is unavailable')
|
|
610
|
+
path=pathlib.Path(fixture['path']);failed=tool_results[0][1].get('is_error') is True
|
|
611
|
+
if record['permissionDecision']=='denied':
|
|
612
|
+
if not failed or path.exists():
|
|
613
|
+
raise PackError('Denied native operation has no matching refusal/absent-fixture outcome')
|
|
614
|
+
outcome='denied';fixture_sha=None
|
|
615
|
+
elif failed:
|
|
616
|
+
outcome='failed';fixture_sha=None
|
|
617
|
+
else:
|
|
618
|
+
self.store._owned(path)
|
|
619
|
+
if read_json(path)!=fixture['expected']:
|
|
620
|
+
raise PackError('Native tool result is not the exact own fixture write/readback')
|
|
621
|
+
outcome='completed';fixture_sha=hashlib.sha256(path.read_bytes()).hexdigest()
|
|
622
|
+
proof={'requestFingerprint':request['fingerprint'],'toolUseId':request['toolUseId'],
|
|
623
|
+
'useMessageId':tool_uses[0][0].get('uuid'),'resultMessageId':tool_results[0][0].get('uuid'),
|
|
624
|
+
'fixtureSHA256':fixture_sha,'fixtureOperationId':fixture['operationId'],'state':outcome}
|
|
625
|
+
if not proof['useMessageId'] or not proof['resultMessageId']:
|
|
626
|
+
raise PackError('Native operation message IDs are missing')
|
|
627
|
+
updated=record_outcome(record,{'nativeId':record['nativeId'],'requestFingerprint':request['fingerprint'],'state':outcome,'verificationDigest':digest(proof)})
|
|
628
|
+
updated=settle(updated,native);updated['fixtureVerification']={k:v for k,v in fixture.items() if k!='text'}|{'state':'outcome-verified','proof':proof}
|
|
629
|
+
return self.store.write(updated,digest(record))
|
|
630
|
+
|
|
631
|
+
def retirement_begin(self,lane_id,models):
|
|
632
|
+
record=self.store.read(lane_id);self.qualified(record,models)
|
|
633
|
+
if record['state']!='settled':
|
|
634
|
+
raise PackError('Only the verified settled own child can retire')
|
|
635
|
+
hold={**record,'state':'uncertain-effect-hold','retirementBeforeDigest':digest(record)}
|
|
636
|
+
self.store.write(hold,digest(record));return {'record':record,'holdDigest':digest(hold)}
|
|
637
|
+
|
|
638
|
+
def retirement_ack(self,operation,models,native):
|
|
639
|
+
record=operation['record'];held=self.store.read(record['laneId'])
|
|
640
|
+
if digest(held)!=operation['holdDigest']:
|
|
641
|
+
raise PackError('Retirement effect owner or beforeimage changed')
|
|
642
|
+
self.qualified(record,models)
|
|
643
|
+
result=retire(record,native)
|
|
644
|
+
return self.store.write(result,digest(held))
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def dispatch(self,action,payload):
|
|
648
|
+
if not isinstance(payload,dict):
|
|
649
|
+
raise PackError('Closed native helper object required')
|
|
650
|
+
base={'models'}
|
|
651
|
+
shapes={
|
|
652
|
+
'peek':{'sourceRoleKey'},'plan':{'request','nativeIds'},'adopt':{'plan','receipt','native'},
|
|
653
|
+
'creation-begin':{'laneId','generation'},'creation-record':{'laneId','generation','nativeId'},'creation-read':{'laneId'},
|
|
654
|
+
'configure-begin':{'laneId','generation'},'configure-ack':{'operation','native'},
|
|
655
|
+
'context-prepare':{'laneId','operationId'},'context-mark':{'laneId','operationId','state','historyCount','historyDigest'},
|
|
656
|
+
'context-ack':{'laneId','operationId','messages'},'fixture-prepare':{'laneId','operationId'},
|
|
657
|
+
'fixture-mark':{'laneId','operationId','state'},'fixture-outcome':{'laneId','messages','native'},
|
|
658
|
+
'permission-request':{'laneId','request'},'permission-decision':{'laneId','decision','intakeKey','fingerprint','generation'},
|
|
659
|
+
'retire-begin':{'laneId'},'retire-ack':{'operation','native'},'read':{'laneId'},
|
|
660
|
+
}
|
|
661
|
+
if action not in shapes or set(payload)!=base|shapes[action]:
|
|
662
|
+
raise PackError('Unsupported native lifecycle helper fields/action')
|
|
663
|
+
p=payload;models=p['models']
|
|
664
|
+
if action=='peek':
|
|
665
|
+
current,_,_=self.current(p['sourceRoleKey'],models)
|
|
666
|
+
state=read_json(self.lib.root/'claude/state.json');titles={}
|
|
667
|
+
for role in [current['role'],current['intake']]:
|
|
668
|
+
rows=[r for r in state['agents'] if r['desktopSessionId']==role['nativeId'] and r['key']==role['key']]
|
|
669
|
+
if len(rows)!=1:
|
|
670
|
+
raise PackError('Exact fixed source/intake native title missing')
|
|
671
|
+
titles[role['nativeId']]=rows[0]['title']
|
|
672
|
+
return {**current,'nativeTitles':titles}
|
|
673
|
+
if action=='plan':
|
|
674
|
+
return self.prepare(p['request'],models,p['nativeIds'])
|
|
675
|
+
if action=='adopt':
|
|
676
|
+
return self.adopt(p['plan'],p['receipt'],p['native'],models)
|
|
677
|
+
if action=='creation-begin':
|
|
678
|
+
return self.creation(p['laneId'],models,True,p['generation'])
|
|
679
|
+
if action=='creation-record':
|
|
680
|
+
return self.creation(p['laneId'],models,generation=p['generation'],native_id=p['nativeId'])
|
|
681
|
+
if action=='creation-read':
|
|
682
|
+
return self.creation(p['laneId'],models)
|
|
683
|
+
if action=='configure-begin':
|
|
684
|
+
return self.configuration_begin(p['laneId'],models,p['generation'])
|
|
685
|
+
if action=='configure-ack':
|
|
686
|
+
return self.configuration_ack(p['operation'],models,p['native'])
|
|
687
|
+
if action=='context-prepare':
|
|
688
|
+
return self.context_prepare(p['laneId'],p['operationId'],models)
|
|
689
|
+
if action=='context-mark':
|
|
690
|
+
return self.context_mark(p['laneId'],p['operationId'],p['state'],models,p['historyCount'],p['historyDigest'])
|
|
691
|
+
if action=='context-ack':
|
|
692
|
+
return self.context_ack(p['laneId'],p['operationId'],models,p['messages'])
|
|
693
|
+
if action=='fixture-prepare':
|
|
694
|
+
return self.fixture_plan(p['laneId'],p['operationId'],models)
|
|
695
|
+
if action=='fixture-mark':
|
|
696
|
+
return self.fixture_mark(p['laneId'],p['operationId'],p['state'],models)
|
|
697
|
+
if action=='fixture-outcome':
|
|
698
|
+
return self.fixture_outcome(p['laneId'],models,p['messages'],p['native'])
|
|
699
|
+
if action=='permission-request':
|
|
700
|
+
return self.permission_request(p['laneId'],models,p['request'])
|
|
701
|
+
if action=='permission-decision':
|
|
702
|
+
return self.permission_decision(p['laneId'],models,p['decision'],p['intakeKey'],p['fingerprint'],p['generation'])
|
|
703
|
+
if action=='retire-begin':
|
|
704
|
+
return self.retirement_begin(p['laneId'],models)
|
|
705
|
+
if action=='retire-ack':
|
|
706
|
+
return self.retirement_ack(p['operation'],models,p['native'])
|
|
707
|
+
record=self.store.read(p['laneId'])
|
|
708
|
+
if not record:
|
|
709
|
+
raise PackError('Exact own dynamic lane record missing')
|
|
710
|
+
self.qualified(record,models);return record
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def model_configuration_template(manifest,revision):
|
|
714
|
+
"""Reviewable new-lane configuration only; no native availability claim."""
|
|
715
|
+
configuration=task_models.default_configuration()
|
|
716
|
+
models=sorted({model for host in configuration['harnesses'].values() for model in host['tiers'].values()})
|
|
717
|
+
layer={'allowedModels':models,'allowedEfforts':['low','medium','high']}
|
|
718
|
+
groups=sorted({a['set'] for a in manifest['agents']})
|
|
719
|
+
roles=sorted('/'.join([a['pack'],a['set'],a['id']]) for a in manifest['agents'])
|
|
720
|
+
return {'format':'topolo-pack-dynamic-models','schemaVersion':1,'sourceRevision':revision,'configuration':configuration,
|
|
721
|
+
'constraints':{'pack':copy.deepcopy(layer),'groups':{key:copy.deepcopy(layer) for key in groups},
|
|
722
|
+
'roles':{key:copy.deepcopy(layer) for key in roles}},'authorizedOverrides':[]}
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def install_model_configuration(lib,pack,document,before_digest):
|
|
726
|
+
manifest,revision=lib.load(pack)
|
|
727
|
+
expected={'format','schemaVersion','sourceRevision','configuration','constraints','authorizedOverrides'}
|
|
728
|
+
if (not isinstance(document,dict) or set(document)!=expected or document['format']!='topolo-pack-dynamic-models' or
|
|
729
|
+
document['schemaVersion']!=1 or document['sourceRevision']!=revision or not isinstance(document['authorizedOverrides'],list) or
|
|
730
|
+
any(not isinstance(v,str) or len(v)!=64 for v in document['authorizedOverrides'])):
|
|
731
|
+
raise PackError('Explicit current reviewable dynamic model configuration required')
|
|
732
|
+
template=model_configuration_template(manifest,revision)
|
|
733
|
+
if set(document['constraints'].get('roles',{}))!=set(template['constraints']['roles']) or set(document['constraints'].get('groups',{}))!=set(template['constraints']['groups']):
|
|
734
|
+
raise PackError('Dynamic model constraints must explicitly cover the exact source pack roles/groups')
|
|
735
|
+
for key in template['constraints']['roles']:task_models.role_constraints(document['constraints'],key)
|
|
736
|
+
directory=lib.root/'dynamic-models';registry=Registry(lib.root,'codex');registry._owned(lib.root,True)
|
|
737
|
+
if not directory.exists():
|
|
738
|
+
directory.mkdir(mode=0o700)
|
|
739
|
+
registry._owned(directory,True);path=directory/(safe_id(pack)+'.json')
|
|
740
|
+
current=None
|
|
741
|
+
if path.exists() or path.is_symlink():
|
|
742
|
+
registry._owned(path);current=read_json(path)
|
|
743
|
+
if (digest(current) if current is not None else None)!=before_digest:
|
|
744
|
+
raise PackError('Current dynamic configuration beforeimage differs; no overwrite')
|
|
745
|
+
atomic_json(path,document)
|
|
746
|
+
if read_json(path)!=document:
|
|
747
|
+
raise PackError('Dynamic configuration readback differs')
|
|
748
|
+
return {'state':'dynamic-task-model-intent-installed','pack':pack,'configurationDigest':digest(document),
|
|
749
|
+
'nativeAvailabilityVerified':False,'existingRoleSettingsChanged':False,'modelTurnsStarted':False}
|