@north-light/cli 0.3.456 → 0.3.458

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.
@@ -10,3 +10,8 @@ surfaces:
10
10
  # `@north-light/plugin`
11
11
 
12
12
  Read `crouter-plugin/README` for the authoring overview and the directory pages for the specific plugin surface. The directory listing names every page. Use the directory only to choose a focused page; the reference pages hold the authoring details.
13
+
14
+ Two things a provider registered in the directory must get right, both in `docs/plugin/deploying`:
15
+
16
+ - A third-party app passes `auth: {issuer, audience, providerId: <its client id>}` to `createFetchHandler`. The directory mints scopes as `<provider id>:<group>`; without `providerId` the kit expects `<plugin name>:<group>` and answers every call 403 `scope_missing`.
17
+ - On Node, Express or Nest, mount the handler with `createNodeHandler(handler, {bodyLimit?, origin?})` instead of hand-writing a request/response adapter. It bounds the body (413 before the handler), streams the response with backpressure, and keeps repeated headers.
@@ -19,4 +19,4 @@ Start with the `node_modules/@north-light/plugin/README.md` for a complete plugi
19
19
  - [[crouter-plugin/deploying]] — Fetch frameworks, mount paths, proxies, authentication, and archive compression.
20
20
  - [[crouter-plugin/bundles-and-memory]] — generating an archive and shipping agent-facing documents.
21
21
 
22
- The package exports only `LeafError`, `ManifestInvalidError`, `PluginDefinitionError`, `buildBundle`, `buildCommandManifest`, `createFetchHandler`, `defineBranch`, `defineLeaf`, `definePlugin`, `defineStreamingLeaf`, `field`, `kebab`, and `param`. Those are the complete authoring surface.
22
+ The primary authoring surface is `LeafError`, `ManifestInvalidError`, `PluginDefinitionError`, `buildBundle`, `buildCommandManifest`, `createFetchHandler`, `createNodeHandler`, `defineBranch`, `defineLeaf`, `definePlugin`, `defineStreamingLeaf`, `field`, `kebab`, and `param`. The package also exports `compareManifests`, `createCallerVerifier`, `postReceipt`, `ReceiptPostError` and the structural types `createNodeHandler` takes.
@@ -7,7 +7,7 @@ when-and-why-to-read: When you need Deployment, this knowledge should be read
7
7
 
8
8
  # Deployment
9
9
 
10
- `createFetchHandler(plugin, options)` returns `(request: Request) => Promise<Response>`. Use it directly in Cloudflare Workers, Bun, Deno, and any framework route that accepts Fetch `Request` and `Response` objects. A framework with different request types needs only an adapter at its boundary; the package itself has no framework dependency.
10
+ `createFetchHandler(plugin, options)` returns `(request: Request) => Promise<Response>`. Use it directly in Cloudflare Workers, Bun, Deno, and any framework route that accepts Fetch `Request` and `Response` objects. For Node's `http`, Express and Nest, mount it with `createNodeHandler` (see [Node, Express and Nest](#node-express-and-nest)); the package itself has no framework dependency.
11
11
 
12
12
  The handler serves the archive for every `GET` request and runs a command for a matching `POST` request. It routes a command from the longest matching suffix of the request path, so the same handler works at the origin root, under `/crtr`, and behind a proxy that preserves that path.
13
13
 
@@ -23,7 +23,33 @@ Set `baseUrl` only when `request.url` is not the public URL, such as a proxy tha
23
23
 
24
24
  Pass `token` to require `Authorization: Bearer <token>` on archive and command requests. `--auth-env NAME` tells crtr which environment variable of the calling process holds that value. It does not configure the server. For a command an agent runs, the calling process is its broker, which reads the profile env store (`crtr profile env set <profile> --name NAME`) rather than your shell's exports. Omitting `token` serves without authentication and logs a warning when the handler is created.
25
25
 
26
- A capability provider registered in the directory passes `auth: {issuer, audience}` instead of `token`; passing both throws. `issuer` is the directory's issuer URL and `audience` is the provider's endpoint URL exactly as registered. Every command request's bearer must be an outbound token the directory minted. The handler verifies it with `@north-light/identity`: it fetches the JWKS from `<issuer>/.well-known/jwks.json`, caches it, and refetches once when a token names an unknown `kid`. It checks the signature, `iss`, `aud` (exact match, so a trailing slash differs) and `exp`. A failed check answers 401 `unauthorized`. Next the called leaf's `<plugin>:<group>` must be in the token's `scope`. If it is not, the handler answers 403 `scope_missing` with that string in both `field` and `received`. A command request under `auth` must also carry `Crtr-Request-Id`. Every leaf then gets `ctx.caller`: `{sub, grantee, clientId, scope, initiatingApp, act, jti, requestId, runId?}`. `act` and `initiatingApp` are for your audit; the kit authorizes nothing on them. The archive and `GET /versions` stay public under `auth`. For local development, point `issuer` at a test issuer that serves `/.well-known/jwks.json`.
26
+ A capability provider registered in the directory passes `auth: {issuer, audience, providerId}` instead of `token`; passing both throws. `providerId` is the provider's directory principal id — for a third-party app, its OAuth client id, bare or as `app:<id>`; 1–64 characters of `A-Z a-z 0-9 _ -`, validated when the handler is created. `issuer` is the directory's issuer URL and `audience` is the provider's endpoint URL exactly as registered. Every command request's bearer must be an outbound token the directory minted. The handler verifies it with `@north-light/identity`: it fetches the JWKS from `<issuer>/.well-known/jwks.json`, caches it, and refetches once when a token names an unknown `kid`. It checks the signature, `iss`, `aud` (exact match, so a trailing slash differs) and `exp`. A failed check answers 401 `unauthorized`. Next the called leaf's `<providerId>:<group>` must be in the token's `scope` (`<plugin name>:<group>` when `providerId` is not set, which is right only for a first-party provider whose name is its id; the directory always mints `<provider id>:<group>`, so a third-party app that omits `providerId` gets 403 on every call). If the scope is missing, the handler answers 403 `scope_missing` with that string in both `field` and `received`; when `providerId` is unset and the token carries the leaf's group under another id, `next` reads `Set auth.providerId to your app's client id (<that id>).` A command request under `auth` must also carry `Crtr-Request-Id`. Every leaf then gets `ctx.caller`: `{sub, grantee, clientId, scope, initiatingApp, act, jti, requestId, runId?}`. `act` and `initiatingApp` are for your audit; the kit authorizes nothing on them. The archive and `GET /versions` stay public under `auth`. For local development, point `issuer` at a test issuer that serves `/.well-known/jwks.json`.
27
+
28
+ ## Node, Express and Nest
29
+
30
+ `createNodeHandler(handler, { bodyLimit?, origin? })` adapts the Fetch handler to `(req, res) => Promise<void>` for Node's `http`, Express and Nest. It imports nothing from `node:`, and its request and response types are structural, so it needs none of those frameworks' types.
31
+
32
+ ```ts
33
+ import { createFetchHandler, createNodeHandler } from '@north-light/plugin';
34
+
35
+ const handler = createFetchHandler(plugin, {
36
+ auth: { issuer: ISSUER, audience: `${APP_ORIGIN}/plugin`, providerId: CLIENT_ID },
37
+ baseUrl: `${APP_ORIGIN}/plugin`,
38
+ requests,
39
+ });
40
+ const node = createNodeHandler(handler, { bodyLimit: 65536 });
41
+
42
+ // Express / Nest controller method:
43
+ // @All(['', '*path']) serve(@Req() req, @Res() res) { return node(req, res); }
44
+ // node:http:
45
+ // createServer((req, res) => void node(req, res));
46
+ ```
47
+
48
+ - **URL.** `originalUrl ?? url` resolved against `origin`, or against `http://<Host header>` when `origin` is not set. Pass `origin` when the public origin differs from the Host the app sees.
49
+ - **Body.** For a non-`GET`/`HEAD` request, `req.rawBody` (Nest with `rawBody: true`) is used when present; otherwise the stream is read. A `content-length` above `bodyLimit` (default 65536 bytes), or more bytes than that, answers 413 `invalid_request` before the handler runs, so before the token is checked.
50
+ - **Headers.** Every request header is passed on; a header Node delivers as an array is appended value by value. The response keeps repeated headers, `set-cookie` included.
51
+ - **Response.** The body is streamed chunk by chunk, waiting for `drain` when the socket is full. When the client disconnects, the request's `signal` aborts and the response stream is cancelled.
52
+ - **Failure.** A handler that throws answers 500 `handler_failed` in the kit's error shape; the adapter never rejects.
27
53
 
28
54
  ## Major versions
29
55
 
