@eddyskywalker/dsh-chatgpt-subscription 0.2.15 → 0.2.17

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - 修复开启系统代理的机器上 `web_fetch` 必然失败的问题:DSH 内置抓取 provider 在连接前解析、校验并固定目标地址,而代理工具(Clash/Mihomo 等)的 fake-ip DNS 会把域名解析成 `198.18.0.0/15` 里的保留地址(实测 `api.github.com` → `198.18.0.17`),于是每次调用都以 `WEB_BLOCKED_URL`(`resolves to a non-public IP address`)结束——代理根本没被用上。DSH 只在进程环境变量里读到代理时才走代理,看不到系统代理。现在只要插件配置了可用代理(系统代理自动检测或自定义代理),`web_fetch` 就改用本插件的抓取 provider:由代理解析源站,与 DSH 对“走代理的请求”采用的语义一致;未配置代理时仍由内置 provider 抓取,其解析与固定策略不变。
6
+ - 抓取 provider 新增地址策略 `src/host/fetch-address-policy.ts`,保留内置 provider 安全边界中不需要 DNS 的那一半:URL 里写明的 IP 字面量只有全球可路由单播才放行(loopback、私网、链路本地、CGNAT、多播、保留地址、IPv6 转换与隧道前缀一律拒绝);域名用本机解析器检查一次,落在私网(含 `localhost`、`127.0.0.1.nip.io` 这类)一律拒绝;只有代理的 fake-ip 答案被接受,本机解析不出的域名交给代理处理。与内置 provider 的差别是不再固定(pin)连接地址——fake-ip 环境下这一步无法成立,已在 README 的安全边界中写明。
7
+ - 代理偏好在运行时变化(例如系统代理 ↔ 直连)会重新选择抓取后端;选择 ChatGPT 搜索来源时同时切换搜索与抓取的行为保持不变。
8
+ - 新增 `test/fetch-address-policy.test.ts`(地址分类、fake-ip 识别、私网解析拒绝、解析失败放行),并扩充 `test/codex-fetch.test.ts`、`test/search-provider-switcher.test.ts`、`test/web-provider-lifecycle.test.ts` 覆盖抓取 provider 的拒绝路径与后端切换。
9
+
10
+ ## 0.2.15 - 2026-09-11
11
+
12
+ - 新增子代理模型授权守卫:DSH 的「Subagent」允许列表(`subagent-model-selection`)原本只在模型**显式**填写 `provider`/`model` 时生效,而委派调用不填路由时子代理会继承父级模型——于是 DeepSeek 会话里的子代理仍会在未授权模型上运行。插件现在在 Host 工具注册表上注册一个单调守卫(`ctx.tools.guard`),当调用会话(或最近的、记录了策略的祖先会话)带有允许列表时,任何生效路由不在此列表内的委派都会在子代理启动前被拒绝,拒绝理由里带上全部已授权路由,模型据此重试即可选中合规模型。
13
+ - 该守卫只在会话确实记录了允许列表时介入(与 DSH 委派工具的会话快照语义一致),不会改变未启用该设置的会话;`subagentModelScope: 'preference'` 可让当前设置卡允许列表也约束未记录策略的会话(含恢复的旧会话)。
14
+ - 只读取公开接口:会话日志事件 `subagent/model-selection-policy`、父会话谱系、Host 设置文档与 `ctx.tools.guard`,不修改 DSH 本体。
15
+ - 新增配置项:`subagentModelAuthorization`(默认 `true`)、`subagentModelTools`(默认 `['subagent']`)、`subagentModelScope`(默认 `session`,可选 `preference`)。
16
+ - 新增 14 条单测覆盖策略解析、谱系继承、设置读取、显式/继承路由的拒绝与放行、作用域切换以及守卫安装路径(含注册与释放)。
3
17
  ## 0.2.14 - 2026-09-10
4
18
 
5
19
  - 移除失效的「子代理最大嵌套深度 / 子代理上下文预算」设置:这两个偏好键在 DSH 侧没有任何消费方(全仓库无引用),插件自身也只有写、没有读,保存了也不生效。它们源于 2026-08-28 的 `5b08e2e`——那次提交迁移到 DSH 原生子代理模型路由,删除了 `subagent-context-adapter` / `subagent-policy` / `SubagentSettingsSection`,但把这两个控件和偏好键留了下来。相关 DTO 字段、schema、路由校验、界面控件、6 条未引用文案与失效单测一并移除;子代理模型与思考深度改用 DSH 自身的“Subagent”设置卡片,最大嵌套深度由 preset 中 `tool-subagent` 的 `maxDepth` 决定,README 同步更正。
@@ -61,4 +75,4 @@
61
75
  - 适配 DSH `0.1.0-rc.8`:peerDependencies / devDependencies 中全部 DSH 包范围从 `^0.1.0-rc.6` 提升至 `^0.1.0-rc.8`,并重新生成 package-lock.json。已在本机 rc.8 依赖集上验证:typecheck、55 项测试与 tsdown 构建全部通过,插件代码无需改动。
62
76
  - 移除已无引用的 `@deepseek-ai/dsh-agent` peer 依赖(子代理报告去重兼容模块已于 0.1.11 移除)。
63
77
  - 新增 `@deepseek-ai/dsh-client-connection` peer 依赖(客户端 `ContentBlock` 类型来源,符合 rc.8 客户端包 peer 声明惯例)。
