openmausbot 0.1.74 → 0.1.75
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-CwoQdM0z.js → index-BTlyRwLl.js} +1 -1
- package/dist/assets/index-CeMbvsgx.js +305 -0
- package/dist/assets/index-DH7Zax6L.css +1 -0
- package/dist/index.html +2 -2
- package/dist-server/index.js +817 -602
- package/dist-server/openmausbot.js +358 -208
- package/dist-server/pair-cli.js +358 -208
- package/dist-server/permission-proxy.js +119 -14
- package/dist-server/server/auto-approve.js +15 -0
- package/dist-server/server/drivers/acp/core.js +2 -1
- package/dist-server/server/drivers/acp/grok.js +12 -1
- package/dist-server/server/drivers/claude.js +60 -3
- package/dist-server/server/drivers/codex.js +15 -4
- package/dist-server/server/drivers/retry.js +4 -0
- package/dist-server/server/index.js +80 -10
- package/dist-server/server/permission-proxy.js +90 -15
- package/dist-server/server/setup-mode.js +6 -7
- package/dist-server/server/store.js +19 -0
- package/dist-server/server/thread-events.js +10 -1
- package/dist-server/server/workspace.js +13 -0
- package/dist-server/shared/ask-question.js +172 -0
- package/dist-server/shared/provider-safety.js +6 -0
- package/package.json +1 -1
- package/dist/assets/index-BRLHsQ5g.js +0 -298
- package/dist/assets/index-ByLQ7a3F.css +0 -1
|
@@ -1,6 +1,80 @@
|
|
|
1
1
|
// server/permission-proxy.ts
|
|
2
2
|
import { connect } from "node:net";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
// shared/ask-question.ts
|
|
6
|
+
var MAX_QUESTIONS = 6;
|
|
7
|
+
var MAX_OPTIONS = 12;
|
|
8
|
+
var MAX_QUESTION_TEXT = 400;
|
|
9
|
+
var MAX_LABEL = 120;
|
|
10
|
+
var MAX_DESCRIPTION = 400;
|
|
11
|
+
function text(value, limit) {
|
|
12
|
+
if (typeof value !== "string") return void 0;
|
|
13
|
+
const trimmed = value.trim();
|
|
14
|
+
return trimmed ? trimmed.slice(0, limit) : void 0;
|
|
15
|
+
}
|
|
16
|
+
function isRecord(value) {
|
|
17
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
function parseOption(value) {
|
|
20
|
+
if (typeof value === "string") {
|
|
21
|
+
const label2 = text(value, MAX_LABEL);
|
|
22
|
+
return label2 ? { label: label2 } : null;
|
|
23
|
+
}
|
|
24
|
+
if (!isRecord(value)) return null;
|
|
25
|
+
const label = text(value.label, MAX_LABEL);
|
|
26
|
+
if (!label) return null;
|
|
27
|
+
const description = text(value.description, MAX_DESCRIPTION);
|
|
28
|
+
return description ? { label, description } : { label };
|
|
29
|
+
}
|
|
30
|
+
function parseQuestion(value) {
|
|
31
|
+
if (!isRecord(value)) return null;
|
|
32
|
+
const question = text(value.question, MAX_QUESTION_TEXT);
|
|
33
|
+
if (!question) return null;
|
|
34
|
+
const options = [];
|
|
35
|
+
const seen = /* @__PURE__ */ new Set();
|
|
36
|
+
for (const raw of Array.isArray(value.options) ? value.options : []) {
|
|
37
|
+
const option = parseOption(raw);
|
|
38
|
+
if (!option || seen.has(option.label)) continue;
|
|
39
|
+
seen.add(option.label);
|
|
40
|
+
options.push(option);
|
|
41
|
+
if (options.length === MAX_OPTIONS) break;
|
|
42
|
+
}
|
|
43
|
+
const header = text(value.header, MAX_LABEL);
|
|
44
|
+
return {
|
|
45
|
+
question,
|
|
46
|
+
...header ? { header } : {},
|
|
47
|
+
...value.multiSelect === true ? { multiSelect: true } : {},
|
|
48
|
+
options
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function parseAskQuestions(input) {
|
|
52
|
+
if (!isRecord(input) || !Array.isArray(input.questions)) return null;
|
|
53
|
+
const questions = [];
|
|
54
|
+
for (const raw of input.questions) {
|
|
55
|
+
const question = parseQuestion(raw);
|
|
56
|
+
if (!question) continue;
|
|
57
|
+
questions.push(question);
|
|
58
|
+
if (questions.length === MAX_QUESTIONS) break;
|
|
59
|
+
}
|
|
60
|
+
return questions.length ? questions : null;
|
|
61
|
+
}
|
|
62
|
+
function questionAnswersByQuestion(message, questions) {
|
|
63
|
+
const answers = {};
|
|
64
|
+
const known = new Map(questions.map((entry) => [entry.question, entry.question]));
|
|
65
|
+
for (const block of message.split("\n\n")) {
|
|
66
|
+
const match = /^Q: ([\s\S]+?)\nA: ([\s\S]+)$/.exec(block.trim());
|
|
67
|
+
if (!match) continue;
|
|
68
|
+
const question = known.get(match[1].trim());
|
|
69
|
+
if (question) answers[question] = match[2].trim();
|
|
70
|
+
}
|
|
71
|
+
if (Object.keys(answers).length) return answers;
|
|
72
|
+
const only = questions.length === 1 ? questions[0] : void 0;
|
|
73
|
+
const flat = message.trim();
|
|
74
|
+
return only && flat ? { [only.question]: flat } : {};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// server/permission-proxy.ts
|
|
4
78
|
var socketPath = process.argv[2] ?? "";
|
|
5
79
|
var waiting = /* @__PURE__ */ new Map();
|
|
6
80
|
var conn = connect(socketPath);
|
|
@@ -32,6 +106,40 @@ conn.on("data", (chunk) => {
|
|
|
32
106
|
}
|
|
33
107
|
});
|
|
34
108
|
var send = (obj) => process.stdout.write(JSON.stringify(obj) + "\n");
|
|
109
|
+
function askBroker(ask) {
|
|
110
|
+
return new Promise((resolve) => {
|
|
111
|
+
waiting.set(String(ask.id), resolve);
|
|
112
|
+
if (conn.destroyed) return dead();
|
|
113
|
+
try {
|
|
114
|
+
conn.write(JSON.stringify(ask) + "\n");
|
|
115
|
+
} catch {
|
|
116
|
+
dead();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
var ASK_USER_QUESTION = "AskUserQuestion";
|
|
121
|
+
var asRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
122
|
+
async function answerNativeQuestions(input) {
|
|
123
|
+
const questions = parseAskQuestions(input);
|
|
124
|
+
if (!questions?.length) {
|
|
125
|
+
return JSON.stringify({
|
|
126
|
+
behavior: "deny",
|
|
127
|
+
message: "OpenMausBot: this AskUserQuestion call had no answerable question (each one needs question text), so nobody was shown it. Ask again with a well-formed call, or continue without it."
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const answer = await askBroker({
|
|
131
|
+
t: "ask",
|
|
132
|
+
id: randomUUID(),
|
|
133
|
+
kind: "question",
|
|
134
|
+
tool: ASK_USER_QUESTION,
|
|
135
|
+
input: { questions }
|
|
136
|
+
});
|
|
137
|
+
if (answer.behavior === "deny") {
|
|
138
|
+
return JSON.stringify({ behavior: "deny", message: answer.message || "Denied from OpenMausBot" });
|
|
139
|
+
}
|
|
140
|
+
const answers = answer.source === "user" && typeof answer.message === "string" ? questionAnswersByQuestion(answer.message, questions) : {};
|
|
141
|
+
return JSON.stringify({ behavior: "allow", updatedInput: { ...asRecord(input), answers } });
|
|
142
|
+
}
|
|
35
143
|
var TOOLS = [
|
|
36
144
|
{
|
|
37
145
|
name: "approve",
|
|
@@ -79,30 +187,27 @@ async function handle(msg) {
|
|
|
79
187
|
if (msg.method === "tools/call") {
|
|
80
188
|
const name = msg.params?.name;
|
|
81
189
|
const args = msg.params?.arguments ?? {};
|
|
190
|
+
const reply = (text3) => send({ jsonrpc: "2.0", id: msg.id, result: { content: [{ type: "text", text: text3 }] } });
|
|
191
|
+
if (name === "approve" && args.tool_name === ASK_USER_QUESTION) {
|
|
192
|
+
return reply(await answerNativeQuestions(args.input));
|
|
193
|
+
}
|
|
82
194
|
const askId = randomUUID();
|
|
83
195
|
const isQuestion = name === "ask_user";
|
|
84
196
|
const suggestions = Array.isArray(args.permission_suggestions) ? args.permission_suggestions : Array.isArray(args.suggestions) ? args.suggestions : null;
|
|
85
|
-
const answer = await
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
conn.write(JSON.stringify(ask) + "\n");
|
|
91
|
-
} catch {
|
|
92
|
-
dead();
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
let text = answer.message || "No answer was given \u2014 use your best judgment.";
|
|
197
|
+
const answer = await askBroker(
|
|
198
|
+
isQuestion ? { t: "ask", id: askId, kind: "question", tool: "ask_user", input: { question: args.question, choices: args.choices } } : { t: "ask", id: askId, tool: args.tool_name, input: args.input }
|
|
199
|
+
);
|
|
200
|
+
let text2 = answer.message || "No answer was given \u2014 use your best judgment.";
|
|
96
201
|
if (!isQuestion) {
|
|
97
202
|
if (answer.behavior === "allow") {
|
|
98
203
|
const result = { behavior: "allow", updatedInput: args.input ?? {} };
|
|
99
204
|
if (answer.always && suggestions) result.updatedPermissions = suggestions;
|
|
100
|
-
|
|
205
|
+
text2 = JSON.stringify(result);
|
|
101
206
|
} else {
|
|
102
|
-
|
|
207
|
+
text2 = JSON.stringify({ behavior: "deny", message: answer.message || "Denied from OpenMausBot" });
|
|
103
208
|
}
|
|
104
209
|
}
|
|
105
|
-
return
|
|
210
|
+
return reply(text2);
|
|
106
211
|
}
|
|
107
212
|
if (String(msg.method ?? "").startsWith("notifications/")) return;
|
|
108
213
|
if (msg.id != null) {
|
|
@@ -17,7 +17,22 @@ export function approvalModeForOrigin(mode, origin) {
|
|
|
17
17
|
return "auto";
|
|
18
18
|
return mode;
|
|
19
19
|
}
|
|
20
|
+
// Tools that ask a PERSON something. A question exists so that a human
|
|
21
|
+
// decides; any mode answering one on their behalf defeats the only reason
|
|
22
|
+
// it was asked. They normally arrive typed as questions and never reach a
|
|
23
|
+
// verdict at all — this is the backstop for the path where one arrives
|
|
24
|
+
// mis-typed as a permission (a malformed AskUserQuestion call falls back to
|
|
25
|
+
// the permission path in permission-proxy). Approving it there does not
|
|
26
|
+
// produce an answer: the CLI runs the tool with none and the model is told
|
|
27
|
+
// "The user did not answer the questions." — a question silently lost.
|
|
28
|
+
const ASKS_A_PERSON = new Set(["askuserquestion", "ask_user"]);
|
|
20
29
|
export function autoVerdict(mode, tool, context) {
|
|
30
|
+
// A question is for a person, whatever channel it arrived on — and
|
|
31
|
+
// whatever the mode: even Full has no answer to give, only an approval
|
|
32
|
+
// that would run the tool with none.
|
|
33
|
+
if (ASKS_A_PERSON.has(tool.replace(/^mcp__[^_]+__/, "").toLowerCase())) {
|
|
34
|
+
return { approve: null, source: "no-grant" };
|
|
35
|
+
}
|
|
21
36
|
// Full's promise is literal: even a sandbox widening is approved. Entering
|
|
22
37
|
// Full is separately consent-gated by the bot PATCH endpoint, and the
|
|
23
38
|
// request.opened caller invokes this for permissions only, never questions.
|
|
@@ -745,7 +745,8 @@ export function createAcpDriver(support) {
|
|
|
745
745
|
}
|
|
746
746
|
}
|
|
747
747
|
const images = turn.images ?? [];
|
|
748
|
-
const runtimeAcceptsImages = init?.agentCapabilities?.promptCapabilities?.image === true
|
|
748
|
+
const runtimeAcceptsImages = init?.agentCapabilities?.promptCapabilities?.image === true ||
|
|
749
|
+
support.acceptsUnadvertisedImages?.(init) === true;
|
|
749
750
|
if (images.length && support.images === true && !runtimeAcceptsImages) {
|
|
750
751
|
throw new Error(`${support.displayName} is configured for image attachments, but this installed runtime does not advertise ACP image input. Update the ${support.displayName} CLI or send the message without an image.`);
|
|
751
752
|
}
|
|
@@ -175,10 +175,21 @@ export function ensureGrokInjectSlug(modelId, env = process.env) {
|
|
|
175
175
|
writeFileSync(path, next);
|
|
176
176
|
return slug;
|
|
177
177
|
}
|
|
178
|
+
/** Grok 1.0.25 consumes native image blocks but advertises image:false.
|
|
179
|
+
* Verified with the real CLI and a loopback model: scripts/verify-grok-images.ts.
|
|
180
|
+
* Keep unknown/older runtimes on the normal capability negotiation path. */
|
|
181
|
+
export function grokAcceptsUnadvertisedImages(init) {
|
|
182
|
+
const meta = init?._meta;
|
|
183
|
+
if (meta?.grokShell !== true || typeof meta.agentVersion !== "string")
|
|
184
|
+
return false;
|
|
185
|
+
const version = /^1\.0\.(\d+)$/.exec(meta.agentVersion);
|
|
186
|
+
return Boolean(version && Number(version[1]) >= 25);
|
|
187
|
+
}
|
|
178
188
|
const support = {
|
|
179
189
|
driverKind: "grokAgent",
|
|
180
190
|
displayName: "Grok",
|
|
181
|
-
images:
|
|
191
|
+
images: true,
|
|
192
|
+
acceptsUnadvertisedImages: grokAcceptsUnadvertisedImages,
|
|
182
193
|
models: STATIC_GROK_MODELS,
|
|
183
194
|
resolveModels: (env) => mergeLocalInject(readGrokModelCatalog(env), env),
|
|
184
195
|
// Grok's accepted levels vary by model and the CLI validates lazily — a
|
|
@@ -26,6 +26,7 @@ import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS }
|
|
|
26
26
|
import { applyClaudeInject, decodeInjectId, mergeLocalInject, probeLocalInjects, resolveInjectId, } from "./local-inject.js";
|
|
27
27
|
import { appendNative } from "./native.js";
|
|
28
28
|
import { SPAWNED_PROXIES } from "../proxy-paths.js";
|
|
29
|
+
import { ASK_USER_QUESTION_TOOL, askQuestionSummary, parseAskQuestions, questionChoices, } from "../../shared/ask-question.js";
|
|
29
30
|
/** Whether `claude` has been signed in.
|
|
30
31
|
*
|
|
31
32
|
* Credential storage is deliberately not inspected here. Claude Code uses the
|
|
@@ -150,6 +151,30 @@ function claudeEnvironment(model, source = process.env, configDir, instanceEnvir
|
|
|
150
151
|
function inheritsUserConfig(env) {
|
|
151
152
|
return env.OMB_CLAUDE_INHERIT_USER_CONFIG === "1";
|
|
152
153
|
}
|
|
154
|
+
/** Retain the selected CLI account's authentication without importing its
|
|
155
|
+
* hooks, permissions, MCP servers or personal instructions. Explicit OMB
|
|
156
|
+
* connections/local endpoints own their entire routing + credential pair. */
|
|
157
|
+
export function readClaudeAuthSettings(env, instanceEnvironment = {}) {
|
|
158
|
+
if (CLAUDE_ACCOUNT_ENV_KEYS.some((key) => instanceEnvironment[key]))
|
|
159
|
+
return {};
|
|
160
|
+
try {
|
|
161
|
+
const settings = JSON.parse(readFileSync(join(resolveClaudeConfigDir(undefined, env), "settings.json"), "utf8"));
|
|
162
|
+
const authEnv = {};
|
|
163
|
+
for (const key of CLAUDE_ACCOUNT_ENV_KEYS) {
|
|
164
|
+
if (!key.endsWith("_FILE_DESCRIPTOR") && typeof settings?.env?.[key] === "string") {
|
|
165
|
+
authEnv[key] = settings.env[key];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
...(Object.keys(authEnv).length ? { env: authEnv } : {}),
|
|
170
|
+
...(typeof settings?.apiKeyHelper === "string" && settings.apiKeyHelper.trim()
|
|
171
|
+
? { apiKeyHelper: settings.apiKeyHelper } : {}),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return {};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
153
178
|
/** MCP servers the bot's own project declares in `<cwd>/.mcp.json`.
|
|
154
179
|
*
|
|
155
180
|
* The CLI would find this file itself, but the harness launches it with
|
|
@@ -370,8 +395,17 @@ function systemEndedReply(kind) {
|
|
|
370
395
|
? { behavior: "answer", message: "OpenMausBot: the turn is ending — wrap up." }
|
|
371
396
|
: { behavior: "deny", message: "OpenMausBot: the turn ended" };
|
|
372
397
|
}
|
|
398
|
+
/** The structured questions behind an ask, when it is one. Claude's own
|
|
399
|
+
* AskUserQuestion carries them; everything else answers null and keeps the
|
|
400
|
+
* plain summary/choices card. */
|
|
401
|
+
function askQuestions(ask) {
|
|
402
|
+
return ask.tool === ASK_USER_QUESTION_TOOL ? parseAskQuestions(ask.input) : null;
|
|
403
|
+
}
|
|
373
404
|
/** One human-readable line for an ask — what the card subtitle shows. */
|
|
374
405
|
function askSummary(ask) {
|
|
406
|
+
const questions = askQuestions(ask);
|
|
407
|
+
if (questions)
|
|
408
|
+
return askQuestionSummary(questions).slice(0, 300);
|
|
375
409
|
return askInputSummary(ask.input) ?? ask.tool ?? "tool";
|
|
376
410
|
}
|
|
377
411
|
export function permissionSocketPath(threadId) {
|
|
@@ -490,7 +524,10 @@ export async function createPermissionBroker(opts) {
|
|
|
490
524
|
// `always` rides to the proxy, which hands the CLI's own suggested
|
|
491
525
|
// permission rules back as updatedPermissions: Claude remembers
|
|
492
526
|
// the allow for the session, the harness remembers nothing.
|
|
493
|
-
|
|
527
|
+
// `source` travels with the answer too: a proxy that cannot tell
|
|
528
|
+
// the human's words from a timeout note would file the timeout
|
|
529
|
+
// note as the human's words.
|
|
530
|
+
conn.write(JSON.stringify({ t: "answer", id: askId, behavior, message, source, ...(always ? { always: true } : {}) }) + "\n");
|
|
494
531
|
}
|
|
495
532
|
catch { }
|
|
496
533
|
opts.onResolve({ ...ask, behavior, source });
|
|
@@ -1028,6 +1065,12 @@ export const ClaudeDriver = {
|
|
|
1028
1065
|
args.push("--allowedTools", allowed.join(","));
|
|
1029
1066
|
}
|
|
1030
1067
|
const env = environment(turnModel);
|
|
1068
|
+
const authSettings = isolated && !injected.injected
|
|
1069
|
+
? readClaudeAuthSettings(env, input.environment) : {};
|
|
1070
|
+
const authSettingsPath = mcpConfigPath && Object.keys(authSettings).length
|
|
1071
|
+
? join(dirname(mcpConfigPath), "auth-settings.json") : null;
|
|
1072
|
+
if (authSettingsPath)
|
|
1073
|
+
args.push("--settings", authSettingsPath);
|
|
1031
1074
|
// Our approvals and browser credentials expire at the user-turn
|
|
1032
1075
|
// boundary. Native background workers cannot outlive that boundary;
|
|
1033
1076
|
// parallel bot work must use the harness's durable delegate_bot path.
|
|
@@ -1035,7 +1078,7 @@ export const ClaudeDriver = {
|
|
|
1035
1078
|
const cwd = turn.cwd ?? homedir();
|
|
1036
1079
|
// Everything that shapes the process, minus session/turn-specific temp
|
|
1037
1080
|
// paths. Their contents are represented directly in the key instead.
|
|
1038
|
-
const privateFileFlags = new Set(["--mcp-config"]);
|
|
1081
|
+
const privateFileFlags = new Set(["--mcp-config", "--settings"]);
|
|
1039
1082
|
const keyArgs = args.filter((a, i) => !privateFileFlags.has(a) && !privateFileFlags.has(args[i - 1] ?? ""));
|
|
1040
1083
|
const argsKey = JSON.stringify({
|
|
1041
1084
|
args: keyArgs,
|
|
@@ -1047,6 +1090,11 @@ export const ClaudeDriver = {
|
|
|
1047
1090
|
model: injected.model ?? null,
|
|
1048
1091
|
base: env.ANTHROPIC_BASE_URL ?? null,
|
|
1049
1092
|
configDir: env.CLAUDE_CONFIG_DIR ?? null,
|
|
1093
|
+
// Rotating an account's key/helper must not reuse the old process.
|
|
1094
|
+
auth: createHash("sha256").update(JSON.stringify({
|
|
1095
|
+
settings: authSettings,
|
|
1096
|
+
env: Object.fromEntries(CLAUDE_ACCOUNT_ENV_KEYS.map((key) => [key, env[key]])),
|
|
1097
|
+
})).digest("hex"),
|
|
1050
1098
|
});
|
|
1051
1099
|
// Reuse the live process when it is idle, unchanged, and is the session
|
|
1052
1100
|
// the harness wants resumed. Anything else: close it and spawn fresh
|
|
@@ -1137,6 +1185,7 @@ export const ClaudeDriver = {
|
|
|
1137
1185
|
const nativeReview = permissionMode === "auto" && nativeMode !== null
|
|
1138
1186
|
? nativeMode === "auto" ? "active" : "inactive"
|
|
1139
1187
|
: undefined;
|
|
1188
|
+
const questions = askQuestions(ask);
|
|
1140
1189
|
emit({
|
|
1141
1190
|
...base(threadId, eventTurnId),
|
|
1142
1191
|
type: "request.opened",
|
|
@@ -1151,7 +1200,12 @@ export const ClaudeDriver = {
|
|
|
1151
1200
|
approvalScope: typeof ask.tool === "string" && controlsHost && ask.tool.startsWith("mcp__computer")
|
|
1152
1201
|
? "local-computer"
|
|
1153
1202
|
: undefined,
|
|
1154
|
-
|
|
1203
|
+
questions: questions ?? undefined,
|
|
1204
|
+
// A structured ask still offers flat labels, for the phone
|
|
1205
|
+
// companions and any client that predates the question card.
|
|
1206
|
+
choices: questions
|
|
1207
|
+
? questionChoices(questions)
|
|
1208
|
+
: Array.isArray(ask.input?.choices) ? ask.input.choices.slice(0, 5) : undefined,
|
|
1155
1209
|
});
|
|
1156
1210
|
},
|
|
1157
1211
|
onResolve: (resolved) => {
|
|
@@ -1180,6 +1234,9 @@ export const ClaudeDriver = {
|
|
|
1180
1234
|
if (mcpConfigPath) {
|
|
1181
1235
|
writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers }), { mode: 0o600 });
|
|
1182
1236
|
}
|
|
1237
|
+
if (authSettingsPath) {
|
|
1238
|
+
writeFileSync(authSettingsPath, JSON.stringify(authSettings), { mode: 0o600 });
|
|
1239
|
+
}
|
|
1183
1240
|
if (sessionId)
|
|
1184
1241
|
args.push("--resume", sessionId);
|
|
1185
1242
|
else
|
|
@@ -524,6 +524,7 @@ export const CodexDriver = {
|
|
|
524
524
|
const earlyNotifications = [];
|
|
525
525
|
const state = {
|
|
526
526
|
settled: false,
|
|
527
|
+
lastError: "",
|
|
527
528
|
lastText: "",
|
|
528
529
|
sawStreamDelta: false,
|
|
529
530
|
// codex reports token usage as a running THREAD total; the harness
|
|
@@ -878,7 +879,15 @@ export const CodexDriver = {
|
|
|
878
879
|
}
|
|
879
880
|
case "turn/completed": {
|
|
880
881
|
const t = p.turn ?? {};
|
|
881
|
-
|
|
882
|
+
const message = typeof t.error?.message === "string" ? t.error.message.slice(0, 400) : "";
|
|
883
|
+
if (t.status !== "completed" && message && message !== state.lastError) {
|
|
884
|
+
state.lastError = message;
|
|
885
|
+
emit({ ...base(threadId, turnId), type: "runtime.error", message,
|
|
886
|
+
...(classifyError({ text: message }).reason === "auth" ? { setup: true } : {}),
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
void settle(t.status === "completed", t.status === "completed" ? null :
|
|
890
|
+
(classifyError({ text: message || state.lastError }).reason === "provider_safety" ? "provider_safety" : (message || t.status || "failed")));
|
|
882
891
|
break;
|
|
883
892
|
}
|
|
884
893
|
case "error":
|
|
@@ -886,8 +895,10 @@ export const CodexDriver = {
|
|
|
886
895
|
// {error:{message}} — surface either (agentcal armor)
|
|
887
896
|
{
|
|
888
897
|
const message = p.message ?? p.error?.message;
|
|
889
|
-
if (message)
|
|
890
|
-
|
|
898
|
+
if (message) {
|
|
899
|
+
state.lastError = String(message).slice(0, 400);
|
|
900
|
+
emit({ ...base(threadId, turnId), type: "runtime.error", message: state.lastError });
|
|
901
|
+
}
|
|
891
902
|
}
|
|
892
903
|
break;
|
|
893
904
|
}
|
|
@@ -1145,7 +1156,7 @@ export const CodexDriver = {
|
|
|
1145
1156
|
message,
|
|
1146
1157
|
...(needsAuth ? { setup: true } : {}),
|
|
1147
1158
|
});
|
|
1148
|
-
await settle(false, needsAuth ? "auth_required" : "rpc_error");
|
|
1159
|
+
await settle(false, needsAuth ? "auth_required" : verdict.reason === "provider_safety" ? "provider_safety" : "rpc_error");
|
|
1149
1160
|
}
|
|
1150
1161
|
}
|
|
1151
1162
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isProviderSafetyBlock } from "../../shared/provider-safety.js";
|
|
1
2
|
export const RETRY_MAX_ATTEMPTS = 3;
|
|
2
3
|
/** Backoff schedule before attempt N (N is 1-based over retries): 1s / 3s / 8s. */
|
|
3
4
|
export const BACKOFF_BASE_MS = [1_000, 3_000, 8_000];
|
|
@@ -42,6 +43,9 @@ const messageOf = (err) => {
|
|
|
42
43
|
*/
|
|
43
44
|
export function classifyError(err) {
|
|
44
45
|
const text = messageOf(err);
|
|
46
|
+
// A surrounding HTTP 5xx/429 must not replay a provider safety block.
|
|
47
|
+
if (isProviderSafetyBlock(text))
|
|
48
|
+
return { transient: false, reason: "provider_safety" };
|
|
45
49
|
if (err && "exitCode" in err) {
|
|
46
50
|
const { exitCode: code } = err;
|
|
47
51
|
if (code !== null && code < 0)
|
|
@@ -75,7 +75,7 @@ import { buildRecoveryText, buildTurnContext, engineIsFresh } from "./turn-conte
|
|
|
75
75
|
import { extractTurnImages } from "./turn-images.js";
|
|
76
76
|
import { TurnWatchdog } from "./turn-watchdog.js";
|
|
77
77
|
import { TurnResources, workspaceResource } from "./turn-resources.js";
|
|
78
|
-
import { ensureWorkspace, ensureTaskWorkspace, updateMemory, appendMemoryLog, isMemoryTopicName, memorySystemPrompt, memorySourceLabel, searchMemoryFiles, SESSION_SEARCH_SYSTEM_PROMPT, workspaceDir, } from "./workspace.js";
|
|
78
|
+
import { ensureWorkspace, ensureTaskWorkspace, workspaceLocationsPrompt, updateMemory, appendMemoryLog, isMemoryTopicName, memorySystemPrompt, memorySourceLabel, searchMemoryFiles, SESSION_SEARCH_SYSTEM_PROMPT, workspaceDir, } from "./workspace.js";
|
|
79
79
|
import { readMemoryTopic } from "./workspace.js";
|
|
80
80
|
import { MEMORY_INDEX, MemoryStoreError, memoryCapacity, memoryOverview, openMemoryLocation, readMemoryDoc, } from "./memory-store.js";
|
|
81
81
|
import { beginMemoryTurn, endMemoryTurn, flushAllMemoryJournals, flushMemoryJournal, journalMemoryDelete, journalMemoryWrite, readMemoryJournal, revertMemoryChange, } from "./memory-journal.js";
|
|
@@ -1213,6 +1213,14 @@ function handleDesktopTrustedApprovalMessage(raw) {
|
|
|
1213
1213
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
1214
1214
|
return false;
|
|
1215
1215
|
const message = raw;
|
|
1216
|
+
const threadCanReceiveGrant = (bot) => {
|
|
1217
|
+
const threadId = bot.approvalGrant?.threadId;
|
|
1218
|
+
if (!threadId)
|
|
1219
|
+
return true;
|
|
1220
|
+
const target = store.projectBotForTask(bot.id, threadId);
|
|
1221
|
+
return Boolean(target && !threadBusy(bot.id, threadId) &&
|
|
1222
|
+
registry.cliTarget(target.modelSelection.instanceId)?.driverKind === registry.cliTarget(bot.modelSelection.instanceId)?.driverKind);
|
|
1223
|
+
};
|
|
1216
1224
|
if (message.type === "approval-trusted-mode-commit") {
|
|
1217
1225
|
const requestId = typeof message.requestId === "string" && /^[A-Za-z0-9_-]{1,80}$/.test(message.requestId)
|
|
1218
1226
|
? message.requestId
|
|
@@ -1231,7 +1239,11 @@ function handleDesktopTrustedApprovalMessage(raw) {
|
|
|
1231
1239
|
bot.approvalGrant.phase === "committed" &&
|
|
1232
1240
|
bot.approvalMode === mode &&
|
|
1233
1241
|
!bot.busy &&
|
|
1242
|
+
threadCanReceiveGrant(bot) &&
|
|
1234
1243
|
supportsApprovalMode(registry.cliTarget(bot.modelSelection.instanceId)?.driverKind, mode)) {
|
|
1244
|
+
if (bot.approvalGrant.threadId) {
|
|
1245
|
+
store.patchTask(botId, bot.approvalGrant.threadId, { approvalMode: mode, autoApprove: false });
|
|
1246
|
+
}
|
|
1235
1247
|
store.patchBot(botId, { approvalGrant: undefined });
|
|
1236
1248
|
}
|
|
1237
1249
|
else if (bot?.approvalGrant?.requestId === requestId) {
|
|
@@ -1272,7 +1284,7 @@ function handleDesktopTrustedApprovalMessage(raw) {
|
|
|
1272
1284
|
return true;
|
|
1273
1285
|
}
|
|
1274
1286
|
store.patchBot(botId, {
|
|
1275
|
-
approvalGrant: { requestId, mode, phase: "confirmed" },
|
|
1287
|
+
approvalGrant: { ...bot.approvalGrant, requestId, mode, phase: "confirmed" },
|
|
1276
1288
|
});
|
|
1277
1289
|
confirm(true);
|
|
1278
1290
|
return true;
|
|
@@ -1321,7 +1333,7 @@ function handleDesktopTrustedApprovalMessage(raw) {
|
|
|
1321
1333
|
}
|
|
1322
1334
|
// Still inert: Electron must receive this acknowledgement and request
|
|
1323
1335
|
// finalization before the durable mode can affect any turn.
|
|
1324
|
-
store.patchBot(botId, { approvalGrant: { requestId, mode, phase: "activated" } });
|
|
1336
|
+
store.patchBot(botId, { approvalGrant: { ...bot.approvalGrant, requestId, mode, phase: "activated" } });
|
|
1325
1337
|
activate(true);
|
|
1326
1338
|
return true;
|
|
1327
1339
|
}
|
|
@@ -1359,10 +1371,11 @@ function handleDesktopTrustedApprovalMessage(raw) {
|
|
|
1359
1371
|
bot.approvalGrant.phase === "activated" &&
|
|
1360
1372
|
bot.approvalMode === mode &&
|
|
1361
1373
|
!bot.busy &&
|
|
1374
|
+
threadCanReceiveGrant(bot) &&
|
|
1362
1375
|
supportsApprovalMode(registry.cliTarget(bot.modelSelection.instanceId)?.driverKind, mode)) {
|
|
1363
1376
|
// Durable but still inert. Electron must observe this exact ACK before
|
|
1364
1377
|
// sending the one-way commit release that clears the journal.
|
|
1365
|
-
store.patchBot(botId, { approvalGrant: { requestId, mode, phase: "committed" } });
|
|
1378
|
+
store.patchBot(botId, { approvalGrant: { ...bot.approvalGrant, requestId, mode, phase: "committed" } });
|
|
1366
1379
|
finalize(true);
|
|
1367
1380
|
return true;
|
|
1368
1381
|
}
|
|
@@ -1406,6 +1419,20 @@ function handleDesktopTrustedApprovalMessage(raw) {
|
|
|
1406
1419
|
return true;
|
|
1407
1420
|
}
|
|
1408
1421
|
const currentMode = approvalModeFor(existing);
|
|
1422
|
+
const threadId = message.threadId;
|
|
1423
|
+
if (threadId !== undefined) {
|
|
1424
|
+
const target = typeof threadId === "string" && /^[\w-]{1,128}$/.test(threadId)
|
|
1425
|
+
? store.projectBotForTask(botId, threadId) : null;
|
|
1426
|
+
if (!target || (mode !== "full" && mode !== "custom") || currentMode !== mode || existing.approvalGrant) {
|
|
1427
|
+
respond({ ok: false, error: "Choose this bot's approval level in bot settings before applying it to an existing thread" });
|
|
1428
|
+
return true;
|
|
1429
|
+
}
|
|
1430
|
+
if (threadBusy(botId, threadId) ||
|
|
1431
|
+
registry.cliTarget(target.modelSelection.instanceId)?.driverKind !== registry.cliTarget(existing.modelSelection.instanceId)?.driverKind) {
|
|
1432
|
+
respond({ ok: false, error: "Stop this thread and use the bot's provider before applying its approval level" });
|
|
1433
|
+
return true;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1409
1436
|
const emergencyDowngrade = existing.busy && isEmergencyApprovalDowngrade(currentMode, mode);
|
|
1410
1437
|
const clearsPendingElevation = mode === "ask" && existing.approvalGrant !== undefined;
|
|
1411
1438
|
if (existing.busy && !emergencyDowngrade && !clearsPendingElevation) {
|
|
@@ -1432,7 +1459,7 @@ function handleDesktopTrustedApprovalMessage(raw) {
|
|
|
1432
1459
|
approvalMode: mode,
|
|
1433
1460
|
autoApprove: mode === "auto",
|
|
1434
1461
|
approvalGrant: mode === "full" || mode === "custom"
|
|
1435
|
-
? { requestId, mode, phase: "prepared" }
|
|
1462
|
+
? { requestId, mode, phase: "prepared", ...(typeof threadId === "string" ? { threadId } : {}) }
|
|
1436
1463
|
: undefined,
|
|
1437
1464
|
});
|
|
1438
1465
|
if (!updated) {
|
|
@@ -2197,6 +2224,15 @@ always) {
|
|
|
2197
2224
|
outcome = "unavailable";
|
|
2198
2225
|
}
|
|
2199
2226
|
}
|
|
2227
|
+
// An answered question keeps its words. `request.resolved` only records
|
|
2228
|
+
// the behavior, so without this the card reads "answer" forever and the
|
|
2229
|
+
// person can no longer see what they told the bot.
|
|
2230
|
+
if (outcome !== "unavailable" && behavior === "answer" && message && cardMessage) {
|
|
2231
|
+
const settled = store.messagesFor(threadId).find((m) => m.id === cardMessage.id)?.card;
|
|
2232
|
+
if (settled) {
|
|
2233
|
+
store.patchMessage(threadId, cardMessage.id, { card: { ...settled, answeredText: message } });
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2200
2236
|
// The human's verdict, recorded only when it actually reached the engine:
|
|
2201
2237
|
// `unavailable` means the action never ran, and a "user-approved" row
|
|
2202
2238
|
// over a request nothing answered would be the audit log lying. A
|
|
@@ -2883,7 +2919,11 @@ bus.subscribe((event) => {
|
|
|
2883
2919
|
}
|
|
2884
2920
|
break;
|
|
2885
2921
|
case "request.opened": {
|
|
2886
|
-
|
|
2922
|
+
// A structured ask carries the model's own options and has no
|
|
2923
|
+
// allow/deny answer, so it is a question no matter which channel the
|
|
2924
|
+
// provider routed it through — and, like every question, no approval
|
|
2925
|
+
// mode may ever answer it for the person.
|
|
2926
|
+
const permission = event.requestType === "permission" && !event.questions?.length;
|
|
2887
2927
|
// A permission request here is one the provider left for a person: its
|
|
2888
2928
|
// own mode already ran (Ask, Edits, Auto's reviewer, Custom's config).
|
|
2889
2929
|
// OpenMausBot decides nothing about the action itself. Only Full access
|
|
@@ -2985,6 +3025,10 @@ bus.subscribe((event) => {
|
|
|
2985
3025
|
break;
|
|
2986
3026
|
}
|
|
2987
3027
|
const heldContext = { source: verdict?.source, permission };
|
|
3028
|
+
// A structured ask (Claude's AskUserQuestion) is a question whatever
|
|
3029
|
+
// the provider routed it as: it has no allow/deny answer, only the
|
|
3030
|
+
// model's own options. The card carries them so the person can choose.
|
|
3031
|
+
const questions = event.questions?.length ? event.questions : undefined;
|
|
2988
3032
|
const message = pushMessage({
|
|
2989
3033
|
role: "bot",
|
|
2990
3034
|
kind: "options",
|
|
@@ -2998,6 +3042,7 @@ bus.subscribe((event) => {
|
|
|
2998
3042
|
options: event.choices?.length ? event.choices : permission ? ["Allow", "Deny"] : [],
|
|
2999
3043
|
requestId: event.requestId,
|
|
3000
3044
|
tool: permission ? event.tool : undefined,
|
|
3045
|
+
questionRequest: questions ? { version: 1, questions } : undefined,
|
|
3001
3046
|
// the provider can keep an allow for its session; the app keeps
|
|
3002
3047
|
// no grant of its own for a provider's tool
|
|
3003
3048
|
allowSession: permission && event.allowSession && !event.requiresExplicitApproval ? true : undefined,
|
|
@@ -4511,6 +4556,7 @@ async function startTurn(botId, text, opts) {
|
|
|
4511
4556
|
// to a turn whose engine actually mounted them (setupMode is already
|
|
4512
4557
|
// false when they are not — see agentsMounted above)
|
|
4513
4558
|
{ id: "setup", label: "Setup", text: setupSystemPrompt(setupMode, { skills: skillAuthoring, cwd: liveBot?.cwd ?? bot.cwd }) },
|
|
4559
|
+
{ id: "files", label: "File locations", text: worksInWorkspace && opts?.runOn !== "cloud" ? workspaceLocationsPrompt(bot.id, cwd, liveBot?.cwd ?? bot.cwd) : "" },
|
|
4514
4560
|
{ id: "computer", label: "Computer", text: computerPrompt(computerPromptKind) },
|
|
4515
4561
|
{ id: "plan", label: "Surface", text: plan.note },
|
|
4516
4562
|
// gated on the integration, not the key: the hint only goes to a
|
|
@@ -5616,6 +5662,7 @@ setupRetry = 0) {
|
|
|
5616
5662
|
store.patchBot(bot.id, { soulDrift: drift.drift });
|
|
5617
5663
|
}
|
|
5618
5664
|
const roomSystem = buildSystemPrompt(system, store.bot(bot.id)?.soul ?? bot.soul ?? "", [
|
|
5665
|
+
{ id: "files", label: "File locations", text: workspace ? workspaceLocationsPrompt(bot.id, cwd, readyBot.cwd) : "" },
|
|
5619
5666
|
{ id: "mcp", label: "MCP servers", text: customMcpPrompt(Object.keys(integrations.custom ?? {})) },
|
|
5620
5667
|
{ id: "computer", label: "Computer", text: computerPrompt(roomVmTarget ? localVmMode(cfg) === "per-bot" ? "vm-private" : "vm-shared" : null) },
|
|
5621
5668
|
{ id: "browser", label: "Browser", text: integrations.browser ? BUILT_IN_BROWSER_SYSTEM_PROMPT : "" },
|
|
@@ -12392,15 +12439,17 @@ const handleRequest = async (req, res) => {
|
|
|
12392
12439
|
const current = store.projectBotForTask(m[1], m[2]);
|
|
12393
12440
|
if (!current)
|
|
12394
12441
|
return json(res, 404, { error: "no such task" });
|
|
12395
|
-
const allowed = new Set(["title", "projectId", "modelSelection", "approvalMode", "autoApprove", "requireAvailableModel", "pinnedMessageId", "acknowledgeLocalAuto"]);
|
|
12442
|
+
const allowed = new Set(["title", "projectId", "modelSelection", "updateBotDefault", "approvalMode", "autoApprove", "requireAvailableModel", "pinnedMessageId", "acknowledgeLocalAuto"]);
|
|
12396
12443
|
if (Object.keys(body).some((key) => !allowed.has(key)))
|
|
12397
12444
|
return json(res, 400, { error: "unsupported thread setting" });
|
|
12398
|
-
for (const key of ["requireAvailableModel", "acknowledgeLocalAuto"]) {
|
|
12445
|
+
for (const key of ["requireAvailableModel", "acknowledgeLocalAuto", "updateBotDefault"]) {
|
|
12399
12446
|
if (body[key] !== undefined && typeof body[key] !== "boolean")
|
|
12400
12447
|
return json(res, 400, { error: `${key} must be a boolean` });
|
|
12401
12448
|
}
|
|
12402
12449
|
if (body.requireAvailableModel === true && body.modelSelection === undefined)
|
|
12403
12450
|
return json(res, 400, { error: "requireAvailableModel requires modelSelection" });
|
|
12451
|
+
if (body.updateBotDefault === true && body.modelSelection === undefined)
|
|
12452
|
+
return json(res, 400, { error: "updateBotDefault requires modelSelection" });
|
|
12404
12453
|
const patch = {};
|
|
12405
12454
|
if (body.projectId !== undefined) {
|
|
12406
12455
|
if (body.projectId === null)
|
|
@@ -12437,8 +12486,11 @@ const handleRequest = async (req, res) => {
|
|
|
12437
12486
|
const mode = body.approvalMode ?? (body.autoApprove ? "auto" : "ask");
|
|
12438
12487
|
// Elevated modes still require the trusted desktop transition. A
|
|
12439
12488
|
// thread settings PATCH cannot manufacture that grant.
|
|
12440
|
-
if (mode !== "ask" && mode !== "auto")
|
|
12441
|
-
return json(res, 403, { error: "Full and Custom access require
|
|
12489
|
+
if (mode !== "ask" && mode !== "auto" && mode !== "edits")
|
|
12490
|
+
return json(res, 403, { error: "Full and Custom access require trusted desktop confirmation" });
|
|
12491
|
+
if (!supportsApprovalMode(registry.cliTarget(current.modelSelection.instanceId)?.driverKind, mode)) {
|
|
12492
|
+
return json(res, 400, { error: "This provider does not support the selected approval level" });
|
|
12493
|
+
}
|
|
12442
12494
|
if (threadBusy(current.id, current.threadId))
|
|
12443
12495
|
return json(res, 409, { error: "stop this thread before changing its approval mode" });
|
|
12444
12496
|
if (current.approvalGrant)
|
|
@@ -12458,6 +12510,24 @@ const handleRequest = async (req, res) => {
|
|
|
12458
12510
|
registry.cliTarget(patch.modelSelection.instanceId)?.driverKind !== registry.cliTarget(current.modelSelection.instanceId)?.driverKind)) {
|
|
12459
12511
|
return json(res, 400, { error: "Choose Ask for this thread before changing providers with elevated permissions" });
|
|
12460
12512
|
}
|
|
12513
|
+
if (body.updateBotDefault === true && patch.modelSelection) {
|
|
12514
|
+
const profile = store.bot(current.id);
|
|
12515
|
+
const checked = checkedModelSelection(patch.modelSelection, {
|
|
12516
|
+
selection: profile.modelSelection, busy: Boolean(activeGroupTurnForBot(current.id)),
|
|
12517
|
+
});
|
|
12518
|
+
if (!checked.ok)
|
|
12519
|
+
return json(res, checked.status, { error: checked.error });
|
|
12520
|
+
const defaultMode = approvalModeFor(profile);
|
|
12521
|
+
if ((defaultMode === "full" || defaultMode === "custom") &&
|
|
12522
|
+
(!supportsApprovalMode(registry.cliTarget(patch.modelSelection.instanceId)?.driverKind, defaultMode) ||
|
|
12523
|
+
registry.cliTarget(patch.modelSelection.instanceId)?.driverKind !== registry.cliTarget(profile.modelSelection.instanceId)?.driverKind)) {
|
|
12524
|
+
return json(res, 400, { error: "Choose Ask in bot settings before changing its default provider with elevated permissions" });
|
|
12525
|
+
}
|
|
12526
|
+
// Validate both scopes before saving either. Existing sibling threads
|
|
12527
|
+
// retain their selections; only this pinned thread and future/group
|
|
12528
|
+
// turns adopt the new default. Do not target the currently selected tab.
|
|
12529
|
+
store.patchBot(current.id, { modelSelection: patch.modelSelection });
|
|
12530
|
+
}
|
|
12461
12531
|
const task = store.patchTask(m[1], m[2], patch);
|
|
12462
12532
|
const fresh = botWithThread(store.bot(m[1]));
|
|
12463
12533
|
broadcast({ kind: "bot", bot: fresh });
|