openwork-server 0.18.60 → 0.18.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/bin/openwork-server.mjs +1 -1
- package/bin/platform.mjs +7 -7
- package/dist/opencode-plugin-deps-1.18.30.tgz +0 -0
- package/dist/opencode-plugins/openwork-cloud-skills-v2.js +87 -41
- package/dist/opencode-plugins/openwork-context-v2.js +67 -35
- package/dist/opencode-plugins/openwork-gateway-quota-v2.js +187 -0
- package/dist/opencode-plugins/openwork-gateway-quota.js +232 -23
- package/dist/opencode-plugins/openwork-provider-sync-v2.js +85 -2
- package/dist/openwork-server.mjs +919 -333
- package/package.json +2 -2
- package/web/assets/{abnfDiagram-VCTEODGH-D0_4pXEx.js → abnfDiagram-VCTEODGH-DGCabXot.js} +1 -1
- package/web/assets/app-CkmqBm_1.css +32 -0
- package/web/assets/{app-BCXQnJUH.js → app-Cy3sY2K8.js} +435 -425
- package/web/assets/{arc-BSTwaImw.js → arc-OJTfUP9L.js} +1 -1
- package/web/assets/{architectureDiagram-5GKGNRK7-cXwF0iP0.js → architectureDiagram-5GKGNRK7-Ch0Zt81S.js} +1 -1
- package/web/assets/{artifact-code-view-CUXv3CYK.js → artifact-code-view-CSo8dBet.js} +1 -1
- package/web/assets/{artifact-spreadsheet-editor-CxhNYmjP.js → artifact-spreadsheet-editor-DG2AL4aO.js} +1 -1
- package/web/assets/{artifact-text-editor-k2TGQI-i.js → artifact-text-editor-BKs1cI6V.js} +1 -1
- package/web/assets/{blockDiagram-OSKFZWR5-Cl9p_z8u.js → blockDiagram-OSKFZWR5-Dur4PitI.js} +1 -1
- package/web/assets/{c4Diagram-7LVT6UL2-YQjnoUQ0.js → c4Diagram-7LVT6UL2-DN-qrh5o.js} +1 -1
- package/web/assets/channel-68fpiRG4.js +1 -0
- package/web/assets/{chunk-2Q5K7J3B-YZaSwlzj.js → chunk-2Q5K7J3B-BSmk0dam.js} +1 -1
- package/web/assets/{chunk-5VM5RSS4-BICC_wIw.js → chunk-5VM5RSS4-BwAmjLhK.js} +1 -1
- package/web/assets/{chunk-F27PBJKO-CBxmcmxs.js → chunk-F27PBJKO-DLKbY_DS.js} +1 -1
- package/web/assets/{chunk-HLEWEB6X-2h2z7zt8.js → chunk-HLEWEB6X-s4I3BuHN.js} +1 -1
- package/web/assets/{chunk-JWPE2WC7-BnAMOpXl.js → chunk-JWPE2WC7-LslAnaQK.js} +1 -1
- package/web/assets/{chunk-POPQ4Y6H-pvDYQBBM.js → chunk-POPQ4Y6H-e0_lKBGk.js} +1 -1
- package/web/assets/{chunk-SVP7TREG-QuKLl3v0.js → chunk-SVP7TREG-CBKOSsQl.js} +1 -1
- package/web/assets/{chunk-XXDRQBXY-4Bt9wCBO.js → chunk-XXDRQBXY-Bwqkh7ZD.js} +1 -1
- package/web/assets/{chunk-ZLD2IHE6-C_RSrkqU.js → chunk-ZLD2IHE6-anLhhsDd.js} +1 -1
- package/web/assets/classDiagram-CYGNFDIV-Z4CG2h2d.js +1 -0
- package/web/assets/classDiagram-v2-TLXNO2FR-Z4CG2h2d.js +1 -0
- package/web/assets/{cose-bilkent-JH36ORCC-CciOYQb4.js → cose-bilkent-JH36ORCC-CCj043X5.js} +1 -1
- package/web/assets/{cynefin-OW5HDTMX-k8-mUlq4.js → cynefin-OW5HDTMX-BGWD-i0N.js} +1 -1
- package/web/assets/{cynefinDiagram-5FMLGOSQ-D1qAHzq3.js → cynefinDiagram-5FMLGOSQ-ZWq_C5SK.js} +1 -1
- package/web/assets/{dagre-OS7QT2EB-BP1VuF_z.js → dagre-OS7QT2EB-In3Mah_T.js} +1 -1
- package/web/assets/{diagram-S7CK7UJ4-crXkWleC.js → diagram-S7CK7UJ4-DaTd6oXD.js} +1 -1
- package/web/assets/{diagram-UQ7AKVKN-CtHAmCY-.js → diagram-UQ7AKVKN-D3jwCd_v.js} +1 -1
- package/web/assets/{diagram-VSXAHHWV-C-Fhuwy8.js → diagram-VSXAHHWV-CsK-_hfn.js} +1 -1
- package/web/assets/{diagram-VX7I27RA-DkRkNPuj.js → diagram-VX7I27RA-BBLQ2mL7.js} +1 -1
- package/web/assets/{diagram-Z3DM3KII-DVERPCFS.js → diagram-Z3DM3KII-CUM2PN0X.js} +1 -1
- package/web/assets/{ebnfDiagram-PWID7BFC-PrSifYGF.js → ebnfDiagram-PWID7BFC-BjJTJ9Vr.js} +1 -1
- package/web/assets/{erDiagram-2YWLMYGG-D8-wfUYq.js → erDiagram-2YWLMYGG-BhjMQMDs.js} +1 -1
- package/web/assets/{flowDiagram-T62WH6J4-BS-_wkGe.js → flowDiagram-T62WH6J4-KrGbrW-t.js} +1 -1
- package/web/assets/{ganttDiagram-EL5Y4UJY-D9PWmuWE.js → ganttDiagram-EL5Y4UJY-Co_OE6ls.js} +1 -1
- package/web/assets/{gitGraphDiagram-WWUBYQGX-DIP9p63c.js → gitGraphDiagram-WWUBYQGX-BxM4C4Ti.js} +1 -1
- package/web/assets/{infoDiagram-FKFFQAWI-BzU6-acC.js → infoDiagram-FKFFQAWI-C8YaW3a6.js} +1 -1
- package/web/assets/{ishikawaDiagram-5VMMS53U-CvYS2ayc.js → ishikawaDiagram-5VMMS53U-MPTb2C9Y.js} +1 -1
- package/web/assets/{journeyDiagram-EYS64GPL-Cr0qdK-D.js → journeyDiagram-EYS64GPL-Dzq-vllP.js} +1 -1
- package/web/assets/{kanban-definition-UXKFOSKX-DhffGp7U.js → kanban-definition-UXKFOSKX-DWhE5bnH.js} +1 -1
- package/web/assets/{layout-b5jqOIam.js → layout-CMyIV62W.js} +1 -1
- package/web/assets/{linear-zLNMwGhI.js → linear-CX-tWibZ.js} +1 -1
- package/web/assets/{mermaid.core-DB_TNiSG.js → mermaid.core-DcgS6fFC.js} +6 -6
- package/web/assets/{mindmap-definition-THT77NOG-DkQz2hbh.js → mindmap-definition-THT77NOG-CJzNRYpk.js} +1 -1
- package/web/assets/{pegDiagram-XKGWAZYB-CB313CDF.js → pegDiagram-XKGWAZYB-BSIi724T.js} +1 -1
- package/web/assets/{pieDiagram-E7YTZNPT-yaDJVY4d.js → pieDiagram-E7YTZNPT-CoArMhZD.js} +1 -1
- package/web/assets/{quadrantDiagram-AXDQQJYC-BkV8iwHS.js → quadrantDiagram-AXDQQJYC-B9HoxOyK.js} +1 -1
- package/web/assets/{railroadDiagram-O6MQD6OU-T_JVDRpV.js → railroadDiagram-O6MQD6OU-C-aYV4dU.js} +1 -1
- package/web/assets/{requirementDiagram-IS5BZ75X-B68yTCqr.js → requirementDiagram-IS5BZ75X-DHPnVlti.js} +1 -1
- package/web/assets/{sankeyDiagram-P5KCCOFB-Bx31O26O.js → sankeyDiagram-P5KCCOFB-DlxAwgv1.js} +1 -1
- package/web/assets/{sequenceDiagram-WJ2MYXX4-CsCoFTAg.js → sequenceDiagram-WJ2MYXX4-DzVPZb2m.js} +1 -1
- package/web/assets/{sizeCapture-INFHLROL-B3a49hkJ.js → sizeCapture-INFHLROL-BkwjaMAx.js} +1 -1
- package/web/assets/{stateDiagram-XQSTLZYL-DjCdHmx5.js → stateDiagram-XQSTLZYL-DIeNtdWj.js} +1 -1
- package/web/assets/stateDiagram-v2-IH3M54BS-CZR_2wND.js +1 -0
- package/web/assets/{swimlanes-V6O3JKXN-4u8S1I3e.js → swimlanes-V6O3JKXN-CPUSs3Tj.js} +1 -1
- package/web/assets/swimlanesDiagram-JKAHXJPX-C1Sms2Ey.js +8 -0
- package/web/assets/{timeline-definition-24CTP7MA-r3GnOjJo.js → timeline-definition-24CTP7MA-CjBwDFJH.js} +1 -1
- package/web/assets/{vennDiagram-4TSXK5OY-59IlTU54.js → vennDiagram-4TSXK5OY-DOsoEsMT.js} +1 -1
- package/web/assets/{wardleyDiagram-VM6X3IG4-C3ALfLHE.js → wardleyDiagram-VM6X3IG4-_fhLZdvF.js} +1 -1
- package/web/assets/{workspace-file-tree-C6GhNx3L.js → workspace-file-tree-CTzNl5A5.js} +1 -1
- package/web/assets/{xychartDiagram-S5SC5T6Z-CoYtWlkb.js → xychartDiagram-S5SC5T6Z-D4Kz81TF.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/app-DVblmMbF.css +0 -32
- package/web/assets/channel-DwV664-P.js +0 -1
- package/web/assets/classDiagram-CYGNFDIV-BKhd9faI.js +0 -1
- package/web/assets/classDiagram-v2-TLXNO2FR-BKhd9faI.js +0 -1
- package/web/assets/stateDiagram-v2-IH3M54BS-Cf0tqY7x.js +0 -1
- package/web/assets/swimlanesDiagram-JKAHXJPX-COK5qx0u.js +0 -8
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ npm install -g openwork-server
|
|
|
9
9
|
openwork-server --workspace /path/to/workspace --approval auto
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
The npm package
|
|
12
|
+
The npm package requires stable Node.js 24.x, version 24.22.1 or newer within that major, on macOS, Linux, and Windows (arm64 or x64); Bun is not required at runtime. Older Node releases and other majors (including 25+) are rejected by the runtime gate. This security baseline also replaces the previous Node.js 22.13 minimum.
|
|
13
13
|
|
|
14
14
|
### Self-hosted web UI
|
|
15
15
|
|
|
@@ -34,8 +34,14 @@ persisted in `~/.openwork/openwork-server/web-tokens.json` so they survive
|
|
|
34
34
|
restarts; `--token`/`--host-token` or `OPENWORK_TOKEN`/`OPENWORK_HOST_TOKEN`
|
|
35
35
|
override them.
|
|
36
36
|
|
|
37
|
+
In `web` mode the UI's pages never carry CORS headers, and the server only
|
|
38
|
+
answers requests addressed to `localhost`, an IP address or the `--host` name.
|
|
39
|
+
Add other names with `--allowed-hosts` (or `OPENWORK_ALLOWED_HOSTS`,
|
|
40
|
+
comma-separated; `*` allows any).
|
|
41
|
+
|
|
37
42
|
To reach it from other machines, keep the server on `127.0.0.1` and put a TLS
|
|
38
|
-
proxy in front (e.g. `tailscale serve --bg --https=8787 http://127.0.0.1:8787`)
|
|
43
|
+
proxy in front (e.g. `tailscale serve --bg --https=8787 http://127.0.0.1:8787`),
|
|
44
|
+
then allow the proxy's name (e.g. `--allowed-hosts my-machine.tailnet.ts.net`).
|
|
39
45
|
Browsers require a secure context for the UI on any non-localhost origin.
|
|
40
46
|
Set `OPENWORK_OPENCODE_BIN` to use your own OpenCode install instead of the
|
|
41
47
|
managed download; `/health` reports both the pinned `opencodeVersion` and the
|
|
@@ -175,6 +181,7 @@ Defaults to `~/.config/openwork/server.json` (override with `OPENWORK_SERVER_CON
|
|
|
175
181
|
- `OPENWORK_APPROVAL_TIMEOUT_MS`
|
|
176
182
|
- `OPENWORK_WORKSPACES` (JSON array or comma-separated list of paths)
|
|
177
183
|
- `OPENWORK_CORS_ORIGINS` (comma-separated list or `*`)
|
|
184
|
+
- `OPENWORK_ALLOWED_HOSTS` (comma-separated host names or `*`; `web` mode always checks the Host)
|
|
178
185
|
- `OPENWORK_OPENCODE_BASE_URL`
|
|
179
186
|
- `OPENWORK_OPENCODE_DIRECTORY`
|
|
180
187
|
- `OPENWORK_OPENCODE_USERNAME`
|
package/bin/openwork-server.mjs
CHANGED
|
@@ -29,7 +29,7 @@ const nodeBundle = fromPackage("./dist/openwork-server.mjs");
|
|
|
29
29
|
if (existsSync(nodeBundle)) {
|
|
30
30
|
if (!nodeVersionSupported(process.versions.node)) {
|
|
31
31
|
console.error(
|
|
32
|
-
`openwork-server needs Node.js ${MIN_NODE_VERSION} or newer (found ${process.versions.node}). Install a
|
|
32
|
+
`openwork-server needs Node.js 24.x, version ${MIN_NODE_VERSION} or newer within that major (found ${process.versions.node}). Install a supported Node.js 24 release and try again.`,
|
|
33
33
|
);
|
|
34
34
|
process.exit(1);
|
|
35
35
|
}
|
package/bin/platform.mjs
CHANGED
|
@@ -16,13 +16,13 @@ export function serverBinaryName(platform, arch) {
|
|
|
16
16
|
return `openwork-server-${target.target}${platform === "win32" ? ".exe" : ""}`;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
export const MIN_NODE_VERSION = "22.
|
|
19
|
+
// Security baseline: only patched releases on the supported Node 24 line.
|
|
20
|
+
// New majors must be reviewed explicitly, not accepted by a lower-bound check.
|
|
21
|
+
export const MIN_NODE_VERSION = "24.22.1";
|
|
22
22
|
|
|
23
23
|
export function nodeVersionSupported(version) {
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
return major >
|
|
24
|
+
const match = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version);
|
|
25
|
+
if (!match) return false;
|
|
26
|
+
const [, major, minor, patch] = match.map(Number);
|
|
27
|
+
return major === 24 && (minor > 22 || (minor === 22 && patch >= 1));
|
|
28
28
|
}
|
|
Binary file
|
|
@@ -19,6 +19,9 @@ const MAX_SKILLS = 150;
|
|
|
19
19
|
const NATIVE_BUDGET_CHARS = 64_000;
|
|
20
20
|
const NATIVE_DESCRIPTION_CHARS = 1_024;
|
|
21
21
|
const MAX_MENTIONED_SKILLS = 3;
|
|
22
|
+
// Bodies kept for the skills messages used most recently, so using one again
|
|
23
|
+
// needs no read before the model runs. Every other skill loads on use.
|
|
24
|
+
const MAX_WARM_SKILLS = 8;
|
|
22
25
|
const skillId = (skill) => `${CLOUD_SKILL_ID_PREFIX}${skill.name}`;
|
|
23
26
|
// Not SKILL.md: the native skill tool must not scan a fake directory. The engine
|
|
24
27
|
// requires path and disables this whole plugin, OpenWork's context tools
|
|
@@ -60,10 +63,10 @@ function budgeted(skills, budget, descriptionChars, content) {
|
|
|
60
63
|
return result;
|
|
61
64
|
}
|
|
62
65
|
/**
|
|
63
|
-
* Each organization skill is a native skill
|
|
64
|
-
*
|
|
65
|
-
* digest Den published
|
|
66
|
-
*
|
|
66
|
+
* Each organization skill is a native skill with its full description. A
|
|
67
|
+
* skill whose authorized SKILL.md has been read for a message, verified against
|
|
68
|
+
* the digest Den published, carries it; every other skill keeps the discovery
|
|
69
|
+
* note, so it still works through get_skill.
|
|
67
70
|
*/
|
|
68
71
|
export function nativeSkills(catalog, contents) {
|
|
69
72
|
return budgeted(catalog.skills, NATIVE_BUDGET_CHARS, NATIVE_DESCRIPTION_CHARS, skill => {
|
|
@@ -113,15 +116,19 @@ export function mentionedSkillIds(text, skills) {
|
|
|
113
116
|
}
|
|
114
117
|
/** Setup starts background discovery without making Cloud a dependency of
|
|
115
118
|
* chatting or local skill discovery. Only authorized catalog entries and
|
|
116
|
-
* digest-verified bodies are added to the native registry.
|
|
119
|
+
* digest-verified bodies are added to the native registry. Discovery reads
|
|
120
|
+
* metadata only; a body is read when a message names or attaches its skill. */
|
|
117
121
|
export async function registerCloudSkillDiscovery(context) {
|
|
118
122
|
let skills = [];
|
|
123
|
+
let catalog = { skills: [] };
|
|
119
124
|
let fingerprint = "[]";
|
|
120
125
|
let closed = false;
|
|
121
126
|
let refreshing = false;
|
|
122
|
-
// Verified SKILL.md bodies by digest, in memory only
|
|
123
|
-
// when Den publishes a new digest
|
|
127
|
+
// Verified SKILL.md bodies by digest, in memory only, for the warm skills.
|
|
128
|
+
// A body is re-read only when Den publishes a new digest for one of them.
|
|
124
129
|
const contents = new Map();
|
|
130
|
+
// Skill IDs messages used, most recent last.
|
|
131
|
+
const warm = [];
|
|
125
132
|
const controller = new AbortController();
|
|
126
133
|
let registration;
|
|
127
134
|
let promptHook;
|
|
@@ -134,44 +141,34 @@ export async function registerCloudSkillDiscovery(context) {
|
|
|
134
141
|
});
|
|
135
142
|
return response.ok ? await response.json() : null;
|
|
136
143
|
};
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
/** Read listed skills' bodies in one request and keep those matching Den's digest. */
|
|
145
|
+
const readBodies = async (entries, use) => {
|
|
146
|
+
const loadable = entries.filter(skill => skill.url && skill.digest && !skill.setup);
|
|
147
|
+
if (loadable.length === 0)
|
|
148
|
+
return;
|
|
149
|
+
const result = z.object({ contents: z.record(z.string(), z.string()) })
|
|
150
|
+
.safeParse(await call("openwork_skill_read", { urls: loadable.map(skill => skill.url), ...(use ? { use } : {}) }).catch(() => null));
|
|
151
|
+
if (!result.success)
|
|
152
|
+
return;
|
|
153
|
+
for (const skill of loadable) {
|
|
154
|
+
const body = result.data.contents[skill.url];
|
|
146
155
|
// Only a body that matches Den's published digest is used.
|
|
147
|
-
if (
|
|
148
|
-
contents.set(skill.digest,
|
|
156
|
+
if (body !== undefined && sha256Digest(body) === skill.digest)
|
|
157
|
+
contents.set(skill.digest, body);
|
|
149
158
|
}
|
|
150
159
|
};
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
promptHook = await context.session.hook("prompt", event => {
|
|
155
|
-
if (skills.length === 0)
|
|
156
|
-
return;
|
|
157
|
-
const attached = new Set((event.prompt.skills ?? []).map(skill => skill.id));
|
|
158
|
-
const mentioned = mentionedSkillIds(event.prompt.text, skills).filter(id => !attached.has(id));
|
|
159
|
-
if (mentioned.length === 0)
|
|
160
|
-
return;
|
|
161
|
-
event.prompt.skills = [...(event.prompt.skills ?? []), ...mentioned.map(id => ({ id }))];
|
|
162
|
-
});
|
|
160
|
+
const warmEntries = () => {
|
|
161
|
+
const listed = new Set(nativeSkills(catalog, contents).map(skill => skill.id));
|
|
162
|
+
return catalog.skills.filter(skill => warm.includes(skillId(skill)) && listed.has(skillId(skill)));
|
|
163
163
|
};
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const catalog = catalogSchema.parse(value);
|
|
173
|
-
await loadContents(catalog);
|
|
174
|
-
await ensurePromptHook();
|
|
164
|
+
// Registry updates run one at a time, from refreshes and prompts alike.
|
|
165
|
+
let publishing = Promise.resolve();
|
|
166
|
+
const publish = () => {
|
|
167
|
+
publishing = publishing.catch(() => { }).then(async () => {
|
|
168
|
+
const wanted = new Set(warmEntries().flatMap(skill => skill.digest && !skill.setup ? [skill.digest] : []));
|
|
169
|
+
for (const digest of contents.keys())
|
|
170
|
+
if (!wanted.has(digest))
|
|
171
|
+
contents.delete(digest);
|
|
175
172
|
const next = nativeSkills(catalog, contents);
|
|
176
173
|
const key = JSON.stringify(next);
|
|
177
174
|
if (closed || key === fingerprint)
|
|
@@ -198,6 +195,54 @@ export async function registerCloudSkillDiscovery(context) {
|
|
|
198
195
|
await context.skill.reload();
|
|
199
196
|
}
|
|
200
197
|
fingerprint = key;
|
|
198
|
+
});
|
|
199
|
+
return publishing;
|
|
200
|
+
};
|
|
201
|
+
const ensurePromptHook = async () => {
|
|
202
|
+
if (promptHook || !context.session)
|
|
203
|
+
return;
|
|
204
|
+
promptHook = await context.session.hook("prompt", async (event) => {
|
|
205
|
+
if (skills.length === 0)
|
|
206
|
+
return;
|
|
207
|
+
const attached = new Set((event.prompt.skills ?? []).map(skill => skill.id));
|
|
208
|
+
const mentioned = mentionedSkillIds(event.prompt.text, skills).filter(id => !attached.has(id));
|
|
209
|
+
if (mentioned.length)
|
|
210
|
+
event.prompt.skills = [...(event.prompt.skills ?? []), ...mentioned.map(id => ({ id }))];
|
|
211
|
+
// Only registered skills: an unlisted ID never costs a read.
|
|
212
|
+
const registered = new Set(skills.map(skill => skill.id));
|
|
213
|
+
const used = new Set([...attached, ...mentioned].filter(id => registered.has(id)));
|
|
214
|
+
const entries = catalog.skills.filter(skill => used.has(skillId(skill)));
|
|
215
|
+
if (entries.length === 0)
|
|
216
|
+
return;
|
|
217
|
+
try {
|
|
218
|
+
// Freshly authorized for this message, and counted by Den as a use.
|
|
219
|
+
// A body that cannot be read leaves the discovery note: get_skill still works.
|
|
220
|
+
await readBodies(entries, true);
|
|
221
|
+
for (const skill of entries) {
|
|
222
|
+
const id = skillId(skill);
|
|
223
|
+
warm.splice(0, warm.length, ...warm.filter(entry => entry !== id), id);
|
|
224
|
+
}
|
|
225
|
+
warm.splice(0, Math.max(0, warm.length - MAX_WARM_SKILLS));
|
|
226
|
+
await publish();
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// A message is never held back by Cloud skill loading.
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
const refresh = async () => {
|
|
234
|
+
if (closed || refreshing)
|
|
235
|
+
return;
|
|
236
|
+
refreshing = true;
|
|
237
|
+
try {
|
|
238
|
+
const value = await call("openwork_skills", {});
|
|
239
|
+
if (value === null)
|
|
240
|
+
return;
|
|
241
|
+
catalog = catalogSchema.parse(value);
|
|
242
|
+
// Only warm skills whose digest changed are read again.
|
|
243
|
+
await readBodies(warmEntries().filter(skill => skill.digest && !contents.has(skill.digest)), false);
|
|
244
|
+
await ensurePromptHook();
|
|
245
|
+
await publish();
|
|
201
246
|
}
|
|
202
247
|
catch {
|
|
203
248
|
// Cloud readiness is never a prerequisite to chatting. Discovery entries
|
|
@@ -216,6 +261,7 @@ export async function registerCloudSkillDiscovery(context) {
|
|
|
216
261
|
controller.abort();
|
|
217
262
|
skills = [];
|
|
218
263
|
contents.clear();
|
|
264
|
+
await publishing.catch(() => { });
|
|
219
265
|
await promptHook?.dispose();
|
|
220
266
|
await registration?.dispose();
|
|
221
267
|
};
|
|
@@ -13813,6 +13813,7 @@ var MAX_SKILLS = 150;
|
|
|
13813
13813
|
var NATIVE_BUDGET_CHARS = 64000;
|
|
13814
13814
|
var NATIVE_DESCRIPTION_CHARS = 1024;
|
|
13815
13815
|
var MAX_MENTIONED_SKILLS = 3;
|
|
13816
|
+
var MAX_WARM_SKILLS = 8;
|
|
13816
13817
|
var skillId = (skill) => `${CLOUD_SKILL_ID_PREFIX}${skill.name}`;
|
|
13817
13818
|
var skillPath = (skill) => `/openwork/remote-skills/${skill.name}.md`;
|
|
13818
13819
|
function discoveryContent(skill) {
|
|
@@ -13890,10 +13891,12 @@ function mentionedSkillIds(text2, skills) {
|
|
|
13890
13891
|
}
|
|
13891
13892
|
async function registerCloudSkillDiscovery(context) {
|
|
13892
13893
|
let skills = [];
|
|
13894
|
+
let catalog = { skills: [] };
|
|
13893
13895
|
let fingerprint = "[]";
|
|
13894
13896
|
let closed = false;
|
|
13895
13897
|
let refreshing = false;
|
|
13896
13898
|
const contents = new Map;
|
|
13899
|
+
const warm = [];
|
|
13897
13900
|
const controller = new AbortController;
|
|
13898
13901
|
let registration;
|
|
13899
13902
|
let promptHook;
|
|
@@ -13907,43 +13910,30 @@ async function registerCloudSkillDiscovery(context) {
|
|
|
13907
13910
|
});
|
|
13908
13911
|
return response.ok ? await response.json() : null;
|
|
13909
13912
|
};
|
|
13910
|
-
const
|
|
13911
|
-
const
|
|
13912
|
-
|
|
13913
|
-
if (!wanted.has(digest))
|
|
13914
|
-
contents.delete(digest);
|
|
13915
|
-
for (const skill of catalog.skills) {
|
|
13916
|
-
if (closed || !skill.digest || !skill.url || skill.setup || contents.has(skill.digest))
|
|
13917
|
-
continue;
|
|
13918
|
-
const result = exports_external.object({ content: exports_external.string() }).safeParse(await call("openwork_skill_read", { url: skill.url }).catch(() => null));
|
|
13919
|
-
if (result.success && sha256Digest(result.data.content) === skill.digest)
|
|
13920
|
-
contents.set(skill.digest, result.data.content);
|
|
13921
|
-
}
|
|
13922
|
-
};
|
|
13923
|
-
const ensurePromptHook = async () => {
|
|
13924
|
-
if (promptHook || !context.session)
|
|
13913
|
+
const readBodies = async (entries, use) => {
|
|
13914
|
+
const loadable = entries.filter((skill) => skill.url && skill.digest && !skill.setup);
|
|
13915
|
+
if (loadable.length === 0)
|
|
13925
13916
|
return;
|
|
13926
|
-
|
|
13927
|
-
|
|
13928
|
-
return;
|
|
13929
|
-
const attached = new Set((event.prompt.skills ?? []).map((skill) => skill.id));
|
|
13930
|
-
const mentioned = mentionedSkillIds(event.prompt.text, skills).filter((id) => !attached.has(id));
|
|
13931
|
-
if (mentioned.length === 0)
|
|
13932
|
-
return;
|
|
13933
|
-
event.prompt.skills = [...event.prompt.skills ?? [], ...mentioned.map((id) => ({ id }))];
|
|
13934
|
-
});
|
|
13935
|
-
};
|
|
13936
|
-
const refresh = async () => {
|
|
13937
|
-
if (closed || refreshing)
|
|
13917
|
+
const result = exports_external.object({ contents: exports_external.record(exports_external.string(), exports_external.string()) }).safeParse(await call("openwork_skill_read", { urls: loadable.map((skill) => skill.url), ...use ? { use } : {} }).catch(() => null));
|
|
13918
|
+
if (!result.success)
|
|
13938
13919
|
return;
|
|
13939
|
-
|
|
13940
|
-
|
|
13941
|
-
|
|
13942
|
-
|
|
13943
|
-
|
|
13944
|
-
|
|
13945
|
-
|
|
13946
|
-
|
|
13920
|
+
for (const skill of loadable) {
|
|
13921
|
+
const body = result.data.contents[skill.url];
|
|
13922
|
+
if (body !== undefined && sha256Digest(body) === skill.digest)
|
|
13923
|
+
contents.set(skill.digest, body);
|
|
13924
|
+
}
|
|
13925
|
+
};
|
|
13926
|
+
const warmEntries = () => {
|
|
13927
|
+
const listed = new Set(nativeSkills(catalog, contents).map((skill) => skill.id));
|
|
13928
|
+
return catalog.skills.filter((skill) => warm.includes(skillId(skill)) && listed.has(skillId(skill)));
|
|
13929
|
+
};
|
|
13930
|
+
let publishing = Promise.resolve();
|
|
13931
|
+
const publish = () => {
|
|
13932
|
+
publishing = publishing.catch(() => {}).then(async () => {
|
|
13933
|
+
const wanted = new Set(warmEntries().flatMap((skill) => skill.digest && !skill.setup ? [skill.digest] : []));
|
|
13934
|
+
for (const digest of contents.keys())
|
|
13935
|
+
if (!wanted.has(digest))
|
|
13936
|
+
contents.delete(digest);
|
|
13947
13937
|
const next = nativeSkills(catalog, contents);
|
|
13948
13938
|
const key = JSON.stringify(next);
|
|
13949
13939
|
if (closed || key === fingerprint)
|
|
@@ -13968,6 +13958,47 @@ async function registerCloudSkillDiscovery(context) {
|
|
|
13968
13958
|
await context.skill.reload();
|
|
13969
13959
|
}
|
|
13970
13960
|
fingerprint = key;
|
|
13961
|
+
});
|
|
13962
|
+
return publishing;
|
|
13963
|
+
};
|
|
13964
|
+
const ensurePromptHook = async () => {
|
|
13965
|
+
if (promptHook || !context.session)
|
|
13966
|
+
return;
|
|
13967
|
+
promptHook = await context.session.hook("prompt", async (event) => {
|
|
13968
|
+
if (skills.length === 0)
|
|
13969
|
+
return;
|
|
13970
|
+
const attached = new Set((event.prompt.skills ?? []).map((skill) => skill.id));
|
|
13971
|
+
const mentioned = mentionedSkillIds(event.prompt.text, skills).filter((id) => !attached.has(id));
|
|
13972
|
+
if (mentioned.length)
|
|
13973
|
+
event.prompt.skills = [...event.prompt.skills ?? [], ...mentioned.map((id) => ({ id }))];
|
|
13974
|
+
const registered = new Set(skills.map((skill) => skill.id));
|
|
13975
|
+
const used = new Set([...attached, ...mentioned].filter((id) => registered.has(id)));
|
|
13976
|
+
const entries = catalog.skills.filter((skill) => used.has(skillId(skill)));
|
|
13977
|
+
if (entries.length === 0)
|
|
13978
|
+
return;
|
|
13979
|
+
try {
|
|
13980
|
+
await readBodies(entries, true);
|
|
13981
|
+
for (const skill of entries) {
|
|
13982
|
+
const id = skillId(skill);
|
|
13983
|
+
warm.splice(0, warm.length, ...warm.filter((entry) => entry !== id), id);
|
|
13984
|
+
}
|
|
13985
|
+
warm.splice(0, Math.max(0, warm.length - MAX_WARM_SKILLS));
|
|
13986
|
+
await publish();
|
|
13987
|
+
} catch {}
|
|
13988
|
+
});
|
|
13989
|
+
};
|
|
13990
|
+
const refresh = async () => {
|
|
13991
|
+
if (closed || refreshing)
|
|
13992
|
+
return;
|
|
13993
|
+
refreshing = true;
|
|
13994
|
+
try {
|
|
13995
|
+
const value = await call("openwork_skills", {});
|
|
13996
|
+
if (value === null)
|
|
13997
|
+
return;
|
|
13998
|
+
catalog = catalogSchema.parse(value);
|
|
13999
|
+
await readBodies(warmEntries().filter((skill) => skill.digest && !contents.has(skill.digest)), false);
|
|
14000
|
+
await ensurePromptHook();
|
|
14001
|
+
await publish();
|
|
13971
14002
|
} catch {} finally {
|
|
13972
14003
|
refreshing = false;
|
|
13973
14004
|
}
|
|
@@ -13981,6 +14012,7 @@ async function registerCloudSkillDiscovery(context) {
|
|
|
13981
14012
|
controller.abort();
|
|
13982
14013
|
skills = [];
|
|
13983
14014
|
contents.clear();
|
|
14015
|
+
await publishing.catch(() => {});
|
|
13984
14016
|
await promptHook?.dispose();
|
|
13985
14017
|
await registration?.dispose();
|
|
13986
14018
|
};
|
|
@@ -13739,6 +13739,138 @@ function hasGatewayUsageLimitHttpMarker(response) {
|
|
|
13739
13739
|
return response.status === 429 && response.headers.get("X-OpenWork-Error-Code") === GATEWAY_USAGE_LIMIT_ERROR_CODE && response.headers.get("X-OpenWork-Usage-State") === "blocked";
|
|
13740
13740
|
}
|
|
13741
13741
|
|
|
13742
|
+
// ../../packages/types/src/den/inference.ts
|
|
13743
|
+
var INFERENCE_WINDOW_DURATIONS_MS = {
|
|
13744
|
+
five_hour: 5 * 60 * 60 * 1000,
|
|
13745
|
+
weekly: 7 * 24 * 60 * 60 * 1000,
|
|
13746
|
+
monthly: 30 * 24 * 60 * 60 * 1000
|
|
13747
|
+
};
|
|
13748
|
+
var OPENWORK_MODELS_ALLOWANCE_ERROR_CODE = "openwork_models_allowance_reached";
|
|
13749
|
+
var OPENWORK_MODELS_ALLOWANCE_SOURCE = "openwork_models";
|
|
13750
|
+
var OPENWORK_MODELS_ALLOWANCE_MESSAGE = "Your workspace used its OpenWork Models allowance.";
|
|
13751
|
+
function openWorkModelsAllowanceErrorBody() {
|
|
13752
|
+
return {
|
|
13753
|
+
error: {
|
|
13754
|
+
message: OPENWORK_MODELS_ALLOWANCE_MESSAGE,
|
|
13755
|
+
type: "usage_limit_error",
|
|
13756
|
+
param: null,
|
|
13757
|
+
code: OPENWORK_MODELS_ALLOWANCE_ERROR_CODE,
|
|
13758
|
+
source: OPENWORK_MODELS_ALLOWANCE_SOURCE
|
|
13759
|
+
}
|
|
13760
|
+
};
|
|
13761
|
+
}
|
|
13762
|
+
function hasOpenWorkModelsAllowanceHttpMarker(response) {
|
|
13763
|
+
return response.status === 429 && response.headers.get("X-OpenWork-Error-Code") === OPENWORK_MODELS_ALLOWANCE_ERROR_CODE && response.headers.get("X-OpenWork-Usage-State") === "blocked";
|
|
13764
|
+
}
|
|
13765
|
+
var INFERENCE_ACCESS_REASONS = [
|
|
13766
|
+
"admin_disabled",
|
|
13767
|
+
"not_eligible",
|
|
13768
|
+
"free_disabled",
|
|
13769
|
+
"accounting_unavailable",
|
|
13770
|
+
"free_allowance_exhausted",
|
|
13771
|
+
"upstream_unavailable"
|
|
13772
|
+
];
|
|
13773
|
+
var freeInferenceProviderSummarySchema = exports_external.object({
|
|
13774
|
+
state: exports_external.enum(["available", "disabled", "unavailable"]),
|
|
13775
|
+
reason: exports_external.union([exports_external.enum(INFERENCE_ACCESS_REASONS), exports_external.null()]),
|
|
13776
|
+
defaultPinned: exports_external.boolean(),
|
|
13777
|
+
modelGroup: exports_external.object({ id: exports_external.literal("free"), name: exports_external.literal("Free") }),
|
|
13778
|
+
catalog: exports_external.array(exports_external.object({ modelID: exports_external.string(), displayName: exports_external.string(), providerName: exports_external.string(), summary: exports_external.string(), recommended: exports_external.boolean(), rank: exports_external.number(), capabilities: exports_external.array(exports_external.string()) })),
|
|
13779
|
+
allowance: exports_external.object({
|
|
13780
|
+
usageScope: exports_external.literal("organization"),
|
|
13781
|
+
allowanceScope: exports_external.literal("person"),
|
|
13782
|
+
windowStartAt: exports_external.string().datetime(),
|
|
13783
|
+
resetsAt: exports_external.string().datetime(),
|
|
13784
|
+
weeklyLimitUsd: exports_external.number().finite().nonnegative(),
|
|
13785
|
+
joinedMembers: exports_external.number().int().nonnegative(),
|
|
13786
|
+
eligibleMembers: exports_external.number().int().nonnegative(),
|
|
13787
|
+
exhaustedMembers: exports_external.number().int().nonnegative().nullable().describe("Current eligible members whose recorded weekly usage has reached their person-wide limit; not a probe of Gateway request headroom. Null when accounting cannot be verified."),
|
|
13788
|
+
usedUsd: exports_external.number().finite().nonnegative().nullable(),
|
|
13789
|
+
requestCount: exports_external.number().int().nonnegative().nullable()
|
|
13790
|
+
})
|
|
13791
|
+
});
|
|
13792
|
+
var inferenceApiKeyMapSecretSchema = exports_external.record(exports_external.string(), exports_external.string()).refine((value) => Object.keys(value).length > 0, {
|
|
13793
|
+
message: "api_key_map must contain at least one entry"
|
|
13794
|
+
});
|
|
13795
|
+
var inferenceAwsKeysSecretSchema = exports_external.object({
|
|
13796
|
+
accessKeyId: exports_external.string().min(1),
|
|
13797
|
+
secretAccessKey: exports_external.string().min(1),
|
|
13798
|
+
sessionToken: exports_external.string().min(1).optional(),
|
|
13799
|
+
region: exports_external.string().min(1).optional()
|
|
13800
|
+
});
|
|
13801
|
+
var inferenceGcpServiceAccountSecretSchema = exports_external.looseObject({
|
|
13802
|
+
client_email: exports_external.string().min(1),
|
|
13803
|
+
private_key: exports_external.string().min(1),
|
|
13804
|
+
token_uri: exports_external.string().min(1)
|
|
13805
|
+
});
|
|
13806
|
+
var authorizationRevisionSchema = exports_external.string().regex(/^[A-Za-z0-9_-]{43}$/);
|
|
13807
|
+
var microsoftTenantIdSchema = exports_external.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
|
|
13808
|
+
var inferenceOauthTokenSecretSchema = exports_external.object({
|
|
13809
|
+
accessToken: exports_external.string().min(1),
|
|
13810
|
+
refreshToken: exports_external.string().min(1).optional(),
|
|
13811
|
+
tokenType: exports_external.string().min(1).optional(),
|
|
13812
|
+
googleIdentity: exports_external.object({
|
|
13813
|
+
subject: exports_external.string().min(1).max(255),
|
|
13814
|
+
email: exports_external.email(),
|
|
13815
|
+
emailVerified: exports_external.literal(true),
|
|
13816
|
+
clientId: exports_external.string().min(1),
|
|
13817
|
+
authorizationRevision: authorizationRevisionSchema.optional()
|
|
13818
|
+
}).optional(),
|
|
13819
|
+
microsoftIdentity: exports_external.object({
|
|
13820
|
+
tenantId: microsoftTenantIdSchema,
|
|
13821
|
+
objectId: exports_external.string().min(1).max(255),
|
|
13822
|
+
userName: exports_external.string().min(1).max(320).nullable(),
|
|
13823
|
+
clientId: exports_external.string().min(1),
|
|
13824
|
+
authorizationRevision: authorizationRevisionSchema
|
|
13825
|
+
}).optional()
|
|
13826
|
+
});
|
|
13827
|
+
var awsAccountIdSchema = exports_external.string().regex(/^\d{12}$/);
|
|
13828
|
+
var awsPermissionSetNameSchema = exports_external.string().regex(/^[\w+=,.@-]{1,32}$/);
|
|
13829
|
+
var awsSsoStartUrlSchema = exports_external.string().max(2048).refine((value) => {
|
|
13830
|
+
try {
|
|
13831
|
+
const url2 = new URL(value);
|
|
13832
|
+
return url2.protocol === "https:" && !url2.username && !url2.password && !url2.search && !url2.hash && !url2.port && /^[a-z0-9-]+(?:\.[a-z0-9-]+)*\.(?:awsapps\.com|app\.aws)$/i.test(url2.hostname);
|
|
13833
|
+
} catch {
|
|
13834
|
+
return false;
|
|
13835
|
+
}
|
|
13836
|
+
}, { message: "Use the AWS access portal URL, for example https://d-xxxxxxxxxx.awsapps.com/start." });
|
|
13837
|
+
var awsRegionSchema = exports_external.string().max(32).regex(/^[a-z]{2}(?:-[a-z]+)+-\d{1,2}$/);
|
|
13838
|
+
var gatewayAwsSsoSettingsSchema = exports_external.object({
|
|
13839
|
+
startUrl: awsSsoStartUrlSchema,
|
|
13840
|
+
region: awsRegionSchema,
|
|
13841
|
+
accountId: awsAccountIdSchema,
|
|
13842
|
+
roleName: awsPermissionSetNameSchema
|
|
13843
|
+
}).strict();
|
|
13844
|
+
var inferenceAwsSsoSecretSchema = exports_external.object({
|
|
13845
|
+
accessToken: exports_external.string().min(1).max(16384),
|
|
13846
|
+
refreshToken: exports_external.string().min(1).max(16384),
|
|
13847
|
+
clientId: exports_external.string().min(1).max(4096),
|
|
13848
|
+
clientSecret: exports_external.string().min(1).max(16384),
|
|
13849
|
+
clientSecretExpiresAt: exports_external.number().int().positive(),
|
|
13850
|
+
sso: gatewayAwsSsoSettingsSchema,
|
|
13851
|
+
identity: exports_external.object({
|
|
13852
|
+
arn: exports_external.string().min(1).max(2048),
|
|
13853
|
+
userName: exports_external.string().min(1).max(320).nullable(),
|
|
13854
|
+
authorizationRevision: authorizationRevisionSchema
|
|
13855
|
+
})
|
|
13856
|
+
});
|
|
13857
|
+
var GATEWAY_MEMBER_SIGN_IN_METHODS = ["google", "aws_sso", "microsoft"];
|
|
13858
|
+
var gatewayMemberConnectionsResponseSchema = exports_external.object({
|
|
13859
|
+
connections: exports_external.array(exports_external.object({
|
|
13860
|
+
providerId: exports_external.string(),
|
|
13861
|
+
signInMethod: exports_external.enum(GATEWAY_MEMBER_SIGN_IN_METHODS).default("google"),
|
|
13862
|
+
credentialSetId: exports_external.string(),
|
|
13863
|
+
providerName: exports_external.string(),
|
|
13864
|
+
name: exports_external.string(),
|
|
13865
|
+
ready: exports_external.boolean(),
|
|
13866
|
+
hasAccess: exports_external.boolean(),
|
|
13867
|
+
hasCredential: exports_external.boolean(),
|
|
13868
|
+
configurationRequired: exports_external.boolean().default(false),
|
|
13869
|
+
authorizationRevision: exports_external.string().nullable(),
|
|
13870
|
+
accountEmail: exports_external.string().nullable()
|
|
13871
|
+
}))
|
|
13872
|
+
});
|
|
13873
|
+
|
|
13742
13874
|
// src/gateway-quota.ts
|
|
13743
13875
|
var GATEWAY_QUOTA_MESSAGE = "You have reached your AI Gateway usage limit.";
|
|
13744
13876
|
var MAX_ERROR_BYTES = 16384;
|
|
@@ -13761,6 +13893,61 @@ function gatewayBase(id, baseURL) {
|
|
|
13761
13893
|
return;
|
|
13762
13894
|
}
|
|
13763
13895
|
}
|
|
13896
|
+
function openWorkModelsBase(id, baseURL) {
|
|
13897
|
+
if (id !== "openwork" || typeof baseURL !== "string")
|
|
13898
|
+
return;
|
|
13899
|
+
try {
|
|
13900
|
+
const url2 = new URL(baseURL);
|
|
13901
|
+
if (!["https:", "http:"].includes(url2.protocol) || url2.username || url2.password || url2.search || url2.hash)
|
|
13902
|
+
return;
|
|
13903
|
+
url2.pathname = url2.pathname.replace(/\/+$/, "");
|
|
13904
|
+
if (!url2.pathname.endsWith("/api/v1"))
|
|
13905
|
+
return;
|
|
13906
|
+
return url2;
|
|
13907
|
+
} catch {
|
|
13908
|
+
return;
|
|
13909
|
+
}
|
|
13910
|
+
}
|
|
13911
|
+
async function isOpenWorkModelsAllowanceResponse(base, url2, response) {
|
|
13912
|
+
if (url2.origin !== base.origin || !url2.pathname.startsWith(`${base.pathname}/`) || response.url && new URL(response.url).origin !== base.origin || !hasOpenWorkModelsAllowanceHttpMarker(response))
|
|
13913
|
+
return false;
|
|
13914
|
+
const body = await readBoundedJson(response);
|
|
13915
|
+
return record2(body) && record2(body.error) && body.error.code === OPENWORK_MODELS_ALLOWANCE_ERROR_CODE && body.error.source === OPENWORK_MODELS_ALLOWANCE_SOURCE;
|
|
13916
|
+
}
|
|
13917
|
+
async function readBoundedJson(response) {
|
|
13918
|
+
const reader = response.clone().body?.getReader();
|
|
13919
|
+
if (!reader)
|
|
13920
|
+
return null;
|
|
13921
|
+
let timer;
|
|
13922
|
+
const expired = new Promise((resolve) => {
|
|
13923
|
+
timer = setTimeout(() => resolve(null), BODY_TIMEOUT_MS);
|
|
13924
|
+
});
|
|
13925
|
+
const read = async () => {
|
|
13926
|
+
let size = 0;
|
|
13927
|
+
let text = "";
|
|
13928
|
+
const decoder = new TextDecoder;
|
|
13929
|
+
while (true) {
|
|
13930
|
+
const chunk = await reader.read();
|
|
13931
|
+
if (chunk.done)
|
|
13932
|
+
break;
|
|
13933
|
+
size += chunk.value.byteLength;
|
|
13934
|
+
if (size > MAX_ERROR_BYTES)
|
|
13935
|
+
return null;
|
|
13936
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
13937
|
+
}
|
|
13938
|
+
return JSON.parse(text + decoder.decode());
|
|
13939
|
+
};
|
|
13940
|
+
try {
|
|
13941
|
+
return await Promise.race([read(), expired]);
|
|
13942
|
+
} catch {
|
|
13943
|
+
return null;
|
|
13944
|
+
} finally {
|
|
13945
|
+
clearTimeout(timer);
|
|
13946
|
+
reader.cancel().catch(() => {
|
|
13947
|
+
return;
|
|
13948
|
+
});
|
|
13949
|
+
}
|
|
13950
|
+
}
|
|
13764
13951
|
async function isGatewayQuotaResponse(base, url2, response) {
|
|
13765
13952
|
if (url2.origin !== base.origin || !url2.pathname.startsWith(`${base.pathname}/`) || response.url && new URL(response.url).origin !== base.origin || !hasGatewayUsageLimitHttpMarker(response))
|
|
13766
13953
|
return false;
|