64
- - 说明:rc.8 中 `@deepseek-ai/dsh-client-ui-slots` 不再是 DSH 运行时依赖图成员(仅保留为各客户端包的 devDependency),插件继续将其声明为 peer 依赖以保证安装时解析(`^0.1.0-rc.6` 联网安装时解析到 rc.8;本地锁文件因离线环境暂固定 rc.7,API 与 rc.8 用法一致)。
78
+ - 说明:rc.8 中 `@deepseek-ai/dsh-client-ui-slots` 不再是 DSH 运行时依赖图成员(仅保留为各客户端包的 devDependency),插件继续将其声明为 peer 依赖以保证安装时解析(`^0.1.0-rc.6` 联网安装时解析到 rc.8;本地锁文件因离线环境暂固定 rc.7,API 与 rc.8 用法一致)。
package/README.md CHANGED
@@ -11,6 +11,7 @@
11
11
  - [环境要求](#环境要求)
12
12
  - [安装](#安装)
13
13
  - [使用](#使用)
14
+ - [子代理模型授权](#子代理模型授权0215-起)
14
15
  - [升级、降级与卸载](#升级降级与卸载)
15
16
  - [安全边界](#安全边界)
16
17
  - [插件路由](#插件路由)
@@ -120,7 +121,9 @@ DSH 模型选择器应显示 **“Codex(ChatGPT 订阅)”**。6 Astra 与 G
120
121
 
121
122
  **设置 → Codex 订阅 → 网络代理** 同时控制 GPT 与 Antigravity(Gemini)的 Host 请求,可选择系统代理(自动检测)、自定义代理或直连。Gemini 模型生成、网页登录后的令牌交换、令牌刷新、账号信息、项目发现、配额与模型目录查询均使用此设置;修改后对后续请求生效,无需重启 DSH。浏览器中的 Google 授权页面使用浏览器自己的网络设置。
122
123
 
123
- **搜索与抓取来源** 同时切换 DSH 的搜索和网页抓取后端。选择 ChatGPT 来源后,网页由本插件在 Host 抓取,并使用上述网络代理设置;纯 TUN 模式可使用直连,流量由虚拟网卡接管。来源切换即时生效,DSH web 服务重载后会重新注册插件后端,并保留切回 DSH 默认来源所需的配置。选择 DSH 默认来源时,抓取仍遵循 DSH 自身的代理及非公网地址校验策略。
124
+ **搜索与抓取来源** 切换 DSH 的搜索后端;网页抓取后端在有可用代理时自动改用本插件。选择 ChatGPT 来源后,网页由本插件在 Host 抓取,并使用上述网络代理设置;纯 TUN 模式可使用直连,流量由虚拟网卡接管。来源切换即时生效,DSH web 服务重载后会重新注册插件后端,并保留切回 DSH 默认来源所需的配置。
125
+
126
+ **网页抓取后端(web_fetch)** DSH 内置抓取 provider 会先解析域名、校验并固定解析结果,而且只在进程环境变量里读到代理时才走代理——系统代理对它不可见。代理工具(Clash/Mihomo 等)常把域名解析成自己的 fake-ip 地址(默认 `198.18.0.0/15`),于是内置 provider 直接以 `WEB_BLOCKED_URL`(resolves to a non-public IP address)拒绝,代理根本没被用上。因此只要插件配置了可用代理(**网络代理** 选系统代理且检测到,或填写自定义代理),`web_fetch` 就改用本插件的 provider:由代理解析源站,与 DSH 对“走代理的请求”采用的语义一致;未配置代理时仍由 DSH 内置 provider 抓取,保留其解析与固定策略。若在纯 TUN 模式下把代理设为**直连**,内置 provider 会重新接管,此时可改回系统代理让插件接管抓取。
124
127
 
125
128
  卸载前建议先在设置页点击 **“注销”**,它会删除当前平台的凭据和 Host 内存中的额度缓存。
126
129
 
@@ -132,6 +135,27 @@ DSH 模型选择器应显示 **“Codex(ChatGPT 订阅)”**。6 Astra 与 G
132
135
 
133
136
  > Windows 文件只能由创建它的用户通过 DPAPI 解密。macOS 凭据由登录钥匙串在本机加密保存。Linux 文件是未额外加密的 JSON,依赖目录 `0700` 和文件 `0600` 隔离;不要复制、打印或提交该文件。跨平台迁移需要重新登录。
134
137
 
138
+ ## 子代理模型授权(0.2.15 起)
139
+
140
+ DSH 设置页的「Subagent」卡片会把勾选的模型写成会话级的允许列表(会话日志事件 `subagent/model-selection-policy`)。DSH 内置委派工具只拒绝**模型显式填写**且不在列表内的路由;调用里不写 `provider`/`model` 时,子代理会继承父级模型,于是白名单之外的主模型(例如 `deepseek-official/deepseek-flash`)仍会被子代理使用。
141
+
142
+ 插件在 Host 工具注册表上补一个单调守卫(`ctx.tools.guard`),在委派执行前判定生效路由:
143
+
144
+ - 调用会话(或最近的、记录了策略的祖先会话)带有允许列表时,生效路由必须落在列表内;
145
+ - 不写路由的委派按“继承父级模型”判定,因此父级模型不在列表内时会被拒绝;
146
+ - 拒绝结果里会列出全部已授权路由(例如 `antigravity/gemini-3.8-flash`),模型照此重试即可;`list_subagent_models` 仍只展示已授权路由;
147
+ - 未记录允许列表的会话(例如恢复的旧会话、未启用该设置的会话)保持 DSH 原有行为。
148
+
149
+ 配置项(插件行 `config`,全部可省略):
150
+
151
+ | 字段 | 默认 | 含义 |
152
+ |---|---|---|
153
+ | `subagentModelAuthorization` | `true` | 是否启用上述授权守卫;设为 `false` 回到 DSH 原有行为 |
154
+ | `subagentModelTools` | `['subagent']` | 需要授权的委派工具名;preset 里自定义了 `toolName` 时在此列出 |
155
+ | `subagentModelScope` | `session` | `session` 只约束记录了允许列表的会话;`preference` 额外用当前设置卡列表约束未记录的会话 |
156
+
157
+ 改动只在设置卡片里保存过的勾选生效:设置改动只影响之后新建的会话(DSH 的会话快照语义),已运行的会话继续使用它自己记录的那份列表。
158
+
135
159
  ## 安全边界
136
160
 
137
161
  Antigravity 的 access token / refresh token 使用独立的系统凭据存储:Windows 使用 CurrentUser DPAPI(`$DSH_HOME/storages/antigravity-oauth.json.dpapi`),macOS 使用登录钥匙串,Linux 使用 Secret Service。macOS / Linux 的服务名为 `dsh-antigravity`,账号键按旧凭据文件的绝对路径生成,隔离不同的 `DSH_HOME`。
@@ -148,6 +172,7 @@ Antigravity 的 Gemini 用量以流结束时的上游累计计数为准,缓存
148
172
  - Windows token bundle 使用 CurrentUser DPAPI;明文只经过 Host 内存和 PowerShell stdin/stdout;
149
173
  - macOS token bundle 存入登录钥匙串,通过系统 `security` 命令读写;明文只经过 Host 内存和 `security` 命令行参数,Keychain 在本机加密保存;
150
174
  - Linux token bundle 原子写入当前用户私有文件并在读取时校验普通文件、所有者与 `0600` 权限,同时拒绝符号链接;该文件没有应用层加密,同 UID 进程、root、备份和磁盘快照仍可读取;
175
+ - 本插件抓取 provider 的地址策略:URL 里直接写明的 IP 字面量只有在全球可路由单播时才放行(loopback、私网、链路本地、CGNAT、多播、保留地址、IPv6 转换与隧道前缀一律拒绝);域名会先用本机解析器检查一次,解析结果落在私网时同样拒绝,只有代理的 fake-ip 地址(`198.18.0.0/15`)被接受——那正是本机代理声称拥有该域名的表现;本机解析不出的域名交给代理处理。与 DSH 内置 provider 的差别是不固定(pin)连接地址,因为 fake-ip 环境下这一步无法成立;
151
176
  - 所有平台的 token 都不会进入浏览器、`settings.yaml` 或日志;
152
177
  - 工具调用只有在参数是完整 JSON 对象时才产生可执行的 `block-end`;畸形参数终止本次生成;
153
178
  - 所有修改状态的路由只接受同源 JSON POST,并校验 `Origin` 与 `Host`。
@@ -190,6 +215,7 @@ npm pack --dry-run
190
215
  | 额度显示旧数据 | 设置页会保留最后成功值;等待 15 秒节流窗口后手动刷新 |
191
216
  | 429 | 插件遵守 `Retry-After`,不会高频轮询;模型请求由 DSH retry policy 有界重试 |
192
217
  | 模型不可用 | 检查 ChatGPT 套餐、workspace 权限与当前模型可用性 |
218
+ | `web_fetch` 报 `resolves to a non-public IP address` | 机器启用了系统代理(fake-ip DNS),但插件没有可用代理可接管抓取:在 **设置 → Codex 订阅 → 网络代理** 选择系统代理(自动检测)或填写自定义代理 |
193
219
  | DPAPI 读取失败 | 确认 DSH 以创建凭据时的同一 Windows 用户运行;必要时清理凭据后重新登录 |
194
220
  | Linux 凭据存储不可用 | 确认凭据属于当前用户且权限为 `0600`,父目录权限为 `0700`;修复权限或注销后重新登录 |
195
- | Linux 上工具调用语法错误 | 确认 DSH 暴露的是 `bash`、`sh` 或 `shell`,并使用相应的 Bash/POSIX 语法与 `/` 路径 |
221
+ | Linux 上工具调用语法错误 | 确认 DSH 暴露的是 `bash`、`sh` 或 `shell`,并使用相应的 Bash/POSIX 语法与 `/` 路径 |
package/lib/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import { createRequire } from "node:module";
2
+ import z from "@deepseek-ai/schemastery";
2
3
  import * as LlmModule from "@deepseek-ai/dsh-llm";
3
4
  import { HarnessError, LlmAdapter, LlmError, ProviderRequestId, ReasoningEffortId, attributionHeaders, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
4
5
  import { WebError } from "@deepseek-ai/dsh-web";
6
+ import { lookup } from "node:dns/promises";
7
+ import { isIP } from "node:net";
5
8
  import { createUserMessage } from "@deepseek-ai/dsh-llm/message";
6
9
  import { defineTool } from "@deepseek-ai/dsh-tools";
7
10
  import { createHash, randomBytes, randomUUID } from "node:crypto";
@@ -12,7 +15,6 @@ import path, { dirname, join } from "node:path";
12
15
  import os, { homedir } from "node:os";
13
16
  import { ProxyAgent, fetch as fetch$1 } from "undici";
14
17
  import * as SettingsModule from "@deepseek-ai/dsh-settings";
15
- import z from "@deepseek-ai/schemastery";
16
18
  import fsPromises, { chmod, lstat, mkdir, open, rename, stat, unlink } from "node:fs/promises";
17
19
  import { isDeepStrictEqual } from "node:util";
18
20
  import { URL as URL$1, URLSearchParams as URLSearchParams$1 } from "node:url";
@@ -274,6 +276,225 @@ var CodexChatGptAdapter = class extends LlmAdapter {
274
276
  }
275
277
  };
276
278
  //#endregion
279
+ //#region src/host/fetch-address-policy.ts
280
+ /**
281
+ * Destination policy for this plugin's fetch provider.
282
+ *
283
+ * DSH's built-in provider resolves every destination and validates and pins the
284
+ * address before it connects. That cannot work on a machine whose proxy answers
285
+ * DNS with its own fake-ip range (`198.18.0.0/15` for Clash/Mihomo, the usual
286
+ * companion of a system proxy): the hostname is ordinary, the answer is reserved
287
+ * space, and every request is refused before the proxy that could have resolved
288
+ * it is ever consulted. This provider exists for that machine, so it cannot
289
+ * validate names the way the built-in one does.
290
+ *
291
+ * It keeps the half of the policy that needs no resolution — an address the URL
292
+ * states outright is refused unless it is globally reachable unicast, because a
293
+ * proxy on this machine must not become a path into loopback or a LAN — and it
294
+ * also refuses a name this machine resolves into private space. Only the proxy's
295
+ * own fake-ip answers are accepted, since that is what the proxy's name
296
+ * resolution looks like from here; the URL hostname still reaches the proxy
297
+ * intact, so the proxy decides where the name really goes.
298
+ *
299
+ * @module dsh-chatgpt-subscription/fetch-address-policy
300
+ */
301
+ /**
302
+ * Every IPv4 range a fetch may not target: "this network", private space,
303
+ * carrier-grade NAT, loopback, link-local, IETF assignments, documentation,
304
+ * the 6to4 relay anycast, the benchmarking range the mainstream proxy tools use
305
+ * for fake-ip answers, multicast, and reserved space including the broadcast
306
+ * address.
307
+ */
308
+ const NON_PUBLIC_IPV4 = [
309
+ "0.0.0.0/8",
310
+ "10.0.0.0/8",
311
+ "100.64.0.0/10",
312
+ "127.0.0.0/8",
313
+ "169.254.0.0/16",
314
+ "172.16.0.0/12",
315
+ "192.0.0.0/24",
316
+ "192.0.2.0/24",
317
+ "192.88.99.0/24",
318
+ "192.168.0.0/16",
319
+ "198.18.0.0/15",
320
+ "198.51.100.0/24",
321
+ "203.0.113.0/24",
322
+ "224.0.0.0/4",
323
+ "240.0.0.0/4"
324
+ ];
325
+ /** The fake-ip range the mainstream proxy tools hand out for the names they own. */
326
+ const FAKE_IP_RANGE = "198.18.0.0/15";
327
+ /**
328
+ * Resolve one hostname to every address the local resolver returns, or to no
329
+ * addresses when it cannot.
330
+ *
331
+ * A failure is not evidence of a local destination: the configured proxy
332
+ * resolves the origin itself, so a name this machine cannot resolve must not
333
+ * fail a request the proxy could have served.
334
+ *
335
+ * @param hostname - the URL hostname, without brackets.
336
+ * @returns the resolved addresses, or an empty list when resolution failed.
337
+ */
338
+ async function lookupHostAddresses(hostname) {
339
+ try {
340
+ return (await lookup(hostname, {
341
+ all: true,
342
+ verbatim: true
343
+ })).map((answer) => answer.address);
344
+ } catch {
345
+ return [];
346
+ }
347
+ }
348
+ /**
349
+ * Whether a host is stated as an IP address rather than as a name.
350
+ *
351
+ * @param hostname - a URL hostname, bracketed or not.
352
+ * @returns true when the host is an IPv4 or IPv6 literal.
353
+ */
354
+ function isIpLiteral(hostname) {
355
+ return isIP(unbracket(hostname)) !== 0;
356
+ }
357
+ /**
358
+ * Whether an address is one only a proxy's fake-ip resolver hands out. The IANA
359
+ * benchmarking range carries no real host, so an answer inside it is this
360
+ * machine's proxy claiming the name, not a private destination.
361
+ *
362
+ * @param address - a textual IPv4 (or IPv4-mapped IPv6) address.
363
+ * @returns true when the address is inside the proxy fake-ip range.
364
+ */
365
+ function isProxyFakeIpAddress(address) {
366
+ const value = ipv4Value(unbracket(address).replace(/^::ffff:/i, ""));
367
+ return value !== void 0 && inRange(value, FAKE_IP_RANGE);
368
+ }
369
+ /**
370
+ * Whether an address is globally reachable unicast. IPv4-mapped IPv6 is
371
+ * classified by the IPv4 address it embeds; every other IPv6 address is public
372
+ * only inside `2000::/3`, which excludes loopback, unique-local, link-local,
373
+ * multicast, and the transition and translation prefixes whose real destination
374
+ * cannot be seen from the address alone.
375
+ *
376
+ * @param address - a textual IPv4 or IPv6 address, bracketed or not.
377
+ * @returns true only for a public unicast destination.
378
+ */
379
+ function isPublicIpAddress(address) {
380
+ const unbracketed = unbracket(address);
381
+ const family = isIP(unbracketed);
382
+ if (family === 4) return isPublicIpv4(unbracketed);
383
+ if (family !== 6) return false;
384
+ const mapped = unbracketed.replace(/^::ffff:/i, "");
385
+ if (isIP(mapped) === 4) return isPublicIpv4(mapped);
386
+ return isPublicIpv6(unbracketed);
387
+ }
388
+ /**
389
+ * Whether a hostname is an IP literal no request may target.
390
+ *
391
+ * @param hostname - a URL hostname, bracketed or not.
392
+ * @returns true when the host is a literal address that is not public.
393
+ */
394
+ function isNonPublicIpLiteral(hostname) {
395
+ const unbracketed = unbracket(hostname);
396
+ return isIP(unbracketed) !== 0 && !isPublicIpAddress(unbracketed);
397
+ }
398
+ /**
399
+ * Refuse a fetch destination the local network policy exists to keep out of
400
+ * reach, before any request is made.
401
+ *
402
+ * @param hostname - the URL hostname, bracketed or not.
403
+ * @param addresses - every address the local resolver returned for the host.
404
+ * @throws WebError `WEB_BLOCKED_URL` for a non-public literal, or for a name
405
+ * this machine resolves into private space that is not the proxy's fake-ip.
406
+ */
407
+ function assertPublicFetchTarget(hostname, addresses) {
408
+ if (isNonPublicIpLiteral(hostname)) throw new WebError(`URL hostname "${hostname}" is not a public IP address`, "WEB_BLOCKED_URL");
409
+ for (const address of addresses) {
410
+ if (isPublicIpAddress(address) || isProxyFakeIpAddress(address)) continue;
411
+ throw new WebError(`URL hostname "${hostname}" resolves to a non-public IP address`, "WEB_BLOCKED_URL");
412
+ }
413
+ }
414
+ /** Whether a dotted quad is public, using {@link NON_PUBLIC_IPV4}. */
415
+ function isPublicIpv4(address) {
416
+ const value = ipv4Value(address);
417
+ if (value === void 0) return false;
418
+ return !NON_PUBLIC_IPV4.some((cidr) => inRange(value, cidr));
419
+ }
420
+ /** Whether a 32-bit address sits inside one `a.b.c.d/prefix` range. */
421
+ function inRange(value, cidr) {
422
+ const slash = cidr.indexOf("/");
423
+ const network = ipv4Value(cidr.slice(0, slash));
424
+ const prefix = Number(cidr.slice(slash + 1));
425
+ if (network === void 0 || !Number.isInteger(prefix) || prefix < 0 || prefix > 32) return false;
426
+ const mask = prefix === 0 ? 0 : 4294967295 << 32 - prefix >>> 0;
427
+ return (value & mask) >>> 0 === (network & mask) >>> 0;
428
+ }
429
+ /** Parse a dotted quad into its 32-bit value, or `undefined` when it is not one. */
430
+ function ipv4Value(address) {
431
+ const parts = address.split(".");
432
+ if (parts.length !== 4) return void 0;
433
+ let value = 0;
434
+ for (const part of parts) {
435
+ if (!/^\d{1,3}$/.test(part)) return void 0;
436
+ const octet = Number(part);
437
+ if (octet > 255) return void 0;
438
+ value = (value << 8 | octet) >>> 0;
439
+ }
440
+ return value;
441
+ }
442
+ /** Whether an IPv6 literal is inside `2000::/3` and outside its blocked sub-ranges. */
443
+ function isPublicIpv6(address) {
444
+ const words = ipv6Words(address);
445
+ if (words === void 0) return false;
446
+ const [first = 0, second = 0] = words;
447
+ if ((first >> 8 & 224) !== 32) return false;
448
+ if (first === 8193) {
449
+ if (second === 3512) return false;
450
+ if (second === 0) return false;
451
+ if (second === 2) return false;
452
+ if ((second & 65520) === 16) return false;
453
+ if ((second & 65520) === 32) return false;
454
+ }
455
+ if (first === 8194) return false;
456
+ if (first === 16383 && second <= 4095) return false;
457
+ return true;
458
+ }
459
+ /** Parse an IPv6 literal into its eight 16-bit words, or `undefined` when it is not one. */
460
+ function ipv6Words(address) {
461
+ const zone = address.indexOf("%");
462
+ const halves = (zone === -1 ? address : address.slice(0, zone)).split("::");
463
+ if (halves.length > 2) return void 0;
464
+ const head = parseIpv6Half(halves[0] ?? "");
465
+ if (head === void 0) return void 0;
466
+ if (halves.length === 1) return head.length === 8 ? head : void 0;
467
+ const tail = parseIpv6Half(halves[1] ?? "");
468
+ if (tail === void 0) return void 0;
469
+ const gap = 8 - head.length - tail.length;
470
+ if (gap < 1) return void 0;
471
+ return [
472
+ ...head,
473
+ ...new Array(gap).fill(0),
474
+ ...tail
475
+ ];
476
+ }
477
+ /** Parse one '::'-free half of an IPv6 literal, expanding a trailing dotted quad. */
478
+ function parseIpv6Half(segment) {
479
+ if (segment === "") return [];
480
+ const words = [];
481
+ for (const piece of segment.split(":")) {
482
+ if (piece.includes(".")) {
483
+ const value = ipv4Value(piece);
484
+ if (value === void 0) return void 0;
485
+ words.push(value >>> 16, value & 65535);
486
+ continue;
487
+ }
488
+ if (!/^[0-9a-fA-F]{1,4}$/.test(piece)) return void 0;
489
+ words.push(parseInt(piece, 16));
490
+ }
491
+ return words;
492
+ }
493
+ /** WHATWG URL keeps brackets around an IPv6 hostname; IP parsers do not. */
494
+ function unbracket(hostname) {
495
+ return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
496
+ }
497
+ //#endregion
277
498
  //#region src/host/codex-fetch.ts
278
499
  const DEFAULT_MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
279
500
  const DEFAULT_MAX_BODY_CHARS = 1e5;
@@ -281,6 +502,7 @@ function createCodexFetchProvider(options = {}) {
281
502
  const fetchFn = options.fetchFn ?? fetch;
282
503
  const maxResponseBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
283
504
  const maxBodyChars = options.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS;
505
+ const resolveHostAddresses = options.resolveHostAddresses ?? lookupHostAddresses;
284
506
  return {
285
507
  id: CODEX_FETCH_PROVIDER_ID,
286
508
  available: () => true,
@@ -293,6 +515,7 @@ function createCodexFetchProvider(options = {}) {
293
515
  throw new WebError(`invalid URL: ${targetUrl}`, "WEB_INVALID_URL", { cause: error });
294
516
  }
295
517
  if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") throw new WebError(`unsupported URL scheme "${parsedUrl.protocol}" (only http and https are allowed)`, "WEB_INVALID_URL");
518
+ assertPublicFetchTarget(parsedUrl.hostname, isIpLiteral(parsedUrl.hostname) ? [] : await resolveHostAddresses(parsedUrl.hostname));
296
519
  let response;
297
520
  try {
298
521
  response = await fetchFn(targetUrl, {
@@ -3538,7 +3761,7 @@ var SearchProviderSwitcher = class {
3538
3761
  constructor(loader) {
3539
3762
  this.loader = loader;
3540
3763
  }
3541
- async select(preference) {
3764
+ async select(preference, options = {}) {
3542
3765
  const entry = this.findWebEntry();
3543
3766
  if (entry === null) return;
3544
3767
  const config = currentConfig(entry);
@@ -3547,9 +3770,9 @@ var SearchProviderSwitcher = class {
3547
3770
  this.originalFetchProvider = typeof config.fetchProvider === "string" && config.fetchProvider !== "codex-subscription" ? config.fetchProvider : void 0;
3548
3771
  this.initialized = true;
3549
3772
  }
3550
- const selected = preference === "codex" ? CODEX_SEARCH_PROVIDER_ID : void 0;
3551
- const nextSearch = selected ?? this.originalSearchProvider;
3552
- const nextFetch = selected ? CODEX_FETCH_PROVIDER_ID : this.originalFetchProvider;
3773
+ const codexSelected = preference === SEARCH_PROVIDER_CODEX;
3774
+ const nextSearch = codexSelected ? CODEX_SEARCH_PROVIDER_ID : this.originalSearchProvider;
3775
+ const nextFetch = codexSelected || options.pluginFetch === true ? CODEX_FETCH_PROVIDER_ID : this.originalFetchProvider;
3553
3776
  if (config.searchProvider === nextSearch && config.fetchProvider === nextFetch) return;
3554
3777
  const nextConfig = { ...config };
3555
3778
  if (nextSearch === void 0) delete nextConfig.searchProvider;
@@ -4912,7 +5135,7 @@ function sanitizeText(text) {
4912
5135
  function isRecord(value) {
4913
5136
  return typeof value === "object" && value !== null && !Array.isArray(value);
4914
5137
  }
4915
- function asString(value) {
5138
+ function asString$1(value) {
4916
5139
  return typeof value === "string" ? value : void 0;
4917
5140
  }
4918
5141
  function safeJsonParse(text) {
@@ -4935,10 +5158,10 @@ function parseArguments(raw) {
4935
5158
  return isRecord(parsed) ? parsed : {};
4936
5159
  }
4937
5160
  function imageBlockToPart(block) {
4938
- let data = asString(block.data) || asString(block.base64);
5161
+ let data = asString$1(block.data) || asString$1(block.base64);
4939
5162
  const source = isRecord(block.source) ? block.source : void 0;
4940
- if (!data && source) data = asString(source.data) || asString(source.base64);
4941
- let mimeType = asString(block.mimeType) || asString(block.mediaType) || (source ? asString(source.mimeType) || asString(source.mediaType) : void 0) || "image/png";
5163
+ if (!data && source) data = asString$1(source.data) || asString$1(source.base64);
5164
+ let mimeType = asString$1(block.mimeType) || asString$1(block.mediaType) || (source ? asString$1(source.mimeType) || asString$1(source.mediaType) : void 0) || "image/png";
4942
5165
  if (data?.startsWith("data:")) {
4943
5166
  const match = data.match(/^data:([^;,]+);base64,(.*)$/s);
4944
5167
  if (match) {
@@ -4984,7 +5207,7 @@ function replayBlockFor(message, index) {
4984
5207
  }
4985
5208
  }
4986
5209
  function thoughtSignature(part) {
4987
- return asString(part?.thoughtSignature) || asString(part?.thought_signature) || asString(part?.thinkingSignature) || asString(part?.textSignature);
5210
+ return asString$1(part?.thoughtSignature) || asString$1(part?.thought_signature) || asString$1(part?.thinkingSignature) || asString$1(part?.textSignature);
4988
5211
  }
4989
5212
  function replayPart(part) {
4990
5213
  const copy = { ...part };
@@ -5003,7 +5226,7 @@ function assistantParts(message, model, runtimeModel, toolCalls) {
5003
5226
  if (!isRecord(block)) continue;
5004
5227
  const replay = replayBlockFor(message, index);
5005
5228
  const originalParts = Array.isArray(replay?.parts) ? replay.parts.filter(isRecord) : [];
5006
- if ((block.type === "text" || block.type === "reasoning") && originalParts.length > 0 && originalParts.every((part) => !part.functionCall) && originalParts.map((part) => asString(part.text) || "").join("") === sanitizeText(String(block.text || ""))) {
5229
+ if ((block.type === "text" || block.type === "reasoning") && originalParts.length > 0 && originalParts.every((part) => !part.functionCall) && originalParts.map((part) => asString$1(part.text) || "").join("") === sanitizeText(String(block.text || ""))) {
5007
5230
  parts.push(...originalParts.map(replayPart));
5008
5231
  continue;
5009
5232
  }
@@ -5024,7 +5247,7 @@ function assistantParts(message, model, runtimeModel, toolCalls) {
5024
5247
  const toolId = String(block.id || "");
5025
5248
  const toolName = String(block.name || "");
5026
5249
  const originalCall = originalParts.find((part) => isRecord(part.functionCall));
5027
- const wireId = asString((isRecord(originalCall?.functionCall) ? originalCall.functionCall : void 0)?.id) || (toolCallIdNeeded(model.id, runtimeModel) ? sanitizeToolCallId(toolId, toolName) : originalCall ? void 0 : toolId || void 0);
5250
+ const wireId = asString$1((isRecord(originalCall?.functionCall) ? originalCall.functionCall : void 0)?.id) || (toolCallIdNeeded(model.id, runtimeModel) ? sanitizeToolCallId(toolId, toolName) : originalCall ? void 0 : toolId || void 0);
5028
5251
  toolCalls.set(toolId, {
5029
5252
  name: toolName,
5030
5253
  id: wireId
@@ -5301,8 +5524,8 @@ function processStreamLine(line, state) {
5301
5524
  if (isRecord(part.functionCall)) {
5302
5525
  out.push(...closeCurrentBlock(state));
5303
5526
  const fc = part.functionCall;
5304
- const toolName = asString(fc.name) || "";
5305
- const toolId = asString(fc.id) || sanitizeToolCallId("", toolName);
5527
+ const toolName = asString$1(fc.name) || "";
5528
+ const toolId = asString$1(fc.id) || sanitizeToolCallId("", toolName);
5306
5529
  const argsText = JSON.stringify(isRecord(fc.args) ? fc.args : {});
5307
5530
  const index = state.blocks.length;
5308
5531
  const block = {
@@ -5340,7 +5563,7 @@ function processStreamLine(line, state) {
5340
5563
  }
5341
5564
  collectUsage(chunk.usageMetadata, state);
5342
5565
  if (responseData !== chunk) collectUsage(responseData.usageMetadata, state);
5343
- const finishReason = asString(candidate?.finishReason) || asString(responseData.finishReason);
5566
+ const finishReason = asString$1(candidate?.finishReason) || asString$1(responseData.finishReason);
5344
5567
  if (finishReason) {
5345
5568
  state.finishReason = finishReason;
5346
5569
  out.push(...closeCurrentBlock(state));
@@ -5685,7 +5908,230 @@ function registerAntigravityRoutes(ctx, store, modelSettings, preferences, fetch
5685
5908
  });
5686
5909
  }
5687
5910
  //#endregion
5911
+ //#region src/host/subagent-model-authorization.ts
5912
+ /** DSH settings namespace owned by the Subagent settings card. */
5913
+ const SUBAGENT_MODEL_SELECTION_NAMESPACE = "subagent-model-selection";
5914
+ /** Durable Session event carrying one Session's authorized child routes. */
5915
+ const SUBAGENT_POLICY_EVENT = "subagent/model-selection-policy";
5916
+ /** Delegation tools whose child routes this guard authorizes. */
5917
+ const DEFAULT_DELEGATION_TOOLS = ["subagent"];
5918
+ /**
5919
+ * Read one deployment event field as a string.
5920
+ * @param value - Candidate field value from a durable event.
5921
+ * @returns the string value, or undefined for any other type.
5922
+ */
5923
+ function asString(value) {
5924
+ return typeof value === "string" ? value : void 0;
5925
+ }
5926
+ /**
5927
+ * Read one deployment event field as a plain object.
5928
+ * @param value - Candidate field value from a durable event.
5929
+ * @returns the record value, or undefined for arrays, null, and primitives.
5930
+ */
5931
+ function asRecord(value) {
5932
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
5933
+ }
5934
+ /**
5935
+ * Read one deployment event field as a boolean.
5936
+ * @param value - Candidate field value from a durable event.
5937
+ * @returns the boolean value, or undefined for any other type.
5938
+ */
5939
+ function asBoolean(value) {
5940
+ return typeof value === "boolean" ? value : void 0;
5941
+ }
5942
+ /**
5943
+ * Parse the allowlist out of a durable route-policy event.
5944
+ * @param data - `subagent/model-selection-policy` event payload.
5945
+ * @returns every well-formed route, or undefined when the payload carries none.
5946
+ */
5947
+ function parseAllowedRoutes(data) {
5948
+ const allowed = asRecord(data)?.["allowedModels"];
5949
+ if (!Array.isArray(allowed)) return void 0;
5950
+ const routes = [];
5951
+ for (const entry of allowed) {
5952
+ const record = asRecord(entry);
5953
+ const provider = asString(record?.["provider"]);
5954
+ const model = asString(record?.["model"]);
5955
+ if (provider === void 0 || model === void 0) continue;
5956
+ if (provider.length === 0 || model.length === 0) continue;
5957
+ routes.push({
5958
+ provider,
5959
+ model
5960
+ });
5961
+ }
5962
+ return routes.length === 0 ? void 0 : routes;
5963
+ }
5964
+ /**
5965
+ * Find one Session's recorded allowlist among its durable events.
5966
+ * @param session - Session whose log is scanned.
5967
+ * @returns the authorized routes, or undefined for a Session that recorded none.
5968
+ */
5969
+ function policyRoutesOf(session) {
5970
+ if (session === void 0 || typeof session.eventAt !== "function") return void 0;
5971
+ for (let seq = 0; seq < 256; seq += 1) {
5972
+ let event;
5973
+ try {
5974
+ event = session.eventAt(seq);
5975
+ } catch {
5976
+ return;
5977
+ }
5978
+ if (event === void 0) return void 0;
5979
+ if (event.type !== "subagent/model-selection-policy") continue;
5980
+ const routes = parseAllowedRoutes(event.data);
5981
+ if (routes !== void 0) return routes;
5982
+ }
5983
+ }
5984
+ /**
5985
+ * Resolve the allowlist a delegation from this agent must respect: the agent's
5986
+ * own recorded policy, else the nearest ancestor Session that recorded one
5987
+ * (the inheritance the delegation tool applies to child Sessions).
5988
+ * @param agent - Calling agent.
5989
+ * @param sessions - Session registry used for ancestor lookup.
5990
+ * @returns the authorized routes, or undefined when no Session recorded any.
5991
+ */
5992
+ function authorizedRoutesFor(agent, sessions) {
5993
+ let session = agent?.session;
5994
+ for (let depth = 0; depth < 8 && session !== void 0; depth += 1) {
5995
+ const recorded = policyRoutesOf(session);
5996
+ if (recorded !== void 0) return recorded;
5997
+ const header = session.header;
5998
+ if (header?.origin !== "subagent") return void 0;
5999
+ const parentId = asString(header.parentSession);
6000
+ if (parentId === void 0) return void 0;
6001
+ session = sessions.get(parentId);
6002
+ }
6003
+ }
6004
+ /**
6005
+ * Read the Host preference that owns the allowlist. Values in the stored
6006
+ * document are untrusted JSON, so every field is narrowed before use.
6007
+ * @param settings - Live settings service, when composed.
6008
+ * @returns the resolved preference, or undefined without a settings service.
6009
+ */
6010
+ function subagentModelSelectionPreference(settings) {
6011
+ if (settings === void 0) return void 0;
6012
+ const register = settings["register"];
6013
+ if (typeof register !== "function") return void 0;
6014
+ try {
6015
+ const value = asRecord(register.call(settings, SUBAGENT_MODEL_SELECTION_NAMESPACE, z.object({
6016
+ enabled: z.boolean().default(false),
6017
+ allowedModels: z.array(z.object({
6018
+ provider: z.string().min(1).required(),
6019
+ model: z.string().min(1).required()
6020
+ })).default([])
6021
+ })).get());
6022
+ if (value === void 0) return void 0;
6023
+ return {
6024
+ enabled: asBoolean(value["enabled"]) === true,
6025
+ allowedModels: parseAllowedRoutes(value) ?? []
6026
+ };
6027
+ } catch {
6028
+ return;
6029
+ }
6030
+ }
6031
+ /** Whether an exact route is authorized by a allowlist. */
6032
+ function routesInclude(allowed, provider, model) {
6033
+ return allowed.some((route) => route.provider === provider && route.model === model);
6034
+ }
6035
+ /**
6036
+ * Build the denial reason for a delegation that would run on an unauthorized
6037
+ * route. The reason names the authorized routes so the next call can select one.
6038
+ * @param provider - Effective child provider id, when one is known.
6039
+ * @param model - Effective child model id, when one is known.
6040
+ * @param allowed - Routes the calling Session authorizes.
6041
+ * @param explicit - Whether the model named the route in the tool arguments.
6042
+ * @returns the corrective reason handed back to the model.
6043
+ */
6044
+ function unauthorizedRouteReason(provider, model, allowed, explicit) {
6045
+ const route = provider === void 0 || model === void 0 ? "the inherited route" : `route "${provider}/${model}"`;
6046
+ return `subagent model selection: ${explicit ? "the model this call selected is not on the Session allowlist" : "the route this call would inherit from the parent is not on the Session allowlist"} (${route}). Provide an authorized provider and model — ${allowed.map((entry) => `${entry.provider}/${entry.model}`).join(", ")} — using list_subagent_models to inspect their reasoning efforts.`;
6047
+ }
6048
+ /**
6049
+ * Decide whether one delegation call may start its child.
6050
+ * @param agent - Calling agent.
6051
+ * @param toolName - Tool being dispatched.
6052
+ * @param args - Parsed tool arguments.
6053
+ * @param preference - Current Host preference, when the settings service exists.
6054
+ * @param sessions - Session registry used for ancestor lookup.
6055
+ * @param toolNames - Delegation tool names this guard authorizes.
6056
+ * @param scope - Whether an unrecorded Session falls back to the preference.
6057
+ * @returns a denial reason, or undefined to leave the call untouched.
6058
+ */
6059
+ function delegationDenialReason(agent, toolName, args, preference, sessions, toolNames = DEFAULT_DELEGATION_TOOLS, scope = "session") {
6060
+ if (!toolNames.includes(toolName)) return void 0;
6061
+ if (preference !== void 0 && !preference.enabled) return void 0;
6062
+ const allowed = authorizedRoutesFor(agent, sessions) ?? (scope === "preference" ? preference?.allowedModels ?? [] : []);
6063
+ if (allowed.length === 0) return void 0;
6064
+ const request = asRecord(args) ?? {};
6065
+ const requestedProvider = asString(request["provider"]);
6066
+ const requestedModel = asString(request["model"]);
6067
+ const explicit = requestedProvider !== void 0 || requestedModel !== void 0;
6068
+ const implicit = requestedProvider === void 0 && requestedModel === void 0;
6069
+ const provider = implicit ? asString(agent?.options?.provider) : requestedProvider;
6070
+ const model = implicit ? asString(agent?.options?.model) : requestedModel;
6071
+ if (provider === void 0 || model === void 0) return;
6072
+ if (routesInclude(allowed, provider, model)) return void 0;
6073
+ return unauthorizedRouteReason(provider, model, allowed, explicit);
6074
+ }
6075
+ /**
6076
+ * Build the monotonic tool guard for one deployment. The Host preference is
6077
+ * sampled from the live settings document on every call, because a settings
6078
+ * edit must not rebuild the guard the way it cannot rebuild a Session.
6079
+ * @param options - settings, session registry, and delegation tool names.
6080
+ * @returns a guard that denies delegations outside the Session allowlist.
6081
+ */
6082
+ function createSubagentAuthorization(options) {
6083
+ return (agent, toolName, args) => delegationDenialReason(agent, toolName, args, subagentModelSelectionPreference(options.settings), options.sessions, options.toolNames, options.scope ?? "session");
6084
+ }
6085
+ /** Delegation names this guard recognizes when configuration omits them. */
6086
+ function normalizeDelegationToolNames(value) {
6087
+ const names = Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [];
6088
+ return [...new Set(names.length === 0 ? [...DEFAULT_DELEGATION_TOOLS] : names.map((name) => name.trim()))];
6089
+ }
6090
+ /**
6091
+ * Validate the plugin configuration that owns this guard.
6092
+ * @param toolNames - Candidate delegation tool names.
6093
+ * @returns the exact names this guard authorizes.
6094
+ */
6095
+ function validateDelegationToolNames(toolNames) {
6096
+ if (toolNames.length === 0) throw new Error("dsh-chatgpt-subscription: subagentModelAuthorization.toolNames must name at least one delegation tool");
6097
+ for (const name of toolNames) if (name.length === 0 || name !== name.trim()) throw new Error(`dsh-chatgpt-subscription: subagentModelAuthorization.toolNames entry "${name}" must be a non-empty trimmed string`);
6098
+ return [...toolNames];
6099
+ }
6100
+ /**
6101
+ * Register the monotonic guard on the Host tool registry.
6102
+ * @param ctx - Host context carrying `tools` (and optionally `settings`).
6103
+ * @param sessions - Session registry used for ancestor lookup.
6104
+ * @param config - Enforcement toggle and delegation tool names.
6105
+ * @returns the exact disposer that unregisters the guard.
6106
+ */
6107
+ function installSubagentModelAuthorization(ctx, sessions, config = {}) {
6108
+ const toolNames = validateDelegationToolNames(config.toolNames ?? [...DEFAULT_DELEGATION_TOOLS]);
6109
+ const scope = validateAuthorizationScope(config.scope ?? "session");
6110
+ const settings = ctx.get?.("settings");
6111
+ const authorize = createSubagentAuthorization({
6112
+ settings,
6113
+ sessions,
6114
+ toolNames,
6115
+ scope
6116
+ });
6117
+ return ctx.tools.guard((exec) => authorize(exec.agent, exec.name, exec.arguments));
6118
+ }
6119
+ /**
6120
+ * Validate the configured authorization scope.
6121
+ * @param scope - Candidate scope from deployment configuration.
6122
+ * @returns the exact scope this guard enforces.
6123
+ */
6124
+ function validateAuthorizationScope(scope) {
6125
+ if (scope === "session" || scope === "preference") return scope;
6126
+ throw new Error(`dsh-chatgpt-subscription: subagentModelScope must be "session" or "preference", received ${JSON.stringify(scope)}`);
6127
+ }
6128
+ //#endregion
5688
6129
  //#region src/index.ts
6130
+ const Config = z.object({
6131
+ subagentModelAuthorization: z.boolean().default(true),
6132
+ subagentModelTools: z.array(z.string()).default([]),
6133
+ subagentModelScope: z.union([z.const("session"), z.const("preference")]).default("session")
6134
+ });
5689
6135
  const inject = [
5690
6136
  "webServer",
5691
6137
  "llm",
@@ -5694,12 +6140,24 @@ const inject = [
5694
6140
  "settings",
5695
6141
  "loader"
5696
6142
  ];
5697
- function apply(ctx) {
6143
+ function apply(ctx, pluginConfig = {}) {
5698
6144
  const store = createPlatformTokenStore();
5699
6145
  const preferences = registerPreferenceStore(ctx.settings);
5700
6146
  const antigravityStore = new FileCredentialStore();
5701
6147
  const antigravityModelSettings = new FileModelSettingsStore();
5702
6148
  const antigravityPreferences = registerAntigravityPreferenceStore(ctx.settings, antigravityModelSettings);
6149
+ const delegationToolNames = normalizeDelegationToolNames(pluginConfig.subagentModelTools);
6150
+ if (pluginConfig.subagentModelAuthorization !== false) ctx.inject(["sessions"], (scoped) => {
6151
+ const sessions = scoped.get("sessions");
6152
+ if (sessions === void 0) return;
6153
+ scoped.effect(() => {
6154
+ if (typeof scoped.tools?.guard !== "function") return () => void 0;
6155
+ return installSubagentModelAuthorization(scoped, sessions, {
6156
+ toolNames: delegationToolNames,
6157
+ scope: pluginConfig.subagentModelScope ?? "session"
6158
+ });
6159
+ }, "dsh-chatgpt-subscription: subagent model authorization");
6160
+ });
5703
6161
  ctx.effect(() => {
5704
6162
  const proxyManager = new ProxyManager({
5705
6163
  getPreferences: () => preferences.status(),
@@ -5723,9 +6181,10 @@ function apply(ctx) {
5723
6181
  reasoningSummary: () => preferences.status().reasoningSummary
5724
6182
  }), preferences);
5725
6183
  const searchSwitcher = new SearchProviderSwitcher(ctx.loader);
5726
- const applySearchPreference = (searchProvider = preferences.status().searchProvider) => {
5727
- searchSwitcher.select(searchProvider).catch((error) => {
5728
- ctx.logger.warn(`[dsh-chatgpt-subscription] Search provider preference could not be applied: ${error instanceof Error ? error.message : String(error)}`);
6184
+ const applyWebProviders = (current = preferences.status()) => {
6185
+ const pluginFetch = proxyManager.resolveActiveProxyUrl() !== null;
6186
+ searchSwitcher.select(current.searchProvider, { pluginFetch }).catch((error) => {
6187
+ ctx.logger.warn(`[dsh-chatgpt-subscription] Web provider selection could not be applied: ${error instanceof Error ? error.message : String(error)}`);
5729
6188
  });
5730
6189
  };
5731
6190
  const disposeRoutes = registerRoutes(ctx, oauth, usage, preferences, proxyManager);
@@ -5734,9 +6193,9 @@ function apply(ctx) {
5734
6193
  ctx.inject(["web"], (ctx) => {
5735
6194
  ctx.web.registerSearchProvider(createCodexSearchProvider(oauth, { fetchFn: proxyFetch }));
5736
6195
  ctx.web.registerFetchProvider(createCodexFetchProvider({ fetchFn: proxyFetch }));
5737
- applySearchPreference();
6196
+ applyWebProviders();
5738
6197
  });
5739
- const disposePreferenceWatch = preferences.watch((next) => applySearchPreference(next.searchProvider));
6198
+ const disposePreferenceWatch = preferences.watch((next) => applyWebProviders(next));
5740
6199
  return () => {
5741
6200
  disposePreferenceWatch();
5742
6201
  disposeImageTool();
@@ -5753,4 +6212,4 @@ function localWebServerBaseUrl(host, port) {
5753
6212
  return `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${port}`;
5754
6213
  }
5755
6214
  //#endregion
5756
- export { AntigravityAdapter, CodexChatGptAdapter, FileCredentialStore, FileModelSettingsStore, LinuxFileTokenStore, MacKeychainTokenStore, OAuthService, ProxyManager, ResponsesClient, UsageService, WindowsDpapiTokenStore, apply, beginWebLogin, clearCachedQuota, createCodexFetchProvider, createCodexImageTool, createCodexSearchProvider, createPlatformTokenStore, credentialPath, detectSystemProxy, fetchAccountQuota, getCachedQuota, inject, loginAndSave, mapCodexUsage, modelSettingsPath, parseCodexUsage, parseResponsesStream, refreshAntigravityToken };
6215
+ export { AntigravityAdapter, CodexChatGptAdapter, Config, FileCredentialStore, FileModelSettingsStore, LinuxFileTokenStore, MacKeychainTokenStore, OAuthService, ProxyManager, ResponsesClient, SUBAGENT_MODEL_SELECTION_NAMESPACE, SUBAGENT_POLICY_EVENT, UsageService, WindowsDpapiTokenStore, apply, authorizedRoutesFor, beginWebLogin, clearCachedQuota, createCodexFetchProvider, createCodexImageTool, createCodexSearchProvider, createPlatformTokenStore, createSubagentAuthorization, credentialPath, delegationDenialReason, detectSystemProxy, fetchAccountQuota, getCachedQuota, inject, installSubagentModelAuthorization, loginAndSave, mapCodexUsage, modelSettingsPath, normalizeDelegationToolNames, parseAllowedRoutes, parseCodexUsage, parseResponsesStream, policyRoutesOf, refreshAntigravityToken, subagentModelSelectionPreference, unauthorizedRouteReason, validateAuthorizationScope, validateDelegationToolNames };
@@ -1,9 +1,16 @@
1
1
  import { type WebFetchProvider } from '@deepseek-ai/dsh-web';
2
2
  type FetchLike = typeof fetch;
3
+ /** Resolves a hostname to every address this machine's resolver returns. */
4
+ export type HostAddressResolver = (hostname: string) => Promise<readonly string[]>;
3
5
  export interface CodexFetchProviderOptions {
4
6
  fetchFn?: FetchLike;
5
7
  maxResponseBytes?: number;
6
8
  maxBodyChars?: number;
9
+ /**
10
+ * Address lookup behind the destination policy. Overridden only by focused
11
+ * tests; the policy itself is what every request goes through.
12
+ */
13
+ resolveHostAddresses?: HostAddressResolver;
7
14
  }
8
15
  export declare function createCodexFetchProvider(options?: CodexFetchProviderOptions): WebFetchProvider;
9
16
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"codex-fetch.d.ts","sourceRoot":"","sources":["../../../src/host/codex-fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,gBAAgB,EAA6C,MAAM,sBAAsB,CAAA;AAGjH,KAAK,SAAS,GAAG,OAAO,KAAK,CAAA;AAE7B,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAKD,wBAAgB,wBAAwB,CACtC,OAAO,GAAE,yBAA8B,GACtC,gBAAgB,CAqFlB"}
1
+ {"version":3,"file":"codex-fetch.d.ts","sourceRoot":"","sources":["../../../src/host/codex-fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,gBAAgB,EAA6C,MAAM,sBAAsB,CAAA;AAIjH,KAAK,SAAS,GAAG,OAAO,KAAK,CAAA;AAE7B,4EAA4E;AAC5E,MAAM,MAAM,mBAAmB,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAA;AAElF,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,mBAAmB,CAAA;CAC3C;AAKD,wBAAgB,wBAAwB,CACtC,OAAO,GAAE,yBAA8B,GACtC,gBAAgB,CAgGlB"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Destination policy for this plugin's fetch provider.
3
+ *
4
+ * DSH's built-in provider resolves every destination and validates and pins the
5
+ * address before it connects. That cannot work on a machine whose proxy answers
6
+ * DNS with its own fake-ip range (`198.18.0.0/15` for Clash/Mihomo, the usual
7
+ * companion of a system proxy): the hostname is ordinary, the answer is reserved
8
+ * space, and every request is refused before the proxy that could have resolved
9
+ * it is ever consulted. This provider exists for that machine, so it cannot
10
+ * validate names the way the built-in one does.
11
+ *
12
+ * It keeps the half of the policy that needs no resolution — an address the URL
13
+ * states outright is refused unless it is globally reachable unicast, because a
14
+ * proxy on this machine must not become a path into loopback or a LAN — and it
15
+ * also refuses a name this machine resolves into private space. Only the proxy's
16
+ * own fake-ip answers are accepted, since that is what the proxy's name
17
+ * resolution looks like from here; the URL hostname still reaches the proxy
18
+ * intact, so the proxy decides where the name really goes.
19
+ *
20
+ * @module dsh-chatgpt-subscription/fetch-address-policy
21
+ */
22
+ /**
23
+ * Resolve one hostname to every address the local resolver returns, or to no
24
+ * addresses when it cannot.
25
+ *
26
+ * A failure is not evidence of a local destination: the configured proxy
27
+ * resolves the origin itself, so a name this machine cannot resolve must not
28
+ * fail a request the proxy could have served.
29
+ *
30
+ * @param hostname - the URL hostname, without brackets.
31
+ * @returns the resolved addresses, or an empty list when resolution failed.
32
+ */
33
+ export declare function lookupHostAddresses(hostname: string): Promise<readonly string[]>;
34
+ /**
35
+ * Whether a host is stated as an IP address rather than as a name.
36
+ *
37
+ * @param hostname - a URL hostname, bracketed or not.
38
+ * @returns true when the host is an IPv4 or IPv6 literal.
39
+ */
40
+ export declare function isIpLiteral(hostname: string): boolean;
41
+ /**
42
+ * Whether an address is one only a proxy's fake-ip resolver hands out. The IANA
43
+ * benchmarking range carries no real host, so an answer inside it is this
44
+ * machine's proxy claiming the name, not a private destination.
45
+ *
46
+ * @param address - a textual IPv4 (or IPv4-mapped IPv6) address.
47
+ * @returns true when the address is inside the proxy fake-ip range.
48
+ */
49
+ export declare function isProxyFakeIpAddress(address: string): boolean;
50
+ /**
51
+ * Whether an address is globally reachable unicast. IPv4-mapped IPv6 is
52
+ * classified by the IPv4 address it embeds; every other IPv6 address is public
53
+ * only inside `2000::/3`, which excludes loopback, unique-local, link-local,
54
+ * multicast, and the transition and translation prefixes whose real destination
55
+ * cannot be seen from the address alone.
56
+ *
57
+ * @param address - a textual IPv4 or IPv6 address, bracketed or not.
58
+ * @returns true only for a public unicast destination.
59
+ */
60
+ export declare function isPublicIpAddress(address: string): boolean;
61
+ /**
62
+ * Whether a hostname is an IP literal no request may target.
63
+ *
64
+ * @param hostname - a URL hostname, bracketed or not.
65
+ * @returns true when the host is a literal address that is not public.
66
+ */
67
+ export declare function isNonPublicIpLiteral(hostname: string): boolean;
68
+ /**
69
+ * Refuse a fetch destination the local network policy exists to keep out of
70
+ * reach, before any request is made.
71
+ *
72
+ * @param hostname - the URL hostname, bracketed or not.
73
+ * @param addresses - every address the local resolver returned for the host.
74
+ * @throws WebError `WEB_BLOCKED_URL` for a non-public literal, or for a name
75
+ * this machine resolves into private space that is not the proxy's fake-ip.
76
+ */
77
+ export declare function assertPublicFetchTarget(hostname: string, addresses: readonly string[]): void;
78
+ //# sourceMappingURL=fetch-address-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-address-policy.d.ts","sourceRoot":"","sources":["../../../src/host/fetch-address-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAkCH;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAOtF;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAErD;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAG7D;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAQ1D;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAG9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAQ5F"}
@@ -3,13 +3,27 @@ import type { SearchProviderPreference } from '../shared/contracts.ts';
3
3
  interface LoaderLike {
4
4
  entries(): Iterable<Entry>;
5
5
  }
6
+ /** What one selection asks for on top of the settings preference. */
7
+ export interface WebProviderSelectionOptions {
8
+ /**
9
+ * Whether this plugin's fetch provider must serve the `web_fetch` tool.
10
+ *
11
+ * DSH's built-in provider resolves and pins every destination before it
12
+ * connects and routes through a proxy only when the process environment names
13
+ * one, so an OS-level proxy plus the fake-ip DNS that usually comes with it
14
+ * fails every fetch with `WEB_BLOCKED_URL`. Selecting this plugin's provider
15
+ * hands the origin's resolution to the configured proxy — the same
16
+ * proxied-hop semantics DSH applies to a URL it routes through a proxy.
17
+ */
18
+ readonly pluginFetch?: boolean;
19
+ }
6
20
  export declare class SearchProviderSwitcher {
7
21
  private readonly loader;
8
22
  private originalSearchProvider;
9
23
  private originalFetchProvider;
10
24
  private initialized;
11
25
  constructor(loader: LoaderLike);
12
- select(preference: SearchProviderPreference): Promise<void>;
26
+ select(preference: SearchProviderPreference, options?: WebProviderSelectionOptions): Promise<void>;
13
27
  private findWebEntry;
14
28
  }
15
29
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"search-provider-switcher.d.ts","sourceRoot":"","sources":["../../../src/host/search-provider-switcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mCAAmC,CAAA;AAG9D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAA;AAEtE,UAAU,UAAU;IAClB,OAAO,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAA;CAC3B;AAED,qBAAa,sBAAsB;IAKrB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJnC,OAAO,CAAC,sBAAsB,CAAoB;IAClD,OAAO,CAAC,qBAAqB,CAAoB;IACjD,OAAO,CAAC,WAAW,CAAQ;gBAEE,MAAM,EAAE,UAAU;IAEzC,MAAM,CAAC,UAAU,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAkCjE,OAAO,CAAC,YAAY;CAOrB"}
1
+ {"version":3,"file":"search-provider-switcher.d.ts","sourceRoot":"","sources":["../../../src/host/search-provider-switcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mCAAmC,CAAA;AAG9D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAA;AAEtE,UAAU,UAAU;IAClB,OAAO,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAA;CAC3B;AAED,qEAAqE;AACrE,MAAM,WAAW,2BAA2B;IAC1C;;;;;;;;;OASG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAC/B;AAED,qBAAa,sBAAsB;IAKrB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJnC,OAAO,CAAC,sBAAsB,CAAoB;IAClD,OAAO,CAAC,qBAAqB,CAAoB;IACjD,OAAO,CAAC,WAAW,CAAQ;gBAEE,MAAM,EAAE,UAAU;IAEzC,MAAM,CAAC,UAAU,EAAE,wBAAwB,EAAE,OAAO,GAAE,2BAAgC,GAAG,OAAO,CAAC,IAAI,CAAC;IAkC5G,OAAO,CAAC,YAAY;CAOrB"}
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Host-side authorization for the models a subagent may run.
3
+ *
4
+ * The `subagent-model-selection` preference is sampled into each Session as a
5
+ * durable route allowlist, but the built-in delegation tool only rejects an
6
+ * explicit route outside that list: a call that selects no route lets the child
7
+ * inherit the parent's model, so a DeepSeek parent could spawn DeepSeek
8
+ * children even though the allowlist authorizes only other models.
9
+ *
10
+ * This guard closes that gap from the plugin plane. Whenever the calling
11
+ * Session (or the nearest ancestor that recorded one) carries an allowlist,
12
+ * a delegation whose effective child route the list does not authorize is
13
+ * denied before the child starts, with the authorized routes in the reason so
14
+ * the model can retry and pick one of them.
15
+ * @module dsh-chatgpt-subscription/subagent-model-authorization
16
+ */
17
+ import type { Context } from '@deepseek-ai/cordis';
18
+ import type { SettingsProvider } from '@deepseek-ai/dsh-settings';
19
+ /** DSH settings namespace owned by the Subagent settings card. */
20
+ export declare const SUBAGENT_MODEL_SELECTION_NAMESPACE = "subagent-model-selection";
21
+ /** Durable Session event carrying one Session's authorized child routes. */
22
+ export declare const SUBAGENT_POLICY_EVENT = "subagent/model-selection-policy";
23
+ /** Delegation tools whose child routes this guard authorizes. */
24
+ export declare const DEFAULT_DELEGATION_TOOLS: readonly string[];
25
+ /** One exact provider/model route the user authorized for children. */
26
+ export interface AllowedModelRoute {
27
+ readonly provider: string;
28
+ readonly model: string;
29
+ }
30
+ /** Resolved model-selection preference read from the Host settings document. */
31
+ export interface SubagentModelSelectionPreference {
32
+ readonly enabled: boolean;
33
+ readonly allowedModels: readonly AllowedModelRoute[];
34
+ }
35
+ /** One durable Session record this module reads: its events and its lineage. */
36
+ export interface PolicySession {
37
+ eventAt?(seq: number): {
38
+ type?: unknown;
39
+ data?: unknown;
40
+ } | undefined;
41
+ readonly header?: {
42
+ readonly origin?: unknown;
43
+ readonly parentSession?: unknown;
44
+ };
45
+ }
46
+ /** Session lookup the guard walks from the calling agent to its ancestors. */
47
+ export interface SessionsResolver {
48
+ get(id: string): PolicySession | undefined;
49
+ }
50
+ /** Calling agent fields the guard reads. */
51
+ export interface AuthorizationAgent {
52
+ readonly session?: PolicySession;
53
+ readonly options?: {
54
+ readonly provider?: unknown;
55
+ readonly model?: unknown;
56
+ };
57
+ }
58
+ /** What the delegation tool received from the model. */
59
+ export interface DelegationArguments {
60
+ readonly provider?: unknown;
61
+ readonly model?: unknown;
62
+ }
63
+ /**
64
+ * Parse the allowlist out of a durable route-policy event.
65
+ * @param data - `subagent/model-selection-policy` event payload.
66
+ * @returns every well-formed route, or undefined when the payload carries none.
67
+ */
68
+ export declare function parseAllowedRoutes(data: unknown): AllowedModelRoute[] | undefined;
69
+ /**
70
+ * Find one Session's recorded allowlist among its durable events.
71
+ * @param session - Session whose log is scanned.
72
+ * @returns the authorized routes, or undefined for a Session that recorded none.
73
+ */
74
+ export declare function policyRoutesOf(session: PolicySession | undefined): AllowedModelRoute[] | undefined;
75
+ /**
76
+ * Resolve the allowlist a delegation from this agent must respect: the agent's
77
+ * own recorded policy, else the nearest ancestor Session that recorded one
78
+ * (the inheritance the delegation tool applies to child Sessions).
79
+ * @param agent - Calling agent.
80
+ * @param sessions - Session registry used for ancestor lookup.
81
+ * @returns the authorized routes, or undefined when no Session recorded any.
82
+ */
83
+ export declare function authorizedRoutesFor(agent: AuthorizationAgent | undefined, sessions: SessionsResolver): AllowedModelRoute[] | undefined;
84
+ /**
85
+ * Read the Host preference that owns the allowlist. Values in the stored
86
+ * document are untrusted JSON, so every field is narrowed before use.
87
+ * @param settings - Live settings service, when composed.
88
+ * @returns the resolved preference, or undefined without a settings service.
89
+ */
90
+ export declare function subagentModelSelectionPreference(settings: SettingsProvider | undefined): SubagentModelSelectionPreference | undefined;
91
+ /**
92
+ * Build the denial reason for a delegation that would run on an unauthorized
93
+ * route. The reason names the authorized routes so the next call can select one.
94
+ * @param provider - Effective child provider id, when one is known.
95
+ * @param model - Effective child model id, when one is known.
96
+ * @param allowed - Routes the calling Session authorizes.
97
+ * @param explicit - Whether the model named the route in the tool arguments.
98
+ * @returns the corrective reason handed back to the model.
99
+ */
100
+ export declare function unauthorizedRouteReason(provider: string | undefined, model: string | undefined, allowed: readonly AllowedModelRoute[], explicit: boolean): string;
101
+ /** Which Session's recorded allowlist a delegation must respect. */
102
+ export type AuthorizationScope =
103
+ /**
104
+ * `session` (default): a Session that recorded an allowlist governs itself
105
+ * and its descendants, matching the delegation tool's durable snapshot; a
106
+ * Session that recorded none keeps the built-in behavior.
107
+ */
108
+ 'session'
109
+ /**
110
+ * `preference`: the current Host preference also governs Sessions that never
111
+ * recorded an allowlist, so the Settings card applies without restarting the
112
+ * Session. A Session that recorded its own routes still outranks it.
113
+ */
114
+ | 'preference';
115
+ /**
116
+ * Decide whether one delegation call may start its child.
117
+ * @param agent - Calling agent.
118
+ * @param toolName - Tool being dispatched.
119
+ * @param args - Parsed tool arguments.
120
+ * @param preference - Current Host preference, when the settings service exists.
121
+ * @param sessions - Session registry used for ancestor lookup.
122
+ * @param toolNames - Delegation tool names this guard authorizes.
123
+ * @param scope - Whether an unrecorded Session falls back to the preference.
124
+ * @returns a denial reason, or undefined to leave the call untouched.
125
+ */
126
+ export declare function delegationDenialReason(agent: AuthorizationAgent | undefined, toolName: string, args: unknown, preference: SubagentModelSelectionPreference | undefined, sessions: SessionsResolver, toolNames?: readonly string[], scope?: AuthorizationScope): string | undefined;
127
+ /** Runtime inputs the guard closes over. */
128
+ export interface SubagentAuthorizationOptions {
129
+ /** Live settings service, when composed; absent leaves recorded policies in charge. */
130
+ readonly settings?: SettingsProvider;
131
+ /** Session registry used for ancestor lookup. */
132
+ readonly sessions: SessionsResolver;
133
+ /** Exact delegation tool names this guard authorizes. */
134
+ readonly toolNames: readonly string[];
135
+ /** Whether an unrecorded Session falls back to the current preference. */
136
+ readonly scope?: AuthorizationScope;
137
+ }
138
+ /** The exact guard signature the Host tool registry evaluates. */
139
+ export type DelegationGuard = (agent: AuthorizationAgent | undefined, toolName: string, args: unknown) => string | undefined;
140
+ /**
141
+ * Build the monotonic tool guard for one deployment. The Host preference is
142
+ * sampled from the live settings document on every call, because a settings
143
+ * edit must not rebuild the guard the way it cannot rebuild a Session.
144
+ * @param options - settings, session registry, and delegation tool names.
145
+ * @returns a guard that denies delegations outside the Session allowlist.
146
+ */
147
+ export declare function createSubagentAuthorization(options: SubagentAuthorizationOptions): DelegationGuard;
148
+ /** Delegation names this guard recognizes when configuration omits them. */
149
+ export declare function normalizeDelegationToolNames(value: unknown): string[];
150
+ /**
151
+ * Validate the plugin configuration that owns this guard.
152
+ * @param toolNames - Candidate delegation tool names.
153
+ * @returns the exact names this guard authorizes.
154
+ */
155
+ export declare function validateDelegationToolNames(toolNames: readonly string[]): string[];
156
+ /** Configuration accepted by {@link installSubagentModelAuthorization}. */
157
+ export interface SubagentModelAuthorizationConfig {
158
+ /** Delegation tool names the guard authorizes. */
159
+ readonly toolNames?: readonly string[];
160
+ /** Whether an unrecorded Session falls back to the current preference. */
161
+ readonly scope?: AuthorizationScope;
162
+ }
163
+ /**
164
+ * Register the monotonic guard on the Host tool registry.
165
+ * @param ctx - Host context carrying `tools` (and optionally `settings`).
166
+ * @param sessions - Session registry used for ancestor lookup.
167
+ * @param config - Enforcement toggle and delegation tool names.
168
+ * @returns the exact disposer that unregisters the guard.
169
+ */
170
+ export declare function installSubagentModelAuthorization(ctx: Context, sessions: SessionsResolver, config?: SubagentModelAuthorizationConfig): () => void;
171
+ /**
172
+ * Validate the configured authorization scope.
173
+ * @param scope - Candidate scope from deployment configuration.
174
+ * @returns the exact scope this guard enforces.
175
+ */
176
+ export declare function validateAuthorizationScope(scope: unknown): AuthorizationScope;
177
+ //# sourceMappingURL=subagent-model-authorization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subagent-model-authorization.d.ts","sourceRoot":"","sources":["../../../src/host/subagent-model-authorization.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,27 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
+ import z from '@deepseek-ai/schemastery';
3
+ /** Optional deployment configuration for this plugin. */
4
+ export interface Config {
5
+ /**
6
+ * Whether the Subagent model allowlist also governs the route a delegation
7
+ * that selects no model would inherit from its parent. Default `true`; set
8
+ * `false` to leave inherited routes to the built-in delegation tool.
9
+ */
10
+ subagentModelAuthorization?: boolean;
11
+ /** Delegation tool names the authorization guard recognizes (default `subagent`). */
12
+ subagentModelTools?: string[];
13
+ /**
14
+ * `session` (default) enforces the allowlist a Session recorded, matching the
15
+ * delegation tool's snapshot; `preference` also enforces the current Settings
16
+ * card allowlist for Sessions that recorded none.
17
+ */
18
+ subagentModelScope?: 'session' | 'preference';
19
+ }
20
+ export declare const Config: z<Config>;
2
21
  export declare const inject: string[];
3
- export declare function apply(ctx: Context): void;
22
+ export declare function apply(ctx: Context, pluginConfig?: Config): void;
4
23
  export { ProxyManager, detectSystemProxy } from './host/proxy-manager.ts';
24
+ export { SUBAGENT_MODEL_SELECTION_NAMESPACE, SUBAGENT_POLICY_EVENT, authorizedRoutesFor, createSubagentAuthorization, delegationDenialReason, installSubagentModelAuthorization, normalizeDelegationToolNames, parseAllowedRoutes, policyRoutesOf, subagentModelSelectionPreference, unauthorizedRouteReason, validateAuthorizationScope, validateDelegationToolNames, } from './host/subagent-model-authorization.ts';
5
25
  export { OAuthService } from './host/oauth-service.ts';
6
26
  export { CodexChatGptAdapter } from './host/adapter.ts';
7
27
  export { createCodexImageTool } from './host/codex-images.ts';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AA+BlD,eAAO,MAAM,MAAM,UAAqE,CAAA;AAExF,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAuExC;AAED,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAA;AAClE,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAA;AAChE,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAA;AAClF,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AACtF,OAAO,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAA;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAA;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAA;AACtE,YAAY,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAE/E,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAA;AAClE,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,cAAc,EACd,iBAAiB,GAClB,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAA;AAClG,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAsCxC,yDAAyD;AACzD,MAAM,WAAW,MAAM;IACrB;;;;OAIG;IACH,0BAA0B,CAAC,EAAE,OAAO,CAAA;IACpC,qFAAqF;IACrF,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC7B;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,SAAS,GAAG,YAAY,CAAA;CAC9C;AAED,eAAO,MAAM,MAAM,EAAE,CAAC,CAAC,MAAM,CAI3B,CAAA;AAEF,eAAO,MAAM,MAAM,UAAqE,CAAA;AAExF,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,YAAY,GAAE,MAAW,GAAG,IAAI,CAsGnE;AAED,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AACzE,OAAO,EACL,kCAAkC,EAClC,qBAAqB,EACrB,mBAAmB,EACnB,2BAA2B,EAC3B,sBAAsB,EACtB,iCAAiC,EACjC,4BAA4B,EAC5B,kBAAkB,EAClB,cAAc,EACd,gCAAgC,EAChC,uBAAuB,EACvB,0BAA0B,EAC1B,2BAA2B,GAC5B,MAAM,wCAAwC,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAA;AAClE,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAA;AAChE,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAA;AAClF,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AACtF,OAAO,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAA;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAA;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAA;AACtE,YAAY,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAE/E,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAA;AAClE,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,cAAc,EACd,iBAAiB,GAClB,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAA;AAClG,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eddyskywalker/dsh-chatgpt-subscription",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "description": "DSH provider plugin for Codex access through a ChatGPT subscription.",
5
5
  "author": {
6
6
  "name": "eddyskywalker",