@@ -1 +1 @@
1
- var k=Object.defineProperty;var u=(n,e)=>k(n,"name",{value:e,configurable:!0});import{resolve as w}from"node:path";import{envNodeId as x}from"../../shared/env.js";import{cliClient as f}from"../../commands/api-client.js";import{defineBranch as h,defineLeaf as C}from"../command.js";import{resolveCommandRegistry as j}from"../command-manifests/registry.js";import{executeExternalLeaf as L}from"./transport/exec-invoke.js";import{invokeHttpLeaf as U}from"./transport/http-invoke.js";async function M(n){const e=await f().request("GET",`/v1/providers/${encodeURIComponent(n)}/tools`);return new Set(e.groups.flatMap(r=>r.leaves.map(a=>a.tool)))}u(M,"allowedProviderTools");function m(n,e,r){return n.children.some(a=>a.kind==="leaf"?r.has([...e,a.name].join(" ")):m(a,[...e,a.name],r))}u(m,"hasAllowedLeaf");function T(n,e,r){return n.children.filter(a=>a.tier!=="hidden"&&(a.kind==="leaf"?r.has([...e,a.name].join(" ")):m(a,[...e,a.name],r))).length}u(T,"visibleChildCount");function S(n,e,r){return async()=>{if(!x())return n.help;const a=await M(e.id);return{...n.help,listing:n.help.listing?.flatMap(i=>{const t=n.children.find(s=>s.name===i.name);if(t?.kind==="leaf")return a.has([...r,t.name].join(" "))?[i]:[];if(t?.kind!=="branch")return[];const o=[...r,t.name];if(!m(t,o,a))return[];const p=T(t,o,a);return[{...i,...i.subCount!==void 0?{subCount:p}:{}}]})}}}u(S,"providerHelp");const A="Runs on one of your devices. Add `--machine <machine_id>` to pick which; without it the call goes to your only device, and with several the answer is `machine_required` listing each one's machine_id. The `runtime:` prefix on the id is optional.";function g(n,e,r,a=!1){return{outputKind:"object",...a?{machineSelector:!0}:{},run:u(async(i,t)=>{const o={...i};for(const p of r.params){if(!("type"in p)||p.type!=="file")continue;const s=p.name.replace(/-([a-z])/g,(c,E)=>E.toUpperCase()),d=o[s];typeof d=="string"&&(o[s]=w(d))}return f({timeoutMs:0}).request("POST","/v1/provider/call",{provider:n,tool:e,arguments:o,...t?.machine===void 0?{}:{machine:t.machine}},{timeout:0})},"run")}}u(g,"providerLeafAdapter");function q(n,e){const r=[],a=[];for(const i of n){const{provider:t,commands:o}=i;if(!/^[A-Za-z0-9_-]{1,64}$/.test(t)||t==="crtr"||e.has(t)||!Array.isArray(o))continue;if(i.status!=="available"){const s=i.error??"bundle installation pending",d=`Unavailable (${i.status}): ${s}`,c=h({name:t,rootEntry:{concept:i.display_name,desc:d,useWhen:d},help:{name:t,summary:d},children:[]});c.unavailable=d,a.push(c);continue}const p=u(s=>s.kind==="branch"?{...s,children:s.children.map(p)}:{...s,rest:{method:"GET",path:"/",params:{}}},"helpNode");for(const s of o)s.name!==t||s.kind!=="branch"||!Array.isArray(s.children)||r.push({contributor:{name:t,scope:"user"},manifestName:s.name,node:p(s),adaptLeaf:u((d,c)=>g(t,c.slice(1).join(" "),d,i.transport==="machine"),"adaptLeaf")})}return[...N(j(r,e)),...a]}u(q,"composeSandboxProviderSubtrees");function v(n,e){return(r,a)=>{const i=n.transport,t=e(a);if(n.provider!==void 0)return g(n.provider.id,t.slice(1).join(" "),r,i.kind==="machine");if(i.kind==="exec"){const p=i.executable;if(p===void 0)throw new Error(`exec plugin "${n.plugin.name}" has an executable leaf without transport.executable`);return{outputKind:"object",run:u(async s=>L({plugin:n.plugin,executable:p,commandPath:t,output:r.output},s),"run")}}if(i.kind==="machine")throw new Error(`machine plugin "${n.plugin.name}" is not a capability provider`);const o=r;return{outputKind:o.rest.streaming===!0?"jsonl":"object",run:u((p,s)=>U({registration:{name:n.plugin.name,endpoint:i.endpoint,...i.authEnv!==void 0?{authEnv:i.authEnv}:{}},...n.manifest?.baseUrl!==void 0?{baseUrl:n.manifest.baseUrl}:{},...n.manifest?.timeouts!==void 0?{timeouts:n.manifest.timeouts}:{},params:o.params,rest:o.rest,output:o.output,commandPath:t},p,s),"run")}}}u(v,"adaptLeaf");function B(n){return{contributions:n.map(e=>({contributor:{name:e.plugin.name,scope:e.plugin.scope,sourceMarketplace:e.plugin.sourceMarketplace},node:e.node,manifestName:e.manifestName,adaptLeaf:v(e,r=>[e.manifestName,...r.slice(1)]),...e.provider!==void 0?{provider:e.provider}:{}})),coreMounts:[],issues:[]}}u(B,"adaptPluginContributions");function G(n){return n.map(e=>({parent:e.parent,plugin:e.plugin.name,contributor:{name:e.plugin.name,scope:e.plugin.scope,sourceMarketplace:e.plugin.sourceMarketplace},node:e.node,...e.provider!==void 0?{provider:e.provider}:{},adaptLeaf:v({...e,manifestName:e.node.name},r=>e.provider===void 0?[...r]:[e.provider.id,...r.slice(e.parent.length)])}))}u(G,"adaptPluginCoreMounts");function N(n){return n.contributions.map(e=>l(e,e.node,[e.node.name]))}u(N,"composeExternalSubtrees");function z(n,e){return n.filter(r=>r.parent.join("\0")===e.join("\0")).map(r=>r.node.kind==="branch"?l(r,r.node,[...e,r.node.name],r.provider===void 0?1:e.length):y(r,r.node,[...e,r.node.name]))}u(z,"composeCoreBranchMounts");function l(n,e,r,a=1){const i=e.rootEntry===void 0?void 0:{concept:e.rootEntry.concept,desc:e.rootEntry.description,useWhen:e.rootEntry.whenToUse},t=h({name:e.name,description:e.description,whenToUse:e.whenToUse,...e.tier!==void 0?{tier:e.tier}:{},...n.plugin!==void 0?{plugin:n.plugin}:{},...i!==void 0?{rootEntry:i}:{},help:{name:r.join(" "),summary:e.summary,...e.model!==void 0?{model:e.model}:{},...r.length===1&&n.extensionIssue!==void 0?{extensionIssue:n.extensionIssue}:{}},...e.passthrough!==void 0?{passthrough:e.passthrough}:{},children:e.children.map(o=>o.kind==="branch"?l(n,o,[...r,o.name],a):y(n,o,[...r,o.name]))});return n.provider!==void 0&&(t.effectiveHelp=S(t,n.provider,r.slice(a))),t}u(l,"buildBranch");function y(n,e,r){const a=n.adaptLeaf(e,r);return C({name:e.name,description:e.description,whenToUse:e.whenToUse,...e.tier!==void 0?{tier:e.tier}:{},...n.plugin!==void 0?{plugin:n.plugin}:{},...a.machineSelector===!0?{machineSelector:!0}:{},help:{name:r.join(" "),summary:e.summary,...a.machineSelector===!0?{guide:A}:{},params:e.params,output:e.output,outputKind:a.outputKind,effects:[...e.effects]},run:a.run})}u(y,"buildLeaf");export{B as adaptPluginContributions,G as adaptPluginCoreMounts,z as composeCoreBranchMounts,N as composeExternalSubtrees,q as composeSandboxProviderSubtrees};
1
+ var w=Object.defineProperty;var u=(n,e)=>w(n,"name",{value:e,configurable:!0});import{resolve as x}from"node:path";import{envNodeId as C}from"../../shared/env.js";import{cliClient as f}from"../../commands/api-client.js";import{defineBranch as h,defineLeaf as j}from"../command.js";import{resolveCommandRegistry as L}from"../command-manifests/registry.js";import{executeExternalLeaf as U}from"./transport/exec-invoke.js";import{invokeHttpLeaf as M}from"./transport/http-invoke.js";async function T(n){const e=await f().request("GET",`/v1/providers/${encodeURIComponent(n)}/tools`);return new Set(e.groups.flatMap(r=>r.leaves.map(a=>a.tool)))}u(T,"allowedProviderTools");function m(n,e,r){return n.children.some(a=>a.kind==="leaf"?r.has([...e,a.name].join(" ")):m(a,[...e,a.name],r))}u(m,"hasAllowedLeaf");function S(n,e,r){return n.children.filter(a=>a.tier!=="hidden"&&(a.kind==="leaf"?r.has([...e,a.name].join(" ")):m(a,[...e,a.name],r))).length}u(S,"visibleChildCount");function A(n,e,r){return async()=>{if(!C())return n.help;const a=await T(e.id);return{...n.help,listing:n.help.listing?.flatMap(t=>{const i=n.children.find(s=>s.name===t.name);if(i?.kind==="leaf")return a.has([...r,i.name].join(" "))?[t]:[];if(i?.kind!=="branch")return[];const o=[...r,i.name];if(!m(i,o,a))return[];const p=S(i,o,a);return[{...t,...t.subCount!==void 0?{subCount:p}:{}}]})}}}u(A,"providerHelp");const N="Runs on one of your devices. Add `--machine <machine_id>` to pick which; without it the call goes to your only device, and with several the answer is `machine_required` listing each one's machine_id. The `runtime:` prefix on the id is optional.";function g(n,e,r,a=!1){return{outputKind:"object",...a?{machineSelector:!0}:{},run:u(async(t,i)=>{const o={...t};for(const p of r.params){if(!("type"in p)||p.type!=="file")continue;const s=p.name.replace(/-([a-z])/g,(c,k)=>k.toUpperCase()),d=o[s];typeof d=="string"&&(o[s]=x(d))}return f({timeoutMs:0}).request("POST","/v1/provider/call",{provider:n,tool:e,arguments:o,...i?.machine===void 0?{}:{machine:i.machine}},{timeout:0})},"run")}}u(g,"providerLeafAdapter");function v(n,e){return/^[A-Za-z0-9_-]{1,64}$/.test(n)&&n!=="crtr"&&!e.has(n)}u(v,"validRootName");function B(n,e){const r=[],a=[];for(const t of n){const{provider:i,commands:o}=t;if(!Array.isArray(o)||t.status!=="available"&&!v(i,e))continue;if(t.status!=="available"){const s=t.error??"bundle installation pending",d=`Unavailable (${t.status}): ${s}`,c=h({name:i,rootEntry:{concept:t.display_name,desc:d,useWhen:d},help:{name:i,summary:d},children:[]});c.unavailable=d,a.push(c);continue}const p=u(s=>s.kind==="branch"?{...s,children:s.children.map(p)}:{...s,rest:{method:"GET",path:"/",params:{}}},"helpNode");for(const s of o)s.kind!=="branch"||!Array.isArray(s.children)||!v(s.name,e)||r.push({contributor:{name:i,scope:"user"},manifestName:s.name,node:p(s),adaptLeaf:u((d,c)=>g(i,c.slice(1).join(" "),d,t.transport==="machine"),"adaptLeaf")})}return[..._(L(r,e)),...a]}u(B,"composeSandboxProviderSubtrees");function y(n,e){return(r,a)=>{const t=n.transport,i=e(a);if(n.provider!==void 0)return g(n.provider.id,i.slice(1).join(" "),r,t.kind==="machine");if(t.kind==="exec"){const p=t.executable;if(p===void 0)throw new Error(`exec plugin "${n.plugin.name}" has an executable leaf without transport.executable`);return{outputKind:"object",run:u(async s=>U({plugin:n.plugin,executable:p,commandPath:i,output:r.output},s),"run")}}if(t.kind==="machine")throw new Error(`machine plugin "${n.plugin.name}" is not a capability provider`);const o=r;return{outputKind:o.rest.streaming===!0?"jsonl":"object",run:u((p,s)=>M({registration:{name:n.plugin.name,endpoint:t.endpoint,...t.authEnv!==void 0?{authEnv:t.authEnv}:{}},...n.manifest?.baseUrl!==void 0?{baseUrl:n.manifest.baseUrl}:{},...n.manifest?.timeouts!==void 0?{timeouts:n.manifest.timeouts}:{},params:o.params,rest:o.rest,output:o.output,commandPath:i},p,s),"run")}}}u(y,"adaptLeaf");function G(n){return{contributions:n.map(e=>({contributor:{name:e.plugin.name,scope:e.plugin.scope,sourceMarketplace:e.plugin.sourceMarketplace},node:e.node,manifestName:e.manifestName,adaptLeaf:y(e,r=>[e.manifestName,...r.slice(1)]),...e.provider!==void 0?{provider:e.provider}:{}})),coreMounts:[],issues:[]}}u(G,"adaptPluginContributions");function z(n){return n.map(e=>({parent:e.parent,plugin:e.plugin.name,contributor:{name:e.plugin.name,scope:e.plugin.scope,sourceMarketplace:e.plugin.sourceMarketplace},node:e.node,...e.provider!==void 0?{provider:e.provider}:{},adaptLeaf:y({...e,manifestName:e.node.name},r=>e.provider===void 0?[...r]:[e.provider.id,...r.slice(e.parent.length)])}))}u(z,"adaptPluginCoreMounts");function _(n){return n.contributions.map(e=>l(e,e.node,[e.node.name]))}u(_,"composeExternalSubtrees");function D(n,e){return n.filter(r=>r.parent.join("\0")===e.join("\0")).map(r=>r.node.kind==="branch"?l(r,r.node,[...e,r.node.name],r.provider===void 0?1:e.length):E(r,r.node,[...e,r.node.name]))}u(D,"composeCoreBranchMounts");function l(n,e,r,a=1){const t=e.rootEntry===void 0?void 0:{concept:e.rootEntry.concept,desc:e.rootEntry.description,useWhen:e.rootEntry.whenToUse},i=h({name:e.name,description:e.description,whenToUse:e.whenToUse,...e.tier!==void 0?{tier:e.tier}:{},...n.plugin!==void 0?{plugin:n.plugin}:{},...t!==void 0?{rootEntry:t}:{},help:{name:r.join(" "),summary:e.summary,...e.model!==void 0?{model:e.model}:{},...r.length===1&&n.extensionIssue!==void 0?{extensionIssue:n.extensionIssue}:{}},...e.passthrough!==void 0?{passthrough:e.passthrough}:{},children:e.children.map(o=>o.kind==="branch"?l(n,o,[...r,o.name],a):E(n,o,[...r,o.name]))});return n.provider!==void 0&&(i.effectiveHelp=A(i,n.provider,r.slice(a))),i}u(l,"buildBranch");function E(n,e,r){const a=n.adaptLeaf(e,r);return j({name:e.name,description:e.description,whenToUse:e.whenToUse,...e.tier!==void 0?{tier:e.tier}:{},...n.plugin!==void 0?{plugin:n.plugin}:{},...a.machineSelector===!0?{machineSelector:!0}:{},help:{name:r.join(" "),summary:e.summary,...a.machineSelector===!0?{guide:N}:{},params:e.params,output:e.output,outputKind:a.outputKind,effects:[...e.effects]},run:a.run})}u(E,"buildLeaf");export{G as adaptPluginContributions,z as adaptPluginCoreMounts,D as composeCoreBranchMounts,_ as composeExternalSubtrees,B as composeSandboxProviderSubtrees};
@@ -1 +1 @@
1
- var C=Object.defineProperty;var u=(e,t)=>C(e,"name",{value:t,configurable:!0});import{realpathSync as j,readFileSync as N,statSync as g}from"node:fs";import{isSandboxedNode as F}from"../../shared/env.js";import{isAbsolute as O,resolve as k,sep as q}from"node:path";import{listInstalledPlugins as A,listInstalledPluginsInRoot as E}from"../resolver.js";import{projectScopeRoots as R}from"../scope.js";import{validateCommandManifest as I}from"../../api/command-manifest/manifest.js";import{qualifiedTopLevelCommandName as J,resolveCommandRegistry as $}from"../command-manifests/registry.js";import{adaptPluginContributions as T,adaptPluginCoreMounts as B}from"./compose.js";import{validateHttpPluginTransport as H}from"./endpoint.js";import{applyCommandExtensions as L}from"./extensions.js";function _(e=process.cwd(),t){const i=new Set,a=[],s=u(n=>{i.has(n.name)||(i.add(n.name),a.push(n))},"consider");for(const n of R(e,t))for(const r of E("project",n))s(r);if(!F())for(const n of A("user"))s(n);return a.filter(n=>n.enabled&&(n.manifest.commands!==void 0||n.manifest.transport!==void 0))}u(_,"effectiveCommandPlugins");function W(e,t){return i=>t.push({...i,plugin:e.name})}u(W,"issueFor");function v(e,t){if(typeof t!="string"||t.length===0)return null;try{const i=j(e),a=j(k(e,t));return(a===i||a.startsWith(i+q))&&g(a).isFile()?a:null}catch{return null}}u(v,"safePath");function X(e,t){const i=e.manifest.transport;if(i===void 0){t.push({code:"command_manifest_invalid",plugin:e.name,message:"commands requires transport in plugin.json",received:"(missing)",expected:"a transport object",next:"Add plugin.json.transport.",path:"transport"});return}if(typeof i!="object"||i===null||Array.isArray(i)){t.push({code:"command_manifest_invalid",plugin:e.name,message:"transport must be an object",received:Array.isArray(i)?"array":typeof i,expected:"an exec or http transport object",next:"Fix plugin.json.transport.",path:"transport"});return}const a=i;if(a.kind==="http"){const n=H(a);if(n.transport!==void 0)return n.transport;t.push({code:"command_manifest_invalid",plugin:e.name,message:n.errors.join("; "),received:JSON.stringify(i),expected:"a valid HTTP transport",next:"Fix plugin.json.transport.",path:"transport"});return}if(a.kind==="machine"){if(e.manifest.provider===void 0||Object.keys(a).length!==1){t.push({code:"command_manifest_invalid",plugin:e.name,message:e.manifest.provider===void 0?"a machine transport belongs to a capability provider bundle":"transport has unknown keys",received:JSON.stringify(i),expected:'{ kind: "machine" } on a provider bundle',next:"Fix plugin.json.transport.",path:"transport"});return}return{kind:"machine"}}const s=Object.keys(a).filter(n=>!["kind","executable"].includes(n));if(a.kind!=="exec"||s.length||a.executable!==void 0&&(typeof a.executable!="string"||a.executable.length===0)){t.push({code:"command_manifest_invalid",plugin:e.name,message:s.length?`transport has unknown keys: ${s.join(", ")}`:"exec transport requires a non-empty executable when declared",received:JSON.stringify(i),expected:'{ kind: "exec", executable?: "plugin-relative path" }',next:"Fix plugin.json.transport.",path:"transport"});return}return{kind:"exec",...a.executable!==void 0?{executable:a.executable}:{}}}u(X,"transportFor");function w(e){return e.kind==="leaf"||e.children.some(w)}u(w,"hasExecutableLeaf");function M(e){return[...e.roots,...e.coreMounts.map(t=>t.node)]}u(M,"manifestNodes");function z(e,t,i,a){if(t.executable===void 0){if(!M(i).some(w))return t;a.push({code:"command_manifest_invalid",plugin:e.name,message:"exec transport requires executable when commands.json declares executable leaves",received:"(missing)",expected:'{ kind: "exec", executable: "plugin-relative path" }',next:"Add plugin.json.transport.executable.",path:"transport.executable"});return}const s=v(e.root,t.executable);if(s===null){a.push({code:"command_path_unsafe",plugin:e.name,message:"executable escapes the plugin root or is not a regular file",received:t.executable,expected:"a relative path to a regular file inside the plugin root",next:"Fix plugin.json.transport.executable.",path:"transport.executable"});return}if((g(s).mode&73)===0){a.push({code:"command_not_executable",plugin:e.name,message:"executable lacks the POSIX exec bit",received:t.executable,expected:"a file with an executable permission bit",next:"chmod +x the plugin executable, then update the plugin.",path:"transport.executable"});return}return{kind:"exec",executable:s}}u(z,"validateExecExecutable");function G(e){return O(e)||e.startsWith(".")||e.includes("/")||e.includes("\\")}u(G,"namesPath");function K(e,t,i){let a=!0;const s=u(n=>{if(n.kind!=="branch")return;const r=n.passthrough;if(r!==void 0&&G(r.bin)){const l=v(e.root,r.bin);l===null?(i.push({code:"command_path_unsafe",plugin:e.name,message:"passthrough binary escapes the plugin root or is not a regular file",received:r.bin,expected:"a relative path to a regular file inside the plugin root",next:"Fix passthrough.bin in commands.json.",path:"passthrough.bin"}),a=!1):(g(l).mode&73)===0?(i.push({code:"command_not_executable",plugin:e.name,message:"passthrough binary lacks the POSIX exec bit",received:r.bin,expected:"a file with an executable permission bit",next:"chmod +x the plugin executable, then update commands.json.",path:"passthrough.bin"}),a=!1):n.passthrough={...r,bin:l}}for(const l of n.children)s(l)},"visit");for(const n of M(t))s(n);return a}u(K,"resolvePassthroughBinaries");function S(e,t=new Set,i,a=new Set){const s=[],n=e.manifest.commands,r=e.manifest.transport;let l=k(e.root,typeof n=="string"?n:"commands.json");const c=u(x=>({plugin:e,manifestPath:l,...x===void 0?{}:{transport:x},contributions:[],coreMounts:[],issues:s}),"empty");if(n===void 0&&r===void 0)return c();if(typeof n!="string"||n.length===0)return s.push({code:"command_manifest_invalid",plugin:e.name,message:"transport requires commands in plugin.json",received:"(missing)",expected:"a plugin-root-relative commands path",next:"Add plugin.json.commands or remove transport.",path:"commands"}),c();const o=X(e,s);if(o===void 0)return c();const d=v(e.root,n);if(d===null)return s.push({code:"command_path_unsafe",plugin:e.name,message:"commands pointer escapes the plugin root or is not a regular file",received:n,expected:"a relative path to a regular file inside the plugin root",next:"Fix plugin.json.commands.",path:"commands"}),c(o);l=d;let p;try{p=N(d)}catch{p=null}if(p===null)return s.push({code:"command_manifest_unreadable",plugin:e.name,message:"commands.json is missing or unreadable",received:d,expected:"a readable JSON file",next:"Reinstall or update the plugin."}),c(o);let h;try{h=JSON.parse(Buffer.from(p).toString("utf8"))}catch{return s.push({code:"command_manifest_invalid",plugin:e.name,message:"commands.json is not valid JSON",received:d,expected:"a JSON object",next:"Regenerate commands.json."}),c(o)}const f=I(h,{transport:o.kind==="machine"?"http":o.kind,reservedCoreNames:t,extensibleCoreBranches:a,...i!==void 0?{coreCommandPaths:i}:{}});if(f.manifest===void 0)return f.issues.forEach(W(e,s)),c(o);const m=o.kind==="exec"?z(e,o,f.manifest,s):o;if(m===void 0)return c(o);if(m.kind==="exec"&&!K(e,f.manifest,s))return c(m);const b=e.manifest.provider,y=f.manifest.roots;if(!y.every(x=>x.kind==="branch"))throw new Error("plugin manifest validation returned a top-level leaf");return{plugin:e,manifestPath:l,transport:m,manifest:f.manifest,contributions:y.map(x=>({plugin:e,transport:m,node:x,manifest:f.manifest,manifestName:x.name,...b!==void 0?{provider:b}:{}})),coreMounts:f.manifest.coreMounts.map(x=>({plugin:e,transport:m,parent:x.parent,node:x.node,manifest:f.manifest,...b!==void 0?{provider:b}:{}})),issues:s}}u(S,"validatePluginCommands");function se(e=process.cwd(),t){return _(e,t).map(i=>S(i))}u(se,"discoverPluginCommandCandidates");function P(e,t=process.cwd(),i,a,s=new Set){const n=_(t,i).map(c=>S(c,e,a,s)),r=new Map;for(const c of n)for(const o of c.contributions){const d=r.get(o.node.name);d===void 0?r.set(o.node.name,[o]):d.push(o)}for(const[c,o]of r)if(o.length>1)for(const d of o){const{plugin:p}=d,h=J({name:p.name,sourceMarketplace:p.sourceMarketplace},c),f=n.find(m=>m.plugin.root===p.root);f.contributions=f.contributions.map(m=>m===d?{...m,node:{...m.node,name:h}}:m),f.issues.push({code:"command_collision",plugin:p.name,path:c,message:`top-level command "${c}" is claimed by ${o.map(m=>m.plugin.name).join(", ")} \u2014 mounted as "${h}"`,received:o.map(m=>m.plugin.name).join(", "),expected:"an origin-qualified top-level command for each claimant",next:`Invoke "${h}" to use this plugin command.`})}const l=new Map;for(const c of n)for(const o of c.coreMounts){const d=[...o.parent,o.node.name].join("/"),p=l.get(d);p===void 0?l.set(d,[o]):p.push(o)}for(const[c,o]of l)if(o.length>1)for(const d of o){const p=n.find(h=>h.plugin.root===d.plugin.root);p.coreMounts=p.coreMounts.filter(h=>h!==d),p.issues.push({code:"command_collision",plugin:d.plugin.name,path:c.replace("/","."),message:`contributed command "${d.node.name}" conflicts with another plugin child below the extensible core branch`,received:o.map(h=>h.plugin.name).join(", "),expected:`a unique child name below ${d.parent.join(".")}`,next:"Rename the contributed command or remove this mount."})}return n}u(P,"validateEffectiveCommandPlugins");function Q(e,t){return{...t,contributor:{name:e.name,scope:e.scope,sourceMarketplace:e.sourceMarketplace}}}u(Q,"contributorIssue");function re(e,t=process.cwd(),i,a,s=new Set){const n=P(e,t,i,a,s);return{contributions:n.flatMap(r=>r.contributions),coreMounts:n.flatMap(r=>r.coreMounts),issues:n.flatMap(r=>r.issues)}}u(re,"discoverCommandContributions");function ce(e,t=process.cwd(),i,a,s=new Set){const n=P(e,t,i,a,s),r=T(n.flatMap(o=>o.contributions)).contributions,l=$(r,e);return{contributions:L(l.contributions,t).contributions,coreMounts:B(n.flatMap(o=>o.coreMounts)),issues:[...n.flatMap(o=>o.issues.map(d=>Q(o.plugin,d))),...l.issues]}}u(ce,"buildExternalCommandSnapshot");export{ce as buildExternalCommandSnapshot,re as discoverCommandContributions,se as discoverPluginCommandCandidates,_ as effectiveCommandPlugins,P as validateEffectiveCommandPlugins,S as validatePluginCommands};
1
+ var P=Object.defineProperty;var u=(e,n)=>P(e,"name",{value:n,configurable:!0});import{realpathSync as j,readFileSync as N,statSync as g}from"node:fs";import{isSandboxedNode as F}from"../../shared/env.js";import{isAbsolute as O,resolve as k,sep as R}from"node:path";import{listInstalledPlugins as q,listInstalledPluginsInRoot as A}from"../resolver.js";import{projectScopeRoots as E}from"../scope.js";import{validateCommandManifest as I}from"../../api/command-manifest/manifest.js";import{qualifiedTopLevelCommandName as J,resolveCommandRegistry as $}from"../command-manifests/registry.js";import{adaptPluginContributions as T,adaptPluginCoreMounts as B}from"./compose.js";import{validateHttpPluginTransport as H}from"./endpoint.js";import{providerRootClaims as L}from"../providers/roots.js";import{applyCommandExtensions as W}from"./extensions.js";function _(e=process.cwd(),n){const i=new Set,o=[],r=u(s=>{i.has(s.name)||(i.add(s.name),o.push(s))},"consider");for(const s of E(e,n))for(const m of A("project",s))r(m);if(!F())for(const s of q("user"))r(s);const{skipped:a}=L(o);return o.filter(s=>!a.has(s.name)).filter(s=>s.enabled&&(s.manifest.commands!==void 0||s.manifest.transport!==void 0))}u(_,"effectiveCommandPlugins");function X(e,n){return i=>n.push({...i,plugin:e.name})}u(X,"issueFor");function v(e,n){if(typeof n!="string"||n.length===0)return null;try{const i=j(e),o=j(k(e,n));return(o===i||o.startsWith(i+R))&&g(o).isFile()?o:null}catch{return null}}u(v,"safePath");function z(e,n){const i=e.manifest.transport;if(i===void 0){n.push({code:"command_manifest_invalid",plugin:e.name,message:"commands requires transport in plugin.json",received:"(missing)",expected:"a transport object",next:"Add plugin.json.transport.",path:"transport"});return}if(typeof i!="object"||i===null||Array.isArray(i)){n.push({code:"command_manifest_invalid",plugin:e.name,message:"transport must be an object",received:Array.isArray(i)?"array":typeof i,expected:"an exec or http transport object",next:"Fix plugin.json.transport.",path:"transport"});return}const o=i;if(o.kind==="http"){const a=H(o);if(a.transport!==void 0)return a.transport;n.push({code:"command_manifest_invalid",plugin:e.name,message:a.errors.join("; "),received:JSON.stringify(i),expected:"a valid HTTP transport",next:"Fix plugin.json.transport.",path:"transport"});return}if(o.kind==="machine"){if(e.manifest.provider===void 0||Object.keys(o).length!==1){n.push({code:"command_manifest_invalid",plugin:e.name,message:e.manifest.provider===void 0?"a machine transport belongs to a capability provider bundle":"transport has unknown keys",received:JSON.stringify(i),expected:'{ kind: "machine" } on a provider bundle',next:"Fix plugin.json.transport.",path:"transport"});return}return{kind:"machine"}}const r=Object.keys(o).filter(a=>!["kind","executable"].includes(a));if(o.kind!=="exec"||r.length||o.executable!==void 0&&(typeof o.executable!="string"||o.executable.length===0)){n.push({code:"command_manifest_invalid",plugin:e.name,message:r.length?`transport has unknown keys: ${r.join(", ")}`:"exec transport requires a non-empty executable when declared",received:JSON.stringify(i),expected:'{ kind: "exec", executable?: "plugin-relative path" }',next:"Fix plugin.json.transport.",path:"transport"});return}return{kind:"exec",...o.executable!==void 0?{executable:o.executable}:{}}}u(z,"transportFor");function w(e){return e.kind==="leaf"||e.children.some(w)}u(w,"hasExecutableLeaf");function M(e){return[...e.roots,...e.coreMounts.map(n=>n.node)]}u(M,"manifestNodes");function G(e,n,i,o){if(n.executable===void 0){if(!M(i).some(w))return n;o.push({code:"command_manifest_invalid",plugin:e.name,message:"exec transport requires executable when commands.json declares executable leaves",received:"(missing)",expected:'{ kind: "exec", executable: "plugin-relative path" }',next:"Add plugin.json.transport.executable.",path:"transport.executable"});return}const r=v(e.root,n.executable);if(r===null){o.push({code:"command_path_unsafe",plugin:e.name,message:"executable escapes the plugin root or is not a regular file",received:n.executable,expected:"a relative path to a regular file inside the plugin root",next:"Fix plugin.json.transport.executable.",path:"transport.executable"});return}if((g(r).mode&73)===0){o.push({code:"command_not_executable",plugin:e.name,message:"executable lacks the POSIX exec bit",received:n.executable,expected:"a file with an executable permission bit",next:"chmod +x the plugin executable, then update the plugin.",path:"transport.executable"});return}return{kind:"exec",executable:r}}u(G,"validateExecExecutable");function K(e){return O(e)||e.startsWith(".")||e.includes("/")||e.includes("\\")}u(K,"namesPath");function Q(e,n,i){let o=!0;const r=u(a=>{if(a.kind!=="branch")return;const s=a.passthrough;if(s!==void 0&&K(s.bin)){const m=v(e.root,s.bin);m===null?(i.push({code:"command_path_unsafe",plugin:e.name,message:"passthrough binary escapes the plugin root or is not a regular file",received:s.bin,expected:"a relative path to a regular file inside the plugin root",next:"Fix passthrough.bin in commands.json.",path:"passthrough.bin"}),o=!1):(g(m).mode&73)===0?(i.push({code:"command_not_executable",plugin:e.name,message:"passthrough binary lacks the POSIX exec bit",received:s.bin,expected:"a file with an executable permission bit",next:"chmod +x the plugin executable, then update commands.json.",path:"passthrough.bin"}),o=!1):a.passthrough={...s,bin:m}}for(const m of a.children)r(m)},"visit");for(const a of M(n))r(a);return o}u(Q,"resolvePassthroughBinaries");function S(e,n=new Set,i,o=new Set){const r=[],a=e.manifest.commands,s=e.manifest.transport;let m=k(e.root,typeof a=="string"?a:"commands.json");const c=u(x=>({plugin:e,manifestPath:m,...x===void 0?{}:{transport:x},contributions:[],coreMounts:[],issues:r}),"empty");if(a===void 0&&s===void 0)return c();if(typeof a!="string"||a.length===0)return r.push({code:"command_manifest_invalid",plugin:e.name,message:"transport requires commands in plugin.json",received:"(missing)",expected:"a plugin-root-relative commands path",next:"Add plugin.json.commands or remove transport.",path:"commands"}),c();const t=z(e,r);if(t===void 0)return c();const d=v(e.root,a);if(d===null)return r.push({code:"command_path_unsafe",plugin:e.name,message:"commands pointer escapes the plugin root or is not a regular file",received:a,expected:"a relative path to a regular file inside the plugin root",next:"Fix plugin.json.commands.",path:"commands"}),c(t);m=d;let p;try{p=N(d)}catch{p=null}if(p===null)return r.push({code:"command_manifest_unreadable",plugin:e.name,message:"commands.json is missing or unreadable",received:d,expected:"a readable JSON file",next:"Reinstall or update the plugin."}),c(t);let h;try{h=JSON.parse(Buffer.from(p).toString("utf8"))}catch{return r.push({code:"command_manifest_invalid",plugin:e.name,message:"commands.json is not valid JSON",received:d,expected:"a JSON object",next:"Regenerate commands.json."}),c(t)}const f=I(h,{transport:t.kind==="machine"?"http":t.kind,reservedCoreNames:n,extensibleCoreBranches:o,...i!==void 0?{coreCommandPaths:i}:{}});if(f.manifest===void 0)return f.issues.forEach(X(e,r)),c(t);const l=t.kind==="exec"?G(e,t,f.manifest,r):t;if(l===void 0)return c(t);if(l.kind==="exec"&&!Q(e,f.manifest,r))return c(l);const b=e.manifest.provider,y=f.manifest.roots;if(!y.every(x=>x.kind==="branch"))throw new Error("plugin manifest validation returned a top-level leaf");return{plugin:e,manifestPath:m,transport:l,manifest:f.manifest,contributions:y.map(x=>({plugin:e,transport:l,node:x,manifest:f.manifest,manifestName:x.name,...b!==void 0?{provider:b}:{}})),coreMounts:f.manifest.coreMounts.map(x=>({plugin:e,transport:l,parent:x.parent,node:x.node,manifest:f.manifest,...b!==void 0?{provider:b}:{}})),issues:r}}u(S,"validatePluginCommands");function ce(e=process.cwd(),n){return _(e,n).map(i=>S(i))}u(ce,"discoverPluginCommandCandidates");function C(e,n=process.cwd(),i,o,r=new Set){const a=_(n,i).map(c=>S(c,e,o,r)),s=new Map;for(const c of a)for(const t of c.contributions){const d=s.get(t.node.name);d===void 0?s.set(t.node.name,[t]):d.push(t)}for(const[c,t]of s)if(t.length>1)for(const d of t){const{plugin:p}=d,h=J({name:p.name,sourceMarketplace:p.sourceMarketplace},c),f=a.find(l=>l.plugin.root===p.root);f.contributions=f.contributions.map(l=>l===d?{...l,node:{...l.node,name:h}}:l),f.issues.push({code:"command_collision",plugin:p.name,path:c,message:`top-level command "${c}" is claimed by ${t.map(l=>l.plugin.name).join(", ")} \u2014 mounted as "${h}"`,received:t.map(l=>l.plugin.name).join(", "),expected:"an origin-qualified top-level command for each claimant",next:`Invoke "${h}" to use this plugin command.`})}const m=new Map;for(const c of a)for(const t of c.coreMounts){const d=[...t.parent,t.node.name].join("/"),p=m.get(d);p===void 0?m.set(d,[t]):p.push(t)}for(const[c,t]of m)if(t.length>1)for(const d of t){const p=a.find(h=>h.plugin.root===d.plugin.root);p.coreMounts=p.coreMounts.filter(h=>h!==d),p.issues.push({code:"command_collision",plugin:d.plugin.name,path:c.replace("/","."),message:`contributed command "${d.node.name}" conflicts with another plugin child below the extensible core branch`,received:t.map(h=>h.plugin.name).join(", "),expected:`a unique child name below ${d.parent.join(".")}`,next:"Rename the contributed command or remove this mount."})}return a}u(C,"validateEffectiveCommandPlugins");function U(e,n){return{...n,contributor:{name:e.name,scope:e.scope,sourceMarketplace:e.sourceMarketplace}}}u(U,"contributorIssue");function de(e,n=process.cwd(),i,o,r=new Set){const a=C(e,n,i,o,r);return{contributions:a.flatMap(s=>s.contributions),coreMounts:a.flatMap(s=>s.coreMounts),issues:a.flatMap(s=>s.issues)}}u(de,"discoverCommandContributions");function ue(e,n=process.cwd(),i,o,r=new Set){const a=C(e,n,i,o,r),s=T(a.flatMap(t=>t.contributions)).contributions,m=$(s,e);return{contributions:W(m.contributions,n).contributions,coreMounts:B(a.flatMap(t=>t.coreMounts)),issues:[...a.flatMap(t=>t.issues.map(d=>U(t.plugin,d))),...m.issues]}}u(ue,"buildExternalCommandSnapshot");export{ue as buildExternalCommandSnapshot,de as discoverCommandContributions,ce as discoverPluginCommandCandidates,_ as effectiveCommandPlugins,C as validateEffectiveCommandPlugins,S as validatePluginCommands};
@@ -1 +1 @@
1
- var w=Object.defineProperty;var c=(n,r)=>w(n,"name",{value:r,configurable:!0});import{prepared as v}from"../sqlite-statements.js";import{relative as b,sep as g}from"node:path";import{activeBackgroundBashJobs as x,readJobPgid as y}from"../bash-jobs.js";import{appSpace as S}from"../layout.js";import{getNode as l,recordSettledTurn as k}from"../canvas/index.js";import{transition as E}from"../runtime/lifecycle.js";import{openDb as _}from"../canvas/db.js";import{contextDir as R}from"../canvas/node-agent-paths.js";import{emitEvent as p}from"../events/emit.js";import{closeGrantStreams as C}from"../runs/events.js";import{grantedMounts as G,isGrantedSource as I,mountAllowed as P}from"../spaces/mounts.js";import{permissionsForRun as m}from"../spaces/permissions.js";import{stopNodeSandboxes as O}from"../spaces/stop.js";import{discardChangedGrantTokens as T}from"../providers/token-cache.js";import{activeGrantForApp as F,allGrants as M,mirrorOnly as A}from"./mirror.js";import{runRemovalCascades as D}from"./remove.js";import{notifyGrantChanges as N}from"./watch.js";import{launcherPresent as W,listSandboxes as j,stopSandboxPid as B}from"./sandboxes.js";import{checkCronFire as J}from"../../daemon/cron/authority.js";import{cronExecutionFor as L}from"../../daemon/cron/live-runs.js";import{cronExecutionsReady as U}from"../../daemon/cron/run-socket.js";function h(){return new Map(v(_(),"SELECT uid, app, status FROM app_users").all().map(n=>[n.uid,{app:n.app,status:n.status}]))}c(h,"appUserOwners");function $(n,r=h()){const o=r.get(n.uid),i=n.nodeId.startsWith("cron-"),e=i?L(n.nodeId.slice(5)):void 0;if(i&&(e===void 0||!o))return"permission_revoked";if(e!==void 0&&o.status==="active"){const s=J({grantee:e.grantee,sub:e.sub,grant_id:e.grantId,scopes:e.scopes});if(!s.allowed)return e.refusal??={reason:s.reason,message:s.message},"permission_revoked"}if(!o||o.app==="app:terminal")return null;if(o.status!=="active")return"app_removed";const t=l(n.nodeId);try{let s;if(e!==void 0)s=m(e.grantee,e.sub,e.grantId,e.scopes);else if(t&&t.grantee===o.app)s=m(o.app,t.sub??"user:local",t.grant_id??null,t.scopes??null);else{const d=F(o.app);if(!d)return"permission_revoked";s=m(o.app,d.sub,d.id,null)}const u=G(s),f=S(o.app).root;return n.mounts.every(d=>{const a=b(f,d.source);return a===""||a!==".."&&!a.startsWith(`..${g}`)&&!a.startsWith(g)||!I(o.app,d.source)||P(d,u)})?null:"permission_revoked"}catch{return"permission_revoked"}}c($,"exceeds");async function H(n){const r=l(n);if(!r)return;(r.status==="active"||r.status==="idle")&&E(n,"release");const o=new Date().toISOString();k(n,o),r.run_id&&r.run_id!==n&&v(_(),"UPDATE nodes SET last_settled_at = COALESCE(last_settled_at, ?) WHERE node_id = ?").run(o,r.run_id)}c(H,"afterRevocationStop");function q(n){const r=new Set,o=l(n);o?.pi_pid!=null&&r.add(o.pi_pid);try{const i=R(n,o?.grantee);for(const e of x(i,!0)){const t=y(i,e.jobId);t!==void 0&&r.add(t)}}catch{}return r}c(q,"nodePids");async function z(){if(!W())return{completion:Promise.resolve()};await U();const n=await j(),r=h(),o=n.flatMap(t=>{const s=$(t,r);return s?[{sandbox:t,reason:s}]:[]}),i=[],e=new Map;for(const t of o){p({level:"info",event:"daemon.grants.sandbox_stop",fields:{sandbox_node:t.sandbox.nodeId,pid:t.sandbox.pid,grantee:r.get(t.sandbox.uid).app,reason:t.reason}});const s=e.get(t.sandbox.nodeId)??[];s.push(t),e.set(t.sandbox.nodeId,s)}for(const[t,s]of e){const u=l(t),f=u?q(t):new Set,d=u!==null&&s.some(({sandbox:a})=>f.has(a.pid));i.push((async()=>{if(d){const a=await O(t,"permission_revoked",{holdRecovery:!0,after:c(()=>H(t),"after")});if(!a.confirmed)throw a.error??new Error(`stop of ${t} was not confirmed`)}for(const{sandbox:a}of s)(!d||!f.has(a.pid))&&await B(a.pid)})().catch(a=>{p({level:"error",event:"daemon.grants.sandbox_stop_failed",...u?{node_id:t}:{fields:{sandbox_node:t}},error:a})}))}return{completion:Promise.all(i).then(()=>{})}}c(z,"sweepSandboxes");function K(n,r){try{const{pairs:o,tokens:i}=T(n,r);o&&p({level:"info",event:"daemon.grants.outbound_tokens_discarded",fields:{grants:o,tokens:i}})}catch(o){p({level:"error",event:"daemon.grants.outbound_tokens_discard_failed",error:o})}}c(K,"discardOutboundTokens");async function he(n,r=o=>{}){const o=M();try{N(n,o)}catch(e){p({level:"error",event:"daemon.grants.notify_failed",error:e})}K(n,o);for(const e of n){if(e.status!=="active"||o.find(u=>u.id===e.id)?.status==="active")continue;const s=C(e.id);s&&p({level:"info",event:"daemon.grants.streams_closed",fields:{grant:e.id,grantee:e.grantee,streams:s}})}try{const{completion:e}=await z();r(e)}catch(e){p({level:"error",event:"daemon.grants.sweep_failed",error:e})}const i=new Set(o.filter(e=>!A(e)&&e.status==="removed"&&n.find(t=>t.id===e.id)?.status!=="removed").map(e=>e.grantee));r(D(i).catch(()=>{})),r(import("../providers/bundles.js").then(({syncProviderBundles:e})=>e()).catch(e=>{p({level:"error",event:"daemon.providers.bundle_sync_failed",error:e})}))}c(he,"onGrantSync");export{$ as exceeds,he as onGrantSync,z as sweepSandboxes};
1
+ var w=Object.defineProperty;var c=(n,r)=>w(n,"name",{value:r,configurable:!0});import{prepared as v}from"../sqlite-statements.js";import{relative as b,sep as g}from"node:path";import{activeBackgroundBashJobs as x,readJobPgid as y}from"../bash-jobs.js";import{appSpace as S}from"../layout.js";import{getNode as l,recordSettledTurn as k}from"../canvas/index.js";import{transition as E}from"../runtime/lifecycle.js";import{openDb as _}from"../canvas/db.js";import{contextDir as R}from"../canvas/node-agent-paths.js";import{emitEvent as p}from"../events/emit.js";import{closeGrantStreams as C}from"../runs/events.js";import{grantedMounts as G,isGrantedSource as I,mountAllowed as P}from"../spaces/mounts.js";import{permissionsForRun as m}from"../spaces/permissions.js";import{stopNodeSandboxes as O}from"../spaces/stop.js";import{discardChangedGrantTokens as T}from"../providers/token-cache.js";import{activeGrantForApp as F,allGrants as M,mirrorOnly as A}from"./mirror.js";import{runRemovalCascades as D}from"./remove.js";import{notifyGrantChanges as N}from"./watch.js";import{launcherPresent as W,listSandboxes as j,stopSandboxPid as B}from"./sandboxes.js";import{checkCronFire as J}from"../../daemon/cron/authority.js";import{cronExecutionFor as L}from"../../daemon/cron/live-runs.js";import{cronExecutionsReady as U}from"../../daemon/cron/run-socket.js";function h(){return new Map(v(_(),"SELECT uid, app, status FROM app_users").all().map(n=>[n.uid,{app:n.app,status:n.status}]))}c(h,"appUserOwners");function $(n,r=h()){const o=r.get(n.uid),i=n.nodeId.startsWith("cron-"),e=i?L(n.nodeId.slice(5)):void 0;if(i&&(e===void 0||!o))return"permission_revoked";if(e!==void 0&&o.status==="active"){const s=J({grantee:e.grantee,sub:e.sub,grant_id:e.grantId,scopes:e.scopes});if(!s.allowed)return e.refusal??={reason:s.reason,message:s.message},"permission_revoked"}if(!o||o.app==="app:terminal")return null;if(o.status!=="active")return"app_removed";const t=l(n.nodeId);try{let s;if(e!==void 0)s=m(e.grantee,e.sub,e.grantId,e.scopes);else if(t&&t.grantee===o.app)s=m(o.app,t.sub??"user:local",t.grant_id??null,t.scopes??null);else{const d=F(o.app);if(!d)return"permission_revoked";s=m(o.app,d.sub,d.id,null)}const u=G(s),f=S(o.app).root;return n.mounts.every(d=>{const a=b(f,d.source);return a===""||a!==".."&&!a.startsWith(`..${g}`)&&!a.startsWith(g)||!I(o.app,d.source)||P(d,u)})?null:"permission_revoked"}catch{return"permission_revoked"}}c($,"exceeds");async function H(n){const r=l(n);if(!r)return;(r.status==="active"||r.status==="idle")&&E(n,"release");const o=new Date().toISOString();k(n,o),r.run_id&&r.run_id!==n&&v(_(),"UPDATE nodes SET last_settled_at = COALESCE(last_settled_at, ?) WHERE node_id = ?").run(o,r.run_id)}c(H,"afterRevocationStop");function q(n){const r=new Set,o=l(n);o?.pi_pid!=null&&r.add(o.pi_pid);try{const i=R(n,o?.grantee);for(const e of x(i,!0)){const t=y(i,e.jobId);t!==void 0&&r.add(t)}}catch{}return r}c(q,"nodePids");async function z(){if(!W())return{completion:Promise.resolve()};await U();const n=await j(),r=h(),o=n.flatMap(t=>{const s=$(t,r);return s?[{sandbox:t,reason:s}]:[]}),i=[],e=new Map;for(const t of o){p({level:"info",event:"daemon.grants.sandbox_stop",fields:{sandbox_node:t.sandbox.nodeId,pid:t.sandbox.pid,grantee:r.get(t.sandbox.uid).app,reason:t.reason}});const s=e.get(t.sandbox.nodeId)??[];s.push(t),e.set(t.sandbox.nodeId,s)}for(const[t,s]of e){const u=l(t),f=u?q(t):new Set,d=u!==null&&s.some(({sandbox:a})=>f.has(a.pid));i.push((async()=>{if(d){const a=await O(t,"permission_revoked",{holdRecovery:!0,after:c(()=>H(t),"after")});if(!a.confirmed)throw a.error??new Error(`stop of ${t} was not confirmed`)}for(const{sandbox:a}of s)(!d||!f.has(a.pid))&&await B(a.pid)})().catch(a=>{p({level:"error",event:"daemon.grants.sandbox_stop_failed",...u?{node_id:t}:{fields:{sandbox_node:t}},error:a})}))}return{completion:Promise.all(i).then(()=>{})}}c(z,"sweepSandboxes");function K(n,r){try{const{pairs:o,providers:i,tokens:e}=T(n,r);(o||i)&&p({level:"info",event:"daemon.grants.outbound_tokens_discarded",fields:{grants:o,providers:i,tokens:e}})}catch(o){p({level:"error",event:"daemon.grants.outbound_tokens_discard_failed",error:o})}}c(K,"discardOutboundTokens");async function he(n,r=o=>{}){const o=M();try{N(n,o)}catch(e){p({level:"error",event:"daemon.grants.notify_failed",error:e})}K(n,o);for(const e of n){if(e.status!=="active"||o.find(u=>u.id===e.id)?.status==="active")continue;const s=C(e.id);s&&p({level:"info",event:"daemon.grants.streams_closed",fields:{grant:e.id,grantee:e.grantee,streams:s}})}try{const{completion:e}=await z();r(e)}catch(e){p({level:"error",event:"daemon.grants.sweep_failed",error:e})}const i=new Set(o.filter(e=>!A(e)&&e.status==="removed"&&n.find(t=>t.id===e.id)?.status!=="removed").map(e=>e.grantee));r(D(i).catch(()=>{})),r(import("../providers/bundles.js").then(({syncProviderBundles:e})=>e()).catch(e=>{p({level:"error",event:"daemon.providers.bundle_sync_failed",error:e})}))}c(he,"onGrantSync");export{$ as exceeds,he as onGrantSync,z as sweepSandboxes};
@@ -1,2 +1,2 @@
1
- var X=Object.defineProperty;var i=(e,n)=>X(e,"name",{value:n,configurable:!0});import{existsSync as h,readdirSync as H,readFileSync as y,rmSync as b,statSync as x}from"node:fs";import{join as u}from"node:path";import{validateCommandManifest as Y}from"../../api/command-manifest/manifest.js";import{crtrHome as O}from"../canvas/paths.js";import{withCanvasWrite as ee}from"../canvas/db.js";import{bundleVersion as ne,isInstalledGeneration as re,recordCheckedGeneration as te}from"../command-plugins/bundle.js";import{readConfig as U,readState as oe}from"../config.js";import{emitEvent as g}from"../events/emit.js";import{withExclusiveDirectoryLockAsync as ie}from"../exclusive-lock.js";import{atomicWriteText as M,ensureDir as I,pathExists as ae}from"../fs-utils.js";import{reconcilePackageDocuments as se}from"../graph/package-docs.js";import{readPluginManifest as W}from"../manifest.js";import{bundleSwapLockPath as de}from"../plugin-swap-lock.js";import{pluginsDir as ue,userScopeRoot as ce}from"../scope.js";import{localGrants as fe}from"../grants/mirror.js";import{ProviderError as c}from"./errors.js";const T="[A-Za-z0-9_-]{1,64}",le=new RegExp(`^(${T}):(${T})$`),z=new RegExp(`^${T}$`),pe=3e4,me=3e4;function ve(e){const n=new Set;for(const r of e)if(r.status==="active")for(const{scope:t}of r.resolved_scopes){const o=le.exec(t);o&&o[1]!=="crtr"&&n.add(o[1])}return[...n].sort()}i(ve,"providerIdsFromGrants");function w(e){return u(ue("user"),e)}i(w,"pluginRoot");function F(){return u(O(),"providers")}i(F,"providersRoot");function G(e,n){return u(F(),e,`v${n}`)}i(G,"majorDir");function k(e){if(!z.test(e))throw new c("not_found",`"${e}" is not a provider id`,{param:"provider"})}i(k,"assertId");function j(){const e=U("user").runtime?.issuer;if(!e)throw new c("not_found","provider bundles need a directory-connected runtime");return e.replace(/\/+$/,"")}i(j,"directoryBase");async function q(e,n){const r=`${j()}/providers/${encodeURIComponent(e)}/plugin${n.version!==void 0?`?version=${n.version}`:""}`;let t;try{t=await fetch(r,{headers:{Accept:"application/x-tar",...n.ifNoneMatch!==void 0?{"If-None-Match":n.ifNoneMatch}:{}},signal:AbortSignal.timeout(pe)})}catch(s){return{status:"failed",message:`could not reach ${r}: ${s instanceof Error?s.message:String(s)}`}}if(t.status===304)return n.ifNoneMatch===void 0?{status:"failed",message:`HTTP 304 from ${r} without a conditional request`}:{status:304};if(t.status===404||t.status===410)return await t.body?.cancel().catch(()=>{}),{status:t.status===404?404:410,message:`HTTP ${t.status} from ${r}`};if(t.status<200||t.status>=300)return await t.body?.cancel().catch(()=>{}),{status:"failed",message:`HTTP ${t.status} from ${r}`};let o;try{o=new Uint8Array(await t.arrayBuffer())}catch(s){return{status:"failed",message:`reading ${r} failed: ${s instanceof Error?s.message:String(s)}`}}const a=t.headers.get("etag");return{status:200,raw:o,...a!==null?{etag:a}:{}}}i(q,"fetchProviderBundle");const _=new WeakSet;class S extends Error{static{i(this,"BundleInvalid")}issues;constructor(n,r){super(n),this.issues=r}}function V(e,n,r){if(r.provider===void 0)return"the archive carries no provider block";if(r.provider.id!==e)return`the archive is provider "${r.provider.id}", not "${e}"`;if(n!==void 0&&r.provider.major!==n)return`the archive is major ${r.provider.major}, not ${n}`;const t=["hooks","kinds","page_components","memory_extensions"].filter(o=>r[o]!==void 0);if(t.length>0)return`a provider bundle may not carry ${t.join(", ")}`}i(V,"wrongBundle");function A(e,n,r){g({level:"error",event:"daemon.providers.bundle_invalid",fields:{provider:e,...n!==void 0?{major:n}:{},...r instanceof S?{issues:r.issues}:{}},error:r})}i(A,"logInvalid");function Z(e,n,r){g({level:"warn",event:"daemon.providers.bundle_fetch_failed",fields:{provider:e,...n!==void 0?{major:n}:{},message:r}})}i(Z,"logFailure");async function L(){return import("../../commands/pkg/plugin-manage.js")}i(L,"pluginManage");const P=new Map,E=i(e=>`${O()}\0${e}`,"installKey");function he(e){const n=e,r=n?.code??n?.cause?.code;return typeof r=="string"&&/^[A-Z][A-Z0-9_]{1,30}$/.test(r)?`${r} on provider install directory`:e instanceof c&&e.message.includes("HTTP 404")?"directory returned HTTP 404":e instanceof c&&e.message.includes("HTTP 410")?"directory returned HTTP 410":"provider bundle could not be installed"}i(he,"publicInstallCause");function ge(e){if(m(e)===void 0)return P.get(E(e))??{status:"installing"}}i(ge,"providerInstallState");function we(e){return new c("provider_unavailable",`provider "${e}" unavailable: ${ge(e)?.error??"bundle installation pending"}`)}i(we,"unavailableProviderError");function m(e){const n=W(w(e));if(!(n?.provider===void 0||n.transport?.kind!=="http"&&n.transport?.kind!=="machine")&&ae(u(w(e),"commands.json"))){try{if(x(u(w(e),"commands.json")).size===0)return}catch{return}return{provider:n.provider,transport:n.transport.kind==="machine"?{kind:"machine"}:{kind:"http",endpoint:n.transport.endpoint}}}}i(m,"installedManifest");async function C(e,n,r=!1){const t=ce();m(e)===void 0&&P.set(E(e),{status:"installing"});try{return I(u(t,"tmp")),await ie(de(t,e),async()=>{const o=w(e),a=W(o);if(a!==null&&a.provider===void 0)throw new c("provider_unavailable",`plugin "${e}" is installed and is not the directory's provider bundle`);const s=m(e)!==void 0;if(s&&r)return"unchanged";const f=s&&n?oe("user").plugins[e]?.etag:void 0,d=await q(e,f!==void 0?{ifNoneMatch:f}:{});if(d.status===304)return"unchanged";if(d.status===404||d.status===410)throw new c("not_found",`the directory serves no provider "${e}" (${d.message})`,{param:"provider"});if(d.status==="failed")throw new c("provider_unavailable",`provider "${e}" bundle fetch failed: ${d.message}`);const l=ne(d.raw);if(s&&re(e,"user",o,l))return te("user",e,d.etag,{swapped:!1}),"unchanged";const{installPluginArchive:v}=await L();try{await v(e,d.raw,{scope:"user",scopeRootPath:t,root:o,version:l,etag:d.etag,enable:!s,bundle:{endpoint:`${j()}/providers/${encodeURIComponent(e)}/plugin`},transport:{kind:"http",endpoint:j()},accept:i(p=>V(e,void 0,p),"accept")})}catch(p){A(e,void 0,p);const N=new c("not_found",`provider "${e}" served an invalid bundle: ${p instanceof Error?p.message:String(p)}`,{param:"provider"});throw _.add(N),N}try{ee(p=>se(p))}catch(p){g({level:"error",event:"daemon.providers.docs_reconcile_failed",fields:{provider:e},error:p})}return g({level:"info",event:"daemon.providers.bundle_installed",fields:{provider:e,version:l,replaced:s}}),P.delete(E(e)),s?"updated":"installed"},{timeoutMs:me,timeoutError:i(()=>new c("provider_unavailable",`timed out waiting for another process replacing plugin "${e}"`),"timeoutError")})}catch(o){throw m(e)===void 0&&P.set(E(e),{status:"install_failed",error:he(o)}),o instanceof c?o:(g({level:"error",event:"daemon.providers.bundle_install_failed",fields:{provider:e},error:o}),we(e))}}i(C,"refreshInstalled");async function We(e){k(e),m(e)===void 0&&await C(e,!1,!0)}i(We,"ensureProviderBundle");async function ze(e,n){return k(e),n.version!==void 0&&m(e)?.provider.major!==n.version?R(e,n.version,n.conditional):C(e,n.conditional)}i(ze,"refreshProviderBundle");async function J(e,n,r){const{validatePluginArchive:t}=await L(),o=await t(r);if(o.bundle===void 0)throw new S(`provider "${e}" major ${n} bundle is invalid: ${o.issues.map(s=>s.message).join("; ")}`,o.issues);const a=V(e,n,o.bundle);if(a!==void 0)throw new S(`provider "${e}" major ${n} bundle is invalid: ${a}`,[]);if(o.bundle.transport===void 0)throw new S(`provider "${e}" major ${n} bundle declares no transport`,[]);return o.bundle}i(J,"validateArchive");function $e(e){I(e),M(u(e,"retired"),`${new Date().toISOString()}
2
- `),b(u(e,"bundle.tar"),{force:!0}),b(u(e,"etag"),{force:!0})}i($e,"retire");function K(e,n){return new c("provider_version_retired",`provider "${e}" major ${n} is retired`,{param:"version"})}i(K,"retiredError");async function R(e,n,r){const t=G(e,n),o=u(t,"bundle.tar"),a=h(o),s=a&&r&&h(u(t,"etag"))?y(u(t,"etag"),"utf8"):void 0,f=await q(e,{version:n,...s!==void 0?{ifNoneMatch:s}:{}});if(f.status===304)return"unchanged";if(f.status===410)throw $e(t),g({level:"info",event:"daemon.providers.major_retired",fields:{provider:e,major:n}}),K(e,n);if(f.status===404)throw new c("not_found",`provider "${e}" never served major ${n}`,{param:"version"});if(f.status==="failed")throw new c("provider_unavailable",`provider "${e}" major ${n} bundle fetch failed: ${f.message}`);try{await J(e,n,f.raw)}catch(l){A(e,n,l);const v=new c(a?"provider_unavailable":"not_found",l instanceof Error?l.message:String(l),{param:"version"});throw _.add(v),v}I(t),b(u(t,"retired"),{force:!0});const d=a&&Buffer.from(f.raw).equals(y(o));return d||M(o,f.raw),f.etag!==void 0?M(u(t,"etag"),f.etag):b(u(t,"etag"),{force:!0}),d?"unchanged":a?"updated":"installed"}i(R,"refreshPinned");function ye(){const e=[],n=F();if(!h(n))return e;for(const r of H(n)){if(!z.test(r))continue;let t;try{t=H(u(n,r))}catch{continue}for(const o of t){const a=/^v([1-9][0-9]*)$/.exec(o);a&&h(u(n,r,o,"bundle.tar"))&&e.push([r,Number(a[1])])}}return e}i(ye,"cachedMajors");function be(e){const n=Y(e,{transport:"http",reservedCoreNames:new Set});if(n.manifest===void 0)throw new c("not_found","the provider bundle's commands.json no longer validates");return n.manifest}i(be,"readCommands");const B=new Map,Q=new Map;function _e(e,n){const r=x(n),t=`${r.ino}:${r.mtimeMs}:${r.size}`,o=Q.get(e);if(o?.stamp===t)return o.commands;const a=be(JSON.parse(y(n,"utf8")));return Q.set(e,{stamp:t,commands:a}),a}i(_e,"installedCommands");async function Fe(e,n){k(e);const r=m(e);if(n===void 0||r?.provider.major===n){if(r===void 0)throw new c("not_found",`provider "${e}" has no installed bundle`,{param:"provider"});const l=w(e),v=_e(e,u(l,"commands.json"));return{id:e,major:r.provider.major,provider:r.provider,commands:v,transport:r.transport,source:"installed",root:l}}if(!Number.isInteger(n)||n<1)throw new c("invalid_request","version must be an integer of at least 1",{param:"version"});const t=G(e,n);if(h(u(t,"retired")))throw K(e,n);const o=u(t,"bundle.tar");h(o)||await R(e,n,!1);const a=`${e}/${n}`,s=x(o),f=`${s.mtimeMs}:${s.size}`;let d=B.get(a)?.stamp===f?B.get(a).bundle:void 0;if(d===void 0){try{d=await J(e,n,new Uint8Array(y(o)))}catch(l){throw A(e,n,l),new c("not_found",`provider "${e}" has no valid bundle for major ${n}`,{param:"version"})}B.set(a,{stamp:f,bundle:d})}return{id:e,major:n,provider:d.provider,commands:d.commands,transport:d.transport.kind==="machine"?{kind:"machine"}:{kind:"http",endpoint:d.transport.endpoint},source:"pinned"}}i(Fe,"resolveProviderBundle");let $=null,D=!1;async function Se(){if(U("user").runtime?.issuer){for(const e of ve(fe()))try{await C(e,!0)}catch(n){n instanceof Object&&_.has(n)||Z(e,void 0,n instanceof Error?n.message:String(n))}for(const[e,n]of ye())try{await R(e,n,!0)}catch(r){if(r instanceof c&&r.code==="provider_version_retired"||r instanceof Object&&_.has(r))continue;Z(e,n,r instanceof Error?r.message:String(r))}}}i(Se,"syncOnce");function Ge(){return $!==null?(D=!0,$):($=(async()=>{try{do D=!1,await Se();while(D)}finally{$=null}})(),$)}i(Ge,"syncProviderBundles");export{We as ensureProviderBundle,ve as providerIdsFromGrants,ge as providerInstallState,ze as refreshProviderBundle,Fe as resolveProviderBundle,Ge as syncProviderBundles,we as unavailableProviderError};
1
+ var se=Object.defineProperty;var i=(e,n)=>se(e,"name",{value:n,configurable:!0});import{existsSync as w,readdirSync as z,readFileSync as S,rmSync as $,statSync as k}from"node:fs";import{join as u}from"node:path";import{validateCommandManifest as de}from"../../api/command-manifest/manifest.js";import{crtrHome as F}from"../canvas/paths.js";import{withCanvasWrite as G}from"../canvas/db.js";import{bundleVersion as ue,isInstalledGeneration as ce,recordCheckedGeneration as fe}from"../command-plugins/bundle.js";import{readConfig as q,readState as le,updateConfig as pe,updateState as me}from"../config.js";import{emitEvent as m}from"../events/emit.js";import{withExclusiveDirectoryLockAsync as V}from"../exclusive-lock.js";import{atomicWriteText as x,ensureDir as T,pathExists as ve}from"../fs-utils.js";import{reconcilePackageDocuments as Z}from"../graph/package-docs.js";import{readPluginManifest as C}from"../manifest.js";import{bundleSwapLockPath as L}from"../plugin-swap-lock.js";import{pluginsDir as he,userScopeRoot as J}from"../scope.js";import{listInstalledPlugins as ge}from"../resolver.js";import{localGrants as j}from"../grants/mirror.js";import{ProviderError as c}from"./errors.js";const A="[A-Za-z0-9_-]{1,64}",we=new RegExp(`^(${A}):(${A})$`),K=new RegExp(`^${A}$`),$e=3e4,Q=3e4;function X(e){const n=new Set;for(const r of e)if(r.status==="active")for(const{scope:t}of r.resolved_scopes){const o=we.exec(t);o&&o[1]!=="crtr"&&n.add(o[1])}return[...n].sort()}i(X,"providerIdsFromGrants");function v(e){return u(he("user"),e)}i(v,"pluginRoot");function R(){return u(F(),"providers")}i(R,"providersRoot");function Y(e,n){return u(R(),e,`v${n}`)}i(Y,"majorDir");function D(e){if(!K.test(e))throw new c("not_found",`"${e}" is not a provider id`,{param:"provider"})}i(D,"assertId");function E(){const e=q("user").runtime?.issuer;if(!e)throw new c("not_found","provider bundles need a directory-connected runtime");return e.replace(/\/+$/,"")}i(E,"directoryBase");async function ee(e,n){const r=`${E()}/providers/${encodeURIComponent(e)}/plugin${n.version!==void 0?`?version=${n.version}`:""}`;let t;try{t=await fetch(r,{headers:{Accept:"application/x-tar",...n.ifNoneMatch!==void 0?{"If-None-Match":n.ifNoneMatch}:{}},signal:AbortSignal.timeout($e)})}catch(s){return{status:"failed",message:`could not reach ${r}: ${s instanceof Error?s.message:String(s)}`}}if(t.status===304)return n.ifNoneMatch===void 0?{status:"failed",message:`HTTP 304 from ${r} without a conditional request`}:{status:304};if(t.status===404||t.status===410)return await t.body?.cancel().catch(()=>{}),{status:t.status===404?404:410,message:`HTTP ${t.status} from ${r}`};if(t.status<200||t.status>=300)return await t.body?.cancel().catch(()=>{}),{status:"failed",message:`HTTP ${t.status} from ${r}`};let o;try{o=new Uint8Array(await t.arrayBuffer())}catch(s){return{status:"failed",message:`reading ${r} failed: ${s instanceof Error?s.message:String(s)}`}}const a=t.headers.get("etag");return{status:200,raw:o,...a!==null?{etag:a}:{}}}i(ee,"fetchProviderBundle");const I=new WeakSet;class M extends Error{static{i(this,"BundleInvalid")}issues;constructor(n,r){super(n),this.issues=r}}function ne(e,n,r){if(r.provider===void 0)return"the archive carries no provider block";if(r.provider.id!==e)return`the archive is provider "${r.provider.id}", not "${e}"`;if(n!==void 0&&r.provider.major!==n)return`the archive is major ${r.provider.major}, not ${n}`;const t=["hooks","kinds","page_components","memory_extensions"].filter(o=>r[o]!==void 0);if(t.length>0)return`a provider bundle may not carry ${t.join(", ")}`}i(ne,"wrongBundle");function B(e,n,r){m({level:"error",event:"daemon.providers.bundle_invalid",fields:{provider:e,...n!==void 0?{major:n}:{},...r instanceof M?{issues:r.issues}:{}},error:r})}i(B,"logInvalid");function re(e,n,r){m({level:"warn",event:"daemon.providers.bundle_fetch_failed",fields:{provider:e,...n!==void 0?{major:n}:{},message:r}})}i(re,"logFailure");async function te(){return import("../../commands/pkg/plugin-manage.js")}i(te,"pluginManage");const y=new Map,b=i(e=>`${F()}\0${e}`,"installKey");function ye(e){const n=e,r=n?.code??n?.cause?.code;return typeof r=="string"&&/^[A-Z][A-Z0-9_]{1,30}$/.test(r)?`${r} on provider install directory`:e instanceof c&&e.message.includes("HTTP 404")?"directory returned HTTP 404":e instanceof c&&e.message.includes("HTTP 410")?"directory returned HTTP 410":"provider bundle could not be installed"}i(ye,"publicInstallCause");function be(e){if(h(e)===void 0)return y.get(b(e))??{status:"installing"}}i(be,"providerInstallState");function _e(e){return new c("provider_unavailable",`provider "${e}" unavailable: ${be(e)?.error??"bundle installation pending"}`)}i(_e,"unavailableProviderError");function h(e){const n=C(v(e));if(!(n?.provider===void 0||n.transport?.kind!=="http"&&n.transport?.kind!=="machine")&&ve(u(v(e),"commands.json"))){try{if(k(u(v(e),"commands.json")).size===0)return}catch{return}return{provider:n.provider,transport:n.transport.kind==="machine"?{kind:"machine"}:{kind:"http",endpoint:n.transport.endpoint}}}}i(h,"installedManifest");async function N(e,n,r=!1){const t=J();h(e)===void 0&&y.set(b(e),{status:"installing"});try{return T(u(t,"tmp")),await V(L(t,e),async()=>{const o=v(e),a=C(o);if(a!==null&&a.provider===void 0)throw new c("provider_unavailable",`plugin "${e}" is installed and is not the directory's provider bundle`);const s=h(e)!==void 0;if(s&&r)return"unchanged";const f=s&&n?le("user").plugins[e]?.etag:void 0,d=await ee(e,f!==void 0?{ifNoneMatch:f}:{});if(d.status===304)return"unchanged";if(d.status===404||d.status===410)throw new c("not_found",`the directory serves no provider "${e}" (${d.message})`,{param:"provider"});if(d.status==="failed")throw new c("provider_unavailable",`provider "${e}" bundle fetch failed: ${d.message}`);const l=ue(d.raw);if(s&&ce(e,"user",o,l))return fe("user",e,d.etag,{swapped:!1}),"unchanged";const{installPluginArchive:g}=await te();try{await g(e,d.raw,{scope:"user",scopeRootPath:t,root:o,version:l,etag:d.etag,enable:!s,bundle:{endpoint:`${E()}/providers/${encodeURIComponent(e)}/plugin`},transport:{kind:"http",endpoint:E()},accept:i(p=>ne(e,void 0,p),"accept")})}catch(p){B(e,void 0,p);const W=new c("not_found",`provider "${e}" served an invalid bundle: ${p instanceof Error?p.message:String(p)}`,{param:"provider"});throw I.add(W),W}try{G(p=>Z(p))}catch(p){m({level:"error",event:"daemon.providers.docs_reconcile_failed",fields:{provider:e},error:p})}return m({level:"info",event:"daemon.providers.bundle_installed",fields:{provider:e,version:l,replaced:s}}),y.delete(b(e)),s?"updated":"installed"},{timeoutMs:Q,timeoutError:i(()=>new c("provider_unavailable",`timed out waiting for another process replacing plugin "${e}"`),"timeoutError")})}catch(o){throw h(e)===void 0&&y.set(b(e),{status:"install_failed",error:ye(o)}),o instanceof c?o:(m({level:"error",event:"daemon.providers.bundle_install_failed",fields:{provider:e},error:o}),_e(e))}}i(N,"refreshInstalled");async function Je(e){D(e),h(e)===void 0&&await N(e,!1,!0)}i(Je,"ensureProviderBundle");async function Ke(e,n){return D(e),n.version!==void 0&&h(e)?.provider.major!==n.version?H(e,n.version,n.conditional):N(e,n.conditional)}i(Ke,"refreshProviderBundle");async function oe(e,n,r){const{validatePluginArchive:t}=await te(),o=await t(r);if(o.bundle===void 0)throw new M(`provider "${e}" major ${n} bundle is invalid: ${o.issues.map(s=>s.message).join("; ")}`,o.issues);const a=ne(e,n,o.bundle);if(a!==void 0)throw new M(`provider "${e}" major ${n} bundle is invalid: ${a}`,[]);if(o.bundle.transport===void 0)throw new M(`provider "${e}" major ${n} bundle declares no transport`,[]);return o.bundle}i(oe,"validateArchive");function Pe(e){T(e),x(u(e,"retired"),`${new Date().toISOString()}
2
+ `),$(u(e,"bundle.tar"),{force:!0}),$(u(e,"etag"),{force:!0})}i(Pe,"retire");function ie(e,n){return new c("provider_version_retired",`provider "${e}" major ${n} is retired`,{param:"version"})}i(ie,"retiredError");async function H(e,n,r){const t=Y(e,n),o=u(t,"bundle.tar"),a=w(o),s=a&&r&&w(u(t,"etag"))?S(u(t,"etag"),"utf8"):void 0,f=await ee(e,{version:n,...s!==void 0?{ifNoneMatch:s}:{}});if(f.status===304)return"unchanged";if(f.status===410)throw Pe(t),m({level:"info",event:"daemon.providers.major_retired",fields:{provider:e,major:n}}),ie(e,n);if(f.status===404)throw new c("not_found",`provider "${e}" never served major ${n}`,{param:"version"});if(f.status==="failed")throw new c("provider_unavailable",`provider "${e}" major ${n} bundle fetch failed: ${f.message}`);try{await oe(e,n,f.raw)}catch(l){B(e,n,l);const g=new c(a?"provider_unavailable":"not_found",l instanceof Error?l.message:String(l),{param:"version"});throw I.add(g),g}T(t),$(u(t,"retired"),{force:!0});const d=a&&Buffer.from(f.raw).equals(S(o));return d||x(o,f.raw),f.etag!==void 0?x(u(t,"etag"),f.etag):$(u(t,"etag"),{force:!0}),d?"unchanged":a?"updated":"installed"}i(H,"refreshPinned");function Se(){const e=[],n=R();if(!w(n))return e;for(const r of z(n)){if(!K.test(r))continue;let t;try{t=z(u(n,r))}catch{continue}for(const o of t){const a=/^v([1-9][0-9]*)$/.exec(o);a&&w(u(n,r,o,"bundle.tar"))&&e.push([r,Number(a[1])])}}return e}i(Se,"cachedMajors");function Ee(e){const n=de(e,{transport:"http",reservedCoreNames:new Set});if(n.manifest===void 0)throw new c("not_found","the provider bundle's commands.json no longer validates");return n.manifest}i(Ee,"readCommands");const _=new Map,O=new Map;function Ie(e,n){const r=k(n),t=`${r.ino}:${r.mtimeMs}:${r.size}`,o=O.get(e);if(o?.stamp===t)return o.commands;const a=Ee(JSON.parse(S(n,"utf8")));return O.set(e,{stamp:t,commands:a}),a}i(Ie,"installedCommands");async function Qe(e,n){D(e);const r=h(e);if(n===void 0||r?.provider.major===n){if(r===void 0)throw new c("not_found",`provider "${e}" has no installed bundle`,{param:"provider"});const l=v(e),g=Ie(e,u(l,"commands.json"));return{id:e,major:r.provider.major,provider:r.provider,commands:g,transport:r.transport,source:"installed",root:l}}if(!Number.isInteger(n)||n<1)throw new c("invalid_request","version must be an integer of at least 1",{param:"version"});const t=Y(e,n);if(w(u(t,"retired")))throw ie(e,n);const o=u(t,"bundle.tar");w(o)||await H(e,n,!1);const a=`${e}/${n}`,s=k(o),f=`${s.mtimeMs}:${s.size}`;let d=_.get(a)?.stamp===f?_.get(a).bundle:void 0;if(d===void 0){try{d=await oe(e,n,new Uint8Array(S(o)))}catch(l){throw B(e,n,l),new c("not_found",`provider "${e}" has no valid bundle for major ${n}`,{param:"version"})}_.set(a,{stamp:f,bundle:d})}return{id:e,major:n,provider:d.provider,commands:d.commands,transport:d.transport.kind==="machine"?{kind:"machine"}:{kind:"http",endpoint:d.transport.endpoint},source:"pinned"}}i(Qe,"resolveProviderBundle");let P=null,U=!1;function Me(e,n){return e?.provider?.id===n&&e.bundle?.endpoint===`${E()}/providers/${encodeURIComponent(n)}/plugin`}i(Me,"isDirectoryBundle");function ae(e){return X(j()).includes(e)}i(ae,"grantNames");async function ke(e){await V(L(J(),e),async()=>{if(!(ae(e)||!Me(C(v(e)),e))){$(v(e),{recursive:!0,force:!0}),pe("user",n=>{delete n.plugins[e]}),me("user",n=>{delete n.plugins[e]}),$(u(R(),e),{recursive:!0,force:!0}),O.delete(e),y.delete(b(e));for(const n of[..._.keys()])n.startsWith(`${e}/`)&&_.delete(n);try{G(n=>Z(n))}catch(n){m({level:"error",event:"daemon.providers.docs_reconcile_failed",fields:{provider:e},error:n})}m({level:"info",event:"daemon.providers.bundle_uninstalled",fields:{provider:e}})}},{timeoutMs:Q,timeoutError:i(()=>new c("provider_unavailable",`timed out waiting for another process replacing plugin "${e}"`),"timeoutError")})}i(ke,"uninstallProvider");async function xe(){if(!j().every(e=>e.status!=="active")){for(const e of ge("user"))if(!(e.manifest.provider===void 0||ae(e.name)))try{await ke(e.name)}catch(n){m({level:"error",event:"daemon.providers.bundle_uninstall_failed",fields:{provider:e.name},error:n})}}}i(xe,"uninstallDisconnected");async function Te(){if(!q("user").runtime?.issuer)return;const e=X(j());for(const n of e)try{await N(n,!0)}catch(r){r instanceof Object&&I.has(r)||re(n,void 0,r instanceof Error?r.message:String(r))}await xe();for(const[n,r]of Se())try{await H(n,r,!0)}catch(t){if(t instanceof c&&t.code==="provider_version_retired"||t instanceof Object&&I.has(t))continue;re(n,r,t instanceof Error?t.message:String(t))}}i(Te,"syncOnce");function Xe(){return P!==null?(U=!0,P):(P=(async()=>{try{do U=!1,await Te();while(U)}finally{P=null}})(),P)}i(Xe,"syncProviderBundles");export{Je as ensureProviderBundle,X as providerIdsFromGrants,be as providerInstallState,Ke as refreshProviderBundle,Qe as resolveProviderBundle,Xe as syncProviderBundles,_e as unavailableProviderError};
@@ -7,7 +7,7 @@ export interface ProviderLeaf {
7
7
  tool: string;
8
8
  leaf: ManifestHttpLeaf;
9
9
  }
