@north-light/cli 0.3.424 → 0.3.426
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/core/runtime/broker/turn-replies.d.ts +21 -3
- package/dist/core/runtime/broker/turn-replies.js +1 -1
- package/dist/core/runtime/broker.js +1 -1
- 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
|
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import { type TurnReply } from '../../../shared/turn-replies.js';
|
|
2
2
|
import type { BrokerToClient } from '../broker-protocol.js';
|
|
3
|
+
export interface TurnReplyTrackerOptions {
|
|
4
|
+
/** Rewrites a turn's prose; null on failure. Defaults to the light-model call. */
|
|
5
|
+
generate?: (userText: string, prose: readonly string[]) => Promise<string | null>;
|
|
6
|
+
/** Whether the `consolidate_replies` experiment is on. Defaults to user config. */
|
|
7
|
+
enabled?: () => boolean;
|
|
8
|
+
}
|
|
3
9
|
/** Consolidated replies (`consolidate_replies` experiment): tracks the open
|
|
4
10
|
* turn's prose messages and rewrites them into one reply with a no-context light
|
|
5
11
|
* model whenever a second or later prose message ends. A turn runs from one
|
|
6
12
|
* message the person typed to the next; everything the runtime injects stays
|
|
7
13
|
* inside it. Rewrites persist to `turn-replies.json` and broadcast as
|
|
8
|
-
* `turn_reply` frames; the viewer owns presentation.
|
|
14
|
+
* `turn_reply` frames; the viewer owns presentation.
|
|
15
|
+
*
|
|
16
|
+
* The open turn is a projection of the bound session's branch, so anything that
|
|
17
|
+
* moves the leaf in place (a fault-retry rewind, `/tree`) must call `seed()`
|
|
18
|
+
* again: otherwise prose from the abandoned branch stays in every later rewrite,
|
|
19
|
+
* and the viewer — which honours a rewrite only when all its sources are still
|
|
20
|
+
* in the turn — falls back to showing every message verbatim. */
|
|
9
21
|
export declare class TurnReplyTracker {
|
|
10
22
|
private readonly broadcast;
|
|
11
23
|
private readonly path;
|
|
@@ -14,10 +26,16 @@ export declare class TurnReplyTracker {
|
|
|
14
26
|
private turn;
|
|
15
27
|
private inFlight;
|
|
16
28
|
private queued;
|
|
17
|
-
|
|
29
|
+
/** Bumped by every `seed()`. A rewrite requested against an earlier branch
|
|
30
|
+
* may name prose that branch no longer holds, so its result is dropped. */
|
|
31
|
+
private epoch;
|
|
32
|
+
private readonly generate;
|
|
33
|
+
private readonly enabled;
|
|
34
|
+
constructor(nodeId: string, broadcast: (frame: BrokerToClient) => void, options?: TurnReplyTrackerOptions);
|
|
18
35
|
snapshot(): Record<string, TurnReply>;
|
|
19
36
|
/** Rebuild the open turn from the bound session's messages, so a broker
|
|
20
|
-
* restart mid-turn keeps rewriting it
|
|
37
|
+
* restart mid-turn keeps rewriting it and a rewind drops the prose of the
|
|
38
|
+
* branch it left. Earlier turns are frozen. */
|
|
21
39
|
seed(messages: readonly unknown[]): void;
|
|
22
40
|
observe(event: {
|
|
23
41
|
type?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var c=Object.defineProperty;var o=(n,t)=>c(n,"name",{value:t,configurable:!0});import{join as l}from"node:path";import{nodeRunJobDir as m}from"../../canvas/paths.js";import{readConfig as y}from"../../config.js";import{emitEvent as k}from"../../events/emit.js";import{atomicWriteJson as g,readJsonIfExists as b}from"../../fs-utils.js";import{assistantVisibleText as T,isTrueUserMessage as h}from"../../../shared/tool-groups.js";import{generatedContextText as p}from"../../../shared/generated-context.js";import{isSettledAssistant as x,messageKey as a,normalizeTurnReplyRecord as j,START_TURN_KEY as d}from"../../../shared/turn-replies.js";import{headlessTurnReply as w}from"../turn-reply-summary.js";function f(n,t){return n!==void 0&&n.sources.length===t.length&&n.sources.every((s,e)=>s===t[e].key)}o(f,"sameSources");class A{static{o(this,"TurnReplyTracker")}broadcast;path;replies=new Map;tasks=new Set;turn={key:d,userText:"",prose:[]};inFlight;queued;epoch=0;generate;enabled;constructor(t,s,e={}){this.broadcast=s,this.generate=e.generate??w,this.enabled=e.enabled??(()=>y("user").consolidate_replies),this.path=l(m(t),"turn-replies.json");try{for(const[r,i]of Object.entries(j(b(this.path))))this.replies.set(r,i)}catch{}}snapshot(){return Object.fromEntries(this.replies)}seed(t){this.epoch+=1,this.queued=void 0;let s={key:d,userText:"",prose:[]};for(const e of t){if(e===null||typeof e!="object")continue;const r=e;if(h(r)){const u=a(r);u!==void 0&&(s={key:u,userText:p(r),prose:[]});continue}const i=this.proseOf(r);i!==void 0&&s.prose.push(i)}this.turn=s,this.maybeGenerate()}observe(t){if(t.type!=="message_start"&&t.type!=="message_end")return;const s=t.message;if(s===null||typeof s!="object")return;const e=s;if(t.type==="message_start"){if(!h(e))return;const i=a(e);i!==void 0&&i!==this.turn.key&&(this.turn={key:i,userText:p(e),prose:[]});return}const r=this.proseOf(e);r===void 0||this.turn.prose.some(i=>i.key===r.key)||(this.turn.prose.push(r),this.maybeGenerate())}hasTasks(){return this.tasks.size>0}async waitForTasks(){for(;this.tasks.size>0;)await Promise.allSettled([...this.tasks])}proseOf(t){if(t.role!=="assistant"||!x(t))return;const s=T(t).trim(),e=a(t);return s===""||e===void 0?void 0:{key:e,text:s}}maybeGenerate(){const{key:t,userText:s,prose:e}=this.turn;if(e.length<2||!this.enabled()||f(this.replies.get(t),e))return;const r={key:t,userText:s,prose:[...e],epoch:this.epoch};if(this.inFlight!==void 0){this.queued=r;return}this.run(r)}run(t){this.inFlight=t;let s;s=this.generate(t.userText,t.prose.map(e=>e.text)).then(e=>{if(e===null||t.epoch!==this.epoch)return;const r={version:1,sources:t.prose.map(i=>i.key),text:e};this.replies.set(t.key,r);try{g(this.path,Object.fromEntries(this.replies))}catch(i){k({level:"warn",event:"broker.runtime.diagnostic",fields:{message:`failed to persist turn reply: ${i instanceof Error?i.message:String(i)}`}})}this.broadcast({type:"turn_reply",key:t.key,reply:r})}).catch(()=>{}).finally(()=>{this.tasks.delete(s),this.inFlight=void 0;const e=this.queued;this.queued=void 0,e!==void 0&&!f(this.replies.get(e.key),e.prose)&&this.run(e)}),this.tasks.add(s)}}export{A as TurnReplyTracker};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var Pe=Object.defineProperty;var e=(t,r)=>Pe(t,"name",{value:r,configurable:!0});import{envInboxBetweenTurns as Oe,envModelIntent as Ie,envNodeCwd as De,envNodeId as Ne,envProfileId as Fe,envScopes as Le,isSandboxedNode as Be,scopeAllowed as $e}from"../../shared/env.js";import{createServer as Ue}from"node:net";import{existsSync as Se,readFileSync as Ge,unlinkSync as ye}from"node:fs";import{join as be}from"node:path";import{randomUUID as x}from"node:crypto";import{mark as Ke}from"../timing.js";import{SUBMIT_RESULT_TOOL as je}from"./structured-output.js";import{installSessionManagerStorePatch as We}from"../conversation-store/index.js";import{getAgentDir as Y,ModelRegistry as qe,ModelRuntime as Ve}from"@earendil-works/pi-coding-agent";import{nodeRunDir as Xe,viewSocketPath as He}from"../canvas/paths.js";import{bindBrokerEventSource as Ye}from"../events/source.js";import{emitEvent as E,flushBrokerLog as ze}from"../events/emit.js";import{operationIdContext as Je}from"../events/operation-id.js";import{findMissingPackagePaths as Qe}from"./package-health.js";import{installBrokerConfigSnapshot as Ze}from"../config.js";import{FRONT_DOOR_ENV as en}from"./front-door.js";import{CANVAS_EXTENSIONS as oe,piInvocationToSdkConfig as nn}from"./launch.js";import{assertEngineVersion as tn,loadBrokerEngine as on}from"./broker-sdk.js";import{initCrtrTheme as rn}from"./crtr-theme.js";import{resolveBundledPiPackageDir as sn}from"./pi-cli.js";import{captureSessionChatCapabilities as an}from"./command-surface.js";import{InMemoryCredentialStore as dn}from"@earendil-works/pi-ai";import{configureBrokerModelRuntime as ln}from"./broker/model-adapter.js";import{mutateBrokerFault as z,recordBrokerTurn as Te}from"./broker/daemon-ops.js";import{modelRequestFromConfig as cn,expandModelCandidates as mn,parseModelSpec as un,unregisteredConcreteModel as fn}from"../model-routes.js";import{KNOWN_STREAM_WAIT_UI as pn}from"./stream-watchdog.js";import{probeOnline as gn}from"./connectivity.js";import{CRTR_CYCLE_CUSTOM_TYPE as vn,cycleAwareMessages as hn,endedByAbort as _n}from"./session-cycles.js";import{turnErrorOf as wn}from"../llm/refusal.js";import{projectTranscript as Sn}from"./session-visibility.js";import{currentTurnVisibility as yn}from"./turn-visibility.js";import{isUnknownModel as re}from"./model-selection.js";import{BrokerClientRegistry as bn}from"./broker/client-registry.js";import{ToolGroupTracker as Tn}from"./broker/tool-groups.js";import{TurnReplyTracker as En}from"./broker/turn-replies.js";import{onNodeNamed as kn}from"./broker/node-named.js";import{FaultRetry as Rn,willDaemonRetry as Mn}from"./broker/fault-retry.js";import{RunTurnEnds as xn}from"./broker/run-turn-end.js";import{createTurnAdmissionGate as Cn}from"./broker/turn-admission.js";import{BROKER_IDLE_TURN_START as An,BROKER_RUN_TURN_BUSY as Pn}from"./broker/held-deferred-inbox.js";import{prependRunDeferredMail as On,settleRunDeferredMail as In}from"./broker/run-deferred-mail.js";import{createRunTurnQueue as Dn}from"./broker/run-turn-queue.js";import{EventProjection as Nn}from"./broker/event-projection.js";import{createFrameDispatchContext as Fn,handleFrame as Ln}from"./broker/frame-dispatch.js";import{hydratePersistedAdvertisedCommandMessages as Bn,installAdvertisedCommandInvocationContract as $n}from"./advertised-command-invocation.js";import{RebindDriver as Un}from"./broker/rebind.js";import{beginRuntimeTeardown as Gn}from"./broker/extension-abort.js";import{receiveNodeToken as Kn}from"./broker/node-token.js";import{serveCliRelay as jn}from"./broker/cli-relay.js";import{decodeKickoffOrigin as Wn,KICKOFF_ORIGIN_ENV as qn}from"./stamp/protocol.js";import{announceMessageOrigin as Vn}from"./stamp/channel.js";import{brokerExtensionState as Xn,commitBrokerModel as Hn}from"./broker/daemon-ops.js";import{composerNames as Yn}from"./broker/frame-memory-refs.js";import{FrameOverflowError as zn,normalizeSnapshotTail as Jn}from"./broker-protocol.js";import{ReviewBoundaryNavigationError as fo,REVIEW_BOUNDARY_NAVIGATION_CODE as po}from"./broker/frame-replies.js";function se(t){return new qe(t.modelRuntime)}e(se,"registryOf");function $(t,r){if(t==null||re(t))return null;const i=r!==void 0&&r!==""&&r!=="off"?`:${r}`:"";return`${t.provider}/${t.id}${i}`}e($,"formatModelSpec");function Qn(t,r){const i=$(t,void 0);return i===null?null:`${i}:${r===void 0||r===""?"off":r}`}e(Qn,"formatExactModelSpec");const Zn=12e4;let J=null;import{brokerSignalClient as Q}from"./broker/signal-stream.js";function et(){Q().stop();const t=J;if(J=null,t!==null)try{t.dispose()}catch{}}e(et,"disposeActiveSession");function nt(t,r){const i=$e(r,"crtr:act")?void 0:["bash"],m=t!==void 0&&t.length>0?[...new Set([...t,je])]:t===void 0?void 0:[...t];return{...m!==void 0?{tools:m}:{},...i!==void 0?{excludeTools:i}:{}}}e(nt,"brokerToolOptions");async function co(t,r=process.hrtime.bigint(),i=new Date().toISOString()){Ye(t);const m=await Kn(),s=await jn(t,m);process.env.CRTR_API_SOCKET=s.path,Je.fresh(()=>{E({level:"info",event:"broker.startup.cli_entry",ts:i,duration_ms:0,fields:{runtime_version:process.version,engine_version:null,model:null}}),E({level:"info",event:"broker.startup.module_loaded",duration_ms:Number(process.hrtime.bigint()-r)/1e6,fields:{runtime_version:process.version,engine_version:null,model:null}})});const a=new Set;let l=null;const d=e((n,o=null)=>{a.has(n)||(a.add(n),Ke(n,{duration_ms:Number(process.hrtime.bigint()-r)/1e6}),E({level:"info",event:n,duration_ms:Number(process.hrtime.bigint()-r)/1e6,fields:{runtime_version:process.version,engine_version:l,model:o}}))},"emitStartupMilestone");process.env[en]="1",process.env.CRTR_BROKER_PID=String(process.pid),delete process.env.TMUX_PANE,delete process.env.TMUX,We(t);const f=await on();tn(f.VERSION),l=f.VERSION;const w=Xe(t),k=be(w,"broker-launch.json"),S=JSON.parse(Ge(k,"utf8"));if(Be()){if(!S.configSnapshot)throw new Error("broker launch is missing its config snapshot");Ze(S.configSnapshot)}const u=nn(S);u.modelExact===!0?process.env.CRTR_MODEL_EXACT="1":delete process.env.CRTR_MODEL_EXACT;const y=await Yn(t);let b,C;const q=e(n=>{b!==void 0?b(n):C??=n},"onSignal");process.on("SIGTERM",()=>q("SIGTERM")),process.on("SIGINT",()=>q("SIGINT"));let{session:U,services:A,resuming:ie,runtime:G}=await st(f,u);d("broker.startup.session_built");const R=(await Xn(t)).node.review_binding?.review_id,T=Oe();J=U;const D=gn;try{process.env.PI_PACKAGE_DIR=sn(),rn(A.settingsManager.getTheme(),Y(),!1)}catch(n){E({level:"error",event:"broker.theme.initialization_failed",error:n})}try{for(const n of Qe(Y()))E({level:"warn",event:"broker.startup.warning",fields:{stage:"package_health",source:n.source,resolved_path:n.resolved}})}catch{}const K=He(t),p=new bn(t),V=new Map,O=new Tn(t,n=>p.broadcast(n)),X=new En(t,n=>p.broadcast(n));kn(n=>p.broadcast({type:"node_named",...n}));let N=!1,ae=!1,H,de,g,j;const Z=new xn;let F,le=e(()=>{},"notifyTurnAccepted"),ce=e(()=>{},"resetFrameState");const ee=Cn();let me=e(()=>0,"pendingPromptDispatches"),ue;const L=Dn({runNode:T,session:e(()=>g.session(),"session"),isReplacing:e(()=>g.isReplacing(),"isReplacing"),isAgentRunActive:e(()=>g.isAgentRunActive(),"isAgentRunActive"),pendingDispatches:e(()=>me(),"pendingDispatches"),turnAdmission:ee,prependDeferred:e(n=>On(t,n),"prependDeferred"),onError:e(n=>E({level:"error",event:"broker.inbox.idle_prompt_failed",error:n}),"onError")}),fe=L.drain;u.firstPrompt!==void 0&&u.firstPrompt!==""&&L.holdForKickoff(),globalThis[An]=L.start,globalThis[Pn]=L.busy,g=new Un({nodeId:t,cfg:u,session:U,services:A,runtime:G,registry:p,toolGroups:O,turnReplies:X,transcriptMessages:e(n=>Ee(n,R)?.map(({message:o})=>o)??n.sessionManager.buildSessionContext().messages,"transcriptMessages"),uiContext:e(()=>de,"uiContext"),faultRetry:e(()=>j,"faultRetry"),projection:e(()=>F,"projection"),setActiveSession:e(n=>{if(J=n,ue?.(),T){let o="stop",c,v=!1;ue=n.subscribe(h=>{if(h.type==="agent_start"&&(o="stop",c=void 0,v=!1,Z.started()),h.type==="agent_end"){const _=[...h.messages??[]].reverse().find(B=>B.role==="assistant");_&&(o=_.stopReason,c=_n(_)?void 0:wn(_),v=_.role==="assistant"&&Mn(_,n.model?.contextWindow))}h.type==="agent_settled"&&(Z.settled(n.sessionManager,{stop_reason:o,...c?{error:c}:{}},v),In(t,n).catch(_=>{E({level:"error",event:"broker.run_deferred_mail.settle_failed",error:_})}),queueMicrotask(()=>{g.session()===n&&fe()}))})}},"setActiveSession"),disposeAndExit:e((n,o)=>W(n,o),"disposeAndExit"),onSessionRebind:e(()=>{ce()},"onSessionRebind"),onReplacementIdle:e(()=>{T&&fe()},"onReplacementIdle"),reWelcomeAll:e(()=>ne(),"reWelcomeAll")});const pe=e(async(n,o=!1)=>{const c=g.session(),v=$(c.model,c.thinkingLevel);if(v===null)return null;const h=await Hn(t,{spec:v,...n!==void 0?{intent:n}:{},userSelected:o});return o&&(u.model=h.modelOverride,n!==void 0?(process.env.CRTR_MODEL_INTENT=`${n.family}/${n.strength}`,delete process.env.CRTR_MODEL_EXACT,u.modelExact=void 0):(delete process.env.CRTR_MODEL_INTENT,process.env.CRTR_MODEL_EXACT="1",u.modelExact=!0)),h.modelOverride},"persistModelChoice"),ge=e(async(n=!0,o,c=!1)=>{const v=g.session(),h=$(v.model,v.thinkingLevel),_=n?await pe(o,c):h;p.broadcast({type:"model_changed",model:re(v.model)?void 0:v.model,spec:_})},"broadcastModelChanged");j=new Rn({nodeId:t,cfg:u,retryProbe:D,currentSession:e(()=>g.session(),"currentSession"),currentServices:e(()=>g.services(),"currentServices"),installedGeneration:e(()=>g.installedGeneration(),"installedGeneration"),registryOf:se,formatModelSpec:$,broadcastModelChanged:ge,turnAdmission:ee,reWelcomeAll:e(()=>ne(),"reWelcomeAll"),...T?{turnEnds:Z}:{}});const ve=e(()=>{j.rearmIfStranded().catch(n=>{E({level:"warn",event:"broker.runtime.diagnostic",fields:{message:`fault retry re-arm failed: ${n instanceof Error?n.message:String(n)}`}})})},"rearmStrandedRetry");Q().subscribe("resync",ve),Q().subscribe("fault-retry",ve),F=new Nn({registry:p,faultRetry:j,installedGeneration:e(()=>g.installedGeneration(),"installedGeneration"),currentSession:e(()=>g.session(),"currentSession"),notifyTurnAccepted:e(()=>le(),"notifyTurnAccepted"),emitStartupMilestone:d,formatModelSpec:$});const Re=e(n=>{const o=g.session(),c=Ee(o,R),v=Jn(n.snapshotTail),h=c===void 0||v===void 0?c:c.slice(-v),_=h?.map(({message:I})=>I)??o.sessionManager.buildSessionContext().messages,B=O.summaries(),we=h!==void 0&&v!==void 0?tt(_):void 0,Ae=we===void 0?B:Object.fromEntries(Object.entries(B).filter(([I])=>we.has(I)));return{messages:_,...h===void 0?{}:{messageIds:h.map(({id:I})=>I)},messageVisibility:h?.map(({visibility:I})=>I)??_.map(()=>"visible"),turnVisibility:yn(),stats:o.getSessionStats(),state:{model:re(o.model)?void 0:o.model,thinkingLevel:o.thinkingLevel,isStreaming:o.isStreaming,isCompacting:o.isCompacting,steeringMode:o.steeringMode,followUpMode:o.followUpMode,sessionFile:o.sessionFile,sessionId:o.sessionId,sessionName:o.sessionName,autoCompactionEnabled:o.autoCompactionEnabled,messageCount:o.messages.length,pendingMessageCount:o.pendingMessageCount},display:{...p.displaySnapshot()},toolGroupSummaries:Ae,turnReplies:X.snapshot(),workingActivity:F.currentWorkingActivity(),queued:{steering:[...o.getSteeringMessages()],followUp:[...o.getFollowUpMessages()]}}},"buildSnapshot"),he=e(n=>{const o=n.role==="controller"?V.values().next().value:void 0;p.sendFrame(n,{type:"welcome",snapshot:Re(n),role:n.role,pending_dialog:o!==void 0?o.request:null,agentDir:Y()},!0)},"sendWelcome"),Me=e(n=>{const o=[...V.values()];for(let c=1;c<o.length;c++)p.sendFrame(n,o[c].request)},"replayExtraPendingDialogsTo"),ne=e(()=>{p.forEachHelloed(he)},"reWelcomeAll"),_e=e(async n=>{if(!N){N=!0,Q().stop(),O.persistPendingSummary(),F.dispose(),et();try{H?.close()}catch{}s.close(),p.dispose();try{Se(K)&&ye(K)}catch{}p.clearAttachState(),await ze(),process.exit(n)}},"completeExit"),W=e((n,o=0)=>{if(N)return;const c=n==="shutdown-hook"||n==="shutdown-after-settle";if(c&&O.finishOpenToolGroup(!0),c&&(O.hasSummaryTasks()||X.hasTasks())){if(ae)return;ae=!0,Promise.allSettled([O.waitForSummaryTasks(),X.waitForTasks()]).finally(()=>_e(o));return}_e(o)},"disposeAndExit");de=it({writable:e(()=>p.writableClients(),"writable"),forward:e((n,o)=>p.sendFrame(n,o),"forward"),pending:V,broadcast:e(n=>p.broadcastUi(n),"broadcast"),onKnownStreamWait:e(n=>F.knownStreamWait(n),"onKnownStreamWait")}),g.setRebindSession(),await g.enqueueRebind(U,A),d("broker.startup.extensions_bound");const M=Fn({nodeId:t,registry:p,toolGroups:O,projection:F,rebind:g,boundaryReviewId:R,pendingDialogs:V,sendWelcome:he,replayExtraPendingDialogsTo:Me,reWelcomeAll:ne,disposeAndExit:W,persistModelChoice:pe,broadcastModelChanged:ge,registryOf:se,formatModelSpec:$,formatExactModelSpec:Qn,turnAdmission:ee,memoryInventory:y,runNode:T,onKickoffAdmitted:L.releaseKickoff});le=M.notifyTurnAccepted,ce=M.resetForSession,me=M.pendingPromptDispatches;const xe=2500;let te=!1;const Ce=e(async n=>{if(N||te)return;if(te=!0,g.isAgentRunActive()||g.session().isStreaming||M.pendingPromptDispatches()>0){const c=Date.now()+xe;for(;M.pendingPromptDispatches()>0&&!g.session().isStreaming&&Date.now()<c;)await new Promise(v=>setTimeout(v,25));if(g.session().isStreaming){const v=c-Date.now();if(v>0){let h;try{Gn(),await Promise.race([g.session().abort().then(()=>!0,()=>!1),new Promise(B=>{h=setTimeout(()=>B(!1),v)})])&&await Te(t,{event:"closed",end_reason:"teardown-aborted"})}finally{h!==void 0&&clearTimeout(h)}}}else(M.pendingPromptDispatches()>0||g.isAgentRunActive())&&await Te(t,{event:"interrupted"})}W(n)},"abortAndExit");if(b=e(n=>{Ce(n)},"signalTeardown"),C!==void 0){b(C);return}try{Se(K)&&ye(K)}catch{}H=Ue(n=>{const o=p.create(n);n.on("data",v=>{let h;try{h=o.decoder.push(v)}catch(_){_ instanceof zn?(p.sendFrame(o,{type:"error",code:"frame_overflow",message:_.message}),p.dropClient(o,`frame overflow: ${_.message}`)):p.dropClient(o,`decoder error: ${String(_)}`);return}for(const _ of h)try{Ln(M,o,_)}catch{}});const c=e(()=>{p.removeClient(o)},"drop");n.on("close",c),n.on("error",()=>{})}),H.on("error",n=>{N||(E({level:"error",event:"broker.socket.failed",error:n}),W("socket-error",1))}),H.listen(K,async()=>{Promise.all([z(t,{op:"clear",link:"viewer\u2194broker"}),z(t,{op:"clear",link:"relay\u2194broker"})]).catch(o=>E({level:"error",event:"broker.daemon.operation.failed",node_id:t,error:o,fields:{operation:"broker_socket_bound_fault_clear"}}));let n;try{n=await j.recoverOnStartup()}catch(o){E({level:"error",event:"broker.daemon.operation.failed",node_id:t,error:o,fields:{operation:"recovery_startup"}}),W("recovery-startup-failed",1);return}if(!(N||te))if(n!=="retry-armed"&&u.firstPrompt!==void 0&&u.firstPrompt!==""){const o=Wn(process.env[qn]);o!==null&&Vn(u.firstPrompt,o);const c=process.env.CRTR_KICKOFF_MESSAGE_IDS;M.kickoff(u.firstPrompt,c?JSON.parse(c):[])}else L.releaseKickoff()}),p.persistAttachState()}e(co,"runBroker");function tt(t){const r=new Set;for(const i of t)if(!(i.role!=="assistant"||!Array.isArray(i.content)))for(const m of i.content)m!==null&&typeof m=="object"&&m.type==="toolCall"&&typeof m.id=="string"&&r.add(m.id);return r}e(tt,"toolCallIds");function Ee(t,r){const i=t.sessionManager,m=hn(i);return m!==void 0&&Bn(m.map(({message:s})=>s)),m===void 0?void 0:Sn(m,{boundaryReviewId:r,sessionEntries:i.getEntries?.()})}e(Ee,"snapshotMessages");function ke(){const t=Ne();if(t===void 0||t==="")throw new Error("broker launch is missing CRTR_NODE_ID");return t}e(ke,"currentBrokerNodeId");async function ot(t,r){const i=cn(r.model,Ie(),r.modelExact===!0);if(i===void 0)return;const m=se(t),s={cwd:De()??r.cwd,profileId:Fe()},a=fn(i,m,s);if(a!==void 0)throw await z(ke(),{op:"record",input:{link:"crtr\u2192pi",op:"model resolution at launch",kind:"model-not-found",retry:{disposition:"fatal"},message:`model '${a.request.spec}' is not registered in the serving model registry`}}),new Error(`[broker] FATAL: model '${a.request.spec}' is not registered in the serving model registry`);const l=mn(i,s.cwd,s.profileId),d=un(i.spec);for(const f of l){const w=m.find(f.providerId,f.modelId);if(w===void 0)continue;const S=d?.providerId===f.providerId&&d.modelId===f.modelId?d.thinkingLevel??(f.thinkingLevel===void 0?void 0:"off"):f.thinkingLevel;return{model:w,thinkingLevel:S}}if(r.modelExact===!0){const f=l.map(w=>`'${w.providerId}/${w.modelId}' unregistered`).join("; ");throw await z(ke(),{op:"record",input:{link:"crtr\u2192pi",op:"model resolution at launch",kind:"model-not-found",retry:{disposition:"fatal"},message:`exact-consult model '${r.model}' is unavailable \u2014 refusing to substitute (${f})`}}),new Error(`[broker] FATAL: exact-consult model '${r.model}' is unavailable`)}E({level:"warn",event:"broker.model.none_viable",fields:{rejected_candidates:l.map(f=>({provider:f.providerId,model:f.modelId,reason:"unregistered"})).slice(0,16)}})}e(ot,"resolveLaunchModel");function rt(t){let r=!0;return{runtime:new Proxy(t,{get(m,s,a){return r&&s==="getAvailableSnapshot"?()=>[]:r&&s==="getAvailable"?async()=>[]:r&&s==="getModel"?()=>{}:Reflect.get(m,s,a)}}),reveal:e(()=>{r=!1},"reveal")}}e(rt,"modelLessRegistry");async function st(t,r){const i=Y(),m=e(async S=>{const u=await Ve.create({credentials:new dn,modelsPath:be(S.agentDir,"models.json")});ln(u);const y=await t.createAgentSessionServices({cwd:S.cwd,agentDir:S.agentDir,modelRuntime:u,resourceLoaderOptions:{additionalExtensionPaths:r.extensionPaths,appendSystemPrompt:r.appendSystemPromptPath!==void 0?[r.appendSystemPromptPath]:void 0,noContextFiles:!0,extensionsOverride:e(P=>({...P,extensions:P.extensions.filter(R=>!/[/\\]provider-rotation\.(?:ts|js)$/.test(R.path))}),"extensionsOverride")}}),b=y.resourceLoader.getExtensions();if(r.extensionPaths.some(P=>oe.includes(P))){const P=new Set(b.extensions.map(T=>T.path)),R=oe.filter(T=>!P.has(T));if(R.length>0){const T=b.errors.filter(D=>oe.includes(D.path)).map(D=>`${D.path}: ${D.error}`).join("; ");throw new Error(`[broker] FATAL: mandatory canvas extensions failed to load: ${R.join(", ")}${T===""?"":` (${T})`}`)}}y.settingsManager.getRetryEnabled=()=>!1;const C=await ot(y,r),q=C?.model,U=C?.thinkingLevel,A=C===void 0?rt(y.modelRuntime):void 0,ie=A!==void 0?{...y,modelRuntime:A.runtime}:y,G=await t.createAgentSessionFromServices({services:ie,sessionManager:S.sessionManager,model:q,thinkingLevel:U,...nt(r.tools,Le()),sessionStartEvent:S.sessionStartEvent});return $n(G.session),an(G.session),A?.reveal(),{...G,services:y}},"buildForManager"),s=r.resumeSessionPath,a=r.forkFrom!==void 0&&r.forkFrom!=="";if(!a&&(s===void 0||s==="")&&r.resumeSessionId!==void 0&&r.resumeSessionId!=="")throw new Error(`[broker] resume requires a session .jsonl PATH; got only a bare id '${r.resumeSessionId}' (pi_session_file was never captured for this node)`);const l=!a&&s!==void 0&&s!=="",d=l&&r.newCycle===!0,f=l&&!d;let w;if(a)w=t.SessionManager.forkFrom(r.forkFrom,r.cwd);else if(f||d){if(w=t.SessionManager.open(s),d){const S=w.getLeafId();w.resetLeaf(),w.appendCustomEntry(vn,{cycle:Number(process.env.CRTR_CYCLES),fromLeaf:S})}}else w=t.SessionManager.create(r.cwd);if(t.createAgentSessionRuntime!==void 0){const S=e(async y=>{const b=await m({cwd:y.cwd,agentDir:y.agentDir,sessionManager:y.sessionManager,sessionStartEvent:y.sessionStartEvent});return{...b,diagnostics:b.services.diagnostics??[]}},"factory"),u=await t.createAgentSessionRuntime(S,{cwd:r.cwd,agentDir:i,sessionManager:w});return{session:u.session,services:u.services,resuming:f,runtime:u}}const k=await m({cwd:r.cwd,agentDir:i,sessionManager:w});return{session:k.session,services:k.services,resuming:f,runtime:void 0}}e(st,"buildBrokerSession");function it(t){const r=e((s,a,l,d)=>{if(d?.signal?.aborted)return Promise.resolve(s);const f=t.writable();return f.length===0?Promise.resolve(s):new Promise(w=>{let k;const S=e(()=>{k!==void 0&&clearTimeout(k),d?.signal?.removeEventListener("abort",u),t.pending.delete(a.id)},"cleanup"),u=e(()=>{S(),t.broadcast({type:"extension_ui_dismiss",id:a.id}),w(s)},"onAbort");d?.signal?.addEventListener("abort",u,{once:!0});const y=d?.timeout!==void 0?d.timeout:Zn;k=setTimeout(()=>{S(),t.broadcast({type:"extension_ui_dismiss",id:a.id}),w(s)},y),typeof k.unref=="function"&&k.unref(),t.pending.set(a.id,{request:a,resolve:e(b=>{S(),t.broadcast({type:"extension_ui_dismiss",id:a.id}),w(l(b))},"resolve")});for(const b of f)t.forward(b,a)})},"dialogPromise"),i=e(()=>{},"noop");return{[pn]:t.onKnownStreamWait??(s=>()=>{}),get theme(){const s=globalThis[Symbol.for("@earendil-works/pi-coding-agent:theme")];if(s==null)throw new Error("Theme not initialized. Call initTheme() first.");return s},select:e((s,a,l)=>r(void 0,{type:"extension_ui_request",id:x(),method:"select",title:s,options:a,timeout:l?.timeout},d=>"cancelled"in d&&d.cancelled?void 0:"value"in d?d.value:void 0,l),"select"),confirm:e((s,a,l)=>r(!1,{type:"extension_ui_request",id:x(),method:"confirm",title:s,message:a,timeout:l?.timeout},d=>"cancelled"in d&&d.cancelled?!1:"confirmed"in d?d.confirmed:!1,l),"confirm"),input:e((s,a,l)=>r(void 0,{type:"extension_ui_request",id:x(),method:"input",title:s,placeholder:a,timeout:l?.timeout},d=>"cancelled"in d&&d.cancelled?void 0:"value"in d?d.value:void 0,l),"input"),editor:e((s,a)=>r(void 0,{type:"extension_ui_request",id:x(),method:"editor",title:s,prefill:a},l=>"cancelled"in l&&l.cancelled?void 0:"value"in l?l.value:void 0),"editor"),setStatus:e((s,a)=>t.broadcast({type:"extension_ui_request",id:x(),method:"setStatus",statusKey:s,statusText:a}),"setStatus"),setWidget:e((s,a,l)=>{typeof a!="function"&&t.broadcast({type:"extension_ui_request",id:x(),method:"setWidget",widgetKey:s,widgetLines:a,widgetPlacement:l?.placement})},"setWidget"),setTitle:e(s=>t.broadcast({type:"extension_ui_request",id:x(),method:"setTitle",title:s}),"setTitle"),notify:e((s,a)=>{const l=a==="warn"?"warning":a;t.broadcast({type:"extension_ui_request",id:x(),method:"notify",message:s,notifyType:l})},"notify"),onTerminalInput:e(()=>i,"onTerminalInput"),setWorkingMessage:i,setWorkingVisible:i,setWorkingIndicator:i,setHiddenThinkingLabel:i,setFooter:i,setHeader:i,custom:e(()=>Promise.resolve(void 0),"custom"),pasteToEditor:i,setEditorText:i,getEditorText:e(()=>"","getEditorText"),addAutocompleteProvider:i,setEditorComponent:i,getEditorComponent:e(()=>{},"getEditorComponent"),getAllThemes:e(()=>[],"getAllThemes"),getTheme:e(()=>{},"getTheme"),setTheme:i,getToolsExpanded:e(()=>!1,"getToolsExpanded"),setToolsExpanded:i}}e(it,"makeBrokerUiContext");export{po as REVIEW_BOUNDARY_NAVIGATION_CODE,fo as ReviewBoundaryNavigationError,nt as brokerToolOptions,st as buildBrokerSession,et as disposeActiveSession,it as makeBrokerUiContext,co as runBroker,Ee as snapshotMessages};
|
|
1
|
+
var Pe=Object.defineProperty;var n=(t,o)=>Pe(t,"name",{value:o,configurable:!0});import{envInboxBetweenTurns as Oe,envModelIntent as Ie,envNodeCwd as De,envNodeId as Ne,envProfileId as Fe,envScopes as Le,isSandboxedNode as $e,scopeAllowed as Be}from"../../shared/env.js";import{createServer as Ue}from"node:net";import{existsSync as ye,readFileSync as Ge,unlinkSync as be}from"node:fs";import{join as Te}from"node:path";import{randomUUID as x}from"node:crypto";import{mark as Ke}from"../timing.js";import{SUBMIT_RESULT_TOOL as je}from"./structured-output.js";import{installSessionManagerStorePatch as We}from"../conversation-store/index.js";import{getAgentDir as Y,ModelRegistry as qe,ModelRuntime as Ve}from"@earendil-works/pi-coding-agent";import{nodeRunDir as Xe,viewSocketPath as He}from"../canvas/paths.js";import{bindBrokerEventSource as Ye}from"../events/source.js";import{emitEvent as T,flushBrokerLog as ze}from"../events/emit.js";import{operationIdContext as Je}from"../events/operation-id.js";import{findMissingPackagePaths as Qe}from"./package-health.js";import{installBrokerConfigSnapshot as Ze}from"../config.js";import{FRONT_DOOR_ENV as en}from"./front-door.js";import{CANVAS_EXTENSIONS as re,piInvocationToSdkConfig as nn}from"./launch.js";import{assertEngineVersion as tn,loadBrokerEngine as rn}from"./broker-sdk.js";import{initCrtrTheme as on}from"./crtr-theme.js";import{resolveBundledPiPackageDir as sn}from"./pi-cli.js";import{captureSessionChatCapabilities as an}from"./command-surface.js";import{InMemoryCredentialStore as dn}from"@earendil-works/pi-ai";import{configureBrokerModelRuntime as ln}from"./broker/model-adapter.js";import{mutateBrokerFault as z,recordBrokerTurn as ke}from"./broker/daemon-ops.js";import{modelRequestFromConfig as cn,expandModelCandidates as mn,parseModelSpec as un,unregisteredConcreteModel as fn}from"../model-routes.js";import{KNOWN_STREAM_WAIT_UI as pn}from"./stream-watchdog.js";import{probeOnline as gn}from"./connectivity.js";import{CRTR_CYCLE_CUSTOM_TYPE as vn,cycleAwareMessages as hn,endedByAbort as Sn}from"./session-cycles.js";import{turnErrorOf as _n}from"../llm/refusal.js";import{projectTranscript as wn}from"./session-visibility.js";import{currentTurnVisibility as yn}from"./turn-visibility.js";import{isUnknownModel as oe}from"./model-selection.js";import{BrokerClientRegistry as bn}from"./broker/client-registry.js";import{ToolGroupTracker as Tn}from"./broker/tool-groups.js";import{TurnReplyTracker as kn}from"./broker/turn-replies.js";import{onNodeNamed as En}from"./broker/node-named.js";import{FaultRetry as Rn,willDaemonRetry as Mn}from"./broker/fault-retry.js";import{RunTurnEnds as xn}from"./broker/run-turn-end.js";import{createTurnAdmissionGate as Cn}from"./broker/turn-admission.js";import{BROKER_IDLE_TURN_START as An,BROKER_RUN_TURN_BUSY as Pn}from"./broker/held-deferred-inbox.js";import{prependRunDeferredMail as On,settleRunDeferredMail as In}from"./broker/run-deferred-mail.js";import{createRunTurnQueue as Dn}from"./broker/run-turn-queue.js";import{EventProjection as Nn}from"./broker/event-projection.js";import{createFrameDispatchContext as Fn,handleFrame as Ln}from"./broker/frame-dispatch.js";import{hydratePersistedAdvertisedCommandMessages as $n,installAdvertisedCommandInvocationContract as Bn}from"./advertised-command-invocation.js";import{RebindDriver as Un}from"./broker/rebind.js";import{beginRuntimeTeardown as Gn}from"./broker/extension-abort.js";import{receiveNodeToken as Kn}from"./broker/node-token.js";import{serveCliRelay as jn}from"./broker/cli-relay.js";import{decodeKickoffOrigin as Wn,KICKOFF_ORIGIN_ENV as qn}from"./stamp/protocol.js";import{announceMessageOrigin as Vn}from"./stamp/channel.js";import{brokerExtensionState as Xn,commitBrokerModel as Hn}from"./broker/daemon-ops.js";import{composerNames as Yn}from"./broker/frame-memory-refs.js";import{FrameOverflowError as zn,normalizeSnapshotTail as Jn}from"./broker-protocol.js";import{ReviewBoundaryNavigationError as ur,REVIEW_BOUNDARY_NAVIGATION_CODE as fr}from"./broker/frame-replies.js";function se(t){return new qe(t.modelRuntime)}n(se,"registryOf");function B(t,o){if(t==null||oe(t))return null;const i=o!==void 0&&o!==""&&o!=="off"?`:${o}`:"";return`${t.provider}/${t.id}${i}`}n(B,"formatModelSpec");function Qn(t,o){const i=B(t,void 0);return i===null?null:`${i}:${o===void 0||o===""?"off":o}`}n(Qn,"formatExactModelSpec");const Zn=12e4;let J=null;import{brokerSignalClient as Q}from"./broker/signal-stream.js";function et(){Q().stop();const t=J;if(J=null,t!==null)try{t.dispose()}catch{}}n(et,"disposeActiveSession");function nt(t,o){const i=Be(o,"crtr:act")?void 0:["bash"],m=t!==void 0&&t.length>0?[...new Set([...t,je])]:t===void 0?void 0:[...t];return{...m!==void 0?{tools:m}:{},...i!==void 0?{excludeTools:i}:{}}}n(nt,"brokerToolOptions");async function lr(t,o=process.hrtime.bigint(),i=new Date().toISOString()){Ye(t);const m=await Kn(),s=await jn(t,m);process.env.CRTR_API_SOCKET=s.path,Je.fresh(()=>{T({level:"info",event:"broker.startup.cli_entry",ts:i,duration_ms:0,fields:{runtime_version:process.version,engine_version:null,model:null}}),T({level:"info",event:"broker.startup.module_loaded",duration_ms:Number(process.hrtime.bigint()-o)/1e6,fields:{runtime_version:process.version,engine_version:null,model:null}})});const a=new Set;let l=null;const d=n((e,r=null)=>{a.has(e)||(a.add(e),Ke(e,{duration_ms:Number(process.hrtime.bigint()-o)/1e6}),T({level:"info",event:e,duration_ms:Number(process.hrtime.bigint()-o)/1e6,fields:{runtime_version:process.version,engine_version:l,model:r}}))},"emitStartupMilestone");process.env[en]="1",process.env.CRTR_BROKER_PID=String(process.pid),delete process.env.TMUX_PANE,delete process.env.TMUX,We(t);const p=await rn();tn(p.VERSION),l=p.VERSION;const _=Xe(t),E=Te(_,"broker-launch.json"),w=JSON.parse(Ge(E,"utf8"));if($e()){if(!w.configSnapshot)throw new Error("broker launch is missing its config snapshot");Ze(w.configSnapshot)}const u=nn(w);u.modelExact===!0?process.env.CRTR_MODEL_EXACT="1":delete process.env.CRTR_MODEL_EXACT;const y=await Yn(t);let b,C;const V=n(e=>{b!==void 0?b(e):C??=e},"onSignal");process.on("SIGTERM",()=>V("SIGTERM")),process.on("SIGINT",()=>V("SIGINT"));let{session:U,services:A,resuming:ae,runtime:G}=await st(p,u);d("broker.startup.session_built");const R=(await Xn(t)).node.review_binding?.review_id,k=Oe();J=U;const D=gn;try{process.env.PI_PACKAGE_DIR=sn(),on(A.settingsManager.getTheme(),Y(),!1)}catch(e){T({level:"error",event:"broker.theme.initialization_failed",error:e})}try{for(const e of Qe(Y()))T({level:"warn",event:"broker.startup.warning",fields:{stage:"package_health",source:e.source,resolved_path:e.resolved}})}catch{}const K=He(t),g=new bn(t),X=new Map,O=new Tn(t,e=>g.broadcast(e)),j=new kn(t,e=>g.broadcast(e));En(e=>g.broadcast({type:"node_named",...e}));let N=!1,de=!1,H,le,f,W;const Z=new xn;let F,ce=n(()=>{},"notifyTurnAccepted"),me=n(()=>{},"resetFrameState");const ee=Cn();let ue=n(()=>0,"pendingPromptDispatches"),fe;const L=Dn({runNode:k,session:n(()=>f.session(),"session"),isReplacing:n(()=>f.isReplacing(),"isReplacing"),isAgentRunActive:n(()=>f.isAgentRunActive(),"isAgentRunActive"),pendingDispatches:n(()=>ue(),"pendingDispatches"),turnAdmission:ee,prependDeferred:n(e=>On(t,e),"prependDeferred"),onError:n(e=>T({level:"error",event:"broker.inbox.idle_prompt_failed",error:e}),"onError")}),pe=L.drain;u.firstPrompt!==void 0&&u.firstPrompt!==""&&L.holdForKickoff(),globalThis[An]=L.start,globalThis[Pn]=L.busy,f=new Un({nodeId:t,cfg:u,session:U,services:A,runtime:G,registry:g,toolGroups:O,turnReplies:j,transcriptMessages:n(e=>ie(e,R)?.map(({message:r})=>r)??e.sessionManager.buildSessionContext().messages,"transcriptMessages"),uiContext:n(()=>le,"uiContext"),faultRetry:n(()=>W,"faultRetry"),projection:n(()=>F,"projection"),setActiveSession:n(e=>{if(J=e,fe?.(),k){let r="stop",c,v=!1;fe=e.subscribe(h=>{if(h.type==="agent_start"&&(r="stop",c=void 0,v=!1,Z.started()),h.type==="agent_end"){const S=[...h.messages??[]].reverse().find($=>$.role==="assistant");S&&(r=S.stopReason,c=Sn(S)?void 0:_n(S),v=S.role==="assistant"&&Mn(S,e.model?.contextWindow))}h.type==="agent_settled"&&(Z.settled(e.sessionManager,{stop_reason:r,...c?{error:c}:{}},v),In(t,e).catch(S=>{T({level:"error",event:"broker.run_deferred_mail.settle_failed",error:S})}),queueMicrotask(()=>{f.session()===e&&pe()}))})}},"setActiveSession"),disposeAndExit:n((e,r)=>q(e,r),"disposeAndExit"),onSessionRebind:n(()=>{me()},"onSessionRebind"),onReplacementIdle:n(()=>{k&&pe()},"onReplacementIdle"),reWelcomeAll:n(()=>ne(),"reWelcomeAll")});const ge=n(async(e,r=!1)=>{const c=f.session(),v=B(c.model,c.thinkingLevel);if(v===null)return null;const h=await Hn(t,{spec:v,...e!==void 0?{intent:e}:{},userSelected:r});return r&&(u.model=h.modelOverride,e!==void 0?(process.env.CRTR_MODEL_INTENT=`${e.family}/${e.strength}`,delete process.env.CRTR_MODEL_EXACT,u.modelExact=void 0):(delete process.env.CRTR_MODEL_INTENT,process.env.CRTR_MODEL_EXACT="1",u.modelExact=!0)),h.modelOverride},"persistModelChoice"),ve=n(async(e=!0,r,c=!1)=>{const v=f.session(),h=B(v.model,v.thinkingLevel),S=e?await ge(r,c):h;g.broadcast({type:"model_changed",model:oe(v.model)?void 0:v.model,spec:S})},"broadcastModelChanged");W=new Rn({nodeId:t,cfg:u,retryProbe:D,currentSession:n(()=>f.session(),"currentSession"),currentServices:n(()=>f.services(),"currentServices"),installedGeneration:n(()=>f.installedGeneration(),"installedGeneration"),registryOf:se,formatModelSpec:B,broadcastModelChanged:ve,turnAdmission:ee,reWelcomeAll:n(()=>ne(),"reWelcomeAll"),...k?{turnEnds:Z}:{}});const he=n(()=>{W.rearmIfStranded().catch(e=>{T({level:"warn",event:"broker.runtime.diagnostic",fields:{message:`fault retry re-arm failed: ${e instanceof Error?e.message:String(e)}`}})})},"rearmStrandedRetry");Q().subscribe("resync",he),Q().subscribe("fault-retry",he),F=new Nn({registry:g,faultRetry:W,installedGeneration:n(()=>f.installedGeneration(),"installedGeneration"),currentSession:n(()=>f.session(),"currentSession"),notifyTurnAccepted:n(()=>ce(),"notifyTurnAccepted"),emitStartupMilestone:d,formatModelSpec:B});const Re=n(e=>{const r=f.session(),c=ie(r,R),v=Jn(e.snapshotTail),h=c===void 0||v===void 0?c:c.slice(-v),S=h?.map(({message:I})=>I)??r.sessionManager.buildSessionContext().messages,$=O.summaries(),we=h!==void 0&&v!==void 0?tt(S):void 0,Ae=we===void 0?$:Object.fromEntries(Object.entries($).filter(([I])=>we.has(I)));return{messages:S,...h===void 0?{}:{messageIds:h.map(({id:I})=>I)},messageVisibility:h?.map(({visibility:I})=>I)??S.map(()=>"visible"),turnVisibility:yn(),stats:r.getSessionStats(),state:{model:oe(r.model)?void 0:r.model,thinkingLevel:r.thinkingLevel,isStreaming:r.isStreaming,isCompacting:r.isCompacting,steeringMode:r.steeringMode,followUpMode:r.followUpMode,sessionFile:r.sessionFile,sessionId:r.sessionId,sessionName:r.sessionName,autoCompactionEnabled:r.autoCompactionEnabled,messageCount:r.messages.length,pendingMessageCount:r.pendingMessageCount},display:{...g.displaySnapshot()},toolGroupSummaries:Ae,turnReplies:j.snapshot(),workingActivity:F.currentWorkingActivity(),queued:{steering:[...r.getSteeringMessages()],followUp:[...r.getFollowUpMessages()]}}},"buildSnapshot"),Se=n(e=>{const r=e.role==="controller"?X.values().next().value:void 0;g.sendFrame(e,{type:"welcome",snapshot:Re(e),role:e.role,pending_dialog:r!==void 0?r.request:null,agentDir:Y()},!0)},"sendWelcome"),Me=n(e=>{const r=[...X.values()];for(let c=1;c<r.length;c++)g.sendFrame(e,r[c].request)},"replayExtraPendingDialogsTo"),ne=n(()=>{try{const e=f.session();j.seed(ie(e,R)?.map(({message:r})=>r)??e.sessionManager.buildSessionContext().messages)}catch(e){T({level:"debug",event:"broker.runtime.diagnostic",fields:{message:`turn reply reseed failed: ${String(e)}`}})}g.forEachHelloed(Se)},"reWelcomeAll"),_e=n(async e=>{if(!N){N=!0,Q().stop(),O.persistPendingSummary(),F.dispose(),et();try{H?.close()}catch{}s.close(),g.dispose();try{ye(K)&&be(K)}catch{}g.clearAttachState(),await ze(),process.exit(e)}},"completeExit"),q=n((e,r=0)=>{if(N)return;const c=e==="shutdown-hook"||e==="shutdown-after-settle";if(c&&O.finishOpenToolGroup(!0),c&&(O.hasSummaryTasks()||j.hasTasks())){if(de)return;de=!0,Promise.allSettled([O.waitForSummaryTasks(),j.waitForTasks()]).finally(()=>_e(r));return}_e(r)},"disposeAndExit");le=it({writable:n(()=>g.writableClients(),"writable"),forward:n((e,r)=>g.sendFrame(e,r),"forward"),pending:X,broadcast:n(e=>g.broadcastUi(e),"broadcast"),onKnownStreamWait:n(e=>F.knownStreamWait(e),"onKnownStreamWait")}),f.setRebindSession(),await f.enqueueRebind(U,A),d("broker.startup.extensions_bound");const M=Fn({nodeId:t,registry:g,toolGroups:O,projection:F,rebind:f,boundaryReviewId:R,pendingDialogs:X,sendWelcome:Se,replayExtraPendingDialogsTo:Me,reWelcomeAll:ne,disposeAndExit:q,persistModelChoice:ge,broadcastModelChanged:ve,registryOf:se,formatModelSpec:B,formatExactModelSpec:Qn,turnAdmission:ee,memoryInventory:y,runNode:k,onKickoffAdmitted:L.releaseKickoff});ce=M.notifyTurnAccepted,me=M.resetForSession,ue=M.pendingPromptDispatches;const xe=2500;let te=!1;const Ce=n(async e=>{if(N||te)return;if(te=!0,f.isAgentRunActive()||f.session().isStreaming||M.pendingPromptDispatches()>0){const c=Date.now()+xe;for(;M.pendingPromptDispatches()>0&&!f.session().isStreaming&&Date.now()<c;)await new Promise(v=>setTimeout(v,25));if(f.session().isStreaming){const v=c-Date.now();if(v>0){let h;try{Gn(),await Promise.race([f.session().abort().then(()=>!0,()=>!1),new Promise($=>{h=setTimeout(()=>$(!1),v)})])&&await ke(t,{event:"closed",end_reason:"teardown-aborted"})}finally{h!==void 0&&clearTimeout(h)}}}else(M.pendingPromptDispatches()>0||f.isAgentRunActive())&&await ke(t,{event:"interrupted"})}q(e)},"abortAndExit");if(b=n(e=>{Ce(e)},"signalTeardown"),C!==void 0){b(C);return}try{ye(K)&&be(K)}catch{}H=Ue(e=>{const r=g.create(e);e.on("data",v=>{let h;try{h=r.decoder.push(v)}catch(S){S instanceof zn?(g.sendFrame(r,{type:"error",code:"frame_overflow",message:S.message}),g.dropClient(r,`frame overflow: ${S.message}`)):g.dropClient(r,`decoder error: ${String(S)}`);return}for(const S of h)try{Ln(M,r,S)}catch{}});const c=n(()=>{g.removeClient(r)},"drop");e.on("close",c),e.on("error",()=>{})}),H.on("error",e=>{N||(T({level:"error",event:"broker.socket.failed",error:e}),q("socket-error",1))}),H.listen(K,async()=>{Promise.all([z(t,{op:"clear",link:"viewer\u2194broker"}),z(t,{op:"clear",link:"relay\u2194broker"})]).catch(r=>T({level:"error",event:"broker.daemon.operation.failed",node_id:t,error:r,fields:{operation:"broker_socket_bound_fault_clear"}}));let e;try{e=await W.recoverOnStartup()}catch(r){T({level:"error",event:"broker.daemon.operation.failed",node_id:t,error:r,fields:{operation:"recovery_startup"}}),q("recovery-startup-failed",1);return}if(!(N||te))if(e!=="retry-armed"&&u.firstPrompt!==void 0&&u.firstPrompt!==""){const r=Wn(process.env[qn]);r!==null&&Vn(u.firstPrompt,r);const c=process.env.CRTR_KICKOFF_MESSAGE_IDS;M.kickoff(u.firstPrompt,c?JSON.parse(c):[])}else L.releaseKickoff()}),g.persistAttachState()}n(lr,"runBroker");function tt(t){const o=new Set;for(const i of t)if(!(i.role!=="assistant"||!Array.isArray(i.content)))for(const m of i.content)m!==null&&typeof m=="object"&&m.type==="toolCall"&&typeof m.id=="string"&&o.add(m.id);return o}n(tt,"toolCallIds");function ie(t,o){const i=t.sessionManager,m=hn(i);return m!==void 0&&$n(m.map(({message:s})=>s)),m===void 0?void 0:wn(m,{boundaryReviewId:o,sessionEntries:i.getEntries?.()})}n(ie,"snapshotMessages");function Ee(){const t=Ne();if(t===void 0||t==="")throw new Error("broker launch is missing CRTR_NODE_ID");return t}n(Ee,"currentBrokerNodeId");async function rt(t,o){const i=cn(o.model,Ie(),o.modelExact===!0);if(i===void 0)return;const m=se(t),s={cwd:De()??o.cwd,profileId:Fe()},a=fn(i,m,s);if(a!==void 0)throw await z(Ee(),{op:"record",input:{link:"crtr\u2192pi",op:"model resolution at launch",kind:"model-not-found",retry:{disposition:"fatal"},message:`model '${a.request.spec}' is not registered in the serving model registry`}}),new Error(`[broker] FATAL: model '${a.request.spec}' is not registered in the serving model registry`);const l=mn(i,s.cwd,s.profileId),d=un(i.spec);for(const p of l){const _=m.find(p.providerId,p.modelId);if(_===void 0)continue;const w=d?.providerId===p.providerId&&d.modelId===p.modelId?d.thinkingLevel??(p.thinkingLevel===void 0?void 0:"off"):p.thinkingLevel;return{model:_,thinkingLevel:w}}if(o.modelExact===!0){const p=l.map(_=>`'${_.providerId}/${_.modelId}' unregistered`).join("; ");throw await z(Ee(),{op:"record",input:{link:"crtr\u2192pi",op:"model resolution at launch",kind:"model-not-found",retry:{disposition:"fatal"},message:`exact-consult model '${o.model}' is unavailable \u2014 refusing to substitute (${p})`}}),new Error(`[broker] FATAL: exact-consult model '${o.model}' is unavailable`)}T({level:"warn",event:"broker.model.none_viable",fields:{rejected_candidates:l.map(p=>({provider:p.providerId,model:p.modelId,reason:"unregistered"})).slice(0,16)}})}n(rt,"resolveLaunchModel");function ot(t){let o=!0;return{runtime:new Proxy(t,{get(m,s,a){return o&&s==="getAvailableSnapshot"?()=>[]:o&&s==="getAvailable"?async()=>[]:o&&s==="getModel"?()=>{}:Reflect.get(m,s,a)}}),reveal:n(()=>{o=!1},"reveal")}}n(ot,"modelLessRegistry");async function st(t,o){const i=Y(),m=n(async w=>{const u=await Ve.create({credentials:new dn,modelsPath:Te(w.agentDir,"models.json")});ln(u);const y=await t.createAgentSessionServices({cwd:w.cwd,agentDir:w.agentDir,modelRuntime:u,resourceLoaderOptions:{additionalExtensionPaths:o.extensionPaths,appendSystemPrompt:o.appendSystemPromptPath!==void 0?[o.appendSystemPromptPath]:void 0,noContextFiles:!0,extensionsOverride:n(P=>({...P,extensions:P.extensions.filter(R=>!/[/\\]provider-rotation\.(?:ts|js)$/.test(R.path))}),"extensionsOverride")}}),b=y.resourceLoader.getExtensions();if(o.extensionPaths.some(P=>re.includes(P))){const P=new Set(b.extensions.map(k=>k.path)),R=re.filter(k=>!P.has(k));if(R.length>0){const k=b.errors.filter(D=>re.includes(D.path)).map(D=>`${D.path}: ${D.error}`).join("; ");throw new Error(`[broker] FATAL: mandatory canvas extensions failed to load: ${R.join(", ")}${k===""?"":` (${k})`}`)}}y.settingsManager.getRetryEnabled=()=>!1;const C=await rt(y,o),V=C?.model,U=C?.thinkingLevel,A=C===void 0?ot(y.modelRuntime):void 0,ae=A!==void 0?{...y,modelRuntime:A.runtime}:y,G=await t.createAgentSessionFromServices({services:ae,sessionManager:w.sessionManager,model:V,thinkingLevel:U,...nt(o.tools,Le()),sessionStartEvent:w.sessionStartEvent});return Bn(G.session),an(G.session),A?.reveal(),{...G,services:y}},"buildForManager"),s=o.resumeSessionPath,a=o.forkFrom!==void 0&&o.forkFrom!=="";if(!a&&(s===void 0||s==="")&&o.resumeSessionId!==void 0&&o.resumeSessionId!=="")throw new Error(`[broker] resume requires a session .jsonl PATH; got only a bare id '${o.resumeSessionId}' (pi_session_file was never captured for this node)`);const l=!a&&s!==void 0&&s!=="",d=l&&o.newCycle===!0,p=l&&!d;let _;if(a)_=t.SessionManager.forkFrom(o.forkFrom,o.cwd);else if(p||d){if(_=t.SessionManager.open(s),d){const w=_.getLeafId();_.resetLeaf(),_.appendCustomEntry(vn,{cycle:Number(process.env.CRTR_CYCLES),fromLeaf:w})}}else _=t.SessionManager.create(o.cwd);if(t.createAgentSessionRuntime!==void 0){const w=n(async y=>{const b=await m({cwd:y.cwd,agentDir:y.agentDir,sessionManager:y.sessionManager,sessionStartEvent:y.sessionStartEvent});return{...b,diagnostics:b.services.diagnostics??[]}},"factory"),u=await t.createAgentSessionRuntime(w,{cwd:o.cwd,agentDir:i,sessionManager:_});return{session:u.session,services:u.services,resuming:p,runtime:u}}const E=await m({cwd:o.cwd,agentDir:i,sessionManager:_});return{session:E.session,services:E.services,resuming:p,runtime:void 0}}n(st,"buildBrokerSession");function it(t){const o=n((s,a,l,d)=>{if(d?.signal?.aborted)return Promise.resolve(s);const p=t.writable();return p.length===0?Promise.resolve(s):new Promise(_=>{let E;const w=n(()=>{E!==void 0&&clearTimeout(E),d?.signal?.removeEventListener("abort",u),t.pending.delete(a.id)},"cleanup"),u=n(()=>{w(),t.broadcast({type:"extension_ui_dismiss",id:a.id}),_(s)},"onAbort");d?.signal?.addEventListener("abort",u,{once:!0});const y=d?.timeout!==void 0?d.timeout:Zn;E=setTimeout(()=>{w(),t.broadcast({type:"extension_ui_dismiss",id:a.id}),_(s)},y),typeof E.unref=="function"&&E.unref(),t.pending.set(a.id,{request:a,resolve:n(b=>{w(),t.broadcast({type:"extension_ui_dismiss",id:a.id}),_(l(b))},"resolve")});for(const b of p)t.forward(b,a)})},"dialogPromise"),i=n(()=>{},"noop");return{[pn]:t.onKnownStreamWait??(s=>()=>{}),get theme(){const s=globalThis[Symbol.for("@earendil-works/pi-coding-agent:theme")];if(s==null)throw new Error("Theme not initialized. Call initTheme() first.");return s},select:n((s,a,l)=>o(void 0,{type:"extension_ui_request",id:x(),method:"select",title:s,options:a,timeout:l?.timeout},d=>"cancelled"in d&&d.cancelled?void 0:"value"in d?d.value:void 0,l),"select"),confirm:n((s,a,l)=>o(!1,{type:"extension_ui_request",id:x(),method:"confirm",title:s,message:a,timeout:l?.timeout},d=>"cancelled"in d&&d.cancelled?!1:"confirmed"in d?d.confirmed:!1,l),"confirm"),input:n((s,a,l)=>o(void 0,{type:"extension_ui_request",id:x(),method:"input",title:s,placeholder:a,timeout:l?.timeout},d=>"cancelled"in d&&d.cancelled?void 0:"value"in d?d.value:void 0,l),"input"),editor:n((s,a)=>o(void 0,{type:"extension_ui_request",id:x(),method:"editor",title:s,prefill:a},l=>"cancelled"in l&&l.cancelled?void 0:"value"in l?l.value:void 0),"editor"),setStatus:n((s,a)=>t.broadcast({type:"extension_ui_request",id:x(),method:"setStatus",statusKey:s,statusText:a}),"setStatus"),setWidget:n((s,a,l)=>{typeof a!="function"&&t.broadcast({type:"extension_ui_request",id:x(),method:"setWidget",widgetKey:s,widgetLines:a,widgetPlacement:l?.placement})},"setWidget"),setTitle:n(s=>t.broadcast({type:"extension_ui_request",id:x(),method:"setTitle",title:s}),"setTitle"),notify:n((s,a)=>{const l=a==="warn"?"warning":a;t.broadcast({type:"extension_ui_request",id:x(),method:"notify",message:s,notifyType:l})},"notify"),onTerminalInput:n(()=>i,"onTerminalInput"),setWorkingMessage:i,setWorkingVisible:i,setWorkingIndicator:i,setHiddenThinkingLabel:i,setFooter:i,setHeader:i,custom:n(()=>Promise.resolve(void 0),"custom"),pasteToEditor:i,setEditorText:i,getEditorText:n(()=>"","getEditorText"),addAutocompleteProvider:i,setEditorComponent:i,getEditorComponent:n(()=>{},"getEditorComponent"),getAllThemes:n(()=>[],"getAllThemes"),getTheme:n(()=>{},"getTheme"),setTheme:i,getToolsExpanded:n(()=>!1,"getToolsExpanded"),setToolsExpanded:i}}n(it,"makeBrokerUiContext");export{fr as REVIEW_BOUNDARY_NAVIGATION_CODE,ur as ReviewBoundaryNavigationError,nt as brokerToolOptions,st as buildBrokerSession,et as disposeActiveSession,it as makeBrokerUiContext,lr as runBroker,ie as snapshotMessages};
|
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.426",
|
|
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.426",
|
|
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.426",
|
|
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.426",
|
|
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.426",
|
|
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.426",
|
|
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.426",
|
|
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.426",
|
|
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.426",
|
|
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.426",
|
|
12120
|
+
"@north-light/identity": "0.3.426"
|
|
12121
12121
|
}
|
|
12122
12122
|
},
|
|
12123
12123
|
"packages/crouter-sdk": {
|
|
12124
12124
|
"name": "@north-light/sdk",
|
|
12125
|
-
"version": "0.3.
|
|
12125
|
+
"version": "0.3.426",
|
|
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.426",
|
|
12129
|
+
"@north-light/identity": "0.3.426",
|
|
12130
12130
|
"jose": "^6.2.1"
|
|
12131
12131
|
},
|
|
12132
12132
|
"bin": {
|