@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
|
@@ -1,50 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
2
|
import db, { stmts, generateUserId, generateAgentSecret, transaction } from './connection.js';
|
|
3
3
|
|
|
4
|
-
function deletionRequestDigest() {
|
|
5
|
-
return createHash('sha256').update(JSON.stringify({ kind: 'remove' })).digest('hex');
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function ensureDeletionRemoval(userId, deletionId, now = Date.now()) {
|
|
9
|
-
const sandbox = db.prepare('SELECT * FROM sandboxes WHERE user_id = ? AND reservation_held = 1').get(userId);
|
|
10
|
-
if (!sandbox) return null;
|
|
11
|
-
const baseKey = `account-delete:${deletionId}`;
|
|
12
|
-
const attempts = db.prepare(`
|
|
13
|
-
SELECT * FROM sandbox_operations
|
|
14
|
-
WHERE user_id = ? AND (idempotency_key = ? OR idempotency_key LIKE ?)
|
|
15
|
-
ORDER BY created_at DESC, rowid DESC
|
|
16
|
-
`).all(userId, baseKey, `${baseKey}:attempt:%`);
|
|
17
|
-
const latest = attempts[0];
|
|
18
|
-
if (latest && latest.status !== 'failed') return latest;
|
|
19
|
-
const key = attempts.length === 0 ? baseKey : `${baseKey}:attempt:${attempts.length + 1}`;
|
|
20
|
-
const existing = db.prepare('SELECT * FROM sandbox_operations WHERE user_id = ? AND idempotency_key = ?')
|
|
21
|
-
.get(userId, key);
|
|
22
|
-
if (existing) return existing;
|
|
23
|
-
db.prepare(`
|
|
24
|
-
UPDATE sandbox_operations SET status = 'failed', error_code = 'ACCOUNT_DELETION_REQUESTED', updated_at = ?
|
|
25
|
-
WHERE sandbox_id = ? AND status IN ('pending', 'running')
|
|
26
|
-
`).run(now, sandbox.id);
|
|
27
|
-
db.prepare('UPDATE sandbox_credentials SET revoked_at = ? WHERE sandbox_id = ? AND revoked_at IS NULL')
|
|
28
|
-
.run(now, sandbox.id);
|
|
29
|
-
const host = db.prepare('SELECT epoch FROM sandbox_hosts WHERE id = ?').get(sandbox.host_id);
|
|
30
|
-
const hostEpoch = host?.epoch || sandbox.host_epoch;
|
|
31
|
-
const generation = sandbox.generation + 1;
|
|
32
|
-
db.prepare(`
|
|
33
|
-
UPDATE sandboxes SET desired_state = 'removed', observed_state = CASE
|
|
34
|
-
WHEN observed_state = 'removed' THEN observed_state ELSE 'recovery_required' END,
|
|
35
|
-
generation = ?, host_epoch = ?, last_error_code = NULL, updated_at = ?
|
|
36
|
-
WHERE id = ? AND reservation_held = 1
|
|
37
|
-
`).run(generation, hostEpoch, now, sandbox.id);
|
|
38
|
-
const operationId = `sandbox_op_${randomUUID()}`;
|
|
39
|
-
db.prepare(`
|
|
40
|
-
INSERT INTO sandbox_operations (
|
|
41
|
-
id, sandbox_id, user_id, idempotency_key, request_digest, kind, status,
|
|
42
|
-
stage, generation, host_epoch, deadline_at, created_at, updated_at
|
|
43
|
-
) VALUES (?, ?, ?, ?, ?, 'remove', 'pending', 'removing', ?, ?, ?, ?, ?)
|
|
44
|
-
`).run(operationId, sandbox.id, userId, key, deletionRequestDigest(), generation,
|
|
45
|
-
hostEpoch, now + 10 * 60_000, now, now);
|
|
46
|
-
return db.prepare('SELECT * FROM sandbox_operations WHERE id = ?').get(operationId);
|
|
47
|
-
}
|
|
48
4
|
|
|
49
5
|
export const userDb = {
|
|
50
6
|
getOrCreate(username, displayName = null) {
|
|
@@ -116,44 +72,17 @@ export const userDb = {
|
|
|
116
72
|
password_hash = NULL, agent_secret = NULL, totp_secret = NULL, totp_enabled = 0
|
|
117
73
|
WHERE id = ? AND deletion_state = 'active'
|
|
118
74
|
`).run(now, deletionId, userId);
|
|
119
|
-
if (changed.changes !== 1)
|
|
120
|
-
throw new Error('ACCOUNT_DELETION_STATE_CONFLICT');
|
|
121
|
-
}
|
|
75
|
+
if (changed.changes !== 1) throw new Error('ACCOUNT_DELETION_STATE_CONFLICT');
|
|
122
76
|
}
|
|
123
|
-
|
|
124
|
-
.run(now, userId);
|
|
125
|
-
const operation = ensureDeletionRemoval(userId, deletionId, now);
|
|
126
|
-
return { deletionId, status: 'pending', operationId: operation?.id || null };
|
|
77
|
+
return { deletionId, status: 'pending', operationId: null };
|
|
127
78
|
})();
|
|
128
79
|
},
|
|
129
80
|
|
|
130
|
-
reconcilePendingDeletions(
|
|
131
|
-
const pending = db.prepare("SELECT id
|
|
81
|
+
reconcilePendingDeletions() {
|
|
82
|
+
const pending = db.prepare("SELECT id FROM users WHERE deletion_state = 'pending'").all();
|
|
132
83
|
let finalized = 0;
|
|
133
84
|
for (const user of pending) {
|
|
134
|
-
if (
|
|
135
|
-
transaction(() => ensureDeletionRemoval(user.id, user.deletion_id, now))();
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
const unsettled = db.prepare(`
|
|
139
|
-
SELECT 1 FROM sandboxes s
|
|
140
|
-
WHERE s.user_id = ? AND (
|
|
141
|
-
s.observed_state != 'removed' OR s.removed_at IS NULL
|
|
142
|
-
OR EXISTS (
|
|
143
|
-
SELECT 1 FROM sandbox_operations o
|
|
144
|
-
WHERE o.sandbox_id = s.id AND o.status IN ('pending', 'running')
|
|
145
|
-
)
|
|
146
|
-
OR NOT EXISTS (
|
|
147
|
-
SELECT 1 FROM sandbox_operations o
|
|
148
|
-
WHERE o.sandbox_id = s.id AND o.kind = 'remove' AND o.status = 'succeeded'
|
|
149
|
-
)
|
|
150
|
-
OR EXISTS (
|
|
151
|
-
SELECT 1 FROM sandbox_credentials c
|
|
152
|
-
WHERE c.sandbox_id = s.id AND c.revoked_at IS NULL
|
|
153
|
-
)
|
|
154
|
-
) LIMIT 1
|
|
155
|
-
`).get(user.id);
|
|
156
|
-
if (!unsettled && this.deleteUser(user.id, { requirePending: true })) finalized++;
|
|
85
|
+
if (this.deleteUser(user.id, { requirePending: true })) finalized++;
|
|
157
86
|
}
|
|
158
87
|
return finalized;
|
|
159
88
|
},
|
|
@@ -255,33 +184,17 @@ export const userDb = {
|
|
|
255
184
|
* session store from here to keep this layer pure).
|
|
256
185
|
*/
|
|
257
186
|
deleteUser(userId, { requirePending = false } = {}) {
|
|
258
|
-
// Final erasure is allowed only after every Sandbox reservation has settled.
|
|
259
187
|
const run = transaction((id) => {
|
|
260
188
|
const user = stmts.getUserById.get(id);
|
|
261
189
|
if (!user || (requirePending && user.deletion_state !== 'pending')) return false;
|
|
262
|
-
|
|
263
|
-
const error = new Error('SANDBOX_REMOVE_REQUIRED');
|
|
264
|
-
error.code = 'SANDBOX_REMOVE_REQUIRED';
|
|
265
|
-
throw error;
|
|
266
|
-
}
|
|
267
|
-
if (requirePending) {
|
|
268
|
-
const unsettled = db.prepare(`
|
|
269
|
-
SELECT 1 FROM sandboxes s WHERE s.user_id = ? AND (
|
|
270
|
-
s.observed_state != 'removed' OR s.removed_at IS NULL
|
|
271
|
-
OR EXISTS (SELECT 1 FROM sandbox_operations o WHERE o.sandbox_id = s.id AND o.status IN ('pending', 'running'))
|
|
272
|
-
OR NOT EXISTS (SELECT 1 FROM sandbox_operations o WHERE o.sandbox_id = s.id AND o.kind = 'remove' AND o.status = 'succeeded')
|
|
273
|
-
OR EXISTS (SELECT 1 FROM sandbox_credentials c WHERE c.sandbox_id = s.id AND c.revoked_at IS NULL)
|
|
274
|
-
) LIMIT 1
|
|
275
|
-
`).get(id);
|
|
276
|
-
if (unsettled) return false;
|
|
277
|
-
}
|
|
278
|
-
stmts.deleteReleasedSandboxesForUser.run(id);
|
|
190
|
+
|
|
279
191
|
stmts.deleteIdentitiesForUser.run(id);
|
|
280
192
|
stmts.deleteUserSessionsByUser.run(id);
|
|
281
193
|
stmts.deleteYeaftSessionsByUserCascade.run(id);
|
|
282
194
|
stmts.deleteUserStats.run(id);
|
|
283
195
|
stmts.deleteDailyStatsForUser.run(id);
|
|
284
196
|
stmts.deleteCustomExpertRolesForUser.run(id);
|
|
197
|
+
stmts.deleteLegacySandboxesForUser.run(id);
|
|
285
198
|
stmts.deleteInvitationsCreatedBy.run(id);
|
|
286
199
|
stmts.clearInvitationUsedBy.run(id);
|
|
287
200
|
if (requirePending) {
|
|
@@ -16,11 +16,6 @@ import { handleAgentConnection } from './ws-agent.js';
|
|
|
16
16
|
import { handleWebConnection } from './ws-client.js';
|
|
17
17
|
import { sendToWebClient } from './ws-utils.js';
|
|
18
18
|
import { markAgentHeartbeatPing, markAgentHeartbeatStall, shouldTerminateAgentHeartbeat } from './heartbeat-policy.js';
|
|
19
|
-
import { createSandboxReconciler } from './sandbox-reconciler.js';
|
|
20
|
-
import {
|
|
21
|
-
closeSandboxAttestationListener,
|
|
22
|
-
startSandboxAttestationListener
|
|
23
|
-
} from './sandbox-attestation-listener.js';
|
|
24
19
|
|
|
25
20
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
21
|
|
|
@@ -227,8 +222,6 @@ if (configValidation.warnings) {
|
|
|
227
222
|
console.warn('');
|
|
228
223
|
}
|
|
229
224
|
|
|
230
|
-
let sandboxReconcileTimer = null;
|
|
231
|
-
let sandboxAttestationListener = null;
|
|
232
225
|
const accountDeletionTimer = setInterval(() => {
|
|
233
226
|
try {
|
|
234
227
|
userDb.reconcilePendingDeletions();
|
|
@@ -240,32 +233,20 @@ accountDeletionTimer.unref?.();
|
|
|
240
233
|
userDb.reconcilePendingDeletions();
|
|
241
234
|
|
|
242
235
|
async function startServers() {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
resolve();
|
|
257
|
-
};
|
|
258
|
-
server.once('error', onError);
|
|
259
|
-
server.once('listening', onListening);
|
|
260
|
-
server.listen(CONFIG.port, CONFIG.host);
|
|
261
|
-
});
|
|
262
|
-
} catch (err) {
|
|
263
|
-
await closeSandboxAttestationListener(sandboxAttestationListener);
|
|
264
|
-
sandboxAttestationListener = null;
|
|
265
|
-
throw err;
|
|
266
|
-
}
|
|
236
|
+
await new Promise((resolve, reject) => {
|
|
237
|
+
const onError = (err) => {
|
|
238
|
+
server.off('listening', onListening);
|
|
239
|
+
reject(err);
|
|
240
|
+
};
|
|
241
|
+
const onListening = () => {
|
|
242
|
+
server.off('error', onError);
|
|
243
|
+
resolve();
|
|
244
|
+
};
|
|
245
|
+
server.once('error', onError);
|
|
246
|
+
server.once('listening', onListening);
|
|
247
|
+
server.listen(CONFIG.port, CONFIG.host);
|
|
248
|
+
});
|
|
267
249
|
|
|
268
|
-
sandboxReconcileTimer = createSandboxReconciler({ config: CONFIG.sandbox }).start();
|
|
269
250
|
console.log(`Server running on http://${CONFIG.host || '0.0.0.0'}:${CONFIG.port}`);
|
|
270
251
|
console.log(`Auth mode: ${CONFIG.skipAuth ? 'SKIP (development)' : 'ENABLED'}`);
|
|
271
252
|
if (!CONFIG.skipAuth) {
|
|
@@ -276,7 +257,6 @@ async function startServers() {
|
|
|
276
257
|
|
|
277
258
|
startServers().catch(err => {
|
|
278
259
|
console.error('[Startup] Failed to start server:', err);
|
|
279
|
-
if (sandboxReconcileTimer) clearInterval(sandboxReconcileTimer);
|
|
280
260
|
process.exit(1);
|
|
281
261
|
});
|
|
282
262
|
|
|
@@ -285,14 +265,7 @@ startServers().catch(err => {
|
|
|
285
265
|
// =====================
|
|
286
266
|
async function gracefulShutdown(signal) {
|
|
287
267
|
console.log(`\n[Shutdown] Received ${signal}, starting graceful shutdown...`);
|
|
288
|
-
if (sandboxReconcileTimer) clearInterval(sandboxReconcileTimer);
|
|
289
268
|
clearInterval(accountDeletionTimer);
|
|
290
|
-
try {
|
|
291
|
-
await closeSandboxAttestationListener(sandboxAttestationListener);
|
|
292
|
-
if (sandboxAttestationListener) console.log('[Shutdown] Sandbox attestation listener closed');
|
|
293
|
-
} catch (err) {
|
|
294
|
-
console.error('[Shutdown] Failed to close Sandbox attestation listener:', err.message);
|
|
295
|
-
}
|
|
296
269
|
|
|
297
270
|
// 1. 通知所有 web client 服务即将更新
|
|
298
271
|
const updateMsg = { type: 'server_updating' };
|
|
@@ -1,123 +1,56 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { sandboxDb, userDb } from '../database.js';
|
|
4
|
-
import { SandboxConflictError } from '../db/sandbox-db.js';
|
|
5
|
-
import {
|
|
6
|
-
registerSandboxHostAttestation,
|
|
7
|
-
SandboxHostAttestationError
|
|
8
|
-
} from '../sandbox-host-attestation.js';
|
|
1
|
+
import { userDb } from '../database.js';
|
|
2
|
+
import { containerAgentService } from '../container-agent-service.js';
|
|
9
3
|
|
|
10
4
|
function loadUser(req) {
|
|
11
5
|
let user = userDb.getByUsername(req.user.username);
|
|
12
|
-
if (!user &&
|
|
6
|
+
if (!user && req.user.username === 'dev-user') user = userDb.getOrCreate('dev-user', 'dev-user');
|
|
13
7
|
return user;
|
|
14
8
|
}
|
|
15
9
|
|
|
16
|
-
function sendError(res,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return res.status(500).json({ code: 'SANDBOX_INTERNAL_ERROR' });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function normalizedFingerprint(value) {
|
|
25
|
-
return String(value || '').replaceAll(':', '').trim().toLowerCase();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function authenticateSandboxController(request, expectedFingerprint) {
|
|
29
|
-
if (!request?.socket?.authorized) return false;
|
|
30
|
-
const expected = Buffer.from(normalizedFingerprint(expectedFingerprint));
|
|
31
|
-
const certificate = request.socket.getPeerCertificate?.();
|
|
32
|
-
const actual = Buffer.from(normalizedFingerprint(certificate?.fingerprint256));
|
|
33
|
-
return expected.length > 0 && expected.length === actual.length
|
|
34
|
-
&& timingSafeEqual(expected, actual);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function handleSandboxHostAttestation(req, res, config = CONFIG.sandbox) {
|
|
38
|
-
if (!authenticateSandboxController(req, config.controllerAttestationFingerprint)) {
|
|
39
|
-
return res.status(401).json({ code: 'SANDBOX_CONTROLLER_IDENTITY_REJECTED' });
|
|
40
|
-
}
|
|
41
|
-
try {
|
|
42
|
-
registerSandboxHostAttestation(req.body, config);
|
|
43
|
-
return res.status(202).json({ accepted: true });
|
|
44
|
-
} catch (err) {
|
|
45
|
-
if (err instanceof SandboxHostAttestationError) {
|
|
46
|
-
return res.status(401).json({ code: 'SANDBOX_HOST_ATTESTATION_REJECTED' });
|
|
47
|
-
}
|
|
48
|
-
return sendError(res, err);
|
|
49
|
-
}
|
|
10
|
+
function sendError(res, error) {
|
|
11
|
+
const code = error.code || error.message || 'SANDBOX_INTERNAL_ERROR';
|
|
12
|
+
const known = code.startsWith('SANDBOX_') || code.startsWith('CONTAINER_AGENT_');
|
|
13
|
+
if (!known) console.error('Container Agent API error:', error);
|
|
14
|
+
return res.status(known ? 409 : 500).json({ code: known ? code : 'SANDBOX_INTERNAL_ERROR' });
|
|
50
15
|
}
|
|
51
16
|
|
|
52
|
-
export function registerSandboxRoutes(app, { requireAuth
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
app.post('/api/sandbox/bootstrap/exchange', (req, res) => {
|
|
56
|
-
try {
|
|
57
|
-
const result = sandboxDb.exchangeBootstrap(req.body?.token, req.body?.claims);
|
|
58
|
-
return res.json(result);
|
|
59
|
-
} catch (err) {
|
|
60
|
-
if (err instanceof SandboxConflictError) {
|
|
61
|
-
return res.status(401).json({ code: 'SANDBOX_BOOTSTRAP_INVALID' });
|
|
62
|
-
}
|
|
63
|
-
return sendError(res, err);
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
app.put('/api/admin/sandbox/entitlements/:userId', requireAuth, requireAdmin, (req, res) => {
|
|
68
|
-
try {
|
|
69
|
-
const entitlement = sandboxDb.setEntitlement(
|
|
70
|
-
req.params.userId,
|
|
71
|
-
req.body?.enabled,
|
|
72
|
-
req.user?.username
|
|
73
|
-
);
|
|
74
|
-
return res.json({ entitlement });
|
|
75
|
-
} catch (err) {
|
|
76
|
-
if (err instanceof SandboxConflictError && err.code === 'SANDBOX_USER_NOT_FOUND') {
|
|
77
|
-
return res.status(404).json({ code: err.code });
|
|
78
|
-
}
|
|
79
|
-
return sendError(res, err);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
app.get('/api/sandbox/capability', requireAuth, (req, res) => {
|
|
84
|
-
const user = loadUser(req);
|
|
85
|
-
if (!user) return res.status(404).json({ code: 'USER_NOT_FOUND' });
|
|
86
|
-
res.json(sandboxDb.capability(user.id, CONFIG.sandbox));
|
|
17
|
+
export function registerSandboxRoutes(app, { requireAuth }) {
|
|
18
|
+
app.get('/api/sandbox/capability', requireAuth, (_req, res) => {
|
|
19
|
+
res.json(containerAgentService.capability());
|
|
87
20
|
});
|
|
88
21
|
|
|
89
|
-
app.get('/api/sandbox', requireAuth, (req, res) => {
|
|
22
|
+
app.get('/api/sandbox', requireAuth, async (req, res) => {
|
|
90
23
|
const user = loadUser(req);
|
|
91
24
|
if (!user) return res.status(404).json({ code: 'USER_NOT_FOUND' });
|
|
92
|
-
|
|
25
|
+
try {
|
|
26
|
+
return res.json({ sandbox: await containerAgentService.snapshot(user.id) });
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return sendError(res, error);
|
|
29
|
+
}
|
|
93
30
|
});
|
|
94
31
|
|
|
95
|
-
app.post('/api/sandbox', requireAuth, (req, res) => {
|
|
32
|
+
app.post('/api/sandbox', requireAuth, async (req, res) => {
|
|
96
33
|
const user = loadUser(req);
|
|
97
34
|
if (!user) return res.status(404).json({ code: 'USER_NOT_FOUND' });
|
|
98
35
|
try {
|
|
99
|
-
const
|
|
36
|
+
const agentSecret = userDb.getAgentSecret(user.id) || userDb.resetAgentSecret(user.id);
|
|
37
|
+
const result = await containerAgentService.create({ ...user, agent_secret: agentSecret }, {
|
|
100
38
|
agentName: req.body?.agentName,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return res
|
|
105
|
-
} catch (err) {
|
|
106
|
-
return sendError(res, err);
|
|
39
|
+
});
|
|
40
|
+
return res.status(201).json(result);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return sendError(res, error);
|
|
107
43
|
}
|
|
108
44
|
});
|
|
109
45
|
|
|
110
46
|
for (const action of ['start', 'stop', 'retry', 'remove']) {
|
|
111
|
-
app.post(`/api/sandbox/${action}`, requireAuth, (req, res) => {
|
|
47
|
+
app.post(`/api/sandbox/${action}`, requireAuth, async (req, res) => {
|
|
112
48
|
const user = loadUser(req);
|
|
113
49
|
if (!user) return res.status(404).json({ code: 'USER_NOT_FOUND' });
|
|
114
50
|
try {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
);
|
|
118
|
-
return res.status(result.replayed ? 200 : 202).json(result);
|
|
119
|
-
} catch (err) {
|
|
120
|
-
return sendError(res, err);
|
|
51
|
+
return res.json(await containerAgentService.action(user.id, action));
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return sendError(res, error);
|
|
121
54
|
}
|
|
122
55
|
});
|
|
123
56
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from '../auth/session-store.js';
|
|
8
8
|
import { clearSessionCookie } from '../auth/request-auth.js';
|
|
9
9
|
import { agents, webClients, userStatsDeltas } from '../context.js';
|
|
10
|
+
import { containerAgentService } from '../container-agent-service.js';
|
|
10
11
|
|
|
11
12
|
// 过滤用户敏感字段
|
|
12
13
|
function sanitizeUser(user) {
|
|
@@ -154,6 +155,9 @@ export function registerUserRoutes(app, { requireAuth, requireAdmin }) {
|
|
|
154
155
|
}
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
// Remove the Server-managed container before deleting the owner record.
|
|
159
|
+
// Manually launched remote container Agents remain outside Server lifecycle control.
|
|
160
|
+
await containerAgentService.action(user.id, 'remove');
|
|
157
161
|
const deletion = userDb.beginDeletion(user.id);
|
|
158
162
|
if (!deletion) return res.status(404).json({ error: 'User not found or already deleted' });
|
|
159
163
|
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from 'crypto';
|
|
|
2
2
|
import { WebSocket } from 'ws';
|
|
3
3
|
import { CONFIG } from './config.js';
|
|
4
4
|
import { verifyAgent } from './auth.js';
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
import { encodeKey } from './encryption.js';
|
|
7
7
|
import { agents, pendingAgentConnections } from './context.js';
|
|
8
8
|
import { userDb } from './database.js';
|
|
@@ -104,15 +104,11 @@ export function handleAgentConnection(ws, url) {
|
|
|
104
104
|
clearTimeout(pending.timeout);
|
|
105
105
|
pendingAgentConnections.delete(tempId);
|
|
106
106
|
|
|
107
|
-
const
|
|
108
|
-
?
|
|
109
|
-
:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
? { valid: false, sessionKey: null, userId: null, username: null }
|
|
113
|
-
: skipAgentAuth
|
|
114
|
-
? { valid: true, sessionKey: null, userId: null, username: null }
|
|
115
|
-
: verifyAgent(msg.secret));
|
|
107
|
+
const authResult = msg.authKind
|
|
108
|
+
? { valid: false, sessionKey: null, userId: null, username: null }
|
|
109
|
+
: skipAgentAuth
|
|
110
|
+
? { valid: true, sessionKey: null, userId: null, username: null }
|
|
111
|
+
: verifyAgent(msg.secret);
|
|
116
112
|
if (!authResult.valid) {
|
|
117
113
|
pruneAgentConnectionGenerations();
|
|
118
114
|
console.log(`Agent auth failed: ${agentName}`);
|
|
@@ -122,21 +118,15 @@ export function handleAgentConnection(ws, url) {
|
|
|
122
118
|
|
|
123
119
|
const capabilities = Array.isArray(msg.capabilities) ? msg.capabilities : urlCapabilities;
|
|
124
120
|
const agentVersion = msg.version || null;
|
|
125
|
-
if (sandboxAuth && !capabilities.includes('managed-sandbox')) {
|
|
126
|
-
pruneAgentConnectionGenerations();
|
|
127
|
-
ws.close(1008, 'Invalid Sandbox Agent capability');
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
121
|
// Local no-auth mode still has one durable browser owner. This makes
|
|
131
122
|
// the server-side Session catalog persistent without changing generic
|
|
132
123
|
// development-server behavior, which remains ownerless.
|
|
133
124
|
const localOwner = skipAgentAuth && process.env.YEAFT_LOCAL_RUN === 'true'
|
|
134
125
|
? userDb.getOrCreate('dev-user')
|
|
135
126
|
: null;
|
|
136
|
-
const ownerId =
|
|
127
|
+
const ownerId = localOwner?.id || authResult.userId;
|
|
137
128
|
const ownerUsername = localOwner?.username || authResult.username || null;
|
|
138
|
-
const registeredInstanceId =
|
|
139
|
-
|| pending.instanceId || pending.agentId || pending.agentName;
|
|
129
|
+
const registeredInstanceId = pending.instanceId || pending.agentId || pending.agentName;
|
|
140
130
|
// Authenticated Agents use an owner-scoped key. SKIP_AUTH preserves
|
|
141
131
|
// its historical unscoped id while still receiving version metadata.
|
|
142
132
|
resolvedAgentId = skipAgentAuth
|
|
@@ -159,11 +149,6 @@ export function handleAgentConnection(ws, url) {
|
|
|
159
149
|
ownerUsername,
|
|
160
150
|
agentVersion,
|
|
161
151
|
registeredInstanceId,
|
|
162
|
-
sandboxAuth ? {
|
|
163
|
-
sandboxId: sandboxAuth.sandboxId,
|
|
164
|
-
generation: sandboxAuth.generation,
|
|
165
|
-
imageDigest: sandboxAuth.imageDigest,
|
|
166
|
-
} : null,
|
|
167
152
|
);
|
|
168
153
|
pruneAgentConnectionGenerations();
|
|
169
154
|
}
|
|
@@ -248,7 +233,7 @@ function handleAgentDisconnect(agentId, agentName, ws) {
|
|
|
248
233
|
broadcastAgentList();
|
|
249
234
|
}
|
|
250
235
|
|
|
251
|
-
function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey, capabilities = [], ownerId = null, ownerUsername = null, agentVersion = null, instanceId = null
|
|
236
|
+
function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey, capabilities = [], ownerId = null, ownerUsername = null, agentVersion = null, instanceId = null) {
|
|
252
237
|
// 如果是重连,保留 conversations;否则(server 重启)创建空 Map
|
|
253
238
|
const existingAgent = agents.get(agentId);
|
|
254
239
|
const conversations = existingAgent?.conversations || new Map();
|
|
@@ -285,13 +270,12 @@ function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey,
|
|
|
285
270
|
ownerId,
|
|
286
271
|
ownerUsername,
|
|
287
272
|
version: agentVersion,
|
|
288
|
-
encryptOutbound
|
|
289
|
-
sandboxIdentity
|
|
273
|
+
encryptOutbound
|
|
290
274
|
});
|
|
291
275
|
|
|
292
276
|
const syncTimeout = setTimeout(() => {
|
|
293
277
|
const ag = agents.get(agentId);
|
|
294
|
-
if (ag?.ws === ws && ag.status === 'syncing'
|
|
278
|
+
if (ag?.ws === ws && ag.status === 'syncing') {
|
|
295
279
|
console.warn(`[Sync] Agent ${agentName} sync timeout, forcing ready`);
|
|
296
280
|
ag.status = 'ready';
|
|
297
281
|
broadcastAgentList();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.387"}
|