10
- export declare function providerLeaves(commands: ValidatedCommandManifest, provider: string): ProviderLeaf[];
10
+ export declare function providerLeaves(commands: ValidatedCommandManifest): ProviderLeaf[];
11
11
  export type ProviderHelpLeaf = Pick<ManifestHttpLeaf, 'kind' | 'name' | 'description' | 'whenToUse' | 'tier' | 'summary' | 'params' | 'output' | 'effects' | 'group'>;
12
12
  export interface ProviderHelpBranch extends Pick<ManifestBranch<ManifestHttpLeaf>, 'kind' | 'name' | 'description' | 'whenToUse' | 'tier' | 'summary' | 'model'> {
13
13
  rootEntry?: ManifestRootEntry;
@@ -63,4 +63,4 @@ export interface ProviderResponseContext {
63
63
  }
64
64
  export declare function onProviderResponse(context: ProviderResponseContext): string | undefined;
65
65
  export declare function mapProviderResponse(status: number, body: unknown, headers: Record<string, string | string[] | undefined>, approved?: boolean): ProviderError;
66
- export declare function callProvider(caller: ProviderCaller, input: ProviderCallInput, request: ProviderRequest): Promise<ProviderCallResult>;
66
+ export declare function callProvider(caller: ProviderCaller, rawInput: ProviderCallInput, request: ProviderRequest): Promise<ProviderCallResult>;
@@ -1 +1 @@
1
- var Z=Object.defineProperty;var g=(e,r)=>Z(e,"name",{value:r,configurable:!0});import{validateDeclaredResult as x}from"../../api/command-manifest/result.js";import{callerAllows as H,requireGroup as ee}from"../../daemon/api/handlers/providers.js";import{grantFor as re,terminalGrant as te}from"../grants/mirror.js";import{assertUnderCap as L,UNCAPPED_GRANTEE as oe}from"../usage/ledger.js";import{listInstalledPlugins as ne}from"../resolver.js";import{ProviderError as p}from"./errors.js";import{ensureProviderBundle as se,refreshProviderBundle as ie,resolveProviderBundle as S,unavailableProviderError as ae}from"./bundles.js";import{prepareFileCall as de,resignForRetry as G,resolveFileResult as z}from"./files.js";import{consumeApproval as ue,requireProviderApproval as pe}from"./approval.js";import{outboundToken as le}from"./mint.js";import{discardFile as ce}from"./token-cache.js";import{registerProviderCall as fe}from"./inflight.js";import{buildRequestPlan as me}from"../command-plugins/transport/http-plan.js";import{sendProviderRequest as ge,ProviderTransportError as ve}from"./request.js";import{acceptProviderReceipt as he}from"./receipts.js";import{writeProviderAudit as we}from"./audit.js";import{selectDevice as be,sendDeviceCall as ye,deviceFailure as _e,deviceUnreachable as Ae,DEFAULT_DEVICE_REQUEST_MS as je}from"./device.js";import{envelopeForProviderCaller as ke}from"../peer/envelope.js";function Q(e,r){const a=[],n=g((i,s)=>{const u=[...s,i.name];if(i.kind==="branch"){for(const v of i.children)n(v,u);return}const t=u[0]===r?u.slice(1).join(" "):u.join(" ");a.push({tool:t,leaf:i})},"walk");for(const i of e.roots)n(i,[]);return a}g(Q,"providerLeaves");function V(e,r,a){if(e.kind!=="branch")return null;const n=[];for(const m of e.children)if(m.kind==="branch"){const f=V(m,r,a);f&&n.push(f)}else if("group"in m&&m.group&&H(a,`${r}:${m.group}`)){const{kind:f,name:A,description:o,whenToUse:P,tier:h,summary:j,params:_,output:$,effects:E,group:k}=m;n.push({kind:f,name:A,description:o,whenToUse:P,...h===void 0?{}:{tier:h},summary:j,params:_,output:$,effects:E,group:k})}if(n.length===0)return null;const{kind:i,name:s,description:u,whenToUse:t,tier:v,summary:c,model:y,rootEntry:b}=e;return{kind:i,name:s,description:u,whenToUse:t,...v===void 0?{}:{tier:v},summary:c,...y===void 0?{}:{model:y},...b===void 0?{}:{rootEntry:b},children:n}}g(V,"providerHelpBranch");async function We(e,r){const a=await S(r),n=Q(a.commands,r),i=ne("user").find(s=>s.name===r)?.manifest.description??r;return{provider:r,status:"available",major:a.major,display_name:i,transport:a.transport.kind,groups:a.provider.groups.filter(s=>H(e,`${r}:${s.group}`)).map(s=>({group:s.group,leaves:n.filter(({leaf:u})=>u.group===s.group).map(({tool:u,leaf:t})=>({tool:u,description:t.description,summary:t.summary,params:t.params,output:t.output}))})),commands:a.commands.roots.flatMap(s=>{const u=V(s,r,e);return u?[u]:[]})}}g(We,"providerTools");function qe(e){return he(e.headers["crtr-receipt"],e)}g(qe,"onProviderResponse");function K(e){if(!(e.class==="person"||!e.sub||!e.grantee))return{grantee:e.grantee,spend_cap:re(e.grantee,e.sub)?.spend_cap??null}}g(K,"capTarget");function Pe(e){if(e.class==="person"){const r=te()?.sub;if(!r)throw new p("grant_revoked","This machine has no active Northlight grant for your terminal. Run `crtr sys join` again from /account/machines.");return{sub:r,grantee:r}}if(!e.sub||!e.grantee)throw new p("grant_revoked","the caller has no active grant");return{sub:e.sub,grantee:e.grantee}}g(Pe,"identity");function $e(e,r){return[`${e.id}:${r.group}`]}g($e,"callScope");function Ee(e,r){const a=Q(e.commands,e.id).find(n=>n.tool===r);if(!a)throw new p("not_found",`tool "${r}" is not in provider "${e.id}"`,{param:"tool"});if(!a.leaf.group)throw new p("not_found",`tool "${r}" has no group`,{param:"tool"});return a.leaf}g(Ee,"leafFor");function U(e){if(!e||typeof e!="object"||Array.isArray(e))return{};const r=e.error;return!r||typeof r!="object"||Array.isArray(r)?{}:r}g(U,"errorBody");function Re(e){const r=Array.isArray(e)?e[0]:e;if(!r)return;const a=Number(r);if(Number.isFinite(a)&&a>=0)return a;const n=Date.parse(r);return Number.isFinite(n)?Math.max(0,Math.ceil((n-Date.now())/1e3)):void 0}g(Re,"retryAfter");function O(e,r,a,n=!1){const i=U(r),s=typeof i.code=="string"?i.code:void 0,u=typeof i.message=="string"?i.message:`provider answered HTTP ${e}`,t=typeof i.field=="string"?i.field:void 0,c={origin:"provider",details:{code:s??null,message:u,...t?{field:t}:{},...typeof i.next=="string"?{next:i.next}:{},...i.received!==void 0?{received:i.received}:{},status:e},...t?{param:t}:{}};return e===429?new p("provider_rate_limited",u,{...c,retry_after_s:Re(a["retry-after"])}):e===401||e>=500?new p("provider_unavailable",u,c):e===403&&s==="provider_refused"?new p("provider_refused",u,c):e===403&&s==="approval_required"?new p("provider_error",n?"provider asked for approval again":"provider approval question is invalid",{...c,status:e}):new p("provider_error",u,{...c,status:e})}g(O,"mapProviderResponse");function W(e,r){return JSON.stringify({major:e.major,endpoint:e.transport.kind==="http"?e.transport.endpoint:null,baseUrl:e.commands.baseUrl??null,rest:r.rest,params:r.params,output:r.output})}g(W,"approvalShape");async function Xe(e,r,a){const n=new AbortController,i=g(()=>n.abort(a.signal.reason),"abort");a.signal.aborted?i():a.signal.addEventListener("abort",i,{once:!0});let s=a.requestId;const u=fe({request_id:s,run_id:e.run_id,node_id:e.node_id,controller:n});let t,v,c,y,b,m,f,A="allowed";try{try{await se(r.provider)}catch{throw ae(r.provider)}let o=await S(r.provider,r.version),P=!1,h,j,_,$=!1,E=!1,k,B=0;for(;;){const w=Ee(o,r.tool);if(c=`${o.id}:${w.group}`,ee(e,o.id,w.group),L(K(e)),o.transport.kind!=="machine"&&r.machine!==void 0)throw new p("invalid_argument",`provider "${o.id}" is not served by a device, so it takes no machine`,{param:"machine"});o.transport.kind==="machine"&&k===void 0&&(k=await be(r.machine),B=Date.now()+(o.commands.timeouts?.requestMs??je));let I=null;if(r.approval&&!$){const d=ue(e,r,W(o,w));_=d.option,I=d.prepared,f=r.approval,s=d.requestId,$=!0}const R={params:w.params.filter(d=>"type"in d&&d.type==="file").map(d=>d.name),output:w.output.find(d=>d.type==="file")?.name??null},J=JSON.stringify(R);if(j!==void 0&&j!==J)throw new p("provider_bad_response","the refreshed manifest changed the tool file contract",{origin:"provider",details:{status:409,message:"manifest_stale changed the tool file contract"}});j=J,I&&(h=await G(e,R,r.arguments,I,{signal:n.signal})),h??=await de(e,R,r.arguments,{signal:n.signal});const{sub:M,grantee:T}=Pe(e);if(L(K(e)),o.transport.kind==="machine"){const d=await Te(e,o,r,w,h,{sub:M,requestId:s,deadlineAt:B,signal:n.signal,machine:k});return m=d.responder,t=d.cleanup,{status:200,body:d.result}}const F=await le({audience:o.transport.endpoint,sub:M,grantee:T,initiating_app:T,scope:$e(o,w)},{signal:n.signal});y=F.jti;const X={registration:{name:o.id,endpoint:o.transport.endpoint},...o.commands.baseUrl?{baseUrl:o.commands.baseUrl}:{},...o.commands.timeouts?{timeouts:o.commands.timeouts}:{},params:w.params,rest:w.rest,output:w.output,commandPath:[o.id,...r.tool.split(" ")]},Y=me(X,h.arguments,void 0,{authorization:`Bearer ${F.access_token}`,requestId:s,fileMode:"pass-through",extraHeaders:{...e.run_id?{"Crtr-Run-Id":e.run_id}:{},..._?{"Crtr-Approval":_}:{},...h.headers}});v??=Date.now()+(o.commands.timeouts?.requestMs??3e4);let l;try{if(l=await ge(Y,{connectMs:o.commands.timeouts?.connectMs,requestMs:o.commands.timeouts?.requestMs,deadlineAt:v,signal:n.signal}),n.signal.aborted)throw n.signal.reason??new Error("provider call aborted")}catch(d){throw n.signal.aborted?n.signal.reason??d:d instanceof ve?new p("provider_unavailable",d.message,{origin:"provider",details:{status:null,code:d.kind}}):d}if(b??=qe({provider:o.id,tool:r.tool,request_id:s,run_id:e.run_id,sub:M,grantee:T,ledger_grantee:e.class==="person"?oe:T,jti:F.jti,status:l.status,headers:l.headers}),m=l.headers["crtr-responder"],l.status===401&&ce(F.key),l.status>=200&&l.status<300){const d=l.body;if(!d||typeof d!="object"||Array.isArray(d))throw new p("provider_bad_response","provider result is not a JSON object",{origin:"provider",details:{status:l.status,message:"provider result is not a JSON object"}});let C;try{C=await z(e,h,d,{signal:n.signal})}catch(q){throw q instanceof p&&q.code==="provider_error"?new p("provider_bad_response",q.message,{origin:"provider",details:{status:l.status,message:q.message}}):q}t=C.cleanup;const D=x(w.output,C.result);if(D)throw new p("provider_bad_response",`provider result has an invalid output field "${D.field}"`,{origin:"provider",param:D.field,details:{status:l.status,...D}});if(n.signal.aborted)throw n.signal.reason??new Error("provider call aborted");return{status:200,body:C.result}}const N=U(l.body);if(l.status===403&&N.code==="approval_required"&&!_&&!E){E=!0;const d=await pe(e,r,s,N.question,n.signal,W(o,w),h);if(f=d.questionId,_=d.option,h=await G(e,R,r.arguments,h,{signal:n.signal}),v=void 0,n.signal.aborted)throw new p("user_denied","the approval wait was canceled");continue}if(l.status===403&&N.code==="approval_required"&&_)throw O(l.status,l.body,l.headers,!0);if(l.status!==401&&N.manifest_stale===!0&&!P){P=!0,await ie(o.id,{conditional:!1,...r.version===void 0?{}:{version:r.version}}),o=await S(o.id,r.version);continue}throw O(l.status,l.body,l.headers,!!_)}}catch(o){throw o instanceof p&&o.question_id&&(f=o.question_id),m===void 0&&o instanceof p&&typeof o.responder=="string"&&(m=o.responder),A=`refused:${o instanceof p?o.code:"server_error"}`,t?.(),o}finally{try{we({request_id:s,caller:e,provider:r.provider,tool:r.tool,scope_checked:c,outcome:A,jti:y,receipt_id:b,responder:m,question_id:f})}finally{u()}a.signal.removeEventListener("abort",i)}}g(Xe,"callProvider");async function Te(e,r,a,n,i,s){const{machine:u}=s,t=await ye(u,{tool:a.tool,arguments:i.arguments,version:r.major,requestId:s.requestId,runId:e.run_id,deadlineAt:s.deadlineAt,upload:i.upload===null?null:{id:i.upload.upload_id,link:i.upload.upload_link}},ke(e,s.sub),s.signal);if(s.signal.aborted)throw s.signal.reason??new Error("provider call aborted");if("unreachable"in t)throw Ae(u,t.unreachable.reason,t.unreachable.message);const v=t.headers["crtr-responder"],c=U(t.body);if(t.status<200||t.status>=300){const f=_e(u,t.status,typeof c.code=="string"?c.code:void 0,typeof c.message=="string"?c.message:void 0,t.body),A=f===null&&t.status>=500&&typeof c.code=="string"?new p("provider_error",typeof c.message=="string"?c.message:`the device answered HTTP ${t.status}`,{origin:"runtime",status:t.status,details:{code:c.code,status:t.status}}):null;throw Object.assign(f??A??O(t.status,t.body,t.headers),{responder:v})}const y=t.body&&typeof t.body=="object"&&!Array.isArray(t.body)?t.body.result:void 0;if(!y||typeof y!="object"||Array.isArray(y))throw Object.assign(new p("provider_bad_response","the device result is not a JSON object",{origin:"provider",details:{status:t.status,message:"the device result is not a JSON object"}}),{responder:v});let b;try{b=await z(e,i,y,{signal:s.signal})}catch(f){throw f instanceof p&&f.code==="provider_error"?Object.assign(new p("provider_bad_response",f.message,{origin:"provider",details:{status:t.status,message:f.message}}),{responder:v}):f}const m=x(n.output,b.result);if(m)throw b.cleanup?.(),Object.assign(new p("provider_bad_response",`the device result has an invalid output field "${m.field}"`,{origin:"provider",param:m.field,details:{status:t.status,...m}}),{responder:v});return{result:b.result,responder:v,cleanup:b.cleanup}}g(Te,"callDevice");export{Xe as callProvider,O as mapProviderResponse,qe as onProviderResponse,Q as providerLeaves,We as providerTools};
1
+ var re=Object.defineProperty;var f=(e,t)=>re(e,"name",{value:t,configurable:!0});import{validateDeclaredResult as L}from"../../api/command-manifest/result.js";import{callerAllows as G,requireGroup as te}from"../../daemon/api/handlers/providers.js";import{grantFor as oe,terminalGrant as ne}from"../grants/mirror.js";import{assertUnderCap as z,UNCAPPED_GRANTEE as ie}from"../usage/ledger.js";import{listInstalledPlugins as se}from"../resolver.js";import{ProviderError as u}from"./errors.js";import{resolveProviderRef as ae}from"./roots.js";import{ensureProviderBundle as de,refreshProviderBundle as ue,resolveProviderBundle as O,unavailableProviderError as pe}from"./bundles.js";import{prepareFileCall as le,resignForRetry as Q,resolveFileResult as V}from"./files.js";import{consumeApproval as ce,requireProviderApproval as fe}from"./approval.js";import{outboundToken as me}from"./mint.js";import{discardFile as ve}from"./token-cache.js";import{registerProviderCall as ge}from"./inflight.js";import{buildRequestPlan as he}from"../command-plugins/transport/http-plan.js";import{sendProviderRequest as we,ProviderTransportError as be}from"./request.js";import{acceptProviderReceipt as ye}from"./receipts.js";import{writeProviderAudit as _e}from"./audit.js";import{selectDevice as Ae,sendDeviceCall as je,deviceFailure as ke,deviceUnreachable as qe,DEFAULT_DEVICE_REQUEST_MS as Pe}from"./device.js";import{envelopeForProviderCaller as $e}from"../peer/envelope.js";function K(e){const t=[],s=e.roots.length===1?e.roots[0].name:void 0,p=f((r,n)=>{const d=[...n,r.name];if(r.kind==="branch"){for(const g of r.children)p(g,d);return}const o=d[0]===s?d.slice(1).join(" "):d.join(" ");t.push({tool:o,leaf:r})},"walk");for(const r of e.roots)p(r,[]);return t}f(K,"providerLeaves");function W(e,t,s){if(e.kind!=="branch")return null;const p=[];for(const v of e.children)if(v.kind==="branch"){const m=W(v,t,s);m&&p.push(m)}else if("group"in v&&v.group&&G(s,`${t}:${v.group}`)){const{kind:m,name:_,description:j,whenToUse:P,tier:i,summary:$,params:y,output:k,effects:A,group:E}=v;p.push({kind:m,name:_,description:j,whenToUse:P,...i===void 0?{}:{tier:i},summary:$,params:y,output:k,effects:A,group:E})}if(p.length===0)return null;const{kind:r,name:n,description:d,whenToUse:o,tier:g,summary:c,model:w,rootEntry:b}=e;return{kind:r,name:n,description:d,whenToUse:o,...g===void 0?{}:{tier:g},summary:c,...w===void 0?{}:{model:w},...b===void 0?{}:{rootEntry:b},children:p}}f(W,"providerHelpBranch");async function er(e,t){const s=await O(t),p=K(s.commands),r=se("user").find(n=>n.name===t)?.manifest.description??t;return{provider:t,status:"available",major:s.major,display_name:r,transport:s.transport.kind,groups:s.provider.groups.filter(n=>G(e,`${t}:${n.group}`)).map(n=>({group:n.group,leaves:p.filter(({leaf:d})=>d.group===n.group).map(({tool:d,leaf:o})=>({tool:d,description:o.description,summary:o.summary,params:o.params,output:o.output}))})),commands:s.commands.roots.flatMap(n=>{const d=W(n,t,e);return d?[d]:[]})}}f(er,"providerTools");function Ee(e){return ye(e.headers["crtr-receipt"],e)}f(Ee,"onProviderResponse");function X(e){if(!(e.class==="person"||!e.sub||!e.grantee))return{grantee:e.grantee,spend_cap:oe(e.grantee,e.sub)?.spend_cap??null}}f(X,"capTarget");function Re(e){if(e.class==="person"){const t=ne()?.sub;if(!t)throw new u("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 u("grant_revoked","the caller has no active grant");return{sub:e.sub,grantee:e.grantee}}f(Re,"identity");function Te(e,t){return[`${e.id}:${t.group}`]}f(Te,"callScope");function Fe(e,t){const s=K(e.commands).find(p=>p.tool===t);if(!s)throw new u("not_found",`tool "${t}" is not in provider "${e.id}"`,{param:"tool"});if(!s.leaf.group)throw new u("not_found",`tool "${t}" has no group`,{param:"tool"});return s.leaf}f(Fe,"leafFor");function I(e){if(!e||typeof e!="object"||Array.isArray(e))return{};const t=e.error;return!t||typeof t!="object"||Array.isArray(t)?{}:t}f(I,"errorBody");function Ne(e){const t=Array.isArray(e)?e[0]:e;if(!t)return;const s=Number(t);if(Number.isFinite(s)&&s>=0)return s;const p=Date.parse(t);return Number.isFinite(p)?Math.max(0,Math.ceil((p-Date.now())/1e3)):void 0}f(Ne,"retryAfter");function B(e,t,s,p=!1){const r=I(t),n=typeof r.code=="string"?r.code:void 0,d=typeof r.message=="string"?r.message:`provider answered HTTP ${e}`,o=typeof r.field=="string"?r.field:void 0,c={origin:"provider",details:{code:n??null,message:d,...o?{field:o}:{},...typeof r.next=="string"?{next:r.next}:{},...r.received!==void 0?{received:r.received}:{},status:e},...o?{param:o}:{}};return e===429?new u("provider_rate_limited",d,{...c,retry_after_s:Ne(s["retry-after"])}):e===401||e>=500?new u("provider_unavailable",d,c):e===403&&n==="provider_refused"?new u("provider_refused",d,c):e===403&&n==="approval_required"?new u("provider_error",p?"provider asked for approval again":"provider approval question is invalid",{...c,status:e}):new u("provider_error",d,{...c,status:e})}f(B,"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})}f(Y,"approvalShape");async function rr(e,t,s){const p=ae(t.provider);if(p===null)throw new u("not_found",`provider "${t.provider}" is not available`,{param:"provider"});const r={...t,provider:p},n=new AbortController,d=f(()=>n.abort(s.signal.reason),"abort");s.signal.aborted?d():s.signal.addEventListener("abort",d,{once:!0});let o=s.requestId;const g=ge({request_id:o,run_id:e.run_id,node_id:e.node_id,controller:n});let c,w,b,v,m,_,j,P="allowed";try{try{await de(r.provider)}catch{throw pe(r.provider)}let i=await O(r.provider,r.version),$=!1,y,k,A,E=!1,J=!1,M,x=0;for(;;){const h=Fe(i,r.tool);if(b=`${i.id}:${h.group}`,te(e,i.id,h.group),z(X(e)),i.transport.kind!=="machine"&&r.machine!==void 0)throw new u("invalid_argument",`provider "${i.id}" is not served by a device, so it takes no machine`,{param:"machine"});i.transport.kind==="machine"&&M===void 0&&(M=await Ae(r.machine),x=Date.now()+(i.commands.timeouts?.requestMs??Pe));let S=null;if(r.approval&&!E){const a=ce(e,r,Y(i,h));A=a.option,S=a.prepared,j=r.approval,o=a.requestId,E=!0}const R={params:h.params.filter(a=>"type"in a&&a.type==="file").map(a=>a.name),output:h.output.find(a=>a.type==="file")?.name??null},H=JSON.stringify(R);if(k!==void 0&&k!==H)throw new u("provider_bad_response","the refreshed manifest changed the tool file contract",{origin:"provider",details:{status:409,message:"manifest_stale changed the tool file contract"}});k=H,S&&(y=await Q(e,R,r.arguments,S,{signal:n.signal})),y??=await le(e,R,r.arguments,{signal:n.signal});const{sub:U,grantee:T}=Re(e);if(z(X(e)),i.transport.kind==="machine"){const a=await Ce(e,i,r,h,y,{sub:U,requestId:o,deadlineAt:x,signal:n.signal,machine:M});return _=a.responder,c=a.cleanup,{status:200,body:a.result}}const F=await me({audience:i.transport.endpoint,sub:U,grantee:T,initiating_app:T,scope:Te(i,h)},{signal:n.signal});v=F.jti;const Z={registration:{name:i.id,endpoint:i.transport.endpoint},...i.commands.baseUrl?{baseUrl:i.commands.baseUrl}:{},...i.commands.timeouts?{timeouts:i.commands.timeouts}:{},params:h.params,rest:h.rest,output:h.output,commandPath:[i.id,...r.tool.split(" ")]},ee=he(Z,y.arguments,void 0,{authorization:`Bearer ${F.access_token}`,requestId:o,fileMode:"pass-through",extraHeaders:{...e.run_id?{"Crtr-Run-Id":e.run_id}:{},...A?{"Crtr-Approval":A}:{},...y.headers}});w??=Date.now()+(i.commands.timeouts?.requestMs??3e4);let l;try{if(l=await we(ee,{connectMs:i.commands.timeouts?.connectMs,requestMs:i.commands.timeouts?.requestMs,deadlineAt:w,signal:n.signal}),n.signal.aborted)throw n.signal.reason??new Error("provider call aborted")}catch(a){throw n.signal.aborted?n.signal.reason??a:a instanceof be?new u("provider_unavailable",a.message,{origin:"provider",details:{status:null,code:a.kind}}):a}if(m??=Ee({provider:i.id,tool:r.tool,request_id:o,run_id:e.run_id,sub:U,grantee:T,ledger_grantee:e.class==="person"?ie:T,jti:F.jti,status:l.status,headers:l.headers}),_=l.headers["crtr-responder"],l.status===401&&ve(F.key),l.status>=200&&l.status<300){const a=l.body;if(!a||typeof a!="object"||Array.isArray(a))throw new u("provider_bad_response","provider result is not a JSON object",{origin:"provider",details:{status:l.status,message:"provider result is not a JSON object"}});let C;try{C=await V(e,y,a,{signal:n.signal})}catch(q){throw q instanceof u&&q.code==="provider_error"?new u("provider_bad_response",q.message,{origin:"provider",details:{status:l.status,message:q.message}}):q}c=C.cleanup;const D=L(h.output,C.result);if(D)throw new u("provider_bad_response",`provider result has an invalid output field "${D.field}"`,{origin:"provider",param:D.field,details:{status:l.status,...D}});if(n.signal.aborted)throw n.signal.reason??new Error("provider call aborted");return{status:200,body:C.result}}const N=I(l.body);if(l.status===403&&N.code==="approval_required"&&!A&&!J){J=!0;const a=await fe(e,r,o,N.question,n.signal,Y(i,h),y);if(j=a.questionId,A=a.option,y=await Q(e,R,r.arguments,y,{signal:n.signal}),w=void 0,n.signal.aborted)throw new u("user_denied","the approval wait was canceled");continue}if(l.status===403&&N.code==="approval_required"&&A)throw B(l.status,l.body,l.headers,!0);if(l.status!==401&&N.manifest_stale===!0&&!$){$=!0,await ue(i.id,{conditional:!1,...r.version===void 0?{}:{version:r.version}}),i=await O(i.id,r.version);continue}throw B(l.status,l.body,l.headers,!!A)}}catch(i){throw i instanceof u&&i.question_id&&(j=i.question_id),_===void 0&&i instanceof u&&typeof i.responder=="string"&&(_=i.responder),P=`refused:${i instanceof u?i.code:"server_error"}`,c?.(),i}finally{try{_e({request_id:o,caller:e,provider:r.provider,tool:r.tool,scope_checked:b,outcome:P,jti:v,receipt_id:m,responder:_,question_id:j})}finally{g()}s.signal.removeEventListener("abort",d)}}f(rr,"callProvider");async function Ce(e,t,s,p,r,n){const{machine:d}=n,o=await je(d,{tool:s.tool,arguments:r.arguments,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}},$e(e,n.sub),n.signal);if(n.signal.aborted)throw n.signal.reason??new Error("provider call aborted");if("unreachable"in o)throw qe(d,o.unreachable.reason,o.unreachable.message);const g=o.headers["crtr-responder"],c=I(o.body);if(o.status<200||o.status>=300){const m=ke(d,o.status,typeof c.code=="string"?c.code:void 0,typeof c.message=="string"?c.message:void 0,o.body),_=m===null&&o.status>=500&&typeof c.code=="string"?new u("provider_error",typeof c.message=="string"?c.message:`the device answered HTTP ${o.status}`,{origin:"runtime",status:o.status,details:{code:c.code,status:o.status}}):null;throw Object.assign(m??_??B(o.status,o.body,o.headers),{responder:g})}const w=o.body&&typeof o.body=="object"&&!Array.isArray(o.body)?o.body.result:void 0;if(!w||typeof w!="object"||Array.isArray(w))throw Object.assign(new u("provider_bad_response","the device result is not a JSON object",{origin:"provider",details:{status:o.status,message:"the device result is not a JSON object"}}),{responder:g});let b;try{b=await V(e,r,w,{signal:n.signal})}catch(m){throw m instanceof u&&m.code==="provider_error"?Object.assign(new u("provider_bad_response",m.message,{origin:"provider",details:{status:o.status,message:m.message}}),{responder:g}):m}const v=L(p.output,b.result);if(v)throw b.cleanup?.(),Object.assign(new u("provider_bad_response",`the device result has an invalid output field "${v.field}"`,{origin:"provider",param:v.field,details:{status:o.status,...v}}),{responder:g});return{result:b.result,responder:g,cleanup:b.cleanup}}f(Ce,"callDevice");export{rr as callProvider,B as mapProviderResponse,Ee as onProviderResponse,K as providerLeaves,er as providerTools};
@@ -0,0 +1,19 @@
1
+ import type { InstalledPlugin } from '../../types.js';
2
+ /** The single root command name of a provider bundle's `commands.json`, or
3
+ * undefined when it declares none or more than one. */
4
+ export declare function rootNameOfCommands(raw: unknown): string | undefined;
5
+ /** An installed provider plugin's root command name, cached per file replacement. */
6
+ export declare function installedRootName(plugin: Pick<InstalledPlugin, 'root'>): string | undefined;
7
+ export interface ProviderRootClaims {
8
+ /** root command name → provider id (the earliest-installed claimant). */
9
+ byRoot: Map<string, string>;
10
+ /** Provider ids skipped because an earlier-installed provider claims their root. */
11
+ skipped: Set<string>;
12
+ }
13
+ /** Every enabled installed provider bundle's root. When two claim one root the
14
+ * later-installed is skipped (and logged once), never shadowing the first. */
15
+ export declare function providerRootClaims(plugins?: readonly InstalledPlugin[]): ProviderRootClaims;
16
+ /** A provider id or a root command name → the provider id. An installed id
17
+ * wins; an unknown name passes through unchanged. A provider skipped for
18
+ * claiming a root another provider serves is unreachable by id as well (`null`). */
19
+ export declare function resolveProviderRef(ref: string): string | null;
@@ -0,0 +1 @@
1
+ var v=Object.defineProperty;var m=(o,n)=>v(o,"name",{value:n,configurable:!0});import{readFileSync as y,statSync as w}from"node:fs";import{join as S}from"node:path";import{emitEvent as $}from"../events/emit.js";import{readState as h}from"../config.js";import{listInstalledPlugins as p}from"../resolver.js";function g(o){const n=o?.mounts;if(!Array.isArray(n))return;const t=new Set;for(const r of n)Array.isArray(r?.parent)&&r.parent.length===0&&typeof r.node?.name=="string"&&t.add(r.node.name);return t.size===1?[...t][0]:void 0}m(g,"rootNameOfCommands");const u=new Map;function C(o){const n=S(o.root,"commands.json");try{const t=w(n),r=`${t.ino}:${t.mtimeMs}:${t.size}`,d=u.get(n);if(d?.stamp===r)return d.root;const a=g(JSON.parse(y(n,"utf8")));return u.set(n,{stamp:r,root:a}),a}catch{return}}m(C,"installedRootName");const l=new Set;function R(o=p("user")){const n=o.filter(e=>e.enabled&&e.manifest.provider!==void 0&&e.manifest.provider.id===e.name),t=new Map(n.map(e=>[e.name,C(e)])),r=new Set,d=new Set;for(const e of t.values())e!==void 0&&(r.has(e)&&d.add(e),r.add(e));if(d.size>0){const e=h("user").plugins;n.sort((s,i)=>(e[s.name]?.last_updated??"").localeCompare(e[i.name]?.last_updated??"")||s.name.localeCompare(i.name))}const a=new Map,c=new Set;for(const e of n){const s=t.get(e.name);if(s===void 0)continue;const i=a.get(s);if(i===void 0){a.set(s,e.name);continue}c.add(e.name);const f=`${e.name}\0${s}\0${i}`;l.has(f)||(l.add(f),$({level:"error",event:"daemon.providers.bundle_invalid",fields:{provider:e.name,issues:[{code:"root_command_claimed",message:`root command "${s}" is already served by provider "${i}"; "${e.name}" is skipped`}]}}))}return{byRoot:a,skipped:c}}m(R,"providerRootClaims");function z(o){const n=p("user"),t=R(n);return t.skipped.has(o)?null:n.some(r=>r.name===o&&r.manifest.provider!==void 0)?o:t.byRoot.get(o)??o}m(z,"resolveProviderRef");export{C as installedRootName,R as providerRootClaims,z as resolveProviderRef,g as rootNameOfCommands};
@@ -22,7 +22,7 @@ export declare function tokenCacheDir(): string;
22
22
  export declare function cacheKey(request: OutboundTokenRequest): string;
