@north-light/cli 0.3.425 → 0.3.427
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/README.md +4 -4
- package/dist/builtin-memory/crouter-sdk/getting-started.md +4 -4
- package/dist/builtin-memory/crouter-sdk/guides/apps-on-crouter-cloud.md +116 -44
- package/dist/clients/attach/viewer.js +1 -1
- package/dist/core/broker-client/transport-relay.js +1 -1
- package/dist/daemon/api/bridge.d.ts +13 -0
- package/dist/daemon/api/bridge.js +3 -3
- package/docs/sdk/README.md +4 -4
- package/docs/sdk/getting-started.md +4 -4
- package/docs/sdk/guides/apps-on-crouter-cloud.md +116 -44
- package/package.json +2 -2
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +12 -12
|
@@ -28,16 +28,16 @@ if (run.kind === 'result') console.log(run.output_parsed.root_cause);
|
|
|
28
28
|
|
|
29
29
|
## Go live with an app key
|
|
30
30
|
|
|
31
|
-
Once you have installed the SDK and tried the local flow in [[crouter-sdk/getting-started]], build against production: [[crouter-sdk/guides/apps-on-crouter-cloud]] takes you from signing up at `https://account.northlight.ai`
|
|
31
|
+
Once you have installed the SDK and tried the local flow in [[crouter-sdk/getting-started]], build against production: [[crouter-sdk/guides/apps-on-crouter-cloud]] takes you from signing up at `https://account.northlight.ai` and joining your laptop (`crtr sys join`) to a running app on your laptop, registered with `crtr cloud apps register`. Generate the key and pull the settings with:
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
34
|
npx @north-light/sdk keygen --app <client id>
|
|
35
35
|
npx @north-light/sdk env --app <client id>
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
`keygen` saves the private JWK to `local/app-key.json` (mode `0600`); keep that file out of git. With `--app`, it offers to add the public key to your registered app through your own `crtr`, as an additional key that never replaces the current one. Otherwise,
|
|
38
|
+
`keygen` saves the private JWK to `local/app-key.json` (mode `0600`); keep that file out of git. With `--app`, it offers to add the public key to your registered app through your own `crtr`, as an additional key that never replaces the current one. Otherwise, add the public JWK printed on stdout with `crtr cloud apps keys add --app-id <client id> --jwk '<public JWK>'`. An app authenticates only with its key; the directory issues no client secret.
|
|
39
39
|
|
|
40
|
-
`env` reads the registration through `crtr cloud apps get` and writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` into `local/app.env` (`--out` picks another file), keeping every other line. Both commands need this machine's `crtr` joined to your Northlight account; on a machine that isn't, they print a join step and exit 2.
|
|
40
|
+
`env` reads the registration through `crtr cloud apps get` and writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` into `local/app.env` (`--out` picks another file), keeping every other line. Both commands need this machine's `crtr` joined to your Northlight account; on a machine that isn't, they print a join step and exit 2. An app is configured from four variables:
|
|
41
41
|
|
|
42
42
|
| Variable | Value |
|
|
43
43
|
|---|---|
|
|
@@ -85,7 +85,7 @@ The directory rejects the key until its public JWK is added to the app. To chang
|
|
|
85
85
|
| [[crouter-sdk/docker]] | `start()`, `attach()`, `connection()` from `@north-light/env-docker` |
|
|
86
86
|
| [[crouter-sdk/testing]] | `FakeCloud` from `@north-light/sdk/testing`: a fake directory and runtime your real `OAuth2Client` and `Crouter` talk to through `fetch` |
|
|
87
87
|
| [[crouter-sdk/release-notes]] | Behavior changes an existing app must act on, newest first |
|
|
88
|
-
| [[crouter-sdk/guides/apps-on-crouter-cloud]] | Building and registering an app against production (
|
|
88
|
+
| [[crouter-sdk/guides/apps-on-crouter-cloud]] | 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 |
|
|
89
89
|
| [[crouter-sdk/guides/apps-with-many-users]] | Per-person runtime connections, app-owned user access, privacy, and spending |
|
|
90
90
|
|
|
91
91
|
## Phases
|
|
@@ -22,12 +22,12 @@ That installs the `crtr` CLI and the `crtrd` daemon. You do not have to start th
|
|
|
22
22
|
## 2. Install the SDK in your application
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
npm i @north-light/sdk
|
|
25
|
+
npm i --save-exact @north-light/sdk@0.3.422
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
One dependency. It re-exports every data type you need, so you do not also install `@north-light/api`.
|
|
29
29
|
|
|
30
|
-
`@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 [[crouter-sdk/errors]] for `RuntimeVersionError`).
|
|
30
|
+
`@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 [[crouter-sdk/errors]] 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 [[crouter-sdk/release-notes]] first.
|
|
31
31
|
|
|
32
32
|
## 3. Node, on the owner's own daemon
|
|
33
33
|
|
|
@@ -176,7 +176,7 @@ An outcome is returned, never thrown — including a decline and a failure. Only
|
|
|
176
176
|
|
|
177
177
|
## Go live with an app key
|
|
178
178
|
|
|
179
|
-
Build against production from your laptop: [[crouter-sdk/guides/apps-on-crouter-cloud]] walks the whole path. In short, sign up at `https://account.northlight.ai`,
|
|
179
|
+
Build against production from your laptop: [[crouter-sdk/guides/apps-on-crouter-cloud]] walks the whole path. In short, sign up at `https://account.northlight.ai`, create a join token at `https://account.northlight.ai/account/machines` and run `crtr sys join --directory https://account.northlight.ai --token <token>` on your laptop, register the app with `crtr cloud apps register` using a `http://localhost:<port>` origin and a `http://localhost:<port>/auth/callback` redirect URI while you develop, then generate the key and pull its settings with:
|
|
180
180
|
|
|
181
181
|
```bash
|
|
182
182
|
npx @north-light/sdk keygen --app <client id>
|
|
@@ -187,7 +187,7 @@ npx @north-light/sdk env --app <client id>
|
|
|
187
187
|
|
|
188
188
|
`env` runs `crtr cloud apps get --app-id <client id>` and writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID`, and `APP_ORIGIN` into `local/app.env` (`--out <path>` writes another file). It updates those three lines in place, keeps every other line and comment, and creates the file with mode `0600` when it doesn't exist. It never writes `CROUTER_KEY_FILE` or anything secret. It prints the values it set and the key file your app will read.
|
|
189
189
|
|
|
190
|
-
Both commands reach your app through this machine's `crtr`, so the machine must be joined to your Northlight account. On one that isn't, they say so, print a join step and exit 2. Against production
|
|
190
|
+
Both commands reach your app through this machine's `crtr`, so the machine must be joined to your Northlight account. On one that isn't, they say so, print a join step and exit 2. Against production, [[crouter-sdk/guides/apps-on-crouter-cloud]] first.
|
|
191
191
|
|
|
192
192
|
An app built on the SDK is configured from four variables:
|
|
193
193
|
|
|
@@ -9,7 +9,7 @@ when-and-why-to-read: When you are building and registering an app on
|
|
|
9
9
|
|
|
10
10
|
# Apps on crouter cloud
|
|
11
11
|
|
|
12
|
-
This page first walks through [building and registering an app](#build-and-register-an-app) against production
|
|
12
|
+
This page first walks through [building and registering an app](#build-and-register-an-app) against production with `crtr cloud apps`, then covers [managing it](#manage-an-app-with-crtr-cloud-apps), then runs.
|
|
13
13
|
|
|
14
14
|
An app holding a person's connection starts runs on their runtime with `runs.start`, follows them with `runs.events` or `runs.reply`, and reads them with `runs.get`. This page covers getting data back from a run. For connections and per-person access, see [[crouter-sdk/guides/apps-with-many-users]].
|
|
15
15
|
|
|
@@ -17,53 +17,100 @@ An app holding a person's connection starts runs on their runtime with `runs.sta
|
|
|
17
17
|
|
|
18
18
|
This part takes you from nothing to an app running on your laptop against Northlight's production directory, `https://account.northlight.ai`. Your app server runs on `localhost`, you sign in to it through Northlight, and it calls your own runtime. There is no local Northlight stack to run.
|
|
19
19
|
|
|
20
|
-
You
|
|
20
|
+
You do all of it from the command line. Your laptop joins your Northlight account as an extra machine, and `crtr cloud apps …` registers and manages the app as you.
|
|
21
21
|
|
|
22
22
|
The example app texts you, so it asks for the `sms` provider's `sms:send` scope. You need Node.js 22.19 or later.
|
|
23
23
|
|
|
24
|
-
### 1.
|
|
24
|
+
### 1. Account and join
|
|
25
25
|
|
|
26
|
-
Open `https://account.northlight.ai/signup`
|
|
26
|
+
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, so use a phone number that has 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.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Install the CLI (`npm i -g @north-light/cli`). Joining needs `crtr` 0.3.422 or later; `crtr --version` shows yours.
|
|
29
|
+
|
|
30
|
+
Then join your laptop to the account:
|
|
31
|
+
|
|
32
|
+
1. Open `https://account.northlight.ai/account/machines` and choose **Create join token**. A token needs a sign-in within the last 10 minutes, so the page asks you to sign in again if yours is older.
|
|
33
|
+
2. The page shows two commands, with the token filled in. On your laptop, run them: first `crtr sys daemon stop` (a running daemon makes the join answer `daemon_running`), then `crtr sys join --directory https://account.northlight.ai --token <token>`. Add `--name <label>` to name the machine on the page. The token works once and expires at the time the page shows; the page shows it only once.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
crtr sys daemon stop
|
|
37
|
+
crtr sys join --directory https://account.northlight.ai --token <token> --name my-laptop
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
It answers with `registered: true`, the machine id (`runtime:<id>`) and your user id.
|
|
41
|
+
3. Check it: `crtr cloud apps mine` lists the apps you manage. Before you have joined, it answers `not_joined` and prints these same steps.
|
|
42
|
+
|
|
43
|
+
Things to know before you join:
|
|
29
44
|
|
|
45
|
+
- **Joining rewrites your crtr home.** Everything in your everyday crtr home that belonged to the local user (`user:local`) becomes your Northlight user, the next time the daemon starts. If you would rather not do that to your everyday home, join from a home you can throw away.
|
|
46
|
+
- **There is no unjoin.** The only undo is removing the machine on `/account/machines`.
|
|
47
|
+
- **Removing the machine stops it.** Once you choose **Remove** next to a machine, it gets no new tokens at once and the ones it holds expire within 5 minutes. After that its `cloud` commands answer `machine_revoked`: "This machine was removed from your Northlight account." It stays that way until you create a new join token and run `crtr sys join` again. Files on the computer stay on it.
|
|
48
|
+
- **Your hosted machine is unchanged.** `/account/machines` lists it as "Northlight (hosted)", the default. Your chat agent at `https://chat.northlight.ai` still runs `crtr cloud apps …` as before.
|
|
49
|
+
|
|
50
|
+
### 2. Install the SDK
|
|
30
51
|
|
|
31
52
|
```bash
|
|
32
53
|
mkdir my-app && cd my-app
|
|
33
54
|
npm init -y
|
|
34
|
-
npm i @north-light/sdk@
|
|
55
|
+
npm i --save-exact @north-light/sdk@0.3.422
|
|
35
56
|
```
|
|
36
57
|
|
|
37
|
-
|
|
58
|
+
`0.3.422` 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 [[crouter-sdk/release-notes]] for the versions in between, and install the new exact version the same way.
|
|
38
59
|
|
|
39
|
-
|
|
60
|
+
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`.
|
|
61
|
+
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.
|
|
40
62
|
|
|
41
|
-
|
|
63
|
+
### 3. Register the app
|
|
42
64
|
|
|
43
65
|
```bash
|
|
44
|
-
|
|
66
|
+
crtr cloud apps register \
|
|
67
|
+
--name "Texty" \
|
|
68
|
+
--origin http://localhost:3000 \
|
|
69
|
+
--logo https://example.com/logo.png \
|
|
70
|
+
--justification "Texts you a summary of your day." \
|
|
71
|
+
--redirect-uri http://localhost:3000/auth/callback
|
|
45
72
|
```
|
|
46
73
|
|
|
47
|
-
|
|
74
|
+
| Flag | Value |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `--name` | Shown to people on the consent page and in their account. |
|
|
77
|
+
| `--origin` | Where your app runs. `http://localhost:<port>` and `http://127.0.0.1:<port>` are accepted for development. |
|
|
78
|
+
| `--logo` | Required. The URL of a square image. |
|
|
79
|
+
| `--justification` | One sentence, quoted on the consent page as your words. |
|
|
80
|
+
| `--redirect-uri` | Repeat the flag for each URI. Sign-in returns only to a URI listed here, matched exactly, so `localhost` and `127.0.0.1` are different URIs and the port counts. |
|
|
48
81
|
|
|
49
|
-
|
|
82
|
+
The answer is the registration: the app id (`app:<client id>`), `verified: false` and `testing: true` (see [Who can sign in: testing mode](#who-can-sign-in-testing-mode)), `key: {"source":"none"}` and an `env` block with `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN`. The client id is the part of the app id after `app:`. `crtr cloud apps get --app-id <client id>` shows it again. Until a key is added, `exchangeCode` throws an `AuthenticationError`.
|
|
50
83
|
|
|
51
|
-
|
|
84
|
+
`--app-id` takes `app:<client id>` or the bare client id everywhere.
|
|
52
85
|
|
|
53
|
-
|
|
54
|
-
|---|---|
|
|
55
|
-
| Name | Shown to people on the consent page and in their account. |
|
|
56
|
-
| Origin URL | Where your app runs, for example `http://localhost:3000`. `http://localhost:<port>` and `http://127.0.0.1:<port>` are accepted for development. |
|
|
57
|
-
| Logo URL | Required. A square image. |
|
|
58
|
-
| Why your app needs access | One sentence, quoted on the consent page as your words. |
|
|
59
|
-
| Redirect URIs | One per line, for example `http://localhost:3000/auth/callback`. Sign-in returns only to a URI listed here, matched exactly, so `localhost` and `127.0.0.1` are different URIs and the port counts. |
|
|
60
|
-
| Public key | Paste the public JWK from step 3. It is optional here; if you leave it empty, add it later with **Add key** on the app's **Sign-in** tab. |
|
|
86
|
+
### 4. Generate the signing key and add it
|
|
61
87
|
|
|
62
|
-
|
|
88
|
+
An app signs in with a key, not a client secret. From your app's folder:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npx @north-light/sdk keygen --app <client id> --yes
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
This writes the private key to `local/app-key.json` (mode `0600`; keep it out of git) and adds its public key to the app through your own `crtr` (`crtr cloud apps keys add`). Without `--yes` it asks first. The answer is "Added key <kid> to <client id> as an additional key. Your current key still works."; `crtr cloud apps keys list --app-id <client id>` then shows the key as `active`. Never paste the private key anywhere.
|
|
63
95
|
|
|
64
96
|
### 5. Say what the app asks for
|
|
65
97
|
|
|
66
|
-
|
|
98
|
+
An app can only ask for scopes it has been given. It starts with `crtr:llm` and `crtr:act`. See what else it may ask for, then set the list:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
crtr cloud apps asks --app-id <client id> --source sms
|
|
102
|
+
crtr cloud apps set-scopes --app-id <client id> --scope crtr:llm --scope crtr:act --scope sms:send
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`asks` lists, per source, what the app may ask for and whether it asks now; `sms:send` is under providers, with the consent sentence "Send you text messages from Northlight's number". `set-scopes` replaces the whole list, so repeat `--scope` for every scope you want (`crtr:act` needs `crtr:llm`). Without `sms:send` the consent page does not offer it and `providers.call` for `sms` is refused. Take scope strings from `asks` and do not invent them.
|
|
106
|
+
|
|
107
|
+
If your app does not work without a provider being set up first (Doppel needs Context), name it:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
crtr cloud apps set-requires --app-id <client id> --provider context
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The person is then taken through that provider's setup page the first time they sign in to your app. Context asks for a profile URL, and its state stays `pending` for a few minutes afterwards; see [Waiting on a provider your app requires](#waiting-on-a-provider-your-app-requires).
|
|
67
114
|
|
|
68
115
|
#### What each scope gates
|
|
69
116
|
|
|
@@ -79,17 +126,19 @@ A run you start can only do what the grant holds and the run's own `scopes` list
|
|
|
79
126
|
|
|
80
127
|
#### Who can sign in: testing mode
|
|
81
128
|
|
|
82
|
-
A new app is in testing: it is registered as `verified: false`, only you and up to 100 testers can sign in to it, and it is not shown in the marketplace. You can sign in as yourself with no setup. To let someone else in, add them by the email or phone number on their Northlight account: `crtr cloud apps testers add --app-id <id> --email them@example.com` (or `--phone +15551234567`). An email matches only while it is verified on that account. They can sign in right away; nobody gets a message, so tell them. `testers list
|
|
83
|
-
|
|
84
|
-
Anyone else who tries sees "<app> is still in testing", and your app's callback receives `error=access_denied` with `error_description=app_in_testing` instead of a `code`. [[crouter-sdk/client]] shows how to read it and what to show the person. Removing a tester stops their access at once, including their refresh tokens, and keeps their data; adding them back and signing in again restores it.
|
|
129
|
+
A new app is in testing: it is registered as `verified: false`, only you and up to 100 testers can sign in to it, and it is not shown in the marketplace. You can sign in as yourself with no setup. To let someone else in, add them by the email or phone number on their Northlight account: `crtr cloud apps testers add --app-id <id> --email them@example.com` (or `--phone +15551234567`). An email matches only while it is verified on that account. They can sign in right away; nobody gets a message, so tell them. (Specified behavior: adding a tester, and everything below about a second person signing in, being refused or being removed, has not been run on production yet.) `crtr cloud apps testers list --app-id <id>` shows who is on the list and `testers remove` takes someone off. A testing app is also left out of the marketplace listing, even once you set `--listed true`.
|
|
85
130
|
|
|
86
|
-
|
|
131
|
+
Per the testing-mode specification, anyone else who tries sees "<app> is still in testing", and your app's callback receives `error=access_denied` with `error_description=app_in_testing` instead of a `code`. [[crouter-sdk/client]] shows how to read it and what to show the person. Removing a tester stops their access at once, including their refresh tokens, and keeps their data; adding them back and signing in again restores it.
|
|
87
132
|
|
|
88
133
|
When you are ready for everyone, email `support@northlight.ai` to have the app verified. Only Northlight staff can verify an app; registering never sets `verified: true`. Apps that existed before testing mode are already verified.
|
|
89
134
|
|
|
90
135
|
### 6. Write the environment file
|
|
91
136
|
|
|
92
|
-
|
|
137
|
+
```bash
|
|
138
|
+
npx @north-light/sdk env --app <client id>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
This reads the registration through `crtr cloud apps get` and writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` into `local/app.env`, keeping every other line and comment in the file. `--out <path>` writes another file. It never writes `CROUTER_KEY_FILE` or anything secret. Add the key path yourself:
|
|
93
142
|
|
|
94
143
|
```bash
|
|
95
144
|
CROUTER_ISSUER=https://account.northlight.ai
|
|
@@ -101,11 +150,11 @@ CROUTER_KEY_FILE=local/app-key.json
|
|
|
101
150
|
| Variable | Value |
|
|
102
151
|
|---|---|
|
|
103
152
|
| `CROUTER_ISSUER` | `https://account.northlight.ai` |
|
|
104
|
-
| `CROUTER_CLIENT_ID` | The
|
|
105
|
-
| `APP_ORIGIN` | The
|
|
106
|
-
| `CROUTER_KEY_FILE` | Path to the private key from step
|
|
153
|
+
| `CROUTER_CLIENT_ID` | The client id of your app |
|
|
154
|
+
| `APP_ORIGIN` | The origin you registered. The redirect URI is `${APP_ORIGIN}/auth/callback` by convention; it must match a registered redirect URI exactly. |
|
|
155
|
+
| `CROUTER_KEY_FILE` | Path to the private key from step 4. Default `local/app-key.json` |
|
|
107
156
|
|
|
108
|
-
The file holds no secret, but keep it out of git with the key.
|
|
157
|
+
The file holds no secret, but keep it out of git with the key.
|
|
109
158
|
|
|
110
159
|
### 7. Run it locally
|
|
111
160
|
|
|
@@ -190,12 +239,9 @@ Test your app with `FakeCloud` from `@north-light/sdk/testing`. Your real `OAuth
|
|
|
190
239
|
|
|
191
240
|
## Manage an app with `crtr cloud apps`
|
|
192
241
|
|
|
193
|
-
Everything
|
|
194
|
-
|
|
195
|
-
- **A laptop cannot run them yet.** The `cloud` commands appear on a machine joined to your Northlight account, and production does not yet let a laptop join as an additional machine. That is coming. Until it ships, do the steps above in the console and write the env file by hand. Do not follow older instructions to open `/account/machines` or to run `crtr sys join` against production: that page does not exist yet. `keygen --app` and `npx @north-light/sdk env` need the same joined machine, so use `keygen` without `--app`, as in step 3.
|
|
196
|
-
- **Your Northlight chat agent can run them.** Open `https://chat.northlight.ai`, and ask your chat agent to run the command. It runs as you on your hosted runtime and prints the real output. Verified on production: `register`, `set-scopes`, `keys add`, `get`, `mine`, `asks`, `set-listing`, `set-requires` and `update`. For `keys add`, generate the key on your laptop as in step 3 and give the agent only the public JWK. Never ask it to generate your key: it would live on the hosted disk. If a command fails once with `launcher refused launch_failed`, run it again.
|
|
242
|
+
Everything above is a `crtr cloud apps` command, and so is the rest of managing an app. They are leaves of Northlight's `cloud` provider, they run as you, and they need a machine joined to your account ([step 1](#1-account-and-join)).
|
|
197
243
|
|
|
198
|
-
|
|
244
|
+
Your Northlight chat agent at `https://chat.northlight.ai` can run the same commands as you, on your hosted runtime. Ask it to run a command and show you the raw output. It can list and change an app (`mine` and `set-scopes` were checked on production). It cannot delete one: see [Deleting an app](#deleting-an-app). Generate keys on your laptop and give an agent only a public JWK, never the private key.
|
|
199
245
|
|
|
200
246
|
`--app-id` takes `app:<client id>` or the bare client id. The leaves:
|
|
201
247
|
|
|
@@ -209,24 +255,50 @@ For example, ask the agent: "Run `crtr cloud apps register --name Texty --origin
|
|
|
209
255
|
| `crtr cloud apps asks --app-id <id>` | Lists what an app may ask for, with whether it asks now. Large sources such as `integrations` print as a count. Narrow it with `--query sms:send`, `--source sms`, `--group providers\|apps\|agents\|space`, or `--query <text>`; for example `crtr cloud apps asks --app-id <id> --source sms`. |
|
|
210
256
|
| `crtr cloud apps set-scopes --app-id <id> --scope …` | Replaces the scope list. Repeat `--scope` for every scope; `--clear` empties it; `crtr:act` needs `crtr:llm`. |
|
|
211
257
|
| `crtr cloud apps set-requires --app-id <id> --provider …` | Names providers your app does not work without (for example `context`); `--clear` empties it. |
|
|
212
|
-
| `crtr cloud apps keys add --app-id <id> --jwk '<public JWK>'` | Adds a signing key beside any existing one. Answers `created: false` if that key is already on the app. |
|
|
213
|
-
| `crtr cloud apps
|
|
214
|
-
| `crtr cloud apps
|
|
258
|
+
| `crtr cloud apps keys add --app-id <id> --jwk '<public JWK>'` | Adds a signing key beside any existing one. Answers `created: false` if that key is already on the app. `keygen --app` calls it for you. |
|
|
259
|
+
| `crtr cloud apps keys list --app-id <id>` | Lists the keys with their `kid` and `active` or `retired` status. |
|
|
260
|
+
| `crtr cloud apps keys retire --app-id <id> --kid <kid>` | Retires a key so it no longer signs in. |
|
|
261
|
+
| `crtr cloud apps set-listing --app-id <id> --login-start-url … --text-welcome …` (also `--listed`, `--price`) | Sets the listing fields; pass at least one. A loopback `--login-start-url` is accepted. `--listed true --price free` lists the app, so `cloud apps install` finds it; while the app is in testing it still stays out of the marketplace list. |
|
|
262
|
+
| `crtr cloud apps install --app-id <id>` | Texts an install link to the signed-in person. If they already hold a grant it answers `status: already_have` and the install URL instead of texting. The app must be listed. |
|
|
263
|
+
| `crtr cloud apps testers add --app-id <id> --email …` (or `--phone …`) | Lets that person sign in while the app is in testing; at most 100. Not yet run on production. |
|
|
215
264
|
| `crtr cloud apps testers list --app-id <id>` | Lists the testers. |
|
|
216
|
-
| `crtr cloud apps testers remove --app-id <id> --email …` (or `--phone …`) | Removes a tester and revokes their grants for the app. |
|
|
265
|
+
| `crtr cloud apps testers remove --app-id <id> --email …` (or `--phone …`) | Removes a tester and revokes their grants for the app. Not yet run on production. |
|
|
217
266
|
| `crtr cloud apps delete --app-id <id> --confirm <client id>` | Deletes the app and removes every person's grant for it. Only you at a joined machine's terminal can run it: an agent, including your chat agent, is refused with `provider_refused`. |
|
|
218
267
|
|
|
219
268
|
### Deleting an app
|
|
220
269
|
|
|
221
|
-
|
|
270
|
+
```bash
|
|
271
|
+
crtr cloud apps delete --app-id <client id> --confirm <client id>
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
It answers `deleted: true` and `removed_grants`, the number of people's grants it removed. Each runtime deletes the app's runs, space and schedules, and the app's next refresh fails with `invalid_grant`. The app's key stops working, the app's page answers 404 and `mine` no longer lists it. It cannot be undone, and apps that hold the provider role cannot be deleted.
|
|
275
|
+
|
|
276
|
+
Only you can delete: run it yourself in the terminal of your joined machine. An agent, including your chat agent, is refused with `provider_refused`: "Only you can delete an app. Ask the person to run `crtr cloud apps delete` in their terminal." Reusing an app for tests is often simpler: change its redirect URIs and keys instead.
|
|
222
277
|
|
|
223
278
|
### Let people find and install it
|
|
224
279
|
|
|
225
|
-
`login_start_url` is where Northlight starts sign-in for a person who installs your app from a link
|
|
280
|
+
`login_start_url` is where Northlight starts sign-in for a person who installs your app from a texted link (specified behavior: pressing the texted link was not run on production, only the text send was). Your `/auth/login` route from step 7 is a valid start URL, and a loopback one is accepted:
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
crtr cloud apps set-listing --app-id <client id> --listed true --price free \
|
|
284
|
+
--login-start-url http://localhost:3000/auth/login \
|
|
285
|
+
--text-welcome "Texts you a summary of your day."
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`--text-welcome` is one or two plain sentences, at most 120 characters, with no links. Agents can install an app for people by text only while it is listed (`--listed true`). `crtr cloud apps install --app-id <client id>` sends the signed-in person a text with the install link. Run it only for yourself or for a person who asked: it sends a real text.
|
|
226
289
|
|
|
227
290
|
### Rotate keys
|
|
228
291
|
|
|
229
|
-
Add a new key beside the old one, switch, verify with a real sign-in, then retire the old key
|
|
292
|
+
Add a new key beside the old one, switch, verify with a real sign-in, then retire the old key:
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
npx @north-light/sdk keygen --out local/app-key.next.json --app <client id> --yes
|
|
296
|
+
# point CROUTER_KEY_FILE at local/app-key.next.json, restart, and sign in once
|
|
297
|
+
crtr cloud apps keys list --app-id <client id>
|
|
298
|
+
crtr cloud apps keys retire --app-id <client id> --kid <old kid>
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
The old key keeps working until you retire it, and your app's refreshes keep working after. [[crouter-sdk/getting-started]] has the full sequence.
|
|
230
302
|
|
|
231
303
|
## Getting a result back from a run
|
|
232
304
|
|
|
@@ -1852,7 +1852,7 @@ Dropped: ${u.dropped}`}}}}var PIn=h(()=>{"use strict";Gt();BD();$D();LD();wU()})
|
|
|
1852
1852
|
${m}
|
|
1853
1853
|
`)}if(f==="node-done"){process.stderr.write(`
|
|
1854
1854
|
\u2713 node ${i} finished \u2014 final report submitted.
|
|
1855
|
-
`);let m=e.tagPane()??process.env.TMUX_PANE??my()?.pane;!o&&m!==void 0&&ua()&&J0(m)}u()}};return{done:l,teardown:d,isTornDown:()=>c,installSignalHandlers:()=>{let f=()=>d("signal");process.once("SIGTERM",f),process.once("SIGHUP",f),process.once("SIGINT",f)}}}var pkn=h(()=>{"use strict";Ap();wye()});var I0,xye=h(()=>{"use strict";I0=class extends Error{constructor(n){super(`node ${n} has no running broker \u2014 focus or revive it first`);this.nodeId=n;this.name="BrokerUnavailableError"}nodeId}});import{EventEmitter as vxi}from"node:events";var _U,fet=h(()=>{"use strict";Fr();dK();Uhe();Kwe();Sh();TYe();xye();_U=class extends vxi{constructor(n,r={}){super();this.nodeId=n;this.options=r}nodeId;options;socket;decoder=new d6(IYe);closeEmitted=!1;redialFaultRecorded=!1;get socketPath(){return qM(this.nodeId)}recordSocketFault(n,r){if(this.options.recordsViewerFaults===!1)return;if(n==="redial"){if(this.redialFaultRecorded)return;this.redialFaultRecorded=!0}let i=Fhe("viewer\u2194broker",r);Cat(this.nodeId,{link:"viewer\u2194broker",op:n,kind:i.kind,retry:n==="redial"?{disposition:"auto",by:"client"}:{disposition:"manual"},message:r instanceof Error?r.message:String(r)}).catch(o=>{dc({event:"broker.daemon.operation.failed",level:"error",node_id:this.nodeId,error:o,fields:{operation:"viewer_fault_record"}})})}connect(){let n=ybe(this.nodeId);this.socket=n,n.on("connect",()=>{this.redialFaultRecorded=!1,this.clearSocketFault(),this.emit("connect")}),n.on("data",r=>this.onData(r)),n.on("error",r=>this.onError(r,"connect")),n.on("close",()=>this.onClose())}redial(n={}){return this.destroy(),this.decoder=new d6(IYe),this.closeEmitted=!1,n.brokerUnavailableIsExpected&&(this.redialFaultRecorded=!1,this.clearSocketFault()),new Promise((r,i)=>{let o=ybe(this.nodeId);this.socket=o;let s=!1;o.on("connect",()=>{s||(s=!0,this.redialFaultRecorded=!1,this.clearSocketFault(),this.emit("connect"),r())}),o.on("data",a=>this.onData(a)),o.on("error",a=>{if(s){this.onError(a,"redial");return}s=!0;let c=a.code==="ECONNREFUSED"||a.code==="ENOENT";(!n.brokerUnavailableIsExpected||!c)&&this.recordSocketFault("redial",a);try{o.destroy()}catch{}i(a)}),o.on("close",()=>this.onClose())})}send(n){let r=this.socket;if(!(r===void 0||r.destroyed))try{r.write(Jme(n))}catch{}}destroy(){let n=this.socket;if(n!==void 0&&!n.destroyed)try{n.destroy()}catch{}}clearSocketFault(){this.options.recordsViewerFaults!==!1&&Iat(this.nodeId,{link:"viewer\u2194broker"}).catch(n=>{dc({event:"broker.daemon.operation.failed",level:"error",node_id:this.nodeId,error:n,fields:{operation:"viewer_fault_clear"}})})}onData(n){let r;try{r=this.decoder.push(n)}catch(i){let o=i instanceof BF?`broker sent an oversized frame (${i.message}) \u2014 disconnecting`:`failed to decode a broker frame: ${String(i)}`;this.emitError(new Error(o)),this.destroy();return}for(let i of r)this.emit("frame",i)}onError(n,r){this.recordSocketFault(r,n),n.code==="ECONNREFUSED"||n.code==="ENOENT"?this.emitError(new I0(this.nodeId)):this.emitError(new Error(`view socket error: ${n.message}`))}onClose(){this.closeEmitted||(this.closeEmitted=!0,this.emit("close"))}emitError(n){this.listenerCount("error")>0&&this.emit("error",n)}}});import{EventEmitter as Sxi}from"node:events";function Cxi(t,e){if("socketPath"in t)return`ws+unix://${t.socketPath}:/v1/nodes/${encodeURIComponent(e)}/attach?revive=0`;let n=new URL(t.previewEndpoint);return n.protocol=n.protocol==="http:"?"ws:":"wss:",n.pathname=`/v1/nodes/${encodeURIComponent(e)}/attach`,n.search="?wakeParked=1",n.hash="",n.toString()}var QD,fkn=h(()=>{"use strict";OFe();Uhe();xye();QD=class extends Sxi{constructor(n,r){super();this.nodeId=n;this.target=r}nodeId;target;ws;closeEmitted=!1;get url(){return Cxi(this.target,this.nodeId)}dial(){let n=new Fk.default(this.url,{..."relayToken"in this.target?{headers:{authorization:`Bearer ${this.target.relayToken}`}}:{},perMessageDeflate:!1});return this.ws=n,n}connect(){let n=this.dial();n.on("open",()=>{this.emit("connect")}),n.on("message",r=>this.onMessage(r)),n.on("error",r=>this.onError(r)),n.on("close",(r,i)=>this.onClose(r,i.toString()))}redial(n){return this.destroy(),this.closeEmitted=!1,new Promise((r,i)=>{let o=this.dial(),s=!1;o.on("open",()=>{s||(s=!0,this.emit("connect"),r())}),o.on("message",a=>this.onMessage(a)),o.on("error",a=>{if(s){this.onError(a);return}s=!0;try{o.terminate()}catch{}i(a)}),o.on("close",(a,c)=>{s&&this.onClose(a,c.toString())})})}send(n){let r=this.ws;if(!(r===void 0||r.readyState!==Fk.default.OPEN))try{r.send(JSON.stringify(n))}catch{}}destroy(){let n=this.ws;if(n!==void 0&&n.readyState<=Fk.default.OPEN)try{n.close()}catch{}}onMessage(n){let r;try{r=JSON.parse(n.toString())}catch{return}this.emit("frame",r)}onError(n){this.emitError(new Error(`relay transport error: ${n.message}`))}onClose(n,r){if(this.closeEmitted)return;this.closeEmitted=!0;let i=Fhe("viewer\u2194broker",{code:n,reason:r});(n===1008||i.kind==="protocol"||n===1011&&(/^no running broker/.test(r)||/^no node /.test(r)))&&this.emitError(new I0(this.nodeId)),this.emit("close")}emitError(n){this.listenerCount("error")>0&&this.emit("error",n)}}});import{EventEmitter as Ixi}from"node:events";import{randomUUID as Txi}from"node:crypto";function met(t){return t===null?!0:t.lifecycle==="resident"&&t.status==="done"&&t.intent==="parked"&&t.final_report==null?!1:t.status==="done"||t.status==="dead"||t.status==="canceled"}var mkn,nV,hkn=h(()=>{"use strict";fet();xye();fet();fkn();mkn=1e4;nV=class extends Ixi{constructor(n,r={}){super();this.nodeId=n;this.transport=r.transport??new _U(n),this.transport.on("connect",()=>{this.closeEmitted=!1,this.connected=!0,this.lastTransportError=void 0,this.emit("connect")}),this.transport.on("frame",i=>this.onFrame(i)),this.transport.on("error",i=>{this.lastTransportError=i.message,this.emitError(i)}),this.transport.on("close",()=>this.onClose())}nodeId;transport;closeEmitted=!1;connected=!1;lastTransportError;pending=new Map;request(n){let r=Txi();return new Promise((i,o)=>{if(!this.connected){o(new Error(`cannot issue '${n.type}': no live broker connection`));return}let s=setTimeout(()=>{this.pending.delete(r),o(new Error(`request '${n.type}' timed out after ${mkn}ms`))},mkn);typeof s.unref=="function"&&s.unref(),this.pending.set(r,{resolve:i,reject:o,timer:s}),this.send({...n,id:r})})}rejectAllPending(n){for(let{reject:r,timer:i}of this.pending.values())clearTimeout(i),r(new Error(n));this.pending.clear()}connect(){this.transport.connect()}redial(n){return this.transport.redial(n)}send(n){this.transport.send(n)}close(){this.transport.destroy()}onFrame(n){if(n.type==="data"){let r=typeof n.id=="string"?this.pending.get(n.id):void 0;r&&(this.pending.delete(n.id),clearTimeout(r.timer),r.resolve(n));return}if(n.type==="error"&&typeof n.id=="string"&&this.pending.has(n.id)){let r=this.pending.get(n.id);this.pending.delete(n.id),clearTimeout(r.timer),r.reject(new Error(n.message||`request failed: ${n.code}`));return}this.emit("frame",n)}onClose(){this.closeEmitted||(this.closeEmitted=!0,this.connected=!1,this.rejectAllPending(this.lastTransportError??"broker connection closed"),this.emit("close"))}emitError(n){this.listenerCount("error")>0&&this.emit("error",n)}}});var het=h(()=>{"use strict";hkn()});function gkn(t,e){let{nodeId:n,socket:r,canvasSource:i}=t,o,s=!1,a=e.initialDormant===!0,c=!1,u=!1,l=()=>{},d,p,f=new Promise(x=>{l=x}),m=x=>u?Promise.resolve(!1):new Promise(E=>{let _=C=>{d=void 0,p=void 0,E(C)};d=setTimeout(()=>_(!0),x),p=()=>{d!==void 0&&clearTimeout(d),_(!1)}}),g=()=>{r.send({type:"hello",role:e.role,client_id:e.clientId,term:{cols:process.stdout.columns??80,rows:process.stdout.rows??24}})},y=async()=>{s=!0,e.setNotice("\u21BB node refreshing \u2014 reconnecting\u2026",{sticky:!0});let x=200;try{for(;!u&&!e.isTornDown();){let E;try{E=await i.getRow(n)}catch{E=void 0}if(E!==void 0&&met(E)){c=E?.status==="done";break}let _=E?.status==="idle"||E?.intent==="idle-release"||E?.lifecycle==="resident"&&E.status==="done"&&E.intent==="parked"&&E.final_report==null,C=_&&!a;a=_,C&&e.onEnterDormancy(),e.setNotice(_?"":"\u21BB node refreshing \u2014 reconnecting\u2026",{sticky:!_});try{if(!await Promise.race([r.redial({brokerUnavailableIsExpected:_}).then(()=>!0),f.then(()=>!1)])||u||e.isTornDown())return;g(),a=!1,e.setNotice("");return}catch{if(!await m(_?Rxi:Math.min(x,kxi)))return;x*=2}}}finally{s=!1}!u&&!e.isTornDown()&&e.teardown(c?"node-done":"broker-gone")};return{sendHello:g,startDormant:()=>{s||u||e.isTornDown()||(e.onEnterDormancy(),y())},isDormant:()=>a,lastSocketError:()=>o,install:()=>{r.on("frame",e.onFrame),r.on("error",x=>{o=x.message}),r.on("close",()=>{e.isTornDown()||s||y()})},stop:()=>{u||(u=!0,p?.(),l())}}}var kxi,Rxi,ykn=h(()=>{"use strict";het();kxi=1e3,Rxi=3e3});function bye(t){if(typeof t!="object"||t===null||Array.isArray(t))return!1;try{let e=Object.getPrototypeOf(t);if(e!==Object.prototype&&e!==null||Object.getOwnPropertySymbols(t).length!==0)return!1;let n=Object.getOwnPropertyNames(t).sort();if(n.length!==3||n[0]!=="body"||n[1]!=="commandName"||n[2]!=="kind")return!1;let r=Object.getOwnPropertyDescriptors(t);if(Object.values(r).some(o=>!("value"in o)))return!1;let i=t;return i.kind!=="memory-slash"||typeof i.commandName!="string"||typeof i.body!="string"?!1:(JSON.stringify(t),!0)}catch{return!1}}function Dxi(t){let e="",n=0,r=!0,i,o;for(;n<t.length;){if(r&&o===void 0){let a=t.slice(n).match(/^(?: {0,3})(`{3,}|~{3,})/);if(a!==null){let c=a[1][0],u=a[1].length;i===void 0?i={marker:c,length:u}:i.marker===c&&u>=i.length&&(i=void 0)}}if(i===void 0&&o===void 0&&t.startsWith("<!--",n)){let a=t.indexOf("-->",n+4),c=a===-1?t.length:a+3,u=t.indexOf(`
|
|
1855
|
+
`);let m=e.tagPane()??process.env.TMUX_PANE??my()?.pane;!o&&m!==void 0&&ua()&&J0(m)}u()}};return{done:l,teardown:d,isTornDown:()=>c,installSignalHandlers:()=>{let f=()=>d("signal");process.once("SIGTERM",f),process.once("SIGHUP",f),process.once("SIGINT",f)}}}var pkn=h(()=>{"use strict";Ap();wye()});var I0,xye=h(()=>{"use strict";I0=class extends Error{constructor(n){super(`node ${n} has no running broker \u2014 focus or revive it first`);this.nodeId=n;this.name="BrokerUnavailableError"}nodeId}});import{EventEmitter as vxi}from"node:events";var _U,fet=h(()=>{"use strict";Fr();dK();Uhe();Kwe();Sh();TYe();xye();_U=class extends vxi{constructor(n,r={}){super();this.nodeId=n;this.options=r}nodeId;options;socket;decoder=new d6(IYe);closeEmitted=!1;redialFaultRecorded=!1;get socketPath(){return qM(this.nodeId)}recordSocketFault(n,r){if(this.options.recordsViewerFaults===!1)return;if(n==="redial"){if(this.redialFaultRecorded)return;this.redialFaultRecorded=!0}let i=Fhe("viewer\u2194broker",r);Cat(this.nodeId,{link:"viewer\u2194broker",op:n,kind:i.kind,retry:n==="redial"?{disposition:"auto",by:"client"}:{disposition:"manual"},message:r instanceof Error?r.message:String(r)}).catch(o=>{dc({event:"broker.daemon.operation.failed",level:"error",node_id:this.nodeId,error:o,fields:{operation:"viewer_fault_record"}})})}connect(){let n=ybe(this.nodeId);this.socket=n,n.on("connect",()=>{this.redialFaultRecorded=!1,this.clearSocketFault(),this.emit("connect")}),n.on("data",r=>this.onData(r)),n.on("error",r=>this.onError(r,"connect")),n.on("close",()=>this.onClose())}redial(n={}){return this.destroy(),this.decoder=new d6(IYe),this.closeEmitted=!1,n.brokerUnavailableIsExpected&&(this.redialFaultRecorded=!1,this.clearSocketFault()),new Promise((r,i)=>{let o=ybe(this.nodeId);this.socket=o;let s=!1;o.on("connect",()=>{s||(s=!0,this.redialFaultRecorded=!1,this.clearSocketFault(),this.emit("connect"),r())}),o.on("data",a=>this.onData(a)),o.on("error",a=>{if(s){this.onError(a,"redial");return}s=!0;let c=a.code==="ECONNREFUSED"||a.code==="ENOENT";(!n.brokerUnavailableIsExpected||!c)&&this.recordSocketFault("redial",a);try{o.destroy()}catch{}i(a)}),o.on("close",()=>this.onClose())})}send(n){let r=this.socket;if(!(r===void 0||r.destroyed))try{r.write(Jme(n))}catch{}}destroy(){let n=this.socket;if(n!==void 0&&!n.destroyed)try{n.destroy()}catch{}}clearSocketFault(){this.options.recordsViewerFaults!==!1&&Iat(this.nodeId,{link:"viewer\u2194broker"}).catch(n=>{dc({event:"broker.daemon.operation.failed",level:"error",node_id:this.nodeId,error:n,fields:{operation:"viewer_fault_clear"}})})}onData(n){let r;try{r=this.decoder.push(n)}catch(i){let o=i instanceof BF?`broker sent an oversized frame (${i.message}) \u2014 disconnecting`:`failed to decode a broker frame: ${String(i)}`;this.emitError(new Error(o)),this.destroy();return}for(let i of r)this.emit("frame",i)}onError(n,r){this.recordSocketFault(r,n),n.code==="ECONNREFUSED"||n.code==="ENOENT"?this.emitError(new I0(this.nodeId)):this.emitError(new Error(`view socket error: ${n.message}`))}onClose(){this.closeEmitted||(this.closeEmitted=!0,this.emit("close"))}emitError(n){this.listenerCount("error")>0&&this.emit("error",n)}}});import{EventEmitter as Sxi}from"node:events";function Cxi(t,e){if("socketPath"in t)return`ws+unix://${t.socketPath}:/v1/nodes/${encodeURIComponent(e)}/attach?revive=0`;let n=new URL(t.previewEndpoint);return n.protocol=n.protocol==="http:"?"ws:":"wss:",n.pathname=`/v1/nodes/${encodeURIComponent(e)}/attach`,n.search="?wakeParked=1",n.hash="",n.toString()}var QD,fkn=h(()=>{"use strict";OFe();Uhe();xye();QD=class extends Sxi{constructor(n,r){super();this.nodeId=n;this.target=r}nodeId;target;ws;closeEmitted=!1;get url(){return Cxi(this.target,this.nodeId)}dial(){let n=new Fk.default(this.url,{..."relayToken"in this.target?{headers:{authorization:`Bearer ${this.target.relayToken}`}}:{},perMessageDeflate:!1});return this.ws=n,n}connect(){let n=this.dial();n.on("open",()=>{this.emit("connect")}),n.on("message",r=>this.onMessage(r)),n.on("error",r=>this.onError(r)),n.on("close",(r,i)=>this.onClose(r,i.toString()))}redial(n){return this.destroy(),this.closeEmitted=!1,new Promise((r,i)=>{let o=this.dial(),s=!1;o.on("open",()=>{s||(s=!0,this.emit("connect"),r())}),o.on("message",a=>this.onMessage(a)),o.on("error",a=>{if(s){this.onError(a);return}s=!0;try{o.terminate()}catch{}i(a)}),o.on("close",(a,c)=>{s&&this.onClose(a,c.toString())})})}send(n){let r=this.ws;if(!(r===void 0||r.readyState!==Fk.default.OPEN))try{r.send(JSON.stringify(n))}catch{}}destroy(){let n=this.ws;if(n!==void 0&&n.readyState<=Fk.default.OPEN)try{n.close(1e3)}catch{}}onMessage(n){let r;try{r=JSON.parse(n.toString())}catch{return}this.emit("frame",r)}onError(n){this.emitError(new Error(`relay transport error: ${n.message}`))}onClose(n,r){if(this.closeEmitted)return;this.closeEmitted=!0;let i=Fhe("viewer\u2194broker",{code:n,reason:r});(n===1008||i.kind==="protocol"||n===1011&&(/^no running broker/.test(r)||/^no node /.test(r)))&&this.emitError(new I0(this.nodeId)),this.emit("close")}emitError(n){this.listenerCount("error")>0&&this.emit("error",n)}}});import{EventEmitter as Ixi}from"node:events";import{randomUUID as Txi}from"node:crypto";function met(t){return t===null?!0:t.lifecycle==="resident"&&t.status==="done"&&t.intent==="parked"&&t.final_report==null?!1:t.status==="done"||t.status==="dead"||t.status==="canceled"}var mkn,nV,hkn=h(()=>{"use strict";fet();xye();fet();fkn();mkn=1e4;nV=class extends Ixi{constructor(n,r={}){super();this.nodeId=n;this.transport=r.transport??new _U(n),this.transport.on("connect",()=>{this.closeEmitted=!1,this.connected=!0,this.lastTransportError=void 0,this.emit("connect")}),this.transport.on("frame",i=>this.onFrame(i)),this.transport.on("error",i=>{this.lastTransportError=i.message,this.emitError(i)}),this.transport.on("close",()=>this.onClose())}nodeId;transport;closeEmitted=!1;connected=!1;lastTransportError;pending=new Map;request(n){let r=Txi();return new Promise((i,o)=>{if(!this.connected){o(new Error(`cannot issue '${n.type}': no live broker connection`));return}let s=setTimeout(()=>{this.pending.delete(r),o(new Error(`request '${n.type}' timed out after ${mkn}ms`))},mkn);typeof s.unref=="function"&&s.unref(),this.pending.set(r,{resolve:i,reject:o,timer:s}),this.send({...n,id:r})})}rejectAllPending(n){for(let{reject:r,timer:i}of this.pending.values())clearTimeout(i),r(new Error(n));this.pending.clear()}connect(){this.transport.connect()}redial(n){return this.transport.redial(n)}send(n){this.transport.send(n)}close(){this.transport.destroy()}onFrame(n){if(n.type==="data"){let r=typeof n.id=="string"?this.pending.get(n.id):void 0;r&&(this.pending.delete(n.id),clearTimeout(r.timer),r.resolve(n));return}if(n.type==="error"&&typeof n.id=="string"&&this.pending.has(n.id)){let r=this.pending.get(n.id);this.pending.delete(n.id),clearTimeout(r.timer),r.reject(new Error(n.message||`request failed: ${n.code}`));return}this.emit("frame",n)}onClose(){this.closeEmitted||(this.closeEmitted=!0,this.connected=!1,this.rejectAllPending(this.lastTransportError??"broker connection closed"),this.emit("close"))}emitError(n){this.listenerCount("error")>0&&this.emit("error",n)}}});var het=h(()=>{"use strict";hkn()});function gkn(t,e){let{nodeId:n,socket:r,canvasSource:i}=t,o,s=!1,a=e.initialDormant===!0,c=!1,u=!1,l=()=>{},d,p,f=new Promise(x=>{l=x}),m=x=>u?Promise.resolve(!1):new Promise(E=>{let _=C=>{d=void 0,p=void 0,E(C)};d=setTimeout(()=>_(!0),x),p=()=>{d!==void 0&&clearTimeout(d),_(!1)}}),g=()=>{r.send({type:"hello",role:e.role,client_id:e.clientId,term:{cols:process.stdout.columns??80,rows:process.stdout.rows??24}})},y=async()=>{s=!0,e.setNotice("\u21BB node refreshing \u2014 reconnecting\u2026",{sticky:!0});let x=200;try{for(;!u&&!e.isTornDown();){let E;try{E=await i.getRow(n)}catch{E=void 0}if(E!==void 0&&met(E)){c=E?.status==="done";break}let _=E?.status==="idle"||E?.intent==="idle-release"||E?.lifecycle==="resident"&&E.status==="done"&&E.intent==="parked"&&E.final_report==null,C=_&&!a;a=_,C&&e.onEnterDormancy(),e.setNotice(_?"":"\u21BB node refreshing \u2014 reconnecting\u2026",{sticky:!_});try{if(!await Promise.race([r.redial({brokerUnavailableIsExpected:_}).then(()=>!0),f.then(()=>!1)])||u||e.isTornDown())return;g(),a=!1,e.setNotice("");return}catch{if(!await m(_?Rxi:Math.min(x,kxi)))return;x*=2}}}finally{s=!1}!u&&!e.isTornDown()&&e.teardown(c?"node-done":"broker-gone")};return{sendHello:g,startDormant:()=>{s||u||e.isTornDown()||(e.onEnterDormancy(),y())},isDormant:()=>a,lastSocketError:()=>o,install:()=>{r.on("frame",e.onFrame),r.on("error",x=>{o=x.message}),r.on("close",()=>{e.isTornDown()||s||y()})},stop:()=>{u||(u=!0,p?.(),l())}}}var kxi,Rxi,ykn=h(()=>{"use strict";het();kxi=1e3,Rxi=3e3});function bye(t){if(typeof t!="object"||t===null||Array.isArray(t))return!1;try{let e=Object.getPrototypeOf(t);if(e!==Object.prototype&&e!==null||Object.getOwnPropertySymbols(t).length!==0)return!1;let n=Object.getOwnPropertyNames(t).sort();if(n.length!==3||n[0]!=="body"||n[1]!=="commandName"||n[2]!=="kind")return!1;let r=Object.getOwnPropertyDescriptors(t);if(Object.values(r).some(o=>!("value"in o)))return!1;let i=t;return i.kind!=="memory-slash"||typeof i.commandName!="string"||typeof i.body!="string"?!1:(JSON.stringify(t),!0)}catch{return!1}}function Dxi(t){let e="",n=0,r=!0,i,o;for(;n<t.length;){if(r&&o===void 0){let a=t.slice(n).match(/^(?: {0,3})(`{3,}|~{3,})/);if(a!==null){let c=a[1][0],u=a[1].length;i===void 0?i={marker:c,length:u}:i.marker===c&&u>=i.length&&(i=void 0)}}if(i===void 0&&o===void 0&&t.startsWith("<!--",n)){let a=t.indexOf("-->",n+4),c=a===-1?t.length:a+3,u=t.indexOf(`
|
|
1856
1856
|
`,c),l=t.lastIndexOf(`
|
|
1857
1857
|
`,n-1)+1,d=t.slice(l,n),p=t.slice(c,u===-1?t.length:u);if(d.trim()===""&&p.trim()===""){d.length>0&&(e=e.slice(0,-d.length)),n=u===-1?t.length:u+1,r=!0;continue}let f=t.slice(n,c);r=f.endsWith(`
|
|
1858
1858
|
`),n+=f.length;continue}if(i===void 0&&t[n]==="`"){let a=n;for(;t[a]==="`";)a++;let c=a-n;o===void 0?o=c:o===c&&(o=void 0),e+=t.slice(n,a),r=!1,n=a;continue}let s=t[n];e+=s,r=s===`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var l=Object.defineProperty;var a=(r,t)=>l(r,"name",{value:t,configurable:!0});import{EventEmitter as d}from"node:events";import{WebSocket as h}from"ws";import{classify as m}from"../fault-classifier.js";import{BrokerUnavailableError as f}from"./transport.js";function u(r,t){if("socketPath"in r)return`ws+unix://${r.socketPath}:/v1/nodes/${encodeURIComponent(t)}/attach?revive=0`;const e=new URL(r.previewEndpoint);return e.protocol=e.protocol==="http:"?"ws:":"wss:",e.pathname=`/v1/nodes/${encodeURIComponent(t)}/attach`,e.search="?wakeParked=1",e.hash="",e.toString()}a(u,"relayUrl");class k extends d{static{a(this,"RelayTransport")}nodeId;target;ws;closeEmitted=!1;constructor(t,e){super(),this.nodeId=t,this.target=e}get url(){return u(this.target,this.nodeId)}dial(){const t=new h(this.url,{..."relayToken"in this.target?{headers:{authorization:`Bearer ${this.target.relayToken}`}}:{},perMessageDeflate:!1});return this.ws=t,t}connect(){const t=this.dial();t.on("open",()=>{this.emit("connect")}),t.on("message",e=>this.onMessage(e)),t.on("error",e=>this.onError(e)),t.on("close",(e,n)=>this.onClose(e,n.toString()))}redial(t){return this.destroy(),this.closeEmitted=!1,new Promise((e,n)=>{const o=this.dial();let i=!1;o.on("open",()=>{i||(i=!0,this.emit("connect"),e())}),o.on("message",s=>this.onMessage(s)),o.on("error",s=>{if(i){this.onError(s);return}i=!0;try{o.terminate()}catch{}n(s)}),o.on("close",(s,c)=>{i&&this.onClose(s,c.toString())})})}send(t){const e=this.ws;if(!(e===void 0||e.readyState!==h.OPEN))try{e.send(JSON.stringify(t))}catch{}}destroy(){const t=this.ws;if(t!==void 0&&t.readyState<=h.OPEN)try{t.close()}catch{}}onMessage(t){let e;try{e=JSON.parse(t.toString())}catch{return}this.emit("frame",e)}onError(t){this.emitError(new Error(`relay transport error: ${t.message}`))}onClose(t,e){if(this.closeEmitted)return;this.closeEmitted=!0;const n=m("viewer\u2194broker",{code:t,reason:e});(t===1008||n.kind==="protocol"||t===1011&&(/^no running broker/.test(e)||/^no node /.test(e)))&&this.emitError(new f(this.nodeId)),this.emit("close")}emitError(t){this.listenerCount("error")>0&&this.emit("error",t)}}export{k as RelayTransport};
|
|
1
|
+
var l=Object.defineProperty;var a=(r,t)=>l(r,"name",{value:t,configurable:!0});import{EventEmitter as d}from"node:events";import{WebSocket as h}from"ws";import{classify as m}from"../fault-classifier.js";import{BrokerUnavailableError as f}from"./transport.js";function u(r,t){if("socketPath"in r)return`ws+unix://${r.socketPath}:/v1/nodes/${encodeURIComponent(t)}/attach?revive=0`;const e=new URL(r.previewEndpoint);return e.protocol=e.protocol==="http:"?"ws:":"wss:",e.pathname=`/v1/nodes/${encodeURIComponent(t)}/attach`,e.search="?wakeParked=1",e.hash="",e.toString()}a(u,"relayUrl");class k extends d{static{a(this,"RelayTransport")}nodeId;target;ws;closeEmitted=!1;constructor(t,e){super(),this.nodeId=t,this.target=e}get url(){return u(this.target,this.nodeId)}dial(){const t=new h(this.url,{..."relayToken"in this.target?{headers:{authorization:`Bearer ${this.target.relayToken}`}}:{},perMessageDeflate:!1});return this.ws=t,t}connect(){const t=this.dial();t.on("open",()=>{this.emit("connect")}),t.on("message",e=>this.onMessage(e)),t.on("error",e=>this.onError(e)),t.on("close",(e,n)=>this.onClose(e,n.toString()))}redial(t){return this.destroy(),this.closeEmitted=!1,new Promise((e,n)=>{const o=this.dial();let i=!1;o.on("open",()=>{i||(i=!0,this.emit("connect"),e())}),o.on("message",s=>this.onMessage(s)),o.on("error",s=>{if(i){this.onError(s);return}i=!0;try{o.terminate()}catch{}n(s)}),o.on("close",(s,c)=>{i&&this.onClose(s,c.toString())})})}send(t){const e=this.ws;if(!(e===void 0||e.readyState!==h.OPEN))try{e.send(JSON.stringify(t))}catch{}}destroy(){const t=this.ws;if(t!==void 0&&t.readyState<=h.OPEN)try{t.close(1e3)}catch{}}onMessage(t){let e;try{e=JSON.parse(t.toString())}catch{return}this.emit("frame",e)}onError(t){this.emitError(new Error(`relay transport error: ${t.message}`))}onClose(t,e){if(this.closeEmitted)return;this.closeEmitted=!0;const n=m("viewer\u2194broker",{code:t,reason:e});(t===1008||n.kind==="protocol"||t===1011&&(/^no running broker/.test(e)||/^no node /.test(e)))&&this.emitError(new f(this.nodeId)),this.emit("close")}emitError(t){this.listenerCount("error")>0&&this.emit("error",t)}}export{k as RelayTransport};
|
|
@@ -41,6 +41,19 @@ export declare function startRemoteAttachHeartbeat(ws: WebSocket, intervalMs?: n
|
|
|
41
41
|
* `ErrorBody`, spec §8) to the raw socket and destroy it. Never throws — a
|
|
42
42
|
* reset peer is swallowed. Shared with server.ts's auth and startup gates. */
|
|
43
43
|
export declare function refuseUpgrade(socket: Duplex, status: number, statusText: string, code: string, message: string): void;
|
|
44
|
+
/** Relay one remote WS ⇄ one node broker `view.sock`. Opens the socket on
|
|
45
|
+
* connect; forwards each complete frame socket→WS (bounded decode, one WS
|
|
46
|
+
* message per frame) and re-frames each WS message WS→socket (trailing
|
|
47
|
+
* newline). The relay is semantically verbatim — it never INTERPRETS a frame
|
|
48
|
+
* (it only splits/joins the newline framing). Closes the WS on socket
|
|
49
|
+
* gone/no-broker, destroys the socket on WS close; idempotent teardown, no
|
|
50
|
+
* leaks. Validates the node id first (path-safety + existence) as belt-and-
|
|
51
|
+
* suspenders — the upgrade handler already guarded both. */
|
|
52
|
+
/** A viewer that closed its WebSocket on purpose: 1000 normal, 1001 going
|
|
53
|
+
* away, or 1005 — a close frame carrying no status, which is what a bare
|
|
54
|
+
* `ws.close()` sends. None is a link fault; only an abnormal drop (1006) or an
|
|
55
|
+
* error code is. */
|
|
56
|
+
export declare function isCleanViewerClose(code: number): boolean;
|
|
44
57
|
/** Handle the HTTP upgrade for `GET /v1/nodes/{id}/attach`. A-9 matches the
|
|
45
58
|
* route, extracts `{id}`, and calls this with the raw upgrade triplet; this
|
|
46
59
|
* owns the WS handshake (its own noServer `wss` above) AND the spec §5.3
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
var
|
|
1
|
+
var $=Object.defineProperty;var s=(r,e)=>$(r,"name",{value:e,configurable:!0});import{WebSocketServer as R}from"ws";import{isSafeNodeId as A}from"../../core/canvas/paths.js";import{getNode as S}from"../../core/canvas/index.js";import{BROKER_READ_CAPS as C,CLIENT_READ_CAPS as T,FrameDecoder as x,FrameOverflowError as B}from"../../core/runtime/broker-protocol.js";import{classify as k}from"../../core/fault-classifier.js";import{clearFault as d,recordFault as g}from"../../core/canvas/recovery.js";import{decideAttachRevive as N}from"../../core/runtime/revive.js";import{connectViewSocket as O,probeViewSocket as P,waitForBrokerViewSocket as F}from"../../core/runtime/view-socket.js";import{isBeneathRefusal as U}from"../../core/spaces/open-beneath.js";import{attachViewer as _,detachViewer as D}from"./viewer-presence.js";const L=new R({noServer:!0,maxPayload:C.maxLineBytes}),V=25e3,H=6e4;function M(r,e=V,i=H){let n=!0,o=!1,a=Date.now();const f=s(()=>{n=!0},"acknowledge"),m=s(()=>{o||(o=!0,clearInterval(h),r.off("pong",f))},"stop");r.on("pong",f);const h=setInterval(()=>{if(Date.now()-a>i){n=!0;return}if(!n){m(),r.terminate();return}n=!1;try{r.ping()}catch{m(),r.terminate()}},e);return h.unref(),{stop:m,noteActivity:s(()=>{a=Date.now()},"noteActivity")}}s(M,"startRemoteAttachHeartbeat");function b(r){const e=Buffer.from(r,"utf8");return e.byteLength<=123?r:e.subarray(0,123).toString("utf8")}s(b,"clampReason");function u(r,e,i,n,o){const a=JSON.stringify({error:{code:n,message:o}}),f=`HTTP/1.1 ${e} ${i}\r
|
|
2
2
|
content-type: application/json; charset=utf-8\r
|
|
3
3
|
content-length: ${Buffer.byteLength(a)}\r
|
|
4
4
|
connection: close\r
|
|
5
5
|
\r
|
|
6
|
-
`+a;try{
|
|
6
|
+
`+a;try{r.end(f)}catch{try{r.destroy()}catch{}}}s(u,"refuseUpgrade");function J(r){if(new URL(r.url??"/","http://127.0.0.1").searchParams.get("revive")==="0")return!0;const i=r.headers["x-crtr-attach-revive"];return(Array.isArray(i)?i[0]:i)==="0"}s(J,"reviveOptedOut");function W(r){return new URL(r.url??"/","http://127.0.0.1").searchParams.get("wakeParked")==="1"}s(W,"wakeParkedRequested");function E(r){return r===1e3||r===1001||r===1005}s(E,"isCleanViewerClose");function j(r,e){if(!A(e)){try{r.close(1008,b(`invalid node id: ${e}`))}catch{}return}if(S(e)===null){try{r.close(1011,b(`no node ${e}`))}catch{}return}d(e,{link:"viewer\u2194crtrd"});const i=O(e),n=new x(T);let o=!1,a=!1,f,m=!1,h=!1,v=!1;i.on("connect",()=>{m=!0,!o&&v&&_(e,r),d(e,{link:"relay\u2194broker"})});const p=s((t,l)=>{if(!o){if(o=!0,D(e,r),f?.stop(),!i.destroyed)try{i.destroy()}catch{}try{r.close(t,b(l))}catch{}}},"teardown");f=M(r),i.on("data",t=>{f?.noteActivity();let l;try{l=n.push(t)}catch(c){const y=c instanceof B?`broker sent an oversized frame (${c.message})`:`failed to decode a broker frame for ${e}`;p(1009,y);return}for(const c of l)r.readyState===r.OPEN&&r.send(JSON.stringify(c))}),i.on("error",t=>{const l=k("relay\u2194broker",t);g(e,{link:"relay\u2194broker",op:"bridge socket",kind:l.kind,retry:{disposition:"manual"},message:t.message});const c=t.code==="ECONNREFUSED"||t.code==="ENOENT"?`no running broker for ${e}`:`view socket error for ${e}: ${t.message}`;p(1011,c)}),i.on("close",t=>{o||t||(d(e,{link:"relay\u2194broker"}),p(1e3,"broker socket closed"))}),r.on("close",(t,l)=>{const c=l.toString("utf8");if(o){E(t)&&d(e,{link:"viewer\u2194crtrd"});return}if(E(t))d(e,{link:"viewer\u2194crtrd"});else if(!a){a=!0;const y=k("viewer\u2194crtrd",{code:t,reason:c});g(e,{link:"viewer\u2194crtrd",op:"remote attach websocket",kind:y.kind,retry:{disposition:"auto"},message:c||`remote attach closed (${t})`})}p(t,c||`remote attach closed (${t})`)}),r.on("error",()=>{if(!a){a=!0;const t=k("viewer\u2194crtrd",{code:1006,reason:"remote attach websocket error"});g(e,{link:"viewer\u2194crtrd",op:"remote attach websocket",kind:t.kind,retry:{disposition:"auto"},message:"remote attach websocket error"})}p(1011,"remote attach websocket error")}),r.on("message",t=>{if(o||i.destroyed)return;f?.noteActivity();const c=(Array.isArray(t)?Buffer.concat(t):Buffer.isBuffer(t)?t:Buffer.from(new Uint8Array(t))).toString("utf8");if(!h)try{const w=JSON.parse(c);w?.type==="hello"&&(h=!0,v=w.attends!==!1,v&&m&&_(e,r))}catch{}const y=c.endsWith(`
|
|
7
7
|
`)?c:c+`
|
|
8
|
-
`;try{i.write(
|
|
8
|
+
`;try{i.write(y)}catch{}})}s(j,"bridgeConnection");async function te(r,e,i,n){e.on("error",()=>{});try{if(!A(n)){u(e,400,"Bad Request","invalid_node_id",`invalid node id: ${n}`);return}if(S(n)===null){u(e,404,"Not Found","node_not_found",`no node ${n}`);return}if(J(r)){if(!await P(n,250)){u(e,409,"Conflict","node_dormant",`node ${n} is dormant and revive was opted out (?revive=0)`);return}}else{let o;try{const a=await N(n,{resume:!0,capacity:"refuse"},W(r));if(a.parked){u(e,409,"Conflict","node_dormant",`node ${n} is parked; attach with ?wakeParked=1 to wake it`);return}o=a.result.launch?.exited}catch(a){const f=a instanceof Error?a.message:String(a);u(e,409,"Conflict","attach_revive_failed",`attach revive failed: ${f}`);return}if(!await F(n,o)){u(e,409,"Conflict","broker_not_ready",`broker view socket for ${n} did not become listenable after revive`);return}}if(e.destroyed)return;L.handleUpgrade(r,e,i,o=>j(o,n))}catch(o){const a=o instanceof Error?o.message:String(o);if(U(o)){u(e,400,"Bad Request","invalid_request",a);return}u(e,500,"Internal Server Error","internal",`attach upgrade failed: ${a}`)}}s(te,"handleAttachUpgrade");export{V as REMOTE_ATTACH_HEARTBEAT_MS,H as REMOTE_ATTACH_QUIET_MS,te as handleAttachUpgrade,E as isCleanViewerClose,u as refuseUpgrade,M as startRemoteAttachHeartbeat};
|
package/docs/sdk/README.md
CHANGED
|
@@ -26,16 +26,16 @@ if (run.kind === 'result') console.log(run.output_parsed.root_cause);
|
|
|
26
26
|
|
|
27
27
|
## Go live with an app key
|
|
28
28
|
|
|
29
|
-
Once you have installed the SDK and tried the local flow in [Getting started](./getting-started.md), build against production: [Build and register an app](./guides/apps-on-crouter-cloud.md#build-and-register-an-app) takes you from signing up at `https://account.northlight.ai`
|
|
29
|
+
Once you have installed the SDK and tried the local flow in [Getting started](./getting-started.md), build against production: [Build and register an app](./guides/apps-on-crouter-cloud.md#build-and-register-an-app) takes you from signing up at `https://account.northlight.ai` and joining your laptop (`crtr sys join`) to a running app on your laptop, registered with `crtr cloud apps register`. Generate the key and pull the settings with:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
npx @north-light/sdk keygen --app <client id>
|
|
33
33
|
npx @north-light/sdk env --app <client id>
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
`keygen` saves the private JWK to `local/app-key.json` (mode `0600`); keep that file out of git. With `--app`, it offers to add the public key to your registered app through your own `crtr`, as an additional key that never replaces the current one. Otherwise,
|
|
36
|
+
`keygen` saves the private JWK to `local/app-key.json` (mode `0600`); keep that file out of git. With `--app`, it offers to add the public key to your registered app through your own `crtr`, as an additional key that never replaces the current one. Otherwise, add the public JWK printed on stdout with `crtr cloud apps keys add --app-id <client id> --jwk '<public JWK>'`. An app authenticates only with its key; the directory issues no client secret.
|
|
37
37
|
|
|
38
|
-
`env` reads the registration through `crtr cloud apps get` and writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` into `local/app.env` (`--out` picks another file), keeping every other line. Both commands need this machine's `crtr` joined to your Northlight account; on a machine that isn't, they print a join step and exit 2.
|
|
38
|
+
`env` reads the registration through `crtr cloud apps get` and writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` into `local/app.env` (`--out` picks another file), keeping every other line. Both commands need this machine's `crtr` joined to your Northlight account; on a machine that isn't, they print a join step and exit 2. An app is configured from four variables:
|
|
39
39
|
|
|
40
40
|
| Variable | Value |
|
|
41
41
|
|---|---|
|
|
@@ -83,7 +83,7 @@ The directory rejects the key until its public JWK is added to the app. To chang
|
|
|
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
85
|
| [Release notes](./release-notes.md) | Behavior changes an existing app must act on, newest first |
|
|
86
|
-
| [Apps on crouter cloud](./guides/apps-on-crouter-cloud.md) | Building and registering an app against production (
|
|
86
|
+
| [Apps on crouter cloud](./guides/apps-on-crouter-cloud.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
|
|
|
89
89
|
## Phases
|
|
@@ -20,12 +20,12 @@ That installs the `crtr` CLI and the `crtrd` daemon. You do not have to start th
|
|
|
20
20
|
## 2. Install the SDK in your application
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
npm i @north-light/sdk
|
|
23
|
+
npm i --save-exact @north-light/sdk@0.3.422
|
|
24
24
|
```
|
|
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`).
|
|
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](./release-notes.md) first.
|
|
29
29
|
|
|
30
30
|
## 3. Node, on the owner's own daemon
|
|
31
31
|
|
|
@@ -174,7 +174,7 @@ An outcome is returned, never thrown — including a decline and a failure. Only
|
|
|
174
174
|
|
|
175
175
|
## Go live with an app key
|
|
176
176
|
|
|
177
|
-
Build against production from your laptop: [Build and register an app](./guides/apps-on-crouter-cloud.md#build-and-register-an-app) walks the whole path. In short, sign up at `https://account.northlight.ai`,
|
|
177
|
+
Build against production from your laptop: [Build and register an app](./guides/apps-on-crouter-cloud.md#build-and-register-an-app) walks the whole path. In short, sign up at `https://account.northlight.ai`, create a join token at `https://account.northlight.ai/account/machines` and run `crtr sys join --directory https://account.northlight.ai --token <token>` on your laptop, register the app with `crtr cloud apps register` using a `http://localhost:<port>` origin and a `http://localhost:<port>/auth/callback` redirect URI while you develop, then generate the key and pull its settings with:
|
|
178
178
|
|
|
179
179
|
```bash
|
|
180
180
|
npx @north-light/sdk keygen --app <client id>
|
|
@@ -185,7 +185,7 @@ npx @north-light/sdk env --app <client id>
|
|
|
185
185
|
|
|
186
186
|
`env` runs `crtr cloud apps get --app-id <client id>` and writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID`, and `APP_ORIGIN` into `local/app.env` (`--out <path>` writes another file). It updates those three lines in place, keeps every other line and comment, and creates the file with mode `0600` when it doesn't exist. It never writes `CROUTER_KEY_FILE` or anything secret. It prints the values it set and the key file your app will read.
|
|
187
187
|
|
|
188
|
-
Both commands reach your app through this machine's `crtr`, so the machine must be joined to your Northlight account. On one that isn't, they say so, print a join step and exit 2. Against production
|
|
188
|
+
Both commands reach your app through this machine's `crtr`, so the machine must be joined to your Northlight account. On one that isn't, they say so, print a join step and exit 2. Against production, [join your laptop](./guides/apps-on-crouter-cloud.md#1-account-and-join) first.
|
|
189
189
|
|
|
190
190
|
An app built on the SDK is configured from four variables:
|
|
191
191
|
|
|
@@ -5,7 +5,7 @@ description: When you are building and registering an app on Northlight, or an a
|
|
|
5
5
|
|
|
6
6
|
# Apps on crouter cloud
|
|
7
7
|
|
|
8
|
-
This page first walks through [building and registering an app](#build-and-register-an-app) against production
|
|
8
|
+
This page first walks through [building and registering an app](#build-and-register-an-app) against production with `crtr cloud apps`, then covers [managing it](#manage-an-app-with-crtr-cloud-apps), then runs.
|
|
9
9
|
|
|
10
10
|
An app holding a person's connection starts runs on their runtime with `runs.start`, follows them with `runs.events` or `runs.reply`, and reads them with `runs.get`. This page covers getting data back from a run. For connections and per-person access, see [Apps with many users](./apps-with-many-users.md).
|
|
11
11
|
|
|
@@ -13,53 +13,100 @@ An app holding a person's connection starts runs on their runtime with `runs.sta
|
|
|
13
13
|
|
|
14
14
|
This part takes you from nothing to an app running on your laptop against Northlight's production directory, `https://account.northlight.ai`. Your app server runs on `localhost`, you sign in to it through Northlight, and it calls your own runtime. There is no local Northlight stack to run.
|
|
15
15
|
|
|
16
|
-
You
|
|
16
|
+
You do all of it from the command line. Your laptop joins your Northlight account as an extra machine, and `crtr cloud apps …` registers and manages the app as you.
|
|
17
17
|
|
|
18
18
|
The example app texts you, so it asks for the `sms` provider's `sms:send` scope. You need Node.js 22.19 or later.
|
|
19
19
|
|
|
20
|
-
### 1.
|
|
20
|
+
### 1. Account and join
|
|
21
21
|
|
|
22
|
-
Open `https://account.northlight.ai/signup`
|
|
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, so use a phone number that has 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
23
|
|
|
24
|
-
|
|
24
|
+
Install the CLI (`npm i -g @north-light/cli`). Joining needs `crtr` 0.3.422 or later; `crtr --version` shows yours.
|
|
25
|
+
|
|
26
|
+
Then join your laptop to the account:
|
|
27
|
+
|
|
28
|
+
1. Open `https://account.northlight.ai/account/machines` and choose **Create join token**. A token needs a sign-in within the last 10 minutes, so the page asks you to sign in again if yours is older.
|
|
29
|
+
2. The page shows two commands, with the token filled in. On your laptop, run them: first `crtr sys daemon stop` (a running daemon makes the join answer `daemon_running`), then `crtr sys join --directory https://account.northlight.ai --token <token>`. Add `--name <label>` to name the machine on the page. The token works once and expires at the time the page shows; the page shows it only once.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
crtr sys daemon stop
|
|
33
|
+
crtr sys join --directory https://account.northlight.ai --token <token> --name my-laptop
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
It answers with `registered: true`, the machine id (`runtime:<id>`) and your user id.
|
|
37
|
+
3. Check it: `crtr cloud apps mine` lists the apps you manage. Before you have joined, it answers `not_joined` and prints these same steps.
|
|
38
|
+
|
|
39
|
+
Things to know before you join:
|
|
25
40
|
|
|
41
|
+
- **Joining rewrites your crtr home.** Everything in your everyday crtr home that belonged to the local user (`user:local`) becomes your Northlight user, the next time the daemon starts. If you would rather not do that to your everyday home, join from a home you can throw away.
|
|
42
|
+
- **There is no unjoin.** The only undo is removing the machine on `/account/machines`.
|
|
43
|
+
- **Removing the machine stops it.** Once you choose **Remove** next to a machine, it gets no new tokens at once and the ones it holds expire within 5 minutes. After that its `cloud` commands answer `machine_revoked`: "This machine was removed from your Northlight account." It stays that way until you create a new join token and run `crtr sys join` again. Files on the computer stay on it.
|
|
44
|
+
- **Your hosted machine is unchanged.** `/account/machines` lists it as "Northlight (hosted)", the default. Your chat agent at `https://chat.northlight.ai` still runs `crtr cloud apps …` as before.
|
|
45
|
+
|
|
46
|
+
### 2. Install the SDK
|
|
26
47
|
|
|
27
48
|
```bash
|
|
28
49
|
mkdir my-app && cd my-app
|
|
29
50
|
npm init -y
|
|
30
|
-
npm i @north-light/sdk@
|
|
51
|
+
npm i --save-exact @north-light/sdk@0.3.422
|
|
31
52
|
```
|
|
32
53
|
|
|
33
|
-
|
|
54
|
+
`0.3.422` 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](../release-notes.md) for the versions in between, and install the new exact version the same way.
|
|
34
55
|
|
|
35
|
-
|
|
56
|
+
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`.
|
|
57
|
+
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.
|
|
36
58
|
|
|
37
|
-
|
|
59
|
+
### 3. Register the app
|
|
38
60
|
|
|
39
61
|
```bash
|
|
40
|
-
|
|
62
|
+
crtr cloud apps register \
|
|
63
|
+
--name "Texty" \
|
|
64
|
+
--origin http://localhost:3000 \
|
|
65
|
+
--logo https://example.com/logo.png \
|
|
66
|
+
--justification "Texts you a summary of your day." \
|
|
67
|
+
--redirect-uri http://localhost:3000/auth/callback
|
|
41
68
|
```
|
|
42
69
|
|
|
43
|
-
|
|
70
|
+
| Flag | Value |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `--name` | Shown to people on the consent page and in their account. |
|
|
73
|
+
| `--origin` | Where your app runs. `http://localhost:<port>` and `http://127.0.0.1:<port>` are accepted for development. |
|
|
74
|
+
| `--logo` | Required. The URL of a square image. |
|
|
75
|
+
| `--justification` | One sentence, quoted on the consent page as your words. |
|
|
76
|
+
| `--redirect-uri` | Repeat the flag for each URI. Sign-in returns only to a URI listed here, matched exactly, so `localhost` and `127.0.0.1` are different URIs and the port counts. |
|
|
44
77
|
|
|
45
|
-
|
|
78
|
+
The answer is the registration: the app id (`app:<client id>`), `verified: false` and `testing: true` (see [Who can sign in: testing mode](#who-can-sign-in-testing-mode)), `key: {"source":"none"}` and an `env` block with `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN`. The client id is the part of the app id after `app:`. `crtr cloud apps get --app-id <client id>` shows it again. Until a key is added, `exchangeCode` throws an `AuthenticationError`.
|
|
46
79
|
|
|
47
|
-
|
|
80
|
+
`--app-id` takes `app:<client id>` or the bare client id everywhere.
|
|
48
81
|
|
|
49
|
-
|
|
50
|
-
|---|---|
|
|
51
|
-
| Name | Shown to people on the consent page and in their account. |
|
|
52
|
-
| Origin URL | Where your app runs, for example `http://localhost:3000`. `http://localhost:<port>` and `http://127.0.0.1:<port>` are accepted for development. |
|
|
53
|
-
| Logo URL | Required. A square image. |
|
|
54
|
-
| Why your app needs access | One sentence, quoted on the consent page as your words. |
|
|
55
|
-
| Redirect URIs | One per line, for example `http://localhost:3000/auth/callback`. Sign-in returns only to a URI listed here, matched exactly, so `localhost` and `127.0.0.1` are different URIs and the port counts. |
|
|
56
|
-
| Public key | Paste the public JWK from step 3. It is optional here; if you leave it empty, add it later with **Add key** on the app's **Sign-in** tab. |
|
|
82
|
+
### 4. Generate the signing key and add it
|
|
57
83
|
|
|
58
|
-
|
|
84
|
+
An app signs in with a key, not a client secret. From your app's folder:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npx @north-light/sdk keygen --app <client id> --yes
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This writes the private key to `local/app-key.json` (mode `0600`; keep it out of git) and adds its public key to the app through your own `crtr` (`crtr cloud apps keys add`). Without `--yes` it asks first. The answer is "Added key <kid> to <client id> as an additional key. Your current key still works."; `crtr cloud apps keys list --app-id <client id>` then shows the key as `active`. Never paste the private key anywhere.
|
|
59
91
|
|
|
60
92
|
### 5. Say what the app asks for
|
|
61
93
|
|
|
62
|
-
|
|
94
|
+
An app can only ask for scopes it has been given. It starts with `crtr:llm` and `crtr:act`. See what else it may ask for, then set the list:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
crtr cloud apps asks --app-id <client id> --source sms
|
|
98
|
+
crtr cloud apps set-scopes --app-id <client id> --scope crtr:llm --scope crtr:act --scope sms:send
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`asks` lists, per source, what the app may ask for and whether it asks now; `sms:send` is under providers, with the consent sentence "Send you text messages from Northlight's number". `set-scopes` replaces the whole list, so repeat `--scope` for every scope you want (`crtr:act` needs `crtr:llm`). Without `sms:send` the consent page does not offer it and `providers.call` for `sms` is refused. Take scope strings from `asks` and do not invent them.
|
|
102
|
+
|
|
103
|
+
If your app does not work without a provider being set up first (Doppel needs Context), name it:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
crtr cloud apps set-requires --app-id <client id> --provider context
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The person is then taken through that provider's setup page the first time they sign in to your app. Context asks for a profile URL, and its state stays `pending` for a few minutes afterwards; see [Waiting on a provider your app requires](#waiting-on-a-provider-your-app-requires).
|
|
63
110
|
|
|
64
111
|
#### What each scope gates
|
|
65
112
|
|
|
@@ -75,17 +122,19 @@ A run you start can only do what the grant holds and the run's own `scopes` list
|
|
|
75
122
|
|
|
76
123
|
#### Who can sign in: testing mode
|
|
77
124
|
|
|
78
|
-
A new app is in testing: it is registered as `verified: false`, only you and up to 100 testers can sign in to it, and it is not shown in the marketplace. You can sign in as yourself with no setup. To let someone else in, add them by the email or phone number on their Northlight account: `crtr cloud apps testers add --app-id <id> --email them@example.com` (or `--phone +15551234567`). An email matches only while it is verified on that account. They can sign in right away; nobody gets a message, so tell them. `testers list
|
|
79
|
-
|
|
80
|
-
Anyone else who tries sees "<app> is still in testing", and your app's callback receives `error=access_denied` with `error_description=app_in_testing` instead of a `code`. [When sign-in is refused: apps in testing](../client.md#when-sign-in-is-refused-apps-in-testing) shows how to read it and what to show the person. Removing a tester stops their access at once, including their refresh tokens, and keeps their data; adding them back and signing in again restores it.
|
|
125
|
+
A new app is in testing: it is registered as `verified: false`, only you and up to 100 testers can sign in to it, and it is not shown in the marketplace. You can sign in as yourself with no setup. To let someone else in, add them by the email or phone number on their Northlight account: `crtr cloud apps testers add --app-id <id> --email them@example.com` (or `--phone +15551234567`). An email matches only while it is verified on that account. They can sign in right away; nobody gets a message, so tell them. (Specified behavior: adding a tester, and everything below about a second person signing in, being refused or being removed, has not been run on production yet.) `crtr cloud apps testers list --app-id <id>` shows who is on the list and `testers remove` takes someone off. A testing app is also left out of the marketplace listing, even once you set `--listed true`.
|
|
81
126
|
|
|
82
|
-
|
|
127
|
+
Per the testing-mode specification, anyone else who tries sees "<app> is still in testing", and your app's callback receives `error=access_denied` with `error_description=app_in_testing` instead of a `code`. [When sign-in is refused: apps in testing](../client.md#when-sign-in-is-refused-apps-in-testing) shows how to read it and what to show the person. Removing a tester stops their access at once, including their refresh tokens, and keeps their data; adding them back and signing in again restores it.
|
|
83
128
|
|
|
84
129
|
When you are ready for everyone, email `support@northlight.ai` to have the app verified. Only Northlight staff can verify an app; registering never sets `verified: true`. Apps that existed before testing mode are already verified.
|
|
85
130
|
|
|
86
131
|
### 6. Write the environment file
|
|
87
132
|
|
|
88
|
-
|
|
133
|
+
```bash
|
|
134
|
+
npx @north-light/sdk env --app <client id>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
This reads the registration through `crtr cloud apps get` and writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` into `local/app.env`, keeping every other line and comment in the file. `--out <path>` writes another file. It never writes `CROUTER_KEY_FILE` or anything secret. Add the key path yourself:
|
|
89
138
|
|
|
90
139
|
```bash
|
|
91
140
|
CROUTER_ISSUER=https://account.northlight.ai
|
|
@@ -97,11 +146,11 @@ CROUTER_KEY_FILE=local/app-key.json
|
|
|
97
146
|
| Variable | Value |
|
|
98
147
|
|---|---|
|
|
99
148
|
| `CROUTER_ISSUER` | `https://account.northlight.ai` |
|
|
100
|
-
| `CROUTER_CLIENT_ID` | The
|
|
101
|
-
| `APP_ORIGIN` | The
|
|
102
|
-
| `CROUTER_KEY_FILE` | Path to the private key from step
|
|
149
|
+
| `CROUTER_CLIENT_ID` | The client id of your app |
|
|
150
|
+
| `APP_ORIGIN` | The origin you registered. The redirect URI is `${APP_ORIGIN}/auth/callback` by convention; it must match a registered redirect URI exactly. |
|
|
151
|
+
| `CROUTER_KEY_FILE` | Path to the private key from step 4. Default `local/app-key.json` |
|
|
103
152
|
|
|
104
|
-
The file holds no secret, but keep it out of git with the key.
|
|
153
|
+
The file holds no secret, but keep it out of git with the key.
|
|
105
154
|
|
|
106
155
|
### 7. Run it locally
|
|
107
156
|
|
|
@@ -186,12 +235,9 @@ Test your app with `FakeCloud` from `@north-light/sdk/testing`. Your real `OAuth
|
|
|
186
235
|
|
|
187
236
|
## Manage an app with `crtr cloud apps`
|
|
188
237
|
|
|
189
|
-
Everything
|
|
190
|
-
|
|
191
|
-
- **A laptop cannot run them yet.** The `cloud` commands appear on a machine joined to your Northlight account, and production does not yet let a laptop join as an additional machine. That is coming. Until it ships, do the steps above in the console and write the env file by hand. Do not follow older instructions to open `/account/machines` or to run `crtr sys join` against production: that page does not exist yet. `keygen --app` and `npx @north-light/sdk env` need the same joined machine, so use `keygen` without `--app`, as in step 3.
|
|
192
|
-
- **Your Northlight chat agent can run them.** Open `https://chat.northlight.ai`, and ask your chat agent to run the command. It runs as you on your hosted runtime and prints the real output. Verified on production: `register`, `set-scopes`, `keys add`, `get`, `mine`, `asks`, `set-listing`, `set-requires` and `update`. For `keys add`, generate the key on your laptop as in step 3 and give the agent only the public JWK. Never ask it to generate your key: it would live on the hosted disk. If a command fails once with `launcher refused launch_failed`, run it again.
|
|
238
|
+
Everything above is a `crtr cloud apps` command, and so is the rest of managing an app. They are leaves of Northlight's `cloud` provider, they run as you, and they need a machine joined to your account ([step 1](#1-account-and-join)).
|
|
193
239
|
|
|
194
|
-
|
|
240
|
+
Your Northlight chat agent at `https://chat.northlight.ai` can run the same commands as you, on your hosted runtime. Ask it to run a command and show you the raw output. It can list and change an app (`mine` and `set-scopes` were checked on production). It cannot delete one: see [Deleting an app](#deleting-an-app). Generate keys on your laptop and give an agent only a public JWK, never the private key.
|
|
195
241
|
|
|
196
242
|
`--app-id` takes `app:<client id>` or the bare client id. The leaves:
|
|
197
243
|
|
|
@@ -205,24 +251,50 @@ For example, ask the agent: "Run `crtr cloud apps register --name Texty --origin
|
|
|
205
251
|
| `crtr cloud apps asks --app-id <id>` | Lists what an app may ask for, with whether it asks now. Large sources such as `integrations` print as a count. Narrow it with `--query sms:send`, `--source sms`, `--group providers\|apps\|agents\|space`, or `--query <text>`; for example `crtr cloud apps asks --app-id <id> --source sms`. |
|
|
206
252
|
| `crtr cloud apps set-scopes --app-id <id> --scope …` | Replaces the scope list. Repeat `--scope` for every scope; `--clear` empties it; `crtr:act` needs `crtr:llm`. |
|
|
207
253
|
| `crtr cloud apps set-requires --app-id <id> --provider …` | Names providers your app does not work without (for example `context`); `--clear` empties it. |
|
|
208
|
-
| `crtr cloud apps keys add --app-id <id> --jwk '<public JWK>'` | Adds a signing key beside any existing one. Answers `created: false` if that key is already on the app. |
|
|
209
|
-
| `crtr cloud apps
|
|
210
|
-
| `crtr cloud apps
|
|
254
|
+
| `crtr cloud apps keys add --app-id <id> --jwk '<public JWK>'` | Adds a signing key beside any existing one. Answers `created: false` if that key is already on the app. `keygen --app` calls it for you. |
|
|
255
|
+
| `crtr cloud apps keys list --app-id <id>` | Lists the keys with their `kid` and `active` or `retired` status. |
|
|
256
|
+
| `crtr cloud apps keys retire --app-id <id> --kid <kid>` | Retires a key so it no longer signs in. |
|
|
257
|
+
| `crtr cloud apps set-listing --app-id <id> --login-start-url … --text-welcome …` (also `--listed`, `--price`) | Sets the listing fields; pass at least one. A loopback `--login-start-url` is accepted. `--listed true --price free` lists the app, so `cloud apps install` finds it; while the app is in testing it still stays out of the marketplace list. |
|
|
258
|
+
| `crtr cloud apps install --app-id <id>` | Texts an install link to the signed-in person. If they already hold a grant it answers `status: already_have` and the install URL instead of texting. The app must be listed. |
|
|
259
|
+
| `crtr cloud apps testers add --app-id <id> --email …` (or `--phone …`) | Lets that person sign in while the app is in testing; at most 100. Not yet run on production. |
|
|
211
260
|
| `crtr cloud apps testers list --app-id <id>` | Lists the testers. |
|
|
212
|
-
| `crtr cloud apps testers remove --app-id <id> --email …` (or `--phone …`) | Removes a tester and revokes their grants for the app. |
|
|
261
|
+
| `crtr cloud apps testers remove --app-id <id> --email …` (or `--phone …`) | Removes a tester and revokes their grants for the app. Not yet run on production. |
|
|
213
262
|
| `crtr cloud apps delete --app-id <id> --confirm <client id>` | Deletes the app and removes every person's grant for it. Only you at a joined machine's terminal can run it: an agent, including your chat agent, is refused with `provider_refused`. |
|
|
214
263
|
|
|
215
264
|
### Deleting an app
|
|
216
265
|
|
|
217
|
-
|
|
266
|
+
```bash
|
|
267
|
+
crtr cloud apps delete --app-id <client id> --confirm <client id>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
It answers `deleted: true` and `removed_grants`, the number of people's grants it removed. Each runtime deletes the app's runs, space and schedules, and the app's next refresh fails with `invalid_grant`. The app's key stops working, the app's page answers 404 and `mine` no longer lists it. It cannot be undone, and apps that hold the provider role cannot be deleted.
|
|
271
|
+
|
|
272
|
+
Only you can delete: run it yourself in the terminal of your joined machine. An agent, including your chat agent, is refused with `provider_refused`: "Only you can delete an app. Ask the person to run `crtr cloud apps delete` in their terminal." Reusing an app for tests is often simpler: change its redirect URIs and keys instead.
|
|
218
273
|
|
|
219
274
|
### Let people find and install it
|
|
220
275
|
|
|
221
|
-
`login_start_url` is where Northlight starts sign-in for a person who installs your app from a link
|
|
276
|
+
`login_start_url` is where Northlight starts sign-in for a person who installs your app from a texted link (specified behavior: pressing the texted link was not run on production, only the text send was). Your `/auth/login` route from step 7 is a valid start URL, and a loopback one is accepted:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
crtr cloud apps set-listing --app-id <client id> --listed true --price free \
|
|
280
|
+
--login-start-url http://localhost:3000/auth/login \
|
|
281
|
+
--text-welcome "Texts you a summary of your day."
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
`--text-welcome` is one or two plain sentences, at most 120 characters, with no links. Agents can install an app for people by text only while it is listed (`--listed true`). `crtr cloud apps install --app-id <client id>` sends the signed-in person a text with the install link. Run it only for yourself or for a person who asked: it sends a real text.
|
|
222
285
|
|
|
223
286
|
### Rotate keys
|
|
224
287
|
|
|
225
|
-
Add a new key beside the old one, switch, verify with a real sign-in, then retire the old key
|
|
288
|
+
Add a new key beside the old one, switch, verify with a real sign-in, then retire the old key:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
npx @north-light/sdk keygen --out local/app-key.next.json --app <client id> --yes
|
|
292
|
+
# point CROUTER_KEY_FILE at local/app-key.next.json, restart, and sign in once
|
|
293
|
+
crtr cloud apps keys list --app-id <client id>
|
|
294
|
+
crtr cloud apps keys retire --app-id <client id> --kid <old kid>
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
The old key keeps working until you retire it, and your app's refreshes keep working after. [Rotating your key](../getting-started.md#rotating-your-key) has the full sequence.
|
|
226
298
|
|
|
227
299
|
## Getting a result back from a run
|
|
228
300
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.427",
|
|
4
4
|
"description": "crtr — agent runtime with memory, plugins, and marketplaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
},
|
|
138
138
|
"license": "UNLICENSED",
|
|
139
139
|
"dependencies": {
|
|
140
|
-
"@north-light/identity": "0.3.
|
|
140
|
+
"@north-light/identity": "0.3.427",
|
|
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.427",
|
|
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.427",
|
|
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.427",
|
|
22
22
|
"cron-parser": "^5.6.0",
|
|
23
23
|
"esbuild": "^0.27.7",
|
|
24
24
|
"jose": "^6.2.12",
|
|
@@ -12095,17 +12095,17 @@
|
|
|
12095
12095
|
},
|
|
12096
12096
|
"packages/crouter-api": {
|
|
12097
12097
|
"name": "@north-light/api",
|
|
12098
|
-
"version": "0.3.
|
|
12098
|
+
"version": "0.3.427",
|
|
12099
12099
|
"license": "UNLICENSED"
|
|
12100
12100
|
},
|
|
12101
12101
|
"packages/crouter-env-docker": {
|
|
12102
12102
|
"name": "@north-light/env-docker",
|
|
12103
|
-
"version": "0.3.
|
|
12103
|
+
"version": "0.3.427",
|
|
12104
12104
|
"license": "UNLICENSED"
|
|
12105
12105
|
},
|
|
12106
12106
|
"packages/crouter-identity": {
|
|
12107
12107
|
"name": "@north-light/identity",
|
|
12108
|
-
"version": "0.3.
|
|
12108
|
+
"version": "0.3.427",
|
|
12109
12109
|
"license": "UNLICENSED",
|
|
12110
12110
|
"dependencies": {
|
|
12111
12111
|
"jose": "^6.2.1"
|
|
@@ -12113,20 +12113,20 @@
|
|
|
12113
12113
|
},
|
|
12114
12114
|
"packages/crouter-plugin": {
|
|
12115
12115
|
"name": "@north-light/plugin",
|
|
12116
|
-
"version": "0.3.
|
|
12116
|
+
"version": "0.3.427",
|
|
12117
12117
|
"license": "UNLICENSED",
|
|
12118
12118
|
"dependencies": {
|
|
12119
|
-
"@north-light/api": "0.3.
|
|
12120
|
-
"@north-light/identity": "0.3.
|
|
12119
|
+
"@north-light/api": "0.3.427",
|
|
12120
|
+
"@north-light/identity": "0.3.427"
|
|
12121
12121
|
}
|
|
12122
12122
|
},
|
|
12123
12123
|
"packages/crouter-sdk": {
|
|
12124
12124
|
"name": "@north-light/sdk",
|
|
12125
|
-
"version": "0.3.
|
|
12125
|
+
"version": "0.3.427",
|
|
12126
12126
|
"license": "UNLICENSED",
|
|
12127
12127
|
"dependencies": {
|
|
12128
|
-
"@north-light/api": "0.3.
|
|
12129
|
-
"@north-light/identity": "0.3.
|
|
12128
|
+
"@north-light/api": "0.3.427",
|
|
12129
|
+
"@north-light/identity": "0.3.427",
|
|
12130
12130
|
"jose": "^6.2.1"
|
|
12131
12131
|
},
|
|
12132
12132
|
"bin": {
|