@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.
Files changed (142) hide show
  1. package/agent-packs/README.md +421 -1
  2. package/agent-packs/docs/selected-artifacts.md +47 -0
  3. package/agent-packs/documentation/cloud-task-state.md +56 -0
  4. package/agent-packs/domain-contexts/README.md +25 -0
  5. package/agent-packs/domain-contexts/cicd.json +8 -0
  6. package/agent-packs/gui/feed-adapter.js +4 -0
  7. package/agent-packs/gui/index.html +9 -0
  8. package/agent-packs/gui/studio.css +14 -0
  9. package/agent-packs/gui/studio.js +121 -0
  10. package/agent-packs/gui/studio_server.py +355 -0
  11. package/agent-packs/roles/cicd/CLAUDE.md +24 -0
  12. package/agent-packs/specializations/cicd--cicd.json +5 -0
  13. package/agent-packs/specializations/cicd--pipeline-engineering.json +5 -0
  14. package/agent-packs/specializations/cicd--promotion-deployment.json +5 -0
  15. package/agent-packs/specializations/cicd--queue-recovery.json +5 -0
  16. package/agent-packs/specializations/cicd--release-verification.json +5 -0
  17. package/agent-packs/specializations/cicd--runner-capacity.json +5 -0
  18. package/agent-packs/specializations/intakes--engineering-02.json +5 -0
  19. package/agent-packs/specializations/intakes--engineering-03.json +5 -0
  20. package/agent-packs/specializations/intakes--engineering.json +5 -0
  21. package/agent-packs/specializations/intakes--operator-01.json +5 -0
  22. package/agent-packs/specializations/intakes--operator-02.json +5 -0
  23. package/agent-packs/specializations/operator-skills--audio-and-narration-specialist.json +5 -0
  24. package/agent-packs/specializations/operator-skills--brand-and-copywriting-specialist.json +5 -0
  25. package/agent-packs/specializations/operator-skills--capability-research-specialist.json +5 -0
  26. package/agent-packs/specializations/operator-skills--data-analysis-and-visualization.json +5 -0
  27. package/agent-packs/specializations/operator-skills--financial-evidence-specialist.json +5 -0
  28. package/agent-packs/specializations/operator-skills--operating-template-specialist.json +5 -0
  29. package/agent-packs/specializations/operator-skills--operator-skill-library.json +5 -0
  30. package/agent-packs/specializations/operator-skills--operator-skills.json +5 -0
  31. package/agent-packs/specializations/operator-skills--operator-workflow-evaluation.json +5 -0
  32. package/agent-packs/specializations/operator-skills--privacy-and-agreement-review.json +5 -0
  33. package/agent-packs/specializations/operator-skills--storytelling-specialist.json +5 -0
  34. package/agent-packs/specializations/operator-skills--ux-and-accessibility-specialist.json +5 -0
  35. package/agent-packs/specializations/operator-skills--video-production-specialist.json +5 -0
  36. package/agent-packs/specializations/operator-skills--visual-design-specialist.json +5 -0
  37. package/agent-packs/specializations/operator-skills--workflow-integration-specialist.json +5 -0
  38. package/agent-packs/specializations/operators--access-and-privacy.json +5 -0
  39. package/agent-packs/specializations/operators--accounting-evidence-review.json +5 -0
  40. package/agent-packs/specializations/operators--agreement-operations.json +5 -0
  41. package/agent-packs/specializations/operators--books-operator.json +5 -0
  42. package/agent-packs/specializations/operators--brand-operator.json +5 -0
  43. package/agent-packs/specializations/operators--commerce-operations.json +5 -0
  44. package/agent-packs/specializations/operators--communications-operator.json +5 -0
  45. package/agent-packs/specializations/operators--content-and-assets-operator.json +5 -0
  46. package/agent-packs/specializations/operators--customer-operations.json +5 -0
  47. package/agent-packs/specializations/operators--design-operator.json +5 -0
  48. package/agent-packs/specializations/operators--design-production-02.json +5 -0
  49. package/agent-packs/specializations/operators--design-production-03.json +5 -0
  50. package/agent-packs/specializations/operators--design-production-04.json +5 -0
  51. package/agent-packs/specializations/operators--device-and-feed-operator.json +5 -0
  52. package/agent-packs/specializations/operators--director-operator.json +5 -0
  53. package/agent-packs/specializations/operators--experience-review.json +5 -0
  54. package/agent-packs/specializations/operators--growth-operations.json +5 -0
  55. package/agent-packs/specializations/operators--learn-operator.json +5 -0
  56. package/agent-packs/specializations/operators--localization-operator.json +5 -0
  57. package/agent-packs/specializations/operators--operators.json +5 -0
  58. package/agent-packs/specializations/operators--pay-operator.json +5 -0
  59. package/agent-packs/specializations/operators--research-and-intelligence.json +5 -0
  60. package/agent-packs/specializations/operators--roadmap-operator.json +5 -0
  61. package/agent-packs/specializations/operators--social-content-operator.json +5 -0
  62. package/agent-packs/specializations/operators--talent-operations.json +5 -0
  63. package/agent-packs/specializations/operators--web-operator.json +5 -0
  64. package/agent-packs/specializations/operators--workflow-operator.json +5 -0
  65. package/agent-packs/specializations/specialists--adversarial-qa.json +5 -0
  66. package/agent-packs/specializations/specialists--backlog-dispatch.json +5 -0
  67. package/agent-packs/specializations/specialists--build-01.json +5 -0
  68. package/agent-packs/specializations/specialists--build-02.json +5 -0
  69. package/agent-packs/specializations/specialists--build-03.json +5 -0
  70. package/agent-packs/specializations/specialists--developer-environment-steward.json +5 -0
  71. package/agent-packs/specializations/specialists--documentation-knowledge-steward.json +5 -0
  72. package/agent-packs/specializations/specialists--execution-readiness.json +5 -0
  73. package/agent-packs/specializations/specialists--incident-triage.json +5 -0
  74. package/agent-packs/specializations/specialists--platform-package-steward.json +5 -0
  75. package/agent-packs/specializations/specialists--product-direction-01.json +5 -0
  76. package/agent-packs/specializations/specialists--product-direction-02.json +5 -0
  77. package/agent-packs/specializations/specialists--release-recovery.json +5 -0
  78. package/agent-packs/specializations/specialists--release-watch.json +5 -0
  79. package/agent-packs/specializations/specialists--roadmap-planning-01.json +5 -0
  80. package/agent-packs/specializations/specialists--roadmap-planning-02.json +5 -0
  81. package/agent-packs/specializations/specialists--security-permissions-review.json +5 -0
  82. package/agent-packs/specializations/specialists--system-architecture-01.json +5 -0
  83. package/agent-packs/specializations/specialists--system-architecture-02.json +5 -0
  84. package/agent-packs/specializations/specialists--translation.json +5 -0
  85. package/agent-packs/specializations/specialists--ux-consistency-review.json +5 -0
  86. package/agent-packs/specializations/specialists--workflow-improvement.json +5 -0
  87. package/agent-packs/specializations/specialists--worktree-retirement.json +5 -0
  88. package/agent-packs/src/topolo_agent_pack/cicd.py +101 -0
  89. package/agent-packs/src/topolo_agent_pack/claude.py +22 -7
  90. package/agent-packs/src/topolo_agent_pack/claude_activation.py +157 -0
  91. package/agent-packs/src/topolo_agent_pack/claude_completion_recovery.py +216 -0
  92. package/agent-packs/src/topolo_agent_pack/claude_desktop.py +181 -0
  93. package/agent-packs/src/topolo_agent_pack/claude_dispatch.py +240 -0
  94. package/agent-packs/src/topolo_agent_pack/claude_prompt.py +74 -0
  95. package/agent-packs/src/topolo_agent_pack/claude_runner.py +207 -0
  96. package/agent-packs/src/topolo_agent_pack/claude_service.py +397 -0
  97. package/agent-packs/src/topolo_agent_pack/cli.py +181 -16
  98. package/agent-packs/src/topolo_agent_pack/closed_ledger_fence.py +118 -0
  99. package/agent-packs/src/topolo_agent_pack/cloud_attach.py +128 -0
  100. package/agent-packs/src/topolo_agent_pack/cloud_cutover.py +134 -0
  101. package/agent-packs/src/topolo_agent_pack/cloud_delta.py +162 -0
  102. package/agent-packs/src/topolo_agent_pack/cloud_migrate.py +142 -0
  103. package/agent-packs/src/topolo_agent_pack/cloud_recovery.py +42 -0
  104. package/agent-packs/src/topolo_agent_pack/cloud_work.py +126 -0
  105. package/agent-packs/src/topolo_agent_pack/coordinator_activity.py +27 -0
  106. package/agent-packs/src/topolo_agent_pack/coordinator_context.py +91 -0
  107. package/agent-packs/src/topolo_agent_pack/coordinator_refresh.py +116 -0
  108. package/agent-packs/src/topolo_agent_pack/desktop_acceptance.py +45 -0
  109. package/agent-packs/src/topolo_agent_pack/desktop_launcher.py +114 -0
  110. package/agent-packs/src/topolo_agent_pack/domain_context.py +38 -0
  111. package/agent-packs/src/topolo_agent_pack/execution.py +103 -6
  112. package/agent-packs/src/topolo_agent_pack/frontdoor.py +23 -0
  113. package/agent-packs/src/topolo_agent_pack/linked_task_context.py +80 -0
  114. package/agent-packs/src/topolo_agent_pack/live.py +285 -18
  115. package/agent-packs/src/topolo_agent_pack/native.py +160 -4
  116. package/agent-packs/src/topolo_agent_pack/review_context.py +33 -0
  117. package/agent-packs/src/topolo_agent_pack/role_catalog.py +137 -0
  118. package/agent-packs/src/topolo_agent_pack/role_context.py +63 -0
  119. package/agent-packs/src/topolo_agent_pack/role_refresh.py +50 -0
  120. package/agent-packs/src/topolo_agent_pack/topolo.py +7 -1
  121. package/agent-packs/src/topolo_agent_pack/work.py +322 -0
  122. package/agent-packs/src/topolo_agent_pack/work_adoption.py +122 -0
  123. package/agent-packs/src/topolo_agent_pack/work_artifacts.py +84 -0
  124. package/agent-packs/src/topolo_agent_pack/work_context.py +77 -0
  125. package/agent-packs/src/topolo_agent_pack/work_gui.py +51 -0
  126. package/agent-packs/src/topolo_agent_pack/work_native.py +414 -0
  127. package/agent-packs/src/topolo_agent_pack/work_wait.py +151 -0
  128. package/agent-packs/templates/topolo-coordinators/AGENTS.md +7 -0
  129. package/agent-packs/templates/topolo-coordinators/agents/intakes/cicd/AGENTS.md +6 -0
  130. package/agent-packs/templates/topolo-coordinators/agents/intakes/cicd/config.json +4 -0
  131. package/agent-packs/templates/topolo-coordinators/agents/intakes/engineering/AGENTS.md +3 -0
  132. package/agent-packs/templates/topolo-coordinators/agents/intakes/engineering/config.json +4 -0
  133. package/agent-packs/templates/topolo-coordinators/agents/intakes/operator-01/AGENTS.md +3 -0
  134. package/agent-packs/templates/topolo-coordinators/agents/intakes/operator-01/config.json +4 -0
  135. package/agent-packs/templates/topolo-coordinators/pack.json +56 -0
  136. package/dist/chunk-NYDIAYVY.js +144 -0
  137. package/dist/{claude-pack-backend-YEVPAOID.js → chunk-UY5LEU5C.js} +1659 -21
  138. package/dist/claude-desktop-profile-JCQKSY3K.js +13 -0
  139. package/dist/claude-pack-activation-Q25MCEPU.js +506 -0
  140. package/dist/claude-pack-backend-HUTKGIDS.js +90 -0
  141. package/dist/index.js +152 -73
  142. 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