fix: guard resolveUserPath against undefined input (#10176)

* fix: guard resolveUserPath against undefined input

When subagent spawner omits workspaceDir, resolveUserPath receives
undefined and crashes on .trim().  Add a falsy guard that falls back
to process.cwd(), matching the behavior callers already expect.

Closes #10089

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: harden runner workspace fallback (#10176) (thanks @Yida-Dev)

* fix: harden workspace fallback scoping (#10176) (thanks @Yida-Dev)

* refactor: centralize workspace fallback classification and redaction (#10176) (thanks @Yida-Dev)

* test: remove explicit any from utils mock (#10176) (thanks @Yida-Dev)

* security: reject malformed agent session keys for workspace resolution (#10176) (thanks @Yida-Dev)

---------

Co-authored-by: Yida-Dev <reyifeijun@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Gustavo Madeira Santana <gumadeiras@gmail.com>
This commit is contained in:
Yida-Dev
2026-02-07 01:16:58 +07:00
committed by GitHub
parent 5842bcaaf7
commit 4216449405
22 changed files with 522 additions and 24 deletions

View File

@@ -38,6 +38,7 @@ Docs: https://docs.openclaw.ai
- Control UI: add hardened fallback for asset resolution in global npm installs. (#4855) Thanks @anapivirtua.
- Update: remove dead restore control-ui step that failed on gitignored dist/ output.
- Update: avoid wiping prebuilt Control UI assets during dev auto-builds (`tsdown --no-clean`), run update doctor via `openclaw.mjs`, and auto-restore missing UI assets after doctor. (#10146) Thanks @gumadeiras.
- Agents: harden embedded and CLI runner workspace resolution for missing/blank runtime inputs by falling back to per-agent workspace defaults (not CWD), preventing `sessions_spawn` early crashes. (#10176) Thanks @Yida-Dev.
- Models: add forward-compat fallback for `openai-codex/gpt-5.3-codex` when model registry hasn't discovered it yet. (#9989) Thanks @w1kke.
- Auto-reply/Docs: normalize `extra-high` (and spaced variants) to `xhigh` for Codex thinking levels, and align Codex 5.3 FAQ examples. (#9976) Thanks @slonce70.
- Compaction: remove orphaned `tool_result` messages during history pruning to prevent session corruption from aborted tool calls. (#9868, fixes #9769, #9724, #9672)

View File

@@ -1,4 +1,8 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import type { CliBackendConfig } from "../config/types.js";
import { runCliAgent } from "./cli-runner.js";
import { cleanupSuspendedCliProcesses } from "./cli-runner/helpers.js";
@@ -58,6 +62,85 @@ describe("runCliAgent resume cleanup", () => {
expect(pkillArgs[1]).toContain("resume");
expect(pkillArgs[1]).toContain("thread-123");
});
it("falls back to per-agent workspace when workspaceDir is missing", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cli-runner-"));
const fallbackWorkspace = path.join(tempDir, "workspace-main");
await fs.mkdir(fallbackWorkspace, { recursive: true });
const cfg = {
agents: {
defaults: {
workspace: fallbackWorkspace,
},
},
} satisfies OpenClawConfig;
runExecMock.mockResolvedValue({ stdout: "", stderr: "" });
runCommandWithTimeoutMock.mockResolvedValueOnce({
stdout: "ok",
stderr: "",
code: 0,
signal: null,
killed: false,
});
try {
await runCliAgent({
sessionId: "s1",
sessionKey: "agent:main:subagent:missing-workspace",
sessionFile: "/tmp/session.jsonl",
workspaceDir: undefined as unknown as string,
config: cfg,
prompt: "hi",
provider: "codex-cli",
model: "gpt-5.2-codex",
timeoutMs: 1_000,
runId: "run-1",
});
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
const options = runCommandWithTimeoutMock.mock.calls[0]?.[1] as { cwd?: string };
expect(options.cwd).toBe(path.resolve(fallbackWorkspace));
});
it("throws when sessionKey is malformed", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cli-runner-"));
const mainWorkspace = path.join(tempDir, "workspace-main");
const researchWorkspace = path.join(tempDir, "workspace-research");
await fs.mkdir(mainWorkspace, { recursive: true });
await fs.mkdir(researchWorkspace, { recursive: true });
const cfg = {
agents: {
defaults: {
workspace: mainWorkspace,
},
list: [{ id: "research", workspace: researchWorkspace }],
},
} satisfies OpenClawConfig;
try {
await expect(
runCliAgent({
sessionId: "s1",
sessionKey: "agent::broken",
agentId: "research",
sessionFile: "/tmp/session.jsonl",
workspaceDir: undefined as unknown as string,
config: cfg,
prompt: "hi",
provider: "codex-cli",
model: "gpt-5.2-codex",
timeoutMs: 1_000,
runId: "run-2",
}),
).rejects.toThrow("Malformed agent session key");
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
expect(runCommandWithTimeoutMock).not.toHaveBeenCalled();
});
});
describe("cleanupSuspendedCliProcesses", () => {

View File

@@ -7,7 +7,6 @@ import { shouldLogVerbose } from "../globals.js";
import { isTruthyEnvValue } from "../infra/env.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { runCommandWithTimeout } from "../process/exec.js";
import { resolveUserPath } from "../utils.js";
import { resolveSessionAgentIds } from "./agent-scope.js";
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "./bootstrap-files.js";
import { resolveCliBackendConfig } from "./cli-backends.js";
@@ -29,12 +28,14 @@ import {
import { resolveOpenClawDocsPath } from "./docs-path.js";
import { FailoverError, resolveFailoverStatus } from "./failover-error.js";
import { classifyFailoverReason, isFailoverErrorMessage } from "./pi-embedded-helpers.js";
import { redactRunIdentifier, resolveRunWorkspaceDir } from "./workspace-run.js";
const log = createSubsystemLogger("agent/claude-cli");
export async function runCliAgent(params: {
sessionId: string;
sessionKey?: string;
agentId?: string;
sessionFile: string;
workspaceDir: string;
config?: OpenClawConfig;
@@ -51,7 +52,21 @@ export async function runCliAgent(params: {
images?: ImageContent[];
}): Promise<EmbeddedPiRunResult> {
const started = Date.now();
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
const workspaceResolution = resolveRunWorkspaceDir({
workspaceDir: params.workspaceDir,
sessionKey: params.sessionKey,
agentId: params.agentId,
config: params.config,
});
const resolvedWorkspace = workspaceResolution.workspaceDir;
const redactedSessionId = redactRunIdentifier(params.sessionId);
const redactedSessionKey = redactRunIdentifier(params.sessionKey);
const redactedWorkspace = redactRunIdentifier(resolvedWorkspace);
if (workspaceResolution.usedFallback) {
log.warn(
`[workspace-fallback] caller=runCliAgent reason=${workspaceResolution.fallbackReason} run=${params.runId} session=${redactedSessionId} sessionKey=${redactedSessionKey} agent=${workspaceResolution.agentId} workspace=${redactedWorkspace}`,
);
}
const workspaceDir = resolvedWorkspace;
const backendResolved = resolveCliBackendConfig(params.provider, params.config);
@@ -311,6 +326,7 @@ export async function runCliAgent(params: {
export async function runClaudeCliAgent(params: {
sessionId: string;
sessionKey?: string;
agentId?: string;
sessionFile: string;
workspaceDir: string;
config?: OpenClawConfig;
@@ -328,6 +344,7 @@ export async function runClaudeCliAgent(params: {
return runCliAgent({
sessionId: params.sessionId,
sessionKey: params.sessionKey,
agentId: params.agentId,
sessionFile: params.sessionFile,
workspaceDir: params.workspaceDir,
config: params.config,

View File

@@ -219,6 +219,75 @@ describe("runEmbeddedPiAgent", () => {
await expect(fs.stat(path.join(agentDir, "models.json"))).resolves.toBeTruthy();
});
it("falls back to per-agent workspace when runtime workspaceDir is missing", async () => {
const sessionFile = nextSessionFile();
const fallbackWorkspace = path.join(tempRoot ?? os.tmpdir(), "workspace-fallback-main");
const cfg = {
...makeOpenAiConfig(["mock-1"]),
agents: {
defaults: {
workspace: fallbackWorkspace,
},
},
} satisfies OpenClawConfig;
await ensureModels(cfg);
const result = await runEmbeddedPiAgent({
sessionId: "session:test-fallback",
sessionKey: "agent:main:subagent:fallback-workspace",
sessionFile,
workspaceDir: undefined as unknown as string,
config: cfg,
prompt: "hello",
provider: "openai",
model: "mock-1",
timeoutMs: 5_000,
agentDir,
runId: "run-fallback-workspace",
enqueue: immediateEnqueue,
});
expect(result.payloads?.[0]?.text).toBe("ok");
await expect(fs.stat(fallbackWorkspace)).resolves.toBeTruthy();
});
it("throws when sessionKey is malformed", async () => {
const sessionFile = nextSessionFile();
const cfg = {
...makeOpenAiConfig(["mock-1"]),
agents: {
defaults: {
workspace: path.join(tempRoot ?? os.tmpdir(), "workspace-fallback-main"),
},
list: [
{
id: "research",
workspace: path.join(tempRoot ?? os.tmpdir(), "workspace-fallback-research"),
},
],
},
} satisfies OpenClawConfig;
await ensureModels(cfg);
await expect(
runEmbeddedPiAgent({
sessionId: "session:test-fallback-malformed",
sessionKey: "agent::broken",
agentId: "research",
sessionFile,
workspaceDir: undefined as unknown as string,
config: cfg,
prompt: "hello",
provider: "openai",
model: "mock-1",
timeoutMs: 5_000,
agentDir,
runId: "run-fallback-workspace-malformed",
enqueue: immediateEnqueue,
}),
).rejects.toThrow("Malformed agent session key");
});
itIfNotWin32(
"persists the first user message before assistant output",
{ timeout: 120_000 },

View File

@@ -3,7 +3,6 @@ import type { ThinkLevel } from "../../auto-reply/thinking.js";
import type { RunEmbeddedPiAgentParams } from "./run/params.js";
import type { EmbeddedPiAgentMeta, EmbeddedPiRunResult } from "./types.js";
import { enqueueCommandInLane } from "../../process/command-queue.js";
import { resolveUserPath } from "../../utils.js";
import { isMarkdownCapableMessageChannel } from "../../utils/message-channel.js";
import { resolveOpenClawAgentDir } from "../agent-paths.js";
import {
@@ -46,6 +45,7 @@ import {
type FailoverReason,
} from "../pi-embedded-helpers.js";
import { normalizeUsage, type UsageLike } from "../usage.js";
import { redactRunIdentifier, resolveRunWorkspaceDir } from "../workspace-run.js";
import { compactEmbeddedPiSessionDirect } from "./compact.js";
import { resolveGlobalLane, resolveSessionLane } from "./lanes.js";
import { log } from "./logger.js";
@@ -92,7 +92,21 @@ export async function runEmbeddedPiAgent(
return enqueueSession(() =>
enqueueGlobal(async () => {
const started = Date.now();
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
const workspaceResolution = resolveRunWorkspaceDir({
workspaceDir: params.workspaceDir,
sessionKey: params.sessionKey,
agentId: params.agentId,
config: params.config,
});
const resolvedWorkspace = workspaceResolution.workspaceDir;
const redactedSessionId = redactRunIdentifier(params.sessionId);
const redactedSessionKey = redactRunIdentifier(params.sessionKey);
const redactedWorkspace = redactRunIdentifier(resolvedWorkspace);
if (workspaceResolution.usedFallback) {
log.warn(
`[workspace-fallback] caller=runEmbeddedPiAgent reason=${workspaceResolution.fallbackReason} run=${params.runId} session=${redactedSessionId} sessionKey=${redactedSessionKey} agent=${workspaceResolution.agentId} workspace=${redactedWorkspace}`,
);
}
const prevCwd = process.cwd();
const provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
@@ -333,7 +347,7 @@ export async function runEmbeddedPiAgent(
replyToMode: params.replyToMode,
hasRepliedRef: params.hasRepliedRef,
sessionFile: params.sessionFile,
workspaceDir: params.workspaceDir,
workspaceDir: resolvedWorkspace,
agentDir,
config: params.config,
skillsSnapshot: params.skillsSnapshot,
@@ -345,6 +359,7 @@ export async function runEmbeddedPiAgent(
model,
authStorage,
modelRegistry,
agentId: workspaceResolution.agentId,
thinkLevel,
verboseLevel: params.verboseLevel,
reasoningLevel: params.reasoningLevel,
@@ -401,7 +416,7 @@ export async function runEmbeddedPiAgent(
agentAccountId: params.agentAccountId,
authProfileId: lastProfileId,
sessionFile: params.sessionFile,
workspaceDir: params.workspaceDir,
workspaceDir: resolvedWorkspace,
agentDir,
config: params.config,
skillsSnapshot: params.skillsSnapshot,

View File

@@ -10,7 +10,7 @@ import { resolveChannelCapabilities } from "../../../config/channel-capabilities
import { getMachineDisplayName } from "../../../infra/machine-name.js";
import { MAX_IMAGE_BYTES } from "../../../media/constants.js";
import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
import { isSubagentSessionKey } from "../../../routing/session-key.js";
import { isSubagentSessionKey, normalizeAgentId } from "../../../routing/session-key.js";
import { resolveSignalReactionLevel } from "../../../signal/reaction-level.js";
import { resolveTelegramInlineButtonsScope } from "../../../telegram/inline-buttons.js";
import { resolveTelegramReactionLevel } from "../../../telegram/reaction-level.js";
@@ -705,6 +705,13 @@ export async function runEmbeddedAttempt(
// Get hook runner once for both before_agent_start and agent_end hooks
const hookRunner = getGlobalHookRunner();
const hookAgentId =
typeof params.agentId === "string" && params.agentId.trim()
? normalizeAgentId(params.agentId)
: resolveSessionAgentIds({
sessionKey: params.sessionKey,
config: params.config,
}).sessionAgentId;
let promptError: unknown = null;
try {
@@ -720,7 +727,7 @@ export async function runEmbeddedAttempt(
messages: activeSession.messages,
},
{
agentId: params.sessionKey?.split(":")[0] ?? "main",
agentId: hookAgentId,
sessionKey: params.sessionKey,
workspaceDir: params.workspaceDir,
messageProvider: params.messageProvider ?? undefined,
@@ -850,7 +857,7 @@ export async function runEmbeddedAttempt(
durationMs: Date.now() - promptStartedAt,
},
{
agentId: params.sessionKey?.split(":")[0] ?? "main",
agentId: hookAgentId,
sessionKey: params.sessionKey,
workspaceDir: params.workspaceDir,
messageProvider: params.messageProvider ?? undefined,

View File

@@ -20,6 +20,7 @@ export type ClientToolDefinition = {
export type RunEmbeddedPiAgentParams = {
sessionId: string;
sessionKey?: string;
agentId?: string;
messageChannel?: string;
messageProvider?: string;
agentAccountId?: string;

View File

@@ -14,6 +14,7 @@ import type { ClientToolDefinition } from "./params.js";
export type EmbeddedRunAttemptParams = {
sessionId: string;
sessionKey?: string;
agentId?: string;
messageChannel?: string;
messageProvider?: string;
agentAccountId?: string;

View File

@@ -0,0 +1,139 @@
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { resolveRunWorkspaceDir } from "./workspace-run.js";
import { DEFAULT_AGENT_WORKSPACE_DIR } from "./workspace.js";
describe("resolveRunWorkspaceDir", () => {
it("resolves explicit workspace values without fallback", () => {
const explicit = path.join(process.cwd(), "tmp", "workspace-run-explicit");
const result = resolveRunWorkspaceDir({
workspaceDir: explicit,
sessionKey: "agent:main:subagent:test",
});
expect(result.usedFallback).toBe(false);
expect(result.agentId).toBe("main");
expect(result.workspaceDir).toBe(path.resolve(explicit));
});
it("falls back to configured per-agent workspace when input is missing", () => {
const defaultWorkspace = path.join(process.cwd(), "tmp", "workspace-default-main");
const researchWorkspace = path.join(process.cwd(), "tmp", "workspace-research");
const cfg = {
agents: {
defaults: { workspace: defaultWorkspace },
list: [{ id: "research", workspace: researchWorkspace }],
},
} satisfies OpenClawConfig;
const result = resolveRunWorkspaceDir({
workspaceDir: undefined,
sessionKey: "agent:research:subagent:test",
config: cfg,
});
expect(result.usedFallback).toBe(true);
expect(result.fallbackReason).toBe("missing");
expect(result.agentId).toBe("research");
expect(result.workspaceDir).toBe(path.resolve(researchWorkspace));
});
it("falls back to default workspace for blank strings", () => {
const defaultWorkspace = path.join(process.cwd(), "tmp", "workspace-default-main");
const cfg = {
agents: {
defaults: { workspace: defaultWorkspace },
},
} satisfies OpenClawConfig;
const result = resolveRunWorkspaceDir({
workspaceDir: " ",
sessionKey: "agent:main:subagent:test",
config: cfg,
});
expect(result.usedFallback).toBe(true);
expect(result.fallbackReason).toBe("blank");
expect(result.agentId).toBe("main");
expect(result.workspaceDir).toBe(path.resolve(defaultWorkspace));
});
it("falls back to built-in main workspace when config is unavailable", () => {
const result = resolveRunWorkspaceDir({
workspaceDir: null,
sessionKey: "agent:main:subagent:test",
config: undefined,
});
expect(result.usedFallback).toBe(true);
expect(result.fallbackReason).toBe("missing");
expect(result.agentId).toBe("main");
expect(result.workspaceDir).toBe(path.resolve(DEFAULT_AGENT_WORKSPACE_DIR));
});
it("throws for malformed agent session keys", () => {
expect(() =>
resolveRunWorkspaceDir({
workspaceDir: undefined,
sessionKey: "agent::broken",
config: undefined,
}),
).toThrow("Malformed agent session key");
});
it("uses explicit agent id for per-agent fallback when config is unavailable", () => {
const result = resolveRunWorkspaceDir({
workspaceDir: undefined,
sessionKey: "definitely-not-a-valid-session-key",
agentId: "research",
config: undefined,
});
expect(result.agentId).toBe("research");
expect(result.agentIdSource).toBe("explicit");
expect(result.workspaceDir).toBe(path.resolve(os.homedir(), ".openclaw", "workspace-research"));
});
it("throws for malformed agent session keys even when config has a default agent", () => {
const mainWorkspace = path.join(process.cwd(), "tmp", "workspace-main-default");
const researchWorkspace = path.join(process.cwd(), "tmp", "workspace-research-default");
const cfg = {
agents: {
defaults: { workspace: mainWorkspace },
list: [
{ id: "main", workspace: mainWorkspace },
{ id: "research", workspace: researchWorkspace, default: true },
],
},
} satisfies OpenClawConfig;
expect(() =>
resolveRunWorkspaceDir({
workspaceDir: undefined,
sessionKey: "agent::broken",
config: cfg,
}),
).toThrow("Malformed agent session key");
});
it("treats non-agent legacy keys as default, not malformed", () => {
const fallbackWorkspace = path.join(process.cwd(), "tmp", "workspace-default-legacy");
const cfg = {
agents: {
defaults: { workspace: fallbackWorkspace },
},
} satisfies OpenClawConfig;
const result = resolveRunWorkspaceDir({
workspaceDir: undefined,
sessionKey: "custom-main-key",
config: cfg,
});
expect(result.agentId).toBe("main");
expect(result.agentIdSource).toBe("default");
expect(result.workspaceDir).toBe(path.resolve(fallbackWorkspace));
});
});

106
src/agents/workspace-run.ts Normal file
View File

@@ -0,0 +1,106 @@
import type { OpenClawConfig } from "../config/config.js";
import { redactIdentifier } from "../logging/redact-identifier.js";
import {
classifySessionKeyShape,
DEFAULT_AGENT_ID,
normalizeAgentId,
parseAgentSessionKey,
} from "../routing/session-key.js";
import { resolveUserPath } from "../utils.js";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "./agent-scope.js";
export type WorkspaceFallbackReason = "missing" | "blank" | "invalid_type";
type AgentIdSource = "explicit" | "session_key" | "default";
export type ResolveRunWorkspaceResult = {
workspaceDir: string;
usedFallback: boolean;
fallbackReason?: WorkspaceFallbackReason;
agentId: string;
agentIdSource: AgentIdSource;
};
function resolveRunAgentId(params: {
sessionKey?: string;
agentId?: string;
config?: OpenClawConfig;
}): {
agentId: string;
agentIdSource: AgentIdSource;
} {
const rawSessionKey = params.sessionKey?.trim() ?? "";
const shape = classifySessionKeyShape(rawSessionKey);
if (shape === "malformed_agent") {
throw new Error("Malformed agent session key; refusing workspace resolution.");
}
const explicit =
typeof params.agentId === "string" && params.agentId.trim()
? normalizeAgentId(params.agentId)
: undefined;
if (explicit) {
return { agentId: explicit, agentIdSource: "explicit" };
}
const defaultAgentId = resolveDefaultAgentId(params.config ?? {});
if (shape === "missing" || shape === "legacy_or_alias") {
return {
agentId: defaultAgentId || DEFAULT_AGENT_ID,
agentIdSource: "default",
};
}
const parsed = parseAgentSessionKey(rawSessionKey);
if (parsed?.agentId) {
return {
agentId: normalizeAgentId(parsed.agentId),
agentIdSource: "session_key",
};
}
// Defensive fallback, should be unreachable for non-malformed shapes.
return {
agentId: defaultAgentId || DEFAULT_AGENT_ID,
agentIdSource: "default",
};
}
export function redactRunIdentifier(value: string | undefined): string {
return redactIdentifier(value, { len: 12 });
}
export function resolveRunWorkspaceDir(params: {
workspaceDir: unknown;
sessionKey?: string;
agentId?: string;
config?: OpenClawConfig;
}): ResolveRunWorkspaceResult {
const requested = params.workspaceDir;
const { agentId, agentIdSource } = resolveRunAgentId({
sessionKey: params.sessionKey,
agentId: params.agentId,
config: params.config,
});
if (typeof requested === "string") {
const trimmed = requested.trim();
if (trimmed) {
return {
workspaceDir: resolveUserPath(trimmed),
usedFallback: false,
agentId,
agentIdSource,
};
}
}
const fallbackReason: WorkspaceFallbackReason =
requested == null ? "missing" : typeof requested === "string" ? "blank" : "invalid_type";
const fallbackWorkspace = resolveAgentWorkspaceDir(params.config ?? {}, agentId);
return {
workspaceDir: resolveUserPath(fallbackWorkspace),
usedFallback: true,
fallbackReason,
agentId,
agentIdSource,
};
}

View File

@@ -178,6 +178,7 @@ export async function runAgentTurnWithFallback(params: {
const result = await runCliAgent({
sessionId: params.followupRun.run.sessionId,
sessionKey: params.sessionKey,
agentId: params.followupRun.run.agentId,
sessionFile: params.followupRun.run.sessionFile,
workspaceDir: params.followupRun.run.workspaceDir,
config: params.followupRun.run.config,
@@ -255,6 +256,7 @@ export async function runAgentTurnWithFallback(params: {
return runEmbeddedPiAgent({
sessionId: params.followupRun.run.sessionId,
sessionKey: params.sessionKey,
agentId: params.followupRun.run.agentId,
messageProvider: params.sessionCtx.Provider?.trim().toLowerCase() || undefined,
agentAccountId: params.sessionCtx.AccountId,
messageTo: params.sessionCtx.OriginatingTo ?? params.sessionCtx.To,

View File

@@ -113,6 +113,7 @@ export async function runMemoryFlushIfNeeded(params: {
return runEmbeddedPiAgent({
sessionId: params.followupRun.run.sessionId,
sessionKey: params.sessionKey,
agentId: params.followupRun.run.agentId,
messageProvider: params.sessionCtx.Provider?.trim().toLowerCase() || undefined,
agentAccountId: params.sessionCtx.AccountId,
messageTo: params.sessionCtx.OriginatingTo ?? params.sessionCtx.To,

View File

@@ -140,6 +140,7 @@ export function createFollowupRunner(params: {
return runEmbeddedPiAgent({
sessionId: queued.run.sessionId,
sessionKey: queued.run.sessionKey,
agentId: queued.run.agentId,
messageProvider: queued.run.messageProvider,
agentAccountId: queued.run.agentAccountId,
messageTo: queued.originatingTo,

View File

@@ -398,6 +398,7 @@ export async function agentCommand(
return runCliAgent({
sessionId,
sessionKey,
agentId: sessionAgentId,
sessionFile,
workspaceDir,
config: cfg,
@@ -418,6 +419,7 @@ export async function agentCommand(
return runEmbeddedPiAgent({
sessionId,
sessionKey,
agentId: sessionAgentId,
messageChannel,
agentAccountId: runContext.accountId,
messageTo: opts.replyTo ?? opts.to,

View File

@@ -319,6 +319,7 @@ async function probeTarget(params: {
await runEmbeddedPiAgent({
sessionId,
sessionFile,
agentId,
workspaceDir,
agentDir,
config: cfg,

View File

@@ -1,4 +1,3 @@
import crypto from "node:crypto";
import fs from "node:fs";
import type {
ChannelAccountSnapshot,
@@ -8,6 +7,7 @@ import type {
import type { OpenClawConfig } from "../../config/config.js";
import { resolveChannelDefaultAccountId } from "../../channels/plugins/helpers.js";
import { listChannelPlugins } from "../../channels/plugins/index.js";
import { sha256HexPrefix } from "../../logging/redact-identifier.js";
import { formatAge } from "./format.js";
export type ChannelRow = {
@@ -57,17 +57,13 @@ function existsSyncMaybe(p: string | undefined): boolean | null {
}
}
function sha256HexPrefix(value: string, len = 8): string {
return crypto.createHash("sha256").update(value).digest("hex").slice(0, len);
}
function formatTokenHint(token: string, opts: { showSecrets: boolean }): string {
const t = token.trim();
if (!t) {
return "empty";
}
if (!opts.showSecrets) {
return `sha256:${sha256HexPrefix(t)} · len ${t.length}`;
return `sha256:${sha256HexPrefix(t, 8)} · len ${t.length}`;
}
const head = t.slice(0, 4);
const tail = t.slice(-4);

View File

@@ -356,6 +356,7 @@ export async function runCronIsolatedAgentTurn(params: {
return runCliAgent({
sessionId: cronSession.sessionEntry.sessionId,
sessionKey: agentSessionKey,
agentId,
sessionFile,
workspaceDir,
config: cfgWithAgentDefaults,
@@ -371,6 +372,7 @@ export async function runCronIsolatedAgentTurn(params: {
return runEmbeddedPiAgent({
sessionId: cronSession.sessionEntry.sessionId,
sessionKey: agentSessionKey,
agentId,
messageChannel,
agentAccountId: resolvedDelivery.accountId,
sessionFile,

View File

@@ -41,6 +41,7 @@ Reply with ONLY the slug, nothing else. Examples: "vendor-pitch", "api-design",
const result = await runEmbeddedPiAgent({
sessionId: `slug-generator-${Date.now()}`,
sessionKey: "temp:slug-generator",
agentId,
sessionFile: tempSessionFile,
workspaceDir,
agentDir,

View File

@@ -0,0 +1,14 @@
import crypto from "node:crypto";
export function sha256HexPrefix(value: string, len = 12): string {
const safeLen = Number.isFinite(len) ? Math.max(1, Math.floor(len)) : 12;
return crypto.createHash("sha256").update(value).digest("hex").slice(0, safeLen);
}
export function redactIdentifier(value: string | undefined, opts?: { len?: number }): string {
const trimmed = value?.trim();
if (!trimmed) {
return "-";
}
return `sha256:${sha256HexPrefix(trimmed, opts?.len ?? 12)}`;
}

View File

@@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";
import { classifySessionKeyShape } from "./session-key.js";
describe("classifySessionKeyShape", () => {
it("classifies empty keys as missing", () => {
expect(classifySessionKeyShape(undefined)).toBe("missing");
expect(classifySessionKeyShape(" ")).toBe("missing");
});
it("classifies valid agent keys", () => {
expect(classifySessionKeyShape("agent:main:main")).toBe("agent");
expect(classifySessionKeyShape("agent:research:subagent:worker")).toBe("agent");
});
it("classifies malformed agent keys", () => {
expect(classifySessionKeyShape("agent::broken")).toBe("malformed_agent");
expect(classifySessionKeyShape("agent:main")).toBe("malformed_agent");
});
it("treats non-agent legacy or alias keys as non-malformed", () => {
expect(classifySessionKeyShape("main")).toBe("legacy_or_alias");
expect(classifySessionKeyShape("custom-main")).toBe("legacy_or_alias");
expect(classifySessionKeyShape("subagent:worker")).toBe("legacy_or_alias");
});
});

View File

@@ -10,6 +10,7 @@ export {
export const DEFAULT_AGENT_ID = "main";
export const DEFAULT_MAIN_KEY = "main";
export const DEFAULT_ACCOUNT_ID = "default";
export type SessionKeyShape = "missing" | "agent" | "legacy_or_alias" | "malformed_agent";
// Pre-compiled regex
const VALID_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
@@ -58,6 +59,17 @@ export function resolveAgentIdFromSessionKey(sessionKey: string | undefined | nu
return normalizeAgentId(parsed?.agentId ?? DEFAULT_AGENT_ID);
}
export function classifySessionKeyShape(sessionKey: string | undefined | null): SessionKeyShape {
const raw = (sessionKey ?? "").trim();
if (!raw) {
return "missing";
}
if (parseAgentSessionKey(raw)) {
return "agent";
}
return raw.toLowerCase().startsWith("agent:") ? "malformed_agent" : "legacy_or_alias";
}
export function normalizeAgentId(value: string | undefined | null): string {
const trimmed = (value ?? "").trim();
if (!trimmed) {

View File

@@ -79,15 +79,12 @@ describe("jidToE164", () => {
it("maps @lid using reverse mapping file", () => {
const mappingPath = path.join(CONFIG_DIR, "credentials", "lid-mapping-123_reverse.json");
const original = fs.readFileSync;
const spy = vi
.spyOn(fs, "readFileSync")
// oxlint-disable-next-line typescript/no-explicit-any
.mockImplementation((path: any, encoding?: any) => {
if (path === mappingPath) {
return `"5551234"`;
}
return original(path, encoding);
});
const spy = vi.spyOn(fs, "readFileSync").mockImplementation((...args) => {
if (args[0] === mappingPath) {
return `"5551234"`;
}
return original(...args);
});
expect(jidToE164("123@lid")).toBe("+5551234");
spy.mockRestore();
});
@@ -167,4 +164,9 @@ describe("resolveUserPath", () => {
it("resolves relative paths", () => {
expect(resolveUserPath("tmp/dir")).toBe(path.resolve("tmp/dir"));
});
it("keeps blank paths blank", () => {
expect(resolveUserPath("")).toBe("");
expect(resolveUserPath(" ")).toBe("");
});
});