@north-light/cli 0.3.593 → 0.3.594

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.
@@ -64,4 +64,4 @@ var oe=Object.defineProperty;var o=(e,n)=>oe(e,"name",{value:n,configurable:!0})
64
64
  JOIN objects d ON d.id = w.object AND d.type = 'document' AND d.deleted_at IS NULL
65
65
  JOIN objects o ON o.id = w.watcher AND o.deleted_at IS NULL
66
66
  LEFT JOIN nodes n ON n.node_id = w.watcher
67
- WHERE w.watcher != d.owner AND d.owner IS NOT NULL AND (n.node_id IS NULL OR n.status IN ('active', 'idle'))`).all();return new Set(e.map(n=>n.owner))}o(Xe,"ownersOfWatchedDocuments");async function te(e){const t=(await s()).prepare("SELECT node_id FROM nodes WHERE run_id = ? AND node_id != ?"),a=new Set(e.map(i=>i.node_id));return await m(e,async i=>(await t.all(i.node_id,i.node_id)).every(r=>a.has(r.node_id)))}o(te,"retainRunRootsWithMembers");async function ae(e,n){if(e.length===0)return;const t=await O(e,async i=>await I(i.node_id)),a=await U(e.map(i=>i.node_id));for(const i of e)await p(async r=>await j(r,i.node_id));if(await x(a),n!==void 0){for(const i of t)await F(i,async()=>{await Z(i),ne(i)});n(t.flatMap(ee));return}for(const i of t)await F(i,async()=>await W(i))}o(ae,"deletePrunedNodes");async function F(e,n){try{n()}catch(t){Ee({level:"error",event:"canvas.node_files.remove_failed",error:t,fields:{node_id:e.nodeId}})}}o(F,"eachNodeBestEffort");function ie(e){let n=0;for(const t of[e.created,e.launched_at,e.last_settled_at,e.finalized_at,e.outcome_at,e.last_activity_at]){if(t==null)continue;const a=Date.parse(t);Number.isFinite(a)&&a>n&&(n=a)}try{const t=ue(S(Ue(e.node_id),"telemetry.json"),{throwIfNoEntry:!1});t!==void 0&&t.mtimeMs>n&&(n=t.mtimeMs)}catch{}return n}o(ie,"lastActivityMs");async function ht(e){const n=e.dryRun??!1,t=await Ye(),a=await m(t,async u=>await X(u)&&!await e.held(u.node_id)),i=t.length-a.length,r=Math.max(0,e.maxNodes-i),d=a.map(u=>({row:u,recency:ie(u)})).sort((u,_)=>_.recency-u.recency).slice(r).map(({row:u})=>({node_id:u.node_id,status:u.status,created:u.created})),l=await te(d);return n||l.length===0?{pruned:l,dryRun:n}:(await ae(l),{pruned:l,dryRun:n})}o(ht,"pruneToLimit");export{K as PARKED_RESIDENT_SQL,Ke as RESUMABLE_NODE_SQL,Fn as abandonCreateRequest,gn as assertRunNotDeleting,at as browseNodes,bn as canonicalSessionFile,wt as childrenOf,ft as claimWarmSpare,Vn as clearFinalReportIfMatches,et as clearFrozen,Wn as completeCreateRequest,An as createNode,Rt as deleteNode,St as deleteNodes,Pn as dueNodeDeadlines,kn as findCreateRequest,Cn as findNodeBySessionFile,Qn as getFinalReport,h as getNode,vn as getNodes,Hn as getRow,Mn as getRowByPane,ot as graphNodes,X as isCollectible,pt as isWarmSpare,ie as lastActivityMs,Ye as listNodes,tt as listNodesByProfile,st as listNodesForSupervision,ut as listParkedResidentsWithWakeMail,mt as listResumableRoots,rt as listTerminalPlacementResidue,_t as listWarmSpares,I as nodeFiles,Ve as nodesForSupervisionSql,xn as pruneExpiredCreateRequests,Ot as pruneNodes,ht as pruneToLimit,jn as recordCreateRequestNode,Gn as recordLaunch,nt as recordRespawnAttempt,Xn as recordSettledTurn,lt as registerWarmSpare,W as removeNodeFiles,Un as reserveCreateRequest,In as reserveCreateRequestNode,dt as resumableNodeCwds,yt as rosterNodes,Zn as setFrozen,Jn as setIntent,$n as setNodeDeadline,Kn as setPresence,zn as setStatus,qn as shiftProfileNodeDeadlines,Qe as terminalPlacementResidueSql,Yn as transitionStatusIntent,ct as unregisterWarmSpare,Bn as updateNode,it as updateNodeTelemetry,Et as warmSpareCount,Nt as withFreshTerminalGuard};
67
+ WHERE w.watcher != d.owner AND d.owner IS NOT NULL AND (n.node_id IS NULL OR n.status IN ('active', 'idle'))`).all();return new Set(e.map(n=>n.owner))}o(Xe,"ownersOfWatchedDocuments");async function te(e){const t=(await s()).prepare("SELECT node_id FROM nodes WHERE run_id = ? AND node_id != ?"),a=new Set(e.map(i=>i.node_id));return await m(e,async i=>(await t.all(i.node_id,i.node_id)).every(r=>a.has(r.node_id)))}o(te,"retainRunRootsWithMembers");async function ae(e,n){if(e.length===0)return;const t=await O(e,async i=>await I(i.node_id)),a=await U(e.map(i=>i.node_id));for(const i of e)await p(async r=>await j(r,i.node_id));if(await x(a),n!==void 0){for(const i of t)await F(i,async()=>{await Z(i),ne(i)});n(t.flatMap(ee));return}for(const i of t)await F(i,async()=>await W(i))}o(ae,"deletePrunedNodes");async function F(e,n){try{await n()}catch(t){Ee({level:"error",event:"canvas.node_files.remove_failed",error:t,fields:{node_id:e.nodeId}})}}o(F,"eachNodeBestEffort");function ie(e){let n=0;for(const t of[e.created,e.launched_at,e.last_settled_at,e.finalized_at,e.outcome_at,e.last_activity_at]){if(t==null)continue;const a=Date.parse(t);Number.isFinite(a)&&a>n&&(n=a)}try{const t=ue(S(Ue(e.node_id),"telemetry.json"),{throwIfNoEntry:!1});t!==void 0&&t.mtimeMs>n&&(n=t.mtimeMs)}catch{}return n}o(ie,"lastActivityMs");async function ht(e){const n=e.dryRun??!1,t=await Ye(),a=await m(t,async u=>await X(u)&&!await e.held(u.node_id)),i=t.length-a.length,r=Math.max(0,e.maxNodes-i),d=a.map(u=>({row:u,recency:ie(u)})).sort((u,_)=>_.recency-u.recency).slice(r).map(({row:u})=>({node_id:u.node_id,status:u.status,created:u.created})),l=await te(d);return n||l.length===0?{pruned:l,dryRun:n}:(await ae(l),{pruned:l,dryRun:n})}o(ht,"pruneToLimit");export{K as PARKED_RESIDENT_SQL,Ke as RESUMABLE_NODE_SQL,Fn as abandonCreateRequest,gn as assertRunNotDeleting,at as browseNodes,bn as canonicalSessionFile,wt as childrenOf,ft as claimWarmSpare,Vn as clearFinalReportIfMatches,et as clearFrozen,Wn as completeCreateRequest,An as createNode,Rt as deleteNode,St as deleteNodes,Pn as dueNodeDeadlines,kn as findCreateRequest,Cn as findNodeBySessionFile,Qn as getFinalReport,h as getNode,vn as getNodes,Hn as getRow,Mn as getRowByPane,ot as graphNodes,X as isCollectible,pt as isWarmSpare,ie as lastActivityMs,Ye as listNodes,tt as listNodesByProfile,st as listNodesForSupervision,ut as listParkedResidentsWithWakeMail,mt as listResumableRoots,rt as listTerminalPlacementResidue,_t as listWarmSpares,I as nodeFiles,Ve as nodesForSupervisionSql,xn as pruneExpiredCreateRequests,Ot as pruneNodes,ht as pruneToLimit,jn as recordCreateRequestNode,Gn as recordLaunch,nt as recordRespawnAttempt,Xn as recordSettledTurn,lt as registerWarmSpare,W as removeNodeFiles,Un as reserveCreateRequest,In as reserveCreateRequestNode,dt as resumableNodeCwds,yt as rosterNodes,Zn as setFrozen,Jn as setIntent,$n as setNodeDeadline,Kn as setPresence,zn as setStatus,qn as shiftProfileNodeDeadlines,Qe as terminalPlacementResidueSql,Yn as transitionStatusIntent,ct as unregisterWarmSpare,Bn as updateNode,it as updateNodeTelemetry,Et as warmSpareCount,Nt as withFreshTerminalGuard};
@@ -1,2 +1,6 @@
1
1
  /** Runtime spec §15 "On removal" (design flow 10). Audit and usage rows stay. */
