@north-light/cli 0.3.415 → 0.3.416

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/cli.js +7 -7
  2. package/dist/clients/attach/viewer.js +501 -501
  3. package/dist/core/grants/on-sync.js +1 -1
  4. package/dist/core/graph/custom.js +5 -5
  5. package/dist/core/graph/reader.js +3 -3
  6. package/dist/core/graph/spaces/app.js +1 -1
  7. package/dist/core/graph/types.d.ts +5 -0
  8. package/dist/core/runs/confinement.d.ts +2 -0
  9. package/dist/core/runs/confinement.js +2 -2
  10. package/dist/core/runtime/run-command.d.ts +4 -0
  11. package/dist/core/runtime/run-command.js +1 -1
  12. package/dist/core/spaces/mounts.d.ts +5 -0
  13. package/dist/core/spaces/mounts.js +1 -1
  14. package/dist/core/spaces/process.d.ts +8 -0
  15. package/dist/core/spaces/process.js +1 -1
  16. package/dist/daemon/api/handlers/crons.js +1 -1
  17. package/dist/daemon/api/handlers/files.js +1 -1
  18. package/dist/daemon/api/handlers/nodes.js +1 -1
  19. package/dist/daemon/api/handlers/providers.js +1 -1
  20. package/dist/daemon/api/handlers/runs.js +1 -1
  21. package/dist/daemon/api/operations.d.ts +1 -1
  22. package/dist/daemon/api/operations.js +1 -1
  23. package/dist/daemon/api/router.d.ts +15 -0
  24. package/dist/daemon/api/router.js +1 -1
  25. package/dist/daemon/api/server.d.ts +5 -1
  26. package/dist/daemon/api/server.js +1 -1
  27. package/dist/daemon/cron/live-runs.d.ts +28 -0
  28. package/dist/daemon/cron/live-runs.js +1 -1
  29. package/dist/daemon/cron/run-socket.d.ts +30 -0
  30. package/dist/daemon/cron/run-socket.js +1 -0
  31. package/dist/daemon/cron-run.js +2 -1
  32. package/dist/daemon/crtrd.js +7 -7
  33. package/dist/daemon/reconcilers/cron-lane.js +1 -1
  34. package/dist/shared/env.d.ts +6 -5
  35. package/dist/shared/env.js +2 -2
  36. package/package.json +2 -2
  37. package/packages/crouter-identity/package.json +1 -1
  38. package/runtime.lock.json +12 -12
