@north-light/cli 0.3.490 → 0.3.492

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.
@@ -15,6 +15,9 @@ export declare function isShipped(db: DatabaseSync, object: ObjectRow): boolean;
15
15
  export declare function toolApps(db: DatabaseSync, reader: Reader): ObjectRow[];
16
16
  /** May `reader` use any of `app`'s tool groups? v0: every run may use every installed plugin's tools. */
17
17
  export declare function mayUseTools(_db: DatabaseSync, _reader: Reader, app: ObjectRow): boolean;
18
+ /** Whether `scopes` hold a provider-group scope `<provider>:<group>` of `app`: what a run needs to call that app's provider commands
19
+ * (`requireGroup`). The provider id is the app's client id. */
20
+ export declare function holdsProviderGroup(db: DatabaseSync, scopes: readonly string[], app: ObjectRow): boolean;
18
21
  /** Whether this reader may register, publish or watch as an object owned by `owner`. */
19
22
  export declare function mayActAs(db: DatabaseSync, reader: Reader, owner: ObjectRow): boolean;
20
23
  /** Run confinement for a node acting on a custom object of its app (Silas, 2026-09-30):
@@ -1,8 +1,8 @@
1
- var O=Object.defineProperty;var s=(e,t)=>O(e,"name",{value:t,configurable:!0});import{prepared as a}from"../sqlite-statements.js";import{covers as R}from"@north-light/identity";import{objectPlace as N,runPlace as L,runsSeeEachOther as g}from"../runs/confinement.js";import{missingOwnMemoryScope as E}from"./spaces/own-memory.js";import{SPACES as x,identityOf as f}from"./spaces/index.js";import{insideRepo as C}from"./spaces/repo.js";function T(e,t){return a(e,`WITH RECURSIVE chain(rowid, depth) AS (
1
+ var R=Object.defineProperty;var s=(n,t)=>R(n,"name",{value:t,configurable:!0});import{prepared as f}from"../sqlite-statements.js";import{covers as O}from"@north-light/identity";import{objectPlace as x,runPlace as N,runsSeeEachOther as g}from"../runs/confinement.js";import{missingOwnMemoryScope as E}from"./spaces/own-memory.js";import{SPACES as L,identityOf as u}from"./spaces/index.js";import{insideRepo as C}from"./spaces/repo.js";function T(n,t){return f(n,`WITH RECURSIVE chain(rowid, depth) AS (
2
2
  SELECT owner, 1 FROM objects WHERE rowid = ? AND owner IS NOT NULL
3
3
  UNION ALL
4
4
  SELECT o.owner, c.depth + 1 FROM objects o JOIN chain c ON o.rowid = c.rowid WHERE o.owner IS NOT NULL
5
- ) SELECT o.* FROM chain c CROSS JOIN objects o WHERE o.rowid = c.rowid ORDER BY c.depth`).all(t.rowid)}s(T,"ownerChain");function v(e,t){return c(e,t).space}s(v,"spaceOf");function c(e,t){const n=T(e,t),p=n.length?n[n.length-1]:t,o=x[p.type];if(!o)throw new Error(`${t.type} ${t.id} has no space: its owner chain ends at ${p.type} ${p.id}`);const r=n.length?[...n.slice(0,-1).reverse(),t]:[];return{space:p,path:r,impl:o}}s(c,"locate");function m(e,t){return t.type!=="document"?!1:a(e,"SELECT source_path FROM documents WHERE object = ?").get(t.rowid)?.source_path!=null}s(m,"isShipped");function J(e,t){return a(e,`SELECT a.* FROM objects a WHERE a.type = 'app' AND a.deleted_at IS NULL AND EXISTS (
5
+ ) SELECT o.* FROM chain c CROSS JOIN objects o WHERE o.rowid = c.rowid ORDER BY c.depth`).all(t.rowid)}s(T,"ownerChain");function J(n,t){return c(n,t).space}s(J,"spaceOf");function c(n,t){const e=T(n,t),p=e.length?e[e.length-1]:t,o=L[p.type];if(!o)throw new Error(`${t.type} ${t.id} has no space: its owner chain ends at ${p.type} ${p.id}`);const r=e.length?[...e.slice(0,-1).reverse(),t]:[];return{space:p,path:r,impl:o}}s(c,"locate");function m(n,t){return t.type!=="document"?!1:f(n,"SELECT source_path FROM documents WHERE object = ?").get(t.rowid)?.source_path!=null}s(m,"isShipped");function B(n,t){return f(n,`SELECT a.* FROM objects a WHERE a.type = 'app' AND a.deleted_at IS NULL AND EXISTS (
6
6
  SELECT 1 FROM objects o JOIN documents d ON d.object = o.rowid
7
7
  WHERE o.owner = a.rowid AND o.type = 'document' AND o.deleted_at IS NULL AND d.source_path IS NOT NULL)
8
- ORDER BY a.name = 'crtr', a.name`).all().filter(p=>h(e,t,p))}s(J,"toolApps");function h(e,t,n){return n.type==="app"}s(h,"mayUseTools");function A(e,t,n){return t.class==="person"?!0:t.class==="object"?t.owner.rowid===n.rowid:t.class==="app"?t.app.rowid===n.rowid||n.type==="profile"&&n.owner===t.app.rowid:t.app.rowid===n.rowid||n.type==="profile"&&t.profile?.rowid===n.rowid||n.type==="repo"&&C(e,t,n,"rw")}s(A,"mayActAs");function I(e,t,n){if(t.class!=="node"&&!(t.class==="app"&&t.place!==void 0))return!0;const p=t.class==="app"?t.place:L(t.node.id,e);if(p===null)return!0;const o=N(n.rowid,e);return o===null?!p.isolated:g(p,o)}s(I,"customReachable");const w=s(e=>e.type.includes("."),"isCustom");function P(e,t,n){return y(e,c(e,t),t,n)}s(P,"objectsTargets");function y(e,{space:t,path:n},p,o){const r=[];if(t.type==="person")r.push("user");else if(t.type==="app"&&n.length>0){const i=n[0];if(i.type==="profile")n.length>1&&r.push(`profile:${f(e,i).external_id}`);else if(i.type==="node"||w(i)){const u=f(e,t).external_id;u!==o&&r.push(u)}}return p.type==="node"&&r.push("node"),r}s(y,"targetsOf");function l(e,t,n,p,o){if(t.class==="person"||t.scopes===null)return!1;const r=t.scopes;if(r.length===0||p.space.type==="app"&&t.app.rowid===p.space.rowid)return!1;const i=f(e,t.app).external_id;return y(e,p,o,i).some(u=>{const S=`crtr:objects:${n}:${u}`;return r.some(d=>R(d,S))})}s(l,"holdsInLocated");function M(e,t,n,p){return t.class==="person"||t.scopes===null?!1:l(e,t,n,c(e,p),p)}s(M,"holdsObjectsScope");function B(e,t,n,p,o){const r=p.owner===null?null:e.prepare("SELECT * FROM objects WHERE rowid = ?").get(p.owner)??null;if(r===null)return!1;const i={class:"app",app:t,scopes:n??[]};return A(e,i,r)?n===null||M(e,i,"link",o):!1}s(B,"linkAuthorizes");function Y(e,t,n){return _(e,n)(t)}s(Y,"canRead");function _(e,t){let n=null,p=null;return o=>{if(o.class==="person")return!0;if(n??=c(e,t),n.space.type==="app"&&n.path.length>0){if(p??=m(e,t),p&&h(e,o,n.space))return!0;if(w(t)){if(o.app.rowid===n.space.rowid&&!I(e,o,t))return!1;if(t.privacy==="public"&&o.app.rowid!==n.space.rowid&&o.scopes!==null&&h(e,o,n.space))return!0}}return l(e,o,"read",n,t)||o.class==="object"&&w(o.object)&&l(e,o,"watch",n,t)?!0:n.impl.canRead(e,o,n.space,n.path,t)}}s(_,"readCheck");function z(e,t,n){if(m(e,n))return!1;const p=c(e,n);if(l(e,t,"write",p,n))return!0;const{space:o,path:r,impl:i}=p;return i.canChange(e,t,o,r,n)}s(z,"canChange");function V(e,t,n,p){if(t.class==="person")return null;const{space:o,path:r}=c(e,n);return o.type==="app"&&t.app.rowid===o.rowid?E(e,t,o,r,n,p):null}s(V,"missingMemoryScope");function X(e,t,n,p){if(t.class==="person")return null;const o=c(e,n),{space:r}=o;return r.type!=="app"||t.app.rowid!==r.rowid?null:E(e,t,r,o.path.length===0?[n]:[...o.path],null,p)}s(X,"missingMemoryScopeUnder");export{z as canChange,Y as canRead,I as customReachable,M as holdsObjectsScope,m as isShipped,B as linkAuthorizes,A as mayActAs,h as mayUseTools,V as missingMemoryScope,X as missingMemoryScopeUnder,P as objectsTargets,T as ownerChain,_ as readCheck,v as spaceOf,J as toolApps};
8
+ ORDER BY a.name = 'crtr', a.name`).all().filter(p=>h(n,t,p))}s(B,"toolApps");function h(n,t,e){return e.type==="app"}s(h,"mayUseTools");const A=s(n=>n.class!=="app"||n.place!==void 0,"isRun");function I(n,t,e){const p=`${u(n,e).external_id.replace(/^app:/,"")}:`;return t.some(o=>o.startsWith(p)&&!o.slice(p.length).includes(":"))}s(I,"holdsProviderGroup");function _(n,t,e){return t.class==="person"?!0:t.class==="object"?t.owner.rowid===e.rowid:t.class==="app"?t.app.rowid===e.rowid||e.type==="profile"&&e.owner===t.app.rowid:t.app.rowid===e.rowid||e.type==="profile"&&t.profile?.rowid===e.rowid||e.type==="repo"&&C(n,t,e,"rw")}s(_,"mayActAs");function M(n,t,e){if(t.class!=="node"&&!(t.class==="app"&&t.place!==void 0))return!0;const p=t.class==="app"?t.place:N(t.node.id,n);if(p===null)return!0;const o=x(e.rowid,n);return o===null?!p.isolated:g(p,o)}s(M,"customReachable");const w=s(n=>n.type.includes("."),"isCustom");function Y(n,t,e){return d(n,c(n,t),t,e)}s(Y,"objectsTargets");function d(n,{space:t,path:e},p,o){const r=[];if(t.type==="person")r.push("user");else if(t.type==="app"&&e.length>0){const i=e[0];if(i.type==="profile")e.length>1&&r.push(`profile:${u(n,i).external_id}`);else if(i.type==="node"||w(i)){const l=u(n,t).external_id;l!==o&&r.push(l)}}return p.type==="node"&&r.push("node"),r}s(d,"targetsOf");function a(n,t,e,p,o){if(t.class==="person"||t.scopes===null)return!1;const r=t.scopes;if(r.length===0||p.space.type==="app"&&t.app.rowid===p.space.rowid)return!1;const i=u(n,t.app).external_id;return d(n,p,o,i).some(l=>{const y=`crtr:objects:${e}:${l}`;return r.some(S=>O(S,y))})}s(a,"holdsInLocated");function U(n,t,e,p){return t.class==="person"||t.scopes===null?!1:a(n,t,e,c(n,p),p)}s(U,"holdsObjectsScope");function z(n,t,e,p,o){const r=p.owner===null?null:n.prepare("SELECT * FROM objects WHERE rowid = ?").get(p.owner)??null;if(r===null)return!1;const i={class:"app",app:t,scopes:e??[]};return _(n,i,r)?e===null||U(n,i,"link",o):!1}s(z,"linkAuthorizes");function G(n,t,e){return W(n,e)(t)}s(G,"canRead");function W(n,t){let e=null,p=null;return o=>{if(o.class==="person")return!0;if(e??=c(n,t),e.space.type==="app"&&e.path.length>0){if(p??=m(n,t),p&&h(n,o,e.space))return!0;if(w(t)){if(o.app.rowid===e.space.rowid&&!M(n,o,t))return!1;if(o.app.rowid!==e.space.rowid&&o.scopes!==null&&h(n,o,e.space)&&(t.privacy==="public"||A(o)&&I(n,o.scopes,e.space)))return!0}}return a(n,o,"read",e,t)||o.class==="object"&&w(o.object)&&a(n,o,"watch",e,t)?!0:e.impl.canRead(n,o,e.space,e.path,t)}}s(W,"readCheck");function V(n,t,e){if(m(n,e))return!1;const p=c(n,e);if(a(n,t,"write",p,e))return!0;const{space:o,path:r,impl:i}=p;return i.canChange(n,t,o,r,e)}s(V,"canChange");function X(n,t,e,p){if(t.class==="person")return null;const{space:o,path:r}=c(n,e);return o.type==="app"&&t.app.rowid===o.rowid?E(n,t,o,r,e,p):null}s(X,"missingMemoryScope");function q(n,t,e,p){if(t.class==="person")return null;const o=c(n,e),{space:r}=o;return r.type!=="app"||t.app.rowid!==r.rowid?null:E(n,t,r,o.path.length===0?[e]:[...o.path],null,p)}s(q,"missingMemoryScopeUnder");export{V as canChange,G as canRead,M as customReachable,U as holdsObjectsScope,I as holdsProviderGroup,m as isShipped,z as linkAuthorizes,_ as mayActAs,h as mayUseTools,X as missingMemoryScope,q as missingMemoryScopeUnder,Y as objectsTargets,T as ownerChain,W as readCheck,J as spaceOf,B as toolApps};
@@ -1,21 +1,7 @@
1
1
  import type { DeclLeaf, DeclNode } from '../api/command-manifest/schema.js';
2
+ import { KIND, LOCAL, RESERVED_KIND_ROOTS, MAX_KIND_EVENTS, MIN_EVENT_LEVEL, MAX_EVENT_LEVEL, eventCatalogReasons, objectBlocksShapeReasons, objectKindReadReasons } from '../api/command-manifest/object-kinds.js';
2
3
  import type { PluginManifest } from '../types.js';
3
- /**
4
- * The `object_kinds` and `scopes` blocks of a plugin manifest (`plugin.json`, `bundle.json`) and the one
5
- * validator every install, update, enable, provider-bundle and reconcile path shares. Canvas-free by
6
- * design: the CLI and the bundle parser import it, so it must not reach `node:sqlite` or the canvas.
7
- */
8
- export declare const KIND: RegExp;
9
- export declare const LOCAL: RegExp;
10
- /** A kind's first segment may not be one of the built-in object types. */
11
- export declare const RESERVED_KIND_ROOTS: readonly string[];
12
- export declare const MAX_KIND_EVENTS = 64;
13
- export declare const MIN_EVENT_LEVEL = 1;
14
- export declare const MAX_EVENT_LEVEL = 10;
15
- /** Reasons a catalog (`events` map) is invalid; shared with `registerCustomObject`. */
16
- export declare function eventCatalogReasons(events: unknown, where: string): string[];
17
- /** The JSON shape of both blocks of a raw manifest record, independent of the plugin's name and commands. */
18
- export declare function objectBlocksShapeReasons(record: Record<string, unknown>): string[];
4
+ export { KIND, LOCAL, RESERVED_KIND_ROOTS, MAX_KIND_EVENTS, MIN_EVENT_LEVEL, MAX_EVENT_LEVEL, eventCatalogReasons, objectBlocksShapeReasons, objectKindReadReasons };
19
5
  /**
20
6
  * The full check. `leafPaths` holds the space-joined path of every leaf below the plugin's root command
21
7
  * (`page read` for plugin `search`'s leaf `search page read`); `streamingLeafPaths` holds those declared `rest.streaming: true`.
@@ -1 +1 @@
1
- var b=Object.defineProperty;var d=(e,n)=>b(e,"name",{value:n,configurable:!0});import{parseScope as y}from"@north-light/identity";const k=/^[a-z][a-z0-9-]*\.[a-z][a-z0-9-]*$/,$=/^[a-z][a-z0-9-]*$/,g=["report","notice","node","job","document","person","app","repo","profile","action"],u=64,l=1,h=10,j=new Set(["description","events","read"]),O=new Set(["level","supersedes"]),S=/^crtr:objects:(?:read|watch|link|write):/;function f(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}d(f,"isRecord");function N(e,n){if(!f(e))return[`${n} events must be an object`];const s=[],i=Object.entries(e);i.length>u&&s.push(`${n} declares more than ${u} events`);for(const[c,t]of i){const r=`${n} event ${JSON.stringify(c)}`;if($.test(c)||s.push(`${r} is not a valid event name`),!f(t)){s.push(`${r} must be an object { level, supersedes? }`);continue}for(const a of Object.keys(t))O.has(a)||s.push(`${r} has unknown key ${JSON.stringify(a)}`);const o=t.level;(typeof o!="number"||!Number.isInteger(o)||o<l||o>h)&&s.push(`${r} level must be an integer ${l}-${h}`),t.supersedes!==void 0&&typeof t.supersedes!="boolean"&&s.push(`${r} supersedes must be a boolean`)}return s}d(N,"eventCatalogReasons");function _(e){const n=[],s=e.object_kinds;if(s!==void 0)if(!f(s))n.push("object_kinds must be an object keyed by kind name");else for(const[c,t]of Object.entries(s)){const r=`object_kinds ${JSON.stringify(c)}`;if(k.test(c)?g.includes(c.split(".")[0])&&n.push(`${r} uses a reserved first segment`):n.push(`${r} is not a valid kind name (<plugin>.<name>, lowercase)`),!f(t)){n.push(`${r} must be an object { description, events, read? }`);continue}for(const a of Object.keys(t))j.has(a)||n.push(`${r} has unknown key ${JSON.stringify(a)}`);(typeof t.description!="string"||t.description.trim()==="")&&n.push(`${r} description must be non-empty text`),n.push(...N(t.events,r));const o=t.read;if(o!==void 0){const a=f(o)?o.command:void 0;!f(o)||Object.keys(o).some(p=>p!=="command")?n.push(`${r} read must be { command: [leaf path] }`):(!Array.isArray(a)||a.length===0||a.some(p=>typeof p!="string"||p===""))&&n.push(`${r} read.command must be a non-empty array of command names`)}}const i=e.scopes;return i!==void 0&&(!Array.isArray(i)||i.some(c=>typeof c!="string"))&&n.push("scopes must be an array of scope strings"),n}d(_,"objectBlocksShapeReasons");function v(e,n){const s=_(e);if(s.length>0)return s;const{name:i,leafPaths:c,streamingLeafPaths:t}=n;for(const[o,a]of Object.entries(e.object_kinds??{})){o.split(".")[0]!==i&&s.push(`object_kinds ${JSON.stringify(o)} must start with the plugin name "${i}."`);const p=a.read?.command;p!==void 0&&!c.has(p.join(" "))?s.push(`object_kinds ${JSON.stringify(o)} read.command "${p.join(" ")}" is not a leaf of plugin "${i}"`):p!==void 0&&t.has(p.join(" "))&&s.push(`object_kinds ${JSON.stringify(o)} read.command "${p.join(" ")}" streams; a read is one {markdown} answer`)}const r=e.scopes??[];e.scopes!==void 0&&e.provider!==void 0&&s.push("scopes is for local plugins; a provider gets its scopes from directory consent");for(const o of r){try{y(o)}catch{s.push(`scope ${JSON.stringify(o)} is not a recognized scope`);continue}S.test(o)?o.endsWith(`:app:${i}`)&&s.push(`scope ${JSON.stringify(o)} names the plugin's own space; an app needs no scope for its own objects`):s.push(`scope ${JSON.stringify(o)} must be crtr:objects:<read|watch|link|write>:<target>`)}return s}d(v,"objectBlocksReasons");function x(e){return e.object_kinds!==void 0||e.scopes!==void 0}d(x,"declaresObjectBlocks");function A(e){return e.provider!==void 0?!1:Object.keys(e.object_kinds??{}).length>0||(e.scopes?.length??0)>0||e.transport?.kind==="http"}d(A,"needsAppToken");function J(e,n){return m(e,n,()=>!0)}d(J,"leafPathsBelowRoot");function R(e,n){return m(e,n,s=>"rest"in s&&s.rest.streaming===!0)}d(R,"streamingLeafPathsBelowRoot");function m(e,n,s){const i=new Set,c=d((t,r)=>{if(t.kind==="leaf")s(t)&&i.add([...r,t.name].join(" "));else for(const o of t.children)c(o,[...r,t.name])},"walk");for(const t of n)if(t.name===e&&t.kind==="branch")for(const r of t.children)c(r,[]);return i}d(m,"collectLeafPaths");export{k as KIND,$ as LOCAL,h as MAX_EVENT_LEVEL,u as MAX_KIND_EVENTS,l as MIN_EVENT_LEVEL,g as RESERVED_KIND_ROOTS,x as declaresObjectBlocks,N as eventCatalogReasons,J as leafPathsBelowRoot,A as needsAppToken,v as objectBlocksReasons,_ as objectBlocksShapeReasons,R as streamingLeafPathsBelowRoot};
1
+ var l=Object.defineProperty;var r=(e,n)=>l(e,"name",{value:n,configurable:!0});import{parseScope as h}from"@north-light/identity";import{KIND as g,LOCAL as k,RESERVED_KIND_ROOTS as b,MAX_KIND_EVENTS as E,MIN_EVENT_LEVEL as j,MAX_EVENT_LEVEL as _,eventCatalogReasons as S,objectBlocksShapeReasons as a,objectKindReadReasons as f}from"../api/command-manifest/object-kinds.js";const w=/^crtr:objects:(?:read|watch|link|write):/;function L(e,n){const t=a(e);if(t.length>0)return t;const{name:c,leafPaths:p,streamingLeafPaths:s}=n;for(const[o,u]of Object.entries(e.object_kinds??{}))o.split(".")[0]!==c&&t.push(`object_kinds ${JSON.stringify(o)} must start with the plugin name "${c}."`),t.push(...f(o,u,c,p,s));const i=e.scopes??[];e.scopes!==void 0&&e.provider!==void 0&&t.push("scopes is for local plugins; a provider gets its scopes from directory consent");for(const o of i){try{h(o)}catch{t.push(`scope ${JSON.stringify(o)} is not a recognized scope`);continue}w.test(o)?o.endsWith(`:app:${c}`)&&t.push(`scope ${JSON.stringify(o)} names the plugin's own space; an app needs no scope for its own objects`):t.push(`scope ${JSON.stringify(o)} must be crtr:objects:<read|watch|link|write>:<target>`)}return t}r(L,"objectBlocksReasons");function m(e){return e.object_kinds!==void 0||e.scopes!==void 0}r(m,"declaresObjectBlocks");function y(e){return e.provider!==void 0?!1:Object.keys(e.object_kinds??{}).length>0||(e.scopes?.length??0)>0||e.transport?.kind==="http"}r(y,"needsAppToken");function P(e,n){return d(e,n,()=>!0)}r(P,"leafPathsBelowRoot");function V(e,n){return d(e,n,t=>"rest"in t&&t.rest.streaming===!0)}r(V,"streamingLeafPathsBelowRoot");function d(e,n,t){const c=new Set,p=r((s,i)=>{if(s.kind==="leaf")t(s)&&c.add([...i,s.name].join(" "));else for(const o of s.children)p(o,[...i,s.name])},"walk");for(const s of n)if(s.name===e&&s.kind==="branch")for(const i of s.children)p(i,[]);return c}r(d,"collectLeafPaths");export{g as KIND,k as LOCAL,_ as MAX_EVENT_LEVEL,E as MAX_KIND_EVENTS,j as MIN_EVENT_LEVEL,b as RESERVED_KIND_ROOTS,m as declaresObjectBlocks,S as eventCatalogReasons,P as leafPathsBelowRoot,y as needsAppToken,L as objectBlocksReasons,a as objectBlocksShapeReasons,f as objectKindReadReasons,V as streamingLeafPathsBelowRoot};
@@ -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};
@@ -58,7 +58,7 @@ Plugins can now declare canvas object kinds and act on the canvas, and an app to
58
58
  - `app:<x>`: nodes and custom objects of another app `x`. An app never needs a scope for its own objects, and a plugin may not declare its own `app:<plugin>` space.
59
59
  - `node`: any node, including the stub of a node on another of the account's machines.
60
60
 
61
- `read` lets the holder read objects of the target, `write` change them, and `link` (below) make a node watch an object the holder owns. `watch` lets a custom object that watches an object of the target read it. Holding a scope never lifts run confinement inside the owning app, and a revoked grant holds nothing. A public custom object is readable by any app that may use the owning app's tools, with no scope. The app listener now serves the canvas operations (`objects.*`, `custom-objects.*`, `docs.*`) to an app grant token under its resolved scopes; those rows answered `unknown operation` there before.
61
+ `read` lets the holder read objects of the target, `write` change them, and `link` (below) make a node watch an object the holder owns. `watch` lets a custom object that watches an object of the target read it. Holding a scope never lifts run confinement inside the owning app, and a revoked grant holds nothing. A public custom object is readable by any app that may use the owning app's tools, with no scope. A `friends` or `private` instance is readable, with no read scope, only by a run (a node, or a node of another machine) that holds one of the owning app's provider-group scopes `<app>:<group>`, the scopes a call to that app's provider commands needs; another app's server reads it only through a held `crtr:objects:read:app:<app>` scope. The app listener now serves the canvas operations (`objects.*`, `custom-objects.*`, `docs.*`) to an app grant token under its resolved scopes; those rows answered `unknown operation` there before.
62
62
 
63
63
  ### The `crtr_app_` credential and its socket fetch (additive)
64
64
 
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.492",
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.492",
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.492",
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.492",
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.492",
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.492",
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.492",
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.492",
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.492",
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.492",
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.492",
12141
+ "@north-light/identity": "0.3.492"
12142
12142
  }
12143
12143
  },
12144
12144
  "packages/crouter-sdk": {
12145
12145
  "name": "@north-light/sdk",
12146
- "version": "0.3.490",
12146
+ "version": "0.3.492",
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.492",
12150
+ "@north-light/identity": "0.3.492",
12151
12151
  "jose": "^6.2.1"
12152
12152
  },
12153
12153
  "bin": {