openmausbot 0.1.89 → 0.1.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{abnfDiagram-O67JEVCF-BwaMaZEm.js → abnfDiagram-O67JEVCF-yP-w8pRq.js} +1 -1
- package/dist/assets/{arc-Be-8GKxd.js → arc-DsivfZEh.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-Dp_U2Kv1.js → architectureDiagram-NJMV4G6O-CILw7lzT.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DQjUjYUk.js → blockDiagram-BEXU5L5S-DEQUV3HF.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-Bd3aE4Kc.js → c4Diagram-YGBWAQC7-lSXCZoKA.js} +1 -1
- package/dist/assets/channel-DrfGMu7X.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BijjTBeJ.js → chunk-2Q5K7J3B-QCD5bgWP.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-CMGdwvzW.js → chunk-3FUC2YCW-C5-iZJuL.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-0G09BfsZ.js → chunk-5VM5RSS4-cxsawMpY.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-FOp3vSHe.js → chunk-GTNCS2PH-DV8tV8H7.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Dv-20VIn.js → chunk-GWA4HPMP-CJFZyfbW.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CRgrdFcz.js → chunk-JWPE2WC7-BigbMXco.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-3hP4f7QB.js → chunk-NETBCI7D-oVxTfpDy.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CG98IjlG.js → chunk-WEXAMYUT-CH4BiroW.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BrSTC9xG.js → chunk-XXDRQBXY-KEDa6gax.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-rpQQQDFX.js → classDiagram-v2-NBCMYWYE-BiPjfqVv.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-G6RSafHA.js → cose-bilkent-JH36ORCC-Caxew3OY.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CXjv-IM2.js → cynefin-OW5HDTMX-B_Pui369.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dw07MR9f.js → cynefinDiagram-VND7K2PF-X64ydIhP.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-BwPnlTg2.js → dagre-6A5THRUB-TqYCI1E1.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-Cm6xcMiz.js → diagram-22UHCM2B-TeOt3rAQ.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CDpogPbE.js → diagram-3UASUU5V-D70BLhDA.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BV8z3Pnc.js → diagram-ATOU4E4O-BbQDLwUC.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CKCQhOOC.js → diagram-CDSNMT55-ChNJZ1u-.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-FGvJg69C.js → diagram-MLGK6HIB-DMBl2APa.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-CwATvgnS.js → diagram-MPIPVDR6-CEqDchOp.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-CH_c3SSC.js → ebnfDiagram-ZINNZB2B-BVDKRp_-.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-3guZi71R.js → elk-276RUBZZ-CRkofO9B.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BMHphR2n.js → erDiagram-OPXOYQCR-DruOMh5e.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Vm5DkW5x.js → flowDiagram-KWPJA3E3-8GMQeB7t.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-DO-1ct07.js → ganttDiagram-FUAMR5RP-ORE9jcsM.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-nxeksEIN.js → gitGraphDiagram-X574FWY7-BmsedsWh.js} +1 -1
- package/dist/assets/index-BlnCEcU6.js +588 -0
- package/dist/assets/{index-Oxk9x45i.js → index-DKNEM8Q6.js} +1 -1
- package/dist/assets/index-DVxjOvID.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-Dkcvr0p7.js → infoDiagram-VRGFBTTK-E_p7AooJ.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BwVibkoO.js → ishikawaDiagram-OU5B5YK6-Y-B2mKda.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-ByiBZqgG.js → journeyDiagram-ZHPQQLJL-eA8RELcK.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-qqjgXBF3.js → kanban-definition-PNTS6WVX-DqIL2e8e.js} +1 -1
- package/dist/assets/{linear-CtJnKe6A.js → linear-yC_29R6e.js} +1 -1
- package/dist/assets/{mermaid.core-BEiDklrY.js → mermaid.core-VhEMkSND.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DfIYHia9.js → mindmap-definition-NLK3R4M7-oJ7kyQZy.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CYyRqa1a.js → pegDiagram-GJSIUBJH-BmfYcY0O.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-hOToy_zH.js → pieDiagram-5QR66LMP-DKvgP7w9.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-DKAsNN9I.js → quadrantDiagram-O4NWA36T-BEKL3W0L.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BSA-d8JU.js → railroadDiagram-XR7U4H2S-BEsiqR5s.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CSmlvb-p.js → requirementDiagram-PLB6GJNP-DWBcWvAP.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CD5F6fnG.js → sankeyDiagram-IPEJSGJF-rpZ2_O4s.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-JUq5sx6d.js → sequenceDiagram-PO4LG4MO-B99HQdJR.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BjIS-Stk.js → sizeCapture-INFHLROL-BjN0qT5T.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-DklkUxOg.js → stateDiagram-v2-GCMORJYK-BbsVgGsC.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-CsjIU6be.js → swimlanes-2SLR337P-DG3gKkeR.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BkFqK7AB.js → timeline-definition-EJHVYXUP-BTHOdtF_.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-70gJB1EU.js → usecaseDiagram-POWQR4AR-acLSLFwc.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-83Tm62dN.js → vennDiagram-UO4OBE2U-C5JzNX9G.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-tTmseoX9.js → wardleyDiagram-VNRHLVJA-DL1-PrR_.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-R1LBA55a.js → xychartDiagram-PMCCYNJV-Bvr1477p.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +14810 -0
- package/dist-server/container-mcp.js +136 -36
- package/dist-server/drivers/agents-proxy.js +31 -10
- package/dist-server/index.js +48251 -44124
- package/dist-server/openmausbot.js +628 -292
- package/dist-server/pair-cli.js +628 -292
- package/dist-server/permission-proxy.js +1 -1
- package/dist-server/server/admission.js +46 -0
- package/dist-server/server/{box-create-idempotency.js → boat-create-idempotency.js} +32 -31
- package/dist-server/server/{box-delete-journal.js → boat-delete-journal.js} +26 -25
- package/dist-server/server/{box.js → boat.js} +381 -334
- package/dist-server/server/bot-overview.js +7 -4
- package/dist-server/server/bot-profile.js +7 -1
- package/dist-server/server/channel-queue.js +58 -18
- package/dist-server/server/checkpoints.js +98 -52
- package/dist-server/server/claim-idle.js +33 -0
- package/dist-server/server/cloud-backend.js +5 -5
- package/dist-server/server/cloud-home-start.js +121 -0
- package/dist-server/server/cloud-home.js +241 -0
- package/dist-server/server/cloud-lending.js +103 -0
- package/dist-server/server/cloud-move-http.js +268 -0
- package/dist-server/server/cloud-move.js +418 -0
- package/dist-server/server/cloud-overflow.js +167 -0
- package/dist-server/server/composio.js +5 -0
- package/dist-server/server/computer-observation.js +2 -2
- package/dist-server/server/config.js +156 -9
- package/dist-server/server/container-computer.js +13 -0
- package/dist-server/server/control-client.js +2 -2
- package/dist-server/server/decider/index.js +238 -0
- package/dist-server/server/decider/jev.js +183 -0
- package/dist-server/server/decider/log.js +62 -0
- package/dist-server/server/decider/relay.js +52 -0
- package/dist-server/server/decider/room-routing.js +88 -0
- package/dist-server/server/decider/types.js +5 -0
- package/dist-server/server/delegations.js +11 -3
- package/dist-server/server/drivers/acp/core.js +5 -4
- package/dist-server/server/drivers/agents-call.js +5 -1
- package/dist-server/server/drivers/agents-catalog.js +25 -3
- package/dist-server/server/drivers/{boxagent.js → boatagent.js} +30 -21
- package/dist-server/server/drivers/builtIn.js +2 -2
- package/dist-server/server/drivers/{chat-box-tools.js → chat-boat-tools.js} +3 -3
- package/dist-server/server/drivers/chat-mcp-tools.js +2 -2
- package/dist-server/server/drivers/claude.js +178 -13
- package/dist-server/server/drivers/native.js +1 -1
- package/dist-server/server/drivers/openai-chat.js +15 -4
- package/dist-server/server/drivers/pi.js +3 -3
- package/dist-server/server/group-goal-run.js +6 -4
- package/dist-server/server/harness-capabilities.js +2 -0
- package/dist-server/server/hosted-models.js +6 -1
- package/dist-server/server/included-services.js +70 -0
- package/dist-server/server/index.js +1462 -576
- package/dist-server/server/local-vm-inventory.js +11 -1
- package/dist-server/server/local-vm-seat-pool.js +141 -0
- package/dist-server/server/managed-policy.js +4 -1
- package/dist-server/server/memory-capture.js +194 -0
- package/dist-server/server/memory-entries.js +76 -0
- package/dist-server/server/memory-organize.js +102 -0
- package/dist-server/server/memory-tidy.js +144 -0
- package/dist-server/server/memory-topics.js +169 -0
- package/dist-server/server/memory-upkeep.js +452 -0
- package/dist-server/server/message-db.js +49 -5
- package/dist-server/server/model-context-window.js +4 -4
- package/dist-server/server/package-export.js +17 -10
- package/dist-server/server/package-import.js +6 -3
- package/dist-server/server/package-parts.js +3 -1
- package/dist-server/server/permission-proxy.js +3 -3
- package/dist-server/server/profile-learned.js +98 -0
- package/dist-server/server/profile-versions.js +45 -11
- package/dist-server/server/recall.js +172 -0
- package/dist-server/server/redact.js +1 -1
- package/dist-server/server/request-auth.js +2 -0
- package/dist-server/server/room-cwd.js +1 -1
- package/dist-server/server/room-handoffs.js +61 -6
- package/dist-server/server/routes/bot-memory.js +215 -0
- package/dist-server/server/routes/decider.js +19 -0
- package/dist-server/server/routine-requests.js +27 -16
- package/dist-server/server/routines.js +10 -6
- package/dist-server/server/shared-computers.js +116 -30
- package/dist-server/server/steer-queue.js +41 -10
- package/dist-server/server/store.js +42 -3
- package/dist-server/server/surface.js +14 -1
- package/dist-server/server/system-prompt.js +16 -5
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-computers.js +2 -2
- package/dist-server/server/team-manifest.js +11 -3
- package/dist-server/server/tightening-requests.js +34 -4
- package/dist-server/server/tool-summary.js +2 -2
- package/dist-server/server/tts/elevenlabs.js +12 -7
- package/dist-server/server/tts/index.js +17 -10
- package/dist-server/server/turn-resources.js +87 -11
- package/dist-server/server/vps-computer.js +1 -1
- package/dist-server/server/workspace-backup-policy.js +22 -3
- package/dist-server/server/workspace-backup.js +25 -8
- package/dist-server/server/workspace.js +57 -22
- package/dist-server/shared/ask-question.js +7 -5
- package/dist-server/shared/bot-avatar.js +19 -0
- package/dist-server/shared/cloud-home.js +4 -0
- package/dist-server/shared/credential-request.js +3 -3
- package/dist-server/shared/package-format.js +30 -4
- package/dist-server/shared/team-backup.js +20 -3
- package/dist-server/shared/tool-surface.js +1 -1
- package/dist-server/src/lib/composer-attachments.js +9 -0
- package/dist-server/vps-container-mcp.js +136 -36
- package/dist-server/workspace-backup.worker.js +165 -35
- package/package.json +1 -1
- package/dist/assets/channel-DqY_gMLg.js +0 -1
- package/dist/assets/index-BIMdIbaw.css +0 -1
- package/dist/assets/index-BVPdbq2r.js +0 -583
- package/dist/assets/swimlanesDiagram-TC7HE7FX-Bk14M0_B.js +0 -8
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// The bot-memory family, extracted from server/index.ts's inline routes
|
|
2
|
+
// into the route table (server/routes/README.md). The table runs after the
|
|
3
|
+
// auth gate; scope rules stay keyed by path in server/request-auth.ts.
|
|
4
|
+
//
|
|
5
|
+
// The files already belong to the person (plain markdown in the bot's
|
|
6
|
+
// workspace). server/memory-store.ts decides which paths can be reached
|
|
7
|
+
// and refuses a save whose expectedHash no longer matches the file;
|
|
8
|
+
// server/memory-journal.ts records every change made here so it can be
|
|
9
|
+
// read back and reverted. Reads never create the workspace — a bot that
|
|
10
|
+
// has not run yet simply has nothing to show. Admin scope by default
|
|
11
|
+
// (request-auth.ts), like the bot profile routes.
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { json } from "../harness/http.js";
|
|
14
|
+
import { MEMORY_INDEX, MemoryStoreError, memoryCapacity, memoryOverview, openMemoryLocation, readMemoryDoc, } from "../memory-store.js";
|
|
15
|
+
import { flushMemoryJournal, journalMemoryDelete, journalMemoryWrite, readMemoryJournal, revertMemoryChange, } from "../memory-journal.js";
|
|
16
|
+
import { isMemoryTopicName, readMemoryTopic } from "../workspace.js";
|
|
17
|
+
import { upkeepEnabled } from "../memory-upkeep.js";
|
|
18
|
+
import { listLearnedFacts, removeLearned } from "../profile-learned.js";
|
|
19
|
+
import { PASS } from "./table.js";
|
|
20
|
+
export function createBotMemoryRoutes(deps) {
|
|
21
|
+
/** A store refusal is a client error with a status of its own (400 path,
|
|
22
|
+
* 409 conflict, 413 too large); a 409 also carries what is on disk now so
|
|
23
|
+
* the editor can show the bot's version instead of guessing. Anything else
|
|
24
|
+
* is a real failure and goes to the handler's catch-all. */
|
|
25
|
+
function replyMemoryError(res, error) {
|
|
26
|
+
if (!(error instanceof MemoryStoreError))
|
|
27
|
+
throw error;
|
|
28
|
+
if (error.code === "conflict") {
|
|
29
|
+
return json(res, error.status, { error: error.message, code: error.code, currentHash: error.currentHash, current: error.current });
|
|
30
|
+
}
|
|
31
|
+
return json(res, error.status, { error: error.message, code: error.code });
|
|
32
|
+
}
|
|
33
|
+
/** The journal row as the panel shows it: the full prior text stays on
|
|
34
|
+
* the server (a revert needs it there, the list does not), and the thread
|
|
35
|
+
* id becomes the chat title people recognise. */
|
|
36
|
+
function journalEntryForClient(botId, entry) {
|
|
37
|
+
const { before: _before, ...visible } = entry;
|
|
38
|
+
const threadTitle = entry.threadId ? deps.taskByThread(botId, entry.threadId)?.title : undefined;
|
|
39
|
+
return threadTitle ? { ...visible, threadTitle } : visible;
|
|
40
|
+
}
|
|
41
|
+
return async ({ req, res, url, path, method, auth, json, readBody }) => {
|
|
42
|
+
/** scratch for route matches, shared by every `path.match` below */
|
|
43
|
+
let m = null;
|
|
44
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/memory$/);
|
|
45
|
+
if (m && method === "GET") {
|
|
46
|
+
if (!deps.bot(m[1]))
|
|
47
|
+
return json(res, 404, { error: "no such bot" });
|
|
48
|
+
try {
|
|
49
|
+
const overview = memoryOverview(m[1]);
|
|
50
|
+
// `text` and `truncated` ride along one release for clients of the
|
|
51
|
+
// old whole-file shape; the panel reads the file through /memory/file
|
|
52
|
+
return json(res, 200, { ...overview, text: readMemoryDoc(m[1], MEMORY_INDEX).text, truncated: overview.index.truncated });
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
return replyMemoryError(res, error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (m && method === "PUT") {
|
|
59
|
+
// The pre-panel whole-file write, kept one release: no hash check, so
|
|
60
|
+
// it can still overwrite a note the bot just wrote — journaled as the
|
|
61
|
+
// person's so at least the journal can undo it.
|
|
62
|
+
if (!deps.bot(m[1]))
|
|
63
|
+
return json(res, 404, { error: "no such bot" });
|
|
64
|
+
const parsed = z.object({ text: z.string() }).safeParse(await readBody(req));
|
|
65
|
+
if (!parsed.success)
|
|
66
|
+
return json(res, 400, { error: "text must be a string" });
|
|
67
|
+
try {
|
|
68
|
+
const { doc } = journalMemoryWrite(m[1], MEMORY_INDEX, parsed.data.text, { actor: "person", via: "api" });
|
|
69
|
+
return json(res, 200, { ok: true, hash: doc.hash, truncated: memoryCapacity(doc.text).truncated });
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
// the old route answered 400 for an oversized body; keep that for
|
|
73
|
+
// its callers while the new route says 413
|
|
74
|
+
if (error instanceof MemoryStoreError && error.code === "too-large")
|
|
75
|
+
return json(res, 400, { error: error.message });
|
|
76
|
+
return replyMemoryError(res, error);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/file$/);
|
|
80
|
+
if (m && method === "GET") {
|
|
81
|
+
if (!deps.bot(m[1]))
|
|
82
|
+
return json(res, 404, { error: "no such bot" });
|
|
83
|
+
try {
|
|
84
|
+
return json(res, 200, readMemoryDoc(m[1], url.searchParams.get("path") ?? MEMORY_INDEX));
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
return replyMemoryError(res, error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (m && method === "PUT") {
|
|
91
|
+
if (!deps.bot(m[1]))
|
|
92
|
+
return json(res, 404, { error: "no such bot" });
|
|
93
|
+
const parsed = z
|
|
94
|
+
.object({ path: z.string().default(MEMORY_INDEX), text: z.string(), expectedHash: z.string().optional() })
|
|
95
|
+
.safeParse(await readBody(req));
|
|
96
|
+
if (!parsed.success)
|
|
97
|
+
return json(res, 400, { error: "text must be a string; path and expectedHash are optional strings" });
|
|
98
|
+
try {
|
|
99
|
+
const { doc, entry } = journalMemoryWrite(m[1], parsed.data.path, parsed.data.text, {
|
|
100
|
+
actor: "person",
|
|
101
|
+
via: "ui",
|
|
102
|
+
expectedHash: parsed.data.expectedHash,
|
|
103
|
+
});
|
|
104
|
+
return json(res, 200, { ok: true, ...doc, entry: entry ? journalEntryForClient(m[1], entry) : null, overview: memoryOverview(m[1]) });
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
return replyMemoryError(res, error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (m && method === "DELETE") {
|
|
111
|
+
if (!deps.bot(m[1]))
|
|
112
|
+
return json(res, 404, { error: "no such bot" });
|
|
113
|
+
const file = url.searchParams.get("path") ?? "";
|
|
114
|
+
try {
|
|
115
|
+
const entry = journalMemoryDelete(m[1], file, { actor: "person", via: "ui" });
|
|
116
|
+
return json(res, 200, { ok: true, path: file, entry: entry ? journalEntryForClient(m[1], entry) : null, overview: memoryOverview(m[1]) });
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
return replyMemoryError(res, error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/journal$/);
|
|
123
|
+
if (m && method === "GET") {
|
|
124
|
+
if (!deps.bot(m[1]))
|
|
125
|
+
return json(res, 404, { error: "no such bot" });
|
|
126
|
+
// the row a save queued a moment ago may not have reached disk yet
|
|
127
|
+
await flushMemoryJournal(m[1]);
|
|
128
|
+
const limit = Math.min(200, Math.max(1, Number(url.searchParams.get("limit") ?? 50) || 50));
|
|
129
|
+
const botId = m[1];
|
|
130
|
+
return json(res, 200, { entries: readMemoryJournal(botId, limit).map((entry) => journalEntryForClient(botId, entry)) });
|
|
131
|
+
}
|
|
132
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/journal\/([\w-]+)\/revert$/);
|
|
133
|
+
if (m && method === "POST") {
|
|
134
|
+
if (!deps.bot(m[1]))
|
|
135
|
+
return json(res, 404, { error: "no such bot" });
|
|
136
|
+
await flushMemoryJournal(m[1]);
|
|
137
|
+
const result = revertMemoryChange(m[1], m[2]);
|
|
138
|
+
if (!result.ok)
|
|
139
|
+
return json(res, result.status, { error: result.error });
|
|
140
|
+
return json(res, 200, { ok: true, ...result.doc, entry: result.entry ? journalEntryForClient(m[1], result.entry) : null, overview: memoryOverview(m[1]) });
|
|
141
|
+
}
|
|
142
|
+
// What upkeep learned about the person is shared through About me.
|
|
143
|
+
if (method === "GET" && path === "/api/profile/learned") {
|
|
144
|
+
return json(res, 200, { learned: listLearnedFacts() });
|
|
145
|
+
}
|
|
146
|
+
m = path.match(/^\/api\/profile\/learned\/([\w-]+)\/remove$/);
|
|
147
|
+
if (m && method === "POST") {
|
|
148
|
+
const removed = removeLearned(m[1], deps.aboutMe(), deps.saveAboutMe);
|
|
149
|
+
if (!removed)
|
|
150
|
+
return json(res, 404, { error: "That fact was already removed." });
|
|
151
|
+
return json(res, 200, { ok: true, aboutMe: deps.aboutMe(), learned: listLearnedFacts() });
|
|
152
|
+
}
|
|
153
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/upkeep$/);
|
|
154
|
+
if (m && method === "GET") {
|
|
155
|
+
const bot = deps.bot(m[1]);
|
|
156
|
+
if (!bot)
|
|
157
|
+
return json(res, 404, { error: "no such bot" });
|
|
158
|
+
return json(res, 200, { enabled: upkeepEnabled(bot), ...deps.upkeep.status(bot.id) });
|
|
159
|
+
}
|
|
160
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/tidy$/);
|
|
161
|
+
if (m && method === "POST") {
|
|
162
|
+
const bot = deps.bot(m[1]);
|
|
163
|
+
if (!bot)
|
|
164
|
+
return json(res, 404, { error: "no such bot" });
|
|
165
|
+
if (!upkeepEnabled(bot))
|
|
166
|
+
return json(res, 409, { error: "Switch on Memory upkeep for this bot first." });
|
|
167
|
+
const report = await deps.upkeep.tidy(bot.id);
|
|
168
|
+
await flushMemoryJournal(bot.id);
|
|
169
|
+
return json(res, 200, { report, overview: memoryOverview(bot.id) });
|
|
170
|
+
}
|
|
171
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/open$/);
|
|
172
|
+
if (m && method === "POST") {
|
|
173
|
+
if (!deps.bot(m[1]))
|
|
174
|
+
return json(res, 404, { error: "no such bot" });
|
|
175
|
+
const parsed = z.object({ target: z.enum(["obsidian", "folder"]) }).safeParse(await readBody(req));
|
|
176
|
+
if (!parsed.success)
|
|
177
|
+
return json(res, 400, { error: "target must be obsidian or folder" });
|
|
178
|
+
// The folder is on this machine's disk; opening it only makes sense
|
|
179
|
+
// from this machine. A paired phone or a remote browser gets the path
|
|
180
|
+
// to open by hand instead.
|
|
181
|
+
const workspacePath = memoryOverview(m[1]).workspacePath;
|
|
182
|
+
if (auth.kind !== "loopback") {
|
|
183
|
+
return json(res, 403, { error: `This only works on the computer running OpenMausBot. The memory folder there is ${workspacePath}`, workspacePath });
|
|
184
|
+
}
|
|
185
|
+
const opened = await openMemoryLocation(m[1], parsed.data.target);
|
|
186
|
+
if (!opened.ok)
|
|
187
|
+
return json(res, 500, { error: opened.error, workspacePath: opened.workspacePath });
|
|
188
|
+
return json(res, 200, opened);
|
|
189
|
+
}
|
|
190
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/topics\/([^/]+)$/);
|
|
191
|
+
if (m && method === "GET") {
|
|
192
|
+
if (!deps.bot(m[1]))
|
|
193
|
+
return json(res, 404, { error: "no such bot" });
|
|
194
|
+
// Decode before validating: a UI-sent name arrives percent-encoded
|
|
195
|
+
// ("my notes.md" → "my%20notes.md"), and an encoded traversal
|
|
196
|
+
// ("..%2F..") must be judged by what it decodes TO, not slip through
|
|
197
|
+
// as an opaque token. The name gate then rejects anything that is not
|
|
198
|
+
// a single plain-markdown path segment.
|
|
199
|
+
let name;
|
|
200
|
+
try {
|
|
201
|
+
name = decodeURIComponent(m[2]);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return json(res, 400, { error: "invalid topic name" });
|
|
205
|
+
}
|
|
206
|
+
if (!isMemoryTopicName(name))
|
|
207
|
+
return json(res, 400, { error: "invalid topic name" });
|
|
208
|
+
const text = readMemoryTopic(m[1], name);
|
|
209
|
+
if (text === null)
|
|
210
|
+
return json(res, 404, { error: "no such topic file" });
|
|
211
|
+
return json(res, 200, { name, text });
|
|
212
|
+
}
|
|
213
|
+
return PASS;
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { PASS } from "./table.js";
|
|
2
|
+
const MAX_KEY_LENGTH = 512;
|
|
3
|
+
export function createDeciderRoutes(deps) {
|
|
4
|
+
return async ({ req, res, path, method, json, readBody }) => {
|
|
5
|
+
if (path !== "/api/decider/test" || method !== "POST")
|
|
6
|
+
return PASS;
|
|
7
|
+
const body = await readBody(req, 8192);
|
|
8
|
+
if (body?.key !== undefined && typeof body.key !== "string")
|
|
9
|
+
return json(res, 400, { error: "key must be a string" });
|
|
10
|
+
const draft = typeof body?.key === "string" ? body.key.trim() : "";
|
|
11
|
+
if (draft.length > MAX_KEY_LENGTH)
|
|
12
|
+
return json(res, 400, { error: "That does not look like an API key." });
|
|
13
|
+
res.setHeader("cache-control", "no-store");
|
|
14
|
+
const result = await deps.decider.testKey(draft ? { key: draft } : {});
|
|
15
|
+
if (result.ok)
|
|
16
|
+
return json(res, 200, { ok: true, latencyMs: result.latencyMs });
|
|
17
|
+
return json(res, 200, { ok: false, reason: result.reason, ...(result.status ? { status: result.status } : {}) });
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -76,11 +76,11 @@ const targetBotSchema = z.object({
|
|
|
76
76
|
}).strict();
|
|
77
77
|
const routineProposalSchema = z.discriminatedUnion("action", [
|
|
78
78
|
z.object({ action: z.literal("create"), routine: routineToolDefinitionSchema, forBot: targetBotSchema.optional() }).strict(),
|
|
79
|
-
z.object({ action: z.literal("update"), routineId: z.string().max(128), changes: routineToolChangesSchema }).strict(),
|
|
80
|
-
z.object({ action: z.literal("pause"), routineId: z.string().max(128) }).strict(),
|
|
81
|
-
z.object({ action: z.literal("resume"), routineId: z.string().max(128) }).strict(),
|
|
82
|
-
z.object({ action: z.literal("run_now"), routineId: z.string().max(128) }).strict(),
|
|
83
|
-
z.object({ action: z.literal("delete"), routineId: z.string().max(128) }).strict(),
|
|
79
|
+
z.object({ action: z.literal("update"), routineId: z.string().max(128), changes: routineToolChangesSchema, forBot: targetBotSchema.optional() }).strict(),
|
|
80
|
+
z.object({ action: z.literal("pause"), routineId: z.string().max(128), forBot: targetBotSchema.optional() }).strict(),
|
|
81
|
+
z.object({ action: z.literal("resume"), routineId: z.string().max(128), forBot: targetBotSchema.optional() }).strict(),
|
|
82
|
+
z.object({ action: z.literal("run_now"), routineId: z.string().max(128), forBot: targetBotSchema.optional() }).strict(),
|
|
83
|
+
z.object({ action: z.literal("delete"), routineId: z.string().max(128), forBot: targetBotSchema.optional() }).strict(),
|
|
84
84
|
]);
|
|
85
85
|
const storedWeekdaysSchema = z.array(z.number().int().min(0).max(6)).min(1).max(7).refine((weekdays) => new Set(weekdays).size === weekdays.length, "Stored routine weekdays must be unique");
|
|
86
86
|
const storedIntervalWindowSchema = z.object({
|
|
@@ -190,6 +190,7 @@ const storedChangesSchema = storedDefinitionSchema
|
|
|
190
190
|
const storedManageBase = {
|
|
191
191
|
routineId: z.string().regex(ROUTINE_ID),
|
|
192
192
|
expectedUpdatedAt: z.number().int().nonnegative(),
|
|
193
|
+
forBot: targetBotSchema.optional(),
|
|
193
194
|
};
|
|
194
195
|
const storedOperationSchema = z.discriminatedUnion("action", [
|
|
195
196
|
z.object({ action: z.literal("create"), routine: storedDefinitionSchema, forBot: targetBotSchema.optional() }).strict(),
|
|
@@ -441,7 +442,11 @@ function normalizedOperation(manager, botId, validated, now) {
|
|
|
441
442
|
return operation;
|
|
442
443
|
}
|
|
443
444
|
const id = routineId(validated.routineId);
|
|
444
|
-
|
|
445
|
+
// A targeted action manages the named bot's routine: ownership and every
|
|
446
|
+
// run's permissions stay with that bot, exactly as a routine created
|
|
447
|
+
// through the for_bot_id path keeps them.
|
|
448
|
+
const ownerBotId = validated.forBot?.botId ?? botId;
|
|
449
|
+
const current = ownedRoutine(manager, id, ownerBotId);
|
|
445
450
|
if (!current)
|
|
446
451
|
throw new RoutineRequestError("That routine does not exist", 404);
|
|
447
452
|
if (validated.action === "update") {
|
|
@@ -450,12 +455,18 @@ function normalizedOperation(manager, botId, validated, now) {
|
|
|
450
455
|
routineId: id,
|
|
451
456
|
expectedUpdatedAt: current.updatedAt,
|
|
452
457
|
changes: normalizeChanges(validated.changes, now),
|
|
458
|
+
...(validated.forBot ? { forBot: validated.forBot } : {}),
|
|
453
459
|
};
|
|
454
460
|
}
|
|
455
461
|
if (validated.action === "resume" && nextOccurrence(current.schedule, now) === null) {
|
|
456
462
|
throw new RoutineRequestError(noFutureResumeMessage(current.schedule), 409);
|
|
457
463
|
}
|
|
458
|
-
return {
|
|
464
|
+
return {
|
|
465
|
+
action: validated.action,
|
|
466
|
+
routineId: id,
|
|
467
|
+
expectedUpdatedAt: current.updatedAt,
|
|
468
|
+
...(validated.forBot ? { forBot: validated.forBot } : {}),
|
|
469
|
+
};
|
|
459
470
|
}
|
|
460
471
|
function asSchedule(schedule, now) {
|
|
461
472
|
if (schedule.type === "cron")
|
|
@@ -651,7 +662,7 @@ function cardCopy(operation, manager, timeZone, now) {
|
|
|
651
662
|
const actionCopy = ACTION_COPY[operation.action];
|
|
652
663
|
const actionLabel = actionCopy.title;
|
|
653
664
|
const name = redactSecretsInText(definition?.name ?? "routine");
|
|
654
|
-
const forBot = operation.
|
|
665
|
+
const forBot = operation.forBot;
|
|
655
666
|
const forSuffix = forBot ? ` for @${redactSecretsInText(forBot.name)}` : "";
|
|
656
667
|
const title = `${actionLabel} “${name}”${forSuffix}?`;
|
|
657
668
|
if (!definition) {
|
|
@@ -666,7 +677,7 @@ function cardCopy(operation, manager, timeZone, now) {
|
|
|
666
677
|
const nextRunAt = nextForOperation(operation, manager, now);
|
|
667
678
|
const scheduleTimeZone = definition.schedule.type === "cron" ? definition.schedule.timeZone : timeZone;
|
|
668
679
|
const when = operation.action === "run_now" ? "Now" : scheduleText(definition.schedule, timeZone);
|
|
669
|
-
const destination = definition.runOn === "cloud" ? "
|
|
680
|
+
const destination = definition.runOn === "cloud" ? "Boat-hosted agent" : "Bot’s current model and configured computer";
|
|
670
681
|
const current = operation.action === "create"
|
|
671
682
|
? null
|
|
672
683
|
: manager.listRoutines().find((routine) => routine.id === operation.routineId) ?? null;
|
|
@@ -838,7 +849,7 @@ function revalidateOperation(operation, manager, botId, now) {
|
|
|
838
849
|
}
|
|
839
850
|
const current = operation.action === "create"
|
|
840
851
|
? null
|
|
841
|
-
: verifyManageSnapshot(operation, manager, botId);
|
|
852
|
+
: verifyManageSnapshot(operation, manager, operation.forBot?.botId ?? botId);
|
|
842
853
|
const schedule = operation.action === "create"
|
|
843
854
|
? operation.routine.schedule
|
|
844
855
|
: operation.action === "update"
|
|
@@ -923,7 +934,7 @@ export class RoutineRequestService {
|
|
|
923
934
|
throw new RoutineRequestError(schemaIssue(parsedProposal.error, "Invalid routine proposal"));
|
|
924
935
|
}
|
|
925
936
|
const operation = normalizedOperation(this.routines, botId, parsedProposal.data, at);
|
|
926
|
-
if (operation.
|
|
937
|
+
if (operation.forBot && this.validateTarget) {
|
|
927
938
|
const refusal = this.validateTarget(botId, operation.forBot);
|
|
928
939
|
if (refusal)
|
|
929
940
|
throw new RoutineRequestError(refusal, 403);
|
|
@@ -1130,7 +1141,7 @@ export class RoutineRequestService {
|
|
|
1130
1141
|
return { claimed: true, state: "denied" };
|
|
1131
1142
|
}
|
|
1132
1143
|
revalidateOperation(payload.operation, this.routines, payload.botId, this.now());
|
|
1133
|
-
if (payload.operation.
|
|
1144
|
+
if (payload.operation.forBot && this.validateTarget) {
|
|
1134
1145
|
const refusal = this.validateTarget(payload.botId, payload.operation.forBot);
|
|
1135
1146
|
if (refusal)
|
|
1136
1147
|
throw new RoutineRequestError(refusal, 404, { terminal: true });
|
|
@@ -1229,7 +1240,7 @@ export class RoutineRequestService {
|
|
|
1229
1240
|
fingerprint,
|
|
1230
1241
|
}).id;
|
|
1231
1242
|
case "update": {
|
|
1232
|
-
const current = verifyManageSnapshot(operation, this.routines, payload.botId);
|
|
1243
|
+
const current = verifyManageSnapshot(operation, this.routines, operation.forBot?.botId ?? payload.botId);
|
|
1233
1244
|
const updated = this.routines.update(operation.routineId, updateFromChanges(operation.changes, confirmationAt, current.schedule), {
|
|
1234
1245
|
requestId: payload.requestId,
|
|
1235
1246
|
messageId,
|
|
@@ -1245,7 +1256,7 @@ export class RoutineRequestService {
|
|
|
1245
1256
|
}
|
|
1246
1257
|
case "pause":
|
|
1247
1258
|
case "resume": {
|
|
1248
|
-
verifyManageSnapshot(operation, this.routines, payload.botId);
|
|
1259
|
+
verifyManageSnapshot(operation, this.routines, operation.forBot?.botId ?? payload.botId);
|
|
1249
1260
|
const updated = this.routines.update(operation.routineId, { enabled: operation.action === "resume" }, {
|
|
1250
1261
|
requestId: payload.requestId,
|
|
1251
1262
|
messageId,
|
|
@@ -1260,7 +1271,7 @@ export class RoutineRequestService {
|
|
|
1260
1271
|
return updated.id;
|
|
1261
1272
|
}
|
|
1262
1273
|
case "run_now": {
|
|
1263
|
-
verifyManageSnapshot(operation, this.routines, payload.botId);
|
|
1274
|
+
verifyManageSnapshot(operation, this.routines, operation.forBot?.botId ?? payload.botId);
|
|
1264
1275
|
const run = this.routines.runNow(operation.routineId, {
|
|
1265
1276
|
requestId: payload.requestId,
|
|
1266
1277
|
messageId,
|
|
@@ -1275,7 +1286,7 @@ export class RoutineRequestService {
|
|
|
1275
1286
|
return run.id;
|
|
1276
1287
|
}
|
|
1277
1288
|
case "delete":
|
|
1278
|
-
verifyManageSnapshot(operation, this.routines, payload.botId);
|
|
1289
|
+
verifyManageSnapshot(operation, this.routines, operation.forBot?.botId ?? payload.botId);
|
|
1279
1290
|
if (!this.routines.remove(operation.routineId, {
|
|
1280
1291
|
requestId: payload.requestId,
|
|
1281
1292
|
messageId,
|
|
@@ -1284,12 +1284,16 @@ export class RoutineManager {
|
|
|
1284
1284
|
continue;
|
|
1285
1285
|
}
|
|
1286
1286
|
// A webhook is an incoming message, so make its task the bot's live
|
|
1287
|
-
// chat immediately. Scheduled work
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
:
|
|
1287
|
+
// chat immediately. Scheduled work stays in its own task unless the
|
|
1288
|
+
// workspace has asked for runs to join the conversation they report to.
|
|
1289
|
+
const joined = run.target === "room-goal" ? null : this.options.joinConversation?.(run) || null;
|
|
1290
|
+
const task = joined
|
|
1291
|
+
? { threadId: joined }
|
|
1292
|
+
: run.target === "room-goal"
|
|
1293
|
+
? run.groupId
|
|
1294
|
+
? this.options.createGoalTask?.(run.groupId, run.routineName) ?? null
|
|
1295
|
+
: null
|
|
1296
|
+
: this.options.createTask(run.botId, run.routineName, run.triggerSource === "webhook");
|
|
1293
1297
|
if (!task) {
|
|
1294
1298
|
this.failRun(run, run.target === "room-goal"
|
|
1295
1299
|
? "Could not create a room task for this goal"
|
|
@@ -15,41 +15,120 @@ export const sharedComputerOperation = z.object({
|
|
|
15
15
|
arguments: z.record(z.string(), z.unknown()).optional(),
|
|
16
16
|
}).strict();
|
|
17
17
|
const failure = (message, status = 409) => Object.assign(new Error(message), { status });
|
|
18
|
+
/** Bounds memory, not anyone's share: a desktop session holds one entry. */
|
|
19
|
+
const TOTAL = 100;
|
|
20
|
+
/** How long a lent computer that stopped polling (asleep, app closed) stays
|
|
21
|
+
* known, so a status reader can tell "lent but offline" from "not lent". */
|
|
22
|
+
const RETAIN_MS = 14 * 24 * 60 * 60_000;
|
|
23
|
+
const OFFLINE = "This shared computer is offline. Keep its desktop app open; do not substitute files on the server.";
|
|
24
|
+
/** The scopes the desktop registered, checked by the server as well. The
|
|
25
|
+
* desktop's own grant is the authority and checks every operation again;
|
|
26
|
+
* this only means the server never even queues work outside what the desktop
|
|
27
|
+
* said it lends. */
|
|
28
|
+
export function sharedComputerScopeRefusal(registration, operation) {
|
|
29
|
+
switch (operation.action) {
|
|
30
|
+
case "run_command":
|
|
31
|
+
return registration.terminal ? null : "Terminal access is not lent to this server.";
|
|
32
|
+
case "computer_tools":
|
|
33
|
+
case "computer_call":
|
|
34
|
+
return registration.computer ? null : "Apps and screen are not lent to this server.";
|
|
35
|
+
case "list_files":
|
|
36
|
+
case "read_file":
|
|
37
|
+
case "write_file": {
|
|
38
|
+
const lent = registration.folders.find(entry => entry.id === operation.folder_id);
|
|
39
|
+
if (!lent)
|
|
40
|
+
return "This folder has not been shared with this server.";
|
|
41
|
+
return operation.action === "write_file" && !lent.write ? "This folder is read-only." : null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** The person a bot turn acts for, from the harness's own record of which
|
|
46
|
+
* message started it: that request must name this exact turn generation (a
|
|
47
|
+
* continuation of the same request adds its generation), must not have been
|
|
48
|
+
* stopped, and its message must come from a person. Anything else is nobody. */
|
|
49
|
+
export function provenRequestPerson(request, generation, personOf) {
|
|
50
|
+
if (!request?.messageId || request.stopped || !request.generations.has(generation))
|
|
51
|
+
return null;
|
|
52
|
+
return personOf(request.messageId) ?? null;
|
|
53
|
+
}
|
|
18
54
|
/** In-memory rendezvous only. Authority over local resources stays on the
|
|
19
|
-
* desktop. Restart/disconnect NEVER replays an operation with an unknown outcome.
|
|
55
|
+
* desktop. Restart/disconnect NEVER replays an operation with an unknown outcome.
|
|
56
|
+
*
|
|
57
|
+
* Partitioned by owner: an entry is keyed by the registering session and the
|
|
58
|
+
* desktop's id, so another session can never take over an id, and a bot turn
|
|
59
|
+
* sees only the computers of the person it acts for. A turn that acts for
|
|
60
|
+
* nobody provable (principal null) sees none. */
|
|
20
61
|
export class SharedComputers {
|
|
21
62
|
computers = new Map();
|
|
22
63
|
sessionLive;
|
|
23
64
|
constructor(sessionLive) { this.sessionLive = sessionLive; }
|
|
65
|
+
static key(session, id) { return `${session}\n${id}`; }
|
|
24
66
|
register(registration, owner, secret) {
|
|
25
67
|
if (!/^[a-f0-9]{64}$/.test(secret))
|
|
26
68
|
throw failure("Invalid computer credential", 400);
|
|
27
|
-
const
|
|
69
|
+
const key = SharedComputers.key(owner.session, registration.id);
|
|
70
|
+
const old = this.computers.get(key);
|
|
28
71
|
if (old) {
|
|
29
|
-
this.authorize(registration.id, owner, secret);
|
|
72
|
+
this.authorize(registration.id, owner.session, secret);
|
|
30
73
|
old.registration = registration;
|
|
31
74
|
old.seen = Date.now();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
this.reap();
|
|
78
|
+
// A paired desktop runs one grant per server, so a new id from the same
|
|
79
|
+
// session is a new save: its earlier entry is stale and goes. No other
|
|
80
|
+
// session can reach, replace or squat this key.
|
|
81
|
+
for (const [stale, entry] of this.computers) {
|
|
82
|
+
if (entry.owner.session !== owner.session)
|
|
83
|
+
continue;
|
|
84
|
+
this.computers.delete(stale);
|
|
85
|
+
entry.wake?.();
|
|
86
|
+
for (const job of entry.jobs.values())
|
|
87
|
+
this.finish(entry, job, failure("This computer's access changed. An in-flight action may have completed; inspect before retrying."));
|
|
32
88
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
89
|
+
if (this.computers.size >= TOTAL)
|
|
90
|
+
throw failure("Too many shared computers");
|
|
91
|
+
this.computers.set(key, { registration, owner: { ...owner }, secret, seen: Date.now(), jobs: new Map() });
|
|
92
|
+
}
|
|
93
|
+
/** Forget entries whose session ended or that have been silent too long. */
|
|
94
|
+
reap() {
|
|
95
|
+
for (const [key, entry] of this.computers) {
|
|
96
|
+
if (entry.jobs.size === 0 && (!this.sessionLive(entry.owner.session) || Date.now() - entry.seen > RETAIN_MS))
|
|
97
|
+
this.computers.delete(key);
|
|
41
98
|
}
|
|
42
99
|
}
|
|
43
|
-
online(entry) { return this.sessionLive(entry.owner) && Date.now() - entry.seen < 40_000; }
|
|
44
|
-
authorize(id,
|
|
45
|
-
const entry = this.computers.get(id);
|
|
46
|
-
if (!entry ||
|
|
100
|
+
online(entry) { return this.sessionLive(entry.owner.session) && Date.now() - entry.seen < 40_000; }
|
|
101
|
+
authorize(id, session, secret) {
|
|
102
|
+
const entry = this.computers.get(SharedComputers.key(session, id));
|
|
103
|
+
if (!entry || !this.sessionLive(session) || !/^[a-f0-9]{64}$/.test(secret) || !timingSafeEqual(Buffer.from(secret), Buffer.from(entry.secret)))
|
|
47
104
|
throw failure("Shared computer is disconnected or not authorized", 403);
|
|
48
105
|
return entry;
|
|
49
106
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
107
|
+
/** The online computers one person lent. */
|
|
108
|
+
list(principal) {
|
|
109
|
+
if (!principal)
|
|
110
|
+
return [];
|
|
111
|
+
return [...this.computers.values()].filter(entry => entry.owner.person === principal && this.online(entry)).map(entry => entry.registration);
|
|
112
|
+
}
|
|
113
|
+
/** What one person has lent, online or not: the documented status a
|
|
114
|
+
* reader (the Cloud UI, the next step's placement) uses to tell "your Mac
|
|
115
|
+
* is lent and awake" from "lent but asleep" and "not lent". No secret, no
|
|
116
|
+
* session. Resets when this server restarts; a lending desktop registers
|
|
117
|
+
* again within seconds of being online. */
|
|
118
|
+
status(principal) {
|
|
119
|
+
if (!principal)
|
|
120
|
+
return [];
|
|
121
|
+
this.reap();
|
|
122
|
+
return [...this.computers.values()].filter(entry => entry.owner.person === principal).map(entry => ({
|
|
123
|
+
id: entry.registration.id, name: entry.registration.name, online: this.online(entry), busy: entry.jobs.size > 0, lastSeenAt: entry.seen,
|
|
124
|
+
scopes: {
|
|
125
|
+
folders: entry.registration.folders.map(({ id, name, write }) => ({ id, name, write })),
|
|
126
|
+
terminal: entry.registration.terminal, screen: entry.registration.computer,
|
|
127
|
+
},
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
async poll(id, session, secret) {
|
|
131
|
+
const entry = this.authorize(id, session, secret);
|
|
53
132
|
if (entry.wake)
|
|
54
133
|
throw failure("A computer poll is already running");
|
|
55
134
|
entry.seen = Date.now();
|
|
@@ -73,34 +152,41 @@ export class SharedComputers {
|
|
|
73
152
|
const timer = setTimeout(() => { entry.wake = undefined; resolve(); }, 20_000);
|
|
74
153
|
entry.wake = () => { clearTimeout(timer); entry.wake = undefined; resolve(); };
|
|
75
154
|
});
|
|
76
|
-
this.authorize(id,
|
|
155
|
+
this.authorize(id, session, secret);
|
|
77
156
|
entry.seen = Date.now();
|
|
78
157
|
return next();
|
|
79
158
|
}
|
|
80
|
-
liveJob(id,
|
|
81
|
-
const entry = this.authorize(id,
|
|
159
|
+
liveJob(id, session, secret, jobId) {
|
|
160
|
+
const entry = this.authorize(id, session, secret);
|
|
82
161
|
entry.seen = Date.now();
|
|
83
162
|
const job = entry.jobs.get(jobId);
|
|
84
163
|
return job?.sent === true && job.active();
|
|
85
164
|
}
|
|
86
|
-
complete(id,
|
|
87
|
-
const entry = this.authorize(id,
|
|
165
|
+
complete(id, session, secret, jobId, result) {
|
|
166
|
+
const entry = this.authorize(id, session, secret);
|
|
88
167
|
const job = entry.jobs.get(jobId);
|
|
89
168
|
if (!job || !job.sent)
|
|
90
169
|
throw failure("This computer operation expired; it will not be replayed");
|
|
91
170
|
this.finish(entry, job, job.active() ? null : failure("The requesting turn ended"), result);
|
|
92
171
|
}
|
|
93
|
-
disconnect(id,
|
|
94
|
-
const entry = this.authorize(id,
|
|
95
|
-
this.computers.delete(id);
|
|
172
|
+
disconnect(id, session, secret) {
|
|
173
|
+
const entry = this.authorize(id, session, secret);
|
|
174
|
+
this.computers.delete(SharedComputers.key(session, id));
|
|
96
175
|
entry.wake?.();
|
|
97
176
|
for (const job of entry.jobs.values())
|
|
98
177
|
this.finish(entry, job, failure("Computer disconnected. An in-flight action may have completed; inspect before retrying."));
|
|
99
178
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
179
|
+
/** One bot operation for the person the turn acts for. Another person's
|
|
180
|
+
* computer answers exactly like one that does not exist. */
|
|
181
|
+
request(operation, principal, active) {
|
|
182
|
+
if (!principal)
|
|
183
|
+
return Promise.reject(failure("This conversation cannot use a shared computer: only a conversation started by the person who shared it can.", 403));
|
|
184
|
+
const entry = [...this.computers.values()].find(candidate => candidate.registration.id === operation.computer_id && candidate.owner.person === principal && this.online(candidate));
|
|
185
|
+
if (!entry)
|
|
186
|
+
return Promise.reject(failure(OFFLINE));
|
|
187
|
+
const refusal = sharedComputerScopeRefusal(entry.registration, operation);
|
|
188
|
+
if (refusal)
|
|
189
|
+
return Promise.reject(failure(refusal, 403));
|
|
104
190
|
if (entry.jobs.size >= 1)
|
|
105
191
|
return Promise.reject(failure("This shared computer is busy. Wait for its current action to finish."));
|
|
106
192
|
if (!active())
|