@staix/agent-console 0.0.0-stage → 0.1.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/CHANGELOG.md +49 -0
- package/LICENSE +21 -0
- package/README.md +354 -2
- package/bin/acon.js +8 -0
- package/package.json +38 -4
- package/shell/asset-viewer.css +5 -0
- package/shell/asset-viewer.html +11 -0
- package/shell/asset-viewer.js +23 -0
- package/shell/icon.svg +1 -0
- package/shell/index.html +53 -0
- package/shell/shell.css +534 -0
- package/shell/shell.js +2214 -0
- package/shell/space-view.js +164 -0
- package/shell/space.js +241 -0
- package/shell/space3d.js +364 -0
- package/shell/static.js +111 -0
- package/shell/vendor/BufferGeometryUtils.js +1523 -0
- package/shell/vendor/GLTFLoader.js +5006 -0
- package/shell/vendor/NOTICE +30 -0
- package/shell/vendor/SkeletonUtils.js +496 -0
- package/shell/vendor/manifest.json +42 -0
- package/shell/vendor/mermaid/NOTICE +25 -0
- package/shell/vendor/mermaid/manifest.json +19 -0
- package/shell/vendor/mermaid/mermaid.js +3580 -0
- package/shell/vendor/three.core.js +61162 -0
- package/shell/vendor/three.module.js +19537 -0
- package/shell/visual-frame.css +5 -0
- package/shell/visual-frame.html +14 -0
- package/shell/visual-frame.js +54 -0
- package/src/assets.ts +200 -0
- package/src/canonical.ts +16 -0
- package/src/cli.ts +246 -0
- package/src/conformance.ts +299 -0
- package/src/contract.ts +276 -0
- package/src/install.ts +248 -0
- package/src/pets.ts +63 -0
- package/src/plugins.ts +357 -0
- package/src/serve.ts +184 -0
- package/src/server.ts +462 -0
- package/src/setup.ts +74 -0
- package/src/space.ts +106 -0
- package/src/static.ts +102 -0
- package/test/fixtures/review/cases.json +726 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.1.1] - 2026-11-04
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- H2 static shell (#46): `acon build --static --nodes id=https://origin[,id=origin] [--out dist]`, fixed credential-free assets and Vercel/Workers static config, per-node bearer/Access auth, namespaced tools, node-owned actions and allowlisted credentialed CORS. Node-specific expiring GET-only grants serve authenticated modules/assets without exposing bearer tokens in URLs. Two-node ui-check covers ownership and distinct offline/origin-refused states. Hosted preview readback remains unverified.
|
|
10
|
+
- H1 follow-ups (#45): `acon serve --read-only` / `ACON_READ_ONLY=1` refuses every plugin action (403 `read-only node`, no plugin call, no confirm token, a replayed token is not honoured), reports `readOnly: true` in `/tools`, and the shell shows a Read-only chip with verb, answer and confirm buttons disabled. `acon token new [--file] [--force]` writes a 0600 base64url bearer token and prints only its path. `acon setup launchd [--label] [--apply]` prints (or installs) a LaunchAgent for `acon serve` with auth passed as env names and file paths. The Dockerfile carries OCI `source` and `revision` labels.
|
|
11
|
+
- Release images (#45): behind `IMAGE_PUBLISH_ENABLED`, tagged releases push multi-arch (`linux/amd64`, `linux/arm64`) images to `ghcr.io/staixbwlb/agent-console:<version>` (and `latest` for stable versions) with OCI labels, read the manifest back and run `--version` on amd64.
|
|
12
|
+
|
|
13
|
+
## [0.1.0] - 2026-11-04
|
|
14
|
+
|
|
15
|
+
### Compatibility
|
|
16
|
+
|
|
17
|
+
Initial 0.1.x minor line: N-1 is not applicable because no previous minor has been published.
|
|
18
|
+
|
|
19
|
+
- Plugin API (#27): a plugin that checks `ctx.features` sends no forms or `result` items to a console before this release, which would report them as malformed; plugins that ignore `meta` are unaffected.
|
|
20
|
+
|
|
21
|
+
- Plugin API, routes and manifest files (#26): plugins registered for api 1 with a single `<tool>.json` and a plain `"api": 1` load unchanged; `/p/<tool>/...` routes keep working beside the new `/p/<tool>@<hash>/...` refs. Consoles before this release refuse `apiMin`/`apiMax` keys and ignore `<tool>@<version>.json` files (they fail the tool-id check), so a tool that needs both should keep writing `<tool>.json`.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Plugin discovery scans files that start with a `#!` line (CLI entry files) instead of marking the plugin "cannot be read"; a file that cannot be scanned is now named in the reason. The stripped line ends at any JavaScript line terminator (LF, CR, U+2028, U+2029), so an import after a CR-terminated `#!` is still checked.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- `host.ui.button(label, onClick, {tone?, disabled?})` (api 1, additive): a `button.btn` with `type="button"` and the `primary` or `danger` modifier. `host.ui.notice` with tone `err` now uses `role="alert"` so errors are announced assertively; other tones keep `role="status"` (#43).
|
|
30
|
+
- `host.ui.visual` (api 1, additive): Mermaid diagrams in a `sandbox="allow-scripts"` frame with its own CSP (no `connect-src`), lazy and reused per panel, falling back to the source text on error, over 20 KB or after 5 s; comparison cards; tables through `ui.table`; before/after with text or asset sides. Mermaid is vendored from `@mermaid-js/tiny` 12.2.0 (2.8 MB) with a hash manifest and `scripts/vendor-mermaid.ts --check` (#29).
|
|
31
|
+
- `host.ui.review` (api 1, additive): text, prose and diff review with range selection by pointer or keyboard, anchors with `lineRange`, `textQuote` and (diffs) `diffHunk`/`side`, placement by hash then unique quote, an unplaced strip and chunked rendering up to 20,000 lines. Contract types `ReviewSelector`, `ReviewAnchor`, `ReviewThread`, `ReviewDoc`; conformance `reviewProblems`; shared cases in `test/fixtures/review/` ship with the package (#28).
|
|
32
|
+
- Inbox choice answers and results (#27): `ctx.features` (`inbox.form`, `inbox.result`, `action.meta`); verb `form` with 1-4 choice questions, an "Other" text, a review step and drafts kept across a refused answer; a `result` group with `seen` receipts after the body end was in view for 1 s in a focused window, and "Mark all read"; `action(input, meta)` with the verified identity; `verbFormProblems` and feature checks in conformance.
|
|
33
|
+
|
|
34
|
+
- Staged plugin upgrades (#26): `apiMin`/`apiMax` ranges, versioned registrations `plugins/<tool>@<version>.json` (newest loadable wins), a re-scan at most every 5 s that serves a new registration to pages opened after it while open pages keep their load until restart, and `scripts/install-release.ts` for immutable `console/releases/<version>-<commit>/` installs with `current` and `previous` links.
|
|
35
|
+
- Plugin assets (api 1, additive): optional `provider.asset({id})`, served at a session-gated `GET /p/<tool>/asset?id=` with a type allowlist (SVG as attachment only), nosniff, a 32 MB default cap and same-origin framing; shell CSP gains `frame-src 'self'`; `host.ui.img`, `host.ui.frame` and `host.ui.link` accept only this plugin's asset ids or panels; conformance checks `asset` answers (#24).
|
|
36
|
+
- `host.ui.table`, `host.ui.bars` and `host.ui.spark` (api 1, additive): a sortable text table with an exported `TableColumn` type, horizontal bars with target markers and a small line, built as shell SVG with token colors, text equivalents and bounded input (500 rows, 12 columns, 50 bars, 365 points); `tableColumnProblems` in conformance (#23).
|
|
37
|
+
- `acon serve` (H1): resident console with `--host`, `--port` and `--origin`, authenticated by Cloudflare Access JWT (RS256 against the team JWKS) or a bearer token file. It refuses a non-loopback bind without auth, binds confirm tokens to the verified identity, answers 204 on `/healthz` and has no idle exit. A Dockerfile runs it as non-root with a `/data` volume and a health check (#20).
|
|
38
|
+
- `acon install <name|https URL|dir>`, `--list` and `--force`: pet and asset packages downloaded separately, verified against the sha256 index at assets.entelecheia.ai (`ACON_ASSETS_INDEX` overrides), https only, streaming size caps, staged and installed with one rename (#17).
|
|
39
|
+
- Space Flat/Room/3D views: CSS room camera, opt-in on-demand vendored Three.js, projected keyboard controls, bounded embedded GLB models and Room fallbacks (#14).
|
|
40
|
+
- Optional Space area: public plugin scenes, keyed actors/cards and a text roster, fact-driven motion, separately installed image packs and a CSS fallback; pack/pet image validation is shared and asset reads require a session (#13).
|
|
41
|
+
- Pet and Mini, moved from agent-hub: Codex v2 pet packages from local folders drawn as CSS sprites whose pose follows the inbox, a compact `#/local/mini` view with one-step answers, and a pop-out through Picture-in-Picture or a popup; `GET /pets/<id>/sheet` behind the session (#12).
|
|
42
|
+
- Repeatable UI checks across phone, tablet and desktop widths, both themes and 200% text; rem type tokens, discoverable scrolling tabs and inline status chips on phones (#7).
|
|
43
|
+
- `host.ui` panel kit with text-only elements, forms, keyboard-navigable rows and a shared detail template; documented public CSS classes and tokens (#5).
|
|
44
|
+
- `acon ui` (H0): loopback console with a one-time link, session cookie, strict CSP and idle exit after 5 minutes.
|
|
45
|
+
- Plugin discovery from `$AGENTHUB_HOME/console/plugins/<tool>.json` with module hash and permission checks; `acon plugins` lists the result.
|
|
46
|
+
- Console plugin contract api 1 (`@staix/agent-console/contract`) with inbox, search, structured previews and undo, plus a conformance suite (`@staix/agent-console/conformance`).
|
|
47
|
+
- Verb reasons (`reason: "optional" | "required"`) asked inline and sent as `reason`; plugin palette commands (`manifest.commands`) that run through `action()`; a conformance check that inbox `at` stays stable; `onSnapshot` skips unchanged polls (#6).
|
|
48
|
+
- Confirm tokens for destructive actions: single use, 60 s, bound to session, tool and input.
|
|
49
|
+
- Shell: unified inbox with keyboard triage, peek sheet, command palette, confirm dialog, undo toast, System area, light and dark themes, tablet and phone layouts.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 STAIx Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,355 @@
|
|
|
1
|
-
#
|
|
1
|
+
# agent-console
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`acon` is a local web console for the agent tool family: agent-hub (agents and tasks), agent-mem (memory) and vault-mem (knowledge vault). Each tool contributes panels as a plugin. The console adds what no single tool has: one inbox of everything waiting for you, a command palette across tools, and one confirm dialog for every destructive action.
|
|
4
|
+
|
|
5
|
+
Status: H0 (`acon ui`, local mode) and H1 (`acon serve`, an authenticated resident node). vault-mem and agent-hub register their plugins; a fixture plugin in `test/fixtures/` shows the contract. H2 (`acon build --static`) serves a credential-free shell over independently authenticated H1 nodes.
|
|
6
|
+
|
|
7
|
+
## Use
|
|
8
|
+
|
|
9
|
+
Requires Bun 1.4.2 or later. Install the published package, or run `bun install` and `bun bin/acon.js` from a checkout:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
bun add -g @staix/agent-console
|
|
13
|
+
acon ui # opens the browser with a one-time link
|
|
14
|
+
acon ui --no-open --json
|
|
15
|
+
acon plugins # registered plugins and their hash check
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The server listens on 127.0.0.1 only and exits 5 minutes after the last request. The link works once and expires after 60 seconds.
|
|
19
|
+
|
|
20
|
+
Keys: `Cmd/Ctrl K` palette, `j`/`k` move, `Space` peek, `Enter` open, `1` primary answer, `2` second answer, `h` snooze for an hour, `?` all keys.
|
|
21
|
+
|
|
22
|
+
### Pet and Mini
|
|
23
|
+
|
|
24
|
+
The pet is an optional character in the top bar that shows what the inbox needs from you. acon ships no character art. It reads Codex v2 pet packages (`pet.json` plus an 8x11 `spritesheet.webp` or `.png` of 192x208 cells, the format the Codex `hatch-pet` skill makes) from these folders, in this order; the first package with an id wins:
|
|
25
|
+
|
|
26
|
+
1. `$AGENTHUB_HOME/console/pets/<id>/`
|
|
27
|
+
2. `$CODEX_HOME/pets/<id>/`
|
|
28
|
+
3. `~/.codex/pets/<id>/`
|
|
29
|
+
|
|
30
|
+
A package is skipped, and listed under System > Errors, when its id is not `[a-z0-9][a-z0-9-]{0,31}`, `spriteVersionNumber` is not 2, the sheet name has a path in it, the sheet is a link, larger than 8 MB or not a WebP or PNG.
|
|
31
|
+
|
|
32
|
+
The pose follows the inbox only, never a timer:
|
|
33
|
+
|
|
34
|
+
| Pose (sheet row) | When |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| waiting (6) | a decision is waiting |
|
|
37
|
+
| failed (5) | an alert is visible, or a tool's inbox could not refresh |
|
|
38
|
+
| review (8) | a review or candidate is waiting |
|
|
39
|
+
| idle (0) | nothing; a still frame |
|
|
40
|
+
|
|
41
|
+
Clicking the pet opens the oldest item that needs you. System > Appearance picks the pet or turns it off, per browser.
|
|
42
|
+
|
|
43
|
+
Mini (`#/local/mini`) is the compact view: the pet, the count and the five oldest items that need you. An item's primary answer runs in place when it is one step (not destructive and no reason); anything else uses Open, which shows the item in the console. The pop-out button (top bar, or Pop out in Mini) opens Mini in an always-on-top Picture-in-Picture window in Chromium browsers, and in a small popup window elsewhere. The console keeps polling while the Picture-in-Picture window is open, even when its tab is hidden.
|
|
44
|
+
|
|
45
|
+
### Space
|
|
46
|
+
|
|
47
|
+
Space is an optional area in the rail, shown when a ready plugin declares `capabilities.space: true`. It is never the landing view. The shell polls `snapshot({panel: "space", after})` at the tool's panel interval and combines public scenes under `tool:id` keys.
|
|
48
|
+
|
|
49
|
+
The area uses `#/local/space`; if a plugin already owns the tool id `space`, the area's link uses `#/local/~space` so that plugin's existing URLs keep working.
|
|
50
|
+
|
|
51
|
+
The view control offers **Flat**, **Room** and, when WebGL2 is available, **3D**. The browser saves the choice as `acon.space.view`. Flat keeps the normal layout. Room uses CSS 3D floor and wall planes with upright actor/card buttons, without downloading Three.js. Rotate left/right changes yaw by 90 degrees; zoom has three steps. Focus the scene and use Left/Right or +/- for the same controls. Reduced motion removes camera transitions.
|
|
52
|
+
|
|
53
|
+
3D is opt-in and loads the vendored renderer only when selected. The default camera frames the scene's content, turning its long axis down the screen on a portrait canvas; pack models are fitted by width with a height cap so desks, boards and shelves read as furniture. Room frames every billboard inside its scroll box. Drag empty canvas space to orbit, or use the same camera controls. Actor and work-item buttons remain in the same keyboard order and follow their projected screen positions; focus or hover reveals their labels. The text roster remains available and leads on phones. Missing WebGL2, renderer startup failure or context loss returns to Room with a notice. Canvas draws happen on changed public facts, asset completion and camera input, with a sprite burst bounded to 0.8 seconds; settled scenes schedule no frame or animation timer. Reduced motion draws the final pose immediately.
|
|
54
|
+
|
|
55
|
+
The exported `SpaceScene` contract contains:
|
|
56
|
+
|
|
57
|
+
- `actors`: `id`, `name`, `state` (`idle`, `working`, `waiting`, `blocked`, `away`), optional `pet`, `card` and `marks`, plus a panel `link`.
|
|
58
|
+
- `marks`: `kind` (`approval`, `inbox`, `paused`, `mode`) and public `text`.
|
|
59
|
+
- `zones`: `id`, `title`, optional `kind` (`queue`, `work`, `review`, `done`) and a non-negative `count`.
|
|
60
|
+
- `cards`: `id`, public `title`, `zone`, optional `owner` and `tone`, plus a panel `link`.
|
|
61
|
+
- Optional `links`: `kind` (`message`, `overlap`, `conflict`, `cohort`), actor ids `from` and `to`, and optional public `label` and ISO `at`.
|
|
62
|
+
- Panel links use `{panel, params?}`. Routing parameters must contain public identifiers, never bodies, private paths or task details.
|
|
63
|
+
|
|
64
|
+
Strings are limited to 200 characters; at most 32 actors, 200 cards and 64 links are rendered across tools. The server copies only allowed fields and reports dropped records; the shell also caps the combined scene. Conformance rejects extra fields, oversized strings and excess records with named problems. All state labels include text and a glyph. Clicking an actor or card navigates to its panel; Space performs no action. A text roster lists the same actors and zone counts and leads on phones, with the visual scene inside its own scroll box.
|
|
65
|
+
|
|
66
|
+
Office packs live at `$AGENTHUB_HOME/console/assets/<id>/`. A `pack.json` is at most 16 KB and has this form:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{"id":"office","name":"My office","version":1,"tile":[64,32],"images":{"floor":"floor.webp","wall":"wall.webp","desk":"desk.webp","shelf":"shelf.webp","board":"board.webp"}}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Pack ids and logical image names match `[a-z0-9][a-z0-9-]{0,31}`. Each image is a leaf `.webp` or `.png` filename, a regular file with matching image magic, at most 4 MB; package, manifest and image symlinks are refused. Bad packs are skipped and reported in System without blocking other packs. Images are served at `GET /assets/<pack>/<logical-name>` with a live session and revalidated, bounded bytes. An optional `models` object maps the same logical furniture names to leaf `.glb` filenames, for example `"models": {"desk":"desk.glb","shelf":"shelf.glb","board":"board.glb"}`. Each model is a regular, non-symlink file capped at 4 MB, with a GLB 2 `glTF` header, valid chunk lengths and embedded buffers/images only; resource URLs are refused. The authenticated `GET /models/<pack>/<logical-name>` revalidates the bytes on every response. 3D loads models when present and uses boxes when absent or unavailable; Room uses images or CSS furniture. Validators are exported from `src/assets.ts` for the separate installer. No artwork is bundled: tests use generated colour pixels. With no pack the scene uses CSS tiles and furniture. Actors use their named pet, a stable assignment from installed pets, or a CSS token.
|
|
73
|
+
|
|
74
|
+
Motion follows reported facts: working actors use pet row 7, waiting row 6, blocked row 5; idle and away stay still. Card moves transition for at most 0.8 s and new message links fade over 10 s. The first snapshot, a gap over 3 s or more than six changed records settles without transition or replay. Unchanged snapshots write nothing to the scene DOM. Reduced motion removes animation, and stale tool data is labelled and drawn still. Flat and Room have no JavaScript scene frame loop; optional 3D draws only on demand. Providers must supply public titles and identifiers; Space never reads message bodies or invents task attribution.
|
|
75
|
+
|
|
76
|
+
### Install packages
|
|
77
|
+
|
|
78
|
+
acon ships no art. Pets and office packs are installed separately:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
acon install --list # what the index offers
|
|
82
|
+
acon install gyul # a package from the index; every file's sha256 is checked
|
|
83
|
+
acon install https://host/x/pet.json # any https host (pet.json or pack.json); not hash-verified
|
|
84
|
+
acon install ./my-pet # a local folder (written as a path; a bare name always means the index) with a pet.json or pack.json; not hash-verified
|
|
85
|
+
acon install gyul --force # replace an installed package with the same id
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Pets go to `$AGENTHUB_HOME/console/pets/<id>/` and packs to `$AGENTHUB_HOME/console/assets/<id>/` (folders 0700, files 0600). To remove a package, delete its folder. The console scans these folders at start, so run `acon ui` again to see a new package.
|
|
89
|
+
|
|
90
|
+
The index is `https://assets.entelecheia.ai/console/index.json`; `ACON_ASSETS_INDEX` overrides it. Its form:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{ "version": 1,
|
|
94
|
+
"packages": [
|
|
95
|
+
{ "kind": "pet", "id": "gyul", "name": "Gyul", "description": "...", "manifest": "../pets/gyul/pet.json",
|
|
96
|
+
"size": 210494, "files": { "pet.json": "<sha256>", "spritesheet.webp": "<sha256>" } },
|
|
97
|
+
{ "kind": "pack", "id": "office", "name": "Office", "manifest": "packs/office/pack.json",
|
|
98
|
+
"size": 3967552, "files": { "pack.json": "<sha256>", "floor.webp": "<sha256>", "desk.glb": "<sha256>" } }
|
|
99
|
+
] }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`manifest` resolves against the index URL and every file is a plain name in the manifest's folder. A pack may list `models` (logical name to `.glb` file) next to `images`.
|
|
103
|
+
|
|
104
|
+
Rules:
|
|
105
|
+
|
|
106
|
+
- Only `https:`; `http:` only on loopback hosts. Redirects are followed only to `https:`.
|
|
107
|
+
- Each file has a 30 s timeout and a cap enforced while streaming: manifest 16 KB, pet sheet 8 MB, pack image 4 MB, model 4 MB (glTF binary magic), 32 MB per package.
|
|
108
|
+
- Files are written to a staging folder under `$AGENTHUB_HOME/console/.install/`, checked with the same validation the console runs at start, then moved into place with one rename. A failure leaves nothing behind. An installed id is refused before any download unless `--force` is given, and `--force` replaces the old folder only after the new one is complete.
|
|
109
|
+
- Nothing is executed.
|
|
110
|
+
|
|
111
|
+
## Run as a server
|
|
112
|
+
|
|
113
|
+
`acon serve` (H1) is the resident form of the console for a node behind a reverse tunnel or on an internal server. `acon ui` is unchanged.
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
acon serve [--host 127.0.0.1] [--port 8787] [--origin https://console.example.com]... [--read-only] [--json]
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- No ticket link, no idle exit and no browser opening. The process stays up until it is stopped. `--port` defaults to `ACON_PORT`, or 8787 when that is unset.
|
|
120
|
+
- `--origin` (repeatable, or comma-separated in `ACON_ORIGINS`) lists the exact browser origins. POST needs one of them as `Origin`. `Host` must be one of their hosts or the bind address, which local health checks use.
|
|
121
|
+
- `GET /healthz` answers 204 with no body and no authentication. Every other route, the shell files included, needs authentication.
|
|
122
|
+
- Authentication is required unless `--host` is a loopback address. Starting on another address without it is refused. There are two modes:
|
|
123
|
+
- Cloudflare Access: set `ACON_ACCESS_TEAM` (the team domain, `<team>.cloudflareaccess.com`) and `ACON_ACCESS_AUD` (the Access application's AUD tag). Each request's `Cf-Access-Jwt-Assertion` must verify as RS256 against `https://<team>/cdn-cgi/access/certs`. The keys are cached and refetched on an unknown `kid`. The checks are `aud` contains the AUD, `iss` is `https://<team>`, `exp`/`nbf` hold with 60 s skew, and `sub` (or `email`) is present.
|
|
124
|
+
- Bearer token: set `ACON_TOKEN_FILE` to a file with mode 0600 holding one random token of at least 32 characters. `acon token new [--file PATH] [--force]` writes one (32 random bytes, base64url) to `$AGENTHUB_HOME/console/token` by default. It creates the file exclusively with mode 0600, refuses an existing file unless `--force` is given, and prints only the path, never the token. Requests send `Authorization: Bearer <token>`. The comparison is constant-time. A file readable by group or others is refused.
|
|
125
|
+
- The verified identity (the Access `sub`, or `bearer`) takes the place of the ticket session. Confirm tokens for destructive actions are bound to it, so a preview confirmed by another identity is refused. The page skips the ticket step because `/session` answers `{ok: true, mode: "serve"}`.
|
|
126
|
+
- CSP, headers, plugins, the confirm path and the 32 KB body limit are the same as under `acon ui`.
|
|
127
|
+
- Read-only node: `--read-only` or `ACON_READ_ONLY=1`. Every `/p/<tool>/action` answers 403 `{ok: false, error: "read-only node"}` before any confirm-token lookup, without calling the plugin and without issuing a confirm token. `/tools` reports `readOnly: true`, and the shell shows a "Read-only" chip and disables verb, answer and confirm buttons. Snapshots, search and assets are unchanged. Authentication rules do not change.
|
|
128
|
+
|
|
129
|
+
### LaunchAgent (macOS)
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
acon setup launchd [--label acon.serve] [--apply] [--host H] [--port P] [--origin https://host]... [--read-only]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Prints a LaunchAgent plist that runs the absolute `bun` and this checkout's `bin/acon.js serve` with the given flags, with `RunAtLoad`, `KeepAlive` and logs in `$AGENTHUB_HOME/console/logs/<label>.{out,err}.log`. It applies the same refusals as `acon serve` first. Auth is copied as environment names that point to files or settings (`ACON_TOKEN_FILE` as an absolute path, `ACON_ACCESS_TEAM`, `ACON_ACCESS_AUD`, `ACON_ORIGINS`, `ACON_PORT`, `ACON_READ_ONLY`), never a token value. Without `--apply` nothing is written. `--apply` writes `~/Library/LaunchAgents/<label>.plist`, runs `launchctl bootout` then `bootstrap` in `gui/<uid>`, and prints `launchctl print`. A tunnel agent is not part of this.
|
|
136
|
+
|
|
137
|
+
### Versioned install
|
|
138
|
+
|
|
139
|
+
From a checkout, `bun scripts/install-release.ts [--home DIR] [--json]` copies the committed package files (not uncommitted changes) into `$AGENTHUB_HOME/console/releases/<version>-<commit>/`, then points `console/current` at it by atomic rename and keeps `console/previous`. An existing release is reused, never changed. Start the resident console from `console/current/bin/acon.js`: an install leaves a running process on the release it started from, and a restart switches it. Roll back by pointing `current` at `previous` and restarting.
|
|
140
|
+
|
|
141
|
+
### Static multi-node shell (H2)
|
|
142
|
+
|
|
143
|
+
Build into a new output directory (an existing destination is refused):
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
acon build --static --nodes desk=https://desk.example.com,dgx=https://dgx.example.com --out dist
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Each URL must be an exact HTTPS origin, without credentials, paths, queries or fragments. HTTP is allowed only for loopback development. Configure each H1 node with both its public origin and the shell origin, for example `acon serve --origin https://desk.example.com --origin https://console.example.com`, and its own bearer token file or Cloudflare Access application. The node origin also registers its public Host value; the shell origin permits CORS. Keep authentication enabled off loopback.
|
|
150
|
+
|
|
151
|
+
Build one static shell only over equally trusted nodes (such as your own nodes). Each node's panel modules execute with the shell's authority; a compromised node or plugin can use every node's credentials in that tab while it is open. Use separate builds for nodes with different trust.
|
|
152
|
+
|
|
153
|
+
The output contains fixed shell/vendor files, public `config.json`, `_headers`, `vercel.json`, an assets-only `wrangler.jsonc` and `.assetsignore`. It contains no plugin data or credentials. Upload the complete directory to a preview with the supplied header configuration. Main-document CSP permits connections, scripts and images only from self and the configured node origins; plugin styles are fetched with node auth into constructed stylesheets, and attachments use a local sandboxed viewer. The sandboxed visual/attachment documents have separate header policies.
|
|
154
|
+
|
|
155
|
+
In System, each node has its own bearer-token form and Access sign-in/reconnect controls. Bearer tokens stay in that tab's `sessionStorage`; Access uses that node's cookies with credentialed requests. Access CORS/cookie policy must also permit the shell origin. Authenticated nodes issue short-lived GET-only resource grants for panel modules and attachments because native module/image requests cannot carry bearer headers. These runtime grants are node-specific, contain no bearer token and never authorize actions. Keep node access logs private, since runtime resource URLs contain these grants. The "Open attachment" link also includes a GET-only grant and can leave it in browser history for its remaining lifetime (at most 1 hour); keep that history private too.
|
|
156
|
+
|
|
157
|
+
Tools are namespaced `node:tool`; panel `host.node` identifies the owner and `host.tool` keeps its original tool id. All actions, confirmations and read-only restrictions stay on that owner. System distinguishes online, offline, unauthorized and `refused (origin)` nodes. A bounded opaque health probe distinguishes an unreachable node from a reachable node blocked by CORS; persistent failures refresh at most once per tool-list interval, without repeated prompts. Use System to reconnect after fixing a URL, token or `--origin`.
|
|
158
|
+
|
|
159
|
+
Vercel and Workers preview URL/CSP readback is **unverified** until the person supplies logins. No production deployment or Access application configuration is performed by the build command.
|
|
160
|
+
|
|
161
|
+
### Docker
|
|
162
|
+
|
|
163
|
+
When the repository variable `IMAGE_PUBLISH_ENABLED` is `true`, a tagged release (after npm publish) pushes `ghcr.io/staixbwlb/agent-console:<version>`, plus `latest` for stable versions, for `linux/amd64` and `linux/arm64`, with OCI `source`, `revision` and `version` labels. The release job reads the manifest back for both architectures, checks the revision label and runs `--version` on amd64.
|
|
164
|
+
|
|
165
|
+
The `Dockerfile` builds a pinned `oven/bun` image that runs `acon serve --host 0.0.0.0` as the non-root `bun` user. `AGENTHUB_HOME=/data` is a volume holding plugins, pets and asset packs, and a `HEALTHCHECK` probes `/healthz`. The image holds no credentials:
|
|
166
|
+
|
|
167
|
+
```sh
|
|
168
|
+
docker build -t acon .
|
|
169
|
+
docker run -d --name acon -p 127.0.0.1:8787:8787 -v acon-data:/data \
|
|
170
|
+
-e ACON_ORIGINS=https://console.example.com \
|
|
171
|
+
-e ACON_ACCESS_TEAM=<team>.cloudflareaccess.com -e ACON_ACCESS_AUD=<aud-tag> acon
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
For bearer mode, mount the token file read-only instead (for example `-v /path/token:/run/acon-token:ro -e ACON_TOKEN_FILE=/run/acon-token`). The file must be owned by the container user with mode 0600. Install packages into the volume with `docker exec acon bun bin/acon.js install <name>`, then restart the container.
|
|
175
|
+
|
|
176
|
+
The app files are world-readable whatever the build checkout's umask, so `--user <uid>:<gid>` works too. Use it with a host directory owned by that uid as `/data` (`-v /srv/acon:/data`), and no chown is needed. To change the port, set `ACON_PORT` rather than passing `--port`: serve and the `HEALTHCHECK` both read it.
|
|
177
|
+
|
|
178
|
+
### Cloudflare Tunnel and Access (outline)
|
|
179
|
+
|
|
180
|
+
1. Run the container with its port published on loopback only, as above.
|
|
181
|
+
2. Create a Cloudflare Tunnel on the same host whose public hostname points at `http://127.0.0.1:8787`.
|
|
182
|
+
3. Create an Access self-hosted application for that hostname with a policy for the people who may use it. Copy its AUD tag.
|
|
183
|
+
4. Start the container with `ACON_ORIGINS=https://<public hostname>`, `ACON_ACCESS_TEAM=<team>.cloudflareaccess.com` and `ACON_ACCESS_AUD=<aud-tag>`.
|
|
184
|
+
|
|
185
|
+
Keep the hostname, team and AUD in your deployment's private configuration, not in this repository.
|
|
186
|
+
|
|
187
|
+
## Writing a plugin
|
|
188
|
+
|
|
189
|
+
A tool registers by writing `$AGENTHUB_HOME/console/plugins/<tool>.json` (default home `~/.agenthub`; directory mode 0700):
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{ "api": 1, "tool": "vmem", "module": "/abs/path/to/src/ui/plugin.ts", "sha256": "<sha256 of that file>" }
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The module's default export is a `ConsolePlugin` (`@staix/agent-console/contract`): a manifest with panels and capabilities, and `open()` returning a provider with `assets.js` (an ES module), optional `assets.css`, `snapshot()` and `action()`. The console refuses a module whose hash changed until the tool's setup runs again. The hash covers the entry file only; files it imports are not hashed, but the import scan refuses an import outside the plugin's package.
|
|
196
|
+
|
|
197
|
+
Versions and upgrades (#26):
|
|
198
|
+
- API range: `"apiMin"` and `"apiMax"` (in the registration file and in the manifest) declare the console apis a plugin works with; `"api"` alone means that one api. This console speaks api 1. A plugin whose range does not overlap is listed as invalid with the ranges named.
|
|
199
|
+
- Versioned registrations: a tool may write `plugins/<tool>@<version>.json` (semantic version) beside `plugins/<tool>.json`. Per tool the newest one that verifies and loads is used; if it fails to load, the next one is tried; the others are listed as `superseded`.
|
|
200
|
+
- No restart needed for a new path: the console re-scans the directory at most every 5 s while a page is open. A module at a path not loaded before (a versioned install such as `releases/<version>/`) serves pages opened after it; an open page keeps the load it started with (System says when a newer one is registered), and every load stays in memory until the console restarts. A module from a package the console already imported from (changed in place, or a file first loaded as a dependency) is not hot-loaded, because the runtime keeps one module per path: the current load keeps serving and System says to restart. If a registration turns stale or disappears, the current load keeps serving and System records the problem once.
|
|
201
|
+
|
|
202
|
+
- Inbox: declare `capabilities.inbox` and answer `snapshot({panel: "inbox"})` with `{items: InboxItem[]}`. Verbs run through `action({action: verb.id, id})`. Keep each item's `at` the same across snapshots: snooze and oldest-first order key on it, and conformance checks it.
|
|
203
|
+
- Reasons: a verb with `reason: "optional"` or `"required"` makes the console ask for a reason inline and send it as `reason` in the action input; a required reason cannot be sent empty.
|
|
204
|
+
- Features (#27): `open(ctx)` receives `ctx.features` from this console: `"inbox.form"`, `"inbox.result"` and `"action.meta"`. Send forms and result items only when the matching feature is present; an older console has no `features` and reports both as malformed.
|
|
205
|
+
- Choice questions: a verb may carry `form: {questions}` (1-4 questions, 2-8 options each, optional `multiple` and `other: {label?, maxLength?}`; bounds in `VerbForm` and `verbFormProblems` from the conformance module). The peek shows the questions, then a review step when there are several, and sends `action({action: verb.id, id, answers})` with `answers: {[questionId]: {selected: string[], other?: string}}`. The console stores no answer: the tool accepts or refuses it, and on `{ok: false, error}` the console shows the error, refreshes and keeps the person's draft. Keys: `1` focuses the form (or submits from its button), `2` runs the second verb, `h` snoozes; inside the form arrows move, Space toggles and Enter goes on.
|
|
206
|
+
- Results: items in the `result` group are finished work to read, shown after reviews and counted apart ("2 results to read"); they do not count for the tab badge or the pet. With `seen: {action}`, the console sends `action({action, id, seenAt})` once per page session when the item's body end has been in view for 1 s in a visible, focused window (the peek), and `explicit: true` from the group's "Mark all read". A row in the list never counts as seen.
|
|
207
|
+
- Who acted: with `"action.meta"`, `action(input, meta)` gets `meta: {identity, node, via: "console"}` built from the verified session (`"local"` under `acon ui`; `"access:<subject>"`, `"bearer"` or `"loopback"` under `acon serve`). The page cannot set it; an input key named `meta` stays page data.
|
|
208
|
+
- Commands: `manifest.commands` (`{id, title, hint?, action, input?}[]`) appear in the palette under the tool and run `action({...input, action})`, so a destructive command gets the same confirm dialog. `input` may not set `action`, `confirmed`, `fingerprint` or `confirmToken`, and with `capabilities.actions` declared the action must be listed there.
|
|
209
|
+
- Panels: `onSnapshot` fires on each `show()` and then only when the snapshot data changed, so a panel need not diff unchanged polls.
|
|
210
|
+
- Search: declare `capabilities.search` and answer `snapshot({panel: "search", q, limit})` with `{hits, total}`.
|
|
211
|
+
- Destructive actions answer `{ok: false, needsConfirm: true, preview, fingerprint, changes?, warnings?, verb?, confirmText?}`. The console shows its dialog and calls again with `confirmed: true` and the fingerprint; recompute it and refuse on drift.
|
|
212
|
+
- Reversible actions may return `undo: {action, ...}`; the console offers it for 10 seconds.
|
|
213
|
+
- Panels run under a strict CSP: no inline scripts or `style=` attributes, nothing from other origins. Style with the `--acon-*` tokens.
|
|
214
|
+
- Space: declare `capabilities.space` and answer `{scene: SpaceScene}`. `registerConsoleConformance()` checks the public fields and bounds. The demo plugin includes a three-actor, four-zone fixture; production feeds are supplied by their plugins.
|
|
215
|
+
|
|
216
|
+
### Panel kit
|
|
217
|
+
|
|
218
|
+
Each panel receives `host.ui`, typed as `ConsoleUi` from `@staix/agent-console/contract`. The kit returns DOM nodes and renders strings as text, including strings containing HTML. Use it through the host; panels do not import shell files.
|
|
219
|
+
|
|
220
|
+
| Method | Result |
|
|
221
|
+
|---|---|
|
|
222
|
+
| `h(tag, props?, ...kids)` | An element with text or node children; `text` sets text and function-valued `on*` props attach event listeners. |
|
|
223
|
+
| `rows(items, render)` | A list of rendered rows, or `null` when the array is empty. |
|
|
224
|
+
| `row({title, sub?, onOpen?, chip?, verbs?})` | A row with an open control, optional subtitle, state chip and action nodes. |
|
|
225
|
+
| `empty(title, hint?)` | An empty state with an optional hint. |
|
|
226
|
+
| `chip(text, tone?)` | A compact state label. |
|
|
227
|
+
| `button(label, onClick, {tone?, disabled?}?)` | A `button.btn` with `type="button"`; `tone` is `"primary"` or `"danger"` and adds that modifier. |
|
|
228
|
+
| `notice(text, tone?)` | A status notice. Tone `err` uses `role="alert"` (announced at once); other tones use `role="status"`. |
|
|
229
|
+
| `form(fields, submit, onSubmit)` | A labeled form; fields are `{name, label, required?, value?}`, submit is `{label, primary?}`, and the callback receives string values by field name. |
|
|
230
|
+
| `detail(options)` | The shared detail template: kind/title/state header, whose move, actions, summary, provenance, collapsible sections and mono footer. |
|
|
231
|
+
| `table({columns, rows, caption?, sortable?})` | A table with text cells. A column is a `TableColumn`: `{key, label, align?: "start" \| "end", format?: "text" \| "number" \| "percent"}`. Header buttons sort stably (ascending, then descending) and set `aria-sort`; `sortable: false` turns this off. |
|
|
232
|
+
| `bars({items, max?, caption})` | Horizontal SVG bars; items are `{label, value, target?}` and `target` draws a marker. `max` defaults to the largest value or target. |
|
|
233
|
+
| `spark({values, caption})` | A small SVG line with a low/high/last summary. |
|
|
234
|
+
| `img({asset, alt})` | An image of this plugin's asset (PNG or JPEG). |
|
|
235
|
+
| `frame({asset, title})` | A same-origin frame of this plugin's asset, for PDF or text. |
|
|
236
|
+
| `link({asset} \| {panel, params?}, text)` | A link to this plugin's asset (opens in a new tab) or to one of its panels. |
|
|
237
|
+
| `review({doc, threads, onComment, onPlacement?})` | A line-numbered review of a `text`, `prose` or `diff` document. Select a range (pointer, or a gutter with Shift+Up/Down, then Enter), write a comment and the kit calls `onComment({anchor, body})`; send it to your tool with `host.action()` and throw (or reject) when the result is not `ok`, so the kit keeps the draft and says the comment was not sent. Existing threads show as gutter marks or, when they cannot be placed, in a strip above the view. |
|
|
238
|
+
| `visual(payload)` | A `mermaid` diagram in a sandboxed frame, `comparison` cards, a `table` (through `table`) or a `beforeAfter` pair (text, or an asset through `img`). See "Visuals". |
|
|
239
|
+
|
|
240
|
+
Table and charts are built by the shell from attributes and CSS classes, so they need no CSP change. Each chart has the caption as its accessible name and a text equivalent: bars show each value (and target) as text, and spark carries a visually hidden list of values. Inputs are bounded: 500 rows and 12 columns per table, 50 bar items and the last 365 spark points. Larger input is cut and a `ui-notice` states how much is shown. `tableColumnProblems(columns)` from `@staix/agent-console/conformance` checks a column list.
|
|
241
|
+
|
|
242
|
+
`h` accepts these tags: `div`, `span`, `p`, `strong`, `em`, `small`, `code`, `pre`, `section`, `article`, `header`, `footer`, `h1`, `h2`, `h3`, `h4`, `ul`, `ol`, `li`, `dl`, `dt`, `dd`, `button`, `input`, `textarea`, `select`, `option`, `form`, `label`, `fieldset`, `legend`, `details`, `summary`, `time`, `hr`, `br`.
|
|
243
|
+
|
|
244
|
+
Its attributes are `class`, `text`, `id`, `title`, `role`, `tabindex`, `type`, `name`, `value`, `placeholder`, `required`, `disabled`, `readonly`, `checked`, `multiple`, `autocomplete`, `maxlength`, `min`, `max`, `step`, `for`, `open`, `datetime`, plus `aria-*` and `data-*`. Event props such as `onclick` accept functions only. Unsupported tags or attributes throw `TypeError`; URL attributes, `style` and string event handlers are unsupported.
|
|
245
|
+
|
|
246
|
+
`Tone` is `"ok" | "warn" | "err" | "neutral"`. `detail` accepts `kind` and `title`, optional `state: {text, tone?}`, `whoseMove: "you" | "agent"`, `actions`, `summary`, `provenance: {label, value, at?}[]`, `sections: {title, body, open?}[]` and `footer: string[]`. Sections start collapsed unless `open` is true; section strings are text, while node bodies can contain other kit elements.
|
|
247
|
+
|
|
248
|
+
```js
|
|
249
|
+
export default (host) => {
|
|
250
|
+
const { ui } = host;
|
|
251
|
+
host.onSnapshot((snapshot) => {
|
|
252
|
+
const rows = ui.rows(snapshot.notes ?? [], (note) => ui.row({
|
|
253
|
+
title: note.title,
|
|
254
|
+
sub: note.status,
|
|
255
|
+
onOpen: () => host.navigate(host.tool, "notes", { id: note.id }),
|
|
256
|
+
verbs: [ui.button("Delete...", () => host.action({ action: "delete", id: note.id }), { tone: "danger" })],
|
|
257
|
+
}));
|
|
258
|
+
host.mount.replaceChildren(rows ?? ui.empty("No notes", "New notes will appear here."));
|
|
259
|
+
});
|
|
260
|
+
return { show() {}, hide() {}, destroy() { host.mount.replaceChildren(); } };
|
|
261
|
+
};
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
With focus on kit rows inside a panel, `j` and `k` move between rows and `Enter` opens the focused row. Form input keeps its normal typing behavior. Run actions through `host.action()` so destructive actions keep the shell's confirmation flow.
|
|
265
|
+
|
|
266
|
+
### Plugin assets
|
|
267
|
+
|
|
268
|
+
A provider may implement `asset({id})` and answer `{bytes: Uint8Array, mime, name}`, or `null` for an unknown id. The console serves it at `GET /p/<tool>/asset?id=<id>` with the same Host, Origin and session checks as other plugin routes (and the `acon serve` authentication), plus `X-Content-Type-Options: nosniff`, a `Content-Disposition` filename and a framing policy of same origin only. A cross-site fetch gets 403, an unknown id 404.
|
|
269
|
+
|
|
270
|
+
- Types: `application/pdf`, `image/png`, `image/jpeg`, `text/plain` and `text/csv` are served inline; `image/svg+xml` is served only as an attachment. Any other type gets 415.
|
|
271
|
+
- Size: 32 MB per asset by default (`ServerOptions.assetMaxBytes`); larger answers get 413.
|
|
272
|
+
- Panels reference assets only through `ui.img`, `ui.frame` and `ui.link`, which build the URL from the panel's own tool and an id. URLs, other tools and other link targets throw `TypeError`; `h` still has no `a`, `img` or `iframe`.
|
|
273
|
+
|
|
274
|
+
A plugin without `asset` behaves as before. Pass `assets: [ids]` to `registerConsoleConformance` to check that those ids answer a served type.
|
|
275
|
+
|
|
276
|
+
### Review anchors
|
|
277
|
+
|
|
278
|
+
`ui.review` builds and places `ReviewAnchor` values from `@staix/agent-console/contract`; acon never stores an anchor or writes a file. The owning tool stores anchors unchanged and stays the authority.
|
|
279
|
+
|
|
280
|
+
- Documents: `{kind: "text" | "prose", path, text, contentHash, projection?}` or `{kind: "diff", path, unified, contentHash}`. Prose splits paragraphs on blank lines and shows Markdown as its source. All text renders as text. Up to 512 KB or 20,000 lines; more is cut with a notice.
|
|
281
|
+
- `contentHash` is the lowercase hex sha256 of the text the anchor was made against: the projected text when `projection` is set, else the raw file bytes.
|
|
282
|
+
- A new anchor has `lineRange` and `textQuote` (`exact` up to 2000 characters, `prefix`/`suffix` up to 64); a diff selection adds `diffHunk` and `side`. The kit displays but does not create `structural` and `region`.
|
|
283
|
+
- Placement: with the same `contentHash`, by `lineRange`; otherwise by a single `textQuote` match, with `prefix`/`suffix` breaking ties; otherwise unplaced. An ambiguous quote, a different projection, another path or an unknown selector kind is unplaced, never guessed. `onPlacement` reports `{threadId, placed, via: "hash" | "quote" | null}` per thread.
|
|
284
|
+
- Keyboard: Tab to the gutter, Up/Down/Home/End to move, Shift+Up/Down to extend, Enter to comment, Escape to clear.
|
|
285
|
+
- Bounds: 1 to 4 selectors per anchor, body up to 4000 characters, 200 threads per view. `reviewProblems(threads)`, `reviewThreadProblems` and `reviewAnchorProblems` from `@staix/agent-console/conformance` check them.
|
|
286
|
+
- Shared cases: `test/fixtures/review/cases.json` ships with the package so other tools (agent-hub) can run the same valid and invalid anchors, threads and hash cases; `expandReviewCase` and `reviewCaseValue` expand its compact `<N x c>` and `repeat` forms.
|
|
287
|
+
|
|
288
|
+
### Visuals
|
|
289
|
+
|
|
290
|
+
`ui.visual(payload)` takes a `VisualPayload` from `@staix/agent-console/contract`:
|
|
291
|
+
|
|
292
|
+
- `{kind: "mermaid", source, title?}`: the diagram renders only inside an `<iframe sandbox="allow-scripts">` served at `/visual/frame`. The frame has an opaque origin (no cookies, no storage, no access to the console) and its own CSP, `default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src data:; font-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'`, which has no `connect-src`, so it cannot fetch anything. The console sends `{source, theme}` by `postMessage` and accepts only `{ok, height}` back from its own frame; the height is clamped to 80-2000 px. Mermaid runs with `securityLevel: "strict"`; the result is shown as an `<img>` of the SVG (a data URL), and the frame's own styles come from `/visual/frame.css`. `'unsafe-inline'` is there only for Mermaid's own measuring during render. The frame loads when the visual scrolls into view and is reused per panel. An empty or invalid source, a source over 20 KB, or a render over 5 s shows the source as text with a notice.
|
|
293
|
+
- `{kind: "comparison", title?, options}`: 2 to 6 options, each `{title, summary?, pros?, cons?, recommended?}` with at most 10 pros and 10 cons. The recommended option is marked by text and a double border, not color alone.
|
|
294
|
+
- `{kind: "table", caption?, columns, rows}`: the same as `ui.table`.
|
|
295
|
+
- `{kind: "beforeAfter", caption?, before, after}`: each side is `{text}` or `{asset, alt}` (a plugin asset, see "Plugin assets"); sides stack below 768 px.
|
|
296
|
+
|
|
297
|
+
The only CSP addition for visuals is the console's `frame-src 'self'`, shared with plugin assets. Mermaid is vendored from `@mermaid-js/tiny` 12.2.0 (2,811,424 bytes, MIT) into `shell/vendor/mermaid/` with a sha256 manifest; `bun scripts/vendor-mermaid.ts` regenerates it from the pinned devDependency and `bun scripts/vendor-mermaid.ts --check` verifies it.
|
|
298
|
+
|
|
299
|
+
### Public styling contract
|
|
300
|
+
|
|
301
|
+
Panels may use these classes. Modifiers apply only with their base class:
|
|
302
|
+
|
|
303
|
+
- Lists and rows: `rows`, `row`, `row-main`, `row-text`, `row-title`, `row-sub`, `row-verbs`, `ui-rows`, `ui-row-open`.
|
|
304
|
+
- Controls and status: `btn` with `primary` or `danger` (what `ui.button` renders); `chip` with `ok`, `warn`, `err` or `accent`; `notice`; `ui-notice` with `ok`, `warn` or `err`.
|
|
305
|
+
- Containers and text: `empty`, `card`, `mono`, `ui-form`, `ui-field`.
|
|
306
|
+
- Details: `ui-detail`, `ui-detail-head`, `ui-detail-kind`, `ui-detail-move`, `ui-detail-actions`, `ui-detail-summary`, `ui-provenance`, `ui-detail-section`, `ui-detail-body`, `ui-detail-footer`.
|
|
307
|
+
- Tables and charts: `ui-table-block`, `ui-table-wrap`, `ui-table`, `ui-sort`, `ui-chart`, `ui-bars`, `ui-bar`, `ui-bar-label`, `ui-bar-value`, `ui-bar-track`, `ui-bar-fill`, `ui-bar-target`, `ui-spark`, `ui-spark-summary`, `ui-spark-line`, `ui-sr` (visually hidden).
|
|
308
|
+
- Assets and links: `ui-img`, `ui-frame`, `ui-link`.
|
|
309
|
+
- Review: `ui-review`, `ui-review-doc`, `ui-review-text`/`prose`/`diff`, `ui-review-unit`, `ui-review-gutter`, `ui-review-src`, `ui-review-add`, `ui-review-del`, `ui-review-hunk`, `ui-review-meta`, `ui-review-selected`, `ui-review-has-thread`, `ui-review-mark`, `ui-review-strip`, `ui-review-unplaced`, `ui-review-quote`, `ui-review-thread`, `ui-review-thread-body`, `ui-review-status`, `ui-review-box`, `ui-review-target`, `ui-review-input`.
|
|
310
|
+
- Visuals: `ui-visual`, `ui-visual-mermaid`, `ui-visual-frame`, `ui-visual-source`, `ui-visual-comparison`, `ui-visual-options`, `ui-visual-option` with `recommended`, `ui-visual-recommended`, `ui-visual-points` with `pros` or `cons`, `ui-visual-before-after`, `ui-visual-pair`, `ui-visual-side`, `ui-visual-text`.
|
|
311
|
+
|
|
312
|
+
The public CSS tokens are:
|
|
313
|
+
|
|
314
|
+
- Surfaces and text: `--acon-bg`, `--acon-surface`, `--acon-surface-2`, `--acon-line`, `--acon-line-2`, `--acon-text`, `--acon-text-2`.
|
|
315
|
+
- Accent and status: `--acon-accent`, `--acon-accent-soft`, `--acon-accent-text`, `--acon-on-accent`, `--acon-ok`, `--acon-ok-soft`, `--acon-warn`, `--acon-warn-soft`, `--acon-err`, `--acon-err-soft`, `--acon-focus`.
|
|
316
|
+
- Spacing: `--acon-space-1`, `--acon-space-2`, `--acon-space-3`, `--acon-space-4`, `--acon-space-5`, `--acon-space-6`.
|
|
317
|
+
- Shape and motion: `--acon-radius-1`, `--acon-radius-2`, `--acon-radius-3`, `--acon-shadow`, `--acon-ease`.
|
|
318
|
+
- Fonts: `--acon-font-ui`, `--acon-font-mono`.
|
|
319
|
+
- Type sizes: `--acon-text-2xs` (11), `--acon-text-xs` (12), `--acon-text-sm` (13), `--acon-text-md` (14), `--acon-text-base` (15), `--acon-text-lg` (16), `--acon-text-xl` (17), `--acon-text-2xl` (22). Values are rem, with the listed pixel equivalents at the default 16 px browser font size; spacing stays in px.
|
|
320
|
+
|
|
321
|
+
Use the tokens in a plugin's own CSS for light and dark themes. `host.theme.tokens` exposes the color tokens; other tokens are available as CSS custom properties. Shell selectors outside the documented public classes are private and may change.
|
|
322
|
+
|
|
323
|
+
Check a plugin with the conformance suite:
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
import { registerConsoleConformance } from "@staix/agent-console/conformance";
|
|
327
|
+
registerConsoleConformance(plugin, { destructive: [{ action: "delete", id: "x" }] });
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Develop
|
|
331
|
+
|
|
332
|
+
```sh
|
|
333
|
+
bun install --frozen-lockfile
|
|
334
|
+
bun run check # typecheck, boundary, tests, pack dry-run
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Before UI PRs, run `node scripts/ui-check.mjs`. It finds an existing Playwright install at run time, starts isolated fixture servers and writes screenshots plus `ui-check/report.json`. It checks 375/768/1024/1440 px, both themes and 100%/200% text, including Space with two tools, an explicit synthetic pet/pack and a no-pack fallback. Mini (`#/local/mini`) is captured in every cell, at the 320x440 pop-out size (light and dark, 100% and 200% text, rendered in the page, not a Picture-in-Picture window) and once with no pack and the pet off; it checks the pet sprite, Tab order with visible focus over item and footer buttons, and that a pet-less Mini requests nothing under `/pets/`. It checks namespace keys, roster parity, navigation and unchanged-poll DOM writes. It exits non-zero on browser errors, text clipping (including the search label), overlap or horizontal overflow. It also checks that the skip link is completely offscreen until focused, reachable by keyboard and moves focus to main when activated. Playwright is a development tool supplied by the person running the check, not a package dependency or CI gate.
|
|
338
|
+
|
|
339
|
+
The full probe also runs a two-node static-shell matrix at 375/1440 px in both themes and text sizes, checking node ownership, auth, offline and origin-refused states. `--static-only` runs that focused matrix.
|
|
340
|
+
|
|
341
|
+
Use `PLAYWRIGHT_MODULE` for an absolute Playwright or playwright-core module path, `PLAYWRIGHT_EXECUTABLE_PATH` for an existing Chromium executable, and `BUN_BIN` for Bun when it is not on PATH. `--output <dir>` changes the artifact directory; `--baseline <report.json>` compares 100% text metrics against an earlier run (1 px tolerance). `--force-overflow` is a negative control and must exit non-zero. Use the workspace's remote runner when local browser workloads are deferred.
|
|
342
|
+
|
|
343
|
+
`--space-views-only` runs the Room/3D camera checks at 375 and 1440 px in both themes and 100%/200% text. It verifies lazy vendor requests, preserved focus order, projected overlay centres within 8 px after rotation, model loading, context-loss and absent-WebGL2 fallbacks, and zero additional canvas renders across a settled 10-second window. Depth-ordered scene buttons can occlude each other in projected views; their geometry and equivalent keyboard/roster access are checked separately from the shell's normal strict overlap checks. The full run includes both view and motion checks. For a separate published-art compatibility run, use `--space-views-only --art-root <directory>` with explicit `assets/` and `pets/` subdirectories; the probe never scans a real agent home.
|
|
344
|
+
|
|
345
|
+
Three.js is an exact build-time devDependency (`0.187.0`), not a runtime package dependency. To update it deliberately, change the pin in `package.json` and `scripts/vendor-three.ts`, install it, then run `bun scripts/vendor-three.ts`. The script walks the module/GLTFLoader import graph, copies only those modules, rewrites imports to relative paths using the installed TypeScript parser and retains the upstream MIT license in `shell/vendor/NOTICE`. `test/vendor-three.test.ts` pins each shipped file's SHA-256 and checks deterministic regeneration. Update its literal hashes only after reviewing the upstream change; serve the checked-in modules from the same origin. No CDN or import map is used. The package dry-run reports the added vendor size.
|
|
346
|
+
|
|
347
|
+
`--space-motion-only` runs the focused Space motion regression context. It changes public fixture facts through intercepted snapshot responses, checks actual CSS transitions and message consumption, and holds responses until the real snapshot gap exceeds 3 seconds. The normal full run includes this check as well as the viewport matrix.
|
|
348
|
+
|
|
349
|
+
Record a real macOS Safari/VoiceOver pass in the PR for inbox, peek, palette and confirm: focused element, announced role/name/state, keyboard result and any finding. DOM checks do not substitute for VoiceOver announcements; the owner decides whether a VoiceOver finding blocks merge.
|
|
350
|
+
|
|
351
|
+
## Release
|
|
352
|
+
|
|
353
|
+
A `v*` tag equal to `v` plus the `package.json` version publishes through `.github/workflows/release.yml`. The tag needs a dated `## [VERSION] - YYYY-MM-DD` section in `CHANGELOG.md`. Linux and macOS run `bun run check`, then stage one tarball, check its SHA-256 manifest and install it into a clean prefix. The publish job uploads that same Linux tarball with npm OIDC trusted publishing and provenance, and no npm token is used. It runs only when the repository variable `NPM_PUBLISH_ENABLED` is `true`, and only after a reviewer approves the `npm-production` environment. The trusted publisher is configured on npm for repository `STAIxBWLB/agent-console`, workflow `release.yml` and environment `npm-production`. A GitHub release with the CHANGELOG section and the tarball follows. Never move a release tag or reuse a published version.
|
|
354
|
+
|
|
355
|
+
License: MIT
|
package/bin/acon.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
const version = /^(\d+)\.(\d+)\.(\d+)/.exec(process.versions.bun ?? "");
|
|
3
|
+
if (!version || Number(version[1]) < 1 || (Number(version[1]) === 1 && (Number(version[2]) < 4 || (Number(version[2]) === 4 && Number(version[3]) < 2)))) {
|
|
4
|
+
console.error("acon: Bun >=1.4.2 is required. Install Bun: https://bun.sh");
|
|
5
|
+
process.exit(1);
|
|
6
|
+
}
|
|
7
|
+
const { main } = await import("../src/cli.ts");
|
|
8
|
+
process.exitCode = await main(process.argv.slice(2));
|
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@staix/agent-console",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Local-first web console that composes agent-hub, agent-mem and vault-mem as plugin panels",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/STAIxBWLB/agent-console.git"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"bun": ">=1.4.2"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@mermaid-js/tiny": "12.2.0",
|
|
16
|
+
"@types/bun": "^1.3.0",
|
|
17
|
+
"three": "0.187.0",
|
|
18
|
+
"typescript": "^5.8.0"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./src/contract.ts",
|
|
22
|
+
"./contract": "./src/contract.ts",
|
|
23
|
+
"./conformance": "./src/conformance.ts"
|
|
24
|
+
},
|
|
25
|
+
"bin": {
|
|
26
|
+
"acon": "bin/acon.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"src/",
|
|
30
|
+
"bin/",
|
|
31
|
+
"shell/",
|
|
32
|
+
"test/fixtures/review/",
|
|
33
|
+
"README.md",
|
|
34
|
+
"CHANGELOG.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"check": "bun x tsc --noEmit && bun scripts/check-boundary.ts && bun test && bun pm pack --dry-run"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Attachment</title>
|
|
7
|
+
<link rel="stylesheet" href="/asset-viewer.css">
|
|
8
|
+
<script src="/asset-viewer.js" defer></script>
|
|
9
|
+
</head>
|
|
10
|
+
<body><main id="asset" role="status">Loading attachment...</main></body>
|
|
11
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Opaque sandbox: only the parent may supply bounded, allowlisted attachment bytes.
|
|
2
|
+
const types = new Set(['application/pdf', 'image/png', 'image/jpeg', 'text/plain', 'text/csv']);
|
|
3
|
+
let objectUrl;
|
|
4
|
+
window.addEventListener('message', (event) => {
|
|
5
|
+
if (event.source !== parent || event.origin !== location.origin || event.data?.kind !== 'acon-asset') return;
|
|
6
|
+
const { mime, bytes } = event.data;
|
|
7
|
+
if (!types.has(mime) || !(bytes instanceof ArrayBuffer) || bytes.byteLength > 32 * 1024 * 1024) return;
|
|
8
|
+
const root = document.getElementById('asset');
|
|
9
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
10
|
+
if (mime.startsWith('text/')) {
|
|
11
|
+
const pre = document.createElement('pre');
|
|
12
|
+
pre.textContent = new TextDecoder().decode(bytes);
|
|
13
|
+
root.replaceChildren(pre);
|
|
14
|
+
} else {
|
|
15
|
+
objectUrl = URL.createObjectURL(new Blob([bytes], { type: mime }));
|
|
16
|
+
const child = document.createElement(mime === 'application/pdf' ? 'iframe' : 'img');
|
|
17
|
+
child.src = objectUrl;
|
|
18
|
+
if (child.tagName === 'IMG') child.alt = 'Attachment';
|
|
19
|
+
else child.title = 'PDF attachment';
|
|
20
|
+
root.replaceChildren(child);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
parent.postMessage({ kind: 'acon-asset-ready' }, location.origin);
|
package/shell/icon.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#2B50C8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="3"/><path d="M7 9l3 3-3 3M12 15h5"/></svg>
|