@north-light/cli 0.3.484 → 0.3.486

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.
@@ -1,3 +1,3 @@
1
- import{prepared as p}from"../../sqlite-statements.js";import{covers as s}from"@north-light/identity";import{identityOf as f}from"./index.js";const d={identity:f,sync:"runtime",canRead(i,r,o,t,u){return r.class==="person"||r.class==="object"&&r.owner.rowid===o.rowid?!0:r.scopes===null?!1:r.app.rowid===o.rowid||t.length===0?!0:t[0].type!=="document"?!1:r.scopes.some(c=>s(c,"crtr:memory:manage:user"))?!0:u.privacy==="public"},canChange(i,r,o,t,u){if(r.class==="person"||r.class==="object"&&r.owner.rowid===o.rowid||r.app.rowid===o.rowid||r.scopes?.some(e=>s(e,"crtr:memory:manage:user")))return!0;if(t.length===0)return r.scopes?.some(e=>s(e,"crtr:memory:rw:user"))??!1;if(t[0].type!=="document")return!1;const c=`crtr:memory:rw:user:${u.name}`;if(r.scopes===null||!r.scopes.some(e=>s(e,c)))return!1;const n=p(i,`SELECT author.type, author.owner AS app FROM revisions r JOIN objects author ON author.rowid = r.author
2
- WHERE r.document = ? ORDER BY r.number, r.created_at LIMIT 1`).get(t[0].rowid);return n?n.type==="person"||n.app===r.app.rowid?!0:p(i,`SELECT g.resolved_scopes_json FROM grants g JOIN identities i ON i.external_id = g.grantee
3
- WHERE i.object = ? AND i.scheme = 'app'`).all(n.app).some(({resolved_scopes_json:e})=>JSON.parse(e).some(({scope:a,mode:m})=>m==="always"&&s(a,"crtr:memory:manage:user"))):!1}};export{d as personSpace};
1
+ import{prepared as p}from"../../sqlite-statements.js";import{covers as o}from"@north-light/identity";import{identityOf as f}from"./index.js";const d={identity:f,sync:"runtime",canRead(c,r,s,t,i){return r.class==="person"||r.class==="object"&&r.owner.rowid===s.rowid?!0:r.scopes===null?!1:r.app.rowid===s.rowid||t.length===0?!0:t[0].type!=="document"?!1:r.scopes.some(n=>o(n,"crtr:memory:manage:user"))||i.privacy==="public"?!0:i.privacy==="private"&&r.scopes.some(n=>o(n,"crtr:memory:read_private:user"))},canChange(c,r,s,t,i){if(r.class==="person"||r.class==="object"&&r.owner.rowid===s.rowid||r.app.rowid===s.rowid||r.scopes?.some(e=>o(e,"crtr:memory:manage:user")))return!0;if(t.length===0)return r.scopes?.some(e=>o(e,"crtr:memory:rw:user"))??!1;if(t[0].type!=="document")return!1;const n=`crtr:memory:rw:user:${i.name}`;if(r.scopes===null||!r.scopes.some(e=>o(e,n)))return!1;const u=p(c,`SELECT author.type, author.owner AS app FROM revisions r JOIN objects author ON author.rowid = r.author
2
+ WHERE r.document = ? ORDER BY r.number, r.created_at LIMIT 1`).get(t[0].rowid);return u?u.type==="person"||u.app===r.app.rowid?!0:p(c,`SELECT g.resolved_scopes_json FROM grants g JOIN identities i ON i.external_id = g.grantee
3
+ WHERE i.object = ? AND i.scheme = 'app'`).all(u.app).some(({resolved_scopes_json:e})=>JSON.parse(e).some(({scope:a,mode:m})=>m==="always"&&o(a,"crtr:memory:manage:user"))):!1}};export{d as personSpace};
@@ -19,7 +19,7 @@ A non-2xx answer is thrown as an `APIError`, never returned as bytes. A bad `ran
19
19
 
20
20
  ## Read the person's public documents
21
21
 
22
- `runtime.memory` is a read-only view of the `knowledge` and `preference` documents the person owns (`memory` and the `/v1/memory` routes are the API's names for this). An app reads the person's `public` documents with `memory.list` and `memory.get`, and needs no scope to do it. It never sees a `friends` or `private` document and cannot tell one from a document that does not exist: both answer `404 not_found`. The list is what the app's agents can read about the person, so an app can show the person exactly which of their documents it draws on.
22
+ `runtime.memory` is a read-only view of the `knowledge` and `preference` documents the person owns (`memory` and the `/v1/memory` routes are the API's names for this). An app reads the person's `public` documents with `memory.list` and `memory.get`, and needs no scope to do it. Without more, it never sees a `friends` or `private` document and cannot tell one from a document that does not exist: both answer `404 not_found`. The optional scope [`crtr:memory:read_private:user`](#private-memory) adds `private`. The list is what the app's agents can read about the person, so an app can show the person exactly which of their documents it draws on.
23
23
 
24
24
  ```ts
