dsh-kingdom 2.0.0 → 3.1.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 (67) hide show
  1. package/ASSETS_NOTICE.md +22 -22
  2. package/README.md +244 -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 +72 -0
  10. package/lib/core/binding.js +2 -0
  11. package/lib/core/binding.js.map +1 -1
  12. package/lib/core/db.js +546 -546
  13. package/lib/core/delivery-ack.js +1356 -0
  14. package/lib/core/delivery-ack.js.map +1 -0
  15. package/lib/core/delivery-change.js +1237 -0
  16. package/lib/core/delivery-change.js.map +1 -0
  17. package/lib/core/owner-window.js +592 -11
  18. package/lib/core/owner-window.js.map +1 -1
  19. package/lib/core/task-service.js +144 -5
  20. package/lib/core/task-service.js.map +1 -1
  21. package/lib/gui/assets/characters/chancellor-idle.svg +67 -67
  22. package/lib/gui/assets/characters/chancellor-sleeping.svg +67 -67
  23. package/lib/gui/assets/characters/chancellor-thinking.svg +67 -67
  24. package/lib/gui/assets/characters/chancellor-working.svg +67 -67
  25. package/lib/gui/assets/characters/knight-redraw-r1-idle.svg +67 -67
  26. package/lib/gui/assets/characters/knight-redraw-r1-sleeping.svg +67 -67
  27. package/lib/gui/assets/characters/knight-redraw-r1-thinking.svg +67 -67
  28. package/lib/gui/assets/characters/knight-redraw-r1-working.svg +67 -67
  29. package/lib/gui/assets/characters/supervisor-idle.svg +67 -67
  30. package/lib/gui/assets/characters/supervisor-sleeping.svg +67 -67
  31. package/lib/gui/assets/characters/supervisor-thinking.svg +67 -67
  32. package/lib/gui/assets/characters/supervisor-working.svg +67 -67
  33. package/lib/gui/console-app.js +7 -1
  34. package/lib/gui/console-app.js.map +1 -1
  35. package/lib/gui/contract.js +6 -0
  36. package/lib/gui/contract.js.map +1 -1
  37. package/lib/gui/owner-control.js +114 -4
  38. package/lib/gui/owner-control.js.map +1 -1
  39. package/lib/gui/owner-ui.js +604 -183
  40. package/lib/gui/owner-ui.js.map +1 -1
  41. package/lib/gui/server.js +109 -109
  42. package/lib/gui/snapshot.js +127 -4
  43. package/lib/gui/snapshot.js.map +1 -1
  44. package/lib/gui/visual-assets.js +37 -21
  45. package/lib/gui/visual-assets.js.map +1 -1
  46. package/lib/gui/visual-system.js +269 -269
  47. package/lib/gui/workbench-ui.js +537 -136
  48. package/lib/gui/workbench-ui.js.map +1 -1
  49. package/lib/gui/workbench.js +123 -0
  50. package/lib/gui/workbench.js.map +1 -1
  51. package/lib/index.js +338 -26
  52. package/lib/index.js.map +1 -1
  53. package/lib/types/core/binding.d.ts +15 -0
  54. package/lib/types/core/delivery-ack.d.ts +688 -0
  55. package/lib/types/core/delivery-change.d.ts +381 -0
  56. package/lib/types/core/owner-window.d.ts +235 -2
  57. package/lib/types/core/task-service.d.ts +22 -0
  58. package/lib/types/gui/contract.d.ts +153 -1
  59. package/lib/types/gui/owner-control.d.ts +61 -1
  60. package/lib/types/gui/snapshot.d.ts +8 -0
  61. package/lib/types/gui/visual-assets.d.ts +16 -0
  62. package/lib/types/gui/workbench.d.ts +47 -1
  63. package/lib/types/index.d.ts +49 -0
  64. package/lib/types/worker/governed-executor.d.ts +15 -0
  65. package/lib/worker/governed-executor.js +21 -0
  66. package/lib/worker/governed-executor.js.map +1 -1
  67. package/package.json +79 -75
