Merge branch 'main' into claude/simplify-remediation-steps-AA2ao

This commit is contained in:
SafeKey Lab 2026-01-26 23:12:16 -05:00 committed by GitHub
commit 7f27973678
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 150 additions and 20934 deletions

2
.gitignore vendored
View File

@ -40,6 +40,8 @@ apps/ios/*.xcfilelist
# Vendor build artifacts
vendor/a2ui/renderers/lit/dist/
src/canvas-host/a2ui/*.bundle.js
src/canvas-host/a2ui/*.map
.bundle.hash
# fastlane (iOS)

View File

@ -39,6 +39,7 @@ Status: unreleased.
- Browser: route browser control via gateway/node; remove standalone browser control command and control URL config.
- Browser: route `browser.request` via node proxies when available; honor proxy timeouts; derive browser ports from `gateway.port`.
- Update: ignore dist/control-ui for dirty checks and restore after ui builds. (#1976) Thanks @Glucksberg.
- Build: bundle A2UI assets during build and stop tracking generated bundles. (#2455) Thanks @0oAstro.
- Telegram: allow caption param for media sends. (#1888) Thanks @mguellsegarra.
- Telegram: support plugin sendPayload channelData (media/buttons) and validate plugin commands. (#1917) Thanks @JoshuaLelon.
- Telegram: avoid block replies when streaming is disabled. (#1885) Thanks @ivancasco.

View File

@ -39,7 +39,7 @@ export default defineConfig({
output: {
file: outputFile,
format: "esm",
inlineDynamicImports: true,
codeSplitting: false,
sourcemap: false,
},
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,15 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Clawdbot Control</title>
<meta name="color-scheme" content="dark light" />
<link rel="icon" href="./favicon.ico" sizes="any" />
<script type="module" crossorigin src="./assets/index-DQcOTEYz.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-08nzABV3.css">
</head>
<body>
<clawdbot-app></clawdbot-app>
</body>
</html>

View File

@ -175,6 +175,7 @@ clawdbot onboard --install-daemon
```
If you dont have a global install yet, run the onboarding step via `pnpm clawdbot ...` from the repo.
`pnpm build` also bundles A2UI assets; if you need to run just that step, use `pnpm canvas:a2ui:bundle`.
Gateway (from this repo):

View File

@ -67,7 +67,8 @@ Example:
- macOS: `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin`, `/bin`
- Linux: `/usr/local/bin`, `/usr/bin`, `/bin`
- `host=sandbox`: runs `sh -lc` (login shell) inside the container, so `/etc/profile` may reset `PATH`.
Clawdbot prepends `env.PATH` after profile sourcing; `tools.exec.pathPrepend` applies here too.
Clawdbot prepends `env.PATH` after profile sourcing via an internal env var (no shell interpolation);
`tools.exec.pathPrepend` applies here too.
- `host=node`: only env overrides you pass are sent to the node. `tools.exec.pathPrepend` only applies
if the exec call already sets `env.PATH`. Headless node hosts accept `PATH` only when it prepends
the node host PATH (no replacement). macOS nodes drop `PATH` overrides entirely.

View File

@ -82,7 +82,7 @@
"docs:bin": "node scripts/build-docs-list.mjs",
"docs:dev": "cd docs && mint dev",
"docs:build": "cd docs && pnpm dlx --reporter append-only mint broken-links",
"build": "tsc -p tsconfig.json && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/write-build-info.ts",
"build": "pnpm canvas:a2ui:bundle && tsc -p tsconfig.json && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/write-build-info.ts",
"plugins:sync": "node --import tsx scripts/sync-plugin-versions.ts",
"release:check": "node --import tsx scripts/release-check.ts",
"ui:install": "node scripts/ui.js install",

View File

@ -1,8 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
on_error() {
echo "A2UI bundling failed. Re-run with: pnpm canvas:a2ui:bundle" >&2
echo "If this persists, verify pnpm deps and try again." >&2
}
trap on_error ERR
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
HASH_FILE="$ROOT_DIR/src/canvas-host/a2ui/.bundle.hash"
OUTPUT_FILE="$ROOT_DIR/src/canvas-host/a2ui/a2ui.bundle.js"
INPUT_PATHS=(
"$ROOT_DIR/package.json"
@ -33,7 +40,7 @@ compute_hash() {
current_hash="$(compute_hash)"
if [[ -f "$HASH_FILE" ]]; then
previous_hash="$(cat "$HASH_FILE")"
if [[ "$previous_hash" == "$current_hash" ]]; then
if [[ "$previous_hash" == "$current_hash" && -f "$OUTPUT_FILE" ]]; then
echo "A2UI bundle up to date; skipping."
exit 0
fi

View File

@ -1,19 +1,44 @@
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const srcDir = path.join(repoRoot, "src", "canvas-host", "a2ui");
const outDir = path.join(repoRoot, "dist", "canvas-host", "a2ui");
async function main() {
await fs.stat(path.join(srcDir, "index.html"));
await fs.stat(path.join(srcDir, "a2ui.bundle.js"));
export function getA2uiPaths(env = process.env) {
const srcDir =
env.CLAWDBOT_A2UI_SRC_DIR ?? path.join(repoRoot, "src", "canvas-host", "a2ui");
const outDir =
env.CLAWDBOT_A2UI_OUT_DIR ?? path.join(repoRoot, "dist", "canvas-host", "a2ui");
return { srcDir, outDir };
}
export async function copyA2uiAssets({
srcDir,
outDir,
}: {
srcDir: string;
outDir: string;
}) {
try {
await fs.stat(path.join(srcDir, "index.html"));
await fs.stat(path.join(srcDir, "a2ui.bundle.js"));
} catch (err) {
const message =
'Missing A2UI bundle assets. Run "pnpm canvas:a2ui:bundle" and retry.';
throw new Error(message, { cause: err });
}
await fs.mkdir(path.dirname(outDir), { recursive: true });
await fs.cp(srcDir, outDir, { recursive: true });
}
main().catch((err) => {
console.error(String(err));
process.exit(1);
});
async function main() {
const { srcDir, outDir } = getA2uiPaths();
await copyA2uiAssets({ srcDir, outDir });
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
main().catch((err) => {
console.error(String(err));
process.exit(1);
});
}

View File

@ -60,11 +60,18 @@ export function buildDockerExecArgs(params: {
for (const [key, value] of Object.entries(params.env)) {
args.push("-e", `${key}=${value}`);
}
const hasCustomPath = typeof params.env.PATH === "string" && params.env.PATH.length > 0;
if (hasCustomPath) {
// Avoid interpolating PATH into the shell command; pass it via env instead.
args.push("-e", `CLAWDBOT_PREPEND_PATH=${params.env.PATH}`);
}
// Login shell (-l) sources /etc/profile which resets PATH to a minimal set,
// overriding both Docker ENV and -e PATH=... environment variables.
// Prepend custom PATH after profile sourcing to ensure custom tools are accessible
// while preserving system paths that /etc/profile may have added.
const pathExport = params.env.PATH ? `export PATH="${params.env.PATH}:$PATH"; ` : "";
const pathExport = hasCustomPath
? 'export PATH="${CLAWDBOT_PREPEND_PATH}:$PATH"; unset CLAWDBOT_PREPEND_PATH; '
: "";
args.push(params.containerName, "sh", "-lc", `${pathExport}${params.command}`);
return args;
}

View File

@ -318,9 +318,30 @@ describe("buildDockerExecArgs", () => {
});
const commandArg = args[args.length - 1];
expect(commandArg).toContain('export PATH="/custom/bin:/usr/local/bin:/usr/bin:$PATH"');
expect(args).toContain("CLAWDBOT_PREPEND_PATH=/custom/bin:/usr/local/bin:/usr/bin");
expect(commandArg).toContain('export PATH="${CLAWDBOT_PREPEND_PATH}:$PATH"');
expect(commandArg).toContain("echo hello");
expect(commandArg).toBe('export PATH="/custom/bin:/usr/local/bin:/usr/bin:$PATH"; echo hello');
expect(commandArg).toBe(
'export PATH="${CLAWDBOT_PREPEND_PATH}:$PATH"; unset CLAWDBOT_PREPEND_PATH; echo hello',
);
});
it("does not interpolate PATH into the shell command", () => {
const injectedPath = "$(touch /tmp/clawdbot-path-injection)";
const args = buildDockerExecArgs({
containerName: "test-container",
command: "echo hello",
env: {
PATH: injectedPath,
HOME: "/home/user",
},
tty: false,
});
const commandArg = args[args.length - 1];
expect(args).toContain(`CLAWDBOT_PREPEND_PATH=${injectedPath}`);
expect(commandArg).not.toContain(injectedPath);
expect(commandArg).toContain("CLAWDBOT_PREPEND_PATH");
});
it("does not add PATH export when PATH is not in env", () => {

View File

@ -48,7 +48,7 @@ export async function fetchBrowserJson<T>(
const timeoutMs = init?.timeoutMs ?? 5000;
try {
if (isAbsoluteHttp(url)) {
return await fetchHttpJson<T>(url, { ...(init ?? {}), timeoutMs });
return await fetchHttpJson<T>(url, init ? { ...init, timeoutMs } : { timeoutMs });
}
const started = await startBrowserControlServiceFromConfig();
if (!started) {

View File

@ -1 +1 @@
a99455ba0c4d0aad0a110bf25440c208b798198d5524b269f0f2d3f984262ae4
6d63b866aa0e917b278c6bef42229e8cd1f43c8ba31c845a96b4d9d5ce780265

File diff suppressed because one or more lines are too long

View File

@ -27,7 +27,7 @@ export function registerBrowserElementCommands(
.filter(Boolean)
: undefined;
try {
const result = await callBrowserAct({
const result = await callBrowserAct<{ url?: string }>({
parent,
profile,
body: {

View File

@ -26,7 +26,7 @@ export function registerBrowserFilesAndDownloadsCommands(
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined;
const result = await callBrowserRequest(
const result = await callBrowserRequest<{ download: { path: string } }>(
parent,
{
method: "POST",
@ -68,7 +68,7 @@ export function registerBrowserFilesAndDownloadsCommands(
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined;
const result = await callBrowserRequest(
const result = await callBrowserRequest<{ download: { path: string } }>(
parent,
{
method: "POST",
@ -108,7 +108,7 @@ export function registerBrowserFilesAndDownloadsCommands(
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined;
const result = await callBrowserRequest(
const result = await callBrowserRequest<{ download: { path: string } }>(
parent,
{
method: "POST",

View File

@ -21,7 +21,7 @@ export function registerBrowserFormWaitEvalCommands(
fields: opts.fields,
fieldsFile: opts.fieldsFile,
});
const result = await callBrowserAct({
const result = await callBrowserAct<{ result?: unknown }>({
parent,
profile,
body: {
@ -66,7 +66,7 @@ export function registerBrowserFormWaitEvalCommands(
? (opts.load as "load" | "domcontentloaded" | "networkidle")
: undefined;
const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined;
const result = await callBrowserAct({
const result = await callBrowserAct<{ result?: unknown }>({
parent,
profile,
body: {
@ -108,7 +108,7 @@ export function registerBrowserFormWaitEvalCommands(
return;
}
try {
const result = await callBrowserAct({
const result = await callBrowserAct<{ result?: unknown }>({
parent,
profile,
body: {

View File

@ -0,0 +1,40 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { copyA2uiAssets } from "../../scripts/canvas-a2ui-copy.js";
describe("canvas a2ui copy", () => {
it("throws a helpful error when assets are missing", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-a2ui-"));
try {
await expect(copyA2uiAssets({ srcDir: dir, outDir: path.join(dir, "out") })).rejects.toThrow(
'Run "pnpm canvas:a2ui:bundle"',
);
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
});
it("copies bundled assets to dist", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-a2ui-"));
const srcDir = path.join(dir, "src");
const outDir = path.join(dir, "dist");
try {
await fs.mkdir(srcDir, { recursive: true });
await fs.writeFile(path.join(srcDir, "index.html"), "<html></html>", "utf8");
await fs.writeFile(path.join(srcDir, "a2ui.bundle.js"), "console.log(1);", "utf8");
await copyA2uiAssets({ srcDir, outDir });
await expect(fs.stat(path.join(outDir, "index.html"))).resolves.toBeTruthy();
await expect(fs.stat(path.join(outDir, "a2ui.bundle.js"))).resolves.toBeTruthy();
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
});
});

View File

@ -2,7 +2,7 @@ import { listChannelPlugins } from "../channels/plugins/index.js";
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
import type { ChannelId } from "../channels/plugins/types.js";
import type { ClawdbotConfig } from "../config/config.js";
import { resolveBrowserConfig } from "../browser/config.js";
import { resolveBrowserConfig, resolveProfile } from "../browser/config.js";
import { resolveConfigPath, resolveStateDir } from "../config/paths.js";
import { resolveGatewayAuth } from "../gateway/auth.js";
import { formatCliCommand } from "../cli/command-format.js";

View File

@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { generateUUID } from "./uuid";
@ -26,7 +26,13 @@ describe("generateUUID", () => {
});
it("still returns a v4 UUID when crypto is missing", () => {
const id = generateUUID(null);
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
try {
const id = generateUUID(null);
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
expect(warnSpy).toHaveBeenCalled();
} finally {
warnSpy.mockRestore();
}
});
});

View File

@ -3,6 +3,8 @@ export type CryptoLike = {
getRandomValues?: ((array: Uint8Array) => Uint8Array) | undefined;
};
let warnedWeakCrypto = false;
function uuidFromBytes(bytes: Uint8Array): string {
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 1
@ -29,6 +31,12 @@ function weakRandomBytes(): Uint8Array {
return bytes;
}
function warnWeakCryptoOnce() {
if (warnedWeakCrypto) return;
warnedWeakCrypto = true;
console.warn("[uuid] crypto API missing; falling back to weak randomness");
}
export function generateUUID(cryptoLike: CryptoLike | null = globalThis.crypto): string {
if (cryptoLike && typeof cryptoLike.randomUUID === "function") return cryptoLike.randomUUID();
@ -38,5 +46,6 @@ export function generateUUID(cryptoLike: CryptoLike | null = globalThis.crypto):
return uuidFromBytes(bytes);
}
warnWeakCryptoOnce();
return uuidFromBytes(weakRandomBytes());
}