feat(workspace): add skipOptionalBootstrapFiles config option (#62110)

Adds `agents.defaults.skipOptionalBootstrapFiles` for optional workspace bootstrap files, validates the supported filenames, and propagates the option through workspace bootstrap callers.

Also preserves legacy setup detection when `USER.md` or `IDENTITY.md` are intentionally skipped, documents the config field, and includes focused regression coverage.

Landing follow-up included small CI unblockers for current-base drift: removing an unused Brave runtime dependency, fixing Telegram RTT lint, and preserving compatible gateway-bindable plugin registry cache reuse when runtime ensures disable bundled dependency installation.
This commit is contained in:
mainstay22
2026-05-01 04:08:22 -05:00
committed by GitHub
parent e5208bd331
commit 94543092be
28 changed files with 251 additions and 40 deletions

View File

@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
### Changes
- Agents/workspace: add `agents.defaults.skipOptionalBootstrapFiles` for skipping selected optional workspace files during bootstrap without disabling required workspace setup. (#62110) Thanks @mainstay22.
- Voice Call/Google Meet: add Twilio Meet join phase logs around pre-connect DTMF, realtime stream setup, and initial greeting handoff for easier live-call debugging. Thanks @donkeykong91 and @PfanP.
- macOS app: move recent session context rows into a Context submenu while keeping usage and cost details root-level, so the menu bar companion stays compact with many active sessions. Thanks @guti.
- Gateway/SDK: add SDK-facing tools.invoke RPC with shared HTTP policy, typed approval/refusal results, and SDK helper support. Refs #74705. Thanks @BunsDev and @ai-hpc.

View File

@@ -1,4 +1,4 @@
d70e31fd5f36d4b117ffa750fba88072d6714edc245a18d4b0915a2d11ce603a config-baseline.json
0a259216178a582c567d1fa48c5236bff4bbd27c3e6af838ffcd042459ffce3c config-baseline.core.json
516de8f5049d2c8b7f326cfc1b665cf459609aa491c432d93b8ca8b9463d7243 config-baseline.json
b06e5cd6e7d3a26d99fd4d31d576c49958195451b0b1e9c2db45f038a3c16c44 config-baseline.core.json
da8e055ebba0730498703d209f9e2cfaa1484a83f3240e611dcdd7280e22a525 config-baseline.channel.json
4d017161b4dc986fdc6cc68167fedbd1d415ddbcd66125a872e18aa1769cd182 config-baseline.plugin.json

View File

@@ -67,6 +67,20 @@ Disables automatic creation of workspace bootstrap files (`AGENTS.md`, `SOUL.md`
}
```
### `agents.defaults.skipOptionalBootstrapFiles`
Skips creation of selected optional workspace files while still writing required bootstrap files. Valid values: `SOUL.md`, `USER.md`, `HEARTBEAT.md`, and `IDENTITY.md`.
```json5
{
agents: {
defaults: {
skipOptionalBootstrapFiles: ["SOUL.md", "USER.md"],
},
},
}
```
### `agents.defaults.contextInjection`
Controls when workspace bootstrap files are injected into the system prompt. Default: `"always"`.

View File

@@ -4,9 +4,6 @@
"private": true,
"description": "OpenClaw Brave plugin",
"type": "module",
"dependencies": {
"typebox": "1.1.34"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"
},

4
pnpm-lock.yaml generated
View File

@@ -327,10 +327,6 @@ importers:
version: link:../../packages/plugin-sdk
extensions/brave:
dependencies:
typebox:
specifier: 1.1.34
version: 1.1.34
devDependencies:
'@openclaw/plugin-sdk':
specifier: workspace:*

View File

@@ -10,12 +10,12 @@ const timeoutMs = Number(process.env.OPENCLAW_QA_TELEGRAM_SCENARIO_TIMEOUT_MS ??
const canaryTimeoutMs = Number(
process.env.OPENCLAW_QA_TELEGRAM_CANARY_TIMEOUT_MS ?? String(timeoutMs),
);
const scenarioIds = (
process.env.OPENCLAW_NPM_TELEGRAM_SCENARIOS ?? "telegram-mentioned-message-reply"
)
.split(",")
.map((value) => value.trim())
.filter(Boolean);
const scenarioIds = new Set(
(process.env.OPENCLAW_NPM_TELEGRAM_SCENARIOS ?? "telegram-mentioned-message-reply")
.split(",")
.map((value) => value.trim())
.filter(Boolean),
);
if (!groupId || !driverToken || !sutToken) {
throw new Error(
@@ -63,10 +63,6 @@ function messageText(message) {
return message.text ?? message.caption ?? "";
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function flushUpdates(bot) {
let updates = await bot.getUpdates({ timeout: 0, allowed_updates: ["message"] });
let nextOffset;
@@ -194,7 +190,7 @@ async function main() {
}),
);
if (scenarioIds.includes("telegram-mentioned-message-reply")) {
if (scenarioIds.has("telegram-mentioned-message-reply")) {
const marker = `OPENCLAW_RTT_${Date.now().toString(36)}`;
scenarios.push(
await runScenario({

View File

@@ -373,6 +373,7 @@ async function prepareAgentCommandExecution(
const workspace = await ensureAgentWorkspace({
dir: workspaceDirRaw,
ensureBootstrapFiles: !agentCfg?.skipBootstrap,
skipOptionalBootstrapFiles: agentCfg?.skipOptionalBootstrapFiles,
});
const workspaceDir = workspace.dir;
const runId = opts.runId?.trim() || sessionId;

View File

@@ -49,6 +49,7 @@ async function ensureSandboxWorkspaceLayout(params: {
sandboxWorkspaceDir,
agentWorkspaceDir,
params.config?.agents?.defaults?.skipBootstrap,
params.config?.agents?.defaults?.skipOptionalBootstrapFiles,
);
if (cfg.workspaceAccess !== "rw") {
try {

View File

@@ -1,6 +1,7 @@
import syncFs from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import type { OptionalBootstrapFileName } from "../../config/types.agent-defaults.js";
import { openBoundaryFile } from "../../infra/boundary-file-read.js";
import { resolveUserPath } from "../../utils.js";
import {
@@ -18,6 +19,7 @@ export async function ensureSandboxWorkspace(
workspaceDir: string,
seedFrom?: string,
skipBootstrap?: boolean,
skipOptionalBootstrapFiles?: OptionalBootstrapFileName[],
) {
await fs.mkdir(workspaceDir, { recursive: true });
if (seedFrom) {
@@ -61,5 +63,6 @@ export async function ensureSandboxWorkspace(
await ensureAgentWorkspace({
dir: workspaceDir,
ensureBootstrapFiles: !skipBootstrap,
skipOptionalBootstrapFiles,
});
}

View File

@@ -155,6 +155,55 @@ describe("ensureAgentWorkspace", () => {
await expectCompletedWithoutBootstrap(tempDir);
});
it("skips configured optional bootstrap files without skipping required files", async () => {
const tempDir = await makeTempWorkspace("openclaw-workspace-");
await ensureAgentWorkspace({
dir: tempDir,
ensureBootstrapFiles: true,
skipOptionalBootstrapFiles: [
DEFAULT_SOUL_FILENAME,
DEFAULT_IDENTITY_FILENAME,
DEFAULT_USER_FILENAME,
DEFAULT_HEARTBEAT_FILENAME,
],
});
await expect(fs.access(path.join(tempDir, DEFAULT_AGENTS_FILENAME))).resolves.toBeUndefined();
await expect(fs.access(path.join(tempDir, DEFAULT_TOOLS_FILENAME))).resolves.toBeUndefined();
await expect(
fs.access(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME)),
).resolves.toBeUndefined();
for (const fileName of [
DEFAULT_SOUL_FILENAME,
DEFAULT_IDENTITY_FILENAME,
DEFAULT_USER_FILENAME,
DEFAULT_HEARTBEAT_FILENAME,
]) {
await expect(fs.access(path.join(tempDir, fileName))).rejects.toMatchObject({
code: "ENOENT",
});
}
});
it("preserves legacy setup detection when skipped profile files already exist", async () => {
const tempDir = await makeTempWorkspace("openclaw-workspace-");
await writeWorkspaceFile({ dir: tempDir, name: DEFAULT_IDENTITY_FILENAME, content: "custom" });
await writeWorkspaceFile({ dir: tempDir, name: DEFAULT_USER_FILENAME, content: "custom" });
await ensureAgentWorkspace({
dir: tempDir,
ensureBootstrapFiles: true,
skipOptionalBootstrapFiles: [DEFAULT_IDENTITY_FILENAME, DEFAULT_USER_FILENAME],
});
await expect(fs.access(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME))).rejects.toMatchObject({
code: "ENOENT",
});
const state = await readWorkspaceState(tempDir);
expect(state.setupCompletedAt).toMatch(/\d{4}-\d{2}-\d{2}T/);
});
it("migrates legacy onboardingCompletedAt markers to setupCompletedAt", async () => {
const tempDir = await makeTempWorkspace("openclaw-workspace-");
await fs.mkdir(path.join(tempDir, ".openclaw"), { recursive: true });

View File

@@ -174,6 +174,13 @@ const VALID_BOOTSTRAP_NAMES: ReadonlySet<string> = new Set([
DEFAULT_MEMORY_FILENAME,
]);
const OPTIONAL_BOOTSTRAP_FILENAMES: ReadonlySet<string> = new Set([
DEFAULT_SOUL_FILENAME,
DEFAULT_IDENTITY_FILENAME,
DEFAULT_USER_FILENAME,
DEFAULT_HEARTBEAT_FILENAME,
]);
async function writeFileIfMissing(filePath: string, content: string): Promise<boolean> {
try {
await fs.writeFile(filePath, content, {
@@ -467,6 +474,12 @@ async function ensureGitRepo(dir: string, isBrandNewWorkspace: boolean) {
export async function ensureAgentWorkspace(params?: {
dir?: string;
ensureBootstrapFiles?: boolean;
/**
* List of optional bootstrap filenames to skip writing.
* Applies only to SOUL.md, USER.md, HEARTBEAT.md, IDENTITY.md.
* Required workspace setup such as AGENTS.md and TOOLS.md still runs.
*/
skipOptionalBootstrapFiles?: string[];
}): Promise<{
dir: string;
agentsPath?: string;
@@ -519,12 +532,24 @@ export async function ensureAgentWorkspace(params?: {
const identityTemplate = await loadTemplate(DEFAULT_IDENTITY_FILENAME);
const userTemplate = await loadTemplate(DEFAULT_USER_FILENAME);
const heartbeatTemplate = await loadTemplate(DEFAULT_HEARTBEAT_FILENAME);
const skipOptionalBootstrapFiles = new Set(params?.skipOptionalBootstrapFiles ?? []);
const shouldWriteBootstrapFile = (fileName: string): boolean =>
!OPTIONAL_BOOTSTRAP_FILENAMES.has(fileName) || !skipOptionalBootstrapFiles.has(fileName);
await writeFileIfMissing(agentsPath, agentsTemplate);
await writeFileIfMissing(soulPath, soulTemplate);
if (shouldWriteBootstrapFile(DEFAULT_SOUL_FILENAME)) {
await writeFileIfMissing(soulPath, soulTemplate);
}
await writeFileIfMissing(toolsPath, toolsTemplate);
const identityPathCreated = await writeFileIfMissing(identityPath, identityTemplate);
await writeFileIfMissing(userPath, userTemplate);
await writeFileIfMissing(heartbeatPath, heartbeatTemplate);
const identityPathCreated = shouldWriteBootstrapFile(DEFAULT_IDENTITY_FILENAME)
? await writeFileIfMissing(identityPath, identityTemplate)
: false;
if (shouldWriteBootstrapFile(DEFAULT_USER_FILENAME)) {
await writeFileIfMissing(userPath, userTemplate);
}
if (shouldWriteBootstrapFile(DEFAULT_HEARTBEAT_FILENAME)) {
await writeFileIfMissing(heartbeatPath, heartbeatTemplate);
}
let state = await readWorkspaceSetupState(statePath, {
persistLegacyMigration: true,

View File

@@ -242,6 +242,7 @@ export async function getReplyFromConfig(
: await ensureAgentWorkspace({
dir: workspaceDirRaw,
ensureBootstrapFiles: !agentCfg?.skipBootstrap && !isFastTestEnv,
skipOptionalBootstrapFiles: agentCfg?.skipOptionalBootstrapFiles,
});
const workspaceDir = workspace.dir;
const agentDir = resolveAgentDir(cfg, agentId);

View File

@@ -177,6 +177,7 @@ export async function agentsAddCommand(
const quietRuntime = opts.json ? createQuietRuntime(runtime) : runtime;
await ensureWorkspaceAndSessions(workspaceDir, quietRuntime, {
skipBootstrap: Boolean(bindingResult.config.agents?.defaults?.skipBootstrap),
skipOptionalBootstrapFiles: bindingResult.config.agents?.defaults?.skipOptionalBootstrapFiles,
agentId,
});
@@ -431,6 +432,7 @@ export async function agentsAddCommand(
logConfigUpdated(runtime);
await ensureWorkspaceAndSessions(workspaceDir, runtime, {
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles,
agentId,
});

View File

@@ -589,7 +589,10 @@ export async function runConfigureWizard(
},
},
};
await ensureWorkspaceAndSessions(workspaceDir, runtime);
await ensureWorkspaceAndSessions(workspaceDir, runtime, {
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles,
});
};
const configureChannelsSection = async () => {

View File

@@ -6,6 +6,7 @@ import { DEFAULT_AGENT_WORKSPACE_DIR, ensureAgentWorkspace } from "../agents/wor
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
import { resolveConfigPath } from "../config/paths.js";
import { resolveSessionTranscriptsDirForAgent } from "../config/sessions/paths.js";
import type { OptionalBootstrapFileName } from "../config/types.agent-defaults.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveControlUiLinks } from "../gateway/control-ui-links.js";
import { normalizeControlUiBasePath } from "../gateway/control-ui-shared.js";
@@ -164,11 +165,16 @@ function resolveSshTargetHint(): string {
export async function ensureWorkspaceAndSessions(
workspaceDir: string,
runtime: RuntimeEnv,
options?: { skipBootstrap?: boolean; agentId?: string },
options?: {
skipBootstrap?: boolean;
skipOptionalBootstrapFiles?: OptionalBootstrapFileName[];
agentId?: string;
},
) {
const ws = await ensureAgentWorkspace({
dir: workspaceDir,
ensureBootstrapFiles: !options?.skipBootstrap,
skipOptionalBootstrapFiles: options?.skipOptionalBootstrapFiles,
});
runtime.log(`Workspace OK: ${shortenHomePath(ws.dir)}`);
const sessionsDir = resolveSessionTranscriptsDirForAgent(options?.agentId);

View File

@@ -214,6 +214,7 @@ export async function runNonInteractiveLocalSetup(params: {
await ensureWorkspaceAndSessions(workspaceDir, runtime, {
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles,
});
const daemonRuntimeRaw = opts.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME;

View File

@@ -84,6 +84,42 @@ describe("setupCommand", () => {
});
});
it("threads skipOptionalBootstrapFiles into workspace creation", async () => {
await withTempHome(async (home) => {
const runtime = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
const configDir = path.join(home, ".openclaw");
const configPath = path.join(configDir, "openclaw.json");
const deps = createSetupDeps(home);
const workspace = path.join(home, "custom-workspace");
await fs.mkdir(configDir, { recursive: true });
await fs.writeFile(
configPath,
JSON.stringify({
agents: {
defaults: {
workspace,
skipOptionalBootstrapFiles: ["IDENTITY.md", "USER.md"],
},
},
}),
);
await setupCommand(undefined, runtime, deps);
expect(deps.ensureAgentWorkspace).toHaveBeenCalledWith(
expect.objectContaining({
dir: workspace,
skipOptionalBootstrapFiles: ["IDENTITY.md", "USER.md"],
}),
);
});
});
it("treats non-object config roots as empty config", async () => {
await withTempHome(async (home) => {
const runtime = {

View File

@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import JSON5 from "json5";
import { z } from "zod";
import type { OptionalBootstrapFileName } from "../config/types.agent-defaults.js";
import type { OpenClawConfig } from "../config/types.js";
import type { RuntimeEnv } from "../runtime.js";
import { defaultRuntime } from "../runtime.js";
@@ -16,6 +17,7 @@ type ConfigIO = {
type EnsureAgentWorkspace = (params: {
dir: string;
ensureBootstrapFiles?: boolean;
skipOptionalBootstrapFiles?: OptionalBootstrapFileName[];
}) => Promise<{ dir: string }>;
type SetupCommandDeps = {
@@ -191,6 +193,7 @@ export async function setupCommand(
const ws = await (deps.ensureAgentWorkspace ?? ensureDefaultAgentWorkspace)({
dir: workspace,
ensureBootstrapFiles: !next.agents?.defaults?.skipBootstrap,
skipOptionalBootstrapFiles: next.agents?.defaults?.skipOptionalBootstrapFiles,
});
runtime.log(`Workspace OK: ${shortenHomePath(ws.dir)}`);

View File

@@ -3690,6 +3690,16 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
skipBootstrap: {
type: "boolean",
},
skipOptionalBootstrapFiles: {
type: "array",
items: {
type: "string",
enum: ["SOUL.md", "USER.md", "HEARTBEAT.md", "IDENTITY.md"],
},
title: "Skipped Optional Bootstrap Files",
description:
"Optional bootstrap files that should not be created in agent workspaces. Valid values: SOUL.md, USER.md, HEARTBEAT.md, IDENTITY.md.",
},
contextInjection: {
anyOf: [
{
@@ -26184,6 +26194,11 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
help: 'Friendly interaction-style layer for GPT-5-family models ("friendly" or "on" enables it; "off" disables only that layer). The tagged behavior contract remains enabled for matching GPT-5 models.',
tags: ["advanced"],
},
"agents.defaults.skipOptionalBootstrapFiles": {
label: "Skipped Optional Bootstrap Files",
help: "Optional bootstrap files that should not be created in agent workspaces. Valid values: SOUL.md, USER.md, HEARTBEAT.md, IDENTITY.md.",
tags: ["storage"],
},
"agents.defaults.contextInjection": {
label: "Context Injection",
help: 'Controls when workspace bootstrap files are injected into the system prompt: "always" (default) or "continuation-skip" for safe continuation turns after a completed assistant response.',

View File

@@ -958,6 +958,8 @@ export const FIELD_HELP: Record<string, string> = {
"Maximum same-provider auth-profile rotations allowed for rate-limit errors before switching to model fallback (default: 1).",
"agents.defaults.workspace":
"Default workspace path exposed to agent runtime tools for filesystem context and repo-aware behavior. Set this explicitly when running from wrappers so path resolution stays deterministic.",
"agents.defaults.skipOptionalBootstrapFiles":
"Optional bootstrap files that should not be created in agent workspaces. Valid values: SOUL.md, USER.md, HEARTBEAT.md, IDENTITY.md.",
"agents.defaults.contextInjection":
'Controls when workspace bootstrap files are injected into the system prompt: "always" (default) or "continuation-skip" for safe continuation turns after a completed assistant response.',
"agents.defaults.bootstrapMaxChars":

View File

@@ -387,6 +387,7 @@ export const FIELD_LABELS: Record<string, string> = {
"agents.defaults.promptOverlays": "Prompt Overlays",
"agents.defaults.promptOverlays.gpt5": "GPT-5 Prompt Overlay",
"agents.defaults.promptOverlays.gpt5.personality": "GPT-5 Personality Overlay",
"agents.defaults.skipOptionalBootstrapFiles": "Skipped Optional Bootstrap Files",
"agents.defaults.contextInjection": "Context Injection",
"agents.defaults.bootstrapMaxChars": "Bootstrap Max Chars",
"agents.defaults.bootstrapTotalMaxChars": "Bootstrap Total Max Chars",

View File

@@ -17,6 +17,7 @@ import type {
import type { MemorySearchConfig } from "./types.tools.js";
export type AgentContextInjection = "always" | "continuation-skip" | "never";
export type OptionalBootstrapFileName = "SOUL.md" | "USER.md" | "HEARTBEAT.md" | "IDENTITY.md";
export type EmbeddedPiExecutionContract = "default" | "strict-agentic";
export type Gpt5PromptOverlayConfig = {
@@ -224,6 +225,13 @@ export type AgentDefaultsConfig = {
promptOverlays?: PromptOverlaysConfig;
/** Skip bootstrap (BOOTSTRAP.md creation, etc.) for pre-configured deployments. */
skipBootstrap?: boolean;
/**
* List of optional bootstrap filenames to skip writing to the workspace root.
* Applies to: SOUL.md, USER.md, HEARTBEAT.md, IDENTITY.md.
* Required workspace setup such as AGENTS.md and TOOLS.md still runs.
* Example: ["SOUL.md", "USER.md", "HEARTBEAT.md", "IDENTITY.md"]
*/
skipOptionalBootstrapFiles?: OptionalBootstrapFileName[];
/**
* Controls when workspace bootstrap files (AGENTS.md, SOUL.md, etc.) are
* injected into the system prompt:

View File

@@ -83,6 +83,25 @@ describe("agent defaults schema", () => {
expect(() => AgentDefaultsSchema.parse({ contextInjection: "unknown" })).toThrow();
});
it("accepts supported optional bootstrap filenames", () => {
const result = AgentDefaultsSchema.parse({
skipOptionalBootstrapFiles: ["SOUL.md", "USER.md", "HEARTBEAT.md", "IDENTITY.md"],
})!;
expect(result.skipOptionalBootstrapFiles).toEqual([
"SOUL.md",
"USER.md",
"HEARTBEAT.md",
"IDENTITY.md",
]);
});
it("rejects unsupported optional bootstrap filenames", () => {
expect(() =>
AgentDefaultsSchema.parse({ skipOptionalBootstrapFiles: ["AGENTS.md"] }),
).toThrow();
expect(() => AgentDefaultsSchema.parse({ skipOptionalBootstrapFiles: ["SOUL.MD"] })).toThrow();
});
it("accepts embeddedPi.executionContract", () => {
const result = AgentDefaultsSchema.parse({
embeddedPi: {

View File

@@ -24,6 +24,13 @@ const NonNegativeByteSizeSchema = z.union([
z.string().refine(isValidNonNegativeByteSizeString, "Expected byte size string like 2mb"),
]);
const OptionalBootstrapFileNameSchema = z.enum([
"SOUL.md",
"USER.md",
"HEARTBEAT.md",
"IDENTITY.md",
]);
export const SilentReplyPolicyConfigSchema = z
.object({
direct: SilentReplyPolicySchema.optional(),
@@ -89,6 +96,7 @@ export const AgentDefaultsSchema = z
.strict()
.optional(),
skipBootstrap: z.boolean().optional(),
skipOptionalBootstrapFiles: z.array(OptionalBootstrapFileNameSchema).optional(),
contextInjection: z
.union([z.literal("always"), z.literal("continuation-skip"), z.literal("never")])
.optional(),
@@ -231,9 +239,7 @@ export const AgentDefaultsSchema = z
])
.optional(),
verboseDefault: z.union([z.literal("off"), z.literal("on"), z.literal("full")]).optional(),
reasoningDefault: z
.union([z.literal("off"), z.literal("on"), z.literal("stream")])
.optional(),
reasoningDefault: z.union([z.literal("off"), z.literal("on"), z.literal("stream")]).optional(),
elevatedDefault: z
.union([z.literal("off"), z.literal("on"), z.literal("ask"), z.literal("full")])
.optional(),

View File

@@ -519,6 +519,7 @@ async function prepareCronRunContext(params: {
const workspace = await ensureAgentWorkspace({
dir: workspaceDirRaw,
ensureBootstrapFiles: !agentCfg?.skipBootstrap && !params.isFastTestEnv,
skipOptionalBootstrapFiles: agentCfg?.skipOptionalBootstrapFiles,
});
const workspaceDir = workspace.dir;

View File

@@ -494,7 +494,11 @@ export const agentsHandlers: GatewayRequestHandlers = {
// Ensure workspace & transcripts exist BEFORE writing config so a failure
// here does not leave a broken config entry behind.
const skipBootstrap = Boolean(nextConfig.agents?.defaults?.skipBootstrap);
await ensureAgentWorkspace({ dir: workspaceDir, ensureBootstrapFiles: !skipBootstrap });
await ensureAgentWorkspace({
dir: workspaceDir,
ensureBootstrapFiles: !skipBootstrap,
skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles,
});
await fs.mkdir(resolveSessionTranscriptsDirForAgent(agentId), { recursive: true });
const persistedIdentity = normalizeIdentityForFile(resolveAgentIdentity(nextConfig, agentId));
@@ -575,6 +579,7 @@ export const agentsHandlers: GatewayRequestHandlers = {
ensuredWorkspace = await ensureAgentWorkspace({
dir: workspaceDir,
ensureBootstrapFiles: !skipBootstrap,
skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles,
});
}

View File

@@ -687,6 +687,24 @@ function hasExplicitCompatibilityInputs(options: PluginLoadOptions): boolean {
);
}
function pluginLoadOptionsMatchCacheKey(
options: PluginLoadOptions,
expectedCacheKey: string,
): boolean {
if (resolvePluginLoadCacheContext(options).cacheKey === expectedCacheKey) {
return true;
}
if (options.installBundledRuntimeDeps !== false) {
return false;
}
return (
resolvePluginLoadCacheContext({
...options,
installBundledRuntimeDeps: undefined,
}).cacheKey === expectedCacheKey
);
}
type PluginRegistrationPlan = {
/** Public compatibility label passed to plugin register(api). */
mode: PluginRegistrationMode;
@@ -896,15 +914,15 @@ function getCompatibleActivePluginRegistry(
return undefined;
}
const loadContext = resolvePluginLoadCacheContext(options);
if (loadContext.cacheKey === activeCacheKey) {
if (pluginLoadOptionsMatchCacheKey(options, activeCacheKey)) {
return activeRegistry;
}
if (!loadContext.shouldActivate) {
const activatingCacheKey = resolvePluginLoadCacheContext({
const activatingOptions = {
...options,
activate: true,
}).cacheKey;
if (activatingCacheKey === activeCacheKey) {
};
if (pluginLoadOptionsMatchCacheKey(activatingOptions, activeCacheKey)) {
return activeRegistry;
}
}
@@ -912,26 +930,26 @@ function getCompatibleActivePluginRegistry(
loadContext.runtimeSubagentMode === "default" &&
getActivePluginRuntimeSubagentMode() === "gateway-bindable"
) {
const gatewayBindableCacheKey = resolvePluginLoadCacheContext({
const gatewayBindableOptions = {
...options,
runtimeOptions: {
...options.runtimeOptions,
allowGatewaySubagentBinding: true,
},
}).cacheKey;
if (gatewayBindableCacheKey === activeCacheKey) {
};
if (pluginLoadOptionsMatchCacheKey(gatewayBindableOptions, activeCacheKey)) {
return activeRegistry;
}
if (!loadContext.shouldActivate) {
const activatingGatewayBindableCacheKey = resolvePluginLoadCacheContext({
const activatingGatewayBindableOptions = {
...options,
activate: true,
runtimeOptions: {
...options.runtimeOptions,
allowGatewaySubagentBinding: true,
},
}).cacheKey;
if (activatingGatewayBindableCacheKey === activeCacheKey) {
};
if (pluginLoadOptionsMatchCacheKey(activatingGatewayBindableOptions, activeCacheKey)) {
return activeRegistry;
}
}

View File

@@ -741,6 +741,7 @@ export async function runSetupWizard(
logConfigUpdated(runtime);
await onboardHelpers.ensureWorkspaceAndSessions(workspaceDir, runtime, {
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles,
});
if (opts.skipSearch) {