@chibitek/chibi 0.1.170 → 0.1.172
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-Dwjw0xC0.js → abnfDiagram-O67JEVCF-CGoW52MW.js} +1 -1
- package/dist/assets/{arc-B__BIHzb.js → arc-DEkKLw4F.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-DQy7n3Ga.js → architectureDiagram-NJMV4G6O-Cyi6MYs6.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-moFo2nSA.js → blockDiagram-BEXU5L5S-CddA0wa_.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-C-dF8UVl.js → c4Diagram-YGBWAQC7-Dg8q9n90.js} +1 -1
- package/dist/assets/channel-Dda8O6gd.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-DnF0mxAV.js → chunk-2Q5K7J3B-BSMub34c.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-Dp8Dsifv.js → chunk-3FUC2YCW--Xc7Up5y.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-D8fmZ4d8.js → chunk-5VM5RSS4-B9Ae4f30.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-DNH2HZNQ.js → chunk-GTNCS2PH-Dh1To9G3.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BvL6j7pQ.js → chunk-GWA4HPMP-C_5eiV2n.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-Co0gUA5M.js → chunk-JWPE2WC7-5wQuPsks.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-Deu9tpCF.js → chunk-NETBCI7D-BGXS05ur.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-DKy8jAU5.js → chunk-WEXAMYUT-ClIMhmD5.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DQhvP8zg.js → chunk-XXDRQBXY-CcKs11-L.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-mhv3hMMV.js → classDiagram-v2-NBCMYWYE-BCiLckGU.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-CR1nOQOe.js → cose-bilkent-JH36ORCC-JyAqT-ND.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-Ddgkhxgt.js → cynefin-OW5HDTMX-BznovZj0.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-4KbkrNhB.js → cynefinDiagram-VND7K2PF-BDRrTIEB.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DNxtjbvT.js → dagre-6A5THRUB-BbWguhLj.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-BRpvnOWX.js → diagram-22UHCM2B-BMk1KTBr.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-DsQVnKUb.js → diagram-3UASUU5V-ChnEP2in.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BRZ5455_.js → diagram-ATOU4E4O-C78imnTV.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CidrDKp0.js → diagram-CDSNMT55-Dekx02f_.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-1hkZ8SsK.js → diagram-MLGK6HIB-D2__fPwe.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-D1dWGFqn.js → diagram-MPIPVDR6-C9l7MZm7.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DGjfEPY5.js → ebnfDiagram-ZINNZB2B-D4x0h3XT.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-C5nvtTy7.js → elk-276RUBZZ-twChz5io.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CC0QBE1A.js → erDiagram-OPXOYQCR-BXEKWWMq.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-BthbP_HO.js → flowDiagram-KWPJA3E3-Ct-XrJMr.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-BQ2wLSEI.js → ganttDiagram-FUAMR5RP-Bn92lxCs.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-lTR8tNlq.js → gitGraphDiagram-X574FWY7-Dvfjo7cn.js} +1 -1
- package/dist/assets/{index-AWsWxgoq.js → index-CarzpTfN.js} +1 -1
- package/dist/assets/{index-VagspC1z.js → index-DvneZE6Z.js} +76 -73
- package/dist/assets/{infoDiagram-VRGFBTTK-BgI9ZuT_.js → infoDiagram-VRGFBTTK-MJ14OyNX.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-qPe6Rhk2.js → ishikawaDiagram-OU5B5YK6-nf9xhTNX.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CpbadZuP.js → journeyDiagram-ZHPQQLJL-DqfK6X2e.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-D9kCb3-t.js → kanban-definition-PNTS6WVX-CjUR1QYT.js} +1 -1
- package/dist/assets/{linear-CdW4LT3g.js → linear-dPqGsZwL.js} +1 -1
- package/dist/assets/{mermaid.core-BbPWTT56.js → mermaid.core-zL0DLPAI.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-BtOaXxvb.js → mindmap-definition-NLK3R4M7-C1-Jqg3_.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BtcTITAK.js → pegDiagram-GJSIUBJH-CFlvuAHC.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-Ch_--zjt.js → pieDiagram-5QR66LMP-BvyT0IgF.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-Di8ygRal.js → quadrantDiagram-O4NWA36T-bg7JeR2c.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-DxgesAmb.js → railroadDiagram-XR7U4H2S-dF2Nqim8.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DjH-jdCr.js → requirementDiagram-PLB6GJNP-CwVu_Or6.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-ClNLceSc.js → sankeyDiagram-IPEJSGJF-wBC3-mRy.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-Cip_EmtV.js → sequenceDiagram-PO4LG4MO-Bs9lHMf-.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BzW2hkq8.js → sizeCapture-INFHLROL-d6YiGOIQ.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-CwHu7P19.js → stateDiagram-v2-GCMORJYK--f6dscuE.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DO9E9HUK.js → swimlanes-2SLR337P-CQpzBoAc.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C1ko4o6t.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-D2_E-i9h.js → timeline-definition-EJHVYXUP-DpoyUyqt.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-oKBQ1FAG.js → usecaseDiagram-POWQR4AR-Dg7mR3GW.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-HEdQsC7L.js → vennDiagram-UO4OBE2U-CoEfTL7k.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-mNpRO8rd.js → wardleyDiagram-VNRHLVJA-24tIY01C.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-uURjTK1X.js → xychartDiagram-PMCCYNJV-CsoqIRqn.js} +1 -1
- package/dist/handbook/atomic-handbook.html +1 -1
- package/dist/index.html +1 -1
- package/dist-server/companion/src/routes.js +36 -1
- package/dist-server/index.js +37509 -37213
- package/dist-server/openmausbot.js +6 -5
- package/dist-server/pair-cli.js +6 -5
- package/dist-server/server/drivers/acp/custom.js +3 -0
- package/dist-server/server/index.js +19 -118
- package/dist-server/server/message-file.js +28 -11
- package/dist-server/server/remote-desktop.js +142 -0
- package/dist-server/server/routes/message-file.js +115 -0
- package/dist-server/server/shared-deliverables.js +109 -0
- package/dist-server/server/skill-learn.js +4 -0
- package/dist-server/server/subscription-bots.js +3 -2
- package/dist-server/shared/provider-accounts.js +5 -5
- package/dist-server/shared/skill-command.js +16 -0
- package/package.json +1 -1
- package/dist/assets/channel-BQM13l0i.js +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-UC0GNJzN.js +0 -8
|
@@ -19556,7 +19556,7 @@ var init_provider_accounts = __esm({
|
|
|
19556
19556
|
// Kimi Code reads its whole data root (login included) from KIMI_CODE_HOME.
|
|
19557
19557
|
{ id: "kimi", kind: "signIn", label: "Kimi Code", billing: "subscription", driver: "kimiAgent", homeEnv: "KIMI_CODE_HOME", loginArgs: ["login"], signupUrl: "https://www.kimi.com/code" },
|
|
19558
19558
|
// Droid resolves <FACTORY_HOME_OVERRIDE>/.factory, so the folder is a home, not the data root.
|
|
19559
|
-
{ id: "droid", kind: "signIn", label: "Factory Droid", billing: "subscription", driver: "droidAgent", homeEnv: "FACTORY_HOME_OVERRIDE", loginArgs: [], signupUrl: "https://factory.
|
|
19559
|
+
{ id: "droid", kind: "signIn", label: "Factory Droid", billing: "subscription", driver: "droidAgent", homeEnv: "FACTORY_HOME_OVERRIDE", loginArgs: [], signupUrl: "https://factory.com/pricing" },
|
|
19560
19560
|
{
|
|
19561
19561
|
id: "anthropic",
|
|
19562
19562
|
kind: "apiKey",
|
|
@@ -19567,7 +19567,7 @@ var init_provider_accounts = __esm({
|
|
|
19567
19567
|
icon: "anthropic",
|
|
19568
19568
|
config: {},
|
|
19569
19569
|
keyTest: { provider: "anthropic" },
|
|
19570
|
-
keyUrl: "https://
|
|
19570
|
+
keyUrl: "https://platform.claude.com/settings/keys"
|
|
19571
19571
|
},
|
|
19572
19572
|
{
|
|
19573
19573
|
id: "openai",
|
|
@@ -19605,7 +19605,7 @@ var init_provider_accounts = __esm({
|
|
|
19605
19605
|
envVar: "PERPLEXITY_API_KEY",
|
|
19606
19606
|
icon: "perplexity",
|
|
19607
19607
|
config: { url: "https://api.perplexity.ai", apiKeyEnv: "PERPLEXITY_API_KEY", model: "sonar-pro", provider: "" },
|
|
19608
|
-
keyUrl: "https://
|
|
19608
|
+
keyUrl: "https://console.perplexity.ai/project/keys"
|
|
19609
19609
|
},
|
|
19610
19610
|
{
|
|
19611
19611
|
// Alibaba's flat-price Qwen Coding Plan: one plan key serves Qwen, GLM, Kimi and MiniMax models.
|
|
@@ -19618,7 +19618,7 @@ var init_provider_accounts = __esm({
|
|
|
19618
19618
|
icon: "qwen",
|
|
19619
19619
|
config: { url: "https://coding-intl.dashscope.aliyuncs.com/v1", apiKeyEnv: "QWEN_CODING_PLAN_API_KEY", model: "qwen3.7-plus", provider: "" },
|
|
19620
19620
|
keyTest: { provider: "openaiCompat", url: "https://coding-intl.dashscope.aliyuncs.com/v1" },
|
|
19621
|
-
keyUrl: "https://modelstudio.console.alibabacloud.com/",
|
|
19621
|
+
keyUrl: "https://modelstudio.console.alibabacloud.com/ap-southeast-1/subscription/coding-plan",
|
|
19622
19622
|
signupUrl: "https://www.alibabacloud.com/help/en/model-studio/coding-plan"
|
|
19623
19623
|
},
|
|
19624
19624
|
{
|
|
@@ -19683,7 +19683,7 @@ var init_provider_accounts = __esm({
|
|
|
19683
19683
|
driver: "opencodeGo",
|
|
19684
19684
|
envVar: "OPENCODE_API_KEY",
|
|
19685
19685
|
config: {},
|
|
19686
|
-
keyUrl: "https://opencode.ai/
|
|
19686
|
+
keyUrl: "https://opencode.ai/go",
|
|
19687
19687
|
signupUrl: "https://opencode.ai/go"
|
|
19688
19688
|
}
|
|
19689
19689
|
];
|
|
@@ -56503,6 +56503,7 @@ var init_custom = __esm({
|
|
|
56503
56503
|
transformEnv: (env, config2, instanceId) => {
|
|
56504
56504
|
applyAtomicEngineAccounts(env, instanceId, config2.cli);
|
|
56505
56505
|
lockAtomicEngineMemory(env, instanceId, config2.cli);
|
|
56506
|
+
if (isAtomicEngine(instanceId, config2.cli)) env.HERMES_ACP_SKIP_CONFIGURED_MCP = "1";
|
|
56506
56507
|
},
|
|
56507
56508
|
pickAuthMethod: () => null,
|
|
56508
56509
|
authFailure: "continue",
|
package/dist-server/pair-cli.js
CHANGED
|
@@ -19556,7 +19556,7 @@ var init_provider_accounts = __esm({
|
|
|
19556
19556
|
// Kimi Code reads its whole data root (login included) from KIMI_CODE_HOME.
|
|
19557
19557
|
{ id: "kimi", kind: "signIn", label: "Kimi Code", billing: "subscription", driver: "kimiAgent", homeEnv: "KIMI_CODE_HOME", loginArgs: ["login"], signupUrl: "https://www.kimi.com/code" },
|
|
19558
19558
|
// Droid resolves <FACTORY_HOME_OVERRIDE>/.factory, so the folder is a home, not the data root.
|
|
19559
|
-
{ id: "droid", kind: "signIn", label: "Factory Droid", billing: "subscription", driver: "droidAgent", homeEnv: "FACTORY_HOME_OVERRIDE", loginArgs: [], signupUrl: "https://factory.
|
|
19559
|
+
{ id: "droid", kind: "signIn", label: "Factory Droid", billing: "subscription", driver: "droidAgent", homeEnv: "FACTORY_HOME_OVERRIDE", loginArgs: [], signupUrl: "https://factory.com/pricing" },
|
|
19560
19560
|
{
|
|
19561
19561
|
id: "anthropic",
|
|
19562
19562
|
kind: "apiKey",
|
|
@@ -19567,7 +19567,7 @@ var init_provider_accounts = __esm({
|
|
|
19567
19567
|
icon: "anthropic",
|
|
19568
19568
|
config: {},
|
|
19569
19569
|
keyTest: { provider: "anthropic" },
|
|
19570
|
-
keyUrl: "https://
|
|
19570
|
+
keyUrl: "https://platform.claude.com/settings/keys"
|
|
19571
19571
|
},
|
|
19572
19572
|
{
|
|
19573
19573
|
id: "openai",
|
|
@@ -19605,7 +19605,7 @@ var init_provider_accounts = __esm({
|
|
|
19605
19605
|
envVar: "PERPLEXITY_API_KEY",
|
|
19606
19606
|
icon: "perplexity",
|
|
19607
19607
|
config: { url: "https://api.perplexity.ai", apiKeyEnv: "PERPLEXITY_API_KEY", model: "sonar-pro", provider: "" },
|
|
19608
|
-
keyUrl: "https://
|
|
19608
|
+
keyUrl: "https://console.perplexity.ai/project/keys"
|
|
19609
19609
|
},
|
|
19610
19610
|
{
|
|
19611
19611
|
// Alibaba's flat-price Qwen Coding Plan: one plan key serves Qwen, GLM, Kimi and MiniMax models.
|
|
@@ -19618,7 +19618,7 @@ var init_provider_accounts = __esm({
|
|
|
19618
19618
|
icon: "qwen",
|
|
19619
19619
|
config: { url: "https://coding-intl.dashscope.aliyuncs.com/v1", apiKeyEnv: "QWEN_CODING_PLAN_API_KEY", model: "qwen3.7-plus", provider: "" },
|
|
19620
19620
|
keyTest: { provider: "openaiCompat", url: "https://coding-intl.dashscope.aliyuncs.com/v1" },
|
|
19621
|
-
keyUrl: "https://modelstudio.console.alibabacloud.com/",
|
|
19621
|
+
keyUrl: "https://modelstudio.console.alibabacloud.com/ap-southeast-1/subscription/coding-plan",
|
|
19622
19622
|
signupUrl: "https://www.alibabacloud.com/help/en/model-studio/coding-plan"
|
|
19623
19623
|
},
|
|
19624
19624
|
{
|
|
@@ -19683,7 +19683,7 @@ var init_provider_accounts = __esm({
|
|
|
19683
19683
|
driver: "opencodeGo",
|
|
19684
19684
|
envVar: "OPENCODE_API_KEY",
|
|
19685
19685
|
config: {},
|
|
19686
|
-
keyUrl: "https://opencode.ai/
|
|
19686
|
+
keyUrl: "https://opencode.ai/go",
|
|
19687
19687
|
signupUrl: "https://opencode.ai/go"
|
|
19688
19688
|
}
|
|
19689
19689
|
];
|
|
@@ -56503,6 +56503,7 @@ var init_custom = __esm({
|
|
|
56503
56503
|
transformEnv: (env, config2, instanceId) => {
|
|
56504
56504
|
applyAtomicEngineAccounts(env, instanceId, config2.cli);
|
|
56505
56505
|
lockAtomicEngineMemory(env, instanceId, config2.cli);
|
|
56506
|
+
if (isAtomicEngine(instanceId, config2.cli)) env.HERMES_ACP_SKIP_CONFIGURED_MCP = "1";
|
|
56506
56507
|
},
|
|
56507
56508
|
pickAuthMethod: () => null,
|
|
56508
56509
|
authFailure: "continue",
|
|
@@ -142,6 +142,9 @@ const support = {
|
|
|
142
142
|
transformEnv: (env, config, instanceId) => {
|
|
143
143
|
applyAtomicEngineAccounts(env, instanceId, config.cli);
|
|
144
144
|
lockAtomicEngineMemory(env, instanceId, config.cli);
|
|
145
|
+
// Atomic owns session mounts; ambient discovery must not race the same names.
|
|
146
|
+
if (isAtomicEngine(instanceId, config.cli))
|
|
147
|
+
env.HERMES_ACP_SKIP_CONFIGURED_MCP = "1";
|
|
145
148
|
},
|
|
146
149
|
pickAuthMethod: () => null,
|
|
147
150
|
authFailure: "continue",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createMessageFileRoutes } from "./routes/message-file.js";
|
|
2
|
+
import { SharedDeliverables, createSharedDeliverablesRoutes, sharedDeliverableAccess } from "./shared-deliverables.js";
|
|
1
3
|
import { connectionCredentialServer } from "./connection-credential.js";
|
|
2
4
|
import { requestCredentialCard } from "./credential-card-request.js";
|
|
3
5
|
import { invalidateBrokenCheckpoint } from "./checkpoint-recovery.js";
|
|
@@ -50,7 +52,6 @@ import { ApprovalGrantStore, describeScope, matchGrant } from "./approval-grants
|
|
|
50
52
|
import { validateBotCwd } from "./bot-cwd.js";
|
|
51
53
|
import { ATTACHMENTS_DIR, attachmentExists, cleanupStaleAttachmentPartials, deleteAttachment, extensionForMime, FILE_MAX_BYTES, IMAGE_MAX_BYTES, readAttachment, saveFile, saveImage, saveImageUpload, validateAttachmentUploadId, } from "./attachments.js";
|
|
52
54
|
import { companyCheck, companyContext } from "./company-document-check.js";
|
|
53
|
-
import { messageFileDisposition, messageFileDownloadName, messageAttachmentName, messageFileRoots, messageImageTargetAt, messageReferencesFile, openMessageFile, } from "./message-file.js";
|
|
54
55
|
import { avatarGenerationRequestSchema, avatarGenerationStateMatches, avatarImageStatus, snapshotAvatarGenerationState } from "./avatar-image.js";
|
|
55
56
|
import { generateRoutedAvatar, installAvatarSubscriptionRoute } from "./avatar-subscription.js";
|
|
56
57
|
import { fitsOnOneLine, parseBotProfilePatch } from "./bot-profile.js";
|
|
@@ -157,11 +158,12 @@ import { applyStagedSkillWrite, applySkillWriteWithReceipt, getStagedSkillWrite,
|
|
|
157
158
|
import { resolveSkillImport, skillImportSchema } from "./skill-fetch.js";
|
|
158
159
|
import { pickBotName } from "./names.js";
|
|
159
160
|
import { botSkillTemplateSchema, newBotDefaultsSchema, resolveBotCreationDefaults } from "./new-bot-defaults.js";
|
|
160
|
-
import {
|
|
161
|
+
import { expandSkillTurnText, learnSource } from "./skill-learn.js";
|
|
161
162
|
import { expandSetupTurnText, setupModeActive, setupSystemPrompt } from "./setup-mode.js";
|
|
162
163
|
import { checkSoulDrift, readSoulDrift, soulFile, writeSoulMirror } from "./bot-folder.js";
|
|
163
164
|
import { buildSystemPrompt, userProfileSystemPrompt, computerPrompt, accountsPrompt, customMcpPrompt, CREDENTIAL_PROMPT, mentionPrompt, THREADS_PROMPT, LEARN_PROMPT, PROFILE_PROMPT, ROUTINE_PROMPT, ROUTINE_EXECUTION_PROMPT, WEBHOOK_PROMPT, } from "./system-prompt.js";
|
|
164
165
|
import { readCuaConnection, readCuaUnavailableReason, gatedLocalComputer } from "./local-computer.js";
|
|
166
|
+
import { remoteDesktopRoute } from "./remote-desktop.js";
|
|
165
167
|
import { discoverExistingPerBotLocalVms, localVmInventoryEntry, shouldArmLocalVmIdle, } from "./local-vm-inventory.js";
|
|
166
168
|
import { ATOMIC_BACKUP_DIR, BROWSER_BACKUP_INTERVAL_MS, backupBrowserProfiles, localVmWorkspaces } from "./atomic-backup.js";
|
|
167
169
|
import { LocalVmIdleTimer } from "./local-vm-idle.js";
|
|
@@ -2664,12 +2666,13 @@ function subscriptionModel(bot, candidate, wanted) {
|
|
|
2664
2666
|
const provider = subscriptionProvider(bot.subscription?.provider);
|
|
2665
2667
|
if (!provider)
|
|
2666
2668
|
return undefined;
|
|
2669
|
+
const usable = (model) => !engineResting(candidate, model);
|
|
2667
2670
|
const flagship = wanted !== undefined ? undefined : provider.host ? hostDefaultModel(provider, candidate) : frontierChoice(bot, candidate)?.model;
|
|
2668
|
-
if (flagship)
|
|
2671
|
+
if (flagship && usable(flagship))
|
|
2669
2672
|
return flagship;
|
|
2670
2673
|
return bestProviderModel({
|
|
2671
2674
|
provider, candidate, wanted: wanted ?? engineTierClasses(cfg.engineTiers)[botTier(bot)],
|
|
2672
|
-
classOf: (model) => modelClassOf(candidate.driverKind, model), ranked: rankedModelIds(candidate.driverKind),
|
|
2675
|
+
classOf: (model) => modelClassOf(candidate.driverKind, model), ranked: rankedModelIds(candidate.driverKind), usable,
|
|
2673
2676
|
});
|
|
2674
2677
|
}
|
|
2675
2678
|
/** Thrown when a subscription bot's turn has no account of its provider
|
|
@@ -10041,7 +10044,7 @@ async function startTurn(botId, text, opts) {
|
|
|
10041
10044
|
// the bot's first tool call (desktop attachments are host paths).
|
|
10042
10045
|
const attachedText = commsDepth === 0 ? await withZipListings(providerText) : providerText;
|
|
10043
10046
|
const setupText = agentsMounted ? expandSetupTurnText(attachedText) : attachedText;
|
|
10044
|
-
const userTurnText = withAssistantProvenance(promptWithReply(
|
|
10047
|
+
const userTurnText = withAssistantProvenance(promptWithReply(expandSkillTurnText(setupText, skillAuthoring), opts?.replyTo, cfg.profile?.name?.trim() || "User"), userMessage.sender, cfg.profile?.name?.trim() || "the person");
|
|
10045
10048
|
// Decided again at dispatch when setup outlasted a soul edit (config).
|
|
10046
10049
|
const decideContext = (config) => {
|
|
10047
10050
|
const handedStale = Boolean(handed && (!unseen || (externalUpdate && handed.config !== config)));
|
|
@@ -12536,7 +12539,7 @@ failover) {
|
|
|
12536
12539
|
.filter(Boolean)
|
|
12537
12540
|
.join("\n");
|
|
12538
12541
|
const latestUserText = usesNativeImageInput ? resolvedLatestImages.text : latestUser?.text;
|
|
12539
|
-
const learnTurn =
|
|
12542
|
+
const learnTurn = latestUserText ? expandSkillTurnText(latestUserText, skillAuthoring) : "";
|
|
12540
12543
|
const learnBlock = learnTurn && learnTurn !== latestUserText ? `\n\n${learnTurn}` : "";
|
|
12541
12544
|
const addressedRequest = orchestration?.roomHandoffId ? roomHandoffs.nodes.get(orchestration.roomHandoffId) : undefined;
|
|
12542
12545
|
// The room transcript already carries recent requests and reports. Repeat
|
|
@@ -16167,6 +16170,14 @@ ROUTES.push(createThreadStopRoutes({
|
|
|
16167
16170
|
routines?.activeBotRunForBot(botId)?.threadId === threadId,
|
|
16168
16171
|
stop: stopBotThread,
|
|
16169
16172
|
}));
|
|
16173
|
+
const sharedDeliverables = new SharedDeliverables(DATA_DIR);
|
|
16174
|
+
const deliverableAccess = (senderId, auth) => sharedDeliverableAccess(sharedDeliverables, {
|
|
16175
|
+
dataDir: DATA_DIR, pawRoot: paw.root, store, senderId,
|
|
16176
|
+
canSeeBot: auth ? (id) => visibleTo(viewerFor(auth)).bot(id) : () => true,
|
|
16177
|
+
canSeeGroup: auth ? (id) => visibleTo(viewerFor(auth)).group(id) : () => true,
|
|
16178
|
+
});
|
|
16179
|
+
ROUTES.push(createSharedDeliverablesRoutes(sharedDeliverables, () => deliverableAccess(undefined).sharedBlockedRoots ?? []));
|
|
16180
|
+
ROUTES.push(createMessageFileRoutes({ store, attachments: ATTACHMENTS_DIR, workspaceDir, access: deliverableAccess }));
|
|
16170
16181
|
ROUTES.push(createPinOrderRoutes({ store, visible: (auth) => visibleTo(viewerFor(auth)), personalHidden: personalBotHidden, fromBot: (req) => authorizedInternalCapability(req.headers.authorization) !== null }));
|
|
16171
16182
|
// Linked repos: Settings > Repos, per-thread links and worktrees. See
|
|
16172
16183
|
// server/routes/repos.ts for the routes and docs/chibitek/README.md.
|
|
@@ -19213,118 +19224,6 @@ const handleRequest = async (req, res) => {
|
|
|
19213
19224
|
});
|
|
19214
19225
|
return res.end(bytes);
|
|
19215
19226
|
}
|
|
19216
|
-
// Download one local file only when this exact stored message grants it:
|
|
19217
|
-
// a bot must render a Markdown link or carry a generated-image attachment,
|
|
19218
|
-
// while a user message must carry the standalone composer tag. The bot
|
|
19219
|
-
// branch derives conversation/workspace roots; the user branch is limited
|
|
19220
|
-
// to OpenMausBot's private attachment directory. This is deliberately not
|
|
19221
|
-
// a general path reader.
|
|
19222
|
-
m = path.match(/^\/api\/threads\/([\w-]+)\/messages\/([\w-]+)\/file$/);
|
|
19223
|
-
const streamsMessageImage = Boolean(m && method === "GET" && url.searchParams.get("preview") === "1");
|
|
19224
|
-
if (m && (method === "POST" || streamsMessageImage)) {
|
|
19225
|
-
if (method === "POST" && !String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
19226
|
-
return json(res, 415, { error: "content-type must be application/json" });
|
|
19227
|
-
}
|
|
19228
|
-
const threadId = m[1];
|
|
19229
|
-
const directBot = store.botByThread(threadId);
|
|
19230
|
-
const group = directBot ? undefined : store.groupByThread(threadId);
|
|
19231
|
-
if (!directBot && !group)
|
|
19232
|
-
return json(res, 404, { error: "no such conversation" });
|
|
19233
|
-
const message = store.messagesFor(threadId).find((candidate) => candidate.id === m[2]);
|
|
19234
|
-
if (!message)
|
|
19235
|
-
return json(res, 404, { error: "no such message" });
|
|
19236
|
-
if (message.kind !== "text") {
|
|
19237
|
-
return json(res, 403, { error: "that message does not share this file" });
|
|
19238
|
-
}
|
|
19239
|
-
const body = method === "POST" ? await readBody(req) : null;
|
|
19240
|
-
const rawReference = streamsMessageImage ? url.searchParams.get("ref") : null;
|
|
19241
|
-
if (streamsMessageImage && (!rawReference || !/^\d+$/.test(rawReference))) {
|
|
19242
|
-
return json(res, 400, { error: "ref must identify a rendered image" });
|
|
19243
|
-
}
|
|
19244
|
-
const href = method === "POST"
|
|
19245
|
-
? (typeof body.path === "string" ? body.path : "")
|
|
19246
|
-
: messageImageTargetAt(message.text ?? "", Number(rawReference));
|
|
19247
|
-
if (!href)
|
|
19248
|
-
return json(res, 400, { error: "path is required" });
|
|
19249
|
-
// Generated image paths are durable capabilities on this exact message.
|
|
19250
|
-
// They do not grant access to arbitrary workspace files or Markdown refs.
|
|
19251
|
-
const generatedImage = method === "POST" && message.role === "bot" &&
|
|
19252
|
-
message.attachments?.some((attachment) => attachment.kind === "image" && attachment.path === href) === true;
|
|
19253
|
-
let roots;
|
|
19254
|
-
let downloadName;
|
|
19255
|
-
if (generatedImage) {
|
|
19256
|
-
roots = [ATTACHMENTS_DIR];
|
|
19257
|
-
}
|
|
19258
|
-
else if (message.role === "user") {
|
|
19259
|
-
downloadName = messageAttachmentName(message.text ?? "", href) ?? undefined;
|
|
19260
|
-
if (!downloadName) {
|
|
19261
|
-
return json(res, 403, { error: "that message does not share this file" });
|
|
19262
|
-
}
|
|
19263
|
-
roots = [ATTACHMENTS_DIR];
|
|
19264
|
-
}
|
|
19265
|
-
else {
|
|
19266
|
-
if (!messageReferencesFile(message.text ?? "", href)) {
|
|
19267
|
-
return json(res, 403, { error: "that bot message does not link to this file" });
|
|
19268
|
-
}
|
|
19269
|
-
const senderId = directBot?.id ?? message.from?.botId;
|
|
19270
|
-
// The persisted bot-role message is the author record. Membership is
|
|
19271
|
-
// intentionally not consulted: removing a bot must not break files it
|
|
19272
|
-
// already shared in channel history.
|
|
19273
|
-
if (!senderId) {
|
|
19274
|
-
return json(res, 403, { error: "the file's bot author could not be verified" });
|
|
19275
|
-
}
|
|
19276
|
-
let pinnedCwd;
|
|
19277
|
-
let configuredCwd;
|
|
19278
|
-
if (directBot) {
|
|
19279
|
-
pinnedCwd = store.taskByThread(directBot.id, threadId)?.cwd;
|
|
19280
|
-
configuredCwd = directBot.cwd;
|
|
19281
|
-
}
|
|
19282
|
-
else if (group) {
|
|
19283
|
-
const task = store.groupTaskByThread(group.id, threadId);
|
|
19284
|
-
pinnedCwd = task ? task.pinnedCwd : group.threadId === threadId ? group.pinnedCwd : undefined;
|
|
19285
|
-
configuredCwd = group.cwd;
|
|
19286
|
-
}
|
|
19287
|
-
roots = messageFileRoots({
|
|
19288
|
-
senderWorkspace: workspaceDir(senderId),
|
|
19289
|
-
attachments: ATTACHMENTS_DIR,
|
|
19290
|
-
pinnedCwd,
|
|
19291
|
-
configuredCwd,
|
|
19292
|
-
});
|
|
19293
|
-
}
|
|
19294
|
-
const file = await openMessageFile(href, roots);
|
|
19295
|
-
if ((streamsMessageImage || generatedImage) && !file.mime.startsWith("image/")) {
|
|
19296
|
-
await file.handle.close();
|
|
19297
|
-
return json(res, 415, { error: "only images can be previewed here" });
|
|
19298
|
-
}
|
|
19299
|
-
res.writeHead(200, {
|
|
19300
|
-
"content-type": file.mime,
|
|
19301
|
-
"content-length": String(file.bytes),
|
|
19302
|
-
...(streamsMessageImage
|
|
19303
|
-
? { "content-disposition": "inline" }
|
|
19304
|
-
: { "content-disposition": messageFileDisposition(messageFileDownloadName(downloadName, file.name)) }),
|
|
19305
|
-
"cache-control": streamsMessageImage ? "private, max-age=3600" : "private, no-store",
|
|
19306
|
-
"cdn-cache-control": "no-store",
|
|
19307
|
-
"cloudflare-cdn-cache-control": "no-store",
|
|
19308
|
-
pragma: "no-cache",
|
|
19309
|
-
vary: "Authorization",
|
|
19310
|
-
"x-content-type-options": "nosniff",
|
|
19311
|
-
...(streamsMessageImage
|
|
19312
|
-
? {
|
|
19313
|
-
"cross-origin-resource-policy": "same-origin",
|
|
19314
|
-
"referrer-policy": "no-referrer",
|
|
19315
|
-
}
|
|
19316
|
-
: {}),
|
|
19317
|
-
});
|
|
19318
|
-
if (file.bytes === 0) {
|
|
19319
|
-
await file.handle.close();
|
|
19320
|
-
return res.end();
|
|
19321
|
-
}
|
|
19322
|
-
const stream = file.handle.createReadStream({ start: 0, end: file.bytes - 1, autoClose: true });
|
|
19323
|
-
stream.on("error", () => res.destroy());
|
|
19324
|
-
res.on("close", () => stream.destroy());
|
|
19325
|
-
stream.pipe(res);
|
|
19326
|
-
return;
|
|
19327
|
-
}
|
|
19328
19227
|
// ── image attachments ────────────────────────────────────────────────
|
|
19329
19228
|
// Pasted/dropped images are stored as files and referenced by path in
|
|
19330
19229
|
// the prompt (<attached-image path="…"/>); this pair of routes is the
|
|
@@ -25136,6 +25035,8 @@ const handleRequest = async (req, res) => {
|
|
|
25136
25035
|
}
|
|
25137
25036
|
return json(res, 405, { error: "method not allowed" });
|
|
25138
25037
|
}
|
|
25038
|
+
if (await remoteDesktopRoute(path, method, req, res, { store, surface: computerPreviewSurface, controlHeld: (id) => botComputerControlSnapshot(id).held }))
|
|
25039
|
+
return;
|
|
25139
25040
|
m = path.match(/^\/api\/bots\/([\w-]+)\/computer\/viewer-close$/);
|
|
25140
25041
|
if (m && method === "POST") {
|
|
25141
25042
|
const bot = computerPreviewBot(m[1], url);
|
|
@@ -343,16 +343,25 @@ function mimeFor(path) {
|
|
|
343
343
|
default: return "application/octet-stream";
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
|
-
|
|
347
|
-
* Open a message-linked file under one of the supplied roots. The returned
|
|
348
|
-
* handle, not the pathname, is what the caller streams. We canonicalise both
|
|
349
|
-
* before and after opening and compare the handle with the post-open path;
|
|
350
|
-
* this rejects symlink escapes and directory swaps instead of checking a
|
|
351
|
-
* path and then opening a potentially different file.
|
|
352
|
-
*/
|
|
353
|
-
export async function openMessageFile(href, roots) {
|
|
346
|
+
export async function openMessageFile(href, roots, access = {}) {
|
|
354
347
|
const requested = referencedPath(href);
|
|
355
|
-
const
|
|
348
|
+
const canonicalize = async (paths) => Promise.all(paths.map(async (path) => {
|
|
349
|
+
try {
|
|
350
|
+
return await realpath(path);
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
return resolve(path);
|
|
354
|
+
}
|
|
355
|
+
}));
|
|
356
|
+
const blocked = [...(access.blockedRoots ?? []), ...await canonicalize(access.blockedRoots ?? [])];
|
|
357
|
+
const sharedBlocked = [...(access.sharedBlockedRoots ?? []), ...await canonicalize(access.sharedBlockedRoots ?? [])];
|
|
358
|
+
const isBlocked = (paths, candidate) => paths.some((root) => containedBy(resolve(root), candidate));
|
|
359
|
+
// Relative links keep their original conversation-relative meaning.
|
|
360
|
+
const shared = isAbsolute(requested) ? access.sharedRoots ?? [] : [];
|
|
361
|
+
const boundaries = await canonicalize(access.sharedRootBoundaries ?? []);
|
|
362
|
+
const canonicalShared = (await canonicalize(shared)).filter(root => !boundaries.some(boundary => containedBy(root, boundary)));
|
|
363
|
+
const canonicalOrdinary = await canonicalize(roots);
|
|
364
|
+
const canonicalRoots = (await Promise.all([...roots, ...canonicalShared].map(async (root) => {
|
|
356
365
|
try {
|
|
357
366
|
const canonical = await realpath(root);
|
|
358
367
|
return (await stat(canonical)).isDirectory() ? canonical : null;
|
|
@@ -375,7 +384,15 @@ export async function openMessageFile(href, roots) {
|
|
|
375
384
|
catch {
|
|
376
385
|
continue;
|
|
377
386
|
}
|
|
378
|
-
const
|
|
387
|
+
const permitted = (canonical) => {
|
|
388
|
+
if (isBlocked(blocked, candidate) || isBlocked(blocked, canonical))
|
|
389
|
+
return false;
|
|
390
|
+
if (canonicalOrdinary.some((root) => containedBy(root, canonical)))
|
|
391
|
+
return true;
|
|
392
|
+
return canonicalShared.some((root) => containedBy(root, canonical))
|
|
393
|
+
&& !isBlocked(sharedBlocked, candidate) && !isBlocked(sharedBlocked, canonical);
|
|
394
|
+
};
|
|
395
|
+
const root = permitted(canonicalBefore) && canonicalRoots.find((allowed) => containedBy(allowed, canonicalBefore));
|
|
379
396
|
if (!root) {
|
|
380
397
|
sawOutsideRoot = true;
|
|
381
398
|
continue;
|
|
@@ -391,7 +408,7 @@ export async function openMessageFile(href, roots) {
|
|
|
391
408
|
throw statusError(413, `file exceeds ${MESSAGE_FILE_MAX_BYTES} bytes`);
|
|
392
409
|
}
|
|
393
410
|
const canonicalAfter = await realpath(candidate);
|
|
394
|
-
if (!containedBy(root, canonicalAfter))
|
|
411
|
+
if (!containedBy(root, canonicalAfter) || !permitted(canonicalAfter))
|
|
395
412
|
throw statusError(403, "the linked file is outside this conversation's workspace");
|
|
396
413
|
const after = await stat(canonicalAfter);
|
|
397
414
|
if (opened.dev !== after.dev || opened.ino !== after.ino) {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// This Mac from the phone: a picture of the whole screen, and taps, keys and
|
|
2
|
+
// typing sent back through the same Cua Driver the bots already use. No VNC
|
|
3
|
+
// server and no second sign-in. The gates are the companion's per-device
|
|
4
|
+
// remote desktop capability and the person's Take control, which pauses the
|
|
5
|
+
// bot so a tap never races its next action.
|
|
6
|
+
//
|
|
7
|
+
// ponytail: one still frame per request (about 1 per second on a Retina Mac).
|
|
8
|
+
// A streamed video path is the upgrade if watching motion ever matters.
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { promisify } from "node:util";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import { json, readBody } from "./harness/http.js";
|
|
16
|
+
import { readCuaConnection, readCuaUnavailableReason } from "./local-computer.js";
|
|
17
|
+
const run = promisify(execFile);
|
|
18
|
+
const coordinate = z.number().finite().min(0).max(100_000);
|
|
19
|
+
const key = z.string().regex(/^[a-z0-9]{1,12}$/i);
|
|
20
|
+
const DESKTOP = { kind: "desktop", display_id: "primary" };
|
|
21
|
+
export const remoteInputSchema = z.discriminatedUnion("type", [
|
|
22
|
+
z.object({
|
|
23
|
+
type: z.literal("click"),
|
|
24
|
+
x: coordinate,
|
|
25
|
+
y: coordinate,
|
|
26
|
+
button: z.enum(["left", "right"]).default("left"),
|
|
27
|
+
count: z.number().int().min(1).max(2).default(1),
|
|
28
|
+
}),
|
|
29
|
+
z.object({ type: z.literal("type"), text: z.string().min(1).max(2_000) }),
|
|
30
|
+
z.object({
|
|
31
|
+
type: z.literal("key"),
|
|
32
|
+
key,
|
|
33
|
+
modifiers: z.array(z.enum(["cmd", "shift", "option", "ctrl"])).max(4).default([]),
|
|
34
|
+
}),
|
|
35
|
+
z.object({
|
|
36
|
+
type: z.literal("scroll"),
|
|
37
|
+
x: coordinate,
|
|
38
|
+
y: coordinate,
|
|
39
|
+
direction: z.enum(["up", "down", "left", "right"]),
|
|
40
|
+
amount: z.number().int().min(1).max(20).default(3),
|
|
41
|
+
}),
|
|
42
|
+
]);
|
|
43
|
+
export function hostCuaCall(connection) {
|
|
44
|
+
const socket = connection.args[connection.args.indexOf("--socket") + 1];
|
|
45
|
+
if (!connection.args.includes("--socket") || !socket)
|
|
46
|
+
throw new Error("Cua Driver has no socket for this computer");
|
|
47
|
+
return async (tool, args, screenshotOut) => {
|
|
48
|
+
const { stdout } = await run(connection.command, ["call", tool, JSON.stringify(args), "--socket", socket, ...(screenshotOut ? ["--screenshot-out-file", screenshotOut] : [])], { env: { ...process.env, ...connection.env }, timeout: 15_000, maxBuffer: 4 * 1024 * 1024 });
|
|
49
|
+
return JSON.parse(stdout);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** The whole primary display. `width`/`height` are the coordinate space
|
|
53
|
+
* clicks use; the image itself is a smaller JPEG to keep it phone-sized. */
|
|
54
|
+
export async function remoteFrame(call, shrink = sipsJpeg) {
|
|
55
|
+
const dir = await mkdtemp(join(tmpdir(), "atomic-remote-"));
|
|
56
|
+
try {
|
|
57
|
+
const png = join(dir, "screen.png");
|
|
58
|
+
const state = z.object({ screenshot_width: z.number().int().positive(), screenshot_height: z.number().int().positive() })
|
|
59
|
+
.parse(await call("get_desktop_state", {}, png));
|
|
60
|
+
const jpeg = join(dir, "screen.jpg");
|
|
61
|
+
await shrink(png, jpeg);
|
|
62
|
+
return {
|
|
63
|
+
image: `data:image/jpeg;base64,${(await readFile(jpeg)).toString("base64")}`,
|
|
64
|
+
width: state.screenshot_width,
|
|
65
|
+
height: state.screenshot_height,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
// The picture is the person's screen: never leave it in a temp folder.
|
|
70
|
+
await rm(dir, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function sipsJpeg(input, output) {
|
|
74
|
+
await run("/usr/bin/sips", ["-s", "format", "jpeg", "-s", "formatOptions", "60", "-Z", "1600", input, "--out", output], { timeout: 15_000 });
|
|
75
|
+
}
|
|
76
|
+
/** Keys and typing go to the frontmost app, the one the person last tapped. */
|
|
77
|
+
async function frontmostPid(call) {
|
|
78
|
+
const { apps } = z.object({ apps: z.array(z.object({ pid: z.number().int().positive(), active: z.boolean().optional() }).passthrough()) })
|
|
79
|
+
.parse(await call("list_apps", {}));
|
|
80
|
+
const active = apps.find((app) => app.active);
|
|
81
|
+
if (!active)
|
|
82
|
+
throw Object.assign(new Error("No app is in front on this computer; tap a window first"), { status: 409 });
|
|
83
|
+
return active.pid;
|
|
84
|
+
}
|
|
85
|
+
export async function remoteInput(call, input) {
|
|
86
|
+
switch (input.type) {
|
|
87
|
+
case "click":
|
|
88
|
+
await call("click", { target: DESKTOP, x: input.x, y: input.y, button: input.button, count: input.count });
|
|
89
|
+
return;
|
|
90
|
+
case "scroll":
|
|
91
|
+
await call("scroll", { target: DESKTOP, x: input.x, y: input.y, direction: input.direction, amount: input.amount });
|
|
92
|
+
return;
|
|
93
|
+
case "type":
|
|
94
|
+
await call("type_text", { pid: await frontmostPid(call), text: input.text });
|
|
95
|
+
return;
|
|
96
|
+
case "key":
|
|
97
|
+
await call("press_key", { pid: await frontmostPid(call), key: input.key, modifiers: input.modifiers });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** `POST /api/bots/:id/computer/remote-frame|remote-input`. The companion
|
|
102
|
+
* admits these only for a device with remote desktop access; input also needs
|
|
103
|
+
* the person to hold Take control, which pauses the bot. */
|
|
104
|
+
export async function remoteDesktopRoute(path, method, req, res, deps) {
|
|
105
|
+
const m = path.match(/^\/api\/bots\/([\w-]+)\/computer\/remote-(frame|input)$/);
|
|
106
|
+
if (!m || method !== "POST")
|
|
107
|
+
return false;
|
|
108
|
+
const reply = (status, body) => (json(res, status, body), true);
|
|
109
|
+
const bot = deps.store.bot(m[1]);
|
|
110
|
+
if (!bot)
|
|
111
|
+
return reply(404, { error: "no such bot" });
|
|
112
|
+
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
113
|
+
return reply(415, { error: "content-type must be application/json" });
|
|
114
|
+
}
|
|
115
|
+
if ((deps.platform ?? process.platform) !== "darwin" || await deps.surface(bot, bot.threadId) !== "local") {
|
|
116
|
+
return reply(409, { error: "Phone remote control works for This Mac for now. Choose This Mac for this bot, or open the cloud desktop." });
|
|
117
|
+
}
|
|
118
|
+
const call = (deps.cua ?? hostCall)();
|
|
119
|
+
if (!call)
|
|
120
|
+
return reply(409, { error: readCuaUnavailableReason() ?? "CUA Driver is not ready on this computer" });
|
|
121
|
+
res.setHeader("cache-control", "private, no-store");
|
|
122
|
+
try {
|
|
123
|
+
if (m[2] === "frame")
|
|
124
|
+
return reply(200, await remoteFrame(call));
|
|
125
|
+
if (!deps.controlHeld(bot.id))
|
|
126
|
+
return reply(409, { error: "Take control before using this computer" });
|
|
127
|
+
const input = remoteInputSchema.safeParse(await readBody(req));
|
|
128
|
+
if (!input.success)
|
|
129
|
+
return reply(400, { error: "invalid remote input" });
|
|
130
|
+
await remoteInput(call, input.data);
|
|
131
|
+
return reply(200, { ok: true });
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error.status !== 409)
|
|
135
|
+
throw error;
|
|
136
|
+
return reply(409, { error: error.message });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function hostCall() {
|
|
140
|
+
const connection = readCuaConnection();
|
|
141
|
+
return connection ? hostCuaCall(connection) : null;
|
|
142
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { messageAttachmentName, messageFileDisposition, messageFileDownloadName, messageFileRoots, messageImageTargetAt, messageReferencesFile, openMessageFile } from "../message-file.js";
|
|
2
|
+
import { PASS } from "./table.js";
|
|
3
|
+
export function createMessageFileRoutes({ store, attachments: ATTACHMENTS_DIR, workspaceDir, access }) {
|
|
4
|
+
return async ({ req, res, url, path, method, auth, json, readBody }) => {
|
|
5
|
+
const m = path.match(/^\/api\/threads\/([\w-]+)\/messages\/([\w-]+)\/file$/);
|
|
6
|
+
const streamsMessageImage = Boolean(m && method === "GET" && url.searchParams.get("preview") === "1");
|
|
7
|
+
if (m && (method === "POST" || streamsMessageImage)) {
|
|
8
|
+
if (method === "POST" && !String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
9
|
+
return json(res, 415, { error: "content-type must be application/json" });
|
|
10
|
+
}
|
|
11
|
+
const threadId = m[1];
|
|
12
|
+
const directBot = store.botByThread(threadId);
|
|
13
|
+
const group = directBot ? undefined : store.groupByThread(threadId);
|
|
14
|
+
if (!directBot && !group)
|
|
15
|
+
return json(res, 404, { error: "no such conversation" });
|
|
16
|
+
const message = store.messagesFor(threadId).find((candidate) => candidate.id === m[2]);
|
|
17
|
+
if (!message)
|
|
18
|
+
return json(res, 404, { error: "no such message" });
|
|
19
|
+
if (message.kind !== "text") {
|
|
20
|
+
return json(res, 403, { error: "that message does not share this file" });
|
|
21
|
+
}
|
|
22
|
+
const body = method === "POST" ? await readBody(req) : null;
|
|
23
|
+
const rawReference = streamsMessageImage ? url.searchParams.get("ref") : null;
|
|
24
|
+
if (streamsMessageImage && (!rawReference || !/^\d+$/.test(rawReference))) {
|
|
25
|
+
return json(res, 400, { error: "ref must identify a rendered image" });
|
|
26
|
+
}
|
|
27
|
+
const href = method === "POST"
|
|
28
|
+
? (typeof body.path === "string" ? body.path : "")
|
|
29
|
+
: messageImageTargetAt(message.text ?? "", Number(rawReference));
|
|
30
|
+
if (!href)
|
|
31
|
+
return json(res, 400, { error: "path is required" });
|
|
32
|
+
// Generated image paths are durable capabilities on this exact message.
|
|
33
|
+
// They do not grant access to arbitrary workspace files or Markdown refs.
|
|
34
|
+
const generatedImage = method === "POST" && message.role === "bot" &&
|
|
35
|
+
message.attachments?.some((attachment) => attachment.kind === "image" && attachment.path === href) === true;
|
|
36
|
+
let roots;
|
|
37
|
+
let downloadName;
|
|
38
|
+
if (generatedImage) {
|
|
39
|
+
roots = [ATTACHMENTS_DIR];
|
|
40
|
+
}
|
|
41
|
+
else if (message.role === "user") {
|
|
42
|
+
downloadName = messageAttachmentName(message.text ?? "", href) ?? undefined;
|
|
43
|
+
if (!downloadName) {
|
|
44
|
+
return json(res, 403, { error: "that message does not share this file" });
|
|
45
|
+
}
|
|
46
|
+
roots = [ATTACHMENTS_DIR];
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
if (!messageReferencesFile(message.text ?? "", href)) {
|
|
50
|
+
return json(res, 403, { error: "that bot message does not link to this file" });
|
|
51
|
+
}
|
|
52
|
+
const senderId = directBot?.id ?? message.from?.botId;
|
|
53
|
+
// The persisted bot-role message is the author record. Membership is
|
|
54
|
+
// intentionally not consulted: removing a bot must not break files it
|
|
55
|
+
// already shared in channel history.
|
|
56
|
+
if (!senderId) {
|
|
57
|
+
return json(res, 403, { error: "the file's bot author could not be verified" });
|
|
58
|
+
}
|
|
59
|
+
let pinnedCwd;
|
|
60
|
+
let configuredCwd;
|
|
61
|
+
if (directBot) {
|
|
62
|
+
pinnedCwd = store.taskByThread(directBot.id, threadId)?.cwd;
|
|
63
|
+
configuredCwd = directBot.cwd;
|
|
64
|
+
}
|
|
65
|
+
else if (group) {
|
|
66
|
+
const task = store.groupTaskByThread(group.id, threadId);
|
|
67
|
+
pinnedCwd = task ? task.pinnedCwd : group.threadId === threadId ? group.pinnedCwd : undefined;
|
|
68
|
+
configuredCwd = group.cwd;
|
|
69
|
+
}
|
|
70
|
+
roots = messageFileRoots({
|
|
71
|
+
senderWorkspace: workspaceDir(senderId),
|
|
72
|
+
attachments: ATTACHMENTS_DIR,
|
|
73
|
+
pinnedCwd,
|
|
74
|
+
configuredCwd,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const file = await openMessageFile(href, roots, message.role === "bot" && !generatedImage
|
|
78
|
+
? access(directBot?.id ?? message.from?.botId, auth) : {});
|
|
79
|
+
if ((streamsMessageImage || generatedImage) && !file.mime.startsWith("image/")) {
|
|
80
|
+
await file.handle.close();
|
|
81
|
+
return json(res, 415, { error: "only images can be previewed here" });
|
|
82
|
+
}
|
|
83
|
+
res.writeHead(200, {
|
|
84
|
+
"content-type": file.mime,
|
|
85
|
+
"content-length": String(file.bytes),
|
|
86
|
+
...(streamsMessageImage
|
|
87
|
+
? { "content-disposition": "inline" }
|
|
88
|
+
: { "content-disposition": messageFileDisposition(messageFileDownloadName(downloadName, file.name)) }),
|
|
89
|
+
"cache-control": streamsMessageImage ? "private, max-age=3600" : "private, no-store",
|
|
90
|
+
"cdn-cache-control": "no-store",
|
|
91
|
+
"cloudflare-cdn-cache-control": "no-store",
|
|
92
|
+
pragma: "no-cache",
|
|
93
|
+
vary: "Authorization",
|
|
94
|
+
"x-content-type-options": "nosniff",
|
|
95
|
+
...(streamsMessageImage
|
|
96
|
+
? {
|
|
97
|
+
"cross-origin-resource-policy": "same-origin",
|
|
98
|
+
"referrer-policy": "no-referrer",
|
|
99
|
+
}
|
|
100
|
+
: {}),
|
|
101
|
+
});
|
|
102
|
+
if (file.bytes === 0) {
|
|
103
|
+
await file.handle.close();
|
|
104
|
+
res.end();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const stream = file.handle.createReadStream({ start: 0, end: file.bytes - 1, autoClose: true });
|
|
108
|
+
stream.on("error", () => res.destroy());
|
|
109
|
+
res.on("close", () => stream.destroy());
|
|
110
|
+
stream.pipe(res);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
return PASS;
|
|
114
|
+
};
|
|
115
|
+
}
|