openmausbot 0.1.75 → 0.1.77
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-BTlyRwLl.js → index-DeK-z9ld.js} +1 -1
- package/dist/assets/index-XdHJ-9CF.css +1 -0
- package/dist/assets/index-lhqE1umn.js +305 -0
- package/dist/index.html +2 -2
- package/dist-server/companion/src/routes.js +6 -2
- package/dist-server/container-mcp.js +5 -1
- package/dist-server/drivers/agents-proxy.js +184 -11
- package/dist-server/enterprise/server/index.js +387 -29
- package/dist-server/index.js +3786 -1135
- package/dist-server/mcp-gate.js +28 -18
- package/dist-server/openmausbot.js +927 -642
- package/dist-server/pair-cli.js +927 -642
- package/dist-server/server/agent-tool-policy.js +2 -0
- package/dist-server/server/atomic.js +37 -1
- package/dist-server/server/bot-overview.js +5 -1
- package/dist-server/server/browser-live.js +3 -0
- package/dist-server/server/chief-of-staff.js +17 -12
- package/dist-server/server/config.js +16 -0
- package/dist-server/server/delegations.js +177 -63
- package/dist-server/server/drivers/acp/core.js +3 -1
- package/dist-server/server/drivers/acp/hermes.js +21 -3
- package/dist-server/server/drivers/agents-proxy.js +169 -10
- package/dist-server/server/drivers/boxagent.js +1 -1
- package/dist-server/server/drivers/claude.js +34 -16
- package/dist-server/server/drivers/codex.js +3 -1
- package/dist-server/server/drivers/openai-chat.js +90 -67
- package/dist-server/server/drivers/openai-compat.js +9 -1
- package/dist-server/server/drivers/pi.js +22 -3
- package/dist-server/server/enterprise.js +38 -0
- package/dist-server/server/env-path.js +18 -17
- package/dist-server/server/environment.js +3 -0
- package/dist-server/server/fleet-agent.js +57 -13
- package/dist-server/server/fleet-cli.js +248 -31
- package/dist-server/server/fleet.js +88 -14
- package/dist-server/server/hosted-contract.js +16 -0
- package/dist-server/server/index.js +1777 -406
- package/dist-server/server/message-db.js +21 -0
- package/dist-server/server/message-file.js +4 -0
- package/dist-server/server/peer-roster.js +48 -9
- package/dist-server/server/procs.js +19 -3
- package/dist-server/server/provider-key-check.js +18 -1
- package/dist-server/server/request-auth.js +14 -5
- package/dist-server/server/room-handoffs.js +237 -0
- package/dist-server/server/section-context.js +64 -14
- package/dist-server/server/sessions.js +115 -8
- package/dist-server/server/shared-computer-control.js +24 -0
- package/dist-server/server/shared-computers.js +131 -0
- package/dist-server/server/steer-queue.js +2 -0
- package/dist-server/server/store.js +267 -15
- package/dist-server/server/surface.js +16 -5
- package/dist-server/server/team-backup.js +17 -1
- package/dist-server/server/team-computers.js +108 -0
- package/dist-server/server/team-setup-requests.js +252 -0
- package/dist-server/server/tool-summary.js +53 -1
- package/dist-server/server/turn-dispatch-guard.js +31 -3
- package/dist-server/server/workspace-backup.js +1 -0
- package/dist-server/shared/approval-mode.js +11 -1
- package/dist-server/shared/team-backup.js +1 -0
- package/dist-server/shared/team-computer.js +1 -0
- package/dist-server/shared/team-setup.js +1 -0
- package/dist-server/vps-container-mcp.js +5 -1
- package/enterprise/README.md +21 -3
- package/enterprise/server/index.js +387 -29
- package/package.json +1 -1
- package/dist/assets/index-CeMbvsgx.js +0 -305
- package/dist/assets/index-DH7Zax6L.css +0 -1
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { newId } from "./contracts.js";
|
|
4
|
+
import { fitsOnOneLine, parseBotProfilePatch } from "./bot-profile.js";
|
|
5
|
+
import { profileSnapshot } from "./profile-revision.js";
|
|
6
|
+
import { redactSecretsInText } from "./redact.js";
|
|
7
|
+
const section = (value) => value?.trim() || "";
|
|
8
|
+
const teamName = z.string().trim().min(1).max(60).refine(fitsOnOneLine).refine((value) => redactSecretsInText(value) === value, "Team names cannot contain credentials");
|
|
9
|
+
const fieldsSchema = z.object({
|
|
10
|
+
name: z.string().optional(), title: z.string().optional(), description: z.string().optional(), soul: z.string().optional(),
|
|
11
|
+
section: z.string().trim().max(60).refine(fitsOnOneLine).refine((value) => redactSecretsInText(value) === value, "Team names cannot contain credentials").optional(),
|
|
12
|
+
modelSelection: z.object({ instanceId: z.string().trim().min(1), model: z.string().trim().min(1), effort: z.string().optional() }).strict().optional(),
|
|
13
|
+
}).strict();
|
|
14
|
+
const planSchema = z.object({
|
|
15
|
+
reason: z.string().trim().min(1).max(500),
|
|
16
|
+
newTeams: z.array(teamName).max(8).default([]),
|
|
17
|
+
operations: z.array(z.discriminatedUnion("action", [
|
|
18
|
+
z.object({ action: z.literal("create"), key: z.string().trim().min(1).max(80), fields: fieldsSchema }).strict(),
|
|
19
|
+
z.object({ action: z.literal("update"), botId: z.string().min(1).max(128), fields: fieldsSchema }).strict(),
|
|
20
|
+
])).min(1).max(24),
|
|
21
|
+
}).strict();
|
|
22
|
+
export class TeamSetupError extends Error {
|
|
23
|
+
status;
|
|
24
|
+
constructor(message, status = 400) { super(message); this.status = status; }
|
|
25
|
+
}
|
|
26
|
+
/** Settings relevant to approval, including the previous durable receipt. */
|
|
27
|
+
export function teamSetupRevision(bot) {
|
|
28
|
+
return createHash("sha256").update(JSON.stringify({
|
|
29
|
+
...profileSnapshot(bot), section: section(bot.section), modelSelection: bot.modelSelection,
|
|
30
|
+
hidden: Boolean(bot.hidden), chiefOfStaff: Boolean(bot.chiefOfStaff), peers: bot.peers,
|
|
31
|
+
managedSections: bot.managedSections,
|
|
32
|
+
approvalMode: bot.approvalMode, autoApprove: bot.autoApprove, approvalGrant: bot.approvalGrant,
|
|
33
|
+
receipt: bot.lastTeamSetupReceipt?.requestId,
|
|
34
|
+
})).digest("hex");
|
|
35
|
+
}
|
|
36
|
+
export class TeamSetupRequestService {
|
|
37
|
+
resolving = new Set();
|
|
38
|
+
options;
|
|
39
|
+
constructor(options) { this.options = options; }
|
|
40
|
+
chief(botId) {
|
|
41
|
+
const chief = this.options.store.bot(botId);
|
|
42
|
+
if (!chief || chief.hidden || !chief.chiefOfStaff)
|
|
43
|
+
throw new TeamSetupError("Only an active Chief of Staff can propose team setup", 403);
|
|
44
|
+
return chief;
|
|
45
|
+
}
|
|
46
|
+
fields(input, current) {
|
|
47
|
+
const { section: targetSection, modelSelection, ...profile } = input;
|
|
48
|
+
const safe = Object.fromEntries(Object.entries(profile).map(([key, value]) => [key, redactSecretsInText(value)]));
|
|
49
|
+
const parsed = parseBotProfilePatch(safe, true);
|
|
50
|
+
if (!parsed.ok)
|
|
51
|
+
throw new TeamSetupError(parsed.error);
|
|
52
|
+
const result = { ...parsed.patch };
|
|
53
|
+
if (targetSection !== undefined)
|
|
54
|
+
result.section = targetSection;
|
|
55
|
+
if (modelSelection) {
|
|
56
|
+
const selection = modelSelection;
|
|
57
|
+
const error = this.options.validateModel(selection, current);
|
|
58
|
+
if (error)
|
|
59
|
+
throw new TeamSetupError(error);
|
|
60
|
+
result.modelSelection = selection;
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
validate(request, confirming) {
|
|
65
|
+
const chief = this.chief(request.botId);
|
|
66
|
+
if (!this.options.ownsThread(request.botId, request.threadId))
|
|
67
|
+
throw new TeamSetupError("The requesting conversation no longer exists", 409);
|
|
68
|
+
if (confirming && !this.options.store.messagesFor(request.threadId).some((message) => message.card?.requestId === request.requestId && !message.card.answered && !message.card.dismissed))
|
|
69
|
+
throw new TeamSetupError("This setup card is no longer pending", 409);
|
|
70
|
+
if (confirming && teamSetupRevision(chief) !== request.requesterRevision)
|
|
71
|
+
throw new TeamSetupError("The Chief's settings changed. This setup was cancelled; review a new proposal.", 409);
|
|
72
|
+
const existingTeams = new Set(this.options.teams().map(section));
|
|
73
|
+
if (new Set([...(chief.managedSections ?? []), ...request.newTeams]).size > 100)
|
|
74
|
+
throw new TeamSetupError("A Chief may coordinate at most 100 additional teams", 409);
|
|
75
|
+
for (const name of request.newTeams)
|
|
76
|
+
if (existingTeams.has(name))
|
|
77
|
+
throw new TeamSetupError(`Team ${JSON.stringify(name)} now exists. Review a new proposal.`, 409);
|
|
78
|
+
const allowed = (name) => request.newTeams.includes(section(name)) || this.options.canAccessTeam(chief, name);
|
|
79
|
+
const targets = new Set();
|
|
80
|
+
const projected = this.options.store.bots.map((bot) => ({ id: bot.id, name: bot.name, section: section(bot.section), chiefOfStaff: bot.chiefOfStaff, hidden: bot.hidden }));
|
|
81
|
+
for (const operation of request.operations) {
|
|
82
|
+
if (targets.has(operation.botId))
|
|
83
|
+
throw new TeamSetupError("A bot appears twice in the prepared setup");
|
|
84
|
+
targets.add(operation.botId);
|
|
85
|
+
const target = this.options.store.bot(operation.botId);
|
|
86
|
+
if (operation.action === "create") {
|
|
87
|
+
if (target)
|
|
88
|
+
throw new TeamSetupError("A proposed bot already exists. Review a new proposal.", 409);
|
|
89
|
+
if (!operation.fields.name?.trim() || !operation.fields.title?.trim() || !operation.fields.soul?.trim() || !operation.fields.modelSelection)
|
|
90
|
+
throw new TeamSetupError("Each new bot needs a name, title, soul instructions, and exact model selection");
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
if (!target || target.hidden || !this.options.canAccessTeam(chief, target.section) || (target.id !== chief.id && Array.isArray(chief.peers) && !chief.peers.includes(target.id)))
|
|
94
|
+
throw new TeamSetupError("A target bot is outside this Chief's authorized team and peer scope", 403);
|
|
95
|
+
if (teamSetupRevision(target) !== operation.expectedRevision)
|
|
96
|
+
throw new TeamSetupError(`@${target.name} changed. This setup was cancelled; review a new proposal.`, 409);
|
|
97
|
+
if (this.options.targetBusy(target.id) && (confirming || target.id !== chief.id))
|
|
98
|
+
throw new TeamSetupError(`Stop @${target.name}'s work before changing its setup`, 409);
|
|
99
|
+
}
|
|
100
|
+
const fields = this.fields(operation.fields, target ?? undefined);
|
|
101
|
+
const destination = fields.section ?? target?.section ?? chief.section;
|
|
102
|
+
if (!allowed(destination))
|
|
103
|
+
throw new TeamSetupError("The destination team is outside this Chief's authorized scope", 403);
|
|
104
|
+
if (!request.newTeams.includes(section(destination)) && !existingTeams.has(section(destination)))
|
|
105
|
+
throw new TeamSetupError("The destination team no longer exists", 409);
|
|
106
|
+
const next = { id: operation.botId, name: fields.name ?? target.name, section: section(destination), chiefOfStaff: target?.chiefOfStaff, hidden: false };
|
|
107
|
+
const at = projected.findIndex((bot) => bot.id === operation.botId);
|
|
108
|
+
if (at < 0)
|
|
109
|
+
projected.push(next);
|
|
110
|
+
else
|
|
111
|
+
projected[at] = next;
|
|
112
|
+
}
|
|
113
|
+
if (this.options.store.bots.length + request.operations.filter((op) => op.action === "create").length > this.options.maxBots)
|
|
114
|
+
throw new TeamSetupError("This setup exceeds the workspace bot limit", 409);
|
|
115
|
+
for (const name of request.newTeams)
|
|
116
|
+
if (!request.operations.some((op) => section(op.fields.section) === name))
|
|
117
|
+
throw new TeamSetupError(`New team ${JSON.stringify(name)} needs a specialist in this setup`);
|
|
118
|
+
for (const operation of request.operations) {
|
|
119
|
+
const candidate = projected.find((bot) => bot.id === operation.botId);
|
|
120
|
+
if (projected.some((bot) => bot.id !== candidate.id && !bot.hidden && bot.section === candidate.section && bot.name.trim().toLowerCase() === candidate.name.trim().toLowerCase()))
|
|
121
|
+
throw new TeamSetupError(`@${candidate.name} already exists in that team`, 409);
|
|
122
|
+
if (candidate.chiefOfStaff && projected.some((bot) => bot.id !== candidate.id && bot.chiefOfStaff && bot.section === candidate.section))
|
|
123
|
+
throw new TeamSetupError("That move would put two Chiefs in one team", 409);
|
|
124
|
+
}
|
|
125
|
+
if (request.deletion) {
|
|
126
|
+
const target = this.options.store.bot(request.deletion.botId);
|
|
127
|
+
if (!target || target.hidden || target.id === chief.id || !this.options.canAccessTeam(chief, target.section) || (Array.isArray(chief.peers) && !chief.peers.includes(target.id)))
|
|
128
|
+
throw new TeamSetupError("This deletion target is no longer an authorized teammate", 403);
|
|
129
|
+
if (teamSetupRevision(target) !== request.deletion.expectedRevision || this.options.targetBusy(target.id))
|
|
130
|
+
throw new TeamSetupError("The deletion target changed or is working. Review a new deletion proposal.", 409);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
propose(args) {
|
|
134
|
+
const parsed = planSchema.safeParse(args.plan);
|
|
135
|
+
if (!parsed.success)
|
|
136
|
+
throw new TeamSetupError(parsed.error.issues[0]?.message ?? "Invalid team setup");
|
|
137
|
+
const chief = this.chief(args.botId);
|
|
138
|
+
const combined = new Map();
|
|
139
|
+
for (const operation of parsed.data.operations) {
|
|
140
|
+
const key = operation.action === "create" ? `create:${operation.key}` : `update:${operation.botId}`;
|
|
141
|
+
const previous = combined.get(key);
|
|
142
|
+
combined.set(key, { ...operation, fields: { ...previous?.fields, ...operation.fields } });
|
|
143
|
+
}
|
|
144
|
+
if (combined.size > 8)
|
|
145
|
+
throw new TeamSetupError("Review at most eight bots in one setup");
|
|
146
|
+
const operations = [...combined.values()].map((operation) => {
|
|
147
|
+
const target = operation.action === "update" ? this.options.store.bot(operation.botId) : undefined;
|
|
148
|
+
return { action: operation.action, botId: operation.action === "create" ? newId() : operation.botId,
|
|
149
|
+
...(operation.action === "create" ? { threadId: newId() } : {}),
|
|
150
|
+
fields: this.fields({ ...operation.fields, section: operation.fields.section ?? target?.section ?? chief.section ?? "" }, target ?? undefined),
|
|
151
|
+
...(target ? { expectedRevision: teamSetupRevision(target) } : {}),
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
const request = { version: 1, requestId: newId(), botId: args.botId, threadId: args.threadId,
|
|
155
|
+
reason: redactSecretsInText(parsed.data.reason), createdAt: Date.now(), requesterRevision: teamSetupRevision(chief),
|
|
156
|
+
newTeams: [...new Set(parsed.data.newTeams)], operations };
|
|
157
|
+
this.validate(request, false);
|
|
158
|
+
return this.card(request, args.from);
|
|
159
|
+
}
|
|
160
|
+
proposeDeletion(args) {
|
|
161
|
+
const chief = this.chief(args.botId);
|
|
162
|
+
const target = this.options.store.bot(args.targetBotId);
|
|
163
|
+
if (!target)
|
|
164
|
+
throw new TeamSetupError("That bot no longer exists", 404);
|
|
165
|
+
if (!args.reason.trim() || args.reason.length > 500)
|
|
166
|
+
throw new TeamSetupError("A deletion reason of at most 500 characters is required");
|
|
167
|
+
const request = { version: 1, requestId: newId(), botId: args.botId, threadId: args.threadId,
|
|
168
|
+
reason: redactSecretsInText(args.reason), createdAt: Date.now(), requesterRevision: teamSetupRevision(chief), newTeams: [], operations: [],
|
|
169
|
+
deletion: { botId: target.id, name: target.name, expectedRevision: teamSetupRevision(target) } };
|
|
170
|
+
this.validate(request, false);
|
|
171
|
+
return this.card(request, args.from);
|
|
172
|
+
}
|
|
173
|
+
card(request, from) {
|
|
174
|
+
const permission = this.options.canPersist(request.botId, request.threadId);
|
|
175
|
+
if (!permission.ok)
|
|
176
|
+
throw new TeamSetupError(permission.error, permission.status);
|
|
177
|
+
const chief = this.chief(request.botId);
|
|
178
|
+
const lines = [`Why: ${request.reason}`];
|
|
179
|
+
if (request.deletion)
|
|
180
|
+
lines.push(`Delete @${request.deletion.name} (${request.deletion.botId}).`, "Permanently removes this bot, all its conversations, memory, instructions, and skills. Generated project files remain. Active work or owned computers can block deletion.");
|
|
181
|
+
if (request.newTeams.length)
|
|
182
|
+
lines.push(`Create teams: ${request.newTeams.map((name) => JSON.stringify(name)).join(", ")}.`, `Authorize @${chief.name} to coordinate and propose setup changes in these new teams.`);
|
|
183
|
+
for (const operation of request.operations) {
|
|
184
|
+
const current = this.options.store.bot(operation.botId);
|
|
185
|
+
lines.push(`\n${operation.action === "create" ? "Create" : "Update"} @${operation.fields.name ?? current?.name} (${operation.action === "create" ? "new bot" : operation.botId})`);
|
|
186
|
+
for (const [key, value] of Object.entries(operation.fields)) {
|
|
187
|
+
const before = current ? (key === "section" ? current.section || "General" : current[key]) : undefined;
|
|
188
|
+
lines.push(`${key === "modelSelection" ? "Default engine/model" : key}: ${current ? `${JSON.stringify(before ?? "")} → ` : ""}${key === "section" ? JSON.stringify(value || "General") : JSON.stringify(value)}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (!request.deletion)
|
|
192
|
+
lines.push("\nDefault models apply to groups and new threads. Every existing thread keeps its current model and permissions.", "New bots start in Ask with connected apps disabled. Existing execution permissions are unchanged.");
|
|
193
|
+
lines.push("After this decision the Chief continues once with the result.");
|
|
194
|
+
const title = request.deletion ? `Delete @${request.deletion.name}?` : `Apply setup for ${request.operations.length} ${request.operations.length === 1 ? "bot" : "bots"}?`;
|
|
195
|
+
const detail = lines.join("\n");
|
|
196
|
+
const message = this.options.store.appendMessage(request.threadId, { role: "bot", kind: "options", ...(from ? { from } : {}), card: {
|
|
197
|
+
title, subtitle: detail, options: [request.deletion ? "Delete bot" : "Apply setup", "Cancel"], requestId: request.requestId,
|
|
198
|
+
tool: request.deletion ? "delete_bot" : "set_up_team", teamSetupRequest: request,
|
|
199
|
+
} });
|
|
200
|
+
return { requestId: request.requestId, messageId: message.id, title, summary: title, detail };
|
|
201
|
+
}
|
|
202
|
+
async resolve(args) {
|
|
203
|
+
const message = this.options.store.messagesFor(args.threadId).find((item) => item.card?.requestId === args.requestId && item.card.teamSetupRequest);
|
|
204
|
+
const card = message?.card, request = card?.teamSetupRequest;
|
|
205
|
+
if (!message || !card || !request)
|
|
206
|
+
return null;
|
|
207
|
+
if (request.botId !== args.botId || request.threadId !== args.threadId || request.requestId !== args.requestId)
|
|
208
|
+
throw new TeamSetupError("This setup belongs to another conversation", 403);
|
|
209
|
+
if (args.behavior !== "allow" && args.behavior !== "deny")
|
|
210
|
+
throw new TeamSetupError("Confirm or cancel this setup", 400);
|
|
211
|
+
// Stop/dismiss can close a card without a setup decision. It must never
|
|
212
|
+
// resurrect that review or wake the stopped conversation.
|
|
213
|
+
if (card.answered || card.dismissed) {
|
|
214
|
+
const result = request.result ?? { state: "cancelled", bots: [], newTeams: [], error: "This setup card was closed" };
|
|
215
|
+
return { result, request: { ...request, result }, messageId: message.id, duplicate: true };
|
|
216
|
+
}
|
|
217
|
+
if (this.resolving.has(request.requestId))
|
|
218
|
+
throw new TeamSetupError("This setup is already being applied", 409);
|
|
219
|
+
this.resolving.add(request.requestId);
|
|
220
|
+
try {
|
|
221
|
+
const receipt = this.options.store.bot(request.botId)?.lastTeamSetupReceipt;
|
|
222
|
+
let result;
|
|
223
|
+
if (receipt?.requestId === request.requestId)
|
|
224
|
+
result = receipt.result;
|
|
225
|
+
else if (args.behavior === "deny")
|
|
226
|
+
result = { state: "denied", bots: [], newTeams: [] };
|
|
227
|
+
else {
|
|
228
|
+
try {
|
|
229
|
+
this.validate(request, true);
|
|
230
|
+
if (request.deletion) {
|
|
231
|
+
await this.options.deleteBot(request.deletion.botId, () => this.validate(request, true), request);
|
|
232
|
+
result = { state: "applied", bots: [{ id: request.deletion.botId, name: request.deletion.name, action: "deleted" }], newTeams: [] };
|
|
233
|
+
}
|
|
234
|
+
else
|
|
235
|
+
result = this.options.store.applyTeamSetup(request);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
const saved = this.options.store.bot(request.botId)?.lastTeamSetupReceipt;
|
|
239
|
+
const detail = redactSecretsInText(error instanceof Error ? error.message : String(error));
|
|
240
|
+
result = saved?.requestId === request.requestId
|
|
241
|
+
? { ...saved.result, error: `The changes were saved, but cleanup needs attention: ${detail}` }
|
|
242
|
+
: { state: error instanceof TeamSetupError ? "cancelled" : "failed", bots: [], newTeams: [], error: detail };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
this.options.store.patchMessage(args.threadId, message.id, { card: { ...card, answered: result.state === "applied" ? "allow" : "deny", held: result.error, teamSetupRequest: { ...request, result } } });
|
|
246
|
+
return { result, request: { ...request, result }, messageId: message.id, duplicate: false };
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
this.resolving.delete(request.requestId);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -1,7 +1,59 @@
|
|
|
1
1
|
// What a tool call is about to do, in words a chip or a permission card can
|
|
2
2
|
// show. Redacted before it is cut: a command line is where credentials get
|
|
3
3
|
// pasted, and a key sliced in half would slip past the shapes redaction knows.
|
|
4
|
-
import { redactSecretsInText } from "./redact.js";
|
|
4
|
+
import { redactSecrets, redactSecretsInText } from "./redact.js";
|
|
5
|
+
/** Display-only excerpt, never the raw protocol payload. Bound traversal and
|
|
6
|
+
* omit binary bodies before redacting; truncate only AFTER redaction so a
|
|
7
|
+
* credential cannot be cut in half and escape detection. */
|
|
8
|
+
export function toolDetailPreview(value) {
|
|
9
|
+
if (value === undefined)
|
|
10
|
+
return undefined;
|
|
11
|
+
let budget = 200;
|
|
12
|
+
let textBudget = 256_000;
|
|
13
|
+
const bounded = (item, depth = 0) => {
|
|
14
|
+
if (--budget < 0 || depth > 6)
|
|
15
|
+
return "[additional data omitted]";
|
|
16
|
+
if (typeof item === "string") {
|
|
17
|
+
if (item.length > textBudget)
|
|
18
|
+
return "[large content omitted]";
|
|
19
|
+
if (/^data:[^,\s]+;base64,/i.test(item))
|
|
20
|
+
return "[binary content omitted]";
|
|
21
|
+
// MCP frequently puts JSON inside text content. Recover its field
|
|
22
|
+
// names so short credentials get the same masking as native objects.
|
|
23
|
+
if (/^\s*[[{]/.test(item)) {
|
|
24
|
+
try {
|
|
25
|
+
return bounded(JSON.parse(item), depth + 1);
|
|
26
|
+
}
|
|
27
|
+
catch { /* ordinary text/code: use the content redactor below */ }
|
|
28
|
+
}
|
|
29
|
+
textBudget -= item.length;
|
|
30
|
+
return item;
|
|
31
|
+
}
|
|
32
|
+
if (typeof item === "bigint")
|
|
33
|
+
return String(item);
|
|
34
|
+
if (item === null || typeof item !== "object")
|
|
35
|
+
return item;
|
|
36
|
+
if (Array.isArray(item))
|
|
37
|
+
return item.slice(0, 40).map((child) => bounded(child, depth + 1)).concat(item.length > 40 ? ["[additional items omitted]"] : []);
|
|
38
|
+
const entries = Object.entries(item);
|
|
39
|
+
const result = Object.create(null);
|
|
40
|
+
for (const [key, child] of entries.slice(0, 40)) {
|
|
41
|
+
result[key] = /token|secret|password|passwd|cookie|authorization|api.?key|(^|[_.-])keys?$/i.test(key)
|
|
42
|
+
? "[redacted]"
|
|
43
|
+
: /^(data|base64|blob)$/i.test(key) && typeof child === "string"
|
|
44
|
+
? "[binary content omitted]"
|
|
45
|
+
: bounded(child, depth + 1);
|
|
46
|
+
}
|
|
47
|
+
if (entries.length > 40)
|
|
48
|
+
result["…"] = "[additional fields omitted]";
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
const safe = redactSecrets(bounded(value));
|
|
52
|
+
const text = typeof safe === "string" ? safe : JSON.stringify(safe, null, 2);
|
|
53
|
+
if (!text?.trim() || text === "{}" || text === "[]")
|
|
54
|
+
return undefined;
|
|
55
|
+
return text.length > 6_000 ? `${text.slice(0, 6_000)}\n[… preview shortened]` : text;
|
|
56
|
+
}
|
|
5
57
|
const QUESTION_LIMIT = 300;
|
|
6
58
|
const LIMIT = 200;
|
|
7
59
|
function fieldsOf(input) {
|
|
@@ -44,8 +44,11 @@ export class RetiredTurnRegistry {
|
|
|
44
44
|
export class ProviderTurnGenerationRegistry {
|
|
45
45
|
#owners = new Map();
|
|
46
46
|
#completed;
|
|
47
|
+
#earlyCompletions = new Map();
|
|
48
|
+
#limit;
|
|
47
49
|
constructor(limit = 4_096) {
|
|
48
50
|
this.#completed = new RetiredTurnRegistry(limit);
|
|
51
|
+
this.#limit = limit;
|
|
49
52
|
}
|
|
50
53
|
bind(threadId, generation, turnId) {
|
|
51
54
|
if (this.#completed.has(turnId))
|
|
@@ -53,23 +56,48 @@ export class ProviderTurnGenerationRegistry {
|
|
|
53
56
|
this.#owners.set(turnId, { threadId, generation });
|
|
54
57
|
return true;
|
|
55
58
|
}
|
|
56
|
-
complete(threadId, turnId) {
|
|
57
|
-
this.#completed.
|
|
59
|
+
complete(threadId, turnId, outcome) {
|
|
60
|
+
if (this.#completed.has(turnId))
|
|
61
|
+
return null;
|
|
58
62
|
const owner = this.#owners.get(turnId);
|
|
59
|
-
if (
|
|
63
|
+
if (owner && owner.threadId !== threadId)
|
|
60
64
|
return null;
|
|
65
|
+
this.#completed.retire(turnId);
|
|
66
|
+
if (!owner) {
|
|
67
|
+
// Only a later ACK for this exact provider id may consume the result.
|
|
68
|
+
// Never attribute an unbound event to the thread's current generation.
|
|
69
|
+
if (outcome !== undefined) {
|
|
70
|
+
this.#earlyCompletions.set(turnId, { threadId, outcome });
|
|
71
|
+
while (this.#earlyCompletions.size > this.#limit) {
|
|
72
|
+
this.#earlyCompletions.delete(this.#earlyCompletions.keys().next().value);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
61
77
|
this.#owners.delete(turnId);
|
|
62
78
|
return owner;
|
|
63
79
|
}
|
|
80
|
+
takeEarlyCompletion(threadId, turnId) {
|
|
81
|
+
const receipt = this.#earlyCompletions.get(turnId);
|
|
82
|
+
if (receipt?.threadId !== threadId)
|
|
83
|
+
return undefined;
|
|
84
|
+
this.#earlyCompletions.delete(turnId);
|
|
85
|
+
return receipt.outcome;
|
|
86
|
+
}
|
|
64
87
|
deleteGeneration(threadId, generation) {
|
|
88
|
+
const removed = [];
|
|
65
89
|
for (const [turnId, owner] of this.#owners) {
|
|
66
90
|
if (owner.threadId === threadId && owner.generation === generation) {
|
|
67
91
|
this.#owners.delete(turnId);
|
|
92
|
+
this.#completed.retire(turnId);
|
|
93
|
+
removed.push(turnId);
|
|
68
94
|
}
|
|
69
95
|
}
|
|
96
|
+
return removed;
|
|
70
97
|
}
|
|
71
98
|
clear() {
|
|
72
99
|
this.#owners.clear();
|
|
100
|
+
this.#earlyCompletions.clear();
|
|
73
101
|
}
|
|
74
102
|
}
|
|
75
103
|
/** Thread gate for the narrow interval after Stop wins but before an async
|
|
@@ -23,6 +23,7 @@ const ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}
|
|
|
23
23
|
const EXCLUDED = new Set([
|
|
24
24
|
".backups", "tools", "cache", ".cache", "tmp", ".tmp", "dist-native", "tunnel-runtime",
|
|
25
25
|
".openmausbot-server-child", "environment-id", "sessions.json", "tunnel-account.json",
|
|
26
|
+
"team-computers.json",
|
|
26
27
|
"openmausbot-server.lease", "box-create-requests.lock", "messages.db-wal", "messages.db-shm",
|
|
27
28
|
]);
|
|
28
29
|
const EXCLUSION_NOTES = [
|
|
@@ -16,6 +16,13 @@ export function supportsApprovalMode(driverKind, mode) {
|
|
|
16
16
|
return true;
|
|
17
17
|
return ["codex", "claudeAgent", "antigravityAgent", "cursorAgent", "grokAgent", "opencodeGo"].includes(driverKind ?? "");
|
|
18
18
|
}
|
|
19
|
+
/** A Full/Custom grant belongs to one provider's tool semantics. Other
|
|
20
|
+
* modes carry across only when the destination actually implements them.
|
|
21
|
+
* Adapted from tahodev's provider-switch guard in PR #1120. */
|
|
22
|
+
export function modelSwitchNeedsAsk(mode, fromDriver, toDriver) {
|
|
23
|
+
return !supportsApprovalMode(toDriver, mode) ||
|
|
24
|
+
((mode === "full" || mode === "custom") && fromDriver !== toDriver);
|
|
25
|
+
}
|
|
19
26
|
export function hasNativeAutoReview(driverKind) {
|
|
20
27
|
return ["codex", "claudeAgent", "cursorAgent", "grokAgent"].includes(driverKind ?? "");
|
|
21
28
|
}
|
|
@@ -27,7 +34,10 @@ export function isApprovalMode(value) {
|
|
|
27
34
|
* behavior: autoApprove=true is safe Auto; everything else asks. Unknown
|
|
28
35
|
* persisted values also fail closed to that legacy behavior. */
|
|
29
36
|
export function approvalModeFor(bot) {
|
|
30
|
-
|
|
37
|
+
const grant = bot.approvalGrant;
|
|
38
|
+
const otherThread = grant && typeof grant === "object" && "threadOnly" in grant && grant.threadOnly === true &&
|
|
39
|
+
"threadId" in grant && typeof grant.threadId === "string" && typeof bot.threadId === "string" && grant.threadId !== bot.threadId;
|
|
40
|
+
if (grant && !otherThread)
|
|
31
41
|
return "ask";
|
|
32
42
|
if (isApprovalMode(bot.approvalMode))
|
|
33
43
|
return bot.approvalMode;
|
|
@@ -21,6 +21,7 @@ const task = z.object({
|
|
|
21
21
|
title: z.string().max(2_000),
|
|
22
22
|
createdAt: timestamp,
|
|
23
23
|
openedBy: z.object({ botId: key, name, at: timestamp }).optional(),
|
|
24
|
+
closedBy: z.object({ botId: key, name, at: timestamp }).optional(),
|
|
24
25
|
activeLeafId: key.nullable(),
|
|
25
26
|
messages: z.array(message).max(100_000),
|
|
26
27
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -15112,7 +15112,11 @@ var featureConfigSchema = external_exports.object({
|
|
|
15112
15112
|
showToolCalls: external_exports.boolean().optional(),
|
|
15113
15113
|
/** Experimental built-in browser. Off until explicitly enabled; each bot
|
|
15114
15114
|
* also has its own switch. */
|
|
15115
|
-
browser: external_exports.boolean().optional()
|
|
15115
|
+
browser: external_exports.boolean().optional(),
|
|
15116
|
+
/** Opt-in computer sharing (a desktop lending folders, a terminal or
|
|
15117
|
+
* computer control to a workspace). Off until explicitly enabled; there is
|
|
15118
|
+
* no Settings toggle — see sharedComputersEnabled. */
|
|
15119
|
+
sharedComputers: external_exports.boolean().optional()
|
|
15116
15120
|
});
|
|
15117
15121
|
var onboardingConfigSchema = external_exports.object({
|
|
15118
15122
|
/** ISO timestamp of finishing (or skipping to the end of) the welcome flow. */
|
package/enterprise/README.md
CHANGED
|
@@ -23,8 +23,11 @@ customer, because the key decides the feature set rather than the code.
|
|
|
23
23
|
{ "edition": "oss", "features": [], "notice": "OMB_LICENSE_KEY expired on 2027-09-02; renew it to keep enterprise features" }
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
A missing, altered, or expired key
|
|
27
|
-
open-source edition and the notice says what to fix.
|
|
26
|
+
A missing, altered, or expired key does not stop an ordinary standalone
|
|
27
|
+
server: it runs the open-source edition and the notice says what to fix.
|
|
28
|
+
A workspace explicitly configured for hosted sign-in fails closed for remote
|
|
29
|
+
access when its `admin` entitlement or identity service is unavailable. It
|
|
30
|
+
must not silently fall back to standalone email or pairing credentials.
|
|
28
31
|
|
|
29
32
|
## Entitlement ids
|
|
30
33
|
|
|
@@ -32,7 +35,7 @@ open-source edition and the notice says what to fix.
|
|
|
32
35
|
|---|---|
|
|
33
36
|
| `whitelabel` | product name, tagline, accent colour, logo, favicon and support link from `brand.json` (below) |
|
|
34
37
|
| `sso` | identity-header trust behind an OIDC proxy |
|
|
35
|
-
| `admin` | the
|
|
38
|
+
| `admin` | Settings → Workspaces and the optional hosted workspace sign-in adapter |
|
|
36
39
|
| `budgets` | per-bot and per-section spend limits |
|
|
37
40
|
|
|
38
41
|
Core gates a feature with `entitled("id")` from `server/enterprise.ts`.
|
|
@@ -59,6 +62,21 @@ working until they expire.
|
|
|
59
62
|
- Customer-specific brand, skills, packages, connectors? The customer's own
|
|
60
63
|
repo: data and config, never a fork.
|
|
61
64
|
|
|
65
|
+
## Hosted workspace sign-in (`admin`)
|
|
66
|
+
|
|
67
|
+
The workspace-side adapter in `server/workspace-access.ts` connects to an
|
|
68
|
+
independently deployed identity service over HTTPS. The service's console,
|
|
69
|
+
invitations, provider gateway and deployment automation are not shipped in
|
|
70
|
+
this repository. The desktop and ordinary self-hosted server do not start
|
|
71
|
+
or depend on a hosted console.
|
|
72
|
+
|
|
73
|
+
Operators opt a workspace into this protocol using `OMB_ADMIN_URL`,
|
|
74
|
+
`OMB_ADMIN_WORKSPACE` and `OMB_PUBLIC_URL`; a valid `admin` entitlement is
|
|
75
|
+
required. The default keeps the local sign-in allow-list as an additional,
|
|
76
|
+
narrowing check. Operators may explicitly delegate membership for verified
|
|
77
|
+
portal sessions with `OMB_ADMIN_MEMBERSHIP=portal`; this never exempts ordinary
|
|
78
|
+
email or pairing sessions. See the [protocol and isolated verification recipe](../docs/verification/hosted-workspaces.md).
|
|
79
|
+
|
|
62
80
|
## White-label (`whitelabel`)
|
|
63
81
|
|
|
64
82
|
Put a `brand.json` in the server's data dir (`OMB_DATA_DIR`, the `/data`
|