@topolo/cli 0.13.19 → 0.13.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +89 -10
  142. package/package.json +4 -4
@@ -7,6 +7,8 @@ reviewed role. Changing that intent requires rebuilding the selected policy.
7
7
  import copy
8
8
  import hashlib
9
9
  import json
10
+ import os
11
+ import datetime
10
12
  import pathlib
11
13
  import sqlite3
12
14
  import subprocess
@@ -16,6 +18,26 @@ from contextlib import closing
16
18
  from . import policy, permissions
17
19
  from .storage import PackError, atomic_json, component_key, read_json
18
20
 
21
+ # Native desktop transport/tool configuration is independent of role execution
22
+ # policy. Keep this list explicit; unknown overrides still fail closed.
23
+ DESKTOP_TRANSPORT_CONFIG_KEYS = frozenset('''
24
+ apps.connector_openai_pages.tools
25
+ features.apps_mcp_path_override features.async_compaction
26
+ features.code_mode_interrupt features.code_mode_tool_description_first
27
+ features.code_mode_tool_search features.collaboration_modes
28
+ features.compaction_image_budget features.concurrent_reasoning_summaries
29
+ features.deferred_tool_world_state features.enable_mcp_apps
30
+ features.guardian_approval features.guardian_reuse_parent_compaction
31
+ features.guardianv2 features.guardianv2_decisions_comparison
32
+ features.image_detail_original features.image_generation features.image_resize_notice
33
+ features.incremental_tools features.instant_interrupt features.item_ids
34
+ features.mcp_oauth_refresh_coordination features.realtime_conversation
35
+ features.recommended_plugins features.request_rule features.unified_image_budget
36
+ features.workspace_dependencies mcp_servers.artifact_session
37
+ mcp_servers.cua_repl mcp_servers.node_repl
38
+ plugins.codex-app-tools@openai-bundled.mcp_servers.codex_app.enabled_tools
39
+ '''.split())
40
+
19
41
 
20
42
  def prepare_runtime(lib, components, runtime):
21
43
  """Validate existing role intent without installing a policy or changing history."""
@@ -30,7 +52,15 @@ def prepare_runtime(lib, components, runtime):
30
52
  component['key'] in keys or component['threadId'] in identities):
31
53
  raise PackError('Execution policy has no unique owned role: ' + component['key'])
32
54
  keys.add(component['key']); identities.add(component['threadId'])
