totemlayer 0.1.59 → 0.1.63
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 +4 -4
- package/bin/agent-memory.mjs +12 -1
- package/bin/agent-profile.mjs +16 -1
- package/bin/agent-runner.mjs +12 -3
- package/bin/inference-broker.mjs +361 -0
- package/bin/isolated-agent-entry.mjs +35 -0
- package/bin/model-adapter.mjs +244 -103
- package/bin/network-runtime.mjs +19 -39
- package/bin/network.mjs +2 -2
- package/bin/process-isolation.mjs +214 -0
- package/bin/sdk/inspector.js +1 -1
- package/bin/totem.mjs +2 -2
- package/bin/worker-core/adapters.mjs +26 -9
- package/bin/worker-core/errors.mjs +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ To configure execution later, rerun setup with `--provider` and the adapter's se
|
|
|
27
27
|
|
|
28
28
|
## Connect with automatic listener setup
|
|
29
29
|
|
|
30
|
-
Configure a local MCP server using command `npx` and arguments `--yes`, `--ignore-scripts`, `--package=https://totemlayer.com/downloads/totemlayer-0.1.
|
|
30
|
+
Configure a local MCP server using command `npx` and arguments `--yes`, `--ignore-scripts`, `--package=https://totemlayer.com/downloads/totemlayer-0.1.63.tgz`, `totem`, `connect`, `--provider`, and `codex` (or `claude` for Claude Code). The host runs the connection itself; the owner does not run a separate listener command. Node.js 22.14 or newer and the selected signed-in client must be available.
|
|
31
31
|
|
|
32
32
|
During browser sign-in, `totem_get_connection_status` reports progress. Normal tools appear after the approved identity is pinned privately. Codex verifies identity, startup and the saved chat callback before receiving work; reconnect preserves bindings and pauses. Missing capabilities leave work queued. A remote HTTP connector cannot install local software.
|
|
33
33
|
|
|
@@ -45,7 +45,7 @@ For a hosted connection, add these settings to your client's MCP configuration:
|
|
|
45
45
|
For a local stdio connection, install the CLI with Node.js 22.14 or newer:
|
|
46
46
|
|
|
47
47
|
```sh
|
|
48
|
-
npm install --global --ignore-scripts https://totemlayer.com/downloads/totemlayer-0.1.
|
|
48
|
+
npm install --global --ignore-scripts https://totemlayer.com/downloads/totemlayer-0.1.63.tgz
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
Save your agent's connection key in a private file. Protect it with mode 0600 on macOS/Linux or your user's filesystem ACL on Windows. Configure these fields in your client's MCP settings:
|
|
@@ -64,7 +64,7 @@ Reload the client if needed, discover the available tools, and call `totem_get_i
|
|
|
64
64
|
Run the guided installer on the host where your agent will execute work:
|
|
65
65
|
|
|
66
66
|
```sh
|
|
67
|
-
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.
|
|
67
|
+
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.63.tgz totem install
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
The installer opens Totem sign-in and saves the approved connection privately. It checks your configured agent runtime, configures supported clients, and starts the listener when an authorized executor is available. The supervised listener is enabled by default for this setup and starts when you sign in to the computer on hosts with a supported user service manager. It recovers after a process exit and retries temporary connection failures. An explicit stop stays paused across restarts.
|
|
@@ -74,7 +74,7 @@ For manual setup, use `totem mcp install` with a supported client and the privat
|
|
|
74
74
|
For a host without a browser:
|
|
75
75
|
|
|
76
76
|
```sh
|
|
77
|
-
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.
|
|
77
|
+
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.63.tgz totem login --device-login --no-browser
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
Open the printed link on your own device and compare the connection code. Preserve existing MCP entries; inspect conflicts before replacing them. If an executor is unavailable, the saved connection remains available while you complete runtime setup.
|
package/bin/agent-memory.mjs
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/** Explicit owner restrictions apply to tools, consolidation, and server checkpoints. */
|
|
2
|
+
export function allowsMemoryWrites(text) {
|
|
3
|
+
const value = String(text ?? "")
|
|
4
|
+
.normalize("NFKC")
|
|
5
|
+
.replace(/\p{Cf}/gu, "")
|
|
6
|
+
.replace(/\s+/gu, " ");
|
|
7
|
+
return !/\bread[\s-]*only\b|\b(?:do\s+not|don['’]t|never|no)\b[^.!?;\n]{0,100}\b(?:memory|memories|remember|save|store)\b|\b(?:keep|leave|preserve)\b[^.!?;\n]{0,40}\bmemor(?:y|ies)\b[^.!?;\n]{0,30}\bunchanged\b/i.test(
|
|
8
|
+
value,
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
1
12
|
/** Fetch before each model turn. Missing support on an older host is explicit and compatible. */
|
|
2
13
|
export async function agentMemoryPrompt(
|
|
3
14
|
client,
|
|
@@ -44,7 +55,7 @@ export async function learnAgentMemory({
|
|
|
44
55
|
signal,
|
|
45
56
|
emit = () => {},
|
|
46
57
|
}) {
|
|
47
|
-
if (!client.memory?.get || !client.memory?.update) return;
|
|
58
|
+
if (!allowsMemoryWrites(turn.user) || !client.memory?.get || !client.memory?.update) return;
|
|
48
59
|
const stateKey = `memory:${key}`;
|
|
49
60
|
let state = await journal.read(stateKey);
|
|
50
61
|
if (state?.done) return;
|
package/bin/agent-profile.mjs
CHANGED
|
@@ -186,6 +186,10 @@ async function normalizedSettings(input, directory) {
|
|
|
186
186
|
"commandArgs",
|
|
187
187
|
"agentUrl",
|
|
188
188
|
"agentTokenEnv",
|
|
189
|
+
"codePaths",
|
|
190
|
+
"inferenceOrigin",
|
|
191
|
+
"inferenceProtocol",
|
|
192
|
+
"inferenceTokenEnv",
|
|
189
193
|
"ollamaUrl",
|
|
190
194
|
"nimUrl",
|
|
191
195
|
"codexThreadId",
|
|
@@ -299,7 +303,18 @@ async function normalizedSettings(input, directory) {
|
|
|
299
303
|
repositoryRoots,
|
|
300
304
|
stateDirectory,
|
|
301
305
|
...Object.fromEntries(
|
|
302
|
-
[
|
|
306
|
+
[
|
|
307
|
+
"command",
|
|
308
|
+
"commandArgs",
|
|
309
|
+
"agentUrl",
|
|
310
|
+
"agentTokenEnv",
|
|
311
|
+
"codePaths",
|
|
312
|
+
"inferenceOrigin",
|
|
313
|
+
"inferenceProtocol",
|
|
314
|
+
"inferenceTokenEnv",
|
|
315
|
+
"ollamaUrl",
|
|
316
|
+
"nimUrl",
|
|
317
|
+
]
|
|
303
318
|
.filter((key) => input[key] !== undefined)
|
|
304
319
|
.map((key) => [key, input[key]]),
|
|
305
320
|
),
|
package/bin/agent-runner.mjs
CHANGED
|
@@ -437,7 +437,16 @@ export async function runAgentSession(profile, options = {}) {
|
|
|
437
437
|
})),
|
|
438
438
|
provider: profile.client ?? profile.provider,
|
|
439
439
|
adapterSettings: Object.fromEntries(
|
|
440
|
-
[
|
|
440
|
+
[
|
|
441
|
+
"command",
|
|
442
|
+
"commandArgs",
|
|
443
|
+
"agentUrl",
|
|
444
|
+
"agentTokenEnv",
|
|
445
|
+
"codePaths",
|
|
446
|
+
"inferenceOrigin",
|
|
447
|
+
"inferenceProtocol",
|
|
448
|
+
"inferenceTokenEnv",
|
|
449
|
+
]
|
|
441
450
|
.filter((key) => profile[key] !== undefined)
|
|
442
451
|
.map((key) => [key, profile[key]]),
|
|
443
452
|
),
|
|
@@ -593,8 +602,8 @@ export async function agentStartMain(args, options = {}) {
|
|
|
593
602
|
.map((name) => [name, environment[name]]),
|
|
594
603
|
),
|
|
595
604
|
});
|
|
596
|
-
|
|
597
|
-
env[
|
|
605
|
+
for (const name of [profile.agentTokenEnv, profile.inferenceTokenEnv])
|
|
606
|
+
if (name && environment[name]) env[name] = environment[name];
|
|
598
607
|
delete env.TOTEM_NETWORK_API_KEY_FILE;
|
|
599
608
|
delete env.TOTEM_BASE_URL;
|
|
600
609
|
if (["local", "hosted"].includes(environment.TOTEM_RUNTIME_HOST_KIND))
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { readPrivateRuntimeJson } from "./mission-agent-session.mjs";
|
|
8
|
+
|
|
9
|
+
export function isolationError(code, message) {
|
|
10
|
+
return Object.assign(new Error(message), { status: 422, code });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function privateAuth(path) {
|
|
14
|
+
try {
|
|
15
|
+
return await readPrivateRuntimeJson(path, 32768);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error.code === "ENOENT") return null;
|
|
18
|
+
throw isolationError(
|
|
19
|
+
"PROVIDER_AUTH_REQUIRED",
|
|
20
|
+
"The provider's private authentication needs attention.",
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Provider secrets stay in this parent process; never copy them into an agent home. */
|
|
26
|
+
export async function inferenceAuthority(provider, environment = process.env, options = {}) {
|
|
27
|
+
if (provider === "codex") {
|
|
28
|
+
if (environment.OPENAI_API_KEY)
|
|
29
|
+
return {
|
|
30
|
+
protocol: "responses",
|
|
31
|
+
origin: "https://api.openai.com",
|
|
32
|
+
prefix: "/v1",
|
|
33
|
+
headers: { authorization: `Bearer ${environment.OPENAI_API_KEY}` },
|
|
34
|
+
};
|
|
35
|
+
const auth = await privateAuth(
|
|
36
|
+
join(environment.CODEX_HOME ?? join(environment.HOME ?? homedir(), ".codex"), "auth.json"),
|
|
37
|
+
);
|
|
38
|
+
if (auth?.OPENAI_API_KEY)
|
|
39
|
+
return {
|
|
40
|
+
protocol: "responses",
|
|
41
|
+
origin: "https://api.openai.com",
|
|
42
|
+
prefix: "/v1",
|
|
43
|
+
headers: { authorization: `Bearer ${auth.OPENAI_API_KEY}` },
|
|
44
|
+
};
|
|
45
|
+
if (typeof auth?.tokens?.access_token === "string" && auth.tokens.access_token)
|
|
46
|
+
return {
|
|
47
|
+
protocol: "responses",
|
|
48
|
+
origin: "https://chatgpt.com",
|
|
49
|
+
prefix: "/backend-api/codex",
|
|
50
|
+
headers: {
|
|
51
|
+
authorization: `Bearer ${auth.tokens.access_token}`,
|
|
52
|
+
...(typeof auth.tokens.account_id === "string"
|
|
53
|
+
? { "chatgpt-account-id": auth.tokens.account_id }
|
|
54
|
+
: {}),
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (provider === "claude") {
|
|
59
|
+
if (environment.ANTHROPIC_API_KEY)
|
|
60
|
+
return {
|
|
61
|
+
protocol: "anthropic",
|
|
62
|
+
origin: "https://api.anthropic.com",
|
|
63
|
+
prefix: "/v1",
|
|
64
|
+
headers: { "x-api-key": environment.ANTHROPIC_API_KEY, "anthropic-version": "2023-06-01" },
|
|
65
|
+
};
|
|
66
|
+
let token = environment.CLAUDE_CODE_OAUTH_TOKEN;
|
|
67
|
+
if (!token) {
|
|
68
|
+
const auth = await privateAuth(
|
|
69
|
+
join(
|
|
70
|
+
environment.CLAUDE_CONFIG_DIR ?? join(environment.HOME ?? homedir(), ".claude"),
|
|
71
|
+
".credentials.json",
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
token = auth?.claudeAiOauth?.accessToken;
|
|
75
|
+
}
|
|
76
|
+
if (!token && (options.platform ?? process.platform) === "darwin") {
|
|
77
|
+
try {
|
|
78
|
+
// Keychain output stays in the trusted parent. No refresh credential is
|
|
79
|
+
// copied into the child, its files, or inference request bodies.
|
|
80
|
+
const readKeychain =
|
|
81
|
+
options.readKeychain ??
|
|
82
|
+
(async () => {
|
|
83
|
+
const result = await promisify(execFile)(
|
|
84
|
+
"/usr/bin/security",
|
|
85
|
+
["find-generic-password", "-s", "Claude Code-credentials", "-w"],
|
|
86
|
+
{
|
|
87
|
+
timeout: 10000,
|
|
88
|
+
maxBuffer: 32768,
|
|
89
|
+
env: { PATH: "/usr/bin:/bin" },
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
return result.stdout;
|
|
93
|
+
});
|
|
94
|
+
token = JSON.parse(await readKeychain())?.claudeAiOauth?.accessToken;
|
|
95
|
+
} catch {
|
|
96
|
+
/* Missing, locked, or unavailable credentials need owner attention. */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (typeof token === "string" && token)
|
|
100
|
+
return {
|
|
101
|
+
protocol: "anthropic",
|
|
102
|
+
origin: "https://api.anthropic.com",
|
|
103
|
+
prefix: "/v1",
|
|
104
|
+
headers: {
|
|
105
|
+
authorization: `Bearer ${token}`,
|
|
106
|
+
"anthropic-version": "2023-06-01",
|
|
107
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
throw isolationError(
|
|
112
|
+
"PROVIDER_AUTH_REQUIRED",
|
|
113
|
+
"Configure the selected provider's API key or supported private login in the parent runtime. Provider credentials are never passed to the isolated agent.",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function plainText(value, type) {
|
|
118
|
+
if (typeof value === "string") return value;
|
|
119
|
+
if (!Array.isArray(value))
|
|
120
|
+
throw isolationError("INFERENCE_REQUEST_DENIED", "Only text inference is allowed.");
|
|
121
|
+
return value.map((item) => {
|
|
122
|
+
if (
|
|
123
|
+
!item ||
|
|
124
|
+
!["text", "input_text", "output_text"].includes(item.type) ||
|
|
125
|
+
typeof item.text !== "string"
|
|
126
|
+
)
|
|
127
|
+
throw isolationError("INFERENCE_REQUEST_DENIED", "Only text inference is allowed.");
|
|
128
|
+
return { type: type ?? item.type, text: item.text };
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Reconstruct the data-plane request: no server tools, uploads, URLs, or account history. */
|
|
133
|
+
export function inferenceRequest(protocol, input, selectedModel) {
|
|
134
|
+
if (
|
|
135
|
+
!input ||
|
|
136
|
+
typeof input !== "object" ||
|
|
137
|
+
Array.isArray(input) ||
|
|
138
|
+
input.previous_response_id ||
|
|
139
|
+
input.conversation ||
|
|
140
|
+
input.prompt ||
|
|
141
|
+
input.background
|
|
142
|
+
)
|
|
143
|
+
throw isolationError(
|
|
144
|
+
"INFERENCE_REQUEST_DENIED",
|
|
145
|
+
"Persistent provider state is unavailable in this session.",
|
|
146
|
+
);
|
|
147
|
+
if (
|
|
148
|
+
typeof input.model !== "string" ||
|
|
149
|
+
!input.model ||
|
|
150
|
+
(selectedModel && selectedModel !== "default" && input.model !== selectedModel)
|
|
151
|
+
)
|
|
152
|
+
throw isolationError(
|
|
153
|
+
"INFERENCE_REQUEST_DENIED",
|
|
154
|
+
"The requested model is outside this session.",
|
|
155
|
+
);
|
|
156
|
+
const body = { model: input.model, stream: input.stream === true };
|
|
157
|
+
for (const key of [
|
|
158
|
+
"temperature",
|
|
159
|
+
"top_p",
|
|
160
|
+
"max_tokens",
|
|
161
|
+
"max_output_tokens",
|
|
162
|
+
"reasoning",
|
|
163
|
+
"text",
|
|
164
|
+
"output_config",
|
|
165
|
+
"thinking",
|
|
166
|
+
])
|
|
167
|
+
if (input[key] !== undefined) body[key] = input[key];
|
|
168
|
+
if (protocol === "responses") {
|
|
169
|
+
body.store = false;
|
|
170
|
+
body.tools = [];
|
|
171
|
+
body.tool_choice = "none";
|
|
172
|
+
if (input.instructions !== undefined) {
|
|
173
|
+
if (typeof input.instructions !== "string")
|
|
174
|
+
throw isolationError("INFERENCE_REQUEST_DENIED", "Text instructions are required.");
|
|
175
|
+
body.instructions = input.instructions;
|
|
176
|
+
}
|
|
177
|
+
if (typeof input.input === "string") body.input = input.input;
|
|
178
|
+
// New Codex clients place tool declarations in input as well as tools.
|
|
179
|
+
// Discard the catalog; calls, results and provider handles still fail below.
|
|
180
|
+
else if (Array.isArray(input.input))
|
|
181
|
+
body.input = input.input
|
|
182
|
+
.filter((message) => message?.type !== "additional_tools")
|
|
183
|
+
.map((message) => {
|
|
184
|
+
if (
|
|
185
|
+
!message ||
|
|
186
|
+
(message.type && message.type !== "message") ||
|
|
187
|
+
!["user", "assistant", "system", "developer"].includes(message.role)
|
|
188
|
+
)
|
|
189
|
+
throw isolationError(
|
|
190
|
+
"INFERENCE_REQUEST_DENIED",
|
|
191
|
+
"Provider objects and tools cannot enter this session.",
|
|
192
|
+
);
|
|
193
|
+
return { type: "message", role: message.role, content: plainText(message.content) };
|
|
194
|
+
});
|
|
195
|
+
else throw isolationError("INFERENCE_REQUEST_DENIED", "Text input is required.");
|
|
196
|
+
} else if (protocol === "anthropic" || protocol === "chat") {
|
|
197
|
+
body.tools = [];
|
|
198
|
+
if (protocol === "anthropic" && input.system !== undefined)
|
|
199
|
+
body.system = plainText(input.system, "text");
|
|
200
|
+
if (!Array.isArray(input.messages))
|
|
201
|
+
throw isolationError("INFERENCE_REQUEST_DENIED", "Text messages are required.");
|
|
202
|
+
body.messages = input.messages.map((message) => {
|
|
203
|
+
if (
|
|
204
|
+
!(
|
|
205
|
+
protocol === "anthropic"
|
|
206
|
+
? ["user", "assistant"]
|
|
207
|
+
: ["user", "assistant", "system", "developer"]
|
|
208
|
+
).includes(message?.role)
|
|
209
|
+
)
|
|
210
|
+
throw isolationError("INFERENCE_REQUEST_DENIED", "Unsupported message role.");
|
|
211
|
+
return { role: message.role, content: plainText(message.content, "text") };
|
|
212
|
+
});
|
|
213
|
+
body.max_tokens ??= 4096;
|
|
214
|
+
} else throw isolationError("INFERENCE_REQUEST_DENIED", "Unsupported inference protocol.");
|
|
215
|
+
return body;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Each broker is private, short-lived, pinned to one account, and has no tool API. */
|
|
219
|
+
export async function startInferenceBroker({
|
|
220
|
+
authority,
|
|
221
|
+
model,
|
|
222
|
+
socketPath,
|
|
223
|
+
signal,
|
|
224
|
+
fetcher = fetch,
|
|
225
|
+
maxRequests = 12,
|
|
226
|
+
timeoutMs = 120000,
|
|
227
|
+
}) {
|
|
228
|
+
const origin = new URL(authority.origin);
|
|
229
|
+
if (
|
|
230
|
+
origin.protocol !== "https:" ||
|
|
231
|
+
origin.username ||
|
|
232
|
+
origin.password ||
|
|
233
|
+
origin.pathname !== "/" ||
|
|
234
|
+
origin.search ||
|
|
235
|
+
origin.hash ||
|
|
236
|
+
!["responses", "anthropic", "chat"].includes(authority.protocol) ||
|
|
237
|
+
!/^\/[A-Za-z0-9/_-]+$/.test(authority.prefix)
|
|
238
|
+
)
|
|
239
|
+
throw isolationError(
|
|
240
|
+
"INFERENCE_CONFIG_INVALID",
|
|
241
|
+
"Choose a pinned HTTPS inference origin and API prefix.",
|
|
242
|
+
);
|
|
243
|
+
const token = randomBytes(32).toString("hex");
|
|
244
|
+
const stop = new AbortController();
|
|
245
|
+
const sessionSignal = signal ? AbortSignal.any([signal, stop.signal]) : stop.signal;
|
|
246
|
+
let requests = 0;
|
|
247
|
+
const server = createServer(async (request, response) => {
|
|
248
|
+
const deny = (status = 403) => {
|
|
249
|
+
if (!response.headersSent) response.writeHead(status, { "content-type": "application/json" });
|
|
250
|
+
response.end('{"error":"INFERENCE_REQUEST_DENIED"}');
|
|
251
|
+
};
|
|
252
|
+
try {
|
|
253
|
+
const supplied =
|
|
254
|
+
request.headers.authorization?.replace(/^Bearer /u, "") ?? request.headers["x-api-key"];
|
|
255
|
+
if (
|
|
256
|
+
typeof supplied !== "string" ||
|
|
257
|
+
Buffer.byteLength(supplied) !== Buffer.byteLength(token) ||
|
|
258
|
+
!timingSafeEqual(Buffer.from(supplied), Buffer.from(token)) ||
|
|
259
|
+
request.method !== "POST" ||
|
|
260
|
+
!request.url?.startsWith("/") ||
|
|
261
|
+
request.url.startsWith("//") ||
|
|
262
|
+
++requests > maxRequests ||
|
|
263
|
+
sessionSignal.aborted
|
|
264
|
+
)
|
|
265
|
+
return deny();
|
|
266
|
+
const url = new URL(request.url, "http://127.0.0.1");
|
|
267
|
+
const operation =
|
|
268
|
+
authority.protocol === "responses"
|
|
269
|
+
? url.pathname === "/v1/responses"
|
|
270
|
+
? "/responses"
|
|
271
|
+
: null
|
|
272
|
+
: authority.protocol === "chat"
|
|
273
|
+
? url.pathname === "/v1/chat/completions"
|
|
274
|
+
? "/chat/completions"
|
|
275
|
+
: null
|
|
276
|
+
: ["/v1/messages", "/v1/messages/count_tokens"].includes(url.pathname)
|
|
277
|
+
? url.pathname.slice(3)
|
|
278
|
+
: null;
|
|
279
|
+
if (!operation || (url.search && url.search !== "?beta=true")) return deny();
|
|
280
|
+
const chunks = [];
|
|
281
|
+
let size = 0;
|
|
282
|
+
for await (const chunk of request) {
|
|
283
|
+
if ((size += chunk.length) > 2 * 1024 * 1024) return deny(413);
|
|
284
|
+
chunks.push(chunk);
|
|
285
|
+
}
|
|
286
|
+
const body = inferenceRequest(
|
|
287
|
+
authority.protocol,
|
|
288
|
+
JSON.parse(Buffer.concat(chunks).toString("utf8")),
|
|
289
|
+
model,
|
|
290
|
+
);
|
|
291
|
+
if (operation.endsWith("/count_tokens")) {
|
|
292
|
+
for (const name of [
|
|
293
|
+
"max_tokens",
|
|
294
|
+
"max_output_tokens",
|
|
295
|
+
"stream",
|
|
296
|
+
"output_config",
|
|
297
|
+
"temperature",
|
|
298
|
+
"top_p",
|
|
299
|
+
])
|
|
300
|
+
delete body[name];
|
|
301
|
+
}
|
|
302
|
+
const upstream = await fetcher(
|
|
303
|
+
`${origin.origin}${authority.prefix}${operation}${url.search}`,
|
|
304
|
+
{
|
|
305
|
+
method: "POST",
|
|
306
|
+
redirect: "error",
|
|
307
|
+
signal: AbortSignal.any([sessionSignal, AbortSignal.timeout(timeoutMs)]),
|
|
308
|
+
headers: { "content-type": "application/json", ...authority.headers },
|
|
309
|
+
body: JSON.stringify(body),
|
|
310
|
+
},
|
|
311
|
+
);
|
|
312
|
+
if (!upstream.ok) {
|
|
313
|
+
await upstream.body?.cancel();
|
|
314
|
+
return deny(502);
|
|
315
|
+
}
|
|
316
|
+
response.writeHead(200, {
|
|
317
|
+
"content-type": upstream.headers.get("content-type") ?? "application/json",
|
|
318
|
+
"cache-control": "no-store",
|
|
319
|
+
});
|
|
320
|
+
let received = 0;
|
|
321
|
+
for await (const chunk of upstream.body) {
|
|
322
|
+
if ((received += chunk.length) > 8 * 1024 * 1024) {
|
|
323
|
+
response.destroy();
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
if (!response.write(chunk))
|
|
327
|
+
await new Promise((resolve) => {
|
|
328
|
+
response.once("drain", resolve);
|
|
329
|
+
response.once("close", resolve);
|
|
330
|
+
});
|
|
331
|
+
if (response.destroyed) break;
|
|
332
|
+
}
|
|
333
|
+
response.end();
|
|
334
|
+
} catch {
|
|
335
|
+
if (response.headersSent) response.destroy();
|
|
336
|
+
else deny(502);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
server.requestTimeout = Math.min(timeoutMs, 120000);
|
|
340
|
+
server.maxConnections = 8;
|
|
341
|
+
server.headersTimeout = 10000;
|
|
342
|
+
server.on("connect", (_request, socket) => socket.destroy());
|
|
343
|
+
server.on("upgrade", (_request, socket) => socket.destroy());
|
|
344
|
+
await new Promise((resolve, reject) => {
|
|
345
|
+
server.once("error", reject);
|
|
346
|
+
if (socketPath) server.listen(socketPath, resolve);
|
|
347
|
+
else server.listen(0, "127.0.0.1", resolve);
|
|
348
|
+
});
|
|
349
|
+
const address = server.address();
|
|
350
|
+
return {
|
|
351
|
+
token,
|
|
352
|
+
socketPath,
|
|
353
|
+
port: typeof address === "object" ? address.port : null,
|
|
354
|
+
baseUrl: typeof address === "object" ? `http://127.0.0.1:${address.port}` : null,
|
|
355
|
+
async close() {
|
|
356
|
+
stop.abort();
|
|
357
|
+
server.closeAllConnections();
|
|
358
|
+
await new Promise((resolve) => server.close(resolve));
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Linux's private network namespace can reach only this relay and the parent broker socket.
|
|
2
|
+
import { createServer } from "node:net";
|
|
3
|
+
import { createConnection } from "node:net";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const config = JSON.parse(process.env.AGENT_SESSION_CONFIG);
|
|
7
|
+
const server = createServer();
|
|
8
|
+
const sockets = new Set();
|
|
9
|
+
server.on("connection", (socket) => {
|
|
10
|
+
sockets.add(socket);
|
|
11
|
+
const parent = createConnection(config.socketPath);
|
|
12
|
+
socket.pipe(parent).pipe(socket);
|
|
13
|
+
parent.on("error", () => socket.destroy());
|
|
14
|
+
socket.on("error", () => parent.destroy());
|
|
15
|
+
socket.on("close", () => {
|
|
16
|
+
sockets.delete(socket);
|
|
17
|
+
parent.destroy();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
await new Promise((resolve) => server.listen(16443, "127.0.0.1", resolve));
|
|
21
|
+
const env = { ...process.env };
|
|
22
|
+
delete env.AGENT_SESSION_CONFIG;
|
|
23
|
+
const child = spawn(config.command, config.args, {
|
|
24
|
+
env,
|
|
25
|
+
cwd: config.cwd,
|
|
26
|
+
stdio: "inherit",
|
|
27
|
+
shell: false,
|
|
28
|
+
});
|
|
29
|
+
const stop = (code) => {
|
|
30
|
+
process.exitCode = code ?? 1;
|
|
31
|
+
for (const socket of sockets) socket.destroy();
|
|
32
|
+
server.close();
|
|
33
|
+
};
|
|
34
|
+
child.on("error", () => stop(1));
|
|
35
|
+
child.on("exit", stop);
|