2
2
  export declare function removeApp(app: string, sub?: string): Promise<void>;
3
+ /** What a fleet does when a person's grant of `app` is removed and the app itself stays: that person's nodes, crons, questions,
4
+ * sandboxes, mail, upload uses and node files go. Never the app's shared parts (shares, uploads, space, env, OS user), even for
5
+ * the last person. This is the one removal that accepts `app:terminal`, whose whole-app removal `removeApp` refuses. */
6
+ export declare function removePersonData(app: string, sub: string): Promise<void>;
@@ -1 +1 @@
1
- var _=Object.defineProperty;var E=(r,o)=>_(r,"name",{value:o,configurable:!0});import{rmSync as v}from"node:fs";import{join as S}from"node:path";import{deleteNodes as h}from"../canvas/canvas.js";import{withCanvasWrite as c,openDb as d}from"../canvas/db.js";import{cancelCron as O}from"../canvas/crons.js";import{emitEvent as p}from"../events/emit.js";import{appSpace as T,appsRoot as w,secretsDir as C}from"../layout.js";import{notifyRunDeleted as D}from"../runs/events.js";import{cancelOpenQuestions as L}from"../runs/questions.js";import{removeTreeBeneath as W}from"../spaces/open-beneath.js";import{markGranteeUploadsDeletePending as F,releaseRunUploads as H,turnSharesOffFor as N}from"../storage/tables.js";import{stopNodeSandboxes as M}from"../spaces/stop.js";import{activeAppUser as $,markAppUserRemoved as y}from"../spaces/users.js";import{launcherRequest as A}from"../../launcher/client.js";import{LaunchRefused as U}from"../../launcher/spec.js";import{localGrants as q}from"./mirror.js";import{launcherPresent as u,listSandboxes as I,stopSandboxPid as P}from"./sandboxes.js";import{filterAsync as B}from"../async-array.js";async function se(r,o){if(r==="app:terminal")throw new Error("app:terminal cannot be removed");const m=T(r),s=o===void 0||!(await q()).some(e=>e.grantee===r&&e.sub!==o&&e.status!=="removed"),i=s?await $(r):null;p({level:"info",event:"daemon.grants.removal_started",fields:{grantee:r,...o===void 0?{}:{sub:o},uid:i?.uid??null}});const t=o===void 0?{sql:"grantee = ?",args:[r]}:{sql:"grantee = ? AND sub = ?",args:[r,o]};for(const{node_id:e}of await(await d()).prepare(`SELECT node_id FROM node_rows WHERE ${t.sql} ORDER BY created DESC`).all(...t.args))await L({node_id:e},"the app was removed");const a=await c(async e=>(await e.prepare(`UPDATE nodes SET deleting_at = COALESCE(deleting_at, ?) WHERE node_id IN (SELECT node_id FROM node_rows WHERE ${t.sql})`).run(new Date().toISOString(),...t.args),(await e.prepare(`SELECT node_id FROM node_rows WHERE ${t.sql} ORDER BY created DESC`).all(...t.args)).map(n=>n.node_id)));for(const{cron_id:e}of await(await d()).prepare(`SELECT cron_id FROM crons WHERE ${t.sql}`).all(...t.args))await O(e);const l=(await Promise.all(a.map(e=>M(e,"app_removed",{deleting:!0})))).filter(e=>!e.confirmed);if(l.length)throw new Error(`sandbox teardown could not be confirmed for ${l.length} node(s) of ${r}`);if(i&&u())for(const e of await I())e.uid===i.uid&&await P(e.pid);const R=await B(a,async e=>(await(await d()).prepare("SELECT run_id FROM nodes WHERE node_id = ?").get(e))?.run_id===e);await c(async e=>{if(s&&await N(r,e),await H(a,e),s&&await F(r,e),!a.length)return;const n=a.map(()=>"?").join(","),g=await e.prepareUncached(`SELECT message_id FROM messages WHERE sender_node_id IN (${n}) UNION SELECT message_id FROM deliveries WHERE destination_id IN (${n})`).all(...a,...a);await e.prepareUncached(`DELETE FROM deliveries WHERE destination_id IN (${n})`).run(...a);for(const{message_id:f}of g)await e.prepare("DELETE FROM messages WHERE message_id = ? AND NOT EXISTS (SELECT 1 FROM deliveries WHERE message_id = ?)").run(f,f)}),await h(a);for(const e of R)D(e);if(s&&(v(S(C(),"env",m.root.slice(w().length+1)),{recursive:!0,force:!0}),W(w(),m.root)),i&&u())try{await A({operation:"user.delete",uid:i.uid})}catch(e){if(!(e instanceof U&&e.refusal==="invalid_uid"))throw e}i&&await y(i.uid),p({level:"info",event:"daemon.grants.removal_completed",fields:{grantee:r,...o===void 0?{}:{sub:o},uid:i?.uid??null,nodes:a.length}})}E(se,"removeApp");export{se as removeApp};
1
+ var h=Object.defineProperty;var l=(r,o)=>h(r,"name",{value:o,configurable:!0});import{rmSync as O}from"node:fs";import{join as D}from"node:path";import{deleteNodes as T,nodeFiles as C,removeNodeFiles as F}from"../canvas/canvas.js";import{hasUnreconciledWorktree as W}from"../canvas/worktrees.js";import{withCanvasWrite as p,openDb as m}from"../canvas/db.js";import{cancelCron as L}from"../canvas/crons.js";import{emitEvent as w}from"../events/emit.js";import{appSpace as N,appsRoot as u,secretsDir as H}from"../layout.js";import{notifyRunDeleted as y}from"../runs/events.js";import{cancelOpenQuestions as M}from"../runs/questions.js";import{removeTreeBeneath as $}from"../spaces/open-beneath.js";import{markGranteeUploadsDeletePending as U,releaseRunUploads as A,turnSharesOffFor as q}from"../storage/tables.js";import{stopNodeSandboxes as I}from"../spaces/stop.js";import{activeAppUser as P,markAppUserRemoved as k}from"../spaces/users.js";import{launcherRequest as b}from"../../launcher/client.js";import{LaunchRefused as j}from"../../launcher/spec.js";import{localGrants as B}from"./mirror.js";import{launcherPresent as g,listSandboxes as x,stopSandboxPid as G}from"./sandboxes.js";import{filterAsync as R}from"../async-array.js";async function Ee(r,o){if(r==="app:terminal")throw new Error("app:terminal cannot be removed");const s=o===void 0||!(await B()).some(d=>d.grantee===r&&d.sub!==o&&d.status!=="removed");await _(r,o,s)}l(Ee,"removeApp");async function pe(r,o){await _(r,o,!1,!0)}l(pe,"removePersonData");async function _(r,o,s,d=!1){const f=N(r),t=s?await P(r):null;w({level:"info",event:"daemon.grants.removal_started",fields:{grantee:r,...o===void 0?{}:{sub:o},uid:t?.uid??null}});const i=o===void 0?{sql:"grantee = ?",args:[r]}:{sql:"grantee = ? AND sub = ?",args:[r,o]};for(const{node_id:e}of await(await m()).prepare(`SELECT node_id FROM node_rows WHERE ${i.sql} ORDER BY created DESC`).all(...i.args))await M({node_id:e},"the app was removed");const a=await p(async e=>(await e.prepare(`UPDATE nodes SET deleting_at = COALESCE(deleting_at, ?) WHERE node_id IN (SELECT node_id FROM node_rows WHERE ${i.sql})`).run(new Date().toISOString(),...i.args),(await e.prepare(`SELECT node_id FROM node_rows WHERE ${i.sql} ORDER BY created DESC`).all(...i.args)).map(n=>n.node_id)));for(const{cron_id:e}of await(await m()).prepare(`SELECT cron_id FROM crons WHERE ${i.sql}`).all(...i.args))await L(e);const c=(await Promise.all(a.map(e=>I(e,"app_removed",{deleting:!0})))).filter(e=>!e.confirmed);if(c.length)throw new Error(`sandbox teardown could not be confirmed for ${c.length} node(s) of ${r}`);if(t&&g())for(const e of await x())e.uid===t.uid&&await G(e.pid);const v=await R(a,async e=>(await(await m()).prepare("SELECT run_id FROM nodes WHERE node_id = ?").get(e))?.run_id===e);if(await p(async e=>{if(s&&await q(r,e),await A(a,e),s&&await U(r,e),!a.length)return;const n=a.map(()=>"?").join(","),S=await e.prepareUncached(`SELECT message_id FROM messages WHERE sender_node_id IN (${n}) UNION SELECT message_id FROM deliveries WHERE destination_id IN (${n})`).all(...a,...a);await e.prepareUncached(`DELETE FROM deliveries WHERE destination_id IN (${n})`).run(...a);for(const{message_id:E}of S)await e.prepare("DELETE FROM messages WHERE message_id = ? AND NOT EXISTS (SELECT 1 FROM deliveries WHERE message_id = ?)").run(E,E)}),d){const e=await R(a,W);if(e.length)throw new Error(`refusing to delete nodes with unreconciled managed worktrees: ${e.join(", ")}`);for(const n of a)await F(await C(n))}await T(a);for(const e of v)y(e);if(s&&(O(D(H(),"env",f.root.slice(u().length+1)),{recursive:!0,force:!0}),$(u(),f.root)),t&&g())try{await b({operation:"user.delete",uid:t.uid})}catch(e){if(!(e instanceof j&&e.refusal==="invalid_uid"))throw e}t&&await k(t.uid),w({level:"info",event:"daemon.grants.removal_completed",fields:{grantee:r,...o===void 0?{}:{sub:o},uid:t?.uid??null,nodes:a.length}})}l(_,"cascade");export{Ee as removeApp,pe as removePersonData};
@@ -22,7 +22,7 @@ export interface HomeDeps {
22
22
  machines(): Promise<Machine[]>;
23
23
  self(): string;
24
24
  now(): number;
25
- /** Whether this daemon belongs to an account at all; an unjoined daemon has no peers, so nothing is ever "elsewhere". */
25
+ /** Whether this daemon has peer machines to ask; one without them has none, so nothing is ever "elsewhere". */
26
26
  joined(): boolean;
27
27
  }
