@neta-art/cohub 4.0.0 → 4.0.1

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.
@@ -205,7 +205,8 @@ result needs `taskrun.view` (a work scope).
205
205
  |---|---|---|---|
206
206
  | Read space config | `space.get()` / `space.getConfig()` | `space.view` | work |
207
207
  | List models | `client.models.list()` / `listMultimodal()` | *(none — just authenticated)* | — |
208
- | Send a prompt | `space.prompt({ content, ... })` | `session.prompt.fullaccess` (or `.readonly`) | viewer |
208
+ | Send a prompt (full) | `space.prompt({ accessMode: "full_access", content, ... })` | `session.prompt.fullaccess` | viewer |
209
+ | Send a prompt (read-only) | `space.prompt({ accessMode: "read_only", content, ... })` | `session.prompt.readonly` | viewer |
209
210
  | Read turn result | `session.turns.get(turnId)` | `session.view` | work |
210
211
  | Stream generation | `session.subscribeGeneration({ state, finalized })` | `session.view` | work |
211
212
  | Read file tree | `space.files.tree()` | `file.view` | work |
@@ -385,14 +386,23 @@ Assume `client` and `space` are already initialized per [§4](#4-initialization-
385
386
  ### LLM chat (`space.prompt` + `subscribeGeneration`)
386
387
 
387
388
  **Scopes:** viewer `session.prompt.fullaccess` (to send) + work `session.view` (to read/stream).
389
+ For a read-only prompt (no side effects), use viewer `session.prompt.readonly`
390
+ instead — but you **must** pass `accessMode: "read_only"` in the call (see the
391
+ read-only recipe below).
392
+
393
+ > **`accessMode` defaults to `full_access`.** If you omit it, the backend
394
+ treats the call as full-access and requires `session.prompt.fullaccess`. This
395
+ is the #1 cause of "I requested `session.prompt.readonly` but still got 403".
396
+ Always set `accessMode` explicitly to match the scope you requested.
388
397
 
389
398
  `space.prompt()` is **asynchronous** — it returns immediately with a turn
390
399
  whose `assistantText` is `null`. You must either stream the reply via
391
400
  `subscribeGeneration` or poll `turns.get()`.
392
401
 
393
402
  ```js
394
- // Send a prompt (creates or continues a session)
403
+ // Send a prompt (creates or continues a session) — full access
395
404
  const result = await space.prompt({
405
+ accessMode: "full_access", // default; needs session.prompt.fullaccess
396
406
  content: [{ type: "text", text: "Describe a shiba inu on Mars." }],
397
407
  sessionId: null, // null → creates a new session; pass an id to continue
398
408
  model: "gpt-5.5", // optional; omit for default
@@ -449,6 +459,58 @@ const reply = turn.assistantText;
449
459
  ignore it, your code silently degrades to polling — which will also 403.
450
460
  Surface the error so you can diagnose the missing scope.
451
461
 
462
+ #### Read-only prompt (`accessMode: "read_only"`)
463
+
464
+ Use this when your Work only needs to **generate** a reply without persisting
465
+ any side effects (no new session is written, no turn stored on the space's
466
+ history). It requires the lighter viewer scope `session.prompt.readonly`
467
+ instead of `session.prompt.fullaccess`.
468
+
469
+ The critical detail: you **must** pass `accessMode: "read_only"` explicitly
470
+ in the `space.prompt()` call. The scope you request via `auth.request` and
471
+ the `accessMode` you send must match — the backend picks the permission check
472
+ based on `accessMode`, defaulting to `full_access` when omitted.
473
+
474
+ ```js
475
+ // 1. Request ONLY the read-only scope from a user gesture
476
+ await client.auth.request({
477
+ scopes: ["session.prompt.readonly"],
478
+ reason: "Generate a one-off character reply (read-only).",
479
+ });
480
+
481
+ // 2. Send the prompt with accessMode matching the granted scope
482
+ const result = await space.prompt({
483
+ accessMode: "read_only", // ← required; omitting it → full_access → 403
484
+ sessionId: null, // read-only prompts use a throwaway session
485
+ content: [{ type: "text", text: prompt }],
486
+ });
487
+ const sessionId = result.session.id;
488
+ const turnId = result.turn.id;
489
+
490
+ // 3. Read the reply — still needs the work scope: session.view
491
+ const stop = space.session(sessionId).subscribeGeneration({
492
+ finalized: (event) => {
493
+ const reply = event.turn.assistantText
494
+ ?? (event.turn.assistantContent ?? [])
495
+ .filter(b => b.type === "text").map(b => b.text).join("");
496
+ console.log("reply:", reply);
497
+ stop();
498
+ },
499
+ error: (event) => console.error("stream error:", event),
500
+ });
501
+ ```
502
+
503
+ Publish the Work with:
504
+ - workScopes: `["space.view", "session.view"]` (still needed to read the reply)
505
+ - allowedViewerScopes: `["session.prompt.readonly"]`
506
+
507
+ > **Scope/accessMode mismatch → 403.** Requesting `session.prompt.readonly`
508
+ but calling `space.prompt({ content })` (no `accessMode`) fails because the
509
+ backend defaults to `full_access` and checks `session.prompt.fullaccess`.
510
+ Symmetrically, requesting `session.prompt.fullaccess` while passing
511
+ `accessMode: "read_only"` also works only if `session.prompt.readonly` is
512
+ additionally granted — otherwise 403. Always keep them in sync.
513
+
452
514
  ### Image / media generation (`generations.createAndWait`)
453
515
 
454
516
  **Scopes:** viewer `generation.create` (to create) + work `taskrun.view` (to poll).
@@ -895,6 +957,10 @@ Before publishing your Work, verify each item:
895
957
  (for file reads). Missing any of these → 403 on reads.
896
958
  - [ ] **Viewer scopes include all action operations**: `session.prompt.fullaccess`
897
959
  (or `.readonly`) for prompts, `generation.create` for generation.
960
+ - [ ] **`session.prompt.*` scope matches `space.prompt({ accessMode })`.**
961
+ Omitting `accessMode` defaults to `full_access`, so requesting only
962
+ `session.prompt.readonly` and then calling `space.prompt({ content })` → 403.
963
+ Set `accessMode: "read_only"` explicitly when using the readonly scope.
898
964
  - [ ] **`session.prompt.fullaccess` does NOT include `session.view`** — they
899
965
  are separate. Sending a prompt succeeds but reading the reply 403s without
900
966
  `session.view`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "description": "Cohub SDK for spaces, sessions, boards, and realtime agent collaboration.",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,