@north-light/cli 0.3.543 → 0.3.545
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/crouter-sdk/release-notes.md +6 -0
- 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/guides/watchable-objects.md +74 -0
- 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/guides/watchable-objects.md +71 -0
- 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/docs/release-local.md +2 -2
- package/docs/sdk/release-notes.md +6 -0
- package/package.json +3 -3
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +13 -12
|
@@ -11,19 +11,19 @@ when-and-why-to-read: When you need Quick setup, this knowledge should be read
|
|
|
11
11
|
npx @north-light/sdk@latest init my-app
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
This command creates a small Node server, a signing key and an env file. About ten minutes later you have an app on your laptop that signs you in with Northlight, starts an agent run on your own Northlight runtime, and texts your phone. The app runs on your machine and talks to Northlight's production directory at `https://account.northlight.ai`. There is no local Northlight stack to run
|
|
14
|
+
This command creates a small Node server, a signing key and an env file. About ten minutes later you have an app on your laptop that signs you in with Northlight, starts an agent run on your own Northlight runtime, and texts your phone. The app runs on your machine and talks to Northlight's production directory at `https://account.northlight.ai`. There is no local Northlight stack to run. To run crouter on your own machine as well, see [[northlight-apps/get-started]].
|
|
15
15
|
|
|
16
16
|
You need Node.js 20.6 or later (the start command uses `--env-file`) and a phone that can receive iMessage. The server was run on Node 20.6, 20.20, 22.14, 22.22 and 26.12.
|
|
17
17
|
|
|
18
18
|
Using a coding agent? Tell it: *Read https://sdk.docs.northlight.ai/SKILL.md and follow it.* It runs every command below; you sign up, then choose **Allow** twice in your browser. See [[northlight-apps/coding-agents]].
|
|
19
19
|
|
|
20
|
-
Each step below ends with what you should see when it worked.
|
|
20
|
+
Each step below ends with what you should see when it worked.
|
|
21
21
|
|
|
22
22
|
## 1. Create an account
|
|
23
23
|
|
|
24
24
|
Open `https://account.northlight.ai/signup` and create your account. Signup texts a confirmation code to your phone, and that text is sent over iMessage. A number that cannot receive iMessage gets "We couldn't send the text", and retrying does not help. One phone number belongs to one account.
|
|
25
25
|
|
|
26
|
-
You should see: your account's page after you enter the code. Opening `https://account.northlight.ai/console` while signed in shows the console, where your apps are listed.
|
|
26
|
+
You should see: your account's page after you enter the code. Opening `https://account.northlight.ai/console` while signed in shows the console, where your apps are listed.
|
|
27
27
|
|
|
28
28
|
## 2. Scaffold the app
|
|
29
29
|
|
|
@@ -51,8 +51,6 @@ You should see, on the terminal: `Created a Northlight app in my-app:` with the
|
|
|
51
51
|
|
|
52
52
|
Only the browser registration below uses that line: `register` reads the public half from `local/app-key.json` itself. If a file already exists, `init` refuses, names it and exits non-zero without writing anything.
|
|
53
53
|
|
|
54
|
-
Checked on production: `npx @north-light/sdk@latest init my-app` (npm `latest` 0.3.476) in an empty directory with no crtr on `PATH` printed this output and wrote these files, the private key has mode `0600`, and `server.ts` is the one listed at the end of this page. Running it again names the skill file and writes nothing.
|
|
55
|
-
|
|
56
54
|
## 3. Log in
|
|
57
55
|
|
|
58
56
|
```bash
|
|
@@ -91,8 +89,6 @@ and "Done. Go back to your terminal." in the browser. The name, id and dates are
|
|
|
91
89
|
|
|
92
90
|
The login is saved outside your project, in `~/.config/northlight/credentials.json` (mode `0600`). Never commit, paste or print it.
|
|
93
91
|
|
|
94
|
-
Checked on production: this whole step, including the confirm page and its wording. The confirm page reads "Let a coding agent manage your Northlight apps?", shows the code, the computer's name and platform and the address it asked from ("The computer named itself; Northlight did not check it"), and lists what the login can and can't do. After **Allow** the page reads "Done / Go back to your terminal." and the terminal prints the output above.
|
|
95
|
-
|
|
96
92
|
If you run `login` again, it does not start over. While a code is pending it prints "Still waiting for the login started at HH:MM." with the same link and code. Once you are logged in it prints "Already logged in as …" and exits 0. If the code expired unapproved it exits 1 and says so: run it again for a new code. See [[northlight-apps/cli]].
|
|
97
93
|
|
|
98
94
|
If `login` cannot reach `account.northlight.ai`, or you would rather use the browser, skip to [Register in the browser instead](#register-in-the-browser-instead).
|
|
@@ -120,8 +116,6 @@ Next: npm i && npm start, then open http://localhost:3000/auth/login
|
|
|
120
116
|
|
|
121
117
|
`local/app.env` now has all four settings. The name, client ID and key ID are examples. Change the defaults with `--name`, `--logo`, `--justification` and `--redirect-uri`; the default logo is a placeholder, `https://sdk.docs.northlight.ai/app-logo.png`. `register` refuses when `CROUTER_CLIENT_ID` is already set. A login can register at most 10 apps a day.
|
|
122
118
|
|
|
123
|
-
Checked on production: this step, including that the directory accepts the default logo.
|
|
124
|
-
|
|
125
119
|
## 5. Run it
|
|
126
120
|
|
|
127
121
|
```bash
|
|
@@ -133,8 +127,6 @@ node --env-file=local/app.env --import tsx server.ts
|
|
|
133
127
|
|
|
134
128
|
Open `http://localhost:3000/auth/login`. Your browser goes to `account.northlight.ai`. Sign in if asked, read the consent page, and choose Allow. You land back on `http://localhost:3000/` with "Signed in" and your Northlight user id. Press **Start a run and send me a text**. The page answers "Done", shows the run's status `idle`, the run's reply (`pong`) and the text's `message_id`, and your phone receives "Hello from my first Northlight app." from Northlight's number.
|
|
135
129
|
|
|
136
|
-
Checked on production, as the account that owns the app: the server starts (with `npm start` too), `/` shows the sign-in link, and `/auth/login` answers `303` to `https://account.northlight.ai/authorize?client_id=…&scope=openid+profile+crtr%3Allm+crtr%3Aact+sms%3Asend…&code_challenge_method=S256…`. The consent page listed the four permissions and the monthly cap, **Allow** returned to `http://localhost:3000/` with "Signed in" and the user id, and the button page answered "Done" with status `idle` and a `message_id` for the text. In the console the key then read "Last used just now". Not yet checked on production: that the text reached the phone (the `message_id` is the provider's answer, not a delivery receipt), and the `426` case. The `pong` reply is read through `runs.reply` and has not been run on production since this example changed. Signing in as a second person needs a second account and is not covered here.
|
|
137
|
-
|
|
138
130
|
If `/` shows "One step left", `CROUTER_CLIENT_ID` is empty: run `npx @north-light/sdk register` (or, for an app you registered in the browser, `env --client-id`) and restart. If a call fails with `426 runtime_version_unsupported`, your SDK is newer than the Northlight runtimes. Install the previous version with `npm i --save-exact @north-light/sdk@<version>` and tell support@northlight.ai.
|
|
139
131
|
|
|
140
132
|
## 6. What next
|
|
@@ -165,16 +157,12 @@ Choose **Register app**.
|
|
|
165
157
|
|
|
166
158
|
You should see: a page titled "*your app's name* is registered" with a **Client ID** and, under it, the key ID of the key you pasted. Copy the client ID, then choose **Continue to *your app's name***. If it says "Next: add your app's public key on the Sign-in tab", the key was left out: open the app's **Sign-in** tab and choose **Add key**. The app starts asking for `crtr:llm` and `crtr:act`.
|
|
167
159
|
|
|
168
|
-
Checked on production: these field labels, registering with a pasted key, and the app page opening in testing with that key active and `crtr:llm` and `crtr:act` listed. The "Next: add your app's public key" message, which shows when the key is left out, is not yet checked on production.
|
|
169
|
-
|
|
170
160
|
### Add the permission to text
|
|
171
161
|
|
|
172
162
|
On the app's page, open the **What it asks for** tab and choose **Add permissions**. Choose **Texting**, tick `sms:send` ("Send you text messages from Northlight's number"), and choose **Done**. Then choose **Save** under the list: **Done** only closes the dialog, and the change is not stored until you save.
|
|
173
163
|
|
|
174
164
|
You should see: `sms:send` under what your app asks for after **Save** says "Saved", next to `crtr:llm` and `crtr:act`. The server in this guide asks for all three, so without `sms:send` the consent page does not offer it and the text in step 5 is refused. Take scope strings from this list. Do not invent them. See [[northlight-apps/concepts/scopes-and-grants]].
|
|
175
165
|
|
|
176
|
-
Checked on production: after **Save** and a reload, the tab lists `crtr:llm` and `crtr:act` under Agents and `sms:send` under Texting, and the consent page in step 5 offered the text permission.
|
|
177
|
-
|
|
178
166
|
### Write the client ID into the env file
|
|
179
167
|
|
|
180
168
|
```bash
|
|
@@ -193,8 +181,6 @@ Key file: local/app-key.json (set CROUTER_KEY_FILE to change it).
|
|
|
193
181
|
|
|
194
182
|
`local/app.env` now has all four settings. The command makes no network call and keeps every other line in the file. Add `--origin <url>` if your app does not run on `http://localhost:3000`.
|
|
195
183
|
|
|
196
|
-
Checked on production: this output, and that the command works in a directory with no `local/` yet. `--origin` is not yet checked on production.
|
|
197
|
-
|
|
198
184
|
## The server `init` writes
|
|
199
185
|
|
|
200
186
|
This is `examples/apps/quickstart-server.ts`. The SDK reads no environment: the server passes the values in.
|
|
@@ -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. Install it with `npm i @north-light/comms`.
|
|
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 |
|