25
25
  const page = await runtime.memory.list({store: 'user'});
@@ -29,7 +29,7 @@ const doc = await runtime.memory.get({store: 'user', name: page.items[0].name});
29
29
  console.log(doc.body);
30
30
  ```
31
31
 
32
- `store` is `'user'` in this release; any other value answers `400 invalid_request` with `param: "store"`. An app whose grant holds `crtr:memory:manage:user` (a first-party app) sees every privacy level. Reading records nothing: no watch, no `read` event, no audit row. Both methods are `GET` requests and follow the SDK's retry rule for safe requests.
32
+ `store` is `'user'` in this release; any other value answers `400 invalid_request` with `param: "store"`. An app whose grant holds `crtr:memory:manage:user` (a first-party app) sees every privacy level; one that holds `crtr:memory:read_private:user` sees `public` and `private`. Reading records nothing: no watch, no `read` event, no audit row. Both methods are `GET` requests and follow the SDK's retry rule for safe requests.
33
33
 
34
34
  ### List
35
35
 
@@ -56,4 +56,13 @@ for await (const item of runtime.memory.listAll({store: 'user', path: 'work'}))
56
56
 
57
57
  - `body` is the current markdown as stored; `[[links]]` are not resolved. A body over 1 MiB is cut to its longest whole-character prefix of at most 1 MiB, with `truncated: true`; `size` is always the full length in UTF-8 bytes.
58
58
  - `grantee` is `app:<id>` of the app whose agent wrote the first revision, or `null` when the person wrote it.
59
- - An absent, deleted, `friends` or `private` name rejects with `NotFoundError` (`code: 'not_found'`); a malformed field rejects with `BadRequestError`.
59
+ - An absent or deleted name, a `friends` name, and a `private` name without `crtr:memory:read_private:user` reject with `NotFoundError` (`code: 'not_found'`); a malformed field rejects with `BadRequestError`.
60
+
61
+ ## Private memory
62
+
63
+ `crtr:memory:read_private:user` is an [optional permission](/docs/guides/grant-changes#optional-permissions) that lets your app read the person's `private` documents. Declare it, then let the person switch it on from your app (sign-in request or `permissionUrl`).
64
+
65
+ - **Read-only, and never `friends`.** With the scope `memory.list` and `memory.get` return `public` and `private` documents; a `friends` document still reads as absent, and the scope writes nothing. Each private document arrives with `privacy: 'private'`.
66
+ - **Both server reads and runs.** Your server's `memory` calls see private documents while the grant holds the scope. A run sees them only if it was started with the scope: `runs.start` `scopes` must cover `crtr:memory:read_private:user` (name it, or name `crtr:memory:manage:user` if the grant holds that) (a run with no `scopes` list gets what the grant holds), and the start is refused `403 scope_missing` while the grant lacks it. Add the scope to the runs that need it only while `resolved_scopes` holds it.
67
+ - **The person can turn it off at any time.** Reads then stop returning private documents at once, including from a run that is still live. That run keeps what it already read in its context, so restart the runs that used it when the permission turns off (`watchGrants` tells you).
68
+ - **Never show a private detail to anyone but the owner**, unless the owner asked your app to. A visitor, a guest or another user must not see a fact, quote or summary taken from a private document.
@@ -37,6 +37,22 @@ await watcher.stop();
37
37
  - **If `onChanged` throws**, the same version is delivered again after 5 s, doubling to 5 min. A transient runtime error backs off the same way. A missing or refused connection drops that user and reports to `onError`; the next sweep decides again from `users()`.
38
38
  - For one person without the watcher, `runtime.grant.get({wait: 25, since: grant.version})` holds a single read the same way. `wait` outside 1–25 throws `RangeError` before sending.
39
39
 
40
+ ## Optional permissions
41
+
42
+ An optional permission is a scope your app can use but does not need to work. The person turns it on or off for your app, on the consent page and later on the app's Northlight account page, and your app keeps working when it is off.
43
+
44
+ - **Declare it** in the app's registration: `crtr cloud apps set-optional-scopes` (or the console's **What it asks for** tab). The first one is `crtr:memory:read_private:user`: see [Private memory](/docs/guides/files-and-documents).
45
+ - **Ask at sign-in only when the person chose to.** Add the scope to `authorizeUrl`'s `scopes` only after the person said they want it in your app. A person who declines an optional row on the consent page sees the page again on every sign-in request that names it, so naming it for everyone nags everyone. Leave the sign-in request unchanged for everyone else.
46
+ - **Or link to the switch** with `permissionUrl`. It opens the account page with that scope's switch first, and `returnTo` (an absolute URL on one of your registered redirect origins; anything else is ignored) puts a "Back to your app" button on it once the person turns it on:
47
+
48
+ ```ts
49
+ const href = await oauth.permissionUrl('crtr:memory:read_private:user', {returnTo: 'https://app.example.com/settings'});
50
+ ```
51
+
52
+ It needs no user or connection, like `accountAppUrl`, and throws `CrouterError` when the scope is not a valid scope string.
53
+ - **Read the state from the grant.** The permission is on when the scope is in `grant.resolved_scopes` and off when it is not. Never keep your own copy of the person's choice.
54
+ - **Follow changes with `watchGrants`** (above). A switch changes the grant's `version`, so `onChanged` delivers it. Decide from the grant as it is now: start the work that needs the scope when it appears, and stop or restart it when it disappears.
55
+
40
56
  ## Wait on a provider your app requires
41
57
 
42
58
  An app whose registration `requires` a provider (the console's **What your app requires**) needs that provider set up for the person before the operations that depend on it work. The runtime does not check this for you. `grant.get()` returns `dependencies`: one entry per setup id the app requires or lists as optional (plus any the directory implies, such as `device` for `context.deep`), keyed by setup id, or `{}` when it has none. Read it on each request that needs the provider; do not store it.
@@ -46,7 +46,7 @@ A visitor chatting with B's clone starts an app run on **B's runtime and B's AI
46
46
 
47
47
  A run your app starts is **your app's agent**, not a continuation of another app's agent. It starts in `/apps/<app>/files`, uses the person's AI under `crtr:llm`, and reaches your app's own documents (owned by `app:<your app>`) without a `crtr:memory` scope. Other people's or apps' data is not shared merely because the app has many users. The grant's resolved permissions govern access to the person's files (`crtr:files:<access>:user:<dir>`), the person's documents (`crtr:memory:<access>:user`), another app's space (only where that app allows it), and another app's conversations (`crtr:conversations:read:app:<B>`).
48
48
 
49
- The exception is the person's **public** documents (owned by the person, read through `memory`; see [Files and documents](/docs/guides/files-and-documents)): an app with an active grant can read these even without a user-memory scope. A third-party app cannot see `friends` or `private` documents, even with `crtr:memory:read:user`. They look absent from lists, searches, reads and history. Only a holder of `crtr:memory:manage:user` (a first-party app), or the person, reads all three levels. A document without a privacy field reads as `private`. Public read access is not write access.
49
+ The exception is the person's **public** documents (owned by the person, read through `memory`; see [Files and documents](/docs/guides/files-and-documents)): an app with an active grant can read these even without a user-memory scope. A third-party app cannot see `friends` documents, and sees `private` ones only with the optional `crtr:memory:read_private:user` the person switched on; `crtr:memory:read:user` reveals neither. They look absent from lists, searches, reads and history. Only a holder of `crtr:memory:manage:user` (a first-party app), or the person, reads all three levels. A document without a privacy field reads as `private`. Public read access is not write access.
50
50
 
51
51
  Every run is sandboxed: its shell runs as the app's OS user and sees only the app's space and the folders its permissions name. Normal runs have network access. Start a run with `isolated: true` when the agent should have **no network** and **no `/apps/<app>/files` mount**: its cwd is its own node folder, and all children inherit the restriction. The shell, stored conversation and model calls still work, and the run's `scopes` can narrow its commands. An isolated run cannot turn isolation off in a child or schedule, or use an outside cwd or worktree.
52
52
 
@@ -117,7 +117,7 @@ In your callback handler, check `authorizationRefusalOf(url)` before reading `co
117
117
 
