@linxin666/dsh-client-ui-task-board 0.3.6 → 0.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +9 -4
- package/README.zh.md +9 -4
- package/lib/client.js +742 -75
- package/lib/client.js.map +1 -1
- package/lib/index.js +698 -190
- package/lib/types/client/TaskBoardSettingsCard.d.ts +2 -1
- package/lib/types/client/board/TaskBoard.d.ts +3 -0
- package/lib/types/client/board-mount.d.ts +4 -1
- package/lib/types/client/host-api.d.ts +2 -2
- package/lib/types/client/index.d.ts +10 -2
- package/lib/types/client/locales.d.ts +21 -0
- package/lib/types/client/settings-form.d.ts +35 -43
- package/lib/types/client/sidebar-entry-core.d.ts +10 -0
- package/lib/types/client/sidebar-entry.d.ts +7 -1
- package/lib/types/core/controller.d.ts +11 -2
- package/lib/types/core/freeze-snapshot.d.ts +71 -0
- package/lib/types/core/handover.d.ts +58 -0
- package/lib/types/core/tasks.d.ts +70 -1
- package/lib/types/core/use-cases/task-update.d.ts +16 -7
- package/lib/types/host-ledger.d.ts +17 -2
- package/lib/types/host-runner.d.ts +43 -23
- package/lib/types/host-service.d.ts +8 -5
- package/lib/types/index.d.ts +9 -1
- package/lib/types/protocol.d.ts +15 -2
- package/package.json +26 -17
- package/src/client/TaskBoardSettingsCard.tsx +2 -1
- package/src/client/board/NewTaskModal.tsx +52 -0
- package/src/client/board/TaskBoard.tsx +5 -3
- package/src/client/board/TaskCard.tsx +3 -0
- package/src/client/board/TaskDetail.tsx +54 -0
- package/src/client/board-mount.tsx +11 -1
- package/src/client/host-api.ts +5 -5
- package/src/client/index.ts +76 -10
- package/src/client/locales.ts +55 -2
- package/src/client/settings-form.ts +78 -92
- package/src/client/sidebar-entry-core.ts +33 -7
- package/src/client/sidebar-entry.ts +7 -1
- package/src/core/controller.ts +35 -10
- package/src/core/freeze-snapshot.ts +213 -0
- package/src/core/handover.ts +117 -0
- package/src/core/store.ts +48 -1
- package/src/core/tasks.ts +87 -0
- package/src/core/use-cases/task-update.ts +30 -10
- package/src/dsh-home.ts +1 -1
- package/src/host-ledger.ts +137 -59
- package/src/host-routes.ts +1 -1
- package/src/host-runner.ts +211 -97
- package/src/host-service.ts +14 -10
- package/src/index.ts +24 -10
- package/src/protocol.ts +98 -14
package/src/protocol.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import type { TaskUpdatePatch } from './core/use-cases/task-update.ts'
|
|
2
|
-
import { isTaskPermission, isTaskStatus, type NewTaskInput, type TaskRecord, type TaskStatus } from './core/tasks.ts'
|
|
2
|
+
import { isTaskPermission, isTaskStatus, type NewTaskInput, type TaskPermission, type TaskRecord, type TaskStatus } from './core/tasks.ts'
|
|
3
3
|
import { parseLedger } from './core/store.ts'
|
|
4
|
+
import { sanitizeFreezeSnapshot, type FreezeSnapshot } from './core/freeze-snapshot.ts'
|
|
5
|
+
import { sanitizeHandover, type TaskHandoverInput } from './core/handover.ts'
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
/** Freeze payload carried by create/update actions after the gate (redacted in place). */
|
|
8
|
+
type FreezePayload = FreezeSnapshot & { redacted?: boolean; frozenBy?: string }
|
|
9
|
+
|
|
10
|
+
export const TASK_BOARD_SCHEMA_VERSION = 3 as const
|
|
11
|
+
/** Ledger documents written before v3; loaded once and migrated on startup. */
|
|
12
|
+
export const TASK_BOARD_LEGACY_SCHEMA_VERSION = 2 as const
|
|
6
13
|
export const TASK_BOARD_API_PREFIX = '/api/task-board'
|
|
7
14
|
|
|
8
15
|
export type PowerPhase = 'disabled' | 'idle' | 'acquiring' | 'active' | 'error' | 'unsupported'
|
|
@@ -31,6 +38,8 @@ export interface TaskBoardSnapshot {
|
|
|
31
38
|
tasks: TaskRecord[]
|
|
32
39
|
scheduler: TaskBoardSchedulerSnapshot
|
|
33
40
|
power: TaskBoardPowerSnapshot
|
|
41
|
+
/** Session-default permission the confirmation gate compares against. */
|
|
42
|
+
sessionDefaultPermission?: TaskPermission
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
/** SSE event frame: revision/scheduler/power only, never the task list. */
|
|
@@ -51,10 +60,17 @@ export type TaskBoardAction =
|
|
|
51
60
|
| { kind: 'set-schedule'; taskId: string; patch: { enabled?: boolean; cron?: string } }
|
|
52
61
|
| { kind: 'run'; taskId: string }
|
|
53
62
|
| { kind: 'rerun'; taskId: string }
|
|
63
|
+
| { kind: 'confirm-permission'; taskId: string }
|
|
54
64
|
|
|
55
65
|
export interface TaskBoardActionEnvelope {
|
|
56
66
|
requestId: string
|
|
57
67
|
action: TaskBoardAction
|
|
68
|
+
/**
|
|
69
|
+
* Session id of the DSH session issuing the action, for the execution audit
|
|
70
|
+
* trail (issue #6). Client-asserted, not a trust boundary; parsed only as a
|
|
71
|
+
* bounded non-empty string and recorded on opened executions / freeze stamps.
|
|
72
|
+
*/
|
|
73
|
+
initiator?: string
|
|
58
74
|
}
|
|
59
75
|
|
|
60
76
|
function record(value: unknown): Record<string, unknown> | undefined {
|
|
@@ -98,6 +114,9 @@ function validImportedKnownFields(value: Record<string, unknown>): boolean {
|
|
|
98
114
|
if (typeof execution.startedAt !== 'number' || !Number.isFinite(execution.startedAt)) return false
|
|
99
115
|
if (!optionalFiniteNumber(execution.endedAt) || !optionalString(execution.error)) return false
|
|
100
116
|
if (execution.result !== undefined && !['succeeded', 'failed', 'cancelled'].includes(String(execution.result))) return false
|
|
117
|
+
if (execution.initiatedBy !== undefined && typeof execution.initiatedBy !== 'string') return false
|
|
118
|
+
if (execution.frozenBy !== undefined && typeof execution.frozenBy !== 'string') return false
|
|
119
|
+
if (execution.frozenAt !== undefined && typeof execution.frozenAt !== 'number') return false
|
|
101
120
|
}
|
|
102
121
|
}
|
|
103
122
|
return true
|
|
@@ -123,6 +142,9 @@ function importedTask(value: unknown): TaskRecord | undefined {
|
|
|
123
142
|
endedAt: execution.endedAt,
|
|
124
143
|
result: execution.result,
|
|
125
144
|
error: execution.error,
|
|
145
|
+
...(execution.initiatedBy === undefined ? {} : { initiatedBy: execution.initiatedBy }),
|
|
146
|
+
...(execution.frozenAt === undefined ? {} : { frozenAt: execution.frozenAt }),
|
|
147
|
+
...(execution.frozenBy === undefined ? {} : { frozenBy: execution.frozenBy }),
|
|
126
148
|
})),
|
|
127
149
|
...(task.schedule === undefined ? {} : {
|
|
128
150
|
schedule: {
|
|
@@ -136,15 +158,47 @@ function importedTask(value: unknown): TaskRecord | undefined {
|
|
|
136
158
|
...(task.mode === undefined ? {} : { mode: task.mode }),
|
|
137
159
|
...(task.permission === undefined ? {} : { permission: task.permission }),
|
|
138
160
|
...(task.archivedAt === undefined ? {} : { archivedAt: task.archivedAt }),
|
|
161
|
+
...(task.freeze === undefined ? {} : { freeze: task.freeze }),
|
|
162
|
+
...(task.handover === undefined ? {} : { handover: task.handover }),
|
|
163
|
+
// 安全门(对抗场景 b):import 不是人工确认动作,确认戳一律剥除——
|
|
164
|
+
// 高于会话默认权限的绑定经 import 进入后必须重新武装 confirm-permission 门。
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Gate a freeze payload from the wire: same T2 security gates as the parser
|
|
170
|
+
* (slash-command taint rejects, sensitive patterns redact in place, 8 KiB
|
|
171
|
+
* per-field cap). Returns the sanitized payload, or undefined when rejected.
|
|
172
|
+
*/
|
|
173
|
+
function freezePayload(value: unknown): FreezePayload | undefined {
|
|
174
|
+
const result = sanitizeFreezeSnapshot(value, ['redacted', 'frozenBy'])
|
|
175
|
+
if (!result.ok) return undefined
|
|
176
|
+
const frozenBy = result.extras.frozenBy
|
|
177
|
+
if (frozenBy !== undefined && (typeof frozenBy !== 'string' || frozenBy === '')) return undefined
|
|
178
|
+
return {
|
|
179
|
+
...result.snapshot,
|
|
180
|
+
...(result.redacted || result.extras.redacted === true ? { redacted: true } : {}),
|
|
181
|
+
...(frozenBy === undefined ? {} : { frozenBy }),
|
|
139
182
|
}
|
|
140
183
|
}
|
|
141
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Gate a handover bundle from the wire: exact keys, bounded string targets,
|
|
187
|
+
* a known permission, and a bounded references list. Returns the sanitized
|
|
188
|
+
* bundle, or undefined when rejected.
|
|
189
|
+
*/
|
|
190
|
+
function handoverPayload(value: unknown): TaskHandoverInput | undefined {
|
|
191
|
+
return sanitizeHandover(value)
|
|
192
|
+
}
|
|
193
|
+
|
|
142
194
|
function createInput(value: unknown): value is NewTaskInput {
|
|
143
195
|
const input = record(value)
|
|
144
|
-
if (input === undefined || !exactKeys(input, ['title', 'description', 'prompt', 'workspaceId', 'mode', 'permission', 'schedule'])) return false
|
|
196
|
+
if (input === undefined || !exactKeys(input, ['title', 'description', 'prompt', 'workspaceId', 'mode', 'permission', 'schedule', 'freeze', 'handover'])) return false
|
|
145
197
|
if (typeof input.title !== 'string' || typeof input.description !== 'string' || typeof input.prompt !== 'string') return false
|
|
146
198
|
if (!optionalString(input.workspaceId) || !optionalString(input.mode)) return false
|
|
147
199
|
if (input.permission !== undefined && !isTaskPermission(input.permission)) return false
|
|
200
|
+
if (input.freeze !== undefined && freezePayload(input.freeze) === undefined) return false
|
|
201
|
+
if (input.handover !== undefined && handoverPayload(input.handover) === undefined) return false
|
|
148
202
|
if (input.schedule !== undefined) {
|
|
149
203
|
const schedule = record(input.schedule)
|
|
150
204
|
if (schedule === undefined || !exactKeys(schedule, ['enabled', 'cron'])) return false
|
|
@@ -155,11 +209,15 @@ function createInput(value: unknown): value is NewTaskInput {
|
|
|
155
209
|
|
|
156
210
|
function updatePatch(value: unknown): boolean {
|
|
157
211
|
const patch = record(value)
|
|
158
|
-
if (patch === undefined || !exactKeys(patch, ['title', 'description', 'prompt', 'workspaceId', 'mode', 'permission'])) return false
|
|
212
|
+
if (patch === undefined || !exactKeys(patch, ['title', 'description', 'prompt', 'workspaceId', 'mode', 'permission', 'freeze', 'handover'])) return false
|
|
159
213
|
for (const key of ['title', 'description', 'prompt', 'workspaceId', 'mode'] as const) {
|
|
160
214
|
if (!optionalString(patch[key])) return false
|
|
161
215
|
}
|
|
162
|
-
|
|
216
|
+
if (patch.permission !== undefined && !isTaskPermission(patch.permission)) return false
|
|
217
|
+
// null clears the snapshot; an object must pass the freeze gate.
|
|
218
|
+
if (patch.freeze !== undefined && patch.freeze !== null && freezePayload(patch.freeze) === undefined) return false
|
|
219
|
+
// Same convention for the handover bundle.
|
|
220
|
+
return patch.handover === undefined || patch.handover === null || handoverPayload(patch.handover) !== undefined
|
|
163
221
|
}
|
|
164
222
|
|
|
165
223
|
function schedulePatch(value: unknown): boolean {
|
|
@@ -171,9 +229,24 @@ function schedulePatch(value: unknown): boolean {
|
|
|
171
229
|
}
|
|
172
230
|
|
|
173
231
|
export function parseActionEnvelope(value: unknown): TaskBoardActionEnvelope | undefined {
|
|
232
|
+
const parsed = parseEnvelopeAction(value)
|
|
233
|
+
if (parsed === undefined) return undefined
|
|
234
|
+
// Attach the audit-only initiator stamp (already validated above) to every accepted action.
|
|
235
|
+
const initiator = initiatorOf(value)
|
|
236
|
+
return initiator === undefined ? parsed : { ...parsed, initiator }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function initiatorOf(value: unknown): string | undefined {
|
|
240
|
+
const envelope = record(value)
|
|
241
|
+
const id = envelope?.initiator
|
|
242
|
+
return typeof id === 'string' && id.trim() !== '' && id.length <= 256 ? id : undefined
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function parseEnvelopeAction(value: unknown): TaskBoardActionEnvelope | undefined {
|
|
174
246
|
const envelope = record(value)
|
|
175
|
-
if (envelope === undefined || !exactKeys(envelope, ['requestId', 'action'])) return undefined
|
|
247
|
+
if (envelope === undefined || !exactKeys(envelope, ['requestId', 'action', 'initiator'])) return undefined
|
|
176
248
|
if (typeof envelope.requestId !== 'string' || envelope.requestId.trim() === '' || envelope.requestId.length > 256) return undefined
|
|
249
|
+
if (envelope.initiator !== undefined && initiatorOf(value) === undefined) return undefined
|
|
177
250
|
const action = record(envelope.action)
|
|
178
251
|
if (action === undefined || typeof action.kind !== 'string') return undefined
|
|
179
252
|
const taskId = typeof action.taskId === 'string' && action.taskId !== '' ? action.taskId : undefined
|
|
@@ -187,16 +260,26 @@ export function parseActionEnvelope(value: unknown): TaskBoardActionEnvelope | u
|
|
|
187
260
|
? { requestId: envelope.requestId, action: { kind: 'import', sourceId: action.sourceId, tasks } }
|
|
188
261
|
: undefined
|
|
189
262
|
}
|
|
190
|
-
case 'create':
|
|
263
|
+
case 'create': {
|
|
191
264
|
if (!exactKeys(action, ['kind', 'id', 'input'])) return undefined
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
265
|
+
if (typeof action.id !== 'string' || action.id === '' || !createInput(action.input)) return undefined
|
|
266
|
+
const input = action.input as NewTaskInput
|
|
267
|
+
const freeze = input.freeze === undefined ? undefined : freezePayload(input.freeze)
|
|
268
|
+
const handover = input.handover === undefined ? undefined : handoverPayload(input.handover)
|
|
269
|
+
const sanitized = freeze === undefined && handover === undefined ? input : { ...input, ...(freeze === undefined ? {} : { freeze }), ...(handover === undefined ? {} : { handover }) }
|
|
270
|
+
return { requestId: envelope.requestId, action: { kind: 'create', id: action.id as string, input: sanitized } }
|
|
271
|
+
}
|
|
272
|
+
case 'update': {
|
|
196
273
|
if (!exactKeys(action, ['kind', 'taskId', 'patch'])) return undefined
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
274
|
+
if (taskId === undefined || !updatePatch(action.patch)) return undefined
|
|
275
|
+
const patch = action.patch as TaskUpdatePatch
|
|
276
|
+
const freeze = patch.freeze === undefined || patch.freeze === null ? patch.freeze : freezePayload(patch.freeze)
|
|
277
|
+
const handover = patch.handover === undefined || patch.handover === null ? patch.handover : handoverPayload(patch.handover)
|
|
278
|
+
const sanitized = ('freeze' in patch && freeze !== patch.freeze) || ('handover' in patch && handover !== patch.handover)
|
|
279
|
+
? { ...patch, ...(freeze === patch.freeze ? {} : { freeze }), ...(handover === patch.handover ? {} : { handover }) }
|
|
280
|
+
: patch
|
|
281
|
+
return { requestId: envelope.requestId, action: { kind: 'update', taskId, patch: sanitized } }
|
|
282
|
+
}
|
|
200
283
|
case 'set-schedule':
|
|
201
284
|
if (!exactKeys(action, ['kind', 'taskId', 'patch'])) return undefined
|
|
202
285
|
return taskId !== undefined && schedulePatch(action.patch)
|
|
@@ -207,6 +290,7 @@ export function parseActionEnvelope(value: unknown): TaskBoardActionEnvelope | u
|
|
|
207
290
|
return taskId !== undefined && isTaskStatus(action.status)
|
|
208
291
|
? { requestId: envelope.requestId, action: action as unknown as Extract<TaskBoardAction, { kind: 'move' }> }
|
|
209
292
|
: undefined
|
|
293
|
+
case 'confirm-permission':
|
|
210
294
|
case 'delete':
|
|
211
295
|
case 'archive':
|
|
212
296
|
case 'restore':
|