@mindexec/cli 0.2.151 → 0.2.152

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.151",
3
+ "version": "0.2.152",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -83,10 +83,17 @@ function createRegistryTarget({ endpoint, endpointCandidates, leaseId, active =
83
83
  };
84
84
  }
85
85
 
86
- function startFakeSupabase(getTarget) {
86
+ function startFakeSupabase(getTarget, setTarget = null) {
87
87
  const requests = [];
88
88
  const realtimeClients = new Set();
89
- const server = createServer((req, res) => {
89
+ const server = createServer(async (req, res) => {
90
+ const readBody = () => new Promise(resolve => {
91
+ let body = '';
92
+ req.on('data', chunk => {
93
+ body += chunk.toString();
94
+ });
95
+ req.on('end', () => resolve(body));
96
+ });
90
97
  const parsed = new URL(req.url || '/', 'http://127.0.0.1');
91
98
  requests.push({
92
99
  method: req.method,
@@ -107,6 +114,40 @@ function startFakeSupabase(getTarget) {
107
114
  return;
108
115
  }
109
116
 
117
+ if (req.method === 'POST' && parsed.pathname === '/rest/v1/rpc/set_remote_host_target') {
118
+ assert.equal(String(req.headers.apikey || ''), SUPABASE_KEY);
119
+ assert.equal(String(req.headers.authorization || ''), `Bearer ${ACCESS_TOKEN}`);
120
+ const body = JSON.parse(await readBody() || '{}');
121
+ const now = Date.now();
122
+ const existing = getTarget();
123
+ const sameLease = existing
124
+ && String(existing.lease_id || '') === String(body.p_lease_id || '')
125
+ && String(existing.host_instance_id || '') === String(body.p_host_instance_id || '');
126
+ const expired = !existing?.expires_at || Date.parse(existing.expires_at) <= now;
127
+ if (existing?.active === true && !sameLease && !expired && body.p_takeover !== true) {
128
+ res.writeHead(200, { 'Content-Type': 'application/json' });
129
+ res.end(JSON.stringify([{ ok: false, reason: 'host-target-taken' }]));
130
+ return;
131
+ }
132
+
133
+ setTarget?.({
134
+ user_id: USER_ID,
135
+ active: true,
136
+ endpoint: String(body.p_endpoint || ''),
137
+ endpoint_candidates: Array.isArray(body.p_endpoint_candidates)
138
+ ? body.p_endpoint_candidates
139
+ : [String(body.p_endpoint || '')].filter(Boolean),
140
+ pair_token: String(body.p_pair_token || ''),
141
+ lease_id: String(body.p_lease_id || ''),
142
+ node_id: String(body.p_node_id || ''),
143
+ host_instance_id: String(body.p_host_instance_id || ''),
144
+ expires_at: String(body.p_expires_at || new Date(Date.now() + 60_000).toISOString())
145
+ });
146
+ res.writeHead(200, { 'Content-Type': 'application/json' });
147
+ res.end(JSON.stringify([{ ok: true, reason: 'ok' }]));
148
+ return;
149
+ }
150
+
110
151
  res.writeHead(404, { 'Content-Type': 'application/json' });
111
152
  res.end(JSON.stringify({ error: 'not-found' }));
112
153
  });
@@ -230,7 +271,7 @@ function startFakeSupabase(getTarget) {
230
271
  });
231
272
  }
232
273
 
233
- function spawnBridge({ bridgePort, remoteHubPort, workspacePath, authRoot, label, supabaseUrl = '', follower = false }) {
274
+ function spawnBridge({ bridgePort, remoteHubPort, workspacePath, authRoot, label, supabaseUrl = '', follower = false, publicHost = '' }) {
234
275
  const child = spawn(process.execPath, ['server.js'], {
235
276
  cwd: LOCAL_BRIDGE_DIR,
236
277
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -243,6 +284,7 @@ function spawnBridge({ bridgePort, remoteHubPort, workspacePath, authRoot, label
243
284
  MINDEXEC_REMOTE_HUB: '1',
244
285
  REMOTE_HUB_HOST: '127.0.0.1',
245
286
  REMOTE_HUB_PORT: String(remoteHubPort),
287
+ REMOTE_HUB_PUBLIC_HOST: publicHost,
246
288
  REMOTE_HUB_PAIR_TOKEN: PAIR_TOKEN,
247
289
  WORKSPACE_PATH: workspacePath,
248
290
  MINDEXEC_AUTH_DATA_ROOT: authRoot,
@@ -251,6 +293,7 @@ function spawnBridge({ bridgePort, remoteHubPort, workspacePath, authRoot, label
251
293
  MINDEXEC_REMOTE_REGISTRY_FAST_RETRY_MS: '250',
252
294
  MINDEXEC_REMOTE_REGISTRY_REALTIME_RECONNECT_MS: '250',
253
295
  MINDEXEC_REMOTE_REGISTRY_REALTIME_DEBOUNCE_MS: '100',
296
+ MINDEXEC_REMOTE_HOST_TARGET_RENEW_MS: '5000',
254
297
  SUPABASE_URL: supabaseUrl,
255
298
  SUPABASE_KEY,
256
299
  NO_COLOR: '1'
@@ -353,10 +396,13 @@ function killProcess(pid) {
353
396
  async function main() {
354
397
  const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'mindexec-remote-registry-smoke-'));
355
398
  let fakeSupabase = null;
399
+ let renewHost = null;
356
400
  let hostA = null;
357
401
  let hostB = null;
358
402
  let client = null;
359
403
 
404
+ const renewHostBridgePort = await findFreePort();
405
+ const renewHostRemotePort = await findFreePort();
360
406
  const hostABridgePort = await findFreePort();
361
407
  const hostARemotePort = await findFreePort();
362
408
  const hostBBridgePort = await findFreePort();
@@ -364,23 +410,82 @@ async function main() {
364
410
  const clientBridgePort = await findFreePort();
365
411
  const clientRemotePort = await findFreePort();
366
412
  const stalePort = await findFreePort();
413
+ const renewPublicHost = '192.0.2.10';
414
+ const renewPublicEndpoint = `${renewPublicHost}:${renewHostRemotePort}`;
367
415
  const hostAEndpoint = `127.0.0.1:${hostARemotePort}`;
368
416
  const hostBEndpoint = `127.0.0.1:${hostBRemotePort}`;
369
417
  const staleEndpoint = `127.0.0.1:${stalePort}`;
370
- let currentTarget = createRegistryTarget({
371
- endpoint: hostAEndpoint,
372
- endpointCandidates: [staleEndpoint, hostAEndpoint],
373
- leaseId: 'lease-a'
374
- });
418
+ let currentTarget = null;
375
419
 
376
420
  try {
377
- fakeSupabase = await startFakeSupabase(() => currentTarget);
421
+ fakeSupabase = await startFakeSupabase(
422
+ () => currentTarget,
423
+ target => {
424
+ currentTarget = target;
425
+ });
378
426
 
427
+ const renewHostAuth = path.join(tempRoot, 'auth-renew-host');
379
428
  const hostAAuth = path.join(tempRoot, 'auth-host-a');
380
429
  const hostBAuth = path.join(tempRoot, 'auth-host-b');
381
430
  const clientAuth = path.join(tempRoot, 'auth-client');
431
+ await writeSession(renewHostAuth);
382
432
  await writeSession(clientAuth);
383
433
 
434
+ renewHost = spawnBridge({
435
+ bridgePort: renewHostBridgePort,
436
+ remoteHubPort: renewHostRemotePort,
437
+ workspacePath: path.join(tempRoot, 'renew-host'),
438
+ authRoot: renewHostAuth,
439
+ label: 'renew-host',
440
+ supabaseUrl: fakeSupabase.url,
441
+ follower: true,
442
+ publicHost: renewPublicHost
443
+ });
444
+ await waitForBridge(renewHost);
445
+
446
+ const renewSetHost = await fetchJson(`${renewHost.baseUrl}/api/remote/host-target`, {
447
+ method: 'POST',
448
+ token: BRIDGE_TOKEN,
449
+ body: JSON.stringify({
450
+ nodeId: 'remote-registry-renew-node',
451
+ enabled: true,
452
+ leaseMs: 60000
453
+ })
454
+ });
455
+ assert.equal(renewSetHost.ok, true, JSON.stringify(renewSetHost.payload));
456
+ assert.equal(renewSetHost.payload?.ok, true, JSON.stringify(renewSetHost.payload));
457
+ assert.equal(renewSetHost.payload?.active, true, JSON.stringify(renewSetHost.payload));
458
+ assert.equal(renewSetHost.payload?.hostTargetRenew?.status, 'active', JSON.stringify(renewSetHost.payload?.hostTargetRenew));
459
+ assert.equal(currentTarget?.endpoint, renewPublicEndpoint, JSON.stringify(currentTarget));
460
+ assert.equal(currentTarget?.node_id, 'remote-registry-renew-node', JSON.stringify(currentTarget));
461
+ await waitFor(() => {
462
+ const publishCount = fakeSupabase.requests.filter(request =>
463
+ request.method === 'POST'
464
+ && request.pathname === '/rest/v1/rpc/set_remote_host_target').length;
465
+ return publishCount >= 2 ? publishCount : null;
466
+ }, 8000, `host-target auto renew publish\n${renewHost.details()}`);
467
+ const renewStatus = await fetchJson(`${renewHost.baseUrl}/api/status`);
468
+ assert.equal(renewStatus.payload?.remoteHostTargetRenew?.status, 'active', JSON.stringify(renewStatus.payload?.remoteHostTargetRenew));
469
+ assert.equal(renewStatus.payload?.remoteHostTargetRenew?.endpoint, renewPublicEndpoint, JSON.stringify(renewStatus.payload?.remoteHostTargetRenew));
470
+
471
+ const renewClear = await fetchJson(`${renewHost.baseUrl}/api/remote/host-target`, {
472
+ method: 'DELETE',
473
+ token: BRIDGE_TOKEN,
474
+ body: JSON.stringify({
475
+ nodeId: 'remote-registry-renew-node'
476
+ })
477
+ });
478
+ assert.equal(renewClear.ok, true, JSON.stringify(renewClear.payload));
479
+ assert.equal(renewClear.payload?.ok, true, JSON.stringify(renewClear.payload));
480
+ await renewHost.stop();
481
+ renewHost = null;
482
+
483
+ currentTarget = createRegistryTarget({
484
+ endpoint: hostAEndpoint,
485
+ endpointCandidates: [staleEndpoint, hostAEndpoint],
486
+ leaseId: 'lease-a'
487
+ });
488
+
384
489
  hostA = spawnBridge({
385
490
  bridgePort: hostABridgePort,
386
491
  remoteHubPort: hostARemotePort,
@@ -465,6 +570,7 @@ async function main() {
465
570
  if (client) await client.stop();
466
571
  if (hostB) await hostB.stop();
467
572
  if (hostA) await hostA.stop();
573
+ if (renewHost) await renewHost.stop();
468
574
  if (fakeSupabase) await fakeSupabase.stop();
469
575
  await rm(tempRoot, { recursive: true, force: true });
470
576
  }
package/server.js CHANGED
@@ -3146,6 +3146,15 @@ const REMOTE_REGISTRY_REALTIME_ENABLED = REMOTE_REGISTRY_FOLLOWER_ENABLED
3146
3146
  const REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS = Math.max(10000, Number(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS || 25000) || 25000);
3147
3147
  const REMOTE_REGISTRY_REALTIME_RECONNECT_MS = Math.max(1000, Number(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME_RECONNECT_MS || 2500) || 2500);
3148
3148
  const REMOTE_REGISTRY_REALTIME_DEBOUNCE_MS = Math.max(100, Number(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME_DEBOUNCE_MS || 250) || 250);
3149
+ const REMOTE_HOST_TARGET_AUTO_RENEW_ENABLED = REMOTE_REGISTRY_FOLLOWER_ENABLED
3150
+ && !/^(0|false|no|off)$/i.test(String(process.env.MINDEXEC_REMOTE_HOST_TARGET_AUTO_RENEW || 'true'));
3151
+ const REMOTE_HOST_TARGET_LEASE_MS = Math.max(60000, Number(process.env.MINDEXEC_REMOTE_HOST_TARGET_LEASE_MS || 120000) || 120000);
3152
+ const REMOTE_HOST_TARGET_RENEW_MS = Math.max(
3153
+ 5000,
3154
+ Math.min(
3155
+ Math.floor(REMOTE_HOST_TARGET_LEASE_MS / 2),
3156
+ Number(process.env.MINDEXEC_REMOTE_HOST_TARGET_RENEW_MS || 25000) || 25000));
3157
+ const REMOTE_HOST_TARGET_RENEW_LOG_REPEAT_MS = 60000;
3149
3158
  let remoteAgentState = createRemoteAgentIdleState();
3150
3159
  let remoteAgentSyncReportState = null;
3151
3160
  let remoteAgentSyncReportLogKey = '';
@@ -3168,6 +3177,10 @@ let remoteRegistryRealtimeHeartbeatTimer = null;
3168
3177
  let remoteRegistryRealtimeReconnectTimer = null;
3169
3178
  let remoteRegistryRealtimeReconnectContext = null;
3170
3179
  let remoteRegistryRealtimeLastWakeAt = 0;
3180
+ let remoteHostTargetRenewTimer = null;
3181
+ let remoteHostTargetRenewInFlight = false;
3182
+ let remoteHostTargetRenewLogKey = '';
3183
+ let remoteHostTargetRenewLogAt = 0;
3171
3184
  let remoteRegistryFollowerState = {
3172
3185
  enabled: REMOTE_REGISTRY_FOLLOWER_ENABLED,
3173
3186
  status: REMOTE_REGISTRY_FOLLOWER_ENABLED ? 'idle' : 'disabled',
@@ -3201,6 +3214,20 @@ let remoteRegistryRealtimeState = {
3201
3214
  changes: 0,
3202
3215
  wakeups: 0
3203
3216
  };
3217
+ let remoteHostTargetRenewState = {
3218
+ enabled: REMOTE_HOST_TARGET_AUTO_RENEW_ENABLED,
3219
+ status: REMOTE_HOST_TARGET_AUTO_RENEW_ENABLED ? 'idle' : 'disabled',
3220
+ reason: '',
3221
+ nodeId: '',
3222
+ leaseId: '',
3223
+ hostInstanceId: '',
3224
+ endpoint: '',
3225
+ endpointCandidates: [],
3226
+ expiresAt: '',
3227
+ lastAttemptAt: '',
3228
+ lastSuccessAt: '',
3229
+ lastError: ''
3230
+ };
3204
3231
 
3205
3232
  function createRemoteAgentIdleState(overrides = {}) {
3206
3233
  return {
@@ -4499,6 +4526,23 @@ function updateRemoteRegistryFollowerState(patch = {}) {
4499
4526
  emitBridgeEvent('RemoteRegistryFollowerUpdated', serializeRemoteRegistryFollowerState());
4500
4527
  }
4501
4528
 
4529
+ function serializeRemoteHostTargetRenewState() {
4530
+ return {
4531
+ ...remoteHostTargetRenewState,
4532
+ leaseMs: REMOTE_HOST_TARGET_LEASE_MS,
4533
+ renewMs: REMOTE_HOST_TARGET_RENEW_MS
4534
+ };
4535
+ }
4536
+
4537
+ function updateRemoteHostTargetRenewState(patch = {}) {
4538
+ remoteHostTargetRenewState = {
4539
+ ...remoteHostTargetRenewState,
4540
+ ...patch,
4541
+ enabled: REMOTE_HOST_TARGET_AUTO_RENEW_ENABLED
4542
+ };
4543
+ emitBridgeEvent('RemoteHostTargetRenewUpdated', serializeRemoteHostTargetRenewState());
4544
+ }
4545
+
4502
4546
  function serializeRemoteRegistryRealtimeState() {
4503
4547
  return {
4504
4548
  ...remoteRegistryRealtimeState,
@@ -5094,6 +5138,328 @@ async function fetchRemoteRegistryTarget(config, session) {
5094
5138
  return normalizeRemoteRegistryTarget(Array.isArray(payload) ? payload[0] : payload);
5095
5139
  }
5096
5140
 
5141
+ async function callRemoteRegistryRpc(config, session, functionName, payload) {
5142
+ const url = new URL(`/rest/v1/rpc/${functionName}`, config.url);
5143
+ const response = await fetch(url.toString(), {
5144
+ method: 'POST',
5145
+ headers: {
5146
+ apikey: config.key,
5147
+ Authorization: `Bearer ${session.accessToken}`,
5148
+ Accept: 'application/json',
5149
+ 'Content-Type': 'application/json'
5150
+ },
5151
+ body: JSON.stringify(payload)
5152
+ });
5153
+
5154
+ if (!response.ok) {
5155
+ const text = await response.text().catch(() => '');
5156
+ const error = new Error(`registry-rpc-${functionName}-${response.status}${text ? `:${shortenText(text, 160)}` : ''}`);
5157
+ error.statusCode = response.status;
5158
+ throw error;
5159
+ }
5160
+
5161
+ const rpcPayload = await response.json().catch(() => null);
5162
+ const row = Array.isArray(rpcPayload) ? rpcPayload[0] : rpcPayload;
5163
+ return {
5164
+ ok: row?.ok === true || row?.Ok === true,
5165
+ reason: safeRemoteAgentField(row?.reason || row?.Reason || '', 160)
5166
+ };
5167
+ }
5168
+
5169
+ async function readRemoteRegistryContext() {
5170
+ const config = readSupabaseRuntimeConfig();
5171
+ if (!config.url || !config.key) {
5172
+ return { ok: false, reason: 'supabase-config-missing' };
5173
+ }
5174
+
5175
+ const sessionPayload = await readStableAuthSessionPayload();
5176
+ const session = sessionPayload.found ? parseSupabaseSessionForRegistry(sessionPayload.content) : null;
5177
+ if (!session) {
5178
+ return { ok: false, reason: 'registry-not-authenticated' };
5179
+ }
5180
+
5181
+ if (session.expiresAtMs && session.expiresAtMs <= Date.now()) {
5182
+ return { ok: false, reason: 'session-expired' };
5183
+ }
5184
+
5185
+ return { ok: true, config, session };
5186
+ }
5187
+
5188
+ function getRemoteHostTargetEndpointReason(endpoint) {
5189
+ const normalized = normalizeRemoteManagerEndpoint(endpoint);
5190
+ if (!normalized) {
5191
+ return 'invalid-manager-endpoint';
5192
+ }
5193
+
5194
+ const match = normalized.match(/^(\[[^\]]+\]|[^:\s]+):(\d{1,5})$/);
5195
+ const host = String(match?.[1] || '').replace(/^\[|\]$/g, '').toLowerCase();
5196
+ if (!host || host === 'localhost' || host === '::1' || host === '0:0:0:0:0:0:0:1' || /^127\./.test(host)) {
5197
+ return 'loopback-endpoint';
5198
+ }
5199
+
5200
+ if (host === '0.0.0.0' || host === '::' || host === '*') {
5201
+ return 'wildcard-endpoint';
5202
+ }
5203
+
5204
+ if (/^169\.254\./.test(host)) {
5205
+ return 'link-local-endpoint';
5206
+ }
5207
+
5208
+ return 'ok';
5209
+ }
5210
+
5211
+ function buildRemoteHostTargetRegistryPayload(hub) {
5212
+ const rawEndpointCandidates = normalizeRemoteManagerEndpointList(
5213
+ hub?.hostTargetEndpointCandidates,
5214
+ hub?.agentEndpointCandidates,
5215
+ hub?.hostTargetEndpoint,
5216
+ hub?.agentEndpoint);
5217
+ const endpointCandidates = rawEndpointCandidates
5218
+ .filter(endpoint => getRemoteHostTargetEndpointReason(endpoint) === 'ok')
5219
+ .slice(0, 12);
5220
+ const endpoint = endpointCandidates[0] || '';
5221
+ const pairToken = safeRemoteAgentField(hub?.pairToken, 512);
5222
+ const leaseId = safeRemoteAgentField(hub?.hostTargetLeaseId, 128);
5223
+ const hostInstanceId = safeRemoteAgentField(hub?.hostTargetHostInstanceId || hub?.hostInstanceId, 128);
5224
+ const nodeId = safeRemoteAgentField(hub?.hostTargetNodeId, 128);
5225
+ const activatedAt = safeRemoteAgentField(hub?.hostTargetActivatedAt, 80) || new Date().toISOString();
5226
+ const expiresAt = safeRemoteAgentField(hub?.hostTargetExpiresAt, 80) || new Date(Date.now() + REMOTE_HOST_TARGET_LEASE_MS).toISOString();
5227
+
5228
+ if (!pairToken || !leaseId || !hostInstanceId || !nodeId) {
5229
+ return {
5230
+ ok: false,
5231
+ reason: 'missing-fields',
5232
+ endpoint: '',
5233
+ endpointCandidates: []
5234
+ };
5235
+ }
5236
+
5237
+ if (endpointCandidates.length === 0) {
5238
+ const reason = rawEndpointCandidates.length === 0
5239
+ ? safeRemoteAgentField(hub?.agentEndpointRouteReason || 'missing-endpoint-candidates', 160)
5240
+ : getRemoteHostTargetEndpointReason(rawEndpointCandidates[0]);
5241
+ return {
5242
+ ok: false,
5243
+ reason,
5244
+ endpoint: rawEndpointCandidates[0] || '',
5245
+ endpointCandidates: rawEndpointCandidates
5246
+ };
5247
+ }
5248
+
5249
+ return {
5250
+ ok: true,
5251
+ endpoint,
5252
+ endpointCandidates,
5253
+ leaseId,
5254
+ hostInstanceId,
5255
+ nodeId,
5256
+ expiresAt,
5257
+ payload: {
5258
+ p_node_id: nodeId,
5259
+ p_lease_id: leaseId,
5260
+ p_host_instance_id: hostInstanceId,
5261
+ p_endpoint: endpoint,
5262
+ p_pair_token: pairToken,
5263
+ p_manager_package: safeRemoteAgentField(hub?.managerPackage || '@mindexec/cli', 160),
5264
+ p_manager_version: safeRemoteAgentField(hub?.managerVersion || '', 80),
5265
+ p_agent_package: safeRemoteAgentField(hub?.agentPackage || '@mindexec/remote', 160),
5266
+ p_activated_at: activatedAt,
5267
+ p_expires_at: expiresAt,
5268
+ p_endpoint_candidates: endpointCandidates
5269
+ }
5270
+ };
5271
+ }
5272
+
5273
+ function logRemoteHostTargetRenew(status, reason, endpoint = '') {
5274
+ const key = [status, reason, endpoint].join('|');
5275
+ const now = Date.now();
5276
+ if (key === remoteHostTargetRenewLogKey
5277
+ && now - remoteHostTargetRenewLogAt < REMOTE_HOST_TARGET_RENEW_LOG_REPEAT_MS) {
5278
+ return;
5279
+ }
5280
+
5281
+ remoteHostTargetRenewLogKey = key;
5282
+ remoteHostTargetRenewLogAt = now;
5283
+ logEvent(
5284
+ 'remote',
5285
+ `host target renew ${status} ${formatKeyValue('reason', reason || '-')} ${formatKeyValue('endpoint', endpoint || '-')}`,
5286
+ status === 'ok' ? 'remote' : 'warn');
5287
+ }
5288
+
5289
+ function isRemoteHostTargetRenewSoftSkipReason(reason) {
5290
+ return /^(registry-not-authenticated|session-expired|supabase-config-missing|missing-endpoint-candidates|loopback-endpoint|wildcard-endpoint|link-local-endpoint)$/i
5291
+ .test(String(reason || '').trim());
5292
+ }
5293
+
5294
+ async function publishLocalRemoteHostTargetToRegistry(hub, { takeover = false } = {}) {
5295
+ const registryPayload = buildRemoteHostTargetRegistryPayload(hub);
5296
+ if (!registryPayload.ok) {
5297
+ return {
5298
+ ok: false,
5299
+ active: false,
5300
+ reason: registryPayload.reason,
5301
+ endpoint: registryPayload.endpoint,
5302
+ endpointCandidates: registryPayload.endpointCandidates,
5303
+ stale: false
5304
+ };
5305
+ }
5306
+
5307
+ const context = await readRemoteRegistryContext();
5308
+ if (!context.ok) {
5309
+ return {
5310
+ ok: false,
5311
+ active: false,
5312
+ reason: context.reason,
5313
+ endpoint: registryPayload.endpoint,
5314
+ endpointCandidates: registryPayload.endpointCandidates,
5315
+ stale: false
5316
+ };
5317
+ }
5318
+
5319
+ const row = await callRemoteRegistryRpc(
5320
+ context.config,
5321
+ context.session,
5322
+ 'set_remote_host_target',
5323
+ {
5324
+ ...registryPayload.payload,
5325
+ p_takeover: takeover === true
5326
+ });
5327
+ const reason = row.reason || (row.ok ? 'ok' : 'registry-publish-failed');
5328
+ return {
5329
+ ok: row.ok,
5330
+ active: row.ok,
5331
+ reason,
5332
+ endpoint: registryPayload.endpoint,
5333
+ endpointCandidates: registryPayload.endpointCandidates,
5334
+ leaseId: registryPayload.leaseId,
5335
+ nodeId: registryPayload.nodeId,
5336
+ hostInstanceId: registryPayload.hostInstanceId,
5337
+ expiresAt: registryPayload.expiresAt,
5338
+ stale: reason === 'host-target-taken'
5339
+ };
5340
+ }
5341
+
5342
+ function scheduleRemoteHostTargetRenew(delayMs = REMOTE_HOST_TARGET_RENEW_MS, reason = 'timer') {
5343
+ if (!REMOTE_HOST_TARGET_AUTO_RENEW_ENABLED || isShuttingDown) {
5344
+ return;
5345
+ }
5346
+
5347
+ if (remoteHostTargetRenewTimer) {
5348
+ clearTimeout(remoteHostTargetRenewTimer);
5349
+ remoteHostTargetRenewTimer = null;
5350
+ }
5351
+
5352
+ remoteHostTargetRenewTimer = setTimeout(() => {
5353
+ remoteHostTargetRenewTimer = null;
5354
+ runRemoteHostTargetRenewOnce(reason).catch(error => {
5355
+ const message = error?.message || String(error || '');
5356
+ updateRemoteHostTargetRenewState({
5357
+ status: 'error',
5358
+ reason: 'renew-error',
5359
+ lastAttemptAt: new Date().toISOString(),
5360
+ lastError: message
5361
+ });
5362
+ logRemoteHostTargetRenew('failed', message);
5363
+ scheduleRemoteHostTargetRenew(REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS, 'error-retry');
5364
+ });
5365
+ }, Math.max(0, Number(delayMs) || 0));
5366
+ remoteHostTargetRenewTimer?.unref?.();
5367
+ }
5368
+
5369
+ function stopRemoteHostTargetRenew(reason = 'stopped') {
5370
+ if (remoteHostTargetRenewTimer) {
5371
+ clearTimeout(remoteHostTargetRenewTimer);
5372
+ remoteHostTargetRenewTimer = null;
5373
+ }
5374
+ updateRemoteHostTargetRenewState({
5375
+ status: REMOTE_HOST_TARGET_AUTO_RENEW_ENABLED ? 'idle' : 'disabled',
5376
+ reason,
5377
+ nodeId: '',
5378
+ leaseId: '',
5379
+ hostInstanceId: '',
5380
+ endpoint: '',
5381
+ endpointCandidates: [],
5382
+ expiresAt: '',
5383
+ lastError: ''
5384
+ });
5385
+ }
5386
+
5387
+ async function runRemoteHostTargetRenewOnce(trigger = 'timer', options = {}) {
5388
+ if (!REMOTE_HOST_TARGET_AUTO_RENEW_ENABLED || remoteHostTargetRenewInFlight) {
5389
+ return serializeRemoteHostTargetRenewState();
5390
+ }
5391
+
5392
+ remoteHostTargetRenewInFlight = true;
5393
+ const attemptedAt = new Date().toISOString();
5394
+ try {
5395
+ const currentHub = remoteHub.getStatus({ includeSecrets: true });
5396
+ if (currentHub?.hostTargetActive !== true || !currentHub.hostTargetNodeId) {
5397
+ stopRemoteHostTargetRenew('no-local-host-target');
5398
+ return serializeRemoteHostTargetRenewState();
5399
+ }
5400
+
5401
+ const renewed = remoteHub.setHostTarget({
5402
+ enabled: true,
5403
+ nodeId: currentHub.hostTargetNodeId,
5404
+ leaseMs: REMOTE_HOST_TARGET_LEASE_MS
5405
+ });
5406
+ if (renewed?.ok !== true || renewed?.active !== true) {
5407
+ updateRemoteHostTargetRenewState({
5408
+ status: 'error',
5409
+ reason: renewed?.error || 'local-host-renew-failed',
5410
+ nodeId: currentHub.hostTargetNodeId,
5411
+ lastAttemptAt: attemptedAt,
5412
+ lastError: renewed?.error || 'local-host-renew-failed'
5413
+ });
5414
+ scheduleRemoteHostTargetRenew(REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS, 'local-renew-failed');
5415
+ return serializeRemoteHostTargetRenewState();
5416
+ }
5417
+
5418
+ const hub = remoteHub.getStatus({ includeSecrets: true });
5419
+ const registry = await publishLocalRemoteHostTargetToRegistry(hub, {
5420
+ takeover: options.takeover === true
5421
+ });
5422
+ const status = registry.ok ? 'active' : registry.stale ? 'superseded' : 'skipped';
5423
+ updateRemoteHostTargetRenewState({
5424
+ status,
5425
+ reason: registry.reason || (registry.ok ? 'ok' : 'registry-skipped'),
5426
+ nodeId: hub.hostTargetNodeId,
5427
+ leaseId: hub.hostTargetLeaseId,
5428
+ hostInstanceId: hub.hostTargetHostInstanceId || hub.hostInstanceId,
5429
+ endpoint: registry.endpoint || hub.hostTargetEndpoint,
5430
+ endpointCandidates: registry.endpointCandidates || hub.hostTargetEndpointCandidates || [],
5431
+ expiresAt: hub.hostTargetExpiresAt,
5432
+ lastAttemptAt: attemptedAt,
5433
+ lastSuccessAt: registry.ok ? new Date().toISOString() : remoteHostTargetRenewState.lastSuccessAt,
5434
+ lastError: registry.ok || /^(registry-not-authenticated|session-expired|supabase-config-missing)$/i.test(registry.reason || '')
5435
+ ? ''
5436
+ : (registry.reason || 'registry-publish-failed')
5437
+ });
5438
+
5439
+ if (registry.stale) {
5440
+ remoteHub.setHostTarget({
5441
+ enabled: false,
5442
+ nodeId: hub.hostTargetNodeId
5443
+ });
5444
+ logRemoteHostTargetRenew('superseded', registry.reason, registry.endpoint);
5445
+ stopRemoteHostTargetRenew('host-target-superseded');
5446
+ wakeRemoteRegistryFollower('host-target-superseded').catch(() => {});
5447
+ return serializeRemoteHostTargetRenewState();
5448
+ }
5449
+
5450
+ logRemoteHostTargetRenew(registry.ok ? 'ok' : 'skipped', registry.reason, registry.endpoint);
5451
+ const retryDelay = registry.ok || isRemoteHostTargetRenewSoftSkipReason(registry.reason)
5452
+ ? REMOTE_HOST_TARGET_RENEW_MS
5453
+ : REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS;
5454
+ scheduleRemoteHostTargetRenew(
5455
+ retryDelay,
5456
+ registry.ok ? 'renewed' : 'renew-skipped');
5457
+ return serializeRemoteHostTargetRenewState();
5458
+ } finally {
5459
+ remoteHostTargetRenewInFlight = false;
5460
+ }
5461
+ }
5462
+
5097
5463
  async function reportRemoteRegistryFollowerSync(payload = {}) {
5098
5464
  const report = createRemoteAgentSyncReport(payload);
5099
5465
  rememberRemoteAgentSyncReport(report);
@@ -9921,6 +10287,7 @@ app.get('/api/status', async (req, res) => {
9921
10287
  remoteAgent: serializeRemoteAgentState(),
9922
10288
  remoteRegistryFollower: serializeRemoteRegistryFollowerState(),
9923
10289
  remoteRegistryRealtime: serializeRemoteRegistryRealtimeState(),
10290
+ remoteHostTargetRenew: serializeRemoteHostTargetRenewState(),
9924
10291
  shellJobsPath: '/api/shell/jobs',
9925
10292
  companyCore: {
9926
10293
  baseUrl: companyCoreBaseUrl,
@@ -9997,20 +10364,27 @@ app.post('/api/remote/frames', (req, res) => {
9997
10364
  app.post('/api/remote/host-target', async (req, res) => {
9998
10365
  res.setHeader('Cache-Control', 'no-store');
9999
10366
  try {
10367
+ const requestedLeaseMs = Number(req.body?.leaseMs);
10000
10368
  const result = remoteHub.setHostTarget({
10001
10369
  enabled: req.body?.enabled !== false,
10002
10370
  nodeId: req.body?.nodeId,
10003
- leaseMs: req.body?.leaseMs
10371
+ leaseMs: Math.max(
10372
+ REMOTE_HOST_TARGET_LEASE_MS,
10373
+ Number.isFinite(requestedLeaseMs) && requestedLeaseMs > 0 ? requestedLeaseMs : 0)
10004
10374
  });
10005
10375
  if (result?.ok === true && result?.active === true && isLocalRemoteHostTargetActive()) {
10006
10376
  if (isRemoteAgentProcessRunning()) {
10007
10377
  await stopRemoteAgentConnection('local-host-target-active');
10008
10378
  logEvent('remote', 'managed RemoteAgent held stopped while local host target is active', 'remote');
10009
10379
  }
10380
+ await runRemoteHostTargetRenewOnce('set-host', { takeover: true });
10381
+ } else if (result?.active !== true) {
10382
+ stopRemoteHostTargetRenew('host-target-inactive');
10010
10383
  }
10011
10384
  res.json({
10012
10385
  ...result,
10013
- agent: serializeRemoteAgentState()
10386
+ agent: serializeRemoteAgentState(),
10387
+ hostTargetRenew: serializeRemoteHostTargetRenewState()
10014
10388
  });
10015
10389
  } catch (err) {
10016
10390
  logError('remote', 'remote host target update failed.', err);
@@ -10018,17 +10392,25 @@ app.post('/api/remote/host-target', async (req, res) => {
10018
10392
  ok: false,
10019
10393
  active: false,
10020
10394
  error: err?.message || String(err),
10021
- agent: serializeRemoteAgentState()
10395
+ agent: serializeRemoteAgentState(),
10396
+ hostTargetRenew: serializeRemoteHostTargetRenewState()
10022
10397
  });
10023
10398
  }
10024
10399
  });
10025
10400
 
10026
10401
  app.delete('/api/remote/host-target', (req, res) => {
10027
10402
  res.setHeader('Cache-Control', 'no-store');
10028
- res.json(remoteHub.setHostTarget({
10403
+ const result = remoteHub.setHostTarget({
10029
10404
  enabled: false,
10030
10405
  nodeId: req.body?.nodeId
10031
- }));
10406
+ });
10407
+ if (result?.ok === true) {
10408
+ stopRemoteHostTargetRenew('host-target-cleared');
10409
+ }
10410
+ res.json({
10411
+ ...result,
10412
+ hostTargetRenew: serializeRemoteHostTargetRenewState()
10413
+ });
10032
10414
  });
10033
10415
 
10034
10416
  app.get('/api/remote/agent/status', (req, res) => {
@@ -11751,6 +12133,12 @@ async function shutdownBridge(signal) {
11751
12133
  // Ignore registry follower shutdown errors
11752
12134
  }
11753
12135
 
12136
+ try {
12137
+ stopRemoteHostTargetRenew('bridge-shutdown');
12138
+ } catch {
12139
+ // Ignore host-target renew shutdown errors
12140
+ }
12141
+
11754
12142
  try {
11755
12143
  closeRemoteRegistryRealtime('bridge-shutdown');
11756
12144
  } catch {