@lelouchhe/webagent 0.9.0 → 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.
Files changed (47) hide show
  1. package/README.md +39 -14
  2. package/config.toml +7 -27
  3. package/dist/index.html +4 -4
  4. package/dist/js/app.INIQQEGD.js +5 -0
  5. package/dist/js/{chunk.S5LRNRJI.js → chunk.7WADDFJZ.js} +27 -27
  6. package/dist/js/viewer.RHZMFYWJ.js +1 -0
  7. package/dist/login.html +1 -1
  8. package/dist/share-viewer.html +5 -5
  9. package/dist/{styles.00nlhhf3.css → styles.01aj0l37.css} +19 -2
  10. package/dist/sw.js +6 -6
  11. package/lib/attachment-dispatch.js +60 -31
  12. package/lib/attachment-interceptor.js +7 -7
  13. package/lib/attachment-labels.js +1 -1
  14. package/lib/attachments.js +25 -0
  15. package/lib/auth-middleware.js +2 -2
  16. package/lib/auth.js +2 -2
  17. package/lib/bridge.js +109 -83
  18. package/lib/client-registry.js +12 -12
  19. package/lib/config.js +2 -31
  20. package/lib/event-handler.js +143 -90
  21. package/lib/files/routes.js +1 -1
  22. package/lib/mcp/capability.js +74 -0
  23. package/lib/mcp/server.js +148 -0
  24. package/lib/mcp/task-history.js +245 -0
  25. package/lib/mcp/task-host.js +253 -0
  26. package/lib/mcp/tools.js +168 -0
  27. package/lib/mode-bucket.js +1 -1
  28. package/lib/push-service.js +33 -35
  29. package/lib/routes.js +947 -489
  30. package/lib/server.js +64 -16
  31. package/lib/share/routes.js +88 -88
  32. package/lib/shared/task-reference.js +20 -0
  33. package/lib/sse-manager.js +8 -8
  34. package/lib/store.js +941 -314
  35. package/lib/task-collaboration.js +15 -0
  36. package/lib/task-manager.js +1409 -0
  37. package/lib/task-path.js +131 -0
  38. package/lib/{session-state.js → task-state.js} +64 -41
  39. package/lib/task-tree-lock.js +74 -0
  40. package/lib/{sessions-anchor.js → tasks-anchor.js} +8 -7
  41. package/lib/tokens.js +1 -1
  42. package/lib/types.js +2 -2
  43. package/package.json +7 -1
  44. package/dist/js/app.QC7IRDTP.js +0 -5
  45. package/dist/js/viewer.GP5VXAUY.js +0 -1
  46. package/lib/session-manager.js +0 -638
  47. package/lib/title-service.js +0 -95
