diff --git a/dist/src/expectedArtifactDirs.d.ts b/dist/src/expectedArtifactDirs.d.ts new file mode 100644 index 0000000..19b84d0 --- /dev/null +++ b/dist/src/expectedArtifactDirs.d.ts @@ -0,0 +1 @@ +export declare function normalizeExpectedArtifactDirs(value: unknown): string[]; diff --git a/dist/src/expectedArtifactDirs.js b/dist/src/expectedArtifactDirs.js new file mode 100644 index 0000000..95fe875 --- /dev/null +++ b/dist/src/expectedArtifactDirs.js @@ -0,0 +1,33 @@ +function optionalString(value) { + return typeof value === "string" ? value.trim() : ""; +} +function safeExpectedArtifactDir(value) { + const relativePath = optionalString(value); + if (!relativePath) { + return ""; + } + if (/^[A-Za-z]:[\\/]/u.test(relativePath) || relativePath.startsWith("/") || relativePath.includes("\0")) { + throw new Error("expectedArtifactDir must stay inside the workspace"); + } + const normalized = relativePath.split(/[\\/]/).filter(Boolean).join("/"); + if (!normalized || normalized.split("/").some((part) => part === ".." || part === ".")) { + throw new Error("expectedArtifactDir must stay inside the workspace"); + } + return normalized.endsWith("/") ? normalized : `${normalized}/`; +} +export function normalizeExpectedArtifactDirs(value) { + if (!Array.isArray(value)) { + return []; + } + const seen = new Set(); + const result = []; + for (const entry of value) { + const normalized = safeExpectedArtifactDir(entry); + if (!normalized || seen.has(normalized)) { + continue; + } + seen.add(normalized); + result.push(normalized); + } + return result; +} diff --git a/dist/src/exportArtifacts.d.ts b/dist/src/exportArtifacts.d.ts index ff435b1..6b16149 100644 --- a/dist/src/exportArtifacts.d.ts +++ b/dist/src/exportArtifacts.d.ts @@ -1,4 +1,4 @@ -export type XWorkmateArtifact = { +type XWorkmateArtifact = { relativePath: string; label: string; contentType: string; @@ -10,8 +10,8 @@ export type XWorkmateArtifact = { encoding?: "base64"; content?: string; }; -export type XWorkmateArtifactScopeKind = "task"; -export type XWorkmateArtifactExport = { +type XWorkmateArtifactScopeKind = "task"; +type XWorkmateArtifactExport = { runId: string; sessionKey: string; remoteWorkingDirectory: string; @@ -25,7 +25,7 @@ export type XWorkmateArtifactExport = { constraintSatisfied: boolean; missingRequiredExtensions: string[]; }; -export type XWorkmateArtifactPrepare = { +type XWorkmateArtifactPrepare = { runId: string; sessionKey: string; remoteWorkingDirectory: string; @@ -38,11 +38,11 @@ export type XWorkmateArtifactPrepare = { expectedArtifactDirs: string[]; expectedArtifactDirStatus: XWorkmateExpectedArtifactDirStatus[]; }; -export type XWorkmateExpectedArtifactDirStatus = { +type XWorkmateExpectedArtifactDirStatus = { relativePath: string; exists: boolean; }; -export type XWorkmateArtifactSnapshot = { +type XWorkmateArtifactSnapshot = { runId: string; sessionKey: string; remoteWorkingDirectory: string; @@ -68,8 +68,6 @@ export declare function prepareXWorkmateArtifacts(input: ExportInput): Promise; export declare function exportXWorkmateArtifacts(input: ExportInput): Promise; export declare function readXWorkmateArtifact(input: ReadInput): Promise; -export declare function normalizeExpectedArtifactDirs(value: unknown): string[]; -export declare function normalizeRequiredExtensions(value: unknown): string[]; export declare function formatArtifactManifestMarkdown(input: { remoteWorkingDirectory: string; artifactScope?: string; diff --git a/dist/src/exportArtifacts.js b/dist/src/exportArtifacts.js index 0e6cc5a..6a5806c 100644 --- a/dist/src/exportArtifacts.js +++ b/dist/src/exportArtifacts.js @@ -2,6 +2,7 @@ import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypt import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { normalizeExpectedArtifactDirs } from "./expectedArtifactDirs.js"; const DEFAULT_MAX_FILES = 64; const DEFAULT_MAX_INLINE_BYTES = 10 * 1024 * 1024; const TASK_SCOPE_ROOT = "tasks"; @@ -369,24 +370,7 @@ export async function readXWorkmateArtifact(input) { }; return result; } -export function normalizeExpectedArtifactDirs(value) { - if (!Array.isArray(value)) { - return []; - } - const seen = new Set(); - const result = []; - for (const entry of value) { - const normalized = safeInputRelativePath(entry, "expectedArtifactDir"); - const withSlash = normalized.endsWith("/") ? normalized : `${normalized}/`; - if (seen.has(withSlash)) { - continue; - } - seen.add(withSlash); - result.push(withSlash); - } - return result; -} -export function normalizeRequiredExtensions(value) { +function normalizeRequiredExtensions(value) { if (!Array.isArray(value)) { return []; } diff --git a/dist/src/taskState.d.ts b/dist/src/taskState.d.ts index 3ddcd14..91bbfd8 100644 --- a/dist/src/taskState.d.ts +++ b/dist/src/taskState.d.ts @@ -1,5 +1,4 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -export declare const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins"; export declare const XWORKMATE_SESSION_EXTENSION_NAMESPACE = "xworkmate.sessionMapping"; export type XWorkmateTaskMetadataV1 = { schemaVersion: 1; @@ -35,17 +34,6 @@ export declare function recordXWorkmateSessionMapping(input: { artifactScope?: string; source?: XWorkmateSessionMappingSource; }): Promise; -export declare function normalizeXWorkmateTaskMetadataV1(input: Record): XWorkmateTaskMetadataV1; -export declare function normalizeExpectedArtifactDirs(value: unknown): string[]; -export declare function upsertXWorkmateSessionMapping(api: OpenClawPluginApi, input: { - metadata: XWorkmateTaskMetadataV1; - openclawSessionKey: string; - source: XWorkmateSessionMappingSource; -}): Promise; -export declare function readXWorkmateSessionMapping(api: OpenClawPluginApi, lookup: { - appThreadKey?: string; - openclawSessionKey?: string; -}): Promise; export declare function getXWorkmateTaskSnapshot(input: { api: OpenClawPluginApi; params: Record; diff --git a/dist/src/taskState.js b/dist/src/taskState.js index 35fe992..0e96753 100644 --- a/dist/src/taskState.js +++ b/dist/src/taskState.js @@ -1,5 +1,6 @@ import { exportXWorkmateArtifacts } from "./exportArtifacts.js"; -export const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins"; +import { normalizeExpectedArtifactDirs } from "./expectedArtifactDirs.js"; +const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins"; export const XWORKMATE_SESSION_EXTENSION_NAMESPACE = "xworkmate.sessionMapping"; export function registerXWorkmateSessionExtension(api) { const registerExtension = api.session?.state?.registerSessionExtension ?? api.registerSessionExtension; @@ -28,7 +29,7 @@ export async function recordXWorkmateSessionMapping(input) { source: input.source ?? "bridge_prepare", }); } -export function normalizeXWorkmateTaskMetadataV1(input) { +function normalizeXWorkmateTaskMetadataV1(input) { const envelope = asRecord(input.xworkmate) ?? asRecord(input.xworkmateMetadata) ?? input; const schemaVersion = Number(envelope.schemaVersion ?? 1); if (schemaVersion !== 1) { @@ -46,29 +47,7 @@ export function normalizeXWorkmateTaskMetadataV1(input) { createdAt, }); } -export function normalizeExpectedArtifactDirs(value) { - if (!Array.isArray(value)) { - return []; - } - const seen = new Set(); - const result = []; - for (const entry of value) { - const text = optionalString(entry).replaceAll("\\", "/").replace(/^\.\/+/u, ""); - if (!text || seen.has(text)) { - continue; - } - if (text.startsWith("/") || /^[A-Za-z]:\//u.test(text) || text.split("/").includes("..")) { - throw new Error("expectedArtifactDirs must be relative paths without traversal"); - } - const normalized = text.endsWith("/") ? text : `${text}/`; - if (!seen.has(normalized)) { - seen.add(normalized); - result.push(normalized); - } - } - return result; -} -export async function upsertXWorkmateSessionMapping(api, input) { +async function upsertXWorkmateSessionMapping(api, input) { const patchSessionEntry = resolvePatchSessionEntry(api); if (!patchSessionEntry) { throw new Error("OpenClaw runtime session patch API is unavailable"); @@ -114,7 +93,7 @@ export async function upsertXWorkmateSessionMapping(api, input) { } return mapping; } -export async function readXWorkmateSessionMapping(api, lookup) { +async function readXWorkmateSessionMapping(api, lookup) { const getSessionEntry = resolveGetSessionEntry(api); if (!getSessionEntry) { return undefined; diff --git a/src/expectedArtifactDirs.ts b/src/expectedArtifactDirs.ts new file mode 100644 index 0000000..341636a --- /dev/null +++ b/src/expectedArtifactDirs.ts @@ -0,0 +1,35 @@ +function optionalString(value: unknown): string { + return typeof value === "string" ? value.trim() : ""; +} + +function safeExpectedArtifactDir(value: unknown): string { + const relativePath = optionalString(value); + if (!relativePath) { + return ""; + } + if (/^[A-Za-z]:[\\/]/u.test(relativePath) || relativePath.startsWith("/") || relativePath.includes("\0")) { + throw new Error("expectedArtifactDir must stay inside the workspace"); + } + const normalized = relativePath.split(/[\\/]/).filter(Boolean).join("/"); + if (!normalized || normalized.split("/").some((part) => part === ".." || part === ".")) { + throw new Error("expectedArtifactDir must stay inside the workspace"); + } + return normalized.endsWith("/") ? normalized : `${normalized}/`; +} + +export function normalizeExpectedArtifactDirs(value: unknown): string[] { + if (!Array.isArray(value)) { + return []; + } + const seen = new Set(); + const result: string[] = []; + for (const entry of value) { + const normalized = safeExpectedArtifactDir(entry); + if (!normalized || seen.has(normalized)) { + continue; + } + seen.add(normalized); + result.push(normalized); + } + return result; +} diff --git a/src/exportArtifacts.ts b/src/exportArtifacts.ts index 00d1243..c6247ef 100644 --- a/src/exportArtifacts.ts +++ b/src/exportArtifacts.ts @@ -2,6 +2,7 @@ import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypt import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { normalizeExpectedArtifactDirs } from "./expectedArtifactDirs.js"; const DEFAULT_MAX_FILES = 64; const DEFAULT_MAX_INLINE_BYTES = 10 * 1024 * 1024; @@ -20,7 +21,7 @@ const SKIPPED_DIRS = new Set([ "node_modules", ]); -export type XWorkmateArtifact = { +type XWorkmateArtifact = { relativePath: string; label: string; contentType: string; @@ -33,9 +34,9 @@ export type XWorkmateArtifact = { content?: string; }; -export type XWorkmateArtifactScopeKind = "task"; +type XWorkmateArtifactScopeKind = "task"; -export type XWorkmateArtifactExport = { +type XWorkmateArtifactExport = { runId: string; sessionKey: string; remoteWorkingDirectory: string; @@ -50,7 +51,7 @@ export type XWorkmateArtifactExport = { missingRequiredExtensions: string[]; }; -export type XWorkmateArtifactPrepare = { +type XWorkmateArtifactPrepare = { runId: string; sessionKey: string; remoteWorkingDirectory: string; @@ -64,12 +65,12 @@ export type XWorkmateArtifactPrepare = { expectedArtifactDirStatus: XWorkmateExpectedArtifactDirStatus[]; }; -export type XWorkmateExpectedArtifactDirStatus = { +type XWorkmateExpectedArtifactDirStatus = { relativePath: string; exists: boolean; }; -export type XWorkmateArtifactSnapshot = { +type XWorkmateArtifactSnapshot = { runId: string; sessionKey: string; remoteWorkingDirectory: string; @@ -491,25 +492,7 @@ export async function readXWorkmateArtifact(input: ReadInput): Promise(); - const result: string[] = []; - for (const entry of value) { - const normalized = safeInputRelativePath(entry, "expectedArtifactDir"); - const withSlash = normalized.endsWith("/") ? normalized : `${normalized}/`; - if (seen.has(withSlash)) { - continue; - } - seen.add(withSlash); - result.push(withSlash); - } - return result; -} - -export function normalizeRequiredExtensions(value: unknown): string[] { +function normalizeRequiredExtensions(value: unknown): string[] { if (!Array.isArray(value)) { return []; } diff --git a/src/taskState.test.ts b/src/taskState.test.ts index 1eddab0..f920e17 100644 --- a/src/taskState.test.ts +++ b/src/taskState.test.ts @@ -3,14 +3,13 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { - XWORKMATE_PLUGIN_ID, XWORKMATE_SESSION_EXTENSION_NAMESPACE, getXWorkmateTaskSnapshot, - normalizeXWorkmateTaskMetadataV1, recordXWorkmateSessionMapping, - readXWorkmateSessionMapping, } from "./taskState.js"; +const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins"; + function createApiFixture(tasks: Record = {}, pluginConfig: Record = {}) { const sessions = new Map(); const api = { @@ -66,14 +65,19 @@ async function createWorkspaceFixture() { } describe("xworkmate task state mapping", () => { - it("requires typed appThreadKey metadata", () => { - expect(() => - normalizeXWorkmateTaskMetadataV1({ - schemaVersion: 1, - sessionKey: "draft:legacy", - expectedArtifactDirs: ["artifacts/"], + it("requires typed appThreadKey metadata", async () => { + const { api } = createApiFixture(); + + await expect( + recordXWorkmateSessionMapping({ + api, + params: { + schemaVersion: 1, + sessionKey: "draft:legacy", + expectedArtifactDirs: ["artifacts/"], + }, }), - ).toThrow("appThreadKey required"); + ).rejects.toThrow("appThreadKey required"); }); it("writes a durable pluginExtensions mapping without deriving the OpenClaw key", async () => { @@ -273,7 +277,13 @@ describe("xworkmate task state mapping", () => { }); it("can read mapping by appThreadKey from pluginExtensions", async () => { - const { api } = createApiFixture(); + const { api } = createApiFixture({ + "draft:lookup:run-1": { + taskId: "task-1", + runId: "run-1", + status: "succeeded", + }, + }); await recordXWorkmateSessionMapping({ api, params: { @@ -284,7 +294,17 @@ describe("xworkmate task state mapping", () => { }, }); - await expect(readXWorkmateSessionMapping(api, { appThreadKey: "draft:lookup" })).resolves.toMatchObject({ + await expect( + getXWorkmateTaskSnapshot({ + api, + params: { + appThreadKey: "draft:lookup", + runId: "run-1", + includeArtifacts: false, + }, + }), + ).resolves.toMatchObject({ + success: true, appThreadKey: "draft:lookup", openclawSessionKey: "draft:lookup", }); diff --git a/src/taskState.ts b/src/taskState.ts index 4c037de..3eda9ca 100644 --- a/src/taskState.ts +++ b/src/taskState.ts @@ -1,7 +1,8 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; import { exportXWorkmateArtifacts } from "./exportArtifacts.js"; +import { normalizeExpectedArtifactDirs } from "./expectedArtifactDirs.js"; -export const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins"; +const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins"; export const XWORKMATE_SESSION_EXTENSION_NAMESPACE = "xworkmate.sessionMapping"; export type XWorkmateTaskMetadataV1 = { @@ -101,7 +102,7 @@ export async function recordXWorkmateSessionMapping(input: { }); } -export function normalizeXWorkmateTaskMetadataV1(input: Record): XWorkmateTaskMetadataV1 { +function normalizeXWorkmateTaskMetadataV1(input: Record): XWorkmateTaskMetadataV1 { const envelope = asRecord(input.xworkmate) ?? asRecord(input.xworkmateMetadata) ?? input; const schemaVersion = Number(envelope.schemaVersion ?? 1); if (schemaVersion !== 1) { @@ -120,30 +121,7 @@ export function normalizeXWorkmateTaskMetadataV1(input: Record) }) as XWorkmateTaskMetadataV1; } -export function normalizeExpectedArtifactDirs(value: unknown): string[] { - if (!Array.isArray(value)) { - return []; - } - const seen = new Set(); - const result: string[] = []; - for (const entry of value) { - const text = optionalString(entry).replaceAll("\\", "/").replace(/^\.\/+/u, ""); - if (!text || seen.has(text)) { - continue; - } - if (text.startsWith("/") || /^[A-Za-z]:\//u.test(text) || text.split("/").includes("..")) { - throw new Error("expectedArtifactDirs must be relative paths without traversal"); - } - const normalized = text.endsWith("/") ? text : `${text}/`; - if (!seen.has(normalized)) { - seen.add(normalized); - result.push(normalized); - } - } - return result; -} - -export async function upsertXWorkmateSessionMapping( +async function upsertXWorkmateSessionMapping( api: OpenClawPluginApi, input: { metadata: XWorkmateTaskMetadataV1; @@ -198,7 +176,7 @@ export async function upsertXWorkmateSessionMapping( return mapping; } -export async function readXWorkmateSessionMapping( +async function readXWorkmateSessionMapping( api: OpenClawPluginApi, lookup: { appThreadKey?: string;