23
23
  /** The cached token for `key` while it is still reusable, else null (a stale or unreadable file is removed). */
24
24
  export declare function cachedToken(key: string, nowMs?: number): CachedToken | null;
25
- /** The discard epoch of the pair a key names; pass it back to `storeToken`. */
25
+ /** The discard epoch of the pair and providers a key names (it only ever grows); pass it back to `storeToken`. */
26
26
  export declare function discardEpoch(key: string): number;
27
27
  /** Store a freshly minted token, unless its pair was discarded since `epoch` was read. Returns whether it was stored. */
28
28
  export declare function storeToken(key: string, token: CachedToken, epoch?: number): boolean;
@@ -36,9 +36,17 @@ export declare function changedGrantPairs(before: readonly GrantRecord[], after:
36
36
  sub: string;
37
37
  grantee: string;
38
38
  }>;
39
+ /** The (`sub`, provider) keys a sync stopped serving: a provider whose scopes
40
+ * the active grants resolved before and no longer do, or whose connection
41
+ * grant (`sub` to `app:<provider>`) was active before and is not now, even
42
+ * while another grant still names its scopes. */
43
+ export declare function droppedProviders(before: readonly GrantRecord[], after: readonly GrantRecord[]): string[];
39
44
  /** The grant-sync hook (D3-TOK-20, CRD-16): discard every cached token of each
40
- * changed grant's (`sub`, `grantee`), and prune expired files. */
45
+ * changed grant's (`sub`, `grantee`) and of each (`sub`, provider) the sync
46
+ * stopped resolving (whatever the token's grantee — a person's own calls mint
47
+ * as (`sub`, `sub`)), and prune expired files. */
41
48
  export declare function discardChangedGrantTokens(before: readonly GrantRecord[], after: readonly GrantRecord[], nowMs?: number): {
42
49
  pairs: number;
50
+ providers: number;
43
51
  tokens: number;
44
52
  };
