@linxin666/dsh-client-ui-task-board 0.4.4 → 0.4.6
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 +59 -20
- package/README.zh.md +59 -20
- package/lib/client.js +6208 -624
- package/lib/client.js.map +1 -1
- package/lib/index.js +4774 -493
- package/lib/types/client/PluginSettingsCard.d.ts +7 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +31 -3
- package/lib/types/client/board/ClampedMarkdown.d.ts +3 -0
- package/lib/types/client/board/ConfirmDialog.d.ts +4 -1
- package/lib/types/client/board/EditTaskModal.d.ts +5 -2
- package/lib/types/client/board/LinkSubtaskModal.d.ts +5 -2
- package/lib/types/client/board/NewTaskModal.d.ts +4 -1
- package/lib/types/client/board/TagManagerModal.d.ts +26 -0
- package/lib/types/client/board/TaskBoard.d.ts +10 -1
- package/lib/types/client/board/TaskCard.d.ts +14 -1
- package/lib/types/client/board/TaskDetail.d.ts +3 -1
- package/lib/types/client/board/TaskForm.d.ts +4 -1
- package/lib/types/client/board/VerificationReport.d.ts +23 -0
- package/lib/types/client/board/card-view.d.ts +113 -0
- package/lib/types/client/board/icons.d.ts +32 -0
- package/lib/types/client/board/overlay.d.ts +69 -0
- package/lib/types/client/board/status-key.d.ts +15 -0
- package/lib/types/client/board/task-markdown.d.ts +19 -0
- package/lib/types/client/index.d.ts +23 -5
- package/lib/types/client/locales.d.ts +168 -1
- package/lib/types/client/native-panel.d.ts +6 -20
- package/lib/types/client/plugin-card-seat.d.ts +5 -0
- package/lib/types/client/schedule-zone.d.ts +50 -0
- package/lib/types/client/seats.d.ts +18 -0
- package/lib/types/client/service.d.ts +32 -0
- package/lib/types/client/settings-form.d.ts +16 -0
- package/lib/types/core/controller.d.ts +46 -11
- package/lib/types/core/extension.d.ts +283 -0
- package/lib/types/core/handover.d.ts +13 -1
- package/lib/types/core/issue-form.d.ts +16 -0
- package/lib/types/core/poll-cadence.d.ts +31 -0
- package/lib/types/core/tasks.d.ts +228 -17
- package/lib/types/core/use-cases/task-schedule.d.ts +68 -10
- package/lib/types/core/use-cases/task-tag.d.ts +48 -0
- package/lib/types/core/use-cases/task-update.d.ts +5 -3
- package/lib/types/core/verification-budget.d.ts +52 -0
- package/lib/types/core/verification.d.ts +753 -0
- package/lib/types/core/workspace-target.d.ts +41 -0
- package/lib/types/host/agent-tools.d.ts +1 -1
- package/lib/types/host/extension-registry.d.ts +111 -0
- package/lib/types/host/llm-dispatch.d.ts +63 -0
- package/lib/types/host/verification-gate.d.ts +95 -0
- package/lib/types/host/verification-runner.d.ts +139 -0
- package/lib/types/host/workspace-evidence.d.ts +53 -0
- package/lib/types/host-ledger.d.ts +133 -14
- package/lib/types/host-runner.d.ts +55 -10
- package/lib/types/host-service.d.ts +205 -11
- package/lib/types/index.d.ts +134 -26
- package/lib/types/protocol.d.ts +92 -10
- package/lib/types/tool-surface.d.ts +71 -0
- package/package.json +28 -31
- package/src/client/PluginSettingsCard.tsx +9 -0
- package/src/client/TaskBoardSettingsCard.tsx +378 -66
- package/src/client/board/ClampedMarkdown.tsx +69 -0
- package/src/client/board/ConfirmDialog.tsx +7 -3
- package/src/client/board/EditTaskModal.tsx +5 -2
- package/src/client/board/LinkSubtaskModal.tsx +12 -7
- package/src/client/board/NewTaskModal.tsx +168 -21
- package/src/client/board/TagManagerModal.tsx +235 -0
- package/src/client/board/TaskBoard.tsx +259 -103
- package/src/client/board/TaskCard.tsx +214 -81
- package/src/client/board/TaskDetail.tsx +289 -43
- package/src/client/board/TaskForm.tsx +13 -3
- package/src/client/board/VerificationReport.tsx +322 -0
- package/src/client/board/card-view.ts +220 -0
- package/src/client/board/icons.tsx +89 -0
- package/src/client/board/overlay.tsx +189 -0
- package/src/client/board/status-key.ts +30 -0
- package/src/client/board/task-markdown.module.css +137 -0
- package/src/client/board/task-markdown.tsx +106 -0
- package/src/client/board-settings.module.css +66 -0
- package/src/client/board.module.css +488 -63
- package/src/client/index.ts +82 -51
- package/src/client/locales.ts +343 -6
- package/src/client/native-panel.tsx +18 -2
- package/src/client/plugin-card-seat.ts +7 -0
- package/src/client/schedule-zone.ts +100 -2
- package/src/client/seats.tsx +58 -0
- package/src/client/service.ts +94 -0
- package/src/client/settings-card.module.css +31 -5
- package/src/client/settings-form.ts +27 -1
- package/src/core/controller.ts +105 -10
- package/src/core/extension.ts +376 -0
- package/src/core/freeze-snapshot.ts +2 -2
- package/src/core/handover.ts +16 -1
- package/src/core/issue-form.ts +19 -0
- package/src/core/poll-cadence.ts +41 -0
- package/src/core/store.ts +80 -15
- package/src/core/tasks.ts +293 -45
- package/src/core/use-cases/task-create.ts +30 -9
- package/src/core/use-cases/task-parent.ts +3 -2
- package/src/core/use-cases/task-schedule.ts +190 -25
- package/src/core/use-cases/task-tag.ts +110 -0
- package/src/core/use-cases/task-update.ts +13 -7
- package/src/core/verification-budget.ts +115 -0
- package/src/core/verification.ts +1594 -0
- package/src/core/workspace-target.ts +59 -0
- package/src/host/agent-tools.ts +125 -38
- package/src/host/extension-registry.ts +509 -0
- package/src/host/llm-dispatch.ts +87 -0
- package/src/host/verification-gate.ts +487 -0
- package/src/host/verification-runner.ts +702 -0
- package/src/host/workspace-evidence.ts +176 -0
- package/src/host-ai.ts +15 -9
- package/src/host-ledger.ts +468 -71
- package/src/host-routes.ts +15 -2
- package/src/host-runner.ts +106 -24
- package/src/host-service.ts +546 -40
- package/src/index.ts +305 -21
- package/src/protocol.ts +226 -28
- package/src/tool-surface.ts +95 -0
- package/lib/invariant.js +0 -6
- package/lib/types/invariant.d.ts +0 -3
- package/src/invariant.ts +0 -4
package/src/protocol.ts
CHANGED
|
@@ -1,20 +1,52 @@
|
|
|
1
1
|
import type { TaskUpdatePatch } from './core/use-cases/task-update.ts'
|
|
2
|
-
import
|
|
2
|
+
import type { SetSchedulePatch } from './core/use-cases/task-schedule.ts'
|
|
3
|
+
import { SCHEDULE_STOP_REASONS, TAG_NAME_MAX_LENGTH, isTaskPermission, isTaskStatus, isTaskTagList, type NewTaskInput, type ScheduleStopReason, type TaskPermission, type TaskRecord, type TaskStatus } from './core/tasks.ts'
|
|
3
4
|
import { parseLedger } from './core/store.ts'
|
|
4
5
|
import { isValidTimeZone } from './core/schedule.ts'
|
|
5
6
|
import { sanitizeFreezeSnapshot, type FreezeSnapshot } from './core/freeze-snapshot.ts'
|
|
6
7
|
import { sanitizeHandover, type TaskHandoverInput } from './core/handover.ts'
|
|
8
|
+
import { isTaskBoardExtensionPayload, normalizeTaskIntegrations, type TaskBoardExtensionPayload } from './core/extension.ts'
|
|
9
|
+
import { normalizeVerification, type ModelCatalogView, type VerificationContract, type VerificationSettings } from './core/verification.ts'
|
|
7
10
|
|
|
8
11
|
/** Freeze payload carried by create/update actions after the gate (redacted in place). */
|
|
9
12
|
type FreezePayload = FreezeSnapshot & { redacted?: boolean; frozenBy?: string }
|
|
10
13
|
|
|
11
|
-
export const TASK_BOARD_SCHEMA_VERSION =
|
|
12
|
-
/** Ledger documents written before
|
|
13
|
-
export const TASK_BOARD_LEGACY_SCHEMA_VERSION =
|
|
14
|
-
/** Ledger documents written before
|
|
15
|
-
export const
|
|
14
|
+
export const TASK_BOARD_SCHEMA_VERSION = 6 as const
|
|
15
|
+
/** Ledger documents written before v6; loaded once and migrated on startup. */
|
|
16
|
+
export const TASK_BOARD_LEGACY_SCHEMA_VERSION = 5 as const
|
|
17
|
+
/** Ledger documents written before v5; migrated through the v5 normalization too. */
|
|
18
|
+
export const TASK_BOARD_PRE_ACCEPTANCE_SCHEMA_VERSION = 4 as const
|
|
19
|
+
/** Ledger documents written before v4; migrated through the v4 normalization too. */
|
|
20
|
+
export const TASK_BOARD_OLDER_SCHEMA_VERSION = 3 as const
|
|
21
|
+
/** Ledger documents written before v3; migrated through every later normalization too. */
|
|
22
|
+
export const TASK_BOARD_OLDEST_SCHEMA_VERSION = 2 as const
|
|
23
|
+
/**
|
|
24
|
+
* The last generation whose rules still need the Host zone stamped on them at
|
|
25
|
+
* migration: v4 introduced the stored zone but a rule written without one must
|
|
26
|
+
* stop following a later TZ change. A v5 rule already carries its zone (or
|
|
27
|
+
* deliberately follows the Host), so it is NOT stamped again.
|
|
28
|
+
*/
|
|
29
|
+
export const TASK_BOARD_ZONE_STAMP_SCHEMA_VERSION = 4 as const
|
|
30
|
+
/**
|
|
31
|
+
* Every older generation the loader migrates in place. v5 only added the
|
|
32
|
+
* per-execution acceptance block and v6 only added the scheduling mode and run
|
|
33
|
+
* budget, so the migration is the normalization pass that re-validates each
|
|
34
|
+
* row; a document from any listed generation upgrades losslessly.
|
|
35
|
+
*/
|
|
36
|
+
export const TASK_BOARD_MIGRATABLE_SCHEMA_VERSIONS: readonly number[] = [
|
|
37
|
+
TASK_BOARD_LEGACY_SCHEMA_VERSION,
|
|
38
|
+
TASK_BOARD_PRE_ACCEPTANCE_SCHEMA_VERSION,
|
|
39
|
+
TASK_BOARD_OLDER_SCHEMA_VERSION,
|
|
40
|
+
TASK_BOARD_OLDEST_SCHEMA_VERSION,
|
|
41
|
+
]
|
|
16
42
|
export const TASK_BOARD_API_PREFIX = '/api/task-board'
|
|
17
43
|
|
|
44
|
+
/** Longest accepted caller identity on an external outcome record. */
|
|
45
|
+
export const EXTERNAL_INITIATOR_MAX_LENGTH = 200
|
|
46
|
+
|
|
47
|
+
/** Longest accepted free-text summary on an external outcome record. */
|
|
48
|
+
export const EXTERNAL_SUMMARY_MAX_LENGTH = 1_000
|
|
49
|
+
|
|
18
50
|
export type PowerPhase = 'disabled' | 'idle' | 'acquiring' | 'active' | 'error' | 'unsupported'
|
|
19
51
|
|
|
20
52
|
export interface TaskBoardPowerSnapshot {
|
|
@@ -53,6 +85,33 @@ export interface TaskBoardSnapshot {
|
|
|
53
85
|
* into team execution (Team Lead session plus one teammate per subtask).
|
|
54
86
|
*/
|
|
55
87
|
teamRunAvailable?: boolean
|
|
88
|
+
/**
|
|
89
|
+
* Live GLOBAL native-/goal switch. The browser uses it to disable the
|
|
90
|
+
* task-level option (and explain that the global switch must be turned on
|
|
91
|
+
* first) WITHOUT rewriting anyone's stored per-task preference; the Host
|
|
92
|
+
* freezes its own read per execution, so this mirror never governs a run in
|
|
93
|
+
* flight.
|
|
94
|
+
*/
|
|
95
|
+
goalRunEnabled?: boolean
|
|
96
|
+
/**
|
|
97
|
+
* Read-only summaries published by running extensions, keyed by extension id.
|
|
98
|
+
* The board forwards them verbatim; it never interprets their shape.
|
|
99
|
+
*/
|
|
100
|
+
extensions?: Record<string, unknown>
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Body of `GET {TASK_BOARD_API_PREFIX}/verification`: the acceptance settings the
|
|
105
|
+
* card edits, the contract they resolve to right now, and the host model catalog
|
|
106
|
+
* the model and reasoning choices come from. Served through the same loopback /
|
|
107
|
+
* authenticated-proxy guard as the rest of the board API, and it carries no
|
|
108
|
+
* credential material — model routes and level ids only.
|
|
109
|
+
*/
|
|
110
|
+
export interface TaskBoardVerificationOptions {
|
|
111
|
+
settings: VerificationSettings
|
|
112
|
+
/** The RESOLVED contract, so the card can show what the next execution freezes. */
|
|
113
|
+
contract: VerificationContract
|
|
114
|
+
catalog: ModelCatalogView
|
|
56
115
|
}
|
|
57
116
|
|
|
58
117
|
/** SSE event frame: revision/scheduler/power only, never the task list. */
|
|
@@ -105,11 +164,26 @@ export type TaskBoardAction =
|
|
|
105
164
|
| { kind: 'archive'; taskId: string }
|
|
106
165
|
| { kind: 'restore'; taskId: string }
|
|
107
166
|
| { kind: 'settle'; taskId: string }
|
|
108
|
-
|
|
167
|
+
/** Clear the acceptance anomalies recorded on a card's open execution (issue #1828). */
|
|
168
|
+
| { kind: 'reset-verification'; taskId: string }
|
|
169
|
+
/**
|
|
170
|
+
* Record an outcome an agent OUTSIDE the Host produced (issue #1826).
|
|
171
|
+
*
|
|
172
|
+
* It writes a real execution record, so the terminal column and the run
|
|
173
|
+
* history both derive from that one record instead of a bare column edit.
|
|
174
|
+
* `cancelled` is deliberately absent: nothing outside a DSH session may
|
|
175
|
+
* claim the Host closed a run, and `running` must stay Host-only.
|
|
176
|
+
*/
|
|
177
|
+
| { kind: 'record-external-outcome'; taskId: string; result: 'succeeded' | 'failed'; initiatedBy: string; summary?: string }
|
|
178
|
+
| { kind: 'set-schedule'; taskId: string; patch: SetSchedulePatch }
|
|
109
179
|
| { kind: 'run'; taskId: string }
|
|
110
180
|
| { kind: 'rerun'; taskId: string }
|
|
111
181
|
| { kind: 'confirm-permission'; taskId: string }
|
|
112
182
|
| { kind: 'set-parent'; taskId: string; parentId: string | null }
|
|
183
|
+
/** Ledger-wide label management; see core/use-cases/task-tag.ts. */
|
|
184
|
+
| { kind: 'rename-tag'; from: string; to: string }
|
|
185
|
+
| { kind: 'delete-tag'; name: string }
|
|
186
|
+
| { kind: 'extension-action'; extensionId: string; action: string; taskId?: string; payload?: TaskBoardExtensionPayload }
|
|
113
187
|
|
|
114
188
|
export interface TaskBoardActionEnvelope {
|
|
115
189
|
requestId: string
|
|
@@ -149,18 +223,48 @@ function optionalFiniteNumber(value: unknown): boolean {
|
|
|
149
223
|
return value === undefined || (typeof value === 'number' && Number.isFinite(value))
|
|
150
224
|
}
|
|
151
225
|
|
|
226
|
+
/** Whether a stop-reason field names a reason this build understands. */
|
|
227
|
+
function validStopReason(value: unknown): boolean {
|
|
228
|
+
return value === undefined || (typeof value === 'string' && SCHEDULE_STOP_REASONS.includes(value as ScheduleStopReason))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Whether an imported schedule rule is structurally usable. A one-shot must
|
|
233
|
+
* carry its planned instant and a recurring rule its cron expression; the
|
|
234
|
+
* counter and stop record are optional (a rule written before them simply has
|
|
235
|
+
* none), but a value that IS present must be usable so the import cannot smuggle
|
|
236
|
+
* a rule the scheduler would misread.
|
|
237
|
+
*/
|
|
238
|
+
function validImportedSchedule(value: unknown): boolean {
|
|
239
|
+
const schedule = record(value)
|
|
240
|
+
if (schedule === undefined || typeof schedule.enabled !== 'boolean') return false
|
|
241
|
+
if (schedule.mode !== undefined && schedule.mode !== 'cron' && schedule.mode !== 'once') return false
|
|
242
|
+
if (schedule.mode === 'once') {
|
|
243
|
+
if (typeof schedule.at !== 'number' || !Number.isFinite(schedule.at)) return false
|
|
244
|
+
} else if (typeof schedule.cron !== 'string') {
|
|
245
|
+
return false
|
|
246
|
+
}
|
|
247
|
+
if (schedule.cron !== undefined && typeof schedule.cron !== 'string') return false
|
|
248
|
+
if (schedule.at !== undefined && (typeof schedule.at !== 'number' || !Number.isFinite(schedule.at))) return false
|
|
249
|
+
if (schedule.maxRuns !== undefined
|
|
250
|
+
&& (typeof schedule.maxRuns !== 'number' || !Number.isInteger(schedule.maxRuns) || schedule.maxRuns < 1)) return false
|
|
251
|
+
if (schedule.runCount !== undefined
|
|
252
|
+
&& (typeof schedule.runCount !== 'number' || !Number.isInteger(schedule.runCount) || schedule.runCount < 0)) return false
|
|
253
|
+
if (!optionalFiniteNumber(schedule.nextRunAt) || !optionalFiniteNumber(schedule.lastTriggeredAt)) return false
|
|
254
|
+
if (!optionalFiniteNumber(schedule.endedAt) || !optionalFiniteNumber(schedule.skippedAt)) return false
|
|
255
|
+
if (!validStopReason(schedule.endedReason) || !validStopReason(schedule.skippedReason)) return false
|
|
256
|
+
if (schedule.timeZone !== undefined
|
|
257
|
+
&& (typeof schedule.timeZone !== 'string' || !isValidTimeZone(schedule.timeZone))) return false
|
|
258
|
+
return true
|
|
259
|
+
}
|
|
260
|
+
|
|
152
261
|
function validImportedKnownFields(value: Record<string, unknown>): boolean {
|
|
153
262
|
// Tags are validated (not repaired) on the import path: an imported task
|
|
154
263
|
// must carry a well-formed list or none at all, so a hand-edited export
|
|
155
264
|
// cannot smuggle a malformed tag past the gate.
|
|
156
265
|
if (value.tags !== undefined && !isTaskTagList(value.tags)) return false
|
|
157
|
-
if (value.
|
|
158
|
-
|
|
159
|
-
if (schedule === undefined || typeof schedule.enabled !== 'boolean' || typeof schedule.cron !== 'string') return false
|
|
160
|
-
if (!optionalFiniteNumber(schedule.nextRunAt) || !optionalFiniteNumber(schedule.lastTriggeredAt)) return false
|
|
161
|
-
if (schedule.timeZone !== undefined
|
|
162
|
-
&& (typeof schedule.timeZone !== 'string' || !isValidTimeZone(schedule.timeZone))) return false
|
|
163
|
-
}
|
|
266
|
+
if (value.integrations !== undefined && normalizeTaskIntegrations(value.integrations) === undefined) return false
|
|
267
|
+
if (value.schedule !== undefined && !validImportedSchedule(value.schedule)) return false
|
|
164
268
|
if (value.executions !== undefined) {
|
|
165
269
|
if (!Array.isArray(value.executions)) return false
|
|
166
270
|
for (const item of value.executions) {
|
|
@@ -170,8 +274,16 @@ function validImportedKnownFields(value: Record<string, unknown>): boolean {
|
|
|
170
274
|
if (!optionalFiniteNumber(execution.endedAt) || !optionalString(execution.error)) return false
|
|
171
275
|
if (execution.result !== undefined && !['succeeded', 'failed', 'cancelled'].includes(String(execution.result))) return false
|
|
172
276
|
if (execution.initiatedBy !== undefined && typeof execution.initiatedBy !== 'string') return false
|
|
277
|
+
// An imported external marker is accepted (it is a declaration an
|
|
278
|
+
// operator made about their own board), but it is still just a boolean:
|
|
279
|
+
// the acceptance block below is what must never arrive from an import.
|
|
280
|
+
if (execution.external !== undefined && typeof execution.external !== 'boolean') return false
|
|
173
281
|
if (execution.frozenBy !== undefined && typeof execution.frozenBy !== 'string') return false
|
|
174
282
|
if (execution.frozenAt !== undefined && typeof execution.frozenAt !== 'number') return false
|
|
283
|
+
// A well-formed acceptance block may be imported for inspection, but the
|
|
284
|
+
// import path DROPS it (see importedTask): a fabricated pass record must
|
|
285
|
+
// never let an imported execution settle as verified.
|
|
286
|
+
if (execution.verification !== undefined && normalizeVerification(execution.verification) === undefined) return false
|
|
175
287
|
}
|
|
176
288
|
}
|
|
177
289
|
return true
|
|
@@ -198,18 +310,17 @@ function importedTask(value: unknown): TaskRecord | undefined {
|
|
|
198
310
|
result: execution.result,
|
|
199
311
|
error: execution.error,
|
|
200
312
|
...(execution.initiatedBy === undefined ? {} : { initiatedBy: execution.initiatedBy }),
|
|
313
|
+
...(execution.external === true ? { external: true } : {}),
|
|
201
314
|
...(execution.frozenAt === undefined ? {} : { frozenAt: execution.frozenAt }),
|
|
202
315
|
...(execution.frozenBy === undefined ? {} : { frozenBy: execution.frozenBy }),
|
|
316
|
+
// 安全门(对抗场景 d):验收报告是 Host 的判定,不接受 import 携带——
|
|
317
|
+
// 一条伪造的通过记录会让导入的执行被当成已验收。
|
|
203
318
|
})),
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
nextRunAt: task.schedule.nextRunAt,
|
|
210
|
-
lastTriggeredAt: task.schedule.lastTriggeredAt,
|
|
211
|
-
},
|
|
212
|
-
}),
|
|
319
|
+
// The rule was already reconstructed field by field by parseLedger (which
|
|
320
|
+
// is what drops an unknown or malformed field), so the import keeps that
|
|
321
|
+
// normalized shape: mode, planned instant, run counter and stop record all
|
|
322
|
+
// travel with it.
|
|
323
|
+
...(task.schedule === undefined ? {} : { schedule: { ...task.schedule } }),
|
|
213
324
|
// The lineage link rides the import; the Host repairs a link whose parent
|
|
214
325
|
// is missing from the merged ledger instead of trusting the export.
|
|
215
326
|
...(task.parentId === undefined ? {} : { parentId: task.parentId }),
|
|
@@ -218,10 +329,16 @@ function importedTask(value: unknown): TaskRecord | undefined {
|
|
|
218
329
|
...(task.permission === undefined ? {} : { permission: task.permission }),
|
|
219
330
|
...(task.reuseSession === undefined ? {} : { reuseSession: task.reuseSession }),
|
|
220
331
|
...(task.goalRun === undefined ? {} : { goalRun: task.goalRun }),
|
|
332
|
+
// The per-card acceptance opt-out rides the import like the /goal opt-in:
|
|
333
|
+
// it is a user-chosen execution preference on their own board, not a
|
|
334
|
+
// verdict. The per-execution `verification` block is still stripped below, so
|
|
335
|
+
// no imported card can arrive already accepted.
|
|
336
|
+
...(task.skipVerification === undefined ? {} : { skipVerification: task.skipVerification }),
|
|
221
337
|
...(task.archivedAt === undefined ? {} : { archivedAt: task.archivedAt }),
|
|
222
338
|
...(task.freeze === undefined ? {} : { freeze: task.freeze }),
|
|
223
339
|
...(task.handover === undefined ? {} : { handover: task.handover }),
|
|
224
340
|
...(task.tags === undefined ? {} : { tags: task.tags }),
|
|
341
|
+
...(task.integrations === undefined ? {} : { integrations: task.integrations }),
|
|
225
342
|
// 安全门(对抗场景 b):import 不是人工确认动作,确认戳一律剥除——
|
|
226
343
|
// 高于会话默认权限的绑定经 import 进入后必须重新武装 confirm-permission 门。
|
|
227
344
|
}
|
|
@@ -255,21 +372,28 @@ function handoverPayload(value: unknown): TaskHandoverInput | undefined {
|
|
|
255
372
|
|
|
256
373
|
function createInput(value: unknown): value is NewTaskInput {
|
|
257
374
|
const input = record(value)
|
|
258
|
-
if (input === undefined || !exactKeys(input, ['title', 'description', 'prompt', 'parentId', 'workspaceId', 'mode', 'permission', 'schedule', 'freeze', 'handover', 'model', 'reuseSession', 'teamRun', 'goalRun', 'tags'])) return false
|
|
375
|
+
if (input === undefined || !exactKeys(input, ['title', 'description', 'prompt', 'parentId', 'workspaceId', 'mode', 'permission', 'schedule', 'freeze', 'handover', 'model', 'reuseSession', 'teamRun', 'goalRun', 'skipVerification', 'tags', 'integrations'])) return false
|
|
259
376
|
if (input.parentId !== undefined && (typeof input.parentId !== 'string' || input.parentId.trim() === '')) return false
|
|
260
377
|
if (typeof input.title !== 'string' || typeof input.description !== 'string' || typeof input.prompt !== 'string') return false
|
|
261
378
|
if (!optionalString(input.workspaceId) || !optionalString(input.mode) || !optionalString(input.model)) return false
|
|
262
379
|
if (input.reuseSession !== undefined && typeof input.reuseSession !== 'boolean') return false
|
|
263
380
|
if (input.teamRun !== undefined && typeof input.teamRun !== 'boolean') return false
|
|
264
381
|
if (input.goalRun !== undefined && typeof input.goalRun !== 'boolean') return false
|
|
382
|
+
if (input.skipVerification !== undefined && typeof input.skipVerification !== 'boolean') return false
|
|
265
383
|
if (input.permission !== undefined && !isTaskPermission(input.permission)) return false
|
|
266
384
|
if (input.tags !== undefined && !isTaskTagList(input.tags)) return false
|
|
385
|
+
if (input.integrations !== undefined && normalizeTaskIntegrations(input.integrations) === undefined) return false
|
|
267
386
|
if (input.freeze !== undefined && freezePayload(input.freeze) === undefined) return false
|
|
268
387
|
if (input.handover !== undefined && handoverPayload(input.handover) === undefined) return false
|
|
269
388
|
if (input.schedule !== undefined) {
|
|
270
389
|
const schedule = record(input.schedule)
|
|
271
|
-
if (schedule === undefined || !exactKeys(schedule, ['enabled', 'cron', 'timeZone'])) return false
|
|
272
|
-
if (typeof schedule.enabled !== 'boolean'
|
|
390
|
+
if (schedule === undefined || !exactKeys(schedule, ['enabled', 'mode', 'cron', 'at', 'timeZone', 'maxRuns'])) return false
|
|
391
|
+
if (typeof schedule.enabled !== 'boolean') return false
|
|
392
|
+
if (schedule.mode !== undefined && schedule.mode !== 'cron' && schedule.mode !== 'once') return false
|
|
393
|
+
if (schedule.cron !== undefined && typeof schedule.cron !== 'string') return false
|
|
394
|
+
if (schedule.at !== undefined && (typeof schedule.at !== 'number' || !Number.isInteger(schedule.at))) return false
|
|
395
|
+
if (schedule.maxRuns !== undefined
|
|
396
|
+
&& (typeof schedule.maxRuns !== 'number' || !Number.isInteger(schedule.maxRuns) || schedule.maxRuns < 1)) return false
|
|
273
397
|
if (schedule.timeZone !== undefined
|
|
274
398
|
&& (typeof schedule.timeZone !== 'string' || !isValidTimeZone(schedule.timeZone))) return false
|
|
275
399
|
}
|
|
@@ -278,13 +402,16 @@ function createInput(value: unknown): value is NewTaskInput {
|
|
|
278
402
|
|
|
279
403
|
function updatePatch(value: unknown): boolean {
|
|
280
404
|
const patch = record(value)
|
|
281
|
-
if (patch === undefined || !exactKeys(patch, ['title', 'description', 'prompt', 'workspaceId', 'mode', 'permission', 'freeze', 'handover', 'model', 'reuseSession', 'teamRun', 'goalRun', 'tags'])) return false
|
|
405
|
+
if (patch === undefined || !exactKeys(patch, ['title', 'description', 'prompt', 'workspaceId', 'mode', 'permission', 'freeze', 'handover', 'model', 'reuseSession', 'teamRun', 'goalRun', 'skipVerification', 'tags'])) return false
|
|
282
406
|
// null (or false) clears the reuse opt-in; only a real boolean is accepted.
|
|
283
407
|
if (patch.reuseSession !== undefined && patch.reuseSession !== null && typeof patch.reuseSession !== 'boolean') return false
|
|
284
408
|
if (patch.teamRun !== undefined && patch.teamRun !== null && typeof patch.teamRun !== 'boolean') return false
|
|
285
409
|
// The goal opt-in is tri-state: null/true return the card to its default
|
|
286
410
|
// (goal run), false pins a single plain turn.
|
|
287
411
|
if (patch.goalRun !== undefined && patch.goalRun !== null && typeof patch.goalRun !== 'boolean') return false
|
|
412
|
+
// The acceptance opt-out is the mirror tri-state: null/false return the card
|
|
413
|
+
// to inheriting the board-wide switch, true pins it out of the gate.
|
|
414
|
+
if (patch.skipVerification !== undefined && patch.skipVerification !== null && typeof patch.skipVerification !== 'boolean') return false
|
|
288
415
|
for (const key of ['title', 'description', 'prompt', 'workspaceId', 'mode', 'model'] as const) {
|
|
289
416
|
if (!optionalString(patch[key])) return false
|
|
290
417
|
}
|
|
@@ -297,16 +424,33 @@ function updatePatch(value: unknown): boolean {
|
|
|
297
424
|
return patch.handover === undefined || patch.handover === null || handoverPayload(patch.handover) !== undefined
|
|
298
425
|
}
|
|
299
426
|
|
|
427
|
+
/**
|
|
428
|
+
* A label name on the wire: a non-empty trimmed string within the tag cap. The
|
|
429
|
+
* trim/merge/duplicate rules belong to the use case; this gate only keeps a
|
|
430
|
+
* malformed name off the ledger.
|
|
431
|
+
*/
|
|
432
|
+
function isTagName(value: unknown): value is string {
|
|
433
|
+
return typeof value === 'string' && value.trim() !== '' && value.trim().length <= TAG_NAME_MAX_LENGTH
|
|
434
|
+
}
|
|
435
|
+
|
|
300
436
|
function schedulePatch(value: unknown): boolean {
|
|
301
437
|
const patch = record(value)
|
|
302
438
|
return patch !== undefined
|
|
303
|
-
&& exactKeys(patch, ['enabled', 'cron', 'timeZone'])
|
|
439
|
+
&& exactKeys(patch, ['enabled', 'mode', 'cron', 'at', 'timeZone', 'maxRuns'])
|
|
304
440
|
&& (patch.enabled === undefined || typeof patch.enabled === 'boolean')
|
|
441
|
+
&& (patch.mode === undefined || patch.mode === 'cron' || patch.mode === 'once')
|
|
305
442
|
&& (patch.cron === undefined || typeof patch.cron === 'string')
|
|
443
|
+
// A one-shot's planned instant is a whole millisecond: a fractional or
|
|
444
|
+
// non-numeric value would make the armed target ambiguous.
|
|
445
|
+
&& (patch.at === undefined || (typeof patch.at === 'number' && Number.isInteger(patch.at)))
|
|
306
446
|
// `null` clears the stored zone back to the Host zone; an unknown name is
|
|
307
447
|
// rejected here so the Host never has to guess at an unusable zone.
|
|
308
448
|
&& (patch.timeZone === undefined || patch.timeZone === null
|
|
309
449
|
|| (typeof patch.timeZone === 'string' && isValidTimeZone(patch.timeZone)))
|
|
450
|
+
// `null` clears the run cap back to unlimited; any other value must be a
|
|
451
|
+
// positive whole number of runs.
|
|
452
|
+
&& (patch.maxRuns === undefined || patch.maxRuns === null
|
|
453
|
+
|| (typeof patch.maxRuns === 'number' && Number.isInteger(patch.maxRuns) && patch.maxRuns >= 1))
|
|
310
454
|
}
|
|
311
455
|
|
|
312
456
|
export function parseActionEnvelope(value: unknown): TaskBoardActionEnvelope | undefined {
|
|
@@ -368,6 +512,16 @@ function parseEnvelopeAction(value: unknown): TaskBoardActionEnvelope | undefine
|
|
|
368
512
|
if (parentId !== null && (typeof parentId !== 'string' || parentId.trim() === '')) return undefined
|
|
369
513
|
return { requestId: envelope.requestId, action: { kind: 'set-parent', taskId, parentId } }
|
|
370
514
|
}
|
|
515
|
+
case 'rename-tag': {
|
|
516
|
+
if (!exactKeys(action, ['kind', 'from', 'to'])) return undefined
|
|
517
|
+
if (!isTagName(action.from) || !isTagName(action.to)) return undefined
|
|
518
|
+
return { requestId: envelope.requestId, action: { kind: 'rename-tag', from: action.from, to: action.to } }
|
|
519
|
+
}
|
|
520
|
+
case 'delete-tag': {
|
|
521
|
+
if (!exactKeys(action, ['kind', 'name'])) return undefined
|
|
522
|
+
if (!isTagName(action.name)) return undefined
|
|
523
|
+
return { requestId: envelope.requestId, action: { kind: 'delete-tag', name: action.name } }
|
|
524
|
+
}
|
|
371
525
|
case 'set-schedule':
|
|
372
526
|
if (!exactKeys(action, ['kind', 'taskId', 'patch'])) return undefined
|
|
373
527
|
return taskId !== undefined && schedulePatch(action.patch)
|
|
@@ -378,11 +532,55 @@ function parseEnvelopeAction(value: unknown): TaskBoardActionEnvelope | undefine
|
|
|
378
532
|
return taskId !== undefined && isTaskStatus(action.status)
|
|
379
533
|
? { requestId: envelope.requestId, action: action as unknown as Extract<TaskBoardAction, { kind: 'move' }> }
|
|
380
534
|
: undefined
|
|
535
|
+
case 'extension-action': {
|
|
536
|
+
// Structural gate only: whether the id names a registered, enabled
|
|
537
|
+
// extension is decided by the host registry, which owns that state.
|
|
538
|
+
if (!exactKeys(action, ['kind', 'extensionId', 'action', 'taskId', 'payload'])) return undefined
|
|
539
|
+
if (typeof action.extensionId !== 'string' || action.extensionId.trim() === '' || action.extensionId.length > 128) return undefined
|
|
540
|
+
if (typeof action.action !== 'string' || action.action.trim() === '' || action.action.length > 128) return undefined
|
|
541
|
+
if (action.taskId !== undefined && (typeof action.taskId !== 'string' || action.taskId === '')) return undefined
|
|
542
|
+
if (action.payload !== undefined && !isTaskBoardExtensionPayload(action.payload)) return undefined
|
|
543
|
+
return {
|
|
544
|
+
requestId: envelope.requestId,
|
|
545
|
+
action: {
|
|
546
|
+
kind: 'extension-action',
|
|
547
|
+
extensionId: action.extensionId,
|
|
548
|
+
action: action.action,
|
|
549
|
+
...(action.taskId === undefined ? {} : { taskId: action.taskId }),
|
|
550
|
+
...(action.payload === undefined ? {} : { payload: action.payload }),
|
|
551
|
+
},
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
case 'record-external-outcome': {
|
|
555
|
+
if (!exactKeys(action, ['kind', 'taskId', 'result', 'initiatedBy', 'summary'])) return undefined
|
|
556
|
+
if (taskId === undefined) return undefined
|
|
557
|
+
// Only a real outside verdict is accepted: `running` is Host-only and
|
|
558
|
+
// `cancelled` is the Host's own bookkeeping for a run it closed.
|
|
559
|
+
if (action.result !== 'succeeded' && action.result !== 'failed') return undefined
|
|
560
|
+
if (typeof action.initiatedBy !== 'string') return undefined
|
|
561
|
+
const initiatedBy = action.initiatedBy.trim()
|
|
562
|
+
// The caller must be identifiable: an unattributed external outcome is
|
|
563
|
+
// the same 'state without history' the record exists to avoid.
|
|
564
|
+
if (initiatedBy === '' || initiatedBy.length > EXTERNAL_INITIATOR_MAX_LENGTH) return undefined
|
|
565
|
+
if (action.summary !== undefined && typeof action.summary !== 'string') return undefined
|
|
566
|
+
const summary = typeof action.summary === 'string' ? action.summary.trim().slice(0, EXTERNAL_SUMMARY_MAX_LENGTH) : ''
|
|
567
|
+
return {
|
|
568
|
+
requestId: envelope.requestId,
|
|
569
|
+
action: {
|
|
570
|
+
kind: 'record-external-outcome',
|
|
571
|
+
taskId,
|
|
572
|
+
result: action.result,
|
|
573
|
+
initiatedBy,
|
|
574
|
+
...(summary === '' ? {} : { summary }),
|
|
575
|
+
},
|
|
576
|
+
}
|
|
577
|
+
}
|
|
381
578
|
case 'confirm-permission':
|
|
382
579
|
case 'delete':
|
|
383
580
|
case 'archive':
|
|
384
581
|
case 'restore':
|
|
385
582
|
case 'settle':
|
|
583
|
+
case 'reset-verification':
|
|
386
584
|
case 'run':
|
|
387
585
|
case 'rerun':
|
|
388
586
|
if (!exactKeys(action, ['kind', 'taskId'])) return undefined
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/host/tool-surface.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
/**
|
|
3
|
+
* Family-shared tool-surface conventions for the packages that register
|
|
4
|
+
* model-facing tools.
|
|
5
|
+
*
|
|
6
|
+
* One home for the prompt-section placement and the visibility gate this
|
|
7
|
+
* family previously restated per package.
|
|
8
|
+
*
|
|
9
|
+
* Modelled on the official tool packages:
|
|
10
|
+
* - `@deepseek-ai/dsh-tool-subagent` renders its section through
|
|
11
|
+
* `tools.get(name, context.scope) === undefined ? '' : text`, so guidance
|
|
12
|
+
* disappears together with a restricted tool.
|
|
13
|
+
* - 21 of the 22 official tool packages register their tools GLOBALLY and gate
|
|
14
|
+
* them with a setting, not per agent. Only the Agent Teams tool package —
|
|
15
|
+
* whose tools belong to one membership — installs into each `agent.ctx`.
|
|
16
|
+
* This family follows the majority: registration stays global so the
|
|
17
|
+
* registry's per-scope `restrict()` mask (which filters inherited global
|
|
18
|
+
* tools and REJECTS scope-local names) can still withhold these tools.
|
|
19
|
+
*
|
|
20
|
+
* Structural types only: this copy lands in packages that do not all depend on
|
|
21
|
+
* the same SDK service packages.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Prompt-section order for each package's tool guidance.
|
|
26
|
+
*
|
|
27
|
+
* These sit in the family band between the deployment persona and the SDK's
|
|
28
|
+
* `PLAN_POLICY` (500). Values are owned here rather than written per package so
|
|
29
|
+
* the host plugins whose guidance shares one prompt cannot drift into an
|
|
30
|
+
* accidental tie.
|
|
31
|
+
*/
|
|
32
|
+
export const PLUGIN_TOOL_SECTION_ORDERS = {
|
|
33
|
+
ssh: 150,
|
|
34
|
+
'task-board': 200,
|
|
35
|
+
} as const
|
|
36
|
+
|
|
37
|
+
/** One plugin id keyed by {@link PLUGIN_TOOL_SECTION_ORDERS}. */
|
|
38
|
+
export type PluginToolSectionId = keyof typeof PLUGIN_TOOL_SECTION_ORDERS
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Order of an external provider extension's own guidance section, right after
|
|
42
|
+
* the band of the plugin it extends.
|
|
43
|
+
*
|
|
44
|
+
* An extension names itself, not the plugin it plugs into, so its order stays
|
|
45
|
+
* out of {@link PLUGIN_TOOL_SECTION_ORDERS}: every package's copy of this module
|
|
46
|
+
* then carries no provider id. The slot still has one owner and cannot collide
|
|
47
|
+
* with a host plugin's band.
|
|
48
|
+
*/
|
|
49
|
+
export const EXTENSION_TOOL_SECTION_ORDER = 210
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The scoped tool lookup a section provider needs: the registry read for one
|
|
53
|
+
* assembly's scope. Matches `ctx.tools.get(name, scope)`.
|
|
54
|
+
*/
|
|
55
|
+
export interface ScopedToolLookup {
|
|
56
|
+
get(name: string, scope?: unknown): unknown
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One assembly context carrying the scope the section is rendered for. */
|
|
60
|
+
export interface SectionScopeContext {
|
|
61
|
+
scope?: unknown
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build a section provider that renders \`text\` only while at least one of
|
|
66
|
+
* \`toolNames\` is visible to the rendering scope.
|
|
67
|
+
*
|
|
68
|
+
* The native convention: a tool that a restriction, a preset, or a missing
|
|
69
|
+
* registry removed must not leave guidance behind telling the model to call it.
|
|
70
|
+
* An unreadable registry renders the text unchanged — guidance that outlives a
|
|
71
|
+
* momentary read failure is recoverable; guidance for a tool that is gone is
|
|
72
|
+
* not.
|
|
73
|
+
* @param tools - the scoped tool registry, when the deployment serves one.
|
|
74
|
+
* @param toolNames - registered names the guidance describes.
|
|
75
|
+
* @param text - the guidance rendered while they are reachable.
|
|
76
|
+
* @returns the section text provider.
|
|
77
|
+
*/
|
|
78
|
+
export function visibleToolText(
|
|
79
|
+
tools: ScopedToolLookup | undefined,
|
|
80
|
+
toolNames: readonly string[],
|
|
81
|
+
text: string,
|
|
82
|
+
): (context: SectionScopeContext) => string {
|
|
83
|
+
if (tools === undefined) return () => text
|
|
84
|
+
return (context: SectionScopeContext) => {
|
|
85
|
+
for (const name of toolNames) {
|
|
86
|
+
try {
|
|
87
|
+
if (tools.get(name, context.scope) !== undefined) return text
|
|
88
|
+
} catch {
|
|
89
|
+
// A refused lookup is not proof the tool is gone; keep the guidance.
|
|
90
|
+
return text
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return ''
|
|
94
|
+
}
|
|
95
|
+
}
|
package/lib/invariant.js
DELETED
package/lib/types/invariant.d.ts
DELETED
package/src/invariant.ts
DELETED