agent-anywhere-gateway 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/README.md +46 -0
- package/bin/agent-anywhere-gateway.js +13 -0
- package/config/cloudflare.gateway.env.example +9 -0
- package/package.json +29 -0
- package/src/adapters/local-agent-adapter.js +131 -0
- package/src/gateway/client.js +422 -0
- package/src/gateway/main.js +224 -0
- package/src/gateway/providers.js +28 -0
- package/src/gateway/runner.js +337 -0
- package/src/gateway.js +7 -0
- package/src/lib/capabilities.js +1 -0
- package/src/lib/local-discovery.js +322 -0
- package/src/lib/path-policy.js +1 -0
- package/src/runtimes/claude-code-headless-runtime.js +547 -0
- package/src/runtimes/claude-code-runtime.js +984 -0
- package/src/runtimes/codex-app-server-client.js +157 -0
- package/src/runtimes/codex-app-server-runtime.js +790 -0
- package/src/runtimes/codex-runtime.js +418 -0
- package/src/runtimes/mock-runtime.js +140 -0
- package/src/shared/capabilities.js +175 -0
- package/src/shared/gateway-protocol.js +26 -0
- package/src/shared/http-utils.js +78 -0
- package/src/shared/image-attachments.js +269 -0
- package/src/shared/path-policy.js +110 -0
- package/src/shared/project-files.js +119 -0
- package/src/shared/providers.js +27 -0
- package/src/shared/runtime-environment.js +32 -0
- package/src/shared/websocket.js +258 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { isInside, resolveProjectPath } = require("./path-policy");
|
|
4
|
+
|
|
5
|
+
const MAX_PROJECT_FILE_BYTES = 20 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
const PROJECT_FILE_MIME_TYPES = Object.freeze({
|
|
8
|
+
".csv": "text/csv; charset=utf-8",
|
|
9
|
+
".doc": "application/msword",
|
|
10
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
11
|
+
".gif": "image/gif",
|
|
12
|
+
".jpeg": "image/jpeg",
|
|
13
|
+
".jpg": "image/jpeg",
|
|
14
|
+
".json": "application/json; charset=utf-8",
|
|
15
|
+
".log": "text/plain; charset=utf-8",
|
|
16
|
+
".markdown": "text/markdown; charset=utf-8",
|
|
17
|
+
".md": "text/markdown; charset=utf-8",
|
|
18
|
+
".pdf": "application/pdf",
|
|
19
|
+
".png": "image/png",
|
|
20
|
+
".txt": "text/plain; charset=utf-8",
|
|
21
|
+
".webp": "image/webp"
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function httpError(message, statusCode) {
|
|
25
|
+
const error = new Error(message);
|
|
26
|
+
error.statusCode = statusCode;
|
|
27
|
+
return error;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cleanRequestedPath(rawPath) {
|
|
31
|
+
let value = String(rawPath || "").trim();
|
|
32
|
+
if (!value) {
|
|
33
|
+
throw httpError("文件路径不能为空。", 400);
|
|
34
|
+
}
|
|
35
|
+
if (value.startsWith("file://")) {
|
|
36
|
+
try {
|
|
37
|
+
value = new URL(value).pathname;
|
|
38
|
+
} catch {
|
|
39
|
+
throw httpError("文件路径无效。", 400);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
value = value.replace(/^["'`<]+|["'`>]+$/g, "");
|
|
43
|
+
try {
|
|
44
|
+
value = decodeURIComponent(value);
|
|
45
|
+
} catch {
|
|
46
|
+
// 已经是普通路径时保留原值。
|
|
47
|
+
}
|
|
48
|
+
return stripLineReference(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function stripLineReference(filePath) {
|
|
52
|
+
const extensions = Object.keys(PROJECT_FILE_MIME_TYPES)
|
|
53
|
+
.map((extension) => extension.slice(1).replace(".", "\\."))
|
|
54
|
+
.join("|");
|
|
55
|
+
return String(filePath || "").replace(new RegExp(`\\.(${extensions})(?::\\d+(?::\\d+)?)?$`, "i"), ".$1");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function supportedProjectFileMimeType(filePath) {
|
|
59
|
+
return PROJECT_FILE_MIME_TYPES[path.extname(filePath).toLowerCase()] || null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveProjectFilePath({ projectPath, requestedPath, allowedRoots }) {
|
|
63
|
+
const projectRoot = resolveProjectPath(projectPath, allowedRoots);
|
|
64
|
+
const cleaned = cleanRequestedPath(requestedPath);
|
|
65
|
+
const targetPath = path.isAbsolute(cleaned) || cleaned.startsWith("~")
|
|
66
|
+
? cleaned
|
|
67
|
+
: path.join(projectRoot, cleaned);
|
|
68
|
+
const resolved = resolveProjectPath(targetPath, allowedRoots);
|
|
69
|
+
if (!isInside(resolved, projectRoot)) {
|
|
70
|
+
throw httpError("只能打开当前项目目录内的文件。", 403);
|
|
71
|
+
}
|
|
72
|
+
return { projectRoot, resolved };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function assertRealFileInsideProject(filePath, projectRoot) {
|
|
76
|
+
if (!fs.existsSync(filePath)) {
|
|
77
|
+
throw httpError("文件不存在。", 404);
|
|
78
|
+
}
|
|
79
|
+
const stat = fs.statSync(filePath);
|
|
80
|
+
if (!stat.isFile()) {
|
|
81
|
+
throw httpError("目标路径不是文件。", 400);
|
|
82
|
+
}
|
|
83
|
+
const realProjectRoot = fs.realpathSync.native(projectRoot);
|
|
84
|
+
const realFilePath = fs.realpathSync.native(filePath);
|
|
85
|
+
if (!isInside(realFilePath, realProjectRoot)) {
|
|
86
|
+
throw httpError("只能打开当前项目目录内的文件。", 403);
|
|
87
|
+
}
|
|
88
|
+
if (stat.size > MAX_PROJECT_FILE_BYTES) {
|
|
89
|
+
throw httpError("文件过大,无法在浏览器中打开。", 413);
|
|
90
|
+
}
|
|
91
|
+
return stat;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readProjectFile({ projectPath, requestedPath, allowedRoots }) {
|
|
95
|
+
const { projectRoot, resolved } = resolveProjectFilePath({ projectPath, requestedPath, allowedRoots });
|
|
96
|
+
const mimeType = supportedProjectFileMimeType(resolved);
|
|
97
|
+
if (!mimeType) {
|
|
98
|
+
throw httpError("只支持打开常见文档和图片文件。", 415);
|
|
99
|
+
}
|
|
100
|
+
const stat = assertRealFileInsideProject(resolved, projectRoot);
|
|
101
|
+
return {
|
|
102
|
+
name: path.basename(resolved),
|
|
103
|
+
path: resolved,
|
|
104
|
+
mime_type: mimeType,
|
|
105
|
+
size: stat.size,
|
|
106
|
+
encoding: "base64",
|
|
107
|
+
content: fs.readFileSync(resolved).toString("base64")
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
MAX_PROJECT_FILE_BYTES,
|
|
113
|
+
PROJECT_FILE_MIME_TYPES,
|
|
114
|
+
cleanRequestedPath,
|
|
115
|
+
readProjectFile,
|
|
116
|
+
resolveProjectFilePath,
|
|
117
|
+
stripLineReference,
|
|
118
|
+
supportedProjectFileMimeType
|
|
119
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const PROVIDER_ALIASES = {
|
|
2
|
+
claude: "claude-code"
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
function normalizeProviderName(provider) {
|
|
6
|
+
const name = String(provider || "").trim();
|
|
7
|
+
return PROVIDER_ALIASES[name] || name;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function parseProviderList(value, fallback = ["mock"]) {
|
|
11
|
+
const providers = String(value || "")
|
|
12
|
+
.split(",")
|
|
13
|
+
.map(normalizeProviderName)
|
|
14
|
+
.filter(Boolean);
|
|
15
|
+
const unique = [];
|
|
16
|
+
for (const provider of providers) {
|
|
17
|
+
if (!unique.includes(provider)) {
|
|
18
|
+
unique.push(provider);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return unique.length ? unique : fallback;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
normalizeProviderName,
|
|
26
|
+
parseProviderList
|
|
27
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
function envFlagDisabled(value) {
|
|
2
|
+
return ["0", "false", "no", "off", "disabled"].includes(String(value || "").trim().toLowerCase());
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function supportsLocalRuntime({
|
|
6
|
+
env = typeof process !== "undefined" ? process.env : {},
|
|
7
|
+
platform = typeof process !== "undefined" ? process.platform : "",
|
|
8
|
+
hasProcess = typeof process !== "undefined"
|
|
9
|
+
} = {}) {
|
|
10
|
+
if (!hasProcess) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
if (envFlagDisabled(env.AGENT_ANYWHERE_LOCAL_RUNTIME)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (env.AGENT_ANYWHERE_DEPLOYMENT_TARGET === "cloudflare-workers") {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return Boolean(platform);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function runtimeSupport(options = {}) {
|
|
23
|
+
return {
|
|
24
|
+
local_runtime: supportsLocalRuntime(options),
|
|
25
|
+
gateway_runtime: true
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
runtimeSupport,
|
|
31
|
+
supportsLocalRuntime
|
|
32
|
+
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
const crypto = require("node:crypto");
|
|
2
|
+
const { EventEmitter } = require("node:events");
|
|
3
|
+
const net = require("node:net");
|
|
4
|
+
const tls = require("node:tls");
|
|
5
|
+
|
|
6
|
+
const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
7
|
+
|
|
8
|
+
function acceptKey(key) {
|
|
9
|
+
return crypto
|
|
10
|
+
.createHash("sha1")
|
|
11
|
+
.update(`${key}${WS_GUID}`)
|
|
12
|
+
.digest("base64");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function encodeFrame(text) {
|
|
16
|
+
const payload = Buffer.from(text, "utf8");
|
|
17
|
+
const header = [];
|
|
18
|
+
header.push(0x81);
|
|
19
|
+
if (payload.length < 126) {
|
|
20
|
+
header.push(payload.length);
|
|
21
|
+
} else if (payload.length <= 0xffff) {
|
|
22
|
+
header.push(126, (payload.length >> 8) & 0xff, payload.length & 0xff);
|
|
23
|
+
} else {
|
|
24
|
+
header.push(127, 0, 0, 0, 0);
|
|
25
|
+
header.push(
|
|
26
|
+
(payload.length >> 24) & 0xff,
|
|
27
|
+
(payload.length >> 16) & 0xff,
|
|
28
|
+
(payload.length >> 8) & 0xff,
|
|
29
|
+
payload.length & 0xff
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return Buffer.concat([Buffer.from(header), payload]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function decodeFrames(buffer) {
|
|
36
|
+
const messages = [];
|
|
37
|
+
let offset = 0;
|
|
38
|
+
let close = false;
|
|
39
|
+
const controlFrames = [];
|
|
40
|
+
|
|
41
|
+
while (buffer.length - offset >= 2) {
|
|
42
|
+
const first = buffer[offset];
|
|
43
|
+
const second = buffer[offset + 1];
|
|
44
|
+
const opcode = first & 0x0f;
|
|
45
|
+
const masked = Boolean(second & 0x80);
|
|
46
|
+
let payloadLength = second & 0x7f;
|
|
47
|
+
let headerLength = 2;
|
|
48
|
+
|
|
49
|
+
if (payloadLength === 126) {
|
|
50
|
+
if (buffer.length - offset < 4) {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
payloadLength = buffer.readUInt16BE(offset + 2);
|
|
54
|
+
headerLength = 4;
|
|
55
|
+
} else if (payloadLength === 127) {
|
|
56
|
+
if (buffer.length - offset < 10) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
const high = buffer.readUInt32BE(offset + 2);
|
|
60
|
+
const low = buffer.readUInt32BE(offset + 6);
|
|
61
|
+
payloadLength = high * 2 ** 32 + low;
|
|
62
|
+
headerLength = 10;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const maskLength = masked ? 4 : 0;
|
|
66
|
+
const frameLength = headerLength + maskLength + payloadLength;
|
|
67
|
+
if (buffer.length - offset < frameLength) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const maskOffset = offset + headerLength;
|
|
72
|
+
const payloadOffset = maskOffset + maskLength;
|
|
73
|
+
const payload = Buffer.from(buffer.subarray(payloadOffset, payloadOffset + payloadLength));
|
|
74
|
+
if (masked) {
|
|
75
|
+
const mask = buffer.subarray(maskOffset, maskOffset + 4);
|
|
76
|
+
for (let index = 0; index < payload.length; index += 1) {
|
|
77
|
+
payload[index] ^= mask[index % 4];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (opcode === 0x1) {
|
|
82
|
+
messages.push(payload.toString("utf8"));
|
|
83
|
+
} else if (opcode === 0x8) {
|
|
84
|
+
close = true;
|
|
85
|
+
} else if (opcode === 0x9) {
|
|
86
|
+
controlFrames.push({ opcode: 0x0a, payload });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
offset += frameLength;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
messages,
|
|
94
|
+
remaining: buffer.subarray(offset),
|
|
95
|
+
close,
|
|
96
|
+
controlFrames
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function encodeControlFrame(opcode, payload = Buffer.alloc(0)) {
|
|
101
|
+
return Buffer.concat([Buffer.from([0x80 | opcode, payload.length]), payload]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
class WebSocketPeer extends EventEmitter {
|
|
105
|
+
constructor(socket, initialBuffer = Buffer.alloc(0)) {
|
|
106
|
+
super();
|
|
107
|
+
this.socket = socket;
|
|
108
|
+
this.buffer = Buffer.alloc(0);
|
|
109
|
+
this.closed = false;
|
|
110
|
+
|
|
111
|
+
socket.on("data", (chunk) => this.handleData(chunk));
|
|
112
|
+
socket.on("close", () => this.closeFromSocket());
|
|
113
|
+
socket.on("error", (error) => this.emit("error", error));
|
|
114
|
+
if (initialBuffer.length > 0) {
|
|
115
|
+
this.handleData(initialBuffer);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
handleData(chunk) {
|
|
120
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
121
|
+
const decoded = decodeFrames(this.buffer);
|
|
122
|
+
this.buffer = decoded.remaining;
|
|
123
|
+
|
|
124
|
+
for (const controlFrame of decoded.controlFrames) {
|
|
125
|
+
this.socket.write(encodeControlFrame(controlFrame.opcode, controlFrame.payload));
|
|
126
|
+
}
|
|
127
|
+
for (const message of decoded.messages) {
|
|
128
|
+
this.emit("message", message);
|
|
129
|
+
}
|
|
130
|
+
if (decoded.close) {
|
|
131
|
+
this.close();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
sendJson(payload) {
|
|
136
|
+
this.send(JSON.stringify(payload));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
send(text) {
|
|
140
|
+
if (this.closed || this.socket.destroyed) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.socket.write(encodeFrame(text));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
close() {
|
|
147
|
+
if (this.closed) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
this.closed = true;
|
|
151
|
+
if (!this.socket.destroyed) {
|
|
152
|
+
this.socket.end(encodeControlFrame(0x8));
|
|
153
|
+
}
|
|
154
|
+
this.emit("close");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
closeFromSocket() {
|
|
158
|
+
if (this.closed) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
this.closed = true;
|
|
162
|
+
this.emit("close");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function upgradeToWebSocket(req, socket) {
|
|
167
|
+
const key = req.headers["sec-websocket-key"];
|
|
168
|
+
if (!key) {
|
|
169
|
+
socket.destroy();
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
socket.write([
|
|
174
|
+
"HTTP/1.1 101 Switching Protocols",
|
|
175
|
+
"Upgrade: websocket",
|
|
176
|
+
"Connection: Upgrade",
|
|
177
|
+
`Sec-WebSocket-Accept: ${acceptKey(key)}`,
|
|
178
|
+
"\r\n"
|
|
179
|
+
].join("\r\n"));
|
|
180
|
+
|
|
181
|
+
return new WebSocketPeer(socket);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function rejectUpgrade(socket, statusCode, message) {
|
|
185
|
+
socket.write(`HTTP/1.1 ${statusCode} ${message}\r\nConnection: close\r\n\r\n`);
|
|
186
|
+
socket.destroy();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function connectWebSocket(urlInput) {
|
|
190
|
+
const url = new URL(urlInput);
|
|
191
|
+
const secure = url.protocol === "wss:";
|
|
192
|
+
const port = Number(url.port || (secure ? 443 : 80));
|
|
193
|
+
const key = crypto.randomBytes(16).toString("base64");
|
|
194
|
+
const socket = secure
|
|
195
|
+
? tls.connect({ host: url.hostname, port, servername: url.hostname })
|
|
196
|
+
: net.connect({ host: url.hostname, port });
|
|
197
|
+
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
let buffer = Buffer.alloc(0);
|
|
200
|
+
const cleanup = () => {
|
|
201
|
+
socket.off("data", onData);
|
|
202
|
+
socket.off("error", onError);
|
|
203
|
+
socket.off("close", onClose);
|
|
204
|
+
};
|
|
205
|
+
const onError = (error) => {
|
|
206
|
+
cleanup();
|
|
207
|
+
reject(error);
|
|
208
|
+
};
|
|
209
|
+
const onClose = () => {
|
|
210
|
+
cleanup();
|
|
211
|
+
reject(new Error("websocket closed before handshake completed"));
|
|
212
|
+
};
|
|
213
|
+
const onData = (chunk) => {
|
|
214
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
215
|
+
const headerEnd = buffer.indexOf("\r\n\r\n");
|
|
216
|
+
if (headerEnd < 0) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const header = buffer.subarray(0, headerEnd).toString("utf8");
|
|
221
|
+
const rest = buffer.subarray(headerEnd + 4);
|
|
222
|
+
if (!/^HTTP\/1\.1 101\b/.test(header)) {
|
|
223
|
+
cleanup();
|
|
224
|
+
socket.destroy();
|
|
225
|
+
reject(new Error(`websocket upgrade failed: ${header.split(/\r?\n/)[0]}`));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
cleanup();
|
|
230
|
+
resolve(new WebSocketPeer(socket, rest));
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
socket.on("data", onData);
|
|
234
|
+
socket.on("error", onError);
|
|
235
|
+
socket.on("close", onClose);
|
|
236
|
+
socket.on(secure ? "secureConnect" : "connect", () => {
|
|
237
|
+
const path = `${url.pathname}${url.search}`;
|
|
238
|
+
socket.write([
|
|
239
|
+
`GET ${path} HTTP/1.1`,
|
|
240
|
+
`Host: ${url.host}`,
|
|
241
|
+
"Upgrade: websocket",
|
|
242
|
+
"Connection: Upgrade",
|
|
243
|
+
"Sec-WebSocket-Version: 13",
|
|
244
|
+
`Sec-WebSocket-Key: ${key}`,
|
|
245
|
+
"\r\n"
|
|
246
|
+
].join("\r\n"));
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = {
|
|
252
|
+
WebSocketPeer,
|
|
253
|
+
connectWebSocket,
|
|
254
|
+
decodeFrames,
|
|
255
|
+
encodeFrame,
|
|
256
|
+
rejectUpgrade,
|
|
257
|
+
upgradeToWebSocket
|
|
258
|
+
};
|