@syengup/friday-channel-next 0.1.30 → 0.1.37

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 (154) hide show
  1. package/README.md +8 -4
  2. package/dist/index.js +1 -1
  3. package/dist/src/agent/abort-run.d.ts +12 -1
  4. package/dist/src/agent/abort-run.js +24 -9
  5. package/dist/src/agent/dispatch-bridge.d.ts +1 -1
  6. package/dist/src/agent/media-bridge.d.ts +8 -1
  7. package/dist/src/agent/media-bridge.js +23 -2
  8. package/dist/src/agent/node-pairing-bridge.d.ts +11 -8
  9. package/dist/src/agent/node-pairing-bridge.js +6 -2
  10. package/dist/src/agent/subagent-registry.js +0 -3
  11. package/dist/src/agent-forward-runtime.d.ts +15 -0
  12. package/dist/src/agent-forward-runtime.js +2 -0
  13. package/dist/src/agent-id.d.ts +8 -0
  14. package/dist/src/agent-id.js +21 -0
  15. package/dist/src/channel-actions.js +48 -15
  16. package/dist/src/channel.js +22 -3
  17. package/dist/src/collect-message-media-paths.js +10 -1
  18. package/dist/src/friday-session.js +34 -10
  19. package/dist/src/history/normalize-message.js +22 -8
  20. package/dist/src/http/handlers/agent-config.d.ts +27 -0
  21. package/dist/src/http/handlers/agent-config.js +188 -0
  22. package/dist/src/http/handlers/agent-files.d.ts +21 -0
  23. package/dist/src/http/handlers/agent-files.js +137 -0
  24. package/dist/src/http/handlers/agent-tools-catalog.d.ts +10 -0
  25. package/dist/src/http/handlers/agent-tools-catalog.js +33 -0
  26. package/dist/src/http/handlers/agents-list.js +1 -19
  27. package/dist/src/http/handlers/cancel.js +14 -6
  28. package/dist/src/http/handlers/device-approve.js +3 -1
  29. package/dist/src/http/handlers/files-download.js +6 -8
  30. package/dist/src/http/handlers/files.d.ts +16 -0
  31. package/dist/src/http/handlers/files.js +81 -13
  32. package/dist/src/http/handlers/health.js +18 -4
  33. package/dist/src/http/handlers/history-messages.js +1 -1
  34. package/dist/src/http/handlers/history-sessions.js +5 -3
  35. package/dist/src/http/handlers/messages.js +33 -14
  36. package/dist/src/http/handlers/models-list.d.ts +5 -0
  37. package/dist/src/http/handlers/models-list.js +9 -1
  38. package/dist/src/http/handlers/nodes-approve.js +1 -6
  39. package/dist/src/http/handlers/plugin-info.js +1 -1
  40. package/dist/src/http/handlers/sessions-settings.js +15 -10
  41. package/dist/src/http/server.js +27 -2
  42. package/dist/src/link-preview/og-parse.js +3 -1
  43. package/dist/src/link-preview/ssrf-guard.js +6 -2
  44. package/dist/src/media-fetch.js +4 -1
  45. package/dist/src/plugin-install-info.js +4 -1
  46. package/dist/src/session/session-manager.js +9 -3
  47. package/dist/src/session-usage-store.js +3 -1
  48. package/dist/src/skills-discovery.d.ts +59 -0
  49. package/dist/src/skills-discovery.js +252 -0
  50. package/dist/src/sse/offline-queue.js +4 -1
  51. package/dist/src/thinking-levels.d.ts +21 -0
  52. package/dist/src/thinking-levels.js +48 -0
  53. package/dist/src/tool-catalog.d.ts +53 -0
  54. package/dist/src/tool-catalog.js +191 -0
  55. package/dist/src/upgrade-runtime.d.ts +1 -1
  56. package/dist/src/version.js +4 -2
  57. package/index.ts +43 -35
  58. package/install.js +131 -43
  59. package/package.json +10 -1
  60. package/src/agent/abort-run.ts +23 -8
  61. package/src/agent/dispatch-bridge.ts +2 -1
  62. package/src/agent/media-bridge.test.ts +71 -0
  63. package/src/agent/media-bridge.ts +30 -1
  64. package/src/agent/node-pairing-bridge.ts +29 -15
  65. package/src/agent/run-usage-accumulator.ts +4 -2
  66. package/src/agent/subagent-registry.ts +0 -4
  67. package/src/agent-forward-runtime.ts +11 -0
  68. package/src/agent-id.ts +24 -0
  69. package/src/agent-run-context-bridge.ts +3 -1
  70. package/src/channel-actions.test.ts +57 -4
  71. package/src/channel-actions.ts +41 -15
  72. package/src/channel.lifecycle.test.ts +41 -0
  73. package/src/channel.outbound.test.ts +18 -4
  74. package/src/channel.ts +140 -120
  75. package/src/collect-message-media-paths.ts +15 -6
  76. package/src/config.ts +1 -4
  77. package/src/e2e/agents-list.e2e.test.ts +9 -2
  78. package/src/e2e/attachments-inbound.e2e.test.ts +5 -1
  79. package/src/e2e/attachments-outbound.e2e.test.ts +7 -2
  80. package/src/e2e/auto-approve.integration.test.ts +13 -7
  81. package/src/e2e/cancel-reconnect-errors.e2e.test.ts +18 -3
  82. package/src/e2e/connect-and-connected.e2e.test.ts +5 -1
  83. package/src/e2e/offline-replay.e2e.test.ts +17 -3
  84. package/src/e2e/send-text.e2e.test.ts +11 -2
  85. package/src/e2e/slash-commands.e2e.test.ts +5 -1
  86. package/src/e2e/status-cors-auth.e2e.test.ts +11 -2
  87. package/src/e2e/subagent-smoke.e2e.test.ts +68 -28
  88. package/src/e2e/subagent.e2e.test.ts +136 -53
  89. package/src/e2e/tool-lifecycle.e2e.test.ts +5 -1
  90. package/src/friday-session.forward-agent.test.ts +44 -12
  91. package/src/friday-session.ts +44 -20
  92. package/src/history/normalize-message.test.ts +35 -8
  93. package/src/history/normalize-message.ts +24 -12
  94. package/src/history/read-transcript.ts +1 -4
  95. package/src/http/handlers/agent-config.test.ts +212 -0
  96. package/src/http/handlers/agent-config.ts +232 -0
  97. package/src/http/handlers/agent-files.test.ts +136 -0
  98. package/src/http/handlers/agent-files.ts +149 -0
  99. package/src/http/handlers/agent-tools-catalog.ts +42 -0
  100. package/src/http/handlers/agents-list.test.ts +1 -5
  101. package/src/http/handlers/agents-list.ts +1 -22
  102. package/src/http/handlers/cancel.test.ts +23 -4
  103. package/src/http/handlers/cancel.ts +14 -6
  104. package/src/http/handlers/device-approve.test.ts +12 -3
  105. package/src/http/handlers/device-approve.ts +33 -21
  106. package/src/http/handlers/files-download.ts +17 -13
  107. package/src/http/handlers/files.test.ts +120 -0
  108. package/src/http/handlers/files.ts +115 -17
  109. package/src/http/handlers/health.test.ts +43 -11
  110. package/src/http/handlers/health.ts +22 -6
  111. package/src/http/handlers/history-messages.test.ts +51 -9
  112. package/src/http/handlers/history-messages.ts +4 -1
  113. package/src/http/handlers/history-sessions.test.ts +46 -9
  114. package/src/http/handlers/history-sessions.ts +5 -3
  115. package/src/http/handlers/history-set-title.test.ts +14 -5
  116. package/src/http/handlers/link-preview.test.ts +57 -16
  117. package/src/http/handlers/link-preview.ts +4 -1
  118. package/src/http/handlers/messages.test.ts +12 -8
  119. package/src/http/handlers/messages.ts +64 -21
  120. package/src/http/handlers/models-list.test.ts +114 -0
  121. package/src/http/handlers/models-list.ts +26 -8
  122. package/src/http/handlers/nodes-approve.test.ts +15 -4
  123. package/src/http/handlers/nodes-approve.ts +38 -40
  124. package/src/http/handlers/plugin-info.ts +5 -6
  125. package/src/http/handlers/plugin-upgrade.ts +4 -1
  126. package/src/http/handlers/sessions-settings.ts +16 -11
  127. package/src/http/handlers/sse.ts +3 -1
  128. package/src/http/server.ts +33 -6
  129. package/src/link-preview/og-parse.test.ts +6 -2
  130. package/src/link-preview/og-parse.ts +10 -3
  131. package/src/link-preview/preview-service.ts +4 -1
  132. package/src/link-preview/ssrf-guard.test.ts +78 -16
  133. package/src/link-preview/ssrf-guard.ts +7 -2
  134. package/src/media-fetch.test.ts +8 -3
  135. package/src/media-fetch.ts +5 -3
  136. package/src/openclaw.d.ts +41 -10
  137. package/src/plugin-install-info.ts +20 -9
  138. package/src/run-metadata.ts +2 -1
  139. package/src/session/session-manager.ts +19 -11
  140. package/src/session-usage-snapshot.ts +3 -1
  141. package/src/session-usage-store.ts +3 -1
  142. package/src/skills-discovery.test.ts +152 -0
  143. package/src/skills-discovery.ts +264 -0
  144. package/src/sse/emitter.test.ts +1 -1
  145. package/src/sse/emitter.ts +9 -3
  146. package/src/sse/offline-queue.ts +17 -8
  147. package/src/test-support/app-simulator.ts +17 -3
  148. package/src/test-support/mock-dispatch.ts +17 -4
  149. package/src/thinking-levels.test.ts +143 -0
  150. package/src/thinking-levels.ts +70 -0
  151. package/src/tool-catalog.ts +261 -0
  152. package/src/upgrade-runtime.ts +4 -2
  153. package/src/version.ts +6 -2
  154. package/tsconfig.json +1 -1
