dsh-kingdom 2.0.0 → 3.2.0

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 (78) hide show
  1. package/ASSETS_NOTICE.md +22 -22
  2. package/README.md +246 -243
  3. package/THIRD_PARTY_NOTICES.md +92 -92
  4. package/docs/DSH-KINGDOM-3.0-ANNOUNCEMENT.md +13 -0
  5. package/docs/V2.0-RELEASE-NOTES.md +30 -30
  6. package/docs/V2.0-USER-GUIDE.md +74 -74
  7. package/docs/V3.0-RELEASE-NOTES.md +21 -0
  8. package/docs/V3.0-USER-GUIDE.md +33 -0
  9. package/docs/V3.1-RELEASE-NOTES.md +76 -0
  10. package/docs/V3.2-RELEASE-NOTES.md +103 -0
  11. package/lib/core/binding.js +167 -60
  12. package/lib/core/binding.js.map +1 -1
  13. package/lib/core/db.js +582 -546
  14. package/lib/core/db.js.map +1 -1
  15. package/lib/core/delivery-ack.js +1356 -0
  16. package/lib/core/delivery-ack.js.map +1 -0
  17. package/lib/core/delivery-change.js +1237 -0
  18. package/lib/core/delivery-change.js.map +1 -0
  19. package/lib/core/owner-binding-intent.js +14 -25
  20. package/lib/core/owner-binding-intent.js.map +1 -1
  21. package/lib/core/owner-window.js +698 -23
  22. package/lib/core/owner-window.js.map +1 -1
  23. package/lib/core/task-service.js +144 -5
  24. package/lib/core/task-service.js.map +1 -1
  25. package/lib/core/territory.js +43 -9
  26. package/lib/core/territory.js.map +1 -1
  27. package/lib/gui/assets/characters/chancellor-idle.svg +67 -67
  28. package/lib/gui/assets/characters/chancellor-sleeping.svg +67 -67
  29. package/lib/gui/assets/characters/chancellor-thinking.svg +67 -67
  30. package/lib/gui/assets/characters/chancellor-working.svg +67 -67
  31. package/lib/gui/assets/characters/knight-redraw-r1-idle.svg +67 -67
  32. package/lib/gui/assets/characters/knight-redraw-r1-sleeping.svg +67 -67
  33. package/lib/gui/assets/characters/knight-redraw-r1-thinking.svg +67 -67
  34. package/lib/gui/assets/characters/knight-redraw-r1-working.svg +67 -67
  35. package/lib/gui/assets/characters/supervisor-idle.svg +67 -67
  36. package/lib/gui/assets/characters/supervisor-sleeping.svg +67 -67
  37. package/lib/gui/assets/characters/supervisor-thinking.svg +67 -67
  38. package/lib/gui/assets/characters/supervisor-working.svg +67 -67
  39. package/lib/gui/console-app.js +7 -1
  40. package/lib/gui/console-app.js.map +1 -1
  41. package/lib/gui/contract.js +6 -0
  42. package/lib/gui/contract.js.map +1 -1
  43. package/lib/gui/control-contract.js +3 -2
  44. package/lib/gui/control-contract.js.map +1 -1
  45. package/lib/gui/owner-control.js +114 -4
  46. package/lib/gui/owner-control.js.map +1 -1
  47. package/lib/gui/owner-ui.js +645 -183
  48. package/lib/gui/owner-ui.js.map +1 -1
  49. package/lib/gui/server.js +109 -109
  50. package/lib/gui/snapshot.js +127 -4
  51. package/lib/gui/snapshot.js.map +1 -1
  52. package/lib/gui/visual-assets.js +37 -21
  53. package/lib/gui/visual-assets.js.map +1 -1
  54. package/lib/gui/visual-system.js +269 -269
  55. package/lib/gui/workbench-ui.js +549 -136
  56. package/lib/gui/workbench-ui.js.map +1 -1
  57. package/lib/gui/workbench.js +133 -0
  58. package/lib/gui/workbench.js.map +1 -1
  59. package/lib/index.js +392 -40
  60. package/lib/index.js.map +1 -1
  61. package/lib/types/core/binding.d.ts +29 -0
  62. package/lib/types/core/db.d.ts +14 -0
  63. package/lib/types/core/delivery-ack.d.ts +688 -0
  64. package/lib/types/core/delivery-change.d.ts +381 -0
  65. package/lib/types/core/owner-binding-intent.d.ts +9 -1
  66. package/lib/types/core/owner-window.d.ts +237 -3
  67. package/lib/types/core/task-service.d.ts +22 -0
  68. package/lib/types/core/territory.d.ts +18 -3
  69. package/lib/types/gui/contract.d.ts +162 -1
  70. package/lib/types/gui/owner-control.d.ts +61 -1
  71. package/lib/types/gui/snapshot.d.ts +8 -0
  72. package/lib/types/gui/visual-assets.d.ts +16 -0
  73. package/lib/types/gui/workbench.d.ts +47 -1
  74. package/lib/types/index.d.ts +60 -1
  75. package/lib/types/worker/governed-executor.d.ts +15 -0
  76. package/lib/worker/governed-executor.js +21 -0
  77. package/lib/worker/governed-executor.js.map +1 -1
  78. package/package.json +80 -75
package/lib/index.js CHANGED
@@ -14,23 +14,78 @@ import { observeDshDispatchUsage } from './adapter/dsh-usage.js';
14
14
  import { ToolDisclosureRuntime, DISCOVERY_TOOL_NAME } from './capability/tool-disclosure.js';
15
15
  import { installRuntimeCostObservers } from './runtime-cost-observer.js';
16
16
  import { setCapabilityCeiling } from './capability/admin.js';
