@yeaft/webchat-agent 1.0.385 → 1.0.387
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/Dockerfile +26 -0
- package/cli.js +5 -4
- package/connection/index.js +2 -12
- package/container-cli.js +86 -0
- package/container-manager.js +165 -0
- package/index.js +5 -5
- package/local-runtime/server/config.js +8 -37
- package/local-runtime/server/container-agent-service.js +78 -0
- package/local-runtime/server/database.js +0 -1
- package/local-runtime/server/db/connection.js +5 -6
- package/local-runtime/server/db/user-db.js +8 -95
- package/local-runtime/server/index.js +13 -40
- package/local-runtime/server/routes/sandbox-routes.js +28 -95
- package/local-runtime/server/routes/user-routes.js +4 -0
- package/local-runtime/server/ws-agent.js +11 -27
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +96 -103
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +1 -1
- package/package.json +4 -1
- package/local-runtime/server/db/sandbox-db.js +0 -673
- package/local-runtime/server/sandbox-agent-auth.js +0 -66
- package/local-runtime/server/sandbox-attestation-listener.js +0 -128
- package/local-runtime/server/sandbox-config.js +0 -42
- package/local-runtime/server/sandbox-host-attestation.js +0 -175
- package/local-runtime/server/sandbox-reconciler.js +0 -355
- package/managed-sandbox/agent-runtime.js +0 -73
- package/managed-sandbox/controller.js +0 -118
- package/managed-sandbox/helper.js +0 -437
- package/managed-sandbox/identity-store.js +0 -9
- package/managed-sandbox/runtime-executor.js +0 -387
package/Dockerfile
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
FROM node:24-bookworm-slim
|
|
2
|
+
|
|
3
|
+
ARG BUILD_VERSION=dev
|
|
4
|
+
ENV NODE_ENV=production \
|
|
5
|
+
HOME=/home/yeaft \
|
|
6
|
+
YEAFT_DIR=/home/yeaft/.yeaft \
|
|
7
|
+
WORK_DIR=/workspace
|
|
8
|
+
|
|
9
|
+
RUN apt-get update \
|
|
10
|
+
&& apt-get install -y --no-install-recommends ca-certificates git openssh-client tini \
|
|
11
|
+
&& rm -rf /var/lib/apt/lists/* \
|
|
12
|
+
&& useradd --create-home --uid 10001 --shell /usr/sbin/nologin yeaft \
|
|
13
|
+
&& mkdir -p /app /workspace /home/yeaft/.yeaft \
|
|
14
|
+
&& chown -R yeaft:yeaft /app /workspace /home/yeaft
|
|
15
|
+
|
|
16
|
+
WORKDIR /app
|
|
17
|
+
COPY package.json package-lock.json ./
|
|
18
|
+
COPY agent/package.json ./agent/
|
|
19
|
+
RUN npm ci --workspace=agent --omit=dev
|
|
20
|
+
COPY --chown=yeaft:yeaft agent ./agent
|
|
21
|
+
RUN node -e "const fs=require('fs'); const p='agent/package.json'; const j=JSON.parse(fs.readFileSync(p)); j.version=process.argv[1].replace(/^v/, ''); fs.writeFileSync(p, JSON.stringify(j, null, 2) + '\\n')" "$BUILD_VERSION" \
|
|
22
|
+
&& chmod 0755 agent/container-entrypoint.sh
|
|
23
|
+
|
|
24
|
+
VOLUME ["/home/yeaft/.yeaft", "/workspace"]
|
|
25
|
+
ENTRYPOINT ["/app/agent/container-entrypoint.sh"]
|
|
26
|
+
CMD []
|
package/cli.js
CHANGED
|
@@ -65,12 +65,12 @@ if (command === 'doctor') {
|
|
|
65
65
|
await handleLlmCommand(subArgs);
|
|
66
66
|
} else if (command === 'local') {
|
|
67
67
|
await handleLocalCommand(subArgs);
|
|
68
|
-
} else if (command === '
|
|
68
|
+
} else if (command === 'container') {
|
|
69
69
|
try {
|
|
70
|
-
const {
|
|
71
|
-
await
|
|
70
|
+
const { runContainerCli } = await import('./container-cli.js');
|
|
71
|
+
await runContainerCli(subArgs);
|
|
72
72
|
} catch (error) {
|
|
73
|
-
console.error(`
|
|
73
|
+
console.error(`Container Agent failed: ${error.code || error.message}`);
|
|
74
74
|
process.exit(1);
|
|
75
75
|
}
|
|
76
76
|
} else if (command === 'upgrade') {
|
|
@@ -138,6 +138,7 @@ function printHelp() {
|
|
|
138
138
|
yeaft-agent logs [options] View service logs (follow mode)
|
|
139
139
|
yeaft-agent doctor Diagnose service configuration
|
|
140
140
|
yeaft-agent llm <command> Configure local Yeaft LLM providers/models
|
|
141
|
+
yeaft-agent container <command> Manage a Dockerized yeaft-agent
|
|
141
142
|
yeaft-agent upgrade [--name <id>] Upgrade and restart the selected service
|
|
142
143
|
yeaft-agent --version Show version
|
|
143
144
|
|
package/connection/index.js
CHANGED
|
@@ -66,18 +66,8 @@ export function connect(WebSocketImpl = WebSocket) {
|
|
|
66
66
|
if (msg.type === 'auth_required' && msg.tempId) {
|
|
67
67
|
console.log('Received auth challenge, sending credentials...');
|
|
68
68
|
ctx.pendingAuthTempId = msg.tempId;
|
|
69
|
-
// Send
|
|
70
|
-
|
|
71
|
-
socket.send(JSON.stringify(managed ? {
|
|
72
|
-
type: 'auth',
|
|
73
|
-
tempId: msg.tempId,
|
|
74
|
-
authKind: 'sandbox',
|
|
75
|
-
credentialId: managed.credentialId,
|
|
76
|
-
secret: managed.secret,
|
|
77
|
-
sandboxClaims: managed.claims,
|
|
78
|
-
capabilities: ctx.agentCapabilities,
|
|
79
|
-
version: ctx.agentVersion
|
|
80
|
-
} : {
|
|
69
|
+
// Send the ordinary Agent credential via WebSocket (not URL).
|
|
70
|
+
socket.send(JSON.stringify({
|
|
81
71
|
type: 'auth',
|
|
82
72
|
tempId: msg.tempId,
|
|
83
73
|
secret: ctx.CONFIG.agentSecret,
|
package/container-cli.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
createContainerAgent,
|
|
6
|
+
inspectContainerAgent,
|
|
7
|
+
logsContainerAgent,
|
|
8
|
+
readSecretInput,
|
|
9
|
+
removeContainerAgent,
|
|
10
|
+
startContainerAgent,
|
|
11
|
+
stopContainerAgent,
|
|
12
|
+
writeAgentSecretFile,
|
|
13
|
+
} from './container-manager.js';
|
|
14
|
+
|
|
15
|
+
function help() {
|
|
16
|
+
console.log(`
|
|
17
|
+
Usage:
|
|
18
|
+
yeaft-agent container create --server <ws-url> --name <name> (--secret <value> | --secret-file <path>) [--image <image>]
|
|
19
|
+
yeaft-agent container start|stop|status|remove|logs --name <name>
|
|
20
|
+
|
|
21
|
+
The container is an ordinary yeaft-agent. This command only manages its Docker lifecycle.
|
|
22
|
+
Use --keep-volumes with remove to preserve its Yeaft data and workspace volumes.
|
|
23
|
+
`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseContainerArgs(args) {
|
|
27
|
+
const options = {};
|
|
28
|
+
const positionals = [];
|
|
29
|
+
for (let i = 0; i < args.length; i++) {
|
|
30
|
+
const arg = args[i];
|
|
31
|
+
if (arg === '--keep-volumes' || arg === '--follow') {
|
|
32
|
+
options[arg.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = true;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!arg.startsWith('--')) {
|
|
36
|
+
positionals.push(arg);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const value = args[++i];
|
|
40
|
+
if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value`);
|
|
41
|
+
options[arg.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = value;
|
|
42
|
+
}
|
|
43
|
+
return { action: positionals[0], options };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function runContainerCli(args) {
|
|
47
|
+
if (args.length === 0 || args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return help();
|
|
48
|
+
const { action, options } = parseContainerArgs(args);
|
|
49
|
+
const name = options.name;
|
|
50
|
+
let result;
|
|
51
|
+
if (action === 'create') {
|
|
52
|
+
const secret = await readSecretInput(options);
|
|
53
|
+
const secretFile = options.secretFile
|
|
54
|
+
? resolve(options.secretFile)
|
|
55
|
+
: join(homedir(), '.yeaft', 'container-agents', name, 'agent-secret');
|
|
56
|
+
await writeAgentSecretFile(secretFile, secret);
|
|
57
|
+
result = await createContainerAgent({
|
|
58
|
+
name,
|
|
59
|
+
serverUrl: options.server,
|
|
60
|
+
secretFile,
|
|
61
|
+
image: options.image,
|
|
62
|
+
});
|
|
63
|
+
} else if (action === 'start') {
|
|
64
|
+
result = await startContainerAgent(name);
|
|
65
|
+
} else if (action === 'stop') {
|
|
66
|
+
result = await stopContainerAgent(name);
|
|
67
|
+
} else if (action === 'status') {
|
|
68
|
+
result = await inspectContainerAgent(name);
|
|
69
|
+
} else if (action === 'remove') {
|
|
70
|
+
result = await removeContainerAgent(name, { removeVolumes: !options.keepVolumes });
|
|
71
|
+
} else if (action === 'logs') {
|
|
72
|
+
result = await logsContainerAgent(name, { follow: options.follow });
|
|
73
|
+
if (!options.follow && result.stdout) console.log(result.stdout);
|
|
74
|
+
return;
|
|
75
|
+
} else {
|
|
76
|
+
throw new Error(`Unknown container action: ${action}`);
|
|
77
|
+
}
|
|
78
|
+
console.log(JSON.stringify({ name, ...result }, null, 2));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (process.argv[1] && import.meta.url === new URL(`file://${resolve(process.argv[1])}`).href) {
|
|
82
|
+
runContainerCli(process.argv.slice(2)).catch(error => {
|
|
83
|
+
console.error(`Container Agent failed: ${error.code || error.message}`);
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_AGENT_IMAGE = 'ghcr.io/yeaft/yeaft-web-code-agent-agent:dev';
|
|
6
|
+
const NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/;
|
|
7
|
+
|
|
8
|
+
export class ContainerAgentError extends Error {
|
|
9
|
+
constructor(code, message = code) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'ContainerAgentError';
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function normalizeContainerAgentName(name) {
|
|
17
|
+
const value = String(name || '').trim();
|
|
18
|
+
if (!NAME_PATTERN.test(value)) throw new ContainerAgentError('CONTAINER_AGENT_INVALID_NAME');
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function containerNameForAgent(name) {
|
|
23
|
+
return `yeaft-agent-${normalizeContainerAgentName(name)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function runDocker(args, { spawnImpl = spawn, allowFailure = false, stdout = 'pipe' } = {}) {
|
|
27
|
+
return new Promise((resolvePromise, reject) => {
|
|
28
|
+
const child = spawnImpl('docker', args, {
|
|
29
|
+
stdio: ['ignore', stdout, 'pipe'],
|
|
30
|
+
windowsHide: true,
|
|
31
|
+
});
|
|
32
|
+
const output = [];
|
|
33
|
+
const errors = [];
|
|
34
|
+
child.stdout?.on('data', chunk => output.push(chunk));
|
|
35
|
+
child.stderr?.on('data', chunk => errors.push(chunk));
|
|
36
|
+
child.once('error', error => reject(new ContainerAgentError('CONTAINER_AGENT_DOCKER_UNAVAILABLE', error.message)));
|
|
37
|
+
child.once('close', code => {
|
|
38
|
+
const result = {
|
|
39
|
+
code: code ?? 1,
|
|
40
|
+
stdout: Buffer.concat(output).toString('utf8').trim(),
|
|
41
|
+
stderr: Buffer.concat(errors).toString('utf8').trim(),
|
|
42
|
+
};
|
|
43
|
+
if (result.code === 0 || allowFailure) resolvePromise(result);
|
|
44
|
+
else reject(new ContainerAgentError('CONTAINER_AGENT_DOCKER_FAILED', result.stderr || `docker ${args[0]} failed`));
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function writeAgentSecretFile(path, secret) {
|
|
50
|
+
const value = String(secret || '').trim();
|
|
51
|
+
if (!value) throw new ContainerAgentError('CONTAINER_AGENT_SECRET_REQUIRED');
|
|
52
|
+
const absolute = resolve(path);
|
|
53
|
+
await mkdir(dirname(absolute), { recursive: true, mode: 0o700 });
|
|
54
|
+
await writeFile(absolute, `${value}\n`, { mode: 0o600 });
|
|
55
|
+
await chmod(absolute, 0o600);
|
|
56
|
+
return absolute;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function buildCreateArgs({
|
|
60
|
+
name,
|
|
61
|
+
serverUrl,
|
|
62
|
+
secretFile,
|
|
63
|
+
image = DEFAULT_AGENT_IMAGE,
|
|
64
|
+
dataVolume,
|
|
65
|
+
workspaceVolume,
|
|
66
|
+
restart = 'unless-stopped',
|
|
67
|
+
}) {
|
|
68
|
+
const agentName = normalizeContainerAgentName(name);
|
|
69
|
+
if (!String(serverUrl || '').match(/^wss?:\/\//)) {
|
|
70
|
+
throw new ContainerAgentError('CONTAINER_AGENT_INVALID_SERVER_URL');
|
|
71
|
+
}
|
|
72
|
+
if (!secretFile) throw new ContainerAgentError('CONTAINER_AGENT_SECRET_REQUIRED');
|
|
73
|
+
const containerName = containerNameForAgent(agentName);
|
|
74
|
+
const safeImage = String(image || '').trim();
|
|
75
|
+
if (!safeImage || safeImage.startsWith('-')) throw new ContainerAgentError('CONTAINER_AGENT_INVALID_IMAGE');
|
|
76
|
+
return [
|
|
77
|
+
'create', '--name', containerName,
|
|
78
|
+
'--label', 'io.yeaft.container-agent=true',
|
|
79
|
+
'--label', `io.yeaft.agent-name=${agentName}`,
|
|
80
|
+
'--restart', restart,
|
|
81
|
+
'--init',
|
|
82
|
+
'--mount', `type=volume,src=${dataVolume || `${containerName}-data`},dst=/home/yeaft/.yeaft`,
|
|
83
|
+
'--mount', `type=volume,src=${workspaceVolume || `${containerName}-workspace`},dst=/workspace`,
|
|
84
|
+
'--mount', `type=bind,src=${resolve(secretFile)},dst=/run/yeaft-host-secret,readonly`,
|
|
85
|
+
'--env', `SERVER_URL=${serverUrl}`,
|
|
86
|
+
'--env', `AGENT_NAME=${agentName}`,
|
|
87
|
+
'--env', 'AGENT_SECRET_FILE=/run/yeaft-host-secret',
|
|
88
|
+
'--env', 'YEAFT_DIR=/home/yeaft/.yeaft',
|
|
89
|
+
'--env', 'WORK_DIR=/workspace',
|
|
90
|
+
safeImage,
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function inspectContainerAgent(name, options = {}) {
|
|
95
|
+
const result = await runDocker([
|
|
96
|
+
'inspect', '--format', '{{json .State}}', containerNameForAgent(name),
|
|
97
|
+
], { ...options, allowFailure: true });
|
|
98
|
+
if (result.code !== 0) {
|
|
99
|
+
if (/no such (object|container)/i.test(result.stderr)) {
|
|
100
|
+
return { exists: false, status: 'absent', running: false };
|
|
101
|
+
}
|
|
102
|
+
throw new ContainerAgentError('CONTAINER_AGENT_DOCKER_FAILED', result.stderr || 'docker inspect failed');
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const state = JSON.parse(result.stdout);
|
|
106
|
+
return {
|
|
107
|
+
exists: true,
|
|
108
|
+
status: state.Status || 'unknown',
|
|
109
|
+
running: state.Running === true,
|
|
110
|
+
startedAt: state.StartedAt || null,
|
|
111
|
+
error: state.Error || null,
|
|
112
|
+
};
|
|
113
|
+
} catch {
|
|
114
|
+
throw new ContainerAgentError('CONTAINER_AGENT_INVALID_DOCKER_RESPONSE');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function createContainerAgent(options, runtime = {}) {
|
|
119
|
+
const current = await inspectContainerAgent(options.name, runtime);
|
|
120
|
+
if (current.exists) throw new ContainerAgentError('CONTAINER_AGENT_ALREADY_EXISTS');
|
|
121
|
+
const containerName = containerNameForAgent(options.name);
|
|
122
|
+
await runDocker(buildCreateArgs(options), runtime);
|
|
123
|
+
try {
|
|
124
|
+
await runDocker(['start', containerName], runtime);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
await runDocker(['rm', '-f', containerName], { ...runtime, allowFailure: true });
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
return inspectContainerAgent(options.name, runtime);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function startContainerAgent(name, runtime = {}) {
|
|
133
|
+
await runDocker(['start', containerNameForAgent(name)], runtime);
|
|
134
|
+
return inspectContainerAgent(name, runtime);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function stopContainerAgent(name, runtime = {}) {
|
|
138
|
+
await runDocker(['stop', '--time', '10', containerNameForAgent(name)], runtime);
|
|
139
|
+
return inspectContainerAgent(name, runtime);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function removeContainerAgent(name, { removeVolumes = true, ...runtime } = {}) {
|
|
143
|
+
const containerName = containerNameForAgent(name);
|
|
144
|
+
const current = await inspectContainerAgent(name, runtime);
|
|
145
|
+
if (current.exists) await runDocker(['rm', '-f', containerName], runtime);
|
|
146
|
+
if (removeVolumes) {
|
|
147
|
+
await runDocker(['volume', 'rm', `${containerName}-data`, `${containerName}-workspace`], {
|
|
148
|
+
...runtime,
|
|
149
|
+
allowFailure: true,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return { exists: false, status: 'absent', running: false };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function logsContainerAgent(name, { follow = false, ...runtime } = {}) {
|
|
156
|
+
const args = ['logs'];
|
|
157
|
+
if (follow) args.push('--follow');
|
|
158
|
+
args.push(containerNameForAgent(name));
|
|
159
|
+
return runDocker(args, { ...runtime, stdout: follow ? 'inherit' : 'pipe' });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function readSecretInput({ secret, secretFile }) {
|
|
163
|
+
if (secretFile) return (await readFile(resolve(secretFile), 'utf8')).trim();
|
|
164
|
+
return String(secret || '').trim();
|
|
165
|
+
}
|
package/index.js
CHANGED
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
import { loadNodePty } from './terminal.js';
|
|
22
22
|
import { connect } from './connection.js';
|
|
23
23
|
import { loadMcpServers } from './mcp.js';
|
|
24
|
-
import { getManagedSandboxIdentity } from './managed-sandbox/identity-store.js';
|
|
25
24
|
import { SAFE_REMOTE_UPGRADE_CAPABILITY } from './upgrade-command.js';
|
|
26
25
|
import { loadConfig as loadYeaftConfig } from './yeaft/config.js';
|
|
27
26
|
import {
|
|
@@ -50,7 +49,6 @@ ctx.pkgName = pkg.name;
|
|
|
50
49
|
// service instances must stay scoped to their standard per-instance config.
|
|
51
50
|
const LOCAL_CONFIG_FILE = join(process.cwd(), '.claude-agent.json');
|
|
52
51
|
const IS_LOCAL_RUN = process.env.YEAFT_LOCAL_RUN === 'true';
|
|
53
|
-
const MANAGED_SANDBOX_IDENTITY = getManagedSandboxIdentity();
|
|
54
52
|
const DEFAULT_AGENT_NAME = getDefaultAgentName();
|
|
55
53
|
|
|
56
54
|
// 加载或创建配置
|
|
@@ -113,6 +111,10 @@ try {
|
|
|
113
111
|
console.warn(`[Agent] Could not ensure yeaft dir ${YEAFT_DIR}: ${err?.message || err}`);
|
|
114
112
|
}
|
|
115
113
|
|
|
114
|
+
const agentSecret = process.env.AGENT_SECRET_FILE
|
|
115
|
+
? readFileSync(process.env.AGENT_SECRET_FILE, 'utf8').trim()
|
|
116
|
+
: (process.env.AGENT_SECRET || fileConfig.agentSecret);
|
|
117
|
+
|
|
116
118
|
const CONFIG = {
|
|
117
119
|
instanceId: INSTANCE_ID,
|
|
118
120
|
serverUrl: process.env.SERVER_URL || fileConfig.serverUrl,
|
|
@@ -121,8 +123,7 @@ const CONFIG = {
|
|
|
121
123
|
yeaftDir: YEAFT_DIR,
|
|
122
124
|
telemetry: loadYeaftConfig({ dir: YEAFT_DIR }).telemetry,
|
|
123
125
|
reconnectInterval: fileConfig.reconnectInterval,
|
|
124
|
-
agentSecret
|
|
125
|
-
managedSandboxIdentity: MANAGED_SANDBOX_IDENTITY,
|
|
126
|
+
agentSecret,
|
|
126
127
|
// 显式禁用的工具(非 MCP 相关)
|
|
127
128
|
explicitDisallowedTools: (() => {
|
|
128
129
|
const raw = process.env.DISALLOWED_TOOLS || fileConfig.disallowedTools || '';
|
|
@@ -159,7 +160,6 @@ async function detectCapabilities() {
|
|
|
159
160
|
// flip `agent.encryptOutbound = false`, stopping outbound encryption
|
|
160
161
|
// to this peer. Old servers ignore the unknown capability token.
|
|
161
162
|
const capabilities = ['background_tasks', 'file_editor', 'ping_session', 'plaintext-ok', SAFE_REMOTE_UPGRADE_CAPABILITY, 'work_center', 'work_center_message_v2', 'session_history_search', 'session_history_outline', 'session_history_window_prefetch'];
|
|
162
|
-
if (MANAGED_SANDBOX_IDENTITY) capabilities.push('managed-sandbox');
|
|
163
163
|
if (process.platform === 'linux') capabilities.push('work_item_attachments');
|
|
164
164
|
const pty = await loadNodePty();
|
|
165
165
|
if (pty) capabilities.push('terminal');
|
|
@@ -3,7 +3,7 @@ import { readFileSync, existsSync, writeFileSync } from 'fs';
|
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { dirname, join } from 'path';
|
|
5
5
|
import { userDb } from './database.js';
|
|
6
|
-
import {
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
7
|
|
|
8
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
9
|
const __dirname = dirname(__filename);
|
|
@@ -115,42 +115,13 @@ export const CONFIG = {
|
|
|
115
115
|
// Agent authentication (global fallback — per-user agent_secret is preferred)
|
|
116
116
|
agentSecret: process.env.AGENT_SECRET || DEFAULT_AGENT_SECRET,
|
|
117
117
|
|
|
118
|
-
//
|
|
119
|
-
//
|
|
118
|
+
// A Sandbox is an ordinary yeaft-agent container managed by this Server's Docker daemon.
|
|
119
|
+
// The Server controls only the container lifecycle; Agent behavior stays on the existing wire.
|
|
120
120
|
sandbox: {
|
|
121
121
|
enabled: process.env.SANDBOX_ENABLED === 'true',
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
agentRecoveryGraceMs: parseInt(process.env.SANDBOX_AGENT_RECOVERY_GRACE_MS, 10) || 60_000,
|
|
126
|
-
operationTimeoutMs: parseInt(process.env.SANDBOX_OPERATION_TIMEOUT_MS, 10) || 10 * 60_000,
|
|
127
|
-
reconcileIntervalMs: parseInt(process.env.SANDBOX_RECONCILE_INTERVAL_MS, 10) || 5_000,
|
|
128
|
-
controllerRequestTimeoutMs: parseInt(process.env.SANDBOX_CONTROLLER_TIMEOUT_MS, 10) || 10_000,
|
|
129
|
-
bootstrapTtlMs: parseInt(process.env.SANDBOX_BOOTSTRAP_TTL_MS, 10) || 5 * 60_000,
|
|
130
|
-
bootstrapSigningKey: process.env.SANDBOX_BOOTSTRAP_SIGNING_KEY || '',
|
|
131
|
-
hostAttestationKey: process.env.SANDBOX_HOST_ATTESTATION_KEY || '',
|
|
132
|
-
controllerAttestationFingerprint: process.env.SANDBOX_CONTROLLER_ATTESTATION_FINGERPRINT || '',
|
|
133
|
-
hostAttestationListenerHost: process.env.SANDBOX_HOST_ATTESTATION_LISTENER_HOST || '',
|
|
134
|
-
hostAttestationListenerPort: parseInt(process.env.SANDBOX_HOST_ATTESTATION_LISTENER_PORT, 10) || 0,
|
|
135
|
-
hostAttestationServerCert: process.env.SANDBOX_HOST_ATTESTATION_SERVER_CERT || '',
|
|
136
|
-
hostAttestationServerKey: process.env.SANDBOX_HOST_ATTESTATION_SERVER_KEY || '',
|
|
137
|
-
hostAttestationClientCa: process.env.SANDBOX_HOST_ATTESTATION_CLIENT_CA || '',
|
|
138
|
-
hostAttestationBodyLimitBytes:
|
|
139
|
-
parseInt(process.env.SANDBOX_HOST_ATTESTATION_BODY_LIMIT_BYTES, 10) || 64 * 1024,
|
|
140
|
-
hostAttestationShutdownTimeoutMs:
|
|
141
|
-
parseInt(process.env.SANDBOX_HOST_ATTESTATION_SHUTDOWN_TIMEOUT_MS, 10) || 1_000,
|
|
142
|
-
helperAttestationPublicKey: process.env.SANDBOX_HELPER_ATTESTATION_PUBLIC_KEY || '',
|
|
143
|
-
hostAttestationMaxSkewMs: parseInt(process.env.SANDBOX_HOST_ATTESTATION_MAX_SKEW_MS, 10) || 30_000,
|
|
144
|
-
imageDigest: process.env.SANDBOX_IMAGE_DIGEST || '',
|
|
145
|
-
controllerUrl: process.env.SANDBOX_CONTROLLER_URL || '',
|
|
146
|
-
controllerToken: process.env.SANDBOX_CONTROLLER_TOKEN || '',
|
|
147
|
-
controllerClientCert: process.env.SANDBOX_CONTROLLER_CLIENT_CERT || '',
|
|
148
|
-
controllerClientKey: process.env.SANDBOX_CONTROLLER_CLIENT_KEY || '',
|
|
149
|
-
controllerCaCert: process.env.SANDBOX_CONTROLLER_CA_CERT || '',
|
|
150
|
-
operationSigningPrivateKey: process.env.SANDBOX_OPERATION_SIGNING_PRIVATE_KEY || '',
|
|
151
|
-
controllerResultPublicKey: process.env.SANDBOX_CONTROLLER_RESULT_PUBLIC_KEY || '',
|
|
152
|
-
controllerProtocolMaxSkewMs: parseInt(process.env.SANDBOX_CONTROLLER_PROTOCOL_MAX_SKEW_MS, 10) || 30_000,
|
|
153
|
-
controllerHostId: process.env.SANDBOX_CONTROLLER_HOST_ID || ''
|
|
122
|
+
image: process.env.SANDBOX_AGENT_IMAGE || 'ghcr.io/yeaft/yeaft-web-code-agent-agent:dev',
|
|
123
|
+
serverUrl: process.env.SANDBOX_SERVER_URL || '',
|
|
124
|
+
stateDir: process.env.SANDBOX_STATE_DIR || join(homedir(), '.yeaft', 'container-agents')
|
|
154
125
|
},
|
|
155
126
|
|
|
156
127
|
// File upload settings
|
|
@@ -299,8 +270,8 @@ export function validateProductionConfig() {
|
|
|
299
270
|
errors.push('JWT_SECRET must be set to a secure value in production mode');
|
|
300
271
|
}
|
|
301
272
|
|
|
302
|
-
if (CONFIG.sandbox.enabled &&
|
|
303
|
-
errors.push('
|
|
273
|
+
if (CONFIG.sandbox.enabled && !/^wss?:\/\//.test(CONFIG.sandbox.serverUrl)) {
|
|
274
|
+
errors.push('SANDBOX_SERVER_URL must be the ws:// or wss:// URL that container Agents use to connect');
|
|
304
275
|
}
|
|
305
276
|
|
|
306
277
|
// Check that at least one user with a password exists (in DB or config)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { CONFIG } from './config.js';
|
|
3
|
+
import {
|
|
4
|
+
createContainerAgent,
|
|
5
|
+
inspectContainerAgent,
|
|
6
|
+
removeContainerAgent,
|
|
7
|
+
startContainerAgent,
|
|
8
|
+
stopContainerAgent,
|
|
9
|
+
writeAgentSecretFile,
|
|
10
|
+
} from '../agent/container-manager.js';
|
|
11
|
+
|
|
12
|
+
function managedName(userId) {
|
|
13
|
+
return `sandbox-${String(userId).replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 48)}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class ContainerAgentService {
|
|
17
|
+
constructor(config = CONFIG.sandbox) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
capability() {
|
|
22
|
+
return {
|
|
23
|
+
available: this.config.enabled,
|
|
24
|
+
reasonCode: this.config.enabled ? null : 'SANDBOX_DISABLED',
|
|
25
|
+
catalog: this.config.enabled ? [{ id: 'standard' }] : [],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
nameForUser(userId) {
|
|
30
|
+
return managedName(userId);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async snapshot(userId) {
|
|
34
|
+
const name = this.nameForUser(userId);
|
|
35
|
+
const state = await inspectContainerAgent(name);
|
|
36
|
+
if (!state.exists) return null;
|
|
37
|
+
return {
|
|
38
|
+
id: name,
|
|
39
|
+
agentName: name,
|
|
40
|
+
sizeId: 'standard',
|
|
41
|
+
desiredState: state.running ? 'running' : 'stopped',
|
|
42
|
+
observedState: state.running ? 'running' : state.status,
|
|
43
|
+
reservationHeld: true,
|
|
44
|
+
operation: null,
|
|
45
|
+
lastErrorCode: state.error || null,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async create(user, { agentName } = {}) {
|
|
50
|
+
if (!this.config.enabled) throw Object.assign(new Error('SANDBOX_DISABLED'), { code: 'SANDBOX_DISABLED' });
|
|
51
|
+
const name = this.nameForUser(user.id);
|
|
52
|
+
const secretFile = join(this.config.stateDir, name, 'agent-secret');
|
|
53
|
+
await writeAgentSecretFile(secretFile, user.agent_secret);
|
|
54
|
+
await createContainerAgent({
|
|
55
|
+
name,
|
|
56
|
+
serverUrl: this.config.serverUrl,
|
|
57
|
+
secretFile,
|
|
58
|
+
image: this.config.image,
|
|
59
|
+
});
|
|
60
|
+
return { snapshot: await this.snapshot(user.id), replayed: false };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async action(userId, action) {
|
|
64
|
+
const name = this.nameForUser(userId);
|
|
65
|
+
if (action === 'start') await startContainerAgent(name);
|
|
66
|
+
else if (action === 'retry') {
|
|
67
|
+
const current = await inspectContainerAgent(name);
|
|
68
|
+
if (current.exists) await startContainerAgent(name);
|
|
69
|
+
else throw Object.assign(new Error('SANDBOX_NOT_FOUND'), { code: 'SANDBOX_NOT_FOUND' });
|
|
70
|
+
}
|
|
71
|
+
else if (action === 'stop') await stopContainerAgent(name);
|
|
72
|
+
else if (action === 'remove') await removeContainerAgent(name);
|
|
73
|
+
else throw Object.assign(new Error('SANDBOX_ACTION_NOT_ALLOWED'), { code: 'SANDBOX_ACTION_NOT_ALLOWED' });
|
|
74
|
+
return { snapshot: await this.snapshot(userId), replayed: false };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const containerAgentService = new ContainerAgentService();
|
|
@@ -10,6 +10,5 @@ export { messageDb } from './db/message-db.js';
|
|
|
10
10
|
export { userStatsDb } from './db/user-stats-db.js';
|
|
11
11
|
export { expertDb } from './db/expert-db.js';
|
|
12
12
|
export { identityDb } from './db/identity-db.js';
|
|
13
|
-
export { sandboxDb } from './db/sandbox-db.js';
|
|
14
13
|
export { closeDb } from './db/connection.js';
|
|
15
14
|
export { default } from './db/connection.js';
|
|
@@ -1316,6 +1316,11 @@ export const stmts = {
|
|
|
1316
1316
|
deleteCustomExpertRolesForUser: db.prepare(`
|
|
1317
1317
|
DELETE FROM custom_expert_roles WHERE user_id = ?
|
|
1318
1318
|
`),
|
|
1319
|
+
// Remove rows from the retired managed-Sandbox control plane before the user.
|
|
1320
|
+
// Child operations, credentials, and audit rows cascade from sandboxes.
|
|
1321
|
+
deleteLegacySandboxesForUser: db.prepare(`
|
|
1322
|
+
DELETE FROM sandboxes WHERE user_id = ?
|
|
1323
|
+
`),
|
|
1319
1324
|
// Invitations: keep history but null-out the FK so it doesn't block deletion.
|
|
1320
1325
|
// (created_by is NOT NULL, so for invitations the user created we just delete them.)
|
|
1321
1326
|
deleteInvitationsCreatedBy: db.prepare(`
|
|
@@ -1324,12 +1329,6 @@ export const stmts = {
|
|
|
1324
1329
|
clearInvitationUsedBy: db.prepare(`
|
|
1325
1330
|
UPDATE invitations SET used_by = NULL WHERE used_by = ?
|
|
1326
1331
|
`),
|
|
1327
|
-
getReservedSandboxForUser: db.prepare(`
|
|
1328
|
-
SELECT id FROM sandboxes WHERE user_id = ? AND reservation_held = 1 LIMIT 1
|
|
1329
|
-
`),
|
|
1330
|
-
deleteReleasedSandboxesForUser: db.prepare(`
|
|
1331
|
-
DELETE FROM sandboxes WHERE user_id = ? AND reservation_held = 0
|
|
1332
|
-
`),
|
|
1333
1332
|
deleteUserById: db.prepare(`
|
|
1334
1333
|
DELETE FROM users WHERE id = ?
|
|
1335
1334
|
`),
|