@topolo/cli 0.13.17 → 0.13.21
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 +18 -4
- package/agent-packs/README.md +469 -1
- package/agent-packs/coverage_changed.py +1 -1
- package/agent-packs/docs/selected-artifacts.md +47 -0
- package/agent-packs/documentation/cloud-task-state.md +56 -0
- package/agent-packs/domain-contexts/README.md +25 -0
- package/agent-packs/domain-contexts/cicd.json +8 -0
- package/agent-packs/gui/feed-adapter.js +4 -0
- package/agent-packs/gui/index.html +9 -0
- package/agent-packs/gui/studio.css +14 -0
- package/agent-packs/gui/studio.js +121 -0
- package/agent-packs/gui/studio_server.py +355 -0
- package/agent-packs/roles/cicd/CLAUDE.md +24 -0
- package/agent-packs/specializations/cicd--cicd.json +5 -0
- package/agent-packs/specializations/cicd--pipeline-engineering.json +5 -0
- package/agent-packs/specializations/cicd--promotion-deployment.json +5 -0
- package/agent-packs/specializations/cicd--queue-recovery.json +5 -0
- package/agent-packs/specializations/cicd--release-verification.json +5 -0
- package/agent-packs/specializations/cicd--runner-capacity.json +5 -0
- package/agent-packs/specializations/intakes--engineering-02.json +5 -0
- package/agent-packs/specializations/intakes--engineering-03.json +5 -0
- package/agent-packs/specializations/intakes--engineering.json +5 -0
- package/agent-packs/specializations/intakes--operator-01.json +5 -0
- package/agent-packs/specializations/intakes--operator-02.json +5 -0
- package/agent-packs/specializations/operator-skills--audio-and-narration-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--brand-and-copywriting-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--capability-research-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--data-analysis-and-visualization.json +5 -0
- package/agent-packs/specializations/operator-skills--financial-evidence-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--operating-template-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--operator-skill-library.json +5 -0
- package/agent-packs/specializations/operator-skills--operator-skills.json +5 -0
- package/agent-packs/specializations/operator-skills--operator-workflow-evaluation.json +5 -0
- package/agent-packs/specializations/operator-skills--privacy-and-agreement-review.json +5 -0
- package/agent-packs/specializations/operator-skills--storytelling-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--ux-and-accessibility-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--video-production-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--visual-design-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--workflow-integration-specialist.json +5 -0
- package/agent-packs/specializations/operators--access-and-privacy.json +5 -0
- package/agent-packs/specializations/operators--accounting-evidence-review.json +5 -0
- package/agent-packs/specializations/operators--agreement-operations.json +5 -0
- package/agent-packs/specializations/operators--books-operator.json +5 -0
- package/agent-packs/specializations/operators--brand-operator.json +5 -0
- package/agent-packs/specializations/operators--commerce-operations.json +5 -0
- package/agent-packs/specializations/operators--communications-operator.json +5 -0
- package/agent-packs/specializations/operators--content-and-assets-operator.json +5 -0
- package/agent-packs/specializations/operators--customer-operations.json +5 -0
- package/agent-packs/specializations/operators--design-operator.json +5 -0
- package/agent-packs/specializations/operators--design-production-02.json +5 -0
- package/agent-packs/specializations/operators--design-production-03.json +5 -0
- package/agent-packs/specializations/operators--design-production-04.json +5 -0
- package/agent-packs/specializations/operators--device-and-feed-operator.json +5 -0
- package/agent-packs/specializations/operators--director-operator.json +5 -0
- package/agent-packs/specializations/operators--experience-review.json +5 -0
- package/agent-packs/specializations/operators--growth-operations.json +5 -0
- package/agent-packs/specializations/operators--learn-operator.json +5 -0
- package/agent-packs/specializations/operators--localization-operator.json +5 -0
- package/agent-packs/specializations/operators--operators.json +5 -0
- package/agent-packs/specializations/operators--pay-operator.json +5 -0
- package/agent-packs/specializations/operators--research-and-intelligence.json +5 -0
- package/agent-packs/specializations/operators--roadmap-operator.json +5 -0
- package/agent-packs/specializations/operators--social-content-operator.json +5 -0
- package/agent-packs/specializations/operators--talent-operations.json +5 -0
- package/agent-packs/specializations/operators--web-operator.json +5 -0
- package/agent-packs/specializations/operators--workflow-operator.json +5 -0
- package/agent-packs/specializations/specialists--adversarial-qa.json +5 -0
- package/agent-packs/specializations/specialists--backlog-dispatch.json +5 -0
- package/agent-packs/specializations/specialists--build-01.json +5 -0
- package/agent-packs/specializations/specialists--build-02.json +5 -0
- package/agent-packs/specializations/specialists--build-03.json +5 -0
- package/agent-packs/specializations/specialists--developer-environment-steward.json +5 -0
- package/agent-packs/specializations/specialists--documentation-knowledge-steward.json +5 -0
- package/agent-packs/specializations/specialists--execution-readiness.json +5 -0
- package/agent-packs/specializations/specialists--incident-triage.json +5 -0
- package/agent-packs/specializations/specialists--platform-package-steward.json +5 -0
- package/agent-packs/specializations/specialists--product-direction-01.json +5 -0
- package/agent-packs/specializations/specialists--product-direction-02.json +5 -0
- package/agent-packs/specializations/specialists--release-recovery.json +5 -0
- package/agent-packs/specializations/specialists--release-watch.json +5 -0
- package/agent-packs/specializations/specialists--roadmap-planning-01.json +5 -0
- package/agent-packs/specializations/specialists--roadmap-planning-02.json +5 -0
- package/agent-packs/specializations/specialists--security-permissions-review.json +5 -0
- package/agent-packs/specializations/specialists--system-architecture-01.json +5 -0
- package/agent-packs/specializations/specialists--system-architecture-02.json +5 -0
- package/agent-packs/specializations/specialists--translation.json +5 -0
- package/agent-packs/specializations/specialists--ux-consistency-review.json +5 -0
- package/agent-packs/specializations/specialists--workflow-improvement.json +5 -0
- package/agent-packs/specializations/specialists--worktree-retirement.json +5 -0
- package/agent-packs/src/topolo_agent_pack/cicd.py +101 -0
- package/agent-packs/src/topolo_agent_pack/claude.py +22 -7
- package/agent-packs/src/topolo_agent_pack/claude_activation.py +157 -0
- package/agent-packs/src/topolo_agent_pack/claude_completion_recovery.py +216 -0
- package/agent-packs/src/topolo_agent_pack/claude_desktop.py +181 -0
- package/agent-packs/src/topolo_agent_pack/claude_dispatch.py +240 -0
- package/agent-packs/src/topolo_agent_pack/claude_prompt.py +74 -0
- package/agent-packs/src/topolo_agent_pack/claude_runner.py +207 -0
- package/agent-packs/src/topolo_agent_pack/claude_service.py +397 -0
- package/agent-packs/src/topolo_agent_pack/cli.py +188 -18
- package/agent-packs/src/topolo_agent_pack/closed_ledger_fence.py +118 -0
- package/agent-packs/src/topolo_agent_pack/cloud_attach.py +128 -0
- package/agent-packs/src/topolo_agent_pack/cloud_cutover.py +134 -0
- package/agent-packs/src/topolo_agent_pack/cloud_delta.py +162 -0
- package/agent-packs/src/topolo_agent_pack/cloud_migrate.py +142 -0
- package/agent-packs/src/topolo_agent_pack/cloud_recovery.py +42 -0
- package/agent-packs/src/topolo_agent_pack/cloud_work.py +126 -0
- package/agent-packs/src/topolo_agent_pack/coordinator_activity.py +27 -0
- package/agent-packs/src/topolo_agent_pack/coordinator_context.py +91 -0
- package/agent-packs/src/topolo_agent_pack/coordinator_refresh.py +116 -0
- package/agent-packs/src/topolo_agent_pack/desktop_acceptance.py +45 -0
- package/agent-packs/src/topolo_agent_pack/desktop_launcher.py +114 -0
- package/agent-packs/src/topolo_agent_pack/domain_context.py +38 -0
- package/agent-packs/src/topolo_agent_pack/execution.py +104 -7
- package/agent-packs/src/topolo_agent_pack/fresh.py +1 -1
- package/agent-packs/src/topolo_agent_pack/frontdoor.py +23 -0
- package/agent-packs/src/topolo_agent_pack/linked_task_context.py +80 -0
- package/agent-packs/src/topolo_agent_pack/live.py +285 -18
- package/agent-packs/src/topolo_agent_pack/native.py +160 -4
- package/agent-packs/src/topolo_agent_pack/policy.py +8 -0
- package/agent-packs/src/topolo_agent_pack/review_context.py +33 -0
- package/agent-packs/src/topolo_agent_pack/role_catalog.py +137 -0
- package/agent-packs/src/topolo_agent_pack/role_context.py +63 -0
- package/agent-packs/src/topolo_agent_pack/role_refresh.py +50 -0
- package/agent-packs/src/topolo_agent_pack/task_models.py +11 -7
- package/agent-packs/src/topolo_agent_pack/throughput.py +149 -0
- package/agent-packs/src/topolo_agent_pack/topolo.py +7 -1
- package/agent-packs/src/topolo_agent_pack/work.py +322 -0
- package/agent-packs/src/topolo_agent_pack/work_adoption.py +122 -0
- package/agent-packs/src/topolo_agent_pack/work_artifacts.py +84 -0
- package/agent-packs/src/topolo_agent_pack/work_context.py +77 -0
- package/agent-packs/src/topolo_agent_pack/work_gui.py +51 -0
- package/agent-packs/src/topolo_agent_pack/work_native.py +414 -0
- package/agent-packs/src/topolo_agent_pack/work_wait.py +151 -0
- package/agent-packs/templates/topolo-coordinators/AGENTS.md +7 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/cicd/AGENTS.md +6 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/cicd/config.json +4 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/engineering/AGENTS.md +3 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/engineering/config.json +4 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/operator-01/AGENTS.md +3 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/operator-01/config.json +4 -0
- package/agent-packs/templates/topolo-coordinators/pack.json +56 -0
- package/dist/chunk-NYDIAYVY.js +144 -0
- package/dist/{claude-pack-backend-YEVPAOID.js → chunk-UY5LEU5C.js} +1659 -21
- package/dist/claude-desktop-profile-JCQKSY3K.js +13 -0
- package/dist/claude-pack-activation-Q25MCEPU.js +506 -0
- package/dist/claude-pack-backend-HUTKGIDS.js +90 -0
- package/dist/index.js +408 -164
- package/package.json +8 -6
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Task workers in the already authenticated Claude Desktop process.
|
|
2
|
+
|
|
3
|
+
The closed native endpoint survives closing the temporary setup inspector.
|
|
4
|
+
It exposes task lifecycle operations only, never vendor credentials or eval.
|
|
5
|
+
"""
|
|
6
|
+
import asyncio
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import pathlib
|
|
11
|
+
import shlex
|
|
12
|
+
import socket
|
|
13
|
+
import stat
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from .claude_runner import public_progress
|
|
17
|
+
from .storage import read_json, canonical, atomic_json
|
|
18
|
+
from .work import require
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def call(connection, operation):
|
|
22
|
+
info = pathlib.Path(connection['secretPath'])
|
|
23
|
+
metadata = info.lstat()
|
|
24
|
+
require(stat.S_ISREG(metadata.st_mode) and metadata.st_uid == os.getuid()
|
|
25
|
+
and metadata.st_mode & 0o077 == 0, 'Unowned native task connection secret')
|
|
26
|
+
target = pathlib.Path(connection['socket']).lstat()
|
|
27
|
+
require(stat.S_ISSOCK(target.st_mode) and target.st_uid == os.getuid(), 'Unowned native task socket')
|
|
28
|
+
secret = read_json(info)
|
|
29
|
+
payload = {'pid': connection['pid'], 'identity': connection['identity'],
|
|
30
|
+
'token': secret['token'], 'operation': operation}
|
|
31
|
+
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as channel:
|
|
32
|
+
channel.settimeout(65)
|
|
33
|
+
channel.connect(connection['socket'])
|
|
34
|
+
channel.sendall(canonical(payload) + b'\n')
|
|
35
|
+
with channel.makefile('rb') as stream:
|
|
36
|
+
raw = stream.readline(1048577)
|
|
37
|
+
require(raw.endswith(b'\n') and len(raw) <= 1048576, 'Native task response is missing or exceeds its boundary')
|
|
38
|
+
result = json.loads(raw)
|
|
39
|
+
require(set(result) == {'result'}, 'Native task operation refused: ' + str(result.get('error', 'invalid response')))
|
|
40
|
+
return result['result']
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def authorize(root, operation):
|
|
44
|
+
"""Independent cloud read before a native creation, send or archive."""
|
|
45
|
+
from .cloud_work import CloudStore
|
|
46
|
+
from .work_artifacts import read_checkpoint
|
|
47
|
+
plan = operation['plan']
|
|
48
|
+
store = CloudStore(root)
|
|
49
|
+
worker = store.worker(plan['workerId'])
|
|
50
|
+
task = store.get(worker['taskId'])
|
|
51
|
+
require(task.get('ownerInstallationId') == store.binding['installationId']
|
|
52
|
+
and worker.get('nativePlan') == plan and worker['taskId'] == plan['taskId']
|
|
53
|
+
and worker['nativeId'] == plan['nativeId'], 'Native worker cloud ownership differs')
|
|
54
|
+
if operation['kind'] == 'create':
|
|
55
|
+
require(set(operation) == {'kind', 'plan', 'files'} and worker['state'] == 'creating'
|
|
56
|
+
and hashlib.sha256(canonical(operation['files'])).hexdigest() == plan['contextDigest'],
|
|
57
|
+
'Native creation has no exact cloud reservation')
|
|
58
|
+
elif operation['kind'] == 'send':
|
|
59
|
+
require(set(operation) == {'kind', 'plan', 'workerId', 'assignment'}
|
|
60
|
+
and worker['state'] == 'running' and worker['turnId'] == operation['assignment']['messageId']
|
|
61
|
+
and worker['resolution']['nativeACK'] == {'model': plan['model'], 'effort': plan['effort']},
|
|
62
|
+
'Native assignment has no acknowledged cloud delivery reservation')
|
|
63
|
+
else:
|
|
64
|
+
require(operation['kind'] == 'archive' and
|
|
65
|
+
set(operation) == {'kind', 'plan', 'workerId', 'messageId', 'terminal'}
|
|
66
|
+
and worker['state'] == 'archiving', 'Native archival has no checkpointed worker')
|
|
67
|
+
payload = read_checkpoint(store.root, worker['checkpoint'], task['id'], worker['id'])
|
|
68
|
+
receipt = payload['nativeReceipt']
|
|
69
|
+
terminal = operation['terminal']
|
|
70
|
+
require(receipt['status'] == 'completed' and receipt['threadId'] == plan['nativeId']
|
|
71
|
+
and receipt['turnId'] == operation['messageId'] == terminal['messageId']
|
|
72
|
+
and receipt['producer']['resultId'] == terminal['resultUuid'],
|
|
73
|
+
'Native archival differs from the verified cloud checkpoint')
|
|
74
|
+
return {'authorized': True}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def main():
|
|
78
|
+
try:
|
|
79
|
+
raw = sys.stdin.read(262145)
|
|
80
|
+
require(len(raw) <= 262144 and len(sys.argv) == 2, 'Bounded native authority request required')
|
|
81
|
+
print(json.dumps(authorize(sys.argv[1], json.loads(raw))))
|
|
82
|
+
return 0
|
|
83
|
+
except Exception as exc:
|
|
84
|
+
print('Native cloud authority refused: ' + str(exc), file=sys.stderr)
|
|
85
|
+
return 2
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Session:
|
|
89
|
+
def __init__(self, cwd, worker_id, model, effort, progress=None):
|
|
90
|
+
self.cwd = pathlib.Path(cwd)
|
|
91
|
+
self.id = worker_id
|
|
92
|
+
self.model = model
|
|
93
|
+
self.effort = effort
|
|
94
|
+
self.progress = progress
|
|
95
|
+
self.plan = None
|
|
96
|
+
self.connection = None
|
|
97
|
+
self.results = {}
|
|
98
|
+
self.pending = None
|
|
99
|
+
self.assigned = False
|
|
100
|
+
self.closed = False
|
|
101
|
+
self.archived = False
|
|
102
|
+
self.allow_tools = True
|
|
103
|
+
self.directory = self.cwd / '.topolo-worker'
|
|
104
|
+
self.directory.mkdir(mode=0o700)
|
|
105
|
+
atomic_json(self.directory / 'gate.json', {'sessionId': None, 'model': model,
|
|
106
|
+
'effort': effort, 'phase': 'readiness'})
|
|
107
|
+
adapter = pathlib.Path(__file__).resolve().parents[2] / 'topolo-agent-pack'
|
|
108
|
+
command = shlex.join([sys.executable, str(adapter), 'work', 'claude-worker-hook',
|
|
109
|
+
'--harness', 'claude', '--input', str(self.directory)]) + ' || exit 2'
|
|
110
|
+
atomic_json(self.cwd / '.claude/settings.json', {'autoMemoryEnabled': False,
|
|
111
|
+
'hooks': {'PreToolUse': [{'hooks': [{'type': 'command', 'command': command, 'timeout': 15}]}]}})
|
|
112
|
+
|
|
113
|
+
async def operation(self, value):
|
|
114
|
+
require(self.connection is not None, 'The signed-in Desktop task connection is unavailable')
|
|
115
|
+
return await asyncio.to_thread(call, self.connection, value)
|
|
116
|
+
|
|
117
|
+
async def start(self):
|
|
118
|
+
require(self.plan is not None, 'Exact cloud-reserved native plan required')
|
|
119
|
+
files = {str(p): hashlib.sha256(p.read_bytes()).hexdigest()
|
|
120
|
+
for p in sorted(self.cwd.rglob('*')) if p.is_file() and not p.is_relative_to(self.directory)}
|
|
121
|
+
require(hashlib.sha256(canonical(files)).hexdigest() == self.plan['contextDigest'],
|
|
122
|
+
'Selected native context changed before creation')
|
|
123
|
+
await self.operation({'kind': 'create', 'plan': self.plan, 'files': files})
|
|
124
|
+
result = await self.wait(self.id)
|
|
125
|
+
require(result['output'].strip() == 'TOPOLO_WORKER_READY:' + self.id and result['toolItems'] == 0,
|
|
126
|
+
'Exact tool-free native readiness did not complete')
|
|
127
|
+
self.sdk_session = result['nativeSessionId']
|
|
128
|
+
return {'sessionId': self.plan['nativeId'], 'processId': self.connection['pid'],
|
|
129
|
+
'model': self.model, 'effort': self.effort, 'boundary': 'signed-in Desktop native terminal producer'}
|
|
130
|
+
|
|
131
|
+
async def assign(self, message_id, prompt):
|
|
132
|
+
require(not self.assigned and not self.closed and self.pending is None,
|
|
133
|
+
'This native worker already has an assignment; reconcile without resend')
|
|
134
|
+
self.assigned = True
|
|
135
|
+
self.pending = message_id
|
|
136
|
+
atomic_json(self.directory / 'gate.json', {'sessionId': self.sdk_session, 'model': self.model,
|
|
137
|
+
'effort': self.effort, 'phase': 'assigned', 'allowTools': self.allow_tools})
|
|
138
|
+
await self.operation({'kind': 'send', 'workerId': self.id,
|
|
139
|
+
'assignment': {'messageId': message_id, 'prompt': prompt}})
|
|
140
|
+
return await self.wait(message_id)
|
|
141
|
+
|
|
142
|
+
async def wait(self, message_id):
|
|
143
|
+
seen = set()
|
|
144
|
+
async with asyncio.timeout(1800):
|
|
145
|
+
while True:
|
|
146
|
+
result = await self.operation({'kind': 'read', 'workerId': self.id, 'messageId': message_id})
|
|
147
|
+
if self.assigned and self.progress:
|
|
148
|
+
for row in result['messages']:
|
|
149
|
+
if row['id'] not in seen and public_progress(row['text']):
|
|
150
|
+
await self.progress(row['id'], row['text'][:1600])
|
|
151
|
+
seen.add(row['id'])
|
|
152
|
+
if result['state'] == 'native-task-completed':
|
|
153
|
+
require(result['nativeId'] == self.plan['nativeId'] and result['messageId'] == message_id
|
|
154
|
+
and result['model'] == self.model and result['effort'] == self.effort,
|
|
155
|
+
'Native terminal identity or settings differ')
|
|
156
|
+
receipt = {'id': message_id, 'status': 'completed', 'output': result['result'],
|
|
157
|
+
'toolItems': result['toolItems'], 'nativeSessionId': result['cliSessionId'],
|
|
158
|
+
'nativeResultId': result['resultUuid']}
|
|
159
|
+
self.results[message_id] = receipt
|
|
160
|
+
self.pending = None
|
|
161
|
+
return receipt
|
|
162
|
+
require(result['state'] == 'native-task-pending', 'Native task state is unqualified')
|
|
163
|
+
await asyncio.sleep(1)
|
|
164
|
+
|
|
165
|
+
async def archive(self, message_id):
|
|
166
|
+
result = await self.operation({'kind': 'archive', 'workerId': self.id, 'messageId': message_id})
|
|
167
|
+
require(result['state'] == 'native-worker-archived' and result['nativeId'] == self.plan['nativeId'],
|
|
168
|
+
'Native Desktop worker archival was not acknowledged')
|
|
169
|
+
self.closed = True
|
|
170
|
+
self.archived = True
|
|
171
|
+
return {'kind': 'native-desktop-session', 'desktopChatArchived': True, **result}
|
|
172
|
+
|
|
173
|
+
async def close(self):
|
|
174
|
+
# An uncertain task stays visible and owned. Closing an adapter is never
|
|
175
|
+
# represented as archival, and never cancels or deletes native work.
|
|
176
|
+
self.closed = True
|
|
177
|
+
return {'state': 'native-session-preserved', 'desktopChatArchived': self.archived}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if __name__ == '__main__':
|
|
181
|
+
sys.exit(main())
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Claude subscription workers behind Desktop's three permanent front doors.
|
|
2
|
+
|
|
3
|
+
Uses the same cloud state, checkpoints, review phases and Activity events as the
|
|
4
|
+
Codex broker. It does not launch a competing local task ledger or import history.
|
|
5
|
+
"""
|
|
6
|
+
import asyncio
|
|
7
|
+
import hashlib
|
|
8
|
+
import copy
|
|
9
|
+
import pathlib
|
|
10
|
+
import sys
|
|
11
|
+
import types
|
|
12
|
+
import uuid
|
|
13
|
+
|
|
14
|
+
from . import task_models
|
|
15
|
+
from .claude_desktop import Session
|
|
16
|
+
from .cicd import RELEASE_RISK_TYPES
|
|
17
|
+
from .role_catalog import digest
|
|
18
|
+
from .storage import atomic_json, read_json, component_key, canonical
|
|
19
|
+
from .work import require
|
|
20
|
+
from .work_native import Broker
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ClaudeBroker(Broker):
|
|
24
|
+
def __init__(self, lib, session_factory=Session):
|
|
25
|
+
owner = types.SimpleNamespace(lib=lib, runtime=str(lib.root / 'claude/task-runtime'),
|
|
26
|
+
execution=types.SimpleNamespace(roles={}, verified=set()))
|
|
27
|
+
super().__init__(owner)
|
|
28
|
+
require(getattr(self.store, 'cloud', False), 'Claude dispatch requires cloud authority; no local fallback')
|
|
29
|
+
self.factory = session_factory
|
|
30
|
+
self.sessions = {}
|
|
31
|
+
|
|
32
|
+
async def initialize(self):
|
|
33
|
+
await self.state_call('snapshot')
|
|
34
|
+
self.config()
|
|
35
|
+
for channel in ['engineering', 'operations', 'cicd']:
|
|
36
|
+
config, _, _ = self.config(channel)
|
|
37
|
+
await asyncio.to_thread(self.qualify, config)
|
|
38
|
+
workers = await self.state_call('workers')
|
|
39
|
+
for task in await self.state_call('list'):
|
|
40
|
+
if task.get('ownerInstallationId') == self.store.binding['installationId'] or task.get('resumeAfterCheckpoint') is not True:
|
|
41
|
+
continue
|
|
42
|
+
if task['state'] not in {'queued', 'executing', 'reviewing'}:
|
|
43
|
+
continue
|
|
44
|
+
owned = [w for w in workers if w['taskId'] == task['id']]
|
|
45
|
+
phase = {'queued': 'route', 'executing': 'execute', 'reviewing': 'review'}[task['state']]
|
|
46
|
+
if any(w['phase'] == phase and w.get('attempt', 1) == task.get('attempt', 1)
|
|
47
|
+
and (phase != 'review' or w.get('reviewGeneration', 0) == task.get('reviewGeneration', 0)) for w in owned):
|
|
48
|
+
continue
|
|
49
|
+
if not all(w['state'] == 'archived' and w.get('nativeArchived') is True or
|
|
50
|
+
w['state'] == 'failed' and not w.get('turnId') and
|
|
51
|
+
(not w.get('nativeId') or w.get('nativeArchived') is True) for w in owned):
|
|
52
|
+
continue
|
|
53
|
+
await self.state_call('adopt_ready', task['id'], task['revision'])
|
|
54
|
+
|
|
55
|
+
def config(self, channel='engineering'):
|
|
56
|
+
root = self.store.root
|
|
57
|
+
saved = read_json(root / 'config.json')
|
|
58
|
+
catalog = read_json(root / 'role-catalog.json')
|
|
59
|
+
require(saved.get('format') == 'topolo-task-dispatch-v2'
|
|
60
|
+
and saved.get('requireCloudAuthority') is True and saved.get('frontdoorDispatch') is True,
|
|
61
|
+
'Claude requires the source-independent cloud dispatcher')
|
|
62
|
+
require(catalog.get('format') == 'topolo-task-role-catalog-v1'
|
|
63
|
+
and catalog.get('pack') == saved['pack'] and digest(catalog) == saved['catalogDigest'],
|
|
64
|
+
'Reviewed specialization catalog changed')
|
|
65
|
+
native, revision = self.owner.lib.load(saved['pack'])
|
|
66
|
+
require(revision == catalog['nativePackRevision'] and len(native['agents']) == 3,
|
|
67
|
+
'The three-coordinator installation changed')
|
|
68
|
+
require(channel in catalog['channels'] and type(saved['maxWorkers']) is int
|
|
69
|
+
and 1 <= saved['maxWorkers'] <= 8, 'Unknown channel or invalid worker capacity')
|
|
70
|
+
registry = read_json(root / 'role-bindings.json')
|
|
71
|
+
require(registry.get('organizationId') == 'org_topolo_platform'
|
|
72
|
+
and registry.get('roles') == catalog['bindings'], 'Agent definition references differ')
|
|
73
|
+
selected = catalog['channels'][channel]
|
|
74
|
+
manifest = {'packId': saved['pack'], 'sourceIndependent': True,
|
|
75
|
+
'agents': [r for r in catalog['roles'] if component_key(r) in selected['roleKeys']]}
|
|
76
|
+
return {**selected, 'pack': saved['pack'], 'catalogOnly': True,
|
|
77
|
+
'maxWorkers': saved['maxWorkers']}, manifest, {
|
|
78
|
+
'configuration': catalog['configuration'], 'constraints': catalog['constraints']}
|
|
79
|
+
|
|
80
|
+
def qualify(self, config):
|
|
81
|
+
from .permissions import load
|
|
82
|
+
from .topolo import Client, organization_execution_policy
|
|
83
|
+
native, _ = self.owner.lib.load(config['pack'])
|
|
84
|
+
consent = load(self.owner.lib, config['pack'], native['agents'])
|
|
85
|
+
require(consent is not None and consent['profile'] == 'full-access',
|
|
86
|
+
'Explicit organization-bound full-access installation consent is required')
|
|
87
|
+
key = config['pack'] + '/intakes/' + config['intakeRole']
|
|
88
|
+
require(consent.get('overrides', {}).get(key, consent['profile']) == 'full-access',
|
|
89
|
+
'Selected channel does not have full-access installation consent')
|
|
90
|
+
client = Client('production', 'org:org_topolo_platform')
|
|
91
|
+
policy, proof = organization_execution_policy(client, consent, native['agents'],
|
|
92
|
+
consent.get('agentWorkspaceId'))
|
|
93
|
+
require(policy.get('scopePolicyMode') == 'unrestricted' and policy.get('fullAccessEnabled') is True
|
|
94
|
+
and proof['profileCeilingVerified'],
|
|
95
|
+
'This worker cannot enforce an organization tool/action allowlist; execution remains blocked')
|
|
96
|
+
self.tool_path = client.env['PATH']
|
|
97
|
+
|
|
98
|
+
async def start(self, task, phase, config, manifest, models):
|
|
99
|
+
await asyncio.to_thread(self.qualify, config)
|
|
100
|
+
if phase == 'route':
|
|
101
|
+
role_id = config['intakeRole']
|
|
102
|
+
model_task = {'type': 'route-request', 'complexity': 'routine', 'risk': 'normal'}
|
|
103
|
+
elif phase == 'execute':
|
|
104
|
+
role_id, model_task = task['route']['role'], task['route']['task']
|
|
105
|
+
else:
|
|
106
|
+
role_id = config['reviewRole']
|
|
107
|
+
model_task = {**task['route']['task'], 'type': 'verify-task',
|
|
108
|
+
'complexity': 'complex' if task['route']['task']['risk'] == 'security-critical'
|
|
109
|
+
or task['channel'] == 'cicd' and task['route']['task']['type'] in RELEASE_RISK_TYPES else 'standard'}
|
|
110
|
+
roles = [r for r in manifest['agents'] if r['id'] == role_id]
|
|
111
|
+
require(len(roles) == 1, 'Selected specialization is ambiguous')
|
|
112
|
+
key = component_key(roles[0])
|
|
113
|
+
constraints = task_models.role_constraints(models['constraints'], key)
|
|
114
|
+
selected = task_models._selection(models['configuration'], model_task, 'claude', constraints)
|
|
115
|
+
require(task['channel'] != 'cicd' or model_task['type'] not in RELEASE_RISK_TYPES
|
|
116
|
+
or selected['rule']['profile'] == 'deep', 'Release-risk tasks require a deep profile')
|
|
117
|
+
effort = next(e for e in selected['preferences'] if e in selected['allowedEfforts'])
|
|
118
|
+
pending = {'harness': 'claude', 'resolved': {'model': selected['model'], 'effort': effort},
|
|
119
|
+
'nativeACK': None, 'state': 'intent-only-awaiting-native-readiness'}
|
|
120
|
+
worker = await self.state_call('reserve', task['id'], phase, key, pending, config['maxWorkers'])
|
|
121
|
+
wid = worker['id']
|
|
122
|
+
cwd = pathlib.Path(self.owner.runtime) / 'task-workspaces' / wid
|
|
123
|
+
cwd.mkdir(parents=True, mode=0o700)
|
|
124
|
+
from .work_context import ensure, materialize
|
|
125
|
+
context = await asyncio.to_thread(ensure, self.store.root, task)
|
|
126
|
+
if task.get('contextRef') != context:
|
|
127
|
+
current = await self.state_call('get', task['id'])
|
|
128
|
+
task = await self.state_call('update', task['id'], current['revision'],
|
|
129
|
+
{'contextRef': context, 'summary': 'Selected cloud task context is verified.'}, 'task-context-bound')
|
|
130
|
+
from .role_context import materialize as materialize_role
|
|
131
|
+
role = await asyncio.to_thread(materialize_role, self.store.root, key, cwd, harness='claude')
|
|
132
|
+
atomic_json(cwd / 'TASK.json', task)
|
|
133
|
+
await asyncio.to_thread(materialize, self.store.root, task, cwd)
|
|
134
|
+
if phase in {'execute', 'review'}:
|
|
135
|
+
from .linked_task_context import materialize as materialize_linked
|
|
136
|
+
await materialize_linked(self.store, task, cwd, lambda tid: self.state_call('get', tid))
|
|
137
|
+
atomic_json(cwd / 'context/retained-read-input.json', {'cursor': 0, 'limit': 50})
|
|
138
|
+
atomic_json(cwd / 'context/dispatch-connection.json', {
|
|
139
|
+
'format': 'topolo-dispatch-transport-v1',
|
|
140
|
+
'cloudReference': {k: self.store.binding[k] for k in ['organizationId', 'workspaceId', 'documentId', 'datasetId']},
|
|
141
|
+
'retainedReadCommand': [sys.executable, str(pathlib.Path(__file__).resolve().parents[2] / 'topolo-agent-pack'), '--store', str(self.owner.lib.root),
|
|
142
|
+
'work', 'retained', '--harness', 'claude', '--input', str(cwd / 'context/retained-read-input.json')],
|
|
143
|
+
'pageInput': {'cursor': 0, 'limit': 50},
|
|
144
|
+
'boundary': 'Read relevant retained context in pages; preserve original owners. Do not load the whole state document, portfolio export or old chats.'})
|
|
145
|
+
if phase == 'review':
|
|
146
|
+
from .work_artifacts import materialize_result
|
|
147
|
+
await asyncio.to_thread(materialize_result, self.store.root, task, cwd)
|
|
148
|
+
from .review_context import materialize as materialize_review
|
|
149
|
+
await asyncio.to_thread(materialize_review, self.store, task, cwd)
|
|
150
|
+
async def progress(identity, text):
|
|
151
|
+
await self.state_call('progress', wid, identity, text)
|
|
152
|
+
await self.notify()
|
|
153
|
+
session = self.factory(cwd, wid, selected['model'], effort, progress if phase == 'execute' else None)
|
|
154
|
+
session.allow_tools = phase != 'route'
|
|
155
|
+
settings_path = cwd / '.claude/settings.json'
|
|
156
|
+
settings = read_json(settings_path, {})
|
|
157
|
+
settings['env'] = {'PATH': self.tool_path}
|
|
158
|
+
atomic_json(settings_path, settings)
|
|
159
|
+
files = {str(p): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(cwd.rglob('*'))
|
|
160
|
+
if p.is_file() and not p.is_relative_to(cwd / '.topolo-worker')}
|
|
161
|
+
native_plan = {'taskId': task['id'], 'workerId': wid, 'nativeId': 'local_' + wid, 'cwd': str(cwd),
|
|
162
|
+
'title': (phase.title() + ' · ' + task['objective'])[:256],
|
|
163
|
+
'model': selected['model'], 'effort': effort, 'permissionMode': 'bypassPermissions',
|
|
164
|
+
'contextDigest': hashlib.sha256(canonical(files)).hexdigest()}
|
|
165
|
+
await self.state_call('worker_update', wid, {'state': 'creating', 'cwd': str(cwd),
|
|
166
|
+
'nativeId': native_plan['nativeId'], 'roleSource': role, 'nativePlan': native_plan})
|
|
167
|
+
session.plan = native_plan
|
|
168
|
+
session.connection = read_json(self.store.root / 'claude-frontdoors.json')['nativeConnection']
|
|
169
|
+
self.sessions[wid] = session
|
|
170
|
+
try:
|
|
171
|
+
ack = await session.start()
|
|
172
|
+
catalog = {'harness': 'claude', 'source': 'native-account-model-ids',
|
|
173
|
+
'connectionGeneration': str(ack['processId']) + ':' + ack['sessionId'],
|
|
174
|
+
'models': [ack['model']]}
|
|
175
|
+
resolution = task_models.resolve_intent(models['configuration'], model_task, 'claude', catalog, constraints)
|
|
176
|
+
resolution = task_models.acknowledge(resolution, {'model': ack['model'], 'effort': ack['effort']},
|
|
177
|
+
configuration=models['configuration'], catalog=catalog, constraints=constraints)
|
|
178
|
+
message_id = str(uuid.uuid4())
|
|
179
|
+
# Cloud commit and readback precede stdin. An uncertain commit or
|
|
180
|
+
# delivery retains this exact worker/input; no replacement is sent.
|
|
181
|
+
await self.state_call('worker_update', wid, {'state': 'running', 'resolution': resolution, 'turnId': message_id})
|
|
182
|
+
current = await self.state_call('get', task['id'])
|
|
183
|
+
await self.state_call('update', task['id'], current['revision'],
|
|
184
|
+
{'state': {'route': 'routing', 'execute': 'executing', 'review': 'reviewing'}[phase],
|
|
185
|
+
'phase': phase, 'workerIds': current['workerIds'] + [wid],
|
|
186
|
+
'summary': 'A fresh Claude ' + phase + ' specialist is running.'}, 'native-resume-verified')
|
|
187
|
+
check = await self.state_call('worker', wid)
|
|
188
|
+
require(check['turnId'] == message_id and check['resolution'] == resolution,
|
|
189
|
+
'Canonical delivery reservation was not acknowledged')
|
|
190
|
+
await self.notify()
|
|
191
|
+
self.launch(self.execute(worker, session, message_id, self.prompt(task, phase, manifest, cwd)))
|
|
192
|
+
except Exception:
|
|
193
|
+
# Task input is sent only by execute(), after this startup block.
|
|
194
|
+
await session.close()
|
|
195
|
+
await self.state_call('worker_update', wid, {'state': 'failed', 'nativeArchived': False,
|
|
196
|
+
'nativeRetirement': {'kind': 'native-desktop-session', 'desktopChatArchived': False}})
|
|
197
|
+
raise
|
|
198
|
+
|
|
199
|
+
def prompt(self, task, phase, manifest, cwd):
|
|
200
|
+
prompt = super().prompt(task, phase, manifest, cwd).replace('Read AGENTS.md', 'Read CLAUDE.md')
|
|
201
|
+
if phase in {'execute', 'review'}:
|
|
202
|
+
prompt += '\nFor relevant existing-work inventory only, context/dispatch-connection.json supplies the current installation transport and paged read command. It grants no takeover or replay authority.'
|
|
203
|
+
if phase == 'review':
|
|
204
|
+
prompt += '\nRead context/dispatch-connection.json and context/workflow-evidence.json, including its evidenceContract, before declaring authority or lifecycle evidence missing. Save any read-only command output you cite in this reviewer workspace; never cite /tmp, /private/tmp or another worker directory. The transport supplies the pinned adapter command and bounded pageInput; run that read-only command to independently verify relevant retained evidence. A role such as queue-recovery is the selected specialization; status-read is its classified task type, not a separate specialist name. Workflow evidence records this attempt only. Inspect prior worker archival and native model acknowledgements there. The coordinator separately verifies this reviewer archives after its checkpoint and that Activity reaches the live GUI; never fabricate those post-review observations or perform work again.'
|
|
205
|
+
return prompt
|
|
206
|
+
|
|
207
|
+
async def execute(self, worker, session, message_id, prompt):
|
|
208
|
+
try:
|
|
209
|
+
result = await session.assign(message_id, prompt)
|
|
210
|
+
await self.finish(worker['id'], result)
|
|
211
|
+
saved = await self.state_call('worker', worker['id'])
|
|
212
|
+
if saved['state'] != 'archived':
|
|
213
|
+
await session.close()
|
|
214
|
+
except Exception as exc:
|
|
215
|
+
await self.state_call('worker_update', worker['id'], {'state': 'unknown', 'error': str(exc)})
|
|
216
|
+
task = await self.state_call('get', worker['taskId'])
|
|
217
|
+
await self.state_call('update', task['id'], task['revision'],
|
|
218
|
+
{'state': 'unknown', 'summary': 'Claude task outcome is unresolved; no retry was sent.'}, 'delivery-unknown')
|
|
219
|
+
await session.close()
|
|
220
|
+
await self.notify()
|
|
221
|
+
|
|
222
|
+
async def read_turn(self, worker, turn):
|
|
223
|
+
receipt = self.sessions[worker['id']].results.get(turn['id'])
|
|
224
|
+
require(receipt is not None and receipt == turn, 'Exact native Claude completion is unavailable')
|
|
225
|
+
return {'id': turn['id'], 'status': 'completed',
|
|
226
|
+
'items': [{'type': 'agentMessage', 'text': turn['output']}] +
|
|
227
|
+
[{'type': 'toolUse'} for _ in range(turn['toolItems'])],
|
|
228
|
+
'nativeProof': {'harness': 'claude-desktop', 'sessionId': receipt['nativeSessionId'],
|
|
229
|
+
'resultId': receipt['nativeResultId']}}
|
|
230
|
+
|
|
231
|
+
async def archive_worker(self, worker):
|
|
232
|
+
session = self.sessions[worker['id']]
|
|
233
|
+
result = await session.archive(worker['turnId'])
|
|
234
|
+
require(session.closed and session.archived, 'Native Desktop worker remains unarchived')
|
|
235
|
+
return {'nativeRetirement': result}
|
|
236
|
+
|
|
237
|
+
def persist(self):
|
|
238
|
+
# The cloud store owns task/worker state. Never write a Codex execution
|
|
239
|
+
# policy for Claude or use a second local ledger as an execution fallback.
|
|
240
|
+
pass
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Supported native coordinator conversation and typed broker-tool boundary.
|
|
2
|
+
|
|
3
|
+
The native prompt_id is the deduplication identity. No transcript, chat export,
|
|
4
|
+
or locally generated substitute is consulted when it is missing.
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
import pathlib
|
|
8
|
+
import sys
|
|
9
|
+
import uuid
|
|
10
|
+
|
|
11
|
+
from .storage import Library, PackError, read_json
|
|
12
|
+
from .work import require
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def submit(root, event, send=None):
|
|
16
|
+
require(isinstance(event, dict), 'Native Claude hook input must be an object')
|
|
17
|
+
lib = Library(root)
|
|
18
|
+
binding = read_json(lib.root / 'work/claude-frontdoors.json')
|
|
19
|
+
require(binding.get('format') == 'topolo-claude-frontdoors-v1',
|
|
20
|
+
'Claude front-door binding is not installed')
|
|
21
|
+
require(event.get('hook_event_name') in {'UserPromptSubmit', 'PreToolUse'},
|
|
22
|
+
'Unsupported Claude front-door hook')
|
|
23
|
+
rows = [r for r in binding['roles'] if r['sessionId'] == event.get('session_id')
|
|
24
|
+
and r['cwd'] == event.get('cwd')]
|
|
25
|
+
require(len(rows) == 1, 'Native Claude session is not the bound front door')
|
|
26
|
+
role = rows[0]
|
|
27
|
+
if event['hook_event_name'] == 'PreToolUse':
|
|
28
|
+
from .coordinator_context import tool_allowed
|
|
29
|
+
try:allowed=tool_allowed(lib.root,role,event)
|
|
30
|
+
except (PackError,ValueError,TypeError,OSError):allowed=False
|
|
31
|
+
return {'hookSpecificOutput': {'hookEventName': 'PreToolUse',
|
|
32
|
+
'permissionDecision': 'allow' if allowed else 'deny', 'permissionDecisionReason':
|
|
33
|
+
'Use only the exact broker schemas in the current Topolo coordinator context. For continuation: read the existing task, then Write {requestId: dispatchRequestId, taskId, revision: latest task.revision, intent: continue/request_changes/hold, note} to context/coordinator-request.json and run the installed respond --input command. Do not use {taskId,message}, create a replacement task, or perform engineering effects.'}}
|
|
34
|
+
identity = event.get('prompt_id')
|
|
35
|
+
try:
|
|
36
|
+
valid = isinstance(identity, str) and str(uuid.UUID(identity)) == identity
|
|
37
|
+
except ValueError:
|
|
38
|
+
valid = False
|
|
39
|
+
require(valid, 'Native Claude prompt identity is unavailable; no task was submitted')
|
|
40
|
+
require(event.get('permission_mode') == 'bypassPermissions',
|
|
41
|
+
'Claude front-door startup permissions differ from the installation')
|
|
42
|
+
require(isinstance(event.get('prompt'),str) and 1<=len(event['prompt'])<=16000,'Bounded native human input required')
|
|
43
|
+
if send is None:
|
|
44
|
+
from .live import request as live_request
|
|
45
|
+
send = lambda operation, params: live_request(lib, operation, params)
|
|
46
|
+
status = send('status', {})
|
|
47
|
+
require(status.get('harness') == 'claude' and status.get('cloudDispatchReady') is True
|
|
48
|
+
and status.get('desktopPid') == binding.get('desktopPid'),
|
|
49
|
+
'Claude cloud dispatch is not active; the coordinator will not execute the request itself')
|
|
50
|
+
from .coordinator_context import prepare
|
|
51
|
+
selected=prepare(lib.root,role,event,send('work-list',{}))
|
|
52
|
+
return {'hookSpecificOutput': {'hookEventName': 'UserPromptSubmit',
|
|
53
|
+
'additionalContext':'Topolo coordinator context (the original human prompt is unchanged; no task was submitted):\n'+json.dumps(selected,sort_keys=True,separators=(',',':'))}}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main(root, stream=None):
|
|
58
|
+
try:
|
|
59
|
+
source = (stream or sys.stdin).read(100001)
|
|
60
|
+
require(len(source) <= 100000, 'Claude hook input exceeds its boundary')
|
|
61
|
+
event = json.loads(source)
|
|
62
|
+
if isinstance(event, dict) and event.get('hook_event_name') == 'UserPromptSubmit':
|
|
63
|
+
from .claude_service import ensure_running, refresh_desktop_owner
|
|
64
|
+
lib = Library(root)
|
|
65
|
+
refresh_desktop_owner(lib)
|
|
66
|
+
ensure_running(lib)
|
|
67
|
+
result = submit(pathlib.Path(root), event)
|
|
68
|
+
print(json.dumps(result))
|
|
69
|
+
return 0
|
|
70
|
+
except Exception as exc:
|
|
71
|
+
# Nonzero hook exit blocks direct execution on a failed handoff. Never
|
|
72
|
+
# print the incoming human prompt or a transcript in a diagnostic.
|
|
73
|
+
print('Topolo handoff unavailable: ' + str(exc), file=sys.stderr)
|
|
74
|
+
return 2
|