diff --git a/README.md b/README.md index a9f5bf7..763ac2d 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,6 @@ Export request params: "runId": "turn-1", "artifactScope": "tasks/thread-main-.../turn-1-...", "sinceUnixMs": 1770000000000, - "latestIfEmpty": true, "maxFiles": 64, "maxInlineBytes": 10485760 } @@ -140,20 +139,15 @@ 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`. When that current task scope has no files and -`latestIfEmpty` is true, the plugin scans 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 top-level -`tasks/` directory so other runs are not exported as workspace fallback files. +derived from `sessionKey/runId`. If that scope has no files, export/list returns +an empty artifact list. The plugin does not scan the workspace root and does not +borrow artifacts from 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 for the current session, including -same-session historical task fallback results returned by the plugin. Workspace -fallback files must be read with a same-session and same-run `artifactRef`; there -is no unscoped arbitrary workspace read API. +Signed task `artifactRef` values are accepted only for the same `sessionKey/runId` +that issued them. There is no unscoped arbitrary workspace read API. ## View And Download @@ -185,7 +179,7 @@ Gateway clients can use: - `xworkmate.artifacts.prepare` before `chat.send` to allocate a task artifact directory. - `xworkmate.artifacts.list` for a metadata-only manifest and Markdown table. - `xworkmate.artifacts.read` with `artifactScope` and `relativePath` for one task file. -- `xworkmate.artifacts.read` with `artifactRef` for a plugin-returned task or `workspace-latest` file. +- `xworkmate.artifacts.read` with `artifactRef` for a plugin-returned task file. - `xworkmate.artifacts.export` with `artifactScope` after `agent.wait` for the XWorkmate APP sync path. Large files are metadata-only in the export payload, but XWorkmate Bridge can @@ -195,8 +189,7 @@ only remote file access path. ## Limits - Only files inside the resolved OpenClaw workspace are exported. -- `.git`, `.openclaw`, `.xworkmate`, `.pi`, build outputs, and dependency folders are skipped when scanning the workspace root. -- Top-level `tasks/` is skipped during workspace fallback scanning. +- `.git`, `.openclaw`, `.xworkmate`, `.pi`, build outputs, and dependency folders are excluded from task artifact exports. - 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//`. diff --git a/dist/index.js b/dist/index.js index fcfbab2..c8fa87c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -100,7 +100,7 @@ function createXWorkmateArtifactsTool(api, ctx) { }, artifactRef: { type: "string", - description: "Plugin-signed artifact reference returned by export/list. Required for workspace-latest reads.", + description: "Plugin-signed artifact reference returned by export/list. Bound to the issuing task scope.", }, sinceUnixMs: { type: "number", diff --git a/dist/src/exportArtifacts.d.ts b/dist/src/exportArtifacts.d.ts index 474386e..c407940 100644 --- a/dist/src/exportArtifacts.d.ts +++ b/dist/src/exportArtifacts.d.ts @@ -10,7 +10,7 @@ export type XWorkmateArtifact = { encoding?: "base64"; content?: string; }; -export type XWorkmateArtifactScopeKind = "task" | "workspace" | "workspace-latest"; +export type XWorkmateArtifactScopeKind = "task"; export type XWorkmateArtifactExport = { runId: string; sessionKey: string; diff --git a/dist/src/exportArtifacts.js b/dist/src/exportArtifacts.js index 15c895e..9dd1203 100644 --- a/dist/src/exportArtifacts.js +++ b/dist/src/exportArtifacts.js @@ -59,8 +59,6 @@ export async function exportXWorkmateArtifacts(input) { const maxInlineBytes = nonNegativeInteger(params.maxInlineBytes, pluginConfig.maxInlineBytes, DEFAULT_MAX_INLINE_BYTES); const sinceUnixMs = nonNegativeNumber(params.sinceUnixMs, 0); const includeContent = optionalBoolean(params.includeContent, true); - const latestIfEmpty = optionalBoolean(params.latestIfEmpty, false); - const latestTaskScopeIfEmpty = optionalBoolean(params.latestTaskScopeIfEmpty, false); const workspaceDir = resolveWorkspaceDir({ config: input.config, pluginConfig, @@ -77,41 +75,14 @@ export async function exportXWorkmateArtifacts(input) { const sessionScope = taskSessionScopeFor(sessionKey); const artifactScope = requestedArtifactScope || expectedArtifactScope; const scopeRoot = resolveScopeRoot(workspaceRoot, artifactScope); - let scopeKind = "task"; - let candidates = await collectCandidates({ + const scopeKind = "task"; + const candidates = await collectCandidates({ scanRoot: scopeRoot, relativeRoot: scopeRoot, sinceUnixMs, skipTaskScopeRoot: false, warnings, }); - if (candidates.length === 0 && latestIfEmpty) { - const latestWarnings = []; - const latestCandidates = latestTaskScopeIfEmpty - ? await collectLatestSessionTaskCandidates({ - workspaceRoot, - sessionKey, - warnings: latestWarnings, - }) - : await collectCandidates({ - scanRoot: workspaceRoot, - relativeRoot: workspaceRoot, - sinceUnixMs: 0, - skipTaskScopeRoot: true, - warnings: latestWarnings, - }); - if (latestCandidates.length > 0) { - warnings.push(...latestWarnings); - if (latestTaskScopeIfEmpty) { - warnings.push("scoped artifact directory is empty; exported latest session task files instead"); - } - else { - warnings.push("scoped artifact directory is empty; exported latest workspace files instead"); - } - candidates = latestCandidates; - scopeKind = "workspace-latest"; - } - } candidates.sort((left, right) => { if (right.mtimeMs !== left.mtimeMs) { return right.mtimeMs - left.mtimeMs; @@ -140,16 +111,14 @@ export async function exportXWorkmateArtifacts(input) { scopeKind: scopeKindForCandidate, sessionScope, runScope: expectedArtifactScope, - ...(scopeKindForCandidate === "task" && artifactScopeForCandidate - ? { artifactScope: artifactScopeForCandidate } - : {}), + ...(artifactScopeForCandidate ? { artifactScope: artifactScopeForCandidate } : {}), relativePath: candidate.relativePath, sizeBytes: bytes.byteLength, sha256, }, pluginConfig), scopeKind: scopeKindForCandidate, }; - if (scopeKindForCandidate === "task" && artifactScopeForCandidate) { + if (artifactScopeForCandidate) { artifact.artifactScope = artifactScopeForCandidate; } if (includeContent && bytes.byteLength <= maxInlineBytes) { @@ -166,7 +135,7 @@ export async function exportXWorkmateArtifacts(input) { sessionKey, remoteWorkingDirectory: workspaceRoot, remoteWorkspaceRefKind: "remotePath", - ...(scopeKind === "task" ? { artifactScope } : {}), + artifactScope, scopeKind, artifacts, warnings, @@ -210,19 +179,13 @@ export async function readXWorkmateArtifact(input) { if (requestedScope && requestedScope !== artifactScope) { throw new Error("artifactRef does not match artifactScope"); } - if (refPayload.scopeKind === "task") { - assertArtifactScopeMatchesRequest(artifactScope, expectedArtifactScope, expectedSessionScope, { - allowSameSessionTaskHistory: true, - }); - } + assertArtifactScopeMatchesRequest(artifactScope, expectedArtifactScope); } else { if (!artifactScope) { throw new Error("artifactScope or artifactRef required"); } - assertArtifactScopeMatchesRequest(artifactScope, expectedArtifactScope, expectedSessionScope, { - allowSameSessionTaskHistory: false, - }); + assertArtifactScopeMatchesRequest(artifactScope, expectedArtifactScope); relativePath = safeInputRelativePath(params.relativePath, "relativePath"); } const scopeRoot = artifactScope ? resolveScopeRoot(workspaceRoot, artifactScope) : workspaceRoot; @@ -372,57 +335,16 @@ async function collectCandidates(input) { } } } -async function collectLatestSessionTaskCandidates(input) { - const sessionScope = taskSessionScopeFor(input.sessionKey); - const sessionRoot = path.join(input.workspaceRoot, sessionScope.split("/").join(path.sep)); - let entries; - try { - entries = await fs.readdir(sessionRoot, { withFileTypes: true }); - } - catch { - return []; - } - const candidates = []; - for (const entry of entries) { - if (!entry.isDirectory() || entry.name === "." || entry.name === "..") { - continue; - } - const artifactScope = [sessionScope, entry.name].join("/"); - let scopeRoot; - try { - scopeRoot = resolveScopeRoot(input.workspaceRoot, artifactScope); - } - catch { - continue; - } - const scopedCandidates = await collectCandidates({ - scanRoot: scopeRoot, - relativeRoot: scopeRoot, - sinceUnixMs: 0, - skipTaskScopeRoot: false, - warnings: input.warnings, - }); - candidates.push(...scopedCandidates.map((candidate) => ({ - ...candidate, - artifactScope, - scopeKind: "task", - }))); - } - return candidates; -} function artifactScopeFor(sessionKey, runId) { return [taskSessionScopeFor(sessionKey), safeScopeSegment(runId)].join("/"); } function taskSessionScopeFor(sessionKey) { return [TASK_SCOPE_ROOT, safeScopeSegment(sessionKey)].join("/"); } -function assertArtifactScopeMatchesRequest(artifactScope, expectedArtifactScope, expectedSessionScope, options) { +function assertArtifactScopeMatchesRequest(artifactScope, expectedArtifactScope) { if (artifactScope === expectedArtifactScope) { return; } - if (options.allowSameSessionTaskHistory && artifactScope.startsWith(`${expectedSessionScope}/`)) { - return; - } throw new Error("artifactScope does not match sessionKey/runId"); } function assertArtifactRefMatchesRequest(payload, expectedRunScope, expectedSessionScope) { @@ -664,7 +586,7 @@ function verifyArtifactRef(artifactRef, workspaceRoot, pluginConfig) { } const payload = objectRecord(parsed); const scopeKind = optionalString(payload.scopeKind); - if (!["task", "workspace", "workspace-latest"].includes(scopeKind)) { + if (scopeKind !== "task") { throw new Error("invalid artifactRef"); } const relativePath = safeInputRelativePath(payload.relativePath, "artifactRef relativePath"); @@ -672,9 +594,6 @@ function verifyArtifactRef(artifactRef, workspaceRoot, pluginConfig) { if (scopeKind === "task" && !artifactScope) { throw new Error("invalid artifactRef"); } - if (scopeKind !== "task" && artifactScope) { - throw new Error("invalid artifactRef"); - } const sizeBytes = nonNegativeInteger(payload.sizeBytes, undefined, -1); const sha256 = optionalString(payload.sha256).toLowerCase(); if (payload.v !== 2 || sizeBytes < 0 || !/^[a-f0-9]{64}$/.test(sha256)) { diff --git a/index.ts b/index.ts index cdcc7d4..91a3095 100644 --- a/index.ts +++ b/index.ts @@ -125,7 +125,7 @@ function createXWorkmateArtifactsTool( }, artifactRef: { type: "string", - description: "Plugin-signed artifact reference returned by export/list. Required for workspace-latest reads.", + description: "Plugin-signed artifact reference returned by export/list. Bound to the issuing task scope.", }, sinceUnixMs: { type: "number", diff --git a/src/exportArtifacts.test.ts b/src/exportArtifacts.test.ts index c2e7723..0ac22eb 100644 --- a/src/exportArtifacts.test.ts +++ b/src/exportArtifacts.test.ts @@ -176,7 +176,7 @@ describe("exportXWorkmateArtifacts", () => { expect(result.artifacts.map((entry) => entry.relativePath)).toEqual(["current.txt"]); }); - it("does not scan the workspace root when the current task scope is empty without latestIfEmpty", async () => { + it("does not scan the workspace root when the current task scope is empty", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); await prepareXWorkmateArtifacts({ params: { sessionKey: "thread-main", runId: "turn-1" }, @@ -219,7 +219,7 @@ describe("exportXWorkmateArtifacts", () => { ).rejects.toThrow("artifactScope does not match sessionKey/runId"); }); - it("falls back to latest workspace files when the scoped directory is empty", async () => { + it("does not fall back to workspace files when the scoped directory is empty", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const prepared = await prepareXWorkmateArtifacts({ params: { sessionKey: "thread-main", runId: "turn-1" }, @@ -241,26 +241,26 @@ describe("exportXWorkmateArtifacts", () => { runId: "turn-1", artifactScope: prepared.artifactScope, sinceUnixMs: stat.mtimeMs + 10_000, - latestIfEmpty: true, }, pluginConfig: { workspaceDir: root }, }); - expect(result.scopeKind).toBe("workspace-latest"); - expect(result.artifactScope).toBeUndefined(); - expect(result.artifacts.map((entry) => entry.relativePath)).toEqual(["existing.pdf"]); - expect(result.artifacts[0]?.artifactScope).toBeUndefined(); - expect(result.artifacts[0]?.scopeKind).toBe("workspace-latest"); - expect(result.artifacts[0]?.artifactRef).toContain("."); - expect(result.warnings).toContain("scoped artifact directory is empty; exported latest workspace files instead"); + expect(result.scopeKind).toBe("task"); + expect(result.artifactScope).toBe(prepared.artifactScope); + expect(result.artifacts).toEqual([]); + expect(result.warnings).toEqual([]); }); - it("falls back to latest session task files when requested", async () => { + it("does not borrow previous session task files when current task scope is empty", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const previousTask = await prepareXWorkmateArtifacts({ params: { sessionKey: "thread-main", runId: "turn-previous" }, pluginConfig: { workspaceDir: root }, }); + await prepareXWorkmateArtifacts({ + params: { sessionKey: "thread-main", runId: "turn-follow-up" }, + pluginConfig: { workspaceDir: root }, + }); await fs.writeFile(path.join(previousTask.artifactDirectory, "k8s-networking.pdf"), "pdf"); await fs.writeFile(path.join(previousTask.artifactDirectory, "k8s-networking.docx"), "docx"); @@ -269,55 +269,57 @@ describe("exportXWorkmateArtifacts", () => { sessionKey: "thread-main", runId: "turn-follow-up", sinceUnixMs: Date.now() + 10_000, - latestIfEmpty: true, - latestTaskScopeIfEmpty: true, }, pluginConfig: { workspaceDir: root }, }); - expect(result.scopeKind).toBe("workspace-latest"); - expect(result.artifactScope).toBeUndefined(); - expect(result.artifacts.map((entry) => entry.relativePath)).toEqual([ - "k8s-networking.docx", - "k8s-networking.pdf", - ]); - expect( - result.artifacts.map((entry) => ({ - artifactScope: entry.artifactScope, - scopeKind: entry.scopeKind, - })), - ).toEqual([ - { artifactScope: previousTask.artifactScope, scopeKind: "task" }, - { artifactScope: previousTask.artifactScope, scopeKind: "task" }, - ]); - expect(result.warnings).toContain("scoped artifact directory is empty; exported latest session task files instead"); + expect(result.scopeKind).toBe("task"); + expect(result.artifactScope).toBe("tasks/thread-main/turn-follow-up"); + expect(result.artifacts).toEqual([]); + expect(result.warnings).toEqual([]); }); - it("does not include another session in latest session task fallback", async () => { + it("exports concurrent task scopes independently", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); - const sameSessionTask = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-previous" }, - pluginConfig: { workspaceDir: root }, - }); - const otherSessionTask = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-other", runId: "turn-previous" }, - pluginConfig: { workspaceDir: root }, - }); - await fs.writeFile(path.join(sameSessionTask.artifactDirectory, "same.txt"), "same"); - await fs.writeFile(path.join(otherSessionTask.artifactDirectory, "other.txt"), "other"); + const prepared = await Promise.all([ + prepareXWorkmateArtifacts({ + params: { sessionKey: "thread-a", runId: "turn-1" }, + pluginConfig: { workspaceDir: root }, + }), + prepareXWorkmateArtifacts({ + params: { sessionKey: "thread-b", runId: "turn-1" }, + pluginConfig: { workspaceDir: root }, + }), + prepareXWorkmateArtifacts({ + params: { sessionKey: "thread-a", runId: "turn-2" }, + pluginConfig: { workspaceDir: root }, + }), + ]); + await fs.writeFile(path.join(prepared[0].artifactDirectory, "a-1.txt"), "a1"); + await fs.writeFile(path.join(prepared[1].artifactDirectory, "b-1.txt"), "b1"); + await fs.writeFile(path.join(prepared[2].artifactDirectory, "a-2.txt"), "a2"); - const result = await exportXWorkmateArtifacts({ - params: { - sessionKey: "thread-main", - runId: "turn-follow-up", - latestIfEmpty: true, - latestTaskScopeIfEmpty: true, - }, - pluginConfig: { workspaceDir: root }, - }); + const results = await Promise.all([ + exportXWorkmateArtifacts({ + params: { sessionKey: "thread-a", runId: "turn-1" }, + pluginConfig: { workspaceDir: root }, + }), + exportXWorkmateArtifacts({ + params: { sessionKey: "thread-b", runId: "turn-1" }, + pluginConfig: { workspaceDir: root }, + }), + exportXWorkmateArtifacts({ + params: { sessionKey: "thread-a", runId: "turn-2" }, + pluginConfig: { workspaceDir: root }, + }), + ]); - expect(result.artifacts.map((entry) => entry.relativePath)).toEqual(["same.txt"]); - expect(result.artifacts[0]?.artifactScope).toBe(sameSessionTask.artifactScope); + expect(results.map((result) => result.artifacts.map((entry) => entry.relativePath))).toEqual([ + ["a-1.txt"], + ["b-1.txt"], + ["a-2.txt"], + ]); + expect(results.map((result) => result.artifactScope)).toEqual(prepared.map((entry) => entry.artifactScope)); }); it("leaves oversized artifacts out of inline content", async () => { @@ -511,59 +513,19 @@ describe("exportXWorkmateArtifacts", () => { ).rejects.toThrow("artifactRef does not match sessionKey/runId"); }); - it("reads a latest workspace artifact only through its artifactRef", async () => { + it("rejects signed task artifact refs from another run", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const prepared = await prepareXWorkmateArtifacts({ params: { sessionKey: "thread-main", runId: "turn-1" }, pluginConfig: { workspaceDir: root }, }); - await fs.writeFile(path.join(root, "existing.txt"), "existing"); + await fs.writeFile(path.join(prepared.artifactDirectory, "existing.txt"), "existing"); const exported = await exportXWorkmateArtifacts({ params: { sessionKey: "thread-main", runId: "turn-1", artifactScope: prepared.artifactScope, - sinceUnixMs: Date.now() + 10_000, - latestIfEmpty: true, - }, - pluginConfig: { workspaceDir: root }, - }); - - const result = await readXWorkmateArtifact({ - params: { - sessionKey: "thread-main", - runId: "turn-1", - artifactRef: exported.artifacts[0]?.artifactRef, - }, - pluginConfig: { workspaceDir: root }, - }); - - expect(result.scopeKind).toBe("workspace-latest"); - expect(result.artifactScope).toBeUndefined(); - expect(result.artifacts[0]).toMatchObject({ - relativePath: "existing.txt", - scopeKind: "workspace-latest", - encoding: "base64", - content: Buffer.from("existing").toString("base64"), - }); - }); - - it("rejects latest workspace artifact refs from another run", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); - const prepared = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-1" }, - pluginConfig: { workspaceDir: root }, - }); - await fs.writeFile(path.join(root, "existing.txt"), "existing"); - - const exported = await exportXWorkmateArtifacts({ - params: { - sessionKey: "thread-main", - runId: "turn-1", - artifactScope: prepared.artifactScope, - sinceUnixMs: Date.now() + 10_000, - latestIfEmpty: true, }, pluginConfig: { workspaceDir: root }, }); @@ -616,7 +578,7 @@ describe("exportXWorkmateArtifacts", () => { JSON.stringify({ v: 1, workspaceRootHash: createHash("sha256").update(path.resolve(root)).digest("hex"), - scopeKind: "workspace-latest", + scopeKind: "task", relativePath: "existing.txt", sizeBytes: 8, sha256: createHash("sha256").update("existing").digest("hex"), diff --git a/src/exportArtifacts.ts b/src/exportArtifacts.ts index 338301a..dba4b71 100644 --- a/src/exportArtifacts.ts +++ b/src/exportArtifacts.ts @@ -34,7 +34,7 @@ export type XWorkmateArtifact = { content?: string; }; -export type XWorkmateArtifactScopeKind = "task" | "workspace" | "workspace-latest"; +export type XWorkmateArtifactScopeKind = "task"; export type XWorkmateArtifactExport = { runId: string; @@ -140,8 +140,6 @@ export async function exportXWorkmateArtifacts(input: ExportInput): Promise 0) { - warnings.push(...latestWarnings); - if (latestTaskScopeIfEmpty) { - warnings.push("scoped artifact directory is empty; exported latest session task files instead"); - } else { - warnings.push("scoped artifact directory is empty; exported latest workspace files instead"); - } - candidates = latestCandidates; - scopeKind = "workspace-latest"; - } - } - candidates.sort((left, right) => { if (right.mtimeMs !== left.mtimeMs) { return right.mtimeMs - left.mtimeMs; @@ -225,9 +196,7 @@ export async function exportXWorkmateArtifacts(input: ExportInput): Promise { - const sessionScope = taskSessionScopeFor(input.sessionKey); - const sessionRoot = path.join(input.workspaceRoot, sessionScope.split("/").join(path.sep)); - let entries; - try { - entries = await fs.readdir(sessionRoot, { withFileTypes: true }); - } catch { - return []; - } - const candidates: Candidate[] = []; - for (const entry of entries) { - if (!entry.isDirectory() || entry.name === "." || entry.name === "..") { - continue; - } - const artifactScope = [sessionScope, entry.name].join("/"); - let scopeRoot: string; - try { - scopeRoot = resolveScopeRoot(input.workspaceRoot, artifactScope); - } catch { - continue; - } - const scopedCandidates = await collectCandidates({ - scanRoot: scopeRoot, - relativeRoot: scopeRoot, - sinceUnixMs: 0, - skipTaskScopeRoot: false, - warnings: input.warnings, - }); - candidates.push( - ...scopedCandidates.map((candidate) => ({ - ...candidate, - artifactScope, - scopeKind: "task" as const, - })), - ); - } - return candidates; -} - function artifactScopeFor(sessionKey: string, runId: string): string { return [taskSessionScopeFor(sessionKey), safeScopeSegment(runId)].join("/"); } @@ -538,15 +458,10 @@ function taskSessionScopeFor(sessionKey: string): string { function assertArtifactScopeMatchesRequest( artifactScope: string, expectedArtifactScope: string, - expectedSessionScope: string, - options: { allowSameSessionTaskHistory: boolean }, ): void { if (artifactScope === expectedArtifactScope) { return; } - if (options.allowSameSessionTaskHistory && artifactScope.startsWith(`${expectedSessionScope}/`)) { - return; - } throw new Error("artifactScope does not match sessionKey/runId"); } @@ -822,7 +737,7 @@ function verifyArtifactRef( } const payload = objectRecord(parsed); const scopeKind = optionalString(payload.scopeKind) as XWorkmateArtifactScopeKind; - if (!["task", "workspace", "workspace-latest"].includes(scopeKind)) { + if (scopeKind !== "task") { throw new Error("invalid artifactRef"); } const relativePath = safeInputRelativePath(payload.relativePath, "artifactRef relativePath"); @@ -830,9 +745,6 @@ function verifyArtifactRef( if (scopeKind === "task" && !artifactScope) { throw new Error("invalid artifactRef"); } - if (scopeKind !== "task" && artifactScope) { - throw new Error("invalid artifactRef"); - } const sizeBytes = nonNegativeInteger(payload.sizeBytes, undefined, -1); const sha256 = optionalString(payload.sha256).toLowerCase(); if (payload.v !== 2 || sizeBytes < 0 || !/^[a-f0-9]{64}$/.test(sha256)) {