convos-remote 0.8.1__py3-none-any.whl

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.
@@ -0,0 +1,522 @@
1
+ """Client-side enrollment, E2EE keyring, automatic sync, membership, and local queries."""
2
+ import fcntl, json, os, sqlite3, time, traceback, urllib.error, urllib.parse, urllib.request
3
+ from contextlib import contextmanager
4
+ from functools import wraps
5
+ from pathlib import Path; from typing import Optional
6
+
7
+ import typer
8
+ from ai_convos_redact import protect_all
9
+ _pending=[]
10
+ def register(app): _pending.append(app) if "remote" not in globals() else app.add_typer(remote,name="remote")
11
+ from ai_convos.cli import PROJECT_ROOT, archive_changes as core_archive_changes, archive_state as core_archive_state, drain_hooks, durable_replace, init_schema, install_hooks, open_db, project_attachment_body, project_workspace_controls
12
+ from .control import CONTROL_V, approved, electorate, proposal as device_proposal, record as control_record, sign as control_sign, state_hash, verify_proposal, verify_state, vote as device_vote
13
+ from .projection import SIGNED, TABLES, apply_row_replicas, attest_rows, blob_replicas, bridge_replicas, bridge_stamp, connect, control_chain, cutover_state, event_support, inspect_state, project, project_many, read_state, relocate_attachments, reset_history, row_replicas, scan, sequence, stored_controls, verify_history
14
+ from .protocol import (b64, certificate, digest, event, fingerprint, identity, open_blob, open_event, open_key, open_origin, open_replica, public, public_id, recover,
15
+ recovery_bundle, registration_proof, seal_event, seal_key, seal_origin, seal_replica, sign_control, signer, unb64, verify_certificate)
16
+ from .service import edit_hooks, enable
17
+
18
+ remote=typer.Typer(help="End-to-end encrypted personal and team synchronization")
19
+ def local_root(root=None): return Path(root or os.environ.get("CONVOS_PROJECT_ROOT",PROJECT_ROOT))
20
+ def paths(root=None):
21
+ base=local_root(root)/"remote"; return base,base/"config.json",base/"state.db"
22
+ def core_path(root=None): return local_root(root)/"data"/"convos.db"
23
+ def archive_info(root=None,create=False):
24
+ path=core_path(root)
25
+ if not path.exists() and not create: return None
26
+ path.parent.mkdir(parents=True,exist_ok=True); db=open_db(path); init_schema(db)
27
+ try: return core_archive_state(db)
28
+ finally: db.close()
29
+ def load(root=None):
30
+ _,path,_=paths(root)
31
+ if not path.exists(): raise ValueError("Remote is not configured. Run `convos remote setup`.")
32
+ return json.loads(path.read_text())
33
+ def save(cfg,root=None):
34
+ base,path,_=paths(root); base.mkdir(parents=True,exist_ok=True); os.chmod(base,0o700); tmp=path.with_name(f".{path.name}.{os.getpid()}"); tmp.write_text(json.dumps(cfg)); os.chmod(tmp,0o600); durable_replace(tmp,path)
35
+ def rescue_bindings(cfg,state_path,root=None):
36
+ db=sqlite3.connect(Path(state_path).resolve().as_uri()+"?mode=ro",uri=True)
37
+ try: columns={r[1] for r in db.execute("PRAGMA table_info(policies)").fetchall()}; rows=db.execute("SELECT workspace,value,local_root FROM policies WHERE kind='path' AND local_root IS NOT NULL").fetchall() if "local_root" in columns else []
38
+ finally: db.close()
39
+ for ws,value,path in rows:
40
+ if not isinstance(path,str) or not Path(path).is_absolute(): raise ValueError("legacy path binding is invalid")
41
+ cfg.setdefault("bindings",{})[f"{ws}:{value}"]=path
42
+ if rows: save(cfg,root)
43
+ return len(rows)
44
+ def encrypted_file(root,event,value):
45
+ path=paths(root)[0]/"outbox"/f"{event}.json"; path.parent.mkdir(parents=True,exist_ok=True)
46
+ if path.parent.is_symlink() or path.is_symlink(): raise ValueError("encrypted outbox path must not be a symlink")
47
+ os.chmod(path.parent,0o700); raw=json.dumps(value,separators=(",",":")).encode(); tmp=path.with_name(f".{path.name}.{os.getpid()}"); handle=tmp.open("xb"); os.chmod(tmp,0o600); handle.write(raw); handle.close(); durable_replace(tmp,path); return path,len(raw)
48
+ def receipt(state,ws,value,cursor,epoch):
49
+ status=value["payload"].get("status") if isinstance(value["payload"],dict) and value["payload"].get("status") in ("active","deleted") else None
50
+ state.execute("INSERT OR REPLACE INTO receipts VALUES (?,?,?,?,?,?,?,?,?,?,?)",(ws,value["id"],cursor,value["author"],value["seq"],epoch,value["kind"],value["payload_v"],value["entity"],value["revision"],status))
51
+ def safe_url(url): parsed=urllib.parse.urlparse(url); parsed.scheme=="https" or parsed.hostname in ("127.0.0.1","localhost","::1") or os.environ.get("CONVOS_REMOTE_INSECURE")=="1" or (_ for _ in ()).throw(ValueError("Remote URL must use HTTPS (set CONVOS_REMOTE_INSECURE=1 only on a trusted test network)"))
52
+ def request(cfg,body,auth=True):
53
+ safe_url(cfg["url"])
54
+ headers={"Content-Type":"application/json"};
55
+ if auth: headers["Authorization"]="Bearer "+cfg["token"]
56
+ req=urllib.request.Request(cfg["url"].rstrip("/")+"/v1",data=json.dumps(body).encode(),headers=headers,method="POST")
57
+ try: return json.loads(urllib.request.urlopen(req,timeout=10).read())
58
+ except urllib.error.HTTPError as e: raise ValueError(json.loads(e.read())["error"]) from e
59
+ def health(cfg): safe_url(cfg["url"]); result=json.loads(urllib.request.urlopen(cfg["url"].rstrip("/")+"/v1/health",timeout=3).read()); result.get("version")==1 or (_ for _ in ()).throw(ValueError("relay protocol v1 required")); return result
60
+ @contextmanager
61
+ def sync_lock(root):
62
+ path=paths(root)[0]/"sync.lock"; path.parent.mkdir(parents=True,exist_ok=True); handle=path.open("a+"); os.chmod(path,0o600); fcntl.flock(handle,fcntl.LOCK_EX)
63
+ try: handle.seek(0); handle.truncate(); handle.write(str(os.getpid())); handle.flush(); yield
64
+ finally: fcntl.flock(handle,fcntl.LOCK_UN); handle.close()
65
+ def locked(fn):
66
+ @wraps(fn)
67
+ def call(*args,**kwargs):
68
+ with sync_lock(None): return fn(*args,**kwargs)
69
+ return call
70
+ def workspace(cfg,value):
71
+ hits=[k for k,v in cfg["workspaces"].items() if k.startswith(value) or v["name"]==value]
72
+ if len(hits)!=1: raise ValueError(f"Workspace must match exactly one of: {', '.join(v['name'] for v in cfg['workspaces'].values())}")
73
+ return hits[0]
74
+ def key(cfg,ws,epoch): return unb64(cfg["keys"][f"{ws}:{epoch}"])
75
+ def trusted(devices):
76
+ for d in devices: d["certificate"] or (_ for _ in ()).throw(ValueError("device certificate missing")); body=verify_certificate(json.loads(d["certificate"]),d["root_public"]); signer({d["id"]:d},d["id"]); (public_id(d["root_public"])==d["user_id"] and body["user"]==d["user_id"] and body["device"]==public(d)) or (_ for _ in ()).throw(ValueError("device certificate mismatch"))
77
+ return devices
78
+ def server_record(d,history=True): return control_record(d["user_id"],d["root_public"],d,json.loads(d["certificate"]) if isinstance(d["certificate"],str) else d["certificate"],history)
79
+ def own_record(cfg,history=True): return control_record(cfg["user"],cfg["root"]["sign_public"],cfg["device"],certificate(cfg["root"],cfg["user"],cfg["device"]),history)
80
+ def control_body(cfg,previous,key_,action,members=None,devices=None,removed=None,approval=None,boundary=None):
81
+ advance=action not in ("history","history_activate"); boundary=(boundary or (_ for _ in ()).throw(ValueError("new workspace epoch requires a signed history boundary"))) if advance else previous["boundary"]; return control_sign(cfg["device"],{"v":CONTROL_V,"kind":"workspace.state","workspace":previous["workspace"],"scope":previous["scope"],"revision":previous["revision"]+1,"prev":state_hash(previous),"epoch":previous["epoch"]+advance,"boundary":boundary,"key_commitment":digest(key_),"members":members or previous["members"],"devices":devices or previous["devices"],"removed":removed if removed is not None else previous["removed"],"action":action,"approval":approval,"approved_at":time.time()})
82
+ def access_from(cfg,ws):
83
+ head=cfg["controls"][ws]; device=cfg["device"]["id"]; first=min(value["epoch"] for remote in cfg["server_state"]["workspaces"] if remote["id"]==ws for value in remote["controls"] if device in value["devices"]); return head["members"][cfg["user"]]["history_from"] if head["devices"][device]["history"] else first
84
+ def sequence_heads(state,ws): return {author:{"seq":seq,"event":event} for author,seq,event in state.execute("SELECT s.author,s.seq,s.event FROM event_sequences s JOIN (SELECT author,MAX(seq) seq FROM event_sequences WHERE workspace=? GROUP BY author) h ON h.author=s.author AND h.seq=s.seq WHERE s.workspace=?",(ws,ws)).fetchall()}
85
+ def prepare_archive(cfg,state,root=None,create=True):
86
+ info=archive_info(root); empty=not info or info[1:]==(0,0); cached=[(state.execute("SELECT value FROM meta WHERE key=?",(key,)).fetchone() or [None])[0] for key in ("archive_id","archive_generation")]; proofs=[p for p in (cfg.get("archive"),{"id":cached[0],"generation":int(cached[1])} if all(cached) else None) if p]; existing={r[0].split(":",1)[1]:r[1] for r in state.execute("SELECT key,value FROM meta WHERE key LIKE 'archive_mode:%'").fetchall()}; bases={r[0].split(":",1)[1]:json.loads(r[1]) for r in state.execute("SELECT key,value FROM meta WHERE key LIKE 'archive_basis:%'").fetchall()}; valid=info and all(ws in bases and bases[ws]["id"]==info[0] and info[1]>=bases[ws]["generation"] for ws in existing); fresh=not state.execute("SELECT 1 FROM sync_states LIMIT 1").fetchone(); mode=None
87
+ if existing and not valid: state.execute("DELETE FROM meta WHERE key LIKE 'archive_mode:%' OR key LIKE 'archive_basis:%'"); existing={}
88
+ if existing and not empty: return existing
89
+ if not existing and not proofs: mode="native" if empty else "adopt"
90
+ elif not existing and (not info or any(info[0]!=p["id"] or info[1]<p["generation"] for p in proofs)): mode="native" if empty else "import"
91
+ elif not existing and fresh: mode="adopt"
92
+ if not existing and not mode: return None
93
+ if not info and not create: return existing or mode
94
+ if create and not info: info=archive_info(root,True)
95
+ active={w["id"] for w in cfg["server_state"]["workspaces"] if w["device_authorized"]}
96
+ for ws in active:
97
+ personal=cfg["workspaces"][ws]["kind"]=="personal"; selected=existing.get(ws,"native" if personal else "adopt") if existing else mode if personal else "adopt"; selected="native" if existing and selected=="import" else selected; state.execute("INSERT OR REPLACE INTO meta VALUES (?,?),(?,?)",(f"archive_mode:{ws}",selected,f"archive_basis:{ws}",json.dumps({"id":info[0],"generation":info[1]}))); state.execute("DELETE FROM meta WHERE key=?",(f"boundary:{ws}",)); state.execute("INSERT OR REPLACE INTO sync_states VALUES (?,'rebaselining',COALESCE((SELECT tail FROM sync_states WHERE workspace=?),0),COALESCE((SELECT floor FROM sync_states WHERE workspace=?),0),NULL)",(ws,ws,ws))
98
+ state.commit(); return existing or mode
99
+ def remember_archive(cfg,state,root=None):
100
+ if info:=archive_info(root): cfg["archive"]={"id":info[0],"generation":info[1]}; save(cfg,root); state.execute("INSERT OR REPLACE INTO meta VALUES ('archive_id',?),('archive_generation',?)",(info[0],str(info[1]))); state.execute("DELETE FROM meta WHERE key LIKE 'archive_mode:%' OR key LIKE 'archive_basis:%'")
101
+ def next_boundary(cfg,ws,root=None):
102
+ epoch=cfg["controls"][ws]["epoch"]+1
103
+ if cfg["device"]["id"] in cfg["controls"][ws]["devices"]:
104
+ state=connect(paths(root)[2])
105
+ try: upload(cfg,state,root,{ws}); prepare_archive(cfg,state,root,False); pull(cfg,state,root); local=sequence_heads(state,ws); remote=request(cfg,{"op":"ledger","workspace":ws})
106
+ finally: state.close()
107
+ if local!=remote["heads"]: raise ValueError("relay ledger changed while creating history boundary")
108
+ else: remote=request(cfg,{"op":"ledger","workspace":ws})
109
+ return {"epoch":epoch,**remote}
110
+ def directory_user(found,user): field="id" if len(user)==32 and all(c in "0123456789abcdef" for c in user) else "name"; values=[v for v in found["users"] if v[field]==user]; return values[0] if len(values)==1 and public_id(values[0]["root_public"])==values[0]["id"] else (_ for _ in ()).throw(ValueError("directory user mismatch"))
111
+ def update_recovery(cfg,root=None):
112
+ personal={ws for ws,v in cfg["workspaces"].items() if v["kind"]=="personal"}; keys={name:value for name,value in cfg["keys"].items() if name.rsplit(":",1)[0] in personal}; _,bundle=recovery_bundle({"root":cfg["root"],"keys":keys,"workspaces":cfg["workspaces"],"controls":{ws:v for ws,v in cfg["controls"].items() if ws in personal}},unb64(cfg["recovery"])); request(cfg,sign_control(cfg["device"],{"op":"recovery","bundle":bundle})); save(cfg,root)
113
+ def refresh(cfg,root=None):
114
+ state=request(cfg,{"op":"state"}); cfg["server_state"]=state; changed=False; seen=set()
115
+ if (state["user"],state["device"])!=(cfg["user"],cfg["device"]["id"]): raise ValueError("relay identity metadata mismatch")
116
+ for ws in state["workspaces"]:
117
+ if ws["id"] in seen: raise ValueError("duplicate relay workspace")
118
+ seen.add(ws["id"])
119
+ previous=None
120
+ for value in ws["controls"]: verify_state(value,previous); previous=value
121
+ if not previous or (ws["id"],ws["kind"],ws["epoch"])!=(previous["workspace"],previous["scope"],previous["epoch"]): raise ValueError("relay workspace metadata does not match signed state")
122
+ member=previous["members"].get(cfg["user"]); authorized=cfg["device"]["id"] in previous["devices"] and cfg["device"]["id"] not in previous["removed"]
123
+ if not member or (ws["role"],ws["history_from"],bool(ws["device_authorized"]))!=(member["role"],member["history_from"],authorized): raise ValueError("relay access metadata does not match signed state")
124
+ pinned=cfg["controls"].get(ws["id"])
125
+ if pinned and (not previous or pinned["revision"]>previous["revision"] or state_hash(pinned) not in {state_hash(v) for v in ws["controls"]}): raise ValueError("workspace control rollback or fork")
126
+ cfg["controls"][ws["id"]]=previous; cfg["workspaces"].setdefault(ws["id"],{"name":ws["id"][:8]}); cfg["workspaces"][ws["id"]].update(kind=previous["scope"],epoch=previous["epoch"]); ws.update(kind=previous["scope"],epoch=previous["epoch"],role=member["role"],history_from=member["history_from"],device_authorized=authorized)
127
+ first=min((v["epoch"] for v in ws["controls"] if cfg["device"]["id"] in v["devices"]),default=previous["epoch"]); start=member["history_from"] if authorized and previous["devices"][cfg["device"]["id"]]["history"] else first; epochs=[int(v["epoch"]) for v in ws["keys"]]
128
+ if authorized and (len(epochs)!=len(set(epochs)) or any(not start<=epoch<=previous["epoch"] for epoch in epochs)): raise ValueError("relay key envelope exceeds signed entitlement")
129
+ for wrapped in ws["keys"] if authorized else []:
130
+ name=f"{ws['id']}:{wrapped['epoch']}"
131
+ if name not in cfg["keys"]:
132
+ opened=open_key(json.loads(wrapped["envelope"]),cfg["device"]["box_private"],f"workspace:{ws['id']}:epoch:{wrapped['epoch']}"); controls=[v for v in ws["controls"] if v["epoch"]==wrapped["epoch"]]
133
+ if not controls or digest(opened)!=controls[-1]["key_commitment"]: raise ValueError("workspace epoch key commitment mismatch")
134
+ cfg["keys"][name]=b64(opened); changed=True
135
+ save(cfg,root)
136
+ if changed: update_recovery(cfg,root)
137
+ return state
138
+ def create(cfg,name,kind="team",root=None):
139
+ ws,key_=digest(os.urandom(32))[:32],os.urandom(32); entry=own_record(cfg); control=control_sign(cfg["device"],{"v":CONTROL_V,"kind":"workspace.state","workspace":ws,"scope":kind,"revision":1,"prev":None,"epoch":1,"boundary":{"epoch":1,"tail":0,"heads":{}},"key_commitment":digest(key_),"members":{cfg["user"]:{"role":"admin","joined":1,"history_from":1}},"devices":{cfg["device"]["id"]:entry},"removed":[],"action":"create","approval":None,"approved_at":time.time()}); env=seal_key(key_,cfg["device"]["box_public"],f"workspace:{ws}:epoch:1"); request(cfg,sign_control(cfg["device"],{"op":"create","workspace":ws,"kind":kind,"control":control,"envelopes":{cfg["device"]["id"]:env}})); cfg["workspaces"][ws]={"name":name,"kind":kind,"epoch":1}; cfg["keys"][f"{ws}:1"]=b64(key_); cfg["controls"][ws]=control; update_recovery(cfg,root); membership_event(cfg,ws,1,{cfg["user"]:"admin"},root); return ws
140
+ def enroll(url,name,root,device,recovery=None):
141
+ cert=certificate(root,root["id"],device); base={"root_public":root["sign_public"],"certificate":cert}; challenge=request({"url":url},{"op":"register_challenge",**base},False)["challenge"]; return request({"url":url},{"op":"register","user_name":name,**base,"challenge":challenge,"proof":registration_proof(device,challenge,root["sign_public"],cert),**({"recovery":recovery} if recovery else {})},False)
142
+ def setup_client(url,user,device="computer",recovery=None,root=None):
143
+ if recovery:
144
+ bundle=request({"url":url},{"op":"recovery_fetch","user":user},False)["bundle"]; recovered=recover(bundle,recovery); root_id=recovered["root"]; keys,workspaces,controls=recovered["keys"],recovered["workspaces"],recovered.get("controls",{})
145
+ else: root_id,keys,workspaces,controls=identity(user+" root"),{},{},{}
146
+ dev=identity(device); uid=root_id["id"]
147
+ if not recovery: recovery,bundle=recovery_bundle({"root":root_id,"keys":keys,"workspaces":workspaces})
148
+ registered=enroll(url,user,root_id,dev,bundle if not workspaces else None)
149
+ cfg={"url":url,"name":user,"user":uid,"token":registered["token"],"root":root_id,"device":dev,"recovery":recovery,"keys":keys,"workspaces":workspaces,"controls":controls,"bindings":{},"server_state":{}}; save(cfg,root)
150
+ if not workspaces: create(cfg,"Personal","personal",root)
151
+ else:
152
+ state=refresh(cfg,root)
153
+ for ws in [w for w in state["workspaces"] if cfg["controls"][w["id"]]["members"][uid]["role"]=="admin" and cfg["controls"][w["id"]]["scope"]=="personal"]:
154
+ rotate(cfg,ws["id"],{u:m["role"] for u,m in cfg["controls"][ws["id"]]["members"].items()},[],root=root); grant_all(cfg,ws["id"],uid,root)
155
+ return cfg,recovery
156
+ def rehome_client(cfg,url,root=None):
157
+ recovery,bundle=recovery_bundle({"root":cfg["root"],"keys":{},"workspaces":{}}); registered=enroll(url,cfg["name"],cfg["root"],cfg["device"],bundle); fresh={**cfg,"url":url,"token":registered["token"],"recovery":recovery,"keys":{},"workspaces":{},"controls":{},"bindings":{},"server_state":{}}; save(fresh,root); create(fresh,"Personal","personal",root); return fresh,recovery
158
+ def rotate(cfg,ws,members,devices,deactivate=(),root=None):
159
+ state=refresh(cfg,root); previous=cfg["controls"][ws]; epoch=previous["epoch"]+1; boundary=next_boundary(cfg,ws,root); new=os.urandom(32); devices=trusted(devices); old=previous["members"]; meta={u:old.get(u,{"joined":epoch,"history_from":epoch})|{"role":role} for u,role in members.items()}; removed=sorted(set(previous["removed"])|set(deactivate)|{d for d,r in previous["devices"].items() if r["user"] not in members}); records={d:r for d,r in previous["devices"].items() if r["user"] in members and d not in deactivate}
160
+ if cfg["device"]["id"] not in previous["devices"] and previous["scope"]=="personal":
161
+ entry=own_record(cfg); req=device_proposal(cfg["device"],ws,previous,{**entry,"history":True},time.time()+300); records|={cfg["device"]["id"]:{**entry,"history":True}}; action,approval="personal_recover",{"proposal":req,"votes":[]}
162
+ else:
163
+ action,approval=("remove",None) if deactivate else ("membership",None); new_users=set(members)-set(old)
164
+ records|={d["id"]:server_record(d) for d in devices if d["user_id"] in new_users and d["id"] not in removed and d.get("active",1) and d.get("allowed",1)}
165
+ control=control_body(cfg,previous,new,action,meta,records,removed,approval,boundary); envs={d:seal_key(new,r["device"]["box_public"],f"workspace:{ws}:epoch:{epoch}") for d,r in records.items()}; request(cfg,sign_control(cfg["device"],{"op":"rotate","workspace":ws,"control":control,"envelopes":envs})); cfg["keys"][f"{ws}:{epoch}"]=b64(new); cfg["workspaces"][ws]["epoch"]=epoch; cfg["controls"][ws]=control; update_recovery(cfg,root); cfg["device"]["id"] in records and membership_event(cfg,ws,epoch,members,root); return epoch
166
+ def publish(cfg,state,ws,record,root=None,defer=False,heads=None):
167
+ if cfg["workspaces"][ws]["kind"]=="team":
168
+ if not (records:=protect_all([record],root,ws)): return None
169
+ record=records[0]
170
+ payload_v=record.get("payload_v",1)
171
+ if event_support({"kind":record["kind"],"payload_v":payload_v})!="supported": raise ValueError(f"unsupported event schema: {record['kind']} payload_v={payload_v}")
172
+ revision=digest(record["payload"]); old=heads.get(record["entity"]) if heads is not None else state.execute("SELECT revision,event FROM publication_heads WHERE workspace=? AND owner=? AND entity=?",(ws,cfg["user"],record["entity"])).fetchone()
173
+ if heads is not None and old==revision: return None
174
+ if heads is None and old and old["revision"]==revision: return old["event"]
175
+ seq=int((state.execute("SELECT value FROM meta WHERE key=?",(f"seq:{ws}",)).fetchone() or ["0"])[0])+1; prev=(state.execute("SELECT value FROM meta WHERE key=?",(f"prev:{ws}",)).fetchone() or [None])[0]; value=event(cfg["device"],seq,record["kind"],record["entity"],record["payload"],[prev] if prev else (),record.get("observed_at"),payload_v); epoch=cfg["workspaces"][ws]["epoch"]; env=seal_event(value,ws,epoch,key(cfg,ws,epoch)); path,size=encrypted_file(root,value["id"],env); status=record["payload"].get("status") if isinstance(record["payload"],dict) and record["payload"].get("status") in ("active","deleted") else None
176
+ state.execute("INSERT INTO outbox VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",(ws,value["id"],record["entity"],revision,value["author"],seq,epoch,record["kind"],payload_v,status,str(path),size)); state.execute("INSERT OR REPLACE INTO publication_heads VALUES (?,?,?,?,?)",(ws,cfg["user"],record["entity"],revision,value["id"])); state.execute("INSERT OR REPLACE INTO meta VALUES (?,?),(?,?)",(f"seq:{ws}",str(seq),f"prev:{ws}",value["id"])); sequence(state,ws,value)
177
+ if not defer: state.commit()
178
+ if heads is not None: heads[record["entity"]]=revision
179
+ project(core_path(root),state,value,ws,cfg["device"]["id"],root=root,authors={cfg["device"]["id"]:cfg["user"]}); return value["id"]
180
+ def membership_event(cfg,ws,epoch,members,root=None):
181
+ state=connect(paths(root)[2]); publish(cfg,state,ws,{"kind":"workspace.membership","entity":f"membership:{epoch}","payload":{"epoch":epoch,"members":members}},root); upload(cfg,state,root); state.close()
182
+ def control_event(cfg,ws,action,target,root=None):
183
+ state=connect(paths(root)[2]); value=cfg["controls"][ws]; publish(cfg,state,ws,{"kind":"workspace.device","entity":f"device:{target}:{value['revision']}","payload":{"action":action,"device":target,"revision":value["revision"],"state":state_hash(value)}},root); upload(cfg,state,root); state.close()
184
+ def _upload_batches(rows,limit=8*1024*1024,measure=lambda row:row["size"]):
185
+ batch,size=[],0
186
+ for row in rows:
187
+ amount=measure(row)
188
+ if amount>limit: raise ValueError("encrypted upload item exceeds byte limit")
189
+ if batch and (len(batch)==500 or size+amount>limit): yield batch; batch,size=[],0
190
+ batch.append(row); size+=amount
191
+ if batch: yield batch
192
+ def upload(cfg,state,root=None,workspaces=None):
193
+ active={w["id"] for w in refresh(cfg,root)["workspaces"]}
194
+ if workspaces is not None: active&=set(workspaces)
195
+ rows=[r for r in state.execute("SELECT * FROM outbox ORDER BY workspace,seq").fetchall() if r["workspace"] in active]
196
+ for batch in _upload_batches(rows):
197
+ prepared=[]; acknowledged=[]
198
+ for row in batch:
199
+ expected=paths(root)[0]/"outbox"/f"{row['event']}.json"
200
+ if Path(row["path"])!=expected or expected.is_symlink(): raise ValueError("invalid encrypted outbox path")
201
+ env=json.loads(expected.read_text()); current=cfg["workspaces"][row["workspace"]]["epoch"]
202
+ if env["epoch"]!=current:
203
+ try: found=request(cfg,{"op":"fetch","workspace":row["workspace"],"event":row["event"]})
204
+ except ValueError as e:
205
+ if str(e)!="event not found": raise
206
+ value=open_event(env,key(cfg,row["workspace"],env["epoch"]),cfg["device"]["sign_public"]); env=seal_event(value,row["workspace"],current,key(cfg,row["workspace"],current)); path,size=encrypted_file(root,row["event"],env); state.execute("UPDATE outbox SET epoch=?,path=?,size=? WHERE workspace=? AND event=?",(current,str(path),size,row["workspace"],row["event"]))
207
+ else:
208
+ if found["envelope"]!=env or not isinstance(found["cursor"],int) or isinstance(found["cursor"],bool): raise ValueError("relay upload acknowledgement mismatch")
209
+ acknowledged.append((row,env,expected,{"cursor":found["cursor"]})); continue
210
+ prepared.append((row,env,expected))
211
+ state.commit(); result=request(cfg,{"op":"upload_many","envelopes":[p[1] for p in prepared]})["events"] if prepared else []
212
+ if len(result)!=len(prepared) or any(not isinstance(r.get("cursor"),int) or isinstance(r["cursor"],bool) or r["cursor"]<1 for r in result): raise ValueError("relay upload acknowledgement mismatch")
213
+ for row,env,path,ack in acknowledged+[(row,env,path,ack) for (row,env,path),ack in zip(prepared,result)]:
214
+ state.execute("INSERT OR REPLACE INTO receipts VALUES (?,?,?,?,?,?,?,?,?,?,?)",(row["workspace"],row["event"],ack["cursor"],row["author"],row["seq"],env["epoch"],row["kind"],row["payload_v"],row["entity"],row["revision"],row["status"])); state.execute("DELETE FROM outbox WHERE workspace=? AND event=?",(row["workspace"],row["event"]))
215
+ state.commit(); [path.unlink(missing_ok=True) for row,env,path,ack in acknowledged+[(row,env,path,ack) for (row,env,path),ack in zip(prepared,result)]]
216
+ upload_blobs(cfg,state,root,active)
217
+ def reconcile_replicas(cfg,state,root,ws,envelopes):
218
+ for page in _upload_batches(envelopes,48*1024**2,lambda env:len(json.dumps(env,separators=(",",":")).encode())):
219
+ ids=[e["replica"] for e in page]; present=request(cfg,{"op":"replica_reconcile","workspace":ws,"replicas":ids})["present"]
220
+ if not isinstance(present,dict) or not set(present)<=set(ids) or any(not isinstance(v,int) or isinstance(v,bool) or v<1 for v in present.values()): raise ValueError("relay replica inventory mismatch")
221
+ missing=[env for env in page if env["replica"] not in present]; uploaded=request(cfg,{"op":"replica_upload_many","envelopes":missing})["replicas"] if missing else []
222
+ if len(uploaded)!=len(missing) or any(not isinstance(r.get("cursor"),int) or isinstance(r["cursor"],bool) or r["cursor"]<1 for r in uploaded): raise ValueError("relay replica acknowledgement mismatch")
223
+ cursors={**present,**{env["replica"]:ack["cursor"] for env,ack in zip(missing,uploaded)}}; [state.execute("INSERT OR REPLACE INTO replica_receipts VALUES (?,?,?,?)",(ws,env["replica"],env["epoch"],cursors[env["replica"]])) for env in page]; state.commit()
224
+ def replica_inventory(cfg,state,ws,candidates):
225
+ found={}
226
+ for rows in (candidates[i:i+500] for i in range(0,len(candidates),500)):
227
+ page=[r[0] for r in rows]
228
+ present=request(cfg,{"op":"replica_reconcile","workspace":ws,"replicas":page})["present"]
229
+ if not isinstance(present,dict) or not set(present)<=set(page) or any(not isinstance(v,int) or isinstance(v,bool) or v<1 for v in present.values()): raise ValueError("relay replica inventory mismatch")
230
+ found.update(present); [state.execute("INSERT OR REPLACE INTO replica_receipts VALUES (?,?,?,?)",(ws,replica,epoch,present[replica])) if replica in present else state.execute("DELETE FROM replica_receipts WHERE workspace=? AND replica=? AND epoch=?",(ws,replica,epoch)) for replica,epoch in rows]
231
+ state.commit(); return set(found)
232
+ def reconcile_blobs(cfg,state,root,ws,envelopes):
233
+ for page in (envelopes[i:i+500] for i in range(0,len(envelopes),500)):
234
+ ids=[e["blob"] for e in page]; present=request(cfg,{"op":"blob_reconcile","workspace":ws,"blobs":ids})["present"]
235
+ if not isinstance(present,dict) or not set(present)<=set(ids) or any(not isinstance(v,int) or isinstance(v,bool) or v<1 for v in present.values()): raise ValueError("relay blob inventory mismatch")
236
+ for env in page:
237
+ if env["blob"] in present: state.execute("INSERT OR REPLACE INTO blob_receipts VALUES (?,?,?,?)",(ws,env["blob"],env["epoch"],present[env["blob"]]))
238
+ else:
239
+ state.execute("DELETE FROM blob_receipts WHERE workspace=? AND blob=? AND epoch=?",(ws,env["blob"],env["epoch"]));
240
+ if not state.execute("SELECT 1 FROM blob_outbox WHERE workspace=? AND blob=? AND epoch=?",(ws,env["blob"],env["epoch"])).fetchone(): path,size=encrypted_file(root,"blob-"+digest((ws,env["blob"],env["epoch"],env["uploader"])),env); state.execute("INSERT INTO blob_outbox VALUES (?,?,?,?,?,?)",(ws,env["blob"],env["epoch"],env["uploader"],str(path),size))
241
+ def upload_blobs(cfg,state,root,workspaces):
242
+ for row in [r for r in state.execute("SELECT * FROM blob_outbox ORDER BY workspace,blob").fetchall() if r["workspace"] in workspaces]:
243
+ path=Path(row["path"]); env=json.loads(path.read_text())
244
+ if path.is_symlink() or (env["workspace"],env["blob"],env["epoch"],env["uploader"])!=(row["workspace"],row["blob"],row["epoch"],row["uploader"]): raise ValueError("invalid blob outbox")
245
+ ack=request(cfg,{"op":"blob_upload","envelope":env}); state.execute("INSERT OR REPLACE INTO blob_receipts VALUES (?,?,?,?)",(row["workspace"],row["blob"],row["epoch"],ack["cursor"])); state.execute("DELETE FROM blob_outbox WHERE workspace=? AND blob=? AND epoch=? AND uploader=?",(row["workspace"],row["blob"],row["epoch"],row["uploader"])); state.commit(); path.unlink(missing_ok=True)
246
+ def pull_origins(cfg,state,root,ws):
247
+ sid=ws["id"]; values=request(cfg,{"op":"origin_pull","workspace":sid})["origins"]
248
+ if not values: return {r[0] for r in state.execute("SELECT origin FROM origin_bindings WHERE workspace=?",(sid,)).fetchall()}
249
+ valid={}; invalid={}
250
+ for item in values:
251
+ env=item["envelope"]; identity=(env["origin"],env["epoch"])
252
+ if env["workspace"]!=sid or not isinstance(item["cursor"],int) or isinstance(item["cursor"],bool) or not access_from(cfg,sid)<=env["epoch"]<=cfg["controls"][sid]["epoch"]: raise ValueError("origin bundle response mismatch")
253
+ if identity in valid: continue
254
+ try: body=open_origin(env,key(cfg,sid,env["epoch"])); controls=control_chain(body["controls"]); origin=controls[0]["workspace"]
255
+ except ValueError: invalid[identity]=item["cursor"]
256
+ else:
257
+ if origin==sid: raise ValueError("origin bundle response mismatch")
258
+ valid[identity]=(item,body,controls,origin); invalid.pop(identity,None)
259
+ if invalid: raise ValueError(f"invalid origin bundle: no valid delivery copy for {len(invalid)} object(s)")
260
+ core=open_db(core_path(root)); init_schema(core); core.execute("BEGIN")
261
+ try:
262
+ for item,body,controls,origin in valid.values():
263
+ env=item["envelope"]; project_workspace_controls(core,controls); state.execute(f"INSERT OR REPLACE INTO {'origin_bindings' if body['rows'] else 'control_dependencies'} VALUES (?,?,?,?,?)",(sid,origin,env["origin"],env["epoch"],item["cursor"]))
264
+ state.commit(); core.execute("COMMIT"); return {r[0] for r in state.execute("SELECT origin FROM origin_bindings WHERE workspace=?",(sid,)).fetchall()}
265
+ except BaseException: core.execute("ROLLBACK"); state.rollback(); raise
266
+ finally: core.close()
267
+ def pull_row_replicas(cfg,state,root,ws,recover=None,origins=()):
268
+ sid=ws["id"]; stamp=bridge_stamp(root); saved=(state.execute("SELECT value FROM meta WHERE key=?",(f"replica_projection:{sid}",)).fetchone() or [None])[0]; reset=saved!=stamp; after=0 if reset else int((state.execute("SELECT value FROM meta WHERE key=?",(f"replica_cursor:{sid}",)).fetchone() or [0])[0]); cursor,total=after,0; dependencies={r[0] for r in state.execute("SELECT origin FROM control_dependencies WHERE workspace=?",(sid,)).fetchall()}; controls=ws["controls"]+stored_controls(core_path(root),set(origins)|dependencies); known=set() if reset else {(r[0],r[1]) for r in state.execute("SELECT replica,epoch FROM replica_receipts WHERE workspace=?",(sid,)).fetchall()}; valid=set(known); invalid={}; checked=recover is None or not known
269
+ while True:
270
+ result=request(cfg,{"op":"replica_pull","workspace":sid,"after":cursor,"limit":500}); floor,tail=result["floor"],result["tail"]
271
+ if not all(isinstance(v,int) and not isinstance(v,bool) and v>=0 for v in (floor,tail)) or floor>tail and tail: raise ValueError("relay replica cursor window is invalid")
272
+ if cursor>tail: cursor=after=0; known=set(); valid=set(); invalid={}; state.execute("DELETE FROM meta WHERE key=?",(f"replica_cursor:{sid}",)); state.execute("INSERT OR REPLACE INTO meta VALUES (?,?)",(f"replica_repair:{sid}","1")); state.commit(); continue
273
+ if not checked and result["replicas"]:
274
+ fields=("workspace","authorization_workspace","row_kind","row_id","encoding_v","content_hash","revision","previous_revision","state","author_user_id","author_device_id","authorization_epoch","signature"); core=open_db(core_path(root),True); proofs=[{"v":1,"kind":"row.proof",**dict(zip(fields,r))} for r in core.execute("SELECT workspace_id,authorization_workspace_id,row_kind,source_row_id,encoding_v,content_hash,revision,previous_revision,state,author_user_id,author_device_id,authorization_epoch,signature FROM remote.row_proofs").fetchall()]; core.close(); epochs={r[1] for r in known}; known&={(fingerprint(key(cfg,sid,epoch),digest(proof)),epoch) for proof in proofs for epoch in epochs}; valid=set(known); checked=True
275
+ opened=[]; received=[]
276
+ for item in result["replicas"]:
277
+ env=item["envelope"]
278
+ if not isinstance(item["cursor"],int) or isinstance(item["cursor"],bool) or not cursor<item["cursor"]<=tail or env["workspace"]!=sid or not access_from(cfg,sid)<=env["epoch"]<=cfg["controls"][sid]["epoch"]: raise ValueError("row replica envelope response mismatch")
279
+ cursor=item["cursor"]; received.append(item); identity=(env["replica"],env["epoch"])
280
+ if identity not in valid:
281
+ try: body=open_replica(env,key(cfg,sid,env["epoch"]))
282
+ except ValueError: invalid.setdefault(identity,item["cursor"])
283
+ else: opened.append((item,body)); valid.add(identity); invalid.pop(identity,None)
284
+ accepted=apply_row_replicas(core_path(root),[body for item,body in opened],sid,controls,recover,cfg["user"],root=root); accepted_keys={(item["envelope"]["replica"],item["envelope"]["epoch"]) for (item,body),ok in zip(opened,accepted) if body["proof"]["kind"]=="row.proof" or ok}
285
+ known|=accepted_keys
286
+ for item in received:
287
+ env=item["envelope"]
288
+ if (env["replica"],env["epoch"]) in known|accepted_keys: state.execute("INSERT OR REPLACE INTO replica_receipts VALUES (?,?,?,?)",(sid,env["replica"],env["epoch"],item["cursor"]))
289
+ after=min(invalid.values())-1 if invalid else cursor; total+=len(received); state.execute("INSERT OR REPLACE INTO meta VALUES (?,?),(?,?)",(f"replica_cursor:{sid}",str(after),f"replica_projection:{sid}",stamp)); state.commit()
290
+ if cursor>=tail:
291
+ if invalid: raise ValueError(f"invalid row replica: no valid delivery copy for {len(invalid)} object(s)")
292
+ return total
293
+ if not result["replicas"]: raise ValueError("relay replica tail cannot be reached")
294
+ def pull_blobs(cfg,state,root,ws,recover=None):
295
+ sid=ws["id"]; after=int((state.execute("SELECT value FROM meta WHERE key=?",(f"blob_cursor:{sid}",)).fetchone() or [0])[0]); cursor,total=after,0; known={(r[0],r[1]) for r in state.execute("SELECT blob,epoch FROM blob_receipts WHERE workspace=?",(sid,)).fetchall()}; valid=set(known); invalid={}; checked=recover is None or not known
296
+ while True:
297
+ result=request(cfg,{"op":"blob_pull","workspace":sid,"after":cursor,"limit":20}); floor,tail=result["floor"],result["tail"]
298
+ if not all(isinstance(v,int) and not isinstance(v,bool) and v>=0 for v in (floor,tail)) or floor>tail and tail: raise ValueError("relay blob cursor window is invalid")
299
+ if cursor>tail: cursor=after=0; known=set(); valid=set(); invalid={}; state.execute("DELETE FROM meta WHERE key=?",(f"blob_cursor:{sid}",)); state.execute("INSERT OR REPLACE INTO meta VALUES (?,?)",(f"replica_repair:{sid}","1")); state.commit(); continue
300
+ if not checked and result["blobs"]:
301
+ core=open_db(core_path(root),True); hashes=[r[0] for r in core.execute("SELECT content_hash FROM attachment_bodies").fetchall()]; core.close(); epochs={r[1] for r in known}; known&={(fingerprint(key(cfg,sid,epoch),body_hash),epoch) for body_hash in hashes for epoch in epochs}; valid=set(known); checked=True
302
+ received=[]
303
+ for item in result["blobs"]:
304
+ env=item["envelope"]
305
+ if not isinstance(item["cursor"],int) or isinstance(item["cursor"],bool) or not cursor<item["cursor"]<=tail or env["workspace"]!=sid or not access_from(cfg,sid)<=env["epoch"]<=cfg["controls"][sid]["epoch"]: raise ValueError("blob replica envelope response mismatch")
306
+ cursor=item["cursor"]; received.append(item); identity=(env["blob"],env["epoch"])
307
+ if identity not in valid:
308
+ try: data,body_hash=open_blob(env,key(cfg,sid,env["epoch"]))
309
+ except ValueError: invalid.setdefault(identity,item["cursor"])
310
+ else: project_attachment_body(core_path(root),data,body_hash); known.add(identity); valid.add(identity); invalid.pop(identity,None)
311
+ for item in received:
312
+ env=item["envelope"]; identity=(env["blob"],env["epoch"])
313
+ if identity in valid: state.execute("INSERT OR REPLACE INTO blob_receipts VALUES (?,?,?,?)",(sid,*identity,item["cursor"]))
314
+ after=min(invalid.values())-1 if invalid else cursor; total+=len(received); state.execute("INSERT OR REPLACE INTO meta VALUES (?,?)",(f"blob_cursor:{sid}",str(after))); state.commit()
315
+ if cursor>=tail:
316
+ if invalid: raise ValueError(f"invalid blob replica: no valid delivery copy for {len(invalid)} object(s)")
317
+ return total
318
+ if not result["blobs"]: raise ValueError("relay blob tail cannot be reached")
319
+ def pull(cfg,state,root=None):
320
+ root=local_root(root)
321
+ server=refresh(cfg,root)
322
+ summary={}
323
+ for ws in server["workspaces"]:
324
+ if not ws["device_authorized"]: continue
325
+ sid=ws["id"]; current=state.execute("SELECT lifecycle FROM sync_states WHERE workspace=?",(sid,)).fetchone(); lifecycle=current[0] if current else "rebaselining"; state.execute("INSERT OR REPLACE INTO sync_states VALUES (?,?,COALESCE((SELECT tail FROM sync_states WHERE workspace=?),0),COALESCE((SELECT floor FROM sync_states WHERE workspace=?),0),NULL)",(sid,lifecycle,sid,sid)); state.commit()
326
+ records={r["device"]["id"]:r for control in ws["controls"] for r in control["devices"].values()}; devices={device:r["device"] for device,r in records.items()}; authors={device:r["user"] for device,r in records.items()}
327
+ after=(state.execute("SELECT cursor FROM cursors WHERE workspace=?",(sid,)).fetchone() or [0])[0]; seen=(state.execute("SELECT value FROM meta WHERE key=?",(f"history_from:{sid}",)).fetchone() or [str(ws["history_from"])])[0]; earliest=min([k["epoch"] for k in ws["keys"]],default=ws["epoch"]); old_key=int((state.execute("SELECT value FROM meta WHERE key=?",(f"key_from:{sid}",)).fetchone() or [earliest])[0]); start=access_from(cfg,sid); candidates=[control["boundary"] for control in ws["controls"] if control["boundary"]["epoch"]==start]; boundary=candidates[0] if candidates and all(value==candidates[0] for value in candidates) else (_ for _ in ()).throw(ValueError("signed history boundary is ambiguous")); proof=digest(boundary); old_proof=(state.execute("SELECT value FROM meta WHERE key=?",(f"boundary:{sid}",)).fetchone() or [None])[0]; mode=(state.execute("SELECT value FROM meta WHERE key=?",(f"archive_mode:{sid}",)).fetchone() or [None])[0]; recover=mode if mode=="adopt" or ws["kind"]=="personal" and mode in ("native","import") else None
328
+ deferred=state.execute("SELECT kind,payload_v,required FROM deferred_events WHERE workspace=?",(sid,)).fetchall(); reclassify=any(event_support(r)!=("required" if r["required"] else "optional") for r in deferred)
329
+ if not current or ws["history_from"]<int(seen) or earliest<old_key or proof!=old_proof or reclassify: after=0; reset_history(state,sid,boundary); state.execute("INSERT OR REPLACE INTO meta VALUES (?,?),(?,?),(?,?)",(f"boundary:{sid}",proof,f"replica_cursor:{sid}","0",f"blob_cursor:{sid}","0")); state.commit()
330
+ try:
331
+ total=0; origins=pull_origins(cfg,state,root,ws); replicas=pull_row_replicas(cfg,state,root,ws,recover,origins); blobs=pull_blobs(cfg,state,root,ws,recover)
332
+ while True:
333
+ result=request(cfg,{"op":"pull","workspace":sid,"after":after,"limit":500}); floor,tail=result["floor"],result["tail"]
334
+ if not all(isinstance(v,int) and not isinstance(v,bool) and v>=0 for v in (floor,tail)) or floor>tail and tail or after>tail: raise ValueError("relay cursor window is invalid")
335
+ incoming=[]
336
+ for item in result["events"]:
337
+ env=request(cfg,{"op":"fetch","workspace":sid,"event":item["event"]})["envelope"] if item.get("lazy") else item["envelope"]
338
+ if (env["workspace"],env["event"])!=(sid,item.get("event",env["event"])) or not access_from(cfg,sid)<=env["epoch"]<=cfg["controls"][sid]["epoch"]: raise ValueError("event envelope response mismatch")
339
+ value=open_event(env,key(cfg,sid,env["epoch"]),signer(devices,env["author"])); support=event_support(value); sequence(state,sid,value); receipt(state,sid,value,item["cursor"],env["epoch"])
340
+ if authors[value["author"]]==cfg["user"]:
341
+ state.execute("INSERT OR REPLACE INTO publication_heads VALUES (?,?,?,?,?)",(sid,cfg["user"],value["entity"],value["revision"],value["id"]))
342
+ if value["author"]==cfg["device"]["id"]:
343
+ if value["seq"]>=int((state.execute("SELECT value FROM meta WHERE key=?",(f"seq:{sid}",)).fetchone() or ["0"])[0]): state.execute("INSERT OR REPLACE INTO meta VALUES (?,?),(?,?)",(f"seq:{sid}",str(value["seq"]),f"prev:{sid}",value["id"]))
344
+ if support!="supported": state.execute("INSERT OR REPLACE INTO deferred_events VALUES (?,?,?,?,?,?)",(sid,value["id"],item["cursor"],value["kind"],value["payload_v"],support=="required"))
345
+ else: incoming.append((sid,value))
346
+ after=max(after,item["cursor"])
347
+ project_many(core_path(root),state,incoming,cfg["device"]["id"],root,False,authors,recover,cfg["user"]); total+=len(result["events"]); state.execute("INSERT OR REPLACE INTO cursors VALUES (?,?)",(sid,after)); state.execute("INSERT OR REPLACE INTO meta VALUES (?,?),(?,?)",(f"history_from:{sid}",str(ws["history_from"]),f"key_from:{sid}",str(earliest))); state.commit()
348
+ if after>=tail: break
349
+ if not result["events"]: raise ValueError("relay tail cannot be reached")
350
+ verify_history(state,sid,ws["controls"],start)
351
+ if mode=="import": raise ValueError("archive rollback or replacement recovered additively; publication remains blocked")
352
+ state.execute("INSERT OR REPLACE INTO sync_states VALUES (?,'ready',?,?,NULL)",(sid,tail,floor)); state.commit(); summary[sid]={"events":total,"replicas":replicas,"blobs":blobs,"cursor":after,"tail":tail,"floor":floor}
353
+ except Exception as e:
354
+ state.rollback(); state.execute("INSERT OR REPLACE INTO sync_states VALUES (?,'blocked',COALESCE((SELECT tail FROM sync_states WHERE workspace=?),0),COALESCE((SELECT floor FROM sync_states WHERE workspace=?),0),?)",(sid,sid,sid,str(e))); state.commit(); raise
355
+ return summary
356
+ def fetch_lazy(cfg,state,event_id=None,root=None):
357
+ root=local_root(root)
358
+ server=refresh(cfg,root); controls={ws["id"]:ws["controls"] for ws in server["workspaces"]}; sql="SELECT workspace,event,cursor FROM lazy_events"+(" WHERE event=?" if event_id else ""); rows=[r for r in state.execute(sql,(event_id,) if event_id else ()).fetchall() if r[0] in controls]
359
+ for ws,eid,cursor in rows:
360
+ records={r["device"]["id"]:r for control in controls[ws] for r in control["devices"].values()}; devices={device:r["device"] for device,r in records.items()}; authors={device:r["user"] for device,r in records.items()}
361
+ env=request(cfg,{"op":"fetch","workspace":ws,"event":eid})["envelope"]
362
+ if (env["workspace"],env["event"])!=(ws,eid) or not access_from(cfg,ws)<=env["epoch"]<=cfg["controls"][ws]["epoch"]: raise ValueError("lazy event response mismatch")
363
+ value=open_event(env,key(cfg,ws,env["epoch"]),signer(devices,env["author"])); support=event_support(value); sequence(state,ws,value); receipt(state,ws,value,cursor,env["epoch"]); support=="supported" and project(core_path(root),state,value,ws,cfg["device"]["id"],root=root,batch=True,authors=authors); support!="supported" and state.execute("INSERT OR REPLACE INTO deferred_events VALUES (?,?,?,?,?,?)",(ws,value["id"],cursor,value["kind"],value["payload_v"],support=="required")); state.execute("DELETE FROM lazy_events WHERE event=?",(eid,))
364
+ state.commit(); return len(rows)
365
+ def sync_once(root=None,force=False):
366
+ root=local_root(root)
367
+ with sync_lock(root):
368
+ cfg=load(root); _,_,state_path=paths(root); info=inspect_state(state_path,force); cutover=None
369
+ relocate_attachments(core_path(root),paths(root)[0]/"attachments")
370
+ if info["status"] in ("incompatible","invalid"): refresh(cfg,root); info["status"]=="incompatible" and rescue_bindings(cfg,state_path,root); cutover=cutover_state(state_path)
371
+ state=connect(state_path)
372
+ try:
373
+ drain_hooks(); refresh(cfg,root); ready={r[0] for r in state.execute("SELECT workspace FROM sync_states WHERE lifecycle='ready'").fetchall()}; force and [state.execute("INSERT OR REPLACE INTO meta VALUES (?,'1')",(f"replica_repair:{ws}",)) for ws in ready]; state.commit(); upload(cfg,state,root,ready); prepare_archive(cfg,state,root); baseline=archive_info(root); bound={r[0] for r in state.execute("SELECT DISTINCT workspace FROM origin_bindings").fetchall()}; pull(cfg,state,root); ready={r[0] for r in state.execute("SELECT workspace FROM sync_states WHERE lifecycle='ready'").fetchall()}; path=core_path(root); active={w["id"] for w in cfg["server_state"]["workspaces"]}; generation=archive_info(root)[1] if path.is_file() else 0; baseline and baseline[2]==0 and [state.execute("INSERT OR IGNORE INTO meta VALUES (?,?)",(f"core_generation:{ws}",str(generation))) for ws in ready&active-bound]; scans=[(ws,meta,state.execute("SELECT value FROM meta WHERE key=?",(f"core_generation:{ws}",)).fetchone()) for ws,meta in cfg["workspaces"].items() if path.is_file() and ws in ready and ws in active and f"{ws}:{meta['epoch']}" in cfg["keys"]]; scans=[(ws,meta,prior) for ws,meta,prior in scans if prior is None or int(prior[0])!=generation or state.execute("SELECT 1 FROM meta WHERE key=?",(f"replica_repair:{ws}",)).fetchone()]
374
+ if scans:
375
+ core=open_db(path,True); batches=[]
376
+ for ws,meta,prior in scans:
377
+ pol=state.execute("SELECT kind,value FROM policies WHERE workspace=?",(ws,)).fetchall(); repos=[p[1] for p in pol if p[0]=="repository"]; roots=[cfg.get("bindings",{}).get(f"{ws}:{p[1]}") for p in pol if p[0]=="path" and cfg.get("bindings",{}).get(f"{ws}:{p[1]}")]; changes=None if prior is None or state.execute("SELECT 1 FROM meta WHERE key=?",(f"replica_repair:{ws}",)).fetchone() else set(core_archive_changes(core,int(prior[0]))[1]); scope=set(); records=scan(core,state,meta["kind"],repos,roots,changes,ws,scope); records=protect_all(records,root,ws) if meta["kind"]=="team" else records; batches.append((ws,records,scope,prior is None))
378
+ core.close()
379
+ for ws,records,scope,initial in batches:
380
+ bindings={r[0]:r[1] for r in state.execute("SELECT origin,epoch FROM origin_bindings WHERE workspace=?",(ws,)).fetchall()}; origins=set(bindings); repair=initial or bool(state.execute("SELECT 1 FROM meta WHERE key=?",(f"replica_repair:{ws}",)).fetchone()); attest_rows(path,cfg,ws,records,origins); keys={epoch:key(cfg,ws,epoch) for epoch in range(access_from(cfg,ws),cfg["workspaces"][ws]["epoch"]+1) if f"{ws}:{epoch}" in cfg["keys"]}; known={r[0] for r in state.execute("SELECT replica FROM replica_receipts WHERE workspace=?",(ws,)).fetchall()}; [reconcile_replicas(cfg,state,root,ws,row_replicas(path,cfg,ws,records[i:i+5000],keys,known,origins,bindings,lambda ids:replica_inventory(cfg,state,ws,ids),repair and i==0)) for i in range(0,max(len(records),1),5000)]; known_blobs={r[0] for r in state.execute("SELECT blob FROM blob_receipts WHERE workspace=? UNION SELECT blob FROM blob_outbox WHERE workspace=?",(ws,ws)).fetchall()}; reconcile_blobs(cfg,state,root,ws,blob_replicas(path,cfg,ws,records,keys,known_blobs,origins,bindings,repair)); heads={r[0]:r[1] for r in state.execute("SELECT entity,revision FROM publication_heads WHERE workspace=? AND owner=?",(ws,cfg["user"])).fetchall()}; [publish(cfg,state,ws,r,root,True,heads) for r in records if r["kind"] not in SIGNED]; state.execute("DELETE FROM team_scopes WHERE workspace=?",(ws,)); state.executemany("INSERT INTO team_scopes VALUES (?,?)",[(ws,c) for c in scope])
381
+ [state.execute("INSERT OR REPLACE INTO meta VALUES (?,?)",(f"core_generation:{ws}",str(generation))) for ws,meta,prior in scans]
382
+ for ws,meta in cfg["workspaces"].items():
383
+ if ws in ready and ws in active and meta["kind"]=="personal" and f"{ws}:{meta['epoch']}" in cfg["keys"]:
384
+ known={r[0] for r in state.execute("SELECT replica FROM replica_receipts WHERE workspace=?",(ws,)).fetchall()}; repair=bool(state.execute("SELECT 1 FROM meta WHERE key=?",(f"replica_repair:{ws}",)).fetchone()); reconcile_replicas(cfg,state,root,ws,bridge_replicas(root,cfg,ws,meta["kind"],key(cfg,ws,meta["epoch"]),known,lambda ids:replica_inventory(cfg,state,ws,ids) if repair else set(known)))
385
+ [state.execute("DELETE FROM meta WHERE key=?",(f"replica_repair:{ws}",)) for ws in ready&active]
386
+ state.commit(); upload(cfg,state,root,ready)
387
+ pull(cfg,state,root); remember_archive(cfg,state,root); state.execute("INSERT OR REPLACE INTO meta VALUES ('last_sync',?)",(str(time.time()),)); state.commit()
388
+ finally: state.close()
389
+ return cutover
390
+ def bind_origin(cfg,state,ws,origin,root=None):
391
+ root=local_root(root); db=open_db(core_path(root),True); exists=db.execute("SELECT 1 FROM remote.row_proofs WHERE workspace_id=?",(origin,)).fetchone(); dependencies={r[0] for r in db.execute("SELECT DISTINCT authorization_workspace_id FROM remote.row_proofs WHERE workspace_id=? AND authorization_workspace_id<>workspace_id",(origin,)).fetchall()}; controls=stored_controls(core_path(root),{origin}|dependencies); db.close(); current=cfg["workspaces"][ws]["epoch"]
392
+ if origin==ws or not exists: raise ValueError("origin has no retained row proofs")
393
+ for target,rows in [(origin,True),*((d,False) for d in dependencies)]:
394
+ chain=control_chain([c for c in controls if c["workspace"]==target]); env=seal_origin(chain,ws,current,key(cfg,ws,current),cfg["device"]["id"],rows); ack=request(cfg,{"op":"origin_upload","envelope":env}); isinstance(ack.get("cursor"),int) and not isinstance(ack["cursor"],bool) or (_ for _ in ()).throw(ValueError("origin upload acknowledgement mismatch")); state.execute(f"INSERT OR REPLACE INTO {'origin_bindings' if rows else 'control_dependencies'} VALUES (?,?,?,?,?)",(ws,target,env["origin"],current,ack["cursor"]))
395
+ state.execute("DELETE FROM meta WHERE key=?",(f"core_generation:{ws}",)); state.commit(); return len(controls)
396
+ def add_member(cfg,ws,user,remove=False,root=None):
397
+ refresh(cfg,root); members={u:m["role"] for u,m in cfg["controls"][ws]["members"].items()}; devices=[]
398
+ if remove:
399
+ found=request(cfg,{"op":"directory","user":user}); target=directory_user(found,user) if found["users"] else {"id":user}; members.pop(target["id"],None)
400
+ else:
401
+ found=request(cfg,{"op":"directory","user":user}); trusted(found["devices"]); target=directory_user(found,user); members[target["id"]]="member"; devices+=found["devices"]
402
+ return rotate(cfg,ws,members,[d for d in {d["id"]:d for d in devices}.values() if d["user_id"] in members],root=root)
403
+ def grant_all(cfg,ws,user,root=None):
404
+ found=request(cfg,{"op":"directory","user":user}); target=directory_user(found,user); refresh(cfg,root); previous=cfg["controls"][ws]; devices=[r["device"] for r in previous["devices"].values() if r["user"]==target["id"]]; envelopes={}
405
+ for name,value in cfg["keys"].items():
406
+ if name.startswith(ws+":"):
407
+ epoch=int(name.rsplit(":",1)[1]); envelopes[str(epoch)]={d["id"]:seal_key(unb64(value),d["box_public"],f"workspace:{ws}:epoch:{epoch}") for d in devices}
408
+ members={**previous["members"],target["id"]:{**previous["members"][target["id"]],"history_from":1}}; records={d:{**r,"history":True} if r["user"]==target["id"] else r for d,r in previous["devices"].items()}; control=control_body(cfg,previous,key(cfg,ws,previous["epoch"]),"history",members,records); request(cfg,sign_control(cfg["device"],{"op":"grant_all","workspace":ws,"user":target["id"],"control":control,"envelopes":envelopes})); cfg["controls"][ws]=control; save(cfg,root); return len(envelopes)
409
+ def remove_device(cfg,ws,device_id,root=None):
410
+ refresh(cfg,root); members={u:m["role"] for u,m in cfg["controls"][ws]["members"].items()}; return rotate(cfg,ws,members,[],[device_id],root)
411
+ def request_device(cfg,ws,root=None,delay=3600):
412
+ refresh(cfg,root); base=cfg["controls"][ws]
413
+ if cfg["user"] not in base["members"] or cfg["device"]["id"] in base["devices"] or cfg["device"]["id"] in base["removed"]: raise ValueError("device is not pending for this workspace")
414
+ target=own_record(cfg,False); voters=electorate(base,cfg["user"]); not_before=time.time()+delay if len(voters)==1 and not any(d["user"]==cfg["user"] for d in base["devices"].values()) else time.time(); value=device_proposal(cfg["device"],ws,base,target,time.time()+86400,not_before); request(cfg,{"op":"propose","proposal":value}); return value
415
+ def proposals(cfg,ws): return request(cfg,{"op":"proposals","workspace":ws})["proposals"]
416
+ def pending(cfg,ws,device_id,kind):
417
+ base=cfg["controls"][ws]; found=[p for p in proposals(cfg,ws) if p["proposal"]["kind"]==kind and p["proposal"]["target"]["device"]["id"]==device_id and p["proposal"]["base"]==state_hash(base)]
418
+ if len(found)!=1: raise ValueError("pending device proposal not found")
419
+ return base,found[0]
420
+ def approve_device(cfg,ws,device_id,approve=True,root=None):
421
+ refresh(cfg,root); base,item=pending(cfg,ws,device_id,"device.proposal"); target=item["proposal"]["target"]; same=cfg["user"]==target["user"] and cfg["device"]["id"] in base["devices"]
422
+ if same and not approve:
423
+ request(cfg,sign_control(cfg["device"],{"op":"reject","workspace":ws,"proposal":state_hash(item["proposal"])})); return {"approved":False,"rejected":True}
424
+ if same: verify_proposal(base,item["proposal"])
425
+ if not same:
426
+ request(cfg,{"op":"vote","vote":device_vote(cfg["device"],cfg["user"],item["proposal"],approve)}); item=next(p for p in proposals(cfg,ws) if state_hash(p["proposal"])==state_hash(item["proposal"]))
427
+ yes=len({v["voter"] for v in item["votes"] if v["approve"]}); needed=len(electorate(base,target["user"]))//2+1
428
+ if not approve or yes<needed: return {"approved":False,"votes":yes,"needed":needed}
429
+ approved(base,item["proposal"],item["votes"])
430
+ new=os.urandom(32); epoch=base["epoch"]+1; boundary=next_boundary(cfg,ws,root); inherit=base["devices"][cfg["device"]["id"]]["history"] if same else False; entry={**target,"history":inherit}; records={**base["devices"],device_id:entry}; action="self_approve" if same else "quorum_approve"; proof={"proposal":item["proposal"],"votes":item["votes"]}; control=control_body(cfg,base,new,action,devices=records,approval=proof,boundary=boundary); envs={d:seal_key(new,r["device"]["box_public"],f"workspace:{ws}:epoch:{epoch}") for d,r in records.items()}; start=base["members"][target["user"]]["history_from"]; history={name.rsplit(":",1)[1]:seal_key(unb64(value),entry["device"]["box_public"],f"workspace:{ws}:epoch:{name.rsplit(':',1)[1]}") for name,value in cfg["keys"].items() if inherit and name.startswith(ws+":") and int(name.rsplit(":",1)[1])>=start}
431
+ body={"op":"rotate","workspace":ws,"control":control,"envelopes":envs}; history and body.update(history_envelopes={device_id:history})
432
+ request(cfg,sign_control(cfg["device"],body)); cfg["keys"][f"{ws}:{epoch}"]=b64(new); cfg["workspaces"][ws]["epoch"]=epoch; cfg["controls"][ws]=control; update_recovery(cfg,root); control_event(cfg,ws,action,device_id,root); return {"approved":True,"epoch":epoch,"history":len(history)}
433
+ def request_history(cfg,ws,root=None,delay=3600):
434
+ refresh(cfg,root); base=cfg["controls"][ws]; current=base["devices"].get(cfg["device"]["id"])
435
+ if not current or current["history"]: raise ValueError("device does not need history approval")
436
+ voters=electorate(base,cfg["user"]); value=device_proposal(cfg["device"],ws,base,{**current,"history":True},time.time()+86400,time.time()+delay if len(voters)==1 else time.time(),kind="history.proposal"); request(cfg,{"op":"propose","proposal":value}); return value
437
+ def approve_history(cfg,ws,device_id,approve=True,root=None):
438
+ refresh(cfg,root); base,item=pending(cfg,ws,device_id,"history.proposal"); target=item["proposal"]["target"]; request(cfg,{"op":"vote","vote":device_vote(cfg["device"],cfg["user"],item["proposal"],approve)}); item=next(p for p in proposals(cfg,ws) if state_hash(p["proposal"])==state_hash(item["proposal"])); yes=len({v["voter"] for v in item["votes"] if v["approve"]}); needed=len(electorate(base,target["user"]))//2+1
439
+ if not approve or yes<needed: return {"approved":False,"votes":yes,"needed":needed}
440
+ approved(base,item["proposal"],item["votes"],kind="history.proposal"); device=target["device"]["id"]; records={**base["devices"],device:{**base["devices"][device],"history":True}}; proof={"proposal":item["proposal"],"votes":item["votes"]}; control=control_body(cfg,base,key(cfg,ws,base["epoch"]),"history_activate",devices=records,approval=proof); start=base["members"][target["user"]]["history_from"]; envs={str(epoch):seal_key(key(cfg,ws,epoch),target["device"]["box_public"],f"workspace:{ws}:epoch:{epoch}") for epoch in range(start,base["epoch"]+1)}
441
+ request(cfg,sign_control(cfg["device"],{"op":"history_activate","workspace":ws,"control":control,"envelopes":envs})); cfg["controls"][ws]=control; save(cfg,root); control_event(cfg,ws,"history_activate",device,root); return {"approved":True,"history":len(envs)}
442
+
443
+ @remote.command("setup")
444
+ @locked
445
+ def setup_cmd(url:str,user:str,device:str=typer.Option("computer","--device")): cfg,recovery=setup_client(url,user,device); typer.echo(f"Personal workspace ready. User ID: {cfg['user']}. Recovery key (store offline): {recovery}")
446
+ @remote.command("recover")
447
+ @locked
448
+ def recover_cmd(url:str,user:str,device:str=typer.Option("computer","--device"),recovery:Optional[str]=typer.Option(None,"--recovery")): setup_client(url,user,device,recovery or typer.prompt("Recovery key",hide_input=True)); typer.echo("Device enrolled and keys rotated")
449
+ @remote.command("rehome")
450
+ @locked
451
+ def rehome_cmd(url:str): cfg,recovery=rehome_client(load(),url); typer.echo(f"Fresh relay ready. User ID retained: {cfg['user']}. New recovery key (store offline): {recovery}")
452
+ @remote.command("workspace")
453
+ @locked
454
+ def workspace_cmd(name:str): cfg=load(); typer.echo(create(cfg,name,"team"))
455
+ @remote.command("invite")
456
+ @locked
457
+ def invite_cmd(space:str,user:str): cfg=load(); typer.echo(f"epoch {add_member(cfg,workspace(cfg,space),user)}")
458
+ @remote.command("remove")
459
+ @locked
460
+ def remove_cmd(space:str,user:str): cfg=load(); typer.echo(f"epoch {add_member(cfg,workspace(cfg,space),user,True)}")
461
+ @remote.command("grant-all")
462
+ @locked
463
+ def grant_all_cmd(space:str,user:str): cfg=load(); typer.echo(f"Granted {grant_all(cfg,workspace(cfg,space),user)} epochs")
464
+ @remote.command("refound")
465
+ def refound_cmd(space:str,origin_workspace_id:str):
466
+ with sync_lock(None): cfg=load(); state=connect(paths()[2]); count=bind_origin(cfg,state,workspace(cfg,space),origin_workspace_id); state.close()
467
+ sync_once(force=True); typer.echo(f"Bound {count} signed origin controls")
468
+ @remote.command("origins")
469
+ def origins_cmd(): db=open_db(core_path(),True); typer.echo(json.dumps([{"workspace":r[0],"rows":r[1]} for r in db.execute("SELECT workspace_id,COUNT(*) FROM remote.row_proofs GROUP BY workspace_id ORDER BY workspace_id").fetchall()])); db.close()
470
+ @remote.command("remove-device")
471
+ @locked
472
+ def remove_device_cmd(space:str,device_id:str): cfg=load(); typer.echo(f"epoch {remove_device(cfg,workspace(cfg,space),device_id)}")
473
+ @remote.command("request-device")
474
+ @locked
475
+ def request_device_cmd(space:str): cfg=load(); value=request_device(cfg,workspace(cfg,space)); typer.echo(f"Pending device {value['target']['device']['id']}; certificate {value['certificate_hash']}")
476
+ @remote.command("approve-device")
477
+ @locked
478
+ def approve_device_cmd(space:str,device_id:str,reject:bool=typer.Option(False,"--reject")): cfg=load(); typer.echo(json.dumps(approve_device(cfg,workspace(cfg,space),device_id,not reject)))
479
+ @remote.command("approvals")
480
+ def approvals_cmd(space:str): cfg=load(); typer.echo(json.dumps(proposals(cfg,workspace(cfg,space))))
481
+ @remote.command("request-history")
482
+ @locked
483
+ def request_history_cmd(space:str): cfg=load(); value=request_history(cfg,workspace(cfg,space)); typer.echo(f"Pending history approval for {value['target']['device']['id']}")
484
+ @remote.command("approve-history")
485
+ @locked
486
+ def approve_history_cmd(space:str,device_id:str,reject:bool=typer.Option(False,"--reject")): cfg=load(); typer.echo(json.dumps(approve_history(cfg,workspace(cfg,space),device_id,not reject)))
487
+ @remote.command("link")
488
+ @locked
489
+ def link_cmd(path:Path,space:str):
490
+ from ai_convos.cli import repository
491
+ cfg=load(); ws=workspace(cfg,space); state=connect(paths()[2]); resolved=path.resolve(); repo=repository(resolved); kind,value=("repository",repo["id"]) if repo else ("path",digest(os.urandom(32))[:32]); state.execute("INSERT OR REPLACE INTO policies VALUES (?,?,?)",(ws,kind,value)); state.execute("DELETE FROM meta WHERE key LIKE 'core_generation:%'"); state.commit(); kind=="path" and cfg.setdefault("bindings",{}).update({f"{ws}:{value}":str(resolved)}); save(cfg)
492
+ publish(cfg,state,ws,{"kind":"workspace.policy","entity":f"policy:{kind}:{value}","payload":{"kind":kind,"value":value}}); upload(cfg,state); typer.echo(f"{kind} {value} -> {cfg['workspaces'][ws]['name']}")
493
+ @remote.command("sync")
494
+ def sync_cmd(): result=sync_once(force=True); typer.echo("Remote synchronized"+(f"; previous state preserved at {result['backup']}" if result else ""))
495
+ @remote.command("fetch")
496
+ @locked
497
+ def fetch_cmd(event_id:Optional[str]=None):
498
+ state=connect(paths()[2])
499
+ try: typer.echo(f"Fetched {fetch_lazy(load(),state,event_id)} lazy events")
500
+ finally: state.close()
501
+ @remote.command("watch")
502
+ def watch(interval:int=typer.Option(2,"--interval")):
503
+ while True:
504
+ try: sync_once()
505
+ except Exception: paths()[0].mkdir(parents=True,exist_ok=True); (paths()[0]/"last_error").write_text(traceback.format_exc())
506
+ time.sleep(interval)
507
+ @remote.command("enable")
508
+ def enable_cmd(remove:bool=typer.Option(False,"--remove")): not remove and install_hooks(False,False); typer.echo(enable(paths()[0],remove))
509
+ def doctor_status():
510
+ try:
511
+ cfg=load(); info=inspect_state(paths()[2],True)
512
+ try: online="reachable" if health(cfg)["ok"] else "error"
513
+ except Exception: online="error"
514
+ if info["status"]!="current": return f"remote: {online}, state={info['status']}, schema={info['version'] or 'unknown'}, next_sync={'backup+rebaseline' if info['status'] in ('incompatible','invalid') else 'initialize'}"
515
+ state=read_state(paths()[2])
516
+ try: pending=state.execute("SELECT COUNT(*) FROM outbox").fetchone()[0]; lazy=state.execute("SELECT COUNT(*) FROM lazy_events").fetchone()[0]; deferred,required=state.execute("SELECT COUNT(*),COALESCE(SUM(required),0) FROM deferred_events").fetchone(); lifecycle=",".join(f"{r[0][:8]}:{r[1]}" for r in state.execute("SELECT workspace,lifecycle FROM sync_states ORDER BY workspace").fetchall()) or "uninitialized"; last=(state.execute("SELECT value FROM meta WHERE key='last_sync'").fetchone() or ["never"])[0]; backup=(state.execute("SELECT value FROM meta WHERE key='state_cutover'").fetchone() or [None])[0]
517
+ finally: state.close()
518
+ return f"remote: {online}, user={cfg['user'][:8]}, device={cfg['device']['id'][:8]}, workspaces={len(cfg['workspaces'])}, epochs={len(cfg['keys'])}, lifecycle={lifecycle}, pending={pending}, lazy={lazy}, deferred={deferred}, required={required}, last={last}"+(f", backup={json.loads(backup)['backup']}" if backup else "")
519
+ except Exception as e: return f"remote: unavailable ({e})"
520
+ @remote.command("doctor")
521
+ def doctor_cmd(): typer.echo(doctor_status())
522
+ [register(app) for app in _pending]; _pending.clear()