@north-light/cli 0.3.454 → 0.3.455
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/contract.d.ts +1 -1
- package/dist/api/contract.js +1 -1
- package/dist/api/error-codes.d.ts +25 -0
- package/dist/api/error-codes.js +1 -1
- package/dist/builtin-memory/crouter-sdk/release-notes.md +6 -0
- package/dist/commands/pkg/browse/catalog.js +1 -1
- package/dist/commands/pkg/plugin-manage.js +1 -1
- package/dist/core/command-manifests/registry.d.ts +2 -1
- package/dist/core/command-plugins/bundle.d.ts +3 -10
- package/dist/core/command-plugins/bundle.js +1 -1
- package/dist/core/command-plugins/compose.js +1 -1
- package/dist/core/command-plugins/discovery.js +1 -1
- package/dist/core/command-plugins/endpoint.d.ts +12 -0
- package/dist/core/command-plugins/endpoint.js +1 -1
- package/dist/core/command.d.ts +13 -0
- package/dist/core/command.js +9 -9
- package/dist/core/peer/client.d.ts +4 -2
- package/dist/core/peer/client.js +1 -1
- package/dist/core/peer/directory.d.ts +7 -0
- package/dist/core/peer/directory.js +1 -1
- package/dist/core/peer/envelope.d.ts +4 -0
- package/dist/core/peer/envelope.js +1 -1
- package/dist/core/peer/home.js +1 -1
- package/dist/core/providers/bundles.d.ts +2 -0
- package/dist/core/providers/bundles.js +2 -2
- package/dist/core/providers/call.d.ts +2 -0
- package/dist/core/providers/call.js +1 -1
- package/dist/core/providers/device.d.ts +39 -0
- package/dist/core/providers/device.js +1 -0
- package/dist/core/providers/errors.js +1 -1
- package/dist/daemon/api/handlers/objects.js +2 -2
- package/dist/daemon/api/handlers/providers.d.ts +2 -0
- package/dist/daemon/api/handlers/providers.js +1 -1
- package/dist/daemon/peer/outbox.js +1 -1
- package/dist/daemon/peer/relay-client.js +1 -1
- package/dist/daemon/peer/relay-protocol.d.ts +1 -1
- package/dist/daemon/peer/relay-protocol.js +1 -1
- package/dist/types.d.ts +4 -0
- package/docs/ci-runner.md +50 -4
- package/docs/sdk/release-notes.md +6 -0
- package/package.json +3 -3
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +12 -12
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var we=Object.defineProperty;var u=(e,t)=>we(e,"name",{value:t,configurable:!0});import{prepared as ye}from"../../../core/sqlite-statements.js";import{ApiError as w}from"../../../api/errors.js";import{PRIVACY_LEVELS_DTO as be}from"../../../api/dto/objects.js";import{openDb as T,withCanvasWrite as y}from"../../../core/canvas/db.js";import{actorOf as V}from"../../../core/graph/documents.js";import{readerForObject as A}from"../../../core/graph/reader.js";import{canRead as ge}from"../../../core/graph/access.js";import{customObjectRef as H,authorizeCustom as B,fenceArgv as Z}from"./custom-objects.js";import{edgesOf as je}from"../../../core/graph/edges.js";import{publish as ke}from"../../../core/graph/events.js";import{objectDTO as b}from"../../../core/graph/kinds/dto.js";import{isKind as _e,kindNames as ve,kindOf as J}from"../../../core/graph/kinds/registry.js";import{listObjects as Ee,searchObjects as K}from"../../../core/graph/objects.js";import{checkoutsForCwd as Oe}from"../../../core/graph/repo-sync/repos.js";import{recordWatch as Q,unwatch as Y,watchersOf as qe,watchesOf as Re}from"../../../core/graph/watches.js";import{readerFor as g,resolveRef as L}from"../reader.js";import{invalidRequest as $,isRecord as k}from"./validate.js";import{callerNode as X,callerStub as N,forwardVerb as x,originates as ee,queryWithoutMachine as P,relayError as C,remoteStub as Te,unknownError as te,forwarding as S,withMachine as F}from"../../peer/forward.js";import{getById as ne}from"../../../core/graph/objects.js";import{isStub as re}from"../../../core/peer/stubs.js";import{isUnreachable as $e}from"../../../core/peer/client.js";import{normalizeMachineId as I}from"../../../core/peer/directory.js";const M=u(e=>t=>Oe(e,t),"reposFnFor");function oe(e,t){return{...b(e,t),...J(e,t.type).listItem?.(e,t)}}u(oe,"listItem");function Se(e){return`no object named ${e} on this runtime; it may be on another runtime, or the name may be misspelled`}u(Se,"remoteMessage");function ie(e,t,n){try{return R(e,t,n)}catch(r){if(r instanceof w&&r.code==="not_found"&&!r.details?.deleted)return null;throw r}}u(ie,"ownerFilter");function R(e,t,n){const r=L(e,t,n);if("remote"in r)throw new w(404,"not_found",Se(r.remote),{next:"Check the name with `crtr canvas search`."});if("deleted"in r)throw new w(404,"not_found",`${r.deleted.name} is deleted`,{id:r.deleted.id,deleted:!0});return r}u(R,"liveRef");const _=u(e=>decodeURIComponent(e.params.ref),"refParam"),se=u(e=>{const t=e.trim().replace(/^\[\[(.*)\]\]$/,"$1").trim(),n=t.indexOf("/");return n>0?t.slice(0,n):t},"idOf");function ae(e){const t=ne(T(),se(e));return t!==null&&re(T(),t)?t.id:null}u(ae,"stubIdOf");async function W(e,t,n,r){const o=ae(t);return o!==null?x(e,o,n):r()?{kind:"local"}:ee(e.principal)?x(e,se(t),n):{kind:"local"}}u(W,"forwardRef");function D(e,t){return y(n=>{try{return!("remote"in L(n,g(n,t),e))}catch{return!1}})}u(D,"resolvesLocally");const G=u(e=>`/v1/objects/${encodeURIComponent(e)}`,"objectPath");function ue(e,t,n,r,o){const i=t?.object;!i?.id||!i.type||y(s=>{const a=X(s,e);if(a===null)return;const c=Te(s,i.id,i.type,n,{kind:i.kind??null,name:i.name??i.id,...i.preview===void 0?{}:{preview:i.preview},owner:i.owner??null});Q(s,a,c,r,o===void 0?{}:{patterns:o})})}u(ue,"watchRemote");async function Ie(e){const t=_(e),n=["1","true"].includes(e.query.get("fields")??""),r=await W(e,t,{method:"GET",path:G(t),query:P(e)},()=>D(t,e));if(r.kind==="unknown")return{state:"unknown",ref:t,unreachable:r.unreachable};if(r.kind==="remote"){if(r.status>=400)throw C(r.status,r.body);return ue(e,r.body,r.machine.id,"read"),F(r.body,r.machine)}return y(o=>{const i=N(o,e),s=i===null?g(o,e):A(o,i,M(o)),a=L(o,s,t);if("remote"in a)return{state:"remote",remote:a.remote};if("deleted"in a)return{state:"deleted",deleted:b(o,a.deleted)};const{data:c,text:l}=J(o,a.type).read(o,s,a,{fields:n,reposFn:M(o)});return{state:"object",type:a.type,object:b(o,a),text:l,data:c}})}u(Ie,"read");function z(e,t,n,r){if(e!=null){if(!n.includes(e)){r.push({field:t,problem:`must be one of ${n.join(", ")}`,received:e});return}return e}}u(z,"oneOf");function ce(e,t,n){if(t!=null){if(typeof t!="string"||!_e(e,t)){n.push({field:"type",problem:`must be a kind: one of ${ve(e).join(", ")}`,received:t});return}return t}}u(ce,"kindName");function le(e,t,n,r){if(e==null)return r;const o=Number(e);return!Number.isInteger(o)||o<1||o>100?(n.push({field:t,problem:"must be an integer from 1 to 100",received:e}),r):o}u(le,"limitOf");function fe(e){const t=e.query,n=[],r=ce(T(),t.get("type"),n),o=z(t.get("privacy"),"privacy",be,n),i=le(t.get("limit"),"limit",n,50),s=t.get("cursor");s!==null&&!/^\d+$/.test(s)&&n.push({field:"cursor",problem:"is not a cursor from a previous page",received:s});const a=t.get("deleted");return a!==null&&!["0","1","true","false"].includes(a)&&n.push({field:"deleted",problem:"must be true or false",received:a}),n.length>0&&$("object list request",n),y(c=>{const l=g(c,e),h={...r?{type:r}:{},...o?{privacy:o}:{},...t.get("kind")!==null?{kind:t.get("kind")}:{},...a==="1"||a==="true"?{deleted:!0}:{}},p=t.get("owner");if(p!==null){const m=ie(c,l,p);if(m===null)return{items:[],next_cursor:null};h.owner=m}const v=t.get("name"),j=[];let d=s===null?0:Number(s),E=!0;for(;j.length<i&&E;){const m=Ee(c,l,h,{limit:100,after_rowid:d});E=m.length===100;for(const q of m)if(d=q.rowid,(v===null||q.name.startsWith(v))&&j.push(q),j.length===i)break}const f=j.at(-1);return{items:j.map(m=>oe(c,m)),next_cursor:j.length===i&&f?String(f.rowid):null}})}u(fe,"listLocal");const Ae={ms:3e4},Le="self",U=u((e,t)=>e.map(n=>({...n,machine:t})),"labelItems");async function de(e,t,n,r){const o=S.current;try{const i=await o.call(t.id,{method:"GET",path:"/v1/objects",query:n},o.envelope(e.principal),r);if($e(i))return{state:"unreachable",items:[],next:null};if(i.status===426)return{state:"too_old",items:[],next:null};const s=i.body;return i.status!==200||!Array.isArray(s?.items)?{state:"unreachable",items:[],next:null}:{state:"ok",items:U(s.items,t),next:s.next_cursor??null}}catch{return{state:"unreachable",items:[],next:null}}}u(de,"remotePage");async function Ne(e){const t=new Promise(n=>e.addEventListener("abort",()=>n(null),{once:!0}));return Promise.race([S.current.machines().catch(()=>null),t])}u(Ne,"machinesWithin");async function Pe(e){const t=e.query.get("machine");if(!ee(e.principal)||!S.current.joined()||t===Le)return fe(me(e));const r=S.current.self(),o=t!==null&&I(t)!==r,i=o?null:fe(me(e)),s=new AbortController,a=setTimeout(()=>s.abort(),Ae.ms);a.unref?.();try{const c=await Ne(s.signal);if(c===null){if(o)throw new w(503,"runtime_unreachable",`The directory did not answer, so ${I(t)} cannot be listed.`,{machine:I(t)});return i}const l=u(f=>({id:f,name:c.find(m=>m.machine_id===f)?.name??f}),"labelOf"),h=l(r),p=c.filter(f=>f.machine_id!==r),v=P(e);if(t!==null){if(!o)return{items:U(i.items,h),next_cursor:i.next_cursor,machines:[{machine_id:r,name:h.name,state:"ok",more:i.next_cursor!==null}]};const f=I(t);if(!p.some(q=>q.machine_id===f))throw new w(404,"not_found",`no machine ${f} on your account`,{machine:f});const m=await de(e,l(f),v,s.signal);return{items:m.items,next_cursor:m.next,machines:[{machine_id:f,name:l(f).name,state:m.state,more:m.next!==null}]}}const{cursor:j,...d}=v,E=await Promise.all(p.map(f=>de(e,l(f.machine_id),d,s.signal)));return{items:[...U(i.items,h),...E.flatMap(f=>f.items)],next_cursor:i.next_cursor,machines:[{machine_id:r,name:h.name,state:"ok",more:i.next_cursor!==null},...p.map((f,m)=>({machine_id:f.machine_id,name:f.name,state:E[m].state,more:E[m].next!==null}))]}}finally{clearTimeout(a)}}u(Pe,"list");function me(e){const t=new URLSearchParams(e.query);return t.delete("machine"),{...e,query:t}}u(me,"withoutMachine");function Ce(e){const t=[],n=k(e.body)?e.body:{};k(e.body)||t.push({field:"body",problem:"must be an object",received:e.body});const r=n.text;(typeof r!="string"||r.trim()==="")&&t.push({field:"text",problem:"is required",received:r});const o=n.grep;if(o!==void 0)if(typeof o!="string")t.push({field:"grep",problem:"must be a regular expression string",received:o});else try{new RegExp(o)}catch(a){t.push({field:"grep",problem:`is not a valid regular expression: ${a.message}`,received:o})}n.body!==void 0&&typeof n.body!="boolean"&&t.push({field:"body",problem:"must be a boolean",received:n.body});const i=ce(T(),n.type,t);n.kind!==void 0&&typeof n.kind!="string"&&t.push({field:"kind",problem:"must be a string",received:n.kind}),n.owner!==void 0&&typeof n.owner!="string"&&t.push({field:"owner",problem:"must be a string",received:n.owner});const s=le(n.limit,"limit",t,50);return t.length>0&&$("object search request",t),y(a=>{const c=g(a,e),l=typeof n.owner=="string"?ie(a,c,n.owner):null;if(typeof n.owner=="string"&&l===null)return{items:[]};const h=K(a,c,{text:r,body:n.body===!0||typeof o=="string"}),p=new Set(h.map(d=>d.object.rowid));return{items:(typeof o=="string"?K(a,c,{text:o,grep:!0}).filter(d=>p.has(d.object.rowid)):h).filter(d=>(!i||d.object.type===i)&&(n.kind===void 0||d.object.kind===n.kind)&&(!l||d.object.owner===l.rowid)).slice(0,s).map(d=>({object:oe(a,d.object),...d.line!==void 0?{matches:[{line:d.line,text:d.text??""}]}:{}}))}})}u(Ce,"search");const he=["interrupt","steer","after-turn","next-turn"],Fe=/^(\*|>|[A-Za-z0-9_-]+)(\.(\*|>|[A-Za-z0-9_-]+))*$/;function Me(e,t){if(e!==void 0){if(!Array.isArray(e)||e.length===0||e.some(n=>typeof n!="string"||!Fe.test(n)||n.includes(">")&&!n.endsWith(">"))){t.push({field:"patterns",problem:"must be a non-empty list of event patterns (dot-separated words, `*` for one word, a final `>` for the rest)",received:e});return}return e}}u(Me,"patternsOf");function We(e,t){if(e===void 0)return;if(!k(e)||Object.keys(e).length===0){t.push({field:"config",problem:"must map levels 1-10 to a delivery",received:e});return}const n=[];for(const[r,o]of Object.entries(e)){const i=Number(r);!Number.isInteger(i)||i<1||i>10?t.push({field:`config.${r}`,problem:"level must be an integer from 1 to 10"}):he.includes(o)?n.push({threshold:i,delivery:o}):t.push({field:`config.${r}`,problem:`delivery must be one of ${he.join(", ")}`,received:o})}return n.sort((r,o)=>o.threshold-r.threshold)}u(We,"configOf");function pe(e){const t=ae(e);if(t!==null)throw new w(400,"invalid_request",`${t} lives on another machine; an explicit watcher cannot watch it from here`,{id:t})}u(pe,"refuseStubWatch");async function De(e){const t=_(e);if(typeof(k(e.body)?e.body:{}).watcher=="string"&&pe(t),typeof(k(e.body)?e.body:{}).watcher!="string"){const n=await W(e,t,{method:"POST",path:`${G(t)}/watch`,body:e.body},()=>D(t,e));if(n.kind==="unknown")throw te(t,n.unreachable);if(n.kind==="remote"){if(n.status>=400)throw C(n.status,n.body);const r=n.body.patterns;return ue(e,n.body,n.machine.id,"ask",r),F(n.body,n.machine)}}return Ge(e)}u(De,"watch");function Ge(e){const t=[],n=e.body===void 0?{}:k(e.body)?e.body:(t.push({field:"body",problem:"must be an object"}),{}),r=Me(n.patterns,t),o=We(n.config,t);return n.filter!==void 0&&!k(n.filter)&&t.push({field:"filter",problem:"must be an object",received:n.filter}),n.watcher!==void 0&&typeof n.watcher!="string"&&t.push({field:"watcher",problem:"must be a custom-object ref",received:n.watcher}),t.length>0&&$("watch request",t),y(i=>{const s=typeof n.watcher=="string"?H(i,n.watcher):null;if(e.publishToken&&!s)throw new w(404,"not_found","watcher not found");if(e.principal.class==="anonymous"&&!s)throw new w(404,"not_found","watcher not found");const a=s?B(i,e,s):N(i,e)??V(g(i,e));s&&Z(i,e,s);const c=s?A(i,s):g(i,e),l=s?(()=>{const p=R(i,c,_(e));if(!ge(i,c,p))throw new w(404,"not_found","object not found");return p})():R(i,c,_(e)),h=r??[">"];return Q(i,s??a,l,s?"act":"ask",{patterns:h,...o?{config:o}:{},...n.filter!==void 0?{filter:n.filter}:{}}),ke(i,{event:"watched",object:l,actor:a,attrs:{}},M(i)),{object:b(i,l),patterns:h}})}u(Ge,"watchLocal");async function ze(e){const t=_(e);if(e.query.get("watcher")&&pe(t),!e.query.get("watcher")){const n=await W(e,t,{method:"DELETE",path:`${G(t)}/watch`,query:P(e)},()=>D(t,e));if(n.kind==="unknown")throw te(t,n.unreachable);if(n.kind==="remote"){if(n.status>=400)throw C(n.status,n.body);const r=n.body.object?.id;return y(o=>{const i=X(o,e),s=r===void 0?null:ne(o,r);i!==null&&s!==null&&re(o,s)&&Y(o,i,s,e.query.get("pattern")??void 0)}),F(n.body,n.machine)}}return Ue(e)}u(ze,"unwatchRoute");function Ue(e){const t=e.query.get("pattern"),n=e.query.get("watcher");return y(r=>{const o=n?H(r,n):null;if((e.publishToken||e.principal.class==="anonymous")&&!o)throw new w(404,"not_found","watcher not found");const i=o?B(r,e,o):N(r,e)??V(g(r,e));o&&Z(r,e,o);const s=o?A(r,o):g(r,e),a=R(r,s,_(e)),c=u(()=>ye(r,`SELECT count(*) AS n FROM watch_subscriptions s JOIN watches w ON w.rowid = s.watch
|
|
2
|
-
WHERE w.watcher = ? AND w.object = ?`).get((o??i).rowid,a.rowid).n,"count"),
|
|
1
|
+
var we=Object.defineProperty;var u=(e,t)=>we(e,"name",{value:t,configurable:!0});import{prepared as ye}from"../../../core/sqlite-statements.js";import{ApiError as w}from"../../../api/errors.js";import{PRIVACY_LEVELS_DTO as be}from"../../../api/dto/objects.js";import{openDb as T,withCanvasWrite as y}from"../../../core/canvas/db.js";import{actorOf as V}from"../../../core/graph/documents.js";import{readerForObject as A}from"../../../core/graph/reader.js";import{canRead as ge}from"../../../core/graph/access.js";import{customObjectRef as H,authorizeCustom as B,fenceArgv as Z}from"./custom-objects.js";import{edgesOf as je}from"../../../core/graph/edges.js";import{publish as ke}from"../../../core/graph/events.js";import{objectDTO as b}from"../../../core/graph/kinds/dto.js";import{isKind as _e,kindNames as ve,kindOf as J}from"../../../core/graph/kinds/registry.js";import{listObjects as Ee,searchObjects as K}from"../../../core/graph/objects.js";import{checkoutsForCwd as Oe}from"../../../core/graph/repo-sync/repos.js";import{recordWatch as Q,unwatch as Y,watchersOf as qe,watchesOf as Re}from"../../../core/graph/watches.js";import{readerFor as g,resolveRef as L}from"../reader.js";import{invalidRequest as $,isRecord as k}from"./validate.js";import{callerNode as X,callerStub as N,forwardVerb as x,originates as ee,queryWithoutMachine as P,relayError as C,remoteStub as Te,unknownError as te,forwarding as S,withMachine as F}from"../../peer/forward.js";import{getById as ne}from"../../../core/graph/objects.js";import{isStub as re}from"../../../core/peer/stubs.js";import{isUnreachable as $e}from"../../../core/peer/client.js";import{normalizeMachineId as I}from"../../../core/peer/directory.js";const M=u(e=>t=>Oe(e,t),"reposFnFor");function oe(e,t){return{...b(e,t),...J(e,t.type).listItem?.(e,t)}}u(oe,"listItem");function Se(e){return`no object named ${e} on this runtime; it may be on another runtime, or the name may be misspelled`}u(Se,"remoteMessage");function ie(e,t,n){try{return R(e,t,n)}catch(r){if(r instanceof w&&r.code==="not_found"&&!r.details?.deleted)return null;throw r}}u(ie,"ownerFilter");function R(e,t,n){const r=L(e,t,n);if("remote"in r)throw new w(404,"not_found",Se(r.remote),{next:"Check the name with `crtr canvas search`."});if("deleted"in r)throw new w(404,"not_found",`${r.deleted.name} is deleted`,{id:r.deleted.id,deleted:!0});return r}u(R,"liveRef");const _=u(e=>decodeURIComponent(e.params.ref),"refParam"),se=u(e=>{const t=e.trim().replace(/^\[\[(.*)\]\]$/,"$1").trim(),n=t.indexOf("/");return n>0?t.slice(0,n):t},"idOf");function ae(e){const t=ne(T(),se(e));return t!==null&&re(T(),t)?t.id:null}u(ae,"stubIdOf");async function W(e,t,n,r){const o=ae(t);return o!==null?x(e,o,n):r()?{kind:"local"}:ee(e.principal)?x(e,se(t),n):{kind:"local"}}u(W,"forwardRef");function D(e,t){return y(n=>{try{return!("remote"in L(n,g(n,t),e))}catch{return!1}})}u(D,"resolvesLocally");const G=u(e=>`/v1/objects/${encodeURIComponent(e)}`,"objectPath");function ue(e,t,n,r,o){const i=t?.object;!i?.id||!i.type||y(s=>{const a=X(s,e);if(a===null)return;const c=Te(s,i.id,i.type,n,{kind:i.kind??null,name:i.name??i.id,...i.preview===void 0?{}:{preview:i.preview},owner:i.owner??null});Q(s,a,c,r,o===void 0?{}:{patterns:o})})}u(ue,"watchRemote");async function Ie(e){const t=_(e),n=["1","true"].includes(e.query.get("fields")??""),r=await W(e,t,{method:"GET",path:G(t),query:P(e)},()=>D(t,e));if(r.kind==="unknown")return{state:"unknown",ref:t,unreachable:r.unreachable};if(r.kind==="remote"){if(r.status>=400)throw C(r.status,r.body);return ue(e,r.body,r.machine.id,"read"),F(r.body,r.machine)}return y(o=>{const i=N(o,e),s=i===null?g(o,e):A(o,i,M(o)),a=L(o,s,t);if("remote"in a)return{state:"remote",remote:a.remote};if("deleted"in a)return{state:"deleted",deleted:b(o,a.deleted)};const{data:c,text:f}=J(o,a.type).read(o,s,a,{fields:n,reposFn:M(o)});return{state:"object",type:a.type,object:b(o,a),text:f,data:c}})}u(Ie,"read");function z(e,t,n,r){if(e!=null){if(!n.includes(e)){r.push({field:t,problem:`must be one of ${n.join(", ")}`,received:e});return}return e}}u(z,"oneOf");function ce(e,t,n){if(t!=null){if(typeof t!="string"||!_e(e,t)){n.push({field:"type",problem:`must be a kind: one of ${ve(e).join(", ")}`,received:t});return}return t}}u(ce,"kindName");function le(e,t,n,r){if(e==null)return r;const o=Number(e);return!Number.isInteger(o)||o<1||o>100?(n.push({field:t,problem:"must be an integer from 1 to 100",received:e}),r):o}u(le,"limitOf");function fe(e){const t=e.query,n=[],r=ce(T(),t.get("type"),n),o=z(t.get("privacy"),"privacy",be,n),i=le(t.get("limit"),"limit",n,50),s=t.get("cursor");s!==null&&!/^\d+$/.test(s)&&n.push({field:"cursor",problem:"is not a cursor from a previous page",received:s});const a=t.get("deleted");return a!==null&&!["0","1","true","false"].includes(a)&&n.push({field:"deleted",problem:"must be true or false",received:a}),n.length>0&&$("object list request",n),y(c=>{const f=g(c,e),h={...r?{type:r}:{},...o?{privacy:o}:{},...t.get("kind")!==null?{kind:t.get("kind")}:{},...a==="1"||a==="true"?{deleted:!0}:{}},p=t.get("owner");if(p!==null){const m=ie(c,f,p);if(m===null)return{items:[],next_cursor:null};h.owner=m}const v=t.get("name"),j=[];let d=s===null?0:Number(s),E=!0;for(;j.length<i&&E;){const m=Ee(c,f,h,{limit:100,after_rowid:d});E=m.length===100;for(const q of m)if(d=q.rowid,(v===null||q.name.startsWith(v))&&j.push(q),j.length===i)break}const l=j.at(-1);return{items:j.map(m=>oe(c,m)),next_cursor:j.length===i&&l?String(l.rowid):null}})}u(fe,"listLocal");const Ae={ms:3e4},Le="self",U=u((e,t)=>e.map(n=>({...n,machine:t})),"labelItems");async function de(e,t,n,r){const o=S.current;try{const i=await o.call(t.id,{method:"GET",path:"/v1/objects",query:n},o.envelope(e.principal),r);if($e(i))return{state:"unreachable",items:[],next:null};if(i.status===426)return{state:"too_old",items:[],next:null};const s=i.body;return i.status!==200||!Array.isArray(s?.items)?{state:"unreachable",items:[],next:null}:{state:"ok",items:U(s.items,t),next:s.next_cursor??null}}catch{return{state:"unreachable",items:[],next:null}}}u(de,"remotePage");async function Ne(e){const t=new Promise(n=>e.addEventListener("abort",()=>n(null),{once:!0}));return Promise.race([S.current.machines().catch(()=>null),t])}u(Ne,"machinesWithin");async function Pe(e){const t=e.query.get("machine");if(!ee(e.principal)||!S.current.joined()||t===Le)return fe(me(e));const r=S.current.self(),o=t!==null&&I(t)!==r,i=o?null:fe(me(e)),s=new AbortController,a=setTimeout(()=>s.abort(),Ae.ms);a.unref?.();try{const c=await Ne(s.signal);if(c===null){if(o)throw new w(503,"runtime_unreachable",`The directory did not answer, so ${I(t)} cannot be listed.`,{machine:I(t)});return i}const f=u(l=>({id:l,name:c.find(m=>m.machine_id===l)?.name??l}),"labelOf"),h=f(r),p=c.filter(l=>l.machine_id!==r&&l.kind!=="device"),v=P(e);if(t!==null){if(!o)return{items:U(i.items,h),next_cursor:i.next_cursor,machines:[{machine_id:r,name:h.name,state:"ok",more:i.next_cursor!==null}]};const l=I(t);if(!p.some(q=>q.machine_id===l))throw new w(404,"not_found",`no machine ${l} on your account`,{machine:l});const m=await de(e,f(l),v,s.signal);return{items:m.items,next_cursor:m.next,machines:[{machine_id:l,name:f(l).name,state:m.state,more:m.next!==null}]}}const{cursor:j,...d}=v,E=await Promise.all(p.map(l=>de(e,f(l.machine_id),d,s.signal)));return{items:[...U(i.items,h),...E.flatMap(l=>l.items)],next_cursor:i.next_cursor,machines:[{machine_id:r,name:h.name,state:"ok",more:i.next_cursor!==null},...p.map((l,m)=>({machine_id:l.machine_id,name:l.name,state:E[m].state,more:E[m].next!==null}))]}}finally{clearTimeout(a)}}u(Pe,"list");function me(e){const t=new URLSearchParams(e.query);return t.delete("machine"),{...e,query:t}}u(me,"withoutMachine");function Ce(e){const t=[],n=k(e.body)?e.body:{};k(e.body)||t.push({field:"body",problem:"must be an object",received:e.body});const r=n.text;(typeof r!="string"||r.trim()==="")&&t.push({field:"text",problem:"is required",received:r});const o=n.grep;if(o!==void 0)if(typeof o!="string")t.push({field:"grep",problem:"must be a regular expression string",received:o});else try{new RegExp(o)}catch(a){t.push({field:"grep",problem:`is not a valid regular expression: ${a.message}`,received:o})}n.body!==void 0&&typeof n.body!="boolean"&&t.push({field:"body",problem:"must be a boolean",received:n.body});const i=ce(T(),n.type,t);n.kind!==void 0&&typeof n.kind!="string"&&t.push({field:"kind",problem:"must be a string",received:n.kind}),n.owner!==void 0&&typeof n.owner!="string"&&t.push({field:"owner",problem:"must be a string",received:n.owner});const s=le(n.limit,"limit",t,50);return t.length>0&&$("object search request",t),y(a=>{const c=g(a,e),f=typeof n.owner=="string"?ie(a,c,n.owner):null;if(typeof n.owner=="string"&&f===null)return{items:[]};const h=K(a,c,{text:r,body:n.body===!0||typeof o=="string"}),p=new Set(h.map(d=>d.object.rowid));return{items:(typeof o=="string"?K(a,c,{text:o,grep:!0}).filter(d=>p.has(d.object.rowid)):h).filter(d=>(!i||d.object.type===i)&&(n.kind===void 0||d.object.kind===n.kind)&&(!f||d.object.owner===f.rowid)).slice(0,s).map(d=>({object:oe(a,d.object),...d.line!==void 0?{matches:[{line:d.line,text:d.text??""}]}:{}}))}})}u(Ce,"search");const he=["interrupt","steer","after-turn","next-turn"],Fe=/^(\*|>|[A-Za-z0-9_-]+)(\.(\*|>|[A-Za-z0-9_-]+))*$/;function Me(e,t){if(e!==void 0){if(!Array.isArray(e)||e.length===0||e.some(n=>typeof n!="string"||!Fe.test(n)||n.includes(">")&&!n.endsWith(">"))){t.push({field:"patterns",problem:"must be a non-empty list of event patterns (dot-separated words, `*` for one word, a final `>` for the rest)",received:e});return}return e}}u(Me,"patternsOf");function We(e,t){if(e===void 0)return;if(!k(e)||Object.keys(e).length===0){t.push({field:"config",problem:"must map levels 1-10 to a delivery",received:e});return}const n=[];for(const[r,o]of Object.entries(e)){const i=Number(r);!Number.isInteger(i)||i<1||i>10?t.push({field:`config.${r}`,problem:"level must be an integer from 1 to 10"}):he.includes(o)?n.push({threshold:i,delivery:o}):t.push({field:`config.${r}`,problem:`delivery must be one of ${he.join(", ")}`,received:o})}return n.sort((r,o)=>o.threshold-r.threshold)}u(We,"configOf");function pe(e){const t=ae(e);if(t!==null)throw new w(400,"invalid_request",`${t} lives on another machine; an explicit watcher cannot watch it from here`,{id:t})}u(pe,"refuseStubWatch");async function De(e){const t=_(e);if(typeof(k(e.body)?e.body:{}).watcher=="string"&&pe(t),typeof(k(e.body)?e.body:{}).watcher!="string"){const n=await W(e,t,{method:"POST",path:`${G(t)}/watch`,body:e.body},()=>D(t,e));if(n.kind==="unknown")throw te(t,n.unreachable);if(n.kind==="remote"){if(n.status>=400)throw C(n.status,n.body);const r=n.body.patterns;return ue(e,n.body,n.machine.id,"ask",r),F(n.body,n.machine)}}return Ge(e)}u(De,"watch");function Ge(e){const t=[],n=e.body===void 0?{}:k(e.body)?e.body:(t.push({field:"body",problem:"must be an object"}),{}),r=Me(n.patterns,t),o=We(n.config,t);return n.filter!==void 0&&!k(n.filter)&&t.push({field:"filter",problem:"must be an object",received:n.filter}),n.watcher!==void 0&&typeof n.watcher!="string"&&t.push({field:"watcher",problem:"must be a custom-object ref",received:n.watcher}),t.length>0&&$("watch request",t),y(i=>{const s=typeof n.watcher=="string"?H(i,n.watcher):null;if(e.publishToken&&!s)throw new w(404,"not_found","watcher not found");if(e.principal.class==="anonymous"&&!s)throw new w(404,"not_found","watcher not found");const a=s?B(i,e,s):N(i,e)??V(g(i,e));s&&Z(i,e,s);const c=s?A(i,s):g(i,e),f=s?(()=>{const p=R(i,c,_(e));if(!ge(i,c,p))throw new w(404,"not_found","object not found");return p})():R(i,c,_(e)),h=r??[">"];return Q(i,s??a,f,s?"act":"ask",{patterns:h,...o?{config:o}:{},...n.filter!==void 0?{filter:n.filter}:{}}),ke(i,{event:"watched",object:f,actor:a,attrs:{}},M(i)),{object:b(i,f),patterns:h}})}u(Ge,"watchLocal");async function ze(e){const t=_(e);if(e.query.get("watcher")&&pe(t),!e.query.get("watcher")){const n=await W(e,t,{method:"DELETE",path:`${G(t)}/watch`,query:P(e)},()=>D(t,e));if(n.kind==="unknown")throw te(t,n.unreachable);if(n.kind==="remote"){if(n.status>=400)throw C(n.status,n.body);const r=n.body.object?.id;return y(o=>{const i=X(o,e),s=r===void 0?null:ne(o,r);i!==null&&s!==null&&re(o,s)&&Y(o,i,s,e.query.get("pattern")??void 0)}),F(n.body,n.machine)}}return Ue(e)}u(ze,"unwatchRoute");function Ue(e){const t=e.query.get("pattern"),n=e.query.get("watcher");return y(r=>{const o=n?H(r,n):null;if((e.publishToken||e.principal.class==="anonymous")&&!o)throw new w(404,"not_found","watcher not found");const i=o?B(r,e,o):N(r,e)??V(g(r,e));o&&Z(r,e,o);const s=o?A(r,o):g(r,e),a=R(r,s,_(e)),c=u(()=>ye(r,`SELECT count(*) AS n FROM watch_subscriptions s JOIN watches w ON w.rowid = s.watch
|
|
2
|
+
WHERE w.watcher = ? AND w.object = ?`).get((o??i).rowid,a.rowid).n,"count"),f=c();return Y(r,o??i,a,t??void 0),{object:b(r,a),removed:f-c()}})}u(Ue,"unwatchLocal");function Ve(e){const t=[],n=z(e.query.get("type"),"type",["acted-on","links-to","watches"],t),r=z(e.query.get("direction"),"direction",["out","in"],t);return t.length>0&&$("edges request",t),y(o=>{const i=g(o,e),s=R(o,i,_(e)),a=[];if(n!=="watches")for(const c of je(o,i,s,{...n?{type:n}:{},direction:r??"both"}))a.push({type:c.type,src:b(o,c.src),dst:b(o,c.dst),...c.written_as?{written_as:c.written_as}:{}});if(n===void 0||n==="watches"){const c=[...r!=="in"?Re(o,i,s):[],...r!=="out"?qe(o,i,s):[]];for(const f of c)a.push({type:"watches",src:b(o,f.watcher),dst:b(o,f.object)})}return{items:a}})}u(Ve,"edges");const O=u(e=>async t=>({status:200,body:await e(t)}),"ok"),ht=[{method:"POST",pattern:"/v1/objects/search",handler:O(Ce)},{method:"GET",pattern:"/v1/objects/:ref/edges",handler:O(Ve)},{method:"POST",pattern:"/v1/objects/:ref/watch",handler:O(De)},{method:"DELETE",pattern:"/v1/objects/:ref/watch",handler:O(ze)},{method:"GET",pattern:"/v1/objects/:ref",handler:O(Ie)},{method:"GET",pattern:"/v1/objects",handler:O(Pe)}];export{Ae as listBudget,ht as objectRoutes};
|
|
@@ -34,6 +34,8 @@ export interface ProviderCallInput {
|
|
|
34
34
|
version?: number;
|
|
35
35
|
/** Outside a run, the answered approval question the repeat names (R5). */
|
|
36
36
|
approval?: string;
|
|
37
|
+
/** Which device machine takes the call (a `machine_id`, `runtime:` prefix optional); only a provider served by a machine accepts it. */
|
|
38
|
+
machine?: string;
|
|
37
39
|
}
|
|
38
40
|
export declare function parseProvider(value: unknown): string;
|
|
39
41
|
export declare function parseCallInput(body: unknown): ProviderCallInput;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var E=Object.defineProperty;var s=(e,r)=>E(e,"name",{value:r,configurable:!0});import{randomUUID as P}from"node:crypto";import{readConfig as I}from"../../../core/config.js";import{activeGrantForApp as R,localGrants as
|
|
1
|
+
var E=Object.defineProperty;var s=(e,r)=>E(e,"name",{value:r,configurable:!0});import{randomUUID as P}from"node:crypto";import{readConfig as I}from"../../../core/config.js";import{activeGrantForApp as R,localGrants as b}from"../../../core/grants/mirror.js";import{getNode as O}from"../../../core/canvas/canvas.js";import{scopeAllowed as $}from"../../../core/scopes.js";import{ProviderError as f,providerErrorBody as q,scopeMissing as x}from"../../../core/providers/errors.js";import{callProvider as G,providerTools as N}from"../../../core/providers/call.js";import{providerIdsFromGrants as T,providerInstallState as F}from"../../../core/providers/bundles.js";import{listInstalledPlugins as j}from"../../../core/resolver.js";import{listAudit as C,writeProviderAudit as D}from"../../../core/providers/audit.js";import{emitEvent as w}from"../../../core/events/emit.js";import{resolvedScopes as _}from"../router.js";const fe=new Set(["provider.tools","provider.call","audit.list"]);function M(e){if(e.class==="forwarded"||e.class==="machine")throw new f("permission_error","this operation is not available to the caller");if(e.class==="person")return{class:"person",sub:null,grantee:null,client_id:null,resolved:null,nodeScopes:null,run_id:null,node_id:null};if(e.class==="app"){const r=R(e.app);if(!r)throw new f("grant_revoked",`app ${e.app} has no active grant`);return{class:"app",sub:r.sub,grantee:e.app,client_id:null,resolved:r.resolved,nodeScopes:e.scopes,run_id:null,node_id:null}}return e.class==="cron"?{class:"app",sub:e.sub,grantee:e.app,client_id:null,resolved:_(e),nodeScopes:e.scopes,run_id:e.origin_run,node_id:null}:{class:"broker",sub:e.sub,grantee:e.app,client_id:e.client_id,resolved:_(e),nodeScopes:e.scopes,run_id:e.run??O(e.node)?.run_id??null,node_id:e.node}}s(M,"callerFromRequest");function ce(e){return{class:"app",sub:e.sub,grantee:e.grantee,client_id:e.client_id,resolved:e.resolved,nodeScopes:null,run_id:null,node_id:null}}s(ce,"callerFromApp");const U=/^([A-Za-z0-9_-]{1,64}):([A-Za-z0-9_-]{1,64})$/;function k(e){const r=U.exec(e);return r!==null&&r[1]!=="crtr"}s(k,"isProviderScope");const g=new WeakMap;function z(e){let r=g.get(e);return r===void 0&&(r=new Set(e),g.set(e,r)),r}s(z,"heldSet");function y(e,r){return e===null?!0:k(r)?z(e).has(r):$(e,r)}s(y,"holds");function S(e,r){return e.class==="person"?!0:y(e.resolved??[],r)&&y(e.nodeScopes,r)}s(S,"callerAllows");function me(e,r,t){const n=`${r}:${t}`;if(!S(e,n))throw x(n)}s(me,"requireGroup");function L(){return!!I("user").runtime?.issuer}s(L,"providerOperationsEnabled");const V=/^[A-Za-z0-9_-]{1,64}$/;function c(e,r){return new f("invalid_request",e,{param:r})}s(c,"invalid");function A(e){if(typeof e!="string"||!V.test(e)||e==="crtr")throw c("provider must be a provider id","provider");return e}s(A,"parseProvider");function Z(e){if(!e||typeof e!="object"||Array.isArray(e))throw new f("invalid_request","a JSON object is required");const r=e,t=A(r.provider),n=r.tool;if(typeof n!="string"||!n.trim())throw c("tool must be a non-empty string","tool");const i=r.arguments??{};if(!i||typeof i!="object"||Array.isArray(i))throw c("arguments must be an object","arguments");const d=r.version;if(d!==void 0&&(typeof d!="number"||!Number.isInteger(d)||d<1))throw c("version must be an integer >= 1","version");const o=r.approval;if(o!==void 0&&(typeof o!="string"||!o))throw c("approval must be a question id","approval");const l=r.machine;if(l!==void 0&&(typeof l!="string"||!l.trim()))throw c("machine must be a machine id","machine");return{provider:t,tool:n.trim(),arguments:i,...d===void 0?{}:{version:d},...o===void 0?{}:{approval:o},...l===void 0?{}:{machine:l.trim()}}}s(Z,"parseCallInput");function B(e,r=new URLSearchParams){const t=s((o,l,m)=>{const u=r.get(o);if(u===null)return l;if(!/^\d+$/.test(u)||Number(u)>m||Number(u)<1)throw c(`invalid ${o}`,o);return Number(u)},"numeric"),n=r.has("after")?t("after",0,Number.MAX_SAFE_INTEGER):void 0,i=t("limit",100,1e3),d=Object.fromEntries(["operation","outcome","run_id","since","until"].filter(o=>r.has(o)).map(o=>[o,r.get(o)]));return{status:200,body:C(e.class==="person"?null:e.grantee,{after:n,limit:i,...d})}}s(B,"auditList");async function H(e,r,t){const n=performance.now(),i=new Set(j("user").filter(a=>a.enabled&&a.manifest.provider?.id===a.name).map(a=>a.name)),d=T(b()),o=[...new Set([...i,...d])].sort();if(r!==void 0&&!o.includes(r))throw new f("not_found",`provider "${r}" is not available`);const l=r===void 0?o:[r],m=e.class==="person"?b().flatMap(a=>a.status==="active"?a.resolved_scopes.map(({scope:v})=>v):[]):e.resolved??[],u=await Promise.all(l.map(async a=>{if(!m.some(p=>p.startsWith(`${a}:`)&&S(e,p)))return null;if(i.has(a)){const p=await N(e,a);return p.groups.length>0?p:null}const h=F(a);return{provider:a,display_name:a,status:h.status,...h.error?{error:h.error}:{},groups:[],commands:[]}})).then(a=>a.filter(v=>v!==null));if(w({level:"debug",event:"api.provider.tools",duration_ms:performance.now()-n,fields:{caller:e.class,provider:r??null,providers:u.length}}),r!==void 0){if(u.length===0)throw new f("not_found",`provider "${r}" is not available`);return{status:200,body:u[0]}}return{status:200,body:{providers:u}}}s(H,"providerTools");async function W(e,r,t){return G(e,r,t)}s(W,"providerCall");async function J(e,r,t,n){if(!L())throw new f("not_found","provider operations need a directory-connected runtime");if(e==="audit.list")return B(r,t.query);if(e==="provider.tools")return H(r,t.params.provider===void 0?void 0:A(t.params.provider),n);if(e==="provider.call"){let i;try{i=Z(t.body)}catch(d){const o=t.body&&typeof t.body=="object"&&!Array.isArray(t.body)?t.body:{};throw D({request_id:n.requestId,caller:r,provider:typeof o.provider=="string"?o.provider:"",tool:typeof o.tool=="string"?o.tool:"",outcome:`refused:${d instanceof f?d.code:"invalid_request"}`}),d}return W(r,i,n)}throw new f("not_found","unknown operation")}s(J,"handleProviderOperation");function X(e){const r=new AbortController,t=s(()=>{e.writableFinished||r.abort()},"abort");return e.on("close",t),{signal:r.signal,release:s(()=>e.off("close",t),"release")}}s(X,"abortOnClose");function K(e,r,t,n){const i=q(e,r,t,n??void 0);return i.status>=500&&!(e instanceof f)&&w({event:"api.provider.failed",level:"error",error:e instanceof Error?e:new Error(String(e)),fields:{request_id:r}}),i}s(K,"providerFailure");const pe=[{method:"GET",pattern:"/v1/audit",name:"audit.list"},{method:"GET",pattern:"/v1/providers/tools",name:"provider.tools"},{method:"GET",pattern:"/v1/providers/:provider/tools",name:"provider.tools"},{method:"POST",pattern:"/v1/provider/call",name:"provider.call"}].map(({method:e,pattern:r,name:t})=>({method:e,pattern:r,handler:s(async n=>{const i=P(),{signal:d,release:o}=X(n.res);let l=null;try{return l=M(n.principal),{...await J(t,l,{params:n.params,body:n.body,query:new URL(n.req.url??"/","http://runtime.local").searchParams},{requestId:i,signal:d}),headers:{"Request-Id":i}}}catch(m){return K(m,i,!1,l?.run_id)}finally{o()}},"handler")}));export{fe as PROVIDER_OPERATIONS,X as abortOnClose,S as callerAllows,ce as callerFromApp,M as callerFromRequest,J as handleProviderOperation,Z as parseCallInput,A as parseProvider,K as providerFailure,L as providerOperationsEnabled,pe as providerRoutes,me as requireGroup};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var T=Object.defineProperty;var f=(
|
|
1
|
+
var T=Object.defineProperty;var f=(t,d)=>T(t,"name",{value:d,configurable:!0});import{openDb as I,withCanvasWrite as L}from"../../core/canvas/db.js";import{emitEvent as R}from"../../core/events/emit.js";import{isUnreachable as A,peerCall as j}from"../../core/peer/client.js";import{listMachines as B,machineReach as C,normalizeMachineId as k,ownMachineId as D}from"../../core/peer/directory.js";import{relayConnected as U}from"../../core/peer/events.js";import{ingestPeerEvents as F}from"../../core/peer/ingest.js";import{dueRows as N,expireOld as W,markDone as q,markExpired as O,markRetry as x,markWaiting as S,onPeerEnqueued as X,pendingFor as Y,pruneOutbox as z,OUTBOX_MAX_AGE_MS as G}from"../../core/peer/outbox-store.js";const g={first:1e3,max:10*6e4},E=5*6e4,H=5*6e4,P=50,J=20,K={db:I,call:f((t,d)=>j(t,d),"call"),reach:C,machines:B,self:D,now:Date.now},v=f(t=>Math.min(g.first*2**Math.max(0,t-1),g.max),"stableBackoffMs");function V(t){const d=new Set;async function p(s,a,o){const e=t.db(),n=t.now(),l=await t.call(s,{method:"POST",path:"/v1/peer/events",body:{events:a.map(r=>JSON.parse(r.payload_json))}}),m=f(r=>{for(const c of a){const h=c.attempts+1,_=c.first_failed_at??n;o&&r?S(e,c.id,h):o?n-_>=E?S(e,c.id,h):x(e,c.id,h,n+Math.min(v(h),E-(n-_)),_):n-_>=G?O(e,c.id):x(e,c.id,h,n+v(h),_)}},"fail");if(A(l)){m(l.unreachable.reason==="machine_offline");return}const i=l.body;if(l.status!==200||i===null||!Array.isArray(i.accepted)){m(!1);return}const y=new Set(i.accepted),b=new Set((Array.isArray(i.rejected)?i.rejected:[]).map(r=>r.key));for(const r of a)y.has(r.idempotency_key)?q(e,r.id):b.has(r.idempotency_key)?O(e,r.id):x(e,r.id,r.attempts+1,n+v(r.attempts+1),r.first_failed_at??n)}f(p,"sendBatch");async function w(s){const a=s.filter(o=>!d.has(o.id));for(const o of a)d.add(o.id);try{const o=new Map;for(const e of a)o.set(e.peer,[...o.get(e.peer)??[],e]);await Promise.all([...o].map(async([e,n])=>{const m=(await t.reach(e).catch(()=>null))?.reach.kind==="relay";for(let i=0;i<n.length;i+=P)await p(e,n.slice(i,i+P),m)}))}finally{for(const o of a)d.delete(o.id)}}f(w,"pushRows");let u=0;return{async run(){const s=t.db(),a=t.now();a-u>=60*6e4&&(u=a,W(s,a),z(s,a)),await w(N(s,a))},flush:f(s=>w(Y(t.db(),s,t.now())),"flush"),async pull(){const s=t.self(),a=(await t.machines()).filter(e=>k(e.machine_id)!==s&&e.kind!=="device"&&e.reach.kind==="stable");let o=0;for(const e of a){const n=k(e.machine_id);for(let l=0;l<J;l++){const m=await t.call(n,{method:"POST",path:"/v1/peer/outbox/claim",body:{limit:100}}),i=!A(m)&&m.status===200?m.body?.rows:void 0;if(!Array.isArray(i)||i.length===0)break;const y=L(c=>F(c,n,i.map(h=>h.payload)));o+=y.accepted.length;const b=new Set([...y.accepted,...y.rejected.map(c=>c.key)]),r=i.filter(c=>b.has(c.key)).map(c=>({key:c.key,claim_token:c.claim_token}));if(r.length>0&&await t.call(n,{method:"POST",path:"/v1/peer/outbox/ack",body:{acks:r}}),i.length<100)break}}return o}}}f(V,"createOutboxLane");function re(t){if(!t.joined)return()=>{};const d=t.deps??K,p=V(d),w=f(n=>l=>{R({level:"warn",event:`daemon.peer.${n}_failed`,error:l})},"quiet"),u=f((n,l)=>t.register(n().then(()=>{},w(l))),"guarded"),M=X(n=>u(()=>p.flush(n),"outbox_flush")),s=setInterval(()=>u(()=>p.run(),"outbox_run"),t.tickMs??5e3);s.unref();const a=f(()=>u(async()=>{(await d.machines()).find(l=>k(l.machine_id)===d.self())?.reach.kind==="relay"&&await p.pull()},"outbox_pull"),"pull"),o=U.on(a),e=setInterval(a,H);return e.unref(),a(),()=>{M(),o(),clearInterval(s),clearInterval(e)}}f(re,"startPeerOutbox");export{H as PULL_INTERVAL_MS,E as RELAY_RETRY_WINDOW_MS,g as STABLE_BACKOFF_MS,V as createOutboxLane,K as liveOutboxDeps,v as stableBackoffMs,re as startPeerOutbox};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var I=Object.defineProperty;var o=(_,e)=>I(_,"name",{value:e,configurable:!0});import{request as Y}from"node:http";import{decodeJwt as D}from"jose";import{WebSocket as R}from"ws";import{emitEvent as b}from"../../core/events/emit.js";import{relayConnected as q}from"../../core/peer/events.js";import{peerTokens as j}from"../../core/peer/token.js";import{parseServerFrame as z,RELAY_CLOSE as B,RELAY_MAX_BODY_BYTES as C,RELAY_PROTOCOL_VERSION as H}from"./relay-protocol.js";const J=1e3,K=6e4,U=6e4,X=6e4,V=1e4,W=["authorization","crtr-envelope","crtr-version","content-type"];function G(_){const e=new URL("/v1/connect",_);return e.protocol=e.protocol==="http:"?"ws:":"wss:",e.toString()}o(G,"relayConnectUrl");const Q=o(_=>{const e={host:"peer.local"};for(const E of W){const p=_[E];p!==void 0&&(e[E]=p)}return e},"pick");function se(_){const e={backoffMinMs:J,backoffMaxMs:K,refreshLeadMs:U,silenceLimitMs:X,helloTimeoutMs:V,tokens:j,connect:o(r=>new R(r,{maxPayload:Math.ceil(C*4/3)+65536}),"connect"),..._},E=G(e.relayOrigin);let p=!1,T,O=!1,k=e.backoffMinMs,m,v=0,L=!1;const h=new Map;function A(){if(m=void 0,p)return;const r=++v;(async()=>{let i;try{i=await e.tokens.get(e.issuer,e.relayOrigin)}catch(n){return b({level:"warn",event:"daemon.peer.relay_token_failed",error:n instanceof Error?n:new Error(String(n))}),x(r)}p||r!==v||N(i,r)})()}o(A,"dial");function x(r,i){if(p||r!==v||m!==void 0)return;const n=i??k;k=Math.min(k*2,e.backoffMaxMs),m=setTimeout(A,n),m.unref()}o(x,"schedule");function N(r,i){const n=e.connect(E);T=n;let l,a,f,u=!1,g=!1;const M=o(t=>{n.readyState===R.OPEN&&n.send(JSON.stringify(t))},"send"),d=o(()=>{clearTimeout(f),f=setTimeout(()=>{b({level:"warn",event:"daemon.peer.relay_silent"}),n.terminate()},e.silenceLimitMs),f.unref()},"heard"),w=o((t,c)=>{if(g)return;g=!0,clearTimeout(l),clearTimeout(a),clearTimeout(f);for(const[,S]of h)S.abort();h.clear(),T===n&&(T=void 0),O=!1,u?b({level:"info",event:"daemon.peer.relay_disconnected",fields:{code:t,reason:c}}):b({level:"warn",event:"daemon.peer.relay_connect_failed",fields:{code:t,reason:c}}),t===B.unauthorized&&e.tokens.drop(e.issuer,e.relayOrigin);const s=L;L=!1,x(i,s?0:t===B.replaced?e.backoffMaxMs:void 0)},"finish"),y=o(t=>{clearTimeout(a);const c=Math.max(0,t*1e3-e.refreshLeadMs-Date.now());a=setTimeout(()=>{e.tokens.drop(e.issuer,e.relayOrigin),e.tokens.get(e.issuer,e.relayOrigin).then(s=>{if(g)return;M({type:"token",token:s});const S=D(s).exp;y(typeof S=="number"&&S>t?S:t+1)},s=>{b({level:"warn",event:"daemon.peer.relay_refresh_failed",error:s instanceof Error?s:new Error(String(s))}),g||(clearTimeout(a),a=setTimeout(()=>y(t),5e3),a.unref())})},c),a.unref()},"planRefresh");n.on("open",()=>{M({type:"hello",protocol:H,token:r}),l=setTimeout(()=>n.terminate(),e.helloTimeoutMs),l.unref(),d()}),n.on("message",(t,c)=>{if(d(),c)return;const s=z(Array.isArray(t)?Buffer.concat(t).toString("utf8"):Buffer.from(t).toString("utf8"));s!==null&&F(s)}),n.on("close",(t,c)=>w(t,c.toString("utf8"))),n.on("error",t=>{b({level:"debug",event:"daemon.peer.relay_socket_error",error:t}),w(1006,"error");try{n.terminate()}catch{}});function F(t){switch(t.type){case"hello_ok":{clearTimeout(l),u=!0,O=!0,k=e.backoffMinMs,y(t.expires_at),b({level:"info",event:"daemon.peer.relay_connected",fields:{relay:e.relayOrigin,machine_id:t.machine_id}}),q.emit();return}case"ping":M({type:"pong"});return;case"pong":return;case"cancel":h.get(t.id)?.abort(),h.delete(t.id);return;case"request":{if(!u)return;const c=new AbortController;h.set(t.id,c),P(t,c.signal).then(s=>{h.delete(t.id),c.signal.aborted||M({type:"response",id:t.id,...s})});return}}}o(F,"handle")}o(N,"run");function P(r,i){const n=o((l,a,f)=>({status:l,headers:{"content-type":"application/json"},body_b64:Buffer.from(JSON.stringify({error:{code:a,message:f}})).toString("base64")}),"fail");return new Promise(l=>{const a=Buffer.from(r.body_b64,"base64"),f=Y({socketPath:e.socketPath,method:r.method,path:r.path,headers:{...Q(r.headers),"content-length":String(a.byteLength)},signal:i},u=>{const g=[];let M=0;u.on("data",d=>{if(M+=d.byteLength,M>C){l(n(502,"response_too_large","the answer is over
|
|
1
|
+
var I=Object.defineProperty;var o=(_,e)=>I(_,"name",{value:e,configurable:!0});import{request as Y}from"node:http";import{decodeJwt as D}from"jose";import{WebSocket as R}from"ws";import{emitEvent as b}from"../../core/events/emit.js";import{relayConnected as q}from"../../core/peer/events.js";import{peerTokens as j}from"../../core/peer/token.js";import{parseServerFrame as z,RELAY_CLOSE as B,RELAY_MAX_BODY_BYTES as C,RELAY_PROTOCOL_VERSION as H}from"./relay-protocol.js";const J=1e3,K=6e4,U=6e4,X=6e4,V=1e4,W=["authorization","crtr-envelope","crtr-version","content-type"];function G(_){const e=new URL("/v1/connect",_);return e.protocol=e.protocol==="http:"?"ws:":"wss:",e.toString()}o(G,"relayConnectUrl");const Q=o(_=>{const e={host:"peer.local"};for(const E of W){const p=_[E];p!==void 0&&(e[E]=p)}return e},"pick");function se(_){const e={backoffMinMs:J,backoffMaxMs:K,refreshLeadMs:U,silenceLimitMs:X,helloTimeoutMs:V,tokens:j,connect:o(r=>new R(r,{maxPayload:Math.ceil(C*4/3)+65536}),"connect"),..._},E=G(e.relayOrigin);let p=!1,T,O=!1,k=e.backoffMinMs,m,v=0,L=!1;const h=new Map;function A(){if(m=void 0,p)return;const r=++v;(async()=>{let i;try{i=await e.tokens.get(e.issuer,e.relayOrigin)}catch(n){return b({level:"warn",event:"daemon.peer.relay_token_failed",error:n instanceof Error?n:new Error(String(n))}),x(r)}p||r!==v||N(i,r)})()}o(A,"dial");function x(r,i){if(p||r!==v||m!==void 0)return;const n=i??k;k=Math.min(k*2,e.backoffMaxMs),m=setTimeout(A,n),m.unref()}o(x,"schedule");function N(r,i){const n=e.connect(E);T=n;let l,a,f,u=!1,g=!1;const M=o(t=>{n.readyState===R.OPEN&&n.send(JSON.stringify(t))},"send"),d=o(()=>{clearTimeout(f),f=setTimeout(()=>{b({level:"warn",event:"daemon.peer.relay_silent"}),n.terminate()},e.silenceLimitMs),f.unref()},"heard"),w=o((t,c)=>{if(g)return;g=!0,clearTimeout(l),clearTimeout(a),clearTimeout(f);for(const[,S]of h)S.abort();h.clear(),T===n&&(T=void 0),O=!1,u?b({level:"info",event:"daemon.peer.relay_disconnected",fields:{code:t,reason:c}}):b({level:"warn",event:"daemon.peer.relay_connect_failed",fields:{code:t,reason:c}}),t===B.unauthorized&&e.tokens.drop(e.issuer,e.relayOrigin);const s=L;L=!1,x(i,s?0:t===B.replaced?e.backoffMaxMs:void 0)},"finish"),y=o(t=>{clearTimeout(a);const c=Math.max(0,t*1e3-e.refreshLeadMs-Date.now());a=setTimeout(()=>{e.tokens.drop(e.issuer,e.relayOrigin),e.tokens.get(e.issuer,e.relayOrigin).then(s=>{if(g)return;M({type:"token",token:s});const S=D(s).exp;y(typeof S=="number"&&S>t?S:t+1)},s=>{b({level:"warn",event:"daemon.peer.relay_refresh_failed",error:s instanceof Error?s:new Error(String(s))}),g||(clearTimeout(a),a=setTimeout(()=>y(t),5e3),a.unref())})},c),a.unref()},"planRefresh");n.on("open",()=>{M({type:"hello",protocol:H,token:r}),l=setTimeout(()=>n.terminate(),e.helloTimeoutMs),l.unref(),d()}),n.on("message",(t,c)=>{if(d(),c)return;const s=z(Array.isArray(t)?Buffer.concat(t).toString("utf8"):Buffer.from(t).toString("utf8"));s!==null&&F(s)}),n.on("close",(t,c)=>w(t,c.toString("utf8"))),n.on("error",t=>{b({level:"debug",event:"daemon.peer.relay_socket_error",error:t}),w(1006,"error");try{n.terminate()}catch{}});function F(t){switch(t.type){case"hello_ok":{clearTimeout(l),u=!0,O=!0,k=e.backoffMinMs,y(t.expires_at),b({level:"info",event:"daemon.peer.relay_connected",fields:{relay:e.relayOrigin,machine_id:t.machine_id}}),q.emit();return}case"ping":M({type:"pong"});return;case"pong":return;case"cancel":h.get(t.id)?.abort(),h.delete(t.id);return;case"request":{if(!u)return;const c=new AbortController;h.set(t.id,c),P(t,c.signal).then(s=>{h.delete(t.id),c.signal.aborted||M({type:"response",id:t.id,...s})});return}}}o(F,"handle")}o(N,"run");function P(r,i){const n=o((l,a,f)=>({status:l,headers:{"content-type":"application/json"},body_b64:Buffer.from(JSON.stringify({error:{code:a,message:f}})).toString("base64")}),"fail");return new Promise(l=>{const a=Buffer.from(r.body_b64,"base64"),f=Y({socketPath:e.socketPath,method:r.method,path:r.path,headers:{...Q(r.headers),"content-length":String(a.byteLength)},signal:i},u=>{const g=[];let M=0;u.on("data",d=>{if(M+=d.byteLength,M>C){l(n(502,"response_too_large","the answer is over 16 MiB")),f.destroy();return}g.push(d)}),u.on("end",()=>{const d={};for(const[w,y]of Object.entries(u.headers))typeof y=="string"?d[w]=y:Array.isArray(y)&&(d[w]=y.join(", "));l({status:Math.min(599,Math.max(200,u.statusCode??502)),headers:d,body_b64:Buffer.concat(g).toString("base64")})}),u.on("error",()=>l(n(502,"peer_ingress_failed","the peer ingress dropped the answer")))});f.on("error",()=>l(n(502,"peer_ingress_failed","the peer ingress could not be reached"))),f.end(a)})}return o(P,"replay"),A(),{connected:o(()=>O,"connected"),reconnectNow(){if(p)return;clearTimeout(m),m=void 0,k=e.backoffMinMs;const r=T;if(r!==void 0){L=!0,r.terminate();return}A()},async stop(){p=!0,clearTimeout(m),m=void 0,v++;for(const[,i]of h)i.abort();h.clear();const r=T;T=void 0,O=!1,r!==void 0&&await new Promise(i=>{const n=setTimeout(()=>{r.terminate(),i()},1e3);n.unref(),r.once("close",()=>{clearTimeout(n),i()});try{r.close(1e3,"going_away")}catch{r.terminate()}})}}}o(se,"startRelayClient");export{K as BACKOFF_MAX_MS,J as BACKOFF_MIN_MS,V as HELLO_TIMEOUT_MS,X as SILENCE_LIMIT_MS,U as TOKEN_REFRESH_LEAD_MS,G as relayConnectUrl,se as startRelayClient};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare const RELAY_PROTOCOL_VERSION = 1;
|
|
2
2
|
/** Largest request or response body, in bytes before base64. */
|
|
3
|
-
export declare const RELAY_MAX_BODY_BYTES
|
|
3
|
+
export declare const RELAY_MAX_BODY_BYTES: number;
|
|
4
4
|
/** WebSocket close codes the relay uses. */
|
|
5
5
|
export declare const RELAY_CLOSE: {
|
|
6
6
|
readonly badFrame: 4000;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var o=Object.defineProperty;var n=(t,e)=>o(t,"name",{value:e,configurable:!0});const c=1,l=
|
|
1
|
+
var o=Object.defineProperty;var n=(t,e)=>o(t,"name",{value:e,configurable:!0});const c=1,l=16777216,s={badFrame:4e3,unauthorized:4001,replaced:4002,tokenExpired:4003},r=n(t=>typeof t=="string","isString"),p=n(t=>typeof t=="object"&&t!==null&&!Array.isArray(t)&&Object.values(t).every(r),"isHeaders");function u(t){let e;try{e=JSON.parse(t)}catch{return null}if(typeof e!="object"||e===null)return null;switch(e.type){case"hello_ok":return r(e.machine_id)&&typeof e.expires_at=="number"&&typeof e.heartbeat_s=="number"&&typeof e.protocol=="number"?e:null;case"request":return r(e.id)&&r(e.method)&&r(e.path)&&p(e.headers)&&r(e.body_b64)?e:null;case"cancel":return r(e.id)?{type:"cancel",id:e.id}:null;case"ping":return{type:"ping"};case"pong":return{type:"pong"};default:return null}}n(u,"parseServerFrame");export{s as RELAY_CLOSE,l as RELAY_MAX_BODY_BYTES,c as RELAY_PROTOCOL_VERSION,u as parseServerFrame};
|
package/dist/types.d.ts
CHANGED
|
@@ -23,6 +23,10 @@ export type PluginTransport = {
|
|
|
23
23
|
kind: 'http';
|
|
24
24
|
endpoint: string;
|
|
25
25
|
authEnv?: string;
|
|
26
|
+
}
|
|
27
|
+
/** A provider whose calls go to one of the account's `kind = device` machines over the relay; there is no endpoint. */
|
|
28
|
+
| {
|
|
29
|
+
kind: 'machine';
|
|
26
30
|
};
|
|
27
31
|
/** Provenance for a crouter-materialized HTTP plugin directory archive. */
|
|
28
32
|
export interface PluginBundle {
|
package/docs/ci-runner.md
CHANGED
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
# CI runners and the double-run gate
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The suite runs on **GitHub-hosted Linux** (ubuntu, inside a root container shaped like the hosted guest), and only the tests that are about the laptop runtime run on **macOS** (the Mac mini runners). macOS minutes bill at 10x Linux, so the macOS lane is deliberately small. The push to `main` does not re-run a tree that already passed.
|
|
4
|
+
|
|
5
|
+
## Why Linux, and why a container
|
|
6
|
+
|
|
7
|
+
On Linux crouter assumes it is the hosted guest (`src/core/layout.ts`): sockets in `/run/crtr`, data under `/runtime`, `/user` and `/apps`, brokers started through a root launcher. A plain non-root runner has none of that (every lane failed with EACCES, which is why the suite once moved to macOS). The `linux` jobs of `test.yml` therefore run as root in a container built from `docker/ci.Dockerfile`:
|
|
8
|
+
|
|
9
|
+
- **Image.** `docker/ci.Dockerfile` is the guest's runner stage (same pinned `node:24-alpine` digest, same apk list, users `crtrd` 10002 / `crtr-data` 10001, the `/run/crtr` `/runtime` `/user` `/apps` directories) plus `build-base`, `linux-headers` (native addon) and GNU `tar`. It does not contain crouter; the job checks the source out. Chosen over `hosting/guest/Dockerfile` (needs sandbox-api, chrony, the python stack and a packed crouter, so it rebuilds per commit) and `docker/unit.Dockerfile` (bakes a packed crouter). `scripts/ci-image.test.mjs` (in `test:contract`) fails when the apk list, ids, directories or node base drift from `docker/base.Dockerfile`; keep the two in step by hand when the guest changes.
|
|
10
|
+
- **Capabilities.** `--cap-add SYS_ADMIN --cap-add NET_ADMIN --security-opt seccomp=unconfined --security-opt apparmor=unconfined`: mount namespaces per test file and bubblewrap's namespaces.
|
|
11
|
+
- **One `/run/crtr` per test file.** All test files would otherwise share `/run/crtr/api.sock`, `store.sock` and `nodes/`. `scripts/ci-test-files.mjs` runs each file under `unshare -m` with a tmpfs on `/run/crtr`, and its own `HOME`, `TMPDIR`, `CRTR_RUNTIME_HOME` and `CRTR_HOME`. A failing file is retried once and reported as flaky if the retry passes (as `scripts/test-retry.mjs` does on macOS).
|
|
12
|
+
- **Running a list of files.** `ci-test-files.mjs <lane> --files <list>` (and `scripts/ci-linux-lane.sh <lane> <shard> --files <list>`) runs exactly the files in a text file of repo paths, one per line (seam: the `dist/…/*.test.js` path); the format is in the script header. A path that is not a test file of that lane exits 2.
|
|
13
|
+
- **Flaky recording.** A file that fails and then passes on the rerun is written to `flaky.json`, which each `linux` and `macos` job uploads as artifact `flaky-<attempt>-<platform>-<lane>-<job index>`. `record-flaky-tests.yml` runs on every completed Test or Publish run (on main the suite is Publish's `tests` job) and `scripts/record-flaky-tests.mjs` files one `flaky-test` issue per lane, file and failing test, or comments on the existing one.
|
|
14
|
+
- **No real launcher.** The root launcher starts `crtrd` on the real `/runtime` and flips code paths on its socket, so it cannot run in the test container. Tests that need it are tagged `laptopOnly` and run on macOS.
|
|
15
|
+
|
|
16
|
+
## The `laptopOnly` tag
|
|
17
|
+
|
|
18
|
+
`src/core/__tests__/helpers/platform.ts` exports `laptopOnly(reason)`, which returns `{ skip: 'laptop runtime only (runs on macOS): <reason>' }` on Linux and `{ skip: false }` elsewhere, and `onLaptop(fn)` for the before/after hooks of a file whose tests are all tagged (node:test runs hooks of skipped tests). Pass it to `test`: `test('name', laptopOnly('needs the root launcher'), async () => {…})`. Tagged tests are never deleted; the skip message states why. `scripts/laptop-only-files.mjs <lane>` lists the files carrying tags; the macOS lane runs exactly those files (`scripts/ci-test-files.mjs <lane> --laptop-only`), so adding a tag adds the file to the macOS lane with no list to maintain.
|
|
19
|
+
|
|
20
|
+
Reasons in use: the app/cron/broker process is started through the hosted root launcher; a node-scoped CLI on Linux asks the daemon for its hook plan, which requires the root launcher; Linux brokers start bash jobs through the launcher; root bypasses the `chmod` the test relies on; Linux reads process identities from `/proc`; the branded host and the listener's `userRead` path are macOS-only; the laptop layout resolves `fileRoot` and `operations.db` differently.
|
|
21
|
+
|
|
22
|
+
## Shards and timings
|
|
23
|
+
|
|
24
|
+
Hosted billing rounds each job up to a whole minute and every job pays fixed setup (checkout, image, node_modules, unpack), so shards are few and large: unit 3, integration 2, seam 2. See the PR for measured wall time and billed minutes.
|
|
4
25
|
|
|
5
26
|
## Where the macOS jobs run: `CRTR_CI_RUNNER`
|
|
6
27
|
|
|
7
|
-
Repository variable `CRTR_CI_RUNNER` selects `runs-on` for the `
|
|
28
|
+
Repository variable `CRTR_CI_RUNNER` selects `runs-on` for the `macos` jobs of `test.yml` (the `checks`, `linux`, `suite` and `tested` jobs are always hosted ubuntu):
|
|
8
29
|
|
|
9
30
|
| Value | Runs on |
|
|
10
31
|
| --- | --- |
|
|
@@ -16,7 +37,7 @@ gh variable set CRTR_CI_RUNNER --body self-hosted -R Northlight-Systems/crouter
|
|
|
16
37
|
gh variable delete CRTR_CI_RUNNER -R Northlight-Systems/crouter # fall back to macos-latest
|
|
17
38
|
```
|
|
18
39
|
|
|
19
|
-
It takes effect on the next run; no commit is needed. **If the mini is offline, jobs queue until it returns (they do not fall back by themselves) — delete the variable.** Cancel and re-run anything already queued. The
|
|
40
|
+
It takes effect on the next run; no commit is needed. **If the mini is offline, jobs queue until it returns (they do not fall back by themselves) — delete the variable.** Cancel and re-run anything already queued. The macOS lane is three small jobs (`macos unit`, `macos integration`, `macos seam`) either way.
|
|
20
41
|
|
|
21
42
|
A pull request from a fork always runs on `macos-latest`, whatever the variable says: a self-hosted runner must never execute fork code. The repository is private and forking is disabled today, so this only guards a later change.
|
|
22
43
|
|
|
@@ -48,7 +69,7 @@ Installing and upgrading: get a registration token (`gh api -X POST repos/Northl
|
|
|
48
69
|
|
|
49
70
|
## No double run on `main`
|
|
50
71
|
|
|
51
|
-
`publish.yml` starts with a `gate` job. It reads the pushed commit's **git tree** and looks for a workflow artifact named `tested-tree-<tree>`. `test.yml`'s final `tested` job creates that artifact only after `
|
|
72
|
+
`publish.yml` starts with a `gate` job. It reads the pushed commit's **git tree** and looks for a workflow artifact named `tested-tree-<tree>`. `test.yml`'s final `tested` job creates that artifact only after `checks` and every `lane` shard pass, on every pull request run and on every run of Publish's own `tests` job.
|
|
52
73
|
|
|
53
74
|
- Rebase-merging a PR whose tested merge ref has the same tree as the resulting `main` commit → artifact found → `tests` is skipped and `publish` proceeds.
|
|
54
75
|
- A direct push to `main`, or a merge after `main` moved (a different tree from the one the PR run tested) → no artifact → the full suite runs exactly as before. A release commit (`chore: release …`, pushed by Publish itself) changes the tree, so a PR not updated since before the latest release runs the suite once on merge; update the branch to avoid it.
|
|
@@ -56,3 +77,28 @@ Installing and upgrading: get a registration token (`gh api -X POST repos/Northl
|
|
|
56
77
|
- Branch protection is not enabled on `main` and the gate does not depend on it: the proof is the tree, not a required check. Artifacts are kept 30 days; older trees are simply re-tested.
|
|
57
78
|
|
|
58
79
|
The OIDC constraint at the top of `publish.yml` is unchanged: every publishing job is still in that file.
|
|
80
|
+
|
|
81
|
+
## What runs when (hosted-runner spend)
|
|
82
|
+
|
|
83
|
+
| Event | ubuntu `checks` (build, contract, build-output test) | ubuntu `linux` (unit, integration, seam in the container) | `macos` (laptop-only tests) |
|
|
84
|
+
| --- | --- | --- | --- |
|
|
85
|
+
| pull request push, no `full-ci` label, or draft | yes | no | no |
|
|
86
|
+
| pull request with the `full-ci` label (not a draft) | yes | yes | yes |
|
|
87
|
+
| merge queue run (`merge_group`; inert until a queue ruleset exists) | yes | yes | yes |
|
|
88
|
+
| `workflow_dispatch` (`gh workflow run test.yml --ref <branch>`) | yes | yes | yes |
|
|
89
|
+
| push to `main` (Publish's `tests` job) | yes | yes, unless `gate` found `tested-tree-<tree>` | same |
|
|
90
|
+
|
|
91
|
+
- Add the label when the change is ready for review: `gh pr edit <n> --add-label full-ci`. Remove it while iterating on a red suite. Labels other than `full-ci` start nothing and cancel nothing.
|
|
92
|
+
- The `full suite` job is the single result to require. It fails if `checks` failed, and on every event except a pull request it fails unless every `linux` and `macos` job passed, so Publish (which calls this file) can only release a tree whose full suite passed.
|
|
93
|
+
- `checks` builds once on ubuntu and uploads the JS output; the `linux` jobs unpack it and compile the Linux native addon in the container (the build is glibc, the image musl), and the `macos` jobs unpack it as is.
|
|
94
|
+
|
|
95
|
+
## The selector: `scripts/ci-select.mjs`
|
|
96
|
+
|
|
97
|
+
On a pull request push the selector turns the diff against the merge base into per-lane file lists (`selection.json`, `linux-<lane>-<i>of<n>.txt`, `macos-<lane>.txt`, and the job outputs `mode`, `linux_matrix`, `macos_matrix`; its header lists the contract). Main, merge queue, dispatch, a `full-ci` label and any selector error give mode `full` without a verdict from the diff. "Any" includes a missing or malformed `timing.json`: a `full` selection never depends on it (it falls back to 3 shards per lane) and always writes its outputs.
|
|
98
|
+
|
|
99
|
+
- **Static map.** One esbuild metafile over every test file and the CLI entries, with the dynamic imports of five hub modules (`build-root`, `front-door`, `command`, `setup-marker`, `command-hooks/compose`) not followed; each test's closure is recomputed on every run, nothing is cached.
|
|
100
|
+
- **Spawner rule.** A unit or integration test that spawns the CLI, daemon or a broker (found by scanning its source) depends on the CLI closure, and the whole seam lane runs when a changed file is in that closure.
|
|
101
|
+
- **Path rules** are in `classify()`; anything not named there under `src/` or `packages/` that is not TypeScript, and any unrecognized path, forces `full`. A no-test rule never beats evidence that a test reads the path: a file in some test's or the CLI's import graph selects the tests that reach it even under `scripts/` (e.g. `scripts/proof/stub-provider/*.ts`, reached through the plugin test fixture); a script whose path or file name a test mentions forces `full`; and a `bin/**` shim selects the CLI closure and its spawners plus every test that names the shim (`spawn-env.test.ts` runs `./bin/runtime-generation-manifest.mjs`).
|
|
102
|
+
- **Shard sizing** uses `scripts/ci-select/timing.json`: per-file local work in seconds from one measured local run, times a per-lane factor that scales it to the Linux test-step totals, targeting 240 s per shard. A committed table, not a runtime measurement, because it must exist before the first selected run and cannot come from a run that is being skipped; a file missing from the table (a new test) costs its lane's median, which only skews shard balance. Selection never reads it.
|
|
103
|
+
- `node scripts/ci-select.mjs --replay scripts/ci-select/replay-prs.json` replays the 44 merged PRs #1–#65 through the selector as shipped, path rules included: this is the projection to quote. Over those PRs it runs a Linux lane on 34/44 (full suite: 44/44), at 26.0 billed ubuntu minutes per PR (full suite 39), 3.0 macOS-equivalent cost including the 4 minutes of `checks` (full suite 4.3), 564 s of laptop-only local work on the minis (759), 9.6 hosted-fallback macOS minutes (13); 8 PRs are docs-only and select no lane.
|
|
104
|
+
- `--static-only` is a **counterfactual**, not the selector: it drops every path rule, so a workflow, manifest, helper or unrecognized path selects nothing instead of `full`. It exists only to compare with the design's replay figures, which used the import-graph-plus-spawner model alone (30/44 PRs, 21.9 ubuntu minutes). Do not read its numbers as what the selector does.
|
|
@@ -7,6 +7,12 @@ description: Behavior changes in @north-light/sdk that an app must act on, newes
|
|
|
7
7
|
|
|
8
8
|
Changes to `@north-light/sdk` that can change what an existing app sees. Newest first.
|
|
9
9
|
|
|
10
|
+
## 0.3.455
|
|
11
|
+
|
|
12
|
+
### `provider.call` can target a device (additive, no app change)
|
|
13
|
+
|
|
14
|
+
`provider.call` takes an optional `machine` (a machine id; the `runtime:` prefix is optional) for a provider served by one of the account's devices, such as `crtr device`. Without it, one device is used; with several the call is refused `machine_required` and `details.candidates` lists them. Five error codes are new and are raised only on that path: `machine_required`, `device_offline`, `device_timeout`, `device_disconnected` and `invalid_argument` (`machine` sent to a provider that is not served by a device). A call that sends no `machine` to an ordinary provider sees no change.
|
|
15
|
+
|
|
10
16
|
## 0.3.452
|
|
11
17
|
|
|
12
18
|
### `oauth.client(userId)` no longer offers methods the runtime does not serve (type change)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.455",
|
|
4
4
|
"description": "crtr — agent runtime with memory, plugins, and marketplaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"test:runtime-packaging": "node --test scripts/runtime-smoke.test.mjs scripts/pack-args.test.mjs scripts/ensure-native-linux.test.mjs scripts/release-version.test.mjs",
|
|
100
100
|
"contract:check": "node scripts/contract.mjs check",
|
|
101
101
|
"contract:surface": "node scripts/contract.mjs generate",
|
|
102
|
-
"test:contract": "node --conditions=crtr-src --import tsx/esm --test scripts/contract.test.mjs scripts/promote-sdk.test.mjs scripts/release-version.test.mjs",
|
|
102
|
+
"test:contract": "node --conditions=crtr-src --import tsx/esm --test scripts/contract.test.mjs scripts/promote-sdk.test.mjs scripts/release-version.test.mjs scripts/ci-image.test.mjs scripts/ci-miss-triage.test.mjs",
|
|
103
103
|
"test:build-output": "node scripts/build-concurrency-test.mjs",
|
|
104
104
|
"check:cli-no-canvas": "node scripts/check-cli-no-canvas.mjs",
|
|
105
105
|
"check:cli-cold-start": "node scripts/check-cli-cold-start.mjs",
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
},
|
|
138
138
|
"license": "UNLICENSED",
|
|
139
139
|
"dependencies": {
|
|
140
|
-
"@north-light/identity": "0.3.
|
|
140
|
+
"@north-light/identity": "0.3.455",
|
|
141
141
|
"@earendil-works/pi-agent-core": "1.1.0",
|
|
142
142
|
"@earendil-works/pi-ai": "1.1.0",
|
|
143
143
|
"@earendil-works/pi-coding-agent": "1.1.0",
|
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.455",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/cli",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.455",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "UNLICENSED",
|
|
12
12
|
"workspaces": [
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"@earendil-works/pi-ai": "1.1.0",
|
|
19
19
|
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
20
20
|
"@earendil-works/pi-tui": "1.1.0",
|
|
21
|
-
"@north-light/identity": "0.3.
|
|
21
|
+
"@north-light/identity": "0.3.455",
|
|
22
22
|
"cron-parser": "^5.6.0",
|
|
23
23
|
"esbuild": "^0.27.7",
|
|
24
24
|
"jose": "^6.2.12",
|
|
@@ -12095,17 +12095,17 @@
|
|
|
12095
12095
|
},
|
|
12096
12096
|
"packages/crouter-api": {
|
|
12097
12097
|
"name": "@north-light/api",
|
|
12098
|
-
"version": "0.3.
|
|
12098
|
+
"version": "0.3.455",
|
|
12099
12099
|
"license": "UNLICENSED"
|
|
12100
12100
|
},
|
|
12101
12101
|
"packages/crouter-env-docker": {
|
|
12102
12102
|
"name": "@north-light/env-docker",
|
|
12103
|
-
"version": "0.3.
|
|
12103
|
+
"version": "0.3.455",
|
|
12104
12104
|
"license": "UNLICENSED"
|
|
12105
12105
|
},
|
|
12106
12106
|
"packages/crouter-identity": {
|
|
12107
12107
|
"name": "@north-light/identity",
|
|
12108
|
-
"version": "0.3.
|
|
12108
|
+
"version": "0.3.455",
|
|
12109
12109
|
"license": "UNLICENSED",
|
|
12110
12110
|
"dependencies": {
|
|
12111
12111
|
"jose": "^6.2.1"
|
|
@@ -12113,20 +12113,20 @@
|
|
|
12113
12113
|
},
|
|
12114
12114
|
"packages/crouter-plugin": {
|
|
12115
12115
|
"name": "@north-light/plugin",
|
|
12116
|
-
"version": "0.3.
|
|
12116
|
+
"version": "0.3.455",
|
|
12117
12117
|
"license": "UNLICENSED",
|
|
12118
12118
|
"dependencies": {
|
|
12119
|
-
"@north-light/api": "0.3.
|
|
12120
|
-
"@north-light/identity": "0.3.
|
|
12119
|
+
"@north-light/api": "0.3.455",
|
|
12120
|
+
"@north-light/identity": "0.3.455"
|
|
12121
12121
|
}
|
|
12122
12122
|
},
|
|
12123
12123
|
"packages/crouter-sdk": {
|
|
12124
12124
|
"name": "@north-light/sdk",
|
|
12125
|
-
"version": "0.3.
|
|
12125
|
+
"version": "0.3.455",
|
|
12126
12126
|
"license": "UNLICENSED",
|
|
12127
12127
|
"dependencies": {
|
|
12128
|
-
"@north-light/api": "0.3.
|
|
12129
|
-
"@north-light/identity": "0.3.
|
|
12128
|
+
"@north-light/api": "0.3.455",
|
|
12129
|
+
"@north-light/identity": "0.3.455",
|
|
12130
12130
|
"jose": "^6.2.1"
|
|
12131
12131
|
},
|
|
12132
12132
|
"bin": {
|