@topolo/cli 0.13.19 → 0.13.22
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 +421 -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 +181 -16
- 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 +103 -6
- 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/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/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 +152 -73
- package/package.json +5 -5
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Recover one already delivered Desktop completion, without resending any input.
|
|
2
|
+
|
|
3
|
+
The legacy native guard requires the original inventory during read/archive.
|
|
4
|
+
Only explicitly reviewed added outputs may be moved, reversibly, beneath its
|
|
5
|
+
excluded recovery directory. Original context is always checked by its digest.
|
|
6
|
+
"""
|
|
7
|
+
import hashlib
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import stat
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
from .storage import atomic_json, canonical, read_json
|
|
14
|
+
from .work import require
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _file(path):
|
|
18
|
+
meta = path.lstat()
|
|
19
|
+
require(stat.S_ISREG(meta.st_mode) and meta.st_uid == os.getuid()
|
|
20
|
+
and not meta.st_mode & 0o022 and path.resolve() == path,
|
|
21
|
+
'Recovery requires an owned regular non-writable-by-others file')
|
|
22
|
+
require(0 < meta.st_size <= 100 * 1024 * 1024, 'Recovery file exceeds boundary')
|
|
23
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OutputCustody:
|
|
27
|
+
def __init__(self, worker, outputs):
|
|
28
|
+
self.cwd = Path(worker['cwd'])
|
|
29
|
+
require(self.cwd.is_absolute() and self.cwd.resolve() == self.cwd,
|
|
30
|
+
'Recovery workspace identity differs')
|
|
31
|
+
require(isinstance(outputs, list) and 0 < len(outputs) <= 16,
|
|
32
|
+
'Explicit selected output manifest required')
|
|
33
|
+
self.outputs = []
|
|
34
|
+
for row in outputs:
|
|
35
|
+
require(isinstance(row, dict) and set(row) == {'path', 'sha256'},
|
|
36
|
+
'Closed recovery output manifest required')
|
|
37
|
+
path = Path(row['path'])
|
|
38
|
+
require(path.is_relative_to(self.cwd) and path != self.cwd
|
|
39
|
+
and not path.is_relative_to(self.cwd / '.topolo-worker')
|
|
40
|
+
and path not in [p for p, _ in self.outputs], 'Recovery output path differs')
|
|
41
|
+
require(_file(path) == row['sha256'], 'Selected output digest differs')
|
|
42
|
+
self.outputs.append((path, row['sha256']))
|
|
43
|
+
self.initial = {}
|
|
44
|
+
for path in sorted(self.cwd.rglob('*')):
|
|
45
|
+
if path.is_relative_to(self.cwd / '.topolo-worker'):
|
|
46
|
+
continue
|
|
47
|
+
require(not path.is_symlink(), 'Recovery workspace contains a link')
|
|
48
|
+
if path.is_file() and path not in [p for p, _ in self.outputs]:
|
|
49
|
+
self.initial[str(path)] = _file(path)
|
|
50
|
+
require(hashlib.sha256(canonical(self.initial)).hexdigest()
|
|
51
|
+
== worker['nativePlan']['contextDigest'], 'Original selected context differs')
|
|
52
|
+
for directory in [self.cwd, self.cwd / '.topolo-worker', self.cwd / '.topolo-worker' / 'recovery']:
|
|
53
|
+
if directory.exists():
|
|
54
|
+
meta = directory.lstat()
|
|
55
|
+
require(stat.S_ISDIR(meta.st_mode) and meta.st_uid == os.getuid()
|
|
56
|
+
and not meta.st_mode & 0o022 and directory.resolve() == directory,
|
|
57
|
+
'Recovery directory ownership differs')
|
|
58
|
+
self.directory = self.cwd / '.topolo-worker' / 'recovery' / worker['id']
|
|
59
|
+
self.journal = self.directory / 'journal.json'
|
|
60
|
+
require(not self.directory.exists(), 'Recovery already exists; reconcile its exact journal')
|
|
61
|
+
self.moved = []
|
|
62
|
+
|
|
63
|
+
def begin(self):
|
|
64
|
+
self.directory.mkdir(parents=True, mode=0o700)
|
|
65
|
+
self.record('prepared')
|
|
66
|
+
|
|
67
|
+
def record(self, state):
|
|
68
|
+
atomic_json(self.journal, {'format': 'topolo-native-completion-recovery-v1',
|
|
69
|
+
'state': state, 'inputs': self.initial,
|
|
70
|
+
'outputs': [{'path': str(p), 'sha256': h} for p, h in self.outputs]})
|
|
71
|
+
|
|
72
|
+
def quarantine(self):
|
|
73
|
+
require(not self.moved, 'Output quarantine already active')
|
|
74
|
+
for path, digest in self.outputs:
|
|
75
|
+
require(_file(path) == digest, 'Output changed before quarantine')
|
|
76
|
+
target = self.directory / ('output-' + str(len(self.moved)))
|
|
77
|
+
require(not target.exists(), 'Quarantine target exists')
|
|
78
|
+
os.rename(path, target)
|
|
79
|
+
self.moved.append((path, target, digest))
|
|
80
|
+
actual = {str(p): _file(p) for p in sorted(self.cwd.rglob('*'))
|
|
81
|
+
if p.is_file() and not p.is_relative_to(self.cwd / '.topolo-worker')}
|
|
82
|
+
require(actual == self.initial, 'Original context changed during quarantine')
|
|
83
|
+
self.record('quarantined')
|
|
84
|
+
|
|
85
|
+
def restore(self):
|
|
86
|
+
for path, target, digest in reversed(self.moved):
|
|
87
|
+
require(not path.exists() and _file(target) == digest,
|
|
88
|
+
'Output restoration collision or digest differs; custody retained')
|
|
89
|
+
os.rename(target, path)
|
|
90
|
+
self.moved = []
|
|
91
|
+
self.record('outputs-restored')
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _terminal(result, worker):
|
|
95
|
+
plan = worker['nativePlan']
|
|
96
|
+
require(result.get('state') == 'native-task-completed'
|
|
97
|
+
and result.get('nativeId') == worker['nativeId']
|
|
98
|
+
and result.get('messageId') == worker['turnId']
|
|
99
|
+
and result.get('model') == plan['model'] and result.get('effort') == plan['effort']
|
|
100
|
+
and isinstance(result.get('result'), str)
|
|
101
|
+
and type(result.get('toolItems')) is int and result['toolItems'] >= 0,
|
|
102
|
+
'Exact existing native terminal result required')
|
|
103
|
+
for key in ['resultUuid', 'cliSessionId']:
|
|
104
|
+
require(str(uuid.UUID(result[key])) == result[key], 'Native producer identity is unqualified')
|
|
105
|
+
return {'id': worker['turnId'], 'status': 'completed', 'output': result['result'],
|
|
106
|
+
'toolItems': result['toolItems'], 'nativeSessionId': result['cliSessionId'],
|
|
107
|
+
'nativeResultId': result['resultUuid']}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _alive(pid):
|
|
111
|
+
try:
|
|
112
|
+
os.kill(pid, 0)
|
|
113
|
+
except ProcessLookupError:
|
|
114
|
+
return False
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def recover_completed(lib, request, store=None, native_call=None, alive=None, checkpoint=None):
|
|
119
|
+
"""Recover after the exact old Python service stops; Desktop remains alive.
|
|
120
|
+
|
|
121
|
+
No dispatcher is started or pumped. The caller must separately qualify the
|
|
122
|
+
corrected native guard before starting the independent review continuation.
|
|
123
|
+
"""
|
|
124
|
+
import fcntl
|
|
125
|
+
from .cloud_work import CloudStore
|
|
126
|
+
from .claude_desktop import call
|
|
127
|
+
from .work_artifacts import checkpoint as publish_checkpoint
|
|
128
|
+
require(set(request) == {'taskId', 'workerId', 'revision', 'attempt', 'turnId',
|
|
129
|
+
'outputs', 'expectedServicePid', 'expectedNative'},
|
|
130
|
+
'Closed native completion recovery request required')
|
|
131
|
+
checkpoint = checkpoint or publish_checkpoint
|
|
132
|
+
native_call = native_call or call
|
|
133
|
+
alive = alive or _alive
|
|
134
|
+
with lib.lock(), (lib.root / 'work/claude-service-start.lock').open('a') as start_lock:
|
|
135
|
+
try:
|
|
136
|
+
fcntl.flock(start_lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
137
|
+
except BlockingIOError:
|
|
138
|
+
require(False, 'Another Claude startup owns this installation')
|
|
139
|
+
previous = read_json(lib.root / 'live-bridge.json')
|
|
140
|
+
require(type(request['expectedServicePid']) is int and request['expectedServicePid'] > 1
|
|
141
|
+
and previous.get('harness') == 'claude'
|
|
142
|
+
and previous.get('pid') == request['expectedServicePid']
|
|
143
|
+
and not alive(request['expectedServicePid']),
|
|
144
|
+
'Exact previous Python owner must be stopped before recovery')
|
|
145
|
+
connection = read_json(lib.root / 'work/claude-frontdoors.json')['nativeConnection']
|
|
146
|
+
require(alive(connection['pid']) and connection['pid'] == request['expectedNative']['pid']
|
|
147
|
+
and connection['identity'] == request['expectedNative']['identity'],
|
|
148
|
+
'Original Desktop native owner differs')
|
|
149
|
+
require(native_call(connection, {'kind': 'status'}) == request['expectedNative']
|
|
150
|
+
and request['expectedNative'].get('qualified') is True
|
|
151
|
+
and request['expectedNative'].get('workers') == 1,
|
|
152
|
+
'Exact sole native worker connection required')
|
|
153
|
+
store = store or CloudStore(lib.root)
|
|
154
|
+
task = store.get(request['taskId'])
|
|
155
|
+
worker = store.worker(request['workerId'])
|
|
156
|
+
require(task['state'] == 'unknown' and task['revision'] == request['revision']
|
|
157
|
+
and task['attempt'] == request['attempt'] and worker['state'] == 'unknown'
|
|
158
|
+
and worker['taskId'] == task['id'] and worker['phase'] == 'execute'
|
|
159
|
+
and worker['attempt'] == task['attempt'] and worker['turnId'] == request['turnId']
|
|
160
|
+
and worker['id'] in task['workerIds'] and not worker.get('checkpoint')
|
|
161
|
+
and task.get('ownerInstallationId') == store.binding['installationId'],
|
|
162
|
+
'Exact owned UNKNOWN execution checkpoint required')
|
|
163
|
+
plan = worker['nativePlan']
|
|
164
|
+
require(all(plan.get(key) == worker.get(key) for key in ['taskId', 'nativeId', 'cwd'])
|
|
165
|
+
and plan.get('workerId') == worker['id'], 'Cloud native plan identity differs')
|
|
166
|
+
for other in store.workers():
|
|
167
|
+
require(other['id'] == worker['id'] or
|
|
168
|
+
other['state'] == 'archived' and other.get('nativeArchived') is True or
|
|
169
|
+
other['state'] == 'failed' and not other.get('turnId') and
|
|
170
|
+
(not other.get('nativeId') or other.get('nativeArchived') is True),
|
|
171
|
+
'Another unresolved cloud worker prevents standalone recovery')
|
|
172
|
+
custody = OutputCustody(worker, request['outputs'])
|
|
173
|
+
custody.begin()
|
|
174
|
+
try:
|
|
175
|
+
custody.quarantine()
|
|
176
|
+
native = native_call(connection, {'kind': 'read', 'workerId': worker['id'],
|
|
177
|
+
'messageId': worker['turnId']})
|
|
178
|
+
receipt = _terminal(native, worker)
|
|
179
|
+
finally:
|
|
180
|
+
custody.restore()
|
|
181
|
+
actual = {'id': receipt['id'], 'status': receipt['status'],
|
|
182
|
+
'items': [{'type': 'agentMessage', 'text': receipt['output']}] +
|
|
183
|
+
[{'type': 'toolUse'} for _ in range(receipt['toolItems'])],
|
|
184
|
+
'nativeProof': {'harness': 'claude-desktop', 'sessionId': receipt['nativeSessionId'],
|
|
185
|
+
'resultId': receipt['nativeResultId']}}
|
|
186
|
+
proof, payload, normalized = checkpoint(store.root, task, worker, actual)
|
|
187
|
+
require(payload.get('outputError') is None and normalized is not None,
|
|
188
|
+
'Native output contract is not checkpoint-ready')
|
|
189
|
+
require(store.get(task['id']) == task and store.worker(worker['id']) == worker,
|
|
190
|
+
'Task or worker changed before recovered checkpoint publication')
|
|
191
|
+
store.worker_update(worker['id'], {'state': 'archiving', 'checkpoint': proof})
|
|
192
|
+
task = store.update(task['id'], task['revision'],
|
|
193
|
+
{'checkpoints': task['checkpoints'] + [proof]}, 'checkpoint-saved')
|
|
194
|
+
custody.record('checkpoint-saved')
|
|
195
|
+
try:
|
|
196
|
+
custody.quarantine()
|
|
197
|
+
retired = native_call(connection, {'kind': 'archive', 'workerId': worker['id'],
|
|
198
|
+
'messageId': worker['turnId']})
|
|
199
|
+
require(retired.get('state') == 'native-worker-archived'
|
|
200
|
+
and retired.get('nativeId') == worker['nativeId'],
|
|
201
|
+
'Existing native worker archival was not acknowledged')
|
|
202
|
+
finally:
|
|
203
|
+
custody.restore()
|
|
204
|
+
require(native_call(connection, {'kind': 'status'}) == {**request['expectedNative'], 'workers': 0},
|
|
205
|
+
'Native archival readback differs')
|
|
206
|
+
store.worker_update(worker['id'], {'state': 'archived', 'nativeArchived': True,
|
|
207
|
+
'nativeRetirement': {'kind': 'native-desktop-session',
|
|
208
|
+
'desktopChatArchived': True, **retired}})
|
|
209
|
+
task = store.update(task['id'], task['revision'],
|
|
210
|
+
{'state': 'reviewing', 'phase': 'review',
|
|
211
|
+
'result': {**normalized, 'checkpoint': proof, 'workerId': worker['id']},
|
|
212
|
+
'summary': 'Existing native completion recovered without resend; independent review pending.'},
|
|
213
|
+
'worker-released')
|
|
214
|
+
custody.record('archived')
|
|
215
|
+
return {'taskId': task['id'], 'workerId': worker['id'], 'attempt': task['attempt'],
|
|
216
|
+
'state': task['state'], 'nativeId': worker['nativeId'], 'turnId': worker['turnId']}
|
|
@@ -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
|