@linxin666/dsh-client-ui-task-board 0.4.3 → 0.4.5
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 +56 -18
- package/README.zh.md +56 -18
- package/lib/client.js +6242 -958
- package/lib/client.js.map +1 -1
- package/lib/index.js +3475 -234
- package/lib/types/client/PluginSettingsCard.d.ts +7 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +19 -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 +23 -2
- 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/preset-label.d.ts +22 -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/board-card-seat.d.ts +34 -0
- package/lib/types/client/host-api.d.ts +24 -1
- package/lib/types/client/index.d.ts +19 -0
- package/lib/types/client/locales.d.ts +141 -3
- package/lib/types/client/native-panel.d.ts +6 -20
- package/lib/types/client/schedule-zone.d.ts +47 -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 +38 -1
- package/lib/types/core/degraded-reason.d.ts +46 -0
- 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/package-name.d.ts +9 -0
- package/lib/types/core/schedule.d.ts +95 -20
- package/lib/types/core/subtask.d.ts +30 -11
- package/lib/types/core/tasks.d.ts +110 -5
- package/lib/types/core/use-cases/task-create.d.ts +2 -1
- package/lib/types/core/use-cases/task-schedule.d.ts +8 -4
- 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.d.ts +386 -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 +71 -0
- package/lib/types/host/verification-runner.d.ts +108 -0
- package/lib/types/host/workspace-evidence.d.ts +53 -0
- package/lib/types/host-ledger.d.ts +107 -11
- package/lib/types/host-runner.d.ts +99 -4
- package/lib/types/host-service.d.ts +114 -2
- package/lib/types/index.d.ts +80 -26
- package/lib/types/protocol.d.ts +53 -3
- package/lib/types/tool-surface.d.ts +71 -0
- package/package.json +27 -26
- package/src/client/PluginSettingsCard.tsx +9 -0
- package/src/client/TaskBoardSettingsCard.tsx +304 -65
- 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 +30 -15
- package/src/client/board/NewTaskModal.tsx +288 -151
- package/src/client/board/TagManagerModal.tsx +235 -0
- package/src/client/board/TaskBoard.tsx +259 -103
- package/src/client/board/TaskCard.tsx +238 -86
- package/src/client/board/TaskDetail.tsx +154 -59
- package/src/client/board/TaskForm.tsx +61 -6
- package/src/client/board/VerificationReport.tsx +203 -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/preset-label.ts +44 -0
- package/src/client/board/status-key.ts +27 -0
- package/src/client/board/task-markdown.module.css +137 -0
- package/src/client/board/task-markdown.tsx +106 -0
- package/src/client/board-card-seat.ts +102 -0
- package/src/client/board-settings.module.css +66 -0
- package/src/client/board.module.css +602 -68
- package/src/client/host-api.ts +109 -6
- package/src/client/index.ts +48 -6
- package/src/client/locales.ts +290 -12
- package/src/client/native-panel.tsx +18 -2
- package/src/client/schedule-zone.ts +135 -0
- 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 +101 -5
- package/src/core/degraded-reason.ts +81 -0
- 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/package-name.ts +9 -0
- package/src/core/schedule.ts +211 -55
- package/src/core/store.ts +26 -1
- package/src/core/subtask.ts +77 -22
- package/src/core/tasks.ts +138 -10
- package/src/core/use-cases/task-create.ts +14 -2
- package/src/core/use-cases/task-parent.ts +3 -2
- package/src/core/use-cases/task-schedule.ts +17 -6
- package/src/core/use-cases/task-tag.ts +110 -0
- package/src/core/use-cases/task-update.ts +13 -4
- package/src/core/verification.ts +826 -0
- package/src/core/workspace-target.ts +59 -0
- package/src/host/agent-tools.ts +58 -24
- package/src/host/extension-registry.ts +509 -0
- package/src/host/llm-dispatch.ts +87 -0
- package/src/host/verification-gate.ts +331 -0
- package/src/host/verification-runner.ts +535 -0
- package/src/host/workspace-evidence.ts +176 -0
- package/src/host-ai.ts +15 -9
- package/src/host-ledger.ts +342 -51
- package/src/host-routes.ts +15 -2
- package/src/host-runner.ts +219 -20
- package/src/host-service.ts +346 -26
- package/src/index.ts +204 -16
- package/src/protocol.ts +122 -9
- package/src/tool-surface.ts +95 -0
- package/lib/types/host/run-guarded.d.ts +0 -39
- package/src/host/run-guarded.ts +0 -76
package/src/index.ts
CHANGED
|
@@ -19,15 +19,20 @@ import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
|
|
|
19
19
|
import type { LlmRuntime } from '@deepseek-ai/dsh-llm'
|
|
20
20
|
import { TaskBoardHostService, type HostTimerFace, type TaskBoardTeamDispatcher } from './host-service.ts'
|
|
21
21
|
import { parseTaskDraft, TaskParseError } from './host-ai.ts'
|
|
22
|
-
import { TASK_PERMISSIONS, type TaskPermission } from './core/tasks.ts'
|
|
22
|
+
import { TASK_PERMISSIONS, isTaskPermission, type TaskPermission } from './core/tasks.ts'
|
|
23
23
|
import { DEFAULT_SUBTASK_DEPTH, SUBTASK_DEPTH_MAX, SUBTASK_DEPTH_MIN } from './core/subtask.ts'
|
|
24
24
|
import { DEFAULT_SESSION_PERMISSION } from './core/handover.ts'
|
|
25
|
-
import { buildTaskBoardTools } from './host/agent-tools.ts'
|
|
25
|
+
import { buildTaskBoardTools, TASK_BOARD_TOOL_NAMES } from './host/agent-tools.ts'
|
|
26
|
+
import { PLUGIN_TOOL_SECTION_ORDERS, visibleToolText } from './tool-surface.ts'
|
|
26
27
|
import { makeTaskBoardRoutes } from './host-routes.ts'
|
|
28
|
+
import { TASK_BOARD_SERVICE_NAME, type TaskBoardExtension } from './core/extension.ts'
|
|
27
29
|
import { mountOnce } from './mount-once.ts'
|
|
30
|
+
import { createGoalVerificationGate, type GoalFace } from './host/verification-gate.ts'
|
|
31
|
+
import { normalizeCatalog, type ModelCatalogView, type VerificationSettings } from './core/verification.ts'
|
|
32
|
+
import { probeWorkspaceChanges } from './host/workspace-evidence.ts'
|
|
28
33
|
|
|
29
|
-
/** Order of the announcement section within the tool-guidance band. */
|
|
30
|
-
const SECTION_ORDER =
|
|
34
|
+
/** Order of the announcement section within the shared tool-guidance band. */
|
|
35
|
+
const SECTION_ORDER = PLUGIN_TOOL_SECTION_ORDERS['task-board']
|
|
31
36
|
|
|
32
37
|
/** Default environment variable holding the authenticated proxy token. */
|
|
33
38
|
export const DEFAULT_PROXY_TOKEN_ENV = 'DSH_TASK_BOARD_PROXY_TOKEN'
|
|
@@ -68,9 +73,12 @@ export interface Config {
|
|
|
68
73
|
/** Environment variable whose value the authenticated proxy injects upstream. */
|
|
69
74
|
proxyTokenEnv?: string
|
|
70
75
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
76
|
+
* An explicit baseline for the permission confirmation gate. Left unset, the
|
|
77
|
+
* board follows the Host's own default permission preset — the value new DSH
|
|
78
|
+
* sessions start at — and falls back to `read-only` when the deployment
|
|
79
|
+
* serves no permission catalog. A card whose effective permission (handover
|
|
80
|
+
* bundle or pin) is above the baseline requires a human confirmation before
|
|
81
|
+
* it may run; cron refuses unconfirmed cards.
|
|
74
82
|
*/
|
|
75
83
|
sessionDefaultPermission?: TaskPermission
|
|
76
84
|
/**
|
|
@@ -86,6 +94,31 @@ export interface Config {
|
|
|
86
94
|
* only team-mode runs use it.
|
|
87
95
|
*/
|
|
88
96
|
teamProvider?: string
|
|
97
|
+
/**
|
|
98
|
+
* Goal acceptance for executions this board starts (default ON). When on,
|
|
99
|
+
* update_goal(action: complete) inside a task execution is refused until an
|
|
100
|
+
* acceptance pass is recorded for that execution: the three coding criteria,
|
|
101
|
+
* a 0.65 threshold, two rounds per criterion with the A/B slots swapped, and
|
|
102
|
+
* at most two acceptances per execution — the first failure returns its
|
|
103
|
+
* findings to the fixing agent, the second ends the cycle. The switch only
|
|
104
|
+
* affects goal-form executions of the board; plain chat and a task pinned to
|
|
105
|
+
* goalRun: false are untouched. When a separate third-party verifier also
|
|
106
|
+
* runs its own automatic acceptance, both judges score independently: this
|
|
107
|
+
* one gates completion, the other only steers, and no public interface lets
|
|
108
|
+
* the two share a verdict.
|
|
109
|
+
*/
|
|
110
|
+
goalVerification?: Volatile<boolean>
|
|
111
|
+
/**
|
|
112
|
+
* Judge model for goal acceptance as provider/model. Blank inherits the HOST's
|
|
113
|
+
* own model catalog default — never the card's pinned execution model.
|
|
114
|
+
*/
|
|
115
|
+
goalVerificationModel?: Volatile<string>
|
|
116
|
+
/**
|
|
117
|
+
* Reasoning effort for the judge. Blank inherits the host default level; an
|
|
118
|
+
* effort the resolved model does not declare is dropped instead of being
|
|
119
|
+
* sent, and the fallback to the model's own default is reported.
|
|
120
|
+
*/
|
|
121
|
+
goalVerificationReasoningEffort?: Volatile<string>
|
|
89
122
|
}
|
|
90
123
|
|
|
91
124
|
/**
|
|
@@ -95,22 +128,57 @@ export interface Config {
|
|
|
95
128
|
export const DEFAULT_TEAM_PROVIDER = 'spawn'
|
|
96
129
|
|
|
97
130
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
131
|
+
* Profile-patch shape of {@link Config}: what the Host validates the row's
|
|
132
|
+
* config against, before the schema turns volatile fields into live references
|
|
133
|
+
* and applies defaults. Declared separately because the two sides no longer
|
|
134
|
+
* share one shape, so the schema is annotated `z<ConfigInput, Config>` — the
|
|
135
|
+
* same split `dsh-git-graph` uses. The annotation is also what keeps the
|
|
136
|
+
* emitted declaration portable: an unannotated schema holding a nested object
|
|
137
|
+
* array infers a type that cannot be named without reaching into a transitive
|
|
138
|
+
* dependency's own types.
|
|
102
139
|
*/
|
|
103
|
-
export
|
|
140
|
+
export interface ConfigInput {
|
|
141
|
+
/** Announce the board in the system prompt. */
|
|
142
|
+
announceToAgent?: boolean
|
|
143
|
+
/** Master switch for the plugin. */
|
|
144
|
+
enabled?: boolean
|
|
145
|
+
/** Prevent idle system sleep while sessions run or schedules are armed. */
|
|
146
|
+
preventIdleSleep?: boolean
|
|
147
|
+
/** Canonical reverse-proxy Host authorities. */
|
|
148
|
+
trustedProxyHosts?: string[]
|
|
149
|
+
/** Environment variable whose value the authenticated proxy injects. */
|
|
150
|
+
proxyTokenEnv?: string
|
|
151
|
+
/** Explicit baseline for the permission confirmation gate; unset follows the Host default. */
|
|
152
|
+
sessionDefaultPermission?: TaskPermission
|
|
153
|
+
/** Subtask depth limit, 1..3. */
|
|
154
|
+
maxSubtaskDepth?: number
|
|
155
|
+
/** Continuable-subagent provider the Agent Teams service composes a teammate from. */
|
|
156
|
+
teamProvider?: string
|
|
157
|
+
/** Goal acceptance switch. */
|
|
158
|
+
goalVerification?: boolean
|
|
159
|
+
/** Judge model route for goal acceptance; blank inherits the host default. */
|
|
160
|
+
goalVerificationModel?: string
|
|
161
|
+
/** Judge reasoning effort; blank inherits the host default. */
|
|
162
|
+
goalVerificationReasoningEffort?: string
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const Config: z<ConfigInput, Config> = z.object({
|
|
104
166
|
announceToAgent: z.boolean().default(false).volatile(),
|
|
105
167
|
enabled: z.boolean().default(true).volatile(),
|
|
106
168
|
preventIdleSleep: z.boolean().default(false).volatile(),
|
|
107
169
|
trustedProxyHosts: z.array(z.string()).default([]),
|
|
108
170
|
proxyTokenEnv: z.string().min(1).default(DEFAULT_PROXY_TOKEN_ENV),
|
|
109
|
-
sessionDefaultPermission: z.union(TASK_PERMISSIONS)
|
|
171
|
+
sessionDefaultPermission: z.union(TASK_PERMISSIONS),
|
|
110
172
|
maxSubtaskDepth: z.number().min(SUBTASK_DEPTH_MIN).max(SUBTASK_DEPTH_MAX).default(DEFAULT_SUBTASK_DEPTH).volatile(),
|
|
111
173
|
teamProvider: z.string().min(1).default(DEFAULT_TEAM_PROVIDER),
|
|
174
|
+
goalVerification: z.boolean().default(true).volatile(),
|
|
175
|
+
goalVerificationModel: z.string().default('').volatile(),
|
|
176
|
+
goalVerificationReasoningEffort: z.string().default('').volatile(),
|
|
112
177
|
})
|
|
113
178
|
|
|
179
|
+
/** Schema default for the goal-acceptance switch, re-read for hand-built contexts. */
|
|
180
|
+
export const DEFAULT_GOAL_VERIFICATION = true
|
|
181
|
+
|
|
114
182
|
declare module '@deepseek-ai/cordis' {
|
|
115
183
|
interface Events {
|
|
116
184
|
/**
|
|
@@ -160,6 +228,13 @@ export function resolveProxyAccess(config: Config | undefined, env: NodeJS.Proce
|
|
|
160
228
|
/** The registry face the agent tools register into. */
|
|
161
229
|
interface AgentToolRegistry {
|
|
162
230
|
register(definition: ToolDefinition): () => void
|
|
231
|
+
/** Scoped lookup; present on the real registry, absent on a stub that only registers. */
|
|
232
|
+
get?(name: string, scope?: unknown): unknown
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The lookup half of a resolved registry, when it exposes one. */
|
|
236
|
+
function lookupOf(registry: AgentToolRegistry | undefined): { get: (name: string, scope?: unknown) => unknown } | undefined {
|
|
237
|
+
return registry !== undefined && typeof registry.get === 'function' ? { get: registry.get.bind(registry) } : undefined
|
|
163
238
|
}
|
|
164
239
|
|
|
165
240
|
/**
|
|
@@ -203,6 +278,33 @@ export function resolveHostTimers(ctx: Context): HostTimerFace | undefined {
|
|
|
203
278
|
/** Schema default, re-read for hand-built test contexts (the loader applies them normally). */
|
|
204
279
|
const DEFAULT_ANNOUNCE = false
|
|
205
280
|
|
|
281
|
+
/** The subset of the official permission-preset service the board reads. */
|
|
282
|
+
interface PermissionPresetsFace {
|
|
283
|
+
catalog(): { defaultPreset?: unknown }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The Host's own default permission preset, mapped into the board's vocabulary.
|
|
288
|
+
*
|
|
289
|
+
* This is the value new DSH sessions start at (the deployment's `permission`
|
|
290
|
+
* settings row), so a board that follows it treats an ordinary card exactly as
|
|
291
|
+
* the Host would. Absent when the deployment serves no permission catalog, or
|
|
292
|
+
* its default is a current-session identity (`auto`) that names no fixed
|
|
293
|
+
* sandbox mode; the caller then keeps the fail-safe baseline.
|
|
294
|
+
* @param ctx - the plugin context.
|
|
295
|
+
* @returns the Host default as a board permission, or undefined when unreadable.
|
|
296
|
+
*/
|
|
297
|
+
export function resolveHostDefaultPermission(ctx: Context): TaskPermission | undefined {
|
|
298
|
+
try {
|
|
299
|
+
const presets = ctx.get('permissionPresets') as PermissionPresetsFace | undefined
|
|
300
|
+
if (presets === undefined || typeof presets.catalog !== 'function') return undefined
|
|
301
|
+
const preset = presets.catalog()?.defaultPreset
|
|
302
|
+
return isTaskPermission(preset) ? preset : undefined
|
|
303
|
+
} catch {
|
|
304
|
+
return undefined
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
206
308
|
/**
|
|
207
309
|
* How long one teammate may take to reach its durable active or failed edge.
|
|
208
310
|
* Agent Teams provisioning normally resolves in seconds; the bound exists so a
|
|
@@ -317,11 +419,35 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
317
419
|
const preventIdleSleep = (): boolean => readConfigField(config?.preventIdleSleep, false)
|
|
318
420
|
/** Current subtask depth limit (read live: the settings card edits it in place). */
|
|
319
421
|
const maxSubtaskDepth = (): number => readConfigField(config?.maxSubtaskDepth, DEFAULT_SUBTASK_DEPTH)
|
|
422
|
+
/**
|
|
423
|
+
* The baseline the permission confirmation gate judges against: the row's own
|
|
424
|
+
* `sessionDefaultPermission` when the deployment pins one, otherwise the Host's
|
|
425
|
+
* default preset. Read per call through the ledger, so a Host Settings change
|
|
426
|
+
* reaches the running board without remounting the row; the fail-safe default
|
|
427
|
+
* still applies when no catalog is available.
|
|
428
|
+
*/
|
|
429
|
+
const sessionDefaultPermission = (): TaskPermission =>
|
|
430
|
+
config?.sessionDefaultPermission ?? resolveHostDefaultPermission(ctx) ?? DEFAULT_SESSION_PERMISSION
|
|
320
431
|
|
|
432
|
+
/** Live acceptance settings the contract of each new execution freezes. */
|
|
433
|
+
const verificationSettings = (): VerificationSettings => ({
|
|
434
|
+
enabled: readConfigField(config?.goalVerification, DEFAULT_GOAL_VERIFICATION),
|
|
435
|
+
model: readConfigField(config?.goalVerificationModel, ''),
|
|
436
|
+
reasoningEffort: readConfigField(config?.goalVerificationReasoningEffort, ''),
|
|
437
|
+
})
|
|
438
|
+
// The catalog reader needs the service, and the service needs the reader;
|
|
439
|
+
// the reader only runs after start(), so a late holder is enough.
|
|
440
|
+
let hostForCatalog: TaskBoardHostService | undefined
|
|
441
|
+
const verificationCatalog = async (): Promise<ModelCatalogView | undefined> => {
|
|
442
|
+
const runner = hostForCatalog?.runner
|
|
443
|
+
return runner === undefined ? undefined : normalizeCatalog(await runner.modelCatalog())
|
|
444
|
+
}
|
|
321
445
|
const host = new TaskBoardHostService(ctx.typertGateway, {
|
|
322
446
|
workspaceRegistry: ctx.workspaceRegistry,
|
|
323
|
-
sessionDefaultPermission
|
|
447
|
+
sessionDefaultPermission,
|
|
324
448
|
maxSubtaskDepth: maxSubtaskDepth(),
|
|
449
|
+
verificationSettings,
|
|
450
|
+
verificationCatalog,
|
|
325
451
|
timers: resolveHostTimers(ctx),
|
|
326
452
|
team: buildTeamDispatcher(ctx, config?.teamProvider ?? DEFAULT_TEAM_PROVIDER),
|
|
327
453
|
commandDispatcher: {
|
|
@@ -332,22 +458,41 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
332
458
|
},
|
|
333
459
|
},
|
|
334
460
|
})
|
|
461
|
+
hostForCatalog = host
|
|
335
462
|
// Configuration before start(): a disabled row must not take the first
|
|
336
463
|
// scheduler tick, which would roll schedules the board is not running.
|
|
337
464
|
host.setConfiguration(enabled(), preventIdleSleep())
|
|
338
465
|
host.start()
|
|
339
466
|
|
|
467
|
+
// External providers. The board publishes its registration service so any
|
|
468
|
+
// provider package can resolve it and admit itself; the board itself knows
|
|
469
|
+
// no provider vocabulary. A capture-only test context implements no service
|
|
470
|
+
// registry and the board still serves its own surfaces.
|
|
471
|
+
if (typeof (ctx as { provide?: unknown }).provide === 'function') {
|
|
472
|
+
ctx.provide(TASK_BOARD_SERVICE_NAME, {
|
|
473
|
+
registerExtension: (extension: TaskBoardExtension) => host.registerExtension(extension),
|
|
474
|
+
isExtensionEnabled: (extensionId: string) => host.extensions.isActive(extensionId),
|
|
475
|
+
})
|
|
476
|
+
}
|
|
477
|
+
|
|
340
478
|
// Agent tools: the same Host ledger the browser drives, so any session can
|
|
341
479
|
// list, create, link, run and settle board work. Registration follows the
|
|
342
480
|
// master switch (a disabled board answers no tool call), and the mount
|
|
343
|
-
// effect below owns the disposers.
|
|
481
|
+
// effect below owns the disposers. Provider tools register through the
|
|
482
|
+
// extension registry, which follows the same gate and rebinds when the tool
|
|
483
|
+
// registry appears late.
|
|
344
484
|
let disposeTools: (() => void) | undefined
|
|
345
485
|
const setToolsEnabled = (active: boolean): void => {
|
|
346
486
|
if (!active) {
|
|
487
|
+
// Releasing is the whole job on the way down. Rebinding the extension
|
|
488
|
+
// registry here would re-register every provider tool, and the teardown
|
|
489
|
+
// path runs this while the fiber is already unloading, so each of those
|
|
490
|
+
// re-registrations is a request the host can only refuse.
|
|
347
491
|
disposeTools?.()
|
|
348
492
|
disposeTools = undefined
|
|
349
493
|
return
|
|
350
494
|
}
|
|
495
|
+
host.extensions.setToolRegistry(() => resolveToolRegistry(ctx))
|
|
351
496
|
if (disposeTools !== undefined) return
|
|
352
497
|
const registry = resolveToolRegistry(ctx)
|
|
353
498
|
if (registry === undefined) return
|
|
@@ -364,6 +509,9 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
364
509
|
if (typeof scopedInject === 'function') {
|
|
365
510
|
scopedInject.call(ctx, ['tools'], () => {
|
|
366
511
|
setToolsEnabled(enabled())
|
|
512
|
+
// The extension registry follows the same rebinding: a registry that
|
|
513
|
+
// appears (or is replaced) after a provider started adopts its tools.
|
|
514
|
+
host.extensions.setToolRegistry(() => resolveToolRegistry(ctx))
|
|
367
515
|
// Cordis unloads and re-runs this callback when the injected service's
|
|
368
516
|
// provider fiber changes, and the old registry dies with its provider.
|
|
369
517
|
// Releasing the guard here is what lets the callback register into the
|
|
@@ -378,6 +526,42 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
378
526
|
ctx.effect(() => {
|
|
379
527
|
const disposers: Array<() => void> = []
|
|
380
528
|
try {
|
|
529
|
+
// The goal acceptance gate: the OFFICIAL tool pre-execution lifecycle,
|
|
530
|
+
// installed before the tool body runs, so a completion claim cannot take
|
|
531
|
+
// effect without a matching acceptance pass record.
|
|
532
|
+
const gate = createGoalVerificationGate({
|
|
533
|
+
ledger: host.ledger,
|
|
534
|
+
llm: () => resolveLlmRuntime(ctx),
|
|
535
|
+
goals: () => {
|
|
536
|
+
try {
|
|
537
|
+
return ctx.get('goals') as GoalFace | undefined
|
|
538
|
+
} catch {
|
|
539
|
+
return undefined
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
// The host's own observation of what a turn changed on disk, when this
|
|
543
|
+
// deployment records it; the acceptance degrades to the trajectory alone
|
|
544
|
+
// when it does not.
|
|
545
|
+
workspaceChanges: () => probeWorkspaceChanges(ctx),
|
|
546
|
+
logger: {
|
|
547
|
+
warn: (message: string, ...rest: unknown[]) => {
|
|
548
|
+
const logger = (ctx as { logger?: { warn?: (...args: unknown[]) => void } }).logger
|
|
549
|
+
if (typeof logger?.warn === 'function') logger.warn(message, ...rest)
|
|
550
|
+
else console.warn(message, ...rest)
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
})
|
|
554
|
+
// A capture-only context may implement no listener registry (it returns
|
|
555
|
+
// no disposer), so the handle is pushed only when it really is one.
|
|
556
|
+
const offGate = ctx.on('tools/pre-execute', async (exec, next) => {
|
|
557
|
+
const decision = await gate({ name: exec.name, arguments: exec.arguments, agent: exec.agent, signal: exec.signal })
|
|
558
|
+
return decision ?? next()
|
|
559
|
+
})
|
|
560
|
+
if (typeof offGate === 'function') disposers.push(offGate)
|
|
561
|
+
else if (typeof (offGate as { dispose?: unknown } | undefined)?.dispose === 'function') {
|
|
562
|
+
const handle = offGate as { dispose(): void }
|
|
563
|
+
disposers.push(() => { handle.dispose() })
|
|
564
|
+
}
|
|
381
565
|
const routes = makeTaskBoardRoutes(host, resolveProxyAccess(config), {
|
|
382
566
|
parseTask: async (request, signal) => {
|
|
383
567
|
const llm = resolveLlmRuntime(ctx)
|
|
@@ -426,7 +610,11 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
426
610
|
disposeSection = ctx.systemPrompt.section({
|
|
427
611
|
name: 'plugin:task-board',
|
|
428
612
|
order: SECTION_ORDER,
|
|
429
|
-
|
|
613
|
+
// The announcement names the task_board_* tools, so it renders only while
|
|
614
|
+
// at least one of them is reachable: a runtime that serves no tool
|
|
615
|
+
// registry, or a restriction that withholds them, must not leave guidance
|
|
616
|
+
// telling the model to call tools this session cannot see.
|
|
617
|
+
text: visibleToolText(lookupOf(resolveToolRegistry(ctx)), TASK_BOARD_TOOL_NAMES, TASK_BOARD_GUIDANCE),
|
|
430
618
|
})
|
|
431
619
|
}
|
|
432
620
|
|
package/src/protocol.ts
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
import type { TaskUpdatePatch } from './core/use-cases/task-update.ts'
|
|
2
|
-
import { isTaskPermission, isTaskStatus, isTaskTagList, type NewTaskInput, type TaskPermission, type TaskRecord, type TaskStatus } from './core/tasks.ts'
|
|
2
|
+
import { TAG_NAME_MAX_LENGTH, isTaskPermission, isTaskStatus, isTaskTagList, type NewTaskInput, type TaskPermission, type TaskRecord, type TaskStatus } from './core/tasks.ts'
|
|
3
3
|
import { parseLedger } from './core/store.ts'
|
|
4
|
+
import { isValidTimeZone } from './core/schedule.ts'
|
|
4
5
|
import { sanitizeFreezeSnapshot, type FreezeSnapshot } from './core/freeze-snapshot.ts'
|
|
5
6
|
import { sanitizeHandover, type TaskHandoverInput } from './core/handover.ts'
|
|
7
|
+
import { isTaskBoardExtensionPayload, normalizeTaskIntegrations, type TaskBoardExtensionPayload } from './core/extension.ts'
|
|
8
|
+
import { normalizeVerification, type ModelCatalogView, type VerificationContract, type VerificationSettings } from './core/verification.ts'
|
|
6
9
|
|
|
7
10
|
/** Freeze payload carried by create/update actions after the gate (redacted in place). */
|
|
8
11
|
type FreezePayload = FreezeSnapshot & { redacted?: boolean; frozenBy?: string }
|
|
9
12
|
|
|
10
|
-
export const TASK_BOARD_SCHEMA_VERSION =
|
|
11
|
-
/** Ledger documents written before
|
|
12
|
-
export const TASK_BOARD_LEGACY_SCHEMA_VERSION =
|
|
13
|
+
export const TASK_BOARD_SCHEMA_VERSION = 5 as const
|
|
14
|
+
/** Ledger documents written before v5; loaded once and migrated on startup. */
|
|
15
|
+
export const TASK_BOARD_LEGACY_SCHEMA_VERSION = 4 as const
|
|
16
|
+
/** Ledger documents written before v4; migrated through the v4 normalization too. */
|
|
17
|
+
export const TASK_BOARD_OLDER_SCHEMA_VERSION = 3 as const
|
|
18
|
+
/** Ledger documents written before v3; migrated through every later normalization too. */
|
|
19
|
+
export const TASK_BOARD_OLDEST_SCHEMA_VERSION = 2 as const
|
|
20
|
+
/**
|
|
21
|
+
* Every older generation the loader migrates in place. v5 only ADDS the
|
|
22
|
+
* per-execution acceptance block, so the migration is the normalization pass
|
|
23
|
+
* that re-validates each row; a document from any listed generation upgrades
|
|
24
|
+
* losslessly.
|
|
25
|
+
*/
|
|
26
|
+
export const TASK_BOARD_MIGRATABLE_SCHEMA_VERSIONS: readonly number[] = [
|
|
27
|
+
TASK_BOARD_LEGACY_SCHEMA_VERSION,
|
|
28
|
+
TASK_BOARD_OLDER_SCHEMA_VERSION,
|
|
29
|
+
TASK_BOARD_OLDEST_SCHEMA_VERSION,
|
|
30
|
+
]
|
|
13
31
|
export const TASK_BOARD_API_PREFIX = '/api/task-board'
|
|
14
32
|
|
|
15
33
|
export type PowerPhase = 'disabled' | 'idle' | 'acquiring' | 'active' | 'error' | 'unsupported'
|
|
@@ -50,6 +68,25 @@ export interface TaskBoardSnapshot {
|
|
|
50
68
|
* into team execution (Team Lead session plus one teammate per subtask).
|
|
51
69
|
*/
|
|
52
70
|
teamRunAvailable?: boolean
|
|
71
|
+
/**
|
|
72
|
+
* Read-only summaries published by running extensions, keyed by extension id.
|
|
73
|
+
* The board forwards them verbatim; it never interprets their shape.
|
|
74
|
+
*/
|
|
75
|
+
extensions?: Record<string, unknown>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Body of `GET {TASK_BOARD_API_PREFIX}/verification`: the acceptance settings the
|
|
80
|
+
* card edits, the contract they resolve to right now, and the host model catalog
|
|
81
|
+
* the model and reasoning choices come from. Served through the same loopback /
|
|
82
|
+
* authenticated-proxy guard as the rest of the board API, and it carries no
|
|
83
|
+
* credential material — model routes and level ids only.
|
|
84
|
+
*/
|
|
85
|
+
export interface TaskBoardVerificationOptions {
|
|
86
|
+
settings: VerificationSettings
|
|
87
|
+
/** The RESOLVED contract, so the card can show what the next execution freezes. */
|
|
88
|
+
contract: VerificationContract
|
|
89
|
+
catalog: ModelCatalogView
|
|
53
90
|
}
|
|
54
91
|
|
|
55
92
|
/** SSE event frame: revision/scheduler/power only, never the task list. */
|
|
@@ -101,11 +138,16 @@ export type TaskBoardAction =
|
|
|
101
138
|
| { kind: 'move'; taskId: string; status: TaskStatus }
|
|
102
139
|
| { kind: 'archive'; taskId: string }
|
|
103
140
|
| { kind: 'restore'; taskId: string }
|
|
104
|
-
| { kind: '
|
|
141
|
+
| { kind: 'settle'; taskId: string }
|
|
142
|
+
| { kind: 'set-schedule'; taskId: string; patch: { enabled?: boolean; cron?: string; timeZone?: string | null } }
|
|
105
143
|
| { kind: 'run'; taskId: string }
|
|
106
144
|
| { kind: 'rerun'; taskId: string }
|
|
107
145
|
| { kind: 'confirm-permission'; taskId: string }
|
|
108
146
|
| { kind: 'set-parent'; taskId: string; parentId: string | null }
|
|
147
|
+
/** Ledger-wide label management; see core/use-cases/task-tag.ts. */
|
|
148
|
+
| { kind: 'rename-tag'; from: string; to: string }
|
|
149
|
+
| { kind: 'delete-tag'; name: string }
|
|
150
|
+
| { kind: 'extension-action'; extensionId: string; action: string; taskId?: string; payload?: TaskBoardExtensionPayload }
|
|
109
151
|
|
|
110
152
|
export interface TaskBoardActionEnvelope {
|
|
111
153
|
requestId: string
|
|
@@ -150,10 +192,13 @@ function validImportedKnownFields(value: Record<string, unknown>): boolean {
|
|
|
150
192
|
// must carry a well-formed list or none at all, so a hand-edited export
|
|
151
193
|
// cannot smuggle a malformed tag past the gate.
|
|
152
194
|
if (value.tags !== undefined && !isTaskTagList(value.tags)) return false
|
|
195
|
+
if (value.integrations !== undefined && normalizeTaskIntegrations(value.integrations) === undefined) return false
|
|
153
196
|
if (value.schedule !== undefined) {
|
|
154
197
|
const schedule = record(value.schedule)
|
|
155
198
|
if (schedule === undefined || typeof schedule.enabled !== 'boolean' || typeof schedule.cron !== 'string') return false
|
|
156
199
|
if (!optionalFiniteNumber(schedule.nextRunAt) || !optionalFiniteNumber(schedule.lastTriggeredAt)) return false
|
|
200
|
+
if (schedule.timeZone !== undefined
|
|
201
|
+
&& (typeof schedule.timeZone !== 'string' || !isValidTimeZone(schedule.timeZone))) return false
|
|
157
202
|
}
|
|
158
203
|
if (value.executions !== undefined) {
|
|
159
204
|
if (!Array.isArray(value.executions)) return false
|
|
@@ -166,6 +211,10 @@ function validImportedKnownFields(value: Record<string, unknown>): boolean {
|
|
|
166
211
|
if (execution.initiatedBy !== undefined && typeof execution.initiatedBy !== 'string') return false
|
|
167
212
|
if (execution.frozenBy !== undefined && typeof execution.frozenBy !== 'string') return false
|
|
168
213
|
if (execution.frozenAt !== undefined && typeof execution.frozenAt !== 'number') return false
|
|
214
|
+
// A well-formed acceptance block may be imported for inspection, but the
|
|
215
|
+
// import path DROPS it (see importedTask): a fabricated pass record must
|
|
216
|
+
// never let an imported execution settle as verified.
|
|
217
|
+
if (execution.verification !== undefined && normalizeVerification(execution.verification) === undefined) return false
|
|
169
218
|
}
|
|
170
219
|
}
|
|
171
220
|
return true
|
|
@@ -194,11 +243,14 @@ function importedTask(value: unknown): TaskRecord | undefined {
|
|
|
194
243
|
...(execution.initiatedBy === undefined ? {} : { initiatedBy: execution.initiatedBy }),
|
|
195
244
|
...(execution.frozenAt === undefined ? {} : { frozenAt: execution.frozenAt }),
|
|
196
245
|
...(execution.frozenBy === undefined ? {} : { frozenBy: execution.frozenBy }),
|
|
246
|
+
// 安全门(对抗场景 d):验收报告是 Host 的判定,不接受 import 携带——
|
|
247
|
+
// 一条伪造的通过记录会让导入的执行被当成已验收。
|
|
197
248
|
})),
|
|
198
249
|
...(task.schedule === undefined ? {} : {
|
|
199
250
|
schedule: {
|
|
200
251
|
enabled: task.schedule.enabled,
|
|
201
252
|
cron: task.schedule.cron,
|
|
253
|
+
...(task.schedule.timeZone === undefined ? {} : { timeZone: task.schedule.timeZone }),
|
|
202
254
|
nextRunAt: task.schedule.nextRunAt,
|
|
203
255
|
lastTriggeredAt: task.schedule.lastTriggeredAt,
|
|
204
256
|
},
|
|
@@ -210,10 +262,17 @@ function importedTask(value: unknown): TaskRecord | undefined {
|
|
|
210
262
|
...(task.mode === undefined ? {} : { mode: task.mode }),
|
|
211
263
|
...(task.permission === undefined ? {} : { permission: task.permission }),
|
|
212
264
|
...(task.reuseSession === undefined ? {} : { reuseSession: task.reuseSession }),
|
|
265
|
+
...(task.goalRun === undefined ? {} : { goalRun: task.goalRun }),
|
|
266
|
+
// The per-card acceptance opt-out rides the import like the /goal opt-in:
|
|
267
|
+
// it is a user-chosen execution preference on their own board, not a
|
|
268
|
+
// verdict. The per-execution `verification` block is still stripped below, so
|
|
269
|
+
// no imported card can arrive already accepted.
|
|
270
|
+
...(task.skipVerification === undefined ? {} : { skipVerification: task.skipVerification }),
|
|
213
271
|
...(task.archivedAt === undefined ? {} : { archivedAt: task.archivedAt }),
|
|
214
272
|
...(task.freeze === undefined ? {} : { freeze: task.freeze }),
|
|
215
273
|
...(task.handover === undefined ? {} : { handover: task.handover }),
|
|
216
274
|
...(task.tags === undefined ? {} : { tags: task.tags }),
|
|
275
|
+
...(task.integrations === undefined ? {} : { integrations: task.integrations }),
|
|
217
276
|
// 安全门(对抗场景 b):import 不是人工确认动作,确认戳一律剥除——
|
|
218
277
|
// 高于会话默认权限的绑定经 import 进入后必须重新武装 confirm-permission 门。
|
|
219
278
|
}
|
|
@@ -247,30 +306,41 @@ function handoverPayload(value: unknown): TaskHandoverInput | undefined {
|
|
|
247
306
|
|
|
248
307
|
function createInput(value: unknown): value is NewTaskInput {
|
|
249
308
|
const input = record(value)
|
|
250
|
-
if (input === undefined || !exactKeys(input, ['title', 'description', 'prompt', 'parentId', 'workspaceId', 'mode', 'permission', 'schedule', 'freeze', 'handover', 'model', 'reuseSession', 'teamRun', 'tags'])) return false
|
|
309
|
+
if (input === undefined || !exactKeys(input, ['title', 'description', 'prompt', 'parentId', 'workspaceId', 'mode', 'permission', 'schedule', 'freeze', 'handover', 'model', 'reuseSession', 'teamRun', 'goalRun', 'skipVerification', 'tags', 'integrations'])) return false
|
|
251
310
|
if (input.parentId !== undefined && (typeof input.parentId !== 'string' || input.parentId.trim() === '')) return false
|
|
252
311
|
if (typeof input.title !== 'string' || typeof input.description !== 'string' || typeof input.prompt !== 'string') return false
|
|
253
312
|
if (!optionalString(input.workspaceId) || !optionalString(input.mode) || !optionalString(input.model)) return false
|
|
254
313
|
if (input.reuseSession !== undefined && typeof input.reuseSession !== 'boolean') return false
|
|
255
314
|
if (input.teamRun !== undefined && typeof input.teamRun !== 'boolean') return false
|
|
315
|
+
if (input.goalRun !== undefined && typeof input.goalRun !== 'boolean') return false
|
|
316
|
+
if (input.skipVerification !== undefined && typeof input.skipVerification !== 'boolean') return false
|
|
256
317
|
if (input.permission !== undefined && !isTaskPermission(input.permission)) return false
|
|
257
318
|
if (input.tags !== undefined && !isTaskTagList(input.tags)) return false
|
|
319
|
+
if (input.integrations !== undefined && normalizeTaskIntegrations(input.integrations) === undefined) return false
|
|
258
320
|
if (input.freeze !== undefined && freezePayload(input.freeze) === undefined) return false
|
|
259
321
|
if (input.handover !== undefined && handoverPayload(input.handover) === undefined) return false
|
|
260
322
|
if (input.schedule !== undefined) {
|
|
261
323
|
const schedule = record(input.schedule)
|
|
262
|
-
if (schedule === undefined || !exactKeys(schedule, ['enabled', 'cron'])) return false
|
|
324
|
+
if (schedule === undefined || !exactKeys(schedule, ['enabled', 'cron', 'timeZone'])) return false
|
|
263
325
|
if (typeof schedule.enabled !== 'boolean' || typeof schedule.cron !== 'string') return false
|
|
326
|
+
if (schedule.timeZone !== undefined
|
|
327
|
+
&& (typeof schedule.timeZone !== 'string' || !isValidTimeZone(schedule.timeZone))) return false
|
|
264
328
|
}
|
|
265
329
|
return true
|
|
266
330
|
}
|
|
267
331
|
|
|
268
332
|
function updatePatch(value: unknown): boolean {
|
|
269
333
|
const patch = record(value)
|
|
270
|
-
if (patch === undefined || !exactKeys(patch, ['title', 'description', 'prompt', 'workspaceId', 'mode', 'permission', 'freeze', 'handover', 'model', 'reuseSession', 'teamRun', 'tags'])) return false
|
|
334
|
+
if (patch === undefined || !exactKeys(patch, ['title', 'description', 'prompt', 'workspaceId', 'mode', 'permission', 'freeze', 'handover', 'model', 'reuseSession', 'teamRun', 'goalRun', 'skipVerification', 'tags'])) return false
|
|
271
335
|
// null (or false) clears the reuse opt-in; only a real boolean is accepted.
|
|
272
336
|
if (patch.reuseSession !== undefined && patch.reuseSession !== null && typeof patch.reuseSession !== 'boolean') return false
|
|
273
337
|
if (patch.teamRun !== undefined && patch.teamRun !== null && typeof patch.teamRun !== 'boolean') return false
|
|
338
|
+
// The goal opt-in is tri-state: null/true return the card to its default
|
|
339
|
+
// (goal run), false pins a single plain turn.
|
|
340
|
+
if (patch.goalRun !== undefined && patch.goalRun !== null && typeof patch.goalRun !== 'boolean') return false
|
|
341
|
+
// The acceptance opt-out is the mirror tri-state: null/false return the card
|
|
342
|
+
// to inheriting the board-wide switch, true pins it out of the gate.
|
|
343
|
+
if (patch.skipVerification !== undefined && patch.skipVerification !== null && typeof patch.skipVerification !== 'boolean') return false
|
|
274
344
|
for (const key of ['title', 'description', 'prompt', 'workspaceId', 'mode', 'model'] as const) {
|
|
275
345
|
if (!optionalString(patch[key])) return false
|
|
276
346
|
}
|
|
@@ -283,12 +353,25 @@ function updatePatch(value: unknown): boolean {
|
|
|
283
353
|
return patch.handover === undefined || patch.handover === null || handoverPayload(patch.handover) !== undefined
|
|
284
354
|
}
|
|
285
355
|
|
|
356
|
+
/**
|
|
357
|
+
* A label name on the wire: a non-empty trimmed string within the tag cap. The
|
|
358
|
+
* trim/merge/duplicate rules belong to the use case; this gate only keeps a
|
|
359
|
+
* malformed name off the ledger.
|
|
360
|
+
*/
|
|
361
|
+
function isTagName(value: unknown): value is string {
|
|
362
|
+
return typeof value === 'string' && value.trim() !== '' && value.trim().length <= TAG_NAME_MAX_LENGTH
|
|
363
|
+
}
|
|
364
|
+
|
|
286
365
|
function schedulePatch(value: unknown): boolean {
|
|
287
366
|
const patch = record(value)
|
|
288
367
|
return patch !== undefined
|
|
289
|
-
&& exactKeys(patch, ['enabled', 'cron'])
|
|
368
|
+
&& exactKeys(patch, ['enabled', 'cron', 'timeZone'])
|
|
290
369
|
&& (patch.enabled === undefined || typeof patch.enabled === 'boolean')
|
|
291
370
|
&& (patch.cron === undefined || typeof patch.cron === 'string')
|
|
371
|
+
// `null` clears the stored zone back to the Host zone; an unknown name is
|
|
372
|
+
// rejected here so the Host never has to guess at an unusable zone.
|
|
373
|
+
&& (patch.timeZone === undefined || patch.timeZone === null
|
|
374
|
+
|| (typeof patch.timeZone === 'string' && isValidTimeZone(patch.timeZone)))
|
|
292
375
|
}
|
|
293
376
|
|
|
294
377
|
export function parseActionEnvelope(value: unknown): TaskBoardActionEnvelope | undefined {
|
|
@@ -350,6 +433,16 @@ function parseEnvelopeAction(value: unknown): TaskBoardActionEnvelope | undefine
|
|
|
350
433
|
if (parentId !== null && (typeof parentId !== 'string' || parentId.trim() === '')) return undefined
|
|
351
434
|
return { requestId: envelope.requestId, action: { kind: 'set-parent', taskId, parentId } }
|
|
352
435
|
}
|
|
436
|
+
case 'rename-tag': {
|
|
437
|
+
if (!exactKeys(action, ['kind', 'from', 'to'])) return undefined
|
|
438
|
+
if (!isTagName(action.from) || !isTagName(action.to)) return undefined
|
|
439
|
+
return { requestId: envelope.requestId, action: { kind: 'rename-tag', from: action.from, to: action.to } }
|
|
440
|
+
}
|
|
441
|
+
case 'delete-tag': {
|
|
442
|
+
if (!exactKeys(action, ['kind', 'name'])) return undefined
|
|
443
|
+
if (!isTagName(action.name)) return undefined
|
|
444
|
+
return { requestId: envelope.requestId, action: { kind: 'delete-tag', name: action.name } }
|
|
445
|
+
}
|
|
353
446
|
case 'set-schedule':
|
|
354
447
|
if (!exactKeys(action, ['kind', 'taskId', 'patch'])) return undefined
|
|
355
448
|
return taskId !== undefined && schedulePatch(action.patch)
|
|
@@ -360,10 +453,30 @@ function parseEnvelopeAction(value: unknown): TaskBoardActionEnvelope | undefine
|
|
|
360
453
|
return taskId !== undefined && isTaskStatus(action.status)
|
|
361
454
|
? { requestId: envelope.requestId, action: action as unknown as Extract<TaskBoardAction, { kind: 'move' }> }
|
|
362
455
|
: undefined
|
|
456
|
+
case 'extension-action': {
|
|
457
|
+
// Structural gate only: whether the id names a registered, enabled
|
|
458
|
+
// extension is decided by the host registry, which owns that state.
|
|
459
|
+
if (!exactKeys(action, ['kind', 'extensionId', 'action', 'taskId', 'payload'])) return undefined
|
|
460
|
+
if (typeof action.extensionId !== 'string' || action.extensionId.trim() === '' || action.extensionId.length > 128) return undefined
|
|
461
|
+
if (typeof action.action !== 'string' || action.action.trim() === '' || action.action.length > 128) return undefined
|
|
462
|
+
if (action.taskId !== undefined && (typeof action.taskId !== 'string' || action.taskId === '')) return undefined
|
|
463
|
+
if (action.payload !== undefined && !isTaskBoardExtensionPayload(action.payload)) return undefined
|
|
464
|
+
return {
|
|
465
|
+
requestId: envelope.requestId,
|
|
466
|
+
action: {
|
|
467
|
+
kind: 'extension-action',
|
|
468
|
+
extensionId: action.extensionId,
|
|
469
|
+
action: action.action,
|
|
470
|
+
...(action.taskId === undefined ? {} : { taskId: action.taskId }),
|
|
471
|
+
...(action.payload === undefined ? {} : { payload: action.payload }),
|
|
472
|
+
},
|
|
473
|
+
}
|
|
474
|
+
}
|
|
363
475
|
case 'confirm-permission':
|
|
364
476
|
case 'delete':
|
|
365
477
|
case 'archive':
|
|
366
478
|
case 'restore':
|
|
479
|
+
case 'settle':
|
|
367
480
|
case 'run':
|
|
368
481
|
case 'rerun':
|
|
369
482
|
if (!exactKeys(action, ['kind', 'taskId'])) return undefined
|