package/lib/index.js CHANGED
@@ -19,18 +19,73 @@ 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
@@ -348,10 +403,92 @@ export function apply(ctx, config, dependencies = {}) {
348
403
  + 'GUI 若提供派发/复核/返工按钮,必须显著标注为「本地可信演示权限」。',
349
404
  };
350
405
  const commandContext = (kingdomId, principal) => ({ kingdomId, auth: authView, ...principal ? { principal } : {} });
406
+ /**
407
+ * 改动证据专用上下文:只对**显式改动选择**与候选查看入口强制 session-bound。
408
+ *
409
+ * 默认产品配置是 `declarative`,此时 `commandContext` 只确认存在角色绑定;改动
410
+ * 证据要求「真实、当前、同领地 Supervisor session」,若继续用弱鉴权,任何本地
411
+ * 调用者都能签发「主管确认的改动证据」。这里只把这两个入口升级为 session-bound
412
+ * (身份仍由既有 `resolveTaskSupervisor` 在 Core 内核对),不顺手修改全局 P0-D
413
+ * 鉴权策略,普通 review/ACCEPT 语义完全不变。取不到 ACTIVE session 时不放行。
414
+ */
415
+ const sessionBoundAuthView = {
416
+ mode: 'session-bound',
417
+ trustLevel: 'session-verified',
418
+ note: '命令调用方的 session 必须与角色 binding 的 session_id 一致。',
419
+ };
420
+ const changeEvidenceContext = (kingdomId, exec) => {
421
+ const principal = trustedToolPrincipal(exec);
422
+ return { kingdomId, auth: sessionBoundAuthView, ...principal ? { principal } : {} };
423
+ };
424
+ /**
425
+ * 交付问答专用上下文:与改动证据同款,**默认 declarative 配置下也必须**由真实、
426
+ * 当前、可信的 ACTIVE session 证明主管身份,再由 Core 核对它是否正是该交付的接收者。
427
+ *
428
+ * 绝不从 caller 自报的 session_id 授权:`trustedToolPrincipal` 只接受 DSH Runtime
429
+ * 注册表可核对的 ACTIVE session;取不到时不放行(Core 侧 fail-closed)。
430
+ */
431
+ const deliveryQuestionContext = (kingdomId, exec) => {
432
+ const principal = trustedToolPrincipal(exec);
433
+ return { kingdomId, auth: sessionBoundAuthView, ...principal ? { principal } : {} };
434
+ };
351
435
  const dshRegistrySeam = () => ({
352
436
  agents: ctx.get('agents'),
353
437
  sessions: ctx.get('sessions'),
354
438
  });
