Keep OpenClaw artifact export task scoped

This commit is contained in:
Haitao Pan 2026-06-06 08:16:28 +08:00
parent d887df0f64
commit 529965aa3b
4 changed files with 20 additions and 103 deletions

View File

@ -72,16 +72,18 @@ Equivalent config shape for a linked checkout:
Prepare request params are supplied by the OpenClaw host, bridge, or APP
runtime. On OpenClaw runtimes that expose a trusted plugin `sessionScope`, the
plugin uses that scope first. Otherwise it falls back to bridge/app runtime
params. The plugin treats `sessionKey`, `runId`, and `workspaceDir` as the
mapping into OpenClaw's built-in multi-session model; it does not parse paths
from chat text and does not invent fallback session/run identities. The optional
agent tool does not expose these fields to the model; it only uses host-injected
tool context.
plugin uses that native scope first and maps native `sessionScope.sessionKey`
to `openclawSessionKey` internally. External Gateway callers must use typed
`appThreadKey`, `openclawSessionKey`, `runId`, and optional `workspaceDir`
params. Legacy `sessionKey` is not accepted as a Gateway task or artifact lookup
alias. The plugin does not parse paths from chat text and does not invent
fallback session/run identities. The optional agent tool does not expose these
fields to the model; it only uses host-injected tool context.
```json
{
"sessionKey": "thread-main",
"appThreadKey": "draft:thread-main",
"openclawSessionKey": "agent:main:draft:thread-main",
"runId": "turn-1",
"workspaceDir": "/home/user/.openclaw/workspace"
}
@ -92,7 +94,7 @@ Prepare response payload:
```json
{
"runId": "turn-1",
"sessionKey": "thread-main",
"sessionKey": "agent:main:draft:thread-main",
"remoteWorkingDirectory": "/home/user/.openclaw/workspace",
"remoteWorkspaceRefKind": "remotePath",
"artifactScope": "tasks/thread-main-.../turn-1-...",
@ -107,7 +109,7 @@ Export request params:
```json
{
"sessionKey": "thread-main",
"openclawSessionKey": "agent:main:draft:thread-main",
"runId": "turn-1",
"artifactScope": "tasks/thread-main-.../turn-1-...",
"sinceUnixMs": 1770000000000,
@ -121,7 +123,7 @@ Export response payload:
```json
{
"runId": "turn-1",
"sessionKey": "thread-main",
"sessionKey": "agent:main:draft:thread-main",
"remoteWorkingDirectory": "/home/user/.openclaw/workspace",
"remoteWorkspaceRefKind": "remotePath",
"artifactScope": "tasks/thread-main-.../turn-1-...",
@ -144,9 +146,10 @@ Export response payload:
Files at or below `maxInlineBytes` also include `encoding: "base64"` and `content`.
When `artifactScope` is omitted, export/list defaults to the current task scope
derived from `sessionKey/runId`. `sinceUnixMs` is only a filter inside that task
scope. The prepared task scope remains authoritative: when it contains files,
the plugin exports only that scope.
derived from `openclawSessionKey/runId` for Gateway calls, or from native
`sessionScope.sessionKey/runId` for host-injected tool calls. `sinceUnixMs` is
only a filter inside that task scope. The prepared task scope remains
authoritative: when it contains files, the plugin exports only that scope.
If the prepared task scope is empty, trusted Gateway callers may pass
`expectedArtifactDirs` such as `["assets/images", "reports"]`. The plugin then
@ -157,9 +160,10 @@ earlier task scopes.
Each exported artifact includes `artifactRef`, a plugin-signed reference over
the issued session/run scope, artifact scope, path, size, and SHA-256 digest. `read` accepts
`artifactScope + relativePath` for the current `sessionKey/runId` task scope.
Signed task `artifactRef` values are accepted only for the same `sessionKey/runId`
that issued them. There is no unscoped arbitrary workspace read API.
`artifactScope + relativePath` for the current `openclawSessionKey/runId` task
scope. Signed task `artifactRef` values are accepted only for the same
`openclawSessionKey/runId` that issued them. There is no unscoped arbitrary
workspace read API.
## View And Download

View File

@ -161,29 +161,6 @@ export async function exportXWorkmateArtifacts(input) {
warnings.push(`Unable to read artifact scope timestamp: ${String(error)}`);
}
}
// Copy global media to task scope to fix path breakage
if (scopePrepared || sinceUnixMs > 0) {
for (const source of openClawSnapshotSources(params, pluginConfig)) {
const globalCandidates = await collectSnapshotSourceCandidates({
source,
sinceUnixMs: effectiveSince,
warnings,
});
for (const gc of globalCandidates) {
const destRelPath = safeSnapshotDestinationRelativePath(source.label, gc.relativePath);
const dest = path.join(scopeRoot, "artifacts", destRelPath.split("/").join(path.sep));
if (isWithinRoot(scopeRoot, dest)) {
try {
await fs.mkdir(path.dirname(dest), { recursive: true });
await fs.copyFile(gc.absolutePath, dest);
}
catch (error) {
warnings.push(`Failed to copy media file ${gc.relativePath}: ${String(error)}`);
}
}
}
}
}
const scopedCandidates = (await directoryExists(scopeRoot))
? await collectCandidates({
scanRoot: scopeRoot,
@ -777,13 +754,6 @@ function resolveWorkspaceDir(input) {
}
throw new Error("UnsupportedError: workspaceDir must be explicitly provided in params or pluginConfig");
}
function agentIdFromSessionKey(sessionKey) {
const parts = sessionKey.split(":");
if (parts.length >= 3 && parts[0] === "agent") {
return parts[1]?.trim() ?? "";
}
return "";
}
function safeRelativePath(root, target) {
const relative = path.relative(root, target);
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {

View File

@ -748,32 +748,6 @@ describe("exportXWorkmateArtifacts", () => {
expect(result.warnings).toContain("artifact limit reached; skipped remaining files after 1");
});
it("selects an agent workspace from agent session keys", async () => {
const mainRoot = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-main-"));
const agentRoot = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-agent-"));
await fs.writeFile(path.join(mainRoot, "main.txt"), "main");
const prepared = await prepareXWorkmateArtifacts({
params: { openclawSessionKey: "agent:research:thread-1", runId: "run-1" },
pluginConfig: { workspaceDir: agentRoot },
});
await fs.writeFile(path.join(prepared.artifactDirectory, "agent.txt"), "agent");
const result = await exportXWorkmateArtifacts({
params: {
openclawSessionKey: "agent:research:thread-1",
runId: "run-1",
},
config: {
agents: {
defaults: { workspace: mainRoot },
list: [{ id: "research", workspace: agentRoot }],
},
},
});
expect(result.artifacts.map((entry) => entry.relativePath)).toEqual(["agent.txt"]);
});
it("rejects unscoped artifact reads by relative path", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-"));
await fs.mkdir(path.join(root, "reports"), { recursive: true });

View File

@ -266,28 +266,6 @@ export async function exportXWorkmateArtifacts(input: ExportInput): Promise<XWor
}
}
// Copy global media to task scope to fix path breakage
if (scopePrepared || sinceUnixMs > 0) {
for (const source of openClawSnapshotSources(params, pluginConfig)) {
const globalCandidates = await collectSnapshotSourceCandidates({
source,
sinceUnixMs: effectiveSince,
warnings,
});
for (const gc of globalCandidates) {
const destRelPath = safeSnapshotDestinationRelativePath(source.label, gc.relativePath);
const dest = path.join(scopeRoot, "artifacts", destRelPath.split("/").join(path.sep));
if (isWithinRoot(scopeRoot, dest)) {
try {
await fs.mkdir(path.dirname(dest), { recursive: true });
await fs.copyFile(gc.absolutePath, dest);
} catch (error) {
warnings.push(`Failed to copy media file ${gc.relativePath}: ${String(error)}`);
}
}
}
}
}
const scopedCandidates = (await directoryExists(scopeRoot))
? await collectCandidates({
scanRoot: scopeRoot,
@ -960,14 +938,6 @@ function resolveWorkspaceDir(input: {
throw new Error("UnsupportedError: workspaceDir must be explicitly provided in params or pluginConfig");
}
function agentIdFromSessionKey(sessionKey: string): string {
const parts = sessionKey.split(":");
if (parts.length >= 3 && parts[0] === "agent") {
return parts[1]?.trim() ?? "";
}
return "";
}
function safeRelativePath(root: string, target: string): string {
const relative = path.relative(root, target);
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
@ -1033,7 +1003,6 @@ function contentTypeForPath(relativePath: string): string {
}
function openClawSnapshotSources(params: Record<string, unknown>, pluginConfig: Record<string, unknown>): SnapshotSource[] {
return [
{
label: "media",