@openagents-org/agent-launcher 0.2.192 → 0.2.194

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.
@@ -5,6 +5,29 @@ const http = require('http');
5
5
 
6
6
  const DEFAULT_ENDPOINT = 'https://workspace-endpoint.openagents.org';
7
7
 
8
+ /**
9
+ * The error a request deadline produces. Every request carries a hard
10
+ * AbortSignal.timeout deadline (see _get), and when it fires Node rejects with
11
+ * a bare "AbortError: The operation was aborted" — which reads like somebody
12
+ * cancelled the call and names no cause. The socket-inactivity timeout said
13
+ * "Request timed out". Both now say the same thing, with the deadline, and
14
+ * carry ETIMEDOUT so callers can tell a slow network from a bug.
15
+ */
16
+ function requestTimeoutError(timeout) {
17
+ const after = timeout >= 1000 ? `${Math.round(timeout / 1000)}s` : `${timeout}ms`;
18
+ const err = new Error(`Request timed out after ${after}`);
19
+ err.code = 'ETIMEDOUT';
20
+ return err;
21
+ }
22
+
23
+ /** A request 'error' handler that reports a fired deadline as a timeout. */
24
+ function rejectWithDeadline(reject, timeout) {
25
+ return (err) => {
26
+ const aborted = err && (err.name === 'AbortError' || err.code === 'ABORT_ERR');
27
+ reject(aborted ? requestTimeoutError(timeout) : err);
28
+ };
29
+ }
30
+
8
31
  /**
9
32
  * Thrown when the workspace rejects a request because our session_id has
10
33
  * been revoked by a newer /v1/join as the same agent. Callers should
@@ -18,6 +41,30 @@ class SessionRevokedError extends Error {
18
41
  }
19
42
  }
20
43
 
44
+ /**
45
+ * Reject when a response is cut off after its headers arrived.
46
+ *
47
+ * Node reports that on the RESPONSE, and nowhere else: once a response
48
+ * exists the request emits no 'error' for a connection that closes early,
49
+ * 'end' only fires for a complete body, and the request's abort signal is a
50
+ * no-op by then because the socket is already gone. A request helper that
51
+ * listens for 'end' alone therefore never settles — and each poll loop is a
52
+ * single await, so one truncated response (a backend restart mid-reply is
53
+ * enough) silences that loop for the life of the daemon. When it is the
54
+ * control poller the agent keeps working and answering, and ignores Stop.
55
+ */
56
+ function rejectOnTruncatedResponse(res, reject) {
57
+ res.on('error', reject);
58
+ res.on('close', () => {
59
+ if (res.complete) return;
60
+ // Same code Node puts on the 'error' above, so callers count either as
61
+ // the network blip it is.
62
+ const err = new Error('Response ended before it was complete');
63
+ err.code = 'ECONNRESET';
64
+ reject(err);
65
+ });
66
+ }
67
+
21
68
  /**
22
69
  * HTTP client for workspace API operations.
23
70
  *
@@ -25,8 +72,34 @@ class SessionRevokedError extends Error {
25
72
  * auth headers (X-Workspace-Token), same request/response shapes.
26
73
  */
