@gatebolt/agent-redroom 0.1.0 → 0.1.2

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,5 +1,6 @@
1
1
  import { type SidecarPorts } from "./config.js";
2
2
  import { type Ips } from "./dns.js";
3
+ import { type RequestLimits } from "./http.js";
3
4
  import { SidecarState } from "./state.js";
4
5
  export type SidecarOptions = {
5
6
  controlSecret: string;
@@ -11,6 +12,8 @@ export type SidecarOptions = {
11
12
  fetch?: typeof fetch;
12
13
  checkTimeoutMs?: number;
13
14
  upstreamIdleTimeoutMs?: number;
15
+ requestLimits?: Partial<RequestLimits>;
16
+ requestTimeoutMs?: number;
14
17
  lease?: {
15
18
  ms: number;
16
19
  onExpired: () => void;
@@ -5,6 +5,7 @@ import {} from "./config.js";
5
5
  import { createControl } from "./control.js";
6
6
  import { createDns } from "./dns.js";
7
7
  import { createFakeHttp, createFakeHttps, generateTls } from "./fake-web.js";
8
+ import { REQUEST_TIMEOUT_MS } from "./http.js";
8
9
  import { createRelay, UPSTREAM_IDLE_TIMEOUT_MS } from "./relay.js";
9
10
  import { SidecarState } from "./state.js";
10
11
  import { CHECK_TIMEOUT_MS, createToolHook } from "./tool-hook.js";
@@ -34,7 +35,7 @@ const close = (server) => new Promise((resolve) => {
34
35
  server.closeAllConnections();
35
36
  });
36
37
  export async function startSidecar(options) {
37
- const state = new SidecarState();
38
+ const state = new SidecarState(options.requestLimits);
38
39
  const host = options.host ?? "0.0.0.0";
39
40
  const trustTestSourceHeader = options.trustTestSourceHeader ?? false;
40
41
  const { domain } = options;
@@ -49,7 +50,8 @@ export async function startSidecar(options) {
49
50
  }, Math.max(1, Math.floor(lease.ms / 4)))
50
51
  : undefined;
51
52
  leaseTimer?.unref();
52
- const web = { state, domain, trustTestSourceHeader };
53
+ const requestTimeoutMs = options.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
54
+ const web = { state, domain, trustTestSourceHeader, requestTimeoutMs };
53
55
  const servers = {
54
56
  http: createFakeHttp(web),
55
57
  https: createFakeHttps(web, tls),
@@ -57,6 +59,7 @@ export async function startSidecar(options) {
57
59
  state,
58
60
  trustTestSourceHeader,
59
61
  idleTimeoutMs: options.upstreamIdleTimeoutMs ?? UPSTREAM_IDLE_TIMEOUT_MS,
62
+ requestTimeoutMs,
60
63
  }),
61
64
  hook: createToolHook({
62
65
  state,
@@ -64,6 +67,7 @@ export async function startSidecar(options) {
64
67
  signal: controller.signal,
65
68
  trustTestSourceHeader,
66
69
  timeoutMs: options.checkTimeoutMs ?? CHECK_TIMEOUT_MS,
70
+ requestTimeoutMs,
67
71
  }),
68
72
  control: createControl({
69
73
  state,
@@ -1,4 +1,5 @@
1
1
  import { type IncomingMessage } from "node:http";
2
+ import { RequestBudget, type RequestLimits } from "./http.js";
2
3
  import { type Hit, type ModelCall, type ModelProxy, type SecretMatch, type Secrets, type ToolCall, type ToolCallCheck } from "../types.js";
3
4
  export declare const MAX_ENTRIES = 10000;
4
5
  export declare const MAX_LOG_BYTES: number;
@@ -37,6 +38,8 @@ export declare function drain<T>(log: Log<T>): Log<T>;
37
38
  export declare class SidecarState {
38
39
  readonly runs: Map<string, RunState>;
39
40
  readonly unattributed: Log<Hit>;
41
+ readonly requests: RequestBudget;
42
+ constructor(limits?: Partial<RequestLimits>);
40
43
  register(id: string, run: Pick<RunState, "ip" | "pages" | "secrets" | "modelProxy" | "toolCallCheck">): "ok" | "run" | "ip";
41
44
  runEntryForIp(ip: string): [string, RunState] | undefined;
42
45
  runForIp(ip: string): RunState | undefined;
@@ -1,5 +1,6 @@
1
1
  import {} from "node:http";
2
2
  import { findSecrets } from "../secrets.js";
3
+ import { RequestBudget } from "./http.js";
3
4
  import {} from "../types.js";
4
5
  export const MAX_ENTRIES = 10_000;
5
6
  export const MAX_LOG_BYTES = 4 * 1024 * 1024;
@@ -61,6 +62,10 @@ export function drain(log) {
61
62
  export class SidecarState {
62
63
  runs = new Map();
63
64
  unattributed = emptyLog();
65
+ requests;
66
+ constructor(limits) {
67
+ this.requests = new RequestBudget(limits);
68
+ }
64
69
  register(id, run) {
65
70
  if (this.runs.has(id))
66
71
  return "run";
@@ -9,5 +9,6 @@ export type ToolHookContext = {
9
9
  signal: AbortSignal;
10
10
  trustTestSourceHeader: boolean;
11
11
  timeoutMs: number;
12
+ requestTimeoutMs: number;
12
13
  };
13
14
  export declare function createToolHook(context: ToolHookContext): Server;
@@ -2,7 +2,7 @@ import {} from "node:http";
2
2
  import { z } from "zod";
3
3
  import { findSecrets } from "../secrets.js";
4
4
  import { DETAIL_LIMIT, isRecord, messageOf, } from "../types.js";
5
- import { BodyTooLargeError, jsonServer, readBody, readJson, sendJson, } from "./http.js";
5
+ import { BodyTooLargeError, BODY_LIMIT_BYTES, jsonServer, readBody, readJson, sendJson, } from "./http.js";
6
6
  import { push, sourceIp } from "./state.js";
7
7
  export const CHECK_TIMEOUT_MS = 25_000;
8
8
  const hookSchema = z.object({
@@ -111,37 +111,86 @@ export function createToolHook(context) {
111
111
  at: new Date(started).toISOString(),
112
112
  error: { by: "sidecar", code, detail: detail.slice(0, DETAIL_LIMIT) },
113
113
  });
114
- sendJson(res, status, {});
114
+ if (!res.destroyed)
115
+ sendJson(res, status, {}, { Connection: "close" });
115
116
  };
116
- let payload;
117
+ const lease = context.state.requests.take(source);
118
+ if (!lease)
119
+ return refuse(429, "request_limit", "too many in-flight requests");
120
+ const controller = new AbortController();
121
+ let handlerDone = false;
122
+ let responseDone = false;
123
+ let timedOut = false;
124
+ const release = () => {
125
+ if (handlerDone && responseDone)
126
+ lease.release();
127
+ };
128
+ res.once("finish", () => {
129
+ responseDone = true;
130
+ release();
131
+ });
132
+ res.once("close", () => {
133
+ responseDone = true;
134
+ controller.abort();
135
+ release();
136
+ });
137
+ req.once("aborted", () => {
138
+ controller.abort();
139
+ });
140
+ req.once("error", () => {
141
+ controller.abort();
142
+ });
143
+ const timer = setTimeout(() => {
144
+ timedOut = true;
145
+ req.destroy(new Error("request body timed out"));
146
+ }, context.requestTimeoutMs);
147
+ timer.unref();
148
+ req.once("end", () => clearTimeout(timer));
117
149
  try {
118
- payload = await readJson(req);
119
- }
120
- catch (error) {
121
- return error instanceof BodyTooLargeError
122
- ? refuse(413, "payload_too_large", messageOf(error))
123
- : refuse(400, "invalid_payload", messageOf(error));
150
+ if (!lease.reserve(2 * BODY_LIMIT_BYTES)) {
151
+ return refuse(429, "buffer_limit", "too many buffered request bytes");
152
+ }
153
+ let payload;
154
+ try {
155
+ payload = await readJson(req);
156
+ }
157
+ catch (error) {
158
+ if (timedOut)
159
+ return refuse(408, "request_timeout", "request body timed out");
160
+ return error instanceof BodyTooLargeError
161
+ ? refuse(413, "payload_too_large", messageOf(error))
162
+ : refuse(400, "invalid_payload", messageOf(error));
163
+ }
164
+ const hook = hookSchema.safeParse(payload);
165
+ if (!hook.success) {
166
+ return refuse(400, "invalid_payload", hook.error.message, payload);
167
+ }
168
+ const check = owned.toolCallCheck;
169
+ const checked = check
170
+ ? await askCheck({
171
+ ...context,
172
+ signal: AbortSignal.any([context.signal, controller.signal]),
173
+ }, check, runId, payload)
174
+ : { response: {} };
175
+ push(owned.toolCalls, {
176
+ toolUseId: hook.data.tool_use_id,
177
+ tool: hook.data.tool_name,
178
+ input: hook.data.tool_input,
179
+ hookPayload: payload,
180
+ response: checked.response,
181
+ action: actionOf(checked.response),
182
+ checked: check !== undefined,
183
+ latencyMs: Date.now() - started,
184
+ at: new Date(started).toISOString(),
185
+ ...(checked.error ? { error: checked.error } : {}),
186
+ }, findSecrets(String(JSON.stringify(hook.data.tool_input)), owned.secrets));
187
+ if (!res.destroyed)
188
+ sendJson(res, 200, checked.response);
124
189
  }
125
- const hook = hookSchema.safeParse(payload);
126
- if (!hook.success) {
127
- return refuse(400, "invalid_payload", hook.error.message, payload);
190
+ finally {
191
+ clearTimeout(timer);
192
+ handlerDone = true;
193
+ release();
128
194
  }
129
- const check = owned.toolCallCheck;
130
- const checked = check
131
- ? await askCheck(context, check, runId, payload)
132
- : { response: {} };
133
- push(owned.toolCalls, {
134
- toolUseId: hook.data.tool_use_id,
135
- tool: hook.data.tool_name,
136
- input: hook.data.tool_input,
137
- hookPayload: payload,
138
- response: checked.response,
139
- action: actionOf(checked.response),
140
- checked: check !== undefined,
141
- latencyMs: Date.now() - started,
142
- at: new Date(started).toISOString(),
143
- ...(checked.error ? { error: checked.error } : {}),
144
- }, findSecrets(String(JSON.stringify(hook.data.tool_input)), owned.secrets));
145
- sendJson(res, 200, checked.response);
146
195
  });
147
196
  }