mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(agents): hydrate CLI images from agent workspaces (#122684)
* fix(agents): hydrate CLI images from agent workspace * fix(agents): preserve resolved CLI workspace owner * fix(agents): keep workspace owner in prepared params * fix(agents): resolve CLI owner before preparation * fix(agents): preserve CLI runtime policy owner --------- Co-authored-by: Adkid-Zephyr <169631528+Adkid-Zephyr@users.noreply.github.com> Co-authored-by: FullerStackDev <263060202+fuller-stack-dev@users.noreply.github.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createSolidPngBuffer } from "../../test/helpers/image-fixtures.js";
|
||||
import { buildInboundMediaNoteProjection } from "../auto-reply/media-note.js";
|
||||
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
|
||||
import { getAgentScopedMediaLocalRoots } from "../media/local-roots.js";
|
||||
import { escapeRegExp } from "../shared/regexp.js";
|
||||
import { captureEnv, setTestEnvValue } from "../test-utils/env.js";
|
||||
import {
|
||||
@@ -107,6 +108,55 @@ describe("prepareCliPromptImagePayload prompt references", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("hydrates structured media from the active agent workspace without widening sibling access", async () => {
|
||||
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cli-agent-image-"));
|
||||
const workspaceDir = path.join(stateDir, "workspace-arthur");
|
||||
const siblingWorkspaceDir = path.join(stateDir, "workspace-merlin");
|
||||
const imagePath = path.join(workspaceDir, "media", "inbound", "photo.png");
|
||||
const siblingImagePath = path.join(siblingWorkspaceDir, "media", "inbound", "photo.png");
|
||||
const image = createSolidPngBuffer(1, 1, { r: 255, g: 0, b: 0 });
|
||||
await fs.mkdir(path.dirname(imagePath), { recursive: true });
|
||||
await fs.mkdir(path.dirname(siblingImagePath), { recursive: true });
|
||||
await fs.writeFile(imagePath, image);
|
||||
await fs.writeFile(siblingImagePath, image);
|
||||
const envSnapshot = captureEnv(["OPENCLAW_STATE_DIR"]);
|
||||
setTestEnvValue("OPENCLAW_STATE_DIR", stateDir);
|
||||
const config = {
|
||||
agents: {
|
||||
entries: {
|
||||
arthur: { default: true, workspace: workspaceDir },
|
||||
merlin: { workspace: siblingWorkspaceDir },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const localRoots = getAgentScopedMediaLocalRoots(config, "arthur");
|
||||
const prepared = await prepareCliPromptImagePayload({
|
||||
backend: { command: "claude", input: "stdin" },
|
||||
prompt: "describe the attachment",
|
||||
workspaceDir,
|
||||
localRoots,
|
||||
media: [{ path: imagePath, contentType: "image/png" }],
|
||||
});
|
||||
|
||||
expect(prepared.imagePaths).toHaveLength(1);
|
||||
await expect(fs.readFile(prepared.imagePaths?.[0] ?? "")).resolves.toEqual(image);
|
||||
await expect(
|
||||
prepareCliPromptImagePayload({
|
||||
backend: { command: "claude", input: "stdin" },
|
||||
prompt: "describe the attachment",
|
||||
workspaceDir,
|
||||
localRoots,
|
||||
media: [{ path: siblingImagePath, contentType: "image/png" }],
|
||||
}),
|
||||
).rejects.toThrow("failed to hydrate 1 structured image attachment");
|
||||
} finally {
|
||||
envSnapshot.restore();
|
||||
await fs.rm(stateDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("dedupes repeated refs and skips failed loads before sanitizing", async () => {
|
||||
const workspaceDir = await fs.mkdtemp(
|
||||
path.join(resolvePreferredOpenClawTmpDir(), "openclaw-cli-ref-dedupe-"),
|
||||
|
||||
@@ -4,6 +4,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createSolidPngBuffer } from "../../test/helpers/image-fixtures.js";
|
||||
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
|
||||
import {
|
||||
markMcpLoopbackToolCallFinished,
|
||||
@@ -149,6 +150,32 @@ async function createCliPackageFixture(version: string): Promise<{
|
||||
}
|
||||
|
||||
describe("runCliAgent spawn path", () => {
|
||||
it("hydrates a session-key-owned agent workspace image before spawning the CLI", async () => {
|
||||
const stateDir = tempDirs.make("openclaw-cli-agent-image-");
|
||||
const workspaceDir = path.join(stateDir, "workspace-arthur");
|
||||
const imagePath = path.join(workspaceDir, "media", "inbound", "photo.png");
|
||||
const image = createSolidPngBuffer(1, 1, { r: 255, g: 0, b: 0 });
|
||||
await fs.mkdir(path.dirname(imagePath), { recursive: true });
|
||||
await fs.writeFile(imagePath, image);
|
||||
vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
|
||||
mockSuccessfulCliRun(CLAUDE_OK_JSONL);
|
||||
const context = buildPreparedCliRunContext({
|
||||
sessionKey: "agent:arthur:main",
|
||||
agentId: "arthur",
|
||||
workspaceDir,
|
||||
config: {
|
||||
agents: { entries: { arthur: { default: true, workspace: workspaceDir } } },
|
||||
},
|
||||
backend: { imageArg: "--image" },
|
||||
});
|
||||
context.params.media = [{ path: imagePath, contentType: "image/png" }];
|
||||
|
||||
await expect(executePreparedCliRun(context)).resolves.toMatchObject({ text: "ok" });
|
||||
const spawn = requireRecord(mockCallArg(supervisorSpawnMock), "CLI spawn");
|
||||
const hydratedPath = requireArgAfter(spawn.argv as string[], "--image");
|
||||
await expect(fs.readFile(hydratedPath)).resolves.toEqual(image);
|
||||
});
|
||||
|
||||
it("formats output digests without logging response content", () => {
|
||||
expect(formatCliBackendOutputDigest("one")).toBe("outBytes=3 outHash=7692c3ad3540");
|
||||
expect(formatCliBackendOutputDigest("∑")).toBe("outBytes=3 outHash=be27c7179a61");
|
||||
|
||||
@@ -6,6 +6,7 @@ import { isTruthyEnvValue } from "../../infra/env.js";
|
||||
import { formatErrorMessage, toErrorObject } from "../../infra/errors.js";
|
||||
import { sanitizeHostExecEnv } from "../../infra/host-env-security.js";
|
||||
import { compareValidSemver } from "../../infra/semver.js";
|
||||
import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js";
|
||||
import type { CliBackendThinkingLevel } from "../../plugins/cli-backend.types.js";
|
||||
import { applySkillEnvOverridesFromSnapshot } from "../../skills/runtime/env-overrides.js";
|
||||
import { appendBootstrapPromptWarning } from "../bootstrap-budget.js";
|
||||
@@ -185,6 +186,7 @@ export async function executePreparedCliRun(
|
||||
prompt,
|
||||
imagePrompt: params.imagePrompt,
|
||||
workspaceDir: context.workspaceDir,
|
||||
localRoots: getAgentScopedMediaLocalRoots(params.config ?? {}, params.agentId),
|
||||
images: params.images,
|
||||
imageOrder: params.imageOrder,
|
||||
media: params.media,
|
||||
|
||||
@@ -388,6 +388,7 @@ export async function prepareCliPromptImagePayload(params: {
|
||||
prompt: string;
|
||||
imagePrompt?: string;
|
||||
workspaceDir: string;
|
||||
localRoots?: readonly string[];
|
||||
images?: ImageContent[];
|
||||
imageOrder?: PromptImageOrderEntry[];
|
||||
media?: MediaFact[];
|
||||
@@ -411,6 +412,7 @@ export async function prepareCliPromptImagePayload(params: {
|
||||
existingImages: params.images,
|
||||
imageOrder: params.imageOrder,
|
||||
maxBytes: MAX_IMAGE_BYTES,
|
||||
localRoots: params.localRoots,
|
||||
})
|
||||
: undefined;
|
||||
if (imageResult?.failedMediaCount) {
|
||||
|
||||
@@ -419,6 +419,34 @@ describe("prepareCliRunContext", () => {
|
||||
fixture.cleanup();
|
||||
});
|
||||
|
||||
it("carries the session-key-derived workspace owner into prepared params", async () => {
|
||||
const { dir } = fixture.session;
|
||||
const arthurWorkspace = path.join(dir, "workspace-arthur");
|
||||
const normalizeConfig = vi.fn((config: CliBackendPlugin["config"]) => config);
|
||||
setRawCliBackendForPrepareTest({ ...defaultTestCliBackend, normalizeConfig });
|
||||
const config = {
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "main", default: true, workspace: path.join(dir, "workspace-main") },
|
||||
{ id: "arthur", workspace: arthurWorkspace },
|
||||
],
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
const context = await fixture.prepare({
|
||||
sessionKey: "agent:arthur:main",
|
||||
workspaceDir: arthurWorkspace,
|
||||
config,
|
||||
});
|
||||
|
||||
expect(normalizeConfig).toHaveBeenCalledWith(expect.any(Object), {
|
||||
backendId: "test-cli",
|
||||
agentId: "arthur",
|
||||
config,
|
||||
});
|
||||
expect(context.params.agentId).toBe("arthur");
|
||||
expect(context.workspaceDir).toBe(arthurWorkspace);
|
||||
});
|
||||
|
||||
it("honors an explicit auth agent directory independently of session identity", async () => {
|
||||
const { dir } = fixture.session;
|
||||
const modelOwnerAgentDir = path.join(dir, "ops-agent");
|
||||
@@ -4289,7 +4317,7 @@ describe("prepareCliRunContext", () => {
|
||||
expect(getLiveSessionGeneration).toHaveBeenCalledWith({
|
||||
backendId: "claude-cli",
|
||||
agentAccountId: undefined,
|
||||
agentId: undefined,
|
||||
agentId: "main",
|
||||
authProfileId: undefined,
|
||||
sessionId: "session-test",
|
||||
sessionKey: "agent:main:telegram:direct:peer",
|
||||
@@ -4303,6 +4331,7 @@ describe("prepareCliRunContext", () => {
|
||||
mode: "reuse",
|
||||
sessionId: "warm-claude-sid",
|
||||
});
|
||||
expect(context.params.agentId).toBe("main");
|
||||
expect(context.requiredClaudeLiveSessionGeneration).toBe("warm-live-generation");
|
||||
expect(context.openClawHistoryPrompt).toContain("earlier warm context");
|
||||
expect(context.openClawHistoryPrompt).toContain("warm follow-up");
|
||||
|
||||
@@ -444,7 +444,7 @@ export async function prepareCliRunContext(
|
||||
preparedRunAdmission: candidate.preparedRunAdmission,
|
||||
});
|
||||
const { preparedRunAdmission: _preparedRunAdmission, ...rest } = candidate;
|
||||
return { ...rest, admittedRunContext };
|
||||
return { ...rest, agentId: workspaceResolution.agentId, admittedRunContext };
|
||||
};
|
||||
const runtimeChatType = params.chatType ?? params.sessionEntry?.chatType;
|
||||
const workspaceResolution = resolveRunWorkspaceDir({
|
||||
@@ -466,8 +466,10 @@ export async function prepareCliRunContext(
|
||||
const cwd = params.cwd ? resolveUserPath(params.cwd) : workspaceDir;
|
||||
const cwdHash = hashCliSessionText(cwd);
|
||||
|
||||
// params.agentId may identify a distinct runtime-policy requester. Backend
|
||||
// config and managed process reuse must key from the resolved session owner.
|
||||
const backendResolved = resolveCliBackendConfig(params.provider, params.config, {
|
||||
agentId: params.agentId,
|
||||
agentId: workspaceResolution.agentId,
|
||||
});
|
||||
if (!backendResolved) {
|
||||
throw new Error(`Unknown CLI backend: ${params.provider}`);
|
||||
@@ -1414,7 +1416,7 @@ export async function prepareCliRunContext(
|
||||
prepareDeps.getClaudeGeneration({
|
||||
backendId: backendResolved.id,
|
||||
agentAccountId: params.agentAccountId,
|
||||
agentId: params.agentId,
|
||||
agentId: workspaceResolution.agentId,
|
||||
authProfileId: effectiveAuthProfileId,
|
||||
sessionId: params.sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
|
||||
Reference in New Issue
Block a user