@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.
- package/dist/core/canvas/canvas.js +1 -1
- package/dist/core/grants/remove.d.ts +4 -0
- package/dist/core/grants/remove.js +1 -1
- package/dist/core/peer/home.d.ts +1 -1
- package/dist/core/peer/home.js +2 -2
- package/dist/core/runtime/runner/blaxel/runner.js +3 -3
- package/dist/daemon/reconcilers/grant-effects.js +1 -1
- package/package.json +2 -2
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +13 -13
- package/scripts/sandbox-bootstrap.sh +10 -4
- package/scripts/sandbox-start.mjs +54 -17
|
@@ -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
|
|
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};
|
package/dist/core/peer/home.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export interface HomeDeps {
|
|
|
22
22
|
machines(): Promise<Machine[]>;
|
|
23
23
|
self(): string;
|
|
24
24
|
now(): number;
|
|
25
|
-
/** Whether this daemon
|
|
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;
|
package/dist/core/peer/home.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var d=Object.defineProperty;var
|
|
2
|
-
ON CONFLICT(id) DO UPDATE SET machine_id = excluded.machine_id, learned_at = excluded.learned_at`).run(n,r,i)}
|
|
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
|
|
2
|
-
${
|
|
3
|
-
${
|
|
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
|
|
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.
|
|
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.
|
|
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",
|
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
11977
|
+
"version": "0.3.594",
|
|
11978
11978
|
"license": "UNLICENSED",
|
|
11979
11979
|
"dependencies": {
|
|
11980
|
-
"@north-light/api": "0.3.
|
|
11981
|
-
"@north-light/identity": "0.3.
|
|
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.
|
|
11986
|
+
"version": "0.3.594",
|
|
11987
11987
|
"license": "UNLICENSED",
|
|
11988
11988
|
"dependencies": {
|
|
11989
|
-
"@north-light/api": "0.3.
|
|
11990
|
-
"@north-light/identity": "0.3.
|
|
11991
|
-
"@north-light/plugin": "0.3.
|
|
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
|
|
9
|
-
#
|
|
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
|
|
55
|
-
|
|
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)
|
|
351
|
-
const
|
|
352
|
-
const
|
|
353
|
-
const
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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)); }
|