33
- consent = permissions.load(lib, matches[0]['pack'], manifest['agents']) if hasattr(lib, 'root') else None
55
+ # Checkpoints contain only the active selection. Installation consent is
56
+ # owned by the original complete pack, not that history snapshot.
57
+ consent = None
58
+ if hasattr(lib, 'root'):
59
+ policy_manifest, _ = lib.load(matches[0]['pack'])
60
+ original = [a for a in policy_manifest['agents'] if component_key(a) == component['key']]
61
+ if len(original) != 1:
62
+ raise PackError('Checkpoint role has no unique original policy identity')
63
+ consent = permissions.load(lib, matches[0]['pack'], policy_manifest['agents'])
34
64
  rows.append({'key': component['key'], 'threadId': component['threadId'],
35
65
  'sourceRevision': revision, 'cwd': cwds[component['threadId']],
36
66
  'intakeKey': consent['intakeRoutes'][component['key']] if consent else None,
@@ -54,7 +84,7 @@ class ExecutionGate:
54
84
  def __init__(self, document):
55
85
  if document.get('format') != 'topolo-codex-runtime-execution' or document.get('schemaVersion') != 1:
56
86
  raise PackError('Unqualified runtime execution policy')
57
- self.roles = {}; self.pending = {}; self.verified = set(); self.catalog = {}
87
+ self.roles = {}; self.pending = {}; self.verified = set(); self.catalog = {}; self.desktop_home = None
58
88
  for role in document['roles']:
59
89
  tid = role['threadId']; spec = role['settings']
60
90
  if tid in self.roles or not pathlib.Path(role['cwd']).is_absolute():
@@ -142,6 +172,26 @@ class ExecutionGate:
142
172
  return {'type': 'workspaceWrite', 'networkAccess': False, 'writableRoots': [role['cwd']],
143
173
  'excludeSlashTmp': True, 'excludeTmpdirEnvVar': True}
144
174
 
175
+ def qualify_roots(self, role, roots):
176
+ if not isinstance(roots, list) or len(roots)>2 or any(not isinstance(p,str) for p in roots) or len(set(roots))!=len(roots):
177
+ raise PackError('Frontend workspace roots have an unsupported shape')
178
+ if not roots:return
179
+ if roots[0]!=role['cwd']:
180
+ raise PackError('Frontend workspace roots differ from the reviewed role')
181
+ if len(roots)==1:return
182
+ if self.desktop_home is None:
183
+ raise PackError('Owned desktop visualization root is unavailable')
184
+ path=pathlib.Path(roots[1]);base=pathlib.Path(self.desktop_home)/'visualizations'
185
+ try:
186
+ relative=path.relative_to(base)
187
+ year,month,day,tid=relative.parts
188
+ if tid!=role['threadId'] or len(year)!=4 or len(month)!=2 or len(day)!=2:
189
+ raise ValueError()
190
+ datetime.date(int(year),int(month),int(day))
191
+ if not path.is_absolute() or path.resolve()!=path or not path.is_dir() or path.stat().st_uid!=os.getuid():
192
+ raise ValueError()
193
+ except (ValueError,OSError):raise PackError('Frontend visualization root is not the owned thread directory')
194
+
145
195
  def prepare(self, message):
146
196
  value = copy.deepcopy(message); method = value.get('method'); params = value.get('params') or {}
147
197
  tid = params.get('threadId'); role = self.roles.get(tid)
@@ -158,11 +208,47 @@ class ExecutionGate:
158
208
  raise PackError('Dynamic lane needs actual loaded role context before task execution')
159
209
  if spec['model_reasoning_effort'] not in self.catalog.get(spec['model'], set()):
160
210
  raise PackError('Role model/effort is absent from the native account catalog; no fallback selected')
161
- if any(params.get(key) is not None for key in ['permissions', 'path', 'history', 'collaborationMode', 'environments', 'runtimeWorkspaceRoots']):
162
- raise PackError('Frontend supplied unreviewed permission, identity or execution overrides')
211
+ collaboration = params.get('collaborationMode')
212
+ if collaboration is not None:
213
+ settings = collaboration.get('settings') if isinstance(collaboration, dict) else None
214
+ if (method != 'turn/start' or not isinstance(collaboration, dict) or
215
+ set(collaboration) != {'mode', 'settings'} or collaboration['mode'] != 'default' or
216
+ not isinstance(settings, dict) or set(settings) - {'model', 'reasoning_effort', 'developer_instructions'} or
217
+ settings.get('model') != spec['model'] or
218
+ settings.get('reasoning_effort') not in (None, spec['model_reasoning_effort']) or
219
+ (settings.get('developer_instructions') is not None and
220
+ (not isinstance(settings['developer_instructions'], str) or
221
+ len(settings['developer_instructions'].encode()) > 64000))):
222
+ raise PackError('Frontend collaboration mode conflicts with the reviewed role policy')
223
+ # Native collaboration settings take precedence over top-level model
224
+ # and effort. Bind them to the same reviewed tuple before forwarding.
225
+ collaboration['settings']['reasoning_effort'] = spec['model_reasoning_effort']
226
+ # Desktop supplies its generic Default-mode prompt here. It is not
227
+ # reviewed role context: discard it rather than accepting any extra
228
+ # frontend developer authority. This is independent of app builds.
229
+ collaboration['settings']['developer_instructions'] = None
230
+ # An empty environment selection explicitly disables environment access;
231
+ # the desktop sends it on ordinary local turns. Nonempty selections
232
+ # still require independently reviewed environment identity and roots.
233
+ environments = params.get('environments')
234
+ if environments not in (None, []):
235
+ if not isinstance(environments,list) or len(environments)!=1 or not isinstance(environments[0],dict):
236
+ raise PackError('Frontend environment selection has an unsupported shape')
237
+ environment=environments[0]
238
+ if (set(environment)-{'environmentId','cwd','runtimeWorkspaceRoots'} or
239
+ environment.get('environmentId')!='local' or environment.get('cwd')!=role['cwd']):
240
+ raise PackError('Frontend environment selection differs from the reviewed local role')
241
+ if environment.get('runtimeWorkspaceRoots') is not None:self.qualify_roots(role,environment['runtimeWorkspaceRoots'])
242
+ roots = params.get('runtimeWorkspaceRoots')
243
+ if roots is not None:self.qualify_roots(role,roots)
244
+ if environments and roots is not None and environments[0].get('runtimeWorkspaceRoots') not in (None,roots):
245
+ raise PackError('Frontend top-level and local environment roots conflict')
246
+ forbidden = [key for key in ['permissions', 'path', 'history'] if params.get(key) is not None]
247
+ if forbidden:
248
+ raise PackError('Frontend supplied unreviewed permission, identity or execution overrides: ' + ','.join(forbidden))
163
249
  wanted = {'cwd': role['cwd'], 'model': spec['model'], 'approvalPolicy': spec['approval_policy'],
164
250
  'approvalsReviewer': spec['approvals_reviewer']}
165
- if role.get('dynamicLaneId') and method=='thread/resume':wanted['allowProviderModelFallback']=False
251
+ if (role.get('dynamicLaneId') or role.get('taskWorkerId')) and method=='thread/resume':wanted['allowProviderModelFallback']=False
166
252
  if method == 'thread/resume':
167
253
  wanted['sandbox'] = spec['sandbox_mode']
168
254
  wanted['config'] = {'model_reasoning_effort': spec['model_reasoning_effort'],
@@ -173,6 +259,13 @@ class ExecutionGate:
173
259
  raise PackError('Role needs a verified native resume before starting a turn')
174
260
  wanted.update(effort=spec['model_reasoning_effort'], sandboxPolicy=self.sandbox(role))
175
261
  for key, expected in wanted.items():
262
+ if key=='config' and isinstance(params.get(key),dict):
263
+ supplied=params[key]
264
+ if (set(supplied)-set(expected)-DESKTOP_TRANSPORT_CONFIG_KEYS or
265
+ any(supplied[k]!=expected[k] for k in set(supplied)&set(expected))):
266
+ raise PackError('Frontend config conflicts with the reviewed role policy')
267
+ params[key]={**copy.deepcopy(supplied),**copy.deepcopy(expected)}
268
+ continue
176
269
  if params.get(key) is not None and params[key] != expected:
177
270
  raise PackError('Frontend ' + key + ' conflicts with the reviewed role policy')
178
271
  params[key] = copy.deepcopy(expected)
@@ -207,7 +300,11 @@ class ExecutionGate:
207
300
 
208
301
  def load_gate(runtime):
209
302
  path = pathlib.Path(runtime) / 'execution-policy.json'
210
- return ExecutionGate(read_json(path)) if path.exists() else None
303
+ if not path.exists():return None
304
+ gate=ExecutionGate(read_json(path))
305
+ runtime=pathlib.Path(runtime).resolve()
306
+ if runtime.parent.name=='runtimes':gate.desktop_home=runtime.parent.parent/'desktop-home'
307
+ return gate
211
308
 
212
309
 
213
310
  def qualify_protocol(binary, run=subprocess.run, *, lane_creation=False):
@@ -0,0 +1,23 @@
1
+ """Legacy explicit native-message request helpers.
2
+
3
+ Conversational coordinators use coordinator_context instead: a human status or
4
+ follow-up must never call this conversion or replace native input with a receipt.
5
+ These helpers retain the original deterministic identity for explicit callers.
6
+ """
7
+ import copy,uuid
8
+ from .work import require
9
+
10
+ def request(value,channel):
11
+ params=value['params'];identity=params.get('clientUserMessageId')
12
+ require(isinstance(identity,str) and 1<=len(identity)<=200,'Native human message identity is missing; use the work GUI so retries cannot duplicate a task')
13
+ inputs=params.get('input')
14
+ require(isinstance(inputs,list) and inputs and all(isinstance(x,dict) and x.get('type')=='text' and isinstance(x.get('text'),str) for x in inputs),'This front door currently requires text; submit attachment-backed work through its bound task packet')
15
+ objective='\n'.join(x['text'] for x in inputs)
16
+ require(objective.strip(),'Empty task request')
17
+ task_id=str(uuid.uuid5(uuid.NAMESPACE_URL,'topolo-frontdoor:'+params['threadId']+':'+identity))
18
+ return {'requestId':task_id,'objective':objective,'channel':channel,'canonicalClaimId':None}
19
+
20
+ def acknowledgement(value,task):
21
+ result=copy.deepcopy(value)
22
+ result['params']['input']=[{'type':'text','text':'Topolo front-door queue receipt. The human request has already been durably recorded as task '+task['id']+'. Its current state is '+task['state']+'. A separate temporary specialist owns routing and execution. This turn is acknowledgement only. Do not read files, use tools, run commands, contact anyone, inspect servers or carry out any previous assignment. Reply briefly that the request is recorded and progress is in the work GUI: http://127.0.0.1:8769/ . Do not claim the work is complete.'}]
23
+ return result
@@ -0,0 +1,80 @@
1
+ """Explicit same-authority task citations, not portfolio or recursive history."""
2
+ import asyncio,copy,hashlib,json,pathlib,re
3
+ from .storage import atomic_json,canonical,PackError
4
+ from .work import require
5
+
6
+ UUID=re.compile(r'(?<![A-Za-z0-9])[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(?![A-Za-z0-9])',re.I)
7
+ MAX_TASKS=4
8
+ MAX_EVIDENCE=8
9
+ MAX_FILE=512*1024
10
+ MAX_BYTES=2*1024*1024
11
+
12
+ def references(task):
13
+ text=task['objective']+'\n'+'\n'.join(r['note'] for r in task.get('humanResponses',[]))
14
+ require(len(text)<=100000,'Linked-task citation input exceeds the bounded reader')
15
+ ids=list(dict.fromkeys(x.lower() for x in UUID.findall(text) if x.lower()!=task['id']))
16
+ require(len(ids)<=MAX_TASKS,'Too many explicit linked task citations; select at most four')
17
+ return ids
18
+
19
+ def legacy_reference(ref):
20
+ return isinstance(ref,dict) and set(ref)=={'path','sha256'} and isinstance(ref['path'],str) and 1<=len(ref['path'])<=4096 and pathlib.PurePosixPath(ref['path']).is_absolute() and isinstance(ref['sha256'],str) and re.fullmatch('[0-9a-f]{64}',ref['sha256']) is not None
21
+
22
+ def portable(store,linked,target,budget):
23
+ """Use only immutable references read from the selected canonical task."""
24
+ from .work_context import materialize
25
+ from .work_artifacts import read_checkpoint
26
+ from .topolo import read_artifact
27
+ context=linked.get('contextRef')
28
+ if context:
29
+ require(isinstance(context,dict),'Selected linked context reference must be an object')
30
+ if 'artifact' in context:require(isinstance(context['artifact'],dict) and type(context['artifact'].get('size')) is int and 0<context['artifact']['size']<=200000,'Selected linked context artifact exceeds the bounded reader')
31
+ materialize(store.root,linked,target,lambda:store.client)
32
+ continuity=target/'context/continuity.md';require(continuity.is_file() and continuity.stat().st_size<=128000,'Selected linked task context exceeds the model context bound')
33
+ checkpoints=[];unavailable=[]
34
+ result=linked.get('result') or {};review=linked.get('verification') or {}
35
+ for label,proof,worker in [('execution',result.get('checkpoint'),result.get('workerId')),('review',review.get('nativeCheckpoint'),None)]:
36
+ if not proof:continue
37
+ if legacy_reference(proof):
38
+ unavailable.append({'kind':label+'-checkpoint','originReference':copy.deepcopy(proof),'reason':'Legacy local-only reference; bytes are not portable or read here. Canonical cloud summary remains recorded evidence, not a reverified checkpoint.'});continue
39
+ payload=read_checkpoint(store.root,proof,linked['id'],worker,store.client)
40
+ expected={k:result[k] for k in ['summary','artifacts','references']} if label=='execution' else {k:review[k] for k in ['verdict','summary','evidence']}
41
+ require(payload['phase']==('execute' if label=='execution' else 'review') and payload['outputError'] is None and json.loads(payload['output'])==expected,'Linked task result differs from its immutable checkpoint')
42
+ path=target/(label+'-checkpoint.json');atomic_json(path,payload)
43
+ checkpoints.append({'phase':label,'path':str(path),'proof':proof,'sha256':hashlib.sha256(path.read_bytes()).hexdigest()})
44
+ evidence=[]
45
+ declared=result.get('artifacts',[])+review.get('evidence',[])
46
+ require(len(declared)<=32,'Too many selected linked task evidence references')
47
+ for row in declared:
48
+ if legacy_reference(row):
49
+ unavailable.append({'kind':'declared-evidence','originReference':copy.deepcopy(row),'reason':'Legacy local-only evidence; do not read the origin path or claim its bytes verified.'});continue
50
+ require(isinstance(row,dict) and isinstance(row.get('artifact'),dict),'Linked task evidence must use a portable cloud artifact reference')
51
+ ref=row['artifact'];require(type(ref.get('size')) is int and 0<ref['size']<=MAX_FILE,'Linked task evidence exceeds the bounded file reader')
52
+ require(budget['files']<MAX_EVIDENCE and budget['bytes']+ref['size']<=MAX_BYTES,'Linked task evidence exceeds the selected context budget')
53
+ path=target/('evidence-'+str(len(evidence)))
54
+ read_artifact(store.client,{'input':{'bytesScope':ref['scope'],'bytesWorkspaceId':ref['workspaceId']}},ref['key'],ref,path)
55
+ data=path.read_bytes();require(len(data)==ref['size'] and hashlib.sha256(data).hexdigest()==ref['sha256'],'Linked task evidence digest differs')
56
+ budget['files']+=1;budget['bytes']+=len(data);evidence.append({'title':row.get('title','Selected task evidence'),'path':str(path),'artifact':copy.deepcopy(ref)})
57
+ return {'contextMaterialized':bool(context),'checkpoints':checkpoints,'evidence':evidence,'unavailableEvidence':unavailable,'recordedEvidenceOnly':bool(unavailable)}
58
+
59
+ async def materialize(store,task,cwd,get_task):
60
+ ids=references(task)
61
+ if not ids:return None
62
+ require(getattr(store,'cloud',False) is True,'Linked tasks require the current cloud task authority')
63
+ cwd=pathlib.Path(cwd);require(cwd.resolve()==cwd and cwd.is_dir(),'Owned linked-task workspace required')
64
+ base=cwd/'context';require(not base.is_symlink(),'Linked-task context cannot be a symlink');base.mkdir(exist_ok=True,mode=0o700)
65
+ target=base/'linked-tasks';require(not target.exists() and not target.is_symlink(),'Linked task context already exists; do not overwrite another dispatch')
66
+ target.mkdir(mode=0o700);rows=[];missing=[];budget={'files':0,'bytes':0}
67
+ for tid in ids:
68
+ try:linked=await get_task(tid)
69
+ except PackError as error:
70
+ if str(error)!='Unknown task':raise
71
+ missing.append(tid);continue
72
+ require(linked.get('id')==tid,'Linked cloud task identity differs')
73
+ selected={k:copy.deepcopy(linked[k]) for k in ['id','revision','state','phase','attempt','channel','objective','summary','ownerInstallationId','route','contextRef','result','verification','checkpoints'] if k in linked}
74
+ require(len(canonical(selected))<=128000,'Linked task selected record exceeds the context bound')
75
+ folder=target/tid;folder.mkdir(mode=0o700);atomic_json(folder/'task.json',selected)
76
+ proof=await asyncio.to_thread(portable,store,linked,folder,budget)
77
+ rows.append({'taskId':tid,'revision':linked['revision'],'state':linked['state'],'recordPath':str(folder/'task.json'),**proof})
78
+ index={'format':'topolo-selected-linked-tasks-v1','taskId':task['id'],'authority':{k:store.binding[k] for k in ['organizationId','workspaceId','documentId','datasetId']},'selection':'Explicit UUID citations in the current objective and human responses only; no recursive history','tasks':rows,'unavailableTaskIds':missing,'evidenceBoundary':'Canonical cloud summaries/verification are recorded evidence. Only listed portable checkpoint/artifact files have verified bytes; unavailableEvidence preserves legacy references without reading origin paths or implying byte verification. Compare recorded findings honestly and disclose omitted legacy proof. Preserve owners, holds and UNKNOWN; this grants no adoption or replay authority. Do not substitute an unrelated first retained page for the cited findings.'}
79
+ atomic_json(base/'linked-tasks.json',index)
80
+ return index