28
28
  export declare const defaultHomeDeps: HomeDeps;
@@ -1,2 +1,2 @@
1
- var d=Object.defineProperty;var c=(e,n)=>d(e,"name",{value:n,configurable:!0});import{getById as p}from"../graph/objects.js";import{isUnreachable as _,peerCall as y}from"./client.js";import{listMachines as E,normalizeMachineId as w,ownMachineId as x}from"./directory.js";import{directoryIssuer as b}from"../directory/issuer.js";import{stubOf as O}from"./stubs.js";const l={call:c((e,n)=>y(e,n),"call"),machines:E,self:x,now:Date.now,joined:c(()=>b()!==null,"joined")};async function T(e,n){return(await e.prepare("SELECT machine_id FROM peer_locations WHERE id = ?").get(n))?.machine_id??null}c(T,"cachedLocation");async function I(e,n,r,i=Date.now()){await e.prepare(`INSERT INTO peer_locations (id, machine_id, learned_at) VALUES (?, ?, ?)
2
- ON CONFLICT(id) DO UPDATE SET machine_id = excluded.machine_id, learned_at = excluded.learned_at`).run(n,r,i)}c(I,"rememberLocation");async function L(e,n){await e.prepare("DELETE FROM peer_locations WHERE id = ?").run(n)}c(L,"dropLocation");async function g(e,n){if(!e.joined())return[];let r;try{r=await e.machines()}catch(a){return{where:"unknown",unreachable:[`directory (${a instanceof Error?a.message:String(a)})`]}}const i=e.self();return r.filter(a=>a.machine_id!==i&&a.machine_id!==n&&a.kind!=="device")}c(g,"peersToAsk");async function f(e,n,r=l,i){const a=await g(r,i);if("where"in a)return a;const o=await Promise.all(a.map(async t=>{const h=await r.call(t.machine_id,{method:"POST",path:"/v1/peer/locate",body:{id:n}});if(_(h))return{m:t,state:"unreachable"};const s=h.body;return h.status===200&&s?.answer==="here"?{m:t,state:"here",machine:t.machine_id}:h.status===200&&s?.answer==="moved"&&typeof s.machine=="string"?{m:t,state:"here",machine:w(s.machine)}:h.status===200&&s?.answer==="no"?{m:t,state:"no"}:{m:t,state:"unreachable"}})),u=o.find(t=>t.state==="here");if(u&&"machine"in u)return await I(e,n,u.machine,r.now()),{where:"machine",machine:u.machine,source:"locate"};const m=o.filter(t=>t.state==="unreachable").map(t=>t.m.name||t.m.machine_id);return m.length>0?{where:"unknown",unreachable:m}:{where:"nowhere"}}c(f,"locate");async function N(e,n,r={}){const i=await p(e,n);if(i!==null){const o=await O(e,i);return o===null?{where:"here"}:{where:"machine",machine:o.home_machine,source:"stub"}}if(!(r.deps??l).joined())return{where:"nowhere"};if(r.hint)return{where:"machine",machine:w(r.hint),source:"hint"};const a=await T(e,n);return a!==null?{where:"machine",machine:a,source:"cache"}:f(e,n,r.deps)}c(N,"homeOf");async function j(e,n,r,i=l){return await L(e,n),f(e,n,i,r)}c(j,"relocate");export{T as cachedLocation,l as defaultHomeDeps,L as dropLocation,N as homeOf,f as locate,g as peersToAsk,j as relocate,I as rememberLocation};
1
+ var d=Object.defineProperty;var o=(e,n)=>d(e,"name",{value:n,configurable:!0});import{getById as p}from"../graph/objects.js";import{isUnreachable as _,peerCall as y}from"./client.js";import{listMachines as E,normalizeMachineId as w,ownMachineId as x}from"./directory.js";import{directoryIssuer as b}from"../directory/issuer.js";import{isCloudRole as O}from"../canvas/person-state.js";import{stubOf as T}from"./stubs.js";const l={call:o((e,n)=>y(e,n),"call"),machines:E,self:x,now:Date.now,joined:o(()=>!O()&&b()!==null,"joined")};async function I(e,n){return(await e.prepare("SELECT machine_id FROM peer_locations WHERE id = ?").get(n))?.machine_id??null}o(I,"cachedLocation");async function L(e,n,r,i=Date.now()){await e.prepare(`INSERT INTO peer_locations (id, machine_id, learned_at) VALUES (?, ?, ?)
2
+ ON CONFLICT(id) DO UPDATE SET machine_id = excluded.machine_id, learned_at = excluded.learned_at`).run(n,r,i)}o(L,"rememberLocation");async function g(e,n){await e.prepare("DELETE FROM peer_locations WHERE id = ?").run(n)}o(g,"dropLocation");async function D(e,n){if(!e.joined())return[];let r;try{r=await e.machines()}catch(a){return{where:"unknown",unreachable:[`directory (${a instanceof Error?a.message:String(a)})`]}}const i=e.self();return r.filter(a=>a.machine_id!==i&&a.machine_id!==n&&a.kind!=="device")}o(D,"peersToAsk");async function f(e,n,r=l,i){const a=await D(r,i);if("where"in a)return a;const c=await Promise.all(a.map(async t=>{const h=await r.call(t.machine_id,{method:"POST",path:"/v1/peer/locate",body:{id:n}});if(_(h))return{m:t,state:"unreachable"};const s=h.body;return h.status===200&&s?.answer==="here"?{m:t,state:"here",machine:t.machine_id}:h.status===200&&s?.answer==="moved"&&typeof s.machine=="string"?{m:t,state:"here",machine:w(s.machine)}:h.status===200&&s?.answer==="no"?{m:t,state:"no"}:{m:t,state:"unreachable"}})),u=c.find(t=>t.state==="here");if(u&&"machine"in u)return await L(e,n,u.machine,r.now()),{where:"machine",machine:u.machine,source:"locate"};const m=c.filter(t=>t.state==="unreachable").map(t=>t.m.name||t.m.machine_id);return m.length>0?{where:"unknown",unreachable:m}:{where:"nowhere"}}o(f,"locate");async function v(e,n,r={}){const i=await p(e,n);if(i!==null){const c=await T(e,i);return c===null?{where:"here"}:{where:"machine",machine:c.home_machine,source:"stub"}}if(!(r.deps??l).joined())return{where:"nowhere"};if(r.hint)return{where:"machine",machine:w(r.hint),source:"hint"};const a=await I(e,n);return a!==null?{where:"machine",machine:a,source:"cache"}:f(e,n,r.deps)}o(v,"homeOf");async function A(e,n,r,i=l){return await g(e,n),f(e,n,i,r)}o(A,"relocate");export{I as cachedLocation,l as defaultHomeDeps,g as dropLocation,v as homeOf,f as locate,D as peersToAsk,A as relocate,L as rememberLocation};
@@ -1,3 +1,3 @@
1
- var R=Object.defineProperty;var i=(n,r)=>R(n,"name",{value:r,configurable:!0});import{openDb as k}from"../../../canvas/db.js";import{mintExecutionToken as I}from"../../../canvas/node-tokens.js";import{BlaxelApiError as T,isWorkloadUnavailable as x}from"./client.js";const f=4180,d="view",C="node /opt/crtr/current/scripts/sandbox-start.mjs --wake",S=3,N=/^crtr-sandbox-start: failed (\S+)/m,m=i(n=>`broker-${n}`,"brokerProcessName"),E=i(n=>n.sandboxName??`${n.nodeId}-${n.attempt}`,"sandboxNameFor");async function y(n){return(await(await k()).prepare("SELECT DISTINCT sandbox_name FROM executions WHERE node_id = ? AND sandbox_name IS NOT NULL").all(n)).map(l=>l.sandbox_name)}i(y,"defaultSandboxNames");function P(n){const r=`${n.logs}
2
- ${n.stderr}
3
- ${n.stdout}`;return{state:"failed",reason:N.exec(r)?.[1]??"start_failed",code:n.exitCode}}i(P,"failureOf");function h(n){const{client:r}=n,l=(n.stopGraceS??10)*1e3,b=new Map;async function u(t){return await r.getSandbox(E(t))}return i(u,"sandboxOf"),{kind:"blaxel",async start(t){const a=await n.launchContext(t),e=E(t),o=await r.createSandbox({name:e,image:a.image,memoryMb:a.memoryMb,ports:[{name:d,target:f}]}),s=m(t.executionId);if((await r.getProcess(o,s))?.status==="running")return{pid:null,exited:null};await r.ensurePreview(e,d,f);const w=await(n.mintToken??(p=>I(p.executionId,p.nodeId)))(t),g={...a.secrets,CRTR_API_URL:a.apiUrl,CRTR_EXECUTION_ID:t.executionId,CRTR_NODE_TOKEN:w,CRTR_RELEASE:t.release,CRTR_RELEASE_URL:a.releaseUrl,CRTR_RELEASE_SHA256:a.releaseSha256};return await r.startProcess(o,{name:s,command:n.bootstrap(),env:g,waitForCompletion:!1}),{pid:null,exited:null}},async wake(t){const a=await u(t);if(a===null)return"no_broker";try{const e=await r.exec(a,`wake-${t.executionId}-${Date.now()}`,`${C} ${t.executionId}`,30);if(e.exitCode===0)return"signalled";if(e.exitCode===S)return"no_broker";throw new Error(`wake of ${t.executionId} failed (exit ${e.exitCode}): ${(e.stderr||e.logs).slice(0,300)}`)}catch(e){if(x(e))return"no_broker";throw e}},async stop(t,a){try{const e=await u(t);if(e===null)return{confirmed:!0};const o=m(t.executionId),s=i(async()=>(await r.getProcess(e,o))?.status==="running","running");if(!await s())return{confirmed:!0};await r.stopProcess(e,o);const c=Date.now()+l;for(;Date.now()<c;){if(!await s())return{confirmed:!0};await new Promise(w=>setTimeout(w,250))}return await r.killProcess(e,o),{confirmed:!await s()}}catch(e){return x(e)?{confirmed:!0}:{confirmed:!1,error:e instanceof Error?e:new Error(String(e))}}},async status(t,a){try{const e=await u(t);if(e===null)return{state:"gone"};if(a?.idle===!0)return{state:"running",pid:null};const o=await r.getProcess(e,m(t.executionId));return o===null?{state:"gone"}:o.status==="running"?{state:"running",pid:null}:o.status==="failed"||o.status==="completed"&&o.exitCode!==0?P(o):{state:"gone"}}catch(e){return x(e)?{state:"gone"}:{state:"unknown",error:e instanceof Error?e.message:String(e)}}},async delete(t){for(const a of await(n.sandboxNamesForNode??y)(t))await r.deleteSandbox(a)},async viewerAddress(t){const a=E(t),e=b.get(a);if(e&&e.expiresAt-Date.now()>5*6e4)return _(e.url,e.token);const o=await r.ensurePreview(a,d,f),s=new Date(Date.now()+60*6e4),{token:c}=await r.createPreviewToken(a,d,s);return b.set(a,{url:o,token:c,expiresAt:s.getTime()}),_(o,c)},async sandboxState(t){return(await u(t))?.state??null}}}i(h,"createBlaxelRunner");function _(n,r){return{kind:"wss",url:`${n.replace(/^https:/,"wss:")}/view`,headers:{"x-blaxel-preview-token":r}}}i(_,"viewer");export{f as BROKER_VIEW_PORT,T as BlaxelApiError,S as EXIT_NO_BROKER,N as FAILURE_LINE,C as WAKE_COMMAND,m as brokerProcessName,h as createBlaxelRunner,E as sandboxNameFor};
1
+ var $=Object.defineProperty;var i=(r,a)=>$(r,"name",{value:a,configurable:!0});import{openDb as k}from"../../../canvas/db.js";import{mintExecutionToken as I}from"../../../canvas/node-tokens.js";import{BlaxelApiError as T,isWorkloadUnavailable as f}from"./client.js";const x=4180,u="view",S="node /opt/crtr/current/scripts/sandbox-start.mjs --wake",C=3,y=/^crtr-sandbox-start: failed (\S+)/m,m=i(r=>`broker-${r}`,"brokerProcessName"),p=i(r=>r.sandboxName??`${r.nodeId}-${r.attempt}`,"sandboxNameFor");async function N(r){return(await(await k()).prepare("SELECT DISTINCT sandbox_name FROM executions WHERE node_id = ? AND sandbox_name IS NOT NULL").all(r)).map(l=>l.sandbox_name)}i(N,"defaultSandboxNames");function h(r){const a=`${r.logs}
2
+ ${r.stderr}
3
+ ${r.stdout}`;return{state:"failed",reason:y.exec(a)?.[1]??"start_failed",code:r.exitCode}}i(h,"failureOf");function v(r){const{client:a}=r,l=(r.stopGraceS??10)*1e3,E=new Map;async function d(t){return await a.getSandbox(p(t))}i(d,"sandboxOf");async function _(t,n){if(!/^[A-Za-z0-9_-]+$/.test(n))throw new Error(`execution id ${JSON.stringify(n)} is not safe to name in a command`);const e=Math.max(1,Math.ceil(l/1e3)),o=`p='[b]roker-cli\\.js .*--epoch ${n}( |$)'; pkill -TERM -f "$p"; i=0; while pgrep -f "$p" >/dev/null && [ $i -lt ${e*5} ]; do sleep 0.2; i=$((i+1)); done; pkill -KILL -f "$p"; sleep 0.3; ! pgrep -f "$p"`;return(await a.exec(t,`stop-${n}-${Date.now()}`,o,e+20)).exitCode===0}return i(_,"brokerGone"),{kind:"blaxel",async start(t){const n=await r.launchContext(t),e=p(t),o=await a.createSandbox({name:e,image:n.image,memoryMb:n.memoryMb,ports:[{name:u,target:x}]}),s=m(t.executionId);if((await a.getProcess(o,s))?.status==="running")return{pid:null,exited:null};await a.ensurePreview(e,u,x);const w=await(r.mintToken??(b=>I(b.executionId,b.nodeId)))(t),R={...n.secrets,CRTR_API_URL:n.apiUrl,CRTR_EXECUTION_ID:t.executionId,CRTR_NODE_TOKEN:w,CRTR_RELEASE:t.release,CRTR_RELEASE_URL:n.releaseUrl,CRTR_RELEASE_SHA256:n.releaseSha256};return await a.startProcess(o,{name:s,command:r.bootstrap(),env:R,waitForCompletion:!1}),{pid:null,exited:null}},async wake(t){const n=await d(t);if(n===null)return"no_broker";try{const e=await a.exec(n,`wake-${t.executionId}-${Date.now()}`,`${S} ${t.executionId}`,30);if(e.exitCode===0)return"signalled";if(e.exitCode===C)return"no_broker";throw new Error(`wake of ${t.executionId} failed (exit ${e.exitCode}): ${(e.stderr||e.logs).slice(0,300)}`)}catch(e){if(f(e))return"no_broker";throw e}},async stop(t,n){try{const e=await d(t);if(e===null)return{confirmed:!0};const o=m(t.executionId),s=i(async()=>(await a.getProcess(e,o))?.status==="running","running");if(await s()){await a.stopProcess(e,o);const c=Date.now()+l;for(;Date.now()<c&&await s();)await new Promise(w=>setTimeout(w,250));if(await s()&&await a.killProcess(e,o),await s())return{confirmed:!1}}return{confirmed:await _(e,t.executionId)}}catch(e){return f(e)?{confirmed:!0}:{confirmed:!1,error:e instanceof Error?e:new Error(String(e))}}},async status(t,n){try{const e=await d(t);if(e===null)return{state:"gone"};if(n?.idle===!0)return{state:"running",pid:null};const o=await a.getProcess(e,m(t.executionId));return o===null?{state:"gone"}:o.status==="running"?{state:"running",pid:null}:o.status==="failed"||o.status==="completed"&&o.exitCode!==0?h(o):{state:"gone"}}catch(e){return f(e)?{state:"gone"}:{state:"unknown",error:e instanceof Error?e.message:String(e)}}},async delete(t){for(const n of await(r.sandboxNamesForNode??N)(t))await a.deleteSandbox(n)},async viewerAddress(t){const n=p(t),e=E.get(n);if(e&&e.expiresAt-Date.now()>5*6e4)return g(e.url,e.token);const o=await a.ensurePreview(n,u,x),s=new Date(Date.now()+60*6e4),{token:c}=await a.createPreviewToken(n,u,s);return E.set(n,{url:o,token:c,expiresAt:s.getTime()}),g(o,c)},async sandboxState(t){return(await d(t))?.state??null}}}i(v,"createBlaxelRunner");function g(r,a){return{kind:"wss",url:`${r.replace(/^https:/,"wss:")}/view`,headers:{"x-blaxel-preview-token":a}}}i(g,"viewer");export{x as BROKER_VIEW_PORT,T as BlaxelApiError,C as EXIT_NO_BROKER,y as FAILURE_LINE,S as WAKE_COMMAND,m as brokerProcessName,v as createBlaxelRunner,p as sandboxNameFor};
@@ -1 +1 @@
1
- var v=Object.defineProperty;var a=(t,e)=>v(t,"name",{value:e,configurable:!0});import{openDb as d}from"../../core/canvas/db.js";import{endLiveExecution as _,liveExecutionForNode as E}from"../../core/canvas/executions.js";import{emitEvent as c}from"../../core/events/emit.js";import{afterRevocationStop as y}from"../../core/grants/on-sync.js";import{allGrants as u,grantIdsWhere as S,grantStanding as m,mirrorOnly as h}from"../../core/grants/mirror.js";import{removeApp as g}from"../../core/grants/remove.js";import{wakeAllGrantWaiters as k,wakeGrantWaiters as x}from"../../core/grants/watch.js";import{discardOrphanedTokens as w,discardTokensForGrant as N}from"../../core/providers/token-cache.js";import{runnerForExecution as O}from"../../core/runtime/runner/index.js";import{closeGrantStreams as T,heldGrantIds as b}from"../../core/runs/events.js";import{stopNodeSandboxes as L}from"../../core/spaces/stop.js";import{brokerSignalBus as R}from"../broker-signals/bus.js";import{withRowClaim as D}from"./row-claim.js";const G=6e4;function f(t,e,n){c({level:"error",event:`daemon.grants.${t}`,error:e,fields:n})}a(f,"failed");function A(t,e,n){try{const o=e.sub===null||e.grantee===null?w(n):N({sub:e.sub,grantee:e.grantee,active:e.standing==="active"},n);o.tokens>0&&c({level:"info",event:"daemon.grants.outbound_tokens_discarded",fields:{grant:t,tokens:o.tokens}})}catch(o){f("outbound_tokens_discard_failed",o,{grant:t})}}a(A,"discardTokens");async function p(){try{await(await import("../../core/providers/bundles.js")).syncProviderBundles()}catch(t){c({level:"error",event:"daemon.providers.bundle_sync_failed",error:t})}}a(p,"reconcileProviderBundles");async function P(t,e){if(x(t),e.standing==="inactive"){const n=T(t);n>0&&c({level:"info",event:"daemon.grants.streams_closed",fields:{grant:t,grantee:e.grantee,streams:n}})}await p()}a(P,"serveEffects");async function C(t,e){const n=await D("nodes",t,G,async o=>{const r=await E(t);if(r===null)return;await o.assertOwned(),c({level:"info",event:"daemon.grants.node_stop",node_id:t,fields:{grant:e,execution_id:r.executionId}});const i=a(async()=>{await o.assertOwned(),await _(t,{reason:"permission_revoked"}),await y(t)},"finish");if(r.runner==="process"){const l=await L(t,"permission_revoked",{holdRecovery:!0,after:i});if(!l.confirmed)throw l.error??new Error(`stop of ${t} was not confirmed`);return}const s=await O(r.runner).stop(r,{reason:"permission_revoked"});if(!s.confirmed)throw s.error??new Error(`stop of ${t} was not confirmed`);await i()})}a(C,"stopRevokedNode");async function W(t,e){if(!(e.grantee!==null&&h({grantee:e.grantee}))){if(e.standing==="inactive"){const n=await(await d()).prepare("SELECT DISTINCT n.node_id FROM nodes n JOIN executions e ON e.node_id = n.node_id AND e.state != 'exited' WHERE n.grant_id = ?").all(t);for(const{node_id:o}of n)try{await C(o,t)}catch(r){f("node_stop_failed",r,{grant:t,node_id:o})}}if(e.status==="removed"&&e.removal_done_at===null&&e.grantee!==null&&e.sub!==null)try{await g(e.grantee,e.sub),await(await d()).prepare("UPDATE grants SET removal_done_at = ? WHERE id = ? AND removal_done_at IS NULL").run(new Date().toISOString(),t)}catch(n){f("removal_failed",n,{grant:t,grantee:e.grantee})}}}a(W,"laneEffects");async function F(t,e){const n=await m(e);A(e,n,await u()),await Promise.all([t.serves?P(e,n):void 0,t.runsLanes&&n.standing!=="lagging"?W(e,n):void 0])}a(F,"applyGrantEffects");async function M(t){const e=new Set;if(t.serves)for(const o of b())e.add(o);if(t.runsLanes){const o=await(await d()).prepare("SELECT DISTINCT n.grant_id FROM nodes n JOIN executions e ON e.node_id = n.node_id AND e.state != 'exited' WHERE n.grant_id IS NOT NULL").all();for(const{grant_id:r}of o)e.add(r)}const n=[];for(const o of e)(await m(o)).status===null&&n.push(o);return n}a(M,"goneGrantIds");async function I(t){const e=new Map,n=a(r=>{const i=(e.get(r)??Promise.resolve()).then(()=>F(t,r)).catch(s=>f("effects_failed",s,{grant:r})).finally(()=>{e.get(r)===i&&e.delete(r)});return e.set(r,i),i},"run"),o=a(async()=>{const r=t.serves?p():void 0;try{t.serves&&k(),w(await u());const i=new Set(await S(`status <> 'active' OR expires_at <= '${new Date().toISOString()}'`));for(const s of await M(t))i.add(s);await Promise.all([...i].map(n))}catch(i){f("catch_up_failed",i,{})}await r},"catchUp");return await R.subscribe(r=>{n(r)},{kinds:["grant"],onListenStart:a(()=>{o()},"onListenStart")})}a(I,"startGrantEffects");export{F as applyGrantEffects,I as startGrantEffects};
1
+ var v=Object.defineProperty;var a=(t,e)=>v(t,"name",{value:e,configurable:!0});import{openDb as l}from"../../core/canvas/db.js";import{isCloudRole as _}from"../../core/canvas/person.js";import{endLiveExecution as E,liveExecutionForNode as y}from"../../core/canvas/executions.js";import{emitEvent as c}from"../../core/events/emit.js";import{afterRevocationStop as S}from"../../core/grants/on-sync.js";import{allGrants as u,grantIdsWhere as h,grantStanding as m,mirrorOnly as g}from"../../core/grants/mirror.js";import{removeApp as b,removePersonData as k}from"../../core/grants/remove.js";import{wakeAllGrantWaiters as x,wakeGrantWaiters as N}from"../../core/grants/watch.js";import{discardOrphanedTokens as p,discardTokensForGrant as O}from"../../core/providers/token-cache.js";import{runnerForExecution as T}from"../../core/runtime/runner/index.js";import{closeGrantStreams as L,heldGrantIds as R}from"../../core/runs/events.js";import{stopNodeSandboxes as D}from"../../core/spaces/stop.js";import{brokerSignalBus as G}from"../broker-signals/bus.js";import{withRowClaim as P}from"./row-claim.js";const A=6e4;function f(t,e,r){c({level:"error",event:`daemon.grants.${t}`,error:e,fields:r})}a(f,"failed");function C(t,e,r){try{const o=e.sub===null||e.grantee===null?p(r):O({sub:e.sub,grantee:e.grantee,active:e.standing==="active"},r);o.tokens>0&&c({level:"info",event:"daemon.grants.outbound_tokens_discarded",fields:{grant:t,tokens:o.tokens}})}catch(o){f("outbound_tokens_discard_failed",o,{grant:t})}}a(C,"discardTokens");async function w(){try{await(await import("../../core/providers/bundles.js")).syncProviderBundles()}catch(t){c({level:"error",event:"daemon.providers.bundle_sync_failed",error:t})}}a(w,"reconcileProviderBundles");async function W(t,e){if(N(t),e.standing==="inactive"){const r=L(t);r>0&&c({level:"info",event:"daemon.grants.streams_closed",fields:{grant:t,grantee:e.grantee,streams:r}})}await w()}a(W,"serveEffects");async function F(t,e){const r=await P("nodes",t,A,async o=>{const n=await y(t);if(n===null)return;await o.assertOwned(),c({level:"info",event:"daemon.grants.node_stop",node_id:t,fields:{grant:e,execution_id:n.executionId}});const i=a(async()=>{await o.assertOwned(),await E(t,{reason:"permission_revoked"}),await S(t)},"finish");if(n.runner==="process"){const d=await D(t,"permission_revoked",{holdRecovery:!0,after:i});if(!d.confirmed)throw d.error??new Error(`stop of ${t} was not confirmed`);return}const s=await T(n.runner).stop(n,{reason:"permission_revoked"});if(!s.confirmed)throw s.error??new Error(`stop of ${t} was not confirmed`);await i()})}a(F,"stopRevokedNode");async function M(t,e){if(!(e.grantee!==null&&g({grantee:e.grantee}))){if(e.standing==="inactive"){const r=await(await l()).prepare("SELECT DISTINCT n.node_id FROM nodes n JOIN executions e ON e.node_id = n.node_id AND e.state != 'exited' WHERE n.grant_id = ?").all(t);for(const{node_id:o}of r)try{await F(o,t)}catch(n){f("node_stop_failed",n,{grant:t,node_id:o})}}if(e.status==="removed"&&e.removal_done_at===null&&e.grantee!==null&&e.sub!==null)try{e.grantee!=="app:terminal"?await b(e.grantee,e.sub):_()?await k(e.grantee,e.sub):c({level:"info",event:"daemon.grants.removal_settled_without_cleanup",fields:{grant:t,grantee:e.grantee,sub:e.sub}}),await(await l()).prepare("UPDATE grants SET removal_done_at = ? WHERE id = ? AND removal_done_at IS NULL").run(new Date().toISOString(),t)}catch(r){f("removal_failed",r,{grant:t,grantee:e.grantee})}}}a(M,"laneEffects");async function U(t,e){const r=await m(e);C(e,r,await u()),await Promise.all([t.serves?W(e,r):void 0,t.runsLanes&&r.standing!=="lagging"?M(e,r):void 0])}a(U,"applyGrantEffects");async function $(t){const e=new Set;if(t.serves)for(const o of R())e.add(o);if(t.runsLanes){const o=await(await l()).prepare("SELECT DISTINCT n.grant_id FROM nodes n JOIN executions e ON e.node_id = n.node_id AND e.state != 'exited' WHERE n.grant_id IS NOT NULL").all();for(const{grant_id:n}of o)e.add(n)}const r=[];for(const o of e)(await m(o)).status===null&&r.push(o);return r}a($,"goneGrantIds");async function re(t){const e=new Map,r=a(n=>{const i=(e.get(n)??Promise.resolve()).then(()=>U(t,n)).catch(s=>f("effects_failed",s,{grant:n})).finally(()=>{e.get(n)===i&&e.delete(n)});return e.set(n,i),i},"run"),o=a(async()=>{const n=t.serves?w():void 0;try{t.serves&&x(),p(await u());const i=new Set(await h(`status <> 'active' OR expires_at <= '${new Date().toISOString()}'`));for(const s of await $(t))i.add(s);await Promise.all([...i].map(r))}catch(i){f("catch_up_failed",i,{})}await n},"catchUp");return await G.subscribe(n=>{r(n)},{kinds:["grant"],onListenStart:a(()=>{o()},"onListenStart")})}a(re,"startGrantEffects");export{U as applyGrantEffects,re as startGrantEffects};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.593",
3
+ "version": "0.3.594",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -145,7 +145,7 @@
145
145
  },
