@abstractframework/assistant 0.0.0-stage → 0.1.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 +62 -0
- package/LICENSE +22 -0
- package/README.md +229 -2
- package/bin/cli.js +44 -0
- package/bin/server.js +263 -0
- package/dist/assets/avatar_core-7Jm5QyAS.wasm +0 -0
- package/dist/assets/index-DqxMtR-3.js +83 -0
- package/dist/assets/index-DsiGtjU5.css +1 -0
- package/dist/index.html +15 -0
- package/dist/llms.txt +110 -0
- package/package.json +72 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Changelog: AbstractAssistant for the web
|
|
2
|
+
|
|
3
|
+
All notable changes to the web Assistant (`@abstractframework/assistant` on npm) are documented
|
|
4
|
+
in this file. It is versioned and released independently of the Python package, whose history is
|
|
5
|
+
in the repository [CHANGELOG](../CHANGELOG.md). Release tags for this package are `web-vX.Y.Z`.
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-11-03
|
|
10
|
+
|
|
11
|
+
First public release on npm. AbstractGateway lists it as **Assistant (web)** and installs it from
|
|
12
|
+
the npm registry (`abstractgateway apps install assistant-web`, or **Install** in the console and
|
|
13
|
+
tray Apps menu); you can also run it yourself with `npx @abstractframework/assistant`.
|
|
14
|
+
It is built on AbstractUIC ui-kit 0.9.14 and panel-chat 0.8.1 (shared bundles re-synced from them) and
|
|
15
|
+
AbstractAvatar 0.1.1.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Conversations in the browser.** Your gateway conversations, grouped by recency and searchable,
|
|
20
|
+
with live answers, Stop, attachments, copy and export, rendered from the gateway's replay view.
|
|
21
|
+
Requests that need you are listed first.
|
|
22
|
+
- **Voice.** A Watch-style animated orb: tap to talk (or hold, in Voice settings), dictation into
|
|
23
|
+
the draft for review or sent automatically, read aloud that follows your voice setting, and
|
|
24
|
+
playback controls. The composer microphone and the orb share one recording.
|
|
25
|
+
- **Avatar conversation view.** A second view over the same work. Under the avatar, one line shows the
|
|
26
|
+
latest thing the assistant reports while it works, replaced as soon as something new arrives; click it
|
|
27
|
+
to read it in full, then **Reply to this** or **Latest** (back to following). The findings it reports
|
|
28
|
+
(facts or evidence it established) accumulate in a **Findings** panel beside the avatar, a sheet
|
|
29
|
+
opened with **Findings** on a phone; each opens to the results it came from. The answer appears once,
|
|
30
|
+
as a message. An open question or approval stays pinned at the top of the view with its buttons on
|
|
31
|
+
screen, also on a laptop-height window (1000×650); on a very short view its context scrolls inside it
|
|
32
|
+
while the heading and the buttons stay visible. Needs the producer-typed public updates of AbstractAgent (`kind`) and AbstractRuntime
|
|
33
|
+
(`update_kind`); older updates are shown as progress. Permission requests use plain actions with the
|
|
34
|
+
assistant's public explanation when the gateway serves one. The view holds no settings: they open
|
|
35
|
+
from **Settings** in the conversations sidebar (behind **Open Conversations** on a phone), as
|
|
36
|
+
everywhere in the app.
|
|
37
|
+
- **Approvals in the conversation.** Deny, Allow once, Allow for this chat or Decide later; a
|
|
38
|
+
chat's tool policy is stored on the gateway when it offers one.
|
|
39
|
+
- **Avatar designs.** The 44-design AbstractAvatar catalog with previews of every activity state;
|
|
40
|
+
your choice is kept in this browser. Only avatars with eyes (such as the Hover robot, the Eye, the
|
|
41
|
+
Rover and the Watch faces and robots) look toward the pointer while Ready, and only their eyes
|
|
42
|
+
move, easing back to centre after two idle seconds; the orb and every other avatar stay still.
|
|
43
|
+
- **Automations, as on the Apple Watch.** **Automations** beside Conversations lists them in the
|
|
44
|
+
gateway's order: the active ones first, the most recently active at the top, then **Paused · N** and
|
|
45
|
+
**Archived · N**, folded until opened (with an older gateway every automation is in one list).
|
|
46
|
+
Search finds them by name. An automation's page shows how many times it ran, how often and when
|
|
47
|
+
next, its task and its latest ten runs as a conversation; opening it marks its new results seen.
|
|
48
|
+
**Run** starts a run now and **Stop** ends the run in progress. **Edit** changes its title, task,
|
|
49
|
+
whether it is active, how often it runs and whether each run builds on the earlier ones (and how
|
|
50
|
+
much of them it reads). Talking on its page opens a conversation about its latest run; a run
|
|
51
|
+
waiting for you opens where you answer it; a tool request is answered on its page (**Deny**,
|
|
52
|
+
**Approve all**, **Approve**), and **Approve all** lists the tools under **Allowed for its runs**,
|
|
53
|
+
which this browser then approves when it sees them until **Ask again**. The archive button asks
|
|
54
|
+
first and **Restore** brings an archived one back. **New** creates one: its task, the flow that
|
|
55
|
+
runs it, its **Tools** (each Off, Ask or Auto, from the gateway's defaults), every interval from
|
|
56
|
+
30 seconds to a week or every day at a time, and whether its runs build on each other.
|
|
57
|
+
- **Docs drawer.** Questions about the Assistant answered from its bundled browser guide, separate
|
|
58
|
+
from your main conversation.
|
|
59
|
+
- **The app server** (`abstractassistant-web`): serves the built app on 127.0.0.1 (port 3008 by
|
|
60
|
+
default) and under the gateway at `/apps/assistant-web/`, takes the shared app flags
|
|
61
|
+
(`--gateway-url`, `--port`, `--host`, `--help`), and keeps the gateway session in an HttpOnly
|
|
62
|
+
cookie so the page never holds a gateway token.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Laurent-Philippe Albou (contact@abstractcore.ai)
|
|
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.
|
|
22
|
+
|
package/README.md
CHANGED
|
@@ -1,3 +1,230 @@
|
|
|
1
|
-
#
|
|
1
|
+
# AbstractAssistant for the web
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Assistant's conversation surface in a browser: your conversations, live answers, tool
|
|
4
|
+
approvals and questions in the conversation, attachments, dictation and read aloud. It is a thin
|
|
5
|
+
client of [AbstractGateway](../../abstractgateway/docs/apps.md): the gateway keeps every
|
|
6
|
+
conversation, runs the Assistant's workflow and holds your sign-in. This page keeps no token and
|
|
7
|
+
no copy of your chats; it keeps your display and voice preferences (appearance, panel widths,
|
|
8
|
+
voice overrides, devices, microphone mode) in this browser.
|
|
9
|
+
|
|
10
|
+
The Watch-inspired layout centres the interaction on a living avatar: tap it to
|
|
11
|
+
talk (or hold, if selected in Voice settings), tap while speaking to pause/resume,
|
|
12
|
+
and use Stop to end playback or the current request. Dictation goes into the draft
|
|
13
|
+
for review unless **Send what you say automatically** is enabled. The pencil moves
|
|
14
|
+
focus to the message field. **Read new replies aloud** uses the same preference as
|
|
15
|
+
Settings → Voice; opening history stays silent. Sending a new message, choosing
|
|
16
|
+
another conversation or starting a new one stops the previous audio.
|
|
17
|
+
|
|
18
|
+
Settings includes the complete 44-design AbstractAvatar catalog: 34 Watch picker
|
|
19
|
+
designs, three additional Watch shape variants, the iPhone woven orb and six shaded
|
|
20
|
+
avatars. Watch Halo is the default for a new browser; existing selections are preserved.
|
|
21
|
+
The shared generator and AbstractUIC presenter provide previews of Ready,
|
|
22
|
+
Listening, Thinking, Working, Speaking, Waiting and Error, and an animation switch.
|
|
23
|
+
Reduced motion and hidden-page animation are handled by the shared renderer.
|
|
24
|
+
The phone layout keeps the orb above the conversation; wider screens give voice
|
|
25
|
+
and the readable conversation their own space. Conversations are searchable and
|
|
26
|
+
grouped by recency, with requests that need you first.
|
|
27
|
+
|
|
28
|
+
**Automations**, beside Conversations in the sidebar, does what the Apple Watch assistant does:
|
|
29
|
+
the active automations first and the most recently active at the top, then **Paused · N** and
|
|
30
|
+
**Archived · N** folded until opened (framework ADR 0048; the gateway serves the order and the
|
|
31
|
+
counts, and an older gateway gets every automation in one list), an automation's latest runs as a
|
|
32
|
+
conversation (opening it marks its new results seen), Run and Stop, Edit (title, task, active, how
|
|
33
|
+
often, memory; sent with the revision it was read at), archive and restore, talking about its
|
|
34
|
+
latest run (a new conversation, the automation unchanged), a run's tool request answered on its
|
|
35
|
+
page with **Approve all** filling **Allowed for its runs** (kept in this browser, per account;
|
|
36
|
+
this browser then approves those tools for the automation's runs when it reads the list), and New
|
|
37
|
+
automation with its **Tools** (Off, Ask or Auto per tool; the split rides the definition's
|
|
38
|
+
`policy.tool_policy`). The cards are AbstractUIC's Watch-style `AutomationCard`; the gateway
|
|
39
|
+
calls, the flow picker, the tool editor and the edit and Approve all rules are the kit's
|
|
40
|
+
(`createAutomationsClient`, `AutomationWorkflowPicker`, `ToolModeEditor`, `reviseChanges`,
|
|
41
|
+
`trustAfterApproveAll`). Run-finished notifications stay on the native apps.
|
|
42
|
+
|
|
43
|
+
The Docs drawer keeps its question field at the bottom and scrolls the conversation
|
|
44
|
+
above it. Its questions use Assistant documentation and stay separate from your
|
|
45
|
+
main conversation. The [browser guide](docs/assistant-guide.md) is bundled as
|
|
46
|
+
`llms.txt` for Gateway to supply to that drawer.
|
|
47
|
+
|
|
48
|
+
Technical streaming notices belong in **Details**, together with optional usage
|
|
49
|
+
and timing, rather than in the conversation. A structured-output model call may
|
|
50
|
+
deliver its answer all at once; this does not indicate a failed reply. Warnings,
|
|
51
|
+
errors and unanswered approval requests remain visible.
|
|
52
|
+
|
|
53
|
+
It is composed from the AbstractFramework UI kit (AbstractUIC `ui-kit` + `panel-chat`, ADR 0001
|
|
54
|
+
there): the app shell and its top-right cluster (docs assistant, appearance, about, who is signed
|
|
55
|
+
in, the connection pill), the chat (`WorkflowChat` + `useWorkflowSession`: thread, live replies,
|
|
56
|
+
Stop, attachment chips, copy/export), the one approval panel (`presentInteraction`: Deny / Allow
|
|
57
|
+
once / Allow for this chat / Decide later; the chat's tool policy is stored on the gateway when it
|
|
58
|
+
offers `session_tool_policy`), the run's tool plan, and voice (Settings → Voice, read aloud,
|
|
59
|
+
Tap/Hold to talk). What stays here: the gateway calls (`src/gateway/`), the conversation list and
|
|
60
|
+
its titles, the workflow choice and the run input (desktop parity ports).
|
|
61
|
+
|
|
62
|
+
Published on npm as [`@abstractframework/assistant`](https://www.npmjs.com/package/@abstractframework/assistant)
|
|
63
|
+
(release history: [CHANGELOG.md](CHANGELOG.md); design: [ADR 0007](../docs/adr/0007_web_assistant.md)).
|
|
64
|
+
Gateway's catalog and tray list it as **Assistant (web)**, next to the desktop Assistant.
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
Through AbstractGateway (recommended): open **Apps → Assistant (web) → Install** in the gateway
|
|
69
|
+
console or tray, or run
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
abstractgateway apps install assistant-web --launch
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The gateway downloads the package from npm, checks it against the registry's integrity hash,
|
|
76
|
+
installs its one runtime dependency and starts it. Open it from **Apps → Assistant (web)**: the
|
|
77
|
+
one-time handover signs you in at `<gateway>/apps/assistant-web/`.
|
|
78
|
+
|
|
79
|
+
On its own, with Node.js 18 or newer:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx @abstractframework/assistant --gateway-url http://127.0.0.1:8080
|
|
83
|
+
# or: npm install -g @abstractframework/assistant && abstractassistant-web
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
It serves on `http://127.0.0.1:3008/`; sign in with a gateway user id and token in the dialog.
|
|
87
|
+
Without `--gateway-url` it follows the local gateway pointer (`~/.abstractframework/gateway.json`),
|
|
88
|
+
then `http://127.0.0.1:8080`.
|
|
89
|
+
|
|
90
|
+
## Run it against a gateway (development)
|
|
91
|
+
|
|
92
|
+
Needs Node.js 18 or newer (developed on 24, CI on 22) and a running gateway.
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
cd web
|
|
96
|
+
npm install
|
|
97
|
+
ABSTRACTASSISTANT_GATEWAY_URL=http://127.0.0.1:8080 npm run dev # http://127.0.0.1:3008/
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Sign in with a gateway user id and token in the dialog. The token goes to this app's own server
|
|
101
|
+
(`bin/server.js`), which trades it for a gateway browser session and keeps that in an HttpOnly
|
|
102
|
+
cookie; the page itself never sees a token. Without the variable the server follows the local
|
|
103
|
+
gateway pointer (`~/.abstractframework/gateway.json`), then `http://127.0.0.1:8080`.
|
|
104
|
+
|
|
105
|
+
## Run the built app
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm run build # tsc + vite build + the relative-URL check
|
|
109
|
+
node bin/cli.js --gateway-url http://127.0.0.1:8080 --port 3008
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`bin/cli.js` takes the framework's shared app flags (`--gateway-url`, `--port`, `--host`,
|
|
113
|
+
`--help`; abstractuic `app-server`). It listens on 127.0.0.1 only. Gateway detects the
|
|
114
|
+
running app and serves it at `<gateway>/apps/assistant-web/`; open it through Gateway
|
|
115
|
+
Apps to use the signed-in handover.
|
|
116
|
+
|
|
117
|
+
Dictation needs a secure page: `http://127.0.0.1`/`localhost`, or `https://`. From another
|
|
118
|
+
computer over plain `http://` the microphone button says why it is off.
|
|
119
|
+
|
|
120
|
+
## Tests
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npm test # vitest: fold, titles, run input and tool-plan parity, workflow choice, the kit transport, hosts, automations
|
|
124
|
+
npm run check:urls # no root-absolute same-origin URL in the browser code
|
|
125
|
+
npm run build && npm run test:layout # headless Chromium at 420/760/1024/1280 px, fake gateway
|
|
126
|
+
npm run test:experience # real UI: virtual mic, draft review, speech ownership, avatar preview, keyboard, reduced motion
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`test:layout` needs Playwright's Chromium (`npx playwright install chromium`); add
|
|
130
|
+
`-- --shots <dir>` to `node scripts/check_layout.mjs` to save the screenshots. Below 1024 px it
|
|
131
|
+
emulates a touch device and every target must reach 44 px; from 1024 px (a mouse) the rule covers
|
|
132
|
+
the app's own controls and the kit sizes its own for a fine pointer. It also opens Settings →
|
|
133
|
+
Voice and checks that it names the gateway default and fits, then the automations at
|
|
134
|
+
400/760/1024/1440 px: the list with Paused and Archived opened, an automation's page, Edit and New.
|
|
135
|
+
|
|
136
|
+
`test:experience` uses a virtual Chromium microphone, silent generated WAV data
|
|
137
|
+
and a fake Gateway. It performs no inference and never opens a physical microphone.
|
|
138
|
+
Both browser checks accept `--shots <dir>` for screenshots.
|
|
139
|
+
|
|
140
|
+
The session fold runs on the Python tests' own fixture (`tests/basic/fixtures/gateway_runs/`); the
|
|
141
|
+
run-input and tool-list tests run on cases generated BY the desktop's code (CI regenerates both and
|
|
142
|
+
fails on a difference):
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
PYTHONPATH=. python -P web/scripts/gen_run_input_parity.py # from the repository root
|
|
146
|
+
PYTHONPATH=. python -P web/scripts/gen_tool_policy_parity.py # needs the package's dependencies
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Layout
|
|
150
|
+
|
|
151
|
+
| Path | What it is |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `bin/server.js`, `bin/cli.js` | The app server: built files, mount under `/apps/assistant-web/`, gateway session proxy |
|
|
154
|
+
| `src/host/` | What the page needs from its host (gateway URLs, authentication, native capabilities); the browser host |
|
|
155
|
+
| `src/gateway/` | Gateway calls (SSE over fetch, framed by the kit's `sseFrames`) and the kit's `WorkflowTransport` over them, with `replayView` (panel-chat 0.6.2: the conversation, every turn of it, is rendered from the gateway's replay view on AbstractGateway >= 0.17.0, the ledger fold otherwise) |
|
|
156
|
+
| `src/lib/` | Pure logic with tests: session fold, titles, workflow choice, run input, theme group, automations (the list's sections, a new automation's body, runs as messages, its tools and "Allowed for its runs") |
|
|
157
|
+
| `src/ui/` | The Assistant space on the kit: the conversation (`WorkflowChat`), the conversation and automation lists (the shell's navigation), an automation's page, Edit and New, Settings → Voice |
|
|
158
|
+
| `src/vendor/abstractavatar/`, `src/vendor/abstractdocs/` | Generated, hash-checked shared rendering and Docs artifacts; standalone builds use these pinned snapshots |
|
|
159
|
+
| `src/styles/index.css` | App styles on the design tokens of `web/design/tokens.css` |
|
|
160
|
+
| `design/` | Design tokens and mockups (owned separately) |
|
|
161
|
+
|
|
162
|
+
The npm package (`@abstractframework/assistant`) ships `dist/`, `bin/`, this README, the
|
|
163
|
+
CHANGELOG and the LICENSE; `npm run check:package` lists what a publish would ship and fails on
|
|
164
|
+
anything else. The Python wheel and sdist do not include `web/`.
|
|
165
|
+
|
|
166
|
+
To adopt a built shared-library update from a workspace checkout, run `npm run sync:avatars`
|
|
167
|
+
and `npm run sync:docs`. Both snapshots come from canonical AbstractUIC implementations. The
|
|
168
|
+
Docs artifact keeps React and the UI kit external so speech and application state retain one
|
|
169
|
+
owner. `npm run build` verifies the committed artifact hashes without needing sibling sources.
|
|
170
|
+
|
|
171
|
+
### Compact conversation cards
|
|
172
|
+
|
|
173
|
+
Conversation rows use AbstractUIC's shared Watch-style `SessionCard`: cyan marks unread
|
|
174
|
+
content, orange marks a pending human question or approval, and a small ring marks work.
|
|
175
|
+
Reading clears unread only when loaded content is visible in the focused page. It never
|
|
176
|
+
answers or dismisses a pending approval. Read receipts are local to this browser and account;
|
|
177
|
+
Code and Assistant share them when served on the same origin. Existing history is baselined
|
|
178
|
+
on first use. Regenerate the canonical compatibility snapshot with `npm run sync:sessions`;
|
|
179
|
+
the normal build verifies its provenance. Other devices keep independent read state.
|
|
180
|
+
|
|
181
|
+
### Message files and tool results
|
|
182
|
+
|
|
183
|
+
The canonical AbstractUIC conversation snapshot (`npm run sync:conversation`) supplies the
|
|
184
|
+
same message attachments, safe clickable links and structured tool-result viewer in both web
|
|
185
|
+
clients. Files accompany the original user or assistant message, including restored history
|
|
186
|
+
and file-only replies. File bytes load when opened through authenticated reads. The build
|
|
187
|
+
verifies snapshot hashes; no sibling checkout is required to build the committed web app.
|
|
188
|
+
|
|
189
|
+
Conversation unread marks use the Gateway's account-wide read ledger. Confirmed consumption
|
|
190
|
+
is shared with Code and native clients; failed writes retain the mark. Native clients need
|
|
191
|
+
a build that supports `/api/gateway/me/session-reads`. Attachment previews fit the media
|
|
192
|
+
with one filename, Download, and Close in the shared AbstractUIC dialog.
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
## Avatar conversation view
|
|
196
|
+
|
|
197
|
+
Choose **Avatar** in the conversation header for a second view over the same work.
|
|
198
|
+
The avatar shows activity and, under it, one live line: the latest thing the assistant
|
|
199
|
+
reports while it works, replaced as soon as something new arrives (nothing cycles). Click
|
|
200
|
+
it to read it in full, then **Reply to this** to answer it or **Latest** to follow again.
|
|
201
|
+
The findings it reports (facts or evidence it established) accumulate in a
|
|
202
|
+
**Findings** panel beside the avatar, or a sheet you open with **Findings** on a phone; open
|
|
203
|
+
one to see the results it came from. The answer appears once, as a message, with Markdown,
|
|
204
|
+
links and files like the conversation.
|
|
205
|
+
**Conversation** restores the ordinary chat and technical details. Switching views
|
|
206
|
+
preserves the current draft and attachments.
|
|
207
|
+
|
|
208
|
+
Avatar permission requests use ordinary actions, destinations and labeled decision
|
|
209
|
+
icons. “Why I’m asking” shows the assistant’s public explanation when it belongs to
|
|
210
|
+
that exact request. “Review details” opens Conversation mode for technical arguments
|
|
211
|
+
and standing permissions. A missing explanation is stated explicitly.
|
|
212
|
+
|
|
213
|
+
While work continues, type or speak naturally to guide it. **Reply to this** on the live line pins a
|
|
214
|
+
specific public update as context. Sending a correction makes it available to the
|
|
215
|
+
assistant; the assistant's later response shows whether it adopted the correction.
|
|
216
|
+
Questions use their displayed answer controls. Permission requests require an explicit
|
|
217
|
+
choice; an ordinary message or silence grants no permission.
|
|
218
|
+
|
|
219
|
+
Essential questions keep waiting. A clarification may state a two-to-five minute
|
|
220
|
+
answer deadline and what will happen without an answer. **Conversation cues** controls
|
|
221
|
+
visual and audio alerts independently for questions, permission requests and completion.
|
|
222
|
+
Audio alerts default off. Voice narration uses the existing voice preference;
|
|
223
|
+
holding the live line, reading the findings sheet or editing a reply silences automatic narration. Playback can be paused
|
|
224
|
+
or silenced without stopping the task.
|
|
225
|
+
|
|
226
|
+
Public updates require a run started in Avatar mode and a backend supporting the public
|
|
227
|
+
conversation protocol. Switching an already active legacy run changes its presentation;
|
|
228
|
+
it does not manufacture missing updates. Tool details remain available in the ordinary
|
|
229
|
+
view. Public updates describe the assistant's communicated conclusions, not private
|
|
230
|
+
reasoning, and source references establish provenance rather than proof of correctness.
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createGatewayUrlResolver, parseAppFlagsOrExit } from "@abstractframework/app-server";
|
|
3
|
+
|
|
4
|
+
import { createAssistantServer } from "./server.js";
|
|
5
|
+
|
|
6
|
+
// The shared launch flags (abstractuic app-server): --gateway-url, --port,
|
|
7
|
+
// --host (default 127.0.0.1: the gateway serves this app at /apps/assistant-web/).
|
|
8
|
+
const flags = parseAppFlagsOrExit(process.argv.slice(2), {
|
|
9
|
+
appName: "AbstractAssistant Web",
|
|
10
|
+
command: "abstractassistant-web",
|
|
11
|
+
envPrefix: "ABSTRACTASSISTANT",
|
|
12
|
+
defaultPort: 3008,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const fixed = flags.gatewayUrlSource === "flag" || flags.gatewayUrlSource.startsWith("env:");
|
|
16
|
+
const server = createAssistantServer({ defaultGatewayUrl: fixed ? flags.gatewayUrl : createGatewayUrlResolver() });
|
|
17
|
+
|
|
18
|
+
// A port already in use (or a host this machine does not have) is a one-line
|
|
19
|
+
// error and exit code 1, never a stack trace: the gateway's app manager and a
|
|
20
|
+
// person at a terminal both read the first line.
|
|
21
|
+
server.on("error", (error) => {
|
|
22
|
+
const where = `${flags.host}:${flags.port}`;
|
|
23
|
+
const reason =
|
|
24
|
+
error?.code === "EADDRINUSE"
|
|
25
|
+
? `port ${flags.port} is already in use (choose another with --port)`
|
|
26
|
+
: error?.code === "EADDRNOTAVAIL"
|
|
27
|
+
? `${flags.host} is not an address of this computer (see --host)`
|
|
28
|
+
: String(error?.message || error);
|
|
29
|
+
console.error(`AbstractAssistant Web could not listen on ${where}: ${reason}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
server.listen(flags.port, flags.host, () => {
|
|
34
|
+
console.log(`AbstractAssistant Web on http://${flags.host}:${flags.port}/ (gateway ${flags.gatewayUrl}, from ${flags.gatewayUrlSource})`);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Stop promptly: close() alone waits for every open connection, and a live
|
|
38
|
+
// reply stream stays open for minutes, so the open sockets are closed too.
|
|
39
|
+
function shutdown() {
|
|
40
|
+
server.close(() => process.exit(0));
|
|
41
|
+
server.closeAllConnections?.();
|
|
42
|
+
}
|
|
43
|
+
process.on("SIGINT", shutdown);
|
|
44
|
+
process.on("SIGTERM", shutdown);
|
package/bin/server.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AbstractAssistant web server: the built app (`dist/`) and the
|
|
3
|
+
* app-origin gateway session proxy, on the shared app-server kit
|
|
4
|
+
* (`@abstractframework/app-server`).
|
|
5
|
+
*
|
|
6
|
+
* Ported from abstractcode/web/bin/server.js (same contract, fewer routes):
|
|
7
|
+
* - Serves under the gateway's `/apps/assistant-web/` as well as on its own
|
|
8
|
+
* port (`createMountedHandler`: base path from `X-Forwarded-Prefix`, the
|
|
9
|
+
* browser's address from a loopback peer's `X-Forwarded-For`, the identity
|
|
10
|
+
* header `X-AbstractFramework-App: assistant-web; mount=1`). The shell gets
|
|
11
|
+
* `<base href>` (`injectShell`), so every asset and API call resolves under
|
|
12
|
+
* the base.
|
|
13
|
+
* - `/api/connection/gateway` and `/api/gateway/*` go through the kit's
|
|
14
|
+
* session proxy (HttpOnly session cookie + CSRF twin at `Path=<base>/`,
|
|
15
|
+
* browser `Authorization` and cookies stripped, server-pinned gateway URL).
|
|
16
|
+
* The browser never holds a gateway token.
|
|
17
|
+
* - In front of the proxy, as in abstractcode: a browser mutation must come
|
|
18
|
+
* from this app's own origin, a sign-in must be JSON, and a sign-in over a
|
|
19
|
+
* loopback socket must name a loopback host (DNS-rebinding guard).
|
|
20
|
+
*
|
|
21
|
+
* The gateway catalog registers `assistant-web` separately from the desktop
|
|
22
|
+
* Assistant (`assistant`), with the cookie prefix `abstractassistant`.
|
|
23
|
+
*/
|
|
24
|
+
import * as http from "node:http";
|
|
25
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
26
|
+
import { isIP } from "node:net";
|
|
27
|
+
import { dirname, extname, join, resolve, sep } from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
createGatewaySessionProxy,
|
|
32
|
+
createMountedHandler,
|
|
33
|
+
injectShell,
|
|
34
|
+
requestContext,
|
|
35
|
+
socketPeerAddress,
|
|
36
|
+
} from "@abstractframework/app-server";
|
|
37
|
+
|
|
38
|
+
const BIN_DIR = dirname(fileURLToPath(import.meta.url));
|
|
39
|
+
const DEFAULT_DIST_DIR = resolve(BIN_DIR, "..", "dist");
|
|
40
|
+
/** The gateway's catalog id: `/apps/assistant-web/` and the identity header. */
|
|
41
|
+
export const APP_ID = "assistant-web";
|
|
42
|
+
/** Cookie / CSRF-header prefix: `abstractassistant_gateway_*`, `x-abstractassistant-csrf`. */
|
|
43
|
+
export const COOKIE_APP_ID = "abstractassistant";
|
|
44
|
+
const CONNECTION_PATH = "/api/connection/gateway";
|
|
45
|
+
const GATEWAY_PREFIX = "/api/gateway/";
|
|
46
|
+
const MIME_TYPES = {
|
|
47
|
+
".html": "text/html",
|
|
48
|
+
".js": "application/javascript",
|
|
49
|
+
".css": "text/css",
|
|
50
|
+
".json": "application/json",
|
|
51
|
+
".txt": "text/plain; charset=utf-8",
|
|
52
|
+
".png": "image/png",
|
|
53
|
+
".svg": "image/svg+xml",
|
|
54
|
+
".ico": "image/x-icon",
|
|
55
|
+
".webmanifest": "application/manifest+json",
|
|
56
|
+
".woff2": "font/woff2",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function sendJson(res, status, payload) {
|
|
60
|
+
if (res.headersSent) {
|
|
61
|
+
res.destroy();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
65
|
+
res.end(JSON.stringify(payload));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function firstHeader(value) {
|
|
69
|
+
return String(Array.isArray(value) ? value[0] : value || "")
|
|
70
|
+
.split(",", 1)[0]
|
|
71
|
+
.trim();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function mutatingMethod(method) {
|
|
75
|
+
return ["POST", "PUT", "PATCH", "DELETE"].includes(String(method || "GET").toUpperCase());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function hostnameOf(host) {
|
|
79
|
+
const raw = String(host || "").trim().toLowerCase();
|
|
80
|
+
if (raw.startsWith("[")) return raw.slice(1).split("]", 1)[0];
|
|
81
|
+
return (raw.match(/:/g) || []).length === 1 ? raw.split(":")[0] : raw;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** `localhost`, `*.localhost`, `::1` or a 127.x IP LITERAL (never a DNS name starting "127."). */
|
|
85
|
+
function isLoopbackHostname(host) {
|
|
86
|
+
const h = hostnameOf(host);
|
|
87
|
+
if (h === "localhost" || h.endsWith(".localhost") || h === "::1") return true;
|
|
88
|
+
return isIP(h) === 4 && h.startsWith("127.");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A browser mutation must come from the origin the browser sees this app at. */
|
|
92
|
+
function browserMutationAllowed(req, ctx) {
|
|
93
|
+
const origin = firstHeader(req.headers.origin);
|
|
94
|
+
if (origin) {
|
|
95
|
+
try {
|
|
96
|
+
return new URL(origin).origin === new URL(`${ctx.proto}://${ctx.host}`).origin;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return firstHeader(req.headers["sec-fetch-site"]).toLowerCase() !== "cross-site";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function jsonRequest(req) {
|
|
105
|
+
return firstHeader(req.headers["content-type"]).split(";", 1)[0].trim().toLowerCase() === "application/json";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isLoopbackPeerSocket(req) {
|
|
109
|
+
const peer = socketPeerAddress(req);
|
|
110
|
+
return peer === "::1" || peer.startsWith("127.");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function trustProxyDeclared() {
|
|
114
|
+
return ["ABSTRACTASSISTANT_TRUST_PROXY_HEADERS", "ABSTRACTGATEWAY_TRUST_PROXY_HEADERS"].some((name) =>
|
|
115
|
+
["1", "true", "yes", "y", "on"].includes(String(process.env[name] || "").trim().toLowerCase()),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function pathnameOf(req) {
|
|
120
|
+
return new URL(req.url || "/", "http://abstractassistant.local").pathname;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The gateway half: connection API + `/api/gateway/*`, guarded, then the kit
|
|
125
|
+
* proxy. Connect-compatible (`next` for every other path), so the Vite dev
|
|
126
|
+
* server mounts the SAME code.
|
|
127
|
+
*/
|
|
128
|
+
export function createGatewayMiddleware(options = {}) {
|
|
129
|
+
const proxy = createGatewaySessionProxy({
|
|
130
|
+
appId: COOKIE_APP_ID,
|
|
131
|
+
...(options.defaultGatewayUrl ? { defaultGatewayUrl: options.defaultGatewayUrl } : {}),
|
|
132
|
+
connectionPath: CONNECTION_PATH,
|
|
133
|
+
proxyPrefix: GATEWAY_PREFIX,
|
|
134
|
+
});
|
|
135
|
+
function middleware(req, res, next) {
|
|
136
|
+
let pathname;
|
|
137
|
+
try {
|
|
138
|
+
pathname = pathnameOf(req);
|
|
139
|
+
} catch {
|
|
140
|
+
sendJson(res, 400, { detail: "Invalid request URL" });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const owned = pathname === CONNECTION_PATH || pathname.startsWith(GATEWAY_PREFIX);
|
|
144
|
+
if (!owned) {
|
|
145
|
+
if (typeof next === "function") next();
|
|
146
|
+
else sendJson(res, 404, { detail: "Not found" });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
let ctx;
|
|
150
|
+
try {
|
|
151
|
+
ctx = requestContext(req);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
sendJson(res, Number(error?.status) || 400, { detail: String(error?.message || error) });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (
|
|
157
|
+
pathname === CONNECTION_PATH &&
|
|
158
|
+
mutatingMethod(req.method) &&
|
|
159
|
+
isLoopbackPeerSocket(req) &&
|
|
160
|
+
!isLoopbackHostname(req.headers.host) &&
|
|
161
|
+
!trustProxyDeclared()
|
|
162
|
+
) {
|
|
163
|
+
sendJson(res, 403, {
|
|
164
|
+
detail: `Sign-in refused: this local app was reached under the host name ${hostnameOf(req.headers.host) || "(none)"}, not a loopback address.`,
|
|
165
|
+
reason_code: "host_not_allowed",
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (mutatingMethod(req.method) && !browserMutationAllowed(req, ctx)) {
|
|
170
|
+
sendJson(res, 403, { detail: "Cross-origin browser requests are not allowed", reason_code: "origin_required" });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (pathname === CONNECTION_PATH && req.method === "POST" && !jsonRequest(req)) {
|
|
174
|
+
sendJson(res, 415, { detail: "Gateway connection requests require Content-Type: application/json" });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
proxy.handle(req, res, pathname);
|
|
178
|
+
}
|
|
179
|
+
middleware.proxy = proxy;
|
|
180
|
+
return middleware;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function unsafeStaticPath(req) {
|
|
184
|
+
const rawPath = String(req.url || "/").split("?", 1)[0];
|
|
185
|
+
let decoded;
|
|
186
|
+
try {
|
|
187
|
+
decoded = decodeURIComponent(rawPath);
|
|
188
|
+
} catch {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
return decoded.includes("\0") || decoded.split(/[\\/]+/).includes("..");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function isFile(filePath) {
|
|
195
|
+
try {
|
|
196
|
+
return existsSync(filePath) && statSync(filePath).isFile();
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function serveFile(res, filePath) {
|
|
203
|
+
if (!isFile(filePath)) return false;
|
|
204
|
+
res.writeHead(200, {
|
|
205
|
+
"Content-Type": MIME_TYPES[extname(filePath).toLowerCase()] || "application/octet-stream",
|
|
206
|
+
"Cache-Control": "no-cache",
|
|
207
|
+
});
|
|
208
|
+
res.end(readFileSync(filePath));
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function serveShell(res, distDir, basePath) {
|
|
213
|
+
const index = join(distDir, "index.html");
|
|
214
|
+
if (!isFile(index)) return false;
|
|
215
|
+
// `host: "browser"` tells the frontend which host adapter to build (src/host/).
|
|
216
|
+
const html = injectShell(readFileSync(index, "utf8"), { basePath, config: { host: "browser" } });
|
|
217
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" });
|
|
218
|
+
res.end(html);
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function createAssistantRequestHandler(options = {}) {
|
|
223
|
+
const distDir = resolve(options.distDir || DEFAULT_DIST_DIR);
|
|
224
|
+
const gatewayMiddleware = createGatewayMiddleware(options);
|
|
225
|
+
return createMountedHandler({ appId: APP_ID }, (req, res, ctx) => {
|
|
226
|
+
gatewayMiddleware(req, res, () => {
|
|
227
|
+
let pathname;
|
|
228
|
+
try {
|
|
229
|
+
pathname = decodeURIComponent(pathnameOf(req));
|
|
230
|
+
} catch {
|
|
231
|
+
res.writeHead(400);
|
|
232
|
+
res.end("Bad Request");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (pathname === "/api" || pathname.startsWith("/api/")) {
|
|
236
|
+
sendJson(res, 404, { detail: "Not found" });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (unsafeStaticPath(req)) {
|
|
240
|
+
res.writeHead(400);
|
|
241
|
+
res.end("Bad Request");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (pathname === "/" || pathname === "/index.html") {
|
|
245
|
+
if (serveShell(res, distDir, ctx.basePath)) return;
|
|
246
|
+
}
|
|
247
|
+
const filePath = resolve(distDir, pathname.replace(/^\/+/, ""));
|
|
248
|
+
if (filePath !== distDir && !filePath.startsWith(`${distDir}${sep}`)) {
|
|
249
|
+
res.writeHead(400);
|
|
250
|
+
res.end("Bad Request");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (serveFile(res, filePath)) return;
|
|
254
|
+
if (serveShell(res, distDir, ctx.basePath)) return;
|
|
255
|
+
res.writeHead(404);
|
|
256
|
+
res.end("Not Found");
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function createAssistantServer(options = {}) {
|
|
262
|
+
return http.createServer(createAssistantRequestHandler(options));
|
|
263
|
+
}
|
|
Binary file
|