openmausbot 0.1.68 → 0.1.69
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-HqooKFTS.js → index-BmHupgVJ.js} +1 -1
- package/dist/assets/index-COp5lMnv.js +279 -0
- package/dist/assets/index-Jk7TIeyW.css +1 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +38 -2
- package/dist-server/drivers/agents-proxy.js +85 -5
- package/dist-server/index.js +1465 -611
- package/dist-server/openmausbot.js +554 -191
- package/dist-server/pair-cli.js +554 -191
- package/dist-server/server/avatar-image.js +86 -34
- package/dist-server/server/bot-package.js +60 -14
- package/dist-server/server/config.js +23 -2
- package/dist-server/server/drivers/acp/qwen.js +142 -5
- package/dist-server/server/drivers/agents-proxy.js +95 -4
- package/dist-server/server/drivers/codex-device-auth.js +102 -3
- package/dist-server/server/drivers/codex-identity.js +115 -0
- package/dist-server/server/drivers/codex.js +6 -0
- package/dist-server/server/harness/registry.js +2 -0
- package/dist-server/server/index.js +26 -5
- package/dist-server/server/package-export.js +7 -0
- package/dist-server/server/provider-auth-sessions.js +24 -0
- package/dist-server/server/routine-requests.js +270 -25
- package/dist-server/server/routines.js +185 -18
- package/dist-server/server/steer-queue.js +28 -0
- package/dist-server/shared/image-generation.js +28 -0
- package/dist-server/vps-container-mcp.js +38 -2
- package/package.json +1 -1
- package/dist/assets/index-CbaejjQA.js +0 -278
- package/dist/assets/index-aJ5EWZOQ.css +0 -1
|
@@ -64,7 +64,7 @@ const WEEKDAYS = [
|
|
|
64
64
|
const ROUTINE_SCHEDULE_SCHEMA = {
|
|
65
65
|
type: "object",
|
|
66
66
|
additionalProperties: false,
|
|
67
|
-
description: 'Either {"type":"once","at":RFC3339} for one future run, {"type":"weekly","time":"HH:MM","weekdays":[...]} for chosen days, {"type":"daily","time":"HH:MM"} for every day, or {"type":"interval","every_minutes":15
|
|
67
|
+
description: 'Either {"type":"once","at":RFC3339} for one future run, {"type":"weekly","time":"HH:MM","weekdays":[...]} for chosen days, {"type":"daily","time":"HH:MM"} for every day, or {"type":"interval","every_minutes":15} to repeat. Intervals can optionally be limited with weekdays, window_start + window_end, and ends_at.',
|
|
68
68
|
properties: {
|
|
69
69
|
type: {
|
|
70
70
|
type: "string",
|
|
@@ -82,7 +82,7 @@ const ROUTINE_SCHEDULE_SCHEMA = {
|
|
|
82
82
|
weekdays: {
|
|
83
83
|
type: "array",
|
|
84
84
|
items: { type: "string", enum: WEEKDAYS },
|
|
85
|
-
description: "
|
|
85
|
+
description: "For type weekly: required run days. For type interval: optional allowed days. Values use the computer's local timezone.",
|
|
86
86
|
},
|
|
87
87
|
every_minutes: {
|
|
88
88
|
type: "integer",
|
|
@@ -94,6 +94,30 @@ const ROUTINE_SCHEDULE_SCHEMA = {
|
|
|
94
94
|
type: "string",
|
|
95
95
|
description: "Optional for type interval: RFC3339 date-time with an explicit timezone offset that anchors the cadence. Omit to start one interval after confirmation.",
|
|
96
96
|
},
|
|
97
|
+
window_start: {
|
|
98
|
+
type: "string",
|
|
99
|
+
description: "Optional for type interval, together with window_end: local 24-hour HH:MM when runs may begin, inclusive.",
|
|
100
|
+
},
|
|
101
|
+
window_end: {
|
|
102
|
+
type: "string",
|
|
103
|
+
description: "Optional for type interval, together with window_start: local 24-hour HH:MM when the allowed window ends, exclusive. It must be later on the same day.",
|
|
104
|
+
},
|
|
105
|
+
ends_at: {
|
|
106
|
+
type: "string",
|
|
107
|
+
description: "Optional for type interval: inclusive RFC3339 date-time cutoff with an explicit timezone offset.",
|
|
108
|
+
},
|
|
109
|
+
every_day: {
|
|
110
|
+
type: "boolean",
|
|
111
|
+
description: "Only for an interval update: true removes an existing weekday restriction.",
|
|
112
|
+
},
|
|
113
|
+
all_day: {
|
|
114
|
+
type: "boolean",
|
|
115
|
+
description: "Only for an interval update: true removes an existing time-window restriction.",
|
|
116
|
+
},
|
|
117
|
+
never_ends: {
|
|
118
|
+
type: "boolean",
|
|
119
|
+
description: "Only for an interval update: true removes an existing end cutoff.",
|
|
120
|
+
},
|
|
97
121
|
},
|
|
98
122
|
required: ["type"],
|
|
99
123
|
};
|
|
@@ -111,7 +135,7 @@ const SHORT_WEEKDAYS = {
|
|
|
111
135
|
};
|
|
112
136
|
const SUPPORTED_SCHEDULES = 'Supported schedules: {"type":"once","at":"2026-09-01T09:00:00+05:30"} (future RFC3339 with explicit offset), ' +
|
|
113
137
|
'{"type":"weekly","time":"09:00","weekdays":["monday","friday"]}, {"type":"daily","time":"09:00"}, ' +
|
|
114
|
-
'or {"type":"interval","every_minutes":15}.';
|
|
138
|
+
'or {"type":"interval","every_minutes":15,"weekdays":["monday","friday"],"window_start":"09:00","window_end":"17:00"}.';
|
|
115
139
|
/** A schedule as the harness accepts it, or a message telling the model
|
|
116
140
|
* exactly what to send instead. Coercion first, error second: models
|
|
117
141
|
* routinely stringify nested objects, say "daily", or shorten weekday
|
|
@@ -135,7 +159,7 @@ function normalizeScheduleInput(args) {
|
|
|
135
159
|
: type === "weekly" || type === "daily"
|
|
136
160
|
? ["type", "time", "weekdays"]
|
|
137
161
|
: type === "interval"
|
|
138
|
-
? ["type", "every_minutes", "everyMinutes", "starts_at", "anchorAt"]
|
|
162
|
+
? ["type", "every_minutes", "everyMinutes", "starts_at", "anchorAt", "weekdays", "every_day", "window_start", "window_end", "window", "all_day", "ends_at", "endsAt", "never_ends"]
|
|
139
163
|
: null;
|
|
140
164
|
// Provider conversions may send unused optional fields as null. Ignore
|
|
141
165
|
// those, but never silently discard an actual scheduling constraint (for
|
|
@@ -181,6 +205,16 @@ function normalizeScheduleInput(args) {
|
|
|
181
205
|
return { schedule: { type: "weekly", time, weekdays: normalized } };
|
|
182
206
|
}
|
|
183
207
|
if (type === "interval") {
|
|
208
|
+
for (const flag of ["every_day", "all_day", "never_ends"]) {
|
|
209
|
+
if (raw[flag] != null && typeof raw[flag] !== "boolean") {
|
|
210
|
+
return { error: `"${flag}" must be true or false.` };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (raw.window != null && (!jsonRecord(raw.window)
|
|
214
|
+
|| Object.keys(raw.window).some((key) => key !== "start" && key !== "end")
|
|
215
|
+
|| typeof raw.window.start !== "string" || typeof raw.window.end !== "string")) {
|
|
216
|
+
return { error: '"window" must contain "start" and "end" in HH:MM, for example {"start":"09:00","end":"17:00"}.' };
|
|
217
|
+
}
|
|
184
218
|
const rawMinutes = raw.every_minutes ?? raw.everyMinutes;
|
|
185
219
|
const everyMinutes = Number(rawMinutes);
|
|
186
220
|
if (!Number.isInteger(everyMinutes) || everyMinutes < 5 || everyMinutes > 1_440) {
|
|
@@ -190,11 +224,68 @@ function normalizeScheduleInput(args) {
|
|
|
190
224
|
if (rawStart !== undefined && (typeof rawStart !== "string" || !rawStart.trim())) {
|
|
191
225
|
return { error: '"starts_at" must be an RFC3339 date-time with an explicit timezone offset.' };
|
|
192
226
|
}
|
|
227
|
+
if (raw.every_day === true && Array.isArray(raw.weekdays) && raw.weekdays.length > 0) {
|
|
228
|
+
return { error: 'Choose interval "weekdays" or "every_day", not both.' };
|
|
229
|
+
}
|
|
230
|
+
let intervalWeekdays;
|
|
231
|
+
if (raw.every_day === true) {
|
|
232
|
+
intervalWeekdays = null;
|
|
233
|
+
}
|
|
234
|
+
else if (raw.weekdays !== undefined) {
|
|
235
|
+
if (!Array.isArray(raw.weekdays) || raw.weekdays.length === 0) {
|
|
236
|
+
return { error: 'Interval "weekdays" must contain at least one full weekday name.' };
|
|
237
|
+
}
|
|
238
|
+
intervalWeekdays = [];
|
|
239
|
+
for (const day of raw.weekdays) {
|
|
240
|
+
const lower = String(day).trim().toLowerCase();
|
|
241
|
+
const full = WEEKDAYS.includes(lower)
|
|
242
|
+
? lower
|
|
243
|
+
: Object.hasOwn(SHORT_WEEKDAYS, lower)
|
|
244
|
+
? SHORT_WEEKDAYS[lower]
|
|
245
|
+
: undefined;
|
|
246
|
+
if (!full)
|
|
247
|
+
return { error: `Unsupported weekday "${String(day)}". Use full names: ${WEEKDAYS.join(", ")}.` };
|
|
248
|
+
if (!intervalWeekdays.includes(full))
|
|
249
|
+
intervalWeekdays.push(full);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const rawWindow = jsonRecord(raw.window) ? raw.window : undefined;
|
|
253
|
+
const windowStart = raw.window_start ?? rawWindow?.start;
|
|
254
|
+
const windowEnd = raw.window_end ?? rawWindow?.end;
|
|
255
|
+
if (raw.all_day === true && (windowStart !== undefined || windowEnd !== undefined)) {
|
|
256
|
+
return { error: 'Choose window_start + window_end or "all_day", not both.' };
|
|
257
|
+
}
|
|
258
|
+
let window;
|
|
259
|
+
if (raw.all_day === true) {
|
|
260
|
+
window = null;
|
|
261
|
+
}
|
|
262
|
+
else if (windowStart !== undefined || windowEnd !== undefined) {
|
|
263
|
+
if (typeof windowStart !== "string" || !windowStart.trim() || typeof windowEnd !== "string" || !windowEnd.trim()) {
|
|
264
|
+
return { error: 'An interval time window needs both "window_start" and "window_end" in 24-hour HH:MM.' };
|
|
265
|
+
}
|
|
266
|
+
window = { start: windowStart.trim(), end: windowEnd.trim() };
|
|
267
|
+
}
|
|
268
|
+
const rawEnd = raw.ends_at ?? raw.endsAt;
|
|
269
|
+
if (raw.never_ends === true && rawEnd !== undefined) {
|
|
270
|
+
return { error: 'Choose "ends_at" or "never_ends", not both.' };
|
|
271
|
+
}
|
|
272
|
+
let endsAt;
|
|
273
|
+
if (raw.never_ends === true)
|
|
274
|
+
endsAt = null;
|
|
275
|
+
else if (rawEnd !== undefined) {
|
|
276
|
+
if (typeof rawEnd !== "string" || !rawEnd.trim()) {
|
|
277
|
+
return { error: '"ends_at" must be an RFC3339 date-time with an explicit timezone offset.' };
|
|
278
|
+
}
|
|
279
|
+
endsAt = rawEnd.trim();
|
|
280
|
+
}
|
|
193
281
|
return {
|
|
194
282
|
schedule: {
|
|
195
283
|
type: "interval",
|
|
196
284
|
everyMinutes,
|
|
197
285
|
...(typeof rawStart === "string" ? { anchorAt: rawStart.trim() } : {}),
|
|
286
|
+
...(intervalWeekdays !== undefined ? { weekdays: intervalWeekdays } : {}),
|
|
287
|
+
...(window !== undefined ? { window } : {}),
|
|
288
|
+
...(endsAt !== undefined ? { endsAt } : {}),
|
|
198
289
|
},
|
|
199
290
|
};
|
|
200
291
|
}
|
|
@@ -45,6 +45,7 @@ function loginFailure(output) {
|
|
|
45
45
|
* output stays in bounded private memory; only a device URL/code is exposed. */
|
|
46
46
|
export class CodexDeviceAuthController {
|
|
47
47
|
flow = null;
|
|
48
|
+
command = null;
|
|
48
49
|
disposed = false;
|
|
49
50
|
options;
|
|
50
51
|
constructor(options) { this.options = options; }
|
|
@@ -121,12 +122,111 @@ export class CodexDeviceAuthController {
|
|
|
121
122
|
await this.flow?.stopping;
|
|
122
123
|
if (this.flow?.terminationFailed)
|
|
123
124
|
throw new Error(this.flow.status.message);
|
|
125
|
+
if (this.command && !await this.stopChild(this.command)) {
|
|
126
|
+
throw new Error("Codex could not be stopped on this server. Ask the server administrator to stop the account command before trying again.");
|
|
127
|
+
}
|
|
124
128
|
}
|
|
125
129
|
async dispose() {
|
|
126
130
|
this.disposed = true;
|
|
127
131
|
await this.cancel();
|
|
128
132
|
this.flow = null;
|
|
129
133
|
}
|
|
134
|
+
/** Remove the ChatGPT sign-in Codex stores for this server account so a
|
|
135
|
+
* different account can connect. A sign-in in progress is never pulled
|
|
136
|
+
* away underneath the browser completing it. */
|
|
137
|
+
async signOut() {
|
|
138
|
+
if (this.disposed)
|
|
139
|
+
throw new Error("This provider was removed. Refresh Settings before signing out.");
|
|
140
|
+
if (this.flow?.status.phase === "waiting")
|
|
141
|
+
throw new Error("Finish or cancel the ChatGPT sign-in in progress before signing out.");
|
|
142
|
+
await this.flow?.stopping;
|
|
143
|
+
if (this.disposed)
|
|
144
|
+
throw new Error("This provider was removed. Refresh Settings before signing out.");
|
|
145
|
+
const env = this.options.environment();
|
|
146
|
+
const home = env.HOME || env.USERPROFILE || homedir();
|
|
147
|
+
if (!isAbsolute(home) || (env.CODEX_HOME && !isAbsolute(env.CODEX_HOME))) {
|
|
148
|
+
throw new Error("Use an absolute HOME and CODEX_HOME path for this server's Codex provider before signing out.");
|
|
149
|
+
}
|
|
150
|
+
const homeKey = canonicalPath(resolve(env.CODEX_HOME || join(home, ".codex")));
|
|
151
|
+
if (authenticatingHomes.has(homeKey)) {
|
|
152
|
+
throw new Error("A ChatGPT sign-in is running for this server account. Finish or cancel it before signing out.");
|
|
153
|
+
}
|
|
154
|
+
authenticatingHomes.add(homeKey);
|
|
155
|
+
try {
|
|
156
|
+
const before = await this.exec(["login", "status"], env, home);
|
|
157
|
+
if (before.code === 1 && /^not logged in\b/im.test(before.output))
|
|
158
|
+
return;
|
|
159
|
+
if (before.code !== 0 || !/^logged in.*chatgpt\b/im.test(before.output)) {
|
|
160
|
+
throw new Error("Codex did not confirm a ChatGPT sign-in. Other authentication methods will not be removed; ask the server administrator to check the account.");
|
|
161
|
+
}
|
|
162
|
+
const logout = await this.exec(["logout"], env, home);
|
|
163
|
+
if (logout.code !== 0)
|
|
164
|
+
throw new Error("Codex could not remove the sign-in on this server. Check the server's Codex installation and try again.");
|
|
165
|
+
// The command's own report is not enough: confirm with the same status
|
|
166
|
+
// check that decides whether bots may run on this account.
|
|
167
|
+
const status = await this.exec(["login", "status"], env, home);
|
|
168
|
+
if (status.code !== 1 || !/^not logged in\b/im.test(status.output)) {
|
|
169
|
+
throw new Error("Codex still reports a sign-in on this server. Update Codex and check its login status before trying again.");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
// If the OS cannot stop a command, it may still change credentials.
|
|
174
|
+
// Keep the home reserved until that exact child actually exits.
|
|
175
|
+
const child = this.command;
|
|
176
|
+
if (child && child.exitCode === null && child.signalCode === null) {
|
|
177
|
+
child.once("close", () => authenticatingHomes.delete(homeKey));
|
|
178
|
+
}
|
|
179
|
+
else
|
|
180
|
+
authenticatingHomes.delete(homeKey);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** One bounded, non-interactive Codex command. Its output stays here;
|
|
184
|
+
* callers see an exit code and a status-line match, never the text. */
|
|
185
|
+
exec(args, env, cwd) {
|
|
186
|
+
return new Promise((resolveExec, rejectExec) => {
|
|
187
|
+
if (this.disposed) {
|
|
188
|
+
rejectExec(new Error("This provider was removed. Refresh Settings before signing out."));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
let child;
|
|
192
|
+
try {
|
|
193
|
+
child = spawnCli(this.options.cli, args, { env: { ...env, NO_COLOR: "1" }, cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
rejectExec(new Error("Codex could not start on this server. Install or update the configured Codex CLI, then try again."));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
this.command = child;
|
|
200
|
+
child.stdin.end();
|
|
201
|
+
let output = "";
|
|
202
|
+
const receive = (chunk) => {
|
|
203
|
+
if (output.length < MAX_OUTPUT)
|
|
204
|
+
output += chunk.toString("utf8").slice(0, MAX_OUTPUT - output.length);
|
|
205
|
+
};
|
|
206
|
+
child.stdout.on("data", receive);
|
|
207
|
+
child.stderr.on("data", receive);
|
|
208
|
+
// A hung CLI must not hold the credential-home lock forever.
|
|
209
|
+
const timer = setTimeout(() => {
|
|
210
|
+
void this.stopChild(child).catch(() => false).then((stopped) => {
|
|
211
|
+
if (!stopped)
|
|
212
|
+
rejectExec(new Error("Codex could not be stopped on this server. Ask the server administrator to stop the account command before trying again."));
|
|
213
|
+
});
|
|
214
|
+
}, this.options.startupTimeoutMs ?? 30_000);
|
|
215
|
+
timer.unref();
|
|
216
|
+
child.once("error", (error) => {
|
|
217
|
+
clearTimeout(timer);
|
|
218
|
+
rejectExec(new Error(error.code === "ENOENT"
|
|
219
|
+
? "Codex is not installed on this server. Run npm install -g @openai/codex@latest on the server, then try again."
|
|
220
|
+
: "Codex could not start on this server. Check the configured CLI path and its executable permissions."));
|
|
221
|
+
});
|
|
222
|
+
child.once("close", (code) => {
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
if (this.command === child)
|
|
225
|
+
this.command = null;
|
|
226
|
+
resolveExec({ code, output: stripVTControlCharacters(output) });
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
}
|
|
130
230
|
run(flow, args, env, cwd, done, parsePrompt = false) {
|
|
131
231
|
if (flow.status.phase !== "waiting")
|
|
132
232
|
return;
|
|
@@ -188,7 +288,7 @@ export class CodexDeviceAuthController {
|
|
|
188
288
|
else
|
|
189
289
|
flow.reject(new Error(message ?? "ChatGPT sign-in did not finish."));
|
|
190
290
|
}
|
|
191
|
-
flow.stopping = this.stopChild(flow).catch(() => false).then((stopped) => {
|
|
291
|
+
flow.stopping = this.stopChild(flow.child).catch(() => false).then((stopped) => {
|
|
192
292
|
const release = () => authenticatingHomes.delete(flow.homeKey);
|
|
193
293
|
if (stopped) {
|
|
194
294
|
release();
|
|
@@ -206,8 +306,7 @@ export class CodexDeviceAuthController {
|
|
|
206
306
|
if (phase === "succeeded")
|
|
207
307
|
void this.options.onAuthenticated?.().catch(() => { });
|
|
208
308
|
}
|
|
209
|
-
async stopChild(
|
|
210
|
-
const child = flow.child;
|
|
309
|
+
async stopChild(child) {
|
|
211
310
|
if (!child || await killCliTree(child, this.options.terminateTimeoutMs ?? 1500))
|
|
212
311
|
return true;
|
|
213
312
|
// A broken CLI may ignore SIGTERM. Do not leave a stale device flow able
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Let Codex resolve its effective account, including keyring credentials.
|
|
2
|
+
// Reading auth.json directly can label an API-key or keyring login with an
|
|
3
|
+
// old file's email. Only the protocol's display email leaves this helper.
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { killCliTree, spawnCli } from "../procs.js";
|
|
7
|
+
const MAX_OUTPUT = 16_384;
|
|
8
|
+
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
9
|
+
/** Where this environment's Codex keeps its credentials, or null when the
|
|
10
|
+
* configured home is not absolute (the login controller refuses those too). */
|
|
11
|
+
export function codexHome(env) {
|
|
12
|
+
if (env.CODEX_HOME)
|
|
13
|
+
return isAbsolute(env.CODEX_HOME) ? resolve(env.CODEX_HOME) : null;
|
|
14
|
+
const home = env.HOME || env.USERPROFILE || homedir();
|
|
15
|
+
return isAbsolute(home) ? join(home, ".codex") : null;
|
|
16
|
+
}
|
|
17
|
+
/** Read-only account metadata. Unsupported CLIs and uncertain responses stay
|
|
18
|
+
* unnamed; never fall back to a potentially inactive credential file. */
|
|
19
|
+
export async function codexAccountEmail(cli, env, timeoutMs = 3_000) {
|
|
20
|
+
const cwd = env.HOME || env.USERPROFILE || homedir();
|
|
21
|
+
if (!isAbsolute(cwd) || !codexHome(env))
|
|
22
|
+
return null;
|
|
23
|
+
return new Promise((resolveEmail) => {
|
|
24
|
+
let child;
|
|
25
|
+
try {
|
|
26
|
+
child = spawnCli(cli, ["app-server"], { cwd, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
resolveEmail(null);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
let finishing = false;
|
|
33
|
+
let buffer = "";
|
|
34
|
+
let outputBytes = 0;
|
|
35
|
+
let initialized = false;
|
|
36
|
+
const finish = async (email) => {
|
|
37
|
+
if (finishing)
|
|
38
|
+
return;
|
|
39
|
+
finishing = true;
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
let stopped = await killCliTree(child, 1_000);
|
|
42
|
+
if (!stopped && child.pid) {
|
|
43
|
+
try {
|
|
44
|
+
if (process.platform === "win32")
|
|
45
|
+
child.kill("SIGKILL");
|
|
46
|
+
else
|
|
47
|
+
process.kill(-child.pid, "SIGKILL");
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
try {
|
|
51
|
+
child.kill("SIGKILL");
|
|
52
|
+
}
|
|
53
|
+
catch { /* already gone */ }
|
|
54
|
+
}
|
|
55
|
+
stopped = await killCliTree(child, 1_000);
|
|
56
|
+
}
|
|
57
|
+
resolveEmail(stopped ? email : null);
|
|
58
|
+
};
|
|
59
|
+
const timer = setTimeout(() => { void finish(null); }, timeoutMs);
|
|
60
|
+
timer.unref();
|
|
61
|
+
const send = (message) => {
|
|
62
|
+
try {
|
|
63
|
+
child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
void finish(null);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
child.stdin.on("error", () => { void finish(null); });
|
|
70
|
+
child.stderr.resume(); // Do not retain or log raw CLI errors or credentials.
|
|
71
|
+
child.on("error", () => { void finish(null); });
|
|
72
|
+
child.on("close", () => { void finish(null); });
|
|
73
|
+
child.stdout.setEncoding("utf8");
|
|
74
|
+
child.stdout.on("data", (chunk) => {
|
|
75
|
+
if (finishing)
|
|
76
|
+
return;
|
|
77
|
+
outputBytes += Buffer.byteLength(chunk);
|
|
78
|
+
if (outputBytes > MAX_OUTPUT) {
|
|
79
|
+
void finish(null);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
buffer += chunk;
|
|
83
|
+
let newline;
|
|
84
|
+
while (!finishing && (newline = buffer.indexOf("\n")) !== -1) {
|
|
85
|
+
const line = buffer.slice(0, newline);
|
|
86
|
+
buffer = buffer.slice(newline + 1);
|
|
87
|
+
if (!line.trim())
|
|
88
|
+
continue;
|
|
89
|
+
let message;
|
|
90
|
+
try {
|
|
91
|
+
message = JSON.parse(line);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
void finish(null);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!initialized && message?.id === 1) {
|
|
98
|
+
if (message.error || !message.result) {
|
|
99
|
+
void finish(null);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
initialized = true;
|
|
103
|
+
send({ method: "initialized", params: {} });
|
|
104
|
+
send({ id: 2, method: "account/read", params: { refreshToken: false } });
|
|
105
|
+
}
|
|
106
|
+
else if (initialized && message?.id === 2) {
|
|
107
|
+
const account = message.error ? null : message.result?.account;
|
|
108
|
+
const email = account?.type === "chatgpt" ? account.email : null;
|
|
109
|
+
void finish(typeof email === "string" && email.length <= 254 && EMAIL.test(email) && !/[\p{Cc}\p{Cf}]/u.test(email) ? email : null);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
send({ id: 1, method: "initialize", params: { clientInfo: { name: "openmausbot", version: "1" } } });
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -24,6 +24,7 @@ import { classifyError, computeBackoff, RETRY_MAX_ATTEMPTS } from "./retry.js";
|
|
|
24
24
|
import { appendNative } from "./native.js";
|
|
25
25
|
import { codexDeveloperInstructions, syncCodexInstructions } from "./codex-instructions.js";
|
|
26
26
|
import { CodexDeviceAuthController } from "./codex-device-auth.js";
|
|
27
|
+
import { codexAccountEmail } from "./codex-identity.js";
|
|
27
28
|
export { decodeCodexSelection, readCodexModelCatalog, STATIC_CODEX_MODELS } from "./codex-catalog.js";
|
|
28
29
|
const DRIVER_KIND = "codex";
|
|
29
30
|
const ASTRA_MODEL_ID = "gpt-6-astra";
|
|
@@ -1093,11 +1094,15 @@ export const CodexDriver = {
|
|
|
1093
1094
|
const authenticated = await new Promise((resolve) => {
|
|
1094
1095
|
execCli(config.cli, ["login", "status"], { timeout: 8000, env }, (err, stdout, stderr) => resolve(!err && /^logged in\b/im.test(`${stdout}\n${stderr ?? ""}`)));
|
|
1095
1096
|
});
|
|
1097
|
+
// Display identity only, so Settings can say whose ChatGPT account the
|
|
1098
|
+
// bots run on; the status command above stays the authority on sign-in.
|
|
1099
|
+
const email = authenticated ? await codexAccountEmail(config.cli, env) : null;
|
|
1096
1100
|
// childEnv drops OPENAI_API_KEY on purpose — turns run on the ChatGPT login
|
|
1097
1101
|
return {
|
|
1098
1102
|
state: "available",
|
|
1099
1103
|
version,
|
|
1100
1104
|
authenticated,
|
|
1105
|
+
...(email ? { account: { email } } : {}),
|
|
1101
1106
|
update: codexAstraUpdate(version, models, config.cli),
|
|
1102
1107
|
billing: "subscription",
|
|
1103
1108
|
};
|
|
@@ -1114,6 +1119,7 @@ export const CodexDriver = {
|
|
|
1114
1119
|
startAuthentication: () => authentication.start(),
|
|
1115
1120
|
getAuthentication: (flowId) => authentication.get(flowId),
|
|
1116
1121
|
cancelAuthentication: () => authentication.cancel(),
|
|
1122
|
+
signOut: () => authentication.signOut(),
|
|
1117
1123
|
snapshot,
|
|
1118
1124
|
adapter: {
|
|
1119
1125
|
provider: DRIVER_KIND,
|
|
@@ -213,6 +213,8 @@ export class ProviderRegistry {
|
|
|
213
213
|
: inst.getAuthentication
|
|
214
214
|
? "device-code" // a code to enter at the provider's page (Codex)
|
|
215
215
|
: "browser", // a link and a callback URL (managed engines)
|
|
216
|
+
// the browser may remove the stored sign-in to switch accounts
|
|
217
|
+
signOut: inst.signOut !== undefined,
|
|
216
218
|
}
|
|
217
219
|
: undefined,
|
|
218
220
|
cli: this.cliByInstance.get(inst.instanceId),
|
|
@@ -21,7 +21,7 @@ import { appendDecision, readDecisions } from "./decision-log.js";
|
|
|
21
21
|
import { validateBotCwd } from "./bot-cwd.js";
|
|
22
22
|
import { ATTACHMENTS_DIR, attachmentExists, cleanupStaleAttachmentPartials, deleteAttachment, extensionForMime, FILE_MAX_BYTES, IMAGE_MAX_BYTES, readAttachment, saveFile, saveImage, saveImageUpload, validateAttachmentUploadId, } from "./attachments.js";
|
|
23
23
|
import { messageFileDisposition, messageFileDownloadName, messageAttachmentName, messageFileRoots, messageImageTargetAt, messageReferencesFile, openMessageFile, } from "./message-file.js";
|
|
24
|
-
import { avatarGenerationRequestSchema, avatarGenerationStateMatches, generateAvatarImage, snapshotAvatarGenerationState, } from "./avatar-image.js";
|
|
24
|
+
import { avatarGenerationRequestSchema, avatarGenerationStateMatches, generateAvatarImage, avatarImageStatus, snapshotAvatarGenerationState, } from "./avatar-image.js";
|
|
25
25
|
import { fitsOnOneLine, parseBotProfilePatch } from "./bot-profile.js";
|
|
26
26
|
import { groupTurnCwd } from "./room-cwd.js";
|
|
27
27
|
import { RoomTurnDeadline, RoomTurnStallRegistry, roomTurnTimeoutMessage } from "./room-turn-timeout.js";
|
|
@@ -54,7 +54,7 @@ import { claimRecallCrossings, recallCrossingLabel } from "./recall-disclosure.j
|
|
|
54
54
|
const SESSION_READ_MAX_CHARS = 8_000;
|
|
55
55
|
import { promptWithReply, transcriptText } from "./replies.js";
|
|
56
56
|
import { _loadPending, buildDelegationFailurePrompt, buildDelegationRevivalPrompt, DelegationWakeBudget, discardDelegations, drainDelegations, findDelegationReceipt, pendingDelegationInfo, pendingDelegationSnapshot, pendingThreads, queueDelegation, recordDelegationReceipt, releaseDelegationsWaitingOn, summarizeDelegatedActivity } from "./delegations.js";
|
|
57
|
-
import { cancelSteeredMessage, drainSteeredMessages, queuedSteeredMessage, queueSteeredMessage, } from "./steer-queue.js";
|
|
57
|
+
import { cancelSteeredMessage, drainSteeredMessages, onSteeredQueueChange, queuedSteerSnapshot, queuedSteeredMessage, queueSteeredMessage, } from "./steer-queue.js";
|
|
58
58
|
import { cancelChannelMessage, drainChannelMessages, queuedChannelMessage, queueChannelMessage, } from "./channel-queue.js";
|
|
59
59
|
import { acceptedSendMatch, parseSendId, sendFingerprint, SendSequencer, } from "./send-idempotency.js";
|
|
60
60
|
import { EventBus } from "./harness/bus.js";
|
|
@@ -1368,6 +1368,7 @@ const publicBot = (bot) => ({
|
|
|
1368
1368
|
activeLeafId: store.activeLeaf(bot.threadId),
|
|
1369
1369
|
tasks: store.tasks(bot.id).map(wireTask),
|
|
1370
1370
|
});
|
|
1371
|
+
const publicBotQueuedMessages = () => queuedSteerSnapshot((botId, threadId) => Boolean(store.taskByThread(botId, threadId)));
|
|
1371
1372
|
// busyBotId names only the speaker that currently owns the provider process.
|
|
1372
1373
|
// A room turn is wider: it also includes async setup and every responder still
|
|
1373
1374
|
// queued behind that speaker. Keep that operation visible for its whole
|
|
@@ -1750,6 +1751,7 @@ store.onChange((change) => {
|
|
|
1750
1751
|
break;
|
|
1751
1752
|
case "thread.deleted":
|
|
1752
1753
|
routines?.forgetRoutineRequestReceiptsForThread(change.threadId);
|
|
1754
|
+
broadcast({ kind: "bot.queued", queues: publicBotQueuedMessages() });
|
|
1753
1755
|
break;
|
|
1754
1756
|
case "bot": {
|
|
1755
1757
|
const bot = store.bot(change.botId);
|
|
@@ -1893,6 +1895,7 @@ function broadcast(payload) {
|
|
|
1893
1895
|
}
|
|
1894
1896
|
}
|
|
1895
1897
|
}
|
|
1898
|
+
onSteeredQueueChange(() => broadcast({ kind: "bot.queued", queues: publicBotQueuedMessages() }));
|
|
1896
1899
|
// ── server-side event folding (upstream's ingestion worker, miniature) ──
|
|
1897
1900
|
// The canonical stream is the source of truth; the persisted transcript
|
|
1898
1901
|
// and every client view are projections of it.
|
|
@@ -4738,6 +4741,13 @@ const agentRoutine = (routine, latestRun) => {
|
|
|
4738
4741
|
type: "interval",
|
|
4739
4742
|
everyMinutes: routine.schedule.everyMinutes,
|
|
4740
4743
|
anchorAt: new Date(routine.schedule.anchorAt).toISOString(),
|
|
4744
|
+
...(routine.schedule.weekdays === undefined
|
|
4745
|
+
? {}
|
|
4746
|
+
: { weekdays: routine.schedule.weekdays.map((day) => ROUTINE_WEEKDAY_NAMES[day]) }),
|
|
4747
|
+
...(routine.schedule.window === undefined ? {} : { window: { ...routine.schedule.window } }),
|
|
4748
|
+
...(routine.schedule.endsAt === undefined
|
|
4749
|
+
? {}
|
|
4750
|
+
: { endsAt: new Date(routine.schedule.endsAt).toISOString() }),
|
|
4741
4751
|
}
|
|
4742
4752
|
: {
|
|
4743
4753
|
type: "weekly",
|
|
@@ -6790,6 +6800,7 @@ function cliProbeEnvironment() {
|
|
|
6790
6800
|
"OMB_COMPOSIO_BROKER_TOKEN",
|
|
6791
6801
|
"OMB_TTS_KEY",
|
|
6792
6802
|
"OMB_OPENAI_IMAGE_KEY",
|
|
6803
|
+
"OMB_CUSTOM_IMAGE_KEY",
|
|
6793
6804
|
"ANTHROPIC_API_KEY",
|
|
6794
6805
|
"OPENAI_API_KEY",
|
|
6795
6806
|
]) {
|
|
@@ -6902,7 +6913,7 @@ function configStatus() {
|
|
|
6902
6913
|
// the chosen voice is a setting, not a secret; the key is reported the
|
|
6903
6914
|
// same configured-or-not way as every other credential
|
|
6904
6915
|
tts: tts.describeVoice(cfg),
|
|
6905
|
-
imageGen:
|
|
6916
|
+
imageGen: avatarImageStatus(cfg),
|
|
6906
6917
|
// not a secret — the sidebar shows it
|
|
6907
6918
|
profile: { name: cfg.profile?.name ?? "", email: cfg.profile?.email ?? "" },
|
|
6908
6919
|
// not a secret — the settings picker shows it; "" = follow the system
|
|
@@ -8732,6 +8743,7 @@ const handleRequest = async (req, res) => {
|
|
|
8732
8743
|
return json(res, 400, { error: "messages must be a non-negative whole number" });
|
|
8733
8744
|
return json(res, 200, {
|
|
8734
8745
|
bots: store.bots.map((bot) => ({ ...publicBot(bot), ...messagePage(bot.threadId, limit) })),
|
|
8746
|
+
botQueuedMessages: publicBotQueuedMessages(),
|
|
8735
8747
|
groups: store.groups.map((g) => ({ ...publicGroupState(g), ...messagePage(g.threadId, limit) })),
|
|
8736
8748
|
computerControl: Object.fromEntries(store.bots.map((bot) => {
|
|
8737
8749
|
const snapshot = computerControl.snapshot(bot.id);
|
|
@@ -10024,7 +10036,7 @@ const handleRequest = async (req, res) => {
|
|
|
10024
10036
|
if (!parsed.success) {
|
|
10025
10037
|
return json(res, 400, { error: `prompt must be at most 400 characters` });
|
|
10026
10038
|
}
|
|
10027
|
-
const generated = await generateAvatarImage(cfg
|
|
10039
|
+
const generated = await generateAvatarImage(cfg, existing, parsed.data.prompt);
|
|
10028
10040
|
const current = store.bot(existing.id);
|
|
10029
10041
|
if (!current)
|
|
10030
10042
|
return json(res, 404, { error: "no such bot" });
|
|
@@ -12005,7 +12017,7 @@ const handleRequest = async (req, res) => {
|
|
|
12005
12017
|
const state = await providerAuthSessions.status(authStatus[1], auth.kind === "session" ? auth.session.id : "loopback", url.searchParams.get("flowId") ?? "");
|
|
12006
12018
|
return json(res, 200, { auth: state });
|
|
12007
12019
|
}
|
|
12008
|
-
const instanceAction = /^\/api\/instances\/([\w.-]+)\/(refresh-models|install|auth\/start|auth\/complete|auth\/cancel)$/.exec(path);
|
|
12020
|
+
const instanceAction = /^\/api\/instances\/([\w.-]+)\/(refresh-models|install|auth\/start|auth\/complete|auth\/cancel|auth\/sign-out)$/.exec(path);
|
|
12009
12021
|
if (method === "POST" && instanceAction) {
|
|
12010
12022
|
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
12011
12023
|
return json(res, 415, { error: "content-type must be application/json" });
|
|
@@ -12038,6 +12050,13 @@ const handleRequest = async (req, res) => {
|
|
|
12038
12050
|
}
|
|
12039
12051
|
return json(res, 200, { auth: started });
|
|
12040
12052
|
}
|
|
12053
|
+
if (action === "auth/sign-out") {
|
|
12054
|
+
const instance = registry.get(instanceId);
|
|
12055
|
+
if (!instance)
|
|
12056
|
+
return json(res, 404, { error: "unknown instance" });
|
|
12057
|
+
await providerAuthSessions.signOut(instance, owner);
|
|
12058
|
+
return json(res, 200, { instances: await describeInstances() });
|
|
12059
|
+
}
|
|
12041
12060
|
if (action === "auth/complete") {
|
|
12042
12061
|
const body = await readBody(req);
|
|
12043
12062
|
const flowId = typeof body?.flowId === "string" ? body.flowId : "";
|
|
@@ -12613,6 +12632,8 @@ const handleRequest = async (req, res) => {
|
|
|
12613
12632
|
persisted.tts.key = "";
|
|
12614
12633
|
if (persisted.imageGen?.key !== undefined)
|
|
12615
12634
|
persisted.imageGen.key = "";
|
|
12635
|
+
if (persisted.imageGen?.customApiKey !== undefined)
|
|
12636
|
+
persisted.imageGen.customApiKey = "";
|
|
12616
12637
|
saveConfig(persisted);
|
|
12617
12638
|
configWriteCommitted = true;
|
|
12618
12639
|
syncCredentialEnv(patch);
|
|
@@ -100,6 +100,13 @@ export function createBotPackageExport(input) {
|
|
|
100
100
|
type: "interval",
|
|
101
101
|
everyMinutes: routine.schedule.everyMinutes,
|
|
102
102
|
anchorAt: routine.schedule.anchorAt,
|
|
103
|
+
...(routine.schedule.weekdays === undefined
|
|
104
|
+
? {}
|
|
105
|
+
: { weekdays: [...routine.schedule.weekdays] }),
|
|
106
|
+
...(routine.schedule.window === undefined
|
|
107
|
+
? {}
|
|
108
|
+
: { window: { ...routine.schedule.window } }),
|
|
109
|
+
...(routine.schedule.endsAt === undefined ? {} : { endsAt: routine.schedule.endsAt }),
|
|
103
110
|
}
|
|
104
111
|
: { type: "daily", time: routine.schedule.time, weekdays: [...routine.schedule.weekdays] },
|
|
105
112
|
durationMinutes: routine.durationMinutes,
|
|
@@ -86,6 +86,30 @@ export class ProviderAuthSessions {
|
|
|
86
86
|
flow.expiresAt = 0;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
/** Remove the server's stored sign-in for this provider. A login another
|
|
90
|
+
* admin is still completing must not be pulled away underneath them, and
|
|
91
|
+
* nobody may start one while the credential is being removed. */
|
|
92
|
+
async signOut(instance, owner) {
|
|
93
|
+
if (!instance.signOut)
|
|
94
|
+
throw failure("Sign-out is unavailable for this provider.", 404);
|
|
95
|
+
const existing = this.flows.get(instance.instanceId);
|
|
96
|
+
if (existing && (existing.busy || (existing.owner !== owner && existing.expiresAt > Date.now() && !existing.revoked))) {
|
|
97
|
+
throw failure("A sign-in is in progress. Finish or cancel it in the browser that started it, or wait for it to expire.", 409);
|
|
98
|
+
}
|
|
99
|
+
// Reserve the slot like a starting flow: start() answers 409 until we finish.
|
|
100
|
+
const flow = { instance, owner, flowId: null, expiresAt: Date.now() + 60_000, busy: true, starting: false, revoked: false };
|
|
101
|
+
this.flows.set(instance.instanceId, flow);
|
|
102
|
+
try {
|
|
103
|
+
// This owner's own leftover flow is theirs to abandon.
|
|
104
|
+
if (existing)
|
|
105
|
+
await existing.instance.cancelAuthentication?.();
|
|
106
|
+
await instance.signOut();
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
if (this.flows.get(instance.instanceId) === flow)
|
|
110
|
+
this.flows.delete(instance.instanceId);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
89
113
|
revokeOwner(owner) {
|
|
90
114
|
for (const [id, flow] of this.flows) {
|
|
91
115
|
if (flow.owner !== owner)
|