openmausbot 0.1.55 → 0.1.57
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-B433sv8K.css +1 -0
- package/dist/assets/index-DSh8AIUy.js +264 -0
- package/dist/assets/{index-D-u0LYsV.js → index-jWqt5dmY.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/companion/src/routes.js +218 -0
- package/dist-server/connector-proxy.js +31 -12
- package/dist-server/container-mcp.js +49 -31
- package/dist-server/drivers/agents-proxy.js +56 -2
- package/dist-server/index.js +2020 -361
- package/dist-server/openmausbot.js +45 -386
- package/dist-server/pair-cli.js +45 -386
- package/dist-server/server/auto-approve.js +48 -0
- package/dist-server/server/bot-folder.js +102 -0
- package/dist-server/server/bot-overview.js +206 -0
- package/dist-server/server/bot-package.js +5 -0
- package/dist-server/server/bot-profile.js +24 -0
- package/dist-server/server/channel-queue.js +1 -0
- package/dist-server/server/computer-control.js +15 -0
- package/dist-server/server/connector-proxy.js +37 -16
- package/dist-server/server/container-computer.js +9 -2
- package/dist-server/server/drivers/agents-proxy.js +63 -2
- package/dist-server/server/drivers/antigravity-acp.js +61 -13
- package/dist-server/server/drivers/antigravity-runtime.js +47 -3
- package/dist-server/server/drivers/claude.js +7 -1
- package/dist-server/server/exit.js +9 -0
- package/dist-server/server/index.js +707 -124
- package/dist-server/server/mcp-bridge.js +73 -44
- package/dist-server/server/openmausbot.js +3 -2
- package/dist-server/server/package-export.js +1 -0
- 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/profile-requests.js +332 -0
- package/dist-server/server/profile-revision.js +15 -0
- package/dist-server/server/profile-versions.js +154 -0
- package/dist-server/server/request-auth.js +10 -0
- package/dist-server/server/routine-requests.js +34 -1
- package/dist-server/server/routines.js +70 -2
- package/dist-server/server/setup-mode.js +63 -0
- package/dist-server/server/skill-fetch.js +67 -11
- package/dist-server/server/skills.js +1 -1
- package/dist-server/server/store.js +87 -7
- package/dist-server/server/system-prompt.js +49 -0
- package/dist-server/server/team-backup.js +2 -2
- package/dist-server/server/team-manifest.js +7 -0
- package/dist-server/server/tunnel.js +1 -1
- package/dist-server/shared/bot-profile.js +5 -1
- package/dist-server/shared/line-diff.js +42 -0
- package/dist-server/shared/profile-request.js +9 -0
- package/dist-server/shared/team-backup.js +4 -0
- package/dist-server/tunnel-guardian.js +6 -19
- package/dist-server/vps-container-mcp.js +49 -31
- package/package.json +1 -1
- package/dist/assets/index-B9rLIY81.js +0 -263
- package/dist/assets/index-CJBrtDbC.css +0 -1
|
@@ -0,0 +1,218 @@
|
|
|
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
|
+
// Read-only summary: who the bot is, what it does and won't do, and its
|
|
88
|
+
// recent activity. No settings, no transcript — read on open and on
|
|
89
|
+
// pull-to-refresh.
|
|
90
|
+
{ method: "GET", path: /^\/api\/bots\/[\w-]+\/overview$/ },
|
|
91
|
+
// Paired-safe profile subset. The harness route itself rejects fields
|
|
92
|
+
// outside identity, standing instructions (soul, byte-capped), avatar,
|
|
93
|
+
// notifications, and voice preferences.
|
|
94
|
+
{ method: "PATCH", path: /^\/api\/bots\/[\w-]+\/profile$/ },
|
|
95
|
+
// Full model selection, but no other bot settings. The harness validates
|
|
96
|
+
// the live catalog and refuses changes while the bot is working.
|
|
97
|
+
{ method: "PATCH", path: /^\/api\/bots\/[\w-]+\/model$/ },
|
|
98
|
+
{ method: "POST", path: /^\/api\/bots\/[\w-]+\/avatar\/generate$/ },
|
|
99
|
+
// Full cloud desktop access. The route is narrow and the proxy applies a
|
|
100
|
+
// second, per-device capability check before it reaches the harness.
|
|
101
|
+
CLOUD_DESKTOP_JOIN_ROUTE,
|
|
102
|
+
CLOUD_DESKTOP_CONTROL_ROUTE,
|
|
103
|
+
// rooms — making one, and talking in one
|
|
104
|
+
{ method: "POST", path: /^\/api\/groups$/ },
|
|
105
|
+
{ method: "POST", path: /^\/api\/groups\/[\w-]+\/messages$/ },
|
|
106
|
+
{ method: "POST", path: /^\/api\/groups\/[\w-]+\/interrupt$/ },
|
|
107
|
+
{ method: "DELETE", path: /^\/api\/groups\/[\w-]+\/queue\/[\w-]+$/ },
|
|
108
|
+
{ method: "POST", path: /^\/api\/groups\/[\w-]+\/read$/ },
|
|
109
|
+
{ method: "POST", path: /^\/api\/groups\/[\w-]+\/tasks$/ },
|
|
110
|
+
{ method: "POST", path: /^\/api\/groups\/[\w-]+\/tasks\/[\w-]+$/ },
|
|
111
|
+
{ method: "PATCH", path: /^\/api\/groups\/[\w-]+\/tasks\/[\w-]+$/ },
|
|
112
|
+
{ method: "DELETE", path: /^\/api\/groups\/[\w-]+\/tasks\/[\w-]+$/ },
|
|
113
|
+
// a transcript, its images, and answering an approval
|
|
114
|
+
{ method: "GET", path: /^\/api\/threads\/[\w-]+\/messages$/ },
|
|
115
|
+
{ method: "GET", path: /^\/api\/threads\/[\w-]+\/messages\/[\w-]+\/image$/ },
|
|
116
|
+
MESSAGE_FILE_ROUTE,
|
|
117
|
+
{ method: "POST", path: /^\/api\/threads\/[\w-]+\/messages\/[\w-]+\/reactions$/ },
|
|
118
|
+
{ method: "GET", path: /^\/api\/threads\/[\w-]+\/export$/ },
|
|
119
|
+
{ method: "POST", path: /^\/api\/threads\/[\w-]+\/respond$/ },
|
|
120
|
+
{ method: "GET", path: /^\/api\/search$/ },
|
|
121
|
+
// App-owned profile images. Upload is image-only and capped at 10 MB by
|
|
122
|
+
// the harness; GET is a single bare generated filename, never a path.
|
|
123
|
+
{ method: "POST", path: /^\/api\/attachments$/ },
|
|
124
|
+
{ method: "GET", path: /^\/api\/attachments\/[\w-]+\.(?:png|jpe?g|gif|webp)$/i },
|
|
125
|
+
// Share-sheet documents are raw, capped at 25 MiB, and stored under a
|
|
126
|
+
// generated filename by the harness. The display name stays in the query;
|
|
127
|
+
// only this exact upload route crosses the companion boundary.
|
|
128
|
+
{ method: "POST", path: /^\/api\/files$/ },
|
|
129
|
+
// Renderer-neutral voice operations. These routes never expose or mutate
|
|
130
|
+
// the workspace ElevenLabs key; the client receives labels or audio only.
|
|
131
|
+
{ method: "GET", path: /^\/api\/tts\/voices$/ },
|
|
132
|
+
{ method: "POST", path: /^\/api\/tts\/prepare$/ },
|
|
133
|
+
{ method: "POST", path: /^\/api\/tts\/speak$/ },
|
|
134
|
+
// Routines create ordinary tasks using an existing agent configuration.
|
|
135
|
+
// Webhook management remains explicitly denied below.
|
|
136
|
+
{ method: "GET", path: /^\/api\/routines$/ },
|
|
137
|
+
{ method: "POST", path: /^\/api\/routines$/ },
|
|
138
|
+
{ method: "PATCH", path: /^\/api\/routines\/[\w-]+$/ },
|
|
139
|
+
{ method: "DELETE", path: /^\/api\/routines\/[\w-]+$/ },
|
|
140
|
+
{ method: "POST", path: /^\/api\/routines\/[\w-]+\/run$/ },
|
|
141
|
+
{ method: "POST", path: /^\/api\/routine-runs\/[\w-]+\/(?:cancel|seen)$/ },
|
|
142
|
+
// Multi-account Composio management exposes opaque ids and aliases only.
|
|
143
|
+
// Revocation stays on the host: the account DELETE route is deliberately
|
|
144
|
+
// absent — a paired client can see and add accounts, never remove one.
|
|
145
|
+
{ method: "GET", path: /^\/api\/connectors\/catalog$/ },
|
|
146
|
+
{ method: "GET", path: /^\/api\/connectors\/connected$/ },
|
|
147
|
+
{ method: "GET", path: /^\/api\/connectors$/ },
|
|
148
|
+
{ method: "POST", path: /^\/api\/connectors\/[\w-]+\/authorize$/ },
|
|
149
|
+
// Inline connector cards are scoped by bot, transcript message, and
|
|
150
|
+
// thread. They expose the same opaque OAuth authorization already allowed
|
|
151
|
+
// above, then only poll, resume, or dismiss that exact pending card.
|
|
152
|
+
{ method: "GET", path: /^\/api\/bots\/[\w-]+\/connector-cards\/[\w-]+\/status$/ },
|
|
153
|
+
{ method: "POST", path: /^\/api\/bots\/[\w-]+\/connector-cards\/[\w-]+\/(?:authorize|resume|dismiss)$/ },
|
|
154
|
+
// A remote client may decline or retry a credential request, but never
|
|
155
|
+
// claim that it stored a host credential. Saving and `provided` stay local
|
|
156
|
+
// to the host's OS-backed credential store.
|
|
157
|
+
{ method: "POST", path: /^\/api\/bots\/[\w-]+\/secret-cards\/[\w-]+\/(?:resume|dismiss)$/ },
|
|
158
|
+
];
|
|
159
|
+
/** Route families worth naming in the refusal.
|
|
160
|
+
*
|
|
161
|
+
* Everything not allowed is denied either way; this only decides whether the
|
|
162
|
+
* person gets a sentence or a 404. These are the ones someone might
|
|
163
|
+
* reasonably expect to work from the phone, where "no route" would read as a
|
|
164
|
+
* bug in the companion rather than a decision about where host configuration
|
|
165
|
+
* happens. Order matters only in that the first match wins. */
|
|
166
|
+
const EXPLAINED = [
|
|
167
|
+
{
|
|
168
|
+
path: /^\/api\/(companion|devices)(\/|$)/,
|
|
169
|
+
// Losing the phone must not mean losing the ability to lock it out.
|
|
170
|
+
error: "Remote access settings are managed on the host computer",
|
|
171
|
+
},
|
|
172
|
+
{ path: /^\/api\/config$/, error: "API keys can only be changed on your computer" },
|
|
173
|
+
{ path: /^\/api\/local-computer(\/|$)/, error: "the Local VM is set up on your computer" },
|
|
174
|
+
{
|
|
175
|
+
// Creating one exposes an endpoint to the internet, and rotating a
|
|
176
|
+
// secret breaks whatever was sending to it. Neither belongs on a device
|
|
177
|
+
// that lives in a pocket.
|
|
178
|
+
path: /^\/api\/webhooks(\/|$)/,
|
|
179
|
+
error: "webhooks are set up on your computer",
|
|
180
|
+
},
|
|
181
|
+
{ path: /^\/api\/connectors(\/|$)/, error: "connected apps are set up on your computer" },
|
|
182
|
+
{
|
|
183
|
+
path: /^\/api\/routines(\/|$)/,
|
|
184
|
+
error: "this routine operation is only available on your computer",
|
|
185
|
+
},
|
|
186
|
+
{ path: /^\/api\/teams(\/|$)/, error: "teams are imported and exported on your computer" },
|
|
187
|
+
];
|
|
188
|
+
/** Why this request may not go through, or null when it may.
|
|
189
|
+
*
|
|
190
|
+
* Default deny: the answer for anything not on the list is "no route", which
|
|
191
|
+
* is what keeps a stolen token from mapping the API. An allowlist rather than
|
|
192
|
+
* a blocklist is the property this whole module exists for, and the one that
|
|
193
|
+
* quietly stopped being true once before. */
|
|
194
|
+
export function denyReason({ path, method, authenticated }) {
|
|
195
|
+
// Pairing is the one thing a device does before it has a credential.
|
|
196
|
+
if (method === "POST" && path === "/api/pair")
|
|
197
|
+
return null;
|
|
198
|
+
// Liveness is the other: it exists to be the first thing anyone curls when
|
|
199
|
+
// pairing will not work, and behind the token check it answered 401 to
|
|
200
|
+
// exactly the person it was for — which reads as "broken" rather than
|
|
201
|
+
// "unpaired". It discloses nothing a port scan would not.
|
|
202
|
+
if (method === "GET" && path === "/api/health")
|
|
203
|
+
return null;
|
|
204
|
+
if (!authenticated) {
|
|
205
|
+
return { status: 401, error: "pair this device from Remote access settings on the host computer" };
|
|
206
|
+
}
|
|
207
|
+
if (ALLOWED.some((route) => route.method === method && route.path.test(path)))
|
|
208
|
+
return null;
|
|
209
|
+
const explained = EXPLAINED.find((family) => family.path.test(path));
|
|
210
|
+
if (explained)
|
|
211
|
+
return { status: 403, error: explained.error };
|
|
212
|
+
// Everything else, including routes the harness really does have. Saying
|
|
213
|
+
// "no route" rather than "not allowed" keeps the sidecar from enumerating
|
|
214
|
+
// the API to anyone holding a stolen token — and it is what the peer-agent
|
|
215
|
+
// endpoints under /api/internal/ always got, since off this machine they
|
|
216
|
+
// genuinely do not exist.
|
|
217
|
+
return { status: 404, error: `no route: ${method} ${path}` };
|
|
218
|
+
}
|
|
@@ -96,20 +96,38 @@ function connectorAdds(args) {
|
|
|
96
96
|
if (!args || typeof args !== "object" || Array.isArray(args)) return [];
|
|
97
97
|
const toolkits = args.toolkits;
|
|
98
98
|
if (!Array.isArray(toolkits)) return [];
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
const seen = /* @__PURE__ */ new Set();
|
|
100
|
+
const requests = [];
|
|
101
|
+
for (const item of toolkits) {
|
|
102
|
+
if (typeof item === "string") {
|
|
103
|
+
const slug2 = item.trim().toLowerCase();
|
|
104
|
+
const key2 = JSON.stringify([slug2, ""]);
|
|
105
|
+
if (!seen.has(key2)) {
|
|
106
|
+
seen.add(key2);
|
|
107
|
+
requests.push({ slug: slug2 });
|
|
108
|
+
}
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
102
112
|
const row = item;
|
|
103
|
-
const slug = typeof row.toolkit === "string" ? row.toolkit : row.name;
|
|
104
113
|
const action = String(row.action ?? "add").toLowerCase();
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
if (!["add", "connect", "initiate"].includes(action)) continue;
|
|
115
|
+
const slug = typeof row.toolkit === "string" ? row.toolkit : row.name;
|
|
116
|
+
if (typeof slug !== "string") continue;
|
|
117
|
+
const normalized = slug.trim().toLowerCase();
|
|
118
|
+
const alias = (typeof row.alias === "string" ? row.alias : typeof row.account === "string" ? row.account : "").trim();
|
|
119
|
+
const key = JSON.stringify([normalized, alias.toLowerCase()]);
|
|
120
|
+
if (seen.has(key)) continue;
|
|
121
|
+
seen.add(key);
|
|
122
|
+
requests.push({ slug: normalized, ...alias ? { alias } : {} });
|
|
123
|
+
}
|
|
124
|
+
return requests;
|
|
107
125
|
}
|
|
108
|
-
async function showConnectorCards(
|
|
126
|
+
async function showConnectorCards(items) {
|
|
109
127
|
const response = await fetch(`${HARNESS}/api/internal/connectors/request`, {
|
|
110
128
|
method: "POST",
|
|
111
129
|
headers: { "content-type": "application/json", authorization: `Bearer ${TOKEN}` },
|
|
112
|
-
body: JSON.stringify({ botId: BOT_ID, threadId: THREAD_ID,
|
|
130
|
+
body: JSON.stringify({ botId: BOT_ID, threadId: THREAD_ID, items, resumeKey: randomUUID() }),
|
|
113
131
|
signal: AbortSignal.timeout(3e4)
|
|
114
132
|
});
|
|
115
133
|
if (!response.ok) {
|
|
@@ -141,12 +159,13 @@ async function handle(message) {
|
|
|
141
159
|
if (method === "tools/call") {
|
|
142
160
|
const params = message.params ?? {};
|
|
143
161
|
const name = String(params.name ?? "");
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
146
|
-
await showConnectorCards(
|
|
162
|
+
const requests = /MANAGE_CONNECTIONS$/i.test(name) ? connectorAdds(params.arguments) : [];
|
|
163
|
+
if (requests.length) {
|
|
164
|
+
await showConnectorCards(requests);
|
|
165
|
+
const labels = requests.map((r) => r.alias ? `${r.slug} (${r.alias})` : r.slug).join(", ");
|
|
147
166
|
send(textResult(
|
|
148
167
|
id,
|
|
149
|
-
`OpenMausBot showed the user a secure connection card for ${
|
|
168
|
+
`OpenMausBot showed the user a secure connection card for ${labels}. End this turn now. The app will continue the task automatically after the connection finishes.`
|
|
150
169
|
));
|
|
151
170
|
return;
|
|
152
171
|
}
|
|
@@ -15082,6 +15082,28 @@ function createGateInterceptor(options) {
|
|
|
15082
15082
|
});
|
|
15083
15083
|
};
|
|
15084
15084
|
}
|
|
15085
|
+
function createMcpBridgeInterceptor(options) {
|
|
15086
|
+
const afterPing = options.gate ? createGateInterceptor({
|
|
15087
|
+
isHeld: options.gate.isHeld,
|
|
15088
|
+
forward: options.forward,
|
|
15089
|
+
refuse: options.answer,
|
|
15090
|
+
refusalText: options.gate.refusalText
|
|
15091
|
+
}) : options.forward;
|
|
15092
|
+
return (line) => {
|
|
15093
|
+
let frame = null;
|
|
15094
|
+
try {
|
|
15095
|
+
frame = JSON.parse(line);
|
|
15096
|
+
} catch {
|
|
15097
|
+
}
|
|
15098
|
+
if (frame && frame.method === "ping") {
|
|
15099
|
+
if (frame.id !== void 0) {
|
|
15100
|
+
options.answer(JSON.stringify({ jsonrpc: "2.0", id: frame.id, result: {} }));
|
|
15101
|
+
}
|
|
15102
|
+
return;
|
|
15103
|
+
}
|
|
15104
|
+
afterPing(line);
|
|
15105
|
+
};
|
|
15106
|
+
}
|
|
15085
15107
|
function runMcpBridge(options) {
|
|
15086
15108
|
const child = spawn(options.command, options.args, {
|
|
15087
15109
|
shell: false,
|
|
@@ -15091,37 +15113,33 @@ function runMcpBridge(options) {
|
|
|
15091
15113
|
child.stdin.on("error", () => {
|
|
15092
15114
|
});
|
|
15093
15115
|
child.stderr.pipe(process.stderr);
|
|
15094
|
-
|
|
15095
|
-
|
|
15096
|
-
|
|
15097
|
-
|
|
15098
|
-
|
|
15099
|
-
|
|
15100
|
-
|
|
15101
|
-
|
|
15102
|
-
|
|
15103
|
-
|
|
15104
|
-
|
|
15105
|
-
|
|
15106
|
-
|
|
15107
|
-
|
|
15108
|
-
|
|
15109
|
-
|
|
15110
|
-
|
|
15111
|
-
|
|
15112
|
-
child.
|
|
15113
|
-
|
|
15114
|
-
|
|
15115
|
-
|
|
15116
|
-
|
|
15117
|
-
|
|
15118
|
-
process.stdin.
|
|
15119
|
-
|
|
15120
|
-
|
|
15121
|
-
process.stdin.unpipe(child.stdin);
|
|
15122
|
-
process.stdin.pause();
|
|
15123
|
-
};
|
|
15124
|
-
}
|
|
15116
|
+
const client = options.gate ? createControlClient({ url: options.gate.url, token: options.gate.token }) : null;
|
|
15117
|
+
const answer = (line) => process.stdout.write(line + "\n");
|
|
15118
|
+
const forward = (line) => child.stdin.write(line + "\n");
|
|
15119
|
+
const inbound = createLineSplitter(
|
|
15120
|
+
createMcpBridgeInterceptor({
|
|
15121
|
+
answer,
|
|
15122
|
+
forward,
|
|
15123
|
+
...options.gate ? {
|
|
15124
|
+
gate: {
|
|
15125
|
+
isHeld: async () => (await client.state(true)).held
|
|
15126
|
+
}
|
|
15127
|
+
} : {}
|
|
15128
|
+
})
|
|
15129
|
+
);
|
|
15130
|
+
const onStdin = (chunk) => inbound.push(chunk);
|
|
15131
|
+
process.stdin.on("data", onStdin);
|
|
15132
|
+
process.stdin.on("end", () => {
|
|
15133
|
+
inbound.flush();
|
|
15134
|
+
child.stdin.end();
|
|
15135
|
+
});
|
|
15136
|
+
const outbound = createLineSplitter((line) => process.stdout.write(line + "\n"));
|
|
15137
|
+
child.stdout.on("data", (chunk) => outbound.push(chunk));
|
|
15138
|
+
child.stdout.on("end", () => outbound.flush());
|
|
15139
|
+
const detach = () => {
|
|
15140
|
+
process.stdin.off("data", onStdin);
|
|
15141
|
+
process.stdin.pause();
|
|
15142
|
+
};
|
|
15125
15143
|
let watchdog = null;
|
|
15126
15144
|
if (options.liveness) {
|
|
15127
15145
|
const liveness = options.liveness;
|
|
@@ -192,6 +192,10 @@ var ROUTINE_FIELDS_SCHEMA = {
|
|
|
192
192
|
clear_timeout: {
|
|
193
193
|
type: "boolean",
|
|
194
194
|
description: "Only for updates: set true to remove an existing safety limit. Do not combine with timeout_minutes."
|
|
195
|
+
},
|
|
196
|
+
continuity: {
|
|
197
|
+
type: "boolean",
|
|
198
|
+
description: "Opt in to using the latest completed run's bounded report as historical context. Defaults to false; set false in an update to start fresh again. Shown on the confirmation card."
|
|
195
199
|
}
|
|
196
200
|
};
|
|
197
201
|
var TOOLS = [
|
|
@@ -371,6 +375,31 @@ var TOOLS = [
|
|
|
371
375
|
required: ["routine_id", "action"]
|
|
372
376
|
}
|
|
373
377
|
},
|
|
378
|
+
{
|
|
379
|
+
name: "propose_profile",
|
|
380
|
+
description: "Propose changes to your own name, title, description, standing instructions (SOUL.md), or working folder (cwd). This only creates a confirmation card; nothing changes until the user approves it. After calling it, end the turn and do not claim the change is applied. Keep SOUL.md short \u2014 who you are and the rules you never break; put step-by-step procedure into a skill instead. A Chief of Staff may pass for_bot_id (from list_bots) to propose a change for another bot in its section.",
|
|
381
|
+
inputSchema: {
|
|
382
|
+
type: "object",
|
|
383
|
+
additionalProperties: false,
|
|
384
|
+
properties: {
|
|
385
|
+
name: { type: "string", maxLength: 100, description: "New display name." },
|
|
386
|
+
title: { type: "string", maxLength: 200, description: "New role or title." },
|
|
387
|
+
description: { type: "string", maxLength: 4e3, description: "New one-line blurb shown in rosters." },
|
|
388
|
+
soul: { type: "string", description: "Full replacement text for SOUL.md, at most 24000 bytes." },
|
|
389
|
+
cwd: {
|
|
390
|
+
type: "string",
|
|
391
|
+
maxLength: 1024,
|
|
392
|
+
description: "Absolute path of the folder your tools read and write in (for example /Users/me/Projects/site). It must already exist. An empty string means your private workspace."
|
|
393
|
+
},
|
|
394
|
+
reason: { type: "string", minLength: 1, maxLength: 500, description: "One sentence the user will see explaining why." },
|
|
395
|
+
for_bot_id: {
|
|
396
|
+
type: "string",
|
|
397
|
+
description: "Chief of Staff only: the id of another bot in your section whose profile this changes. Omit to change your own."
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
required: ["reason"]
|
|
401
|
+
}
|
|
402
|
+
},
|
|
374
403
|
{
|
|
375
404
|
name: "skills_list",
|
|
376
405
|
description: "List this bot's imported skills (enabled and disabled) and any staged skill writes waiting for the user to confirm. Use this before skill_manage to avoid duplicate names. Listing does not enable anything.",
|
|
@@ -445,16 +474,17 @@ function routineFields(args) {
|
|
|
445
474
|
if (typeof args.run_on === "string") fields.runOn = args.run_on;
|
|
446
475
|
if (args.clear_timeout === true) fields.timeoutMinutes = null;
|
|
447
476
|
else if (typeof args.timeout_minutes === "number") fields.timeoutMinutes = args.timeout_minutes;
|
|
477
|
+
if (typeof args.continuity === "boolean") fields.continuity = args.continuity;
|
|
448
478
|
return { fields };
|
|
449
479
|
}
|
|
450
|
-
function confirmationResult(r, fallback) {
|
|
480
|
+
function confirmationResult(r, fallback, noun = "routine") {
|
|
451
481
|
const summary = typeof r.summary === "string" && r.summary.trim() ? `
|
|
452
482
|
|
|
453
483
|
${r.summary.trim()}` : "";
|
|
454
484
|
return {
|
|
455
485
|
text: `A confirmation card is now visible to the user for ${fallback}.${summary}
|
|
456
486
|
|
|
457
|
-
This change has not been applied yet. End this turn and wait for the user to confirm or deny the card; do not claim the
|
|
487
|
+
This change has not been applied yet. End this turn and wait for the user to confirm or deny the card; do not claim the ${noun} was created or changed before confirmation.`
|
|
458
488
|
};
|
|
459
489
|
}
|
|
460
490
|
function recallSpeaker(hit) {
|
|
@@ -723,6 +753,30 @@ ${JSON.stringify(routines, null, 2)}`
|
|
|
723
753
|
});
|
|
724
754
|
return confirmationResult(r, `${action.replace("_", " ")} on routine ${routineId}`);
|
|
725
755
|
}
|
|
756
|
+
if (name === "propose_profile") {
|
|
757
|
+
const changes = {};
|
|
758
|
+
if (typeof args.name === "string") changes.name = args.name.trim();
|
|
759
|
+
if (typeof args.title === "string") changes.title = args.title.trim();
|
|
760
|
+
if (typeof args.description === "string") changes.description = args.description.trim();
|
|
761
|
+
if (typeof args.soul === "string") changes.soul = args.soul;
|
|
762
|
+
if (typeof args.cwd === "string") changes.cwd = args.cwd.trim();
|
|
763
|
+
if (!Object.keys(changes).length) {
|
|
764
|
+
return { text: "propose_profile needs at least one of name, title, description, soul, or cwd.", isError: true };
|
|
765
|
+
}
|
|
766
|
+
const forBotId = String(args.for_bot_id ?? "").trim();
|
|
767
|
+
const r = await api("/api/internal/profile-requests", {
|
|
768
|
+
method: "POST",
|
|
769
|
+
body: JSON.stringify({
|
|
770
|
+
fromBotId: BOT_ID,
|
|
771
|
+
fromThreadId: THREAD_ID,
|
|
772
|
+
changes,
|
|
773
|
+
reason: args.reason,
|
|
774
|
+
// JSON.stringify drops the key entirely when no target was named
|
|
775
|
+
forBotId: forBotId || void 0
|
|
776
|
+
})
|
|
777
|
+
});
|
|
778
|
+
return confirmationResult(r, "the profile change", "profile");
|
|
779
|
+
}
|
|
726
780
|
if (name === "session_search") {
|
|
727
781
|
const q = String(args.query ?? "").trim();
|
|
728
782
|
if (!q) return { text: 'session_search needs a query, for example {"query":"site audit broken links"}.', isError: true };
|