@topolo/cli 0.13.9 → 0.13.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -5
- package/agent-packs/README.md +128 -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 +42 -5
- package/agent-packs/src/topolo_agent_pack/execution.py +70 -2
- package/agent-packs/src/topolo_agent_pack/lanes.py +902 -0
- package/agent-packs/src/topolo_agent_pack/live.py +982 -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 +172 -8
- package/dist/{claude-pack-backend-4F4VHMI5.js → claude-pack-backend-YEVPAOID.js} +969 -16
- package/dist/index.js +65 -8
- package/package.json +5 -5
|
@@ -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,26 +1,28 @@
|
|
|
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):
|
|
6
6
|
pass
|
|
7
7
|
|
|
8
8
|
class Client:
|
|
9
|
-
def __init__(self,environment,context,binary=None):
|
|
9
|
+
def __init__(self,environment,context,binary=None,env=None,node=None):
|
|
10
10
|
if environment not in {'development','staging','production'}:
|
|
11
11
|
raise PackError('Explicit Topolo environment required')
|
|
12
12
|
if not context or any(c in context for c in '\n\r'):
|
|
13
13
|
raise PackError('Explicit Topolo context required')
|
|
14
|
-
|
|
14
|
+
selected=os.environ if env is None else env
|
|
15
|
+
if any(selected.get(k) for k in ['TOPOLO_API_KEY','TOPOLO_ACCESS_TOKEN','TOPOLO_'+environment.upper()+'_API_KEY','TOPOLO_'+environment.upper()+'_ACCESS_TOKEN']):
|
|
15
16
|
raise PackError('Pinned connections require the CLI protected account context, not injected credentials')
|
|
16
17
|
self.environment=environment;self.context=context
|
|
17
18
|
self.binary=binary or shutil.which('topolo')
|
|
18
19
|
if not self.binary:
|
|
19
20
|
raise PackError('Install the public @topolo/cli and authenticate before connecting')
|
|
20
|
-
self.env={**
|
|
21
|
-
self.
|
|
21
|
+
self.env={**selected,'TOPOLO_CONTEXT':context}
|
|
22
|
+
self.command=[node,self.binary] if node else [self.binary]
|
|
23
|
+
self.env['PATH']=(str(pathlib.Path(node).parent)+os.pathsep+'/usr/bin:/bin') if node else str(pathlib.Path(self.binary).parent)+os.pathsep+self.env.get('PATH','')
|
|
22
24
|
def run(self,args):
|
|
23
|
-
p=subprocess.run([self.
|
|
25
|
+
p=subprocess.run([*self.command,'--env',self.environment,*args,'--json'],env=self.env,capture_output=True,text=True,timeout=90)
|
|
24
26
|
if p.returncode:
|
|
25
27
|
raise PackError('Topolo command failed (exit '+str(p.returncode)+'); inspect the published action, account permissions and environment. No fallback was used.')
|
|
26
28
|
try:value=json.loads(p.stdout)
|
|
@@ -100,7 +102,11 @@ def attach_runtime(lib,runtime):
|
|
|
100
102
|
if not pointer.exists():continue
|
|
101
103
|
selected=read_json(pointer);binding=read_json(binding_path(lib,pack,selected['environment'],selected['contextKey']))
|
|
102
104
|
role=agent['set']+'/'+agent['id']
|
|
103
|
-
|
|
105
|
+
matches=[r for r in binding['input']['roles'] if r['logicalId']==role]
|
|
106
|
+
if not matches:continue
|
|
107
|
+
if len(matches)!=1:
|
|
108
|
+
raise PackError('Connected role binding is not unique')
|
|
109
|
+
reference=matches[0]
|
|
104
110
|
import sqlite3,shlex
|
|
105
111
|
con=sqlite3.connect(runtime/'home/state_5.sqlite')
|
|
106
112
|
try:row=con.execute('SELECT cwd FROM threads WHERE id=?',(agent['threadId'],)).fetchone()
|
|
@@ -117,7 +123,9 @@ def attach_runtime(lib,runtime):
|
|
|
117
123
|
# The descriptor contains identity references, never tokens or cached memory.
|
|
118
124
|
atomic_json(directory/'topolo-connection.json',{'environment':selected['environment'],'contextKey':selected['contextKey'],
|
|
119
125
|
'pack':pack,'role':role,'instanceId':binding['input']['instanceId'],'agentWorkspaceId':binding['agentWorkspaceId'],
|
|
120
|
-
'bytesWorkspaceId':binding['input']['bytesWorkspaceId']
|
|
126
|
+
'bytesWorkspaceId':binding['input']['bytesWorkspaceId'],
|
|
127
|
+
'agentId':reference['agentId'],'agentVersionId':reference['agentVersionId'],
|
|
128
|
+
'identity':{key:binding['identity'][key] for key in ['userId','organizationId']}})
|
|
121
129
|
text=old+'\n<!-- topolo-continuity -->\n## Shared Topolo continuity\n'+\
|
|
122
130
|
'Before starting assigned work, refresh your approved operating instructions and scoped memory using:\n\n'+command+'\n\n'+\
|
|
123
131
|
'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 +320,28 @@ def qualify_connection_scope(scope):
|
|
|
312
320
|
except (PackError,OSError,ValueError,TypeError) as error:
|
|
313
321
|
raise ConnectionSourceRefused('Pack source or native context changed before cloud dispatch; no mutating call') from error
|
|
314
322
|
|
|
323
|
+
def private_profile_reference(client,args):
|
|
324
|
+
"""Explicit selected-profile metadata only; never open credential contents."""
|
|
325
|
+
if not getattr(args,'private_profile',False):
|
|
326
|
+
return None
|
|
327
|
+
env=getattr(client,'env',None)
|
|
328
|
+
selected=env.get('TOPOLO_CONFIG_PATH') if isinstance(env,dict) else None
|
|
329
|
+
if (not isinstance(selected,str) or not selected or
|
|
330
|
+
getattr(client,'environment',None)!=args.environment or getattr(client,'context',None)!=args.context_key):
|
|
331
|
+
raise PackError('Explicit private profile and exact selected client environment/context required')
|
|
332
|
+
path=pathlib.Path(selected)
|
|
333
|
+
try:
|
|
334
|
+
leaf=path.lstat();parent=path.parent.lstat()
|
|
335
|
+
if (not path.is_absolute() or path.resolve()!=path or not stat.S_ISREG(leaf.st_mode) or
|
|
336
|
+
leaf.st_uid!=os.getuid() or leaf.st_nlink!=1 or stat.S_IMODE(leaf.st_mode)!=0o600 or
|
|
337
|
+
not stat.S_ISDIR(parent.st_mode) or parent.st_uid!=os.getuid() or stat.S_IMODE(parent.st_mode)!=0o700):
|
|
338
|
+
raise PackError('Private profile requires an owned single-link0600 leaf in an owned0700 directory')
|
|
339
|
+
except OSError as error:
|
|
340
|
+
raise PackError('Explicit private profile metadata is unavailable; no default profile fallback') from error
|
|
341
|
+
return {'format':'topolo-private-credential-profile','schemaVersion':1,'configPath':str(path),
|
|
342
|
+
'environment':args.environment,'contextKey':args.context_key}
|
|
343
|
+
|
|
344
|
+
|
|
315
345
|
def connect(lib,args,client):
|
|
316
346
|
manifest,revision=lib.load(args.name)
|
|
317
347
|
claude_plan=claude_context_plan(lib,args.name,manifest,revision) if getattr(args,'harness','codex')=='claude' else None
|
|
@@ -337,6 +367,9 @@ def connect(lib,args,client):
|
|
|
337
367
|
if not getattr(args,'reconcile_local_context',False):
|
|
338
368
|
raise PackError('Partial Claude context attachment requires explicit --reconcile-local-context; no replay')
|
|
339
369
|
old=read_json(path,{})
|
|
370
|
+
if old.get('privateCredentialProfile') and not getattr(args,'private_profile',False):
|
|
371
|
+
raise PackError('Explicit private-profile selection required to requalify this existing private connection')
|
|
372
|
+
private_profile=private_profile_reference(client,args)
|
|
340
373
|
journal=path.with_suffix('.pending.json')
|
|
341
374
|
pending=read_json(journal,{})
|
|
342
375
|
instance=args.instance or old.get('input',{}).get('instanceId') or pending.get('input',{}).get('instanceId') or 'pack_'+uuid.uuid4().hex
|
|
@@ -345,6 +378,8 @@ def connect(lib,args,client):
|
|
|
345
378
|
'ownershipScope':args.ownership,'bytesWorkspaceId':args.bytes_workspace,'bytesScope':args.bytes_scope,'roles':roles}
|
|
346
379
|
scope=(lib,args,dict(vars(args)),canonical(manifest),revision,roles_bytes,roles,claude_plan)
|
|
347
380
|
owner=identity(client)
|
|
381
|
+
if private_profile is not None and args.context_key!='org:'+owner['organizationId']:
|
|
382
|
+
raise PackError('Private profile identity differs from the exact selected organization context')
|
|
348
383
|
qualify_connection_scope(scope)
|
|
349
384
|
journal=path.with_suffix('.pending.json')
|
|
350
385
|
if journal.exists():
|
|
@@ -358,6 +393,8 @@ def connect(lib,args,client):
|
|
|
358
393
|
reuse=bool(old and old.get('input')==payload and old.get('agentWorkspaceId')==args.agent_workspace and old.get('packRevision')==revision)
|
|
359
394
|
proposed={'schemaVersion':1,'environment':args.environment,'contextKey':args.context_key,'identity':owner,
|
|
360
395
|
'agentWorkspaceId':args.agent_workspace,'input':payload,'packRevision':revision}
|
|
396
|
+
if private_profile is not None:
|
|
397
|
+
proposed['privateCredentialProfile']={**private_profile,'identity':owner}
|
|
361
398
|
if claude_plan is not None:
|
|
362
399
|
claude_context_targets(lib,args,proposed,claude_plan)
|
|
363
400
|
if local_pending:
|
|
@@ -384,6 +421,10 @@ def connect(lib,args,client):
|
|
|
384
421
|
raise PackError('Pack connection read-back mismatch; preserve pending receipt')
|
|
385
422
|
binding={'schemaVersion':1,'environment':args.environment,'contextKey':args.context_key,'identity':owner,
|
|
386
423
|
'agentWorkspaceId':args.agent_workspace,'input':payload,'connection':readback,'packRevision':revision}
|
|
424
|
+
if private_profile is not None:
|
|
425
|
+
if private_profile_reference(client,args)!=private_profile:
|
|
426
|
+
raise PackError('Private profile selection changed during connection; preserve existing receipts')
|
|
427
|
+
binding['privateCredentialProfile']={**private_profile,'identity':owner}
|
|
387
428
|
if local_pending:
|
|
388
429
|
if binding!=old:
|
|
389
430
|
raise PackError('Claude attachment verified remote binding changed; no local replay')
|
|
@@ -560,6 +601,8 @@ def connection_lock(lib,args):
|
|
|
560
601
|
yield
|
|
561
602
|
|
|
562
603
|
def dispatch(lib,args):
|
|
604
|
+
if args.command=='remote-context' and getattr(args,'continuity_input',None):
|
|
605
|
+
return native_cli_continuity_read(lib,args)
|
|
563
606
|
client=Client(args.environment,args.context_key)
|
|
564
607
|
if args.command=='connect':return connect(lib,args,client)
|
|
565
608
|
if args.command=='remote-context':return remote_context(lib,args,client)[1]
|
|
@@ -571,3 +614,124 @@ def dispatch(lib,args):
|
|
|
571
614
|
reference=read_json(args.reference)
|
|
572
615
|
return read_artifact(client,binding,reference['key'],reference,args.destination)
|
|
573
616
|
raise PackError('Unsupported Topolo command')
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def native_cli_response(stdout,stderr,service):
|
|
621
|
+
"""Preserve only canonical public response metadata; never echo raw stderr."""
|
|
622
|
+
from . import lanes
|
|
623
|
+
if len(stdout.encode())>262144 or len(stderr.encode())>65536:
|
|
624
|
+
raise PackError('native_cli_request_metadata_unavailable: bounded public response required')
|
|
625
|
+
try:
|
|
626
|
+
body=json.loads(stdout)
|
|
627
|
+
except ValueError as error:
|
|
628
|
+
raise PackError('native_cli_request_metadata_unavailable: invalid public JSON') from error
|
|
629
|
+
if not isinstance(body,dict) or body.get('error'):
|
|
630
|
+
raise PackError('native_cli_request_metadata_unavailable: unsuccessful public response')
|
|
631
|
+
events=[];seen=set()
|
|
632
|
+
pattern=r'^\[topolo\] ← (\d{3}) (GET|POST) (auth|agent) (/[^\s?#\\]+) \([0-9.]+ms, req=([A-Za-z0-9][A-Za-z0-9._:-]{0,127})\)$'
|
|
633
|
+
for line in stderr.splitlines():
|
|
634
|
+
match=re.fullmatch(pattern,line)
|
|
635
|
+
if not match:
|
|
636
|
+
continue
|
|
637
|
+
status,method,target,path,request_id=match.groups()
|
|
638
|
+
if request_id=='unavailable' or request_id in seen or not 200<=int(status)<300:
|
|
639
|
+
raise PackError('native_cli_request_metadata_unavailable: ambiguous or unsuccessful response request IDs')
|
|
640
|
+
seen.add(request_id);events.append({'requestId':request_id,'method':method,'service':target,'path':path,'status':int(status)})
|
|
641
|
+
if not 1<=len(events)<=8 or service not in {event['service'] for event in events}:
|
|
642
|
+
raise PackError('native_cli_request_metadata_unavailable: actual canonical server response IDs required')
|
|
643
|
+
return body.get('data',body),events
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def native_cli_command_response(client,args,service):
|
|
647
|
+
env={**client.env,'TOPOLO_DEBUG':'1'}
|
|
648
|
+
result=subprocess.run([*client.command,'--env',client.environment,*args,'--json'],env=env,
|
|
649
|
+
capture_output=True,text=True,timeout=90)
|
|
650
|
+
if result.returncode:
|
|
651
|
+
raise PackError('native_cli_request_metadata_unavailable: public command refused; no fallback or retry')
|
|
652
|
+
return native_cli_response(result.stdout,result.stderr,service)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def native_cli_selected_environment(value,binding,host_env):
|
|
656
|
+
from . import lanes
|
|
657
|
+
from types import SimpleNamespace
|
|
658
|
+
context=value['context'];profile=binding.get('privateCredentialProfile')
|
|
659
|
+
lanes.native_cli_closed(profile,{'format','schemaVersion','configPath','environment','contextKey','identity'},'private profile reference')
|
|
660
|
+
if (profile['format']!='topolo-private-credential-profile' or profile['schemaVersion']!=1 or
|
|
661
|
+
profile['environment']!=context['environment'] or profile['contextKey']!=context['contextKey'] or
|
|
662
|
+
profile['identity']!={'userId':context['principalId'],'organizationId':context['organizationId']}):
|
|
663
|
+
raise PackError('Exact native-cli approved profile/principal/context reference required')
|
|
664
|
+
selected=SimpleNamespace(env={'TOPOLO_CONFIG_PATH':profile['configPath']},environment=context['environment'],context=context['contextKey'])
|
|
665
|
+
args=SimpleNamespace(private_profile=True,environment=context['environment'],context_key=context['contextKey'])
|
|
666
|
+
private_profile_reference(selected,args)
|
|
667
|
+
for path,sha in [(value['cli']['nodeExecutable'],value['cli']['nodeDigest']),(value['cli']['entry'],value['cli']['entryDigest'])]:
|
|
668
|
+
p=pathlib.Path(path);info=p.lstat()
|
|
669
|
+
if (p.resolve()!=p or not stat.S_ISREG(info.st_mode) or info.st_uid not in {0,os.getuid()} or info.st_mode&0o022 or
|
|
670
|
+
not info.st_mode&0o111 or hashlib.sha256(p.read_bytes()).hexdigest()!=sha):
|
|
671
|
+
raise PackError('Native-cli executable source/provenance changed')
|
|
672
|
+
allowed={'HOME','USER','LOGNAME','PATH','LANG','LC_ALL','TMPDIR'}
|
|
673
|
+
env={k:v for k,v in host_env.items() if k in allowed and isinstance(v,str)}
|
|
674
|
+
env['PATH']=str(pathlib.Path(value['cli']['nodeExecutable']).parent)+os.pathsep+'/usr/bin:/bin'
|
|
675
|
+
env.update(TOPOLO_CONFIG_PATH=profile['configPath'],TOPOLO_ENV=context['environment'],TOPOLO_CONTEXT=context['contextKey'])
|
|
676
|
+
return env
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def native_cli_continuity_read(lib,args,*,_admit=None,_client_factory=None,_response=None):
|
|
680
|
+
"""Native command producer. No current runtime authority producer exists.
|
|
681
|
+
|
|
682
|
+
Injected callbacks are internal hermetic seams, never CLI/JSON options.
|
|
683
|
+
Receipt output is public response evidence, not native-query attribution.
|
|
684
|
+
"""
|
|
685
|
+
from . import lanes
|
|
686
|
+
path=pathlib.Path(args.continuity_input);info=path.lstat()
|
|
687
|
+
if (path.is_symlink() or not stat.S_ISREG(info.st_mode) or info.st_uid!=os.getuid() or info.st_nlink!=1 or
|
|
688
|
+
info.st_mode&0o077 or info.st_size>65536):
|
|
689
|
+
raise PackError('Exact owned bounded native-cli input required')
|
|
690
|
+
value=lanes.native_cli_input(read_json(path))
|
|
691
|
+
(lanes.native_cli_admission if _admit is None else _admit)(value)
|
|
692
|
+
context=value['context']
|
|
693
|
+
if (args.environment!=context['environment'] or args.context_key!=context['contextKey'] or args.role!=context['logicalId'] or
|
|
694
|
+
value['sourceRoleKey']!=args.name+'/'+args.role):
|
|
695
|
+
raise PackError('Native-cli command differs from the exact source/operation')
|
|
696
|
+
file=binding_path(lib,args.name,args.environment,args.context_key);info=file.lstat()
|
|
697
|
+
if (file.is_symlink() or not stat.S_ISREG(info.st_mode) or info.st_uid!=os.getuid() or info.st_nlink!=1 or
|
|
698
|
+
hashlib.sha256(file.read_bytes()).hexdigest()!=value['source']['bindingDigest']):
|
|
699
|
+
raise PackError('Native-cli current owned binding source differs')
|
|
700
|
+
binding=read_json(file);roles=[r for r in binding['input']['roles'] if r['logicalId']==args.role]
|
|
701
|
+
if (len(roles)!=1 or binding['identity']!={'userId':context['principalId'],'organizationId':context['organizationId']} or
|
|
702
|
+
binding['packRevision']!=value['source']['packRevision'] or binding['input']['instanceId']!=context['installationId'] or
|
|
703
|
+
binding['agentWorkspaceId']!=context['agentWorkspaceId'] or binding['input']['bytesWorkspaceId']!=context['bytesWorkspaceId'] or
|
|
704
|
+
binding['input']['bytesScope']!=context['bytesScope'] or roles[0]['agentId']!=context['agentId'] or roles[0]['agentVersionId']!=context['agentVersionId']):
|
|
705
|
+
raise PackError('Native-cli source principal/Agent/Bytes binding differs')
|
|
706
|
+
env=native_cli_selected_environment(value,binding,os.environ)
|
|
707
|
+
client=(Client if _client_factory is None else _client_factory)(args.environment,args.context_key,
|
|
708
|
+
binary=value['cli']['entry'],env=env,node=value['cli']['nodeExecutable'])
|
|
709
|
+
call=native_cli_command_response if _response is None else _response
|
|
710
|
+
identity,identity_events=call(client,['whoami'],'auth')
|
|
711
|
+
if (identity.get('user',{}).get('id')!=context['principalId'] or (identity.get('organization')or{}).get('id')!=context['organizationId']):
|
|
712
|
+
raise PackError('Actual native-cli public identity differs')
|
|
713
|
+
with tempfile.TemporaryDirectory(prefix='topolo-native-cli-context-') as directory:
|
|
714
|
+
input_file=pathlib.Path(directory)/'input.json';atomic_json(input_file,{'instanceId':context['installationId'],'logicalId':context['logicalId']})
|
|
715
|
+
envelope,context_events=call(client,['actions','call','app_topolo_agent.agent_packs.context','--detail','--data','@'+str(input_file),
|
|
716
|
+
'--resource-type','workspace','--resource-id',context['agentWorkspaceId']],'agent')
|
|
717
|
+
result=envelope.get('data',envelope)
|
|
718
|
+
actual=result.get('context',{})
|
|
719
|
+
expected={'organizationId':context['organizationId'],'workspaceId':context['agentWorkspaceId'],'instanceId':context['installationId'],
|
|
720
|
+
'logicalId':context['logicalId'],'agentId':context['agentId'],'agentVersionId':context['agentVersionId'],
|
|
721
|
+
'artifacts':{'appId':'app_topolo_bytes','resourceType':'workspace','resourceId':context['bytesWorkspaceId'],'scope':context['bytesScope']}}
|
|
722
|
+
if envelope.get('success') is False or any(actual.get(k)!=v for k,v in expected.items()):
|
|
723
|
+
raise PackError('Actual native-cli public Agent/Bytes context differs')
|
|
724
|
+
# Recheck mutable public binding, binary and stat-only profile after awaited reads.
|
|
725
|
+
if hashlib.sha256(file.read_bytes()).hexdigest()!=value['source']['bindingDigest']:
|
|
726
|
+
raise PackError('Native-cli binding changed during response; no receipt ACK')
|
|
727
|
+
native_cli_selected_environment(value,binding,os.environ)
|
|
728
|
+
(lanes.native_cli_admission if _admit is None else _admit)(value)
|
|
729
|
+
responses={}
|
|
730
|
+
for kind,body,events in [('identity',{'userId':context['principalId'],'organizationId':context['organizationId']},identity_events),
|
|
731
|
+
('context',expected,context_events)]:
|
|
732
|
+
responses[kind]={'requestEvents':events,'bodyProjectionDigest':lanes.digest(body),'actionId':None if kind=='identity' else 'app_topolo_agent.agent_packs.context',
|
|
733
|
+
'resourceType':None if kind=='identity' else 'workspace','resourceId':None if kind=='identity' else context['agentWorkspaceId']}
|
|
734
|
+
receipt={k:value[k] for k in lanes.NATIVE_CLI_RECEIPT_FIELDS-{'format','schemaVersion','responses','claim'}}
|
|
735
|
+
receipt.update(format='topolo-native-cli-continuity-receipt',schemaVersion=1,responses=responses,
|
|
736
|
+
claim='PUBLIC_IDENTITY_CONTEXT_RESPONSE_ONLY_NATIVE_ATTRIBUTION_REQUIRED')
|
|
737
|
+
return lanes.native_cli_receipt(value,receipt)
|