@blue-soda/dsh-remote 0.5.6 → 0.5.8

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 (232) hide show
  1. package/README.md +14 -3
  2. package/cordis.patch.yml +30 -30
  3. package/dist/client.github.js +1 -1
  4. package/dist/client.js +1 -1
  5. package/dist/index.js +1216 -396
  6. package/dist/index.js.map +4 -4
  7. package/dsh-plugin.json +41 -41
  8. package/package.json +165 -165
  9. package/dist/acp.d.ts +0 -75
  10. package/dist/acp.d.ts.map +0 -1
  11. package/dist/acp.js +0 -61
  12. package/dist/acp.js.map +0 -1
  13. package/dist/api-proxy-switch.d.ts +0 -33
  14. package/dist/api-proxy-switch.d.ts.map +0 -1
  15. package/dist/api-proxy-switch.js +0 -109
  16. package/dist/api-proxy-switch.js.map +0 -1
  17. package/dist/atomic-file.d.ts +0 -16
  18. package/dist/atomic-file.d.ts.map +0 -1
  19. package/dist/atomic-file.js +0 -81
  20. package/dist/atomic-file.js.map +0 -1
  21. package/dist/bin/doctor.d.ts +0 -3
  22. package/dist/bin/doctor.d.ts.map +0 -1
  23. package/dist/bin/doctor.js +0 -13
  24. package/dist/bin/doctor.js.map +0 -1
  25. package/dist/cli.d.ts +0 -32
  26. package/dist/cli.d.ts.map +0 -1
  27. package/dist/cli.js +0 -289
  28. package/dist/cli.js.map +0 -1
  29. package/dist/client-runtime.d.ts +0 -375
  30. package/dist/client-runtime.d.ts.map +0 -1
  31. package/dist/client-runtime.js +0 -1768
  32. package/dist/client-runtime.js.map +0 -1
  33. package/dist/client-secure-transport.d.ts +0 -23
  34. package/dist/client-secure-transport.d.ts.map +0 -1
  35. package/dist/client-secure-transport.js +0 -144
  36. package/dist/client-secure-transport.js.map +0 -1
  37. package/dist/client-target-store.d.ts +0 -40
  38. package/dist/client-target-store.d.ts.map +0 -1
  39. package/dist/client-target-store.js +0 -79
  40. package/dist/client-target-store.js.map +0 -1
  41. package/dist/client.d.ts +0 -13
  42. package/dist/client.d.ts.map +0 -1
  43. package/dist/codex/app-server.d.ts +0 -71
  44. package/dist/codex/app-server.d.ts.map +0 -1
  45. package/dist/codex/app-server.js +0 -268
  46. package/dist/codex/app-server.js.map +0 -1
  47. package/dist/codex/codex-image-codec.d.ts +0 -7
  48. package/dist/codex/codex-image-codec.d.ts.map +0 -1
  49. package/dist/codex/codex-image-codec.js +0 -288
  50. package/dist/codex/codex-image-codec.js.map +0 -1
  51. package/dist/codex/domain.d.ts +0 -102
  52. package/dist/codex/domain.d.ts.map +0 -1
  53. package/dist/codex/domain.js +0 -1323
  54. package/dist/codex/domain.js.map +0 -1
  55. package/dist/codex/method-policy.d.ts +0 -338
  56. package/dist/codex/method-policy.d.ts.map +0 -1
  57. package/dist/codex/method-policy.js +0 -141
  58. package/dist/codex/method-policy.js.map +0 -1
  59. package/dist/codex/peer-bridge.d.ts +0 -57
  60. package/dist/codex/peer-bridge.d.ts.map +0 -1
  61. package/dist/codex/peer-bridge.js +0 -299
  62. package/dist/codex/peer-bridge.js.map +0 -1
  63. package/dist/codex/permissions.d.ts +0 -4
  64. package/dist/codex/permissions.d.ts.map +0 -1
  65. package/dist/codex/permissions.js +0 -21
  66. package/dist/codex/permissions.js.map +0 -1
  67. package/dist/codex/session-id.d.ts +0 -10
  68. package/dist/codex/session-id.d.ts.map +0 -1
  69. package/dist/codex/session-id.js +0 -15
  70. package/dist/codex/session-id.js.map +0 -1
  71. package/dist/codex/virtual-harness.d.ts +0 -209
  72. package/dist/codex/virtual-harness.d.ts.map +0 -1
  73. package/dist/codex/virtual-harness.js +0 -2650
  74. package/dist/codex/virtual-harness.js.map +0 -1
  75. package/dist/codex-workspace-bridge.d.ts +0 -108
  76. package/dist/codex-workspace-bridge.d.ts.map +0 -1
  77. package/dist/codex-workspace-bridge.js +0 -502
  78. package/dist/codex-workspace-bridge.js.map +0 -1
  79. package/dist/config.d.ts +0 -99
  80. package/dist/config.d.ts.map +0 -1
  81. package/dist/config.js +0 -127
  82. package/dist/config.js.map +0 -1
  83. package/dist/connection-controller.d.ts +0 -44
  84. package/dist/connection-controller.d.ts.map +0 -1
  85. package/dist/connection-controller.js +0 -185
  86. package/dist/connection-controller.js.map +0 -1
  87. package/dist/control-route.d.ts +0 -34
  88. package/dist/control-route.d.ts.map +0 -1
  89. package/dist/control-route.js +0 -136
  90. package/dist/control-route.js.map +0 -1
  91. package/dist/control-runtime.d.ts +0 -138
  92. package/dist/control-runtime.d.ts.map +0 -1
  93. package/dist/control-runtime.js +0 -543
  94. package/dist/control-runtime.js.map +0 -1
  95. package/dist/control-stream.d.ts +0 -67
  96. package/dist/control-stream.d.ts.map +0 -1
  97. package/dist/control-stream.js +0 -179
  98. package/dist/control-stream.js.map +0 -1
  99. package/dist/defaults.d.ts +0 -9
  100. package/dist/defaults.d.ts.map +0 -1
  101. package/dist/defaults.js +0 -9
  102. package/dist/defaults.js.map +0 -1
  103. package/dist/file-viewer-bridge.d.ts +0 -30
  104. package/dist/file-viewer-bridge.d.ts.map +0 -1
  105. package/dist/file-viewer-bridge.js +0 -128
  106. package/dist/file-viewer-bridge.js.map +0 -1
  107. package/dist/file-viewer-contract.d.ts +0 -5
  108. package/dist/file-viewer-contract.d.ts.map +0 -1
  109. package/dist/file-viewer-contract.js +0 -4
  110. package/dist/file-viewer-contract.js.map +0 -1
  111. package/dist/harness-api-bridge.d.ts +0 -81
  112. package/dist/harness-api-bridge.d.ts.map +0 -1
  113. package/dist/harness-api-bridge.js +0 -862
  114. package/dist/harness-api-bridge.js.map +0 -1
  115. package/dist/harness-api-history.d.ts +0 -3
  116. package/dist/harness-api-history.d.ts.map +0 -1
  117. package/dist/harness-api-history.js +0 -59
  118. package/dist/harness-api-history.js.map +0 -1
  119. package/dist/harness-remote-bridge.d.ts +0 -60
  120. package/dist/harness-remote-bridge.d.ts.map +0 -1
  121. package/dist/harness-remote-bridge.js +0 -540
  122. package/dist/harness-remote-bridge.js.map +0 -1
  123. package/dist/harness-version.d.ts +0 -23
  124. package/dist/harness-version.d.ts.map +0 -1
  125. package/dist/harness-version.js +0 -95
  126. package/dist/harness-version.js.map +0 -1
  127. package/dist/identity-store.d.ts +0 -45
  128. package/dist/identity-store.d.ts.map +0 -1
  129. package/dist/identity-store.js +0 -186
  130. package/dist/identity-store.js.map +0 -1
  131. package/dist/ids.d.ts +0 -2
  132. package/dist/ids.d.ts.map +0 -1
  133. package/dist/ids.js +0 -17
  134. package/dist/ids.js.map +0 -1
  135. package/dist/index.d.ts +0 -78
  136. package/dist/index.d.ts.map +0 -1
  137. package/dist/logging.d.ts +0 -18
  138. package/dist/logging.d.ts.map +0 -1
  139. package/dist/logging.js +0 -30
  140. package/dist/logging.js.map +0 -1
  141. package/dist/loopback-host.d.ts +0 -20
  142. package/dist/loopback-host.d.ts.map +0 -1
  143. package/dist/loopback-host.js +0 -245
  144. package/dist/loopback-host.js.map +0 -1
  145. package/dist/loopback-preview.d.ts +0 -19
  146. package/dist/loopback-preview.d.ts.map +0 -1
  147. package/dist/loopback-preview.js +0 -232
  148. package/dist/loopback-preview.js.map +0 -1
  149. package/dist/native-rtc-helper.d.ts +0 -14
  150. package/dist/native-rtc-helper.d.ts.map +0 -1
  151. package/dist/native-rtc-helper.js +0 -575
  152. package/dist/native-rtc-helper.js.map +0 -1
  153. package/dist/remote-api-proxy.d.ts +0 -17
  154. package/dist/remote-api-proxy.d.ts.map +0 -1
  155. package/dist/remote-api-proxy.js +0 -451
  156. package/dist/remote-api-proxy.js.map +0 -1
  157. package/dist/remote-directory-browser.d.ts +0 -16
  158. package/dist/remote-directory-browser.d.ts.map +0 -1
  159. package/dist/remote-directory-browser.js +0 -48
  160. package/dist/remote-directory-browser.js.map +0 -1
  161. package/dist/remote-file-content-provider.d.ts +0 -48
  162. package/dist/remote-file-content-provider.d.ts.map +0 -1
  163. package/dist/remote-file-content-provider.js +0 -93
  164. package/dist/remote-file-content-provider.js.map +0 -1
  165. package/dist/remote-typert-gateway.d.ts +0 -20
  166. package/dist/remote-typert-gateway.d.ts.map +0 -1
  167. package/dist/remote-typert-gateway.js +0 -334
  168. package/dist/remote-typert-gateway.js.map +0 -1
  169. package/dist/rpc-binary-attachments.d.ts +0 -33
  170. package/dist/rpc-binary-attachments.d.ts.map +0 -1
  171. package/dist/rpc-binary-attachments.js +0 -142
  172. package/dist/rpc-binary-attachments.js.map +0 -1
  173. package/dist/rpc-router.d.ts +0 -31
  174. package/dist/rpc-router.d.ts.map +0 -1
  175. package/dist/rpc-router.js +0 -201
  176. package/dist/rpc-router.js.map +0 -1
  177. package/dist/safe-error.d.ts +0 -8
  178. package/dist/safe-error.d.ts.map +0 -1
  179. package/dist/safe-error.js +0 -20
  180. package/dist/safe-error.js.map +0 -1
  181. package/dist/server-api.d.ts +0 -109
  182. package/dist/server-api.d.ts.map +0 -1
  183. package/dist/server-api.js +0 -520
  184. package/dist/server-api.js.map +0 -1
  185. package/dist/server-connection.d.ts +0 -79
  186. package/dist/server-connection.d.ts.map +0 -1
  187. package/dist/server-connection.js +0 -1023
  188. package/dist/server-connection.js.map +0 -1
  189. package/dist/server-credentials.d.ts +0 -55
  190. package/dist/server-credentials.d.ts.map +0 -1
  191. package/dist/server-credentials.js +0 -108
  192. package/dist/server-credentials.js.map +0 -1
  193. package/dist/service.d.ts +0 -151
  194. package/dist/service.d.ts.map +0 -1
  195. package/dist/service.js +0 -448
  196. package/dist/service.js.map +0 -1
  197. package/dist/session-format-compat.d.ts +0 -3
  198. package/dist/session-format-compat.d.ts.map +0 -1
  199. package/dist/session-format-compat.js +0 -183
  200. package/dist/session-format-compat.js.map +0 -1
  201. package/dist/status-stream.d.ts +0 -48
  202. package/dist/status-stream.d.ts.map +0 -1
  203. package/dist/status-stream.js +0 -133
  204. package/dist/status-stream.js.map +0 -1
  205. package/dist/terminal-policy.d.ts +0 -21
  206. package/dist/terminal-policy.d.ts.map +0 -1
  207. package/dist/terminal-policy.js +0 -68
  208. package/dist/terminal-policy.js.map +0 -1
  209. package/dist/tui-command.d.ts +0 -13
  210. package/dist/tui-command.d.ts.map +0 -1
  211. package/dist/tui-command.js +0 -392
  212. package/dist/tui-command.js.map +0 -1
  213. package/dist/typert-gateway-contract.d.ts +0 -59
  214. package/dist/typert-gateway-contract.d.ts.map +0 -1
  215. package/dist/typert-gateway-contract.js +0 -2
  216. package/dist/typert-gateway-contract.js.map +0 -1
  217. package/dist/typert-gateway-switch.d.ts +0 -85
  218. package/dist/typert-gateway-switch.d.ts.map +0 -1
  219. package/dist/typert-gateway-switch.js +0 -448
  220. package/dist/typert-gateway-switch.js.map +0 -1
  221. package/dist/types.d.ts +0 -20
  222. package/dist/types.d.ts.map +0 -1
  223. package/dist/types.js +0 -2
  224. package/dist/types.js.map +0 -1
  225. package/dist/version.d.ts +0 -12
  226. package/dist/version.d.ts.map +0 -1
  227. package/dist/version.js +0 -12
  228. package/dist/version.js.map +0 -1
  229. package/dist/werift-rtc.d.ts +0 -121
  230. package/dist/werift-rtc.d.ts.map +0 -1
  231. package/dist/werift-rtc.js +0 -490
  232. package/dist/werift-rtc.js.map +0 -1
