@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.
- package/docs/work-runtime-guide.md +68 -2
- package/package.json +1 -1
|
@@ -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`
|
|
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`.
|