27
74
  class WorkspaceClient {
28
- constructor(endpoint) {
75
+ /**
76
+ * @param {string} [endpoint]
77
+ * @param {{ agentName?: string }} [opts] — when the client acts for ONE
78
+ * agent, pass its name: every request then carries `X-Agent-Name` so the
79
+ * workspace can apply that agent's permissions (permission model v1.1 §3)
80
+ * instead of treating the call as an anonymous machine. Machine-level
81
+ * clients (daemon device heartbeat, pairing redeem, workspace removal)
82
+ * pass nothing and send no agent identity.
83
+ */
84
+ constructor(endpoint, { agentName } = {}) {
29
85
  this.endpoint = (endpoint || DEFAULT_ENDPOINT).replace(/\/$/, '');
86
+ this.agentName = typeof agentName === 'string' && agentName.trim() ? agentName.trim() : null;
87
+ }
88
+
89
+ /** A client for the same endpoint that acts as `agentName` (or none). */
90
+ withAgent(agentName) {
91
+ return new WorkspaceClient(this.endpoint, { agentName });
92
+ }
93
+
94
+ /**
95
+ * Request headers + the agent identity header. Applied in every transport
96
+ * helper (_get/_getRaw/_post/_put/_patch/_delete) so no call site can forget
97
+ * it. Returns a copy — callers' header objects are never mutated.
98
+ */
99
+ _withAgent(headers) {
100
+ const out = { ...(headers || {}) };
101
+ if (this.agentName && !out['X-Agent-Name']) out['X-Agent-Name'] = this.agentName;
102
+ return out;
30
103
  }
31
104
 
32
105
  /**
@@ -194,16 +267,80 @@ class WorkspaceClient {
194
267
  * If the server's current session for this agent differs, _post()
195
268
  * throws SessionRevokedError and the caller should stop its adapter.
196
269
  */
197
- async heartbeat(workspaceId, agentName, token, sessionId) {
270
+ async heartbeat(workspaceId, agentName, token, sessionId, presence) {
198
271
  const body = {
199
272
  agent_name: agentName,
200
273
  network: workspaceId,
201
274
  };
202
275
  if (sessionId) body.session_id = sessionId;
276
+ // v1.1 M3 — busy/queued detail ({ presence_state, busy_channels,
277
+ // queue_depth }, see BaseAdapter.buildPresenceFields). Optional so older
278
+ // callers keep sending the legacy body.
279
+ if (presence && typeof presence === 'object') {
280
+ if (presence.presence_state !== undefined) body.presence_state = presence.presence_state;
281
+ if (Array.isArray(presence.busy_channels)) body.busy_channels = presence.busy_channels;
282
+ if (Number.isFinite(presence.queue_depth)) body.queue_depth = presence.queue_depth;
283
+ }
203
284
  const data = await this._post('/v1/heartbeat', body, this._wsHeaders(token));
204
285
  return data.data || data;
205
286
  }
206
287
 
288
+ // ── Agent profile / presence (v1.1 M3/M4) ────────────────────────────────
289
+
290
+ /** GET /v1/agents/{agent}/availability → { status, presence_state,
291
+ * busy_channels, queue_depth, runtime_status, runtime_name, reason }. */
292
+ async getAgentAvailability(workspaceId, token, agentName) {
293
+ const data = await this._get(
294
+ `/v1/agents/${encodeURIComponent(agentName)}/availability?network=${encodeURIComponent(workspaceId)}`,
295
+ this._wsHeaders(token),
296
+ );
297
+ return data.data || data;
298
+ }
299
+
300
+ /** GET /v1/agents/{agent}/profile — the specialist card. */
301
+ async getAgentProfile(workspaceId, token, agentName) {
302
+ const data = await this._get(
303
+ `/v1/agents/${encodeURIComponent(agentName)}/profile?network=${encodeURIComponent(workspaceId)}`,
304
+ this._wsHeaders(token),
305
+ );
306
+ return data.data || data;
307
+ }
308
+
309
+ /**
310
+ * GET /v1/agents/{agent}/shared-context — what THIS agent's run may use when
311
+ * the request comes from a teammate rather than its owner. Machine-only.
312
+ * Returns { apply, owner_email, requester_email, shared_instructions,
313
+ * allowed_knowledge: [{ slug, title, content }], cost_owner }.
314
+ */
315
+ async getSharedContext(workspaceId, token, agentName, { requesterEmail, channel, timeout } = {}) {
316
+ const params = new URLSearchParams({ network: workspaceId });
317
+ if (requesterEmail) params.set('requester_email', requesterEmail);
318
+ if (channel) params.set('channel', channel);
319
+ const data = await this._get(
320
+ `/v1/agents/${encodeURIComponent(agentName)}/shared-context?${params}`,
321
+ this._wsHeaders(token),
322
+ timeout || 5000,
323
+ );
324
+ return data.data || data;
325
+ }
326
+
327
+ /**
328
+ * Raw message events of a channel (newest first by default) — for callers
329
+ * that need payload/metadata fields _eventToMessage drops (e.g. hand-offs).
330
+ */
331
+ async getChannelEvents(workspaceId, channelName, token, { limit = 100, sort = 'desc' } = {}) {
332
+ const params = new URLSearchParams({
333
+ network: workspaceId,
334
+ channel: channelName,
335
+ type: 'workspace.message',
336
+ sort,
337
+ limit: String(Math.min(Math.max(1, limit), 500)),
338
+ });
339
+ const data = await this._get(`/v1/events?${params}`, this._wsHeaders(token));
340
+ const result = data.data || data;
341
+ return (result && result.events) || [];
342
+ }
343
+
207
344
  /**
208
345
  * Disconnect agent via POST /v1/leave. Best-effort (ignores errors).
209
346
  */
@@ -300,7 +437,7 @@ class WorkspaceClient {
300
437
  * previous session fails (the channel's chat history is the only thing
301
438
  * that survives a session-storage rotation).
302
439
  */
303
- async getRecentMessages(workspaceId, channelName, token, limit = 30, { sort = 'desc' } = {}) {
440
+ async getRecentMessages(workspaceId, channelName, token, limit = 30, { sort = 'desc', before, timeout, throwOnError = false } = {}) {
304
441
  try {
305
442
  const params = new URLSearchParams({
306
443
  network: workspaceId,
@@ -309,14 +446,16 @@ class WorkspaceClient {
309
446
  sort,
310
447
  limit: String(limit),
311
448
  });
312
- const data = await this._get(`/v1/events?${params}`, this._wsHeaders(token));
449
+ if (before) params.set('before', before);
450
+ const data = await this._get(`/v1/events?${params}`, this._wsHeaders(token), timeout);
313
451
  const result = data.data || data;
314
452
  const events = (result && result.events) || [];
315
453
  // A desc window arrives newest-first; reverse so the caller always gets
316
454
  // chronological order. An asc window is already chronological.
317
455
  const ordered = sort === 'asc' ? events.slice() : events.slice().reverse();
318
456
  return ordered.map((e) => this._eventToMessage(e));
319
- } catch {
457
+ } catch (error) {
458
+ if (throwOnError) throw error;
320
459
  return [];
321
460
  }
322
461
  }
@@ -564,8 +703,17 @@ class WorkspaceClient {
564
703
  /**
565
704
  * Upload a file via POST /v1/files/base64.
566
705
  */
706
+ async publishWebProject(workspaceId, token, project, projectId) {
707
+ const body = { ...project, network: workspaceId };
708
+ const route = '/v1/web-projects' + (projectId ? '/' + encodeURIComponent(projectId) : '');
709
+ const data = projectId
710
+ ? await this._put(route, body, this._wsHeaders(token), 60000)
711
+ : await this._post(route, body, this._wsHeaders(token), 60000);
712
+ return data.data || data;
713
+ }
714
+
567
715
  async uploadFile(workspaceId, token, filename, contentBase64, {
568
- contentType = 'application/octet-stream', source = 'human:user', channelName,
716
+ contentType = 'application/octet-stream', source = 'human:user', channelName, visibility,
569
717
  } = {}) {
570
718
  const body = {
571
719
  filename,
@@ -575,6 +723,7 @@ class WorkspaceClient {
575
723
  source,
576
724
  };
577
725
  if (channelName) body.channel_name = channelName;
726
+ if (visibility) body.visibility = visibility;
578
727
 
579
728
  const data = await this._post('/v1/files/base64', body, this._wsHeaders(token), 60000);
580
729
  return data.data || data;
@@ -652,8 +801,8 @@ class WorkspaceClient {
652
801
  /**
653
802
  * Open a new browser tab via POST /v1/browser/tabs.
654
803
  */
655
- async browserOpenTab(workspaceId, token, { url = 'about:blank', source = 'human:user', context_id } = {}) {
656
- const body = { url, network: workspaceId, source };
804
+ async browserOpenTab(workspaceId, token, { url = 'about:blank', source = 'human:user', context_id, channel } = {}) {
805
+ const body = { url, network: workspaceId, source, channel };
657
806
  if (context_id) body.context_id = context_id;
658
807
  const data = await this._post('/v1/browser/tabs', body, this._wsHeaders(token));
659
808
  return data.data || data;
@@ -671,24 +820,24 @@ class WorkspaceClient {
671
820
  /**
672
821
  * Navigate a browser tab via POST /v1/browser/tabs/{tabId}/navigate.
673
822
  */
674
- async browserNavigate(workspaceId, token, tabId, url) {
675
- const data = await this._post(`/v1/browser/tabs/${tabId}/navigate`, { url }, this._wsHeaders(token));
823
+ async browserNavigate(workspaceId, token, tabId, url, { source, channel } = {}) {
824
+ const data = await this._post(`/v1/browser/tabs/${tabId}/navigate`, { url, source, channel }, this._wsHeaders(token));
676
825
  return data.data || data;
677
826
  }
678
827
 
679
828
  /**
680
829
  * Click an element via POST /v1/browser/tabs/{tabId}/click.
681
830
  */
682
- async browserClick(workspaceId, token, tabId, selector) {
683
- const data = await this._post(`/v1/browser/tabs/${tabId}/click`, { selector }, this._wsHeaders(token));
831
+ async browserClick(workspaceId, token, tabId, selector, { source, channel } = {}) {
832
+ const data = await this._post(`/v1/browser/tabs/${tabId}/click`, { selector, source, channel }, this._wsHeaders(token));
684
833
  return data.data || data;
685
834
  }
686
835
 
687
836
  /**
688
837
  * Type text via POST /v1/browser/tabs/{tabId}/type.
689
838
  */
690
- async browserType(workspaceId, token, tabId, selector, text) {
691
- const data = await this._post(`/v1/browser/tabs/${tabId}/type`, { selector, text }, this._wsHeaders(token));
839
+ async browserType(workspaceId, token, tabId, selector, text, { source, channel } = {}) {
840
+ const data = await this._post(`/v1/browser/tabs/${tabId}/type`, { selector, text, source, channel }, this._wsHeaders(token));
692
841
  return data.data || data;
693
842
  }
694
843
 
@@ -696,16 +845,22 @@ class WorkspaceClient {
696
845
  * Get screenshot via GET /v1/browser/tabs/{tabId}/screenshot.
697
846
  * @returns {Buffer}
698
847
  */
699
- async browserScreenshot(workspaceId, token, tabId) {
700
- return this._getRaw(`/v1/browser/tabs/${tabId}/screenshot`, this._wsHeaders(token), 30000);
848
+ async browserScreenshot(workspaceId, token, tabId, { source, channel } = {}) {
849
+ const query = new URLSearchParams();
850
+ if (source) query.set('source', source);
851
+ if (channel) query.set('channel', channel);
852
+ return this._getRaw(`/v1/browser/tabs/${tabId}/screenshot?${query}`, this._wsHeaders(token), 30000);
701
853
  }
702
854
 
703
855
  /**
704
856
  * Get accessibility snapshot via GET /v1/browser/tabs/{tabId}/snapshot.
705
857
  * @returns {string}
706
858
  */
707
- async browserSnapshot(workspaceId, token, tabId) {
708
- const buf = await this._getRaw(`/v1/browser/tabs/${tabId}/snapshot`, this._wsHeaders(token));
859
+ async browserSnapshot(workspaceId, token, tabId, { source, channel } = {}) {
860
+ const query = new URLSearchParams();
861
+ if (source) query.set('source', source);
862
+ if (channel) query.set('channel', channel);
863
+ const buf = await this._getRaw(`/v1/browser/tabs/${tabId}/snapshot?${query}`, this._wsHeaders(token));
709
864
  return buf.toString('utf-8');
710
865
  }
711
866
 
@@ -920,11 +1075,25 @@ class WorkspaceClient {
920
1075
  sessionId,
921
1076
  senderType: isHuman ? 'human' : 'agent',
922
1077
  senderName,
1078
+ senderDisplayName: payload.sender_display_name || (event.metadata || {}).sender_display_name || null,
923
1079
  content: (payload.content || event.content || ''),
924
1080
  mentions: payload.mentions || [],
925
1081
  messageType: payload.message_type || 'chat',
926
1082
  metadata: event.metadata || {},
927
1083
  };
1084
+ // The person behind a human message (v1.1): mobile puts it in the
1085
+ // payload, the Launcher in metadata, the web app uses its email as the
1086
+ // sender id. Used to fetch the owner-reviewed shared context for a
1087
+ // teammate's request and to tell the owner who asked.
1088
+ if (isHuman) {
1089
+ const meta = event.metadata || {};
1090
+ const email = [payload.sender_email, meta.sender_email, payload.sender_id]
1091
+ .find((v) => typeof v === 'string' && /^[^\s@]+@[^\s@]+$/.test(v.trim()));
1092
+ if (email) msg.senderEmail = email.trim().toLowerCase();
1093
+ }
1094
+ if (payload.handoff && typeof payload.handoff === 'object') {
1095
+ msg.handoff = payload.handoff;
1096
+ }
928
1097
  if (ts) {
929
1098
  msg.createdAt = new Date(ts).toISOString();
930
1099
  }
@@ -934,6 +1103,60 @@ class WorkspaceClient {
934
1103
  return msg;
935
1104
  }
936
1105
 
1106
+ // ── Approvals ─────────────────────────────────────────────────────────────
1107
+
1108
+ /** File an approval request (POST /v1/approvals). Returns the record; its
1109
+ * `status` may already be approved/rejected when policy auto-decided. */
1110
+ async createApproval(workspaceId, token, {
1111
+ channel, kind, action, details, risk, source, assignee_email, requester_email,
1112
+ } = {}) {
1113
+ const body = {
1114
+ network: workspaceId,
1115
+ channel,
1116
+ kind: kind || 'other',
1117
+ action,
1118
+ source: source || 'openagents:unknown',
1119
+ };
1120
+ if (details) body.details = String(details).slice(0, 4000);
1121
+ if (risk) body.risk = risk;
1122
+ if (assignee_email) body.assignee_email = assignee_email;
1123
+ if (requester_email) body.requester_email = requester_email;
1124
+ const data = await this._post('/v1/approvals', body, this._wsHeaders(token));
1125
+ return data.data || data;
1126
+ }
1127
+
1128
+ async getApproval(workspaceId, token, approvalId) {
1129
+ const params = new URLSearchParams({ network: workspaceId });
1130
+ const data = await this._get(`/v1/approvals/${encodeURIComponent(approvalId)}?${params}`, this._wsHeaders(token));
1131
+ return data.data || data;
1132
+ }
1133
+
1134
+ // ── Work brief (v1.1 M5) ──────────────────────────────────────────────────
1135
+
1136
+ /** Read a thread's shared work brief (GET /v1/channels/{channel}/brief).
1137
+ * Empty fields come back null / [] when nothing has been written yet. */
1138
+ async getBrief(workspaceId, token, channelName) {
1139
+ const params = new URLSearchParams({ network: workspaceId });
1140
+ const data = await this._get(
1141
+ `/v1/channels/${encodeURIComponent(channelName)}/brief?${params}`,
1142
+ this._wsHeaders(token),
1143
+ );
1144
+ return data.data || data;
1145
+ }
1146
+
1147
+ /** Partial upsert of a thread's brief (PUT /v1/channels/{channel}/brief).
1148
+ * Only the keys present in `fields` change; `source` is recorded as the
1149
+ * writer ("openagents:<agent>"). */
1150
+ async putBrief(workspaceId, token, channelName, fields = {}, { source } = {}) {
1151
+ const body = { network: workspaceId, source: source || 'openagents:unknown', ...fields };
1152
+ const data = await this._put(
1153
+ `/v1/channels/${encodeURIComponent(channelName)}/brief`,
1154
+ body,
1155
+ this._wsHeaders(token),
1156
+ );
1157
+ return data.data || data;
1158
+ }
1159
+
937
1160
  _wsHeaders(token) {
938
1161
  return {
939
1162
  'Content-Type': 'application/json',
@@ -942,6 +1165,7 @@ class WorkspaceClient {
942
1165
  }
943
1166
 
944
1167
  _get(urlPath, headers = {}, timeout = 15000) {
1168
+ headers = this._withAgent(headers);
945
1169
  const fullUrl = this.endpoint + urlPath;
946
1170
 
947
1171
  return new Promise((resolve, reject) => {
@@ -962,28 +1186,37 @@ class WorkspaceClient {
962
1186
  signal: AbortSignal.timeout(timeout),
963
1187
  }, (res) => {
964
1188
  let data = '';
1189
+ rejectOnTruncatedResponse(res, reject);
965
1190
  res.on('data', (chunk) => { data += chunk; });
966
1191
  res.on('end', () => {
967
1192
  try {
968
1193
  const parsed = JSON.parse(data);
969
1194
  if (res.statusCode >= 400) {
970
- reject(new Error(parsed.message || `HTTP ${res.statusCode}`));
1195
+ const err = new Error(parsed.message || `HTTP ${res.statusCode}`);
1196
+ // Lets the poll loop tell a rejected credential (401/403) and an
1197
+ // overloaded server (5xx) apart from a bug.
1198
+ err.status = res.statusCode;
1199
+ reject(err);
971
1200
  } else {
972
1201
  resolve(parsed);
973
1202
  }
974
1203
  } catch {
975
- reject(new Error(`Invalid response: ${data.slice(0, 200)}`));
1204
+ // A proxy's HTML error page (502/504) lands here.
1205
+ const err = new Error(`Invalid response: ${data.slice(0, 200)}`);
1206
+ if (res.statusCode >= 400) err.status = res.statusCode;
1207
+ reject(err);
976
1208
  }
977
1209
  });
978
1210
  });
979
1211
 
980
- req.on('error', reject);
981
- req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
1212
+ req.on('error', rejectWithDeadline(reject, timeout));
1213
+ req.on('timeout', () => { req.destroy(); reject(requestTimeoutError(timeout)); });
982
1214
  req.end();
983
1215
  });
984
1216
  }
985
1217
 
986
1218
  _getRaw(urlPath, headers = {}, timeout = 15000) {
1219
+ headers = this._withAgent(headers);
987
1220
  const fullUrl = this.endpoint + urlPath;
988
1221
 
989
1222
  return new Promise((resolve, reject) => {
@@ -997,6 +1230,7 @@ class WorkspaceClient {
997
1230
  signal: AbortSignal.timeout(timeout), // hard deadline (covers DNS/connect); see _get
998
1231
  }, (res) => {
999
1232
  const chunks = [];
1233
+ rejectOnTruncatedResponse(res, reject);
1000
1234
  res.on('data', (chunk) => { chunks.push(chunk); });
1001
1235
  res.on('end', () => {
1002
1236
  const buf = Buffer.concat(chunks);
@@ -1008,13 +1242,14 @@ class WorkspaceClient {
1008
1242
  });
1009
1243
  });
1010
1244
 
1011
- req.on('error', reject);
1012
- req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
1245
+ req.on('error', rejectWithDeadline(reject, timeout));
1246
+ req.on('timeout', () => { req.destroy(); reject(requestTimeoutError(timeout)); });
1013
1247
  req.end();
1014
1248
  });
1015
1249
  }
1016
1250
 
1017
1251
  _post(urlPath, body, headers = {}, timeout = 30000) {
1252
+ headers = this._withAgent(headers);
1018
1253
  if (!headers['Content-Type']) headers['Content-Type'] = 'application/json';
1019
1254
  const jsonBody = JSON.stringify(body);
1020
1255
  const fullUrl = this.endpoint + urlPath;
@@ -1030,6 +1265,7 @@ class WorkspaceClient {
1030
1265
  signal: AbortSignal.timeout(timeout), // hard deadline (covers DNS/connect); see _get
1031
1266
  }, (res) => {
1032
1267
  let data = '';
1268
+ rejectOnTruncatedResponse(res, reject);
1033
1269
  res.on('data', (chunk) => { data += chunk; });
1034
1270
  res.on('end', () => {
1035
1271
  try {
@@ -1058,14 +1294,15 @@ class WorkspaceClient {
1058
1294
  });
1059
1295
  });
1060
1296
 
1061
- req.on('error', reject);
1062
- req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
1297
+ req.on('error', rejectWithDeadline(reject, timeout));
1298
+ req.on('timeout', () => { req.destroy(); reject(requestTimeoutError(timeout)); });
1063
1299
  req.write(jsonBody);
1064
1300
  req.end();
1065
1301
  });
1066
1302
  }
1067
1303
 
1068
1304
  _put(urlPath, body, headers = {}, timeout = 30000) {
1305
+ headers = this._withAgent(headers);
1069
1306
  if (!headers['Content-Type']) headers['Content-Type'] = 'application/json';
1070
1307
  const jsonBody = JSON.stringify(body);
1071
1308
  const fullUrl = this.endpoint + urlPath;
@@ -1081,6 +1318,7 @@ class WorkspaceClient {
1081
1318
  signal: AbortSignal.timeout(timeout), // hard deadline (covers DNS/connect); see _get
1082
1319
  }, (res) => {
1083
1320
  let data = '';
1321
+ rejectOnTruncatedResponse(res, reject);
1084
1322
  res.on('data', (chunk) => { data += chunk; });
1085
1323
  res.on('end', () => {
1086
1324
  try {
@@ -1101,14 +1339,15 @@ class WorkspaceClient {
1101
1339
  });
1102
1340
  });
1103
1341
 
1104
- req.on('error', reject);
1105
- req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
1342
+ req.on('error', rejectWithDeadline(reject, timeout));
1343
+ req.on('timeout', () => { req.destroy(); reject(requestTimeoutError(timeout)); });
1106
1344
  req.write(jsonBody);
1107
1345
  req.end();
1108
1346
  });
1109
1347
  }
1110
1348
 
1111
1349
  _patch(urlPath, body, headers = {}, timeout = 15000) {
1350
+ headers = this._withAgent(headers);
1112
1351
  if (!headers['Content-Type']) headers['Content-Type'] = 'application/json';
1113
1352
  const jsonBody = JSON.stringify(body);
1114
1353
  const fullUrl = this.endpoint + urlPath;
@@ -1124,6 +1363,7 @@ class WorkspaceClient {
1124
1363
  signal: AbortSignal.timeout(timeout), // hard deadline (covers DNS/connect); see _get
1125
1364
  }, (res) => {
1126
1365
  let data = '';
1366
+ rejectOnTruncatedResponse(res, reject);
1127
1367
  res.on('data', (chunk) => { data += chunk; });
1128
1368
  res.on('end', () => {
1129
1369
  try {
@@ -1139,14 +1379,15 @@ class WorkspaceClient {
1139
1379
  });
1140
1380
  });
1141
1381
 
1142
- req.on('error', reject);
1143
- req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
1382
+ req.on('error', rejectWithDeadline(reject, timeout));
1383
+ req.on('timeout', () => { req.destroy(); reject(requestTimeoutError(timeout)); });
1144
1384
  req.write(jsonBody);
1145
1385
  req.end();
1146
1386
  });
1147
1387
  }
1148
1388
 
1149
1389
  _delete(urlPath, headers = {}, network) {
1390
+ headers = this._withAgent(headers);
1150
1391
  const fullUrl = this.endpoint + urlPath + (network ? `?network=${network}` : '');
1151
1392
 
1152
1393
  return new Promise((resolve, reject) => {
@@ -1160,6 +1401,7 @@ class WorkspaceClient {
1160
1401
  signal: AbortSignal.timeout(15000), // hard deadline (covers DNS/connect); see _get
1161
1402
  }, (res) => {
1162
1403
  let data = '';
1404
+ rejectOnTruncatedResponse(res, reject);
1163
1405
  res.on('data', (chunk) => { data += chunk; });
1164
1406
  res.on('end', () => {
1165
1407
  try {
@@ -1175,8 +1417,8 @@ class WorkspaceClient {
1175
1417
  });
1176
1418
  });
1177
1419
 
1178
- req.on('error', reject);
1179
- req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
1420
+ req.on('error', rejectWithDeadline(reject, 15000));
1421
+ req.on('timeout', () => { req.destroy(); reject(requestTimeoutError(15000)); });
1180
1422
  req.end();
1181
1423
  });
1182
1424
  }