@topolo/cli 0.13.0 → 0.13.2
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/agent-packs/README.md +154 -0
- package/agent-packs/src/topolo_agent_pack/claude.py +265 -0
- package/agent-packs/src/topolo_agent_pack/cli.py +29 -2
- package/agent-packs/src/topolo_agent_pack/execution.py +154 -0
- package/agent-packs/src/topolo_agent_pack/live.py +49 -8
- package/agent-packs/src/topolo_agent_pack/policy.py +92 -0
- package/dist/claude-pack-backend-IDQQX3DG.js +555 -0
- package/dist/index.js +256 -82
- package/package.json +5 -5
|
@@ -24,6 +24,7 @@ import urllib.parse
|
|
|
24
24
|
import uuid
|
|
25
25
|
|
|
26
26
|
from .storage import Library,PackError,atomic_json,canonical,read_json
|
|
27
|
+
from .execution import load_gate, qualify_protocol
|
|
27
28
|
|
|
28
29
|
def socket_path(lib):
|
|
29
30
|
directory=pathlib.Path('/tmp')/('topolo-agent-pack-'+hashlib.sha256(str(lib.root).encode()).hexdigest()[:16])
|
|
@@ -68,11 +69,16 @@ class BackendProxy:
|
|
|
68
69
|
self.pending=set();self.active_turns=set();self.initialized=False
|
|
69
70
|
self.quiesced=False;self.output_task=None;self.closed=False
|
|
70
71
|
self.token=secrets.token_hex(32);self.path=socket_path(lib)
|
|
72
|
+
self.execution=None;self.catalog_ready=asyncio.Event();self.catalog_id='topolo-catalog-'+uuid.uuid4().hex
|
|
73
|
+
self.catalog_rows=[];self.catalog_cursors=set()
|
|
71
74
|
|
|
72
75
|
async def start_child(self):
|
|
73
76
|
active=self.lib.active()
|
|
74
77
|
if not active.get('runtime'): raise PackError('No selected managed runtime')
|
|
75
78
|
self.runtime=active['runtime'];env=os.environ.copy()
|
|
79
|
+
self.execution=load_gate(self.runtime);self.catalog_ready.clear()
|
|
80
|
+
self.catalog_rows=[];self.catalog_cursors=set()
|
|
81
|
+
self.protocol=qualify_protocol(self.real) if self.execution else None
|
|
76
82
|
env['CODEX_HOME']=str(pathlib.Path(self.runtime)/'home');env['CODEX_CLI_PATH']=self.real
|
|
77
83
|
self.child=await asyncio.create_subprocess_exec(self.real,*self.args,env=env,stdin=asyncio.subprocess.PIPE,stdout=asyncio.subprocess.PIPE,limit=64*1024*1024)
|
|
78
84
|
self.output_task=asyncio.create_task(self.output())
|
|
@@ -80,6 +86,23 @@ class BackendProxy:
|
|
|
80
86
|
async def output(self):
|
|
81
87
|
while line:=await self.child.stdout.readline():
|
|
82
88
|
value=json.loads(line)
|
|
89
|
+
if value.get('id')==self.catalog_id:
|
|
90
|
+
self.pending.discard(self.catalog_id)
|
|
91
|
+
if self.execution and 'result' in value:
|
|
92
|
+
result=value['result'];self.catalog_rows.extend(result['data'])
|
|
93
|
+
cursor=result.get('nextCursor')
|
|
94
|
+
if cursor:
|
|
95
|
+
if cursor in self.catalog_cursors or len(self.catalog_cursors)>=20:
|
|
96
|
+
self.execution.catalog={};self.catalog_ready.set();continue
|
|
97
|
+
self.catalog_cursors.add(cursor)
|
|
98
|
+
await self.request_catalog(cursor);continue
|
|
99
|
+
self.execution.set_catalog({'data':self.catalog_rows})
|
|
100
|
+
self.catalog_ready.set();continue
|
|
101
|
+
if self.execution:
|
|
102
|
+
tracked=str(value.get('id')) in self.execution.pending
|
|
103
|
+
value=self.execution.reply(value);line=canonical(value)+b'\n'
|
|
104
|
+
if tracked:atomic_json(pathlib.Path(self.runtime)/'execution-proof.json',
|
|
105
|
+
{**self.execution.status(),'protocol':self.protocol})
|
|
83
106
|
if 'id' in value and ('result' in value or 'error' in value):
|
|
84
107
|
self.pending.discard(str(value['id']))
|
|
85
108
|
if 'result' in value and isinstance(value['result'],dict) and 'userAgent' in value['result']: self.initialized=True
|
|
@@ -100,10 +123,34 @@ class BackendProxy:
|
|
|
100
123
|
raise PackError('Owned backend did not stop; no storage switch committed')
|
|
101
124
|
if self.output_task: await self.output_task
|
|
102
125
|
|
|
126
|
+
async def forward(self,value):
|
|
127
|
+
if self.quiesced:
|
|
128
|
+
if 'id' in value:
|
|
129
|
+
sys.stdout.buffer.write(canonical({'id':value['id'],'error':{'code':-32000,'message':'Pack switch in progress; reconnecting backend'}})+b'\n');sys.stdout.buffer.flush()
|
|
130
|
+
return
|
|
131
|
+
if self.execution and value.get('method') in {'thread/resume','turn/start'} and (value.get('params') or {}).get('threadId') in self.execution.roles:
|
|
132
|
+
try:
|
|
133
|
+
await asyncio.wait_for(self.catalog_ready.wait(),10)
|
|
134
|
+
value=self.execution.prepare(value)
|
|
135
|
+
except (PackError,asyncio.TimeoutError) as exc:
|
|
136
|
+
sys.stdout.buffer.write(canonical({'id':value.get('id'),'error':{'code':-32000,'message':str(exc) or 'Native model catalog unavailable'}})+b'\n');sys.stdout.buffer.flush();return
|
|
137
|
+
if 'id' in value and 'method' in value:self.pending.add(str(value['id']))
|
|
138
|
+
self.child.stdin.write(canonical(value)+b'\n');await self.child.stdin.drain()
|
|
139
|
+
if self.execution and value.get('method')=='initialized':
|
|
140
|
+
await self.request_catalog()
|
|
141
|
+
|
|
142
|
+
async def request_catalog(self,cursor=None):
|
|
143
|
+
self.pending.add(self.catalog_id)
|
|
144
|
+
self.child.stdin.write(canonical({'id':self.catalog_id,'method':'model/list',
|
|
145
|
+
'params':{'includeHidden':True,**({'cursor':cursor} if cursor else {})}})+b'\n')
|
|
146
|
+
await self.child.stdin.drain()
|
|
147
|
+
|
|
103
148
|
def status(self):
|
|
104
149
|
return {'pid':os.getpid(),'childPid':self.child.pid if self.child else None,'desktopPid':os.getppid(),'runtime':self.runtime,
|
|
105
150
|
'quiesced':self.quiesced,'initialized':self.initialized,
|
|
106
|
-
'activeTurns':sorted(self.active_turns),'pendingRequests':len(self.pending)
|
|
151
|
+
'activeTurns':sorted(self.active_turns),'pendingRequests':len(self.pending),
|
|
152
|
+
'executionPolicy':self.execution.status() if self.execution else {'state':'not-configured'},
|
|
153
|
+
'executionProtocol':self.protocol if self.execution else None}
|
|
107
154
|
|
|
108
155
|
async def control(self,reader,writer):
|
|
109
156
|
try:
|
|
@@ -141,13 +188,7 @@ class BackendProxy:
|
|
|
141
188
|
await asyncio.get_running_loop().connect_read_pipe(lambda:protocol,sys.stdin.buffer)
|
|
142
189
|
while line:=await reader.readline():
|
|
143
190
|
value=json.loads(line)
|
|
144
|
-
|
|
145
|
-
# No new model work may start between quiesce and reconnect.
|
|
146
|
-
if 'id' in value:
|
|
147
|
-
sys.stdout.buffer.write(canonical({'id':value['id'],'error':{'code':-32000,'message':'Pack switch in progress; reconnecting backend'}})+b'\n');sys.stdout.buffer.flush()
|
|
148
|
-
continue
|
|
149
|
-
if 'id' in value and 'method' in value: self.pending.add(str(value['id']))
|
|
150
|
-
self.child.stdin.write(line);await self.child.stdin.drain()
|
|
191
|
+
await self.forward(value)
|
|
151
192
|
finally:
|
|
152
193
|
await self.stop_child();server.close();await server.wait_closed()
|
|
153
194
|
record=read_json(self.lib.root/'live-bridge.json',{})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Portable per-role execution intent, distinct from harness-applied settings.
|
|
2
|
+
|
|
3
|
+
Rendering a policy never changes a session, permission grant, or pack revision.
|
|
4
|
+
Adapters must independently apply and read back every required setting.
|
|
5
|
+
"""
|
|
6
|
+
import copy
|
|
7
|
+
|
|
8
|
+
from .storage import PackError, component_key
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
GROUPS = {'specialists': 'Topolo Specialists', 'operators': 'Topolo Operators',
|
|
12
|
+
'operator-skills': 'Operator Skills'}
|
|
13
|
+
DEEP_ROLES = {'product-direction-01', 'product-direction-02', 'system-architecture-01',
|
|
14
|
+
'system-architecture-02', 'architecture', 'security-permissions-review',
|
|
15
|
+
'adversarial-qa', 'incident-triage', 'release-recovery', 'execution-readiness',
|
|
16
|
+
'privacy-and-agreement-review', 'workflow-integration-specialist',
|
|
17
|
+
'financial-evidence-specialist', 'accounting-evidence-review',
|
|
18
|
+
'access-and-privacy'}
|
|
19
|
+
BUILD_ROLES = {'build-01', 'build-02', 'build-03', 'developer-environment-steward',
|
|
20
|
+
'platform-package-steward', 'worktree-retirement'}
|
|
21
|
+
PROFILES = {
|
|
22
|
+
'intake': {'tier': 'reasoning', 'effort': 'medium', 'localAccess': 'read-only'},
|
|
23
|
+
'analysis': {'tier': 'reasoning', 'effort': 'high', 'localAccess': 'read-only'},
|
|
24
|
+
'review': {'tier': 'workhorse', 'effort': 'high', 'localAccess': 'read-only'},
|
|
25
|
+
'build': {'tier': 'workhorse', 'effort': 'high', 'localAccess': 'workspace-write'},
|
|
26
|
+
'operator': {'tier': 'workhorse', 'effort': 'medium', 'localAccess': 'workspace-write'},
|
|
27
|
+
}
|
|
28
|
+
MODELS = {'codex': {'reasoning': 'gpt-6-astra', 'workhorse': 'gpt-6.1-sol'},
|
|
29
|
+
'claude': {'reasoning': 'claude-opus-5-5', 'workhorse': 'claude-sonnet-5-5'}}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def role_profile(agent):
|
|
33
|
+
explicit = agent.get('executionProfile')
|
|
34
|
+
if explicit is not None:
|
|
35
|
+
if explicit not in PROFILES:
|
|
36
|
+
raise PackError('Unknown execution profile for ' + component_key(agent))
|
|
37
|
+
return explicit
|
|
38
|
+
if agent['set'] == 'intakes':
|
|
39
|
+
return 'intake'
|
|
40
|
+
if agent['id'] in DEEP_ROLES:
|
|
41
|
+
return 'analysis'
|
|
42
|
+
if agent['id'] in BUILD_ROLES:
|
|
43
|
+
return 'build'
|
|
44
|
+
if agent['set'] == 'specialists':
|
|
45
|
+
return 'review'
|
|
46
|
+
return 'operator'
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def settings(agent, harness):
|
|
50
|
+
if harness not in MODELS:
|
|
51
|
+
raise PackError('Execution settings adapter is not qualified: ' + harness)
|
|
52
|
+
profile = role_profile(agent)
|
|
53
|
+
spec = PROFILES[profile]
|
|
54
|
+
model = MODELS[harness][spec['tier']]
|
|
55
|
+
if harness == 'codex':
|
|
56
|
+
native = {'model': model, 'model_reasoning_effort': spec['effort'],
|
|
57
|
+
'approval_policy': 'on-request', 'approvals_reviewer': 'user',
|
|
58
|
+
'sandbox_mode': spec['localAccess'],
|
|
59
|
+
'sandbox_workspace_write': {'network_access': False, 'writable_roots': [],
|
|
60
|
+
'exclude_slash_tmp': True, 'exclude_tmpdir_env_var': True},
|
|
61
|
+
'apps': {'_default': {'default_tools_approval_mode': 'writes'}}}
|
|
62
|
+
else:
|
|
63
|
+
native = {'model': model, 'effortLevel': spec['effort'],
|
|
64
|
+
'permissions': {'defaultMode': 'acceptEdits' if profile == 'build' else 'default'}}
|
|
65
|
+
return {'profile': profile, 'nativeSettings': native,
|
|
66
|
+
'localAccessIntent': spec['localAccess'], 'applied': False,
|
|
67
|
+
# Claude Manual is an approval mode, not a read-only filesystem sandbox.
|
|
68
|
+
'requiresAdditionalLocalConfinement': harness == 'claude' and spec['localAccess'] == 'read-only'}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def render(manifest, revision, agents, harnesses=('codex', 'claude')):
|
|
72
|
+
rows = []
|
|
73
|
+
for agent in agents:
|
|
74
|
+
rows.append({'key': component_key(agent), 'title': agent['title'],
|
|
75
|
+
'sidebar': {'group': GROUPS.get(agent['set'], agent['set']),
|
|
76
|
+
'pinned': agent.get('pinned', False)},
|
|
77
|
+
'harnesses': {h: settings(agent, h) for h in harnesses}})
|
|
78
|
+
return {'format': 'topolo-agent-execution-policy', 'schemaVersion': 1,
|
|
79
|
+
'pack': manifest['packId'], 'sourceRevision': revision, 'agents': rows,
|
|
80
|
+
'applied': False,
|
|
81
|
+
'contextPolicy': {'load': ['role charter', 'current task', 'relevant retrieved memory'],
|
|
82
|
+
'sharedMemory': 'retrieve by task and user or organization scope',
|
|
83
|
+
'history': 'preserve, do not copy between roles',
|
|
84
|
+
'skills': 'load on demand', 'toolResults': 'bounded relevant excerpts',
|
|
85
|
+
'idleModelTurns': False, 'backgroundPolling': False,
|
|
86
|
+
'handoffs': 'one concise outcome with evidence references'},
|
|
87
|
+
'verification': {'required': ['session identity', 'sidebar membership', 'pins',
|
|
88
|
+
'model', 'effort', 'permissions', 'context identity'],
|
|
89
|
+
'unsupportedSettings': 'refuse activation; never silently substitute'},
|
|
90
|
+
'authority': {'externalActions': 'existing human authorization and platform access controls',
|
|
91
|
+
'packMembershipGrantsAuthority': False},
|
|
92
|
+
'profiles': copy.deepcopy(PROFILES)}
|