@gatebolt/agent-redroom 0.1.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.
Files changed (61) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +4 -0
  3. package/README.md +387 -0
  4. package/dist/agents/agent.d.ts +37 -0
  5. package/dist/agents/agent.js +1 -0
  6. package/dist/agents/claude-code.d.ts +39 -0
  7. package/dist/agents/claude-code.js +91 -0
  8. package/dist/agents/claude-stream.d.ts +2 -0
  9. package/dist/agents/claude-stream.js +64 -0
  10. package/dist/deadline.d.ts +1 -0
  11. package/dist/deadline.js +17 -0
  12. package/dist/docker/cli.d.ts +28 -0
  13. package/dist/docker/cli.js +102 -0
  14. package/dist/docker/plan.d.ts +42 -0
  15. package/dist/docker/plan.js +80 -0
  16. package/dist/docker/session.d.ts +14 -0
  17. package/dist/docker/session.js +118 -0
  18. package/dist/hooks.d.ts +5 -0
  19. package/dist/hooks.js +13 -0
  20. package/dist/index.d.ts +9 -0
  21. package/dist/index.js +9 -0
  22. package/dist/paths.d.ts +2 -0
  23. package/dist/paths.js +3 -0
  24. package/dist/planting.d.ts +6 -0
  25. package/dist/planting.js +57 -0
  26. package/dist/redact.d.ts +3 -0
  27. package/dist/redact.js +24 -0
  28. package/dist/redroom.d.ts +37 -0
  29. package/dist/redroom.js +438 -0
  30. package/dist/run.d.ts +39 -0
  31. package/dist/run.js +251 -0
  32. package/dist/secrets.d.ts +2 -0
  33. package/dist/secrets.js +44 -0
  34. package/dist/sidecar/config.d.ts +25 -0
  35. package/dist/sidecar/config.js +15 -0
  36. package/dist/sidecar/control.d.ts +83 -0
  37. package/dist/sidecar/control.js +92 -0
  38. package/dist/sidecar/dns.d.ts +10 -0
  39. package/dist/sidecar/dns.js +71 -0
  40. package/dist/sidecar/fake-web.d.ts +15 -0
  41. package/dist/sidecar/fake-web.js +87 -0
  42. package/dist/sidecar/http.d.ts +9 -0
  43. package/dist/sidecar/http.js +37 -0
  44. package/dist/sidecar/main.d.ts +1 -0
  45. package/dist/sidecar/main.js +22 -0
  46. package/dist/sidecar/relay.d.ts +9 -0
  47. package/dist/sidecar/relay.js +174 -0
  48. package/dist/sidecar/sidecar.d.ts +24 -0
  49. package/dist/sidecar/sidecar.js +106 -0
  50. package/dist/sidecar/state.d.ts +45 -0
  51. package/dist/sidecar/state.js +93 -0
  52. package/dist/sidecar/tool-hook.d.ts +13 -0
  53. package/dist/sidecar/tool-hook.js +147 -0
  54. package/dist/sidecar-bundle/sidecar.mjs +4929 -0
  55. package/dist/sightings.d.ts +10 -0
  56. package/dist/sightings.js +13 -0
  57. package/dist/types.d.ts +139 -0
  58. package/dist/types.js +32 -0
  59. package/images/agent/Dockerfile +18 -0
  60. package/images/sidecar/Dockerfile +10 -0
  61. package/package.json +73 -0
