@north-light/cli 0.3.543 → 0.3.544
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtin-memory/northlight-apps/build-on-your-app.md +47 -0
- package/dist/builtin-memory/northlight-apps/cli.md +3 -3
- package/dist/builtin-memory/northlight-apps/coding-agents.md +16 -7
- package/dist/builtin-memory/northlight-apps/concepts/apps-and-sign-in.md +0 -4
- package/dist/builtin-memory/northlight-apps/concepts/connections.md +0 -4
- package/dist/builtin-memory/northlight-apps/concepts/providers.md +0 -4
- package/dist/builtin-memory/northlight-apps/concepts/runs.md +0 -4
- package/dist/builtin-memory/northlight-apps/concepts/scopes-and-grants.md +0 -4
- package/dist/builtin-memory/northlight-apps/concepts/testing-mode.md +0 -4
- package/dist/builtin-memory/northlight-apps/get-started.md +56 -0
- package/dist/builtin-memory/northlight-apps/guides/go-live.md +1 -1
- package/dist/builtin-memory/northlight-apps/guides/manage-your-app.md +4 -4
- package/dist/builtin-memory/northlight-apps/how-it-fits.md +1 -1
- package/dist/builtin-memory/northlight-apps/know-the-person.md +29 -0
- package/dist/builtin-memory/northlight-apps/let-their-ai-use-your-app.md +67 -0
- package/dist/builtin-memory/northlight-apps/providers/README.md +1 -1
- package/dist/builtin-memory/northlight-apps/providers/context.md +2 -2
- package/dist/builtin-memory/northlight-apps/providers/device.md +3 -3
- package/dist/builtin-memory/northlight-apps/providers/integrations.md +1 -1
- package/dist/builtin-memory/northlight-apps/providers/search.md +1 -1
- package/dist/builtin-memory/northlight-apps/providers/sms.md +3 -3
- package/dist/builtin-memory/northlight-apps/quickstart.md +3 -17
- package/dist/builtin-memory/northlight-apps/reach-people-by-text.md +50 -0
- package/dist/builtin-memory/northlight-apps/use-our-ai.md +55 -0
- package/dist/builtin-memory/northlight-apps/use-our-apps.md +46 -0
- package/dist/core/command-plugins/transport/http-plan.d.ts +2 -0
- package/dist/core/command-plugins/transport/http-plan.js +1 -1
- package/dist/core/providers/call.js +1 -1
- package/dist/daemon/api/app-listener.d.ts +1 -1
- package/dist/daemon/api/app-listener.js +13 -13
- package/dist/daemon/api/handlers/providers.d.ts +3 -1
- package/dist/shared/traceparent.d.ts +6 -0
- package/dist/shared/traceparent.js +1 -0
- package/docs/apps/build-on-your-app.md +44 -0
- package/docs/apps/cli.md +3 -3
- package/docs/apps/coding-agents.md +16 -7
- package/docs/apps/concepts/apps-and-sign-in.md +0 -4
- package/docs/apps/concepts/connections.md +0 -4
- package/docs/apps/concepts/providers.md +0 -4
- package/docs/apps/concepts/runs.md +0 -4
- package/docs/apps/concepts/scopes-and-grants.md +0 -4
- package/docs/apps/concepts/testing-mode.md +0 -4
- package/docs/apps/get-started.md +47 -0
- package/docs/apps/guides/go-live.md +1 -1
- package/docs/apps/guides/manage-your-app.md +4 -4
- package/docs/apps/how-it-fits.md +1 -1
- package/docs/apps/know-the-person.md +27 -0
- package/docs/apps/let-their-ai-use-your-app.md +63 -0
- package/docs/apps/providers/README.md +1 -1
- package/docs/apps/providers/context.md +2 -2
- package/docs/apps/providers/device.md +3 -3
- package/docs/apps/providers/integrations.md +1 -1
- package/docs/apps/providers/search.md +1 -1
- package/docs/apps/providers/sms.md +3 -3
- package/docs/apps/quickstart.md +3 -17
- package/docs/apps/reach-people-by-text.md +47 -0
- package/docs/apps/use-our-ai.md +27 -0
- package/docs/apps/use-our-apps.md +32 -0
- package/docs/cli/join-northlight.md +1 -1
- package/package.json +2 -2
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +12 -12
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: knowledge
|
|
3
|
+
when-and-why-to-read: When you need Reach people by text, this knowledge should
|
|
4
|
+
be read because Give your app its own iMessage number with Comms. People text
|
|
5
|
+
it, your server answers with an agent run on their runtime, and you can text
|
|
6
|
+
them again later.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Reach people by text
|
|
10
|
+
|
|
11
|
+
Comms gives your app its own iMessage number, under your app's name and avatar. People text that number, Comms sends each text to your server as a signed webhook, and your server answers with an agent run on the person's own runtime. Once a person has texted you, your app can text them again later, for example from a timer.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import {continueInRuntime, formatReply, verifyWebhook} from '@north-light/comms';
|
|
15
|
+
|
|
16
|
+
const event = verifyWebhook(req.headers, rawBody, [process.env.COMMS_WEBHOOK_SECRET!]);
|
|
17
|
+
if (event.type === 'message.received' && event.data.content.type === 'text') {
|
|
18
|
+
const {contact, content} = event.data;
|
|
19
|
+
if (!contact.user) return; // only a sender texting from an Apple ID email has no user
|
|
20
|
+
const client = await oauth.client(contact.user.id); // their Northlight user id, the same as Connection.userId
|
|
21
|
+
const {runId, reply} = await continueInRuntime({client, message: content.text, runId: runs.get(contact.id), idempotencyKey: event.id});
|
|
22
|
+
runs.set(contact.id, runId); // later texts continue the same run
|
|
23
|
+
const {text} = await reply.collect();
|
|
24
|
+
for (const part of formatReply(text)) {
|
|
25
|
+
await comms.sendMessage({environment: event.environment, to: {contact_id: contact.id}, text: part, idempotency_key: `reply:${event.id}:${part.length}`});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`oauth.client` throws `CrouterError` with code `not_connected` when the person has not signed in to your app yet. Answer that with `sendSignInLink(comms, contact, {environment: event.environment})`: Comms texts them a link that signs them in to your app, which lands on your **Sign-in start URL** (console → **Profile** → **Marketplace**).
|
|
31
|
+
|
|
32
|
+
## The steps
|
|
33
|
+
|
|
34
|
+
The [Comms guide for coding agents](https://comms.northlight.ai/llms.txt) is the whole reference: the webhook events, the send API, its errors, STOP and HELP handling, and the `crtr comms` commands. In short:
|
|
35
|
+
|
|
36
|
+
1. **Build in fake mode.** `npx @north-light/comms fake --webhook http://localhost:3000/hook` runs a local Comms with no account. `npx @north-light/comms fake send --from +15550001 --text "hi"` plays a person texting you.
|
|
37
|
+
2. **Register your app on Northlight.** [[northlight-apps/quickstart]] steps 1 to 4: sign up, `npx @north-light/sdk login`, `npx @north-light/sdk register`. Comms signs in with the same app key.
|
|
38
|
+
3. **Connect the app to Comms.** At `https://comms.northlight.ai`, sign in and choose **Connect an app**. This is done in a browser only.
|
|
39
|
+
4. **Turn Texting on.** On the app's page there, or with `crtr comms turn-on`: a display name, a square avatar, the HELP text and your test webhook URL. The page shows the test webhook signing secret.
|
|
40
|
+
5. **Text it from your own phone, in test mode.** Add yourself as a tester (`crtr comms testers add --app-id <id> --phone <number> --label <name>`, or the site), then text `JOIN <code>` to the test number. Test mode is free; the [Comms guide](https://comms.northlight.ai/llms.txt) has the tester limit.
|
|
41
|
+
6. **Go to production** when you want your app's own number and anyone to text it. Production is paid: add a card on the billing account, and Comms gets your app its own number.
|
|
42
|
+
|
|
43
|
+
Comms is priced per person your app texts with in production each month; fake mode and testing are free. The SDK is [`@north-light/comms`](https://www.npmjs.com/package/@north-light/comms) on npm. Its 0.3.0 release declares `@north-light/sdk` 0.3.457 as an exact optional peer, so in a project pinned to a newer SDK install it with `npm i @north-light/comms --legacy-peer-deps`.
|
|
44
|
+
|
|
45
|
+
## Rules
|
|
46
|
+
|
|
47
|
+
- **People text first.** Your app can text a person only after they texted your number in that environment, and until they send STOP. A send to anyone else fails `403 not_texted_first`.
|
|
48
|
+
- **Address people by `contact_id` or `user_id`**, never by phone number.
|
|
49
|
+
- **Derive `idempotency_key` from the event you are answering**, so a retried webhook sends nothing twice. Deduplicate deliveries on `event.id`.
|
|
50
|
+
- **Verify the raw body** with `verifyWebhook` before you parse it, and answer `2xx` within 15 seconds.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: knowledge
|
|
3
|
+
when-and-why-to-read: When you need Let your app use our AI, this knowledge
|
|
4
|
+
should be read because Start agent runs on each person's own Northlight
|
|
5
|
+
runtime, with their memory, files and tools, paid by their AI subscription.
|
|
6
|
+
Typed results, streaming, background runs and documents.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Let your app use our AI
|
|
10
|
+
|
|
11
|
+
Your app starts agents on the person's own Northlight runtime. Each run has their memory, files and tools, works under the scopes they approved, and its model calls are paid by their AI subscription, not by you.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import {z} from 'zod';
|
|
15
|
+
import type {ConnectedCrouter} from '@north-light/sdk';
|
|
16
|
+
|
|
17
|
+
const Profile = z.object({
|
|
18
|
+
display_name: z.string(),
|
|
19
|
+
bio: z.string(),
|
|
20
|
+
facts: z.array(z.string()),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/** Start a run on the person's runtime and get a value back that was checked against the schema. */
|
|
24
|
+
export async function draftProfile(runtime: ConnectedCrouter) {
|
|
25
|
+
const outcome = await runtime.runs.parse({
|
|
26
|
+
prompt: 'Draft a public profile for me from my notes.',
|
|
27
|
+
scopes: ['crtr:llm'], // submitting a result needs no other scope
|
|
28
|
+
output_schema: Profile,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (outcome.kind === 'result') return outcome.output_parsed; // {display_name, bio, facts}, typed from Profile
|
|
32
|
+
if (outcome.reason === 'declined') {
|
|
33
|
+
// The agent could not honestly satisfy the schema; outcome.details says why.
|
|
34
|
+
throw new Error(`Declined: ${outcome.details?.reason ?? 'no reason given'}`);
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`The run ended without a result: ${outcome.reason}`); // canceled, deadline_exceeded or runtime_error
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`runs.parse` starts a run and returns a value checked against your schema. `runs.start` returns at once and lets you follow the run, reply to it or leave it running.
|
|
41
|
+
|
|
42
|
+
## Guides
|
|
43
|
+
|
|
44
|
+
| Page | What it covers |
|
|
45
|
+
|---|---|
|
|
46
|
+
| [[northlight-apps/concepts/runs]] | Start a run, its status and outcome, a run's own scopes, and how to bound one |
|
|
47
|
+
| [[northlight-apps/guides/typed-results]] | Get a value back with `runs.parse` and a schema; never parse the trace |
|
|
48
|
+
| [[northlight-apps/guides/stream-a-run]] | Show the agent's text and tool calls as they happen |
|
|
49
|
+
| [[northlight-apps/guides/background-runs]] | Learn when a run you did not wait on has ended, with `RunWatcher` or run notices |
|
|
50
|
+
| [[northlight-apps/guides/files-and-documents]] | Download a file from a run, and read the person's public documents |
|
|
51
|
+
| [[northlight-apps/guides/many-users]] | Keep each person's runtime separate when people meet in your app |
|
|
52
|
+
|
|
53
|
+
To schedule work, call `runs.start` from your own timer. A run's agent may also schedule itself with `crtr cron add` when the run holds `crtr:schedule` ([[northlight-apps/concepts/scopes-and-grants]]).
|
|
54
|
+
|
|
55
|
+
An app that pays for its own model calls instead can serve them from its own service with `inferenceEndpoint` from `@north-light/sdk`; see the [release notes](https://sdk.docs.northlight.ai/docs/release-notes) and the [[northlight-apps/reference]].
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: knowledge
|
|
3
|
+
when-and-why-to-read: When you need Let your app use our apps, this knowledge
|
|
4
|
+
should be read because Call Northlight's providers from your app on the
|
|
5
|
+
person's runtime — texting their phone, web search, their connected accounts,
|
|
6
|
+
their Mac — with providers.tools and providers.call.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Let your app use our apps
|
|
10
|
+
|
|
11
|
+
Northlight's own apps are providers your app can call on the person's runtime: text their phone, place calls, search the web, open a cloud browser, read and act in the accounts they connected, and use their Mac. The person approves each provider's scope once, on your app's consent page.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import type {ConnectedCrouter} from '@north-light/sdk';
|
|
15
|
+
|
|
16
|
+
/** List the tools a provider offers this person, then call one. Scope strings come from the console's
|
|
17
|
+
* "Add permissions" list; tool names and arguments come from `providers.tools`. Never invent either. */
|
|
18
|
+
export async function textMe(runtime: ConnectedCrouter, body: string) {
|
|
19
|
+
const sms = await runtime.providers.tools('sms');
|
|
20
|
+
console.log(JSON.stringify(sms.groups, null, 2)); // each leaf: tool, description, params
|
|
21
|
+
|
|
22
|
+
return runtime.providers.call({provider: 'sms', tool: 'send', arguments: {body}});
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Every provider works the same way: ask for its scope, list its tools with `providers.tools('<provider>')`, then call one with `providers.call`. Scope strings, tool names and arguments come from those lists, never from memory.
|
|
27
|
+
|
|
28
|
+
## Providers
|
|
29
|
+
|
|
30
|
+
| Provider id | What it does | Page |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `sms` | Texts the signed-in person's own verified phone | [[northlight-apps/providers/sms]] |
|
|
33
|
+
| `search` | Web search, sourced answers, page contents and paid data endpoints | [[northlight-apps/providers/search]] |
|
|
34
|
+
| `integrations` | The person's own accounts at other services | [[northlight-apps/providers/integrations]] |
|
|
35
|
+
| `context` | Ask about the person from their memory, and look up public profiles | [[northlight-apps/providers/context]] |
|
|
36
|
+
| `device` | The person's Mac: browser, files, Messages and shell | [[northlight-apps/providers/device]] |
|
|
37
|
+
|
|
38
|
+
These five have pages here. Northlight also runs `phone` (calls from Northlight's number, with an AI voice), `browser` (cloud browser sessions) and `meetings` (the person's meeting notes and transcripts), which have no page yet. For any of them, `providers.tools()` with no argument lists every provider installed on the person's runtime, `providers.tools('<provider>')` lists its tools and arguments, and the console's **Add permissions** list shows every scope your app can ask for.
|
|
39
|
+
|
|
40
|
+
## Guides
|
|
41
|
+
|
|
42
|
+
| Page | What it covers |
|
|
43
|
+
|---|---|
|
|
44
|
+
| [[northlight-apps/concepts/providers]] | Scopes, finding and calling tools, and providers your app requires |
|
|
45
|
+
| [[northlight-apps/providers/README]] | Every first-party provider, its scopes and where names come from |
|
|
46
|
+
| [[northlight-apps/guides/grant-changes]] | When a person adds or removes a scope, and waiting on a provider's setup |
|
|
@@ -25,6 +25,8 @@ export interface HttpInvokerSpec {
|
|
|
25
25
|
export interface RequestPlanOptions {
|
|
26
26
|
authorization: string | null;
|
|
27
27
|
requestId: string;
|
|
28
|
+
/** The trace id a provider call carries on as `traceparent: 00-<trace>-<new parent>-00`; absent sends none. */
|
|
29
|
+
traceId?: string;
|
|
28
30
|
extraHeaders: Record<string, string>;
|
|
29
31
|
fileMode: 'read-local' | 'pass-through';
|
|
30
32
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var R=Object.defineProperty;var c=(e,t)=>R(e,"name",{value:t,configurable:!0});import{readFileSync as O,statSync as k}from"node:fs";import{resolve as j}from"node:path";import{validateDeclaredResult as C}from"../../../api/command-manifest/result.js";import{CrtrError as w}from"../../errors.js";import{envelopeClaimsStale as P,staleErrorDetails as z}from"../../manifest-stale.js";import{ExitCode as h}from"../../../types.js";import{isRecord as x}from"../../../shared/predicates.js";import{trimHeaderValue as U}from"./sign.js";const
|
|
1
|
+
var R=Object.defineProperty;var c=(e,t)=>R(e,"name",{value:t,configurable:!0});import{readFileSync as O,statSync as k}from"node:fs";import{resolve as j}from"node:path";import{validateDeclaredResult as C}from"../../../api/command-manifest/result.js";import{CrtrError as w}from"../../errors.js";import{envelopeClaimsStale as P,staleErrorDetails as z}from"../../manifest-stale.js";import{ExitCode as h}from"../../../types.js";import{isRecord as x}from"../../../shared/predicates.js";import{trimHeaderValue as U}from"./sign.js";import{outboundTraceparent as q}from"../../../shared/traceparent.js";const T=/^[a-z][a-z0-9]*(_[a-z0-9]+)*$/,I=new Set(["internal","unknown_path","command_collision","cli_protocol_error"]),D=new Set(["authorization","crtr-request-id","crtr-run-id","crtr-approval","crtr-upload-id","crtr-upload-link","accept","content-type","crtr-caller","crtr-signature","traceparent"]),M=new Set(["crtr-caller","crtr-signature","crtr-request-id"]);function ne(e,t,r,n){const i=new Map;for(const o of e.params)i.set(o.name,o);const f=Object.entries(e.rest.params),u=e.baseUrl??new URL(e.registration.endpoint).origin;let y=e.rest.path;for(const[o,a]of f){if(a.in!=="path")continue;const s=t[b(o)];y=y.split(`{${o}}`).join(J(s))}const S=new URL(y,u);for(const[o,a]of f){if(a.in!=="query")continue;const s=b(o);v(i.get(o),s,t,r)&&S.searchParams.append(a.as??o,String(t[s]))}const m=G(e,f,i,t,r,n.fileMode),p=Object.keys(m).length>0?JSON.stringify(m):void 0,l={},d=c((o,a)=>{const s=Object.keys(l).find(g=>g.toLowerCase()===o.toLowerCase());s!==void 0&&delete l[s],l[o]=U(a)},"setHeader");d("Accept","application/json"),d("Crtr-Request-Id",n.requestId),n.authorization!==null&&d("Authorization",n.authorization);for(const[o,a]of f){if(a.in!=="header")continue;const s=b(o);if(!v(i.get(o),s,t,r))continue;const g=a.as;if(M.has(g.toLowerCase())||n.fileMode==="pass-through"&&D.has(g.toLowerCase()))throw new w("invalid_type",`${E(e)}: header "${g}" is reserved for the runtime`,h.USAGE,{field:o,next:"Remove the runtime-owned header mapping from the manifest."});const A=String(t[s]);if(/[\r\n]/.test(A))throw new w("invalid_type",`${E(e)}: header "${g}" value must not contain a newline`,h.USAGE,{field:o,received:"(newline in header value)",next:"Remove newline characters from the value."});d(g,A)}p!==void 0&&d("Content-Type","application/json");for(const[o,a]of Object.entries(n.extraHeaders)){if(n.fileMode==="pass-through"&&/[\r\n]/.test(a))throw new w("invalid_type",`${E(e)}: header "${o}" value must not contain a newline`,h.USAGE);d(o,a)}if(n.fileMode==="pass-through"){if(d("Accept","application/json"),p!==void 0)d("Content-Type","application/json");else{const o=Object.keys(l).find(a=>a.toLowerCase()==="content-type");o!==void 0&&delete l[o]}if(d("Crtr-Request-Id",n.requestId),n.traceId!==void 0&&d("traceparent",q(n.traceId)),n.authorization!==null)d("Authorization",n.authorization);else{const o=Object.keys(l).find(a=>a.toLowerCase()==="authorization");o!==void 0&&delete l[o]}}return{url:S,method:e.rest.method,headers:l,...p!==void 0?{body:p}:{}}}c(ne,"buildRequestPlan");function G(e,t,r,n,i,f){const u=new Map;for(const[m,p]of t){if(p.in!=="body")continue;const l=p.as??m,d=u.get(l);d!==void 0?d.push(m):u.set(l,[m])}const y={};for(const[m,p]of u){const l=p.map(s=>({name:s,param:r.get(s)})),d=l.find(s=>s.param.kind!=="stdin"),o=l.find(s=>s.param.kind==="stdin");let a;d!==void 0&&v(d.param,b(d.name),n,i)?a={param:d.param,value:n[b(d.name)]}:o!==void 0&&v(o.param,b(o.name),n,i)&&(a={param:o.param,value:n[b(o.name)]}),a!==void 0&&(y[m]=H(e,a.param,a.value,f))}const S={...e.rest.body};return e.rest.bodyRoot!==void 0?S[e.rest.bodyRoot]=y:Object.assign(S,y),S}c(G,"assembleBody");function H(e,t,r,n){return n==="pass-through"||t.kind==="context-file"?r:(t.kind==="positional"||t.kind==="flag")&&t.type==="path"?N(E(e),t,r):r}c(H,"materializeBodyValue");function N(e,t,r){if(Array.isArray(r))return r.map(u=>N(e,t,u));const n=String(r),i=j(process.cwd(),n);let f;try{f=k(i)}catch{throw _(e,t.name,n,"no file exists at that path","an existing local file path")}if(!f.isFile())throw _(e,t.name,n,"the path is not a regular file","a regular local file (not a directory, socket, or device)");try{return"encoding"in t&&t.encoding==="base64"?O(i).toString("base64"):O(i,"utf8")}catch{throw _(e,t.name,n,"the file could not be read","a readable local file path")}}c(N,"readPathParam");function _(e,t,r,n,i){return new w("invalid_type",`${e}: cannot read file for "${t}" \u2014 ${n}: ${r}`,h.USAGE,{field:t,received:r,next:`Pass ${i}, relative to the current directory or absolute.`})}c(_,"fileError");function v(e,t,r,n){return n!==void 0?n.providedParams.has(t):Object.prototype.hasOwnProperty.call(r,t)&&r[t]!==void 0}c(v,"isPresent");function J(e){const t=String(e);return t==="."||t===".."?t.replace(/\./g,"%2E"):encodeURIComponent(t)}c(J,"encodePathSegmentValue");function b(e){return e.replace(/-([a-z])/g,(t,r)=>r.toUpperCase())}c(b,"paramKey");function oe(e,t){let r;try{r=JSON.parse(t)}catch{throw $(e,"response body was not valid JSON","a JSON object matching the declared output")}if(!x(r))throw $(e,`response body was not a JSON object (got ${B(r)})`,"a JSON object matching the declared output");const n=C(e.output,r);if(n!==null){const f=e.output.find(u=>u.name===n.field)?.type??"unknown";throw n.receivedType!==void 0?$(e,`output field "${n.field}" has the wrong type`,`${n.field}: ${n.expected??f}`):$(e,`response is missing required output field "${n.field}"`,`a "${n.field}" field of type ${f}`)}return r}c(oe,"mapUnary2xx");function ie(e,t,r){const n=V(t),i=L(r);if(i!==null){const f=T.test(i.code)&&!I.has(i.code);throw new w(f?i.code:"cli_protocol_error",i.message,n,{...i.received!==void 0?{received:i.received}:{},...i.field!==void 0?{field:i.field}:{},...i.manifestStale?z(e.registration.name):{},http_status:t,next:i.next??"Inspect the backend response, then retry if appropriate."})}throw new w("cli_http_error",`${E(e)}: request failed with HTTP ${t}`,n,{http_status:t,next:"Inspect the backend response, then retry if appropriate."})}c(ie,"throwNon2xx");function L(e){let t;try{t=JSON.parse(e)}catch{return null}if(!x(t)||!x(t.error))return null;const r=t.error;return typeof r.code!="string"||typeof r.message!="string"?null:{code:r.code,message:r.message,...typeof r.field=="string"?{field:r.field}:{},...r.received!==void 0?{received:r.received}:{},...typeof r.next=="string"?{next:r.next}:{},manifestStale:P(r),...r.question!==void 0?{question:r.question}:{}}}c(L,"parseHttpError");function V(e){return e===400||e===401||e===403||e===422?h.USAGE:e===404?h.NOT_FOUND:e===409?h.AMBIGUOUS:h.GENERAL}c(V,"deriveExit");function $(e,t,r){return new w("cli_protocol_error",`${E(e)}: ${t}. Expected ${r}.`,h.GENERAL,{received:t,next:`Run \`crtr pkg plugin update ${e.registration.name}\` or inspect the backend response.`})}c($,"protocolError");function E(e){return`HTTP-transport plugin "${e.registration.name}" command \`${e.commandPath.join(" ")}\``}c(E,"label");function B(e){return e===null?"null":Array.isArray(e)?"array":typeof e}c(B,"typeName");export{ne as buildRequestPlan,oe as mapUnary2xx,L as parseHttpError,N as readPathParam,ie as throwNon2xx};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var re=Object.defineProperty;var v=(e,r)=>re(e,"name",{value:r,configurable:!0});import{validateDeclaredResult as I}from"../../api/command-manifest/result.js";import{callerAllows as te,requireGroup as oe}from"../../daemon/api/handlers/providers.js";import{terminalGrant as ie}from"../grants/mirror.js";import{UNCAPPED_GRANTEE as ne}from"../usage/ledger.js";import{listInstalledPlugins as se}from"../resolver.js";import{ProviderError as p}from"./errors.js";import{resolveProviderRef as ae}from"./roots.js";import{providerToolPath as de}from"../plugin-objects-manifest.js";import{ensureProviderBundle as ue,refreshProviderBundle as le,resolveProviderBundle as U,unavailableProviderError as pe}from"./bundles.js";import{prepareFileCall as ce,resignForRetry as G,resolveFileResult as z}from"./files.js";import{consumeApproval as fe,requireProviderApproval as me}from"./approval.js";import{outboundToken as ve}from"./mint.js";import{discardFile as ge}from"./token-cache.js";import{registerProviderCall as he}from"./inflight.js";import{buildRequestPlan as we}from"../command-plugins/transport/http-plan.js";import{sendProviderRequest as be,ProviderTransportError as ye}from"./request.js";import{acceptProviderReceipt as _e}from"./receipts.js";import{writeProviderAudit as Ae}from"./audit.js";import{selectDevice as je,sendDeviceCall as ke,deviceFailure as Pe,deviceUnreachable as Q,DEFAULT_DEVICE_REQUEST_MS as qe}from"./device.js";import{isLibsSearch as V,wantsBundling as Ee,callLibsSearch as $e,bundleExec as Ce,mapLibOutcome as Re,reportLibCalls as Se}from"./device-libs.js";import{directoryIssuer as Ne,ownMachineId as Te}from"../peer/directory.js";import{formatCrtrCaller as K}from"../command-plugins/caller-header.js";import{getNode as De}from"../canvas/canvas.js";import{envelopeForProviderCaller as Fe}from"../peer/envelope.js";function W(e){const r=[],s=v((c,t,o)=>{const a=[...t,c.name],d=o||c.tier==="hidden";if(c.kind==="branch"){for(const m of c.children)s(m,a,d);return}const A=de(e.roots.length,a);r.push({tool:A,leaf:c,hidden:d})},"walk");for(const c of e.roots)s(c,[],!1);return r}v(W,"providerLeaves");function X(e){if(e.kind!=="branch")return null;const r=[];for(const h of e.children)if(h.kind==="branch"){const j=X(h);j&&r.push(j)}else if("group"in h&&h.group){const{kind:j,name:u,description:_,whenToUse:g,tier:w,summary:P,params:i,output:k,effects:n,group:b}=h;r.push({kind:j,name:u,description:_,whenToUse:g,...w===void 0?{}:{tier:w},summary:P,params:i,output:k,effects:n,group:b})}if(r.length===0)return null;const{kind:s,name:c,description:t,whenToUse:o,tier:a,summary:d,model:A,rootEntry:m}=e;return{kind:s,name:c,description:t,whenToUse:o,...a===void 0?{}:{tier:a},summary:d,...A===void 0?{}:{model:A},...m===void 0?{}:{rootEntry:m},children:r}}v(X,"providerHelpBranch");async function mr(e,r){const s=await U(r),c=W(s.commands),t=se("user").find(o=>o.name===r)?.manifest.description??r;return{provider:r,status:"available",major:s.major,display_name:t,transport:s.transport.kind,groups:s.provider.groups.filter(o=>te(e,`${r}:${o.group}`)).map(o=>({group:o.group,leaves:c.filter(({leaf:a,hidden:d})=>!d&&a.group===o.group).map(({tool:a,leaf:d})=>({tool:a,description:d.description,summary:d.summary,params:d.params,output:d.output}))})),commands:s.commands.roots.flatMap(o=>{const a=X(o);return a?[a]:[]})}}v(mr,"providerTools");async function Me(e){return await _e(e.headers["crtr-receipt"],e)}v(Me,"onProviderResponse");async function Oe(e){if(e.class==="person"){const r=(await ie())?.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}}v(Oe,"identity");function Ie(e,r){return[`${e.id}:${r.group}`]}v(Ie,"callScope");function Ue(e,r){const s=W(e.commands).find(c=>c.tool===r);if(!s)throw new p("not_found",`tool "${r}" is not in provider "${e.id}"`,{param:"tool"});if(!s.leaf.group)throw new p("not_found",`tool "${r}" has no group`,{param:"tool"});return s.leaf}v(Ue,"leafFor");function B(e){if(!e||typeof e!="object"||Array.isArray(e))return{};const r=e.error;return!r||typeof r!="object"||Array.isArray(r)?{}:r}v(B,"errorBody");function Be(e){const r=Array.isArray(e)?e[0]:e;if(!r)return;const s=Number(r);if(Number.isFinite(s)&&s>=0)return s;const c=Date.parse(r);return Number.isFinite(c)?Math.max(0,Math.ceil((c-Date.now())/1e3)):void 0}v(Be,"retryAfter");function D(e,r,s,c=!1){const t=B(r),o=typeof t.code=="string"?t.code:void 0,a=typeof t.message=="string"?t.message:`provider answered HTTP ${e}`,d=typeof t.field=="string"?t.field:void 0,m={origin:"provider",details:{code:o??null,message:a,...d?{field:d}:{},...typeof t.next=="string"?{next:t.next}:{},...t.received!==void 0?{received:t.received}:{},status:e},...d?{param:d}:{}};return e===429?new p("provider_rate_limited",a,{...m,retry_after_s:Be(s["retry-after"])}):e===401||e>=500?new p("provider_unavailable",a,m):e===403&&o==="provider_refused"?new p("provider_refused",a,m):e===403&&o==="approval_required"?new p("provider_error",c?"provider asked for approval again":"provider approval question is invalid",{...m,status:e}):new p("provider_error",a,{...m,status:e})}v(D,"mapProviderResponse");function Y(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})}v(Y,"approvalShape");async function Le(e){const r=e.origin!==void 0?K({node:e.origin.node??void 0,run:e.origin.run??void 0,machine:e.origin.machine}):K({node:e.node_id??void 0,run:e.run_id??void 0,profile:e.node_id?(await De(e.node_id))?.profile_id??void 0:void 0,machine:Ne()!==null?Te():void 0});return r===""?{}:{"Crtr-Caller":r}}v(Le,"crtrCallerHeader");async function vr(e,r,s){const c=ae(r.provider);if(c===null)throw new p("not_found",`provider "${r.provider}" is not available`,{param:"provider"});const t={...r,provider:c},o=new AbortController,a=v(()=>o.abort(s.signal.reason),"abort");s.signal.aborted?a():s.signal.addEventListener("abort",a,{once:!0});let d=s.requestId;const A=he({request_id:d,run_id:e.run_id,node_id:e.node_id,controller:o});let m,h,j,u,_,g,w,P="allowed";try{try{await ue(t.provider)}catch{throw pe(t.provider)}let i=await U(t.provider,t.version),k=!1,n,b,q,L=!1,J=!1,F,H=0;for(;;){const y=Ue(i,t.tool);if(j=`${i.id}:${y.group}`,oe(e,i.id,y.group),i.transport.kind!=="machine"&&t.machine!==void 0)throw new p("invalid_argument",`provider "${i.id}" is not served by a device, so it takes no machine`,{param:"machine"});i.transport.kind==="machine"&&F===void 0&&(F=await je(t.machine,void 0,{newest:V(i.major,t.tool)}),H=Date.now()+(i.commands.timeouts?.requestMs??qe));let M=null;if(t.approval&&!L){const l=await fe(e,t,Y(i,y));q=l.option,M=l.prepared,w=t.approval,d=l.requestId,L=!0}const C={params:y.params.filter(l=>"type"in l&&l.type==="file").map(l=>l.name),output:y.output.find(l=>l.type==="file")?.name??null},x=JSON.stringify(C);if(b!==void 0&&b!==x)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"}});b=x,M&&(n=await G(e,C,t.arguments,M,{signal:o.signal})),n??=await ce(e,C,t.arguments,{signal:o.signal});const{sub:O,grantee:R}=await Oe(e);if(i.transport.kind==="machine"){const l=await Je(e,i,t,y,n,{sub:O,requestId:d,deadlineAt:H,signal:o.signal,machine:F});return g=l.responder,m=l.cleanup,{status:200,body:l.result}}const E=await ve({audience:i.transport.endpoint,sub:O,grantee:R,initiating_app:R,scope:Ie(i,y)},{signal:o.signal});u=E.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:y.params,rest:y.rest,output:y.output,commandPath:[i.id,...t.tool.split(" ")]},ee=we(Z,n.arguments,void 0,{authorization:`Bearer ${E.access_token}`,requestId:d,fileMode:"pass-through",extraHeaders:{...e.run_id?{"Crtr-Run-Id":e.run_id}:{},...await Le(e),...q?{"Crtr-Approval":q}:{},...n.headers}});h??=Date.now()+(i.commands.timeouts?.requestMs??3e4);let f;try{if(f=await be(ee,{connectMs:i.commands.timeouts?.connectMs,requestMs:i.commands.timeouts?.requestMs,deadlineAt:h,signal:o.signal}),o.signal.aborted)throw o.signal.reason??new Error("provider call aborted")}catch(l){throw o.signal.aborted?o.signal.reason??l:l instanceof ye?new p("provider_unavailable",l.message,{origin:"provider",details:{status:null,code:l.kind}}):l}if(_??=await Me({provider:i.id,tool:t.tool,request_id:d,run_id:e.run_id,sub:O,grantee:R,payer:E.payer,ledger_grantee:e.class==="person"?ne:R,jti:E.jti,status:f.status,headers:f.headers}),g=f.headers["crtr-responder"],f.status===401&&ge(E.key),f.status>=200&&f.status<300){const l=f.body;if(!l||typeof l!="object"||Array.isArray(l))throw new p("provider_bad_response","provider result is not a JSON object",{origin:"provider",details:{status:f.status,message:"provider result is not a JSON object"}});let N;try{N=await z(e,n,l,{signal:o.signal})}catch($){throw $ instanceof p&&$.code==="provider_error"?new p("provider_bad_response",$.message,{origin:"provider",details:{status:f.status,message:$.message}}):$}m=N.cleanup;const T=I(y.output,N.result);if(T)throw new p("provider_bad_response",`provider result has an invalid output field "${T.field}"`,{origin:"provider",param:T.field,details:{status:f.status,...T}});if(o.signal.aborted)throw o.signal.reason??new Error("provider call aborted");return{status:200,body:N.result}}const S=B(f.body);if(f.status===403&&S.code==="approval_required"&&!q&&!J){J=!0;const l=await me(e,t,d,S.question,o.signal,Y(i,y),n);if(w=l.questionId,q=l.option,n=await G(e,C,t.arguments,n,{signal:o.signal}),h=void 0,o.signal.aborted)throw new p("user_denied","the approval wait was canceled");continue}if(f.status===403&&S.code==="approval_required"&&q)throw D(f.status,f.body,f.headers,!0);if(f.status!==401&&S.manifest_stale===!0&&!k){k=!0,await le(i.id,{conditional:!1,...t.version===void 0?{}:{version:t.version}}),i=await U(i.id,t.version);continue}throw D(f.status,f.body,f.headers,!!q)}}catch(i){throw i instanceof p&&i.question_id&&(w=i.question_id),g===void 0&&i instanceof p&&typeof i.responder=="string"&&(g=i.responder),P=`refused:${i instanceof p?i.code:"server_error"}`,await m?.(),i}finally{try{await Ae({request_id:d,caller:e,provider:t.provider,tool:t.tool,scope_checked:j,outcome:P,jti:u,receipt_id:_,responder:g,question_id:w})}finally{A()}s.signal.removeEventListener("abort",a)}}v(vr,"callProvider");async function Je(e,r,s,c,t,o){const{machine:a}=o,d=await Fe(e,o.sub),A={machine:a,envelope:d,signal:o.signal,deadlineAt:o.deadlineAt,mapFailure:D};if(V(r.major,s.tool)){const n=await $e(A,t.arguments,r.major),b=I(c.output,n);if(b)throw new p("provider_bad_response",`the libraries result has an invalid output field "${b.field}"`,{origin:"provider",param:b.field,details:{status:200,...b}});return{result:n,responder:void 0,cleanup:void 0}}let m=t.arguments,h=null,j="";if(Ee(r.major,s.tool,t.arguments)){const n=await Ce(A,t.arguments,r.major);if(o.deadlineAt-Date.now()<=0)throw Q(a,"timeout","");n!==null&&(m=n.arguments,h=n.services,j=n.release)}const u=await ke(a,{tool:s.tool,arguments:m,version:r.major,requestId:o.requestId,runId:e.run_id,deadlineAt:o.deadlineAt,upload:t.upload===null?null:{id:t.upload.upload_id,link:t.upload.upload_link}},d,o.signal);if(o.signal.aborted)throw o.signal.reason??new Error("provider call aborted");if("unreachable"in u)throw Q(a,u.unreachable.reason,u.unreachable.message);const _=u.headers["crtr-responder"],g=B(u.body);if(u.status<200||u.status>=300){const n=Pe(a,u.status,typeof g.code=="string"?g.code:void 0,typeof g.message=="string"?g.message:void 0,u.body),b=n===null&&u.status>=500&&typeof g.code=="string"?new p("provider_error",typeof g.message=="string"?g.message:`the device answered HTTP ${u.status}`,{origin:"runtime",status:u.status,details:{code:g.code,status:u.status}}):null;throw Object.assign(n??b??D(u.status,u.body,u.headers),{responder:_})}const w=u.body&&typeof u.body=="object"&&!Array.isArray(u.body)?u.body.result:void 0;if(!w||typeof w!="object"||Array.isArray(w))throw Object.assign(new p("provider_bad_response","the device result is not a JSON object",{origin:"provider",details:{status:u.status,message:"the device result is not a JSON object"}}),{responder:_});let P=w;if(h!==null){const n=Re(P,h);if(Se(a,d,j,n.calls),"error"in n)throw Object.assign(n.error,{responder:_});P=n.result}let i;try{i=await z(e,t,P,{signal:o.signal})}catch(n){throw n instanceof p&&n.code==="provider_error"?Object.assign(new p("provider_bad_response",n.message,{origin:"provider",details:{status:u.status,message:n.message}}),{responder:_}):n}const k=I(c.output,i.result);if(k)throw await i.cleanup?.(),Object.assign(new p("provider_bad_response",`the device result has an invalid output field "${k.field}"`,{origin:"provider",param:k.field,details:{status:u.status,...k}}),{responder:_});return{result:i.result,responder:_,cleanup:i.cleanup}}v(Je,"callDevice");export{vr as callProvider,Le as crtrCallerHeader,D as mapProviderResponse,Me as onProviderResponse,W as providerLeaves,mr as providerTools};
|
|
1
|
+
var te=Object.defineProperty;var v=(e,r)=>te(e,"name",{value:r,configurable:!0});import{validateDeclaredResult as U}from"../../api/command-manifest/result.js";import{callerAllows as oe,requireGroup as ie}from"../../daemon/api/handlers/providers.js";import{terminalGrant as ne}from"../grants/mirror.js";import{UNCAPPED_GRANTEE as se}from"../usage/ledger.js";import{listInstalledPlugins as ae}from"../resolver.js";import{ProviderError as l}from"./errors.js";import{resolveProviderRef as de}from"./roots.js";import{providerToolPath as ue}from"../plugin-objects-manifest.js";import{ensureProviderBundle as le,refreshProviderBundle as pe,resolveProviderBundle as B,unavailableProviderError as ce}from"./bundles.js";import{prepareFileCall as fe,resignForRetry as z,resolveFileResult as Q}from"./files.js";import{consumeApproval as me,requireProviderApproval as ve}from"./approval.js";import{outboundToken as ge}from"./mint.js";import{discardFile as he}from"./token-cache.js";import{registerProviderCall as we}from"./inflight.js";import{buildRequestPlan as be}from"../command-plugins/transport/http-plan.js";import{newTraceId as ye}from"../../shared/traceparent.js";import{sendProviderRequest as _e,ProviderTransportError as Ae}from"./request.js";import{acceptProviderReceipt as je}from"./receipts.js";import{writeProviderAudit as ke}from"./audit.js";import{selectDevice as Pe,sendDeviceCall as qe,deviceFailure as Ee,deviceUnreachable as V,DEFAULT_DEVICE_REQUEST_MS as $e}from"./device.js";import{isLibsSearch as K,wantsBundling as Ce,callLibsSearch as Re,bundleExec as Ie,mapLibOutcome as Se,reportLibCalls as Te}from"./device-libs.js";import{directoryIssuer as Ne,ownMachineId as De}from"../peer/directory.js";import{formatCrtrCaller as W}from"../command-plugins/caller-header.js";import{getNode as Fe}from"../canvas/canvas.js";import{envelopeForProviderCaller as Me}from"../peer/envelope.js";function X(e){const r=[],n=v((c,t,o)=>{const s=[...t,c.name],a=o||c.tier==="hidden";if(c.kind==="branch"){for(const g of c.children)n(g,s,a);return}const j=ue(e.roots.length,s);r.push({tool:j,leaf:c,hidden:a})},"walk");for(const c of e.roots)n(c,[],!1);return r}v(X,"providerLeaves");function Y(e){if(e.kind!=="branch")return null;const r=[];for(const h of e.children)if(h.kind==="branch"){const _=Y(h);_&&r.push(_)}else if("group"in h&&h.group){const{kind:_,name:d,description:A,whenToUse:w,tier:b,summary:k,params:P,output:i,effects:p,group:m}=h;r.push({kind:_,name:d,description:A,whenToUse:w,...b===void 0?{}:{tier:b},summary:k,params:P,output:i,effects:p,group:m})}if(r.length===0)return null;const{kind:n,name:c,description:t,whenToUse:o,tier:s,summary:a,model:j,rootEntry:g}=e;return{kind:n,name:c,description:t,whenToUse:o,...s===void 0?{}:{tier:s},summary:a,...j===void 0?{}:{model:j},...g===void 0?{}:{rootEntry:g},children:r}}v(Y,"providerHelpBranch");async function hr(e,r){const n=await B(r),c=X(n.commands),t=ae("user").find(o=>o.name===r)?.manifest.description??r;return{provider:r,status:"available",major:n.major,display_name:t,transport:n.transport.kind,groups:n.provider.groups.filter(o=>oe(e,`${r}:${o.group}`)).map(o=>({group:o.group,leaves:c.filter(({leaf:s,hidden:a})=>!a&&s.group===o.group).map(({tool:s,leaf:a})=>({tool:s,description:a.description,summary:a.summary,params:a.params,output:a.output}))})),commands:n.commands.roots.flatMap(o=>{const s=Y(o);return s?[s]:[]})}}v(hr,"providerTools");async function Oe(e){return await je(e.headers["crtr-receipt"],e)}v(Oe,"onProviderResponse");async function Ue(e){if(e.class==="person"){const r=(await ne())?.sub;if(!r)throw new l("grant_revoked","This machine has no active Northlight grant for your terminal. Run `crtr sys join` again from /account/machines.");return{sub:r,grantee:r}}if(!e.sub||!e.grantee)throw new l("grant_revoked","the caller has no active grant");return{sub:e.sub,grantee:e.grantee}}v(Ue,"identity");function Be(e,r){return[`${e.id}:${r.group}`]}v(Be,"callScope");function Le(e,r){const n=X(e.commands).find(c=>c.tool===r);if(!n)throw new l("not_found",`tool "${r}" is not in provider "${e.id}"`,{param:"tool"});if(!n.leaf.group)throw new l("not_found",`tool "${r}" has no group`,{param:"tool"});return n.leaf}v(Le,"leafFor");function L(e){if(!e||typeof e!="object"||Array.isArray(e))return{};const r=e.error;return!r||typeof r!="object"||Array.isArray(r)?{}:r}v(L,"errorBody");function Je(e){const r=Array.isArray(e)?e[0]:e;if(!r)return;const n=Number(r);if(Number.isFinite(n)&&n>=0)return n;const c=Date.parse(r);return Number.isFinite(c)?Math.max(0,Math.ceil((c-Date.now())/1e3)):void 0}v(Je,"retryAfter");function N(e,r,n,c=!1){const t=L(r),o=typeof t.code=="string"?t.code:void 0,s=typeof t.message=="string"?t.message:`provider answered HTTP ${e}`,a=typeof t.field=="string"?t.field:void 0,g={origin:"provider",details:{code:o??null,message:s,...a?{field:a}:{},...typeof t.next=="string"?{next:t.next}:{},...t.received!==void 0?{received:t.received}:{},status:e},...a?{param:a}:{}};return e===429?new l("provider_rate_limited",s,{...g,retry_after_s:Je(n["retry-after"])}):e===401||e>=500?new l("provider_unavailable",s,g):e===403&&o==="provider_refused"?new l("provider_refused",s,g):e===403&&o==="approval_required"?new l("provider_error",c?"provider asked for approval again":"provider approval question is invalid",{...g,status:e}):new l("provider_error",s,{...g,status:e})}v(N,"mapProviderResponse");function Z(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})}v(Z,"approvalShape");async function He(e){const r=e.origin!==void 0?W({node:e.origin.node??void 0,run:e.origin.run??void 0,machine:e.origin.machine}):W({node:e.node_id??void 0,run:e.run_id??void 0,profile:e.node_id?(await Fe(e.node_id))?.profile_id??void 0:void 0,machine:Ne()!==null?De():void 0});return r===""?{}:{"Crtr-Caller":r}}v(He,"crtrCallerHeader");async function wr(e,r,n){const c=de(r.provider);if(c===null)throw new l("not_found",`provider "${r.provider}" is not available`,{param:"provider"});const t={...r,provider:c},o=new AbortController,s=v(()=>o.abort(n.signal.reason),"abort");n.signal.aborted?s():n.signal.addEventListener("abort",s,{once:!0});let a=n.requestId;const j=n.traceId??ye(),g=we({request_id:a,run_id:e.run_id,node_id:e.node_id,controller:o});let h,_,d,A,w,b,k,P="allowed";try{try{await le(t.provider)}catch{throw ce(t.provider)}let i=await B(t.provider,t.version),p=!1,m,D,q,J=!1,H=!1,F,x=0;for(;;){const y=Le(i,t.tool);if(d=`${i.id}:${y.group}`,ie(e,i.id,y.group),i.transport.kind!=="machine"&&t.machine!==void 0)throw new l("invalid_argument",`provider "${i.id}" is not served by a device, so it takes no machine`,{param:"machine"});i.transport.kind==="machine"&&F===void 0&&(F=await Pe(t.machine,void 0,{newest:K(i.major,t.tool)}),x=Date.now()+(i.commands.timeouts?.requestMs??$e));let M=null;if(t.approval&&!J){const u=await me(e,t,Z(i,y));q=u.option,M=u.prepared,k=t.approval,a=u.requestId,J=!0}const C={params:y.params.filter(u=>"type"in u&&u.type==="file").map(u=>u.name),output:y.output.find(u=>u.type==="file")?.name??null},G=JSON.stringify(C);if(D!==void 0&&D!==G)throw new l("provider_bad_response","the refreshed manifest changed the tool file contract",{origin:"provider",details:{status:409,message:"manifest_stale changed the tool file contract"}});D=G,M&&(m=await z(e,C,t.arguments,M,{signal:o.signal})),m??=await fe(e,C,t.arguments,{signal:o.signal});const{sub:O,grantee:R}=await Ue(e);if(i.transport.kind==="machine"){const u=await xe(e,i,t,y,m,{sub:O,requestId:a,deadlineAt:x,signal:o.signal,machine:F});return b=u.responder,h=u.cleanup,{status:200,body:u.result}}const E=await ge({audience:i.transport.endpoint,sub:O,grantee:R,initiating_app:R,scope:Be(i,y)},{signal:o.signal});A=E.jti;const ee={registration:{name:i.id,endpoint:i.transport.endpoint},...i.commands.baseUrl?{baseUrl:i.commands.baseUrl}:{},...i.commands.timeouts?{timeouts:i.commands.timeouts}:{},params:y.params,rest:y.rest,output:y.output,commandPath:[i.id,...t.tool.split(" ")]},re=be(ee,m.arguments,void 0,{authorization:`Bearer ${E.access_token}`,requestId:a,traceId:j,fileMode:"pass-through",extraHeaders:{...e.run_id?{"Crtr-Run-Id":e.run_id}:{},...await He(e),...q?{"Crtr-Approval":q}:{},...m.headers}});_??=Date.now()+(i.commands.timeouts?.requestMs??3e4);let f;try{if(f=await _e(re,{connectMs:i.commands.timeouts?.connectMs,requestMs:i.commands.timeouts?.requestMs,deadlineAt:_,signal:o.signal}),o.signal.aborted)throw o.signal.reason??new Error("provider call aborted")}catch(u){throw o.signal.aborted?o.signal.reason??u:u instanceof Ae?new l("provider_unavailable",u.message,{origin:"provider",details:{status:null,code:u.kind}}):u}if(w??=await Oe({provider:i.id,tool:t.tool,request_id:a,run_id:e.run_id,sub:O,grantee:R,payer:E.payer,ledger_grantee:e.class==="person"?se:R,jti:E.jti,status:f.status,headers:f.headers}),b=f.headers["crtr-responder"],f.status===401&&he(E.key),f.status>=200&&f.status<300){const u=f.body;if(!u||typeof u!="object"||Array.isArray(u))throw new l("provider_bad_response","provider result is not a JSON object",{origin:"provider",details:{status:f.status,message:"provider result is not a JSON object"}});let S;try{S=await Q(e,m,u,{signal:o.signal})}catch($){throw $ instanceof l&&$.code==="provider_error"?new l("provider_bad_response",$.message,{origin:"provider",details:{status:f.status,message:$.message}}):$}h=S.cleanup;const T=U(y.output,S.result);if(T)throw new l("provider_bad_response",`provider result has an invalid output field "${T.field}"`,{origin:"provider",param:T.field,details:{status:f.status,...T}});if(o.signal.aborted)throw o.signal.reason??new Error("provider call aborted");return{status:200,body:S.result}}const I=L(f.body);if(f.status===403&&I.code==="approval_required"&&!q&&!H){H=!0;const u=await ve(e,t,a,I.question,o.signal,Z(i,y),m);if(k=u.questionId,q=u.option,m=await z(e,C,t.arguments,m,{signal:o.signal}),_=void 0,o.signal.aborted)throw new l("user_denied","the approval wait was canceled");continue}if(f.status===403&&I.code==="approval_required"&&q)throw N(f.status,f.body,f.headers,!0);if(f.status!==401&&I.manifest_stale===!0&&!p){p=!0,await pe(i.id,{conditional:!1,...t.version===void 0?{}:{version:t.version}}),i=await B(i.id,t.version);continue}throw N(f.status,f.body,f.headers,!!q)}}catch(i){throw i instanceof l&&i.question_id&&(k=i.question_id),b===void 0&&i instanceof l&&typeof i.responder=="string"&&(b=i.responder),P=`refused:${i instanceof l?i.code:"server_error"}`,await h?.(),i}finally{try{await ke({request_id:a,caller:e,provider:t.provider,tool:t.tool,scope_checked:d,outcome:P,jti:A,receipt_id:w,responder:b,question_id:k})}finally{g()}n.signal.removeEventListener("abort",s)}}v(wr,"callProvider");async function xe(e,r,n,c,t,o){const{machine:s}=o,a=await Me(e,o.sub),j={machine:s,envelope:a,signal:o.signal,deadlineAt:o.deadlineAt,mapFailure:N};if(K(r.major,n.tool)){const p=await Re(j,t.arguments,r.major),m=U(c.output,p);if(m)throw new l("provider_bad_response",`the libraries result has an invalid output field "${m.field}"`,{origin:"provider",param:m.field,details:{status:200,...m}});return{result:p,responder:void 0,cleanup:void 0}}let g=t.arguments,h=null,_="";if(Ce(r.major,n.tool,t.arguments)){const p=await Ie(j,t.arguments,r.major);if(o.deadlineAt-Date.now()<=0)throw V(s,"timeout","");p!==null&&(g=p.arguments,h=p.services,_=p.release)}const d=await qe(s,{tool:n.tool,arguments:g,version:r.major,requestId:o.requestId,runId:e.run_id,deadlineAt:o.deadlineAt,upload:t.upload===null?null:{id:t.upload.upload_id,link:t.upload.upload_link}},a,o.signal);if(o.signal.aborted)throw o.signal.reason??new Error("provider call aborted");if("unreachable"in d)throw V(s,d.unreachable.reason,d.unreachable.message);const A=d.headers["crtr-responder"],w=L(d.body);if(d.status<200||d.status>=300){const p=Ee(s,d.status,typeof w.code=="string"?w.code:void 0,typeof w.message=="string"?w.message:void 0,d.body),m=p===null&&d.status>=500&&typeof w.code=="string"?new l("provider_error",typeof w.message=="string"?w.message:`the device answered HTTP ${d.status}`,{origin:"runtime",status:d.status,details:{code:w.code,status:d.status}}):null;throw Object.assign(p??m??N(d.status,d.body,d.headers),{responder:A})}const b=d.body&&typeof d.body=="object"&&!Array.isArray(d.body)?d.body.result:void 0;if(!b||typeof b!="object"||Array.isArray(b))throw Object.assign(new l("provider_bad_response","the device result is not a JSON object",{origin:"provider",details:{status:d.status,message:"the device result is not a JSON object"}}),{responder:A});let k=b;if(h!==null){const p=Se(k,h);if(Te(s,a,_,p.calls),"error"in p)throw Object.assign(p.error,{responder:A});k=p.result}let P;try{P=await Q(e,t,k,{signal:o.signal})}catch(p){throw p instanceof l&&p.code==="provider_error"?Object.assign(new l("provider_bad_response",p.message,{origin:"provider",details:{status:d.status,message:p.message}}),{responder:A}):p}const i=U(c.output,P.result);if(i)throw await P.cleanup?.(),Object.assign(new l("provider_bad_response",`the device result has an invalid output field "${i.field}"`,{origin:"provider",param:i.field,details:{status:d.status,...i}}),{responder:A});return{result:P.result,responder:A,cleanup:P.cleanup}}v(xe,"callDevice");export{wr as callProvider,He as crtrCallerHeader,N as mapProviderResponse,Oe as onProviderResponse,X as providerLeaves,hr as providerTools};
|
|
@@ -12,7 +12,7 @@ export interface RunStreamOptions {
|
|
|
12
12
|
idleCheckMs?: number;
|
|
13
13
|
isRunning?: (runId: string) => Promise<boolean>;
|
|
14
14
|
}
|
|
15
|
-
export declare function streamRunEvents(req: IncomingMessage, res: ServerResponse, id: string, principal: AppPrincipal | RequestPrincipal, after: number, requestId: string, framing: 'sse' | 'ndjson', options?: RunStreamOptions): Promise<void>;
|
|
15
|
+
export declare function streamRunEvents(req: IncomingMessage, res: ServerResponse, id: string, principal: AppPrincipal | RequestPrincipal, after: number, requestId: string, framing: 'sse' | 'ndjson', options?: RunStreamOptions, traceId?: string): Promise<void>;
|
|
16
16
|
export declare function createAppListener(config: NonNullable<ScopeConfig['runtime']>, grantsReady: () => Promise<boolean>, syncGrants: GrantSync, listenerOptions?: {
|
|
17
17
|
deferHead?: DeferHeadOptions;
|
|
18
18
|
stream?: RunStreamOptions;
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
var
|
|
2
|
-
data: ${JSON.stringify(
|
|
1
|
+
var Y=Object.defineProperty;var i=(e,r)=>Y(e,"name",{value:r,configurable:!0});import{randomUUID as Q}from"node:crypto";import{createServer as W}from"node:http";import{Router as V}from"./router.js";import{toErrorBody as q}from"./map.js";import{objectRoutes as K}from"./handlers/objects.js";import{customObjectRoutes as Z}from"./handlers/custom-objects.js";import{docRoutes as ee}from"./handlers/docs.js";import{OPERATIONS as te}from"./operations.js";import{runtimeReadiness as ne}from"../readiness.js";import{errorCodes as $}from"../../api/error-codes.js";import{MINIMUM_RUNTIME_VERSION_HEADER as re,RUNTIME_VERSION_HEADER as ae,minimumRuntimeRefusal as oe}from"../../api/runtime-version.js";import{CONTRACT_HEADER as ie,CONTRACT_LEVEL as X,REQUIRED_CONTRACT_HEADER as se,contractRefusal as ue}from"../../api/contract.js";import{crtrVersion as B}from"../../shared/crtr-version.js";import{newTraceId as ce,parseTraceparent as de}from"../../shared/traceparent.js";import{AppPrincipalResolver as le}from"./principal.js";import{isDaemonRoute as me,runtimeOwner as fe,servePeer as z}from"../peer/ingress.js";import{applySyncNotice as pe}from"./sync-notice.js";import{ownMachineId as he}from"../../core/peer/directory.js";import{handleRunOperation as we,integer as ye}from"./handlers/runs.js";import{llmCallerOf as _e,parseLlmRequest as ve,runLlm as ge}from"../../core/llm/run.js";import{parseUsageMonth as Re,usageFor as Oe}from"../../core/usage/ledger.js";import{capFields as Ee}from"./handlers/llm.js";import{CONTROL_BODY_MAX_BYTES as be,MODEL_CONTEXT_BODY_MAX_BYTES as Te}from"../../core/llm/body-limits.js";import{RunError as m,runIsRunning as Se}from"../../core/runs/operations.js";import{requireRun as J}from"./router.js";import{deferHead as Ae}from"./deferred-head.js";import{readRunEvents as G,registerGrantStream as Ne,subscribeRunEvents as Ce}from"../../core/runs/events.js";import{activeGrant as F,payerOfGrant as Ie}from"../../core/grants/mirror.js";async function Pe(e){if(await Ie(e)==="app")throw new m(401,"grant_revoked","this app-paid grant is no longer active");return null}i(Pe,"appPaidGone");import{releaseGrantWaiters as ke}from"../../core/grants/watch.js";import{emitEvent as He}from"../../core/events/emit.js";import{runNotices as Me}from"../run-notices.js";import{handleVendorSignIn as De,isVendorSignInPath as je}from"./vendor-signin.js";import{listAppProfiles as xe}from"./handlers/app-profiles.js";import{handleTransferDownload as Be,handleTransferOperation as Je,TRANSFER_OPERATIONS as Le}from"./handlers/transfer.js";import{handleQuestionOperation as Ue,QUESTION_OPERATIONS as Ve}from"./handlers/questions.js";import{removeVendorAccount as $e,vendorStatus as Xe}from"./handlers/vendor-status.js";import{handleMemoryOperation as ze,MEMORY_OPERATIONS as Ge}from"./handlers/memory.js";import{abortOnClose as Fe,callerFromApp as Ye,handleProviderOperation as Qe,PROVIDER_OPERATIONS as We,providerFailure as qe}from"./handlers/providers.js";function Ke(e){return Object.prototype.hasOwnProperty.call($,e)?$[e]:void 0}i(Ke,"fixedRow");function Ze(e){const r=Ke(e);return r?r.type:e==="stream_gap"||e==="provider_error"||e==="runtime_version_unsupported"?"invalid_request_error":"server_error"}i(Ze,"errorType");function et(e){if(!e||typeof e!="object"||Array.isArray(e)||!("rate_limit"in e))return e;const{rate_limit:r,...o}=e;return o}i(et,"appDetails");function x(e,r,o){const p=et(e.details);return{error:{code:e.code,message:e.message,...p===void 0?{}:{details:p},type:Ze(e.code),...e.param?{param:e.param}:{},origin:"runtime",retryable:e.retryable||e.code==="stream_dropped",request_id:r,...o?{run_id:o}:{},...e.scope?{scope:e.scope}:{},...e.code==="unauthorized"?{user_action:"reconnect"}:{},...Ee(e),...e.code==="all_routes_exhausted"?{retry_after_s:e.details.retry_after_s}:{}}}}i(x,"envelope");function tt(e,r,o,p,f){const l=r instanceof m?r:new m(500,"server_failure","runtime request failed");if(r instanceof m||console.error("[app-listener]",`trace_id=${p}`,r),e.headersSent&&!String(e.getHeader("Content-Type")??"").startsWith("text/event-stream")){e.destroy();return}if(e.headersSent){e.end(`event: error
|
|
2
|
+
data: ${JSON.stringify(x(l,o,f))}
|
|
3
3
|
|
|
4
|
-
`);return}e.writeHead(
|
|
4
|
+
`);return}e.writeHead(l.status,{"Content-Type":"application/json; charset=utf-8","Request-Id":o,"X-Content-Type-Options":"nosniff",...l.code==="all_routes_exhausted"?{"Retry-After":String(l.details.retry_after_s)}:{}}),e.end(JSON.stringify(x(l,o,f)))}i(tt,"fail");function L(e){return Array.isArray(e)?e[0]:e}i(L,"headerValue");function g(e,r,o){e.writeHead(r,{"Content-Type":"application/json; charset=utf-8"}),e.end(JSON.stringify(o))}i(g,"json");async function C(e,r=be){const o=[];let p=0;for await(const f of e){if(p+=f.length,p>r)throw new m(400,"invalid_request","request body too large");o.push(f)}if(p)try{return JSON.parse(Buffer.concat(o).toString("utf8"))}catch{throw new m(400,"invalid_request","invalid JSON body")}}i(C,"bodyOf");function nt(e,r){const o=e.headers["last-event-id"]??r.get("after");if(Array.isArray(o))throw new m(400,"invalid_request","invalid cursor","after");return ye(o??null,"after",0,Number.MAX_SAFE_INTEGER,0)}i(nt,"cursor");function rt(e){return`id: ${e.sequence_number}
|
|
5
5
|
event: ${e.type}
|
|
6
6
|
data: ${JSON.stringify(e)}
|
|
7
7
|
|
|
8
|
-
`}i(
|
|
8
|
+
`}i(rt,"frame");const at=6e4;async function ot(e,r,o,p,f,l,b,I={},H){await J(p,o);let w=await G(o,f);if(f>w.latest_sequence)throw new m(400,"invalid_request","cursor is beyond the run","after");if(f<w.latest_sequence&&(w.earliest_sequence===null||f<w.earliest_sequence-1))throw new m(409,"stream_gap","events have expired",void 0,{earliest_sequence:w.earliest_sequence,latest_sequence:w.latest_sequence});if(r.destroyed||e.destroyed)return r.destroy(),Promise.resolve();r.writeHead(200,{"Content-Type":b==="sse"?"text/event-stream":"application/x-ndjson","Cache-Control":"no-cache","X-Accel-Buffering":"no"}),b==="sse"?r.write(`: connected
|
|
9
9
|
|
|
10
|
-
`):r.flushHeaders();let
|
|
11
|
-
data: ${JSON.stringify(
|
|
10
|
+
`):r.flushHeaders();let S=f;const n=await Me().streamOpened(o);let t=!1,y=!1,O=!1,T;const P=new Promise(u=>{T=u}),_=i(()=>{t||(t=!0,n(S).catch(u=>console.error("[app-listener]",...H===void 0?[]:[`trace_id=${H}`],u)),clearInterval(c),clearInterval(N),k(),D(),r.off("close",_),r.off("drain",h),r.end(),T())},"end"),h=i(()=>{O=!1,a()},"onDrain"),R=i(u=>{t||r.write(b==="sse"?`event: error
|
|
11
|
+
data: ${JSON.stringify(x(u,l,o))}
|
|
12
12
|
|
|
13
|
-
`:`${JSON.stringify({type:"error",...
|
|
14
|
-
`),_()},"error"),a=i(async()=>{if(!(
|
|
15
|
-
`),
|
|
13
|
+
`:`${JSON.stringify({type:"error",...x(u,l,o)})}
|
|
14
|
+
`),_()},"error"),a=i(async()=>{if(!(t||y)){y=!0;try{if(!await A()){M();return}if(await J(p,o),w=await G(o,S),w.backlog_count>1e3||w.backlog_bytes>32*1024*1024){R(new m(503,"stream_dropped","stream reader fell behind",void 0,{last_sequence:S}));return}for(const u of w.events){if(O)break;O=!r.write(b==="sse"?rt(u):`${JSON.stringify(u)}
|
|
15
|
+
`),S=u.sequence_number}}catch(u){R(u instanceof m?u:new m(500,"server_failure","stream failed"))}finally{y=!1}}},"flush"),M=i(()=>R(new m(401,"grant_revoked","grant is not active")),"revoked"),A=i(async()=>"class"in p||await F(p.grant)!==null,"grantActive"),k=await Ce(o,a),D="class"in p?()=>{}:Ne(p.grant,M),c=setInterval(()=>{!t&&!O&&b==="sse"&&r.write(`: keepalive
|
|
16
16
|
|
|
17
|
-
`)},15e3),
|
|
17
|
+
`)},15e3),j=I.idleMs??at,s=I.isRunning??Se;let v=Date.now(),d=S;const E=i(async()=>{if(t||b!=="sse")return;const u=Date.now();let U=!0;try{U=await s(o)}catch{}if(!t){if(U||S!==d){v=u,d=S;return}u-v<j||O||(r.write(`event: idle
|
|
18
18
|
data: {}
|
|
19
19
|
|
|
20
|
-
`),_())}},"checkIdle"),
|
|
20
|
+
`),_())}},"checkIdle"),N=setInterval(()=>{E().catch(u=>He({level:"error",event:"api.run_stream.idle_check_failed",error:u}))},I.idleCheckMs??1e3);return N.unref(),r.on("drain",h),r.on("close",_),await a(),P}i(ot,"streamRunEvents");async function zt(e,r,o,p={}){const f=e.listener;if(!e.issuer||!e.audience||!f?.address||!Number.isInteger(f.port)||f.port<1||f.port>65535)throw new Error("runtime issuer, audience and listener address/port are required");if(["0.0.0.0","::","[::]"].includes(f.address))throw new Error("runtime listener must bind a specific address");const l=new le(e.issuer,e.audience,o,e.jwksUrl),b=new V,I=new Map;for(const n of te.filter(t=>t.stage!==void 0)){const t=i(()=>({status:500}),"handler");b.register({method:n.method,pattern:n.path,handler:t}),I.set(t,n)}const H=new V().registerAll(K).registerAll(Z).registerAll(ee),w=W((n,t)=>{(async()=>{Ae(n,t,p.deferHead);const y=Q();t.setHeader("Request-Id",y);const O=de(L(n.headers.traceparent))??ce();t.setHeader(ae,B()),t.setHeader(ie,X);let T;try{const P=L(n.headers[re.toLowerCase()]),_=oe(P,B())??ue(L(n.headers[se.toLowerCase()]),X,{legacyMinimum:P,runtimeVersion:B()});if(_!==null)throw _.status===400?new m(400,_.code,_.message,_.param):new m(426,_.code,_.message,void 0,_.details);const h=new URL(n.url??"/","http://runtime.local");if(je(h.pathname)){await De(n,t,h,l);return}const R=b.match(n.method??"GET",h.pathname),a=R?I.get(R.handler):void 0,M=a?.name==="runtime.sync-notice"||a?.name==="runtime.vendor-status"||a?.name==="runtime.vendor-account-remove";let A,k;if(!M&&n.headers.authorization!==void 0)try{A=await l.verifyCall(n.headers.authorization)}catch(s){k=s}const D={issuer:e.issuer,jwks:l.jwks,audience:i(async()=>e.audience,"audience"),owner:fe,self:he,syncGrants:o,ready:i(async()=>l.isReady()&&await r(),"ready")};if(A?.chain[0]?.sub.startsWith("runtime:")){await z(n,t,D,A);return}if(A===void 0&&a===void 0&&await me(n.method??"GET",h.pathname)){await z(n,t,D,{status:401,code:"unauthorized",message:k===void 0?"a machine token is required":"invalid machine token"});return}if(a?.name==="ready"){const s=await r(),v=await ne({jwksReady:l.isReady(),grantsReady:i(()=>s,"grantsReady")});g(t,v.ready?200:503,v);return}if((n.method!=="GET"||h.pathname!=="/v1/health")&&(!l.isReady()||!await r()))throw new m(503,"runtime_starting","runtime_starting");if(!R||!a)throw new m(404,"not_found","unknown operation");if(T=R.params.run_id,a.name==="health"){g(t,200,{status:"ok",api_version:"2026-09-23"});return}if(a.name==="runtime.sync-notice"){await pe(await l.verifySyncNotice(n.headers.authorization),o,y),t.writeHead(204),t.end();return}if(a.name==="runtime.vendor-status"){await l.verifyDirectoryRead(n.headers.authorization,"vendor_status"),g(t,200,Xe());return}if(a.name==="runtime.vendor-account-remove"){await l.verifyDirectoryRead(n.headers.authorization,"vendor_account_remove"),g(t,200,await $e(await C(n)));return}if(A===void 0)throw k??new m(401,"unauthorized","a directory access token is required");const c=await l.resolveVerified(A,a.scope);if(t.setHeader("Request-Id",y),T&&await J(c,T,["run.message","run.interrupt","run.cancel","run.rename","run.delete"].includes(a.name),a.name==="run.delete"),a.stage==="objects"){const s=H.match(a.method,h.pathname);if(!s)throw new m(404,"not_found","unknown operation");const v={method:a.method,path:h.pathname,params:s.params,query:h.searchParams,body:await C(n,s.maxBodyBytes),ceiling:c.resolved,principal:{class:"app",app:c.grantee,scopes:c.resolved},req:n,res:t};let d;try{d=await s.handler(v)}catch(E){const{status:N,body:u}=q(E);throw N>=500&&console.error("[app-listener]",`trace_id=${O}`,E),new m(N,u.error.code,u.error.message,void 0,u.error.details)}if(t.headersSent)return;if(d.rawBody!==void 0||d.status===204||d.body===void 0){t.writeHead(d.status,d.headers??{}),t.end(d.rawBody);return}t.writeHead(d.status,{"Content-Type":"application/json; charset=utf-8",...d.headers??{}}),t.end(JSON.stringify(d.body));return}if(a.stream==="bytes"){await Be(n,t,c,h.searchParams);return}if(a.stream){await ot(n,t,T,c,nt(n,h.searchParams),y,"sse",p.stream,O);return}if(a.name==="llm"){const s=ve(await C(n,Te)),v=new AbortController,d=i(()=>v.abort(),"abort");t.on("close",d);try{const E=await ge(s,v.signal,s.stream?N=>{t.headersSent||t.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache","X-Accel-Buffering":"no"}),t.write(`data: ${JSON.stringify(N)}
|
|
21
21
|
|
|
22
|
-
`)}:void 0,void 0,{grantee:
|
|
22
|
+
`)}:void 0,void 0,{grantee:c.grantee,spend_cap:c.spend_cap},_e(await F(c.grant)??await Pe(c.grant)));s.stream?(t.headersSent||t.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache","X-Accel-Buffering":"no"}),t.end(`data: [DONE]
|
|
23
23
|
|
|
24
|
-
`)):
|
|
24
|
+
`)):g(t,200,E.completion)}finally{t.off("close",d)}return}if(a.name==="usage.get"){g(t,200,await Oe(c.grantee,c.spend_cap,Re(h.searchParams.get("month"))));return}if(a.name==="profile.list"){g(t,200,xe(c));return}if(Ve.has(a.name)){const s=await Ue(a.name,c,R.params.question_id,a.method==="POST"?await C(n):void 0);g(t,s.status,s.body);return}if(We.has(a.name)){const{signal:s,release:v}=Fe(t);try{const d=await Qe(a.name,Ye(c),{params:R.params,body:a.method==="POST"?await C(n):void 0,query:h.searchParams},{requestId:y,traceId:O,signal:s});g(t,d.status,d.body)}catch(d){const E=qe(d,y,!0);t.writeHead(E.status,{"Content-Type":"application/json; charset=utf-8","X-Content-Type-Options":"nosniff",...E.headers}),t.end(JSON.stringify(E.body))}finally{v()}return}if(Ge.has(a.name)){const s=await ze(a.name,{class:"app",app:c.grantee,scopes:c.resolved},h.searchParams);g(t,s.status,s.body);return}if(Le.has(a.name)){const s=await Je(a,c,R.params,h.searchParams,await C(n));g(t,s.status,s.body);return}const j=await we(a,c,T,h.searchParams,await C(n),n.headers["idempotency-key"]);g(t,j.status,j.body)}catch(P){tt(t,P,y,O,T)}})()});return{ready:new Promise((n,t)=>{w.once("error",t),w.listen(f.port,f.address,()=>{w.off("error",t),n()})}),close:i(async()=>{l.close(),ke(),w.closeAllConnections(),await new Promise((n,t)=>w.close(y=>y?t(y):n()))},"close")}}i(zt,"createAppListener");export{at as RUN_STREAM_IDLE_MS,zt as createAppListener,nt as cursor,ot as streamRunEvents};
|
|
@@ -48,9 +48,11 @@ export declare function parseProvider(value: unknown): string;
|
|
|
48
48
|
export declare function parseCallInput(body: unknown): ProviderCallInput;
|
|
49
49
|
/** Per-request context the call path needs: the audit `request_id` (also the
|
|
50
50
|
* `Crtr-Request-Id` a provider receives) and the signal that aborts the call
|
|
51
|
-
* when the caller's socket closes (R9).
|
|
51
|
+
* when the caller's socket closes (R9). `traceId` is the trace of the inbound HTTP request;
|
|
52
|
+
* without one the call starts a fresh trace. */
|
|
52
53
|
export interface ProviderRequest {
|
|
53
54
|
requestId: string;
|
|
55
|
+
traceId?: string;
|
|
54
56
|
signal: AbortSignal;
|
|
55
57
|
}
|
|
56
58
|
export interface ProviderInput {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The trace id of a version-`00` `traceparent` whose trace id and parent id are lowercase hex and not all zeros, else null. */
|
|
2
|
+
export declare function parseTraceparent(value: string | undefined): string | null;
|
|
3
|
+
/** A fresh nonzero 16-byte trace id as 32 lowercase hex. */
|
|
4
|
+
export declare function newTraceId(): string;
|
|
5
|
+
/** The `traceparent` value for one outbound call: the trace id unchanged, a new 8-byte parent id, flags `00` (nothing sampled). */
|
|
6
|
+
export declare function outboundTraceparent(traceId: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var o=Object.defineProperty;var e=(t,n)=>o(t,"name",{value:n,configurable:!0});import{randomBytes as r}from"node:crypto";const f=/^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/;function a(t){const n=t===void 0?null:f.exec(t);return n===null||/^0+$/.test(n[1])||/^0+$/.test(n[2])?null:n[1]}e(a,"parseTraceparent");function i(){for(;;){const t=r(16).toString("hex");if(!/^0+$/.test(t))return t}}e(i,"newTraceId");function s(t){for(;;){const n=r(8).toString("hex");if(!/^0+$/.test(n))return`00-${t}-${n}-00`}}e(s,"outboundTraceparent");export{i as newTraceId,s as outboundTraceparent,a as parseTraceparent};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Let other apps build on yours
|
|
3
|
+
description: Offer your app as a service other Northlight apps build on — as a provider their runs call, or as your own API — and list it with a docs link so their coding agents find it.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Let other apps build on yours
|
|
7
|
+
|
|
8
|
+
Other developers' apps can build on yours the way they build on Northlight's: by calling your provider tools from a run, or by calling your own API. Set a docs link on your app and it is listed on [Services you can build on](/docs/services), where their coding agents look.
|
|
9
|
+
|
|
10
|
+
Comms is built this way. Another app calls it with its own Northlight app key, and reads [comms.northlight.ai/llms.txt](https://comms.northlight.ai/llms.txt) to learn how:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import {readFile} from 'node:fs/promises';
|
|
14
|
+
import {CommsClient} from '@north-light/comms';
|
|
15
|
+
|
|
16
|
+
const comms = new CommsClient({
|
|
17
|
+
baseUrl: 'https://comms.northlight.ai',
|
|
18
|
+
issuer: 'https://account.northlight.ai',
|
|
19
|
+
clientId: process.env.CROUTER_CLIENT_ID!, // the calling app's own client id and key
|
|
20
|
+
privateKeyJwk: JSON.parse(await readFile(process.env.CROUTER_KEY_FILE!, 'utf8')),
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Two ways to offer it
|
|
25
|
+
|
|
26
|
+
- **As a provider.** Your tools run on the person's runtime for any app the person has granted them, and you can charge per call. This is the same work as [letting the person's AI use your app](/docs/let-their-ai-use-your-app); other apps ask for your groups as scopes named `<your client id>:<group>`.
|
|
27
|
+
- **As your own API.** Serve HTTP from your own host, as Comms does. A calling app identifies itself with its Northlight client id and key, so you know which app is asking.
|
|
28
|
+
|
|
29
|
+
## Get listed
|
|
30
|
+
|
|
31
|
+
1. In the console, open your app's **Profile** tab.
|
|
32
|
+
2. Under **Marketplace**, set **Docs link** to the https URL of your guide for agents and developers, for example `https://yourapp.com/llms.txt`, and choose **Save docs link**.
|
|
33
|
+
3. Write that guide for a coding agent: what your service does, how to install the client, one working snippet, the errors and their fixes. [comms.northlight.ai/llms.txt](https://comms.northlight.ai/llms.txt) is a working example.
|
|
34
|
+
|
|
35
|
+
[Services you can build on](/docs/services) is rebuilt from the directory's public marketplace list each time this site is built, so a new docs link appears with no change to these docs.
|
|
36
|
+
|
|
37
|
+
## Guides
|
|
38
|
+
|
|
39
|
+
| Page | What it covers |
|
|
40
|
+
|---|---|
|
|
41
|
+
| [Let the person's AI use your app](/docs/let-their-ai-use-your-app) | Define tools with `@north-light/plugin`, serve them, add the provider role, set a price |
|
|
42
|
+
| [Services you can build on](/docs/services) | Every listed app with a docs link |
|
|
43
|
+
| [Manage your app](/docs/guides/manage-your-app) | The console's **Profile** and **Provider** tabs |
|
|
44
|
+
| [Go live](/docs/guides/go-live) | Verification and the marketplace listing |
|
package/docs/apps/cli.md
CHANGED
|
@@ -5,7 +5,7 @@ description: The npx @north-light/sdk commands for scaffolding an app, logging i
|
|
|
5
5
|
|
|
6
6
|
# CLI reference
|
|
7
7
|
|
|
8
|
-
`npx @north-light/sdk <command>` runs the package's
|
|
8
|
+
`npx @north-light/sdk <command>` runs the command-line tool of [`@north-light/sdk`](https://www.npmjs.com/package/@north-light/sdk) on npm. (crouter's own CLI, `crtr`, is a different package: [`@north-light/cli`](https://www.npmjs.com/package/@north-light/cli). Tools you offer to agents are built with [`@north-light/plugin`](https://www.npmjs.com/package/@north-light/plugin).) Run it with no command, or with `--help`, to print the list below. `npx @north-light/sdk <command> --help` (or `-h`) prints that command's usage and exits 0. An unknown command, or a bad flag, prints the usage and exits 1.
|
|
9
9
|
|
|
10
10
|
With no dependencies installed, `npx` downloads the newest published release of the SDK (the one tagged latest on npm), not the version your project pins. Run `npm i` in the project first if the pinned version must run; `init` pins the version of the CLI that ran it.
|
|
11
11
|
|
|
@@ -21,7 +21,7 @@ With no dependencies installed, `npx` downloads the newest published release of
|
|
|
21
21
|
| `npx @north-light/sdk register [--name <text>] [--scope <scope>]... [--logo <url>] [--justification <text>] [--redirect-uri <uri>]... [--issuer <url>]` | Run in a project made by `init`, after `login`: registers the app with the public key from `local/app-key.json`, adds each `--scope` beyond `crtr:llm crtr:act`, and writes `CROUTER_CLIENT_ID` and `CROUTER_ISSUER` to `local/app.env`. | No |
|
|
22
22
|
| `npx @north-light/sdk apps <leaf> [--app <client id>] [--issuer <url>] [leaf flags] [--json]` | Call one app-management leaf with the login and print its answer as JSON. Leaves: `mine`, `get`, `update`, `set-logo --file <path>`, `set-brand-color --color <#rrggbb|none>`, `set-redirect-uris`, `asks`, `set-scopes`, `set-requires`, `set-run-notice-url`, `keys list\|add\|retire\|delete\|set-jwks-uri\|remove-jwks-uri`, `testers list\|add\|remove`. `--app` defaults to `CROUTER_CLIENT_ID` in `local/app.env`; `keys add` takes `--key-file <path>` and sends only the public half. Deleting an app, its marketplace listing, monthly spend cap, sharing and provider role are console-only: those leaves exit 1 and name the console page, with no request. | No |
|
|
23
23
|
|
|
24
|
-
`keygen` and `env` are the only commands that can call `crtr`, and only with `--app`. The
|
|
24
|
+
`keygen` and `env` are the only commands that can call `crtr`, and only with `--app`. The rest of this site never uses `--app`, so crtr is optional; to use it, see [Run `crtr cloud apps` yourself](/docs/get-started#run-crtr-cloud-apps-yourself). `login`, `logout`, `whoami`, `register` and `apps` call only the Northlight directory.
|
|
25
25
|
|
|
26
26
|
## Details
|
|
27
27
|
|
|
@@ -63,7 +63,7 @@ Writes the `northlight` Agent Skill that ships in the package, replacing an olde
|
|
|
63
63
|
|
|
64
64
|
### `keygen`
|
|
65
65
|
|
|
66
|
-
Writes a private signing key to `local/app-key.json` with mode `0600`, refuses to overwrite an existing file (pass `--out` for another path), and prints the public JWK on stdout. Register the JWK in the console with **Add key** on the app's **Sign-in** tab, or paste it under **Register an app**. With `--app <client id>`, it offers to add the key to your app through your own `crtr` (`crtr cloud apps keys add`); `--yes` skips the question. This needs a joined crtr
|
|
66
|
+
Writes a private signing key to `local/app-key.json` with mode `0600`, refuses to overwrite an existing file (pass `--out` for another path), and prints the public JWK on stdout. Register the JWK in the console with **Add key** on the app's **Sign-in** tab, or paste it under **Register an app**. With `--app <client id>`, it offers to add the key to your app through your own `crtr` (`crtr cloud apps keys add`); `--yes` skips the question. This needs a joined crtr.
|
|
67
67
|
|
|
68
68
|
### `env`
|
|
69
69
|
|
|
@@ -5,7 +5,7 @@ description: What npx @north-light/sdk init and skill install for Claude Code, C
|
|
|
5
5
|
|
|
6
6
|
# Use your coding agent
|
|
7
7
|
|
|
8
|
-
The `northlight` skill teaches a coding agent the whole path in the [quick setup](/docs/quickstart): the commands, the code patterns and the rules. With it, your agent signs you up, logs in, registers the app and runs it, and you choose **Allow** twice in your browser: once on the login page, once on the app's consent page. `npx @north-light/sdk init` installs it into your project. The skill needs no account and
|
|
8
|
+
The `northlight` skill teaches a coding agent the whole path in the [quick setup](/docs/quickstart): the commands, the code patterns and the rules. With it, your agent signs you up, logs in, registers the app and runs it, and you choose **Allow** twice in your browser: once on the login page, once on the app's consent page. `npx @north-light/sdk init` installs it into your project. The skill needs no account to install, and works whether or not crouter is on your machine.
|
|
9
9
|
|
|
10
10
|
## Install it
|
|
11
11
|
|
|
@@ -25,6 +25,17 @@ npx @north-light/sdk skill
|
|
|
25
25
|
|
|
26
26
|
`skill` always replaces the `northlight` skill it finds. `--print` writes the skill to stdout instead of to files.
|
|
27
27
|
|
|
28
|
+
## Install it once for every project (Claude Code)
|
|
29
|
+
|
|
30
|
+
If you build more than one app, install the Claude Code plugin instead of a copy in each project:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
/plugin marketplace add Northlight-Systems/plugins
|
|
34
|
+
/plugin install northlight
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
It adds the `northlight` skill, one skill for each section of this site, and the commands `/northlight:new-app`, `/northlight:add-permission`, `/northlight:text-people`, `/northlight:become-a-provider` and `/northlight:go-live`. The plugin repository is generated from these docs and the skill at each release, so it matches the current SDK version.
|
|
38
|
+
|
|
28
39
|
## Where it goes
|
|
29
40
|
|
|
30
41
|
| Agent | In the project (default) | With `--global` |
|
|
@@ -36,7 +47,7 @@ npx @north-light/sdk skill
|
|
|
36
47
|
|
|
37
48
|
The project copy is the default so it is committed with the project, and teammates get it from git. It matches the SDK version your project pins.
|
|
38
49
|
|
|
39
|
-
|
|
50
|
+
In a directory that already has a `package.json`, `init` creates a `.gitignore` with `local/`, or adds `local/` to the one you have, so the key stays out of git.
|
|
40
51
|
|
|
41
52
|
## Without a project: the hosted skill
|
|
42
53
|
|
|
@@ -50,7 +61,7 @@ If the agent's shell call times out or is interrupted while you approve, it runs
|
|
|
50
61
|
|
|
51
62
|
The skill tells the agent to read the whole message `login` printed and its exit code before it tells you what happened.
|
|
52
63
|
|
|
53
|
-
|
|
64
|
+
Name what you want built in the prompt.
|
|
54
65
|
|
|
55
66
|
## What the login can do
|
|
56
67
|
|
|
@@ -65,13 +76,11 @@ Checked on production with Claude Code (an interactive session, no crtr on `PATH
|
|
|
65
76
|
| Change signing keys, redirect URIs and the run notice URL on an app in testing | Change those on a verified app: do it in the console |
|
|
66
77
|
| | Approve another login |
|
|
67
78
|
|
|
68
|
-
Deleting an app, its listing, spend cap and sharing stay in the console on purpose. The CLI refuses them without sending a request, and the directory refuses them (403 `provider_refused`) if a call is made by hand.
|
|
79
|
+
Deleting an app, its listing, spend cap and sharing stay in the console on purpose. The CLI refuses them without sending a request, and the directory refuses them (403 `provider_refused`) if a call is made by hand.
|
|
69
80
|
|
|
70
81
|
The login lives in `~/.config/northlight/credentials.json` (`$XDG_CONFIG_HOME/northlight/credentials.json` when set), mode `0600`, outside your project. The CLI refuses a file that other users can read. It lasts 30 days, or until it has gone 14 days unused, and then you run `login` again. An account holds at most 10 logins; an eleventh replaces the oldest. The Developer access page shows the date the login goes unused, "Expires Oct 31" for a login approved on Oct 17, not the 30-day date the CLI prints.
|
|
71
82
|
|
|
72
|
-
Revoke it on your account's Developer access page (`https://account.northlight.ai/account/developer`), or run `npx @north-light/sdk logout`, which revokes it on the server and removes it from the file. The file is not deleted: it is rewritten as `{"version":1,"issuers":{}}`, with no secret in it. A revoked login stops working at once: `whoami` and every other command fail with "This login was revoked" and ask for `login`, and a token minted before the revoke is refused with 401 `credential_revoked` even before it expires.
|
|
73
|
-
|
|
74
|
-
Not yet checked on production: the 10-login limit and the eleventh login replacing the oldest, and the 14-day and 30-day expiries.
|
|
83
|
+
Revoke it on your account's Developer access page (`https://account.northlight.ai/account/developer`), or run `npx @north-light/sdk logout`, which revokes it on the server and removes it from the file. The file is not deleted: it is rewritten as `{"version":1,"issuers":{}}`, with no secret in it. A revoked login stops working at once: `whoami` and every other command fail with "This login was revoked" and ask for `login`, and a token minted before the revoke is refused with 401 `credential_revoked` even before it expires.
|
|
75
84
|
|
|
76
85
|
## How it stays current
|
|
77
86
|
|
|
@@ -76,7 +76,3 @@ LoggerModule.forRoot({
|
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
Test it: send a callback with a recognizable fake code and assert that the code, the state and the cookie never appear in the log output.
|
|
79
|
-
|
|
80
|
-
## Checked and not checked
|
|
81
|
-
|
|
82
|
-
Checked on production: the discovery document lists `S256` and `private_key_jwt`, and the login route of the quickstart server redirects to `https://account.northlight.ai/authorize` with the PKCE challenge and nonce. The rest is from the SDK and directory source. A full sign-in as the account that owns the app, through the consent page and the callback, was run on production with the quickstart server. A sign-in by a second person, token refresh and the refusals are not yet checked on production.
|