@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
|
@@ -29,6 +29,12 @@ MODELS = {'codex': {'reasoning': 'gpt-6-astra', 'workhorse': 'gpt-6.1-sol'},
|
|
|
29
29
|
'claude': {'reasoning': 'claude-opus-5-5', 'workhorse': 'claude-sonnet-5-5'}}
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
def dynamic_task_settings(configuration, task, harness, catalog, constraints, *, override_authorized=False):
|
|
33
|
+
"""Dynamic task intent only; preserve every fixed role's settings."""
|
|
34
|
+
from .task_models import resolve
|
|
35
|
+
return resolve(configuration, task, harness, catalog, constraints, override_authorized=override_authorized)
|
|
36
|
+
|
|
37
|
+
|
|
32
38
|
def role_profile(agent):
|
|
33
39
|
explicit = agent.get('executionProfile')
|
|
34
40
|
if explicit is not None:
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Versioned task selection intersected with a current native account catalog.
|
|
2
|
+
|
|
3
|
+
This module selects intent only. It neither opens a host nor grants permissions.
|
|
4
|
+
An unavailable choice is a refusal, never a provider or parent-model fallback.
|
|
5
|
+
"""
|
|
6
|
+
import copy
|
|
7
|
+
import hashlib
|
|
8
|
+
|
|
9
|
+
from .storage import PackError, canonical
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def digest(value):
|
|
13
|
+
return hashlib.sha256(canonical(value)).hexdigest()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def default_configuration():
|
|
17
|
+
"""Explicit template for a new pack configuration; existing roles are unchanged."""
|
|
18
|
+
return {
|
|
19
|
+
'format': 'topolo-task-model-configuration', 'schemaVersion': 1, 'revision': '1',
|
|
20
|
+
'rules': [
|
|
21
|
+
{'id': 'routine', 'complexity': ['routine'], 'risk': ['low'], 'profile': 'routine'},
|
|
22
|
+
{'id': 'deep', 'complexity': ['complex'], 'risk': ['low', 'normal', 'security-critical'], 'profile': 'deep'},
|
|
23
|
+
{'id': 'security', 'complexity': ['routine', 'standard'], 'risk': ['security-critical'], 'profile': 'deep'},
|
|
24
|
+
{'id': 'standard', 'complexity': ['standard'], 'risk': ['low', 'normal'], 'profile': 'standard'},
|
|
25
|
+
],
|
|
26
|
+
'harnesses': {
|
|
27
|
+
'codex': {
|
|
28
|
+
'tiers': {'standard': 'gpt-6.1-sol', 'deep': 'gpt-6-astra'},
|
|
29
|
+
'profiles': {'routine': {'tier': 'standard', 'efforts': ['low', 'medium']},
|
|
30
|
+
'standard': {'tier': 'standard', 'efforts': ['medium']},
|
|
31
|
+
'deep': {'tier': 'deep', 'efforts': ['high']}},
|
|
32
|
+
},
|
|
33
|
+
'claude': {
|
|
34
|
+
'tiers': {'economy': 'claude-haiku-5-5', 'standard': 'claude-sonnet-5-5', 'deep': 'claude-opus-5-5'},
|
|
35
|
+
'profiles': {'routine': {'tier': 'economy', 'efforts': ['low', 'medium']},
|
|
36
|
+
'standard': {'tier': 'standard', 'efforts': ['medium']},
|
|
37
|
+
'deep': {'tier': 'deep', 'efforts': ['high']}},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _strings(values, label):
|
|
44
|
+
if (not isinstance(values, list) or not values or len(values) > 64 or
|
|
45
|
+
any(not isinstance(v, str) or not v or len(v) > 128 for v in values) or
|
|
46
|
+
len(set(values)) != len(values)):
|
|
47
|
+
raise PackError('Invalid exact ' + label)
|
|
48
|
+
return set(values)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def role_constraints(configuration, role_key):
|
|
52
|
+
"""Intersect explicit pack, group and role ceilings; none is an implied grant."""
|
|
53
|
+
if (not isinstance(configuration,dict) or set(configuration)!={'pack','groups','roles'} or
|
|
54
|
+
not isinstance(role_key,str) or len(role_key.split('/'))!=3 or
|
|
55
|
+
not isinstance(configuration['groups'],dict) or not isinstance(configuration['roles'],dict)):
|
|
56
|
+
raise PackError('Explicit pack, group and role model constraints required')
|
|
57
|
+
group=role_key.split('/')[1]
|
|
58
|
+
rows=[configuration['pack'],configuration['groups'].get(group),configuration['roles'].get(role_key)]
|
|
59
|
+
models=None;efforts=None
|
|
60
|
+
for row in rows:
|
|
61
|
+
if not isinstance(row,dict) or set(row)!={'allowedModels','allowedEfforts'}:
|
|
62
|
+
raise PackError('Task model constraint layer is missing or unsupported')
|
|
63
|
+
m=_strings(row['allowedModels'],'constraint models');e=_strings(row['allowedEfforts'],'constraint efforts')
|
|
64
|
+
models=m if models is None else models&m;efforts=e if efforts is None else efforts&e
|
|
65
|
+
if not models or not efforts:
|
|
66
|
+
raise PackError('Task model constraints have no permitted intersection')
|
|
67
|
+
return {'allowedModels':sorted(models),'allowedEfforts':sorted(efforts)}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _selection(configuration, task, harness, constraints, *, override_authorized=False):
|
|
71
|
+
"""Resolve one bounded task against supplied, independently qualified host data.
|
|
72
|
+
|
|
73
|
+
The caller binds catalog/config digests to its current native connection and
|
|
74
|
+
checks them again before every effect. API-wide availability is insufficient.
|
|
75
|
+
"""
|
|
76
|
+
if (not isinstance(configuration, dict) or set(configuration) != {'format', 'schemaVersion', 'revision', 'rules', 'harnesses'} or
|
|
77
|
+
configuration.get('format') != 'topolo-task-model-configuration' or
|
|
78
|
+
configuration.get('schemaVersion') != 1 or not configuration.get('revision')):
|
|
79
|
+
raise PackError('Task model configuration is missing or unsupported')
|
|
80
|
+
if (not isinstance(task, dict) or not {'type', 'complexity', 'risk'} <= set(task) or
|
|
81
|
+
set(task) - {'type', 'complexity', 'risk', 'override'} or
|
|
82
|
+
not isinstance(task['type'], str) or not task['type'] or len(task['type']) > 120 or
|
|
83
|
+
task['complexity'] not in {'routine', 'standard', 'complex'} or
|
|
84
|
+
task['risk'] not in {'low', 'normal', 'security-critical'}):
|
|
85
|
+
raise PackError('An intake must supply an exact task type, complexity and risk')
|
|
86
|
+
rules = configuration.get('rules')
|
|
87
|
+
if not isinstance(rules, list) or not 0 < len(rules) <= 32:
|
|
88
|
+
raise PackError('Task model rules are unavailable')
|
|
89
|
+
matches = []
|
|
90
|
+
for rule in rules:
|
|
91
|
+
if (not isinstance(rule, dict) or not {'id', 'complexity', 'risk', 'profile'} <= set(rule) or
|
|
92
|
+
not isinstance(rule['id'], str) or not rule['id'] or
|
|
93
|
+
not isinstance(rule['profile'], str) or not rule['profile'] or
|
|
94
|
+
set(rule) - {'id', 'complexity', 'risk', 'profile', 'types'}):
|
|
95
|
+
raise PackError('Invalid task model rule')
|
|
96
|
+
complexities = _strings(rule['complexity'], 'complexity rule')
|
|
97
|
+
risks = _strings(rule['risk'], 'risk rule')
|
|
98
|
+
types = _strings(rule['types'], 'task type rule') if 'types' in rule else None
|
|
99
|
+
if task['complexity'] in complexities and task['risk'] in risks and (types is None or task['type'] in types):
|
|
100
|
+
matches.append(rule)
|
|
101
|
+
if len(matches) != 1:
|
|
102
|
+
raise PackError('Task has no unique configured model profile')
|
|
103
|
+
rule = matches[0]
|
|
104
|
+
harnesses = configuration.get('harnesses')
|
|
105
|
+
host = harnesses.get(harness) if isinstance(harnesses, dict) else None
|
|
106
|
+
if (not isinstance(host, dict) or set(host) != {'tiers', 'profiles'} or
|
|
107
|
+
not isinstance(host['tiers'], dict) or not isinstance(host['profiles'], dict)):
|
|
108
|
+
raise PackError('Task model harness is not configured')
|
|
109
|
+
profile = host['profiles'].get(rule['profile'])
|
|
110
|
+
if (not isinstance(profile, dict) or set(profile) != {'tier', 'efforts'} or
|
|
111
|
+
not isinstance(profile['tier'], str)):
|
|
112
|
+
raise PackError('Task model profile is unavailable')
|
|
113
|
+
requested = {'model': host['tiers'].get(profile['tier']), 'efforts': profile['efforts']}
|
|
114
|
+
if not isinstance(requested['model'], str) or not requested['model'] or requested['model'].endswith('-latest'):
|
|
115
|
+
raise PackError('Configured model must be an exact versioned native model')
|
|
116
|
+
_strings(requested['efforts'], 'configured effort preference')
|
|
117
|
+
if not isinstance(constraints, dict) or set(constraints) != {'allowedModels', 'allowedEfforts'}:
|
|
118
|
+
raise PackError('Pack, group and role model constraints are required')
|
|
119
|
+
allowed_models = _strings(constraints['allowedModels'], 'allowed models')
|
|
120
|
+
allowed_efforts = _strings(constraints['allowedEfforts'], 'allowed efforts')
|
|
121
|
+
override = task.get('override')
|
|
122
|
+
if override is not None:
|
|
123
|
+
if (not override_authorized or not isinstance(override, dict) or set(override) != {'model', 'effort'}):
|
|
124
|
+
raise PackError('Task model override has no exact intake authorization')
|
|
125
|
+
model = override['model']; preferences = [override['effort']]
|
|
126
|
+
_strings(preferences, 'override effort')
|
|
127
|
+
else:
|
|
128
|
+
model = requested['model']; preferences = requested['efforts']
|
|
129
|
+
if not isinstance(model,str) or model not in allowed_models:
|
|
130
|
+
raise PackError('Requested model is outside the configured constraints; no fallback')
|
|
131
|
+
if not any(e in allowed_efforts for e in preferences):
|
|
132
|
+
raise PackError('Requested effort is outside the configured constraints; no fallback')
|
|
133
|
+
return {'rule':rule,'requested':requested,'override':override,'model':model,'preferences':preferences,
|
|
134
|
+
'allowedModels':allowed_models,'allowedEfforts':allowed_efforts}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def resolve(configuration, task, harness, catalog, constraints, *, override_authorized=False):
|
|
138
|
+
selected=_selection(configuration,task,harness,constraints,override_authorized=override_authorized)
|
|
139
|
+
rule=selected['rule'];requested=selected['requested'];override=selected['override']
|
|
140
|
+
model=selected['model'];preferences=selected['preferences']
|
|
141
|
+
allowed_models=selected['allowedModels'];allowed_efforts=selected['allowedEfforts']
|
|
142
|
+
if (not isinstance(catalog, dict) or catalog.get('harness') != harness or
|
|
143
|
+
catalog.get('source') != 'native-account' or not catalog.get('connectionGeneration') or
|
|
144
|
+
not isinstance(catalog.get('models'), list) or not catalog['models']):
|
|
145
|
+
raise PackError('Current native account model and effort catalog is required')
|
|
146
|
+
available = {}
|
|
147
|
+
for row in catalog['models']:
|
|
148
|
+
if (not isinstance(row, dict) or set(row) != {'model', 'efforts'} or
|
|
149
|
+
not isinstance(row['model'], str) or not row['model'] or row['model'] in available):
|
|
150
|
+
raise PackError('Native catalog has duplicate or malformed model identities')
|
|
151
|
+
available[row['model']] = _strings(row['efforts'], 'native effort capabilities')
|
|
152
|
+
if not isinstance(model, str) or model not in allowed_models or model not in available:
|
|
153
|
+
raise PackError('Requested model is outside the configured or native account constraints; no fallback')
|
|
154
|
+
effort = next((v for v in preferences if v in allowed_efforts and v in available[model]), None)
|
|
155
|
+
if effort is None:
|
|
156
|
+
raise PackError('Requested effort is not supported for the exact model; no fallback')
|
|
157
|
+
return {
|
|
158
|
+
'format': 'topolo-task-model-resolution', 'schemaVersion': 1,
|
|
159
|
+
'task': copy.deepcopy(task), 'harness': harness, 'profile': rule['profile'],
|
|
160
|
+
'rule': rule['id'], 'requested': copy.deepcopy(override or requested),
|
|
161
|
+
'resolved': {'model': model, 'effort': effort},
|
|
162
|
+
'reason': 'Exact configured task profile intersected with pack/group/role and native account constraints',
|
|
163
|
+
'configurationRevision': configuration['revision'], 'configurationDigest': digest(configuration),
|
|
164
|
+
'catalogDigest': digest(catalog), 'connectionGeneration': catalog['connectionGeneration'],
|
|
165
|
+
'constraintsDigest': digest(constraints), 'nativeACK': None,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def resolve_intent(configuration, task, harness, models, constraints, *, override_authorized=False):
|
|
170
|
+
"""Explicit Claude intent when native effort metadata is unavailable.
|
|
171
|
+
|
|
172
|
+
This does not fabricate effort capability rows. No task may run until the
|
|
173
|
+
exact fresh child's actual process settings acknowledge the selected tuple.
|
|
174
|
+
"""
|
|
175
|
+
if (harness!='claude' or not isinstance(models,dict) or
|
|
176
|
+
set(models)!={'harness','source','connectionGeneration','models'} or models['harness']!=harness or
|
|
177
|
+
models['source']!='native-account-model-ids' or not models['connectionGeneration']):
|
|
178
|
+
raise PackError('Current native account model-ID observation required for staged Claude intent')
|
|
179
|
+
actual=_strings(models['models'],'native model IDs')
|
|
180
|
+
selected=_selection(configuration,task,harness,constraints,override_authorized=override_authorized)
|
|
181
|
+
if selected['model'] not in actual:
|
|
182
|
+
raise PackError('Exact task model is absent from the actual native account; no fallback')
|
|
183
|
+
effort=next(e for e in selected['preferences'] if e in selected['allowedEfforts'])
|
|
184
|
+
return {'format':'topolo-task-model-resolution','schemaVersion':1,'task':copy.deepcopy(task),'harness':harness,
|
|
185
|
+
'profile':selected['rule']['profile'],'rule':selected['rule']['id'],
|
|
186
|
+
'requested':copy.deepcopy(selected['override'] or selected['requested']),
|
|
187
|
+
'resolved':{'model':selected['model'],'effort':effort},
|
|
188
|
+
'reason':'Versioned task intent intersected with pack/group/role constraints and current native model IDs; effort support awaits actual process ACK',
|
|
189
|
+
'configurationRevision':configuration['revision'],'configurationDigest':digest(configuration),
|
|
190
|
+
'catalogDigest':digest(models),'connectionGeneration':models['connectionGeneration'],
|
|
191
|
+
'constraintsDigest':digest(constraints),'nativeEffortSupport':'awaiting-native-process-settings-ACK','nativeACK':None}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def acknowledge(resolution, native, *, configuration, catalog, constraints):
|
|
195
|
+
if (resolution['configurationDigest'] != digest(configuration) or
|
|
196
|
+
resolution['catalogDigest'] != digest(catalog) or
|
|
197
|
+
resolution['constraintsDigest'] != digest(constraints) or
|
|
198
|
+
native != resolution['resolved']):
|
|
199
|
+
raise PackError('Task model inputs or actual native model/effort changed; execution remains blocked')
|
|
200
|
+
return {**copy.deepcopy(resolution), 'nativeACK': copy.deepcopy(native)}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""Optional public Topolo continuity adapter. No credentials or transcript uploads."""
|
|
2
|
-
import re,contextlib,fcntl,base64,hashlib,json,os,pathlib,shutil,subprocess,tempfile,uuid
|
|
2
|
+
import re,contextlib,fcntl,base64,hashlib,json,os,pathlib,shutil,stat,subprocess,tempfile,uuid
|
|
3
3
|
from .storage import PackError,atomic_json,canonical,read_json,safe_id
|
|
4
4
|
|
|
5
5
|
class ConnectionSourceRefused(PackError):
|
|
@@ -100,7 +100,11 @@ def attach_runtime(lib,runtime):
|
|
|
100
100
|
if not pointer.exists():continue
|
|
101
101
|
selected=read_json(pointer);binding=read_json(binding_path(lib,pack,selected['environment'],selected['contextKey']))
|
|
102
102
|
role=agent['set']+'/'+agent['id']
|
|
103
|
-
|
|
103
|
+
matches=[r for r in binding['input']['roles'] if r['logicalId']==role]
|
|
104
|
+
if not matches:continue
|
|
105
|
+
if len(matches)!=1:
|
|
106
|
+
raise PackError('Connected role binding is not unique')
|
|
107
|
+
reference=matches[0]
|
|
104
108
|
import sqlite3,shlex
|
|
105
109
|
con=sqlite3.connect(runtime/'home/state_5.sqlite')
|
|
106
110
|
try:row=con.execute('SELECT cwd FROM threads WHERE id=?',(agent['threadId'],)).fetchone()
|
|
@@ -117,7 +121,9 @@ def attach_runtime(lib,runtime):
|
|
|
117
121
|
# The descriptor contains identity references, never tokens or cached memory.
|
|
118
122
|
atomic_json(directory/'topolo-connection.json',{'environment':selected['environment'],'contextKey':selected['contextKey'],
|
|
119
123
|
'pack':pack,'role':role,'instanceId':binding['input']['instanceId'],'agentWorkspaceId':binding['agentWorkspaceId'],
|
|
120
|
-
'bytesWorkspaceId':binding['input']['bytesWorkspaceId']
|
|
124
|
+
'bytesWorkspaceId':binding['input']['bytesWorkspaceId'],
|
|
125
|
+
'agentId':reference['agentId'],'agentVersionId':reference['agentVersionId'],
|
|
126
|
+
'identity':{key:binding['identity'][key] for key in ['userId','organizationId']}})
|
|
121
127
|
text=old+'\n<!-- topolo-continuity -->\n## Shared Topolo continuity\n'+\
|
|
122
128
|
'Before starting assigned work, refresh your approved operating instructions and scoped memory using:\n\n'+command+'\n\n'+\
|
|
123
129
|
'If retrieval fails, report the exact blocker and do not silently use stale shared memory. Read only relevant Bytes artifact references on demand. '+\
|
|
@@ -312,6 +318,28 @@ def qualify_connection_scope(scope):
|
|
|
312
318
|
except (PackError,OSError,ValueError,TypeError) as error:
|
|
313
319
|
raise ConnectionSourceRefused('Pack source or native context changed before cloud dispatch; no mutating call') from error
|
|
314
320
|
|
|
321
|
+
def private_profile_reference(client,args):
|
|
322
|
+
"""Explicit selected-profile metadata only; never open credential contents."""
|
|
323
|
+
if not getattr(args,'private_profile',False):
|
|
324
|
+
return None
|
|
325
|
+
env=getattr(client,'env',None)
|
|
326
|
+
selected=env.get('TOPOLO_CONFIG_PATH') if isinstance(env,dict) else None
|
|
327
|
+
if (not isinstance(selected,str) or not selected or
|
|
328
|
+
getattr(client,'environment',None)!=args.environment or getattr(client,'context',None)!=args.context_key):
|
|
329
|
+
raise PackError('Explicit private profile and exact selected client environment/context required')
|
|
330
|
+
path=pathlib.Path(selected)
|
|
331
|
+
try:
|
|
332
|
+
leaf=path.lstat();parent=path.parent.lstat()
|
|
333
|
+
if (not path.is_absolute() or path.resolve()!=path or not stat.S_ISREG(leaf.st_mode) or
|
|
334
|
+
leaf.st_uid!=os.getuid() or leaf.st_nlink!=1 or stat.S_IMODE(leaf.st_mode)!=0o600 or
|
|
335
|
+
not stat.S_ISDIR(parent.st_mode) or parent.st_uid!=os.getuid() or stat.S_IMODE(parent.st_mode)!=0o700):
|
|
336
|
+
raise PackError('Private profile requires an owned single-link0600 leaf in an owned0700 directory')
|
|
337
|
+
except OSError as error:
|
|
338
|
+
raise PackError('Explicit private profile metadata is unavailable; no default profile fallback') from error
|
|
339
|
+
return {'format':'topolo-private-credential-profile','schemaVersion':1,'configPath':str(path),
|
|
340
|
+
'environment':args.environment,'contextKey':args.context_key}
|
|
341
|
+
|
|
342
|
+
|
|
315
343
|
def connect(lib,args,client):
|
|
316
344
|
manifest,revision=lib.load(args.name)
|
|
317
345
|
claude_plan=claude_context_plan(lib,args.name,manifest,revision) if getattr(args,'harness','codex')=='claude' else None
|
|
@@ -337,6 +365,9 @@ def connect(lib,args,client):
|
|
|
337
365
|
if not getattr(args,'reconcile_local_context',False):
|
|
338
366
|
raise PackError('Partial Claude context attachment requires explicit --reconcile-local-context; no replay')
|
|
339
367
|
old=read_json(path,{})
|
|
368
|
+
if old.get('privateCredentialProfile') and not getattr(args,'private_profile',False):
|
|
369
|
+
raise PackError('Explicit private-profile selection required to requalify this existing private connection')
|
|
370
|
+
private_profile=private_profile_reference(client,args)
|
|
340
371
|
journal=path.with_suffix('.pending.json')
|
|
341
372
|
pending=read_json(journal,{})
|
|
342
373
|
instance=args.instance or old.get('input',{}).get('instanceId') or pending.get('input',{}).get('instanceId') or 'pack_'+uuid.uuid4().hex
|
|
@@ -345,6 +376,8 @@ def connect(lib,args,client):
|
|
|
345
376
|
'ownershipScope':args.ownership,'bytesWorkspaceId':args.bytes_workspace,'bytesScope':args.bytes_scope,'roles':roles}
|
|
346
377
|
scope=(lib,args,dict(vars(args)),canonical(manifest),revision,roles_bytes,roles,claude_plan)
|
|
347
378
|
owner=identity(client)
|
|
379
|
+
if private_profile is not None and args.context_key!='org:'+owner['organizationId']:
|
|
380
|
+
raise PackError('Private profile identity differs from the exact selected organization context')
|
|
348
381
|
qualify_connection_scope(scope)
|
|
349
382
|
journal=path.with_suffix('.pending.json')
|
|
350
383
|
if journal.exists():
|
|
@@ -358,6 +391,8 @@ def connect(lib,args,client):
|
|
|
358
391
|
reuse=bool(old and old.get('input')==payload and old.get('agentWorkspaceId')==args.agent_workspace and old.get('packRevision')==revision)
|
|
359
392
|
proposed={'schemaVersion':1,'environment':args.environment,'contextKey':args.context_key,'identity':owner,
|
|
360
393
|
'agentWorkspaceId':args.agent_workspace,'input':payload,'packRevision':revision}
|
|
394
|
+
if private_profile is not None:
|
|
395
|
+
proposed['privateCredentialProfile']={**private_profile,'identity':owner}
|
|
361
396
|
if claude_plan is not None:
|
|
362
397
|
claude_context_targets(lib,args,proposed,claude_plan)
|
|
363
398
|
if local_pending:
|
|
@@ -384,6 +419,10 @@ def connect(lib,args,client):
|
|
|
384
419
|
raise PackError('Pack connection read-back mismatch; preserve pending receipt')
|
|
385
420
|
binding={'schemaVersion':1,'environment':args.environment,'contextKey':args.context_key,'identity':owner,
|
|
386
421
|
'agentWorkspaceId':args.agent_workspace,'input':payload,'connection':readback,'packRevision':revision}
|
|
422
|
+
if private_profile is not None:
|
|
423
|
+
if private_profile_reference(client,args)!=private_profile:
|
|
424
|
+
raise PackError('Private profile selection changed during connection; preserve existing receipts')
|
|
425
|
+
binding['privateCredentialProfile']={**private_profile,'identity':owner}
|
|
387
426
|
if local_pending:
|
|
388
427
|
if binding!=old:
|
|
389
428
|
raise PackError('Claude attachment verified remote binding changed; no local replay')
|