@huynle/hub 0.8.7 → 0.9.0
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/CHANGELOG.md +129 -0
- package/README.md +69 -12
- package/package.json +5 -1
- package/packages/claude-code/package.json +1 -1
- package/packages/cli/dist/hub-cli.js +278 -35
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/cli.ts +23 -10
- package/packages/cli/src/http.ts +1 -0
- package/packages/cli/src/launches.ts +5 -1
- package/packages/cli/src/mcp.ts +28 -8
- package/packages/cli/src/messaging.ts +14 -0
- package/packages/cli/src/team-intake-mcp.ts +26 -2
- package/packages/codex/package.json +1 -1
- package/packages/node/package.json +1 -1
- package/packages/preset/package.json +1 -1
- package/packages/protocol/package.json +1 -1
- package/packages/protocol/schema/app-delivery/v1.schema.json +677 -0
- package/packages/protocol/src/artifacts.ts +43 -0
- package/packages/protocol/src/credentials.ts +38 -0
- package/packages/protocol/src/index.ts +56 -5
- package/packages/protocol/src/launch.ts +7 -2
- package/packages/protocol/src/markdown.ts +753 -0
- package/packages/protocol/src/media-allowlist.ts +240 -0
- package/packages/protocol/src/media.ts +120 -0
- package/packages/protocol/src/preview-bundle.ts +221 -0
- package/packages/protocol/src/voice-recording.ts +8 -0
- package/packages/runtime-openshell/package.json +1 -1
- package/packages/runtime-sdk/package.json +1 -1
- package/packages/sdk/package.json +1 -1
- package/packages/sdk/src/client.ts +17 -3
- package/packages/sdk/src/errors.ts +1 -0
- package/packages/sdk/src/team-intake.ts +8 -0
- package/packages/sdk/src/types.ts +16 -2
- package/packages/spoke-sdk/package.json +1 -1
- package/src/central/agent/delivery.ts +35 -6
- package/src/central/agent/errors.ts +2 -0
- package/src/central/agent/events.ts +11 -2
- package/src/central/agent/launch-engine.ts +65 -4
- package/src/central/agent/launch-routes.ts +8 -0
- package/src/central/agent/launch-spec.ts +18 -1
- package/src/central/agent/launch-store.ts +2 -2
- package/src/central/agent/message-routes.ts +49 -11
- package/src/central/agent/messages.ts +17 -4
- package/src/central/agent/not-after.ts +28 -0
- package/src/central/agent/placement.ts +36 -4
- package/src/central/agent/routes.ts +3 -1
- package/src/central/agent/webhook-routes.ts +21 -9
- package/src/central/agent/webhooks.ts +27 -9
- package/src/central/artifacts/host.ts +3 -1
- package/src/central/artifacts/maintenance.ts +2 -1
- package/src/central/artifacts/preview-bundle.ts +81 -0
- package/src/central/artifacts/previews.ts +94 -0
- package/src/central/artifacts/routes.ts +12 -1
- package/src/central/artifacts/store.ts +198 -8
- package/src/central/config.ts +34 -0
- package/src/central/fetch-budget.ts +59 -0
- package/src/central/fetcher.ts +372 -0
- package/src/central/http.ts +277 -22
- package/src/central/media-remote.ts +248 -0
- package/src/central/media-timeline.ts +317 -0
- package/src/central/media-unfurl.ts +119 -0
- package/src/central/profiles/content.ts +3 -35
- package/src/central/recording/routes.ts +2 -2
- package/src/central/recording/schema.ts +3 -1
- package/src/central/registry.ts +449 -21
- package/src/central/spoke-ws.ts +17 -3
- package/src/central/store.ts +21 -0
- package/src/central/teams/intake-routes.ts +2 -1
- package/src/central/teams/intake.ts +200 -15
- package/src/central/unfurl-parse.ts +313 -0
- package/src/index.ts +68 -8
- package/src/speakable.ts +33 -1
- package/src/spoke/conversation.ts +7 -4
- package/src/spoke/deliver.ts +19 -0
- package/src/spoke/media-bytes.ts +171 -0
- package/src/spoke/media-policy.ts +394 -0
- package/src/spoke/media.ts +189 -0
- package/src/spoke/own-server.ts +30 -0
- package/src/spoke/preview-bundle.ts +538 -0
- package/src/spoke/spoke.ts +387 -17
- package/src/spoke/timeline.ts +59 -5
- package/src/spoke/voice-front.ts +335 -0
- package/web/dist/assets/24BXRY42-Zfr7jm_p.js +2 -0
- package/web/dist/assets/4FHPYW2L-C6Z67098.js +2 -0
- package/web/dist/assets/4LWW3ZNH-CUiYeygH.js +1 -0
- package/web/dist/assets/5THPTBWS-Ce_hS451.js +1 -0
- package/web/dist/assets/5XYWFMKW-CW-BCKiK.js +4 -0
- package/web/dist/assets/64PMEAB3-BjpCDGU9.js +8 -0
- package/web/dist/assets/6BEWHPQE-BpNa96Ze.js +2 -0
- package/web/dist/assets/6PIGVMFT-YXn9Hgnb.js +1 -0
- package/web/dist/assets/75JW6PN6-BFpDBuGN.js +2 -0
- package/web/dist/assets/7WLXJQOR-D6QVYUu9.js +1 -0
- package/web/dist/assets/7XWI72E6-BMuIspEy.js +2 -0
- package/web/dist/assets/A4UD2L5Z-BQzcD9dU.js +8 -0
- package/web/dist/assets/ArtifactPage-4GoBqSpU.js +1 -0
- package/web/dist/assets/ArtifactPane-Dx_0iWwH.js +1 -0
- package/web/dist/assets/ArtifactWorkbench-CuHgARMk.js +6 -0
- package/web/dist/assets/BGW36YJW-CJMhya8S.js +4 -0
- package/web/dist/assets/CGIZI67J-DYDSLME8.js +2 -0
- package/web/dist/assets/DKTLEL5E-DciMwFJ1.js +1 -0
- package/web/dist/assets/DNNCRC2E-BIeKf5t-.js +5 -0
- package/web/dist/assets/Diagram-CZM_R7MG.js +1 -0
- package/web/dist/assets/Diagram-vdpmp3xu.css +1 -0
- package/web/dist/assets/E67EBMDD--AucIw59.js +5 -0
- package/web/dist/assets/EFYWJBJK-CVywe_D5.js +1 -0
- package/web/dist/assets/EXE5EH6O-DR1IHCTr.js +4 -0
- package/web/dist/assets/FTXIJQGB-BZVuiBn1.js +1 -0
- package/web/dist/assets/G3MDBVQH-BZmnMkFw.js +1 -0
- package/web/dist/assets/G6GPDDNC-DEz8F449.js +2 -0
- package/web/dist/assets/G76NGGFM-CrB2V34K.js +1 -0
- package/web/dist/assets/GH6G3TJC-D3RTYEH4.js +2 -0
- package/web/dist/assets/HPNUA7RO-B19NeNOJ.js +1 -0
- package/web/dist/assets/HQRGC5D7-Dmpzq6Rn.js +4 -0
- package/web/dist/assets/IX6Z2TVR-DphRHnek.js +5 -0
- package/web/dist/assets/J77NQRCJ-B7AGcxvw.js +3 -0
- package/web/dist/assets/JAD36DTZ-B9-dbsS0.js +5 -0
- package/web/dist/assets/LF3NJQAX-koL91UZ9.js +2 -0
- package/web/dist/assets/LRZCOFJZ-Cdjasfuf.js +6 -0
- package/web/dist/assets/Library-TwvrwqSk.js +2 -0
- package/web/dist/assets/Lightbox-C8KNKwOA.css +1 -0
- package/web/dist/assets/Lightbox-Du56MXGX.js +1 -0
- package/web/dist/assets/MediaImage-Ds_JUx3a.js +4 -0
- package/web/dist/assets/N5NF2FGY-OuapYTfr.js +2 -0
- package/web/dist/assets/N7OWWQHY-BNLd-Q_U.js +2 -0
- package/web/dist/assets/NT5JY5WP-1lJM-aun.js +1 -0
- package/web/dist/assets/OT7W3RNY-jDL3drzX.js +4 -0
- package/web/dist/assets/P67P66DB-CE-opRb8.js +3 -0
- package/web/dist/assets/PFLLLM7K-DDJFgzW3.js +2 -0
- package/web/dist/assets/PUAMC4MJ-CtBm-WuU.js +12 -0
- package/web/dist/assets/PVFCHRLY-CG5JXv60.js +2 -0
- package/web/dist/assets/ProfilesDestination-rKNDpLjR.js +1 -0
- package/web/dist/assets/QQW2P742-BbcEkpa4.js +1 -0
- package/web/dist/assets/RVARI57O-CgA1WoRv.js +1 -0
- package/web/dist/assets/S63OEDPG-CKlhpvl2.js +3 -0
- package/web/dist/assets/SG5TVYLW-BBtiwXPL.js +2 -0
- package/web/dist/assets/TeamsPage-DtJylLrt.js +1 -0
- package/web/dist/assets/UGRSUGA2-urZ_hx83.js +5 -0
- package/web/dist/assets/V44WA6U7-DYfX3OlM.js +1 -0
- package/web/dist/assets/V67O4WWV-xYxJ00QW.js +1 -0
- package/web/dist/assets/VBUCM3YD-JHGjuJC_.js +1 -0
- package/web/dist/assets/VINGRCPA-DZuJa3SH.js +7 -0
- package/web/dist/assets/WL6DNKX4-BHqripdA.js +2 -0
- package/web/dist/assets/WQ63CJYP-D6G5frDY.js +2 -0
- package/web/dist/assets/Watchtower-Bmgdyfjb.js +2 -0
- package/web/dist/assets/XOYAOB3S-L-KeA1EF.js +13 -0
- package/web/dist/assets/Y5LQLG3M-dr2-erb8.js +2 -0
- package/web/dist/assets/YIK6JTN6--1t-Ud8k.js +2 -0
- package/web/dist/assets/artifacts-DU9uRgs1.js +1 -0
- package/web/dist/assets/index-C3ci5UtY.css +1 -0
- package/web/dist/assets/index-D-xG9nrc.js +36 -0
- package/web/dist/assets/lazy-f3CuUNJZ.js +43 -0
- package/web/dist/assets/live-7q72Lyj2.js +5 -0
- package/web/dist/assets/mermaid.worker-WDVRAh32.js +1 -0
- package/web/dist/assets/pane-layout-0RrLHGDr.js +1 -0
- package/web/dist/assets/preload-helper-BLdHbfdN.js +1 -0
- package/web/dist/assets/{renderer2d-CYag1CnR.js → renderer2d-hfZxM6Xl.js} +1 -1
- package/web/dist/assets/{renderer3d-BRB6rgDJ.js → renderer3d-Km9vipTM.js} +1 -1
- package/web/dist/assets/{safe-area-fOy0AOuG.js → safe-area-BAbgBiFk.js} +1 -1
- package/web/dist/assets/{silero-worker-DhlIHyD_.js → silero-worker-e802lB5T.js} +1 -1
- package/web/dist/assets/smart-turn-v3.2-cpu-WpeLRbPA.onnx +0 -0
- package/web/dist/assets/smart-turn-worker-DaVJPdvJ.js +11 -0
- package/web/dist/assets/source-BWlvaODa.js +1 -0
- package/web/dist/assets/toast-Cuu2K0hx.js +1 -0
- package/web/dist/assets/ui-CFB4wQfF.js +3 -0
- package/web/dist/index.html +8 -6
- package/web/src/voice/turn/LICENSE +28 -0
- package/web/dist/assets/ArtifactPage-Dq7VW-bU.js +0 -1
- package/web/dist/assets/ArtifactPane-DKwbMTX9.js +0 -1
- package/web/dist/assets/ArtifactWorkbench-vrgIfncX.js +0 -6
- package/web/dist/assets/Library-D6-Go6Cu.js +0 -2
- package/web/dist/assets/ProfilesDestination-BOML2Dgf.js +0 -1
- package/web/dist/assets/TeamsPage-CNBTE8gB.js +0 -1
- package/web/dist/assets/Watchtower-Dor1Ubjy.js +0 -2
- package/web/dist/assets/hub-CrbItiwP.js +0 -13
- package/web/dist/assets/index-82Ce0IAf.css +0 -1
- package/web/dist/assets/index-D9SP3_0z.js +0 -22
- package/web/dist/assets/live-BNJgy0vg.js +0 -5
- package/web/dist/assets/pane-layout-EdR1kRrG.js +0 -1
- package/web/dist/assets/preload-helper-DqrJTq0t.js +0 -1
- package/web/dist/assets/source-Wbq35qGp.js +0 -1
- package/web/dist/assets/ui-j5FFk90B.js +0 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,134 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
Three things in one release: **media in sessions** (screenshots, images, Mermaid diagrams, safe HTML previews, and
|
|
6
|
+
web images and link previews for the sites you allow), **app delivery** for hub apps (idempotent messages, delivery
|
|
7
|
+
deadlines, and team-intake webhooks), and two **voice experiments** that were prepared as 0.8.8 (Smart Turn and the
|
|
8
|
+
Voice front, both off by default). Session images, local image paths, HTML previews and the Voice front need the
|
|
9
|
+
0.9.0 plugin on the machine (the OpenCode harness; other harnesses keep file chips and today's Voice mode); Mermaid
|
|
10
|
+
diagrams, web images and link previews work in any reply. No migration is needed.
|
|
11
|
+
[docs/session-view.md → Media in sessions](docs/session-view.md#media-in-sessions),
|
|
12
|
+
[docs/media-security.md](docs/media-security.md),
|
|
13
|
+
[docs/plans/2026-11-06-app-delivery-contracts.md](docs/plans/2026-11-06-app-delivery-contracts.md).
|
|
14
|
+
|
|
15
|
+
### Media in sessions
|
|
16
|
+
- **Tool screenshots and attachments in the transcript.** A tool group shows its images as a strip of thumbnails
|
|
17
|
+
under its head (also while folded; 8, then "+N"), so a screenshot is no longer just a `[image/png, 1.2 MB — not
|
|
18
|
+
shown]` placeholder, and your attachments load the same way. Images keep their space while loading, load once on screen, and on a phone
|
|
19
|
+
over cellular, 2g or Save-Data an image over 512 KB waits for a tap ("Screenshot · 1.8 MB · Tap to load").
|
|
20
|
+
- **Image files a reply names** (``) show inline, read from the agent's machine only when
|
|
21
|
+
the path is in that message's text and inside the session's folder (its `.worktrees/` too) or a file the session
|
|
22
|
+
itself read, wrote or edited; never dotfiles (`.git/`, `.env`, `.ssh/`) it did not touch, never `~`.
|
|
23
|
+
- **Full-screen viewer** for any image or diagram: pinch, wheel or double tap to zoom, drag to pan, ←/→ between a
|
|
24
|
+
message's images, Download, Escape to close.
|
|
25
|
+
- **A failed image says why** on its chip: the machine is offline, not in this session's files, too large, not an
|
|
26
|
+
image, this harness can't share images, or try again (one quiet retry, then Retry).
|
|
27
|
+
- **Artifact images in replies**: `` (or a revision) shows the artifact's image inline with a
|
|
28
|
+
caption link, in session replies, the Concierge panel and artifact Markdown.
|
|
29
|
+
- **Mermaid diagrams**: ```` ```mermaid ```` and ```` ```mmd ```` blocks draw as diagrams (in the session, the
|
|
30
|
+
Concierge panel and artifacts) with a Diagram / Source toggle, Copy source and Download SVG, themed to light or
|
|
31
|
+
dark. They draw once the fence has closed, when scrolled into view; a broken or unsupported diagram shows its code
|
|
32
|
+
with the reason, one over 50 KB stays code. Voice says "a diagram" instead of reading the source.
|
|
33
|
+
- **HTML previews**: an ```` ```html ```` block, or a code span or link naming a local `.html` page, gets a
|
|
34
|
+
**Preview** button. The page (with its scripts, styles and images) opens beside the session in the isolated
|
|
35
|
+
artifact host, where external resources are blocked; a local page lists the files it left out and has
|
|
36
|
+
**Refresh**. Previews are private artifacts: **Keep** one to make it a normal artifact, else it is archived a day
|
|
37
|
+
after you last opened it and deleted a week later. Artifacts → **Show: Previews** lists them.
|
|
38
|
+
- **Web images**: Settings → **Media** holds the sites whose images load by themselves (`github.com`,
|
|
39
|
+
`*.githubusercontent.com`; empty at first). Any other site's image is a card naming the domain, with **Load
|
|
40
|
+
image**, **Always allow <domain>** and "Load all from this domain in this session". The hub fetches the image, not
|
|
41
|
+
your browser.
|
|
42
|
+
- **Link previews**: a link to an allowed site gets a card (title, description, image) under its paragraph, at most
|
|
43
|
+
3 per message; other links get a small preview button that fetches only when you tap it. No favicons.
|
|
44
|
+
|
|
45
|
+
### Security
|
|
46
|
+
- **Agent media is untrusted everywhere.** The console's CSP is unchanged. Raster images show only as `blob:` URLs of
|
|
47
|
+
bytes the hub checked; SVG (files, artifacts, diagrams) only as a `data:` URL in an `<img>`, never in the page and
|
|
48
|
+
never as a `blob:` (the viewer offers SVG as a download only); diagrams with active content are not shown; raw HTML
|
|
49
|
+
in a reply still never renders, and HTML runs only in the isolated artifact host.
|
|
50
|
+
- **The spoke decides which local files can be shown**, never the console: the path must be in the message's own
|
|
51
|
+
text; realpath under the session's folder or in its touched set (completed read/write/edit only); no unvisited
|
|
52
|
+
dotfiles; regular files whose open handle is the checked file (Linux also through `/proc/self/fd`, macOS 11+ with
|
|
53
|
+
`O_NOFOLLOW_ANY`); magic bytes PNG/JPEG/WebP/GIF/SVG, ≤ 8 MiB, ≤ 40 MP, ≤ 16,384 px per side. The hub re-sniffs
|
|
54
|
+
every image and checks its length and SHA-256 before answering.
|
|
55
|
+
- **A safe outbound fetcher** for web images and link previews: signed-in users only (session and app tokens are
|
|
56
|
+
refused, so agents cannot use the hub as a fetcher), http/https on default ports without userinfo, private,
|
|
57
|
+
loopback, link-local (cloud metadata), CGNAT, reserved, ULA and mapped/translated/tunnelled IPv6 addresses refused,
|
|
58
|
+
the checked address pinned (DNS rebinding), redirects re-checked (3 at most), no cookies, 3 s / 8 s timeouts,
|
|
59
|
+
bodies capped and never decompressed (gzip bombs), web images PNG/JPEG/WebP/GIF only (no SVG from the web), 30
|
|
60
|
+
fetches a minute and 4 at once per user. The audit log keeps the host and outcome, never the URL (it may hold the
|
|
61
|
+
leaked data).
|
|
62
|
+
- **Allowlist rules**: no IP addresses, ports, paths, bare `*` or public suffixes alone (`*.com`, `*.github.io`); a
|
|
63
|
+
URL with userinfo never loads by itself. Settings warns that an allowed site hosting other people's content can
|
|
64
|
+
still receive data an agent puts in an image URL.
|
|
65
|
+
- Known residual risks (Windows/macOS < 11 directory-swap race, hard links inside the workspace, per-pod limits in
|
|
66
|
+
cluster mode, a Bun wildcard-certificate quirk) are listed in [docs/media-security.md](docs/media-security.md#residual-risks).
|
|
67
|
+
|
|
68
|
+
### For operators
|
|
69
|
+
- **`HUB_REMOTE_FETCH`** (`on` | `off`): `off` turns off web images and link previews (cards say so). Default `on`,
|
|
70
|
+
but **off when `HTTP_PROXY`/`HTTPS_PROXY` is set** and `NO_PROXY` is not `*` (a proxy could undo the fetcher's
|
|
71
|
+
address pinning), with a startup warning; set `NO_PROXY=*` for the hub process or `HUB_REMOTE_FETCH=on` knowingly.
|
|
72
|
+
- **`HUB_FETCH_DENY_CIDRS`**: extra CIDRs the fetcher never connects to, on top of the built-in ranges. In Kubernetes
|
|
73
|
+
add your pod and service CIDRs (e.g. `10.42.0.0/16,10.43.0.0/16`); an invalid entry stops the hub at start.
|
|
74
|
+
- New routes (README route table): `GET /api/sessions/:id/timeline/messages/:message/media/:ref` (`…/files/:index`
|
|
75
|
+
is its alias on new spokes), `GET /api/media/remote?url=`, `GET /api/media/unfurl?url=`, `GET`/`PUT
|
|
76
|
+
/api/settings/media`, `POST /api/sessions/:id/previews`; artifacts gain `origin` (`GET /api/agent/artifacts?origin=preview|all`,
|
|
77
|
+
previews hidden by default) and `POST /api/agent/artifacts/:id/keep`.
|
|
78
|
+
- The hub now makes outbound HTTP(S) requests for users (when allowed): permit egress to the public internet, or set
|
|
79
|
+
`HUB_REMOTE_FETCH=off`. Caches and rate limits are in memory per pod in cluster mode.
|
|
80
|
+
- CI and releases run on the org's self-hosted runner (`CI_RUNNER`). The release's Windows smoke test of hubctl runs
|
|
81
|
+
on a self-hosted Windows runner when the `WINDOWS_RUNNER` variable is set (JSON labels), else GitHub-hosted
|
|
82
|
+
`windows-latest`.
|
|
83
|
+
|
|
84
|
+
### For harness authors
|
|
85
|
+
- **Two new optional spoke ops**, announced as features of their own and never implied (also not for OpenCode
|
|
86
|
+
hellos): `timelineMedia` (one timeline image by ref `f<n>` / `t<part>.<item>` / `p:<base64url path>`, in 512 KiB
|
|
87
|
+
chunks, `MEDIA_LIMITS`) and `previewBundle` (a local HTML page and its files: a manifest, then chunks,
|
|
88
|
+
`PREVIEW_BUNDLE_LIMITS`). Timeline rows of a spoke with `timelineMedia` carry `ref`/`bytes`/`width`/`height` on
|
|
89
|
+
user files and `media` on tool and assistant rows. With HarnessSpoke, a `timelineMedia` or `previewBundle` handler
|
|
90
|
+
announces it; the conformance suite checks both answers' shapes. Spokes without them get `422 unsupported
|
|
91
|
+
{feature, harness}` from the hub and file chips in the console. Contract:
|
|
92
|
+
[docs/spoke-sdk.md](docs/spoke-sdk.md#media-ops-timelinemedia-and-previewbundle).
|
|
93
|
+
- The console's Markdown parser moved to `packages/protocol/src/markdown.ts` (re-exported by the web app), so the
|
|
94
|
+
hub, the spokes and the console agree on what is an image source, an HTML block or a page link.
|
|
95
|
+
|
|
96
|
+
### App delivery (for hub apps)
|
|
97
|
+
- **`Idempotency-Key` on `POST /api/agent/msg` and `/ask`**: a retry with the same key and body replays the first
|
|
98
|
+
answer (200) instead of sending twice; the same key with a different body is `409 conflict`.
|
|
99
|
+
- **`notAfter` delivery deadlines** (epoch ms, at most 30 days ahead; the CLI and MCP also take ISO 8601) on msg/ask,
|
|
100
|
+
launches and team intake. A deadline already passed is `422 expired` and nothing is stored; a pending message
|
|
101
|
+
that misses it ends `expired` with reason `not_after`, a queued launch ends with outcome `expired`, a queued
|
|
102
|
+
intake item is cancelled with `reasonCode: "not_after"`. It is a delivery/start deadline, never a work deadline.
|
|
103
|
+
- **`team.intake` webhooks for apps**: an app holding `teams:submit` gets signed `team.intake` webhooks for the items
|
|
104
|
+
it submitted.
|
|
105
|
+
- **A machine-checkable contract**: `packages/protocol/schema/app-delivery/v1.schema.json` (JSON Schema, shipped in
|
|
106
|
+
the npm package) is checked against real route answers, events and webhook bodies in the test suite, so the two
|
|
107
|
+
cannot drift. A test fixture runs a real hub with simulated machines for app integration tests.
|
|
108
|
+
|
|
109
|
+
### Voice front (experimental)
|
|
110
|
+
- **A hidden, fast voice agent in front of the session** (Settings → Voice & speech → Voice front). It starts every
|
|
111
|
+
reply by saying what it understood and what it is doing, then relays to the session (steering a running turn,
|
|
112
|
+
queueing an unrelated request, or stopping it). The session's agent and model are never switched. Its result,
|
|
113
|
+
questions, permissions and failures are spoken once; no spoken "On it." phrases (the tones stay). Concierge
|
|
114
|
+
conversations keep today's path. If the front is unavailable, Voice mode talks to the session directly.
|
|
115
|
+
- The front runs on the machine's voice model (`voiceModel` in `~/.config/hub/config.json`, or the session's voice
|
|
116
|
+
model). Pick a fast model; free models can hit usage limits.
|
|
117
|
+
- Spoken and pushed failure reasons now hide credentials ("[redacted: api key]"), for workers too.
|
|
118
|
+
|
|
119
|
+
### Experiments
|
|
120
|
+
- **Smart Turn (experimental) turn detection in Voice mode** (Settings → Voice → Turn detection, per device; the
|
|
121
|
+
timers stay the default): Pipecat Smart Turn v3.2 (BSD-2-Clause) scores the turn's audio on the device at each
|
|
122
|
+
pause; a confident "finished" sends at once, otherwise Voice mode waits (re-scoring) up to 3 s. The Voice bar shows
|
|
123
|
+
the score; the recording mode records it (`turn.predict`, a hub feature an older hub never receives). Tunable with
|
|
124
|
+
`voiceTurn.*` (URL or localStorage). The model (8.7 MB) loads only when the mode is on.
|
|
125
|
+
|
|
126
|
+
### Fixes
|
|
127
|
+
- **Hub messages now show in the OpenCode TUI.** A message, ask, reply or prompt the hub delivers to a session used
|
|
128
|
+
to be invisible in the terminal UI: you only saw the agent react to it. It now shows as a one-line notice where it
|
|
129
|
+
arrived (`hub · ask from @api: Which release order?`). The agent still sees only the full message. Needs the
|
|
130
|
+
0.9.0 plugin on the machine.
|
|
131
|
+
|
|
3
132
|
## 0.8.7
|
|
4
133
|
|
|
5
134
|
The one-liner now installs OpenCode on Windows when a machine does not have it yet (the reviewed OpenCode 2.2.0 pin for
|
package/README.md
CHANGED
|
@@ -61,6 +61,8 @@ Formerly **opencode-voice** and, briefly, **opencode-hub**
|
|
|
61
61
|
- **Launches**: you, a script or an app start a session on one of your machines (`hub launch --branch feat/x
|
|
62
62
|
--prompt "…"`): a `hub node` daemon prepares a git worktree, the session gets its name, roles, labels and first
|
|
63
63
|
prompt, and ends by itself when it is done, with signed webhooks for apps ([docs/launches.md](docs/launches.md)).
|
|
64
|
+
Test an app against a real hub with simulated machines: the app fixture
|
|
65
|
+
([docs/app-integration-testing.md](docs/app-integration-testing.md)).
|
|
64
66
|
- **More harnesses**: Claude Code (`hub claude`, through its hooks), Codex (`hub codex`, through its app-server)
|
|
65
67
|
and any terminal agent described by a data-only preset (`hub preset run`); the console shows only the controls
|
|
66
68
|
each harness's spoke serves, with an evidence badge and honest delivery labels
|
|
@@ -83,6 +85,10 @@ Formerly **opencode-voice** and, briefly, **opencode-hub**
|
|
|
83
85
|
[intake](docs/teams-intake.md)).
|
|
84
86
|
- **Watchtower** (formerly Bird's-eye view): a 3D, 2D or list picture of machines, sessions, teams and live
|
|
85
87
|
message flow, with a team editor ([docs/watchtower.md](docs/watchtower.md)).
|
|
88
|
+
- **Media in sessions**: tool screenshots, attachments and image files a reply names show in the transcript (with a
|
|
89
|
+
full-screen viewer), Mermaid diagrams draw, HTML blocks and local pages preview in the isolated host, and web images
|
|
90
|
+
and link previews load through the hub for sites you allow, without loosening the console's CSP
|
|
91
|
+
([docs/session-view.md](docs/session-view.md#media-in-sessions), security: [docs/media-security.md](docs/media-security.md)).
|
|
86
92
|
- **Session details**: context usage, todos, MCP status, branch and per-file diffs, on demand; unread
|
|
87
93
|
marks synced per user.
|
|
88
94
|
- **Self-hostable**: a hub on your computer with nothing extra to install, or your own hub for phones
|
|
@@ -390,7 +396,14 @@ conversation mode and utterances (403 `not_exposed` for sessions not shared with
|
|
|
390
396
|
|---|---|
|
|
391
397
|
| `POST /api/sessions/:id/turn {text, images?}` | sends a turn; while the session tree is running it steers instead (202 `{mode: "steer", inboxID}`). An idle turn answers its `TurnResult` (`{outcome, reply, message?}`) when it ends; with `Prefer: respond-async` (the console sends it) a turn still running after 1.5 s answers 202 `{mode: "turn"}` (`Preference-Applied: respond-async`) and its result follows as an SSE `turn` event `{sessionID, outcome, reply, message?}`, so no proxy cuts a long request. The session stays busy until the turn ends either way. `images` is up to 4 `{mime, data, name?}` (PNG, JPEG, WebP or GIF; canonical base64 whose bytes match the type; each at most 512 KB decoded, together at most 750,000 base64 characters); `text` may be empty when images are attached. They reach OpenCode as inline file attachments. 400 empty, over 100,000 characters or invalid images (with the reason); 409 "A turn is already running for this session" |
|
|
392
398
|
| `GET /api/sessions/:id/timeline?limit=N&order=desc` · `?limit=N&before=<older>` | the conversation as timeline rows, newest first: the latest `limit` rows (1–200, default 50) in chronological order, plus `older` (the cursor of the rows before them) while there are more. `before=<older>` returns the page before that cursor. Cursors are row keys, so they stay valid while new messages arrive. A page starts at a message boundary when it can (it may be up to twice `limit` then). 400 for an unknown cursor (history reverted or compacted: start again from the newest page). The forward mode `?limit=N&cursor=` (oldest rows first, `cursor` for the next page) still works; it cannot be combined with `before` or `order=desc`. The spoke reads OpenCode's history for a first page and serves the following pages from a short-lived per-session cache (30 s, dropped when the session changes) |
|
|
393
|
-
| `GET /api/sessions/:id/timeline/messages/:message/files/:index` | one image attached to a user message, as bytes with its type (`cache-control: private, no-store`). Timeline user items list their attachments as `files: [{index, mime, name?}]` (never the bytes). Every timeline row carries `pos` (ordering position: session message order, then part order; derived from the message's creation time, kept increasing, so it does not change as pages move); tool rows `callID`, subagent tool rows `childSessionID` and `title` (the task description), assistant text rows `ordinal`. Only PNG, JPEG, WebP and GIF are served (404 otherwise or when missing); 413 when the image would not fit in a protocol frame (about 650 KB) |
|
|
399
|
+
| `GET /api/sessions/:id/timeline/messages/:message/files/:index` | one image attached to a user message, as bytes with its type (`cache-control: private, no-store`). On a spoke that announces `timelineMedia` it is an alias of `…/media/f<index>` below (its headers, cache and limits). Timeline user items list their attachments as `files: [{index, mime, name?}]` (never the bytes). Every timeline row carries `pos` (ordering position: session message order, then part order; derived from the message's creation time, kept increasing, so it does not change as pages move); tool rows `callID`, subagent tool rows `childSessionID` and `title` (the task description), assistant text rows `ordinal`. Only PNG, JPEG, WebP and GIF are served (404 otherwise or when missing); 413 when the image would not fit in a protocol frame (about 650 KB) |
|
|
400
|
+
| `GET /api/sessions/:id/timeline/messages/:message/media/:ref` | (feature `timelineMedia`) one timeline image by ref: `f<n>` (a user attachment), `t<part>.<item>` (a tool content item), `p:<base64url path>` (an image path in an assistant message, served under the spoke's path policy). Pulled from the spoke in 512 KiB chunks and collected whole (≤ 8 MiB, 60 s in all) before anything is answered; the hub re-sniffs the magic bytes (PNG, JPEG, WebP, GIF, SVG) and verifies the length and SHA-256 (a mismatch is 502 `integrity`, never a partial body or a 304). Headers: `ETag: "sha256-<hex>"`, `cache-control: private, no-cache`, `nosniff`, `content-security-policy: default-src 'none'; sandbox`, CORP same-origin, `referrer-policy: no-referrer`, `content-disposition` inline (SVG always `attachment`; `?download=1` for any type). Cached per user on the pod (64 MiB): `f`/`t` images answer `If-None-Match` with 304 without contacting the spoke; `p:` refs are revalidated with one first-chunk call; entries live at most 10 min and are dropped when the session's committed revert (as its spoke reports it, seen from any pod) differs from when they were cached; a staged revert bypasses the cache, a deleted session clears it. The message ID may not contain `/` or `..`. 400 bad ref, 404 `not_found`/`path`/`hidden`, 409 offline, 413 `too_large {bytes,max}`/`dimensions`, 415 `type`, 422 `unsupported {feature, harness}`, 429 `rate_limited` (4 in flight per user, 2 per spoke connection; `Retry-After`), 502 `integrity`, 504 `timeout` |
|
|
401
|
+
| `GET /api/media/remote?url=` | (signed-in users only: session and app tokens get 403) a web image through the hub's safe fetcher ([docs/media-security.md](docs/media-security.md#what-each-layer-does)): http/https on default ports, no userinfo, private and reserved addresses refused (plus `HUB_FETCH_DENY_CIDRS`), the checked address pinned, at most 3 redirects each re-checked, no cookies, 3 s to headers and 8 s in all, never decompressed. Only PNG, JPEG, WebP or GIF by magic bytes (never SVG), ≤ 8 MiB, ≤ 40 MP, ≤ 16,384 px per side, with the media headers of the timeline route and an `ETag` (304 on `If-None-Match`). Cached per user for the site's lifetime clamped to 1–24 h, failures for 5 min. 400 `bad_url`, 403 `refused`/`disabled` (`HUB_REMOTE_FETCH=off`), 413 `too_large`, 415 `not_image`, 429 `rate_limited` (30 fetches a minute and 4 at once per user, shared with `GET /api/media/unfurl`; `Retry-After`), 502 `upstream`, 504 `timeout`. Audited as `media.remote` with the host and outcome, never the URL |
|
|
402
|
+
| `GET /api/settings/media` | (signed-in users only: session and app tokens get 403) `{allowlist: string[], remoteFetch: boolean}`: the caller's media allowlist (hostnames like `github.com` or suffixes like `*.githubusercontent.com`, stored lowercase without a trailing dot, IDNs as punycode; empty by default) and whether this hub fetches web images (`HUB_REMOTE_FETCH`). In a session reply (and the Concierge panel) a web image from an allowlisted site loads at once through `GET /api/media/remote`; any other site's is a card naming the domain with **Load image**, **Always allow <domain>** and "Load all from this domain in this session" (that last one is kept in the page only), and nothing is fetched until a tap. The hub's own origin is never sent to the remote route |
|
|
403
|
+
| `PUT /api/settings/media {allowlist}` | replaces the list, answers like GET. 400 (naming the entry) for more than 100 entries, entries over 253 characters, IP addresses, ports, schemes, paths, userinfo, a bare `*`, a wildcard anywhere but a leading `*.`, and public suffixes alone (`*.com`, `*.co.uk`, `*.github.io`, `co.uk`; a small built-in list). A `*.` entry matches subdomains only. Settings → Media edits it and warns that an allowed site hosting other people's content can still receive data an agent puts in an image URL |
|
|
404
|
+
| `GET /api/media/unfurl?url=` | (signed-in users only: session and app tokens get 403) a link's preview through the hub's safe fetcher: `{url, title?, description?, siteName?, image?}` (`url` is the final URL after redirects; plain text, entities decoded, title ≤ 200, description ≤ 300, site name ≤ 100 characters; `image` resolved against the final URL, http/https on the default port without userinfo, else left out). Reads at most the first 512 KiB; only a 2xx `text/html` or `application/xhtml+xml` answer counts (else `415 not_html`). `cache-control: private, max-age` ≤ 1 h; the hub caches previews per user for the site's lifetime clamped to 1–24 h and failures for 5 min. Shares the per-user budget of `GET /api/media/remote` (30 fetches a minute and 4 at once for both together, `429` with `Retry-After`); `403 disabled` with `HUB_REMOTE_FETCH=off`; the audit log keeps the host and outcome, never the URL. In a session reply (and the Concierge panel) a link to an allowlisted site gets a preview card under its paragraph (at most 3 per message, never for links in code or same-origin links; the card's image loads through `GET /api/media/remote` only when the image's host is allowlisted too); any other link gets a small preview button that fetches only on a tap |
|
|
405
|
+
| `POST /api/sessions/:id/previews {messageID, block}` · `{messageID, path}` | (console only) an HTML preview of the message's `block`-th (0-based) ```` ```html ````/```` ```htm ```` block, or of a local `.html`/`.htm` page the message names with the files it refers to (spoke feature `previewBundle`, read under the media path policy; ≤ 200 files, 5 MiB each, 20 MiB in all). The hub reads the content from the message, never from the request, and stores it as a private `html` artifact with `origin: "preview"`: `{artifact, revision, reused, host}` (+ `skipped`, `skippedMore` for a page), 201 stored, 200 unchanged (`reused`); `POST host` opens it in the isolated artifact host. 400, 404 `not_found`/`too_old`/`refused`, 409 offline/`changed`, 413 `too_large`, 422 `unsupported` (no artifacts, or a spoke without `previewBundle`), 502 `bad_bundle`. Previews are archived 24 h after their last view and purged 7 days later. Contract: [docs/artifacts.md](docs/artifacts.md#html-previews) |
|
|
406
|
+
| `GET /api/agent/artifacts?origin=preview\|all` · `POST /api/agent/artifacts/:id/keep {expectedRevision}` | the library leaves previews out unless `origin` asks for them (`preview`: only previews, `all`: both); **Keep** turns a preview into a normal artifact (`409` on a stale `expectedRevision`; no change for other artifacts). Users and apps with `artifacts:write`, never sessions. See [docs/artifacts.md](docs/artifacts.md#http-api-apiagent) |
|
|
394
407
|
| `POST /api/sessions/:id/rename` `{auto: true}` | Auto-name: OpenCode regenerates the title with its Title agent (`session.update` with an empty title on the process's own server). 200 `{title}` (or `{title, unchanged: true}` when OpenCode kept it), 202 `{pending: true}` when it is still generating after 20 s (the catalog updates when it lands), 409 `plugin_outdated` when no process reporting the session supports it, 502 when the process cannot reach its server or the title model failed |
|
|
395
408
|
| `POST /api/sessions/:id/rename` | `{title}` → `{title}`: renames the session in OpenCode (`session.update`), through the process that owns it, else any other process that reports it. The title is trimmed and must be 1–200 characters (400 otherwise). Works with Voice off; the new title shows in every console at once (the spoke reports it before answering) |
|
|
396
409
|
| `POST /api/sessions/:id/fork {messageID}` · `GET …/revert/preview?messageID=` · `POST …/revert {messageID, files?}` · `POST …/revert/restore` · `POST …/revert/commit` | fork before one of the session's user messages (201 `{sessionID, text, attachments}`), or stage, undo and commit a revert from it (409 `busy` while it runs); console only, audited; needs a spoke announcing `fork`/`revert` (409 `unsupported` otherwise). Contract: [docs/session-view.md](docs/session-view.md#fork-and-revert-a-message) |
|
|
@@ -509,8 +522,8 @@ cookie and a hub bearer token (400).
|
|
|
509
522
|
| `GET /api/agent/who?project=&role=` | ✓ | ✓ | ✓ | named, role-holding and online sessions |
|
|
510
523
|
| `POST /api/agent/name {name, session?}` | ✓ | own session | `sessions:manage` | names a session (`409 name_taken`) |
|
|
511
524
|
| `POST /api/agent/roles/claim {role, exclusive?, project?, session?}` · `…/roles/release` | ✓ | own session | `sessions:manage` + `roles` grant | claims (with a new `generation`) or releases a role; never a reserved role for an app |
|
|
512
|
-
| `POST /api/agent/msg {to, text, wake?, urgent?, all?}` | ✓ | ✓ | `messages:send` | tells a session (`all`: every holder of a role); never yourself |
|
|
513
|
-
| `POST /api/agent/ask {to, text, expiresIn?, wait?}` | ✓ | ✓ | `messages:send` | an ask, open until answered, declined or expired; `wait` (≤ 60 s) returns the `answer` |
|
|
525
|
+
| `POST /api/agent/msg {to, text, wake?, urgent?, all?}`; `Idempotency-Key` | ✓ | ✓ | `messages:send` | tells a session (`all`: every holder of a role); never yourself |
|
|
526
|
+
| `POST /api/agent/ask {to, text, expiresIn?, wait?}`; `Idempotency-Key` | ✓ | ✓ | `messages:send` | an ask, open until answered, declined or expired; `wait` (≤ 60 s) returns the `answer` |
|
|
514
527
|
| `POST /api/agent/ask {to: "@me", text, options?, recommendation?, urgency?, tier?, deadline?, onTimeout?, about?, wait?}` | – | ✓ | `messages:send` | opens a **Decision** for the user (below): `{message, decision, answer?}` |
|
|
515
528
|
| `GET /api/agent/decisions?state=` · `…/decisions/:id` | ✓ (all) | own | `messages:read` (own) | the Decisions the caller asked |
|
|
516
529
|
| `POST /api/agent/reply {replyTo, text}` · `…/decline {replyTo, why}` | ✓ | ✓ | `messages:send` | answers a message you received (to its real sender); not a Decision's ask |
|
|
@@ -524,6 +537,15 @@ whether the hub sent it to wake the recipient, and when not, `wakeSkipped` says
|
|
|
524
537
|
`hop_cap`, `budget`; see **Limits**). The CLI and MCP tools print it, e.g. `queued (read at its next turn) — wake
|
|
525
538
|
skipped: hop limit reached`. Hubs before 0.8.1 send neither field.
|
|
526
539
|
|
|
540
|
+
`msg` and `ask` (also to `@me`, also with `all`) accept an `Idempotency-Key` header (1–200 of `A-Z a-z 0-9 _ . : -`),
|
|
541
|
+
so a client that retries after a timeout never delivers, pushes or opens a Decision twice. Keys are per sender (the
|
|
542
|
+
user, each app token, each session). The same key and body within 24 h answer `200` with the original body
|
|
543
|
+
(`{message, delivery?, …}`, `{sent: [...]}`, or `{message, decision, …}`) and `idempotent-replayed: true`, sending
|
|
544
|
+
nothing; another body under the key is `409 conflict`, and so is a repeat while the first is still running. A
|
|
545
|
+
malformed key is `400 invalid`. The keys are kept in the hub's state store, so a replay works across restarts.
|
|
546
|
+
SDK: `hub.msg(to, text, {idempotencyKey})`, `hub.ask(…, {idempotencyKey})`; CLI: `hub msg|ask … --key K`; MCP:
|
|
547
|
+
the `idempotencyKey` argument of `msg` and `ask`.
|
|
548
|
+
|
|
527
549
|
The act verbs, grants, kill switches and the agent audit are in [Agent actions and grants](#agent-actions-and-grants).
|
|
528
550
|
|
|
529
551
|
**Decisions and `@me`.** `@me` addresses the user: `msg @me` is stored in the user's inbox (push kind `message`);
|
|
@@ -573,6 +595,14 @@ you. Addresses resolve among your own sessions only; another user's sessions do
|
|
|
573
595
|
`--no-wake` admits a message without starting a turn and `--urgent` steers it into a running turn. Texts are at
|
|
574
596
|
most 10,000 characters (a decline's reason 1,000).
|
|
575
597
|
|
|
598
|
+
**Not after.** `msg` and `ask` take `notAfter`: never deliver the message after that time. REST and the SDK take
|
|
599
|
+
epoch milliseconds; the CLI (`--not-after`) and `hub mcp` (`notAfter`) also take an ISO 8601 date-time such as
|
|
600
|
+
`2027-01-15T12:00:00Z`. It must lie in the future and at most 30 days ahead (else `invalid`, exit 2); one that has
|
|
601
|
+
already passed answers `422 expired` (exit 8) and nothing is stored, delivered or pushed. A message pending for an
|
|
602
|
+
offline or busy session expires at the earlier of its 24 h hold and `notAfter` (its sender is told as below, and the
|
|
603
|
+
`message.changed` event says why: `reason` `not_after` or `pending_timeout`); an ask expires at the earlier of its
|
|
604
|
+
`expiresIn` and `notAfter`. Messages to `@me` are stored at once, so for them `notAfter` is only checked when sent.
|
|
605
|
+
|
|
576
606
|
**Delivery.** A message reaches an OpenCode session as a hub message in its conversation (the spoke's `deliver`,
|
|
577
607
|
OpenCode's synthetic message; it shows in the console's conversation as a message row). An idle session gets a
|
|
578
608
|
turn; a busy one gets a plain `msg` after its running turn and an `ask` steered into it. A `--no-wake` message never
|
|
@@ -840,8 +870,8 @@ hub name <@name> [--session <addr>]
|
|
|
840
870
|
hub intent <addr> [text… | --clear] what the session is for (one line, ≤ 280 characters)
|
|
841
871
|
hub role claim <role[.project]> [--exclusive] [--session <addr>]
|
|
842
872
|
hub role release <role[.project]> [--session <addr>]
|
|
843
|
-
hub msg <addr> "text" [--all] [--no-wake] [--urgent] tell: fire and forget
|
|
844
|
-
hub ask <addr> "text" [--wait [secs]] [--expires 2h] [--all] [--no-wake] [--urgent]
|
|
873
|
+
hub msg <addr> "text" [--all] [--no-wake] [--urgent] [--key K] tell: fire and forget
|
|
874
|
+
hub ask <addr> "text" [--wait [secs]] [--expires 2h] [--all] [--no-wake] [--urgent] [--key K]
|
|
845
875
|
hub reply <msgID> "text" [--no-wake] to the message's real sender, never via the role
|
|
846
876
|
hub decline <msgID> --why "reason"
|
|
847
877
|
hub ask @me "question" [--option id=label[:safe]]... [--recommend id [--why "…"]] [--urgency low|normal|high]
|
|
@@ -888,7 +918,8 @@ message, any candidates or retry time, a missing feature, and the hint on stderr
|
|
|
888
918
|
the running turn, `queued` for its next turn, `retained` while it has a question open, `pending` while offline (or,
|
|
889
919
|
for a `--no-wake` message, while the recipient's turn runs), or
|
|
890
920
|
`stored` in a user's or app's inbox); `--all` sends to every session the address names (each holder of a role),
|
|
891
|
-
`--no-wake` admits without starting a turn, `--urgent` steers into a running turn
|
|
921
|
+
`--no-wake` admits without starting a turn, `--urgent` steers into a running turn, `--key K` sends an
|
|
922
|
+
`Idempotency-Key` (a retry with the same key and text answers the first send again and sends nothing). A text (or `--why`) of `-` is
|
|
892
923
|
read from stdin, so long or quoted bodies need no escaping: `git diff | hub msg @reviewer -`. `--expires` and
|
|
893
924
|
`--wait` take seconds or `30s`, `5m`, `2h`, `1d` (asks expire after 1 minute to 7 days, default 24 h). `ask --wait N`
|
|
894
925
|
waits up to N seconds for the answer (the CLI loops on the hub's 60 s long poll); a bare `--wait` waits until the
|
|
@@ -925,8 +956,10 @@ repository on the node and must be under that node's workspace roots. `--label k
|
|
|
925
956
|
(`hub launches list --label brain.task=abc` finds it again); `--node-label k=v` and `--machine` pick nodes, required
|
|
926
957
|
unless `--prefer`. `--prompt -` reads the first prompt from stdin. `--key` makes retries safe: the same key and
|
|
927
958
|
flags answer the existing launch (`… was already launched with key …`), the same key with other flags is invalid
|
|
928
|
-
(exit 2).
|
|
929
|
-
`
|
|
959
|
+
(exit 2). `--not-after <ms|ISO time>` sets a deadline: a launch still waiting for a machine then is never started
|
|
960
|
+
and ends `expired` (already passed: nothing is launched, exit 8). It prints the launch and its state (`queued`,
|
|
961
|
+
`placing`, `provisioning`, `running` with the session, `ended` with an outcome: `idle`, `needs_user`, `failed`,
|
|
962
|
+
`timeout`, `stopped`, `lost`, `expired`); `hub launches show` adds the
|
|
930
963
|
workspace, steps and artifacts (branch, head, commits). `hub launches stop` ends a launch (`stopped`); one that
|
|
931
964
|
already ended exits 8 (`gone`). The user may always launch; an app token needs scope `sessions:launch` **and** a
|
|
932
965
|
grant with verb `launch` covering the repo (exit 5 otherwise; the kill switch pauses it); sessions launch only
|
|
@@ -989,7 +1022,7 @@ is set and the credential is not that session. The CLI removes `BUN_BE_BUN` from
|
|
|
989
1022
|
| 5 | `forbidden`, `paused` |
|
|
990
1023
|
| 6 | `rate_limited` (with `retryAfter`) |
|
|
991
1024
|
| 7 | `unsupported`, `offline`, `busy` |
|
|
992
|
-
| 8 | `gone
|
|
1025
|
+
| 8 | `gone`, `expired` (a `--not-after` already passed); an `ask --wait` that expired |
|
|
993
1026
|
| 9 | `unauthorized`, `token_expired` (a session token: start a new shell), or not logged in |
|
|
994
1027
|
|
|
995
1028
|
### hub mcp (MCP server)
|
|
@@ -1015,7 +1048,8 @@ required and absolute; `mode`, `branch`, `base`, `remote`, `key`, `name`, `roles
|
|
|
1015
1048
|
(`to`, `text`, `addr`, `id`, `requestID`, `formID`, `why`, `n`, `wake`, `urgent`, `all`, `project`, `role`, …). The act
|
|
1016
1049
|
tools need an app token with scope `sessions:act` and a grant, the launch tools scope `sessions:launch` and a
|
|
1017
1050
|
launch grant; `needs_user` results include the evaluation. `ask` takes `wait` of at
|
|
1018
|
-
most 60 s (one long poll; a longer wait is cut to 60) and `expires` in seconds or as `2h`; `
|
|
1051
|
+
most 60 s (one long poll; a longer wait is cut to 60) and `expires` in seconds or as `2h`; `msg` and `ask` take
|
|
1052
|
+
`notAfter` as epoch milliseconds or an ISO 8601 date-time; `ask @me` reports the
|
|
1019
1053
|
Decision it opened. Answers are the CLI's
|
|
1020
1054
|
short text; messages and answers from other sessions come in the hub's envelope and are marked as not from the
|
|
1021
1055
|
user. Hub errors are tool results with `isError: true` and the hub's `code: message` (plus candidates and hint),
|
|
@@ -1256,6 +1290,8 @@ holds the startup (`bin/hub.ts` serves `web/` from disk; the compiled binaries e
|
|
|
1256
1290
|
| `HUB_VAPID_SUBJECT` | the https public URL, else `mailto:hub@localhost` | contact sent to push services |
|
|
1257
1291
|
| `HUB_RELEASE_BASE` | `https://github.com/huynle/hub-releases/releases/download/v<version>` | where `/install.sh` and `/install.ps1` download hubctl ([Set up a machine](#set-up-a-machine-one-line)): https, or plain http on loopback or a private-network IP address; the hub refuses to start with any other value. Without it a development version (not a plain `x.y.z`) answers 503 |
|
|
1258
1292
|
| `HUB_CSP` | `enforce` | `report-only` sends the console's Content-Security-Policy as `Content-Security-Policy-Report-Only` (violations are only logged in the browser console); see [Content-Security-Policy](#content-security-policy) |
|
|
1293
|
+
| `HUB_REMOTE_FETCH` | `on`; `off` when `HTTP_PROXY`/`HTTPS_PROXY` (either case) is set and `NO_PROXY` is not `*` | `on` or `off`. A proxy could re-resolve hostnames and undo the fetcher's address pinning, so with a proxy in the environment remote fetch stays off (with a startup warning) unless you set `NO_PROXY=*` for the hub process (direct fetches) or `HUB_REMOTE_FETCH=on` (accept proxying knowingly). `off` turns off the hub fetching web images and link previews for the console (`GET /api/media/remote` and `GET /api/media/unfurl` answer `403 disabled`). Signed-in users only (session and app tokens are refused); http/https on default ports, private and reserved addresses refused, 8 MiB, never decompressed (compressed responses are refused), PNG/JPEG/WebP/GIF only, 30 a minute and 4 at once per user (images and link previews together); the audit log keeps the host, never the URL. Any other value stops the hub at start. Threat model and residual risks: [docs/media-security.md](docs/media-security.md) |
|
|
1294
|
+
| `HUB_FETCH_DENY_CIDRS` | | comma list of extra IPv4 or IPv6 CIDRs the hub never fetches from (`GET /api/media/remote`, `GET /api/media/unfurl`), on top of the built-in loopback, private, link-local (cloud metadata), CGNAT, multicast, reserved, ULA and mapped ranges (e.g. your Kubernetes pod and service CIDRs: `10.42.0.0/16,10.43.0.0/16`); an invalid entry stops the hub at start |
|
|
1259
1295
|
| `OPENROUTER_API_KEY` | | default key for the `openrouter` speech provider (never sent to other providers) |
|
|
1260
1296
|
| `HUB_STT_MODEL`, `HUB_TTS_MODEL`, `HUB_VOICE`, `HUB_MAX_*` | see `src/central/config.ts` | speech defaults; the phone's speech settings override them |
|
|
1261
1297
|
|
|
@@ -1277,11 +1313,14 @@ Nothing inline and no `eval`: scripts and styles come only from the built bundle
|
|
|
1277
1313
|
that runs before the first paint is `/theme-boot.js` (not an inline script), components use classes or style
|
|
1278
1314
|
properties (never a `style="…"` attribute in markup), and the stylesheets 3d-force-graph would inject for the
|
|
1279
1315
|
Watchtower are stripped at build time and shipped as `web/src/app/watchtower/force-graph.css`. `data:`/`blob:`
|
|
1280
|
-
images are attachments
|
|
1316
|
+
images are attachments, pasted images and session media (screenshots, local images, artifact images and web images
|
|
1317
|
+
the hub fetched, as `blob:` URLs of checked raster bytes; SVG and Mermaid diagrams only as `data:` URLs in an
|
|
1318
|
+
`<img>`, never DOM; [docs/media-security.md](docs/media-security.md)), `blob:` media is Voice playback. Mermaid draws
|
|
1319
|
+
in a same-origin worker (`worker-src 'self'`). `connect-src 'self'` covers the API, the
|
|
1281
1320
|
SSE stream and same-origin WebSockets. The only frames are the isolated artifact host's (`/artifact-host/v1/…`, same
|
|
1282
1321
|
origin), which keeps its own sandboxing policy ([docs/artifact-host.md](docs/artifact-host.md)); nothing may frame the
|
|
1283
1322
|
console. Sign-in (OIDC) is a navigation, not a form post, so `form-action 'self'` does not affect it. The Silero VAD
|
|
1284
|
-
|
|
1323
|
+
and Smart Turn workers compile their WebAssembly inside the worker, so no `'wasm-unsafe-eval'` is needed.
|
|
1285
1324
|
|
|
1286
1325
|
API responses carry no console policy (JSON is never rendered as a page; downloads, the sign-in result pages and the
|
|
1287
1326
|
artifact host send their own). If a deployment hits a violation (the browser console says `Refused to …`), set
|
|
@@ -1770,6 +1809,24 @@ it again, say "pause listening", or leave the page to turn it off. Leaving the p
|
|
|
1770
1809
|
it ends in `. ? !`, or has 3+ words and doesn't end in "and", "but", "so", "um", "the" and the like.
|
|
1771
1810
|
Otherwise it waits up to 0.8 s. If you speak again during the wait, the next segment's transcript
|
|
1772
1811
|
is joined to it. Empty and filler-only transcripts are dropped. Utterances are capped at 60 s.
|
|
1812
|
+
- **Turn detection (experimental, Settings → Voice → Turn detection, per device)**: "Timers (current)" is the
|
|
1813
|
+
default and works as above. "Smart Turn (experimental)" decides the end of a turn from the audio: on-device
|
|
1814
|
+
[Pipecat Smart Turn v3](https://huggingface.co/pipecat-ai/smart-turn-v3) (v3.2 int8, BSD-2-Clause, 8.7 MB, loaded on
|
|
1815
|
+
first use as a hashed asset; license in `web/src/voice/turn/LICENSE`) in its own Web Worker
|
|
1816
|
+
(`web/src/voice/smart-turn-worker.ts`, onnxruntime-web as for Silero, so the console policy needs nothing new). The
|
|
1817
|
+
console keeps the last 8 s of the turn's 16 kHz mic audio, pauses included (from the turn's first speech start),
|
|
1818
|
+
computes Whisper's log-mel features in TS (`web/src/core/smart-turn-features.ts`, matched to transformers'
|
|
1819
|
+
`WhisperFeatureExtractor` within 1e-5) and scores it at each segment end: a score ≥ 0.5 sends 150 ms after the
|
|
1820
|
+
score once the transcripts are in; a lower one holds and is re-scored every 400 ms of quiet, with a fallback after
|
|
1821
|
+
3 s. Speech resuming cancels the send as with the timers; commands, the 60 s cap and noise transcripts are
|
|
1822
|
+
unchanged. A model that cannot load, or a score slower than the slow cap, leaves that turn to the timers (one
|
|
1823
|
+
console warning). The slow cap is 1.5 s, or twice the median of the recent score times when the device is slower,
|
|
1824
|
+
at most 5 s; the model runs once on silence right after it loads, so the first turn is not the slow one. The Voice bar shows the latest score ("turn 6%", "turn 91% → sending"), or why there is none ("turn: model
|
|
1825
|
+
loading", "turn: timers (slow)"). A turn buffer without speech (silence scores about 0.99 "complete") is never
|
|
1826
|
+
scored. Tuning per device: Settings →
|
|
1827
|
+
Voice → Advanced (threshold, grace, fallback, slow cap; clamped to their ranges, each with the value in use), or `voiceTurn.threshold`, `voiceTurn.grace`, `voiceTurn.recheck`,
|
|
1828
|
+
`voiceTurn.fallback`, `voiceTurn.slow` as URL query parameters (one visit) or localStorage keys. The recording
|
|
1829
|
+
mode records each score as `turn.predict` ([docs/voice-recording.md](docs/voice-recording.md)).
|
|
1773
1830
|
- **Commands**: an utterance that is only "stop", "cancel", "never mind", "stop that" or "hold on"
|
|
1774
1831
|
calls `POST …/interrupt` and stops speech. "Pause listening" turns the mode off.
|
|
1775
1832
|
- **Speech out**: root replies arrive one sentence at a time while they are written (`say` with `seq`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huynle/hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Hub: a harness-agnostic central hub your AI agent harnesses connect to (OpenCode first). Follow, steer and stop sessions from your phone or browser, with notifications, optional hands-free Voice and a self-hostable server.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Huy Le",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"src",
|
|
48
48
|
"packages/protocol/src",
|
|
49
49
|
"packages/protocol/package.json",
|
|
50
|
+
"packages/protocol/schema",
|
|
50
51
|
"packages/sdk/src",
|
|
51
52
|
"packages/sdk/package.json",
|
|
52
53
|
"packages/cli/src",
|
|
@@ -70,6 +71,7 @@
|
|
|
70
71
|
"packages/codex/package.json",
|
|
71
72
|
"bin",
|
|
72
73
|
"web/dist",
|
|
74
|
+
"web/src/voice/turn/LICENSE",
|
|
73
75
|
"docker",
|
|
74
76
|
"Dockerfile",
|
|
75
77
|
"scripts/hub-local.sh",
|
|
@@ -124,7 +126,9 @@
|
|
|
124
126
|
"@types/three": "0.186.0",
|
|
125
127
|
"@types/web-push": "^3.6.4",
|
|
126
128
|
"3d-force-graph": "1.80.1",
|
|
129
|
+
"ajv": "^8.20.0",
|
|
127
130
|
"onnxruntime-web": "1.23.2",
|
|
131
|
+
"pele": "1.13.0",
|
|
128
132
|
"playwright-core": "^1.64.1",
|
|
129
133
|
"solid-js": "^1.9.17",
|
|
130
134
|
"typescript": "^5.9.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huynle/hub-claude-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "The Claude Code hooks spoke: per-user daemon, hook bridge, plugin dir, `hub claude` wrapper and daemon-backed MCP server (internal; shipped inside @huynle/hub, not published on its own).",
|
|
6
6
|
"license": "MIT",
|