@north-light/cli 0.3.471 → 0.3.473
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/dto/nodes.d.ts +1 -1
- package/dist/builtin-memory/crouter-sdk/README.md +1 -1
- package/dist/builtin-memory/crouter-sdk/errors.md +1 -1
- package/dist/builtin-memory/crouter-sdk/getting-started.md +1 -1
- package/dist/builtin-memory/crouter-sdk/guides/apps-on-northlight.md +1 -1
- package/dist/builtin-memory/crouter-sdk/nodes.md +1 -1
- package/dist/builtin-memory/crouter-sdk/release-notes.md +32 -20
- package/dist/commands/node/create.js +2 -2
- package/dist/commands/sys/panels/provider-panel.d.ts +2 -0
- package/dist/commands/sys/panels/provider-panel.js +1 -1
- package/dist/commands/sys/setup-wizard.js +1 -1
- package/docs/apps/cli.md +40 -0
- package/docs/apps/coding-agents.md +72 -0
- package/docs/apps/concepts/apps-and-sign-in.md +82 -0
- package/docs/apps/concepts/connections.md +106 -0
- package/docs/apps/concepts/providers.md +59 -0
- package/docs/apps/concepts/runs.md +94 -0
- package/docs/apps/concepts/scopes-and-grants.md +62 -0
- package/docs/apps/concepts/testing-mode.md +36 -0
- package/docs/apps/errors.md +117 -0
- package/docs/apps/guides/background-runs.md +78 -0
- package/docs/apps/guides/files-and-documents.md +59 -0
- package/docs/apps/guides/go-live.md +37 -0
- package/docs/apps/guides/grant-changes.md +98 -0
- package/docs/apps/guides/manage-your-app.md +75 -0
- package/docs/apps/guides/many-users.md +61 -0
- package/docs/apps/guides/stream-a-run.md +82 -0
- package/docs/apps/guides/test-your-app.md +191 -0
- package/docs/apps/guides/typed-results.md +72 -0
- package/docs/apps/how-it-fits.md +46 -0
- package/docs/apps/providers/README.md +26 -0
- package/docs/apps/providers/context.md +32 -0
- package/docs/apps/providers/device.md +26 -0
- package/docs/apps/providers/integrations.md +26 -0
- package/docs/apps/providers/search.md +29 -0
- package/docs/apps/providers/sms.md +37 -0
- package/docs/apps/quickstart.md +126 -0
- package/docs/sdk/README.md +1 -1
- package/docs/sdk/errors.md +1 -1
- package/docs/sdk/getting-started.md +1 -1
- package/docs/sdk/guides/apps-on-northlight.md +1 -1
- package/docs/sdk/nodes.md +1 -1
- package/docs/sdk/release-notes.md +32 -20
- package/package.json +3 -3
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +32 -12
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Quick setup
|
|
3
|
+
description: From no account to a running app that signs a person in with Northlight, starts a run on their runtime and sends them a text.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Quick setup
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npx @north-light/sdk@latest init my-app
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
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, and you do not need crouter.
|
|
13
|
+
|
|
14
|
+
You need Node.js 22.19 or later and a phone that can receive iMessage.
|
|
15
|
+
|
|
16
|
+
Using a coding agent? Run the same `init` command, or tell your agent: *Read https://sdk.docs.northlight.ai/SKILL.md and follow it.* See [Use your coding agent](/docs/coding-agents).
|
|
17
|
+
|
|
18
|
+
Each step below ends with what you should see when it worked. A step marked "(not yet checked on production)" is written from the source code and has not been run end to end against `account.northlight.ai`.
|
|
19
|
+
|
|
20
|
+
## 1. Create an account
|
|
21
|
+
|
|
22
|
+
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.
|
|
23
|
+
|
|
24
|
+
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. Checked on production: `/signup` redirects to the phone step at `/signup/phone`, and `/console` while signed out redirects to sign-in. Completing signup and the signed-in console are not yet checked on production.
|
|
25
|
+
|
|
26
|
+
## 2. Scaffold the app
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx @north-light/sdk@latest init my-app
|
|
30
|
+
cd my-app
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`init` writes these files into `my-app/`:
|
|
34
|
+
|
|
35
|
+
| File | What it is |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `package.json` | Pins `@north-light/sdk` to the exact version of the CLI you ran, and adds `tsx`. |
|
|
38
|
+
| `server.ts` | The server shown at the end of this page. |
|
|
39
|
+
| `local/app-key.json` | Your app's private signing key, mode `0600`. Never commit it or paste it anywhere. |
|
|
40
|
+
| `local/app.env` | The settings the server reads: `CROUTER_ISSUER`, `APP_ORIGIN`, an empty `CROUTER_CLIENT_ID=` and `CROUTER_KEY_FILE`. |
|
|
41
|
+
| `.gitignore` | Keeps `local/` and `node_modules/` out of git. |
|
|
42
|
+
| `.agents/skills/northlight/SKILL.md` and `.claude/skills/northlight/SKILL.md` | The Northlight skill for your coding agent. |
|
|
43
|
+
|
|
44
|
+
You should see, on the terminal: `Created a Northlight app in my-app:` with the file names, the path of the private key, the next steps in order, and last, on its own line, the **public key** as one line of JSON:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{"kty":"EC","x":"…","y":"…","crv":"P-256","kid":"…","alg":"ES256","use":"sig"}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Copy that line. You paste it into the console in the next step. If a file already exists, `init` refuses, names it and exits non-zero without writing anything.
|
|
51
|
+
|
|
52
|
+
Checked: this output and these files, from the SDK source built on this branch in an empty directory with no crtr on `PATH`. `npx @north-light/sdk@latest` fetches the npm `latest` release, and that release does not include `init` until the site goes live (not yet checked on production).
|
|
53
|
+
|
|
54
|
+
## 3. Register the app in the console
|
|
55
|
+
|
|
56
|
+
Open `https://account.northlight.ai/console/new`. The page is titled **Register an app**. Fill in:
|
|
57
|
+
|
|
58
|
+
| Field | Value |
|
|
59
|
+
|---|---|
|
|
60
|
+
| **Name** | Shown to people on the consent page and in their account. |
|
|
61
|
+
| **Origin URL** | `http://localhost:3000` |
|
|
62
|
+
| **Logo URL** | Required. A square image shown beside your app's name. |
|
|
63
|
+
| **Why your app needs access** | One sentence. It is quoted on the consent page as your words. |
|
|
64
|
+
| **Redirect URIs** | `http://localhost:3000/auth/callback`, one URL per line. Sign-in returns only to a URI listed here, matched exactly. `localhost` and `127.0.0.1` are different URIs, and the port counts. |
|
|
65
|
+
| **Public key (optional)** | The JSON line `init` printed. |
|
|
66
|
+
|
|
67
|
+
Choose **Register app**.
|
|
68
|
+
|
|
69
|
+
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. 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`.
|
|
70
|
+
|
|
71
|
+
The field labels are from the console's source. Pasting a key at registration is not yet checked on production.
|
|
72
|
+
|
|
73
|
+
## 4. Add the permission to text
|
|
74
|
+
|
|
75
|
+
On the app's page, open the **What it asks for** tab and choose **Add permissions**. Add `sms:send` from the list.
|
|
76
|
+
|
|
77
|
+
You should see: `sms:send` under what your app asks for, 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 6 is refused. Take scope strings from this list. Do not invent them. See [Scopes and grants](/docs/concepts/scopes-and-grants).
|
|
78
|
+
|
|
79
|
+
Not yet checked on production.
|
|
80
|
+
|
|
81
|
+
## 5. Write the client ID into the env file
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npx @north-light/sdk env --client-id <client id>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
You should see:
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
Set in local/app.env:
|
|
91
|
+
CROUTER_ISSUER=https://account.northlight.ai
|
|
92
|
+
CROUTER_CLIENT_ID=<client id>
|
|
93
|
+
APP_ORIGIN=http://localhost:3000
|
|
94
|
+
Key file: local/app-key.json (set CROUTER_KEY_FILE to change it).
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`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`.
|
|
98
|
+
|
|
99
|
+
Checked from the SDK source built on this branch.
|
|
100
|
+
|
|
101
|
+
## 6. Run it
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npm i
|
|
105
|
+
node --env-file=local/app.env --import tsx server.ts
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
(`npm start` runs the same command.) You should see `Listening on http://localhost:3000`.
|
|
109
|
+
|
|
110
|
+
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`, and your phone receives "Hello from my first Northlight app." from Northlight's number.
|
|
111
|
+
|
|
112
|
+
What was checked: the server starts, `/` 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, the callback, the run and the text are not yet checked on production.
|
|
113
|
+
|
|
114
|
+
If `/` shows "One step left", `CROUTER_CLIENT_ID` is empty: repeat step 5 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.
|
|
115
|
+
|
|
116
|
+
## 7. What next
|
|
117
|
+
|
|
118
|
+
- Only you can sign in until you add testers: see [Testing mode](/docs/concepts/testing-mode).
|
|
119
|
+
- Keep each person's sign-in across restarts and processes: see [Connections](/docs/concepts/connections).
|
|
120
|
+
- See what `server.ts` does and where each piece runs: [Where your app runs](/docs/how-it-fits).
|
|
121
|
+
|
|
122
|
+
## The server `init` writes
|
|
123
|
+
|
|
124
|
+
This is `examples/apps/quickstart-server.ts`. The SDK reads no environment: the server passes the values in.
|
|
125
|
+
|
|
126
|
+
<include lang="ts">../../examples/apps/quickstart-server.ts</include>
|
package/docs/sdk/README.md
CHANGED
|
@@ -82,7 +82,7 @@ The directory rejects the key until its public JWK is added to the app. To chang
|
|
|
82
82
|
| [Errors](./errors.md) | The error class table and the retry policy |
|
|
83
83
|
| [Docker environment](./docker.md) | `start()`, `attach()`, `connection()` from `@north-light/env-docker` |
|
|
84
84
|
| [Testing your app](./testing.md) | `FakeCloud` from `@north-light/sdk/testing`: a fake directory and runtime your real `OAuth2Client` and `Crouter` talk to through `fetch` |
|
|
85
|
-
| [Release notes](
|
|
85
|
+
| [Release notes](https://sdk.docs.northlight.ai/docs/release-notes) | Behavior changes an existing app must act on, newest first |
|
|
86
86
|
| [Apps on Northlight](./guides/apps-on-northlight.md) | Building and registering an app against production (joining a laptop, `crtr cloud apps` registration, keys, env file, scopes, running locally, the `crtr cloud apps` commands); typed results from app runs with `runs.parse`, declines, bounding a run, `run_idle`, and waiting on a provider your app requires |
|
|
87
87
|
| [Apps with many users](./guides/apps-with-many-users.md) | Per-person runtime connections, app-owned user access, privacy, and spending |
|
|
88
88
|
|
package/docs/sdk/errors.md
CHANGED
|
@@ -24,7 +24,7 @@ Every class below extends `APIError`, which carries `status`, `code`, `message`,
|
|
|
24
24
|
| Condition | Class |
|
|
25
25
|
|---|---|
|
|
26
26
|
| SDK configuration error | `CrouterError` with `status: 0`, `code: 'crouter_error'` |
|
|
27
|
-
| The runtime does not serve the contract level the SDK requires, or names none. The hosting router or the runtime refuses this *before* handling (the SDK sends `Crouter-Required-Contract` and a frozen `Crouter-Minimum-Runtime-Version`): nothing was accepted and no idempotency key was recorded. A runtime that predates the contract level handles the request first, and the SDK then raises after | `RuntimeVersionError` with `status: 426`, `code: 'runtime_version_unsupported'`; `requiredContract` and `runtimeContract` (or `null`) name both levels, `sdkVersion` and `runtimeVersion` stay for logging, and it is never retryable. A level is `MAJOR.MINOR`; the runtime satisfies it when it serves the same major and a minor at or above. See [Release notes](
|
|
27
|
+
| The runtime does not serve the contract level the SDK requires, or names none. The hosting router or the runtime refuses this *before* handling (the SDK sends `Crouter-Required-Contract` and a frozen `Crouter-Minimum-Runtime-Version`): nothing was accepted and no idempotency key was recorded. A runtime that predates the contract level handles the request first, and the SDK then raises after | `RuntimeVersionError` with `status: 426`, `code: 'runtime_version_unsupported'`; `requiredContract` and `runtimeContract` (or `null`) name both levels, `sdkVersion` and `runtimeVersion` stay for logging, and it is never retryable. A level is `MAJOR.MINOR`; the runtime satisfies it when it serves the same major and a minor at or above. See [Release notes](https://sdk.docs.northlight.ai/docs/release-notes) |
|
|
28
28
|
| HTTP 400 | `BadRequestError` |
|
|
29
29
|
| HTTP 401 | `AuthenticationError` |
|
|
30
30
|
| HTTP 403 | `PermissionDeniedError` |
|
|
@@ -25,7 +25,7 @@ npm i --save-exact @north-light/sdk@0.3.451
|
|
|
25
25
|
|
|
26
26
|
One dependency. It re-exports every data type you need, so you do not also install `@north-light/api`.
|
|
27
27
|
|
|
28
|
-
`@latest` always works against production: a release reaches `latest` only once production serves its contract level. The `next` tag carries every release the moment it is published and may need a newer runtime than production runs (see [Errors](./errors.md) for `RuntimeVersionError`). Keep the exact pin: the compatibility gate protects the wire, not the TypeScript API. To move it, check `npm view @north-light/sdk dist-tags` and read the [release notes](
|
|
28
|
+
`@latest` always works against production: a release reaches `latest` only once production serves its contract level. The `next` tag carries every release the moment it is published and may need a newer runtime than production runs (see [Errors](./errors.md) for `RuntimeVersionError`). Keep the exact pin: the compatibility gate protects the wire, not the TypeScript API. To move it, check `npm view @north-light/sdk dist-tags` and read the [release notes](https://sdk.docs.northlight.ai/docs/release-notes) first.
|
|
29
29
|
|
|
30
30
|
## 3. Node, on the owner's own daemon
|
|
31
31
|
|
|
@@ -50,7 +50,7 @@ npm init -y
|
|
|
50
50
|
npm i --save-exact @north-light/sdk@0.3.451
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
`0.3.451` is the version this guide was last checked against on production. To move on purpose, run `npm view @north-light/sdk dist-tags`, read the [release notes](
|
|
53
|
+
`0.3.451` is the version this guide was last checked against on production. To move on purpose, run `npm view @north-light/sdk dist-tags`, read the [release notes](https://sdk.docs.northlight.ai/docs/release-notes) for the versions in between, and install the new exact version the same way.
|
|
54
54
|
|
|
55
55
|
Pin an exact version of `@north-light/sdk` rather than a range or `latest`. A compatibility gate between the SDK and Northlight's runtimes protects the wire, so an SDK that is too new for the runtime is refused cleanly. It does not protect the TypeScript API: a later SDK release can change a type or a method your code uses, and an unpinned install picks that up on its next `npm i`.
|
|
56
56
|
If a call fails with `426 runtime_version_unsupported`, the SDK is newer than the Northlight runtimes. This can happen briefly right after a new SDK release, until production runtimes are upgraded. Install the previous version or wait, and tell Northlight at support@northlight.ai.
|
package/docs/sdk/nodes.md
CHANGED
|
@@ -63,7 +63,7 @@ Wire fields are `snake_case`. Every `NodeCreateParams` property is optional; `pa
|
|
|
63
63
|
| `description` | `string` | Display description. |
|
|
64
64
|
| `parent` | node id | Graph placement. An external caller leaves this unset. |
|
|
65
65
|
| `creator` | node id | Graph placement. An external caller leaves this unset. |
|
|
66
|
-
| `scopes` | `string[]` | Per-run allow-list. Omit it to inherit every scope, or under a scoped token to receive that token's ceiling; a list outside the ceiling answers `403 scope_denied` with the offending scopes in `details.scopes`. Scopes are prefixed (`crtr:llm`, `crtr:act`, `crtr:schedule`, `crtr:memory:…`, `crtr:files:…`); `crtr:act` requires `crtr:llm`, and the list must stay inside what the creator's grant holds.
|
|
66
|
+
| `scopes` | `string[]` | Per-run allow-list. Omit it to inherit every scope, or under a scoped token to receive that token's ceiling; a list outside the ceiling answers `403 scope_denied` with the offending scopes in `details.scopes`. Scopes are prefixed (`crtr:llm`, `crtr:act`, `crtr:schedule`, `crtr:memory:…`, `crtr:files:…`); `crtr:act` requires `crtr:llm`, and the list must stay inside what the creator's grant holds. Provider-group scopes (`device:messages_send`) are enforced on the broker route; on an unsandboxed host a same-UID process can bypass the list by calling the daemon socket directly, so it guards against a mistaken call, not a hostile node. Keep the list short: it travels in a request header on every device call. See [scoped tokens](./getting-started.md#a-token-that-holds-less-than-the-owner). |
|
|
67
67
|
| `worktree` | `string \| boolean` | Create a managed git worktree for the run. |
|
|
68
68
|
| `fork_from` | `string` | Start from an existing conversation. |
|
|
69
69
|
| `no_kickoff` | `boolean` | Create the node without sending the first message. |
|
|
@@ -7,6 +7,18 @@ description: Behavior changes in @north-light/sdk that an app must act on, newes
|
|
|
7
7
|
|
|
8
8
|
Changes to `@north-light/sdk` that can change what an existing app sees. Newest first.
|
|
9
9
|
|
|
10
|
+
## 0.3.472
|
|
11
|
+
|
|
12
|
+
### `npx @north-light/sdk init`, `skill` and `env --client-id` (additive, no app change)
|
|
13
|
+
|
|
14
|
+
The `crouter-sdk` bin gains three commands that need neither crtr nor a joined machine:
|
|
15
|
+
|
|
16
|
+
- `init [dir] [--agent claude|agents|all] [--global-skill]` scaffolds an app in an empty or new directory (`package.json` with `@north-light/sdk` pinned to the CLI's own version plus `tsx`, `server.ts`, `.gitignore`, `local/app.env`, a signing key in `local/app-key.json`) and installs the `northlight` Agent Skill into `.agents/skills` and `.claude/skills`. In a directory that already has a `package.json` it writes no app code: it runs `npm i --save-exact @north-light/sdk@<this version>`, makes a key if `local/app-key.json` is missing, and installs the skill. It never overwrites a file; it exits 1 and names the file it refused.
|
|
17
|
+
- `skill [--agent claude|agents|all] [--global] [--print]` writes the skill that ships in the package, replacing an older copy. The skill's `metadata.sdk-version` is the package version; run `skill` again after changing the pinned SDK version.
|
|
18
|
+
- `env --client-id <id> [--origin <url>] [--issuer <url>] [--out <path>]` writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` into `local/app.env` and keeps every other line, with no network call. `env --app <id>` still reads them through crtr; passing both is a usage error.
|
|
19
|
+
|
|
20
|
+
`keygen` is unchanged. The package still has one bin, so `npx @north-light/sdk <command>` keeps working. A bare `npx @north-light/sdk`, `--help` and an unknown command print the command list (exit 1 for the unknown command).
|
|
21
|
+
|
|
10
22
|
## 0.3.462
|
|
11
23
|
|
|
12
24
|
### `ProfileDTO.memory_path` is removed (profile objects carry only `profile_path`)
|
|
@@ -59,28 +71,28 @@ The SDK no longer refuses a runtime because its release number is lower than the
|
|
|
59
71
|
|
|
60
72
|
Not breaking: no type or wire shape an app reads is removed or changed.
|
|
61
73
|
|
|
62
|
-
- An app Northlight has not verified is in testing: only its admin and named testers can sign in. A refused sign-in redirects with `error=access_denied`, `error_description=app_in_testing`; a refused refresh answers `invalid_grant` `app_in_testing`. The refresh refusal is a new `Refusal` with `code: 'app_in_testing'` and `kind: 'paused'`, like `grant_suspended`: keep the person's data. If you `switch` exhaustively on `refusal.code`, add the case. See [When a refresh is refused](
|
|
63
|
-
- New `authorizationRefusalOf(callback)` reads the sign-in refusal from the callback's query and returns `AuthorizationRefusal` (`{code: 'app_in_testing', error: 'access_denied', state}`), else `null`. See [When sign-in is refused](
|
|
64
|
-
- `@north-light/sdk/testing`: `cloud.testing` (`enable`, `disable`, `active`, `addTester`, `removeTester`, `testers`, `allowed`), `cloud.authorize(userId, {redirectUri, state})`, `user.setAccount({email, emailVerified, phone})`, and `'app_in_testing'` in `FakeRefusalCode`. The fake directory now serves `grant_type=authorization_code` for codes `cloud.authorize` issued. See [Testing mode](
|
|
74
|
+
- An app Northlight has not verified is in testing: only its admin and named testers can sign in. A refused sign-in redirects with `error=access_denied`, `error_description=app_in_testing`; a refused refresh answers `invalid_grant` `app_in_testing`. The refresh refusal is a new `Refusal` with `code: 'app_in_testing'` and `kind: 'paused'`, like `grant_suspended`: keep the person's data. If you `switch` exhaustively on `refusal.code`, add the case. See [When a refresh is refused](/docs/concepts/connections#when-a-refresh-is-refused).
|
|
75
|
+
- New `authorizationRefusalOf(callback)` reads the sign-in refusal from the callback's query and returns `AuthorizationRefusal` (`{code: 'app_in_testing', error: 'access_denied', state}`), else `null`. See [When sign-in is refused](/docs/concepts/apps-and-sign-in#when-the-directory-refuses-to-sign-someone-in).
|
|
76
|
+
- `@north-light/sdk/testing`: `cloud.testing` (`enable`, `disable`, `active`, `addTester`, `removeTester`, `testers`, `allowed`), `cloud.authorize(userId, {redirectUri, state})`, `user.setAccount({email, emailVerified, phone})`, and `'app_in_testing'` in `FakeRefusalCode`. The fake directory now serves `grant_type=authorization_code` for codes `cloud.authorize` issued. See [Testing mode](/docs/guides/test-your-app#testing-mode).
|
|
65
77
|
|
|
66
78
|
## 0.3.415
|
|
67
79
|
|
|
68
80
|
### Monthly spend cap enforced; `usage.get` (additive; one behavior change)
|
|
69
81
|
|
|
70
82
|
- **Behavior change:** user-paid model calls (`/v1/llm`) and provider calls now count against the grant's `spend_cap` (from `grant.get()`; Northlight sets $50) for the current UTC month. Once the month reaches it, the next call answers 402 `cap_exhausted` (a `RateLimitError`, `retryable: false`, `userAction: 'raise_cap'`, `resetAt` the first instant of next month) before any model or provider is called. A run whose model call is refused this way ends with outcome reason `cap_exhausted`. The check runs before each call, so the call that crosses the cap still completes. App-paid usage never counts. A grant with `spend_cap: null` is not capped.
|
|
71
|
-
- New `client.usage.get({month?})` (`month` is `YYYY-MM`, UTC; default the current month) answers `SpendUsageDTO`: `usd_equivalent` (user-paid spend, provider receipts included), `by_model`, `app_paid`, `spend_cap` and `reset_at`. See [Show what a person's month has cost](
|
|
83
|
+
- New `client.usage.get({month?})` (`month` is `YYYY-MM`, UTC; default the current month) answers `SpendUsageDTO`: `usd_equivalent` (user-paid spend, provider receipts included), `by_model`, `app_paid`, `spend_cap` and `reset_at`. See [Show what a person's month has cost](/docs/guides/grant-changes#show-what-a-persons-month-has-cost).
|
|
72
84
|
- A completion's `usage.usd_equivalent` is now the served model's list-price cost; before it was always `null`. It stays `null` for a model with no list price, and that usage adds nothing to the month.
|
|
73
85
|
|
|
74
86
|
### `npx @north-light/sdk env` (additive); keygen on an unjoined machine exits 2
|
|
75
87
|
|
|
76
|
-
- New `npx @north-light/sdk env --app <client id> [--out <path>]` writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` from `crtr cloud apps get` into `local/app.env`, keeping every other line. See [Go live with an app key](
|
|
88
|
+
- New `npx @north-light/sdk env --app <client id> [--out <path>]` writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` from `crtr cloud apps get` into `local/app.env`, keeping every other line. See [Go live with an app key](/docs/guides/go-live).
|
|
77
89
|
- `keygen --app` on a machine whose `crtr` isn't joined to a Northlight account now prints the join step and the manual steps and exits 2. A missing `crtr` keeps its message and exit 0.
|
|
78
90
|
|
|
79
91
|
### One deadline per SDK call, and a deadline on every OAuth/JWKS fetch (additive, with one behaviour change)
|
|
80
92
|
|
|
81
93
|
Not breaking: no type or wire shape an app reads is removed or changed. Apps pinned to the previous release keep working unchanged.
|
|
82
94
|
|
|
83
|
-
- `RequestOptions` gains `budgetMs` and `deadlineAt`: one absolute deadline for the call covering every attempt, every retry wait (including a `Retry-After`) and the token fetch/refresh. A wait that would pass the deadline rejects at once with the new `DeadlineExceededError` (`code: 'deadline_exceeded'`, `reason: 'elapsed' | 'retry_after'`, `deadlineAt`, `cause`), which extends `APIConnectionTimeoutError`, so code catching that still catches it. A transport that ignores its `AbortSignal` can no longer outlive the deadline. Without `budgetMs`/`deadlineAt` nothing about retries changes. See [One deadline per call](
|
|
95
|
+
- `RequestOptions` gains `budgetMs` and `deadlineAt`: one absolute deadline for the call covering every attempt, every retry wait (including a `Retry-After`) and the token fetch/refresh. A wait that would pass the deadline rejects at once with the new `DeadlineExceededError` (`code: 'deadline_exceeded'`, `reason: 'elapsed' | 'retry_after'`, `deadlineAt`, `cause`), which extends `APIConnectionTimeoutError`, so code catching that still catches it. A transport that ignores its `AbortSignal` can no longer outlive the deadline. Without `budgetMs`/`deadlineAt` nothing about retries changes. See [One deadline per call](/docs/errors#retries).
|
|
84
96
|
- An app that holds a lease or lock while it makes an SDK call should pass `budgetMs` and keep the lease longer than it, with margin, and state that in code.
|
|
85
97
|
- `OAuth2Options.timeoutMs` (default 10 s): one deadline per directory operation (discovery, token exchange, refresh, revoke, `runtime_provisioning` retries and their `retry_after_s`). A refresh under the connection-store lock now holds the lock at most `timeoutMs`. `JwksCache` (and so `verifyIdToken`) takes `{timeoutMs}` (default 10 s) over its discovery and key-set reads together.
|
|
86
98
|
- **Behaviour change:** these directory fetches used to have no deadline (only discovery had one). A directory that takes longer than 10 s now fails with `DeadlineExceededError` where it used to wait; set `timeoutMs` to change it.
|
|
@@ -100,11 +112,11 @@ Not breaking: no type or wire shape an app reads is removed or changed. Apps pin
|
|
|
100
112
|
|
|
101
113
|
### `@north-light/sdk/testing` (additive)
|
|
102
114
|
|
|
103
|
-
New `@north-light/sdk/testing`: `FakeCloud`, a fake directory and runtime your real `OAuth2Client` and `Crouter` talk to through `fetch`. It covers runs (events, replies, long-poll, idempotency, endings), grant watch, refusals, directory failures, memory, providers and run notices, and is checked against the real runtime on every release. Additive: `verifyRunNotice` accepts `fetch`. No behaviour change. See [Testing your app](
|
|
115
|
+
New `@north-light/sdk/testing`: `FakeCloud`, a fake directory and runtime your real `OAuth2Client` and `Crouter` talk to through `fetch`. It covers runs (events, replies, long-poll, idempotency, endings), grant watch, refusals, directory failures, memory, providers and run notices, and is checked against the real runtime on every release. Additive: `verifyRunNotice` accepts `fetch`. No behaviour change. See [Testing your app](/docs/guides/test-your-app).
|
|
104
116
|
|
|
105
117
|
### `oauth.accountAppUrl()` (additive), and a failed discovery read is retried (behaviour fix)
|
|
106
118
|
|
|
107
|
-
- New `OAuth2Client.accountAppUrl()` returns the app's page on the signed-in person's Northlight account, as the directory's discovery document advertises it (`account_app_uri_template`). Use it instead of building `<issuer>/account/apps/<client id>` yourself. It needs no user or connection and throws `CrouterError` naming the fault when the directory advertises no template or an invalid one. See [Link to the app's settings on Northlight](
|
|
119
|
+
- New `OAuth2Client.accountAppUrl()` returns the app's page on the signed-in person's Northlight account, as the directory's discovery document advertises it (`account_app_uri_template`). Use it instead of building `<issuer>/account/apps/<client id>` yourself. It needs no user or connection and throws `CrouterError` naming the fault when the directory advertises no template or an invalid one. See [Link to the app's settings on Northlight](/docs/guides/grant-changes#link-to-the-apps-settings-on-northlight).
|
|
108
120
|
- **Behaviour fix:** `OAuth2Client`'s discovery read now times out after 10 seconds, and a read that failed or timed out is retried by the next call that needs it. Before, one failed read was kept for the client's life, so every later `authorizeUrl`, `exchangeCode`, refresh and `verifyIdToken` failed until the process restarted, and a hung directory stalled them indefinitely. A successful read is still kept for the client's life.
|
|
109
121
|
|
|
110
122
|
## 0.3.405
|
|
@@ -155,25 +167,25 @@ The runtime gains what a sandbox that sleeps needs; no app-facing type or route
|
|
|
155
167
|
|
|
156
168
|
### Follow a person's grant with `oauth.watchGrants` (additive)
|
|
157
169
|
|
|
158
|
-
`grant.get()` answers a new field, `version`: opaque, it changes exactly when `rev`, `resolved_scopes`, `spend_cap`, `payer` or `dependencies` changes. `grant.get({wait, since})` (`wait` an integer 1–25; anything else throws `RangeError` before sending) holds the read until the grant's `version` differs from `since` or `wait` seconds pass, and extends the request timeout by `wait`. A grant revoked or removed during the hold answers 401 `grant_revoked`. `oauth.watchGrants({users, onChanged, onError?, sweepMs?, concurrency?, wait?})` returns a `GrantWatcher` that follows the grants of the people `users()` names and calls `onChanged({userId, grant, previous})` level-triggered, at least once per version, so an app learns when a required provider's setup turns `ready` without a browser tab or a poll loop. New exports `GrantWatcher`, `GrantWatchOptions`, `GrantChanged`. Not breaking: nothing existing changes shape, and `RunWatcher` behaves exactly as before. The hold needs a runtime at this release and, for a hosted runtime, a hosting router that lets `GET /v1/grant?wait=N` through (before then a held read answers 504 after 10 s and the watcher backs off). See [Learn when a person's grant changes](
|
|
170
|
+
`grant.get()` answers a new field, `version`: opaque, it changes exactly when `rev`, `resolved_scopes`, `spend_cap`, `payer` or `dependencies` changes. `grant.get({wait, since})` (`wait` an integer 1–25; anything else throws `RangeError` before sending) holds the read until the grant's `version` differs from `since` or `wait` seconds pass, and extends the request timeout by `wait`. A grant revoked or removed during the hold answers 401 `grant_revoked`. `oauth.watchGrants({users, onChanged, onError?, sweepMs?, concurrency?, wait?})` returns a `GrantWatcher` that follows the grants of the people `users()` names and calls `onChanged({userId, grant, previous})` level-triggered, at least once per version, so an app learns when a required provider's setup turns `ready` without a browser tab or a poll loop. New exports `GrantWatcher`, `GrantWatchOptions`, `GrantChanged`. Not breaking: nothing existing changes shape, and `RunWatcher` behaves exactly as before. The hold needs a runtime at this release and, for a hosted runtime, a hosting router that lets `GET /v1/grant?wait=N` through (before then a held read answers 504 after 10 s and the watcher backs off). See [Learn when a person's grant changes](/docs/guides/grant-changes#follow-grants-with-watchgrants).
|
|
159
171
|
|
|
160
172
|
## 0.3.399
|
|
161
173
|
|
|
162
174
|
### `onActivity` no longer blocks the watcher
|
|
163
175
|
|
|
164
|
-
In 0.3.398 the watcher awaited `onActivity` before its next read, so a slow or hung hook delayed that run's `onEnded`, held its `concurrency` slot, and kept `stop()` from resolving. The watcher now never awaits it: at most one call per run runs at a time, a newer reading taken meanwhile replaces any still waiting, and `stop()` does not wait for a call already running. A call can now finish after that run's `onEnded`: guard the write on the row still waiting on the run. Runs that leave `pending()` without an ending no longer keep their last phase in memory. See [Learn when a background run ends](
|
|
176
|
+
In 0.3.398 the watcher awaited `onActivity` before its next read, so a slow or hung hook delayed that run's `onEnded`, held its `concurrency` slot, and kept `stop()` from resolving. The watcher now never awaits it: at most one call per run runs at a time, a newer reading taken meanwhile replaces any still waiting, and `stop()` does not wait for a call already running. A call can now finish after that run's `onEnded`: guard the write on the row still waiting on the run. Runs that leave `pending()` without an ending no longer keep their last phase in memory. See [Learn when a background run ends](/docs/guides/background-runs#follow-runs-with-runwatcher).
|
|
165
177
|
|
|
166
178
|
## 0.3.398
|
|
167
179
|
|
|
168
180
|
### `RunWatcher` reports live progress with `onActivity` (additive)
|
|
169
181
|
|
|
170
|
-
`oauth.watchRuns({..., onActivity, activityWait?})` calls `onActivity({userId, runId, activity, run})` with each read of a still-`running` run whose `activity` differs from the last one handed over for that run (compared as JSON: `phase`, `tool.name`, `tool.summary`), so an app can show a progress badge from the watcher it already runs instead of a second `runs.get` per page read. It is never called for a run at rest. With `onActivity` set, running runs are read with `wait: activityWait` (integer 1–25, default 2) rather than 25, because the runtime's long-poll answers early only at rest. Best effort: a throwing `onActivity` goes to `onError` and is not retried, and `onEnded` delivery is unchanged. Not breaking: without `onActivity` the watcher behaves exactly as before. New exported type `RunActivityEvent`. See [Learn when a background run ends](
|
|
182
|
+
`oauth.watchRuns({..., onActivity, activityWait?})` calls `onActivity({userId, runId, activity, run})` with each read of a still-`running` run whose `activity` differs from the last one handed over for that run (compared as JSON: `phase`, `tool.name`, `tool.summary`), so an app can show a progress badge from the watcher it already runs instead of a second `runs.get` per page read. It is never called for a run at rest. With `onActivity` set, running runs are read with `wait: activityWait` (integer 1–25, default 2) rather than 25, because the runtime's long-poll answers early only at rest. Best effort: a throwing `onActivity` goes to `onError` and is not retried, and `onEnded` delivery is unchanged. Not breaking: without `onActivity` the watcher behaves exactly as before. New exported type `RunActivityEvent`. See [Learn when a background run ends](/docs/guides/background-runs#follow-runs-with-runwatcher).
|
|
171
183
|
|
|
172
184
|
## 0.3.397
|
|
173
185
|
|
|
174
186
|
### A runtime older than the SDK is refused before it handles the request
|
|
175
187
|
|
|
176
|
-
The SDK now sends `Crouter-Minimum-Runtime-Version: <SDK_VERSION>` on every request except `/healthz`. The hosting router in front of a hosted runtime, and a runtime at this release or later, answer a request that names a newer release than the runtime's with `RuntimeVersionError` (`status` 426, `code` `runtime_version_unsupported`) **before any handler runs**: nothing is accepted, no `Idempotency-Key` is recorded, and a retry after the runtime is rolled forward is a first send. Previously an older runtime handled the request and the SDK raised afterwards. Not breaking: no app code changes. An app that worked around the late check, such as a `runs.list` read before every send, can delete it once its runtimes are at or after this release behind a hosting router that has the check. Back off on `RuntimeVersionError` and retry under the same key. A browser app's preflight now lists `crouter-minimum-runtime-version` in `Access-Control-Allow-Headers`. See [Errors](
|
|
188
|
+
The SDK now sends `Crouter-Minimum-Runtime-Version: <SDK_VERSION>` on every request except `/healthz`. The hosting router in front of a hosted runtime, and a runtime at this release or later, answer a request that names a newer release than the runtime's with `RuntimeVersionError` (`status` 426, `code` `runtime_version_unsupported`) **before any handler runs**: nothing is accepted, no `Idempotency-Key` is recorded, and a retry after the runtime is rolled forward is a first send. Previously an older runtime handled the request and the SDK raised afterwards. Not breaking: no app code changes. An app that worked around the late check, such as a `runs.list` read before every send, can delete it once its runtimes are at or after this release behind a hosting router that has the check. Back off on `RuntimeVersionError` and retry under the same key. A browser app's preflight now lists `crouter-minimum-runtime-version` in `Access-Control-Allow-Headers`. See [Errors](/docs/errors).
|
|
177
189
|
|
|
178
190
|
## 0.3.396
|
|
179
191
|
|
|
@@ -187,7 +199,7 @@ The SDK now sends `Crouter-Minimum-Runtime-Version: <SDK_VERSION>` on every requ
|
|
|
187
199
|
|
|
188
200
|
### `OAuth2Client` owns the connection store; one `onRefusal` for a dead connection (breaking)
|
|
189
201
|
|
|
190
|
-
`OAuth2Client` now takes the app's `ConnectionStore` once, as `store` (required), plus an optional `onRefusal(refusal)` handler, and every refused refresh goes through that one handler: a request on a connected client, `refreshIdleConnections`, and the new `RunWatcher`. Apps no longer keep their own refusal table or delete dead connections themselves. See [When a refresh is refused](
|
|
202
|
+
`OAuth2Client` now takes the app's `ConnectionStore` once, as `store` (required), plus an optional `onRefusal(refusal)` handler, and every refused refresh goes through that one handler: a request on a connected client, `refreshIdleConnections`, and the new `RunWatcher`. Apps no longer keep their own refusal table or delete dead connections themselves. See [When a refresh is refused](/docs/concepts/connections#when-a-refresh-is-refused).
|
|
191
203
|
|
|
192
204
|
- **`oauth.client(connection, store)` is now `await oauth.client(userId)`.** It loads the stored connection and returns a `ConnectedCrouter` (a `Crouter` with `userId` and `runtimeUrl`). Nothing stored throws `CrouterError` code `not_connected`. Save `exchangeCode`'s connection with `store.save(connection)` first.
|
|
193
205
|
- **`oauth.refreshIdleConnections(store, {olderThanMs})` is now `oauth.refreshIdleConnections({olderThanMs})`** and returns `{refreshed, refused, failures}`. `RefreshFailure.reason` is removed: a refused connection is handled by `onRefusal`, removed, and counted in `refused`; `failures` holds only connections still stored (any other error, or a refusal whose handler threw).
|
|
@@ -201,25 +213,25 @@ The SDK now sends `Crouter-Minimum-Runtime-Version: <SDK_VERSION>` on every requ
|
|
|
201
213
|
|
|
202
214
|
### `RunWatcher`: learn when a background run ends (additive)
|
|
203
215
|
|
|
204
|
-
`oauth.watchRuns({pending, onEnded, onError?, sweepMs?, concurrency?})` returns a `RunWatcher` (`start()`, `nudge()`, `stop()`) that follows the runs `pending()` reads from the app's own rows with `runs.get(wait)` and calls `onEnded({userId, runId, ended, run})` when each comes to rest (`settled`, `idle`, `waiting_on_user`) or is deleted (`gone`, only for a 404 `not_found` from the runtime, never the router). Delivery is at least once per `(runId, status, latest_sequence)`: a delivered run is re-read once per sweep and delivered again only when that key changes (an `idle` run later canceled is delivered as `settled`). A throwing `onEnded` is retried after 5 s, doubling to 5 min. Runs are opened with `oauth.client(userId)`, so a refused connection reaches `onRefusal`. There is no runtime-to-app webhook: the runtime cannot mint an app-audience token, a directory relay would add a hop, and the app's rows already make a pull durable and work on localhost. See [Learn when a background run ends](
|
|
216
|
+
`oauth.watchRuns({pending, onEnded, onError?, sweepMs?, concurrency?})` returns a `RunWatcher` (`start()`, `nudge()`, `stop()`) that follows the runs `pending()` reads from the app's own rows with `runs.get(wait)` and calls `onEnded({userId, runId, ended, run})` when each comes to rest (`settled`, `idle`, `waiting_on_user`) or is deleted (`gone`, only for a 404 `not_found` from the runtime, never the router). Delivery is at least once per `(runId, status, latest_sequence)`: a delivered run is re-read once per sweep and delivered again only when that key changes (an `idle` run later canceled is delivered as `settled`). A throwing `onEnded` is retried after 5 s, doubling to 5 min. Runs are opened with `oauth.client(userId)`, so a refused connection reaches `onRefusal`. There is no runtime-to-app webhook: the runtime cannot mint an app-audience token, a directory relay would add a hop, and the app's rows already make a pull durable and work on localhost. See [Learn when a background run ends](/docs/guides/background-runs#follow-runs-with-runwatcher).
|
|
205
217
|
|
|
206
218
|
## 0.3.394
|
|
207
219
|
|
|
208
220
|
### `grant.get` dependencies are one `{ state, message?, next }` entry per provider (breaking)
|
|
209
221
|
|
|
210
|
-
`GrantGetDTO.dependencies[provider]` was a bare `SetupState` string; it is now a `ProviderDependency` (exported from `@north-light/sdk` and `@north-light/api`): `{ state, message?, next }`. `state` is the setup state the string used to be. `message` is the provider's own sentence for `pending` and `failed` (its `message_pending` / `message_failed`), so delete any copy of a provider's prose your app hard-codes and show `message` instead. `next` is the absolute URL of the directory's setup page for that provider and your app; link the person there (append `return_to` with your registered redirect origin) instead of building the URL yourself. **Change every read of `dependencies[p]` to `dependencies[p].state`** (or `dependencies[p]?.state` where the entry may be missing), and carry `message` and `next` into your `provider_setup_pending` details. The runtime reads only this shape from the directory's grant sync: a runtime on this release paired with a directory that still sends bare strings reports `dependencies` as `{}`, so roll the directory change with it. See [Waiting on a provider your app requires](
|
|
222
|
+
`GrantGetDTO.dependencies[provider]` was a bare `SetupState` string; it is now a `ProviderDependency` (exported from `@north-light/sdk` and `@north-light/api`): `{ state, message?, next }`. `state` is the setup state the string used to be. `message` is the provider's own sentence for `pending` and `failed` (its `message_pending` / `message_failed`), so delete any copy of a provider's prose your app hard-codes and show `message` instead. `next` is the absolute URL of the directory's setup page for that provider and your app; link the person there (append `return_to` with your registered redirect origin) instead of building the URL yourself. **Change every read of `dependencies[p]` to `dependencies[p].state`** (or `dependencies[p]?.state` where the entry may be missing), and carry `message` and `next` into your `provider_setup_pending` details. The runtime reads only this shape from the directory's grant sync: a runtime on this release paired with a directory that still sends bare strings reports `dependencies` as `{}`, so roll the directory change with it. See [Waiting on a provider your app requires](/docs/guides/grant-changes#wait-on-a-provider-your-app-requires).
|
|
211
223
|
|
|
212
224
|
## 0.3.393
|
|
213
225
|
|
|
214
226
|
### The runtime orders grant state by revision; a failed grant check is `service_unavailable` (breaking)
|
|
215
227
|
|
|
216
|
-
A runtime on this release requires the `rev` claim in every app access token, so it needs a directory that issues it; a token without `rev` is refused 401 `unauthorized`, "access token has no rev claim: the directory is older than this runtime". A freshly consented or re-consented grant is now authorized on the first call, with no wait: the runtime pulls grant state once whenever a token is newer than its mirror. **Delete any retry your app added for a 401 `grant_revoked` right after consent.** When that pull fails, the call answers 503 `service_unavailable` (`retryable: true`) instead of 401 `grant_revoked`, so a directory outage no longer looks like a refused connection. `service_unavailable` was already in `errorCodes`; app calls can now receive it. See [An app connection and refresh](
|
|
228
|
+
A runtime on this release requires the `rev` claim in every app access token, so it needs a directory that issues it; a token without `rev` is refused 401 `unauthorized`, "access token has no rev claim: the directory is older than this runtime". A freshly consented or re-consented grant is now authorized on the first call, with no wait: the runtime pulls grant state once whenever a token is newer than its mirror. **Delete any retry your app added for a 401 `grant_revoked` right after consent.** When that pull fails, the call answers 503 `service_unavailable` (`retryable: true`) instead of 401 `grant_revoked`, so a directory outage no longer looks like a refused connection. `service_unavailable` was already in `errorCodes`; app calls can now receive it. See [An app connection and refresh](/docs/concepts/connections).
|
|
217
229
|
|
|
218
230
|
## 0.3.391
|
|
219
231
|
|
|
220
232
|
### The SDK refuses a runtime older than itself (breaking)
|
|
221
233
|
|
|
222
|
-
`@north-light/sdk` now accepts only a runtime at or above its own version, and fails loudly otherwise instead of handing an app data its types do not describe. Every runtime answer carries `Crouter-Runtime-Version`; when that version is older than `SDK_VERSION`, or the header is absent (every runtime before this release), the call throws `RuntimeVersionError` (`code: 'runtime_version_unsupported'`, `status: 426`, `retryable: false`) whose message and `sdkVersion` / `runtimeVersion` name both versions. A refusal without the header (a router in front of the runtime answering 401 or 503) is still raised as that refusal. There is no capability negotiation: `RunObject.activity` and every other field are typed for a runtime at this release. **Upgrade every runtime the app talks to before upgrading the SDK**, and pin the SDK to an exact version. A test double that fakes the runtime with `fetch` must send `Crouter-Runtime-Version` at the SDK's version on its 2xx answers. See [Errors](
|
|
234
|
+
`@north-light/sdk` now accepts only a runtime at or above its own version, and fails loudly otherwise instead of handing an app data its types do not describe. Every runtime answer carries `Crouter-Runtime-Version`; when that version is older than `SDK_VERSION`, or the header is absent (every runtime before this release), the call throws `RuntimeVersionError` (`code: 'runtime_version_unsupported'`, `status: 426`, `retryable: false`) whose message and `sdkVersion` / `runtimeVersion` name both versions. A refusal without the header (a router in front of the runtime answering 401 or 503) is still raised as that refusal. There is no capability negotiation: `RunObject.activity` and every other field are typed for a runtime at this release. **Upgrade every runtime the app talks to before upgrading the SDK**, and pin the SDK to an exact version. A test double that fakes the runtime with `fetch` must send `Crouter-Runtime-Version` at the SDK's version on its 2xx answers. See [Errors](/docs/errors).
|
|
223
235
|
|
|
224
236
|
### One release is one version set
|
|
225
237
|
|
|
@@ -247,7 +259,7 @@ A runtime on this release requires the `rev` claim in every app access token, so
|
|
|
247
259
|
|
|
248
260
|
### `runs.get` reports what a running run is doing (additive)
|
|
249
261
|
|
|
250
|
-
`RunObject.activity` is `{phase: 'starting' | 'thinking' | 'writing' | 'between_turns'}` or `{phase: 'tool', tool: {name, summary}}` while the run is `running`, and `null` otherwise, so a polling app can show progress from `runs.get` without reading `runs.trace`. `runs.list` entries carry it too. See [Showing a background run's progress](
|
|
262
|
+
`RunObject.activity` is `{phase: 'starting' | 'thinking' | 'writing' | 'between_turns'}` or `{phase: 'tool', tool: {name, summary}}` while the run is `running`, and `null` otherwise, so a polling app can show progress from `runs.get` without reading `runs.trace`. `runs.list` entries carry it too. See [Showing a background run's progress](/docs/guides/typed-results#showing-progress). A test double that builds a `RunObject` literal must add `activity`.
|
|
251
263
|
|
|
252
264
|
## 0.3.374
|
|
253
265
|
|
|
@@ -259,7 +271,7 @@ A sign-in that grants the `profile` scope returns `name`, `givenName`, and `fami
|
|
|
259
271
|
|
|
260
272
|
### `CrouterError.code` is typed `SdkErrorCode`
|
|
261
273
|
|
|
262
|
-
Every code the SDK raises itself is now listed in `sdkErrorCodes` (exported with `SdkErrorCode` and `mapError` from `@north-light/sdk/errors`), and `CrouterError`'s `code` and constructor are typed with it. Runtime behavior is unchanged. **An app that constructs `new CrouterError(message, code)` with a code of its own, for example in a test double, no longer typechecks:** throw its own error class or an `APIError` instead. See [Errors](
|
|
274
|
+
Every code the SDK raises itself is now listed in `sdkErrorCodes` (exported with `SdkErrorCode` and `mapError` from `@north-light/sdk/errors`), and `CrouterError`'s `code` and constructor are typed with it. Runtime behavior is unchanged. **An app that constructs `new CrouterError(message, code)` with a code of its own, for example in a test double, no longer typechecks:** throw its own error class or an `APIError` instead. See [Errors](/docs/errors).
|
|
263
275
|
|
|
264
276
|
### Run ergonomics (additive)
|
|
265
277
|
|
|
@@ -275,7 +287,7 @@ When the directory refuses a connection's refresh token, the SDK now raises the
|
|
|
275
287
|
- `grant_suspended`: your app is paused for that person. Nothing was deleted; keep their data.
|
|
276
288
|
- `refresh_token_expired`: the refresh token went 90 days unused. The grant is still active; reconnecting skips the consent page.
|
|
277
289
|
|
|
278
|
-
Each is a 401 `AuthenticationError` with `retryable: false`, `userAction: 'reconnect'`, and `origin: 'directory'`, and the connected client from `oauth.client(...)` stops refreshing after it, as it already did for `grant_revoked` and `refresh_token_reused`. `errorCodes` and the `ErrorCode` type gain the three codes. The table of codes and app actions is in [When a refresh is refused](
|
|
290
|
+
Each is a 401 `AuthenticationError` with `retryable: false`, `userAction: 'reconnect'`, and `origin: 'directory'`, and the connected client from `oauth.client(...)` stops refreshing after it, as it already did for `grant_revoked` and `refresh_token_reused`. `errorCodes` and the `ErrorCode` type gain the three codes. The table of codes and app actions is in [When a refresh is refused](/docs/concepts/connections#when-a-refresh-is-refused).
|
|
279
291
|
|
|
280
292
|
**Behavior changes for existing apps:**
|
|
281
293
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.473",
|
|
4
4
|
"description": "crtr — agent runtime with memory, plugins, and marketplaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"activate:pull": "git pull && npm install && npm run activate",
|
|
94
94
|
"build:api": "npm run build:identity && tsc -p packages/crouter-api/tsconfig.json",
|
|
95
95
|
"build:env-docker": "tsc -p packages/crouter-env-docker/tsconfig.json",
|
|
96
|
-
"build:sdk": "npm run build:api && npm run build:identity &&
|
|
96
|
+
"build:sdk": "npm run build:api && npm run build:identity && npm run build --workspace packages/crouter-sdk",
|
|
97
97
|
"build:plugin": "tsc -p packages/crouter-plugin/tsconfig.json",
|
|
98
98
|
"typecheck:pi-packages": "npx tsc -p src/builtin-pi-packages/pi-crtr-extensions/tsconfig.json",
|
|
99
99
|
"test:runtime-packaging": "node --test scripts/runtime-smoke.test.mjs scripts/pack-args.test.mjs scripts/ensure-native-linux.test.mjs scripts/release-version.test.mjs",
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
},
|
|
138
138
|
"license": "UNLICENSED",
|
|
139
139
|
"dependencies": {
|
|
140
|
-
"@north-light/identity": "0.3.
|
|
140
|
+
"@north-light/identity": "0.3.473",
|
|
141
141
|
"@earendil-works/pi-agent-core": "1.1.0",
|
|
142
142
|
"@earendil-works/pi-ai": "1.1.0",
|
|
143
143
|
"@earendil-works/pi-coding-agent": "1.1.0",
|
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.473",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/cli",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.473",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "UNLICENSED",
|
|
12
12
|
"workspaces": [
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"@earendil-works/pi-ai": "1.1.0",
|
|
19
19
|
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
20
20
|
"@earendil-works/pi-tui": "1.1.0",
|
|
21
|
-
"@north-light/identity": "0.3.
|
|
21
|
+
"@north-light/identity": "0.3.473",
|
|
22
22
|
"cron-parser": "^5.6.0",
|
|
23
23
|
"esbuild": "^0.27.7",
|
|
24
24
|
"jose": "^6.2.12",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
"name": "@north-light/docs",
|
|
51
51
|
"version": "0.0.0",
|
|
52
52
|
"dependencies": {
|
|
53
|
+
"@fontsource-variable/geist": "^5.3.0",
|
|
54
|
+
"@fontsource-variable/geist-mono": "^5.3.0",
|
|
53
55
|
"@fontsource-variable/inter": "^5.3.0",
|
|
54
56
|
"@tanstack/react-router": "^1.170.41",
|
|
55
57
|
"@tanstack/react-start": "^1.168.60",
|
|
@@ -2469,6 +2471,24 @@
|
|
|
2469
2471
|
"integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
|
|
2470
2472
|
"license": "MIT"
|
|
2471
2473
|
},
|
|
2474
|
+
"node_modules/@fontsource-variable/geist": {
|
|
2475
|
+
"version": "5.3.0",
|
|
2476
|
+
"resolved": "https://registry.npmjs.org/@fontsource-variable/geist/-/geist-5.3.0.tgz",
|
|
2477
|
+
"integrity": "sha512-j0m+vLQuG5XAYoHtGCVu0spvlGreR3EzpECUVzkFmI1mTVnAO38l/NEPDCFgZ177JxzYJCLSmTQibIiYPilGrA==",
|
|
2478
|
+
"license": "OFL-1.1",
|
|
2479
|
+
"funding": {
|
|
2480
|
+
"url": "https://github.com/sponsors/ayuhito"
|
|
2481
|
+
}
|
|
2482
|
+
},
|
|
2483
|
+
"node_modules/@fontsource-variable/geist-mono": {
|
|
2484
|
+
"version": "5.3.0",
|
|
2485
|
+
"resolved": "https://registry.npmjs.org/@fontsource-variable/geist-mono/-/geist-mono-5.3.0.tgz",
|
|
2486
|
+
"integrity": "sha512-vBbuwDEo9AkrqADMXOrlAR3DFcJi4/JxeuU43FoiQERnNwsfXNnvxvReZG02cQKmyk4DZkZdBZX3oTDvy2zBAw==",
|
|
2487
|
+
"license": "OFL-1.1",
|
|
2488
|
+
"funding": {
|
|
2489
|
+
"url": "https://github.com/sponsors/ayuhito"
|
|
2490
|
+
}
|
|
2491
|
+
},
|
|
2472
2492
|
"node_modules/@fontsource-variable/inter": {
|
|
2473
2493
|
"version": "5.3.0",
|
|
2474
2494
|
"resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz",
|
|
@@ -12095,17 +12115,17 @@
|
|
|
12095
12115
|
},
|
|
12096
12116
|
"packages/crouter-api": {
|
|
12097
12117
|
"name": "@north-light/api",
|
|
12098
|
-
"version": "0.3.
|
|
12118
|
+
"version": "0.3.473",
|
|
12099
12119
|
"license": "UNLICENSED"
|
|
12100
12120
|
},
|
|
12101
12121
|
"packages/crouter-env-docker": {
|
|
12102
12122
|
"name": "@north-light/env-docker",
|
|
12103
|
-
"version": "0.3.
|
|
12123
|
+
"version": "0.3.473",
|
|
12104
12124
|
"license": "UNLICENSED"
|
|
12105
12125
|
},
|
|
12106
12126
|
"packages/crouter-identity": {
|
|
12107
12127
|
"name": "@north-light/identity",
|
|
12108
|
-
"version": "0.3.
|
|
12128
|
+
"version": "0.3.473",
|
|
12109
12129
|
"license": "UNLICENSED",
|
|
12110
12130
|
"dependencies": {
|
|
12111
12131
|
"jose": "^6.2.1"
|
|
@@ -12113,20 +12133,20 @@
|
|
|
12113
12133
|
},
|
|
12114
12134
|
"packages/crouter-plugin": {
|
|
12115
12135
|
"name": "@north-light/plugin",
|
|
12116
|
-
"version": "0.3.
|
|
12136
|
+
"version": "0.3.473",
|
|
12117
12137
|
"license": "UNLICENSED",
|
|
12118
12138
|
"dependencies": {
|
|
12119
|
-
"@north-light/api": "0.3.
|
|
12120
|
-
"@north-light/identity": "0.3.
|
|
12139
|
+
"@north-light/api": "0.3.473",
|
|
12140
|
+
"@north-light/identity": "0.3.473"
|
|
12121
12141
|
}
|
|
12122
12142
|
},
|
|
12123
12143
|
"packages/crouter-sdk": {
|
|
12124
12144
|
"name": "@north-light/sdk",
|
|
12125
|
-
"version": "0.3.
|
|
12145
|
+
"version": "0.3.473",
|
|
12126
12146
|
"license": "UNLICENSED",
|
|
12127
12147
|
"dependencies": {
|
|
12128
|
-
"@north-light/api": "0.3.
|
|
12129
|
-
"@north-light/identity": "0.3.
|
|
12148
|
+
"@north-light/api": "0.3.473",
|
|
12149
|
+
"@north-light/identity": "0.3.473",
|
|
12130
12150
|
"jose": "^6.2.1"
|
|
12131
12151
|
},
|
|
12132
12152
|
"bin": {
|