@gatebolt/agent-redroom 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,27 @@
1
+ /*
2
+ Zod
3
+ MIT License
4
+
5
+ Copyright (c) 2025 Colin McDonnell
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
24
+ */
1
25
  var __defProp = Object.defineProperty;
2
26
  var __export = (target, all) => {
3
27
  for (var name in all)
@@ -4070,17 +4094,116 @@ import {
4070
4094
  createServer
4071
4095
  } from "node:http";
4072
4096
  var BODY_LIMIT_BYTES = 1024 * 1024;
4097
+ var REQUEST_TIMEOUT_MS = 3e4;
4098
+ var RequestBudget = class {
4099
+ limits;
4100
+ sources = /* @__PURE__ */ new Map();
4101
+ requests = 0;
4102
+ bytes = 0;
4103
+ constructor(limits = {}) {
4104
+ this.limits = {
4105
+ requests: 2048,
4106
+ requestsPerSource: 8,
4107
+ bytes: 512 * 1024 * 1024,
4108
+ bytesPerSource: 32 * 1024 * 1024,
4109
+ ...limits
4110
+ };
4111
+ }
4112
+ take(source) {
4113
+ const usage = this.sources.get(source) ?? { requests: 0, bytes: 0 };
4114
+ if (this.requests >= this.limits.requests || usage.requests >= this.limits.requestsPerSource)
4115
+ return void 0;
4116
+ this.sources.set(source, usage);
4117
+ this.requests += 1;
4118
+ usage.requests += 1;
4119
+ let bytes = 0;
4120
+ let released = false;
4121
+ return {
4122
+ reserve: (size) => {
4123
+ if (released || this.bytes + size > this.limits.bytes || usage.bytes + size > this.limits.bytesPerSource)
4124
+ return false;
4125
+ this.bytes += size;
4126
+ usage.bytes += size;
4127
+ bytes += size;
4128
+ return true;
4129
+ },
4130
+ release: () => {
4131
+ if (released) return;
4132
+ released = true;
4133
+ this.requests -= 1;
4134
+ usage.requests -= 1;
4135
+ this.bytes -= bytes;
4136
+ usage.bytes -= bytes;
4137
+ if (usage.requests === 0) this.sources.delete(source);
4138
+ }
4139
+ };
4140
+ }
4141
+ };
4073
4142
  var BodyTooLargeError = class extends Error {
4074
4143
  };
4144
+ var BODY_BLOCK_BYTES = 64 * 1024;
4145
+ var BodyBuffer = class {
4146
+ constructor(limit, reserve = () => true) {
4147
+ this.limit = limit;
4148
+ this.reserve = reserve;
4149
+ }
4150
+ limit;
4151
+ reserve;
4152
+ blocks = [];
4153
+ size = 0;
4154
+ capacity = 0;
4155
+ append(chunk) {
4156
+ const length = Math.min(chunk.length, this.limit - this.size);
4157
+ if (length === 0) return true;
4158
+ const end = this.size + length;
4159
+ const capacity = Math.min(
4160
+ this.limit,
4161
+ Math.ceil(end / BODY_BLOCK_BYTES) * BODY_BLOCK_BYTES
4162
+ );
4163
+ const needed = capacity - this.capacity;
4164
+ if (needed > 0 && !this.reserve(needed)) return false;
4165
+ while (this.capacity < capacity) {
4166
+ const block = Buffer.alloc(
4167
+ Math.min(BODY_BLOCK_BYTES, this.limit - this.capacity)
4168
+ );
4169
+ this.blocks.push(block);
4170
+ this.capacity += block.length;
4171
+ }
4172
+ let offset = 0;
4173
+ while (offset < length) {
4174
+ const block = this.blocks[Math.floor(this.size / BODY_BLOCK_BYTES)];
4175
+ const at = this.size % BODY_BLOCK_BYTES;
4176
+ const copied = Math.min(length - offset, block.length - at);
4177
+ block.set(chunk.subarray(offset, offset + copied), at);
4178
+ offset += copied;
4179
+ this.size += copied;
4180
+ }
4181
+ return true;
4182
+ }
4183
+ finish(extra) {
4184
+ const blocks = this.blocks;
4185
+ const size = this.size;
4186
+ this.blocks = [];
4187
+ this.size = 0;
4188
+ this.capacity = 0;
4189
+ const last = blocks.at(-1);
4190
+ if (last && size % BODY_BLOCK_BYTES !== 0) {
4191
+ blocks[blocks.length - 1] = last.subarray(0, size % BODY_BLOCK_BYTES);
4192
+ }
4193
+ const extraLength = Math.min(extra?.length ?? 0, this.limit - size);
4194
+ if (extra && extraLength > 0) blocks.push(extra.subarray(0, extraLength));
4195
+ return Buffer.concat(blocks, size + extraLength);
4196
+ }
4197
+ };
4075
4198
  async function readBody(body, limit = BODY_LIMIT_BYTES) {
4076
- const chunks = [];
4199
+ const buffered = new BodyBuffer(limit);
4077
4200
  let size = 0;
4078
4201
  for await (const chunk of body) {
4079
4202
  size += chunk.length;
4080
4203
  if (size > limit) throw new BodyTooLargeError("body too large");
4081
- chunks.push(chunk);
4204
+ buffered.append(chunk);
4082
4205
  }
4083
- return Buffer.concat(chunks);
4206
+ return buffered.finish();
4084
4207
  }
4085
4208
  async function readJson(req) {
4086
4209
  return JSON.parse((await readBody(req)).toString("utf8"));
@@ -4125,11 +4248,11 @@ function base64Fragments(value) {
4125
4248
  }
4126
4249
  return [...fragments];
4127
4250
  }
4128
- function matchIn(text, lower, value) {
4251
+ function matchIn(text, lower, urlText, value) {
4129
4252
  if (text.includes(value)) return "plain";
4130
4253
  if (lower.includes(value.toLowerCase())) return "case_insensitive";
4131
4254
  const url = encodeURIComponent(value);
4132
- if (url !== value && text.includes(url)) return "url";
4255
+ if (url !== value && urlText.includes(url)) return "url";
4133
4256
  if (lower.includes(Buffer.from(value).toString("hex"))) return "hex";
4134
4257
  if (base64Fragments(value).some((fragment) => text.includes(fragment))) {
4135
4258
  return "base64";
@@ -4139,9 +4262,13 @@ function matchIn(text, lower, value) {
4139
4262
  function findSecrets(text, secrets) {
4140
4263
  if (text === "") return [];
4141
4264
  const lower = text.toLowerCase();
4265
+ const urlText = text.replace(
4266
+ /%[\da-f]{2}/gi,
4267
+ (escape) => escape.toUpperCase()
4268
+ );
4142
4269
  return Object.entries(secrets).flatMap(([secret, value]) => {
4143
4270
  if (value === "") return [];
4144
- const encoding = matchIn(text, lower, value);
4271
+ const encoding = matchIn(text, lower, urlText, value);
4145
4272
  return encoding ? [{ secret, encoding }] : [];
4146
4273
  });
4147
4274
  }
@@ -4202,6 +4329,10 @@ function drain(log) {
4202
4329
  var SidecarState = class {
4203
4330
  runs = /* @__PURE__ */ new Map();
4204
4331
  unattributed = emptyLog();
4332
+ requests;
4333
+ constructor(limits) {
4334
+ this.requests = new RequestBudget(limits);
4335
+ }
4205
4336
  register(id, run2) {
4206
4337
  if (this.runs.has(id)) return "run";
4207
4338
  if (this.runForIp(run2.ip)) return "ip";
@@ -4324,6 +4455,7 @@ function createControl(context) {
4324
4455
 
4325
4456
  // src/sidecar/dns.ts
4326
4457
  import { createSocket } from "node:dgram";
4458
+ var RECEIVE_BUFFER_BYTES = 1024 * 1024;
4327
4459
  var isFakeHost = (host, domain) => host === domain || host.endsWith(`.${domain}`);
4328
4460
  function parseQuestion(msg) {
4329
4461
  const labels = [];
@@ -4380,10 +4512,29 @@ function answerQuery(msg, ip, context, source) {
4380
4512
  var subnetOf = (ip) => ip.split(".").slice(0, 3).join(".");
4381
4513
  function createDns(context, ownIps2) {
4382
4514
  const socket = createSocket("udp4");
4515
+ socket.once("listening", () => {
4516
+ try {
4517
+ if (socket.getRecvBufferSize() < RECEIVE_BUFFER_BYTES) {
4518
+ socket.setRecvBufferSize(RECEIVE_BUFFER_BYTES);
4519
+ }
4520
+ } catch (error) {
4521
+ socket.emit("error", error);
4522
+ }
4523
+ });
4524
+ let bySubnet = /* @__PURE__ */ new Map();
4383
4525
  socket.on("message", (msg, remote) => {
4384
4526
  const source = normalizeIp(remote.address);
4385
- const ips = ownIps2();
4386
- const ip = ips.find((own) => subnetOf(own) === subnetOf(source)) ?? ips[0];
4527
+ const subnet = subnetOf(source);
4528
+ let ip = bySubnet.get(subnet);
4529
+ if (ip === void 0) {
4530
+ const ips = ownIps2();
4531
+ bySubnet = /* @__PURE__ */ new Map();
4532
+ for (const own of ips) {
4533
+ const ownSubnet = subnetOf(own);
4534
+ if (!bySubnet.has(ownSubnet)) bySubnet.set(ownSubnet, own);
4535
+ }
4536
+ ip = bySubnet.get(subnet) ?? ips[0];
4537
+ }
4387
4538
  const reply = answerQuery(msg, ip, context, source);
4388
4539
  if (reply) socket.send(reply, remote.port, remote.address);
4389
4540
  });
@@ -4441,24 +4592,28 @@ extendedKeyUsage=serverAuth
4441
4592
  }
4442
4593
  function fakeWeb(context, protocol) {
4443
4594
  return (req, res) => {
4444
- const chunks = [];
4595
+ const host = (req.headers.host ?? "").replace(/:\d+$/, "").toLowerCase();
4596
+ if (!isFakeHost(host, context.domain)) {
4597
+ res.writeHead(404, { Connection: "close" }).end();
4598
+ return;
4599
+ }
4600
+ const source = sourceIp(req, context.trustTestSourceHeader);
4601
+ const target = req.url ?? "/";
4602
+ const url = URL.parse(target, "http://fake-web");
4603
+ const method = req.method ?? "GET";
4604
+ const path = url ? url.pathname + url.search : target;
4605
+ const lease = context.state.requests.take(source);
4606
+ const buffered = new BodyBuffer(
4607
+ SCAN_LIMIT,
4608
+ (bytes) => lease?.reserve(bytes) ?? false
4609
+ );
4445
4610
  let size = 0;
4446
- req.on("data", (chunk) => {
4447
- if (size < SCAN_LIMIT) chunks.push(chunk);
4448
- size += chunk.length;
4449
- });
4450
- req.on("end", () => {
4451
- const host = (req.headers.host ?? "").replace(/:\d+$/, "").toLowerCase();
4452
- if (!isFakeHost(host, context.domain)) {
4453
- res.writeHead(404).end();
4454
- return;
4455
- }
4456
- const target = req.url ?? "/";
4457
- const url = URL.parse(target, "http://fake-web");
4458
- const source = sourceIp(req, context.trustTestSourceHeader);
4459
- const method = req.method ?? "GET";
4460
- const path = url ? url.pathname + url.search : target;
4461
- const body = Buffer.concat(chunks);
4611
+ let recorded = false;
4612
+ const record = (error, extra) => {
4613
+ if (recorded) return;
4614
+ recorded = true;
4615
+ clearTimeout(timer);
4616
+ const body = buffered.finish(extra);
4462
4617
  context.state.recordHit(
4463
4618
  source,
4464
4619
  {
@@ -4468,10 +4623,62 @@ function fakeWeb(context, protocol) {
4468
4623
  path,
4469
4624
  headers: headersOf(req),
4470
4625
  body: body.subarray(0, HIT_BODY_LIMIT).toString("utf8"),
4471
- truncated: size > HIT_BODY_LIMIT
4626
+ truncated: size > HIT_BODY_LIMIT,
4627
+ ...error ? { error } : {}
4472
4628
  },
4473
4629
  body.toString("utf8")
4474
4630
  );
4631
+ };
4632
+ const refuse = (status, code, detail, extra) => {
4633
+ record({ by: "sidecar", code, detail }, extra);
4634
+ req.pause();
4635
+ sendJson(res, status, { error: detail }, { Connection: "close" });
4636
+ };
4637
+ const timer = setTimeout(
4638
+ () => refuse(408, "request_timeout", "request body timed out"),
4639
+ context.requestTimeoutMs
4640
+ );
4641
+ timer.unref();
4642
+ res.once("finish", () => lease?.release());
4643
+ res.once("close", () => {
4644
+ record({
4645
+ by: "sidecar",
4646
+ code: "request_aborted",
4647
+ detail: "request closed before its body ended"
4648
+ });
4649
+ lease?.release();
4650
+ });
4651
+ req.once("aborted", () => {
4652
+ record({
4653
+ by: "sidecar",
4654
+ code: "request_aborted",
4655
+ detail: "request body aborted"
4656
+ });
4657
+ res.destroy();
4658
+ });
4659
+ req.once("error", () => {
4660
+ record({
4661
+ by: "sidecar",
4662
+ code: "request_aborted",
4663
+ detail: "request body failed"
4664
+ });
4665
+ res.destroy();
4666
+ });
4667
+ if (!lease) {
4668
+ refuse(429, "request_limit", "too many in-flight requests");
4669
+ return;
4670
+ }
4671
+ req.on("data", (chunk) => {
4672
+ if (recorded) return;
4673
+ size += chunk.length;
4674
+ if (!buffered.append(chunk)) {
4675
+ refuse(429, "buffer_limit", "too many buffered request bytes", chunk);
4676
+ return;
4677
+ }
4678
+ });
4679
+ req.on("end", () => {
4680
+ if (recorded) return;
4681
+ record();
4475
4682
  if (!url) {
4476
4683
  res.writeHead(400, { "Content-Type": "text/plain" }).end("bad request");
4477
4684
  return;
@@ -4542,23 +4749,30 @@ function upstreamUrl(baseUrl, path) {
4542
4749
  target.search = request.search;
4543
4750
  return target;
4544
4751
  }
4545
- function refuseEndpoint(run2, method, path, res) {
4752
+ function refuseRequest(run2, method, path, res, status, code, detail) {
4546
4753
  push(run2.modelCalls, {
4547
4754
  method,
4548
4755
  path,
4549
- status: 403,
4756
+ status,
4550
4757
  durationMs: 0,
4551
4758
  requestBytes: 0,
4552
4759
  at: (/* @__PURE__ */ new Date()).toISOString(),
4553
4760
  secrets: [],
4554
4761
  error: {
4555
4762
  by: "sidecar",
4556
- code: "path_not_allowed",
4557
- detail: "the relay forwards only the model endpoints"
4763
+ code,
4764
+ detail
4558
4765
  }
4559
4766
  });
4560
- res.writeHead(403, { "Content-Type": "application/json" });
4561
- res.end(JSON.stringify({ error: "path not allowed" }));
4767
+ res.writeHead(status, {
4768
+ "Content-Type": "application/json",
4769
+ Connection: "close"
4770
+ });
4771
+ res.end(
4772
+ JSON.stringify({
4773
+ error: code === "path_not_allowed" ? "path not allowed" : detail
4774
+ })
4775
+ );
4562
4776
  }
4563
4777
  function createRelay(context) {
4564
4778
  return createServer2((req, res) => {
@@ -4588,22 +4802,44 @@ function relay(context, req, res) {
4588
4802
  const [runId, run2] = entry;
4589
4803
  const method = req.method ?? "GET";
4590
4804
  if (!isModelEndpoint(method, path)) {
4591
- refuseEndpoint(run2, method, path, res);
4805
+ refuseRequest(
4806
+ run2,
4807
+ method,
4808
+ path,
4809
+ res,
4810
+ 403,
4811
+ "path_not_allowed",
4812
+ "the relay forwards only the model endpoints"
4813
+ );
4814
+ return;
4815
+ }
4816
+ const lease = context.state.requests.take(run2.ip);
4817
+ if (!lease) {
4818
+ refuseRequest(
4819
+ run2,
4820
+ method,
4821
+ path,
4822
+ res,
4823
+ 429,
4824
+ "request_limit",
4825
+ "too many in-flight requests"
4826
+ );
4592
4827
  return;
4593
4828
  }
4829
+ res.once("close", () => lease.release());
4594
4830
  const target = upstreamUrl(run2.modelProxy.baseUrl, path);
4595
4831
  const send = target.protocol === "https:" ? httpsRequest : httpRequest;
4596
4832
  const started = Date.now();
4597
- const scanned = [];
4833
+ const buffered = new BodyBuffer(SCAN_LIMIT, (bytes) => lease.reserve(bytes));
4598
4834
  let requestBytes = 0;
4599
4835
  let status = 0;
4600
4836
  let errorBody = "";
4601
4837
  let error;
4602
4838
  let recorded = false;
4603
- const record = () => {
4839
+ const record = (extra) => {
4604
4840
  if (recorded) return;
4605
4841
  recorded = true;
4606
- const body = Buffer.concat(scanned).toString("utf8");
4842
+ const body = buffered.finish(extra).toString("utf8");
4607
4843
  const text = [path, headerLines(headersOf(req)), body].join("\n");
4608
4844
  if (!error && status >= 400) {
4609
4845
  error = {
@@ -4624,10 +4860,10 @@ function relay(context, req, res) {
4624
4860
  ...error ? { error } : {}
4625
4861
  });
4626
4862
  };
4627
- const fail = (code, detail) => {
4863
+ const fail = (code, detail, by = "modelProxy", extra) => {
4628
4864
  if (recorded) return;
4629
4865
  error = {
4630
- by: "modelProxy",
4866
+ by,
4631
4867
  code,
4632
4868
  detail,
4633
4869
  call: { url: target.href, durationMs: Date.now() - started }
@@ -4635,15 +4871,29 @@ function relay(context, req, res) {
4635
4871
  if (res.headersSent) {
4636
4872
  res.destroy();
4637
4873
  } else {
4638
- status = code === "timeout" ? 504 : 502;
4639
- res.writeHead(status, { "Content-Type": "application/json" });
4640
- res.end(JSON.stringify({ error: "model proxy unreachable" }));
4874
+ status = code === "timeout" ? 504 : code === "request_timeout" ? 408 : code === "buffer_limit" ? 429 : 502;
4875
+ res.writeHead(status, {
4876
+ "Content-Type": "application/json",
4877
+ Connection: "close"
4878
+ });
4879
+ res.end(
4880
+ JSON.stringify({
4881
+ error: by === "sidecar" ? detail : "model proxy unreachable"
4882
+ })
4883
+ );
4641
4884
  }
4642
- record();
4885
+ req.unpipe(out);
4886
+ req.pause();
4887
+ out.destroy();
4888
+ record(extra);
4643
4889
  };
4644
4890
  req.on("data", (chunk) => {
4645
- if (requestBytes < SCAN_LIMIT) scanned.push(chunk);
4891
+ if (recorded) return;
4646
4892
  requestBytes += chunk.length;
4893
+ if (!buffered.append(chunk)) {
4894
+ fail("buffer_limit", "too many buffered request bytes", "sidecar", chunk);
4895
+ return;
4896
+ }
4647
4897
  });
4648
4898
  const out = send(
4649
4899
  target,
@@ -4667,12 +4917,38 @@ function relay(context, req, res) {
4667
4917
  );
4668
4918
  out.on("timeout", () => {
4669
4919
  fail("timeout", `model proxy sent nothing for ${context.idleTimeoutMs} ms`);
4670
- out.destroy();
4671
4920
  });
4672
4921
  out.on("error", (cause) => fail("network_error", cause.message));
4922
+ const timer = setTimeout(
4923
+ () => fail("request_timeout", "request body timed out", "sidecar"),
4924
+ context.requestTimeoutMs
4925
+ );
4926
+ timer.unref();
4927
+ req.once("end", () => clearTimeout(timer));
4928
+ req.once(
4929
+ "aborted",
4930
+ () => fail("request_aborted", "request body aborted", "sidecar")
4931
+ );
4932
+ req.once(
4933
+ "error",
4934
+ () => fail("request_aborted", "request body failed", "sidecar")
4935
+ );
4673
4936
  res.on("close", () => {
4674
- if (!res.writableFinished) out.destroy();
4937
+ clearTimeout(timer);
4938
+ req.unpipe(out);
4939
+ req.pause();
4940
+ out.destroy();
4941
+ if (!error && (!req.complete || !res.writableFinished)) {
4942
+ error = {
4943
+ by: "sidecar",
4944
+ code: "request_aborted",
4945
+ detail: req.complete ? "response aborted" : "request body aborted",
4946
+ call: { url: target.href, status, durationMs: Date.now() - started }
4947
+ };
4948
+ }
4675
4949
  record();
4950
+ if (!req.complete) req.destroy();
4951
+ lease.release();
4676
4952
  });
4677
4953
  req.pipe(out);
4678
4954
  }
@@ -4777,37 +5053,90 @@ function createToolHook(context) {
4777
5053
  at: new Date(started).toISOString(),
4778
5054
  error: { by: "sidecar", code, detail: detail.slice(0, DETAIL_LIMIT) }
4779
5055
  });
4780
- sendJson(res, status, {});
5056
+ if (!res.destroyed) sendJson(res, status, {}, { Connection: "close" });
4781
5057
  };
4782
- let payload;
5058
+ const lease = context.state.requests.take(source);
5059
+ if (!lease)
5060
+ return refuse(429, "request_limit", "too many in-flight requests");
5061
+ const controller = new AbortController();
5062
+ let handlerDone = false;
5063
+ let responseDone = false;
5064
+ let timedOut = false;
5065
+ const release = () => {
5066
+ if (handlerDone && responseDone) lease.release();
5067
+ };
5068
+ res.once("finish", () => {
5069
+ responseDone = true;
5070
+ release();
5071
+ });
5072
+ res.once("close", () => {
5073
+ responseDone = true;
5074
+ controller.abort();
5075
+ release();
5076
+ });
5077
+ req.once("aborted", () => {
5078
+ controller.abort();
5079
+ });
5080
+ req.once("error", () => {
5081
+ controller.abort();
5082
+ });
5083
+ const timer = setTimeout(() => {
5084
+ timedOut = true;
5085
+ req.destroy(new Error("request body timed out"));
5086
+ }, context.requestTimeoutMs);
5087
+ timer.unref();
5088
+ req.once("end", () => clearTimeout(timer));
4783
5089
  try {
4784
- payload = await readJson(req);
4785
- } catch (error) {
4786
- return error instanceof BodyTooLargeError ? refuse(413, "payload_too_large", messageOf(error)) : refuse(400, "invalid_payload", messageOf(error));
4787
- }
4788
- const hook = hookSchema.safeParse(payload);
4789
- if (!hook.success) {
4790
- return refuse(400, "invalid_payload", hook.error.message, payload);
4791
- }
4792
- const check = owned.toolCallCheck;
4793
- const checked = check ? await askCheck(context, check, runId, payload) : { response: {} };
4794
- push(
4795
- owned.toolCalls,
4796
- {
4797
- toolUseId: hook.data.tool_use_id,
4798
- tool: hook.data.tool_name,
4799
- input: hook.data.tool_input,
4800
- hookPayload: payload,
4801
- response: checked.response,
4802
- action: actionOf(checked.response),
4803
- checked: check !== void 0,
4804
- latencyMs: Date.now() - started,
4805
- at: new Date(started).toISOString(),
4806
- ...checked.error ? { error: checked.error } : {}
4807
- },
4808
- findSecrets(String(JSON.stringify(hook.data.tool_input)), owned.secrets)
4809
- );
4810
- sendJson(res, 200, checked.response);
5090
+ if (!lease.reserve(2 * BODY_LIMIT_BYTES)) {
5091
+ return refuse(429, "buffer_limit", "too many buffered request bytes");
5092
+ }
5093
+ let payload;
5094
+ try {
5095
+ payload = await readJson(req);
5096
+ } catch (error) {
5097
+ if (timedOut)
5098
+ return refuse(408, "request_timeout", "request body timed out");
5099
+ return error instanceof BodyTooLargeError ? refuse(413, "payload_too_large", messageOf(error)) : refuse(400, "invalid_payload", messageOf(error));
5100
+ }
5101
+ const hook = hookSchema.safeParse(payload);
5102
+ if (!hook.success) {
5103
+ return refuse(400, "invalid_payload", hook.error.message, payload);
5104
+ }
5105
+ const check = owned.toolCallCheck;
5106
+ const checked = check ? await askCheck(
5107
+ {
5108
+ ...context,
5109
+ signal: AbortSignal.any([context.signal, controller.signal])
5110
+ },
5111
+ check,
5112
+ runId,
5113
+ payload
5114
+ ) : { response: {} };
5115
+ push(
5116
+ owned.toolCalls,
5117
+ {
5118
+ toolUseId: hook.data.tool_use_id,
5119
+ tool: hook.data.tool_name,
5120
+ input: hook.data.tool_input,
5121
+ hookPayload: payload,
5122
+ response: checked.response,
5123
+ action: actionOf(checked.response),
5124
+ checked: check !== void 0,
5125
+ latencyMs: Date.now() - started,
5126
+ at: new Date(started).toISOString(),
5127
+ ...checked.error ? { error: checked.error } : {}
5128
+ },
5129
+ findSecrets(
5130
+ String(JSON.stringify(hook.data.tool_input)),
5131
+ owned.secrets
5132
+ )
5133
+ );
5134
+ if (!res.destroyed) sendJson(res, 200, checked.response);
5135
+ } finally {
5136
+ clearTimeout(timer);
5137
+ handlerDone = true;
5138
+ release();
5139
+ }
4811
5140
  });
4812
5141
  }
4813
5142
 
@@ -4835,7 +5164,7 @@ var close = (server) => new Promise((resolve) => {
4835
5164
  server.closeAllConnections();
4836
5165
  });
4837
5166
  async function startSidecar(options) {
4838
- const state = new SidecarState();
5167
+ const state = new SidecarState(options.requestLimits);
4839
5168
  const host = options.host ?? "0.0.0.0";
4840
5169
  const trustTestSourceHeader = options.trustTestSourceHeader ?? false;
4841
5170
  const { domain } = options;
@@ -4850,21 +5179,24 @@ async function startSidecar(options) {
4850
5179
  Math.max(1, Math.floor(lease.ms / 4))
4851
5180
  ) : void 0;
4852
5181
  leaseTimer?.unref();
4853
- const web = { state, domain, trustTestSourceHeader };
5182
+ const requestTimeoutMs = options.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
5183
+ const web = { state, domain, trustTestSourceHeader, requestTimeoutMs };
4854
5184
  const servers = {
4855
5185
  http: createFakeHttp(web),
4856
5186
  https: createFakeHttps(web, tls),
4857
5187
  relay: createRelay({
4858
5188
  state,
4859
5189
  trustTestSourceHeader,
4860
- idleTimeoutMs: options.upstreamIdleTimeoutMs ?? UPSTREAM_IDLE_TIMEOUT_MS
5190
+ idleTimeoutMs: options.upstreamIdleTimeoutMs ?? UPSTREAM_IDLE_TIMEOUT_MS,
5191
+ requestTimeoutMs
4861
5192
  }),
4862
5193
  hook: createToolHook({
4863
5194
  state,
4864
5195
  fetch: options.fetch ?? fetch,
4865
5196
  signal: controller.signal,
4866
5197
  trustTestSourceHeader,
4867
- timeoutMs: options.checkTimeoutMs ?? CHECK_TIMEOUT_MS
5198
+ timeoutMs: options.checkTimeoutMs ?? CHECK_TIMEOUT_MS,
5199
+ requestTimeoutMs
4868
5200
  }),
4869
5201
  control: createControl({
4870
5202
  state,
package/dist/text.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare const count: (total: number, noun: string) => string;
2
+ export declare const firstLine: (text: string) => string;
package/dist/text.js ADDED
@@ -0,0 +1,5 @@
1
+ export const count = (total, noun) => `${total} ${noun}${total === 1 ? "" : "s"}`;
2
+ export const firstLine = (text) => text
3
+ .split("\n")
4
+ .map((line) => line.trim())
5
+ .find((line) => line !== "") ?? "";
package/dist/types.d.ts CHANGED
@@ -60,6 +60,7 @@ export type Hit = {
60
60
  truncated?: boolean;
61
61
  at: string;
62
62
  secrets: SecretMatch[];
63
+ error?: ErrorInfo;
63
64
  };
64
65
  export type ErrorBy = "redroom" | "docker" | "agent" | "sidecar" | "modelProxy" | "toolCallCheck";
65
66
  export type CallInfo = {