@botlearn-course/daemon 0.0.10 → 0.0.11
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
CHANGED
|
@@ -38,9 +38,10 @@ npx --yes --package @botlearn-course/daemon@latest botlearn-course-daemon course
|
|
|
38
38
|
|
|
39
39
|
包内还包含供 BotLearn 托管 E2B Template 使用的内部命令
|
|
40
40
|
`botlearn-sandbox-supervisor agent-service session`。它不是 BYOA 用户入口:生产环境只允许由
|
|
41
|
-
Agent Service 以固定 argv 启动,通过 stdin 接收一次性 bootstrap
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
Agent Service 以固定 argv 启动,通过 stdin 接收一次性 bootstrap。E2B 中 daemon/runtime 分别
|
|
42
|
+
降权到 `botlearn-control`/`user` UID;继承 `NoNewPrivs=1`、无法使用 sudo 的平台保留 root control
|
|
43
|
+
daemon,由 root-owned 固定 launcher 清空附加组后直接降权为 `user`。两条路径都为 DeepSeek
|
|
44
|
+
设置 `no_new_privs`,runtime 用户本身没有 sudo 权限。托管启动链只执行
|
|
44
45
|
`/opt` 下的固定 Node、supervisor、daemon、launcher 与 DeepSeek 文件,不信任 E2B 会开放给 runtime
|
|
45
46
|
写入的 `/usr/local/bin`。
|
|
46
47
|
`agent-service session --bootstrap-stdin` 同样属于受 supervisor 保护的内部协议,不应直接暴露给
|
package/dist/runtime-env.d.ts
CHANGED
|
@@ -14,14 +14,16 @@ export declare function runtimeChildIdentity(env?: NodeJS.ProcessEnv): {
|
|
|
14
14
|
gid?: number;
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
|
-
* Build the fixed control-to-runtime privilege boundary used by
|
|
17
|
+
* Build the fixed control-to-runtime privilege boundary used by managed sandboxes.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* E2B runs the daemon as botlearn-control and grants it one fixed sudoers command.
|
|
20
|
+
* Platforms that inherit no_new_privs cannot use sudo, so their root supervisor daemon
|
|
21
|
+
* invokes the same immutable launcher directly; the launcher clears supplementary groups
|
|
22
|
+
* and performs the one-way uid/gid drop. BYOA retains direct-spawn behavior.
|
|
23
23
|
*/
|
|
24
|
-
export declare function runtimeChildLaunch(binary: string, args: string[], env?: NodeJS.ProcessEnv
|
|
24
|
+
export declare function runtimeChildLaunch(binary: string, args: string[], env?: NodeJS.ProcessEnv, options?: {
|
|
25
|
+
currentUid?: number;
|
|
26
|
+
}): {
|
|
25
27
|
binary: string;
|
|
26
28
|
args: string[];
|
|
27
29
|
identity: {
|
package/dist/runtime-env.js
CHANGED
|
@@ -16,6 +16,7 @@ const AGENT_SERVICE_SUPERVISOR_ENV_KEYS = [
|
|
|
16
16
|
"BOTLEARN_RUNTIME_GROUP",
|
|
17
17
|
"BOTLEARN_RUNTIME_HOME",
|
|
18
18
|
"BOTLEARN_RUNTIME_LAUNCHER",
|
|
19
|
+
"BOTLEARN_RUNTIME_LAUNCH_MODE",
|
|
19
20
|
"BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT",
|
|
20
21
|
"BOTLEARN_AGENT_SERVICE_PROFILE_ROOT",
|
|
21
22
|
];
|
|
@@ -27,6 +28,8 @@ const RUNTIME_USER_PATTERN = /^[a-z_][a-z0-9_-]{0,31}$/;
|
|
|
27
28
|
const RUNTIME_SUDO_BINARY = "/usr/bin/sudo";
|
|
28
29
|
const MANAGED_RUNTIME_LAUNCHER = "/opt/botlearn/bin/botlearn-runtime-launcher";
|
|
29
30
|
const MANAGED_RUNTIME_BINARY = "/opt/deepseek-tui/0.8.39/bin/deepseek";
|
|
31
|
+
const MANAGED_RUNTIME_UID = 1001;
|
|
32
|
+
const MANAGED_RUNTIME_GID = 2000;
|
|
30
33
|
/** Remove Course control-plane coordinates before any model/runtime child is created. */
|
|
31
34
|
export function clearAgentServiceControlEnv(env = process.env) {
|
|
32
35
|
for (const key of AGENT_SERVICE_CONTROL_ENV_KEYS)
|
|
@@ -84,16 +87,41 @@ export function runtimeChildIdentity(env = process.env) {
|
|
|
84
87
|
return { uid, gid };
|
|
85
88
|
}
|
|
86
89
|
/**
|
|
87
|
-
* Build the fixed control-to-runtime privilege boundary used by
|
|
90
|
+
* Build the fixed control-to-runtime privilege boundary used by managed sandboxes.
|
|
88
91
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
92
|
+
* E2B runs the daemon as botlearn-control and grants it one fixed sudoers command.
|
|
93
|
+
* Platforms that inherit no_new_privs cannot use sudo, so their root supervisor daemon
|
|
94
|
+
* invokes the same immutable launcher directly; the launcher clears supplementary groups
|
|
95
|
+
* and performs the one-way uid/gid drop. BYOA retains direct-spawn behavior.
|
|
93
96
|
*/
|
|
94
|
-
export function runtimeChildLaunch(binary, args, env = process.env) {
|
|
97
|
+
export function runtimeChildLaunch(binary, args, env = process.env, options = {}) {
|
|
98
|
+
const launchMode = env.BOTLEARN_RUNTIME_LAUNCH_MODE?.trim();
|
|
99
|
+
if (launchMode === "direct-uid") {
|
|
100
|
+
const currentUid = options.currentUid ?? process.getuid?.();
|
|
101
|
+
if (currentUid !== 0) {
|
|
102
|
+
throw new Error("direct managed runtime launch requires a root supervisor daemon");
|
|
103
|
+
}
|
|
104
|
+
if (binary !== MANAGED_RUNTIME_BINARY) {
|
|
105
|
+
throw new Error("invalid supervisor-provided runtime binary");
|
|
106
|
+
}
|
|
107
|
+
const identity = runtimeChildIdentity(env);
|
|
108
|
+
if (identity.uid !== MANAGED_RUNTIME_UID || identity.gid !== MANAGED_RUNTIME_GID) {
|
|
109
|
+
throw new Error("direct managed runtime launch requires a fixed runtime uid and gid");
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
binary: MANAGED_RUNTIME_LAUNCHER,
|
|
113
|
+
args: [binary, ...args],
|
|
114
|
+
identity: {},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (launchMode !== undefined && launchMode !== "sudo") {
|
|
118
|
+
throw new Error("invalid supervisor-provided runtime launch mode");
|
|
119
|
+
}
|
|
95
120
|
const runtimeUser = env.BOTLEARN_RUNTIME_USER;
|
|
96
121
|
if (runtimeUser === undefined) {
|
|
122
|
+
if (launchMode === "sudo") {
|
|
123
|
+
throw new Error("sudo managed runtime launch requires a fixed runtime user");
|
|
124
|
+
}
|
|
97
125
|
return { binary, args, identity: runtimeChildIdentity(env) };
|
|
98
126
|
}
|
|
99
127
|
if (!RUNTIME_USER_PATTERN.test(runtimeUser)) {
|
|
@@ -3,6 +3,7 @@ import { existsSync, realpathSync } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import net from "node:net";
|
|
5
5
|
import { MAX_PROGRESS_EVENTS_PER_ATTEMPT } from "../mcp/report-progress.js";
|
|
6
|
+
import { sanitizeRuntimeFailureText } from "../redaction.js";
|
|
6
7
|
import { runtimeChildEnv, runtimeChildLaunch } from "../runtime-env.js";
|
|
7
8
|
import { readCommandVersion, resolveCommandOnPath } from "./probe.js";
|
|
8
9
|
import { adaptDeepseekProgressStarted, cleanupProgressMcpConfig, createDeepseekProgressState, createProgressMcpConfig, deepseekProgressDispositions, isDeepseekProgressCompletion, progressMcpAutoInjectionSupported, progressSystemContext, } from "./progress.js";
|
|
@@ -270,7 +271,7 @@ export class DeepseekTuiAdapter {
|
|
|
270
271
|
};
|
|
271
272
|
child.stderr?.setEncoding("utf8");
|
|
272
273
|
child.stderr?.on("data", (chunk) => {
|
|
273
|
-
handle.stderrTail = (handle.stderrTail + chunk
|
|
274
|
+
handle.stderrTail = sanitizeRuntimeFailureText(handle.stderrTail + chunk, 4096);
|
|
274
275
|
});
|
|
275
276
|
child.on("close", () => {
|
|
276
277
|
handle.closed = true;
|
|
@@ -287,7 +288,7 @@ export class DeepseekTuiAdapter {
|
|
|
287
288
|
handle.progressMcpConfig = undefined;
|
|
288
289
|
});
|
|
289
290
|
try {
|
|
290
|
-
await waitForHealth(baseUrl, this.fetchFn,
|
|
291
|
+
await waitForHealth(baseUrl, this.fetchFn, handle, STARTUP_TIMEOUT_MS, signal);
|
|
291
292
|
}
|
|
292
293
|
catch (error) {
|
|
293
294
|
shutdownHandle(handle, "startup-failed");
|
|
@@ -929,14 +930,18 @@ function shutdownHandle(handle, reason) {
|
|
|
929
930
|
}
|
|
930
931
|
log.debug("deepseek-tui.shutdown", { reason });
|
|
931
932
|
}
|
|
932
|
-
async function waitForHealth(baseUrl, fetchFn,
|
|
933
|
+
async function waitForHealth(baseUrl, fetchFn, handle, timeoutMs, signal) {
|
|
933
934
|
const deadline = Date.now() + timeoutMs;
|
|
934
935
|
let lastError = "";
|
|
935
936
|
while (Date.now() < deadline) {
|
|
936
937
|
if (signal.aborted)
|
|
937
938
|
throw abortReason(signal);
|
|
938
|
-
if (child.exitCode !== null) {
|
|
939
|
-
|
|
939
|
+
if (handle.child.exitCode !== null) {
|
|
940
|
+
const detail = sanitizeRuntimeFailureText(handle.stderrTail, 1024)
|
|
941
|
+
.trim()
|
|
942
|
+
.replace(/\s+/gu, " ");
|
|
943
|
+
throw new Error(`deepseek serve exited with code ${handle.child.exitCode}`
|
|
944
|
+
+ (detail ? `: ${detail}` : ""));
|
|
940
945
|
}
|
|
941
946
|
try {
|
|
942
947
|
const res = await fetchFn(`${baseUrl}/health`, { method: "GET", signal });
|
|
@@ -191,7 +191,8 @@ function resolveManagedProgressRoot(explicit) {
|
|
|
191
191
|
return null;
|
|
192
192
|
let root = explicit?.trim();
|
|
193
193
|
if (root === undefined) {
|
|
194
|
-
const managedRuntime = process.env.BOTLEARN_RUNTIME_USER?.trim()
|
|
194
|
+
const managedRuntime = process.env.BOTLEARN_RUNTIME_USER?.trim()
|
|
195
|
+
|| process.env.BOTLEARN_RUNTIME_LAUNCH_MODE?.trim();
|
|
195
196
|
if (!managedRuntime)
|
|
196
197
|
return null;
|
|
197
198
|
root = process.env.BOTLEARN_AGENT_SERVICE_PROFILE_ROOT?.trim();
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
export declare function noNewPrivilegesEnabled(status: string): boolean;
|
|
3
|
+
export declare function sandboxSupervisorLaunchPlan(noNewPrivileges: boolean, controlUid: number, controlGid: number): {
|
|
4
|
+
directoryOwnerUid: number;
|
|
5
|
+
daemonIdentity: {
|
|
6
|
+
uid?: number;
|
|
7
|
+
gid: number;
|
|
8
|
+
};
|
|
9
|
+
runtimeLaunchEnv: Record<string, string>;
|
|
10
|
+
};
|
|
2
11
|
export declare function acquireSandboxSupervisorLock(sandboxId: string, lockRoot?: string): (() => void) | null;
|
|
3
12
|
export declare function runSandboxSupervisor(argv: string[]): Promise<number>;
|
|
4
13
|
export declare function isMainModule(entry?: string): boolean;
|
|
@@ -23,6 +23,35 @@ const MANAGED_PATH = [
|
|
|
23
23
|
// use it for a managed control-plane executable.
|
|
24
24
|
"/usr/local/bin",
|
|
25
25
|
].join(":");
|
|
26
|
+
export function noNewPrivilegesEnabled(status) {
|
|
27
|
+
const match = /^NoNewPrivs:\s*([01])\s*$/mu.exec(status);
|
|
28
|
+
if (!match)
|
|
29
|
+
throw new Error("sandbox supervisor could not read NoNewPrivs state");
|
|
30
|
+
return match[1] === "1";
|
|
31
|
+
}
|
|
32
|
+
export function sandboxSupervisorLaunchPlan(noNewPrivileges, controlUid, controlGid) {
|
|
33
|
+
if (noNewPrivileges) {
|
|
34
|
+
return {
|
|
35
|
+
directoryOwnerUid: 0,
|
|
36
|
+
// Keep the root supervisor's uid so Node can perform a one-way uid/gid drop for
|
|
37
|
+
// the runtime child. The control gid preserves the existing workspace/profile ACLs.
|
|
38
|
+
daemonIdentity: { gid: controlGid },
|
|
39
|
+
runtimeLaunchEnv: {
|
|
40
|
+
BOTLEARN_RUNTIME_LAUNCH_MODE: "direct-uid",
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
directoryOwnerUid: controlUid,
|
|
46
|
+
daemonIdentity: { uid: controlUid, gid: controlGid },
|
|
47
|
+
runtimeLaunchEnv: {
|
|
48
|
+
BOTLEARN_RUNTIME_LAUNCH_MODE: "sudo",
|
|
49
|
+
BOTLEARN_RUNTIME_USER: RUNTIME_USER,
|
|
50
|
+
BOTLEARN_RUNTIME_GROUP: CONTROL_USER,
|
|
51
|
+
BOTLEARN_RUNTIME_LAUNCHER: RUNTIME_LAUNCHER,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
26
55
|
function numericId(flag, user) {
|
|
27
56
|
const output = execFileSync("/usr/bin/id", [flag, user], {
|
|
28
57
|
encoding: "utf8",
|
|
@@ -112,6 +141,8 @@ export async function runSandboxSupervisor(argv) {
|
|
|
112
141
|
const controlUid = numericId("-u", CONTROL_USER);
|
|
113
142
|
const controlGid = numericId("-g", CONTROL_USER);
|
|
114
143
|
const runtimeUid = numericId("-u", RUNTIME_USER);
|
|
144
|
+
const noNewPrivileges = noNewPrivilegesEnabled(readFileSync("/proc/self/status", "utf8"));
|
|
145
|
+
const launchPlan = sandboxSupervisorLaunchPlan(noNewPrivileges, controlUid, controlGid);
|
|
115
146
|
const bootstrap = await readOneShotBootstrap();
|
|
116
147
|
let child;
|
|
117
148
|
let releaseLock = null;
|
|
@@ -128,25 +159,25 @@ export async function runSandboxSupervisor(argv) {
|
|
|
128
159
|
releaseLock = acquireSandboxSupervisorLock(sandboxId);
|
|
129
160
|
if (releaseLock === null)
|
|
130
161
|
return 0;
|
|
131
|
-
prepareDirectories(
|
|
162
|
+
prepareDirectories(launchPlan.directoryOwnerUid, controlGid);
|
|
163
|
+
if (noNewPrivileges) {
|
|
164
|
+
process.stderr.write("sandbox supervisor: NoNewPrivs=1; using root daemon with direct runtime uid drop\n");
|
|
165
|
+
}
|
|
132
166
|
child = spawn(NODE_BINARY, [
|
|
133
167
|
DAEMON_ENTRY,
|
|
134
168
|
"agent-service",
|
|
135
169
|
"session",
|
|
136
170
|
"--bootstrap-stdin",
|
|
137
171
|
], {
|
|
138
|
-
|
|
139
|
-
gid: controlGid,
|
|
172
|
+
...launchPlan.daemonIdentity,
|
|
140
173
|
env: {
|
|
141
174
|
HOME: "/home/botlearn-control",
|
|
142
175
|
PATH: MANAGED_PATH,
|
|
143
176
|
BOTLEARN_DAEMON_HOME: CONTROL_HOME,
|
|
144
177
|
BOTLEARN_RUNTIME_UID: String(runtimeUid),
|
|
145
178
|
BOTLEARN_RUNTIME_GID: String(controlGid),
|
|
146
|
-
BOTLEARN_RUNTIME_USER: RUNTIME_USER,
|
|
147
|
-
BOTLEARN_RUNTIME_GROUP: CONTROL_USER,
|
|
148
179
|
BOTLEARN_RUNTIME_HOME: "/home/user",
|
|
149
|
-
|
|
180
|
+
...launchPlan.runtimeLaunchEnv,
|
|
150
181
|
BOTLEARN_DEEPSEEK_TUI_BIN: DEEPSEEK_BINARY,
|
|
151
182
|
BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT: WORKSPACE,
|
|
152
183
|
BOTLEARN_AGENT_SERVICE_PROFILE_ROOT: RUNTIME_PROFILE_ROOT,
|
package/package.json
CHANGED