@@ -16,6 +16,9 @@ import { handleNodesApprove } from "./handlers/nodes-approve.js";
16
16
  import { handleSessionsSettings } from "./handlers/sessions-settings.js";
17
17
  import { handleModelsList } from "./handlers/models-list.js";
18
18
  import { handleAgentsList } from "./handlers/agents-list.js";
19
+ import { handleAgentConfig } from "./handlers/agent-config.js";
20
+ import { handleAgentFiles } from "./handlers/agent-files.js";
21
+ import { handleAgentToolsCatalog } from "./handlers/agent-tools-catalog.js";
19
22
  import { handleHistorySessions } from "./handlers/history-sessions.js";
20
23
  import { handleHistoryMessages } from "./handlers/history-messages.js";
21
24
  import { handleHistorySetTitle } from "./handlers/history-set-title.js";
@@ -31,10 +34,7 @@ import { getFridayNextRuntime } from "../runtime.js";
31
34
  import { sseEmitter } from "../sse/emitter.js";
32
35
 
33
36
  /** Route matcher - returns the matched handler or null. */
34
- async function handleFridayNextRoute(
35
- req: IncomingMessage,
36
- res: ServerResponse,
37
- ): Promise<boolean> {
37
+ async function handleFridayNextRoute(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
38
38
  const url = new URL(req.url ?? "/", "http://localhost");
39
39
  const pathname = url.pathname;
40
40
  applyCorsHeaders(res);
@@ -76,7 +76,10 @@ async function handleFridayNextRoute(
76
76
  return await handleNodesApprove(req, res);
77
77
  }
78
78
 
79
- if ((req.method === "PUT" || req.method === "GET") && pathname === "/friday-next/sessions/settings") {
79
+ if (
80
+ (req.method === "PUT" || req.method === "GET") &&
81
+ pathname === "/friday-next/sessions/settings"
82
+ ) {
80
83
  return await handleSessionsSettings(req, res);
81
84
  }
82
85
 
@@ -88,6 +91,27 @@ async function handleFridayNextRoute(
88
91
  return await handleAgentsList(req, res);
89
92
  }
90
93
 
94
+ // Routes: GET/PUT /friday-next/agents/{id}/config
95
+ // GET /friday-next/agents/{id}/files
96
+ // GET/PUT /friday-next/agents/{id}/files/{name}
97
+ if (pathname.startsWith("/friday-next/agents/")) {
98
+ const segs = pathname
99
+ .slice("/friday-next/agents/".length)
100
+ .split("/")
101
+ .filter(Boolean)
102
+ .map((s) => decodeURIComponent(s));
103
+ const [id, sub, name] = segs;
104
+ if (id && sub === "config" && segs.length === 2) {
105
+ return await handleAgentConfig(req, res, id);
106
+ }
107
+ if (id && sub === "files" && (segs.length === 2 || segs.length === 3)) {
108
+ return await handleAgentFiles(req, res, id, name);
109
+ }
110
+ if (id && sub === "tools" && name === "catalog" && segs.length === 3) {
111
+ return await handleAgentToolsCatalog(req, res, id);
112
+ }
113
+ }
114
+
91
115
  if (req.method === "GET" && pathname === "/friday-next/status") {
92
116
  return await handleStatus(req, res);
93
117
  }
@@ -103,7 +127,10 @@ async function handleFridayNextRoute(
103
127
  }
104
128
 
105
129
  // Route: PUT /friday-next/sessions/title (sync app session name → server displayName)
106
- if ((req.method === "PUT" || req.method === "POST") && pathname === "/friday-next/sessions/title") {
130
+ if (
131
+ (req.method === "PUT" || req.method === "POST") &&
132
+ pathname === "/friday-next/sessions/title"
133
+ ) {
107
134
  return await handleHistorySetTitle(req, res);
108
135
  }
109
136
 
@@ -5,7 +5,9 @@ const BASE = "https://example.com/article/42";
5
5
 
6
6
  describe("decodeHtmlEntities", () => {
7
7
  it("decodes named, decimal, and hex entities", () => {
8
- expect(decodeHtmlEntities("Tom &amp; Jerry &mdash; &quot;fun&quot;")).toBe('Tom & Jerry — "fun"');
8
+ expect(decodeHtmlEntities("Tom &amp; Jerry &mdash; &quot;fun&quot;")).toBe(
9
+ 'Tom & Jerry — "fun"',
10
+ );
9
11
  expect(decodeHtmlEntities("&#20013;&#25991;")).toBe("中文");
10
12
  expect(decodeHtmlEntities("&#x27;quoted&#x27;")).toBe("'quoted'");
11
13
  });
@@ -143,7 +145,9 @@ describe("parseOpenGraph", () => {
143
145
  it("extracts a cover image from inline JSON (extensionless, escaped slashes)", () => {
144
146
  const html = `<title>搜索资讯页</title>
145
147
  <script>window.__INFO__={"imgUrl":"http:\\/\\/qqpublic.qpic.cn\\/qq_public_cover\\/0\\/0-2342_op"}</script>`;
146
- expect(parseOpenGraph(html, BASE).imageUrl).toBe("http://qqpublic.qpic.cn/qq_public_cover/0/0-2342_op");
148
+ expect(parseOpenGraph(html, BASE).imageUrl).toBe(
149
+ "http://qqpublic.qpic.cn/qq_public_cover/0/0-2342_op",
150
+ );
147
151
  });
148
152
 
149
153
  it("standard og tags still win over body/json fallbacks", () => {
@@ -91,7 +91,9 @@ export function parseOpenGraph(html: string, baseUrl: string): OpenGraphResult {
91
91
  let metaDescription: string | null = null;
92
92
  for (const match of slice.matchAll(META_TAG_RE)) {
93
93
  const tag = match[0];
94
- const key = (attributeValue(tag, "property") ?? attributeValue(tag, "name"))?.trim().toLowerCase();
94
+ const key = (attributeValue(tag, "property") ?? attributeValue(tag, "name"))
95
+ ?.trim()
96
+ .toLowerCase();
95
97
  if (!key) continue;
96
98
  const content = attributeValue(tag, "content");
97
99
  if (content == null || !content.trim()) continue;
@@ -140,10 +142,15 @@ export function parseOpenGraph(html: string, baseUrl: string): OpenGraphResult {
140
142
  };
141
143
  }
142
144
 
143
- const JSON_LD_RE = /<script[^>]*type\s*=\s*["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
145
+ const JSON_LD_RE =
146
+ /<script[^>]*type\s*=\s*["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
144
147
 
145
148
  /** Extract title/description/image from JSON-LD blocks (schema.org Article/NewsArticle/etc.). */
146
- function parseJsonLd(html: string): { title: string | null; description: string | null; image: string | null } {
149
+ function parseJsonLd(html: string): {
150
+ title: string | null;
151
+ description: string | null;
152
+ image: string | null;
153
+ } {
147
154
  for (const match of html.matchAll(JSON_LD_RE)) {
148
155
  let data: unknown;
149
156
  try {
@@ -155,7 +155,10 @@ async function buildPreview(pageUrl: string): Promise<LinkPreviewResult> {
155
155
  }
156
156
 
157
157
  /** Re-host a favicon: try the parsed `<link rel icon>`, then `<origin>/favicon.ico`. */
158
- async function resolveFavicon(parsedIconUrl: string | null, finalUrl: string): Promise<string | null> {
158
+ async function resolveFavicon(
159
+ parsedIconUrl: string | null,
160
+ finalUrl: string,
161
+ ): Promise<string | null> {
159
162
  const candidates: string[] = [];
160
163
  if (parsedIconUrl) candidates.push(parsedIconUrl);
161
164
  try {
@@ -46,7 +46,9 @@ describe("parseHttpUrl", () => {
46
46
  describe("assertPublicHttpUrl", () => {
47
47
  it("rejects non-default ports", () => {
48
48
  expect(() => assertPublicHttpUrl(new URL("http://example.com:8080/"))).toThrow(BlockedUrlError);
49
- expect(() => assertPublicHttpUrl(new URL("https://example.com:8443/"))).toThrow(BlockedUrlError);
49
+ expect(() => assertPublicHttpUrl(new URL("https://example.com:8443/"))).toThrow(
50
+ BlockedUrlError,
51
+ );
50
52
  });
51
53
 
52
54
  it("allows default and explicit 80/443 ports", () => {
@@ -66,7 +68,7 @@ describe("assertPublicHttpUrl", () => {
66
68
  expect(() => assertPublicHttpUrl(new URL("http://127.0.0.1/"))).toThrow(BlockedUrlError);
67
69
  expect(() => assertPublicHttpUrl(new URL("http://10.0.0.5/"))).toThrow(BlockedUrlError);
68
70
  expect(() => assertPublicHttpUrl(new URL("http://[::1]/"))).toThrow(BlockedUrlError);
69
- expect(() => assertPublicHttpUrl(new URL("http://[fc00::1]/"))).toThrow(BlockedUrlError);
71
+ expect(() => assertPublicHttpUrl(new URL("http://[fd00::1]/"))).toThrow(BlockedUrlError);
70
72
  });
71
73
 
72
74
  it("allows public IP literals", () => {
@@ -95,7 +97,14 @@ describe("isPrivateAddress", () => {
95
97
  });
96
98
 
97
99
  it("passes public IPv4", () => {
98
- for (const ip of ["8.8.8.8", "93.184.216.34", "100.63.0.1", "100.128.0.1", "172.32.0.1", "198.20.0.1"]) {
100
+ for (const ip of [
101
+ "8.8.8.8",
102
+ "93.184.216.34",
103
+ "100.63.0.1",
104
+ "100.128.0.1",
105
+ "172.32.0.1",
106
+ "198.20.0.1",
107
+ ]) {
99
108
  expect(isPrivateAddress(ip), ip).toBe(false);
100
109
  }
101
110
  });
@@ -106,11 +115,24 @@ describe("isPrivateAddress", () => {
106
115
  });
107
116
 
108
117
  it("flags private/reserved IPv6 and mapped IPv4", () => {
109
- for (const ip of ["::1", "::", "fc00::1", "fd12:3456::1", "fe80::1", "::ffff:10.0.0.1", "::ffff:127.0.0.1"]) {
118
+ for (const ip of [
119
+ "::1",
120
+ "::",
121
+ "fd00::1",
122
+ "fd12:3456::1",
123
+ "fe80::1",
124
+ "::ffff:10.0.0.1",
125
+ "::ffff:127.0.0.1",
126
+ ]) {
110
127
  expect(isPrivateAddress(ip), ip).toBe(true);
111
128
  }
112
129
  });
113
130
 
131
+ it("passes fc00::/8 (fake-IP DNS v6 range used by clash/mihomo tun mode)", () => {
132
+ expect(isPrivateAddress("fc00::1e7")).toBe(false);
133
+ expect(isPrivateAddress("fc00::ffff:ffff")).toBe(false);
134
+ });
135
+
114
136
  it("passes public IPv6 and mapped public IPv4", () => {
115
137
  expect(isPrivateAddress("2606:2800:220:1:248:1893:25c8:1946")).toBe(false);
116
138
  expect(isPrivateAddress("::ffff:93.184.216.34")).toBe(false);
@@ -135,11 +157,15 @@ describe("assertResolvesPublic", () => {
135
157
  { address: "93.184.216.34", family: 4 },
136
158
  { address: "10.0.0.5", family: 4 },
137
159
  ] as never);
138
- await expect(assertResolvesPublic(new URL("https://rebind.example.com/"))).rejects.toThrow(BlockedUrlError);
160
+ await expect(assertResolvesPublic(new URL("https://rebind.example.com/"))).rejects.toThrow(
161
+ BlockedUrlError,
162
+ );
139
163
  });
140
164
 
141
165
  it("validates IP-literal hosts without a DNS lookup", async () => {
142
- await expect(assertResolvesPublic(new URL("http://127.0.0.1/"))).rejects.toThrow(BlockedUrlError);
166
+ await expect(assertResolvesPublic(new URL("http://127.0.0.1/"))).rejects.toThrow(
167
+ BlockedUrlError,
168
+ );
143
169
  await expect(assertResolvesPublic(new URL("http://93.184.216.34/"))).resolves.toBeUndefined();
144
170
  expect(lookupMock).not.toHaveBeenCalled();
145
171
  });
@@ -159,7 +185,13 @@ describe("fetchPublicUrl", () => {
159
185
  mockPublicDns();
160
186
  vi.stubGlobal(
161
187
  "fetch",
162
- vi.fn(async () => new Response("<html>hi</html>", { status: 200, headers: { "content-type": "text/html; charset=utf-8" } })),
188
+ vi.fn(
189
+ async () =>
190
+ new Response("<html>hi</html>", {
191
+ status: 200,
192
+ headers: { "content-type": "text/html; charset=utf-8" },
193
+ }),
194
+ ),
163
195
  );
164
196
  const result = await fetchPublicUrl("https://example.com/page", opts);
165
197
  expect(result?.finalUrl).toBe("https://example.com/page");
@@ -171,8 +203,15 @@ describe("fetchPublicUrl", () => {
171
203
  mockPublicDns();
172
204
  const fetchMock = vi
173
205
  .fn()
174
- .mockResolvedValueOnce(new Response(null, { status: 302, headers: { location: "https://other.example.com/final" } }))
175
- .mockResolvedValueOnce(new Response("ok", { status: 200, headers: { "content-type": "text/html" } }));
206
+ .mockResolvedValueOnce(
207
+ new Response(null, {
208
+ status: 302,
209
+ headers: { location: "https://other.example.com/final" },
210
+ }),
211
+ )
212
+ .mockResolvedValueOnce(
213
+ new Response("ok", { status: 200, headers: { "content-type": "text/html" } }),
214
+ );
176
215
  vi.stubGlobal("fetch", fetchMock);
177
216
  const result = await fetchPublicUrl("https://example.com/start", opts);
178
217
  expect(result?.finalUrl).toBe("https://other.example.com/final");
@@ -184,9 +223,14 @@ describe("fetchPublicUrl", () => {
184
223
  mockPublicDns();
185
224
  vi.stubGlobal(
186
225
  "fetch",
187
- vi.fn(async () => new Response(null, { status: 302, headers: { location: "http://127.0.0.1/admin" } })),
226
+ vi.fn(
227
+ async () =>
228
+ new Response(null, { status: 302, headers: { location: "http://127.0.0.1/admin" } }),
229
+ ),
230
+ );
231
+ await expect(fetchPublicUrl("https://example.com/start", opts)).rejects.toThrow(
232
+ BlockedUrlError,
188
233
  );
189
- await expect(fetchPublicUrl("https://example.com/start", opts)).rejects.toThrow(BlockedUrlError);
190
234
  });
191
235
 
192
236
  it("throws BlockedUrlError for a directly-blocked URL", async () => {
@@ -198,7 +242,13 @@ describe("fetchPublicUrl", () => {
198
242
  const big = "x".repeat(2048);
199
243
  vi.stubGlobal(
200
244
  "fetch",
201
- vi.fn(async () => new Response(big, { status: 200, headers: { "content-type": "text/html", "content-length": "10" } })),
245
+ vi.fn(
246
+ async () =>
247
+ new Response(big, {
248
+ status: 200,
249
+ headers: { "content-type": "text/html", "content-length": "10" },
250
+ }),
251
+ ),
202
252
  );
203
253
  expect(await fetchPublicUrl("https://example.com/big", opts)).toBeNull();
204
254
  });
@@ -207,16 +257,25 @@ describe("fetchPublicUrl", () => {
207
257
  mockPublicDns();
208
258
  vi.stubGlobal(
209
259
  "fetch",
210
- vi.fn(async () => new Response("{}", { status: 200, headers: { "content-type": "application/json" } })),
260
+ vi.fn(
261
+ async () =>
262
+ new Response("{}", { status: 200, headers: { "content-type": "application/json" } }),
263
+ ),
211
264
  );
212
265
  expect(
213
- await fetchPublicUrl("https://example.com/api", { ...opts, requireContentTypePrefixes: ["text/html", "application/xhtml+xml"] }),
266
+ await fetchPublicUrl("https://example.com/api", {
267
+ ...opts,
268
+ requireContentTypePrefixes: ["text/html", "application/xhtml+xml"],
269
+ }),
214
270
  ).toBeNull();
215
271
  });
216
272
 
217
273
  it("returns null on non-2xx and on DNS failure", async () => {
218
274
  mockPublicDns();
219
- vi.stubGlobal("fetch", vi.fn(async () => new Response("nope", { status: 404 })));
275
+ vi.stubGlobal(
276
+ "fetch",
277
+ vi.fn(async () => new Response("nope", { status: 404 })),
278
+ );
220
279
  expect(await fetchPublicUrl("https://example.com/missing", opts)).toBeNull();
221
280
 
222
281
  lookupMock.mockRejectedValue(new Error("ENOTFOUND"));
@@ -227,7 +286,10 @@ describe("fetchPublicUrl", () => {
227
286
  mockPublicDns();
228
287
  vi.stubGlobal(
229
288
  "fetch",
230
- vi.fn(async () => new Response(null, { status: 302, headers: { location: "https://example.com/loop" } })),
289
+ vi.fn(
290
+ async () =>
291
+ new Response(null, { status: 302, headers: { location: "https://example.com/loop" } }),
292
+ ),
231
293
  );
232
294
  expect(await fetchPublicUrl("https://example.com/loop", opts)).toBeNull();
233
295
  });
@@ -66,7 +66,8 @@ export function isPrivateAddress(ip: string): boolean {
66
66
 
67
67
  function isPrivateIPv4(ip: string): boolean {
68
68
  const parts = ip.split(".").map(Number);
69
- if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return true;
69
+ if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255))
70
+ return true;
70
71
  const [a, b] = parts;
71
72
  if (a === 0) return true; // 0.0.0.0/8
72
73
  if (a === 10) return true; // 10/8
@@ -90,7 +91,11 @@ function isPrivateIPv6(ip: string): boolean {
90
91
  if (mapped) return isPrivateIPv4(mapped[1]);
91
92
  if (lower === "::" || lower === "::1") return true; // unspecified / loopback
92
93
  const head = lower.split(":")[0];
93
- if (head.startsWith("fc") || head.startsWith("fd")) return true; // fc00::/7 ULA
94
+ // fc00::/8 (the reserved, never-assigned half of ULA fc00::/7) intentionally NOT blocked:
95
+ // RFC 4193 requires locally-assigned ULA to set the L bit, so real LAN services live in
96
+ // fd00::/8, while fake-IP DNS setups (mihomo/clash tun mode with IPv6, common on gateway
97
+ // hosts) resolve EVERY domain into fc00::/18. Same rationale as 198.18/15 on the IPv4 side.
98
+ if (head.startsWith("fd")) return true; // fd00::/8 locally-assigned ULA
94
99
  if (/^fe[89ab]/.test(head)) return true; // fe80::/10 link-local
95
100
  return false;
96
101
  }
@@ -21,7 +21,9 @@ describe("downloadRemoteMedia", () => {
21
21
  const bytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
22
22
  vi.stubGlobal(
23
23
  "fetch",
24
- vi.fn(async () => new Response(bytes, { status: 200, headers: { "content-type": "image/png" } })),
24
+ vi.fn(
25
+ async () => new Response(bytes, { status: 200, headers: { "content-type": "image/png" } }),
26
+ ),
25
27
  );
26
28
 
27
29
  const result = await downloadRemoteMedia("https://picsum.photos/600/400");
@@ -48,7 +50,10 @@ describe("downloadRemoteMedia", () => {
48
50
  });
49
51
 
50
52
  it("returns null on a non-2xx response", async () => {
51
- vi.stubGlobal("fetch", vi.fn(async () => new Response("nope", { status: 404 })));
53
+ vi.stubGlobal(
54
+ "fetch",
55
+ vi.fn(async () => new Response("nope", { status: 404 })),
56
+ );
52
57
  expect(await downloadRemoteMedia("https://example.com/missing.png")).toBeNull();
53
58
  });
54
59
 
@@ -59,7 +64,7 @@ describe("downloadRemoteMedia", () => {
59
64
  async () =>
60
65
  new Response(new Uint8Array([1, 2, 3]), {
61
66
  status: 200,
62
- headers: { "content-type": "image/png", "content-length": String(64 * 1024 * 1024) },
67
+ headers: { "content-type": "image/png", "content-length": String(128 * 1024 * 1024) },
63
68
  }),
64
69
  ),
65
70
  );
@@ -10,7 +10,10 @@ import { guessMimeType } from "./http/handlers/files.js";
10
10
  * download story uniform (always talks to the trusted gateway host with a bearer token).
11
11
  */
12
12
 
13
- const MAX_REMOTE_MEDIA_BYTES = 25 * 1024 * 1024; // 25MB
13
+ // Aligns with openclaw's own remote-media ceiling (DEFAULT_FETCH_MEDIA_MAX_BYTES =
14
+ // MAX_DOCUMENT_BYTES = 100MB). The real per-kind limit is enforced downstream by
15
+ // saveMediaBuffer (via resolveMediaMaxBytes); this is just the download ceiling.
16
+ const MAX_REMOTE_MEDIA_BYTES = 100 * 1024 * 1024; // 100MB
14
17
  const REMOTE_MEDIA_TIMEOUT_MS = 20_000;
15
18
 
16
19
  export function isHttpUrl(value: string): boolean {
@@ -50,8 +53,7 @@ export function decodeBase64Media(
50
53
  }
51
54
  if (!buffer.length) return null;
52
55
 
53
- const mimeType =
54
- mimeHint?.trim().toLowerCase() || dataUrlMime || "application/octet-stream";
56
+ const mimeType = mimeHint?.trim().toLowerCase() || dataUrlMime || "application/octet-stream";
55
57
  return { buffer, mimeType };
56
58
  }
57
59
 
package/src/openclaw.d.ts CHANGED
@@ -1,11 +1,26 @@
1
1
  declare module "openclaw/plugin-sdk/agent-harness" {
2
- export const abortAgentHarnessRun: (runId: string) => void;
2
+ /** Abort the active embedded run keyed by its internal `sessionId` (NOT the channel runId). */
3
+ export const abortAgentHarnessRun: (sessionId: string) => boolean;
4
+ /** Abort the active embedded run and wait for it to actually settle. */
5
+ export const abortAndDrainAgentHarnessRun: (params: {
6
+ sessionId: string;
7
+ sessionKey?: string;
8
+ settleMs?: number;
9
+ forceClear?: boolean;
10
+ reason?: string;
11
+ }) => Promise<{ aborted: boolean; drained: boolean; forceCleared: boolean }>;
12
+ /** Map a channel sessionKey → the active embedded run's internal sessionId. */
13
+ export const resolveActiveEmbeddedRunSessionId: (sessionKey: string) => string | undefined;
3
14
  export const runAgentHarness: (...args: any[]) => any;
4
15
  }
5
16
 
6
17
  declare module "openclaw/plugin-sdk/device-bootstrap" {
7
18
  export const listDevicePairing: (baseDir?: string) => Promise<DevicePairingList>;
8
- export const approveDevicePairing: (requestId: string, options?: { callerScopes?: readonly string[] }, baseDir?: string) => Promise<ApproveDevicePairingResult>;
19
+ export const approveDevicePairing: (
20
+ requestId: string,
21
+ options?: { callerScopes?: readonly string[] },
22
+ baseDir?: string,
23
+ ) => Promise<ApproveDevicePairingResult>;
9
24
 
10
25
  interface DevicePairingPendingRequest {
11
26
  requestId: string;
@@ -23,14 +38,17 @@ declare module "openclaw/plugin-sdk/device-bootstrap" {
23
38
  pending: DevicePairingPendingRequest[];
24
39
  paired: PairedDevice[];
25
40
  }
26
- type ApproveDevicePairingResult = {
27
- status: "approved";
28
- requestId: string;
29
- device: PairedDevice;
30
- } | {
31
- status: "forbidden";
32
- reason: string;
33
- } | null;
41
+ type ApproveDevicePairingResult =
42
+ | {
43
+ status: "approved";
44
+ requestId: string;
45
+ device: PairedDevice;
46
+ }
47
+ | {
48
+ status: "forbidden";
49
+ reason: string;
50
+ }
51
+ | null;
34
52
  }
35
53
 
36
54
  declare module "openclaw/plugin-sdk/core" {
@@ -43,6 +61,19 @@ declare module "openclaw/plugin-sdk/media-store" {
43
61
  export const saveMediaBuffer: (...args: any[]) => any;
44
62
  }
45
63
 
64
+ declare module "openclaw/plugin-sdk/channel-lifecycle" {
65
+ export const waitUntilAbort: (
66
+ signal?: AbortSignal,
67
+ onAbort?: () => void | Promise<void>,
68
+ ) => Promise<void>;
69
+ }
70
+
71
+ declare module "openclaw/plugin-sdk/media-runtime" {
72
+ export type MediaKind = "image" | "audio" | "video" | "document";
73
+ export const mediaKindFromMime: (mime?: string | null) => MediaKind | undefined;
74
+ export const maxBytesForKind: (kind: MediaKind) => number;
75
+ }
76
+
46
77
  declare module "openclaw/plugin-sdk/plugin-entry" {
47
78
  export type OpenClawPluginApi = any;
48
79
  }
@@ -7,7 +7,14 @@ import { getUpgradeRuntime } from "./upgrade-runtime.js";
7
7
  import { PLUGIN_ID, PLUGIN_PACKAGE_NAME } from "./version.js";
8
8
 
9
9
  /** Install source from the OpenClaw config `plugins.installs[<id>].source`. */
10
- export type InstallSource = "npm" | "path" | "archive" | "clawhub" | "git" | "marketplace" | "unknown";
10
+ export type InstallSource =
11
+ | "npm"
12
+ | "path"
13
+ | "archive"
14
+ | "clawhub"
15
+ | "git"
16
+ | "marketplace"
17
+ | "unknown";
11
18
 
12
19
  /**
13
20
  * Infer the install source from the loaded plugin's filesystem path (`api.source`).
@@ -20,7 +27,9 @@ export type InstallSource = "npm" | "path" | "archive" | "clawhub" | "git" | "ma
20
27
  * dev-no-duplicate-plugin-install), so anything under the managed projects dir
21
28
  * is treated as "npm".
22
29
  */
23
- export function classifyInstallSourceFromLoadedPath(loadedPath: string | null | undefined): InstallSource {
30
+ export function classifyInstallSourceFromLoadedPath(
31
+ loadedPath: string | null | undefined,
32
+ ): InstallSource {
24
33
  if (!loadedPath) return "unknown";
25
34
  return loadedPath.includes("/.openclaw/npm/projects/") ? "npm" : "path";
26
35
  }
@@ -42,9 +51,11 @@ export function getInstallSource(): InstallSource {
42
51
  const rt = getUpgradeRuntime();
43
52
  if (!rt) return "unknown";
44
53
  try {
45
- const cfg = rt.currentConfig() as {
46
- plugins?: { installs?: Record<string, { source?: string } | undefined> };
47
- } | undefined;
54
+ const cfg = rt.currentConfig() as
55
+ | {
56
+ plugins?: { installs?: Record<string, { source?: string } | undefined> };
57
+ }
58
+ | undefined;
48
59
  const source = cfg?.plugins?.installs?.[PLUGIN_ID]?.source;
49
60
  if (
50
61
  source === "npm" ||
@@ -94,10 +105,10 @@ export async function fetchLatestVersion(nowMs: number): Promise<string | null>
94
105
  const controller = new AbortController();
95
106
  const timer = setTimeout(() => controller.abort(), 5000);
96
107
  try {
97
- const res = await fetch(
98
- `https://registry.npmjs.org/${PLUGIN_PACKAGE_NAME}/latest`,
99
- { signal: controller.signal, headers: { Accept: "application/json" } },
100
- );
108
+ const res = await fetch(`https://registry.npmjs.org/${PLUGIN_PACKAGE_NAME}/latest`, {
109
+ signal: controller.signal,
110
+ headers: { Accept: "application/json" },
111
+ });
101
112
  if (res.ok) {
102
113
  const body = (await res.json()) as { version?: string };
103
114
  if (typeof body.version === "string" && body.version) version = body.version;
@@ -173,7 +173,8 @@ export function ingestAgentEventMetadata(runId: string, data: Record<string, unk
173
173
  const cacheRead = pickCacheRead(usageForTokens);
174
174
  if (typeof cacheRead === "number" && cacheRead >= 0) next.cacheReadTokens = Math.floor(cacheRead);
175
175
  const cacheWrite = pickCacheWrite(usageForTokens);
176
- if (typeof cacheWrite === "number" && cacheWrite >= 0) next.cacheWriteTokens = Math.floor(cacheWrite);
176
+ if (typeof cacheWrite === "number" && cacheWrite >= 0)
177
+ next.cacheWriteTokens = Math.floor(cacheWrite);
177
178
 
178
179
  const usageForContext = usage ?? data;
179
180
  const ctxUsed = contextTokensFromUsageRecord(usageForContext);
@@ -162,7 +162,11 @@ export function setSessionSettings(
162
162
  upsertSessionEntry(data, fileKey, sessionKey);
163
163
 
164
164
  const fieldKeys: (keyof FridaySessionSettingsUpdate)[] = [
165
- "reasoningLevel", "thinkingLevel", "modelRef", "providerOverride", "modelOverride",
165
+ "reasoningLevel",
166
+ "thinkingLevel",
167
+ "modelRef",
168
+ "providerOverride",
169
+ "modelOverride",
166
170
  ];
167
171
  let updated = false;
168
172
  for (const key of fieldKeys) {
@@ -193,22 +197,21 @@ export function setSessionSettings(
193
197
  }
194
198
 
195
199
  function readSettingsFromEntry(entry: Record<string, unknown>): FridaySessionSettings {
196
- const provider = typeof entry["providerOverride"] === "string" ? entry["providerOverride"] : undefined;
200
+ const provider =
201
+ typeof entry["providerOverride"] === "string" ? entry["providerOverride"] : undefined;
197
202
  const model = typeof entry["modelOverride"] === "string" ? entry["modelOverride"] : undefined;
198
203
  const storedModelRef = typeof entry["modelRef"] === "string" ? entry["modelRef"] : undefined;
199
204
  const modelRef = storedModelRef ?? (provider && model ? `${provider}/${model}` : undefined);
200
205
 
201
206
  return {
202
- reasoningLevel: typeof entry["reasoningLevel"] === "string" ? entry["reasoningLevel"] : undefined,
207
+ reasoningLevel:
208
+ typeof entry["reasoningLevel"] === "string" ? entry["reasoningLevel"] : undefined,
203
209
  thinkingLevel: typeof entry["thinkingLevel"] === "string" ? entry["thinkingLevel"] : undefined,
204
210
  modelRef,
205
211
  };
206
212
  }
207
213
 
208
- export function getSessionSettings(
209
- sessionKey: string,
210
- historyDir?: string,
211
- ): FridaySessionSettings {
214
+ export function getSessionSettings(sessionKey: string, historyDir?: string): FridaySessionSettings {
212
215
  try {
213
216
  const fileKey = toSessionStoreKey(sessionKey);
214
217
  const sessionsFile = resolveSessionsFilePath(historyDir, agentIdFromSessionKey(fileKey));
@@ -243,7 +246,10 @@ export function resolveAgentDefaults(sessionKey: string): { model?: string; thin
243
246
  const targetAgentId = agentIdFromSessionKey(sessionKey);
244
247
 
245
248
  const agentEntry = (agents?.list as Array<Record<string, unknown>> | undefined)?.find(
246
- (a) => agentIdFromSessionKey(`agent:${String(a?.id ?? "")}:x`) === targetAgentId,
249
+ (a) =>
250
+ agentIdFromSessionKey(
251
+ `agent:${typeof a?.id === "string" ? a.id : typeof a?.id === "number" ? String(a.id) : ""}:x`,
252
+ ) === targetAgentId,
247
253
  );
248
254
  const agentModel = agentEntry?.model;
249
255
  const perAgentModel =
@@ -253,13 +259,15 @@ export function resolveAgentDefaults(sessionKey: string): { model?: string; thin
253
259
  ? ((agentModel as Record<string, unknown>).primary as string)
254
260
  : undefined;
255
261
  const perAgentThinking =
256
- typeof agentEntry?.thinkingDefault === "string" ? (agentEntry.thinkingDefault as string) : undefined;
262
+ typeof agentEntry?.thinkingDefault === "string" ? agentEntry.thinkingDefault : undefined;
257
263
 
258
264
  const agentDefaults = agents?.defaults as Record<string, unknown> | undefined;
259
265
  const model = agentDefaults?.model as Record<string, unknown> | undefined;
260
- const globalModel = typeof model?.primary === "string" ? (model.primary as string) : undefined;
266
+ const globalModel = typeof model?.primary === "string" ? model.primary : undefined;
261
267
  const globalThinking =
262
- typeof agentDefaults?.thinkingDefault === "string" ? (agentDefaults.thinkingDefault as string) : undefined;
268
+ typeof agentDefaults?.thinkingDefault === "string"
269
+ ? agentDefaults.thinkingDefault
270
+ : undefined;
263
271
 
264
272
  return { model: perAgentModel ?? globalModel, thinking: perAgentThinking ?? globalThinking };
265
273
  } catch {
@@ -33,7 +33,9 @@ function finiteCost(n: unknown): number | undefined {
33
33
  }
34
34
 
35
35
  /** Build a compact snapshot from a loaded session store entry (unknown shape). */
36
- export function buildSessionUsageSnapshot(entry: Record<string, unknown>): FridaySessionUsagePayload | undefined {
36
+ export function buildSessionUsageSnapshot(
37
+ entry: Record<string, unknown>,
38
+ ): FridaySessionUsagePayload | undefined {
37
39
  const payload: FridaySessionUsagePayload = {};
38
40
 
39
41
  const modelId = typeof entry.model === "string" ? entry.model.trim() : "";
@@ -28,7 +28,9 @@ export function readSessionUsageSnapshotFromStore(
28
28
  const cfg = access.getConfig() as { session?: { store?: string } } | null | undefined;
29
29
  const storeConfig = cfg?.session?.store;
30
30
  const canonical = toSessionStoreKey(sessionKeyForStore);
31
- const storePath = access.resolveStorePath(storeConfig, { agentId: agentIdFromSessionKey(canonical) });
31
+ const storePath = access.resolveStorePath(storeConfig, {
32
+ agentId: agentIdFromSessionKey(canonical),
33
+ });
32
34
  const store = access.loadSessionStore(storePath, { skipCache: true }) as Record<
33
35
  string,
34
36
  Record<string, unknown>