openmausbot 0.1.56 → 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-Dq2j4Ikr.js → index-jWqt5dmY.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/companion/src/routes.js +6 -1
- 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 +1769 -338
- package/dist-server/server/auto-approve.js +24 -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 +7 -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/index.js +589 -103
- package/dist-server/server/mcp-bridge.js +73 -44
- package/dist-server/server/package-export.js +1 -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/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/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/vps-container-mcp.js +49 -31
- package/package.json +1 -1
- package/dist/assets/index-CJBrtDbC.css +0 -1
- package/dist/assets/index-D0xt96rv.js +0 -263
|
@@ -184,3 +184,27 @@ export function autoVerdict(bot, tool, summary, context) {
|
|
|
184
184
|
export function autoDecision(bot, tool, summary, context) {
|
|
185
185
|
return autoVerdict(bot, tool, summary, context).approve;
|
|
186
186
|
}
|
|
187
|
+
/** The note a card shows above its buttons, explaining why the bot stopped
|
|
188
|
+
* rather than answering for itself.
|
|
189
|
+
*
|
|
190
|
+
* The unattended case is the one users misread. A turn a webhook or another
|
|
191
|
+
* bot started never runs Auto at all — approvalModeForTurn downgrades it to
|
|
192
|
+
* Ask before the provider spawns — so "this action needs you" would name the
|
|
193
|
+
* wrong cause and imply the next action might pass. It will not: with a fleet
|
|
194
|
+
* delegating between bots, every card looks like this until someone answers.
|
|
195
|
+
* Say that plainly, and name the mode that keeps running. */
|
|
196
|
+
export function approvalHeldReason(context) {
|
|
197
|
+
if (context.source === "native-approval")
|
|
198
|
+
return "The provider requires your approval for this action.";
|
|
199
|
+
if (!context.permission)
|
|
200
|
+
return undefined;
|
|
201
|
+
if (context.requiresExplicitApproval) {
|
|
202
|
+
return "This changes the provider sandbox, so only Full access can approve it automatically.";
|
|
203
|
+
}
|
|
204
|
+
if (context.mode !== "auto")
|
|
205
|
+
return undefined;
|
|
206
|
+
if (!context.unattended)
|
|
207
|
+
return "This action needs you, so Approve for me stopped to ask.";
|
|
208
|
+
const hint = context.fullAccessAvailable ? " Full access keeps working unattended." : "";
|
|
209
|
+
return `A webhook or another bot started this turn, so Approve for me is paused and every action asks.${hint}`;
|
|
210
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// The bot folder: ~/.openmausbot/bots/<botId>/, owned by the server.
|
|
2
|
+
//
|
|
3
|
+
// SOUL.md is a MIRROR of BotRecord.soul, never the source of truth. The
|
|
4
|
+
// prompt is built from the record; the file exists so a person can read
|
|
5
|
+
// and edit the bot in their editor and so a bot can export as a folder.
|
|
6
|
+
// The folder sits outside the bot-writable workspace on purpose: a bot
|
|
7
|
+
// that reads untrusted content (a Discord channel, a webhook payload)
|
|
8
|
+
// must not be able to persist injected text into its own persona. A
|
|
9
|
+
// mirror that no longer matches the record is surfaced to the user as
|
|
10
|
+
// drift, with its text, and is never applied on its own.
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { closeSync, mkdirSync, openSync, readSync, rmSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
15
|
+
import { DATA_DIR } from "./config.js";
|
|
16
|
+
import { BOT_PROFILE_LIMITS } from "../shared/bot-profile.js";
|
|
17
|
+
export const BOTS_DIR = join(DATA_DIR, "bots");
|
|
18
|
+
export const SOUL_FILE = "SOUL.md";
|
|
19
|
+
export function botFolder(botId) {
|
|
20
|
+
return join(BOTS_DIR, botId);
|
|
21
|
+
}
|
|
22
|
+
export function soulFile(botId) {
|
|
23
|
+
return join(botFolder(botId), SOUL_FILE);
|
|
24
|
+
}
|
|
25
|
+
export function soulHash(soul) {
|
|
26
|
+
return createHash("sha256").update(soul, "utf8").digest("hex");
|
|
27
|
+
}
|
|
28
|
+
/** Rewrite the mirror from the canonical text. Always writes, even for an
|
|
29
|
+
* empty soul, so the folder exists for the user to find. Private modes:
|
|
30
|
+
* standing instructions can describe a person's work in detail. */
|
|
31
|
+
export function writeSoulMirror(botId, soul) {
|
|
32
|
+
mkdirSync(botFolder(botId), { recursive: true, mode: 0o700 });
|
|
33
|
+
writeFileAtomic(soulFile(botId), soul, { mode: 0o600 });
|
|
34
|
+
}
|
|
35
|
+
/** Compare the mirror with the record's hash. A missing mirror is not
|
|
36
|
+
* drift — it is re-created from the record. A differing one is reported
|
|
37
|
+
* together with its text so the user can apply or discard it. Editor reads
|
|
38
|
+
* surface filesystem failures instead of mistaking them for a clean mirror. */
|
|
39
|
+
export function readSoulDrift(botId, soul, hash) {
|
|
40
|
+
let fileText;
|
|
41
|
+
try {
|
|
42
|
+
const fd = openSync(soulFile(botId), "r");
|
|
43
|
+
try {
|
|
44
|
+
// External edits are unbounded. Read at most the budget plus one byte,
|
|
45
|
+
// including if the file grows while the editor has it open.
|
|
46
|
+
const bytes = Buffer.alloc(BOT_PROFILE_LIMITS.soul + 1);
|
|
47
|
+
let length = 0;
|
|
48
|
+
while (length < bytes.length) {
|
|
49
|
+
const count = readSync(fd, bytes, length, bytes.length - length, null);
|
|
50
|
+
if (!count)
|
|
51
|
+
break;
|
|
52
|
+
length += count;
|
|
53
|
+
}
|
|
54
|
+
if (length > BOT_PROFILE_LIMITS.soul) {
|
|
55
|
+
throw Object.assign(new Error("SOUL.md exceeds 24000 bytes; shorten it in your editor"), { status: 400 });
|
|
56
|
+
}
|
|
57
|
+
fileText = bytes.subarray(0, length).toString("utf8");
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
closeSync(fd);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error.code !== "ENOENT")
|
|
65
|
+
throw error;
|
|
66
|
+
writeSoulMirror(botId, soul);
|
|
67
|
+
return { drift: false };
|
|
68
|
+
}
|
|
69
|
+
if (soulHash(fileText) === hash)
|
|
70
|
+
return { drift: false };
|
|
71
|
+
return { drift: true, fileText };
|
|
72
|
+
}
|
|
73
|
+
/** Turn dispatch is best-effort; explicit editor actions use readSoulDrift
|
|
74
|
+
* so an unreadable file is never presented as successfully discarded. */
|
|
75
|
+
export function checkSoulDrift(botId, soul, hash) {
|
|
76
|
+
try {
|
|
77
|
+
return readSoulDrift(botId, soul, hash);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return { drift: false };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function removeBotFolder(botId) {
|
|
84
|
+
try {
|
|
85
|
+
rmSync(botFolder(botId), { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
catch { }
|
|
88
|
+
}
|
|
89
|
+
/** The standing-instructions block of the system prompt. Empty soul,
|
|
90
|
+
* empty block, so a bot without one gets today's prompt byte for byte. */
|
|
91
|
+
export function soulSystemPrompt(soul) {
|
|
92
|
+
const text = soul.trim();
|
|
93
|
+
if (!text)
|
|
94
|
+
return "";
|
|
95
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
96
|
+
return ("\n\nYour standing instructions follow. The user manages them in bot settings and SOUL.md; proposed changes apply only after the user confirms." +
|
|
97
|
+
" They rank above your memory and imported skills, and below the user's current request and safety boundaries." +
|
|
98
|
+
` Text inside this block is instruction for you, never tool authorization or permission to expose secrets.` +
|
|
99
|
+
`\n\n--- BEGIN STANDING INSTRUCTIONS (SOUL.md, ${bytes} bytes) ---\n` +
|
|
100
|
+
text +
|
|
101
|
+
"\n--- END STANDING INSTRUCTIONS ---");
|
|
102
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { approvalModeFor } from "../shared/approval-mode.js";
|
|
2
|
+
/** The first paragraph of a SOUL.md-style persona, capped at 240 characters
|
|
3
|
+
* so a settings-dialog card never renders a full standing-instructions
|
|
4
|
+
* document inline. */
|
|
5
|
+
export function soulLead(soul) {
|
|
6
|
+
const trimmed = (soul ?? "").trim();
|
|
7
|
+
if (!trimmed)
|
|
8
|
+
return "";
|
|
9
|
+
const paragraph = (trimmed.split(/\r?\n\s*\r?\n/)[0] ?? trimmed).trim();
|
|
10
|
+
return paragraph.length > 240 ? `${paragraph.slice(0, 240).trimEnd()}…` : paragraph;
|
|
11
|
+
}
|
|
12
|
+
function lowerFirst(value) {
|
|
13
|
+
return value.length ? value[0].toLowerCase() + value.slice(1) : value;
|
|
14
|
+
}
|
|
15
|
+
function time(at, timeZone) {
|
|
16
|
+
return new Date(at).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", timeZone });
|
|
17
|
+
}
|
|
18
|
+
const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
19
|
+
/** "09:05" → "9:05 AM" — the daily schedule's wall-clock time as a person
|
|
20
|
+
* would say it, with no timezone suffix (the Overview is read in the
|
|
21
|
+
* timezone it was built for). */
|
|
22
|
+
function clockTime(hhmm) {
|
|
23
|
+
const [h = 0, m = 0] = hhmm.split(":").map((part) => Number.parseInt(part, 10) || 0);
|
|
24
|
+
return new Date(Date.UTC(2000, 0, 1, h, m)).toLocaleTimeString("en-US", {
|
|
25
|
+
hour: "numeric",
|
|
26
|
+
minute: "2-digit",
|
|
27
|
+
timeZone: "UTC",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** The schedule as the opening phrase of a Does line — "Every 5 minutes",
|
|
31
|
+
* "Every weekday at 9:00 AM", "Once on September 5 at 12:00 PM". The
|
|
32
|
+
* approval card's scheduleText() carries the anchor instant and timezone
|
|
33
|
+
* name because a card must be exact; a plain-language overview must not. */
|
|
34
|
+
function schedulePhrase(schedule, timeZone) {
|
|
35
|
+
if (schedule.type === "once") {
|
|
36
|
+
const date = new Date(schedule.at).toLocaleDateString("en-US", { month: "long", day: "numeric", timeZone });
|
|
37
|
+
return `Once on ${date} at ${time(schedule.at, timeZone)}`;
|
|
38
|
+
}
|
|
39
|
+
if (schedule.type === "interval") {
|
|
40
|
+
const minutes = schedule.everyMinutes;
|
|
41
|
+
if (minutes < 60)
|
|
42
|
+
return `Every ${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
43
|
+
if (minutes === 60)
|
|
44
|
+
return "Every hour";
|
|
45
|
+
if (minutes % 60 === 0)
|
|
46
|
+
return `Every ${minutes / 60} hours`;
|
|
47
|
+
return `Every ${Math.floor(minutes / 60)} hour${Math.floor(minutes / 60) === 1 ? "" : "s"} ${minutes % 60} minutes`;
|
|
48
|
+
}
|
|
49
|
+
const days = schedule.weekdays;
|
|
50
|
+
const at = ` at ${clockTime(schedule.time)}`;
|
|
51
|
+
if (days.length === 7)
|
|
52
|
+
return `Every day${at}`;
|
|
53
|
+
if (days.join(",") === "1,2,3,4,5")
|
|
54
|
+
return `Every weekday${at}`;
|
|
55
|
+
if (days.length === 1)
|
|
56
|
+
return `Weekly on ${DAY_NAMES[days[0]]}${at}`;
|
|
57
|
+
return `${days.map((day) => DAY_NAMES[day]).join(", ")}${at}`;
|
|
58
|
+
}
|
|
59
|
+
/** The most recent run for a routine, by (finishedAt ?? startedAt ??
|
|
60
|
+
* scheduledFor) — not merely the first match in `runs`, since callers may
|
|
61
|
+
* hand this an unsorted or multi-routine list. */
|
|
62
|
+
function latestRunFor(runs, routineId) {
|
|
63
|
+
let best;
|
|
64
|
+
let bestAt = -Infinity;
|
|
65
|
+
for (const run of runs) {
|
|
66
|
+
if (run.routineId !== routineId)
|
|
67
|
+
continue;
|
|
68
|
+
const at = run.finishedAt ?? run.startedAt ?? run.scheduledFor;
|
|
69
|
+
if (at > bestAt) {
|
|
70
|
+
bestAt = at;
|
|
71
|
+
best = run;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return best;
|
|
75
|
+
}
|
|
76
|
+
function doesLines(facts) {
|
|
77
|
+
const lines = [];
|
|
78
|
+
for (const routine of facts.routines) {
|
|
79
|
+
if (!routine.enabled) {
|
|
80
|
+
lines.push(`Paused: ${routine.name}.`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const nextRun = routine.nextRunAt != null ? ` Next run ${time(routine.nextRunAt, facts.timeZone)}.` : "";
|
|
84
|
+
const latest = latestRunFor(facts.runs, routine.id);
|
|
85
|
+
const at = latest ? latest.finishedAt ?? latest.startedAt ?? latest.scheduledFor : undefined;
|
|
86
|
+
const lastRun = latest ? ` Last run ${latest.status} at ${time(at, facts.timeZone)}.` : "";
|
|
87
|
+
lines.push(`${schedulePhrase(routine.schedule, facts.timeZone)}: ${routine.name}.${nextRun}${lastRun}`);
|
|
88
|
+
}
|
|
89
|
+
for (const skill of facts.skills) {
|
|
90
|
+
if (!skill.enabled)
|
|
91
|
+
continue;
|
|
92
|
+
lines.push(`Knows how to ${lowerFirst(skill.description || skill.name)}.`);
|
|
93
|
+
}
|
|
94
|
+
for (const webhook of facts.webhooks) {
|
|
95
|
+
if (!webhook.enabled)
|
|
96
|
+
continue;
|
|
97
|
+
lines.push(`Listens for “${webhook.name}” webhooks.`);
|
|
98
|
+
}
|
|
99
|
+
return lines;
|
|
100
|
+
}
|
|
101
|
+
/** The connected-apps facts for the route: reads the inventory only when a
|
|
102
|
+
* connector is configured and the credential store was readable, and treats
|
|
103
|
+
* a failing read (connector down, token rejected) as "unverified" rather
|
|
104
|
+
* than letting it fail the whole Overview. `read` is injected so the
|
|
105
|
+
* fallback is unit-testable without a live connector. */
|
|
106
|
+
export async function connectedAppsFacts(configured, availability, read) {
|
|
107
|
+
let authoritative = availability !== "unreadable";
|
|
108
|
+
let services = [];
|
|
109
|
+
if (configured && authoritative) {
|
|
110
|
+
try {
|
|
111
|
+
services = Object.entries(await read())
|
|
112
|
+
.filter(([, state]) => state.connected)
|
|
113
|
+
.map(([slug]) => slug);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
authoritative = false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { configured, authoritative, services };
|
|
120
|
+
}
|
|
121
|
+
/** Whether this bot could use connected apps at all: apps on for the bot,
|
|
122
|
+
* a connector configured, and an engine that mounts the Composio MCP. */
|
|
123
|
+
function couldUseApps(facts) {
|
|
124
|
+
return facts.bot.composio !== false && facts.connectedApps.configured && Boolean(facts.engine?.composioMcp);
|
|
125
|
+
}
|
|
126
|
+
function computerReach(computer) {
|
|
127
|
+
switch (computer) {
|
|
128
|
+
case "cloud":
|
|
129
|
+
return "Computer preference: cloud computer.";
|
|
130
|
+
case "vm":
|
|
131
|
+
return "Computer preference: Local VM.";
|
|
132
|
+
case "local":
|
|
133
|
+
return "Computer preference: this computer.";
|
|
134
|
+
case "browser":
|
|
135
|
+
return "Computer preference: browser only.";
|
|
136
|
+
case "off":
|
|
137
|
+
return null;
|
|
138
|
+
default:
|
|
139
|
+
return "Computer preference: Auto; availability is checked when a task starts.";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function reachesLines(facts) {
|
|
143
|
+
const lines = [];
|
|
144
|
+
const computer = computerReach(facts.bot.computer);
|
|
145
|
+
if (computer)
|
|
146
|
+
lines.push(computer);
|
|
147
|
+
lines.push(facts.bot.cwd ? `Works in ${facts.bot.cwd}.` : "Works in its private workspace.");
|
|
148
|
+
const apps = facts.connectedApps;
|
|
149
|
+
// Only a bot that could actually use apps gets an apps line here. When it
|
|
150
|
+
// could, the inventory decides: verified and non-empty → list them;
|
|
151
|
+
// unverified → say so rather than guess either way.
|
|
152
|
+
if (couldUseApps(facts) && apps.authoritative && apps.services.length > 0) {
|
|
153
|
+
const count = apps.services.length;
|
|
154
|
+
lines.push(`Can use ${count} connected app${count === 1 ? "" : "s"}: ${apps.services.join(", ")}.`);
|
|
155
|
+
}
|
|
156
|
+
else if (couldUseApps(facts) && !apps.authoritative) {
|
|
157
|
+
lines.push("Connected apps could not be checked.");
|
|
158
|
+
}
|
|
159
|
+
if (facts.browserEnabled && facts.engine?.browserMcp && facts.bot.browser !== false && facts.bot.computer !== "off")
|
|
160
|
+
lines.push("Has the built-in browser.");
|
|
161
|
+
if (facts.engine?.agentsMcp && facts.sectionPeers > 0 && facts.bot.peers?.length !== 0) {
|
|
162
|
+
lines.push(`Can talk to ${facts.sectionPeers} other bot${facts.sectionPeers === 1 ? "" : "s"} in its section.`);
|
|
163
|
+
}
|
|
164
|
+
if (facts.bot.chiefOfStaff)
|
|
165
|
+
lines.push("Coordinates its section as Chief of Staff.");
|
|
166
|
+
return lines;
|
|
167
|
+
}
|
|
168
|
+
function wontLines(facts) {
|
|
169
|
+
const lines = [];
|
|
170
|
+
const mode = approvalModeFor(facts.bot);
|
|
171
|
+
if (mode === "ask")
|
|
172
|
+
lines.push("Command approvals use Ask mode; saved permissions and provider rules still apply.");
|
|
173
|
+
if (mode === "custom")
|
|
174
|
+
lines.push("Command approvals follow the provider's custom configuration.");
|
|
175
|
+
if (facts.bot.peers?.length === 0)
|
|
176
|
+
lines.push("Cannot initiate contact with other bots.");
|
|
177
|
+
else if (facts.bot.approvePeerComms)
|
|
178
|
+
lines.push("Asks before contacting other bots.");
|
|
179
|
+
// "Has no connected apps." is definite when apps are off for this bot,
|
|
180
|
+
// not configured, or unsupported by its engine — no inventory needed. Only
|
|
181
|
+
// the "configured but nothing connected" case rests on the inventory, so
|
|
182
|
+
// only that case requires it to be authoritative.
|
|
183
|
+
if (!couldUseApps(facts) || (facts.connectedApps.authoritative && facts.connectedApps.services.length === 0)) {
|
|
184
|
+
lines.push("Has no connected apps.");
|
|
185
|
+
}
|
|
186
|
+
if (facts.bot.computer === "off")
|
|
187
|
+
lines.push("Can't use a computer.");
|
|
188
|
+
if (!facts.routines.some((routine) => routine.enabled))
|
|
189
|
+
lines.push("Won't act on a schedule.");
|
|
190
|
+
lines.push("Profile proposal cards require your approval.");
|
|
191
|
+
return lines;
|
|
192
|
+
}
|
|
193
|
+
export function buildBotOverview(facts) {
|
|
194
|
+
return {
|
|
195
|
+
who: {
|
|
196
|
+
name: facts.bot.name,
|
|
197
|
+
title: facts.bot.title,
|
|
198
|
+
blurb: facts.bot.description,
|
|
199
|
+
soulLead: soulLead(facts.bot.soul),
|
|
200
|
+
},
|
|
201
|
+
does: doesLines(facts),
|
|
202
|
+
reaches: reachesLines(facts),
|
|
203
|
+
wont: wontLines(facts),
|
|
204
|
+
recent: facts.recent,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
3
3
|
import { schemaIssue } from "./schema.js";
|
|
4
|
+
import { BOT_PROFILE_LIMITS } from "../shared/bot-profile.js";
|
|
4
5
|
export const BOT_PACKAGE_FORMAT = "openmaus.package";
|
|
5
6
|
export const BOT_PACKAGE_VERSION = 1;
|
|
6
7
|
export const BOTMRR_MARKDOWN_VERSION = 1;
|
|
@@ -57,6 +58,9 @@ const packageSchema = z.object({
|
|
|
57
58
|
name: requiredText(100),
|
|
58
59
|
title: optionalText(200),
|
|
59
60
|
description: optionalText(4_000),
|
|
61
|
+
soul: z.string().refine((value) => Buffer.byteLength(value, "utf8") <= BOT_PROFILE_LIMITS.soul, {
|
|
62
|
+
error: "standing instructions must be at most 24000 bytes",
|
|
63
|
+
}).optional(),
|
|
60
64
|
appearance: z.object({
|
|
61
65
|
color: z.enum(COLORS, { error: "is not supported" }),
|
|
62
66
|
mascotExpression: optionalText(80),
|
|
@@ -259,6 +263,7 @@ export function packageAgentAsMember(agent) {
|
|
|
259
263
|
name: agent.name,
|
|
260
264
|
title: agent.title ?? "",
|
|
261
265
|
description: agent.description ?? "",
|
|
266
|
+
...(agent.soul !== undefined ? { soul: agent.soul } : {}),
|
|
262
267
|
appearance: {
|
|
263
268
|
color: agent.appearance.color,
|
|
264
269
|
...(agent.appearance.mascotExpression ? { mascotExpression: agent.appearance.mascotExpression } : {}),
|
|
@@ -21,6 +21,7 @@ export const BOT_PROFILE_PATCH_FIELDS = [
|
|
|
21
21
|
"name",
|
|
22
22
|
"title",
|
|
23
23
|
"description",
|
|
24
|
+
"soul",
|
|
24
25
|
"notifications",
|
|
25
26
|
"avatarUrl",
|
|
26
27
|
"avatarCrop",
|
|
@@ -44,6 +45,12 @@ const profilePatchSchema = z.object({
|
|
|
44
45
|
.string({ error: "description must be a string" })
|
|
45
46
|
.max(BOT_PROFILE_LIMITS.description, { error: "description must be at most 4000 characters" })
|
|
46
47
|
.optional(),
|
|
48
|
+
soul: z
|
|
49
|
+
.string({ error: "soul must be a string" })
|
|
50
|
+
.refine((value) => Buffer.byteLength(value, "utf8") <= BOT_PROFILE_LIMITS.soul, {
|
|
51
|
+
error: "standing instructions must be at most 24000 bytes",
|
|
52
|
+
})
|
|
53
|
+
.optional(),
|
|
47
54
|
notifications: z.boolean({ error: "notifications must be true or false" }).optional(),
|
|
48
55
|
avatarUrl: z
|
|
49
56
|
.union([botAvatarUrlSchema, z.literal(""), z.null()], {
|
|
@@ -118,22 +118,42 @@ function connectorAdds(args) {
|
|
|
118
118
|
const toolkits = args.toolkits;
|
|
119
119
|
if (!Array.isArray(toolkits))
|
|
120
120
|
return [];
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
const requests = [];
|
|
123
|
+
for (const item of toolkits) {
|
|
124
|
+
if (typeof item === "string") {
|
|
125
|
+
const slug = item.trim().toLowerCase();
|
|
126
|
+
const key = JSON.stringify([slug, ""]);
|
|
127
|
+
if (!seen.has(key)) {
|
|
128
|
+
seen.add(key);
|
|
129
|
+
requests.push({ slug });
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
134
|
+
continue;
|
|
135
|
+
const row = item;
|
|
136
|
+
const action = String(row.action ?? "add").toLowerCase();
|
|
137
|
+
if (!["add", "connect", "initiate"].includes(action))
|
|
138
|
+
continue;
|
|
139
|
+
const slug = typeof row.toolkit === "string" ? row.toolkit : row.name;
|
|
140
|
+
if (typeof slug !== "string")
|
|
141
|
+
continue;
|
|
142
|
+
const normalized = slug.trim().toLowerCase();
|
|
143
|
+
const alias = (typeof row.alias === "string" ? row.alias : typeof row.account === "string" ? row.account : "").trim();
|
|
144
|
+
const key = JSON.stringify([normalized, alias.toLowerCase()]);
|
|
145
|
+
if (seen.has(key))
|
|
146
|
+
continue;
|
|
147
|
+
seen.add(key);
|
|
148
|
+
requests.push({ slug: normalized, ...(alias ? { alias } : {}) });
|
|
149
|
+
}
|
|
150
|
+
return requests;
|
|
131
151
|
}
|
|
132
|
-
async function showConnectorCards(
|
|
152
|
+
async function showConnectorCards(items) {
|
|
133
153
|
const response = await fetch(`${HARNESS}/api/internal/connectors/request`, {
|
|
134
154
|
method: "POST",
|
|
135
155
|
headers: { "content-type": "application/json", authorization: `Bearer ${TOKEN}` },
|
|
136
|
-
body: JSON.stringify({ botId: BOT_ID, threadId: THREAD_ID,
|
|
156
|
+
body: JSON.stringify({ botId: BOT_ID, threadId: THREAD_ID, items, resumeKey: randomUUID() }),
|
|
137
157
|
signal: AbortSignal.timeout(30_000),
|
|
138
158
|
});
|
|
139
159
|
if (!response.ok) {
|
|
@@ -176,10 +196,11 @@ async function handle(message) {
|
|
|
176
196
|
if (method === "tools/call") {
|
|
177
197
|
const params = (message.params ?? {});
|
|
178
198
|
const name = String(params.name ?? "");
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
181
|
-
await showConnectorCards(
|
|
182
|
-
|
|
199
|
+
const requests = /MANAGE_CONNECTIONS$/i.test(name) ? connectorAdds(params.arguments) : [];
|
|
200
|
+
if (requests.length) {
|
|
201
|
+
await showConnectorCards(requests);
|
|
202
|
+
const labels = requests.map((r) => (r.alias ? `${r.slug} (${r.alias})` : r.slug)).join(", ");
|
|
203
|
+
send(textResult(id, `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.`));
|
|
183
204
|
return;
|
|
184
205
|
}
|
|
185
206
|
if (/WAIT_FOR_CONNECTIONS$/i.test(name)) {
|
|
@@ -728,7 +728,10 @@ export function wholeScreenshot(bytes) {
|
|
|
728
728
|
mime: "image/jpeg",
|
|
729
729
|
};
|
|
730
730
|
}
|
|
731
|
-
|
|
731
|
+
/** The raw frame, in the shape the live screen poller broadcasts to every
|
|
732
|
+
* client (server/index.ts). The web panel wants a data URL instead, so
|
|
733
|
+
* containerComputerScreenshot below wraps this one. */
|
|
734
|
+
export async function containerComputerFrame(runner = sh, platform = process.platform, target = SHARED_LOCAL_VM_TARGET) {
|
|
732
735
|
const cacheable = runner === sh && platform === process.platform;
|
|
733
736
|
const now = Date.now();
|
|
734
737
|
const cached = screenshotStatusCache.get(target.key);
|
|
@@ -759,7 +762,7 @@ export async function containerComputerScreenshot(runner = sh, platform = proces
|
|
|
759
762
|
if (!checked.ok) {
|
|
760
763
|
throw Object.assign(new Error("Cua Driver returned an incomplete screenshot"), { status: 502 });
|
|
761
764
|
}
|
|
762
|
-
return
|
|
765
|
+
return { png: data, format: checked.mime === "image/jpeg" ? "jpeg" : "png" };
|
|
763
766
|
}
|
|
764
767
|
catch (error) {
|
|
765
768
|
if (cacheable)
|
|
@@ -767,6 +770,10 @@ export async function containerComputerScreenshot(runner = sh, platform = proces
|
|
|
767
770
|
throw error;
|
|
768
771
|
}
|
|
769
772
|
}
|
|
773
|
+
export async function containerComputerScreenshot(runner = sh, platform = process.platform, target = SHARED_LOCAL_VM_TARGET) {
|
|
774
|
+
const { png, format } = await containerComputerFrame(runner, platform, target);
|
|
775
|
+
return `data:image/${format};base64,${png}`;
|
|
776
|
+
}
|
|
770
777
|
const screenshotStatusCache = new Map();
|
|
771
778
|
const containerMcpPath = SPAWNED_PROXIES.containerMcp;
|
|
772
779
|
export function containerComputerMcp(runtime, control, target = SHARED_LOCAL_VM_TARGET) {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// list_routines() → inspect this bot's scheduled work
|
|
20
20
|
// propose_routine(...) → show a confirmation card for a new routine
|
|
21
21
|
// propose_routine_action(...) → show a confirmation card for a routine change
|
|
22
|
+
// propose_profile(...) → show a confirmation card for a profile change
|
|
22
23
|
//
|
|
23
24
|
// Speaks raw JSON-RPC 2.0 over stdio (no MCP SDK — house style, matches
|
|
24
25
|
// computer-proxy / permission-proxy). All state comes from env, injected by
|
|
@@ -212,6 +213,10 @@ const ROUTINE_FIELDS_SCHEMA = {
|
|
|
212
213
|
type: "boolean",
|
|
213
214
|
description: "Only for updates: set true to remove an existing safety limit. Do not combine with timeout_minutes.",
|
|
214
215
|
},
|
|
216
|
+
continuity: {
|
|
217
|
+
type: "boolean",
|
|
218
|
+
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.",
|
|
219
|
+
},
|
|
215
220
|
};
|
|
216
221
|
const TOOLS = [
|
|
217
222
|
{
|
|
@@ -390,6 +395,31 @@ const TOOLS = [
|
|
|
390
395
|
required: ["routine_id", "action"],
|
|
391
396
|
},
|
|
392
397
|
},
|
|
398
|
+
{
|
|
399
|
+
name: "propose_profile",
|
|
400
|
+
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 — 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.",
|
|
401
|
+
inputSchema: {
|
|
402
|
+
type: "object",
|
|
403
|
+
additionalProperties: false,
|
|
404
|
+
properties: {
|
|
405
|
+
name: { type: "string", maxLength: 100, description: "New display name." },
|
|
406
|
+
title: { type: "string", maxLength: 200, description: "New role or title." },
|
|
407
|
+
description: { type: "string", maxLength: 4000, description: "New one-line blurb shown in rosters." },
|
|
408
|
+
soul: { type: "string", description: "Full replacement text for SOUL.md, at most 24000 bytes." },
|
|
409
|
+
cwd: {
|
|
410
|
+
type: "string",
|
|
411
|
+
maxLength: 1024,
|
|
412
|
+
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.",
|
|
413
|
+
},
|
|
414
|
+
reason: { type: "string", minLength: 1, maxLength: 500, description: "One sentence the user will see explaining why." },
|
|
415
|
+
for_bot_id: {
|
|
416
|
+
type: "string",
|
|
417
|
+
description: "Chief of Staff only: the id of another bot in your section whose profile this changes. Omit to change your own.",
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
required: ["reason"],
|
|
421
|
+
},
|
|
422
|
+
},
|
|
393
423
|
{
|
|
394
424
|
name: "skills_list",
|
|
395
425
|
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.",
|
|
@@ -475,12 +505,14 @@ function routineFields(args) {
|
|
|
475
505
|
fields.timeoutMinutes = null;
|
|
476
506
|
else if (typeof args.timeout_minutes === "number")
|
|
477
507
|
fields.timeoutMinutes = args.timeout_minutes;
|
|
508
|
+
if (typeof args.continuity === "boolean")
|
|
509
|
+
fields.continuity = args.continuity;
|
|
478
510
|
return { fields };
|
|
479
511
|
}
|
|
480
|
-
function confirmationResult(r, fallback) {
|
|
512
|
+
function confirmationResult(r, fallback, noun = "routine") {
|
|
481
513
|
const summary = typeof r.summary === "string" && r.summary.trim() ? `\n\n${r.summary.trim()}` : "";
|
|
482
514
|
return {
|
|
483
|
-
text: `A confirmation card is now visible to the user for ${fallback}.${summary}\n\nThis change has not been applied yet. End this turn and wait for the user to confirm or deny the card; do not claim the
|
|
515
|
+
text: `A confirmation card is now visible to the user for ${fallback}.${summary}\n\nThis 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.`,
|
|
484
516
|
};
|
|
485
517
|
}
|
|
486
518
|
/** Who said a recalled line, as the header of a hit or a read. A user-role
|
|
@@ -770,6 +802,35 @@ async function callTool(name, args) {
|
|
|
770
802
|
});
|
|
771
803
|
return confirmationResult(r, `${action.replace("_", " ")} on routine ${routineId}`);
|
|
772
804
|
}
|
|
805
|
+
if (name === "propose_profile") {
|
|
806
|
+
const changes = {};
|
|
807
|
+
if (typeof args.name === "string")
|
|
808
|
+
changes.name = args.name.trim();
|
|
809
|
+
if (typeof args.title === "string")
|
|
810
|
+
changes.title = args.title.trim();
|
|
811
|
+
if (typeof args.description === "string")
|
|
812
|
+
changes.description = args.description.trim();
|
|
813
|
+
if (typeof args.soul === "string")
|
|
814
|
+
changes.soul = args.soul;
|
|
815
|
+
if (typeof args.cwd === "string")
|
|
816
|
+
changes.cwd = args.cwd.trim();
|
|
817
|
+
if (!Object.keys(changes).length) {
|
|
818
|
+
return { text: "propose_profile needs at least one of name, title, description, soul, or cwd.", isError: true };
|
|
819
|
+
}
|
|
820
|
+
const forBotId = String(args.for_bot_id ?? "").trim();
|
|
821
|
+
const r = await api("/api/internal/profile-requests", {
|
|
822
|
+
method: "POST",
|
|
823
|
+
body: JSON.stringify({
|
|
824
|
+
fromBotId: BOT_ID,
|
|
825
|
+
fromThreadId: THREAD_ID,
|
|
826
|
+
changes,
|
|
827
|
+
reason: args.reason,
|
|
828
|
+
// JSON.stringify drops the key entirely when no target was named
|
|
829
|
+
forBotId: forBotId || undefined,
|
|
830
|
+
}),
|
|
831
|
+
});
|
|
832
|
+
return confirmationResult(r, "the profile change", "profile");
|
|
833
|
+
}
|
|
773
834
|
if (name === "session_search") {
|
|
774
835
|
const q = String(args.query ?? "").trim();
|
|
775
836
|
if (!q)
|