From 529965aa3b65dcfce0996e2ed1381a4e0845a0b5 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sat, 6 Jun 2026 08:16:28 +0800 Subject: [PATCH] Keep OpenClaw artifact export task scoped --- README.md | 36 ++++++++++++++++++++---------------- dist/src/exportArtifacts.js | 30 ------------------------------ src/exportArtifacts.test.ts | 26 -------------------------- src/exportArtifacts.ts | 31 ------------------------------- 4 files changed, 20 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index c2546d4..84d8416 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/dist/src/exportArtifacts.js b/dist/src/exportArtifacts.js index e08bdd5..8895a8a 100644 --- a/dist/src/exportArtifacts.js +++ b/dist/src/exportArtifacts.js @@ -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)) { diff --git a/src/exportArtifacts.test.ts b/src/exportArtifacts.test.ts index 5ecbf1a..cc0fee7 100644 --- a/src/exportArtifacts.test.ts +++ b/src/exportArtifacts.test.ts @@ -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 }); diff --git a/src/exportArtifacts.ts b/src/exportArtifacts.ts index 6242d32..c8ced4e 100644 --- a/src/exportArtifacts.ts +++ b/src/exportArtifacts.ts @@ -266,28 +266,6 @@ export async function exportXWorkmateArtifacts(input: ExportInput): Promise 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, pluginConfig: Record): SnapshotSource[] { - return [ { label: "media",