118
118
  ## Memory and providers
119
119
 
120
- `user.memory.put({name, body, privacy = 'public', kind = 'knowledge', ...})` and `remove(name)`. Without `crtr:memory:manage:user` in the grant, only `public` documents are listed and others read as 404, as on the runtime. List items carry the full `MemoryListItem` fields.
120
+ `user.memory.put({name, body, privacy = 'public', kind = 'knowledge', ...})` and `remove(name)`. Without `crtr:memory:manage:user` in the grant, only `public` documents are listed and others read as 404, as on the runtime; `crtr:memory:read_private:user` adds `private` (never `friends`). FakeCloud applies the user's whole grant (`user.grant.set`), not the `scopes` a run was started with; test narrowed runs on the real runtime. List items carry the full `MemoryListItem` fields.
121
121
 
122
122
  ```ts
123
123
  cloud.providers.define('sms', { tools: { send: { group: 'send', handler: ({ userId, arguments: args }) => ({ sent: true }) } } });
package/docs/ci-runner.md CHANGED
@@ -62,7 +62,8 @@ Isolation, per instance `~/ci-runners/crouter-N/`:
62
62
  - its own runner directory, and its own **short** work directory `/Users/Shared/ci/N` (checkout and `_temp`; override with `CI_RUNNER_WORK`). Short is a requirement: the suite binds unix sockets under `$RUNNER_TEMP/t` (the workflow sets `TMPDIR` there), and macOS refuses a socket path over 104 bytes with `listen EINVAL`. The longest test socket adds ~76 bytes, so the work dir plus `/_temp/t` must stay under ~27 (hosted runners use `/Users/runner/work/_temp/t`, 26). Do not put the work dir under the account's home;
63
63
  - its own `HOME` (`~/ci-runners/crouter-N/home`), never the account's. The suite refuses to start if `$HOME/.crouter` exists, so the account's own crouter install (`~/.crouter`, the selected runtime, `~/.local/bin/crtr`, any running `crtrd`) is neither read nor written;
64
64
  - a minimal `PATH` (Homebrew + system) that does not include the account's crtr or npm globals;
65
- - `hook-job-started.sh` / `hook-job-completed.sh` (`ACTIONS_RUNNER_HOOK_JOB_*`), which kill every process whose environment or command line points into that instance's directory (brokers, `crtrd`, test tmux servers, orphaned node processes) and wipe `home/.crouter*` and the work dir's `_temp`. They match only that instance's path, so one instance's cleanup cannot touch another's job or the account's processes.
65
+ - `hook-job-started.sh` / `hook-job-completed.sh` (`ACTIONS_RUNNER_HOOK_JOB_*`), which kill every process whose environment or command line points into that instance's directory or work dir (brokers, `crtrd`, test tmux servers, orphaned node processes) and wipe `home/.crouter*` and `_temp`. They match only that instance's paths, so one instance's cleanup cannot touch another's job or the account's processes.
66
+ - `ci-watchdog.sh`, one detached copy per instance, started by either hook (pidfile `watchdog.pid`, log `watchdog.log`; kills are logged there). The hooks are not enough on their own: cancelling a run makes the runner SIGINT, SIGTERM, then kill the step's process *tree*, and brokers, `crtrd` and tmux servers that daemonized are reparented to launchd and are outside that tree. The completed hook does run after a cancel (it did in all 77 cancelled jobs in the runners' logs) and kills what it can match by path, but it never runs when the Worker is killed hard, and it cannot see a process whose argv and environment name neither the instance dir nor the work dir (the per-job directory `/private/tmp/c.XXXXXX` is neither). The watchdog polls every 5 s and, once this instance has had no `Runner.Worker` for two polls in a row, kills every process that has `RUNNER_TEMP=<work>/_temp` in its environment, names `<work>/`, or names a per-job directory whose `.runner-temp` marker (written by `test.yml`'s "Move HOME" step) holds this instance's `<work>/_temp`, then removes those directories. It re-checks for a `Runner.Worker` before every kill pass and before removing the directories, so a job that starts mid-sweep is left alone after at most one pass; the marker and `RUNNER_TEMP` keys are per instance, and matching is by substring of a process's command line and environment, so another instance's job or the account's own processes are touched only if their command line or environment literally names this instance's work dir, `RUNNER_TEMP`, or marked per-job directory. The completed hook also runs one unconditional sweep (`ci-watchdog.sh --once --now`).
66
67
 
67
68
  The tests bind unix sockets under per-test temp dirs and tmux servers under the per-test `HOME`, with no fixed TCP ports, so instances do not collide.
68
69
 
@@ -77,6 +78,8 @@ tail -f ~/ci-runners/crouter-1/runner.log # l
77
78
  gh api repos/Northlight-Systems/crouter/actions/runners --jq '.runners[]|[.name,.status,.busy]|@tsv'
78
79
  ```
