@north-light/cli 0.3.538 → 0.3.539

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/api/contract.d.ts +1 -1
  2. package/dist/api/contract.js +1 -1
  3. package/dist/api/dto/profiles.d.ts +2 -0
  4. package/dist/builtin-memory/crouter-sdk/release-notes.md +8 -0
  5. package/dist/clients/attach/viewer.js +454 -454
  6. package/dist/commands/profile/list.js +1 -1
  7. package/dist/commands/profile/show.js +1 -1
  8. package/dist/commands/sys/join.js +1 -1
  9. package/dist/commands/sys/sync-shared.d.ts +1 -1
  10. package/dist/commands/sys/sync-shared.js +1 -1
  11. package/dist/core/canvas/canvas.d.ts +0 -6
  12. package/dist/core/canvas/canvas.js +17 -22
  13. package/dist/core/canvas/crons.js +10 -10
  14. package/dist/core/canvas/db.js +4 -4
  15. package/dist/core/canvas/export-sqlite.js +1 -1
  16. package/dist/core/canvas/legacy-person.d.ts +4 -0
  17. package/dist/core/canvas/legacy-person.js +1 -0
  18. package/dist/core/canvas/migrations.js +58 -58
  19. package/dist/core/canvas/node-tokens.d.ts +0 -4
  20. package/dist/core/canvas/node-tokens.js +3 -3
  21. package/dist/core/canvas/person.d.ts +39 -0
  22. package/dist/core/canvas/person.js +18 -0
  23. package/dist/core/canvas/schema-pg.sql +11 -0
  24. package/dist/core/directory/join.d.ts +2 -2
  25. package/dist/core/environments.d.ts +10 -0
  26. package/dist/core/environments.js +1 -0
  27. package/dist/core/grants/local.d.ts +4 -4
  28. package/dist/core/grants/local.js +1 -1
  29. package/dist/core/grants/mirror.d.ts +1 -1
  30. package/dist/core/grants/on-sync.js +1 -1
  31. package/dist/core/grants/sync.js +1 -1
  32. package/dist/core/graph/kinds/environment.d.ts +2 -0
  33. package/dist/core/graph/kinds/environment.js +2 -0
  34. package/dist/core/graph/kinds/registry.js +1 -1
  35. package/dist/core/graph/objects.d.ts +1 -1
  36. package/dist/core/graph/objects.js +7 -7
  37. package/dist/core/graph/reader.d.ts +0 -1
  38. package/dist/core/graph/reader.js +3 -3
  39. package/dist/core/layout-migrate/index.js +3 -3
  40. package/dist/core/layout-migrate/stores.js +3 -3
  41. package/dist/core/peer/stubs.js +1 -1
  42. package/dist/core/runs/events.d.ts +0 -2
  43. package/dist/core/runs/events.js +4 -4
  44. package/dist/core/runs/operations.d.ts +0 -3
  45. package/dist/core/runs/operations.js +6 -6
  46. package/dist/core/runtime/close.js +1 -1
  47. package/dist/core/runtime/host.js +3 -3
  48. package/dist/core/runtime/launch-target.d.ts +10 -1
  49. package/dist/core/runtime/launch-target.js +1 -1
  50. package/dist/core/runtime/nodes.js +1 -1
  51. package/dist/core/scoped-state/db.js +2 -2
  52. package/dist/core/scoped-state/profiles.js +1 -1
  53. package/dist/core/scoped-state/schema.d.ts +4 -0
  54. package/dist/core/scoped-state/schema.js +3 -3
  55. package/dist/core/spaces/process.js +1 -1
  56. package/dist/daemon/api/handlers/crons.js +1 -1
  57. package/dist/daemon/api/handlers/hook-exec.js +1 -1
  58. package/dist/daemon/api/handlers/memory.js +5 -5
  59. package/dist/daemon/api/handlers/messages.js +2 -2
  60. package/dist/daemon/api/handlers/nodes.js +1 -1
  61. package/dist/daemon/api/handlers/profiles.js +1 -1
  62. package/dist/daemon/api/reader.js +1 -1
  63. package/dist/daemon/api/router.js +1 -1
  64. package/dist/daemon/cron/armed-context.js +1 -1
  65. package/dist/daemon/cron/authority.d.ts +1 -2
  66. package/dist/daemon/cron/authority.js +1 -1
  67. package/dist/daemon/crtrd.js +12 -12
  68. package/dist/daemon/reconcilers/storage-maintenance.d.ts +3 -0
  69. package/dist/daemon/reconcilers/storage-maintenance.js +1 -1
  70. package/dist/migrations/004-canvas-documents/apply.js +1 -1
  71. package/dist/migrations/004-canvas-documents/index.js +2 -2
  72. package/dist/types.d.ts +2 -0
  73. package/docs/cli/join-northlight.md +1 -1
  74. package/docs/sdk/release-notes.md +8 -0
  75. package/package.json +2 -2
  76. package/packages/crouter-identity/package.json +1 -1
  77. package/runtime.lock.json +12 -12
  78. package/dist/daemon/joined-owner.d.ts +0 -5
  79. package/dist/daemon/joined-owner.js +0 -1
