@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.
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +387 -0
- package/dist/agents/agent.d.ts +37 -0
- package/dist/agents/agent.js +1 -0
- package/dist/agents/claude-code.d.ts +39 -0
- package/dist/agents/claude-code.js +91 -0
- package/dist/agents/claude-stream.d.ts +2 -0
- package/dist/agents/claude-stream.js +64 -0
- package/dist/deadline.d.ts +1 -0
- package/dist/deadline.js +17 -0
- package/dist/docker/cli.d.ts +28 -0
- package/dist/docker/cli.js +102 -0
- package/dist/docker/plan.d.ts +42 -0
- package/dist/docker/plan.js +80 -0
- package/dist/docker/session.d.ts +14 -0
- package/dist/docker/session.js +118 -0
- package/dist/hooks.d.ts +5 -0
- package/dist/hooks.js +13 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +3 -0
- package/dist/planting.d.ts +6 -0
- package/dist/planting.js +57 -0
- package/dist/redact.d.ts +3 -0
- package/dist/redact.js +24 -0
- package/dist/redroom.d.ts +37 -0
- package/dist/redroom.js +438 -0
- package/dist/run.d.ts +39 -0
- package/dist/run.js +251 -0
- package/dist/secrets.d.ts +2 -0
- package/dist/secrets.js +44 -0
- package/dist/sidecar/config.d.ts +25 -0
- package/dist/sidecar/config.js +15 -0
- package/dist/sidecar/control.d.ts +83 -0
- package/dist/sidecar/control.js +92 -0
- package/dist/sidecar/dns.d.ts +10 -0
- package/dist/sidecar/dns.js +71 -0
- package/dist/sidecar/fake-web.d.ts +15 -0
- package/dist/sidecar/fake-web.js +87 -0
- package/dist/sidecar/http.d.ts +9 -0
- package/dist/sidecar/http.js +37 -0
- package/dist/sidecar/main.d.ts +1 -0
- package/dist/sidecar/main.js +22 -0
- package/dist/sidecar/relay.d.ts +9 -0
- package/dist/sidecar/relay.js +174 -0
- package/dist/sidecar/sidecar.d.ts +24 -0
- package/dist/sidecar/sidecar.js +106 -0
- package/dist/sidecar/state.d.ts +45 -0
- package/dist/sidecar/state.js +93 -0
- package/dist/sidecar/tool-hook.d.ts +13 -0
- package/dist/sidecar/tool-hook.js +147 -0
- package/dist/sidecar-bundle/sidecar.mjs +4929 -0
- package/dist/sightings.d.ts +10 -0
- package/dist/sightings.js +13 -0
- package/dist/types.d.ts +139 -0
- package/dist/types.js +32 -0
- package/images/agent/Dockerfile +18 -0
- package/images/sidecar/Dockerfile +10 -0
- package/package.json +73 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import {} from "node:http";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { findSecrets } from "../secrets.js";
|
|
4
|
+
import { DETAIL_LIMIT, isRecord, messageOf, } from "../types.js";
|
|
5
|
+
import { BodyTooLargeError, jsonServer, readBody, readJson, sendJson, } from "./http.js";
|
|
6
|
+
import { push, sourceIp } from "./state.js";
|
|
7
|
+
export const CHECK_TIMEOUT_MS = 25_000;
|
|
8
|
+
const hookSchema = z.object({
|
|
9
|
+
tool_name: z.string(),
|
|
10
|
+
tool_input: z.unknown().default({}),
|
|
11
|
+
tool_use_id: z.string().default(""),
|
|
12
|
+
});
|
|
13
|
+
function parseJson(text) {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(text);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function actionOf(response) {
|
|
22
|
+
if (!isRecord(response))
|
|
23
|
+
return "allow";
|
|
24
|
+
if (response.decision === "block")
|
|
25
|
+
return "deny";
|
|
26
|
+
const specific = response.hookSpecificOutput;
|
|
27
|
+
const decision = isRecord(specific) ? specific.permissionDecision : undefined;
|
|
28
|
+
return decision === "deny" || decision === "ask" ? decision : "allow";
|
|
29
|
+
}
|
|
30
|
+
const deny = (reason) => ({
|
|
31
|
+
hookSpecificOutput: {
|
|
32
|
+
hookEventName: "PreToolUse",
|
|
33
|
+
permissionDecision: "deny",
|
|
34
|
+
permissionDecisionReason: reason,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
const readText = async (answer) => answer.body ? (await readBody(answer.body)).toString("utf8") : "";
|
|
38
|
+
function failureCode(error) {
|
|
39
|
+
if (error instanceof BodyTooLargeError)
|
|
40
|
+
return "response_too_large";
|
|
41
|
+
const name = error instanceof Error ? error.name : "";
|
|
42
|
+
return name === "TimeoutError" ? "timeout" : "network_error";
|
|
43
|
+
}
|
|
44
|
+
async function askCheck(context, check, runId, payload) {
|
|
45
|
+
const started = Date.now();
|
|
46
|
+
const call = (status) => ({
|
|
47
|
+
url: check.url,
|
|
48
|
+
...(status === undefined ? {} : { status }),
|
|
49
|
+
durationMs: Date.now() - started,
|
|
50
|
+
});
|
|
51
|
+
const failed = (code, detail, status) => {
|
|
52
|
+
const error = {
|
|
53
|
+
by: "toolCallCheck",
|
|
54
|
+
code,
|
|
55
|
+
detail: detail.slice(0, DETAIL_LIMIT),
|
|
56
|
+
call: call(status),
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
response: deny(`Tool call check unavailable (${code})`),
|
|
60
|
+
error,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
let answer;
|
|
64
|
+
let text;
|
|
65
|
+
try {
|
|
66
|
+
answer = await context.fetch(check.url, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: {
|
|
69
|
+
...check.headers,
|
|
70
|
+
"content-type": "application/json",
|
|
71
|
+
"x-redroom-run": runId,
|
|
72
|
+
},
|
|
73
|
+
body: JSON.stringify(payload),
|
|
74
|
+
signal: AbortSignal.any([
|
|
75
|
+
context.signal,
|
|
76
|
+
AbortSignal.timeout(context.timeoutMs),
|
|
77
|
+
]),
|
|
78
|
+
});
|
|
79
|
+
text = await readText(answer);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return failed(failureCode(error), messageOf(error));
|
|
83
|
+
}
|
|
84
|
+
if (!answer.ok)
|
|
85
|
+
return failed(`http_${answer.status}`, text, answer.status);
|
|
86
|
+
const response = parseJson(text);
|
|
87
|
+
return isRecord(response)
|
|
88
|
+
? { response }
|
|
89
|
+
: failed("invalid_response", text, answer.status);
|
|
90
|
+
}
|
|
91
|
+
export function createToolHook(context) {
|
|
92
|
+
return jsonServer(async (req, res) => {
|
|
93
|
+
const runId = /^\/hook\/([^/?]+)$/.exec(req.url ?? "")?.[1];
|
|
94
|
+
const run = runId ? context.state.runs.get(runId) : undefined;
|
|
95
|
+
const source = sourceIp(req, context.trustTestSourceHeader);
|
|
96
|
+
const owned = run?.ip === source ? run : undefined;
|
|
97
|
+
if (req.method !== "POST" || !runId || !owned) {
|
|
98
|
+
return sendJson(res, 404, {});
|
|
99
|
+
}
|
|
100
|
+
const started = Date.now();
|
|
101
|
+
const refuse = (status, code, detail, hookPayload = null) => {
|
|
102
|
+
push(owned.toolCalls, {
|
|
103
|
+
toolUseId: "",
|
|
104
|
+
tool: "",
|
|
105
|
+
input: null,
|
|
106
|
+
hookPayload,
|
|
107
|
+
response: {},
|
|
108
|
+
action: "deny",
|
|
109
|
+
checked: false,
|
|
110
|
+
latencyMs: Date.now() - started,
|
|
111
|
+
at: new Date(started).toISOString(),
|
|
112
|
+
error: { by: "sidecar", code, detail: detail.slice(0, DETAIL_LIMIT) },
|
|
113
|
+
});
|
|
114
|
+
sendJson(res, status, {});
|
|
115
|
+
};
|
|
116
|
+
let payload;
|
|
117
|
+
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));
|
|
124
|
+
}
|
|
125
|
+
const hook = hookSchema.safeParse(payload);
|
|
126
|
+
if (!hook.success) {
|
|
127
|
+
return refuse(400, "invalid_payload", hook.error.message, payload);
|
|
128
|
+
}
|
|
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
|
+
});
|
|
147
|
+
}
|