@@ -1 +1 @@
1
- var k=Object.defineProperty;var o=(t,e)=>k(t,"name",{value:e,configurable:!0});import{createHash as J}from"node:crypto";import{mkdirSync as N,readdirSync as O,readFileSync as b,rmSync as p}from"node:fs";import{join as l}from"node:path";import{crtrHome as _}from"../canvas/paths.js";import{atomicWriteJson as j}from"../fs-utils.js";const F=30;function f(){return l(_(),"secrets","outbound-tokens")}o(f,"tokenCacheDir");function C(t){return JSON.stringify([t.audience,t.sub,t.grantee,t.initiating_app,[...new Set(t.scope)].sort()])}o(C,"cacheKey");function g(t){return l(f(),`${J("sha256").update(t).digest("hex")}.json`)}o(g,"fileFor");function m(t){try{const e=JSON.parse(t);return Array.isArray(e)&&typeof e[1]=="string"&&typeof e[2]=="string"?d(e[1],e[2]):null}catch{return null}}o(m,"pairOf");function d(t,e){return JSON.stringify([t,e])}o(d,"pairKey");function y(t){try{const e=JSON.parse(b(t,"utf8"));return typeof e.key!="string"||typeof e.access_token!="string"||typeof e.jti!="string"||typeof e.exp!="number"?null:e}catch{return null}}o(y,"readFile");function v(t,e){return e<(t.exp-F)*1e3}o(v,"usable");function H(t,e=Date.now()){const r=g(t),n=y(r);return!n||n.key!==t||!v(n,e)?(p(r,{force:!0}),null):{access_token:n.access_token,jti:n.jti,exp:n.exp}}o(H,"cachedToken");const a=new Map;function h(t){const e=m(t);return e===null?0:a.get(e)??0}o(h,"discardEpoch");function K(t,e,r=h(t)){return h(t)!==r?!1:(N(f(),{recursive:!0,mode:448}),j(g(t),{key:t,...e},{mode:384}),!0)}o(K,"storeToken");function P(t){p(g(t),{force:!0})}o(P,"discardFile");function x(t,e){let r;try{r=O(f())}catch{return 0}let n=0;for(const s of r){if(!s.endsWith(".json"))continue;const i=l(f(),s),c=y(i),u=c?m(c.key):null;c&&u!==null&&t.has(u)?(p(i,{force:!0}),n++):(!c||e>=c.exp*1e3)&&p(i,{force:!0})}return n}o(x,"sweep");function R(t,e,r=Date.now()){const n=d(t,e);return a.set(n,(a.get(n)??0)+1),x(new Set([n]),r)}o(R,"discardFor");function S(t){const e=new Map;for(const r of t){const n=r.resolved_scopes.map(({scope:i,mode:c})=>`${c} ${i}`).sort(),s=d(r.sub,r.grantee);e.set(s,[...e.get(s)??[],JSON.stringify([r.id,r.status,n])])}return new Map([...e].map(([r,n])=>[r,JSON.stringify(n.sort())]))}o(S,"grantStates");function D(t,e){const r=S(t),n=S(e),s=[];for(const[i,c]of r){if(n.get(i)===c)continue;const[u,w]=JSON.parse(i);s.push({sub:u,grantee:w})}return s}o(D,"changedGrantPairs");function W(t,e,r=Date.now()){const n=D(t,e).map(({sub:s,grantee:i})=>d(s,i));for(const s of n)a.set(s,(a.get(s)??0)+1);return{pairs:n.length,tokens:x(new Set(n),r)}}o(W,"discardChangedGrantTokens");export{F as REUSE_MARGIN_S,C as cacheKey,H as cachedToken,D as changedGrantPairs,W as discardChangedGrantTokens,h as discardEpoch,P as discardFile,R as discardFor,K as storeToken,f as tokenCacheDir};
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 y(e){return h(l(),`${A("sha256").update(e).digest("hex")}.json`)}o(y,"fileFor");function m(e){try{const t=JSON.parse(e);if(!Array.isArray(t)||typeof t[1]!="string"||typeof t[2]!="string")return null;const n=(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(n)]}}catch{return null}}o(m,"partsOf");function B(e){const t=m(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 g(e,t){return JSON.stringify([e,t])}o(g,"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"));return typeof t.key!="string"||typeof t.access_token!="string"||typeof t.jti!="string"||typeof t.exp!="number"?null:t}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 r=y(e),n=x(r);return!n||n.key!==e||!K(n,t)?(d(r,{force:!0}),null):{access_token:n.access_token,jti:n.jti,exp:n.exp}}o(L,"cachedToken");const f=new Map,S=new Map;function w(e){const t=m(e);return t===null?0:(f.get(p(t.sub,t.grantee))??0)+t.providers.reduce((r,n)=>r+(S.get(g(t.sub,n))??0),0)}o(w,"discardEpoch");function Q(e,t,r=w(e)){return w(e)!==r?!1:(M(l(),{recursive:!0,mode:448}),P(y(e),{key:e,...t},{mode:384}),!0)}o(Q,"storeToken");function V(e){d(y(e),{force:!0})}o(V,"discardFile");function b(e,t,r){let n;try{n=E(l())}catch{return 0}let s=0;for(const i of n){if(!i.endsWith(".json"))continue;const c=h(l(),i),a=x(c),u=a?m(a.key):null,N=u!==null&&(e.has(p(u.sub,u.grantee))||u.providers.some(_=>t.has(g(u.sub,_))));a&&N?(d(c,{force:!0}),s++):(!a||r>=a.exp*1e3)&&d(c,{force:!0})}return s}o(b,"sweep");function X(e,t,r=Date.now()){const n=p(e,t);return f.set(n,(f.get(n)??0)+1),b(new Set([n]),new Set,r)}o(X,"discardFor");function O(e){const t=new Map;for(const r of e){const n=r.resolved_scopes.map(({scope:i,mode:c})=>`${c} ${i}`).sort(),s=p(r.sub,r.grantee);t.set(s,[...t.get(s)??[],JSON.stringify([r.id,r.status,n])])}return new Map([...t].map(([r,n])=>[r,JSON.stringify(n.sort())]))}o(O,"grantStates");function T(e,t){const r=O(e),n=O(t),s=[];for(const[i,c]of r){if(n.get(i)===c)continue;const[a,u]=JSON.parse(i);s.push({sub:a,grantee:u})}return s}o(T,"changedGrantPairs");function k(e){const t=new Set;for(const r of e)if(r.status==="active")for(const{scope:n}of r.resolved_scopes){const s=v(n);s!==null&&t.add(g(r.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 r=k(t),n=new Set([...k(e)].filter(i=>!r.has(i))),s=J(t);for(const i of J(e)){if(s.has(i))continue;const[c,a]=JSON.parse(i);n.add(g(c,a.slice(4)))}return[...n]}o(W,"droppedProviders");function Y(e,t,r=Date.now()){const n=T(e,t),s=new Set(n.map(({sub:c,grantee:a})=>p(c,a))),i=new Set(W(e,t));for(const c of s)f.set(c,(f.get(c)??0)+1);for(const c of i)S.set(c,(S.get(c)??0)+1);return{pairs:s.size,providers:i.size,tokens:b(s,i,r)}}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};
@@ -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 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
+ var I=Object.defineProperty;var i=(e,r)=>I(e,"name",{value:r,configurable:!0});import{randomUUID as O}from"node:crypto";import{readConfig as $}from"../../../core/config.js";import{activeGrantForApp as q,localGrants as w}from"../../../core/grants/mirror.js";import{getNode as x}from"../../../core/canvas/canvas.js";import{scopeAllowed as G}from"../../../core/scopes.js";import{ProviderError as u,providerErrorBody as N,scopeMissing as T}from"../../../core/providers/errors.js";import{callProvider as C,providerTools as F}from"../../../core/providers/call.js";import{providerIdsFromGrants as j,providerInstallState as D}from"../../../core/providers/bundles.js";import{providerRootClaims as k,resolveProviderRef as M}from"../../../core/providers/roots.js";import{listInstalledPlugins as U}from"../../../core/resolver.js";import{listAudit as z,writeProviderAudit as L}from"../../../core/providers/audit.js";import{emitEvent as _}from"../../../core/events/emit.js";import{resolvedScopes as g}from"../router.js";const be=new Set(["provider.tools","provider.call","audit.list"]);function V(e){if(e.class==="forwarded"||e.class==="machine")throw new u("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=q(e.app);if(!r)throw new u("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:g(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:g(e),nodeScopes:e.scopes,run_id:e.run??x(e.node)?.run_id??null,node_id:e.node}}i(V,"callerFromRequest");function we(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}}i(we,"callerFromApp");const Z=/^([A-Za-z0-9_-]{1,64}):([A-Za-z0-9_-]{1,64})$/;function B(e){const r=Z.exec(e);return r!==null&&r[1]!=="crtr"}i(B,"isProviderScope");const y=new WeakMap;function H(e){let r=y.get(e);return r===void 0&&(r=new Set(e),y.set(e,r)),r}i(H,"heldSet");function S(e,r){return e===null?!0:B(r)?H(e).has(r):G(e,r)}i(S,"holds");function A(e,r){return e.class==="person"?!0:S(e.resolved??[],r)&&S(e.nodeScopes,r)}i(A,"callerAllows");function _e(e,r,t){const o=`${r}:${t}`;if(!A(e,o))throw T(o)}i(_e,"requireGroup");function W(){return!!$("user").runtime?.issuer}i(W,"providerOperationsEnabled");const J=/^[A-Za-z0-9_-]{1,64}$/;function c(e,r){return new u("invalid_request",e,{param:r})}i(c,"invalid");function E(e){if(typeof e!="string"||!J.test(e)||e==="crtr")throw c("provider must be a provider id","provider");return e}i(E,"parseProvider");function X(e){if(!e||typeof e!="object"||Array.isArray(e))throw new u("invalid_request","a JSON object is required");const r=e,t=E(r.provider),o=r.tool;if(typeof o!="string"||!o.trim())throw c("tool must be a non-empty string","tool");const d=r.arguments??{};if(!d||typeof d!="object"||Array.isArray(d))throw c("arguments must be an object","arguments");const a=r.version;if(a!==void 0&&(typeof a!="number"||!Number.isInteger(a)||a<1))throw c("version must be an integer >= 1","version");const n=r.approval;if(n!==void 0&&(typeof n!="string"||!n))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:o.trim(),arguments:d,...a===void 0?{}:{version:a},...n===void 0?{}:{approval:n},...l===void 0?{}:{machine:l.trim()}}}i(X,"parseCallInput");function K(e,r=new URLSearchParams){const t=i((n,l,p)=>{const f=r.get(n);if(f===null)return l;if(!/^\d+$/.test(f)||Number(f)>p||Number(f)<1)throw c(`invalid ${n}`,n);return Number(f)},"numeric"),o=r.has("after")?t("after",0,Number.MAX_SAFE_INTEGER):void 0,d=t("limit",100,1e3),a=Object.fromEntries(["operation","outcome","run_id","since","until"].filter(n=>r.has(n)).map(n=>[n,r.get(n)]));return{status:200,body:z(e.class==="person"?null:e.grantee,{after:o,limit:d,...a})}}i(K,"auditList");async function Q(e,r,t){let o=r;const d=performance.now(),a=U("user"),{skipped:n}=k(a),l=new Set(a.filter(s=>s.enabled&&s.manifest.provider?.id===s.name&&!n.has(s.name)).map(s=>s.name)),p=j(w()).filter(s=>!n.has(s)),f=[...new Set([...l,...p])].sort();if(o!==void 0){const s=M(o);if(s===null)throw new u("not_found",`provider "${o}" is not available`);o=s}if(o!==void 0&&!f.includes(o))throw new u("not_found",`provider "${o}" is not available`);const P=o===void 0?f:[o],R=e.class==="person"?w().flatMap(s=>s.status==="active"?s.resolved_scopes.map(({scope:h})=>h):[]):e.resolved??[],v=await Promise.all(P.map(async s=>{if(!R.some(m=>m.startsWith(`${s}:`)&&A(e,m)))return null;if(l.has(s)){const m=await F(e,s);return m.groups.length>0?m:null}const b=D(s);return{provider:s,display_name:s,status:b.status,...b.error?{error:b.error}:{},groups:[],commands:[]}})).then(s=>s.filter(h=>h!==null));if(_({level:"debug",event:"api.provider.tools",duration_ms:performance.now()-d,fields:{caller:e.class,provider:o??null,providers:v.length}}),o!==void 0){if(v.length===0)throw new u("not_found",`provider "${o}" is not available`);return{status:200,body:v[0]}}return{status:200,body:{providers:v}}}i(Q,"providerTools");async function Y(e,r,t){return C(e,r,t)}i(Y,"providerCall");async function ee(e,r,t,o){if(!W())throw new u("not_found","provider operations need a directory-connected runtime");if(e==="audit.list")return K(r,t.query);if(e==="provider.tools")return Q(r,t.params.provider===void 0?void 0:E(t.params.provider),o);if(e==="provider.call"){let d;try{d=X(t.body)}catch(a){const n=t.body&&typeof t.body=="object"&&!Array.isArray(t.body)?t.body:{};throw L({request_id:o.requestId,caller:r,provider:typeof n.provider=="string"?n.provider:"",tool:typeof n.tool=="string"?n.tool:"",outcome:`refused:${a instanceof u?a.code:"invalid_request"}`}),a}return Y(r,d,o)}throw new u("not_found","unknown operation")}i(ee,"handleProviderOperation");function re(e){const r=new AbortController,t=i(()=>{e.writableFinished||r.abort()},"abort");return e.on("close",t),{signal:r.signal,release:i(()=>e.off("close",t),"release")}}i(re,"abortOnClose");function oe(e,r,t,o){const d=N(e,r,t,o??void 0);return d.status>=500&&!(e instanceof u)&&_({event:"api.provider.failed",level:"error",error:e instanceof Error?e:new Error(String(e)),fields:{request_id:r}}),d}i(oe,"providerFailure");const ge=[{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:i(async o=>{const d=O(),{signal:a,release:n}=re(o.res);let l=null;try{return l=V(o.principal),{...await ee(t,l,{params:o.params,body:o.body,query:new URL(o.req.url??"/","http://runtime.local").searchParams},{requestId:d,signal:a}),headers:{"Request-Id":d}}}catch(p){return oe(p,d,!1,l?.run_id)}finally{n()}},"handler")}));export{be as PROVIDER_OPERATIONS,re as abortOnClose,A as callerAllows,we as callerFromApp,V as callerFromRequest,ee as handleProviderOperation,X as parseCallInput,E as parseProvider,oe as providerFailure,W as providerOperationsEnabled,ge as providerRoutes,_e as requireGroup};
@@ -17,4 +17,4 @@ Start with the [package README](../../packages/crouter-plugin/README.md) for a c
17
17
  - [Deployment](deploying.md) — Fetch frameworks, mount paths, proxies, authentication, and archive compression.
