dsh-kingdom 0.8.0 → 2.0.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 (162) hide show
  1. package/ASSETS_NOTICE.md +22 -0
  2. package/LICENSE +661 -29
  3. package/NOTICE +9 -0
  4. package/README.md +93 -94
  5. package/THIRD_PARTY_NOTICES.md +92 -0
  6. package/cordis.patch.yml +6 -6
  7. package/docs/V2.0-RELEASE-NOTES.md +30 -0
  8. package/docs/V2.0-USER-GUIDE.md +74 -0
  9. package/lib/adapter/dsh-backend.js +327 -24
  10. package/lib/adapter/dsh-backend.js.map +1 -1
  11. package/lib/adapter/dsh-cost.js +68 -0
  12. package/lib/adapter/dsh-cost.js.map +1 -0
  13. package/lib/adapter/dsh-session-events.js +86 -0
  14. package/lib/adapter/dsh-session-events.js.map +1 -0
  15. package/lib/adapter/dsh-usage.js +159 -0
  16. package/lib/adapter/dsh-usage.js.map +1 -0
  17. package/lib/capability/admin.js +72 -0
  18. package/lib/capability/admin.js.map +1 -0
  19. package/lib/capability/dsh-enforcement.js +143 -34
  20. package/lib/capability/dsh-enforcement.js.map +1 -1
  21. package/lib/capability/resolver.js +21 -8
  22. package/lib/capability/resolver.js.map +1 -1
  23. package/lib/capability/service.js +169 -26
  24. package/lib/capability/service.js.map +1 -1
  25. package/lib/capability/tool-disclosure.js +144 -0
  26. package/lib/capability/tool-disclosure.js.map +1 -0
  27. package/lib/core/binding.js +52 -34
  28. package/lib/core/binding.js.map +1 -1
  29. package/lib/core/budget.js +304 -0
  30. package/lib/core/budget.js.map +1 -0
  31. package/lib/core/collaboration.js +422 -0
  32. package/lib/core/collaboration.js.map +1 -0
  33. package/lib/core/cost.js +212 -0
  34. package/lib/core/cost.js.map +1 -0
  35. package/lib/core/db.js +197 -4
  36. package/lib/core/db.js.map +1 -1
  37. package/lib/core/execution.js +2 -2
  38. package/lib/core/execution.js.map +1 -1
  39. package/lib/core/governed-claim.js +46 -0
  40. package/lib/core/governed-claim.js.map +1 -0
  41. package/lib/core/governed.js +891 -98
  42. package/lib/core/governed.js.map +1 -1
  43. package/lib/core/kingdom.js +78 -70
  44. package/lib/core/kingdom.js.map +1 -1
  45. package/lib/core/owner-binding-intent.js +732 -0
  46. package/lib/core/owner-binding-intent.js.map +1 -0
  47. package/lib/core/owner-control.js +86 -0
  48. package/lib/core/owner-control.js.map +1 -0
  49. package/lib/core/owner-window.js +822 -0
  50. package/lib/core/owner-window.js.map +1 -0
  51. package/lib/core/task-service.js +296 -133
  52. package/lib/core/task-service.js.map +1 -1
  53. package/lib/core/territory.js +44 -14
  54. package/lib/core/territory.js.map +1 -1
  55. package/lib/core/usage.js +39 -0
  56. package/lib/core/usage.js.map +1 -0
  57. package/lib/core/workspace-admission.js +225 -0
  58. package/lib/core/workspace-admission.js.map +1 -0
  59. package/lib/dispatch/evidence.js +5 -2
  60. package/lib/dispatch/evidence.js.map +1 -1
  61. package/lib/dispatch/reconcile.js +3 -9
  62. package/lib/dispatch/reconcile.js.map +1 -1
  63. package/lib/dispatch/service.js +464 -64
  64. package/lib/dispatch/service.js.map +1 -1
  65. package/lib/gui/assets/characters/chancellor-idle.svg +67 -0
  66. package/lib/gui/assets/characters/chancellor-sleeping.svg +67 -0
  67. package/lib/gui/assets/characters/chancellor-thinking.svg +67 -0
  68. package/lib/gui/assets/characters/chancellor-working.svg +67 -0
  69. package/lib/gui/assets/characters/knight-redraw-r1-idle.svg +67 -0
  70. package/lib/gui/assets/characters/knight-redraw-r1-sleeping.svg +67 -0
  71. package/lib/gui/assets/characters/knight-redraw-r1-thinking.svg +67 -0
  72. package/lib/gui/assets/characters/knight-redraw-r1-working.svg +67 -0
  73. package/lib/gui/assets/characters/supervisor-idle.svg +67 -0
  74. package/lib/gui/assets/characters/supervisor-sleeping.svg +67 -0
  75. package/lib/gui/assets/characters/supervisor-thinking.svg +67 -0
  76. package/lib/gui/assets/characters/supervisor-working.svg +67 -0
  77. package/lib/gui/console-app.js +2330 -0
  78. package/lib/gui/console-app.js.map +1 -0
  79. package/lib/gui/contract.js +5 -5
  80. package/lib/gui/contract.js.map +1 -1
  81. package/lib/gui/control-contract.js +232 -0
  82. package/lib/gui/control-contract.js.map +1 -0
  83. package/lib/gui/local-control.js +423 -0
  84. package/lib/gui/local-control.js.map +1 -0
  85. package/lib/gui/owner-control.js +310 -0
  86. package/lib/gui/owner-control.js.map +1 -0
  87. package/lib/gui/owner-ui.js +191 -0
  88. package/lib/gui/owner-ui.js.map +1 -0
  89. package/lib/gui/server.js +445 -35
  90. package/lib/gui/server.js.map +1 -1
  91. package/lib/gui/snapshot.js +1346 -67
  92. package/lib/gui/snapshot.js.map +1 -1
  93. package/lib/gui/visual-assets.js +56 -0
  94. package/lib/gui/visual-assets.js.map +1 -0
  95. package/lib/gui/visual-system.js +272 -0
  96. package/lib/gui/visual-system.js.map +1 -0
  97. package/lib/gui/workbench-ui.js +358 -0
  98. package/lib/gui/workbench-ui.js.map +1 -0
  99. package/lib/gui/workbench.js +163 -0
  100. package/lib/gui/workbench.js.map +1 -0
  101. package/lib/index.js +1384 -361
  102. package/lib/index.js.map +1 -1
  103. package/lib/runner-context-broker.js +1134 -0
  104. package/lib/runner-context-broker.js.map +1 -0
  105. package/lib/runtime-cost-observer.js +145 -0
  106. package/lib/runtime-cost-observer.js.map +1 -0
  107. package/lib/types/adapter/contract.d.ts +61 -1
  108. package/lib/types/adapter/dsh-backend.d.ts +32 -13
  109. package/lib/types/adapter/dsh-cost.d.ts +13 -0
  110. package/lib/types/adapter/dsh-session-events.d.ts +16 -0
  111. package/lib/types/adapter/dsh-usage.d.ts +36 -0
  112. package/lib/types/capability/admin.d.ts +13 -0
  113. package/lib/types/capability/dsh-enforcement.d.ts +11 -7
  114. package/lib/types/capability/service.d.ts +10 -3
  115. package/lib/types/capability/tool-disclosure.d.ts +36 -0
  116. package/lib/types/core/binding.d.ts +19 -10
  117. package/lib/types/core/budget.d.ts +81 -0
  118. package/lib/types/core/collaboration.d.ts +110 -0
  119. package/lib/types/core/cost.d.ts +99 -0
  120. package/lib/types/core/db.d.ts +44 -4
  121. package/lib/types/core/execution.d.ts +1 -1
  122. package/lib/types/core/governed-claim.d.ts +7 -0
  123. package/lib/types/core/governed.d.ts +182 -1
  124. package/lib/types/core/kingdom.d.ts +3 -0
  125. package/lib/types/core/owner-binding-intent.d.ts +176 -0
  126. package/lib/types/core/owner-control.d.ts +53 -0
  127. package/lib/types/core/owner-window.d.ts +226 -0
  128. package/lib/types/core/task-service.d.ts +60 -13
  129. package/lib/types/core/territory.d.ts +9 -2
  130. package/lib/types/core/usage.d.ts +26 -0
  131. package/lib/types/core/workspace-admission.d.ts +43 -0
  132. package/lib/types/dispatch/evidence.d.ts +2 -5
  133. package/lib/types/dispatch/service.d.ts +62 -4
  134. package/lib/types/gui/console-app.d.ts +137 -0
  135. package/lib/types/gui/contract.d.ts +396 -16
  136. package/lib/types/gui/control-contract.d.ts +111 -0
  137. package/lib/types/gui/local-control.d.ts +120 -0
  138. package/lib/types/gui/owner-control.d.ts +45 -0
  139. package/lib/types/gui/owner-ui.d.ts +2 -0
  140. package/lib/types/gui/server.d.ts +17 -3
  141. package/lib/types/gui/snapshot.d.ts +33 -11
  142. package/lib/types/gui/visual-assets.d.ts +27 -0
  143. package/lib/types/gui/visual-system.d.ts +2 -0
  144. package/lib/types/gui/workbench-ui.d.ts +5 -0
  145. package/lib/types/gui/workbench.d.ts +33 -0
  146. package/lib/types/index.d.ts +157 -6
  147. package/lib/types/runner-context-broker.d.ts +119 -0
  148. package/lib/types/runtime-cost-observer.d.ts +22 -0
  149. package/lib/types/worker/collaboration.d.ts +35 -0
  150. package/lib/types/worker/dsh-subagent.d.ts +4 -4
  151. package/lib/types/worker/executor-factory.d.ts +2 -1
  152. package/lib/types/worker/executor.d.ts +8 -3
  153. package/lib/types/worker/governed-executor.d.ts +8 -1
  154. package/lib/worker/collaboration.js +172 -0
  155. package/lib/worker/collaboration.js.map +1 -0
  156. package/lib/worker/dsh-subagent.js +4 -4
  157. package/lib/worker/executor-factory.js.map +1 -1
  158. package/lib/worker/executor.js +7 -2
  159. package/lib/worker/executor.js.map +1 -1
  160. package/lib/worker/governed-executor.js +295 -84
  161. package/lib/worker/governed-executor.js.map +1 -1
  162. package/package.json +23 -8
package/lib/index.js CHANGED
@@ -1,21 +1,156 @@
1
1
  import { defineTool } from '@deepseek-ai/dsh-tools';
2
2
  import z from '@deepseek-ai/schemastery';
3
- import { randomUUID } from 'node:crypto';
3
+ import { spawnSync } from 'node:child_process';
4
4
  import { hostname } from 'node:os';
5
5
  import { KingdomManager } from './core/kingdom.js';
6
6
  import { DshRuntimeAdapter } from './adapter/dsh-backend.js';
7
7
  import { runGovernedTask } from './worker/governed-executor.js';
8
- import { settleAndRelease } from './dispatch/service.js';
9
- import { bindRole, listBindings, rebindSession, setExecutionProfile, unbindRole } from './core/binding.js';
8
+ import { advanceCollaboration } from './worker/collaboration.js';
9
+ import { proposeCollaborationPlan } from './core/collaboration.js';
10
+ import { settleAndRelease, reconcileGovernedDispatch, discardGovernedRecoveryContexts } from './dispatch/service.js';
11
+ import { submitGovernedClaim } from './core/governed-claim.js';
12
+ import { recordDispatchUsage } from './core/usage.js';
13
+ import { observeDshDispatchUsage } from './adapter/dsh-usage.js';
14
+ import { ToolDisclosureRuntime, DISCOVERY_TOOL_NAME } from './capability/tool-disclosure.js';
15
+ import { installRuntimeCostObservers } from './runtime-cost-observer.js';
16
+ import { setCapabilityCeiling } from './capability/admin.js';
17
+ import { bindRole, listBindings, parseExecutionProfile, rebindSession, resolveBinding, setExecutionProfile, unbindRole, } from './core/binding.js';
18
+ import { createRunnerContextPort } from './core/governed.js';
19
+ import { forgetRunnerContextPort, getRunnerContextPort, revokeRunnerContextBrokerContext, } from './runner-context-broker.js';
20
+ import { asExecutionState, isTerminalExecutionState } from './core/execution.js';
10
21
  import { createTerritory, deleteTerritory, listTerritories, setTerritorySupervisor } from './core/territory.js';
11
- import { abortExecution, assignTask, listTasks, pauseExecution, planTask, reclaimOrphanExecutions, resumeExecution, reviewTask, startTask, } from './core/task-service.js';
22
+ import { abortExecution, assignTask, listTasks, pauseExecution, planTask, reclaimOrphanExecutions, resolveGovernedStartSupervisor, resumeExecution, reviewTask, startTask, } from './core/task-service.js';
12
23
  import { buildSnapshot, buildTaskDetail, toEventView } from './gui/snapshot.js';
13
24
  import { startGuiServer } from './gui/server.js';
14
- import { guiWriteGuard } from './gui/contract.js';
25
+ import { DuplicateJsonKeyError, GUI_OWNER_ONLY_HTTP_COMMANDS, GUI_SESSION_COMMANDS, GUI_START_SANDBOX_MODES, parseStrictJsonObject, } from './gui/control-contract.js';
26
+ import { LOCAL_CONTROL_LAUNCH_PATH, LocalControlManager } from './gui/local-control.js';
27
+ import { OwnerLocalControlManager } from './gui/owner-control.js';
28
+ import { OwnerDecisionController } from './core/owner-window.js';
15
29
  import { resolveWorkerExecution } from './worker/executor-factory.js';
30
+ import { issueOwnerControlCapability, ownerControlAuth, } from './core/owner-control.js';
31
+ import { draftOwnerBindingIntent, draftOwnerBindingIntentFromRejectedWrite, serializeOwnerBindingIntentDraft, } from './core/owner-binding-intent.js';
16
32
  export const name = 'dsh-kingdom';
17
33
  export const inject = ['tools', 'commands'];
