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.
Files changed (79) hide show
  1. package/README.md +9 -2
  2. package/bin/openwork-server.mjs +1 -1
  3. package/bin/platform.mjs +7 -7
  4. package/dist/opencode-plugin-deps-1.18.30.tgz +0 -0
  5. package/dist/opencode-plugins/openwork-cloud-skills-v2.js +87 -41
  6. package/dist/opencode-plugins/openwork-context-v2.js +67 -35
  7. package/dist/opencode-plugins/openwork-gateway-quota-v2.js +187 -0
  8. package/dist/opencode-plugins/openwork-gateway-quota.js +232 -23
  9. package/dist/opencode-plugins/openwork-provider-sync-v2.js +85 -2
  10. package/dist/openwork-server.mjs +919 -333
  11. package/package.json +2 -2
  12. package/web/assets/{abnfDiagram-VCTEODGH-D0_4pXEx.js → abnfDiagram-VCTEODGH-DGCabXot.js} +1 -1
  13. package/web/assets/app-CkmqBm_1.css +32 -0
  14. package/web/assets/{app-BCXQnJUH.js → app-Cy3sY2K8.js} +435 -425
  15. package/web/assets/{arc-BSTwaImw.js → arc-OJTfUP9L.js} +1 -1
  16. package/web/assets/{architectureDiagram-5GKGNRK7-cXwF0iP0.js → architectureDiagram-5GKGNRK7-Ch0Zt81S.js} +1 -1
  17. package/web/assets/{artifact-code-view-CUXv3CYK.js → artifact-code-view-CSo8dBet.js} +1 -1
  18. package/web/assets/{artifact-spreadsheet-editor-CxhNYmjP.js → artifact-spreadsheet-editor-DG2AL4aO.js} +1 -1
  19. package/web/assets/{artifact-text-editor-k2TGQI-i.js → artifact-text-editor-BKs1cI6V.js} +1 -1
  20. package/web/assets/{blockDiagram-OSKFZWR5-Cl9p_z8u.js → blockDiagram-OSKFZWR5-Dur4PitI.js} +1 -1
  21. package/web/assets/{c4Diagram-7LVT6UL2-YQjnoUQ0.js → c4Diagram-7LVT6UL2-DN-qrh5o.js} +1 -1
  22. package/web/assets/channel-68fpiRG4.js +1 -0
  23. package/web/assets/{chunk-2Q5K7J3B-YZaSwlzj.js → chunk-2Q5K7J3B-BSmk0dam.js} +1 -1
  24. package/web/assets/{chunk-5VM5RSS4-BICC_wIw.js → chunk-5VM5RSS4-BwAmjLhK.js} +1 -1
  25. package/web/assets/{chunk-F27PBJKO-CBxmcmxs.js → chunk-F27PBJKO-DLKbY_DS.js} +1 -1
  26. package/web/assets/{chunk-HLEWEB6X-2h2z7zt8.js → chunk-HLEWEB6X-s4I3BuHN.js} +1 -1
  27. package/web/assets/{chunk-JWPE2WC7-BnAMOpXl.js → chunk-JWPE2WC7-LslAnaQK.js} +1 -1
  28. package/web/assets/{chunk-POPQ4Y6H-pvDYQBBM.js → chunk-POPQ4Y6H-e0_lKBGk.js} +1 -1
  29. package/web/assets/{chunk-SVP7TREG-QuKLl3v0.js → chunk-SVP7TREG-CBKOSsQl.js} +1 -1
  30. package/web/assets/{chunk-XXDRQBXY-4Bt9wCBO.js → chunk-XXDRQBXY-Bwqkh7ZD.js} +1 -1
  31. package/web/assets/{chunk-ZLD2IHE6-C_RSrkqU.js → chunk-ZLD2IHE6-anLhhsDd.js} +1 -1
  32. package/web/assets/classDiagram-CYGNFDIV-Z4CG2h2d.js +1 -0
  33. package/web/assets/classDiagram-v2-TLXNO2FR-Z4CG2h2d.js +1 -0
  34. package/web/assets/{cose-bilkent-JH36ORCC-CciOYQb4.js → cose-bilkent-JH36ORCC-CCj043X5.js} +1 -1
  35. package/web/assets/{cynefin-OW5HDTMX-k8-mUlq4.js → cynefin-OW5HDTMX-BGWD-i0N.js} +1 -1
  36. package/web/assets/{cynefinDiagram-5FMLGOSQ-D1qAHzq3.js → cynefinDiagram-5FMLGOSQ-ZWq_C5SK.js} +1 -1
  37. package/web/assets/{dagre-OS7QT2EB-BP1VuF_z.js → dagre-OS7QT2EB-In3Mah_T.js} +1 -1
  38. package/web/assets/{diagram-S7CK7UJ4-crXkWleC.js → diagram-S7CK7UJ4-DaTd6oXD.js} +1 -1
  39. package/web/assets/{diagram-UQ7AKVKN-CtHAmCY-.js → diagram-UQ7AKVKN-D3jwCd_v.js} +1 -1
  40. package/web/assets/{diagram-VSXAHHWV-C-Fhuwy8.js → diagram-VSXAHHWV-CsK-_hfn.js} +1 -1
  41. package/web/assets/{diagram-VX7I27RA-DkRkNPuj.js → diagram-VX7I27RA-BBLQ2mL7.js} +1 -1
  42. package/web/assets/{diagram-Z3DM3KII-DVERPCFS.js → diagram-Z3DM3KII-CUM2PN0X.js} +1 -1
  43. package/web/assets/{ebnfDiagram-PWID7BFC-PrSifYGF.js → ebnfDiagram-PWID7BFC-BjJTJ9Vr.js} +1 -1
  44. package/web/assets/{erDiagram-2YWLMYGG-D8-wfUYq.js → erDiagram-2YWLMYGG-BhjMQMDs.js} +1 -1
  45. package/web/assets/{flowDiagram-T62WH6J4-BS-_wkGe.js → flowDiagram-T62WH6J4-KrGbrW-t.js} +1 -1
  46. package/web/assets/{ganttDiagram-EL5Y4UJY-D9PWmuWE.js → ganttDiagram-EL5Y4UJY-Co_OE6ls.js} +1 -1
  47. package/web/assets/{gitGraphDiagram-WWUBYQGX-DIP9p63c.js → gitGraphDiagram-WWUBYQGX-BxM4C4Ti.js} +1 -1
  48. package/web/assets/{infoDiagram-FKFFQAWI-BzU6-acC.js → infoDiagram-FKFFQAWI-C8YaW3a6.js} +1 -1
  49. package/web/assets/{ishikawaDiagram-5VMMS53U-CvYS2ayc.js → ishikawaDiagram-5VMMS53U-MPTb2C9Y.js} +1 -1
  50. package/web/assets/{journeyDiagram-EYS64GPL-Cr0qdK-D.js → journeyDiagram-EYS64GPL-Dzq-vllP.js} +1 -1
  51. package/web/assets/{kanban-definition-UXKFOSKX-DhffGp7U.js → kanban-definition-UXKFOSKX-DWhE5bnH.js} +1 -1
  52. package/web/assets/{layout-b5jqOIam.js → layout-CMyIV62W.js} +1 -1
  53. package/web/assets/{linear-zLNMwGhI.js → linear-CX-tWibZ.js} +1 -1
  54. package/web/assets/{mermaid.core-DB_TNiSG.js → mermaid.core-DcgS6fFC.js} +6 -6
  55. package/web/assets/{mindmap-definition-THT77NOG-DkQz2hbh.js → mindmap-definition-THT77NOG-CJzNRYpk.js} +1 -1
  56. package/web/assets/{pegDiagram-XKGWAZYB-CB313CDF.js → pegDiagram-XKGWAZYB-BSIi724T.js} +1 -1
  57. package/web/assets/{pieDiagram-E7YTZNPT-yaDJVY4d.js → pieDiagram-E7YTZNPT-CoArMhZD.js} +1 -1
  58. package/web/assets/{quadrantDiagram-AXDQQJYC-BkV8iwHS.js → quadrantDiagram-AXDQQJYC-B9HoxOyK.js} +1 -1
  59. package/web/assets/{railroadDiagram-O6MQD6OU-T_JVDRpV.js → railroadDiagram-O6MQD6OU-C-aYV4dU.js} +1 -1
  60. package/web/assets/{requirementDiagram-IS5BZ75X-B68yTCqr.js → requirementDiagram-IS5BZ75X-DHPnVlti.js} +1 -1
  61. package/web/assets/{sankeyDiagram-P5KCCOFB-Bx31O26O.js → sankeyDiagram-P5KCCOFB-DlxAwgv1.js} +1 -1
  62. package/web/assets/{sequenceDiagram-WJ2MYXX4-CsCoFTAg.js → sequenceDiagram-WJ2MYXX4-DzVPZb2m.js} +1 -1
  63. package/web/assets/{sizeCapture-INFHLROL-B3a49hkJ.js → sizeCapture-INFHLROL-BkwjaMAx.js} +1 -1
  64. package/web/assets/{stateDiagram-XQSTLZYL-DjCdHmx5.js → stateDiagram-XQSTLZYL-DIeNtdWj.js} +1 -1
  65. package/web/assets/stateDiagram-v2-IH3M54BS-CZR_2wND.js +1 -0
  66. package/web/assets/{swimlanes-V6O3JKXN-4u8S1I3e.js → swimlanes-V6O3JKXN-CPUSs3Tj.js} +1 -1
  67. package/web/assets/swimlanesDiagram-JKAHXJPX-C1Sms2Ey.js +8 -0
  68. package/web/assets/{timeline-definition-24CTP7MA-r3GnOjJo.js → timeline-definition-24CTP7MA-CjBwDFJH.js} +1 -1
  69. package/web/assets/{vennDiagram-4TSXK5OY-59IlTU54.js → vennDiagram-4TSXK5OY-DOsoEsMT.js} +1 -1
  70. package/web/assets/{wardleyDiagram-VM6X3IG4-C3ALfLHE.js → wardleyDiagram-VM6X3IG4-_fhLZdvF.js} +1 -1
  71. package/web/assets/{workspace-file-tree-C6GhNx3L.js → workspace-file-tree-CTzNl5A5.js} +1 -1
  72. package/web/assets/{xychartDiagram-S5SC5T6Z-CoYtWlkb.js → xychartDiagram-S5SC5T6Z-D4Kz81TF.js} +1 -1
  73. package/web/index.html +2 -2
  74. package/web/assets/app-DVblmMbF.css +0 -32
  75. package/web/assets/channel-DwV664-P.js +0 -1
  76. package/web/assets/classDiagram-CYGNFDIV-BKhd9faI.js +0 -1
  77. package/web/assets/classDiagram-v2-TLXNO2FR-BKhd9faI.js +0 -1
  78. package/web/assets/stateDiagram-v2-IH3M54BS-Cf0tqY7x.js +0 -1
  79. 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 runs on Node.js 22.13 or newer on macOS, Linux, and Windows (arm64 or x64); Bun is not required at runtime.
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`
@@ -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 current Node.js LTS and try again.`,
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
- // node:sqlite (the server's runtime database) is available without a flag
20
- // from Node 22.13 and 23.4.
21
- export const MIN_NODE_VERSION = "22.13.0";
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 [major, minor] = version.replace(/^v/, "").split(".").map(Number);
25
- if (major === 22) return minor >= 13;
26
- if (major === 23) return minor >= 4;
27
- return major > 23;
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
  }