@@ -1,5 +1,5 @@
1
- var g=Object.defineProperty;var r=(e,n)=>g(e,"name",{value:n,configurable:!0});import{createHash as k,randomBytes as m}from"node:crypto";import{openDb as u}from"./db.js";import{terminalGrant as w}from"../grants/mirror.js";import{readConfig as _}from"../config.js";import{uidForApp as N}from"../spaces/users.js";const a=new Map,l=r(e=>k("sha256").update(e).digest("hex"),"hash");async function O(e,n,o,i){const t=await e.prepare("SELECT grantee, sub, grant_id, client_id, scopes_json, run_id FROM node_rows WHERE node_id = ?").get(o);if(!t)throw new Error(`cannot mint node token: missing node ${o}`);const s=t.run_id&&t.run_id!==o?await e.prepare("SELECT sub, grant_id, client_id FROM nodes WHERE node_id = ?").get(t.run_id):void 0,d=t.grantee==="app:terminal"?await w():null,p=d?.sub??s?.sub??t.sub,E=d?.id??s?.grant_id??t.grant_id??null;if(t.grantee==="app:terminal"&&_("user").runtime?.issuer&&!d)throw new Error("cannot mint node token: terminal grant is not active");if(!p&&t.grantee!=="user:local"&&!(t.grantee==="app:terminal"&&!_("user").runtime?.issuer))throw new Error(`cannot mint node token: no subject for ${t.grantee}`);const c=m(32).toString("base64url");await e.prepare('INSERT INTO node_tokens (broker_process_id, node_id, token_hash, grantee, uid, sub, "grant", client_id, scopes_json, issued_at, revoked_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)').run(n,o,l(c),t.grantee==="user:local"?"app:terminal":t.grantee,await N(t.grantee==="user:local"?"app:terminal":t.grantee),p??"user:local",E,s?.client_id??t.client_id,t.scopes_json,i),a.set(n,c)}r(O,"mintNodeToken");function x(e){const n=a.get(e);if(a.delete(e),!n)throw new Error(`missing node token for execution ${e}`);return n}r(x,"takeNodeToken");function T(e){a.delete(e)}r(T,"forgetNodeToken");async function A(e){if(!/^[A-Za-z0-9_-]{43}$/.test(e))return null;const n=await(await u()).prepare(`SELECT t.node_id, t.grantee, t.uid, t.sub, t."grant", t.client_id, t.scopes_json, n.run_id
1
+ var E=Object.defineProperty;var o=(e,n)=>E(e,"name",{value:n,configurable:!0});import{createHash as g,randomBytes as l}from"node:crypto";import{openDb as _}from"./db.js";import{terminalGrant as N}from"../grants/mirror.js";import{personId as w}from"./person.js";import{uidForApp as m}from"../spaces/users.js";const i=new Map,c=o(e=>g("sha256").update(e).digest("hex"),"hash");async function v(e,n,r,a){const t=await e.prepare("SELECT grantee, sub, grant_id, client_id, scopes_json, run_id FROM node_rows WHERE node_id = ?").get(r);if(!t)throw new Error(`cannot mint node token: missing node ${r}`);const s=t.run_id&&t.run_id!==r?await e.prepare("SELECT sub, grant_id, client_id FROM nodes WHERE node_id = ?").get(t.run_id):void 0,d=t.grantee==="app:terminal"?await N():null;if(t.grantee==="app:terminal"&&!d)throw new Error("cannot mint node token: terminal grant is not active");const u=d?.sub??s?.sub??t.sub??w(),k=d?.id??s?.grant_id??t.grant_id??null,p=l(32).toString("base64url");await e.prepare('INSERT INTO node_tokens (broker_process_id, node_id, token_hash, grantee, uid, sub, "grant", client_id, scopes_json, issued_at, revoked_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)').run(n,r,c(p),t.grantee,await m(t.grantee),u,k,s?.client_id??t.client_id,t.scopes_json,a),i.set(n,p)}o(v,"mintNodeToken");function x(e){const n=i.get(e);if(i.delete(e),!n)throw new Error(`missing node token for execution ${e}`);return n}o(x,"takeNodeToken");function f(e){i.delete(e)}o(f,"forgetNodeToken");async function D(e){if(!/^[A-Za-z0-9_-]{43}$/.test(e))return null;const n=await(await _()).prepare(`SELECT t.node_id, t.grantee, t.uid, t.sub, t."grant", t.client_id, t.scopes_json, n.run_id
2
2
  FROM node_tokens t JOIN executions e ON e.execution_id = t.broker_process_id
3
3
  JOIN nodes n ON n.node_id = t.node_id
4
- WHERE t.token_hash = ? AND t.revoked_at IS NULL AND e.state != 'exited'`).get(l(e));return n?{app:n.grantee,node:n.node_id,run:n.run_id,uid:n.uid,sub:n.sub,grant:n.grant,client_id:n.client_id,scopes:n.scopes_json===null?null:JSON.parse(n.scopes_json)}:null}r(A,"resolveNodeToken");async function U(e){T(e),await f(await u(),"broker_process_id",e,new Date().toISOString())}r(U,"revokeExitedNodeToken");async function v(e){await(await u()).prepare(`UPDATE node_tokens SET revoked_at = ? WHERE revoked_at IS NULL
5
- AND broker_process_id IN (SELECT execution_id FROM executions WHERE daemon_epoch IS DISTINCT FROM ?)`).run(new Date().toISOString(),e)}r(v,"revokePriorEpochNodeTokens");async function H(e,n,o){return Number((await e.prepare("UPDATE node_tokens SET sub = ? WHERE sub = 'user:local'").run(n)).changes)+Number((await e.prepare("UPDATE node_tokens SET grantee = 'app:terminal' WHERE grantee = 'user:local'").run()).changes)+Number((await e.prepare('UPDATE node_tokens SET "grant" = NULL WHERE "grant" = ?').run(o)).changes)}r(H,"rewriteLocalNodeTokenIdentity");async function f(e,n,o,i){await e.prepareUncached(`UPDATE node_tokens SET revoked_at = ? WHERE ${n} = ? AND revoked_at IS NULL`).run(i,o)}r(f,"revokeNodeTokens");export{T as forgetNodeToken,O as mintNodeToken,A as resolveNodeToken,U as revokeExitedNodeToken,f as revokeNodeTokens,v as revokePriorEpochNodeTokens,H as rewriteLocalNodeTokenIdentity,x as takeNodeToken};
4
+ WHERE t.token_hash = ? AND t.revoked_at IS NULL AND e.state != 'exited'`).get(c(e));return n?{app:n.grantee,node:n.node_id,run:n.run_id,uid:n.uid,sub:n.sub,grant:n.grant,client_id:n.client_id,scopes:n.scopes_json===null?null:JSON.parse(n.scopes_json)}:null}o(D,"resolveNodeToken");async function A(e){f(e),await S(await _(),"broker_process_id",e,new Date().toISOString())}o(A,"revokeExitedNodeToken");async function U(e){await(await _()).prepare(`UPDATE node_tokens SET revoked_at = ? WHERE revoked_at IS NULL
5
+ AND broker_process_id IN (SELECT execution_id FROM executions WHERE daemon_epoch IS DISTINCT FROM ?)`).run(new Date().toISOString(),e)}o(U,"revokePriorEpochNodeTokens");async function S(e,n,r,a){await e.prepareUncached(`UPDATE node_tokens SET revoked_at = ? WHERE ${n} = ? AND revoked_at IS NULL`).run(a,r)}o(S,"revokeNodeTokens");export{f as forgetNodeToken,v as mintNodeToken,D as resolveNodeToken,A as revokeExitedNodeToken,S as revokeNodeTokens,U as revokePriorEpochNodeTokens,x as takeNodeToken};
@@ -0,0 +1,39 @@
1
+ import { type CanvasDb } from './db.js';
2
+ import { LEGACY_LOCAL_PERSON } from './legacy-person.js';
3
+ export { LEGACY_LOCAL_PERSON };
4
+ /** Thrown by `personId()` before the person is known (a hosted first boot, before its first grant snapshot). */
5
+ export declare class PersonUnresolvedError extends Error {
6
+ readonly code = "person_unresolved";
7
+ constructor();
8
+ }
9
+ export type PersonSource = 'stored' | 'joined' | 'grants' | 'minted';
10
+ /** crtrd sets this once at boot; it wins unconditionally. */
11
+ export declare function setPersonId(id: string): void;
12
+ /** The boot-set person. Throws `PersonUnresolvedError` when unset. Tests and non-daemon processes use `personIdAsync()`. */
13
+ export declare function personId(): string;
14
+ /** Lazy get-or-create keyed by the active canvas database, for processes that are not the daemon. */
15
+ export declare function personIdAsync(): Promise<string>;
16
+ /** Get-or-create the person and say where it came from. Order: stored, the join file's owner, the single `sub` of the
17
+ * mirrored grants (a runtime with an issuer and no join file), else a minted id on a runtime without an issuer.
18
+ * Null when an issuer runtime has no mirrored grants yet; nothing is stored then. */
19
+ export declare function resolvePerson(): Promise<{
20
+ id: string;
21
+ source: PersonSource;
22
+ } | null>;
23
+ export declare function resolvePersonId(): Promise<string | null>;
24
+ type Counts = Record<string, number>;
25
+ /** Replace person `from` with `to` in every known column, inside the caller's `withCanvasWrite` transaction. Idempotent.
26
+ * Rows keyed by the principal merge on collision. Returns rows changed per column. */
27
+ export declare function substitutePerson(db: CanvasDb, from: string, to: string): Promise<Counts>;
28
+ export interface RetireResult {
29
+ person: string;
30
+ /** The join reconcile (part A) ran: the person changed from the stored one to the join file's owner. */
31
+ joinedFrom: string | null;
32
+ /** The legacy retire (part B) ran. */
33
+ retired: boolean;
34
+ /** Rows changed per column (zeros omitted). */
35
+ columns: Counts;
36
+ }
37
+ /** The boot step (flow 2), one transaction. Part A applies a join that landed after the person was minted; part B retires the
38
+ * legacy local person once per person; part C installs the local terminal grant on a runtime without an issuer. */
39
+ export declare function retireLocalPerson(): Promise<RetireResult>;
@@ -0,0 +1,18 @@
1
+ var j=Object.defineProperty;var s=(n,e)=>j(n,"name",{value:e,configurable:!0});import{randomBytes as k}from"node:crypto";import{CrtrError as N}from"../errors.js";import{readConfig as A}from"../config.js";import{joinedOwner as R}from"../directory/join.js";import{installLocalTerminalGrant as U}from"../grants/local.js";import{dropLocalGrantMirror as P}from"../grants/mirror.js";import{ownerForGrantee as C,ensureOwner as H}from"../graph/objects.js";import{canvasDatabaseUrl as x,CanvasConstraintError as W,withCanvasWrite as O}from"./db.js";import{LEGACY_LOCAL_PERSON as c,LEGACY_LOCAL_TERMINAL_GRANT as $}from"./legacy-person.js";const p="person_id",L="local_person_retired",M=/^user:[A-Za-z0-9._-]+$/,v="abcdefghijklmnopqrstuvwxyz0123456789";class T extends Error{static{s(this,"PersonUnresolvedError")}code="person_unresolved";constructor(){super("the install's person is not resolved yet"),this.name="PersonUnresolvedError"}}let E;const I=new Map;function F(n){E=n}s(F,"setPersonId");function ae(){if(E===void 0)throw new T;return E}s(ae,"personId");async function oe(){if(E!==void 0)return E;const n=await x(),e=I.get(n);if(e!==void 0)return e;const t=await G();if(t===null)throw new T;return I.set(n,t),t}s(oe,"personIdAsync");function b(){const n=k(12);let e="user:";for(let t=0;t<12;t++)e+=v[n[t]%v.length];return e}s(b,"mintPersonId");async function m(n,e){return(await n.prepare("SELECT value FROM canvas_meta WHERE key = ?").get(e))?.value??null}s(m,"readMeta");async function l(n,e,t){await n.prepare("INSERT INTO canvas_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(e,t)}s(l,"writeMeta");async function q(){return await O(async n=>{const e=await m(n,p);if(e!==null)return{id:e,source:"stored"};const t=R();if(t!==null)return await l(n,p,t),{id:t,source:"joined"};if(A("user").runtime?.issuer){const r=await n.prepare("SELECT DISTINCT sub FROM grants").all();return r.length!==1?null:(await l(n,p,r[0].sub),{id:r[0].sub,source:"grants"})}const a=b();return await l(n,p,a),{id:a,source:"minted"}})}s(q,"resolvePerson");async function G(){return(await q())?.id??null}s(G,"resolvePersonId");const o=s(n=>Number(n.changes),"changes"),J=[["nodes","sub"],["node_tokens","sub"],["grants","sub"],["crons","sub"],["audit_rows","sub"],["provider_approvals","sub"],["audit_rows","grantee"],["run_events","grantee"],["node_tokens","grantee"],["uploads","grantee"],["provider_receipts","grantee"],["provider_approvals","grantee"]];function D(n,e){if(!M.test(e))throw new N("invalid_person",`${n} must be a user:<id> principal, got ${JSON.stringify(e)}`)}s(D,"assertPerson");async function S(n,e,t){if(D("from",e),D("to",t),e===t)throw new N("invalid_person","cannot substitute a person with itself");const a={};try{a.identities=o(await n.prepare("UPDATE identities SET scheme = 'user', external_id = ? WHERE scheme IN ('local-user', 'user') AND external_id = ?").run(t,e))}catch(r){throw r instanceof W?new N("person_conflict",`another person object already holds ${t}; merging two person objects is not supported (${r.message})`,void 0,{from:e,to:t}):r}a["daemon_actions.principal_sub"]=o(await n.prepare(`UPDATE daemon_actions SET adapter_json = jsonb_set(adapter_json::jsonb, '{principal,sub}', to_jsonb(?::text))::text
2
+ WHERE state IN ('pending', 'handed_off') AND adapter_json::jsonb ->> 'adapter' = 'cron_spawn'
3
+ AND adapter_json::jsonb #>> '{principal,sub}' = ?`).run(t,e));for(const[r,i]of J)a[`${r}.${i}`]=o(await n.prepareUncached(`UPDATE ${r} SET ${i} = ? WHERE ${i} = ?`).run(t,e));return a["idempotency_keys.grantee"]=o(await n.prepare("DELETE FROM idempotency_keys WHERE grantee = ? AND key IN (SELECT key FROM idempotency_keys WHERE grantee = ?)").run(e,t))+o(await n.prepare("UPDATE idempotency_keys SET grantee = ? WHERE grantee = ?").run(t,e)),await n.prepare(`INSERT INTO grantee_usage (grantee, month, usd_equivalent)
4
+ SELECT ?::text, month, usd_equivalent FROM grantee_usage WHERE grantee = ?
5
+ ON CONFLICT(grantee, month) DO UPDATE SET usd_equivalent = grantee_usage.usd_equivalent + excluded.usd_equivalent`).run(t,e),a["grantee_usage.grantee"]=o(await n.prepare("DELETE FROM grantee_usage WHERE grantee = ?").run(e)),await n.prepare(`INSERT INTO usage (grantee, month, payer, source, served_model, input_tokens, output_tokens, cached_tokens, usd_equivalent)
6
+ SELECT ?::text, month, payer, source, served_model, input_tokens, output_tokens, cached_tokens, usd_equivalent FROM usage WHERE grantee = ?
7
+ ON CONFLICT(grantee, month, payer, source, served_model) DO UPDATE SET
8
+ input_tokens = usage.input_tokens + excluded.input_tokens,
9
+ output_tokens = usage.output_tokens + excluded.output_tokens,
10
+ cached_tokens = usage.cached_tokens + excluded.cached_tokens,
11
+ usd_equivalent = CASE WHEN usage.usd_equivalent IS NULL AND excluded.usd_equivalent IS NULL THEN NULL
12
+ ELSE COALESCE(usage.usd_equivalent, 0) + COALESCE(excluded.usd_equivalent, 0) END`).run(t,e),a["usage.grantee"]=o(await n.prepare("DELETE FROM usage WHERE grantee = ?").run(e)),a}s(S,"substitutePerson");async function Y(n){const e=`SELECT o.id FROM objects o JOIN identities i ON i.object = o.owner JOIN nodes n ON n.node_id = o.id
13
+ WHERE o.type = 'node' AND i.scheme = 'local-user' AND i.external_id = '${c}'`;return(await n.prepareUncached(`${e} LIMIT 1`).get()!==void 0?o(await n.prepareUncached(`UPDATE objects SET owner = ? WHERE id IN (${e})`).run((await C(n,"app:terminal")).id)):0)+o(await n.prepare("UPDATE node_tokens SET grantee = 'app:terminal' WHERE grantee = ?").run(c))+o(await n.prepare("UPDATE run_events SET grantee = 'app:terminal' WHERE grantee = ?").run(c))+o(await n.prepare("DELETE FROM idempotency_keys WHERE grantee = ? AND key IN (SELECT key FROM idempotency_keys WHERE grantee = 'app:terminal')").run(c))+o(await n.prepare("UPDATE idempotency_keys SET grantee = 'app:terminal' WHERE grantee = ?").run(c))}s(Y,"rewriteLegacyGrantees");async function z(n,e,t){const a=[$,`terminal:${e}`].filter(d=>d!==t),r=a.map(()=>"?").join(", "),i={};i["nodes.grant_id"]=o(await n.prepareUncached(`UPDATE nodes SET grant_id = ? WHERE grant_id IN (${r})`).run(t,...a)),i["node_tokens.grant"]=o(await n.prepareUncached(`UPDATE node_tokens SET "grant" = ? WHERE "grant" IN (${r})`).run(t,...a)),i["crons.grant_id"]=o(await n.prepareUncached(`UPDATE crons SET grant_id = ? WHERE grant_id IN (${r})`).run(t,...a));const u=t===null?"'null'::jsonb":"to_jsonb(?::text)";return i["daemon_actions.principal_grant_id"]=o(await n.prepareUncached(`UPDATE daemon_actions SET adapter_json = jsonb_set(adapter_json::jsonb, '{principal,grant_id}', ${u})::text
14
+ WHERE state IN ('pending', 'handed_off') AND adapter_json::jsonb ->> 'adapter' = 'cron_spawn'
15
+ AND adapter_json::jsonb #>> '{principal,grant_id}' IN (${r})`).run(...t===null?[]:[t],...a)),i}s(z,"repointGrantReferences");async function se(){const n=await O(async e=>{const t={},a=s((w,g="")=>{for(const[h,y]of Object.entries(w))y!==0&&(t[g+h]=(t[g+h]??0)+y)},"add");let r=await m(e,p);if(r===null)throw new T;let i=null;const u=R();u!==null&&u!==r&&(a(await S(e,r,u),"join."),await l(e,p,u),i=r,r=u);const d=A("user").runtime?.issuer,_=await m(e,"install_id");if(_===null)throw new Error("retireLocalPerson: the install id is not stored");const f=await m(e,L)!==r;if(f){a({legacy_grantees:await Y(e)}),a(await S(e,c,r));const w=`SELECT o.id FROM objects o JOIN peer_stubs s ON s.object = o.id JOIN identities i ON i.object = o.owner
16
+ WHERE o.type = 'node' AND o.reception = 2 AND s.envelope_json ->> 'app' IS NULL AND i.scheme = 'app' AND i.external_id = 'app:terminal'`;if(await e.prepareUncached(`${w} LIMIT 1`).get()!==void 0){const g=await H(e,"person","user",r,"user");a({repaired_stubs:o(await e.prepareUncached(`UPDATE objects SET owner = ? WHERE id IN (${w})`).run(g.id))})}a({"nodes.sub_null_terminal":o(await e.prepare(`UPDATE nodes SET sub = ? WHERE sub IS NULL AND node_id IN (
17
+ SELECT o.id FROM objects o JOIN identities i ON i.object = o.owner
18
+ WHERE o.type = 'node' AND i.scheme = 'app' AND i.external_id = 'app:terminal')`).run(r))}),a(await z(e,_,d?null:`terminal:${_}`)),d&&a({local_grant_mirror:await P(e)}),await l(e,L,r)}return d||await U(_,r),{person:r,joinedFrom:i,retired:f,columns:t}});return F(n.person),n}s(se,"retireLocalPerson");export{c as LEGACY_LOCAL_PERSON,T as PersonUnresolvedError,ae as personId,oe as personIdAsync,q as resolvePerson,G as resolvePersonId,se as retireLocalPerson,F as setPersonId,S as substitutePerson};
@@ -73,6 +73,17 @@ CREATE TABLE jobs (
73
73
  log_path text NOT NULL
74
74
  );
75
75
 
76
+ -- The environments an object can run in (design: Environment module). Added after schema version 1 shipped, so
77
+ -- `ensureSchema` also runs this block alone against a database that predates it; version 1 stays.
78
+ -- environments:begin
79
+ CREATE TABLE IF NOT EXISTS environments (
80
+ object text PRIMARY KEY REFERENCES objects(id) ON DELETE CASCADE,
81
+ kind text NOT NULL CHECK (kind IN ('machine')),
82
+ machine_id text NOT NULL UNIQUE,
83
+ last_seen_at text NOT NULL
84
+ );
85
+ -- environments:end
86
+
76
87
  CREATE TABLE identities (
77
88
  object text PRIMARY KEY REFERENCES objects(id) ON DELETE CASCADE,
78
89
  scheme text NOT NULL,
@@ -42,7 +42,7 @@ export interface JoinDaemonControl {
42
42
  export declare function installedPluginIds(): string[];
43
43
  /** The owner a successful join recorded, or null when `crtr sys join` never
44
44
  * wrote the client file (no file, or one provisioned without join, which has
45
- * no `token_sha256`). The daemon applies OWN-11 to canvas.db from it at the first start under each owner. */
45
+ * no `token_sha256`). The daemon's `retireLocalPerson` makes it the install's person at the next start. */
46
46
  export declare function joinedOwner(): string | null;
47
47
  /** Join this runtime to a directory.
48
48
  *
@@ -50,6 +50,6 @@ export declare function joinedOwner(): string | null;
50
50
  * join lock (so no viewer's redial can autostart a daemon while it is down),
51
51
  * registers with the directory while the daemon still serves, stops the
52
52
  * daemon, writes the identity, and starts the daemon again so its start
53
- * applies the canvas owner rewrite. Without it, a running daemon refuses. */
53
+ * applies the person change to canvas.db. Without it, a running daemon refuses. */
54
54
  export declare function joinDirectory(directory: string, token: string, name?: string, daemon?: JoinDaemonControl): Promise<JoinResult>;
55
55
  export {};
@@ -0,0 +1,10 @@
1
+ /** Find the `environments` row for `installId`, or create its object and row; either way set `last_seen_at`, and cache
2
+ * the environment id and install id for {@link machineEnvironmentId} and {@link touchMachineEnvironment}. */
3
+ export declare function registerMachineEnvironment(installId: string): Promise<{
4
+ environmentId: string;
5
+ created: boolean;
6
+ }>;
7
+ /** Stamp `last_seen_at` on the machine environment: one statement, no read. A no-op before {@link registerMachineEnvironment}. */
8
+ export declare function touchMachineEnvironment(now: string): Promise<void>;
9
+ /** The machine environment's object id; throws before {@link registerMachineEnvironment} has run. */
10
+ export declare function machineEnvironmentId(): string;
@@ -0,0 +1 @@
1
+ var s=Object.defineProperty;var i=(e,r)=>s(e,"name",{value:r,configurable:!0});import{openDb as p,withCanvasWrite as u}from"./canvas/db.js";import{createObject as E,ownerForGrantee as h}from"./graph/objects.js";let t=null;async function f(e){const r=new Date().toISOString(),a=await u(async n=>{const o=await n.prepare("SELECT object FROM environments WHERE machine_id = ?").get(e);if(o!==void 0)return await n.prepare("UPDATE environments SET last_seen_at = ? WHERE machine_id = ?").run(r,e),{environmentId:o.object,created:!1};const m=await h(n,"app:terminal"),c=await E(n,{type:"environment",kind:"machine",owner:m.id,privacy:"public",name:e,preview:"machine"});return await n.prepare("INSERT INTO environments (object, kind, machine_id, last_seen_at) VALUES (?, 'machine', ?, ?)").run(c.id,e,r),{environmentId:c.id,created:!0}});return t={environmentId:a.environmentId,machineId:e},a}i(f,"registerMachineEnvironment");async function _(e){t!==null&&await(await p()).prepare("UPDATE environments SET last_seen_at = ? WHERE machine_id = ?").run(e,t.machineId)}i(_,"touchMachineEnvironment");function I(){if(t===null)throw new Error("the machine environment is not registered yet");return t.environmentId}i(I,"machineEnvironmentId");export{I as machineEnvironmentId,f as registerMachineEnvironment,_ as touchMachineEnvironment};
@@ -1,4 +1,4 @@
1
- /** The unjoined runtime's one grant; nodes created before join reference it by id. */
2
- export declare const LOCAL_TERMINAL_GRANT_ID = "local:terminal";
3
- /** An unjoined local runtime has exactly one grant, owned by its terminal app. */
4
- export declare function installLocalTerminalGrant(): Promise<void>;
1
+ /** A runtime without an issuer has exactly one grant, owned by its terminal app: id `terminal:<install id>`, subject the install's person.
2
+ * Joins the caller's `withCanvasWrite` transaction when one is open. A mirror that already holds exactly that grant is left alone,
3
+ * so a restart changes no grant rows and does not start a new grant generation. */
4
+ export declare function installLocalTerminalGrant(installId: string, person: string): Promise<void>;
@@ -1 +1 @@
1
- var l=Object.defineProperty;var r=(e,t)=>l(e,"name",{value:t,configurable:!0});import{replaceGrants as n}from"./mirror.js";const a=["crtr:llm","crtr:act","crtr:schedule","crtr:memory:rw:user","crtr:memory:manage:user","crtr:files:rw:user:."].map(e=>({scope:e,mode:"always"})),s="local:terminal";async function p(){const e=new Date().toISOString();await n([{id:s,runtime:"runtime:local",sub:"user:local",grantee:"app:terminal",rev:1,scopes:a,resolved_scopes:a,spend_cap:null,expires_at:null,status:"active",revoked_at:null,approved_at:e,payer:"user",dependencies:{},synced_at:e}],e,"local")}r(p,"installLocalTerminalGrant");export{s as LOCAL_TERMINAL_GRANT_ID,p as installLocalTerminalGrant};
1
+ var u=Object.defineProperty;var i=(r,t)=>u(r,"name",{value:t,configurable:!0});import{withCanvasWrite as p}from"../canvas/db.js";import{replaceGrants as m}from"./mirror.js";const a=["crtr:llm","crtr:act","crtr:schedule","crtr:memory:rw:user","crtr:memory:manage:user","crtr:files:rw:user:."].map(r=>({scope:r,mode:"always"}));async function g(r,t){const n=`terminal:${r}`,o=JSON.stringify(a);if(await p(async c=>{const l=await c.prepare("SELECT value FROM runtime_state WHERE key = 'grants_source'").get(),e=await c.prepare("SELECT id, runtime, sub, grantee, status, scopes_json, resolved_scopes_json FROM grants").all();return l?.value==="local"&&e.length===1&&e[0].id===n&&e[0].runtime===r&&e[0].sub===t&&e[0].grantee==="app:terminal"&&e[0].status==="active"&&e[0].scopes_json===o&&e[0].resolved_scopes_json===o}))return;const s=new Date().toISOString();await m([{id:n,runtime:r,sub:t,grantee:"app:terminal",rev:1,scopes:a,resolved_scopes:a,spend_cap:null,expires_at:null,status:"active",revoked_at:null,approved_at:s,payer:"user",dependencies:{},synced_at:s}],s,"local")}i(g,"installLocalTerminalGrant");export{g as installLocalTerminalGrant};
@@ -97,7 +97,7 @@ export declare function terminalGrant(): Promise<ActiveGrant | null>;
97
97
  export declare function grantsSource(): Promise<string | null>;
98
98
  export declare function hasSyncedGrants(source: string): Promise<boolean>;
99
99
  /** OWN-11 for a joined runtime: the unjoined runtime's local mirror grants
100
- * `user:local` full local scopes and is not the directory's. Delete it so no
100
+ * the install's person full local scopes and is not the directory's. Delete it so no
101
101
  * grant exists until grant sync installs the directory's mirror. */
102
102
  export declare function dropLocalGrantMirror(db: CanvasDb): Promise<number>;
103
103
  /** The directory response is complete; commit it or keep the prior snapshot intact. */
@@ -1 +1 @@
1
- var _=Object.defineProperty;var c=(n,r)=>_(n,"name",{value:r,configurable:!0});import{relative as y,sep as w}from"node:path";import{activeBackgroundBashJobs as h,readJobPgid as b}from"../bash-jobs.js";import{appSpace as x}from"../layout.js";import{getNode as l,recordSettledTurn as S}from"../canvas/index.js";import{transition as k}from"../runtime/lifecycle.js";import{openDb as v}from"../canvas/db.js";import{contextDir as E}from"../canvas/node-agent-paths.js";import{emitEvent as p}from"../events/emit.js";import{closeGrantStreams as R}from"../runs/events.js";import{grantedMounts as C,isGrantedSource as G,mountAllowed as I}from"../spaces/mounts.js";import{permissionsForRun as m}from"../spaces/permissions.js";import{stopNodeSandboxes as P}from"../spaces/stop.js";import{discardChangedGrantTokens as O}from"../providers/token-cache.js";import{activeGrantForApp as T,allGrants as A,mirrorOnly as F}from"./mirror.js";import{runRemovalCascades as M}from"./remove.js";import{notifyGrantChanges as D}from"./watch.js";import{launcherPresent as N,listSandboxes as W,stopSandboxPid as j}from"./sandboxes.js";import{checkCronFire as B}from"../../daemon/cron/authority.js";import{cronExecutionFor as J}from"../../daemon/cron/live-runs.js";import{cronExecutionsReady as L}from"../../daemon/cron/run-socket.js";import{flatMapAsync as U}from"../async-array.js";async function g(){return new Map((await(await v()).prepare("SELECT uid, app, status FROM app_users").all()).map(n=>[n.uid,{app:n.app,status:n.status}]))}c(g,"appUserOwners");async function $(n,r){r??=await g();const t=r.get(n.uid),i=n.nodeId.startsWith("cron-"),e=i?J(n.nodeId.slice(5)):void 0;if(i&&(e===void 0||!t))return"permission_revoked";if(e!==void 0&&t.status==="active"){const a=await B({grantee:e.grantee,sub:e.sub,grant_id:e.grantId,scopes:e.scopes});if(!a.allowed)return e.refusal??={reason:a.reason,message:a.message},"permission_revoked"}if(!t||t.app==="app:terminal")return null;if(t.status!=="active")return"app_removed";const o=await l(n.nodeId);try{let a;if(e!==void 0)a=await m(e.grantee,e.sub,e.grantId,e.scopes);else if(o&&o.grantee===t.app)a=await m(t.app,o.sub??"user:local",o.grant_id??null,o.scopes??null);else{const d=await T(t.app);if(!d)return"permission_revoked";a=await m(t.app,d.sub,d.id,null)}const u=C(a),f=x(t.app).root;return n.mounts.every(d=>{const s=y(f,d.source);return s===""||s!==".."&&!s.startsWith(`..${w}`)&&!s.startsWith(w)||!G(t.app,d.source)||I(d,u)})?null:"permission_revoked"}catch{return"permission_revoked"}}c($,"exceeds");async function H(n){const r=await l(n);if(!r)return;(r.status==="active"||r.status==="idle")&&await k(n,"release");const t=new Date().toISOString();await S(n,t),r.run_id&&r.run_id!==n&&await(await v()).prepare("UPDATE nodes SET last_settled_at = COALESCE(last_settled_at, ?) WHERE node_id = ?").run(t,r.run_id)}c(H,"afterRevocationStop");async function q(n){const r=new Set,t=await l(n);t?.pi_pid!=null&&r.add(t.pi_pid);try{const i=await E(n,t?.grantee);for(const e of h(i,!0)){const o=b(i,e.jobId);o!==void 0&&r.add(o)}}catch{}return r}c(q,"nodePids");async function z(){if(!N())return{completion:Promise.resolve()};await L();const n=await W(),r=await g(),t=await U(n,async o=>{const a=await $(o,r);return a?[{sandbox:o,reason:a}]:[]}),i=[],e=new Map;for(const o of t){p({level:"info",event:"daemon.grants.sandbox_stop",fields:{sandbox_node:o.sandbox.nodeId,pid:o.sandbox.pid,grantee:r.get(o.sandbox.uid).app,reason:o.reason}});const a=e.get(o.sandbox.nodeId)??[];a.push(o),e.set(o.sandbox.nodeId,a)}for(const[o,a]of e){const u=await l(o),f=u?await q(o):new Set,d=u!==null&&a.some(({sandbox:s})=>f.has(s.pid));i.push((async()=>{if(d){const s=await P(o,"permission_revoked",{holdRecovery:!0,after:c(()=>H(o),"after")});if(!s.confirmed)throw s.error??new Error(`stop of ${o} was not confirmed`)}for(const{sandbox:s}of a)(!d||!f.has(s.pid))&&await j(s.pid)})().catch(s=>{p({level:"error",event:"daemon.grants.sandbox_stop_failed",...u?{node_id:o}:{fields:{sandbox_node:o}},error:s})}))}return{completion:Promise.all(i).then(()=>{})}}c(z,"sweepSandboxes");function K(n,r){try{const{pairs:t,providers:i,tokens:e}=O(n,r);(t||i)&&p({level:"info",event:"daemon.grants.outbound_tokens_discarded",fields:{grants:t,providers:i,tokens:e}})}catch(t){p({level:"error",event:"daemon.grants.outbound_tokens_discard_failed",error:t})}}c(K,"discardOutboundTokens");async function _e(n,r=t=>{}){const t=await A();try{D(n,t)}catch(e){p({level:"error",event:"daemon.grants.notify_failed",error:e})}K(n,t);for(const e of n){if(e.status!=="active"||t.find(u=>u.id===e.id)?.status==="active")continue;const a=R(e.id);a&&p({level:"info",event:"daemon.grants.streams_closed",fields:{grant:e.id,grantee:e.grantee,streams:a}})}try{const{completion:e}=await z();r(e)}catch(e){p({level:"error",event:"daemon.grants.sweep_failed",error:e})}const i=new Set(t.filter(e=>!F(e)&&e.status==="removed"&&n.find(o=>o.id===e.id)?.status!=="removed").map(e=>e.grantee));r(M(i).catch(()=>{})),r(import("../providers/bundles.js").then(({syncProviderBundles:e})=>e()).catch(e=>{p({level:"error",event:"daemon.providers.bundle_sync_failed",error:e})}))}c(_e,"onGrantSync");export{$ as exceeds,_e as onGrantSync,z as sweepSandboxes};
1
+ var _=Object.defineProperty;var c=(n,r)=>_(n,"name",{value:r,configurable:!0});import{relative as y,sep as w}from"node:path";import{activeBackgroundBashJobs as h,readJobPgid as b}from"../bash-jobs.js";import{appSpace as x}from"../layout.js";import{getNode as l,recordSettledTurn as S}from"../canvas/index.js";import{transition as k}from"../runtime/lifecycle.js";import{openDb as v}from"../canvas/db.js";import{contextDir as E}from"../canvas/node-agent-paths.js";import{emitEvent as p}from"../events/emit.js";import{closeGrantStreams as R}from"../runs/events.js";import{grantedMounts as C,isGrantedSource as I,mountAllowed as G}from"../spaces/mounts.js";import{permissionsForRun as m}from"../spaces/permissions.js";import{stopNodeSandboxes as P}from"../spaces/stop.js";import{discardChangedGrantTokens as O}from"../providers/token-cache.js";import{activeGrantForApp as T,allGrants as A,mirrorOnly as F}from"./mirror.js";import{runRemovalCascades as M}from"./remove.js";import{notifyGrantChanges as D}from"./watch.js";import{launcherPresent as N,listSandboxes as W,stopSandboxPid as j}from"./sandboxes.js";import{checkCronFire as B}from"../../daemon/cron/authority.js";import{cronExecutionFor as J}from"../../daemon/cron/live-runs.js";import{cronExecutionsReady as L}from"../../daemon/cron/run-socket.js";import{flatMapAsync as U}from"../async-array.js";import{personId as $}from"../canvas/person.js";async function g(){return new Map((await(await v()).prepare("SELECT uid, app, status FROM app_users").all()).map(n=>[n.uid,{app:n.app,status:n.status}]))}c(g,"appUserOwners");async function H(n,r){r??=await g();const t=r.get(n.uid),i=n.nodeId.startsWith("cron-"),e=i?J(n.nodeId.slice(5)):void 0;if(i&&(e===void 0||!t))return"permission_revoked";if(e!==void 0&&t.status==="active"){const a=await B({grantee:e.grantee,sub:e.sub,grant_id:e.grantId,scopes:e.scopes});if(!a.allowed)return e.refusal??={reason:a.reason,message:a.message},"permission_revoked"}if(!t||t.app==="app:terminal")return null;if(t.status!=="active")return"app_removed";const o=await l(n.nodeId);try{let a;if(e!==void 0)a=await m(e.grantee,e.sub,e.grantId,e.scopes);else if(o&&o.grantee===t.app)a=await m(t.app,o.sub??$(),o.grant_id??null,o.scopes??null);else{const d=await T(t.app);if(!d)return"permission_revoked";a=await m(t.app,d.sub,d.id,null)}const u=C(a),f=x(t.app).root;return n.mounts.every(d=>{const s=y(f,d.source);return s===""||s!==".."&&!s.startsWith(`..${w}`)&&!s.startsWith(w)||!I(t.app,d.source)||G(d,u)})?null:"permission_revoked"}catch{return"permission_revoked"}}c(H,"exceeds");async function q(n){const r=await l(n);if(!r)return;(r.status==="active"||r.status==="idle")&&await k(n,"release");const t=new Date().toISOString();await S(n,t),r.run_id&&r.run_id!==n&&await(await v()).prepare("UPDATE nodes SET last_settled_at = COALESCE(last_settled_at, ?) WHERE node_id = ?").run(t,r.run_id)}c(q,"afterRevocationStop");async function z(n){const r=new Set,t=await l(n);t?.pi_pid!=null&&r.add(t.pi_pid);try{const i=await E(n,t?.grantee);for(const e of h(i,!0)){const o=b(i,e.jobId);o!==void 0&&r.add(o)}}catch{}return r}c(z,"nodePids");async function K(){if(!N())return{completion:Promise.resolve()};await L();const n=await W(),r=await g(),t=await U(n,async o=>{const a=await H(o,r);return a?[{sandbox:o,reason:a}]:[]}),i=[],e=new Map;for(const o of t){p({level:"info",event:"daemon.grants.sandbox_stop",fields:{sandbox_node:o.sandbox.nodeId,pid:o.sandbox.pid,grantee:r.get(o.sandbox.uid).app,reason:o.reason}});const a=e.get(o.sandbox.nodeId)??[];a.push(o),e.set(o.sandbox.nodeId,a)}for(const[o,a]of e){const u=await l(o),f=u?await z(o):new Set,d=u!==null&&a.some(({sandbox:s})=>f.has(s.pid));i.push((async()=>{if(d){const s=await P(o,"permission_revoked",{holdRecovery:!0,after:c(()=>q(o),"after")});if(!s.confirmed)throw s.error??new Error(`stop of ${o} was not confirmed`)}for(const{sandbox:s}of a)(!d||!f.has(s.pid))&&await j(s.pid)})().catch(s=>{p({level:"error",event:"daemon.grants.sandbox_stop_failed",...u?{node_id:o}:{fields:{sandbox_node:o}},error:s})}))}return{completion:Promise.all(i).then(()=>{})}}c(K,"sweepSandboxes");function Q(n,r){try{const{pairs:t,providers:i,tokens:e}=O(n,r);(t||i)&&p({level:"info",event:"daemon.grants.outbound_tokens_discarded",fields:{grants:t,providers:i,tokens:e}})}catch(t){p({level:"error",event:"daemon.grants.outbound_tokens_discard_failed",error:t})}}c(Q,"discardOutboundTokens");async function he(n,r=t=>{}){const t=await A();try{D(n,t)}catch(e){p({level:"error",event:"daemon.grants.notify_failed",error:e})}Q(n,t);for(const e of n){if(e.status!=="active"||t.find(u=>u.id===e.id)?.status==="active")continue;const a=R(e.id);a&&p({level:"info",event:"daemon.grants.streams_closed",fields:{grant:e.id,grantee:e.grantee,streams:a}})}try{const{completion:e}=await K();r(e)}catch(e){p({level:"error",event:"daemon.grants.sweep_failed",error:e})}const i=new Set(t.filter(e=>!F(e)&&e.status==="removed"&&n.find(o=>o.id===e.id)?.status!=="removed").map(e=>e.grantee));r(M(i).catch(()=>{})),r(import("../providers/bundles.js").then(({syncProviderBundles:e})=>e()).catch(e=>{p({level:"error",event:"daemon.providers.bundle_sync_failed",error:e})}))}c(he,"onGrantSync");export{H as exceeds,he as onGrantSync,K as sweepSandboxes};
@@ -1 +1 @@
1
- var k=Object.defineProperty;var r=(t,n)=>k(t,"name",{value:n,configurable:!0});import{createPrivateKey as v,randomUUID as E,sign as S}from"node:crypto";import{readFileSync as b}from"node:fs";import{join as j}from"node:path";import{crtrHome as x}from"../canvas/paths.js";import{isUnsandboxedMachine as g}from"../runtime/unsandboxed.js";import{emitEvent as f}from"../events/emit.js";import{allGrants as $,dependenciesOf as T,inferenceEndpointOf as A,replaceGrants as N}from"./mirror.js";import{onGrantSync as O}from"./on-sync.js";const H=300;function G(t,n){const e=Math.floor(Date.now()/1e3),s=Buffer.from(JSON.stringify({alg:"ES256",typ:"JWT",kid:t.kid})).toString("base64url"),c=Buffer.from(JSON.stringify({iss:t.client_id,sub:t.client_id,aud:n,iat:e,exp:e+300,jti:E()})).toString("base64url"),o=`${s}.${c}`,i=S("sha256",Buffer.from(o),{key:v({key:t.jwk,format:"jwk"}),dsaEncoding:"ieee-p1363"});return`${o}.${i.toString("base64url")}`}r(G,"assertion");function y(){const t=JSON.parse(b(j(x(),"secrets","runtime-client.json"),"utf8"));if(!t.jwk||t.alg!=="ES256"||!t.kid||!t.client_id||!t.runtime||t.jwk.kid!==t.kid||t.jwk.kty!=="EC"||t.jwk.crv!=="P-256"||!t.jwk.d)throw new TypeError("invalid runtime client key");return t}r(y,"runtimeClient");function K(t=y()){return t.runtime.startsWith("runtime:")?t.runtime.slice(8):t.runtime}r(K,"runtimeId");function R(t){return`This machine was removed from your Northlight account. To use it again, create a join token at ${t.replace(/\/$/,"")}/account/machines, and run \`crtr sys join\` from a plain terminal.`}r(R,"machineRevokedMessage");class h extends Error{static{r(this,"MachineRevokedError")}constructor(n){super(R(n)),this.name="MachineRevokedError"}}async function B(t,n){const e=y(),s=`${t.replace(/\/$/,"")}/token`,c=new URLSearchParams({grant_type:"client_credentials",client_id:e.client_id,client_assertion_type:"urn:ietf:params:oauth:client-assertion-type:jwt-bearer",client_assertion:G(e,s),...n===void 0?{}:{audience:n}}),o=await fetch(s,{method:"POST",body:c,signal:AbortSignal.timeout(1e4)});if(!o.ok)throw o.status===401&&g()&&await D(o)?new h(t):new Error(`directory token request failed: ${o.status}`);const i=await o.json();if(typeof i.access_token!="string")throw new TypeError("directory token response has no access_token");return{access_token:i.access_token,expires_at:J(i.access_token,i.expires_in)}}r(B,"mintDirectoryToken");async function D(t){try{return(await t.json()).error==="invalid_client"}catch{return!1}}r(D,"isInvalidClient");function J(t,n){if(typeof n=="number"&&Number.isFinite(n)&&n>0)return Date.now()+n*1e3;try{const e=JSON.parse(Buffer.from(t.split(".")[1]??"","base64url").toString("utf8"));if(typeof e.exp=="number")return e.exp*1e3}catch{}return Date.now()+3e5}r(J,"tokenExpiry");function M(t,n){if(!t||typeof t!="object")throw new TypeError("invalid grant sync response");const e=t,s=n.runtime.startsWith("runtime:")?n.runtime:`runtime:${n.runtime}`;if(!(e.runtime===s||g()&&typeof e.runtime=="string"&&e.runtime.startsWith("runtime:")&&e.grantee!=="app:terminal")||typeof e.id!="string"||typeof e.sub!="string"||typeof e.grantee!="string"||!Number.isInteger(e.rev)||!Array.isArray(e.scopes)||!Array.isArray(e.resolved_scopes)||!["active","revoked","removed"].includes(e.status??"")||typeof e.synced_at!="string")throw new TypeError("invalid grant sync response");const i=t.revisions?.find(a=>a.rev===e.rev)?.approved_at;if(typeof i!="string")throw new TypeError("grant revision has no approval time");const d=e.payer==="app"?"app":"user";return{...e,spend_cap:e.spend_cap??null,expires_at:e.expires_at??null,revoked_at:e.revoked_at??null,approved_at:i,payer:d,dependencies:T(e.dependencies),inference_endpoint:d==="app"?A(e.inference_endpoint):null,run_notice_url:typeof e.run_notice_url=="string"&&e.run_notice_url!==""?e.run_notice_url:null}}r(M,"responseGrant");let _;function V(t){return _?_(t):Promise.resolve()}r(V,"requestGrantSync");function Y(t,n=e=>{}){let e;const s=t.replace(/\/$/,""),c=r(async()=>{const a=y(),u=await B(s),l=await fetch(`${s}/v1/runtime/grants`,{headers:{Authorization:`Bearer ${u.access_token}`},signal:AbortSignal.timeout(1e4)});if(!l.ok)throw new Error(`directory grant sync failed: ${l.status}`);const p=await l.json();if(!Array.isArray(p.grants)||typeof p.synced_at!="string")throw new TypeError("invalid grant sync response");const w=await $();await N(p.grants.map(m=>M(m,a)),p.synced_at,s),f({level:"info",event:"daemon.grants.synced",fields:{grants:p.grants.length,synced_at:p.synced_at}});try{await O(w,n)}catch(m){f({level:"error",event:"daemon.grants.on_sync_failed",error:m})}},"run");let o;const i=r(()=>{const a=c().catch(u=>{throw u instanceof h?f({level:"error",event:"daemon.grants.machine_revoked",error:u}):f({level:"error",event:"daemon.grants.sync_failed",error:u}),u}).finally(()=>{e===a&&(e=void 0)});return e=a,a},"start"),d=r((a={})=>e?a.fresh?(o??=e.catch(()=>{}).then(()=>(o=void 0,e??i())),o):e:i(),"sync");return _=d,d}r(Y,"createGrantSync");export{H as GRANT_SYNC_INTERVAL_S,h as MachineRevokedError,G as assertion,Y as createGrantSync,R as machineRevokedMessage,B as mintDirectoryToken,V as requestGrantSync,y as runtimeClient,K as runtimeId};
1
+ var w=Object.defineProperty;var t=(e,n)=>w(e,"name",{value:n,configurable:!0});import{createPrivateKey as k,randomUUID as E,sign as S}from"node:crypto";import{readFileSync as b}from"node:fs";import{join as j}from"node:path";import{crtrHome as x}from"../canvas/paths.js";import{isUnsandboxedMachine as g}from"../runtime/unsandboxed.js";import{emitEvent as d}from"../events/emit.js";import{allGrants as $,dependenciesOf as T,inferenceEndpointOf as A,replaceGrants as N}from"./mirror.js";import{onGrantSync as P}from"./on-sync.js";import{PersonUnresolvedError as O,personId as G,resolvePerson as R,retireLocalPerson as B,setPersonId as D}from"../canvas/person.js";const ee=300;function J(e,n){const r=Math.floor(Date.now()/1e3),s=Buffer.from(JSON.stringify({alg:"ES256",typ:"JWT",kid:e.kid})).toString("base64url"),c=Buffer.from(JSON.stringify({iss:e.client_id,sub:e.client_id,aud:n,iat:r,exp:r+300,jti:E()})).toString("base64url"),o=`${s}.${c}`,i=S("sha256",Buffer.from(o),{key:k({key:e.jwk,format:"jwk"}),dsaEncoding:"ieee-p1363"});return`${o}.${i.toString("base64url")}`}t(J,"assertion");function y(){const e=JSON.parse(b(j(x(),"secrets","runtime-client.json"),"utf8"));if(!e.jwk||e.alg!=="ES256"||!e.kid||!e.client_id||!e.runtime||e.jwk.kid!==e.kid||e.jwk.kty!=="EC"||e.jwk.crv!=="P-256"||!e.jwk.d)throw new TypeError("invalid runtime client key");return e}t(y,"runtimeClient");function re(e=y()){return e.runtime.startsWith("runtime:")?e.runtime.slice(8):e.runtime}t(re,"runtimeId");function M(e){return`This machine was removed from your Northlight account. To use it again, create a join token at ${e.replace(/\/$/,"")}/account/machines, and run \`crtr sys join\` from a plain terminal.`}t(M,"machineRevokedMessage");class h extends Error{static{t(this,"MachineRevokedError")}constructor(n){super(M(n)),this.name="MachineRevokedError"}}async function U(e,n){const r=y(),s=`${e.replace(/\/$/,"")}/token`,c=new URLSearchParams({grant_type:"client_credentials",client_id:r.client_id,client_assertion_type:"urn:ietf:params:oauth:client-assertion-type:jwt-bearer",client_assertion:J(r,s),...n===void 0?{}:{audience:n}}),o=await fetch(s,{method:"POST",body:c,signal:AbortSignal.timeout(1e4)});if(!o.ok)throw o.status===401&&g()&&await C(o)?new h(e):new Error(`directory token request failed: ${o.status}`);const i=await o.json();if(typeof i.access_token!="string")throw new TypeError("directory token response has no access_token");return{access_token:i.access_token,expires_at:I(i.access_token,i.expires_in)}}t(U,"mintDirectoryToken");async function C(e){try{return(await e.json()).error==="invalid_client"}catch{return!1}}t(C,"isInvalidClient");function I(e,n){if(typeof n=="number"&&Number.isFinite(n)&&n>0)return Date.now()+n*1e3;try{const r=JSON.parse(Buffer.from(e.split(".")[1]??"","base64url").toString("utf8"));if(typeof r.exp=="number")return r.exp*1e3}catch{}return Date.now()+3e5}t(I,"tokenExpiry");function L(e,n){if(!e||typeof e!="object")throw new TypeError("invalid grant sync response");const r=e,s=n.runtime.startsWith("runtime:")?n.runtime:`runtime:${n.runtime}`;if(!(r.runtime===s||g()&&typeof r.runtime=="string"&&r.runtime.startsWith("runtime:")&&r.grantee!=="app:terminal")||typeof r.id!="string"||typeof r.sub!="string"||typeof r.grantee!="string"||!Number.isInteger(r.rev)||!Array.isArray(r.scopes)||!Array.isArray(r.resolved_scopes)||!["active","revoked","removed"].includes(r.status??"")||typeof r.synced_at!="string")throw new TypeError("invalid grant sync response");const i=e.revisions?.find(a=>a.rev===r.rev)?.approved_at;if(typeof i!="string")throw new TypeError("grant revision has no approval time");const l=r.payer==="app"?"app":"user";return{...r,spend_cap:r.spend_cap??null,expires_at:r.expires_at??null,revoked_at:r.revoked_at??null,approved_at:i,payer:l,dependencies:T(r.dependencies),inference_endpoint:l==="app"?A(r.inference_endpoint):null,run_notice_url:typeof r.run_notice_url=="string"&&r.run_notice_url!==""?r.run_notice_url:null}}t(L,"responseGrant");async function W(){try{G();return}catch(r){if(!(r instanceof O))throw r}const e=await R();if(e===null)return;D(e.id);const n=await B();d({level:"info",event:"daemon.person.resolved",fields:{person_id:e.id,source:e.source}}),d({level:"info",event:"daemon.person.retired",fields:{person_id:n.person,retired:n.retired,columns:n.columns}})}t(W,"resolveLatePerson");let _;function ne(e){return _?_(e):Promise.resolve()}t(ne,"requestGrantSync");function te(e,n=r=>{}){let r;const s=e.replace(/\/$/,""),c=t(async()=>{const a=y(),u=await U(s),f=await fetch(`${s}/v1/runtime/grants`,{headers:{Authorization:`Bearer ${u.access_token}`},signal:AbortSignal.timeout(1e4)});if(!f.ok)throw new Error(`directory grant sync failed: ${f.status}`);const p=await f.json();if(!Array.isArray(p.grants)||typeof p.synced_at!="string")throw new TypeError("invalid grant sync response");const v=await $();await N(p.grants.map(m=>L(m,a)),p.synced_at,s),d({level:"info",event:"daemon.grants.synced",fields:{grants:p.grants.length,synced_at:p.synced_at}}),await W();try{await P(v,n)}catch(m){d({level:"error",event:"daemon.grants.on_sync_failed",error:m})}},"run");let o;const i=t(()=>{const a=c().catch(u=>{throw u instanceof h?d({level:"error",event:"daemon.grants.machine_revoked",error:u}):d({level:"error",event:"daemon.grants.sync_failed",error:u}),u}).finally(()=>{r===a&&(r=void 0)});return r=a,a},"start"),l=t((a={})=>r?a.fresh?(o??=r.catch(()=>{}).then(()=>(o=void 0,r??i())),o):r:i(),"sync");return _=l,l}t(te,"createGrantSync");export{ee as GRANT_SYNC_INTERVAL_S,h as MachineRevokedError,J as assertion,te as createGrantSync,M as machineRevokedMessage,U as mintDirectoryToken,ne as requestGrantSync,y as runtimeClient,re as runtimeId};
@@ -0,0 +1,2 @@
1
+ import { type ObjectKind } from './kind.js';
2
+ export declare const environmentKind: ObjectKind;
@@ -0,0 +1,2 @@
1
+ import{objectDTO as c}from"./dto.js";import{withBase as _}from"./kind.js";import{recordLiveRead as m}from"./live-read.js";import{attr as i,header as h}from"./render.js";const j={name:"environment",addressable:"id",events:_(),async read(a,o,t,r){const n=await a.prepare("SELECT kind, machine_id, last_seen_at FROM environments WHERE object = ?").get(t.id);if(!n)throw new Error(`environment object ${t.id} has no environments row`);await m(a,o,t,r.reposFn);const s=await c(a,t),e={kind:n.kind,machine_id:n.machine_id,last_seen_at:n.last_seen_at},d=[h("object",s,i("machine_id",e.machine_id)+i("last_seen_at",e.last_seen_at)),`Kind: ${e.kind}`,`Machine: ${e.machine_id}`,`Last seen: ${e.last_seen_at}`].join(`
2
+ `);return{data:e,text:d}}};export{j as environmentKind};
@@ -1 +1 @@
1
- var m=Object.defineProperty;var o=(t,n)=>m(t,"name",{value:n,configurable:!0});import{CrtrError as d}from"../../errors.js";import{ExitCode as u}from"../../../types.js";import{documentKind as l}from"./document.js";import{jobKind as w}from"./job.js";import{nodeKind as k}from"./node.js";import{actionKind as O}from"./action.js";import{ownerKinds as j}from"./owner.js";import{customKind as E}from"./custom.js";let f=null;function s(){return f??=new Map([l,k,O,w,...j].map(t=>[t.name,t])),f}o(s,"kinds");const a=new WeakMap;function $(t,n){a.get(t)?.delete(n)}o($,"invalidateCustomKind");async function y(t){return[...s().keys(),...(await t.prepare("SELECT name FROM object_kinds ORDER BY name").all()).map(n=>n.name)]}o(y,"kindNames");async function v(t,n){return s().has(n)||await p(t,n)!==null}o(v,"isKind");async function p(t,n){let e=a.get(t);e||(e=new Map,a.set(t,e));const i=e.get(n);if(i)return i;const r=await t.prepare("SELECT events_json FROM object_kinds WHERE name = ?").get(n);if(!r)return null;const c=E(n,JSON.parse(r.events_json));return e.set(n,c),c}o(p,"kindOfCustom");async function g(t,n){const e=s().get(n)??await p(t,n);if(e==null)throw new d("unknown_kind",`no object kind named ${JSON.stringify(n)}; the kinds are ${(await y(t)).join(", ")}`,u.USAGE,{kind:n});return e}o(g,"kindOf");async function N(t,n,e){const i=(await g(t,n)).events,r=Object.hasOwn(i,e)?i[e]:void 0;if(r===void 0)throw new d("unknown_event",`${n} declares no event ${JSON.stringify(e)}; it declares ${Object.keys(i).join(", ")}`,u.USAGE,{kind:n,event:e,declared:Object.keys(i)});return r}o(N,"eventSpec");export{N as eventSpec,$ as invalidateCustomKind,v as isKind,y as kindNames,g as kindOf};
1
+ var p=Object.defineProperty;var o=(t,n)=>p(t,"name",{value:n,configurable:!0});import{CrtrError as d}from"../../errors.js";import{ExitCode as u}from"../../../types.js";import{documentKind as l}from"./document.js";import{environmentKind as w}from"./environment.js";import{jobKind as k}from"./job.js";import{nodeKind as O}from"./node.js";import{actionKind as j}from"./action.js";import{ownerKinds as E}from"./owner.js";import{customKind as y}from"./custom.js";let f=null;function s(){return f??=new Map([l,O,j,k,w,...E].map(t=>[t.name,t])),f}o(s,"kinds");const a=new WeakMap;function N(t,n){a.get(t)?.delete(n)}o(N,"invalidateCustomKind");async function K(t){return[...s().keys(),...(await t.prepare("SELECT name FROM object_kinds ORDER BY name").all()).map(n=>n.name)]}o(K,"kindNames");async function J(t,n){return s().has(n)||await m(t,n)!==null}o(J,"isKind");async function m(t,n){let e=a.get(t);e||(e=new Map,a.set(t,e));const i=e.get(n);if(i)return i;const r=await t.prepare("SELECT events_json FROM object_kinds WHERE name = ?").get(n);if(!r)return null;const c=y(n,JSON.parse(r.events_json));return e.set(n,c),c}o(m,"kindOfCustom");async function g(t,n){const e=s().get(n)??await m(t,n);if(e==null)throw new d("unknown_kind",`no object kind named ${JSON.stringify(n)}; the kinds are ${(await K(t)).join(", ")}`,u.USAGE,{kind:n});return e}o(g,"kindOf");async function A(t,n,e){const i=(await g(t,n)).events,r=Object.hasOwn(i,e)?i[e]:void 0;if(r===void 0)throw new d("unknown_event",`${n} declares no event ${JSON.stringify(e)}; it declares ${Object.keys(i).join(", ")}`,u.USAGE,{kind:n,event:e,declared:Object.keys(i)});return r}o(A,"eventSpec");export{A as eventSpec,N as invalidateCustomKind,J as isKind,K as kindNames,g as kindOf};
@@ -24,7 +24,7 @@ export declare function ensureOwner(db: CanvasDb, type: OwnerType, scheme: strin
24
24
  * A profile with no object yet has nothing to rename (its object is created from the manifest name
25
25
  * on first use). Returns whether an object was renamed. Links are edges to ids, so they keep resolving. */
26
26
  export declare function renameProfileObject(db: CanvasDb, profileId: string, handle: string): Promise<boolean>;
27
- /** The owner object of a node grantee: `user:local` → person `user`; `app:<x>` → app `<x>`. */
27
+ /** The owner object of a principal: `app:<x>` → app `<x>`; `user:<id>` → person (handle `user` for this install's person, else the id). */
28
28
  export declare function ownerForGrantee(db: CanvasDb, grantee: string): Promise<ObjectRow>;
29
29
  /** The owner's live document named `name`, or null. */
30
30
  export declare function liveDocumentNamed(db: CanvasDb, owner: string, name: string): Promise<ObjectRow | null>;
@@ -1,11 +1,11 @@
1
- var y=Object.defineProperty;var s=(o,e)=>y(o,"name",{value:e,configurable:!0});import{CrtrError as N,notFound as _}from"../errors.js";import{ExitCode as L}from"../../types.js";import{newNodeId as j}from"../runtime/nodes.js";import{kindOf as I}from"./kinds/registry.js";import{canRead as l}from"./access.js";import{bodyOf as A}from"./bodies.js";const b=new Set(["person","app","repo","profile"]);function O(o){return o||null}s(O,"rowFrom");async function T(o,e){return O(await o.prepare("SELECT * FROM objects WHERE id = ?").get(e))}s(T,"getById");async function D(o,e){return await I(o,e.type),await o.prepare(`INSERT INTO objects (id, type, kind, owner, privacy, name, preview, created_at, home_machine)
2
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`).get(e.id??await j(),e.type,e.kind,e.owner,e.privacy,e.name,e.preview,new Date().toISOString(),e.home_machine??null)}s(D,"createObject");async function R(o,e,t,r,n,i){if(!b.has(e))throw new Error(`ensureOwner: ${e} is not an owner type`);const d=await o.prepare(`SELECT o.* FROM identities i JOIN objects o ON o.id = i.object
1
+ var y=Object.defineProperty;var s=(o,e)=>y(o,"name",{value:e,configurable:!0});import{CrtrError as N,notFound as _}from"../errors.js";import{ExitCode as L}from"../../types.js";import{newNodeId as I}from"../runtime/nodes.js";import{kindOf as j}from"./kinds/registry.js";import{canRead as h}from"./access.js";import{bodyOf as A}from"./bodies.js";import{personId as b}from"../canvas/person.js";const T=new Set(["person","app","repo","profile"]);function O(o){return o||null}s(O,"rowFrom");async function D(o,e){return O(await o.prepare("SELECT * FROM objects WHERE id = ?").get(e))}s(D,"getById");async function x(o,e){return await j(o,e.type),await o.prepare(`INSERT INTO objects (id, type, kind, owner, privacy, name, preview, created_at, home_machine)
2
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`).get(e.id??await I(),e.type,e.kind,e.owner,e.privacy,e.name,e.preview,new Date().toISOString(),e.home_machine??null)}s(x,"createObject");async function R(o,e,t,r,n,i){if(!T.has(e))throw new Error(`ensureOwner: ${e} is not an owner type`);const d=await o.prepare(`SELECT o.* FROM identities i JOIN objects o ON o.id = i.object
3
3
  WHERE i.scheme = ? AND i.external_id = ?`).get(t,r);if(d)return d;const c=await o.prepare(`SELECT o.id, o.type, i.scheme, i.external_id FROM objects o
4
4
  LEFT JOIN identities i ON i.object = o.id
5
- WHERE o.name = ? AND o.type = ? AND o.home_machine IS NULL`).get(n,e);if(c)throw new N("conflict",`handle "${n}" is held by ${c.type} ${c.id} (${c.scheme}:${c.external_id}); cannot give it to ${e} ${t}:${r}`,L.GENERAL,{handle:n,holder:c.id});const p=await D(o,{type:e,kind:null,owner:e==="profile"?i?.id??null:null,privacy:"public",name:n,preview:""});return await o.prepare("INSERT INTO identities (object, scheme, external_id) VALUES (?, ?, ?)").run(p.id,t,r),p}s(R,"ensureOwner");async function M(o,e,t){const r=await o.prepare(`SELECT o.* FROM identities i JOIN objects o ON o.id = i.object
5
+ WHERE o.name = ? AND o.type = ? AND o.home_machine IS NULL`).get(n,e);if(c)throw new N("conflict",`handle "${n}" is held by ${c.type} ${c.id} (${c.scheme}:${c.external_id}); cannot give it to ${e} ${t}:${r}`,L.GENERAL,{handle:n,holder:c.id});const p=await x(o,{type:e,kind:null,owner:e==="profile"?i?.id??null:null,privacy:"public",name:n,preview:""});return await o.prepare("INSERT INTO identities (object, scheme, external_id) VALUES (?, ?, ?)").run(p.id,t,r),p}s(R,"ensureOwner");async function P(o,e,t){const r=await o.prepare(`SELECT o.* FROM identities i JOIN objects o ON o.id = i.object
6
6
  WHERE i.scheme = 'profile' AND i.external_id = ? AND o.type = 'profile' AND o.deleted_at IS NULL`).get(e),n=await o.prepare(`SELECT o.id FROM objects o
7
- WHERE o.name = ? AND o.type = 'profile' AND o.deleted_at IS NULL AND o.home_machine IS NULL AND o.id <> ?`).get(t,r?r.id:"");if(n)throw new N("conflict",`handle "${t}" is already held by profile object ${n.id}; cannot rename profile ${e} to it`,L.GENERAL,{handle:t,holder:n.id});if(!r)return!1;const i=r;return i.name===t?!1:(await x(o,i,t),!0)}s(M,"renameProfileObject");async function k(o,e){if(e==="user:local")return await R(o,"person","local-user",e,"user");if(/^app:[a-z0-9][a-z0-9._-]*$/.test(e))return await R(o,"app","app",e,e.slice(4));throw new Error(`node grantee ${JSON.stringify(e)} is neither app:<name> nor user:local`)}s(k,"ownerForGrantee");async function P(o,e,t){return O(await o.prepare("SELECT * FROM objects WHERE (type = 'document' OR strpos(type, '.') > 0) AND owner = ? AND name = ? AND deleted_at IS NULL AND home_machine IS NULL").get(e,t))}s(P,"liveDocumentNamed");async function J(o,e){const t=await T(o,e);if(!t||t.type!=="node")throw _(`no node object ${e}`);return t}s(J,"nodeObject");async function x(o,e,t){await o.prepare("UPDATE objects SET name = ? WHERE id = ?").run(t,e.id)}s(x,"setObjectName");async function B(o,e,t){await o.prepare("UPDATE objects SET kind = ? WHERE id = ?").run(t,e.id)}s(B,"setObjectKind");async function G(o,e,t,r={}){const n=["seq > ?"],i=[r.after_seq??0];return t.type!==void 0&&(n.push("type = ?"),i.push(t.type)),t.kind!==void 0&&(n.push("kind = ?"),i.push(t.kind)),t.owner!==void 0&&(n.push("owner = ?"),i.push(t.owner.id)),t.privacy!==void 0&&(n.push("privacy = ?"),i.push(t.privacy)),t.name!==void 0&&(n.push("name = ?"),i.push(t.name)),n.push("home_machine IS NULL"),n.push(t.deleted?"deleted_at IS NOT NULL":"deleted_at IS NULL"),await $(o,e,`SELECT * FROM objects WHERE ${n.join(" AND ")}`,i,Math.min(r.limit??100,100))}s(G,"listObjects");async function K(o,e,t){const n=[],i=`%${t.text.replace(/[\\%_]/g,a=>`\\${a}`)}%`;if(!t.grep){const a=await o.prepare(`SELECT o.*, d.summary AS _summary FROM objects o LEFT JOIN documents d ON d.object = o.id
7
+ WHERE o.name = ? AND o.type = 'profile' AND o.deleted_at IS NULL AND o.home_machine IS NULL AND o.id <> ?`).get(t,r?r.id:"");if(n)throw new N("conflict",`handle "${t}" is already held by profile object ${n.id}; cannot rename profile ${e} to it`,L.GENERAL,{handle:t,holder:n.id});if(!r)return!1;const i=r;return i.name===t?!1:(await $(o,i,t),!0)}s(P,"renameProfileObject");async function J(o,e){if(/^app:[a-z0-9][a-z0-9._-]*$/.test(e))return await R(o,"app","app",e,e.slice(4));if(/^user:[a-z0-9][a-z0-9._-]*$/.test(e))return await R(o,"person","user",e,e===b()?"user":e.slice(5));throw new Error(`principal ${JSON.stringify(e)} is neither app:<name> nor user:<id>`)}s(J,"ownerForGrantee");async function z(o,e,t){return O(await o.prepare("SELECT * FROM objects WHERE (type = 'document' OR strpos(type, '.') > 0) AND owner = ? AND name = ? AND deleted_at IS NULL AND home_machine IS NULL").get(e,t))}s(z,"liveDocumentNamed");async function B(o,e){const t=await D(o,e);if(!t||t.type!=="node")throw _(`no node object ${e}`);return t}s(B,"nodeObject");async function $(o,e,t){await o.prepare("UPDATE objects SET name = ? WHERE id = ?").run(t,e.id)}s($,"setObjectName");async function G(o,e,t){await o.prepare("UPDATE objects SET kind = ? WHERE id = ?").run(t,e.id)}s(G,"setObjectKind");async function K(o,e,t,r={}){const n=["seq > ?"],i=[r.after_seq??0];return t.type!==void 0&&(n.push("type = ?"),i.push(t.type)),t.kind!==void 0&&(n.push("kind = ?"),i.push(t.kind)),t.owner!==void 0&&(n.push("owner = ?"),i.push(t.owner.id)),t.privacy!==void 0&&(n.push("privacy = ?"),i.push(t.privacy)),t.name!==void 0&&(n.push("name = ?"),i.push(t.name)),n.push("home_machine IS NULL"),n.push(t.deleted?"deleted_at IS NOT NULL":"deleted_at IS NULL"),await C(o,e,`SELECT * FROM objects WHERE ${n.join(" AND ")}`,i,Math.min(r.limit??100,100))}s(K,"listObjects");async function Y(o,e,t){const n=[],i=`%${t.text.replace(/[\\%_]/g,a=>`\\${a}`)}%`;if(!t.grep){const a=await o.prepare(`SELECT o.*, d.summary AS _summary FROM objects o LEFT JOIN documents d ON d.object = o.id
8
8
  WHERE o.deleted_at IS NULL AND o.home_machine IS NULL AND (o.name ILIKE ?1 ESCAPE '\\' OR o.preview ILIKE ?1 ESCAPE '\\' OR d.summary ILIKE ?1 ESCAPE '\\')
9
- ORDER BY o.seq`).all(i),u=t.text.toLowerCase();for(const f of a){const{_summary:m,...E}=f,w=E;if(!await l(o,e,w))continue;const h=w.name.toLowerCase().includes(u)?"name":w.preview.toLowerCase().includes(u)?"preview":"summary";if(n.push({object:w,field:h,...h==="summary"?{text:String(m)}:{}}),n.length>=100)return n}}if(!t.body&&!t.grep)return n;const d=new Set(n.map(a=>a.object.id)),c=t.grep?(a=>u=>a.test(u))(new RegExp(t.text)):a=>a.includes(t.text),p=await o.prepare("SELECT * FROM objects WHERE type = 'document' AND deleted_at IS NULL ORDER BY seq").all(),S=o.prepare(`SELECT r.body_hash, d.source_path FROM revisions r JOIN documents d ON d.object = r.document WHERE r.document = ?
10
- AND NOT EXISTS (SELECT 1 FROM revision_parents p WHERE p.parent = r.rev_id)`);for(const a of p)if(!(d.has(a.id)||!await l(o,e,a)))for(const{body_hash:u,source_path:f}of await S.all(a.id)){const m=A(u,f).split(`
11
- `),E=m.findIndex(c);if(!(E<0)){if(n.push({object:a,field:"body",line:E+1,text:m[E]}),n.length>=100)return n;break}}return n}s(K,"searchObjects");async function $(o,e,t,r,n){const i=[],d=o.prepareUncached(`${t} ORDER BY seq LIMIT ${n*4}`);for(;;){const c=await d.all(...r);for(const p of c)if(await l(o,e,p)&&i.push(p),i.length===n)return i;if(c.length<n*4)return i;r[0]=c[c.length-1].seq}}s($,"readablePages");export{D as createObject,R as ensureOwner,T as getById,G as listObjects,P as liveDocumentNamed,J as nodeObject,k as ownerForGrantee,M as renameProfileObject,K as searchObjects,B as setObjectKind,x as setObjectName};
9
+ ORDER BY o.seq`).all(i),u=t.text.toLowerCase();for(const f of a){const{_summary:m,...E}=f,w=E;if(!await h(o,e,w))continue;const l=w.name.toLowerCase().includes(u)?"name":w.preview.toLowerCase().includes(u)?"preview":"summary";if(n.push({object:w,field:l,...l==="summary"?{text:String(m)}:{}}),n.length>=100)return n}}if(!t.body&&!t.grep)return n;const d=new Set(n.map(a=>a.object.id)),c=t.grep?(a=>u=>a.test(u))(new RegExp(t.text)):a=>a.includes(t.text),p=await o.prepare("SELECT * FROM objects WHERE type = 'document' AND deleted_at IS NULL ORDER BY seq").all(),S=o.prepare(`SELECT r.body_hash, d.source_path FROM revisions r JOIN documents d ON d.object = r.document WHERE r.document = ?
10
+ AND NOT EXISTS (SELECT 1 FROM revision_parents p WHERE p.parent = r.rev_id)`);for(const a of p)if(!(d.has(a.id)||!await h(o,e,a)))for(const{body_hash:u,source_path:f}of await S.all(a.id)){const m=A(u,f).split(`
11
+ `),E=m.findIndex(c);if(!(E<0)){if(n.push({object:a,field:"body",line:E+1,text:m[E]}),n.length>=100)return n;break}}return n}s(Y,"searchObjects");async function C(o,e,t,r,n){const i=[],d=o.prepareUncached(`${t} ORDER BY seq LIMIT ${n*4}`);for(;;){const c=await d.all(...r);for(const p of c)if(await h(o,e,p)&&i.push(p),i.length===n)return i;if(c.length<n*4)return i;r[0]=c[c.length-1].seq}}s(C,"readablePages");export{x as createObject,R as ensureOwner,D as getById,K as listObjects,z as liveDocumentNamed,B as nodeObject,J as ownerForGrantee,P as renameProfileObject,Y as searchObjects,G as setObjectKind,$ as setObjectName};
@@ -30,7 +30,6 @@ export declare function appScopesNow(db: CanvasDb, app: ObjectRow, lookups: Read
30
30
  export interface ReaderLookups {
31
31
  grantById(grantId: string): Promise<ActiveGrant | null>;
32
32
  grantFor(app: string, sub: string): Promise<ActiveGrant | null>;
33
- terminalHasIssuer(): boolean;
34
33
  /** The scopes of the `crtr_app_` token a local plugin's app (`app:<plugin>`) holds, or null when it holds none. */
35
34
  pluginScopes(grantee: string): readonly string[] | null;
36
35
  object(id: string): Promise<ObjectRow | null>;
@@ -1,3 +1,3 @@
1
- var F=Object.defineProperty;var p=(o,e)=>F(o,"name",{value:e,configurable:!0});import{coveredByAny as f,isSandboxedNode as x}from"../../shared/env.js";import{existsSync as H}from"node:fs";import{CrtrError as C}from"../errors.js";import{getStoredProfile as L}from"../scoped-state/profiles.js";import{userScopeRoot as A}from"../scope.js";import{parseScope as O}from"@north-light/identity";import{activeGrant as R,grantFor as v}from"../grants/mirror.js";import{ApiError as b}from"../../api/errors.js";import{readConfig as $}from"../config.js";import{pluginAppToken as M}from"../secrets.js";import{SCOPE_VOCABULARY as d}from"../scopes.js";import{loadExactProfileManifest as T}from"../profiles/manifest.js";import{ensureOwner as _,getById as W,nodeObject as D}from"./objects.js";async function y(o,e,t,a){const r=e===null?[]:a.projectPaths(e),n=await t(o),l=new Set(n.map(s=>s.repo.id)),c=[];for(const s of r){if(!H(s))continue;let i;try{i=await t(s)}catch(u){if(u instanceof C&&u.code==="git-lookup-failed")continue;throw u}for(const u of i)l.has(u.repo.id)||(l.add(u.repo.id),c.push(u))}return{repos:n.map(s=>s.repo),checkouts:n,projectRepos:c.map(s=>s.repo),projectCheckouts:c}}p(y,"viewRepos");async function re(o,e,t){if(e.class==="anonymous")throw new Error("a publish token cannot read as a principal");if(e.class==="person")return{class:"person",person:await E(o)};if(e.class==="app")return{class:"app",app:await g(o,e.app),scopes:e.scopes};if(e.class==="machine")throw new Error("a machine token cannot read as a principal");if(e.class==="forwarded"){if(e.actor.kind==="person")return{class:"person",person:await E(o)};const c=await v(e.actor.app,e.sub);if(!c)throw new b(403,"app_not_granted",`${e.actor.app} holds no active grant for ${e.sub} on this machine`);const s=e.actor.node_scopes;return{class:"app",app:await g(o,e.actor.app),place:e.actor.place,launched:s,scopes:s===null?c.resolved:s.filter(i=>f(c.resolved,i))}}if(e.class==="cron"){const c=e.grant?(await R(e.grant))?.resolved??[]:d,s=e.scopes;return{class:"app",app:await g(o,e.app),scopes:s===null?c:s.filter(i=>f(c,i)),launched:s,place:{run:e.origin_run??"",isolated:e.isolated}}}const a=m(o),r=await D(o,e.node),n=r.owner===null?null:await a.object(r.owner);if(!n)throw new Error(`node ${e.node} has no owner`);const l=await o.prepare("SELECT cwd, profile_id FROM nodes WHERE node_id = ?").get(e.node);return{class:"node",node:r,app:n,scopes:await N(e.app,e.sub,e.grant,e.scopes,a),launched:e.scopes,cwd:l.cwd,...await y(l.cwd,l.profile_id,t,a),profile:l.profile_id===null?null:await a.profile(l.profile_id,n)}}p(re,"readerFor");async function B(o,e,t,a=m(o),r=!1){const n=await o.prepare("SELECT o.* FROM objects o WHERE o.id = ? AND o.type = 'node'").get(e),l=await o.prepare(`SELECT (SELECT external_id FROM identities WHERE object = ?) AS grantee,
2
- sub, grant_id, scopes_json, cwd, profile_id FROM nodes WHERE node_id = ?`).get(n?.owner??null,e);if(!n||!l)throw new Error(`no node ${e}`);const{grantee:c,sub:s,grant_id:i,scopes_json:u,cwd:w,profile_id:j}=l,h=n.owner===null?null:await a.object(n.owner);if(!h)throw new Error(`node ${e} has no owner`);const S=u===null?null:JSON.parse(u);return{class:"node",node:n,app:h,scopes:await N(c,s??"",i,S,a,r),launched:S,cwd:w,...await y(w,j,t,a),profile:j===null?null:await a.profile(j,h)}}p(B,"readerForNode");async function ne(o,e,t=async()=>[],a=m(o)){const r=await o.prepare("SELECT envelope_json FROM peer_stubs WHERE object = ?").get(e.id);if(r!==void 0)return await P(o,e,r.envelope_json===null?null:JSON.parse(r.envelope_json),a);if(e.type==="node")return await B(o,e.id,t,a,!0);const n=e.owner===null?null:await a.object(e.owner);if(!n)throw new Error(`watcher ${e.id} has no owner`);const c=(n.type==="app"?n:n.type==="profile"&&n.owner!==null?await a.object(n.owner):null)??await o.prepare("SELECT o.* FROM identities i JOIN objects o ON o.id = i.object WHERE i.external_id = 'app:terminal' AND i.scheme = 'app'").get()??await o.prepare("SELECT * FROM objects WHERE type = 'person' AND name = 'user' AND home_machine IS NULL").get();if(!c)throw new Error("no terminal app or person object");const s=(await o.prepare("SELECT external_id FROM identities WHERE object = ? AND scheme = 'app'").get(c.id))?.external_id??"app:terminal",i=await o.prepare("SELECT resolved_scopes_json FROM grants WHERE grantee = ? AND status = 'active'").all(s),u=n.type==="person"?null:i.length===0?s==="app:terminal"&&!a.terminalHasIssuer()?d:a.pluginScopes(s):i.length===1?JSON.parse(i[0].resolved_scopes_json).filter(w=>w.mode==="always").map(w=>O(w.scope)):null;return{class:"object",object:e,node:e,owner:n,app:c,scopes:u,repos:n.type==="repo"?[n]:[]}}p(ne,"readerForObject");async function te(o,e,t){const a=(await o.prepare("SELECT external_id FROM identities WHERE object = ? AND scheme = 'app'").get(e.id))?.external_id;if(a===void 0)return null;const r=await o.prepare("SELECT resolved_scopes_json FROM grants WHERE grantee = ? AND status = 'active'").all(a);return r.length===0?a==="app:terminal"&&!t.terminalHasIssuer()?d:t.pluginScopes(a):r.length>1?null:JSON.parse(r[0].resolved_scopes_json).filter(n=>n.mode==="always").map(n=>O(n.scope))}p(te,"appScopesNow");async function P(o,e,t,a){const r=await E(o);if(t!==null&&t.app===null)return{class:"object",object:e,node:e,owner:r,app:r,scopes:null,repos:[]};const n=t?.app==null?null:await g(o,t.app),l=t?.app==null?null:await a.grantFor(t.app,t.sub),c=t===null||l===null?null:t.node_scopes===null?l.resolved:t.node_scopes.filter(i=>f(l.resolved,i)),s=n??(e.owner===null?r:await a.object(e.owner)??r);return{class:"object",object:e,node:e,owner:s,app:s,scopes:c,launched:t?.node_scopes??null,repos:[],...t?.place==null?{}:{place:t.place}}}p(P,"readerForStub");function m(o){return{grantById:R,grantFor:v,terminalHasIssuer:p(()=>!!$("user").runtime?.issuer,"terminalHasIssuer"),pluginScopes:p(e=>M(e.replace(/^app:/,""))?.scopes??null,"pluginScopes"),object:p(async e=>await W(o,e),"object"),profile:p(async(e,t)=>await I(o,e,t),"profile"),projectPaths:p(e=>x()?[]:L(A(),e)?.manifest.projects.map(t=>t.path)??[],"projectPaths")}}p(m,"liveLookups");function ae(o){const e=m(o),t=new Map,a=p((r,n)=>(t.has(r)||t.set(r,n()),t.get(r)),"memo");return{grantById:p(r=>a(`g\0${r}`,()=>e.grantById(r)),"grantById"),grantFor:p((r,n)=>a(`a\0${r}\0${n}`,()=>e.grantFor(r,n)),"grantFor"),terminalHasIssuer:p(()=>a("issuer",()=>e.terminalHasIssuer()),"terminalHasIssuer"),pluginScopes:p(r=>a(`t\0${r}`,()=>e.pluginScopes(r)),"pluginScopes"),object:p(async r=>await a(`o\0${r}`,async()=>await e.object(r)),"object"),profile:p(async(r,n)=>await a(`p\0${r}\0${n.id}`,async()=>await e.profile(r,n)),"profile"),projectPaths:p(r=>a(`j\0${r}`,()=>e.projectPaths(r)),"projectPaths")}}p(ae,"publishLookups");async function N(o,e,t,a,r,n=!1){const l=t?await r.grantById(t):await r.grantFor(o,e),c=l&&l.grantee===o?l.resolved:o==="app:terminal"&&!r.terminalHasIssuer()?d:null;if(c===null){if(n)return null;throw new Error(`the grant of ${o} is not active`)}return a===null?c:a.filter(s=>f(c,s))}p(N,"nodeScopes");async function E(o){return await _(o,"person","local-user","user:local","user")}p(E,"personObject");async function g(o,e){return e==="user:local"?await E(o):await _(o,"app","app",e,e.replace(/^app:/,""))}p(g,"appObject");async function I(o,e,t){return await o.prepare(`SELECT o.* FROM identities i JOIN objects o ON o.id = i.object
3
- WHERE i.scheme = 'profile' AND i.external_id = ?`).get(e)??await _(o,"profile","profile",e,T(e).manifest.name,t)}p(I,"profileObject");export{te as appScopesNow,m as liveLookups,I as profileObject,ae as publishLookups,re as readerFor,B as readerForNode,ne as readerForObject,y as viewRepos};
1
+ var F=Object.defineProperty;var p=(o,e)=>F(o,"name",{value:e,configurable:!0});import{coveredByAny as f,isSandboxedNode as N}from"../../shared/env.js";import{existsSync as x}from"node:fs";import{CrtrError as b}from"../errors.js";import{getStoredProfile as L}from"../scoped-state/profiles.js";import{userScopeRoot as $}from"../scope.js";import{parseScope as _}from"@north-light/identity";import{activeGrant as S,grantFor as R}from"../grants/mirror.js";import{ApiError as A}from"../../api/errors.js";import{pluginAppToken as C}from"../secrets.js";import{loadExactProfileManifest as M}from"../profiles/manifest.js";import{ensureOwner as O,getById as T,nodeObject as H,ownerForGrantee as W}from"./objects.js";import{personId as D}from"../canvas/person.js";async function v(o,e,r,a){const n=e===null?[]:a.projectPaths(e),t=await r(o),l=new Set(t.map(s=>s.repo.id)),c=[];for(const s of n){if(!x(s))continue;let i;try{i=await r(s)}catch(u){if(u instanceof b&&u.code==="git-lookup-failed")continue;throw u}for(const u of i)l.has(u.repo.id)||(l.add(u.repo.id),c.push(u))}return{repos:t.map(s=>s.repo),checkouts:t,projectRepos:c.map(s=>s.repo),projectCheckouts:c}}p(v,"viewRepos");async function oe(o,e,r){if(e.class==="anonymous")throw new Error("a publish token cannot read as a principal");if(e.class==="person")return{class:"person",person:await j(o)};if(e.class==="app")return{class:"app",app:await E(o,e.app),scopes:e.scopes};if(e.class==="machine")throw new Error("a machine token cannot read as a principal");if(e.class==="forwarded"){if(e.actor.kind==="person")return{class:"person",person:await j(o)};const c=await R(e.actor.app,e.sub);if(!c)throw new A(403,"app_not_granted",`${e.actor.app} holds no active grant for ${e.sub} on this machine`);const s=e.actor.node_scopes;return{class:"app",app:await E(o,e.actor.app),place:e.actor.place,launched:s,scopes:s===null?c.resolved:s.filter(i=>f(c.resolved,i))}}if(e.class==="cron"){const c=e.grant?(await S(e.grant))?.resolved??[]:[],s=e.scopes;return{class:"app",app:await E(o,e.app),scopes:s===null?c:s.filter(i=>f(c,i)),launched:s,place:{run:e.origin_run??"",isolated:e.isolated}}}const a=d(o),n=await H(o,e.node),t=n.owner===null?null:await a.object(n.owner);if(!t)throw new Error(`node ${e.node} has no owner`);const l=await o.prepare("SELECT cwd, profile_id FROM nodes WHERE node_id = ?").get(e.node);return{class:"node",node:n,app:t,scopes:await y(e.app,e.sub,e.grant,e.scopes,a),launched:e.scopes,cwd:l.cwd,...await v(l.cwd,l.profile_id,r,a),profile:l.profile_id===null?null:await a.profile(l.profile_id,t)}}p(oe,"readerFor");async function P(o,e,r,a=d(o),n=!1){const t=await o.prepare("SELECT o.* FROM objects o WHERE o.id = ? AND o.type = 'node'").get(e),l=await o.prepare(`SELECT (SELECT external_id FROM identities WHERE object = ?) AS grantee,
2
+ sub, grant_id, scopes_json, cwd, profile_id FROM nodes WHERE node_id = ?`).get(t?.owner??null,e);if(!t||!l)throw new Error(`no node ${e}`);const{grantee:c,sub:s,grant_id:i,scopes_json:u,cwd:w,profile_id:g}=l,m=t.owner===null?null:await a.object(t.owner);if(!m)throw new Error(`node ${e} has no owner`);const h=u===null?null:JSON.parse(u);return{class:"node",node:t,app:m,scopes:await y(c,s??"",i,h,a,n),launched:h,cwd:w,...await v(w,g,r,a),profile:g===null?null:await a.profile(g,m)}}p(P,"readerForNode");async function ne(o,e,r=async()=>[],a=d(o)){const n=await o.prepare("SELECT envelope_json FROM peer_stubs WHERE object = ?").get(e.id);if(n!==void 0)return await B(o,e,n.envelope_json===null?null:JSON.parse(n.envelope_json),a);if(e.type==="node")return await P(o,e.id,r,a,!0);const t=e.owner===null?null:await a.object(e.owner);if(!t)throw new Error(`watcher ${e.id} has no owner`);const c=(t.type==="app"?t:t.type==="profile"&&t.owner!==null?await a.object(t.owner):null)??await o.prepare("SELECT o.* FROM identities i JOIN objects o ON o.id = i.object WHERE i.external_id = 'app:terminal' AND i.scheme = 'app'").get()??await o.prepare("SELECT * FROM objects WHERE type = 'person' AND name = 'user' AND home_machine IS NULL").get();if(!c)throw new Error("no terminal app or person object");const s=(await o.prepare("SELECT external_id FROM identities WHERE object = ? AND scheme = 'app'").get(c.id))?.external_id??"app:terminal",i=await o.prepare("SELECT resolved_scopes_json FROM grants WHERE grantee = ? AND status = 'active'").all(s),u=t.type==="person"?null:i.length===0?a.pluginScopes(s):i.length===1?JSON.parse(i[0].resolved_scopes_json).filter(w=>w.mode==="always").map(w=>_(w.scope)):null;return{class:"object",object:e,node:e,owner:t,app:c,scopes:u,repos:t.type==="repo"?[t]:[]}}p(ne,"readerForObject");async function te(o,e,r){const a=(await o.prepare("SELECT external_id FROM identities WHERE object = ? AND scheme = 'app'").get(e.id))?.external_id;if(a===void 0)return null;const n=await o.prepare("SELECT resolved_scopes_json FROM grants WHERE grantee = ? AND status = 'active'").all(a);return n.length===0?r.pluginScopes(a):n.length>1?null:JSON.parse(n[0].resolved_scopes_json).filter(t=>t.mode==="always").map(t=>_(t.scope))}p(te,"appScopesNow");async function B(o,e,r,a){const n=await j(o);if(r!==null&&r.app===null)return{class:"object",object:e,node:e,owner:n,app:n,scopes:null,repos:[]};const t=r?.app==null?null:await E(o,r.app),l=r?.app==null?null:await a.grantFor(r.app,r.sub),c=r===null||l===null?null:r.node_scopes===null?l.resolved:r.node_scopes.filter(i=>f(l.resolved,i)),s=t??(e.owner===null?n:await a.object(e.owner)??n);return{class:"object",object:e,node:e,owner:s,app:s,scopes:c,launched:r?.node_scopes??null,repos:[],...r?.place==null?{}:{place:r.place}}}p(B,"readerForStub");function d(o){return{grantById:S,grantFor:R,pluginScopes:p(e=>C(e.replace(/^app:/,""))?.scopes??null,"pluginScopes"),object:p(async e=>await T(o,e),"object"),profile:p(async(e,r)=>await J(o,e,r),"profile"),projectPaths:p(e=>N()?[]:L($(),e)?.manifest.projects.map(r=>r.path)??[],"projectPaths")}}p(d,"liveLookups");function re(o){const e=d(o),r=new Map,a=p((n,t)=>(r.has(n)||r.set(n,t()),r.get(n)),"memo");return{grantById:p(n=>a(`g\0${n}`,()=>e.grantById(n)),"grantById"),grantFor:p((n,t)=>a(`a\0${n}\0${t}`,()=>e.grantFor(n,t)),"grantFor"),pluginScopes:p(n=>a(`t\0${n}`,()=>e.pluginScopes(n)),"pluginScopes"),object:p(async n=>await a(`o\0${n}`,async()=>await e.object(n)),"object"),profile:p(async(n,t)=>await a(`p\0${n}\0${t.id}`,async()=>await e.profile(n,t)),"profile"),projectPaths:p(n=>a(`j\0${n}`,()=>e.projectPaths(n)),"projectPaths")}}p(re,"publishLookups");async function y(o,e,r,a,n,t=!1){const l=r?await n.grantById(r):await n.grantFor(o,e),c=l&&l.grantee===o?l.resolved:null;if(c===null){if(t)return null;throw new Error(`the grant of ${o} is not active`)}return a===null?c:a.filter(s=>f(c,s))}p(y,"nodeScopes");async function j(o){return await W(o,D())}p(j,"personObject");async function E(o,e){return await O(o,"app","app",e,e.replace(/^app:/,""))}p(E,"appObject");async function J(o,e,r){return await o.prepare(`SELECT o.* FROM identities i JOIN objects o ON o.id = i.object
3
+ WHERE i.scheme = 'profile' AND i.external_id = ?`).get(e)??await O(o,"profile","profile",e,M(e).manifest.name,r)}p(J,"profileObject");export{te as appScopesNow,d as liveLookups,J as profileObject,re as publishLookups,oe as readerFor,P as readerForNode,ne as readerForObject,v as viewRepos};
@@ -1,4 +1,4 @@
1
- var Y=Object.defineProperty;var l=(t,e)=>Y(t,"name",{value:e,configurable:!0});import{DatabaseSync as M}from"node:sqlite";import{randomUUID as D}from"node:crypto";import{symlinkSync as Z,linkSync as ee,chmodSync as _,chownSync as j,closeSync as g,cpSync as te,existsSync as d,fsyncSync as R,lchownSync as ne,lstatSync as f,mkdirSync as v,openSync as S,readFileSync as w,readlinkSync as L,readSync as U,readdirSync as h,renameSync as k,rmSync as T,rmdirSync as b,unlinkSync as $,writeSync as q}from"node:fs";import{request as re}from"node:http";import{basename as A,dirname as y,isAbsolute as oe,join as c,relative as E,resolve as se}from"node:path";import{LAYOUT_VERSION as ie,assertSupportedLayoutMarker as C}from"../layout.js";import{InputError as I}from"../io.js";import{syncDirectory as ce}from"../fs-utils.js";import{oldToNewPathMap as ae}from"./paths.js";import{conversations as ue,memoryAndProfiles as le,previewStores as de,profileFolders as fe,repairMovedRepositories as pe,worktrees as he}from"./stores.js";import{storedPaths as ye}from"./stored-paths.js";import{OldToNewPathMap as ze,oldToNewPathMap as Be}from"./paths.js";const me=new Set([".DS_Store","Thumbs.db","desktop.ini"]);function ve(t){return me.has(t)||t.startsWith("._")}l(ve,"isJunkFile");function F(t,e,n){const r=c(t.map.old.root,"runtime","set-aside",e);let o=c(r,n);for(let i=2;d(o);i++)o=c(r,`${n}.${i}`);return o}l(F,"setAsidePath");function O(t,e,n,r=A(t)){const o=F(n,e,r);return v(y(o),{recursive:!0}),k(t,o),n.moved++,n.changes.push(`${t} -> ${o} (set aside)`),o}l(O,"setAside");function we(t){const e=f(t);return e.isDirectory()?h(t).length===0:e.isFile()&&e.size===0}l(we,"empty");function W(t,e,n,r=!1){const o=f(t),i=f(e);if(o.isDirectory()&&i.isDirectory()&&!r)return{action:"merge"};if(o.isFile()&&i.isFile()&&Q(t,e))return{action:"drop-source"};if(we(e))return{action:"remove-destination",line:`stray-destination: removed empty ${i.isDirectory()?"folder":"file"} ${e} that predates the migration`};const s=F(n,"replaced",E(n.map.old.root,e));return{action:"set-aside-destination",line:`stray-destination: ${e} predates the migration; moved it to ${s} and put ${t} in its place`}}l(W,"claim");function x(t,e,n,r=!1){if(d(t)&&t!==e){if(d(e)){const o=W(t,e,n,r);if(o.line&&n.resolutions.push(o.line),o.action==="merge"){for(const i of h(t))x(c(t,i),c(e,i),n);b(t);return}if(o.action==="drop-source"){T(t,{recursive:!0}),n.changes.push(`${t} (identical to ${e}; removed)`);return}o.action==="remove-destination"?(T(e,{recursive:!0}),n.changes.push(`${e} (removed)`)):O(e,"replaced",n,E(n.map.old.root,e))}v(y(e),{recursive:!0}),k(t,e),n.moved++,n.changes.push(`${t} -> ${e}`)}}l(x,"movePath");function z(t,e,n,r=!1){if(!d(t)||t===e||!d(e))return;const o=W(t,e,n,r);if(o.line&&n.resolutions.push(o.line),o.action==="merge")for(const i of h(t))z(c(t,i),c(e,i),n)}l(z,"previewMove");function B(t){const{map:e}=t,n=c(e.old.canvas,"nodes"),r=c(e.old.canvas,"canvas.db"),o=c(e.old.root,"runtime","canvas.db"),i=d(r)?r:d(o)&&f(o).size>0?o:"";if(!i){if(d(n))throw new Error("old node records exist without canvas.db");return null}const s=new M(i,{readOnly:t.dryRun||i!==r});try{if(i===r&&!t.dryRun&&s.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get().busy!==0)throw new Error("old canvas WAL is busy; stop every database user before migrating");return new Map(s.prepare("SELECT node_id, grantee FROM nodes").all().map(a=>{const u=a.grantee==="user:local"?"terminal":a.grantee.startsWith("app:")?a.grantee.slice(4):"";if(!u)throw new Error(`node ${a.node_id} has no app assignment`);return[a.node_id,u]}))}finally{s.close()}}l(B,"canvasNodeApps");function J(t,e){const n=c(t.map.old.canvas,"nodes");if(!d(n))return[];const r=[];for(const o of h(n)){const i=c(n,o),s=f(i).isDirectory();e.has(o)&&s||r.push({path:i,remove:!s&&ve(o)})}return r}l(J,"orphanNodeEntries");function G(t,e){return t.remove?`node-leftover: deleted ${t.path} (not a node record)`:`node-leftover: ${t.path} has no node row; moved to ${F(e,"nodes",A(t.path))}`}l(G,"orphanLine");function be(t){const{map:e}=t,n=c(e.old.canvas,"nodes"),r=B(t);if(r){for(const[o,i]of r)e.node(o,i);if(d(n)){for(const o of J(t,r)){if(t.resolutions.push(G(o,t)),o.remove){$(o.path),t.changes.push(`${o.path} (removed)`);continue}const i=O(o.path,"nodes",t),s=c(e.old.targetRoot,E(e.old.root,i));for(const a of new Set([e.old.storedCanvas,e.old.storedScope,c(e.old.storedScope,"canvas")]))e.add(c(a,"nodes",A(o.path)),s)}for(const[o,i]of r){const s=c(n,o);if(d(s)){for(const a of["context","reports","attachments"])x(c(s,a),c(e.old.root,"apps",i,"nodes",o,a),t);for(const a of h(s))["worktree","view.sock","broker-launch.json","system-prompt.md","situational-context.md","injected-docs.json"].includes(a)||x(c(s,a),c(e.old.root,"runtime","nodes",o,a),t);h(s).length===0&&b(s)}}h(n).length===0&&b(n)}}}l(be,"records");const H=["auth.json","anthropic-credentials.json","openai-codex-credentials.json","openai-credentials.json","crouter-operations.db"];function ge(t,e,n){if(!d(t))return;if(!f(t).isFile())throw new Error(`credential source is not a regular file: ${t}`);const r=w(t);if(d(e)){(!f(e).isFile()||!w(e).equals(r))&&n.resolutions.push(`credential: kept the existing ${e}; the older ${t} differs and stays where it is`);return}v(y(e),{recursive:!0});const o=c(y(e),`.${D()}.tmp`);try{const i=S(o,"wx",384);try{for(let s=0;s<r.length;)s+=q(i,r,s,r.length-s);R(i)}finally{g(i)}if(k(o,e),!w(e).equals(r))throw new Error(`credential verification failed: ${e}`)}finally{d(o)&&$(o)}n.moved++,n.changes.push(`${t} -> ${e} (source retained)`)}l(ge,"copyCredential");function K(t){if(!d(t))return;const e=new M(t);try{if(e.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get().busy!==0)throw new Error(`SQLite WAL is busy: ${t}`)}finally{e.close()}}l(K,"checkpointDb");function Q(t,e){if(f(t).size!==f(e).size)return!1;const n=S(t,"r"),r=S(e,"r"),o=Buffer.allocUnsafe(64*1024),i=Buffer.allocUnsafe(o.length);try{for(;;){const s=U(n,o,0,o.length,null);if(s===0)return!0;let a=0;for(;a<s;){const u=U(r,i,a,s-a,null);if(u===0)return!1;a+=u}if(!o.subarray(0,s).equals(i.subarray(0,s)))return!1}}finally{g(n),g(r)}}l(Q,"sameFile");function P(t,e,n=!1){let r;try{r=f(e)}catch(u){if(u.code==="ENOENT")return!1;throw u}const o=f(t);if(o.isSymbolicLink())return r.isSymbolicLink()&&L(t)===L(e);if(o.isFile())return r.isFile()&&Q(t,e);if(!o.isDirectory()||!r.isDirectory())return!1;const i=h(t).filter(u=>!n||u!==".git"),s=h(e).filter(u=>!n||u!==".git"),a=new Set(s);return i.length===s.length&&i.every(u=>a.has(u)&&P(c(t,u),c(e,u),n))}l(P,"sameTree");function Se(t,e,n){if(!d(t))return;if(!f(t).isDirectory())throw new Error(`hosted work is not a directory: ${t}`);if(d(e)){if(P(t,e,!0))return;if(!f(e).isDirectory()||h(e).length!==0){n.resolutions.push(`hosted-work: kept the existing ${e}; the old ${t} differs and stays where it is`);return}b(e)}v(y(e),{recursive:!0});const r=c(y(e),`.work-${D()}.migrating`);try{if(te(t,r,{recursive:!0,verbatimSymlinks:!0,preserveTimestamps:!0}),!P(t,r))throw new Error(`hosted work copy verification failed: ${t}`);k(r,e),n.moved++,n.changes.push(`${t} -> ${e} (source retained)`)}finally{d(r)&&T(r,{recursive:!0,force:!0})}}l(Se,"copyHostedWork");function V(t){const{old:e}=t.map,n=c(e.root,"runtime"),r=c(n,"scope"),o=[];for(const i of["canvas.db","canvas.db-wal","canvas.db-shm","on-disk-migrations.json","requests","reviews","plugins","marketplaces","crtrd.log","crtrd.err","cache"])o.push([c(e.canvas,i),c(n,i)]);for(const i of["prompts","personas","config.json","profile-defaults.json","operations.db","operations.db-wal","operations.db-shm"])o.push([c(e.scope,i),c(r,i)]);for(const i of["support","prompt-reviews","plugins","marketplaces"])o.push([c(e.scope,i),c(n,i)]);return o.push([c(e.canvas,"secrets"),c(n,"secrets")]),o}l(V,"topLevelMoves");function ke(t){const{old:e}=t.map,n=c(e.root,"runtime");K(c(e.scope,"operations.db"));for(const[r,o]of V(t)){const i=$e(r);x(r,o,t,!0),Ee(i,r,o,t)}K(c(e.agent,"crouter-operations.db"));for(const r of H)ge(c(e.agent,r),c(n,"secrets",r),t);process.platform==="linux"&&Se(e.work,c(e.root,"user","files"),t)}l(ke,"topLevel");function $e(t){if(!d(t)||!f(t).isDirectory())return[];const e=[];for(const n of h(t)){const r=c(t,n);if(!f(r).isSymbolicLink())continue;const o=L(r);oe(o)||e.push({name:n,target:o})}return e}l($e,"relativeLinks");function Ee(t,e,n,r){if(!(t.length===0||!d(n)))for(const{name:o,target:i}of t){const s=c(n,o);if(!xe(s))continue;const a=r.map.mapPath(se(e,i)),u=E(n,a);u!==i&&($(s),Z(u,s),r.changes.push(`${s}: link ${i} -> ${u}`))}}l(Ee,"retargetLinks");function xe(t){try{return f(t),!0}catch{return!1}}l(xe,"lstatExists");function Ne(t){if(process.platform!=="linux")return;if(process.getuid?.()!==0)throw new Error("Linux layout migration requires root for the ownership pass");const e=w("/etc/passwd","utf8").split(`
1
+ var X=Object.defineProperty;var l=(t,e)=>X(t,"name",{value:e,configurable:!0});import{DatabaseSync as _}from"node:sqlite";import{randomUUID as L}from"node:crypto";import{symlinkSync as Z,linkSync as ee,chmodSync as M,chownSync as j,closeSync as g,cpSync as te,existsSync as d,fsyncSync as C,lchownSync as ne,lstatSync as f,mkdirSync as v,openSync as S,readFileSync as w,readlinkSync as D,readSync as O,readdirSync as h,renameSync as k,rmSync as A,rmdirSync as b,unlinkSync as $,writeSync as R}from"node:fs";import{request as re}from"node:http";import{basename as T,dirname as m,isAbsolute as oe,join as c,relative as E,resolve as se}from"node:path";import{LEGACY_LOCAL_PERSON as ie}from"../canvas/legacy-person.js";import{LAYOUT_VERSION as ce,assertSupportedLayoutMarker as U}from"../layout.js";import{InputError as q}from"../io.js";import{syncDirectory as ae}from"../fs-utils.js";import{oldToNewPathMap as ue}from"./paths.js";import{conversations as le,memoryAndProfiles as de,previewStores as fe,profileFolders as pe,repairMovedRepositories as he,worktrees as me}from"./stores.js";import{storedPaths as ye}from"./stored-paths.js";import{OldToNewPathMap as Ge,oldToNewPathMap as Je}from"./paths.js";const ve=new Set([".DS_Store","Thumbs.db","desktop.ini"]);function we(t){return ve.has(t)||t.startsWith("._")}l(we,"isJunkFile");function F(t,e,n){const r=c(t.map.old.root,"runtime","set-aside",e);let o=c(r,n);for(let i=2;d(o);i++)o=c(r,`${n}.${i}`);return o}l(F,"setAsidePath");function I(t,e,n,r=T(t)){const o=F(n,e,r);return v(m(o),{recursive:!0}),k(t,o),n.moved++,n.changes.push(`${t} -> ${o} (set aside)`),o}l(I,"setAside");function be(t){const e=f(t);return e.isDirectory()?h(t).length===0:e.isFile()&&e.size===0}l(be,"empty");function W(t,e,n,r=!1){const o=f(t),i=f(e);if(o.isDirectory()&&i.isDirectory()&&!r)return{action:"merge"};if(o.isFile()&&i.isFile()&&K(t,e))return{action:"drop-source"};if(be(e))return{action:"remove-destination",line:`stray-destination: removed empty ${i.isDirectory()?"folder":"file"} ${e} that predates the migration`};const s=F(n,"replaced",E(n.map.old.root,e));return{action:"set-aside-destination",line:`stray-destination: ${e} predates the migration; moved it to ${s} and put ${t} in its place`}}l(W,"claim");function x(t,e,n,r=!1){if(d(t)&&t!==e){if(d(e)){const o=W(t,e,n,r);if(o.line&&n.resolutions.push(o.line),o.action==="merge"){for(const i of h(t))x(c(t,i),c(e,i),n);b(t);return}if(o.action==="drop-source"){A(t,{recursive:!0}),n.changes.push(`${t} (identical to ${e}; removed)`);return}o.action==="remove-destination"?(A(e,{recursive:!0}),n.changes.push(`${e} (removed)`)):I(e,"replaced",n,E(n.map.old.root,e))}v(m(e),{recursive:!0}),k(t,e),n.moved++,n.changes.push(`${t} -> ${e}`)}}l(x,"movePath");function z(t,e,n,r=!1){if(!d(t)||t===e||!d(e))return;const o=W(t,e,n,r);if(o.line&&n.resolutions.push(o.line),o.action==="merge")for(const i of h(t))z(c(t,i),c(e,i),n)}l(z,"previewMove");function B(t){const{map:e}=t,n=c(e.old.canvas,"nodes"),r=c(e.old.canvas,"canvas.db"),o=c(e.old.root,"runtime","canvas.db"),i=d(r)?r:d(o)&&f(o).size>0?o:"";if(!i){if(d(n))throw new Error("old node records exist without canvas.db");return null}const s=new _(i,{readOnly:t.dryRun||i!==r});try{if(i===r&&!t.dryRun&&s.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get().busy!==0)throw new Error("old canvas WAL is busy; stop every database user before migrating");return new Map(s.prepare("SELECT node_id, grantee FROM nodes").all().map(a=>{const u=a.grantee===ie?"terminal":a.grantee.startsWith("app:")?a.grantee.slice(4):"";if(!u)throw new Error(`node ${a.node_id} has no app assignment`);return[a.node_id,u]}))}finally{s.close()}}l(B,"canvasNodeApps");function G(t,e){const n=c(t.map.old.canvas,"nodes");if(!d(n))return[];const r=[];for(const o of h(n)){const i=c(n,o),s=f(i).isDirectory();e.has(o)&&s||r.push({path:i,remove:!s&&we(o)})}return r}l(G,"orphanNodeEntries");function J(t,e){return t.remove?`node-leftover: deleted ${t.path} (not a node record)`:`node-leftover: ${t.path} has no node row; moved to ${F(e,"nodes",T(t.path))}`}l(J,"orphanLine");function ge(t){const{map:e}=t,n=c(e.old.canvas,"nodes"),r=B(t);if(r){for(const[o,i]of r)e.node(o,i);if(d(n)){for(const o of G(t,r)){if(t.resolutions.push(J(o,t)),o.remove){$(o.path),t.changes.push(`${o.path} (removed)`);continue}const i=I(o.path,"nodes",t),s=c(e.old.targetRoot,E(e.old.root,i));for(const a of new Set([e.old.storedCanvas,e.old.storedScope,c(e.old.storedScope,"canvas")]))e.add(c(a,"nodes",T(o.path)),s)}for(const[o,i]of r){const s=c(n,o);if(d(s)){for(const a of["context","reports","attachments"])x(c(s,a),c(e.old.root,"apps",i,"nodes",o,a),t);for(const a of h(s))["worktree","view.sock","broker-launch.json","system-prompt.md","situational-context.md","injected-docs.json"].includes(a)||x(c(s,a),c(e.old.root,"runtime","nodes",o,a),t);h(s).length===0&&b(s)}}h(n).length===0&&b(n)}}}l(ge,"records");const Y=["auth.json","anthropic-credentials.json","openai-codex-credentials.json","openai-credentials.json","crouter-operations.db"];function Se(t,e,n){if(!d(t))return;if(!f(t).isFile())throw new Error(`credential source is not a regular file: ${t}`);const r=w(t);if(d(e)){(!f(e).isFile()||!w(e).equals(r))&&n.resolutions.push(`credential: kept the existing ${e}; the older ${t} differs and stays where it is`);return}v(m(e),{recursive:!0});const o=c(m(e),`.${L()}.tmp`);try{const i=S(o,"wx",384);try{for(let s=0;s<r.length;)s+=R(i,r,s,r.length-s);C(i)}finally{g(i)}if(k(o,e),!w(e).equals(r))throw new Error(`credential verification failed: ${e}`)}finally{d(o)&&$(o)}n.moved++,n.changes.push(`${t} -> ${e} (source retained)`)}l(Se,"copyCredential");function H(t){if(!d(t))return;const e=new _(t);try{if(e.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get().busy!==0)throw new Error(`SQLite WAL is busy: ${t}`)}finally{e.close()}}l(H,"checkpointDb");function K(t,e){if(f(t).size!==f(e).size)return!1;const n=S(t,"r"),r=S(e,"r"),o=Buffer.allocUnsafe(64*1024),i=Buffer.allocUnsafe(o.length);try{for(;;){const s=O(n,o,0,o.length,null);if(s===0)return!0;let a=0;for(;a<s;){const u=O(r,i,a,s-a,null);if(u===0)return!1;a+=u}if(!o.subarray(0,s).equals(i.subarray(0,s)))return!1}}finally{g(n),g(r)}}l(K,"sameFile");function P(t,e,n=!1){let r;try{r=f(e)}catch(u){if(u.code==="ENOENT")return!1;throw u}const o=f(t);if(o.isSymbolicLink())return r.isSymbolicLink()&&D(t)===D(e);if(o.isFile())return r.isFile()&&K(t,e);if(!o.isDirectory()||!r.isDirectory())return!1;const i=h(t).filter(u=>!n||u!==".git"),s=h(e).filter(u=>!n||u!==".git"),a=new Set(s);return i.length===s.length&&i.every(u=>a.has(u)&&P(c(t,u),c(e,u),n))}l(P,"sameTree");function ke(t,e,n){if(!d(t))return;if(!f(t).isDirectory())throw new Error(`hosted work is not a directory: ${t}`);if(d(e)){if(P(t,e,!0))return;if(!f(e).isDirectory()||h(e).length!==0){n.resolutions.push(`hosted-work: kept the existing ${e}; the old ${t} differs and stays where it is`);return}b(e)}v(m(e),{recursive:!0});const r=c(m(e),`.work-${L()}.migrating`);try{if(te(t,r,{recursive:!0,verbatimSymlinks:!0,preserveTimestamps:!0}),!P(t,r))throw new Error(`hosted work copy verification failed: ${t}`);k(r,e),n.moved++,n.changes.push(`${t} -> ${e} (source retained)`)}finally{d(r)&&A(r,{recursive:!0,force:!0})}}l(ke,"copyHostedWork");function Q(t){const{old:e}=t.map,n=c(e.root,"runtime"),r=c(n,"scope"),o=[];for(const i of["canvas.db","canvas.db-wal","canvas.db-shm","on-disk-migrations.json","requests","reviews","plugins","marketplaces","crtrd.log","crtrd.err","cache"])o.push([c(e.canvas,i),c(n,i)]);for(const i of["prompts","personas","config.json","profile-defaults.json","operations.db","operations.db-wal","operations.db-shm"])o.push([c(e.scope,i),c(r,i)]);for(const i of["support","prompt-reviews","plugins","marketplaces"])o.push([c(e.scope,i),c(n,i)]);return o.push([c(e.canvas,"secrets"),c(n,"secrets")]),o}l(Q,"topLevelMoves");function $e(t){const{old:e}=t.map,n=c(e.root,"runtime");H(c(e.scope,"operations.db"));for(const[r,o]of Q(t)){const i=Ee(r);x(r,o,t,!0),xe(i,r,o,t)}H(c(e.agent,"crouter-operations.db"));for(const r of Y)Se(c(e.agent,r),c(n,"secrets",r),t);process.platform==="linux"&&ke(e.work,c(e.root,"user","files"),t)}l($e,"topLevel");function Ee(t){if(!d(t)||!f(t).isDirectory())return[];const e=[];for(const n of h(t)){const r=c(t,n);if(!f(r).isSymbolicLink())continue;const o=D(r);oe(o)||e.push({name:n,target:o})}return e}l(Ee,"relativeLinks");function xe(t,e,n,r){if(!(t.length===0||!d(n)))for(const{name:o,target:i}of t){const s=c(n,o);if(!Ne(s))continue;const a=r.map.mapPath(se(e,i)),u=E(n,a);u!==i&&($(s),Z(u,s),r.changes.push(`${s}: link ${i} -> ${u}`))}}l(xe,"retargetLinks");function Ne(t){try{return f(t),!0}catch{return!1}}l(Ne,"lstatExists");function Le(t){if(process.platform!=="linux")return;if(process.getuid?.()!==0)throw new Error("Linux layout migration requires root for the ownership pass");const e=w("/etc/passwd","utf8").split(`
2
2
  `).find(s=>s.startsWith("crtrd:")),n=w("/etc/group","utf8").split(`
3
- `).find(s=>s.startsWith("crtr-data:"));if(!e||!n)throw new Error("layout migration requires crtrd user and crtr-data group");const r=Number(e.split(":")[2]),o=Number(n.split(":")[2]);if(!Number.isInteger(r)||!Number.isInteger(o))throw new Error("invalid migration user or group");const i=l((s,a)=>{if(!d(s))return;const u=f(s);if(u.isSymbolicLink()){ne(s,r,a?o:u.gid);return}if(j(s,r,a?o:u.gid),u.isDirectory()){_(s,a?1528:448);for(const m of h(s))i(c(s,m),a)}else a&&_(s,u.mode&73?504:432)},"walk");i(c(t,"runtime"),!1),i(c(t,"user"),!0),i(c(t,"apps"),!0)}l(Ne,"ownership");function De(t){return new Promise(e=>{const n=re({socketPath:t,path:"/healthz",method:"GET",timeout:1e3},r=>{r.resume(),e(!0)});n.on("timeout",()=>n.destroy(new Error("daemon probe timed out"))),n.on("error",r=>{r.code==="ENOENT"||r.code==="ECONNREFUSED"?e(!1):e(!0)}),n.end()})}l(De,"daemonAnswers");async function Ie(t){const e=ae(),n=e.old,r=c(n.root,"runtime","layout");if(d(r))return C(r),null;const o=new Set([c(n.canvas,"crtrd.sock"),c(n.scope,"crtrd.sock"),process.platform==="linux"?"/run/crtr/api.sock":c(n.root,"runtime","api.sock")]);for(const p of o)if(await De(p))throw new I({error:"daemon_running",message:`cannot migrate while a daemon answers on ${p}`,next:"Stop the daemon that owns this volume, then run `crtr sys migrate` again."});if(process.platform==="linux"&&!d(n.agent))throw new I({error:"credential_volume_missing",message:`the old credential volume is not mounted at ${n.agent}`,next:`Mount the old provider-auth volume at ${n.agent}, then run \`crtr sys migrate\` again.`});const i=H.map(p=>c(n.agent,p)).filter(p=>!d(p)),s={map:e,moved:0,changes:[],resolutions:[],bodyPaths:[],absentCredentials:i,dryRun:t};if(t){const p=B(s);if(p)for(const N of J(s,p))s.resolutions.push(G(N,s));for(const[N,X]of V(s))z(N,X,s,!0);return de(s),s}fe(s);const a=[be,he,ke,pe,le,ue,ye];for(const p of a)p(s);n.canvas!==n.root&&d(n.canvas)&&h(n.canvas).length===0&&b(n.canvas),Ne(n.root),v(y(r),{recursive:!0});const u=`${r}.${D()}.tmp`,m=S(u,"wx",384);try{try{if(process.platform==="linux"){const p=f(y(r));j(u,p.uid,p.gid)}q(m,`${ie}
4
- `),R(m)}finally{g(m)}try{ee(u,r)}catch(p){if(p.code==="EEXIST")C(r);else throw p}}finally{$(u)}return ce(y(r)),s}l(Ie,"migrateLayout");export{ze as OldToNewPathMap,W as claim,De as daemonAnswers,ve as isJunkFile,Ie as migrateLayout,x as movePath,Be as oldToNewPathMap,z as previewMove,Q as sameFile,O as setAside,F as setAsidePath};
3
+ `).find(s=>s.startsWith("crtr-data:"));if(!e||!n)throw new Error("layout migration requires crtrd user and crtr-data group");const r=Number(e.split(":")[2]),o=Number(n.split(":")[2]);if(!Number.isInteger(r)||!Number.isInteger(o))throw new Error("invalid migration user or group");const i=l((s,a)=>{if(!d(s))return;const u=f(s);if(u.isSymbolicLink()){ne(s,r,a?o:u.gid);return}if(j(s,r,a?o:u.gid),u.isDirectory()){M(s,a?1528:448);for(const y of h(s))i(c(s,y),a)}else a&&M(s,u.mode&73?504:432)},"walk");i(c(t,"runtime"),!1),i(c(t,"user"),!0),i(c(t,"apps"),!0)}l(Le,"ownership");function De(t){return new Promise(e=>{const n=re({socketPath:t,path:"/healthz",method:"GET",timeout:1e3},r=>{r.resume(),e(!0)});n.on("timeout",()=>n.destroy(new Error("daemon probe timed out"))),n.on("error",r=>{r.code==="ENOENT"||r.code==="ECONNREFUSED"?e(!1):e(!0)}),n.end()})}l(De,"daemonAnswers");async function We(t){const e=ue(),n=e.old,r=c(n.root,"runtime","layout");if(d(r))return U(r),null;const o=new Set([c(n.canvas,"crtrd.sock"),c(n.scope,"crtrd.sock"),process.platform==="linux"?"/run/crtr/api.sock":c(n.root,"runtime","api.sock")]);for(const p of o)if(await De(p))throw new q({error:"daemon_running",message:`cannot migrate while a daemon answers on ${p}`,next:"Stop the daemon that owns this volume, then run `crtr sys migrate` again."});if(process.platform==="linux"&&!d(n.agent))throw new q({error:"credential_volume_missing",message:`the old credential volume is not mounted at ${n.agent}`,next:`Mount the old provider-auth volume at ${n.agent}, then run \`crtr sys migrate\` again.`});const i=Y.map(p=>c(n.agent,p)).filter(p=>!d(p)),s={map:e,moved:0,changes:[],resolutions:[],bodyPaths:[],absentCredentials:i,dryRun:t};if(t){const p=B(s);if(p)for(const N of G(s,p))s.resolutions.push(J(N,s));for(const[N,V]of Q(s))z(N,V,s,!0);return fe(s),s}pe(s);const a=[ge,me,$e,he,de,le,ye];for(const p of a)p(s);n.canvas!==n.root&&d(n.canvas)&&h(n.canvas).length===0&&b(n.canvas),Le(n.root),v(m(r),{recursive:!0});const u=`${r}.${L()}.tmp`,y=S(u,"wx",384);try{try{if(process.platform==="linux"){const p=f(m(r));j(u,p.uid,p.gid)}R(y,`${ce}
4
+ `),C(y)}finally{g(y)}try{ee(u,r)}catch(p){if(p.code==="EEXIST")U(r);else throw p}}finally{$(u)}return ae(m(r)),s}l(We,"migrateLayout");export{Ge as OldToNewPathMap,W as claim,De as daemonAnswers,we as isJunkFile,We as migrateLayout,x as movePath,Je as oldToNewPathMap,z as previewMove,K as sameFile,I as setAside,F as setAsidePath};
@@ -1,3 +1,3 @@
1
- var oe=Object.defineProperty;var h=(e,t)=>oe(e,"name",{value:t,configurable:!0});import{DatabaseSync as z}from"node:sqlite";import{closeSync as se,copyFileSync as re,existsSync as u,realpathSync as ne,fsyncSync as pe,lstatSync as $,mkdirSync as B,openSync as ce,readFileSync as T,readdirSync as E,renameSync as M,rmdirSync as F,unlinkSync as D,writeFileSync as J}from"node:fs";import{execFileSync as Z}from"node:child_process";import{basename as x,dirname as R,isAbsolute as q,join as n,relative as k,resolve as C,sep as P}from"node:path";import{profileSpace as ae}from"../layout.js";import{isJunkFile as j,movePath as S,sameFile as A,setAside as O,setAsidePath as H}from"./index.js";import{rewriteBodyPaths as ie}from"./stored-paths.js";function L(e,t){Z("git",["-c",`safe.directory=${e}`,"-C",e,...t],{stdio:"pipe"})}h(L,"gitInRepo");function _(e,t){const{root:s,targetRoot:p}=t.map.old;return C(s,k(p,e))}h(_,"physical");function N(e,t=!1){const{old:s}=e.map,p=n(s.canvas,"canvas.db"),o=n(s.root,"runtime","canvas.db"),r=u(p)?p:o;return u(r)&&$(r).size>0?new z(r,{readOnly:!t||e.dryRun}):null}h(N,"canvasDb");function W(e){return new Map(e.prepare("SELECT node_id, grantee FROM nodes").all().map(({node_id:t,grantee:s})=>{const p=s==="user:local"?"terminal":s.startsWith("app:")?s.slice(4):"";if(!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(p))throw new Error(`node ${t} has no valid app`);return[t,p]}))}h(W,"nodeApps");function Y(e,t,s){return n(s.map.old.targetRoot,"apps",e,"files","profiles",t)}h(Y,"profileFiles");function G(e,t,s,p){if(!u(e))return;if(!u(t)){S(e,t,p);return}const o=$(e),r=$(t);if(o.isDirectory()&&r.isDirectory()){for(const d of E(e))G(n(e,d),n(t,d),n(s,d),p);F(e);return}if(o.isFile()&&r.isFile()&&A(e,t)){D(e);return}const a=_(s,p);p.dryRun||S(e,a,p),p.resolutions.push(`profile-home: ${t} differs from the profile's ${e}; kept the app home's copy, moved the profile's to ${a}`)}h(G,"mergeHome");function K(e,t,s,p){if(!u(e)||!u(t))return;const o=$(e),r=$(t);if(o.isDirectory()&&r.isDirectory()){for(const a of E(e))K(n(e,a),n(t,a),n(s,a),p);return}o.isFile()&&r.isFile()&&A(e,t)||p.resolutions.push(`profile-home: ${t} differs from the profile's ${e}; kept the app home's copy, moved the profile's to ${_(s,p)}`)}h(K,"previewHome");function Q(e,t,s){e.bodyPaths.some(([p])=>p===t)||e.bodyPaths.push([t,s])}h(Q,"bodyPath");function V(e){const{map:t}=e,s=[n(t.old.scope,"operations.db"),n(t.old.root,"runtime","scope","operations.db")].find(u),p=s?new z(s,{readOnly:!0}):null,o=new Map;try{if(p&&p.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='profiles'").get())for(const i of p.prepare("SELECT id, grantee FROM profiles").all())o.set(i.id,i.grantee)}finally{p?.close()}const r=new Map;for(const[i,c]of o){const w=c==null||c==="user:local"?"terminal":c.startsWith("app:")?c.slice(4):"",f=ae(w,i),y=n(t.old.storedScope,"profiles",i);t.retire(y,f.dir),t.add(n(y,"memory"),f.dir),t.add(n(y,"env.json"),f.env),t.add(n(y,"home"),f.home),r.set(i,{target:f,app:w})}const a=n(t.old.scope,"profiles"),d={folders:[],junk:[],aside:[]};if(!u(a))return d;for(const i of E(a)){const c=n(a,i);if(!$(c).isDirectory()){(j(i)?d.junk:d.aside).push(c);continue}if(i===".locks"){for(const l of E(c)){const v=n(c,l);!(l.endsWith(".lock")&&$(v).isFile())&&!j(l)&&d.aside.push(v)}continue}const w=n(t.old.storedScope,"profiles",i),f=r.get(i);if(!f){const l=Y("terminal",i,e);t.add(w,l),Q(e,w,l),d.folders.push({id:i,source:c,target:null,files:l,extras:[]});continue}const y=Y(f.app,i,e),b=[];for(const l of E(c)){const v=$(n(c,l));(l==="memory"||l==="home"?v.isDirectory():l==="env.json"||l==="profile.json"||l.endsWith(".lock")?v.isFile():j(l))||(b.push(l),t.add(n(w,l),n(y,l)),Q(e,n(w,l),n(y,l)))}d.folders.push({id:i,source:c,target:f.target,files:y,extras:b})}return d}h(V,"profileFolders");function X(e,t){for(const s of e.folders){s.target||t.resolutions.push(`profile-folder: ${s.source} has no profile row; moved whole to ${_(s.files,t)}`);for(const p of s.extras)t.resolutions.push(`profile-extra: ${n(s.source,p)} moved to ${_(n(s.files,p),t)}`)}for(const s of e.junk)t.resolutions.push(`profile-leftover: deleted ${s} (not profile data)`);for(const s of e.aside)t.resolutions.push(`profile-leftover: ${s} is not a profile; moved to ${H(t,"profiles",x(s))}`)}h(X,"profileLines");function Fe(e){const{map:t}=e;S(n(t.old.scope,"memory"),n(t.old.root,"user","memory"),e);const s=N(e);if(s)try{for(const[r,a]of W(s)){const d=n(t.old.root,"apps",a,"memory","nodes",r);S(n(t.old.root,"apps",a,"nodes",r,"context","memory"),d,e),S(n(t.old.canvas,"nodes",r,"context","memory"),d,e)}}finally{s.close()}const p=V(e);X(p,e);for(const{source:r,target:a,files:d,extras:i}of p.folders){if(!a){S(r,_(d,e),e,!0);continue}for(const c of i)S(n(r,c),_(n(d,c),e),e);S(n(r,"memory"),_(a.dir,e),e),S(n(r,"env.json"),_(a.env,e),e),G(n(r,"home"),_(a.home,e),n(d,"home"),e);for(const c of E(r))D(n(r,c));F(r),e.moved++,e.changes.push(`${r} (removed)`)}for(const r of p.junk)D(r);for(const r of p.aside)O(r,"profiles",e);const o=n(t.old.scope,"profiles");if(u(o)){const r=n(o,".locks");if(u(r)){for(const a of E(r))D(n(r,a));F(r)}F(o)}}h(Fe,"memoryAndProfiles");function de(e,t,s,p){if(u(e)){if(!$(e).isFile())throw new Error(`session path is not a file: ${e}`);if(B(R(t),{recursive:!0}),u(t)){if($(t).isFile()&&A(e,t))return;const o=O(t,"replaced",s,k(s.map.old.root,t));s.resolutions.push(`stray-destination: ${t} predates the migration; moved it to ${o} and put ${e} in its place`)}if(p){const o=n(R(t),`.${x(t)}.migrating`);u(o)&&D(o),re(e,o);const r=ce(o,"r");try{pe(r)}finally{se(r)}if(!A(e,o))throw new Error(`session copy verification failed: ${e}`);M(o,t),s.moved++,s.changes.push(`${e} -> ${t} (source retained)`);return}M(e,t),s.moved++,s.changes.push(`${e} -> ${t}`)}}h(de,"moveSession");function le(e,t,s){const p=new Set(t.prepare("SELECT s.transcript_path AS path FROM sessions s JOIN nodes n ON n.node_id = s.node_id WHERE s.unbound_at IS NULL AND n.status IN ('active', 'idle') AND s.transcript_path IS NOT NULL").all().map(r=>r.path)),o=e.dryRun?null:t.prepare("UPDATE sessions SET transcript_path = NULL WHERE transcript_path = ?");for(const r of s)if(!(u(r.source)||u(_(r.destination,e)))){if(p.has(r.transcript_path)){e.resolutions.push(`lost-transcript: ${r.transcript_path} is missing but is a live node's current session; kept its path`);continue}o?.run(r.transcript_path),e.resolutions.push(`lost-transcript: ${r.transcript_path} is missing from both paths; cleared the session's transcript path`)}}h(le,"lostTranscripts");function fe(e){const t=N(e,!0);if(t)try{const s=W(t),p=t.prepare("SELECT session_id, node_id, transcript_path FROM sessions WHERE transcript_path IS NOT NULL ORDER BY session_id, (unbound_at IS NULL) DESC, created_at DESC, rowid DESC").all(),o=new Map,r=new Map,a=new Map;for(const c of p){o.has(c.session_id)||o.set(c.session_id,c.node_id),a.set(c.transcript_path,(a.get(c.transcript_path)??0)+1);const w=r.get(c.transcript_path)??new Set;w.add(c.session_id),r.set(c.transcript_path,w)}const d=new Set,i=[];for(const{session_id:c,node_id:w,transcript_path:f}of p){if(d.has(f))continue;if(d.add(f),r.get(f).size!==1)throw new Error(`two conversations share a transcript: ${f}`);const y=a.get(f)>1?o.get(c):w,b=s.get(y);if(!b)throw new Error(`session belongs to unknown node: ${y}`);if(!q(f))throw new Error(`session path is not absolute: ${f}`);const l=n(e.map.old.targetRoot,"apps",b,"conversations",y,x(f));if(f===l)continue;const v=f===e.map.old.storedAgent||f.startsWith(e.map.old.storedAgent+P),m=v?n(e.map.old.agent,k(e.map.old.storedAgent,f)):f;i.push({transcript_path:f,destination:l,source:m,retain:process.platform==="linux"&&v})}le(e,t,i);for(const{transcript_path:c,destination:w}of i)e.map.add(c,w);if(e.dryRun)return;for(const{destination:c,source:w,retain:f}of i)de(w,_(c,e),e,f)}finally{t.close()}}h(fe,"conversations");function De(e){if(process.platform!=="linux")return;const t=N(e);if(t)try{const s=t.prepare("SELECT node_id, path, repo_root FROM worktrees").all(),p=W(t);for(const o of s){const r=e.map.mapPath(o.repo_root);if(r===o.repo_root)continue;const a=p.get(o.node_id);if(!a)throw new Error(`worktree belongs to unknown node: ${o.node_id}`);const d=_(n(e.map.old.targetRoot,"apps",a,"nodes",o.node_id,"worktree"),e);if(!u(d))continue;L(_(r,e),["worktree","repair",d]);const i=o.path.startsWith(e.map.old.storedCanvas+P)?n(e.map.old.canvas,k(e.map.old.storedCanvas,o.path)):o.path;I(i,d,e)}}finally{t.close()}}h(De,"repairMovedRepositories");function I(e,t,s){const p=h(o=>{for(const r of E(o)){const a=n(o,r);if(r===".git"&&a!==n(t,".git")&&$(a).isFile()){const d=T(a,"utf8");if(!d.startsWith("gitdir: "))throw new Error(`invalid submodule git link: ${a}`);const i=d.slice(8).trim(),c=C(R(a),i),w=n(e,k(t,a)),f=C(R(w),i),y=u(c)?c:f;if(!u(y))throw new Error(`submodule git directory missing: ${w}`);const b=s?.map.mapPath(y)??y,l=s&&b!==y?_(b,s):y;if(!u(l))throw new Error(`moved submodule git directory missing: ${l}`);c!==l&&J(a,`gitdir: ${k(R(a),l)}
2
- `);const v=n(l,"config"),m=T(v,"utf8");if(!/^\s*worktree\s*=.*$/m.test(m))throw new Error(`submodule has no worktree setting: ${v}`);const te=JSON.stringify(k(l,R(a))),U=m.replace(/^(\s*worktree\s*=).*$/m,`$1 ${te}`);U!==m&&J(v,U)}else r!==".git"&&$(a).isDirectory()&&p(a)}},"visit");p(t)}h(I,"repairSubmoduleLinks");function ue(e,t){return Z("git",["-c",`safe.directory=${e}`,"-C",e,...t],{stdio:"pipe",encoding:"utf8"})}h(ue,"gitOut");function g(e){try{return ne(e)}catch{return C(e)}}h(g,"real");function he(e,t){try{const s=g(t);return ue(e,["worktree","list","--porcelain"]).split(`
3
- `).some(p=>p.startsWith("worktree ")&&g(p.slice(9))===s)}catch{return!1}}h(he,"registered");function we(e,t){if(!u(e))return!1;try{const s=T(n(t,".git"),"utf8");return s.startsWith("gitdir: ")?u(C(t,s.slice(8).trim())):!1}catch{return!1}}h(we,"repairable");function ee(e,t,s){s.dryRun||t.state!=="open"&&t.cleanup_state==="complete"||e.prepare("UPDATE worktrees SET state = CASE WHEN state = 'open' THEN 'closed' ELSE state END, cleanup_state = 'complete', closed_at = COALESCE(closed_at, ?) WHERE node_id = ?").run(new Date().toISOString(),t.node_id)}h(ee,"closeRow");function ye(e){const t=N(e,!0);if(t){try{const s=W(t),p=t.prepare("SELECT node_id, state, cleanup_state, path, repo_root FROM worktrees").all();for(const o of p){const r=s.get(o.node_id);if(!r)throw new Error(`worktree belongs to unknown node: ${o.node_id}`);if(!q(o.path))throw new Error(`worktree path is not absolute: ${o.path}`);const a=n(e.map.old.targetRoot,"apps",r,"nodes",o.node_id,"worktree");if(o.path===a)continue;const d=o.path===e.map.old.storedCanvas||o.path.startsWith(e.map.old.storedCanvas+P)?n(e.map.old.canvas,k(e.map.old.storedCanvas,o.path)):o.path,i=_(a,e);if(!u(d)){e.map.add(o.path,a),u(i)?!e.dryRun&&u(o.repo_root)&&(L(o.repo_root,["worktree","repair",i]),I(d,i)):o.state!=="open"&&o.cleanup_state==="complete"||(ee(t,o,e),e.resolutions.push(`missing-worktree: ${o.path} (node ${o.node_id}) is gone; closed its row`));continue}if(!he(o.repo_root,d))if(o.state==="open"&&we(o.repo_root,d))e.resolutions.push(`unregistered-worktree: ${o.path} (node ${o.node_id}) is not registered in ${o.repo_root}; relinked it with git worktree repair`),e.dryRun||L(o.repo_root,["worktree","repair",d]);else{const c=H(e,"worktrees",x(d));e.resolutions.push(`unregistered-worktree: ${o.path} (node ${o.node_id}${o.state==="open"?"":`, ${o.state}`}) is not a worktree of ${u(o.repo_root)?o.repo_root:`the missing repository ${o.repo_root}`}; ${o.state==="open"?"closed its row and ":""}moved the folder to ${c}`),e.map.redirect(o.path,n(e.map.old.targetRoot,k(e.map.old.root,c))),e.dryRun||(O(d,"worktrees",e),ee(t,o,e));continue}if(e.map.add(o.path,a),u(i)){const c=H(e,"replaced",k(e.map.old.root,i));e.resolutions.push(`stray-destination: ${i} predates the migration; moved it to ${c} and put the worktree ${d} in its place`),e.dryRun||O(i,"replaced",e,k(e.map.old.root,i))}if(!e.dryRun){B(R(i),{recursive:!0});try{L(o.repo_root,["worktree","move",d,i])}catch(c){if(!String(c.stderr??"").includes("submodule"))throw c;M(d,i),L(o.repo_root,["worktree","repair",i])}I(d,i),e.moved++,e.changes.push(`${d} -> ${i} (git worktree move)`)}}}finally{t.close()}e.dryRun||_e(e)}}h(ye,"worktrees");function _e(e){const t=n(e.map.old.canvas,"nodes");if(u(t)){for(const s of E(t)){const p=n(t,s);$(p).isDirectory()&&E(p).length===0&&F(p)}E(t).length===0&&F(t)}}h(_e,"pruneOldNodeFolders");function Ce(e){ye(e);const t=V(e);X(t,e);for(const{source:s,target:p,files:o}of t.folders)p&&K(n(s,"home"),_(p.home,e),n(o,"home"),e);fe(e),ie(e)}h(Ce,"previewStores");export{fe as conversations,Fe as memoryAndProfiles,Ce as previewStores,V as profileFolders,De as repairMovedRepositories,ye as worktrees};
1
+ var se=Object.defineProperty;var h=(e,t)=>se(e,"name",{value:t,configurable:!0});import{DatabaseSync as z}from"node:sqlite";import{closeSync as re,copyFileSync as ne,existsSync as u,realpathSync as pe,fsyncSync as ce,lstatSync as $,mkdirSync as B,openSync as ae,readFileSync as T,readdirSync as E,renameSync as M,rmdirSync as F,unlinkSync as L,writeFileSync as J}from"node:fs";import{execFileSync as Y}from"node:child_process";import{basename as A,dirname as b,isAbsolute as Z,join as n,relative as k,resolve as C,sep as P}from"node:path";import{LEGACY_LOCAL_PERSON as q}from"../canvas/legacy-person.js";import{profileSpace as ie}from"../layout.js";import{isJunkFile as j,movePath as S,sameFile as O,setAside as x,setAsidePath as H}from"./index.js";import{rewriteBodyPaths as de}from"./stored-paths.js";function D(e,t){Y("git",["-c",`safe.directory=${e}`,"-C",e,...t],{stdio:"pipe"})}h(D,"gitInRepo");function _(e,t){const{root:s,targetRoot:p}=t.map.old;return C(s,k(p,e))}h(_,"physical");function N(e,t=!1){const{old:s}=e.map,p=n(s.canvas,"canvas.db"),o=n(s.root,"runtime","canvas.db"),r=u(p)?p:o;return u(r)&&$(r).size>0?new z(r,{readOnly:!t||e.dryRun}):null}h(N,"canvasDb");function W(e){return new Map(e.prepare("SELECT node_id, grantee FROM nodes").all().map(({node_id:t,grantee:s})=>{const p=s===q?"terminal":s.startsWith("app:")?s.slice(4):"";if(!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(p))throw new Error(`node ${t} has no valid app`);return[t,p]}))}h(W,"nodeApps");function G(e,t,s){return n(s.map.old.targetRoot,"apps",e,"files","profiles",t)}h(G,"profileFiles");function K(e,t,s,p){if(!u(e))return;if(!u(t)){S(e,t,p);return}const o=$(e),r=$(t);if(o.isDirectory()&&r.isDirectory()){for(const d of E(e))K(n(e,d),n(t,d),n(s,d),p);F(e);return}if(o.isFile()&&r.isFile()&&O(e,t)){L(e);return}const a=_(s,p);p.dryRun||S(e,a,p),p.resolutions.push(`profile-home: ${t} differs from the profile's ${e}; kept the app home's copy, moved the profile's to ${a}`)}h(K,"mergeHome");function Q(e,t,s,p){if(!u(e)||!u(t))return;const o=$(e),r=$(t);if(o.isDirectory()&&r.isDirectory()){for(const a of E(e))Q(n(e,a),n(t,a),n(s,a),p);return}o.isFile()&&r.isFile()&&O(e,t)||p.resolutions.push(`profile-home: ${t} differs from the profile's ${e}; kept the app home's copy, moved the profile's to ${_(s,p)}`)}h(Q,"previewHome");function V(e,t,s){e.bodyPaths.some(([p])=>p===t)||e.bodyPaths.push([t,s])}h(V,"bodyPath");function X(e){const{map:t}=e,s=[n(t.old.scope,"operations.db"),n(t.old.root,"runtime","scope","operations.db")].find(u),p=s?new z(s,{readOnly:!0}):null,o=new Map;try{if(p&&p.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='profiles'").get())for(const i of p.prepare("SELECT id, grantee FROM profiles").all())o.set(i.id,i.grantee)}finally{p?.close()}const r=new Map;for(const[i,c]of o){const w=c==null||c===q?"terminal":c.startsWith("app:")?c.slice(4):"",f=ie(w,i),y=n(t.old.storedScope,"profiles",i);t.retire(y,f.dir),t.add(n(y,"memory"),f.dir),t.add(n(y,"env.json"),f.env),t.add(n(y,"home"),f.home),r.set(i,{target:f,app:w})}const a=n(t.old.scope,"profiles"),d={folders:[],junk:[],aside:[]};if(!u(a))return d;for(const i of E(a)){const c=n(a,i);if(!$(c).isDirectory()){(j(i)?d.junk:d.aside).push(c);continue}if(i===".locks"){for(const l of E(c)){const v=n(c,l);!(l.endsWith(".lock")&&$(v).isFile())&&!j(l)&&d.aside.push(v)}continue}const w=n(t.old.storedScope,"profiles",i),f=r.get(i);if(!f){const l=G("terminal",i,e);t.add(w,l),V(e,w,l),d.folders.push({id:i,source:c,target:null,files:l,extras:[]});continue}const y=G(f.app,i,e),R=[];for(const l of E(c)){const v=$(n(c,l));(l==="memory"||l==="home"?v.isDirectory():l==="env.json"||l==="profile.json"||l.endsWith(".lock")?v.isFile():j(l))||(R.push(l),t.add(n(w,l),n(y,l)),V(e,n(w,l),n(y,l)))}d.folders.push({id:i,source:c,target:f.target,files:y,extras:R})}return d}h(X,"profileFolders");function g(e,t){for(const s of e.folders){s.target||t.resolutions.push(`profile-folder: ${s.source} has no profile row; moved whole to ${_(s.files,t)}`);for(const p of s.extras)t.resolutions.push(`profile-extra: ${n(s.source,p)} moved to ${_(n(s.files,p),t)}`)}for(const s of e.junk)t.resolutions.push(`profile-leftover: deleted ${s} (not profile data)`);for(const s of e.aside)t.resolutions.push(`profile-leftover: ${s} is not a profile; moved to ${H(t,"profiles",A(s))}`)}h(g,"profileLines");function Ce(e){const{map:t}=e;S(n(t.old.scope,"memory"),n(t.old.root,"user","memory"),e);const s=N(e);if(s)try{for(const[r,a]of W(s)){const d=n(t.old.root,"apps",a,"memory","nodes",r);S(n(t.old.root,"apps",a,"nodes",r,"context","memory"),d,e),S(n(t.old.canvas,"nodes",r,"context","memory"),d,e)}}finally{s.close()}const p=X(e);g(p,e);for(const{source:r,target:a,files:d,extras:i}of p.folders){if(!a){S(r,_(d,e),e,!0);continue}for(const c of i)S(n(r,c),_(n(d,c),e),e);S(n(r,"memory"),_(a.dir,e),e),S(n(r,"env.json"),_(a.env,e),e),K(n(r,"home"),_(a.home,e),n(d,"home"),e);for(const c of E(r))L(n(r,c));F(r),e.moved++,e.changes.push(`${r} (removed)`)}for(const r of p.junk)L(r);for(const r of p.aside)x(r,"profiles",e);const o=n(t.old.scope,"profiles");if(u(o)){const r=n(o,".locks");if(u(r)){for(const a of E(r))L(n(r,a));F(r)}F(o)}}h(Ce,"memoryAndProfiles");function le(e,t,s,p){if(u(e)){if(!$(e).isFile())throw new Error(`session path is not a file: ${e}`);if(B(b(t),{recursive:!0}),u(t)){if($(t).isFile()&&O(e,t))return;const o=x(t,"replaced",s,k(s.map.old.root,t));s.resolutions.push(`stray-destination: ${t} predates the migration; moved it to ${o} and put ${e} in its place`)}if(p){const o=n(b(t),`.${A(t)}.migrating`);u(o)&&L(o),ne(e,o);const r=ae(o,"r");try{ce(r)}finally{re(r)}if(!O(e,o))throw new Error(`session copy verification failed: ${e}`);M(o,t),s.moved++,s.changes.push(`${e} -> ${t} (source retained)`);return}M(e,t),s.moved++,s.changes.push(`${e} -> ${t}`)}}h(le,"moveSession");function fe(e,t,s){const p=new Set(t.prepare("SELECT s.transcript_path AS path FROM sessions s JOIN nodes n ON n.node_id = s.node_id WHERE s.unbound_at IS NULL AND n.status IN ('active', 'idle') AND s.transcript_path IS NOT NULL").all().map(r=>r.path)),o=e.dryRun?null:t.prepare("UPDATE sessions SET transcript_path = NULL WHERE transcript_path = ?");for(const r of s)if(!(u(r.source)||u(_(r.destination,e)))){if(p.has(r.transcript_path)){e.resolutions.push(`lost-transcript: ${r.transcript_path} is missing but is a live node's current session; kept its path`);continue}o?.run(r.transcript_path),e.resolutions.push(`lost-transcript: ${r.transcript_path} is missing from both paths; cleared the session's transcript path`)}}h(fe,"lostTranscripts");function ue(e){const t=N(e,!0);if(t)try{const s=W(t),p=t.prepare("SELECT session_id, node_id, transcript_path FROM sessions WHERE transcript_path IS NOT NULL ORDER BY session_id, (unbound_at IS NULL) DESC, created_at DESC, rowid DESC").all(),o=new Map,r=new Map,a=new Map;for(const c of p){o.has(c.session_id)||o.set(c.session_id,c.node_id),a.set(c.transcript_path,(a.get(c.transcript_path)??0)+1);const w=r.get(c.transcript_path)??new Set;w.add(c.session_id),r.set(c.transcript_path,w)}const d=new Set,i=[];for(const{session_id:c,node_id:w,transcript_path:f}of p){if(d.has(f))continue;if(d.add(f),r.get(f).size!==1)throw new Error(`two conversations share a transcript: ${f}`);const y=a.get(f)>1?o.get(c):w,R=s.get(y);if(!R)throw new Error(`session belongs to unknown node: ${y}`);if(!Z(f))throw new Error(`session path is not absolute: ${f}`);const l=n(e.map.old.targetRoot,"apps",R,"conversations",y,A(f));if(f===l)continue;const v=f===e.map.old.storedAgent||f.startsWith(e.map.old.storedAgent+P),m=v?n(e.map.old.agent,k(e.map.old.storedAgent,f)):f;i.push({transcript_path:f,destination:l,source:m,retain:process.platform==="linux"&&v})}fe(e,t,i);for(const{transcript_path:c,destination:w}of i)e.map.add(c,w);if(e.dryRun)return;for(const{destination:c,source:w,retain:f}of i)le(w,_(c,e),e,f)}finally{t.close()}}h(ue,"conversations");function De(e){if(process.platform!=="linux")return;const t=N(e);if(t)try{const s=t.prepare("SELECT node_id, path, repo_root FROM worktrees").all(),p=W(t);for(const o of s){const r=e.map.mapPath(o.repo_root);if(r===o.repo_root)continue;const a=p.get(o.node_id);if(!a)throw new Error(`worktree belongs to unknown node: ${o.node_id}`);const d=_(n(e.map.old.targetRoot,"apps",a,"nodes",o.node_id,"worktree"),e);if(!u(d))continue;D(_(r,e),["worktree","repair",d]);const i=o.path.startsWith(e.map.old.storedCanvas+P)?n(e.map.old.canvas,k(e.map.old.storedCanvas,o.path)):o.path;I(i,d,e)}}finally{t.close()}}h(De,"repairMovedRepositories");function I(e,t,s){const p=h(o=>{for(const r of E(o)){const a=n(o,r);if(r===".git"&&a!==n(t,".git")&&$(a).isFile()){const d=T(a,"utf8");if(!d.startsWith("gitdir: "))throw new Error(`invalid submodule git link: ${a}`);const i=d.slice(8).trim(),c=C(b(a),i),w=n(e,k(t,a)),f=C(b(w),i),y=u(c)?c:f;if(!u(y))throw new Error(`submodule git directory missing: ${w}`);const R=s?.map.mapPath(y)??y,l=s&&R!==y?_(R,s):y;if(!u(l))throw new Error(`moved submodule git directory missing: ${l}`);c!==l&&J(a,`gitdir: ${k(b(a),l)}
2
+ `);const v=n(l,"config"),m=T(v,"utf8");if(!/^\s*worktree\s*=.*$/m.test(m))throw new Error(`submodule has no worktree setting: ${v}`);const oe=JSON.stringify(k(l,b(a))),U=m.replace(/^(\s*worktree\s*=).*$/m,`$1 ${oe}`);U!==m&&J(v,U)}else r!==".git"&&$(a).isDirectory()&&p(a)}},"visit");p(t)}h(I,"repairSubmoduleLinks");function he(e,t){return Y("git",["-c",`safe.directory=${e}`,"-C",e,...t],{stdio:"pipe",encoding:"utf8"})}h(he,"gitOut");function ee(e){try{return pe(e)}catch{return C(e)}}h(ee,"real");function we(e,t){try{const s=ee(t);return he(e,["worktree","list","--porcelain"]).split(`
3
+ `).some(p=>p.startsWith("worktree ")&&ee(p.slice(9))===s)}catch{return!1}}h(we,"registered");function ye(e,t){if(!u(e))return!1;try{const s=T(n(t,".git"),"utf8");return s.startsWith("gitdir: ")?u(C(t,s.slice(8).trim())):!1}catch{return!1}}h(ye,"repairable");function te(e,t,s){s.dryRun||t.state!=="open"&&t.cleanup_state==="complete"||e.prepare("UPDATE worktrees SET state = CASE WHEN state = 'open' THEN 'closed' ELSE state END, cleanup_state = 'complete', closed_at = COALESCE(closed_at, ?) WHERE node_id = ?").run(new Date().toISOString(),t.node_id)}h(te,"closeRow");function _e(e){const t=N(e,!0);if(t){try{const s=W(t),p=t.prepare("SELECT node_id, state, cleanup_state, path, repo_root FROM worktrees").all();for(const o of p){const r=s.get(o.node_id);if(!r)throw new Error(`worktree belongs to unknown node: ${o.node_id}`);if(!Z(o.path))throw new Error(`worktree path is not absolute: ${o.path}`);const a=n(e.map.old.targetRoot,"apps",r,"nodes",o.node_id,"worktree");if(o.path===a)continue;const d=o.path===e.map.old.storedCanvas||o.path.startsWith(e.map.old.storedCanvas+P)?n(e.map.old.canvas,k(e.map.old.storedCanvas,o.path)):o.path,i=_(a,e);if(!u(d)){e.map.add(o.path,a),u(i)?!e.dryRun&&u(o.repo_root)&&(D(o.repo_root,["worktree","repair",i]),I(d,i)):o.state!=="open"&&o.cleanup_state==="complete"||(te(t,o,e),e.resolutions.push(`missing-worktree: ${o.path} (node ${o.node_id}) is gone; closed its row`));continue}if(!we(o.repo_root,d))if(o.state==="open"&&ye(o.repo_root,d))e.resolutions.push(`unregistered-worktree: ${o.path} (node ${o.node_id}) is not registered in ${o.repo_root}; relinked it with git worktree repair`),e.dryRun||D(o.repo_root,["worktree","repair",d]);else{const c=H(e,"worktrees",A(d));e.resolutions.push(`unregistered-worktree: ${o.path} (node ${o.node_id}${o.state==="open"?"":`, ${o.state}`}) is not a worktree of ${u(o.repo_root)?o.repo_root:`the missing repository ${o.repo_root}`}; ${o.state==="open"?"closed its row and ":""}moved the folder to ${c}`),e.map.redirect(o.path,n(e.map.old.targetRoot,k(e.map.old.root,c))),e.dryRun||(x(d,"worktrees",e),te(t,o,e));continue}if(e.map.add(o.path,a),u(i)){const c=H(e,"replaced",k(e.map.old.root,i));e.resolutions.push(`stray-destination: ${i} predates the migration; moved it to ${c} and put the worktree ${d} in its place`),e.dryRun||x(i,"replaced",e,k(e.map.old.root,i))}if(!e.dryRun){B(b(i),{recursive:!0});try{D(o.repo_root,["worktree","move",d,i])}catch(c){if(!String(c.stderr??"").includes("submodule"))throw c;M(d,i),D(o.repo_root,["worktree","repair",i])}I(d,i),e.moved++,e.changes.push(`${d} -> ${i} (git worktree move)`)}}}finally{t.close()}e.dryRun||$e(e)}}h(_e,"worktrees");function $e(e){const t=n(e.map.old.canvas,"nodes");if(u(t)){for(const s of E(t)){const p=n(t,s);$(p).isDirectory()&&E(p).length===0&&F(p)}E(t).length===0&&F(t)}}h($e,"pruneOldNodeFolders");function Ae(e){_e(e);const t=X(e);g(t,e);for(const{source:s,target:p,files:o}of t.folders)p&&Q(n(s,"home"),_(p.home,e),n(o,"home"),e);ue(e),de(e)}h(Ae,"previewStores");export{ue as conversations,Ce as memoryAndProfiles,Ae as previewStores,X as profileFolders,De as repairMovedRepositories,_e as worktrees};