34
+ function exactRuntimeToken(value) {
35
+ return typeof value === 'string'
36
+ && value.length > 0
37
+ && value === value.trim()
38
+ && !/[\s\u0000-\u001f\u007f]/u.test(value);
39
+ }
40
+ function agentSession(agent) {
41
+ if (typeof agent !== 'object' || agent === null)
42
+ return null;
43
+ const candidate = agent;
44
+ if (typeof candidate.session !== 'object' || candidate.session === null)
45
+ return null;
46
+ const session = candidate.session;
47
+ if (!exactRuntimeToken(candidate.id) || !exactRuntimeToken(session.id))
48
+ return null;
49
+ return { agent: candidate, session, sessionId: candidate.id };
50
+ }
51
+ function failClassification(classification, sessionId, reason) {
52
+ return { classification, sessionId, reason };
53
+ }
54
+ /**
55
+ * Resolve the exact DSH Tool caller. A non-empty session id is only a target
56
+ * reference; it becomes an ACTIVE Principal after every public identity/life
57
+ * check succeeds. In particular, tool arguments are not accepted here.
58
+ */
59
+ export function resolveTrustedToolSession(execution, seam) {
60
+ const target = agentSession(execution?.agent);
61
+ if (!target)
62
+ return failClassification('ABSENT', null, '缺少 exact exec.agent.session.id。');
63
+ const agents = seam.agents;
64
+ const sessions = seam.sessions;
65
+ if (!agents || !sessions || typeof agents.currentInitiator !== 'function'
66
+ || typeof agents.get !== 'function' || typeof sessions.get !== 'function') {
67
+ return failClassification('UNKNOWN', target.sessionId, 'DSH agents/sessions registry seam 不完整。');
68
+ }
69
+ try {
70
+ const initiator = agents.currentInitiator();
71
+ if (initiator !== target.agent) {
72
+ return failClassification('FOREIGN', target.sessionId, 'exec.agent 不是当前 DSH current initiator。');
73
+ }
74
+ if (agents.get(target.sessionId) !== target.agent) {
75
+ return failClassification('FOREIGN', target.sessionId, 'agents.get 未返回同一 live Agent 对象。');
76
+ }
77
+ if (typeof agents.list === 'function') {
78
+ const matches = agents.list().filter(item => agentSession(item)?.sessionId === target.sessionId);
79
+ if (matches.length !== 1) {
80
+ return failClassification('MULTIPLE', target.sessionId, 'live Agent registry 未能证明 target 唯一。');
81
+ }
82
+ }
83
+ if (sessions.get(target.sessionId) !== target.session) {
84
+ return failClassification('FOREIGN', target.sessionId, 'sessions.get 未返回同一 live Session 对象。');
85
+ }
86
+ if (target.agent.id !== target.session.id) {
87
+ return failClassification('FOREIGN', target.sessionId, 'Agent id 与 Session id 不一致。');
88
+ }
89
+ if (target.agent.status === 'expired') {
90
+ return failClassification('EXPIRED', target.sessionId, 'Agent/Session 已过期。');
91
+ }
92
+ if (target.agent.status !== 'running') {
93
+ return failClassification('UNKNOWN', target.sessionId, `Agent status=${String(target.agent.status)} 不是 running。`);
94
+ }
95
+ if (execution?.signal?.aborted !== false) {
96
+ return failClassification(execution?.signal?.aborted === true ? 'ABORTED' : 'UNKNOWN', target.sessionId, execution?.signal?.aborted === true ? 'Tool execution signal 已 aborted。' : '缺少未 aborted 的 Tool execution signal。');
97
+ }
98
+ return {
99
+ classification: 'ACTIVE',
100
+ sessionId: target.sessionId,
101
+ agent: target.agent,
102
+ session: target.session,
103
+ };
104
+ }
105
+ catch (error) {
106
+ return failClassification('UNKNOWN', target.sessionId, `DSH registry lookup 失败:${error instanceof Error ? error.message : String(error)}`);
107
+ }
108
+ }
109
+ /** Validate a direct Owner Slash target without requiring current-initiator scope. */
110
+ export function validateLiveDirectSession(sessionId, seam) {
111
+ if (!exactRuntimeToken(sessionId)) {
112
+ return { ok: false, classification: 'ABSENT', reason: 'session_id 必须是非空 exact token。' };
113
+ }
114
+ const agents = seam.agents;
115
+ const sessions = seam.sessions;
116
+ if (!agents || !sessions || typeof agents.get !== 'function' || typeof sessions.get !== 'function') {
117
+ return { ok: false, classification: 'UNKNOWN', reason: 'DSH agents/sessions registry seam 不完整。' };
118
+ }
119
+ try {
120
+ const agent = agents.get(sessionId);
121
+ const target = agentSession(agent);
122
+ if (!target || target.sessionId !== sessionId || agent !== target.agent) {
123
+ return { ok: false, classification: 'ABSENT', reason: 'session_id 不对应 live Agent。' };
124
+ }
125
+ if (typeof agents.list === 'function') {
126
+ const matches = agents.list().filter(item => agentSession(item)?.sessionId === sessionId);
127
+ if (matches.length !== 1) {
128
+ return { ok: false, classification: 'MULTIPLE', reason: 'live Agent registry 未能证明 session 唯一。' };
129
+ }
130
+ }
131
+ if (sessions.get(sessionId) !== target.session) {
132
+ return { ok: false, classification: 'FOREIGN', reason: 'sessions.get 未返回绑定的 live Session 对象。' };
133
+ }
134
+ if (target.agent.id !== target.session.id) {
135
+ return { ok: false, classification: 'FOREIGN', reason: 'Agent id 与 Session id 不一致。' };
136
+ }
137
+ if (target.agent.status !== 'idle' && target.agent.status !== 'running') {
138
+ return { ok: false, classification: 'EXPIRED', reason: `Agent status=${String(target.agent.status)} 不可作为 live session。` };
139
+ }
140
+ return { ok: true, sessionId, agent: target.agent, session: target.session };
141
+ }
142
+ catch (error) {
143
+ return { ok: false, classification: 'UNKNOWN', reason: `DSH registry lookup 失败:${error instanceof Error ? error.message : String(error)}` };
144
+ }
145
+ }
18
146
  export const Config = z.object({
147
+ toolDisclosure: z.object({
148
+ mode: z.union(['off', 'pilot']).default('off'),
149
+ residentTools: z.array(z.string()).default([]),
150
+ phaseTools: z.array(z.string()).default([]),
151
+ maxResults: z.number().default(3),
152
+ maxResultBytes: z.number().default(12000),
153
+ }).default({ mode: 'off', residentTools: [], phaseTools: [], maxResults: 3, maxResultBytes: 12000 }),
19
154
  kingdomName: z.string().default('My Kingdom'),
20
155
  ownerName: z.string().default(''),
21
156
  /** Worker 执行用的 subagent provider(dsh base bundle 默认注册 spawn / fork)。 */
@@ -25,7 +160,7 @@ export const Config = z.object({
25
160
  * 设为非零值即启用,只绑定 127.0.0.1。
26
161
  */
27
162
  guiPort: z.number().default(0),
28
- /** 可选 bearer token;设置后 GUI 所有请求都要带 Authorization 头。 */
163
+ /** Role GUI 可选 bearer;独立 Owner 路由使用其直接激活的短期凭据,不升级 Role 权限。 */
29
164
  guiToken: z.string().default(''),
30
165
  /** CORS 允许的 Origin 列表;默认放开(服务只绑本机回环)。 */
31
166
  guiAllowOrigins: z.array(z.string()).default(['*']),
@@ -43,7 +178,134 @@ export const Config = z.object({
43
178
  */
44
179
  migrateV4: z.boolean().default(false),
45
180
  });
46
- export function apply(ctx, config) {
181
+ /** Smallest capability fact used by the GUI thin slice's governed path. */
182
+ export const MINIMAL_CAPABILITY_JSON = '{"tool:pwsh":true}';
183
+ // R20 public root surface. These helpers carry transport bootstrap only;
184
+ // Product registration/settlement helpers remain private to this module.
185
+ export { connectRunnerContextBroker, createRunnerContextBrokerLaunch, createRunnerContextBrokerProductLifecycle, } from './runner-context-broker.js';
186
+ const GUI_OWNER_ONLY_COMMAND_SET = new Set(GUI_OWNER_ONLY_HTTP_COMMANDS);
187
+ /**
188
+ * Format the direct GUI activation result without accepting any launch secret
189
+ * as presentation data. The ticket is handed only to the local URL opener;
190
+ * command/done persistence, logger output, and domain events receive this
191
+ * clean result instead.
192
+ */
193
+ export function formatGuiLaunchCommandResult(origin, expiresAt, browserOpened) {
194
+ const cleanUrl = `${origin}${LOCAL_CONTROL_LAUNCH_PATH}`;
195
+ return browserOpened
196
+ ? {
197
+ kind: 'success',
198
+ text: `GUI Control Session 已激活:${cleanUrl};expiresAt=${expiresAt}。已请求在本机浏览器打开 Console。`,
199
+ }
200
+ : {
201
+ kind: 'error',
202
+ text: `GUI Control Session 已激活:${cleanUrl};expiresAt=${expiresAt}。本机浏览器自动打开失败;未输出一次性 launch ticket,请重新执行 /kingdom gui 后重试。`,
203
+ };
204
+ }
205
+ function throwOnGuiSetupFailure(value) {
206
+ if (/^(?:错误:|OWNER_CONTROL_REQUIRED|CONFIG_DENIED|UNKNOWN\/)/u.test(value))
207
+ throw new Error(value);
208
+ }
209
+ /** Reuse an ACTIVE same-name Territory without changing its topology. */
210
+ export function ensureGuiSetupTerritory(store, input, auth) {
211
+ const name = input.name.trim();
212
+ const workspacePath = input.workspacePath?.trim() || null;
213
+ const existing = store.getTerritoryByName(input.kingdomId, name);
214
+ if (existing) {
215
+ if (existing.status !== 'ACTIVE') {
216
+ throw new Error(`错误:setup.basic 不能复用 ${existing.status} 领地「${name}」;不会自动改写拓扑。`);
217
+ }
218
+ if (workspacePath !== null && existing.workspace_path !== workspacePath) {
219
+ throw new Error(`错误:setup.basic 同名领地「${name}」的 workspace 冲突(已有 ${existing.workspace_path ?? '未设置'},请求 ${workspacePath});不会自动改写拓扑。`);
220
+ }
221
+ return existing;
222
+ }
223
+ try {
224
+ const result = createTerritory(store, {
225
+ kingdomId: input.kingdomId,
226
+ name,
227
+ workspacePath: workspacePath ?? undefined,
228
+ }, auth);
229
+ throwOnGuiSetupFailure(result);
230
+ }
231
+ catch (error) {
232
+ const detail = error instanceof Error ? error.message : String(error);
233
+ if (/UNIQUE constraint failed: territories/u.test(detail)) {
234
+ throw new Error(`错误:setup.basic 同名领地「${name}」已有 DELETED/tombstone 记录;不会自动重建或改写拓扑。`);
235
+ }
236
+ throw error;
237
+ }
238
+ const created = store.getTerritoryByName(input.kingdomId, name);
239
+ if (!created)
240
+ throw new Error('错误:setup.basic 创建后找不到 Territory。');
241
+ return created;
242
+ }
243
+ /** Ensure setup role identity without ever binding a Worker to the activator. */
244
+ export function ensureGuiSetupBinding(store, input, auth) {
245
+ const existing = store.getBindingByRole(input.kingdomId, input.roleType);
246
+ 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);
253
+ throwOnGuiSetupFailure(result);
254
+ const created = store.getBindingByRole(input.kingdomId, input.roleType);
255
+ if (!created)
256
+ throw new Error(`错误:setup.basic 创建 ${input.roleType} 后找不到 binding。`);
257
+ return created;
258
+ }
259
+ if ((input.roleType === 'CHANCELLOR' || input.roleType === 'SUPERVISOR')
260
+ && existing.session_id !== input.sessionId) {
261
+ const result = rebindSession(store, {
262
+ kingdomId: input.kingdomId,
263
+ bindingId: existing.binding_id,
264
+ sessionId: input.sessionId,
265
+ }, auth);
266
+ throwOnGuiSetupFailure(result);
267
+ return store.getBindingById(existing.binding_id) ?? existing;
268
+ }
269
+ return existing;
270
+ }
271
+ function sameGuiExecutionProfile(current, desired) {
272
+ return (current?.model ?? null) === (desired.model ?? null)
273
+ && (current?.provider ?? null) === (desired.provider ?? null);
274
+ }
275
+ /** Set the requested Worker profile only when its semantic value changes. */
276
+ export function ensureGuiSetupExecutionProfile(store, input, auth) {
277
+ const binding = store.getBindingById(input.bindingId);
278
+ if (!binding || binding.kingdom_id !== input.kingdomId) {
279
+ throw new Error(`错误:找不到当前王国的 Worker binding ${input.bindingId}。`);
280
+ }
281
+ if (sameGuiExecutionProfile(parseExecutionProfile(binding.execution_profile_json), input.profile)) {
282
+ return null;
283
+ }
284
+ const result = setExecutionProfile(store, {
285
+ kingdomId: input.kingdomId,
286
+ bindingId: input.bindingId,
287
+ profile: input.profile,
288
+ }, auth);
289
+ throwOnGuiSetupFailure(result);
290
+ return result;
291
+ }
292
+ /** Set Territory scope only when the target Supervisor binding changes. */
293
+ export function ensureGuiSetupTerritorySupervisor(store, input, auth) {
294
+ const territory = store.getTerritoryById(input.territoryId);
295
+ if (!territory || territory.kingdom_id !== input.kingdomId) {
296
+ throw new Error(`错误:领地不存在(id=${input.territoryId})。`);
297
+ }
298
+ if (territory.supervisor_binding_id === input.supervisorBindingId)
299
+ return null;
300
+ const result = setTerritorySupervisor(store, {
301
+ kingdomId: input.kingdomId,
302
+ territoryId: input.territoryId,
303
+ supervisorBindingId: input.supervisorBindingId,
304
+ }, auth);
305
+ throwOnGuiSetupFailure(result);
306
+ return result;
307
+ }
308
+ export function apply(ctx, config, dependencies = {}) {
47
309
  // 单一王国管理器(自包含 SQLite;KingdomStore 连接生命周期 = 插件生命周期)
48
310
  const manager = new KingdomManager({
49
311
  kingdomName: config.kingdomName,
@@ -53,6 +315,21 @@ export function apply(ctx, config) {
53
315
  // 卸载/重载时关闭 SQLite 连接(disposer 由 fiber 自动收集执行)
54
316
  ctx.effect(() => () => manager.close());
55
317
  const store = manager.storeHandle;
318
+ const disclosure = new ToolDisclosureRuntime(config.toolDisclosure ?? { mode: 'off' });
319
+ ctx.effect(() => () => disclosure.dispose());
320
+ const costObserver = installRuntimeCostObservers(ctx, store, {
321
+ runtimeInstanceRef: `dsh-${hostname()}`, mode: disclosure.config.mode, disclosureMeta: assembly => disclosure.metadata(assembly),
322
+ });
323
+ ctx.effect(() => () => costObserver.dispose());
324
+ const costRuntime = { toolDisclosureMode: disclosure.config.mode, observerAvailable: costObserver.available };
325
+ if (disclosure.config.mode === 'pilot')
326
+ ctx.effect(() => ctx.tools.register(defineTool({
327
+ name: DISCOVERY_TOOL_NAME,
328
+ description: 'Find currently granted tools by a short name or purpose query. Matching native schemas become visible on the next step; discovery grants no permission.',
329
+ parameters: { query: { type: 'string', required: true, description: 'A short exact tool name or purpose; at most 160 characters.' } },
330
+ output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: String(value) }] },
331
+ async execute(args, exec) { return JSON.stringify(disclosure.find(args, exec)); },
332
+ })), 'dsh-kingdom: authorized tool discovery pilot');
56
333
  const requireKingdom = () => {
57
334
  const kingdom = store.getDefaultKingdom();
58
335
  return kingdom ? kingdom.kingdom_id : null;
@@ -71,57 +348,422 @@ export function apply(ctx, config) {
71
348
  + 'GUI 若提供派发/复核/返工按钮,必须显著标注为「本地可信演示权限」。',
72
349
  };
73
350
  const commandContext = (kingdomId, principal) => ({ kingdomId, auth: authView, ...principal ? { principal } : {} });
351
+ const dshRegistrySeam = () => ({
352
+ agents: ctx.get('agents'),
353
+ sessions: ctx.get('sessions'),
354
+ });
355
+ const trustedToolPrincipal = (exec) => {
356
+ const resolved = resolveTrustedToolSession(exec, dshRegistrySeam());
357
+ return resolved.classification === 'ACTIVE' ? { sessionId: resolved.sessionId } : undefined;
358
+ };
359
+ const targetSessionRef = (exec) => {
360
+ const target = agentSession(exec.agent);
361
+ return target?.sessionId ?? null;
362
+ };
363
+ const ownerBindingIntentContext = (exec) => {
364
+ const kingdomId = requireKingdom();
365
+ const resolved = resolveTrustedToolSession(exec, dshRegistrySeam());
366
+ return {
367
+ // This is only the target reference echoed by the runtime context. The
368
+ // explicit classification below is the only liveness/authority seam.
369
+ target_session_ref: targetSessionRef(exec),
370
+ target_session_classification: resolved.classification,
371
+ kingdom_id: kingdomId,
372
+ role_bindings: kingdomId
373
+ ? store.listBindings(kingdomId).map(binding => ({
374
+ binding_id: binding.binding_id,
375
+ role_type: binding.role_type,
376
+ role_name: binding.role_name,
377
+ session_id: binding.session_id,
378
+ status: binding.status,
379
+ kingdom_id: binding.kingdom_id,
380
+ }))
381
+ : [],
382
+ territories: kingdomId
383
+ ? store.listTerritories(kingdomId).map(territory => ({
384
+ territory_id: territory.territory_id,
385
+ name: territory.name,
386
+ status: territory.status,
387
+ kingdom_id: territory.kingdom_id,
388
+ supervisor_binding_id: territory.supervisor_binding_id,
389
+ }))
390
+ : [],
391
+ };
392
+ };
393
+ /**
394
+ * Public governed start 永远不接受 declarative 权限:调用者必须由 DSH Runtime
395
+ * 提供可验证的 session,再由 Core 将它解析为该领地的 Supervisor binding。
396
+ */
397
+ const governedStartCommandContext = (kingdomId, principal) => ({
398
+ kingdomId,
399
+ auth: {
400
+ mode: 'session-bound',
401
+ trustLevel: 'session-verified',
402
+ note: 'kingdom_start_task_governed 只接受由 DSH Runtime session 证明的 Supervisor 调用。',
403
+ },
404
+ ...principal ? { principal } : {},
405
+ });
74
406
  /**
75
- * v0.4:从工具执行上下文提取「调用方会话」注入 principal。
76
- * 配合 `authMode: session-bound`(requireRole 校验 principal.sessionId 与
77
- * binding.session_id 一致),让角色真正属于某个独立会话可被强制校验;
78
- * declarative 模式下 principal 仅是告知属性,不影响放行。
407
+ * The ticket is a launch-only secret. It may cross the local process into
408
+ * the user's browser, but it must never enter CommandResult.text, logger
409
+ * output, or a Kingdom event. The host has no browser service in its public
410
+ * Cordis surface, so use only the platform's existing URL opener seam.
79
411
  */
80
- const sessionPrincipal = (exec) => {
81
- const id = exec?.agent?.session?.id;
82
- return typeof id === 'string' && id.length > 0 ? { sessionId: id } : undefined;
412
+ const openLocalConsole = dependencies.openLocalConsole ?? ((url) => {
413
+ try {
414
+ if (process.platform === 'win32') {
415
+ const result = spawnSync('cmd.exe', ['/d', '/c', 'start', '', url], {
416
+ stdio: 'ignore',
417
+ windowsHide: true,
418
+ });
419
+ return result.error === undefined && result.status === 0;
420
+ }
421
+ const command = process.platform === 'darwin' ? 'open' : 'xdg-open';
422
+ const result = spawnSync(command, [url], { stdio: 'ignore' });
423
+ return result.error === undefined && result.status === 0;
424
+ }
425
+ catch {
426
+ return false;
427
+ }
428
+ });
429
+ /** Grant 仅表达当前已认证 Supervisor 授予的 capability,绝不承载调用者身份。 */
430
+ const parseSupervisorGrant = (grantJson) => {
431
+ try {
432
+ const parsed = JSON.parse(grantJson);
433
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
434
+ return { ok: false, message: 'grant_json 必须是非空 JSON 对象。' };
435
+ }
436
+ const entries = Object.entries(parsed);
437
+ if (entries.length === 0 || entries.some(([, allowed]) => typeof allowed !== 'boolean')) {
438
+ return { ok: false, message: 'grant_json 必须是非空、value 全为 boolean 的 capability Grant 对象。' };
439
+ }
440
+ return { ok: true, grant: Object.fromEntries(entries) };
441
+ }
442
+ catch {
443
+ return { ok: false, message: 'grant_json 必须是合法 JSON。' };
444
+ }
83
445
  };
84
- // ── init 引导(首次初始化后给出组织方式;重点:角色应绑到独立会话,勿默认代跑)──
85
- // 地址语义(问题修复 v0.4.1):
86
- // · 后端数据网关 = 本插件 HTTP 通道(如 http://127.0.0.1:<guiPort>)——
87
- // 这是**填进前端页面连接框的地址**,不是要打开的页面。
88
- // · 前端页面 = GUI 前端(vite dev http://localhost:5173,或部署站点)——
89
- // 由 GUI 部署方提供,插件不硬编码也不冒充。
90
- const gatewayUrl = config.guiPort > 0 ? `http://127.0.0.1:${config.guiPort}` : '(未开启,可配置 guiPort 后获得)';
91
- const guiLine = `后端数据网关:${gatewayUrl} —— 把它填入 GUI 前端的连接框;前端页面地址由 GUI 部署方提供(本地开发如 vite,或已部署站点),不要把网关地址当页面打开`;
92
- const initGuidance = '\n\n接下来把王国组织起来。**重要:不要把全部角色默认安在当前会话身上——角色应绑定到各自的独立会话**:\n'
93
- + `① 绑定角色到独立会话(推荐):用 kingdom_bind_session 为 CHANCELLOR / SUPERVISOR / WORKER 分别绑定具名 dsh 会话(可顺带填 model_name / agent_name);`
94
- + `配 authMode=session-bound 后,只有被绑定的那个会话才能行使该角色职权。\n`
95
- + '② 快速演示(不推荐长期用):会话纯文字逐步创建 —— 由当前会话代行各角色(角色席位 sessionId 为空,属本地演示兜底)。\n'
96
- + `GUI(可选):${guiLine}`;
446
+ const collectDispatchUsage = async (dispatchId, authorized = () => true) => {
447
+ const dispatch = store.getDispatch(dispatchId);
448
+ if (!dispatch?.runtime_dispatch_ref)
449
+ return;
450
+ // Resolve the persisted exact reference against Runtime-owned agents; do not mint a branded SessionId.
451
+ const agent = ctx.get('agents')?.list().find(candidate => candidate.id === dispatch.session_ref);
452
+ const observed = await observeDshDispatchUsage(agent?.session, dispatch.runtime_dispatch_ref);
453
+ if (!authorized())
454
+ return;
455
+ recordDispatchUsage(store, dispatchId, observed);
456
+ };
457
+ const runGovernedStart = async (args, caller) => {
458
+ const requestedSandboxMode = args.sandboxMode ?? 'workspace-write';
459
+ if (!GUI_START_SANDBOX_MODES.includes(requestedSandboxMode)) {
460
+ return `INPUT_DENIED [INVALID_SANDBOX_MODE]: sandbox_mode 必须是 ${GUI_START_SANDBOX_MODES.join(' / ')} 之一。`;
461
+ }
462
+ const sandboxMode = requestedSandboxMode;
463
+ const kingdomId = requireKingdom();
464
+ if (!kingdomId)
465
+ return '尚未初始化王国。请先 /kingdom init。';
466
+ const principal = caller.kind === 'tool'
467
+ ? trustedToolPrincipal(caller.execution)
468
+ : caller.principal;
469
+ if (!principal) {
470
+ return 'AUTHZ_DENIED [UNAUTHORIZED_PRINCIPAL]: 无法从 DSH Runtime 证明当前调用方的唯一活动 session。';
471
+ }
472
+ // Public governed start 不随全局 authMode 降级:真实 DSH session → Supervisor
473
+ // binding → Territory scope 必须在任何 executor/session/lease 准备前全部通过。
474
+ const supervisor = resolveGovernedStartSupervisor(store, governedStartCommandContext(kingdomId, principal), args.taskId);
475
+ if (!supervisor.ok) {
476
+ return `AUTHZ_DENIED [${supervisor.code}]: ${supervisor.message}`;
477
+ }
478
+ const parsedGrant = parseSupervisorGrant(args.grantJson);
479
+ if (!parsedGrant.ok) {
480
+ return `INPUT_DENIED [INVALID_SUPERVISOR_GRANT]: ${parsedGrant.message}`;
481
+ }
482
+ if (!store.isSchemaV4) {
483
+ return '错误:Schema v4 未迁移(正式 kingdom.db 迁移须经 Formal DB Migration Gate);governed 执行不可用(fail-closed)。';
484
+ }
485
+ const loadedTask = supervisor.task;
486
+ if (loadedTask.status !== 'ASSIGNED' && loadedTask.status !== 'RUNNING') {
487
+ return `错误:任务状态 ${loadedTask.status} 不可 governed 启动(仅 ASSIGNED / REWORK 后的 RUNNING)。`;
488
+ }
489
+ const unsettledGovernedExecution = store.listExecutions(loadedTask.task_id)
490
+ .slice()
491
+ .reverse()
492
+ .find(execution => execution.execution_contract === 'GOVERNED_PERSISTENT'
493
+ && !isTerminalExecutionState(asExecutionState(execution.state)));
494
+ if (unsettledGovernedExecution) {
495
+ const code = unsettledGovernedExecution.state === 'RECOVERING'
496
+ ? 'EXISTING_EXECUTION_RECOVERING'
497
+ : 'EXISTING_EXECUTION_UNSETTLED';
498
+ return `GOVERNED_EXECUTION_DENIED [${code}]: Task ${loadedTask.task_id} 已有未终结的 governed Execution `
499
+ + `${unsettledGovernedExecution.execution_id}(${unsettledGovernedExecution.state})。`
500
+ + '请先完成 terminal/reconcile 对账;未访问 Runtime/Session/Lease,Task 保持不变且不会自动重试。';
501
+ }
502
+ const territory = store.getTerritoryById(loadedTask.territory_id);
503
+ if (!territory)
504
+ return '错误:任务领地缺失。';
505
+ const worker = loadedTask.assigned_binding_id;
506
+ if (!worker)
507
+ return '错误:任务未指派 Worker。';
508
+ const initialCeiling = store.getKingdomCapabilityCeiling(kingdomId);
509
+ const initialProfile = store.getBindingById(worker)?.execution_profile_json;
510
+ const stillAuthorized = () => {
511
+ if (caller.kind === 'gui' && caller.signal?.aborted)
512
+ return false;
513
+ const currentPrincipal = caller.kind === 'tool' ? trustedToolPrincipal(caller.execution) : caller.principal;
514
+ if (!currentPrincipal || currentPrincipal.sessionId !== principal.sessionId)
515
+ return false;
516
+ const checked = resolveGovernedStartSupervisor(store, governedStartCommandContext(kingdomId, currentPrincipal), args.taskId);
517
+ return checked.ok && checked.binding.binding_id === supervisor.binding.binding_id
518
+ && checked.task.assigned_binding_id === worker && checked.task.territory_id === loadedTask.territory_id
519
+ && checked.task.title === loadedTask.title && checked.task.description === loadedTask.description
520
+ && checked.task.acceptance_criteria === loadedTask.acceptance_criteria
521
+ && checked.task.capability_requirement_json === loadedTask.capability_requirement_json
522
+ && store.getTerritoryById(loadedTask.territory_id)?.workspace_path === territory.workspace_path
523
+ && store.getKingdomCapabilityCeiling(kingdomId) === initialCeiling
524
+ && store.getBindingById(worker)?.execution_profile_json === initialProfile;
525
+ };
526
+ const agents = ctx.get('agents');
527
+ if (!agents) {
528
+ return '错误:当前 dsh 组合未提供 agents 服务(Persistent Session 不可用);governed 执行不可用。';
529
+ }
530
+ // Owner V0.8 PRODUCTION-PATH CLOSURE(正式入口 E2E seam):S4 materialize 需要**模块函数** seam
531
+ // (`setSandboxMode = session.append('sandbox/mode')` / `setApprovalPolicy = session.append('approval/policy')`),
532
+ // 而非 ctx 服务对象(SandboxPolicyService/ApprovalService 无这些方法,传服务 → MATERIALIZE_FAILED)。
533
+ // 动态 import(懒解析):失败 → null → materialize 报「缺失」→ DENIED(fail-closed,不崩溃插件)。
534
+ const s4Seam = dependencies.loadS4Policy
535
+ ? await dependencies.loadS4Policy()
536
+ : await (async () => {
537
+ try {
538
+ // 变量 specifier:运行时经 dsh loader 解析(动态 seam;静态类型不做解析校验)
539
+ const sbSpec = '@deepseek-ai/dsh-sandbox-policy';
540
+ const apSpec = '@deepseek-ai/dsh-user-approval';
541
+ const sandbox = (await import(sbSpec));
542
+ const approval = (await import(apSpec));
543
+ return {
544
+ sandboxPolicy: { setSandboxMode: sandbox.setSandboxMode },
545
+ approval: { setApprovalPolicy: approval.setApprovalPolicy },
546
+ };
547
+ }
548
+ catch {
549
+ return { sandboxPolicy: null, approval: null };
550
+ }
551
+ })();
552
+ if (!stillAuthorized())
553
+ return 'AUTHZ_DENIED [AUTHORITY_CHANGED]: 准备期间身份、任务范围或执行配置变化;未访问 Worker Session。';
554
+ const adapter = new DshRuntimeAdapter({
555
+ runtimeInstanceRef: `dsh-${hostname()}`,
556
+ ...(disclosure.config.mode === 'pilot' ? { toolDisclosure: { install: disclosure.install.bind(disclosure) } } : {}),
557
+ provider: config.workerProvider || 'spawn',
558
+ model: null,
559
+ agents,
560
+ permission: ctx.get('permission'),
561
+ sandboxPolicy: s4Seam.sandboxPolicy,
562
+ approval: s4Seam.approval,
563
+ presets: ctx.get('agentPresets'),
564
+ });
565
+ // attempt 编号必须纳入 Lease Ledger(zero-execution 的 gate 拒绝也会消耗 attempt_no,
566
+ // 否则重试会撞 UNIQUE(task_id, attempt_no)——正式入口 E2E 实证)
567
+ const leaseMax = store.listLeases(kingdomId)
568
+ .filter((l) => l.task_id === loadedTask.task_id)
569
+ .reduce((m, l) => Math.max(m, l.attempt_no), 0);
570
+ const attemptNo = Math.max(store.nextAttemptNo(loadedTask.task_id), leaseMax + 1);
571
+ let result;
572
+ try {
573
+ result = await runGovernedTask({
574
+ store, adapter, kingdomId, workerBindingId: worker,
575
+ territoryId: loadedTask.territory_id, cwd: territory.workspace_path ?? process.cwd(),
576
+ taskId: loadedTask.task_id, attemptNo, supervisorBindingId: supervisor.binding.binding_id,
577
+ grant: parsedGrant.grant, requirementJson: store.getTaskCapabilityRequirement(loadedTask.task_id),
578
+ sandboxMode,
579
+ // Owner V0.8 PRODUCTION-PATH CLOSURE A:全局 workerProvider 仅作 provider 回退;
580
+ // Worker 的 provider/model 权威来源 = execution_profile_json(runGovernedTask 内部解析,
581
+ // model 缺失 → fail closed configuration error,不创建 Session、不 dispatch)。
582
+ globalProvider: config.workerProvider || 'spawn',
583
+ stillAuthorized,
584
+ // 真实模型 turn 轮询窗口(正式入口 E2E 实证:默认 100ms×40=4s 太短,turn 未及 terminal):
585
+ // 1s × 60 = 60s;超窗 → fail-closed 返回(进 RECOVERING,由 reconcile 处理)。
586
+ pollIntervalMs: dependencies.governedPolling?.intervalMs ?? 1000,
587
+ maxPolls: dependencies.governedPolling?.maxPolls ?? 60,
588
+ });
589
+ }
590
+ catch (error) {
591
+ const detail = (error instanceof Error ? error.message : String(error)).slice(0, 256);
592
+ const terminalDispatch = store.listDispatches(kingdomId).some(dispatch => dispatch.task_id === loadedTask.task_id
593
+ && dispatch.attempt_no === attemptNo
594
+ && dispatch.state === 'TERMINAL');
595
+ if (terminalDispatch) {
596
+ return `GOVERNED_SETTLEMENT_BLOCKED [UNPROVEN_TERMINAL_RELATION]: ${detail};未写入 Claim/REVIEW。`;
597
+ }
598
+ return `GOVERNED_EXECUTION_DENIED [DISPATCH_EXCEPTION]: ${detail}`;
599
+ }
600
+ if (!result.ok) {
601
+ const observedDispatch = store.listDispatchesForTaskAttempt(loadedTask.task_id, attemptNo)[0];
602
+ if (observedDispatch)
603
+ await collectDispatchUsage(observedDispatch.dispatch_id);
604
+ const prefix = result.reason.startsWith('Capability DENIED') ? 'CAPABILITY_DENIED' : 'GOVERNED_EXECUTION_DENIED';
605
+ if (observedDispatch && store.getDispatch(observedDispatch.dispatch_id)?.state === 'RECOVERING') {
606
+ return `${prefix}: ${result.reason}\n(任务保持 ${loadedTask.status};请由当前 Supervisor 使用 kingdom_reconcile_task 对账原 Dispatch ${observedDispatch.dispatch_id}。对账完成前不得再次启动任务;不会自动降级为 LEGACY_COMPAT。)`;
607
+ }
608
+ return `${prefix}: ${result.reason}\n(任务保持 ${loadedTask.status};请检查 Capability/Runtime 配置后,在同一 governed persistent 入口重试。不会自动降级为 LEGACY_COMPAT。)`;
609
+ }
610
+ // R11 settlement-before-Claim:先从已提交的 canonical
611
+ // Task/Execution/Lease/Dispatch relation 签发 product context,并消费同一
612
+ // opaque handle + version 完成 settlement。若 exact relation、version 或
613
+ // incident 记录无法证明,必须在任何 Claim/Task 写入前 fail-closed。
614
+ const task = store.getTask(loadedTask.task_id);
615
+ if (!task)
616
+ return 'GOVERNED_SETTLEMENT_BLOCKED [RELATION_MISSING]: terminal Task 已消失,未写入 Claim。';
617
+ let settledLease;
618
+ let runnerContext = null;
619
+ try {
620
+ runnerContext = getRunnerContextPort(result.dispatchId)
621
+ ?? createRunnerContextPort(store, { dispatchId: result.dispatchId });
622
+ const activeRunnerContext = runnerContext;
623
+ const terminal = activeRunnerContext.read(activeRunnerContext.handle, activeRunnerContext.initialVersion);
624
+ const settled = activeRunnerContext.settle(activeRunnerContext.handle, terminal.version, (context) => {
625
+ // The callback receives the exact product-minted capability. Do not
626
+ // substitute result.* IDs: the port's bounded view is the relation
627
+ // consumed by settlement and the same handle/version is in-flight.
628
+ if (context.handle !== activeRunnerContext.handle || context.version !== terminal.version) {
629
+ throw new Error('R11 RunnerContext settlement handle/version continuity failed');
630
+ }
631
+ return settleAndRelease(store, context.view.leaseId, result.cleanupReceipt, 'governed-terminal-settlement', {
632
+ adapter,
633
+ fence: result.trustFence,
634
+ leaseId: context.view.leaseId,
635
+ sessionRef: context.view.sessionRef,
636
+ });
637
+ });
638
+ settledLease = settled.value;
639
+ }
640
+ catch (error) {
641
+ const detail = error instanceof Error ? error.message : String(error);
642
+ return `GOVERNED_SETTLEMENT_BLOCKED [UNPROVEN_TERMINAL_RELATION]: ${detail};未写入 Claim/REVIEW。`;
643
+ }
644
+ finally {
645
+ // Ticket/connection revocation is transport cleanup only. It cannot
646
+ // release a Lease or alter Claim/Dispatch governance facts.
647
+ revokeRunnerContextBrokerContext(runnerContext);
648
+ forgetRunnerContextPort(result.dispatchId);
649
+ }
650
+ // Claim 到达(Claim ≠ Fact):结算完成或已明确进入 RECOVERING 后,才落
651
+ // worker_results + Task RUNNING→REVIEW(不是 DONE)。普通 cleanup false/
652
+ // throw/missing 与 post-TX-4 incident 都保留可信 terminal Claim。
653
+ // Owner V0.8 FINAL RELEASE BLOCKER:Claim outcome 按已验证 terminalOutcome 收敛
654
+ // (COMPLETED/FAILED/ABORTED)——禁止 hardcode COMPLETED;FAILED 不得生成 COMPLETED Claim;
655
+ // summary 仅来自真实 assistant 文本,无文本 → 诚实回退占位(不伪造"任务完成"类摘要)。
656
+ const outcome = result.terminalOutcome;
657
+ const summary = result.summary;
658
+ submitGovernedClaim(store, result.dispatchId, summary);
659
+ await collectDispatchUsage(result.dispatchId);
660
+ const recoveryNotice = settledLease?.state === 'RECOVERING'
661
+ ? result.cleanupReceipt.status === 'CONFIRMED'
662
+ ? `\n注意:terminal settlement 未能安全释放(${settledLease.release_reason ?? 'integrity recovery'}),Lease=RECOVERING;不得复用该 Session 或发起新 Dispatch,须先完成 reconcile/恢复。`
663
+ : `\n注意:enforcement cleanup 未确认(${result.cleanupReceipt.status}),Lease=RECOVERING;不得复用该 Session 或发起新 Dispatch,须先完成 reconcile/恢复。`
664
+ : '';
665
+ return `Governed Worker 已提交第 ${attemptNo} 次尝试的 Claim(outcome=${outcome},长期 Session ${result.sessionRef.slice(-8)}${result.created ? ' 新建' : ' 复用'})。\n`
666
+ + `摘要:${summary}\n`
667
+ + `任务「${task.title}」现在处于 **REVIEW**。这是一个待审查的 Claim,尚未成为任务完成事实——请 Supervisor 用 kingdom_review_task 裁定 ACCEPT / REWORK / FAIL。`
668
+ + recoveryNotice;
669
+ };
670
+ // ── init 引导(首次初始化后给出组织方式与内置 GUI 入口)──
671
+ const guiLine = '直接键入 exact `/kingdom gui` 激活短期本地 control session 并打开内置操作台;不用时执行 `/kingdom gui stop`。';
672
+ const initGuidance = '\n\nOwner Control 已建立:Owner 是稳定的人类本机 principal,OWNER.session_id 保持 null。\n'
673
+ + 'Owner-only 初始化、Territory、角色/session、Execution Profile 与 Capability Ceiling 写入必须来自 direct `/kingdom` Slash;GUI/HTTP 只保留 session-bound Role 命令。\n'
674
+ + 'Agent Role Plane 请为 CHANCELLOR / SUPERVISOR / WORKER 使用真实独立 DSH Session;正常执行入口是 kingdom_start_task_governed。\n'
675
+ + `GUI:${guiLine}`;
97
676
  /** 首次初始化返回带引导文案;再次接入保持简洁。 */
98
677
  const initResultText = (result) => result.action === 'initialized' ? `${result.detail}${initGuidance}` : result.detail;
99
- // ── 加载期回收:清掉上一个插件生命周期留下的孤儿 Execution ──────
678
+ const ownerToolDenied = (rejection) => {
679
+ if (!rejection) {
680
+ return 'OWNER_CONTROL_REQUIRED: Owner-only 写操作只能通过 direct `/kingdom` Slash 执行;'
681
+ + 'Agent Tool、未激活的 GUI/HTTP、工具参数和 caller session 不能代表 Owner。';
682
+ }
683
+ return serializeOwnerBindingIntentDraft(draftOwnerBindingIntentFromRejectedWrite({
684
+ code: 'OWNER_CONTROL_REQUIRED',
685
+ operation: rejection.operation,
686
+ request: rejection.request,
687
+ context: ownerBindingIntentContext(rejection.exec),
688
+ }));
689
+ };
690
+ // ── 加载期孤儿处理:按 Execution Contract 保持事实诚实 ──────────
100
691
  //
101
- // Worker 是 one-shot in-process subagent,生命周期绑在插件 fiber 上,
102
- // 不可能跨越重载存活。所以开库时看到的"活跃"执行必然是残骸。
103
- // 不回收的话,GUI 会看到骑士永远在工作,且该任务再也无法重新 start。
692
+ // LEGACY_COMPAT one-shot 随插件生命周期结束,可标记 ABORTED;
693
+ // GOVERNED_PERSISTENT 的 Runtime outcome 在重启后未知,只能进入
694
+ // RECOVERING 并等待显式对账,不能声称 Session 已停止或自动重试。
104
695
  {
105
696
  const kingdomId = requireKingdom();
106
697
  if (kingdomId) {
107
698
  const reclaimed = reclaimOrphanExecutions(store, kingdomId);
108
699
  if (reclaimed > 0) {
109
- ctx.logger.info(`dsh-kingdom:已回收 ${reclaimed} 个上次运行残留的 Execution(标记为 ABORTED)`);
700
+ ctx.logger.info(`dsh-kingdom:已按 Execution Contract 处理 ${reclaimed} 个加载期残留 Execution`);
110
701
  }
111
702
  }
112
703
  }
704
+ ctx.effect(() => () => discardGovernedRecoveryContexts(store));
705
+ const runGovernedReconcile = async (args, caller) => {
706
+ const kingdomId = requireKingdom();
707
+ if (!kingdomId)
708
+ return '尚未初始化王国。';
709
+ const resolveCaller = () => {
710
+ if (caller.kind === 'tool')
711
+ return trustedToolPrincipal(caller.execution);
712
+ if (caller.signal?.aborted)
713
+ return undefined;
714
+ if (!caller.principal.sessionId)
715
+ return undefined;
716
+ // GUI has a broker-owned activation, not an active Tool/current-initiator scope.
717
+ const live = validateLiveDirectSession(caller.principal.sessionId, dshRegistrySeam());
718
+ return live.ok && live.agent === caller.agent ? caller.principal : undefined;
719
+ };
720
+ const initialPrincipal = resolveCaller();
721
+ if (!initialPrincipal)
722
+ return 'AUTHZ_DENIED [UNAUTHORIZED_PRINCIPAL]: 无法证明当前 DSH Session。';
723
+ const authorized = resolveGovernedStartSupervisor(store, governedStartCommandContext(kingdomId, initialPrincipal), args.taskId);
724
+ if (!authorized.ok)
725
+ return `AUTHZ_DENIED [${authorized.code}]: ${authorized.message}`;
726
+ const execution = store.latestExecution(args.taskId);
727
+ const dispatch = args.dispatchId ? store.getDispatch(args.dispatchId) : execution && store.listDispatchesForTaskAttempt(args.taskId, execution.attempt_no)
728
+ .find(row => row.execution_id === execution.execution_id);
729
+ if (!dispatch || dispatch.task_id !== args.taskId || dispatch.kingdom_id !== kingdomId)
730
+ return 'RECOVERY_REQUIRED [DISPATCH_MISSING]: 未找到该任务的执行凭据。';
731
+ const stillAuthorized = () => {
732
+ const principal = resolveCaller();
733
+ if (!principal || principal.sessionId !== initialPrincipal.sessionId)
734
+ return false;
735
+ const current = resolveGovernedStartSupervisor(store, governedStartCommandContext(kingdomId, principal), args.taskId);
736
+ return current.ok && current.binding.binding_id === authorized.binding.binding_id;
737
+ };
738
+ const reconciled = await reconcileGovernedDispatch(store, dispatch.dispatch_id, stillAuthorized);
739
+ if (!stillAuthorized())
740
+ return 'AUTHZ_DENIED [AUTHORITY_REVOKED]: 对账期间授权已失效,请重新确认当前任务。';
741
+ await collectDispatchUsage(dispatch.dispatch_id, stillAuthorized);
742
+ if (!stillAuthorized())
743
+ return 'AUTHZ_DENIED [AUTHORITY_REVOKED]: 对账期间授权已失效。';
744
+ if (reconciled.status !== 'TERMINAL')
745
+ return `RECOVERY_REQUIRED [${reconciled.status}]: ${reconciled.reason};未重新派发任务,未释放 Lease。`;
746
+ try {
747
+ const claim = submitGovernedClaim(store, dispatch.dispatch_id, reconciled.summary ?? '(终态证据已确认;最终消息文本当前不可恢复)');
748
+ const lease = store.getLease(dispatch.lease_id);
749
+ const task = store.getTask(args.taskId);
750
+ return `${claim.created ? '对账已补齐' : '已存在'}第 ${dispatch.attempt_no} 次 Claim(${claim.outcome})。\n摘要:${claim.summary}\n任务状态 ${task?.status ?? 'UNKNOWN'};Lease=${lease?.state ?? 'UNKNOWN'}。${lease?.state === 'RECOVERING' ? '清理或结算证据不足,不重试清理、不复用 Session。' : ''}任务完成仍由 Supervisor 审查裁定。`;
751
+ }
752
+ catch (error) {
753
+ return `RECOVERY_REQUIRED [CLAIM_BLOCKED]: ${error instanceof Error ? error.message : String(error)}`;
754
+ }
755
+ };
113
756
  // ── 工具注册(全部挂 ctx.effect)────────────────────────────
114
757
  ctx.effect(() => ctx.tools.register(defineTool({
115
758
  name: 'kingdom_init',
116
- description: '初始化或接入本地王国(幂等:无则新建 kingdom.db,有则接入)',
759
+ description: 'Owner-only 写操作已关闭:请由用户直接键入 `/kingdom init`(Agent Tool 调用确定性 OWNER_CONTROL_REQUIRED)',
117
760
  parameters: {},
118
761
  output: {
119
762
  schema: { type: 'string' },
120
763
  render: (_args, value) => [{ type: 'text', text: String(value) }],
121
764
  },
122
765
  async execute() {
123
- const result = manager.init();
124
- return initResultText(result);
766
+ return ownerToolDenied();
125
767
  },
126
768
  })), 'dsh-kingdom: init tool');
127
769
  ctx.effect(() => ctx.tools.register(defineTool({
@@ -138,7 +780,7 @@ export function apply(ctx, config) {
138
780
  })), 'dsh-kingdom: status tool');
139
781
  ctx.effect(() => ctx.tools.register(defineTool({
140
782
  name: 'kingdom_create_territory',
141
- description: '在当前王国创建一个领地(Territory),绑定工作区路径',
783
+ description: 'Owner-only:Agent Tool 不代行;请使用 `/kingdom territory.create <JSON>`',
142
784
  parameters: {
143
785
  name: { type: 'string', required: true, description: '领地名,如 RAG 研发领' },
144
786
  workspace_path: { type: 'string', description: '工作区绝对路径,默认当前目录' },
@@ -149,18 +791,7 @@ export function apply(ctx, config) {
149
791
  render: (_args, value) => [{ type: 'text', text: String(value) }],
150
792
  },
151
793
  async execute(args, exec) {
152
- const kingdomId = requireKingdom();
153
- if (!kingdomId)
154
- return '尚未初始化王国。请先 /kingdom init 或说“初始化王国”。';
155
- return createTerritory(store, {
156
- kingdomId,
157
- name: args.name,
158
- workspacePath: args.workspace_path,
159
- summary: args.summary,
160
- }, {
161
- mode: config.authMode,
162
- principalSessionId: sessionPrincipal(exec)?.sessionId,
163
- });
794
+ return ownerToolDenied();
164
795
  },
165
796
  })), 'dsh-kingdom: create-territory tool');
166
797
  ctx.effect(() => ctx.tools.register(defineTool({
@@ -180,7 +811,7 @@ export function apply(ctx, config) {
180
811
  })), 'dsh-kingdom: list-territories tool');
181
812
  ctx.effect(() => ctx.tools.register(defineTool({
182
813
  name: 'kingdom_delete_territory',
183
- description: '删除领地(真实用户反馈 #1)。治理语义:领地下存在任务(任意状态)时默认拒绝;确认级联删除请传 force=true(未终态任务统一标记 FAILED、活跃执行终止、TERRITORY_DELETED 事件留痕;DONE/FAILED 终态任务不篡改)',
814
+ description: 'Owner-only:Agent Tool 不代行;请使用 `/kingdom territory.delete <JSON>`(任务级联语义保持)',
184
815
  parameters: {
185
816
  territory_id: { type: 'string', description: '领地 id(与 name 二选一,优先)' },
186
817
  name: { type: 'string', description: '领地名(与 territory_id 二选一)' },
@@ -192,26 +823,12 @@ export function apply(ctx, config) {
192
823
  render: (_args, value) => [{ type: 'text', text: String(value) }],
193
824
  },
194
825
  async execute(args, exec) {
195
- const kingdomId = requireKingdom();
196
- if (!kingdomId)
197
- return '尚未初始化王国。请先 /kingdom init。';
198
- if (!args.territory_id && !args.name)
199
- return '错误:请提供 territory_id 或 name 指定要删除的领地。';
200
- return deleteTerritory(store, {
201
- kingdomId,
202
- territoryId: args.territory_id,
203
- name: args.name,
204
- force: args.force,
205
- reason: args.reason,
206
- }, {
207
- mode: config.authMode,
208
- principalSessionId: sessionPrincipal(exec)?.sessionId,
209
- });
826
+ return ownerToolDenied();
210
827
  },
211
828
  })), 'dsh-kingdom: delete-territory tool');
212
829
  ctx.effect(() => ctx.tools.register(defineTool({
213
830
  name: 'kingdom_set_territory_supervisor',
214
- description: '设置/解除领地主理 Supervisor(Topology Administration,session-bound 下仅 OWNER 可执行)。null 解除主理 → fail-closed(无 Supervisor 可治理该领地);指派必须是 ACTIVE SUPERVISOR 绑定',
831
+ description: 'Owner-only:Agent Tool 不代行;请使用 `/kingdom territory.supervisor <JSON>`',
215
832
  parameters: {
216
833
  territory_id: { type: 'string', required: true, description: '领地 id' },
217
834
  supervisor_binding_id: { type: 'string', description: 'ACTIVE SUPERVISOR 绑定 id;传 null 解除主理' },
@@ -221,22 +838,12 @@ export function apply(ctx, config) {
221
838
  render: (_args, value) => [{ type: 'text', text: String(value) }],
222
839
  },
223
840
  async execute(args, exec) {
224
- const kingdomId = requireKingdom();
225
- if (!kingdomId)
226
- return '尚未初始化王国。请先 /kingdom init。';
227
- return setTerritorySupervisor(store, {
228
- kingdomId,
229
- territoryId: args.territory_id,
230
- supervisorBindingId: args.supervisor_binding_id === undefined ? null : args.supervisor_binding_id,
231
- }, {
232
- mode: config.authMode,
233
- principalSessionId: sessionPrincipal(exec)?.sessionId,
234
- });
841
+ return ownerToolDenied();
235
842
  },
236
843
  })), 'dsh-kingdom: set-territory-supervisor tool');
237
844
  ctx.effect(() => ctx.tools.register(defineTool({
238
845
  name: 'kingdom_bind_role',
239
- description: '绑定一个角色(OWNER/CHANCELLOR/SUPERVISOR/WORKER)到 DSH 会话,Role 与 Session 解耦;可携带会话身份预留字段(模型名/agent 名/扩展元数据,可不填)',
846
+ description: 'Owner-only:Agent Tool 不代行;请使用 `/kingdom role.bind <JSON>`(OWNER 不得绑定 Session)',
240
847
  parameters: {
241
848
  role_type: { type: 'string', required: true, description: '角色:OWNER/CHANCELLOR/SUPERVISOR/WORKER' },
242
849
  role_name: { type: 'string', description: '角色名,如 Worker-01' },
@@ -250,26 +857,20 @@ export function apply(ctx, config) {
250
857
  render: (_args, value) => [{ type: 'text', text: String(value) }],
251
858
  },
252
859
  async execute(args, exec) {
253
- const kingdomId = requireKingdom();
254
- if (!kingdomId)
255
- return '尚未初始化王国。请先 /kingdom init。';
256
- return bindRole(store, {
257
- kingdomId,
258
- roleType: args.role_type,
259
- roleName: args.role_name,
260
- sessionId: args.session_id,
261
- modelName: args.model_name,
262
- agentName: args.agent_name,
263
- sessionMeta: args.session_meta,
264
- }, {
265
- mode: config.authMode,
266
- principalSessionId: sessionPrincipal(exec)?.sessionId,
860
+ return ownerToolDenied({
861
+ operation: 'kingdom_bind_role',
862
+ request: {
863
+ role_type: args.role_type,
864
+ role_name: args.role_name,
865
+ session_id: args.session_id,
866
+ },
867
+ exec,
267
868
  });
268
869
  },
269
870
  })), 'dsh-kingdom: bind-role tool');
270
871
  ctx.effect(() => ctx.tools.register(defineTool({
271
872
  name: 'kingdom_unbind_role',
272
- description: '解绑一个角色(换届通道):按 role_type 或 binding_id 移除绑定。OWNER 受保护不可解绑。解绑后该角色空缺,可重新绑定',
873
+ description: 'Owner-only:Agent Tool 不代行;请使用 `/kingdom role.unbind <JSON>`(OWNER 受保护)',
273
874
  parameters: {
274
875
  role_type: { type: 'string', description: '角色:CHANCELLOR/SUPERVISOR/WORKER(与 binding_id 二选一)' },
275
876
  binding_id: { type: 'string', description: '绑定 id(与 role_type 二选一)' },
@@ -280,23 +881,12 @@ export function apply(ctx, config) {
280
881
  render: (_args, value) => [{ type: 'text', text: String(value) }],
281
882
  },
282
883
  async execute(args, exec) {
283
- const kingdomId = requireKingdom();
284
- if (!kingdomId)
285
- return '尚未初始化王国。请先 /kingdom init。';
286
- return unbindRole(store, {
287
- kingdomId,
288
- roleType: args.role_type,
289
- bindingId: args.binding_id,
290
- reason: args.reason,
291
- }, {
292
- mode: config.authMode,
293
- principalSessionId: sessionPrincipal(exec)?.sessionId,
294
- });
884
+ return ownerToolDenied();
295
885
  },
296
886
  })), 'dsh-kingdom: unbind-role tool');
297
887
  ctx.effect(() => ctx.tools.register(defineTool({
298
888
  name: 'kingdom_bind_session',
299
- description: '把角色绑定到(或改绑到)某个独立 DSH 会话,并更新会话身份预留字段(模型名/agent 名/扩展元数据)。null 清空、缺省不变。配合 authMode=session-bound 时按调用方会话校验身份',
889
+ description: 'Owner-only:Agent Tool 不代行;请使用 `/kingdom role.session <JSON>`(OWNER.session_id 永远不是 Authority)',
300
890
  parameters: {
301
891
  role_type: { type: 'string', description: '角色:OWNER/CHANCELLOR/SUPERVISOR/WORKER(与 binding_id 二选一)' },
302
892
  binding_id: { type: 'string', description: '绑定 id(与 role_type 二选一)' },
@@ -310,23 +900,35 @@ export function apply(ctx, config) {
310
900
  render: (_args, value) => [{ type: 'text', text: String(value) }],
311
901
  },
312
902
  async execute(args, exec) {
313
- const kingdomId = requireKingdom();
314
- if (!kingdomId)
315
- return '尚未初始化王国。请先 /kingdom init。';
316
- return rebindSession(store, {
317
- kingdomId,
318
- roleType: args.role_type,
319
- bindingId: args.binding_id,
320
- sessionId: args.session_id === undefined ? undefined : args.session_id,
321
- modelName: args.model_name === undefined ? undefined : args.model_name,
322
- agentName: args.agent_name === undefined ? undefined : args.agent_name,
323
- sessionMeta: args.session_meta === undefined ? undefined : args.session_meta,
324
- }, {
325
- mode: config.authMode,
326
- principalSessionId: sessionPrincipal(exec)?.sessionId,
903
+ return ownerToolDenied({
904
+ operation: 'kingdom_bind_session',
905
+ request: {
906
+ role_type: args.role_type,
907
+ binding_id: args.binding_id,
908
+ session_id: args.session_id,
909
+ },
910
+ exec,
327
911
  });
328
912
  },
329
913
  })), 'dsh-kingdom: bind-session tool');
914
+ ctx.effect(() => ctx.tools.register(defineTool({
915
+ name: 'kingdom_draft_owner_binding_intent',
916
+ description: '解析 bounded 自然语言 Owner 绑定意图;只返回 zero-write Draft,不执行绑定、不 mint Owner authority,最终须 direct /kingdom Slash 确认',
917
+ parameters: {
918
+ text: { type: 'string', required: true, description: '自然语言意图,例如“把当前会话设为宰相”或“让该会话主管 RAG 研发辖区”' },
919
+ },
920
+ output: {
921
+ schema: { type: 'string' },
922
+ render: (_args, value) => [{ type: 'text', text: String(value) }],
923
+ },
924
+ async execute(args, exec) {
925
+ const draft = draftOwnerBindingIntent({
926
+ text: args.text,
927
+ context: ownerBindingIntentContext(exec),
928
+ });
929
+ return serializeOwnerBindingIntentDraft(draft);
930
+ },
931
+ })), 'dsh-kingdom: owner-binding-intent draft tool');
330
932
  ctx.effect(() => ctx.tools.register(defineTool({
331
933
  name: 'kingdom_list_bindings',
332
934
  description: '列出当前王国的全部角色绑定',
@@ -344,7 +946,7 @@ export function apply(ctx, config) {
344
946
  })), 'dsh-kingdom: list-bindings tool');
345
947
  ctx.effect(() => ctx.tools.register(defineTool({
346
948
  name: 'kingdom_set_execution_profile',
347
- description: '设置/清空角色(通常 WORKER)的执行配置 ExecutionProfile {provider, model}——组织资源配置,治理对象:session-bound 下仅真实 OWNER 会话可执行。provider 缺省回退全局 workerProvider;model 缺省继承父 Agent(均留证据)。传空对象或 null 清空配置',
949
+ description: 'Owner-only:Agent Tool 不代行;请使用 `/kingdom execution-profile <JSON>`',
348
950
  parameters: {
349
951
  role_type: { type: 'string', description: '角色:OWNER/CHANCELLOR/SUPERVISOR/WORKER(与 binding_id 二选一)' },
350
952
  binding_id: { type: 'string', description: '绑定 id(与 role_type 二选一)' },
@@ -357,34 +959,24 @@ export function apply(ctx, config) {
357
959
  render: (_args, value) => [{ type: 'text', text: String(value) }],
358
960
  },
359
961
  async execute(args, exec) {
360
- const kingdomId = requireKingdom();
361
- if (!kingdomId)
362
- return '尚未初始化王国。请先 /kingdom init。';
363
- if (!args.role_type && !args.binding_id)
364
- return '错误:请提供 role_type 或 binding_id 指定要配置的绑定。';
365
- if (args.clear) {
366
- return setExecutionProfile(store, { kingdomId, roleType: args.role_type, bindingId: args.binding_id, profile: null }, {
367
- mode: config.authMode,
368
- principalSessionId: sessionPrincipal(exec)?.sessionId,
369
- });
370
- }
371
- if (!args.provider && !args.model) {
372
- return '错误:请至少提供 provider 或 model 之一(或传 clear=true 清空配置)。';
373
- }
374
- return setExecutionProfile(store, {
375
- kingdomId,
376
- roleType: args.role_type,
377
- bindingId: args.binding_id,
378
- profile: {
379
- ...args.provider ? { provider: args.provider } : {},
380
- ...args.model ? { model: args.model } : {},
381
- },
382
- }, {
383
- mode: config.authMode,
384
- principalSessionId: sessionPrincipal(exec)?.sessionId,
385
- });
962
+ return ownerToolDenied();
386
963
  },
387
964
  })), 'dsh-kingdom: set-execution-profile tool');
965
+ ctx.effect(() => ctx.tools.register(defineTool({
966
+ name: 'kingdom_set_capability_ceiling',
967
+ description: 'Owner-only:Agent Tool 不代行;请使用 `/kingdom ceiling <JSON>`;这是 governed persistent 的必要前置。',
968
+ parameters: {
969
+ ceiling_json: { type: 'string', description: 'capability allow-list JSON 对象,例如 {"filesystem.write":true,"tool:pwsh":true};value 必须全为 boolean' },
970
+ clear: { type: 'boolean', description: 'true = 清空 Ceiling(保持 governed execution fail-closed,直到重新配置)' },
971
+ },
972
+ output: {
973
+ schema: { type: 'string' },
974
+ render: (_args, value) => [{ type: 'text', text: String(value) }],
975
+ },
976
+ async execute(args, exec) {
977
+ return ownerToolDenied();
978
+ },
979
+ })), 'dsh-kingdom: set-capability-ceiling tool');
388
980
  // ── Phase 2:Task 治理闭环(5 个工具)──────────────────────────
389
981
  //
390
982
  // 注意这里**没有** kingdom_report_result 工具。Worker 是 one-shot subagent,
@@ -399,6 +991,7 @@ export function apply(ctx, config) {
399
991
  description: { type: 'string', description: '任务详细描述' },
400
992
  acceptance_criteria: { type: 'string', description: '验收标准(Supervisor 审查的客观依据,强烈建议填写)' },
401
993
  territory_id: { type: 'string', description: '领地 id;王国只有一个领地时可省略' },
994
+ capability_requirement_json: { type: 'string', description: '可选 Capability Requirement JSON;默认使用最小 governed requirement,并与 Task 同事务写入' },
402
995
  },
403
996
  output: {
404
997
  schema: { type: 'string' },
@@ -408,14 +1001,77 @@ export function apply(ctx, config) {
408
1001
  const kingdomId = requireKingdom();
409
1002
  if (!kingdomId)
410
1003
  return '尚未初始化王国。请先 /kingdom init。';
411
- return planTask(store, commandContext(kingdomId, sessionPrincipal(exec)), {
1004
+ const capabilityRequirementJson = args.capability_requirement_json === undefined
1005
+ ? MINIMAL_CAPABILITY_JSON
1006
+ : args.capability_requirement_json;
1007
+ if (capabilityRequirementJson.trim() && !store.isSchemaV4) {
1008
+ return '错误:Schema v4 未迁移;PlanTask capability requirement 不可安全写入,命令拒绝(fail-closed)。';
1009
+ }
1010
+ return planTask(store, commandContext(kingdomId, trustedToolPrincipal(exec)), {
412
1011
  title: args.title,
413
1012
  description: args.description,
414
1013
  acceptanceCriteria: args.acceptance_criteria,
415
1014
  territoryId: args.territory_id,
1015
+ capabilityRequirementJson,
416
1016
  }).message;
417
1017
  },
418
1018
  })), 'dsh-kingdom: plan-task tool');
1019
+ ctx.effect(() => ctx.tools.register(defineTool({
1020
+ name: 'kingdom_propose_collaboration',
1021
+ description: '宰相提议一层协作计划:含主整合者最多3位Worker,先由人类按准确版本采纳。小任务仍默认单执行者。',
1022
+ parameters: { plan_json: { type: 'string', required: true, description: 'JSON: parentTaskId,expectedVersion?,mode(EXPERT|TEAM),reason,integratorBindingId,budgetTokens,reserveTokens,items(1..2)。每项key/title/description/acceptanceCriteria/territoryId/workerBindingId/access(READ_ONLY|WRITE)/dependsOn/expectedArtifact;EXPERT只读。' } },
1023
+ output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: String(value) }] },
1024
+ async execute(args, exec) {
1025
+ const kingdomId = requireKingdom(), principal = trustedToolPrincipal(exec);
1026
+ if (!kingdomId || !principal)
1027
+ return 'AUTHZ_DENIED: 需要当前王国的真实宰相 Session。';
1028
+ try {
1029
+ if (typeof args.plan_json !== 'string' || args.plan_json.length > 24000)
1030
+ return 'INPUT_DENIED: 计划过长。';
1031
+ return JSON.stringify(proposeCollaborationPlan(store, governedStartCommandContext(kingdomId, principal), parseStrictJsonObject(args.plan_json)));
1032
+ }
1033
+ catch (error) {
1034
+ return `PLAN_REJECTED: ${error instanceof Error ? error.message : String(error)}`;
1035
+ }
1036
+ },
1037
+ })), 'dsh-kingdom: propose-collaboration tool');
1038
+ ctx.effect(() => ctx.tools.register(defineTool({
1039
+ name: 'kingdom_advance_collaboration',
1040
+ description: '当前主管推进已获人类采纳的准确计划;串行单项或并行至多两个独立项,自动完成正常指派,沿原Gate/执行/Claim链,仍须主管审查,不自动ACCEPT。',
1041
+ parameters: { plan_id: { type: 'string', required: true, description: '已采纳计划ID' },
1042
+ version: { type: 'number', required: true, description: '准确版本' }, digest: { type: 'string', required: true, description: '完整计划摘要' },
1043
+ strategy: { type: 'string', required: true, description: 'SERIAL单项等待已有结算 / PARALLEL独立子项至多两个' },
1044
+ items_json: { type: 'string', required: true, description: 'JSON数组,每项{taskId,grant:{能力名:boolean}};不携带身份,不自动扩大权限。' } },
1045
+ output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: String(value) }] },
1046
+ async execute(args, exec) {
1047
+ const kingdomId = requireKingdom();
1048
+ if (!kingdomId)
1049
+ return '尚未初始化王国。';
1050
+ try {
1051
+ if (typeof args.items_json !== 'string' || args.items_json.length > 20000)
1052
+ return 'INPUT_DENIED: 批次过长。';
1053
+ const items = parseStrictJsonObject(`{"items":${args.items_json}}`).items;
1054
+ const result = await advanceCollaboration(store, () => {
1055
+ const principal = trustedToolPrincipal(exec);
1056
+ return principal ? governedStartCommandContext(kingdomId, principal) : null;
1057
+ }, { planId: args.plan_id, version: args.version, digest: args.digest, strategy: args.strategy, items }, async (input) => {
1058
+ const previous = store.latestWorkerResult(input.taskId)?.result_id;
1059
+ const message = await runGovernedStart(input, { kind: 'tool', execution: exec });
1060
+ const task = store.getTask(input.taskId), claim = store.latestWorkerResult(input.taskId), execution = store.latestExecution(input.taskId);
1061
+ const dispatch = execution ? store.listDispatchesForTaskAttempt(input.taskId, execution.attempt_no).find(row => row.execution_id === execution.execution_id) : null;
1062
+ const lease = dispatch ? store.getLease(dispatch.lease_id) : null;
1063
+ const ok = task?.status === 'REVIEW' && !!claim && claim.result_id !== previous && claim.outcome === 'COMPLETED'
1064
+ && execution?.state === 'COMPLETED' && execution.attempt_no === claim.attempt_no && execution.worker_binding_id === claim.worker_binding_id
1065
+ && dispatch?.state === 'TERMINAL' && dispatch.session_ref === claim.session_id && lease?.state === 'RELEASED';
1066
+ return { ok, message };
1067
+ });
1068
+ return JSON.stringify(result);
1069
+ }
1070
+ catch (error) {
1071
+ return `ADVANCE_REJECTED: ${error instanceof Error ? error.message : String(error)}`;
1072
+ }
1073
+ },
1074
+ })), 'dsh-kingdom: advance-collaboration tool');
419
1075
  ctx.effect(() => ctx.tools.register(defineTool({
420
1076
  name: 'kingdom_assign_task',
421
1077
  description: '把任务派给 Worker 绑定(Supervisor 职权),CREATED → ASSIGNED',
@@ -431,7 +1087,7 @@ export function apply(ctx, config) {
431
1087
  const kingdomId = requireKingdom();
432
1088
  if (!kingdomId)
433
1089
  return '尚未初始化王国。请先 /kingdom init。';
434
- return assignTask(store, commandContext(kingdomId, sessionPrincipal(exec)), {
1090
+ return assignTask(store, commandContext(kingdomId, trustedToolPrincipal(exec)), {
435
1091
  taskId: args.task_id,
436
1092
  workerBindingId: args.worker_binding_id,
437
1093
  }).message;
@@ -439,15 +1095,19 @@ export function apply(ctx, config) {
439
1095
  })), 'dsh-kingdom: assign-task tool');
440
1096
  ctx.effect(() => ctx.tools.register(defineTool({
441
1097
  name: 'kingdom_start_task',
442
- description: '触发 Worker 执行任务并等待其结果(Supervisor 职权)。结果只会让任务进入 REVIEW,不会直接完成任务',
1098
+ description: 'LEGACY_COMPAT(仅显式选择):用旧 one-shot Worker 执行任务并等待结果;不会自动作为 governed persistent 的 fallback。结果只会让任务进入 REVIEW,不会直接完成任务',
443
1099
  parameters: {
444
1100
  task_id: { type: 'string', required: true, description: '任务 id(状态需为 ASSIGNED,或 REWORK 后的 RUNNING)' },
1101
+ legacy_opt_in: { type: 'boolean', description: '必须显式传 true 选择 LEGACY_COMPAT;省略/false 永远拒绝,persistent 失败不会自动 fallback' },
445
1102
  },
446
1103
  output: {
447
1104
  schema: { type: 'string' },
448
1105
  render: (_args, value) => [{ type: 'text', text: String(value) }],
449
1106
  },
450
1107
  async execute(args, exec) {
1108
+ if (args.legacy_opt_in !== true) {
1109
+ return 'LEGACY_COMPAT_REQUIRED: kingdom_start_task 是旧 one-shot 兼容入口;必须显式传 legacy_opt_in=true。persistent governed 失败不会自动降级。';
1110
+ }
451
1111
  const kingdomId = requireKingdom();
452
1112
  if (!kingdomId)
453
1113
  return '尚未初始化王国。请先 /kingdom init。';
@@ -472,16 +1132,16 @@ export function apply(ctx, config) {
472
1132
  });
473
1133
  if (!resolved.ok)
474
1134
  return resolved.error;
475
- const result = await startTask(store, resolved.executor, commandContext(kingdomId, sessionPrincipal(exec)), { taskId: args.task_id });
1135
+ const result = await startTask(store, resolved.executor, commandContext(kingdomId, trustedToolPrincipal(exec)), { taskId: args.task_id });
476
1136
  return result.message;
477
1137
  },
478
1138
  })), 'dsh-kingdom: start-task tool');
479
1139
  ctx.effect(() => ctx.tools.register(defineTool({
480
1140
  name: 'kingdom_start_task_governed',
481
- description: '以 Governed Persistent Worker 执行任务(v0.8):Worker 长期 DSH Session + Capability Gate(仅 GRANTED+ENFORCED 才 dispatch)+ Lease/Dispatch/Receipt/Evidence 全链;DENIED/CANNOT_ENFORCE → zero execution。需王国已迁移 Schema v4;Worker 获得并复用长期 Session(REWORK 唤醒同一 Worker)',
1141
+ description: 'CANONICAL HEADLESS:以 Governed Persistent Worker 执行任务(v0.8):真实 DSH caller session 中的 Supervisor + Territory scope 授权后,Worker 长期 DSH Session + Capability Gate(仅 GRANTED+ENFORCED 才 dispatch)+ Lease/Dispatch/Receipt/Evidence 全链;DENIED/CANNOT_ENFORCE → zero execution。需王国已迁移 Schema v4;Worker 获得并复用长期 Session(REWORK 唤醒同一 Worker)。失败只 fail-closed,不自动降级 LEGACY_COMPAT',
482
1142
  parameters: {
483
1143
  task_id: { type: 'string', required: true, description: '任务 id(状态需为 ASSIGNED,或 REWORK 后的 RUNNING)' },
484
- grant_json: { type: 'string', required: true, description: 'Supervisor 本次 Attempt 的权威授权 JSON(如 {"tool:pwsh": true, "filesystem.write": true};与任务 capability requirement 求交集)' },
1144
+ grant_json: { type: 'string', required: true, description: '当前已认证 Supervisor 的本次 capability Grant JSON(仅表达能力,不代表身份;如 {"tool:pwsh": true, "filesystem.write": true})' },
485
1145
  sandbox_mode: { type: 'string', description: 'workspace-write(默认,领地写边界)/ read-only' },
486
1146
  },
487
1147
  output: {
@@ -489,135 +1149,25 @@ export function apply(ctx, config) {
489
1149
  render: (_args, value) => [{ type: 'text', text: String(value) }],
490
1150
  },
491
1151
  async execute(args, exec) {
492
- const kingdomId = requireKingdom();
493
- if (!kingdomId)
494
- return '尚未初始化王国。请先 /kingdom init。';
495
- if (!store.isSchemaV4) {
496
- return '错误:Schema v4 未迁移(正式 kingdom.db 迁移须经 Formal DB Migration Gate);governed 执行不可用(fail-closed)。';
497
- }
498
- const loadedTask = store.getTask(args.task_id);
499
- if (!loadedTask)
500
- return `错误:找不到任务 ${args.task_id}。`;
501
- if (loadedTask.status !== 'ASSIGNED' && loadedTask.status !== 'RUNNING') {
502
- return `错误:任务状态 ${loadedTask.status} 不可 governed 启动(仅 ASSIGNED / REWORK 后的 RUNNING)。`;
503
- }
504
- const territory = store.getTerritoryById(loadedTask.territory_id);
505
- if (!territory)
506
- return '错误:任务领地缺失。';
507
- const worker = loadedTask.assigned_binding_id;
508
- if (!worker)
509
- return '错误:任务未指派 Worker。';
510
- const agents = ctx.get('agents');
511
- if (!agents) {
512
- return '错误:当前 dsh 组合未提供 agents 服务(Persistent Session 不可用);governed 执行不可用。';
513
- }
514
- let grant;
515
- try {
516
- const parsed = JSON.parse(args.grant_json);
517
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
518
- throw new Error('shape');
519
- grant = parsed;
520
- }
521
- catch {
522
- return '错误:grant_json 必须是非空 JSON 对象(Supervisor 权威授权)。';
523
- }
524
- const sup = territory.supervisor_binding_id ?? store.getBindingsByRole(kingdomId, 'SUPERVISOR')[0]?.binding_id ?? null;
525
- // Owner V0.8 PRODUCTION-PATH CLOSURE(正式入口 E2E seam):S4 materialize 需要**模块函数** seam
526
- // (`setSandboxMode = session.append('sandbox/mode')` / `setApprovalPolicy = session.append('approval/policy')`),
527
- // 而非 ctx 服务对象(SandboxPolicyService/ApprovalService 无这些方法,传服务 → MATERIALIZE_FAILED)。
528
- // 动态 import(懒解析):失败 → null → materialize 报「缺失」→ DENIED(fail-closed,不崩溃插件)。
529
- const s4Seam = await (async () => {
530
- try {
531
- // 变量 specifier:运行时经 dsh loader 解析(动态 seam;静态类型不做解析校验)
532
- const sbSpec = '@deepseek-ai/dsh-sandbox-policy';
533
- const apSpec = '@deepseek-ai/dsh-user-approval';
534
- const sandbox = (await import(sbSpec));
535
- const approval = (await import(apSpec));
536
- return {
537
- sandboxPolicy: { setSandboxMode: sandbox.setSandboxMode },
538
- approval: { setApprovalPolicy: approval.setApprovalPolicy },
539
- };
540
- }
541
- catch {
542
- return { sandboxPolicy: null, approval: null };
543
- }
544
- })();
545
- const adapter = new DshRuntimeAdapter({
546
- runtimeInstanceRef: `dsh-${hostname()}`,
547
- provider: config.workerProvider || 'spawn',
548
- model: null,
549
- agents,
550
- permission: ctx.get('permission'),
551
- sandboxPolicy: s4Seam.sandboxPolicy,
552
- approval: s4Seam.approval,
553
- presets: ctx.get('agentPresets'),
554
- });
555
- // attempt 编号必须纳入 Lease Ledger(zero-execution 的 gate 拒绝也会消耗 attempt_no,
556
- // 否则重试会撞 UNIQUE(task_id, attempt_no)——正式入口 E2E 实证)
557
- const leaseMax = store.listLeases(kingdomId)
558
- .filter((l) => l.task_id === loadedTask.task_id)
559
- .reduce((m, l) => Math.max(m, l.attempt_no), 0);
560
- const attemptNo = Math.max(store.nextAttemptNo(loadedTask.task_id), leaseMax + 1);
561
- const result = await runGovernedTask({
562
- store, adapter, kingdomId, workerBindingId: worker,
563
- territoryId: loadedTask.territory_id, cwd: territory.workspace_path ?? process.cwd(),
564
- taskId: loadedTask.task_id, attemptNo, supervisorBindingId: sup,
565
- grant, requirementJson: store.getTaskCapabilityRequirement(loadedTask.task_id),
566
- sandboxMode: args.sandbox_mode === 'read-only' ? 'read-only' : 'workspace-write',
567
- // Owner V0.8 PRODUCTION-PATH CLOSURE A:全局 workerProvider 仅作 provider 回退;
568
- // Worker 的 provider/model 权威来源 = execution_profile_json(runGovernedTask 内部解析,
569
- // model 缺失 → fail closed configuration error,不创建 Session、不 dispatch)。
570
- globalProvider: config.workerProvider || 'spawn',
571
- // 真实模型 turn 轮询窗口(正式入口 E2E 实证:默认 100ms×40=4s 太短,turn 未及 terminal):
572
- // 1s × 60 = 60s;超窗 → fail-closed 返回(进 RECOVERING,由 reconcile 处理)。
573
- pollIntervalMs: 1000,
574
- maxPolls: 60,
575
- });
576
- if (!result.ok) {
577
- return `Governed 执行未发生(zero execution):${result.reason}\n(任务保持 ${loadedTask.status};请检查 Capability 配置后重试或改用 kingdom_start_task legacy 路径。)`;
578
- }
579
- // Claim 到达(Claim ≠ Fact):落 worker_results + Task RUNNING→REVIEW(不是 DONE)。
580
- // Owner V0.8 FINAL RELEASE BLOCKER:Claim outcome 按已验证 terminalOutcome 收敛
581
- // (COMPLETED/FAILED/ABORTED)——禁止 hardcode COMPLETED;FAILED 不得生成 COMPLETED Claim;
582
- // summary 仅来自真实 assistant 文本,无文本 → 诚实回退占位(不伪造"任务完成"类摘要)。
583
- const task = store.getTask(loadedTask.task_id);
584
- const running = task.status === 'ASSIGNED' ? store.transitionTask(task, 'RUNNING') : task;
585
- const outcome = result.terminalOutcome;
586
- const summary = result.summary;
587
- store.insertWorkerResult({
588
- result_id: randomUUID(),
589
- task_id: task.task_id,
590
- attempt_no: attemptNo,
591
- worker_binding_id: worker,
592
- session_id: result.sessionRef,
593
- outcome,
594
- result_json: JSON.stringify({ outcome, summary }),
595
- created_at: new Date().toISOString(),
596
- });
597
- store.transitionTask(running, 'REVIEW', { result_summary: summary });
598
- // settlement:cleanup 确认 → release lease
599
- const lease = store.getLease(result.leaseId);
600
- if (lease && lease.state !== 'RELEASED') {
601
- settleAndRelease(store, result.leaseId, true, 'settled-cleanup-ok');
602
- }
603
- const nowIso = new Date().toISOString();
604
- store.appendEvent({
605
- event_id: randomUUID(), kingdom_id: kingdomId, event_type: 'WORKER_RESULT_SUBMITTED',
606
- actor_role: 'WORKER', actor_id: worker, target_type: 'task', target_id: task.task_id,
607
- payload_json: JSON.stringify({ attempt_no: attemptNo, claimed_outcome: outcome, session_id: result.sessionRef, executor: 'dsh-governed:persistent' }),
608
- created_at: nowIso,
609
- });
610
- store.appendEvent({
611
- event_id: randomUUID(), kingdom_id: kingdomId, event_type: 'SESSION_STOPPED',
612
- actor_role: 'WORKER', actor_id: worker, target_type: 'execution', target_id: result.executionId,
613
- payload_json: JSON.stringify({ task_id: task.task_id, attempt_no: attemptNo, reason: outcome === 'COMPLETED' ? 'completed' : outcome.toLowerCase() }),
614
- created_at: nowIso,
615
- });
616
- return `Governed Worker 已提交第 ${attemptNo} 次尝试的 Claim(outcome=${outcome},长期 Session ${result.sessionRef.slice(-8)}${result.created ? ' 新建' : ' 复用'})。\n`
617
- + `摘要:${summary}\n`
618
- + `任务「${task.title}」现在处于 **REVIEW**。这是一个待审查的 Claim,尚未成为任务完成事实——请 Supervisor 用 kingdom_review_task 裁定 ACCEPT / REWORK / FAIL。`;
1152
+ return runGovernedStart({
1153
+ taskId: args.task_id,
1154
+ grantJson: args.grant_json,
1155
+ sandboxMode: args.sandbox_mode,
1156
+ }, { kind: 'tool', execution: exec });
619
1157
  },
620
1158
  })), 'dsh-kingdom: governed start-task tool');
1159
+ ctx.effect(() => ctx.tools.register(defineTool({
1160
+ name: 'kingdom_reconcile_task',
1161
+ description: '对账已有任务的 governed Dispatch:仅原进程保留的可信上下文可补齐迟到 Claim;缺凭据或清理证据保持 RECOVERING。不会重发任务或自动 ACCEPT。',
1162
+ parameters: {
1163
+ task_id: { type: 'string', required: true, description: '当前 Supervisor 管辖的任务 id' },
1164
+ dispatch_id: { type: 'string', description: '可选;默认选任务最新 Execution 的原 Dispatch' },
1165
+ },
1166
+ output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: String(value) }] },
1167
+ async execute(args, exec) {
1168
+ return runGovernedReconcile({ taskId: args.task_id, dispatchId: args.dispatch_id }, { kind: 'tool', execution: exec });
1169
+ },
1170
+ })), 'dsh-kingdom: governed reconcile-task tool');
621
1171
  ctx.effect(() => ctx.tools.register(defineTool({
622
1172
  name: 'kingdom_review_task',
623
1173
  description: '审查 Worker 提交的结果并裁定(Supervisor 职权,须在任务领地的主理范围内)。这是任务能变成 DONE 的唯一路径:ACCEPT(→DONE)/ REWORK(→RUNNING 同 Worker)/ FAIL(→FAILED)/ HANDOFF(→RUNNING 转交新 Worker,需 to_binding_id + reason)',
@@ -635,7 +1185,7 @@ export function apply(ctx, config) {
635
1185
  const kingdomId = requireKingdom();
636
1186
  if (!kingdomId)
637
1187
  return '尚未初始化王国。请先 /kingdom init。';
638
- return reviewTask(store, commandContext(kingdomId, sessionPrincipal(exec)), {
1188
+ return reviewTask(store, commandContext(kingdomId, trustedToolPrincipal(exec)), {
639
1189
  taskId: args.task_id,
640
1190
  decision: args.decision,
641
1191
  reason: args.reason,
@@ -685,7 +1235,7 @@ export function apply(ctx, config) {
685
1235
  const kingdomId = requireKingdom();
686
1236
  if (!kingdomId)
687
1237
  return { error: 'KINGDOM_NOT_INITIALIZED', message: '尚未初始化王国。请先 /kingdom init。' };
688
- return buildSnapshot(store, { auth: authView });
1238
+ return buildSnapshot(store, { auth: authView, costRuntime });
689
1239
  }), 'dsh-kingdom: snapshot tool');
690
1240
  ctx.effect(() => jsonTool('kingdom_task_detail', '返回单个任务的结构化详情 JSON:验收标准、尝试历史、全部 Claim、Supervisor 决策、执行记录、关联事件与允许的下一步动作', { task_id: { type: 'string', required: true, description: '任务 id' } }, (args) => {
691
1241
  const kingdomId = requireKingdom();
@@ -711,7 +1261,7 @@ export function apply(ctx, config) {
711
1261
  if (!kingdomId)
712
1262
  return '尚未初始化王国。请先 /kingdom init。';
713
1263
  const input = { executionId: args.execution_id, reason: args.reason };
714
- const cmd = commandContext(kingdomId, sessionPrincipal(exec));
1264
+ const cmd = commandContext(kingdomId, trustedToolPrincipal(exec));
715
1265
  switch (args.action.trim().toLowerCase()) {
716
1266
  case 'pause': return pauseExecution(store, cmd, input).message;
717
1267
  case 'resume': return resumeExecution(store, cmd, input).message;
@@ -720,30 +1270,200 @@ export function apply(ctx, config) {
720
1270
  }
721
1271
  },
722
1272
  })), 'dsh-kingdom: execution-control tool');
723
- // ── 本地 GUI 通道(默认关闭;配置 guiPort 后启用)────────────────
724
- if (config.guiPort > 0) {
725
- ctx.effect(() => startGuiServer({
726
- snapshot: () => buildSnapshot(store, { auth: authView }),
727
- taskDetail: (taskId) => {
728
- const kingdomId = requireKingdom();
729
- return kingdomId ? buildTaskDetail(store, kingdomId, taskId) : null;
730
- },
731
- eventsSince: (afterSeq, limit) => {
732
- const kingdomId = requireKingdom();
733
- if (!kingdomId)
734
- return { revision: 0, events: [] };
735
- return {
736
- revision: store.revision(kingdomId),
737
- events: store.listEventsSince(kingdomId, afterSeq, limit).map(toEventView),
1273
+ // ── 本地 GUI 通道(direct `/kingdom gui` 激活;配置端口可预启动)────
1274
+ const GUI_HOST = '127.0.0.1';
1275
+ let guiRuntime = null;
1276
+ let guiStartPromise = null;
1277
+ let guiStopRequested = false;
1278
+ const projectionSecurityFor = (readContext) => {
1279
+ const principalSessionId = readContext?.principalSessionId;
1280
+ if (typeof principalSessionId !== 'string' || principalSessionId.length === 0)
1281
+ return {};
1282
+ const kingdomId = requireKingdom();
1283
+ const supervisorBindingIds = new Set(kingdomId
1284
+ ? store.getBindingsByRole(kingdomId, 'SUPERVISOR')
1285
+ .filter(binding => binding.status === 'ACTIVE' && binding.session_id === principalSessionId)
1286
+ .map(binding => binding.binding_id)
1287
+ : []);
1288
+ const scope = kingdomId
1289
+ ? store.listTerritories(kingdomId)
1290
+ .filter(territory => territory.supervisor_binding_id !== null
1291
+ && supervisorBindingIds.has(territory.supervisor_binding_id))
1292
+ .map(territory => `territory:${territory.territory_id}`)
1293
+ : [];
1294
+ return {
1295
+ sessionVerified: true,
1296
+ principalSessionId,
1297
+ hostContext: true,
1298
+ commandCoverage: [...GUI_SESSION_COMMANDS],
1299
+ scope,
1300
+ };
1301
+ };
1302
+ const closeGuiRuntime = (runtime) => {
1303
+ runtime.control.revokeAllSessions();
1304
+ runtime.ownerControl.dispose();
1305
+ try {
1306
+ runtime.close();
1307
+ }
1308
+ catch { /* server may already be unavailable */ }
1309
+ if (guiRuntime === runtime)
1310
+ guiRuntime = null;
1311
+ };
1312
+ const ensureGuiServer = () => {
1313
+ if (guiRuntime)
1314
+ return Promise.resolve(guiRuntime);
1315
+ if (guiStartPromise)
1316
+ return guiStartPromise;
1317
+ guiStopRequested = false;
1318
+ let closeServer = null;
1319
+ const pending = new Promise((resolve, reject) => {
1320
+ let listeningAddress = null;
1321
+ let settled = false;
1322
+ const control = new LocalControlManager({
1323
+ host: GUI_HOST,
1324
+ // This remains empty until startGuiServer reports the actual port.
1325
+ // In particular, port=0 can never activate against a guessed origin.
1326
+ expectedOrigin: () => listeningAddress?.origin ?? '',
1327
+ });
1328
+ const ownerController = new OwnerDecisionController(store, {
1329
+ validateTargetSession: async ({ sessionId, signal }) => {
1330
+ if (signal.aborted)
1331
+ 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 会话;请刷新后重新选择。' };
1334
+ },
1335
+ listTargetSessions: async ({ sessionIds, signal }) => {
1336
+ const seam = dshRegistrySeam();
1337
+ return signal.aborted ? [] : sessionIds.filter(id => validateLiveDirectSession(id, seam).ok)
1338
+ .map(id => ({ id, label: `DSH 会话 ${id}` }));
1339
+ },
1340
+ validateExecutionProfile: async ({ profile, signal }) => {
1341
+ const provider = profile?.provider?.trim() || config.workerProvider;
1342
+ const model = profile?.model?.trim();
1343
+ if (!model)
1344
+ return { ok: false, code: 'OWNER_MODEL_REQUIRED', message: '请明确选择模型;保存配置不会证明模型已运行成功。' };
1345
+ const llm = ctx.get('llm');
1346
+ if (!llm || typeof llm.resolveModelInfo !== 'function')
1347
+ return { ok: false, code: 'OWNER_MODEL_VALIDATION_UNAVAILABLE', message: '当前宿主无法核对模型配置,未写入变更。' };
1348
+ try {
1349
+ await llm.resolveModelInfo(provider, model, signal);
1350
+ return signal.aborted ? { ok: false, code: 'OWNER_VALIDATION_ABORTED', message: '管理窗口已撤销或校验超时。' } : { ok: true };
1351
+ }
1352
+ catch {
1353
+ return { ok: false, code: 'OWNER_MODEL_UNAVAILABLE', message: '宿主未能确认该提供方与模型路由,请核对现有模型配置。' };
1354
+ }
1355
+ },
1356
+ });
1357
+ const ownerControl = new OwnerLocalControlManager({ controller: ownerController, expectedOrigin: () => listeningAddress?.origin ?? '' });
1358
+ const safeClose = () => {
1359
+ try {
1360
+ closeServer?.();
1361
+ }
1362
+ catch { /* startup failure is already reported */ }
1363
+ };
1364
+ closeServer = () => {
1365
+ try {
1366
+ serverClose();
1367
+ }
1368
+ catch { /* server may not have reached listen */ }
1369
+ };
1370
+ // The returned close function is assigned after startGuiServer creates
1371
+ // its Server. Keep the indirection local so callbacks never expose the
1372
+ // ticket or depend on a second launch protocol.
1373
+ let serverClose = () => undefined;
1374
+ const onListening = (address) => {
1375
+ listeningAddress = address;
1376
+ const runtime = {
1377
+ address,
1378
+ control,
1379
+ ownerControl,
1380
+ close: safeClose,
738
1381
  };
739
- },
740
- command: (name, payload) => runGuiCommand(name, payload),
741
- }, {
742
- port: config.guiPort,
743
- ...config.guiToken ? { token: config.guiToken } : {},
744
- allowOrigins: config.guiAllowOrigins,
745
- logger: ctx.logger,
746
- }), 'dsh-kingdom: GUI channel');
1382
+ if (guiStopRequested) {
1383
+ control.revokeAllSessions();
1384
+ ownerControl.dispose();
1385
+ safeClose();
1386
+ settled = true;
1387
+ reject(new Error('GUI_STOPPED_DURING_START'));
1388
+ return;
1389
+ }
1390
+ guiRuntime = runtime;
1391
+ settled = true;
1392
+ resolve(runtime);
1393
+ };
1394
+ const onUnavailable = (error) => {
1395
+ if (settled)
1396
+ return;
1397
+ settled = true;
1398
+ control.dispose();
1399
+ ownerControl.dispose();
1400
+ reject(error);
1401
+ };
1402
+ serverClose = startGuiServer({
1403
+ snapshot: (readContext) => buildSnapshot(store, {
1404
+ auth: authView,
1405
+ costRuntime,
1406
+ security: projectionSecurityFor(readContext),
1407
+ }),
1408
+ taskDetail: (taskId, readContext) => {
1409
+ const kingdomId = requireKingdom();
1410
+ return kingdomId
1411
+ ? buildTaskDetail(store, kingdomId, taskId, {
1412
+ security: projectionSecurityFor(readContext),
1413
+ })
1414
+ : null;
1415
+ },
1416
+ eventsSince: (afterSeq, limit) => {
1417
+ const kingdomId = requireKingdom();
1418
+ if (!kingdomId)
1419
+ return { revision: 0, events: [] };
1420
+ return {
1421
+ revision: store.revision(kingdomId),
1422
+ events: store.listEventsSince(kingdomId, afterSeq, limit).map(toEventView),
1423
+ };
1424
+ },
1425
+ command: (name, payload, context) => runGuiCommand(name, payload, context),
1426
+ }, {
1427
+ port: config.guiPort,
1428
+ host: GUI_HOST,
1429
+ ...config.guiToken ? { token: config.guiToken } : {},
1430
+ allowOrigins: config.guiAllowOrigins,
1431
+ control,
1432
+ ownerControl,
1433
+ onListening,
1434
+ onUnavailable,
1435
+ logger: ctx.logger,
1436
+ });
1437
+ });
1438
+ guiStartPromise = pending;
1439
+ void pending.catch(() => {
1440
+ if (guiStartPromise === pending)
1441
+ guiStartPromise = null;
1442
+ });
1443
+ return pending;
1444
+ };
1445
+ const stopGuiServer = async () => {
1446
+ guiStopRequested = true;
1447
+ const pending = guiStartPromise;
1448
+ if (pending && !guiRuntime) {
1449
+ try {
1450
+ const runtime = await pending;
1451
+ closeGuiRuntime(runtime);
1452
+ }
1453
+ catch {
1454
+ // onUnavailable already failed closed and disposed the broker.
1455
+ }
1456
+ }
1457
+ else if (guiRuntime) {
1458
+ closeGuiRuntime(guiRuntime);
1459
+ }
1460
+ guiStartPromise = null;
1461
+ };
1462
+ ctx.effect(() => () => { void stopGuiServer(); });
1463
+ if (config.guiPort > 0) {
1464
+ void ensureGuiServer().catch((error) => {
1465
+ ctx.logger.warn(`dsh-kingdom GUI 通道启动失败(${error instanceof Error ? error.message : String(error)})`);
1466
+ });
747
1467
  }
748
1468
  const guiFailure = (errorCode, message) => ({
749
1469
  ok: false,
@@ -757,10 +1477,17 @@ export function apply(ctx, config) {
757
1477
  });
758
1478
  /** 把 Phase-1 风格(返回字符串)的领地/角色操作包装成 CommandResultView(M2:GUI 写层)。 */
759
1479
  const plainResult = (text, kId) => {
760
- const failed = text.startsWith('错误:');
1480
+ const failed = /^(?:错误:|OWNER_CONTROL_REQUIRED|CONFIG_DENIED|UNKNOWN\/|INPUT_DENIED|AUTHZ_DENIED|CAPABILITY_DENIED|GOVERNED_EXECUTION_DENIED|GOVERNED_SETTLEMENT_BLOCKED|RECOVERY_REQUIRED)/u.test(text);
1481
+ const errorCode = !failed
1482
+ ? null
1483
+ : text.startsWith('RECOVERY_REQUIRED') || text.startsWith('GOVERNED_SETTLEMENT_BLOCKED') ? 'RECOVERY_REQUIRED'
1484
+ : text.startsWith('AUTHZ_DENIED') ? 'SESSION_AUTH_REQUIRED'
1485
+ : text.startsWith('INPUT_DENIED') || text.startsWith('错误:') ? 'INVALID_INPUT'
1486
+ : text.startsWith('OWNER_CONTROL_REQUIRED') || text.startsWith('CONFIG_DENIED') ? 'SESSION_AUTH_REQUIRED'
1487
+ : 'WORKER_EXECUTION_FAILED';
761
1488
  return {
762
1489
  ok: !failed,
763
- errorCode: failed ? 'INVALID_INPUT' : null,
1490
+ errorCode,
764
1491
  message: text,
765
1492
  task: null,
766
1493
  execution: null,
@@ -770,28 +1497,24 @@ export function apply(ctx, config) {
770
1497
  };
771
1498
  };
772
1499
  /** GUI 写命令的分发。GUI 仍然经插件执行命令,绝不直接写 SQLite。 */
773
- async function runGuiCommand(name, payload) {
774
- // `init` 是唯一允许在王国未初始化时执行的命令(幂等,本地建库)。
775
- if (name === 'init') {
776
- const result = manager.init();
777
- return plainResult(result.detail, result.kingdomId);
778
- }
779
- const kingdomId = requireKingdom();
780
- if (!kingdomId) {
781
- return guiFailure('KINGDOM_NOT_INITIALIZED', '尚未初始化王国。请先 /kingdom init。');
1500
+ async function runGuiCommand(name, payload, control) {
1501
+ if (GUI_OWNER_ONLY_COMMAND_SET.has(name)) {
1502
+ return guiFailure('SESSION_AUTH_REQUIRED', `DIRECT_SLASH_REQUIRED: Owner-only action "${name}" 不能由 GUI/HTTP 执行;请由人类 Owner 直接使用 /kingdom Slash。`);
782
1503
  }
783
1504
  const str = (key) => typeof payload[key] === 'string' ? payload[key] : '';
784
1505
  const opt = (key) => typeof payload[key] === 'string' ? payload[key] : undefined;
785
- // v0.5.2(M1-B/P0-B):GUI 写命令**不再信任 HTTP payload 里的 session_id**——
786
- // principal 只能来自 DSH Runtime 证明(工具面 sessionPrincipal),
787
- // 网关面永远无 principal。session-bound 下所有写命令 fail-closed(SESSION_AUTH_REQUIRED)。
788
- const cmd = commandContext(kingdomId, undefined);
789
- const guard = guiWriteGuard(authView.mode);
790
- if (!guard.allowed) {
791
- return guiFailure(guard.code, '该操作需要由已绑定角色的真实 DSH Session 发起。'
792
- + 'GUI 当前没有可信 DSH Principal,因此拒绝代行。'
793
- + '请在 DSH 会话中调用对应 kingdom_* 工具完成此操作。');
1506
+ if (!control || control.signal.aborted) {
1507
+ return guiFailure('SESSION_AUTH_REQUIRED', 'GUI Control Session 不存在或已撤销;命令未进入 Core。');
794
1508
  }
1509
+ const kingdomId = requireKingdom();
1510
+ if (!kingdomId) {
1511
+ return guiFailure('KINGDOM_NOT_INITIALIZED', '尚未初始化王国。请由人类 Owner 先直接执行 `/kingdom init` 与所需 Owner-only 配置。');
1512
+ }
1513
+ // GUI role actions use the exact activation-time session, never a browser
1514
+ // field and never the configured declarative demo mode.
1515
+ const cmd = governedStartCommandContext(kingdomId, {
1516
+ sessionId: control.principalSessionId,
1517
+ });
795
1518
  switch (name) {
796
1519
  case 'territory.create':
797
1520
  return plainResult(createTerritory(store, {
@@ -836,12 +1559,18 @@ export function apply(ctx, config) {
836
1559
  sessionMeta: opt('session_meta'),
837
1560
  }), kingdomId);
838
1561
  case 'plan':
839
- return planTask(store, cmd, {
1562
+ if (!store.isSchemaV4) {
1563
+ return guiFailure('EXECUTOR_UNAVAILABLE', 'Schema v4 未迁移;GUI governed plan capability requirement 不可安全写入,命令拒绝。');
1564
+ }
1565
+ const capabilityRequirementJson = opt('capability_requirement_json') ?? MINIMAL_CAPABILITY_JSON;
1566
+ const planned = planTask(store, cmd, {
840
1567
  title: str('title'),
841
1568
  description: opt('description'),
842
1569
  acceptanceCriteria: opt('acceptance_criteria'),
843
1570
  territoryId: opt('territory_id'),
1571
+ capabilityRequirementJson,
844
1572
  });
1573
+ return planned;
845
1574
  case 'assign':
846
1575
  return assignTask(store, cmd, { taskId: str('task_id'), workerBindingId: opt('worker_binding_id') });
847
1576
  case 'review':
@@ -849,6 +1578,7 @@ export function apply(ctx, config) {
849
1578
  taskId: str('task_id'),
850
1579
  decision: str('decision'),
851
1580
  reason: opt('reason'),
1581
+ to_binding_id: opt('to_binding_id'),
852
1582
  });
853
1583
  case 'execution.pause':
854
1584
  return pauseExecution(store, cmd, { executionId: str('execution_id'), reason: opt('reason') });
@@ -856,61 +1586,354 @@ export function apply(ctx, config) {
856
1586
  return resumeExecution(store, cmd, { executionId: str('execution_id'), reason: opt('reason') });
857
1587
  case 'execution.abort':
858
1588
  return abortExecution(store, cmd, { executionId: str('execution_id'), reason: opt('reason') });
1589
+ case 'reconcile': {
1590
+ if (control.signal.aborted)
1591
+ return guiFailure('UNAUTHORIZED_PRINCIPAL', '本地控制已关闭。');
1592
+ const reconciled = await runGovernedReconcile({ taskId: str('task_id') }, {
1593
+ kind: 'gui', principal: { sessionId: control.principalSessionId }, agent: control.agent,
1594
+ signal: control.signal,
1595
+ });
1596
+ return plainResult(reconciled, kingdomId);
1597
+ }
859
1598
  case 'start':
860
- // 诚实的 Beta 边界:启动 Worker 需要一个**活的委派父 Agent**
861
- // (in-process provider 从它派生 workspace / 血缘 / 委派深度)。
862
- // HTTP 请求没有 Agent 上下文,所以这条命令只能从 DSH 会话里
863
- // 经 kingdom_start_task 触发。这里明确报错,而不是伪造一次执行。
864
- return guiFailure('EXECUTOR_UNAVAILABLE', 'kingdom_start_task 需要由 DSH 会话中的 Agent 触发(Worker subagent 需要委派父 Agent)。'
865
- + 'GUI 请引导用户在 DSH 会话中说“开始执行这个任务”,或调用 kingdom_start_task 工具。');
1599
+ if (!control.principalSessionId.trim()) {
1600
+ return guiFailure('UNAUTHORIZED_PRINCIPAL', '激活时没有 exact agent.session.id;governed start 拒绝执行。');
1601
+ }
1602
+ const callerAgent = control.agent;
1603
+ if (!callerAgent) {
1604
+ return guiFailure('EXECUTOR_UNAVAILABLE', '激活时没有 exact DSH Agent reference;GUI 不伪造 Worker 父 Agent。');
1605
+ }
1606
+ try {
1607
+ const started = await runGovernedStart({
1608
+ taskId: str('task_id'),
1609
+ grantJson: str('grant_json'),
1610
+ sandboxMode: opt('sandbox_mode'),
1611
+ }, {
1612
+ kind: 'gui',
1613
+ principal: { sessionId: control.principalSessionId },
1614
+ agent: callerAgent,
1615
+ });
1616
+ if (control.signal.aborted) {
1617
+ return plainResult('UNKNOWN/RECOVERY_REQUIRED: GUI Control Session 在 governed start 期间被撤销;请先刷新并对账,不自动重试。', kingdomId);
1618
+ }
1619
+ return plainResult(started, kingdomId);
1620
+ }
1621
+ catch (error) {
1622
+ const detail = error instanceof Error ? error.message : String(error);
1623
+ return plainResult(`UNKNOWN/RECOVERY_REQUIRED: governed start 结果不确定,请先刷新 Projection 对账;不自动重试。(${detail})`, kingdomId);
1624
+ }
866
1625
  default:
867
1626
  return guiFailure('INVALID_INPUT', `未知命令 "${name}"。`);
868
1627
  }
869
1628
  }
870
- // ── Slash 命令(确定性管理入口)──────────────────────────────
1629
+ const parseJsonEnvelope = (rest, allowed, required = []) => {
1630
+ if (!rest)
1631
+ return { ok: false, message: '需要一个 JSON object envelope;不得省略参数。' };
1632
+ let parsed;
1633
+ try {
1634
+ parsed = parseStrictJsonObject(rest);
1635
+ }
1636
+ catch (error) {
1637
+ if (error instanceof DuplicateJsonKeyError) {
1638
+ return { ok: false, message: `JSON envelope 不得包含重复字段 "${error.key}"。` };
1639
+ }
1640
+ return { ok: false, message: 'JSON envelope 非法;必须是单个 JSON object。' };
1641
+ }
1642
+ const value = parsed;
1643
+ const allowedSet = new Set(allowed);
1644
+ const unknown = Object.keys(value).filter(key => !allowedSet.has(key));
1645
+ if (unknown.length > 0)
1646
+ return { ok: false, message: `不允许的字段:${unknown.join(', ')}。` };
1647
+ const missing = required.filter(key => !Object.prototype.hasOwnProperty.call(value, key));
1648
+ if (missing.length > 0)
1649
+ return { ok: false, message: `缺少字段:${missing.join(', ')}。` };
1650
+ return { ok: true, value };
1651
+ };
1652
+ const directCommandParts = (rawInput) => {
1653
+ let line = rawInput.trim();
1654
+ if (line.startsWith('/kingdom'))
1655
+ line = line.slice('/kingdom'.length).trim();
1656
+ const match = line.match(/^(\S+)(?:\s+([\s\S]*))?$/u);
1657
+ return { sub: match?.[1] ?? '', rest: match?.[2] ?? '' };
1658
+ };
1659
+ const ownerWrite = (operation, fn) => {
1660
+ const kingdomId = requireKingdom();
1661
+ if (!kingdomId)
1662
+ return 'OWNER_CONTROL_REQUIRED: 请先直接执行 `/kingdom init` 建立 Owner principal。';
1663
+ try {
1664
+ // Each direct operation gets one IMMEDIATE boundary. Core helpers may
1665
+ // append events inside it; failure rolls back all domain writes.
1666
+ return store.withImmediateTransaction(fn);
1667
+ }
1668
+ catch (error) {
1669
+ const detail = error instanceof Error ? error.message : String(error);
1670
+ return `UNKNOWN/RECOVERY_REQUIRED: Owner operation ${operation} 的写结果不确定;请先 /kingdom status 对账。(${detail})`;
1671
+ }
1672
+ };
1673
+ const validateDirectSessionPayload = (operation, value) => {
1674
+ 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} 未写入。`;
1683
+ };
1684
+ const ownerAuth = () => ownerControlAuth(issueOwnerControlCapability());
871
1685
  ctx.effect(() => ctx.commands.register({
872
1686
  name: 'kingdom',
873
- description: '初始化/接入本地王国,查看王国状态',
874
- input: { hint: 'init | status | reset | help' },
1687
+ description: 'direct Owner Control:初始化、状态、内置 GUI 与精确 JSON 配置;独立 Agent Tool/HTTP payload 不得代行 Owner 写入',
1688
+ input: { hint: 'gui [start|stop] | owner.gui <json> | init | status | ceiling <json> | territory.* <json> | role.* <json> | execution-profile <json> | help' },
875
1689
  handler: async (invocation) => {
876
- const line = invocation.rawInput.trim();
877
- const [sub] = line.split(/[\s]+/u).filter(Boolean);
878
- switch (sub ?? '') {
879
- case 'init': {
880
- const result = manager.init();
881
- return { kind: 'success', text: initResultText(result) };
1690
+ const { sub, rest } = directCommandParts(invocation.rawInput);
1691
+ const auth = ownerAuth();
1692
+ switch (sub) {
1693
+ case 'owner.gui': {
1694
+ const parsed = parseJsonEnvelope(rest, ['kingdomId', 'actions', 'scope', 'ttlMs']);
1695
+ if (!parsed.ok)
1696
+ return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${parsed.message}` };
1697
+ try {
1698
+ const runtime = await ensureGuiServer();
1699
+ const activation = runtime.ownerControl.activate(issueOwnerControlCapability(), parsed.value);
1700
+ const opened = openLocalConsole(`${runtime.address.origin}${activation.launchPath}?ticket=${encodeURIComponent(activation.launchTicket)}`);
1701
+ return {
1702
+ kind: opened ? 'success' : 'error',
1703
+ text: opened
1704
+ ? `人类管理窗口已激活:${runtime.address.origin}/owner;有效期至 ${activation.expiresAt}。网页仅可操作本次授权范围。`
1705
+ : '管理窗口已建立,但浏览器未能打开;未输出一次性票据,请重新直接激活。',
1706
+ };
1707
+ }
1708
+ catch {
1709
+ return { kind: 'error', text: 'OWNER_GUI_ACTIVATION_DENIED: 管理窗口未能激活。请核对严格 JSON 中的王国、动作、具体范围及期限;没有输出控制凭据。' };
1710
+ }
882
1711
  }
883
- case 'status': {
1712
+ case 'gui': {
1713
+ const action = rest.trim().toLowerCase();
1714
+ if (action === 'stop') {
1715
+ await stopGuiServer();
1716
+ return {
1717
+ kind: 'success',
1718
+ text: 'GUI Control Session 已停止;活动控制会话已撤销,本地 GUI server 已关闭(如曾启动)。',
1719
+ };
1720
+ }
1721
+ if (action !== '' && action !== 'start') {
1722
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: `/kingdom gui` 只接受 start、stop 或无参数。' };
1723
+ }
1724
+ try {
1725
+ // ensureGuiServer resolves only from the server's onListening
1726
+ // callback, so port=0 uses the actual bound origin.
1727
+ const runtime = await ensureGuiServer();
1728
+ const activation = runtime.control.activate(invocation.agent);
1729
+ const launchUrl = `${runtime.address.origin}${activation.launchPath}?ticket=${encodeURIComponent(activation.launchTicket)}`;
1730
+ const browserOpened = openLocalConsole(launchUrl);
1731
+ return formatGuiLaunchCommandResult(runtime.address.origin, activation.expiresAt, browserOpened);
1732
+ }
1733
+ catch {
1734
+ // Never reflect an exception into command/done if it could contain
1735
+ // a transport value. The launch ticket is intentionally absent.
1736
+ return {
1737
+ kind: 'error',
1738
+ text: 'UNKNOWN/RECOVERY_REQUIRED: GUI Control Session 启动失败;未输出 launch ticket,请检查本地 GUI server 后重试。',
1739
+ };
1740
+ }
1741
+ }
1742
+ case 'init':
1743
+ if (rest)
1744
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: `/kingdom init` 不接受额外 token。' };
1745
+ try {
1746
+ return { kind: 'success', text: initResultText(manager.init()) };
1747
+ }
1748
+ catch (error) {
1749
+ const detail = error instanceof Error ? error.message : String(error);
1750
+ return { kind: 'error', text: `UNKNOWN/RECOVERY_REQUIRED: init 写结果不确定,请先对账。(${detail})` };
1751
+ }
1752
+ case 'status':
1753
+ if (rest)
1754
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: `/kingdom status` 不接受额外 token。' };
884
1755
  return { kind: 'success', text: store.statusSummary() };
1756
+ case 'reset':
1757
+ if (rest)
1758
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: `/kingdom reset` 不接受额外 token。' };
1759
+ try {
1760
+ return { kind: 'success', text: `已重新接入。${manager.rescan().detail}` };
1761
+ }
1762
+ catch (error) {
1763
+ const detail = error instanceof Error ? error.message : String(error);
1764
+ return { kind: 'error', text: `UNKNOWN/RECOVERY_REQUIRED: reset 读结果不确定,请先对账。(${detail})` };
1765
+ }
1766
+ case 'ceiling': {
1767
+ const parsed = parseJsonEnvelope(rest, ['ceiling', 'ceiling_json', 'clear']);
1768
+ if (!parsed.ok)
1769
+ return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${parsed.message}` };
1770
+ const value = parsed.value;
1771
+ if (value.clear !== undefined && typeof value.clear !== 'boolean')
1772
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: clear 必须是 boolean。' };
1773
+ if (value.clear === true && (value.ceiling !== undefined || value.ceiling_json !== undefined))
1774
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: clear=true 不得同时提供 ceiling。' };
1775
+ let ceilingJson;
1776
+ if (value.clear === true)
1777
+ ceilingJson = null;
1778
+ else if (typeof value.ceiling_json === 'string')
1779
+ ceilingJson = value.ceiling_json;
1780
+ else if (value.ceiling !== undefined) {
1781
+ if (typeof value.ceiling !== 'object' || value.ceiling === null || Array.isArray(value.ceiling))
1782
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ceiling 必须是 capability→boolean object。' };
1783
+ ceilingJson = JSON.stringify(value.ceiling);
1784
+ }
1785
+ else
1786
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: 请提供 ceiling object、ceiling_json 或 clear=true。' };
1787
+ const text = ownerWrite('ceiling', () => setCapabilityCeiling(store, { kingdomId: requireKingdom(), ceilingJson }, auth));
1788
+ return { kind: text.startsWith('UNKNOWN/') || text.startsWith('CONFIG_DENIED') ? 'error' : 'success', text };
1789
+ }
1790
+ case 'territory.create': {
1791
+ const parsed = parseJsonEnvelope(rest, ['name', 'workspace_path', 'summary'], ['name']);
1792
+ if (!parsed.ok || typeof parsed.value.name !== 'string')
1793
+ return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${parsed.ok ? 'name 必须是 string。' : parsed.message}` };
1794
+ const value = parsed.value;
1795
+ if (value.workspace_path !== undefined && typeof value.workspace_path !== 'string')
1796
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: workspace_path 必须是 string。' };
1797
+ if (value.summary !== undefined && typeof value.summary !== 'string')
1798
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: summary 必须是 string。' };
1799
+ const text = ownerWrite('territory.create', () => createTerritory(store, { kingdomId: requireKingdom(), name: value.name, workspacePath: value.workspace_path, summary: value.summary }, auth));
1800
+ return { kind: text.startsWith('UNKNOWN/') || text.startsWith('错误:') ? 'error' : 'success', text };
1801
+ }
1802
+ case 'territory.delete': {
1803
+ const parsed = parseJsonEnvelope(rest, ['territory_id', 'name', 'force', 'reason']);
1804
+ if (!parsed.ok)
1805
+ return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${parsed.message}` };
1806
+ const value = parsed.value;
1807
+ if (typeof value.territory_id !== 'string' && typeof value.name !== 'string')
1808
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: territory_id/name 二选一。' };
1809
+ if (value.force !== undefined && typeof value.force !== 'boolean')
1810
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: force 必须是 boolean。' };
1811
+ if (value.reason !== undefined && typeof value.reason !== 'string')
1812
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: reason 必须是 string。' };
1813
+ const text = ownerWrite('territory.delete', () => deleteTerritory(store, { kingdomId: requireKingdom(), territoryId: value.territory_id, name: value.name, force: value.force, reason: value.reason }, auth));
1814
+ return { kind: text.startsWith('UNKNOWN/') || text.startsWith('错误:') ? 'error' : 'success', text };
1815
+ }
1816
+ case 'territory.supervisor': {
1817
+ const parsed = parseJsonEnvelope(rest, ['territory_id', 'supervisor_binding_id'], ['territory_id', 'supervisor_binding_id']);
1818
+ if (!parsed.ok)
1819
+ return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${parsed.message}` };
1820
+ const value = parsed.value;
1821
+ if (typeof value.territory_id !== 'string' || (value.supervisor_binding_id !== null && typeof value.supervisor_binding_id !== 'string'))
1822
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: territory_id/supervisor_binding_id 类型错误。' };
1823
+ const text = ownerWrite('territory.supervisor', () => setTerritorySupervisor(store, { kingdomId: requireKingdom(), territoryId: value.territory_id, supervisorBindingId: value.supervisor_binding_id }, auth));
1824
+ return { kind: text.startsWith('UNKNOWN/') || text.startsWith('错误:') ? 'error' : 'success', text };
1825
+ }
1826
+ case 'role.bind': {
1827
+ const parsed = parseJsonEnvelope(rest, ['role_type', 'role_name', 'session_id', 'model_name', 'agent_name', 'session_meta'], ['role_type']);
1828
+ if (!parsed.ok || typeof parsed.value.role_type !== 'string')
1829
+ return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${parsed.ok ? 'role_type 必须是 string。' : parsed.message}` };
1830
+ const value = parsed.value;
1831
+ if (value.role_type.toUpperCase() === 'OWNER')
1832
+ 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']) {
1834
+ if (value[key] !== undefined && typeof value[key] !== 'string')
1835
+ return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${key} 必须是 string。` };
1836
+ }
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));
1841
+ return { kind: text.startsWith('UNKNOWN/') || text.startsWith('错误:') || text.startsWith('角色 ') && text.includes('已有绑定') ? 'error' : 'success', text };
1842
+ }
1843
+ case 'role.unbind': {
1844
+ const parsed = parseJsonEnvelope(rest, ['role_type', 'binding_id', 'reason']);
1845
+ if (!parsed.ok)
1846
+ return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${parsed.message}` };
1847
+ const value = parsed.value;
1848
+ if (value.role_type !== undefined && typeof value.role_type !== 'string')
1849
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: role_type 必须是 string。' };
1850
+ if (value.binding_id !== undefined && typeof value.binding_id !== 'string')
1851
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: binding_id 必须是 string。' };
1852
+ if (value.reason !== undefined && typeof value.reason !== 'string')
1853
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: reason 必须是 string。' };
1854
+ if ((value.role_type === undefined) === (value.binding_id === undefined))
1855
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: role_type/binding_id 必须二选一。' };
1856
+ if (String(value.role_type ?? '').toUpperCase() === 'OWNER')
1857
+ return { kind: 'error', text: 'OWNER_CONTROL_REQUIRED: OWNER projection 不得退任。' };
1858
+ const text = ownerWrite('role.unbind', () => unbindRole(store, { kingdomId: requireKingdom(), roleType: value.role_type, bindingId: value.binding_id, reason: value.reason }, auth));
1859
+ return { kind: text.startsWith('UNKNOWN/') || text.startsWith('错误:') ? 'error' : 'success', text };
1860
+ }
1861
+ case 'role.session': {
1862
+ const parsed = parseJsonEnvelope(rest, ['role_type', 'binding_id', 'session_id', 'model_name', 'agent_name', 'session_meta']);
1863
+ if (!parsed.ok)
1864
+ return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${parsed.message}` };
1865
+ const value = parsed.value;
1866
+ // parseJsonEnvelope has already enforced the strict object,
1867
+ // duplicate-key, and unknown-field boundary. For this valid
1868
+ // envelope, Owner is a projection rather than a runtime-session
1869
+ // target: resolve explicit role/binding Owner before the remaining
1870
+ // xor/type/live-session proof so mixed requests fail closed alike.
1871
+ const directKingdomId = requireKingdom();
1872
+ const directBinding = directKingdomId && typeof value.binding_id === 'string'
1873
+ ? resolveBinding(store, directKingdomId, undefined, value.binding_id)
1874
+ : null;
1875
+ const explicitOwner = typeof value.role_type === 'string'
1876
+ && value.role_type.trim().toUpperCase() === 'OWNER';
1877
+ if (explicitOwner || directBinding?.role_type.trim().toUpperCase() === 'OWNER') {
1878
+ return { kind: 'error', text: 'OWNER_CONTROL_REQUIRED: OWNER.session_id 永远不作为 Owner Authority。' };
1879
+ }
1880
+ if ((value.role_type === undefined) === (value.binding_id === undefined))
1881
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: role_type/binding_id 必须二选一。' };
1882
+ for (const key of ['role_type', 'binding_id', 'session_id', 'model_name', 'agent_name', 'session_meta']) {
1883
+ if (value[key] !== undefined && value[key] !== null && typeof value[key] !== 'string')
1884
+ return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${key} 必须是 string 或 null。` };
1885
+ }
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));
1890
+ return { kind: text.startsWith('UNKNOWN/') || text.startsWith('错误:') ? 'error' : 'success', text };
885
1891
  }
886
- case 'reset': {
887
- const result = manager.rescan();
888
- return { kind: 'success', text: `已重新接入。${result.detail}` };
1892
+ case 'execution-profile': {
1893
+ const parsed = parseJsonEnvelope(rest, ['role_type', 'binding_id', 'provider', 'model', 'clear']);
1894
+ if (!parsed.ok)
1895
+ return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${parsed.message}` };
1896
+ const value = parsed.value;
1897
+ if ((value.role_type === undefined) === (value.binding_id === undefined))
1898
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: role_type/binding_id 必须二选一。' };
1899
+ if (value.clear !== undefined && typeof value.clear !== 'boolean')
1900
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: clear 必须是 boolean。' };
1901
+ for (const key of ['role_type', 'binding_id', 'provider', 'model']) {
1902
+ if (value[key] !== undefined && typeof value[key] !== 'string')
1903
+ return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: ${key} 必须是 string。` };
1904
+ }
1905
+ if (value.clear !== true && typeof value.provider !== 'string' && typeof value.model !== 'string')
1906
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: 至少提供 provider/model,或 clear=true。' };
1907
+ const text = ownerWrite('execution-profile', () => setExecutionProfile(store, { kingdomId: requireKingdom(), roleType: value.role_type, bindingId: value.binding_id, profile: value.clear === true ? null : { ...(typeof value.provider === 'string' ? { provider: value.provider } : {}), ...(typeof value.model === 'string' ? { model: value.model } : {}) } }, auth));
1908
+ return { kind: text.startsWith('UNKNOWN/') || text.startsWith('错误:') ? 'error' : 'success', text };
889
1909
  }
890
1910
  case 'help':
891
1911
  case '':
1912
+ if (rest)
1913
+ return { kind: 'error', text: 'INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: help 不接受额外 token。' };
892
1914
  return {
893
1915
  kind: 'success',
894
1916
  text: [
895
- 'dsh-Kingdom v0.4(换届 + 会话归属)',
896
- '/kingdom init 初始化或接入本地王国(幂等;首次初始化后会给出创建方式二选一)',
897
- '/kingdom status 查看王国真实状态',
898
- '/kingdom reset 重新扫描接入(不删除数据)',
899
- '组织王国:先为角色绑定独立会话(kingdom_bind_session),不要默认由当前会话代跑全部角色',
900
- '换届与归属:kingdom_unbind_role 解绑;kingdom_bind_session 绑定/更换角色会话(含 model/agent 预留字段)',
901
- 'GUI:前端页面由 GUI 部署方提供;后端网关 http://127.0.0.1:<guiPort> 填入前端连接框(默认关闭,配置 guiPort 开启)',
902
- '也可用自然语言:初始化王国 / 创建领地 / 绑定角色 / 规划任务 / 派发任务 / 审查结果',
903
- '',
904
- '任务闭环:plan → assign → start(Worker 执行)→ review(ACCEPT/REWORK/FAIL)',
905
- 'Worker 交回的结果只是待审查的 Claim(任务进入 REVIEW),',
906
- '只有 Supervisor 的 ACCEPT 才能让任务成为 DONE。',
907
- '',
908
- 'GUI:kingdom_snapshot / kingdom_task_detail 返回结构化 JSON;',
909
- '配置 guiPort 可开本地 HTTP 通道(默认关闭,只绑 127.0.0.1)。',
1917
+ 'dsh-Kingdom Owner Control Plane(Owner ≠ Agent ≠ Session)',
1918
+ '/kingdom init 原子初始化/接入;OWNER.session_id 永远为 null',
1919
+ '/kingdom status 查看真实状态(只读)',
1920
+ '/kingdom owner.gui <严格 JSON> 激活具体动作/资源范围的短期人类管理窗口;/owner 页面提供激活格式说明',
1921
+ '/kingdom ceiling {"ceiling":{"tool:pwsh":true}} | {"clear":true}',
1922
+ '/kingdom territory.create {"name":"研发领","workspace_path":"C:/work"}',
1923
+ '/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":"换届"}',
1927
+ '/kingdom role.session {"binding_id":"...","session_id":"真实 DSH session"}',
1928
+ '/kingdom execution-profile {"binding_id":"...","provider":"spawn","model":"..."}',
1929
+ '以上写命令只接受一个 JSON object;unknown key、额外 token、Owner Session 均拒绝。',
1930
+ 'Agent Role Plane:plan → assign → kingdom_start_task_governed(CANONICAL HEADLESS,Persistent Worker) → Claim(REVIEW) → Supervisor ACCEPT/REWORK/FAIL。',
1931
+ 'LEGACY_COMPAT:kingdom_start_task 仅在用户明确选择旧 one-shot 兼容模式时显式调用;persistent 失败不会自动 fallback。',
1932
+ 'GUI 需先由 direct `/kingdom gui` 激活;HTTP 只作无权 transport,命令仍经同一 Core/Role/Capability gates。',
910
1933
  ].join('\n'),
911
1934
  };
912
1935
  default:
913
- return { kind: 'error', text: `未知子命令 "${sub}"。可用:init / status / reset / help` };
1936
+ return { kind: 'error', text: `INPUT_DENIED [OWNER_COMMAND_GRAMMAR]: 未知子命令 "${sub}"。请使用 /kingdom help。` };
914
1937
  }
915
1938
  },
916
1939
  }), 'dsh-kingdom: /kingdom command');