18
18
  - [Bundles and documents](bundles-and-memory.md) — generating an archive and shipping agent-facing documents.
19
19
 
20
- The package exports only `LeafError`, `ManifestInvalidError`, `PluginDefinitionError`, `buildBundle`, `buildCommandManifest`, `createFetchHandler`, `defineBranch`, `defineLeaf`, `definePlugin`, `defineStreamingLeaf`, `field`, `kebab`, and `param`. Those are the complete authoring surface.
20
+ The primary authoring surface is `LeafError`, `ManifestInvalidError`, `PluginDefinitionError`, `buildBundle`, `buildCommandManifest`, `createFetchHandler`, `createNodeHandler`, `defineBranch`, `defineLeaf`, `definePlugin`, `defineStreamingLeaf`, `field`, `kebab`, and `param`. The package also exports `compareManifests`, `createCallerVerifier`, `postReceipt`, `ReceiptPostError` and the structural types `createNodeHandler` takes.
@@ -5,7 +5,7 @@ description: Serve plugin Fetch handlers with authentication, mount paths, and a
5
5
 
6
6
  # Deployment
7
7
 
8
- `createFetchHandler(plugin, options)` returns `(request: Request) => Promise<Response>`. Use it directly in Cloudflare Workers, Bun, Deno, and any framework route that accepts Fetch `Request` and `Response` objects. A framework with different request types needs only an adapter at its boundary; the package itself has no framework dependency.
8
+ `createFetchHandler(plugin, options)` returns `(request: Request) => Promise<Response>`. Use it directly in Cloudflare Workers, Bun, Deno, and any framework route that accepts Fetch `Request` and `Response` objects. For Node's `http`, Express and Nest, mount it with `createNodeHandler` (see [Node, Express and Nest](#node-express-and-nest)); the package itself has no framework dependency.
9
9
 