@@ -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
- * with its full description and its authorized SKILL.md, verified against the
65
- * digest Den published. A skill whose body could not be read or verified keeps
66
- * the discovery note, so it still works through get_skill.
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. A body is re-read only
123
- // when Den publishes a new digest, and dropped as soon as Den stops listing it.
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
- const loadContents = async (catalog) => {
138
- const wanted = new Set(catalog.skills.flatMap(skill => skill.digest && !skill.setup ? [skill.digest] : []));
139
- for (const digest of contents.keys())
140
- if (!wanted.has(digest))
141
- contents.delete(digest);
142
- for (const skill of catalog.skills) {
143
- if (closed || !skill.digest || !skill.url || skill.setup || contents.has(skill.digest))
144
- continue;
145
- const result = z.object({ content: z.string() }).safeParse(await call("openwork_skill_read", { url: skill.url }).catch(() => null));
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 (result.success && sha256Digest(result.data.content) === skill.digest)
148
- contents.set(skill.digest, result.data.content);
156
+ if (body !== undefined && sha256Digest(body) === skill.digest)
157
+ contents.set(skill.digest, body);
149
158
  }
150
159
  };
151
- const ensurePromptHook = async () => {
152
- if (promptHook || !context.session)
153
- return;
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
- const refresh = async () => {
165
- if (closed || refreshing)
166
- return;
167
- refreshing = true;
168
- try {
169
- const value = await call("openwork_skills", {});
170
- if (value === null)
171
- return;
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 loadContents = async (catalog) => {
13911
- const wanted = new Set(catalog.skills.flatMap((skill) => skill.digest && !skill.setup ? [skill.digest] : []));
13912
- for (const digest of contents.keys())
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
- promptHook = await context.session.hook("prompt", (event) => {
13927
- if (skills.length === 0)
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
- refreshing = true;
13940
- try {
13941
- const value = await call("openwork_skills", {});
13942
- if (value === null)
13943
- return;
13944
- const catalog = catalogSchema.parse(value);
13945
- await loadContents(catalog);
13946
- await ensurePromptHook();
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;