@jingyi0605/codingns4dsh 0.2.1-rc.5 → 0.2.1-rc.7

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 (174) hide show
  1. package/README.en.md +4 -4
  2. package/README.md +5 -5
  3. package/data/build/dist/bootstrap/dsh-peer-host-preboot-shim.d.ts +1 -1
  4. package/data/build/dist/bootstrap/dsh-peer-host-preboot-shim.js +1 -1
  5. package/data/build/dist/client/bundle.js +1550 -148
  6. package/data/build/dist/client/bundle.js.map +1 -1
  7. package/data/build/dist/client/cli-slots.js +2 -2
  8. package/data/build/dist/client/cli-slots.js.map +1 -1
  9. package/data/build/dist/client/client.delegate-command.js +3 -2
  10. package/data/build/dist/client/client.delegate-command.js.map +1 -1
  11. package/data/build/dist/client/client.magpie-model-tag.js +24 -8
  12. package/data/build/dist/client/client.magpie-model-tag.js.map +1 -1
  13. package/data/build/dist/client/client.native-workspace-store.js +26 -15
  14. package/data/build/dist/client/client.native-workspace-store.js.map +1 -1
  15. package/data/build/dist/client/client.session-translation.js +25 -0
  16. package/data/build/dist/client/client.session-translation.js.map +1 -1
  17. package/data/build/dist/client/client.transport.js +224 -0
  18. package/data/build/dist/client/client.transport.js.map +1 -1
  19. package/data/build/dist/client/delegate-command.js +2 -1
  20. package/data/build/dist/client/delegate-command.js.map +1 -1
  21. package/data/build/dist/client/desktop-assistant.js +224 -0
  22. package/data/build/dist/client/features/index.d.ts +2 -1
  23. package/data/build/dist/client/features/index.d.ts.map +1 -1
  24. package/data/build/dist/client/features/index.js +3 -1
  25. package/data/build/dist/client/features/index.js.map +1 -1
  26. package/data/build/dist/client/features/magpie-management-page.d.ts +7 -3
  27. package/data/build/dist/client/features/magpie-management-page.d.ts.map +1 -1
  28. package/data/build/dist/client/features/magpie-management-page.js +142 -8
  29. package/data/build/dist/client/features/magpie-management-page.js.map +1 -1
  30. package/data/build/dist/client/features/workspace-mcp.d.ts +3 -0
  31. package/data/build/dist/client/features/workspace-mcp.d.ts.map +1 -0
  32. package/data/build/dist/client/features/workspace-mcp.js +320 -0
  33. package/data/build/dist/client/features/workspace-mcp.js.map +1 -0
  34. package/data/build/dist/client/locale.d.ts.map +1 -1
  35. package/data/build/dist/client/locale.js +180 -0
  36. package/data/build/dist/client/locale.js.map +1 -1
  37. package/data/build/dist/client/locales/magpie.d.ts.map +1 -1
  38. package/data/build/dist/client/locales/magpie.js +12 -0
  39. package/data/build/dist/client/locales/magpie.js.map +1 -1
  40. package/data/build/dist/client/locales/panels.d.ts.map +1 -1
  41. package/data/build/dist/client/locales/panels.js +18 -0
  42. package/data/build/dist/client/locales/panels.js.map +1 -1
  43. package/data/build/dist/client/locales/usage.d.ts.map +1 -1
  44. package/data/build/dist/client/locales/usage.js +14 -0
  45. package/data/build/dist/client/locales/usage.js.map +1 -1
  46. package/data/build/dist/client/magpie-model-tag.d.ts +4 -2
  47. package/data/build/dist/client/magpie-model-tag.d.ts.map +1 -1
  48. package/data/build/dist/client/magpie-model-tag.js +25 -8
  49. package/data/build/dist/client/magpie-model-tag.js.map +1 -1
  50. package/data/build/dist/client/native-model-translation-menu.d.ts +1 -0
  51. package/data/build/dist/client/native-model-translation-menu.d.ts.map +1 -1
  52. package/data/build/dist/client/native-model-translation-menu.js +2 -2
  53. package/data/build/dist/client/native-model-translation-menu.js.map +1 -1
  54. package/data/build/dist/client/session-translation-chat.d.ts.map +1 -1
  55. package/data/build/dist/client/session-translation-chat.js +6 -2
  56. package/data/build/dist/client/session-translation-chat.js.map +1 -1
  57. package/data/build/dist/client/settings-section.d.ts.map +1 -1
  58. package/data/build/dist/client/settings-section.js +6 -3
  59. package/data/build/dist/client/settings-section.js.map +1 -1
  60. package/data/build/dist/client/subscription-slot.d.ts.map +1 -1
  61. package/data/build/dist/client/subscription-slot.js +104 -11
  62. package/data/build/dist/client/subscription-slot.js.map +1 -1
  63. package/data/build/dist/dsh-capabilities/host/workspace-approval-adapter.d.ts +18 -0
  64. package/data/build/dist/dsh-capabilities/host/workspace-approval-adapter.d.ts.map +1 -0
  65. package/data/build/dist/dsh-capabilities/host/workspace-approval-adapter.js +172 -0
  66. package/data/build/dist/dsh-capabilities/host/workspace-approval-adapter.js.map +1 -0
  67. package/data/build/dist/dsh-capabilities/host/workspace-pending-questions-adapter.d.ts +28 -0
  68. package/data/build/dist/dsh-capabilities/host/workspace-pending-questions-adapter.d.ts.map +1 -0
  69. package/data/build/dist/dsh-capabilities/host/workspace-pending-questions-adapter.js +103 -0
  70. package/data/build/dist/dsh-capabilities/host/workspace-pending-questions-adapter.js.map +1 -0
  71. package/data/build/dist/dsh-capabilities/host/workspace-session-control-adapter.d.ts +26 -0
  72. package/data/build/dist/dsh-capabilities/host/workspace-session-control-adapter.d.ts.map +1 -0
  73. package/data/build/dist/dsh-capabilities/host/workspace-session-control-adapter.js +86 -0
  74. package/data/build/dist/dsh-capabilities/host/workspace-session-control-adapter.js.map +1 -0
  75. package/data/build/dist/dsh-capabilities/matrix.d.ts.map +1 -1
  76. package/data/build/dist/dsh-capabilities/matrix.js +17 -14
  77. package/data/build/dist/dsh-capabilities/matrix.js.map +1 -1
  78. package/data/build/dist/dsh-capabilities/routes.d.ts.map +1 -1
  79. package/data/build/dist/dsh-capabilities/routes.js +19 -10
  80. package/data/build/dist/dsh-capabilities/routes.js.map +1 -1
  81. package/data/build/dist/dsh-capabilities/types.d.ts +1 -1
  82. package/data/build/dist/dsh-capabilities/types.d.ts.map +1 -1
  83. package/data/build/dist/dsh-capabilities/types.js.map +1 -1
  84. package/data/build/dist/host/cli-adapters/claude-model-options.d.ts.map +1 -1
  85. package/data/build/dist/host/cli-adapters/claude-model-options.js +13 -2
  86. package/data/build/dist/host/cli-adapters/claude-model-options.js.map +1 -1
  87. package/data/build/dist/host/cli-adapters/dsh-message-projector.d.ts.map +1 -1
  88. package/data/build/dist/host/cli-adapters/dsh-message-projector.js +6 -2
  89. package/data/build/dist/host/cli-adapters/dsh-message-projector.js.map +1 -1
  90. package/data/build/dist/host/cli-adapters/feature.d.ts.map +1 -1
  91. package/data/build/dist/host/cli-adapters/feature.js +12 -4
  92. package/data/build/dist/host/cli-adapters/feature.js.map +1 -1
  93. package/data/build/dist/host/cli-adapters/magpie-usage.d.ts +13 -2
  94. package/data/build/dist/host/cli-adapters/magpie-usage.d.ts.map +1 -1
  95. package/data/build/dist/host/cli-adapters/magpie-usage.js +186 -14
  96. package/data/build/dist/host/cli-adapters/magpie-usage.js.map +1 -1
  97. package/data/build/dist/host/dsh-runtime-version.d.ts.map +1 -1
  98. package/data/build/dist/host/dsh-runtime-version.js +4 -1
  99. package/data/build/dist/host/dsh-runtime-version.js.map +1 -1
  100. package/data/build/dist/host/features/index.d.ts +1 -0
  101. package/data/build/dist/host/features/index.d.ts.map +1 -1
  102. package/data/build/dist/host/features/index.js +3 -0
  103. package/data/build/dist/host/features/index.js.map +1 -1
  104. package/data/build/dist/host/features/workspace-mcp.d.ts +9 -0
  105. package/data/build/dist/host/features/workspace-mcp.d.ts.map +1 -0
  106. package/data/build/dist/host/features/workspace-mcp.js +197 -0
  107. package/data/build/dist/host/features/workspace-mcp.js.map +1 -0
  108. package/data/build/dist/host/index.d.ts.map +1 -1
  109. package/data/build/dist/host/index.js +6 -2
  110. package/data/build/dist/host/index.js.map +1 -1
  111. package/data/build/dist/host/native-session-bridge.d.ts +2 -0
  112. package/data/build/dist/host/native-session-bridge.d.ts.map +1 -1
  113. package/data/build/dist/host/native-session-bridge.js +30 -0
  114. package/data/build/dist/host/native-session-bridge.js.map +1 -1
  115. package/data/build/dist/host/rpc.d.ts +4 -1
  116. package/data/build/dist/host/rpc.d.ts.map +1 -1
  117. package/data/build/dist/host/rpc.js +57 -9
  118. package/data/build/dist/host/rpc.js.map +1 -1
  119. package/data/build/dist/host/session-handle-eviction.d.ts +4 -2
  120. package/data/build/dist/host/session-handle-eviction.d.ts.map +1 -1
  121. package/data/build/dist/host/session-handle-eviction.js +10 -9
  122. package/data/build/dist/host/session-handle-eviction.js.map +1 -1
  123. package/data/build/dist/host/terminal/terminal-process-service.d.ts +22 -0
  124. package/data/build/dist/host/terminal/terminal-process-service.d.ts.map +1 -1
  125. package/data/build/dist/host/terminal/terminal-process-service.js +102 -0
  126. package/data/build/dist/host/terminal/terminal-process-service.js.map +1 -1
  127. package/data/build/dist/host/terminal/terminal-service.d.ts +5 -1
  128. package/data/build/dist/host/terminal/terminal-service.d.ts.map +1 -1
  129. package/data/build/dist/host/terminal/terminal-service.js +32 -0
  130. package/data/build/dist/host/terminal/terminal-service.js.map +1 -1
  131. package/data/build/dist/host/workspace-mcp/api-key-store.d.ts +16 -0
  132. package/data/build/dist/host/workspace-mcp/api-key-store.d.ts.map +1 -0
  133. package/data/build/dist/host/workspace-mcp/api-key-store.js +65 -0
  134. package/data/build/dist/host/workspace-mcp/api-key-store.js.map +1 -0
  135. package/data/build/dist/host/workspace-mcp/tools.d.ts +54 -0
  136. package/data/build/dist/host/workspace-mcp/tools.d.ts.map +1 -0
  137. package/data/build/dist/host/workspace-mcp/tools.js +800 -0
  138. package/data/build/dist/host/workspace-mcp/tools.js.map +1 -0
  139. package/data/build/dist/host/workspace-mcp/transport.d.ts +35 -0
  140. package/data/build/dist/host/workspace-mcp/transport.d.ts.map +1 -0
  141. package/data/build/dist/host/workspace-mcp/transport.js +575 -0
  142. package/data/build/dist/host/workspace-mcp/transport.js.map +1 -0
  143. package/data/build/dist/shared/contracts/cli-adapter.d.ts +2 -0
  144. package/data/build/dist/shared/contracts/cli-adapter.d.ts.map +1 -1
  145. package/data/build/dist/shared/contracts/errors.d.ts +2 -2
  146. package/data/build/dist/shared/contracts/errors.d.ts.map +1 -1
  147. package/data/build/dist/shared/contracts/errors.js +1 -1
  148. package/data/build/dist/shared/contracts/errors.js.map +1 -1
  149. package/data/build/dist/shared/contracts/magpie-management.d.ts +15 -1
  150. package/data/build/dist/shared/contracts/magpie-management.d.ts.map +1 -1
  151. package/data/build/dist/shared/contracts/session-translation.d.ts +4 -0
  152. package/data/build/dist/shared/contracts/session-translation.d.ts.map +1 -1
  153. package/data/build/dist/shared/contracts/session-translation.js +21 -0
  154. package/data/build/dist/shared/contracts/session-translation.js.map +1 -1
  155. package/data/build/dist/shared/contracts/subscription.d.ts +7 -0
  156. package/data/build/dist/shared/contracts/subscription.d.ts.map +1 -1
  157. package/data/build/dist/shared/contracts/subscription.js.map +1 -1
  158. package/data/build/dist/shared/contracts/terminal.d.ts +2 -0
  159. package/data/build/dist/shared/contracts/terminal.d.ts.map +1 -1
  160. package/data/build/dist/shared/contracts/version.d.ts +7 -7
  161. package/data/build/dist/shared/contracts/version.d.ts.map +1 -1
  162. package/data/build/dist/shared/contracts/version.js +11 -17
  163. package/data/build/dist/shared/contracts/version.js.map +1 -1
  164. package/data/build/dist/shared/index.d.ts +1 -1
  165. package/data/build/dist/shared/index.d.ts.map +1 -1
  166. package/data/build/dist/shared/index.js +1 -1
  167. package/data/build/dist/shared/index.js.map +1 -1
  168. package/package.json +3 -3
  169. package/profile/README.md +6 -6
  170. package/profile/package.json +3 -3
  171. package/profile/scripts/check-dsh-install.mjs +14 -1
  172. package/profile/version.json +2 -2
  173. package/scripts/check-dsh-install.mjs +14 -1
  174. package/version.json +2 -2