17
- import { bindRole, listBindings, parseExecutionProfile, rebindSession, resolveBinding, setExecutionProfile, unbindRole, } from './core/binding.js';
17
+ import { bindRole, bindSupervisorForTerritory, listBindings, parseExecutionProfile, rebindSession, resolveBinding, setExecutionProfile, unbindRole, } from './core/binding.js';
18
18
  import { createRunnerContextPort } from './core/governed.js';
19
19
  import { forgetRunnerContextPort, getRunnerContextPort, revokeRunnerContextBrokerContext, } from './runner-context-broker.js';
20
20
  import { asExecutionState, isTerminalExecutionState } from './core/execution.js';
21
21
  import { createTerritory, deleteTerritory, listTerritories, setTerritorySupervisor } from './core/territory.js';
22
- import { abortExecution, assignTask, listTasks, pauseExecution, planTask, reclaimOrphanExecutions, resolveGovernedStartSupervisor, resumeExecution, reviewTask, startTask, } from './core/task-service.js';
22
+ import { abortExecution, assignTask, describeTaskChangeCandidates, listTasks, pauseExecution, planTask, reclaimOrphanExecutions, resolveGovernedStartSupervisor, resumeExecution, reviewTask, startTask, } from './core/task-service.js';
23
+ import { DELIVERY_CHANGE_SNAPSHOT_EVENT_TYPE, changeSnapshotEventId, changeSnapshotEventPayload, } from './core/delivery-change.js';
24
+ import { DELIVERY_QUESTION_UNCLAIMED, readDeliveryQuestionInboxForSession, replyToDeliveryQuestion, } from './core/delivery-ack.js';
23
25
  import { buildSnapshot, buildTaskDetail, toEventView } from './gui/snapshot.js';
24
26
  import { startGuiServer } from './gui/server.js';
25
27
  import { DuplicateJsonKeyError, GUI_OWNER_ONLY_HTTP_COMMANDS, GUI_SESSION_COMMANDS, GUI_START_SANDBOX_MODES, parseStrictJsonObject, } from './gui/control-contract.js';
26
28
  import { LOCAL_CONTROL_LAUNCH_PATH, LocalControlManager } from './gui/local-control.js';
27
- import { OwnerLocalControlManager } from './gui/owner-control.js';
29
+ import { OwnerLocalControlManager, validateLaunchAckHint } from './gui/owner-control.js';
28
30
  import { OwnerDecisionController } from './core/owner-window.js';
29
31
  import { resolveWorkerExecution } from './worker/executor-factory.js';
30
32
  import { issueOwnerControlCapability, ownerControlAuth, } from './core/owner-control.js';
31
33
  import { draftOwnerBindingIntent, draftOwnerBindingIntentFromRejectedWrite, serializeOwnerBindingIntentDraft, } from './core/owner-binding-intent.js';
32
34
  export const name = 'dsh-kingdom';
33
35
  export const inject = ['tools', 'commands'];
