openmausbot 0.1.86 → 0.1.88
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 +4 -3
- package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-qd9VD3GV.js} +1 -1
- package/dist/assets/{arc-DW7mrI4p.js → arc-3RwkUjRe.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-CFKSJS7a.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CG1rEfWD.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-CBW9N664.js} +1 -1
- package/dist/assets/channel-CnRnO3tF.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-2mRfTksB.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-Cv1dpErj.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-B7IACJp-.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-An0hZtYa.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-BcJB2nEm.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-CV_W7TN7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Bcs_kwnA.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-lrx_DxFP.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-DabHlLGM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BnHj6P7d.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-CfLp6UDi.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Dm4dbVA1.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-B9mH-PID.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-DDc_NVQJ.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-lwr8Ip8-.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-BLLe6dmI.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-6eUIq4lz.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-O-pRN7VQ.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-BietvGAI.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-Bd2Cl62X.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-B6iDy69W.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-DQVHfKe0.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BWdVp2tQ.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-WLE6rKB4.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-BbjS-4gn.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-Dpy2QSGq.js} +1 -1
- package/dist/assets/index-BIMdIbaw.css +1 -0
- package/dist/assets/index-CHnODCr2.js +583 -0
- package/dist/assets/{index-h9FFNwTu.js → index-CIaHn5NS.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-D_jKWlKe.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-DLJohSuK.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DPOFj0qN.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-BI7v-xpK.js} +1 -1
- package/dist/assets/{linear-3su2TG93.js → linear-C2pWPSxO.js} +1 -1
- package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-CR-j5WQU.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-lz9GvXRh.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-Dl4_ia61.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-DWuT3n88.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-DXWe9Jx7.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-COY1IBVY.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-DnyVuhsQ.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-BSc4NdGK.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-kqEU2EGq.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-BvB4bkWD.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BIrqNv7y.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-CFnalQUE.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-CsSWJnSQ.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-DkAmJ1EE.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-C3vd9drL.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-j26CM-pi.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-BYu1Q_at.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +2 -1
- package/dist-server/companion/src/routes.js +8 -1
- package/dist-server/connector-proxy.js +105 -1
- package/dist-server/container-mcp.js +1032 -165
- package/dist-server/drivers/agents-proxy.js +338 -29
- package/dist-server/drivers/phone-proxy.js +33 -2
- package/dist-server/index.js +40590 -31811
- package/dist-server/local-computer-proxy.js +5 -3
- package/dist-server/local-computer.js +53 -17
- package/dist-server/mcp-gate.js +2 -0
- package/dist-server/mcp-server.js +5 -3
- package/dist-server/message-search.worker.js +52 -0
- package/dist-server/openmausbot.js +4457 -1399
- package/dist-server/pair-cli.js +4457 -1399
- package/dist-server/permission-proxy.js +29 -9
- package/dist-server/server/admission.js +81 -0
- package/dist-server/server/aside-queue.js +294 -0
- package/dist-server/server/attachments.js +98 -1
- package/dist-server/server/auto-approve.js +4 -3
- package/dist-server/server/auto-vm-claims.js +1 -0
- package/dist-server/server/bot-attachment.js +107 -0
- package/dist-server/server/bot-overview.js +19 -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-api-setup.js +1 -0
- package/dist-server/server/cli.js +2 -2
- package/dist-server/server/command-allowlist.js +154 -0
- package/dist-server/server/composio.js +297 -1
- package/dist-server/server/computer-wait.js +22 -9
- package/dist-server/server/config.js +84 -12
- package/dist-server/server/connector-advertisement.js +165 -0
- package/dist-server/server/connector-proxy.js +14 -2
- package/dist-server/server/connector-verdict.js +168 -0
- package/dist-server/server/container-computer.js +61 -1
- package/dist-server/server/contracts.js +22 -0
- 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 +80 -20
- package/dist-server/server/drivers/acp/cursor.js +3 -7
- package/dist-server/server/drivers/acp/droid.js +4 -7
- package/dist-server/server/drivers/acp/gemini.js +4 -4
- package/dist-server/server/drivers/acp/grok.js +4 -9
- package/dist-server/server/drivers/acp/hermes.js +3 -5
- package/dist-server/server/drivers/acp/opencode-go.js +2 -5
- package/dist-server/server/drivers/acp/qwen.js +9 -15
- package/dist-server/server/drivers/agents-call.js +179 -13
- package/dist-server/server/drivers/agents-catalog.js +134 -10
- package/dist-server/server/drivers/boxagent.js +160 -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 +38 -19
- package/dist-server/server/drivers/codex-catalog.js +14 -6
- package/dist-server/server/drivers/codex-identity.js +2 -1
- package/dist-server/server/drivers/codex-instructions.js +48 -6
- package/dist-server/server/drivers/codex-release.js +48 -0
- package/dist-server/server/drivers/codex-tool-surface.js +20 -0
- package/dist-server/server/drivers/codex.js +102 -55
- package/dist-server/server/drivers/mistral.js +115 -0
- package/dist-server/server/drivers/openai-chat.js +180 -33
- 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 +306 -28
- package/dist-server/server/drivers/prompt-split.js +110 -0
- package/dist-server/server/drivers/session-idle.js +29 -0
- package/dist-server/server/env-path.js +15 -0
- package/dist-server/server/fleet.js +1 -1
- package/dist-server/server/group-thread-usage.js +103 -0
- package/dist-server/server/guarded-requests.js +2 -2
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/harness-capabilities.js +46 -0
- package/dist-server/server/hosted-models.js +1 -1
- package/dist-server/server/index.js +2396 -505
- 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/message-db.js +22 -42
- package/dist-server/server/message-file.js +9 -1
- package/dist-server/server/message-search-query.js +43 -0
- package/dist-server/server/message-search-worker.js +80 -0
- package/dist-server/server/message-search.worker.js +21 -0
- package/dist-server/server/model-requests.js +248 -0
- package/dist-server/server/new-bot-defaults.js +115 -0
- package/dist-server/server/options-card.js +21 -0
- package/dist-server/server/org-library.js +1065 -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-delivery.js +60 -0
- package/dist-server/server/peer-roster.js +67 -0
- package/dist-server/server/presets.js +492 -0
- package/dist-server/server/profile-requests.js +66 -39
- package/dist-server/server/profile-revision.js +9 -1
- package/dist-server/server/profile-versions.js +6 -3
- 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 +35 -10
- package/dist-server/server/routines.js +41 -1
- package/dist-server/server/section-context.js +10 -0
- package/dist-server/server/skill-fetch.js +72 -14
- package/dist-server/server/skills.js +35 -61
- package/dist-server/server/steer-queue.js +3 -2
- package/dist-server/server/store.js +291 -17
- package/dist-server/server/surface.js +10 -4
- package/dist-server/server/system-prompt.js +38 -6
- 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 +139 -27
- package/dist-server/server/thread-events.js +20 -2
- package/dist-server/server/tightening-requests.js +405 -0
- package/dist-server/server/tts/chatterbox.js +27 -5
- 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 +19 -2
- 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 +42 -5
- package/dist-server/server/workspace-backup.worker.js +10 -0
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +207 -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 +6 -2
- package/dist-server/shared/line-diff.js +23 -0
- package/dist-server/shared/model-request.js +1 -0
- package/dist-server/shared/options-card.js +51 -0
- package/dist-server/shared/package-format.js +967 -0
- package/dist-server/shared/profile-request.js +5 -4
- package/dist-server/shared/skill-md.js +64 -0
- package/dist-server/shared/team-backup.js +7 -0
- package/dist-server/shared/tightening-request.js +87 -0
- package/dist-server/shared/wire.js +4 -0
- package/dist-server/src/lib/composer-attachments.js +25 -3
- package/dist-server/vps-container-mcp.js +1132 -171
- package/dist-server/workspace-backup.worker.js +19474 -0
- 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,967 @@
|
|
|
1
|
+
// The OpenMausBot package file ("openmaus.package"): one team, or a library
|
|
2
|
+
// of skills and preset bots, as a single portable document.
|
|
3
|
+
//
|
|
4
|
+
// This module is the single validation gate for that file. The server, the
|
|
5
|
+
// renderer's import preview and the Admin upload (through the pinned
|
|
6
|
+
// runtime) all parse with exactly this code, so it may only import zod, yaml,
|
|
7
|
+
// croner (via routine-schedule) and the other pure modules in shared/. No
|
|
8
|
+
// Buffer, no node:fs, no node:crypto: TextEncoder and atob only.
|
|
9
|
+
//
|
|
10
|
+
// Two versions exist. v1 is the original team package (JSON or the BotMRR
|
|
11
|
+
// Markdown playbook). v2 adds the team brief, pictures, starter notes,
|
|
12
|
+
// connection slots, presets, room goals and library-only packages. Every
|
|
13
|
+
// reader here accepts both and always returns v2.
|
|
14
|
+
//
|
|
15
|
+
// What never travels is excluded by construction: the schema has no field
|
|
16
|
+
// for chat history, secrets, model choices, computers, visibility or
|
|
17
|
+
// personal settings, and unknown fields are stripped. Secrets written INSIDE
|
|
18
|
+
// free text are a separate problem, handled by redactPackageSecrets (export)
|
|
19
|
+
// and packageSecretFindings (Admin upload).
|
|
20
|
+
import { parse as parseYaml } from "yaml";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import { BOT_PROFILE_LIMITS, fitsOnOneLine } from "./bot-profile.js";
|
|
23
|
+
import { redactSecretsInText } from "./redact.js";
|
|
24
|
+
import { normalizeCronSchedule } from "./routine-schedule.js";
|
|
25
|
+
import { isSkillName, parseSkillMd, scanSkillText, SKILL_FILE_MAX_BYTES } from "./skill-md.js";
|
|
26
|
+
export const PACKAGE_FORMAT = "openmaus.package";
|
|
27
|
+
/** The newest version this module reads and writes. */
|
|
28
|
+
export const PACKAGE_VERSION = 2;
|
|
29
|
+
/** Canonical UTF-8 bytes of the whole document. */
|
|
30
|
+
export const PACKAGE_MAX_BYTES = 4 * 1024 * 1024;
|
|
31
|
+
export const PACKAGE_MAX_SKILLS = 60, AGENT_MAX_SKILLS = 30, PACKAGE_MAX_PRESETS = 20, PACKAGE_MAX_CONNECTIONS = 20;
|
|
32
|
+
/** Decoded picture bytes. */
|
|
33
|
+
export const AVATAR_MAX_BYTES = 64 * 1024;
|
|
34
|
+
export const SEED_FILE_MAX_BYTES = 256 * 1024, SEED_MAX_FILES = 100, SEED_MAX_BYTES = 1024 * 1024;
|
|
35
|
+
export const TEAM_BRIEF_MAX_BYTES = 24_000, RELEASE_NOTES_MAX = 4_000;
|
|
36
|
+
/** v1 limits, kept exactly as the v1 reader always enforced them. */
|
|
37
|
+
export const PACKAGE_V1_VERSION = 1;
|
|
38
|
+
export const PACKAGE_V1_MAX_SKILLS = 20;
|
|
39
|
+
export const BOTMRR_MARKDOWN_VERSION = 1;
|
|
40
|
+
const BOTMRR_MARKDOWN_MAX_BYTES = 1_000_000;
|
|
41
|
+
export const NEWER_PACKAGE_MESSAGE = "This file was made by a newer OpenMausBot. Update the app, then import it again.";
|
|
42
|
+
export class PackageFormatError extends Error {
|
|
43
|
+
code;
|
|
44
|
+
constructor(code, message) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = "PackageFormatError";
|
|
47
|
+
this.code = code;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const COLORS = ["green", "blue", "red", "orange", "purple", "cyan", "pink", "yellow", "teal", "coral"];
|
|
51
|
+
const AVATAR_MIMES = ["image/png", "image/jpeg", "image/webp"];
|
|
52
|
+
const AVATAR_CROPS = ["circle", "rounded", "square"];
|
|
53
|
+
const encoder = new TextEncoder();
|
|
54
|
+
/** UTF-8 byte length, the unit every byte cap here is written in. */
|
|
55
|
+
export function utf8Bytes(value) {
|
|
56
|
+
return encoder.encode(value).length;
|
|
57
|
+
}
|
|
58
|
+
// ── shared field helpers (identical to the v1 reader) ─────────────────────
|
|
59
|
+
const requiredText = (max) => z.string({ error: "must be text" }).trim().min(1, { message: "is required" }).max(max, { message: "is too long" });
|
|
60
|
+
const optionalText = (max) => z
|
|
61
|
+
.union([z.string({ error: "must be text" }), z.null(), z.undefined()])
|
|
62
|
+
.transform((value) => value?.trim() || undefined)
|
|
63
|
+
.refine((value) => value === undefined || value.length <= max, { message: "is too long" })
|
|
64
|
+
.optional();
|
|
65
|
+
const oneLine = (max) => requiredText(max).refine(fitsOnOneLine, { message: "must fit on one line" });
|
|
66
|
+
const optionalLine = (max) => optionalText(max).refine((value) => value === undefined || fitsOnOneLine(value), { message: "must fit on one line" });
|
|
67
|
+
const key = requiredText(64).regex(/^[a-z0-9][a-z0-9_-]*$/, {
|
|
68
|
+
message: "may only contain lowercase letters, numbers, - and _",
|
|
69
|
+
});
|
|
70
|
+
const MAX_DATE_MS = 8_640_000_000_000_000;
|
|
71
|
+
const skillName = requiredText(64).refine(isSkillName, { message: "must be a lowercase skill name" });
|
|
72
|
+
const portableSource = optionalText(2_000).refine((value) => value === undefined || !/^(?:[a-z]:[\\/]|[\\/]|\\\\|file:)/i.test(value), { message: "must not be an absolute path" });
|
|
73
|
+
const skillInstructions = z
|
|
74
|
+
.string({ error: "must be text" })
|
|
75
|
+
.min(1, { message: "is required" })
|
|
76
|
+
.max(SKILL_FILE_MAX_BYTES, { message: "is too long" })
|
|
77
|
+
.refine((value) => utf8Bytes(value) <= SKILL_FILE_MAX_BYTES, { message: "is too large" })
|
|
78
|
+
.refine((value) => /^---\r?\n/.test(value), { message: "must start with SKILL.md frontmatter" });
|
|
79
|
+
const portableSkillSchema = z.object({
|
|
80
|
+
name: skillName,
|
|
81
|
+
description: requiredText(1_024),
|
|
82
|
+
source: portableSource,
|
|
83
|
+
license: optionalText(200),
|
|
84
|
+
compatibility: optionalText(200),
|
|
85
|
+
instructions: skillInstructions,
|
|
86
|
+
});
|
|
87
|
+
const soulSchema = z.string().refine((value) => utf8Bytes(value) <= BOT_PROFILE_LIMITS.soul, {
|
|
88
|
+
error: "standing instructions must be at most 24000 bytes",
|
|
89
|
+
});
|
|
90
|
+
const CLOCK_TIME = /^([01]\d|2[0-3]):[0-5]\d$/;
|
|
91
|
+
const intervalWeekdays = z.array(z.number().int().min(0).max(6)).min(1).max(7).refine((weekdays) => new Set(weekdays).size === weekdays.length, "must contain unique weekdays");
|
|
92
|
+
const intervalWindow = z.object({
|
|
93
|
+
start: z.string().regex(CLOCK_TIME, { message: "must use HH:MM" }),
|
|
94
|
+
end: z.string().regex(CLOCK_TIME, { message: "must use HH:MM" }),
|
|
95
|
+
}).strict().refine(({ start, end }) => start < end, {
|
|
96
|
+
message: "must end later on the same day",
|
|
97
|
+
});
|
|
98
|
+
/** Unchanged from v1: once, daily, interval and cron. */
|
|
99
|
+
export const packageRoutineScheduleSchema = z.discriminatedUnion("type", [
|
|
100
|
+
z.object({ type: z.literal("cron"), expression: requiredText(256), timeZone: requiredText(100) }).strict(),
|
|
101
|
+
z.object({ type: z.literal("once"), at: z.number().int() }),
|
|
102
|
+
z.object({
|
|
103
|
+
type: z.literal("daily"),
|
|
104
|
+
time: requiredText(5).regex(CLOCK_TIME, { message: "must use HH:MM" }),
|
|
105
|
+
weekdays: z.array(z.number().int().min(0).max(6)).min(1).max(7),
|
|
106
|
+
}),
|
|
107
|
+
z.object({
|
|
108
|
+
type: z.literal("interval"),
|
|
109
|
+
everyMinutes: z.number().int().min(5).max(1_440),
|
|
110
|
+
anchorAt: z.number().int().nonnegative().max(MAX_DATE_MS),
|
|
111
|
+
weekdays: intervalWeekdays.optional(),
|
|
112
|
+
window: intervalWindow.optional(),
|
|
113
|
+
endsAt: z.number().int().nonnegative().max(MAX_DATE_MS).optional(),
|
|
114
|
+
}),
|
|
115
|
+
]).superRefine((schedule, context) => {
|
|
116
|
+
if (schedule.type === "cron") {
|
|
117
|
+
try {
|
|
118
|
+
normalizeCronSchedule(schedule);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
context.addIssue({ code: "custom", message: error instanceof Error ? error.message : "Invalid cron schedule" });
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (schedule.type !== "interval")
|
|
126
|
+
return;
|
|
127
|
+
if (schedule.window) {
|
|
128
|
+
const [startHour, startMinute] = schedule.window.start.split(":").map(Number);
|
|
129
|
+
const [endHour, endMinute] = schedule.window.end.split(":").map(Number);
|
|
130
|
+
const windowMinutes = endHour * 60 + endMinute - (startHour * 60 + startMinute);
|
|
131
|
+
if (windowMinutes < schedule.everyMinutes) {
|
|
132
|
+
context.addIssue({
|
|
133
|
+
code: "custom",
|
|
134
|
+
message: "must be at least as long as the interval cadence",
|
|
135
|
+
path: ["window"],
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (schedule.endsAt !== undefined && schedule.endsAt < schedule.anchorAt) {
|
|
140
|
+
context.addIssue({
|
|
141
|
+
code: "custom",
|
|
142
|
+
message: "must not be before the interval anchor",
|
|
143
|
+
path: ["endsAt"],
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
const playbookSchema = z.object({
|
|
148
|
+
key,
|
|
149
|
+
name: requiredText(100),
|
|
150
|
+
summary: requiredText(300),
|
|
151
|
+
triggers: z.array(requiredText(100)).min(1).max(30),
|
|
152
|
+
instructions: requiredText(24_000),
|
|
153
|
+
});
|
|
154
|
+
const requirementsSchema = z.object({
|
|
155
|
+
apps: z.array(z.object({
|
|
156
|
+
slug: key,
|
|
157
|
+
label: requiredText(100),
|
|
158
|
+
reason: requiredText(240),
|
|
159
|
+
optional: z.boolean().optional(),
|
|
160
|
+
})).max(30),
|
|
161
|
+
capabilities: z.array(requiredText(80)).max(20),
|
|
162
|
+
platforms: z.array(requiredText(80)).max(10).optional(),
|
|
163
|
+
});
|
|
164
|
+
const roomSchema = z.object({
|
|
165
|
+
key,
|
|
166
|
+
name: requiredText(100),
|
|
167
|
+
members: z.array(key).min(1).max(200),
|
|
168
|
+
bulletin: optionalText(12_000),
|
|
169
|
+
defaultResponder: z.discriminatedUnion("kind", [
|
|
170
|
+
z.object({ kind: z.literal("agent"), agent: key }),
|
|
171
|
+
z.object({ kind: z.literal("everyone") }),
|
|
172
|
+
z.object({ kind: z.literal("mentions") }),
|
|
173
|
+
]),
|
|
174
|
+
});
|
|
175
|
+
const exampleSchema = z.object({
|
|
176
|
+
title: requiredText(120),
|
|
177
|
+
input: requiredText(4_000),
|
|
178
|
+
output: requiredText(8_000),
|
|
179
|
+
});
|
|
180
|
+
// ── v1 (read only; upgraded on the way in, produced only by downgradeToV1) ─
|
|
181
|
+
const packageDocumentV1Schema = z.object({
|
|
182
|
+
format: z.literal(PACKAGE_FORMAT, { error: "This is not an OpenMaus package" }),
|
|
183
|
+
version: z.literal(PACKAGE_V1_VERSION, { error: "Package version is not supported" }),
|
|
184
|
+
package: z.object({
|
|
185
|
+
id: requiredText(80).regex(/^[a-z0-9][a-z0-9-]*$/, { message: "must be a lowercase slug" }),
|
|
186
|
+
release: requiredText(30).regex(/^\d+\.\d+\.\d+$/, { message: "must be semantic versioning" }),
|
|
187
|
+
name: requiredText(100),
|
|
188
|
+
tagline: requiredText(160),
|
|
189
|
+
summary: requiredText(2_000),
|
|
190
|
+
category: requiredText(80),
|
|
191
|
+
author: z.object({ name: requiredText(100), url: optionalText(500) }),
|
|
192
|
+
license: requiredText(80),
|
|
193
|
+
featured: z.boolean().optional(),
|
|
194
|
+
tags: z.array(requiredText(80)).max(30).optional(),
|
|
195
|
+
outcomes: z.array(requiredText(240)).min(1).max(12),
|
|
196
|
+
setupMinutes: z.number().int().min(1).max(240),
|
|
197
|
+
requirements: requirementsSchema,
|
|
198
|
+
agents: z.array(z.object({
|
|
199
|
+
key,
|
|
200
|
+
name: requiredText(100),
|
|
201
|
+
title: optionalText(200),
|
|
202
|
+
description: optionalText(4_000),
|
|
203
|
+
soul: soulSchema.optional(),
|
|
204
|
+
appearance: z.object({
|
|
205
|
+
color: z.enum(COLORS, { error: "is not supported" }),
|
|
206
|
+
mascotExpression: optionalText(80),
|
|
207
|
+
mascotBody: optionalText(40),
|
|
208
|
+
}),
|
|
209
|
+
playbooks: z.array(key).max(40).optional(),
|
|
210
|
+
skills: z.array(skillName).max(PACKAGE_V1_MAX_SKILLS).optional(),
|
|
211
|
+
})).min(1).max(200),
|
|
212
|
+
chiefOfStaff: key.optional(),
|
|
213
|
+
rooms: z.array(roomSchema).max(30).optional(),
|
|
214
|
+
routines: z.array(z.object({
|
|
215
|
+
key,
|
|
216
|
+
name: requiredText(80),
|
|
217
|
+
agent: key,
|
|
218
|
+
prompt: requiredText(20_000),
|
|
219
|
+
runOn: z.enum(["maus", "cloud"]),
|
|
220
|
+
schedule: packageRoutineScheduleSchema,
|
|
221
|
+
durationMinutes: z.number().int().min(5).max(240),
|
|
222
|
+
timeoutMinutes: z.number().int().min(5).max(240).optional(),
|
|
223
|
+
overlap: z.enum(["skip", "queue"]).optional(),
|
|
224
|
+
enabledAfterInstall: z.literal(false),
|
|
225
|
+
})).max(50).optional(),
|
|
226
|
+
playbooks: z.array(playbookSchema).max(80).optional(),
|
|
227
|
+
skills: z.object({
|
|
228
|
+
version: z.literal(1),
|
|
229
|
+
entries: z.array(portableSkillSchema).min(1).max(PACKAGE_V1_MAX_SKILLS),
|
|
230
|
+
}).optional(),
|
|
231
|
+
examples: z.array(exampleSchema).max(12).optional(),
|
|
232
|
+
}),
|
|
233
|
+
});
|
|
234
|
+
// ── v2 ─────────────────────────────────────────────────────────────────────
|
|
235
|
+
const semver = z.string({ error: "must be text" }).regex(/^(0|[1-9]\d{0,5})\.(0|[1-9]\d{0,5})\.(0|[1-9]\d{0,5})$/, {
|
|
236
|
+
message: "must be a version like 1.2.3",
|
|
237
|
+
});
|
|
238
|
+
const slug = requiredText(80).regex(/^[a-z0-9][a-z0-9-]*$/, { message: "must be a lowercase slug" });
|
|
239
|
+
const orgSlug = z.string({ error: "must be text" }).regex(/^[a-z][a-z0-9-]{1,30}$/, { message: "must be an organization's short name" });
|
|
240
|
+
/** The MCP server name rule, with room left for a "-NN" suffix on import. */
|
|
241
|
+
const connKey = z.string({ error: "must be text" }).regex(/^[a-z][a-z0-9_-]{0,27}$/, {
|
|
242
|
+
message: "must be 1-28 lowercase letters, numbers, - or _, starting with a letter",
|
|
243
|
+
});
|
|
244
|
+
const headerName = z.string({ error: "must be text" }).regex(/^[!#$%&'*+.^_`|~0-9A-Za-z-]{1,128}$/, {
|
|
245
|
+
message: "must be a valid header name",
|
|
246
|
+
});
|
|
247
|
+
/** atob + bytes, or null when the text is not base64. */
|
|
248
|
+
export function decodeBase64(value) {
|
|
249
|
+
let binary;
|
|
250
|
+
try {
|
|
251
|
+
binary = atob(value);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const bytes = new Uint8Array(binary.length);
|
|
257
|
+
for (let i = 0; i < binary.length; i += 1)
|
|
258
|
+
bytes[i] = binary.charCodeAt(i);
|
|
259
|
+
return bytes;
|
|
260
|
+
}
|
|
261
|
+
/** The image really is the type it claims: no SVG or HTML behind an image mime. */
|
|
262
|
+
export function pictureMatchesMime(bytes, mime) {
|
|
263
|
+
const starts = (...values) => values.every((value, index) => bytes[index] === value);
|
|
264
|
+
if (mime === "image/png")
|
|
265
|
+
return starts(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a);
|
|
266
|
+
if (mime === "image/jpeg")
|
|
267
|
+
return starts(0xff, 0xd8, 0xff);
|
|
268
|
+
if (mime === "image/webp") {
|
|
269
|
+
return starts(0x52, 0x49, 0x46, 0x46) && bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50;
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
const avatarSchema = z.object({
|
|
274
|
+
mime: z.enum(AVATAR_MIMES, { error: "must be a PNG, JPEG or WebP picture" }),
|
|
275
|
+
// base64 of at most AVATAR_MAX_BYTES
|
|
276
|
+
data: z.string({ error: "must be text" }).max(87_384, { message: "is larger than 64 KB" }).regex(/^[A-Za-z0-9+/]+={0,2}$/, {
|
|
277
|
+
message: "must be base64",
|
|
278
|
+
}),
|
|
279
|
+
crop: z.enum(AVATAR_CROPS, { error: "must be circle, rounded or square" }),
|
|
280
|
+
}).superRefine((avatar, context) => {
|
|
281
|
+
const bytes = decodeBase64(avatar.data);
|
|
282
|
+
if (!bytes) {
|
|
283
|
+
context.addIssue({ code: "custom", path: ["data"], message: "must be base64" });
|
|
284
|
+
}
|
|
285
|
+
else if (bytes.length > AVATAR_MAX_BYTES) {
|
|
286
|
+
context.addIssue({ code: "custom", path: ["data"], message: "is larger than 64 KB" });
|
|
287
|
+
}
|
|
288
|
+
else if (!pictureMatchesMime(bytes, avatar.mime)) {
|
|
289
|
+
context.addIssue({ code: "custom", path: ["data"], message: `is not a ${avatar.mime.slice(6).toUpperCase()} picture` });
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
const appearanceSchema = z.object({
|
|
293
|
+
color: z.enum(COLORS, { error: "is not supported" }),
|
|
294
|
+
mascotExpression: optionalText(80),
|
|
295
|
+
mascotBody: optionalText(40),
|
|
296
|
+
avatar: avatarSchema.optional(),
|
|
297
|
+
});
|
|
298
|
+
/** Starter notes travel as MEMORY.md plus topic files. Daily logs never do. */
|
|
299
|
+
export function isSeedPath(path) {
|
|
300
|
+
return path === "MEMORY.md" || (path.startsWith("memory/") && /^[\w][\w .-]{0,199}\.md$/.test(path.slice(7)));
|
|
301
|
+
}
|
|
302
|
+
const seedSchema = z.object({
|
|
303
|
+
memory: z.record(z.string(), z.string({ error: "must be text" })),
|
|
304
|
+
}).superRefine((seed, context) => {
|
|
305
|
+
const entries = Object.entries(seed.memory);
|
|
306
|
+
let total = 0;
|
|
307
|
+
for (const [path, text] of entries) {
|
|
308
|
+
if (!isSeedPath(path)) {
|
|
309
|
+
context.addIssue({ code: "custom", path: ["memory", path], message: "must be MEMORY.md or memory/<topic>.md; daily logs never travel" });
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const bytes = utf8Bytes(text);
|
|
313
|
+
total += bytes;
|
|
314
|
+
if (bytes > SEED_FILE_MAX_BYTES)
|
|
315
|
+
context.addIssue({ code: "custom", path: ["memory", path], message: "is larger than 256 KB" });
|
|
316
|
+
}
|
|
317
|
+
if (entries.length > SEED_MAX_FILES)
|
|
318
|
+
context.addIssue({ code: "custom", path: ["memory"], message: `has more than ${SEED_MAX_FILES} files` });
|
|
319
|
+
if (total > SEED_MAX_BYTES)
|
|
320
|
+
context.addIssue({ code: "custom", path: ["memory"], message: "is larger than 1 MB in total" });
|
|
321
|
+
});
|
|
322
|
+
const agentSchema = z.object({
|
|
323
|
+
key,
|
|
324
|
+
name: oneLine(100),
|
|
325
|
+
title: optionalLine(200),
|
|
326
|
+
description: optionalText(4_000),
|
|
327
|
+
soul: soulSchema.optional(),
|
|
328
|
+
appearance: appearanceSchema,
|
|
329
|
+
/** A suggestion only: every imported bot starts on Ask. */
|
|
330
|
+
approval: z.enum(["ask", "auto"], { error: "must be ask or auto" }).optional(),
|
|
331
|
+
playbooks: z.array(key).max(40).optional(),
|
|
332
|
+
skills: z.array(skillName).max(AGENT_MAX_SKILLS).optional(),
|
|
333
|
+
connections: z.array(connKey).max(PACKAGE_MAX_CONNECTIONS).optional(),
|
|
334
|
+
seed: seedSchema.optional(),
|
|
335
|
+
});
|
|
336
|
+
const routineSchema = z.object({
|
|
337
|
+
key,
|
|
338
|
+
name: requiredText(80),
|
|
339
|
+
/** The owner; for a room goal, the lead coordinator. */
|
|
340
|
+
agent: key,
|
|
341
|
+
/** Present = a room goal (runs on this computer, no continuity). */
|
|
342
|
+
room: key.optional(),
|
|
343
|
+
prompt: requiredText(20_000),
|
|
344
|
+
runOn: z.enum(["maus", "cloud"]),
|
|
345
|
+
schedule: packageRoutineScheduleSchema,
|
|
346
|
+
durationMinutes: z.number().int().min(5).max(240),
|
|
347
|
+
timeoutMinutes: z.number().int().min(1).max(1_440).optional(),
|
|
348
|
+
overlap: z.enum(["skip", "queue"]).optional(),
|
|
349
|
+
continuity: z.boolean().optional(),
|
|
350
|
+
/** The publisher's intent. File imports always arrive paused. */
|
|
351
|
+
enabledAfterInstall: z.boolean(),
|
|
352
|
+
});
|
|
353
|
+
/** Preset bots: an allowlist. No approval, computer, browser, MCP, apps or model. */
|
|
354
|
+
const presetSchema = z.object({
|
|
355
|
+
key,
|
|
356
|
+
name: oneLine(100),
|
|
357
|
+
description: optionalText(300),
|
|
358
|
+
bot: z.object({
|
|
359
|
+
name: optionalLine(100),
|
|
360
|
+
title: optionalLine(200),
|
|
361
|
+
description: optionalText(4_000),
|
|
362
|
+
soul: soulSchema.optional(),
|
|
363
|
+
appearance: appearanceSchema.optional(),
|
|
364
|
+
}),
|
|
365
|
+
playbooks: z.array(key).max(40).optional(),
|
|
366
|
+
skills: z.array(skillName).max(AGENT_MAX_SKILLS).optional(),
|
|
367
|
+
seed: seedSchema.optional(),
|
|
368
|
+
});
|
|
369
|
+
function isLoopbackHost(hostname) {
|
|
370
|
+
return hostname === "localhost" || hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(hostname);
|
|
371
|
+
}
|
|
372
|
+
function parsedUrl(value) {
|
|
373
|
+
try {
|
|
374
|
+
return new URL(value);
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/** A credential SLOT: the address and the names of the values it needs. */
|
|
381
|
+
const connectionSchema = z.object({
|
|
382
|
+
key: connKey,
|
|
383
|
+
label: requiredText(100),
|
|
384
|
+
reason: requiredText(240),
|
|
385
|
+
mcp: z.object({
|
|
386
|
+
transport: z.enum(["http", "sse"], { error: "must be http or sse; a package never carries a command to run" }),
|
|
387
|
+
url: requiredText(2_000)
|
|
388
|
+
.refine((value) => {
|
|
389
|
+
const url = parsedUrl(value);
|
|
390
|
+
return Boolean(url && (url.protocol === "https:" || (url.protocol === "http:" && isLoopbackHost(url.hostname))));
|
|
391
|
+
}, { message: "must be an https:// address (http:// only for this computer)" })
|
|
392
|
+
.refine((value) => {
|
|
393
|
+
const url = parsedUrl(value);
|
|
394
|
+
return !url || (!url.username && !url.password);
|
|
395
|
+
}, { message: "must not contain a user name or password" }),
|
|
396
|
+
valueNames: z.array(headerName).max(32).refine((names) => new Set(names.map((name) => name.toLowerCase())).size === names.length, {
|
|
397
|
+
message: "must not repeat a header",
|
|
398
|
+
}),
|
|
399
|
+
}),
|
|
400
|
+
});
|
|
401
|
+
export const packageDocumentSchema = z.object({
|
|
402
|
+
format: z.literal(PACKAGE_FORMAT, { error: "This is not an OpenMaus package" }),
|
|
403
|
+
version: z.literal(PACKAGE_VERSION, { error: "Package version is not supported" }),
|
|
404
|
+
package: z.object({
|
|
405
|
+
/** The package slug: its identity across releases. */
|
|
406
|
+
id: slug,
|
|
407
|
+
release: semver,
|
|
408
|
+
name: requiredText(100),
|
|
409
|
+
tagline: requiredText(160),
|
|
410
|
+
summary: requiredText(2_000),
|
|
411
|
+
notes: optionalText(RELEASE_NOTES_MAX),
|
|
412
|
+
category: requiredText(80),
|
|
413
|
+
author: z.object({ name: requiredText(100), url: optionalText(500) }),
|
|
414
|
+
license: requiredText(80),
|
|
415
|
+
featured: z.boolean().optional(),
|
|
416
|
+
tags: z.array(requiredText(80)).max(30).optional(),
|
|
417
|
+
outcomes: z.array(requiredText(240)).min(1).max(12),
|
|
418
|
+
setupMinutes: z.number().int().min(1).max(240),
|
|
419
|
+
/** Stamped by Admin only; a file never chooses it (trust "file" drops it). */
|
|
420
|
+
publisher: z.object({ organization: orgSlug, name: requiredText(100) }).optional(),
|
|
421
|
+
requirements: requirementsSchema,
|
|
422
|
+
team: z.object({
|
|
423
|
+
name: requiredText(60),
|
|
424
|
+
brief: z.string({ error: "must be text" }).refine((value) => utf8Bytes(value) <= TEAM_BRIEF_MAX_BYTES, {
|
|
425
|
+
message: `must be at most ${TEAM_BRIEF_MAX_BYTES} bytes`,
|
|
426
|
+
}).optional(),
|
|
427
|
+
/** The team's Chief of Staff. */
|
|
428
|
+
leader: key.optional(),
|
|
429
|
+
}).optional(),
|
|
430
|
+
agents: z.array(agentSchema).max(200).default([]),
|
|
431
|
+
rooms: z.array(roomSchema).max(30).optional(),
|
|
432
|
+
routines: z.array(routineSchema).max(50).optional(),
|
|
433
|
+
playbooks: z.array(playbookSchema).max(80).optional(),
|
|
434
|
+
skills: z.object({
|
|
435
|
+
version: z.literal(1),
|
|
436
|
+
entries: z.array(portableSkillSchema).min(1).max(PACKAGE_MAX_SKILLS),
|
|
437
|
+
}).optional(),
|
|
438
|
+
presets: z.array(presetSchema).max(PACKAGE_MAX_PRESETS).optional(),
|
|
439
|
+
connections: z.array(connectionSchema).max(PACKAGE_MAX_CONNECTIONS).optional(),
|
|
440
|
+
examples: z.array(exampleSchema).max(12).optional(),
|
|
441
|
+
}),
|
|
442
|
+
});
|
|
443
|
+
// ── part paths ─────────────────────────────────────────────────────────────
|
|
444
|
+
/** Lists whose items are named by `key` in part paths: agents[scout].soul. */
|
|
445
|
+
const KEYED_LISTS = new Set(["agents", "rooms", "routines", "playbooks", "presets", "connections"]);
|
|
446
|
+
function segmentsAfter(head, rest) {
|
|
447
|
+
let out = head;
|
|
448
|
+
for (const segment of rest) {
|
|
449
|
+
if (typeof segment === "number")
|
|
450
|
+
out += `[${segment}]`;
|
|
451
|
+
else if (out.endsWith(".seed.memory"))
|
|
452
|
+
out += `[${JSON.stringify(String(segment))}]`;
|
|
453
|
+
else
|
|
454
|
+
out += `.${String(segment)}`;
|
|
455
|
+
}
|
|
456
|
+
return out;
|
|
457
|
+
}
|
|
458
|
+
/** Map a schema path onto the part-path grammar shared with Admin:
|
|
459
|
+
* package.summary, team.brief, agents[scout].soul, skills[x].instructions,
|
|
460
|
+
* agents[scout].seed.memory["MEMORY.md"]. Keys come from the raw input. */
|
|
461
|
+
function describePath(input, path) {
|
|
462
|
+
if (path[0] !== "package")
|
|
463
|
+
return path.map(String).join(".") || "package";
|
|
464
|
+
const pkg = input && typeof input === "object" ? input.package : undefined;
|
|
465
|
+
const field = path[1];
|
|
466
|
+
if (field === undefined)
|
|
467
|
+
return "package";
|
|
468
|
+
const name = String(field);
|
|
469
|
+
const list = pkg && typeof pkg === "object" ? pkg[name] : undefined;
|
|
470
|
+
if (KEYED_LISTS.has(name)) {
|
|
471
|
+
const index = path[2];
|
|
472
|
+
if (typeof index !== "number")
|
|
473
|
+
return segmentsAfter(name, path.slice(2));
|
|
474
|
+
const item = Array.isArray(list) ? list[index] : undefined;
|
|
475
|
+
const id = item && typeof item === "object" ? item.key : undefined;
|
|
476
|
+
return segmentsAfter(`${name}[${typeof id === "string" && id.trim() ? id.trim() : index}]`, path.slice(3));
|
|
477
|
+
}
|
|
478
|
+
if (name === "skills" && path[2] === "entries" && typeof path[3] === "number") {
|
|
479
|
+
const entries = list && typeof list === "object" ? list.entries : undefined;
|
|
480
|
+
const entry = Array.isArray(entries) ? entries[path[3]] : undefined;
|
|
481
|
+
const id = entry && typeof entry === "object" ? entry.name : undefined;
|
|
482
|
+
return segmentsAfter(`skills[${typeof id === "string" && id.trim() ? id.trim() : path[3]}]`, path.slice(4));
|
|
483
|
+
}
|
|
484
|
+
if (name === "team")
|
|
485
|
+
return segmentsAfter("team", path.slice(2));
|
|
486
|
+
return segmentsAfter(`package.${name}`, path.slice(2));
|
|
487
|
+
}
|
|
488
|
+
function schemaSentence(error, input) {
|
|
489
|
+
const issue = error.issues[0];
|
|
490
|
+
if (!issue)
|
|
491
|
+
return "This is not a valid package";
|
|
492
|
+
if (issue.path.length <= 2 && (issue.path[0] === "format" || issue.path[0] === "version"))
|
|
493
|
+
return issue.message;
|
|
494
|
+
return `${describePath(input, issue.path)} ${issue.message}`;
|
|
495
|
+
}
|
|
496
|
+
/** The v1 reader's historical message shape ("package.agents.0.name is too long"). */
|
|
497
|
+
function v1SchemaSentence(error) {
|
|
498
|
+
const issue = error.issues[0];
|
|
499
|
+
if (!issue)
|
|
500
|
+
return "This is not a bot package";
|
|
501
|
+
const path = issue.path.map(String).join(".");
|
|
502
|
+
return path ? `${path} ${issue.message}` : issue.message;
|
|
503
|
+
}
|
|
504
|
+
// ── canonical form ─────────────────────────────────────────────────────────
|
|
505
|
+
/** Sorted keys (default UTF-16 sort), undefined omitted, no whitespace,
|
|
506
|
+
* leaves by JSON.stringify. The release sha256 is over exactly these bytes. */
|
|
507
|
+
export function canonicalJson(value) {
|
|
508
|
+
if (value === null || typeof value !== "object")
|
|
509
|
+
return JSON.stringify(value) ?? "null";
|
|
510
|
+
if (Array.isArray(value)) {
|
|
511
|
+
return `[${value.map((item) => (item === undefined ? "null" : canonicalJson(item))).join(",")}]`;
|
|
512
|
+
}
|
|
513
|
+
const record = value;
|
|
514
|
+
const keys = Object.keys(record).filter((name) => record[name] !== undefined).sort();
|
|
515
|
+
return `{${keys.map((name) => `${JSON.stringify(name)}:${canonicalJson(record[name])}`).join(",")}}`;
|
|
516
|
+
}
|
|
517
|
+
/** Canonical UTF-8 bytes of a document: what Admin stores and the cap measures. */
|
|
518
|
+
export function packageBytes(document) {
|
|
519
|
+
return utf8Bytes(canonicalJson(document));
|
|
520
|
+
}
|
|
521
|
+
function tooLarge() {
|
|
522
|
+
return new PackageFormatError("too_large", `This package is larger than ${PACKAGE_MAX_BYTES / (1024 * 1024)} MB.`);
|
|
523
|
+
}
|
|
524
|
+
// ── cross-references ───────────────────────────────────────────────────────
|
|
525
|
+
function invalid(message) {
|
|
526
|
+
return new PackageFormatError("invalid", message);
|
|
527
|
+
}
|
|
528
|
+
function uniqueKeys(values, label) {
|
|
529
|
+
const seen = new Set();
|
|
530
|
+
for (const value of values) {
|
|
531
|
+
if (seen.has(value))
|
|
532
|
+
throw invalid(`Duplicate ${label} key: ${value}`);
|
|
533
|
+
seen.add(value);
|
|
534
|
+
}
|
|
535
|
+
return seen;
|
|
536
|
+
}
|
|
537
|
+
function checkSkillEntries(entries) {
|
|
538
|
+
for (const skill of entries) {
|
|
539
|
+
const parsed = parseSkillMd(skill.instructions);
|
|
540
|
+
if ("error" in parsed)
|
|
541
|
+
throw invalid(`Skill ${skill.name} is invalid: ${parsed.error}`);
|
|
542
|
+
if (parsed.name !== skill.name)
|
|
543
|
+
throw invalid(`Skill ${skill.name} does not match its SKILL.md name`);
|
|
544
|
+
if (parsed.description !== skill.description)
|
|
545
|
+
throw invalid(`Skill ${skill.name} does not match its description`);
|
|
546
|
+
for (const field of ["license", "compatibility"]) {
|
|
547
|
+
if (skill[field] !== undefined && skill[field] !== parsed[field]) {
|
|
548
|
+
throw invalid(`Skill ${skill.name} does not match its ${field}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function checkV1References(pkg) {
|
|
554
|
+
const agents = uniqueKeys(pkg.agents.map((agent) => agent.key), "agent");
|
|
555
|
+
const playbooks = uniqueKeys((pkg.playbooks ?? []).map((playbook) => playbook.key), "playbook");
|
|
556
|
+
const skills = uniqueKeys((pkg.skills?.entries ?? []).map((skill) => skill.name), "skill");
|
|
557
|
+
uniqueKeys((pkg.rooms ?? []).map((room) => room.key), "room");
|
|
558
|
+
uniqueKeys((pkg.routines ?? []).map((routine) => routine.key), "routine");
|
|
559
|
+
if (pkg.chiefOfStaff && !agents.has(pkg.chiefOfStaff))
|
|
560
|
+
throw invalid(`Unknown Chief of Staff: ${pkg.chiefOfStaff}`);
|
|
561
|
+
for (const agent of pkg.agents) {
|
|
562
|
+
for (const playbook of agent.playbooks ?? []) {
|
|
563
|
+
if (!playbooks.has(playbook))
|
|
564
|
+
throw invalid(`Agent ${agent.key} references unknown playbook: ${playbook}`);
|
|
565
|
+
}
|
|
566
|
+
for (const skill of uniqueKeys(agent.skills ?? [], `skill in agent ${agent.key}`)) {
|
|
567
|
+
if (!skills.has(skill))
|
|
568
|
+
throw invalid(`Agent ${agent.key} references unknown skill: ${skill}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const referenced = new Set(pkg.agents.flatMap((agent) => agent.skills ?? []));
|
|
572
|
+
checkSkillEntries(pkg.skills?.entries ?? []);
|
|
573
|
+
for (const skill of pkg.skills?.entries ?? []) {
|
|
574
|
+
if (!referenced.has(skill.name))
|
|
575
|
+
throw invalid(`Skill ${skill.name} is not referenced by an agent`);
|
|
576
|
+
}
|
|
577
|
+
for (const room of pkg.rooms ?? []) {
|
|
578
|
+
const members = uniqueKeys(room.members, `member in room ${room.key}`);
|
|
579
|
+
for (const member of members) {
|
|
580
|
+
if (!agents.has(member))
|
|
581
|
+
throw invalid(`Room ${room.key} references unknown agent: ${member}`);
|
|
582
|
+
}
|
|
583
|
+
if (room.defaultResponder.kind === "agent" && !members.has(room.defaultResponder.agent)) {
|
|
584
|
+
throw invalid(`Room ${room.key} has an unknown default responder`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
for (const routine of pkg.routines ?? []) {
|
|
588
|
+
if (!agents.has(routine.agent))
|
|
589
|
+
throw invalid(`Routine ${routine.key} references unknown agent: ${routine.agent}`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function checkV2References(pkg) {
|
|
593
|
+
const isTeam = pkg.agents.length > 0;
|
|
594
|
+
if (isTeam && !pkg.team)
|
|
595
|
+
throw invalid("A package with bots needs a team");
|
|
596
|
+
if (!isTeam) {
|
|
597
|
+
if (pkg.team || pkg.rooms?.length || pkg.routines?.length) {
|
|
598
|
+
throw invalid("A package without bots cannot have a team, group chats or routines");
|
|
599
|
+
}
|
|
600
|
+
if (!pkg.skills?.entries.length && !pkg.presets?.length) {
|
|
601
|
+
throw invalid("A package without bots needs at least one skill or preset");
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
const agents = uniqueKeys(pkg.agents.map((agent) => agent.key), "agent");
|
|
605
|
+
const rooms = uniqueKeys((pkg.rooms ?? []).map((room) => room.key), "room");
|
|
606
|
+
uniqueKeys((pkg.routines ?? []).map((routine) => routine.key), "routine");
|
|
607
|
+
const playbooks = uniqueKeys((pkg.playbooks ?? []).map((playbook) => playbook.key), "playbook");
|
|
608
|
+
uniqueKeys((pkg.presets ?? []).map((preset) => preset.key), "preset");
|
|
609
|
+
const connections = uniqueKeys((pkg.connections ?? []).map((connection) => connection.key), "connection");
|
|
610
|
+
const skills = uniqueKeys((pkg.skills?.entries ?? []).map((skill) => skill.name), "skill");
|
|
611
|
+
if (pkg.team?.leader && !agents.has(pkg.team.leader))
|
|
612
|
+
throw invalid(`Unknown team leader: ${pkg.team.leader}`);
|
|
613
|
+
const checkAssignments = (owner, assigned) => {
|
|
614
|
+
for (const playbook of uniqueKeys(assigned.playbooks ?? [], `playbook in ${owner}`)) {
|
|
615
|
+
if (!playbooks.has(playbook))
|
|
616
|
+
throw invalid(`${owner} references unknown playbook: ${playbook}`);
|
|
617
|
+
}
|
|
618
|
+
for (const skill of uniqueKeys(assigned.skills ?? [], `skill in ${owner}`)) {
|
|
619
|
+
if (!skills.has(skill))
|
|
620
|
+
throw invalid(`${owner} references unknown skill: ${skill}`);
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
for (const agent of pkg.agents) {
|
|
624
|
+
checkAssignments(`Agent ${agent.key}`, agent);
|
|
625
|
+
for (const connection of uniqueKeys(agent.connections ?? [], `connection in agent ${agent.key}`)) {
|
|
626
|
+
if (!connections.has(connection))
|
|
627
|
+
throw invalid(`Agent ${agent.key} references unknown connection: ${connection}`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
for (const preset of pkg.presets ?? [])
|
|
631
|
+
checkAssignments(`Preset ${preset.key}`, preset);
|
|
632
|
+
checkSkillEntries(pkg.skills?.entries ?? []);
|
|
633
|
+
const roomMembers = new Map();
|
|
634
|
+
for (const room of pkg.rooms ?? []) {
|
|
635
|
+
const members = uniqueKeys(room.members, `member in room ${room.key}`);
|
|
636
|
+
for (const member of members) {
|
|
637
|
+
if (!agents.has(member))
|
|
638
|
+
throw invalid(`Room ${room.key} references unknown agent: ${member}`);
|
|
639
|
+
}
|
|
640
|
+
if (room.defaultResponder.kind === "agent" && !members.has(room.defaultResponder.agent)) {
|
|
641
|
+
throw invalid(`Room ${room.key} has a default responder who is not a member: ${room.defaultResponder.agent}`);
|
|
642
|
+
}
|
|
643
|
+
roomMembers.set(room.key, members);
|
|
644
|
+
}
|
|
645
|
+
for (const routine of pkg.routines ?? []) {
|
|
646
|
+
if (!agents.has(routine.agent))
|
|
647
|
+
throw invalid(`Routine ${routine.key} references unknown agent: ${routine.agent}`);
|
|
648
|
+
if (routine.room === undefined)
|
|
649
|
+
continue;
|
|
650
|
+
if (!rooms.has(routine.room))
|
|
651
|
+
throw invalid(`Routine ${routine.key} references unknown group chat: ${routine.room}`);
|
|
652
|
+
if (routine.runOn !== "maus")
|
|
653
|
+
throw invalid(`Routine ${routine.key} is a group chat goal, which only runs on this computer`);
|
|
654
|
+
if (routine.continuity)
|
|
655
|
+
throw invalid(`Routine ${routine.key} is a group chat goal, which cannot carry continuity`);
|
|
656
|
+
// The goal scheduler refuses a lead who is not in the room.
|
|
657
|
+
if (!roomMembers.get(routine.room).has(routine.agent)) {
|
|
658
|
+
throw invalid(`Routine ${routine.key} is led by ${routine.agent}, who is not a member of group chat ${routine.room}`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// ── Markdown (v1 only) ─────────────────────────────────────────────────────
|
|
663
|
+
function markdownDocument(markdown) {
|
|
664
|
+
if (markdown.length > PACKAGE_MAX_BYTES)
|
|
665
|
+
throw tooLarge();
|
|
666
|
+
if (utf8Bytes(markdown) > BOTMRR_MARKDOWN_MAX_BYTES)
|
|
667
|
+
throw new PackageFormatError("too_large", "The bot playbook is too large");
|
|
668
|
+
const frontmatter = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
669
|
+
if (!frontmatter)
|
|
670
|
+
throw new PackageFormatError("not_a_package", "This Markdown is missing YAML frontmatter");
|
|
671
|
+
let metadata;
|
|
672
|
+
try {
|
|
673
|
+
metadata = parseYaml(frontmatter[1]);
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
throw new PackageFormatError("not_a_package", "This Markdown has invalid YAML frontmatter");
|
|
677
|
+
}
|
|
678
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
679
|
+
throw new PackageFormatError("not_a_package", "This Markdown is missing its BotMRR blueprint");
|
|
680
|
+
}
|
|
681
|
+
const { botmrr, ...definition } = metadata;
|
|
682
|
+
if (botmrr !== BOTMRR_MARKDOWN_VERSION)
|
|
683
|
+
throw new PackageFormatError("unsupported_version", "BotMRR Markdown version is not supported");
|
|
684
|
+
for (const heading of ["Activation", "Mission", "Outcomes", "Connections", "Team", "Chief of Staff", "Completion rule"]) {
|
|
685
|
+
if (!markdown.includes(`## ${heading}`))
|
|
686
|
+
throw new PackageFormatError("invalid", `This Markdown is missing its ${heading} section`);
|
|
687
|
+
}
|
|
688
|
+
return { format: PACKAGE_FORMAT, version: PACKAGE_V1_VERSION, package: definition };
|
|
689
|
+
}
|
|
690
|
+
// ── readers ────────────────────────────────────────────────────────────────
|
|
691
|
+
/** True for a BotMRR Markdown string or an object that says it is a package. */
|
|
692
|
+
export function isPackageDocument(value) {
|
|
693
|
+
if (typeof value === "string")
|
|
694
|
+
return /^---\r?\n[\s\S]*?\bbotmrr:\s*1\b/m.test(value);
|
|
695
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value) &&
|
|
696
|
+
value.format === PACKAGE_FORMAT;
|
|
697
|
+
}
|
|
698
|
+
/** Parse exactly one v1 document (JSON object or Markdown) with the v1 rules.
|
|
699
|
+
* Kept for the v1 exporter and its Markdown renderer. */
|
|
700
|
+
export function parsePackageV1(value) {
|
|
701
|
+
const source = typeof value === "string" ? markdownDocument(value) : value;
|
|
702
|
+
const parsed = packageDocumentV1Schema.safeParse(source);
|
|
703
|
+
if (!parsed.success)
|
|
704
|
+
throw new PackageFormatError("invalid", v1SchemaSentence(parsed.error));
|
|
705
|
+
checkV1References(parsed.data.package);
|
|
706
|
+
return parsed.data;
|
|
707
|
+
}
|
|
708
|
+
function validateV2(source) {
|
|
709
|
+
const parsed = packageDocumentSchema.safeParse(source);
|
|
710
|
+
if (!parsed.success)
|
|
711
|
+
throw invalid(schemaSentence(parsed.error, source));
|
|
712
|
+
checkV2References(parsed.data.package);
|
|
713
|
+
return parsed.data;
|
|
714
|
+
}
|
|
715
|
+
/** Normalize a v1 release ("01.2.03" → "1.2.3"): numeric parts, no leading zeros. */
|
|
716
|
+
function normalizedRelease(release) {
|
|
717
|
+
return release.split(".").map((part) => String(Number.parseInt(part, 10))).join(".");
|
|
718
|
+
}
|
|
719
|
+
/** v1 → v2: chiefOfStaff becomes team.leader, the team is named after the
|
|
720
|
+
* package, routines stay paused. Everything else is copied as it is. */
|
|
721
|
+
export function upgradeV1(document) {
|
|
722
|
+
const { chiefOfStaff, ...pkg } = document.package;
|
|
723
|
+
return validateV2({
|
|
724
|
+
format: PACKAGE_FORMAT,
|
|
725
|
+
version: PACKAGE_VERSION,
|
|
726
|
+
package: {
|
|
727
|
+
...pkg,
|
|
728
|
+
release: normalizedRelease(pkg.release),
|
|
729
|
+
team: { name: pkg.name.slice(0, 60).trim(), ...(chiefOfStaff ? { leader: chiefOfStaff } : {}) },
|
|
730
|
+
...(pkg.routines ? { routines: pkg.routines.map((routine) => ({ ...routine, enabledAfterInstall: false })) } : {}),
|
|
731
|
+
},
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
/** Accepts v1 (object or BotMRR Markdown string) and v2 (object). Always
|
|
735
|
+
* returns v2. trust "file" (the default) deletes package.publisher: only
|
|
736
|
+
* Admin may stamp who published a release. */
|
|
737
|
+
export function parsePackageDocument(value, options = {}) {
|
|
738
|
+
const trust = options.trust ?? "file";
|
|
739
|
+
let document;
|
|
740
|
+
if (typeof value === "string") {
|
|
741
|
+
document = upgradeV1(parsePackageV1(value));
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || value.format !== PACKAGE_FORMAT) {
|
|
745
|
+
throw new PackageFormatError("not_a_package", "This is not an OpenMaus package");
|
|
746
|
+
}
|
|
747
|
+
const version = value.version;
|
|
748
|
+
if (typeof version === "number" && Number.isInteger(version) && version > PACKAGE_VERSION) {
|
|
749
|
+
throw new PackageFormatError("newer_version", NEWER_PACKAGE_MESSAGE);
|
|
750
|
+
}
|
|
751
|
+
if (version !== PACKAGE_V1_VERSION && version !== PACKAGE_VERSION) {
|
|
752
|
+
throw new PackageFormatError("unsupported_version", "Package version is not supported");
|
|
753
|
+
}
|
|
754
|
+
// A cheap guard before any schema work; the exact cap is re-checked on
|
|
755
|
+
// the normalized document below.
|
|
756
|
+
if (packageBytes(value) > PACKAGE_MAX_BYTES)
|
|
757
|
+
throw tooLarge();
|
|
758
|
+
document = version === PACKAGE_V1_VERSION ? upgradeV1(parsePackageV1(value)) : validateV2(value);
|
|
759
|
+
}
|
|
760
|
+
if (trust === "file")
|
|
761
|
+
delete document.package.publisher;
|
|
762
|
+
if (packageBytes(document) > PACKAGE_MAX_BYTES)
|
|
763
|
+
throw tooLarge();
|
|
764
|
+
return document;
|
|
765
|
+
}
|
|
766
|
+
// ── older apps ─────────────────────────────────────────────────────────────
|
|
767
|
+
/** A v1 document for apps older than v2 support. Refuses what v1 cannot
|
|
768
|
+
* represent; lists what it had to leave out. */
|
|
769
|
+
export function downgradeToV1(document) {
|
|
770
|
+
const pkg = document.package;
|
|
771
|
+
if (!pkg.agents.length || !pkg.team)
|
|
772
|
+
return { error: "Packages without bots cannot be opened by older versions of OpenMausBot." };
|
|
773
|
+
const referenced = new Set(pkg.agents.flatMap((agent) => agent.skills ?? []));
|
|
774
|
+
const skills = (pkg.skills?.entries ?? []).filter((skill) => referenced.has(skill.name));
|
|
775
|
+
if (skills.length > PACKAGE_V1_MAX_SKILLS) {
|
|
776
|
+
return { error: `Older versions of OpenMausBot accept at most ${PACKAGE_V1_MAX_SKILLS} skills in a package.` };
|
|
777
|
+
}
|
|
778
|
+
const crowded = pkg.agents.find((agent) => (agent.skills?.length ?? 0) > PACKAGE_V1_MAX_SKILLS);
|
|
779
|
+
if (crowded)
|
|
780
|
+
return { error: `Older versions of OpenMausBot accept at most ${PACKAGE_V1_MAX_SKILLS} skills per bot (${crowded.key} has more).` };
|
|
781
|
+
const outOfRange = (pkg.routines ?? []).find((routine) => routine.timeoutMinutes !== undefined && (routine.timeoutMinutes < 5 || routine.timeoutMinutes > 240));
|
|
782
|
+
if (outOfRange)
|
|
783
|
+
return { error: `Older versions of OpenMausBot accept run limits from 5 to 240 minutes (${outOfRange.key} has ${outOfRange.timeoutMinutes}).` };
|
|
784
|
+
const dropped = [];
|
|
785
|
+
if (pkg.publisher)
|
|
786
|
+
dropped.push("package.publisher");
|
|
787
|
+
if (pkg.notes !== undefined)
|
|
788
|
+
dropped.push("package.notes");
|
|
789
|
+
if (pkg.team.name !== pkg.name.slice(0, 60).trim())
|
|
790
|
+
dropped.push("team.name");
|
|
791
|
+
if (pkg.team.brief !== undefined)
|
|
792
|
+
dropped.push("team.brief");
|
|
793
|
+
for (const skill of pkg.skills?.entries ?? [])
|
|
794
|
+
if (!referenced.has(skill.name))
|
|
795
|
+
dropped.push(`skills[${skill.name}]`);
|
|
796
|
+
for (const preset of pkg.presets ?? [])
|
|
797
|
+
dropped.push(`presets[${preset.key}]`);
|
|
798
|
+
for (const connection of pkg.connections ?? [])
|
|
799
|
+
dropped.push(`connections[${connection.key}]`);
|
|
800
|
+
const agents = pkg.agents.map((agent) => {
|
|
801
|
+
const { approval, connections, seed, appearance, ...kept } = agent;
|
|
802
|
+
const { avatar, ...look } = appearance;
|
|
803
|
+
if (avatar)
|
|
804
|
+
dropped.push(`agents[${agent.key}].appearance.avatar`);
|
|
805
|
+
if (approval !== undefined)
|
|
806
|
+
dropped.push(`agents[${agent.key}].approval`);
|
|
807
|
+
if (connections?.length)
|
|
808
|
+
dropped.push(`agents[${agent.key}].connections`);
|
|
809
|
+
if (seed)
|
|
810
|
+
dropped.push(`agents[${agent.key}].seed`);
|
|
811
|
+
return { ...kept, appearance: look };
|
|
812
|
+
});
|
|
813
|
+
const routines = (pkg.routines ?? []).flatMap((routine) => {
|
|
814
|
+
if (routine.room !== undefined) {
|
|
815
|
+
dropped.push(`routines[${routine.key}]`);
|
|
816
|
+
return [];
|
|
817
|
+
}
|
|
818
|
+
const { room: _room, continuity, ...kept } = routine;
|
|
819
|
+
if (continuity !== undefined)
|
|
820
|
+
dropped.push(`routines[${routine.key}].continuity`);
|
|
821
|
+
return [{ ...kept, enabledAfterInstall: false }];
|
|
822
|
+
});
|
|
823
|
+
const { team, publisher: _publisher, notes: _notes, presets: _presets, connections: _connections, skills: _skills, agents: _agents, routines: _routines, ...rest } = pkg;
|
|
824
|
+
const v1 = {
|
|
825
|
+
...rest,
|
|
826
|
+
agents,
|
|
827
|
+
...(team.leader ? { chiefOfStaff: team.leader } : {}),
|
|
828
|
+
...(routines.length ? { routines } : {}),
|
|
829
|
+
...(skills.length ? { skills: { version: 1, entries: skills } } : {}),
|
|
830
|
+
};
|
|
831
|
+
try {
|
|
832
|
+
return { document: parsePackageV1({ format: PACKAGE_FORMAT, version: PACKAGE_V1_VERSION, package: v1 }), dropped };
|
|
833
|
+
}
|
|
834
|
+
catch (error) {
|
|
835
|
+
return { error: error instanceof Error ? error.message : "This package cannot be written for older versions of OpenMausBot." };
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
/** Every string leaf of the document, with its part path, except picture
|
|
839
|
+
* bytes. A visitor that returns a string replaces the leaf in place. */
|
|
840
|
+
function visitText(document, visit) {
|
|
841
|
+
const walk = (holder, slot, part) => {
|
|
842
|
+
const value = holder[slot];
|
|
843
|
+
if (typeof value === "string") {
|
|
844
|
+
const next = visit(part, value);
|
|
845
|
+
if (typeof next === "string" && next !== value)
|
|
846
|
+
holder[slot] = next;
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
if (Array.isArray(value)) {
|
|
850
|
+
value.forEach((_, index) => walk(value, index, `${part}[${index}]`));
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if (!value || typeof value !== "object")
|
|
854
|
+
return;
|
|
855
|
+
const record = value;
|
|
856
|
+
for (const name of Object.keys(record)) {
|
|
857
|
+
if (part.endsWith(".appearance.avatar") && name === "data")
|
|
858
|
+
continue;
|
|
859
|
+
walk(record, name, part.endsWith(".seed.memory") ? `${part}[${JSON.stringify(name)}]` : `${part}.${name}`);
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
const pkg = document.package;
|
|
863
|
+
for (const name of Object.keys(pkg)) {
|
|
864
|
+
const value = pkg[name];
|
|
865
|
+
if (KEYED_LISTS.has(name) && Array.isArray(value)) {
|
|
866
|
+
value.forEach((item, index) => {
|
|
867
|
+
const id = item && typeof item === "object" ? item.key : undefined;
|
|
868
|
+
const head = `${name}[${typeof id === "string" ? id : index}]`;
|
|
869
|
+
if (!item || typeof item !== "object")
|
|
870
|
+
return walk(value, index, head);
|
|
871
|
+
for (const field of Object.keys(item))
|
|
872
|
+
walk(item, field, `${head}.${field}`);
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
else if (name === "skills" && value && typeof value === "object") {
|
|
876
|
+
const entries = value.entries;
|
|
877
|
+
if (!Array.isArray(entries))
|
|
878
|
+
continue;
|
|
879
|
+
entries.forEach((entry, index) => {
|
|
880
|
+
const id = entry && typeof entry === "object" ? entry.name : undefined;
|
|
881
|
+
const head = `skills[${typeof id === "string" ? id : index}]`;
|
|
882
|
+
if (!entry || typeof entry !== "object")
|
|
883
|
+
return;
|
|
884
|
+
for (const field of Object.keys(entry))
|
|
885
|
+
walk(entry, field, `${head}.${field}`);
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
else if (name === "team") {
|
|
889
|
+
if (!value || typeof value !== "object")
|
|
890
|
+
continue;
|
|
891
|
+
for (const field of Object.keys(value))
|
|
892
|
+
walk(value, field, `team.${field}`);
|
|
893
|
+
}
|
|
894
|
+
else {
|
|
895
|
+
walk(pkg, name, `package.${name}`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
/** Part paths where a key- or password-shaped value appears. Never values. */
|
|
900
|
+
export function packageSecretFindings(document) {
|
|
901
|
+
const parts = [];
|
|
902
|
+
visitText(document, (part, value) => {
|
|
903
|
+
if (redactSecretsInText(value) !== value)
|
|
904
|
+
parts.push(part);
|
|
905
|
+
});
|
|
906
|
+
return parts;
|
|
907
|
+
}
|
|
908
|
+
/** Replace every key- or password-shaped value with the redaction marker.
|
|
909
|
+
* Idempotent. The caller re-parses: a redaction can break a skill's
|
|
910
|
+
* frontmatter, and then the export must refuse with the parser's sentence. */
|
|
911
|
+
export function redactPackageSecrets(document) {
|
|
912
|
+
const copy = structuredClone(document);
|
|
913
|
+
const redacted = [];
|
|
914
|
+
visitText(copy, (part, value) => {
|
|
915
|
+
const next = redactSecretsInText(value);
|
|
916
|
+
if (next === value)
|
|
917
|
+
return;
|
|
918
|
+
redacted.push(part);
|
|
919
|
+
return next;
|
|
920
|
+
});
|
|
921
|
+
return { document: copy, redacted };
|
|
922
|
+
}
|
|
923
|
+
/** The skill scan (hidden blobs, curl|sh, invisible text) over every text part. */
|
|
924
|
+
export function packageScanFindings(document) {
|
|
925
|
+
const findings = [];
|
|
926
|
+
visitText(document, (part, value) => {
|
|
927
|
+
for (const warning of scanSkillText(value))
|
|
928
|
+
findings.push({ part, warning });
|
|
929
|
+
});
|
|
930
|
+
return findings;
|
|
931
|
+
}
|
|
932
|
+
// ── identity and contents ──────────────────────────────────────────────────
|
|
933
|
+
/** Every item's stable identity: "agents:scout", "skills:x", … */
|
|
934
|
+
export function packageKeys(document) {
|
|
935
|
+
const pkg = document.package;
|
|
936
|
+
return [
|
|
937
|
+
...pkg.agents.map((agent) => `agents:${agent.key}`),
|
|
938
|
+
...(pkg.rooms ?? []).map((room) => `rooms:${room.key}`),
|
|
939
|
+
...(pkg.routines ?? []).map((routine) => `routines:${routine.key}`),
|
|
940
|
+
...(pkg.skills?.entries ?? []).map((skill) => `skills:${skill.name}`),
|
|
941
|
+
...(pkg.presets ?? []).map((preset) => `presets:${preset.key}`),
|
|
942
|
+
...(pkg.connections ?? []).map((connection) => `connections:${connection.key}`),
|
|
943
|
+
...(pkg.playbooks ?? []).map((playbook) => `playbooks:${playbook.key}`),
|
|
944
|
+
];
|
|
945
|
+
}
|
|
946
|
+
export function packageSummary(document) {
|
|
947
|
+
const pkg = document.package;
|
|
948
|
+
return {
|
|
949
|
+
kind: pkg.agents.length ? "team" : "library",
|
|
950
|
+
name: pkg.name,
|
|
951
|
+
tagline: pkg.tagline,
|
|
952
|
+
counts: {
|
|
953
|
+
bots: pkg.agents.length,
|
|
954
|
+
skills: pkg.skills?.entries.length ?? 0,
|
|
955
|
+
presets: pkg.presets?.length ?? 0,
|
|
956
|
+
rooms: pkg.rooms?.length ?? 0,
|
|
957
|
+
routines: pkg.routines?.length ?? 0,
|
|
958
|
+
connections: pkg.connections?.length ?? 0,
|
|
959
|
+
playbooks: pkg.playbooks?.length ?? 0,
|
|
960
|
+
},
|
|
961
|
+
botNames: pkg.agents.slice(0, 12).map((agent) => agent.name),
|
|
962
|
+
skillNames: (pkg.skills?.entries ?? []).slice(0, PACKAGE_MAX_SKILLS).map((skill) => skill.name),
|
|
963
|
+
presetNames: (pkg.presets ?? []).map((preset) => preset.name),
|
|
964
|
+
connectionLabels: (pkg.connections ?? []).map((connection) => connection.label),
|
|
965
|
+
apps: pkg.requirements.apps.map((app) => app.label),
|
|
966
|
+
};
|
|
967
|
+
}
|