openmausbot 0.1.86 → 0.1.87
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/README.md +2 -2
- package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-BTiuL79F.js} +1 -1
- package/dist/assets/{arc-DW7mrI4p.js → arc-C88Blc2v.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-BZsJIZrb.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CVeMe9Ah.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-Cedk3Bhx.js} +1 -1
- package/dist/assets/channel-taCNRlUU.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-D-VRqzTB.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-BChov01Y.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-C-TfwvHy.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-Db7CGj5Y.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-CNvIfqKT.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-C_bEp5tq.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Coi4BOls.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-CzylEuL3.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-m7p7ztaD.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BMy6o9x2.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-BDQ__0_J.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Djv4ghUb.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-BULrlGYH.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-D-jrT_hU.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-p2UhQmfL.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-DHvGWZ7q.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-ChhKDot-.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-B-T7A43R.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-CJm8ENNY.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-510tlza8.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-PkvRPHel.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-BVBVPoX3.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BgYMVm7L.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-BGqsnc_3.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-D_-UeReH.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-D0uooONx.js} +1 -1
- package/dist/assets/index-BwdMAQ6p.css +1 -0
- package/dist/assets/index-ByJennr7.js +581 -0
- package/dist/assets/{index-h9FFNwTu.js → index-zRsJERGJ.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-B9e1z77f.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-BB4vNwvD.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DpkosOTB.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-DbDP4kUu.js} +1 -1
- package/dist/assets/{linear-3su2TG93.js → linear-DBJGPmjT.js} +1 -1
- package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-DWPRi-ue.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-nszBbJmx.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-CWykJWu_.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-g3Da-Vnv.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-Wzrlpzgh.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-moHTFMC0.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-CwfXdFH8.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-3ZROYvnq.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-E1TBebP8.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-DHKEU5pu.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BguEj09D.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-C7IDYnhd.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-DCW-F5PW.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-xBMFtWYV.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-B5nIkuH8.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-DYFAhdbr.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-DeXwX6Cz.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +2 -1
- package/dist-server/container-mcp.js +1029 -164
- package/dist-server/drivers/agents-proxy.js +64 -10
- package/dist-server/drivers/phone-proxy.js +33 -2
- package/dist-server/index.js +14577 -9001
- package/dist-server/local-computer-proxy.js +3 -3
- package/dist-server/local-computer.js +53 -17
- package/dist-server/openmausbot.js +3981 -1165
- package/dist-server/pair-cli.js +3981 -1165
- package/dist-server/permission-proxy.js +29 -9
- package/dist-server/server/attachments.js +55 -0
- package/dist-server/server/auto-approve.js +4 -3
- package/dist-server/server/auto-vm-claims.js +1 -0
- package/dist-server/server/bot-package.js +18 -292
- package/dist-server/server/bot-profile.js +5 -17
- package/dist-server/server/box-create-idempotency.js +3 -3
- package/dist-server/server/box.js +57 -55
- package/dist-server/server/browser-proxy.js +3 -2
- package/dist-server/server/browser-runtime.js +37 -1
- package/dist-server/server/cli.js +2 -2
- package/dist-server/server/command-allowlist.js +154 -0
- package/dist-server/server/computer-wait.js +22 -9
- package/dist-server/server/config.js +59 -9
- package/dist-server/server/connector-verdict.js +141 -0
- package/dist-server/server/container-computer.js +1 -1
- package/dist-server/server/default-model-selection.js +9 -0
- package/dist-server/server/delegations.js +85 -34
- package/dist-server/server/drivers/acp/core.js +57 -15
- package/dist-server/server/drivers/agents-call.js +42 -3
- package/dist-server/server/drivers/agents-catalog.js +34 -4
- package/dist-server/server/drivers/boxagent.js +174 -36
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/chat-box-tools.js +90 -0
- package/dist-server/server/drivers/chat-images.js +54 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +38 -8
- package/dist-server/server/drivers/chat-tool-approval.js +70 -18
- package/dist-server/server/drivers/claude.js +7 -2
- package/dist-server/server/drivers/codex-instructions.js +48 -6
- package/dist-server/server/drivers/codex-tool-surface.js +20 -0
- package/dist-server/server/drivers/codex.js +95 -30
- package/dist-server/server/drivers/mistral.js +115 -0
- package/dist-server/server/drivers/openai-chat.js +155 -32
- package/dist-server/server/drivers/openai-compat.js +1 -0
- package/dist-server/server/drivers/permission-command.js +35 -0
- package/dist-server/server/drivers/phone-proxy.js +47 -2
- package/dist-server/server/drivers/pi.js +75 -4
- package/dist-server/server/drivers/prompt-split.js +87 -0
- package/dist-server/server/fleet.js +1 -1
- package/dist-server/server/group-thread-usage.js +103 -0
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/hosted-models.js +1 -1
- package/dist-server/server/index.js +1385 -322
- package/dist-server/server/local-computer.js +72 -23
- package/dist-server/server/managed-desktop.js +1 -1
- package/dist-server/server/mcp-bridge.js +6 -6
- package/dist-server/server/new-bot-defaults.js +115 -0
- package/dist-server/server/org-library.js +1051 -0
- package/dist-server/server/package-export.js +627 -39
- package/dist-server/server/package-import.js +458 -0
- package/dist-server/server/package-parts.js +87 -0
- package/dist-server/server/peer-approval.js +16 -7
- package/dist-server/server/peer-roster.js +10 -0
- package/dist-server/server/presets.js +492 -0
- package/dist-server/server/provider-key-check.js +3 -2
- package/dist-server/server/published-teams.js +80 -0
- package/dist-server/server/room-handoffs.js +19 -3
- package/dist-server/server/routes/bot-presets.js +25 -0
- package/dist-server/server/routine-requests.js +6 -3
- package/dist-server/server/routines.js +41 -1
- package/dist-server/server/section-context.js +10 -0
- package/dist-server/server/skills.js +34 -61
- package/dist-server/server/steer-queue.js +3 -2
- package/dist-server/server/store.js +259 -15
- package/dist-server/server/surface.js +10 -4
- package/dist-server/server/system-prompt.js +12 -4
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-computers.js +10 -0
- package/dist-server/server/team-library.js +6 -2
- package/dist-server/server/team-manifest.js +4 -3
- package/dist-server/server/team-setup-requests.js +16 -5
- package/dist-server/server/thread-events.js +17 -1
- package/dist-server/server/turn-context.js +2 -0
- package/dist-server/server/turn-watchdog.js +12 -3
- package/dist-server/server/usage-ledger.js +12 -1
- package/dist-server/server/vps-computer.js +139 -51
- package/dist-server/server/vps-container-mcp.js +7 -0
- package/dist-server/server/vps-ssh.js +36 -7
- package/dist-server/server/workspace-backup-policy.js +12 -0
- package/dist-server/server/workspace-backup.js +2 -2
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +188 -12
- package/dist-server/shared/bot-profile.js +15 -0
- package/dist-server/shared/command-allowlist.js +1 -0
- package/dist-server/shared/credential-request.js +1 -1
- package/dist-server/shared/package-format.js +967 -0
- package/dist-server/shared/skill-md.js +64 -0
- package/dist-server/shared/team-backup.js +7 -0
- package/dist-server/shared/wire.js +4 -0
- package/dist-server/vps-container-mcp.js +1129 -170
- package/enterprise/README.md +1 -1
- package/package.json +1 -1
- package/dist/assets/channel-BXFkgus9.js +0 -1
- package/dist/assets/index-BGgqlWPJ.css +0 -1
- package/dist/assets/index-BtHo8ZKl.js +0 -577
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +0 -8
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
// Adding a shared team to this installation.
|
|
2
|
+
//
|
|
3
|
+
// Import is additive-only. A package is untrusted input (a file someone
|
|
4
|
+
// sent, GitHub, the community catalog), so it is structurally unable to reach
|
|
5
|
+
// records the person already has: every bot, group chat, routine, section and
|
|
6
|
+
// connection it creates is NEW, and every privilege-bearing field is chosen
|
|
7
|
+
// here, never read from the file. What the file may set is persona and
|
|
8
|
+
// structure: names, standing instructions, looks, playbooks, skills (which
|
|
9
|
+
// land switched off), group chats, routines (which land paused), the team
|
|
10
|
+
// brief and Chief, starter notes, and connection SLOTS (an address and the
|
|
11
|
+
// names of values the person fills in later; never the values).
|
|
12
|
+
//
|
|
13
|
+
// `trust` is a function argument, never read from a request. The HTTP import
|
|
14
|
+
// route always passes "file". Only the organization library (wave 2, a
|
|
15
|
+
// separate module) passes "org": its skills arrive switched on because the
|
|
16
|
+
// organization's Admin published them, and its records carry the provenance
|
|
17
|
+
// the organization channel needs. Everything else is identical.
|
|
18
|
+
//
|
|
19
|
+
// A failure anywhere rolls back every record this import created.
|
|
20
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
21
|
+
import { mcpServerNameError, MAX_MCP_SERVERS, parseStoredMcpServer } from "./mcp-registry.js";
|
|
22
|
+
import { AGENT_PARTS, agentReleaseValues, connectionLocalValue, connectionReleaseValue, pair, pairs, playbookValues, ROOM_PARTS, roomReleaseValues, ROUTINE_PARTS, routineReleaseValues, scheduleValue, sha256Hex, TEAM_PARTS, } from "./package-parts.js";
|
|
23
|
+
import { importedMemberProfile } from "./team-manifest.js";
|
|
24
|
+
import { takeImportName } from "../shared/import-name.js";
|
|
25
|
+
import { decodeBase64 } from "../shared/package-format.js";
|
|
26
|
+
export const NO_BOTS_MESSAGE = "This package has no bots. Add it from your organization's shelf, or update OpenMausBot.";
|
|
27
|
+
/** A file with skills only: a file never adds a skill without a bot or a preset to hold it. */
|
|
28
|
+
export const NO_PRESETS_MESSAGE = "This file has no bots or preset bots to add. Its skills can be added from your organization's shelf.";
|
|
29
|
+
/** A refusal before anything was created, as a plain sentence. */
|
|
30
|
+
export class PackageImportError extends Error {
|
|
31
|
+
status = 400;
|
|
32
|
+
code;
|
|
33
|
+
constructor(code, message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "PackageImportError";
|
|
36
|
+
this.code = code;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Deterministic, so reconnecting to the same organization recognizes what
|
|
40
|
+
* it already added. `packageId` is Admin's package UUID. */
|
|
41
|
+
export function orgInstallId(adminOrigin, organizationId, packageId) {
|
|
42
|
+
return createHash("sha256").update(`omb-install:v1\n${adminOrigin}\n${organizationId}\n${packageId}`, "utf8").digest("hex").slice(0, 32);
|
|
43
|
+
}
|
|
44
|
+
function memberFromAgent(agent) {
|
|
45
|
+
return {
|
|
46
|
+
key: agent.key,
|
|
47
|
+
name: agent.name,
|
|
48
|
+
title: agent.title ?? "",
|
|
49
|
+
description: agent.description ?? "",
|
|
50
|
+
...(agent.soul !== undefined ? { soul: agent.soul } : {}),
|
|
51
|
+
appearance: {
|
|
52
|
+
color: agent.appearance.color,
|
|
53
|
+
...(agent.appearance.mascotExpression ? { mascotExpression: agent.appearance.mascotExpression } : {}),
|
|
54
|
+
...(agent.appearance.mascotBody ? { mascotBody: agent.appearance.mascotBody } : {}),
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** The first free server name for a slot: key, key-2, … (never an existing
|
|
59
|
+
* or reserved name, so a slot can never bind to credentials already here). */
|
|
60
|
+
function freeServerName(key, taken) {
|
|
61
|
+
for (let n = 1; n < 100; n += 1) {
|
|
62
|
+
const name = n === 1 ? key : `${key}-${n}`;
|
|
63
|
+
if (name.length > 32)
|
|
64
|
+
return null;
|
|
65
|
+
if (mcpServerNameError(name) || Object.hasOwn(taken, name))
|
|
66
|
+
continue;
|
|
67
|
+
return name;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
/** Add a shared team (v1 or v2, already parsed) as new records. */
|
|
72
|
+
export function importPackageDocument(document, options, deps) {
|
|
73
|
+
const pkg = document.package;
|
|
74
|
+
const library = !pkg.agents.length || !pkg.team;
|
|
75
|
+
// A library package (skills and presets, no bots) from a file needs the preset store.
|
|
76
|
+
if (library && options.trust !== "org" && !deps.presets)
|
|
77
|
+
throw new PackageImportError("no_bots", NO_BOTS_MESSAGE);
|
|
78
|
+
if (options.trust === "org") {
|
|
79
|
+
const org = options.org;
|
|
80
|
+
if (!org)
|
|
81
|
+
throw new PackageImportError("invalid_package", "This package is missing its organization details.");
|
|
82
|
+
if (org.installId !== orgInstallId(org.adminOrigin, org.organizationId, org.packageId)) {
|
|
83
|
+
throw new PackageImportError("invalid_package", "This package's install id does not match its organization.");
|
|
84
|
+
}
|
|
85
|
+
if (pkg.publisher?.organization !== org.publisher.slug) {
|
|
86
|
+
throw new PackageImportError("invalid_package", "This package was not published by the organization that shared it.");
|
|
87
|
+
}
|
|
88
|
+
if (org.ref !== `${org.publisher.slug}/${pkg.id}`) {
|
|
89
|
+
throw new PackageImportError("invalid_package", "This package is not the one your organization listed.");
|
|
90
|
+
}
|
|
91
|
+
if (!deps.skills.installOrg || !deps.routines.stampInstalledPackage) {
|
|
92
|
+
throw new PackageImportError("invalid_package", "This installation cannot add packages from an organization.");
|
|
93
|
+
}
|
|
94
|
+
// A bot someone made from one of the install's presets carries its
|
|
95
|
+
// install id too, but it is theirs, not one of the package's records;
|
|
96
|
+
// preset rows are not records either. Re-adding a removed team, or
|
|
97
|
+
// retrying after a crash, re-registers its presets in place (presets.ts).
|
|
98
|
+
if (deps.store.bots.some((bot) => bot.installedPackage?.installId === org.installId && !bot.installedPackage.presetKey) ||
|
|
99
|
+
deps.store.groups.some((group) => group.installedPackage?.installId === org.installId)) {
|
|
100
|
+
return { alreadyAdded: true, installId: org.installId };
|
|
101
|
+
}
|
|
102
|
+
// A library package (skills and presets, no team) creates no bots:
|
|
103
|
+
// its skills are offered under Bot → Skills, its presets go to New bot,
|
|
104
|
+
// and the organization library keeps the index of what it offers.
|
|
105
|
+
if (library) {
|
|
106
|
+
const registered = deps.presets?.register(document, { source: "org", installId: org.installId, org });
|
|
107
|
+
return {
|
|
108
|
+
alreadyAdded: false, installId: org.installId, name: pkg.name, section: "", bots: [], groups: [], routines: [],
|
|
109
|
+
offeredSkills: (pkg.skills?.entries ?? []).map((skill) => skill.name), connections: [],
|
|
110
|
+
skipped: registered?.skipped ?? (pkg.presets ?? []).map((preset) => ({ part: `presets[${preset.key}]`, reason: "presets_not_supported_yet" })),
|
|
111
|
+
brief: false, notes: 0,
|
|
112
|
+
...(registered ? { presets: [...registered.added, ...registered.existing] } : {}),
|
|
113
|
+
org: { kind: "library", section: "", team: { parts: {} }, bots: {}, rooms: {}, routines: {}, connections: {} },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (library)
|
|
118
|
+
return importLibraryFile(document, deps.presets);
|
|
119
|
+
return runImport({ kind: "package", document }, options, deps);
|
|
120
|
+
}
|
|
121
|
+
/** A preset file (skills and presets, no team): the presets go to New bot,
|
|
122
|
+
* and the skills are offered (a file never installs a skill without a bot or
|
|
123
|
+
* a preset to hold it). */
|
|
124
|
+
function importLibraryFile(document, presets) {
|
|
125
|
+
const pkg = document.package;
|
|
126
|
+
if (!pkg.presets?.length)
|
|
127
|
+
throw new PackageImportError("no_bots", NO_PRESETS_MESSAGE);
|
|
128
|
+
const installId = randomUUID();
|
|
129
|
+
const registered = presets.register(document, { source: "file", installId });
|
|
130
|
+
return {
|
|
131
|
+
alreadyAdded: false, installId, name: pkg.name, section: "", bots: [], groups: [], routines: [],
|
|
132
|
+
offeredSkills: (pkg.skills?.entries ?? []).map((skill) => skill.name), connections: [],
|
|
133
|
+
skipped: registered.skipped, brief: false, notes: 0, presets: [...registered.added, ...registered.existing],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** Add a legacy `openmaus.team` file: people only (plus a project room when
|
|
137
|
+
* the caller asks for one). */
|
|
138
|
+
export function importTeamManifest(manifest, options, deps) {
|
|
139
|
+
return runImport({ kind: "manifest", manifest }, { ...options, trust: "file" }, deps);
|
|
140
|
+
}
|
|
141
|
+
function runImport(source, options, deps) {
|
|
142
|
+
const { store } = deps;
|
|
143
|
+
const pkg = source.kind === "package" ? source.document.package : null;
|
|
144
|
+
const name = pkg?.name ?? (source.kind === "manifest" ? source.manifest.team.name : "");
|
|
145
|
+
const teamName = pkg?.team?.name ?? name;
|
|
146
|
+
const org = options.trust === "org" ? options.org : undefined;
|
|
147
|
+
const installId = pkg ? org?.installId ?? randomUUID() : undefined;
|
|
148
|
+
const members = pkg
|
|
149
|
+
? pkg.agents.map((agent) => ({ member: memberFromAgent(agent), agent }))
|
|
150
|
+
: source.manifest.team.members.map((member) => ({ member, agent: undefined }));
|
|
151
|
+
const importedBots = [];
|
|
152
|
+
const createdGroups = [];
|
|
153
|
+
const createdRoutines = [];
|
|
154
|
+
const createdServers = [];
|
|
155
|
+
const createdPresets = [];
|
|
156
|
+
const skipped = [];
|
|
157
|
+
const orgIndex = { kind: "team", section: "", team: { parts: {} }, bots: {}, rooms: {}, routines: {}, connections: {} };
|
|
158
|
+
// Names already in use, hidden bots included: an archived bot can be
|
|
159
|
+
// un-archived later, and a revived duplicate would be just as ambiguous.
|
|
160
|
+
const takenNames = new Set(store.bots.map((bot) => bot.name.trim().toLowerCase()));
|
|
161
|
+
const botIds = new Map();
|
|
162
|
+
let group;
|
|
163
|
+
let section;
|
|
164
|
+
let notes = 0;
|
|
165
|
+
try {
|
|
166
|
+
const selection = deps.defaultSelection();
|
|
167
|
+
const existingSections = new Set([...store.sections, ...store.bots.map((bot) => bot.section), ...store.groups.map((candidate) => candidate.section)]
|
|
168
|
+
.filter((value) => Boolean(value?.trim()))
|
|
169
|
+
.map((value) => value.trim().toLowerCase()));
|
|
170
|
+
// Every template gets its own new section, numbered on collision. Never
|
|
171
|
+
// merge into an existing section (or replace its Chief).
|
|
172
|
+
section = takeImportName(teamName, existingSections, 60);
|
|
173
|
+
// Connection slots first, so bots can be bound to exactly these servers.
|
|
174
|
+
const serverForKey = new Map();
|
|
175
|
+
const connections = [];
|
|
176
|
+
if (pkg?.connections?.length) {
|
|
177
|
+
const next = { ...deps.mcp.servers() };
|
|
178
|
+
for (const connection of pkg.connections) {
|
|
179
|
+
const part = `connections[${connection.key}]`;
|
|
180
|
+
if (Object.keys(next).length >= MAX_MCP_SERVERS) {
|
|
181
|
+
skipped.push({ part, reason: "too_many_connections" });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const serverName = freeServerName(connection.key, next);
|
|
185
|
+
const server = {
|
|
186
|
+
type: connection.mcp.transport,
|
|
187
|
+
url: connection.mcp.url,
|
|
188
|
+
...(connection.mcp.valueNames.length ? { headers: Object.fromEntries(connection.mcp.valueNames.map((header) => [header, ""])) } : {}),
|
|
189
|
+
// Off until the person fills in the values and switches it on.
|
|
190
|
+
enabled: false,
|
|
191
|
+
};
|
|
192
|
+
if (!serverName || !parseStoredMcpServer(serverName, server).ok) {
|
|
193
|
+
skipped.push({ part, reason: "connection_invalid" });
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (deps.mcp.refusal(serverName, server)) {
|
|
197
|
+
skipped.push({ part, reason: "connection_refused_by_policy" });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
next[serverName] = server;
|
|
201
|
+
createdServers.push(serverName);
|
|
202
|
+
serverForKey.set(connection.key, serverName);
|
|
203
|
+
connections.push({ key: connection.key, name: serverName, label: connection.label });
|
|
204
|
+
if (org) {
|
|
205
|
+
orgIndex.connections[connection.key] = { name: serverName, ...pair(connectionReleaseValue(connection), connectionLocalValue(server)) };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (createdServers.length)
|
|
209
|
+
deps.mcp.persist(next);
|
|
210
|
+
}
|
|
211
|
+
const playbookByKey = new Map((pkg?.playbooks ?? []).map((playbook) => [playbook.key, playbook]));
|
|
212
|
+
const skillByName = new Map((pkg?.skills?.entries ?? []).map((skill) => [skill.name, skill]));
|
|
213
|
+
for (const { member, agent } of members) {
|
|
214
|
+
// importedMemberProfile is the authority boundary: persona fields
|
|
215
|
+
// only, colliding names numbered. seedMessages: false — an imported
|
|
216
|
+
// bot must not open by greeting the user as though it were new.
|
|
217
|
+
const created = store.createBot({
|
|
218
|
+
...importedMemberProfile(member, takenNames),
|
|
219
|
+
modelSelection: selection,
|
|
220
|
+
section,
|
|
221
|
+
...(options.visibility ? { visibility: options.visibility } : {}),
|
|
222
|
+
}, { seedMessages: false });
|
|
223
|
+
importedBots.push(created);
|
|
224
|
+
botIds.set(member.key, created.id);
|
|
225
|
+
const playbooks = (agent?.playbooks ?? []).flatMap((key) => {
|
|
226
|
+
const playbook = playbookByKey.get(key);
|
|
227
|
+
return playbook ? [{ ...playbook }] : [];
|
|
228
|
+
});
|
|
229
|
+
let avatar = {};
|
|
230
|
+
let avatarHash = null;
|
|
231
|
+
if (agent?.appearance.avatar) {
|
|
232
|
+
const bytes = decodeBase64(agent.appearance.avatar.data);
|
|
233
|
+
if (!bytes)
|
|
234
|
+
throw new Error(`The picture for ${member.name} could not be read`);
|
|
235
|
+
avatar = { avatarUrl: deps.images.save(bytes, agent.appearance.avatar.mime), avatarCrop: agent.appearance.avatar.crop };
|
|
236
|
+
avatarHash = sha256Hex(bytes);
|
|
237
|
+
}
|
|
238
|
+
const installed = pkg
|
|
239
|
+
? {
|
|
240
|
+
id: pkg.id,
|
|
241
|
+
name: pkg.name,
|
|
242
|
+
release: pkg.release,
|
|
243
|
+
requiredApps: pkg.requirements.apps.map((app) => ({ ...app })),
|
|
244
|
+
source: org ? "org" : "file",
|
|
245
|
+
installId,
|
|
246
|
+
agentKey: member.key,
|
|
247
|
+
...(agent?.approval ? { suggestedApproval: agent.approval } : {}),
|
|
248
|
+
...(org ? { publisher: { ...org.publisher }, ref: org.ref, sha256: org.sha256 } : {}),
|
|
249
|
+
}
|
|
250
|
+
: undefined;
|
|
251
|
+
// composio: false — a shared persona never starts with reach into the
|
|
252
|
+
// person's connected apps (absence would mean allowed). Approval is
|
|
253
|
+
// never set: every imported bot starts on Ask, whatever was suggested.
|
|
254
|
+
store.patchBot(created.id, {
|
|
255
|
+
composio: false,
|
|
256
|
+
connectorTools: {},
|
|
257
|
+
...(playbooks.length ? { playbooks } : {}),
|
|
258
|
+
...(installed ? { installedPackage: installed } : {}),
|
|
259
|
+
...(agent?.connections ? { mcpServers: agent.connections.flatMap((key) => serverForKey.has(key) ? [serverForKey.get(key)] : []) } : {}),
|
|
260
|
+
...avatar,
|
|
261
|
+
});
|
|
262
|
+
for (const skillName of agent?.skills ?? []) {
|
|
263
|
+
const skill = skillByName.get(skillName);
|
|
264
|
+
if (!skill)
|
|
265
|
+
throw new Error(`Package skill "${skillName}" is unavailable`);
|
|
266
|
+
// A file's `source` is display provenance only; the organization
|
|
267
|
+
// channel names its own source so ownership can never be forged.
|
|
268
|
+
// Organization skills arrive switched on, stamped with their install.
|
|
269
|
+
const added = org
|
|
270
|
+
? deps.skills.installOrg(created.id, `org:${org.ref}@${pkg.release}`, skill.instructions, { installId: org.installId, key: skill.name, release: pkg.release, ...pair(skill.instructions, skill.instructions) })
|
|
271
|
+
: deps.skills.install(created.id, skill.source ?? `package:${pkg.id}`, [{ path: "SKILL.md", content: skill.instructions }]);
|
|
272
|
+
if ("error" in added)
|
|
273
|
+
throw new Error(`Package skill "${skillName}" could not be imported: ${added.error}`);
|
|
274
|
+
}
|
|
275
|
+
// Starter notes are copied once, on this first add.
|
|
276
|
+
for (const [path, text] of Object.entries(agent?.seed?.memory ?? {})) {
|
|
277
|
+
if (path === "MEMORY.md")
|
|
278
|
+
deps.memory.writeIndex(created.id, text);
|
|
279
|
+
else
|
|
280
|
+
deps.memory.writeTopic(created.id, path.slice("memory/".length), text);
|
|
281
|
+
notes += 1;
|
|
282
|
+
}
|
|
283
|
+
// An organization bot's part hashes are its last write, so a bot that
|
|
284
|
+
// has them is complete (org-library.ts reads that after a crash).
|
|
285
|
+
if (org && agent) {
|
|
286
|
+
const written = store.bot(created.id);
|
|
287
|
+
const local = {
|
|
288
|
+
name: written.name,
|
|
289
|
+
title: written.title ?? "",
|
|
290
|
+
description: written.description ?? "",
|
|
291
|
+
soul: written.soul ?? "",
|
|
292
|
+
look: {
|
|
293
|
+
color: written.color,
|
|
294
|
+
mascotExpression: written.mascotExpression ?? null,
|
|
295
|
+
mascotBody: written.mascotBody ?? null,
|
|
296
|
+
avatar: avatarHash,
|
|
297
|
+
crop: written.avatarCrop ?? null,
|
|
298
|
+
},
|
|
299
|
+
playbooks: playbookValues(written.playbooks ?? []),
|
|
300
|
+
skills: [...(agent.skills ?? [])].sort(),
|
|
301
|
+
connections: [...(written.mcpServers ?? [])].sort(),
|
|
302
|
+
approval: "ask",
|
|
303
|
+
};
|
|
304
|
+
store.patchBot(created.id, { packageBase: pairs(AGENT_PARTS, agentReleaseValues(agent, playbookByKey), local) });
|
|
305
|
+
orgIndex.bots[member.key] = created.id;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// The brief goes before the group chats and routines, and the leader is
|
|
309
|
+
// set last, so an organization Add the app stopped partway through can
|
|
310
|
+
// tell a finished team from a partial one (org-library.ts).
|
|
311
|
+
const brief = Boolean(pkg?.team?.brief?.trim());
|
|
312
|
+
if (brief)
|
|
313
|
+
deps.sections.writeBrief(section, pkg.team.brief);
|
|
314
|
+
// A package is an explicit structure import: its rooms are created from
|
|
315
|
+
// package-local keys only, then normalized to the fresh bot ids.
|
|
316
|
+
const groupIds = new Map();
|
|
317
|
+
for (const room of pkg?.rooms ?? []) {
|
|
318
|
+
const ids = room.members.map((key) => botIds.get(key));
|
|
319
|
+
let created = store.createGroup(room.name, ids, false, section);
|
|
320
|
+
createdGroups.push(created);
|
|
321
|
+
const defaultResponder = room.defaultResponder.kind === "agent"
|
|
322
|
+
? { kind: "member", botId: botIds.get(room.defaultResponder.agent) }
|
|
323
|
+
: { kind: room.defaultResponder.kind };
|
|
324
|
+
created = store.patchGroup(created.id, {
|
|
325
|
+
bulletin: room.bulletin ?? "",
|
|
326
|
+
defaultResponder,
|
|
327
|
+
setupCompletedAt: Date.now(),
|
|
328
|
+
}) ?? created;
|
|
329
|
+
groupIds.set(room.key, created.id);
|
|
330
|
+
if (org) {
|
|
331
|
+
const local = {
|
|
332
|
+
name: created.name,
|
|
333
|
+
bulletin: created.bulletin ?? "",
|
|
334
|
+
members: [...created.memberIds].sort(),
|
|
335
|
+
defaultResponder: created.defaultResponder,
|
|
336
|
+
};
|
|
337
|
+
created = store.patchGroup(created.id, {
|
|
338
|
+
installedPackage: { installId: org.installId, key: room.key, memberKeys: [...room.members].sort(), parts: pairs(ROOM_PARTS, roomReleaseValues(room), local) },
|
|
339
|
+
}) ?? created;
|
|
340
|
+
orgIndex.rooms[room.key] = created.id;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
for (const routine of pkg?.routines ?? []) {
|
|
344
|
+
// Routines always arrive paused from a file. The organization channel
|
|
345
|
+
// also pauses them for now; honouring enabledAfterInstall there is a
|
|
346
|
+
// later, Admin-side decision.
|
|
347
|
+
let timeoutMinutes = routine.timeoutMinutes;
|
|
348
|
+
if (timeoutMinutes !== undefined && (timeoutMinutes < 5 || timeoutMinutes > 240)) {
|
|
349
|
+
timeoutMinutes = Math.min(240, Math.max(5, timeoutMinutes));
|
|
350
|
+
skipped.push({ part: `routines[${routine.key}].timeoutMinutes`, reason: "run_limit_adjusted" });
|
|
351
|
+
}
|
|
352
|
+
const input = {
|
|
353
|
+
name: routine.name,
|
|
354
|
+
prompt: routine.prompt,
|
|
355
|
+
botId: botIds.get(routine.agent),
|
|
356
|
+
...(routine.room !== undefined ? { target: "room-goal", groupId: groupIds.get(routine.room) } : {}),
|
|
357
|
+
runOn: routine.runOn,
|
|
358
|
+
enabled: false,
|
|
359
|
+
schedule: routine.schedule,
|
|
360
|
+
durationMinutes: routine.durationMinutes,
|
|
361
|
+
...(timeoutMinutes === undefined ? {} : { timeoutMinutes }),
|
|
362
|
+
...(routine.overlap ? { overlap: routine.overlap } : {}),
|
|
363
|
+
...(routine.continuity && routine.room === undefined ? { continuity: true } : {}),
|
|
364
|
+
};
|
|
365
|
+
const created = deps.routines.create(input);
|
|
366
|
+
createdRoutines.push(created);
|
|
367
|
+
if (org) {
|
|
368
|
+
const local = {
|
|
369
|
+
name: created.name,
|
|
370
|
+
prompt: created.prompt,
|
|
371
|
+
schedule: scheduleValue(created),
|
|
372
|
+
target: { botId: created.botId, groupId: created.groupId ?? null },
|
|
373
|
+
};
|
|
374
|
+
deps.routines.stampInstalledPackage(created.id, { installId: org.installId, key: routine.key, parts: pairs(ROUTINE_PARTS, routineReleaseValues(routine), local) });
|
|
375
|
+
orgIndex.routines[routine.key] = created.id;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (pkg?.team?.leader)
|
|
379
|
+
store.setChiefOfStaff(botIds.get(pkg.team.leader));
|
|
380
|
+
if (org && pkg?.team) {
|
|
381
|
+
orgIndex.section = section;
|
|
382
|
+
orgIndex.team.parts = pairs(TEAM_PARTS, {
|
|
383
|
+
name: pkg.team.name, brief: pkg.team.brief ?? "", leader: pkg.team.leader ?? null,
|
|
384
|
+
}, {
|
|
385
|
+
name: section, brief: brief ? pkg.team.brief : "", leader: pkg.team.leader ? botIds.get(pkg.team.leader) : null,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
let presets;
|
|
389
|
+
// An organization install always registers, so a release re-added without
|
|
390
|
+
// presets also drops the rows an earlier one left (presets.ts).
|
|
391
|
+
if (source.kind === "package" && deps.presets && (pkg?.presets?.length || org)) {
|
|
392
|
+
const registered = deps.presets.register(source.document, { source: org ? "org" : "file", installId: installId, ...(org ? { org } : {}) });
|
|
393
|
+
createdPresets.push(...registered.added.map((preset) => preset.id));
|
|
394
|
+
skipped.push(...registered.skipped);
|
|
395
|
+
presets = [...registered.added, ...registered.existing];
|
|
396
|
+
}
|
|
397
|
+
else
|
|
398
|
+
for (const preset of pkg?.presets ?? [])
|
|
399
|
+
skipped.push({ part: `presets[${preset.key}]`, reason: "presets_not_supported_yet" });
|
|
400
|
+
// The legacy project room is created last, so a failure anywhere above
|
|
401
|
+
// leaves no half-built project behind.
|
|
402
|
+
if (!pkg && options.mode === "project" && importedBots.length > 0) {
|
|
403
|
+
const roomName = options.room?.trim() || name;
|
|
404
|
+
group = store.createGroup(roomName, importedBots.map((bot) => bot.id), false, section);
|
|
405
|
+
createdGroups.push(group);
|
|
406
|
+
if (options.cwd) {
|
|
407
|
+
// `cwd` is the folder the room WANTS; the store pins it on the
|
|
408
|
+
// first turn (pinGroupCwd).
|
|
409
|
+
group = store.patchGroup(group.id, { cwd: options.cwd }) ?? group;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const referenced = new Set((pkg?.agents ?? []).flatMap((agent) => agent.skills ?? []));
|
|
413
|
+
const bots = importedBots.map((bot) => store.bot(bot.id));
|
|
414
|
+
for (const bot of bots)
|
|
415
|
+
deps.broadcast?.({ kind: "bot", bot });
|
|
416
|
+
if (group)
|
|
417
|
+
deps.broadcast?.({ kind: "group", group });
|
|
418
|
+
return {
|
|
419
|
+
alreadyAdded: false,
|
|
420
|
+
...(installId ? { installId } : {}),
|
|
421
|
+
...(org ? { org: orgIndex } : {}),
|
|
422
|
+
name,
|
|
423
|
+
section,
|
|
424
|
+
bots,
|
|
425
|
+
groups: createdGroups.map((created) => store.group(created.id) ?? created),
|
|
426
|
+
...(group ? { group } : {}),
|
|
427
|
+
routines: createdRoutines,
|
|
428
|
+
offeredSkills: (pkg?.skills?.entries ?? []).filter((skill) => !referenced.has(skill.name)).map((skill) => skill.name),
|
|
429
|
+
connections,
|
|
430
|
+
skipped,
|
|
431
|
+
brief,
|
|
432
|
+
notes,
|
|
433
|
+
...(presets ? { presets } : {}),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
// A room of deleted members must not survive either — patchGroup can
|
|
438
|
+
// throw (disk) after createGroup already saved.
|
|
439
|
+
for (const routine of createdRoutines)
|
|
440
|
+
deps.routines.remove(routine.id);
|
|
441
|
+
for (const created of createdGroups)
|
|
442
|
+
store.deleteGroup(created.id);
|
|
443
|
+
for (const bot of importedBots)
|
|
444
|
+
store.deleteBot(bot.id);
|
|
445
|
+
deps.presets?.remove(createdPresets);
|
|
446
|
+
if (createdServers.length) {
|
|
447
|
+
const next = { ...deps.mcp.servers() };
|
|
448
|
+
for (const serverName of createdServers)
|
|
449
|
+
delete next[serverName];
|
|
450
|
+
deps.mcp.persist(next);
|
|
451
|
+
}
|
|
452
|
+
// This import allocated a fresh section (and maybe its brief); a failed
|
|
453
|
+
// installation retires that identity again.
|
|
454
|
+
if (section && store.sections.includes(section))
|
|
455
|
+
store.changeEmptySection(section, null);
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Part hashes for records added from an organization's library (contract
|
|
2
|
+
// §1.6). Every part of an added team keeps two hashes: `r`, the value the
|
|
3
|
+
// release shipped, and `w`, the value OpenMausBot wrote at install. The
|
|
4
|
+
// automatic updater compares them later: `hash(local) ≠ w` means the person
|
|
5
|
+
// edited the part, `r(new release) ≠ r` means the publisher changed it. Only
|
|
6
|
+
// the hashes are kept, never a second copy of the content.
|
|
7
|
+
//
|
|
8
|
+
// The value shapes are frozen: a stamp written today must still compare
|
|
9
|
+
// correctly in a later version, so change nothing here without a new stamp
|
|
10
|
+
// version.
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { canonicalJson, decodeBase64 } from "../shared/package-format.js";
|
|
13
|
+
export const AGENT_PARTS = ["name", "title", "description", "soul", "look", "playbooks", "skills", "connections", "approval"];
|
|
14
|
+
export const ROOM_PARTS = ["name", "bulletin", "members", "defaultResponder"];
|
|
15
|
+
export const ROUTINE_PARTS = ["name", "prompt", "schedule", "target"];
|
|
16
|
+
export const TEAM_PARTS = ["name", "brief", "leader"];
|
|
17
|
+
export const sha256Hex = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
18
|
+
/** sha256hex(utf8(canonicalJson(value ?? null))) */
|
|
19
|
+
export function partHash(value) {
|
|
20
|
+
return sha256Hex(canonicalJson(value ?? null));
|
|
21
|
+
}
|
|
22
|
+
export const pair = (release, written) => ({ r: partHash(release), w: partHash(written) });
|
|
23
|
+
export function releaseLook(appearance) {
|
|
24
|
+
const bytes = appearance.avatar ? decodeBase64(appearance.avatar.data) : null;
|
|
25
|
+
return {
|
|
26
|
+
color: appearance.color,
|
|
27
|
+
mascotExpression: appearance.mascotExpression ?? null,
|
|
28
|
+
mascotBody: appearance.mascotBody ?? null,
|
|
29
|
+
avatar: bytes ? sha256Hex(bytes) : null,
|
|
30
|
+
crop: appearance.avatar?.crop ?? null,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function playbookValues(playbooks) {
|
|
34
|
+
return playbooks.map(({ key, name, summary, triggers, instructions }) => ({ key, name, summary, triggers: [...triggers], instructions }));
|
|
35
|
+
}
|
|
36
|
+
/** The release side of every bot part. */
|
|
37
|
+
export function agentReleaseValues(agent, playbooks) {
|
|
38
|
+
return {
|
|
39
|
+
name: agent.name,
|
|
40
|
+
title: agent.title ?? "",
|
|
41
|
+
description: agent.description ?? "",
|
|
42
|
+
soul: agent.soul ?? "",
|
|
43
|
+
look: releaseLook(agent.appearance),
|
|
44
|
+
playbooks: playbookValues((agent.playbooks ?? []).flatMap((key) => (playbooks.has(key) ? [playbooks.get(key)] : []))),
|
|
45
|
+
skills: [...(agent.skills ?? [])].sort(),
|
|
46
|
+
connections: [...(agent.connections ?? [])].sort(),
|
|
47
|
+
approval: agent.approval ?? null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export const roomReleaseValues = (room) => ({
|
|
51
|
+
name: room.name,
|
|
52
|
+
bulletin: room.bulletin ?? "",
|
|
53
|
+
members: [...room.members].sort(),
|
|
54
|
+
defaultResponder: room.defaultResponder.kind === "agent"
|
|
55
|
+
? { kind: "agent", agent: room.defaultResponder.agent }
|
|
56
|
+
: { kind: room.defaultResponder.kind },
|
|
57
|
+
});
|
|
58
|
+
/** One normal form for a routine's timing on both sides, so an untouched
|
|
59
|
+
* routine compares equal: absent cap → null, absent overlap → "skip". */
|
|
60
|
+
export function scheduleValue(routine) {
|
|
61
|
+
return {
|
|
62
|
+
runOn: routine.runOn,
|
|
63
|
+
schedule: routine.schedule,
|
|
64
|
+
durationMinutes: routine.durationMinutes,
|
|
65
|
+
timeoutMinutes: routine.timeoutMinutes ?? null,
|
|
66
|
+
overlap: routine.overlap ?? "skip",
|
|
67
|
+
continuity: routine.continuity === true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export const routineReleaseValues = (routine) => ({
|
|
71
|
+
name: routine.name,
|
|
72
|
+
prompt: routine.prompt,
|
|
73
|
+
schedule: scheduleValue(routine),
|
|
74
|
+
target: { agent: routine.agent, room: routine.room ?? null },
|
|
75
|
+
});
|
|
76
|
+
export const connectionReleaseValue = (connection) => ({
|
|
77
|
+
transport: connection.mcp.transport,
|
|
78
|
+
url: connection.mcp.url,
|
|
79
|
+
valueNames: [...connection.mcp.valueNames],
|
|
80
|
+
});
|
|
81
|
+
export function connectionLocalValue(server) {
|
|
82
|
+
const headers = server.headers && typeof server.headers === "object" ? Object.keys(server.headers) : [];
|
|
83
|
+
return { transport: server.type ?? null, url: server.url ?? null, valueNames: headers.sort() };
|
|
84
|
+
}
|
|
85
|
+
export function pairs(keys, release, written) {
|
|
86
|
+
return Object.fromEntries(keys.map((key) => [key, pair(release[key], written[key])]));
|
|
87
|
+
}
|
|
@@ -17,6 +17,15 @@ import { newId } from "./contracts.js";
|
|
|
17
17
|
import { buildNotification } from "./notify.js";
|
|
18
18
|
import { peerAllowKey } from "./peer-approval-key.js";
|
|
19
19
|
export { peerAllowKey } from "./peer-approval-key.js";
|
|
20
|
+
export function peerApprovalFailure(outcome) {
|
|
21
|
+
return {
|
|
22
|
+
error: outcome === "deny" ? "denied by user"
|
|
23
|
+
: outcome === "expired" ? "the approval card expired without an answer"
|
|
24
|
+
: "the approval was cancelled before a decision",
|
|
25
|
+
approvalOutcome: outcome,
|
|
26
|
+
approvalSource: outcome === "deny" ? "user" : "system",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
20
29
|
/** Mark the card answered so the UI stops treating it as pending. Mirrors
|
|
21
30
|
* what the `request.resolved` fold does for provider cards; a harness-native
|
|
22
31
|
* card never emits that event, so it has to settle itself. */
|
|
@@ -102,7 +111,7 @@ function announceCard(bus, from, card, sourceThreadId) {
|
|
|
102
111
|
bus.notify?.(buildNotification("approval", from, sourceThreadId, detail, { avatarUrl: from.avatarUrl, group }));
|
|
103
112
|
}
|
|
104
113
|
/** Ask the user (in the source task thread) whether `from` may `action` `target`.
|
|
105
|
-
*
|
|
114
|
+
* Distinguishes user denial from expiry and cancellation. If `from.alwaysAllow` already
|
|
106
115
|
* covers the (action, target) pair, returns `"allow"` immediately
|
|
107
116
|
* without a card. */
|
|
108
117
|
export function requestPeerApproval(bus, from, target, message, action, sourceThreadId = from.threadId) {
|
|
@@ -116,14 +125,14 @@ export function requestPeerApproval(bus, from, target, message, action, sourceTh
|
|
|
116
125
|
const card = pushApprovalCard(bus, from, target, message, action, requestId, sourceThreadId);
|
|
117
126
|
announceCard(bus, from, card, sourceThreadId);
|
|
118
127
|
const timer = setTimeout(() => {
|
|
119
|
-
// 15 minutes without an answer →
|
|
128
|
+
// 15 minutes without an answer → expired. Keeps an unattended bot from
|
|
120
129
|
// stalling its own turn forever (matches the Claude broker timeout).
|
|
121
130
|
const pending = pendingComms.get(requestId);
|
|
122
131
|
if (!pending)
|
|
123
132
|
return;
|
|
124
133
|
pendingComms.delete(requestId);
|
|
125
134
|
settleCard(pending, "deny", "system");
|
|
126
|
-
resolve("
|
|
135
|
+
resolve("expired");
|
|
127
136
|
}, APPROVAL_TIMEOUT_MS);
|
|
128
137
|
timer.unref?.(); // a waiting card must never hold the process open
|
|
129
138
|
pendingComms.set(requestId, {
|
|
@@ -154,7 +163,7 @@ export function resolvePeerComms(_bus, requestId, behavior) {
|
|
|
154
163
|
return true;
|
|
155
164
|
}
|
|
156
165
|
/** Drop every approval waiting on a bot that no longer exists (or is being
|
|
157
|
-
* deleted),
|
|
166
|
+
* deleted), cancelling it so the caller's turn doesn't wait out the timeout. */
|
|
158
167
|
export function cancelPeerApprovalsFor(botId) {
|
|
159
168
|
for (const [requestId, pending] of Array.from(pendingComms)) {
|
|
160
169
|
if (pending.fromBotId !== botId && pending.toBotId !== botId)
|
|
@@ -162,10 +171,10 @@ export function cancelPeerApprovalsFor(botId) {
|
|
|
162
171
|
pendingComms.delete(requestId);
|
|
163
172
|
clearTimeout(pending.timer);
|
|
164
173
|
settleCard(pending, "deny", "system");
|
|
165
|
-
pending.resolve("
|
|
174
|
+
pending.resolve("cancelled");
|
|
166
175
|
}
|
|
167
176
|
}
|
|
168
|
-
/**
|
|
177
|
+
/** Cancel every peer-communication approval owned by a thread whose turn was
|
|
169
178
|
* interrupted. Patching the card alone is not enough: the in-memory promise
|
|
170
179
|
* must resolve too, or the delegation queue waits until its 15-minute timer. */
|
|
171
180
|
export function cancelPeerApprovalsForThread(threadId) {
|
|
@@ -175,7 +184,7 @@ export function cancelPeerApprovalsForThread(threadId) {
|
|
|
175
184
|
pendingComms.delete(requestId);
|
|
176
185
|
clearTimeout(pending.timer);
|
|
177
186
|
settleCard(pending, "deny", "system");
|
|
178
|
-
pending.resolve("
|
|
187
|
+
pending.resolve("cancelled");
|
|
179
188
|
}
|
|
180
189
|
}
|
|
181
190
|
/** Every thread a peer card can be raised in. A card lands in the thread
|
|
@@ -14,6 +14,16 @@ export function canAccessTeam(from, section) {
|
|
|
14
14
|
return target === sectionKey(from.section) || Boolean(from.chiefOfStaff &&
|
|
15
15
|
Array.isArray(from.managedSections) && from.managedSections.some(value => typeof value === "string" && sectionKey(value) === target));
|
|
16
16
|
}
|
|
17
|
+
/** Returns whether `coordinator` is an authorized Chief of Staff supervising `bot`'s section. */
|
|
18
|
+
export function coordinatorSupervises(coordinator, bot) {
|
|
19
|
+
if (!coordinator?.chiefOfStaff || !bot)
|
|
20
|
+
return false;
|
|
21
|
+
const target = sectionKey(bot.section);
|
|
22
|
+
if (!target)
|
|
23
|
+
return false;
|
|
24
|
+
return Boolean(Array.isArray(coordinator.managedSections) &&
|
|
25
|
+
coordinator.managedSections.some((value) => typeof value === "string" && sectionKey(value) === target));
|
|
26
|
+
}
|
|
17
27
|
const PEER_STATUS_WORDS = {
|
|
18
28
|
available: "available",
|
|
19
29
|
working: "working right now",
|