@epoch-agent/server 0.8.1 → 0.10.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,12 +1,13 @@
1
1
  import { createServer } from 'http';
2
- import { t, ApprovalRelay, QuestionRelay, serializeStream, withRoleScope, withModelScope, withToolScope, nextRunAt, 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, 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';
3
3
  import { timingSafeEqual, randomBytes } 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, MAX_ATTACH_TOTAL_BYTES, MAX_ATTACH_BYTES, WIRE_LANG_PARAM, isLang, isQuestionAnswerMap, isProviderType, apiKeyEnvVar, WIRE_FILE_STAT_MAX, WIRE_SETTING_WRITE_LAYERS, isWirePlanAction, WIRE_PLAN_ACTIONS, isPermissionLevel, PERMISSION_LEVELS, collectPendingApprovals, unfixedRules, isWireSettingWriteLayer, parseModelRef, WIRE_GOAL_ACTIONS, OPERATION_TYPES, isOperationType } 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, WIRE_LANG_PARAM, isLang, isQuestionAnswerMap, isProviderType, apiKeyEnvVar, WIRE_FILE_STAT_MAX, WIRE_SETTING_WRITE_LAYERS, isWirePlanAction, WIRE_PLAN_ACTIONS, isPermissionLevel, PERMISSION_LEVELS, conformsToWire, 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
- import { existsSync, realpathSync, statSync, mkdirSync, createReadStream, readdirSync, accessSync, constants } from 'fs';
6
+ import { existsSync, realpathSync, statSync, mkdirSync, createReadStream, readdirSync, createWriteStream, accessSync, constants } from 'fs';
7
+ import { stat, readFile, rm } from 'fs/promises';
8
+ import { pipeline } from 'stream/promises';
7
9
  import { homedir } from 'os';
8
10
  import { execFile, spawn } from 'child_process';
9
- import { stat, readFile } from 'fs/promises';
10
11
  import { fileURLToPath } from 'url';
11
12
 
12
13
  // src/index.ts
@@ -271,6 +272,22 @@ function toWireSessionWorkspace(bound) {
271
272
  skippedInstructionFiles: bound.projectContext.skippedInstructions
272
273
  };
273
274
  }
