fix(bootstrap): workspace bootstrap prompt routing#68000
Conversation
🔒 Aisle Security AnalysisWe found 4 potential security issue(s) in this PR:
1. 🟠 Prompt injection risk: agent instructed to read and follow untrusted BOOTSTRAP.md from workspace
DescriptionThe new bootstrap flow prepends a user prompt prefix that instructs the model to read and follow Because Key concerns grounded in the code change:
Vulnerable code (user-visible prompt coercion): if (userPromptPrefixText) {
effectivePrompt = `${userPromptPrefixText}\n\n${effectivePrompt}`;
}And the prefix itself instructs following the file: "Please read BOOTSTRAP.md from the workspace and follow it before replying normally."This is a classic prompt injection via untrusted content pattern. RecommendationTreat Concrete improvements:
// Pseudocode: before executing exec/network tools during bootstrap
if (bootstrapMode !== "none" && toolIsDangerous(toolName)) {
throw new Error("Bootstrap cannot run dangerous tools without explicit user confirmation.");
}
2. 🟡 Bootstrap mode can be downgraded via path canonicalization / symlink mismatch (canonical workspace check uses raw string equality)
DescriptionThe code determines whether the current run is operating in the canonical workspace by doing a direct string comparison between two path strings:
Because neither side is normalized via This value is then used to compute
Vulnerable code (new behavior): const canonicalWorkspace = resolveUserPath(
resolveAgentWorkspaceDir(params.config ?? {}, workspaceResolution.agentId),
);
const isCanonicalWorkspace = canonicalWorkspace === resolvedWorkspace;And later: isCanonicalWorkspace:
(params.isCanonicalWorkspace ?? true) && effectiveWorkspace === resolvedWorkspace,If an untrusted caller can influence RecommendationNormalize both paths using the same canonicalization routine before comparison. At minimum:
Example fix: import fs from "node:fs";
import path from "node:path";
function normalizePathForEquality(p: string): string {
const resolved = path.resolve(p);
try {
return fs.realpathSync.native(resolved);
} catch {
return resolved;
}
}
const canonicalWorkspace = normalizePathForEquality(
resolveUserPath(resolveAgentWorkspaceDir(params.config ?? {}, workspaceResolution.agentId)),
);
const resolvedWorkspace = normalizePathForEquality(workspaceResolution.workspaceDir);
const isCanonicalWorkspace = canonicalWorkspace === resolvedWorkspace;Also consider using this same normalization when comparing 3. 🟡 Untrusted workspace state file can mark bootstrap as complete and bypass BOOTSTRAP.md workflow
Description
Impact:
Vulnerable code: const state = await readWorkspaceSetupStateForDir(resolvedDir);
if (typeof state.setupCompletedAt === "string" && state.setupCompletedAt.trim().length > 0) {
return "complete";
}
const bootstrapExists = await fileExists(path.join(resolvedDir, DEFAULT_BOOTSTRAP_FILENAME));
return bootstrapExists ? "pending" : "complete";Because RecommendationTreat Option A (simple logic fix): if export async function resolveWorkspaceBootstrapStatus(dir: string): Promise<"pending"|"complete"> {
const resolvedDir = resolveUserPath(dir);
const bootstrapExists = await fileExists(path.join(resolvedDir, DEFAULT_BOOTSTRAP_FILENAME));
if (bootstrapExists) return "pending";
const state = await readWorkspaceSetupStateForDir(resolvedDir);
const completed = typeof state.setupCompletedAt === "string" && state.setupCompletedAt.trim().length > 0;
return completed ? "complete" : "complete"; // or keep additional states if needed
}(If you need the timestamp, still read it, but do not let it override Option B (trust boundary fix): store 4. 🟡 Arbitrary path probing via untrusted workspaceDir in bootstrap status resolution
Description
The As a result:
Vulnerable code: const resolvedDir = resolveUserPath(dir);
const state = await readWorkspaceSetupStateForDir(resolvedDir);
const bootstrapExists = await fileExists(path.join(resolvedDir, DEFAULT_BOOTSTRAP_FILENAME));RecommendationConstrain Options:
Example approach: import path from "node:path";
import fs from "node:fs/promises";
async function resolveWorkspaceDirSafe(input: string, allowedRoot: string): Promise<string> {
const candidate = path.resolve(input); // handles ..
const realCandidate = await fs.realpath(candidate); // resolves symlinks
const realRoot = await fs.realpath(allowedRoot);
if (!realCandidate.startsWith(realRoot + path.sep)) {
throw new Error("workspaceDir outside allowed root");
}
return realCandidate;
}Additionally, consider removing Analyzed PR: #68000 at commit Last updated on: 2026-04-17T14:58:12Z |
Greptile SummaryThis PR fixes workspace bootstrap prompt routing by resolving bootstrap phase from workspace truth (state file +
Confidence Score: 4/5Functionally correct but blocked by a TypeScript excess-property error in the test file that would fail pnpm tsgo / pnpm build. The workspace-truth bootstrap logic, BOOTSTRAP.md context filtering, user-prompt-prefix injection, and session-reset-prompt split are all well-reasoned and well-tested. The single P1 issue is a spurious bootstrapPending: true property passed to buildAgentPromptChannels in the test file, which TypeScript's excess-property check would reject. Fix is a one-line removal. src/agents/system-prompt.test.ts — excess property in buildAgentPromptChannels call at line 78–81. Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/agents/system-prompt.test.ts
Line: 78-81
Comment:
**Excess property causes TypeScript error**
`bootstrapPending` is not in `buildAgentPromptChannels`'s parameter type — it only exists on `buildAgentUserPromptPrefix`. Passing it as an object literal property triggers TypeScript's excess-property check, so `pnpm tsgo` will fail. The assertion is already correct without this field (bootstrap guidance is never written to the system prompt by design), so just remove it.
```suggestion
const channels = buildAgentPromptChannels({
workspaceDir: "/tmp/openclaw",
});
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(bootstrap): workspace bootstrap prom..." | Re-trigger Greptile |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6c7b77c38b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
6c7b77c to
6c5d5a6
Compare
|
Follow-up pushed in What changed:
Validation rerun: env PATH=/opt/homebrew/opt/node@24/bin:$PATH npm_config_cache=/tmp/openclaw-npm-cache \
pnpm exec vitest run \
src/agents/workspace.test.ts \
src/agents/bootstrap-files.test.ts \
src/agents/system-prompt.test.ts \
src/auto-reply/reply/session-reset-prompt.test.ts \
src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-injection.test.tsResult: On the Aisle security comment specifically: #3 was the only one with a real mapping to the current PR diff, and that warning dedupe is restored in this push. The other findings were pointing at stale runtime deltas from the earlier branch snapshot and do not apply to the rebuilt PR diff. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6c5d5a6884
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Follow-up pushed in What changed:
Validation rerun: env PATH=/opt/homebrew/opt/node@24/bin:$PATH npm_config_cache=/tmp/openclaw-npm-cache
pnpm exec vitest run
src/agents/pi-embedded-runner/run/attempt.test.ts
src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-routing.test.ts
src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-injection.test.ts
src/agents/workspace.test.ts
src/agents/bootstrap-files.test.ts
src/agents/system-prompt.test.ts
src/auto-reply/reply/session-reset-prompt.test.tsResult: Note: the local git hook still trips over unrelated existing |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a96482cebb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 043de21985
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 96ec64a40e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b935cb8cc3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
* fix(bootstrap): workspace bootstrap prompt routing * Fix bootstrap routing edge cases * Refine bootstrap mode routing and reset prompts * Fix bootstrap workspace routing for embedded runs * Fix embedded bootstrap compile follow-up * Align bare reset bootstrap file access * Honor reset override model for bootstrap gating * Align chat reset bootstrap topology
* fix(bootstrap): workspace bootstrap prompt routing * Fix bootstrap routing edge cases * Refine bootstrap mode routing and reset prompts * Fix bootstrap workspace routing for embedded runs * Fix embedded bootstrap compile follow-up * Align bare reset bootstrap file access * Honor reset override model for bootstrap gating * Align chat reset bootstrap topology
* fix(bootstrap): workspace bootstrap prompt routing * Fix bootstrap routing edge cases * Refine bootstrap mode routing and reset prompts * Fix bootstrap workspace routing for embedded runs * Fix embedded bootstrap compile follow-up * Align bare reset bootstrap file access * Honor reset override model for bootstrap gating * Align chat reset bootstrap topology
* fix(bootstrap): workspace bootstrap prompt routing * Fix bootstrap routing edge cases * Refine bootstrap mode routing and reset prompts * Fix bootstrap workspace routing for embedded runs * Fix embedded bootstrap compile follow-up * Align bare reset bootstrap file access * Honor reset override model for bootstrap gating * Align chat reset bootstrap topology
* fix(bootstrap): workspace bootstrap prompt routing * Fix bootstrap routing edge cases * Refine bootstrap mode routing and reset prompts * Fix bootstrap workspace routing for embedded runs * Fix embedded bootstrap compile follow-up * Align bare reset bootstrap file access * Honor reset override model for bootstrap gating * Align chat reset bootstrap topology
* fix(bootstrap): workspace bootstrap prompt routing * Fix bootstrap routing edge cases * Refine bootstrap mode routing and reset prompts * Fix bootstrap workspace routing for embedded runs * Fix embedded bootstrap compile follow-up * Align bare reset bootstrap file access * Honor reset override model for bootstrap gating * Align chat reset bootstrap topology
* fix(bootstrap): workspace bootstrap prompt routing * Fix bootstrap routing edge cases * Refine bootstrap mode routing and reset prompts * Fix bootstrap workspace routing for embedded runs * Fix embedded bootstrap compile follow-up * Align bare reset bootstrap file access * Honor reset override model for bootstrap gating * Align chat reset bootstrap topology
* fix(bootstrap): workspace bootstrap prompt routing * Fix bootstrap routing edge cases * Refine bootstrap mode routing and reset prompts * Fix bootstrap workspace routing for embedded runs * Fix embedded bootstrap compile follow-up * Align bare reset bootstrap file access * Honor reset override model for bootstrap gating * Align chat reset bootstrap topology
Summary
This updates bootstrap handling from a single pending-state behavior to a tri-state runtime policy:
full,limited, andnone.The key change is that bootstrap is no longer treated as a universal concern for every embedded run. It is now scoped to the runs that can actually participate in onboarding safely.
BootstrapModeinstead of branching prompt behavior on a raw booleanfullbootstrap only for primary, user-facing, interactive, canonical-workspace runs with real bootstrap file accesslimitedbootstrap only for constrained but still user-facing runs, such as copied-sandbox or spawned-workspace interactive runsnoneforcron,heartbeat, subagent/helper, non-primary, and no-file-access runs where bootstrap awareness would only add noiseBOOTSTRAP.mdinto embedded system context as a no-tool fallback/newand/resetmode-aware so they only use bootstrap-specific wording when the resolved mode calls for itnonemode for bare resets targeting subagent/helper sessionsWhat changed
Workspace truth still determines whether bootstrap is pending:
What changed is the read-side policy layered on top of that workspace truth. Embedded and reset paths now resolve a shared bootstrap mode:
The resolver uses the actual runtime surface, not just pending state:
That means bootstrap now behaves like this:
full/new/resetlimitednonecronheartbeatIn the embedded attempt path, raw
BOOTSTRAP.mdis no longer injected into embedded system context. Instead, prompt behavior is mode-specific:The shared bootstrap prompt text is now centralized and shorter. The default
fullinstruction is intentionally calmer:For bare
/newand/reset, the shared reset path now resolves bootstrap mode from the same effective workspace source the real run uses. That means spawned/custom-workspace sessions no longer derive bootstrap state from the default agent workspace by mistake, and subagent/helper reset targets no longer get forced intofullbootstrap.This also changes continuation behavior. Only
fullmode blocks continuation-skip and records the full-bootstrap marker path.limitedandnoneno longer accidentally inherit full-bootstrap continuation semantics.Validation
Focused bootstrap/reset regressions passed:
pnpm exec vitest run \ src/auto-reply/reply/session-reset-prompt.test.ts \ src/gateway/server-methods/agent.test.ts \ src/agents/system-prompt.test.ts \ src/agents/pi-embedded-runner/run/attempt.test.ts \ src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-injection.test.tsResult: