@north-light/cli 0.3.490 → 0.3.491

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.
@@ -7,5 +7,10 @@ import type { Socket } from 'node:net';
7
7
  * `handle` returns the single reply line and calls `requestRead` once the request is read, which
8
8
  * resumes the socket so a peer that leaves during a long operation (`wait`) is noticed and destroyed.
9
9
  * Destroying happens after the reply flushes, so an async reply is never truncated.
10
+ *
11
+ * Contract: a peer's FIN after its request means it left (the socket is not `allowHalfOpen`). The only
12
+ * client, `launcherRequest` in client.ts, never half-closes and waits for the reply; an `allowHalfOpen`
13
+ * listener could not tell a half-close from a departure and would hold the fd of every client that
14
+ * leaves during `wait`.
10
15
  */
11
16
  export declare function serveAccepted(socket: Socket, handle: (requestRead: () => void) => Promise<string>): void;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A launch pins `/etc/passwd` and `/etc/group` as descriptors and bwrap binds them a moment later. Replacing
3
+ * either file in that interval hands bwrap an unlinked inode ("Can't find source path /proc/self/fd/N"). Every
4
+ * launch runs through `launch`, and every registry write through `write`: a write holds back launches that
5
+ * arrive after it, waits for those already running, then runs with none in flight.
6
+ */
7
+ export declare class LaunchGate {
8
+ private readonly running;
9
+ private writers;
10
+ private release;
11
+ private open;
12
+ launch<T>(start: () => Promise<T>): Promise<T>;
13
+ write<T>(run: () => T): Promise<T>;
14
+ }
@@ -0,0 +1 @@
1
+ var r=Object.defineProperty;var n=(t,i)=>r(t,"name",{value:i,configurable:!0});class h{static{n(this,"LaunchGate")}running=new Set;writers=0;release;open=Promise.resolve();async launch(i){for(;this.writers>0;)await this.open;const e=i();this.running.add(e);const s=n(()=>{this.running.delete(e)},"done");return e.then(s,s),e}async write(i){this.writers++===0&&(this.open=new Promise(e=>{this.release=e}));try{for(;this.running.size>0;)await Promise.allSettled([...this.running]);return i()}finally{--this.writers===0&&this.release()}}}export{h as LaunchGate};
@@ -1,3 +1,3 @@
1
- var q=Object.defineProperty;var h=(l,d)=>q(l,"name",{value:d,configurable:!0});import{spawn as V}from"node:child_process";import{createHash as z}from"node:crypto";import{chownSync as O,chmodSync as P,closeSync as Q,existsSync as W,mkdirSync as A,readFileSync as X,rmSync as Y,statSync as Z}from"node:fs";import{createServer as ee}from"node:net";import{dirname as H,join as _}from"node:path";import{fileURLToPath as te}from"node:url";import{loadLinuxNative as re}from"../native/linux.js";import{envHomeOverride as oe}from"../shared/env.js";import{launchSandbox as ne}from"./bwrap.js";import{serveAccepted as se}from"./accepted-socket.js";import{installNetworkRule as ie}from"./netrule.js";import{LAUNCHER_SOCKET as v,LaunchRefused as f}from"./spec.js";import{systemIdentity as ae,AppUsers as ce}from"./users.js";import{SourceValidator as de}from"./validate.js";import{prepareDaemonScopeDirectories as ue}from"./scope-directories.js";import{LimitsManager as le}from"./limits/manager.js";import{CRTRD_OOM_SCORE_ADJ as pe}from"./limits/policy.js";import{writeLimitsLog as fe}from"./limits/log.js";const B=re();function U(l){const d=l._handle?.fd;if(d===void 0||d<0)throw new Error("launcher socket fd unavailable");return d}h(U,"socketFd");async function me(){if(process.platform!=="linux"||process.getuid?.()!==0||!B)throw new Error("launcher requires Linux root and its native addon");const{daemonUid:l,daemonGid:d,dataGid:T}=ae(),I=H(v);if(A(I,{recursive:!0,mode:493}),(Z(I).mode&511)!==493)throw new Error(`${I} must be mode 0755`);O(I,l,d),process.umask(7);const G=_(I,"nodes");A(G,{recursive:!0,mode:504}),O(G,0,d),P(G,504);const S=new de(oe()||"/",d);ue(_(S.root,"runtime"),l,d);const b=new ce(T);ie();const p=new le({runtimeRoot:S.runtime,appsRoot:_(S.root,"apps"),stateDir:_(S.runtime,"limits-state"),dataGid:T,users:b});p.init();const L=new Map,$=h(r=>{const u=p.takeEvents();for(const s of u)fe(s);return u.length===0?r:{...r,events:u}},"reply_"),k=new Map,x=new Map,F=h((r,u)=>{const s=u.capture,c=z("sha256"),e=h((a,n)=>{let m=0,w=!1;const R=[];return{take(M){n?.update(M);const C=Math.max(0,Math.min(M.length,a-m));C&&(R.push(M.subarray(0,C)),m+=C),C!==M.length&&(w=!0)},result:h(()=>({text:Buffer.concat(R).toString("utf8"),truncated:w}),"result")}},"chunks"),i=e(s.stdoutBytes,c),t=e(s.stderrBytes);let o=Buffer.alloc(0),y=0;return r.stdout?.on("data",a=>i.take(a)),r.stderr?.on("data",a=>{const n=a;s.stderrTail?(y+=n.length,o=n.length>=s.stderrBytes?n.subarray(n.length-s.stderrBytes):Buffer.concat([o,n]).subarray(Math.max(0,o.length+n.length-s.stderrBytes))):t.take(n)}),new Promise(a=>r.once("close",(n,m)=>{const w=i.result(),R=s.stderrTail?{text:o.toString("utf8"),truncated:y>s.stderrBytes}:t.result();a({exitCode:n,signal:m,stdout:w.text,stderr:R.text,stdoutTruncated:w.truncated,stderrTruncated:R.truncated,stdoutSha256:c.digest("hex")})}))},"captureExit"),J=h(r=>{const u=new Set;for(;r>1&&!u.has(r);){const s=k.get(r);if(s)return s;u.add(r);try{const c=X(`/proc/${r}/status`,"utf8");r=Number(/^PPid:\s*(\d+)/m.exec(c)?.[1]??0)}catch{return null}}return null},"sandboxForPid");let g,E=!1;const j=_(H(te(import.meta.url)),"../daemon/crtrd-cli.js"),D=h(()=>{E||(g=V("/bin/setpriv",["--reuid",String(l),"--regid",String(d),"--groups",String(T),"--",process.execPath,j,...process.argv.slice(2)],{stdio:"inherit",env:{...process.env,CRTR_LAUNCHER_SOCKET:v,CRTR_HOME:S.root}}),g.pid&&p.protectDaemon(g.pid,pe),g.once("exit",()=>{g=void 0,E||D()}),g.once("error",r=>{console.error("crtrd spawn failed:",r),process.exitCode=1,E=!0}))},"startDaemon");W(v)&&Y(v);const N=ee({pauseOnConnect:!0},r=>se(r,u=>{const s=[];return(async()=>{try{if(B.peerCred(U(r)).uid!==l)throw new f("invalid_spec","launcher peer is not crtrd");let c;for(;;)try{c=B.recvFds(U(r));break}catch(t){if(t.code!=="EINTR")throw t}u(),s.push(...c.fds);const e=JSON.parse(c.data);let i;switch(e.operation){case"launch":{if(s.length>1||!b.has(e.spec.uid))throw new f("invalid_uid","app uid is not registered");let t,o;const y=p.prepareRun(e.spec.uid,e.spec.nodeId);try{o=await ne(e.spec,s[0],S,T,a=>{if(!a.pid)throw new f("launch_failed","bwrap has no pid");t=a.pid;const n=a.pid;if(k.set(n,{child:a,uid:e.spec.uid,nodeId:e.spec.nodeId,mounts:e.spec.mounts,plan:y}),a.once("exit",()=>{k.delete(n);const m=p.finishRun(y);L.set(n,m.oomKilled),setTimeout(()=>L.delete(n),5*6e4).unref()}),e.spec.capture){const m=F(a,e.spec).then(w=>({...w,oomKilled:L.get(n)===!0}));x.set(n,m),m.then(()=>{setTimeout(()=>{x.get(n)===m&&x.delete(n)},3e5).unref()})}},y)}catch(a){throw t!==void 0?x.delete(t):p.finishRun(y),a}if(!o.pid)throw new f("launch_failed","bwrap has no pid");p.confirmStarted(y,o.pid),i={pid:o.pid};break}case"wait":{const t=x.get(e.pid);if(!t)throw new f("invalid_spec","unknown captured sandbox pid");const o=await t;x.delete(e.pid),i={result:o};break}case"stop":{const t=Number.isInteger(e.pid)?J(e.pid):null;if(!t||e.nodeId!==void 0&&t.nodeId!==e.nodeId)throw new f("invalid_spec","unknown sandbox pid");if(e.signal!==void 0&&e.signal!=="SIGTERM"&&e.signal!=="SIGKILL")throw new f("invalid_spec","invalid stop signal");const o=e.signal??"SIGKILL";t.child.kill(o),o==="SIGKILL"&&p.killRun(t.plan),i={ok:!0};break}case"user.create":i={uid:b.create(e.app,e.uid)};break;case"user.delete":{if([...k.values()].some(t=>t.uid===e.uid))throw new f("invalid_spec","stop this app\u2019s sandboxes before deleting its user");b.delete(e.uid),i={ok:!0};break}case"users.sync":b.sync(e.users),i={ok:!0};break;case"list":i={sandboxes:[...k].map(([t,o])=>({pid:t,uid:o.uid,nodeId:o.nodeId,mounts:o.mounts}))};break;case"limits.apply":{const t=b.entries().find(o=>o.app===e.app.replace(/^app:/,""));if(!t)throw new f("invalid_uid","app is not registered");p.applyApp(t.app,t.uid,!0),i={ok:!0};break}case"limits.show":i={limits:p.show()};break;case"cache.purge":i={purged:await p.cachePass()};break;case"exit.status":i={oomKilled:L.get(e.pid)===!0};break;case"daemon.handover":g?.kill("SIGTERM"),i={ok:!0};break;default:throw new f("invalid_spec","unknown launcher operation")}return`${JSON.stringify($(i))}
2
- `}catch(c){const e=c instanceof f?c.refusal:"launch_failed";return console.error(`launcher refusal ${e}:`,c),`${JSON.stringify({refusal:e})}
3
- `}finally{for(const c of s)Q(c)}})()}));await new Promise((r,u)=>{N.once("error",u),N.listen(v,r)}),O(v,l,d),P(v,384);const K=h(()=>{E=!0,g?.kill("SIGTERM");for(const{child:r}of k.values())r.kill("SIGKILL");N.close(),S.close()},"shutdown");process.once("SIGTERM",K),process.once("SIGINT",K),D()}h(me,"runLauncher"),await me().catch(l=>{console.error(l),process.exitCode=1});export{me as runLauncher};
1
+ var V=Object.defineProperty;var w=(f,u)=>V(f,"name",{value:u,configurable:!0});import{spawn as z}from"node:child_process";import{createHash as Q}from"node:crypto";import{chownSync as B,chmodSync as A,closeSync as W,existsSync as X,mkdirSync as H,readFileSync as Y,rmSync as Z,statSync as ee}from"node:fs";import{createServer as te}from"node:net";import{dirname as U,join as _}from"node:path";import{fileURLToPath as re}from"node:url";import{loadLinuxNative as ne}from"../native/linux.js";import{envHomeOverride as oe}from"../shared/env.js";import{launchSandbox as se}from"./bwrap.js";import{LaunchGate as ie}from"./launch-gate.js";import{serveAccepted as ae}from"./accepted-socket.js";import{installNetworkRule as ce}from"./netrule.js";import{LAUNCHER_SOCKET as b,LaunchRefused as p}from"./spec.js";import{systemIdentity as de,AppUsers as ue}from"./users.js";import{SourceValidator as le}from"./validate.js";import{prepareDaemonScopeDirectories as pe}from"./scope-directories.js";import{LimitsManager as fe}from"./limits/manager.js";import{CRTRD_OOM_SCORE_ADJ as me}from"./limits/policy.js";import{writeLimitsLog as he}from"./limits/log.js";const D=ne();function $(f){const u=f._handle?.fd;if(u===void 0||u<0)throw new Error("launcher socket fd unavailable");return u}w($,"socketFd");async function we(){if(process.platform!=="linux"||process.getuid?.()!==0||!D)throw new Error("launcher requires Linux root and its native addon");const{daemonUid:f,daemonGid:u,dataGid:L}=de(),R=U(b);if(H(R,{recursive:!0,mode:493}),(ee(R).mode&511)!==493)throw new Error(`${R} must be mode 0755`);B(R,f,u),process.umask(7);const N=_(R,"nodes");H(N,{recursive:!0,mode:504}),B(N,0,u),A(N,504);const v=new le(oe()||"/",u);pe(_(v.root,"runtime"),f,u);const k=new ue(L);ce();const m=new fe({runtimeRoot:v.runtime,appsRoot:_(v.root,"apps"),stateDir:_(v.runtime,"limits-state"),dataGid:L,users:k});m.init();const T=new Map,F=w(r=>{const l=m.takeEvents();for(const o of l)he(o);return l.length===0?r:{...r,events:l}},"reply_"),E=new ie,x=new Map,I=new Map,J=w((r,l)=>{const o=l.capture,a=Q("sha256"),e=w((c,s)=>{let d=0,h=!1;const g=[];return{take(C){s?.update(C);const G=Math.max(0,Math.min(C.length,c-d));G&&(g.push(C.subarray(0,G)),d+=G),G!==C.length&&(h=!0)},result:w(()=>({text:Buffer.concat(g).toString("utf8"),truncated:h}),"result")}},"chunks"),i=e(o.stdoutBytes,a),t=e(o.stderrBytes);let n=Buffer.alloc(0),S=0;return r.stdout?.on("data",c=>i.take(c)),r.stderr?.on("data",c=>{const s=c;o.stderrTail?(S+=s.length,n=s.length>=o.stderrBytes?s.subarray(s.length-o.stderrBytes):Buffer.concat([n,s]).subarray(Math.max(0,n.length+s.length-o.stderrBytes))):t.take(s)}),new Promise(c=>r.once("close",(s,d)=>{const h=i.result(),g=o.stderrTail?{text:n.toString("utf8"),truncated:S>o.stderrBytes}:t.result();c({exitCode:s,signal:d,stdout:h.text,stderr:g.text,stdoutTruncated:h.truncated,stderrTruncated:g.truncated,stdoutSha256:a.digest("hex")})}))},"captureExit"),j=w(r=>{const l=new Set;for(;r>1&&!l.has(r);){const o=x.get(r);if(o)return o;l.add(r);try{const a=Y(`/proc/${r}/status`,"utf8");r=Number(/^PPid:\s*(\d+)/m.exec(a)?.[1]??0)}catch{return null}}return null},"sandboxForPid");let y,M=!1;const q=_(U(re(import.meta.url)),"../daemon/crtrd-cli.js"),K=w(()=>{M||(y=z("/bin/setpriv",["--reuid",String(f),"--regid",String(u),"--groups",String(L),"--",process.execPath,q,...process.argv.slice(2)],{stdio:"inherit",env:{...process.env,CRTR_LAUNCHER_SOCKET:b,CRTR_HOME:v.root}}),y.pid&&m.protectDaemon(y.pid,me),y.once("exit",()=>{y=void 0,M||K()}),y.once("error",r=>{console.error("crtrd spawn failed:",r),process.exitCode=1,M=!0}))},"startDaemon");X(b)&&Z(b);const O=te({pauseOnConnect:!0},r=>ae(r,l=>{const o=[];return(async()=>{try{if(D.peerCred($(r)).uid!==f)throw new p("invalid_spec","launcher peer is not crtrd");let a;for(;;)try{a=D.recvFds($(r));break}catch(t){if(t.code!=="EINTR")throw t}l(),o.push(...a.fds);const e=JSON.parse(a.data);let i;switch(e.operation){case"launch":{if(o.length>1)throw new p("invalid_uid","app uid is not registered");let t,n,S;try{n=await E.launch(async()=>{if(!k.has(e.spec.uid))throw new p("invalid_uid","app uid is not registered");const c=S=m.prepareRun(e.spec.uid,e.spec.nodeId);return se(e.spec,o[0],v,L,s=>{if(!s.pid)throw new p("launch_failed","bwrap has no pid");t=s.pid;const d=s.pid;if(x.set(d,{child:s,uid:e.spec.uid,nodeId:e.spec.nodeId,mounts:e.spec.mounts,plan:c}),s.once("exit",()=>{x.delete(d);const h=m.finishRun(c);T.set(d,h.oomKilled),setTimeout(()=>T.delete(d),5*6e4).unref()}),e.spec.capture){const h=J(s,e.spec).then(g=>({...g,oomKilled:T.get(d)===!0}));I.set(d,h),h.then(()=>{setTimeout(()=>{I.get(d)===h&&I.delete(d)},3e5).unref()})}},c)})}catch(c){throw t!==void 0?I.delete(t):S&&m.finishRun(S),c}if(!n.pid)throw new p("launch_failed","bwrap has no pid");m.confirmStarted(S,n.pid),i={pid:n.pid};break}case"wait":{const t=I.get(e.pid);if(!t)throw new p("invalid_spec","unknown captured sandbox pid");const n=await t;I.delete(e.pid),i={result:n};break}case"stop":{const t=Number.isInteger(e.pid)?j(e.pid):null;if(!t||e.nodeId!==void 0&&t.nodeId!==e.nodeId)throw new p("invalid_spec","unknown sandbox pid");if(e.signal!==void 0&&e.signal!=="SIGTERM"&&e.signal!=="SIGKILL")throw new p("invalid_spec","invalid stop signal");const n=e.signal??"SIGKILL";t.child.kill(n),n==="SIGKILL"&&m.killRun(t.plan),i={ok:!0};break}case"user.create":i={uid:await E.write(()=>k.create(e.app,e.uid))};break;case"user.delete":{await E.write(()=>{if([...x.values()].some(t=>t.uid===e.uid))throw new p("invalid_spec","stop this app\u2019s sandboxes before deleting its user");k.delete(e.uid)}),i={ok:!0};break}case"users.sync":await E.write(()=>k.sync(e.users)),i={ok:!0};break;case"list":i={sandboxes:[...x].map(([t,n])=>({pid:t,uid:n.uid,nodeId:n.nodeId,mounts:n.mounts}))};break;case"limits.apply":{const t=k.entries().find(n=>n.app===e.app.replace(/^app:/,""));if(!t)throw new p("invalid_uid","app is not registered");m.applyApp(t.app,t.uid,!0),i={ok:!0};break}case"limits.show":i={limits:m.show()};break;case"cache.purge":i={purged:await m.cachePass()};break;case"exit.status":i={oomKilled:T.get(e.pid)===!0};break;case"daemon.handover":y?.kill("SIGTERM"),i={ok:!0};break;default:throw new p("invalid_spec","unknown launcher operation")}return`${JSON.stringify(F(i))}
2
+ `}catch(a){const e=a instanceof p?a.refusal:"launch_failed";return console.error(`launcher refusal ${e}:`,a),`${JSON.stringify({refusal:e})}
3
+ `}finally{for(const a of o)W(a)}})()}));await new Promise((r,l)=>{O.once("error",l),O.listen(b,r)}),B(b,f,u),A(b,384);const P=w(()=>{M=!0,y?.kill("SIGTERM");for(const{child:r}of x.values())r.kill("SIGKILL");O.close(),v.close()},"shutdown");process.once("SIGTERM",P),process.once("SIGINT",P),K()}w(we,"runLauncher"),await we().catch(f=>{console.error(f),process.exitCode=1});export{we as runLauncher};
@@ -1,7 +1,7 @@
1
- var h=Object.defineProperty;var i=(n,t)=>h(n,"name",{value:t,configurable:!0});import{readFileSync as a,writeFileSync as l,renameSync as f,openSync as w,closeSync as m,fchmodSync as g,constants as u}from"node:fs";import{APP_UID_MAX as y,APP_UID_MIN as _,LaunchRefused as p}from"./spec.js";function I(){const n=a("/etc/passwd","utf8").split(`
2
- `).map(s=>s.split(":")),t=a("/etc/group","utf8").split(`
3
- `).map(s=>s.split(":")),e=n.find(s=>s[0]==="crtrd"),r=t.find(s=>s[0]==="crtr-data");if(!e||!r)throw new Error("crtrd user and crtr-data group must exist");return{daemonUid:Number(e[2]),daemonGid:Number(e[3]),dataGid:Number(r[2])}}i(I,"systemIdentity");const $=/^[a-z0-9][a-z0-9-]{0,62}$/;function d(n,t){if(!$.test(n)||!Number.isInteger(t)||t<_||t>y)throw new p("invalid_uid","invalid app or uid")}i(d,"check");class b{static{i(this,"AppUsers")}dataGid;users=new Map;constructor(t){this.dataGid=t}sync(t){const e=new Map,r=new Set;for(const s of t){if(d(s.app,s.uid),e.has(s.uid)||r.has(s.app))throw new p("invalid_uid","duplicate app or uid");e.set(s.uid,s.app),r.add(s.app)}this.users=e,this.write()}create(t,e){if(d(t,e),this.users.has(e)&&this.users.get(e)!==t||[...this.users.values()].includes(t)&&this.users.get(e)!==t)throw new p("invalid_uid","app uid conflict");return this.users.set(e,t),this.write(),e}delete(t){if(!this.users.has(t))throw new p("invalid_uid","unknown app uid");this.users.delete(t),this.write()}has(t){return this.users.has(t)}appFor(t){return this.users.get(t)}entries(){return[...this.users].map(([t,e])=>({app:e,uid:t}))}write(){const t=a("/etc/passwd","utf8").split(`
4
- `).filter(r=>r&&!/^nl-app-[a-z0-9-]+:/.test(r)),e=a("/etc/group","utf8").split(`
5
- `).filter(r=>r&&!/^nl-app-[a-z0-9-]+:/.test(r));for(const[r,s]of this.users)t.push(`nl-app-${s}:x:${r}:${this.dataGid}:Northlight app:/apps/${s}/home:/bin/sh`),e.push(`nl-app-${s}:x:${r}:`);for(const[r,s]of[["/etc/passwd",t],["/etc/group",e]]){const c=`${r}.launcher-${process.pid}`,o=w(c,u.O_WRONLY|u.O_CREAT|u.O_EXCL,420);try{l(o,`${s.join(`
1
+ var l=Object.defineProperty;var a=(r,t)=>l(r,"name",{value:t,configurable:!0});import{readFileSync as i,writeFileSync as f,renameSync as w,openSync as m,closeSync as g,fchmodSync as y,constants as c}from"node:fs";import{APP_UID_MAX as _,APP_UID_MIN as $,LaunchRefused as o}from"./spec.js";function b(){const r=i("/etc/passwd","utf8").split(`
2
+ `).map(s=>s.split(":")),t=i("/etc/group","utf8").split(`
3
+ `).map(s=>s.split(":")),e=r.find(s=>s[0]==="crtrd"),n=t.find(s=>s[0]==="crtr-data");if(!e||!n)throw new Error("crtrd user and crtr-data group must exist");return{daemonUid:Number(e[2]),daemonGid:Number(e[3]),dataGid:Number(n[2])}}a(b,"systemIdentity");const N=/^[a-z0-9][a-z0-9-]{0,62}$/;function h(r,t){if(!N.test(r)||!Number.isInteger(t)||t<$||t>_)throw new o("invalid_uid","invalid app or uid")}a(h,"check");class A{static{a(this,"AppUsers")}dataGid;users=new Map;constructor(t){this.dataGid=t}sync(t){const e=new Map,n=new Set;for(const s of t){if(h(s.app,s.uid),e.has(s.uid)||n.has(s.app))throw new o("invalid_uid","duplicate app or uid");e.set(s.uid,s.app),n.add(s.app)}this.users=e,this.write()}create(t,e){if(h(t,e),this.users.has(e)&&this.users.get(e)!==t||[...this.users.values()].includes(t)&&this.users.get(e)!==t)throw new o("invalid_uid","app uid conflict");return this.users.set(e,t),this.write(),e}delete(t){if(!this.users.has(t))throw new o("invalid_uid","unknown app uid");this.users.delete(t),this.write()}has(t){return this.users.has(t)}appFor(t){return this.users.get(t)}entries(){return[...this.users].map(([t,e])=>({app:e,uid:t}))}write(){const t=i("/etc/passwd","utf8").split(`
4
+ `).filter(n=>n&&!/^nl-app-[a-z0-9-]+:/.test(n)),e=i("/etc/group","utf8").split(`
5
+ `).filter(n=>n&&!/^nl-app-[a-z0-9-]+:/.test(n));for(const[n,s]of this.users)t.push(`nl-app-${s}:x:${n}:${this.dataGid}:Northlight app:/apps/${s}/home:/bin/sh`),e.push(`nl-app-${s}:x:${n}:`);for(const[n,s]of[["/etc/passwd",t],["/etc/group",e]]){const u=`${s.join(`
6
6
  `)}
7
- `),g(o,420)}finally{m(o)}f(c,r)}}}export{b as AppUsers,I as systemIdentity};
7
+ `;if(i(n,"utf8")===u)continue;const d=`${n}.launcher-${process.pid}`,p=m(d,c.O_WRONLY|c.O_CREAT|c.O_EXCL,420);try{f(p,u),y(p,420)}finally{g(p)}w(d,n)}}}export{A as AppUsers,b as systemIdentity};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.490",
3
+ "version": "0.3.491",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -138,7 +138,7 @@
138
138
  },
139
139
  "license": "UNLICENSED",
140
140
  "dependencies": {
141
- "@north-light/identity": "0.3.490",
141
+ "@north-light/identity": "0.3.491",
142
142
  "@earendil-works/pi-agent-core": "1.1.0",
143
143
  "@earendil-works/pi-ai": "1.1.0",
144
144
  "@earendil-works/pi-coding-agent": "1.1.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/identity",
3
- "version": "0.3.490",
3
+ "version": "0.3.491",
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.490",
3
+ "version": "0.3.491",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/cli",
9
- "version": "0.3.490",
9
+ "version": "0.3.491",
10
10
  "hasInstallScript": true,
11
11
  "license": "UNLICENSED",
12
12
  "workspaces": [
@@ -18,7 +18,7 @@
18
18
  "@earendil-works/pi-ai": "1.1.0",
19
19
  "@earendil-works/pi-coding-agent": "1.1.0",
20
20
  "@earendil-works/pi-tui": "1.1.0",
21
- "@north-light/identity": "0.3.490",
21
+ "@north-light/identity": "0.3.491",
22
22
  "cron-parser": "^5.6.0",
23
23
  "esbuild": "^0.27.7",
24
24
  "jose": "^6.2.12",
@@ -12116,17 +12116,17 @@
12116
12116
  },
12117
12117
  "packages/crouter-api": {
12118
12118
  "name": "@north-light/api",
12119
- "version": "0.3.490",
12119
+ "version": "0.3.491",
12120
12120
  "license": "UNLICENSED"
12121
12121
  },
12122
12122
  "packages/crouter-env-docker": {
12123
12123
  "name": "@north-light/env-docker",
12124
- "version": "0.3.490",
12124
+ "version": "0.3.491",
12125
12125
  "license": "UNLICENSED"
12126
12126
  },
12127
12127
  "packages/crouter-identity": {
12128
12128
  "name": "@north-light/identity",
12129
- "version": "0.3.490",
12129
+ "version": "0.3.491",
12130
12130
  "license": "UNLICENSED",
12131
12131
  "dependencies": {
12132
12132
  "jose": "^6.2.1"
@@ -12134,20 +12134,20 @@
12134
12134
  },
12135
12135
  "packages/crouter-plugin": {
12136
12136
  "name": "@north-light/plugin",
12137
- "version": "0.3.490",
12137
+ "version": "0.3.491",
12138
12138
  "license": "UNLICENSED",
12139
12139
  "dependencies": {
12140
- "@north-light/api": "0.3.490",
12141
- "@north-light/identity": "0.3.490"
12140
+ "@north-light/api": "0.3.491",
12141
+ "@north-light/identity": "0.3.491"
12142
12142
  }
12143
12143
  },
12144
12144
  "packages/crouter-sdk": {
12145
12145
  "name": "@north-light/sdk",
12146
- "version": "0.3.490",
12146
+ "version": "0.3.491",
12147
12147
  "license": "UNLICENSED",
12148
12148
  "dependencies": {
12149
- "@north-light/api": "0.3.490",
12150
- "@north-light/identity": "0.3.490",
12149
+ "@north-light/api": "0.3.491",
12150
+ "@north-light/identity": "0.3.491",
12151
12151
  "jose": "^6.2.1"
12152
12152
  },
12153
12153
  "bin": {