10
10
  The handler serves the archive for every `GET` request and runs a command for a matching `POST` request. It routes a command from the longest matching suffix of the request path, so the same handler works at the origin root, under `/crtr`, and behind a proxy that preserves that path.
11
11
 
@@ -21,7 +21,33 @@ Set `baseUrl` only when `request.url` is not the public URL, such as a proxy tha
21
21
 
22
22
  Pass `token` to require `Authorization: Bearer <token>` on archive and command requests. `--auth-env NAME` tells crtr which environment variable of the calling process holds that value. It does not configure the server. For a command an agent runs, the calling process is its broker, which reads the profile env store (`crtr profile env set <profile> --name NAME`) rather than your shell's exports. Omitting `token` serves without authentication and logs a warning when the handler is created.
23
23
 
24
- A capability provider registered in the directory passes `auth: {issuer, audience}` instead of `token`; passing both throws. `issuer` is the directory's issuer URL and `audience` is the provider's endpoint URL exactly as registered. Every command request's bearer must be an outbound token the directory minted. The handler verifies it with `@north-light/identity`: it fetches the JWKS from `<issuer>/.well-known/jwks.json`, caches it, and refetches once when a token names an unknown `kid`. It checks the signature, `iss`, `aud` (exact match, so a trailing slash differs) and `exp`. A failed check answers 401 `unauthorized`. Next the called leaf's `<plugin>:<group>` must be in the token's `scope`. If it is not, the handler answers 403 `scope_missing` with that string in both `field` and `received`. A command request under `auth` must also carry `Crtr-Request-Id`. Every leaf then gets `ctx.caller`: `{sub, grantee, clientId, scope, initiatingApp, act, jti, requestId, runId?}`. `act` and `initiatingApp` are for your audit; the kit authorizes nothing on them. The archive and `GET /versions` stay public under `auth`. For local development, point `issuer` at a test issuer that serves `/.well-known/jwks.json`.
24
+ A capability provider registered in the directory passes `auth: {issuer, audience, providerId}` instead of `token`; passing both throws. `providerId` is the provider's directory principal id — for a third-party app, its OAuth client id, bare or as `app:<id>`; 1–64 characters of `A-Z a-z 0-9 _ -`, validated when the handler is created. `issuer` is the directory's issuer URL and `audience` is the provider's endpoint URL exactly as registered. Every command request's bearer must be an outbound token the directory minted. The handler verifies it with `@north-light/identity`: it fetches the JWKS from `<issuer>/.well-known/jwks.json`, caches it, and refetches once when a token names an unknown `kid`. It checks the signature, `iss`, `aud` (exact match, so a trailing slash differs) and `exp`. A failed check answers 401 `unauthorized`. Next the called leaf's `<providerId>:<group>` must be in the token's `scope` (`<plugin name>:<group>` when `providerId` is not set, which is right only for a first-party provider whose name is its id; the directory always mints `<provider id>:<group>`, so a third-party app that omits `providerId` gets 403 on every call). If the scope is missing, the handler answers 403 `scope_missing` with that string in both `field` and `received`; when `providerId` is unset and the token carries the leaf's group under another id, `next` reads `Set auth.providerId to your app's client id (<that id>).` A command request under `auth` must also carry `Crtr-Request-Id`. Every leaf then gets `ctx.caller`: `{sub, grantee, clientId, scope, initiatingApp, act, jti, requestId, runId?}`. `act` and `initiatingApp` are for your audit; the kit authorizes nothing on them. The archive and `GET /versions` stay public under `auth`. For local development, point `issuer` at a test issuer that serves `/.well-known/jwks.json`.
25
+
26
+ ## Node, Express and Nest
27
+
28
+ `createNodeHandler(handler, { bodyLimit?, origin? })` adapts the Fetch handler to `(req, res) => Promise<void>` for Node's `http`, Express and Nest. It imports nothing from `node:`, and its request and response types are structural, so it needs none of those frameworks' types.
29
+
30
+ ```ts
31
+ import { createFetchHandler, createNodeHandler } from '@north-light/plugin';
32
+
33
+ const handler = createFetchHandler(plugin, {
34
+ auth: { issuer: ISSUER, audience: `${APP_ORIGIN}/plugin`, providerId: CLIENT_ID },
35
+ baseUrl: `${APP_ORIGIN}/plugin`,
36
+ requests,
37
+ });
38
+ const node = createNodeHandler(handler, { bodyLimit: 65536 });
39
+
40
+ // Express / Nest controller method:
41
+ // @All(['', '*path']) serve(@Req() req, @Res() res) { return node(req, res); }
42
+ // node:http:
43
+ // createServer((req, res) => void node(req, res));
44
+ ```
45
+
46
+ - **URL.** `originalUrl ?? url` resolved against `origin`, or against `http://<Host header>` when `origin` is not set. Pass `origin` when the public origin differs from the Host the app sees.
47
+ - **Body.** For a non-`GET`/`HEAD` request, `req.rawBody` (Nest with `rawBody: true`) is used when present; otherwise the stream is read. A `content-length` above `bodyLimit` (default 65536 bytes), or more bytes than that, answers 413 `invalid_request` before the handler runs, so before the token is checked.
48
+ - **Headers.** Every request header is passed on; a header Node delivers as an array is appended value by value. The response keeps repeated headers, `set-cookie` included.
49
+ - **Response.** The body is streamed chunk by chunk, waiting for `drain` when the socket is full. When the client disconnects, the request's `signal` aborts and the response stream is cancelled.
50
+ - **Failure.** A handler that throws answers 500 `handler_failed` in the kit's error shape; the adapter never rejects.
25
51
 