@@ -1,862 +0,0 @@
1
- import { HARNESS_API_TRANSFER_CHUNK_BYTES, MAX_ACTIVE_TRANSFERS_PER_DIRECTION, MAX_HARNESS_API_TRANSFER_BYTES, TRANSFER_IDLE_MS, } from '@dsh-remote/protocol';
2
- import { z } from 'zod';
3
- import { harnessSessionGeneration, normalizeHarnessVersion, selectHarnessVersion, } from './harness-version.js';
4
- import { RpcError } from './rpc-router.js';
5
- import { collectRpcAttachments } from './rpc-binary-attachments.js';
6
- import { safeErrorCode } from './safe-error.js';
7
- import { listRemoteDirectory } from './remote-directory-browser.js';
8
- import { callSessionHistory } from './harness-api-history.js';
9
- const callSchema = z.object({
10
- method: z.string().min(1).max(80),
11
- rpcId: z.string().min(1).max(128),
12
- payload: z.unknown(),
13
- }).strict();
14
- const respondSchema = z.object({
15
- message: z.object({
16
- type: z.literal('client-response'),
17
- rpcId: z.string().min(1).max(128),
18
- result: z.unknown(),
19
- }).strict(),
20
- }).strict();
21
- const streamOpenSchema = z.object({
22
- streamId: z.string().min(1).max(128),
23
- stream: z.enum(['mux', 'host']),
24
- rpcId: z.string().min(1).max(128),
25
- payload: z.object({
26
- // Optional focus for a mux stream: only frames belonging to this session
27
- // are forwarded. The Remote Web selects one session at a time, so without
28
- // this every active session's events (potentially megabytes) would be
29
- // pushed over the tunnel and stall the WebRTC data channel.
30
- sessionId: z.string().min(1).max(128).optional(),
31
- }).strict(),
32
- }).strict();
33
- const streamCloseSchema = z.object({ streamId: z.string().min(1).max(128) }).strict();
34
- const transferIdSchema = z.string().min(1).max(128);
35
- const transferOpenSchema = z.object({
36
- transferId: transferIdSchema,
37
- totalBytes: z.number().int().positive().max(MAX_HARNESS_API_TRANSFER_BYTES),
38
- totalChunks: z.number().int().positive().max(Math.ceil(MAX_HARNESS_API_TRANSFER_BYTES / HARNESS_API_TRANSFER_CHUNK_BYTES)),
39
- }).strict();
40
- const transferChunkSchema = z.object({
41
- transferId: transferIdSchema,
42
- index: z.number().int().nonnegative(),
43
- data: z.string().max(Math.ceil(HARNESS_API_TRANSFER_CHUNK_BYTES / 3) * 4),
44
- }).strict();
45
- const transferCommitSchema = z.object({ transferId: transferIdSchema }).strict();
46
- const transferReadSchema = z.object({ transferId: transferIdSchema, index: z.number().int().nonnegative() }).strict();
47
- const transferCloseSchema = z.object({ transferId: transferIdSchema }).strict();
48
- const commandExecuteSchema = z.object({
49
- agentId: z.string().min(1).max(128),
50
- line: z.string().min(1).max(2048),
51
- // dsh-commands <= 0.1.2 calls this field `images`; dsh-commands 0.1.5
52
- // renamed it to `submittedAttachments` and expanded its element type to
53
- // include staged file receipts. Remote command execution keeps attachments
54
- // out of scope, so either compatibility field is restricted to an empty
55
- // list. The dispatcher below chooses the matching Host descriptor.
56
- images: z.array(z.never()).length(0).optional(),
57
- submittedAttachments: z.array(z.never()).length(0).optional(),
58
- }).strict().superRefine((value, context) => {
59
- if (value.images !== undefined && value.submittedAttachments !== undefined) {
60
- context.addIssue({
61
- code: z.ZodIssueCode.custom,
62
- message: 'Use either images or submittedAttachments, not both.',
63
- path: ['submittedAttachments'],
64
- });
65
- }
66
- });
67
- const commandListSchema = z.object({
68
- agentId: z.string().min(1).max(128),
69
- }).strict();
70
- const directoryListSchema = z.object({
71
- path: z.string().min(1).max(4096).optional(),
72
- }).strict();
73
- /** POSIX-portable environment-variable name, mirroring the seam's credentialRef guard. */
74
- const credentialRefSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, 'Invalid credential reference name').max(128);
75
- const CONFIG_PLANE_SETTINGS_BYTES = 64 * 1024;
76
- const CONFIG_PLANE_MAX_OPS = 64;
77
- const CONFIG_PLANE_MAX_PATH_SEGMENTS = 8;
78
- const CONFIG_PLANE_MAX_PATH_SEGMENT_BYTES = 64;
79
- const CONFIG_PLANE_MAX_CREDENTIAL_VALUE_BYTES = 8 * 1024;
80
- const CONFIG_PLANE_MAX_BASE_URL_BYTES = 2048;
81
- const CONFIG_PLANE_MAX_NS_BYTES = 128;
82
- const settingsDescribeSchema = z.object({}).strict();
83
- const settingsWriteBase = {
84
- ns: z.string().min(1).max(CONFIG_PLANE_MAX_NS_BYTES),
85
- expectedRevision: z.number().int().nonnegative().optional(),
86
- };
87
- const settingsUpdateSchema = z.object({
88
- ...settingsWriteBase,
89
- patch: z.record(z.string().min(1).max(CONFIG_PLANE_MAX_PATH_SEGMENT_BYTES), z.unknown()),
90
- }).strict();
91
- const settingsReplaceSchema = z.object({
92
- ...settingsWriteBase,
93
- section: z.record(z.string().min(1).max(CONFIG_PLANE_MAX_PATH_SEGMENT_BYTES), z.unknown()),
94
- }).strict();
95
- const settingsOpSchema = z.union([
96
- z.object({
97
- op: z.literal('set'),
98
- path: z.array(z.string().min(1).max(CONFIG_PLANE_MAX_PATH_SEGMENT_BYTES)).max(CONFIG_PLANE_MAX_PATH_SEGMENTS),
99
- value: z.unknown(),
100
- }).strict(),
101
- z.object({
102
- op: z.literal('unset'),
103
- path: z.array(z.string().min(1).max(CONFIG_PLANE_MAX_PATH_SEGMENT_BYTES)).max(CONFIG_PLANE_MAX_PATH_SEGMENTS),
104
- }).strict(),
105
- ]);
106
- const settingsMutateSchema = z.object({
107
- ...settingsWriteBase,
108
- ops: z.array(settingsOpSchema).min(1).max(CONFIG_PLANE_MAX_OPS),
109
- }).strict();
110
- const credentialsDescribeSchema = z.object({
111
- refs: z.array(credentialRefSchema).max(CONFIG_PLANE_MAX_OPS),
112
- }).strict();
113
- const credentialsSetSchema = z.object({
114
- ref: credentialRefSchema,
115
- value: z.string().min(1).max(CONFIG_PLANE_MAX_CREDENTIAL_VALUE_BYTES),
116
- }).strict();
117
- const credentialsUnsetSchema = z.object({
118
- ref: credentialRefSchema,
119
- }).strict();
120
- const discoverModelsSchema = z.object({
121
- settingsNs: z.string().min(1).max(CONFIG_PLANE_MAX_NS_BYTES),
122
- provider: z.string().min(1).max(CONFIG_PLANE_MAX_NS_BYTES).optional(),
123
- baseURL: z.string().max(CONFIG_PLANE_MAX_BASE_URL_BYTES).optional(),
124
- api: z.string().min(1).max(CONFIG_PLANE_MAX_NS_BYTES).optional(),
125
- apiKey: z.string().min(1).max(CONFIG_PLANE_MAX_CREDENTIAL_VALUE_BYTES).optional(),
126
- }).strict();
127
- /**
128
- * Harness API methods that are safe to expose to an authenticated remote UI.
129
- * Native open/picker calls, directory mutation, file contents, downloads,
130
- * attachment upload, and `settings.openDocument` intentionally remain outside
131
- * this bridge. `session.attachment` is the native read-only lookup used by
132
- * Harness rc.2 to render an image already referenced by that same session.
133
- * Directory listing exposes metadata only for workspace picking.
134
- * `commands.*` follows the official Host registry so the authenticated Remote
135
- * UI sees the same effective command catalog and handlers as the local UI.
136
- *
137
- * The authenticated Remote UI may configure every namespace currently
138
- * registered with the official Host settings seam. Writes remain bounded and
139
- * must target that live directory; credential values remain write-only;
140
- * `settings.openDocument` stays local-only; and `discoverModels` endpoints must
141
- * be HTTPS (HTTP only for localhost). Anything outside that scope fails closed.
142
- */
143
- export const HARNESS_API_ALLOWLIST = [
144
- 'session.list',
145
- 'session.search',
146
- 'session.create',
147
- 'session.history',
148
- 'session.models',
149
- 'session.selectModel',
150
- 'session.rename',
151
- 'session.fork',
152
- 'session.prompt',
153
- 'session.attachment',
154
- 'session.updateQueue',
155
- 'session.cancel',
156
- 'subagent.list',
157
- 'subagent.history',
158
- 'subagent.prompt',
159
- 'subagent.interrupt',
160
- 'host.describe',
161
- 'host.listDirectory',
162
- 'workspace.list',
163
- 'workspace.create',
164
- 'workspace.rename',
165
- 'workspace.delete',
166
- 'workspace.insertBefore',
167
- 'workspace.insertSessionBefore',
168
- 'workspace.archiveSession',
169
- 'skill.list',
170
- 'agentPreset.list',
171
- 'agentPreset.select',
172
- 'agentPreset.read',
173
- 'goal.create',
174
- 'goal.edit',
175
- 'goal.pause',
176
- 'goal.resume',
177
- 'goal.complete',
178
- 'goal.clear',
179
- 'commands.execute',
180
- 'commands.list',
181
- 'llm.providers',
182
- 'llm.models',
183
- 'llm.discoverModels',
184
- 'settings.describe',
185
- 'settings.update',
186
- 'settings.replace',
187
- 'settings.mutate',
188
- 'credentials.describe',
189
- 'credentials.set',
190
- 'credentials.unset',
191
- ];
192
- /**
193
- * Native Harness ApiProxy call timeout. The Remote Web frontend gives each
194
- * `harness.api.call` RPC a 60s window; this bridge must fail faster so the
195
- * RPC error (not a silent Web-side timeout) reaches the peer and the pending
196
- * call is released. 30s gives slow native methods room while still beating the
197
- * Web-side 60s timer by a wide margin.
198
- */
199
- const NATIVE_CALL_TIMEOUT_MS = 30_000;
200
- const MAX_ACTIVE_API_TRANSFERS = MAX_ACTIVE_TRANSFERS_PER_DIRECTION;
201
- const API_TRANSFER_IDLE_MS = TRANSFER_IDLE_MS;
202
- const INLINE_TRANSFER_RESPONSE_BYTES = 2 * 1024 * 1024;
203
- export class HarnessApiBridge {
204
- api;
205
- publish;
206
- maxStreams;
207
- logger;
208
- harnessVersion;
209
- methods;
210
- streams = new Map();
211
- respondable = new Map();
212
- incomingTransfers = new Map();
213
- outgoingTransfers = new Map();
214
- mux;
215
- host;
216
- answer;
217
- constructor(api, publish, maxStreams = 8, logger, typertGateway, harnessVersion) {
218
- this.api = api;
219
- this.publish = publish;
220
- this.maxStreams = maxStreams;
221
- this.logger = logger;
222
- this.harnessVersion = harnessVersion;
223
- this.methods = createMethodMap(api, typertGateway, harnessVersion);
224
- this.mux = api.events.mux.bind(api.events);
225
- this.host = api.events.host.bind(api.events);
226
- this.answer = api.respond.bind(api);
227
- }
228
- /** Answer one ApiProxy call, sending bytes in the form DSH's own connection layer expects. */
229
- async call(input) {
230
- const response = await this.dispatchCall(input);
231
- const result = response.result;
232
- if (result === undefined)
233
- return response;
234
- const tagged = collectRpcAttachments(result);
235
- return tagged === result ? response : { ...response, result: tagged };
236
- }
237
- async dispatchCall(input) {
238
- const params = callSchema.parse(input);
239
- const signal = AbortSignal.timeout(NATIVE_CALL_TIMEOUT_MS);
240
- const method = this.methods.get(params.method);
241
- if (method === undefined) {
242
- if (params.method === 'host.listDirectory') {
243
- return this.callDirectoryFallback(params, signal);
244
- }
245
- if (params.method === 'host.describe') {
246
- return this.describeFallback(params.rpcId);
247
- }
248
- throw deniedMethod(params.method);
249
- }
250
- const startedAt = performance.now();
251
- const request = { rpcId: params.rpcId, payload: params.payload };
252
- try {
253
- // Race the native call against the timeout: some native ApiProxy
254
- // methods ignore AbortSignal, and without this the Host would never
255
- // answer and the Web-side 60s timer would fire instead. A guaranteed
256
- // local response turns that into a fast, explicit RPC error.
257
- const callWithTimeout = (overridePayload) => withTimeout(method({ rpcId: params.rpcId, payload: overridePayload }, signal), NATIVE_CALL_TIMEOUT_MS, `Harness API call ${params.method} timed out after ${NATIVE_CALL_TIMEOUT_MS}ms`);
258
- let response;
259
- if (params.method === 'session.history') {
260
- response = await callSessionHistory(callWithTimeout, params.payload, params.rpcId);
261
- }
262
- else {
263
- response = await callWithTimeout(request.payload);
264
- }
265
- if (params.method === 'host.listDirectory' && needsRemoteDirectoryFallback(response)) {
266
- response = await this.callDirectoryFallback(params, signal);
267
- }
268
- if (params.method === 'host.describe') {
269
- response = normalizeHostDescribe(response, this.harnessVersion, true);
270
- }
271
- this.logger?.debug('harness api call ok', {
272
- method: params.method,
273
- durationMs: Math.round(performance.now() - startedAt),
274
- });
275
- return response;
276
- }
277
- catch (error) {
278
- const durationMs = Math.round(performance.now() - startedAt);
279
- this.logger?.warn('harness api call failed', {
280
- method: params.method,
281
- durationMs,
282
- timedOut: signal.aborted,
283
- code: safeErrorCode(error),
284
- });
285
- throw error;
286
- }
287
- }
288
- async callDirectoryFallback(params, signal) {
289
- const payload = directoryListSchema.parse(params.payload);
290
- const value = await listRemoteDirectory(payload.path, signal);
291
- return { rpcId: params.rpcId, result: { ok: true, value } };
292
- }
293
- describeFallback(rpcId) {
294
- return {
295
- rpcId: rpcId,
296
- result: {
297
- ok: true,
298
- value: {
299
- version: this.harnessVersion ?? '0.0.1',
300
- cwd: '',
301
- home: '',
302
- attachedSessions: 0,
303
- canOpenPath: true,
304
- },
305
- },
306
- };
307
- }
308
- openTransfer(input) {
309
- this.pruneTransfers();
310
- const params = transferOpenSchema.parse(input);
311
- if (params.totalChunks !== Math.ceil(params.totalBytes / HARNESS_API_TRANSFER_CHUNK_BYTES)) {
312
- throw new RpcError('INVALID_MESSAGE', 'The Harness API transfer chunk count is invalid.');
313
- }
314
- if (this.incomingTransfers.has(params.transferId) || this.outgoingTransfers.has(params.transferId)) {
315
- throw new RpcError('REQUEST_CONFLICT', 'The Harness API transfer id is already active.');
316
- }
317
- if (this.incomingTransfers.size >= MAX_ACTIVE_API_TRANSFERS) {
318
- throw new RpcError('RATE_LIMITED', 'Too many Harness API transfers are active.', undefined, true);
319
- }
320
- this.incomingTransfers.set(params.transferId, {
321
- totalBytes: params.totalBytes,
322
- totalChunks: params.totalChunks,
323
- chunks: [],
324
- receivedBytes: 0,
325
- touchedAt: Date.now(),
326
- });
327
- return { opened: true, transferId: params.transferId };
328
- }
329
- appendTransfer(input) {
330
- this.pruneTransfers();
331
- const params = transferChunkSchema.parse(input);
332
- const transfer = this.incomingTransfers.get(params.transferId);
333
- if (transfer === undefined)
334
- throw new RpcError('TRANSFER_NOT_FOUND', 'The Harness API transfer is not active.');
335
- if (params.index !== transfer.chunks.length || params.index >= transfer.totalChunks) {
336
- this.incomingTransfers.delete(params.transferId);
337
- throw new RpcError('INVALID_MESSAGE', 'Harness API transfer chunks must arrive exactly once and in order.');
338
- }
339
- let chunk;
340
- try {
341
- chunk = decodeCanonicalBase64(params.data);
342
- }
343
- catch (error) {
344
- this.incomingTransfers.delete(params.transferId);
345
- throw error;
346
- }
347
- const expectedBytes = Math.min(HARNESS_API_TRANSFER_CHUNK_BYTES, transfer.totalBytes - params.index * HARNESS_API_TRANSFER_CHUNK_BYTES);
348
- if (chunk.byteLength !== expectedBytes) {
349
- this.incomingTransfers.delete(params.transferId);
350
- throw new RpcError('INVALID_MESSAGE', 'The Harness API transfer chunk size is invalid.');
351
- }
352
- transfer.chunks.push(chunk);
353
- transfer.receivedBytes += chunk.byteLength;
354
- transfer.touchedAt = Date.now();
355
- return { accepted: true, transferId: params.transferId, index: params.index };
356
- }
357
- async commitTransfer(input) {
358
- this.pruneTransfers();
359
- const params = transferCommitSchema.parse(input);
360
- const transfer = this.incomingTransfers.get(params.transferId);
361
- if (transfer === undefined)
362
- throw new RpcError('TRANSFER_NOT_FOUND', 'The Harness API transfer is not active.');
363
- this.incomingTransfers.delete(params.transferId);
364
- if (transfer.chunks.length !== transfer.totalChunks || transfer.receivedBytes !== transfer.totalBytes) {
365
- throw new RpcError('INVALID_MESSAGE', 'The Harness API transfer is incomplete.');
366
- }
367
- let request;
368
- try {
369
- const bytes = concatChunks(transfer.chunks, transfer.totalBytes);
370
- request = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
371
- }
372
- catch {
373
- throw new RpcError('INVALID_MESSAGE', 'The Harness API transfer does not contain a valid request.');
374
- }
375
- // Parse before dispatch so a transfer cannot bypass the exact same native
376
- // envelope validation and method allowlist used by harness.api.call.
377
- const nativeRequest = callSchema.parse(request);
378
- const response = await this.call(nativeRequest);
379
- const responseBytes = new TextEncoder().encode(JSON.stringify(response));
380
- if (responseBytes.byteLength <= INLINE_TRANSFER_RESPONSE_BYTES) {
381
- return { kind: 'inline', response };
382
- }
383
- if (responseBytes.byteLength > MAX_HARNESS_API_TRANSFER_BYTES) {
384
- throw new RpcError('RESPONSE_TOO_LARGE', 'The Harness API response exceeds the bounded transfer limit.');
385
- }
386
- this.pruneTransfers();
387
- if (this.outgoingTransfers.size >= MAX_ACTIVE_API_TRANSFERS) {
388
- throw new RpcError('RATE_LIMITED', 'Too many Harness API response transfers are active.', undefined, true);
389
- }
390
- const totalChunks = Math.ceil(responseBytes.byteLength / HARNESS_API_TRANSFER_CHUNK_BYTES);
391
- this.outgoingTransfers.set(params.transferId, {
392
- bytes: responseBytes,
393
- totalChunks,
394
- nextIndex: 0,
395
- touchedAt: Date.now(),
396
- });
397
- return { kind: 'chunked', transferId: params.transferId, totalBytes: responseBytes.byteLength, totalChunks };
398
- }
399
- readTransfer(input) {
400
- this.pruneTransfers();
401
- const params = transferReadSchema.parse(input);
402
- const transfer = this.outgoingTransfers.get(params.transferId);
403
- if (transfer === undefined)
404
- throw new RpcError('TRANSFER_NOT_FOUND', 'The Harness API response transfer is not active.');
405
- if (params.index !== transfer.nextIndex || params.index >= transfer.totalChunks) {
406
- this.outgoingTransfers.delete(params.transferId);
407
- throw new RpcError('INVALID_MESSAGE', 'Harness API response chunks must be read exactly once and in order.');
408
- }
409
- const start = params.index * HARNESS_API_TRANSFER_CHUNK_BYTES;
410
- const end = Math.min(start + HARNESS_API_TRANSFER_CHUNK_BYTES, transfer.bytes.byteLength);
411
- transfer.nextIndex += 1;
412
- transfer.touchedAt = Date.now();
413
- return {
414
- transferId: params.transferId,
415
- index: params.index,
416
- data: Buffer.from(transfer.bytes.subarray(start, end)).toString('base64'),
417
- };
418
- }
419
- closeTransfer(input) {
420
- const params = transferCloseSchema.parse(input);
421
- const incoming = this.incomingTransfers.delete(params.transferId);
422
- const outgoing = this.outgoingTransfers.delete(params.transferId);
423
- return { closed: incoming || outgoing, transferId: params.transferId };
424
- }
425
- async respond(input) {
426
- const params = respondSchema.parse(input);
427
- this.logger?.debug('harness api respond', { rpcId: shortId(params.message.rpcId) });
428
- if (!this.respondable.has(params.message.rpcId)) {
429
- throw new RpcError('PERMISSION_NOT_PENDING', 'The response id was not emitted on this peer connection.');
430
- }
431
- const receipt = await this.answer(params.message);
432
- if (receipt.accepted || receipt.reason === 'not-pending')
433
- this.respondable.delete(params.message.rpcId);
434
- return receipt;
435
- }
436
- openStream(input) {
437
- const params = streamOpenSchema.parse(input);
438
- if (this.streams.has(params.streamId))
439
- throw new RpcError('REQUEST_CONFLICT', 'The Harness event stream is already open.');
440
- if (this.streams.size >= this.maxStreams)
441
- throw new RpcError('RATE_LIMITED', 'Too many Harness event streams are open.', undefined, true);
442
- const controller = new AbortController();
443
- const request = { rpcId: params.rpcId, payload: params.payload };
444
- const stream = params.stream === 'mux'
445
- ? this.mux(request, controller.signal)
446
- : this.host(request, controller.signal);
447
- const focusSessionId = params.stream === 'mux' ? params.payload.sessionId : undefined;
448
- const task = this.pump(params.streamId, stream, controller.signal, focusSessionId);
449
- this.streams.set(params.streamId, { controller, task, ...(focusSessionId === undefined ? {} : { focusSessionId }) });
450
- this.logger?.debug('harness api stream open', {
451
- stream: params.stream,
452
- streamId: shortId(params.streamId),
453
- ...(focusSessionId === undefined ? {} : { focusSessionId: shortId(focusSessionId) }),
454
- });
455
- return { opened: true, streamId: params.streamId };
456
- }
457
- closeStream(input) {
458
- const params = streamCloseSchema.parse(input);
459
- const active = this.streams.get(params.streamId);
460
- if (active !== undefined) {
461
- // Free the slot synchronously. A native ApiProxy stream may not observe
462
- // the abort until its next frame, so a session-focused mux stream on an
463
- // idle session could otherwise hold its slot forever and block the
464
- // client's documented close-then-reopen session switch with
465
- // RATE_LIMITED. The pump's finally performs a no-op delete later.
466
- this.streams.delete(params.streamId);
467
- active.controller.abort();
468
- }
469
- this.logger?.debug('harness api stream close', { streamId: shortId(params.streamId), closed: active !== undefined });
470
- return { closed: active !== undefined, streamId: params.streamId };
471
- }
472
- async closeAll(reason = 'peer-disconnected') {
473
- const streams = [...this.streams.values()];
474
- this.streams.clear();
475
- this.respondable.clear();
476
- this.incomingTransfers.clear();
477
- this.outgoingTransfers.clear();
478
- for (const stream of streams)
479
- stream.controller.abort(reason);
480
- // A native ApiProxy stream may not observe AbortSignal until its next
481
- // frame. Waiting for every pump here would block a replacement peer from
482
- // installing its message handler indefinitely. The detached pumps own
483
- // their errors and terminal event publication, so abort and release them
484
- // without holding up the authenticated connection handoff.
485
- }
486
- pruneTransfers() {
487
- const cutoff = Date.now() - API_TRANSFER_IDLE_MS;
488
- for (const [transferId, transfer] of this.incomingTransfers) {
489
- if (transfer.touchedAt < cutoff)
490
- this.incomingTransfers.delete(transferId);
491
- }
492
- for (const [transferId, transfer] of this.outgoingTransfers) {
493
- if (transfer.touchedAt < cutoff)
494
- this.outgoingTransfers.delete(transferId);
495
- }
496
- }
497
- async pump(streamId, stream, signal, focusSessionId) {
498
- let reason = 'completed';
499
- try {
500
- for await (const frame of stream) {
501
- if (signal.aborted)
502
- break;
503
- if (focusSessionId !== undefined && frameSessionId(frame) !== undefined && frameSessionId(frame) !== focusSessionId) {
504
- // Keep pushing to the native stream (the peer's upstream may still
505
- // emit approvals for the focused session through the same pump) but
506
- // do not forward other sessions' traffic over the tunnel.
507
- continue;
508
- }
509
- this.trackRespondable(frame);
510
- await this.publish('harness.api.frame', { streamId, frame });
511
- }
512
- if (signal.aborted)
513
- reason = 'cancelled';
514
- }
515
- catch {
516
- reason = signal.aborted ? 'cancelled' : 'failed';
517
- }
518
- finally {
519
- this.streams.delete(streamId);
520
- await this.publish('harness.api.stream.closed', { streamId, reason }).catch(() => undefined);
521
- }
522
- }
523
- trackRespondable(frame) {
524
- const payload = frame.payload;
525
- if (payload.type === 'approval/requested') {
526
- this.respondable.set(String(frame.rpcId), `approval:${String(payload.approvalId)}`);
527
- return;
528
- }
529
- if (payload.type === 'question/requested') {
530
- this.respondable.set(String(frame.rpcId), `question:${String(frame.rpcId)}`);
531
- return;
532
- }
533
- if (payload.type === 'approval/resolved') {
534
- this.deleteRespondable(`approval:${String(payload.approvalId)}`);
535
- return;
536
- }
537
- if (payload.type === 'question/resolved') {
538
- this.respondable.delete(String(payload.questionRpcId));
539
- }
540
- }
541
- deleteRespondable(value) {
542
- for (const [rpcId, correlation] of this.respondable) {
543
- if (correlation === value)
544
- this.respondable.delete(rpcId);
545
- }
546
- }
547
- }
548
- function needsRemoteDirectoryFallback(response) {
549
- const result = response.result;
550
- return typeof result === 'object' && result !== null && 'ok' in result && result.ok === false
551
- && 'error' in result && typeof result.error === 'object' && result.error !== null
552
- && 'code' in result.error && result.error.code === 'directory-picker-unavailable';
553
- }
554
- function normalizeHostDescribe(response, harnessVersion, canListDirectory) {
555
- if (!response.result.ok)
556
- return response;
557
- const value = response.result.value;
558
- if (typeof value !== 'object' || value === null || Array.isArray(value))
559
- return response;
560
- const description = value;
561
- const selectedVersion = selectHarnessVersion(normalizeHarnessVersion(description.version), harnessVersion);
562
- const versionChanged = selectedVersion !== undefined && description.version !== selectedVersion;
563
- const canOpenPathChanged = canListDirectory && description.canOpenPath !== true;
564
- if (!versionChanged && !canOpenPathChanged)
565
- return response;
566
- const normalized = {
567
- ...description,
568
- ...(versionChanged ? { version: selectedVersion } : {}),
569
- ...(canOpenPathChanged ? { canOpenPath: true } : {}),
570
- };
571
- return {
572
- ...response,
573
- result: {
574
- ...response.result,
575
- value: normalized,
576
- },
577
- };
578
- }
579
- function createMethodMap(api, typertGateway, harnessVersion) {
580
- const domains = api;
581
- const methods = new Map();
582
- for (const method of HARNESS_API_ALLOWLIST) {
583
- if (method === 'commands.execute' || method === 'commands.list') {
584
- // Commands live behind the official Typert registry (the ApiProxy has
585
- // no `commands` domain). Dispatch them through the gateway when it is
586
- // available; without it the method stays denied (fail-closed).
587
- if (typertGateway === undefined)
588
- continue;
589
- const [namespace, commandMethod] = method.split('.');
590
- const implementation = async (request, signal) => {
591
- if (commandMethod === 'execute') {
592
- const payload = commandExecuteSchema.parse(request.payload);
593
- const legacyArgs = {
594
- agentId: payload.agentId,
595
- line: payload.line,
596
- images: payload.images ?? [],
597
- };
598
- const currentArgs = {
599
- agentId: payload.agentId,
600
- line: payload.line,
601
- submittedAttachments: payload.submittedAttachments ?? [],
602
- };
603
- const args = harnessSessionGeneration(harnessVersion) === 'v3' ? currentArgs : legacyArgs;
604
- const value = await typertGateway.invoke({
605
- namespace,
606
- method: 'execute',
607
- args,
608
- ...(signal === undefined ? {} : { signal }),
609
- });
610
- return { rpcId: request.rpcId, result: { ok: true, value } };
611
- }
612
- const args = commandListSchema.parse(request.payload);
613
- const value = await typertGateway.invoke({
614
- namespace,
615
- method: 'list',
616
- args,
617
- ...(signal === undefined ? {} : { signal }),
618
- });
619
- return { rpcId: request.rpcId, result: { ok: true, value } };
620
- };
621
- methods.set(method, implementation);
622
- continue;
623
- }
624
- const [wireDomain, action] = method.split('.');
625
- const domain = domainProperty(wireDomain);
626
- const implementation = domains[domain]?.[action];
627
- if (typeof implementation !== 'function')
628
- continue;
629
- const scoped = CONFIG_PLANE_METHODS.includes(method)
630
- ? scopeConfigPlaneMethod(api, method, implementation)
631
- : undefined;
632
- methods.set(method, scoped ?? implementation.bind(domains[domain]));
633
- }
634
- return methods;
635
- }
636
- function domainProperty(wireDomain) {
637
- if (wireDomain === 'session')
638
- return 'sessions';
639
- if (wireDomain === 'subagent')
640
- return 'subagents';
641
- if (wireDomain === 'skill')
642
- return 'skills';
643
- if (wireDomain === 'agentPreset')
644
- return 'agentPresets';
645
- if (wireDomain === 'goal')
646
- return 'goals';
647
- return wireDomain;
648
- }
649
- /**
650
- * Config-plane methods that need scoping beyond the plain allowlist. Settings
651
- * writes target only namespaces currently registered with the Host settings
652
- * seam, credential payloads are bounded and write-only, and model discovery
653
- * probes only HTTPS/localhost-HTTP endpoints with credential-free failures.
654
- */
655
- const CONFIG_PLANE_METHODS = [
656
- 'llm.discoverModels',
657
- 'settings.describe',
658
- 'settings.update',
659
- 'settings.replace',
660
- 'settings.mutate',
661
- 'credentials.describe',
662
- 'credentials.set',
663
- 'credentials.unset',
664
- ];
665
- function scopeConfigPlaneMethod(api, method, native) {
666
- switch (method) {
667
- case 'llm.discoverModels':
668
- return async (request, signal) => {
669
- const payload = parseConfigPlane(discoverModelsSchema, request.payload, 'llm.discoverModels');
670
- if (payload.baseURL !== undefined)
671
- validateDiscoverBaseUrl(payload.baseURL);
672
- try {
673
- const response = await native({ rpcId: request.rpcId, payload }, signal);
674
- return sanitizeModelDiscoveryResponse(response, payload.settingsNs);
675
- }
676
- catch {
677
- return modelDiscoveryFailure(request.rpcId, payload.settingsNs);
678
- }
679
- };
680
- case 'settings.describe':
681
- return async (request, signal) => {
682
- parseConfigPlane(settingsDescribeSchema, request.payload, 'settings.describe');
683
- const response = await native({ rpcId: request.rpcId, payload: {} }, signal);
684
- return disableRemoteSettingsDocument(response);
685
- };
686
- case 'settings.update':
687
- return async (request, signal) => {
688
- const payload = parseConfigPlane(settingsUpdateSchema, request.payload, 'settings.update');
689
- await assertRegisteredSettingsNamespace(api, payload.ns);
690
- assertSerializedBytes(payload.patch, CONFIG_PLANE_SETTINGS_BYTES, 'settings.update patch');
691
- return native({ rpcId: request.rpcId, payload }, signal);
692
- };
693
- case 'settings.replace':
694
- return async (request, signal) => {
695
- const payload = parseConfigPlane(settingsReplaceSchema, request.payload, 'settings.replace');
696
- await assertRegisteredSettingsNamespace(api, payload.ns);
697
- assertSerializedBytes(payload.section, CONFIG_PLANE_SETTINGS_BYTES, 'settings.replace section');
698
- return native({ rpcId: request.rpcId, payload }, signal);
699
- };
700
- case 'settings.mutate':
701
- return async (request, signal) => {
702
- const payload = parseConfigPlane(settingsMutateSchema, request.payload, 'settings.mutate');
703
- await assertRegisteredSettingsNamespace(api, payload.ns);
704
- assertSerializedBytes(payload.ops, CONFIG_PLANE_SETTINGS_BYTES, 'settings.mutate ops');
705
- return native({ rpcId: request.rpcId, payload }, signal);
706
- };
707
- case 'credentials.describe':
708
- return async (request, signal) => {
709
- const payload = parseConfigPlane(credentialsDescribeSchema, request.payload, 'credentials.describe');
710
- return native({ rpcId: request.rpcId, payload }, signal);
711
- };
712
- case 'credentials.set':
713
- return async (request, signal) => {
714
- const payload = parseConfigPlane(credentialsSetSchema, request.payload, 'credentials.set');
715
- return native({ rpcId: request.rpcId, payload }, signal);
716
- };
717
- case 'credentials.unset':
718
- return async (request, signal) => {
719
- const payload = parseConfigPlane(credentialsUnsetSchema, request.payload, 'credentials.unset');
720
- return native({ rpcId: request.rpcId, payload }, signal);
721
- };
722
- }
723
- }
724
- function parseConfigPlane(schema, payload, label) {
725
- try {
726
- return schema.parse(payload);
727
- }
728
- catch (error) {
729
- if (error instanceof z.ZodError) {
730
- throw new RpcError('INVALID_MESSAGE', `The ${label} payload is invalid for the remote channel.`);
731
- }
732
- throw error;
733
- }
734
- }
735
- function validateDiscoverBaseUrl(value) {
736
- let url;
737
- try {
738
- url = new URL(value);
739
- }
740
- catch {
741
- throw new RpcError('INVALID_MESSAGE', 'The model discovery baseURL is invalid.');
742
- }
743
- const local = url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '::1';
744
- if (url.protocol !== 'https:' && !(local && url.protocol === 'http:')) {
745
- throw new RpcError('INVALID_MESSAGE', 'The model discovery baseURL must use HTTPS (HTTP is allowed only for localhost).');
746
- }
747
- if (url.username !== '' || url.password !== '') {
748
- throw new RpcError('INVALID_MESSAGE', 'The model discovery baseURL must not contain credentials.');
749
- }
750
- if (url.hash !== '') {
751
- throw new RpcError('INVALID_MESSAGE', 'The model discovery baseURL must not contain a fragment.');
752
- }
753
- }
754
- function assertSerializedBytes(value, maxBytes, label) {
755
- const bytes = new TextEncoder().encode(JSON.stringify(value)).byteLength;
756
- if (bytes > maxBytes) {
757
- throw new RpcError('INVALID_MESSAGE', `The ${label} exceeds the ${maxBytes}-byte remote limit.`);
758
- }
759
- }
760
- function sanitizeModelDiscoveryResponse(response, settingsNs) {
761
- const result = response.result;
762
- if (typeof result === 'object' && result !== null && 'ok' in result && result.ok === true)
763
- return response;
764
- return modelDiscoveryFailure(response.rpcId, settingsNs);
765
- }
766
- function modelDiscoveryFailure(rpcId, settingsNs) {
767
- return {
768
- rpcId,
769
- result: {
770
- ok: false,
771
- error: {
772
- code: 'model-discovery-failed',
773
- message: 'Model discovery failed.',
774
- details: { settingsNs },
775
- },
776
- },
777
- };
778
- }
779
- function disableRemoteSettingsDocument(response) {
780
- const result = response.result;
781
- if (typeof result !== 'object' || result === null || !('ok' in result) || result.ok !== true)
782
- return response;
783
- const value = result.value;
784
- if (typeof value !== 'object' || value === null)
785
- return response;
786
- return { ...response, result: { ...result, value: { ...value, hasDocument: false } } };
787
- }
788
- const EMPTY_SETTINGS_NAMESPACES = new Set();
789
- async function assertRegisteredSettingsNamespace(api, ns) {
790
- if (ns === 'ds-harness-remote' || ns === 'dsh-remote') {
791
- throw new RpcError('PERMISSION_DENIED', 'Remote access settings can only be changed locally on the Host.');
792
- }
793
- const allowed = await registeredSettingsNamespaces(api);
794
- if (!allowed.has(ns))
795
- throw deniedSettingsNamespace(ns);
796
- }
797
- /** Namespaces currently exposed by the official live Host settings directory. */
798
- async function registeredSettingsNamespaces(api) {
799
- const settings = api.settings;
800
- const describe = settings?.describe;
801
- if (typeof describe !== 'function')
802
- return EMPTY_SETTINGS_NAMESPACES;
803
- const response = await withTimeout(describe.call(settings, { rpcId: 'bridge-settings-scope', payload: {} }, AbortSignal.timeout(NATIVE_CALL_TIMEOUT_MS)), NATIVE_CALL_TIMEOUT_MS, 'The Host settings.describe call timed out.');
804
- const value = unwrapNativeValue(response, 'settings.describe');
805
- const list = typeof value === 'object' && value !== null ? value.namespaces : undefined;
806
- if (!Array.isArray(list))
807
- return EMPTY_SETTINGS_NAMESPACES;
808
- const namespaces = new Set();
809
- for (const item of list) {
810
- if (typeof item !== 'object' || item === null)
811
- continue;
812
- const ns = item.ns;
813
- if (typeof ns === 'string' && ns.length > 0)
814
- namespaces.add(ns);
815
- }
816
- return namespaces;
817
- }
818
- function unwrapNativeValue(response, method) {
819
- const result = response.result;
820
- if (typeof result !== 'object' || result === null || !('ok' in result) || result.ok !== true || !('value' in result)) {
821
- throw new RpcError('INTERNAL_ERROR', `The Host ${method} call did not succeed.`);
822
- }
823
- return result.value;
824
- }
825
- function deniedSettingsNamespace(ns) {
826
- return new RpcError('METHOD_NOT_ALLOWED', `Harness settings namespace ${JSON.stringify(ns)} is not registered on the Host.`);
827
- }
828
- function deniedMethod(method) {
829
- return new RpcError('METHOD_NOT_ALLOWED', `Harness API method ${JSON.stringify(method)} is not available in remote mode.`);
830
- }
831
- /** Session id of a mux frame, or undefined for frames without one (e.g. stream/error). */
832
- function frameSessionId(frame) {
833
- const payload = frame.payload;
834
- return typeof payload.sessionId === 'string' && payload.sessionId.length > 0 ? payload.sessionId : undefined;
835
- }
836
- function withTimeout(promise, ms, message) {
837
- return new Promise((resolve, reject) => {
838
- const timer = setTimeout(() => {
839
- reject(new RpcError('TIMEOUT', message, undefined, true));
840
- }, ms);
841
- timer.unref?.();
842
- promise.then(value => { clearTimeout(timer); resolve(value); }, error => { clearTimeout(timer); reject(error); });
843
- });
844
- }
845
- function shortId(value) { return value.length <= 12 ? value : `${value.slice(0, 8)}…${value.slice(-4)}`; }
846
- function decodeCanonicalBase64(value) {
847
- const bytes = Buffer.from(value, 'base64');
848
- if (bytes.toString('base64') !== value) {
849
- throw new RpcError('INVALID_MESSAGE', 'The Harness API transfer chunk is not canonical base64.');
850
- }
851
- return bytes;
852
- }
853
- function concatChunks(chunks, totalBytes) {
854
- const result = new Uint8Array(totalBytes);
855
- let offset = 0;
856
- for (const chunk of chunks) {
857
- result.set(chunk, offset);
858
- offset += chunk.byteLength;
859
- }
860
- return result;
861
- }
862
- //# sourceMappingURL=harness-api-bridge.js.map