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
|
@@ -36,12 +36,16 @@
|
|
|
36
36
|
import readline from "node:readline";
|
|
37
37
|
import { CREDENTIAL_TARGETS, isCredentialTargetId } from "../../shared/credential-request.js";
|
|
38
38
|
import { agentToolAnnotations } from "../agent-tool-policy.js";
|
|
39
|
+
import { peerName } from "../peer-roster.js";
|
|
39
40
|
const HARNESS = process.env.OMB_HARNESS_URL ?? "http://127.0.0.1:8799";
|
|
40
41
|
const BOT_ID = process.env.OMB_BOT_ID ?? "";
|
|
41
42
|
const THREAD_ID = process.env.OMB_THREAD_ID ?? "";
|
|
42
43
|
const TOKEN = process.env.OMB_COMMS_TOKEN ?? "";
|
|
43
44
|
const DEPTH = Number(process.env.OMB_TURN_DEPTH ?? "0") || 0;
|
|
44
45
|
const SKILL_AUTHORING_ENABLED = process.env.OMB_SKILL_AUTHORING_ENABLED === "1";
|
|
46
|
+
// Opt-in computer sharing (server features.sharedComputers). Off unless the
|
|
47
|
+
// harness says "1", the same way skill authoring is gated above.
|
|
48
|
+
const SHARED_COMPUTERS_ENABLED = process.env.OMB_SHARED_COMPUTERS_ENABLED === "1";
|
|
45
49
|
const MAX_CREATED_PER_TURN = 4;
|
|
46
50
|
let createdThisTurn = 0;
|
|
47
51
|
// Same spirit as MAX_CREATED_PER_TURN above and MAX_QUEUED_PER_THREAD in
|
|
@@ -342,14 +346,44 @@ const ROUTINE_FIELDS_SCHEMA = {
|
|
|
342
346
|
},
|
|
343
347
|
};
|
|
344
348
|
const TOOLS = [
|
|
349
|
+
{
|
|
350
|
+
name: "list_shared_computers",
|
|
351
|
+
description: "List online desktop computers explicitly shared with this workspace, and their allowed folders/capabilities. These are the user's computers, not this server. An offline or unshared computer cannot be accessed. Folder paths use opaque folder IDs and relative paths.",
|
|
352
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
name: "shared_computer",
|
|
356
|
+
description: "Use a desktop explicitly shared by the user. Discover computer_id and folder_id with list_shared_computers. list_files/read_file/write_file are confined to chosen folders; paths are relative. read_file returns sha256; overwriting requires expected_sha256. Binary files support base64 encoding. run_command requires a SEPARATE unrestricted terminal grant. computer_tools lists the native computer-control tools; computer_call invokes one with arguments and needs a SEPARATE computer-control grant. Never substitute the server's filesystem when this desktop is offline. Actions are not retried automatically; inspect an uncertain outcome before retrying.",
|
|
357
|
+
inputSchema: { type: "object", additionalProperties: false, properties: {
|
|
358
|
+
computer_id: { type: "string" }, action: { type: "string", enum: ["list_files", "read_file", "write_file", "run_command", "computer_tools", "computer_call"] },
|
|
359
|
+
folder_id: { type: "string" }, path: { type: "string" }, content: { type: "string" }, encoding: { type: "string", enum: ["utf8", "base64"] }, expected_sha256: { type: "string" },
|
|
360
|
+
command: { type: "string" }, tool_name: { type: "string" }, arguments: { type: "object", additionalProperties: true },
|
|
361
|
+
}, required: ["computer_id", "action"] },
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: "list_room_targets",
|
|
365
|
+
description: "Discover actual OpenMausBot teammates and rooms in your allowed teams. Works in a normal bot conversation too; no room is required. Returns bot and room IDs, roles and working folders, never other conversations' history. Use these bots, not native coding helpers with similar names, when the user asks their team to work together.",
|
|
366
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
name: "coordinate_bots",
|
|
370
|
+
description: "Ask existing OpenMausBot teammates for advice or assign concrete work. From normal chat each assignment gets a separate recipient conversation; from a room it defaults to this room. Use group_id from list_room_targets for a specific room. Name 1-4 bot_ids: they receive only your brief and use their own model, tools and permissions. Busy bots queue. They can consult their specialists; all results return here and resume you automatically. Include exact file paths, constraints and what must be verified. After sending all assignments, END your turn; do not poll or wait. On return, resolve tradeoffs, verify the requested outcome and request concrete corrections if necessary before giving one final answer. Do not send acknowledgements as new work.",
|
|
371
|
+
inputSchema: { type: "object", additionalProperties: false, properties: {
|
|
372
|
+
group_id: { type: "string", description: "Optional destination room. Omit for this room, or separate recipient tasks when chatting directly." },
|
|
373
|
+
bot_ids: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 4, uniqueItems: true },
|
|
374
|
+
message: { type: "string", minLength: 1, maxLength: 4000, description: "Self-contained question or task for these teammates. Send separate requests when responsibilities differ." },
|
|
375
|
+
request_key: { type: "string", description: "A short unique assignment key. Reuse for an identical retry." },
|
|
376
|
+
rework: { type: "boolean", description: "True only for concrete additional work from someone who already completed a request." },
|
|
377
|
+
}, required: ["bot_ids", "message", "request_key"] },
|
|
378
|
+
},
|
|
345
379
|
{
|
|
346
380
|
name: "list_bots",
|
|
347
|
-
description: "List the other bots (agents) in your
|
|
381
|
+
description: "List the other bots (agents) you may contact in your own team and any additional teams the owner has explicitly allowed you to coordinate, with their team, model and current status. Call this to discover exact teammate IDs before assigning work or requesting advice through your available coordination tools.",
|
|
348
382
|
inputSchema: { type: "object", properties: {} },
|
|
349
383
|
},
|
|
350
384
|
{
|
|
351
385
|
name: "list_rooms",
|
|
352
|
-
description: "List the shared rooms (team channels) you belong to, with the other members of each. Call this before post_to_room
|
|
386
|
+
description: "List the shared rooms (team channels) you belong to, with the other members of each. Call this before post_to_room. One-to-one bot channels are never listed; discover individual teammates with list_bots. A room you are in but cannot post into is named without an id, together with the reason, so you can tell the user why.",
|
|
353
387
|
inputSchema: { type: "object", additionalProperties: false, properties: {} },
|
|
354
388
|
},
|
|
355
389
|
{
|
|
@@ -402,12 +436,12 @@ const TOOLS = [
|
|
|
402
436
|
},
|
|
403
437
|
{
|
|
404
438
|
name: "list_threads",
|
|
405
|
-
description: "See your own threads and the threads you opened on teammates, newest first: each with its bot, title, state (running, waiting on the person, queued, or
|
|
439
|
+
description: "See your own threads and the threads you opened on teammates, newest first: each with its bot, title, state (running, waiting on the person, queued, idle, or closed), whether the person has unread there, and the delegation id if it was a handoff. Use it to check how the threads you started are going before reporting to the person; write a thread's title as #Title when you mention it. A teammate's other threads are never listed — only the ones you opened. This is a read: it starts nothing and changes nothing.",
|
|
406
440
|
inputSchema: { type: "object", additionalProperties: false, properties: {} },
|
|
407
441
|
},
|
|
408
442
|
{
|
|
409
443
|
name: "close_thread",
|
|
410
|
-
description: "Mark a thread you opened (or one of your own) as finished once you have read its result: it
|
|
444
|
+
description: "Mark a thread you opened (or one of your own) as finished once you have read its result: it leaves the person's default sidebar list (still under all threads, with a note saying you closed it) and list_threads reports it as closed. Nothing is deleted — deleting stays the person's decision — and a thread that is still running cannot be closed; wait for it or leave it. Use the thread id from list_threads or from the start_thread result. If a close is refused, do not retry it.",
|
|
411
445
|
inputSchema: {
|
|
412
446
|
type: "object",
|
|
413
447
|
additionalProperties: false,
|
|
@@ -456,6 +490,45 @@ const TOOLS = [
|
|
|
456
490
|
required: ["name", "role", "instructions"],
|
|
457
491
|
},
|
|
458
492
|
},
|
|
493
|
+
{
|
|
494
|
+
name: "list_team_setup",
|
|
495
|
+
description: "Chief of Staff only: list authorized teams, teammate IDs, and exact engine/model choices for team setup. Call before proposing configuration; never invent model IDs. Existing thread models are independent of bot defaults.",
|
|
496
|
+
inputSchema: { type: "object", additionalProperties: false, properties: {} },
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
name: "propose_team_setup",
|
|
500
|
+
description: "Chief of Staff only: propose all requested specialist creation, profile/model configuration, and authorized team moves in ONE combined review card. Nothing changes until the user applies it. Use exact catalog engine/model IDs from list_team_setup. Combine all fields for each bot; use the same create key or botId to coalesce repeated entries. New teams must be named explicitly in newTeams and have a specialist in this plan; the card also asks to authorize your access to just those new teams. Existing unauthorized teams cannot be included. Models change bot defaults for groups/new threads; existing threads and execution permissions stay unchanged. After proposing, end your turn. The decision and structured result automatically resume you once; do not ask again, poll, or repeat the proposal.",
|
|
501
|
+
inputSchema: {
|
|
502
|
+
type: "object", additionalProperties: false,
|
|
503
|
+
properties: {
|
|
504
|
+
reason: { type: "string", minLength: 1, maxLength: 500 },
|
|
505
|
+
newTeams: { type: "array", maxItems: 8, items: { type: "string", minLength: 1, maxLength: 60 } },
|
|
506
|
+
operations: { type: "array", minItems: 1, maxItems: 24, items: {
|
|
507
|
+
type: "object", additionalProperties: false,
|
|
508
|
+
properties: {
|
|
509
|
+
action: { type: "string", enum: ["create", "update"] },
|
|
510
|
+
key: { type: "string", description: "For create: your stable short name for this new bot in this plan." },
|
|
511
|
+
botId: { type: "string", description: "For update: exact existing bot ID from list_team_setup." },
|
|
512
|
+
fields: { type: "object", additionalProperties: false, properties: {
|
|
513
|
+
name: { type: "string", maxLength: 100 }, title: { type: "string", maxLength: 200 },
|
|
514
|
+
description: { type: "string", maxLength: 4000 }, soul: { type: "string", description: "Standing instructions; required with name/title/modelSelection for every new bot." },
|
|
515
|
+
section: { type: "string", maxLength: 60, description: "Exact authorized existing team, or a team explicitly named in newTeams. Empty string means General." },
|
|
516
|
+
modelSelection: { type: "object", additionalProperties: false, properties: {
|
|
517
|
+
instanceId: { type: "string" }, model: { type: "string" }, effort: { type: "string" },
|
|
518
|
+
}, required: ["instanceId", "model"] },
|
|
519
|
+
} },
|
|
520
|
+
}, required: ["action", "fields"],
|
|
521
|
+
} },
|
|
522
|
+
}, required: ["reason", "operations"],
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
name: "propose_bot_deletion",
|
|
527
|
+
description: "Chief of Staff only: when the user explicitly asks to delete a named teammate, create a separate confirmation card for that exact bot. Deletion removes its conversations, memory, instructions and skills; generated project files remain. Running work and owned computers can block deletion. Never delete yourself, substitute an archive, or put deletion into a setup batch. End your turn after proposing; the decision and result resume you once.",
|
|
528
|
+
inputSchema: { type: "object", additionalProperties: false, properties: {
|
|
529
|
+
bot_id: { type: "string", minLength: 1 }, reason: { type: "string", minLength: 1, maxLength: 500 },
|
|
530
|
+
}, required: ["bot_id", "reason"] },
|
|
531
|
+
},
|
|
459
532
|
{
|
|
460
533
|
name: "create_room",
|
|
461
534
|
description: "Create a room in your own section when the user asks for one (maximum four per turn). Chiefs only. Choose active peers from list_bots; you are included automatically as the default responder. This creates no turns or messages. Section moves stay with the user. If peer approval is enabled, ask the user to make the room change instead.",
|
|
@@ -694,9 +767,33 @@ const TOOLS = [
|
|
|
694
767
|
return annotations ? { ...tool, annotations } : tool;
|
|
695
768
|
});
|
|
696
769
|
const SKILL_TOOL_NAMES = new Set(["skills_list", "skill_manage"]);
|
|
697
|
-
const
|
|
770
|
+
const AUTHORING_TOOLS = SKILL_AUTHORING_ENABLED
|
|
698
771
|
? TOOLS
|
|
699
772
|
: TOOLS.filter((tool) => !SKILL_TOOL_NAMES.has(tool.name));
|
|
773
|
+
// A workspace with computer sharing off refuses the routes behind these two,
|
|
774
|
+
// so they must not be advertised at all: a model that sees a tool it cannot
|
|
775
|
+
// use spends turns discovering that.
|
|
776
|
+
const SHARED_COMPUTER_TOOL_NAMES = new Set(["list_shared_computers", "shared_computer"]);
|
|
777
|
+
const SHAREABLE_TOOLS = SHARED_COMPUTERS_ENABLED
|
|
778
|
+
? AUTHORING_TOOLS
|
|
779
|
+
: AUTHORING_TOOLS.filter((tool) => !SHARED_COMPUTER_TOOL_NAMES.has(tool.name));
|
|
780
|
+
// One teamwork path in room turns; keep all unrelated integrations available.
|
|
781
|
+
// Ordinary direct chats use this same bounded coordinator. Goal-owned turns
|
|
782
|
+
// retain their independent loop and cannot start a second coordinator.
|
|
783
|
+
const ROOM_ONLY_TOOLS = new Set(["list_room_targets", "coordinate_bots"]);
|
|
784
|
+
const ROOM_REPLACED_TOOLS = new Set(["ask_bot", "delegate_bot", "check_delegation", "wait_delegation", "start_thread", "send_to_thread", "wait_thread"]);
|
|
785
|
+
const COORDINATING = process.env.OMB_ROOM_TURN === "1";
|
|
786
|
+
const OWN_THREAD_CREATION = process.env.OMB_OWN_THREAD_CREATION === "1";
|
|
787
|
+
const AVAILABLE_TOOLS = COORDINATING
|
|
788
|
+
? SHAREABLE_TOOLS.filter(tool => !ROOM_REPLACED_TOOLS.has(tool.name) || (tool.name === "start_thread" && OWN_THREAD_CREATION))
|
|
789
|
+
.map(tool => tool.name === "start_thread" ? {
|
|
790
|
+
...tool,
|
|
791
|
+
description: "Open a separate job on yourself with its own history and run, without switching the person's selected conversation. Use only when the user requests independent jobs (for example one review per pull request). Give a short specific title and complete instructions; you can open at most five per turn. This is not a teammate handoff: use coordinate_bots for teammates and their automatic replies. Self-opened jobs cannot recursively open more jobs. If refused, do not retry; explain what remains.",
|
|
792
|
+
inputSchema: { ...tool.inputSchema, properties: { ...tool.inputSchema.properties,
|
|
793
|
+
bot_id: { type: "string", enum: [BOT_ID], description: "Leave out, or use your own bot ID. For teammates use coordinate_bots." },
|
|
794
|
+
} },
|
|
795
|
+
} : tool)
|
|
796
|
+
: SHAREABLE_TOOLS.filter(tool => !ROOM_ONLY_TOOLS.has(tool.name));
|
|
700
797
|
const send = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
|
|
701
798
|
const ok = (id, result) => send({ jsonrpc: "2.0", id, result });
|
|
702
799
|
const rpcErr = (id, code, message) => send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
@@ -799,18 +896,35 @@ function recallSpeaker(hit) {
|
|
|
799
896
|
return hit.role === "user" ? "user" : "you";
|
|
800
897
|
}
|
|
801
898
|
async function callTool(name, args) {
|
|
899
|
+
if (name === "list_room_targets") {
|
|
900
|
+
const r = await api("/api/internal/room-targets");
|
|
901
|
+
return { text: JSON.stringify(r), ...(r.error ? { isError: true } : {}) };
|
|
902
|
+
}
|
|
903
|
+
if (name === "coordinate_bots") {
|
|
904
|
+
const r = await api("/api/internal/coordinate-bots", { method: "POST", body: JSON.stringify({
|
|
905
|
+
groupId: args.group_id, botIds: args.bot_ids, message: args.message,
|
|
906
|
+
requestKey: args.request_key, rework: args.rework,
|
|
907
|
+
}) });
|
|
908
|
+
return { text: JSON.stringify(r), ...(r.error ? { isError: true } : {}) };
|
|
909
|
+
}
|
|
802
910
|
if (name === "list_bots") {
|
|
803
911
|
const r = await api(`/api/internal/agents?self=${encodeURIComponent(BOT_ID)}`);
|
|
804
912
|
const bots = r.bots ?? [];
|
|
805
913
|
if (!bots.length)
|
|
806
|
-
return { text: "No other bots
|
|
914
|
+
return { text: "No other reachable bots yet." };
|
|
807
915
|
const lines = bots.map((b) => {
|
|
808
916
|
const role = b.title ? ` — ${b.title}` : "";
|
|
809
917
|
const about = b.description ? ` (${String(b.description).slice(0, 120)})` : "";
|
|
810
|
-
|
|
918
|
+
// statusText is the server's own wording for what the teammate is
|
|
919
|
+
// doing; an older server only sends busy, so fall back to that.
|
|
920
|
+
const state = typeof b.statusText === "string"
|
|
921
|
+
? (b.status === "available" ? "" : b.statusText)
|
|
922
|
+
: (b.busy ? "busy" : "");
|
|
923
|
+
const team = typeof b.section === "string" ? `, team: ${peerName(b.section) || "General"}` : "";
|
|
924
|
+
return `- ${b.name}${role}${about} [id: ${b.id}, model: ${b.model}${team}${state ? `, ${state}` : ""}]`;
|
|
811
925
|
});
|
|
812
926
|
return {
|
|
813
|
-
text: `
|
|
927
|
+
text: `Reachable teammates:\n${lines.join("\n")}\n\n${COORDINATING ? "Use coordinate_bots for advice or concrete work, then end your turn. Busy teammates queue and results resume you automatically." : "Assign work with delegate_bot. Use ask_bot only for a short answer you need inline."}`,
|
|
814
928
|
};
|
|
815
929
|
}
|
|
816
930
|
if (name === "list_rooms") {
|
|
@@ -943,7 +1057,16 @@ async function callTool(name, args) {
|
|
|
943
1057
|
if (r.status === "done")
|
|
944
1058
|
return { text: `${who} finished task ${taskId}:\n${String(r.result || "(no reply text)")}` };
|
|
945
1059
|
if (r.status === "queued") {
|
|
946
|
-
|
|
1060
|
+
const why = r.targetStatus === "waiting-on-user"
|
|
1061
|
+
? ` ${who} is waiting on the user, so it goes through after they answer.`
|
|
1062
|
+
: r.targetStatus === "working" ? ` ${who} is busy with other work.` : "";
|
|
1063
|
+
const expiresInMs = Number(r.expiresInMs);
|
|
1064
|
+
const expiry = !Number.isFinite(expiresInMs)
|
|
1065
|
+
? ""
|
|
1066
|
+
: expiresInMs <= 0
|
|
1067
|
+
? " It is past its 24-hour limit and will expire the next time it cannot be delivered."
|
|
1068
|
+
: ` It expires if not picked up within ${Math.ceil(expiresInMs / 3_600_000)} hour${Math.ceil(expiresInMs / 3_600_000) === 1 ? "" : "s"}.`;
|
|
1069
|
+
return { text: `Task ${taskId} is still queued — ${who} hasn't picked it up yet${waitMs ? ` after ${timeout}s` : ""}.${why}${expiry} Keep working and check again later.` };
|
|
947
1070
|
}
|
|
948
1071
|
if (r.status === "running") {
|
|
949
1072
|
const elapsedMs = Number.isFinite(r.elapsedMs) ? Number(r.elapsedMs) : 0;
|
|
@@ -998,6 +1121,9 @@ async function callTool(name, args) {
|
|
|
998
1121
|
};
|
|
999
1122
|
}
|
|
1000
1123
|
const toBotId = typeof args.bot_id === "string" ? args.bot_id.trim() : "";
|
|
1124
|
+
if (COORDINATING && toBotId && toBotId !== BOT_ID) {
|
|
1125
|
+
return { text: "Use coordinate_bots for teammates; start_thread only opens a separate job on yourself.", isError: true };
|
|
1126
|
+
}
|
|
1001
1127
|
const folder = typeof args.folder === "string" ? args.folder.trim() : "";
|
|
1002
1128
|
const body = { fromBotId: BOT_ID, fromThreadId: THREAD_ID, title, message, depth: DEPTH };
|
|
1003
1129
|
if (toBotId)
|
|
@@ -1041,6 +1167,18 @@ async function callTool(name, args) {
|
|
|
1041
1167
|
text: `${opened}${timing}${approval} Its result will be delivered to this conversation automatically (delegation id: ${delegationId || "unknown"}). Acknowledge it, mention it to the person as #${threadTitle}, and finish your turn; do not check or wait for it in this turn.`,
|
|
1042
1168
|
};
|
|
1043
1169
|
}
|
|
1170
|
+
if (name === "list_team_setup") {
|
|
1171
|
+
return { text: JSON.stringify(await api("/api/internal/team-setup-catalog")) };
|
|
1172
|
+
}
|
|
1173
|
+
if (name === "propose_team_setup" || name === "propose_bot_deletion") {
|
|
1174
|
+
const deleting = name === "propose_bot_deletion";
|
|
1175
|
+
const result = await api(deleting ? "/api/internal/bot-deletion-requests" : "/api/internal/team-setup-requests", {
|
|
1176
|
+
method: "POST", body: JSON.stringify({ fromBotId: BOT_ID, fromThreadId: THREAD_ID,
|
|
1177
|
+
...(deleting ? { targetBotId: args.bot_id, reason: args.reason } : { plan: args }),
|
|
1178
|
+
}),
|
|
1179
|
+
});
|
|
1180
|
+
return { text: `One review card is visible: ${String(result.title)}. Nothing has been applied. End this turn; the decision and structured result resume you automatically once. Do not ask again, poll, or repeat this proposal.` };
|
|
1181
|
+
}
|
|
1044
1182
|
if (name === "create_bot") {
|
|
1045
1183
|
const botName = String(args.name ?? "").trim();
|
|
1046
1184
|
const role = String(args.role ?? "").trim();
|
|
@@ -1063,7 +1201,7 @@ async function callTool(name, args) {
|
|
|
1063
1201
|
});
|
|
1064
1202
|
createdThisTurn += 1;
|
|
1065
1203
|
return {
|
|
1066
|
-
text: `Created @${r.name ?? botName} in ${r.section ?? "General"} [id: ${r.id}]. Assign work with delegate_bot.`,
|
|
1204
|
+
text: `Created @${r.name ?? botName} in ${r.section ?? "General"} [id: ${r.id}]. Assign work with ${COORDINATING ? "coordinate_bots" : "delegate_bot"}.`,
|
|
1067
1205
|
};
|
|
1068
1206
|
}
|
|
1069
1207
|
if (name === "create_room") {
|
|
@@ -1447,6 +1585,27 @@ async function handle(msg) {
|
|
|
1447
1585
|
if (!AVAILABLE_TOOLS.some((t) => t.name === name))
|
|
1448
1586
|
return rpcErr(id, -32602, `Unknown tool: ${name}`);
|
|
1449
1587
|
try {
|
|
1588
|
+
// Second lock. With sharing off the tool is not in AVAILABLE_TOOLS, so
|
|
1589
|
+
// a call is already refused above as an unknown tool — the same answer
|
|
1590
|
+
// a build without the feature gives. This keeps the handler itself
|
|
1591
|
+
// refusing if that list is ever assembled differently.
|
|
1592
|
+
if (SHARED_COMPUTER_TOOL_NAMES.has(name) && !SHARED_COMPUTERS_ENABLED) {
|
|
1593
|
+
textResult(id, "Computer sharing is turned off in this workspace. There are no shared computers to use.", true);
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
if (name === "list_shared_computers") {
|
|
1597
|
+
textResult(id, JSON.stringify(await api("/api/internal/shared-computers")));
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
if (name === "shared_computer") {
|
|
1601
|
+
const response = await api("/api/internal/shared-computers", { method: "POST", body: JSON.stringify(params.arguments ?? {}) });
|
|
1602
|
+
const result = response.result;
|
|
1603
|
+
if (Array.isArray(result?.content))
|
|
1604
|
+
ok(id, result);
|
|
1605
|
+
else
|
|
1606
|
+
textResult(id, JSON.stringify(result));
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1450
1609
|
const { text, isError } = await callTool(name, (params.arguments ?? {}));
|
|
1451
1610
|
textResult(id, text, isError);
|
|
1452
1611
|
}
|
|
@@ -64,7 +64,7 @@ export const BoxAgentDriver = {
|
|
|
64
64
|
const model = turn.model || MODELS.default;
|
|
65
65
|
const prompt = [
|
|
66
66
|
turn.system,
|
|
67
|
-
"You are working on
|
|
67
|
+
"You are working on the assigned cloud computer — use its desktop, Chrome, and shell within the access described above.",
|
|
68
68
|
"",
|
|
69
69
|
turn.text,
|
|
70
70
|
]
|
|
@@ -21,7 +21,7 @@ import { ClaudeLoginController } from "./claude-login-auth.js";
|
|
|
21
21
|
import { computerProxyEnv } from "../container-computer.js";
|
|
22
22
|
import { gateServer, resultBudget } from "../mcp-gate-config.js";
|
|
23
23
|
import { newEventId, newId } from "../contracts.js";
|
|
24
|
-
import { askInputSummary, commandSummary } from "../tool-summary.js";
|
|
24
|
+
import { askInputSummary, commandSummary, toolDetailPreview } from "../tool-summary.js";
|
|
25
25
|
import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS } from "./retry.js";
|
|
26
26
|
import { applyClaudeInject, decodeInjectId, mergeLocalInject, probeLocalInjects, resolveInjectId, } from "./local-inject.js";
|
|
27
27
|
import { appendNative } from "./native.js";
|
|
@@ -408,7 +408,7 @@ function askSummary(ask) {
|
|
|
408
408
|
return askQuestionSummary(questions).slice(0, 300);
|
|
409
409
|
return askInputSummary(ask.input) ?? ask.tool ?? "tool";
|
|
410
410
|
}
|
|
411
|
-
export function permissionSocketPath(threadId) {
|
|
411
|
+
export function permissionSocketPath(threadId, botId) {
|
|
412
412
|
// A readable prefix alone is not unique: ids that agree on their first
|
|
413
413
|
// characters ("t-perm-dup-1", "t-perm-dup-2") would share a socket. POSIX
|
|
414
414
|
// hides that — a new broker's listen replaces the socket FILE, so the name
|
|
@@ -418,8 +418,16 @@ export function permissionSocketPath(threadId) {
|
|
|
418
418
|
// id so distinct threads get distinct sockets; the tag stays at 8 chars
|
|
419
419
|
// total because the POSIX path already brushes the 104-byte sun_path
|
|
420
420
|
// limit under deep tmp home dirs.
|
|
421
|
+
//
|
|
422
|
+
// botId is folded into the digest too (#1017): the driver's session/broker
|
|
423
|
+
// maps are a single process-wide table keyed on threadId alone, so a
|
|
424
|
+
// delegated child turn whose threadId ever coincides with its still-open
|
|
425
|
+
// parent's (or any other bot's) would otherwise collide on the exact same
|
|
426
|
+
// socket. Namespacing by bot makes that collision structurally impossible
|
|
427
|
+
// regardless of how two turns end up sharing a threadId.
|
|
428
|
+
const key = botId ? `${botId}\0${threadId}` : threadId;
|
|
421
429
|
const prefix = threadId.replace(/[^\w-]/g, "").slice(0, 4);
|
|
422
|
-
const digest = createHash("sha256").update(
|
|
430
|
+
const digest = createHash("sha256").update(key).digest("hex").slice(0, 4);
|
|
423
431
|
return brokerSocketPath(DATA_DIR, `${prefix}${digest}`);
|
|
424
432
|
}
|
|
425
433
|
/** Paths the broker may bind, tried in order. Windows named pipes are never
|
|
@@ -429,11 +437,11 @@ export function permissionSocketPath(threadId) {
|
|
|
429
437
|
* longer than its small `sun_path` limit; a deep test HOME or long username
|
|
430
438
|
* can otherwise make every approval silently unavailable. The proxy learns
|
|
431
439
|
* the actual bound path from its argv, so either fallback is transparent. */
|
|
432
|
-
export function brokerSocketCandidates(threadId) {
|
|
433
|
-
const base = permissionSocketPath(threadId);
|
|
440
|
+
export function brokerSocketCandidates(threadId, botId) {
|
|
441
|
+
const base = permissionSocketPath(threadId, botId);
|
|
434
442
|
if (process.platform !== "win32") {
|
|
435
443
|
const scope = createHash("sha256")
|
|
436
|
-
.update(`${DATA_DIR}\0${process.pid}\0${threadId}`)
|
|
444
|
+
.update(`${DATA_DIR}\0${process.pid}\0${botId ?? ""}\0${threadId}`)
|
|
437
445
|
.digest("hex")
|
|
438
446
|
.slice(0, 16);
|
|
439
447
|
return [base, join(tmpdir(), `omb-perm-${scope}.sock`)];
|
|
@@ -866,8 +874,8 @@ export const ClaudeDriver = {
|
|
|
866
874
|
// retry bookkeeping lives PER THREAD, not per sendTurn call: a relaunch
|
|
867
875
|
// is a fresh sendTurn, and the attempt cap must survive across launches
|
|
868
876
|
const retryState = new Map();
|
|
869
|
-
const sendTurn = async (turn) => {
|
|
870
|
-
const { threadId } = turn;
|
|
877
|
+
const sendTurn = async (turn, logicalTurnId) => {
|
|
878
|
+
const { threadId, botId } = turn;
|
|
871
879
|
if (active.has(threadId))
|
|
872
880
|
throw new Error("a turn is already running on this thread");
|
|
873
881
|
// A bot-level mode is authoritative for this turn. In particular, an
|
|
@@ -887,7 +895,10 @@ export const ClaudeDriver = {
|
|
|
887
895
|
// Materialize before creating a broker or process. A missing/corrupt
|
|
888
896
|
// attachment must fail this call without leaving a live session behind.
|
|
889
897
|
const promptMsg = claudeUserMessage(turn.text, turn.images);
|
|
890
|
-
|
|
898
|
+
// Internal relaunches are still the turn acknowledged to the harness.
|
|
899
|
+
// A new user message gets a fresh id, but retry/recovery must not orphan
|
|
900
|
+
// its capability, coordination result or queued continuation ownership.
|
|
901
|
+
const turnId = logicalTurnId ?? newId();
|
|
891
902
|
const retryAbort = new AbortController();
|
|
892
903
|
const retry = retryState.get(threadId) ?? { attempt: 0, cancelled: false };
|
|
893
904
|
retry.cancelled = false;
|
|
@@ -1046,7 +1057,7 @@ export const ClaudeDriver = {
|
|
|
1046
1057
|
// Keep ask_user available even in Full access. Native bypass skips
|
|
1047
1058
|
// permission prompts, not questions requiring a person's answer.
|
|
1048
1059
|
let broker;
|
|
1049
|
-
const socketPath = permissionSocketPath(threadId);
|
|
1060
|
+
const socketPath = permissionSocketPath(threadId, botId);
|
|
1050
1061
|
if (permissionMode !== "bypassPermissions") {
|
|
1051
1062
|
args.push("--permission-prompt-tool", "mcp__ogb__approve");
|
|
1052
1063
|
}
|
|
@@ -1103,9 +1114,11 @@ export const ClaudeDriver = {
|
|
|
1103
1114
|
if (live && !live.turn && !live.closing && live.child.exitCode === null && live.argsKey === argsKey && (!sessionId || sessionId === live.sessionId)) {
|
|
1104
1115
|
if (live.idleTimer)
|
|
1105
1116
|
clearTimeout(live.idleTimer);
|
|
1106
|
-
live.turn = { turnId, settled: false, sawStreamDelta: false };
|
|
1117
|
+
live.turn = { turnId, input: turn, retryAbort, settled: false, sawStreamDelta: false };
|
|
1107
1118
|
active.set(threadId, { stop: () => {
|
|
1108
1119
|
closeSession(threadId, "interrupted");
|
|
1120
|
+
retry.cancelled = true;
|
|
1121
|
+
retryAbort.abort();
|
|
1109
1122
|
stopSession(live);
|
|
1110
1123
|
}, turnId, broker: live.broker });
|
|
1111
1124
|
emit({ ...base(threadId, turnId), type: "turn.started" });
|
|
@@ -1173,7 +1186,7 @@ export const ClaudeDriver = {
|
|
|
1173
1186
|
// event can scope approvals to real desktop-control tools only
|
|
1174
1187
|
const askTools = new Map();
|
|
1175
1188
|
broker = await createPermissionBroker({
|
|
1176
|
-
socketPaths: brokerSocketCandidates(threadId),
|
|
1189
|
+
socketPaths: brokerSocketCandidates(threadId, botId),
|
|
1177
1190
|
isActive: () => Boolean(sessions.get(threadId)?.turn),
|
|
1178
1191
|
onAsk: (ask) => {
|
|
1179
1192
|
const eventTurnId = sessions.get(threadId)?.turn?.turnId ?? turnId;
|
|
@@ -1268,7 +1281,7 @@ export const ClaudeDriver = {
|
|
|
1268
1281
|
sessionId: sessionId ?? newSessionId,
|
|
1269
1282
|
sawInit: false,
|
|
1270
1283
|
nativePermissionMode: null,
|
|
1271
|
-
turn: { turnId, settled: false, sawStreamDelta: false },
|
|
1284
|
+
turn: { turnId, input: turn, retryAbort, settled: false, sawStreamDelta: false },
|
|
1272
1285
|
idleTimer: null,
|
|
1273
1286
|
closing: false,
|
|
1274
1287
|
stderr: "",
|
|
@@ -1383,6 +1396,7 @@ export const ClaudeDriver = {
|
|
|
1383
1396
|
itemId: b.id,
|
|
1384
1397
|
title: b.name,
|
|
1385
1398
|
summary: commandSummary(b.input),
|
|
1399
|
+
input: toolDetailPreview(b.input),
|
|
1386
1400
|
});
|
|
1387
1401
|
}
|
|
1388
1402
|
}
|
|
@@ -1402,7 +1416,7 @@ export const ClaudeDriver = {
|
|
|
1402
1416
|
case "user":
|
|
1403
1417
|
for (const b of Array.isArray(o.message?.content) ? o.message.content : []) {
|
|
1404
1418
|
if (b.type === "tool_result") {
|
|
1405
|
-
emit({ ...base(threadId, currentTurnId()), type: "item.completed", itemType: "tool", itemId: b.tool_use_id, ok: !b.is_error });
|
|
1419
|
+
emit({ ...base(threadId, currentTurnId()), type: "item.completed", itemType: "tool", itemId: b.tool_use_id, ok: !b.is_error, output: toolDetailPreview(b.content) });
|
|
1406
1420
|
}
|
|
1407
1421
|
}
|
|
1408
1422
|
break;
|
|
@@ -1472,6 +1486,10 @@ export const ClaudeDriver = {
|
|
|
1472
1486
|
// process that exited between turns (idle close, contract change)
|
|
1473
1487
|
// is just a session ending
|
|
1474
1488
|
if (session.turn && !session.turn.settled) {
|
|
1489
|
+
// A retained process may be running a later user turn. Its close
|
|
1490
|
+
// handler must retry that request, not the process's first prompt.
|
|
1491
|
+
const { turnId, input: turn, retryAbort } = session.turn;
|
|
1492
|
+
const retry = retryState.get(threadId) ?? { attempt: 0, cancelled: false };
|
|
1475
1493
|
const message = `claude exited ${code} before result${session.stderr ? `: ${session.stderr.trim().slice(-300)}` : ""}`;
|
|
1476
1494
|
const verdict = classifyError({ exitCode: code, stderr: message });
|
|
1477
1495
|
if (!retry.cancelled &&
|
|
@@ -1531,7 +1549,7 @@ export const ClaudeDriver = {
|
|
|
1531
1549
|
active.delete(threadId);
|
|
1532
1550
|
try {
|
|
1533
1551
|
const cursor = session.sessionId ?? sessionId ?? undefined;
|
|
1534
|
-
await sendTurn({ ...turn, resumeCursor: cursor });
|
|
1552
|
+
await sendTurn({ ...turn, resumeCursor: cursor }, turnId);
|
|
1535
1553
|
}
|
|
1536
1554
|
catch (e) {
|
|
1537
1555
|
retryState.delete(threadId);
|
|
@@ -1598,7 +1616,7 @@ export const ClaudeDriver = {
|
|
|
1598
1616
|
void (async () => {
|
|
1599
1617
|
try {
|
|
1600
1618
|
// no cursor: a fresh session, carrying the rebuild
|
|
1601
|
-
await sendTurn({ ...turn, resumeCursor: undefined, recoveryText: undefined, text: recovery.text });
|
|
1619
|
+
await sendTurn({ ...turn, resumeCursor: undefined, recoveryText: undefined, text: recovery.text }, turnId);
|
|
1602
1620
|
}
|
|
1603
1621
|
catch (e) {
|
|
1604
1622
|
retryState.delete(threadId);
|
|
@@ -22,7 +22,7 @@ import { codexLocalProviderArgs } from "./local-inject.js";
|
|
|
22
22
|
import { augmentedPath, splitCliString } from "../env-path.js";
|
|
23
23
|
import { classifyError, computeBackoff, RETRY_MAX_ATTEMPTS } from "./retry.js";
|
|
24
24
|
import { appendNative } from "./native.js";
|
|
25
|
-
import { commandSummary } from "../tool-summary.js";
|
|
25
|
+
import { commandSummary, toolDetailPreview } from "../tool-summary.js";
|
|
26
26
|
import { codexDeveloperInstructions, syncCodexInstructions } from "./codex-instructions.js";
|
|
27
27
|
import { CodexDeviceAuthController } from "./codex-device-auth.js";
|
|
28
28
|
import { codexAccountEmail } from "./codex-identity.js";
|
|
@@ -799,6 +799,7 @@ export const CodexDriver = {
|
|
|
799
799
|
itemId: item.id,
|
|
800
800
|
title,
|
|
801
801
|
summary: item.type === "commandExecution" ? commandSummary({ command: item.command }) : undefined,
|
|
802
|
+
input: toolDetailPreview(item.type === "commandExecution" ? { command: item.command, cwd: item.cwd } : item.type === "mcpToolCall" ? item.arguments : item.type === "fileChange" ? item.changes : item.query),
|
|
802
803
|
});
|
|
803
804
|
}
|
|
804
805
|
break;
|
|
@@ -839,6 +840,7 @@ export const CodexDriver = {
|
|
|
839
840
|
itemType: "tool",
|
|
840
841
|
itemId: item.id,
|
|
841
842
|
ok: item.status !== "failed" && item.status !== "declined",
|
|
843
|
+
output: toolDetailPreview(item.type === "commandExecution" ? { output: item.aggregatedOutput, exitCode: item.exitCode } : item.type === "mcpToolCall" ? item.error ?? item.result : item.type === "fileChange" ? item.changes : item.action),
|
|
842
844
|
});
|
|
843
845
|
}
|
|
844
846
|
else if (item.type === "reasoning") {
|