79
80
 
81
+ Updating the hooks or watchdog on the mini (no sudo needed): copy `scripts/ci-runner/{hook-job-started,hook-job-completed,ci-watchdog}.sh` to `~/ci-runner-scripts/` and to each `~/ci-runners/crouter-N/hooks/`; the next job's hook starts the watchdog (or `pkill -f ci-watchdog` to restart it). Never touch the mini's `~/Code/cli/crouter` checkout.
82
+
80
83
  Installing and upgrading: get a registration token (`gh api -X POST repos/Northlight-Systems/crouter/actions/runners/registration-token --jq .token`), run `RUNNER_TOKEN=… scripts/ci-runner/install.sh 3` on the mini, then run the `sudo launchctl bootstrap` lines it prints. Prerequisites on the mini: `tmux`, `uv`, `git` (Homebrew); Node comes from `actions/setup-node`.
81
84
 
82
85
  ## No double run on `main`
@@ -15,6 +15,7 @@ Scope strings are prefixed with `crtr:`. These are the ones the daemon and CLI c
15
15
  | `crtr:act` | Creating, forking, reviving, closing and messaging nodes; `bash` and `files`; creating a human request or review (the `crtr human` commands are hidden from a run without it) |
16
16
  | `crtr:schedule` | Creating, pausing, resuming, running and deleting crons (the `crtr cron` commands are hidden without it) |