@@ -0,0 +1,39 @@
1
+ import { type Agent, type AgentWiring } from "./agent.js";
2
+ export declare const ANTHROPIC_URL = "https://api.anthropic.com";
3
+ export declare const PLACEHOLDER_API_KEY = "redroom-placeholder-key";
4
+ export declare const MANAGED_SETTINGS = "/etc/claude-code/managed-settings.json";
5
+ export type ClaudeCodeOptions = {
6
+ version: string;
7
+ model: string;
8
+ maxTurns?: number;
9
+ maxBudgetUsd?: number;
10
+ env?: NodeJS.ProcessEnv;
11
+ };
12
+ export declare function managedSettings(wiring: AgentWiring): {
13
+ allowManagedHooksOnly: boolean;
14
+ disableAllHooks: boolean;
15
+ allowedHttpHookUrls: string[];
16
+ skipWebFetchPreflight: boolean;
17
+ permissions: {
18
+ defaultMode: string;
19
+ };
20
+ env: {
21
+ ANTHROPIC_BASE_URL: string;
22
+ ANTHROPIC_API_KEY: string;
23
+ CLAUDE_CODE_USE_BEDROCK: string;
24
+ CLAUDE_CODE_USE_VERTEX: string;
25
+ CLAUDE_CODE_USE_FOUNDRY: string;
26
+ };
27
+ hooks: {
28
+ PreToolUse: {
29
+ matcher: string;
30
+ hooks: {
31
+ type: string;
32
+ url: string;
33
+ onFailure: string;
34
+ timeout: number;
35
+ }[];
36
+ }[];
37
+ };
38
+ };
39
+ export declare function claudeCode(options: ClaudeCodeOptions): Agent;
@@ -0,0 +1,91 @@
1
+ import { WORKSPACE } from "../docker/plan.js";
2
+ import { packagePath } from "../paths.js";
3
+ import {} from "./agent.js";
4
+ import { parseClaudeStream } from "./claude-stream.js";
5
+ export const ANTHROPIC_URL = "https://api.anthropic.com";
6
+ export const PLACEHOLDER_API_KEY = "redroom-placeholder-key";
7
+ export const MANAGED_SETTINGS = "/etc/claude-code/managed-settings.json";
8
+ const HOOK_TIMEOUT_S = 30;
9
+ // Claude Code names the session folder after the cwd, non-alphanumerics replaced by "-".
10
+ const SESSION_DIR = `/home/node/.claude/projects/${WORKSPACE.replace(/[^a-zA-Z0-9]/g, "-")}`;
11
+ export function managedSettings(wiring) {
12
+ return {
13
+ allowManagedHooksOnly: true,
14
+ disableAllHooks: false,
15
+ allowedHttpHookUrls: [wiring.toolHookUrl],
16
+ skipWebFetchPreflight: true,
17
+ permissions: { defaultMode: "bypassPermissions" },
18
+ env: {
19
+ ANTHROPIC_BASE_URL: wiring.modelBaseUrl,
20
+ ANTHROPIC_API_KEY: PLACEHOLDER_API_KEY,
21
+ CLAUDE_CODE_USE_BEDROCK: "0",
22
+ CLAUDE_CODE_USE_VERTEX: "0",
23
+ CLAUDE_CODE_USE_FOUNDRY: "0",
24
+ },
25
+ hooks: {
26
+ PreToolUse: [
27
+ {
28
+ matcher: "*",
29
+ hooks: [
30
+ {
31
+ type: "http",
32
+ url: wiring.toolHookUrl,
33
+ onFailure: "block",
34
+ timeout: HOOK_TIMEOUT_S,
35
+ },
36
+ ],
37
+ },
38
+ ],
39
+ },
40
+ };
41
+ }
42
+ export function claudeCode(options) {
43
+ const maxTurns = options.maxTurns ?? 30;
44
+ const maxBudgetUsd = options.maxBudgetUsd ?? 1;
45
+ const env = options.env ?? process.env;
46
+ return {
47
+ name: "claude-code",
48
+ version: options.version,
49
+ model: options.model,
50
+ image: {
51
+ tag: `agent-redroom-claude-code:${options.version}`,
52
+ context: packagePath("images", "agent"),
53
+ buildArgs: { CLAUDE_CODE_VERSION: options.version },
54
+ },
55
+ user: "node",
56
+ uid: 1000,
57
+ defaultModelProxy() {
58
+ const key = env.ANTHROPIC_API_KEY;
59
+ return key
60
+ ? { baseUrl: ANTHROPIC_URL, headers: { "x-api-key": key } }
61
+ : null;
62
+ },
63
+ env: (wiring) => ({
64
+ ANTHROPIC_BASE_URL: wiring.modelBaseUrl,
65
+ ANTHROPIC_API_KEY: PLACEHOLDER_API_KEY,
66
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
67
+ }),
68
+ rootFiles: (wiring) => [
69
+ {
70
+ path: MANAGED_SETTINGS,
71
+ body: JSON.stringify(managedSettings(wiring), null, 2),
72
+ },
73
+ ],
74
+ versionCommand: () => ["claude", "--version"],
75
+ turnCommand: ({ runId, message, firstTurn }) => [
76
+ ...["claude", "-p", "--output-format", "stream-json", "--verbose"],
77
+ ...["--model", options.model],
78
+ ...["--max-turns", String(maxTurns)],
79
+ ...["--max-budget-usd", String(maxBudgetUsd)],
80
+ ...[firstTurn ? "--session-id" : "--resume", runId],
81
+ ...["--", message],
82
+ ],
83
+ killCommand: () => ["pkill", "-f", "claude"],
84
+ parseTurn: parseClaudeStream,
85
+ transcriptCommand: (runId) => [
86
+ "cat",
87
+ "--",
88
+ `${SESSION_DIR}/${runId}.jsonl`,
89
+ ],
90
+ };
91
+ }
@@ -0,0 +1,2 @@
1
+ import { type ParsedTurn } from "./agent.js";
2
+ export declare function parseClaudeStream(stdout: string): ParsedTurn;
@@ -0,0 +1,64 @@
1
+ import { isRecord } from "../types.js";
2
+ import {} from "./agent.js";
3
+ const LIMITS = {
4
+ error_max_turns: "max_turns",
5
+ error_max_budget_usd: "max_budget",
6
+ };
7
+ function parseLine(line) {
8
+ if (line.trim() === "")
9
+ return null;
10
+ try {
11
+ const value = JSON.parse(line);
12
+ return isRecord(value) ? value : null;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ function assistantText(event) {
19
+ if (event.type !== "assistant" || !isRecord(event.message))
20
+ return null;
21
+ const { content } = event.message;
22
+ if (!Array.isArray(content))
23
+ return null;
24
+ const text = content
25
+ .filter(isRecord)
26
+ .map((block) => block.type === "text" && typeof block.text === "string" ? block.text : "")
27
+ .join("");
28
+ return text === "" ? null : text;
29
+ }
30
+ const numberOr = (value, fallback) => typeof value === "number" ? value : fallback;
31
+ export function parseClaudeStream(stdout) {
32
+ const events = stdout
33
+ .split("\n")
34
+ .map(parseLine)
35
+ .filter((event) => event !== null);
36
+ const latest = [...events].reverse();
37
+ const result = latest.find((event) => event.type === "result");
38
+ const lastText = latest.map(assistantText).find((text) => text !== null) ?? "";
39
+ if (!result) {
40
+ return {
41
+ complete: false,
42
+ events,
43
+ finalText: lastText,
44
+ costUsd: 0,
45
+ usage: null,
46
+ durationMs: 0,
47
+ stopReason: null,
48
+ limitHit: null,
49
+ isError: true,
50
+ };
51
+ }
52
+ const subtype = typeof result.subtype === "string" ? result.subtype : "";
53
+ return {
54
+ complete: true,
55
+ events,
56
+ finalText: typeof result.result === "string" ? result.result : lastText,
57
+ costUsd: numberOr(result.total_cost_usd, 0),
58
+ usage: result.usage ?? null,
59
+ durationMs: numberOr(result.duration_ms, 0),
60
+ stopReason: typeof result.stop_reason === "string" ? result.stop_reason : null,
61
+ limitHit: LIMITS[subtype] ?? null,
62
+ isError: result.is_error === true || subtype !== "success",
63
+ };
64
+ }
@@ -0,0 +1 @@
1
+ export declare function withDeadline<T>(ms: number, timeout: () => Error, work: (signal: AbortSignal) => Promise<T>): Promise<T>;
@@ -0,0 +1,17 @@
1
+ export async function withDeadline(ms, timeout, work) {
2
+ const controller = new AbortController();
3
+ let timer;
4
+ const expired = new Promise((_, reject) => {
5
+ timer = setTimeout(() => {
6
+ const error = timeout();
7
+ controller.abort(error);
8
+ reject(error);
9
+ }, ms);
10
+ });
11
+ try {
12
+ return await Promise.race([work(controller.signal), expired]);
13
+ }
14
+ finally {
15
+ clearTimeout(timer);
16
+ }
17
+ }
@@ -0,0 +1,28 @@
1
+ export type DockerResult = {
2
+ exitCode: number;
3
+ stdout: string;
4
+ stderr: string;
5
+ timedOut: boolean;
6
+ outputTooLarge?: boolean;
7
+ };
8
+ export type DockerOptions = {
9
+ env?: Record<string, string>;
10
+ input?: Buffer;
11
+ timeoutMs?: number;
12
+ };
13
+ export type Docker = (args: string[], options?: DockerOptions) => Promise<DockerResult>;
14
+ export declare const MAX_OUTPUT_MB = 64;
15
+ export declare const MAX_OUTPUT_BYTES: number;
16
+ export declare const DOCKER_TIMEOUT_MS: number;
17
+ export declare const BUILD_TIMEOUT_MS: number;
18
+ export declare function dockerCli(bin?: string): Docker;
19
+ export declare function withDefaultTimeouts(docker: Docker): Docker;
20
+ export type TarEntry = {
21
+ path: string;
22
+ body: string | Buffer;
23
+ mode?: number;
24
+ owner?: number;
25
+ };
26
+ export declare function tarOf(entries: readonly TarEntry[], options?: {
27
+ dirs: boolean;
28
+ }): Buffer;
@@ -0,0 +1,102 @@
1
+ import { execFile } from "node:child_process";
2
+ import { posix } from "node:path";
3
+ import { withDeadline } from "../deadline.js";
4
+ import { RedroomError } from "../types.js";
5
+ export const MAX_OUTPUT_MB = 64;
6
+ export const MAX_OUTPUT_BYTES = MAX_OUTPUT_MB * 1024 * 1024;
7
+ export const DOCKER_TIMEOUT_MS = 2 * 60 * 1000;
8
+ export const BUILD_TIMEOUT_MS = 30 * 60 * 1000;
9
+ export function dockerCli(bin = "docker") {
10
+ return (args, options = {}) => new Promise((resolve, reject) => {
11
+ const child = execFile(bin, args, {
12
+ env: { ...process.env, ...options.env },
13
+ maxBuffer: MAX_OUTPUT_BYTES,
14
+ timeout: options.timeoutMs ?? 0,
15
+ killSignal: "SIGKILL",
16
+ encoding: "utf8",
17
+ }, (error, stdout, stderr) => {
18
+ if (error?.code === "ENOENT") {
19
+ reject(new RedroomError("docker_unavailable", `${bin} not found on PATH`, "docker", { command: bin }));
20
+ return;
21
+ }
22
+ resolve({
23
+ exitCode: typeof error?.code === "number" ? error.code : error ? 1 : 0,
24
+ stdout,
25
+ stderr,
26
+ timedOut: error?.killed === true,
27
+ outputTooLarge: error?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER",
28
+ });
29
+ });
30
+ child.stdin?.on("error", () => undefined);
31
+ child.stdin?.end(options.input);
32
+ });
33
+ }
34
+ export function withDefaultTimeouts(docker) {
35
+ return async (args, options = {}) => {
36
+ if (options.timeoutMs !== undefined)
37
+ return docker(args, options);
38
+ const timeoutMs = args[0] === "build" ? BUILD_TIMEOUT_MS : DOCKER_TIMEOUT_MS;
39
+ const timeoutError = () => new RedroomError("docker_timeout", `docker ${args[0]} ran past ${timeoutMs} ms`, "docker", {
40
+ command: `docker ${args.slice(0, 2).join(" ")}`,
41
+ durationMs: timeoutMs,
42
+ });
43
+ const result = await withDeadline(timeoutMs, timeoutError, () => docker(args, { ...options, timeoutMs }));
44
+ if (result.timedOut)
45
+ throw timeoutError();
46
+ return result;
47
+ };
48
+ }
49
+ const BLOCK = 512;
50
+ function octal(value, width) {
51
+ return `${value.toString(8).padStart(width - 1, "0")}\0`;
52
+ }
53
+ function header(path, size, mode, owner, type) {
54
+ const block = Buffer.alloc(BLOCK);
55
+ const slash = path.length > 100 ? path.lastIndexOf("/", 155) : -1;
56
+ const [prefix, name] = slash > 0 ? [path.slice(0, slash), path.slice(slash + 1)] : ["", path];
57
+ if (Buffer.byteLength(name) > 100 || Buffer.byteLength(prefix) > 155) {
58
+ throw new Error(`tar path too long: ${path}`);
59
+ }
60
+ block.write(name, 0);
61
+ block.write(octal(mode, 8), 100);
62
+ block.write(octal(owner, 8), 108);
63
+ block.write(octal(owner, 8), 116);
64
+ block.write(octal(size, 12), 124);
65
+ block.write(octal(0, 12), 136);
66
+ block.write(" ", 148);
67
+ block.write(type, 156);
68
+ block.write("ustar\0", 257);
69
+ block.write("00", 263);
70
+ block.write(prefix, 345);
71
+ const sum = block.reduce((total, byte) => total + byte, 0);
72
+ block.write(`${sum.toString(8).padStart(6, "0")}\0 `, 148);
73
+ return block;
74
+ }
75
+ function safePath(path) {
76
+ const normal = posix.normalize(path);
77
+ if (posix.isAbsolute(normal) || normal === ".." || normal.startsWith("../")) {
78
+ throw new Error(`tar path leaves the archive: ${path}`);
79
+ }
80
+ return normal;
81
+ }
82
+ export function tarOf(entries, options = { dirs: true }) {
83
+ const parts = [];
84
+ const dirs = new Set();
85
+ for (const entry of entries) {
86
+ const path = safePath(entry.path);
87
+ const owner = entry.owner ?? 0;
88
+ const segments = options.dirs ? path.split("/").slice(0, -1) : [];
89
+ for (let index = 1; index <= segments.length; index += 1) {
90
+ const dir = `${segments.slice(0, index).join("/")}/`;
91
+ if (dirs.has(dir))
92
+ continue;
93
+ dirs.add(dir);
94
+ parts.push(header(dir, 0, 0o755, owner, "5"));
95
+ }
96
+ const body = Buffer.from(entry.body);
97
+ parts.push(header(path, body.length, entry.mode ?? 0o644, owner, "0"));
98
+ parts.push(body, Buffer.alloc((BLOCK - (body.length % BLOCK)) % BLOCK));
99
+ }
100
+ parts.push(Buffer.alloc(BLOCK * 2));
101
+ return Buffer.concat(parts);
102
+ }
@@ -0,0 +1,42 @@
1
+ import { type Limits } from "../types.js";
2
+ export declare const LABEL = "agent-redroom=1";
3
+ export declare const SESSION_LABEL = "agent-redroom.session";
4
+ export declare const ROLE_LABEL = "agent-redroom.role";
5
+ export declare const RUN_LABEL = "agent-redroom.run";
6
+ export declare const SIDECAR_IMAGE = "agent-redroom-sidecar:local";
7
+ export declare const WORKSPACE = "/workspace";
8
+ export declare const CA_DIR = "/etc/redroom";
9
+ export declare const CA_FILE = "ca.pem";
10
+ export declare const SYSTEM_CA_DIR = "/usr/local/share/ca-certificates";
11
+ export declare const SYSTEM_CA_FILE = "agent-redroom-ca.crt";
12
+ export declare const SIDECAR_HOST = 2;
13
+ export declare const AGENT_HOST = 10;
14
+ export declare const DEFAULT_LIMITS: Limits;
15
+ export declare const SIDECAR_LIMITS: {
16
+ memory: string;
17
+ heapMb: number;
18
+ pids: number;
19
+ };
20
+ export declare const setEnv: (vars: Record<string, string>) => string[];
21
+ export declare const passEnv: (vars: Record<string, string>) => string[];
22
+ export declare function sessionPlan(session: string): {
23
+ session: string;
24
+ labels: string[];
25
+ egress: string;
26
+ sidecar: string;
27
+ agent: (runId: string) => string;
28
+ internal: (runId: string) => string;
29
+ };
30
+ export type SessionPlan = ReturnType<typeof sessionPlan>;
31
+ export declare function sidecarArgs(plan: SessionPlan, env: Record<string, string>, publish: boolean): string[];
32
+ export declare function agentArgs(plan: SessionPlan, input: {
33
+ name: string;
34
+ runId: string;
35
+ ip: string;
36
+ sidecarIp: string;
37
+ image: string;
38
+ limits: Limits;
39
+ env: Record<string, string>;
40
+ }): string[];
41
+ export declare const GIT_INIT = "git init -q && git add -A && git -c user.name=redroom -c user.email=redroom@localhost commit -q --allow-empty -m workspace";
42
+ export declare const gitDiff: (baseline: string) => string;
@@ -0,0 +1,80 @@
1
+ import { PORTS } from "../sidecar/config.js";
2
+ import {} from "../types.js";
3
+ export const LABEL = "agent-redroom=1";
4
+ export const SESSION_LABEL = "agent-redroom.session";
5
+ export const ROLE_LABEL = "agent-redroom.role";
6
+ export const RUN_LABEL = "agent-redroom.run";
7
+ export const SIDECAR_IMAGE = "agent-redroom-sidecar:local";
8
+ export const WORKSPACE = "/workspace";
9
+ export const CA_DIR = "/etc/redroom";
10
+ export const CA_FILE = "ca.pem";
11
+ export const SYSTEM_CA_DIR = "/usr/local/share/ca-certificates";
12
+ export const SYSTEM_CA_FILE = "agent-redroom-ca.crt";
13
+ const SYSTEM_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt";
14
+ export const SIDECAR_HOST = 2;
15
+ export const AGENT_HOST = 10;
16
+ export const DEFAULT_LIMITS = {
17
+ cpus: 2,
18
+ memory: "2g",
19
+ pids: 1024,
20
+ turnTimeoutMs: 10 * 60 * 1000,
21
+ };
22
+ // MAX_PARALLEL (245) runs x 3 logs x MAX_LOG_BYTES is about 2.9 GiB; heap leaves
23
+ // room for overhead, the rest holds off-heap request buffers.
24
+ export const SIDECAR_LIMITS = { memory: "8g", heapMb: 6144, pids: 256 };
25
+ export const setEnv = (vars) => Object.entries(vars).flatMap(([name, value]) => ["-e", `${name}=${value}`]);
26
+ export const passEnv = (vars) => Object.keys(vars).flatMap((name) => ["-e", name]);
27
+ export function sessionPlan(session) {
28
+ const prefix = `redroom-${session}`;
29
+ return {
30
+ session,
31
+ labels: ["--label", LABEL, "--label", `${SESSION_LABEL}=${session}`],
32
+ egress: `${prefix}-egress`,
33
+ sidecar: `${prefix}-sidecar`,
34
+ agent: (runId) => `${prefix}-${runId}`,
35
+ internal: (runId) => `${prefix}-${runId}-internal`,
36
+ };
37
+ }
38
+ export function sidecarArgs(plan, env, publish) {
39
+ return [
40
+ ...["create", "--name", plan.sidecar, "--network", plan.egress],
41
+ ...plan.labels,
42
+ ...["--label", `${ROLE_LABEL}=sidecar`],
43
+ ...["--add-host", "host.docker.internal:host-gateway"],
44
+ ...["--memory", SIDECAR_LIMITS.memory],
45
+ ...["--pids-limit", String(SIDECAR_LIMITS.pids)],
46
+ ...(publish ? ["-p", `127.0.0.1::${PORTS.control}`] : []),
47
+ ...passEnv(env),
48
+ ...setEnv({
49
+ NODE_OPTIONS: `--max-old-space-size=${SIDECAR_LIMITS.heapMb}`,
50
+ }),
51
+ SIDECAR_IMAGE,
52
+ ];
53
+ }
54
+ export function agentArgs(plan, input) {
55
+ return [
56
+ ...["create", "--name", input.name, ...plan.labels],
57
+ ...[
58
+ "--label",
59
+ `${ROLE_LABEL}=agent`,
60
+ "--label",
61
+ `${RUN_LABEL}=${input.runId}`,
62
+ ],
63
+ ...["--network", plan.internal(input.runId), "--ip", input.ip],
64
+ ...["--dns", input.sidecarIp],
65
+ ...["--init", "--memory", input.limits.memory],
66
+ ...["--cpus", String(input.limits.cpus)],
67
+ ...["--pids-limit", String(input.limits.pids)],
68
+ ...["--cap-drop", "ALL", "--security-opt", "no-new-privileges"],
69
+ ...setEnv(input.env),
70
+ ...setEnv({
71
+ NODE_EXTRA_CA_CERTS: `${CA_DIR}/${CA_FILE}`,
72
+ SSL_CERT_FILE: SYSTEM_CA_BUNDLE,
73
+ CURL_CA_BUNDLE: SYSTEM_CA_BUNDLE,
74
+ REQUESTS_CA_BUNDLE: SYSTEM_CA_BUNDLE,
75
+ }),
76
+ input.image,
77
+ ];
78
+ }
79
+ export const GIT_INIT = "git init -q && git add -A && git -c user.name=redroom -c user.email=redroom@localhost commit -q --allow-empty -m workspace";
80
+ export const gitDiff = (baseline) => `git add -A && git diff --cached --binary ${baseline}`;
@@ -0,0 +1,14 @@
1
+ import { type Docker } from "./cli.js";
2
+ import { type SessionPlan } from "./plan.js";
3
+ export declare const LEASE_MS = 90000;
4
+ export declare const LEASE_RENEW_MS = 30000;
5
+ export declare function dockerTime(text: string): number | null;
6
+ export type Leftovers = {
7
+ containers: string[];
8
+ networks: string[];
9
+ };
10
+ export declare function findLeftovers(docker: Docker, now: number): Promise<Leftovers>;
11
+ export declare function removeLeftovers(docker: Docker, now?: number): Promise<Leftovers>;
12
+ export declare function createInternalNetwork(docker: Docker, labels: string[], name: string, pickSubnet: () => string, redact: (text: string) => string): Promise<string>;
13
+ export declare function tearDownSession(docker: Docker, plan: Pick<SessionPlan, "session">): Promise<string[]>;
14
+ export declare function networkIp(inspected: string, network: string): string | null;
@@ -0,0 +1,118 @@
1
+ import { messageOf, RedroomError } from "../types.js";
2
+ import {} from "./cli.js";
3
+ import { LABEL, ROLE_LABEL, SESSION_LABEL } from "./plan.js";
4
+ export const LEASE_MS = 90_000;
5
+ export const LEASE_RENEW_MS = 30_000;
6
+ const GRACE_MS = 2 * LEASE_MS;
7
+ const SUBNET_ATTEMPTS = 8;
8
+ const NO_HOST_GATEWAY = "com.docker.network.bridge.gateway_mode_ipv4=isolated";
9
+ const rows = (text) => text
10
+ .split("\n")
11
+ .map((line) => line.trim())
12
+ .filter((line) => line !== "")
13
+ .map((line) => line.split("\t"));
14
+ const DOCKER_TIME = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})(\.\d+)? ([+-]\d{2})(\d{2})/;
15
+ export function dockerTime(text) {
16
+ const match = DOCKER_TIME.exec(text);
17
+ if (!match)
18
+ return null;
19
+ const [, day, time, fraction = "", hours, minutes] = match;
20
+ return Date.parse(`${day}T${time}${fraction.slice(0, 4)}${hours}:${minutes}`);
21
+ }
22
+ export async function findLeftovers(docker, now) {
23
+ const filter = ["--filter", `label=${LABEL}`];
24
+ const ps = await docker([
25
+ ...["ps", "-a", ...filter, "--format"],
26
+ `{{.ID}}\t{{.Label "${SESSION_LABEL}"}}\t{{.Label "${ROLE_LABEL}"}}\t{{.State}}\t{{.CreatedAt}}`,
27
+ ]);
28
+ const nets = await docker([
29
+ ...["network", "ls", ...filter, "--format"],
30
+ `{{.ID}}\t{{.Label "${SESSION_LABEL}"}}\t{{.CreatedAt}}`,
31
+ ]);
32
+ const containers = rows(ps.stdout).map(([id = "", session = "", role = "", state = "", created = ""]) => ({
33
+ id,
34
+ session,
35
+ live: role === "sidecar" && state === "running",
36
+ created,
37
+ }));
38
+ const networks = rows(nets.stdout).map(([id = "", session = "", created = ""]) => ({
39
+ id,
40
+ session,
41
+ live: false,
42
+ created,
43
+ }));
44
+ const young = (created) => {
45
+ const at = dockerTime(created);
46
+ return at === null || now - at < GRACE_MS;
47
+ };
48
+ const live = new Set([...containers, ...networks]
49
+ .filter((item) => item.live || young(item.created))
50
+ .map((item) => item.session));
51
+ return {
52
+ containers: containers
53
+ .filter((item) => !live.has(item.session))
54
+ .map((item) => item.id),
55
+ networks: networks
56
+ .filter((item) => !live.has(item.session))
57
+ .map((item) => item.id),
58
+ };
59
+ }
60
+ export async function removeLeftovers(docker, now = Date.now()) {
61
+ const found = await findLeftovers(docker, now);
62
+ if (found.containers.length > 0) {
63
+ await docker(["rm", "-f", ...found.containers]);
64
+ }
65
+ if (found.networks.length > 0) {
66
+ await docker(["network", "rm", ...found.networks]);
67
+ }
68
+ return found;
69
+ }
70
+ export async function createInternalNetwork(docker, labels, name, pickSubnet, redact) {
71
+ let stderr = "";
72
+ for (let attempt = 0; attempt < SUBNET_ATTEMPTS; attempt += 1) {
73
+ const subnet = pickSubnet();
74
+ const created = await docker([
75
+ ...["network", "create", "--internal", "-o", NO_HOST_GATEWAY],
76
+ ...["--subnet", `${subnet}.0/24`, ...labels, name],
77
+ ]);
78
+ if (created.exitCode === 0)
79
+ return subnet;
80
+ stderr = created.stderr;
81
+ if (!/overlap|pool/i.test(stderr))
82
+ break;
83
+ }
84
+ throw new RedroomError("network_create_failed", `could not create ${name}: ${redact(stderr.trim())}`, "docker", { command: "docker network create" });
85
+ }
86
+ export async function tearDownSession(docker, plan) {
87
+ const problems = [];
88
+ const attempt = async (args) => {
89
+ try {
90
+ const result = await docker(args);
91
+ if (result.exitCode !== 0)
92
+ problems.push(result.stderr.trim());
93
+ return result.stdout;
94
+ }
95
+ catch (error) {
96
+ problems.push(messageOf(error));
97
+ return "";
98
+ }
99
+ };
100
+ const filter = ["--filter", `label=${SESSION_LABEL}=${plan.session}`];
101
+ const ids = rows(await attempt(["ps", "-a", "-q", ...filter])).map(([id = ""]) => id);
102
+ if (ids.length > 0)
103
+ await attempt(["rm", "-f", ...ids]);
104
+ const networks = rows(await attempt(["network", "ls", "-q", ...filter])).map(([id = ""]) => id);
105
+ if (networks.length > 0)
106
+ await attempt(["network", "rm", ...networks]);
107
+ return problems;
108
+ }
109
+ export function networkIp(inspected, network) {
110
+ try {
111
+ const parsed = JSON.parse(inspected);
112
+ const ip = parsed[network]?.IPAddress;
113
+ return ip === undefined || ip === "" ? null : ip;
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ }
@@ -0,0 +1,5 @@
1
+ import { type AgentWiring } from "./agents/agent.js";
2
+ export declare function wiring(sidecarIp: string, runId: string): AgentWiring;
3
+ export declare function headerValues(...settings: ({
4
+ headers?: Record<string, string>;
5
+ } | null | undefined)[]): string[];
package/dist/hooks.js ADDED
@@ -0,0 +1,13 @@
1
+ import {} from "./agents/agent.js";
2
+ import { PORTS } from "./sidecar/config.js";
3
+ export function wiring(sidecarIp, runId) {
4
+ return {
5
+ runId,
6
+ modelBaseUrl: `http://${sidecarIp}:${PORTS.relay}`,
7
+ toolHookUrl: `http://${sidecarIp}:${PORTS.hook}/hook/${runId}`,
8
+ };
9
+ }
10
+ export function headerValues(...settings) {
11
+ const values = settings.flatMap((setting) => Object.values(setting?.headers ?? {}));
12
+ return [...values, ...values.map((value) => value.replace(/^\S+\s+/, ""))];
13
+ }
@@ -0,0 +1,9 @@
1
+ export { type Agent, type AgentWiring, type ParsedTurn, type RootFile, } from "./agents/agent.js";
2
+ export { claudeCode, type ClaudeCodeOptions } from "./agents/claude-code.js";
3
+ export { type Docker, dockerCli } from "./docker/cli.js";
4
+ export { removeLeftovers } from "./docker/session.js";
5
+ export { redactSecrets } from "./redact.js";
6
+ export { DEFAULT_DOMAIN, type Redroom, type RedroomOptions, startRedroom, } from "./redroom.js";
7
+ export { type Run } from "./run.js";
8
+ export { findSecrets } from "./secrets.js";
9
+ export { type Artifact, type CallInfo, type Encoding, type ErrorBy, type ErrorInfo, type FileBody, type Hit, type Limits, type ModelCall, type ModelProxy, type Page, type PlantedFile, RedroomError, type RunOptions, type RunResult, type SecretMatch, type Secrets, type Sighting, type ToolCall, type ToolCallCheck, type Turn, type Workspace, } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export {} from "./agents/agent.js";
2
+ export { claudeCode } from "./agents/claude-code.js";
3
+ export { dockerCli } from "./docker/cli.js";
4
+ export { removeLeftovers } from "./docker/session.js";
5
+ export { redactSecrets } from "./redact.js";
6
+ export { DEFAULT_DOMAIN, startRedroom, } from "./redroom.js";
7
+ export {} from "./run.js";
8
+ export { findSecrets } from "./secrets.js";
9
+ export { RedroomError, } from "./types.js";
@@ -0,0 +1,2 @@
1
+ export declare const PACKAGE_ROOT: string;
2
+ export declare const packagePath: (...parts: string[]) => string;
package/dist/paths.js ADDED
@@ -0,0 +1,3 @@
1
+ import { fileURLToPath } from "node:url";
2
+ export const PACKAGE_ROOT = fileURLToPath(new URL("..", import.meta.url));
3
+ export const packagePath = (...parts) => [PACKAGE_ROOT.replace(/\/$/, ""), ...parts].join("/");
@@ -0,0 +1,6 @@
1
+ import { type TarEntry } from "./docker/cli.js";
2
+ import { type ServedPage } from "./sidecar/state.js";
3
+ import { type Page, type PlantedFile } from "./types.js";
4
+ export declare function servedPages(pages: Record<string, Page>, domain: string): Record<string, ServedPage>;
5
+ export declare function workspaceEntries(workspace: Record<string, PlantedFile>, owner: number): TarEntry[];
6
+ export declare function absoluteEntries(files: Record<string, PlantedFile>, owner: number): TarEntry[];