@epoch-agent/server 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Lang, WireSkillImportFailure, WireSkillImportForm, WireCapabilitiesResponse, WireSkillBodyResponse, EpochUserContent, AgentEvent, WireEnvelope, CustomCommandDef, ExpandedCommand, ModelRef, SetSelectionResult, WireCommandExpansion, WireCustomCommand, WireGoalPhase, WireGoalRefusal, WireTurnState, WireSessionFinish, ApprovalOutcome, QuestionAnswer, SessionSurfaceView, WireSessionReference, WireFileReference, ProviderType, ModelSelectionOrigin, WireModelCaveat, WireModelRejection, ContextBreakdown, PermissionLevel, WirePluginSourceType, WirePluginRefuseReason, WirePluginEntry, WireModelSource, WireModelProbeStatus, ScheduleDefinition, ScheduleRun, ScheduleBackendKind, OperationType, ScheduleTrigger, WireScheduleIssue, ScheduleRunStatus, TrustLevel, WireSandboxReason, WireAuditOutcome, WireAuditCode, WireCachedApproval, WireCachedDecision, WireToolVerdict, WireToolGateAxis, WireSecurityResponse, WireRewindScope, WireRewindRequest, WireCheckpointSummary, WireRewindPreview, WireRewindResponse, ActiveAgentRole, WireSettingValue, WireSettingLayer, WireSettingWriteLayer, WireSettingFutile, WireSettingApply, WireSettingsResponse, EpochConfig, WireCompressionLine, WireProviderSummary, WireToolSummary, Diagnostic, WireReplayMessage, WireFileChange, WireAuthGuard, WireWorkspaceDiffResponse, BackgroundTaskInfo, WireTasksResponse, WireBackgroundTask, WireScheduleRow } from '@epoch-agent/protocol';
1
+ import { Lang, WireSkillImportFailure, WireSkillImportForm, WireSkillRemoveFailure, WireCapabilitiesResponse, WireSkillBodyResponse, EpochUserContent, AgentEvent, WireEnvelope, CustomCommandDef, ExpandedCommand, ModelRef, SetSelectionResult, WireCommandExpansion, WireCustomCommand, WireGoalPhase, WireGoalRefusal, WireTurnState, WireSessionFinish, ApprovalOutcome, QuestionAnswer, SessionSurfaceView, WireSessionReference, WireFileReference, ProviderType, ModelSelectionOrigin, WireModelCaveat, WireModelRejection, ContextBreakdown, PermissionLevel, WirePermissionRememberFailure, WirePluginSourceType, WirePluginRefuseReason, WirePluginEntry, WireModelSource, WireModelProbeStatus, ScheduleDefinition, ScheduleRun, ScheduleBackendKind, OperationType, ScheduleTrigger, WireScheduleIssue, ScheduleRunStatus, TrustLevel, WireSandboxReason, WireAuditOutcome, WireAuditCode, WireCachedApproval, WireCachedDecision, WireToolVerdict, WireToolGateAxis, WireSecurityResponse, WireRewindScope, WireRewindRequest, WireCheckpointSummary, WireRewindPreview, WireRewindResponse, ActiveAgentRole, WireSettingValue, WireSettingLayer, WireSettingWriteLayer, WireSettingFutile, WireSettingApply, WireSettingValueKind, WireSettingsResponse, EpochConfig, WireCompressionLine, WireProviderSummary, WireToolSummary, Diagnostic, WireReplayMessage, WireFileChange, WireAuthGuard, WireWorkspaceDiffResponse, BackgroundTaskInfo, WireTasksResponse, WireBackgroundTask, WireScheduleRow } from '@epoch-agent/protocol';
2
2
  import { SerializableApprovalRequest, SerializableQuestionRequest, ApprovalRevokeResult, LevelChangeResult, WorkspaceFilesView } from '@epoch-agent/runtime';
3
3
  import { ServerResponse } from 'node:http';
4
4
 