17
17
  | `crtr:memory:read:user[:path]`, `crtr:memory:rw:user[:path]`, `crtr:memory:manage:user` | The person's documents: reading, writing (`crtr doc`), and `manage` for every privacy level and every document |
18
+ | `crtr:memory:read_private:user` | Read-only access to the person's `public` and `private` documents, never `friends`; no path suffix; an optional permission the person switches on; `manage:user` covers it, `read:user` and `rw:user` do not |
18
19
  | `crtr:memory:read:app:<id>`, `crtr:memory:rw:app:<id>` | An app's documents; `…:profile:<profile>` narrows to one of its profiles. A run with no `scopes` list, and an app's own server, reads and writes its own app's space freely; a run started with a `scopes` list needs these for its own app too |
19
20
  | `crtr:files:read\|rw:user:<dir>`, `crtr:files:read\|rw:app:<id>:<dir>`, `crtr:files:share` | Host files the run may mount, and sharing files beyond the app's own space |
20
21
  | `crtr:conversations:read:app:<id>`, `crtr:objects:read:app:<id>` | Reading another app's conversations or node objects |
@@ -22,7 +23,7 @@ Scope strings are prefixed with `crtr:`. These are the ones the daemon and CLI c
22
23
 
23
24
  The `memory` in `crtr:memory:*` is the scope family's name for access to **documents** (see [Documents](./documents.md)); there is no separate memory store. The older short names (`ask`, `act`, `schedule`, `memory:read`, `memory:write`, `llm`) are not accepted by `crtr sys connect --scopes`. A node's own documents, and an app's documents for its own nodes, need no `crtr:memory` scope.
24
25
 
25
- This is an allow-list, not a claim that code will behave. The daemon checks the scope at the route that performs the action and rejects an unavailable one. The SDK’s [`nodes.create`](../sdk/nodes.md) `scopes` field narrows a run: a run's list must stay inside what its creator and grant hold, and `crtr:act` requires `crtr:llm`. `client.memory` is not scope-gated: it lists and reads the person's `knowledge` and `preference` documents, and the answer is filtered by privacy — an app sees `public` documents, and a grant holding `crtr:memory:manage:user` also sees `friends` and `private` ones. Who may read any document is the owner-and-privacy model, not a per-call scope. The exception is a run started with a `scopes` list: it reads its own app's documents (including automatic delivery of them at boot) only with `crtr:memory:read:app:<id>` and writes or edits them only with `crtr:memory:rw:app:<id>`; a refused write answers `scope_missing` naming the scope. Such a run still publishes its own reports with `crtr push` and its promoted node still gets a roadmap, but cannot edit any other document through that path, and a child never regains a scope its parent's list dropped.
26
+ This is an allow-list, not a claim that code will behave. The daemon checks the scope at the route that performs the action and rejects an unavailable one. The SDK’s [`nodes.create`](../sdk/nodes.md) `scopes` field narrows a run: a run's list must stay inside what its creator and grant hold, and `crtr:act` requires `crtr:llm`. `client.memory` is not scope-gated: it lists and reads the person's `knowledge` and `preference` documents, and the answer is filtered by privacy — an app sees `public` documents, a grant holding `crtr:memory:read_private:user` also sees `private` ones, and a grant holding `crtr:memory:manage:user` sees `friends` and `private` ones. Who may read any document is the owner-and-privacy model, not a per-call scope. The exception is a run started with a `scopes` list: it reads its own app's documents (including automatic delivery of them at boot) only with `crtr:memory:read:app:<id>` and writes or edits them only with `crtr:memory:rw:app:<id>`; a refused write answers `scope_missing` naming the scope. Such a run still publishes its own reports with `crtr push` and its promoted node still gets a roadmap, but cannot edit any other document through that path, and a child never regains a scope its parent's list dropped.
26
27
 
