@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.
@@ -1,50 +1,6 @@
1
- import { createHash, randomUUID } from 'crypto';
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
- db.prepare('UPDATE sandbox_entitlements SET enabled = 0, updated_at = ? WHERE user_id = ?')
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(now = Date.now()) {
131
- const pending = db.prepare("SELECT id, deletion_id FROM users WHERE deletion_state = 'pending'").all();
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 (stmts.getReservedSandboxForUser.get(user.id)) {
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
- if (stmts.getReservedSandboxForUser.get(id)) {
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
- sandboxAttestationListener = await startSandboxAttestationListener({ config: CONFIG.sandbox });
244
- if (sandboxAttestationListener) {
245
- console.log(`Sandbox Host attestation listener running on https://${CONFIG.sandbox.hostAttestationListenerHost}:${CONFIG.sandbox.hostAttestationListenerPort}`);
246
- }
247
-
248
- try {
249
- await new Promise((resolve, reject) => {
250
- const onError = (err) => {
251
- server.off('listening', onListening);
252
- reject(err);
253
- };
254
- const onListening = () => {
255
- server.off('error', onError);
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 { timingSafeEqual } from 'crypto';
2
- import { CONFIG } from '../config.js';
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 && CONFIG.skipAuth) user = userDb.getOrCreate(req.user.username, req.user.username);
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, err) {
17
- if (err instanceof SandboxConflictError) {
18
- return res.status(409).json({ code: err.code });
19
- }
20
- console.error('Sandbox API error:', err);
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, requireAdmin }) {
53
- // The managed runtime has no user Agent secret. Its one-time bootstrap token
54
- // is itself the scoped authorization for obtaining a revocable credential.
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
- res.json({ sandbox: sandboxDb.snapshot(user.id) });
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 result = sandboxDb.create(user.id, {
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
- sizeId: req.body?.sizeId,
102
- idempotencyKey: req.get('Idempotency-Key')
103
- }, CONFIG.sandbox);
104
- return res.status(result.replayed ? 200 : 202).json(result);
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
- const result = sandboxDb.requestAction(
116
- user.id, action, req.get('Idempotency-Key'), CONFIG.sandbox
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
- import { authenticateSandboxAgent, canForceReadyAfterSyncTimeout } from './sandbox-agent-auth.js';
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 sandboxAuth = !skipAgentAuth && msg.authKind === 'sandbox'
108
- ? authenticateSandboxAgent(msg, pending)
109
- : null;
110
- const authResult = sandboxAuth
111
- || (msg.authKind
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 = sandboxAuth?.userId || localOwner?.id || authResult.userId;
127
+ const ownerId = localOwner?.id || authResult.userId;
137
128
  const ownerUsername = localOwner?.username || authResult.username || null;
138
- const registeredInstanceId = sandboxAuth?.instanceId
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, sandboxIdentity = 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' && canForceReadyAfterSyncTimeout(ag)) {
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.385"}
1
+ {"version":"1.0.387"}