openmausbot 0.1.55 → 0.1.56
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/assets/{index-B9rLIY81.js → index-D0xt96rv.js} +15 -15
- package/dist/assets/{index-D-u0LYsV.js → index-Dq2j4Ikr.js} +1 -1
- package/dist/index.html +1 -1
- package/dist-server/companion/src/routes.js +213 -0
- package/dist-server/index.js +252 -24
- package/dist-server/openmausbot.js +45 -386
- package/dist-server/pair-cli.js +45 -386
- package/dist-server/server/auto-approve.js +24 -0
- package/dist-server/server/bot-profile.js +17 -0
- package/dist-server/server/channel-queue.js +1 -0
- package/dist-server/server/computer-control.js +15 -0
- package/dist-server/server/exit.js +9 -0
- package/dist-server/server/index.js +119 -22
- package/dist-server/server/openmausbot.js +3 -2
- package/dist-server/server/pair-cli.js +3 -2
- package/dist-server/server/peer-approval.js +27 -0
- package/dist-server/server/peer-provenance.js +4 -1
- package/dist-server/server/peer-roster.js +15 -0
- package/dist-server/server/request-auth.js +10 -0
- package/dist-server/server/tunnel.js +1 -1
- package/dist-server/tunnel-guardian.js +6 -19
- package/package.json +1 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// What a paired device is allowed to ask for.
|
|
2
|
+
//
|
|
3
|
+
// The default is deny, and that direction is the whole point: the sidecar
|
|
4
|
+
// sits in front of an API it does not own and cannot see the future of. A
|
|
5
|
+
// route that appears in the harness later is closed to phones until someone
|
|
6
|
+
// decides otherwise, because the alternative is that every upstream release
|
|
7
|
+
// silently widens what a lost phone can reach.
|
|
8
|
+
//
|
|
9
|
+
// This file used to claim that and not do it — it listed refusals and let
|
|
10
|
+
// everything else under `/api/` through. In the time between writing it and
|
|
11
|
+
// noticing, upstream added webhook triggers, connected-app authorisation and
|
|
12
|
+
// routines, all of which a paired phone could drive: minting an
|
|
13
|
+
// internet-reachable trigger, rotating a signing secret out from under
|
|
14
|
+
// whatever was sending to it, disconnecting a Google account. None of that
|
|
15
|
+
// was a decision anyone made. It was the default.
|
|
16
|
+
//
|
|
17
|
+
// So the list below is the surface, derived from what the app actually
|
|
18
|
+
// calls. Adding a feature to the phone means adding its route here, on
|
|
19
|
+
// purpose, in a diff someone can read. That cost is the feature.
|
|
20
|
+
/** The one companion route that crosses into full interactive desktop
|
|
21
|
+
* control. Both the allowlist and capability gate consume this classifier so
|
|
22
|
+
* their security decisions cannot drift apart. */
|
|
23
|
+
export const CLOUD_DESKTOP_JOIN_ROUTE = {
|
|
24
|
+
method: "POST",
|
|
25
|
+
path: /^\/api\/bots\/[\w-]+\/computer\/join$/,
|
|
26
|
+
};
|
|
27
|
+
/** A POST whose response is file bytes. Keep this exact classifier shared
|
|
28
|
+
* with the proxy: a `.json` document must not enter the ordinary JSON
|
|
29
|
+
* scrub/re-serialise path and come back as different bytes. */
|
|
30
|
+
export const MESSAGE_FILE_ROUTE = {
|
|
31
|
+
method: "POST",
|
|
32
|
+
path: /^\/api\/threads\/[\w-]+\/messages\/[\w-]+\/file$/,
|
|
33
|
+
};
|
|
34
|
+
export const CLOUD_DESKTOP_CONTROL_ROUTE = {
|
|
35
|
+
method: "POST",
|
|
36
|
+
path: /^\/api\/bots\/[\w-]+\/computer\/(?:control|screenshot|viewer-close)$/,
|
|
37
|
+
};
|
|
38
|
+
export function isCloudDesktopJoin(method, path) {
|
|
39
|
+
return method === CLOUD_DESKTOP_JOIN_ROUTE.method && CLOUD_DESKTOP_JOIN_ROUTE.path.test(path);
|
|
40
|
+
}
|
|
41
|
+
export function isMessageFileDownload(method, path) {
|
|
42
|
+
return method === MESSAGE_FILE_ROUTE.method && MESSAGE_FILE_ROUTE.path.test(path);
|
|
43
|
+
}
|
|
44
|
+
export function isCloudDesktopAccess(method, path) {
|
|
45
|
+
return isCloudDesktopJoin(method, path)
|
|
46
|
+
|| (method === CLOUD_DESKTOP_CONTROL_ROUTE.method && CLOUD_DESKTOP_CONTROL_ROUTE.path.test(path));
|
|
47
|
+
}
|
|
48
|
+
/** Every request the iOS app makes, and nothing else.
|
|
49
|
+
*
|
|
50
|
+
* Ids are `[\w-]+`, matching the harness's own route patterns. The paths
|
|
51
|
+
* arrive undecoded and are anchored at both ends, so an encoded traversal
|
|
52
|
+
* fails to match and is denied rather than forwarded — the failure mode of
|
|
53
|
+
* a strict pattern is a closed door, which is the one to have. */
|
|
54
|
+
const ALLOWED = [
|
|
55
|
+
// configured-or-not booleans. The write side is refused below: reading
|
|
56
|
+
// which providers are set up is not reading their keys.
|
|
57
|
+
{ method: "GET", path: /^\/api\/config$/ },
|
|
58
|
+
{ method: "GET", path: /^\/api\/events$/ },
|
|
59
|
+
{ method: "GET", path: /^\/api\/instances$/ },
|
|
60
|
+
{ method: "GET", path: /^\/api\/team-map$/ },
|
|
61
|
+
// Sidecar-owned, authenticated endpoint metadata. The proxy terminates it
|
|
62
|
+
// locally; it never becomes a newly exposed harness route.
|
|
63
|
+
{ method: "GET", path: /^\/api\/companion\/endpoints$/ },
|
|
64
|
+
// the fleet, and making a bot
|
|
65
|
+
{ method: "GET", path: /^\/api\/bots$/ },
|
|
66
|
+
{ method: "POST", path: /^\/api\/bots$/ },
|
|
67
|
+
// One narrow, atomic organizer write. This can only file visible bots;
|
|
68
|
+
// unlike the desktop's broad PATCH it cannot alter execution policy.
|
|
69
|
+
{ method: "POST", path: /^\/api\/sidebar-sections$/ },
|
|
70
|
+
{ method: "POST", path: /^\/api\/bots\/[\w-]+\/messages$/ },
|
|
71
|
+
{ method: "PATCH", path: /^\/api\/bots\/[\w-]+\/cards\/[\w-]+$/ },
|
|
72
|
+
{ method: "POST", path: /^\/api\/bots\/[\w-]+\/respond$/ },
|
|
73
|
+
{ method: "POST", path: /^\/api\/bots\/[\w-]+\/interrupt$/ },
|
|
74
|
+
{ method: "DELETE", path: /^\/api\/bots\/[\w-]+\/queue\/[\w-]+$/ },
|
|
75
|
+
{ method: "POST", path: /^\/api\/bots\/[\w-]+\/read$/ },
|
|
76
|
+
{ method: "POST", path: /^\/api\/bots\/[\w-]+\/always-allow$/ },
|
|
77
|
+
{ method: "POST", path: /^\/api\/bots\/[\w-]+\/messages\/[\w-]+\/edit$/ },
|
|
78
|
+
// A credential value crosses this one route only as an HPKE envelope. The
|
|
79
|
+
// server binds it to the authenticated device and exact pending card before
|
|
80
|
+
// Electron opens it into the OS-encrypted credential store.
|
|
81
|
+
{ method: "POST", path: /^\/api\/bots\/[\w-]+\/secret-cards\/[\w-]+\/provide$/ },
|
|
82
|
+
{ method: "POST", path: /^\/api\/bots\/[\w-]+\/active-branch$/ },
|
|
83
|
+
{ method: "POST", path: /^\/api\/bots\/[\w-]+\/tasks$/ },
|
|
84
|
+
{ method: "POST", path: /^\/api\/bots\/[\w-]+\/tasks\/[\w-]+$/ },
|
|
85
|
+
{ method: "PATCH", path: /^\/api\/bots\/[\w-]+\/tasks\/[\w-]+$/ },
|
|
86
|
+
{ method: "DELETE", path: /^\/api\/bots\/[\w-]+\/tasks\/[\w-]+$/ },
|
|
87
|
+
// Paired-safe profile subset. The harness route itself rejects fields
|
|
88
|
+
// outside identity, avatar, notifications, and voice preferences.
|
|
89
|
+
{ method: "PATCH", path: /^\/api\/bots\/[\w-]+\/profile$/ },
|
|
90
|
+
// Full model selection, but no other bot settings. The harness validates
|
|
91
|
+
// the live catalog and refuses changes while the bot is working.
|
|
92
|
+
{ method: "PATCH", path: /^\/api\/bots\/[\w-]+\/model$/ },
|
|
93
|
+
{ method: "POST", path: /^\/api\/bots\/[\w-]+\/avatar\/generate$/ },
|
|
94
|
+
// Full cloud desktop access. The route is narrow and the proxy applies a
|
|
95
|
+
// second, per-device capability check before it reaches the harness.
|
|
96
|
+
CLOUD_DESKTOP_JOIN_ROUTE,
|
|
97
|
+
CLOUD_DESKTOP_CONTROL_ROUTE,
|
|
98
|
+
// rooms — making one, and talking in one
|
|
99
|
+
{ method: "POST", path: /^\/api\/groups$/ },
|
|
100
|
+
{ method: "POST", path: /^\/api\/groups\/[\w-]+\/messages$/ },
|
|
101
|
+
{ method: "POST", path: /^\/api\/groups\/[\w-]+\/interrupt$/ },
|
|
102
|
+
{ method: "DELETE", path: /^\/api\/groups\/[\w-]+\/queue\/[\w-]+$/ },
|
|
103
|
+
{ method: "POST", path: /^\/api\/groups\/[\w-]+\/read$/ },
|
|
104
|
+
{ method: "POST", path: /^\/api\/groups\/[\w-]+\/tasks$/ },
|
|
105
|
+
{ method: "POST", path: /^\/api\/groups\/[\w-]+\/tasks\/[\w-]+$/ },
|
|
106
|
+
{ method: "PATCH", path: /^\/api\/groups\/[\w-]+\/tasks\/[\w-]+$/ },
|
|
107
|
+
{ method: "DELETE", path: /^\/api\/groups\/[\w-]+\/tasks\/[\w-]+$/ },
|
|
108
|
+
// a transcript, its images, and answering an approval
|
|
109
|
+
{ method: "GET", path: /^\/api\/threads\/[\w-]+\/messages$/ },
|
|
110
|
+
{ method: "GET", path: /^\/api\/threads\/[\w-]+\/messages\/[\w-]+\/image$/ },
|
|
111
|
+
MESSAGE_FILE_ROUTE,
|
|
112
|
+
{ method: "POST", path: /^\/api\/threads\/[\w-]+\/messages\/[\w-]+\/reactions$/ },
|
|
113
|
+
{ method: "GET", path: /^\/api\/threads\/[\w-]+\/export$/ },
|
|
114
|
+
{ method: "POST", path: /^\/api\/threads\/[\w-]+\/respond$/ },
|
|
115
|
+
{ method: "GET", path: /^\/api\/search$/ },
|
|
116
|
+
// App-owned profile images. Upload is image-only and capped at 10 MB by
|
|
117
|
+
// the harness; GET is a single bare generated filename, never a path.
|
|
118
|
+
{ method: "POST", path: /^\/api\/attachments$/ },
|
|
119
|
+
{ method: "GET", path: /^\/api\/attachments\/[\w-]+\.(?:png|jpe?g|gif|webp)$/i },
|
|
120
|
+
// Share-sheet documents are raw, capped at 25 MiB, and stored under a
|
|
121
|
+
// generated filename by the harness. The display name stays in the query;
|
|
122
|
+
// only this exact upload route crosses the companion boundary.
|
|
123
|
+
{ method: "POST", path: /^\/api\/files$/ },
|
|
124
|
+
// Renderer-neutral voice operations. These routes never expose or mutate
|
|
125
|
+
// the workspace ElevenLabs key; the client receives labels or audio only.
|
|
126
|
+
{ method: "GET", path: /^\/api\/tts\/voices$/ },
|
|
127
|
+
{ method: "POST", path: /^\/api\/tts\/prepare$/ },
|
|
128
|
+
{ method: "POST", path: /^\/api\/tts\/speak$/ },
|
|
129
|
+
// Routines create ordinary tasks using an existing agent configuration.
|
|
130
|
+
// Webhook management remains explicitly denied below.
|
|
131
|
+
{ method: "GET", path: /^\/api\/routines$/ },
|
|
132
|
+
{ method: "POST", path: /^\/api\/routines$/ },
|
|
133
|
+
{ method: "PATCH", path: /^\/api\/routines\/[\w-]+$/ },
|
|
134
|
+
{ method: "DELETE", path: /^\/api\/routines\/[\w-]+$/ },
|
|
135
|
+
{ method: "POST", path: /^\/api\/routines\/[\w-]+\/run$/ },
|
|
136
|
+
{ method: "POST", path: /^\/api\/routine-runs\/[\w-]+\/(?:cancel|seen)$/ },
|
|
137
|
+
// Multi-account Composio management exposes opaque ids and aliases only.
|
|
138
|
+
// Revocation stays on the host: the account DELETE route is deliberately
|
|
139
|
+
// absent — a paired client can see and add accounts, never remove one.
|
|
140
|
+
{ method: "GET", path: /^\/api\/connectors\/catalog$/ },
|
|
141
|
+
{ method: "GET", path: /^\/api\/connectors\/connected$/ },
|
|
142
|
+
{ method: "GET", path: /^\/api\/connectors$/ },
|
|
143
|
+
{ method: "POST", path: /^\/api\/connectors\/[\w-]+\/authorize$/ },
|
|
144
|
+
// Inline connector cards are scoped by bot, transcript message, and
|
|
145
|
+
// thread. They expose the same opaque OAuth authorization already allowed
|
|
146
|
+
// above, then only poll, resume, or dismiss that exact pending card.
|
|
147
|
+
{ method: "GET", path: /^\/api\/bots\/[\w-]+\/connector-cards\/[\w-]+\/status$/ },
|
|
148
|
+
{ method: "POST", path: /^\/api\/bots\/[\w-]+\/connector-cards\/[\w-]+\/(?:authorize|resume|dismiss)$/ },
|
|
149
|
+
// A remote client may decline or retry a credential request, but never
|
|
150
|
+
// claim that it stored a host credential. Saving and `provided` stay local
|
|
151
|
+
// to the host's OS-backed credential store.
|
|
152
|
+
{ method: "POST", path: /^\/api\/bots\/[\w-]+\/secret-cards\/[\w-]+\/(?:resume|dismiss)$/ },
|
|
153
|
+
];
|
|
154
|
+
/** Route families worth naming in the refusal.
|
|
155
|
+
*
|
|
156
|
+
* Everything not allowed is denied either way; this only decides whether the
|
|
157
|
+
* person gets a sentence or a 404. These are the ones someone might
|
|
158
|
+
* reasonably expect to work from the phone, where "no route" would read as a
|
|
159
|
+
* bug in the companion rather than a decision about where host configuration
|
|
160
|
+
* happens. Order matters only in that the first match wins. */
|
|
161
|
+
const EXPLAINED = [
|
|
162
|
+
{
|
|
163
|
+
path: /^\/api\/(companion|devices)(\/|$)/,
|
|
164
|
+
// Losing the phone must not mean losing the ability to lock it out.
|
|
165
|
+
error: "Remote access settings are managed on the host computer",
|
|
166
|
+
},
|
|
167
|
+
{ path: /^\/api\/config$/, error: "API keys can only be changed on your computer" },
|
|
168
|
+
{ path: /^\/api\/local-computer(\/|$)/, error: "the Local VM is set up on your computer" },
|
|
169
|
+
{
|
|
170
|
+
// Creating one exposes an endpoint to the internet, and rotating a
|
|
171
|
+
// secret breaks whatever was sending to it. Neither belongs on a device
|
|
172
|
+
// that lives in a pocket.
|
|
173
|
+
path: /^\/api\/webhooks(\/|$)/,
|
|
174
|
+
error: "webhooks are set up on your computer",
|
|
175
|
+
},
|
|
176
|
+
{ path: /^\/api\/connectors(\/|$)/, error: "connected apps are set up on your computer" },
|
|
177
|
+
{
|
|
178
|
+
path: /^\/api\/routines(\/|$)/,
|
|
179
|
+
error: "this routine operation is only available on your computer",
|
|
180
|
+
},
|
|
181
|
+
{ path: /^\/api\/teams(\/|$)/, error: "teams are imported and exported on your computer" },
|
|
182
|
+
];
|
|
183
|
+
/** Why this request may not go through, or null when it may.
|
|
184
|
+
*
|
|
185
|
+
* Default deny: the answer for anything not on the list is "no route", which
|
|
186
|
+
* is what keeps a stolen token from mapping the API. An allowlist rather than
|
|
187
|
+
* a blocklist is the property this whole module exists for, and the one that
|
|
188
|
+
* quietly stopped being true once before. */
|
|
189
|
+
export function denyReason({ path, method, authenticated }) {
|
|
190
|
+
// Pairing is the one thing a device does before it has a credential.
|
|
191
|
+
if (method === "POST" && path === "/api/pair")
|
|
192
|
+
return null;
|
|
193
|
+
// Liveness is the other: it exists to be the first thing anyone curls when
|
|
194
|
+
// pairing will not work, and behind the token check it answered 401 to
|
|
195
|
+
// exactly the person it was for — which reads as "broken" rather than
|
|
196
|
+
// "unpaired". It discloses nothing a port scan would not.
|
|
197
|
+
if (method === "GET" && path === "/api/health")
|
|
198
|
+
return null;
|
|
199
|
+
if (!authenticated) {
|
|
200
|
+
return { status: 401, error: "pair this device from Remote access settings on the host computer" };
|
|
201
|
+
}
|
|
202
|
+
if (ALLOWED.some((route) => route.method === method && route.path.test(path)))
|
|
203
|
+
return null;
|
|
204
|
+
const explained = EXPLAINED.find((family) => family.path.test(path));
|
|
205
|
+
if (explained)
|
|
206
|
+
return { status: 403, error: explained.error };
|
|
207
|
+
// Everything else, including routes the harness really does have. Saying
|
|
208
|
+
// "no route" rather than "not allowed" keeps the sidecar from enumerating
|
|
209
|
+
// the API to anyone holding a stolen token — and it is what the peer-agent
|
|
210
|
+
// endpoints under /api/internal/ always got, since off this machine they
|
|
211
|
+
// genuinely do not exist.
|
|
212
|
+
return { status: 404, error: `no route: ${method} ${path}` };
|
|
213
|
+
}
|