darkfire514 commited on
Commit
34b00eb
·
verified ·
1 Parent(s): 9573928

Upload 2526 files

Browse files
src/agents/bash-tools.exec.pty-fallback.test.ts CHANGED
@@ -19,7 +19,7 @@ test("exec falls back when PTY spawn fails", async () => {
19
  const { createExecTool } = await import("./bash-tools.exec");
20
  const tool = createExecTool({ allowBackground: false });
21
  const result = await tool.execute("toolcall", {
22
- command: "printf ok",
23
  pty: true,
24
  });
25
 
 
19
  const { createExecTool } = await import("./bash-tools.exec");
20
  const tool = createExecTool({ allowBackground: false });
21
  const result = await tool.execute("toolcall", {
22
+ command: "echo ok",
23
  pty: true,
24
  });
25
 
src/agents/bash-tools.process.send-keys.test.ts CHANGED
@@ -29,7 +29,7 @@ test("process send-keys encodes Enter for pty sessions", async () => {
29
  keys: ["h", "i", "Enter"],
30
  });
31
 
32
- const deadline = Date.now() + (process.platform === "win32" ? 4000 : 2000);
33
  while (Date.now() < deadline) {
34
  await wait(50);
35
  const poll = await processTool.execute("toolcall", { action: "poll", sessionId });
@@ -63,7 +63,7 @@ test("process submit sends Enter for pty sessions", async () => {
63
  sessionId,
64
  });
65
 
66
- const deadline = Date.now() + (process.platform === "win32" ? 4000 : 2000);
67
  while (Date.now() < deadline) {
68
  await wait(50);
69
  const poll = await processTool.execute("toolcall", { action: "poll", sessionId });
 
29
  keys: ["h", "i", "Enter"],
30
  });
31
 
32
+ const deadline = Date.now() + (process.platform === "win32" ? 10000 : 5000);
33
  while (Date.now() < deadline) {
34
  await wait(50);
35
  const poll = await processTool.execute("toolcall", { action: "poll", sessionId });
 
63
  sessionId,
64
  });
65
 
66
+ const deadline = Date.now() + (process.platform === "win32" ? 10000 : 5000);
67
  while (Date.now() < deadline) {
68
  await wait(50);
69
  const poll = await processTool.execute("toolcall", { action: "poll", sessionId });
src/agents/bash-tools.process.ts CHANGED
@@ -476,7 +476,7 @@ export function createProcessTool(
476
  };
477
  }
478
  await new Promise<void>((resolve, reject) => {
479
- stdin.write("\r", (err) => {
480
  if (err) {
481
  reject(err);
482
  } else {
@@ -488,7 +488,7 @@ export function createProcessTool(
488
  content: [
489
  {
490
  type: "text",
491
- text: `Submitted session ${params.sessionId} (sent CR).`,
492
  },
493
  ],
494
  details: {
 
476
  };
477
  }
478
  await new Promise<void>((resolve, reject) => {
479
+ stdin.write("\r\n", (err) => {
480
  if (err) {
481
  reject(err);
482
  } else {
 
488
  content: [
489
  {
490
  type: "text",
491
+ text: `Submitted session ${params.sessionId} (sent CRLF).`,
492
  },
493
  ],
494
  details: {
src/agents/bash-tools.test.ts CHANGED
@@ -193,6 +193,7 @@ describe("exec tool backgrounding", () => {
193
  elevated: { enabled: true, allowed: false, defaultLevel: "on" },
194
  backgroundMs: 1000,
195
  timeoutSec: 5,
 
196
  });
197
 
198
  const result = await customBash.execute("call1", {
 
193
  elevated: { enabled: true, allowed: false, defaultLevel: "on" },
194
  backgroundMs: 1000,
195
  timeoutSec: 5,
196
+ allowBackground: false,
197
  });
198
 
199
  const result = await customBash.execute("call1", {
src/agents/pi-tools.workspace-paths.test.ts CHANGED
@@ -19,7 +19,9 @@ function getTextContent(result?: { content?: Array<{ type: string; text?: string
19
  }
20
 
21
  describe("workspace path resolution", () => {
22
- it("reads relative paths against workspaceDir even after cwd changes", async () => {
 
 
23
  await withTempDir("openclaw-ws-", async (workspaceDir) => {
24
  await withTempDir("openclaw-cwd-", async (otherDir) => {
25
  const prevCwd = process.cwd();
@@ -40,7 +42,9 @@ describe("workspace path resolution", () => {
40
  }
41
  });
42
  });
43
- });
 
 
44
 
45
  it("writes relative paths against workspaceDir even after cwd changes", async () => {
46
  await withTempDir("openclaw-ws-", async (workspaceDir) => {
 
19
  }
20
 
21
  describe("workspace path resolution", () => {
22
+ it(
23
+ "reads relative paths against workspaceDir even after cwd changes",
24
+ async () => {
25
  await withTempDir("openclaw-ws-", async (workspaceDir) => {
26
  await withTempDir("openclaw-cwd-", async (otherDir) => {
27
  const prevCwd = process.cwd();
 
42
  }
43
  });
44
  });
45
+ },
46
+ 240_000,
47
+ );
48
 
49
  it("writes relative paths against workspaceDir even after cwd changes", async () => {
50
  await withTempDir("openclaw-ws-", async (workspaceDir) => {
src/cli/program.smoke.test.ts CHANGED
@@ -49,6 +49,7 @@ vi.mock("../gateway/call.js", () => ({
49
  message: "Gateway target: ws://127.0.0.1:1234",
50
  }),
51
  }));
 
52
  vi.mock("./deps.js", () => ({ createDefaultDeps: () => ({}) }));
53
 
54
  const { buildProgram } = await import("./program.js");
 
49
  message: "Gateway target: ws://127.0.0.1:1234",
50
  }),
51
  }));
52
+ vi.mock("./plugin-registry.js", () => ({ ensurePluginRegistryLoaded: () => {} }));
53
  vi.mock("./deps.js", () => ({ createDefaultDeps: () => ({}) }));
54
 
55
  const { buildProgram } = await import("./program.js");
src/gateway/control-ui.ts CHANGED
@@ -209,7 +209,9 @@ function normalizeIdentifier(value: string) {
209
 
210
  function parseCsvSet(value: string | undefined): Set<string> | null {
211
  const raw = value?.trim();
212
- if (!raw) return null;
 
 
213
  const items = raw
214
  .split(/[,\n]/g)
215
  .map((item) => normalizeIdentifier(item))
@@ -265,14 +267,20 @@ function signToken(secret: string, payload: unknown) {
265
 
266
  function verifyToken<T>(secret: string, token: string): { ok: true; value: T } | { ok: false } {
267
  const parts = token.split(".");
268
- if (parts.length !== 2) return { ok: false };
 
 
269
  const [body, sig] = parts;
270
  const expected = crypto.createHmac("sha256", secret).update(body).digest("base64url");
271
  try {
272
  const a = Buffer.from(sig);
273
  const b = Buffer.from(expected);
274
- if (a.length !== b.length) return { ok: false };
275
- if (!crypto.timingSafeEqual(a, b)) return { ok: false };
 
 
 
 
276
  } catch {
277
  return { ok: false };
278
  }
@@ -285,14 +293,20 @@ function verifyToken<T>(secret: string, token: string): { ok: true; value: T } |
285
  }
286
 
287
  function parseCookies(header: string | undefined): Record<string, string> {
288
- if (!header) return {};
 
 
289
  const out: Record<string, string> = {};
290
  for (const part of header.split(";")) {
291
  const idx = part.indexOf("=");
292
- if (idx === -1) continue;
 
 
293
  const k = part.slice(0, idx).trim();
294
  const v = part.slice(idx + 1).trim();
295
- if (!k) continue;
 
 
296
  out[k] = decodeURIComponent(v);
297
  }
298
  return out;
@@ -314,7 +328,9 @@ function appendSetCookie(res: ServerResponse, value: string) {
314
  function isSecureRequest(req: IncomingMessage) {
315
  const xfProto = req.headers["x-forwarded-proto"];
316
  const proto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
317
- if (proto && proto.toLowerCase().includes("https")) return true;
 
 
318
  return Boolean((req.socket as { encrypted?: boolean }).encrypted);
319
  }
320
 
@@ -326,7 +342,7 @@ function getRequestOrigin(req: IncomingMessage) {
326
  return `${proto}://${host}`;
327
  }
328
 
329
- function controlUiCookiePath(basePath: string) {
330
  return "/";
331
  }
332
 
@@ -347,7 +363,9 @@ function setCookie(
347
  if (typeof opts.maxAgeSeconds === "number") {
348
  parts.push(`Max-Age=${Math.max(0, Math.floor(opts.maxAgeSeconds))}`);
349
  }
350
- if (opts.secure) parts.push("Secure");
 
 
351
  appendSetCookie(res, parts.join("; "));
352
  }
353
 
@@ -380,15 +398,21 @@ function isAllowedAccount(cfg: ControlUiOauthConfig, payload: ControlUiSessionPa
380
  }
381
 
382
  if (cfg.allowedEmails) {
383
- if (!email || !cfg.allowedEmails.has(email)) return false;
 
 
384
  }
385
 
386
  if (payload.provider === "google" && cfg.allowedGoogleEmails) {
387
- if (!email || !cfg.allowedGoogleEmails.has(email)) return false;
 
 
388
  }
389
 
390
  if (payload.provider === "github" && cfg.allowedGithubLogins) {
391
- if (!login || !cfg.allowedGithubLogins.has(login)) return false;
 
 
392
  }
393
 
394
  return true;
@@ -397,15 +421,29 @@ function isAllowedAccount(cfg: ControlUiOauthConfig, payload: ControlUiSessionPa
397
  function readSessionFromRequest(req: IncomingMessage, cfg: ControlUiOauthConfig): ControlUiSessionPayload | null {
398
  const cookies = parseCookies(req.headers.cookie);
399
  const raw = cookies[CONTROL_UI_SESSION_COOKIE];
400
- if (!raw) return null;
 
 
401
  const verified = verifyToken<ControlUiSessionPayload>(cfg.sessionSecret, raw);
402
- if (!verified.ok) return null;
 
 
403
  const value = verified.value;
404
- if (!value || value.v !== 1) return null;
405
- if (typeof value.exp !== "number" || value.exp <= Math.floor(Date.now() / 1000)) return null;
406
- if (value.provider !== "google" && value.provider !== "github") return null;
407
- if (typeof value.sub !== "string" || !value.sub) return null;
408
- if (!isAllowedAccount(cfg, value)) return null;
 
 
 
 
 
 
 
 
 
 
409
  return value;
410
  }
411
 
@@ -421,9 +459,13 @@ export function readControlUiOauthSessionIdentityFromRequest(
421
  req: IncomingMessage,
422
  ): ControlUiOauthSessionIdentity | null {
423
  const cfg = resolveControlUiOauthConfig();
424
- if (!isControlUiOauthEnabled(cfg)) return null;
 
 
425
  const session = readSessionFromRequest(req, cfg);
426
- if (!session) return null;
 
 
427
  return {
428
  provider: session.provider,
429
  sub: session.sub,
@@ -446,15 +488,27 @@ function issueOAuthState(res: ServerResponse, cfg: ControlUiOauthConfig, basePat
446
  function consumeOAuthState(req: IncomingMessage, res: ServerResponse, cfg: ControlUiOauthConfig, basePath: string, secure: boolean) {
447
  const cookies = parseCookies(req.headers.cookie);
448
  const raw = cookies[CONTROL_UI_OAUTH_STATE_COOKIE];
449
- if (!raw) return null;
 
 
450
  const verified = verifyToken<OAuthStatePayload>(cfg.sessionSecret, raw);
451
  clearCookie(res, { name: CONTROL_UI_OAUTH_STATE_COOKIE, basePath, secure });
452
- if (!verified.ok) return null;
 
 
453
  const value = verified.value;
454
- if (!value || value.v !== 1) return null;
455
- if (typeof value.exp !== "number" || value.exp <= Math.floor(Date.now() / 1000)) return null;
456
- if (value.provider !== "google" && value.provider !== "github") return null;
457
- if (typeof value.nonce !== "string" || !value.nonce) return null;
 
 
 
 
 
 
 
 
458
  return { token: raw, payload: value };
459
  }
460
 
@@ -528,7 +582,9 @@ async function exchangeGoogleUser(opts: {
528
  }
529
  const tokenJson = (await tokenRes.json()) as { access_token?: unknown };
530
  const accessToken = typeof tokenJson.access_token === "string" ? tokenJson.access_token : null;
531
- if (!accessToken) throw new Error("google access token missing");
 
 
532
 
533
  const userRes = await fetch("https://www.googleapis.com/oauth2/v3/userinfo", {
534
  headers: { Authorization: `Bearer ${accessToken}` },
@@ -548,8 +604,12 @@ async function exchangeGoogleUser(opts: {
548
  const emailVerified = userJson.email_verified === true;
549
  const name = typeof userJson.name === "string" ? userJson.name : null;
550
  const picture = typeof userJson.picture === "string" ? userJson.picture : null;
551
- if (!sub) throw new Error("google subject missing");
552
- if (email && !emailVerified) throw new Error("google email not verified");
 
 
 
 
553
  return { sub, email, name, picture };
554
  }
555
 
@@ -569,7 +629,9 @@ async function exchangeGithubUser(opts: { code: string; redirectUri: string; cli
569
  }
570
  const tokenJson = (await tokenRes.json()) as { access_token?: unknown; token_type?: unknown };
571
  const accessToken = typeof tokenJson.access_token === "string" ? tokenJson.access_token : null;
572
- if (!accessToken) throw new Error("github access token missing");
 
 
573
 
574
  const userRes = await fetch("https://api.github.com/user", {
575
  headers: {
@@ -578,13 +640,17 @@ async function exchangeGithubUser(opts: { code: string; redirectUri: string; cli
578
  "User-Agent": "openclaw-control-ui",
579
  },
580
  });
581
- if (!userRes.ok) throw new Error(`github user fetch failed: ${userRes.status}`);
 
 
582
  const userJson = (await userRes.json()) as { id?: unknown; login?: unknown; name?: unknown; avatar_url?: unknown };
583
  const sub = typeof userJson.id === "number" ? String(userJson.id) : typeof userJson.id === "string" ? userJson.id : null;
584
  const login = typeof userJson.login === "string" ? userJson.login : null;
585
  const name = typeof userJson.name === "string" ? userJson.name : null;
586
  const picture = typeof userJson.avatar_url === "string" ? userJson.avatar_url : null;
587
- if (!sub || !login) throw new Error("github user identity missing");
 
 
588
 
589
  const emailsRes = await fetch("https://api.github.com/user/emails", {
590
  headers: {
@@ -620,11 +686,17 @@ export async function handleControlUiAuthRequest(
620
  opts?: ControlUiRequestOptions,
621
  ): Promise<boolean> {
622
  const urlRaw = req.url;
623
- if (!urlRaw) return false;
624
- if (req.method !== "GET" && req.method !== "HEAD") return false;
 
 
 
 
625
 
626
  const cfg = resolveControlUiOauthConfig();
627
- if (!isControlUiOauthEnabled(cfg)) return false;
 
 
628
 
629
  const url = new URL(urlRaw, "http://localhost");
630
  const basePath = normalizeControlUiBasePath(opts?.basePath);
 
209
 
210
  function parseCsvSet(value: string | undefined): Set<string> | null {
211
  const raw = value?.trim();
212
+ if (!raw) {
213
+ return null;
214
+ }
215
  const items = raw
216
  .split(/[,\n]/g)
217
  .map((item) => normalizeIdentifier(item))
 
267
 
268
  function verifyToken<T>(secret: string, token: string): { ok: true; value: T } | { ok: false } {
269
  const parts = token.split(".");
270
+ if (parts.length !== 2) {
271
+ return { ok: false };
272
+ }
273
  const [body, sig] = parts;
274
  const expected = crypto.createHmac("sha256", secret).update(body).digest("base64url");
275
  try {
276
  const a = Buffer.from(sig);
277
  const b = Buffer.from(expected);
278
+ if (a.length !== b.length) {
279
+ return { ok: false };
280
+ }
281
+ if (!crypto.timingSafeEqual(a, b)) {
282
+ return { ok: false };
283
+ }
284
  } catch {
285
  return { ok: false };
286
  }
 
293
  }
294
 
295
  function parseCookies(header: string | undefined): Record<string, string> {
296
+ if (!header) {
297
+ return {};
298
+ }
299
  const out: Record<string, string> = {};
300
  for (const part of header.split(";")) {
301
  const idx = part.indexOf("=");
302
+ if (idx === -1) {
303
+ continue;
304
+ }
305
  const k = part.slice(0, idx).trim();
306
  const v = part.slice(idx + 1).trim();
307
+ if (!k) {
308
+ continue;
309
+ }
310
  out[k] = decodeURIComponent(v);
311
  }
312
  return out;
 
328
  function isSecureRequest(req: IncomingMessage) {
329
  const xfProto = req.headers["x-forwarded-proto"];
330
  const proto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
331
+ if (proto && proto.toLowerCase().includes("https")) {
332
+ return true;
333
+ }
334
  return Boolean((req.socket as { encrypted?: boolean }).encrypted);
335
  }
336
 
 
342
  return `${proto}://${host}`;
343
  }
344
 
345
+ function controlUiCookiePath(_basePath: string) {
346
  return "/";
347
  }
348
 
 
363
  if (typeof opts.maxAgeSeconds === "number") {
364
  parts.push(`Max-Age=${Math.max(0, Math.floor(opts.maxAgeSeconds))}`);
365
  }
366
+ if (opts.secure) {
367
+ parts.push("Secure");
368
+ }
369
  appendSetCookie(res, parts.join("; "));
370
  }
371
 
 
398
  }
399
 
400
  if (cfg.allowedEmails) {
401
+ if (!email || !cfg.allowedEmails.has(email)) {
402
+ return false;
403
+ }
404
  }
405
 
406
  if (payload.provider === "google" && cfg.allowedGoogleEmails) {
407
+ if (!email || !cfg.allowedGoogleEmails.has(email)) {
408
+ return false;
409
+ }
410
  }
411
 
412
  if (payload.provider === "github" && cfg.allowedGithubLogins) {
413
+ if (!login || !cfg.allowedGithubLogins.has(login)) {
414
+ return false;
415
+ }
416
  }
417
 
418
  return true;
 
421
  function readSessionFromRequest(req: IncomingMessage, cfg: ControlUiOauthConfig): ControlUiSessionPayload | null {
422
  const cookies = parseCookies(req.headers.cookie);
423
  const raw = cookies[CONTROL_UI_SESSION_COOKIE];
424
+ if (!raw) {
425
+ return null;
426
+ }
427
  const verified = verifyToken<ControlUiSessionPayload>(cfg.sessionSecret, raw);
428
+ if (!verified.ok) {
429
+ return null;
430
+ }
431
  const value = verified.value;
432
+ if (!value || value.v !== 1) {
433
+ return null;
434
+ }
435
+ if (typeof value.exp !== "number" || value.exp <= Math.floor(Date.now() / 1000)) {
436
+ return null;
437
+ }
438
+ if (value.provider !== "google" && value.provider !== "github") {
439
+ return null;
440
+ }
441
+ if (typeof value.sub !== "string" || !value.sub) {
442
+ return null;
443
+ }
444
+ if (!isAllowedAccount(cfg, value)) {
445
+ return null;
446
+ }
447
  return value;
448
  }
449
 
 
459
  req: IncomingMessage,
460
  ): ControlUiOauthSessionIdentity | null {
461
  const cfg = resolveControlUiOauthConfig();
462
+ if (!isControlUiOauthEnabled(cfg)) {
463
+ return null;
464
+ }
465
  const session = readSessionFromRequest(req, cfg);
466
+ if (!session) {
467
+ return null;
468
+ }
469
  return {
470
  provider: session.provider,
471
  sub: session.sub,
 
488
  function consumeOAuthState(req: IncomingMessage, res: ServerResponse, cfg: ControlUiOauthConfig, basePath: string, secure: boolean) {
489
  const cookies = parseCookies(req.headers.cookie);
490
  const raw = cookies[CONTROL_UI_OAUTH_STATE_COOKIE];
491
+ if (!raw) {
492
+ return null;
493
+ }
494
  const verified = verifyToken<OAuthStatePayload>(cfg.sessionSecret, raw);
495
  clearCookie(res, { name: CONTROL_UI_OAUTH_STATE_COOKIE, basePath, secure });
496
+ if (!verified.ok) {
497
+ return null;
498
+ }
499
  const value = verified.value;
500
+ if (!value || value.v !== 1) {
501
+ return null;
502
+ }
503
+ if (typeof value.exp !== "number" || value.exp <= Math.floor(Date.now() / 1000)) {
504
+ return null;
505
+ }
506
+ if (value.provider !== "google" && value.provider !== "github") {
507
+ return null;
508
+ }
509
+ if (typeof value.nonce !== "string" || !value.nonce) {
510
+ return null;
511
+ }
512
  return { token: raw, payload: value };
513
  }
514
 
 
582
  }
583
  const tokenJson = (await tokenRes.json()) as { access_token?: unknown };
584
  const accessToken = typeof tokenJson.access_token === "string" ? tokenJson.access_token : null;
585
+ if (!accessToken) {
586
+ throw new Error("google access token missing");
587
+ }
588
 
589
  const userRes = await fetch("https://www.googleapis.com/oauth2/v3/userinfo", {
590
  headers: { Authorization: `Bearer ${accessToken}` },
 
604
  const emailVerified = userJson.email_verified === true;
605
  const name = typeof userJson.name === "string" ? userJson.name : null;
606
  const picture = typeof userJson.picture === "string" ? userJson.picture : null;
607
+ if (!sub) {
608
+ throw new Error("google subject missing");
609
+ }
610
+ if (email && !emailVerified) {
611
+ throw new Error("google email not verified");
612
+ }
613
  return { sub, email, name, picture };
614
  }
615
 
 
629
  }
630
  const tokenJson = (await tokenRes.json()) as { access_token?: unknown; token_type?: unknown };
631
  const accessToken = typeof tokenJson.access_token === "string" ? tokenJson.access_token : null;
632
+ if (!accessToken) {
633
+ throw new Error("github access token missing");
634
+ }
635
 
636
  const userRes = await fetch("https://api.github.com/user", {
637
  headers: {
 
640
  "User-Agent": "openclaw-control-ui",
641
  },
642
  });
643
+ if (!userRes.ok) {
644
+ throw new Error(`github user fetch failed: ${userRes.status}`);
645
+ }
646
  const userJson = (await userRes.json()) as { id?: unknown; login?: unknown; name?: unknown; avatar_url?: unknown };
647
  const sub = typeof userJson.id === "number" ? String(userJson.id) : typeof userJson.id === "string" ? userJson.id : null;
648
  const login = typeof userJson.login === "string" ? userJson.login : null;
649
  const name = typeof userJson.name === "string" ? userJson.name : null;
650
  const picture = typeof userJson.avatar_url === "string" ? userJson.avatar_url : null;
651
+ if (!sub || !login) {
652
+ throw new Error("github user identity missing");
653
+ }
654
 
655
  const emailsRes = await fetch("https://api.github.com/user/emails", {
656
  headers: {
 
686
  opts?: ControlUiRequestOptions,
687
  ): Promise<boolean> {
688
  const urlRaw = req.url;
689
+ if (!urlRaw) {
690
+ return false;
691
+ }
692
+ if (req.method !== "GET" && req.method !== "HEAD") {
693
+ return false;
694
+ }
695
 
696
  const cfg = resolveControlUiOauthConfig();
697
+ if (!isControlUiOauthEnabled(cfg)) {
698
+ return false;
699
+ }
700
 
701
  const url = new URL(urlRaw, "http://localhost");
702
  const basePath = normalizeControlUiBasePath(opts?.basePath);
src/gateway/server/ws-connection/message-handler.ts CHANGED
@@ -625,7 +625,9 @@ export function attachGatewayWsMessageHandler(params: {
625
  return;
626
  }
627
 
628
- const skipPairing = allowControlUiBypass && hasSharedAuth;
 
 
629
  if (device && devicePublicKey && !skipPairing) {
630
  const requirePairing = async (reason: string, _paired?: { deviceId: string }) => {
631
  const pairing = await requestDevicePairing({
 
625
  return;
626
  }
627
 
628
+ const hasControlUiOauth =
629
+ isControlUi && authResult.ok && authResult.method === "control-ui-oauth";
630
+ const skipPairing = (allowControlUiBypass && hasSharedAuth) || hasControlUiOauth;
631
  if (device && devicePublicKey && !skipPairing) {
632
  const requirePairing = async (reason: string, _paired?: { deviceId: string }) => {
633
  const pairing = await requestDevicePairing({