@epoch-agent/server 0.20.1 → 0.22.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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createServer } from 'http';
2
2
  import { createLogger, t, ApprovalRelay, QuestionRelay, serializeStream, withRoleScope, withModelScope, withToolScope, nextRunAt, observeBackgroundTasks, skillIndexDescription, SANDBOX_EXCLUDES, SANDBOX_COVERS, canProbeGit, 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, randomUUID } from 'crypto';
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';
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, isAppearanceTheme, isAppearanceLang, 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';
6
6
  import { existsSync, realpathSync, statSync, mkdirSync, createReadStream, readdirSync, createWriteStream, accessSync, constants } from 'fs';
7
7
  import { stat, readFile, rm } from 'fs/promises';
@@ -129,12 +129,28 @@ function generateToken() {
129
129
  }
130
130
  var UI_THEME_PARAM = "theme";
131
131
  var UI_LANG_PARAM = "lang";
132
+ function storedUiPreset(stored) {
133
+ const preset = {};
134
+ if (isAppearanceTheme(stored.theme)) preset.theme = stored.theme;
135
+ if (isAppearanceLang(stored.lang)) preset.lang = stored.lang;
136
+ return preset;
137
+ }
138
+ function resolveUiPreset(host, stored) {
139
+ return { ...host, ...storedUiPreset(stored) };
140
+ }
141
+ function uiPresetParams(ui) {
142
+ const params = [];
143
+ if (ui.theme) params.push([UI_THEME_PARAM, ui.theme]);
144
+ if (ui.lang) params.push([UI_LANG_PARAM, ui.lang]);
145
+ return params;
146
+ }
132
147
  function firstScreenUrl(host, port, token, ui = {}) {
133
148
  const shown = displayHost(host);
134
149
  const authority = shown.includes(":") && !shown.startsWith("[") ? `[${shown}]` : shown;
135
150
  const params = [`${WIRE_AUTH_TOKEN_PARAM}=${encodeURIComponent(token)}`];
136
- if (ui.theme) params.push(`${UI_THEME_PARAM}=${encodeURIComponent(ui.theme)}`);
137
- if (ui.lang) params.push(`${UI_LANG_PARAM}=${encodeURIComponent(ui.lang)}`);
151
+ for (const [name, value] of uiPresetParams(ui)) {
152
+ params.push(`${name}=${encodeURIComponent(value)}`);
153
+ }
138
154
  return `http://${authority}:${port}/?${params.join("&")}`;
139
155
  }
140
156
  var HOST_COMPOSE_TTL_MS = 10 * 6e4;
@@ -262,6 +278,7 @@ var SessionRegistry = class {
262
278
  turnStartedAt: null,
263
279
  lastFinish: null,
264
280
  persisted: null,
281
+ announcedTitle: null,
265
282
  queue: [],
266
283
  lastActiveTick: ++this.tick
267
284
  });
@@ -353,10 +370,13 @@ var EMPTY_INVENTORY = {
353
370
  policies: 0,
354
371
  settingsFiles: []
355
372
  };