439
+ /**
440
+ * v3.1:持久会话存在性证明。DSH 未挂载 `sessionQuery` 时返回 null,
441
+ * 此时目标会话解析退化为原来的 live Agent 单一路径(fail-closed 不变)。
442
+ */
443
+ const durableSessionProbe = () => {
444
+ const query = ctx.get('sessionQuery');
445
+ if (!query || typeof query.observeSession !== 'function')
446
+ return null;
447
+ const observeSession = query.observeSession.bind(query);
448
+ return {
449
+ observe: async (sessionId) => {
450
+ let observation;
451
+ try {
452
+ observation = await observeSession(sessionId, { projectionMode: 'none' });
453
+ }
454
+ catch (error) {
455
+ const code = error?.code;
456
+ if (code === 'SESSION_QUERY_SESSION_NOT_FOUND') {
457
+ return { ok: false, notFound: true, reason: '持久会话存储中没有该 session_id' };
458
+ }
459
+ return {
460
+ ok: false,
461
+ notFound: false,
462
+ reason: error instanceof Error ? error.message : String(error),
463
+ };
464
+ }
465
+ try {
466
+ const header = observation?.header;
467
+ const headerId = header?.id;
468
+ if (typeof headerId !== 'string' || headerId !== sessionId) {
469
+ return {
470
+ ok: false,
471
+ notFound: false,
472
+ reason: 'observation header.id 与请求的 session_id 不一致',
473
+ };
474
+ }
475
+ return { ok: true };
476
+ }
477
+ finally {
478
+ const lease = observation;
479
+ const dispose = lease?.[Symbol.dispose];
480
+ if (typeof dispose === 'function') {
481
+ try {
482
+ dispose.call(observation);
483
+ }
484
+ catch {
485
+ // 观察租约释放失败不影响本次存在性判定
486
+ }
487
+ }
488
+ }
489
+ },
490
+ };
491
+ };
355
492
  const trustedToolPrincipal = (exec) => {
356
493
  const resolved = resolveTrustedToolSession(exec, dshRegistrySeam());
357
494
  return resolved.classification === 'ACTIVE' ? { sessionId: resolved.sessionId } : undefined;
@@ -572,7 +709,12 @@ export function apply(ctx, config, dependencies = {}) {
572
709
  try {
573
710
  result = await runGovernedTask({
574
711
  store, adapter, kingdomId, workerBindingId: worker,
712
+ // R4 范围回退:Runtime 输入保持既有行为——无 workspace 的旧 Territory 仍以
713
+ // `process.cwd()` 作为 createSession 的 cwd,执行/错误路径不变。
575
714
  territoryId: loadedTask.territory_id, cwd: territory.workspace_path ?? process.cwd(),
715
+ // 差异采集**不复用**该回退值:单独传入 Territory 的 canonical workspace;
716
+ // 缺失时采集端如实 `NOT_SUPPORTED`,绝不把插件安装目录当成领地工作区。
717
+ changeWorkspacePath: territory.workspace_path ?? null,
576
718
  taskId: loadedTask.task_id, attemptNo, supervisorBindingId: supervisor.binding.binding_id,
577
719
  grant: parsedGrant.grant, requirementJson: store.getTaskCapabilityRequirement(loadedTask.task_id),
578
720
  sandboxMode,
@@ -656,6 +798,24 @@ export function apply(ctx, config, dependencies = {}) {
656
798
  const outcome = result.terminalOutcome;
657
799
  const summary = result.summary;
658
800
  submitGovernedClaim(store, result.dispatchId, summary);
801
+ // 改动快照事件:只记 task/attempt/result、manifest/hash 与有界元数据。
802
+ // 仓库正文留在专用本地证据目录;这里不写任何正文。
803
+ if (result.changeManifest) {
804
+ const claimRow = store.latestWorkerResult(loadedTask.task_id);
805
+ if (claimRow && claimRow.attempt_no === attemptNo && !store.getEventById(changeSnapshotEventId(kingdomId, loadedTask.task_id, attemptNo, result.executionId))) {
806
+ store.appendEvent({
807
+ event_id: changeSnapshotEventId(kingdomId, loadedTask.task_id, attemptNo, result.executionId),
808
+ kingdom_id: kingdomId,
809
+ event_type: DELIVERY_CHANGE_SNAPSHOT_EVENT_TYPE,
810
+ actor_role: 'SUPERVISOR',
811
+ actor_id: supervisor.binding.binding_id,
812
+ target_type: 'task',
813
+ target_id: loadedTask.task_id,
814
+ payload_json: JSON.stringify(changeSnapshotEventPayload(result.changeManifest, claimRow.result_id)),
815
+ created_at: new Date().toISOString(),
816
+ });
817
+ }
818
+ }
659
819
  await collectDispatchUsage(result.dispatchId);
660
820
  const recoveryNotice = settledLease?.state === 'RECOVERING'
661
821
  ? result.cleanupReceipt.status === 'CONFIRMED'
@@ -1176,6 +1336,8 @@ export function apply(ctx, config, dependencies = {}) {
1176
1336
  decision: { type: 'string', required: true, description: 'ACCEPT / REWORK / FAIL / HANDOFF' },
1177
1337
  reason: { type: 'string', description: '裁定理由;REWORK/FAIL/HANDOFF 必填' },
1178
1338
  to_binding_id: { type: 'string', description: 'HANDOFF 专用:目标 Worker binding id(ACTIVE)' },
1339
+ change_evidence_id: { type: 'string', description: 'ACCEPT 专用:主管显式选择改动证据时的 evidence id(先用 kingdom_delivery_changes 查看候选)' },
1340
+ change_entry_ids: { type: 'array', items: { type: 'string' }, description: 'ACCEPT 专用:显式选择属于本次交付的改动条目 id;与 change_evidence_id 同时给出,普通 ACCEPT 不自动确认全部差异' },
1179
1341
  },
1180
1342
  output: {
1181
1343
  schema: { type: 'string' },
@@ -1185,14 +1347,127 @@ export function apply(ctx, config, dependencies = {}) {
1185
1347
  const kingdomId = requireKingdom();
1186
1348
  if (!kingdomId)
1187
1349
  return '尚未初始化王国。请先 /kingdom init。';
1188
- return reviewTask(store, commandContext(kingdomId, trustedToolPrincipal(exec)), {
1350
+ if (args.change_entry_ids !== undefined && (!Array.isArray(args.change_entry_ids) || args.change_entry_ids.some(id => typeof id !== 'string'))) {
1351
+ return 'INPUT_DENIED [CHANGE_SELECTION_INVALID]: change_entry_ids 必须是字符串数组。';
1352
+ }
1353
+ // 只有显式改动选择才切换到 session-bound 上下文;普通 ACCEPT/REWORK/FAIL/HANDOFF
1354
+ // 继续使用既有(可能 declarative)上下文,语义不变。
1355
+ const selectingChange = args.change_evidence_id !== undefined || args.change_entry_ids !== undefined;
1356
+ const context = selectingChange
1357
+ ? changeEvidenceContext(kingdomId, exec)
1358
+ : commandContext(kingdomId, trustedToolPrincipal(exec));
1359
+ return reviewTask(store, context, {
1189
1360
  taskId: args.task_id,
1190
1361
  decision: args.decision,
1191
1362
  reason: args.reason,
1192
1363
  to_binding_id: args.to_binding_id,
1364
+ change_evidence_id: args.change_evidence_id,
1365
+ change_entry_ids: args.change_entry_ids,
1193
1366
  }).message;
1194
1367
  },
1195
1368
  })), 'dsh-kingdom: review-task tool');
1369
+ ctx.effect(() => ctx.tools.register(defineTool({
1370
+ name: 'kingdom_delivery_changes',
1371
+ description: '只读列出当前 Task/attempt 的有界改动候选(真实 session-bound 主管专用):改动证据来自执行前后有界快照与主管确认,不证明 Git 作者身份。只读,不写任何事实,也不能替代 ACCEPT。',
1372
+ parameters: {
1373
+ task_id: { type: 'string', required: true, description: '当前主管管辖的任务 id' },
1374
+ },
1375
+ output: {
1376
+ schema: { type: 'string' },
1377
+ render: (_args, value) => [{ type: 'text', text: String(value) }],
1378
+ },
1379
+ async execute(args, exec) {
1380
+ const kingdomId = requireKingdom();
1381
+ if (!kingdomId)
1382
+ return '尚未初始化王国。请先 /kingdom init。';
1383
+ const result = describeTaskChangeCandidates(store, changeEvidenceContext(kingdomId, exec), args.task_id);
1384
+ return result.ok ? result.text : result.message;
1385
+ },
1386
+ })), 'dsh-kingdom: delivery change candidates tool');
1387
+ ctx.effect(() => ctx.tools.register(defineTool({
1388
+ name: 'kingdom_delivery_questions',
1389
+ description: '读取**只属于当前 session-bound 主管本人**的交付条目提问(含问题正文与已有回复):按权威 events 账本逐条精确读取,不是有界的最近事件投影。只接受真实、当前、可信的 ACTIVE 主管 session;默认 declarative 配置下同样拒绝。只读,不写任何事实;未回复的问题只表示「待领取」,不代表已通知或已阅读。',
1390
+ parameters: {
1391
+ pending_only: { type: 'boolean', description: 'true 时只列出尚无回复的问题;省略/false 列出全部(含已回复)' },
1392
+ },
1393
+ output: {
1394
+ schema: { type: 'string' },
1395
+ render: (_args, value) => [{ type: 'text', text: String(value) }],
1396
+ },
1397
+ async execute(args, exec) {
1398
+ const kingdomId = requireKingdom();
1399
+ if (!kingdomId)
1400
+ return '尚未初始化王国。请先 /kingdom init。';
1401
+ const principal = trustedToolPrincipal(exec);
1402
+ if (!principal)
1403
+ return 'AUTHZ_DENIED [UNAUTHORIZED_PRINCIPAL]: 无法从 DSH Runtime 证明当前调用方的唯一活动 session。';
1404
+ // 一个 session 可以合法持有多个 ACTIVE SUPERVISOR binding:必须枚举并精确核对
1405
+ // **全部**该 session 可证明的 binding,不能只取第一个而静默遗漏其余绑定的问题。
1406
+ const inbox = readDeliveryQuestionInboxForSession(store, kingdomId, principal.sessionId);
1407
+ if (!inbox.bindings.length)
1408
+ return 'AUTHZ_DENIED [UNAUTHORIZED_PRINCIPAL]: 当前 session 没有匹配的 ACTIVE SUPERVISOR binding。';
1409
+ // 每条问题事实只有一个接收主管,逐 binding 读取再合并因此不会重复;每个 binding
1410
+ // 仍只看到写给自己的问题。旧内容版本与无法重验的问题仍列出,但明确标出:它们不计
1411
+ // 当前待办,也不能被说成「当前可回复」。
1412
+ const entries = inbox.entries.filter(entry => args.pending_only === true
1413
+ ? entry.question.reply === null && entry.question.itemVersion === 'CURRENT' : true);
1414
+ const bindingLabel = inbox.bindings.map(candidate => `${candidate.role_name}(${candidate.binding_id})`).join('、');
1415
+ if (!entries.length) {
1416
+ return args.pending_only === true
1417
+ ? `主管 ${bindingLabel} 当前没有待回复的交付条目提问。`
1418
+ : `主管 ${bindingLabel} 当前没有被提问的交付条目。`;
1419
+ }
1420
+ const lines = [
1421
+ `主管 ${bindingLabel} 的交付条目提问共 ${entries.length} 条${args.pending_only === true ? `(仅当前版待回复)` : ''}。`,
1422
+ '这些正文只由此 session-bound Tool 返回,不进入工作台、快照或通用事件投影。',
1423
+ '回复请用 kingdom_delivery_reply(question_id, reply_text);回复只记一条对话事实,不写知悉、Task/Claim、ACCEPT、Owner acceptance 或发布。',
1424
+ ];
1425
+ for (const entry of entries) {
1426
+ const question = entry.question;
1427
+ const versionLabel = question.itemVersion === 'CURRENT' ? '当前版'
1428
+ : question.itemVersion === 'HISTORICAL' ? '历史版(旧内容版本;不计当前待办,也不能回复)'
1429
+ : '无法重验版本(当前 ACCEPT/证据无法重验该条目;不计当前待办,也不能回复)';
1430
+ lines.push('', `- 问题 ${question.questionId} · ${question.reply ? '已回复' : '待领取'} · ${versionLabel} · ${question.itemLabel || question.itemId}`);
1431
+ lines.push(` 任务:${entry.taskTitle}(${question.taskId})· 领地:${entry.territoryName} · 已接受尝试第 ${question.attemptNo} 次`);
1432
+ lines.push(` 条目:${question.itemId} · 问题内容版本 ${question.contentHash}${question.currentItemContentHash ? ` · 当前内容版本 ${question.currentItemContentHash}` : ''}`);
1433
+ lines.push(` 提问时间:${question.askedAt} · 接收主管:${question.reviewerBindingId}`);
1434
+ lines.push(` 问题:${question.questionText}`);
1435
+ if (question.reply)
1436
+ lines.push(` 回复(${question.reply.repliedAt}):${question.reply.replyText}`);
1437
+ else if (question.itemVersion === 'HISTORICAL')
1438
+ lines.push(' 状态:该问题属于旧内容版本,仅留历史,不能回复。');
1439
+ else if (question.itemVersion === 'UNVERIFIABLE')
1440
+ lines.push(' 状态:当前无法重验该条目的内容版本(例如主管确认的改动证据已丢失或被替换),不能作为当前待办,也不能回复。问题正文仍可读。');
1441
+ else
1442
+ lines.push(` 状态:${DELIVERY_QUESTION_UNCLAIMED};${question.replyState === 'REPLY_ACCESSIBLE' ? '该主管当前仍可回复。' : '该问题当前不可达:' + question.replyState + ',不会改投继任主管。'}`);
1443
+ }
1444
+ return lines.join('\n');
1445
+ },
1446
+ })), 'dsh-kingdom: delivery questions inbox tool');
1447
+ ctx.effect(() => ctx.tools.register(defineTool({
1448
+ name: 'kingdom_delivery_reply',
1449
+ description: '以当前 session-bound 主管身份回复一条写给他的交付条目提问(被提问的只能是接受该交付的主管本人)。一问最多一个当前回复:同一文本重试幂等,冲突回复被拒绝。只写一条对话事实,不写知悉、Task/Claim、主管 ACCEPT、Owner acceptance 或发布,也不自动派发或唤醒。',
1450
+ parameters: {
1451
+ question_id: { type: 'string', required: true, description: '先用 kingdom_delivery_questions 读取到的 question id' },
1452
+ reply_text: { type: 'string', required: true, description: '给 Owner 的回复正文;非空且长度受限,写入前统一脱敏' },
1453
+ },
1454
+ output: {
1455
+ schema: { type: 'string' },
1456
+ render: (_args, value) => [{ type: 'text', text: String(value) }],
1457
+ },
1458
+ async execute(args, exec) {
1459
+ const kingdomId = requireKingdom();
1460
+ if (!kingdomId)
1461
+ return '尚未初始化王国。请先 /kingdom init。';
1462
+ // CHANGE 条目的回复不再在调用边界预先解析证据:Core 会在同一个写锁事务内按
1463
+ // 当前 ACCEPT 引用重新读取并重验 exact evidence/item/hash,证据漂移即 fail-closed。
1464
+ const result = replyToDeliveryQuestion(store, deliveryQuestionContext(kingdomId, exec), {
1465
+ questionId: args.question_id,
1466
+ replyText: args.reply_text,
1467
+ });
1468
+ return result.ok ? result.text : `${result.code}: ${result.message}`;
1469
+ },
1470
+ })), 'dsh-kingdom: delivery question reply tool');
1196
1471
  ctx.effect(() => ctx.tools.register(defineTool({
1197
1472
  name: 'kingdom_list_tasks',
1198
1473
  description: '列出王国任务的真实状态(含尝试次数与最新 Worker Claim 摘要),可按领地/状态过滤',
@@ -1329,13 +1604,24 @@ export function apply(ctx, config, dependencies = {}) {
1329
1604
  validateTargetSession: async ({ sessionId, signal }) => {
1330
1605
  if (signal.aborted)
1331
1606
  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 会话;请刷新后重新选择。' };
1607
+ const resolved = await resolveDirectTargetSession(sessionId, dshRegistrySeam(), durableSessionProbe());
1608
+ if (resolved.ok)
1609
+ return { ok: true };
1610
+ return {
1611
+ ok: false,
1612
+ code: 'OWNER_SESSION_UNAVAILABLE',
1613
+ message: resolved.classification === 'ABSENT'
1614
+ ? '目标会话既不是当前有效的 DSH 会话,也不在本机持久会话存储中;请刷新后重新选择。'
1615
+ : '目标会话不是当前唯一、有效的 DSH 会话;请刷新后重新选择。',
1616
+ };
1334
1617
  },
1335
1618
  listTargetSessions: async ({ sessionIds, signal }) => {
1619
+ if (signal.aborted)
1620
+ return [];
1336
1621
  const seam = dshRegistrySeam();
1337
- return signal.aborted ? [] : sessionIds.filter(id => validateLiveDirectSession(id, seam).ok)
1338
- .map(id => ({ id, label: `DSH 会话 ${id}` }));
1622
+ const probe = durableSessionProbe();
1623
+ const resolved = await Promise.all(sessionIds.map(async (id) => ({ id, ok: (await resolveDirectTargetSession(id, seam, probe)).ok })));
1624
+ return resolved.filter(item => item.ok).map(item => ({ id: item.id, label: `DSH 会话 ${item.id}` }));
1339
1625
  },
1340
1626
  validateExecutionProfile: async ({ profile, signal }) => {
1341
1627
  const provider = profile?.provider?.trim() || config.workerProvider;
@@ -1670,16 +1956,26 @@ export function apply(ctx, config, dependencies = {}) {
1670
1956
  return `UNKNOWN/RECOVERY_REQUIRED: Owner operation ${operation} 的写结果不确定;请先 /kingdom status 对账。(${detail})`;
1671
1957
  }
1672
1958
  };
1673
- const validateDirectSessionPayload = (operation, value) => {
1959
+ /**
1960
+ * direct Owner 目标会话校验(v3.1:异步,返回值携带证明强度)。
1961
+ *
1962
+ * `evidence` 只描述“写入当时怎么证明这条会话属于本机 DSH”,会记进事件 payload;
1963
+ * 它既不是 Role Authority,也不承诺该会话将来可执行。
1964
+ */
1965
+ const validateDirectSessionPayload = async (operation, value) => {
1674
1966
  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} 未写入。`;
1967
+ return { ok: true, evidence: null };
1968
+ if (typeof value !== 'string') {
1969
+ return { ok: false, message: `INPUT_DENIED [${operation}]: session_id 必须是 string 或 null。` };
1970
+ }
1971
+ const resolved = await resolveDirectTargetSession(value, dshRegistrySeam(), durableSessionProbe());
1972
+ if (!resolved.ok) {
1973
+ return {
1974
+ ok: false,
1975
+ message: `INPUT_DENIED [SESSION_${resolved.classification}]: ${resolved.reason};direct /kingdom ${operation} 未写入。`,
1976
+ };
1977
+ }
1978
+ return { ok: true, evidence: resolved.classification === 'LIVE' ? 'LIVE_AGENT' : 'DURABLE_SESSION' };
1683
1979
  };
1684
1980
  const ownerAuth = () => ownerControlAuth(issueOwnerControlCapability());
1685
1981
  ctx.effect(() => ctx.commands.register({
@@ -1691,12 +1987,27 @@ export function apply(ctx, config, dependencies = {}) {
1691
1987
  const auth = ownerAuth();
1692
1988
  switch (sub) {
1693
1989
  case 'owner.gui': {
1694
- const parsed = parseJsonEnvelope(rest, ['kingdomId', 'actions', 'scope', 'ttlMs']);
1990
+ // taskHint/itemHint/contentHashHint 只用于在新标签页预选人类刚点的那一条;
1991
+ // 它们不进入 OwnerDecisionInput,也不改变授权含义。
1992
+ const parsed = parseJsonEnvelope(rest, ['kingdomId', 'actions', 'scope', 'ttlMs',
1993
+ 'taskHint', 'itemHint', 'contentHashHint', 'hintAction']);
1695
1994
  if (!parsed.ok)
1696
1995
  return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${parsed.message}` };
1996
+ const envelope = parsed.value;
1997
+ for (const key of ['taskHint', 'itemHint', 'contentHashHint', 'hintAction']) {
1998
+ if (envelope[key] !== undefined && typeof envelope[key] !== 'string') {
1999
+ return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${key} 必须是 string。` };
2000
+ }
2001
+ }
2002
+ const { taskHint, itemHint, contentHashHint, hintAction, ...authorization } = envelope;
2003
+ // 三个 hint 必须同时提供且格式有效;hintAction 只决定兑换后预选知悉还是提问,
2004
+ // 不参与授权。任一不合法都在激活前整体拒绝,不做「丢掉提示照常激活」。
2005
+ const hint = validateLaunchAckHint({ task: taskHint, item: itemHint, contentHash: contentHashHint, action: hintAction });
2006
+ if (!hint.ok)
2007
+ return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${hint.message}` };
1697
2008
  try {
1698
2009
  const runtime = await ensureGuiServer();
1699
- const activation = runtime.ownerControl.activate(issueOwnerControlCapability(), parsed.value);
2010
+ const activation = runtime.ownerControl.activate(issueOwnerControlCapability(), authorization, hint.hint ?? undefined);
1700
2011
  const opened = openLocalConsole(`${runtime.address.origin}${activation.launchPath}?ticket=${encodeURIComponent(activation.launchTicket)}`);
1701
2012
  return {
1702
2013
  kind: opened ? 'success' : 'error',
@@ -1834,10 +2145,10 @@ export function apply(ctx, config, dependencies = {}) {
1834
2145
  if (value[key] !== undefined && typeof value[key] !== 'string')
1835
2146
  return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${key} 必须是 string。` };
1836
2147
  }
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));
2148
+ const sessionCheck = await validateDirectSessionPayload('role.bind', value.session_id);
2149
+ if (!sessionCheck.ok)
2150
+ return { kind: 'error', text: sessionCheck.message };
2151
+ 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, sessionEvidence: sessionCheck.evidence }, auth));
1841
2152
  return { kind: text.startsWith('UNKNOWN/') || text.startsWith('错误:') || text.startsWith('角色 ') && text.includes('已有绑定') ? 'error' : 'success', text };
1842
2153
  }
1843
2154
  case 'role.unbind': {
@@ -1883,10 +2194,10 @@ export function apply(ctx, config, dependencies = {}) {
1883
2194
  if (value[key] !== undefined && value[key] !== null && typeof value[key] !== 'string')
1884
2195
  return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${key} 必须是 string 或 null。` };
1885
2196
  }
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));
2197
+ const sessionCheck = await validateDirectSessionPayload('role.session', value.session_id);
2198
+ if (!sessionCheck.ok)
2199
+ return { kind: 'error', text: sessionCheck.message };
2200
+ 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
2201
  return { kind: text.startsWith('UNKNOWN/') || text.startsWith('错误:') ? 'error' : 'success', text };
1891
2202
  }
1892
2203
  case 'execution-profile': {
@@ -1918,6 +2229,7 @@ export function apply(ctx, config, dependencies = {}) {
1918
2229
  '/kingdom init 原子初始化/接入;OWNER.session_id 永远为 null',
1919
2230
  '/kingdom status 查看真实状态(只读)',
1920
2231
  '/kingdom owner.gui <严格 JSON> 激活具体动作/资源范围的短期人类管理窗口;/owner 页面提供激活格式说明',
2232
+ 'owner.gui 可选携带非授权的 taskHint/itemHint/contentHashHint(仅用于预选刚点的那一条,不授予权限);工作台条目图标可一键复制该命令',
1921
2233
  '/kingdom ceiling {"ceiling":{"tool:pwsh":true}} | {"clear":true}',
1922
2234
  '/kingdom territory.create {"name":"研发领","workspace_path":"C:/work"}',
1923
2235
  '/kingdom territory.delete {"territory_id":"...","force":false}',