acrylblends 1.0.10 → 1.2.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/README.md +9 -0
- package/dist/index.d.ts +60 -2
- package/dist/index.js +1992 -33
- package/dist/index.js.map +1 -1
- package/docs/framework/agents/index.md +1 -1
- package/docs/framework/api/summary.md +1 -1
- package/docs/framework/getting-started/desktop.md +23 -4
- package/docs/framework/guides/create-acrylblends.md +1 -1
- package/docs/framework/guides/production.md +20 -0
- package/docs/framework/workshop/README.md +174 -285
- package/docs/framework/workshop/facilitator.md +81 -90
- package/legacy/README.md +9 -0
- package/legacy/agent/157ce3db1112/src/app.ts +98 -0
- package/legacy/agent/157ce3db1112/src/routes.ts +63 -0
- package/legacy/agent/24c681a00fca/src/app.ts +160 -0
- package/legacy/agent/24c681a00fca/src/routes.ts +83 -0
- package/legacy/agent/2e790da977b2/src/app.ts +98 -0
- package/legacy/agent/2e790da977b2/src/routes.ts +63 -0
- package/legacy/agent/6307045545dc/src/app.ts +168 -0
- package/legacy/agent/6307045545dc/src/routes.ts +83 -0
- package/legacy/agent/6a5fb9c15c53/src/app.ts +158 -0
- package/legacy/agent/6a5fb9c15c53/src/routes.ts +83 -0
- package/legacy/agent/71fb54a5a942/src/app.ts +111 -0
- package/legacy/agent/71fb54a5a942/src/routes.ts +63 -0
- package/legacy/agent/74ece148269c/src/app.ts +168 -0
- package/legacy/agent/74ece148269c/src/routes.ts +93 -0
- package/legacy/agent/855359304707/src/app.ts +168 -0
- package/legacy/agent/855359304707/src/routes.ts +126 -0
- package/legacy/agent/b5ef3c786ae4/src/app.ts +127 -0
- package/legacy/agent/b5ef3c786ae4/src/routes.ts +76 -0
- package/legacy/agent/c2f30958043b/src/app.ts +98 -0
- package/legacy/agent/c2f30958043b/src/routes.ts +53 -0
- package/legacy/blank/5a91c558af22/src/routes.ts +18 -0
- package/legacy/blank/9c650cf52bf3/src/routes.ts +14 -0
- package/legacy/blank/aea589283445/src/routes.ts +16 -0
- package/legacy/blank/b2cbb759c889/src/routes.ts +16 -0
- package/legacy/blank/d6d371c9a65e/src/routes.ts +18 -0
- package/legacy/chat/7076cc821e2c/src/app.ts +72 -0
- package/legacy/chat/7076cc821e2c/src/routes.ts +13 -0
- package/legacy/chat/7dbe6d5170df/src/app.ts +107 -0
- package/legacy/chat/7dbe6d5170df/src/routes.ts +19 -0
- package/legacy/chat/a9a9ab44c58e/src/app.ts +107 -0
- package/legacy/chat/a9a9ab44c58e/src/routes.ts +15 -0
- package/legacy/chat/f20ad9a51b06/src/app.ts +106 -0
- package/legacy/chat/f20ad9a51b06/src/routes.ts +15 -0
- package/legacy/layouts.json +524 -0
- package/legacy/nodejs/1d477f37aaf4/src/app.ts +92 -0
- package/legacy/nodejs/1d477f37aaf4/src/routes.ts +40 -0
- package/legacy/nodejs/b1bd9462496c/src/app.ts +61 -0
- package/legacy/nodejs/b1bd9462496c/src/routes.ts +37 -0
- package/legacy/nodejs/d5bab75076ef/src/app.ts +95 -0
- package/legacy/nodejs/d5bab75076ef/src/routes.ts +40 -0
- package/legacy/notes/4edc36949c15/src/app.ts +107 -0
- package/legacy/notes/4edc36949c15/src/routes.ts +14 -0
- package/legacy/notes/6cab2d09747c/src/app.ts +72 -0
- package/legacy/notes/6cab2d09747c/src/routes.ts +12 -0
- package/legacy/notes/7ca2dfed623a/src/app.ts +106 -0
- package/legacy/notes/7ca2dfed623a/src/routes.ts +14 -0
- package/legacy/todo/02e613b3625c/src/app.ts +71 -0
- package/legacy/todo/02e613b3625c/src/todo.ts +123 -0
- package/legacy/todo/45694a8d9e15/src/app.ts +71 -0
- package/legacy/todo/45694a8d9e15/src/todo.ts +96 -0
- package/legacy/todo/5923d3a6099f/src/app.ts +71 -0
- package/legacy/todo/5923d3a6099f/src/todo.ts +125 -0
- package/legacy/todo/849a18d66dc1/src/app.ts +91 -0
- package/legacy/todo/849a18d66dc1/src/todo.ts +125 -0
- package/legacy/todo/8e64211312f3/src/app.ts +71 -0
- package/legacy/todo/8e64211312f3/src/todo.ts +125 -0
- package/legacy/todo/b79078a87565/src/app.ts +71 -0
- package/legacy/todo/b79078a87565/src/todo.ts +125 -0
- package/lib/{audit-Dw-siYTp.d.ts → audit-Bok_UNCn.d.ts} +41 -6
- package/lib/{client-modules-drj4mjhj.js → client-modules-Bkdr83jO.js} +1 -1
- package/lib/cordis/door.d.ts +44 -7
- package/lib/cordis/door.js +214 -60
- package/lib/cordis/inspect.js +1 -1
- package/lib/cordis/node-server.d.ts +101 -3
- package/lib/cordis/node-server.js +3 -3
- package/lib/cordis/router-http.d.ts +7 -1
- package/lib/cordis/router-http.js +44 -3
- package/lib/cordis/router.d.ts +1 -1
- package/lib/cordis/router.js +1 -1
- package/lib/cordis/state.d.ts +3 -2
- package/lib/cordis/state.js +371 -7
- package/lib/door/host.d.ts +2 -2
- package/lib/door/host.js +1 -1
- package/lib/door/page.d.ts +20 -1
- package/lib/door/page.js +75 -14
- package/lib/door/protocol.d.ts +1 -1
- package/lib/drivers.d.ts +10 -1
- package/lib/{index-DMOtA-NJ.d.ts → index-DGq7H0lb.d.ts} +11 -1
- package/lib/node-server.d.ts +7 -1
- package/lib/node-server.js +2 -2
- package/lib/{protocol-a2aqrp2X.d.ts → protocol-BeIYCgTo.d.ts} +4 -0
- package/lib/{snapshot-text-BfEbEOjA.js → snapshot-text-CXTnjxGx.js} +121 -9
- package/lib/src-CtkIX_Gg.js +427 -0
- package/lib/{src-BTBm4OPD.js → src-iBJ2ysV9.js} +77 -14
- package/lib/{test-websocket-BLeEvybj.js → test-websocket-CQOSvBth.js} +17 -7
- package/lib/{test-websocket-DyeS50Tr.d.ts → test-websocket-GUTAkFQT.d.ts} +3 -1
- package/package.json +2 -1
- package/plugins/acryl-door/src/host/channel.ts +124 -9
- package/plugins/acryl-door/src/host/chat-tools.ts +8 -6
- package/plugins/acryl-door/src/host/door-file.ts +7 -2
- package/plugins/acryl-door/src/host/door.ts +58 -23
- package/plugins/acryl-door/src/host/page-routes.ts +3 -1
- package/plugins/acryl-door/src/host/page-socket.ts +18 -8
- package/plugins/acryl-door/src/host/verbs.ts +3 -0
- package/plugins/acryl-door/src/index.ts +166 -29
- package/plugins/acryl-door/src/page/base.ts +32 -0
- package/plugins/acryl-door/src/page/channel.ts +11 -1
- package/plugins/acryl-door/src/page/driver/accessibility.ts +11 -0
- package/plugins/acryl-door/src/page/driver/actions.ts +4 -0
- package/plugins/acryl-door/src/page/driver/driver.ts +49 -7
- package/plugins/acryl-door/src/page/driver/snapshot.ts +5 -2
- package/plugins/acryl-door/src/page/index.ts +46 -2
- package/plugins/acryl-door/src/page/indicator.ts +14 -6
- package/plugins/acryl-door/src/page/outline.ts +56 -0
- package/plugins/acryl-door/src/page/protocol.ts +4 -0
- package/plugins/acryl-door/src/page/snapshot-text.ts +2 -1
- package/plugins/acryl-node-server/src/adapter/listener.ts +4 -0
- package/plugins/acryl-node-server/src/adapter/raw.ts +18 -0
- package/plugins/acryl-node-server/src/adapter/websocket.ts +15 -9
- package/plugins/acryl-node-server/src/client-modules.ts +7 -1
- package/plugins/acryl-node-server/src/index.ts +304 -16
- package/plugins/acryl-node-server/src/upgrade-hub.ts +2 -2
- package/plugins/acryl-page/plugin.json +12 -0
- package/plugins/acryl-page/src/index.ts +163 -0
- package/plugins/acryl-router/src/service.ts +77 -14
- package/plugins/acryl-router-http/src/index.ts +15 -1
- package/plugins/acryl-router-http/src/serve.ts +31 -0
- package/plugins/acryl-state/plugin.json +4 -3
- package/plugins/acryl-state/src/client-source.ts +244 -0
- package/plugins/acryl-state/src/service.ts +33 -6
- package/plugins/acryl-state/src/socket.ts +96 -0
- package/plugins/acryl-web-server/plugin.json +14 -0
- package/plugins/acryl-web-server/src/index.ts +164 -0
- package/plugins/agent/agent.ts +38 -7
- package/plugins/agent/index.ts +1 -1
- package/plugins/agent/model.ts +10 -2
- package/plugins/agent/tools.ts +9 -3
- package/plugins/agent-panel/client/describe.ts +57 -0
- package/plugins/agent-panel/client/driver-pick.ts +22 -0
- package/plugins/agent-panel/client/frame.ts +51 -0
- package/plugins/agent-panel/client/main.ts +343 -81
- package/plugins/agent-panel/drivers.ts +7 -2
- package/plugins/agent-panel/headless.ts +5 -2
- package/plugins/agent-panel/index.ts +187 -61
- package/plugins/agent-panel/mount.ts +27 -0
- package/plugins/agent-panel/visitors.ts +231 -0
- package/plugins/desktop/frame.ts +231 -0
- package/plugins/desktop/launch.ts +44 -9
- package/plugins/desktop/main.ts +100 -32
- package/plugins/desktop/window.ts +21 -4
- package/plugins/driver-pty/client/terminal.ts +4 -1
- package/plugins/driver-pty/index.ts +38 -11
- package/plugins/driver-pty/spawn.ts +38 -0
- package/plugins/driver-pty/terminal.ts +2 -0
- package/plugins/extensions/index.ts +26 -1
- package/plugins/extensions/resolve.ts +13 -0
- package/plugins/feedback/client.d.ts +22 -0
- package/plugins/feedback/client.js +471 -0
- package/plugins/feedback/index.ts +143 -0
- package/plugins/feedback/plugin.json +11 -0
- package/plugins/feedback/store.ts +307 -0
- package/plugins/home/index.ts +122 -0
- package/plugins/home/plugin.json +11 -0
- package/plugins/instance/index.ts +12 -3
- package/plugins/manifest/app-plugins.ts +86 -0
- package/plugins/manifest/index.ts +12 -2
- package/plugins/manifest/snapshot.ts +275 -0
- package/plugins/openai/index.ts +64 -5
- package/plugins/openai/prompt.ts +23 -0
- package/shared/AGENTS.core.md +36 -16
- package/shared/skills/acryl-add-ui/SKILL.md +19 -0
- package/shared/skills/acryl-app/SKILL.md +13 -0
- package/shared/skills/acryl-build-plugin/SKILL.md +22 -0
- package/shared/skills/acryl-change-plugin/SKILL.md +14 -0
- package/shared/skills/acryl-diagnose-plugin/SKILL.md +13 -0
- package/shared/skills/acryl-fix-plugin/SKILL.md +13 -0
- package/shared/skills/acryl-remove-plugin/SKILL.md +12 -0
- package/shared/skills/acryl-share-plugin/SKILL.md +16 -0
- package/shared/skills/cordis-plugin/SKILL.md +26 -0
- package/templates/agent/AGENTS.md +4 -2
- package/templates/agent/blend.yaml +4 -0
- package/templates/agent/gitignore +1 -0
- package/templates/agent/package.json +1 -0
- package/templates/agent/src/app.ts +150 -33
- package/templates/agent/src/frame.ts +17 -0
- package/templates/agent/src/main.ts +5 -3
- package/templates/agent/src/routes.ts +73 -21
- package/templates/agent/template.json +2 -0
- package/templates/agent/test/app.test.ts +83 -5
- package/templates/blank/AGENTS.md +3 -2
- package/templates/blank/blend.yaml +11 -0
- package/templates/blank/gitignore +1 -0
- package/templates/blank/package.json +1 -0
- package/templates/blank/src/app.ts +100 -29
- package/templates/blank/src/door.ts +7 -0
- package/templates/blank/src/first-page.ts +5 -0
- package/templates/blank/src/frame.ts +17 -0
- package/templates/blank/src/routes.ts +65 -8
- package/templates/blank/template.json +14 -1
- package/templates/blank/test/app.test.ts +103 -16
- package/templates/chat/blend.yaml +2 -1
- package/templates/chat/gitignore +1 -0
- package/templates/chat/package.json +1 -0
- package/templates/chat/src/app.ts +72 -22
- package/templates/chat/src/chat-app.ts +11 -2
- package/templates/chat/src/client/chat.tsx +4 -2
- package/templates/chat/src/frame.ts +17 -0
- package/templates/chat/src/main.ts +1 -6
- package/templates/chat/src/page.tsx +6 -4
- package/templates/chat/src/routes.ts +14 -4
- package/templates/chat/src/runtime.ts +9 -3
- package/templates/chat/template.json +3 -1
- package/templates/chat/test/chat.test.ts +46 -1
- package/templates/chat/test/server.test.ts +17 -3
- package/templates/nodejs/blend.yaml +2 -1
- package/templates/nodejs/gitignore +1 -0
- package/templates/nodejs/package.json +1 -0
- package/templates/nodejs/src/app.ts +76 -21
- package/templates/nodejs/src/frame.ts +17 -0
- package/templates/nodejs/src/main.ts +1 -6
- package/templates/nodejs/src/page.tsx +3 -3
- package/templates/nodejs/src/routes.ts +32 -13
- package/templates/nodejs/template.json +3 -1
- package/templates/nodejs/test/app.test.ts +38 -3
- package/templates/nodejs/test/server.test.ts +17 -3
- package/templates/notes/blend.yaml +2 -1
- package/templates/notes/gitignore +1 -0
- package/templates/notes/package.json +1 -0
- package/templates/notes/src/app.ts +72 -22
- package/templates/notes/src/frame.ts +17 -0
- package/templates/notes/src/main.ts +1 -6
- package/templates/notes/src/notes-app.ts +24 -17
- package/templates/notes/src/routes.ts +11 -4
- package/templates/notes/src/runtime.ts +9 -3
- package/templates/notes/src/views.tsx +26 -17
- package/templates/notes/template.json +3 -1
- package/templates/notes/test/notes.test.ts +39 -1
- package/templates/notes/test/server.test.ts +17 -3
- package/templates/todo/blend.yaml +3 -0
- package/templates/todo/gitignore +1 -0
- package/templates/todo/package.json +1 -0
- package/templates/todo/src/app.ts +81 -27
- package/templates/todo/src/door.ts +7 -0
- package/templates/todo/src/frame.ts +17 -0
- package/templates/todo/src/page.ts +13 -6
- package/templates/todo/src/todo.ts +18 -6
- package/templates/todo/template.json +11 -1
- package/templates/todo/test/todo.test.ts +67 -9
- package/lib/src-LX0E4ec4.js +0 -117
- package/shared/SKILL.md +0 -14
|
@@ -112,8 +112,11 @@ export function agentDriver(ctx: Context): ChatDriver {
|
|
|
112
112
|
return {
|
|
113
113
|
kind: 'chat',
|
|
114
114
|
name: 'agent',
|
|
115
|
-
async *start({ message, signal }) {
|
|
116
|
-
const result = await ctx.agent.run(message, {
|
|
115
|
+
async *start({ message, signal, page }) {
|
|
116
|
+
const result = await ctx.agent.run(message, {
|
|
117
|
+
signal,
|
|
118
|
+
...(page === undefined ? {} : { page }),
|
|
119
|
+
})
|
|
117
120
|
for (const call of result.trace)
|
|
118
121
|
yield {
|
|
119
122
|
event: 'tool',
|
|
@@ -7,9 +7,15 @@
|
|
|
7
7
|
* - `GET /api/agent-panel/session` gives the page the drivers it may use and a token; `POST /api/agent-panel/ask` runs one message and answers server-sent events:
|
|
8
8
|
* `text`, `tool`, `change`, `done`, `error`.
|
|
9
9
|
* - Only the app's own page may ask (a same-origin request carrying the token), one run at a time, and every ask is written to `.acryl/audit/agent-panel.jsonl`.
|
|
10
|
-
* -
|
|
10
|
+
* - Who asks (`visitors.ts`): in development one person, the owner, whose pages share one token and one run. As the Assistant each visitor has their own token, run and
|
|
11
|
+
* replay, named by an HttpOnly cookie: no visitor sees, follows or stops another's run, and the asks are bounded (per visitor per hour, per day, runs at once).
|
|
12
|
+
* - The routes live under the app's mount: the root of the app's own server, or the frame's prefix (`/app`) when the frame hosts it, where the host's own
|
|
13
|
+
* `/api` routes would otherwise answer first. The row's `mount` config names it.
|
|
14
|
+
* - Development only: the plugin does nothing with `NODE_ENV=production` or `ACRYL_DOOR=off`, except as the Assistant (`assistant: true`): then it runs in production too,
|
|
15
|
+
* docked open on the right, with the app's own agent only. The app gives that agent the page tools alone and a page-only door, so it uses the app for a visitor and
|
|
16
|
+
* cannot change it (src/app.ts, `ACRYL_ASSISTANT=on`). In the ACRYL frame's canvas the panel follows the frame's view: Build hides it, and Preview shows it only as
|
|
17
|
+
* the Assistant and only when the app's production environment names one (`assistantInProduction` in the session).
|
|
11
18
|
*/
|
|
12
|
-
import { randomBytes, timingSafeEqual } from 'node:crypto'
|
|
13
19
|
import { appendFileSync, mkdirSync } from 'node:fs'
|
|
14
20
|
import { dirname, join } from 'node:path'
|
|
15
21
|
import type { Context } from '@deepseek-ai/cordis'
|
|
@@ -19,7 +25,7 @@ import { Acryl, type MiddlewareHandler } from '../acryl-router/src/index.ts'
|
|
|
19
25
|
import { loopbackAuthority } from '../acryl-router/src/middleware/index.ts'
|
|
20
26
|
import type {} from '../agent/index.ts'
|
|
21
27
|
import type {} from '../extensions/index.ts'
|
|
22
|
-
import type { ChatDriver } from './drivers.ts'
|
|
28
|
+
import type { ChatDriver, Driver } from './drivers.ts'
|
|
23
29
|
import {
|
|
24
30
|
agentDriver,
|
|
25
31
|
availableHeadless,
|
|
@@ -27,7 +33,9 @@ import {
|
|
|
27
33
|
headlessDriver,
|
|
28
34
|
whyNotChat,
|
|
29
35
|
} from './headless.ts'
|
|
36
|
+
import { mountedBase, mountedPrefix } from './mount.ts'
|
|
30
37
|
import { provideDrivers } from './registry.ts'
|
|
38
|
+
import { type AssistantLimits, type Visitor, Visitors } from './visitors.ts'
|
|
31
39
|
|
|
32
40
|
export type {
|
|
33
41
|
ChatDriver,
|
|
@@ -41,14 +49,40 @@ export type {
|
|
|
41
49
|
export const name = 'agent-panel'
|
|
42
50
|
export const inject = ['router', 'agent', 'extensions', 'appInstance']
|
|
43
51
|
|
|
44
|
-
|
|
52
|
+
/** The panel's own routes, fixed relative to the app's mount. */
|
|
53
|
+
const PANEL_PATH = '/api/agent-panel'
|
|
54
|
+
|
|
55
|
+
/** The one driver the Assistant offers its visitors: the app's own agent, held to the page tools. */
|
|
56
|
+
const ASSISTANT_DRIVER = 'agent'
|
|
57
|
+
|
|
58
|
+
/** The slice of the door the panel uses; the door is optional (it is a development plugin, and a different row). */
|
|
59
|
+
interface DoorPort {
|
|
60
|
+
driveFor(windowId: string): () => void
|
|
61
|
+
}
|
|
45
62
|
const CLIENT_DIR = join(import.meta.dirname, 'client')
|
|
46
63
|
|
|
47
64
|
export interface PanelConfig {
|
|
48
65
|
/** Headless coding agents to offer besides the app's own agent; default: the ones found on the machine. */
|
|
49
66
|
drivers?: readonly HeadlessName[]
|
|
67
|
+
/**
|
|
68
|
+
* The URL prefix the app is mounted at: empty on the app's own server, `/app` when the ACRYL frame hosts it (the same prefix `makeApp` and
|
|
69
|
+
* `routerHttp.mount` take). The panel's routes, script tag and client live under it, so they belong to the app and are served wherever it is.
|
|
70
|
+
*/
|
|
71
|
+
mount?: string
|
|
72
|
+
/** The Assistant: a production panel for the app's visitors, with the app's own agent only (no coding agent, no terminal). Runs with `NODE_ENV=production`. */
|
|
73
|
+
assistant?: boolean
|
|
74
|
+
/** The Assistant's limits on what visitors may ask (`visitors.ts` has the defaults); the development panel has none but one run at a time. */
|
|
75
|
+
limits?: AssistantLimits
|
|
50
76
|
}
|
|
51
77
|
|
|
78
|
+
/** Whether the page came over https (directly, or through a proxy that says so): the visitor cookie is then Secure. */
|
|
79
|
+
const overHttps = (url: string, headers: Headers): boolean =>
|
|
80
|
+
url.startsWith('https:') || headers.get('x-forwarded-proto')?.split(',')[0]?.trim() === 'https'
|
|
81
|
+
|
|
82
|
+
/** The panel's routes under a checked mount: empty, or an absolute path such as `/app` with no trailing slash (the `routerHttp.mount` rule). */
|
|
83
|
+
const panelPrefix = (mount: string | undefined): string =>
|
|
84
|
+
mountedPrefix(mount, PANEL_PATH, 'agent-panel')
|
|
85
|
+
|
|
52
86
|
/** Whether a request comes from the app's own page: a browser says so itself (`Sec-Fetch-Site: same-origin`), or the Origin is the Host. */
|
|
53
87
|
export function fromOwnPage(headers: Headers): boolean {
|
|
54
88
|
const site = headers.get('sec-fetch-site')
|
|
@@ -59,9 +93,24 @@ export function fromOwnPage(headers: Headers): boolean {
|
|
|
59
93
|
}
|
|
60
94
|
|
|
61
95
|
export function apply(ctx: Context, config: PanelConfig = {}): void {
|
|
62
|
-
|
|
96
|
+
const assistant = config.assistant === true
|
|
97
|
+
if (process.env.ACRYL_DOOR === 'off' || (process.env.NODE_ENV === 'production' && !assistant))
|
|
98
|
+
return
|
|
99
|
+
const PREFIX = panelPrefix(config.mount)
|
|
100
|
+
// The panel's routes mount at the app's own mount and keep the fixed path inside (`/app` + `/api/agent-panel/...`); the frame's router prefixes the
|
|
101
|
+
// mount path itself, so a prefix inside the routes too would become `/app/app/...`. PREFIX stays absolute for the page's script tag and client URLs.
|
|
102
|
+
const MOUNT = mountedBase(config.mount, 'agent-panel')
|
|
63
103
|
const root = ctx.appInstance.root
|
|
64
|
-
const
|
|
104
|
+
const visitors = new Visitors<Run>({
|
|
105
|
+
shared: !assistant,
|
|
106
|
+
...(config.limits === undefined ? {} : { limits: config.limits }),
|
|
107
|
+
})
|
|
108
|
+
/** The visitor of a request that must carry its token, or `undefined` when it does not come from the app's own page with the right one. */
|
|
109
|
+
const caller = (headers: Headers, presented: unknown): Visitor<Run> | undefined => {
|
|
110
|
+
if (!fromOwnPage(headers)) return undefined
|
|
111
|
+
const visitor = visitors.of(headers.get('cookie'))
|
|
112
|
+
return visitors.accepts(visitor, presented) ? visitor : undefined
|
|
113
|
+
}
|
|
65
114
|
const auditFile = join(root, '.acryl', 'audit', 'agent-panel.jsonl')
|
|
66
115
|
const audit = (entry: Record<string, unknown>): void => {
|
|
67
116
|
try {
|
|
@@ -71,13 +120,17 @@ export function apply(ctx: Context, config: PanelConfig = {}): void {
|
|
|
71
120
|
// The audit never stops a run.
|
|
72
121
|
}
|
|
73
122
|
}
|
|
123
|
+
// The Assistant's visitors use the app's own agent and nothing else: no plugin's driver (a terminal, a coding agent) is open to them, whatever plugins the app mounts.
|
|
74
124
|
const registry = provideDrivers(
|
|
75
125
|
ctx,
|
|
76
|
-
(headers) =>
|
|
126
|
+
(headers) => !assistant && caller(headers, headers.get('x-agent-panel-token')) !== undefined,
|
|
77
127
|
)
|
|
128
|
+
const offered = (): Driver[] =>
|
|
129
|
+
registry.list().filter((driver) => !assistant || driver.name === ASSISTANT_DRIVER)
|
|
78
130
|
ctx.effect(() => registry.register(agentDriver(ctx)))
|
|
79
|
-
|
|
80
|
-
|
|
131
|
+
if (!assistant)
|
|
132
|
+
for (const program of availableHeadless(config.drivers))
|
|
133
|
+
ctx.effect(() => registry.register(headlessDriver(program, root)))
|
|
81
134
|
|
|
82
135
|
const tag = `<script type="module" src="${PREFIX}/panel.js"></script>`
|
|
83
136
|
const addScript = async (c: { res: Response }, next: () => Promise<void>): Promise<void> => {
|
|
@@ -105,58 +158,95 @@ export function apply(ctx: Context, config: PanelConfig = {}): void {
|
|
|
105
158
|
if (wrapped) return next()
|
|
106
159
|
await addScript(c, next)
|
|
107
160
|
})
|
|
108
|
-
.get(`${
|
|
109
|
-
c.body(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
161
|
+
.get(`${PANEL_PATH}/panel.js`, (c) =>
|
|
162
|
+
c.body(
|
|
163
|
+
`import { start } from '${PREFIX}/client/main.ts'\nstart(${JSON.stringify(PREFIX)})\n`,
|
|
164
|
+
200,
|
|
165
|
+
{
|
|
166
|
+
'content-type': 'text/javascript; charset=utf-8',
|
|
167
|
+
'cache-control': 'no-cache',
|
|
168
|
+
},
|
|
169
|
+
),
|
|
113
170
|
)
|
|
114
|
-
.route(`${
|
|
115
|
-
.get(`${
|
|
171
|
+
.route(`${PANEL_PATH}/client`, clientModules({ root: CLIENT_DIR, mount: `${PREFIX}/client` }))
|
|
172
|
+
.get(`${PANEL_PATH}/session`, (c) => {
|
|
116
173
|
if (!fromOwnPage(c.req.raw.headers))
|
|
117
174
|
return c.json({ error: "only the app's own page may use the panel" }, 403)
|
|
175
|
+
const { visitor, setCookie } = visitors.session(
|
|
176
|
+
c.req.header('cookie'),
|
|
177
|
+
overHttps(c.req.url, c.req.raw.headers),
|
|
178
|
+
)
|
|
179
|
+
if (setCookie !== undefined) c.header('set-cookie', setCookie)
|
|
118
180
|
return c.json({
|
|
119
|
-
token,
|
|
120
|
-
|
|
181
|
+
token: visitor.token,
|
|
182
|
+
...(assistant ? { assistant: true } : {}),
|
|
183
|
+
// The frame's Preview shows the app as production would. This is the fact that lets its page show the Assistant when the app's production
|
|
184
|
+
// environment names it (ACRYL_ASSISTANT=on), even though the frame host itself runs in development and never mounts the Assistant row.
|
|
185
|
+
...(process.env.ACRYL_ASSISTANT === 'on' ? { assistantInProduction: true } : {}),
|
|
186
|
+
// A terminal driver names the browser module that draws its view and the prefix its endpoints live under: the plugin that owns the view
|
|
187
|
+
// also knows where the app is mounted, so the panel never guesses either.
|
|
188
|
+
drivers: offered().map((driver) => ({
|
|
189
|
+
name: driver.name,
|
|
190
|
+
kind: driver.kind,
|
|
191
|
+
...(driver.kind === 'terminal' ? { view: driver.view } : {}),
|
|
192
|
+
})),
|
|
121
193
|
})
|
|
122
194
|
})
|
|
123
|
-
.post(`${
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const body = (await c.req.json().catch(() => ({}))) as {
|
|
127
|
-
|
|
195
|
+
.post(`${PANEL_PATH}/ask`, async (c) => {
|
|
196
|
+
const visitor = caller(c.req.raw.headers, c.req.header('x-agent-panel-token'))
|
|
197
|
+
if (visitor === undefined) return c.json({ error: "only the app's own page may ask" }, 403)
|
|
198
|
+
const body = (await c.req.json().catch(() => ({}))) as {
|
|
199
|
+
message?: unknown
|
|
200
|
+
driver?: unknown
|
|
201
|
+
page?: unknown
|
|
202
|
+
}
|
|
203
|
+
const checked = checkAsk(visitor, body.message, body.driver)
|
|
128
204
|
if ('error' in checked) return c.json({ error: checked.error }, checked.status)
|
|
129
|
-
const run = startRun(
|
|
205
|
+
const run = startRun(
|
|
206
|
+
visitor,
|
|
207
|
+
checked.driverName,
|
|
208
|
+
checked.message,
|
|
209
|
+
checked.driver,
|
|
210
|
+
pageOf(body.page),
|
|
211
|
+
)
|
|
130
212
|
return follow(c, run, false)
|
|
131
213
|
})
|
|
132
214
|
// Stop: the only thing that cancels a run. A page that goes away (a reload, a move to another page, a closed tab) does not: the agent goes on, and the page picks it up again.
|
|
133
|
-
.post(`${
|
|
134
|
-
|
|
215
|
+
.post(`${PANEL_PATH}/stop`, (c) => {
|
|
216
|
+
const visitor = caller(c.req.raw.headers, c.req.header('x-agent-panel-token'))
|
|
217
|
+
if (visitor === undefined)
|
|
135
218
|
return c.json({ error: "only the app's own page may stop a run" }, 403)
|
|
136
|
-
|
|
219
|
+
// A visitor's own run, never another's.
|
|
220
|
+
visitor.current?.cancel.abort()
|
|
137
221
|
return c.json({ ok: true })
|
|
138
222
|
})
|
|
139
223
|
// The latest run, for a page that just loaded: what was asked, then every event so far, then the rest as it happens (until the run ends).
|
|
140
|
-
.get(`${
|
|
141
|
-
|
|
224
|
+
.get(`${PANEL_PATH}/run`, (c) => {
|
|
225
|
+
const visitor = caller(c.req.raw.headers, c.req.header('x-agent-panel-token'))
|
|
226
|
+
if (visitor === undefined)
|
|
142
227
|
return c.json({ error: "only the app's own page may read the run" }, 403)
|
|
143
|
-
return current === undefined
|
|
228
|
+
return visitor.current === undefined
|
|
229
|
+
? c.json({ run: null })
|
|
230
|
+
: follow(c, visitor.current, true)
|
|
144
231
|
})
|
|
145
|
-
/**
|
|
232
|
+
/**
|
|
233
|
+
* What `/ask` and the socket's `ask` both check: a message of a sensible size, a chat driver that is here, and no run of this visitor going; for the Assistant also its
|
|
234
|
+
* limits (an admitted ask is counted).
|
|
235
|
+
*/
|
|
146
236
|
const checkAsk = (
|
|
237
|
+
visitor: Visitor<Run>,
|
|
147
238
|
rawMessage: unknown,
|
|
148
239
|
rawDriver: unknown,
|
|
149
240
|
):
|
|
150
241
|
| { message: string; driverName: string; driver: ChatDriver }
|
|
151
|
-
| { error: string; status: 400 | 409 } => {
|
|
242
|
+
| { error: string; status: 400 | 409 | 429 | 503 } => {
|
|
152
243
|
const message = typeof rawMessage === 'string' ? rawMessage.trim() : ''
|
|
153
244
|
const driverName = typeof rawDriver === 'string' ? rawDriver : 'agent'
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const driver =
|
|
245
|
+
const tooLong = visitors.checkMessage(message)
|
|
246
|
+
if (tooLong !== undefined) return tooLong
|
|
247
|
+
const driver = offered().find((one) => one.name === driverName)
|
|
157
248
|
if (driver?.kind !== 'chat') {
|
|
158
|
-
const chat =
|
|
159
|
-
.list()
|
|
249
|
+
const chat = offered()
|
|
160
250
|
.filter((d) => d.kind === 'chat')
|
|
161
251
|
.map((d) => d.name)
|
|
162
252
|
.join(', ')
|
|
@@ -166,8 +256,11 @@ export function apply(ctx: Context, config: PanelConfig = {}): void {
|
|
|
166
256
|
status: 400,
|
|
167
257
|
}
|
|
168
258
|
}
|
|
169
|
-
|
|
170
|
-
|
|
259
|
+
const refused = visitors.admit(visitor)
|
|
260
|
+
if (refused !== undefined) {
|
|
261
|
+
if (assistant) audit({ ask: 'refused', visitor: visitor.tag, status: refused.status })
|
|
262
|
+
return refused
|
|
263
|
+
}
|
|
171
264
|
return { message, driverName, driver }
|
|
172
265
|
}
|
|
173
266
|
/** One run of an agent: its events as they came, and who is following. It lives in the app, not in a page. */
|
|
@@ -182,9 +275,17 @@ export function apply(ctx: Context, config: PanelConfig = {}): void {
|
|
|
182
275
|
ended: boolean
|
|
183
276
|
endedAt?: number
|
|
184
277
|
}
|
|
185
|
-
let current: Run | undefined
|
|
186
278
|
let runs = 0
|
|
187
|
-
|
|
279
|
+
/** The door's id of the page that asked, when it sent one that looks like it. */
|
|
280
|
+
const pageOf = (raw: unknown): string | undefined =>
|
|
281
|
+
typeof raw === 'string' && /^[A-Za-z0-9-]{1,80}$/u.test(raw) ? raw : undefined
|
|
282
|
+
const startRun = (
|
|
283
|
+
visitor: Visitor<Run>,
|
|
284
|
+
driverName: string,
|
|
285
|
+
message: string,
|
|
286
|
+
driver: ChatDriver,
|
|
287
|
+
page?: string,
|
|
288
|
+
): Run => {
|
|
188
289
|
runs += 1
|
|
189
290
|
const run: Run = {
|
|
190
291
|
id: runs,
|
|
@@ -196,7 +297,8 @@ export function apply(ctx: Context, config: PanelConfig = {}): void {
|
|
|
196
297
|
cancel: new AbortController(),
|
|
197
298
|
ended: false,
|
|
198
299
|
}
|
|
199
|
-
current = run
|
|
300
|
+
visitor.current = run
|
|
301
|
+
const who = assistant ? { visitor: visitor.tag } : {}
|
|
200
302
|
const emit = (event: string, data: unknown): void => {
|
|
201
303
|
const item = { event, data }
|
|
202
304
|
run.events.push(item)
|
|
@@ -207,21 +309,31 @@ export function apply(ctx: Context, config: PanelConfig = {}): void {
|
|
|
207
309
|
changed = true
|
|
208
310
|
emit('change', { extension, action })
|
|
209
311
|
})
|
|
210
|
-
|
|
211
|
-
|
|
312
|
+
// The run ends when the plugin unloads; the effect goes when the run ends, so runs do not pile up on the plugin.
|
|
313
|
+
const unloadStops = ctx.effect(() => () => run.cancel.abort(), `agent-panel: run ${runs}`)
|
|
314
|
+
audit({ ask: 'start', ...who, driver: driverName, characters: message.length })
|
|
315
|
+
// The agent's door calls name no window: while this run goes, they are for the page whose panel asked, not for another tab or window that happens to be open.
|
|
316
|
+
const door = page === undefined ? undefined : (ctx.get('door') as DoorPort | undefined)
|
|
317
|
+
const releasePage = door?.driveFor(page as string)
|
|
212
318
|
void (async () => {
|
|
213
319
|
try {
|
|
214
|
-
for await (const { event, data } of driver.start({
|
|
320
|
+
for await (const { event, data } of driver.start({
|
|
321
|
+
message,
|
|
322
|
+
signal: run.cancel.signal,
|
|
323
|
+
...(page === undefined ? {} : { page }),
|
|
324
|
+
}))
|
|
215
325
|
emit(event, data)
|
|
216
326
|
emit('done', { changed })
|
|
217
327
|
} catch (error) {
|
|
218
328
|
emit('error', { message: error instanceof Error ? error.message : String(error) })
|
|
219
329
|
} finally {
|
|
220
330
|
off()
|
|
331
|
+
releasePage?.()
|
|
221
332
|
run.ended = true
|
|
222
333
|
run.endedAt = Date.now()
|
|
223
|
-
audit({ ask: 'end', driver: driverName, changed })
|
|
334
|
+
audit({ ask: 'end', ...who, driver: driverName, changed })
|
|
224
335
|
for (const follower of [...run.followers]) follower({ event: 'end', data: {} })
|
|
336
|
+
void unloadStops()
|
|
225
337
|
}
|
|
226
338
|
})()
|
|
227
339
|
return run
|
|
@@ -319,17 +431,13 @@ export function apply(ctx: Context, config: PanelConfig = {}): void {
|
|
|
319
431
|
return c.json({ error: "only the app's own page may use the panel" }, 403)
|
|
320
432
|
await next()
|
|
321
433
|
}
|
|
322
|
-
const sameToken = (presented: unknown): boolean => {
|
|
323
|
-
if (typeof presented !== 'string') return false
|
|
324
|
-
const a = Buffer.from(presented)
|
|
325
|
-
const b = Buffer.from(token)
|
|
326
|
-
return a.length === b.length && timingSafeEqual(a, b)
|
|
327
|
-
}
|
|
328
434
|
ws.get(
|
|
329
435
|
`${PREFIX}/run/ws`,
|
|
330
436
|
own,
|
|
331
437
|
upgrade(
|
|
332
|
-
() => {
|
|
438
|
+
(c) => {
|
|
439
|
+
// The visitor the socket's cookie names (the owner in development): its hello must carry that visitor's token, and it reaches that visitor's runs only.
|
|
440
|
+
const visitor = visitors.of(c.req.header('cookie'))
|
|
333
441
|
let authed = false
|
|
334
442
|
let following: { detach(): void } | undefined
|
|
335
443
|
let hello: NodeJS.Timeout | undefined
|
|
@@ -339,26 +447,35 @@ export function apply(ctx: Context, config: PanelConfig = {}): void {
|
|
|
339
447
|
},
|
|
340
448
|
onMessage(event, ws) {
|
|
341
449
|
if (typeof event.data !== 'string') return
|
|
342
|
-
let message: {
|
|
450
|
+
let message: {
|
|
451
|
+
t?: unknown
|
|
452
|
+
token?: unknown
|
|
453
|
+
message?: unknown
|
|
454
|
+
driver?: unknown
|
|
455
|
+
page?: unknown
|
|
456
|
+
}
|
|
343
457
|
try {
|
|
344
458
|
message = JSON.parse(event.data) as typeof message
|
|
345
459
|
} catch {
|
|
346
460
|
return
|
|
347
461
|
}
|
|
348
462
|
if (!authed) {
|
|
349
|
-
if (message.t !== 'hello' || !
|
|
463
|
+
if (message.t !== 'hello' || !visitors.accepts(visitor, message.token)) {
|
|
350
464
|
ws.close(4401, 'token')
|
|
351
465
|
return
|
|
352
466
|
}
|
|
353
467
|
authed = true
|
|
354
468
|
if (hello !== undefined) clearTimeout(hello)
|
|
469
|
+
// The page waits for this before it counts the socket as open: a token it was given by an earlier start of the app is refused (4401) and it asks for a new one.
|
|
470
|
+
ws.send(JSON.stringify({ e: 'accepted', d: {} }))
|
|
355
471
|
return
|
|
356
472
|
}
|
|
357
473
|
const send = (e: string, d: unknown): void => {
|
|
358
474
|
ws.send(JSON.stringify({ e, d }))
|
|
359
475
|
}
|
|
476
|
+
if (visitor === undefined) return
|
|
360
477
|
if (message.t === 'stop') {
|
|
361
|
-
current?.cancel.abort()
|
|
478
|
+
visitor.current?.cancel.abort()
|
|
362
479
|
return
|
|
363
480
|
}
|
|
364
481
|
if (following !== undefined) return
|
|
@@ -368,18 +485,27 @@ export function apply(ctx: Context, config: PanelConfig = {}): void {
|
|
|
368
485
|
void pumped.done.then(() => ws.close(1000, 'run ended'))
|
|
369
486
|
}
|
|
370
487
|
if (message.t === 'ask') {
|
|
371
|
-
const checked = checkAsk(message.message, message.driver)
|
|
488
|
+
const checked = checkAsk(visitor, message.message, message.driver)
|
|
372
489
|
if ('error' in checked) {
|
|
373
490
|
send('error', { message: checked.error })
|
|
374
491
|
ws.close(1000, 'refused')
|
|
375
492
|
return
|
|
376
493
|
}
|
|
377
|
-
watch(
|
|
494
|
+
watch(
|
|
495
|
+
startRun(
|
|
496
|
+
visitor,
|
|
497
|
+
checked.driverName,
|
|
498
|
+
checked.message,
|
|
499
|
+
checked.driver,
|
|
500
|
+
pageOf(message.page),
|
|
501
|
+
),
|
|
502
|
+
false,
|
|
503
|
+
)
|
|
378
504
|
} else if (message.t === 'follow') {
|
|
379
|
-
if (current === undefined) {
|
|
505
|
+
if (visitor.current === undefined) {
|
|
380
506
|
send('idle', {})
|
|
381
507
|
ws.close(1000, 'no run')
|
|
382
|
-
} else watch(current, true)
|
|
508
|
+
} else watch(visitor.current, true)
|
|
383
509
|
}
|
|
384
510
|
},
|
|
385
511
|
onClose() {
|
|
@@ -393,5 +519,5 @@ export function apply(ctx: Context, config: PanelConfig = {}): void {
|
|
|
393
519
|
)
|
|
394
520
|
inner.router.route('/', ws)
|
|
395
521
|
})
|
|
396
|
-
ctx.router.route('/', app)
|
|
522
|
+
ctx.router.route(MOUNT === '' ? '/' : MOUNT, app)
|
|
397
523
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A plugin's own route prefix under the app's mount. The app is served at the root of its own server `''`, or at the prefix a host serves it at (`/app` in
|
|
3
|
+
* the ACRYL frame); a plugin that mounts routes of its own (the panel, the PTY driver) takes that mount as config and derives its prefix from it, so its
|
|
4
|
+
* routes belong to the app wherever the app is served. The same rule `routerHttp.mount` applies.
|
|
5
|
+
*/
|
|
6
|
+
export function mountedPrefix(mount: string | undefined, path: string, plugin: string): string {
|
|
7
|
+
if (mount === undefined || mount === '') return path
|
|
8
|
+
if (!/^\/[A-Za-z0-9._~\-/]*[A-Za-z0-9._~-]$/u.test(mount))
|
|
9
|
+
throw new TypeError(
|
|
10
|
+
`${plugin}: the app's mount "${mount}" must be empty or an absolute path such as "/app"`,
|
|
11
|
+
)
|
|
12
|
+
return `${mount}${path}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The app's own mount, validated like {@link mountedPrefix}: empty at the root or the prefix a host serves the app at (`/app`). A plugin's routes are
|
|
17
|
+
* mounted at this path and keep only their own fixed path inside, so the frame's router (which prefixes a mount with the app's base) composes
|
|
18
|
+
* `/app` + `/api/agent-panel/panel.js` once, not twice.
|
|
19
|
+
*/
|
|
20
|
+
export function mountedBase(mount: string | undefined, plugin: string): string {
|
|
21
|
+
if (mount === undefined || mount === '') return ''
|
|
22
|
+
if (!/^\/[A-Za-z0-9._~\-/]*[A-Za-z0-9._~-]$/u.test(mount))
|
|
23
|
+
throw new TypeError(
|
|
24
|
+
`${plugin}: the app's mount "${mount}" must be empty or an absolute path such as "/app"`,
|
|
25
|
+
)
|
|
26
|
+
return mount
|
|
27
|
+
}
|