275
+ function projectInventoryOf(workspaces, sessionId, trustRequest) {
276
+ const root = workspaces.of(sessionId)?.workspace.root;
277
+ return {
278
+ inventory: root === void 0 ? EMPTY_INVENTORY : workspaces.inventoryOf(root),
279
+ trustRequest
280
+ };
281
+ }
282
+ var EMPTY_INVENTORY = {
283
+ instructionFiles: [],
284
+ skills: 0,
285
+ roles: 0,
286
+ commands: 0,
287
+ hookFiles: [],
288
+ policies: 0,
289
+ settingsFiles: []
290
+ };
274
291
  function toWireBinding(state, bound) {
275
292
  if (state === "bound" && bound)
276
293
  return { state: "bound", workspace: toWireSessionWorkspace(bound) };
@@ -417,6 +434,7 @@ var SessionHub = class {
417
434
  registry;
418
435
  sinks = /* @__PURE__ */ new Set();
419
436
  observers = /* @__PURE__ */ new Set();
437
+ counted = /* @__PURE__ */ new Set();
420
438
  requestOwner = /* @__PURE__ */ new Map();
421
439
  seq = 0;
422
440
  graceTimer = null;
@@ -474,6 +492,9 @@ var SessionHub = class {
474
492
  get connectionCount() {
475
493
  return this.sinks.size;
476
494
  }
495
+ get present() {
496
+ return this.sinks.size + this.counted.size;
497
+ }
477
498
  subscribe(sink, lastEventId) {
478
499
  sink(this.connectionFrame({ type: "connected" }));
479
500
  if (lastEventId !== void 0) {
@@ -491,18 +512,24 @@ var SessionHub = class {
491
512
  if (released) return;
492
513
  released = true;
493
514
  this.sinks.delete(sink);
494
- if (this.sinks.size === 0) this.armGrace();
515
+ if (this.present === 0) this.armGrace();
495
516
  };
496
517
  }
497
- observe(sink) {
518
+ observe(sink, opts = {}) {
498
519
  if (this.closed) return () => {
499
520
  };
500
521
  this.observers.add(sink);
522
+ if (opts.counts === true) {
523
+ this.counted.add(sink);
524
+ this.clearGrace();
525
+ }
501
526
  let released = false;
502
527
  return () => {
503
528
  if (released) return;
504
529
  released = true;
505
530
  this.observers.delete(sink);
531
+ const wasPresent = this.counted.delete(sink);
532
+ if (wasPresent && this.present === 0) this.armGrace();
506
533
  };
507
534
  }
508
535
  start(sessionId, message, decorate) {
@@ -554,6 +581,14 @@ var SessionHub = class {
554
581
  this.syncApprovalState(entry);
555
582
  return true;
556
583
  }
584
+ notifyTasksChanged(sessionId) {
585
+ if (this.closed) return;
586
+ this.publish(sessionId, { type: "tasks-changed" });
587
+ }
588
+ notifySessionsChanged(sessionId) {
589
+ if (this.closed) return;
590
+ this.publish(sessionId, { type: "sessions-changed" });
591
+ }
557
592
  shutdown() {
558
593
  if (this.closed) return;
559
594
  this.closed = true;
@@ -561,6 +596,7 @@ var SessionHub = class {
561
596
  this.reap();
562
597
  this.sinks.clear();
563
598
  this.observers.clear();
599
+ this.counted.clear();
564
600
  }
565
601
  publish(sessionId, event) {
566
602
  const frame = { seq: ++this.seq, sessionId, ts: this.clock(), event };
@@ -624,7 +660,7 @@ var SessionHub = class {
624
660
  }
625
661
  drainQueue(entry) {
626
662
  if (this.closed || !this.registry.has(entry.session.sessionId)) return;
627
- if (this.sinks.size === 0) {
663
+ if (this.present === 0) {
628
664
  entry.queue.length = 0;
629
665
  return;
630
666
  }
@@ -645,7 +681,7 @@ var SessionHub = class {
645
681
  this.graceTimer = null;
646
682
  }
647
683
  reap() {
648
- if (this.sinks.size > 0 && !this.closed) return;
684
+ if (this.present > 0 && !this.closed) return;
649
685
  for (const [, entry] of this.registry.all()) {
650
686
  entry.queue.length = 0;
651
687
  if (entry.state === "idle") continue;
@@ -763,6 +799,54 @@ async function readJsonBody(req, limit = JSON_BODY_LIMIT, lang) {
763
799
  };
764
800
  }
765
801
  }
802
+ var PLUGIN_UPLOAD_BODY_LIMIT = 50 * 1024 * 1024;
803
+ async function readRawBody(req, limit, lang) {
804
+ const chunks = [];
805
+ let size = 0;
806
+ try {
807
+ for await (const chunk of req) {
808
+ const buf = chunk;
809
+ size += buf.length;
810
+ if (size > limit) {
811
+ req.destroy();
812
+ return { ok: false, status: 413, message: t("web.body_too_large", { limit }, lang) };
813
+ }
814
+ chunks.push(buf);
815
+ }
816
+ } catch (err) {
817
+ return {
818
+ ok: false,
819
+ status: 400,
820
+ message: t("web.body_read_failed", { reason: describe(err) }, lang)
821
+ };
822
+ }
823
+ if (size === 0) {
824
+ return { ok: false, status: 400, message: t("web.body_empty", void 0, lang) };
825
+ }
826
+ return { ok: true, value: Buffer.concat(chunks) };
827
+ }
828
+ async function streamBodyToFile(req, target, lang) {
829
+ const sink = createWriteStream(target, { flags: "wx" });
830
+ try {
831
+ await pipeline(req, sink);
832
+ } catch (err) {
833
+ await rm(target, { force: true }).catch(() => void 0);
834
+ return {
835
+ ok: false,
836
+ status: 400,
837
+ message: t("web.body_read_failed", { reason: describe(err) }, lang)
838
+ };
839
+ }
840
+ try {
841
+ return { ok: true, bytes: (await stat(target)).size };
842
+ } catch (err) {
843
+ return {
844
+ ok: false,
845
+ status: 500,
846
+ message: t("web.body_read_failed", { reason: describe(err) }, lang)
847
+ };
848
+ }
849
+ }
766
850
  function describe(err) {
767
851
  return err instanceof Error ? err.message : String(err);
768
852
  }
@@ -929,7 +1013,7 @@ function estimateIndexTokens(text) {
929
1013
  return Math.ceil(text.length / 4);
930
1014
  }
931
1015
  function toWireMcp(server) {
932
- const state = server.needsLogin ? "needs-login" : server.connected ? "connected" : "failed";
1016
+ const state = mcpConnectorState(server);
933
1017
  return {
934
1018
  name: server.name,
935
1019
  state,
@@ -939,7 +1023,7 @@ function toWireMcp(server) {
939
1023
  source: server.source
940
1024
  };
941
1025
  }
942
- function collectCapabilities(runtime, tools, bound, role) {
1026
+ function collectCapabilities(runtime, tools, bound, role, project) {
943
1027
  const residency = runtime.skillIndexResidency(role);
944
1028
  return {
945
1029
  roles: runtime.agentRoles.map((r) => toWireRole(r, runtime.agentRoleNotices)),
@@ -948,7 +1032,10 @@ function collectCapabilities(runtime, tools, bound, role) {
948
1032
  mcpServers: runtime.mcpServers.map(toWireMcp),
949
1033
  project: {
950
1034
  root: bound?.workspace.root ?? null,
951
- trusted: bound?.trust.trusted ?? false
1035
+ trusted: bound?.trust.trusted ?? false,
1036
+ execTrusted: bound?.trust.exec ?? false,
1037
+ inventory: project.inventory,
1038
+ trustRequest: project.trustRequest
952
1039
  }
953
1040
  };
954
1041
  }
@@ -1018,6 +1105,81 @@ function replaceLeadingText(message, prompt) {
1018
1105
  const head = { type: "text", text: prompt };
1019
1106
  return [head, ...message.slice(1)];
1020
1107
  }
1108
+ async function uploadAttachment(ctx, req, res, sessionId, lang) {
1109
+ const raw = nameParam(req.url);
1110
+ if (raw === void 0) {
1111
+ return sendError(res, 400, "bad-request", t("web.attachment_name_required", void 0, lang));
1112
+ }
1113
+ const control = ctx.runtime.attachments;
1114
+ let placed;
1115
+ try {
1116
+ placed = control.place(sessionId, raw);
1117
+ } catch (err) {
1118
+ return sendError(res, 500, "attachment-failed", describe2(err));
1119
+ }
1120
+ if (!placed) {
1121
+ return sendError(
1122
+ res,
1123
+ 400,
1124
+ "bad-request",
1125
+ t("web.attachment_name_invalid", { name: raw }, lang)
1126
+ );
1127
+ }
1128
+ const out = await streamBodyToFile(req, placed.path, lang);
1129
+ if (!out.ok) return sendError(res, out.status, "bad-body", out.message);
1130
+ control.prune(sessionId);
1131
+ return sendJson(res, 200, {
1132
+ name: placed.name,
1133
+ bytes: out.bytes
1134
+ });
1135
+ }
1136
+ function attachUploads(ctx, sessionId, names, usedBytes = 0) {
1137
+ const parts = [];
1138
+ const receipts = [];
1139
+ let used = usedBytes;
1140
+ for (const name of names) {
1141
+ const part = readOne(ctx, sessionId, name, used);
1142
+ parts.push(part);
1143
+ if (part.text !== void 0) used += utf8ByteLength(part.text);
1144
+ const { type: _type, text: _text, ...rest } = part;
1145
+ receipts.push(rest);
1146
+ }
1147
+ return { parts, receipts, usedBytes: used };
1148
+ }
1149
+ function readOne(ctx, sessionId, name, usedBytes) {
1150
+ const remaining = MAX_ATTACH_TOTAL_BYTES - usedBytes;
1151
+ const path = ctx.runtime.attachments.resolve(sessionId, name) ?? name;
1152
+ if (remaining <= 0) return { type: "file", path, omitted: "over-limit" };
1153
+ const budget = Math.min(MAX_ATTACH_BYTES, remaining);
1154
+ const out = ctx.runtime.attachments.read(sessionId, name);
1155
+ if (!out.ok) {
1156
+ return {
1157
+ type: "file",
1158
+ path,
1159
+ omitted: out.reason,
1160
+ ...out.bytes === void 0 ? {} : { bytes: out.bytes }
1161
+ };
1162
+ }
1163
+ if (out.bytes <= budget) {
1164
+ return { type: "file", path: out.path, text: out.text, bytes: out.bytes };
1165
+ }
1166
+ const { text, truncatedTo } = truncateToBudget(new TextEncoder().encode(out.text), budget);
1167
+ return { type: "file", path: out.path, text, truncatedTo, bytes: out.bytes };
1168
+ }
1169
+ function nameParam(url) {
1170
+ let raw;
1171
+ try {
1172
+ raw = new URL(url ?? "/", "http://127.0.0.1").searchParams.get("name");
1173
+ } catch {
1174
+ return void 0;
1175
+ }
1176
+ return raw && raw.length > 0 ? raw : void 0;
1177
+ }
1178
+ function describe2(err) {
1179
+ return err instanceof Error ? err.message : String(err);
1180
+ }
1181
+ // src/mentions.ts
1182
+ var EMPTY_MENTIONS = { files: [], sessions: [] };
1021
1183
  var MAX_CANDIDATES = 20;
1022
1184
  var MAX_CANDIDATE_LIMIT = 50;
1023
1185
  function visibilityOf(ctx, sessionId) {
@@ -1099,16 +1261,18 @@ function listFileCandidates(ctx, res, sessionId, rawUrl, lang) {
1099
1261
  truncated: result.truncated
1100
1262
  });
1101
1263
  }
1102
- function attachMentions(ctx, sessionId, message) {
1264
+ function attachMentions(ctx, sessionId, message, uploads = []) {
1103
1265
  const text = leadingText2(message);
1104
- if (text === null) return { message, references: [], files: [] };
1105
- const mentions = extractAllMentions(text);
1106
- if (mentions.files.length === 0 && mentions.sessions.length === 0) {
1266
+ const mentions = text === null ? EMPTY_MENTIONS : extractAllMentions(text);
1267
+ if (uploads.length === 0 && mentions.files.length === 0 && mentions.sessions.length === 0) {
1107
1268
  return { message, references: [], files: [] };
1108
1269
  }
1109
1270
  const parts = [];
1110
1271
  let used = 0;
1111
- const fileReceipts = [];
1272
+ const uploaded = attachUploads(ctx, sessionId, uploads, used);
1273
+ parts.push(...uploaded.parts);
1274
+ used = uploaded.usedBytes;
1275
+ const fileReceipts = [...uploaded.receipts];
1112
1276
  for (const [index, path] of mentions.files.entries()) {
1113
1277
  const part = index >= MAX_ATTACH_FILES ? { type: "file", path, omitted: "over-limit" } : readOneFile(ctx, sessionId, path, used);
1114
1278
  parts.push(part);
@@ -1245,11 +1409,14 @@ function toWirePolicy(policy) {
1245
1409
  }));
1246
1410
  return { dirs, projectSkipped: policy.projectSkipped, skippedDir: policy.skippedDir };
1247
1411
  }
1248
- function toWireSecurityWorkspace(bound) {
1412
+ function toWireSecurityWorkspace(bound, project) {
1249
1413
  return {
1250
1414
  ...toWireSessionWorkspace(bound),
1251
1415
  trustLevel: bound.trust.level,
1252
- unloadedExtraInstructionFiles: bound.unloadedInstructions
1416
+ unloadedExtraInstructionFiles: bound.unloadedInstructions,
1417
+ execTrusted: bound.trust.exec,
1418
+ inventory: project.inventory,
1419
+ trustRequest: project.trustRequest
1253
1420
  };
1254
1421
  }
1255
1422
  function toWireTrustList(known, workspaces, currentRoot) {
@@ -1260,11 +1427,13 @@ function toWireTrustList(known, workspaces, currentRoot) {
1260
1427
  name: w.name,
1261
1428
  level: decision.level,
1262
1429
  trusted: decision.trusted,
1263
- current: currentRoot !== null && w.root === currentRoot
1430
+ execTrusted: decision.exec,
1431
+ current: currentRoot !== null && w.root === currentRoot,
1432
+ exists: existsSync(w.root)
1264
1433
  };
1265
1434
  });
1266
1435
  }
1267
- function collectSecurity(runtime, workspaces, bound, session) {
1436
+ function collectSecurity(runtime, workspaces, bound, session, project) {
1268
1437
  const perms = runtime.permissions;
1269
1438
  return {
1270
1439
  sandbox: runtime.isolation ? toWireSandbox(runtime.isolation, runtime.config.sandbox?.terminal !== false) : null,
@@ -1277,7 +1446,7 @@ function collectSecurity(runtime, workspaces, bound, session) {
1277
1446
  managed: toWireManaged(perms.managed()),
1278
1447
  cached: perms.approvals().map(toWireApproval)
1279
1448
  } : PERMISSIONS_UNAVAILABLE,
1280
- workspace: bound ? toWireSecurityWorkspace(bound) : null,
1449
+ workspace: bound ? toWireSecurityWorkspace(bound, project) : null,
1281
1450
  knownWorkspaces: toWireTrustList(workspaces.known(), workspaces, bound?.workspace.root ?? null),
1282
1451
  policy: toWirePolicy(runtime.policy),
1283
1452
  audit: perms ? toWireAudit(perms.audit()) : { rows: [], dropped: 0 }
@@ -1447,6 +1616,7 @@ function config(ctx, res, lang) {
1447
1616
  usageScope: runtime.usageScope,
1448
1617
  compression: runtime.compression,
1449
1618
  ...runtime.config.budget?.maxCostUsd !== void 0 ? { maxCostUsd: runtime.config.budget.maxCostUsd } : {},
1619
+ ...runtime.config.display?.currency !== void 0 ? { currency: runtime.config.display.currency } : {},
1450
1620
  ...runtime.config.brand !== void 0 ? { brand: runtime.config.brand } : {},
1451
1621
  ...runtime.config.sidebarMenu !== void 0 ? { sidebarMenu: runtime.config.sidebarMenu } : {},
1452
1622
  ...runtime.config.userMenu !== void 0 ? { userMenu: runtime.config.userMenu } : {},
@@ -1510,6 +1680,7 @@ async function deleteSession(ctx, res, sessionId, lang) {
1510
1680
  }
1511
1681
  const outcome = await catalog.delete(sessionId);
1512
1682
  if (!outcome.deleted && !inHub) return sendUnknownSession(res, sessionId, lang);
1683
+ ctx.hub.notifySessionsChanged(sessionId);
1513
1684
  sendJson(res, 200, outcome);
1514
1685
  }
1515
1686
  async function patchSession(ctx, req, res, sessionId, lang) {
@@ -1562,7 +1733,9 @@ async function postMessage(ctx, req, res, sessionId, lang) {
1562
1733
  }
1563
1734
  const body = await readJsonBody(req, void 0, lang);
1564
1735
  if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
1565
- const message = parseUserContent(asRecord(body.value).message);
1736
+ const uploads = parseAttachmentNames(asRecord(body.value)["attachments"]);
1737
+ const parsed = parseUserContent(asRecord(body.value).message);
1738
+ const message = parsed ?? (uploads.length > 0 ? [] : null);
1566
1739
  if (message === null) {
1567
1740
  return sendError(res, 400, "bad-message", t("web.bad_message", void 0, lang));
1568
1741
  }
@@ -1573,7 +1746,7 @@ async function postMessage(ctx, req, res, sessionId, lang) {
1573
1746
  return sendError(res, mapped.status, mapped.code, mapped.message);
1574
1747
  }
1575
1748
  const expansion = expandUserContent(ctx.runtime, message, live?.model ?? null, picked.role);
1576
- const attached = attachMentions(ctx, sessionId, expansion.message);
1749
+ const attached = attachMentions(ctx, sessionId, expansion.message, uploads);
1577
1750
  const started = ctx.hub.start(sessionId, attached.message, expansion.decorate);
1578
1751
  if (started.ok) {
1579
1752
  return sendJson(res, 202, {
@@ -1664,7 +1837,8 @@ function listCapabilities(ctx, res, sessionId, lang) {
1664
1837
  ctx.runtime,
1665
1838
  ctx.runtime.tools,
1666
1839
  ctx.runtime.workspaces.of(sessionId),
1667
- ctx.runtime.sessionFactory.get(sessionId)?.role?.name ?? null
1840
+ ctx.runtime.sessionFactory.get(sessionId)?.role?.name ?? null,
1841
+ projectInventoryOf(ctx.runtime.workspaces, sessionId, ctx.trustRequest !== void 0)
1668
1842
  );
1669
1843
  sendJson(res, 200, payload);
1670
1844
  }
@@ -1683,7 +1857,8 @@ function getSecurity(ctx, res, sessionId, lang) {
1683
1857
  ctx.runtime,
1684
1858
  ctx.runtime.workspaces,
1685
1859
  ctx.runtime.workspaces.of(sessionId),
1686
- ctx.runtime.sessionFactory.get(sessionId)?.permissions ?? null
1860
+ ctx.runtime.sessionFactory.get(sessionId)?.permissions ?? null,
1861
+ projectInventoryOf(ctx.runtime.workspaces, sessionId, ctx.trustRequest !== void 0)
1687
1862
  );
1688
1863
  sendJson(res, 200, payload);
1689
1864
  }
@@ -1753,6 +1928,10 @@ function parseUserContent(raw) {
1753
1928
  if (Array.isArray(raw) && raw.length > 0) return raw;
1754
1929
  return null;
1755
1930
  }
1931
+ function parseAttachmentNames(raw) {
1932
+ if (!Array.isArray(raw)) return [];
1933
+ return raw.filter((name) => typeof name === "string" && name.length > 0);
1934
+ }
1756
1935
  async function revokeApprovalCache(ctx, req, res, sessionId, lang) {
1757
1936
  if (!ctx.hub.has(sessionId)) return sendUnknownSession(res, sessionId, lang);
1758
1937
  const perms = ctx.runtime.permissions;
@@ -2282,10 +2461,11 @@ function sessionHasImages(session) {
2282
2461
  }
2283
2462
  function getModel(ctx, res, sessionId, _lang) {
2284
2463
  const { runtime } = ctx;
2285
- const selection = runtime.sessionFactory.get(sessionId)?.model.get();
2464
+ const live = runtime.sessionFactory.get(sessionId);
2465
+ const selection = live?.model.get();
2286
2466
  sendJson(res, 200, {
2287
2467
  selection: selection ? toWireSelection(selection) : null,
2288
- configured: runtime.config.model || null
2468
+ configured: (live ? live.model.configured() : runtime.config.model) || null
2289
2469
  });
2290
2470
  }
2291
2471
  async function setModel(ctx, req, res, sessionId, lang) {
@@ -2433,9 +2613,18 @@ function toWireCounts(counts) {
2433
2613
  mcpServers: counts.mcpServers
2434
2614
  };
2435
2615
  }
2436
- function toWirePluginEntry(entry) {
2616
+ function displayNamesOf(control) {
2617
+ const out = /* @__PURE__ */ new Map();
2618
+ for (const hit of control.search()) {
2619
+ const shown = hit.entry.displayName;
2620
+ if (shown !== void 0 && shown !== "") out.set(`${hit.marketplace}/${hit.entry.name}`, shown);
2621
+ }
2622
+ return out;
2623
+ }
2624
+ function toWirePluginEntry(entry, displayNames) {
2437
2625
  return {
2438
2626
  name: entry.name,
2627
+ displayName: displayNames.get(`${entry.marketplace}/${entry.name}`) ?? null,
2439
2628
  version: entry.version,
2440
2629
  source: entry.source,
2441
2630
  sourceType: entry.sourceType,
@@ -2470,6 +2659,7 @@ function toWirePluginHit(hit, installed) {
2470
2659
  size: hit.entry.size ?? null,
2471
2660
  homepage: hit.entry.homepage ?? null,
2472
2661
  visibility: hit.entry.visibility ?? null,
2662
+ status: hit.entry.status ?? null,
2473
2663
  updateAvailable: mine !== void 0 && !mine.linked && hit.entry.version !== void 0 && hit.entry.version !== mine.version
2474
2664
  };
2475
2665
  }
@@ -2526,21 +2716,39 @@ function fieldOf(payload, key) {
2526
2716
  const value = payload[key];
2527
2717
  return typeof value === "string" ? value.trim() : "";
2528
2718
  }
2719
+ function uploadFormOf(payload) {
2720
+ const form = {};
2721
+ for (const name of WIRE_PLUGIN_UPLOAD_FORM_FIELDS) {
2722
+ const raw = fieldOf(payload, name);
2723
+ if (raw === "") continue;
2724
+ if (name === "visibility") {
2725
+ if (raw === "private" || raw === "public") form[name] = raw;
2726
+ continue;
2727
+ }
2728
+ form[name] = raw;
2729
+ }
2730
+ return form;
2731
+ }
2529
2732
  function listPlugins(ctx, res, lang) {
2530
2733
  const control = controlOf(ctx, res, lang);
2531
2734
  if (!control) return;
2735
+ sendJson(res, 200, pluginsPayload(control));
2736
+ }
2737
+ function pluginsPayload(control) {
2532
2738
  const installed = control.list();
2533
2739
  const installedMap = new Map(
2534
2740
  installed.map((p) => [p.name, { version: p.version, linked: p.linked }])
2535
2741
  );
2536
- sendJson(res, 200, {
2537
- installed: installed.map(toWirePluginEntry),
2742
+ const shown = displayNamesOf(control);
2743
+ return {
2744
+ installed: installed.map((p) => toWirePluginEntry(p, shown)),
2538
2745
  hits: control.search().map((hit) => toWirePluginHit(hit, installedMap)),
2539
2746
  marketplaces: control.marketplaces().map(toWireMarketplace),
2540
2747
  pendingRestart: control.pendingRestart,
2541
2748
  uploadEnabled: control.uploadEnabled,
2542
- uploadOptions: control.uploadOptions
2543
- });
2749
+ uploadOptions: control.uploadOptions,
2750
+ refreshEnabled: control.refreshEnabled
2751
+ };
2544
2752
  }
2545
2753
  function sendPreview(res, outcome) {
2546
2754
  if (!outcome.ok) {
@@ -2597,7 +2805,7 @@ function sendAction(res, control, outcome) {
2597
2805
  const fresh = "record" in outcome ? control.list().find((p) => p.name === outcome.record.name) : void 0;
2598
2806
  sendJson(res, 200, {
2599
2807
  ok: true,
2600
- entry: fresh ? toWirePluginEntry(fresh) : null,
2808
+ entry: fresh ? toWirePluginEntry(fresh, displayNamesOf(control)) : null,
2601
2809
  pendingRestart: control.pendingRestart,
2602
2810
  detail: "detail" in outcome ? outcome.detail : null,
2603
2811
  reason: null
@@ -2629,6 +2837,51 @@ async function installLocalPlugin(ctx, req, res, lang) {
2629
2837
  await control.installLocal(fieldOf(payload, "path"), fieldOf(payload, "token"), lang)
2630
2838
  );
2631
2839
  }
2840
+ async function stagePlugin(ctx, req, res, lang) {
2841
+ if (refusedByLan(ctx, res, lang)) return;
2842
+ const control = controlOf(ctx, res, lang);
2843
+ if (!control) return;
2844
+ const asJson = (req.headers["content-type"] ?? "").toLowerCase().includes("application/json");
2845
+ if (asJson) {
2846
+ const body = await readJsonBody(req, void 0, lang);
2847
+ if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
2848
+ return sendStage(
2849
+ res,
2850
+ await control.stage({ kind: "path", path: fieldOf(asRecord(body.value), "path") }, lang)
2851
+ );
2852
+ }
2853
+ const raw = await readRawBody(req, PLUGIN_UPLOAD_BODY_LIMIT, lang);
2854
+ if (!raw.ok) return sendError(res, raw.status, "bad-body", raw.message);
2855
+ return sendStage(
2856
+ res,
2857
+ await control.stage({ kind: "bytes", name: nameParam2(req), data: raw.value }, lang)
2858
+ );
2859
+ }
2860
+ function nameParam2(req) {
2861
+ try {
2862
+ return new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("name") ?? "";
2863
+ } catch {
2864
+ return "";
2865
+ }
2866
+ }
2867
+ function sendStage(res, outcome) {
2868
+ const payload = outcome.ok ? {
2869
+ ok: true,
2870
+ path: outcome.path,
2871
+ name: outcome.name,
2872
+ version: outcome.version,
2873
+ detail: null,
2874
+ reason: null
2875
+ } : {
2876
+ ok: false,
2877
+ path: null,
2878
+ name: null,
2879
+ version: null,
2880
+ detail: outcome.detail,
2881
+ reason: outcome.reason
2882
+ };
2883
+ sendJson(res, 200, payload);
2884
+ }
2632
2885
  async function updateInstalledPlugin(ctx, req, res, lang) {
2633
2886
  if (refusedByLan(ctx, res, lang)) return;
2634
2887
  const control = controlOf(ctx, res, lang);
@@ -2686,10 +2939,7 @@ function uploadPlugin(ctx, req, res, lang) {
2686
2939
  await control.upload(
2687
2940
  fieldOf(payload, "path"),
2688
2941
  fieldOf(payload, "token"),
2689
- {
2690
- ...fieldOf(payload, "visibility") === "private" ? { visibility: "private" } : fieldOf(payload, "visibility") === "public" ? { visibility: "public" } : {},
2691
- ...fieldOf(payload, "changelog") ? { changelog: fieldOf(payload, "changelog") } : {}
2692
- },
2942
+ uploadFormOf(payload),
2693
2943
  lang
2694
2944
  )
2695
2945
  );
@@ -2739,13 +2989,34 @@ function sendUpload(res, outcome) {
2739
2989
  reason: outcome.ok ? null : outcome.reason
2740
2990
  });
2741
2991
  }
2992
+ async function refreshPlugins(ctx, res, lang) {
2993
+ if (refusedByLan(ctx, res, lang)) return;
2994
+ const control = controlOf(ctx, res, lang);
2995
+ if (!control) return;
2996
+ const outcome = await control.refresh(lang);
2997
+ if (!outcome.ok) {
2998
+ return sendJson(res, 200, {
2999
+ ok: false,
3000
+ plugins: null,
3001
+ reason: outcome.reason,
3002
+ detail: outcome.detail
3003
+ });
3004
+ }
3005
+ sendJson(res, 200, {
3006
+ ok: true,
3007
+ plugins: pluginsPayload(control),
3008
+ reason: null,
3009
+ detail: outcome.detail === "" ? null : outcome.detail
3010
+ });
3011
+ }
2742
3012
  function toWireProvider(option, lang) {
2743
3013
  return {
2744
3014
  type: option.type,
2745
3015
  label: providerLabel(option, lang),
2746
3016
  envVar: option.envVar,
2747
3017
  hasKey: option.hasKey,
2748
- keyHint: option.keyHint
3018
+ keyHint: option.keyHint,
3019
+ current: option.current
2749
3020
  };
2750
3021
  }
2751
3022
  function toWireSuggestions(view) {
@@ -3310,6 +3581,33 @@ async function removeSkill(ctx, req, res, lang) {
3310
3581
  reason: null
3311
3582
  });
3312
3583
  }
3584
+ function refusedByLan2(ctx, res, lang) {
3585
+ if (!ctx.lanExposed) return false;
3586
+ sendError(res, 403, "skill-stage-lan-exposed", t("web.skill_stage_lan_exposed", void 0, lang));
3587
+ return true;
3588
+ }
3589
+ async function stageSkillDrop(ctx, req, res, lang) {
3590
+ if (refusedByLan2(ctx, res, lang)) return;
3591
+ const raw = await readRawBody(req, PLUGIN_UPLOAD_BODY_LIMIT, lang);
3592
+ if (!raw.ok) return sendError(res, raw.status, "bad-body", raw.message);
3593
+ const outcome = await ctx.runtime.skillStage.stage(
3594
+ { name: nameParam3(req), data: raw.value },
3595
+ lang
3596
+ );
3597
+ sendJson(res, 200, {
3598
+ ok: outcome.ok,
3599
+ path: outcome.ok ? outcome.path : null,
3600
+ detail: outcome.ok ? null : outcome.detail,
3601
+ reason: outcome.ok ? null : outcome.reason
3602
+ });
3603
+ }
3604
+ function nameParam3(req) {
3605
+ try {
3606
+ return new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("name") ?? "";
3607
+ } catch {
3608
+ return "";
3609
+ }
3610
+ }
3313
3611
  // src/sse.ts
3314
3612
  var DEFAULT_HEARTBEAT_MS = 15e3;
3315
3613
  function parseLastEventId(req) {
@@ -3793,9 +4091,9 @@ var VIEWABLE = /* @__PURE__ */ new Map([
3793
4091
  [".webm", { kind: "video", mime: "video/webm" }],
3794
4092
  [".mov", { kind: "video", mime: "video/quicktime" }]
3795
4093
  ]);
3796
- function kindOf(stat2) {
3797
- if (stat2.kind === "text") return "text";
3798
- return VIEWABLE.get(extname(stat2.rel).toLowerCase())?.kind ?? "opaque";
4094
+ function kindOf(stat3) {
4095
+ if (stat3.kind === "text") return "text";
4096
+ return VIEWABLE.get(extname(stat3.rel).toLowerCase())?.kind ?? "opaque";
3799
4097
  }
3800
4098
  function toWireRefusal(reason) {
3801
4099
  return reason;
@@ -3843,15 +4141,15 @@ function statFiles(ctx, res, sessionId, rawUrl, lang) {
3843
4141
  const asked = params.getAll("path").filter((p) => p.trim() !== "");
3844
4142
  const taken = asked.slice(0, WIRE_FILE_STAT_MAX);
3845
4143
  const files = taken.map((path) => {
3846
- const stat2 = ctx.runtime.workspaceFiles.statFile(path, root);
3847
- if (!stat2.ok) return { path, ok: false, refusal: toWireRefusal(stat2.reason) };
4144
+ const stat3 = ctx.runtime.workspaceFiles.statFile(path, root);
4145
+ if (!stat3.ok) return { path, ok: false, refusal: toWireRefusal(stat3.reason) };
3848
4146
  return {
3849
4147
  path,
3850
4148
  ok: true,
3851
- kind: kindOf(stat2),
3852
- bytes: stat2.bytes,
3853
- mtimeMs: stat2.mtimeMs,
3854
- openable: ctx.sameMachine && isSystemOpenable(stat2.rel)
4149
+ kind: kindOf(stat3),
4150
+ bytes: stat3.bytes,
4151
+ mtimeMs: stat3.mtimeMs,
4152
+ openable: ctx.sameMachine && isSystemOpenable(stat3.rel)
3855
4153
  };
3856
4154
  });
3857
4155
  sendJson(res, 200, {
@@ -3865,18 +4163,18 @@ function readFileText(ctx, res, sessionId, rawUrl, lang) {
3865
4163
  if (root === null) return;
3866
4164
  const path = singlePath(res, rawUrl, lang);
3867
4165
  if (path === null) return;
3868
- const stat2 = ctx.runtime.workspaceFiles.statFile(path, root);
3869
- if (!stat2.ok) {
3870
- const refusal = toWireRefusal(stat2.reason);
4166
+ const stat3 = ctx.runtime.workspaceFiles.statFile(path, root);
4167
+ if (!stat3.ok) {
4168
+ const refusal = toWireRefusal(stat3.reason);
3871
4169
  return sendError(res, statusFor(refusal), refusal, describeRefusal(refusal, lang));
3872
4170
  }
3873
- if (stat2.kind !== "text") {
4171
+ if (stat3.kind !== "text") {
3874
4172
  return sendError(res, 415, "not-text", t("web.file_view_not_text", void 0, lang));
3875
4173
  }
3876
- if (stat2.bytes > MAX_TEXT_BYTES) {
4174
+ if (stat3.bytes > MAX_TEXT_BYTES) {
3877
4175
  return sendError(res, 413, "too-large", describeRefusal("too-large", lang));
3878
4176
  }
3879
- const out = ctx.runtime.workspaceFiles.readFile(stat2.rel, root);
4177
+ const out = ctx.runtime.workspaceFiles.readFile(stat3.rel, root);
3880
4178
  if (!out.ok) {
3881
4179
  const refusal = out.reason === "binary" ? "unreadable" : out.reason;
3882
4180
  return sendError(res, statusFor(refusal), refusal, describeRefusal(refusal, lang));
@@ -3892,26 +4190,26 @@ function readFileBytes(ctx, res, sessionId, rawUrl, headOnly, lang) {
3892
4190
  if (root === null) return;
3893
4191
  const path = singlePath(res, rawUrl, lang);
3894
4192
  if (path === null) return;
3895
- const stat2 = ctx.runtime.workspaceFiles.statFile(path, root);
3896
- if (!stat2.ok) {
3897
- const refusal = toWireRefusal(stat2.reason);
4193
+ const stat3 = ctx.runtime.workspaceFiles.statFile(path, root);
4194
+ if (!stat3.ok) {
4195
+ const refusal = toWireRefusal(stat3.reason);
3898
4196
  return sendError(res, statusFor(refusal), refusal, describeRefusal(refusal, lang));
3899
4197
  }
3900
- const known = VIEWABLE.get(extname(stat2.rel).toLowerCase());
3901
- const inline = known !== void 0 && stat2.kind === "binary";
4198
+ const known = VIEWABLE.get(extname(stat3.rel).toLowerCase());
4199
+ const inline = known !== void 0 && stat3.kind === "binary";
3902
4200
  const headers = {
3903
4201
  "Content-Type": inline ? known.mime : "application/octet-stream",
3904
4202
  "X-Content-Type-Options": "nosniff",
3905
- "Content-Disposition": inline ? "inline" : `attachment; filename*=UTF-8''${encodeURIComponent(basenameOf(stat2.rel))}`
4203
+ "Content-Disposition": inline ? "inline" : `attachment; filename*=UTF-8''${encodeURIComponent(basenameOf(stat3.rel))}`
3906
4204
  };
3907
4205
  if (inline && known.kind === "pdf") headers["Content-Security-Policy"] = "sandbox";
3908
4206
  if (headOnly) {
3909
4207
  return sendEmpty(res, 200, {
3910
4208
  ...headers,
3911
- "Content-Length": String(stat2.bytes)
4209
+ "Content-Length": String(stat3.bytes)
3912
4210
  });
3913
4211
  }
3914
- sendFile(res, stat2.absPath, headers);
4212
+ sendFile(res, stat3.absPath, headers);
3915
4213
  }
3916
4214
  function basenameOf(rel) {
3917
4215
  const cut = rel.lastIndexOf("/");
@@ -3962,6 +4260,26 @@ async function openFileWithSystemApp(ctx, req, res, sessionId, lang) {
3962
4260
  }
3963
4261
  sendJson(res, 200, { ok: true });
3964
4262
  }
4263
+ async function forgetWorkspace(ctx, sessionId, req, res, lang) {
4264
+ const body = await readJsonBody(req, void 0, lang);
4265
+ if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
4266
+ if (!conformsToWire(body.value, WIRE_FORGET_WORKSPACE_FIELDS)) {
4267
+ return sendError(res, 400, "bad-request", t("web.forget_bad_request", {}, lang));
4268
+ }
4269
+ const { root } = body.value;
4270
+ const workspaces = ctx.runtime.workspaces;
4271
+ if (!workspaces.known().some((w) => w.root === root)) {
4272
+ return sendError(res, 404, "unknown-workspace", t("web.forget_unknown", {}, lang));
4273
+ }
4274
+ const current = workspaces.of(sessionId)?.workspace.root;
4275
+ if (current !== void 0 && current === root) {
4276
+ return sendError(res, 409, "workspace-in-use", t("web.forget_in_use", {}, lang));
4277
+ }
4278
+ workspaces.forget(root);
4279
+ sendJson(res, 200, {
4280
+ known: toWireTrustList(workspaces.known(), workspaces, current ?? null)
4281
+ });
4282
+ }
3965
4283
  var GIT_TIMEOUT_MS = 1e4;
3966
4284
  var GIT_MAX_BUFFER = 16 * 1024 * 1024;
3967
4285
  function runGit(cwd, args) {
@@ -4072,7 +4390,7 @@ async function gitDiff(root) {
4072
4390
  for (const item of picked) {
4073
4391
  const old = await gitSide(root, item.oldSha, sizes);
4074
4392
  const fresh = await worktreeSide(join(root, ...item.path.split("/")));
4075
- files.push(describe2(item.path, old, fresh));
4393
+ files.push(describe3(item.path, old, fresh));
4076
4394
  }
4077
4395
  return { source: "git", root, files, truncated, notes };
4078
4396
  }
@@ -4118,7 +4436,7 @@ async function checkpointDiff(opts) {
4118
4436
  for (const absolute of picked) {
4119
4437
  const fresh = await worktreeSide(absolute);
4120
4438
  files.push(
4121
- describe2(displayPath(opts.workDir, absolute), ABSENT, fresh, {
4439
+ describe3(displayPath(opts.workDir, absolute), ABSENT, fresh, {
4122
4440
  status: fresh.kind === "absent" ? "deleted" : "modified",
4123
4441
  omitted: "no-snapshot"
4124
4442
  })
@@ -4156,7 +4474,7 @@ async function worktreeSide(absolute) {
4156
4474
  return ABSENT;
4157
4475
  }
4158
4476
  }
4159
- function describe2(path, old, fresh, override = {}) {
4477
+ function describe3(path, old, fresh, override = {}) {
4160
4478
  const status = override.status ?? (old.kind === "absent" ? "added" : fresh.kind === "absent" ? "deleted" : "modified");
4161
4479
  const oldBytes = bytesOf(old);
4162
4480
  const newBytes = bytesOf(fresh);
@@ -4239,6 +4557,7 @@ async function createSession(ctx, req, res, lang) {
4239
4557
  }
4240
4558
  ctx.hub.register(outcome.live.session);
4241
4559
  if (defer) ctx.runtime.workspaces.defer(outcome.live.sessionId);
4560
+ if (outcome.created) ctx.hub.notifySessionsChanged(outcome.live.sessionId);
4242
4561
  sendJson(res, 200, {
4243
4562
  sessionId: outcome.live.sessionId,
4244
4563
  created: outcome.created,
@@ -4323,6 +4642,36 @@ async function sessionDiff(ctx, res, sessionId, _lang) {
4323
4642
  })
4324
4643
  );
4325
4644
  }
4645
+ function isKnownRoot(ctx, sessionId, root) {
4646
+ if (ctx.runtime.workspaces.of(sessionId)?.workspace.root === root) return true;
4647
+ return ctx.runtime.workspaces.known().some((w) => w.root === root);
4648
+ }
4649
+ async function requestTrust(ctx, sessionId, req, res, lang) {
4650
+ const ask = ctx.trustRequest;
4651
+ if (ask === void 0) {
4652
+ return sendError(res, 404, "trust-request-unavailable", t("web.trust_unavailable", {}, lang));
4653
+ }
4654
+ const body = await readJsonBody(req, void 0, lang);
4655
+ if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
4656
+ if (!conformsToWire(body.value, WIRE_TRUST_REQUEST_FIELDS)) {
4657
+ return sendError(res, 400, "bad-request", t("web.trust_bad_request", {}, lang));
4658
+ }
4659
+ const { root, want } = body.value;
4660
+ if (!isKnownRoot(ctx, sessionId, root)) {
4661
+ return sendError(res, 400, "unknown-workspace", t("web.trust_unknown_root", {}, lang));
4662
+ }
4663
+ try {
4664
+ await ask({ root, want: want === "exec" ? "exec" : "read" });
4665
+ } catch (err) {
4666
+ return sendError(
4667
+ res,
4668
+ 500,
4669
+ "trust-request-failed",
4670
+ t("web.trust_failed", { detail: err instanceof Error ? err.message : String(err) }, lang)
4671
+ );
4672
+ }
4673
+ sendJson(res, 200, { asked: true });
4674
+ }
4326
4675
  // src/routes.ts
4327
4676
  var DENY_KEY = {
4328
4677
  "missing-credential": "web.route.deny_missing_credential",
@@ -4330,10 +4679,17 @@ var DENY_KEY = {
4330
4679
  "bad-origin": "web.route.deny_bad_origin",
4331
4680
  "bad-host": "web.route.deny_bad_host"
4332
4681
  };
4682
+ var log = createLogger("server-route");
4333
4683
  function createRequestHandler(opts) {
4334
4684
  return (req, res) => {
4335
4685
  const lang = langOf(req.url);
4336
- dispatch(opts, req, res, lang).catch(() => {
4686
+ dispatch(opts, req, res, lang).catch((err) => {
4687
+ log.error("unhandled handler exception", {
4688
+ method: req.method ?? "",
4689
+ path: (req.url ?? "").split("?")[0],
4690
+ error: err instanceof Error ? err.message : String(err),
4691
+ stack: err instanceof Error ? err.stack : void 0
4692
+ });
4337
4693
  if (!res.headersSent) {
4338
4694
  sendError(res, 500, "internal", t("web.route.internal_error", void 0, lang));
4339
4695
  } else res.destroy();
@@ -4418,6 +4774,9 @@ async function handleApi(opts, req, res, method, segments, lang) {
4418
4774
  if (resource === "skills" && first === "import" && second === void 0 && method === "POST") {
4419
4775
  return importSkills(ctx, req, res, lang);
4420
4776
  }
4777
+ if (resource === "skills" && first === "stage" && second === void 0 && method === "POST") {
4778
+ return stageSkillDrop(ctx, req, res, lang);
4779
+ }
4421
4780
  if (resource === "skills" && first === "remove" && second === void 0 && method === "POST") {
4422
4781
  return removeSkill(ctx, req, res, lang);
4423
4782
  }
@@ -4457,6 +4816,12 @@ async function handleApi(opts, req, res, method, segments, lang) {
4457
4816
  if (resource === "plugins" && first === "upload" && second === void 0 && method === "POST") {
4458
4817
  return uploadPlugin(ctx, req, res, lang);
4459
4818
  }
4819
+ if (resource === "plugins" && first === "refresh" && second === void 0 && method === "POST") {
4820
+ return refreshPlugins(ctx, res, lang);
4821
+ }
4822
+ if (resource === "plugins" && first === "stage" && second === void 0 && method === "POST") {
4823
+ return stagePlugin(ctx, req, res, lang);
4824
+ }
4460
4825
  if (resource === "providers" && first === void 0 && isRead(method)) {
4461
4826
  return listProviders(ctx, res, lang);
4462
4827
  }
@@ -4538,6 +4903,9 @@ async function handleSessions(opts, req, res, method, sessionId, action, detail,
4538
4903
  if (method === "POST") return postMessage(ctx, req, res, sessionId, lang);
4539
4904
  return methodNotAllowed(res, "GET, POST", lang);
4540
4905
  }
4906
+ if (action === "attachments" && method === "POST") {
4907
+ return uploadAttachment(ctx, req, res, sessionId, lang);
4908
+ }
4541
4909
  if (action === "abort" && method === "POST") return abortSession(ctx, res, sessionId, lang);
4542
4910
  if (action === "reference-candidates" && isRead(method)) {
4543
4911
  return listReferenceCandidates(ctx, res, sessionId, req.url, lang);
@@ -4596,6 +4964,12 @@ async function handleSessions(opts, req, res, method, sessionId, action, detail,
4596
4964
  if (action === "permission" && method === "POST") {
4597
4965
  return setPermission(ctx, req, res, sessionId, lang);
4598
4966
  }
4967
+ if (action === "trust-request" && method === "POST") {
4968
+ return requestTrust(ctx, sessionId, req, res, lang);
4969
+ }
4970
+ if (action === "workspaces" && method === "POST" && detail === "forget") {
4971
+ return forgetWorkspace(ctx, sessionId, req, res, lang);
4972
+ }
4599
4973
  if (action === "approval-cache" && method === "POST") {
4600
4974
  return revokeApprovalCache(ctx, req, res, sessionId, lang);
4601
4975
  }
@@ -4686,6 +5060,9 @@ async function createWebServer(opts) {
4686
5060
  onCooled: (sessionId) => opts.runtime.sessionFactory.release(sessionId)
4687
5061
  });
4688
5062
  if (opts.runtime.session) hub.register(opts.runtime.session);
5063
+ const unsubscribeTasks = observeBackgroundTasks((sessionId) => {
5064
+ hub.notifyTasksChanged(sessionId);
5065
+ });
4689
5066
  let handle = null;
4690
5067
  const server = createServer((req, res) => {
4691
5068
  if (handle) return handle(req, res);
@@ -4694,7 +5071,10 @@ async function createWebServer(opts) {
4694
5071
  });
4695
5072
  const webRoot = opts.webRoot ?? defaultWebRoot();
4696
5073
  const listened = await listen(server, binding.host, binding.port);
4697
- if (!listened.ok) return listened;
5074
+ if (!listened.ok) {
5075
+ unsubscribeTasks();
5076
+ return listened;
5077
+ }
4698
5078
  handle = buildHandler(opts, hub, binding, listened.port, webRoot);
4699
5079
  return {
4700
5080
  ok: true,
@@ -4704,9 +5084,9 @@ async function createWebServer(opts) {
4704
5084
  host: binding.host,
4705
5085
  token: binding.token,
4706
5086
  ...webRoot === void 0 ? {} : { webRoot },
4707
- observe: (sink) => hub.observe(sink),
5087
+ observe: (sink, opts2) => hub.observe(sink, opts2 ?? {}),
4708
5088
  snapshot: () => hub.listSessions(),
4709
- close: makeCloser(server, hub, opts.runtime)
5089
+ close: makeCloser(server, hub, opts.runtime, unsubscribeTasks)
4710
5090
  }
4711
5091
  };
4712
5092
  }
@@ -4724,7 +5104,8 @@ function buildHandler(opts, hub, binding, port, webRoot) {
4724
5104
  artifactsRoot: opts.artifactsRoot ?? opts.runtime.artifactsRoot,
4725
5105
  lanExposed: binding.lanExposed,
4726
5106
  nativeDirPicker: resolveNativeDirPicker(machineFacts),
4727
- sameMachine: isSameMachine(machineFacts)
5107
+ sameMachine: isSameMachine(machineFacts),
5108
+ ...opts.onTrustRequest === void 0 ? {} : { trustRequest: opts.onTrustRequest }
4728
5109
  },
4729
5110
  guard: createAuthGuard({ token: binding.token, port, lanExposed: binding.lanExposed }),
4730
5111
  ...webRoot === void 0 ? {} : { webRoot },
@@ -4757,10 +5138,11 @@ function describeListenError(err, host, port) {
4757
5138
  }
4758
5139
  return t("web.listen_failed", { host, port, reason: err.message }, void 0);
4759
5140
  }
4760
- function makeCloser(server, hub, runtime) {
5141
+ function makeCloser(server, hub, runtime, unsubscribeTasks) {
4761
5142
  let closing = null;
4762
5143
  return () => {
4763
5144
  closing ??= (async () => {
5145
+ unsubscribeTasks();
4764
5146
  hub.shutdown();
4765
5147
  server.closeAllConnections();
4766
5148
  await new Promise((resolve4) => server.close(() => resolve4()));