36
+ /**
37
+ * 解析 direct `/kingdom role.bind|role.session` 的**目标**会话。
38
+ *
39
+ * live Agent 优先;**只有** ABSENT(注册表里没有这条 live Agent)才回退到持久会话
40
+ * 存在性证明。FOREIGN / MULTIPLE / EXPIRED / UNKNOWN 表示身份不一致或无法判定,
41
+ * 一律保持 fail-closed,绝不因为“可能是它”而放行。
42
+ */
43
+ export async function resolveDirectTargetSession(sessionId, seam, probe) {
44
+ if (!exactRuntimeToken(sessionId)) {
45
+ return { ok: false, classification: 'ABSENT', reason: 'session_id 必须是非空 exact token。' };
46
+ }
47
+ const live = validateLiveDirectSession(sessionId, seam);
48
+ if (live.ok) {
49
+ return {
50
+ ok: true,
51
+ classification: 'LIVE',
52
+ sessionId: live.sessionId,
53
+ agent: live.agent,
54
+ session: live.session,
55
+ };
56
+ }
57
+ if (live.classification !== 'ABSENT') {
58
+ return { ok: false, classification: live.classification, reason: live.reason };
59
+ }
60
+ if (probe === null) {
61
+ return {
62
+ ok: false,
63
+ classification: 'ABSENT',
64
+ reason: `${live.reason};宿主未提供持久会话观察通道,无法证明该会话属于本机 DSH。`,
65
+ };
66
+ }
67
+ const durable = await probe.observe(sessionId);
68
+ if (durable.ok) {
69
+ return {
70
+ ok: true,
71
+ classification: 'REGISTERED_UNSTARTED',
72
+ sessionId,
73
+ detail: '该会话已在本机 DSH 持久会话存储中登记,但当前没有 live Agent;席位写入后,该会话真正启动前不能执行。',
74
+ };
75
+ }
76
+ if (durable.notFound) {
77
+ return {
78
+ ok: false,
79
+ classification: 'ABSENT',
80
+ reason: `${live.reason};本机持久会话存储中也没有该 session_id。`,
81
+ };
82
+ }
83
+ return {
84
+ ok: false,
85
+ classification: 'UNKNOWN',
86
+ reason: `${live.reason};持久会话存在性无法判定:${durable.reason}`,
87
+ };
88
+ }
34
89
  function exactRuntimeToken(value) {
35
90
  return typeof value === 'string'
36
91
  && value.length > 0
@@ -240,16 +295,32 @@ export function ensureGuiSetupTerritory(store, input, auth) {
240
295
  throw new Error('错误:setup.basic 创建后找不到 Territory。');
241
296
  return created;
242
297
  }
243
- /** Ensure setup role identity without ever binding a Worker to the activator. */
298
+ /**
299
+ * Ensure setup role identity without ever binding a Worker to the activator.
300
+ *
301
+ * v3.2.0:主管席位必须隶属一个领地,因此这里的 SUPERVISOR 分支走**原子通道**
302
+ * (`bindSupervisorForTerritory`:建席位 + 设主理同一事务),并要求显式 `territoryId`。
303
+ * 缺少领地时由该通道给出明确拒绝,不会先建出一个没有领地的主管席位。
304
+ * 本助手当前只有测试调用(产品入口是 direct Slash / Owner 管理窗口),但它是包的公开导出,
305
+ * 所以同样不放宽不变量。
306
+ */
244
307
  export function ensureGuiSetupBinding(store, input, auth) {
245
308
  const existing = store.getBindingByRole(input.kingdomId, input.roleType);
246
309
  if (!existing) {
247
- const result = bindRole(store, {
248
- kingdomId: input.kingdomId,
249
- roleType: input.roleType,
250
- roleName: input.roleName,
251
- sessionId: input.sessionId,
252
- }, auth);
310
+ const result = input.roleType === 'SUPERVISOR'
311
+ ? bindSupervisorForTerritory(store, {
312
+ kingdomId: input.kingdomId,
313
+ roleType: input.roleType,
314
+ roleName: input.roleName,
315
+ sessionId: input.sessionId,
316
+ territoryId: input.territoryId ?? '',
317
+ }, auth)
318
+ : bindRole(store, {
319
+ kingdomId: input.kingdomId,
320
+ roleType: input.roleType,
321
+ roleName: input.roleName,
322
+ sessionId: input.sessionId,
323
+ }, auth);
253
324
  throwOnGuiSetupFailure(result);
254
325
  const created = store.getBindingByRole(input.kingdomId, input.roleType);
255
326
  if (!created)
@@ -348,10 +419,92 @@ export function apply(ctx, config, dependencies = {}) {
348
419
  + 'GUI 若提供派发/复核/返工按钮,必须显著标注为「本地可信演示权限」。',
349
420
  };
350
421
  const commandContext = (kingdomId, principal) => ({ kingdomId, auth: authView, ...principal ? { principal } : {} });
422
+ /**
423
+ * 改动证据专用上下文:只对**显式改动选择**与候选查看入口强制 session-bound。
424
+ *
425
+ * 默认产品配置是 `declarative`,此时 `commandContext` 只确认存在角色绑定;改动
426
+ * 证据要求「真实、当前、同领地 Supervisor session」,若继续用弱鉴权,任何本地
427
+ * 调用者都能签发「主管确认的改动证据」。这里只把这两个入口升级为 session-bound
428
+ * (身份仍由既有 `resolveTaskSupervisor` 在 Core 内核对),不顺手修改全局 P0-D
429
+ * 鉴权策略,普通 review/ACCEPT 语义完全不变。取不到 ACTIVE session 时不放行。
430
+ */
431
+ const sessionBoundAuthView = {
432
+ mode: 'session-bound',
433
+ trustLevel: 'session-verified',
434
+ note: '命令调用方的 session 必须与角色 binding 的 session_id 一致。',
435
+ };
436
+ const changeEvidenceContext = (kingdomId, exec) => {
437
+ const principal = trustedToolPrincipal(exec);
438
+ return { kingdomId, auth: sessionBoundAuthView, ...principal ? { principal } : {} };
439
+ };
440
+ /**
441
+ * 交付问答专用上下文:与改动证据同款,**默认 declarative 配置下也必须**由真实、
442
+ * 当前、可信的 ACTIVE session 证明主管身份,再由 Core 核对它是否正是该交付的接收者。
443
+ *
444
+ * 绝不从 caller 自报的 session_id 授权:`trustedToolPrincipal` 只接受 DSH Runtime
445
+ * 注册表可核对的 ACTIVE session;取不到时不放行(Core 侧 fail-closed)。
446
+ */
447
+ const deliveryQuestionContext = (kingdomId, exec) => {
448
+ const principal = trustedToolPrincipal(exec);
449
+ return { kingdomId, auth: sessionBoundAuthView, ...principal ? { principal } : {} };
450
+ };
351
451
  const dshRegistrySeam = () => ({
352
452
  agents: ctx.get('agents'),
353
453
  sessions: ctx.get('sessions'),
354
454
  });
455
+ /**
456
+ * v3.1:持久会话存在性证明。DSH 未挂载 `sessionQuery` 时返回 null,
457
+ * 此时目标会话解析退化为原来的 live Agent 单一路径(fail-closed 不变)。
458
+ */
459
+ const durableSessionProbe = () => {
460
+ const query = ctx.get('sessionQuery');
461
+ if (!query || typeof query.observeSession !== 'function')
462
+ return null;
463
+ const observeSession = query.observeSession.bind(query);
464
+ return {
465
+ observe: async (sessionId) => {
466
+ let observation;
467
+ try {
468
+ observation = await observeSession(sessionId, { projectionMode: 'none' });
469
+ }
470
+ catch (error) {
471
+ const code = error?.code;
472
+ if (code === 'SESSION_QUERY_SESSION_NOT_FOUND') {
473
+ return { ok: false, notFound: true, reason: '持久会话存储中没有该 session_id' };
474
+ }
475
+ return {
476
+ ok: false,
477
+ notFound: false,
478
+ reason: error instanceof Error ? error.message : String(error),
479
+ };
480
+ }
481
+ try {
482
+ const header = observation?.header;
483
+ const headerId = header?.id;
484
+ if (typeof headerId !== 'string' || headerId !== sessionId) {
485
+ return {
486
+ ok: false,
487
+ notFound: false,
488
+ reason: 'observation header.id 与请求的 session_id 不一致',
489
+ };
490
+ }
491
+ return { ok: true };
492
+ }
493
+ finally {
494
+ const lease = observation;
495
+ const dispose = lease?.[Symbol.dispose];
496
+ if (typeof dispose === 'function') {
497
+ try {
498
+ dispose.call(observation);
499
+ }
500
+ catch {
501
+ // 观察租约释放失败不影响本次存在性判定
502
+ }
503
+ }
504
+ }
505
+ },
506
+ };
507
+ };
355
508
  const trustedToolPrincipal = (exec) => {
356
509
  const resolved = resolveTrustedToolSession(exec, dshRegistrySeam());
357
510
  return resolved.classification === 'ACTIVE' ? { sessionId: resolved.sessionId } : undefined;
@@ -572,7 +725,12 @@ export function apply(ctx, config, dependencies = {}) {
572
725
  try {
573
726
  result = await runGovernedTask({
574
727
  store, adapter, kingdomId, workerBindingId: worker,
728
+ // R4 范围回退:Runtime 输入保持既有行为——无 workspace 的旧 Territory 仍以
729
+ // `process.cwd()` 作为 createSession 的 cwd,执行/错误路径不变。
575
730
  territoryId: loadedTask.territory_id, cwd: territory.workspace_path ?? process.cwd(),
731
+ // 差异采集**不复用**该回退值:单独传入 Territory 的 canonical workspace;
732
+ // 缺失时采集端如实 `NOT_SUPPORTED`,绝不把插件安装目录当成领地工作区。
733
+ changeWorkspacePath: territory.workspace_path ?? null,
576
734
  taskId: loadedTask.task_id, attemptNo, supervisorBindingId: supervisor.binding.binding_id,
577
735
  grant: parsedGrant.grant, requirementJson: store.getTaskCapabilityRequirement(loadedTask.task_id),
578
736
  sandboxMode,
@@ -656,6 +814,24 @@ export function apply(ctx, config, dependencies = {}) {
656
814
  const outcome = result.terminalOutcome;
657
815
  const summary = result.summary;
658
816
  submitGovernedClaim(store, result.dispatchId, summary);
817
+ // 改动快照事件:只记 task/attempt/result、manifest/hash 与有界元数据。
818
+ // 仓库正文留在专用本地证据目录;这里不写任何正文。
819
+ if (result.changeManifest) {
820
+ const claimRow = store.latestWorkerResult(loadedTask.task_id);
821
+ if (claimRow && claimRow.attempt_no === attemptNo && !store.getEventById(changeSnapshotEventId(kingdomId, loadedTask.task_id, attemptNo, result.executionId))) {
822
+ store.appendEvent({
823
+ event_id: changeSnapshotEventId(kingdomId, loadedTask.task_id, attemptNo, result.executionId),
824
+ kingdom_id: kingdomId,
825
+ event_type: DELIVERY_CHANGE_SNAPSHOT_EVENT_TYPE,
826
+ actor_role: 'SUPERVISOR',
827
+ actor_id: supervisor.binding.binding_id,
828
+ target_type: 'task',
829
+ target_id: loadedTask.task_id,
830
+ payload_json: JSON.stringify(changeSnapshotEventPayload(result.changeManifest, claimRow.result_id)),
831
+ created_at: new Date().toISOString(),
832
+ });
833
+ }
834
+ }
659
835
  await collectDispatchUsage(result.dispatchId);
660
836
  const recoveryNotice = settledLease?.state === 'RECOVERING'
661
837
  ? result.cleanupReceipt.status === 'CONFIRMED'
@@ -1176,6 +1352,8 @@ export function apply(ctx, config, dependencies = {}) {
1176
1352
  decision: { type: 'string', required: true, description: 'ACCEPT / REWORK / FAIL / HANDOFF' },
1177
1353
  reason: { type: 'string', description: '裁定理由;REWORK/FAIL/HANDOFF 必填' },
1178
1354
  to_binding_id: { type: 'string', description: 'HANDOFF 专用:目标 Worker binding id(ACTIVE)' },
1355
+ change_evidence_id: { type: 'string', description: 'ACCEPT 专用:主管显式选择改动证据时的 evidence id(先用 kingdom_delivery_changes 查看候选)' },
1356
+ change_entry_ids: { type: 'array', items: { type: 'string' }, description: 'ACCEPT 专用:显式选择属于本次交付的改动条目 id;与 change_evidence_id 同时给出,普通 ACCEPT 不自动确认全部差异' },
1179
1357
  },
1180
1358
  output: {
1181
1359
  schema: { type: 'string' },
@@ -1185,14 +1363,127 @@ export function apply(ctx, config, dependencies = {}) {
1185
1363
  const kingdomId = requireKingdom();
1186
1364
  if (!kingdomId)
1187
1365
  return '尚未初始化王国。请先 /kingdom init。';
1188
- return reviewTask(store, commandContext(kingdomId, trustedToolPrincipal(exec)), {
1366
+ if (args.change_entry_ids !== undefined && (!Array.isArray(args.change_entry_ids) || args.change_entry_ids.some(id => typeof id !== 'string'))) {
1367
+ return 'INPUT_DENIED [CHANGE_SELECTION_INVALID]: change_entry_ids 必须是字符串数组。';
1368
+ }
1369
+ // 只有显式改动选择才切换到 session-bound 上下文;普通 ACCEPT/REWORK/FAIL/HANDOFF
1370
+ // 继续使用既有(可能 declarative)上下文,语义不变。
1371
+ const selectingChange = args.change_evidence_id !== undefined || args.change_entry_ids !== undefined;
1372
+ const context = selectingChange
1373
+ ? changeEvidenceContext(kingdomId, exec)
1374
+ : commandContext(kingdomId, trustedToolPrincipal(exec));
1375
+ return reviewTask(store, context, {
1189
1376
  taskId: args.task_id,
1190
1377
  decision: args.decision,
1191
1378
  reason: args.reason,
1192
1379
  to_binding_id: args.to_binding_id,
1380
+ change_evidence_id: args.change_evidence_id,
1381
+ change_entry_ids: args.change_entry_ids,
1193
1382
  }).message;
1194
1383
  },
1195
1384
  })), 'dsh-kingdom: review-task tool');
1385
+ ctx.effect(() => ctx.tools.register(defineTool({
1386
+ name: 'kingdom_delivery_changes',
1387
+ description: '只读列出当前 Task/attempt 的有界改动候选(真实 session-bound 主管专用):改动证据来自执行前后有界快照与主管确认,不证明 Git 作者身份。只读,不写任何事实,也不能替代 ACCEPT。',
1388
+ parameters: {
1389
+ task_id: { type: 'string', required: true, description: '当前主管管辖的任务 id' },
1390
+ },
1391
+ output: {
1392
+ schema: { type: 'string' },
1393
+ render: (_args, value) => [{ type: 'text', text: String(value) }],
1394
+ },
1395
+ async execute(args, exec) {
1396
+ const kingdomId = requireKingdom();
1397
+ if (!kingdomId)
1398
+ return '尚未初始化王国。请先 /kingdom init。';
1399
+ const result = describeTaskChangeCandidates(store, changeEvidenceContext(kingdomId, exec), args.task_id);
1400
+ return result.ok ? result.text : result.message;
1401
+ },
1402
+ })), 'dsh-kingdom: delivery change candidates tool');
1403
+ ctx.effect(() => ctx.tools.register(defineTool({
1404
+ name: 'kingdom_delivery_questions',
1405
+ description: '读取**只属于当前 session-bound 主管本人**的交付条目提问(含问题正文与已有回复):按权威 events 账本逐条精确读取,不是有界的最近事件投影。只接受真实、当前、可信的 ACTIVE 主管 session;默认 declarative 配置下同样拒绝。只读,不写任何事实;未回复的问题只表示「待领取」,不代表已通知或已阅读。',
1406
+ parameters: {
1407
+ pending_only: { type: 'boolean', description: 'true 时只列出尚无回复的问题;省略/false 列出全部(含已回复)' },
1408
+ },
1409
+ output: {
1410
+ schema: { type: 'string' },
1411
+ render: (_args, value) => [{ type: 'text', text: String(value) }],
1412
+ },
1413
+ async execute(args, exec) {
1414
+ const kingdomId = requireKingdom();
1415
+ if (!kingdomId)
1416
+ return '尚未初始化王国。请先 /kingdom init。';
1417
+ const principal = trustedToolPrincipal(exec);
1418
+ if (!principal)
1419
+ return 'AUTHZ_DENIED [UNAUTHORIZED_PRINCIPAL]: 无法从 DSH Runtime 证明当前调用方的唯一活动 session。';
1420
+ // 一个 session 可以合法持有多个 ACTIVE SUPERVISOR binding:必须枚举并精确核对
1421
+ // **全部**该 session 可证明的 binding,不能只取第一个而静默遗漏其余绑定的问题。
1422
+ const inbox = readDeliveryQuestionInboxForSession(store, kingdomId, principal.sessionId);
1423
+ if (!inbox.bindings.length)
1424
+ return 'AUTHZ_DENIED [UNAUTHORIZED_PRINCIPAL]: 当前 session 没有匹配的 ACTIVE SUPERVISOR binding。';
1425
+ // 每条问题事实只有一个接收主管,逐 binding 读取再合并因此不会重复;每个 binding
1426
+ // 仍只看到写给自己的问题。旧内容版本与无法重验的问题仍列出,但明确标出:它们不计
1427
+ // 当前待办,也不能被说成「当前可回复」。
1428
+ const entries = inbox.entries.filter(entry => args.pending_only === true
1429
+ ? entry.question.reply === null && entry.question.itemVersion === 'CURRENT' : true);
1430
+ const bindingLabel = inbox.bindings.map(candidate => `${candidate.role_name}(${candidate.binding_id})`).join('、');
1431
+ if (!entries.length) {
1432
+ return args.pending_only === true
1433
+ ? `主管 ${bindingLabel} 当前没有待回复的交付条目提问。`
1434
+ : `主管 ${bindingLabel} 当前没有被提问的交付条目。`;
1435
+ }
1436
+ const lines = [
1437
+ `主管 ${bindingLabel} 的交付条目提问共 ${entries.length} 条${args.pending_only === true ? `(仅当前版待回复)` : ''}。`,
1438
+ '这些正文只由此 session-bound Tool 返回,不进入工作台、快照或通用事件投影。',
1439
+ '回复请用 kingdom_delivery_reply(question_id, reply_text);回复只记一条对话事实,不写知悉、Task/Claim、ACCEPT、Owner acceptance 或发布。',
1440
+ ];
1441
+ for (const entry of entries) {
1442
+ const question = entry.question;
1443
+ const versionLabel = question.itemVersion === 'CURRENT' ? '当前版'
1444
+ : question.itemVersion === 'HISTORICAL' ? '历史版(旧内容版本;不计当前待办,也不能回复)'
1445
+ : '无法重验版本(当前 ACCEPT/证据无法重验该条目;不计当前待办,也不能回复)';
1446
+ lines.push('', `- 问题 ${question.questionId} · ${question.reply ? '已回复' : '待领取'} · ${versionLabel} · ${question.itemLabel || question.itemId}`);
1447
+ lines.push(` 任务:${entry.taskTitle}(${question.taskId})· 领地:${entry.territoryName} · 已接受尝试第 ${question.attemptNo} 次`);
1448
+ lines.push(` 条目:${question.itemId} · 问题内容版本 ${question.contentHash}${question.currentItemContentHash ? ` · 当前内容版本 ${question.currentItemContentHash}` : ''}`);
1449
+ lines.push(` 提问时间:${question.askedAt} · 接收主管:${question.reviewerBindingId}`);
1450
+ lines.push(` 问题:${question.questionText}`);
1451
+ if (question.reply)
1452
+ lines.push(` 回复(${question.reply.repliedAt}):${question.reply.replyText}`);
1453
+ else if (question.itemVersion === 'HISTORICAL')
1454
+ lines.push(' 状态:该问题属于旧内容版本,仅留历史,不能回复。');
1455
+ else if (question.itemVersion === 'UNVERIFIABLE')
1456
+ lines.push(' 状态:当前无法重验该条目的内容版本(例如主管确认的改动证据已丢失或被替换),不能作为当前待办,也不能回复。问题正文仍可读。');
1457
+ else
1458
+ lines.push(` 状态:${DELIVERY_QUESTION_UNCLAIMED};${question.replyState === 'REPLY_ACCESSIBLE' ? '该主管当前仍可回复。' : '该问题当前不可达:' + question.replyState + ',不会改投继任主管。'}`);
1459
+ }
1460
+ return lines.join('\n');
1461
+ },
1462
+ })), 'dsh-kingdom: delivery questions inbox tool');
1463
+ ctx.effect(() => ctx.tools.register(defineTool({
1464
+ name: 'kingdom_delivery_reply',
1465
+ description: '以当前 session-bound 主管身份回复一条写给他的交付条目提问(被提问的只能是接受该交付的主管本人)。一问最多一个当前回复:同一文本重试幂等,冲突回复被拒绝。只写一条对话事实,不写知悉、Task/Claim、主管 ACCEPT、Owner acceptance 或发布,也不自动派发或唤醒。',
1466
+ parameters: {
1467
+ question_id: { type: 'string', required: true, description: '先用 kingdom_delivery_questions 读取到的 question id' },
1468
+ reply_text: { type: 'string', required: true, description: '给 Owner 的回复正文;非空且长度受限,写入前统一脱敏' },
1469
+ },
1470
+ output: {
1471
+ schema: { type: 'string' },
1472
+ render: (_args, value) => [{ type: 'text', text: String(value) }],
1473
+ },
1474
+ async execute(args, exec) {
1475
+ const kingdomId = requireKingdom();
1476
+ if (!kingdomId)
1477
+ return '尚未初始化王国。请先 /kingdom init。';
1478
+ // CHANGE 条目的回复不再在调用边界预先解析证据:Core 会在同一个写锁事务内按
1479
+ // 当前 ACCEPT 引用重新读取并重验 exact evidence/item/hash,证据漂移即 fail-closed。
1480
+ const result = replyToDeliveryQuestion(store, deliveryQuestionContext(kingdomId, exec), {
1481
+ questionId: args.question_id,
1482
+ replyText: args.reply_text,
1483
+ });
1484
+ return result.ok ? result.text : `${result.code}: ${result.message}`;
1485
+ },
1486
+ })), 'dsh-kingdom: delivery question reply tool');
1196
1487
  ctx.effect(() => ctx.tools.register(defineTool({
1197
1488
  name: 'kingdom_list_tasks',
1198
1489
  description: '列出王国任务的真实状态(含尝试次数与最新 Worker Claim 摘要),可按领地/状态过滤',
@@ -1329,13 +1620,24 @@ export function apply(ctx, config, dependencies = {}) {
1329
1620
  validateTargetSession: async ({ sessionId, signal }) => {
1330
1621
  if (signal.aborted)
1331
1622
  return { ok: false, code: 'OWNER_VALIDATION_ABORTED', message: '管理窗口已撤销或校验超时。' };
1332
- const checked = validateLiveDirectSession(sessionId, dshRegistrySeam());
1333
- return checked.ok ? { ok: true } : { ok: false, code: 'OWNER_SESSION_UNAVAILABLE', message: '目标会话不是当前唯一、有效的 DSH 会话;请刷新后重新选择。' };
1623
+ const resolved = await resolveDirectTargetSession(sessionId, dshRegistrySeam(), durableSessionProbe());
1624
+ if (resolved.ok)
1625
+ return { ok: true };
1626
+ return {
1627
+ ok: false,
1628
+ code: 'OWNER_SESSION_UNAVAILABLE',
1629
+ message: resolved.classification === 'ABSENT'
1630
+ ? '目标会话既不是当前有效的 DSH 会话,也不在本机持久会话存储中;请刷新后重新选择。'
1631
+ : '目标会话不是当前唯一、有效的 DSH 会话;请刷新后重新选择。',
1632
+ };
1334
1633
  },
1335
1634
  listTargetSessions: async ({ sessionIds, signal }) => {
1635
+ if (signal.aborted)
1636
+ return [];
1336
1637
  const seam = dshRegistrySeam();
1337
- return signal.aborted ? [] : sessionIds.filter(id => validateLiveDirectSession(id, seam).ok)
1338
- .map(id => ({ id, label: `DSH 会话 ${id}` }));
1638
+ const probe = durableSessionProbe();
1639
+ const resolved = await Promise.all(sessionIds.map(async (id) => ({ id, ok: (await resolveDirectTargetSession(id, seam, probe)).ok })));
1640
+ return resolved.filter(item => item.ok).map(item => ({ id: item.id, label: `DSH 会话 ${item.id}` }));
1339
1641
  },
1340
1642
  validateExecutionProfile: async ({ profile, signal }) => {
1341
1643
  const provider = profile?.provider?.trim() || config.workerProvider;
@@ -1670,16 +1972,26 @@ export function apply(ctx, config, dependencies = {}) {
1670
1972
  return `UNKNOWN/RECOVERY_REQUIRED: Owner operation ${operation} 的写结果不确定;请先 /kingdom status 对账。(${detail})`;
1671
1973
  }
1672
1974
  };
1673
- const validateDirectSessionPayload = (operation, value) => {
1975
+ /**
1976
+ * direct Owner 目标会话校验(v3.1:异步,返回值携带证明强度)。
1977
+ *
1978
+ * `evidence` 只描述“写入当时怎么证明这条会话属于本机 DSH”,会记进事件 payload;
1979
+ * 它既不是 Role Authority,也不承诺该会话将来可执行。
1980
+ */
1981
+ const validateDirectSessionPayload = async (operation, value) => {
1674
1982
  if (value === undefined || value === null)
1675
- return null;
1676
- if (typeof value !== 'string')
1677
- return `INPUT_DENIED [${operation}]: session_id 必须是 string 或 null。`;
1678
- const seam = dshRegistrySeam();
1679
- const live = validateLiveDirectSession(value, seam);
1680
- if (live.ok)
1681
- return null;
1682
- return `INPUT_DENIED [SESSION_${live.classification}]: ${live.reason};direct /kingdom ${operation} 未写入。`;
1983
+ return { ok: true, evidence: null };
1984
+ if (typeof value !== 'string') {
1985
+ return { ok: false, message: `INPUT_DENIED [${operation}]: session_id 必须是 string 或 null。` };
1986
+ }
1987
+ const resolved = await resolveDirectTargetSession(value, dshRegistrySeam(), durableSessionProbe());
1988
+ if (!resolved.ok) {
1989
+ return {
1990
+ ok: false,
1991
+ message: `INPUT_DENIED [SESSION_${resolved.classification}]: ${resolved.reason};direct /kingdom ${operation} 未写入。`,
1992
+ };
1993
+ }
1994
+ return { ok: true, evidence: resolved.classification === 'LIVE' ? 'LIVE_AGENT' : 'DURABLE_SESSION' };
1683
1995
  };
1684
1996
  const ownerAuth = () => ownerControlAuth(issueOwnerControlCapability());
1685
1997
  ctx.effect(() => ctx.commands.register({
@@ -1691,12 +2003,27 @@ export function apply(ctx, config, dependencies = {}) {
1691
2003
  const auth = ownerAuth();
1692
2004
  switch (sub) {
1693
2005
  case 'owner.gui': {
1694
- const parsed = parseJsonEnvelope(rest, ['kingdomId', 'actions', 'scope', 'ttlMs']);
2006
+ // taskHint/itemHint/contentHashHint 只用于在新标签页预选人类刚点的那一条;
2007
+ // 它们不进入 OwnerDecisionInput,也不改变授权含义。
2008
+ const parsed = parseJsonEnvelope(rest, ['kingdomId', 'actions', 'scope', 'ttlMs',
2009
+ 'taskHint', 'itemHint', 'contentHashHint', 'hintAction']);
1695
2010
  if (!parsed.ok)
1696
2011
  return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${parsed.message}` };
2012
+ const envelope = parsed.value;
2013
+ for (const key of ['taskHint', 'itemHint', 'contentHashHint', 'hintAction']) {
2014
+ if (envelope[key] !== undefined && typeof envelope[key] !== 'string') {
2015
+ return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${key} 必须是 string。` };
2016
+ }
2017
+ }
2018
+ const { taskHint, itemHint, contentHashHint, hintAction, ...authorization } = envelope;
2019
+ // 三个 hint 必须同时提供且格式有效;hintAction 只决定兑换后预选知悉还是提问,
2020
+ // 不参与授权。任一不合法都在激活前整体拒绝,不做「丢掉提示照常激活」。
2021
+ const hint = validateLaunchAckHint({ task: taskHint, item: itemHint, contentHash: contentHashHint, action: hintAction });
2022
+ if (!hint.ok)
2023
+ return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${hint.message}` };
1697
2024
  try {
1698
2025
  const runtime = await ensureGuiServer();
1699
- const activation = runtime.ownerControl.activate(issueOwnerControlCapability(), parsed.value);
2026
+ const activation = runtime.ownerControl.activate(issueOwnerControlCapability(), authorization, hint.hint ?? undefined);
1700
2027
  const opened = openLocalConsole(`${runtime.address.origin}${activation.launchPath}?ticket=${encodeURIComponent(activation.launchTicket)}`);
1701
2028
  return {
1702
2029
  kind: opened ? 'success' : 'error',
@@ -1824,20 +2151,43 @@ export function apply(ctx, config, dependencies = {}) {
1824
2151
  return { kind: text.startsWith('UNKNOWN/') || text.startsWith('错误:') ? 'error' : 'success', text };
1825
2152
  }
1826
2153
  case 'role.bind': {
1827
- const parsed = parseJsonEnvelope(rest, ['role_type', 'role_name', 'session_id', 'model_name', 'agent_name', 'session_meta'], ['role_type']);
2154
+ const parsed = parseJsonEnvelope(rest, ['role_type', 'role_name', 'session_id', 'model_name', 'agent_name', 'session_meta', 'territory_id'], ['role_type']);
1828
2155
  if (!parsed.ok || typeof parsed.value.role_type !== 'string')
1829
2156
  return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${parsed.ok ? 'role_type 必须是 string。' : parsed.message}` };
1830
2157
  const value = parsed.value;
1831
- if (value.role_type.toUpperCase() === 'OWNER')
2158
+ const boundRoleType = value.role_type.trim().toUpperCase();
2159
+ if (boundRoleType === 'OWNER')
1832
2160
  return { kind: 'error', text: 'OWNER_CONTROL_REQUIRED: OWNER projection 不得通过 role.bind 改绑为 Session。' };
1833
- for (const key of ['role_name', 'session_id', 'model_name', 'agent_name', 'session_meta']) {
2161
+ for (const key of ['role_name', 'session_id', 'model_name', 'agent_name', 'session_meta', 'territory_id']) {
1834
2162
  if (value[key] !== undefined && typeof value[key] !== 'string')
1835
2163
  return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${key} 必须是 string。` };
1836
2164
  }
1837
- const liveSessionError = validateDirectSessionPayload('role.bind', value.session_id);
1838
- if (liveSessionError)
1839
- return { kind: 'error', text: liveSessionError };
1840
- const text = ownerWrite('role.bind', () => bindRole(store, { kingdomId: requireKingdom(), roleType: value.role_type, roleName: value.role_name, sessionId: value.session_id, modelName: value.model_name, agentName: value.agent_name, sessionMeta: value.session_meta }, auth));
2165
+ const sessionCheck = await validateDirectSessionPayload('role.bind', value.session_id);
2166
+ if (!sessionCheck.ok)
2167
+ return { kind: 'error', text: sessionCheck.message };
2168
+ // v3.2.0(Owner 2026-09-28 口径):领地与主管完全绑定——任命主管必须同时给出领地,
2169
+ // 且「建席位 + 设主理」必须在同一事务里原子完成,不存在"有主管、没领地"的中间态。
2170
+ // 语义校验放在会话校验之后,保证「会话不可信」仍按 SESSION_* 原因码优先暴露。
2171
+ const territoryId = typeof value.territory_id === 'string' ? value.territory_id.trim() : '';
2172
+ if (boundRoleType !== 'SUPERVISOR' && territoryId) {
2173
+ return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: territory_id 只用于任命 SUPERVISOR(收到 ${boundRoleType || '空'});其它角色不隶属领地。` };
2174
+ }
2175
+ if (boundRoleType === 'SUPERVISOR' && !territoryId) {
2176
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: 任命 SUPERVISOR 必须同时给出 territory_id —— 主管不能没有领地(未写入)。' };
2177
+ }
2178
+ const bindParams = {
2179
+ kingdomId: requireKingdom(),
2180
+ roleType: value.role_type,
2181
+ roleName: value.role_name,
2182
+ sessionId: value.session_id,
2183
+ modelName: value.model_name,
2184
+ agentName: value.agent_name,
2185
+ sessionMeta: value.session_meta,
2186
+ sessionEvidence: sessionCheck.evidence,
2187
+ };
2188
+ const text = ownerWrite('role.bind', () => boundRoleType === 'SUPERVISOR'
2189
+ ? bindSupervisorForTerritory(store, { ...bindParams, territoryId }, auth)
2190
+ : bindRole(store, bindParams, auth));
1841
2191
  return { kind: text.startsWith('UNKNOWN/') || text.startsWith('错误:') || text.startsWith('角色 ') && text.includes('已有绑定') ? 'error' : 'success', text };
1842
2192
  }
1843
2193
  case 'role.unbind': {
@@ -1883,10 +2233,10 @@ export function apply(ctx, config, dependencies = {}) {
1883
2233
  if (value[key] !== undefined && value[key] !== null && typeof value[key] !== 'string')
1884
2234
  return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${key} 必须是 string 或 null。` };
1885
2235
  }
1886
- const liveSessionError = validateDirectSessionPayload('role.session', value.session_id);
1887
- if (liveSessionError)
1888
- return { kind: 'error', text: liveSessionError };
1889
- const text = ownerWrite('role.session', () => rebindSession(store, { kingdomId: requireKingdom(), roleType: value.role_type, bindingId: value.binding_id, sessionId: value.session_id, modelName: value.model_name, agentName: value.agent_name, sessionMeta: value.session_meta }, auth));
2236
+ const sessionCheck = await validateDirectSessionPayload('role.session', value.session_id);
2237
+ if (!sessionCheck.ok)
2238
+ return { kind: 'error', text: sessionCheck.message };
2239
+ const text = ownerWrite('role.session', () => rebindSession(store, { kingdomId: requireKingdom(), roleType: value.role_type, bindingId: value.binding_id, sessionId: value.session_id, modelName: value.model_name, agentName: value.agent_name, sessionMeta: value.session_meta, sessionEvidence: sessionCheck.evidence }, auth));
1890
2240
  return { kind: text.startsWith('UNKNOWN/') || text.startsWith('错误:') ? 'error' : 'success', text };
1891
2241
  }
1892
2242
  case 'execution-profile': {
@@ -1918,12 +2268,14 @@ export function apply(ctx, config, dependencies = {}) {
1918
2268
  '/kingdom init 原子初始化/接入;OWNER.session_id 永远为 null',
1919
2269
  '/kingdom status 查看真实状态(只读)',
1920
2270
  '/kingdom owner.gui <严格 JSON> 激活具体动作/资源范围的短期人类管理窗口;/owner 页面提供激活格式说明',
2271
+ 'owner.gui 可选携带非授权的 taskHint/itemHint/contentHashHint(仅用于预选刚点的那一条,不授予权限);工作台条目图标可一键复制该命令',
1921
2272
  '/kingdom ceiling {"ceiling":{"tool:pwsh":true}} | {"clear":true}',
1922
2273
  '/kingdom territory.create {"name":"研发领","workspace_path":"C:/work"}',
1923
2274
  '/kingdom territory.delete {"territory_id":"...","force":false}',
1924
- '/kingdom territory.supervisor {"territory_id":"...","supervisor_binding_id":"..."}',
1925
- '/kingdom role.bind {"role_type":"SUPERVISOR","role_name":"主理","session_id":"真实 DSH session"}',
1926
- '/kingdom role.unbind {"binding_id":"...","reason":"换届"}',
2275
+ '/kingdom territory.supervisor {"territory_id":"...","supervisor_binding_id":"..."}(改派/解除既有主理)',
2276
+ '/kingdom role.bind {"role_type":"SUPERVISOR","role_name":"主理","territory_id":"...","session_id":"真实 DSH session"} ← 主管必须同时指定领地(同一事务原子完成)',
2277
+ '/kingdom role.bind {"role_type":"WORKER","role_name":"执行者"}(非主管角色不带 territory_id)',
2278
+ '/kingdom role.unbind {"binding_id":"...","reason":"换届"}(退任主管会同时解除其领地主理)',
1927
2279
  '/kingdom role.session {"binding_id":"...","session_id":"真实 DSH session"}',
1928
2280
  '/kingdom execution-profile {"binding_id":"...","provider":"spawn","model":"..."}',
1929
2281
  '以上写命令只接受一个 JSON object;unknown key、额外 token、Owner Session 均拒绝。',