@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.
- 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 +89 -10
- package/package.json +4 -4
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""Durable task records and single-use native workers; no model or transport mocks.
|
|
2
|
+
|
|
3
|
+
The SQLite record owns continuity. Native threads are disposable execution slots.
|
|
4
|
+
A reserved/uncertain send is never replayed by process recovery.
|
|
5
|
+
"""
|
|
6
|
+
import copy
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import pathlib
|
|
11
|
+
import re
|
|
12
|
+
import sqlite3
|
|
13
|
+
import time
|
|
14
|
+
import uuid
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
from .storage import PackError, canonical
|
|
17
|
+
|
|
18
|
+
STATES={'queued','routing','executing','reviewing','completed','needs_input','blocked','failed','unknown'}
|
|
19
|
+
TERMINAL={'completed','needs_input','blocked','failed','unknown'}
|
|
20
|
+
|
|
21
|
+
def require(ok,message):
|
|
22
|
+
if not ok:raise PackError(message)
|
|
23
|
+
|
|
24
|
+
def text(value,limit=16000):
|
|
25
|
+
return isinstance(value,str) and 0<len(value.strip())<=limit
|
|
26
|
+
|
|
27
|
+
class Store:
|
|
28
|
+
def __init__(self,root):
|
|
29
|
+
self.root=pathlib.Path(root)/'work';self.root.mkdir(mode=0o700,exist_ok=True)
|
|
30
|
+
require(not self.root.is_symlink() and self.root.stat().st_uid==os.getuid() and not self.root.stat().st_mode&0o022,'Work store must be privately owned')
|
|
31
|
+
self.path=self.root/'tasks.sqlite'
|
|
32
|
+
require(not self.path.is_symlink(),'Work database cannot be a symlink')
|
|
33
|
+
with self.db() as db:
|
|
34
|
+
db.executescript('''CREATE TABLE IF NOT EXISTS tasks(id TEXT PRIMARY KEY, input_hash TEXT NOT NULL, body TEXT NOT NULL);
|
|
35
|
+
CREATE TABLE IF NOT EXISTS workers(id TEXT PRIMARY KEY,task_id TEXT NOT NULL,body TEXT NOT NULL);
|
|
36
|
+
CREATE TABLE IF NOT EXISTS events(sequence INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL, body TEXT NOT NULL);
|
|
37
|
+
CREATE TABLE IF NOT EXISTS retained(id TEXT PRIMARY KEY,body TEXT NOT NULL);''')
|
|
38
|
+
db.execute('CREATE TABLE IF NOT EXISTS responses(id TEXT PRIMARY KEY,input_hash TEXT NOT NULL,task_id TEXT NOT NULL)')
|
|
39
|
+
self.path.chmod(0o600)
|
|
40
|
+
@contextmanager
|
|
41
|
+
def db(self):
|
|
42
|
+
require(not (self.root/'cutover.json').exists(),'Legacy work is durably fenced for cloud cutover')
|
|
43
|
+
db=sqlite3.connect(self.path,timeout=10)
|
|
44
|
+
try:
|
|
45
|
+
db.execute('BEGIN IMMEDIATE')
|
|
46
|
+
require(not (self.root/'cutover.json').exists(),'Legacy work is durably fenced for cloud cutover')
|
|
47
|
+
yield db;db.commit()
|
|
48
|
+
except BaseException:db.rollback();raise
|
|
49
|
+
finally:db.close()
|
|
50
|
+
def event(self,db,tid,kind,summary):
|
|
51
|
+
db.execute('INSERT INTO events(task_id,body) VALUES (?,?)',(tid,json.dumps({'taskId':tid,'kind':kind,'summary':summary,'at':time.time()})))
|
|
52
|
+
def submit(self,request):
|
|
53
|
+
require(isinstance(request,dict) and set(request)=={'requestId','objective','channel','canonicalClaimId'},'Exact work request fields required')
|
|
54
|
+
try:valid=str(uuid.UUID(request['requestId']))==request['requestId']
|
|
55
|
+
except (ValueError,TypeError,AttributeError):valid=False
|
|
56
|
+
require(valid and text(request['objective']) and request['channel'] in {'engineering','operations','cicd'} and (request['canonicalClaimId'] is None or text(request['canonicalClaimId'],240)),'Invalid work request')
|
|
57
|
+
identity=request['requestId'];fingerprint=hashlib.sha256(canonical(request)).hexdigest()
|
|
58
|
+
with self.db() as db:
|
|
59
|
+
row=db.execute('SELECT input_hash,body FROM tasks WHERE id=?',(identity,)).fetchone()
|
|
60
|
+
if row:
|
|
61
|
+
require(row[0]==fingerprint,'Request ID already has a different objective');return json.loads(row[1]),False
|
|
62
|
+
claim=request['canonicalClaimId']
|
|
63
|
+
if claim:
|
|
64
|
+
retained=db.execute('SELECT body FROM retained WHERE id=?',(claim,)).fetchone()
|
|
65
|
+
require(retained is None,'Existing claim retains its owner and checkpoint; per-task adoption is required before dispatch')
|
|
66
|
+
raise PackError('Unknown canonical claim; reconcile its current owner before dispatch')
|
|
67
|
+
task={**copy.deepcopy(request),'id':identity,'revision':1,'attempt':1,'state':'queued','phase':'route','summary':'Request recorded; routing pending.','createdAt':time.time(),'updatedAt':time.time(),'deliveryOwner':'task:'+identity,'workerIds':[],'checkpoints':[]}
|
|
68
|
+
db.execute('INSERT INTO tasks VALUES (?,?,?)',(identity,fingerprint,json.dumps(task)));self.event(db,identity,'requested',task['summary']);return task,True
|
|
69
|
+
def get(self,tid):
|
|
70
|
+
with self.db() as db:
|
|
71
|
+
row=db.execute('SELECT body FROM tasks WHERE id=?',(tid,)).fetchone();require(row is not None,'Unknown task');return json.loads(row[0])
|
|
72
|
+
def adopt_retained(self,request,prepared,receipt,parent_proof=None):
|
|
73
|
+
"""Commit a broker-verified receipt and unique continuation atomically.
|
|
74
|
+
|
|
75
|
+
Retained inventory is immutable. The original delivery task, source
|
|
76
|
+
owner, failed jobs and all uncertain effects retain their custody.
|
|
77
|
+
"""
|
|
78
|
+
from .work_adoption import validate,digest,request_digest
|
|
79
|
+
validate(request)
|
|
80
|
+
require(prepared.get('requestSHA256')==request_digest(request)
|
|
81
|
+
and receipt.get('planSHA256')==digest(prepared)
|
|
82
|
+
and receipt.get('authority')=='current-authenticated-human-instruction'
|
|
83
|
+
and receipt.get('scope')==prepared.get('scope')=='read-only-checkpoint-diagnostic',
|
|
84
|
+
'Verified current human diagnostic authorization required')
|
|
85
|
+
identity=request['requestId'];fingerprint=digest(request)
|
|
86
|
+
with self.db() as db:
|
|
87
|
+
row=db.execute('SELECT input_hash,body FROM tasks WHERE id=?',(identity,)).fetchone()
|
|
88
|
+
if row:
|
|
89
|
+
require(row[0]==fingerprint,'Request ID already has a different adoption')
|
|
90
|
+
task=json.loads(row[1])
|
|
91
|
+
require(task.get('adoption',{}).get('receipt')==receipt,'Adoption receipt differs')
|
|
92
|
+
return task,False
|
|
93
|
+
row=db.execute('SELECT body FROM retained WHERE id=?',(request['canonicalClaimId'],)).fetchone()
|
|
94
|
+
require(row is not None and digest(json.loads(row[0]))==prepared['retainedSHA256'],
|
|
95
|
+
'Retained claim changed; preserve original custody')
|
|
96
|
+
tasks=[json.loads(r[0]) for r in db.execute('SELECT body FROM tasks')]
|
|
97
|
+
require(not any(t.get('canonicalClaimId')==request['canonicalClaimId'] for t in tasks),
|
|
98
|
+
'Canonical claim already has a continuation; no duplicate dispatch')
|
|
99
|
+
related={t['id'] for t in tasks if t.get('canonicalClaimId')==request['canonicalClaimId']}|{request['continuationOwnerTaskId']}
|
|
100
|
+
workers=[json.loads(r[0]) for r in db.execute('SELECT body FROM workers')]
|
|
101
|
+
require(all(w['state']=='archived' and w.get('nativeArchived') is True for w in workers if w['taskId'] in related),
|
|
102
|
+
'Current cloud worker retains active or unresolved custody')
|
|
103
|
+
parents=[t for t in tasks if t['id']==request['continuationOwnerTaskId']]
|
|
104
|
+
require(len(parents)==1 and parents[0]['channel']=='engineering'
|
|
105
|
+
and parents[0]['state'] not in {'unknown','completed','failed'},
|
|
106
|
+
'Originating delivery task is missing, closed or uncertain')
|
|
107
|
+
parent=parents[0]
|
|
108
|
+
require(parent['revision']==request['continuationRevision'],'Parent revision changed; reread before adoption')
|
|
109
|
+
from .work_adoption import parent_heads
|
|
110
|
+
owned_workers=[w for w in workers if w['taskId']==parent['id']]
|
|
111
|
+
require(digest(parent_heads(parent,owned_workers))==request['parentCheckpointSHA256'],'Parent checkpoint heads changed')
|
|
112
|
+
if parent.get('ownerInstallationId')!=receipt['installationId']:
|
|
113
|
+
require(parent_proof is not None and parent['state'] in {'blocked','needs_input'}
|
|
114
|
+
and parent_proof['fromInstallationId']==parent.get('ownerInstallationId')
|
|
115
|
+
and parent_proof['toInstallationId']==receipt['installationId']
|
|
116
|
+
and parent_proof['revision']==parent['revision']
|
|
117
|
+
and parent_proof['checkpointHeadsSHA256']==request['parentCheckpointSHA256'],
|
|
118
|
+
'Closed parent cloud checkpoint transfer is unverified')
|
|
119
|
+
parent['ownerInstallationId']=receipt['installationId']
|
|
120
|
+
self.event(db,parent['id'],'closed-parent-owner-adopted','Current human authorized portable checkpoint ownership; parent state and holds unchanged.')
|
|
121
|
+
task={k:copy.deepcopy(request[k]) for k in ['requestId','objective','channel','canonicalClaimId']}
|
|
122
|
+
task.update(id=identity,revision=1,attempt=1,state='executing',phase='execute',
|
|
123
|
+
summary='Current human authorized diagnostic-only continuation; original source and UNKNOWN holds preserved.',
|
|
124
|
+
createdAt=time.time(),updatedAt=time.time(),
|
|
125
|
+
deliveryOwner=parents[0]['deliveryOwner'],workerIds=[],checkpoints=[],
|
|
126
|
+
route={'role':request['role'],'task':{'type':'retained-checkpoint-diagnostic',
|
|
127
|
+
'complexity':'complex','risk':'security-critical'},'acceptance':request['acceptance']},
|
|
128
|
+
adoption={'request':copy.deepcopy(request),'plan':copy.deepcopy(prepared),'receipt':copy.deepcopy(receipt),'parentTransfer':copy.deepcopy(parent_proof)},
|
|
129
|
+
effectBoundary='Read-only checkpoint diagnosis. Source, tests, provider writes and releases require their separate existing gates.')
|
|
130
|
+
db.execute('INSERT INTO tasks VALUES (?,?,?)',(identity,fingerprint,json.dumps(task)))
|
|
131
|
+
parent=parents[0];parent.setdefault('continuations',[]).append({'taskId':identity,'canonicalClaimId':request['canonicalClaimId']})
|
|
132
|
+
parent.update(revision=parent['revision']+1,updatedAt=time.time())
|
|
133
|
+
db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(parent),parent['id']))
|
|
134
|
+
self.event(db,identity,'retained-checkpoint-adopted',task['summary'])
|
|
135
|
+
self.event(db,parent['id'],'continuation-linked','Canonical claim checkpoint continuation linked; delivery acceptance remains open.')
|
|
136
|
+
return task,True
|
|
137
|
+
def list(self):
|
|
138
|
+
with self.db() as db:return [json.loads(x[0]) for x in db.execute('SELECT body FROM tasks ORDER BY rowid DESC LIMIT 500')]
|
|
139
|
+
def update(self,tid,revision,changes,kind):
|
|
140
|
+
require(isinstance(changes,dict) and not(set(changes)-{'state','phase','summary','route','workerIds','checkpoints','result','verification','archiveError','contextRef'}),'Unsupported work transition')
|
|
141
|
+
if 'state' in changes:require(changes['state'] in STATES,'Unknown work state')
|
|
142
|
+
with self.db() as db:
|
|
143
|
+
row=db.execute('SELECT body FROM tasks WHERE id=?',(tid,)).fetchone();require(row is not None,'Unknown task');task=json.loads(row[0]);require(task['revision']==revision,'Task changed; reread before transition')
|
|
144
|
+
task.update(copy.deepcopy(changes));task.update(revision=revision+1,updatedAt=time.time());db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(task),tid));self.event(db,tid,kind,task['summary']);return task
|
|
145
|
+
def re_review(self,request):
|
|
146
|
+
require(isinstance(request,dict) and set(request)=={'requestId','taskId','revision','reviewWorkerId','reviewCheckpoint','note'},'Exact review-only continuation required')
|
|
147
|
+
require(type(request['revision']) is int and text(request['requestId'],100) and text(request['note'],8000),'Review continuation identity and reason required')
|
|
148
|
+
fingerprint=hashlib.sha256(canonical(request)).hexdigest()
|
|
149
|
+
with self.db() as db:
|
|
150
|
+
previous=db.execute('SELECT input_hash,task_id FROM responses WHERE id=?',(request['requestId'],)).fetchone()
|
|
151
|
+
if previous:
|
|
152
|
+
require(previous==(fingerprint,request['taskId']),'Review continuation identity already used')
|
|
153
|
+
return json.loads(db.execute('SELECT body FROM tasks WHERE id=?',(request['taskId'],)).fetchone()[0]),False
|
|
154
|
+
row=db.execute('SELECT body FROM tasks WHERE id=?',(request['taskId'],)).fetchone()
|
|
155
|
+
require(row is not None,'Unknown review task');task=json.loads(row[0])
|
|
156
|
+
require(task['revision']==request['revision'] and task['state'] in {'blocked','needs_input'}
|
|
157
|
+
and task.get('phase')=='review','Task is not the exact rejected review checkpoint')
|
|
158
|
+
workers=[json.loads(w[0]) for w in db.execute('SELECT body FROM workers WHERE task_id=?',(task['id'],))]
|
|
159
|
+
require(all(w['state']=='archived' and w.get('nativeArchived') is True or w['state']=='failed' and not w.get('turnId') and (not w.get('nativeId') or w.get('nativeArchived') is True) for w in workers),'Existing native closure is unresolved; no review continuation')
|
|
160
|
+
rejected=[w for w in workers if w['id']==request['reviewWorkerId']]
|
|
161
|
+
require(len(rejected)==1 and rejected[0]['phase']=='review' and rejected[0]['state']=='archived'
|
|
162
|
+
and rejected[0].get('nativeArchived') is True
|
|
163
|
+
and rejected[0].get('attempt',1)==task.get('attempt',1)
|
|
164
|
+
and rejected[0].get('reviewGeneration',0)==task.get('reviewGeneration',0)
|
|
165
|
+
and rejected[0].get('checkpoint')==request['reviewCheckpoint']
|
|
166
|
+
and task.get('verification',{}).get('nativeCheckpoint')==request['reviewCheckpoint']
|
|
167
|
+
and task['verification']['verdict'] in {'blocked','needs_input'},'Exact latest rejected review must be preserved')
|
|
168
|
+
result=task.get('result',{});execution=[w for w in workers if w['id']==result.get('workerId')]
|
|
169
|
+
require(len(execution)==1 and execution[0]['phase']=='execute' and execution[0]['state']=='archived'
|
|
170
|
+
and execution[0].get('nativeArchived') is True
|
|
171
|
+
and execution[0].get('attempt',1)==task.get('attempt',1)
|
|
172
|
+
and execution[0].get('checkpoint')==result.get('checkpoint') and task.get('route'),
|
|
173
|
+
'Existing exact execution result is required; execution cannot replay')
|
|
174
|
+
task.setdefault('reviewHistory',[]).append({'reviewWorkerId':request['reviewWorkerId'],
|
|
175
|
+
'generation':task.get('reviewGeneration',0),'verification':copy.deepcopy(task['verification']),
|
|
176
|
+
'continuation':copy.deepcopy(request)})
|
|
177
|
+
task.update(state='reviewing',phase='review',reviewGeneration=task.get('reviewGeneration',0)+1,
|
|
178
|
+
revision=task['revision']+1,updatedAt=time.time(),
|
|
179
|
+
summary='Review-only continuation authorized; original execution and rejected review retained.')
|
|
180
|
+
db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(task),task['id']))
|
|
181
|
+
db.execute('INSERT INTO responses VALUES (?,?,?)',(request['requestId'],fingerprint,task['id']))
|
|
182
|
+
self.event(db,task['id'],'review-continuation-requested',task['summary'])
|
|
183
|
+
return task,True
|
|
184
|
+
|
|
185
|
+
def reserve(self,tid,phase,role,resolution,capacity):
|
|
186
|
+
require(phase in {'route','execute','review'} and type(capacity) is int and 1<=capacity<=16,'Invalid worker reservation')
|
|
187
|
+
with self.db() as db:
|
|
188
|
+
rows=[json.loads(r[0]) for r in db.execute('SELECT body FROM workers')]
|
|
189
|
+
require(sum(w['state'] in {'reserved','creating','running','archiving','unknown'} for w in rows)<capacity,'Execution capacity occupied')
|
|
190
|
+
task=json.loads(db.execute('SELECT body FROM tasks WHERE id=?',(tid,)).fetchone()[0]);attempt=task.get('attempt',1)
|
|
191
|
+
require(not any(w['taskId']==tid and w['phase']==phase and w.get('attempt',1)==attempt and (phase!='review' or w.get('reviewGeneration',0)==task.get('reviewGeneration',0)) for w in rows),'A task phase already has a worker; never reuse or replay it')
|
|
192
|
+
wid=str(uuid.uuid4());worker={'id':wid,'taskId':tid,'phase':phase,'attempt':attempt,'role':role,'resolution':copy.deepcopy(resolution),'state':'reserved','nativeId':None,'turnId':None,'createdAt':time.time()}
|
|
193
|
+
if phase=='review':worker['reviewGeneration']=task.get('reviewGeneration',0)
|
|
194
|
+
db.execute('INSERT INTO workers VALUES (?,?,?)',(wid,tid,json.dumps(worker)));self.event(db,tid,'worker-reserved',phase+' worker reserved');return worker
|
|
195
|
+
def worker(self,wid):
|
|
196
|
+
with self.db() as db:
|
|
197
|
+
row=db.execute('SELECT body FROM workers WHERE id=?',(wid,)).fetchone();require(row is not None,'Unknown worker');return json.loads(row[0])
|
|
198
|
+
def workers(self):
|
|
199
|
+
with self.db() as db:return [json.loads(r[0]) for r in db.execute('SELECT body FROM workers')]
|
|
200
|
+
def worker_update(self,wid,changes):
|
|
201
|
+
require(not(set(changes)-{'state','nativeId','turnId','cwd','checkpoint','error','nativeArchived','roleSource','resolution','nativeRetirement','nativePlan'}),'Unsupported worker transition')
|
|
202
|
+
with self.db() as db:
|
|
203
|
+
row=db.execute('SELECT body FROM workers WHERE id=?',(wid,)).fetchone();require(row is not None,'Unknown worker');worker=json.loads(row[0]);worker.update(copy.deepcopy(changes));db.execute('UPDATE workers SET body=? WHERE id=?',(json.dumps(worker),wid));return worker
|
|
204
|
+
def progress(self,wid,item_id,summary):
|
|
205
|
+
require(text(item_id,200) and text(summary,1600),'Invalid worker progress')
|
|
206
|
+
with self.db() as db:
|
|
207
|
+
row=db.execute('SELECT body FROM workers WHERE id=?',(wid,)).fetchone();require(row is not None,'Unknown worker')
|
|
208
|
+
worker=json.loads(row[0])
|
|
209
|
+
if worker['state']!='running' or item_id in worker.get('progressItems',[]):
|
|
210
|
+
return False
|
|
211
|
+
task=json.loads(db.execute('SELECT body FROM tasks WHERE id=?',(worker['taskId'],)).fetchone()[0])
|
|
212
|
+
if task['state'] in TERMINAL:
|
|
213
|
+
return False
|
|
214
|
+
worker.setdefault('progressItems',[]).append(item_id)
|
|
215
|
+
task.update(summary='Reported progress: '+summary,revision=task['revision']+1,updatedAt=time.time())
|
|
216
|
+
db.execute('UPDATE workers SET body=? WHERE id=?',(json.dumps(worker),wid))
|
|
217
|
+
db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(task),task['id']))
|
|
218
|
+
self.event(db,task['id'],'worker-progress',task['summary'])
|
|
219
|
+
return True
|
|
220
|
+
def adopt_ready(self,tid,revision,installation):
|
|
221
|
+
require(text(installation,200),'Installation identity required')
|
|
222
|
+
with self.db() as db:
|
|
223
|
+
row=db.execute('SELECT body FROM tasks WHERE id=?',(tid,)).fetchone();require(row is not None,'Unknown task');task=json.loads(row[0])
|
|
224
|
+
require(task['revision']==revision,'Task changed; reread before checkpoint adoption')
|
|
225
|
+
require(task.get('resumeAfterCheckpoint') is True and task['state'] in {'queued','executing','reviewing'},'Task is held, uncertain or has no checkpoint-resumption authority')
|
|
226
|
+
workers=[json.loads(w[0]) for w in db.execute('SELECT body FROM workers WHERE task_id=?',(tid,))]
|
|
227
|
+
require(all(w['state']=='archived' and w.get('nativeArchived') is True or w['state']=='failed' and not w.get('turnId') and (not w.get('nativeId') or w.get('nativeArchived') is True) for w in workers),'Existing worker closure is unresolved; preserve its owner')
|
|
228
|
+
phase={'queued':'route','executing':'execute','reviewing':'review'}[task['state']]
|
|
229
|
+
require(not any(w['phase']==phase and w.get('attempt',1)==task.get('attempt',1) and (phase!='review' or w.get('reviewGeneration',0)==task.get('reviewGeneration',0)) for w in workers),'Existing phase result needs reconciliation, not replay')
|
|
230
|
+
if task.get('ownerInstallationId')==installation:
|
|
231
|
+
return task
|
|
232
|
+
previous=task.get('ownerInstallationId');require(isinstance(previous,str) and previous,'Previous installation ownership is missing')
|
|
233
|
+
task.update(ownerInstallationId=installation,revision=revision+1,updatedAt=time.time(),summary='Verified closed checkpoint adopted; only the pending phase may execute.')
|
|
234
|
+
task.setdefault('ownershipCheckpoints',[]).append({'from':previous,'to':installation,'at':task['updatedAt'],'pendingPhase':phase})
|
|
235
|
+
db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(task),tid));self.event(db,tid,'checkpoint-owner-adopted',task['summary']);return task
|
|
236
|
+
def portable_delta(self,tid,revision,source_digest,workers,changes,operation):
|
|
237
|
+
require(set(changes)=={'contextRef','result','verification'} and isinstance(workers,dict) and isinstance(operation,str) and len(operation)==64,'Exact portable delta transition required')
|
|
238
|
+
with self.db() as db:
|
|
239
|
+
row=db.execute('SELECT body FROM tasks WHERE id=?',(tid,)).fetchone();require(row is not None,'Unknown task');task=json.loads(row[0])
|
|
240
|
+
current=[json.loads(w[0]) for w in db.execute('SELECT body FROM workers WHERE task_id=? ORDER BY id',(tid,))]
|
|
241
|
+
require(task['state'] in TERMINAL and set(workers)=={w['id'] for w in current} and all(w['state']=='archived' and w.get('nativeArchived') is True for w in current),'Only verified archived terminal task checkpoints may be converted')
|
|
242
|
+
migration={'operationId':operation,'sourceDigest':source_digest,'nativeIdentities':{w['id']:{k:w.get(k) for k in ['nativeId','turnId','phase']} for w in current}}
|
|
243
|
+
if task.get('portableMigration')==migration:
|
|
244
|
+
require(all(task.get(k)==v for k,v in changes.items()) and all(w['checkpoint']==workers[w['id']] for w in current),'Applied portable delta differs');return task
|
|
245
|
+
require(task['revision']==revision and hashlib.sha256(canonical({'task':task,'workers':current})).hexdigest()==source_digest,'Portable delta source changed; no records overwritten')
|
|
246
|
+
require(isinstance(changes['contextRef'].get('artifact'),dict) and all(isinstance(p.get('artifact'),dict) for p in workers.values()),'Verified cloud references are required')
|
|
247
|
+
for worker in current:
|
|
248
|
+
worker['historicalCheckpoint']=worker['checkpoint'];worker['checkpoint']=copy.deepcopy(workers[worker['id']]);db.execute('UPDATE workers SET body=? WHERE id=?',(json.dumps(worker),worker['id']))
|
|
249
|
+
task.update(copy.deepcopy(changes),portableMigration=migration,revision=revision+1,updatedAt=time.time())
|
|
250
|
+
db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(task),tid));self.event(db,tid,'portable-delta-bound','Existing closed task checkpoints are verified in cloud storage; no work was replayed.');return task
|
|
251
|
+
def retained_import(self,claims,source):
|
|
252
|
+
with self.db() as db:
|
|
253
|
+
for index,claim in enumerate(claims):
|
|
254
|
+
identity=claim.get('claimId') or 'legacy:'+str(index)+':'+str(claim.get('taskId'))
|
|
255
|
+
value={k:claim.get(k) for k in ['claimId','taskId','ownerRole','ownerThreadId','state','worktree','branch']};value.update(source=source,pointer='/claims/'+str(index),disposition='retained_original_owner_no_dispatch')
|
|
256
|
+
db.execute('INSERT INTO retained VALUES (?,?) ON CONFLICT(id) DO UPDATE SET body=excluded.body',(identity,json.dumps(value)))
|
|
257
|
+
return db.execute('SELECT COUNT(*) FROM retained').fetchone()[0]
|
|
258
|
+
def retained(self):
|
|
259
|
+
with self.db() as db:return [json.loads(r[0]) for r in db.execute('SELECT body FROM retained')]
|
|
260
|
+
def retained_page(self,params):
|
|
261
|
+
require(isinstance(params,dict) and not set(params)-{'cursor','limit'},'Unsupported retained inventory fields')
|
|
262
|
+
cursor=params.get('cursor',0);limit=params.get('limit',50)
|
|
263
|
+
require(type(cursor) is int and cursor>=0 and type(limit) is int and 1<=limit<=100,'Invalid retained inventory page')
|
|
264
|
+
with self.db() as db:
|
|
265
|
+
total=db.execute('SELECT COUNT(*) FROM retained').fetchone()[0]
|
|
266
|
+
rows=db.execute('SELECT rowid,body FROM retained WHERE rowid>? ORDER BY rowid LIMIT ?',(cursor,limit+1)).fetchall()
|
|
267
|
+
chosen=[];size=0;last=cursor
|
|
268
|
+
for rowid,body in rows[:limit]:
|
|
269
|
+
value=json.loads(body);count=len(canonical(value))
|
|
270
|
+
if count>800000:
|
|
271
|
+
value={'source':value.get('source'),'pointer':value.get('pointer'),'oversizedRecord':True,'bytes':count,'disposition':'retained_original_owner_no_dispatch'};count=len(canonical(value))
|
|
272
|
+
if size+count>900000 and chosen:break
|
|
273
|
+
chosen.append(value);size+=count;last=rowid
|
|
274
|
+
more=bool(rows and (len(rows)>len(chosen)))
|
|
275
|
+
return {'claims':chosen,'total':total,'nextCursor':last if more else None,'disposition':'inventory_only_no_work_resumed'}
|
|
276
|
+
|
|
277
|
+
def events(self,tid=None):
|
|
278
|
+
with self.db() as db:
|
|
279
|
+
rows=db.execute('SELECT sequence,body FROM events'+(' WHERE task_id=?' if tid else '')+' ORDER BY sequence', (tid,) if tid else ())
|
|
280
|
+
return [{'sequence':r[0],**json.loads(r[1])} for r in rows]
|
|
281
|
+
def recover(self):
|
|
282
|
+
# Native effects may have happened between a send and its receipt. Keep
|
|
283
|
+
# custody and prohibit replay; recovery never starts a thread or turn.
|
|
284
|
+
for worker in self.workers():
|
|
285
|
+
if worker['state'] not in {'archived','failed','unknown'}:
|
|
286
|
+
self.worker_update(worker['id'],{'state':'unknown','error':'Owning connection ended; reconcile native receipt before any replay.'})
|
|
287
|
+
task=self.get(worker['taskId'])
|
|
288
|
+
self.update(task['id'],task['revision'],{'state':'unknown','summary':'Native delivery requires reconciliation; no retry was started.'},'recovery-required')
|
|
289
|
+
|
|
290
|
+
def respond(self,request):
|
|
291
|
+
require(isinstance(request,dict) and set(request)=={'requestId','taskId','revision','intent','note'},'Exact task response required')
|
|
292
|
+
require(text(request['requestId'],100) and text(request['note'],8000) and request['intent'] in {'continue','request_changes','hold'},'Invalid task response')
|
|
293
|
+
fingerprint=hashlib.sha256(canonical(request)).hexdigest()
|
|
294
|
+
with self.db() as db:
|
|
295
|
+
previous=db.execute('SELECT input_hash,task_id FROM responses WHERE id=?',(request['requestId'],)).fetchone()
|
|
296
|
+
if previous:
|
|
297
|
+
require(previous==(fingerprint,request['taskId']),'Response identity already used')
|
|
298
|
+
return json.loads(db.execute('SELECT body FROM tasks WHERE id=?',(request['taskId'],)).fetchone()[0]),False
|
|
299
|
+
row=db.execute('SELECT body FROM tasks WHERE id=?',(request['taskId'],)).fetchone();require(row is not None,'Unknown task');task=json.loads(row[0])
|
|
300
|
+
require(task['revision']==request['revision'],'Task changed; review current progress before responding')
|
|
301
|
+
require(task['state'] in TERMINAL and task['state']!='unknown','Task is active or delivery is uncertain; reconcile before a new attempt')
|
|
302
|
+
workers=[json.loads(w[0]) for w in db.execute('SELECT body FROM workers WHERE task_id=?',(task['id'],))]
|
|
303
|
+
require(all(w['state']=='archived' or (w['state']=='failed' and not w.get('turnId') and (not w.get('nativeId') or w.get('nativeArchived') is True)) for w in workers),'Native worker closure is unresolved; no new attempt')
|
|
304
|
+
task.setdefault('humanResponses',[]).append(copy.deepcopy(request));task['revision']+=1;task['updatedAt']=time.time()
|
|
305
|
+
if request['intent']=='hold':task.update(state='blocked',summary='Held by your request: '+request['note'])
|
|
306
|
+
else:task.update(state='queued',phase='route',attempt=task.get('attempt',1)+1,objective=task['objective']+'\n\nCurrent human response: '+request['note'],summary='Your response is recorded; a fresh specialist attempt will be routed.')
|
|
307
|
+
db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(task),task['id']));db.execute('INSERT INTO responses VALUES (?,?,?)',(request['requestId'],fingerprint,task['id']));self.event(db,task['id'],'human-response',task['summary']);return task,True
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def parse_route(text_value,roles):
|
|
311
|
+
raw=text_value.strip()
|
|
312
|
+
if raw.startswith('```'):raw=re.sub(r'^```(?:json)?\s*|\s*```$','',raw)
|
|
313
|
+
try:value=json.loads(raw)
|
|
314
|
+
except (TypeError,ValueError):raise PackError('Coordinator did not return a valid route; no worker started')
|
|
315
|
+
require(isinstance(value,dict) and set(value)=={'role','task','acceptance'},'Coordinator route has unsupported fields')
|
|
316
|
+
require(value['role'] in roles,'Coordinator selected an unknown discipline')
|
|
317
|
+
if isinstance(value['acceptance'],list):
|
|
318
|
+
require(1<=len(value['acceptance'])<=30 and all(text(v,4000) for v in value['acceptance']),'Coordinator acceptance criteria are invalid')
|
|
319
|
+
value['acceptance']='\n'.join(value['acceptance'])
|
|
320
|
+
require(text(value['acceptance'],12000),'Coordinator returned no bounded acceptance criteria')
|
|
321
|
+
require(isinstance(value['task'],dict) and set(value['task'])=={'type','complexity','risk'},'Coordinator must classify type, complexity and risk')
|
|
322
|
+
return value
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Human-authorized diagnostic continuation of one immutable cloud claim.
|
|
2
|
+
|
|
3
|
+
Never reconstruct authority from a chat, local source path or worker assertion.
|
|
4
|
+
The original failed jobs, source custody and uncertain effects remain held.
|
|
5
|
+
"""
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import pathlib
|
|
9
|
+
import tempfile
|
|
10
|
+
import uuid
|
|
11
|
+
from .storage import canonical
|
|
12
|
+
from .work import require, text
|
|
13
|
+
|
|
14
|
+
FIELDS={'requestId','objective','channel','canonicalClaimId','continuationOwnerTaskId',
|
|
15
|
+
'role','acceptance','checkpointSHA256','checkpointRef','authorizationRef','continuationRevision','parentCheckpointSHA256'}
|
|
16
|
+
SCOPE='read-only-checkpoint-diagnostic'
|
|
17
|
+
|
|
18
|
+
def digest(value):
|
|
19
|
+
return hashlib.sha256(canonical(value)).hexdigest()
|
|
20
|
+
|
|
21
|
+
def request_digest(request):
|
|
22
|
+
return digest(request)
|
|
23
|
+
|
|
24
|
+
def validate(request):
|
|
25
|
+
require(isinstance(request,dict) and set(request)==FIELDS,'Exact retained adoption fields required')
|
|
26
|
+
for key in ['requestId','continuationOwnerTaskId']:
|
|
27
|
+
try:valid=str(uuid.UUID(request[key]))==request[key]
|
|
28
|
+
except (TypeError,ValueError,AttributeError):valid=False
|
|
29
|
+
require(valid,'Adoption requires exact task identities')
|
|
30
|
+
require(type(request['continuationRevision']) is int and request['continuationRevision']>0,'Exact parent revision required')
|
|
31
|
+
require(isinstance(request['parentCheckpointSHA256'],str) and len(request['parentCheckpointSHA256'])==64 and all(c in '0123456789abcdef' for c in request['parentCheckpointSHA256']),'Exact parent checkpoint heads required')
|
|
32
|
+
require(request['requestId']!=request['continuationOwnerTaskId'],'Continuation must retain its originating delivery task')
|
|
33
|
+
require(request['channel']=='engineering','Retained engineering channel required')
|
|
34
|
+
require(all(text(request[k],12000 if k=='acceptance' else 16000 if k=='objective' else 240)
|
|
35
|
+
for k in ['objective','canonicalClaimId','role','acceptance']),'Adoption requires objective, claim, registered role and string acceptance')
|
|
36
|
+
require(isinstance(request['checkpointSHA256'],str) and len(request['checkpointSHA256'])==64
|
|
37
|
+
and all(c in '0123456789abcdef' for c in request['checkpointSHA256']),'Exact canonical checkpoint digest required')
|
|
38
|
+
for key in ['checkpointRef','authorizationRef']:
|
|
39
|
+
ref=request[key]
|
|
40
|
+
require(isinstance(ref,dict) and set(ref)=={'appId','workspaceId','scope','key','etag','size','sha256'}
|
|
41
|
+
and ref['appId']=='app_topolo_bytes' and ref['scope']=='org'
|
|
42
|
+
and all(text(ref[k],1000) for k in ['workspaceId','key','etag','sha256'])
|
|
43
|
+
and type(ref['size']) is int and 0<ref['size']<=131072,'Exact bounded cloud evidence reference required')
|
|
44
|
+
|
|
45
|
+
def read_evidence(store,ref):
|
|
46
|
+
from .topolo import read_artifact
|
|
47
|
+
require(getattr(store,'cloud',False),'Retained continuation requires activated cloud authority; no local history fallback')
|
|
48
|
+
authority=json.loads((store.root/'context-authority.json').read_text())
|
|
49
|
+
require(authority.get('organizationId')==store.binding['organizationId']
|
|
50
|
+
and authority.get('scope')==ref['scope'] and authority.get('workspaceId')==ref['workspaceId'],
|
|
51
|
+
'Selected claim evidence belongs to another cloud authority')
|
|
52
|
+
with tempfile.TemporaryDirectory(prefix='topolo-selected-claim-') as tmp:
|
|
53
|
+
path=pathlib.Path(tmp)/'selected.json'
|
|
54
|
+
read_artifact(store.client,{'input':{'bytesScope':ref['scope'],'bytesWorkspaceId':ref['workspaceId']}},ref['key'],ref,path)
|
|
55
|
+
return json.loads(path.read_text())
|
|
56
|
+
|
|
57
|
+
def plan(request,retained,claim,roles):
|
|
58
|
+
validate(request)
|
|
59
|
+
require(request['role'] in roles,'Exact registered specialist role required')
|
|
60
|
+
require(retained['claimId']==request['canonicalClaimId']==claim.get('claimId'),'Claim identity differs')
|
|
61
|
+
require(text(retained.get('ownerThreadId'),200) and retained['ownerThreadId']==claim.get('ownerThreadId'),'Recorded original controller differs')
|
|
62
|
+
require(digest(claim)==request['checkpointSHA256'],'Canonical checkpoint changed')
|
|
63
|
+
require(any(word in str(claim.get('state','')).lower() for word in ['failed','blocked','held','paused','unknown']),
|
|
64
|
+
'Recorded claim is active or unqualified; preserve current custody')
|
|
65
|
+
require(not any(claim.get(key) for key in ['lease','activeLease','ownerLease','leaseId','ownerLeaseId','leaseExpiresAt','ownerLeaseExpiresAt','activeWorkerId','activeRunId']),
|
|
66
|
+
'Canonical claim has a live or unresolved owner lease')
|
|
67
|
+
return {'format':'topolo-retained-continuation-plan-v2','canonicalClaimId':request['canonicalClaimId'],
|
|
68
|
+
'checkpointSHA256':digest(claim),'retainedSHA256':digest(retained),
|
|
69
|
+
'fromOwnerThreadId':claim['ownerThreadId'],'toRole':request['role'],
|
|
70
|
+
'continuationOwnerTaskId':request['continuationOwnerTaskId'],'acceptance':request['acceptance'],
|
|
71
|
+
'requestSHA256':request_digest(request),'canonicalSource':request['checkpointRef'],
|
|
72
|
+
'authorizationRef':request['authorizationRef'],'checkpoint':claim,'scope':SCOPE,
|
|
73
|
+
'preserveUnknownEffects':True,'sourceOwnerRetained':True,'releaseGatesRetained':True}
|
|
74
|
+
|
|
75
|
+
def verify_authority(request,prepared,authorization,actor,consent,installation):
|
|
76
|
+
consent=consent or {}
|
|
77
|
+
required={'format','principalId','organizationId','canonicalClaimId','checkpointSHA256','sourceHead',
|
|
78
|
+
'continuationOwnerTaskId','continuationRevision','parentCheckpointSHA256','scope','instruction','instructionSHA256'}
|
|
79
|
+
require(isinstance(authorization,dict) and set(authorization)==required
|
|
80
|
+
and authorization['format']=='topolo-human-retained-continuation-v1'
|
|
81
|
+
and authorization['scope']==prepared['scope']==SCOPE,'Exact human diagnostic authorization required')
|
|
82
|
+
require(actor.get('user',{}).get('role')=='owner' and actor.get('user',{}).get('id')==authorization['principalId']==consent.get('principalId')
|
|
83
|
+
and actor.get('organization',{}).get('id')==authorization['organizationId']=='org_topolo_platform'
|
|
84
|
+
and consent.get('approved') is True and consent.get('contextKey')=='org:org_topolo_platform',
|
|
85
|
+
'Current authenticated owner and installation consent differ')
|
|
86
|
+
require(all(authorization[k]==request[k] for k in ['canonicalClaimId','checkpointSHA256','continuationOwnerTaskId','continuationRevision','parentCheckpointSHA256'])
|
|
87
|
+
and text(authorization['instruction']) and hashlib.sha256(authorization['instruction'].encode()).hexdigest()==authorization['instructionSHA256'],
|
|
88
|
+
'Human instruction does not bind this exact claim continuation')
|
|
89
|
+
head=prepared['checkpoint'].get('head') or prepared['checkpoint'].get('sourceCommit')
|
|
90
|
+
require(isinstance(head,str) and len(head)==40 and all(c in '0123456789abcdef' for c in head)
|
|
91
|
+
and authorization['sourceHead']==head,'Exact source head differs from human authorization')
|
|
92
|
+
return {'authority':'current-authenticated-human-instruction','principalId':authorization['principalId'],
|
|
93
|
+
'organizationId':authorization['organizationId'],'installationId':installation,
|
|
94
|
+
'authorizationSHA256':digest(authorization),'consentSHA256':digest(consent),
|
|
95
|
+
'planSHA256':digest(prepared),'scope':SCOPE,'originalWorkerAcceptance':False}
|
|
96
|
+
|
|
97
|
+
def parent_heads(parent,workers):
|
|
98
|
+
return {'result':parent.get('result',{}).get('checkpoint'),
|
|
99
|
+
'workers':[{'id':w['id'],'phase':w['phase'],'checkpoint':w.get('checkpoint')} for w in sorted(workers,key=lambda w:w['id'])]}
|
|
100
|
+
|
|
101
|
+
def verify_parent(parent,workers,request,root,client,installation):
|
|
102
|
+
"""Only closed portable checkpoints permit a foreign installation transfer."""
|
|
103
|
+
from .work_artifacts import read_checkpoint
|
|
104
|
+
require(parent['state'] in {'blocked','needs_input'} and parent['revision']==request['continuationRevision'],
|
|
105
|
+
'Parent is active, uncertain or revision changed; preserve custody')
|
|
106
|
+
require(workers and digest(parent_heads(parent,workers))==request['parentCheckpointSHA256'],
|
|
107
|
+
'Parent cloud checkpoint heads differ')
|
|
108
|
+
for worker in workers:
|
|
109
|
+
require(worker['state']=='archived' and worker.get('nativeArchived') is True
|
|
110
|
+
and text(worker.get('nativeId'),200) and text(worker.get('turnId'),200)
|
|
111
|
+
and isinstance(worker.get('checkpoint',{}).get('artifact'),dict),
|
|
112
|
+
'Parent native worker has unresolved or nonportable custody')
|
|
113
|
+
payload=read_checkpoint(root,worker['checkpoint'],parent['id'],worker['id'],client=client)
|
|
114
|
+
native=payload['nativeReceipt']
|
|
115
|
+
require(payload['phase']==worker['phase'] and native['threadId']==worker['nativeId']
|
|
116
|
+
and native['turnId']==worker['turnId'] and native['status'] in {'completed','failed','cancelled'},
|
|
117
|
+
'Parent terminal checkpoint differs')
|
|
118
|
+
require(parent.get('result',{}).get('checkpoint') in [w['checkpoint'] for w in workers],
|
|
119
|
+
'Parent result does not bind a verified terminal checkpoint')
|
|
120
|
+
return {'fromInstallationId':parent.get('ownerInstallationId'),'toInstallationId':installation,
|
|
121
|
+
'revision':parent['revision'],'checkpointHeadsSHA256':request['parentCheckpointSHA256'],
|
|
122
|
+
'scope':SCOPE,'taskStatePreserved':parent['state']}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Portable task results and declared evidence; never exports native reasoning/history."""
|
|
2
|
+
import hashlib,json,pathlib,tempfile,mimetypes
|
|
3
|
+
from .storage import atomic_json,read_json,canonical,PackError,safe_id
|
|
4
|
+
from .work import require
|
|
5
|
+
|
|
6
|
+
def client_default():
|
|
7
|
+
from .topolo import Client
|
|
8
|
+
return Client('production','org:org_topolo_platform')
|
|
9
|
+
|
|
10
|
+
def publish(root,task_id,label,data,content_type='application/json',client=None):
|
|
11
|
+
safe_id(task_id)
|
|
12
|
+
root=pathlib.Path(root);authority=read_json(root/'context-authority.json',{})
|
|
13
|
+
require(authority.get('format')=='topolo-task-context-authority-v1' and authority.get('organizationId')=='org_topolo_platform' and authority.get('scope')=='org' and authority.get('workspaceId'),'Cloud artifact authority is missing')
|
|
14
|
+
require(isinstance(data,bytes) and 0<len(data)<=100*1024*1024 and all(c.isalnum() or c in '-_.' for c in label),'Invalid declared artifact')
|
|
15
|
+
digest=hashlib.sha256(data).hexdigest();directory=root/'artifact-uploads'/task_id;directory.mkdir(parents=True,exist_ok=True,mode=0o700)
|
|
16
|
+
name=label+'-'+digest[:16];file=directory/name;receipt=directory/(name+'.receipt.json');prior=read_json(receipt,{})
|
|
17
|
+
client=client or client_default()
|
|
18
|
+
if prior:
|
|
19
|
+
require(prior.get('sha256')==digest and prior.get('state') in {'uploaded','verified'},'Artifact upload is uncertain; reconcile before retrying')
|
|
20
|
+
key=prior['key']
|
|
21
|
+
else:
|
|
22
|
+
require(not file.exists(),'Artifact source exists without an upload receipt; reconcile first')
|
|
23
|
+
file.write_bytes(data);file.chmod(0o600);atomic_json(receipt,{'state':'prepared','sha256':digest})
|
|
24
|
+
result=client.action('app_topolo_bytes.files.upload_url.create',{'key':'task-results/'+task_id+'/'+name,'type':content_type,'size':len(data),'scope':authority['scope']},authority['workspaceId'],write=True,upload=file)
|
|
25
|
+
key=result.get('key') or result.get('session',{}).get('key');require(isinstance(key,str) and key,'Artifact upload key is unconfirmed; reconcile before retrying')
|
|
26
|
+
atomic_json(receipt,{'state':'uploaded','sha256':digest,'key':key})
|
|
27
|
+
from .topolo import read_artifact
|
|
28
|
+
ref=read_artifact(client,{'input':{'bytesScope':authority['scope'],'bytesWorkspaceId':authority['workspaceId']}},key)
|
|
29
|
+
require(ref['sha256']==digest,'Cloud artifact differs from declared output')
|
|
30
|
+
atomic_json(receipt,{'state':'verified','sha256':digest,'key':key,'artifact':ref});return ref
|
|
31
|
+
|
|
32
|
+
def declared_files(worker,values):
|
|
33
|
+
require(isinstance(values,list) and len(values)<=16,'Declare at most sixteen evidence files')
|
|
34
|
+
cwd=pathlib.Path(worker['cwd']).resolve();paths=[]
|
|
35
|
+
for value in values:
|
|
36
|
+
require(isinstance(value,str),'Evidence path must be a string')
|
|
37
|
+
path=pathlib.Path(value)
|
|
38
|
+
require(path.is_absolute() and '..' not in path.parts and path.is_file() and not path.is_symlink() and path.resolve().is_relative_to(cwd) and not any(p.is_symlink() for p in path.parents if p!=cwd.parent),'Evidence must be a regular file in this worker workspace')
|
|
39
|
+
require(0<path.stat().st_size<=100*1024*1024,'Declared evidence exceeds upload boundary');paths.append(path)
|
|
40
|
+
return paths
|
|
41
|
+
|
|
42
|
+
def checkpoint(root,task,worker,actual,client=None):
|
|
43
|
+
text='\n'.join(i.get('text','') for i in actual.get('items',[]) if i.get('type')=='agentMessage' and i.get('phase')!='commentary')
|
|
44
|
+
require(len(text.encode())<=100000,'Worker output exceeds bounded checkpoint')
|
|
45
|
+
normalized=None;error=None
|
|
46
|
+
if worker['phase'] in {'execute','review'} and actual['status']=='completed':
|
|
47
|
+
try:
|
|
48
|
+
raw=text.strip()
|
|
49
|
+
if raw.startswith('```'):raw=raw.split('\n',1)[1].rsplit('```',1)[0]
|
|
50
|
+
output=json.loads(raw)
|
|
51
|
+
if worker['phase']=='execute':
|
|
52
|
+
require(set(output)=={'summary','artifacts','references'} and isinstance(output['summary'],str) and isinstance(output['references'],list) and len(output['references'])<=32 and all(isinstance(r,str) and len(r)<=2000 for r in output['references']),'Invalid portable worker result contract')
|
|
53
|
+
paths=declared_files(worker,output['artifacts'])
|
|
54
|
+
else:
|
|
55
|
+
require(set(output)=={'verdict','summary','evidence'} and output['verdict'] in {'completed','needs_input','blocked'} and isinstance(output['summary'],str),'Invalid independent review contract')
|
|
56
|
+
paths=declared_files(worker,output['evidence']);require(output['verdict']!='completed' or bool(paths),'Completion requires inspected evidence')
|
|
57
|
+
except (PackError,ValueError,TypeError,KeyError) as exc:error=str(exc)
|
|
58
|
+
else:
|
|
59
|
+
refs=[{'title':path.name,'artifact':publish(root,task['id'],worker['id']+'-evidence-'+str(n),path.read_bytes(),mimetypes.guess_type(path.name)[0] or 'application/octet-stream',client)} for n,path in enumerate(paths)]
|
|
60
|
+
normalized={**output,('artifacts' if worker['phase']=='execute' else 'evidence'):refs}
|
|
61
|
+
payload={'format':'topolo-task-checkpoint-v1','taskId':task['id'],'workerId':worker['id'],'phase':worker['phase'],'nativeReceipt':{'threadId':worker['nativeId'],'turnId':actual['id'],'status':actual['status'],'toolItems':sum(i.get('type') not in {'agentMessage','userMessage','reasoning','plan'} for i in actual.get('items',[]))},'resolution':worker['resolution'],'roleSource':worker.get('roleSource'),'contextRef':task.get('contextRef'),'output':canonical(normalized).decode() if normalized is not None else text,'outputError':error,'nativeHistoryIncluded':False}
|
|
62
|
+
if actual.get('nativeProof'):
|
|
63
|
+
payload['nativeReceipt']['producer']=actual['nativeProof']
|
|
64
|
+
data=canonical(payload);ref=publish(root,task['id'],worker['id']+'-checkpoint',data,client=client)
|
|
65
|
+
return {'artifact':ref,'sha256':ref['sha256']},payload,normalized
|
|
66
|
+
|
|
67
|
+
def read_checkpoint(root,proof,task_id,worker_id=None,client=None):
|
|
68
|
+
from .topolo import read_artifact
|
|
69
|
+
ref=proof['artifact'];require(proof['sha256']==ref['sha256'] and 0<ref['size']<=200000,'Checkpoint digest or size binding differs');client=client or client_default()
|
|
70
|
+
with tempfile.TemporaryDirectory(prefix='topolo-one-checkpoint-') as tmp:
|
|
71
|
+
file=pathlib.Path(tmp)/'checkpoint.json';read_artifact(client,{'input':{'bytesScope':ref['scope'],'bytesWorkspaceId':ref['workspaceId']}},ref['key'],ref,file);payload=json.loads(file.read_text())
|
|
72
|
+
require(set(payload)=={'format','taskId','workerId','phase','nativeReceipt','resolution','roleSource','contextRef','output','outputError','nativeHistoryIncluded'} and payload.get('format')=='topolo-task-checkpoint-v1' and payload.get('taskId')==task_id and (worker_id is None or payload.get('workerId')==worker_id) and payload.get('nativeHistoryIncluded') is False,'Cloud checkpoint identity differs')
|
|
73
|
+
return payload
|
|
74
|
+
|
|
75
|
+
def materialize_result(root,task,cwd,client=None):
|
|
76
|
+
from .topolo import read_artifact
|
|
77
|
+
result=task.get('result',{});payload=read_checkpoint(root,result['checkpoint'],task['id'],result['workerId'],client)
|
|
78
|
+
require(payload['phase']=='execute' and payload['outputError'] is None,'Worker checkpoint is not a qualified result')
|
|
79
|
+
output=json.loads(payload['output']);require(output=={k:result[k] for k in ['summary','artifacts','references']},'Task result differs from the immutable worker checkpoint')
|
|
80
|
+
target=pathlib.Path(cwd)/'context';target.mkdir(exist_ok=True);atomic_json(target/'worker-result.json',payload)
|
|
81
|
+
evidence=target/'evidence';evidence.mkdir(exist_ok=True);mapping=[];client=client or client_default()
|
|
82
|
+
for n,row in enumerate(result.get('artifacts',[])):
|
|
83
|
+
ref=row['artifact'];path=evidence/str(n);read_artifact(client,{'input':{'bytesScope':ref['scope'],'bytesWorkspaceId':ref['workspaceId']}},ref['key'],ref,path);mapping.append({'title':row['title'],'path':str(path),'artifact':ref})
|
|
84
|
+
atomic_json(target/'result-source.json',{'checkpoint':result['checkpoint'],'evidence':mapping})
|