From 3e173d1e5d6df4da69c6bdff22430517440a7af5 Mon Sep 17 00:00:00 2001 From: wing Date: Wed, 18 Feb 2026 15:07:36 +0800 Subject: [PATCH] fix(agents/contracts): return cloned task snapshots from shared task board --- js/agents/core/contracts/shared-task-board.js | 36 +++++++++++++------ .../core/contracts/shared-task-board.test.js | 24 +++++++++++-- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/js/agents/core/contracts/shared-task-board.js b/js/agents/core/contracts/shared-task-board.js index 69f1f506..fba3e137 100644 --- a/js/agents/core/contracts/shared-task-board.js +++ b/js/agents/core/contracts/shared-task-board.js @@ -18,6 +18,7 @@ */ import { TaskStatus } from './agent-message.js'; +import { deepClone } from '../../shared/utils/value-utils.js'; // ─── 常量 ─────────────────────────────────────────────── @@ -43,6 +44,16 @@ function createTaskId() { return `task_${Date.now().toString(36)}_${(++_counter).toString(36)}`; } +/** @param {BoardTask} task @returns {BoardTask} */ +function cloneTask(task) { + return deepClone(task); +} + +/** @param {BoardTask[]} tasks @returns {BoardTask[]} */ +function cloneTasks(tasks) { + return tasks.map((task) => cloneTask(task)); +} + // ─── 类型定义 ─────────────────────────────────────────── /** @@ -112,7 +123,7 @@ export class SharedTaskBoard { this._tasks.set(task.id, task); this._emit(TaskBoardEvents.TASK_ADDED, { task }); - return { ok: true, task }; + return { ok: true, task: cloneTask(task) }; } /** @@ -136,7 +147,7 @@ export class SharedTaskBoard { task.claimedAt = Date.now(); this._emit(TaskBoardEvents.TASK_CLAIMED, { task }); - return { ok: true, task }; + return { ok: true, task: cloneTask(task) }; } /** @@ -159,7 +170,7 @@ export class SharedTaskBoard { task.completedAt = Date.now(); this._emit(TaskBoardEvents.TASK_COMPLETED, { task }); - return { ok: true, task }; + return { ok: true, task: cloneTask(task) }; } /** @@ -182,7 +193,7 @@ export class SharedTaskBoard { task.completedAt = Date.now(); this._emit(TaskBoardEvents.TASK_FAILED, { task }); - return { ok: true, task }; + return { ok: true, task: cloneTask(task) }; } /** @@ -207,7 +218,7 @@ export class SharedTaskBoard { task.completedAt = Date.now(); this._emit(TaskBoardEvents.TASK_CANCELLED, { task }); - return { ok: true, task }; + return { ok: true, task: cloneTask(task) }; } /** @@ -219,7 +230,8 @@ export class SharedTaskBoard { for (const task of this._tasks.values()) { if (task.status === 'pending') pending.push(task); } - return pending.sort((a, b) => a.priority - b.priority || a.createdAt - b.createdAt); + pending.sort((a, b) => a.priority - b.priority || a.createdAt - b.createdAt); + return cloneTasks(pending); } /** @@ -234,7 +246,7 @@ export class SharedTaskBoard { for (const task of this._tasks.values()) { if (task.claimedBy === agent) result.push(task); } - return result; + return cloneTasks(result); } /** @@ -243,7 +255,8 @@ export class SharedTaskBoard { * @returns {BoardTask | null} */ getTask(taskId) { - return this._tasks.get(taskId) ?? null; + const task = this._tasks.get(taskId); + return task ? cloneTask(task) : null; } /** @returns {number} */ @@ -256,7 +269,7 @@ export class SharedTaskBoard { getSnapshot() { return { boardId: this.boardId, - tasks: Array.from(this._tasks.values()), + tasks: cloneTasks(Array.from(this._tasks.values())), ts: Date.now(), }; } @@ -274,7 +287,7 @@ export class SharedTaskBoard { let count = 0; for (const task of snapshot.tasks) { if (task && typeof task === 'object' && str(task.id)) { - this._tasks.set(task.id, { ...task }); + this._tasks.set(task.id, cloneTask(task)); count++; } } @@ -289,10 +302,11 @@ export class SharedTaskBoard { _emit(eventName, payload) { if (!this._eventBus || typeof this._eventBus.emit !== 'function') return; try { + const safePayload = deepClone(payload); this._eventBus.emit(eventName, { actor: 'taskboard', status: 'info', - payload: { ...payload, boardId: this.boardId }, + payload: { ...safePayload, boardId: this.boardId }, }); } catch { // best-effort diff --git a/tests/unit/agents/core/contracts/shared-task-board.test.js b/tests/unit/agents/core/contracts/shared-task-board.test.js index d318a8d9..ef54fb00 100644 --- a/tests/unit/agents/core/contracts/shared-task-board.test.js +++ b/tests/unit/agents/core/contracts/shared-task-board.test.js @@ -201,14 +201,30 @@ describe('SharedTaskBoard', () => { }); describe('getTask', () => { - it('returns task by id', () => { + it('returns cloned task by id', () => { const { task } = board.addTask({ taskType: 'a:b', createdBy: 'x' }); - expect(board.getTask(task.id)).toBe(task); + const stored = board.getTask(task.id); + expect(stored).toEqual(task); + expect(stored).not.toBe(task); }); it('returns null for unknown id', () => { expect(board.getTask('nope')).toBeNull(); }); + + it('does not allow external mutation through returned task objects', () => { + const { task } = board.addTask({ + taskType: 'a:b', + createdBy: 'x', + payload: { nested: { value: 1 } }, + }); + + const view = board.getTask(task.id); + view.payload.nested.value = 999; + + const fresh = board.getTask(task.id); + expect(fresh.payload.nested.value).toBe(1); + }); }); describe('snapshot and restore', () => { @@ -221,6 +237,10 @@ describe('SharedTaskBoard', () => { expect(snapshot.tasks).toHaveLength(2); expect(snapshot.ts).toBeGreaterThan(0); + snapshot.tasks[0].status = 'failed'; + const fresh = board.getTask(snapshot.tasks[0].id); + expect(fresh.status).toBe('pending'); + const board2 = new SharedTaskBoard(); const result = board2.restore(snapshot); expect(result.ok).toBe(true);