@north-light/cli 0.3.480 → 0.3.482
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/builtin-memory/crouter-plugin/bundles-and-memory.md +2 -2
- package/dist/builtin-memory/crouter-plugin/commands.md +1 -1
- package/dist/builtin-memory/crouter-sdk/release-notes.md +19 -0
- package/dist/builtin-memory/northlight-apps/cli.md +22 -5
- package/dist/builtin-memory/northlight-apps/coding-agents.md +29 -5
- package/dist/builtin-memory/northlight-apps/concepts/apps-and-sign-in.md +1 -1
- package/dist/builtin-memory/northlight-apps/guides/manage-your-app.md +23 -8
- package/dist/builtin-memory/northlight-apps/quickstart.md +102 -30
- package/dist/core/command-plugins/compose.js +1 -1
- package/dist/core/command-plugins/transport/http-plan.d.ts +5 -0
- package/dist/core/command-plugins/transport/http-plan.js +1 -1
- package/dist/core/providers/call.d.ts +2 -0
- package/dist/core/providers/call.js +1 -1
- package/dist/core/providers/mint.js +1 -1
- package/dist/core/providers/receipts.d.ts +2 -0
- package/dist/core/providers/receipts.js +2 -2
- package/dist/core/providers/token-cache.d.ts +3 -1
- package/dist/core/providers/token-cache.js +1 -1
- package/dist/core/runs/operations.d.ts +5 -0
- package/dist/core/runs/operations.js +5 -5
- package/dist/core/usage/ledger.d.ts +1 -1
- package/dist/core/usage/ledger.js +1 -1
- package/dist/daemon/api/handlers/runs.js +1 -1
- package/docs/apps/cli.md +21 -4
- package/docs/apps/coding-agents.md +29 -5
- package/docs/apps/concepts/apps-and-sign-in.md +1 -1
- package/docs/apps/guides/manage-your-app.md +23 -8
- package/docs/apps/quickstart.md +102 -30
- package/docs/ci-runner.md +2 -1
- package/docs/plugin/bundles-and-memory.md +2 -2
- package/docs/plugin/commands.md +1 -1
- package/docs/sdk/release-notes.md +19 -0
- package/package.json +3 -3
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +12 -12
|
@@ -1 +1 @@
|
|
|
1
|
-
var oe=Object.defineProperty;var m=(e,t)=>oe(e,"name",{value:t,configurable:!0});import{validateDeclaredResult as I}from"../../api/command-manifest/result.js";import{callerAllows as x,requireGroup as ne}from"../../daemon/api/handlers/providers.js";import{grantFor as ie,terminalGrant as se}from"../grants/mirror.js";import{assertUnderCap as z,UNCAPPED_GRANTEE as ae}from"../usage/ledger.js";import{listInstalledPlugins as de}from"../resolver.js";import{ProviderError as l}from"./errors.js";import{resolveProviderRef as ue}from"./roots.js";import{ensureProviderBundle as le,refreshProviderBundle as pe,resolveProviderBundle as B,unavailableProviderError as ce}from"./bundles.js";import{prepareFileCall as fe,resignForRetry as Q,resolveFileResult as V}from"./files.js";import{consumeApproval as me,requireProviderApproval as ve}from"./approval.js";import{outboundToken as ge}from"./mint.js";import{discardFile as he}from"./token-cache.js";import{registerProviderCall as we}from"./inflight.js";import{buildRequestPlan as be}from"../command-plugins/transport/http-plan.js";import{sendProviderRequest as ye,ProviderTransportError as _e}from"./request.js";import{acceptProviderReceipt as je}from"./receipts.js";import{writeProviderAudit as Ae}from"./audit.js";import{selectDevice as ke,sendDeviceCall as qe,deviceFailure as Pe,deviceUnreachable as K,DEFAULT_DEVICE_REQUEST_MS as $e}from"./device.js";import{isLibsSearch as W,wantsBundling as Ee,callLibsSearch as Re,bundleExec as Se,mapLibOutcome as Fe,reportLibCalls as Te}from"./device-libs.js";import{envelopeForProviderCaller as Ce}from"../peer/envelope.js";function X(e){const t=[],a=e.roots.length===1?e.roots[0].name:void 0,c=m((r,n)=>{const d=[...n,r.name];if(r.kind==="branch"){for(const _ of r.children)c(_,d);return}const p=d[0]===a?d.slice(1).join(" "):d.join(" ");t.push({tool:p,leaf:r})},"walk");for(const r of e.roots)c(r,[]);return t}m(X,"providerLeaves");function Y(e,t,a){if(e.kind!=="branch")return null;const c=[];for(const i of e.children)if(i.kind==="branch"){const h=Y(i,t,a);h&&c.push(h)}else if("group"in i&&i.group&&x(a,`${t}:${i.group}`)){const{kind:h,name:v,description:y,whenToUse:P,tier:o,summary:A,params:s,output:w,effects:k,group:E}=i;c.push({kind:h,name:v,description:y,whenToUse:P,...o===void 0?{}:{tier:o},summary:A,params:s,output:w,effects:k,group:E})}if(c.length===0)return null;const{kind:r,name:n,description:d,whenToUse:p,tier:_,summary:g,model:j,rootEntry:q}=e;return{kind:r,name:n,description:d,whenToUse:p,..._===void 0?{}:{tier:_},summary:g,...j===void 0?{}:{model:j},...q===void 0?{}:{rootEntry:q},children:c}}m(Y,"providerHelpBranch");async function ar(e,t){const a=await B(t),c=X(a.commands),r=de("user").find(n=>n.name===t)?.manifest.description??t;return{provider:t,status:"available",major:a.major,display_name:r,transport:a.transport.kind,groups:a.provider.groups.filter(n=>x(e,`${t}:${n.group}`)).map(n=>({group:n.group,leaves:c.filter(({leaf:d})=>d.group===n.group).map(({tool:d,leaf:p})=>({tool:d,description:p.description,summary:p.summary,params:p.params,output:p.output}))})),commands:a.commands.roots.flatMap(n=>{const d=Y(n,t,e);return d?[d]:[]})}}m(ar,"providerTools");function De(e){return je(e.headers["crtr-receipt"],e)}m(De,"onProviderResponse");function Z(e){if(!(e.class==="person"||!e.sub||!e.grantee))return{grantee:e.grantee,spend_cap:ie(e.grantee,e.sub)?.spend_cap??null}}m(Z,"capTarget");function Ne(e){if(e.class==="person"){const t=se()?.sub;if(!t)throw new l("grant_revoked","This machine has no active Northlight grant for your terminal. Run `crtr sys join` again from /account/machines.");return{sub:t,grantee:t}}if(!e.sub||!e.grantee)throw new l("grant_revoked","the caller has no active grant");return{sub:e.sub,grantee:e.grantee}}m(Ne,"identity");function Oe(e,t){return[`${e.id}:${t.group}`]}m(Oe,"callScope");function Me(e,t){const a=X(e.commands).find(c=>c.tool===t);if(!a)throw new l("not_found",`tool "${t}" is not in provider "${e.id}"`,{param:"tool"});if(!a.leaf.group)throw new l("not_found",`tool "${t}" has no group`,{param:"tool"});return a.leaf}m(Me,"leafFor");function L(e){if(!e||typeof e!="object"||Array.isArray(e))return{};const t=e.error;return!t||typeof t!="object"||Array.isArray(t)?{}:t}m(L,"errorBody");function Ue(e){const t=Array.isArray(e)?e[0]:e;if(!t)return;const a=Number(t);if(Number.isFinite(a)&&a>=0)return a;const c=Date.parse(t);return Number.isFinite(c)?Math.max(0,Math.ceil((c-Date.now())/1e3)):void 0}m(Ue,"retryAfter");function N(e,t,a,c=!1){const r=L(t),n=typeof r.code=="string"?r.code:void 0,d=typeof r.message=="string"?r.message:`provider answered HTTP ${e}`,p=typeof r.field=="string"?r.field:void 0,g={origin:"provider",details:{code:n??null,message:d,...p?{field:p}:{},...typeof r.next=="string"?{next:r.next}:{},...r.received!==void 0?{received:r.received}:{},status:e},...p?{param:p}:{}};return e===429?new l("provider_rate_limited",d,{...g,retry_after_s:Ue(a["retry-after"])}):e===401||e>=500?new l("provider_unavailable",d,g):e===403&&n==="provider_refused"?new l("provider_refused",d,g):e===403&&n==="approval_required"?new l("provider_error",c?"provider asked for approval again":"provider approval question is invalid",{...g,status:e}):new l("provider_error",d,{...g,status:e})}m(N,"mapProviderResponse");function ee(e,t){return JSON.stringify({major:e.major,endpoint:e.transport.kind==="http"?e.transport.endpoint:null,baseUrl:e.commands.baseUrl??null,rest:t.rest,params:t.params,output:t.output})}m(ee,"approvalShape");async function dr(e,t,a){const c=ue(t.provider);if(c===null)throw new l("not_found",`provider "${t.provider}" is not available`,{param:"provider"});const r={...t,provider:c},n=new AbortController,d=m(()=>n.abort(a.signal.reason),"abort");a.signal.aborted?d():a.signal.addEventListener("abort",d,{once:!0});let p=a.requestId;const _=we({request_id:p,run_id:e.run_id,node_id:e.node_id,controller:n});let g,j,q,i,h,v,y,P="allowed";try{try{await le(r.provider)}catch{throw ce(r.provider)}let o=await B(r.provider,r.version),A=!1,s,w,k,E=!1,J=!1,O,H=0;for(;;){const b=Me(o,r.tool);if(q=`${o.id}:${b.group}`,ne(e,o.id,b.group),z(Z(e)),o.transport.kind!=="machine"&&r.machine!==void 0)throw new l("invalid_argument",`provider "${o.id}" is not served by a device, so it takes no machine`,{param:"machine"});o.transport.kind==="machine"&&O===void 0&&(O=await ke(r.machine,void 0,{newest:W(o.major,r.tool)}),H=Date.now()+(o.commands.timeouts?.requestMs??$e));let M=null;if(r.approval&&!E){const u=me(e,r,ee(o,b));k=u.option,M=u.prepared,y=r.approval,p=u.requestId,E=!0}const R={params:b.params.filter(u=>"type"in u&&u.type==="file").map(u=>u.name),output:b.output.find(u=>u.type==="file")?.name??null},G=JSON.stringify(R);if(w!==void 0&&w!==G)throw new l("provider_bad_response","the refreshed manifest changed the tool file contract",{origin:"provider",details:{status:409,message:"manifest_stale changed the tool file contract"}});w=G,M&&(s=await Q(e,R,r.arguments,M,{signal:n.signal})),s??=await fe(e,R,r.arguments,{signal:n.signal});const{sub:U,grantee:S}=Ne(e);if(z(Z(e)),o.transport.kind==="machine"){const u=await Ie(e,o,r,b,s,{sub:U,requestId:p,deadlineAt:H,signal:n.signal,machine:O});return v=u.responder,g=u.cleanup,{status:200,body:u.result}}const F=await ge({audience:o.transport.endpoint,sub:U,grantee:S,initiating_app:S,scope:Oe(o,b)},{signal:n.signal});i=F.jti;const re={registration:{name:o.id,endpoint:o.transport.endpoint},...o.commands.baseUrl?{baseUrl:o.commands.baseUrl}:{},...o.commands.timeouts?{timeouts:o.commands.timeouts}:{},params:b.params,rest:b.rest,output:b.output,commandPath:[o.id,...r.tool.split(" ")]},te=be(re,s.arguments,void 0,{authorization:`Bearer ${F.access_token}`,requestId:p,fileMode:"pass-through",extraHeaders:{...e.run_id?{"Crtr-Run-Id":e.run_id}:{},...k?{"Crtr-Approval":k}:{},...s.headers}});j??=Date.now()+(o.commands.timeouts?.requestMs??3e4);let f;try{if(f=await ye(te,{connectMs:o.commands.timeouts?.connectMs,requestMs:o.commands.timeouts?.requestMs,deadlineAt:j,signal:n.signal}),n.signal.aborted)throw n.signal.reason??new Error("provider call aborted")}catch(u){throw n.signal.aborted?n.signal.reason??u:u instanceof _e?new l("provider_unavailable",u.message,{origin:"provider",details:{status:null,code:u.kind}}):u}if(h??=De({provider:o.id,tool:r.tool,request_id:p,run_id:e.run_id,sub:U,grantee:S,ledger_grantee:e.class==="person"?ae:S,jti:F.jti,status:f.status,headers:f.headers}),v=f.headers["crtr-responder"],f.status===401&&he(F.key),f.status>=200&&f.status<300){const u=f.body;if(!u||typeof u!="object"||Array.isArray(u))throw new l("provider_bad_response","provider result is not a JSON object",{origin:"provider",details:{status:f.status,message:"provider result is not a JSON object"}});let C;try{C=await V(e,s,u,{signal:n.signal})}catch($){throw $ instanceof l&&$.code==="provider_error"?new l("provider_bad_response",$.message,{origin:"provider",details:{status:f.status,message:$.message}}):$}g=C.cleanup;const D=I(b.output,C.result);if(D)throw new l("provider_bad_response",`provider result has an invalid output field "${D.field}"`,{origin:"provider",param:D.field,details:{status:f.status,...D}});if(n.signal.aborted)throw n.signal.reason??new Error("provider call aborted");return{status:200,body:C.result}}const T=L(f.body);if(f.status===403&&T.code==="approval_required"&&!k&&!J){J=!0;const u=await ve(e,r,p,T.question,n.signal,ee(o,b),s);if(y=u.questionId,k=u.option,s=await Q(e,R,r.arguments,s,{signal:n.signal}),j=void 0,n.signal.aborted)throw new l("user_denied","the approval wait was canceled");continue}if(f.status===403&&T.code==="approval_required"&&k)throw N(f.status,f.body,f.headers,!0);if(f.status!==401&&T.manifest_stale===!0&&!A){A=!0,await pe(o.id,{conditional:!1,...r.version===void 0?{}:{version:r.version}}),o=await B(o.id,r.version);continue}throw N(f.status,f.body,f.headers,!!k)}}catch(o){throw o instanceof l&&o.question_id&&(y=o.question_id),v===void 0&&o instanceof l&&typeof o.responder=="string"&&(v=o.responder),P=`refused:${o instanceof l?o.code:"server_error"}`,g?.(),o}finally{try{Ae({request_id:p,caller:e,provider:r.provider,tool:r.tool,scope_checked:q,outcome:P,jti:i,receipt_id:h,responder:v,question_id:y})}finally{_()}a.signal.removeEventListener("abort",d)}}m(dr,"callProvider");async function Ie(e,t,a,c,r,n){const{machine:d}=n,p=Ce(e,n.sub),_={machine:d,envelope:p,signal:n.signal,deadlineAt:n.deadlineAt,mapFailure:N};if(W(t.major,a.tool)){const s=await Re(_,r.arguments,t.major),w=I(c.output,s);if(w)throw new l("provider_bad_response",`the libraries result has an invalid output field "${w.field}"`,{origin:"provider",param:w.field,details:{status:200,...w}});return{result:s,responder:void 0,cleanup:void 0}}let g=r.arguments,j=null,q="";if(Ee(t.major,a.tool,r.arguments)){const s=await Se(_,r.arguments,t.major);if(n.deadlineAt-Date.now()<=0)throw K(d,"timeout","");s!==null&&(g=s.arguments,j=s.services,q=s.release)}const i=await qe(d,{tool:a.tool,arguments:g,version:t.major,requestId:n.requestId,runId:e.run_id,deadlineAt:n.deadlineAt,upload:r.upload===null?null:{id:r.upload.upload_id,link:r.upload.upload_link}},p,n.signal);if(n.signal.aborted)throw n.signal.reason??new Error("provider call aborted");if("unreachable"in i)throw K(d,i.unreachable.reason,i.unreachable.message);const h=i.headers["crtr-responder"],v=L(i.body);if(i.status<200||i.status>=300){const s=Pe(d,i.status,typeof v.code=="string"?v.code:void 0,typeof v.message=="string"?v.message:void 0,i.body),w=s===null&&i.status>=500&&typeof v.code=="string"?new l("provider_error",typeof v.message=="string"?v.message:`the device answered HTTP ${i.status}`,{origin:"runtime",status:i.status,details:{code:v.code,status:i.status}}):null;throw Object.assign(s??w??N(i.status,i.body,i.headers),{responder:h})}const y=i.body&&typeof i.body=="object"&&!Array.isArray(i.body)?i.body.result:void 0;if(!y||typeof y!="object"||Array.isArray(y))throw Object.assign(new l("provider_bad_response","the device result is not a JSON object",{origin:"provider",details:{status:i.status,message:"the device result is not a JSON object"}}),{responder:h});let P=y;if(j!==null){const s=Fe(P,j);if(Te(d,p,q,s.calls),"error"in s)throw Object.assign(s.error,{responder:h});P=s.result}let o;try{o=await V(e,r,P,{signal:n.signal})}catch(s){throw s instanceof l&&s.code==="provider_error"?Object.assign(new l("provider_bad_response",s.message,{origin:"provider",details:{status:i.status,message:s.message}}),{responder:h}):s}const A=I(c.output,o.result);if(A)throw o.cleanup?.(),Object.assign(new l("provider_bad_response",`the device result has an invalid output field "${A.field}"`,{origin:"provider",param:A.field,details:{status:i.status,...A}}),{responder:h});return{result:o.result,responder:h,cleanup:o.cleanup}}m(Ie,"callDevice");export{dr as callProvider,N as mapProviderResponse,De as onProviderResponse,X as providerLeaves,ar as providerTools};
|
|
1
|
+
var re=Object.defineProperty;var v=(e,t)=>re(e,"name",{value:t,configurable:!0});import{validateDeclaredResult as I}from"../../api/command-manifest/result.js";import{callerAllows as x,requireGroup as te}from"../../daemon/api/handlers/providers.js";import{terminalGrant as oe}from"../grants/mirror.js";import{UNCAPPED_GRANTEE as ie}from"../usage/ledger.js";import{listInstalledPlugins as ne}from"../resolver.js";import{ProviderError as l}from"./errors.js";import{resolveProviderRef as se}from"./roots.js";import{ensureProviderBundle as ae,refreshProviderBundle as de,resolveProviderBundle as B,unavailableProviderError as ue}from"./bundles.js";import{prepareFileCall as le,resignForRetry as z,resolveFileResult as Q}from"./files.js";import{consumeApproval as pe,requireProviderApproval as ce}from"./approval.js";import{outboundToken as fe}from"./mint.js";import{discardFile as me}from"./token-cache.js";import{registerProviderCall as ve}from"./inflight.js";import{buildRequestPlan as ge}from"../command-plugins/transport/http-plan.js";import{sendProviderRequest as he,ProviderTransportError as we}from"./request.js";import{acceptProviderReceipt as be}from"./receipts.js";import{writeProviderAudit as ye}from"./audit.js";import{selectDevice as _e,sendDeviceCall as je,deviceFailure as Ae,deviceUnreachable as V,DEFAULT_DEVICE_REQUEST_MS as ke}from"./device.js";import{isLibsSearch as K,wantsBundling as qe,callLibsSearch as Pe,bundleExec as $e,mapLibOutcome as Ee,reportLibCalls as Re}from"./device-libs.js";import{envelopeForProviderCaller as Se}from"../peer/envelope.js";function W(e){const t=[],a=e.roots.length===1?e.roots[0].name:void 0,c=v((r,i)=>{const d=[...i,r.name];if(r.kind==="branch"){for(const _ of r.children)c(_,d);return}const p=d[0]===a?d.slice(1).join(" "):d.join(" ");t.push({tool:p,leaf:r})},"walk");for(const r of e.roots)c(r,[]);return t}v(W,"providerLeaves");function X(e,t,a){if(e.kind!=="branch")return null;const c=[];for(const n of e.children)if(n.kind==="branch"){const h=X(n,t,a);h&&c.push(h)}else if("group"in n&&n.group&&x(a,`${t}:${n.group}`)){const{kind:h,name:m,description:y,whenToUse:P,tier:o,summary:A,params:s,output:w,effects:k,group:R}=n;c.push({kind:h,name:m,description:y,whenToUse:P,...o===void 0?{}:{tier:o},summary:A,params:s,output:w,effects:k,group:R})}if(c.length===0)return null;const{kind:r,name:i,description:d,whenToUse:p,tier:_,summary:g,model:j,rootEntry:q}=e;return{kind:r,name:i,description:d,whenToUse:p,..._===void 0?{}:{tier:_},summary:g,...j===void 0?{}:{model:j},...q===void 0?{}:{rootEntry:q},children:c}}v(X,"providerHelpBranch");async function ir(e,t){const a=await B(t),c=W(a.commands),r=ne("user").find(i=>i.name===t)?.manifest.description??t;return{provider:t,status:"available",major:a.major,display_name:r,transport:a.transport.kind,groups:a.provider.groups.filter(i=>x(e,`${t}:${i.group}`)).map(i=>({group:i.group,leaves:c.filter(({leaf:d})=>d.group===i.group).map(({tool:d,leaf:p})=>({tool:d,description:p.description,summary:p.summary,params:p.params,output:p.output}))})),commands:a.commands.roots.flatMap(i=>{const d=X(i,t,e);return d?[d]:[]})}}v(ir,"providerTools");function De(e){return be(e.headers["crtr-receipt"],e)}v(De,"onProviderResponse");function Fe(e){if(e.class==="person"){const t=oe()?.sub;if(!t)throw new l("grant_revoked","This machine has no active Northlight grant for your terminal. Run `crtr sys join` again from /account/machines.");return{sub:t,grantee:t}}if(!e.sub||!e.grantee)throw new l("grant_revoked","the caller has no active grant");return{sub:e.sub,grantee:e.grantee}}v(Fe,"identity");function Ne(e,t){return[`${e.id}:${t.group}`]}v(Ne,"callScope");function Oe(e,t){const a=W(e.commands).find(c=>c.tool===t);if(!a)throw new l("not_found",`tool "${t}" is not in provider "${e.id}"`,{param:"tool"});if(!a.leaf.group)throw new l("not_found",`tool "${t}" has no group`,{param:"tool"});return a.leaf}v(Oe,"leafFor");function L(e){if(!e||typeof e!="object"||Array.isArray(e))return{};const t=e.error;return!t||typeof t!="object"||Array.isArray(t)?{}:t}v(L,"errorBody");function Te(e){const t=Array.isArray(e)?e[0]:e;if(!t)return;const a=Number(t);if(Number.isFinite(a)&&a>=0)return a;const c=Date.parse(t);return Number.isFinite(c)?Math.max(0,Math.ceil((c-Date.now())/1e3)):void 0}v(Te,"retryAfter");function T(e,t,a,c=!1){const r=L(t),i=typeof r.code=="string"?r.code:void 0,d=typeof r.message=="string"?r.message:`provider answered HTTP ${e}`,p=typeof r.field=="string"?r.field:void 0,g={origin:"provider",details:{code:i??null,message:d,...p?{field:p}:{},...typeof r.next=="string"?{next:r.next}:{},...r.received!==void 0?{received:r.received}:{},status:e},...p?{param:p}:{}};return e===429?new l("provider_rate_limited",d,{...g,retry_after_s:Te(a["retry-after"])}):e===401||e>=500?new l("provider_unavailable",d,g):e===403&&i==="provider_refused"?new l("provider_refused",d,g):e===403&&i==="approval_required"?new l("provider_error",c?"provider asked for approval again":"provider approval question is invalid",{...g,status:e}):new l("provider_error",d,{...g,status:e})}v(T,"mapProviderResponse");function Y(e,t){return JSON.stringify({major:e.major,endpoint:e.transport.kind==="http"?e.transport.endpoint:null,baseUrl:e.commands.baseUrl??null,rest:t.rest,params:t.params,output:t.output})}v(Y,"approvalShape");async function nr(e,t,a){const c=se(t.provider);if(c===null)throw new l("not_found",`provider "${t.provider}" is not available`,{param:"provider"});const r={...t,provider:c},i=new AbortController,d=v(()=>i.abort(a.signal.reason),"abort");a.signal.aborted?d():a.signal.addEventListener("abort",d,{once:!0});let p=a.requestId;const _=ve({request_id:p,run_id:e.run_id,node_id:e.node_id,controller:i});let g,j,q,n,h,m,y,P="allowed";try{try{await ae(r.provider)}catch{throw ue(r.provider)}let o=await B(r.provider,r.version),A=!1,s,w,k,R=!1,J=!1,C,H=0;for(;;){const b=Oe(o,r.tool);if(q=`${o.id}:${b.group}`,te(e,o.id,b.group),o.transport.kind!=="machine"&&r.machine!==void 0)throw new l("invalid_argument",`provider "${o.id}" is not served by a device, so it takes no machine`,{param:"machine"});o.transport.kind==="machine"&&C===void 0&&(C=await _e(r.machine,void 0,{newest:K(o.major,r.tool)}),H=Date.now()+(o.commands.timeouts?.requestMs??ke));let M=null;if(r.approval&&!R){const u=pe(e,r,Y(o,b));k=u.option,M=u.prepared,y=r.approval,p=u.requestId,R=!0}const S={params:b.params.filter(u=>"type"in u&&u.type==="file").map(u=>u.name),output:b.output.find(u=>u.type==="file")?.name??null},G=JSON.stringify(S);if(w!==void 0&&w!==G)throw new l("provider_bad_response","the refreshed manifest changed the tool file contract",{origin:"provider",details:{status:409,message:"manifest_stale changed the tool file contract"}});w=G,M&&(s=await z(e,S,r.arguments,M,{signal:i.signal})),s??=await le(e,S,r.arguments,{signal:i.signal});const{sub:U,grantee:D}=Fe(e);if(o.transport.kind==="machine"){const u=await Ce(e,o,r,b,s,{sub:U,requestId:p,deadlineAt:H,signal:i.signal,machine:C});return m=u.responder,g=u.cleanup,{status:200,body:u.result}}const $=await fe({audience:o.transport.endpoint,sub:U,grantee:D,initiating_app:D,scope:Ne(o,b)},{signal:i.signal});n=$.jti;const Z={registration:{name:o.id,endpoint:o.transport.endpoint},...o.commands.baseUrl?{baseUrl:o.commands.baseUrl}:{},...o.commands.timeouts?{timeouts:o.commands.timeouts}:{},params:b.params,rest:b.rest,output:b.output,commandPath:[o.id,...r.tool.split(" ")]},ee=ge(Z,s.arguments,void 0,{authorization:`Bearer ${$.access_token}`,requestId:p,fileMode:"pass-through",extraHeaders:{...e.run_id?{"Crtr-Run-Id":e.run_id}:{},...k?{"Crtr-Approval":k}:{},...s.headers}});j??=Date.now()+(o.commands.timeouts?.requestMs??3e4);let f;try{if(f=await he(ee,{connectMs:o.commands.timeouts?.connectMs,requestMs:o.commands.timeouts?.requestMs,deadlineAt:j,signal:i.signal}),i.signal.aborted)throw i.signal.reason??new Error("provider call aborted")}catch(u){throw i.signal.aborted?i.signal.reason??u:u instanceof we?new l("provider_unavailable",u.message,{origin:"provider",details:{status:null,code:u.kind}}):u}if(h??=De({provider:o.id,tool:r.tool,request_id:p,run_id:e.run_id,sub:U,grantee:D,payer:$.payer,ledger_grantee:e.class==="person"?ie:D,jti:$.jti,status:f.status,headers:f.headers}),m=f.headers["crtr-responder"],f.status===401&&me($.key),f.status>=200&&f.status<300){const u=f.body;if(!u||typeof u!="object"||Array.isArray(u))throw new l("provider_bad_response","provider result is not a JSON object",{origin:"provider",details:{status:f.status,message:"provider result is not a JSON object"}});let N;try{N=await Q(e,s,u,{signal:i.signal})}catch(E){throw E instanceof l&&E.code==="provider_error"?new l("provider_bad_response",E.message,{origin:"provider",details:{status:f.status,message:E.message}}):E}g=N.cleanup;const O=I(b.output,N.result);if(O)throw new l("provider_bad_response",`provider result has an invalid output field "${O.field}"`,{origin:"provider",param:O.field,details:{status:f.status,...O}});if(i.signal.aborted)throw i.signal.reason??new Error("provider call aborted");return{status:200,body:N.result}}const F=L(f.body);if(f.status===403&&F.code==="approval_required"&&!k&&!J){J=!0;const u=await ce(e,r,p,F.question,i.signal,Y(o,b),s);if(y=u.questionId,k=u.option,s=await z(e,S,r.arguments,s,{signal:i.signal}),j=void 0,i.signal.aborted)throw new l("user_denied","the approval wait was canceled");continue}if(f.status===403&&F.code==="approval_required"&&k)throw T(f.status,f.body,f.headers,!0);if(f.status!==401&&F.manifest_stale===!0&&!A){A=!0,await de(o.id,{conditional:!1,...r.version===void 0?{}:{version:r.version}}),o=await B(o.id,r.version);continue}throw T(f.status,f.body,f.headers,!!k)}}catch(o){throw o instanceof l&&o.question_id&&(y=o.question_id),m===void 0&&o instanceof l&&typeof o.responder=="string"&&(m=o.responder),P=`refused:${o instanceof l?o.code:"server_error"}`,g?.(),o}finally{try{ye({request_id:p,caller:e,provider:r.provider,tool:r.tool,scope_checked:q,outcome:P,jti:n,receipt_id:h,responder:m,question_id:y})}finally{_()}a.signal.removeEventListener("abort",d)}}v(nr,"callProvider");async function Ce(e,t,a,c,r,i){const{machine:d}=i,p=Se(e,i.sub),_={machine:d,envelope:p,signal:i.signal,deadlineAt:i.deadlineAt,mapFailure:T};if(K(t.major,a.tool)){const s=await Pe(_,r.arguments,t.major),w=I(c.output,s);if(w)throw new l("provider_bad_response",`the libraries result has an invalid output field "${w.field}"`,{origin:"provider",param:w.field,details:{status:200,...w}});return{result:s,responder:void 0,cleanup:void 0}}let g=r.arguments,j=null,q="";if(qe(t.major,a.tool,r.arguments)){const s=await $e(_,r.arguments,t.major);if(i.deadlineAt-Date.now()<=0)throw V(d,"timeout","");s!==null&&(g=s.arguments,j=s.services,q=s.release)}const n=await je(d,{tool:a.tool,arguments:g,version:t.major,requestId:i.requestId,runId:e.run_id,deadlineAt:i.deadlineAt,upload:r.upload===null?null:{id:r.upload.upload_id,link:r.upload.upload_link}},p,i.signal);if(i.signal.aborted)throw i.signal.reason??new Error("provider call aborted");if("unreachable"in n)throw V(d,n.unreachable.reason,n.unreachable.message);const h=n.headers["crtr-responder"],m=L(n.body);if(n.status<200||n.status>=300){const s=Ae(d,n.status,typeof m.code=="string"?m.code:void 0,typeof m.message=="string"?m.message:void 0,n.body),w=s===null&&n.status>=500&&typeof m.code=="string"?new l("provider_error",typeof m.message=="string"?m.message:`the device answered HTTP ${n.status}`,{origin:"runtime",status:n.status,details:{code:m.code,status:n.status}}):null;throw Object.assign(s??w??T(n.status,n.body,n.headers),{responder:h})}const y=n.body&&typeof n.body=="object"&&!Array.isArray(n.body)?n.body.result:void 0;if(!y||typeof y!="object"||Array.isArray(y))throw Object.assign(new l("provider_bad_response","the device result is not a JSON object",{origin:"provider",details:{status:n.status,message:"the device result is not a JSON object"}}),{responder:h});let P=y;if(j!==null){const s=Ee(P,j);if(Re(d,p,q,s.calls),"error"in s)throw Object.assign(s.error,{responder:h});P=s.result}let o;try{o=await Q(e,r,P,{signal:i.signal})}catch(s){throw s instanceof l&&s.code==="provider_error"?Object.assign(new l("provider_bad_response",s.message,{origin:"provider",details:{status:n.status,message:s.message}}),{responder:h}):s}const A=I(c.output,o.result);if(A)throw o.cleanup?.(),Object.assign(new l("provider_bad_response",`the device result has an invalid output field "${A.field}"`,{origin:"provider",param:A.field,details:{status:n.status,...A}}),{responder:h});return{result:o.result,responder:h,cleanup:o.cleanup}}v(Ce,"callDevice");export{nr as callProvider,T as mapProviderResponse,De as onProviderResponse,W as providerLeaves,ir as providerTools};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var T=Object.defineProperty;var a=(r,e)=>T(r,"name",{value:e,configurable:!0});import{createHash as M}from"node:crypto";import{readConfig as E}from"../config.js";import{emitEvent as $}from"../events/emit.js";import{grantSyncStateFor as A}from"../grants/mirror.js";import{assertion as N,machineRevokedMessage as O,requestGrantSync as P,runtimeClient as I}from"../grants/sync.js";import{isUnsandboxedMachine as F}from"../runtime/unsandboxed.js";import{ProviderError as h}from"./errors.js";import{cacheKey as G,cachedToken as L,discardEpoch as U,storeToken as C}from"./token-cache.js";import{discardFile as ce}from"./token-cache.js";const D=1e4;function J(r){const e=r.issuer??E("user").runtime?.issuer;if(!e)throw new h("not_found","provider calls need a directory-connected runtime");return e.replace(/\/$/,"")}a(J,"issuerOf");function f(r,e,t){return new h(r,e,{origin:"directory",details:t})}a(f,"directoryError");function B(r,e,t,i){const n=typeof e.error=="string"?e.error:null,o=typeof e.error_description=="string"?e.error_description:void 0,c={directory_status:r,...n?{directory_error:n}:{},...o?{directory_description:o}:{}},u=o?`: ${o}`:"";switch(n){case"invalid_client":return F()?{error:f("machine_revoked",O(i),c),grantRefusal:!1}:{error:f("server_error",`the directory refused the outbound token request (${n})${u}`,c),grantRefusal:!1};case"invalid_request":return{error:f("server_error",`the directory refused the outbound token request (${n})${u}`,c),grantRefusal:!1};case"invalid_target":return{error:f("not_found",`the directory knows no provider at ${t.audience}${u}`,c),grantRefusal:!1};case"invalid_grant":return{error:f("grant_revoked",`the directory has no active grant for ${t.grantee}${u}`,c),grantRefusal:!0};case"provider_not_connected":return{error:f("provider_not_connected",o??"The user has not connected this provider.",c),grantRefusal:!1};case"invalid_scope":{const s=typeof e.scope=="string"&&e.scope?e.scope:t.scope.length===1?t.scope[0]:void 0,m=s?`missing scope ${s}`:`the grant does not cover ${t.scope.join(" ")}`;return{error:new h("scope_missing",m,{origin:"directory",details:c,...s?{scope:s}:{}}),grantRefusal:!0}}default:return{error:f("server_error",`the directory token endpoint answered ${r}${n?` ${n}`:""}${u}`,c),grantRefusal:!1}}}a(B,"refusal");function H(r){try{return JSON.parse(Buffer.from(r.split(".")[1]??"","base64url").toString("utf8"))}catch{return{}}}a(H,"claims");function K(r){const e=r.grant_rev,t=r.grant_status;return typeof e!="number"||!Number.isInteger(e)||e<0||typeof t!="string"?null:{rev:e,status:t}}a(K,"refusedGrantState");function z(r,e){const t=a(i=>i.status==="active"?0:1,"phase");return r.rev>e.rev||r.rev===e.rev&&t(r)>t(e)}a(z,"newer");function Q(r,e,t){if(!e)return;const i=A(r.grantee,r.sub)??{rev:0,status:"removed"};if(!z(e,i))return;const n=t.syncGrants??P;Promise.resolve().then(()=>n({fresh:!0})).catch(()=>{})}a(Q,"syncIfNewer");async function V(r,e={}){const t=J(e),i=`${t}/token`,n=I(),o=new URLSearchParams({grant_type:"client_credentials",client_id:n.client_id,client_assertion_type:"urn:ietf:params:oauth:client-assertion-type:jwt-bearer",client_assertion:N(n,i),audience:r.audience,sub:r.sub,grantee:r.grantee,scope:[...new Set(r.scope)].sort().join(" "),initiating_app:r.initiating_app}),c=AbortSignal.timeout(D),u=e.signal?AbortSignal.any([e.signal,c]):c;let s,m;try{s=await(e.fetch??fetch)(i,{method:"POST",body:o,signal:u,headers:{Accept:"application/json"}}),m=await s.text()}catch(l){throw e.signal?.aborted?e.signal.reason:new h("server_error",`the directory token endpoint could not be reached: ${l instanceof Error?l.message:String(l)}`,{origin:"directory"})}let p=null;try{p=m?JSON.parse(m):null}catch{p=null}const g=p&&typeof p=="object"&&!Array.isArray(p)?p:{};if(!s.ok){const{error:l,grantRefusal:j}=B(s.status,g,r,t);throw l.code==="machine_revoked"&&$({level:"error",event:"daemon.grants.machine_revoked",fields:{audience:r.audience}}),$({level:"info",event:"daemon.providers.mint_refused",fields:{audience:r.audience,grantee:r.grantee,code:l.code,directory_error:typeof g.error=="string"?g.error:null,status:s.status}}),j&&Q(r,K(g),e),l}const y=g.access_token;if(typeof y!="string"||!y)throw new h("server_error","the directory token response has no access_token",{origin:"directory"});const d=H(y),S=Math.floor((e.now?.()??Date.now())/1e3),_=g.expires_in,v=typeof _=="number"&&Number.isFinite(_)&&_>0?S+Math.floor(_):null,w=typeof d.exp=="number"?d.exp:null,b=v!==null&&w!==null?Math.min(v,w):v??w;if(b===null)throw new h("server_error","the directory token response names no expiry",{origin:"directory"});const x=typeof d.jti=="string"&&d.jti?d.jti:M("sha256").update(y).digest("hex"),R=typeof d.payer=="string"&&d.payer?d.payer:r.sub;return{access_token:y,jti:x,exp:b,payer:R}}a(V,"mintOutboundToken");const k=new Map;async function ie(r,e={}){const t=G(r),i=e.now??Date.now,n=L(t,i());if(n)return{...n,key:t};let o=k.get(t);if(!o){const u=U(t);o=V(r,{...e,signal:void 0}).then(s=>(C(t,s,u),s)).finally(()=>{k.delete(t)}),o.catch(()=>{}),k.set(t,o)}return{...e.signal?await W(o,e.signal):await o,key:t}}a(ie,"outboundToken");function W(r,e){return e.aborted?Promise.reject(e.reason):new Promise((t,i)=>{const n=a(()=>i(e.reason),"onAbort");e.addEventListener("abort",n,{once:!0}),r.then(t,i).finally(()=>e.removeEventListener("abort",n))})}a(W,"abortable");export{D as MINT_TIMEOUT_MS,ce as discardFile,V as mintOutboundToken,ie as outboundToken};
|
|
@@ -6,6 +6,8 @@ export declare function acceptProviderReceipt(value: string | string[] | undefin
|
|
|
6
6
|
sub: string;
|
|
7
7
|
grantee: string;
|
|
8
8
|
run_id: string | null;
|
|
9
|
+
/** The outbound token's `payer` claim: the principal the directory charges (an app, or the user). */
|
|
10
|
+
payer: string;
|
|
9
11
|
/** Whose month the receipt adds to when it differs from the receipt's grantee (the person's calls count under `app:terminal`). */
|
|
10
12
|
ledger_grantee?: string;
|
|
11
13
|
}): string | undefined;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var
|
|
2
|
-
(provider, receipt_id, grantee, run_id, usd_equivalent, received_at) VALUES (?, ?, ?, ?, ?, ?)`).run(r.provider,e.receipt_id,r.grantee,r.run_id,e.usd_equivalent,new Date().toISOString()).changes&&(g(n,{grantee:r.ledger_grantee??r.grantee,payer:"user",source:`provider:${r.provider}`,served_model:"",input_tokens:0,output_tokens:0,cached_tokens:0,usd_equivalent:e.usd_equivalent}),r.run_id)){const d=o(n,"SELECT usage_json FROM nodes WHERE node_id = ? AND run_id = ?").get(r.run_id,r.run_id);if(d){const
|
|
1
|
+
var _=Object.defineProperty;var p=(t,r)=>_(t,"name",{value:r,configurable:!0});import{prepared as o}from"../sqlite-statements.js";import{withCanvasWrite as a}from"../canvas/db.js";import{addUsage as g}from"../usage/ledger.js";function E(t,r){const u=Array.isArray(t)?t[0]:t;if(!u||u.length>16384)return;let i;try{i=JSON.parse(u)}catch{return}if(!i||typeof i!="object"||Array.isArray(i))return;const e=i;if(!(typeof e.receipt_id!="string"||!e.receipt_id||e.receipt_id.length>256||e.provider!==`app:${r.provider}`||e.tool!==r.tool||e.request_id!==r.request_id||e.sub!==r.sub||e.grantee!==r.grantee||e.payer!==r.payer||(e.run_id??null)!==r.run_id||typeof e.unit!="string"||!e.unit||typeof e.units!="number"||!Number.isFinite(e.units)||e.units<0||typeof e.time!="string"||!Number.isFinite(Date.parse(e.time))||typeof e.usd_equivalent!="number"||!Number.isFinite(e.usd_equivalent)||e.usd_equivalent<0))return a(n=>{if(o(n,`INSERT OR IGNORE INTO provider_receipts
|
|
2
|
+
(provider, receipt_id, grantee, run_id, usd_equivalent, received_at) VALUES (?, ?, ?, ?, ?, ?)`).run(r.provider,e.receipt_id,r.grantee,r.run_id,e.usd_equivalent,new Date().toISOString()).changes&&(g(n,{grantee:r.ledger_grantee??r.grantee,payer:r.payer.startsWith("app:")?"app":"user",source:`provider:${r.provider}`,served_model:"",input_tokens:0,output_tokens:0,cached_tokens:0,usd_equivalent:e.usd_equivalent}),r.run_id)){const d=o(n,"SELECT usage_json FROM nodes WHERE node_id = ? AND run_id = ?").get(r.run_id,r.run_id);if(d){const s=d.usage_json?JSON.parse(d.usage_json):{};s.cost_usd=(typeof s.cost_usd=="number"?s.cost_usd:0)+e.usd_equivalent,o(n,"UPDATE nodes SET usage_json = ? WHERE node_id = ?").run(JSON.stringify(s),r.run_id)}}}),e.receipt_id}p(E,"acceptProviderReceipt");export{E as acceptProviderReceipt};
|
|
@@ -11,11 +11,13 @@ export interface OutboundTokenRequest {
|
|
|
11
11
|
/** `<provider>:<group>` strings; order does not matter. */
|
|
12
12
|
scope: readonly string[];
|
|
13
13
|
}
|
|
14
|
-
/** A minted token. `exp` is epoch seconds (the JWT's own claim).
|
|
14
|
+
/** A minted token. `exp` is epoch seconds (the JWT's own claim). `payer` is the principal the directory
|
|
15
|
+
* charges for calls under it (its `payer` claim; `sub` when the claim is absent). */
|
|
15
16
|
export interface CachedToken {
|
|
16
17
|
access_token: string;
|
|
17
18
|
jti: string;
|
|
18
19
|
exp: number;
|
|
20
|
+
payer: string;
|
|
19
21
|
}
|
|
20
22
|
export declare function tokenCacheDir(): string;
|
|
21
23
|
/** JSON of `[aud, sub, grantee, initiating_app, sorted scopes]`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var j=Object.defineProperty;var o=(e,t)=>j(e,"name",{value:t,configurable:!0});import{createHash as A}from"node:crypto";import{mkdirSync as M,readdirSync as E,readFileSync as F,rmSync as d}from"node:fs";import{join as h}from"node:path";import{crtrHome as D}from"../canvas/paths.js";import{atomicWriteJson as P}from"../fs-utils.js";const G=30;function l(){return h(D(),"secrets","outbound-tokens")}o(l,"tokenCacheDir");function U(e){return JSON.stringify([e.audience,e.sub,e.grantee,e.initiating_app,[...new Set(e.scope)].sort()])}o(U,"cacheKey");function
|
|
1
|
+
var j=Object.defineProperty;var o=(e,t)=>j(e,"name",{value:t,configurable:!0});import{createHash as A}from"node:crypto";import{mkdirSync as M,readdirSync as E,readFileSync as F,rmSync as d}from"node:fs";import{join as h}from"node:path";import{crtrHome as D}from"../canvas/paths.js";import{atomicWriteJson as P}from"../fs-utils.js";const G=30;function l(){return h(D(),"secrets","outbound-tokens")}o(l,"tokenCacheDir");function U(e){return JSON.stringify([e.audience,e.sub,e.grantee,e.initiating_app,[...new Set(e.scope)].sort()])}o(U,"cacheKey");function m(e){return h(l(),`${A("sha256").update(e).digest("hex")}.json`)}o(m,"fileFor");function g(e){try{const t=JSON.parse(e);if(!Array.isArray(t)||typeof t[1]!="string"||typeof t[2]!="string")return null;const r=(Array.isArray(t[4])?t[4]:[]).flatMap(s=>typeof s=="string"?[v(s)]:[]).filter(s=>s!==null);return{sub:t[1],grantee:t[2],providers:[...new Set(r)]}}catch{return null}}o(g,"partsOf");function B(e){const t=g(e);return t===null?null:p(t.sub,t.grantee)}o(B,"pairOf");function p(e,t){return JSON.stringify([e,t])}o(p,"pairKey");function y(e,t){return JSON.stringify([e,t])}o(y,"providerKey");function v(e){const t=e.indexOf(":");return t>0?e.slice(0,t):null}o(v,"providerOf");function x(e){try{const t=JSON.parse(F(e,"utf8"));if(typeof t.key!="string"||typeof t.access_token!="string"||typeof t.jti!="string"||typeof t.exp!="number")return null;const n=typeof t.payer=="string"&&t.payer?t.payer:g(t.key)?.sub??"";return n?{...t,payer:n}:null}catch{return null}}o(x,"readFile");function K(e,t){return t<(e.exp-G)*1e3}o(K,"usable");function L(e,t=Date.now()){const n=m(e),r=x(n);return!r||r.key!==e||!K(r,t)?(d(n,{force:!0}),null):{access_token:r.access_token,jti:r.jti,exp:r.exp,payer:r.payer}}o(L,"cachedToken");const f=new Map,S=new Map;function w(e){const t=g(e);return t===null?0:(f.get(p(t.sub,t.grantee))??0)+t.providers.reduce((n,r)=>n+(S.get(y(t.sub,r))??0),0)}o(w,"discardEpoch");function Q(e,t,n=w(e)){return w(e)!==n?!1:(M(l(),{recursive:!0,mode:448}),P(m(e),{key:e,...t},{mode:384}),!0)}o(Q,"storeToken");function V(e){d(m(e),{force:!0})}o(V,"discardFile");function b(e,t,n){let r;try{r=E(l())}catch{return 0}let s=0;for(const c of r){if(!c.endsWith(".json"))continue;const i=h(l(),c),a=x(i),u=a?g(a.key):null,N=u!==null&&(e.has(p(u.sub,u.grantee))||u.providers.some(_=>t.has(y(u.sub,_))));a&&N?(d(i,{force:!0}),s++):(!a||n>=a.exp*1e3)&&d(i,{force:!0})}return s}o(b,"sweep");function X(e,t,n=Date.now()){const r=p(e,t);return f.set(r,(f.get(r)??0)+1),b(new Set([r]),new Set,n)}o(X,"discardFor");function O(e){const t=new Map;for(const n of e){const r=n.resolved_scopes.map(({scope:c,mode:i})=>`${i} ${c}`).sort(),s=p(n.sub,n.grantee);t.set(s,[...t.get(s)??[],JSON.stringify([n.id,n.status,r])])}return new Map([...t].map(([n,r])=>[n,JSON.stringify(r.sort())]))}o(O,"grantStates");function T(e,t){const n=O(e),r=O(t),s=[];for(const[c,i]of n){if(r.get(c)===i)continue;const[a,u]=JSON.parse(c);s.push({sub:a,grantee:u})}return s}o(T,"changedGrantPairs");function k(e){const t=new Set;for(const n of e)if(n.status==="active")for(const{scope:r}of n.resolved_scopes){const s=v(r);s!==null&&t.add(y(n.sub,s))}return t}o(k,"resolvedProviders");function J(e){return new Set(e.filter(t=>t.status==="active"&&t.grantee.startsWith("app:")).map(t=>p(t.sub,t.grantee)))}o(J,"connectedApps");function W(e,t){const n=k(t),r=new Set([...k(e)].filter(c=>!n.has(c))),s=J(t);for(const c of J(e)){if(s.has(c))continue;const[i,a]=JSON.parse(c);r.add(y(i,a.slice(4)))}return[...r]}o(W,"droppedProviders");function Y(e,t,n=Date.now()){const r=T(e,t),s=new Set(r.map(({sub:i,grantee:a})=>p(i,a))),c=new Set(W(e,t));for(const i of s)f.set(i,(f.get(i)??0)+1);for(const i of c)S.set(i,(S.get(i)??0)+1);return{pairs:s.size,providers:c.size,tokens:b(s,c,n)}}o(Y,"discardChangedGrantTokens");export{G as REUSE_MARGIN_S,U as cacheKey,L as cachedToken,T as changedGrantPairs,Y as discardChangedGrantTokens,w as discardEpoch,V as discardFile,X as discardFor,W as droppedProviders,Q as storeToken,l as tokenCacheDir};
|
|
@@ -34,6 +34,10 @@ export declare const IDEMPOTENCY_WINDOW_MS = 86400000;
|
|
|
34
34
|
export declare function idempotencyKeyOf(header: unknown): string | undefined;
|
|
35
35
|
/** Delete keys past their window; returns how many were removed. */
|
|
36
36
|
export declare function pruneExpiredIdempotencyKeys(now?: number): number;
|
|
37
|
+
/** Longest `run.start` deadline an app may set: 24 hours. The grammar is `crtr node new --deadline`'s (parts of `<int>[smhd]`, summed). */
|
|
38
|
+
export declare const RUN_DEADLINE_MAX_MS: number;
|
|
39
|
+
/** The milliseconds of a `run.start` `deadline` duration; refuses a malformed, zero, or over-24h value as `invalid_request` on `deadline`. */
|
|
40
|
+
export declare function parseRunDeadlineMs(raw: string): number;
|
|
37
41
|
export declare function startRun(principal: AppPrincipal, input: {
|
|
38
42
|
prompt: string;
|
|
39
43
|
scopes: string[] | null;
|
|
@@ -42,6 +46,7 @@ export declare function startRun(principal: AppPrincipal, input: {
|
|
|
42
46
|
isolated?: boolean;
|
|
43
47
|
cwd?: string;
|
|
44
48
|
model?: string;
|
|
49
|
+
deadline?: string;
|
|
45
50
|
output_schema?: Record<string, unknown>;
|
|
46
51
|
message?: string;
|
|
47
52
|
}, idempotencyKey?: string, resolveAttachments?: AttachmentResolver): Promise<Record<string, unknown>>;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
var
|
|
1
|
+
var Ne=Object.defineProperty;var m=(e,t)=>Ne(e,"name",{value:t,configurable:!0});import{prepared as w}from"../sqlite-statements.js";import{coveredByAny as ve}from"../../shared/env.js";import{createHash as J}from"node:crypto";import{ApiError as be}from"../../api/errors.js";import{CrtrError as Oe}from"../errors.js";import{isAbsolute as Y,relative as De,resolve as Me,sep as Ie,join as Te}from"node:path";import{SessionManager as Ae}from"@earendil-works/pi-coding-agent";import{covers as W}from"@north-light/identity";import{openDb as G,withCanvasWrite as L}from"../canvas/db.js";import{deleteNodeWithOwned as Le}from"../graph/deletion.js";import{getById as Ce}from"../graph/objects.js";import{repairRunConversations as X}from"../canvas/sessions.js";import{getNode as N,nodeFiles as xe,removeNodeFiles as ke,updateNode as We}from"../canvas/canvas.js";import{fullName as Fe}from"../canvas/labels.js";import{renameWindow as He}from"../runtime/placement.js";import{liveExecutionForNode as z}from"../canvas/executions.js";import{conversationStore as Q,isConversationNotFound as qe}from"../conversation-store/index.js";import{enqueueNodeMail as Ue}from"../feed/messages.js";import{newestReportBody as Pe}from"../feed/reports.js";import{ensureAppProfile as Be,loadExactProfileManifest as $e}from"../profiles/manifest.js";import{stopNodeSandboxes as V}from"../spaces/stop.js";import{BrokerUnreachableError as je}from"../runtime/broker-request.js";import{interruptLive as Je}from"../runtime/interactive-deliver.js";import{isBrokerLive as Z}from"../runtime/model-swap.js";import{newNodeId as Ye}from"../runtime/nodes.js";import{writeGoal as Ge}from"../runtime/kickoff.js";import{reviveNode as F}from"../runtime/revive.js";import{spawnChildPrepared as Xe}from"../runtime/spawn.js";import{isPortableModelToken as ze}from"../runtime/launch.js";import{parseDurationMs as Qe}from"../wake.js";import{expandModelCandidates as Ve,parseModelSpec as Ze}from"../model-routes.js";import{isolatedBrokerLauncherAvailable as Ke}from"../runtime/host.js";import{resolveRunKind as et}from"../runtime/launch-target.js";import{cycleAwareMessages as tt}from"../runtime/session-cycles.js";import{originFor as K}from"../runtime/stamp/protocol.js";import{readOutputRequest as nt,readOutputResult as ee}from"../runtime/structured-output.js";import{admitLegacyStructuredOutput as te}from"../../daemon/structured-requests/sidecar.js";import{runSchemaRepromptPending as st}from"../runtime/stop-guard.js";import{toRunOutcomeDTO as rt}from"../../daemon/api/map.js";import{openAssistantMessageStartSequence as ot}from"../../daemon/runs/observer.js";import{closeNode as ne}from"../runtime/close.js";import{parseScope as it}from"@north-light/identity";import{appendRunEvent as se,subscribeRunEvents as dt}from"./events.js";import{runActivity as at}from"./activity.js";import{cancelOpenQuestions as re,openQuestionIds as oe}from"./questions.js";import{appSpace as H}from"../layout.js";import{ensureAppSpace as ct}from"../spaces/space.js";import{assertUploadsUsable as ie,resolveStorageUrls as ut}from"../storage/attachments.js";import{storageClient as mt,storageConfig as lt}from"../storage/client.js";import{deleteUploadRow as ft,insertUploadUses as de,releaseRunUploads as _t}from"../storage/tables.js";import{abortProviderCalls as ae}from"../providers/inflight.js";const ce=m((e,t,r)=>ut(t,e,r),"defaultResolver"),pt=3e3;function wn(e){return Number(w(e,"UPDATE idempotency_keys SET grantee = 'app:terminal' WHERE grantee = 'user:local'").run().changes)}m(wn,"rewriteLocalIdempotencyGrantee");class S extends Error{static{m(this,"RunError")}status;code;param;details;scope;retryable;constructor(t,r,s,i,c,d,o=t===503){super(s),this.status=t,this.code=r,this.param=i,this.details=c,this.scope=d,this.retryable=o}}const R=m((e,t)=>new S(400,"invalid_request",e,t),"invalid"),q=m(()=>new S(404,"not_found","run not found"),"missing"),U=m(()=>({input_tokens:0,output_tokens:0,cache_read_tokens:0,cache_write_tokens:0,cost_usd:0}),"usage"),O=m(()=>G(),"db");function C(e,t,r=!1){const s=N(e);if(!s||s.parent!==null||s.run_id!==e||s.grantee!==t||!r&&s.deleting_at)throw q();return s}m(C,"ownedRun");const P=["waiting_on_user","idle","settled"];function I(e){if(e.status==="canceled"||e.status==="dead"||e.status==="done"&&e.intent!=="parked")return"settled";const t=w(O(),`SELECT 1 FROM node_rows n WHERE n.run_id = ? AND n.status = 'active' AND
|
|
2
2
|
(n.last_settled_at IS NULL OR EXISTS (
|
|
3
3
|
SELECT 1 FROM run_events e WHERE e.run_id = ? AND e.node_id = n.node_id AND e.type = 'node.turn.started'
|
|
4
4
|
AND e.sequence_number > COALESCE((SELECT MAX(c.sequence_number) FROM run_events c
|
|
5
5
|
WHERE c.run_id = e.run_id AND c.node_id = e.node_id AND c.type = 'node.turn.completed'), 0)
|
|
6
|
-
)) LIMIT 1`).get(e.node_id,e.node_id),r=w(
|
|
6
|
+
)) LIMIT 1`).get(e.node_id,e.node_id),r=w(O(),"SELECT 1 FROM deliveries WHERE destination_id IN (SELECT node_id FROM nodes WHERE run_id = ?) AND state IN ('armed', 'pending') AND wake = 1 LIMIT 1").get(e.node_id);return t||r||!e.last_settled_at||st(e)?"running":oe(e.node_id).length?"waiting_on_user":"idle"}m(I,"runStatus");function hn(e){const t=N(e);return t!==null&&I(t)==="running"}m(hn,"runIsRunning");function A(e){X(e.node_id),e=N(e.node_id)??e;const t=I(e),r=t==="settled"?[]:oe(e.node_id),s=Pe(e.node_id)||void 0,i=t==="settled"?rt(e):null,c=i?.kind==="result"?ee(e.node_id,e.grantee):void 0;return{run_id:e.node_id,name:e.name,status:t,...i?{outcome:i}:{},...c===void 0?{}:{result:c},...s!==void 0?{last_report:s}:{},pending_question_ids:r,...e.last_turn_error_json?{last_turn_error:JSON.parse(e.last_turn_error_json)}:{},usage:JSON.parse(e.usage_json??JSON.stringify(U())),payer:"user",scopes:e.scopes,metadata:e.metadata_json?JSON.parse(e.metadata_json):{},created_at:e.created,...t==="settled"&&e.outcome_at?{settled_at:e.outcome_at}:{},latest_sequence:(e.event_cursor??1)-1,last_activity_at:e.last_activity_at,activity:at(e.node_id,t)}}m(A,"runObject");const B=864e5,ue=255;function yn(e){if(e!==void 0){if(typeof e!="string"||!e.length||e.length>ue)throw R(`Idempotency-Key must be 1-${ue} characters`);return e}}m(yn,"idempotencyKeyOf");function x(e,t,r,s=G()){const i=w(s,"SELECT body_hash, run_id, response_json, created_at FROM idempotency_keys WHERE grantee = ? AND key = ?").get(e,t);if(!(!i||Date.now()-Date.parse(i.created_at)>=B)){if(i.body_hash!==r)throw new S(409,"idempotency_conflict","key used with a different request");return{run_id:i.run_id,response:JSON.parse(i.response_json)}}}m(x,"findIdempotent");function me(e,t,r,s,i,c,d){w(e,"DELETE FROM idempotency_keys WHERE grantee = ? AND key = ? AND created_at <= ?").run(t,r,new Date(Date.now()-B).toISOString()),w(e,"INSERT INTO idempotency_keys (grantee, key, body_hash, run_id, response_json, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(t,r,s,i,JSON.stringify(c),d)}m(me,"recordIdempotent");function Rn(e=Date.now()){return L(t=>Number(w(t,"DELETE FROM idempotency_keys WHERE created_at <= ?").run(new Date(e-B).toISOString()).changes))}m(Rn,"pruneExpiredIdempotencyKeys");function le(e,t,r,s,i){const c=s.text;let d=0;return{messageId:Ue(t,{kind:"message",label:c.slice(0,120),senderNodeId:"human",senderName:r,bodyJson:i?{body:c}:{body:c,start_turn:!1},urgency:i?"normal":"deferred",wake:i?1:0,withinEnqueue:m((a,_)=>{s.uses.length&&(ie(r,s.uses,a),de(s.uses.map(({upload_id:y,path:E})=>({upload_id:y,path:E,run_id:t})),a)),d=se(a,t,t,"node.message.accepted",{message_id:_,start_turn:i}).sequence_number},"withinEnqueue")}).messageId,sequence:d}}m(le,"acceptRunMessage");class fe extends Error{static{m(this,"IdempotentReplay")}winner;constructor(t){super("idempotent replay"),this.winner=t}}const gt=1440*60*1e3;function _e(e){const t=typeof e=="string"?Qe(e):null;if(t===null||!Number.isSafeInteger(t))throw R(`deadline '${String(e)}' is not a duration; use one or more <int>[smhd] parts such as 10m or 1h30m`,"deadline");if(t<=0)throw R("deadline must be greater than zero","deadline");if(t>gt)throw R("deadline must be at most 24h","deadline");return t}m(_e,"parseRunDeadlineMs");async function Sn(e,t,r,s=ce){if(t.isolated&&!Ke())throw new S(503,"isolated_unavailable","isolated runs require the Linux root launcher","isolated");t.deadline!==void 0&&_e(t.deadline);const i=J("sha256").update(JSON.stringify(t)).digest("hex");if(r){const n=x(e.grantee,r,i);if(n){const u=C(n.run_id,e.grantee);return!u.pi_pid&&I(u)==="running"&&(t.output_schema!==void 0&&!u.outcome_at&&nt(u.node_id).state==="absent"&&ee(u.node_id,u.grantee)===void 0&&te(u.node_id,"terminal",t.output_schema),await F(u.node_id,{resume:!0,capacity:"freeze"})),n.response}}await ct(e.grantee);const c=t.profile===void 0?Be(e.grantee):$e(t.profile),d=c.manifest.grantee??"app:terminal";if(d!==e.grantee){const n=`crtr:memory:rw:${d}:profile:${c.profileId}`;if(!e.resolved.some(u=>W(u,n))||t.scopes!==null&&!t.scopes.some(u=>W(u,n)))throw new S(403,"scope_missing",`missing scope ${n}`,"profile",void 0,n)}if(t.model!==void 0){const n=t.model;if(ze(n)){if(n.includes("/")){const u=n.lastIndexOf("/"),h=n.slice(0,u),g=n.slice(u+1);if(["ultra","strong","medium","light"].includes(g)&&Ve({kind:"logical",family:h,strength:g,spec:n},H(e.grantee).files,c.profileId).every(T=>T.family!==h))throw R(`model '${n}' names a model family that is not configured in this daemon`,"model")}}else if(Ze(n)===void 0)throw R(`model '${n}' is not a strength (ultra, strong, medium, light), a family/strength, or a provider/model id`,"model")}const o=Ye(),a=Te(H(e.grantee).nodes,o);if(t.isolated&&t.cwd!==void 0){const n=De(a,Me(t.cwd));if(!Y(t.cwd)||n===".."||n.startsWith(`..${Ie}`)||Y(n))throw R("cwd must be inside the isolated root node folder","cwd")}const _=await s(o,e.grantee,t.prompt),y=_.text;let E;try{E=t.message===void 0?void 0:await s(o,e.grantee,t.message)}catch(n){throw _.cleanup(),n}let p;try{await Xe({kind:et(c),mode:"base",root:!0,rootLifecycle:"resident",readiness:"launch",cwd:t.isolated?t.cwd??a:H(e.grantee).files,profile:c.profileId,prompt:y,nodeId:o,isolated:t.isolated??!1,...t.model===void 0?{}:{model:t.model},...t.deadline===void 0?{}:{deadlineAt:new Date(Date.now()+_e(t.deadline)).toISOString()},...t.scopes===null?{}:{scopes:t.scopes},run:{grantee:e.grantee,sub:e.sub,grant_id:e.grant,client_id:e.client_id,metadata_json:JSON.stringify(t.metadata),run_id:o,event_cursor:1,usage_json:JSON.stringify(U())},withinCreate:m((n,u)=>{if(r){const g=x(e.grantee,r,i,n);if(g)throw new fe(g)}const h=_.uses;if(h.length&&(ie(e.grantee,h,n),de(h.map(({upload_id:g,path:T})=>({upload_id:g,path:T,run_id:o})),n)),Ge(o,y),p={run_id:o,node_id:o,grantee:e.grantee,grant:e.grant,rev:e.rev,scopes:t.scopes,payer:"user",status:"running",created_at:u.created,metadata:t.metadata,sequence_number:0},se(n,o,o,"run.started",{scopes:t.scopes,payer:"user",usage:U()}),E){const g=le(n,o,e.grantee,E,!1);p.message={run_id:o,message_id:g.messageId,sequence_number:g.sequence}}r&&me(n,e.grantee,r,i,o,p,u.created)},"withinCreate")},t.output_schema===void 0?void 0:n=>{te(n,"terminal",t.output_schema)})}catch(n){if(N(o)||(_.cleanup(),E?.cleanup()),n instanceof fe)return n.winner.response;if(t.model!==void 0&&n instanceof Oe&&n.code==="usage")throw R(n.message,"model");if(n instanceof be&&(n.code==="isolated_run"||n.code==="invalid_request")){const u=n.details&&typeof n.details=="object"&&"param"in n.details&&typeof n.details.param=="string"?n.details.param:void 0;throw new S(n.status,n.code,n.message,u)}throw n}if(!p)throw new Error("run creation did not commit");return p}m(Sn,"startRun");function Nn(e,t){let r;if(t.after)try{const o=JSON.parse(Buffer.from(t.after,"base64url").toString());if(!o||typeof o!="object"||!("activity"in o)||typeof o.activity!="string"||!("id"in o)||typeof o.id!="string")throw new Error("invalid cursor");r={activity:o.activity,id:o.id}}catch{throw R("invalid list cursor","after")}const i=O().prepare(`SELECT node_id, COALESCE(last_activity_at, created) AS activity FROM node_rows
|
|
7
7
|
WHERE run_id = node_id AND deleting_at IS NULL ${e===null?"":"AND grantee = ?"}
|
|
8
|
-
ORDER BY activity DESC, node_id DESC`).all(...e===null?[]:[e]).filter(({node_id:o})=>!t.visible||t.visible(o)).filter(({activity:o,node_id:a})=>!r||o<r.activity||o===r.activity&&a<r.id).map(o=>({root:
|
|
8
|
+
ORDER BY activity DESC, node_id DESC`).all(...e===null?[]:[e]).filter(({node_id:o})=>!t.visible||t.visible(o)).filter(({activity:o,node_id:a})=>!r||o<r.activity||o===r.activity&&a<r.id).map(o=>({root:N(o.node_id),activity:o.activity})).filter(({root:o})=>!t.status||I(o)===t.status),c=i.slice(0,t.limit),d=c.at(-1);return{runs:c.map(({root:o})=>A(o)),next:i.length>t.limit&&d?Buffer.from(JSON.stringify({activity:d.activity,id:d.root.node_id})).toString("base64url"):null}}m(Nn,"listRuns");async function vn(e,t,r){const s=C(e,t),i=m(()=>A(C(e,t)),"read");return!r||P.includes(I(s))?i():new Promise((c,d)=>{const o=m(()=>{clearTimeout(y),clearInterval(_),a();try{c(i())}catch(E){d(E)}},"finish"),a=dt(e,()=>{try{P.includes(String(i().status))&&o()}catch(E){clearTimeout(y),clearInterval(_),a(),d(E)}}),_=setInterval(()=>{P.includes(String(i().status))&&o()},250),y=setTimeout(o,r*1e3)})}m(vn,"waitRun");async function bn(e,t,r=ce,s=!0,i){const c=e.status==="canceled"||e.outcome_reason==="closed",d=e.grantee??"app:terminal",o=J("sha256").update(JSON.stringify(["run.message",e.node_id,t,s])).digest("hex"),a=m(async p=>{const n=N(e.node_id),u=w(O(),"SELECT 1 FROM deliveries WHERE message_id = ? AND state = 'pending' AND canceled_at IS NULL").get(String(p.message_id));return n&&!c&&s&&!n.pi_pid&&u&&await F(e.node_id,{resume:!0,capacity:"freeze"}),p},"replay");if(i){const p=x(d,i,o);if(p)return a(p.response)}if(c)throw new S(409,"run_settled","canceled run cannot receive a message");const _=await r(e.node_id,d,t),y=s&&e.final_report&&!e.pi_pid?(await import("../runtime/reopen.js")).commitReopenResident:void 0;let E;try{E=L(p=>{if(i){const h=x(d,i,o,p);if(h)return{replayed:h.response}}y?.(e.node_id);const n=le(p,e.node_id,d,_,s),u={run_id:e.node_id,message_id:n.messageId,sequence_number:n.sequence};return i&&me(p,d,i,o,e.node_id,u,new Date().toISOString()),{result:u}})}catch(p){throw _.cleanup(),p}return"replayed"in E?(_.cleanup(),a(E.replayed)):(s&&!e.pi_pid&&await F(e.node_id,{resume:!0,capacity:"freeze"}),E.result)}m(bn,"messageRun");async function Et(e){const t=m(()=>new S(503,"service_unavailable",`node ${e} broker was unavailable before the interrupt reached it`),"unavailable");for(let r=0;r<2;r++){const s=N(e);if(!s)throw q();if(!Z(s)){if(s.streaming)throw t();return}const i=z(e)?.executionId;try{await Je(e)==="turn"&&ae({nodes:[e]});return}catch(c){if(!(c instanceof je))throw c;const d=N(e);if(!d)throw q();if(!d.streaming)return;if(r===0&&Z(d)&&z(e)?.executionId!==i)continue;throw t()}}throw t()}m(Et,"interruptNode");async function On(e,t="stop"){const r=t==="stop"?w(O(),"SELECT node_id FROM nodes WHERE run_id = ?").all(e.node_id).map(({node_id:s})=>s):[e.node_id];return await Promise.all(r.map(s=>Et(s))),A(N(e.node_id)??e)}m(On,"interruptRun");async function Dn(e,t={}){const r=w(O(),"SELECT node_id FROM nodes WHERE run_id = ? ORDER BY created DESC").all(e.node_id).map(d=>d.node_id);ae({run:e.node_id});const s=[];if(I(e)!=="settled")s.push(...ne(e.node_id,{preserveEmpty:!0,stopReason:"run_canceled"}).closed);else for(const d of r){if(d===e.node_id)continue;const o=N(d);o&&o.status!=="done"&&o.status!=="canceled"&&o.status!=="dead"&&s.push(...ne(d,{preserveEmpty:!0,stopReason:"run_canceled"}).closed)}if(await re({run_id:e.node_id},"the run was canceled"),(await Promise.all([...new Set([e.node_id,...s])].map(d=>V(d,"run_canceled")))).some(d=>!d.confirmed))throw new S(500,"server_error","run process teardown could not be confirmed",void 0,void 0,void 0,!0);const c=N(e.node_id);return A(c??e)}m(Dn,"cancelRun");const pe=200;function Mn(e,t){if(typeof t!="string"||!t.trim())throw R("name is required","name");const r=t.trim();if(r.length>pe)throw R(`name exceeds ${pe} characters`,"name");const s=We(e.node_id,{name:r});return s.window&&He(s.window,Fe(s)),A(s)}m(Mn,"renameRun");async function In(e,t={config:lt,client:mt}){await re({run_id:e.node_id},"the run was deleted");const r=L(a=>(w(a,"UPDATE nodes SET deleting_at = COALESCE(deleting_at, ?) WHERE node_id = ?").run(new Date().toISOString(),e.node_id),w(a,"SELECT node_id FROM nodes WHERE run_id = ? ORDER BY created DESC").all(e.node_id).map(_=>_.node_id)));if((await Promise.all(r.map(a=>V(a,"run_deleted",{deleting:!0})))).some(a=>!a.confirmed))throw new S(500,"server_error","run process teardown could not be confirmed",void 0,void 0,void 0,!0);const i=r.map(a=>{const _=xe(a);if(_.app===null)throw new Error(`run node missing during deletion: ${a}`);return _}),c=w(O(),"SELECT DISTINCT session_id FROM sessions WHERE session_id IS NOT NULL AND node_id IN (SELECT node_id FROM nodes WHERE run_id = ?)").all(e.node_id);for(const{session_id:a}of c)try{Q().delete(a)}catch(_){if(!qe(_))throw _}const d=L(a=>{for(const p of r){const n=Ce(a,p);n!==null&&n.deleted_at===null&&Le(a,n)}const _=_t([e.node_id],a);w(a,"DELETE FROM run_events WHERE run_id = ?").run(e.node_id),w(a,"DELETE FROM idempotency_keys WHERE run_id = ?").run(e.node_id);const y=r.map(()=>"?").join(","),E=a.prepare(`SELECT message_id FROM messages WHERE sender_node_id IN (${y}) UNION SELECT message_id FROM deliveries WHERE destination_id IN (${y})`).all(...r,...r);a.prepare(`DELETE FROM deliveries WHERE destination_id IN (${y})`).run(...r);for(const{message_id:p}of E)w(a,"DELETE FROM messages WHERE message_id = ? AND NOT EXISTS (SELECT 1 FROM deliveries WHERE message_id = ?)").run(p,p);return _});for(const a of i)ke(a);const{notifyRunDeleted:o}=await import("./events.js");return o(e.node_id),{uploads_pending:await wt(d,t)}}m(In,"deleteRun");async function wt(e,t){if(!e.length||!t.config())return e.length;let r;try{r=t.client()}catch{return e.length}const s=new Set,i=e.map(d=>r.deleteUpload(d).then(()=>{ft(d),s.add(d)},()=>{}));let c;return await Promise.race([Promise.all(i),new Promise(d=>{c=setTimeout(d,t.budgetMs??pt),c.unref?.()})]),clearTimeout(c),e.length-s.size}m(wt,"deleteReleasedUploads");function Tn(e,t,r){C(e,t),X(e);const s=O();s.exec("BEGIN");try{const c=w(s,"SELECT node_id, parent FROM nodes WHERE run_id = ? ORDER BY CASE WHEN node_id = ? THEN 0 ELSE 1 END, created, node_id").all(e,e).map(n=>({node_id:n.node_id,parent_node_id:n.parent,conversations:w(s,"SELECT DISTINCT session_id FROM sessions WHERE node_id = ? ORDER BY created_at").all(n.node_id).map(({session_id:u})=>{const{header:h,entries:g}=Q().read(u);if(r==="entries")return{conversation_id:u,header:h,entries:g};const T=Ae.inMemory(h.cwd,void 0,[h,...g]),ge=tt(T)??[],$=new Map(g.map((f,v)=>[f.id,v])),Ee=g.filter(f=>f.type==="custom"&&f.customType==="crtr-system-prompt").map(f=>({id:f.id,message:{role:"custom",customType:"crtr-system-prompt",data:"data"in f?f.data:void 0,timestamp:f.timestamp}})),j=g.filter(f=>f.type==="custom_message"&&f.customType==="app_message").map(f=>({id:f.id,message:{role:"custom",customType:"app_message",content:"content"in f?f.content:"",timestamp:f.timestamp}})),we=n.node_id===e?g.find(f=>{if(f.type!=="message"||f.message.role!=="user")return!1;const v=K(g,f.message.timestamp);return v?.type==="runtime"&&v.kind==="kickoff"})?.id:void 0,he=[...ge.filter(({id:f})=>!j.some(v=>v.id===f)),...Ee,...j].sort((f,v)=>($.get(f.id)??Number.MAX_SAFE_INTEGER)-($.get(v.id)??Number.MAX_SAFE_INTEGER)).map(({id:f,message:v})=>{const l=v,D=l.usage,k=l.role==="user"?K(g,Number(l.timestamp)):null,ye=k?.type==="human"||f===we,M=l.role==="toolResult"?"tool_result":l.role==="bashExecution"?"bash_execution":l.role==="compactionSummary"?"compaction_summary":l.role==="branchSummary"?"branch_summary":l.role==="custom"&&l.customType==="crtr-system-prompt"?"system":l.role==="user"?ye?"user":"custom":l.role,Re=M==="custom"?l.role==="custom"?l.customType:k?.type==="runtime"?k.kind:"unknown":void 0,Se=M==="system"&&l.customType==="crtr-system-prompt"?[{type:"text",text:l.data?.text??""}]:typeof l.content=="string"?[{type:"text",text:l.content}]:Array.isArray(l.content)?l.content.map(b=>b.type==="toolCall"?{type:"tool_call",tool_call_id:b.id,name:b.name,arguments:b.arguments}:b.type==="image"?{type:"image",mime_type:b.mimeType}:b.type==="thinking"?{type:"thinking",text:b.thinking}:b):[];return{entry_id:f,role:M,...M==="custom"?{custom_type:Re}:{},time:typeof l.timestamp=="string"?l.timestamp:new Date(Number(l.timestamp??Date.now())).toISOString(),content:Se,...M==="assistant"?{model:l.model,provider:l.provider,stop_reason:l.stopReason,...l.errorMessage?{error_message:l.errorMessage}:{},...D?{usage:{input_tokens:D.input??0,output_tokens:D.output??0,cache_read_tokens:D.cacheRead??0,cache_write_tokens:D.cacheWrite??0,cost_usd:D.cost?.total??0}}:{}}:{},...M==="tool_result"?{tool_call_id:l.toolCallId,tool_name:l.toolName,is_error:l.isError}:{}}})??[];return{conversation_id:u,messages:he}})})),o=w(s,"SELECT event_cursor FROM nodes WHERE node_id = ?").get(e).event_cursor-1,a=new Map(c.map(n=>[n.node_id,ot(n.node_id)])),_=w(s,`SELECT node_id, sequence_number FROM run_events
|
|
9
9
|
WHERE run_id = ? AND sequence_number <= ? AND type IN ('node.thinking.done', 'node.output_text.done', 'usage')
|
|
10
|
-
ORDER BY sequence_number`).all(e,o);for(const n of
|
|
10
|
+
ORDER BY sequence_number`).all(e,o);for(const n of _){const u=a.get(n.node_id);u!=null&&n.sequence_number>=u&&a.set(n.node_id,null)}const y=w(s,`SELECT node_id, sequence_number, type, body_json FROM run_events
|
|
11
11
|
WHERE run_id = ? AND sequence_number <= ? AND type IN ('node.thinking.delta', 'node.output_text.delta')
|
|
12
|
-
ORDER BY sequence_number`).all(e,o),E=new Map;for(const n of y){const u=a.get(n.node_id);if(u==null||n.sequence_number<u)continue;const h=JSON.parse(n.body_json),g=E.get(n.node_id)??{thinking:"",text:""};n.type==="node.thinking.delta"?g.thinking+=h.delta:g.text+=h.delta,E.set(n.node_id,g)}const
|
|
12
|
+
ORDER BY sequence_number`).all(e,o),E=new Map;for(const n of y){const u=a.get(n.node_id);if(u==null||n.sequence_number<u)continue;const h=JSON.parse(n.body_json),g=E.get(n.node_id)??{thinking:"",text:""};n.type==="node.thinking.delta"?g.thinking+=h.delta:g.text+=h.delta,E.set(n.node_id,g)}const p=c.map(n=>({...n,in_progress_message:a.get(n.node_id)==null?null:E.get(n.node_id)??{thinking:"",text:""}}));return s.exec("COMMIT"),{run_id:e,latest_sequence:o,nodes:p}}catch(i){throw s.exec("ROLLBACK"),i}}m(Tn,"traceRun");function An(e,t,r){if(t===void 0){if(!e.some(s=>W(s,"crtr:llm")))throw new S(403,"scope_missing","missing scope crtr:llm",void 0,void 0,"crtr:llm");return null}if(!Array.isArray(t)||t.some(s=>typeof s!="string"))throw R("scopes must be a list of scope strings","scopes");for(const s of t){try{it(s)}catch{throw R(`invalid scope ${s}`,"scopes")}if(/^crtr:files:(?:read|rw):app:/.test(s)&&s.split(":")[4]===r.slice(4))throw R(`own-app files cannot narrow a run: ${s}`,"scopes");if(!(/^crtr:memory:(?:read|rw):app:/.test(s)&&s.split(":")[4]===r.slice(4))&&!ve(e,s))throw new S(403,"scope_missing",`missing scope ${s}`,void 0,void 0,s)}if(!t.includes("crtr:llm"))throw new S(403,"scope_missing","missing scope crtr:llm",void 0,void 0,"crtr:llm");return t}m(An,"requestedScopes");export{B as IDEMPOTENCY_WINDOW_MS,gt as RUN_DEADLINE_MAX_MS,pt as RUN_DELETE_STORAGE_BUDGET_MS,pe as RUN_NAME_MAX,S as RunError,Dn as cancelRun,In as deleteRun,yn as idempotencyKeyOf,On as interruptRun,Nn as listRuns,bn as messageRun,C as ownedRun,_e as parseRunDeadlineMs,Rn as pruneExpiredIdempotencyKeys,Mn as renameRun,An as requestedScopes,wn as rewriteLocalIdempotencyGrantee,hn as runIsRunning,A as runObject,Sn as startRun,Tn as traceRun,vn as waitRun};
|
|
@@ -30,7 +30,7 @@ export declare function recordModelUsage(grantee: string, payer: Payer, servedMo
|
|
|
30
30
|
output: number;
|
|
31
31
|
cacheRead: number;
|
|
32
32
|
}, usdEquivalent: number | null): void;
|
|
33
|
-
/** The grantee's user-paid month-to-date spend
|
|
33
|
+
/** The grantee's user-paid month-to-date model spend; provider receipts are not in it. */
|
|
34
34
|
export declare function monthSpend(grantee: string, month?: string): number;
|
|
35
35
|
/** Who a user-paid call bills, and the cap that bounds it (`null` = uncapped). */
|
|
36
36
|
export interface SpendTarget {
|
|
@@ -5,6 +5,6 @@ var q=Object.defineProperty;var s=(e,t)=>q(e,"name",{value:t,configurable:!0});i
|
|
|
5
5
|
output_tokens = output_tokens + excluded.output_tokens,
|
|
6
6
|
cached_tokens = cached_tokens + excluded.cached_tokens,
|
|
7
7
|
usd_equivalent = CASE WHEN usd_equivalent IS NULL AND excluded.usd_equivalent IS NULL THEN NULL
|
|
8
|
-
ELSE COALESCE(usd_equivalent, 0) + COALESCE(excluded.usd_equivalent, 0) END`).run(t.grantee,u,t.payer,t.source,t.served_model,t.input_tokens,t.output_tokens,t.cached_tokens,t.usd_equivalent),t.payer==="user"&&t.usd_equivalent!==null&&t.usd_equivalent>0&&d(e,`INSERT INTO grantee_usage (grantee, month, usd_equivalent) VALUES (?, ?, ?)
|
|
8
|
+
ELSE COALESCE(usd_equivalent, 0) + COALESCE(excluded.usd_equivalent, 0) END`).run(t.grantee,u,t.payer,t.source,t.served_model,t.input_tokens,t.output_tokens,t.cached_tokens,t.usd_equivalent),t.payer==="user"&&t.source==="model"&&t.usd_equivalent!==null&&t.usd_equivalent>0&&d(e,`INSERT INTO grantee_usage (grantee, month, usd_equivalent) VALUES (?, ?, ?)
|
|
9
9
|
ON CONFLICT(grantee, month) DO UPDATE SET usd_equivalent = usd_equivalent + excluded.usd_equivalent`).run(t.grantee,u,t.usd_equivalent)}s(S,"addUsage");function L(e,t,n,u,o){k(i=>S(i,{grantee:e,payer:t,source:"model",served_model:n,input_tokens:u.input,output_tokens:u.output,cached_tokens:u.cacheRead,usd_equivalent:o}))}s(L,"recordModelUsage");function m(e,t=p()){return d(_(),"SELECT usd_equivalent FROM grantee_usage WHERE grantee = ? AND month = ?").get(e,t)?.usd_equivalent??0}s(m,"monthSpend");function E(e){return e.grantee===N?null:e.spend_cap}s(E,"effectiveCap");function O(e,t=new Date){if(!e)return;const n=E(e);if(n===null)return;const u=p(t),o=m(e.grantee,u);if(!(o<n))throw new l(402,"cap_exhausted",`${e.grantee} reached its $${n} monthly spend cap ($${o.toFixed(2)} used this month)`,void 0,{spend_cap:n,usd_equivalent:o,reset_at:c(u)})}s(O,"assertUnderCap");function T(e,t,n=p()){const u=d(_(),`SELECT payer, source, served_model, input_tokens, output_tokens, usd_equivalent
|
|
10
10
|
FROM usage WHERE grantee = ? AND month = ? ORDER BY served_model`).all(e,n),o=s(r=>u.filter(a=>a.payer===r&&a.source==="model").map(({served_model:a,input_tokens:v,output_tokens:h,usd_equivalent:f})=>({served_model:a,input_tokens:v,output_tokens:h,usd_equivalent:f})),"byModel"),i=u.filter(r=>r.payer==="app").reduce((r,a)=>r+(a.usd_equivalent??0),0);return{grantee:e,month:n,usd_equivalent:m(e,n),by_model:o("user"),app_paid:{usd_equivalent:i,by_model:o("app")},spend_cap:E({grantee:e,spend_cap:t}),reset_at:c(n)}}s(T,"usageFor");function A(e){if(e===null)return p();if(!/^[1-9]\d{3}-(0[1-9]|1[0-2])$/.test(e))throw new l(400,"invalid_request","month must be YYYY-MM","month");return e}s(A,"parseUsageMonth");export{N as UNCAPPED_GRANTEE,S as addUsage,O as assertUnderCap,E as effectiveCap,m as monthSpend,A as parseUsageMonth,L as recordModelUsage,c as resetAt,T as usageFor,p as usageMonth};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var R=Object.defineProperty;var m=(a,t)=>R(a,"name",{value:t,configurable:!0});import{callerGrant as h}from"../principal.js";import{grantVersion as g}from"../../../core/grants/mirror.js";import{waitGrantChange as S}from"../../../core/grants/watch.js";import{RunError as n,idempotencyKeyOf as b,cancelRun as $,deleteRun as N,interruptRun as O,listRuns as A,messageRun as j,renameRun as x,requestedScopes as B,startRun as J,traceRun as z,waitRun as C}from"../../../core/runs/operations.js";import{canReadRun as v,requireRun as
|
|
1
|
+
var R=Object.defineProperty;var m=(a,t)=>R(a,"name",{value:t,configurable:!0});import{callerGrant as h}from"../principal.js";import{grantVersion as g}from"../../../core/grants/mirror.js";import{waitGrantChange as S}from"../../../core/grants/watch.js";import{RunError as n,idempotencyKeyOf as b,cancelRun as $,deleteRun as N,interruptRun as O,listRuns as A,messageRun as j,renameRun as x,requestedScopes as B,startRun as J,traceRun as z,waitRun as C}from"../../../core/runs/operations.js";import{canReadRun as v,requireRun as l,runCallerApp as E}from"../router.js";import{waitSeconds as y}from"../long-poll-wait.js";import{outputSchemaProblem as K}from"../../../core/runtime/structured-output.js";function f(a){if(!a||typeof a!="object"||Array.isArray(a))throw new n(400,"invalid_request","a JSON object is required");return a}m(f,"object");function q(a,t,i,d,u){if(a===null)return u;if(!/^(0|[1-9]\d*)$/.test(a)||!Number.isSafeInteger(Number(a))||Number(a)<i||Number(a)>d)throw new n(400,"invalid_request",`invalid ${t}`,t);return Number(a)}m(q,"integer");const T=256*1024;async function W(a,t,i,d,u,_){switch(a.name){case"grant.get":{if("class"in t)throw new n(403,"permission_error","an app token is required");const e=y("grant.get",d),s=d.get("since");if(s!==null&&!/^[A-Za-z0-9_-]{1,64}$/.test(s))throw new n(400,"invalid_request","invalid since","since");let o=h(t);return e&&s!==null&&g(o)===s&&(await S(t.grant,e),o=h(t)),{status:200,body:{sub:t.sub,grantee:t.grantee,client_id:t.client_id,grant:t.grant,rev:o.rev,resolved_scopes:o.resolved_scopes,spend_cap:o.spend_cap,exp:t.exp,payer:"user",dependencies:o.dependencies??{},version:g(o)}}}case"run.start":{if("class"in t)throw new n(403,"permission_error","an app token is required");const e=f(u),s=["mode","parent","fork_from","node_id","env","tools","system_prompt"];for(const r of s)if(e[r]!==void 0)throw new n(403,"owner_only",`${r} is owner-only`,r);const o=["payer","model_policy","kind"];for(const r of o)if(e[r]!==void 0)throw new n(400,"invalid_request",`${r} is not available`,r);for(const r of Object.keys(e))if(!["prompt","scopes","metadata","profile","isolated","cwd","worktree","output_schema","message","model","deadline"].includes(r))throw new n(400,"invalid_request",`unknown field ${r}`,r);if(e.model!==void 0&&(typeof e.model!="string"||!e.model.trim()))throw new n(400,"invalid_request","model must be a non-empty string","model");if(e.deadline!==void 0&&(typeof e.deadline!="string"||!e.deadline.trim()))throw new n(400,"invalid_request","deadline must be a duration string such as 10m","deadline");if(e.profile!==void 0&&(typeof e.profile!="string"||!e.profile))throw new n(400,"invalid_request","profile must be a profile id","profile");if(e.isolated!==void 0&&typeof e.isolated!="boolean")throw new n(400,"invalid_request","isolated must be a boolean","isolated");if(e.cwd!==void 0&&(e.isolated!==!0||typeof e.cwd!="string"||!e.cwd))throw new n(400,"invalid_request","cwd is available only for an isolated run and must be an absolute path inside its root folder","cwd");if(e.worktree!==void 0&&e.isolated!==!0)throw new n(400,"invalid_request","worktree is not available","worktree");if(e.isolated===!0&&e.worktree!==void 0)throw new n(400,"invalid_request","worktree is unavailable for an isolated run","worktree");if(typeof e.prompt!="string"||!e.prompt.trim())throw new n(400,"invalid_request","prompt is required","prompt");if(e.message!==void 0&&(typeof e.message!="string"||!e.message.trim()))throw new n(400,"invalid_request","message must be a non-empty string","message");const w=e.metadata===void 0?{}:f(e.metadata);if(Buffer.byteLength(JSON.stringify(w))>4096)throw new n(400,"invalid_request","metadata exceeds 4 KB","metadata");let c;if(e.output_schema!==void 0){const r=e.output_schema;if(!r||typeof r!="object"||Array.isArray(r))throw new n(400,"invalid_request","output_schema must be a JSON Schema object","output_schema");if(Buffer.byteLength(JSON.stringify(r))>T)throw new n(400,"invalid_request","output_schema exceeds 256 KB","output_schema");const p=await K(r);if(p!==null)throw new n(400,"invalid_request",`output_schema is not usable: ${p}`,"output_schema");c=r}const k=B(t.resolved,e.scopes,t.grantee);return{status:201,body:await J(t,{prompt:e.prompt,scopes:k,metadata:w,profile:e.profile,isolated:e.isolated,cwd:e.cwd,...e.model===void 0?{}:{model:e.model},...e.deadline===void 0?{}:{deadline:e.deadline},...c===void 0?{}:{output_schema:c},...e.message===void 0?{}:{message:e.message}},b(_))}}case"run.list":{const e=d.get("status")??void 0;if(e&&!["running","waiting_on_user","idle","settled"].includes(e))throw new n(400,"invalid_request","invalid status","status");const s=d.get("app");if(s!==null&&!/^(?:app:)?[a-z0-9][a-z0-9-]*$/.test(s))throw new n(400,"invalid_request","invalid app","app");const o=s===null?E(t):s.startsWith("app:")?s:`app:${s}`;if(o!==null&&!v(t,o))throw new n(404,"not_found","run not found");return{status:200,body:A(o,{status:e,after:d.get("after")??void 0,limit:q(d.get("limit"),"limit",1,100,50),..."class"in t&&(t.class==="broker"||t.class==="cron")&&o!==null?{visible:m(w=>v(t,o,w),"visible")}:{}})}}case"run.get":{const e=y("run.get",d),s=l(t,i),o=await C(i,s.grantee,e);return l(t,i),{status:200,body:o}}case"run.message":{const e=f(u);if(typeof e.message!="string"||!e.message.trim())throw new n(400,"invalid_request","message is required","message");if(e.start_turn!==void 0&&typeof e.start_turn!="boolean")throw new n(400,"invalid_request","start_turn must be a boolean","start_turn");return{status:202,body:await j(l(t,i,!0),e.message,void 0,e.start_turn,b(_))}}case"run.interrupt":{const e=f(u===void 0?{}:u);if(e.children!==void 0&&e.children!=="stop"&&e.children!=="continue")throw new n(400,"invalid_request","invalid children","children");return{status:200,body:await O(l(t,i,!0),e.children)}}case"run.cancel":{const e=f(u===void 0?{}:u);if(e.keep_schedules!==void 0&&typeof e.keep_schedules!="boolean")throw new n(400,"invalid_request","invalid keep_schedules","keep_schedules");return{status:200,body:await $(l(t,i,!0),{keepSchedules:e.keep_schedules})}}case"run.rename":{const e=f(u);for(const s of Object.keys(e))if(s!=="name")throw new n(400,"invalid_request",`unknown field ${s}`,s);return{status:200,body:x(l(t,i,!0),e.name)}}case"run.delete":{const{uploads_pending:e}=await N(l(t,i,!0,!0));return{status:200,body:{run_id:i,uploads_pending:e}}}case"trace.get":{const e=d.get("format")??void 0;if(e&&e!=="entries")throw new n(400,"invalid_request","unknown trace format","format");const s=l(t,i);return{status:200,body:z(i,s.grantee,e)}}default:throw new n(404,"not_found","unknown operation")}}m(W,"handleRunOperation");export{W as handleRunOperation,q as integer};
|
package/docs/apps/cli.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: CLI reference
|
|
3
|
-
description: The npx @north-light/sdk commands for scaffolding an app,
|
|
3
|
+
description: The npx @north-light/sdk commands for scaffolding an app, logging in, registering it, managing it, and installing the agent skill.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# CLI reference
|
|
@@ -13,19 +13,36 @@ description: The npx @north-light/sdk commands for scaffolding an app, writing i
|
|
|
13
13
|
| `npx @north-light/sdk skill [--agent claude\|agents\|all] [--global] [--print]` | Write the packaged `northlight` skill into `.agents/skills` and `.claude/skills` (or `~/` with `--global`), replacing an older copy. `--print` writes it to stdout. | No |
|
|
14
14
|
| `npx @north-light/sdk keygen [--out <path>] [--app <client id>] [--yes]` | Write a private signing key to `local/app-key.json` (mode 0600) and print the public JWK to register. | Only with `--app` |
|
|
15
15
|
| `npx @north-light/sdk env --client-id <client id> [--origin <url>] [--issuer <url>] [--out <path>]`<br/>`npx @north-light/sdk env --app <client id> [--out <path>]` | Write `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` into `local/app.env`, keeping every other line. `--client-id` makes no network call; `--app` reads them through crtr. | Only with `--app` |
|
|
16
|
+
| `npx @north-light/sdk login [--no-browser] [--new] [--issuer <url>]` | Log this computer in to your Northlight account: prints a link and a code, you approve once in the browser, and a revocable login is saved to `~/.config/northlight/credentials.json`. Run it again if it times out: it resumes the same code. Calls only the Northlight directory. | No |
|
|
17
|
+
| `npx @north-light/sdk logout [--issuer <url>]` | Revoke this computer's login on the server and delete it locally. | No |
|
|
18
|
+
| `npx @north-light/sdk whoami [--issuer <url>]` | Show the account this computer is logged in as and when the login expires. Exits 1 when not logged in. | No |
|
|
19
|
+
| `npx @north-light/sdk register [--name <text>] [--scope <scope>]... [--logo <url>] [--justification <text>] [--redirect-uri <uri>]... [--issuer <url>]` | Run in a project made by `init`, after `login`: registers the app with the public key from `local/app-key.json`, adds each `--scope` beyond `crtr:llm crtr:act`, and writes `CROUTER_CLIENT_ID` and `CROUTER_ISSUER` to `local/app.env`. | No |
|
|
20
|
+
| `npx @north-light/sdk apps <leaf> [--app <client id>] [--issuer <url>] [leaf flags] [--json]` | Call one app-management leaf with the login and print its answer as JSON. Leaves: `mine`, `get`, `update`, `set-redirect-uris`, `asks`, `set-scopes`, `set-requires`, `set-run-notice-url`, `keys list\|add\|retire\|delete\|set-jwks-uri\|remove-jwks-uri`, `testers list\|add\|remove`. `--app` defaults to `CROUTER_CLIENT_ID` in `local/app.env`; `keys add` takes `--key-file <path>` and sends only the public half. Deleting an app, its marketplace listing, spend cap, sharing and provider role are console-only. | No |
|
|
16
21
|
|
|
17
|
-
`keygen` and `env` are the only commands that can call `crtr`, and only with `--app`. The documented path on this site never uses `--app`, so you do not need crtr.
|
|
22
|
+
`keygen` and `env` are the only commands that can call `crtr`, and only with `--app`. The documented path on this site never uses `--app`, so you do not need crtr. `login`, `logout`, `whoami`, `register` and `apps` call only the Northlight directory.
|
|
18
23
|
|
|
19
24
|
## Details
|
|
20
25
|
|
|
21
26
|
### `init`
|
|
22
27
|
|
|
23
|
-
In an empty or new directory it writes `package.json` (with `@north-light/sdk` pinned to the CLI's own exact version, plus `tsx`), `server.ts` (the quickstart server), `.gitignore` (`local/`, `node_modules/`) and `local/app.env`, and runs `keygen` into `local/app-key.json`. It installs the skill. Then it prints the public JWK
|
|
28
|
+
In an empty or new directory it writes `package.json` (with `@north-light/sdk` pinned to the CLI's own exact version, plus `tsx`), `server.ts` (the quickstart server), `.gitignore` (`local/`, `node_modules/`) and `local/app.env`, and runs `keygen` into `local/app-key.json`. It installs the skill. Then it prints the public JWK and the next steps: `login`, then `register`, then the start command. It also prints the console address for registering in the browser instead.
|
|
24
29
|
|
|
25
|
-
In a directory that already has a `package.json` it writes no app code. It runs `npm i --save-exact @north-light/sdk@<its own version>`, runs `keygen` unless `local/app-key.json` exists, and installs the skill.
|
|
30
|
+
In a directory that already has a `package.json` it writes no app code. It runs `npm i --save-exact @north-light/sdk@<its own version>`, runs `keygen` unless `local/app-key.json` exists, and installs the skill. Because `register` reads `APP_ORIGIN` from `local/app.env`, which `init` writes only for a new app, an existing project without that file gets the console steps and the `env --client-id` command as its next step instead.
|
|
26
31
|
|
|
27
32
|
It never overwrites a file. On a refused step it exits non-zero and names the file it refused.
|
|
28
33
|
|
|
34
|
+
### `login`, `logout`, `whoami`
|
|
35
|
+
|
|
36
|
+
`login` prints a link and a code and waits while you approve in the browser, then saves a revocable login to `~/.config/northlight/credentials.json`. `--no-browser` does not open the link; `--new` discards a pending code and starts a fresh one. `whoami` shows the account and when the login expires, and exits 1 when this computer is not logged in. `logout` revokes the login on the server and deletes the local file. The login can manage your apps; it cannot delete one (not yet checked on production).
|
|
37
|
+
|
|
38
|
+
### `register`
|
|
39
|
+
|
|
40
|
+
Run it in the project `init` made, after `login`. It sends the app's origin from `local/app.env`, the public half of `local/app-key.json` and the permissions you pass with `--scope`, and writes the client ID and issuer it gets back into `local/app.env`. `crtr:llm` and `crtr:act` are always requested (not yet checked on production).
|
|
41
|
+
|
|
42
|
+
### `apps`
|
|
43
|
+
|
|
44
|
+
`apps <leaf>` runs one app-management leaf with the login and prints the answer as JSON. It reads the app from `--app`, else from `CROUTER_CLIENT_ID` in `local/app.env`. `apps keys add --key-file local/app-key.json` sends only the public half.
|
|
45
|
+
|
|
29
46
|
### `skill`
|
|
30
47
|
|
|
31
48
|
Writes the `northlight` Agent Skill that ships in the package, replacing an older copy. `--agent` picks `.claude/skills`, `.agents/skills` or both. `--global` writes under your home directory instead of the project. The skill's `metadata.sdk-version` is the package version, so run `skill` again after you change the pinned SDK version.
|
|
@@ -5,7 +5,7 @@ description: What npx @north-light/sdk init and skill install for Claude Code, C
|
|
|
5
5
|
|
|
6
6
|
# Use your coding agent
|
|
7
7
|
|
|
8
|
-
The `northlight` skill teaches a coding agent the whole path in the [quick setup](/docs/quickstart): the commands, the
|
|
8
|
+
The `northlight` skill teaches a coding agent the whole path in the [quick setup](/docs/quickstart): the commands, the code patterns and the rules. With it, your agent signs you up, logs in, registers the app and runs it, and you choose **Allow** twice in your browser: once on the login page, once on the app's consent page. `npx @north-light/sdk init` installs it into your project. The skill needs no account and no crtr.
|
|
9
9
|
|
|
10
10
|
## Install it
|
|
11
11
|
|
|
@@ -44,9 +44,32 @@ An agent that has no project yet can read the hosted copy:
|
|
|
44
44
|
|
|
45
45
|
> Read https://sdk.docs.northlight.ai/SKILL.md and follow it.
|
|
46
46
|
|
|
47
|
-
The agent runs `npx @north-light/sdk@latest init`
|
|
47
|
+
The agent runs `npx @north-light/sdk@latest init`, then `npx @north-light/sdk login`, which prints a link and a code. It shows you both. You open the link, check the page shows the same code, and choose **Allow**. The agent then runs `npx @north-light/sdk register --scope sms:send`, installs, starts the app, and asks you to open `/auth/login` and choose **Allow** on the consent page. You sign up for Northlight first. The hosted copy matches the SDK version the docs site was last built from.
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
If the agent's shell call times out while you approve, it runs `login` again and the same code resumes. If it cannot reach `account.northlight.ai`, it falls back to asking you to register in the console.
|
|
50
|
+
|
|
51
|
+
Checked with Claude Code (`claude -p`, no crtr on `PATH`, an empty directory) before `login` existed: given only the skill URL plus "I want the quickstart app: sign in with Northlight, start a run, text me", it ran `init my-app`, read the generated `server.ts`, and stopped with the console steps and the public key for you to paste. Name what you want built in the prompt. The journey with `login` and `register` has not been run by an agent on production (not yet checked on production).
|
|
52
|
+
|
|
53
|
+
## What the login can do
|
|
54
|
+
|
|
55
|
+
`login` saves a revocable login for your computer. It is for you and the agent on your laptop, not for CI.
|
|
56
|
+
|
|
57
|
+
| It can | It cannot |
|
|
58
|
+
|---|---|
|
|
59
|
+
| Register new apps (at most 10 a day) | Delete an app |
|
|
60
|
+
| Change a name, origin, logo and description | List an app in the marketplace, or change its spend cap or sharing |
|
|
61
|
+
| Change what an app asks for and requires | Set the provider role |
|
|
62
|
+
| Add and remove testers | See private keys, or use your runtime, your AI subscription, your texts or your data |
|
|
63
|
+
| Change signing keys, redirect URIs and the run notice URL on an app in testing | Change those on a verified app: do it in the console |
|
|
64
|
+
| | Approve another login |
|
|
65
|
+
|
|
66
|
+
Deleting an app, its listing, spend cap and sharing stay in the console on purpose. The CLI refuses them without sending a request, and the directory refuses them if a call is made by hand.
|
|
67
|
+
|
|
68
|
+
The login lives in `~/.config/northlight/credentials.json` (`$XDG_CONFIG_HOME/northlight/credentials.json` when set), mode `0600`, outside your project. The CLI refuses a file that other users can read. It lasts 30 days, or until it has gone 14 days unused, and then you run `login` again. An account holds at most 10 logins; an eleventh replaces the oldest.
|
|
69
|
+
|
|
70
|
+
Revoke it on your account's Developer access page (`https://account.northlight.ai/account/developer`), or run `npx @north-light/sdk logout`, which revokes it on the server and deletes the file. The next command then fails and asks for `login`.
|
|
71
|
+
|
|
72
|
+
Not yet checked on production: every statement in this section, including the limits and the page.
|
|
50
73
|
|
|
51
74
|
## How it stays current
|
|
52
75
|
|
|
@@ -60,14 +83,15 @@ After `init`:
|
|
|
60
83
|
|
|
61
84
|
Starting from nothing:
|
|
62
85
|
|
|
63
|
-
> Read https://sdk.docs.northlight.ai/SKILL.md and build me an app where people sign in with Northlight and the app texts them a daily summary.
|
|
86
|
+
> Read https://sdk.docs.northlight.ai/SKILL.md and build me an app where people sign in with Northlight and the app texts them a daily summary.
|
|
64
87
|
|
|
65
88
|
## Rules the skill gives your agent
|
|
66
89
|
|
|
67
90
|
Your agent reads them from the skill. They are worth knowing as the person reviewing its work:
|
|
68
91
|
|
|
69
92
|
- Pin the exact SDK version.
|
|
70
|
-
- Take scope strings from the console's Add permissions list, never from memory.
|
|
93
|
+
- Take scope strings from `apps asks` or the console's Add permissions list, never from memory.
|
|
94
|
+
- Never print, paste or commit `~/.config/northlight/credentials.json`, and leave deleting, listing and the spend cap to you in the console.
|
|
71
95
|
- Never print, paste or commit `local/app-key.json`.
|
|
72
96
|
- Reach a person's runtime with `oauth.client(userId)`, not `new Crouter()`.
|
|
73
97
|
- Use a persistent `ConnectionStore` in production.
|
|
@@ -5,7 +5,7 @@ description: What registering an app creates, how an app proves who it is with a
|
|
|
5
5
|
|
|
6
6
|
# Apps and sign-in
|
|
7
7
|
|
|
8
|
-
An app is a registration in the Northlight directory: a client ID, a public signing key, the redirect URIs people return to after sign-in, and the list of scopes the app may ask for. You create it in the console at `https://account.northlight.ai/console/new`. Your server proves it is that app by signing its requests with the matching private key.
|
|
8
|
+
An app is a registration in the Northlight directory: a client ID, a public signing key, the redirect URIs people return to after sign-in, and the list of scopes the app may ask for. You create it with `npx @north-light/sdk register` after `npx @north-light/sdk login`, or in the console at `https://account.northlight.ai/console/new`. Your server proves it is that app by signing its requests with the matching private key.
|
|
9
9
|
|
|
10
10
|
## What registration holds
|
|
11
11
|
|