diff --git a/README.md b/README.md index 2a2455f..d19cf02 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ xworkmate.artifacts.list xworkmate.artifacts.read ``` -`prepare` creates a per-task artifact scope under `tasks/` in the resolved OpenClaw workspace. `export` +`prepare` creates a per-task artifact scope under `.xworkmate/artifacts/tasks/` in the resolved OpenClaw workspace. `export` and `read` then return safe, relative artifact entries that XWorkmate Bridge can normalize into the APP `artifacts[]` contract. @@ -78,10 +78,10 @@ Prepare response payload: "sessionKey": "thread-main", "remoteWorkingDirectory": "/home/user/.openclaw/workspace", "remoteWorkspaceRefKind": "remotePath", - "artifactScope": "tasks/thread-main-.../turn-1-...", + "artifactScope": ".xworkmate/artifacts/tasks/thread-main-.../turn-1-...", "scopeKind": "task", - "artifactDirectory": "/home/user/.openclaw/workspace/tasks/thread-main-.../turn-1-...", - "relativeArtifactDirectory": "tasks/thread-main-.../turn-1-...", + "artifactDirectory": "/home/user/.openclaw/workspace/.xworkmate/artifacts/tasks/thread-main-.../turn-1-...", + "relativeArtifactDirectory": ".xworkmate/artifacts/tasks/thread-main-.../turn-1-...", "warnings": [] } ``` @@ -92,7 +92,7 @@ Export request params: { "sessionKey": "thread-main", "runId": "turn-1", - "artifactScope": "tasks/thread-main-.../turn-1-...", + "artifactScope": ".xworkmate/artifacts/tasks/thread-main-.../turn-1-...", "sinceUnixMs": 1770000000000, "latestIfEmpty": true, "maxFiles": 64, @@ -108,7 +108,7 @@ Export response payload: "sessionKey": "thread-main", "remoteWorkingDirectory": "/home/user/.openclaw/workspace", "remoteWorkspaceRefKind": "remotePath", - "artifactScope": "tasks/thread-main-.../turn-1-...", + "artifactScope": ".xworkmate/artifacts/tasks/thread-main-.../turn-1-...", "scopeKind": "task", "artifacts": [ { @@ -118,7 +118,7 @@ Export response payload: "sizeBytes": 1234, "sha256": "...", "artifactRef": "...", - "artifactScope": "tasks/thread-main-.../turn-1-...", + "artifactScope": ".xworkmate/artifacts/tasks/thread-main-.../turn-1-...", "scopeKind": "task" } ], @@ -131,7 +131,7 @@ When scoped export finds no task files and `latestIfEmpty` is true, the plugin s the workspace root for the latest real files and returns them with `scopeKind: "workspace-latest"`. This is a controlled recovery path for existing files already present in `/home/ubuntu/.openclaw/workspace`; it still skips plugin metadata and -runtime directories, including the plugin-owned top-level `tasks/` directory. +runtime directories, including the plugin-owned `.xworkmate/` directory. Each exported artifact includes `artifactRef`, a plugin-signed reference over the artifact scope, path, size, and SHA-256 digest. `read` accepts @@ -179,10 +179,10 @@ only remote file access path. ## Limits - Only files inside the resolved OpenClaw workspace are exported. -- `.git`, `.openclaw`, `.xworkmate`, `.pi`, top-level `tasks/`, build outputs, and dependency folders are skipped when scanning the workspace root. +- `.git`, `.openclaw`, `.xworkmate`, `.pi`, build outputs, and dependency folders are skipped when scanning the workspace root. - Symlinks are skipped to avoid workspace escape. - Files larger than `maxInlineBytes` are listed with metadata and a warning, but are not inlined. -- `artifactScope` must be `tasks//`. +- `artifactScope` must be `.xworkmate/artifacts/tasks//`. - `artifactScope`, `artifactRef`, and `relativePath` must stay inside the workspace; absolute paths, `..`, empty path segments, and symlink escapes are rejected. ## Development diff --git a/dist/src/exportArtifacts.js b/dist/src/exportArtifacts.js index 4f0f355..0afc489 100644 --- a/dist/src/exportArtifacts.js +++ b/dist/src/exportArtifacts.js @@ -4,7 +4,7 @@ import os from "node:os"; import path from "node:path"; const DEFAULT_MAX_FILES = 64; const DEFAULT_MAX_INLINE_BYTES = 10 * 1024 * 1024; -const TASK_SCOPE_ROOT = "tasks"; +const TASK_SCOPE_ROOT = ".xworkmate/artifacts/tasks"; const GENERATED_ARTIFACT_REF_SECRET = randomBytes(32).toString("hex"); const SKIPPED_DIRS = new Set([ ".git", @@ -360,7 +360,9 @@ function optionalArtifactScope(value) { function safeTaskArtifactScope(value) { const scope = safeInputRelativePath(value, "artifactScope"); const parts = scope.split("/"); - if (parts.length !== 3 || parts[0] !== TASK_SCOPE_ROOT) { + const rootParts = TASK_SCOPE_ROOT.split("/"); + const scopeRoot = parts.slice(0, rootParts.length).join("/"); + if (parts.length !== rootParts.length + 2 || scopeRoot !== TASK_SCOPE_ROOT) { throw new Error("artifactScope must be a task artifact scope"); } return scope; diff --git a/package.json b/package.json index e94b712..ec7652a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xworkmate-artifacts", - "version": "0.1.4", + "version": "0.1.5", "description": "XWorkmate artifact export plugin for OpenClaw Gateway", "type": "module", "license": "MIT", diff --git a/src/exportArtifacts.test.ts b/src/exportArtifacts.test.ts index a52764b..ddf61ac 100644 --- a/src/exportArtifacts.test.ts +++ b/src/exportArtifacts.test.ts @@ -22,8 +22,12 @@ describe("exportXWorkmateArtifacts", () => { pluginConfig: { workspaceDir: root }, }); - expect(first.artifactScope).toMatch(/^tasks\/thread-main-[a-f0-9]{12}\/turn-1-[a-f0-9]{12}$/); - expect(second.artifactScope).toMatch(/^tasks\/thread-main-[a-f0-9]{12}\/turn-2-[a-f0-9]{12}$/); + expect(first.artifactScope).toMatch( + /^\.xworkmate\/artifacts\/tasks\/thread-main-[a-f0-9]{12}\/turn-1-[a-f0-9]{12}$/, + ); + expect(second.artifactScope).toMatch( + /^\.xworkmate\/artifacts\/tasks\/thread-main-[a-f0-9]{12}\/turn-2-[a-f0-9]{12}$/, + ); expect(first.artifactScope).not.toBe(second.artifactScope); expect((await fs.stat(first.artifactDirectory)).isDirectory()).toBe(true); expect(first.remoteWorkingDirectory).toBe(await fs.realpath(root)); diff --git a/src/exportArtifacts.ts b/src/exportArtifacts.ts index 53a7aac..9aea652 100644 --- a/src/exportArtifacts.ts +++ b/src/exportArtifacts.ts @@ -5,7 +5,7 @@ import path from "node:path"; const DEFAULT_MAX_FILES = 64; const DEFAULT_MAX_INLINE_BYTES = 10 * 1024 * 1024; -const TASK_SCOPE_ROOT = "tasks"; +const TASK_SCOPE_ROOT = ".xworkmate/artifacts/tasks"; const GENERATED_ARTIFACT_REF_SECRET = randomBytes(32).toString("hex"); const SKIPPED_DIRS = new Set([ @@ -471,7 +471,9 @@ function optionalArtifactScope(value: unknown): string { function safeTaskArtifactScope(value: unknown): string { const scope = safeInputRelativePath(value, "artifactScope"); const parts = scope.split("/"); - if (parts.length !== 3 || parts[0] !== TASK_SCOPE_ROOT) { + const rootParts = TASK_SCOPE_ROOT.split("/"); + const scopeRoot = parts.slice(0, rootParts.length).join("/"); + if (parts.length !== rootParts.length + 2 || scopeRoot !== TASK_SCOPE_ROOT) { throw new Error("artifactScope must be a task artifact scope"); } return scope;