@@ -0,0 +1,800 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ const MAX_PAGE_SIZE = 100;
3
+ const MAX_QUESTIONS = 16;
4
+ const MAX_TEXT_LENGTH = 64 * 1024;
5
+ const MAX_TERMINAL_READ_BYTES = 64 * 1024;
6
+ const MAX_TERMINAL_INPUT_BYTES = 16 * 1024;
7
+ const MAX_PENDING_PERMISSIONS = 256;
8
+ const PERMISSION_RETENTION_MS = 15 * 60 * 1000;
9
+ /**
10
+ * Fixed public contracts for local workspace/session/terminal control.
11
+ * Authentication and local full-workspace access are enforced by the transport;
12
+ * every resource identifier is still validated and rechecked by its handler.
13
+ * Workspace filesystem roots are never returned in tool results.
14
+ */
15
+ export const WORKSPACE_MCP_TOOL_DEFINITIONS = [
16
+ defineTool('workspace_list', 'List managed workspaces without exposing filesystem roots.', {}),
17
+ defineTool('session_list', 'List bounded session metadata in managed workspaces.', {
18
+ workspaceId: stringField('Optional managed workspace identifier.'),
19
+ limit: integerField(1, MAX_PAGE_SIZE, 'Maximum number of sessions to return.'),
20
+ offset: integerField(0, Number.MAX_SAFE_INTEGER, 'Number of matching sessions to skip.'),
21
+ }),
22
+ defineTool('session_create', 'Create a native session in a managed workspace and optionally send an initial prompt. adapterId defaults to dsh; modelId is selected for the new session.', {
23
+ workspaceId: stringField('Managed workspace identifier.'),
24
+ adapterId: stringField('Optional adapter ID. Defaults to dsh.'),
25
+ modelId: stringField('Optional model ID, validated against the matching model catalog.'),
26
+ prompt: stringField('Initial prompt. Required when adapterId is an external adapter.'),
27
+ }, ['workspaceId']),
28
+ defineTool('session_continue', 'Queue a message to an exact existing session without replacing it.', {
29
+ sessionId: stringField('Existing session identifier.'),
30
+ message: stringField('Text to send.'),
31
+ }, ['sessionId', 'message']),
32
+ defineTool('session_questions', 'List unresolved native questions for an existing session.', {
33
+ sessionId: stringField('Existing session identifier.'),
34
+ }, ['sessionId']),
35
+ defineTool('session_answer', 'Answer one exact open native question request using its call ID.', {
36
+ sessionId: stringField('Existing session identifier.'),
37
+ callId: stringField('Exact pending question call ID returned by session_questions.'),
38
+ answer: {
39
+ type: 'object',
40
+ properties: {
41
+ answers: {
42
+ type: 'array',
43
+ minItems: 1,
44
+ maxItems: 16,
45
+ items: {
46
+ type: 'object',
47
+ properties: {
48
+ id: { type: 'string', minLength: 1, maxLength: 128 },
49
+ selected: { type: 'array', maxItems: 32, items: { type: 'string', maxLength: 256 } },
50
+ custom: { type: 'string', maxLength: 4096 },
51
+ },
52
+ required: ['id', 'selected'],
53
+ additionalProperties: false,
54
+ },
55
+ },
56
+ },
57
+ required: ['answers'],
58
+ additionalProperties: false,
59
+ description: 'Complete answers for every question in that request.',
60
+ },
61
+ }, ['sessionId', 'callId', 'answer']),
62
+ defineTool('session_request_permission', 'Request a decision through native DSH approval for an existing active turn; callers cannot decide approval.', {
63
+ sessionId: stringField('Existing session identifier.'),
64
+ toolName: stringField('Operation requiring approval.'),
65
+ reason: stringField('Explanation shown in the native approval UI.'),
66
+ callId: stringField('Optional operation identifier.'),
67
+ }, ['sessionId', 'toolName', 'reason']),
68
+ defineTool('session_permission_status', 'Read the current outcome for an exact DSH-issued permission request ID.', {
69
+ sessionId: stringField('Existing session identifier.'),
70
+ requestId: stringField('DSH-issued approval request ID from the session event log.'),
71
+ }, ['sessionId', 'requestId']),
72
+ defineTool('terminal_list', 'List terminal metadata in a managed workspace.', {
73
+ workspaceId: stringField('Managed workspace identifier.'),
74
+ sessionId: stringField('Live session owning the terminal instances.'),
75
+ }, ['workspaceId', 'sessionId']),
76
+ defineTool('terminal_read', 'Read a bounded output snapshot from a live workspace terminal.', {
77
+ workspaceId: stringField('Managed workspace identifier.'),
78
+ sessionId: stringField('Live session owning the terminal instance.'),
79
+ terminalId: stringField('Running terminal identifier from terminal_list.'),
80
+ limitBytes: integerField(1, MAX_TERMINAL_READ_BYTES, 'Maximum UTF-8 bytes to return.'),
81
+ }, ['workspaceId', 'sessionId', 'terminalId']),
82
+ defineTool('terminal_write', 'Write bounded input through the existing terminal process service.', {
83
+ workspaceId: stringField('Managed workspace identifier.'),
84
+ sessionId: stringField('Live session owning the terminal instance.'),
85
+ terminalId: stringField('Running terminal identifier from terminal_list.'),
86
+ data: stringField('Input text to write.'),
87
+ }, ['workspaceId', 'sessionId', 'terminalId', 'data']),
88
+ ];
89
+ /** Create an isolated tool dispatcher over existing Host services. */
90
+ export function createWorkspaceMcpToolHandlers(services, adapters) {
91
+ const permissions = new Map();
92
+ const pendingPermissionSessions = new Map();
93
+ let disposed = false;
94
+ let permissionLifetime = new AbortController();
95
+ const sessionControl = adapters?.sessionControl;
96
+ const pendingQuestions = adapters?.pendingQuestions;
97
+ const approval = adapters?.approval;
98
+ return {
99
+ listTools(signal) {
100
+ signal.throwIfAborted();
101
+ return WORKSPACE_MCP_TOOL_DEFINITIONS;
102
+ },
103
+ async callTool(name, args, signal) {
104
+ if (disposed)
105
+ return toolError('WORKSPACE_MCP_STOPPED', 'Workspace MCP tools are stopped.');
106
+ try {
107
+ signal.throwIfAborted();
108
+ switch (name) {
109
+ case 'workspace_list': return toolSuccess(listWorkspaces(services));
110
+ case 'session_list': return toolSuccess(await listSessions(services, sessionControl, args, signal));
111
+ case 'session_create': return toolSuccess(await createSession(services, sessionControl, args, signal, adapters));
112
+ case 'session_continue': return toolSuccess(await continueSession(services, sessionControl, args, signal));
113
+ case 'session_questions': return toolSuccess(listPendingQuestions(services, pendingQuestions, args));
114
+ case 'session_answer': return toolSuccess(answerPendingQuestion(services, pendingQuestions, args));
115
+ case 'session_request_permission': return toolSuccess(await requestPermission(services, approval, permissions, pendingPermissionSessions, args, permissionLifetime));
116
+ case 'session_permission_status': return toolSuccess(permissionStatus(services, permissions, args));
117
+ case 'terminal_list': return toolSuccess(listTerminals(services, args));
118
+ case 'terminal_read': return toolSuccess(await readTerminal(services, args, signal));
119
+ case 'terminal_write': return toolSuccess(await writeTerminal(services, args, signal));
120
+ default: return toolError('WORKSPACE_MCP_TOOL_NOT_FOUND', 'Unknown workspace MCP tool.');
121
+ }
122
+ }
123
+ catch (error) {
124
+ if (signal.aborted)
125
+ return toolError('WORKSPACE_MCP_CANCELLED', 'The request was cancelled.');
126
+ return toolError(error instanceof WorkspaceToolError ? error.code : 'WORKSPACE_MCP_OPERATION_FAILED', error instanceof WorkspaceToolError ? error.message : 'The requested operation could not be completed.');
127
+ }
128
+ },
129
+ async dispose() {
130
+ disposed = true;
131
+ this.abortRequests();
132
+ permissions.clear();
133
+ pendingPermissionSessions.clear();
134
+ approval?.dispose?.();
135
+ },
136
+ abortRequests() {
137
+ permissionLifetime.abort();
138
+ permissionLifetime = new AbortController();
139
+ for (const permission of permissions.values()) {
140
+ if (permission.status === 'pending')
141
+ permission.status = 'cancelled';
142
+ permission.turnEndSignal.abort();
143
+ }
144
+ pendingPermissionSessions.clear();
145
+ },
146
+ };
147
+ }
148
+ function defineTool(name, description, properties, required = []) {
149
+ return {
150
+ name,
151
+ description,
152
+ inputSchema: { type: 'object', properties, required: [...required], additionalProperties: false },
153
+ };
154
+ }
155
+ function stringField(description) {
156
+ return { type: 'string', minLength: 1, maxLength: MAX_TEXT_LENGTH, description };
157
+ }
158
+ function integerField(minimum, maximum, description) {
159
+ return { type: 'integer', minimum, maximum, description };
160
+ }
161
+ function listWorkspaces(services) {
162
+ const rows = workspaceRows(services).map(({ id, name, sessionIds, archivedSessionIds }) => ({
163
+ workspaceId: id,
164
+ name: name.slice(0, 160),
165
+ sessionCount: sessionIds.filter((sessionId) => !archivedSessionIds.includes(sessionId)).length,
166
+ }));
167
+ return { workspaces: rows.slice(0, MAX_PAGE_SIZE), hasMore: rows.length > MAX_PAGE_SIZE };
168
+ }
169
+ async function listSessions(services, capability, args, signal) {
170
+ const workspaceId = args.workspaceId === undefined ? undefined : requireId(args.workspaceId, 'workspaceId');
171
+ const limit = args.limit === undefined ? 50 : requireInteger(args.limit, 1, MAX_PAGE_SIZE, 'limit');
172
+ const offset = args.offset === undefined ? 0 : requireInteger(args.offset, 0, Number.MAX_SAFE_INTEGER, 'offset');
173
+ const workspaces = workspaceId === undefined ? workspaceRows(services) : [requireWorkspace(services, workspaceId)];
174
+ if (capability === undefined)
175
+ return unavailable('WORKSPACE_MCP_SESSION_LIST_UNAVAILABLE', 'The versioned DSH workspace session-list capability is unavailable.');
176
+ let listResult;
177
+ try {
178
+ listResult = await capability.list(signal);
179
+ }
180
+ catch {
181
+ return unavailable('WORKSPACE_MCP_SESSION_LIST_UNAVAILABLE', 'DSH could not read the session list.');
182
+ }
183
+ const remote = asRecord(listResult);
184
+ if (!Array.isArray(remote?.items))
185
+ return unavailable('WORKSPACE_MCP_SESSION_LIST_UNAVAILABLE', 'DSH did not return a session list.');
186
+ const listed = remote.items;
187
+ const workspaceSessions = new Map(workspaces.map((workspace) => [workspace.id, new Set(workspace.sessionIds.filter((id) => !workspace.archivedSessionIds.includes(id)))]));
188
+ const metadata = new Map();
189
+ for (const raw of listed) {
190
+ const sessionId = sessionIdOf(raw);
191
+ if (sessionId === null)
192
+ continue;
193
+ const workspace = workspaces.find((entry) => workspaceSessions.get(entry.id)?.has(sessionId) === true);
194
+ const origin = readText(asRecord(raw), ['origin']);
195
+ const record = asRecord(raw);
196
+ const parentSessionId = readText(record, ['parentSessionId']);
197
+ const parentWorkspace = parentSessionId === null ? undefined : workspaces.find((entry) => workspaceSessions.get(entry.id)?.has(parentSessionId) === true);
198
+ const effectiveWorkspace = workspace ?? (origin === 'subagent' ? parentWorkspace : undefined);
199
+ if (effectiveWorkspace === undefined)
200
+ continue;
201
+ metadata.set(sessionId, { ...sessionMetadata(raw, sessionId), workspaceId: effectiveWorkspace.id });
202
+ }
203
+ const ordered = [...metadata.values()]
204
+ .sort((left, right) => Number(right.updatedAt ?? 0) - Number(left.updatedAt ?? 0));
205
+ const page = ordered.slice(offset, offset + limit);
206
+ return {
207
+ sessions: page,
208
+ offset,
209
+ limit,
210
+ hasMore: offset + Math.min(limit, Math.max(0, ordered.length - offset)) < ordered.length,
211
+ };
212
+ }
213
+ async function createSession(services, capability, args, signal, adapters) {
214
+ const workspaceId = requireId(args.workspaceId, 'workspaceId');
215
+ requireWorkspace(services, workspaceId);
216
+ const root = services.resolveManagedWorkspaceRoot?.(workspaceId);
217
+ if (typeof root !== 'string' || root.trim() === '')
218
+ return unavailable('WORKSPACE_MCP_WORKSPACE_UNAVAILABLE', 'The managed workspace is unavailable.');
219
+ const adapterId = args.adapterId === undefined ? 'dsh' : requireAdapterId(args.adapterId);
220
+ const modelId = args.modelId === undefined ? undefined : requireModelId(args.modelId);
221
+ const prompt = args.prompt === undefined ? undefined : requireText(args.prompt, 'prompt', MAX_TEXT_LENGTH);
222
+ if (adapterId !== 'dsh' && prompt === undefined)
223
+ return invalid('WORKSPACE_MCP_INITIAL_PROMPT_REQUIRED', 'External adapter sessions require an initial prompt.');
224
+ if (capability === undefined)
225
+ return unavailable('WORKSPACE_MCP_SESSION_CREATE_UNAVAILABLE', 'The versioned DSH workspace session-control capability is unavailable.');
226
+ let dshRoute;
227
+ const adapterRegistry = adapterId === 'dsh' ? undefined : adapters?.getAdapterRegistry?.();
228
+ if (adapterId === 'dsh' && modelId !== undefined) {
229
+ if (capability.selectModel === undefined)
230
+ return unavailable('WORKSPACE_MCP_MODEL_SELECTION_UNAVAILABLE', 'Session-local model selection is unavailable on this DSH Host.');
231
+ dshRoute = findDshModel(await capability.modelCatalog(), modelId);
232
+ if (dshRoute === undefined)
233
+ return invalid('WORKSPACE_MCP_MODEL_INVALID', 'The requested model is not uniquely available in the DSH model catalog.');
234
+ }
235
+ else if (adapterId !== 'dsh') {
236
+ if (!EXTERNAL_ADAPTER_ID.test(adapterId))
237
+ return invalid('WORKSPACE_MCP_ADAPTER_INVALID', 'adapterId is invalid.');
238
+ if (adapterRegistry === undefined)
239
+ return unavailable('WORKSPACE_MCP_ADAPTER_UNAVAILABLE', 'The external adapter registry is unavailable.');
240
+ const catalog = await adapterRegistry.catalogForUse([adapterId]);
241
+ const descriptor = catalog.find((entry) => entry.id === adapterId);
242
+ if (descriptor === undefined || !descriptor.enabled || !descriptor.installed)
243
+ return unavailable('WORKSPACE_MCP_ADAPTER_UNAVAILABLE', 'The requested adapter is not installed and enabled.');
244
+ if (modelId !== undefined && !catalogHasModel(await adapterRegistry.models(adapterId), modelId)) {
245
+ return invalid('WORKSPACE_MCP_MODEL_INVALID', 'The requested model is not in the adapter model catalog.');
246
+ }
247
+ }
248
+ signal.throwIfAborted();
249
+ // DSH's supported create contract accepts workspaceId. Do not send cwd or
250
+ // synthetic session IDs: DSH allocates the canonical identity itself.
251
+ const created = await capability.create({ workspaceId }, signal);
252
+ const sessionId = sessionIdOf(created);
253
+ if (sessionId === null)
254
+ return unavailable('WORKSPACE_MCP_SESSION_CREATE_UNAVAILABLE', 'DSH did not return a valid session identity.');
255
+ if (adapterId !== 'dsh') {
256
+ try {
257
+ adapterRegistry.setSession(sessionId, { adapterId, ...(modelId === undefined ? {} : { modelId }) }, { rememberPreference: false, inheritSelection: false });
258
+ await adapterRegistry.flushSessionBindings();
259
+ }
260
+ catch {
261
+ return { ok: false, sessionId, workspaceId, adapterId, ...(modelId === undefined ? {} : { modelId }), status: 'uncertain', error: { code: 'WORKSPACE_MCP_ADAPTER_BINDING_FAILED', message: 'The session was created but its adapter could not be bound.' } };
262
+ }
263
+ }
264
+ // Resolving the newly-created Session eagerly makes its Agent identity and
265
+ // initial model route stable before any optional prompt is admitted.
266
+ if (adapterId === 'dsh') {
267
+ try {
268
+ await capability.prepareSession?.(sessionId, signal);
269
+ if (modelId !== undefined && dshRoute !== undefined)
270
+ await capability.selectModel(sessionId, dshRoute, signal);
271
+ }
272
+ catch {
273
+ return { ok: false, sessionId, workspaceId, adapterId, ...(modelId === undefined ? {} : { modelId }), status: 'uncertain', error: { code: 'WORKSPACE_MCP_SESSION_SETUP_FAILED', message: 'The session was created but DSH could not prepare the requested Agent and model.' } };
274
+ }
275
+ }
276
+ if (adapterId !== 'dsh') {
277
+ const sessionBinding = adapterRegistry.getSession(sessionId);
278
+ if (sessionBinding.adapterId !== adapterId || (modelId !== undefined && sessionBinding.modelId !== modelId)) {
279
+ return { ok: false, sessionId, workspaceId, adapterId, ...(modelId === undefined ? {} : { modelId }), status: 'uncertain', error: { code: 'WORKSPACE_MCP_ADAPTER_BINDING_FAILED', message: 'The new session did not retain its requested adapter/model binding.' } };
280
+ }
281
+ }
282
+ if (prompt !== undefined) {
283
+ let accepted;
284
+ try {
285
+ accepted = await capability.prompt({
286
+ requestId: randomUUID(),
287
+ sessionId,
288
+ mode: 'queue',
289
+ content: [{ type: 'text', text: prompt }],
290
+ }, signal);
291
+ }
292
+ catch {
293
+ return { ok: false, sessionId, workspaceId, adapterId, ...(modelId === undefined ? {} : { modelId }), status: 'uncertain', error: { code: 'WORKSPACE_MCP_PROMPT_UNCERTAIN', message: 'The session was created but initial prompt admission could not be confirmed. Inspect the session before retrying.' } };
294
+ }
295
+ if (asRecord(accepted)?.accepted !== true) {
296
+ return { ok: false, sessionId, workspaceId, adapterId, ...(modelId === undefined ? {} : { modelId }), status: 'uncertain', error: { code: 'WORKSPACE_MCP_PROMPT_REJECTED', message: 'The session was created but the initial prompt was not accepted.' } };
297
+ }
298
+ }
299
+ return { sessionId, workspaceId, adapterId, ...(modelId === undefined ? {} : { modelId }), status: prompt === undefined ? 'created' : 'accepted' };
300
+ }
301
+ async function continueSession(services, capability, args, signal) {
302
+ const sessionId = requireId(args.sessionId, 'sessionId');
303
+ const message = requireText(args.message, 'message', MAX_TEXT_LENGTH);
304
+ const workspace = findSessionWorkspace(services, sessionId);
305
+ if (workspace === undefined)
306
+ return unavailable('WORKSPACE_MCP_SESSION_UNAVAILABLE', 'The session is not available in a managed workspace.');
307
+ if (capability === undefined)
308
+ return unavailable('WORKSPACE_MCP_SESSION_CONTINUE_UNAVAILABLE', 'The versioned DSH workspace session-control capability is unavailable.');
309
+ signal.throwIfAborted();
310
+ let response;
311
+ try {
312
+ response = await capability.prompt({
313
+ requestId: randomUUID(),
314
+ sessionId,
315
+ mode: 'queue',
316
+ content: [{ type: 'text', text: message }],
317
+ }, signal);
318
+ }
319
+ catch {
320
+ return { accepted: false, sessionId, workspaceId: workspace.id, status: 'uncertain', error: { code: 'WORKSPACE_MCP_SESSION_CONTINUE_UNCERTAIN', message: 'Prompt admission could not be confirmed. Inspect the session before retrying.' } };
321
+ }
322
+ if (asRecord(response)?.accepted !== true)
323
+ return unavailable('WORKSPACE_MCP_SESSION_CONTINUE_UNAVAILABLE', 'DSH did not accept the message.');
324
+ return { accepted: true, sessionId, workspaceId: workspace.id };
325
+ }
326
+ function listPendingQuestions(services, capability, args) {
327
+ const sessionId = requireId(args.sessionId, 'sessionId');
328
+ const workspace = findSessionWorkspace(services, sessionId);
329
+ if (workspace === undefined || workspace.archivedSessionIds.includes(sessionId))
330
+ return unavailable('WORKSPACE_MCP_SESSION_UNAVAILABLE', 'The session is not available in a managed workspace.');
331
+ if (capability === undefined)
332
+ return unavailable('WORKSPACE_MCP_QUESTIONS_UNAVAILABLE', 'The versioned DSH pending-question capability is unavailable.');
333
+ const questions = capability.list(sessionId);
334
+ if (questions === null)
335
+ return unavailable('WORKSPACE_MCP_QUESTIONS_UNAVAILABLE', 'Pending question state could not be read.');
336
+ const bounded = questions.slice(0, MAX_PAGE_SIZE).map((pending) => ({
337
+ callId: pending.callId.slice(0, 128),
338
+ state: pending.state,
339
+ questions: pending.questions.slice(0, MAX_QUESTIONS).map((question) => ({
340
+ id: question.id.slice(0, 128),
341
+ question: question.question.slice(0, 2048),
342
+ ...(question.detail === undefined ? {} : { detail: question.detail.slice(0, 4096) }),
343
+ ...(question.header === undefined ? {} : { header: question.header.slice(0, 128) }),
344
+ ...(question.multiSelect === undefined ? {} : { multiSelect: question.multiSelect }),
345
+ ...(question.options === undefined ? {} : { options: question.options.slice(0, 32).map((option) => ({
346
+ label: option.label.slice(0, 256),
347
+ ...(option.description === undefined ? {} : { description: option.description.slice(0, 1024) }),
348
+ })) }),
349
+ })),
350
+ }));
351
+ const truncated = questions.length > bounded.length || questions.some((pending) => pending.questions.length > MAX_QUESTIONS
352
+ || pending.questions.some((question) => (question.options?.length ?? 0) > 32));
353
+ return { sessionId, workspaceId: workspace.id, questions: bounded, truncated };
354
+ }
355
+ function answerPendingQuestion(services, capability, args) {
356
+ const sessionId = requireId(args.sessionId, 'sessionId');
357
+ const callId = requireQuestionId(args.callId, 'callId');
358
+ const workspace = findSessionWorkspace(services, sessionId);
359
+ if (workspace === undefined || workspace.archivedSessionIds.includes(sessionId))
360
+ return unavailable('WORKSPACE_MCP_SESSION_UNAVAILABLE', 'The session is not available in a managed workspace.');
361
+ if (capability === undefined)
362
+ return unavailable('WORKSPACE_MCP_QUESTIONS_UNAVAILABLE', 'The versioned DSH pending-question capability is unavailable.');
363
+ const pending = capability.list(sessionId)?.find((question) => question.callId === callId
364
+ && (question.state === 'continued' || question.state === 'open'));
365
+ if (pending === undefined) {
366
+ return unavailable('WORKSPACE_MCP_QUESTION_NOT_PENDING', 'The pending question is no longer answerable.');
367
+ }
368
+ const answer = validateAnswer(args.answer, pending.questions);
369
+ const accepted = capability.answer(sessionId, callId, answer);
370
+ return accepted
371
+ ? { accepted: true, sessionId, workspaceId: workspace.id, callId }
372
+ : unavailable('WORKSPACE_MCP_QUESTION_NOT_PENDING', 'The pending question is no longer answerable.');
373
+ }
374
+ function requestPermission(services, capability, requests, pendingSessions, args, featureLifetime) {
375
+ const sessionId = requireId(args.sessionId, 'sessionId');
376
+ const workspace = findSessionWorkspace(services, sessionId);
377
+ if (workspace === undefined)
378
+ return unavailable('WORKSPACE_MCP_SESSION_UNAVAILABLE', 'The session is not available in a managed workspace.');
379
+ const toolName = requireText(args.toolName, 'toolName', 256);
380
+ const reason = requireText(args.reason, 'reason', 2048);
381
+ const callId = args.callId === undefined ? undefined : requireId(args.callId, 'callId');
382
+ if (featureLifetime.signal.aborted)
383
+ return unavailable('WORKSPACE_MCP_STOPPED', 'Workspace MCP tools are stopped.');
384
+ if (capability === undefined)
385
+ return Promise.resolve(unavailable('WORKSPACE_MCP_APPROVAL_UNAVAILABLE', 'The versioned DSH approval capability is unavailable.'));
386
+ prunePermissions(requests);
387
+ if (requests.size >= MAX_PENDING_PERMISSIONS)
388
+ return unavailable('WORKSPACE_MCP_REQUEST_LIMIT', 'Too many permission requests are being tracked.');
389
+ if (pendingSessions.has(sessionId))
390
+ return unavailable('WORKSPACE_MCP_APPROVAL_REQUEST_PENDING', 'This session already has an unresolved MCP permission request.');
391
+ const turnEndSignal = new AbortController();
392
+ const onFeatureAbort = () => turnEndSignal.abort();
393
+ if (featureLifetime.signal.aborted)
394
+ onFeatureAbort();
395
+ else
396
+ featureLifetime.signal.addEventListener('abort', onFeatureAbort, { once: true });
397
+ const started = capability.startRequest({
398
+ sessionId,
399
+ toolName,
400
+ ...(callId === undefined ? {} : { callId }),
401
+ reason,
402
+ signal: turnEndSignal.signal,
403
+ });
404
+ if (started === null) {
405
+ featureLifetime.signal.removeEventListener('abort', onFeatureAbort);
406
+ return unavailable('WORKSPACE_MCP_APPROVAL_REQUIRES_ACTIVE_TURN', 'DSH approval requires a live Agent inside an active turn and no other pending permission request.');
407
+ }
408
+ const requestId = started.requestId;
409
+ const request = {
410
+ requestId,
411
+ sessionId,
412
+ workspaceId: workspace.id,
413
+ createdAt: new Date().toISOString(),
414
+ expiresAt: Date.now() + PERMISSION_RETENTION_MS,
415
+ turnEndSignal,
416
+ status: 'pending',
417
+ };
418
+ requests.set(requestId, request);
419
+ pendingSessions.set(sessionId, requestId);
420
+ void started.outcome.then((outcome) => {
421
+ const wasPending = request.status === 'pending';
422
+ if (wasPending)
423
+ request.status = outcome;
424
+ if (pendingSessions.get(sessionId) === requestId)
425
+ pendingSessions.delete(sessionId);
426
+ request.turnEndSignal.abort();
427
+ featureLifetime.signal.removeEventListener('abort', onFeatureAbort);
428
+ }, () => {
429
+ const wasPending = request.status === 'pending';
430
+ if (wasPending)
431
+ request.status = featureLifetime.signal.aborted ? 'cancelled' : 'unavailable';
432
+ if (pendingSessions.get(sessionId) === requestId)
433
+ pendingSessions.delete(sessionId);
434
+ request.turnEndSignal.abort();
435
+ featureLifetime.signal.removeEventListener('abort', onFeatureAbort);
436
+ });
437
+ return { accepted: true, sessionId, workspaceId: workspace.id, requestId, status: 'pending', createdAt: request.createdAt };
438
+ }
439
+ function permissionStatus(services, requests, args) {
440
+ const sessionId = requireId(args.sessionId, 'sessionId');
441
+ const requestId = requireId(args.requestId, 'requestId');
442
+ const workspace = findSessionWorkspace(services, sessionId);
443
+ if (workspace === undefined)
444
+ return unavailable('WORKSPACE_MCP_SESSION_UNAVAILABLE', 'The session is not available in a managed workspace.');
445
+ const request = requests.get(requestId);
446
+ if (request !== undefined && (request.sessionId !== sessionId || request.workspaceId !== workspace.id)) {
447
+ return unavailable('WORKSPACE_MCP_PERMISSION_REQUEST_UNKNOWN', 'The permission request is unknown or expired.');
448
+ }
449
+ if (request !== undefined) {
450
+ if (request.expiresAt <= Date.now()) {
451
+ request.turnEndSignal.abort();
452
+ requests.delete(requestId);
453
+ return unavailable('WORKSPACE_MCP_PERMISSION_REQUEST_UNKNOWN', 'The permission request is unknown or expired.');
454
+ }
455
+ return { requestId, sessionId, workspaceId: workspace.id, status: request.status, createdAt: request.createdAt };
456
+ }
457
+ return unavailable('WORKSPACE_MCP_PERMISSION_REQUEST_UNKNOWN', 'The permission request is unknown or expired.');
458
+ }
459
+ function listTerminals(services, args) {
460
+ const workspaceId = requireId(args.workspaceId, 'workspaceId');
461
+ const sessionId = requireId(args.sessionId, 'sessionId');
462
+ const workspace = requireWorkspace(services, workspaceId);
463
+ if (!workspace.sessionIds.includes(sessionId) || workspace.archivedSessionIds.includes(sessionId))
464
+ return unavailable('WORKSPACE_MCP_SESSION_UNAVAILABLE', 'The session is not available in this workspace.');
465
+ if (services.terminalProcesses === undefined)
466
+ return unavailable('WORKSPACE_MCP_TERMINAL_LIST_UNAVAILABLE', 'The terminal process service is unavailable.');
467
+ const instances = services.terminalProcesses.listWorkspaceTerminalsForSession(workspaceId, sessionId);
468
+ return {
469
+ terminals: instances.slice(0, MAX_PAGE_SIZE).map((terminal) => ({ terminalId: terminal.id, dshSessionId: sessionId, state: terminal.state, title: terminal.title.slice(0, 120) })),
470
+ hasMore: instances.length > MAX_PAGE_SIZE,
471
+ };
472
+ }
473
+ async function readTerminal(services, args, signal) {
474
+ const workspaceId = requireId(args.workspaceId, 'workspaceId');
475
+ const sessionId = requireId(args.sessionId, 'sessionId');
476
+ const terminalId = requireTerminalId(args.terminalId);
477
+ const workspace = requireWorkspace(services, workspaceId);
478
+ if (!workspace.sessionIds.includes(sessionId) || workspace.archivedSessionIds.includes(sessionId))
479
+ return unavailable('WORKSPACE_MCP_SESSION_UNAVAILABLE', 'The session is not available in this workspace.');
480
+ const limitBytes = args.limitBytes === undefined ? 16 * 1024 : requireInteger(args.limitBytes, 1, MAX_TERMINAL_READ_BYTES, 'limitBytes');
481
+ const terminalProcesses = services.terminalProcesses;
482
+ if (terminalProcesses === undefined) {
483
+ return unavailable('WORKSPACE_MCP_TERMINAL_READ_UNAVAILABLE', 'The scoped terminal output service is unavailable.');
484
+ }
485
+ if (!terminalProcesses.listWorkspaceTerminalsForSession(workspaceId, sessionId).some((terminal) => terminal.id === terminalId)) {
486
+ return unavailable('WORKSPACE_MCP_TERMINAL_UNAVAILABLE', 'The terminal instance is unavailable in this workspace.');
487
+ }
488
+ signal.throwIfAborted();
489
+ const output = await terminalProcesses.readSessionTerminalOutput(workspaceId, sessionId, terminalId, limitBytes);
490
+ if (typeof output !== 'object' || output === null || typeof output.text !== 'string' || typeof output.truncated !== 'boolean') {
491
+ return unavailable('WORKSPACE_MCP_TERMINAL_READ_UNAVAILABLE', 'The terminal service returned no bounded output snapshot.');
492
+ }
493
+ return { workspaceId, terminalId, data: output.text, truncated: output.truncated };
494
+ }
495
+ async function writeTerminal(services, args, signal) {
496
+ const workspaceId = requireId(args.workspaceId, 'workspaceId');
497
+ const sessionId = requireId(args.sessionId, 'sessionId');
498
+ const terminalId = requireTerminalId(args.terminalId);
499
+ if (typeof args.data !== 'string' || args.data.length === 0 || args.data.includes('\u0000')) {
500
+ return invalid('WORKSPACE_MCP_INVALID_INPUT', 'data must contain non-empty text without NUL characters.');
501
+ }
502
+ const data = args.data;
503
+ const workspace = requireWorkspace(services, workspaceId);
504
+ if (!workspace.sessionIds.includes(sessionId) || workspace.archivedSessionIds.includes(sessionId))
505
+ return unavailable('WORKSPACE_MCP_SESSION_UNAVAILABLE', 'The session is not available in this workspace.');
506
+ if (new TextEncoder().encode(data).byteLength > MAX_TERMINAL_INPUT_BYTES)
507
+ return invalid('WORKSPACE_MCP_TERMINAL_INPUT_TOO_LARGE', 'Terminal input exceeds the allowed byte limit.');
508
+ const terminalProcesses = services.terminalProcesses;
509
+ if (terminalProcesses === undefined) {
510
+ return unavailable('WORKSPACE_MCP_TERMINAL_WRITE_UNAVAILABLE', 'The scoped terminal input service is unavailable.');
511
+ }
512
+ if (!terminalProcesses.listWorkspaceTerminalsForSession(workspaceId, sessionId).some((terminal) => terminal.id === terminalId)) {
513
+ return unavailable('WORKSPACE_MCP_TERMINAL_UNAVAILABLE', 'The terminal instance is unavailable in this workspace.');
514
+ }
515
+ signal.throwIfAborted();
516
+ await terminalProcesses.writeSessionTerminalInput(workspaceId, sessionId, terminalId, data);
517
+ return { accepted: true, workspaceId, terminalId };
518
+ }
519
+ function workspaceRows(services) {
520
+ const registry = readService(services, 'workspaceRegistry');
521
+ if (typeof registry?.list !== 'function')
522
+ return [];
523
+ let values;
524
+ try {
525
+ values = registry.list.call(registry);
526
+ }
527
+ catch {
528
+ return [];
529
+ }
530
+ if (!Array.isArray(values))
531
+ return [];
532
+ return values.flatMap((raw) => {
533
+ const value = asRecord(raw);
534
+ const id = readText(value, ['id', 'workspaceId', 'key']);
535
+ if (id === null || !isValidId(id))
536
+ return [];
537
+ const root = services.resolveManagedWorkspaceRoot?.(id);
538
+ if (typeof root !== 'string' || root.trim() === '')
539
+ return [];
540
+ const workspaceRegistry = readService(services, 'workspaceRegistry');
541
+ if (workspaceRegistry === undefined || !Array.isArray(workspaceRegistry.archivedSessionIds))
542
+ return [];
543
+ const archivedSessionIds = readStringArray(workspaceRegistry.archivedSessionIds).filter(isValidId);
544
+ return [{
545
+ id,
546
+ name: readText(value, ['title', 'displayName', 'name']) ?? id,
547
+ sessionIds: readStringArray(value?.sessionIds).filter(isValidId),
548
+ archivedSessionIds,
549
+ }];
550
+ });
551
+ }
552
+ function requireWorkspace(services, workspaceId) {
553
+ const workspace = workspaceRows(services).find((entry) => entry.id === workspaceId);
554
+ if (workspace === undefined)
555
+ throw new WorkspaceToolError('WORKSPACE_MCP_WORKSPACE_UNAVAILABLE', 'The workspace is unavailable.');
556
+ const root = services.resolveManagedWorkspaceRoot?.(workspaceId);
557
+ if (typeof root !== 'string' || root.trim() === '') {
558
+ throw new WorkspaceToolError('WORKSPACE_MCP_WORKSPACE_UNAVAILABLE', 'The managed workspace is unavailable.');
559
+ }
560
+ return workspace;
561
+ }
562
+ function findSessionWorkspace(services, sessionId) {
563
+ const workspace = workspaceRows(services).find((entry) => entry.sessionIds.includes(sessionId) && !entry.archivedSessionIds.includes(sessionId));
564
+ const root = workspace === undefined ? null : services.resolveManagedWorkspaceRoot?.(workspace.id);
565
+ if (workspace !== undefined && typeof root === 'string' && root.trim() !== '')
566
+ return workspace;
567
+ // DSH native subagent headers may carry an exact parent session ID; inherit only
568
+ // that parent's live workspace membership, never a caller-provided workspace.
569
+ let current = sessionId;
570
+ const visited = new Set([current]);
571
+ for (let depth = 0; depth < 8; depth += 1) {
572
+ const header = asRecord(asRecord(services.nativeSessions?.get(current))?.header);
573
+ const parent = readText(header, ['parentSession', 'parentSessionId']);
574
+ if (parent === null || visited.has(parent))
575
+ return undefined;
576
+ const parentWorkspace = workspaceRows(services).find((entry) => entry.sessionIds.includes(parent) && !entry.archivedSessionIds.includes(parent));
577
+ const parentRoot = parentWorkspace === undefined ? null : services.resolveManagedWorkspaceRoot?.(parentWorkspace.id);
578
+ if (parentWorkspace !== undefined && typeof parentRoot === 'string' && parentRoot.trim() !== '')
579
+ return parentWorkspace;
580
+ visited.add(parent);
581
+ current = parent;
582
+ }
583
+ return undefined;
584
+ }
585
+ function validateAnswer(value, questions) {
586
+ const record = asRecord(value);
587
+ if (!Array.isArray(record?.answers) || record.answers.length < 1 || record.answers.length > MAX_QUESTIONS) {
588
+ throw new WorkspaceToolError('WORKSPACE_MCP_QUESTIONS_INVALID', `answer.answers must contain 1 to ${String(MAX_QUESTIONS)} items.`);
589
+ }
590
+ const answers = record.answers.map((raw, index) => {
591
+ const answer = asRecord(raw);
592
+ if (answer === undefined)
593
+ throw new WorkspaceToolError('WORKSPACE_MCP_QUESTIONS_INVALID', `answer.answers[${String(index)}] must be an object.`);
594
+ const id = requireQuestionId(answer.id, `answer.answers[${String(index)}].id`);
595
+ if (!Array.isArray(answer.selected) || answer.selected.length > 32 || answer.selected.some((entry) => typeof entry !== 'string' || entry.length > 256)) {
596
+ throw new WorkspaceToolError('WORKSPACE_MCP_QUESTIONS_INVALID', 'Each answer must provide a bounded selected string array.');
597
+ }
598
+ if (answer.custom !== undefined && (typeof answer.custom !== 'string' || answer.custom.length > 4096)) {
599
+ throw new WorkspaceToolError('WORKSPACE_MCP_QUESTIONS_INVALID', 'Answer custom text exceeds the allowed size.');
600
+ }
601
+ return {
602
+ id,
603
+ selected: answer.selected,
604
+ ...(answer.custom === undefined ? {} : { custom: answer.custom }),
605
+ };
606
+ });
607
+ if (!validAnswers(questions, answers))
608
+ throw new WorkspaceToolError('WORKSPACE_MCP_QUESTIONS_INVALID', 'Answers must match every pending question and its allowed choices exactly once.');
609
+ return { answers };
610
+ }
611
+ function validAnswers(questions, answers) {
612
+ const questionById = new Map(questions.map((question) => [question.id, question]));
613
+ if (answers.length !== questions.length)
614
+ return false;
615
+ const seen = new Set();
616
+ for (const answer of answers) {
617
+ const question = questionById.get(answer.id);
618
+ if (question === undefined || seen.has(answer.id) || !Array.isArray(answer.selected))
619
+ return false;
620
+ seen.add(answer.id);
621
+ if (question.multiSelect !== true && answer.selected.length > 1)
622
+ return false;
623
+ if (question.options !== undefined && answer.selected.some((selection) => !question.options.some((option) => option.label === selection)))
624
+ return false;
625
+ if (answer.custom !== undefined && (typeof answer.custom !== 'string' || answer.custom.length > 4096))
626
+ return false;
627
+ }
628
+ return seen.size === questions.length;
629
+ }
630
+ function findDshModel(value, modelId) {
631
+ const catalog = asRecord(value);
632
+ if (!Array.isArray(catalog?.groups))
633
+ return undefined;
634
+ const matches = [];
635
+ for (const rawGroup of catalog.groups) {
636
+ const group = asRecord(rawGroup);
637
+ if (typeof group?.id !== 'string' || !Array.isArray(group.models))
638
+ continue;
639
+ for (const rawModel of group.models) {
640
+ const model = asRecord(rawModel);
641
+ if (model?.id !== modelId)
642
+ continue;
643
+ const reasoning = asRecord(model.reasoning);
644
+ matches.push({ provider: group.id, model: modelId, ...(typeof reasoning?.defaultEffort === 'string' ? { reasoningEffort: reasoning.defaultEffort } : {}) });
645
+ }
646
+ }
647
+ return matches.length === 1 ? matches[0] : undefined;
648
+ }
649
+ function catalogHasModel(value, modelId) {
650
+ const catalog = asRecord(value);
651
+ return Array.isArray(catalog?.groups) && catalog.groups.some((rawGroup) => {
652
+ const group = asRecord(rawGroup);
653
+ return Array.isArray(group?.models) && group.models.some((rawModel) => asRecord(rawModel)?.id === modelId);
654
+ });
655
+ }
656
+ function sessionIdOf(value) {
657
+ const record = asRecord(value);
658
+ const header = asRecord(record?.header);
659
+ const id = readText(record, ['sessionId', 'id', 'key']) ?? readText(header, ['id']);
660
+ return id !== null && isValidId(id) ? id : null;
661
+ }
662
+ function sessionMetadata(raw, sessionId) {
663
+ const value = asRecord(raw);
664
+ const projectionHints = asRecord(value?.projections);
665
+ const projectionValues = asRecord(projectionHints?.values);
666
+ const listMetadata = asRecord(projectionValues?.sessionListMetadata);
667
+ const title = projectionValues?.title;
668
+ const updatedAt = readTimestamp(value, ['updatedAt', 'lastUpdatedAt', 'createdAt']);
669
+ const parentSessionId = readText(value, ['parentSessionId']);
670
+ return {
671
+ sessionId,
672
+ status: value?.running === true ? 'running' : value?.running === false ? 'idle' : 'unknown',
673
+ updatedAt: updatedAt ?? 0,
674
+ blank: listMetadata?.blank === true || value?.blank === true,
675
+ agentAvailable: value?.agentAvailable === true,
676
+ ...(typeof title === 'string' && title.trim() !== '' ? { title: title.slice(0, 160) } : {}),
677
+ ...(parentSessionId === null ? {} : { parentSessionId }),
678
+ ...(typeof value?.origin === 'string' ? { origin: value.origin } : {}),
679
+ };
680
+ }
681
+ function readService(services, name) {
682
+ try {
683
+ return asRecord(services.dshContext?.get(name));
684
+ }
685
+ catch {
686
+ return undefined;
687
+ }
688
+ }
689
+ function asRecord(value) {
690
+ return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : undefined;
691
+ }
692
+ function readStringArray(value) {
693
+ return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
694
+ }
695
+ function readText(value, keys) {
696
+ if (value === undefined)
697
+ return null;
698
+ for (const key of keys) {
699
+ const candidate = value[key];
700
+ if (typeof candidate === 'string' && candidate.trim() !== '')
701
+ return candidate.trim();
702
+ }
703
+ return null;
704
+ }
705
+ function readTimestamp(value, keys) {
706
+ if (value === undefined)
707
+ return undefined;
708
+ for (const key of keys) {
709
+ const candidate = value[key];
710
+ if (typeof candidate === 'number' && Number.isFinite(candidate))
711
+ return candidate;
712
+ if (typeof candidate === 'string' && Number.isFinite(Date.parse(candidate)))
713
+ return candidate;
714
+ }
715
+ return undefined;
716
+ }
717
+ const EXTERNAL_ADAPTER_ID = /^[a-z0-9][a-z0-9-]{0,63}$/u;
718
+ function requireAdapterId(value) {
719
+ if (typeof value !== 'string' || !EXTERNAL_ADAPTER_ID.test(value.trim()))
720
+ throw new WorkspaceToolError('WORKSPACE_MCP_INVALID_INPUT', 'adapterId is invalid.');
721
+ return value.trim();
722
+ }
723
+ function requireModelId(value) {
724
+ if (typeof value !== 'string' || value.trim() === '' || value.length > 256 || /[\u0000-\u001f]/u.test(value))
725
+ throw new WorkspaceToolError('WORKSPACE_MCP_INVALID_INPUT', 'modelId is invalid.');
726
+ return value.trim();
727
+ }
728
+ function requireId(value, name) {
729
+ if (typeof value !== 'string' || !isValidId(value.trim()))
730
+ throw new WorkspaceToolError('WORKSPACE_MCP_INVALID_ID', `${name} is invalid.`);
731
+ return value.trim();
732
+ }
733
+ function requireQuestionId(value, name) {
734
+ if (typeof value !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value.trim()))
735
+ throw new WorkspaceToolError('WORKSPACE_MCP_QUESTIONS_INVALID', `${name} is invalid.`);
736
+ return value.trim();
737
+ }
738
+ function requireTerminalId(value) {
739
+ if (typeof value !== 'string' || !/^[\w-]{1,128}$/u.test(value.trim()))
740
+ throw new WorkspaceToolError('WORKSPACE_MCP_INVALID_ID', 'terminalId is invalid.');
741
+ return value.trim();
742
+ }
743
+ function requireText(value, name, maximum) {
744
+ if (typeof value !== 'string' || value.trim() === '' || value.length > maximum || value.includes('\u0000'))
745
+ throw new WorkspaceToolError('WORKSPACE_MCP_INVALID_INPUT', `${name} must contain 1 to ${String(maximum)} characters.`);
746
+ return value;
747
+ }
748
+ function requireInteger(value, minimum, maximum, name) {
749
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < minimum || value > maximum)
750
+ throw new WorkspaceToolError('WORKSPACE_MCP_INVALID_INPUT', `${name} is outside its allowed range.`);
751
+ return value;
752
+ }
753
+ function isValidId(value) {
754
+ return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u.test(value);
755
+ }
756
+ function prunePermissions(requests) {
757
+ const now = Date.now();
758
+ for (const [id, value] of requests) {
759
+ if (value.expiresAt <= now && value.status !== 'pending')
760
+ requests.delete(id);
761
+ }
762
+ while (requests.size >= MAX_PENDING_PERMISSIONS) {
763
+ const oldest = requests.keys().next().value;
764
+ if (oldest === undefined)
765
+ return;
766
+ const request = requests.get(oldest);
767
+ if (request?.status === 'pending' && request.expiresAt > now)
768
+ return;
769
+ requests.delete(oldest);
770
+ }
771
+ }
772
+ function toolSuccess(value) {
773
+ const result = asRecord(value);
774
+ const isError = result?.ok === false || result?.error !== undefined;
775
+ return {
776
+ resultType: 'complete',
777
+ isError,
778
+ content: [{ type: 'text', text: JSON.stringify(value) }],
779
+ structuredContent: value,
780
+ };
781
+ }
782
+ function toolError(code, message) {
783
+ const value = { ok: false, error: { code, message } };
784
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(value) }], structuredContent: value };
785
+ }
786
+ function unavailable(code, message) {
787
+ return { ok: false, error: { code, message } };
788
+ }
789
+ function invalid(code, message) {
790
+ return { ok: false, error: { code, message } };
791
+ }
792
+ class WorkspaceToolError extends Error {
793
+ code;
794
+ constructor(code, message) {
795
+ super(message);
796
+ this.code = code;
797
+ this.name = 'WorkspaceToolError';
798
+ }
799
+ }
800
+ //# sourceMappingURL=tools.js.map