acrylblends 1.1.0 → 1.3.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.
Files changed (263) hide show
  1. package/README.md +9 -0
  2. package/dist/index.d.ts +47 -2
  3. package/dist/index.js +2017 -32
  4. package/dist/index.js.map +1 -1
  5. package/docs/framework/agents/index.md +1 -1
  6. package/docs/framework/getting-started/desktop.md +23 -4
  7. package/docs/framework/guides/create-acrylblends.md +5 -3
  8. package/docs/framework/workshop/README.md +139 -153
  9. package/docs/framework/workshop/facilitator.md +69 -101
  10. package/legacy/README.md +9 -0
  11. package/legacy/agent/157ce3db1112/src/app.ts +98 -0
  12. package/legacy/agent/157ce3db1112/src/routes.ts +63 -0
  13. package/legacy/agent/24c681a00fca/src/app.ts +160 -0
  14. package/legacy/agent/24c681a00fca/src/routes.ts +83 -0
  15. package/legacy/agent/2e790da977b2/src/app.ts +98 -0
  16. package/legacy/agent/2e790da977b2/src/routes.ts +63 -0
  17. package/legacy/agent/6307045545dc/src/app.ts +168 -0
  18. package/legacy/agent/6307045545dc/src/routes.ts +83 -0
  19. package/legacy/agent/6a5fb9c15c53/src/app.ts +158 -0
  20. package/legacy/agent/6a5fb9c15c53/src/routes.ts +83 -0
  21. package/legacy/agent/71fb54a5a942/src/app.ts +111 -0
  22. package/legacy/agent/71fb54a5a942/src/routes.ts +63 -0
  23. package/legacy/agent/74ece148269c/src/app.ts +168 -0
  24. package/legacy/agent/74ece148269c/src/routes.ts +93 -0
  25. package/legacy/agent/855359304707/src/app.ts +168 -0
  26. package/legacy/agent/855359304707/src/routes.ts +126 -0
  27. package/legacy/agent/b5ef3c786ae4/src/app.ts +127 -0
  28. package/legacy/agent/b5ef3c786ae4/src/routes.ts +76 -0
  29. package/legacy/agent/c2f30958043b/src/app.ts +98 -0
  30. package/legacy/agent/c2f30958043b/src/routes.ts +53 -0
  31. package/legacy/blank/5a91c558af22/src/routes.ts +18 -0
  32. package/legacy/blank/9c650cf52bf3/src/routes.ts +14 -0
  33. package/legacy/blank/aea589283445/src/routes.ts +16 -0
  34. package/legacy/blank/b2cbb759c889/src/routes.ts +16 -0
  35. package/legacy/blank/d6d371c9a65e/src/routes.ts +18 -0
  36. package/legacy/chat/7076cc821e2c/src/app.ts +72 -0
  37. package/legacy/chat/7076cc821e2c/src/routes.ts +13 -0
  38. package/legacy/chat/7dbe6d5170df/src/app.ts +107 -0
  39. package/legacy/chat/7dbe6d5170df/src/routes.ts +19 -0
  40. package/legacy/chat/a9a9ab44c58e/src/app.ts +107 -0
  41. package/legacy/chat/a9a9ab44c58e/src/routes.ts +15 -0
  42. package/legacy/chat/f20ad9a51b06/src/app.ts +106 -0
  43. package/legacy/chat/f20ad9a51b06/src/routes.ts +15 -0
  44. package/legacy/layouts.json +524 -0
  45. package/legacy/nodejs/1d477f37aaf4/src/app.ts +92 -0
  46. package/legacy/nodejs/1d477f37aaf4/src/routes.ts +40 -0
  47. package/legacy/nodejs/b1bd9462496c/src/app.ts +61 -0
  48. package/legacy/nodejs/b1bd9462496c/src/routes.ts +37 -0
  49. package/legacy/nodejs/d5bab75076ef/src/app.ts +95 -0
  50. package/legacy/nodejs/d5bab75076ef/src/routes.ts +40 -0
  51. package/legacy/notes/4edc36949c15/src/app.ts +107 -0
  52. package/legacy/notes/4edc36949c15/src/routes.ts +14 -0
  53. package/legacy/notes/6cab2d09747c/src/app.ts +72 -0
  54. package/legacy/notes/6cab2d09747c/src/routes.ts +12 -0
  55. package/legacy/notes/7ca2dfed623a/src/app.ts +106 -0
  56. package/legacy/notes/7ca2dfed623a/src/routes.ts +14 -0
  57. package/legacy/todo/02e613b3625c/src/app.ts +71 -0
  58. package/legacy/todo/02e613b3625c/src/todo.ts +123 -0
  59. package/legacy/todo/45694a8d9e15/src/app.ts +71 -0
  60. package/legacy/todo/45694a8d9e15/src/todo.ts +96 -0
  61. package/legacy/todo/5923d3a6099f/src/app.ts +71 -0
  62. package/legacy/todo/5923d3a6099f/src/todo.ts +125 -0
  63. package/legacy/todo/849a18d66dc1/src/app.ts +91 -0
  64. package/legacy/todo/849a18d66dc1/src/todo.ts +125 -0
  65. package/legacy/todo/8e64211312f3/src/app.ts +71 -0
  66. package/legacy/todo/8e64211312f3/src/todo.ts +125 -0
  67. package/legacy/todo/b79078a87565/src/app.ts +71 -0
  68. package/legacy/todo/b79078a87565/src/todo.ts +125 -0
  69. package/lib/{audit-BFpULBCj.d.ts → audit-Bok_UNCn.d.ts} +26 -3
  70. package/lib/{client-modules-drj4mjhj.js → client-modules-Bkdr83jO.js} +1 -1
  71. package/lib/cordis/door.d.ts +87 -9
  72. package/lib/cordis/door.js +324 -60
  73. package/lib/cordis/inspect.js +1 -1
  74. package/lib/cordis/node-server.d.ts +101 -3
  75. package/lib/cordis/node-server.js +3 -3
  76. package/lib/cordis/router-http.d.ts +7 -1
  77. package/lib/cordis/router-http.js +44 -3
  78. package/lib/cordis/router.d.ts +1 -1
  79. package/lib/cordis/router.js +1 -1
  80. package/lib/cordis/state.js +1 -1
  81. package/lib/door/host.d.ts +2 -2
  82. package/lib/door/host.js +1 -1
  83. package/lib/door/page.d.ts +10 -1
  84. package/lib/door/page.js +46 -7
  85. package/lib/door/protocol.d.ts +1 -1
  86. package/lib/drivers.d.ts +10 -1
  87. package/lib/{index-DMOtA-NJ.d.ts → index-DGq7H0lb.d.ts} +11 -1
  88. package/lib/node-server.d.ts +7 -1
  89. package/lib/node-server.js +2 -2
  90. package/lib/{protocol-a2aqrp2X.d.ts → protocol-BeIYCgTo.d.ts} +4 -0
  91. package/lib/{snapshot-text-BwM6T2nG.js → snapshot-text-CXTnjxGx.js} +87 -7
  92. package/lib/src-CtkIX_Gg.js +427 -0
  93. package/lib/{src-BTBm4OPD.js → src-iBJ2ysV9.js} +77 -14
  94. package/lib/{test-websocket-BLeEvybj.js → test-websocket-CQOSvBth.js} +17 -7
  95. package/lib/{test-websocket-DyeS50Tr.d.ts → test-websocket-GUTAkFQT.d.ts} +3 -1
  96. package/package.json +2 -1
  97. package/plugins/acryl-door/plugin.json +3 -0
  98. package/plugins/acryl-door/src/cli/acryl.ts +63 -6
  99. package/plugins/acryl-door/src/host/channel.ts +86 -9
  100. package/plugins/acryl-door/src/host/chat-tools.ts +8 -6
  101. package/plugins/acryl-door/src/host/door-file.ts +7 -2
  102. package/plugins/acryl-door/src/host/door.ts +128 -24
  103. package/plugins/acryl-door/src/host/operator-routes.ts +21 -1
  104. package/plugins/acryl-door/src/host/page-routes.ts +3 -1
  105. package/plugins/acryl-door/src/host/page-socket.ts +18 -8
  106. package/plugins/acryl-door/src/host/verbs.ts +14 -1
  107. package/plugins/acryl-door/src/index.ts +168 -30
  108. package/plugins/acryl-door/src/page/base.ts +32 -0
  109. package/plugins/acryl-door/src/page/channel.ts +3 -1
  110. package/plugins/acryl-door/src/page/driver/accessibility.ts +11 -0
  111. package/plugins/acryl-door/src/page/driver/actions.ts +4 -0
  112. package/plugins/acryl-door/src/page/driver/driver.ts +32 -4
  113. package/plugins/acryl-door/src/page/driver/snapshot.ts +5 -2
  114. package/plugins/acryl-door/src/page/index.ts +41 -2
  115. package/plugins/acryl-door/src/page/outline.ts +56 -0
  116. package/plugins/acryl-door/src/page/protocol.ts +4 -0
  117. package/plugins/acryl-door/src/page/snapshot-text.ts +2 -1
  118. package/plugins/acryl-node-server/src/adapter/listener.ts +4 -0
  119. package/plugins/acryl-node-server/src/adapter/raw.ts +18 -0
  120. package/plugins/acryl-node-server/src/adapter/websocket.ts +15 -9
  121. package/plugins/acryl-node-server/src/client-modules.ts +7 -1
  122. package/plugins/acryl-node-server/src/index.ts +304 -16
  123. package/plugins/acryl-node-server/src/upgrade-hub.ts +2 -2
  124. package/plugins/acryl-page/plugin.json +12 -0
  125. package/plugins/acryl-page/src/index.ts +163 -0
  126. package/plugins/acryl-router/src/service.ts +77 -14
  127. package/plugins/acryl-router-http/src/index.ts +15 -1
  128. package/plugins/acryl-router-http/src/serve.ts +31 -0
  129. package/plugins/acryl-web-server/plugin.json +14 -0
  130. package/plugins/acryl-web-server/src/index.ts +164 -0
  131. package/plugins/agent/agent.ts +38 -7
  132. package/plugins/agent/index.ts +1 -1
  133. package/plugins/agent/model.ts +10 -2
  134. package/plugins/agent/tools.ts +9 -3
  135. package/plugins/agent-panel/client/describe.ts +57 -0
  136. package/plugins/agent-panel/client/driver-pick.ts +22 -0
  137. package/plugins/agent-panel/client/frame.ts +51 -0
  138. package/plugins/agent-panel/client/main.ts +238 -26
  139. package/plugins/agent-panel/drivers.ts +7 -2
  140. package/plugins/agent-panel/headless.ts +5 -2
  141. package/plugins/agent-panel/index.ts +146 -57
  142. package/plugins/agent-panel/mount.ts +27 -0
  143. package/plugins/agent-panel/visitors.ts +231 -0
  144. package/plugins/builder/index.ts +13 -35
  145. package/plugins/builder/package.ts +186 -0
  146. package/plugins/builder/plugin.json +3 -1
  147. package/plugins/desktop/frame.ts +231 -0
  148. package/plugins/desktop/launch.ts +44 -9
  149. package/plugins/desktop/main.ts +100 -32
  150. package/plugins/desktop/window.ts +21 -4
  151. package/plugins/driver-pty/client/terminal.ts +4 -1
  152. package/plugins/driver-pty/index.ts +38 -11
  153. package/plugins/driver-pty/spawn.ts +38 -0
  154. package/plugins/driver-pty/terminal.ts +2 -0
  155. package/plugins/extensions/index.ts +47 -18
  156. package/plugins/extensions/plugin-store.ts +27 -0
  157. package/plugins/extensions/plugin.json +1 -1
  158. package/plugins/extensions/resolve.ts +13 -0
  159. package/plugins/extensions/stage.ts +15 -5
  160. package/plugins/feedback/client.d.ts +22 -0
  161. package/plugins/feedback/client.js +471 -0
  162. package/plugins/feedback/index.ts +143 -0
  163. package/plugins/feedback/plugin.json +11 -0
  164. package/plugins/feedback/store.ts +307 -0
  165. package/plugins/home/index.ts +122 -0
  166. package/plugins/home/plugin.json +11 -0
  167. package/plugins/instance/index.ts +6 -0
  168. package/plugins/manifest/app-plugins.ts +86 -0
  169. package/plugins/manifest/index.ts +12 -2
  170. package/plugins/manifest/plugin.json +1 -1
  171. package/plugins/manifest/snapshot.ts +275 -0
  172. package/plugins/openai/index.ts +64 -5
  173. package/plugins/openai/prompt.ts +23 -0
  174. package/plugins/plugin-share/README.md +51 -0
  175. package/plugins/plugin-share/index.ts +467 -0
  176. package/plugins/plugin-share/plugin.json +15 -0
  177. package/plugins/plugin-share/publish.ts +65 -0
  178. package/plugins/plugin-share/pull-check.ts +129 -0
  179. package/plugins/plugin-share/pulled-records.ts +70 -0
  180. package/shared/AGENTS.core.md +36 -16
  181. package/shared/skills/acryl-add-ui/SKILL.md +19 -0
  182. package/shared/skills/acryl-app/SKILL.md +13 -0
  183. package/shared/skills/acryl-build-plugin/SKILL.md +22 -0
  184. package/shared/skills/acryl-change-plugin/SKILL.md +14 -0
  185. package/shared/skills/acryl-diagnose-plugin/SKILL.md +13 -0
  186. package/shared/skills/acryl-fix-plugin/SKILL.md +13 -0
  187. package/shared/skills/acryl-remove-plugin/SKILL.md +12 -0
  188. package/shared/skills/acryl-share-plugin/SKILL.md +16 -0
  189. package/shared/skills/cordis-plugin/SKILL.md +26 -0
  190. package/templates/agent/.gitattributes +2 -0
  191. package/templates/agent/AGENTS.md +1 -1
  192. package/templates/agent/blend.yaml +6 -0
  193. package/templates/agent/gitignore +1 -0
  194. package/templates/agent/package.json +1 -0
  195. package/templates/agent/src/app.ts +154 -33
  196. package/templates/agent/src/frame.ts +17 -0
  197. package/templates/agent/src/main.ts +5 -3
  198. package/templates/agent/src/routes.ts +38 -19
  199. package/templates/agent/template.json +3 -0
  200. package/templates/agent/test/app.test.ts +45 -3
  201. package/templates/blank/.gitattributes +2 -0
  202. package/templates/blank/AGENTS.md +3 -2
  203. package/templates/blank/blend.yaml +11 -0
  204. package/templates/blank/gitignore +1 -0
  205. package/templates/blank/package.json +1 -0
  206. package/templates/blank/src/app.ts +100 -29
  207. package/templates/blank/src/door.ts +7 -0
  208. package/templates/blank/src/first-page.ts +5 -0
  209. package/templates/blank/src/frame.ts +17 -0
  210. package/templates/blank/src/routes.ts +65 -8
  211. package/templates/blank/template.json +14 -1
  212. package/templates/blank/test/app.test.ts +103 -16
  213. package/templates/chat/.gitattributes +2 -0
  214. package/templates/chat/blend.yaml +2 -1
  215. package/templates/chat/gitignore +1 -0
  216. package/templates/chat/package.json +1 -0
  217. package/templates/chat/src/app.ts +72 -22
  218. package/templates/chat/src/chat-app.ts +11 -2
  219. package/templates/chat/src/client/chat.tsx +4 -2
  220. package/templates/chat/src/frame.ts +17 -0
  221. package/templates/chat/src/page.tsx +6 -4
  222. package/templates/chat/src/routes.ts +14 -4
  223. package/templates/chat/src/runtime.ts +9 -3
  224. package/templates/chat/template.json +3 -1
  225. package/templates/chat/test/chat.test.ts +46 -1
  226. package/templates/chat/test/server.test.ts +17 -3
  227. package/templates/nodejs/.gitattributes +2 -0
  228. package/templates/nodejs/blend.yaml +2 -1
  229. package/templates/nodejs/gitignore +1 -0
  230. package/templates/nodejs/package.json +1 -0
  231. package/templates/nodejs/src/app.ts +76 -21
  232. package/templates/nodejs/src/frame.ts +17 -0
  233. package/templates/nodejs/src/page.tsx +3 -3
  234. package/templates/nodejs/src/routes.ts +32 -13
  235. package/templates/nodejs/template.json +3 -1
  236. package/templates/nodejs/test/app.test.ts +38 -3
  237. package/templates/nodejs/test/server.test.ts +17 -3
  238. package/templates/notes/.gitattributes +2 -0
  239. package/templates/notes/blend.yaml +2 -1
  240. package/templates/notes/gitignore +1 -0
  241. package/templates/notes/package.json +1 -0
  242. package/templates/notes/src/app.ts +72 -22
  243. package/templates/notes/src/frame.ts +17 -0
  244. package/templates/notes/src/notes-app.ts +24 -17
  245. package/templates/notes/src/routes.ts +11 -4
  246. package/templates/notes/src/runtime.ts +9 -3
  247. package/templates/notes/src/views.tsx +26 -17
  248. package/templates/notes/template.json +3 -1
  249. package/templates/notes/test/notes.test.ts +39 -1
  250. package/templates/notes/test/server.test.ts +17 -3
  251. package/templates/todo/.gitattributes +2 -0
  252. package/templates/todo/blend.yaml +3 -0
  253. package/templates/todo/gitignore +1 -0
  254. package/templates/todo/package.json +1 -0
  255. package/templates/todo/src/app.ts +81 -27
  256. package/templates/todo/src/door.ts +7 -0
  257. package/templates/todo/src/frame.ts +17 -0
  258. package/templates/todo/src/page.ts +13 -6
  259. package/templates/todo/src/todo.ts +18 -6
  260. package/templates/todo/template.json +11 -1
  261. package/templates/todo/test/todo.test.ts +67 -9
  262. package/lib/src-LX0E4ec4.js +0 -117
  263. package/shared/SKILL.md +0 -14