356
- function toWireBinding(state, bound) {
357
- if (state === "bound" && bound)
358
- return { state: "bound", workspace: toWireSessionWorkspace(bound) };
359
- return { state: state === "bound" ? "unbound" : state };
373
+ function toWireBinding(state, bound, changeable) {
374
+ if (state === "bound") {
375
+ if (bound) return { state: "bound", workspace: toWireSessionWorkspace(bound), changeable };
376
+ return { state: "unbound" };
377
+ }
378
+ if (state === "none") return { state: "none", changeable };
379
+ return { state: "unbound" };
360
380
  }
361
381
  function toWireWorkspaceRef(bound) {
362
382
  if (!bound) return null;
@@ -442,6 +462,7 @@ function toWireSummary(id, hub, row, extra = {}) {
442
462
  lastFinish: state.lastFinish,
443
463
  role: extra.role ?? null,
444
464
  archived: row?.archived ?? false,
465
+ pinned: row?.pinned ?? false,
445
466
  ...extra.snippet === void 0 ? {} : { snippet: extra.snippet }
446
467
  };
447
468
  }
@@ -474,7 +495,9 @@ function listSessionSummaries(hubStates, catalog, query, workspaceOf = () => nul
474
495
  return hits;
475
496
  }
476
497
  const rows = catalog?.list(limit, archived ? { archived: true } : {}) ?? [];
477
- const summaries = rows.map(
498
+ const pinnedRows = archived ? [] : catalog?.list(limit, { pinnedOnly: true }) ?? [];
499
+ const listedIds = new Set(rows.map((r) => r.id));
500
+ const summaries = [...rows, ...pinnedRows.filter((r) => !listedIds.has(r.id))].map(
478
501
  (row) => toWireSummary(row.id, byId.get(row.id), row, {
479
502
  workspace: workspaceOf(row.id),
480
503
  role: roleOf(row.id)
@@ -487,6 +510,7 @@ function listSessionSummaries(hubStates, catalog, query, workspaceOf = () => nul
487
510
  return [...fresh, ...summaries].sort(compareSummaries).slice(0, limit);
488
511
  }
489
512
  function compareSummaries(a, b) {
513
+ if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
490
514
  if (a.live !== b.live) return a.live ? -1 : 1;
491
515
  const at = a.updatedAt ?? Number.POSITIVE_INFINITY;
492
516
  const bt = b.updatedAt ?? Number.POSITIVE_INFINITY;
@@ -499,6 +523,7 @@ var SessionHub = class {
499
523
  idleGraceMs;
500
524
  clock;
501
525
  onCooled;
526
+ readTitle;
502
527
  registry;
503
528
  sinks = /* @__PURE__ */ new Set();
504
529
  observers = /* @__PURE__ */ new Set();
@@ -512,6 +537,7 @@ var SessionHub = class {
512
537
  this.idleGraceMs = opts.idleGraceMs ?? 3e4;
513
538
  this.clock = opts.clock ?? Date.now;
514
539
  this.onCooled = opts.onCooled;
540
+ this.readTitle = opts.readTitle;
515
541
  this.registry = new SessionRegistry({
516
542
  ...opts.maxActiveSessions === void 0 ? {} : { maxActive: opts.maxActiveSessions },
517
543
  ...opts.maxQueuedMessages === void 0 ? {} : { maxQueued: opts.maxQueuedMessages }
@@ -661,6 +687,12 @@ var SessionHub = class {
661
687
  if (this.closed) return;
662
688
  this.publish(sessionId, { type: "sessions-changed" });
663
689
  }
690
+ notifySessionTitleChanged(sessionId, title) {
691
+ if (this.closed) return;
692
+ const entry = this.registry.get(sessionId);
693
+ if (entry) entry.announcedTitle = title;
694
+ this.publish(sessionId, { type: "session-title-changed", title });
695
+ }
664
696
  notifyPluginsChanged() {
665
697
  if (this.closed) return;
666
698
  this.publish(void 0, { type: "plugins-changed" });
@@ -732,6 +764,10 @@ var SessionHub = class {
732
764
  if (through !== null && through !== entry.persisted?.id) {
733
765
  entry.persisted = { id: through, seq: frame.seq };
734
766
  }
767
+ const title = this.readTitle?.(sessionId) ?? null;
768
+ if (title !== null && title !== entry.announcedTitle) {
769
+ this.notifySessionTitleChanged(sessionId, title);
770
+ }
735
771
  if (event.type === "finish") {
736
772
  entry.lastFinish = toWireFinish(event, frame.ts, entry.turnStartedAt);
737
773
  }
@@ -1747,6 +1783,7 @@ function config(ctx, res, lang) {
1747
1783
  model: runtime.config.model,
1748
1784
  permission: runtime.config.permission,
1749
1785
  workspace: bound ? toWireSessionWorkspace(bound) : null,
1786
+ ...runtime.defaultWorkDir !== void 0 ? { defaultWorkDir: workspaceRefOf(runtime.defaultWorkDir) } : {},
1750
1787
  knownWorkspaces: toWireKnownWorkspaces(runtime.workspaces.known()),
1751
1788
  usageScope: runtime.usageScope,
1752
1789
  compression: runtime.compression,
@@ -1834,9 +1871,11 @@ async function patchSession(ctx, req, res, sessionId, lang) {
1834
1871
  const fields = asRecord(body.value);
1835
1872
  const rawTitle = fields["title"];
1836
1873
  const rawArchived = fields["archived"];
1874
+ const rawPinned = fields["pinned"];
1837
1875
  const wantsTitle = rawTitle !== void 0;
1838
1876
  const wantsArchive = typeof rawArchived === "boolean";
1839
- if (!wantsTitle && !wantsArchive) {
1877
+ const wantsPinned = typeof rawPinned === "boolean";
1878
+ if (!wantsTitle && !wantsArchive && !wantsPinned) {
1840
1879
  return sendError(res, 400, "bad-patch", t("web.patch_needs_field", void 0, lang));
1841
1880
  }
1842
1881
  const title = typeof rawTitle === "string" ? rawTitle.trim() : "";
@@ -1852,10 +1891,15 @@ async function patchSession(ctx, req, res, sessionId, lang) {
1852
1891
  t("web.session_store_unavailable", void 0, lang)
1853
1892
  );
1854
1893
  }
1894
+ const titleBefore = wantsTitle ? catalog.get(sessionId)?.title ?? null : null;
1855
1895
  let row = wantsTitle ? catalog.rename(sessionId, title) : catalog.get(sessionId);
1856
1896
  const archivedBefore = row?.archived;
1857
1897
  if (row && wantsArchive) row = catalog.setArchived(sessionId, rawArchived);
1898
+ if (row && wantsPinned) row = catalog.setPinned(sessionId, rawPinned);
1858
1899
  if (!row) return sendUnknownSession(res, sessionId, lang);
1900
+ if (wantsTitle && row.title !== titleBefore) {
1901
+ ctx.hub.notifySessionTitleChanged(sessionId, row.title);
1902
+ }
1859
1903
  if (wantsArchive && archivedBefore !== row.archived) ctx.hub.notifySessionsChanged(sessionId);
1860
1904
  const hub = ctx.hub.listSessions().find((s) => s.id === sessionId);
1861
1905
  sendJson(res, 200, {
@@ -2098,6 +2142,37 @@ function parseAttachmentNames(raw) {
2098
2142
  if (!Array.isArray(raw)) return [];
2099
2143
  return raw.filter((name) => typeof name === "string" && name.length > 0);
2100
2144
  }
2145
+ async function postAppearance(ctx, req, res, lang) {
2146
+ const control = ctx.runtime.appearance;
2147
+ if (!control) {
2148
+ return sendError(res, 503, "no-appearance-store", t("web.appearance_unavailable", {}, lang));
2149
+ }
2150
+ const body = await readJsonBody(req, void 0, lang);
2151
+ if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
2152
+ const raw = body.value;
2153
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
2154
+ return sendError(res, 400, "bad-request", t("web.appearance_bad_request", {}, lang));
2155
+ }
2156
+ const record = raw;
2157
+ const rawTheme = record["theme"];
2158
+ const rawLang = record["lang"];
2159
+ const theme = isAppearanceTheme(rawTheme) ? rawTheme : void 0;
2160
+ const langPref = isAppearanceLang(rawLang) ? rawLang : void 0;
2161
+ if (rawTheme !== void 0 && theme === void 0 || rawLang !== void 0 && langPref === void 0) {
2162
+ return sendError(res, 400, "bad-request", t("web.appearance_bad_request", {}, lang));
2163
+ }
2164
+ const failure = control.set({
2165
+ ...theme === void 0 ? {} : { theme },
2166
+ ...langPref === void 0 ? {} : { lang: langPref }
2167
+ });
2168
+ if (failure) return sendError(res, 500, "write-failed", failure(lang));
2169
+ const stored = control.get();
2170
+ sendJson(res, 200, {
2171
+ ok: true,
2172
+ ...isAppearanceTheme(stored.theme) ? { theme: stored.theme } : {},
2173
+ ...isAppearanceLang(stored.lang) ? { lang: stored.lang } : {}
2174
+ });
2175
+ }
2101
2176
  async function revokeApprovalCache(ctx, req, res, sessionId, lang) {
2102
2177
  if (!ctx.hub.has(sessionId)) return sendUnknownSession(res, sessionId, lang);
2103
2178
  const perms = ctx.runtime.permissions;
@@ -2756,14 +2831,15 @@ function sessionWorkspace(ctx, res, sessionId, lang) {
2756
2831
  return sendUnknownSession(res, sessionId);
2757
2832
  }
2758
2833
  const state = ctx.runtime.workspaces.stateOf(sessionId);
2834
+ const changeable = ctx.runtime.workspaces.canChange(sessionId);
2759
2835
  if (state !== "none") {
2760
2836
  return sendJson(res, 200, {
2761
- binding: toWireBinding(state, ctx.runtime.workspaces.of(sessionId))
2837
+ binding: toWireBinding(state, ctx.runtime.workspaces.of(sessionId), changeable)
2762
2838
  });
2763
2839
  }
2764
2840
  if (ctx.hub.has(sessionId) || ctx.hub.isCooled(sessionId)) {
2765
2841
  return sendJson(res, 200, {
2766
- binding: { state: "none" }
2842
+ binding: { state: "none", changeable }
2767
2843
  });
2768
2844
  }
2769
2845
  sendError(res, 409, "not-live-session", t("web.workspace_not_live", void 0, lang));
@@ -2777,7 +2853,7 @@ async function bindWorkspace(ctx, req, res, sessionId, lang) {
2777
2853
  const parsed = readBindWorkspaceBody(asRecord(body.value), lang);
2778
2854
  if (!parsed.ok) return sendError(res, 400, "bad-workspace", parsed.message);
2779
2855
  const state = ctx.runtime.workspaces.stateOf(sessionId);
2780
- if (state !== "unbound") {
2856
+ if (state !== "unbound" && !ctx.runtime.workspaces.canChange(sessionId)) {
2781
2857
  return sendError(
2782
2858
  res,
2783
2859
  409,
@@ -2793,8 +2869,9 @@ async function bindWorkspace(ctx, req, res, sessionId, lang) {
2793
2869
  }
2794
2870
  if (parsed.kind === "none") {
2795
2871
  ctx.runtime.workspaces.decideNone(sessionId);
2872
+ if (state !== "none") ctx.hub.notifySessionsChanged(sessionId);
2796
2873
  return sendJson(res, 200, {
2797
- binding: { state: "none" }
2874
+ binding: { state: "none", changeable: ctx.runtime.workspaces.canChange(sessionId) }
2798
2875
  });
2799
2876
  }
2800
2877
  const outcome = ctx.runtime.workspaces.bind(sessionId, parsed.root, lang);
@@ -2802,8 +2879,13 @@ async function bindWorkspace(ctx, req, res, sessionId, lang) {
2802
2879
  const mapped = bindFailureToHttp(outcome, lang);
2803
2880
  return sendError(res, mapped.status, mapped.code, mapped.message);
2804
2881
  }
2882
+ if (outcome.changed) ctx.hub.notifySessionsChanged(sessionId);
2805
2883
  sendJson(res, 200, {
2806
- binding: { state: "bound", workspace: toWireSessionWorkspace(outcome.bound) }
2884
+ binding: {
2885
+ state: "bound",
2886
+ workspace: toWireSessionWorkspace(outcome.bound),
2887
+ changeable: ctx.runtime.workspaces.canChange(sessionId)
2888
+ }
2807
2889
  });
2808
2890
  }
2809
2891
  async function sessionDiff(ctx, res, sessionId, _lang) {
@@ -3344,7 +3426,30 @@ function toWirePreview2(preview) {
3344
3426
  mcpServers: [...inventory.mcpServers],
3345
3427
  ignoredBuckets: [...inventory.ignoredBuckets],
3346
3428
  hasJsTools: inventory.jsTools,
3347
- conflict: preview.conflict ? { name: preview.conflict.name, version: preview.conflict.version } : null
3429
+ conflict: preview.conflict ? { name: preview.conflict.name, version: preview.conflict.version } : null,
3430
+ dependencies: (preview.dependencies ?? []).map(toWireDependency)
3431
+ };
3432
+ }
3433
+ function toWireDependency(dep) {
3434
+ const inventory = dep.inventory;
3435
+ return {
3436
+ name: dep.name,
3437
+ status: dep.status,
3438
+ requiredBy: dep.requiredBy,
3439
+ source: dep.source ?? null,
3440
+ reason: dep.reason ?? null,
3441
+ version: dep.manifest?.version ?? null,
3442
+ versionUnmet: dep.versionUnmet ?? null,
3443
+ inventory: inventory ? {
3444
+ commands: [...inventory.commands],
3445
+ roles: [...inventory.roles],
3446
+ skills: [...inventory.skills],
3447
+ hooks: inventory.hooks.map((h) => ({ type: h.type, count: h.count })),
3448
+ denyRules: inventory.denyRules,
3449
+ mcpServers: [...inventory.mcpServers],
3450
+ ignoredBuckets: [...inventory.ignoredBuckets],
3451
+ hasJsTools: inventory.jsTools
3452
+ } : null
3348
3453
  };
3349
3454
  }
3350
3455
  function controlOf(ctx, res, lang) {
@@ -5072,7 +5177,7 @@ async function dispatch(opts, req, res, lang) {
5072
5177
  }
5073
5178
  if (verdict.setCookie) res.setHeader("Set-Cookie", verdict.setCookie);
5074
5179
  if (verdict.setCookie && isRead(method) && !pathname.startsWith("/api/")) {
5075
- return redirectWithoutToken(res, req.url);
5180
+ return redirectWithoutToken(res, req.url, ctx.runtime.appearance);
5076
5181
  }
5077
5182
  const segments = splitPath(pathname);
5078
5183
  if (segments[0] === "api") return handleApi(opts, req, res, method, segments, lang);
@@ -5101,6 +5206,9 @@ async function handleApi(opts, req, res, method, segments, lang) {
5101
5206
  if (resource === "last-session" && first === void 0 && method === "PUT") {
5102
5207
  return putLastSession(ctx, req, res, lang);
5103
5208
  }
5209
+ if (resource === "appearance" && first === void 0 && method === "POST") {
5210
+ return postAppearance(ctx, req, res, lang);
5211
+ }
5104
5212
  if (resource === "host" && first === "compose" && second === void 0 && method === "POST") {
5105
5213
  return postHostCompose(ctx, req, res, lang);
5106
5214
  }
@@ -5413,9 +5521,12 @@ function archivedFlag(rawUrl) {
5413
5521
  const raw = new URL(rawUrl ?? "/", "http://127.0.0.1").searchParams.get("archived");
5414
5522
  return raw === "1" || raw === "true";
5415
5523
  }
5416
- function redirectWithoutToken(res, rawUrl) {
5524
+ function redirectWithoutToken(res, rawUrl, store) {
5417
5525
  const url = new URL(rawUrl ?? "/", "http://127.0.0.1");
5418
5526
  url.searchParams.delete(WIRE_AUTH_TOKEN_PARAM);
5527
+ for (const [name, value] of uiPresetParams(storedUiPreset(store?.get() ?? {}))) {
5528
+ url.searchParams.set(name, value);
5529
+ }
5419
5530
  res.writeHead(303, { ...baseHeaders(), Location: `${url.pathname}${url.search}` });
5420
5531
  res.end();
5421
5532
  }
@@ -5445,7 +5556,8 @@ async function createWebServer(opts) {
5445
5556
  ...opts.idleGraceMs === void 0 ? {} : { idleGraceMs: opts.idleGraceMs },
5446
5557
  ...opts.clock === void 0 ? {} : { clock: opts.clock },
5447
5558
  ...opts.maxActiveSessions === void 0 ? {} : { maxActiveSessions: opts.maxActiveSessions },
5448
- onCooled: (sessionId) => opts.runtime.sessionFactory.release(sessionId)
5559
+ onCooled: (sessionId) => opts.runtime.sessionFactory.release(sessionId),
5560
+ readTitle: (sessionId) => opts.runtime.sessions?.get(sessionId)?.title || null
5449
5561
  });
5450
5562
  if (opts.runtime.session) hub.register(opts.runtime.session);
5451
5563
  const unsubscribeTasks = observeBackgroundTasks((sessionId) => {
@@ -5469,7 +5581,12 @@ async function createWebServer(opts) {
5469
5581
  return {
5470
5582
  ok: true,
5471
5583
  server: {
5472
- url: firstScreenUrl(binding.host, listened.port, binding.token, opts.ui ?? {}),
5584
+ url: firstScreenUrl(
5585
+ binding.host,
5586
+ listened.port,
5587
+ binding.token,
5588
+ resolveUiPreset(opts.ui ?? {}, opts.runtime.appearance?.get() ?? {})
5589
+ ),
5473
5590
  port: listened.port,
5474
5591
  host: binding.host,
5475
5592
  token: binding.token,