@@ -543,11 +543,18 @@ type ImportResultView<T> = ({
543
543
  issues?: readonly IssueView[];
544
544
  };
545
545
  /**
546
- * `SkillControl`(runtime)在服务端眼里的样子 —— **只有这两个动作**。
546
+ * `SkillControl`(runtime)在服务端眼里的样子 —— **只有导入那两个动作**。
547
547
  *
548
- * 镜像里只有它们,于是这个包**写不出** `SkillSystem` 上那六个写方法、
549
- * 也写不出会 `matchCount++` 的 `view()` 和会自动删技能的 `maintain()`:
550
- * 那不是靠 review 盯住的,是编译期的事(同 `McpControlView` 只有 reconnect)。
548
+ * 镜像里只有它们,于是这个包**写不出** `SkillSystem` 上那几个写方法、
549
+ * 也写不出会 `matchCount++` 的 `view()`:那不是靠 review 盯住的,
550
+ * 是编译期的事(同 `McpControlView` 只有 reconnect)。
551
+ *
552
+ * ⚠️ **「删一条技能」不在这张镜像里,它自己有一张**
553
+ * ([skill-remove.ts](./skill-remove.js) 的 `SkillRemoveControlView`),
554
+ * 而 `RuntimeCapabilities.skillWrite` 声明成两张的**交集**。镜像跟着 handler
555
+ * 走、而不是跟着 runtime 那一格走,是这一套收窄能起作用的前提:一张把两件事
556
+ * 都装进来的镜像,等于任何一个 handler 都能顺手调另一件事 —— 判据同
557
+ * `capability.ts` 上 `mcp` 那一格。
551
558
  */
552
559
  interface SkillImportControlView {
553
560
  preview(source: string, lang?: Lang): Promise<ImportResultView<{
@@ -556,6 +563,83 @@ interface SkillImportControlView {
556
563
  import(source: string, token: string, lang?: Lang): Promise<ImportResultView<ImportDoneView>>;
557
564
  }
558
565
 
566
+ /**
567
+ * 删掉一条技能的那一条端点(2026-09-01):
568
+ *
569
+ * ```
570
+ * POST /api/skills/remove {name} → 递归删掉 ~/.epoch/skills/<分类>/<名字>
571
+ * ```
572
+ *
573
+ * 起因是一句话:**能加就得能删**。在这之前能力页技能那一栏只有「导入技能」,
574
+ * 拿掉一份得让用户自己去 `~/.epoch/skills/` 里翻目录 —— 而那一栏里的每一行
575
+ * 都是每一轮都在花钱的常驻开销(行尾那个 tokens 数就是它)。
576
+ *
577
+ * ## 一、为什么是 POST,为什么没有 token
578
+ *
579
+ * POST 和导入那两条同一条判据(`auth.ts` 的 Origin 校验只在非安全方法上要求,
580
+ * 而这条会在用户机器上真的删文件)。**但没有「预览 → 确认」那一对**,
581
+ * 而那不是省事:
582
+ *
583
+ * - 导入那个 token 防的是「用户看过的那一份」和「真装上的那一份」不是一份 ——
584
+ * 预览和导入之间目录可能被改过。删除这边没有这条缝:**要删掉的东西由名字
585
+ * 唯一确定**(技能名在 `SkillSystem` 里是全局唯一的 map 键),而不是由一次
586
+ * 扫描的结果确定;
587
+ * - 删除的风险不是「删错了一份」,是**不可撤销**(`rmSync(recursive)`,没有
588
+ * 回收站)。挡它的是另外两样东西:界面上那一步「再按一次确认」,
589
+ * 和回执里那个 `path` —— 用户得看得见自己刚销毁了盘上的哪一处。
590
+ *
591
+ * ## 二、⚠️ 这条**不吃 `refusedByLan`**,而那是一个判断
592
+ *
593
+ * 插件那几个写口和 `POST /api/roles` 都要求回环绑定才给写。这一条不要求,
594
+ * 判据是:**导入那条也不要求**。只给删除加一道闸的结果,是 `--host 0.0.0.0`
595
+ * 那一档下技能变成「只能加不能删」—— 那正是这次要修的病,换个地方原样长回来。
596
+ *
597
+ * 两条一起加闸不行(那是把已经在用的导入功能收回去),所以这一格的口径是
598
+ * 「和导入同档」。真要收,得两条一起收,而那是一次独立的决定 ——
599
+ * 别只改这半边。
600
+ *
601
+ * ## 三、只有**用户级**删得掉,而那道闸不在这一层
602
+ *
603
+ * 项目级 / 插件 / 宿主那三档 `SkillSystem.requireWritable()` 会抛,
604
+ * runtime 的 `SkillControl.remove` 把它翻成 `reason: 'readonly'` 加上 core
605
+ * 那句带路径的原话。这一层**不自己判 scope** —— 判两遍会有一天判得不一样,
606
+ * 而只有 core 那一遍碰得到磁盘。
607
+ *
608
+ * 界面上那颗按钮只画在用户级那一组的行上,但那是**体贴,不是安全性质**:
609
+ * 手搓一条 `POST /api/skills/remove {"name":"项目里那条"}` 会拿到 200 +
610
+ * `ok:false` + `readonly`。
611
+ *
612
+ * ## 四、失败是 200,判据在 `WireSkillRemoveResponse` 上
613
+ *
614
+ * 四个 `reason` 各对一个不同的下一步(那张表在 protocol 那个类型上)。
615
+ * `detail` 那句话在 **core / runtime** 里现渲染,所以 `lang` 要从这一层
616
+ * 一路递下去 —— 这条病 2026-08-18 在导入那一屏上实测见过一次
617
+ * (判据在 [skill-import.ts](./skill-import.js) 文件头倒数第三节)。
618
+ */
619
+
620
+ /** 删成了那一下回的东西(runtime 的 `SkillRemoveDone`) */
621
+ interface RemoveDoneView {
622
+ name: string;
623
+ path: string;
624
+ }
625
+ type RemoveResultView = ({
626
+ ok: true;
627
+ } & RemoveDoneView) | {
628
+ ok: false;
629
+ reason: WireSkillRemoveFailure;
630
+ detail: string;
631
+ };
632
+ /**
633
+ * `SkillControl`(runtime)在服务端眼里的样子 —— **只有 `remove` 一个动作**。
634
+ *
635
+ * 和 [skill-import.ts](./skill-import.js) 那张镜像刻意分开,理由在那张镜像的
636
+ * ⚠️ 上:镜像跟着 handler 走,于是**这个文件里写不出 `import`**,
637
+ * 而 `RuntimeCapabilities.skillWrite` 是两张的交集(同 `mcp` 那一格)。
638
+ */
639
+ interface SkillRemoveControlView {
640
+ remove(name: string, lang?: Lang): Promise<RemoveResultView>;
641
+ }
642
+
559
643
  /**
560
644
  * 能力页的取数与投影 —— `GET /api/sessions/:id/capabilities`(方案 42 PR-1),
561
645
  * 外加技能正文那条下钻(方案 56 §1.2)。
@@ -694,12 +778,21 @@ interface RuntimeCapabilities {
694
778
  */
695
779
  skillIndexResidency(role: string | null): ReadonlyMap<string, 'indexed' | 'capped' | 'role'>;
696
780
  /**
697
- * 从**本机目录**导入技能(方案 42 §六)。**进程级**,同 {@link mcp}。
781
+ * 技能目录的写口:从**本机目录**导入(方案 42 §六)+ 删掉一条用户级技能
782
+ * (2026-09-01)。**进程级**,同 {@link mcp}。
698
783
  *
699
- * ⚠️ 它收的是**路径**不是字节 —— 那是这条路上的安全性质本体,判据全文在
784
+ * ⚠️ 导入收的是**路径**不是字节 —— 那是那条路上的安全性质本体,判据全文在
700
785
  * [skill-import.ts](./skill-import.js) 的文件头。
786
+ *
787
+ * ⚠️ **交集,理由逐字同下面 {@link mcp} 那一格**:两个动作挂在同一个 runtime
788
+ * 对象上(`runtime.skillWrite`),分成两份声明是为了让镜像跟着 handler 走 ——
789
+ * 于是 `skill-import.ts` 里写不出 `remove`、`skill-remove.ts` 里写不出 `import`。
790
+ *
791
+ * ⚠️ **删除那条刻意不吃 `refusedByLan`**,和导入同档;那是一个判断而不是遗漏,
792
+ * 判据全文在 [skill-remove.ts](./skill-remove.js) 文件头第二节 ——
793
+ * 要收得两条一起收。
701
794
  */
702
- readonly skillImport: SkillImportControlView;
795
+ readonly skillWrite: SkillImportControlView & SkillRemoveControlView;
703
796
  /**
704
797
  * MCP 的写口:重连一台(方案 56 §1.1)+ 加一台(2026-08-18)+ **那份文件的原文
705
798
  * 读写与应用**(2026-08-18)。**全都是进程级**。
@@ -1870,7 +1963,16 @@ interface SessionPermissionsView {
1870
1963
  managed(): {
1871
1964
  bypassDisabled: boolean;
1872
1965
  };
1873
- setLevel(level: PermissionLevel): SetLevelView;
1966
+ /**
1967
+ * @param opts.remember 这一发**算不算用户显式挑的**(2026-08-31)。
1968
+ * ⚠️ **照抄真身的必填形状**(runtime 的 `SessionPermissions.setLevel`),
1969
+ * 别在这份镜像上写成可选的:真身那边这个参数存在的理由是「定时任务和用户
1970
+ * 菜单共用同一个方法」,写成可选之后,漏传的那一方会静默拿到默认行为 ——
1971
+ * 而那正是这个参数要防的东西。三行表在 runtime 的 `DefaultLevelSlot` 上。
1972
+ */
1973
+ setLevel(level: PermissionLevel, opts: {
1974
+ remember: boolean;
1975
+ }): SetLevelView;
1874
1976
  }
1875
1977
  /**
1876
1978
  * 一次换档的结果。**只有码,没有句子** —— 措辞归浏览器的两份 catalog
@@ -1878,6 +1980,14 @@ interface SessionPermissionsView {
1878
1980
  */
1879
1981
  type SetLevelView = {
1880
1982
  ok: true;
1983
+ /**
1984
+ * 「换成了,但没记住」(2026-08-31)。**只在真没记住时有值** ——
1985
+ * 它直接转成 `WirePermissionSetResponse.notRemembered`,判据在那儿。
1986
+ */
1987
+ notRemembered?: {
1988
+ reason: WirePermissionRememberFailure;
1989
+ path?: string;
1990
+ };
1881
1991
  } | {
1882
1992
  ok: false;
1883
1993
  reason: 'managed-bypass-disabled';
@@ -2361,10 +2471,10 @@ interface PluginControlView {
2361
2471
  declare function toWirePluginEntry(entry: PluginEntryView): WirePluginEntry;
2362
2472
 
2363
2473
  /**
2364
- * 「我们支持哪几家、这一家有哪些模型」 —— `GET /api/providers` 和
2365
- * `POST /api/providers/:type/models`。
2474
+ * 「我们支持哪几家、这一家有哪些模型、给这一家配一把 key」 —— `GET /api/providers`、
2475
+ * `POST /api/providers/:type/models` 和 `POST /api/providers/:type/key`。
2366
2476
  *
2367
- * ## ⚠️ 这两条**进程级,URL 上一个 `:id` 都没有**
2477
+ * ## ⚠️ 这三条**进程级,URL 上一个 `:id` 都没有**
2368
2478
  *
2369
2479
  * 判据同 `POST /api/mcp/:name/reconnect` 和 `GET /api/workspaces/dirs`:
2370
2480
  * provider 清单和模型清单**不属于某一段会话**。挂到 `/sessions/:id/` 底下会得到
@@ -2408,6 +2518,17 @@ declare function toWirePluginEntry(entry: PluginEntryView): WirePluginEntry;
2408
2518
  * (`cli/src/commands/model.ts` 的 `describeDiscovery()`),照抄过来就能省一份
2409
2519
  * catalog。不许 —— 那一句是给同进程宿主(终端)拼的一次渲染,
2410
2520
  * 而且它只分两档(有没有 key),这一屏要分四档。
2521
+ *
2522
+ * ⚠️ 配 key 那一条上同一个诱惑又来了一遍,形状是 `SecretWriteResult.detail`
2523
+ * (「已写入系统钥匙串」那句现成的话)。**照旧不发**:下发的是
2524
+ * `backend` 这个码,措辞在浏览器那侧。判据逐字同上。
2525
+ *
2526
+ * ## ⚠️ 配 key 那一条是**单向**的:key 上得去,下不来
2527
+ *
2528
+ * 请求体里有裸 key,响应体里只有 `maskApiKey` 过的那份。
2529
+ * [§6.4 的第一条「绝不」](../../../docs/verify/VERIFY_RECORD-20-web.md)
2530
+ * 管的是**下行**,这一条一个字都没松 —— 而这个包在 `check-layers.mjs` 的
2531
+ * `LIBRARY_DIRS` 里(一处 console 都不许有),所以它也没有把 key 打进日志的口子。
2411
2532
  */
2412
2533
 
2413
2534
  /** 一个可挑的 provider 在服务端眼里的样子。runtime 的 `ProviderOption` 满足它 */
@@ -2416,6 +2537,15 @@ interface ProviderOptionView {
2416
2537
  label: string;
2417
2538
  envVar: string | null;
2418
2539
  hasKey: boolean;
2540
+ keyHint: string | null;
2541
+ }
2542
+ /** 一次配 key 的结果。runtime 的 `ProviderKeyWritten` 满足它 */
2543
+ interface ProviderKeyWrittenView {
2544
+ envVar: string;
2545
+ backend: string;
2546
+ encrypted: boolean;
2547
+ envPath: string | null;
2548
+ keyHint: string;
2419
2549
  }
2420
2550
  /** 探一家的结果。runtime 的 `ModelSuggestions` 满足它 */
2421
2551
  interface ModelSuggestionsView {
@@ -2425,17 +2555,22 @@ interface ModelSuggestionsView {
2425
2555
  status: WireModelProbeStatus;
2426
2556
  }
2427
2557
  /**
2428
- * `ModelCatalogControl`(runtime)在服务端眼里的样子 —— **只有两个动词**。
2558
+ * `ModelCatalogControl`(runtime)在服务端眼里的样子 —— **只有三个动词**。
2429
2559
  *
2430
- * 镜像里只有这两个,于是这个包**写不出**「把 homeDir 拿来自己拼一个缓存路径」
2560
+ * 镜像里只有这三个,于是这个包**写不出**「把 homeDir 拿来自己拼一个缓存路径」
2431
2561
  * 或者「读一把裸 key」这种事:那两样在收窄口上压根不存在。
2432
2562
  * 同 `McpControlView` 只有 `reconnect`、五份安全镜像各自漏掉一个中文字段。
2563
+ *
2564
+ * ⚠️ `setKey` 是**单向**的:裸 key 只往里走,回来的
2565
+ * {@link ProviderKeyWrittenView} 上只有脱敏串 —— 这个包因此**读不出**
2566
+ * 任何一把已经存着的 key,连自己刚写的那把都读不回来。
2433
2567
  */
2434
2568
  interface ModelCatalogView {
2435
2569
  providers(): readonly ProviderOptionView[];
2436
2570
  discover(provider: ProviderType, opts?: {
2437
2571
  refresh?: boolean;
2438
2572
  }): Promise<ModelSuggestionsView>;
2573
+ setKey(provider: ProviderType, apiKey: string): Promise<ProviderKeyWrittenView>;
2439
2574
  }
2440
2575
  /**
2441
2576
  * server 从 runtime 借的「provider 目录」那一片。`WebRuntimeView` 继承它。
@@ -3008,8 +3143,13 @@ interface PermissionsView {
3008
3143
  * **当场编译不过**(同 `AuditRowView.code` 用 `WireAuditCode` 的机制)。
3009
3144
  * 写成 `string` 的话,新码会被静默报成 `managed`,而界面照着它说一句
3010
3145
  * 和真实原因无关的话。
3146
+ *
3147
+ * @param opts.remember 这一发算不算用户显式挑的(2026-08-31)。同 `SetLevelView`
3148
+ * 那份镜像:**照抄真身的必填形状,别写成可选的**,判据在那儿。
3011
3149
  */
3012
- setLevel: (level: PermissionLevel) => LevelChangeResult;
3150
+ setLevel: (level: PermissionLevel, opts: {
3151
+ remember: boolean;
3152
+ }) => LevelChangeResult;
3013
3153
  /**
3014
3154
  * ⚠️ 第七样,消费方也不在这个文件里 —— 是 [tools.ts](./tools.js)
3015
3155
  * (设置 › 工具那一节)。摆在这儿的理由同上面 `setLevel` 那一段:
@@ -3667,6 +3807,15 @@ interface SettingRowView {
3667
3807
  chain: readonly SettingStepView[];
3668
3808
  overridable: boolean;
3669
3809
  writes: readonly SettingWriteView[];
3810
+ /**
3811
+ * 界面画哪种输入控件。**钉成 {@link WireSettingValueKind} 而不是 `string`**,
3812
+ * 判据同 `layer` 那一处(文件头第 2 条):引擎那边多一档控件形态而忘了同步
3813
+ * wire 上这个联合,装配那一行当场编译不过 —— 写成 `string` 的话,那一档会
3814
+ * 一路发到浏览器,然后落进「认不出就当输入框」的兜底分支里,
3815
+ * 而那看起来只是「这个控件有点怪」,查不出来。
3816
+ */
3817
+ valueKind?: WireSettingValueKind;
3818
+ choices?: readonly string[];
3670
3819
  }
3671
3820
  /** runtime 的 `SettingWriteReport` 的镜像。`reason` 是码,不是句子 —— 见文件头第 1 条 */
3672
3821
  type SettingWriteReportView = {
@@ -4084,8 +4233,49 @@ interface ApiContext {
4084
4233
  * 今天的两个消费方:那条端点的 403,和 `GET /api/config` 上原样下发的那一格
4085
4234
  * (界面据此在按下去之前把话说出来)。完整判据在 protocol 的
4086
4235
  * `wire-agent-role.ts` 文件头。
4236
+ *
4237
+ * ⚠️ **2026-09-01 起它还是另一格的输入**:{@link nativeDirPicker} 由它算出来
4238
+ * (远程绑定时不许弹原生框)。那一格是**算出来的结论**,这一格是**事实** ——
4239
+ * 别把两者合成一个,判据在它自己那条 JSDoc 上。
4087
4240
  */
4088
4241
  lanExposed: boolean;
4242
+ /**
4243
+ * 这个进程能不能替浏览器弹一个**系统目录选择框**(2026-09-01)。
4244
+ *
4245
+ * 真源是 `workspace/native-pick.ts` 的 `resolveNativeDirPicker()`,装配时算一次
4246
+ * (回环绑定 + 非 SSH + 平台在支持列表里,三件的合取;它继承的那个 `ssh -L`
4247
+ * 盲区也写在那儿)。
4248
+ *
4249
+ * ## ⚠️ 和 {@link lanExposed} 一样**必传**,理由却更硬一档
4250
+ *
4251
+ * 那一格忘了传的后果是「以为自己在本机」;这一格忘了传(默认成 `true`)的后果是
4252
+ * **一个远程用户点一下,框弹在服务器那台没人看着的机器上,而这一发 HTTP 就
4253
+ * 挂在那儿等一个永远不会有的答复**。所以它没有默认值,而且往「不能弹」那一侧落。
4254
+ *
4255
+ * 两个消费方,形状同上:`POST /api/workspaces/pick` 的 403,和
4256
+ * `GET /api/config` 上原样下发的那一格(界面据此决定那一下走原生框还是自绘
4257
+ * 那一屏)。完整判据在 protocol 的 `WirePickDirectoryResponse` 那一节。
4258
+ */
4259
+ nativeDirPicker: boolean;
4260
+ /**
4261
+ * 浏览器多半就在这个服务进程的机器上吗(2026-09-02)。
4262
+ *
4263
+ * 真源是 `workspace/native-pick.ts` 的 `isSameMachine()` —— **和
4264
+ * {@link nativeDirPicker} 是同一个函数、同一批事实**,今天两格的值必然相等。
4265
+ *
4266
+ * ## 那为什么是两格,而不是一格
4267
+ *
4268
+ * 因为它们是**两个结论**,只是今天恰好共用一条判据:一格答「能不能弹目录框」
4269
+ * (要 osascript / PowerShell 那两套 UI 实现),一格答「能不能把文件交给系统
4270
+ * 默认应用」(要的是 `open` / `rundll32`)。合成一格之后,哪天有人给目录框
4271
+ * 补上 Linux 的 `zenity`,「用默认应用打开」会**跟着**在 Linux 上打开 ——
4272
+ * 而那一侧一次都没验过。判据在 `isSameMachine` 那条 📮 上。
4273
+ *
4274
+ * 消费方只有一个:`GET /api/sessions/:id/file-stat` 里 `openable` 那一格
4275
+ * (界面据此决定画不画那个按钮 —— 决定 20 ①)。⚠️ **`POST .../file-open`
4276
+ * 自己也要再查一遍**,别指望浏览器只在按钮亮着的时候才发那一下。
4277
+ */
4278
+ sameMachine: boolean;
4089
4279
  }
4090
4280
 
4091
4281
  /**
@@ -4387,6 +4577,122 @@ interface WorkspaceDiffOptions {
4387
4577
  */
4388
4578
  declare function collectWorkspaceDiff(opts: WorkspaceDiffOptions): Promise<WireWorkspaceDiffResponse>;
4389
4579
 
4580
+ /**
4581
+ * 在**服务进程那台机器**上弹一个系统目录选择框 —— `POST /api/workspaces/pick`
4582
+ * (2026-09-01)。
4583
+ *
4584
+ * ## 它推翻的是 [browse.ts](./browse.js) 文件头那四条里的一条半
4585
+ *
4586
+ * 那份文件头 2026-08-17 写着「为什么不弹原生对话框」四条,末尾那句是
4587
+ * 「哪天这个赌注被推翻了,回来读这一段,**别重新推一遍**」。这一份就是那一天,
4588
+ * 所以这里**只记这一轮新增的判据**,四条各自的下场逐条写在 protocol 的
4589
+ * `WirePickDirectoryResponse` 上(那儿是契约,也是判据的落点)。一句话版:
4590
+ * ① 远程那条**成立**,于是有了下面那道闸;④ 信任那条**作废**(方案 55 §2.6
4591
+ * 判掉了「新建目录默认信任」,今天所有目录一律未信任,裸路径什么都没丢)。
4592
+ *
4593
+ * ## 这道闸是**四件事的合取**,而其中第一件是一句推断
4594
+ *
4595
+ * {@link resolveNativeDirPicker} 那五行的形状照抄 deepseek-harness 的
4596
+ * `packages/host/directory-picker-auto/src/resolve.ts`(同一个形态:纯浏览器页 +
4597
+ * 本机 Node 服务端),判据也是同一条:**原生框弹在服务进程那台机器的屏幕上**,
4598
+ * 所以只有在「浏览器多半就在那台机器上」时它才是对的。
4599
+ *
4600
+ * ⚠️ **继承了它一个修不掉的盲区,如实记下**:`ssh -L` 端口转发时,请求从回环
4601
+ * 进来、`SSH_*` 又不在服务进程的环境里(服务是本机先起的),于是这道闸判成
4602
+ * 「能弹」——框弹在一台没人看着的机器上。harness 的 README 明写这一档
4603
+ * per-connection 适配也修不了,那种部署要直接钉死走自绘。我们这一侧的兜底是
4604
+ * 一样的:`epoch web` 起在一台会被远程访问的机器上时,用 `--host 0.0.0.0`
4605
+ * (那时这道闸自己就关了),别用回环 + 转发。
4606
+ *
4607
+ * ## ⚠️ 第二个盲区,这一个是 2026-09-01 真机量出来的
4608
+ *
4609
+ * **macOS 上,一个连不到 WindowServer 的进程调 `choose folder` 不会报错 ——
4610
+ * 它直接回那个进程的 cwd。** 现场(同一台机器,同一条命令):
4611
+ *
4612
+ * | 跑在哪儿 | `osascript … choose folder` 的结果 |
4613
+ * | ---------------------------- | ---------------------------------------------- |
4614
+ * | 普通终端会话 | 真弹框;取消回 `execution error: 用户已取消。 (-128)` |
4615
+ * | 被 seatbelt 关起来的子进程 | **1.5 秒后回一条路径**(就是 cwd),一个框都没有 |
4616
+ *
4617
+ * 也就是说那一档下用户会**拿到一个他从没挑过的目录**,而屏幕上没有任何东西
4618
+ * 说过这件事。**这道闸拦不住它**:那台机器上 `launchctl managername` 照样是
4619
+ * `Aqua`(实测),SSH 那两格也是空的 —— 会话元数据看着完全正常,被挡掉的是
4620
+ * 真正那次连接。
4621
+ *
4622
+ * **没有为它加探测**,判据是「加一个查不出这一档的探测,比不加更坏」:
4623
+ * 它会让下一个人以为这条缝已经堵上了。今天的形态是**如实记在这儿**,
4624
+ * 而现实里踩得到它的是「epoch web 被某个沙箱化的父进程拉起来」这一种 ——
4625
+ * 用户自己在终端里敲 `epoch web` 不在其中。
4626
+ *
4627
+ * 📮 真要堵它,能想到的唯一可靠做法是**让框自己说话**:给 `choose folder` 一个
4628
+ * `default location`(比如 home),那时「回的是 cwd」就成了一个可判的信号 ——
4629
+ * 但它同时也把「系统记住的上次位置」这件事顶掉了,那是原生框最值钱的东西之一。
4630
+ * 谁要收这张 📮,先量一遍那两件哪个更贵。
4631
+ *
4632
+ * ## 平台只写两个,而这不是「先做一半」
4633
+ *
4634
+ * AGENTS.md「支持平台」那一节只有 Windows 和 macOS。Linux 分支写了也没人验
4635
+ * (harness 那边是 `zenity → kdialog` 两级降级),所以这里**一个字都不写** ——
4636
+ * 那台机器上这道闸判成假,界面回落自绘那一屏,而那一屏在任何机器上都能用。
4637
+ * 这是「不做」,不是「没做完」:加 Linux 的人要连着加它的探测(`DISPLAY` /
4638
+ * `WAYLAND_DISPLAY` + PATH 上有没有那两个二进制),别只加一条命令。
4639
+ *
4640
+ * ## 三条实现上的规矩
4641
+ *
4642
+ * 1. **`execFile` 不过 shell**,全仓同一条(`cli/src/host-actions.ts` /
4643
+ * `workspace/git.ts`)。这一条在这儿尤其硬:Windows 那一支是一段脚本文本;
4644
+ * 2. **不设 timeout。** `git.ts` 那个 10 秒在这儿是错的 —— 一个对话框本来就可以
4645
+ * 开五分钟。取而代之的是 {@link PickOptions.signal}:HTTP 请求断开(用户关了
4646
+ * 标签页)就杀掉那个子进程,否则那个框会永远留在别人屏幕上;
4647
+ * 3. **同一进程只许一个框**({@link pickNativeDirectory} 的单飞闸)。两个框叠在
4648
+ * 一台机器上,用户答的那一个未必是浏览器还等着的那一个。
4649
+ */
4650
+ /** 这台机器上能不能弹的四件事,全是**事实**不是结论 —— 判据在下面那个函数上 */
4651
+ interface NativePickerFacts {
4652
+ /** 服务绑在回环之外吗(`bind.ts` 的 `decideBinding().lanExposed`) */
4653
+ lanExposed: boolean;
4654
+ /** `process.platform` */
4655
+ platform: NodeJS.Platform;
4656
+ /** `process.env` 的一个子集 —— 只读这两格,见下 */
4657
+ env: Readonly<Partial<Record<'SSH_CONNECTION' | 'SSH_TTY', string>>>;
4658
+ }
4659
+ /**
4660
+ * 这个服务进程能不能替浏览器弹一个系统目录框。**纯函数,装配时算一次。**
4661
+ *
4662
+ * 四条缺一不可,每一条挡的是一种「框弹出来也没用 / 弹错地方」:
4663
+ *
4664
+ * | 条件 | 挡的是什么 |
4665
+ * | ------------------------ | --------------------------------------------------------- |
4666
+ * | 绑在回环上 | `--host 0.0.0.0` 那一档浏览器可能在别的机器上 |
4667
+ * | 没有 `SSH_CONNECTION` / `SSH_TTY` | 服务是被 ssh 进来的人起的 —— 框会弹在无人值守的服务器上 |
4668
+ * | 平台是 darwin / win32 | 别的平台这一份没有实现(见文件头「平台只写两个」) |
4669
+ *
4670
+ * ⚠️ **一律往「不能弹」那一侧落**:猜错成「能弹」的表现是用户点下去吃一个 403
4671
+ * 或者框弹在别人屏幕上,猜错成「不能弹」的表现只是他看到自绘那一屏 ——
4672
+ * 而那一屏在任何机器上都能用。
4673
+ */
4674
+ declare function resolveNativeDirPicker(facts: NativePickerFacts): boolean;
4675
+ /**
4676
+ * 「浏览器多半就在服务进程这台机器上」—— 上面那个判断的**正身**。
4677
+ *
4678
+ * 2026-09-02 抽出来,因为有了第二个问同一句话的地方:Web UI 的
4679
+ * 「用默认应用打开」(`workspace/file-view.ts`)。它和弹目录框是**同一个赌注**
4680
+ * —— `open` / `rundll32` 起的那个窗口和 `choose folder` 弹的那个框,
4681
+ * 出现在同一块屏幕上;那块屏幕前面没人的话,两者一样白搭。
4682
+ *
4683
+ * ⚠️ **两处共用一份,不许各写各的。** 三条判据里最容易被漏掉的是 SSH 那两格
4684
+ * (它问的是「这个服务是被谁起的」,不是「这一发请求从哪儿来」),
4685
+ * 而漏掉它的表现不是报错,是**一个窗口弹在无人值守的服务器上**。
4686
+ *
4687
+ * ⚠️ 它继承 `resolveNativeDirPicker` 文件头记的那两个盲区,一个字都没少:
4688
+ * `ssh -L` 端口转发这道闸判不出来;macOS 上被沙箱关起来的进程也判不出来。
4689
+ *
4690
+ * 📮 平台那一条对这一格其实**偏严**了:`xdg-open` 在 Linux 上是有的,
4691
+ * 而目录框那一侧没有 Linux 实现。今天故意不放宽 —— AGENTS.md 的支持平台只有
4692
+ * Windows / macOS,放宽等于多一条没人验的分支。要放宽的人先去改那一节。
4693
+ */
4694
+ declare function isSameMachine(facts: NativePickerFacts): boolean;
4695
+
4390
4696
  /**
4391
4697
  * 后台任务的取数与投影 —— `GET /api/sessions/:id/tasks`(方案 36 PR-2 的 Web 那半)。
4392
4698
  *
@@ -4728,4 +5034,4 @@ type CreateWebServerResult = {
4728
5034
  */
4729
5035
  declare function createWebServer(opts: CreateWebServerOptions): Promise<CreateWebServerResult>;
4730
5036
 
4731
- export { type ApiContext, type AttachedMentions, type AuthGuardOptions, type BindDecision, type BindRequest, type BoundWorkspace, type CatalogDeletion, type CatalogHit, type CatalogRow, type CheckpointSummaryView, type CommandsView, type CreateSessionResult, type CreateWebServerOptions, type CreateWebServerResult, DEFAULT_MAX_ACTIVE_SESSIONS, DEFAULT_MAX_QUEUED_MESSAGES, DEFAULT_RING_CAPACITY, DEFAULT_WEB_HOST, DEFAULT_WEB_PORT, EnvelopeRing, type EpochWebServer, type ExpansionOutcome, type FrameSink, type HubSession, type HubSessionState, type LiveSessionView, MAX_CANDIDATES, MAX_DIFF_BYTES, MAX_DIFF_FILES, MAX_RECORDING_FRAMES, MAX_SKILL_BODY_BYTES, type ModelTurnsView, type PluginControlView, type QueuedMessage, type RewindFilePlanView, type RewindOutcomeView, type RewindPreviewView, type RewindResultView, type RuntimeCapabilities, type RuntimeCheckpoints, type RuntimeCommands, type RuntimeSecurity, type RuntimeSettings, type ScheduleCapabilityView, type ScheduleControlView, type ScheduleCreateView, type ScheduleFireView, type ScheduleRegisterView, type ScheduleSaveView, type ScheduleUpdateView, type SelectionView, type SessionCatalog, type SessionContextFacts, type SessionFactoryView, type SessionHistoryFacts, SessionHub, type SessionHubOptions, type SessionLoopView, type SessionModelView, type SessionPermissionsView, type SessionPlanView, type SessionReferencesView, type SessionRoleView, type SetLevelView, type SetSelectionView, type StartResult, TASK_TAIL_BYTES, type TaskRegistryView, type TrustView, type TurnDecorator, UI_LANG_PARAM, UI_THEME_PARAM, type UiLangPref, type UiPreset, type UiThemePref, type WebRuntimeView, type WorkspaceBindFailure, type WorkspaceBindOutcome, type WorkspaceCheckpoints, type WorkspaceDiffOptions, type WorkspaceView, attachMentions, collectCapabilities, collectSecurity, collectSettings, collectTasks, collectWorkspaceDiff, createAuthGuard, createWebServer, decideBinding, defaultWebRoot, expandUserContent, firstScreenUrl, generateToken, listFileCandidates, listReferenceCandidates, readRewindInput, resolveArtifactPath, toWireCheckpoint, toWireCommand, toWirePluginEntry, toWirePreview, toWireRewindResult, toWireRow, toWireSkillBody, toWireTask, unavailableToHttp };
5037
+ export { type ApiContext, type AttachedMentions, type AuthGuardOptions, type BindDecision, type BindRequest, type BoundWorkspace, type CatalogDeletion, type CatalogHit, type CatalogRow, type CheckpointSummaryView, type CommandsView, type CreateSessionResult, type CreateWebServerOptions, type CreateWebServerResult, DEFAULT_MAX_ACTIVE_SESSIONS, DEFAULT_MAX_QUEUED_MESSAGES, DEFAULT_RING_CAPACITY, DEFAULT_WEB_HOST, DEFAULT_WEB_PORT, EnvelopeRing, type EpochWebServer, type ExpansionOutcome, type FrameSink, type HubSession, type HubSessionState, type LiveSessionView, MAX_CANDIDATES, MAX_DIFF_BYTES, MAX_DIFF_FILES, MAX_RECORDING_FRAMES, MAX_SKILL_BODY_BYTES, type ModelTurnsView, type NativePickerFacts, type PluginControlView, type QueuedMessage, type RewindFilePlanView, type RewindOutcomeView, type RewindPreviewView, type RewindResultView, type RuntimeCapabilities, type RuntimeCheckpoints, type RuntimeCommands, type RuntimeSecurity, type RuntimeSettings, type ScheduleCapabilityView, type ScheduleControlView, type ScheduleCreateView, type ScheduleFireView, type ScheduleRegisterView, type ScheduleSaveView, type ScheduleUpdateView, type SelectionView, type SessionCatalog, type SessionContextFacts, type SessionFactoryView, type SessionHistoryFacts, SessionHub, type SessionHubOptions, type SessionLoopView, type SessionModelView, type SessionPermissionsView, type SessionPlanView, type SessionReferencesView, type SessionRoleView, type SetLevelView, type SetSelectionView, type StartResult, TASK_TAIL_BYTES, type TaskRegistryView, type TrustView, type TurnDecorator, UI_LANG_PARAM, UI_THEME_PARAM, type UiLangPref, type UiPreset, type UiThemePref, type WebRuntimeView, type WorkspaceBindFailure, type WorkspaceBindOutcome, type WorkspaceCheckpoints, type WorkspaceDiffOptions, type WorkspaceView, attachMentions, collectCapabilities, collectSecurity, collectSettings, collectTasks, collectWorkspaceDiff, createAuthGuard, createWebServer, decideBinding, defaultWebRoot, expandUserContent, firstScreenUrl, generateToken, isSameMachine, listFileCandidates, listReferenceCandidates, readRewindInput, resolveArtifactPath, resolveNativeDirPicker, toWireCheckpoint, toWireCommand, toWirePluginEntry, toWirePreview, toWireRewindResult, toWireRow, toWireSkillBody, toWireTask, unavailableToHttp };