26
52
  ## Major versions
27
53
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.456",
3
+ "version": "0.3.458",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -137,7 +137,7 @@
137
137
  },
138
138
  "license": "UNLICENSED",
139
139
  "dependencies": {
140
- "@north-light/identity": "0.3.456",
140
+ "@north-light/identity": "0.3.458",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/identity",
3
- "version": "0.3.456",
3
+ "version": "0.3.458",
4
4
  "description": "Northlight access-token verification and scope rules",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.456",
3
+ "version": "0.3.458",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/cli",
9
- "version": "0.3.456",
9
+ "version": "0.3.458",
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.456",
21
+ "@north-light/identity": "0.3.458",
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.456",
12098
+ "version": "0.3.458",
12099
12099
  "license": "UNLICENSED"
12100
12100
  },
12101
12101
  "packages/crouter-env-docker": {
12102
12102
  "name": "@north-light/env-docker",
12103
- "version": "0.3.456",
12103
+ "version": "0.3.458",
12104
12104
  "license": "UNLICENSED"
12105
12105
  },
12106
12106
  "packages/crouter-identity": {
12107
12107
  "name": "@north-light/identity",
12108
- "version": "0.3.456",
12108
+ "version": "0.3.458",
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.456",
12116
+ "version": "0.3.458",
12117
12117
  "license": "UNLICENSED",
12118
12118
  "dependencies": {
12119
- "@north-light/api": "0.3.456",
12120
- "@north-light/identity": "0.3.456"
12119
+ "@north-light/api": "0.3.458",
12120
+ "@north-light/identity": "0.3.458"
12121
12121
  }
12122
12122
  },
12123
12123
  "packages/crouter-sdk": {
12124
12124
  "name": "@north-light/sdk",
12125
- "version": "0.3.456",
12125
+ "version": "0.3.458",
12126
12126
  "license": "UNLICENSED",
12127
12127
  "dependencies": {
12128
- "@north-light/api": "0.3.456",
12129
- "@north-light/identity": "0.3.456",
12128
+ "@north-light/api": "0.3.458",
12129
+ "@north-light/identity": "0.3.458",
12130
12130
  "jose": "^6.2.1"
12131
12131
  },
12132
12132
  "bin": {