@linxin666/dsh-client-ui-task-board 0.3.6 → 0.3.9

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.
Files changed (45) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +9 -4
  3. package/README.zh.md +9 -4
  4. package/lib/client.js +688 -66
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +698 -190
  7. package/lib/types/client/TaskBoardSettingsCard.d.ts +2 -1
  8. package/lib/types/client/board/TaskBoard.d.ts +3 -0
  9. package/lib/types/client/host-api.d.ts +2 -2
  10. package/lib/types/client/index.d.ts +10 -2
  11. package/lib/types/client/locales.d.ts +19 -0
  12. package/lib/types/client/settings-form.d.ts +35 -43
  13. package/lib/types/core/controller.d.ts +11 -2
  14. package/lib/types/core/freeze-snapshot.d.ts +71 -0
  15. package/lib/types/core/handover.d.ts +58 -0
  16. package/lib/types/core/tasks.d.ts +70 -1
  17. package/lib/types/core/use-cases/task-update.d.ts +16 -7
  18. package/lib/types/host-ledger.d.ts +17 -2
  19. package/lib/types/host-runner.d.ts +43 -23
  20. package/lib/types/host-service.d.ts +8 -5
  21. package/lib/types/index.d.ts +9 -1
  22. package/lib/types/protocol.d.ts +15 -2
  23. package/package.json +26 -17
  24. package/src/client/TaskBoardSettingsCard.tsx +2 -1
  25. package/src/client/board/NewTaskModal.tsx +52 -0
  26. package/src/client/board/TaskBoard.tsx +5 -3
  27. package/src/client/board/TaskCard.tsx +3 -0
  28. package/src/client/board/TaskDetail.tsx +54 -0
  29. package/src/client/host-api.ts +5 -5
  30. package/src/client/index.ts +67 -7
  31. package/src/client/locales.ts +40 -2
  32. package/src/client/settings-form.ts +78 -92
  33. package/src/core/controller.ts +35 -10
  34. package/src/core/freeze-snapshot.ts +213 -0
  35. package/src/core/handover.ts +117 -0
  36. package/src/core/store.ts +48 -1
  37. package/src/core/tasks.ts +87 -0
  38. package/src/core/use-cases/task-update.ts +30 -10
  39. package/src/dsh-home.ts +1 -1
  40. package/src/host-ledger.ts +137 -59
  41. package/src/host-routes.ts +1 -1
  42. package/src/host-runner.ts +211 -97
  43. package/src/host-service.ts +14 -10
  44. package/src/index.ts +24 -10
  45. 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
- export const TASK_BOARD_SCHEMA_VERSION = 2 as const
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
- return patch.permission === undefined || isTaskPermission(patch.permission)
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
- return typeof action.id === 'string' && action.id !== '' && createInput(action.input)
193
- ? { requestId: envelope.requestId, action: action as unknown as Extract<TaskBoardAction, { kind: 'create' }> }
194
- : undefined
195
- case 'update':
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
- return taskId !== undefined && updatePatch(action.patch)
198
- ? { requestId: envelope.requestId, action: action as unknown as Extract<TaskBoardAction, { kind: 'update' }> }
199
- : undefined
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':