@@ -1 +1 @@
1
- var b=Object.defineProperty;var p=(o,r)=>b(o,"name",{value:r,configurable:!0});import{prepared as m}from"../sqlite-statements.js";import{relative as h,sep as v}from"node:path";import{activeBackgroundBashJobs as x,readJobPgid as S}from"../bash-jobs.js";import{appSpace as y}from"../layout.js";import{getNode as f,recordSettledTurn as k}from"../canvas/index.js";import{transition as E}from"../runtime/lifecycle.js";import{openDb as _}from"../canvas/db.js";import{contextDir as R}from"../canvas/node-agent-paths.js";import{emitEvent as c}from"../events/emit.js";import{closeGrantStreams as G}from"../runs/events.js";import{grantedMounts as P,isGrantedSource as C,mountAllowed as T}from"../spaces/mounts.js";import{permissionsForRun as g}from"../spaces/permissions.js";import{stopNodeSandboxes as M}from"../spaces/stop.js";import{discardChangedGrantTokens as O}from"../providers/token-cache.js";import{activeGrantForApp as A,allGrants as D}from"./mirror.js";import{runRemovalCascades as N}from"./remove.js";import{notifyGrantChanges as F}from"./watch.js";import{launcherPresent as I,listSandboxes as W,stopSandboxPid as j}from"./sandboxes.js";function w(){return new Map(m(_(),"SELECT uid, app, status FROM app_users").all().map(o=>[o.uid,{app:o.app,status:o.status}]))}p(w,"appUserOwners");function B(o,r=w()){const e=r.get(o.uid);if(!e||e.app==="app:terminal")return null;if(e.status!=="active")return"app_removed";const s=f(o.nodeId);try{let t;if(s&&s.grantee===e.app)t=g(e.app,s.sub??"user:local",s.grant_id??null,s.scopes??null);else{const i=A(e.app);if(!i)return"permission_revoked";t=g(e.app,i.sub,i.id,null)}const n=P(t),a=y(e.app).root;return o.mounts.every(i=>{const l=h(a,i.source);return l===""||l!==".."&&!l.startsWith(`..${v}`)&&!l.startsWith(v)||!C(e.app,i.source)||T(i,n)})?null:"permission_revoked"}catch{return"permission_revoked"}}p(B,"exceeds");async function J(o){const r=f(o);if(!r)return;(r.status==="active"||r.status==="idle")&&E(o,"release");const e=new Date().toISOString();k(o,e),r.run_id&&r.run_id!==o&&m(_(),"UPDATE nodes SET last_settled_at = COALESCE(last_settled_at, ?) WHERE node_id = ?").run(e,r.run_id)}p(J,"afterRevocationStop");function L(o){const r=new Set,e=f(o);e?.pi_pid!=null&&r.add(e.pi_pid);try{const s=R(o,e?.grantee);for(const t of x(s,!0)){const n=S(s,t.jobId);n!==void 0&&r.add(n)}}catch{}return r}p(L,"nodePids");async function U(){if(!I())return{completion:Promise.resolve()};const o=await W(),r=w(),e=o.flatMap(n=>{const a=B(n,r);return a?[{sandbox:n,reason:a}]:[]}),s=[],t=new Map;for(const n of e){c({level:"info",event:"daemon.grants.sandbox_stop",fields:{sandbox_node:n.sandbox.nodeId,pid:n.sandbox.pid,grantee:r.get(n.sandbox.uid).app,reason:n.reason}});const a=t.get(n.sandbox.nodeId)??[];a.push(n),t.set(n.sandbox.nodeId,a)}for(const[n,a]of t){const i=f(n),l=i?L(n):new Set,u=i!==null&&a.some(({sandbox:d})=>l.has(d.pid));s.push((async()=>{if(u){const d=await M(n,"permission_revoked",{holdRecovery:!0,after:p(()=>J(n),"after")});if(!d.confirmed)throw d.error??new Error(`stop of ${n} was not confirmed`)}for(const{sandbox:d}of a)(!u||!l.has(d.pid))&&await j(d.pid)})().catch(d=>{c({level:"error",event:"daemon.grants.sandbox_stop_failed",...i?{node_id:n}:{fields:{sandbox_node:n}},error:d})}))}return{completion:Promise.all(s).then(()=>{})}}p(U,"sweepSandboxes");function $(o,r){try{const{pairs:e,tokens:s}=O(o,r);e&&c({level:"info",event:"daemon.grants.outbound_tokens_discarded",fields:{grants:e,tokens:s}})}catch(e){c({level:"error",event:"daemon.grants.outbound_tokens_discard_failed",error:e})}}p($,"discardOutboundTokens");async function ce(o,r=e=>{}){const e=D();try{F(o,e)}catch(t){c({level:"error",event:"daemon.grants.notify_failed",error:t})}$(o,e);for(const t of o){if(t.status!=="active"||e.find(i=>i.id===t.id)?.status==="active")continue;const a=G(t.id);a&&c({level:"info",event:"daemon.grants.streams_closed",fields:{grant:t.id,grantee:t.grantee,streams:a}})}try{const{completion:t}=await U();r(t)}catch(t){c({level:"error",event:"daemon.grants.sweep_failed",error:t})}const s=new Set(e.filter(t=>t.status==="removed"&&o.find(n=>n.id===t.id)?.status!=="removed").map(t=>t.grantee));r(N(s).catch(()=>{})),r(import("../providers/bundles.js").then(({syncProviderBundles:t})=>t()).catch(t=>{c({level:"error",event:"daemon.providers.bundle_sync_failed",error:t})}))}p(ce,"onGrantSync");export{B as exceeds,ce as onGrantSync,U as sweepSandboxes};
1
+ var w=Object.defineProperty;var c=(n,r)=>w(n,"name",{value:r,configurable:!0});import{prepared as v}from"../sqlite-statements.js";import{relative as b,sep as g}from"node:path";import{activeBackgroundBashJobs as x,readJobPgid as y}from"../bash-jobs.js";import{appSpace as S}from"../layout.js";import{getNode as l,recordSettledTurn as k}from"../canvas/index.js";import{transition as E}from"../runtime/lifecycle.js";import{openDb as _}from"../canvas/db.js";import{contextDir as R}from"../canvas/node-agent-paths.js";import{emitEvent as p}from"../events/emit.js";import{closeGrantStreams as C}from"../runs/events.js";import{grantedMounts as G,isGrantedSource as I,mountAllowed as P}from"../spaces/mounts.js";import{permissionsForRun as m}from"../spaces/permissions.js";import{stopNodeSandboxes as T}from"../spaces/stop.js";import{discardChangedGrantTokens as F}from"../providers/token-cache.js";import{activeGrantForApp as M,allGrants as O}from"./mirror.js";import{runRemovalCascades as A}from"./remove.js";import{notifyGrantChanges as D}from"./watch.js";import{launcherPresent as N,listSandboxes as W,stopSandboxPid as j}from"./sandboxes.js";import{checkCronFire as B}from"../../daemon/cron/authority.js";import{cronExecutionFor as J}from"../../daemon/cron/live-runs.js";import{cronExecutionsReady as L}from"../../daemon/cron/run-socket.js";function h(){return new Map(v(_(),"SELECT uid, app, status FROM app_users").all().map(n=>[n.uid,{app:n.app,status:n.status}]))}c(h,"appUserOwners");function U(n,r=h()){const o=r.get(n.uid),i=n.nodeId.startsWith("cron-"),e=i?J(n.nodeId.slice(5)):void 0;if(i&&(e===void 0||!o))return"permission_revoked";if(e!==void 0&&o.status==="active"){const s=B({grantee:e.grantee,sub:e.sub,grant_id:e.grantId,scopes:e.scopes});if(!s.allowed)return e.refusal??={reason:s.reason,message:s.message},"permission_revoked"}if(!o||o.app==="app:terminal")return null;if(o.status!=="active")return"app_removed";const t=l(n.nodeId);try{let s;if(e!==void 0)s=m(e.grantee,e.sub,e.grantId,e.scopes);else if(t&&t.grantee===o.app)s=m(o.app,t.sub??"user:local",t.grant_id??null,t.scopes??null);else{const d=M(o.app);if(!d)return"permission_revoked";s=m(o.app,d.sub,d.id,null)}const u=G(s),f=S(o.app).root;return n.mounts.every(d=>{const a=b(f,d.source);return a===""||a!==".."&&!a.startsWith(`..${g}`)&&!a.startsWith(g)||!I(o.app,d.source)||P(d,u)})?null:"permission_revoked"}catch{return"permission_revoked"}}c(U,"exceeds");async function $(n){const r=l(n);if(!r)return;(r.status==="active"||r.status==="idle")&&E(n,"release");const o=new Date().toISOString();k(n,o),r.run_id&&r.run_id!==n&&v(_(),"UPDATE nodes SET last_settled_at = COALESCE(last_settled_at, ?) WHERE node_id = ?").run(o,r.run_id)}c($,"afterRevocationStop");function H(n){const r=new Set,o=l(n);o?.pi_pid!=null&&r.add(o.pi_pid);try{const i=R(n,o?.grantee);for(const e of x(i,!0)){const t=y(i,e.jobId);t!==void 0&&r.add(t)}}catch{}return r}c(H,"nodePids");async function q(){if(!N())return{completion:Promise.resolve()};await L();const n=await W(),r=h(),o=n.flatMap(t=>{const s=U(t,r);return s?[{sandbox:t,reason:s}]:[]}),i=[],e=new Map;for(const t of o){p({level:"info",event:"daemon.grants.sandbox_stop",fields:{sandbox_node:t.sandbox.nodeId,pid:t.sandbox.pid,grantee:r.get(t.sandbox.uid).app,reason:t.reason}});const s=e.get(t.sandbox.nodeId)??[];s.push(t),e.set(t.sandbox.nodeId,s)}for(const[t,s]of e){const u=l(t),f=u?H(t):new Set,d=u!==null&&s.some(({sandbox:a})=>f.has(a.pid));i.push((async()=>{if(d){const a=await T(t,"permission_revoked",{holdRecovery:!0,after:c(()=>$(t),"after")});if(!a.confirmed)throw a.error??new Error(`stop of ${t} was not confirmed`)}for(const{sandbox:a}of s)(!d||!f.has(a.pid))&&await j(a.pid)})().catch(a=>{p({level:"error",event:"daemon.grants.sandbox_stop_failed",...u?{node_id:t}:{fields:{sandbox_node:t}},error:a})}))}return{completion:Promise.all(i).then(()=>{})}}c(q,"sweepSandboxes");function z(n,r){try{const{pairs:o,tokens:i}=F(n,r);o&&p({level:"info",event:"daemon.grants.outbound_tokens_discarded",fields:{grants:o,tokens:i}})}catch(o){p({level:"error",event:"daemon.grants.outbound_tokens_discard_failed",error:o})}}c(z,"discardOutboundTokens");async function _e(n,r=o=>{}){const o=O();try{D(n,o)}catch(e){p({level:"error",event:"daemon.grants.notify_failed",error:e})}z(n,o);for(const e of n){if(e.status!=="active"||o.find(u=>u.id===e.id)?.status==="active")continue;const s=C(e.id);s&&p({level:"info",event:"daemon.grants.streams_closed",fields:{grant:e.id,grantee:e.grantee,streams:s}})}try{const{completion:e}=await q();r(e)}catch(e){p({level:"error",event:"daemon.grants.sweep_failed",error:e})}const i=new Set(o.filter(e=>e.status==="removed"&&n.find(t=>t.id===e.id)?.status!=="removed").map(e=>e.grantee));r(A(i).catch(()=>{})),r(import("../providers/bundles.js").then(({syncProviderBundles:e})=>e()).catch(e=>{p({level:"error",event:"daemon.providers.bundle_sync_failed",error:e})}))}c(_e,"onGrantSync");export{U as exceeds,_e as onGrantSync,q as sweepSandboxes};
@@ -1,11 +1,11 @@
1
- var $=Object.defineProperty;var u=(e,t)=>$(e,"name",{value:t,configurable:!0});import{prepared as a}from"../sqlite-statements.js";import{objectPlace as C,runPlace as x,runsSeeEachOther as P}from"../runs/confinement.js";import{createHash as J,randomBytes as q,timingSafeEqual as F}from"node:crypto";import{CrtrError as S,usage as g}from"../errors.js";import{ExitCode as O}from"../../types.js";import{createObject as M,liveDocumentNamed as B}from"./objects.js";import{invalidateCustomKind as z}from"./kinds/registry.js";import{mayActAs as L}from"./access.js";import{publish as b,HOP_LIMIT as K,HopLimitError as G}from"./events.js";import{checkoutsForCwd as N}from"./repo-sync/repos.js";import{onQueueSignal as Q,signalQueue as R}from"./receivers.js";import{receiverEndpoint as V,isPublicReceiverAddress as Y}from"./receiver-url.js";import{isIP as X}from"node:net";const Z=/^[a-z][a-z0-9-]*\.[a-z][a-z0-9-]*$/,k=/^[a-z][a-z0-9-]*$/;function h(e,t){throw new S(e,t,O.GENERAL)}u(h,"conflict");function D(e){return J("sha256").update(e).digest("hex")}u(D,"hash");function H(){return`crtr_pub_${q(32).toString("base64url")}`}u(H,"issueToken");function ee(e){if(e==null)return null;if(e==="queue")return JSON.stringify(e);if(typeof e!="object"||Array.isArray(e))throw g("invalid custom receiver; expected queue, an https url, or argv and absolute cwd");if("url"in e&&typeof e.url=="string")try{const s=V(e.url).hostname.replace(/^\[|\]$/g,"");if(X(s)&&!Y(s))throw new Error("non-public IP address");return JSON.stringify({url:e.url})}catch(t){throw g(`invalid custom receiver: ${String(t)}`)}if("argv"in e&&Array.isArray(e.argv)&&e.argv.length>0&&e.argv.every(t=>typeof t=="string")&&typeof e.cwd=="string"&&e.cwd.startsWith("/"))return JSON.stringify(e);throw g("invalid custom receiver; url must be https and argv requires a command and absolute cwd")}u(ee,"receiverJson");function _e(e,t,s){const{kind:n,name:i,owner:o,description:r,events:l,receiver:m,rotateToken:w}=s;if(L(e,t,o)||h("forbidden",`cannot act as ${o.id}`),!Z.test(n)||["report","notice","node","job","document","person","app","repo","profile","action"].includes(n.split(".")[0]))throw g(`invalid or reserved kind ${n}`);if(!i.trim()||i!==i.trim()||i.includes("\0"))throw g(`invalid object name ${JSON.stringify(i)}`);const f=ee(m);if(m!=="queue"&&m!==null&&m!==void 0&&"argv"in m&&t.class!=="person"&&t.class!=="node"&&h("argv_forbidden","argv receiver requires a person or node principal"),Object.keys(l).length>64||Object.entries(l).some(([c,_])=>!k.test(c)||!Number.isInteger(_?.level)||_.level<1||_.level>10||_.supersedes!==void 0&&typeof _.supersedes!="boolean"))throw g("invalid custom event catalog");const d=B(e,o.rowid,i);if(d&&!te(e,t,d)&&h("forbidden",`cannot act as ${o.id}`),d&&t.class!=="person"&&t.class!=="node"){const c=a(e,"SELECT receiver_json FROM custom_objects WHERE object = ?").get(d.rowid)?.receiver_json;c&&typeof JSON.parse(c)=="object"&&"argv"in JSON.parse(c)&&h("argv_forbidden","argv receiver requires a person or node principal")}d&&d.type!==n&&h("name_taken",`${o.name}/${i} is already held by ${d.id}`);const p=a(e,"SELECT owner, description, events_json FROM object_kinds WHERE name = ?").get(n);p&&p.owner!==o.rowid&&h("kind_owned",`${n} belongs to another owner`);const T=new Date().toISOString(),v=U(l),A=p===void 0?null:U(JSON.parse(p.events_json));if(p?(p.description!==r||A!==v)&&a(e,"UPDATE object_kinds SET description = ?, events_json = ?, updated_at = ? WHERE name = ?").run(r,v,T,n):a(e,"INSERT INTO object_kinds VALUES (?, ?, ?, ?, ?, ?)").run(n,o.rowid,r,v,T,T),(!p||A!==v)&&z(e,n),!d){const c=M(e,{type:n,kind:null,owner:o.rowid,privacy:"public",name:i,preview:""}),_=H();return a(e,`INSERT INTO custom_objects (object, description, receiver_json, token_hash)
2
- VALUES (?, ?, ?, ?)`).run(c.rowid,r,f,D(_)),t.class==="node"&&a(e,"UPDATE custom_objects SET registered_by = ? WHERE object = ?").run(t.node.id,c.rowid),f!==null&&a(e,"UPDATE objects SET reception = 2 WHERE rowid = ?").run(c.rowid),b(e,{event:"created",object:c,actor:t.class==="object"?t.object:t.class==="person"?t.person:t.class==="app"?t.app:t.node,attrs:{}},I=>N(e,I)),{object:c,token:_,created:!0,changed:[]}}const y=a(e,"SELECT description, receiver_json FROM custom_objects WHERE object = ?").get(d.rowid);y||h("name_taken",`${o.name}/${i} is not a custom object`);const E=[];if(y.description!==r&&E.push("description"),y.receiver_json!==f&&E.push("receiver"),p&&A!==v&&E.push("events"),p&&p.description!==r&&!E.includes("description")&&E.push("description"),w&&E.push("token"),E.length===0)return{object:d,created:!1,changed:E};const j=w?H():void 0;if(a(e,"UPDATE custom_objects SET description = ?, receiver_json = ?, token_hash = COALESCE(?, token_hash) WHERE object = ?").run(r,f,j===void 0?null:D(j),d.rowid),a(e,"UPDATE objects SET reception = ? WHERE rowid = ?").run(f===null?0:2,d.rowid),y.receiver_json!==f){const c=Date.now();if(f===null){const _=a(e,`UPDATE deliveries SET canceled_at = ?, updated_at = ?
1
+ var $=Object.defineProperty;var u=(e,t)=>$(e,"name",{value:t,configurable:!0});import{prepared as a}from"../sqlite-statements.js";import{objectPlace as C,runPlace as x,runsSeeEachOther as P}from"../runs/confinement.js";import{createHash as J,randomBytes as q,timingSafeEqual as F}from"node:crypto";import{CrtrError as S,usage as g}from"../errors.js";import{ExitCode as O}from"../../types.js";import{createObject as M,liveDocumentNamed as B}from"./objects.js";import{invalidateCustomKind as z}from"./kinds/registry.js";import{mayActAs as L}from"./access.js";import{publish as b,HOP_LIMIT as K,HopLimitError as G}from"./events.js";import{checkoutsForCwd as N}from"./repo-sync/repos.js";import{onQueueSignal as Q,signalQueue as R}from"./receivers.js";import{receiverEndpoint as V,isPublicReceiverAddress as Y}from"./receiver-url.js";import{isIP as X}from"node:net";const Z=/^[a-z][a-z0-9-]*\.[a-z][a-z0-9-]*$/,k=/^[a-z][a-z0-9-]*$/;function h(e,t){throw new S(e,t,O.GENERAL)}u(h,"conflict");function D(e){return J("sha256").update(e).digest("hex")}u(D,"hash");function H(){return`crtr_pub_${q(32).toString("base64url")}`}u(H,"issueToken");function ee(e){if(e==null)return null;if(e==="queue")return JSON.stringify(e);if(typeof e!="object"||Array.isArray(e))throw g("invalid custom receiver; expected queue, an https url, or argv and absolute cwd");if("url"in e&&typeof e.url=="string")try{const s=V(e.url).hostname.replace(/^\[|\]$/g,"");if(X(s)&&!Y(s))throw new Error("non-public IP address");return JSON.stringify({url:e.url})}catch(t){throw g(`invalid custom receiver: ${String(t)}`)}if("argv"in e&&Array.isArray(e.argv)&&e.argv.length>0&&e.argv.every(t=>typeof t=="string")&&typeof e.cwd=="string"&&e.cwd.startsWith("/"))return JSON.stringify(e);throw g("invalid custom receiver; url must be https and argv requires a command and absolute cwd")}u(ee,"receiverJson");function _e(e,t,s){const{kind:n,name:i,owner:o,description:r,events:l,receiver:m,rotateToken:w}=s;if(L(e,t,o)||h("forbidden",`cannot act as ${o.id}`),!Z.test(n)||["report","notice","node","job","document","person","app","repo","profile","action"].includes(n.split(".")[0]))throw g(`invalid or reserved kind ${n}`);if(!i.trim()||i!==i.trim()||i.includes("\0"))throw g(`invalid object name ${JSON.stringify(i)}`);const p=ee(m);if(m!=="queue"&&m!==null&&m!==void 0&&"argv"in m&&t.class!=="person"&&t.class!=="node"&&h("argv_forbidden","argv receiver requires a person or node principal"),Object.keys(l).length>64||Object.entries(l).some(([c,_])=>!k.test(c)||!Number.isInteger(_?.level)||_.level<1||_.level>10||_.supersedes!==void 0&&typeof _.supersedes!="boolean"))throw g("invalid custom event catalog");const d=B(e,o.rowid,i);if(d&&!te(e,t,d)&&h("forbidden",`cannot act as ${o.id}`),d&&t.class!=="person"&&t.class!=="node"){const c=a(e,"SELECT receiver_json FROM custom_objects WHERE object = ?").get(d.rowid)?.receiver_json;c&&typeof JSON.parse(c)=="object"&&"argv"in JSON.parse(c)&&h("argv_forbidden","argv receiver requires a person or node principal")}d&&d.type!==n&&h("name_taken",`${o.name}/${i} is already held by ${d.id}`);const f=a(e,"SELECT owner, description, events_json FROM object_kinds WHERE name = ?").get(n);f&&f.owner!==o.rowid&&h("kind_owned",`${n} belongs to another owner`);const T=new Date().toISOString(),v=U(l),A=f===void 0?null:U(JSON.parse(f.events_json));if(f?(f.description!==r||A!==v)&&a(e,"UPDATE object_kinds SET description = ?, events_json = ?, updated_at = ? WHERE name = ?").run(r,v,T,n):a(e,"INSERT INTO object_kinds VALUES (?, ?, ?, ?, ?, ?)").run(n,o.rowid,r,v,T,T),(!f||A!==v)&&z(e,n),!d){const c=M(e,{type:n,kind:null,owner:o.rowid,privacy:"public",name:i,preview:""}),_=H();return a(e,`INSERT INTO custom_objects (object, description, receiver_json, token_hash)
2
+ VALUES (?, ?, ?, ?)`).run(c.rowid,r,p,D(_)),t.class==="node"&&a(e,"UPDATE custom_objects SET registered_by = ? WHERE object = ?").run(t.node.id,c.rowid),p!==null&&a(e,"UPDATE objects SET reception = 2 WHERE rowid = ?").run(c.rowid),b(e,{event:"created",object:c,actor:t.class==="object"?t.object:t.class==="person"?t.person:t.class==="app"?t.app:t.node,attrs:{}},I=>N(e,I)),{object:c,token:_,created:!0,changed:[]}}const y=a(e,"SELECT description, receiver_json FROM custom_objects WHERE object = ?").get(d.rowid);y||h("name_taken",`${o.name}/${i} is not a custom object`);const E=[];if(y.description!==r&&E.push("description"),y.receiver_json!==p&&E.push("receiver"),f&&A!==v&&E.push("events"),f&&f.description!==r&&!E.includes("description")&&E.push("description"),w&&E.push("token"),E.length===0)return{object:d,created:!1,changed:E};const j=w?H():void 0;if(a(e,"UPDATE custom_objects SET description = ?, receiver_json = ?, token_hash = COALESCE(?, token_hash) WHERE object = ?").run(r,p,j===void 0?null:D(j),d.rowid),a(e,"UPDATE objects SET reception = ? WHERE rowid = ?").run(p===null?0:2,d.rowid),y.receiver_json!==p){const c=Date.now();if(p===null){const _=a(e,`UPDATE deliveries SET canceled_at = ?, updated_at = ?
3
3
  WHERE destination_id = ? AND state = 'pending' AND canceled_at IS NULL`).run(c,c,d.id);a(e,"UPDATE custom_objects SET dropped = dropped + ? WHERE object = ?").run(_.changes,d.rowid)}else a(e,`UPDATE deliveries SET next_attempt_at = ?, updated_at = ?
4
- WHERE destination_id = ? AND state = 'pending' AND canceled_at IS NULL`).run(f==='"queue"'?null:c,c,d.id);R(d.id)}return w&&R(d.id),b(e,{event:"configured",object:d,actor:t.class==="object"?t.object:t.class==="person"?t.person:t.class==="app"?t.app:t.node,attrs:{changed:E.join(",")}},c=>N(e,c)),{object:d,...j===void 0?{}:{token:j},created:!1,changed:E}}u(_e,"registerCustomObject");function ge(e,t,s,n){const i=a(e,"SELECT events_json FROM object_kinds WHERE name = ?").get(t.type);if(!i||t.deleted_at!==null)throw g(`${t.id} is not a live custom object`);const o=JSON.parse(i.events_json);if(!k.test(n.event)||!Object.hasOwn(o,n.event))throw new S("unknown_event",`${t.type} declares ${Object.keys(o).join(", ")}, not ${n.event}`,O.USAGE,{declared:Object.keys(o),event:n.event});if(typeof n.summary!="string")throw g("summary must be text");const r=n.state===void 0?null:JSON.stringify(n.state);if(n.state!==void 0&&r===void 0)throw g("state must be JSON-serializable");if(Buffer.byteLength(JSON.stringify({summary:n.summary,...n.state===void 0?{}:{state:n.state}}))>64*1024)throw new S("body_too_large","custom event summary and state exceed 64 KiB",O.USAGE);if(r!==null&&Buffer.byteLength(r)>64*1024)throw g("state exceeds 64 KiB");const l=n.cause??null,m=l===null?null:a(e,`SELECT depth FROM messages WHERE message_id = ?
4
+ WHERE destination_id = ? AND state = 'pending' AND canceled_at IS NULL`).run(p==='"queue"'?null:c,c,d.id);R(d.id)}return w&&R(d.id),b(e,{event:"configured",object:d,actor:t.class==="object"?t.object:t.class==="person"?t.person:t.class==="app"?t.app:t.node,attrs:{changed:E.join(",")}},c=>N(e,c)),{object:d,...j===void 0?{}:{token:j},created:!1,changed:E}}u(_e,"registerCustomObject");function ge(e,t,s,n){const i=a(e,"SELECT events_json FROM object_kinds WHERE name = ?").get(t.type);if(!i||t.deleted_at!==null)throw g(`${t.id} is not a live custom object`);const o=JSON.parse(i.events_json);if(!k.test(n.event)||!Object.hasOwn(o,n.event))throw new S("unknown_event",`${t.type} declares ${Object.keys(o).join(", ")}, not ${n.event}`,O.USAGE,{declared:Object.keys(o),event:n.event});if(typeof n.summary!="string")throw g("summary must be text");const r=n.state===void 0?null:JSON.stringify(n.state);if(n.state!==void 0&&r===void 0)throw g("state must be JSON-serializable");if(Buffer.byteLength(JSON.stringify({summary:n.summary,...n.state===void 0?{}:{state:n.state}}))>64*1024)throw new S("body_too_large","custom event summary and state exceed 64 KiB",O.USAGE);if(r!==null&&Buffer.byteLength(r)>64*1024)throw g("state exceeds 64 KiB");const l=n.cause??null,m=l===null?null:a(e,`SELECT depth FROM messages WHERE message_id = ?
5
5
  UNION ALL SELECT depth FROM message_causes WHERE message_id = ? LIMIT 1`).get(l,l);if(l!==null&&m===void 0)throw new S("unknown_cause",`no message ${l}`,O.USAGE);if(m&&m.depth>=K)throw new G(m.depth+1);const w=new Date().toISOString();return a(e,"UPDATE custom_objects SET summary = ?, state_json = ?, published_at = ? WHERE object = ?").run(n.summary,r,w,t.rowid),a(e,"UPDATE objects SET preview = ? WHERE rowid = ?").run(n.summary.split(`
6
- `)[0],t.rowid),b(e,{event:`${t.type}.${n.event}`,object:t,actor:s,attrs:{},messageBody:{summary:n.summary,...n.state===void 0?{}:{state:n.state}},...n.cause===void 0?{}:{cause:n.cause}},f=>N(e,f))}u(ge,"publishCustom");function he(e,t,s){if(t.deleted_at!==null)return;b(e,{event:"deleted",object:t,actor:s,attrs:{}},i=>N(e,i));const n=new Date().toISOString();a(e,"UPDATE objects SET deleted_at = ?, reception = 0 WHERE rowid = ?").run(n,t.rowid),a(e,"UPDATE deliveries SET canceled_at = ?, updated_at = ? WHERE destination_id = ? AND state = 'pending' AND canceled_at IS NULL").run(Date.now(),Date.now(),t.id),R(t.id)}u(he,"unregisterCustomObject");function U(e){return JSON.stringify(Object.fromEntries(Object.entries(e).sort(([t],[s])=>t.localeCompare(s)).map(([t,s])=>[t,{level:s.level,...s.supersedes===void 0?{}:{supersedes:s.supersedes}}])))}u(U,"canonicalEvents");function we(e,t,s=0){return W(e,t.id,s,"pending")}u(we,"pendingFor");function ve(e,t){const s=a(e,"SELECT destination_id FROM deliveries WHERE delivery_id = ?").get(t);return s?W(e,s.destination_id,-1,"handed_off").find(n=>n.delivery_id===t)??null:null}u(ve,"pushLineFor");function W(e,t,s,n){return a(e,`SELECT d.delivery_id, m.message_id, m.seq, m.event, m.level, m.object_id, m.actor_id, m.body_json, m.body_ref, m.depth, m.created_at,
6
+ `)[0],t.rowid),b(e,{event:`${t.type}.${n.event}`,object:t,actor:s,attrs:{},messageBody:{summary:n.summary,...n.state===void 0?{}:{state:n.state}},...n.cause===void 0?{}:{cause:n.cause}},p=>N(e,p))}u(ge,"publishCustom");function he(e,t,s){if(t.deleted_at!==null)return;b(e,{event:"deleted",object:t,actor:s,attrs:{}},i=>N(e,i));const n=new Date().toISOString();a(e,"UPDATE objects SET deleted_at = ?, reception = 0 WHERE rowid = ?").run(n,t.rowid),a(e,"UPDATE deliveries SET canceled_at = ?, updated_at = ? WHERE destination_id = ? AND state = 'pending' AND canceled_at IS NULL").run(Date.now(),Date.now(),t.id),R(t.id)}u(he,"unregisterCustomObject");function U(e){return JSON.stringify(Object.fromEntries(Object.entries(e).sort(([t],[s])=>t.localeCompare(s)).map(([t,s])=>[t,{level:s.level,...s.supersedes===void 0?{}:{supersedes:s.supersedes}}])))}u(U,"canonicalEvents");function we(e,t,s=0){return W(e,t.id,s,"pending")}u(we,"pendingFor");function ve(e,t){const s=a(e,"SELECT destination_id FROM deliveries WHERE delivery_id = ?").get(t);return s?W(e,s.destination_id,-1,"handed_off").find(n=>n.delivery_id===t)??null:null}u(ve,"pushLineFor");function W(e,t,s,n){return a(e,`SELECT d.delivery_id, m.message_id, m.seq, m.event, m.level, m.object_id, m.actor_id, m.body_json, m.body_ref, m.depth, m.created_at,
7
7
  o.type, o.name, owner.id AS owner_id
8
8
  FROM deliveries d JOIN messages m ON m.message_id = d.message_id JOIN objects o ON o.id = m.object_id
9
9
  LEFT JOIN objects owner ON owner.rowid = o.owner
10
10
  WHERE d.destination_id = ? AND d.state = ? AND d.canceled_at IS NULL AND m.seq > ? ORDER BY m.seq`).all(t,n,s).map(o=>{const r=o.body_json?JSON.parse(o.body_json):{};return{delivery_id:o.delivery_id,message_id:o.message_id,seq:o.seq,event:o.event,level:o.level,object:{id:o.object_id,type:o.type,name:o.name,owner:o.owner_id},actor:o.actor_id,attrs:r.push?.attrs??{},...r.summary===void 0?{}:{summary:r.summary},...r.state===void 0?{}:{state:r.state},...r.push?.pointer===void 0?{}:{pointer:r.push.pointer},depth:o.depth,created_at:o.created_at}})}u(W,"queueLines");function ye(e,t,s){const n=Date.now(),i=[];let o=0;for(const r of s){const l=a(e,`UPDATE deliveries SET state = 'acknowledged', acknowledged_at = ?, updated_at = ?
11
- WHERE delivery_id = ? AND destination_id = ? AND state = 'pending' AND canceled_at IS NULL`).run(n,n,r,t.id);Number(l.changes)?o++:i.push(r)}return{acknowledged:o,...i.length?{unknown:i}:{}}}u(ye,"acknowledge");function je(e,t,s){const n=a(e,"SELECT token_hash FROM custom_objects WHERE object = ?").get(t.rowid),i=Buffer.from(n?.token_hash??"".padEnd(64,"0"),"hex"),o=Buffer.from(D(s),"hex");return s.startsWith("crtr_pub_")&&t.deleted_at===null&&i.length===o.length&&F(i,o)&&n?.token_hash!=null}u(je,"verifyPublishToken");function te(e,t,s){if(t.class!=="node")return!0;const n=x(t.node.id,e);if(n===null)return!0;const i=C(s.rowid,e);return i===null?!n.isolated:P(n,i)}u(te,"customReachable");export{ye as acknowledge,te as customReachable,L as mayActAs,Q as onQueueSignal,we as pendingFor,ge as publishCustom,ve as pushLineFor,_e as registerCustomObject,he as unregisterCustomObject,je as verifyPublishToken};
11
+ WHERE delivery_id = ? AND destination_id = ? AND state = 'pending' AND canceled_at IS NULL`).run(n,n,r,t.id);Number(l.changes)?o++:i.push(r)}return{acknowledged:o,...i.length?{unknown:i}:{}}}u(ye,"acknowledge");function je(e,t,s){const n=a(e,"SELECT token_hash FROM custom_objects WHERE object = ?").get(t.rowid),i=Buffer.from(n?.token_hash??"".padEnd(64,"0"),"hex"),o=Buffer.from(D(s),"hex");return s.startsWith("crtr_pub_")&&t.deleted_at===null&&i.length===o.length&&F(i,o)&&n?.token_hash!=null}u(je,"verifyPublishToken");function te(e,t,s){if(t.class!=="node"&&!(t.class==="app"&&t.place!==void 0))return!0;const n=t.class==="app"?t.place:x(t.node.id,e);if(n===null)return!0;const i=C(s.rowid,e);return i===null?!n.isolated:P(n,i)}u(te,"customReachable");export{ye as acknowledge,te as customReachable,L as mayActAs,Q as onQueueSignal,we as pendingFor,ge as publishCustom,ve as pushLineFor,_e as registerCustomObject,he as unregisterCustomObject,je as verifyPublishToken};
@@ -1,3 +1,3 @@
1
- var h=Object.defineProperty;var s=(o,e)=>h(o,"name",{value:e,configurable:!0});import{prepared as u}from"../sqlite-statements.js";import{covers as H,parseScope as b}from"@north-light/identity";import{activeGrant as N,grantFor as x}from"../grants/mirror.js";import{prepared as j}from"../canvas/db.js";import{readConfig as y}from"../config.js";import{SCOPE_VOCABULARY as O}from"../scopes.js";import{loadExactProfileManifest as C}from"../profiles/manifest.js";import{ensureOwner as w,getByRowid as L,nodeObject as M}from"./objects.js";function R(o){return{repos:o.map(e=>e.repo),checkouts:o}}s(R,"inRepos");function V(o,e,a){if(e.class==="anonymous")throw new Error("a publish token cannot read as a principal");if(e.class==="person")return{class:"person",person:F(o)};if(e.class==="app")return{class:"app",app:v(o,e.app),scopes:e.scopes};const n=E(o),r=M(o,e.node),t=r.owner===null?null:n.object(r.owner);if(!t)throw new Error(`node ${e.node} has no owner`);const i=u(o,"SELECT cwd, profile_id FROM nodes WHERE node_id = ?").get(e.node);return{class:"node",node:r,app:t,scopes:_(e.app,e.sub,e.grant,e.scopes,n),cwd:i.cwd,...R(a(i.cwd)),profile:i.profile_id===null?null:n.profile(i.profile_id,t)}}s(V,"readerFor");function $(o,e,a,n=E(o),r=!1){const t=j(o,"SELECT o.* FROM objects o WHERE o.id = ? AND o.type = 'node'").get(e),i=j(o,`SELECT (SELECT external_id FROM identities WHERE object = ?) AS grantee,
2
- sub, grant_id, scopes_json, cwd, profile_id FROM nodes WHERE node_id = ?`).get(t?.owner??null,e);if(!t||!i)throw new Error(`no node ${e}`);const{grantee:l,sub:p,grant_id:f,scopes_json:c,cwd:m,profile_id:g}=i,d=t.owner===null?null:n.object(t.owner);if(!d)throw new Error(`node ${e} has no owner`);const S=c===null?null:JSON.parse(c);return{class:"node",node:t,app:d,scopes:_(l,p??"",f,S,n,r),cwd:m,...R(a(m)),profile:g===null?null:n.profile(g,d)}}s($,"readerForNode");function Y(o,e,a=()=>[],n=E(o)){if(e.type==="node")return $(o,e.id,a,n,!0);const r=e.owner===null?null:n.object(e.owner);if(!r)throw new Error(`watcher ${e.id} has no owner`);const i=(r.type==="app"?r:r.type==="profile"&&r.owner!==null?n.object(r.owner):null)??u(o,"SELECT o.* FROM identities i JOIN objects o ON o.rowid = i.object WHERE i.external_id = 'app:terminal' AND i.scheme = 'app'").get()??u(o,"SELECT * FROM objects WHERE type = 'person' AND name = 'user'").get();if(!i)throw new Error("no terminal app or person object");const l=u(o,"SELECT external_id FROM identities WHERE object = ? AND scheme = 'app'").get(i.rowid)?.external_id??"app:terminal",p=u(o,"SELECT resolved_scopes_json FROM grants WHERE grantee = ? AND status = 'active'").all(l),f=r.type==="person"?null:p.length===0?l==="app:terminal"&&!n.terminalHasIssuer()?O:null:p.length===1?JSON.parse(p[0].resolved_scopes_json).filter(c=>c.mode==="always").map(c=>b(c.scope)):null;return{class:"object",object:e,node:e,owner:r,app:i,scopes:f,repos:r.type==="repo"?[r]:[]}}s(Y,"readerForObject");function E(o){return{grantById:N,grantFor:x,terminalHasIssuer:s(()=>!!y("user").runtime?.issuer,"terminalHasIssuer"),object:s(e=>L(o,e),"object"),profile:s((e,a)=>A(o,e,a),"profile")}}s(E,"liveLookups");function q(o){const e=E(o),a=new Map,n=s((r,t)=>(a.has(r)||a.set(r,t()),a.get(r)),"memo");return{grantById:s(r=>n(`g\0${r}`,()=>e.grantById(r)),"grantById"),grantFor:s((r,t)=>n(`a\0${r}\0${t}`,()=>e.grantFor(r,t)),"grantFor"),terminalHasIssuer:s(()=>n("issuer",()=>e.terminalHasIssuer()),"terminalHasIssuer"),object:s(r=>n(`o\0${r}`,()=>e.object(r)),"object"),profile:s((r,t)=>n(`p\0${r}\0${t.rowid}`,()=>e.profile(r,t)),"profile")}}s(q,"publishLookups");function _(o,e,a,n,r,t=!1){const i=a?r.grantById(a):r.grantFor(o,e),l=i&&i.grantee===o?i.resolved:o==="app:terminal"&&!r.terminalHasIssuer()?O:null;if(l===null){if(t)return null;throw new Error(`the grant of ${o} is not active`)}return n===null?l:n.filter(p=>l.some(f=>H(f,p)))}s(_,"nodeScopes");function F(o){return w(o,"person","local-user","user:local","user")}s(F,"personObject");function v(o,e){return e==="user:local"?F(o):w(o,"app","app",e,e.replace(/^app:/,""))}s(v,"appObject");function A(o,e,a){return u(o,`SELECT o.* FROM identities i JOIN objects o ON o.rowid = i.object
3
- WHERE i.scheme = 'profile' AND i.external_id = ?`).get(e)??w(o,"profile","profile",e,C(e).manifest.name,a)}s(A,"profileObject");export{E as liveLookups,A as profileObject,q as publishLookups,V as readerFor,$ as readerForNode,Y as readerForObject};
1
+ var b=Object.defineProperty;var s=(o,e)=>b(o,"name",{value:e,configurable:!0});import{prepared as f}from"../sqlite-statements.js";import{covers as O,parseScope as x}from"@north-light/identity";import{activeGrant as _,grantFor as y}from"../grants/mirror.js";import{prepared as R}from"../canvas/db.js";import{readConfig as C}from"../config.js";import{SCOPE_VOCABULARY as m}from"../scopes.js";import{loadExactProfileManifest as L}from"../profiles/manifest.js";import{ensureOwner as w,getByRowid as v,nodeObject as M}from"./objects.js";function F(o){return{repos:o.map(e=>e.repo),checkouts:o}}s(F,"inRepos");function V(o,e,a){if(e.class==="anonymous")throw new Error("a publish token cannot read as a principal");if(e.class==="person")return{class:"person",person:h(o)};if(e.class==="app")return{class:"app",app:H(o,e.app),scopes:e.scopes};if(e.class==="cron"){const i=e.grant?_(e.grant)?.resolved??[]:m,c=e.scopes;return{class:"app",app:H(o,e.app),scopes:c===null?i:c.filter(u=>i.some(p=>O(p,u))),place:{run:e.origin_run??"",isolated:e.isolated}}}const n=d(o),r=M(o,e.node),t=r.owner===null?null:n.object(r.owner);if(!t)throw new Error(`node ${e.node} has no owner`);const l=f(o,"SELECT cwd, profile_id FROM nodes WHERE node_id = ?").get(e.node);return{class:"node",node:r,app:t,scopes:S(e.app,e.sub,e.grant,e.scopes,n),cwd:l.cwd,...F(a(l.cwd)),profile:l.profile_id===null?null:n.profile(l.profile_id,t)}}s(V,"readerFor");function $(o,e,a,n=d(o),r=!1){const t=R(o,"SELECT o.* FROM objects o WHERE o.id = ? AND o.type = 'node'").get(e),l=R(o,`SELECT (SELECT external_id FROM identities WHERE object = ?) AS grantee,
2
+ sub, grant_id, scopes_json, cwd, profile_id FROM nodes WHERE node_id = ?`).get(t?.owner??null,e);if(!t||!l)throw new Error(`no node ${e}`);const{grantee:i,sub:c,grant_id:u,scopes_json:p,cwd:g,profile_id:j}=l,E=t.owner===null?null:n.object(t.owner);if(!E)throw new Error(`node ${e} has no owner`);const N=p===null?null:JSON.parse(p);return{class:"node",node:t,app:E,scopes:S(i,c??"",u,N,n,r),cwd:g,...F(a(g)),profile:j===null?null:n.profile(j,E)}}s($,"readerForNode");function Y(o,e,a=()=>[],n=d(o)){if(e.type==="node")return $(o,e.id,a,n,!0);const r=e.owner===null?null:n.object(e.owner);if(!r)throw new Error(`watcher ${e.id} has no owner`);const l=(r.type==="app"?r:r.type==="profile"&&r.owner!==null?n.object(r.owner):null)??f(o,"SELECT o.* FROM identities i JOIN objects o ON o.rowid = i.object WHERE i.external_id = 'app:terminal' AND i.scheme = 'app'").get()??f(o,"SELECT * FROM objects WHERE type = 'person' AND name = 'user'").get();if(!l)throw new Error("no terminal app or person object");const i=f(o,"SELECT external_id FROM identities WHERE object = ? AND scheme = 'app'").get(l.rowid)?.external_id??"app:terminal",c=f(o,"SELECT resolved_scopes_json FROM grants WHERE grantee = ? AND status = 'active'").all(i),u=r.type==="person"?null:c.length===0?i==="app:terminal"&&!n.terminalHasIssuer()?m:null:c.length===1?JSON.parse(c[0].resolved_scopes_json).filter(p=>p.mode==="always").map(p=>x(p.scope)):null;return{class:"object",object:e,node:e,owner:r,app:l,scopes:u,repos:r.type==="repo"?[r]:[]}}s(Y,"readerForObject");function d(o){return{grantById:_,grantFor:y,terminalHasIssuer:s(()=>!!C("user").runtime?.issuer,"terminalHasIssuer"),object:s(e=>v(o,e),"object"),profile:s((e,a)=>A(o,e,a),"profile")}}s(d,"liveLookups");function q(o){const e=d(o),a=new Map,n=s((r,t)=>(a.has(r)||a.set(r,t()),a.get(r)),"memo");return{grantById:s(r=>n(`g\0${r}`,()=>e.grantById(r)),"grantById"),grantFor:s((r,t)=>n(`a\0${r}\0${t}`,()=>e.grantFor(r,t)),"grantFor"),terminalHasIssuer:s(()=>n("issuer",()=>e.terminalHasIssuer()),"terminalHasIssuer"),object:s(r=>n(`o\0${r}`,()=>e.object(r)),"object"),profile:s((r,t)=>n(`p\0${r}\0${t.rowid}`,()=>e.profile(r,t)),"profile")}}s(q,"publishLookups");function S(o,e,a,n,r,t=!1){const l=a?r.grantById(a):r.grantFor(o,e),i=l&&l.grantee===o?l.resolved:o==="app:terminal"&&!r.terminalHasIssuer()?m:null;if(i===null){if(t)return null;throw new Error(`the grant of ${o} is not active`)}return n===null?i:n.filter(c=>i.some(u=>O(u,c)))}s(S,"nodeScopes");function h(o){return w(o,"person","local-user","user:local","user")}s(h,"personObject");function H(o,e){return e==="user:local"?h(o):w(o,"app","app",e,e.replace(/^app:/,""))}s(H,"appObject");function A(o,e,a){return f(o,`SELECT o.* FROM identities i JOIN objects o ON o.rowid = i.object
3
+ WHERE i.scheme = 'profile' AND i.external_id = ?`).get(e)??w(o,"profile","profile",e,L(e).manifest.name,a)}s(A,"profileObject");export{d as liveLookups,A as profileObject,q as publishLookups,V as readerFor,$ as readerForNode,Y as readerForObject};
@@ -1 +1 @@
1
- var g=Object.defineProperty;var s=(t,n)=>g(t,"name",{value:n,configurable:!0});import{covers as u}from"@north-light/identity";import{identityOf as c}from"./index.js";import{objectPlace as $,runPlace as f,runsSeeEachOther as a}from"../../runs/confinement.js";const _={identity:c,sync:"runtime",canRead(t,n,o,e,r){if(n.class==="person"||e.length===0)return!0;if(n.app.rowid===o.rowid)return m(t,n,e);const i=c(t,o).external_id,l=s(d=>n.scopes!==null&&n.scopes.some(w=>u(w,d)),"held");return e[0].type==="node"?l(`crtr:objects:read:${i}`)||r.type==="document"&&r.kind==="report"&&l(`crtr:conversations:read:${i}`):l(y(t,i,"read",e))},canChange(t,n,o,e){if(n.class==="person")return!0;if(n.app.rowid===o.rowid)return e.length===0||m(t,n,e);if(e.length===0||e[0].type==="node")return!1;const r=y(t,c(t,o).external_id,"rw",e);return n.scopes!==null&&n.scopes.some(i=>u(i,r))}},p=new WeakMap;function m(t,n,o){if(n.class!=="node"&&n.class!=="object"||o.length===0||o[0].type!=="node")return!0;let e=p.get(n);if(e===void 0&&(e=n.class==="node"?f(n.node.id,t):$(n.object.rowid,t),p.set(n,e)),e===null)return!0;const r=f(o[0].id,t);return r===null?!e.isolated:a(e,r)}s(m,"runConfined");function y(t,n,o,e){const r=e[0];return r.type!=="profile"?`crtr:memory:${o}:${n}`:`crtr:memory:${o}:${n}:profile:${c(t,r).external_id}`}s(y,"memoryScope");export{_ as appSpace};
1
+ var g=Object.defineProperty;var s=(t,n)=>g(t,"name",{value:n,configurable:!0});import{covers as u}from"@north-light/identity";import{identityOf as c}from"./index.js";import{objectPlace as $,runPlace as p,runsSeeEachOther as a}from"../../runs/confinement.js";const j={identity:c,sync:"runtime",canRead(t,n,o,e,r){if(n.class==="person"||e.length===0)return!0;if(n.app.rowid===o.rowid)return m(t,n,e);const i=c(t,o).external_id,l=s(d=>n.scopes!==null&&n.scopes.some(w=>u(w,d)),"held");return e[0].type==="node"?l(`crtr:objects:read:${i}`)||r.type==="document"&&r.kind==="report"&&l(`crtr:conversations:read:${i}`):l(y(t,i,"read",e))},canChange(t,n,o,e){if(n.class==="person")return!0;if(n.app.rowid===o.rowid)return e.length===0||m(t,n,e);if(e.length===0||e[0].type==="node")return!1;const r=y(t,c(t,o).external_id,"rw",e);return n.scopes!==null&&n.scopes.some(i=>u(i,r))}},f=new WeakMap;function m(t,n,o){if(n.class==="person"||o.length===0||o[0].type!=="node"||n.class==="app"&&n.place===void 0)return!0;let e=f.get(n);if(e===void 0&&(e=n.class==="app"?n.place:n.class==="node"?p(n.node.id,t):$(n.object.rowid,t),f.set(n,e)),e===null)return!0;const r=p(o[0].id,t);return r===null?!e.isolated:a(e,r)}s(m,"runConfined");function y(t,n,o,e){const r=e[0];return r.type!=="profile"?`crtr:memory:${o}:${n}`:`crtr:memory:${o}:${n}:profile:${c(t,r).external_id}`}s(y,"memoryScope");export{j as appSpace};
@@ -52,6 +52,11 @@ export type Reader = {
52
52
  class: 'app';
53
53
  app: ObjectRow;
54
54
  scopes: readonly string[];
55
+ /** A cron command's run place (its origin run, and whether that run is isolated): it reads its own app's nodes under run confinement, as a node of that run does. Absent for the app's server. */
56
+ place?: {
57
+ run: string;
58
+ isolated: boolean;
59
+ };
55
60
  } | {
56
61
  class: 'node';
57
62
  node: ObjectRow;
@@ -19,5 +19,7 @@ export declare function runsSeeEachOther(a: RunPlace, b: RunPlace): boolean;
19
19
  /** Whether a node caller of `app` may reach `target` (a node id) under run confinement.
20
20
  * Confinement applies only within one app; cross-app reach stays with the scope rules. */
21
21
  export declare function nodeMayReach(callerNode: string, callerApp: string, targetNode: string, targetApp: string | null, db?: DatabaseSync): boolean;
22
+ /** The same question for a caller that has a place but no node (a cron command: its origin run). */
23
+ export declare function placeMayReach(caller: RunPlace, callerApp: string, targetNode: string, targetApp: string | null, db?: DatabaseSync): boolean;
22
24
  /** The node ids of one run (its root and every node carrying its `run_id`). */
23
25
  export declare function runNodeIds(run: string, db?: DatabaseSync): string[];
@@ -1,2 +1,2 @@
1
- var _=Object.defineProperty;var n=(e,r)=>_(e,"name",{value:r,configurable:!0});import{openDb as o,prepared as i}from"../canvas/db.js";function u(e,r=o()){const t=i(r,`SELECT n.run_id, n.isolated, r.isolated AS root_isolated FROM nodes n
2
- LEFT JOIN nodes r ON r.node_id = n.run_id WHERE n.node_id = ?`).get(e);if(!t){const d=i(r,"SELECT run_id FROM deleted_isolated_nodes WHERE node_id = ?").get(e);return d?{run:d.run_id,isolated:!0}:null}return{run:t.run_id??e,isolated:t.isolated===1||t.root_isolated===1}}n(u,"runPlace");function f(e,r=o()){const t=u(e,r);t?.isolated&&i(r,"INSERT OR REPLACE INTO deleted_isolated_nodes (node_id, run_id) VALUES (?, ?)").run(e,t.run)}n(f,"rememberDeletedIsolated");function R(e,r=o()){const t=i(r,"SELECT registered_by FROM custom_objects WHERE object = ?").get(e);return t?.registered_by?u(t.registered_by,r)??{run:t.registered_by,isolated:!1}:null}n(R,"objectPlace");function c(e,r){return e.run===r.run||!e.isolated&&!r.isolated}n(c,"runsSeeEachOther");function O(e,r,t,d,l=o()){if((d??"app:terminal")!==r)return!0;const s=u(e,l);if(!s)return!0;const a=u(t,l);return a===null?!s.isolated:c(s,a)}n(O,"nodeMayReach");function g(e,r=o()){return i(r,"SELECT node_id FROM nodes WHERE run_id = ? OR node_id = ?").all(e,e).map(t=>t.node_id)}n(g,"runNodeIds");export{O as nodeMayReach,R as objectPlace,f as rememberDeletedIsolated,g as runNodeIds,u as runPlace,c as runsSeeEachOther};
1
+ var a=Object.defineProperty;var n=(e,r)=>a(e,"name",{value:r,configurable:!0});import{openDb as i,prepared as u}from"../canvas/db.js";function l(e,r=i()){const t=u(r,`SELECT n.run_id, n.isolated, r.isolated AS root_isolated FROM nodes n
2
+ LEFT JOIN nodes r ON r.node_id = n.run_id WHERE n.node_id = ?`).get(e);if(!t){const o=u(r,"SELECT run_id FROM deleted_isolated_nodes WHERE node_id = ?").get(e);return o?{run:o.run_id,isolated:!0}:null}return{run:t.run_id??e,isolated:t.isolated===1||t.root_isolated===1}}n(l,"runPlace");function R(e,r=i()){const t=l(e,r);t?.isolated&&u(r,"INSERT OR REPLACE INTO deleted_isolated_nodes (node_id, run_id) VALUES (?, ?)").run(e,t.run)}n(R,"rememberDeletedIsolated");function f(e,r=i()){const t=u(r,"SELECT registered_by FROM custom_objects WHERE object = ?").get(e);return t?.registered_by?l(t.registered_by,r)??{run:t.registered_by,isolated:!1}:null}n(f,"objectPlace");function _(e,r){return e.run===r.run||!e.isolated&&!r.isolated}n(_,"runsSeeEachOther");function O(e,r,t,o,s=i()){const d=l(e,s);return d===null?!0:c(d,r,t,o,s)}n(O,"nodeMayReach");function c(e,r,t,o,s=i()){if((o??"app:terminal")!==r)return!0;const d=l(t,s);return d===null?!e.isolated:_(e,d)}n(c,"placeMayReach");function g(e,r=i()){return u(r,"SELECT node_id FROM nodes WHERE run_id = ? OR node_id = ?").all(e,e).map(t=>t.node_id)}n(g,"runNodeIds");export{O as nodeMayReach,f as objectPlace,c as placeMayReach,R as rememberDeletedIsolated,g as runNodeIds,l as runPlace,_ as runsSeeEachOther};
@@ -34,6 +34,10 @@ export declare function runAppCommand(options: RunCommandOnceOptions & {
34
34
  scopes: string[] | null;
35
35
  };
36
36
  folder?: string;
37
+ cron?: {
38
+ executionId: string;
39
+ anchorDir?: string;
40
+ };
37
41
  }): Promise<RunningCommand>;
38
42
  /** Start one admitted-PATH bash command in its own process group. The returned
39
43
  * handle exposes the pid immediately for callers that must record ownership
@@ -1 +1 @@
1
- var H=Object.defineProperty;var a=(e,u)=>H(e,"name",{value:u,configurable:!0});import{spawn as b}from"node:child_process";import{createHash as k}from"node:crypto";import{captureTeardownSnapshot as R,isAnyPidAlive as x,killProcessTreePids as I}from"../canvas/pid.js";import{CRON_DEATH_POLL_MS as B,CRON_KILL_GRACE_MS as G,newHeadCapture as L}from"../../daemon/cron/capture.js";import{resolveAdmittedHostCommand as M}from"./spawn-env.js";import{appProcessSandboxed as K,launchAppProcess as N}from"../spaces/process.js";async function V(e){if(!K()){if(e.isolated)throw new Error("isolated cron requires the root launcher");return q(e)}const u=Date.now(),n=await N({nodeId:e.nodeId,app:e.app,isolated:e.isolated,...e.principal===void 0?{}:{principal:e.principal},...e.folder===void 0?{}:{folder:e.folder},command:[M("bash",e.env,e.cwd),"-c",e.command],cwd:e.cwd,env:e.env,stdoutBytes:e.headBytes,stderrBytes:e.headBytes});let m=!1,i;const p=a(()=>{n.stop().catch(t=>{console.error(`app command ${n.pid} could not be stopped:`,t)})},"terminate");i=setTimeout(()=>{m=!0,p()},e.timeoutMs),i.unref?.();const o=n.completion.then(t=>({exitCode:t.exitCode,signal:t.signal,stdout:t.stdout,stderr:t.stderr,stdoutTruncated:t.stdoutTruncated,stderrTruncated:t.stderrTruncated,stdoutSha256:t.stdoutSha256,timedOut:m,durationMs:Date.now()-u,processError:null})).finally(()=>{i&&clearTimeout(i)});return{pid:n.pid,completion:o,terminate:p}}a(V,"runAppCommand");function q({command:e,cwd:u,env:n,timeoutMs:m,headBytes:i}){const p=Date.now(),o=b(M("bash",n,u),["-c",e],{cwd:u,env:n,detached:!0,stdio:["ignore","pipe","pipe"]}),t=o.pid??null,h=L(i),T=L(i),P=k("sha256");o.stdout?.on("data",d=>{P.update(d),h.take(d)}),o.stderr?.on("data",d=>T.take(d));let g=!1,w=null,s,v=null,c=!1,y=!1,l,f,C,r,D;const O=new Promise(d=>{D=d}),S=a(()=>{c||s===void 0||(c=!0,l!==void 0&&clearTimeout(l),f!==void 0&&clearTimeout(f),C!==void 0&&clearTimeout(C),D({exitCode:s,signal:v,stdout:h.decode(),stdoutTruncated:h.truncated(),stderr:T.decode(),stderrTruncated:T.truncated(),timedOut:g,durationMs:Date.now()-p,stdoutSha256:P.digest("hex"),processError:w}))},"finish"),A=a(()=>{if(r===void 0||!x(r.tree)){S();return}C=setTimeout(A,B)},"awaitTeardownDeath"),E=a(()=>{t===null||c||(r??=R(t,null),I(r.tree,"SIGTERM",r.identities),f===void 0&&(f=setTimeout(()=>{x(r.tree)&&I(r.tree,"SIGKILL",r.identities),y=!0,s!==void 0&&A()},G),f.unref?.()))},"terminate");return o.on("error",d=>{w===null&&(w=d),t===null&&(s=null,S())}),o.on("close",(d,_)=>{if(!c){if(s=d,v=_,l!==void 0&&clearTimeout(l),r!==void 0&&x(r.tree)){y&&A();return}S()}}),t!==null&&(l=setTimeout(()=>{c||(g=!0,E())},m),l.unref?.()),{pid:t,completion:O,terminate:E}}a(q,"runCommandOnce");export{V as runAppCommand,q as runCommandOnce};
1
+ var H=Object.defineProperty;var a=(e,u)=>H(e,"name",{value:u,configurable:!0});import{spawn as b}from"node:child_process";import{createHash as k}from"node:crypto";import{captureTeardownSnapshot as R,isAnyPidAlive as x,killProcessTreePids as I}from"../canvas/pid.js";import{CRON_DEATH_POLL_MS as B,CRON_KILL_GRACE_MS as G,newHeadCapture as L}from"../../daemon/cron/capture.js";import{resolveAdmittedHostCommand as M}from"./spawn-env.js";import{appProcessSandboxed as K,launchAppProcess as N}from"../spaces/process.js";async function V(e){if(!K()){if(e.isolated)throw new Error("isolated cron requires the root launcher");return q(e)}const u=Date.now(),n=await N({nodeId:e.nodeId,app:e.app,isolated:e.isolated,...e.principal===void 0?{}:{principal:e.principal},...e.folder===void 0?{}:{folder:e.folder},...e.cron===void 0?{}:{cron:e.cron},command:[M("bash",e.env,e.cwd),"-c",e.command],cwd:e.cwd,env:e.env,stdoutBytes:e.headBytes,stderrBytes:e.headBytes});let m=!1,i;const p=a(()=>{n.stop().catch(t=>{console.error(`app command ${n.pid} could not be stopped:`,t)})},"terminate");i=setTimeout(()=>{m=!0,p()},e.timeoutMs),i.unref?.();const o=n.completion.then(t=>({exitCode:t.exitCode,signal:t.signal,stdout:t.stdout,stderr:t.stderr,stdoutTruncated:t.stdoutTruncated,stderrTruncated:t.stderrTruncated,stdoutSha256:t.stdoutSha256,timedOut:m,durationMs:Date.now()-u,processError:null})).finally(()=>{i&&clearTimeout(i)});return{pid:n.pid,completion:o,terminate:p}}a(V,"runAppCommand");function q({command:e,cwd:u,env:n,timeoutMs:m,headBytes:i}){const p=Date.now(),o=b(M("bash",n,u),["-c",e],{cwd:u,env:n,detached:!0,stdio:["ignore","pipe","pipe"]}),t=o.pid??null,h=L(i),T=L(i),P=k("sha256");o.stdout?.on("data",d=>{P.update(d),h.take(d)}),o.stderr?.on("data",d=>T.take(d));let g=!1,w=null,s,v=null,c=!1,y=!1,l,f,C,r,D;const O=new Promise(d=>{D=d}),S=a(()=>{c||s===void 0||(c=!0,l!==void 0&&clearTimeout(l),f!==void 0&&clearTimeout(f),C!==void 0&&clearTimeout(C),D({exitCode:s,signal:v,stdout:h.decode(),stdoutTruncated:h.truncated(),stderr:T.decode(),stderrTruncated:T.truncated(),timedOut:g,durationMs:Date.now()-p,stdoutSha256:P.digest("hex"),processError:w}))},"finish"),A=a(()=>{if(r===void 0||!x(r.tree)){S();return}C=setTimeout(A,B)},"awaitTeardownDeath"),E=a(()=>{t===null||c||(r??=R(t,null),I(r.tree,"SIGTERM",r.identities),f===void 0&&(f=setTimeout(()=>{x(r.tree)&&I(r.tree,"SIGKILL",r.identities),y=!0,s!==void 0&&A()},G),f.unref?.()))},"terminate");return o.on("error",d=>{w===null&&(w=d),t===null&&(s=null,S())}),o.on("close",(d,_)=>{if(!c){if(s=d,v=_,l!==void 0&&clearTimeout(l),r!==void 0&&x(r.tree)){y&&A();return}S()}}),t!==null&&(l=setTimeout(()=>{c||(g=!0,E())},m),l.unref?.()),{pid:t,completion:O,terminate:E}}a(q,"runCommandOnce");export{V as runAppCommand,q as runCommandOnce};
@@ -15,6 +15,11 @@ export interface LaunchNode {
15
15
  /** The folder of a process that has no node but owns one (an isolated cron's `<app space>/crons/<id>`): it takes the place of the node agent folder. */
16
16
  folder?: string;
17
17
  cwd?: string;
18
+ /** A cron execution (launch id `cron-<execution_id>`): its only daemon path is `cli.sock` in its own run folder
19
+ * `nodeRunDir(nodeId)`, so neither `api.sock` nor `store.sock` is mounted. `anchorDir` is an anchored cron's anchor agent folder. */
20
+ cron?: {
21
+ anchorDir?: string;
22
+ };
18
23
  /** Hook code alone may be mounted from the installed plugin root, read-only. */
19
24
  pluginRoot?: string;
20
25
  /** Missing granted directories are omitted and reported to the daemon. */
@@ -1 +1 @@
1
- var y=Object.defineProperty;var l=(t,r)=>y(t,"name",{value:r,configurable:!0});import{lstatSync as D}from"node:fs";import{join as a,relative as f,sep as c}from"node:path";import{appSpace as w,appsRoot as S,runtimeRoot as h,socketDir as d,userFilesRoot as R}from"../layout.js";import{nodeAgentDir as I,nodeRunDir as k}from"../canvas/paths.js";import{uidForApp as A}from"./users.js";import{permissionsForRun as E}from"./permissions.js";function x(t){const r=[];for(const e of t.otherFiles){const i=e.holder==="user"?R():w(e.holder).files;r.push({source:e.path==="."?i:a(i,e.path),writable:e.writable})}for(const e of t.otherNodes)r.push({source:w(e).nodes,writable:!1});return r}l(x,"grantedMounts");function m(t,r){const e=f(t,r);return e===""||e!==".."&&!e.startsWith(`..${c}`)&&!e.startsWith(c)}l(m,"beneath");function M(t,r){return m(R(),r)||m(S(),r)&&!m(w(t).root,r)}l(M,"isGrantedSource");function j(t,r){return r.some(e=>m(e.source,t.source)&&(e.writable||!t.writable))}l(j,"mountAllowed");function z(t,r){try{const e=x(E(t.app,t.sub,t.grantId,t.scopes));return r.mounts.every(i=>!M(t.app,i.source)||j(i,e))}catch{return!1}}l(z,"launchStillGranted");function B(t){const r=w(t.app),e=E(t.app,t.sub,t.grantId,t.scopes);if(t.isolated&&t.withoutNode&&t.folder===void 0)throw new Error("isolated launch requires a node");const i=t.folder??(t.isolated?I(t.nodeId,t.app):void 0);if(i&&t.cwd!==void 0&&!m(i,t.cwd))throw new Error("isolated cwd must be inside the node folder");const p=new Map,s=l((o,n,u=!1,W=!1)=>{if(u)try{D(o)}catch(b){if(b.code!=="ENOENT")throw b;W&&t.auditMissing(o);return}p.set(o,n||(p.get(o)??!1))},"add");for(const o of["/usr","/bin","/lib","/lib64","/opt/crouter","/etc/passwd","/etc/group","/etc/resolv.conf","/etc/hosts","/etc/ssl","/etc/ca-certificates"])s(o,!1,!0);t.withoutNode||s(I(t.nodeId,t.app),!0),t.folder!==void 0&&s(t.folder,!0),t.isolated||s(r.home,!0),t.isolated||s(r.files,!0),s(r.cache,!0,!0),t.withoutNode||s(k(t.nodeId),!0),s(a(d(),"api.sock"),!0),s(a(d(),"store.sock"),!0);for(const o of x(e))s(o.source,o.writable,!0,!0);if(t.pluginRoot!==void 0){const o=l(u=>t.pluginRoot===u||f(u,t.pluginRoot)!==".."&&!f(u,t.pluginRoot).startsWith(`..${c}`),"within"),n=a(h(),"plugins");if([...p].some(([u])=>o(u))){if(p.get(t.pluginRoot)===!0)throw new Error("hook root cannot also be writable");s(t.pluginRoot,!1)}else if(o(n)&&t.pluginRoot!==n)s(t.pluginRoot,!1);else if(o("/opt/crouter"))s(t.pluginRoot,!1);else throw new Error("hook root is not readable with this app\u2019s mounts")}const N=t.cwd??i??r.files,g=[...p].sort(([o],[n])=>o.split(c).length-n.split(c).length||o.localeCompare(n)),v=a(h(),"nodes",t.nodeId,"job","broker.log"),F=process.platform!=="linux"?[k(t.nodeId),a(d(),"api.sock"),a(d(),"store.sock")]:[];if(g.some(([o])=>o!==t.pluginRoot&&!F.includes(o)&&(o===h()||!f(h(),o).startsWith("..")&&!f(h(),o).startsWith(c))))throw new Error("runtime records cannot be mounted");return{nodeId:t.nodeId,uid:A(t.app),mounts:g.map(([o,n])=>({source:o,writable:n})),cwd:N,env:t.env,command:t.command,log:v,...t.isolated?{network:"none"}:{}}}l(B,"compileLaunch");export{B as compileLaunch,x as grantedMounts,M as isGrantedSource,z as launchStillGranted,j as mountAllowed};
1
+ var W=Object.defineProperty;var l=(t,o)=>W(t,"name",{value:o,configurable:!0});import{lstatSync as y}from"node:fs";import{join as c,relative as f,sep as a}from"node:path";import{appSpace as m,appsRoot as S,runtimeRoot as h,socketDir as w,userFilesRoot as R}from"../layout.js";import{nodeAgentDir as I,nodeRunDir as k}from"../canvas/paths.js";import{uidForApp as A}from"./users.js";import{permissionsForRun as E}from"./permissions.js";function x(t){const o=[];for(const e of t.otherFiles){const s=e.holder==="user"?R():m(e.holder).files;o.push({source:e.path==="."?s:c(s,e.path),writable:e.writable})}for(const e of t.otherNodes)o.push({source:m(e).nodes,writable:!1});return o}l(x,"grantedMounts");function d(t,o){const e=f(t,o);return e===""||e!==".."&&!e.startsWith(`..${a}`)&&!e.startsWith(a)}l(d,"beneath");function M(t,o){return d(R(),o)||d(S(),o)&&!d(m(t).root,o)}l(M,"isGrantedSource");function j(t,o){return o.some(e=>d(e.source,t.source)&&(e.writable||!t.writable))}l(j,"mountAllowed");function z(t,o){try{const e=x(E(t.app,t.sub,t.grantId,t.scopes));return o.mounts.every(s=>!M(t.app,s.source)||j(s,e))}catch{return!1}}l(z,"launchStillGranted");function B(t){const o=m(t.app),e=E(t.app,t.sub,t.grantId,t.scopes);if(t.isolated&&t.withoutNode&&t.folder===void 0)throw new Error("isolated launch requires a node");const s=t.folder??(t.isolated?I(t.nodeId,t.app):void 0);if(s&&t.cwd!==void 0&&!d(s,t.cwd))throw new Error("isolated cwd must be inside the node folder");const p=new Map,i=l((r,n,u=!1,F=!1)=>{if(u)try{y(r)}catch(b){if(b.code!=="ENOENT")throw b;F&&t.auditMissing(r);return}p.set(r,n||(p.get(r)??!1))},"add");for(const r of["/usr","/bin","/lib","/lib64","/opt/crouter","/etc/passwd","/etc/group","/etc/resolv.conf","/etc/hosts","/etc/ssl","/etc/ca-certificates"])i(r,!1,!0);t.withoutNode||i(I(t.nodeId,t.app),!0),t.folder!==void 0&&i(t.folder,!0),t.cron?.anchorDir!==void 0&&i(t.cron.anchorDir,!0),t.isolated||i(o.home,!0),t.isolated||i(o.files,!0),i(o.cache,!0,!0),(!t.withoutNode||t.cron!==void 0)&&i(k(t.nodeId),!0),t.cron===void 0&&(i(c(w(),"api.sock"),!0),i(c(w(),"store.sock"),!0));for(const r of x(e))i(r.source,r.writable,!0,!0);if(t.pluginRoot!==void 0){const r=l(u=>t.pluginRoot===u||f(u,t.pluginRoot)!==".."&&!f(u,t.pluginRoot).startsWith(`..${a}`),"within"),n=c(h(),"plugins");if([...p].some(([u])=>r(u))){if(p.get(t.pluginRoot)===!0)throw new Error("hook root cannot also be writable");i(t.pluginRoot,!1)}else if(r(n)&&t.pluginRoot!==n)i(t.pluginRoot,!1);else if(r("/opt/crouter"))i(t.pluginRoot,!1);else throw new Error("hook root is not readable with this app\u2019s mounts")}const D=t.cwd??s??o.files,g=[...p].sort(([r],[n])=>r.split(a).length-n.split(a).length||r.localeCompare(n)),N=c(h(),"nodes",t.nodeId,"job","broker.log"),v=process.platform!=="linux"?[k(t.nodeId),c(w(),"api.sock"),c(w(),"store.sock")]:[];if(g.some(([r])=>r!==t.pluginRoot&&!v.includes(r)&&(r===h()||!f(h(),r).startsWith("..")&&!f(h(),r).startsWith(a))))throw new Error("runtime records cannot be mounted");return{nodeId:t.nodeId,uid:A(t.app),mounts:g.map(([r,n])=>({source:r,writable:n})),cwd:D,env:t.env,command:t.command,log:N,...t.isolated?{network:"none"}:{}}}l(B,"compileLaunch");export{B as compileLaunch,x as grantedMounts,M as isGrantedSource,z as launchStillGranted,j as mountAllowed};
@@ -1,4 +1,6 @@
1
1
  import { type CapturedExit } from '../../launcher/spec.js';
2
+ /** The launcher's node id for one cron execution; its run folder is `nodeRunDir(<this>)`. */
3
+ export declare function cronLaunchId(executionId: string): string;
2
4
  export interface AppProcess {
3
5
  pid: number;
4
6
  completion: Promise<CapturedExit>;
@@ -15,6 +17,12 @@ export interface AppProcessOptions {
15
17
  grantId: string | null;
16
18
  scopes: string[] | null;
17
19
  };
20
+ /** A cron execution: launch id `cron-<executionId>`, no node, and no `api.sock`/`store.sock` — its only API path is
21
+ * its own execution folder's `cli.sock`. `anchorDir` is an anchored cron's anchor agent folder (rw). Needs `principal`. */
22
+ cron?: {
23
+ executionId: string;
24
+ anchorDir?: string;
25
+ };
18
26
  /** The folder an isolated process without a node runs in (an isolated cron's own folder). */
19
27
  folder?: string;
20
28
  command: string[];
@@ -1 +1 @@
1
- var N=Object.defineProperty;var i=(e,r)=>N(e,"name",{value:r,configurable:!0});import{existsSync as x}from"node:fs";import{randomUUID as $}from"node:crypto";import{join as w}from"node:path";import{getNode as _}from"../canvas/canvas.js";import{nodeAgentDir as D,nodeHome as O,nodeRunDir as k}from"../canvas/paths.js";import{appSpace as K}from"../layout.js";import{activeGrant as h,activeGrantForApp as R,grantFor as I,grantsGeneration as S}from"../grants/mirror.js";import{readConfig as B}from"../config.js";import{emitEvent as T}from"../events/emit.js";import{ensureAppSpace as F}from"./space.js";import{compileLaunch as y,launchStillGranted as H}from"./mounts.js";import{pinSandboxRuntimeHome as M}from"./runtime-home.js";import{sandboxCacheEnv as j}from"./cache-env.js";import{launcherRequest as b}from"../../launcher/client.js";import{LAUNCHER_SOCKET as A}from"../../launcher/spec.js";function ue(){if(process.platform!=="linux")return!1;if(x(A))return!0;throw new Error("root launcher socket is unavailable; refusing an unsandboxed app process")}i(ue,"appProcessSandboxed");function ie(){return G??(process.platform==="linux"&&x(A))}i(ie,"appProcessesSandboxed");let G;function ae(e){G=e}i(ae,"__setAppProcessesSandboxedForTest");function ce(e,r,n,o=!1,t,d){const s=n===void 0?null:_(n),a=d!==void 0?d.grantId?h(d.grantId):I(e,d.sub):s?null:R(e);if(d!==void 0?!a&&!(d.grantId===null&&e==="app:terminal"):!s&&!a)throw new Error(`no active grant for ${e}`);return y({nodeId:n??"proc-cwd-check",app:e,...d!==void 0?{sub:d.sub,grantId:a?.id??null,scopes:d.scopes}:{sub:s?s.sub??"user:local":a.sub,grantId:s?s.grant_id??null:a.id,scopes:s?.scopes??null},command:["true"],env:{},...n===void 0?{withoutNode:!0}:{},...t===void 0?{}:{folder:t},isolated:s?.isolated===!0||o,auditMissing:i(()=>{},"auditMissing")}).mounts.some(({source:c})=>r===c||r.startsWith(c==="/"?"/":`${c}/`))}i(ce,"cwdIsMounted");async function P(e,r="SIGKILL",n){if(!("ok"in await b({operation:"stop",pid:e,signal:r,...n===void 0?{}:{nodeId:n}})))throw new Error("launcher did not stop process")}i(P,"stopAppProcess");function q(e,r,n,o,t){return o?t??D(e,r):n}i(q,"appProcessCwd");async function le(e){const r=e.nodeId===void 0?null:_(e.nodeId);if(e.nodeId!==void 0&&!r)throw new Error(`missing node ${e.nodeId}`);const n=r?.grantee??e.app??"app:terminal";if(e.app!==void 0&&e.app!==n)throw new Error("app does not match node");const o=e.principal;let t=null;if(o!==void 0){if(t=o.grantId?h(o.grantId):I(n,o.sub),!(t===null&&o.grantId===null&&n==="app:terminal"&&!B("user").runtime?.issuer)&&(!t||t.grantee!==n||t.sub!==o.sub))throw new Error(`no active grant for ${n}`)}else if(r){if(n!=="app:terminal"&&!(r.grant_id?h(r.grant_id):I(n,r.sub??"user:local")))throw new Error(`no active grant for ${n}`)}else if(t=R(n),!t)throw new Error(`no active grant for ${n}`);await F(n);const d=e.nodeId??`proc-${$()}`,s=r?.isolated===!0||e.isolated===!0;if(s&&!r&&e.folder===void 0)throw new Error("isolated process requires a live node");const a=K(n),m=q(d,n,e.cwd,s,e.folder),c=r?O(d,n,s):s&&e.folder!==void 0?w(e.folder,"home"):a.home,C=M(Object.fromEntries(Object.entries({...e.env,...process.platform==="linux"?j(n):{},HOME:c,PI_CODING_AGENT_DIR:w(c,".pi","agent"),PWD:m,...e.nodeId===void 0?{}:{CRTR_API_SOCKET:w(k(d),"cli.sock")}}).filter(u=>u[1]!==void 0))),E=o!==void 0?{nodeId:d,app:n,sub:o.sub,grantId:t?.id??null,scopes:o.scopes}:{nodeId:d,app:n,sub:r?r.sub??"user:local":t.sub,grantId:r?r.grant_id??null:t.id,scopes:r?.scopes??null},L=i(()=>{const u=y({...E,command:e.command,env:C,...e.nodeId===void 0?{withoutNode:!0}:{},isolated:s,cwd:m,...e.folder===void 0?{}:{folder:e.folder},...e.pluginRoot===void 0?{}:{pluginRoot:e.pluginRoot},auditMissing:i(g=>T({level:"warn",event:"process.mount.missing",...e.nodeId===void 0?{}:{node_id:e.nodeId},fields:{source:g}}),"auditMissing")});if(e.pluginRoot!==void 0&&(u.pluginRoot=e.pluginRoot),e.capture===!1){if(e.nodeId===void 0||e.stdin!==void 0)throw new Error("uncaptured process requires a node and no stdin")}else u.capture={stdoutBytes:e.stdoutBytes??256*1024,stderrBytes:e.stderrBytes??256*1024,...e.stderrTail?{stderrTail:!0}:{},...e.stdin===void 0?{}:{stdin:e.stdin}};return u},"compile");let l;for(let u=0;u<2&&l===void 0;u++){const g=S(),v=L(),f=await b({operation:"launch",spec:v});if(!("pid"in f))throw new Error("launcher did not return a process id");if(S()===g||H(E,v)){l=f.pid;break}T({level:"info",event:"process.launch.regranted",...e.nodeId===void 0?{}:{node_id:e.nodeId},fields:{pid:f.pid}}),await P(f.pid,"SIGKILL").catch(()=>{})}if(l===void 0)throw new Error("grant changed during every launch attempt");const p=l;return{pid:p,get completion(){return b({operation:"wait",pid:p}).then(u=>{if(!("result"in u))throw new Error("launcher did not return process completion");return u.result})},stop:i((u="SIGKILL")=>P(p,u,e.nodeId),"stop")}}i(le,"launchAppProcess");export{ae as __setAppProcessesSandboxedForTest,q as appProcessCwd,ue as appProcessSandboxed,ie as appProcessesSandboxed,ce as cwdIsMounted,le as launchAppProcess,P as stopAppProcess};
1
+ var C=Object.defineProperty;var i=(e,r)=>C(e,"name",{value:r,configurable:!0});import{existsSync as v}from"node:fs";import{randomUUID as $}from"node:crypto";import{join as h}from"node:path";import{getNode as _}from"../canvas/canvas.js";import{nodeAgentDir as N,nodeHome as O,nodeRunDir as k}from"../canvas/paths.js";import{appSpace as K}from"../layout.js";import{activeGrant as w,activeGrantForApp as R,grantFor as I,grantsGeneration as S}from"../grants/mirror.js";import{readConfig as B}from"../config.js";import{emitEvent as T}from"../events/emit.js";import{ensureAppSpace as F}from"./space.js";import{compileLaunch as y,launchStillGranted as H}from"./mounts.js";import{pinSandboxRuntimeHome as M}from"./runtime-home.js";import{sandboxCacheEnv as j}from"./cache-env.js";import{launcherRequest as b}from"../../launcher/client.js";import{LAUNCHER_SOCKET as A}from"../../launcher/spec.js";function q(e){return`cron-${e}`}i(q,"cronLaunchId");function ie(){if(process.platform!=="linux")return!1;if(v(A))return!0;throw new Error("root launcher socket is unavailable; refusing an unsandboxed app process")}i(ie,"appProcessSandboxed");function se(){return D??(process.platform==="linux"&&v(A))}i(se,"appProcessesSandboxed");let D;function ce(e){D=e}i(ce,"__setAppProcessesSandboxedForTest");function fe(e,r,n,o=!1,t,d){const u=n===void 0?null:_(n),s=d!==void 0?d.grantId?w(d.grantId):I(e,d.sub):u?null:R(e);if(d!==void 0?!s&&!(d.grantId===null&&e==="app:terminal"):!u&&!s)throw new Error(`no active grant for ${e}`);return y({nodeId:n??"proc-cwd-check",app:e,...d!==void 0?{sub:d.sub,grantId:s?.id??null,scopes:d.scopes}:{sub:u?u.sub??"user:local":s.sub,grantId:u?u.grant_id??null:s.id,scopes:u?.scopes??null},command:["true"],env:{},...n===void 0?{withoutNode:!0}:{},...t===void 0?{}:{folder:t},isolated:u?.isolated===!0||o,auditMissing:i(()=>{},"auditMissing")}).mounts.some(({source:c})=>r===c||r.startsWith(c==="/"?"/":`${c}/`))}i(fe,"cwdIsMounted");async function G(e,r="SIGKILL",n){if(!("ok"in await b({operation:"stop",pid:e,signal:r,...n===void 0?{}:{nodeId:n}})))throw new Error("launcher did not stop process")}i(G,"stopAppProcess");function U(e,r,n,o,t){return o?t??N(e,r):n}i(U,"appProcessCwd");async function le(e){if(e.cron!==void 0&&(e.nodeId!==void 0||e.principal===void 0))throw new Error("a cron launch has a stored principal and no node");const r=e.nodeId===void 0?null:_(e.nodeId);if(e.nodeId!==void 0&&!r)throw new Error(`missing node ${e.nodeId}`);const n=r?.grantee??e.app??"app:terminal";if(e.app!==void 0&&e.app!==n)throw new Error("app does not match node");const o=e.principal;let t=null;if(o!==void 0){if(t=o.grantId?w(o.grantId):I(n,o.sub),!(t===null&&o.grantId===null&&n==="app:terminal"&&!B("user").runtime?.issuer)&&(!t||t.grantee!==n||t.sub!==o.sub))throw new Error(`no active grant for ${n}`)}else if(r){if(n!=="app:terminal"&&!(r.grant_id?w(r.grant_id):I(n,r.sub??"user:local")))throw new Error(`no active grant for ${n}`)}else if(t=R(n),!t)throw new Error(`no active grant for ${n}`);await F(n);const d=e.cron===void 0?e.nodeId??`proc-${$()}`:q(e.cron.executionId),u=r?.isolated===!0||e.isolated===!0;if(u&&!r&&e.folder===void 0)throw new Error("isolated process requires a live node");const s=K(n),m=U(d,n,e.cwd,u,e.folder),c=r?O(d,n,u):u&&e.folder!==void 0?h(e.folder,"home"):s.home,L=M(Object.fromEntries(Object.entries({...e.env,...process.platform==="linux"?j(n):{},HOME:c,PI_CODING_AGENT_DIR:h(c,".pi","agent"),PWD:m,...e.nodeId===void 0&&e.cron===void 0?{}:{CRTR_API_SOCKET:h(k(d),"cli.sock")}}).filter(a=>a[1]!==void 0))),E=o!==void 0?{nodeId:d,app:n,sub:o.sub,grantId:t?.id??null,scopes:o.scopes}:{nodeId:d,app:n,sub:r?r.sub??"user:local":t.sub,grantId:r?r.grant_id??null:t.id,scopes:r?.scopes??null},P=i(()=>{const a=y({...E,command:e.command,env:L,...e.nodeId===void 0?{withoutNode:!0}:{},isolated:u,cwd:m,...e.folder===void 0?{}:{folder:e.folder},...e.cron===void 0?{}:{cron:e.cron.anchorDir===void 0?{}:{anchorDir:e.cron.anchorDir}},...e.pluginRoot===void 0?{}:{pluginRoot:e.pluginRoot},auditMissing:i(g=>T({level:"warn",event:"process.mount.missing",...e.nodeId===void 0?{}:{node_id:e.nodeId},fields:{source:g}}),"auditMissing")});if(e.pluginRoot!==void 0&&(a.pluginRoot=e.pluginRoot),e.capture===!1){if(e.nodeId===void 0||e.stdin!==void 0)throw new Error("uncaptured process requires a node and no stdin")}else a.capture={stdoutBytes:e.stdoutBytes??256*1024,stderrBytes:e.stderrBytes??256*1024,...e.stderrTail?{stderrTail:!0}:{},...e.stdin===void 0?{}:{stdin:e.stdin}};return a},"compile");let f;for(let a=0;a<2&&f===void 0;a++){const g=S(),x=P(),l=await b({operation:"launch",spec:x});if(!("pid"in l))throw new Error("launcher did not return a process id");if(S()===g||H(E,x)){f=l.pid;break}T({level:"info",event:"process.launch.regranted",...e.nodeId===void 0?{}:{node_id:e.nodeId},fields:{pid:l.pid}}),await G(l.pid,"SIGKILL").catch(()=>{})}if(f===void 0)throw new Error("grant changed during every launch attempt");const p=f;return{pid:p,get completion(){return b({operation:"wait",pid:p}).then(a=>{if(!("result"in a))throw new Error("launcher did not return process completion");return a.result})},stop:i((a="SIGKILL")=>G(p,a,e.cron===void 0?e.nodeId:d),"stop")}}i(le,"launchAppProcess");export{ce as __setAppProcessesSandboxedForTest,U as appProcessCwd,ie as appProcessSandboxed,se as appProcessesSandboxed,q as cronLaunchId,fe as cwdIsMounted,le as launchAppProcess,G as stopAppProcess};
@@ -1 +1 @@
1
- var Q=Object.defineProperty;var t=(e,n)=>Q(e,"name",{value:n,configurable:!0});import{randomUUID as V}from"node:crypto";import{relative as X,resolve as W,sep as z}from"node:path";import{ApiError as c}from"../../../api/errors.js";import{armCron as nn,cancelCron as en,getCron as v,listCrons as x,listCronRuns as G,pokeHeldCrons as rn,setCronState as B}from"../../../core/canvas/crons.js";import{getNode as _}from"../../../core/canvas/canvas.js";import{appSpace as on,cronFolder as tn}from"../../../core/layout.js";import{appProcessesSandboxed as sn,cwdIsMounted as an}from"../../../core/spaces/process.js";import{activeGrant as cn,activeGrantForApp as ln,grantFor as dn,terminalGrant as un}from"../../../core/grants/mirror.js";import{readConfig as pn}from"../../../core/config.js";import{assertScope as F}from"../../../core/scopes.js";import{InputError as s}from"../../../core/io.js";import{nextSlotAfter as fn}from"../../../core/wake.js";import{executeCron as _n,markSelfCanceledCronRun as hn}from"../../cron-run.js";import{toCronDTO as y,toCronRunDTO as L}from"../map.js";import{assertCeiling as mn,canReadNode as gn}from"../router.js";import{requireUsableProfile as wn}from"./profiles.js";function P(e,n){const r=e[n];if(typeof r!="string"||r.trim()==="")throw new s({error:"missing_field",message:`cron arm request requires a non-empty "${n}"`,field:n,next:`Send a non-empty string "${n}".`});return r}t(P,"requireString");function $(e,n,r,o){if(e==null)return o;if(typeof e=="string"&&r.includes(e))return e;throw new s({error:`bad_${n}`,message:`"${String(e)}" is not a valid ${n}`,field:n,next:`Use one of: ${r.join(", ")}.`})}t($,"requireEnum");function bn(e){if(e==="human")return JSON.stringify({human:!0});if(e.startsWith("node:")){const n=e.slice(5).trim();if(n==="")throw new s({error:"bad_sink",message:"node sink requires a node id",field:"sink",next:'Use sink "node:<node-id>".'});if(_(n)===null)throw new s({error:"sink_node_not_found",message:`sink node ${n} does not exist on the canvas`,field:"sink",next:"Name a live node id, or use spawn:<kind> for a delivery that outlives any one node."});return JSON.stringify({node:n})}if(e.startsWith("spawn:")){const n=e.slice(6).trim();if(n==="")throw new s({error:"bad_sink",message:"spawn sink requires a node kind",field:"sink",next:'Use sink "spawn:<kind>" (e.g. spawn:general).'});return JSON.stringify({spawn:n})}throw new s({error:"bad_sink",message:`"${e}" is not a valid sink`,field:"sink",next:"Use node:<node-id>, spawn:<kind>, or human."})}t(bn,"parseSinkSpec");const vn=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;function Z(e,n){if(!vn.test(e)||Number.isNaN(Date.parse(e)))throw new s({error:`bad_${n}`,message:`"${e}" is not a UTC ISO instant`,field:n,next:'Send a resolved UTC instant like "2026-06-07T09:00:00.000Z" (the client resolves the time grammar).'});return e}t(Z,"requireUtcIso");function yn(e){try{fn(e,new Date)}catch(n){throw new s({error:"bad_recur",message:`recur is not advanceable: ${n instanceof Error?n.message:String(n)}`,field:"recur",next:'Send recur JSON the daemon can advance: {"every":"6h"} or {"cron":"0 9 * * *","tz":"America/New_York"}.'})}return e}t(yn,"requireRecur");function kn(e){let n;try{n=JSON.parse(e)}catch{throw new s({error:"bad_env_json",message:"env_json is not valid JSON",field:"env_json",next:'Send a JSON object of string values, e.g. {"API_BASE":"https://x"}.'})}if(typeof n!="object"||n===null||Array.isArray(n))throw new s({error:"bad_env_json",message:"env_json must be a JSON object",field:"env_json",next:'Send a JSON object of string values, e.g. {"API_BASE":"https://x"}.'});for(const[r,o]of Object.entries(n))if(typeof o!="string")throw new s({error:"bad_env_json",message:`env_json value for "${r}" is not a string`,field:"env_json",next:"Every env value must be a string."});return e}t(kn,"requireEnvJson");function A(e){const n=e.query.get("profile");return n!==null&&n.trim()!==""?n.trim():null}t(A,"callerProfile");function H(e,n){return n===null||e.scope==="global"||e.profile===n}t(H,"inScope");function M(e){return e.principal.class==="person"?null:e.principal.app}t(M,"callerApp");function E(e,n){return gn(e.principal,n.grantee,n.created_by??void 0)}t(E,"visibleTo");function Sn(e){return typeof e.anchor_node=="string"&&e.anchor_node!==""?e.anchor_node:null}t(Sn,"anchorNodeOf");function $n(e,n,r,o,a,u){const l=typeof e.cwd=="string"&&e.cwd.trim()!==""?W(e.cwd):null;if(!sn()){if(l===null)throw new s({error:"missing_field",message:'cron arm request requires a non-empty "cwd"',field:"cwd",next:'Send a non-empty string "cwd".'});return l}const p=o!==null&&_(o)!==null?o:void 0,d=t(I=>an(n,I,p,a,void 0,u),"reaches");if(l!==null&&d(l))return l;const i=on(n).files;if(l!==null&&e.cwd_is_default!==!0)throw new c(400,"invalid_request",`cwd ${l} is not inside a directory this app's cron runs can see, so every run would fail to start; use a directory under ${i}`,{cwd:l,default:i},void 0,{param:"cwd"});const h=r===null?null:_(r)?.cwd??null;return h!==null&&d(h)?h:i}t($n,"resolveArmedCwd");function On(e,n,r){const o=e.principal;if(o.class==="person"){const i=un();return{sub:i?.sub??"user:local",grant_id:i?.id??null,scopes:null,origin_run_id:null}}if(o.class==="app"){const i=ln(n);if(i===null)throw new c(401,"grant_revoked",`no active grant for ${n}`);const h=e.ceiling??o.scopes;return{sub:i.sub,grant_id:i.id,scopes:[...h??i.resolved],origin_run_id:null}}if(o.class!=="broker")throw new c(401,"unauthorized","this caller cannot arm a cron");const a=r??_(o.node),u=a?.grant_id??o.grant,l=a?.sub??o.sub,p=u!=null?cn(u):dn(n,l);if(p===null){if(n==="app:terminal"&&!pn("user").runtime?.issuer)return{sub:l,grant_id:u??null,scopes:a?.scopes??o.scopes,origin_run_id:a?.run_id??o.run};throw new c(401,"grant_revoked","the node grant is not active")}const d=a?.scopes??o.scopes;return{sub:p.sub,grant_id:p.id,scopes:d===null?[...p.resolved]:[...d],origin_run_id:a?.run_id??o.run}}t(On,"armPrincipal");function Cn(e){const n=e.body??{},r=P(n,"name"),o=P(n,"command"),a=Z(P(n,"fire_at"),"fire_at"),u=typeof n.recur=="string"&&n.recur!==""?yn(n.recur):null,l=typeof n.expires_at=="string"&&n.expires_at!==""?Z(n.expires_at,"expires_at"):null,p=typeof n.env_json=="string"&&n.env_json!==""?kn(n.env_json):null,d=typeof n.sink=="string"&&n.sink.trim()!==""?n.sink.trim():null,i=$(n.on_output,"on_output",["silent","on-failure","always","on-change"],d?.startsWith("node:")?"on-change":"on-failure"),h=$(n.overlap,"overlap",["skip","queue","replace"],"skip"),I=$(n.tier,"tier",["normal","urgent","critical"],"normal"),J=i==="always"||i==="on-change";if(J&&d===null)throw new s({error:"sink_required",message:`on_output "${i}" delivers stdout every qualifying run \u2014 it needs a sink`,field:"sink",next:"Pass sink node:<node-id> (a living node's inbox), spawn:<kind> (birth a fresh node per delivery), or human (the humanloop inbox)."});if(!J&&d!==null)throw new s({error:"sink_unused",message:`on_output "${i}" never delivers stdout \u2014 a sink would be dead config`,field:"sink",next:'Drop the sink, or use on_output "always"/"on-change" if delivery is the point.'});const Y=d!==null?bn(d):"{}";d?.startsWith("spawn:")&&mn(e.ceiling,"crtr:act");const w=e.principal.class==="person"?"app:terminal":e.principal.app,b=e.principal.class==="broker"?e.principal.node:typeof n.created_by=="string"&&n.created_by!==""?n.created_by:null;if(e.principal.class==="broker"&&n.created_by!=null&&n.created_by!==b)throw new c(403,"permission_error","a broker may only arm a cron as its own node");const m=b===null?null:_(b);if(m!==null){if(m.grantee!==w)throw new s({error:"creator_app_mismatch",message:"creator node belongs to another app",field:"created_by",next:"Use a node of the calling app."});F(m,"crtr:schedule"),d?.startsWith("spawn:")&&F(m,"crtr:act")}if(n.isolated!==void 0&&typeof n.isolated!="boolean")throw new c(400,"invalid_request","isolated must be a boolean");if(m?.isolated&&n.isolated===!1)throw new c(403,"isolated_run","a cron created by an isolated node cannot escape isolation");const N=m?.isolated===!0||n.isolated===!0,g=On(e,w,m),q=V(),k=N?tn(w,q):null;if(k!==null&&n.cwd_is_default!==!0){const T=X(k,typeof n.cwd=="string"&&n.cwd.trim()!==""?W(n.cwd):k);if(T===".."||T.startsWith(`..${z}`)||T.startsWith(z))throw new c(400,"invalid_request","an isolated cron must run in its own cron folder",{param:"cwd"})}const K=k??$n(n,w,b,Sn(n),N,{sub:g.sub,grantId:g.grant_id,scopes:g.scopes}),f=typeof n.anchor_node=="string"&&n.anchor_node!==""?n.anchor_node:null;if(f!==null&&_(f)!==null&&_(f).grantee!==w)throw new s({error:"anchor_app_mismatch",message:"anchor node belongs to another app",field:"anchor_node",next:"Use an anchor of the calling app."});if(f!==null&&_(f)===null)throw new s({error:"anchor_not_found",message:`anchor node ${f} does not exist on the canvas`,field:"anchor_node",next:"Name a live node id, or drop the anchor \u2014 an unanchored cron outlives every node."});const R=n.cancel_on_wake===!0;if(R&&f===null)throw new s({error:"cancel_on_wake_needs_anchor",message:"cancel_on_wake couples the cron to a node's dormancy \u2014 it requires an anchor node",field:"anchor_node",next:"Pass anchor_node with the node whose wake should cancel this cron."});const S=n.run_timeout_s??300;if(typeof S!="number"||!Number.isInteger(S)||S<=0)throw new s({error:"bad_run_timeout",message:`run_timeout_s must be a positive integer (got ${String(n.run_timeout_s)})`,field:"run_timeout_s",next:"Send the timeout in whole seconds, e.g. 300."});const j=typeof n.profile=="string"&&n.profile!==""?wn(e,n.profile).profileId:null,D=$(n.scope,"scope",["profile","global"],j!==null?"profile":"global");if(D==="profile"&&j===null)throw new s({error:"scope_needs_profile",message:'scope "profile" names the profile the cron lists and cancels within \u2014 it requires a profile',field:"profile",next:'Send a profile, or use scope "global" for a canvas-home-wide cron.'});return nn({cron_id:q,name:r,created_by:b,grantee:w,sub:g.sub,grant_id:g.grant_id,scopes:g.scopes,origin_run_id:g.origin_run_id,isolated:N,command:o,fire_at:a,recur:u,tz:typeof n.tz=="string"?n.tz:null,expires_at:l,anchor_node:f,cancel_on_wake:R,cwd:K,env_json:p,profile:j,scope:D,run_timeout_s:S,overlap:h,on_output:i,sink:Y,tier:I}),{status:201,body:y(v(q),null)}}t(Cn,"handleArm");function In(e){const n=A(e);return{status:200,body:(n===null?x({all:!0}):[...x({profile:n}),...x({global:!0})].sort((o,a)=>o.fire_at<a.fire_at?-1:o.fire_at>a.fire_at?1:0)).filter(o=>E(e,o)).map(o=>y(o,U(o)))}}t(In,"handleList");function U(e){return G(e.cron_id)[0]??null}t(U,"lastRunOf");function O(e){const n=e.params.cronId,r=v(n);if(r===null||!E(e,r))throw new c(404,"cron_not_found",`no cron ${n}`);if(!H(r,A(e)))throw new c(404,"cron_not_found",`no cron ${n} in this profile \u2014 it is scoped to another profile`);return r}t(O,"requireCron");function C(e,n){const r=M(e);if(r!==null&&n.grantee!==r)throw new c(403,"other_app_run",`cron ${n.cron_id} belongs to another app`);return n}t(C,"requireOwnCron");function Nn(e){const n=O(e),r=G(n.cron_id);return{status:200,body:{cron:y(n,r[0]??null),runs:r.map(L)}}}t(Nn,"handleShow");function qn(e){const n=C(e,O(e));B(n.cron_id,"paused");const r=v(n.cron_id);return{status:200,body:y(r,U(r))}}t(qn,"handlePause");function jn(e){const n=C(e,O(e));B(n.cron_id,"active");const r=v(n.cron_id);return{status:200,body:y(r,U(r))}}t(jn,"handleResume");async function Tn(e){const n=C(e,O(e));if(n.run_state==="running")throw new c(409,"cron_running",`cron ${n.cron_id} has a run in flight \u2014 wait for it to settle`);const r=await _n(n,{outOfBand:!0});if(r===null)throw new c(409,"cron_running",`cron ${n.cron_id} was claimed by a concurrent run \u2014 check \`cron show\` for its result`);return{status:200,body:L(r)}}t(Tn,"handleRun");function xn(e){const n=new Date().toISOString();return{status:200,body:{unparked:rn(n,M(e)),at:n}}}t(xn,"handlePoke");function Pn(e){const n=e.params.cronId,r=v(n);if(r!==null&&!E(e,r))throw new c(404,"cron_not_found",`no cron ${n}`);if(r!==null&&!H(r,A(e)))throw new c(404,"cron_not_found",`no cron ${n} in this profile \u2014 it is scoped to another profile`);r!==null&&C(e,r);const o=e.query.get("run_id"),a=o!==null&&hn(n,o);return en(n,{preserveRunningProcess:a}),{status:204}}t(Pn,"handleCancel");const Qn=[{method:"POST",pattern:"/v1/crons",scope:"crtr:schedule",handler:Cn},{method:"GET",pattern:"/v1/crons",handler:In},{method:"GET",pattern:"/v1/crons/:cronId",handler:Nn},{method:"POST",pattern:"/v1/crons/:cronId/pause",scope:"crtr:schedule",handler:qn},{method:"POST",pattern:"/v1/crons/:cronId/resume",scope:"crtr:schedule",handler:jn},{method:"POST",pattern:"/v1/crons/poke",scope:"crtr:schedule",handler:xn},{method:"POST",pattern:"/v1/crons/:cronId/run",scope:"crtr:schedule",handler:Tn},{method:"DELETE",pattern:"/v1/crons/:cronId",scope:"crtr:schedule",handler:Pn}];export{Qn as cronRoutes};
1
+ var V=Object.defineProperty;var t=(e,n)=>V(e,"name",{value:n,configurable:!0});import{randomUUID as X}from"node:crypto";import{relative as nn,resolve as z,sep as B}from"node:path";import{ApiError as a}from"../../../api/errors.js";import{armCron as en,cancelCron as rn,getCron as y,listCrons as T,listCronRuns as G,pokeHeldCrons as on,setCronState as F}from"../../../core/canvas/crons.js";import{getNode as h}from"../../../core/canvas/canvas.js";import{appSpace as tn,cronFolder as sn}from"../../../core/layout.js";import{appProcessesSandboxed as an,cwdIsMounted as cn}from"../../../core/spaces/process.js";import{activeGrant as ln,activeGrantForApp as dn,grantFor as un,terminalGrant as pn}from"../../../core/grants/mirror.js";import{readConfig as fn}from"../../../core/config.js";import{assertScope as L}from"../../../core/scopes.js";import{InputError as i}from"../../../core/io.js";import{nextSlotAfter as _n}from"../../../core/wake.js";import{executeCron as hn,markSelfCanceledCronRun as mn}from"../../cron-run.js";import{toCronDTO as k,toCronRunDTO as Z}from"../map.js";import{assertCeiling as gn,canReadNode as wn}from"../router.js";import{checkCronBirth as bn,checkCronFire as vn}from"../../cron/authority.js";import{requireUsableProfile as yn}from"./profiles.js";function A(e,n){const r=e[n];if(typeof r!="string"||r.trim()==="")throw new i({error:"missing_field",message:`cron arm request requires a non-empty "${n}"`,field:n,next:`Send a non-empty string "${n}".`});return r}t(A,"requireString");function C(e,n,r,o){if(e==null)return o;if(typeof e=="string"&&r.includes(e))return e;throw new i({error:`bad_${n}`,message:`"${String(e)}" is not a valid ${n}`,field:n,next:`Use one of: ${r.join(", ")}.`})}t(C,"requireEnum");function kn(e){if(e==="human")return JSON.stringify({human:!0});if(e.startsWith("node:")){const n=e.slice(5).trim();if(n==="")throw new i({error:"bad_sink",message:"node sink requires a node id",field:"sink",next:'Use sink "node:<node-id>".'});if(h(n)===null)throw new i({error:"sink_node_not_found",message:`sink node ${n} does not exist on the canvas`,field:"sink",next:"Name a live node id, or use spawn:<kind> for a delivery that outlives any one node."});return JSON.stringify({node:n})}if(e.startsWith("spawn:")){const n=e.slice(6).trim();if(n==="")throw new i({error:"bad_sink",message:"spawn sink requires a node kind",field:"sink",next:'Use sink "spawn:<kind>" (e.g. spawn:general).'});return JSON.stringify({spawn:n})}throw new i({error:"bad_sink",message:`"${e}" is not a valid sink`,field:"sink",next:"Use node:<node-id>, spawn:<kind>, or human."})}t(kn,"parseSinkSpec");const Sn=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;function H(e,n){if(!Sn.test(e)||Number.isNaN(Date.parse(e)))throw new i({error:`bad_${n}`,message:`"${e}" is not a UTC ISO instant`,field:n,next:'Send a resolved UTC instant like "2026-06-07T09:00:00.000Z" (the client resolves the time grammar).'});return e}t(H,"requireUtcIso");function $n(e){try{_n(e,new Date)}catch(n){throw new i({error:"bad_recur",message:`recur is not advanceable: ${n instanceof Error?n.message:String(n)}`,field:"recur",next:'Send recur JSON the daemon can advance: {"every":"6h"} or {"cron":"0 9 * * *","tz":"America/New_York"}.'})}return e}t($n,"requireRecur");function Cn(e){let n;try{n=JSON.parse(e)}catch{throw new i({error:"bad_env_json",message:"env_json is not valid JSON",field:"env_json",next:'Send a JSON object of string values, e.g. {"API_BASE":"https://x"}.'})}if(typeof n!="object"||n===null||Array.isArray(n))throw new i({error:"bad_env_json",message:"env_json must be a JSON object",field:"env_json",next:'Send a JSON object of string values, e.g. {"API_BASE":"https://x"}.'});for(const[r,o]of Object.entries(n))if(typeof o!="string")throw new i({error:"bad_env_json",message:`env_json value for "${r}" is not a string`,field:"env_json",next:"Every env value must be a string."});return e}t(Cn,"requireEnvJson");function E(e){const n=e.query.get("profile");return n!==null&&n.trim()!==""?n.trim():null}t(E,"callerProfile");function M(e,n){return n===null||e.scope==="global"||e.profile===n}t(M,"inScope");function Y(e){return e.principal.class==="person"?null:e.principal.app}t(Y,"callerApp");function U(e,n){return wn(e.principal,n.grantee,n.created_by??void 0)}t(U,"visibleTo");function On(e){return typeof e.anchor_node=="string"&&e.anchor_node!==""?e.anchor_node:null}t(On,"anchorNodeOf");function In(e,n,r,o,c,u){const l=typeof e.cwd=="string"&&e.cwd.trim()!==""?z(e.cwd):null;if(!an()){if(l===null)throw new i({error:"missing_field",message:'cron arm request requires a non-empty "cwd"',field:"cwd",next:'Send a non-empty string "cwd".'});return l}const p=o!==null&&h(o)!==null?o:void 0,d=t(N=>cn(n,N,p,c,void 0,u),"reaches");if(l!==null&&d(l))return l;const s=tn(n).files;if(l!==null&&e.cwd_is_default!==!0)throw new a(400,"invalid_request",`cwd ${l} is not inside a directory this app's cron runs can see, so every run would fail to start; use a directory under ${s}`,{cwd:l,default:s},void 0,{param:"cwd"});const m=r===null?null:h(r)?.cwd??null;return m!==null&&d(m)?m:s}t(In,"resolveArmedCwd");function Nn(e,n,r){const o=e.principal;if(o.class==="person"){const s=pn();return{sub:s?.sub??"user:local",grant_id:s?.id??null,scopes:null,origin_run_id:null}}if(o.class==="app"){const s=dn(n);if(s===null)throw new a(401,"grant_revoked",`no active grant for ${n}`);const m=e.ceiling??o.scopes;return{sub:s.sub,grant_id:s.id,scopes:[...m??s.resolved],origin_run_id:null}}if(o.class==="cron"){const s=vn({grantee:o.app,sub:o.sub,grant_id:o.grant,scopes:o.scopes});if(!s.allowed)throw new a(s.reason==="scope_missing"?403:401,s.reason==="scope_missing"?"scope_missing":"grant_revoked",s.message);return{sub:o.sub,grant_id:o.grant,scopes:o.scopes===null?[...s.effectiveScopes]:[...o.scopes],origin_run_id:o.origin_run}}if(o.class!=="broker")throw new a(401,"unauthorized","this caller cannot arm a cron");const c=r??h(o.node),u=c?.grant_id??o.grant,l=c?.sub??o.sub,p=u!=null?ln(u):un(n,l);if(p===null){if(n==="app:terminal"&&!fn("user").runtime?.issuer)return{sub:l,grant_id:u??null,scopes:c?.scopes??o.scopes,origin_run_id:c?.run_id??o.run};throw new a(401,"grant_revoked","the node grant is not active")}const d=c?.scopes??o.scopes;return{sub:p.sub,grant_id:p.id,scopes:d===null?[...p.resolved]:[...d],origin_run_id:c?.run_id??o.run}}t(Nn,"armPrincipal");function qn(e){const n=e.body??{},r=A(n,"name"),o=A(n,"command"),c=H(A(n,"fire_at"),"fire_at"),u=typeof n.recur=="string"&&n.recur!==""?$n(n.recur):null,l=typeof n.expires_at=="string"&&n.expires_at!==""?H(n.expires_at,"expires_at"):null,p=typeof n.env_json=="string"&&n.env_json!==""?Cn(n.env_json):null,d=typeof n.sink=="string"&&n.sink.trim()!==""?n.sink.trim():null,s=C(n.on_output,"on_output",["silent","on-failure","always","on-change"],d?.startsWith("node:")?"on-change":"on-failure"),m=C(n.overlap,"overlap",["skip","queue","replace"],"skip"),N=C(n.tier,"tier",["normal","urgent","critical"],"normal"),R=s==="always"||s==="on-change";if(R&&d===null)throw new i({error:"sink_required",message:`on_output "${s}" delivers stdout every qualifying run \u2014 it needs a sink`,field:"sink",next:"Pass sink node:<node-id> (a living node's inbox), spawn:<kind> (birth a fresh node per delivery), or human (the humanloop inbox)."});if(!R&&d!==null)throw new i({error:"sink_unused",message:`on_output "${s}" never delivers stdout \u2014 a sink would be dead config`,field:"sink",next:'Drop the sink, or use on_output "always"/"on-change" if delivery is the point.'});const K=d!==null?kn(d):"{}";if(d?.startsWith("spawn:")&&(gn(e.ceiling,"crtr:act"),e.principal.class==="cron")){const _=bn({grantee:e.principal.app,sub:e.principal.sub,grant_id:e.principal.grant,scopes:e.principal.scopes});if(!_.allowed)throw new a(_.reason==="scope_missing"?403:401,_.reason==="scope_missing"?"scope_missing":"grant_revoked",_.message)}const b=e.principal.class==="person"?"app:terminal":e.principal.app,v=e.principal.class==="broker"?e.principal.node:e.principal.class==="cron"?null:typeof n.created_by=="string"&&n.created_by!==""?n.created_by:null;if(e.principal.class==="broker"&&n.created_by!=null&&n.created_by!==v)throw new a(403,"permission_error","a broker may only arm a cron as its own node");const g=v===null?null:h(v);if(g!==null){if(g.grantee!==b)throw new i({error:"creator_app_mismatch",message:"creator node belongs to another app",field:"created_by",next:"Use a node of the calling app."});L(g,"crtr:schedule"),d?.startsWith("spawn:")&&L(g,"crtr:act")}if(n.isolated!==void 0&&typeof n.isolated!="boolean")throw new a(400,"invalid_request","isolated must be a boolean");if(g?.isolated&&n.isolated===!1)throw new a(403,"isolated_run","a cron created by an isolated node cannot escape isolation");const D=g?.isolated===!0||e.principal.class==="cron"&&e.principal.isolated;if(D&&n.isolated===!1)throw new a(403,"isolated_run","a cron created by an isolated caller cannot escape isolation");const q=D||n.isolated===!0,w=Nn(e,b,g),j=X(),S=q?sn(b,j):null;if(S!==null&&n.cwd_is_default!==!0){const _=nn(S,typeof n.cwd=="string"&&n.cwd.trim()!==""?z(n.cwd):S);if(_===".."||_.startsWith(`..${B}`)||_.startsWith(B))throw new a(400,"invalid_request","an isolated cron must run in its own cron folder",{param:"cwd"})}const Q=S??In(n,b,v,On(n),q,{sub:w.sub,grantId:w.grant_id,scopes:w.scopes}),f=typeof n.anchor_node=="string"&&n.anchor_node!==""?n.anchor_node:null;if(f!==null&&h(f)!==null&&h(f).grantee!==b)throw new i({error:"anchor_app_mismatch",message:"anchor node belongs to another app",field:"anchor_node",next:"Use an anchor of the calling app."});if(f!==null&&h(f)===null)throw new i({error:"anchor_not_found",message:`anchor node ${f} does not exist on the canvas`,field:"anchor_node",next:"Name a live node id, or drop the anchor \u2014 an unanchored cron outlives every node."});const W=n.cancel_on_wake===!0;if(W&&f===null)throw new i({error:"cancel_on_wake_needs_anchor",message:"cancel_on_wake couples the cron to a node's dormancy \u2014 it requires an anchor node",field:"anchor_node",next:"Pass anchor_node with the node whose wake should cancel this cron."});const $=n.run_timeout_s??300;if(typeof $!="number"||!Number.isInteger($)||$<=0)throw new i({error:"bad_run_timeout",message:`run_timeout_s must be a positive integer (got ${String(n.run_timeout_s)})`,field:"run_timeout_s",next:"Send the timeout in whole seconds, e.g. 300."});const P=typeof n.profile=="string"&&n.profile!==""?yn(e,n.profile).profileId:null,x=C(n.scope,"scope",["profile","global"],P!==null?"profile":"global");if(x==="profile"&&P===null)throw new i({error:"scope_needs_profile",message:'scope "profile" names the profile the cron lists and cancels within \u2014 it requires a profile',field:"profile",next:'Send a profile, or use scope "global" for a canvas-home-wide cron.'});return en({cron_id:j,name:r,created_by:v,grantee:b,sub:w.sub,grant_id:w.grant_id,scopes:w.scopes,origin_run_id:w.origin_run_id,isolated:q,command:o,fire_at:c,recur:u,tz:typeof n.tz=="string"?n.tz:null,expires_at:l,anchor_node:f,cancel_on_wake:W,cwd:Q,env_json:p,profile:P,scope:x,run_timeout_s:$,overlap:m,on_output:s,sink:K,tier:N}),{status:201,body:k(y(j),null)}}t(qn,"handleArm");function jn(e){const n=E(e);return{status:200,body:(n===null?T({all:!0}):[...T({profile:n}),...T({global:!0})].sort((o,c)=>o.fire_at<c.fire_at?-1:o.fire_at>c.fire_at?1:0)).filter(o=>U(e,o)).map(o=>k(o,J(o)))}}t(jn,"handleList");function J(e){return G(e.cron_id)[0]??null}t(J,"lastRunOf");function O(e){const n=e.params.cronId,r=y(n);if(r===null||!U(e,r))throw new a(404,"cron_not_found",`no cron ${n}`);if(!M(r,E(e)))throw new a(404,"cron_not_found",`no cron ${n} in this profile \u2014 it is scoped to another profile`);return r}t(O,"requireCron");function I(e,n){const r=Y(e);if(r!==null&&n.grantee!==r)throw new a(403,"other_app_run",`cron ${n.cron_id} belongs to another app`);return n}t(I,"requireOwnCron");function Pn(e){const n=O(e),r=G(n.cron_id);return{status:200,body:{cron:k(n,r[0]??null),runs:r.map(Z)}}}t(Pn,"handleShow");function Tn(e){const n=I(e,O(e));F(n.cron_id,"paused");const r=y(n.cron_id);return{status:200,body:k(r,J(r))}}t(Tn,"handlePause");function An(e){const n=I(e,O(e));F(n.cron_id,"active");const r=y(n.cron_id);return{status:200,body:k(r,J(r))}}t(An,"handleResume");async function En(e){const n=I(e,O(e));if(n.run_state==="running")throw new a(409,"cron_running",`cron ${n.cron_id} has a run in flight \u2014 wait for it to settle`);const r=await hn(n,{outOfBand:!0});if(r===null)throw new a(409,"cron_running",`cron ${n.cron_id} was claimed by a concurrent run \u2014 check \`cron show\` for its result`);return{status:200,body:Z(r)}}t(En,"handleRun");function Un(e){const n=new Date().toISOString();return{status:200,body:{unparked:on(n,Y(e)),at:n}}}t(Un,"handlePoke");function Jn(e){const n=e.params.cronId,r=y(n);if(r!==null&&!U(e,r))throw new a(404,"cron_not_found",`no cron ${n}`);if(r!==null&&!M(r,E(e)))throw new a(404,"cron_not_found",`no cron ${n} in this profile \u2014 it is scoped to another profile`);r!==null&&I(e,r);const o=e.query.get("run_id"),c=e.principal.class==="cron"?o===e.principal.execution&&n===e.principal.cron:!0,u=o!==null&&c&&mn(n,o);return rn(n,{preserveRunningProcess:u}),{status:204}}t(Jn,"handleCancel");const ee=[{method:"POST",pattern:"/v1/crons",scope:"crtr:schedule",handler:qn},{method:"GET",pattern:"/v1/crons",handler:jn},{method:"GET",pattern:"/v1/crons/:cronId",handler:Pn},{method:"POST",pattern:"/v1/crons/:cronId/pause",scope:"crtr:schedule",handler:Tn},{method:"POST",pattern:"/v1/crons/:cronId/resume",scope:"crtr:schedule",handler:An},{method:"POST",pattern:"/v1/crons/poke",scope:"crtr:schedule",handler:Un},{method:"POST",pattern:"/v1/crons/:cronId/run",scope:"crtr:schedule",handler:En},{method:"DELETE",pattern:"/v1/crons/:cronId",scope:"crtr:schedule",handler:Jn}];export{ee as cronRoutes};
@@ -1 +1 @@
1
- var S=Object.defineProperty;var o=(e,t)=>S(e,"name",{value:t,configurable:!0});import{closeSync as A,constants as _,fstatSync as q,readSync as B}from"node:fs";import{isAbsolute as T,resolve as R}from"node:path";import{appSpace as I}from"../../../core/layout.js";import{ApiError as u}from"../../../api/index.js";import{notFound as b,usage as c}from"../../../core/errors.js";import{InputError as L}from"../../../core/io.js";import{RunError as m}from"../../../core/runs/operations.js";import{isBeneathRefusal as M,listBeneath as k,openBeneath as v,writeFileBeneath as z}from"../../../core/spaces/open-beneath.js";import{fileRoot as F}from"../../../core/spaces/permissions.js";import{nodeMayReach as N,runNodeIds as P,runPlace as O}from"../../../core/runs/confinement.js";import{resolvedScopes as D}from"../router.js";const p=512*1024,w=1024*1024,C=1e3;function h(e,t){if(e===null||e==="")throw c(`a file ${t} requires a \`path\` ${t==="write"?"body field":"query param"}`);if(!T(e))throw c(`file ${t} path must be absolute`,{received:e});return e}o(h,"requireAbsolutePath");function E(e){if(e==null)return"utf8";if(e==="utf8"||e==="base64")return e;throw c("encoding must be `utf8` or `base64`",{received:e})}o(E,"parseEncoding");function d(e,t,r){if(e instanceof u||e instanceof L||e instanceof m)throw e;if(M(e))throw new m(400,"invalid_request",`${t} meets a symlink, leaves its permitted root, or is not a regular file`,"path");const n=e.code;throw n==="ENOENT"?b(`no such file: ${t}`,{received:t}):n==="EACCES"||n==="EPERM"?new u(403,"forbidden",`permission denied ${r} ${t}`):new u(500,"internal",`cannot ${r} ${t}: ${e instanceof Error?e.message:String(e)}`)}o(d,"mapFilesystemError");function $(e,t){const r=O(e);if(r?.isolated){const n=new Set(P(r.run));return{isolated:!0,reaches:o(i=>n.has(i),"reaches")}}return{isolated:!1,reaches:o(n=>N(e,t,n,t),"reaches")}}o($,"runConfinement");function y(e,t,r){const n=e.principal,i=n.class==="person"?null:n.class==="app"?{app:n.app,resolved:n.scopes,launched:null}:{app:n.app,resolved:D(n),launched:n.scopes,run:$(n.node,n.app)},s=F(i,t,r);if("root"in s)return s.root;throw"hidden"in s?b(`no such file: ${t}`,{received:t}):new m(403,"scope_missing",s.missing?`missing scope ${s.missing}`:`no permission names ${t}`,void 0,void 0,s.missing??void 0)}o(y,"rootFor");function W(e,t,r){let n;try{n=v(e,t,_.O_RDONLY)}catch(i){return d(i,t,"reading")}try{const i=q(n);if(i.isDirectory())throw c(`file peek path is a directory: ${t}`,{received:t});const s=Buffer.allocUnsafe(Math.min(i.size,p)+1),l=B(n,s,0,s.length,0),g=l>p,f=s.subarray(0,Math.min(l,p)).toString(r);return{path:t,content:f,truncated:g}}catch(i){return d(i,t,"reading")}finally{A(n)}}o(W,"readHead");function Y(e){const t=h(e.query.get("path"),"peek");return{status:200,body:W(y(e,t,"read"),t,E(e.query.get("encoding")))}}o(Y,"handlePeek");function Z(e){return e.length%4===0&&/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(e)}o(Z,"isBase64");function j(e){if(typeof e!="object"||e===null||Array.isArray(e))throw c("file write requires an object body");const t=e,r=h(typeof t.path=="string"?t.path:null,"write");if(typeof t.content!="string")throw c("file write requires a string `content` body field");const n=E(t.encoding);if(n==="base64"&&!Z(t.content))throw c("base64 file content must be valid base64");return{path:r,content:t.content,encoding:n}}o(j,"parseWriteBody");function G(e,t,r){if(r.byteLength>w)throw new u(413,"file_too_large",`file content exceeds the ${w} byte limit`);try{return z(e,t,r,{mode:process.platform==="linux"?432:384,atomic:!0}),{path:t,bytes_written:r.byteLength}}catch(n){return d(n,t,"writing")}}o(G,"writeAtomically");function U(e){const{path:t,content:r,encoding:n}=j(e.body);return{status:200,body:G(y(e,t,"rw"),t,Buffer.from(r,n))}}o(U,"handleWrite");function X(e){if(e===null)return C;const t=Number(e);if(!Number.isInteger(t)||t<1)throw c("file list limit must be a positive integer",{received:e});return t}o(X,"parseLimit");function H(e){const t=h(e.query.get("path"),"list"),r=X(e.query.get("limit")),n=y(e,t,"read");let i;try{i=k(n,t,r+1)}catch(f){return d(f,t,"listing")}const s=e.principal;if(s.class==="broker"&&R(t)===I(s.app).nodes){const f=$(s.node,s.app);i=i.filter(({name:a})=>f.reaches(a))}const l=i.slice(0,r).map(({name:f,stat:a})=>({name:f,type:a.isFile()?"file":a.isDirectory()?"dir":"other",size:a.size,modified:a.mtime.toISOString()}));return{status:200,body:{path:t,entries:l,truncated:i.length>r}}}o(H,"handleList");const ce=[{method:"GET",pattern:"/v1/files/peek",scope:"crtr:act",handler:Y},{method:"POST",pattern:"/v1/files/write",scope:"crtr:act",handler:U},{method:"GET",pattern:"/v1/files/list",scope:"crtr:act",handler:H}];export{ce as fileRoutes};
1
+ var A=Object.defineProperty;var o=(e,t)=>A(e,"name",{value:t,configurable:!0});import{closeSync as _,constants as q,fstatSync as B,readSync as T}from"node:fs";import{isAbsolute as R,resolve as I}from"node:path";import{appSpace as L}from"../../../core/layout.js";import{ApiError as u}from"../../../api/index.js";import{notFound as b,usage as c}from"../../../core/errors.js";import{InputError as v}from"../../../core/io.js";import{RunError as p}from"../../../core/runs/operations.js";import{isBeneathRefusal as M,listBeneath as k,openBeneath as z,writeFileBeneath as F}from"../../../core/spaces/open-beneath.js";import{fileRoot as N}from"../../../core/spaces/permissions.js";import{nodeMayReach as P,runNodeIds as O,runPlace as D}from"../../../core/runs/confinement.js";import{resolvedScopes as w}from"../router.js";const m=512*1024,E=1024*1024,C=1e3;function h(e,t){if(e===null||e==="")throw c(`a file ${t} requires a \`path\` ${t==="write"?"body field":"query param"}`);if(!R(e))throw c(`file ${t} path must be absolute`,{received:e});return e}o(h,"requireAbsolutePath");function $(e){if(e==null)return"utf8";if(e==="utf8"||e==="base64")return e;throw c("encoding must be `utf8` or `base64`",{received:e})}o($,"parseEncoding");function d(e,t,r){if(e instanceof u||e instanceof v||e instanceof p)throw e;if(M(e))throw new p(400,"invalid_request",`${t} meets a symlink, leaves its permitted root, or is not a regular file`,"path");const n=e.code;throw n==="ENOENT"?b(`no such file: ${t}`,{received:t}):n==="EACCES"||n==="EPERM"?new u(403,"forbidden",`permission denied ${r} ${t}`):new u(500,"internal",`cannot ${r} ${t}: ${e instanceof Error?e.message:String(e)}`)}o(d,"mapFilesystemError");function S(e,t){const r=D(e);if(r?.isolated){const n=new Set(O(r.run));return{isolated:!0,reaches:o(i=>n.has(i),"reaches")}}return{isolated:!1,reaches:o(n=>P(e,t,n,t),"reaches")}}o(S,"runConfinement");function y(e,t,r){const n=e.principal,i=n.class==="person"?null:n.class==="app"?{app:n.app,resolved:n.scopes,launched:null}:n.class==="cron"?{app:n.app,resolved:w(n),launched:n.scopes}:{app:n.app,resolved:w(n),launched:n.scopes,run:S(n.node,n.app)},s=N(i,t,r);if("root"in s)return s.root;throw"hidden"in s?b(`no such file: ${t}`,{received:t}):new p(403,"scope_missing",s.missing?`missing scope ${s.missing}`:`no permission names ${t}`,void 0,void 0,s.missing??void 0)}o(y,"rootFor");function W(e,t,r){let n;try{n=z(e,t,q.O_RDONLY)}catch(i){return d(i,t,"reading")}try{const i=B(n);if(i.isDirectory())throw c(`file peek path is a directory: ${t}`,{received:t});const s=Buffer.allocUnsafe(Math.min(i.size,m)+1),l=T(n,s,0,s.length,0),g=l>m,a=s.subarray(0,Math.min(l,m)).toString(r);return{path:t,content:a,truncated:g}}catch(i){return d(i,t,"reading")}finally{_(n)}}o(W,"readHead");function Y(e){const t=h(e.query.get("path"),"peek");return{status:200,body:W(y(e,t,"read"),t,$(e.query.get("encoding")))}}o(Y,"handlePeek");function Z(e){return e.length%4===0&&/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(e)}o(Z,"isBase64");function j(e){if(typeof e!="object"||e===null||Array.isArray(e))throw c("file write requires an object body");const t=e,r=h(typeof t.path=="string"?t.path:null,"write");if(typeof t.content!="string")throw c("file write requires a string `content` body field");const n=$(t.encoding);if(n==="base64"&&!Z(t.content))throw c("base64 file content must be valid base64");return{path:r,content:t.content,encoding:n}}o(j,"parseWriteBody");function G(e,t,r){if(r.byteLength>E)throw new u(413,"file_too_large",`file content exceeds the ${E} byte limit`);try{return F(e,t,r,{mode:process.platform==="linux"?432:384,atomic:!0}),{path:t,bytes_written:r.byteLength}}catch(n){return d(n,t,"writing")}}o(G,"writeAtomically");function U(e){const{path:t,content:r,encoding:n}=j(e.body);return{status:200,body:G(y(e,t,"rw"),t,Buffer.from(r,n))}}o(U,"handleWrite");function X(e){if(e===null)return C;const t=Number(e);if(!Number.isInteger(t)||t<1)throw c("file list limit must be a positive integer",{received:e});return t}o(X,"parseLimit");function H(e){const t=h(e.query.get("path"),"list"),r=X(e.query.get("limit")),n=y(e,t,"read");let i;try{i=k(n,t,r+1)}catch(a){return d(a,t,"listing")}const s=e.principal;if(s.class==="broker"&&I(t)===L(s.app).nodes){const a=S(s.node,s.app);i=i.filter(({name:f})=>a.reaches(f))}const l=i.slice(0,r).map(({name:a,stat:f})=>({name:a,type:f.isFile()?"file":f.isDirectory()?"dir":"other",size:f.size,modified:f.mtime.toISOString()}));return{status:200,body:{path:t,entries:l,truncated:i.length>r}}}o(H,"handleList");const ce=[{method:"GET",pattern:"/v1/files/peek",scope:"crtr:act",handler:Y},{method:"POST",pattern:"/v1/files/write",scope:"crtr:act",handler:U},{method:"GET",pattern:"/v1/files/list",scope:"crtr:act",handler:H}];export{ce as fileRoutes};