@blue-soda/dsh-remote 0.4.28

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