openmausbot 0.1.85 → 0.1.86
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/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/abnfDiagram-O67JEVCF-DpUZNnFD.js +1 -0
- package/dist/assets/arc-DW7mrI4p.js +1 -0
- package/dist/assets/architectureDiagram-NJMV4G6O-CyeZ5WLZ.js +36 -0
- package/dist/assets/blockDiagram-BEXU5L5S-CBVaNjO7.js +140 -0
- package/dist/assets/c4Diagram-YGBWAQC7-sf8d41uT.js +38 -0
- package/dist/assets/channel-BXFkgus9.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-BOGvy7Y_.js +1 -0
- package/dist/assets/chunk-3FUC2YCW-BiF9zyiz.js +2 -0
- package/dist/assets/chunk-5VM5RSS4-EFuZ1SBZ.js +15 -0
- package/dist/assets/chunk-GTNCS2PH-pJLNSAT7.js +88 -0
- package/dist/assets/chunk-GWA4HPMP-Cb-3_3IT.js +1 -0
- package/dist/assets/chunk-JWPE2WC7-DAf5aogn.js +1 -0
- package/dist/assets/chunk-NETBCI7D-DR3kiwZz.js +1 -0
- package/dist/assets/chunk-WEXAMYUT-BmU8PeyM.js +1 -0
- package/dist/assets/chunk-XXDRQBXY-Buh8g8OT.js +1 -0
- package/dist/assets/classDiagram-v2-NBCMYWYE-BurfmMWM.js +217 -0
- package/dist/assets/cose-bilkent-JH36ORCC-KkGZUEkU.js +1 -0
- package/dist/assets/cynefin-OW5HDTMX-C3PpV96d.js +166 -0
- package/dist/assets/cynefinDiagram-VND7K2PF-Dt8JwJaI.js +62 -0
- package/dist/assets/cytoscape.esm-BB4DxJjf.js +321 -0
- package/dist/assets/dagre-6A5THRUB-DkjN8266.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-22UHCM2B-DUAJS0JV.js +200 -0
- package/dist/assets/diagram-3UASUU5V-Dw4KhF1a.js +24 -0
- package/dist/assets/diagram-ATOU4E4O-Dd1gmQqf.js +3 -0
- package/dist/assets/diagram-CDSNMT55-CjwGs0Yw.js +30 -0
- package/dist/assets/diagram-MLGK6HIB-BL-69Zrq.js +24 -0
- package/dist/assets/diagram-MPIPVDR6-5Zi2N7wo.js +41 -0
- package/dist/assets/ebnfDiagram-ZINNZB2B-D9NekWUX.js +1 -0
- package/dist/assets/elk-276RUBZZ-CkaSQjxw.js +27 -0
- package/dist/assets/erDiagram-OPXOYQCR-CdAVBV8-.js +99 -0
- package/dist/assets/flowDiagram-KWPJA3E3-Bm_lWWH7.js +213 -0
- package/dist/assets/ganttDiagram-FUAMR5RP-FZ8ejYWJ.js +292 -0
- package/dist/assets/gitGraphDiagram-X574FWY7-Dlv9rizZ.js +106 -0
- package/dist/assets/graph-D8UmR1vK.js +1 -0
- package/dist/assets/index-BGgqlWPJ.css +1 -0
- package/dist/assets/index-BtHo8ZKl.js +577 -0
- package/dist/assets/{index-CTNNoqSJ.js → index-h9FFNwTu.js} +1 -1
- package/dist/assets/infoDiagram-VRGFBTTK-BdQ_gW5B.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-OU5B5YK6-EwssEp2X.js +70 -0
- package/dist/assets/journeyDiagram-ZHPQQLJL-BX1G1vOd.js +139 -0
- package/dist/assets/kanban-definition-PNTS6WVX-BB4kAsyC.js +89 -0
- package/dist/assets/linear-3su2TG93.js +1 -0
- package/dist/assets/mermaid.core-CDD1E0s9.js +311 -0
- package/dist/assets/mindmap-definition-NLK3R4M7-Ba50MsNK.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pegDiagram-GJSIUBJH-DZLkkZlM.js +1 -0
- package/dist/assets/pieDiagram-5QR66LMP-DaTz77Uq.js +39 -0
- package/dist/assets/quadrantDiagram-O4NWA36T-CpaiidpR.js +7 -0
- package/dist/assets/railroadDiagram-XR7U4H2S-mN4JrMeI.js +1 -0
- package/dist/assets/requirementDiagram-PLB6GJNP-BSNDRpg7.js +84 -0
- package/dist/assets/sankeyDiagram-IPEJSGJF-FngzGNug.js +40 -0
- package/dist/assets/sequenceDiagram-PO4LG4MO-CM4pCC3L.js +169 -0
- package/dist/assets/sizeCapture-INFHLROL-CRICHXkw.js +1 -0
- package/dist/assets/stateDiagram-v2-GCMORJYK-oyTWpxH2.js +291 -0
- package/dist/assets/swimlanes-2SLR337P-D9h9mPBq.js +1 -0
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +8 -0
- package/dist/assets/timeline-definition-EJHVYXUP-BJUlN9bW.js +120 -0
- package/dist/assets/usecaseDiagram-POWQR4AR-3uBNkbAb.js +370 -0
- package/dist/assets/vennDiagram-UO4OBE2U-D2odalzT.js +34 -0
- package/dist/assets/wardleyDiagram-VNRHLVJA-DHgFaB9S.js +78 -0
- package/dist/assets/xychartDiagram-PMCCYNJV-w5GYM2h0.js +7 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +19 -10
- package/dist-server/drivers/agents-proxy.js +16159 -1596
- package/dist-server/enterprise/LICENSE +32 -0
- package/dist-server/enterprise/server/index.js +5 -5
- package/dist-server/index.js +4779 -1667
- package/dist-server/mcp-server.js +3 -1
- package/dist-server/openmausbot.js +4679 -4266
- package/dist-server/pair-cli.js +4679 -4266
- package/dist-server/server/admin-activity.js +410 -0
- package/dist-server/server/bot-visibility.js +462 -0
- package/dist-server/server/box.js +2 -1
- package/dist-server/server/browser-runtime.js +1 -1
- package/dist-server/server/channel-queue.js +2 -0
- package/dist-server/server/cli-prompts.js +24 -2
- package/dist-server/server/cli.js +79 -10
- package/dist-server/server/composio.js +49 -11
- package/dist-server/server/config.js +79 -13
- package/dist-server/server/decision-log.js +209 -48
- package/dist-server/server/default-model-selection.js +20 -3
- package/dist-server/server/delegations.js +11 -9
- package/dist-server/server/drivers/acp/core.js +110 -22
- package/dist-server/server/drivers/acp/grok.js +4 -3
- package/dist-server/server/drivers/agents-call.js +1123 -0
- package/dist-server/server/drivers/agents-catalog.js +659 -0
- package/dist-server/server/drivers/agents-client.js +28 -0
- package/dist-server/server/drivers/agents-proxy.js +18 -1722
- package/dist-server/server/drivers/chat-mcp-tools.js +17 -2
- package/dist-server/server/drivers/claude.js +21 -6
- package/dist-server/server/drivers/codex-mcp-names.js +44 -0
- package/dist-server/server/drivers/codex.js +26 -4
- package/dist-server/server/drivers/grok.js +2 -1
- package/dist-server/server/drivers/openai-compat.js +15 -8
- package/dist-server/server/enterprise.js +97 -16
- package/dist-server/server/external-runtime.js +83 -0
- package/dist-server/server/guarded-requests.js +60 -0
- package/dist-server/server/harness/http.js +61 -0
- package/dist-server/server/hosted-models.js +151 -0
- package/dist-server/server/hosted-slack.js +11 -0
- package/dist-server/server/index.js +1718 -348
- package/dist-server/server/managed-desktop.js +79 -14
- package/dist-server/server/managed-policy.js +153 -0
- package/dist-server/server/message-db.js +28 -0
- package/dist-server/server/model-context-window.js +7 -0
- package/dist-server/server/model-prices.js +121 -0
- package/dist-server/server/notify.js +10 -1
- package/dist-server/server/peer-roster.js +13 -2
- package/dist-server/server/recent-work.js +15 -0
- package/dist-server/server/request-auth.js +96 -0
- package/dist-server/server/room-handoffs.js +25 -11
- package/dist-server/server/routes/hosted-slack.js +20 -0
- package/dist-server/server/routes/table.js +14 -0
- package/dist-server/server/routines.js +22 -0
- package/dist-server/server/spend.js +111 -16
- package/dist-server/server/steer-queue.js +6 -1
- package/dist-server/server/store.js +292 -21
- package/dist-server/server/team-backup.js +10 -1
- package/dist-server/server/thread-starters.js +53 -0
- package/dist-server/server/usage-ledger.js +31 -8
- package/dist-server/server/workspace-backup-policy.js +9 -2
- package/dist-server/server/workspace-backup.js +7 -4
- package/dist-server/shared/mention-boundary.js +15 -0
- package/dist-server/shared/routines.js +7 -1
- package/dist-server/shared/team-backup.js +2 -0
- package/dist-server/src/lib/composer-attachments.js +16 -0
- package/dist-server/vps-container-mcp.js +19 -10
- package/enterprise/README.md +33 -12
- package/enterprise/server/index.js +5 -5
- package/package.json +1 -1
- package/dist/assets/index-Ba9G44HI.js +0 -310
- package/dist/assets/index-CkRNp7wX.css +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { lstatSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, renameSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
const uuid = z.string().uuid();
|
|
@@ -9,9 +9,18 @@ const connectionSchema = z.object({
|
|
|
9
9
|
email: z.string().email().max(320), deviceId: uuid, token: z.string().regex(/^omg_[A-Za-z0-9_-]{43}$/),
|
|
10
10
|
expiresAt: z.number().finite().int().positive(),
|
|
11
11
|
providers: z.array(z.object({ id: z.enum(["anthropic", "openai", "openrouter"]), configured: z.boolean(), models: z.array(model).max(500) }).strict()).max(3),
|
|
12
|
+
/** The Admin is up but its operator licence lapsed: keep the instances
|
|
13
|
+
* listed as unavailable instead of treating it as a revocation. */
|
|
14
|
+
suspended: z.literal("license-expired").optional(),
|
|
12
15
|
}).strict();
|
|
16
|
+
/** The saved enrollment's identity (never its token), sent before it is
|
|
17
|
+
* cleared or when it has already expired, so references to its old
|
|
18
|
+
* device-scoped ids still move to the stable ids. */
|
|
19
|
+
const identitySchema = z.object({ portalOrigin: z.string().max(2048), organizationId: uuid, email: z.string().email().max(320), deviceId: uuid }).strict();
|
|
20
|
+
const PROVIDER_IDS = ["anthropic", "openai", "openrouter"];
|
|
13
21
|
const signature = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
14
22
|
const expiredMessage = "Company access has ended. Reconnect your organization or explicitly choose a personal model; personal billing will not be used automatically.";
|
|
23
|
+
export const LICENSE_EXPIRED_MESSAGE = "Your organisation's OpenMaus Admin licence has expired. Contact your admin.";
|
|
15
24
|
/** Validate only messages received over Electron's private utility-parent port.
|
|
16
25
|
* Never expose this input to renderer HTTP or merge it into saved AppConfig.
|
|
17
26
|
*/
|
|
@@ -33,10 +42,18 @@ export function parseManagedDesktopConnection(raw, now = Date.now()) {
|
|
|
33
42
|
return { ...value, portalOrigin: url.origin, email: value.email.toLowerCase(), providers: value.providers.map(provider => ({ ...provider, models: [...new Set(provider.models)] })).sort((a, b) => a.id.localeCompare(b.id)) };
|
|
34
43
|
}
|
|
35
44
|
export function companyInstanceId(connection, provider) {
|
|
36
|
-
//
|
|
37
|
-
//
|
|
45
|
+
// Stable for one person in one organisation, so re-enrolling this computer
|
|
46
|
+
// keeps bots' Company model choices and native resume. Another account or
|
|
47
|
+
// organisation still gets separate ids, homes and cursors.
|
|
48
|
+
return `company.${signature([connection.portalOrigin, connection.organizationId, connection.email.toLowerCase()]).slice(0, 24)}.${provider}`;
|
|
49
|
+
}
|
|
50
|
+
/** The pre-0.1.86 id, which also hashed the enrollment's deviceId. */
|
|
51
|
+
export function legacyCompanyInstanceId(connection, provider) {
|
|
38
52
|
return `company.${signature([connection.portalOrigin, connection.organizationId, connection.deviceId]).slice(0, 24)}.${provider}`;
|
|
39
53
|
}
|
|
54
|
+
/** Everything that decides which native instances exist and what they hold.
|
|
55
|
+
* Expiry and licence suspension change availability only, never a process. */
|
|
56
|
+
const instanceSignature = (connection) => signature(connection && (({ expiresAt: _expiresAt, suspended: _suspended, ...identity }) => identity)(connection));
|
|
40
57
|
export function companyInstanceConfigs(connection, runtimeDirectory) {
|
|
41
58
|
const entries = {};
|
|
42
59
|
for (const provider of connection.providers) {
|
|
@@ -86,9 +103,18 @@ export class ManagedDesktopProviders {
|
|
|
86
103
|
known = new Set();
|
|
87
104
|
owns(instanceId) { return this.known.has(instanceId); }
|
|
88
105
|
apply(raw, force = false) {
|
|
89
|
-
const connection = parseManagedDesktopConnection(raw, this.now()), nextSignature =
|
|
90
|
-
if (!force && nextSignature === this.currentSignature)
|
|
106
|
+
const connection = parseManagedDesktopConnection(raw, this.now()), nextSignature = instanceSignature(connection);
|
|
107
|
+
if (!force && nextSignature === this.currentSignature) {
|
|
108
|
+
// A renewal or licence change: same processes, new availability.
|
|
109
|
+
const previous = this.connection;
|
|
110
|
+
this.connection = connection;
|
|
111
|
+
if (connection && previous && (connection.expiresAt !== previous.expiresAt || connection.suspended !== previous.suspended)) {
|
|
112
|
+
clearTimeout(this.timer);
|
|
113
|
+
this.scheduleExpiry(this.revision);
|
|
114
|
+
this.options.onAvailability?.();
|
|
115
|
+
}
|
|
91
116
|
return this.tail;
|
|
117
|
+
}
|
|
92
118
|
this.currentSignature = nextSignature;
|
|
93
119
|
this.connection = connection;
|
|
94
120
|
const revision = ++this.revision;
|
|
@@ -107,6 +133,7 @@ export class ManagedDesktopProviders {
|
|
|
107
133
|
// workspace backups. Device-scoped ids prevent a new enrollment from
|
|
108
134
|
// inheriting another account's native conversation or credentials.
|
|
109
135
|
const directory = this.ensureDirectory(["providers", "company"]);
|
|
136
|
+
await this.migrateLegacyIds(connection);
|
|
110
137
|
const configs = companyInstanceConfigs(connection, directory);
|
|
111
138
|
for (const [id, entry] of Object.entries(configs)) {
|
|
112
139
|
if (this.options.registry.entries().some(existing => existing.instanceId === id))
|
|
@@ -117,7 +144,9 @@ export class ManagedDesktopProviders {
|
|
|
117
144
|
this.ids.add(id);
|
|
118
145
|
this.known.add(id);
|
|
119
146
|
}
|
|
120
|
-
|
|
147
|
+
// Read the current grant: renewals and licence changes update it in place.
|
|
148
|
+
const active = () => revision === this.revision && Boolean(this.connection && this.connection.expiresAt > this.now() && !this.connection.suspended);
|
|
149
|
+
const unavailable = () => this.connection?.suspended === "license-expired" ? LICENSE_EXPIRED_MESSAGE : expiredMessage;
|
|
121
150
|
await this.options.registry.load(configs, instance => {
|
|
122
151
|
const provider = connection.providers.find(provider => companyInstanceId(connection, provider.id) === instance.instanceId);
|
|
123
152
|
const models = { default: provider.models[0], options: provider.models.map(id => ({ id, label: `${id} (Company)` })) };
|
|
@@ -127,17 +156,17 @@ export class ManagedDesktopProviders {
|
|
|
127
156
|
startAuthentication: undefined, getAuthentication: undefined, completeAuthentication: undefined, cancelAuthentication: undefined, signOut: undefined,
|
|
128
157
|
snapshot: async () => {
|
|
129
158
|
if (!active())
|
|
130
|
-
return { state: "unavailable", reason:
|
|
159
|
+
return { state: "unavailable", reason: unavailable() };
|
|
131
160
|
const snapshot = await instance.snapshot();
|
|
132
161
|
if (!active())
|
|
133
|
-
return { state: "unavailable", reason:
|
|
162
|
+
return { state: "unavailable", reason: unavailable() };
|
|
134
163
|
return { ...snapshot, authenticated: snapshot.state === "available", billing: "metered", account: { email: connection.email, organization: connection.organizationName, method: "api-key" } };
|
|
135
164
|
},
|
|
136
165
|
adapter: {
|
|
137
166
|
...instance.adapter,
|
|
138
167
|
sendTurn: async (input) => {
|
|
139
168
|
if (!active())
|
|
140
|
-
throw new Error(
|
|
169
|
+
throw new Error(unavailable());
|
|
141
170
|
if (!input.model || !provider.models.includes(input.model))
|
|
142
171
|
throw new Error("This model is not enabled by your Company administrator.");
|
|
143
172
|
return instance.adapter.sendTurn(input);
|
|
@@ -147,7 +176,7 @@ export class ManagedDesktopProviders {
|
|
|
147
176
|
});
|
|
148
177
|
if (revision === this.revision) {
|
|
149
178
|
this.options.afterReplace?.([...this.ids]);
|
|
150
|
-
this.scheduleExpiry(
|
|
179
|
+
this.scheduleExpiry(revision);
|
|
151
180
|
}
|
|
152
181
|
};
|
|
153
182
|
this.tail = this.tail.catch(() => { }).then(replace).catch(async (error) => {
|
|
@@ -188,17 +217,53 @@ export class ManagedDesktopProviders {
|
|
|
188
217
|
}
|
|
189
218
|
return directory;
|
|
190
219
|
}
|
|
191
|
-
scheduleExpiry(
|
|
220
|
+
scheduleExpiry(revision) {
|
|
221
|
+
const expiresAt = this.connection?.expiresAt;
|
|
222
|
+
if (expiresAt === undefined)
|
|
223
|
+
return;
|
|
192
224
|
this.timer = setTimeout(() => {
|
|
193
225
|
if (revision !== this.revision)
|
|
194
226
|
return;
|
|
195
|
-
if (connection
|
|
196
|
-
this.scheduleExpiry(
|
|
227
|
+
if ((this.connection?.expiresAt ?? 0) > this.now())
|
|
228
|
+
this.scheduleExpiry(revision);
|
|
197
229
|
else
|
|
198
230
|
void this.apply(null).catch(() => { });
|
|
199
|
-
}, Math.min(2_147_483_647, Math.max(1,
|
|
231
|
+
}, Math.min(2_147_483_647, Math.max(1, expiresAt - this.now())));
|
|
200
232
|
this.timer.unref?.();
|
|
201
233
|
}
|
|
234
|
+
/** Moves an enrollment's old device-scoped ids to the stable ids: native
|
|
235
|
+
* homes are renamed only when the stable one does not exist yet, then saved
|
|
236
|
+
* references follow. Best effort: a failure is logged and never keeps
|
|
237
|
+
* Company models down. */
|
|
238
|
+
async migrateLegacyIds(identity) {
|
|
239
|
+
try {
|
|
240
|
+
const directory = this.ensureDirectory(["providers", "company"]);
|
|
241
|
+
const aliases = PROVIDER_IDS.map(provider => ({ from: legacyCompanyInstanceId(identity, provider), to: companyInstanceId(identity, provider) }));
|
|
242
|
+
for (const { from, to } of aliases) {
|
|
243
|
+
const legacy = join(directory, from), stable = join(directory, to);
|
|
244
|
+
if (!existsSync(legacy) || existsSync(stable))
|
|
245
|
+
continue;
|
|
246
|
+
const stat = lstatSync(legacy);
|
|
247
|
+
if (stat.isDirectory() && !stat.isSymbolicLink())
|
|
248
|
+
renameSync(legacy, stable);
|
|
249
|
+
}
|
|
250
|
+
await this.options.migrate?.(aliases);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
console.warn(`[company] could not move saved Company model references to their stable ids: ${error instanceof Error ? error.message : String(error)}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/** Migrates a saved enrollment that is being cleared or has expired. */
|
|
257
|
+
async migrateIdentity(raw) {
|
|
258
|
+
const parsed = identitySchema.parse(raw), url = new URL(parsed.portalOrigin);
|
|
259
|
+
if (url.origin !== parsed.portalOrigin || (url.protocol !== "https:" && !(url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)))) {
|
|
260
|
+
throw new Error("Company identity must use an HTTPS origin or loopback fixture.");
|
|
261
|
+
}
|
|
262
|
+
const identity = { ...parsed, email: parsed.email.toLowerCase() };
|
|
263
|
+
const migration = this.tail.catch(() => { }).then(() => this.migrateLegacyIds(identity));
|
|
264
|
+
this.tail = migration;
|
|
265
|
+
return migration;
|
|
266
|
+
}
|
|
202
267
|
async close() {
|
|
203
268
|
await this.apply(null, true);
|
|
204
269
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** An organisation's desktop policy, received only over Electron's private
|
|
3
|
+
* utility-parent port while this desktop is enrolled. It lives in memory: it
|
|
4
|
+
* is never merged into config.json, and it adds no prompts or approvals —
|
|
5
|
+
* a disallowed action is simply refused with a sentence saying who manages it. */
|
|
6
|
+
const policySchema = z.object({
|
|
7
|
+
organizationId: z.string().uuid(),
|
|
8
|
+
organizationName: z.string().trim().min(1).max(100),
|
|
9
|
+
expiresAt: z.number().int().positive(),
|
|
10
|
+
version: z.number().int().min(0).max(2_147_483_647),
|
|
11
|
+
companyModelsOnly: z.boolean(),
|
|
12
|
+
allowedEngines: z.union([z.literal("all"), z.array(z.string().regex(/^[A-Za-z][A-Za-z0-9-]{0,63}$/)).max(64)]),
|
|
13
|
+
mcp: z.object({ allowCustom: z.boolean(), allowlist: z.array(z.string().trim().min(1).max(200)).max(100) }).strict(),
|
|
14
|
+
computers: z.object({ thisComputer: z.boolean(), localVm: z.boolean(), box: z.boolean(), vps: z.boolean() }).strict(),
|
|
15
|
+
remoteAccess: z.boolean(),
|
|
16
|
+
}).strict();
|
|
17
|
+
const computerLabels = { thisComputer: "this computer", localVm: "local virtual machines", box: "Box cloud computers", vps: "VPS computers" };
|
|
18
|
+
export function parseManagedPolicy(raw, now = Date.now()) {
|
|
19
|
+
if (raw === null)
|
|
20
|
+
return null;
|
|
21
|
+
const parsed = policySchema.safeParse(raw);
|
|
22
|
+
if (!parsed.success)
|
|
23
|
+
throw new Error("Invalid organisation policy from the desktop parent.");
|
|
24
|
+
return parsed.data.expiresAt > now ? parsed.data : null;
|
|
25
|
+
}
|
|
26
|
+
/** `*` in a host pattern stands for one or more whole labels, never part of
|
|
27
|
+
* one, so `*.example.com` cannot match `evil.test/x.example.com`. */
|
|
28
|
+
function hostMatches(pattern, host) {
|
|
29
|
+
if (!pattern.length)
|
|
30
|
+
return !host.length;
|
|
31
|
+
if (pattern[0] === "*") {
|
|
32
|
+
for (let taken = 1; taken <= host.length - (pattern.length - 1); taken++)
|
|
33
|
+
if (hostMatches(pattern.slice(1), host.slice(taken)))
|
|
34
|
+
return true;
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return host.length > 0 && pattern[0] === host[0] && hostMatches(pattern.slice(1), host.slice(1));
|
|
38
|
+
}
|
|
39
|
+
/** A name entry matches the configured server name. An address entry
|
|
40
|
+
* (https:// only) is parsed like the server's URL: no credentials, the same
|
|
41
|
+
* port, the host compared label by label, and the path separately (`*` there
|
|
42
|
+
* matches any characters). Anything that does not parse never matches. */
|
|
43
|
+
export function mcpEntryMatches(entry, name, url) {
|
|
44
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(entry))
|
|
45
|
+
return entry.toLowerCase() === name.toLowerCase();
|
|
46
|
+
if (!url)
|
|
47
|
+
return false;
|
|
48
|
+
let pattern, target;
|
|
49
|
+
try {
|
|
50
|
+
pattern = new URL(entry);
|
|
51
|
+
target = new URL(url);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
if (pattern.protocol !== "https:" || target.protocol !== "https:" || pattern.username || pattern.password || target.username || target.password ||
|
|
57
|
+
pattern.search || pattern.hash || pattern.port !== target.port)
|
|
58
|
+
return false;
|
|
59
|
+
const labels = pattern.hostname.split(".");
|
|
60
|
+
if (labels.some(label => !label || (label !== "*" && label.includes("*"))) || labels.every(label => label === "*"))
|
|
61
|
+
return false;
|
|
62
|
+
if (!hostMatches(labels, target.hostname.split(".")))
|
|
63
|
+
return false;
|
|
64
|
+
const path = pattern.pathname.split("*").map(part => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
|
|
65
|
+
return new RegExp(`^${path}$`).test(target.pathname);
|
|
66
|
+
}
|
|
67
|
+
/** A resource key claimed by bindTurnComputer, mapped to the policy's kind. */
|
|
68
|
+
export function computerKindForResource(resource) {
|
|
69
|
+
if (resource === "computer:host")
|
|
70
|
+
return "thisComputer";
|
|
71
|
+
if (resource.startsWith("computer:vm:"))
|
|
72
|
+
return "localVm";
|
|
73
|
+
if (resource.startsWith("computer:box:") || resource.startsWith("computer:box-bot:"))
|
|
74
|
+
return "box";
|
|
75
|
+
if (resource.startsWith("computer:vps:"))
|
|
76
|
+
return "vps";
|
|
77
|
+
}
|
|
78
|
+
export class ManagedDesktopPolicy {
|
|
79
|
+
policy = null;
|
|
80
|
+
timer;
|
|
81
|
+
now;
|
|
82
|
+
onChange;
|
|
83
|
+
constructor(options = {}) {
|
|
84
|
+
this.now = options.now ?? Date.now;
|
|
85
|
+
this.onChange = options.onChange ?? (() => { });
|
|
86
|
+
}
|
|
87
|
+
apply(raw) {
|
|
88
|
+
const next = parseManagedPolicy(raw, this.now());
|
|
89
|
+
const changed = JSON.stringify(next) !== JSON.stringify(this.policy);
|
|
90
|
+
this.policy = next;
|
|
91
|
+
clearTimeout(this.timer);
|
|
92
|
+
// The last applied policy holds only as long as the device credential.
|
|
93
|
+
if (next) {
|
|
94
|
+
this.timer = setTimeout(() => this.apply(null), Math.min(2_147_483_647, Math.max(1, next.expiresAt - this.now())));
|
|
95
|
+
this.timer.unref?.();
|
|
96
|
+
}
|
|
97
|
+
if (changed)
|
|
98
|
+
this.onChange();
|
|
99
|
+
}
|
|
100
|
+
current() {
|
|
101
|
+
return this.policy && this.policy.expiresAt > this.now() ? this.policy : null;
|
|
102
|
+
}
|
|
103
|
+
close() { clearTimeout(this.timer); this.policy = null; }
|
|
104
|
+
/** Why this organisation does not allow an instance to run a turn, or undefined. */
|
|
105
|
+
modelRefusal(instance, company) {
|
|
106
|
+
const policy = this.current();
|
|
107
|
+
if (!policy)
|
|
108
|
+
return;
|
|
109
|
+
if (policy.companyModelsOnly && !company)
|
|
110
|
+
return `${policy.organizationName} allows only company models on this computer. Choose a Company model for this bot.`;
|
|
111
|
+
if (policy.allowedEngines !== "all" && !policy.allowedEngines.includes(instance.driverKind)) {
|
|
112
|
+
return `${policy.organizationName} does not allow the ${instance.displayName ?? instance.driverKind} engine on this computer. Choose another model for this bot.`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
mcpAllowed(name, url) {
|
|
116
|
+
const policy = this.current();
|
|
117
|
+
return !policy || policy.mcp.allowCustom || policy.mcp.allowlist.some(entry => mcpEntryMatches(entry, name, url));
|
|
118
|
+
}
|
|
119
|
+
/** Only approved servers reach engines while custom servers are off. */
|
|
120
|
+
filterMcp(servers) {
|
|
121
|
+
if (!this.restrictsMcp())
|
|
122
|
+
return servers;
|
|
123
|
+
return Object.fromEntries(Object.entries(servers).filter(([name, server]) => this.mcpAllowed(name, "url" in server && typeof server.url === "string" ? server.url : undefined)));
|
|
124
|
+
}
|
|
125
|
+
restrictsMcp() { const policy = this.current(); return Boolean(policy && !policy.mcp.allowCustom); }
|
|
126
|
+
mcpRefusal(name, url) {
|
|
127
|
+
const policy = this.current();
|
|
128
|
+
if (!policy || this.mcpAllowed(name, url))
|
|
129
|
+
return;
|
|
130
|
+
return `${policy.organizationName} allows only MCP servers it has approved. Ask your administrator to add this server to the approved list.`;
|
|
131
|
+
}
|
|
132
|
+
computerAllowed(kind) { return this.current()?.computers[kind] ?? true; }
|
|
133
|
+
computerRefusal(kind) {
|
|
134
|
+
const policy = this.current();
|
|
135
|
+
if (!policy || policy.computers[kind])
|
|
136
|
+
return;
|
|
137
|
+
return `${policy.organizationName} does not allow bots to use ${computerLabels[kind]}.`;
|
|
138
|
+
}
|
|
139
|
+
remoteAccessRefusal() {
|
|
140
|
+
const policy = this.current();
|
|
141
|
+
if (!policy || policy.remoteAccess)
|
|
142
|
+
return;
|
|
143
|
+
return `${policy.organizationName} does not allow remote access to this computer.`;
|
|
144
|
+
}
|
|
145
|
+
/** Read-only view for the renderer; carries nothing secret. */
|
|
146
|
+
summary() {
|
|
147
|
+
const policy = this.current();
|
|
148
|
+
if (!policy)
|
|
149
|
+
return null;
|
|
150
|
+
const { organizationId: _id, expiresAt: _expires, ...visible } = policy;
|
|
151
|
+
return visible;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -408,6 +408,25 @@ export function setActiveLeaf(threadId, leafId) {
|
|
|
408
408
|
"ON CONFLICT(thread_id) DO UPDATE SET active_leaf_id = excluded.active_leaf_id")
|
|
409
409
|
.run(threadId, leafId);
|
|
410
410
|
}
|
|
411
|
+
/** Newest message timestamp per thread. One grouped read, chunked under
|
|
412
|
+
* SQLite's variable limit. Threads with no rows are absent. */
|
|
413
|
+
export function latestMessageAts(threadIds) {
|
|
414
|
+
const ids = [...new Set(threadIds.filter((id) => id.length > 0))];
|
|
415
|
+
const out = new Map();
|
|
416
|
+
const chunk = 400;
|
|
417
|
+
for (let i = 0; i < ids.length; i += chunk) {
|
|
418
|
+
const slice = ids.slice(i, i + chunk);
|
|
419
|
+
const placeholders = slice.map(() => "?").join(", ");
|
|
420
|
+
const rows = db()
|
|
421
|
+
.prepare(`SELECT thread_id, MAX(at) AS at FROM messages WHERE thread_id IN (${placeholders}) GROUP BY thread_id`)
|
|
422
|
+
.all(...slice);
|
|
423
|
+
for (const row of rows) {
|
|
424
|
+
if (typeof row.at === "number" && Number.isFinite(row.at))
|
|
425
|
+
out.set(row.thread_id, row.at);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
411
430
|
export function deleteThread(threadId) {
|
|
412
431
|
writeFollowups((connection) => {
|
|
413
432
|
connection.prepare("DELETE FROM chat_followups WHERE thread_id = ?").run(threadId);
|
|
@@ -415,6 +434,15 @@ export function deleteThread(threadId) {
|
|
|
415
434
|
connection.prepare("DELETE FROM thread_state WHERE thread_id = ?").run(threadId);
|
|
416
435
|
});
|
|
417
436
|
}
|
|
437
|
+
/** Every thread whose stored messages mention `fragment` anywhere (an
|
|
438
|
+
* attachment's file name, say). A scan, like search; used only to decide
|
|
439
|
+
* whether a member on a workspace with a restricted bot may fetch a file. */
|
|
440
|
+
export function threadsReferencing(fragment) {
|
|
441
|
+
if (!fragment)
|
|
442
|
+
return [];
|
|
443
|
+
const rows = db().prepare("SELECT DISTINCT thread_id FROM messages WHERE instr(json, ?) > 0").all(fragment);
|
|
444
|
+
return rows.map((row) => row.thread_id);
|
|
445
|
+
}
|
|
418
446
|
/** Case-insensitive substring search over text messages, newest first.
|
|
419
447
|
* A LIKE scan, deliberately: local transcripts are megabytes at most, a
|
|
420
448
|
* scan is milliseconds, and it needs no FTS extension to exist. */
|
|
@@ -6,6 +6,11 @@ export function modelContextWindow(model) {
|
|
|
6
6
|
if (!model)
|
|
7
7
|
return undefined;
|
|
8
8
|
const id = model.toLowerCase();
|
|
9
|
+
// Opus 5.5 is 1M without a `[1m]` suffix. Only the exact slug, or that
|
|
10
|
+
// slug after a slash or a provider dot. A longer suffix such as
|
|
11
|
+
// `claude-opus-5-5-local` stays on the generic Claude rule, as does `host::`.
|
|
12
|
+
if (/(?:^|[/.])claude-opus-5(?:-5|\.5)$/.test(id))
|
|
13
|
+
return 1_000_000;
|
|
9
14
|
if (/claude/.test(id) || /^(opus|sonnet|haiku|fable)\b/.test(id))
|
|
10
15
|
return /\[1m\]|-1m\b|1m-context/.test(id) ? 1_000_000 : 200_000;
|
|
11
16
|
if (/^(gpt-5|o[34]\b|codex)/.test(id))
|
|
@@ -14,6 +19,8 @@ export function modelContextWindow(model) {
|
|
|
14
19
|
return 1_000_000;
|
|
15
20
|
if (/gemini/.test(id))
|
|
16
21
|
return 1_000_000;
|
|
22
|
+
if (id === "grok-4.7")
|
|
23
|
+
return 500_000;
|
|
17
24
|
if (id.startsWith("grok"))
|
|
18
25
|
return 256_000;
|
|
19
26
|
return undefined;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { billableFor } from "./prices.js";
|
|
2
|
+
const AS_OF = "2026-09-23";
|
|
3
|
+
const OPENAI = "https://developers.openai.com/api/docs/pricing";
|
|
4
|
+
const ANTHROPIC = "https://platform.claude.com/docs/en/about-claude/pricing";
|
|
5
|
+
const XAI = "https://docs.x.ai/developers/models";
|
|
6
|
+
const GOOGLE = "https://ai.google.dev/gemini-api/docs/pricing";
|
|
7
|
+
const MINIMAX = "https://platform.minimax.io/docs/guides/pricing-paygo";
|
|
8
|
+
// OpenRouter's FAQ (https://openrouter.ai/docs/faq) says it passes provider
|
|
9
|
+
// prices through without markup, but its live list does discount some
|
|
10
|
+
// models, so these are read from that list rather than copied from the
|
|
11
|
+
// vendors. Its 5.5% fee on buying credits is not a per-token price.
|
|
12
|
+
const OPENROUTER = "https://openrouter.ai/api/v1/models";
|
|
13
|
+
const price = (source, input, cached, output, extra = {}) => ({
|
|
14
|
+
inputPerMillion: input,
|
|
15
|
+
cachedInputPerMillion: cached,
|
|
16
|
+
outputPerMillion: output,
|
|
17
|
+
asOf: AS_OF,
|
|
18
|
+
source,
|
|
19
|
+
...extra,
|
|
20
|
+
});
|
|
21
|
+
/** Keyed by the model id as a bot's selection names it, lower-cased. */
|
|
22
|
+
export const LIST_PRICES = {
|
|
23
|
+
// OpenAI, Standard tier, short context (<272K input tokens).
|
|
24
|
+
"gpt-6-astra": price(OPENAI, 10, 1, 50),
|
|
25
|
+
// Labelled promotional pricing on the source page, with no end date given.
|
|
26
|
+
"gpt-5.6-sol": price(OPENAI, 4, 0.4, 20),
|
|
27
|
+
"gpt-5.6-terra": price(OPENAI, 2, 0.2, 12),
|
|
28
|
+
"gpt-5.6-luna": price(OPENAI, 0.2, 0.02, 1.2),
|
|
29
|
+
"gpt-5.5": price(OPENAI, 5, 0.5, 30),
|
|
30
|
+
"gpt-5.4": price(OPENAI, 2.5, 0.25, 15),
|
|
31
|
+
"gpt-5.4-mini": price(OPENAI, 0.75, 0.075, 4.5),
|
|
32
|
+
"gpt-5.3-codex": price(OPENAI, 1.75, 0.175, 14),
|
|
33
|
+
// gpt-5.3-codex-spark has no published API price: unpriced.
|
|
34
|
+
// Anthropic: base input, cache hit, output. No long-context tier.
|
|
35
|
+
"claude-fable-5-1": price(ANTHROPIC, 10, 0.25, 50),
|
|
36
|
+
"claude-fable-5": price(ANTHROPIC, 10, 1, 50),
|
|
37
|
+
"claude-opus-5": price(ANTHROPIC, 5, 0.5, 25),
|
|
38
|
+
"claude-sonnet-5": price(ANTHROPIC, 2, 0.2, 10),
|
|
39
|
+
"claude-haiku-4-5": price(ANTHROPIC, 1, 0.1, 5),
|
|
40
|
+
"claude-haiku-4-5-20251001": price(ANTHROPIC, 1, 0.1, 5),
|
|
41
|
+
// xAI, under 200k prompt tokens. grok-4, grok-4-fast and grok-3-mini are
|
|
42
|
+
// retired or unlisted: unpriced.
|
|
43
|
+
"grok-4.7": price(XAI, 2, 0.5, 6),
|
|
44
|
+
"grok-4.6": price(XAI, 2, 0.5, 6),
|
|
45
|
+
"grok-4.5": price(XAI, 2, 0.3, 6),
|
|
46
|
+
// Google Gemini API, paid tier, prompts up to 200k tokens.
|
|
47
|
+
"gemini-2.5-pro": price(GOOGLE, 1.25, 0.125, 10),
|
|
48
|
+
"gemini-2.5-flash": price(GOOGLE, 0.3, 0.03, 2.5),
|
|
49
|
+
"gemini-3.1-pro-preview": price(GOOGLE, 2, 0.2, 12),
|
|
50
|
+
"gemini-3.8-flash": price(GOOGLE, 0.75, 0.075, 3.75, {
|
|
51
|
+
changes: [{ from: "2027-01-01", inputPerMillion: 1.5, cachedInputPerMillion: 0.15, outputPerMillion: 7.5 }],
|
|
52
|
+
}),
|
|
53
|
+
// MiniMax, international pay-as-you-go, Standard. M3 up to 512k input
|
|
54
|
+
// tokens, at the price charged (the page shows it as 50% off, permanently).
|
|
55
|
+
"minimax-m3": price(MINIMAX, 0.3, 0.06, 1.2),
|
|
56
|
+
"minimax-m2.7": price(MINIMAX, 0.3, 0.06, 1.2),
|
|
57
|
+
"minimax-m2.7-highspeed": price(MINIMAX, 0.6, 0.06, 2.4),
|
|
58
|
+
// OpenRouter ids, as its live model list prices them.
|
|
59
|
+
"openai/gpt-6-astra": price(OPENROUTER, 10, 1, 50),
|
|
60
|
+
"openai/gpt-5.6-sol": price(OPENROUTER, 2, 0.2, 10),
|
|
61
|
+
"openai/gpt-5.6-terra": price(OPENROUTER, 2, 0.2, 12),
|
|
62
|
+
"openai/gpt-5.6-luna": price(OPENROUTER, 0.2, 0.02, 1.2),
|
|
63
|
+
"openai/gpt-5.5": price(OPENROUTER, 5, 0.5, 30),
|
|
64
|
+
"openai/gpt-5.4": price(OPENROUTER, 2.5, 0.25, 15),
|
|
65
|
+
"openai/gpt-5.4-mini": price(OPENROUTER, 0.75, 0.075, 4.5),
|
|
66
|
+
"openai/gpt-5.3-codex": price(OPENROUTER, 1.75, 0.175, 14),
|
|
67
|
+
"anthropic/claude-fable-5.1": price(OPENROUTER, 10, 0.25, 50),
|
|
68
|
+
"anthropic/claude-fable-5": price(OPENROUTER, 10, 1, 50),
|
|
69
|
+
"anthropic/claude-opus-5": price(OPENROUTER, 5, 0.5, 25),
|
|
70
|
+
"anthropic/claude-sonnet-5": price(OPENROUTER, 2, 0.2, 10),
|
|
71
|
+
"anthropic/claude-haiku-4.5": price(OPENROUTER, 1, 0.1, 5),
|
|
72
|
+
"x-ai/grok-4.7": price(OPENROUTER, 1.6, 0.4, 4.8),
|
|
73
|
+
"x-ai/grok-4.6": price(OPENROUTER, 2, 0.5, 6),
|
|
74
|
+
"x-ai/grok-4.5": price(OPENROUTER, 2, 0.3, 6),
|
|
75
|
+
"google/gemini-2.5-pro": price(OPENROUTER, 1.25, 0.125, 10),
|
|
76
|
+
"google/gemini-2.5-flash": price(OPENROUTER, 0.3, 0.03, 2.5),
|
|
77
|
+
"google/gemini-3.1-pro-preview": price(OPENROUTER, 2, 0.2, 12),
|
|
78
|
+
"google/gemini-3.8-flash": price(OPENROUTER, 0.75, 0.075, 3.75),
|
|
79
|
+
"minimax/minimax-m3": price(OPENROUTER, 0.3, 0.06, 1.2),
|
|
80
|
+
"minimax/minimax-m2.7": price(OPENROUTER, 0.3, 0.06, 1.2),
|
|
81
|
+
};
|
|
82
|
+
/** Catalog ids that name a listed model with a setting attached: the
|
|
83
|
+
* Antigravity picker offers Gemini 3.8 Flash at three thinking levels. */
|
|
84
|
+
const ALIASES = {
|
|
85
|
+
"gemini-3.8-flash-high": "gemini-3.8-flash",
|
|
86
|
+
"gemini-3.8-flash-medium": "gemini-3.8-flash",
|
|
87
|
+
"gemini-3.8-flash-low": "gemini-3.8-flash",
|
|
88
|
+
};
|
|
89
|
+
/** The list price for a model at a moment, or null when it is not listed. */
|
|
90
|
+
export function listPriceFor(model, at = new Date()) {
|
|
91
|
+
const key = model.trim().toLowerCase();
|
|
92
|
+
const entry = LIST_PRICES[ALIASES[key] ?? key];
|
|
93
|
+
if (!entry)
|
|
94
|
+
return null;
|
|
95
|
+
const day = at.toISOString().slice(0, 10);
|
|
96
|
+
const change = entry.changes?.filter((candidate) => candidate.from <= day).at(-1);
|
|
97
|
+
const current = change ?? entry;
|
|
98
|
+
return {
|
|
99
|
+
inputPerMillion: current.inputPerMillion,
|
|
100
|
+
outputPerMillion: current.outputPerMillion,
|
|
101
|
+
...(current.cachedInputPerMillion !== undefined ? { cachedInputPerMillion: current.cachedInputPerMillion } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** The price an estimate uses: the operator's own price for this exact
|
|
105
|
+
* model (Settings → Usage → prices, `driver/model` or model id), then the
|
|
106
|
+
* built-in list, then the operator's `default` row. */
|
|
107
|
+
export function estimatePriceFor(turn, operatorPrices, at = new Date()) {
|
|
108
|
+
return operatorPrices?.[`${turn.driverKind}/${turn.model}`] ?? operatorPrices?.[turn.model] ??
|
|
109
|
+
listPriceFor(turn.model, at) ?? operatorPrices?.default ?? null;
|
|
110
|
+
}
|
|
111
|
+
/** What the ledger books for a settled turn: the engine's own cost when it
|
|
112
|
+
* reported one, else an estimate, else null (unpriced). */
|
|
113
|
+
export function ledgerCost(turn, operatorPrices, at = new Date()) {
|
|
114
|
+
if (typeof turn.costUsd === "number" && Number.isFinite(turn.costUsd) && turn.costUsd >= 0) {
|
|
115
|
+
return { costUsd: turn.costUsd, costSource: "reported" };
|
|
116
|
+
}
|
|
117
|
+
const unit = estimatePriceFor(turn, operatorPrices, at);
|
|
118
|
+
if (!unit)
|
|
119
|
+
return { costUsd: null };
|
|
120
|
+
return { costUsd: billableFor(turn, { [turn.model]: unit }), costSource: "estimated" };
|
|
121
|
+
}
|
|
@@ -33,6 +33,13 @@ export function blockedTarget(bot, room) {
|
|
|
33
33
|
return { threadId: bot.threadId };
|
|
34
34
|
return { threadId: room.threadId, group: { id: room.id, name: room.name } };
|
|
35
35
|
}
|
|
36
|
+
/** A workspace spend notice (server/spend.ts decides when). It opens the
|
|
37
|
+
* thread whose turn crossed the line, so clicking it lands somewhere real,
|
|
38
|
+
* but it is the workspace's news, not that bot's: the bot's notification
|
|
39
|
+
* toggle does not silence it. */
|
|
40
|
+
export function buildSpendNotification(bot, threadId, text) {
|
|
41
|
+
return { kind: "spend", botId: bot.id, botName: bot.name, threadId, title: text.title, body: summarize(text.body, 200) };
|
|
42
|
+
}
|
|
36
43
|
/** Build the frame for one event, or null when it should stay quiet.
|
|
37
44
|
*
|
|
38
45
|
* Kept pure and separate from the event fold so the policy — which is the
|
|
@@ -62,7 +69,9 @@ export function buildNotification(kind, bot, threadId, detail, extra) {
|
|
|
62
69
|
? `${who} couldn't start`
|
|
63
70
|
: kind === "incident"
|
|
64
71
|
? `${who} hit a problem`
|
|
65
|
-
:
|
|
72
|
+
: kind === "delegation-settled"
|
|
73
|
+
? `${who} resumed with results`
|
|
74
|
+
: `${who} finished`;
|
|
66
75
|
// A "finished" with nothing to say is not worth a notification — the
|
|
67
76
|
// badge in the sidebar already carries that much.
|
|
68
77
|
if (kind === "done" && !body)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// every other bot now gets are rendered from here, so there is one set of
|
|
4
4
|
// caps, one sanitizer, and one reachability rule to audit rather than two
|
|
5
5
|
// that drift.
|
|
6
|
+
import { sameAudience } from "./bot-visibility.js";
|
|
6
7
|
const sectionKey = (section) => section?.trim() || "";
|
|
7
8
|
export const PEER_ACCESS_HELP = "Call list_bots for reachable teammates. If the intended Chief is missing, ask the user to check team membership and this bot's allowed peers, or message the Chief directly. A Chief's access to another team does not grant that team's bots access back to the Chief. Do not use computer control to bypass this.";
|
|
8
9
|
/** Coordination is scoped to the bot's own team unless the owner explicitly
|
|
@@ -51,9 +52,19 @@ export function peerStatusWords(status) {
|
|
|
51
52
|
* written by an older build) falls back to the unset rule rather than
|
|
52
53
|
* throwing mid-turn: the list is operator-owned local state, so degrading to
|
|
53
54
|
* the documented default is safer than failing a turn. */
|
|
54
|
-
export const peerAllowed = (from,
|
|
55
|
+
export const peerAllowed = (from, target) => {
|
|
56
|
+
const targetId = typeof target === "string" ? target : target.id;
|
|
57
|
+
if (Array.isArray(from.peers) && !from.peers.includes(targetId))
|
|
58
|
+
return false;
|
|
59
|
+
// Given the record, also require the same audience: a teammate that other
|
|
60
|
+
// people can see would carry this bot's words, or bring back a restricted
|
|
61
|
+
// bot's answers, to people who cannot see the other (bot-visibility.ts).
|
|
62
|
+
// Bots nobody restricted all share "everyone", so this changes nothing
|
|
63
|
+
// until an admin restricts one.
|
|
64
|
+
return typeof target === "string" || sameAudience(from.visibility, target.visibility);
|
|
65
|
+
};
|
|
55
66
|
export function canReachPeer(from, target) {
|
|
56
|
-
return from.id !== target.id && !target.hidden && canAccessTeam(from, target.section) && peerAllowed(from, target
|
|
67
|
+
return from.id !== target.id && !target.hidden && canAccessTeam(from, target.section) && peerAllowed(from, target);
|
|
57
68
|
}
|
|
58
69
|
/** The peers a bot can both see and reach right now. The roster, list_bots
|
|
59
70
|
* and @mention resolution all read this one list, so what a bot is TOLD
|
|
@@ -143,3 +143,18 @@ export function parseSince(value, now = Date.now()) {
|
|
|
143
143
|
const parsed = Date.parse(/^\d{4}-\d{2}-\d{2}$/.test(text) ? `${text}T00:00` : value.trim());
|
|
144
144
|
return Number.isFinite(parsed) ? parsed : null;
|
|
145
145
|
}
|
|
146
|
+
/** The forms that name a whole day rather than one instant. */
|
|
147
|
+
const WHOLE_DAY = /^(?:today|yesterday|\d{4}-\d{2}-\d{2})$/;
|
|
148
|
+
/** The closing end of a window a person types. `since` takes a day's first
|
|
149
|
+
* instant, so `until` has to take its last: read the same way, `since` and
|
|
150
|
+
* `until` on one day span no time at all and a full day of work reads back as
|
|
151
|
+
* nothing. Only the forms that name a day are stretched — a span ("24h"), an
|
|
152
|
+
* epoch, or a date with a clock time still means the instant it names. */
|
|
153
|
+
export function parseUntil(value, now = Date.now()) {
|
|
154
|
+
const at = parseSince(value, now);
|
|
155
|
+
if (at === null || !WHOLE_DAY.test(value.trim().toLowerCase()))
|
|
156
|
+
return at;
|
|
157
|
+
const end = new Date(at);
|
|
158
|
+
end.setHours(23, 59, 59, 999);
|
|
159
|
+
return end.getTime();
|
|
160
|
+
}
|