acrylblends 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +83 -7
- package/dist/index.js.map +1 -1
- package/lib/audit-BXnYsLge.d.ts +58 -0
- package/lib/cell-ChU41DZD.d.ts +32 -0
- package/lib/client-modules-CLvWOjbd.js +267 -0
- package/lib/connection-ZLEso7XL.d.ts +25 -0
- package/lib/conninfo.d.ts +1 -23
- package/lib/cordis/db-sqlite.d.ts +56 -0
- package/lib/cordis/db-sqlite.js +58 -0
- package/lib/cordis/db.d.ts +8 -0
- package/lib/cordis/db.js +1 -0
- package/lib/cordis/door.d.ts +159 -0
- package/lib/cordis/door.js +757 -0
- package/lib/cordis/inspect.d.ts +77 -0
- package/lib/cordis/inspect.js +149 -0
- package/lib/cordis/kv-db.d.ts +16 -0
- package/lib/cordis/kv-db.js +18 -0
- package/lib/cordis/kv-json.d.ts +16 -0
- package/lib/cordis/kv-json.js +44 -0
- package/lib/cordis/kv.d.ts +8 -0
- package/lib/cordis/kv.js +1 -0
- package/lib/cordis/node-server.d.ts +45 -0
- package/lib/cordis/node-server.js +93 -0
- package/lib/cordis/router-http.d.ts +56 -0
- package/lib/cordis/router-http.js +117 -0
- package/lib/cordis/router.d.ts +8 -0
- package/lib/cordis/router.js +7 -0
- package/lib/cordis/state/react.d.ts +6 -0
- package/lib/cordis/state/react.js +11 -0
- package/lib/cordis/state.d.ts +33 -0
- package/lib/cordis/state.js +163 -0
- package/lib/core-CaY8XDZ6.js +19 -0
- package/lib/database-DBHdwefi.d.ts +44 -0
- package/lib/db/conformance.d.ts +11 -0
- package/lib/db/conformance.js +267 -0
- package/lib/db/sqlite.d.ts +1 -1
- package/lib/db/sqlite.js +1 -196
- package/lib/db.d.ts +2 -1
- package/lib/{src-0hI7M7cN.js → deprecated-BP4fPt38.js} +0 -17
- package/lib/door/host.d.ts +24 -0
- package/lib/door/host.js +2 -0
- package/lib/door/page.d.ts +105 -0
- package/lib/door/page.js +1030 -0
- package/lib/door/protocol.d.ts +2 -0
- package/lib/door/protocol.js +197 -0
- package/lib/error-t4N2js2I.d.ts +8 -0
- package/lib/etag-DaKMOjty.js +135 -0
- package/lib/etag.js +1 -134
- package/lib/helpers-CvTauQT5.js +5 -0
- package/lib/http-exception-kpGJkmpi.d.ts +57 -0
- package/lib/http-exception.d.ts +1 -56
- package/lib/{index-BLTYsyc1.d.ts → index-BnGgRKTv.d.ts} +3 -39
- package/lib/index-BqVQkTnu.d.ts +98 -0
- package/lib/index-Dmst2QCQ.d.ts +44 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +2 -1
- package/lib/jwk-LkzeoaU8.js +117 -0
- package/lib/jwk.js +1 -116
- package/lib/jwt-CskdhEW9.js +115 -0
- package/lib/jwt.js +2 -114
- package/lib/kv/conformance.d.ts +13 -0
- package/lib/kv/conformance.js +301 -0
- package/lib/kv/db.d.ts +2 -2
- package/lib/kv/json.d.ts +1 -1
- package/lib/kv-9f-AlRKN.d.ts +39 -0
- package/lib/kv.d.ts +2 -1
- package/lib/language-D9a4zqvn.js +198 -0
- package/lib/language.js +1 -197
- package/lib/middleware-CPQ8himN.js +12 -0
- package/lib/node-server/serve-static.d.ts +0 -1
- package/lib/node-server/serve-static.js +1 -1
- package/lib/node-server.d.ts +1 -7
- package/lib/node-server.js +3 -1586
- package/lib/open-CMhnJODn.js +197 -0
- package/lib/protocol-BizVt4J6.d.ts +144 -0
- package/lib/request-id-CKVvHOO1.js +34 -0
- package/lib/request-id.js +1 -33
- package/lib/secure-headers-mP3j6Hm6.js +163 -0
- package/lib/secure-headers.js +1 -162
- package/lib/{serve-static-B849HlX2.js → serve-static-3uo_9rjF.js} +1 -4
- package/lib/server-DvgMGtzo.js +1323 -0
- package/lib/snapshot-text-D3NspDyT.js +338 -0
- package/lib/src-CwABFr6r.js +328 -0
- package/lib/timeout.d.ts +1 -1
- package/lib/timing-Cky7C1Jf.js +183 -0
- package/lib/timing.js +1 -182
- package/package.json +89 -1
- package/plugins/acryl-db/plugin.json +6 -1
- package/plugins/acryl-db-sqlite/plugin.json +4 -0
- package/plugins/acryl-db-sqlite/src/facade/node.ts +1 -1
- package/plugins/acryl-db-sqlite/src/index.ts +2 -2
- package/plugins/acryl-db-sqlite/src/open.ts +2 -2
- package/plugins/acryl-door/plugin.json +4 -1
- package/plugins/acryl-door/src/host/http.ts +2 -2
- package/plugins/acryl-door/src/host/mcp-routes.ts +1 -1
- package/plugins/acryl-door/src/host/operator-routes.ts +1 -1
- package/plugins/acryl-door/src/host/page-routes.ts +3 -3
- package/plugins/acryl-door/src/index.ts +2 -2
- package/plugins/acryl-inspect/plugin.json +4 -1
- package/plugins/acryl-inspect/src/service.ts +2 -2
- package/plugins/acryl-kv/plugin.json +6 -1
- package/plugins/acryl-kv-db/plugin.json +4 -0
- package/plugins/acryl-kv-db/src/index.ts +1 -1
- package/plugins/acryl-kv-db/src/open.ts +3 -3
- package/plugins/acryl-kv-json/plugin.json +4 -0
- package/plugins/acryl-kv-json/src/index.ts +1 -1
- package/plugins/acryl-kv-json/src/open.ts +2 -2
- package/plugins/acryl-node-server/plugin.json +5 -1
- package/plugins/acryl-node-server/src/adapter/conninfo.ts +1 -1
- package/plugins/acryl-node-server/src/adapter/early-hints.ts +1 -1
- package/plugins/acryl-node-server/src/adapter/serve-static.ts +2 -2
- package/plugins/acryl-node-server/src/adapter/websocket.ts +2 -2
- package/plugins/acryl-node-server/src/client-modules.ts +2 -2
- package/plugins/acryl-node-server/src/index.ts +1 -1
- package/plugins/acryl-router/plugin.json +11 -1
- package/plugins/acryl-router/src/connection.d.ts +25 -0
- package/plugins/acryl-router/src/core/app-base.d.ts +215 -0
- package/plugins/acryl-router/src/core/app.d.ts +19 -0
- package/plugins/acryl-router/src/core/client/client.d.ts +4 -0
- package/plugins/acryl-router/src/core/client/fetch-result-please.d.ts +35 -0
- package/plugins/acryl-router/src/core/client/index.d.ts +7 -0
- package/plugins/acryl-router/src/core/client/types.d.ts +229 -0
- package/plugins/acryl-router/src/core/client/utils.d.ts +18 -0
- package/plugins/acryl-router/src/core/compose.d.ts +14 -0
- package/plugins/acryl-router/src/core/context.d.ts +443 -0
- package/plugins/acryl-router/src/core/helper/accepts/accepts.d.ts +30 -0
- package/plugins/acryl-router/src/core/helper/accepts/index.d.ts +5 -0
- package/plugins/acryl-router/src/core/helper/conninfo/index.d.ts +5 -0
- package/plugins/acryl-router/src/core/helper/conninfo/types.d.ts +36 -0
- package/plugins/acryl-router/src/core/helper/cookie/index.d.ts +25 -0
- package/plugins/acryl-router/src/core/helper/css/common.d.ts +59 -0
- package/plugins/acryl-router/src/core/helper/css/index.d.ts +80 -0
- package/plugins/acryl-router/src/core/helper/dev/index.d.ts +20 -0
- package/plugins/acryl-router/src/core/helper/factory/index.d.ts +82 -0
- package/plugins/acryl-router/src/core/helper/html/index.d.ts +8 -0
- package/plugins/acryl-router/src/core/helper/proxy/index.d.ts +73 -0
- package/plugins/acryl-router/src/core/helper/route/index.d.ts +75 -0
- package/plugins/acryl-router/src/core/helper/ssg/index.d.ts +7 -0
- package/plugins/acryl-router/src/core/helper/ssg/middleware.d.ts +44 -0
- package/plugins/acryl-router/src/core/helper/ssg/plugins.d.ts +27 -0
- package/plugins/acryl-router/src/core/helper/ssg/ssg.d.ts +91 -0
- package/plugins/acryl-router/src/core/helper/ssg/utils.d.ts +16 -0
- package/plugins/acryl-router/src/core/helper/streaming/index.d.ts +8 -0
- package/plugins/acryl-router/src/core/helper/streaming/sse.d.ts +13 -0
- package/plugins/acryl-router/src/core/helper/streaming/stream.d.ts +3 -0
- package/plugins/acryl-router/src/core/helper/streaming/text.d.ts +3 -0
- package/plugins/acryl-router/src/core/helper/streaming/utils.d.ts +1 -0
- package/plugins/acryl-router/src/core/helper/testing/index.d.ts +13 -0
- package/plugins/acryl-router/src/core/helper/websocket/index.d.ts +70 -0
- package/plugins/acryl-router/src/core/http-exception.d.ts +58 -0
- package/plugins/acryl-router/src/core/index.d.ts +37 -0
- package/plugins/acryl-router/src/core/jsx/base.d.ts +61 -0
- package/plugins/acryl-router/src/core/jsx/children.d.ts +9 -0
- package/plugins/acryl-router/src/core/jsx/components.d.ts +15 -0
- package/plugins/acryl-router/src/core/jsx/constants.d.ts +6 -0
- package/plugins/acryl-router/src/core/jsx/context.d.ts +51 -0
- package/plugins/acryl-router/src/core/jsx/dom/components.d.ts +10 -0
- package/plugins/acryl-router/src/core/jsx/dom/context.d.ts +3 -0
- package/plugins/acryl-router/src/core/jsx/dom/css.d.ts +83 -0
- package/plugins/acryl-router/src/core/jsx/dom/hooks/index.d.ts +39 -0
- package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.d.ts +16 -0
- package/plugins/acryl-router/src/core/jsx/dom/jsx-dev-runtime.d.ts +8 -0
- package/plugins/acryl-router/src/core/jsx/dom/jsx-runtime.d.ts +7 -0
- package/plugins/acryl-router/src/core/jsx/dom/render.d.ts +73 -0
- package/plugins/acryl-router/src/core/jsx/dom/utils.d.ts +1 -0
- package/plugins/acryl-router/src/core/jsx/hooks/index.d.ts +47 -0
- package/plugins/acryl-router/src/core/jsx/index.d.ts +76 -0
- package/plugins/acryl-router/src/core/jsx/intrinsic-element/common.d.ts +6 -0
- package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.d.ts +13 -0
- package/plugins/acryl-router/src/core/jsx/intrinsic-elements.d.ts +735 -0
- package/plugins/acryl-router/src/core/jsx/streaming.d.ts +36 -0
- package/plugins/acryl-router/src/core/jsx/types.d.ts +36 -0
- package/plugins/acryl-router/src/core/jsx/utils.d.ts +4 -0
- package/plugins/acryl-router/src/core/request/constants.d.ts +1 -0
- package/plugins/acryl-router/src/core/request.d.ts +306 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/index.d.ts +6 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/matcher.d.ts +7 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/node.d.ts +13 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.d.ts +17 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/router.d.ts +11 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/trie.d.ts +8 -0
- package/plugins/acryl-router/src/core/router/smart-router/index.d.ts +5 -0
- package/plugins/acryl-router/src/core/router/smart-router/router.d.ts +11 -0
- package/plugins/acryl-router/src/core/router/trie-router/index.d.ts +5 -0
- package/plugins/acryl-router/src/core/router/trie-router/node.d.ts +6 -0
- package/plugins/acryl-router/src/core/router/trie-router/router.d.ts +7 -0
- package/plugins/acryl-router/src/core/router/utils.d.ts +1 -0
- package/plugins/acryl-router/src/core/router.d.ts +97 -0
- package/plugins/acryl-router/src/core/types.d.ts +573 -0
- package/plugins/acryl-router/src/core/utils/accept.d.ts +6 -0
- package/plugins/acryl-router/src/core/utils/body.d.ts +79 -0
- package/plugins/acryl-router/src/core/utils/buffer.d.ts +17 -0
- package/plugins/acryl-router/src/core/utils/color.d.ts +21 -0
- package/plugins/acryl-router/src/core/utils/concurrent.d.ts +11 -0
- package/plugins/acryl-router/src/core/utils/constants.d.ts +4 -0
- package/plugins/acryl-router/src/core/utils/cookie.d.ts +41 -0
- package/plugins/acryl-router/src/core/utils/crypto.d.ts +15 -0
- package/plugins/acryl-router/src/core/utils/handler.d.ts +6 -0
- package/plugins/acryl-router/src/core/utils/headers.d.ts +8 -0
- package/plugins/acryl-router/src/core/utils/html.d.ts +44 -0
- package/plugins/acryl-router/src/core/utils/http-status.d.ts +32 -0
- package/plugins/acryl-router/src/core/utils/mime.d.ts +70 -0
- package/plugins/acryl-router/src/core/utils/stream.d.ts +31 -0
- package/plugins/acryl-router/src/core/utils/types.d.ts +128 -0
- package/plugins/acryl-router/src/core/utils/url.d.ts +44 -0
- package/plugins/acryl-router/src/core/validator/index.d.ts +7 -0
- package/plugins/acryl-router/src/core/validator/utils.d.ts +29 -0
- package/plugins/acryl-router/src/core/validator/validator.d.ts +16 -0
- package/plugins/acryl-router/src/helpers/index.d.ts +17 -0
- package/plugins/acryl-router/src/helpers/schema-validator.d.ts +54 -0
- package/plugins/acryl-router-http/plugin.json +4 -1
- package/plugins/acryl-router-http/src/index.ts +1 -1
- package/plugins/acryl-state/plugin.json +5 -1
- package/plugins/acryl-state/src/service.ts +3 -3
- package/plugins/tsconfig.json +32 -20
- package/docs/framework/UPSTREAM.md +0 -3
- package/docs/framework/upstream.json +0 -9
- package/lib/index-D4FAsBZu.d.ts +0 -85
- package/plugins/acryl-db/upstream.json +0 -18
- package/plugins/acryl-db-sqlite/upstream.json +0 -21
- package/plugins/acryl-door/upstream.json +0 -28
- package/plugins/acryl-inspect/upstream.json +0 -18
- package/plugins/acryl-kv/upstream.json +0 -18
- package/plugins/acryl-kv-db/upstream.json +0 -21
- package/plugins/acryl-kv-json/upstream.json +0 -22
- package/plugins/acryl-node-server/upstream.json +0 -21
- package/plugins/acryl-router/upstream.json +0 -20
- package/plugins/acryl-router-http/upstream.json +0 -22
- package/plugins/acryl-state/NOTICE +0 -30
- package/plugins/acryl-state/upstream.json +0 -22
- package/plugins/desktop/upstream.json +0 -12
- package/plugins/desktop-web/upstream.json +0 -12
- package/templates/todo/upstream.json +0 -12
- /package/lib/{index-CTy4N7ng.d.ts → deprecated-CTy4N7ng.d.ts} +0 -0
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
import { t as Acryl } from "../app-BM0rTCp7.js";
|
|
2
|
+
import { n as clientModules } from "../client-modules-CLvWOjbd.js";
|
|
3
|
+
import { MAX_SNAPSHOT_NODES, MAX_WAIT_MS, UiControlError, parseUiRequest } from "../door/protocol.js";
|
|
4
|
+
import { a as INVALID_JSON, c as operatorOnly, d as AuditLog, f as auditPath, i as DOOR_PREFIX, l as privateApi, o as error, p as UiChannel, r as pageChannelRoutes, s as jsonBody, t as renderSnapshot, u as readJson } from "../snapshot-text-D3NspDyT.js";
|
|
5
|
+
import "../src-CwABFr6r.js";
|
|
6
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
7
|
+
import { randomBytes } from "node:crypto";
|
|
8
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import z from "@deepseek-ai/schemastery";
|
|
10
|
+
//#region ../../plugins/acryl-door/src/host/verbs.ts
|
|
11
|
+
/**
|
|
12
|
+
* The one verb list (spec 049 slice 13). Every face of the door reads it: the operator client (`acryl <verb>`, its `verbs` and `brief`),
|
|
13
|
+
* MCP (`tools/list`) and the built-in chat (the `tools` registry). A verb added here is in all three, or the comparison test fails.
|
|
14
|
+
*/
|
|
15
|
+
/** Every verb, in the order `brief` lists them. */
|
|
16
|
+
const VERBS = [
|
|
17
|
+
"brief",
|
|
18
|
+
"look",
|
|
19
|
+
"act",
|
|
20
|
+
"plugin-list",
|
|
21
|
+
"plugin-add",
|
|
22
|
+
"plugin-remove",
|
|
23
|
+
"plugin-reload",
|
|
24
|
+
"tool-list",
|
|
25
|
+
"tool-call"
|
|
26
|
+
];
|
|
27
|
+
const none = {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {},
|
|
30
|
+
additionalProperties: false
|
|
31
|
+
};
|
|
32
|
+
const VERB_SPECS = {
|
|
33
|
+
"brief": {
|
|
34
|
+
usage: "brief: this summary",
|
|
35
|
+
description: "What this app is, which files to read first, whether its page is open, and the verbs.",
|
|
36
|
+
arguments: none
|
|
37
|
+
},
|
|
38
|
+
"look": {
|
|
39
|
+
usage: "look [--cursor <n>] [--max-nodes <n>]: the page as text, with refs",
|
|
40
|
+
description: "The open page as text: its roles, names, values and visible text, each with a ref to act on. A long page comes in parts (cursor).",
|
|
41
|
+
arguments: {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
cursor: {
|
|
45
|
+
type: "integer",
|
|
46
|
+
minimum: 0,
|
|
47
|
+
description: "where the previous part ended"
|
|
48
|
+
},
|
|
49
|
+
maxNodes: {
|
|
50
|
+
type: "integer",
|
|
51
|
+
minimum: 1,
|
|
52
|
+
maximum: MAX_SNAPSHOT_NODES
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
additionalProperties: false
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"act": {
|
|
59
|
+
usage: "act click|type|select|press|scroll|wait ...: do one thing on the page (refs from the latest look)",
|
|
60
|
+
description: "Do one thing on the page: click, type, select, press a key, scroll, or wait for text or a control. Look again before the next act.",
|
|
61
|
+
arguments: {
|
|
62
|
+
type: "object",
|
|
63
|
+
properties: {
|
|
64
|
+
op: {
|
|
65
|
+
type: "string",
|
|
66
|
+
enum: [
|
|
67
|
+
"click",
|
|
68
|
+
"type",
|
|
69
|
+
"select",
|
|
70
|
+
"press",
|
|
71
|
+
"scroll",
|
|
72
|
+
"wait"
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
ref: {
|
|
76
|
+
type: "string",
|
|
77
|
+
description: "a ref from the latest look, e.g. 3.12"
|
|
78
|
+
},
|
|
79
|
+
text: {
|
|
80
|
+
type: "string",
|
|
81
|
+
description: "type: what to type; wait: the text to wait for"
|
|
82
|
+
},
|
|
83
|
+
submit: {
|
|
84
|
+
type: "boolean",
|
|
85
|
+
description: "type: press Enter after"
|
|
86
|
+
},
|
|
87
|
+
clear: {
|
|
88
|
+
type: "boolean",
|
|
89
|
+
description: "type: false keeps what the field holds"
|
|
90
|
+
},
|
|
91
|
+
option: {
|
|
92
|
+
type: "string",
|
|
93
|
+
description: "select: the option to choose"
|
|
94
|
+
},
|
|
95
|
+
key: {
|
|
96
|
+
type: "string",
|
|
97
|
+
description: "press: a character or Enter, Escape, Tab, ArrowDown, ..."
|
|
98
|
+
},
|
|
99
|
+
direction: {
|
|
100
|
+
type: "string",
|
|
101
|
+
enum: [
|
|
102
|
+
"up",
|
|
103
|
+
"down",
|
|
104
|
+
"left",
|
|
105
|
+
"right"
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
amount: {
|
|
109
|
+
type: "number",
|
|
110
|
+
minimum: 1,
|
|
111
|
+
maximum: 1e4,
|
|
112
|
+
description: "scroll: pixels"
|
|
113
|
+
},
|
|
114
|
+
role: {
|
|
115
|
+
type: "string",
|
|
116
|
+
description: "wait: the role to wait for"
|
|
117
|
+
},
|
|
118
|
+
name: {
|
|
119
|
+
type: "string",
|
|
120
|
+
description: "wait: its name"
|
|
121
|
+
},
|
|
122
|
+
gone: {
|
|
123
|
+
type: "boolean",
|
|
124
|
+
description: "wait: until it is gone instead"
|
|
125
|
+
},
|
|
126
|
+
timeoutMs: {
|
|
127
|
+
type: "integer",
|
|
128
|
+
minimum: 0,
|
|
129
|
+
maximum: MAX_WAIT_MS
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
required: ["op"],
|
|
133
|
+
additionalProperties: false
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
"plugin-list": {
|
|
137
|
+
usage: "plugin list: the plugins installed while the app runs",
|
|
138
|
+
description: "The plugins installed into the running app.",
|
|
139
|
+
arguments: none
|
|
140
|
+
},
|
|
141
|
+
"plugin-add": {
|
|
142
|
+
usage: "plugin add <folder>: check, install and start a plugin folder, live",
|
|
143
|
+
description: "Check, install and start a plugin folder inside the app, without a restart.",
|
|
144
|
+
arguments: {
|
|
145
|
+
type: "object",
|
|
146
|
+
properties: { folder: {
|
|
147
|
+
type: "string",
|
|
148
|
+
description: "a folder inside the app, e.g. plugins/pomodoro"
|
|
149
|
+
} },
|
|
150
|
+
required: ["folder"],
|
|
151
|
+
additionalProperties: false
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"plugin-remove": {
|
|
155
|
+
usage: "plugin remove <name>: stop and remove an installed plugin",
|
|
156
|
+
description: "Stop and remove an installed plugin.",
|
|
157
|
+
arguments: {
|
|
158
|
+
type: "object",
|
|
159
|
+
properties: { name: { type: "string" } },
|
|
160
|
+
required: ["name"],
|
|
161
|
+
additionalProperties: false
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
"plugin-reload": {
|
|
165
|
+
usage: "plugin reload: bring every installed plugin in line with its folder",
|
|
166
|
+
description: "Bring every installed plugin in line with its folder (after editing one).",
|
|
167
|
+
arguments: none
|
|
168
|
+
},
|
|
169
|
+
"tool-list": {
|
|
170
|
+
usage: "tool list: the app tools the door offers",
|
|
171
|
+
description: "The app's own tools that the door offers.",
|
|
172
|
+
arguments: none
|
|
173
|
+
},
|
|
174
|
+
"tool-call": {
|
|
175
|
+
usage: "tool call --tool <name> [--args <json>]: run one of them",
|
|
176
|
+
description: "Run one of the app tools that tool-list names.",
|
|
177
|
+
arguments: {
|
|
178
|
+
type: "object",
|
|
179
|
+
properties: {
|
|
180
|
+
tool: { type: "string" },
|
|
181
|
+
args: { description: "the tool's input" }
|
|
182
|
+
},
|
|
183
|
+
required: ["tool"],
|
|
184
|
+
additionalProperties: false
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
/** A verb's name in the `tools` registry the built-in chat reads: prefixed, so it never meets an app tool's name. */
|
|
189
|
+
const chatToolName = (verb) => `door_${verb.replaceAll("-", "_")}`;
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region ../../plugins/acryl-door/src/host/door.ts
|
|
192
|
+
/**
|
|
193
|
+
* The door's verbs, one implementation for every face: `brief`, `look`, `act`, `plugin list|add|remove|reload` and `tool list|call`
|
|
194
|
+
* (`verbs.ts` lists them; the operator client, MCP and the built-in chat all call these). Each verb takes plain JSON arguments,
|
|
195
|
+
* returns a typed value and a text rendering, and fails with a {@link DoorError} or a {@link UiControlError} carrying a code.
|
|
196
|
+
*/
|
|
197
|
+
var DoorError = class extends Error {
|
|
198
|
+
code;
|
|
199
|
+
constructor(code, message) {
|
|
200
|
+
super(message);
|
|
201
|
+
this.code = code;
|
|
202
|
+
this.name = "DoorError";
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
/** The files an agent should read first, when the app has them. */
|
|
206
|
+
const SELF_KNOWLEDGE = [
|
|
207
|
+
"AGENTS.md",
|
|
208
|
+
"CLAUDE.md",
|
|
209
|
+
".agents/skills",
|
|
210
|
+
".claude/skills",
|
|
211
|
+
"docs",
|
|
212
|
+
"plugins"
|
|
213
|
+
];
|
|
214
|
+
var Door = class {
|
|
215
|
+
deps;
|
|
216
|
+
constructor(deps) {
|
|
217
|
+
this.deps = deps;
|
|
218
|
+
}
|
|
219
|
+
/** Run one verb. @throws DoorError or UiControlError. */
|
|
220
|
+
async run(verb, args, signal) {
|
|
221
|
+
const input = typeof args === "object" && args !== null && !Array.isArray(args) ? args : {};
|
|
222
|
+
switch (verb) {
|
|
223
|
+
case "brief": return this.brief();
|
|
224
|
+
case "look": {
|
|
225
|
+
const snapshot = await this.look(input, signal);
|
|
226
|
+
return {
|
|
227
|
+
value: snapshot,
|
|
228
|
+
text: renderSnapshot(snapshot, (cursor) => `run look --cursor ${String(cursor)}`)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
case "act": {
|
|
232
|
+
const result = await this.act(input, signal);
|
|
233
|
+
return {
|
|
234
|
+
value: result,
|
|
235
|
+
text: `done${result.target === void 0 ? "" : `: ${result.target.role} "${result.target.name}"`}${result.detail === void 0 ? "" : ` (${result.detail})`}; look again before the next act`
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
case "plugin-list": return this.plugin((extensions) => extensions.list());
|
|
239
|
+
case "plugin-add": return this.plugin((extensions) => extensions.install(this.insideApp(input.folder)));
|
|
240
|
+
case "plugin-remove": {
|
|
241
|
+
const name = input.name;
|
|
242
|
+
if (typeof name !== "string" || name === "") throw new DoorError("invalid", "plugin remove needs a plugin name");
|
|
243
|
+
return this.plugin((extensions) => extensions.remove(name));
|
|
244
|
+
}
|
|
245
|
+
case "plugin-reload": return this.plugin((extensions) => extensions.reload());
|
|
246
|
+
case "tool-list": {
|
|
247
|
+
const offered = this.offeredTools();
|
|
248
|
+
return {
|
|
249
|
+
value: offered,
|
|
250
|
+
text: offered.length === 0 ? "no app tools are offered through the door (tools.expose is empty)" : offered.map((tool) => `${tool.name}: ${tool.description}`).join("\n")
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
case "tool-call": return this.callTool(input);
|
|
254
|
+
default: throw new DoorError("unknown-verb", `unknown verb ${JSON.stringify(verb)}; the verbs are ${VERBS.join(", ")}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
brief() {
|
|
258
|
+
const { name, root } = this.deps.app;
|
|
259
|
+
const read = SELF_KNOWLEDGE.filter((path) => existsSync(resolve(root, path)));
|
|
260
|
+
const value = {
|
|
261
|
+
app: name,
|
|
262
|
+
root,
|
|
263
|
+
read,
|
|
264
|
+
page: this.deps.channel.connected ? "open" : "not open",
|
|
265
|
+
plugins: this.deps.extensions() === void 0 ? "unavailable" : "available",
|
|
266
|
+
verbs: VERBS.map((verb) => VERB_SPECS[verb].usage)
|
|
267
|
+
};
|
|
268
|
+
return {
|
|
269
|
+
value,
|
|
270
|
+
text: [
|
|
271
|
+
`App ${name} in ${root}.`,
|
|
272
|
+
read.length === 0 ? "No AGENTS.md or skill in the folder." : `Read first: ${read.join(", ")}.`,
|
|
273
|
+
`Page: ${value.page}${value.page === "not open" ? " (open the app in a browser to look and act)" : ""}.`,
|
|
274
|
+
`Live plugins: ${value.plugins}.`,
|
|
275
|
+
"Verbs:",
|
|
276
|
+
...value.verbs.map((line) => ` ${line}`)
|
|
277
|
+
].join("\n")
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
look(input, signal) {
|
|
281
|
+
const request = this.parse({
|
|
282
|
+
...input,
|
|
283
|
+
op: "snapshot"
|
|
284
|
+
});
|
|
285
|
+
return this.callPage("look", request, signal);
|
|
286
|
+
}
|
|
287
|
+
act(input, signal) {
|
|
288
|
+
if (input.op === "snapshot") throw new DoorError("invalid", "use look to see the page");
|
|
289
|
+
return this.callPage(`act ${String(input.op)}`, this.parse(input), signal);
|
|
290
|
+
}
|
|
291
|
+
parse(input) {
|
|
292
|
+
try {
|
|
293
|
+
return parseUiRequest(input);
|
|
294
|
+
} catch (cause) {
|
|
295
|
+
throw new DoorError("invalid", cause instanceof Error ? cause.message : "invalid request");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/** Run one call on the page and record it. The audit holds which control was touched and how it ended, never what was typed. */
|
|
299
|
+
async callPage(verb, request, signal) {
|
|
300
|
+
const approval = "token";
|
|
301
|
+
try {
|
|
302
|
+
const value = await this.deps.channel.call(request, {
|
|
303
|
+
signal,
|
|
304
|
+
...request.op === "wait" ? { timeoutMs: (request.timeoutMs ?? 3e3) + 5e3 } : {}
|
|
305
|
+
});
|
|
306
|
+
const target = "target" in value ? value.target : void 0;
|
|
307
|
+
this.deps.audit.record({
|
|
308
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
309
|
+
tool: verb,
|
|
310
|
+
...target === void 0 ? {} : { target },
|
|
311
|
+
outcome: "ok",
|
|
312
|
+
approval
|
|
313
|
+
});
|
|
314
|
+
return value;
|
|
315
|
+
} catch (cause) {
|
|
316
|
+
const code = cause instanceof UiControlError ? cause.code : "failed";
|
|
317
|
+
const refused = cause instanceof UiControlError && [
|
|
318
|
+
"protected",
|
|
319
|
+
"sensitive",
|
|
320
|
+
"killed",
|
|
321
|
+
"invalid",
|
|
322
|
+
"stale-ref",
|
|
323
|
+
"unknown-ref",
|
|
324
|
+
"not-actionable"
|
|
325
|
+
].includes(cause.code);
|
|
326
|
+
this.deps.audit.record({
|
|
327
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
328
|
+
tool: verb,
|
|
329
|
+
outcome: refused ? "refused" : "failed",
|
|
330
|
+
detail: code,
|
|
331
|
+
approval
|
|
332
|
+
});
|
|
333
|
+
throw cause;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async plugin(run) {
|
|
337
|
+
const extensions = this.deps.extensions();
|
|
338
|
+
if (extensions === void 0) throw new DoorError("unavailable", "this app has no extensions plugin, so plugins cannot be installed while it runs");
|
|
339
|
+
const value = await run(extensions);
|
|
340
|
+
return {
|
|
341
|
+
value,
|
|
342
|
+
text: JSON.stringify(value, null, 2)
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/** A folder named by an agent, resolved from the app folder; it must be inside the app. */
|
|
346
|
+
insideApp(folder) {
|
|
347
|
+
if (typeof folder !== "string" || folder === "") throw new DoorError("invalid", "plugin add needs a folder, e.g. plugins/pomodoro");
|
|
348
|
+
const root = this.deps.app.root;
|
|
349
|
+
const full = resolve(root, folder);
|
|
350
|
+
const path = relative(root, full);
|
|
351
|
+
if (path === "" || path.startsWith("..") || isAbsolute(path)) throw new DoorError("outside-app", `${folder} is not a folder inside the app (${root})`);
|
|
352
|
+
return full;
|
|
353
|
+
}
|
|
354
|
+
offeredTools() {
|
|
355
|
+
const tools = this.deps.tools();
|
|
356
|
+
if (tools === void 0) return [];
|
|
357
|
+
const allowed = new Set(this.deps.expose);
|
|
358
|
+
return tools.list().filter((tool) => allowed.has(tool.name)).map(({ name, description }) => ({
|
|
359
|
+
name,
|
|
360
|
+
description
|
|
361
|
+
}));
|
|
362
|
+
}
|
|
363
|
+
async callTool(input) {
|
|
364
|
+
const name = input.tool;
|
|
365
|
+
if (typeof name !== "string" || name === "") throw new DoorError("invalid", "tool call needs --tool <name>");
|
|
366
|
+
if (!this.deps.expose.includes(name)) throw new DoorError("not-exposed", `${name} is not offered through the door`);
|
|
367
|
+
const tools = this.deps.tools();
|
|
368
|
+
if (tools === void 0 || !tools.list().some((tool) => tool.name === name)) throw new DoorError("unavailable", `${name} is not available right now`);
|
|
369
|
+
let value;
|
|
370
|
+
try {
|
|
371
|
+
value = await tools.call(name, input.args ?? {});
|
|
372
|
+
} catch (cause) {
|
|
373
|
+
throw new DoorError("failed", cause instanceof Error ? cause.message : String(cause));
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
value,
|
|
377
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region ../../plugins/acryl-door/src/host/chat-tools.ts
|
|
383
|
+
/**
|
|
384
|
+
* The door in the built-in chat: each verb registered in the app's `tools` registry (the one the chat's agent reads), as
|
|
385
|
+
* `door_<verb>`. A tool answers the verb's text, what an agent reads in a terminal; a failed verb throws with its code, as the operator
|
|
386
|
+
* client prints it.
|
|
387
|
+
*/
|
|
388
|
+
/** Register every verb. @returns the function that removes them all; it also aborts the calls still running. */
|
|
389
|
+
function registerChatTools(tools, door) {
|
|
390
|
+
const running = new AbortController();
|
|
391
|
+
const removers = [];
|
|
392
|
+
const removeAll = () => {
|
|
393
|
+
running.abort();
|
|
394
|
+
for (const remove of removers.splice(0).reverse()) remove();
|
|
395
|
+
};
|
|
396
|
+
try {
|
|
397
|
+
for (const verb of VERBS) removers.push(tools.register(chatToolName(verb), {
|
|
398
|
+
description: VERB_SPECS[verb].description,
|
|
399
|
+
run: async (input) => {
|
|
400
|
+
try {
|
|
401
|
+
return (await door.run(verb, input ?? {}, running.signal)).text;
|
|
402
|
+
} catch (cause) {
|
|
403
|
+
if (cause instanceof DoorError || cause instanceof UiControlError) throw new Error(`${cause.code}: ${cause.message}`, { cause });
|
|
404
|
+
throw cause;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}));
|
|
408
|
+
} catch (cause) {
|
|
409
|
+
removeAll();
|
|
410
|
+
throw cause;
|
|
411
|
+
}
|
|
412
|
+
return removeAll;
|
|
413
|
+
}
|
|
414
|
+
//#endregion
|
|
415
|
+
//#region ../../plugins/acryl-door/src/host/door-file.ts
|
|
416
|
+
/**
|
|
417
|
+
* The door's connection file, `<app>/.acryl/door.json`: where the running app listens and this run's secret, for the operator client in
|
|
418
|
+
* the same folder. The secret is never on a command line or in an environment variable another process can read.
|
|
419
|
+
*
|
|
420
|
+
* Protection: on macOS and Linux the folder is `0700` and the file `0600`, so only the OS user who started the app can read it. On
|
|
421
|
+
* Windows file modes mean nothing; the file is protected by where it lives, the app folder in the user's profile, whose ACL gives access
|
|
422
|
+
* to that user, SYSTEM and Administrators only. The door does not widen it. The folder also carries its own `.gitignore` of `*`, so
|
|
423
|
+
* `git add -A` never stages the secret even in an app whose `.gitignore` lacks the `.acryl/` line.
|
|
424
|
+
*/
|
|
425
|
+
/** The folder of the door's state below the app folder. */
|
|
426
|
+
const STATE_DIR = ".acryl";
|
|
427
|
+
const DOOR_FILE = "door.json";
|
|
428
|
+
function stateDir(appRoot) {
|
|
429
|
+
return join(appRoot, STATE_DIR);
|
|
430
|
+
}
|
|
431
|
+
function doorFilePath(appRoot) {
|
|
432
|
+
return join(stateDir(appRoot), DOOR_FILE);
|
|
433
|
+
}
|
|
434
|
+
/** A fresh secret for this run; a restart rotates it. */
|
|
435
|
+
function newSecret() {
|
|
436
|
+
return randomBytes(24).toString("hex");
|
|
437
|
+
}
|
|
438
|
+
/** The state folder, private, with its own `.gitignore`. */
|
|
439
|
+
function ensureStateDir(appRoot) {
|
|
440
|
+
const dir = stateDir(appRoot);
|
|
441
|
+
mkdirSync(dir, {
|
|
442
|
+
recursive: true,
|
|
443
|
+
mode: 448
|
|
444
|
+
});
|
|
445
|
+
if (process.platform !== "win32") chmodSync(dir, 448);
|
|
446
|
+
const ignore = join(dir, ".gitignore");
|
|
447
|
+
if (!existsSync(ignore)) writeFileSync(ignore, "# The door's state: this run's secret and the audit. Never commit it.\n*\n");
|
|
448
|
+
return dir;
|
|
449
|
+
}
|
|
450
|
+
/** The connection file as written, or undefined when there is none or it does not parse. */
|
|
451
|
+
function readDoorFile(appRoot) {
|
|
452
|
+
try {
|
|
453
|
+
const value = JSON.parse(readFileSync(doorFilePath(appRoot), "utf8"));
|
|
454
|
+
if (value.version === 1 && typeof value.url === "string" && typeof value.secret === "string" && typeof value.pid === "number") return value;
|
|
455
|
+
} catch {}
|
|
456
|
+
}
|
|
457
|
+
/** True while the process is alive (a process of another user still counts: `EPERM`). */
|
|
458
|
+
function alive(pid) {
|
|
459
|
+
try {
|
|
460
|
+
process.kill(pid, 0);
|
|
461
|
+
return true;
|
|
462
|
+
} catch (cause) {
|
|
463
|
+
return cause.code === "EPERM";
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Write this run's connection file, replacing a stale one.
|
|
468
|
+
* @throws when another live process already serves this folder's door: two apps from one folder would race for one file.
|
|
469
|
+
*/
|
|
470
|
+
function writeDoorFile(appRoot, url, secret) {
|
|
471
|
+
const dir = ensureStateDir(appRoot);
|
|
472
|
+
const existing = readDoorFile(appRoot);
|
|
473
|
+
if (existing !== void 0 && existing.pid !== process.pid && alive(existing.pid)) throw new Error(`acryl-door: another process (pid ${String(existing.pid)}) serves the door of ${appRoot}; stop it first`);
|
|
474
|
+
const file = doorFilePath(appRoot);
|
|
475
|
+
const temporary = join(dir, `${DOOR_FILE}.${String(process.pid)}.tmp`);
|
|
476
|
+
const content = {
|
|
477
|
+
version: 1,
|
|
478
|
+
url,
|
|
479
|
+
secret,
|
|
480
|
+
pid: process.pid
|
|
481
|
+
};
|
|
482
|
+
writeFileSync(temporary, `${JSON.stringify(content)}\n`, { mode: 384 });
|
|
483
|
+
if (process.platform !== "win32") chmodSync(temporary, 384);
|
|
484
|
+
renameSync(temporary, file);
|
|
485
|
+
return file;
|
|
486
|
+
}
|
|
487
|
+
/** Remove the connection file, only when it is this run's: a newer run's file is left alone. */
|
|
488
|
+
function removeDoorFile(appRoot, secret) {
|
|
489
|
+
if (readDoorFile(appRoot)?.secret === secret) rmSync(doorFilePath(appRoot), { force: true });
|
|
490
|
+
}
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region ../../plugins/acryl-door/src/host/mcp-routes.ts
|
|
493
|
+
/**
|
|
494
|
+
* The door over MCP: `POST <prefix>/mcp`, streamable HTTP in its request/response form (one JSON-RPC message or a batch in, its JSON
|
|
495
|
+
* answer out, 202 for notifications only; no event stream is offered). Each verb is one tool, named as the verb, with the arguments the
|
|
496
|
+
* operator client sends. The same guard as the operator's routes: loopback, this run's secret.
|
|
497
|
+
*/
|
|
498
|
+
const SUPPORTED_PROTOCOLS = [
|
|
499
|
+
"2025-06-18",
|
|
500
|
+
"2025-03-26",
|
|
501
|
+
"2024-11-05"
|
|
502
|
+
];
|
|
503
|
+
const SERVER_INFO = {
|
|
504
|
+
name: "acryl-door",
|
|
505
|
+
version: "1"
|
|
506
|
+
};
|
|
507
|
+
function isRequest(value) {
|
|
508
|
+
return typeof value === "object" && value !== null && value.jsonrpc === "2.0" && typeof value.method === "string";
|
|
509
|
+
}
|
|
510
|
+
const rpcError = (id, code, message) => ({
|
|
511
|
+
jsonrpc: "2.0",
|
|
512
|
+
id: id ?? null,
|
|
513
|
+
error: {
|
|
514
|
+
code,
|
|
515
|
+
message
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
const result = (id, value) => ({
|
|
519
|
+
jsonrpc: "2.0",
|
|
520
|
+
id: id ?? null,
|
|
521
|
+
result: value
|
|
522
|
+
});
|
|
523
|
+
/** The verbs as MCP tools. */
|
|
524
|
+
function mcpTools() {
|
|
525
|
+
return VERBS.map((verb) => ({
|
|
526
|
+
name: verb,
|
|
527
|
+
description: VERB_SPECS[verb].description,
|
|
528
|
+
inputSchema: VERB_SPECS[verb].arguments
|
|
529
|
+
}));
|
|
530
|
+
}
|
|
531
|
+
const isVerb = (name) => typeof name === "string" && VERBS.includes(name);
|
|
532
|
+
/** One JSON-RPC message; `undefined` for a notification (no answer owed). */
|
|
533
|
+
async function answer(message, door, signal) {
|
|
534
|
+
const isNotification = message.id === void 0;
|
|
535
|
+
switch (message.method) {
|
|
536
|
+
case "initialize": {
|
|
537
|
+
const asked = message.params?.protocolVersion;
|
|
538
|
+
const protocolVersion = SUPPORTED_PROTOCOLS.includes(asked) ? asked : SUPPORTED_PROTOCOLS[0];
|
|
539
|
+
return result(message.id, {
|
|
540
|
+
protocolVersion,
|
|
541
|
+
capabilities: { tools: { listChanged: false } },
|
|
542
|
+
serverInfo: SERVER_INFO
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
case "ping": return isNotification ? void 0 : result(message.id, {});
|
|
546
|
+
case "tools/list": return result(message.id, { tools: mcpTools() });
|
|
547
|
+
case "tools/call": {
|
|
548
|
+
const params = typeof message.params === "object" && message.params !== null ? message.params : {};
|
|
549
|
+
if (!isVerb(params.name)) return rpcError(message.id, -32602, `unknown tool ${JSON.stringify(params.name)}; the tools are ${VERBS.join(", ")}`);
|
|
550
|
+
try {
|
|
551
|
+
const verbAnswer = await door.run(params.name, params.arguments ?? {}, signal);
|
|
552
|
+
return result(message.id, {
|
|
553
|
+
content: [{
|
|
554
|
+
type: "text",
|
|
555
|
+
text: verbAnswer.text
|
|
556
|
+
}],
|
|
557
|
+
structuredContent: { value: verbAnswer.value },
|
|
558
|
+
isError: false
|
|
559
|
+
});
|
|
560
|
+
} catch (cause) {
|
|
561
|
+
if (cause instanceof DoorError || cause instanceof UiControlError) return result(message.id, {
|
|
562
|
+
content: [{
|
|
563
|
+
type: "text",
|
|
564
|
+
text: `${cause.code}: ${cause.message}`
|
|
565
|
+
}],
|
|
566
|
+
isError: true
|
|
567
|
+
});
|
|
568
|
+
throw cause;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
default: return isNotification ? void 0 : rpcError(message.id, -32601, `method not found: ${message.method}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function mcpRoutes(prefix, secret, door, reportError) {
|
|
575
|
+
return new Acryl().post(`${prefix}/mcp`, operatorOnly(secret), jsonBody, async (c) => {
|
|
576
|
+
const body = await readJson(c);
|
|
577
|
+
if (body === INVALID_JSON) return c.json(rpcError(null, -32700, "parse error"), 400);
|
|
578
|
+
try {
|
|
579
|
+
const messages = Array.isArray(body) ? body : [body];
|
|
580
|
+
const answers = [];
|
|
581
|
+
for (const message of messages) {
|
|
582
|
+
if (!isRequest(message)) {
|
|
583
|
+
answers.push(rpcError(null, -32600, "invalid request"));
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
const response = await answer(message, door, c.req.raw.signal);
|
|
587
|
+
if (response !== void 0) answers.push(response);
|
|
588
|
+
}
|
|
589
|
+
if (answers.length === 0) return c.body(null, 202);
|
|
590
|
+
return c.json(Array.isArray(body) ? answers : answers[0]);
|
|
591
|
+
} catch (cause) {
|
|
592
|
+
reportError("answer an MCP request", cause);
|
|
593
|
+
return c.json(rpcError(null, -32603, "internal error"), 500);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
//#endregion
|
|
598
|
+
//#region ../../plugins/acryl-door/src/host/operator-routes.ts
|
|
599
|
+
/**
|
|
600
|
+
* The operator's routes: an agent's shell on this machine, through `.acryl/bin/acryl`. Loopback only, with this run's secret.
|
|
601
|
+
*
|
|
602
|
+
* `POST <prefix>/call` takes `{ verb, args }` and answers {@link CallResponse}; `GET <prefix>/verbs` lists the verbs. A caller that goes
|
|
603
|
+
* away aborts its call through the request's signal.
|
|
604
|
+
*/
|
|
605
|
+
function operatorRoutes(prefix, secret, door, reportError) {
|
|
606
|
+
return new Acryl().get(`${prefix}/verbs`, operatorOnly(secret), (c) => c.json({ verbs: VERBS })).post(`${prefix}/call`, operatorOnly(secret), jsonBody, async (c) => {
|
|
607
|
+
const body = await readJson(c);
|
|
608
|
+
if (body === INVALID_JSON || typeof body !== "object" || body === null || typeof body.verb !== "string") return c.json({
|
|
609
|
+
ok: false,
|
|
610
|
+
code: "invalid",
|
|
611
|
+
message: "the body is { verb, args }"
|
|
612
|
+
}, 400);
|
|
613
|
+
const { verb, args } = body;
|
|
614
|
+
try {
|
|
615
|
+
const answer = await door.run(verb, args, c.req.raw.signal);
|
|
616
|
+
return c.json({
|
|
617
|
+
ok: true,
|
|
618
|
+
value: answer.value,
|
|
619
|
+
text: answer.text
|
|
620
|
+
});
|
|
621
|
+
} catch (cause) {
|
|
622
|
+
if (cause instanceof UiControlError || cause instanceof DoorError) return c.json({
|
|
623
|
+
ok: false,
|
|
624
|
+
code: cause.code,
|
|
625
|
+
message: cause.message
|
|
626
|
+
}, cause.code === "invalid" || cause.code === "unknown-verb" ? 400 : 200);
|
|
627
|
+
reportError(`run the door verb ${verb}`, cause);
|
|
628
|
+
return c.json(error("the verb could not be run"), 500);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
/** A word for `sh`: single-quoted, with each `'` closed, escaped and reopened. */
|
|
633
|
+
function shWord(value) {
|
|
634
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
635
|
+
}
|
|
636
|
+
/** A word for `cmd.exe`: double-quoted; `%` doubled so a batch file does not expand it. A path cannot hold `"`. */
|
|
637
|
+
function cmdWord(value) {
|
|
638
|
+
return `"${value.replaceAll("%", "%%")}"`;
|
|
639
|
+
}
|
|
640
|
+
function posixShim(root, target) {
|
|
641
|
+
const words = [
|
|
642
|
+
target.node,
|
|
643
|
+
...target.nodeOptions,
|
|
644
|
+
target.client,
|
|
645
|
+
"--dir",
|
|
646
|
+
root
|
|
647
|
+
].map(shWord).join(" ");
|
|
648
|
+
return `#!/bin/sh\n# This app's door (acryl-door): reaches only the app running from this folder. Rewritten each time the app starts.\n${target.electron ? "export ELECTRON_RUN_AS_NODE=1\n" : ""}exec ${words} "$@"\n`;
|
|
649
|
+
}
|
|
650
|
+
function windowsShim(root, target) {
|
|
651
|
+
const words = [
|
|
652
|
+
target.node,
|
|
653
|
+
...target.nodeOptions,
|
|
654
|
+
target.client,
|
|
655
|
+
"--dir",
|
|
656
|
+
root
|
|
657
|
+
].map(cmdWord).join(" ");
|
|
658
|
+
return `@echo off\r\nsetlocal\r\nrem This app's door (acryl-door): reaches only the app running from this folder. Rewritten each time the app starts.\r\n${target.electron ? "set ELECTRON_RUN_AS_NODE=1\r\n" : ""}${words} %*\r\n`;
|
|
659
|
+
}
|
|
660
|
+
function writeAtomically(file, content, mode) {
|
|
661
|
+
const temporary = `${file}.${String(process.pid)}.tmp`;
|
|
662
|
+
writeFileSync(temporary, content, { mode });
|
|
663
|
+
if (process.platform !== "win32") chmodSync(temporary, mode);
|
|
664
|
+
renameSync(temporary, file);
|
|
665
|
+
}
|
|
666
|
+
/** Write `.acryl/bin/acryl` and `.acryl/bin/acryl.cmd` for the app at `root`. @returns the folder they are in. */
|
|
667
|
+
function writeShims(root, target) {
|
|
668
|
+
const bin = join(ensureStateDir(root), "bin");
|
|
669
|
+
mkdirSync(bin, {
|
|
670
|
+
recursive: true,
|
|
671
|
+
mode: 448
|
|
672
|
+
});
|
|
673
|
+
if (process.platform !== "win32") chmodSync(bin, 448);
|
|
674
|
+
writeAtomically(join(bin, "acryl"), posixShim(root, target), 448);
|
|
675
|
+
writeAtomically(join(bin, "acryl.cmd"), windowsShim(root, target), 384);
|
|
676
|
+
return bin;
|
|
677
|
+
}
|
|
678
|
+
//#endregion
|
|
679
|
+
//#region ../../plugins/acryl-door/src/index.ts
|
|
680
|
+
/**
|
|
681
|
+
* `acryl-door`: the Door of the five nouns, as a Cordis plugin of a framework app (spec 049 slice 9). An agent working in the app folder
|
|
682
|
+
* looks at the running page, acts on it, and installs plugins live, through `.acryl/bin/acryl`; everything else it does with its own
|
|
683
|
+
* file tools and shell.
|
|
684
|
+
*
|
|
685
|
+
* The page loads `/api/acryl-door/page.js` (one script tag) and keeps an event stream open to the Host; the Host sends it calls. An
|
|
686
|
+
* operator on this machine reaches `/api/acryl-door/call` with the secret from `<app>/.acryl/door.json`. One effect owns the channel and
|
|
687
|
+
* the audit, the routes are mounted after it, and a second effect writes the connection file last, so disposal removes the file first,
|
|
688
|
+
* then the routes (their in-flight calls end), then closes the channel (every pending call settles).
|
|
689
|
+
*/
|
|
690
|
+
/** Stable Cordis plugin name, equal to the package name and the Loader row id. */
|
|
691
|
+
const name = "acryl-door";
|
|
692
|
+
const inject = [
|
|
693
|
+
"router",
|
|
694
|
+
"appInstance",
|
|
695
|
+
"nodeServer"
|
|
696
|
+
];
|
|
697
|
+
const Config = z.object({ tools: z.object({ expose: z.array(z.string().pattern(/^(?!door_)[A-Za-z][A-Za-z0-9_-]{0,63}$/u)).description("App tools (from the tools registry) the door offers; none by default.") }) });
|
|
698
|
+
/** The page module, served compiled from source by the app's own TypeScript. */
|
|
699
|
+
const PAGE_DIR = join(import.meta.dirname, import.meta.url.endsWith(".ts") ? "page" : join("..", "src", "page"));
|
|
700
|
+
/** The operator client this build runs: the source next to this file, or the built `cli.js`. */
|
|
701
|
+
const CLIENT = import.meta.url.endsWith(".ts") ? join(import.meta.dirname, "cli", "acryl.ts") : join(import.meta.dirname, "cli.js");
|
|
702
|
+
/** How the folder's `.acryl/bin/acryl` starts the client: with the Node that runs the app, and its type-stripping flag when run from source. */
|
|
703
|
+
function shimTarget() {
|
|
704
|
+
const fromSource = CLIENT.endsWith(".ts");
|
|
705
|
+
return {
|
|
706
|
+
node: process.execPath,
|
|
707
|
+
nodeOptions: fromSource ? process.execArgv.filter((option) => /^--experimental-(strip|transform)-types$/u.test(option)) : [],
|
|
708
|
+
client: CLIENT,
|
|
709
|
+
electron: process.versions.electron !== void 0
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
/** A listening address as one the operator reaches: an any-address bind is reached on loopback. */
|
|
713
|
+
function loopbackUrl(url) {
|
|
714
|
+
const parsed = new URL(url);
|
|
715
|
+
if (parsed.hostname === "0.0.0.0" || parsed.hostname === "[::]") parsed.hostname = "127.0.0.1";
|
|
716
|
+
return parsed.origin;
|
|
717
|
+
}
|
|
718
|
+
function apply(ctx, rawConfig = {}) {
|
|
719
|
+
const config = Config(rawConfig);
|
|
720
|
+
const app = ctx.get("appInstance");
|
|
721
|
+
const server = ctx.get("nodeServer");
|
|
722
|
+
if (typeof app?.root !== "string") throw new Error("acryl-door: the appInstance service has no app folder (root)");
|
|
723
|
+
const reportError = (operation, cause) => {
|
|
724
|
+
ctx.logger?.error?.(`acryl-door: failed to ${operation}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
725
|
+
};
|
|
726
|
+
const secret = newSecret();
|
|
727
|
+
const channel = new UiChannel();
|
|
728
|
+
const door = new Door({
|
|
729
|
+
channel,
|
|
730
|
+
audit: new AuditLog(auditPath(stateDir(app.root))),
|
|
731
|
+
app: {
|
|
732
|
+
name: app.name,
|
|
733
|
+
root: app.root
|
|
734
|
+
},
|
|
735
|
+
extensions: () => ctx.get("extensions"),
|
|
736
|
+
tools: () => ctx.get("tools"),
|
|
737
|
+
expose: config.tools?.expose ?? []
|
|
738
|
+
});
|
|
739
|
+
ctx.effect(() => () => {
|
|
740
|
+
channel.close();
|
|
741
|
+
}, "acryl-door: page channel");
|
|
742
|
+
const pageModule = new Acryl().get(`${DOOR_PREFIX}/page.js`, (c) => c.body(`import '${DOOR_PREFIX}/page/main.ts'\n`, 200, { "content-type": "text/javascript; charset=utf-8" })).route(`${DOOR_PREFIX}/page`, clientModules({ root: PAGE_DIR }));
|
|
743
|
+
ctx.router.route("/", privateApi(DOOR_PREFIX, pageModule, pageChannelRoutes(channel, { prefix: DOOR_PREFIX }), operatorRoutes(DOOR_PREFIX, secret, door, reportError), mcpRoutes(DOOR_PREFIX, secret, door, reportError)));
|
|
744
|
+
ctx.inject(["tools"], (inner) => {
|
|
745
|
+
inner.effect(() => registerChatTools(inner.get("tools"), door), "acryl-door: the verbs in the tools registry");
|
|
746
|
+
});
|
|
747
|
+
ctx.effect(function* () {
|
|
748
|
+
yield ctx.reflect.provide("door", door);
|
|
749
|
+
writeDoorFile(app.root, loopbackUrl(server.url), secret);
|
|
750
|
+
writeShims(app.root, shimTarget());
|
|
751
|
+
yield () => {
|
|
752
|
+
removeDoorFile(app.root, secret);
|
|
753
|
+
};
|
|
754
|
+
}, "acryl-door: the door service, .acryl/door.json and .acryl/bin/acryl");
|
|
755
|
+
}
|
|
756
|
+
//#endregion
|
|
757
|
+
export { Config, DOOR_PREFIX, Door, DoorError, STATE_DIR, VERBS, VERB_SPECS, apply, chatToolName, doorFilePath, inject, mcpTools, name, readDoorFile };
|