@@ -0,0 +1 @@
1
+ import{c as g,e as h,n as y,p as T,q as C}from"./chunk.7WADDFJZ.js";import{a as p}from"./chunk.3CLGCUHW.js";import"./chunk.AJZBJBMO.js";var $=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function f(t){return String(t).padStart(2,"0")}function E(t,n){if(!t)return"";let r=new Date(t),i=r.getTime();if(isNaN(i))return"";let l=Math.floor((n.getTime()-i)/1e3);if(l<60)return"just now";let c=Math.floor(l/60);if(c<60)return`${c}m ago`;let o=Math.floor(c/60);if(o<24)return`${o}h ago`;let e=Math.floor(o/24);if(e<7)return`${e}d ago`;let m=r.getUTCFullYear()===n.getUTCFullYear(),s=$[r.getUTCMonth()],a=r.getUTCDate();return m?`${s} ${a}`:`${s} ${a}, ${r.getUTCFullYear()}`}function _(t){if(!t)return"";let n=new Date(t);return isNaN(n.getTime())?"":`${n.getUTCFullYear()}-${f(n.getUTCMonth()+1)}-${f(n.getUTCDate())} ${f(n.getUTCHours())}:${f(n.getUTCMinutes())} UTC`}function S(t){let n=/^\/api\/v1\/tasks\/[^/]+\/attachments\/([A-Za-z0-9._-]+)(?:\?.*)?$/;return r=>{let i=n.exec(r);return i?`/s/${encodeURIComponent(t)}/attachments/${i[1]}`:r}}function w(t){if(typeof t.data=="string")try{return JSON.parse(t.data)}catch{return{}}return t.data}function x(t,n,r,i){if(t.type!=="assistant_message"||n?.type!=="assistant_message"||t.seq!==n.seq+1||!r?.classList.contains("assistant"))return!1;let l=(r.getAttribute("data-raw")??"")+(typeof i.text=="string"?i.text:"");return r.setAttribute("data-raw",l),C(r,l,void 0,!0),g(r),!0}function v(t,n,r){let i=new Set;for(let s of t)if(s.type==="permission_response")try{let a=JSON.parse(s.data);typeof a.requestId=="string"&&i.add(a.requestId)}catch{}let l=new Map,c=new Map,o=null,e=null,m={rewriteAttachmentSrc:S(r),enhanceMarkdown:g,findToolCallEl:s=>l.get(s)??null,findPermissionEl:s=>c.get(s)??null,findBashEl:()=>o,isPermissionResolved:s=>i.has(s),finalAnswerToolText:null};for(let s=0;s<t.length;s++){let a=t[s];if(!y(a.type))continue;let u=w(a),A=s>0?t[s-1]:null,M=n.lastElementChild;if(x(a,A,M,u)){e=null;continue}m.finalAnswerToolText=a.type==="assistant_message"?e:null;let d=T(a.type,u,m);d?(n.appendChild(d),a.type==="tool_call"&&typeof u.id=="string"?l.set(u.id,d):a.type==="permission_request"&&typeof u.requestId=="string"?c.set(u.requestId,d):a.type==="bash_command"&&(o=d)):a.type==="bash_result"&&(o=null),a.type==="tool_call_update"?e=h(typeof u.status=="string"?u.status:"",Array.isArray(u.content)?u.content:void 0):e=null}}async function k(){try{let e=localStorage.getItem("theme")??"auto";document.documentElement.setAttribute("data-theme",e)}catch{}let t=/^\/s\/([A-Za-z0-9_-]{24})(?:[/?#]|$)/.exec(location.pathname);if(!t){document.body.textContent="invalid share URL";return}let n=t[1],r=document.getElementById("task-info"),i=document.getElementById("messages"),l=document.querySelector(".share-footer-author"),c=document.querySelector(".share-footer-meta");if(!i)return;let o;try{let e=await fetch(`/api/v1/shared/${encodeURIComponent(n)}/events`,{headers:{Accept:"application/json"},credentials:"omit"});if(e.status===p.GONE){document.body.innerHTML="<div class='share-gone'>\u6B64\u94FE\u63A5\u5DF2\u64A4\u9500\u6216\u8FC7\u671F\u3002</div>";return}if(!e.ok){document.body.textContent=`error ${e.status}`;return}o=await e.json()}catch(e){document.body.textContent=`failed to load: ${String(e)}`;return}if(document.title=o.share.task_title?`${o.share.task_title} \u2014 shared`:"shared task",r&&(r.textContent=o.share.task_title??"(untitled)"),l){let e=o.share.display_name??"";l.textContent=e?`by ${e}`:""}if(v(o.events,i,n),c){let e=o.share.shared_at??o.share.created_at;c.textContent=E(e,new Date),c instanceof HTMLElement&&(c.title=_(e))}}k();
package/dist/login.html CHANGED
@@ -17,7 +17,7 @@
17
17
  media="(prefers-color-scheme: light)"
18
18
  />
19
19
  <script src="/theme-init.js"></script>
20
- <link rel="stylesheet" href="/styles.00nlhhf3.css" />
20
+ <link rel="stylesheet" href="/styles.01aj0l37.css" />
21
21
  </head>
22
22
  <body class="login-page">
23
23
  <div class="login-card">
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>shared session — >_</title>
6
+ <title>shared task — >_</title>
7
7
  <link rel="icon" href="/icons/icon.svg" type="image/svg+xml" />
8
8
  <meta
9
9
  name="theme-color"
@@ -16,9 +16,9 @@
16
16
  media="(prefers-color-scheme: light)"
17
17
  />
18
18
  <meta name="robots" content="noindex, nofollow" />
19
- <link rel="stylesheet" href="/s/_/styles.00nlhhf3.css" />
19
+ <link rel="stylesheet" href="/s/_/styles.01aj0l37.css" />
20
20
  <link rel="stylesheet" href="/s/_/share-viewer.00gubshk.css" />
21
- <link rel="modulepreload" href="/s/_/chunk.S5LRNRJI.js">
21
+ <link rel="modulepreload" href="/s/_/chunk.7WADDFJZ.js">
22
22
  <link rel="modulepreload" href="/s/_/chunk.3CLGCUHW.js">
23
23
  <link rel="modulepreload" href="/s/_/chunk.AJZBJBMO.js">
24
24
  </head>
@@ -28,7 +28,7 @@
28
28
  <span class="logo">>_</span>
29
29
  <span class="share-badge">shared</span>
30
30
  </div>
31
- <span id="session-info" class="status"></span>
31
+ <span id="task-info" class="status"></span>
32
32
  <div class="header-side header-right">
33
33
  <button id="theme-btn" title="Toggle theme">◑</button>
34
34
  </div>
@@ -49,6 +49,6 @@
49
49
  >
50
50
  </div>
51
51
  </footer>
52
- <script type="module" src="/s/_/viewer.GP5VXAUY.js"></script>
52
+ <script type="module" src="/s/_/viewer.RHZMFYWJ.js"></script>
53
53
  </body>
54
54
  </html>
@@ -5,6 +5,7 @@
5
5
  --text: #c9d1d9;
6
6
  --text-dim: #6e7681;
7
7
  --accent: #58a6ff;
8
+ --system-accent: #bc8cff;
8
9
  --green: #3fb950;
9
10
  --yellow: #d29922;
10
11
  --red: #f85149;
@@ -25,6 +26,7 @@
25
26
  --text: #1f2328;
26
27
  --text-dim: #656d76;
27
28
  --accent: #0969da;
29
+ --system-accent: #8250df;
28
30
  --green: #1a7f37;
29
31
  --yellow: #9a6700;
30
32
  --red: #cf222e;
@@ -43,6 +45,7 @@
43
45
  --text: #1f2328;
44
46
  --text-dim: #656d76;
45
47
  --accent: #0969da;
48
+ --system-accent: #8250df;
46
49
  --green: #1a7f37;
47
50
  --yellow: #9a6700;
48
51
  --red: #cf222e;
@@ -157,7 +160,7 @@ body {
157
160
  opacity: 1;
158
161
  }
159
162
  }
160
- #session-info {
163
+ #task-info {
161
164
  overflow: hidden;
162
165
  text-overflow: ellipsis;
163
166
  white-space: nowrap;
@@ -191,7 +194,7 @@ body {
191
194
  .header-right {
192
195
  margin-left: auto;
193
196
  }
194
- #session-info {
197
+ #task-info {
195
198
  flex: 1 1 auto;
196
199
  text-align: left;
197
200
  }
@@ -428,6 +431,20 @@ body {
428
431
  color: var(--text-dim);
429
432
  white-space: pre-wrap;
430
433
  }
434
+ .system-msg.expandable {
435
+ padding-top: 4px;
436
+ padding-bottom: 4px;
437
+ border-left: 2px solid var(--system-accent);
438
+ }
439
+ .system-msg.expandable summary {
440
+ cursor: pointer;
441
+ color: var(--system-accent);
442
+ }
443
+ .system-msg-body {
444
+ margin: 4px 0 2px 16px;
445
+ color: var(--text);
446
+ white-space: pre-wrap;
447
+ }
431
448
  .system-msg a {
432
449
  color: var(--accent);
433
450
  text-decoration: underline;
package/dist/sw.js CHANGED
@@ -31,7 +31,7 @@ self.addEventListener("push", (e) => {
31
31
 
32
32
  // Backward-compat: older payloads had no `kind`. Treat as notify.
33
33
  const { title, body, tag, data } = payload;
34
- const finalTag = tag || data?.sessionId || "default";
34
+ const finalTag = tag || data?.taskId || "default";
35
35
  e.waitUntil(showNotify(title, body, finalTag, data));
36
36
  });
37
37
 
@@ -79,15 +79,15 @@ self.addEventListener("notificationclick", (e) => {
79
79
  e.notification.close();
80
80
 
81
81
  const data = e.notification.data || {};
82
- const sessionId = data.sessionId;
82
+ const taskId = data.taskId;
83
83
  const messageId = data.messageId;
84
- const target = sessionId
85
- ? { type: "navigate", sessionId }
84
+ const target = taskId
85
+ ? { type: "navigate", taskId }
86
86
  : messageId
87
87
  ? { type: "navigate", messageId }
88
88
  : { type: "navigate" };
89
- const targetUrl = sessionId
90
- ? `/#${encodeURIComponent(sessionId)}`
89
+ const targetUrl = taskId
90
+ ? `/#${encodeURIComponent(taskId)}`
91
91
  : messageId
92
92
  ? `/?message=${encodeURIComponent(messageId)}`
93
93
  : "/";
@@ -1,33 +1,38 @@
1
1
  import { readFile, realpath } from "node:fs/promises";
2
2
  import { pathToFileURL } from "node:url";
3
- import { isInsideSessionAttachments } from "./sessions-anchor.js";
3
+ import { isUpstreamImageMime } from "./attachments.js";
4
+ import { isInsideTaskAttachments } from "./tasks-anchor.js";
4
5
  const NOOP_LOGGER = { warn: () => { } };
5
6
  /**
6
- * Builds the ACP prompt block for one client-supplied attachment. Returns
7
+ * Builds the ACP prompt blocks for one client-supplied attachment. Returns
7
8
  * a fallback text block on any failure (DB miss, disk miss, anchor breach,
8
- * cross-session reference) so the prompt turn never gets stuck in a retry
9
+ * cross-task reference) so the prompt turn never gets stuck in a retry
9
10
  * loop just because one image vanished.
10
11
  *
11
12
  * Trust boundary (decision 10 in uploads-plan v2.6): client only supplies
12
13
  * `attachmentId`. Everything else (realpath, anchor, MIME for read) comes
13
- * from the server-side row.
14
+ * from the server-side row — including whether the payload is an image and
15
+ * whether its mime is one the upstream model accepts. A client cannot
16
+ * change the wire format by mislabelling the attachment.
14
17
  */
15
18
  export class AttachmentDispatcher {
16
19
  store;
17
- sessionsAnchor;
20
+ tasksAnchor;
18
21
  logger;
19
- constructor(store, sessionsAnchor, logger = NOOP_LOGGER) {
22
+ constructor(store, tasksAnchor, logger = NOOP_LOGGER) {
20
23
  this.store = store;
21
- this.sessionsAnchor = sessionsAnchor;
24
+ this.tasksAnchor = tasksAnchor;
22
25
  this.logger = logger;
23
26
  }
24
- async dispatch(sessionId, ref) {
27
+ async dispatch(taskId, ref) {
25
28
  const fallback = (reason) => {
26
- this.logger.warn(`[attachments] dispatch fallback (${reason}) for ${sessionId}/${ref.attachmentId}`);
27
- return {
28
- type: "text",
29
- text: `[attachment removed: ${ref.displayName}]`,
30
- };
29
+ this.logger.warn(`[attachments] dispatch fallback (${reason}) for ${taskId}/${ref.attachmentId}`);
30
+ return [
31
+ {
32
+ type: "text",
33
+ text: `[attachment removed: ${ref.displayName}]`,
34
+ },
35
+ ];
31
36
  };
32
37
  // Reject any client trying to smuggle a uri / data / path. The shape
33
38
  // of AttachmentRef already forbids these statically; this guard is a
@@ -38,18 +43,18 @@ export class AttachmentDispatcher {
38
43
  typeof wider.path === "string") {
39
44
  return fallback("client_supplied_external_field");
40
45
  }
41
- const row = this.store.getAttachment(sessionId, ref.attachmentId);
46
+ const row = this.store.getAttachment(taskId, ref.attachmentId);
42
47
  if (!row)
43
48
  return fallback("row_not_found");
44
- // Cross-session reference — the row exists but for a DIFFERENT session.
45
- // store.getAttachment scopes by session_id so this should already be
49
+ // Cross-task reference — the row exists but for a DIFFERENT task.
50
+ // store.getAttachment scopes by task_id so this should already be
46
51
  // caught by row_not_found, but assert defensively.
47
- if (row.session_id !== sessionId) {
48
- return fallback("cross_session");
52
+ if (row.task_id !== taskId) {
53
+ return fallback("cross_task");
49
54
  }
50
55
  // Anchor check on the stored realpath. If the file was moved out from
51
56
  // under us, or a future bug let an attacker inject a row with a path
52
- // outside SESSIONS_ANCHOR/<sid>/attachments/, we MUST refuse to dispatch
57
+ // outside TASKS_ANCHOR/<sid>/attachments/, we MUST refuse to dispatch
53
58
  // it as a `file://` URI — the agent would happily read it.
54
59
  let resolvedPath;
55
60
  try {
@@ -58,24 +63,48 @@ export class AttachmentDispatcher {
58
63
  catch {
59
64
  return fallback("realpath_failed");
60
65
  }
61
- if (!isInsideSessionAttachments(this.sessionsAnchor, sessionId, resolvedPath)) {
66
+ if (!isInsideTaskAttachments(this.tasksAnchor, taskId, resolvedPath)) {
62
67
  return fallback("path_outside_anchor");
63
68
  }
64
- if (ref.kind === "image") {
65
- try {
66
- const buf = await readFile(resolvedPath);
67
- return {
68
- type: "image",
69
- data: buf.toString("base64"),
70
- mimeType: row.mime,
71
- };
72
- }
73
- catch {
74
- return fallback("read_failed");
69
+ // Storage truth, not the client's `ref.kind`: the browser guesses the
70
+ // mime from the file extension / OS registry (renaming IMG_1040.HEIC to
71
+ // .jpg makes it claim image/jpeg), while `row.kind` is what the sniff on
72
+ // upload produced.
73
+ if (row.kind === "image") {
74
+ if (isUpstreamImageMime(row.mime)) {
75
+ try {
76
+ const buf = await readFile(resolvedPath);
77
+ return [
78
+ {
79
+ type: "image",
80
+ data: buf.toString("base64"),
81
+ mimeType: row.mime,
82
+ },
83
+ ];
84
+ }
85
+ catch {
86
+ return fallback("read_failed");
87
+ }
75
88
  }
89
+ // Image container the upstream model rejects (heic/heif/avif/bmp/
90
+ // tiff and other `image/*` containers — note an SVG sniffs as text,
91
+ // so it never reaches this branch). Sending it as an image block
92
+ // poisons the session history: the provider 400s on every later turn
93
+ // too. Degrade to a file link plus a hint instead.
94
+ this.logger.warn(`[attachments] image downgraded to resource_link (mime=${row.mime}) for ${taskId}/${ref.attachmentId}`);
95
+ return [
96
+ {
97
+ type: "text",
98
+ text: `[attachment ${row.name} (${row.mime}) cannot be read as an inline image by the model; convert it to png or jpeg to make it readable]`,
99
+ },
100
+ this.resourceLink(row, resolvedPath),
101
+ ];
76
102
  }
77
103
  // kind === "file" → ACP resource_link with file:// URI built from the
78
104
  // realpath (NOT from any client-supplied string).
105
+ return [this.resourceLink(row, resolvedPath)];
106
+ }
107
+ resourceLink(row, resolvedPath) {
79
108
  return {
80
109
  type: "resource_link",
81
110
  uri: pathToFileURL(resolvedPath).toString(),
@@ -2,12 +2,12 @@
2
2
  //
3
3
  // Plan §1.4 (uploads-plan v2.6, lines 105-183). When an agent requests
4
4
  // permission to *read* a path that we know is one of the user-uploaded
5
- // session attachments, auto-approve with `allow_once`. Any deviation
5
+ // task attachments, auto-approve with `allow_once`. Any deviation
6
6
  // from the strict allowlist falls through to the user prompt.
7
7
  //
8
8
  // Defenses (mirrored from the plan):
9
9
  // F1 kind === "read" only
10
- // F2 every locations[].path realpath ∈ session attachment realpaths
10
+ // F2 every locations[].path realpath ∈ task attachment realpaths
11
11
  // F4 schema gate — locations must exist; if rawInput has known path
12
12
  // keys they must also realpath into the attachment set; if it has
13
13
  // none, allow but bump schemaDrift counter so we notice when the
@@ -67,7 +67,7 @@ async function checkRawInputPaths(raw, attachmentRealpaths, miss) {
67
67
  }
68
68
  /**
69
69
  * Returns true iff the request matches the strict
70
- * "agent reading a known session attachment" pattern.
70
+ * "agent reading a known task attachment" pattern.
71
71
  *
72
72
  * Returning false does NOT deny the user prompt — it just declines to
73
73
  * auto-approve, so the normal permission UI continues to render.
@@ -79,7 +79,7 @@ export async function shouldAutoApproveAttachmentRead(ev, deps) {
79
79
  counters.fellThrough++;
80
80
  log.debug?.("attachment auto-allow miss", {
81
81
  reason,
82
- sessionId: ev.sessionId,
82
+ taskId: ev.taskId,
83
83
  toolKind: ev.toolKind,
84
84
  toolName: ev.toolName,
85
85
  });
@@ -96,11 +96,11 @@ export async function shouldAutoApproveAttachmentRead(ev, deps) {
96
96
  }
97
97
  let attachmentRealpaths;
98
98
  try {
99
- attachmentRealpaths = new Set(deps.listAttachmentRealpaths(ev.sessionId));
99
+ attachmentRealpaths = new Set(deps.listAttachmentRealpaths(ev.taskId));
100
100
  }
101
101
  catch (e) {
102
102
  log.warn?.("attachment interceptor db error", {
103
- sessionId: ev.sessionId,
103
+ taskId: ev.taskId,
104
104
  error: e.message,
105
105
  });
106
106
  return miss("db_error");
@@ -121,7 +121,7 @@ export async function shouldAutoApproveAttachmentRead(ev, deps) {
121
121
  }
122
122
  counters.autoAllowed++;
123
123
  log.info?.("attachment auto-allowed", {
124
- sessionId: ev.sessionId,
124
+ taskId: ev.taskId,
125
125
  toolKind: ev.toolKind,
126
126
  toolName: ev.toolName,
127
127
  locations: ev.locations.map((l) => l.path),
@@ -109,7 +109,7 @@ export function enrichStoredEventDataForDisplay(type, data, map) {
109
109
  return data;
110
110
  }
111
111
  // Synthesize a minimal event shape so we can reuse the object-level
112
- // enricher. `sessionId` etc. don't matter to enrich logic.
112
+ // enricher. `taskId` etc. don't matter to enrich logic.
113
113
  const ev = { type, ...parsed };
114
114
  const out = enrichEventForDisplay(ev, map);
115
115
  if (out === ev)
@@ -16,6 +16,11 @@ const MIME_TO_EXT = {
16
16
  "image/jpeg": "jpg",
17
17
  "image/gif": "gif",
18
18
  "image/webp": "webp",
19
+ "image/heic": "heic",
20
+ "image/heif": "heif",
21
+ "image/avif": "avif",
22
+ "image/bmp": "bmp",
23
+ "image/tiff": "tif",
19
24
  "image/svg+xml": "svg",
20
25
  "application/pdf": "pdf",
21
26
  "text/plain": "txt",
@@ -130,6 +135,26 @@ const INLINE_MIMES = new Set([
130
135
  export function isInlineMime(mime) {
131
136
  return INLINE_MIMES.has(mime.toLowerCase());
132
137
  }
138
+ /**
139
+ * Image mimes the upstream model accepts as inline ACP image blocks.
140
+ *
141
+ * This is deliberately a SEPARATE set from INLINE_MIMES even though the two
142
+ * currently hold the same values: INLINE_MIMES is a display/XSS policy (which
143
+ * mimes may be served `Content-Disposition: inline` in the browser), while
144
+ * UPSTREAM_IMAGE_MIMES is an upstream compatibility policy (which image
145
+ * containers the model provider accepts on the wire). They answer different
146
+ * questions and will diverge — e.g. upstream support for HEIC would change
147
+ * only this set. Never reference one from the other's check.
148
+ */
149
+ const UPSTREAM_IMAGE_MIMES = new Set([
150
+ "image/png",
151
+ "image/jpeg",
152
+ "image/gif",
153
+ "image/webp",
154
+ ]);
155
+ export function isUpstreamImageMime(mime) {
156
+ return UPSTREAM_IMAGE_MIMES.has(mime.toLowerCase());
157
+ }
133
158
  /**
134
159
  * Classify an upload as "image" (sent as ACP image block, inlined as base64)
135
160
  * or "file" (sent as ACP resource_link). Decision is mime-prefix-based —
@@ -27,14 +27,14 @@ const WHITELIST = [
27
27
  { method: "GET", test: (p) => p === "/api/v1/events/stream" },
28
28
  {
29
29
  method: "GET",
30
- test: (p) => /^\/api\/v1\/sessions\/[A-Za-z0-9_-]+\/events\/stream$/.test(p),
30
+ test: (p) => /^\/api\/v1\/tasks\/[A-Za-z0-9_-]+\/events\/stream$/.test(p),
31
31
  },
32
32
  // Image GETs — authenticated via HMAC sig+exp query string (an <img>
33
33
  // tag cannot send Authorization headers). The image route handler does
34
34
  // its own verification before serving bytes.
35
35
  {
36
36
  method: "GET",
37
- test: (p) => /^\/api\/v1\/sessions\/[A-Za-z0-9_-]+\/attachments\/[A-Za-z0-9._-]+$/.test(p),
37
+ test: (p) => /^\/api\/v1\/tasks\/[A-Za-z0-9_-]+\/attachments\/[A-Za-z0-9._-]+$/.test(p),
38
38
  },
39
39
  // File viewer content — read-only bytes for one confirmed path,
40
40
  // authenticated via HMAC sig+exp query string for the same reason as
package/lib/auth.js CHANGED
@@ -75,12 +75,12 @@ export function verifyAttachmentSig(path, expRaw, sigHex, secret) {
75
75
  return timingSafeEqual(actual, expected);
76
76
  }
77
77
  /**
78
- * Rewrite every `/api/v1/sessions/:id/attachments/:file` URL inside a JSON-serialized
78
+ * Rewrite every `/api/v1/tasks/:id/attachments/:file` URL inside a JSON-serialized
79
79
  * payload to carry a fresh `?exp=&sig=`. Applied at egress (history GET, SSE
80
80
  * push) so a 1h-old stored URL is re-signed on the way out — the user can
81
81
  * reload history days later and images still resolve.
82
82
  */
83
- const ATTACHMENT_URL_RE = /\/api\/v1\/sessions\/[A-Za-z0-9_-]+\/attachments\/[A-Za-z0-9._-]+(?:\?(?:exp=\d+&sig=[a-f0-9]+|sig=[a-f0-9]+&exp=\d+))?/g;
83
+ const ATTACHMENT_URL_RE = /\/api\/v1\/tasks\/[A-Za-z0-9_-]+\/attachments\/[A-Za-z0-9._-]+(?:\?(?:exp=\d+&sig=[a-f0-9]+|sig=[a-f0-9]+&exp=\d+))?/g;
84
84
  export function reSignAttachmentUrlsInJson(json, secret, ttlSeconds = 3600) {
85
85
  return json.replace(ATTACHMENT_URL_RE, (match) => {
86
86
  const basePath = match.split("?")[0];