Restrict artifacts to current task scope
This commit is contained in:
parent
097a62e2fa
commit
0ee969ea16
21
README.md
21
README.md
@ -106,7 +106,6 @@ Export request params:
|
|||||||
"runId": "turn-1",
|
"runId": "turn-1",
|
||||||
"artifactScope": "tasks/thread-main-.../turn-1-...",
|
"artifactScope": "tasks/thread-main-.../turn-1-...",
|
||||||
"sinceUnixMs": 1770000000000,
|
"sinceUnixMs": 1770000000000,
|
||||||
"latestIfEmpty": true,
|
|
||||||
"maxFiles": 64,
|
"maxFiles": 64,
|
||||||
"maxInlineBytes": 10485760
|
"maxInlineBytes": 10485760
|
||||||
}
|
}
|
||||||
@ -140,20 +139,15 @@ Export response payload:
|
|||||||
|
|
||||||
Files at or below `maxInlineBytes` also include `encoding: "base64"` and `content`.
|
Files at or below `maxInlineBytes` also include `encoding: "base64"` and `content`.
|
||||||
When `artifactScope` is omitted, export/list defaults to the current task scope
|
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
|
derived from `sessionKey/runId`. If that scope has no files, export/list returns
|
||||||
`latestIfEmpty` is true, the plugin scans the workspace root for the latest real
|
an empty artifact list. The plugin does not scan the workspace root and does not
|
||||||
files and returns them with `scopeKind: "workspace-latest"`. This is a controlled
|
borrow artifacts from earlier task scopes.
|
||||||
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.
|
|
||||||
|
|
||||||
Each exported artifact includes `artifactRef`, a plugin-signed reference over
|
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
|
the issued session/run scope, artifact scope, path, size, and SHA-256 digest. `read` accepts
|
||||||
`artifactScope + relativePath` for the current `sessionKey/runId` task scope.
|
`artifactScope + relativePath` for the current `sessionKey/runId` task scope.
|
||||||
Signed task `artifactRef` values are accepted for the current session, including
|
Signed task `artifactRef` values are accepted only for the same `sessionKey/runId`
|
||||||
same-session historical task fallback results returned by the plugin. Workspace
|
that issued them. There is no unscoped arbitrary workspace read API.
|
||||||
fallback files must be read with a same-session and same-run `artifactRef`; there
|
|
||||||
is no unscoped arbitrary workspace read API.
|
|
||||||
|
|
||||||
## View And Download
|
## 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.prepare` before `chat.send` to allocate a task artifact directory.
|
||||||
- `xworkmate.artifacts.list` for a metadata-only manifest and Markdown table.
|
- `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 `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.
|
- `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
|
Large files are metadata-only in the export payload, but XWorkmate Bridge can
|
||||||
@ -195,8 +189,7 @@ only remote file access path.
|
|||||||
## Limits
|
## Limits
|
||||||
|
|
||||||
- Only files inside the resolved OpenClaw workspace are exported.
|
- 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.
|
- `.git`, `.openclaw`, `.xworkmate`, `.pi`, build outputs, and dependency folders are excluded from task artifact exports.
|
||||||
- Top-level `tasks/` is skipped during workspace fallback scanning.
|
|
||||||
- Symlinks are skipped to avoid workspace escape.
|
- Symlinks are skipped to avoid workspace escape.
|
||||||
- Files larger than `maxInlineBytes` are listed with metadata and a warning, but are not inlined.
|
- Files larger than `maxInlineBytes` are listed with metadata and a warning, but are not inlined.
|
||||||
- `artifactScope` must be `tasks/<safe-session-key>/<safe-run-id>`.
|
- `artifactScope` must be `tasks/<safe-session-key>/<safe-run-id>`.
|
||||||
|
|||||||
2
dist/index.js
vendored
2
dist/index.js
vendored
@ -100,7 +100,7 @@ function createXWorkmateArtifactsTool(api, ctx) {
|
|||||||
},
|
},
|
||||||
artifactRef: {
|
artifactRef: {
|
||||||
type: "string",
|
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: {
|
sinceUnixMs: {
|
||||||
type: "number",
|
type: "number",
|
||||||
|
|||||||
2
dist/src/exportArtifacts.d.ts
vendored
2
dist/src/exportArtifacts.d.ts
vendored
@ -10,7 +10,7 @@ export type XWorkmateArtifact = {
|
|||||||
encoding?: "base64";
|
encoding?: "base64";
|
||||||
content?: string;
|
content?: string;
|
||||||
};
|
};
|
||||||
export type XWorkmateArtifactScopeKind = "task" | "workspace" | "workspace-latest";
|
export type XWorkmateArtifactScopeKind = "task";
|
||||||
export type XWorkmateArtifactExport = {
|
export type XWorkmateArtifactExport = {
|
||||||
runId: string;
|
runId: string;
|
||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
|
|||||||
99
dist/src/exportArtifacts.js
vendored
99
dist/src/exportArtifacts.js
vendored
@ -59,8 +59,6 @@ export async function exportXWorkmateArtifacts(input) {
|
|||||||
const maxInlineBytes = nonNegativeInteger(params.maxInlineBytes, pluginConfig.maxInlineBytes, DEFAULT_MAX_INLINE_BYTES);
|
const maxInlineBytes = nonNegativeInteger(params.maxInlineBytes, pluginConfig.maxInlineBytes, DEFAULT_MAX_INLINE_BYTES);
|
||||||
const sinceUnixMs = nonNegativeNumber(params.sinceUnixMs, 0);
|
const sinceUnixMs = nonNegativeNumber(params.sinceUnixMs, 0);
|
||||||
const includeContent = optionalBoolean(params.includeContent, true);
|
const includeContent = optionalBoolean(params.includeContent, true);
|
||||||
const latestIfEmpty = optionalBoolean(params.latestIfEmpty, false);
|
|
||||||
const latestTaskScopeIfEmpty = optionalBoolean(params.latestTaskScopeIfEmpty, false);
|
|
||||||
const workspaceDir = resolveWorkspaceDir({
|
const workspaceDir = resolveWorkspaceDir({
|
||||||
config: input.config,
|
config: input.config,
|
||||||
pluginConfig,
|
pluginConfig,
|
||||||
@ -77,41 +75,14 @@ export async function exportXWorkmateArtifacts(input) {
|
|||||||
const sessionScope = taskSessionScopeFor(sessionKey);
|
const sessionScope = taskSessionScopeFor(sessionKey);
|
||||||
const artifactScope = requestedArtifactScope || expectedArtifactScope;
|
const artifactScope = requestedArtifactScope || expectedArtifactScope;
|
||||||
const scopeRoot = resolveScopeRoot(workspaceRoot, artifactScope);
|
const scopeRoot = resolveScopeRoot(workspaceRoot, artifactScope);
|
||||||
let scopeKind = "task";
|
const scopeKind = "task";
|
||||||
let candidates = await collectCandidates({
|
const candidates = await collectCandidates({
|
||||||
scanRoot: scopeRoot,
|
scanRoot: scopeRoot,
|
||||||
relativeRoot: scopeRoot,
|
relativeRoot: scopeRoot,
|
||||||
sinceUnixMs,
|
sinceUnixMs,
|
||||||
skipTaskScopeRoot: false,
|
skipTaskScopeRoot: false,
|
||||||
warnings,
|
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) => {
|
candidates.sort((left, right) => {
|
||||||
if (right.mtimeMs !== left.mtimeMs) {
|
if (right.mtimeMs !== left.mtimeMs) {
|
||||||
return right.mtimeMs - left.mtimeMs;
|
return right.mtimeMs - left.mtimeMs;
|
||||||
@ -140,16 +111,14 @@ export async function exportXWorkmateArtifacts(input) {
|
|||||||
scopeKind: scopeKindForCandidate,
|
scopeKind: scopeKindForCandidate,
|
||||||
sessionScope,
|
sessionScope,
|
||||||
runScope: expectedArtifactScope,
|
runScope: expectedArtifactScope,
|
||||||
...(scopeKindForCandidate === "task" && artifactScopeForCandidate
|
...(artifactScopeForCandidate ? { artifactScope: artifactScopeForCandidate } : {}),
|
||||||
? { artifactScope: artifactScopeForCandidate }
|
|
||||||
: {}),
|
|
||||||
relativePath: candidate.relativePath,
|
relativePath: candidate.relativePath,
|
||||||
sizeBytes: bytes.byteLength,
|
sizeBytes: bytes.byteLength,
|
||||||
sha256,
|
sha256,
|
||||||
}, pluginConfig),
|
}, pluginConfig),
|
||||||
scopeKind: scopeKindForCandidate,
|
scopeKind: scopeKindForCandidate,
|
||||||
};
|
};
|
||||||
if (scopeKindForCandidate === "task" && artifactScopeForCandidate) {
|
if (artifactScopeForCandidate) {
|
||||||
artifact.artifactScope = artifactScopeForCandidate;
|
artifact.artifactScope = artifactScopeForCandidate;
|
||||||
}
|
}
|
||||||
if (includeContent && bytes.byteLength <= maxInlineBytes) {
|
if (includeContent && bytes.byteLength <= maxInlineBytes) {
|
||||||
@ -166,7 +135,7 @@ export async function exportXWorkmateArtifacts(input) {
|
|||||||
sessionKey,
|
sessionKey,
|
||||||
remoteWorkingDirectory: workspaceRoot,
|
remoteWorkingDirectory: workspaceRoot,
|
||||||
remoteWorkspaceRefKind: "remotePath",
|
remoteWorkspaceRefKind: "remotePath",
|
||||||
...(scopeKind === "task" ? { artifactScope } : {}),
|
artifactScope,
|
||||||
scopeKind,
|
scopeKind,
|
||||||
artifacts,
|
artifacts,
|
||||||
warnings,
|
warnings,
|
||||||
@ -210,19 +179,13 @@ export async function readXWorkmateArtifact(input) {
|
|||||||
if (requestedScope && requestedScope !== artifactScope) {
|
if (requestedScope && requestedScope !== artifactScope) {
|
||||||
throw new Error("artifactRef does not match artifactScope");
|
throw new Error("artifactRef does not match artifactScope");
|
||||||
}
|
}
|
||||||
if (refPayload.scopeKind === "task") {
|
assertArtifactScopeMatchesRequest(artifactScope, expectedArtifactScope);
|
||||||
assertArtifactScopeMatchesRequest(artifactScope, expectedArtifactScope, expectedSessionScope, {
|
|
||||||
allowSameSessionTaskHistory: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (!artifactScope) {
|
if (!artifactScope) {
|
||||||
throw new Error("artifactScope or artifactRef required");
|
throw new Error("artifactScope or artifactRef required");
|
||||||
}
|
}
|
||||||
assertArtifactScopeMatchesRequest(artifactScope, expectedArtifactScope, expectedSessionScope, {
|
assertArtifactScopeMatchesRequest(artifactScope, expectedArtifactScope);
|
||||||
allowSameSessionTaskHistory: false,
|
|
||||||
});
|
|
||||||
relativePath = safeInputRelativePath(params.relativePath, "relativePath");
|
relativePath = safeInputRelativePath(params.relativePath, "relativePath");
|
||||||
}
|
}
|
||||||
const scopeRoot = artifactScope ? resolveScopeRoot(workspaceRoot, artifactScope) : workspaceRoot;
|
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) {
|
function artifactScopeFor(sessionKey, runId) {
|
||||||
return [taskSessionScopeFor(sessionKey), safeScopeSegment(runId)].join("/");
|
return [taskSessionScopeFor(sessionKey), safeScopeSegment(runId)].join("/");
|
||||||
}
|
}
|
||||||
function taskSessionScopeFor(sessionKey) {
|
function taskSessionScopeFor(sessionKey) {
|
||||||
return [TASK_SCOPE_ROOT, safeScopeSegment(sessionKey)].join("/");
|
return [TASK_SCOPE_ROOT, safeScopeSegment(sessionKey)].join("/");
|
||||||
}
|
}
|
||||||
function assertArtifactScopeMatchesRequest(artifactScope, expectedArtifactScope, expectedSessionScope, options) {
|
function assertArtifactScopeMatchesRequest(artifactScope, expectedArtifactScope) {
|
||||||
if (artifactScope === expectedArtifactScope) {
|
if (artifactScope === expectedArtifactScope) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (options.allowSameSessionTaskHistory && artifactScope.startsWith(`${expectedSessionScope}/`)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error("artifactScope does not match sessionKey/runId");
|
throw new Error("artifactScope does not match sessionKey/runId");
|
||||||
}
|
}
|
||||||
function assertArtifactRefMatchesRequest(payload, expectedRunScope, expectedSessionScope) {
|
function assertArtifactRefMatchesRequest(payload, expectedRunScope, expectedSessionScope) {
|
||||||
@ -664,7 +586,7 @@ function verifyArtifactRef(artifactRef, workspaceRoot, pluginConfig) {
|
|||||||
}
|
}
|
||||||
const payload = objectRecord(parsed);
|
const payload = objectRecord(parsed);
|
||||||
const scopeKind = optionalString(payload.scopeKind);
|
const scopeKind = optionalString(payload.scopeKind);
|
||||||
if (!["task", "workspace", "workspace-latest"].includes(scopeKind)) {
|
if (scopeKind !== "task") {
|
||||||
throw new Error("invalid artifactRef");
|
throw new Error("invalid artifactRef");
|
||||||
}
|
}
|
||||||
const relativePath = safeInputRelativePath(payload.relativePath, "artifactRef relativePath");
|
const relativePath = safeInputRelativePath(payload.relativePath, "artifactRef relativePath");
|
||||||
@ -672,9 +594,6 @@ function verifyArtifactRef(artifactRef, workspaceRoot, pluginConfig) {
|
|||||||
if (scopeKind === "task" && !artifactScope) {
|
if (scopeKind === "task" && !artifactScope) {
|
||||||
throw new Error("invalid artifactRef");
|
throw new Error("invalid artifactRef");
|
||||||
}
|
}
|
||||||
if (scopeKind !== "task" && artifactScope) {
|
|
||||||
throw new Error("invalid artifactRef");
|
|
||||||
}
|
|
||||||
const sizeBytes = nonNegativeInteger(payload.sizeBytes, undefined, -1);
|
const sizeBytes = nonNegativeInteger(payload.sizeBytes, undefined, -1);
|
||||||
const sha256 = optionalString(payload.sha256).toLowerCase();
|
const sha256 = optionalString(payload.sha256).toLowerCase();
|
||||||
if (payload.v !== 2 || sizeBytes < 0 || !/^[a-f0-9]{64}$/.test(sha256)) {
|
if (payload.v !== 2 || sizeBytes < 0 || !/^[a-f0-9]{64}$/.test(sha256)) {
|
||||||
|
|||||||
2
index.ts
2
index.ts
@ -125,7 +125,7 @@ function createXWorkmateArtifactsTool(
|
|||||||
},
|
},
|
||||||
artifactRef: {
|
artifactRef: {
|
||||||
type: "string",
|
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: {
|
sinceUnixMs: {
|
||||||
type: "number",
|
type: "number",
|
||||||
|
|||||||
@ -176,7 +176,7 @@ describe("exportXWorkmateArtifacts", () => {
|
|||||||
expect(result.artifacts.map((entry) => entry.relativePath)).toEqual(["current.txt"]);
|
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-"));
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-"));
|
||||||
await prepareXWorkmateArtifacts({
|
await prepareXWorkmateArtifacts({
|
||||||
params: { sessionKey: "thread-main", runId: "turn-1" },
|
params: { sessionKey: "thread-main", runId: "turn-1" },
|
||||||
@ -219,7 +219,7 @@ describe("exportXWorkmateArtifacts", () => {
|
|||||||
).rejects.toThrow("artifactScope does not match sessionKey/runId");
|
).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 root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-"));
|
||||||
const prepared = await prepareXWorkmateArtifacts({
|
const prepared = await prepareXWorkmateArtifacts({
|
||||||
params: { sessionKey: "thread-main", runId: "turn-1" },
|
params: { sessionKey: "thread-main", runId: "turn-1" },
|
||||||
@ -241,26 +241,26 @@ describe("exportXWorkmateArtifacts", () => {
|
|||||||
runId: "turn-1",
|
runId: "turn-1",
|
||||||
artifactScope: prepared.artifactScope,
|
artifactScope: prepared.artifactScope,
|
||||||
sinceUnixMs: stat.mtimeMs + 10_000,
|
sinceUnixMs: stat.mtimeMs + 10_000,
|
||||||
latestIfEmpty: true,
|
|
||||||
},
|
},
|
||||||
pluginConfig: { workspaceDir: root },
|
pluginConfig: { workspaceDir: root },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.scopeKind).toBe("workspace-latest");
|
expect(result.scopeKind).toBe("task");
|
||||||
expect(result.artifactScope).toBeUndefined();
|
expect(result.artifactScope).toBe(prepared.artifactScope);
|
||||||
expect(result.artifacts.map((entry) => entry.relativePath)).toEqual(["existing.pdf"]);
|
expect(result.artifacts).toEqual([]);
|
||||||
expect(result.artifacts[0]?.artifactScope).toBeUndefined();
|
expect(result.warnings).toEqual([]);
|
||||||
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");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-"));
|
||||||
const previousTask = await prepareXWorkmateArtifacts({
|
const previousTask = await prepareXWorkmateArtifacts({
|
||||||
params: { sessionKey: "thread-main", runId: "turn-previous" },
|
params: { sessionKey: "thread-main", runId: "turn-previous" },
|
||||||
pluginConfig: { workspaceDir: root },
|
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.pdf"), "pdf");
|
||||||
await fs.writeFile(path.join(previousTask.artifactDirectory, "k8s-networking.docx"), "docx");
|
await fs.writeFile(path.join(previousTask.artifactDirectory, "k8s-networking.docx"), "docx");
|
||||||
|
|
||||||
@ -269,55 +269,57 @@ describe("exportXWorkmateArtifacts", () => {
|
|||||||
sessionKey: "thread-main",
|
sessionKey: "thread-main",
|
||||||
runId: "turn-follow-up",
|
runId: "turn-follow-up",
|
||||||
sinceUnixMs: Date.now() + 10_000,
|
sinceUnixMs: Date.now() + 10_000,
|
||||||
latestIfEmpty: true,
|
|
||||||
latestTaskScopeIfEmpty: true,
|
|
||||||
},
|
},
|
||||||
pluginConfig: { workspaceDir: root },
|
pluginConfig: { workspaceDir: root },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.scopeKind).toBe("workspace-latest");
|
expect(result.scopeKind).toBe("task");
|
||||||
expect(result.artifactScope).toBeUndefined();
|
expect(result.artifactScope).toBe("tasks/thread-main/turn-follow-up");
|
||||||
expect(result.artifacts.map((entry) => entry.relativePath)).toEqual([
|
expect(result.artifacts).toEqual([]);
|
||||||
"k8s-networking.docx",
|
expect(result.warnings).toEqual([]);
|
||||||
"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");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-"));
|
||||||
const sameSessionTask = await prepareXWorkmateArtifacts({
|
const prepared = await Promise.all([
|
||||||
params: { sessionKey: "thread-main", runId: "turn-previous" },
|
prepareXWorkmateArtifacts({
|
||||||
pluginConfig: { workspaceDir: root },
|
params: { sessionKey: "thread-a", runId: "turn-1" },
|
||||||
});
|
pluginConfig: { workspaceDir: root },
|
||||||
const otherSessionTask = await prepareXWorkmateArtifacts({
|
}),
|
||||||
params: { sessionKey: "thread-other", runId: "turn-previous" },
|
prepareXWorkmateArtifacts({
|
||||||
pluginConfig: { workspaceDir: root },
|
params: { sessionKey: "thread-b", runId: "turn-1" },
|
||||||
});
|
pluginConfig: { workspaceDir: root },
|
||||||
await fs.writeFile(path.join(sameSessionTask.artifactDirectory, "same.txt"), "same");
|
}),
|
||||||
await fs.writeFile(path.join(otherSessionTask.artifactDirectory, "other.txt"), "other");
|
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({
|
const results = await Promise.all([
|
||||||
params: {
|
exportXWorkmateArtifacts({
|
||||||
sessionKey: "thread-main",
|
params: { sessionKey: "thread-a", runId: "turn-1" },
|
||||||
runId: "turn-follow-up",
|
pluginConfig: { workspaceDir: root },
|
||||||
latestIfEmpty: true,
|
}),
|
||||||
latestTaskScopeIfEmpty: true,
|
exportXWorkmateArtifacts({
|
||||||
},
|
params: { sessionKey: "thread-b", runId: "turn-1" },
|
||||||
pluginConfig: { workspaceDir: root },
|
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(results.map((result) => result.artifacts.map((entry) => entry.relativePath))).toEqual([
|
||||||
expect(result.artifacts[0]?.artifactScope).toBe(sameSessionTask.artifactScope);
|
["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 () => {
|
it("leaves oversized artifacts out of inline content", async () => {
|
||||||
@ -511,59 +513,19 @@ describe("exportXWorkmateArtifacts", () => {
|
|||||||
).rejects.toThrow("artifactRef does not match sessionKey/runId");
|
).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 root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-"));
|
||||||
const prepared = await prepareXWorkmateArtifacts({
|
const prepared = await prepareXWorkmateArtifacts({
|
||||||
params: { sessionKey: "thread-main", runId: "turn-1" },
|
params: { sessionKey: "thread-main", runId: "turn-1" },
|
||||||
pluginConfig: { workspaceDir: root },
|
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({
|
const exported = await exportXWorkmateArtifacts({
|
||||||
params: {
|
params: {
|
||||||
sessionKey: "thread-main",
|
sessionKey: "thread-main",
|
||||||
runId: "turn-1",
|
runId: "turn-1",
|
||||||
artifactScope: prepared.artifactScope,
|
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 },
|
pluginConfig: { workspaceDir: root },
|
||||||
});
|
});
|
||||||
@ -616,7 +578,7 @@ describe("exportXWorkmateArtifacts", () => {
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
v: 1,
|
v: 1,
|
||||||
workspaceRootHash: createHash("sha256").update(path.resolve(root)).digest("hex"),
|
workspaceRootHash: createHash("sha256").update(path.resolve(root)).digest("hex"),
|
||||||
scopeKind: "workspace-latest",
|
scopeKind: "task",
|
||||||
relativePath: "existing.txt",
|
relativePath: "existing.txt",
|
||||||
sizeBytes: 8,
|
sizeBytes: 8,
|
||||||
sha256: createHash("sha256").update("existing").digest("hex"),
|
sha256: createHash("sha256").update("existing").digest("hex"),
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export type XWorkmateArtifact = {
|
|||||||
content?: string;
|
content?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type XWorkmateArtifactScopeKind = "task" | "workspace" | "workspace-latest";
|
export type XWorkmateArtifactScopeKind = "task";
|
||||||
|
|
||||||
export type XWorkmateArtifactExport = {
|
export type XWorkmateArtifactExport = {
|
||||||
runId: string;
|
runId: string;
|
||||||
@ -140,8 +140,6 @@ export async function exportXWorkmateArtifacts(input: ExportInput): Promise<XWor
|
|||||||
);
|
);
|
||||||
const sinceUnixMs = nonNegativeNumber(params.sinceUnixMs, 0);
|
const sinceUnixMs = nonNegativeNumber(params.sinceUnixMs, 0);
|
||||||
const includeContent = optionalBoolean(params.includeContent, true);
|
const includeContent = optionalBoolean(params.includeContent, true);
|
||||||
const latestIfEmpty = optionalBoolean(params.latestIfEmpty, false);
|
|
||||||
const latestTaskScopeIfEmpty = optionalBoolean(params.latestTaskScopeIfEmpty, false);
|
|
||||||
const workspaceDir = resolveWorkspaceDir({
|
const workspaceDir = resolveWorkspaceDir({
|
||||||
config: input.config,
|
config: input.config,
|
||||||
pluginConfig,
|
pluginConfig,
|
||||||
@ -158,8 +156,8 @@ export async function exportXWorkmateArtifacts(input: ExportInput): Promise<XWor
|
|||||||
const sessionScope = taskSessionScopeFor(sessionKey);
|
const sessionScope = taskSessionScopeFor(sessionKey);
|
||||||
const artifactScope = requestedArtifactScope || expectedArtifactScope;
|
const artifactScope = requestedArtifactScope || expectedArtifactScope;
|
||||||
const scopeRoot = resolveScopeRoot(workspaceRoot, artifactScope);
|
const scopeRoot = resolveScopeRoot(workspaceRoot, artifactScope);
|
||||||
let scopeKind: XWorkmateArtifactScopeKind = "task";
|
const scopeKind: XWorkmateArtifactScopeKind = "task";
|
||||||
let candidates = await collectCandidates({
|
const candidates = await collectCandidates({
|
||||||
scanRoot: scopeRoot,
|
scanRoot: scopeRoot,
|
||||||
relativeRoot: scopeRoot,
|
relativeRoot: scopeRoot,
|
||||||
sinceUnixMs,
|
sinceUnixMs,
|
||||||
@ -167,33 +165,6 @@ export async function exportXWorkmateArtifacts(input: ExportInput): Promise<XWor
|
|||||||
warnings,
|
warnings,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (candidates.length === 0 && latestIfEmpty) {
|
|
||||||
const latestWarnings: string[] = [];
|
|
||||||
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) => {
|
candidates.sort((left, right) => {
|
||||||
if (right.mtimeMs !== left.mtimeMs) {
|
if (right.mtimeMs !== left.mtimeMs) {
|
||||||
return right.mtimeMs - left.mtimeMs;
|
return right.mtimeMs - left.mtimeMs;
|
||||||
@ -225,9 +196,7 @@ export async function exportXWorkmateArtifacts(input: ExportInput): Promise<XWor
|
|||||||
scopeKind: scopeKindForCandidate,
|
scopeKind: scopeKindForCandidate,
|
||||||
sessionScope,
|
sessionScope,
|
||||||
runScope: expectedArtifactScope,
|
runScope: expectedArtifactScope,
|
||||||
...(scopeKindForCandidate === "task" && artifactScopeForCandidate
|
...(artifactScopeForCandidate ? { artifactScope: artifactScopeForCandidate } : {}),
|
||||||
? { artifactScope: artifactScopeForCandidate }
|
|
||||||
: {}),
|
|
||||||
relativePath: candidate.relativePath,
|
relativePath: candidate.relativePath,
|
||||||
sizeBytes: bytes.byteLength,
|
sizeBytes: bytes.byteLength,
|
||||||
sha256,
|
sha256,
|
||||||
@ -236,7 +205,7 @@ export async function exportXWorkmateArtifacts(input: ExportInput): Promise<XWor
|
|||||||
),
|
),
|
||||||
scopeKind: scopeKindForCandidate,
|
scopeKind: scopeKindForCandidate,
|
||||||
};
|
};
|
||||||
if (scopeKindForCandidate === "task" && artifactScopeForCandidate) {
|
if (artifactScopeForCandidate) {
|
||||||
artifact.artifactScope = artifactScopeForCandidate;
|
artifact.artifactScope = artifactScopeForCandidate;
|
||||||
}
|
}
|
||||||
if (includeContent && bytes.byteLength <= maxInlineBytes) {
|
if (includeContent && bytes.byteLength <= maxInlineBytes) {
|
||||||
@ -253,7 +222,7 @@ export async function exportXWorkmateArtifacts(input: ExportInput): Promise<XWor
|
|||||||
sessionKey,
|
sessionKey,
|
||||||
remoteWorkingDirectory: workspaceRoot,
|
remoteWorkingDirectory: workspaceRoot,
|
||||||
remoteWorkspaceRefKind: "remotePath" as const,
|
remoteWorkspaceRefKind: "remotePath" as const,
|
||||||
...(scopeKind === "task" ? { artifactScope } : {}),
|
artifactScope,
|
||||||
scopeKind,
|
scopeKind,
|
||||||
artifacts,
|
artifacts,
|
||||||
warnings,
|
warnings,
|
||||||
@ -302,18 +271,12 @@ export async function readXWorkmateArtifact(input: ReadInput): Promise<XWorkmate
|
|||||||
if (requestedScope && requestedScope !== artifactScope) {
|
if (requestedScope && requestedScope !== artifactScope) {
|
||||||
throw new Error("artifactRef does not match artifactScope");
|
throw new Error("artifactRef does not match artifactScope");
|
||||||
}
|
}
|
||||||
if (refPayload.scopeKind === "task") {
|
assertArtifactScopeMatchesRequest(artifactScope, expectedArtifactScope);
|
||||||
assertArtifactScopeMatchesRequest(artifactScope, expectedArtifactScope, expectedSessionScope, {
|
|
||||||
allowSameSessionTaskHistory: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (!artifactScope) {
|
if (!artifactScope) {
|
||||||
throw new Error("artifactScope or artifactRef required");
|
throw new Error("artifactScope or artifactRef required");
|
||||||
}
|
}
|
||||||
assertArtifactScopeMatchesRequest(artifactScope, expectedArtifactScope, expectedSessionScope, {
|
assertArtifactScopeMatchesRequest(artifactScope, expectedArtifactScope);
|
||||||
allowSameSessionTaskHistory: false,
|
|
||||||
});
|
|
||||||
relativePath = safeInputRelativePath(params.relativePath, "relativePath");
|
relativePath = safeInputRelativePath(params.relativePath, "relativePath");
|
||||||
}
|
}
|
||||||
const scopeRoot = artifactScope ? resolveScopeRoot(workspaceRoot, artifactScope) : workspaceRoot;
|
const scopeRoot = artifactScope ? resolveScopeRoot(workspaceRoot, artifactScope) : workspaceRoot;
|
||||||
@ -484,49 +447,6 @@ async function collectCandidates(input: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function collectLatestSessionTaskCandidates(input: {
|
|
||||||
workspaceRoot: string;
|
|
||||||
sessionKey: string;
|
|
||||||
warnings: string[];
|
|
||||||
}): Promise<Candidate[]> {
|
|
||||||
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 {
|
function artifactScopeFor(sessionKey: string, runId: string): string {
|
||||||
return [taskSessionScopeFor(sessionKey), safeScopeSegment(runId)].join("/");
|
return [taskSessionScopeFor(sessionKey), safeScopeSegment(runId)].join("/");
|
||||||
}
|
}
|
||||||
@ -538,15 +458,10 @@ function taskSessionScopeFor(sessionKey: string): string {
|
|||||||
function assertArtifactScopeMatchesRequest(
|
function assertArtifactScopeMatchesRequest(
|
||||||
artifactScope: string,
|
artifactScope: string,
|
||||||
expectedArtifactScope: string,
|
expectedArtifactScope: string,
|
||||||
expectedSessionScope: string,
|
|
||||||
options: { allowSameSessionTaskHistory: boolean },
|
|
||||||
): void {
|
): void {
|
||||||
if (artifactScope === expectedArtifactScope) {
|
if (artifactScope === expectedArtifactScope) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (options.allowSameSessionTaskHistory && artifactScope.startsWith(`${expectedSessionScope}/`)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error("artifactScope does not match sessionKey/runId");
|
throw new Error("artifactScope does not match sessionKey/runId");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -822,7 +737,7 @@ function verifyArtifactRef(
|
|||||||
}
|
}
|
||||||
const payload = objectRecord(parsed);
|
const payload = objectRecord(parsed);
|
||||||
const scopeKind = optionalString(payload.scopeKind) as XWorkmateArtifactScopeKind;
|
const scopeKind = optionalString(payload.scopeKind) as XWorkmateArtifactScopeKind;
|
||||||
if (!["task", "workspace", "workspace-latest"].includes(scopeKind)) {
|
if (scopeKind !== "task") {
|
||||||
throw new Error("invalid artifactRef");
|
throw new Error("invalid artifactRef");
|
||||||
}
|
}
|
||||||
const relativePath = safeInputRelativePath(payload.relativePath, "artifactRef relativePath");
|
const relativePath = safeInputRelativePath(payload.relativePath, "artifactRef relativePath");
|
||||||
@ -830,9 +745,6 @@ function verifyArtifactRef(
|
|||||||
if (scopeKind === "task" && !artifactScope) {
|
if (scopeKind === "task" && !artifactScope) {
|
||||||
throw new Error("invalid artifactRef");
|
throw new Error("invalid artifactRef");
|
||||||
}
|
}
|
||||||
if (scopeKind !== "task" && artifactScope) {
|
|
||||||
throw new Error("invalid artifactRef");
|
|
||||||
}
|
|
||||||
const sizeBytes = nonNegativeInteger(payload.sizeBytes, undefined, -1);
|
const sizeBytes = nonNegativeInteger(payload.sizeBytes, undefined, -1);
|
||||||
const sha256 = optionalString(payload.sha256).toLowerCase();
|
const sha256 = optionalString(payload.sha256).toLowerCase();
|
||||||
if (payload.v !== 2 || sizeBytes < 0 || !/^[a-f0-9]{64}$/.test(sha256)) {
|
if (payload.v !== 2 || sizeBytes < 0 || !/^[a-f0-9]{64}$/.test(sha256)) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user