@epoch-agent/server 0.13.0 → 0.14.0

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
@@ -2709,39 +2709,31 @@ interface SessionLoopView {
2709
2709
  *
2710
2710
  * 另一项(`usedTokens`)反过来**只有浏览器有**,见 {@link readModelSetBody}。
2711
2711
  *
2712
- * ## ✅ 2026-08-16 合并时收掉了(原文是一张 📮,留着当判据)
2712
+ * ## ⚠️ 斜杠命令的 `model:` 必须按会话取,**别让它再借进程那一份**
2713
2713
  *
2714
- * 自定义斜杠命令 frontmatter 的 `model:`(「这一条命令只换这一轮」)走的是
2715
- * [commands.ts](./commands.ts) 的 `scopeTurn()`,它读的**曾经**是 `runtime.model`
2716
- * 那一格 —— 也就是引导会话那一份。模型选择这一轮变成会话级之后,那条路在
2717
- * 第二个会话上的表现是:
2714
+ * 自定义斜杠命令 frontmatter 的 `model:`(「这一条命令只换这一轮」)走
2715
+ * [commands.ts](./commands.ts) 的 `scopeTurn()`。那一格的真源是
2716
+ * `ctx.runtime.sessionFactory.get(sessionId)?.model`,由调用方递进来
2717
+ * (形状照 `checkpointsOf(ctx, sessionId)`)—— **`commands.ts` 上没有、也别再加回
2718
+ * 一个 `model: ModelTurnsView | null`**:那种借法借到的恒是引导会话那一份。
2718
2719
  *
2719
- * - 那条命令的 `model:` **对这个会话一点作用都没有**(B 的循环读的是 B 自己
2720
+ * 借错了的症状有两半,第二半更糟:
2721
+ *
2722
+ * - 那条命令的 `model:` **对当前这个会话一点作用都没有**(B 的循环读的是 B 自己
2720
2723
  * 那个 router,而 `enterTurn` 换的是引导会话那个);
2721
- * - 更糟的一半:它**顺带把引导会话的选择换掉了一轮**。A 正好在跑的话,
2722
- * A 那一轮会用上 B 那条命令点名的模型。
2723
- *
2724
- * ⚠️ **这不是这一轮引入的一个新 bug,是一个旧 bug 换了形状**:在会话级之前,
2725
- * B 那条命令换的是**所有人共用**的那个选择,A 同样会被拖下水。两版都错,
2726
- * 而这一版多了「对 B 无效」这一半。如实记着,别当成没动过。
2727
- *
2728
- * **写这张 📮 的那一轮不改**:`commands.ts` / `api.ts` 不归它,而正确的改法要动
2729
- * 它们两个([方案 30 §十二](../../../docs/verify/VERIFY_RECORD-30-web-multi-session.md)
2730
- * 的第 3 条:不绕过去、不伸手改、就地写下来)。合并的那一轮照着三步收完了:
2731
- *
2732
- * 1. `RuntimeCommandModel`(`commands.ts` 上那个 `model: ModelTurnsView | null`)
2733
- * **整个删掉**了 —— 它借的恒是进程那一份。`expandUserContent()` /
2734
- * `scopeTurn()` 改成从调用方收这一格,形状照 `checkpointsOf(ctx, sessionId)`;
2735
- * 2. 真源本来就在手边:`ctx.runtime.sessionFactory.get(sessionId)?.model`。
2736
- * **runtime 一个字没加** —— `ModelControl` 上五个动词一直都全,
2737
- * 只差 server 这一跳;
2738
- * 3. `enterTurn` 加进了 `sessions/scope.ts` 那份镜像。原文说这一步「有意留给
2739
- * 收的人做」,因为加它等于承认「这条路上有一个和 `run()` 成对的还原动作」——
2740
- * 那句话由 `commands.ts` 说了,判据抄在 `SessionModelView.enterTurn` 上。
2741
- *
2742
- * ⚠️ 一处**没有**跟着变的:取不到会话(冷却中、或 `sessionFactory` 里没有)时
2724
+ * - 它**顺带把引导会话的选择换掉了一轮** —— A 正好在跑的话,A 那一轮会用上
2725
+ * B 那条命令点名的模型。
2726
+ *
2727
+ * ⚠️ 这在会话级之前是**同一个 bug 的另一个形状**:那时 B 那条命令换的是所有人
2728
+ * 共用的那个选择,A 同样被拖下水。两版都错,如实记着,别当成没动过。
2729
+ *
2730
+ * ⚠️ 一处**刻意不跟着变**:取不到会话(冷却中、或 `sessionFactory` 里没有)时
2743
2731
  * 传 null,`withModelScope` 对 null 是零开销直通 —— 命令正文照跑,只有 `model:`
2744
2732
  * 那一半不生效。**和 provider 没配起来时同一档**,判据在 `api.ts` 那一处。
2733
+ *
2734
+ * ⚠️ `enterTurn` 在 `sessions/scope.ts` 那份镜像里是有意加的:加它等于承认
2735
+ * 「这条路上有一个和 `run()` 成对的还原动作」,那句话由 `commands.ts` 说,
2736
+ * 判据抄在 `SessionModelView.enterTurn` 上。
2745
2737
  */
2746
2738
 
2747
2739
  /**
@@ -3385,6 +3377,8 @@ interface ScheduleCreateView {
3385
3377
  /** 不给就落到 `~/.epoch/automation/<id>/`,**不是**服务进程的 cwd */
3386
3378
  workDir?: string;
3387
3379
  model?: string;
3380
+ /** 「召唤专家」——这条任务用哪个身份跑。见 `WireScheduleCreateRequest.role` */
3381
+ role?: string;
3388
3382
  permission: PermissionLevel;
3389
3383
  allowTools?: readonly string[];
3390
3384
  allowOperations?: readonly OperationType[];
@@ -3397,11 +3391,16 @@ interface ScheduleCreateView {
3397
3391
  startDate?: string;
3398
3392
  endDate?: string;
3399
3393
  enabled?: boolean;
3394
+ /**
3395
+ * 跑完的会话直接进会话列表吗。不给 = 收进归档,判据全文在
3396
+ * `ScheduleDefinition.showInSessionList` 上。
3397
+ */
3398
+ showInSessionList?: boolean;
3400
3399
  }
3401
3400
  /**
3402
3401
  * 改一条任务 —— core 的 `UpdateScheduleInput`。没给的字段一个都不动。
3403
3402
  *
3404
- * ⚠️ **四格收 `null` = 清空这一格**,和网线上那份(`WireScheduleUpdateRequest`)
3403
+ * ⚠️ **五格收 `null` = 清空这一格**,和网线上那份(`WireScheduleUpdateRequest`)
3405
3404
  * 逐字段对得上。别写成 `Partial<ScheduleCreateView>`(2026-09-07 之前就是那样):
3406
3405
  * 那一版比真身**窄**,而窄的方向 TS 不报 —— 入参是逆变的,一个「什么都收得下」
3407
3406
  * 的真身赋给一个「只收 string」的镜像永远合法。于是这一层压根表达不出「清空」,
@@ -3411,10 +3410,12 @@ interface ScheduleCreateView {
3411
3410
  * 也就是说**这一格的正确性没有编译器守着**,只有这段注释和用例。判据全文在
3412
3411
  * core 的 `UpdateScheduleInput` 上。
3413
3412
  */
3414
- type ScheduleUpdateView = Partial<Omit<ScheduleCreateView, 'workDir' | 'model' | 'startDate' | 'endDate'>> & {
3413
+ type ScheduleUpdateView = Partial<Omit<ScheduleCreateView, 'workDir' | 'model' | 'role' | 'startDate' | 'endDate'>> & {
3415
3414
  /** `null` = 不绑工作区了 → 落回 `~/.epoch/automation/<id>/`(runtime 那一层解析) */
3416
3415
  workDir?: string | null;
3417
3416
  model?: string | null;
3417
+ /** `null` = 回到默认 agent */
3418
+ role?: string | null;
3418
3419
  startDate?: string | null;
3419
3420
  endDate?: string | null;
3420
3421
  };
@@ -3736,68 +3737,42 @@ interface WorkspaceView {
3736
3737
  * 是 `<root>/.epoch/policies`),**那时这里要改**:`runtime.policy` 得按
3737
3738
  * `workspaces.of(sessionId)` 重算,而不是读装配时那一份。
3738
3739
  *
3739
- * ## ✅ 收掉了(2026-08-17):`permission.level` 这一格曾经答的是**引导会话**
3740
+ * ## ⚠️ `permission.level` 必须按会话取 —— **别动 `session` 那个参数**
3741
+ *
3742
+ * 这一格今天对了,靠的是 [api.ts](./api.ts) 的 `getSecurity()` 递进来的那一个实参
3743
+ * (第 4 个):`ctx.runtime.sessionFactory.get(sessionId)?.permissions ?? null`。
3740
3744
  *
3741
- * 下面整节是那张 📮 的原文,**一个字没改**。它留着不是纪念品:这一格今天对了
3742
- * 靠的是 `api.ts` 里那**一个实参**,而那一行看上去像是可有可无的第四个参数。
3743
- * 哪天有人为了「少传一个东西」把它删回三参,症状就是下面写的那些 ——
3744
- * 印错一个词、外加一张在最该发黄的时候是绿的卡。**判据比修法长,是因为
3745
- * 这类 bug 编译得过、跑得通、每一条用例都绿。**
3745
+ * 它看上去像是可有可无的一个。**删掉它,症状是下面这些 —— 而它编译得过、
3746
+ * 跑得通、每一条用例都绿**,所以判据写得比修法长:
3746
3747
  *
3747
- * {@link collectSecurity} 里那句 `perms.level()` 读的是 {@link PermissionsView},
3748
- * 也就是 `WebRuntimeView.permissions`。runtime 的 `buildPermissionsControl()`
3749
- * 把 `level` / `setLevel` 两格**整个转发给引导会话那一份**(那边逐字是
3750
- * `level: () => bootstrap?.level() ?? services.config.permission`)。
3748
+ * 少了它,{@link collectSecurity} 里那句 `perms.level()` 读的是
3749
+ * {@link PermissionsView}(`WebRuntimeView.permissions`),而 runtime 的
3750
+ * `buildPermissionsControl()` 把 `level` / `setLevel` 两格**整个转发给引导会话
3751
+ * 那一份**(逐字是 `level: () => bootstrap?.level() ?? services.config.permission`)。
3751
3752
  * 于是浏览器看着会话 B 时,安全中心那一格印的是会话 A 那一档。
3752
3753
  *
3753
3754
  * 这和 2026-08-15 治掉的 `config.workspace` 是同一类病(方案 30 §10.4 第 1 条):
3754
3755
  * **端点按会话取,载荷里那一格却是进程级的。** 契约那边早把它说死了 ——
3755
3756
  * `WirePermissionState.level` 的 JSDoc 逐字写着「安全中心那一格和底栏这一格印的
3756
- * **必须是同一个名词**」,而底栏那一格从 2026-08-16 起按会话取
3757
- * ([permission.ts](./permission.js) 的 `permissionState()`)。今天这两格能不一样。
3757
+ * **必须是同一个名词**」,而底栏那一格按会话取
3758
+ * ([permission.ts](./permission.js) 的 `permissionState()`)。
3758
3759
  *
3759
3760
  * ⚠️ **代价不止是印错一个词。** 那一屏顶上四张状态卡的第一条纪律是「取最差的
3760
3761
  * 那一处」,而档位那张卡的色带就照这个值算:会话 B 在 `bypass` 上、引导会话在
3761
3762
  * `default` 上时,**那张卡是绿的**。一屏「我现在安不安全」的界面,在最该发黄的
3762
3763
  * 那一刻答了别人的状态。
3763
3764
  *
3764
- * **缺的那一段只有一处,而它在 [api.ts](./api.ts) 里,那个文件这一轮不归我改**
3765
- * (✅ 这一段就是收掉的那一笔,`getSecurity()` 里现在逐字是这个 diff):
3766
- *
3767
- * ```diff
3768
- * // api.ts / getSecurity()
3769
- * const payload = collectSecurity(
3770
- * ctx.runtime,
3771
- * ctx.runtime.workspaces,
3772
- * ctx.runtime.workspaces.of(sessionId),
3773
- * + ctx.runtime.sessionFactory.get(sessionId)?.permissions ?? null,
3774
- * );
3775
- * ```
3776
- *
3777
- * 这一层跟着收第四个参数
3778
- * `session: Pick<SessionPermissionsView, 'level'> | null`(类型在
3779
- * [sessions/scope.ts](./sessions/scope.js),**那一层已经镜像好了,不用动
3780
- * core / runtime**),投影里 `level` 改成 `session?.level() ?? perms.level()` ——
3781
- * 这个进程没在跑那段会话(只剩历史 / 被冷却)时退回进程那一份,那仍然是一句
3782
- * 真话,判据逐字同 `permissionState()` 那条兜底。
3765
+ * ⚠️ **这个参数是必填的,不是可选的**,`null` 由调用方显式写出来。回落发生在投影里
3766
+ * (`session?.level() ?? perms.level()` —— 这个进程没在跑那段会话时退回进程那一份,
3767
+ * 那仍然是一句真话,判据同 `permissionState()` 那条兜底),**而不是发生在
3768
+ * 「忘了传」这件事上**。区别不在行为上(都退回进程那一份),在于**下一个新加
3769
+ * 调用点的人会不会被编译器拦住**:可选参数不拦,必填参数拦。类型在
3770
+ * [sessions/scope.ts](./sessions/scope.js),core / runtime 不用动。
3783
3771
  *
3784
3772
  * ⚠️ **`rules` / `shadows` / `managed` / `audit` / `cached` 五样一个都不许跟着搬。**
3785
3773
  * 判据在 core 的 `permission/shared.ts`:它们**没有**跟着会话分家。搬过去的
3786
3774
  * 具体代价是这一屏只剩下这一个会话判过的那几行 ——
3787
3775
  * **一本漏了行的安全流水比没有账更坏,因为它看起来是完整的。**
3788
- *
3789
- * ⚠️ 这一轮**刻意没有**顺手加一个「不传就回落到进程那一份」的可选第四参。
3790
- * 那个形状编译得过、跑得通、只是答错会话,正是 `context.ts` 删掉进程级
3791
- * `checkpoints` 那一段点名的病;而且半接上的参数会让下一个人以为这条已经收了。
3792
- * 要么四个实参一起到位,要么这一格照旧是一处**写在文件头上的**缺陷。
3793
- *
3794
- * > ✅ **收的时候按这句办了:第四参是必填的,不是可选的。** 唯一的调用点
3795
- * > (`api.ts` 的 `getSecurity()`)传的是
3796
- * > `sessionFactory.get(sessionId)?.permissions ?? null` —— **`null` 是显式的**,
3797
- * > 由调用方写出来,不是省略出来的。回落发生在投影里
3798
- * > (`session?.level() ?? perms.level()`),而不是发生在「忘了传」这件事上。
3799
- * > 这两者的区别不在行为上(都退回进程那一份),在于**下一个新加调用点的人
3800
- * > 会不会被编译器拦住**:可选参数不拦,必填参数拦。
3801
3776
  */
3802
3777
 
3803
3778
  /**
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createServer } from 'http';
2
- import { createLogger, t, ApprovalRelay, QuestionRelay, serializeStream, withRoleScope, withModelScope, withToolScope, nextRunAt, observeBackgroundTasks, skillIndexDescription, SANDBOX_EXCLUDES, SANDBOX_COVERS, isSystemOpenable, backgroundTaskOutput, listBackgroundTasks, providerLabel, MAX_REASON_CHARS, MAX_OBJECTIVE_CHARS, MAX_GOAL_MAX_ROUNDS, DEFAULT_GOAL_MAX_ROUNDS, SCHEDULE_DEFAULT_ALLOWLIST, SCHEDULE_DEFAULTS, coalesceRecording, readRecording } from '@epoch-agent/runtime';
2
+ import { createLogger, t, ApprovalRelay, QuestionRelay, serializeStream, withRoleScope, withModelScope, withToolScope, nextRunAt, observeBackgroundTasks, skillIndexDescription, SANDBOX_EXCLUDES, SANDBOX_COVERS, guardSpawn, isSystemOpenable, backgroundTaskOutput, listBackgroundTasks, providerLabel, MAX_REASON_CHARS, MAX_OBJECTIVE_CHARS, MAX_GOAL_MAX_ROUNDS, DEFAULT_GOAL_MAX_ROUNDS, SCHEDULE_DEFAULT_ALLOWLIST, SCHEDULE_FORM_BUDGET_USD, SCHEDULE_DEFAULTS, coalesceRecording, readRecording } from '@epoch-agent/runtime';
3
3
  import { timingSafeEqual, randomBytes } from 'crypto';
4
4
  import { PLAN_OUTCOMES, WIRE_AUTH_COOKIE, WIRE_AUTH_COOKIE_ATTRS, WIRE_AUTH_TOKEN_PARAM, extractAllMentions, MAX_ATTACH_FILES, utf8ByteLength, MAX_SESSION_REFS, attachSessionSurface, isWireRewindScope, WIRE_REWIND_SCOPES, mcpConnectorState, MAX_ATTACH_TOTAL_BYTES, MAX_ATTACH_BYTES, truncateToBudget, WIRE_LANG_PARAM, isLang, conformsToWire, WIRE_LAST_SESSION_FIELDS, isQuestionAnswerMap, isProviderType, apiKeyEnvVar, WIRE_FILE_STAT_MAX, WIRE_SETTING_WRITE_LAYERS, isWirePlanAction, WIRE_PLAN_ACTIONS, isPermissionLevel, PERMISSION_LEVELS, WIRE_TRUST_REQUEST_FIELDS, WIRE_FORGET_WORKSPACE_FIELDS, collectPendingApprovals, unfixedRules, isWireSettingWriteLayer, parseModelRef, WIRE_GOAL_ACTIONS, OPERATION_TYPES, isOperationType, WIRE_PLUGIN_UPLOAD_FORM_FIELDS } from '@epoch-agent/protocol';
5
5
  import { resolve, join, relative, isAbsolute, sep, dirname, basename, extname } from 'path';
@@ -1074,6 +1074,11 @@ function toWireSkillBody(name, body) {
1074
1074
  const safe = Buffer.byteLength(cut, "utf-8") > MAX_SKILL_BODY_BYTES ? cut.slice(0, -1) : cut;
1075
1075
  return { name, body: safe, truncated: true, bytes };
1076
1076
  }
1077
+ function getRoles(ctx, res) {
1078
+ sendJson(res, 200, {
1079
+ roles: ctx.runtime.agentRoles.map((r) => toWireRole(r, ctx.runtime.agentRoleNotices))
1080
+ });
1081
+ }
1077
1082
  function getSkillBody(ctx, res, sessionId, name, lang) {
1078
1083
  if (!ctx.hub.has(sessionId) && !ctx.runtime.sessions?.get(sessionId)) {
1079
1084
  return sendUnknownSession(res, sessionId);
@@ -3334,7 +3339,7 @@ function numberList(value) {
3334
3339
  }
3335
3340
  return out;
3336
3341
  }
3337
- var CLEARABLE = ["model", "workDir", "startDate", "endDate"];
3342
+ var CLEARABLE = ["model", "role", "workDir", "startDate", "endDate"];
3338
3343
  function optionals(raw, allowClear, lang) {
3339
3344
  const out = {};
3340
3345
  const bad = (field) => ({
@@ -3384,11 +3389,13 @@ function optionals(raw, allowClear, lang) {
3384
3389
  }
3385
3390
  out.allowOperations = ops;
3386
3391
  }
3387
- if (raw["enabled"] !== void 0) {
3388
- if (typeof raw["enabled"] !== "boolean") {
3389
- return { ok: false, message: t("web.schedule_bad_field", { field: "enabled" }, lang) };
3392
+ for (const key of ["enabled", "showInSessionList"]) {
3393
+ const value = raw[key];
3394
+ if (value === void 0) continue;
3395
+ if (typeof value !== "boolean") {
3396
+ return { ok: false, message: t("web.schedule_bad_field", { field: key }, lang) };
3390
3397
  }
3391
- out.enabled = raw["enabled"];
3398
+ out[key] = value;
3392
3399
  }
3393
3400
  if (raw["permission"] !== void 0) {
3394
3401
  const level = raw["permission"];
@@ -3465,6 +3472,7 @@ function defaultsOf(ctx) {
3465
3472
  return {
3466
3473
  maxTurns: ctx.runtime.config.maxTurns,
3467
3474
  timeoutMs: SCHEDULE_DEFAULTS.timeoutMs,
3475
+ maxBudgetUsd: SCHEDULE_FORM_BUDGET_USD,
3468
3476
  allowTools: SCHEDULE_DEFAULT_ALLOWLIST.allowTools,
3469
3477
  allowOperations: SCHEDULE_DEFAULT_ALLOWLIST.allowOperations,
3470
3478
  allowRules: SCHEDULE_DEFAULT_ALLOWLIST.allowRules
@@ -4078,25 +4086,34 @@ async function pickNativeDirectory(opts) {
4078
4086
  }
4079
4087
  }
4080
4088
  var execFileRunner = (command, args, signal) => new Promise((done) => {
4081
- execFile(
4082
- command,
4083
- [...args],
4084
- {
4085
- ...signal ? { signal } : {},
4086
- encoding: "utf-8",
4087
- windowsHide: true,
4088
- maxBuffer: 1024 * 1024
4089
- },
4090
- (error, stdout, stderr) => {
4091
- const err = error;
4092
- done({
4093
- code: err?.code ?? 0,
4094
- stdout,
4095
- stderr,
4096
- missing: err?.code === "ENOENT",
4097
- message: err?.message ?? ""
4098
- });
4099
- }
4089
+ guardSpawn(
4090
+ () => execFile(
4091
+ command,
4092
+ [...args],
4093
+ {
4094
+ ...signal ? { signal } : {},
4095
+ encoding: "utf-8",
4096
+ windowsHide: true,
4097
+ maxBuffer: 1024 * 1024
4098
+ },
4099
+ (error, stdout, stderr) => {
4100
+ const err = error;
4101
+ done({
4102
+ code: err?.code ?? 0,
4103
+ stdout,
4104
+ stderr,
4105
+ missing: err?.code === "ENOENT",
4106
+ message: err?.message ?? ""
4107
+ });
4108
+ }
4109
+ ),
4110
+ (err) => done({
4111
+ code: err.code ?? 1,
4112
+ stdout: "",
4113
+ stderr: "",
4114
+ missing: err.code === "ENOENT" || err.code === "ENOTDIR",
4115
+ message: `${err.message} (command=${command})`
4116
+ })
4100
4117
  );
4101
4118
  });
4102
4119
  // src/workspace/dirs.ts
@@ -4427,14 +4444,17 @@ var GIT_TIMEOUT_MS = 1e4;
4427
4444
  var GIT_MAX_BUFFER = 16 * 1024 * 1024;
4428
4445
  function runGit(cwd, args) {
4429
4446
  return new Promise((resolve4) => {
4430
- execFile(
4431
- "git",
4432
- [...args],
4433
- { cwd, timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_BUFFER, encoding: "buffer" },
4434
- (err, stdout) => {
4435
- if (err) return resolve4({ ok: false, error: err.message });
4436
- resolve4({ ok: true, stdout });
4437
- }
4447
+ guardSpawn(
4448
+ () => execFile(
4449
+ "git",
4450
+ [...args],
4451
+ { cwd, timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_BUFFER, encoding: "buffer" },
4452
+ (err, stdout) => {
4453
+ if (err) return resolve4({ ok: false, error: err.message });
4454
+ resolve4({ ok: true, stdout });
4455
+ }
4456
+ ),
4457
+ (err) => resolve4({ ok: false, error: `${err.message} (cwd=${cwd})` })
4438
4458
  );
4439
4459
  });
4440
4460
  }
@@ -4911,6 +4931,9 @@ async function handleApi(opts, req, res, method, segments, lang) {
4911
4931
  if (resource === "mcp" && first === void 0 && method === "POST") {
4912
4932
  return addMcp(ctx, req, res, lang);
4913
4933
  }
4934
+ if (resource === "roles" && first === void 0 && isRead(method)) {
4935
+ return getRoles(ctx, res);
4936
+ }
4914
4937
  if (resource === "roles" && first === void 0 && method === "POST") {
4915
4938
  return addRole(ctx, req, res, lang);
4916
4939
  }