Gateway: hide BOOTSTRAP in agent files after onboarding completes (#17491)
Merged via /review-pr -> /prepare-pr -> /merge-pr. Prepared head SHA: f95f6dd052daf618bac6ed16bb4a8112a376d47d Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Reviewed-by: @gumadeiras
This commit is contained in:
committed by
GitHub
parent
3cd786cc2d
commit
b4f14d6f7a
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Web UI/Agents: hide `BOOTSTRAP.md` in the Agents Files list after onboarding is completed, avoiding confusing missing-file warnings for completed workspaces. (#17491) Thanks @gumadeiras.
|
||||
- Telegram: omit `message_thread_id` for DM sends/draft previews and keep forum-topic handling (`id=1` general omitted, non-general kept), preventing DM failures with `400 Bad Request: message thread not found`. (#10942) Thanks @garnetlyx.
|
||||
- Subagents/Models: preserve `agents.defaults.model.fallbacks` when subagent sessions carry a model override, so subagent runs fail over to configured fallback models instead of retrying only the overridden primary model.
|
||||
- Config/Gateway: make sensitive-key whitelist suffix matching case-insensitive while preserving `passwordFile` path exemptions, preventing accidental redaction of non-secret config values like `maxTokens` and IRC password-file paths. (#16042) Thanks @akramcodez.
|
||||
|
||||
@@ -184,6 +184,18 @@ async function readWorkspaceOnboardingState(statePath: string): Promise<Workspac
|
||||
}
|
||||
}
|
||||
|
||||
async function readWorkspaceOnboardingStateForDir(dir: string): Promise<WorkspaceOnboardingState> {
|
||||
const statePath = resolveWorkspaceStatePath(resolveUserPath(dir));
|
||||
return await readWorkspaceOnboardingState(statePath);
|
||||
}
|
||||
|
||||
export async function isWorkspaceOnboardingCompleted(dir: string): Promise<boolean> {
|
||||
const state = await readWorkspaceOnboardingStateForDir(dir);
|
||||
return (
|
||||
typeof state.onboardingCompletedAt === "string" && state.onboardingCompletedAt.trim().length > 0
|
||||
);
|
||||
}
|
||||
|
||||
async function writeWorkspaceOnboardingState(
|
||||
statePath: string,
|
||||
state: WorkspaceOnboardingState,
|
||||
|
||||
@@ -25,6 +25,8 @@ const mocks = vi.hoisted(() => ({
|
||||
fsAccess: vi.fn(async () => {}),
|
||||
fsMkdir: vi.fn(async () => undefined),
|
||||
fsAppendFile: vi.fn(async () => {}),
|
||||
fsReadFile: vi.fn(async () => ""),
|
||||
fsStat: vi.fn(async () => null),
|
||||
}));
|
||||
|
||||
vi.mock("../../config/config.js", () => ({
|
||||
@@ -81,6 +83,8 @@ vi.mock("node:fs/promises", async () => {
|
||||
access: mocks.fsAccess,
|
||||
mkdir: mocks.fsMkdir,
|
||||
appendFile: mocks.fsAppendFile,
|
||||
readFile: mocks.fsReadFile,
|
||||
stat: mocks.fsStat,
|
||||
};
|
||||
return { ...patched, default: patched };
|
||||
});
|
||||
@@ -109,6 +113,27 @@ function makeCall(method: keyof typeof agentsHandlers, params: Record<string, un
|
||||
return { respond, promise };
|
||||
}
|
||||
|
||||
function createEnoentError() {
|
||||
const err = new Error("ENOENT") as NodeJS.ErrnoException;
|
||||
err.code = "ENOENT";
|
||||
return err;
|
||||
}
|
||||
|
||||
function createErrnoError(code: string) {
|
||||
const err = new Error(code) as NodeJS.ErrnoException;
|
||||
err.code = code;
|
||||
return err;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.fsReadFile.mockImplementation(async () => {
|
||||
throw createEnoentError();
|
||||
});
|
||||
mocks.fsStat.mockImplementation(async () => {
|
||||
throw createEnoentError();
|
||||
});
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Tests */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -371,3 +396,53 @@ describe("agents.delete", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("agents.files.list", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.loadConfigReturn = {};
|
||||
});
|
||||
|
||||
it("includes BOOTSTRAP.md when onboarding has not completed", async () => {
|
||||
const { respond, promise } = makeCall("agents.files.list", { agentId: "main" });
|
||||
await promise;
|
||||
|
||||
const [, result] = respond.mock.calls[0] ?? [];
|
||||
const files = (result as { files: Array<{ name: string }> }).files;
|
||||
expect(files.some((file) => file.name === "BOOTSTRAP.md")).toBe(true);
|
||||
});
|
||||
|
||||
it("hides BOOTSTRAP.md when workspace onboarding is complete", async () => {
|
||||
mocks.fsReadFile.mockImplementation(async (filePath: string | URL | number) => {
|
||||
if (String(filePath).endsWith("workspace-state.json")) {
|
||||
return JSON.stringify({
|
||||
onboardingCompletedAt: "2026-02-15T14:00:00.000Z",
|
||||
});
|
||||
}
|
||||
throw createEnoentError();
|
||||
});
|
||||
|
||||
const { respond, promise } = makeCall("agents.files.list", { agentId: "main" });
|
||||
await promise;
|
||||
|
||||
const [, result] = respond.mock.calls[0] ?? [];
|
||||
const files = (result as { files: Array<{ name: string }> }).files;
|
||||
expect(files.some((file) => file.name === "BOOTSTRAP.md")).toBe(false);
|
||||
});
|
||||
|
||||
it("falls back to showing BOOTSTRAP.md when workspace state cannot be read", async () => {
|
||||
mocks.fsReadFile.mockImplementation(async (filePath: string | URL | number) => {
|
||||
if (String(filePath).endsWith("workspace-state.json")) {
|
||||
throw createErrnoError("EACCES");
|
||||
}
|
||||
throw createEnoentError();
|
||||
});
|
||||
|
||||
const { respond, promise } = makeCall("agents.files.list", { agentId: "main" });
|
||||
await promise;
|
||||
|
||||
const [, result] = respond.mock.calls[0] ?? [];
|
||||
const files = (result as { files: Array<{ name: string }> }).files;
|
||||
expect(files.some((file) => file.name === "BOOTSTRAP.md")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
DEFAULT_TOOLS_FILENAME,
|
||||
DEFAULT_USER_FILENAME,
|
||||
ensureAgentWorkspace,
|
||||
isWorkspaceOnboardingCompleted,
|
||||
} from "../../agents/workspace.js";
|
||||
import { movePathToTrash } from "../../browser/trash.js";
|
||||
import {
|
||||
@@ -52,6 +53,9 @@ const BOOTSTRAP_FILE_NAMES = [
|
||||
DEFAULT_HEARTBEAT_FILENAME,
|
||||
DEFAULT_BOOTSTRAP_FILENAME,
|
||||
] as const;
|
||||
const BOOTSTRAP_FILE_NAMES_POST_ONBOARDING = BOOTSTRAP_FILE_NAMES.filter(
|
||||
(name) => name !== DEFAULT_BOOTSTRAP_FILENAME,
|
||||
);
|
||||
|
||||
const MEMORY_FILE_NAMES = [DEFAULT_MEMORY_FILENAME, DEFAULT_MEMORY_ALT_FILENAME] as const;
|
||||
|
||||
@@ -108,7 +112,7 @@ async function statFile(filePath: string): Promise<FileMeta | null> {
|
||||
}
|
||||
}
|
||||
|
||||
async function listAgentFiles(workspaceDir: string) {
|
||||
async function listAgentFiles(workspaceDir: string, options?: { hideBootstrap?: boolean }) {
|
||||
const files: Array<{
|
||||
name: string;
|
||||
path: string;
|
||||
@@ -117,7 +121,10 @@ async function listAgentFiles(workspaceDir: string) {
|
||||
updatedAtMs?: number;
|
||||
}> = [];
|
||||
|
||||
for (const name of BOOTSTRAP_FILE_NAMES) {
|
||||
const bootstrapFileNames = options?.hideBootstrap
|
||||
? BOOTSTRAP_FILE_NAMES_POST_ONBOARDING
|
||||
: BOOTSTRAP_FILE_NAMES;
|
||||
for (const name of bootstrapFileNames) {
|
||||
const filePath = path.join(workspaceDir, name);
|
||||
const meta = await statFile(filePath);
|
||||
if (meta) {
|
||||
@@ -417,7 +424,13 @@ export const agentsHandlers: GatewayRequestHandlers = {
|
||||
return;
|
||||
}
|
||||
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
||||
const files = await listAgentFiles(workspaceDir);
|
||||
let hideBootstrap = false;
|
||||
try {
|
||||
hideBootstrap = await isWorkspaceOnboardingCompleted(workspaceDir);
|
||||
} catch {
|
||||
// Fall back to showing BOOTSTRAP if workspace state cannot be read.
|
||||
}
|
||||
const files = await listAgentFiles(workspaceDir, { hideBootstrap });
|
||||
respond(true, { agentId, workspace: workspaceDir, files }, undefined);
|
||||
},
|
||||
"agents.files.get": async ({ params, respond }) => {
|
||||
|
||||
Reference in New Issue
Block a user