146
146
  "license": "UNLICENSED",
147
147
  "dependencies": {
148
- "@north-light/identity": "0.3.593",
148
+ "@north-light/identity": "0.3.594",
149
149
  "@earendil-works/chord": "1.1.0",
150
150
  "@earendil-works/pi-agent-core": "1.1.0",
151
151
  "@earendil-works/pi-ai": "1.1.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/identity",
3
- "version": "0.3.593",
3
+ "version": "0.3.594",
4
4
  "description": "Northlight access-token verification and scope rules",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.593",
3
+ "version": "0.3.594",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/cli",
9
- "version": "0.3.593",
9
+ "version": "0.3.594",
10
10
  "hasInstallScript": true,
11
11
  "license": "UNLICENSED",
12
12
  "workspaces": [
@@ -28,7 +28,7 @@
28
28
  "@milkdown/preset-gfm": "^7.22.3",
29
29
  "@milkdown/prose": "^7.22.3",
30
30
  "@milkdown/utils": "^7.22.3",
31
- "@north-light/identity": "0.3.593",
31
+ "@north-light/identity": "0.3.594",
32
32
  "cron-parser": "^5.6.0",
33
33
  "esbuild": "^0.27.7",
34
34
  "jose": "^6.2.12",
@@ -11956,17 +11956,17 @@
11956
11956
  },
11957
11957
  "packages/crouter-api": {
11958
11958
  "name": "@north-light/api",
11959
- "version": "0.3.593",
11959
+ "version": "0.3.594",
11960
11960
  "license": "UNLICENSED"
11961
11961
  },
11962
11962
  "packages/crouter-env-docker": {
11963
11963
  "name": "@north-light/env-docker",
11964
- "version": "0.3.593",
11964
+ "version": "0.3.594",
11965
11965
  "license": "UNLICENSED"
11966
11966
  },
11967
11967
  "packages/crouter-identity": {
11968
11968
  "name": "@north-light/identity",
11969
- "version": "0.3.593",
11969
+ "version": "0.3.594",
11970
11970
  "license": "UNLICENSED",
11971
11971
  "dependencies": {
11972
11972
  "jose": "^6.2.1"
@@ -11974,21 +11974,21 @@
11974
11974
  },
11975
11975
  "packages/crouter-plugin": {
11976
11976
  "name": "@north-light/plugin",
11977
- "version": "0.3.593",
11977
+ "version": "0.3.594",
11978
11978
  "license": "UNLICENSED",
11979
11979
  "dependencies": {
11980
- "@north-light/api": "0.3.593",
11981
- "@north-light/identity": "0.3.593"
11980
+ "@north-light/api": "0.3.594",
11981
+ "@north-light/identity": "0.3.594"
11982
11982
  }
11983
11983
  },
11984
11984
  "packages/crouter-sdk": {
11985
11985
  "name": "@north-light/sdk",
11986
- "version": "0.3.593",
11986
+ "version": "0.3.594",
11987
11987
  "license": "UNLICENSED",
11988
11988
  "dependencies": {
11989
- "@north-light/api": "0.3.593",
11990
- "@north-light/identity": "0.3.593",
11991
- "@north-light/plugin": "0.3.593",
11989
+ "@north-light/api": "0.3.594",
11990
+ "@north-light/identity": "0.3.594",
11991
+ "@north-light/plugin": "0.3.594",
11992
11992
  "jose": "^6.2.1"
11993
11993
  },
11994
11994
  "bin": {
@@ -5,8 +5,11 @@
5
5
  # CRTR_RELEASE_URL, CRTR_RELEASE_SHA256 and the rest the runner set. Once per sandbox it creates the `crtr` user
6
6
  # (`crtrd` refuses root) and its directories; it installs the release if it is absent (download, sha256 check,
7
7
  # unpack into a temp dir, rename into place, keep three), then hands over to `crtr-sandbox-start` as `crtr`.
8
- # The node token reaches the start script on stdin, never in its environment, so no process of user `crtr` can
9
- # read it from /proc. A failed download or checksum exits non-zero and prints `crtr-sandbox-start: failed <reason>`
8
+ # The node token reaches the start script on stdin, never in its environment, so it is in no process's
9
+ # /proc/<pid>/environ. It is not hidden from user `crtr` otherwise: the broker holds it in memory, and the agent's
10
+ # shell runs as the same user (design-fleet D13/D78 scope the token to this execution's node, nothing more). The final `exec` chain forks nothing (busybox `nsenter -S/-G` execs, and the token goes in as a here-document, not a pipe, which would
11
+ # put a forking shell in front of it), so the process Blaxel tracks as `broker-<execution id>` is the start script itself: stopping it reaches
12
+ # the node and the broker, and its exit means the broker is gone. A failed download or checksum exits non-zero and prints `crtr-sandbox-start: failed <reason>`
10
13
  # as its last line (the runner's `status` reports that reason).
11
14
  set -eu
12
15
 
@@ -51,5 +54,8 @@ done
51
54
 
52
55
  token=$CRTR_NODE_TOKEN
53
56
  unset CRTR_NODE_TOKEN
54
- # `su -p` keeps the rest of the environment; the start script derives HOME and the layout itself.
55
- printf '%s' "$token" | exec su -p -s /bin/sh "$RUN_USER" -c "exec node '$RELEASE_DIR/scripts/sandbox-start.mjs'"
57
+ # busybox `su` forks, which leaves the tracked process a wrapper whose children outlive a stop; `nsenter` with no namespace flag only sets the
58
+ # uid and gid, then execs. It keeps the environment (as `su -p` did); the start script derives HOME and the layout itself.
59
+ exec nsenter -S "$(id -u "$RUN_USER")" -G "$(id -g "$RUN_USER")" node "$RELEASE_DIR/scripts/sandbox-start.mjs" <<EOT
60
+ $token
61
+ EOT
@@ -344,34 +344,71 @@ async function start(env) {
344
344
  gitConfig('credential.https://github.com.helper', join(releaseDir, 'scripts', 'git-credential-crtr'));
345
345
  if (bundle.git_identity) { gitConfig('user.name', bundle.git_identity.name); gitConfig('user.email', bundle.git_identity.email); }
346
346
 
347
- // 5.5 plugins, once per revision.
347
+ // 5.5 plugins, once per revision. The person's install rows are already authoritative on the control plane, and no broker or daemon exists yet, so
348
+ // this materializes each plugin's code the way `pkg plugin install` lays it out in the user scope (`<runtime>/marketplaces/<name>`, `<runtime>/plugins/<plugin>`)
349
+ // with git alone; the release CLI would load the person's settings from a daemon this start cannot reach.
348
350
  const marker = join(work, '.crtr', 'plugins-revision');
349
351
  const have = existsSync(marker) ? readFileSync(marker, 'utf8').trim() : null;
350
- if (have !== String(bundle.plugins_revision) && bundle.plugins.length > 0) {
351
- const crtr = join(releaseDir, 'bin', 'crtr');
352
- const pluginEnv = { ...gitEnv, NPM_CONFIG_PREFIX: join(home, '.npm-global'), CRTR_HOME: root, CRTR_NO_AUTO_INIT: '1' };
353
- const crtrRun = (args) => run(process.execPath, [crtr, ...args], { env: pluginEnv, timeout: 300_000 });
352
+ if (have !== String(bundle.plugins_revision)) {
353
+ const pluginsDir = join(root, 'runtime', 'plugins');
354
+ const marketplacesDir = join(root, 'runtime', 'marketplaces');
355
+ const cloneEnv = { ...gitEnv, GIT_TERMINAL_PROMPT: '0' };
354
356
  const detail = (r) => (r.stderr || r.stdout || '').slice(0, 300);
355
- // A plugin of a marketplace is installed as `<marketplace name>/<plugin>`; the bundle names only the marketplace's url and ref, so the name is what
356
- // `pkg market add` reports. Adding re-clones, so a marketplace added once per start is current. A plugin already installed (an earlier revision) is
357
- // removed first, since install refuses an installed name.
357
+ const cloneInto = (url, ref, dest) => {
358
+ const r = run('git', ['clone', '--depth', '1', ...(ref ? ['--branch', ref] : []), url, dest], { env: cloneEnv, timeout: 300_000 });
359
+ return r.status === 0 ? null : detail(r) || `git clone exited ${r.status}`;
360
+ };
361
+ // The directory holds exactly the bundle's plugins: one an earlier revision installed and this one no longer lists is removed.
362
+ let current = null;
363
+ try {
364
+ const wanted = new Set(bundle.plugins.map((plugin) => plugin.name));
365
+ if (existsSync(pluginsDir)) for (const name of readdirSync(pluginsDir)) if (!wanted.has(name)) rmSync(join(pluginsDir, name), { recursive: true, force: true });
366
+ const readJson = (path) => { try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; } };
367
+ // A plugin of a marketplace is installed as `<plugin>` from the marketplace the bundle's url and ref name. Cloning again each start keeps a marketplace
368
+ // current; a plugin installed by an earlier revision is replaced.
358
369
  const marketplaces = new Map();
359
370
  for (const plugin of bundle.plugins) {
371
+ current = plugin.name;
360
372
  if ('path' in plugin.source) throw new StartFailure('plugin_source_unreachable', `${plugin.name}: its source is ${plugin.source.path} on the machine that registered it, which a sandbox cannot read`);
361
373
  const { url, ref } = plugin.source.marketplace;
362
374
  if (!url) throw new StartFailure('plugin_source_unreachable', `${plugin.name}: its marketplace has no url`);
375
+ const fail = (message) => new StartFailure('plugin_install_failed', `${plugin.name}: ${message}`);
376
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(plugin.name)) throw fail('invalid plugin name');
363
377
  const key = `${url}\0${ref ?? ''}`;
364
378
  if (!marketplaces.has(key)) {
365
- const added = crtrRun(['--json', 'pkg', 'market', 'add', url, ...(ref ? ['--ref', ref] : []), '--scope', 'user']);
366
- let name;
367
- try { name = JSON.parse(added.stdout).name; } catch { /* reported below */ }
368
- if (added.status !== 0 || typeof name !== 'string') throw new StartFailure('plugin_install_failed', `${plugin.name}: marketplace ${url}: ${detail(added)}`);
369
- marketplaces.set(key, name);
379
+ mkdirSync(marketplacesDir, { recursive: true });
380
+ const staging = join(marketplacesDir, `.clone-${process.pid}-${marketplaces.size}`);
381
+ rmSync(staging, { recursive: true, force: true });
382
+ const cloneError = cloneInto(url, ref, staging);
383
+ if (cloneError !== null) throw fail(`marketplace ${url}: ${cloneError}`);
384
+ const manifest = readJson(join(staging, '.crouter-marketplace', 'marketplace.json'));
385
+ if (typeof manifest?.name !== 'string' || !Array.isArray(manifest.plugins)) { rmSync(staging, { recursive: true, force: true }); throw fail(`marketplace ${url}: not a valid marketplace`); }
386
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(manifest.name)) { rmSync(staging, { recursive: true, force: true }); throw fail(`marketplace ${url}: invalid marketplace name ${JSON.stringify(manifest.name)}`); }
387
+ const dest = join(marketplacesDir, manifest.name);
388
+ rmSync(dest, { recursive: true, force: true });
389
+ renameSync(staging, dest);
390
+ marketplaces.set(key, { dest, manifest });
370
391
  }
371
- const removed = crtrRun(['pkg', 'plugin', 'remove', plugin.name, '--scope', 'user']);
372
- if (removed.status !== 0 && !/not found/i.test(removed.stderr + removed.stdout)) throw new StartFailure('plugin_install_failed', `${plugin.name}: remove: ${detail(removed)}`);
373
- const installed = crtrRun(['pkg', 'plugin', 'install', `${marketplaces.get(key)}/${plugin.name}`, '--scope', 'user']);
374
- if (installed.status !== 0) throw new StartFailure('plugin_install_failed', `${plugin.name}: ${detail(installed)}`);
392
+ const { dest: marketplaceDir, manifest } = marketplaces.get(key);
393
+ const entry = manifest.plugins.find((candidate) => candidate?.name === plugin.name);
394
+ if (!entry || typeof entry.source !== 'string') throw fail(`not found in marketplace ${manifest.name}`);
395
+ const target = join(pluginsDir, plugin.name);
396
+ mkdirSync(pluginsDir, { recursive: true });
397
+ rmSync(target, { recursive: true, force: true });
398
+ if (/^(https?:\/\/|git@|ssh:\/\/|file:\/\/)/.test(entry.source) || entry.source.endsWith('.git')) {
399
+ const cloneError = cloneInto(entry.source, undefined, target);
400
+ if (cloneError !== null) throw fail(cloneError);
401
+ } else {
402
+ const source = resolve(marketplaceDir, entry.source);
403
+ if (!existsSync(source) || !within(realpathSync(marketplaceDir), realpathSync(source))) throw fail(`plugin source ${entry.source} is not in marketplace ${manifest.name}`);
404
+ symlinkSync(source, target, 'dir');
405
+ }
406
+ if (readJson(join(target, '.crouter-plugin', 'plugin.json')) === null) { rmSync(target, { recursive: true, force: true }); throw fail('its .crouter-plugin/plugin.json is missing or invalid'); }
407
+ }
408
+ } catch (error) {
409
+ // A filesystem error (mkdir, remove, rename, symlink) is an install failure of the plugin being installed, not a generic start failure.
410
+ if (error instanceof StartFailure) throw error;
411
+ throw new StartFailure('plugin_install_failed', `${current ?? 'plugins'}: ${error.message}`.slice(0, 300));
375
412
  }
376
413
  }
377
414
  if (have !== String(bundle.plugins_revision)) { mkdirSync(dirname(marker), { recursive: true }); writeFileSync(marker, String(bundle.plugins_revision)); }