27
28
  ```mermaid
28
29
  flowchart LR
@@ -78,7 +78,7 @@ crtr sys connect --app my-app --scopes crtr:llm,crtr:act,crtr:memory:read:user
78
78
 
79
79
  `--scopes` requires `--app` (the app id, without the `app:` prefix) and accepts only `crtr:llm`, `crtr:act`, `crtr:schedule`, `crtr:memory:read:user`, `crtr:memory:read:app`, `crtr:memory:rw:app`, `crtr:files:read:user:.` and `crtr:files:rw:user:.`. This mints a new token for that app whose scope list is a ceiling, appends it to the same secrets store, prints it in place of the owner token, and always hands the daemon over (it reads tokens once, at boot). The ceiling is checked on every request that carries the token:
80
80
 
81
- - A scope-gated operation the ceiling lacks answers `403 scope_denied`. Creating, reviving, forking, messaging, or yielding a node needs `crtr:act`, as do `bash`, `files`, and creating a human request or review (there is no separate scope for human requests); arming or running a cron needs `crtr:schedule`; `/v1/llm` needs `crtr:llm`. `client.memory` has no scope of its own: it reads the person's documents filtered by privacy, and a grant holding `crtr:memory:manage:user` sees every level.
81
+ - A scope-gated operation the ceiling lacks answers `403 scope_denied`. Creating, reviving, forking, messaging, or yielding a node needs `crtr:act`, as do `bash`, `files`, and creating a human request or review (there is no separate scope for human requests); arming or running a cron needs `crtr:schedule`; `/v1/llm` needs `crtr:llm`. `client.memory` has no scope of its own: it reads the person's documents filtered by privacy, and a grant holding `crtr:memory:read_private:user` also sees `private` documents, and one holding `crtr:memory:manage:user` sees every level.
82
82
  - `nodes.create` with `scopes` outside the ceiling answers `403 scope_denied`; `details.scopes` lists the offending scopes. Omit `scopes` and the run gets the ceiling.
83
83
  - Owner-only operations — daemon restart, the attach viewer, broker internals, canvas prune, profile pause/resume/delete, model credential install — answer `403 owner_only` to any scoped token.
84
84
 
@@ -7,13 +7,22 @@ description: Behavior changes in @north-light/sdk that an app must act on, newes
7
7
 
8
8
  Changes to `@north-light/sdk` that can change what an existing app sees. Newest first.
9
9
 
10
+ ## 0.3.486
11
+
12
+ ### `crtr:memory:read_private:user` and `oauth.permissionUrl` (additive, no app change)
13
+
14
+ - The scope `crtr:memory:read_private:user` lets an app read the person's `private` memory as well as `public`, read-only. `friends` documents are never included, and the scope grants no write. `crtr:memory:manage:user` covers it; `read:user` and `rw:user` do not. It takes no path suffix. With it, `memory.list` and `memory.get` (and the memory a run reads) return `private` documents marked `privacy: 'private'`; without it they read as absent. A run started with a `scopes` list sees `private` only if that list covers it (names it, or names `crtr:memory:manage:user`). The grant can lose the scope while a run is live: the run's next read stops returning private documents, but what it already read stays in its context, so restart runs when access turns off.
15
+ - New `await oauth.permissionUrl(scope, {returnTo?})` returns `accountAppUrl()` with `permission=<scope>` and, when given, `return_to=<returnTo>`, so the person lands on the account page with that optional permission's switch first. It throws `CrouterError` when `scope` is not a valid scope string.
16
+ - `FakeCloud` memory visibility honors `read_private` (public and private, never friends). It follows the user's whole grant, not a run's narrowed `scopes`.
17
+ - A runtime older than this release cannot parse the scope and marks a grant that holds it inactive, so a directory change that offers it waits until hosted runtimes are on this release.
18
+
10
19
  ## 0.3.484
11
20
 
12
21
  Plugins can now declare canvas object kinds and act on the canvas, and an app token can be granted canvas scopes. Everything below is additive: no request or response an existing app sends or reads changes, with one runtime-side exception under [Unknown bearers](#unknown-bearers-are-refused).
13
22
 
14
23
  ### Plugin manifests declare `object_kinds` and `scopes` (additive)
15
24
 
16
- `plugin.json` and `bundle.json` take two optional top-level blocks. `object_kinds` maps a kind name (`<plugin>.<name>`, so the first segment is the plugin's own name) to `{ description, events, read? }`: `events` holds up to 64 entries `{ level: 1-10, supersedes?: boolean }`, and `read.command` is a leaf path below the plugin's root command. That leaf must answer once: a leaf declared `rest.streaming: true` is refused as a `read.command`. `scopes` lists the canvas scopes the plugin asks for. A provider bundle may not declare `scopes`: a cloud app's scopes come from directory consent. Install, update and enable refuse an invalid block and keep the installed version; a kind another owner already holds is reported as `kind_owned` and the plugin's other kinds still apply. `crtr pkg plugin install|update|enable` print these as `warnings`, plus `appScopes` (the scopes the plugin's token now carries). Plugin authors: see [Local plugins](../plugin/deploying.md#local-plugins).
25
+ `plugin.json` and `bundle.json` take two optional top-level blocks. `object_kinds` maps a kind name (`<plugin>.<name>`, so the first segment is the plugin's own name) to `{ description, events, read? }`: `events` holds up to 64 entries `{ level: 1-10, supersedes?: boolean }`, and `read.command` is a leaf path below the plugin's root command. That leaf must answer once: a leaf declared `rest.streaming: true` is refused as a `read.command`. `scopes` lists the canvas scopes the plugin asks for. A provider bundle may not declare `scopes`: a cloud app's scopes come from directory consent. Install, update and enable refuse an invalid block and keep the installed version; a kind another owner already holds is reported as `kind_owned` and the plugin's other kinds still apply. `crtr pkg plugin install|update|enable` print these as `warnings`, plus `appScopes` (the scopes the plugin's token now carries). Plugin authors: see [Local plugins](https://crouter.docs.northlight.ai/docs/plugin/deploying#local-plugins).
17
26
 
18
27
  `ReconcilePackagesDTO` (the answer of `POST /v1/documents/reconcile-packages`) gains two optional fields: `kinds` (`applied` with `outcome` of `inserted`, `updated` or `unchanged`; `refused` with code `kind_owned`; `invalid` plugins with the reason) and `tokens` (`granted` plugins with their scopes, and `revoked` plugin names).
19
28
 
@@ -38,7 +47,7 @@ Each enabled local plugin that declares `object_kinds` or `scopes`, or serves ov
38
47
 
39
48
  ### `Crtr-Caller` and `Crtr-Signature` on local HTTP calls (additive)
40
49
 
41
- Every HTTP call to a plugin carries `Crtr-Caller: node=<id>; machine=<id>; profile=<id>; run=<id>` (fields in that order, empty ones left out, header omitted when none). Calls to a local plugin's endpoint origin also carry `Crtr-Signature: v1,t=<unix seconds>,h=<signed header names>,sig=<base64url HMAC-SHA256 keyed by the plugin's token>` plus a fresh `Crtr-Request-Id`. A manifest cannot set these three header names. Provider calls carry `Crtr-Caller` too. Plugin authors: the canonical string and a reference verifier are in [Local plugins](../plugin/deploying.md#local-plugins).
50
+ Every HTTP call to a plugin carries `Crtr-Caller: node=<id>; machine=<id>; profile=<id>; run=<id>` (fields in that order, empty ones left out, header omitted when none). Calls to a local plugin's endpoint origin also carry `Crtr-Signature: v1,t=<unix seconds>,h=<signed header names>,sig=<base64url HMAC-SHA256 keyed by the plugin's token>` plus a fresh `Crtr-Request-Id`. A manifest cannot set these three header names. Provider calls carry `Crtr-Caller` too. Plugin authors: the canonical string and a reference verifier are in [Local plugins](https://crouter.docs.northlight.ai/docs/plugin/deploying#local-plugins).
42
51
 
43
52
  crtrd signs the call; the CLI never holds the token. `POST /v1/plugins/:name/call-headers` (new operation `plugins.call-headers`, person and broker callers) takes only digests, `{ method, url, body_sha256, header_names, headers_sha256 }`, and answers `{ headers }` (`Crtr-Caller`, and `Crtr-Signature` when the URL is on the installed endpoint's origin and that endpoint is loopback). Any other URL gets `Crtr-Caller` only. An unknown or disabled plugin and a provider plugin answer `404 not_found`; malformed digests answer `400 invalid_request`. SDK: `CrtrClient.plugins.callHeaders(name, request)`, `PluginCallHeadersRequestDTO`, `PluginCallHeadersDTO`, `routes.pluginCallHeaders(name)`.
44
53
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.484",
3
+ "version": "0.3.486",
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.484",
140
+ "@north-light/identity": "0.3.486",
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",
@@ -22,7 +22,7 @@ export interface IdTokenClaims extends JWTPayload {
22
22
  family_name?: string;
23
23
  picture?: string;
24
24
  }
25
- export type Scope = 'crtr:llm' | 'crtr:act' | 'crtr:schedule' | `crtr:memory:${'read' | 'rw'}:user${'' | `:${string}`}` | 'crtr:memory:manage:user' | `crtr:memory:${'read' | 'rw'}:app:${string}` | `crtr:files:${'read' | 'rw'}:${'user' | `app:${string}`}:${string}` | 'crtr:files:share' | `crtr:${'conversations' | 'objects'}:read:app:${string}` | `crtr:objects:${'message' | 'spawn'}:app:${string}` | `crtr:objects:${'read' | 'watch' | 'link' | 'write'}:${'user' | 'node' | `profile:${string}` | `app:${string}`}` | `crtr:peer:${string}` | `${string}:${string}`;
25
+ export type Scope = 'crtr:llm' | 'crtr:act' | 'crtr:schedule' | `crtr:memory:${'read' | 'rw'}:user${'' | `:${string}`}` | 'crtr:memory:manage:user' | 'crtr:memory:read_private:user' | `crtr:memory:${'read' | 'rw'}:app:${string}` | `crtr:files:${'read' | 'rw'}:${'user' | `app:${string}`}:${string}` | 'crtr:files:share' | `crtr:${'conversations' | 'objects'}:read:app:${string}` | `crtr:objects:${'message' | 'spawn'}:app:${string}` | `crtr:objects:${'read' | 'watch' | 'link' | 'write'}:${'user' | 'node' | `profile:${string}` | `app:${string}`}` | `crtr:peer:${string}` | `${string}:${string}`;
26
26
  /** App-memory form keys omit the app id. */
27
27
  export declare const ENFORCED: ReadonlySet<string>;
28
28
  export declare function parseScope(value: string, grantee?: string): Scope;
@@ -21,6 +21,9 @@ function selector(value) {
21
21
  return { kind: 'share', value };
22
22
  if (value === 'crtr:memory:manage:user')
23
23
  return { kind: 'memory', access: 'manage', holder: 'user' };
24
+ // Read-only access to the person's `public` and `private` memory (never `friends`). It names no document path.
25
+ if (value === 'crtr:memory:read_private:user')
26
+ return { kind: 'memory', access: 'read_private', holder: 'user' };
24
27
  let match = /^crtr:memory:(read|rw):user(?::(.+))?$/.exec(value);
25
28
  if (match && (match[2] === undefined || validPath(match[2])))
26
29
  return { kind: 'memory', access: match[1], holder: 'user', path: match[2] };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/identity",
3
- "version": "0.3.484",
3
+ "version": "0.3.486",
4
4
  "description": "Northlight access-token verification and scope rules",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.484",
3
+ "version": "0.3.486",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/cli",
9
- "version": "0.3.484",
9
+ "version": "0.3.486",
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.484",
21
+ "@north-light/identity": "0.3.486",
22
22
  "cron-parser": "^5.6.0",
23
23
  "esbuild": "^0.27.7",
24
24
  "jose": "^6.2.12",
@@ -12116,17 +12116,17 @@
12116
12116
  },
12117
12117
  "packages/crouter-api": {
12118
12118
  "name": "@north-light/api",
12119
- "version": "0.3.484",
12119
+ "version": "0.3.486",
12120
12120
  "license": "UNLICENSED"
12121
12121
  },
12122
12122
  "packages/crouter-env-docker": {
12123
12123
  "name": "@north-light/env-docker",
12124
- "version": "0.3.484",
12124
+ "version": "0.3.486",
12125
12125
  "license": "UNLICENSED"
12126
12126
  },
12127
12127
  "packages/crouter-identity": {
12128
12128
  "name": "@north-light/identity",
12129
- "version": "0.3.484",
12129
+ "version": "0.3.486",
12130
12130
  "license": "UNLICENSED",
12131
12131
  "dependencies": {
12132
12132
  "jose": "^6.2.1"
@@ -12134,20 +12134,20 @@
12134
12134
  },
12135
12135
  "packages/crouter-plugin": {
12136
12136
  "name": "@north-light/plugin",
12137
- "version": "0.3.484",
12137
+ "version": "0.3.486",
12138
12138
  "license": "UNLICENSED",
12139
12139
  "dependencies": {
12140
- "@north-light/api": "0.3.484",
12141
- "@north-light/identity": "0.3.484"
12140
+ "@north-light/api": "0.3.486",
12141
+ "@north-light/identity": "0.3.486"
12142
12142
  }
12143
12143
  },
12144
12144
  "packages/crouter-sdk": {
12145
12145
  "name": "@north-light/sdk",
12146
- "version": "0.3.484",
12146
+ "version": "0.3.486",
12147
12147
  "license": "UNLICENSED",
12148
12148
  "dependencies": {
12149
- "@north-light/api": "0.3.484",
12150
- "@north-light/identity": "0.3.484",
12149
+ "@north-light/api": "0.3.486",
12150
+ "@north-light/identity": "0.3.486",
12151
12151
  "jose": "^6.2.1"
12152
12152
  },
12153
12153
  "bin": {