@@ -0,0 +1,83 @@
1
+ import type { Context } from '@deepseek-ai/cordis'
2
+ import { Acryl } from '../plugins/acryl-router/src/index.ts'
3
+ import { bodyLimit } from '../plugins/acryl-router/src/middleware/index.ts'
4
+ import type {} from '../plugins/agent/index.ts'
5
+ import type {} from '../plugins/extensions/index.ts'
6
+ import type {} from '../plugins/tasks/index.ts'
7
+
8
+ export const APP_NAME = '__APP_NAME__'
9
+
10
+ /** The door's page script (`.acryl/bin/acryl look` and `act`), in development only: `src/app.ts` leaves the door out in production and with ACRYL_DOOR=off. */
11
+ const DOOR_TAG =
12
+ process.env.NODE_ENV !== 'production' && process.env.ACRYL_DOOR !== 'off'
13
+ ? '<script type="module" src="/api/acryl-door/page.js"></script>'
14
+ : ''
15
+
16
+ /**
17
+ * The app's own routes on the router. Always: the page and `GET /health`. While the agent, the extensions and the task recorder run: `POST /ask` for the agent; `GET /extensions`
18
+ * (and `POST /extensions/approve` and `/deny`) and `GET /tasks` to observe and steer what it did (every run is a durable task owning one task per tool call); `POST /tasks/cancel`.
19
+ * What changes is also streamed: `GET /api/state/tasks/events` and `GET /api/state/extensions/events`. In production `src/app.ts` leaves the agent, builder and extensions rows
20
+ * out (unless `ACRYL_PRODUCTION_KEEP` names them), and then those routes are not mounted at all. The mounts are removed when the plugin is disposed.
21
+ */
22
+ export const routes = {
23
+ name: 'routes',
24
+ inject: ['router'],
25
+ apply(ctx: Context) {
26
+ // A body here is a name, an id or a question; anything bigger is refused before it is read.
27
+ const json = async (c: {
28
+ req: { json(): Promise<unknown> }
29
+ }): Promise<Record<string, unknown>> => {
30
+ const body = await c.req.json().catch(() => undefined)
31
+ return typeof body === 'object' && body !== null ? (body as Record<string, unknown>) : {}
32
+ }
33
+ const limit = bodyLimit({ maxSize: 64 * 1024 })
34
+ const base = new Acryl()
35
+ .use(limit)
36
+ // A browser gets a page (the door's and the agent panel's script tags are added where they are mounted); everything else gets the JSON.
37
+ .get('/', (c) =>
38
+ (c.req.header('accept') ?? '').includes('text/html')
39
+ ? c.html(
40
+ `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${APP_NAME}</title></head><body><h1>${APP_NAME}</h1><p>This app runs an agent unless it is switched off (production switches it off, see ACRYL_PRODUCTION_KEEP in AGENTS.md). Ask it for a feature in the agent panel on this page (or with <code>POST /ask</code>).</p>${DOOR_TAG}</body></html>`,
41
+ )
42
+ : c.json({ app: APP_NAME, ok: true }),
43
+ )
44
+ .get('/health', (c) => c.json({ ok: true }))
45
+ ctx.router.route('/', base)
46
+ // The routes that give a visitor the agent: mounted only while the agent, the extensions and the tasks run (see the header).
47
+ ctx.inject(['agent', 'extensions', 'tasks'], (inner) => {
48
+ const app = new Acryl().use(limit).get('/extensions', (c) =>
49
+ c.json({
50
+ installed: inner.extensions.list(),
51
+ notInstalled: inner.extensions.pending(),
52
+ waitingForApproval: inner.extensions.approvals(),
53
+ }),
54
+ )
55
+ // The owner's decision on an install that is waiting (the `approval` parameter in blend.yaml).
56
+ for (const [path, decide] of [
57
+ ['/extensions/approve', (name: string) => inner.extensions.approve(name)],
58
+ ['/extensions/deny', (name: string) => inner.extensions.deny(name)],
59
+ ] as const) {
60
+ app.post(path, async (c) => {
61
+ const { name } = await json(c)
62
+ if (typeof name !== 'string') return c.json({ error: 'name must be a string' }, 400)
63
+ return c.json(await decide(name))
64
+ })
65
+ }
66
+ app
67
+ .get('/tasks', (c) => c.json(inner.tasks.list().slice(0, 100)))
68
+ .post('/tasks/cancel', async (c) => {
69
+ const { id } = await json(c)
70
+ if (typeof id !== 'string' || !inner.tasks.get(id))
71
+ return c.json({ error: 'no such task' }, 404)
72
+ return c.json({ cancelled: inner.tasks.cancel(id) })
73
+ })
74
+ .post('/ask', async (c) => {
75
+ const { question } = await json(c)
76
+ if (typeof question !== 'string')
77
+ return c.json({ error: 'question must be a string' }, 400)
78
+ return c.json(await inner.agent.run(question))
79
+ })
80
+ inner.router.route('/', app)
81
+ })
82
+ },
83
+ }
@@ -0,0 +1,98 @@
1
+ import { join } from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { Context, type Fiber } from '@deepseek-ai/cordis'
4
+ import * as nodeServer from './plugins/acryl-node-server/src/index.ts'
5
+ import * as router from './plugins/acryl-router/src/index.ts'
6
+ import * as state from './plugins/acryl-state/src/index.ts'
7
+ import { agent, memory, model, tools } from './plugins/agent/index.ts'
8
+ import { builder } from './plugins/builder/index.ts'
9
+ import { extensions } from './plugins/extensions/index.ts'
10
+ import { type AppInstance, instance } from './plugins/instance/index.ts'
11
+ import {
12
+ blueprintsIn,
13
+ compose,
14
+ loadManifest,
15
+ type ParameterValue,
16
+ } from './plugins/manifest/index.ts'
17
+ import { type OpenAIConfig, openai } from './plugins/openai/index.ts'
18
+ import { sandboxPlugin } from './plugins/sandbox/index.ts'
19
+ import { save } from './plugins/save/index.ts'
20
+ import { tasks } from './plugins/tasks/index.ts'
21
+ import { routes } from './routes.ts'
22
+
23
+ /** The plugins this app's code provides. `blend.yaml` chooses, orders and configures them. */
24
+ export const registry = {
25
+ tasks,
26
+ model,
27
+ tools,
28
+ memory,
29
+ agent,
30
+ 'acryl-router': router,
31
+ 'acryl-state': state,
32
+ extensions,
33
+ builder,
34
+ save,
35
+ sandbox: sandboxPlugin,
36
+ routes,
37
+ 'acryl-node-server': nodeServer,
38
+ }
39
+
40
+ /** The manifest next to this app's source. */
41
+ export const MANIFEST = join(fileURLToPath(new URL('..', import.meta.url)), 'blend.yaml')
42
+
43
+ /**
44
+ * The app's manifest, resolved. A Blend finds its Blueprint in `blueprints/<id>.yaml`; an app that is itself a Blueprint never looks.
45
+ * `createApp` and the tests read it the same way.
46
+ */
47
+ export function readManifest(values?: Readonly<Record<string, ParameterValue>>) {
48
+ return loadManifest(MANIFEST, {
49
+ known: Object.keys(registry),
50
+ getBlueprint: blueprintsIn(join(fileURLToPath(new URL('..', import.meta.url)), 'blueprints')),
51
+ ...(values ? { values } : {}),
52
+ })
53
+ }
54
+
55
+ export interface App {
56
+ readonly root: Context
57
+ readonly url: string
58
+ stop(): Promise<void>
59
+ }
60
+
61
+ /**
62
+ * The app: `instance` first (where it lives, chosen at run time), then the manifest's rows in order. The app's own `extensions/`
63
+ * folder is trusted to install at start, and is where the builder writes.
64
+ */
65
+ export async function createApp(
66
+ chosen: AppInstance,
67
+ options: {
68
+ values?: Readonly<Record<string, ParameterValue>>
69
+ /** A real model (any OpenAI-compatible API) in place of the offline scripted one. */
70
+ model?: OpenAIConfig
71
+ } = {},
72
+ ): Promise<App> {
73
+ const manifest = readManifest(options.values)
74
+ const extensionsDir = join(chosen.root, 'extensions')
75
+ const root = new Context()
76
+ const first = root.plugin(instance, chosen)
77
+ await first.await()
78
+ const fibers: Fiber[] = [
79
+ first,
80
+ // The `model` row mounts the scripted model, or the real one when a model is configured.
81
+ ...(await compose(root, manifest, options.model ? { ...registry, model: openai } : registry, {
82
+ ...(options.model ? { model: { ...options.model } } : {}),
83
+ // Runtime values the manifest cannot hold: the manifest's own digest (every task records it), and who may install.
84
+ tasks: { manifest: `sha256:${manifest.digest}` },
85
+ // The `approval` parameter in blend.yaml (the extensions row) turns this source into one that asks.
86
+ extensions: { sources: [{ dir: extensionsDir, trust: 'install' }] },
87
+ builder: { dir: extensionsDir },
88
+ server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0, hostname: chosen.host },
89
+ })),
90
+ ]
91
+ return {
92
+ root,
93
+ url: root.nodeServer.url,
94
+ async stop() {
95
+ for (const fiber of [...fibers].reverse()) await fiber.dispose()
96
+ },
97
+ }
98
+ }
@@ -0,0 +1,63 @@
1
+ import type { Context } from '@deepseek-ai/cordis'
2
+ import { App } from './plugins/acryl-router/src/index.ts'
3
+ import { bodyLimit } from './plugins/acryl-router/src/middleware/index.ts'
4
+ import type {} from './plugins/agent/index.ts'
5
+ import type {} from './plugins/extensions/index.ts'
6
+ import type {} from './plugins/tasks/index.ts'
7
+
8
+ export const APP_NAME = '__APP_NAME__'
9
+
10
+ /**
11
+ * The app's own routes on the router: `POST /ask` for the agent; `GET /extensions` and `GET /tasks` to observe what it did (every run is a
12
+ * durable task owning one task per tool call); `POST /tasks/cancel`. What changes is also streamed: `GET /api/state/tasks/events` and
13
+ * `GET /api/state/extensions/events`. The mount is removed when the plugin is disposed.
14
+ */
15
+ export const routes = {
16
+ name: 'routes',
17
+ inject: ['router', 'agent', 'extensions', 'tasks'],
18
+ apply(ctx: Context) {
19
+ // A body here is a name, an id or a question; anything bigger is refused before it is read.
20
+ const json = async (c: {
21
+ req: { json(): Promise<unknown> }
22
+ }): Promise<Record<string, unknown>> => {
23
+ const body = await c.req.json().catch(() => undefined)
24
+ return typeof body === 'object' && body !== null ? (body as Record<string, unknown>) : {}
25
+ }
26
+ const app = new App()
27
+ .use(bodyLimit({ maxSize: 64 * 1024 }))
28
+ .get('/', (c) => c.json({ app: APP_NAME, ok: true }))
29
+ .get('/health', (c) => c.json({ ok: true }))
30
+ .get('/extensions', (c) =>
31
+ c.json({
32
+ installed: ctx.extensions.list(),
33
+ notInstalled: ctx.extensions.pending(),
34
+ waitingForApproval: ctx.extensions.approvals(),
35
+ }),
36
+ )
37
+ // The owner's decision on an install that is waiting (the `approval` parameter in blend.yaml).
38
+ for (const [path, decide] of [
39
+ ['/extensions/approve', (name: string) => ctx.extensions.approve(name)],
40
+ ['/extensions/deny', (name: string) => ctx.extensions.deny(name)],
41
+ ] as const) {
42
+ app.post(path, async (c) => {
43
+ const { name } = await json(c)
44
+ if (typeof name !== 'string') return c.json({ error: 'name must be a string' }, 400)
45
+ return c.json(await decide(name))
46
+ })
47
+ }
48
+ app
49
+ .get('/tasks', (c) => c.json(ctx.tasks.list().slice(0, 100)))
50
+ .post('/tasks/cancel', async (c) => {
51
+ const { id } = await json(c)
52
+ if (typeof id !== 'string' || !ctx.tasks.get(id))
53
+ return c.json({ error: 'no such task' }, 404)
54
+ return c.json({ cancelled: ctx.tasks.cancel(id) })
55
+ })
56
+ .post('/ask', async (c) => {
57
+ const { question } = await json(c)
58
+ if (typeof question !== 'string') return c.json({ error: 'question must be a string' }, 400)
59
+ return c.json(await ctx.agent.run(question))
60
+ })
61
+ ctx.router.route('/', app)
62
+ },
63
+ }
@@ -0,0 +1,168 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { Context, type Fiber } from '@deepseek-ai/cordis'
5
+ import * as door from '../plugins/acryl-door/src/index.ts'
6
+ import * as nodeServer from '../plugins/acryl-node-server/src/index.ts'
7
+ import * as router from '../plugins/acryl-router/src/index.ts'
8
+ import * as state from '../plugins/acryl-state/src/index.ts'
9
+ import { agent, memory, model, tools } from '../plugins/agent/index.ts'
10
+ import * as panel from '../plugins/agent-panel/index.ts'
11
+ import { builder } from '../plugins/builder/index.ts'
12
+ import { extensions } from '../plugins/extensions/index.ts'
13
+ import { type AppInstance, instance } from '../plugins/instance/index.ts'
14
+ import {
15
+ blueprintsIn,
16
+ compose,
17
+ loadManifest,
18
+ type ParameterValue,
19
+ } from '../plugins/manifest/index.ts'
20
+ import { type OpenAIConfig, openai } from '../plugins/openai/index.ts'
21
+ import { sandboxPlugin } from '../plugins/sandbox/index.ts'
22
+ import { save } from '../plugins/save/index.ts'
23
+ import { tasks } from '../plugins/tasks/index.ts'
24
+ import { routes } from './routes.ts'
25
+
26
+ /** The plugins this app's code provides. `blend.yaml` chooses, orders and configures them. */
27
+ export const registry = {
28
+ tasks,
29
+ model,
30
+ tools,
31
+ memory,
32
+ agent,
33
+ 'acryl-router': router,
34
+ 'acryl-state': state,
35
+ extensions,
36
+ builder,
37
+ save,
38
+ sandbox: sandboxPlugin,
39
+ routes,
40
+ 'acryl-node-server': nodeServer,
41
+ 'agent-panel': panel,
42
+ 'acryl-door': door,
43
+ }
44
+
45
+ /** The manifest next to this app's source. */
46
+ export const MANIFEST = join(fileURLToPath(new URL('..', import.meta.url)), 'blend.yaml')
47
+
48
+ /**
49
+ * The app's manifest, resolved. A Blend finds its Blueprint in `blueprints/<id>.yaml`; an app that is itself a Blueprint never looks.
50
+ * `createApp` and the tests read it the same way.
51
+ */
52
+ export function readManifest(values?: Readonly<Record<string, ParameterValue>>) {
53
+ return loadManifest(MANIFEST, {
54
+ known: Object.keys(registry),
55
+ getBlueprint: blueprintsIn(join(fileURLToPath(new URL('..', import.meta.url)), 'blueprints')),
56
+ ...(values ? { values } : {}),
57
+ })
58
+ }
59
+
60
+ export interface App {
61
+ readonly root: Context
62
+ /** The address the server listens on; empty when the app was made with `listen: false`. */
63
+ readonly url: string
64
+ stop(): Promise<void>
65
+ }
66
+
67
+ /**
68
+ * The development rows, and what each one gives a visitor. Production leaves the agent and the builder out. The extensions row is not left out: it starts again, read-only,
69
+ * the extensions in the app's `plugins/` folder (what the agent built and installed in development, committed with the app), so a deployed app has its own features; nothing
70
+ * can be installed, updated or approved while it runs, and `/ask`, `/extensions*` and `/tasks*` stay unmounted (they need the agent). `ACRYL_PRODUCTION_KEEP=extensions` keeps the
71
+ * live install itself.
72
+ */
73
+ const DEVELOPMENT_ROWS: Record<string, string> = {
74
+ agent: 'POST /ask runs the agent, which spends the model key and calls its tools',
75
+ builder: 'the agent can write plugin source into plugins/',
76
+ extensions:
77
+ 'a plugin installed live runs in this process, and POST /extensions/approve decides installs',
78
+ }
79
+
80
+ /**
81
+ * The app: `instance` first (where it lives, chosen at run time), then the manifest's rows in order. The app's `plugins/`
82
+ * folder is trusted to install at start, and is where the builder writes.
83
+ */
84
+ export async function createApp(
85
+ chosen: AppInstance,
86
+ options: {
87
+ values?: Readonly<Record<string, ParameterValue>>
88
+ /** A real model (any OpenAI-compatible API) in place of the offline scripted one. */
89
+ model?: OpenAIConfig
90
+ /** Start the HTTP server (default). Tests pass `false` and call `app.root.router.request(path)`: it needs no port, so it also runs where a sandbox forbids listening. */
91
+ listen?: boolean
92
+ } = {},
93
+ ): Promise<App> {
94
+ const read = readManifest(options.values)
95
+ // Rows left out: the server (and so the door, which needs it) when the app is reached in process; the door in production and with ACRYL_DOOR=off (one process at a time
96
+ // serves a folder's door, so the tests that start several apps set it).
97
+ const listen = options.listen !== false
98
+ const leaveOut = new Set<string>()
99
+ let extensionsReadOnly = false
100
+ if (!listen) leaveOut.add('acryl-node-server').add('acryl-door')
101
+ if (process.env.NODE_ENV === 'production' || process.env.ACRYL_DOOR === 'off')
102
+ leaveOut.add('acryl-door')
103
+ // Production: the rows that give a visitor power over the running app or the owner's model key are left out too. The agent (`POST /ask` spends the model key and runs tools),
104
+ // the builder and the extensions (live plugin install runs code in this process, and `POST /extensions/approve` decides it) are development tools. `ACRYL_PRODUCTION_KEEP`
105
+ // names the ones to keep (`agent,builder,extensions`); each one kept is announced at start, because none of them is behind a login.
106
+ if (process.env.NODE_ENV === 'production') {
107
+ const keep = new Set(
108
+ (process.env.ACRYL_PRODUCTION_KEEP ?? '')
109
+ .split(',')
110
+ .map((name) => name.trim())
111
+ .filter((name) => name !== ''),
112
+ )
113
+ for (const name of keep)
114
+ if (!(name in DEVELOPMENT_ROWS))
115
+ console.warn(
116
+ `WARNING: ACRYL_PRODUCTION_KEEP names "${name}", which is not one of ${Object.keys(DEVELOPMENT_ROWS).join(', ')}`,
117
+ )
118
+ for (const [row, danger] of Object.entries(DEVELOPMENT_ROWS)) {
119
+ if (keep.has(row))
120
+ console.warn(
121
+ `WARNING: the "${row}" row runs in production because ACRYL_PRODUCTION_KEEP names it: ${danger}. Nothing here is behind a login; put the app behind your own.`,
122
+ )
123
+ else if (row === 'extensions') extensionsReadOnly = true
124
+ else leaveOut.add(row)
125
+ }
126
+ }
127
+ const manifest = {
128
+ ...read,
129
+ plugins: read.plugins.map((row) => (leaveOut.has(row.use) ? { ...row, disabled: true } : row)),
130
+ }
131
+ const extensionsDir = join(chosen.root, 'plugins')
132
+ // One release of grace: an app made before 0.7.8 kept what the agent wrote in `extensions/`. It is still installed, with a warning.
133
+ const legacyDir = join(chosen.root, 'extensions')
134
+ const legacy = existsSync(legacyDir)
135
+ if (legacy)
136
+ console.warn(
137
+ 'warning: extensions/ is the old folder for plugins the agent writes; move them to plugins/ ("git mv extensions/<name> plugins/<name>"). extensions/ is read for this release only.',
138
+ )
139
+ const root = new Context()
140
+ const first = root.plugin(instance, chosen)
141
+ await first.await()
142
+ const fibers: Fiber[] = [
143
+ first,
144
+ // The `model` row mounts the scripted model, or the real one when a model is configured.
145
+ ...(await compose(root, manifest, options.model ? { ...registry, model: openai } : registry, {
146
+ ...(options.model ? { model: { ...options.model } } : {}),
147
+ // Runtime values the manifest cannot hold: the manifest's own digest (every task records it), and who may install.
148
+ tasks: { manifest: `sha256:${manifest.digest}` },
149
+ // The `approval` parameter in blend.yaml (the extensions row) turns this source into one that asks.
150
+ extensions: {
151
+ ...(extensionsReadOnly ? { readOnly: true } : {}),
152
+ sources: [
153
+ { dir: extensionsDir, trust: 'install' },
154
+ ...(legacy ? [{ dir: legacyDir, trust: 'install' as const }] : []),
155
+ ],
156
+ },
157
+ builder: { dir: extensionsDir },
158
+ server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0, hostname: chosen.host },
159
+ })),
160
+ ]
161
+ return {
162
+ root,
163
+ url: listen ? root.nodeServer.url : '',
164
+ async stop() {
165
+ for (const fiber of [...fibers].reverse()) await fiber.dispose()
166
+ },
167
+ }
168
+ }
@@ -0,0 +1,83 @@
1
+ import type { Context } from '@deepseek-ai/cordis'
2
+ import { Acryl } from '../plugins/acryl-router/src/index.ts'
3
+ import { bodyLimit } from '../plugins/acryl-router/src/middleware/index.ts'
4
+ import type {} from '../plugins/agent/index.ts'
5
+ import type {} from '../plugins/extensions/index.ts'
6
+ import type {} from '../plugins/tasks/index.ts'
7
+
8
+ export const APP_NAME = '__APP_NAME__'
9
+
10
+ /** The door's page script (`.acryl/bin/acryl look` and `act`), in development only: `src/app.ts` leaves the door out in production and with ACRYL_DOOR=off. */
11
+ const DOOR_TAG =
12
+ process.env.NODE_ENV !== 'production' && process.env.ACRYL_DOOR !== 'off'
13
+ ? '<script type="module" src="/api/acryl-door/page.js"></script>'
14
+ : ''
15
+
16
+ /**
17
+ * The app's own routes on the router. Always: the page and `GET /health`. While the agent, the extensions and the task recorder run: `POST /ask` for the agent; `GET /extensions`
18
+ * (and `POST /extensions/approve` and `/deny`) and `GET /tasks` to observe and steer what it did (every run is a durable task owning one task per tool call); `POST /tasks/cancel`.
19
+ * What changes is also streamed: `GET /api/state/tasks/events` and `GET /api/state/extensions/events`. In production `src/app.ts` leaves the agent and builder rows out
20
+ * and starts the extensions read-only (unless `ACRYL_PRODUCTION_KEEP` names them), and then those routes are not mounted at all. The mounts are removed when the plugin is disposed.
21
+ */
22
+ export const routes = {
23
+ name: 'routes',
24
+ inject: ['router'],
25
+ apply(ctx: Context) {
26
+ // A body here is a name, an id or a question; anything bigger is refused before it is read.
27
+ const json = async (c: {
28
+ req: { json(): Promise<unknown> }
29
+ }): Promise<Record<string, unknown>> => {
30
+ const body = await c.req.json().catch(() => undefined)
31
+ return typeof body === 'object' && body !== null ? (body as Record<string, unknown>) : {}
32
+ }
33
+ const limit = bodyLimit({ maxSize: 64 * 1024 })
34
+ const base = new Acryl()
35
+ .use(limit)
36
+ // A browser gets a page (the door's and the agent panel's script tags are added where they are mounted); everything else gets the JSON.
37
+ .get('/', (c) =>
38
+ (c.req.header('accept') ?? '').includes('text/html')
39
+ ? c.html(
40
+ `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${APP_NAME}</title></head><body><h1>${APP_NAME}</h1><p>This app runs an agent unless it is switched off (production switches it off, see ACRYL_PRODUCTION_KEEP in AGENTS.md). Ask it for a feature in the agent panel on this page (or with <code>POST /ask</code>).</p>${DOOR_TAG}</body></html>`,
41
+ )
42
+ : c.json({ app: APP_NAME, ok: true }),
43
+ )
44
+ .get('/health', (c) => c.json({ ok: true }))
45
+ ctx.router.route('/', base)
46
+ // The routes that give a visitor the agent: mounted only while the agent, the extensions and the tasks run (see the header).
47
+ ctx.inject(['agent', 'extensions', 'tasks'], (inner) => {
48
+ const app = new Acryl().use(limit).get('/extensions', (c) =>
49
+ c.json({
50
+ installed: inner.extensions.list(),
51
+ notInstalled: inner.extensions.pending(),
52
+ waitingForApproval: inner.extensions.approvals(),
53
+ }),
54
+ )
55
+ // The owner's decision on an install that is waiting (the `approval` parameter in blend.yaml).
56
+ for (const [path, decide] of [
57
+ ['/extensions/approve', (name: string) => inner.extensions.approve(name)],
58
+ ['/extensions/deny', (name: string) => inner.extensions.deny(name)],
59
+ ] as const) {
60
+ app.post(path, async (c) => {
61
+ const { name } = await json(c)
62
+ if (typeof name !== 'string') return c.json({ error: 'name must be a string' }, 400)
63
+ return c.json(await decide(name))
64
+ })
65
+ }
66
+ app
67
+ .get('/tasks', (c) => c.json(inner.tasks.list().slice(0, 100)))
68
+ .post('/tasks/cancel', async (c) => {
69
+ const { id } = await json(c)
70
+ if (typeof id !== 'string' || !inner.tasks.get(id))
71
+ return c.json({ error: 'no such task' }, 404)
72
+ return c.json({ cancelled: inner.tasks.cancel(id) })
73
+ })
74
+ .post('/ask', async (c) => {
75
+ const { question } = await json(c)
76
+ if (typeof question !== 'string')
77
+ return c.json({ error: 'question must be a string' }, 400)
78
+ return c.json(await inner.agent.run(question))
79
+ })
80
+ inner.router.route('/', app)
81
+ })
82
+ },
83
+ }
@@ -0,0 +1,158 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { Context, type Fiber } from '@deepseek-ai/cordis'
5
+ import * as door from '../plugins/acryl-door/src/index.ts'
6
+ import * as nodeServer from '../plugins/acryl-node-server/src/index.ts'
7
+ import * as router from '../plugins/acryl-router/src/index.ts'
8
+ import * as state from '../plugins/acryl-state/src/index.ts'
9
+ import { agent, memory, model, tools } from '../plugins/agent/index.ts'
10
+ import { builder } from '../plugins/builder/index.ts'
11
+ import { extensions } from '../plugins/extensions/index.ts'
12
+ import { type AppInstance, instance } from '../plugins/instance/index.ts'
13
+ import {
14
+ blueprintsIn,
15
+ compose,
16
+ loadManifest,
17
+ type ParameterValue,
18
+ } from '../plugins/manifest/index.ts'
19
+ import { type OpenAIConfig, openai } from '../plugins/openai/index.ts'
20
+ import { sandboxPlugin } from '../plugins/sandbox/index.ts'
21
+ import { save } from '../plugins/save/index.ts'
22
+ import { tasks } from '../plugins/tasks/index.ts'
23
+ import { routes } from './routes.ts'
24
+
25
+ /** The plugins this app's code provides. `blend.yaml` chooses, orders and configures them. */
26
+ export const registry = {
27
+ tasks,
28
+ model,
29
+ tools,
30
+ memory,
31
+ agent,
32
+ 'acryl-router': router,
33
+ 'acryl-state': state,
34
+ extensions,
35
+ builder,
36
+ save,
37
+ sandbox: sandboxPlugin,
38
+ routes,
39
+ 'acryl-node-server': nodeServer,
40
+ 'acryl-door': door,
41
+ }
42
+
43
+ /** The manifest next to this app's source. */
44
+ export const MANIFEST = join(fileURLToPath(new URL('..', import.meta.url)), 'blend.yaml')
45
+
46
+ /**
47
+ * The app's manifest, resolved. A Blend finds its Blueprint in `blueprints/<id>.yaml`; an app that is itself a Blueprint never looks.
48
+ * `createApp` and the tests read it the same way.
49
+ */
50
+ export function readManifest(values?: Readonly<Record<string, ParameterValue>>) {
51
+ return loadManifest(MANIFEST, {
52
+ known: Object.keys(registry),
53
+ getBlueprint: blueprintsIn(join(fileURLToPath(new URL('..', import.meta.url)), 'blueprints')),
54
+ ...(values ? { values } : {}),
55
+ })
56
+ }
57
+
58
+ export interface App {
59
+ readonly root: Context
60
+ /** The address the server listens on; empty when the app was made with `listen: false`. */
61
+ readonly url: string
62
+ stop(): Promise<void>
63
+ }
64
+
65
+ /** The development rows production leaves out, and what each one gives a visitor. */
66
+ const DEVELOPMENT_ROWS: Record<string, string> = {
67
+ agent: 'POST /ask runs the agent, which spends the model key and calls its tools',
68
+ builder: 'the agent can write plugin source into plugins/',
69
+ extensions:
70
+ 'a plugin installed live runs in this process, and POST /extensions/approve decides installs',
71
+ }
72
+
73
+ /**
74
+ * The app: `instance` first (where it lives, chosen at run time), then the manifest's rows in order. The app's `plugins/`
75
+ * folder is trusted to install at start, and is where the builder writes.
76
+ */
77
+ export async function createApp(
78
+ chosen: AppInstance,
79
+ options: {
80
+ values?: Readonly<Record<string, ParameterValue>>
81
+ /** A real model (any OpenAI-compatible API) in place of the offline scripted one. */
82
+ model?: OpenAIConfig
83
+ /** Start the HTTP server (default). Tests pass `false` and call `app.root.router.request(path)`: it needs no port, so it also runs where a sandbox forbids listening. */
84
+ listen?: boolean
85
+ } = {},
86
+ ): Promise<App> {
87
+ const read = readManifest(options.values)
88
+ // Rows left out: the server (and so the door, which needs it) when the app is reached in process; the door in production and with ACRYL_DOOR=off (one process at a time
89
+ // serves a folder's door, so the tests that start several apps set it).
90
+ const listen = options.listen !== false
91
+ const leaveOut = new Set<string>()
92
+ if (!listen) leaveOut.add('acryl-node-server').add('acryl-door')
93
+ if (process.env.NODE_ENV === 'production' || process.env.ACRYL_DOOR === 'off')
94
+ leaveOut.add('acryl-door')
95
+ // Production: the rows that give a visitor power over the running app or the owner's model key are left out too. The agent (`POST /ask` spends the model key and runs tools),
96
+ // the builder and the extensions (live plugin install runs code in this process, and `POST /extensions/approve` decides it) are development tools. `ACRYL_PRODUCTION_KEEP`
97
+ // names the ones to keep (`agent,builder,extensions`); each one kept is announced at start, because none of them is behind a login.
98
+ if (process.env.NODE_ENV === 'production') {
99
+ const keep = new Set(
100
+ (process.env.ACRYL_PRODUCTION_KEEP ?? '')
101
+ .split(',')
102
+ .map((name) => name.trim())
103
+ .filter((name) => name !== ''),
104
+ )
105
+ for (const name of keep)
106
+ if (!(name in DEVELOPMENT_ROWS))
107
+ console.warn(
108
+ `WARNING: ACRYL_PRODUCTION_KEEP names "${name}", which is not one of ${Object.keys(DEVELOPMENT_ROWS).join(', ')}`,
109
+ )
110
+ for (const [row, danger] of Object.entries(DEVELOPMENT_ROWS)) {
111
+ if (keep.has(row))
112
+ console.warn(
113
+ `WARNING: the "${row}" row runs in production because ACRYL_PRODUCTION_KEEP names it: ${danger}. Nothing here is behind a login; put the app behind your own.`,
114
+ )
115
+ else leaveOut.add(row)
116
+ }
117
+ }
118
+ const manifest = {
119
+ ...read,
120
+ plugins: read.plugins.map((row) => (leaveOut.has(row.use) ? { ...row, disabled: true } : row)),
121
+ }
122
+ const extensionsDir = join(chosen.root, 'plugins')
123
+ // One release of grace: an app made before 0.7.8 kept what the agent wrote in `extensions/`. It is still installed, with a warning.
124
+ const legacyDir = join(chosen.root, 'extensions')
125
+ const legacy = existsSync(legacyDir)
126
+ if (legacy)
127
+ console.warn(
128
+ 'warning: extensions/ is the old folder for plugins the agent writes; move them to plugins/ ("git mv extensions/<name> plugins/<name>"). extensions/ is read for this release only.',
129
+ )
130
+ const root = new Context()
131
+ const first = root.plugin(instance, chosen)
132
+ await first.await()
133
+ const fibers: Fiber[] = [
134
+ first,
135
+ // The `model` row mounts the scripted model, or the real one when a model is configured.
136
+ ...(await compose(root, manifest, options.model ? { ...registry, model: openai } : registry, {
137
+ ...(options.model ? { model: { ...options.model } } : {}),
138
+ // Runtime values the manifest cannot hold: the manifest's own digest (every task records it), and who may install.
139
+ tasks: { manifest: `sha256:${manifest.digest}` },
140
+ // The `approval` parameter in blend.yaml (the extensions row) turns this source into one that asks.
141
+ extensions: {
142
+ sources: [
143
+ { dir: extensionsDir, trust: 'install' },
144
+ ...(legacy ? [{ dir: legacyDir, trust: 'install' as const }] : []),
145
+ ],
146
+ },
147
+ builder: { dir: extensionsDir },
148
+ server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0, hostname: chosen.host },
149
+ })),
150
+ ]
151
+ return {
152
+ root,
153
+ url: listen ? root.nodeServer.url : '',
154
+ async stop() {
155
+ for (const fiber of [...fibers].reverse()) await fiber.dispose()
156
+ },
157
+ }
158
+ }