forkit-connect 0.1.34 → 0.1.36

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/dist/launcher.js CHANGED
@@ -58,6 +58,17 @@ function formatApiFailureMessage(fallback, status, body) {
58
58
  return `${fallback} (${explicitCode}).`;
59
59
  return `${fallback} (${status}).`;
60
60
  }
61
+ function formatAgentTypeLabel(value) {
62
+ return String(value || 'agent')
63
+ .split(/[_-]+/)
64
+ .filter(Boolean)
65
+ .map((part) => part.toLowerCase() === 'ai' ? 'AI' : `${part.charAt(0).toUpperCase()}${part.slice(1).toLowerCase()}`)
66
+ .join(' ');
67
+ }
68
+ function formatLocalSourceLabel(value) {
69
+ const source = String(value || 'Local device').trim();
70
+ return source || 'Local device';
71
+ }
61
72
  function resolveLauncherOperatingMode(service) {
62
73
  const entitlements = service.getServiceEntitlements();
63
74
  const normalizedTier = String(entitlements.tier || '').trim().toLowerCase();
@@ -1328,6 +1339,186 @@ function buildHealthPayload(service, host, port) {
1328
1339
  },
1329
1340
  };
1330
1341
  }
1342
+ function renderAgentHandoffHtml(launcherToken) {
1343
+ const safeLauncherToken = JSON.stringify(launcherToken);
1344
+ return `<!doctype html>
1345
+ <html lang="en">
1346
+ <head>
1347
+ <meta charset="utf-8">
1348
+ <meta name="viewport" content="width=device-width,initial-scale=1">
1349
+ <title>Connect your Agent · Forkit Connect</title>
1350
+ <style>
1351
+ :root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
1352
+ * { box-sizing: border-box; }
1353
+ body { margin: 0; min-height: 100vh; color: #f7efe4; background: radial-gradient(circle at 50% 0%, #24213c 0, #10131d 46%, #090d14 100%); }
1354
+ main { width: min(720px, calc(100% - 32px)); margin: 0 auto; padding: 52px 0; }
1355
+ .brand { display: flex; align-items: center; gap: 12px; margin-bottom: 34px; color: #9deef5; font-size: 13px; font-weight: 800; letter-spacing: .15em; text-transform: uppercase; }
1356
+ .brand img { width: 34px; height: 34px; }
1357
+ .panel { border: 1px solid rgba(157,238,245,.22); border-radius: 28px; padding: clamp(24px, 5vw, 42px); background: rgba(20,22,34,.94); box-shadow: 0 30px 80px rgba(0,0,0,.34); }
1358
+ .eyebrow { margin: 0 0 10px; color: #9deef5; font-size: 12px; font-weight: 800; letter-spacing: .16em; text-transform: uppercase; }
1359
+ h1 { margin: 0; font-size: clamp(30px, 6vw, 48px); line-height: 1.06; letter-spacing: -.04em; }
1360
+ .lead { margin: 14px 0 0; color: rgba(247,239,228,.72); font-size: 16px; line-height: 1.55; }
1361
+ .passport { margin: 24px 0; padding: 16px 18px; border-radius: 18px; background: rgba(157,238,245,.08); border: 1px solid rgba(157,238,245,.14); }
1362
+ .passport strong { display: block; font-size: 17px; }
1363
+ .passport span { display: block; margin-top: 5px; color: rgba(247,239,228,.62); font-size: 12px; overflow-wrap: anywhere; }
1364
+ .agents { display: grid; gap: 10px; margin: 18px 0 24px; }
1365
+ .agent { width: 100%; display: flex; align-items: center; gap: 14px; padding: 16px; color: inherit; text-align: left; border: 1px solid rgba(247,239,228,.14); border-radius: 18px; background: rgba(255,255,255,.035); cursor: pointer; }
1366
+ .agent:hover, .agent.selected { border-color: #9deef5; background: rgba(157,238,245,.1); }
1367
+ .agent-mark { width: 38px; height: 38px; display: grid; place-items: center; flex: 0 0 auto; border-radius: 12px; color: #121522; background: #9deef5; font-weight: 900; }
1368
+ .agent-copy { min-width: 0; flex: 1; }
1369
+ .agent-name { display: block; font-size: 16px; font-weight: 750; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1370
+ .agent-meta { display: block; margin-top: 4px; color: rgba(247,239,228,.62); font-size: 13px; }
1371
+ .check { opacity: 0; color: #9deef5; font-weight: 900; }
1372
+ .agent.selected .check { opacity: 1; }
1373
+ .actions { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
1374
+ button, .button { min-height: 48px; padding: 0 20px; border-radius: 15px; border: 1px solid rgba(247,239,228,.16); font: inherit; font-weight: 750; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; }
1375
+ .primary { border-color: transparent; color: #19142b; background: linear-gradient(135deg, #9deef5, #f49355); }
1376
+ .secondary { color: #f7efe4; background: transparent; }
1377
+ button:disabled { opacity: .45; cursor: not-allowed; }
1378
+ .status { margin: 18px 0 0; min-height: 24px; color: rgba(247,239,228,.7); font-size: 14px; }
1379
+ .status.error { color: #ffb7c2; }
1380
+ .privacy { margin: 22px 0 0; color: rgba(247,239,228,.52); font-size: 12px; line-height: 1.5; }
1381
+ [hidden] { display: none !important; }
1382
+ @media (max-width: 560px) { main { padding: 24px 0; } .panel { border-radius: 22px; } .actions > * { width: 100%; } }
1383
+ </style>
1384
+ </head>
1385
+ <body>
1386
+ <main>
1387
+ <div class="brand"><img src="/assets/logo.svg" alt=""><span>Forkit Connect</span></div>
1388
+ <section class="panel">
1389
+ <p class="eyebrow">Agent connection</p>
1390
+ <h1 id="title">Connect your Agent</h1>
1391
+ <p class="lead" id="lead">Checking this connection request…</p>
1392
+ <div class="passport" id="passport" hidden><strong id="passport-name"></strong><span id="passport-id"></span></div>
1393
+ <div id="agent-section" hidden>
1394
+ <p class="eyebrow">Detected on this device</p>
1395
+ <div class="agents" id="agents"></div>
1396
+ </div>
1397
+ <div class="actions">
1398
+ <button class="primary" id="approve" hidden disabled>Approve &amp; connect</button>
1399
+ <button class="primary" id="sign-in" hidden>Sign in to Forkit</button>
1400
+ <a class="button primary" id="return" hidden>Return to Forkit</a>
1401
+ <button class="secondary" id="decline">Not now</button>
1402
+ </div>
1403
+ <p class="status" id="status" role="status" aria-live="polite"></p>
1404
+ <p class="privacy">Forkit records connection and operational metadata only. It does not collect prompts, responses, file contents, browser content, emails or credentials.</p>
1405
+ </section>
1406
+ </main>
1407
+ <script>
1408
+ const launcherToken = ${safeLauncherToken};
1409
+ const query = new URLSearchParams(window.location.search);
1410
+ const fragment = new URLSearchParams(window.location.hash.slice(1));
1411
+ const handoffToken = fragment.get('handoff') || '';
1412
+ const expectedPassport = query.get('passport') || '';
1413
+ window.history.replaceState(null, '', window.location.pathname + window.location.search);
1414
+ let selectedAgent = '';
1415
+ let returnUrl = 'https://www.forkit.dev/register-passport?connect=' + encodeURIComponent(expectedPassport);
1416
+ try {
1417
+ const candidate = new URL(query.get('returnTo') || '');
1418
+ const local = candidate.protocol === 'http:' && (candidate.hostname === '127.0.0.1' || candidate.hostname === 'localhost');
1419
+ if (candidate.origin === 'https://www.forkit.dev' || local) returnUrl = candidate.href;
1420
+ } catch {}
1421
+
1422
+ const byId = (id) => document.getElementById(id);
1423
+ const escapeHtml = (value) => String(value || '').replace(/[&<>"']/g, (char) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[char]));
1424
+ async function api(path, options = {}) {
1425
+ const response = await fetch(path, {
1426
+ ...options,
1427
+ headers: { 'content-type': 'application/json', 'x-forkit-launcher-token': launcherToken, ...(options.headers || {}) },
1428
+ });
1429
+ const body = await response.json().catch(() => ({}));
1430
+ return { response, body };
1431
+ }
1432
+ function setStatus(message, error = false) {
1433
+ byId('status').textContent = message || '';
1434
+ byId('status').classList.toggle('error', error);
1435
+ }
1436
+ function showSignIn() {
1437
+ byId('lead').textContent = 'Sign in once to verify that this Passport belongs to you.';
1438
+ byId('sign-in').hidden = false;
1439
+ setStatus('');
1440
+ }
1441
+ function renderPreview(body) {
1442
+ const passport = body.handoff.passport;
1443
+ if (expectedPassport && passport.gaid !== expectedPassport) throw new Error('The Passport request does not match.');
1444
+ byId('lead').textContent = 'Choose the matching Agent, then approve once.';
1445
+ byId('passport-name').textContent = passport.name;
1446
+ byId('passport-id').textContent = passport.gaid;
1447
+ byId('passport').hidden = false;
1448
+ byId('agent-section').hidden = false;
1449
+ byId('approve').hidden = false;
1450
+ const agents = Array.isArray(body.agents) ? body.agents : [];
1451
+ byId('agents').innerHTML = agents.map((agent) => '<button class="agent" type="button" data-agent="' + escapeHtml(agent.id) + '"><span class="agent-mark">A</span><span class="agent-copy"><span class="agent-name">' + escapeHtml(agent.name) + '</span><span class="agent-meta">' + escapeHtml(agent.typeLabel) + ' · ' + escapeHtml(agent.source) + '</span></span><span class="check">✓</span></button>').join('');
1452
+ if (agents.length === 1) selectedAgent = agents[0].id;
1453
+ for (const card of byId('agents').querySelectorAll('.agent')) {
1454
+ if (card.dataset.agent === selectedAgent) card.classList.add('selected');
1455
+ card.addEventListener('click', () => {
1456
+ selectedAgent = card.dataset.agent || '';
1457
+ for (const other of byId('agents').querySelectorAll('.agent')) other.classList.toggle('selected', other === card);
1458
+ byId('approve').disabled = !selectedAgent;
1459
+ });
1460
+ }
1461
+ byId('approve').disabled = !selectedAgent;
1462
+ setStatus(agents.length ? '' : 'No supported Agent is active on this device.', !agents.length);
1463
+ }
1464
+ async function load() {
1465
+ if (query.get('action') !== 'connect-agent' || !handoffToken) {
1466
+ byId('lead').textContent = 'Open this page from your Agent Passport on Forkit.dev.';
1467
+ setStatus('The connection request is missing or invalid.', true);
1468
+ return;
1469
+ }
1470
+ const { response, body } = await api('/api/agent-handoff/preview', { method: 'POST', body: JSON.stringify({ handoffToken }) });
1471
+ if (response.status === 401 && body.requiresSignIn) { showSignIn(); return; }
1472
+ if (!response.ok || !body.ok) throw new Error(body.message || 'The connection request could not be verified.');
1473
+ renderPreview(body);
1474
+ }
1475
+ byId('approve').addEventListener('click', async () => {
1476
+ if (!selectedAgent) return;
1477
+ byId('approve').disabled = true;
1478
+ setStatus('Connecting securely…');
1479
+ try {
1480
+ const { response, body } = await api('/api/agent-handoff/approve', { method: 'POST', body: JSON.stringify({ handoffToken, agentSelector: selectedAgent }) });
1481
+ if (!response.ok || !body.ok) throw new Error(body.message || 'Connection failed.');
1482
+ byId('title').textContent = 'Connected';
1483
+ byId('lead').textContent = body.activityConfirmed ? 'Forkit received the connection event.' : 'The connection is saved and will sync when the service is available.';
1484
+ byId('agent-section').hidden = true;
1485
+ byId('approve').hidden = true;
1486
+ byId('decline').hidden = true;
1487
+ byId('return').href = returnUrl;
1488
+ byId('return').hidden = false;
1489
+ setStatus(body.agentName + ' is linked to ' + body.passportName + '.');
1490
+ } catch (error) {
1491
+ byId('approve').disabled = false;
1492
+ setStatus(error.message || 'Connection failed.', true);
1493
+ }
1494
+ });
1495
+ byId('sign-in').addEventListener('click', async () => {
1496
+ byId('sign-in').disabled = true;
1497
+ setStatus('Preparing sign-in…');
1498
+ try {
1499
+ const { response, body } = await api('/api/login/start', { method: 'POST', body: '{}' });
1500
+ if (!response.ok || !body.ok) throw new Error(body.message || 'Sign-in could not start.');
1501
+ window.open(body.verificationUrl, '_blank', 'noopener,noreferrer');
1502
+ const poll = window.setInterval(async () => {
1503
+ const result = await api('/api/login/poll?deviceCode=' + encodeURIComponent(body.deviceCode));
1504
+ if (result.body && result.body.approved) {
1505
+ window.clearInterval(poll);
1506
+ byId('sign-in').hidden = true;
1507
+ setStatus('Signed in. Verifying the request…');
1508
+ load().catch((error) => setStatus(error.message, true));
1509
+ }
1510
+ }, Math.max(2000, Number(body.pollIntervalSeconds || 3) * 1000));
1511
+ } catch (error) {
1512
+ byId('sign-in').disabled = false;
1513
+ setStatus(error.message || 'Sign-in could not start.', true);
1514
+ }
1515
+ });
1516
+ byId('decline').addEventListener('click', () => { window.location.href = returnUrl; });
1517
+ load().catch((error) => { byId('lead').textContent = 'Return to Forkit.dev and try again.'; setStatus(error.message || 'Connection failed.', true); });
1518
+ </script>
1519
+ </body>
1520
+ </html>`;
1521
+ }
1331
1522
  function renderLauncherHtml(launcherToken) {
1332
1523
  return `<!doctype html>
1333
1524
  <html lang="en">
@@ -11252,8 +11443,11 @@ function createLauncherApp(options) {
11252
11443
  app.get('/assets/logo-2line.svg', (_request, response) => {
11253
11444
  response.type('image/svg+xml').sendFile(getLogoTwoLineSourcePath());
11254
11445
  });
11255
- app.get('/', (_request, response) => {
11256
- response.type('html').send(renderLauncherHtml(launcherToken));
11446
+ app.get('/', (request, response) => {
11447
+ const isAgentHandoff = request.query.action === 'connect-agent';
11448
+ response.type('html').send(isAgentHandoff
11449
+ ? renderAgentHandoffHtml(launcherToken)
11450
+ : renderLauncherHtml(launcherToken));
11257
11451
  });
11258
11452
  app.use('/api', createApiRequestGuard(getBoundPort, launcherToken));
11259
11453
  app.get('/api/health', (_request, response) => {
@@ -11497,6 +11691,77 @@ function createLauncherApp(options) {
11497
11691
  sendInternalError(response, error, 'launcher_discovery_failed');
11498
11692
  }
11499
11693
  });
11694
+ app.post('/api/agent-handoff/preview', async (request, response) => {
11695
+ try {
11696
+ const handoffToken = typeof request.body?.handoffToken === 'string' ? request.body.handoffToken.trim() : '';
11697
+ if (!handoffToken) {
11698
+ response.status(400).json({ ok: false, message: 'The connection request is missing.' });
11699
+ return;
11700
+ }
11701
+ const handoff = await options.service.resolveWebsiteAgentHandoff(handoffToken);
11702
+ const scan = await options.service.scanAgents({ includeConnectSelf: false });
11703
+ const agents = scan.agents
11704
+ .filter((agent) => agent.status !== 'inactive' && agent.metadata?.signature !== 'forkit-connect-desktop')
11705
+ .map((agent) => ({
11706
+ id: agent.agent_id,
11707
+ name: agent.agent_name,
11708
+ typeLabel: formatAgentTypeLabel(agent.agent_type),
11709
+ source: formatLocalSourceLabel(agent.process_name),
11710
+ status: agent.status,
11711
+ }));
11712
+ response.json({ ok: true, handoff, agents });
11713
+ }
11714
+ catch (error) {
11715
+ const code = error instanceof Error ? error.message : 'AGENT_HANDOFF_FAILED';
11716
+ const requiresSignIn = code === 'AGENT_HANDOFF_SIGN_IN_REQUIRED';
11717
+ const status = requiresSignIn ? 401 : code === 'AGENT_HANDOFF_ACCOUNT_MISMATCH' ? 403 : 400;
11718
+ response.status(status).json({
11719
+ ok: false,
11720
+ requiresSignIn,
11721
+ message: requiresSignIn
11722
+ ? 'Sign in to verify this Agent Passport.'
11723
+ : 'This Agent connection request is invalid or has expired.',
11724
+ });
11725
+ }
11726
+ });
11727
+ app.post('/api/agent-handoff/approve', async (request, response) => {
11728
+ try {
11729
+ const handoffToken = typeof request.body?.handoffToken === 'string' ? request.body.handoffToken.trim() : '';
11730
+ const agentSelector = typeof request.body?.agentSelector === 'string' ? request.body.agentSelector.trim() : '';
11731
+ if (!handoffToken || !agentSelector) {
11732
+ response.status(400).json({ ok: false, message: 'Choose an Agent to continue.' });
11733
+ return;
11734
+ }
11735
+ const result = await options.service.approveWebsiteAgentHandoff({ handoffToken, agentSelector });
11736
+ response.json({
11737
+ ok: true,
11738
+ action: result.action,
11739
+ agentName: result.agent.agent_name,
11740
+ passportName: result.passport.name,
11741
+ passportGaid: result.passport.gaid,
11742
+ activityConfirmed: result.activityConfirmed,
11743
+ syncPending: result.syncPending,
11744
+ });
11745
+ }
11746
+ catch (error) {
11747
+ const code = error instanceof Error ? error.message : 'AGENT_HANDOFF_FAILED';
11748
+ const status = code === 'AGENT_HANDOFF_SIGN_IN_REQUIRED'
11749
+ ? 401
11750
+ : code === 'AGENT_ALREADY_CONNECTED_TO_ANOTHER_PASSPORT'
11751
+ ? 409
11752
+ : code === 'SECURE_STORAGE_REQUIRED' || code === 'SECURE_STORAGE_WRITE_FAILED'
11753
+ ? 503
11754
+ : 400;
11755
+ const message = code === 'AGENT_HANDOFF_SIGN_IN_REQUIRED'
11756
+ ? 'Sign in to verify this Agent Passport.'
11757
+ : code === 'AGENT_ALREADY_CONNECTED_TO_ANOTHER_PASSPORT'
11758
+ ? 'This Agent is already connected to another Passport.'
11759
+ : code === 'SECURE_STORAGE_REQUIRED' || code === 'SECURE_STORAGE_WRITE_FAILED'
11760
+ ? 'Secure credential storage is unavailable on this device.'
11761
+ : 'The Agent could not be connected. Return to Forkit.dev and try again.';
11762
+ response.status(status).json({ ok: false, message });
11763
+ }
11764
+ });
11500
11765
  app.post('/api/discovery/connect-model', async (request, response) => {
11501
11766
  try {
11502
11767
  const selector = typeof request.body?.selector === 'string' ? request.body.selector.trim() : '';
package/dist/v1/api.d.ts CHANGED
@@ -156,6 +156,17 @@ export interface ApiKeyCreateResponse {
156
156
  rawKey?: string | null;
157
157
  _warning?: string | null;
158
158
  }
159
+ export interface AgentHandoffResolveResponse {
160
+ protocol?: 'agent-handoff.v1' | string;
161
+ expiresAt?: string | null;
162
+ passport?: {
163
+ gaid?: string | null;
164
+ name?: string | null;
165
+ passportType?: string | null;
166
+ workspaceId?: string | null;
167
+ projectId?: string | null;
168
+ } | null;
169
+ }
159
170
  export interface WorkspaceProject {
160
171
  id?: string | null;
161
172
  workspaceId?: string | null;
@@ -377,6 +388,8 @@ export declare class ConnectApiClient {
377
388
  * owned passport. Returns the rawKey exactly once — caller must persist it.
378
389
  */
379
390
  createPassportApiKey(gaid: string, label?: string): Promise<ApiCallResult>;
391
+ revokePassportApiKey(gaid: string, keyId: string): Promise<ApiCallResult>;
392
+ resolveAgentHandoff(handoffToken: string): Promise<ApiCallResult>;
380
393
  createRuntimeApiKey(gaid: string, label?: string): Promise<ApiCallResult>;
381
394
  private parseBody;
382
395
  }
package/dist/v1/api.js CHANGED
@@ -550,6 +550,33 @@ class ConnectApiClient {
550
550
  contract: this.parseContract(res),
551
551
  };
552
552
  }
553
+ async revokePassportApiKey(gaid, keyId) {
554
+ const url = `${this.config.baseUrl}/api/v1/passports/${encodeURIComponent(gaid)}/api-keys/${encodeURIComponent(keyId)}`;
555
+ const res = await fetch(url, {
556
+ method: 'DELETE',
557
+ headers: this.getHeaders(),
558
+ });
559
+ return {
560
+ ok: res.ok,
561
+ status: res.status,
562
+ body: await this.parseBody(res),
563
+ contract: this.parseContract(res),
564
+ };
565
+ }
566
+ async resolveAgentHandoff(handoffToken) {
567
+ const url = `${this.config.baseUrl}/api/v1/connect/agent-handoffs/resolve`;
568
+ const res = await fetch(url, {
569
+ method: 'POST',
570
+ headers: this.getHeaders(),
571
+ body: JSON.stringify({ handoffToken }),
572
+ });
573
+ return {
574
+ ok: res.ok,
575
+ status: res.status,
576
+ body: await this.parseBody(res),
577
+ contract: this.parseContract(res),
578
+ };
579
+ }
553
580
  async createRuntimeApiKey(gaid, label) {
554
581
  const url = `${this.config.baseUrl}/api/v1/runtimes/${encodeURIComponent(gaid)}/api-keys`;
555
582
  const res = await fetch(url, {
@@ -255,6 +255,9 @@ class FilesystemModelProvider {
255
255
  }
256
256
  exports.FilesystemModelProvider = FilesystemModelProvider;
257
257
  function createDefaultFilesystemModelProviders() {
258
+ if (process.env.FORKIT_CONNECT_DISABLE_FILESYSTEM_MODEL_SCAN === '1') {
259
+ return [];
260
+ }
258
261
  const roots = getDefaultFilesystemModelDirs();
259
262
  return roots.length > 0 ? [new FilesystemModelProvider(roots)] : [];
260
263
  }
@@ -185,6 +185,24 @@ export interface AgentStatusResult {
185
185
  lastError: string | null;
186
186
  } | null;
187
187
  }
188
+ export interface WebsiteAgentHandoff {
189
+ protocol: 'agent-handoff.v1';
190
+ expiresAt: string;
191
+ passport: {
192
+ gaid: string;
193
+ name: string;
194
+ passportType: 'agent';
195
+ workspaceId: string | null;
196
+ projectId: string | null;
197
+ };
198
+ }
199
+ export interface WebsiteAgentHandoffApprovalResult {
200
+ action: 'connected' | 'already_connected';
201
+ agent: DetectedAgent;
202
+ passport: WebsiteAgentHandoff['passport'];
203
+ activityConfirmed: boolean;
204
+ syncPending: boolean;
205
+ }
188
206
  export interface TrayStatusResult {
189
207
  daemonRunning: boolean;
190
208
  detectedModels: number;
@@ -427,6 +445,8 @@ export declare class ConnectV1Service {
427
445
  private anchorBuildSessionToBinding;
428
446
  private resolveRuntimeRunScope;
429
447
  private getPrimaryBoundBinding;
448
+ private getOperationalPassportContext;
449
+ private recordAgentConnectionFault;
430
450
  private setRuntimeSignalApiKeyForGaid;
431
451
  private clearRuntimeSignalApiKeyForGaid;
432
452
  private resolveRuntimeSignalApiKeyForEvent;
@@ -512,6 +532,7 @@ export declare class ConnectV1Service {
512
532
  };
513
533
  private buildC2LifecycleEvent;
514
534
  private recordC2LifecycleEvents;
535
+ private recordC2ConnectivityUnavailable;
515
536
  private getC2EventSyncSummary;
516
537
  recordC2LifecycleEvent(input: {
517
538
  eventType: C2LiteEventType;
@@ -597,6 +618,12 @@ export declare class ConnectV1Service {
597
618
  }): Promise<AgentScanResult>;
598
619
  buildAgentReviewSnapshot(): AgentReviewSnapshot;
599
620
  connectAgent(selector: string): DetectedAgent;
621
+ resolveWebsiteAgentHandoff(handoffToken: string): Promise<WebsiteAgentHandoff>;
622
+ private syncWebsiteAgentConnectionEvent;
623
+ approveWebsiteAgentHandoff(input: {
624
+ handoffToken: string;
625
+ agentSelector: string;
626
+ }): Promise<WebsiteAgentHandoffApprovalResult>;
600
627
  registerObservedAgent(input: {
601
628
  agentName: string;
602
629
  agentType: AgentType;
@@ -720,7 +747,12 @@ export declare class ConnectV1Service {
720
747
  private isProcessAlive;
721
748
  getDaemonStatus(): DaemonStatus;
722
749
  markDaemonStarted(pid: number): DaemonStatus;
723
- markDaemonStopped(lastError: string | null): DaemonStatus;
750
+ markDaemonStopped(lastError: string | null, options?: {
751
+ unclean?: boolean;
752
+ detectedAt?: string;
753
+ previousStartedAt?: string | null;
754
+ previousLastScanAt?: string | null;
755
+ }): DaemonStatus;
724
756
  recordDaemonScan(summary: DaemonScanSummary): DaemonStatus;
725
757
  recordDaemonError(lastError: string): DaemonStatus;
726
758
  getPulseSummary(): VitalityPulseSummary | null;
@@ -26,6 +26,15 @@ const DEFAULT_BASE_URL = process.env.FORKIT_API_URL ?? process.env.FORKIT_BASE_U
26
26
  const AGENT_INACTIVE_GRACE_MS = 5 * 60 * 1000;
27
27
  const CONNECT_PRIVACY_SUMMARY = 'Forkit Connect uses metadata-only local status by default. Project-scoped vibe-code observation is explicit opt-in, records only tool metadata, and does not store prompts, outputs, logs, file contents, or traffic.';
28
28
  const MAX_RUNTIME_SIGNAL_QUEUE_ITEMS = 200;
29
+ const AGENT_ACTIVITY_ADAPTER_BY_SIGNATURE = Object.freeze({
30
+ antigravity: 'antigravity_v1',
31
+ claude: 'claude_v1',
32
+ 'claude-code': 'claude_v1',
33
+ codex: 'codex_v1',
34
+ 'codex-cli': 'codex_v1',
35
+ cursor: 'cursor_v1',
36
+ 'vscode-agent': 'vscode_v1',
37
+ });
29
38
  /** Max events flushed per daemon cycle — prevents thundering-herd on large queues. */
30
39
  const C2_SYNC_BATCH_SIZE = 50;
31
40
  /** Cooldown seconds after a rate-limit (429) or unavailability (503) response. */
@@ -52,9 +61,54 @@ const SMART_INBOX_CONFIDENCE_PRIORITY = {
52
61
  low: 2,
53
62
  };
54
63
  const SMART_INBOX_FRESH_MAX_AGE_MS = 45 * 1000;
64
+ const C2_RUNTIME_SIGNAL_SENSITIVE_METADATA_PATTERNS = [
65
+ /prompt(?!_?tokens?$)/i,
66
+ /message/i,
67
+ /conversation/i,
68
+ /transcript/i,
69
+ /chat/i,
70
+ /log(s|_?text|_?content)?$/i,
71
+ /secret/i,
72
+ /password/i,
73
+ /api[_-]?key/i,
74
+ /token(?!s?($|_?(count|in|out|total|usage)$)|Usage$)/i,
75
+ /authorization/i,
76
+ /cookie/i,
77
+ /private[_-]?key/i,
78
+ /credential/i,
79
+ /email/i,
80
+ /phone/i,
81
+ /ssn/i,
82
+ /address/i,
83
+ /file[_-]?(content|body|text|data)/i,
84
+ /model[_-]?(weight|weights|binary|blob)/i,
85
+ /dataset[_-]?(row|rows|sample|content|record)/i,
86
+ ];
55
87
  function isRecord(value) {
56
88
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
57
89
  }
90
+ function isSensitiveC2RuntimeSignalMetadataKey(key) {
91
+ return C2_RUNTIME_SIGNAL_SENSITIVE_METADATA_PATTERNS.some((pattern) => pattern.test(key));
92
+ }
93
+ function sanitizeC2RuntimeSignalMetadataValue(value) {
94
+ if (Array.isArray(value)) {
95
+ return value.map((item) => sanitizeC2RuntimeSignalMetadataValue(item));
96
+ }
97
+ if (!isRecord(value)) {
98
+ return value;
99
+ }
100
+ return sanitizeC2RuntimeSignalMetadata(value);
101
+ }
102
+ function sanitizeC2RuntimeSignalMetadata(metadata) {
103
+ const safeMetadata = {};
104
+ for (const [key, value] of Object.entries(metadata)) {
105
+ if (isSensitiveC2RuntimeSignalMetadataKey(key)) {
106
+ continue;
107
+ }
108
+ safeMetadata[key] = sanitizeC2RuntimeSignalMetadataValue(value);
109
+ }
110
+ return safeMetadata;
111
+ }
58
112
  function extractApiErrorCode(body) {
59
113
  if (!isRecord(body))
60
114
  return null;
@@ -2854,6 +2908,51 @@ class ConnectV1Service {
2854
2908
  })
2855
2909
  .at(0);
2856
2910
  }
2911
+ getOperationalPassportContext(state) {
2912
+ const normalize = (value) => typeof value === 'string' && value.trim() ? value.trim() : null;
2913
+ const agent = [...state.detected_agents]
2914
+ .filter((item) => normalize(item.metadata?.passport_gaid))
2915
+ .sort((left, right) => {
2916
+ const connectedDelta = Number(right.status === 'connected') - Number(left.status === 'connected');
2917
+ if (connectedDelta !== 0)
2918
+ return connectedDelta;
2919
+ return String(right.last_seen_at || '').localeCompare(String(left.last_seen_at || ''));
2920
+ })
2921
+ .at(0);
2922
+ if (agent) {
2923
+ return {
2924
+ passportGaid: normalize(agent.metadata?.passport_gaid),
2925
+ workspaceId: normalize(agent.metadata?.workspace_id),
2926
+ projectId: normalize(agent.metadata?.project_id),
2927
+ };
2928
+ }
2929
+ const binding = this.getPrimaryBoundBinding(state);
2930
+ return {
2931
+ passportGaid: normalize(binding?.gaid),
2932
+ workspaceId: normalize(binding?.workspaceId) ?? normalize(state.workspace_binding.workspaceId),
2933
+ projectId: normalize(binding?.projectId) ?? normalize(state.project_binding.projectId),
2934
+ };
2935
+ }
2936
+ recordAgentConnectionFault(input) {
2937
+ const alreadyRecorded = this.stateStore.readState().c2_events.some((event) => (event.event_type === input.eventType
2938
+ && event.passport_gaid === input.passportGaid
2939
+ && event.metadata?.agent_id === input.agent.agent_id
2940
+ && event.metadata?.failure_stage === input.metadata.failure_stage));
2941
+ if (alreadyRecorded)
2942
+ return;
2943
+ this.recordC2LifecycleEvent({
2944
+ eventType: input.eventType,
2945
+ passportGaid: input.passportGaid,
2946
+ workspaceId: input.workspaceId,
2947
+ projectId: input.projectId,
2948
+ metadata: {
2949
+ agent_id: input.agent.agent_id,
2950
+ agent_name: input.agent.agent_name,
2951
+ agent_type: input.agent.agent_type,
2952
+ ...input.metadata,
2953
+ },
2954
+ });
2955
+ }
2857
2956
  setRuntimeSignalApiKeyForGaid(gaid, apiKey) {
2858
2957
  if (!gaid || !apiKey)
2859
2958
  return null;
@@ -3191,6 +3290,27 @@ class ConnectV1Service {
3191
3290
  });
3192
3291
  }
3193
3292
  }
3293
+ recordC2ConnectivityUnavailable(event, reasonCode) {
3294
+ const state = this.stateStore.readState();
3295
+ if (state.c2_offline_since)
3296
+ return;
3297
+ const offlineSince = nowIso();
3298
+ this.stateStore.updateC2ConnectivityStatus(offlineSince, state.last_c2_restored_at);
3299
+ this.recordC2LifecycleEvent({
3300
+ eventType: 'connect_sync_offline',
3301
+ occurredAt: offlineSince,
3302
+ passportGaid: event.passport_gaid,
3303
+ workspaceId: event.workspace_id,
3304
+ projectId: event.project_id,
3305
+ metadata: {
3306
+ period_kind: 'connectivity_unavailable',
3307
+ offline_since: offlineSince,
3308
+ detected_at: offlineSince,
3309
+ reason_code: reasonCode,
3310
+ evidence: 'observed_sync_failure',
3311
+ },
3312
+ });
3313
+ }
3194
3314
  getC2EventSyncSummary(state) {
3195
3315
  return state.c2_events.reduce((summary, event) => {
3196
3316
  if (event.sync_state === 'synced') {
@@ -3622,10 +3742,11 @@ class ConnectV1Service {
3622
3742
  process_signature_hash: processSignatureHash,
3623
3743
  detected_at: input.existing?.detected_at ?? input.detectedAt,
3624
3744
  last_seen_at: input.detectedAt,
3625
- status: isConnectSelf || input.existing?.status === 'connected' ? 'connected' : 'detected',
3745
+ status: isConnectSelf || existingPermanent ? 'connected' : 'detected',
3626
3746
  persistence,
3627
3747
  discovery_sources: [...new Set([...(input.existing?.discovery_sources ?? []), input.discoverySource])],
3628
3748
  metadata: {
3749
+ ...(input.existing?.metadata ?? {}),
3629
3750
  executable_name: input.candidate.executable_name,
3630
3751
  signature: input.candidate.signature,
3631
3752
  persistence,
@@ -3642,12 +3763,29 @@ class ConnectV1Service {
3642
3763
  };
3643
3764
  }
3644
3765
  queueAgentC2Event(agent, eventType, metadata = {}, occurredAt) {
3766
+ const passportGaid = typeof metadata.passport_gaid === 'string' && metadata.passport_gaid.trim()
3767
+ ? metadata.passport_gaid.trim()
3768
+ : typeof agent.metadata?.passport_gaid === 'string' && agent.metadata.passport_gaid.trim()
3769
+ ? agent.metadata.passport_gaid.trim()
3770
+ : null;
3771
+ const state = this.stateStore.readState();
3772
+ const workspaceId = typeof metadata.workspace_id === 'string' && metadata.workspace_id.trim()
3773
+ ? metadata.workspace_id.trim()
3774
+ : typeof agent.metadata?.workspace_id === 'string' && agent.metadata.workspace_id.trim()
3775
+ ? agent.metadata.workspace_id.trim()
3776
+ : state.workspace_binding.workspaceId;
3777
+ const projectId = typeof metadata.project_id === 'string' && metadata.project_id.trim()
3778
+ ? metadata.project_id.trim()
3779
+ : typeof agent.metadata?.project_id === 'string' && agent.metadata.project_id.trim()
3780
+ ? agent.metadata.project_id.trim()
3781
+ : state.project_binding.projectId;
3645
3782
  const eventInput = {
3646
3783
  eventType,
3784
+ passportGaid,
3647
3785
  runtimeGaid: typeof metadata.runtime_gaid === 'string' ? metadata.runtime_gaid : null,
3648
3786
  modelName: null,
3649
- workspaceId: this.stateStore.readState().workspace_binding.workspaceId,
3650
- projectId: this.stateStore.readState().project_binding.projectId,
3787
+ workspaceId,
3788
+ projectId,
3651
3789
  metadata: {
3652
3790
  agent_id: agent.agent_id,
3653
3791
  agent_name: agent.agent_name,
@@ -3706,6 +3844,19 @@ class ConnectV1Service {
3706
3844
  byId.set(nextAgent.agent_id, nextAgent);
3707
3845
  if (existing) {
3708
3846
  knownAgents += 1;
3847
+ if (existing.status === 'inactive' && nextAgent.status === 'connected') {
3848
+ this.stateStore.addEvidenceEvent({
3849
+ type: 'agent_connected',
3850
+ details: {
3851
+ agent_id: nextAgent.agent_id,
3852
+ agent_name: nextAgent.agent_name,
3853
+ source: 'process_reappeared',
3854
+ },
3855
+ });
3856
+ this.queueAgentC2Event(nextAgent, 'connect_agent_started', {
3857
+ observation: 'process_reappeared',
3858
+ }, detectedAt);
3859
+ }
3709
3860
  }
3710
3861
  else {
3711
3862
  newAgents += 1;
@@ -3838,6 +3989,234 @@ class ConnectV1Service {
3838
3989
  this.queueAgentC2Event(nextAgent, 'connect_agent_connected', {}, updatedAt);
3839
3990
  return nextAgent;
3840
3991
  }
3992
+ async resolveWebsiteAgentHandoff(handoffToken) {
3993
+ const token = String(handoffToken || '').trim();
3994
+ if (!token)
3995
+ throw new Error('AGENT_HANDOFF_INVALID');
3996
+ if (!this.hasSessionRef())
3997
+ throw new Error('AGENT_HANDOFF_SIGN_IN_REQUIRED');
3998
+ const result = await this.getApiClient(this.stateStore.readState()).resolveAgentHandoff(token);
3999
+ if (!result.ok || !isRecord(result.body)) {
4000
+ const code = this.extractBackendErrorCode(result.body);
4001
+ if (result.status === 401)
4002
+ throw new Error(code || 'AGENT_HANDOFF_SIGN_IN_REQUIRED');
4003
+ throw new Error(code || `AGENT_HANDOFF_RESOLVE_FAILED:${result.status}`);
4004
+ }
4005
+ const passport = isRecord(result.body.passport) ? result.body.passport : null;
4006
+ const protocol = normalizeDisplayText(result.body.protocol);
4007
+ const expiresAt = normalizeDisplayText(result.body.expiresAt);
4008
+ const gaid = normalizeDisplayText(passport?.gaid);
4009
+ const name = normalizeDisplayText(passport?.name);
4010
+ const passportType = normalizeDisplayText(passport?.passportType)?.toLowerCase();
4011
+ if (protocol !== 'agent-handoff.v1' || !expiresAt || !gaid || !name || passportType !== 'agent') {
4012
+ throw new Error('AGENT_HANDOFF_RESPONSE_INVALID');
4013
+ }
4014
+ return {
4015
+ protocol: 'agent-handoff.v1',
4016
+ expiresAt,
4017
+ passport: {
4018
+ gaid,
4019
+ name,
4020
+ passportType: 'agent',
4021
+ workspaceId: normalizeDisplayText(passport?.workspaceId),
4022
+ projectId: normalizeDisplayText(passport?.projectId),
4023
+ },
4024
+ };
4025
+ }
4026
+ async syncWebsiteAgentConnectionEvent(gaid, apiKey) {
4027
+ const state = this.stateStore.readState();
4028
+ const event = [...state.c2_events]
4029
+ .reverse()
4030
+ .find((item) => item.passport_gaid === gaid && item.event_type === 'connect_agent_connection_verified');
4031
+ if (!event)
4032
+ return { confirmed: false, pending: false };
4033
+ if (event.sync_state === 'synced')
4034
+ return { confirmed: true, pending: false };
4035
+ try {
4036
+ const result = await this.getApiClientWithSessionToken(apiKey).pushRuntimeSignalEvent(this.buildC2RuntimeSignalPayload(event, apiKey, null));
4037
+ if (!result.ok) {
4038
+ const error = `c2_sync_${result.status}`;
4039
+ if (result.status === 429 || (result.status >= 500 && result.status <= 599)) {
4040
+ this.stateStore.markC2EventRetryable(event.event_id, error);
4041
+ if (result.status >= 500) {
4042
+ this.recordC2ConnectivityUnavailable(event, error);
4043
+ }
4044
+ }
4045
+ else {
4046
+ this.stateStore.markC2EventFailed(event.event_id, error);
4047
+ }
4048
+ return { confirmed: false, pending: true };
4049
+ }
4050
+ this.stateStore.markC2EventSynced(event.event_id);
4051
+ this.stateStore.addEvidenceEvent({
4052
+ type: 'runtimeSignal_synced',
4053
+ details: {
4054
+ eventId: event.event_id,
4055
+ eventType: event.event_type,
4056
+ source: 'website_agent_handoff',
4057
+ status: result.status,
4058
+ gaid,
4059
+ },
4060
+ });
4061
+ return { confirmed: true, pending: false };
4062
+ }
4063
+ catch (error) {
4064
+ this.stateStore.markC2EventRetryable(event.event_id, error instanceof Error ? error.message : 'c2_sync_failed');
4065
+ this.recordC2ConnectivityUnavailable(event, 'c2_network_unavailable');
4066
+ return { confirmed: false, pending: true };
4067
+ }
4068
+ }
4069
+ async approveWebsiteAgentHandoff(input) {
4070
+ const handoff = await this.resolveWebsiteAgentHandoff(input.handoffToken);
4071
+ const state = this.stateStore.readState();
4072
+ const agent = this.findAgentByIdOrName(state, input.agentSelector);
4073
+ if (!agent || agent.metadata?.signature === 'forkit-connect-desktop') {
4074
+ throw new Error('AGENT_NOT_FOUND');
4075
+ }
4076
+ const existingGaid = normalizeDisplayText(agent.metadata?.passport_gaid);
4077
+ if (existingGaid && existingGaid !== handoff.passport.gaid) {
4078
+ throw new Error('AGENT_ALREADY_CONNECTED_TO_ANOTHER_PASSPORT');
4079
+ }
4080
+ const alreadyConnected = existingGaid === handoff.passport.gaid && this.hasRuntimeSignalKey(handoff.passport.gaid);
4081
+ let rawKey = alreadyConnected
4082
+ ? this.credentialStore.getRuntimeSignalApiKey(handoff.passport.gaid)
4083
+ : null;
4084
+ let keyRef = normalizeDisplayText(agent.metadata?.runtime_signal_key_ref);
4085
+ if (!alreadyConnected) {
4086
+ const secureStorage = this.getCredentialStoreStatus();
4087
+ if (!secureStorage.available || secureStorage.plaintextFallbackActive) {
4088
+ this.recordAgentConnectionFault({
4089
+ eventType: 'connect_secure_storage_failed',
4090
+ agent,
4091
+ passportGaid: handoff.passport.gaid,
4092
+ workspaceId: handoff.passport.workspaceId,
4093
+ projectId: handoff.passport.projectId,
4094
+ metadata: {
4095
+ failure_stage: 'preflight',
4096
+ reason_code: secureStorage.plaintextFallbackActive
4097
+ ? 'plaintext_fallback_not_allowed'
4098
+ : 'secure_storage_unavailable',
4099
+ storage_backend: secureStorage.backend,
4100
+ credential_persisted: false,
4101
+ },
4102
+ });
4103
+ throw new Error('SECURE_STORAGE_REQUIRED');
4104
+ }
4105
+ const keyResult = await this.getApiClient(this.stateStore.readState()).createPassportApiKey(handoff.passport.gaid, `Forkit Connect · ${agent.agent_name}`.slice(0, 100));
4106
+ if (!keyResult.ok || !isRecord(keyResult.body)) {
4107
+ throw new Error(this.extractBackendErrorCode(keyResult.body) || `AGENT_KEY_PROVISION_FAILED:${keyResult.status}`);
4108
+ }
4109
+ rawKey = normalizeDisplayText(keyResult.body.rawKey);
4110
+ const keyId = normalizeDisplayText(keyResult.body.keyId);
4111
+ if (!rawKey || !keyId)
4112
+ throw new Error('AGENT_KEY_RESPONSE_INVALID');
4113
+ try {
4114
+ const credential = this.setRuntimeSignalApiKeyForGaid(handoff.passport.gaid, rawKey);
4115
+ keyRef = credential?.credentialRef ?? null;
4116
+ }
4117
+ catch {
4118
+ let registryKeyRevocationConfirmed = false;
4119
+ try {
4120
+ const revocation = await this.getApiClient(this.stateStore.readState()).revokePassportApiKey(handoff.passport.gaid, keyId);
4121
+ registryKeyRevocationConfirmed = revocation.ok;
4122
+ }
4123
+ catch {
4124
+ // The original secure-storage failure is the actionable local error.
4125
+ // Registry-side key expiry/revocation remains independently auditable.
4126
+ }
4127
+ this.recordAgentConnectionFault({
4128
+ eventType: 'connect_secure_storage_failed',
4129
+ agent,
4130
+ passportGaid: handoff.passport.gaid,
4131
+ workspaceId: handoff.passport.workspaceId,
4132
+ projectId: handoff.passport.projectId,
4133
+ metadata: {
4134
+ failure_stage: 'credential_write',
4135
+ reason_code: 'secure_storage_write_failed',
4136
+ storage_backend: secureStorage.backend,
4137
+ credential_persisted: false,
4138
+ registry_key_revocation_confirmed: registryKeyRevocationConfirmed,
4139
+ },
4140
+ });
4141
+ throw new Error('SECURE_STORAGE_WRITE_FAILED');
4142
+ }
4143
+ }
4144
+ const approvedAt = nowIso();
4145
+ const agentSignature = normalizeDisplayText(agent.metadata?.signature)?.toLowerCase() ?? null;
4146
+ const activityAdapter = agentSignature ? AGENT_ACTIVITY_ADAPTER_BY_SIGNATURE[agentSignature] ?? null : null;
4147
+ const connectedAgent = {
4148
+ ...agent,
4149
+ status: 'connected',
4150
+ persistence: 'permanent',
4151
+ last_seen_at: approvedAt,
4152
+ discovery_sources: [...new Set([...agent.discovery_sources, 'manual_connect'])],
4153
+ metadata: {
4154
+ ...agent.metadata,
4155
+ passport_gaid: handoff.passport.gaid,
4156
+ passport_name: handoff.passport.name,
4157
+ workspace_id: handoff.passport.workspaceId,
4158
+ project_id: handoff.passport.projectId,
4159
+ tracking_status: 'connected',
4160
+ activity_adapter: activityAdapter,
4161
+ activity_observation_level: activityAdapter ? 'detailed' : 'presence_only',
4162
+ website_handoff_approved_at: approvedAt,
4163
+ runtime_signal_key_ref: keyRef,
4164
+ },
4165
+ };
4166
+ this.stateStore.upsertDetectedAgent(connectedAgent);
4167
+ let verificationEvent = [...this.stateStore.readState().c2_events]
4168
+ .reverse()
4169
+ .find((item) => item.passport_gaid === handoff.passport.gaid && item.event_type === 'connect_agent_connection_verified');
4170
+ if (!verificationEvent) {
4171
+ verificationEvent = this.recordC2LifecycleEvent({
4172
+ eventType: 'connect_agent_connection_verified',
4173
+ occurredAt: approvedAt,
4174
+ passportGaid: handoff.passport.gaid,
4175
+ workspaceId: handoff.passport.workspaceId,
4176
+ projectId: handoff.passport.projectId,
4177
+ metadata: {
4178
+ agent_id: connectedAgent.agent_id,
4179
+ agent_name: connectedAgent.agent_name,
4180
+ agent_type: connectedAgent.agent_type,
4181
+ process_signature_hash: connectedAgent.process_signature_hash,
4182
+ source: 'website_agent_handoff',
4183
+ safe_metadata_only: true,
4184
+ },
4185
+ });
4186
+ this.stateStore.addEvidenceEvent({
4187
+ type: 'agent_connected',
4188
+ details: {
4189
+ agent_id: connectedAgent.agent_id,
4190
+ agent_name: connectedAgent.agent_name,
4191
+ gaid: handoff.passport.gaid,
4192
+ source: 'website_agent_handoff',
4193
+ },
4194
+ });
4195
+ }
4196
+ if (!activityAdapter) {
4197
+ this.recordAgentConnectionFault({
4198
+ eventType: 'connect_agent_adapter_unavailable',
4199
+ agent: connectedAgent,
4200
+ passportGaid: handoff.passport.gaid,
4201
+ workspaceId: handoff.passport.workspaceId,
4202
+ projectId: handoff.passport.projectId,
4203
+ metadata: {
4204
+ adapter_status: 'unavailable',
4205
+ reason_code: 'no_supported_activity_adapter',
4206
+ observation_level: 'presence_only',
4207
+ detected_signature: agentSignature ?? 'unknown',
4208
+ },
4209
+ });
4210
+ }
4211
+ const sync = await this.syncWebsiteAgentConnectionEvent(handoff.passport.gaid, rawKey);
4212
+ return {
4213
+ action: alreadyConnected ? 'already_connected' : 'connected',
4214
+ agent: connectedAgent,
4215
+ passport: handoff.passport,
4216
+ activityConfirmed: sync.confirmed,
4217
+ syncPending: sync.pending,
4218
+ };
4219
+ }
3841
4220
  async registerObservedAgent(input) {
3842
4221
  const candidate = this.buildObservedAgentCandidate(input);
3843
4222
  const scanResult = await this.scanAgents({
@@ -6921,34 +7300,35 @@ class ConnectV1Service {
6921
7300
  return nextSession;
6922
7301
  }
6923
7302
  buildC2RuntimeSignalPayload(event, apiKey, scope) {
7303
+ const metadata = sanitizeC2RuntimeSignalMetadata({
7304
+ client_event_id: event.event_id, // traceable but excluded from server idempotency hash
7305
+ runtime_gaid: event.runtime_gaid,
7306
+ runtime_name: event.runtime_name,
7307
+ runtime_type: event.runtime_type,
7308
+ model_name: event.model_name,
7309
+ discoveryHash: event.discovery_hash,
7310
+ registrationKey: event.registration_key,
7311
+ passportGaid: event.passport_gaid,
7312
+ pulse_status: event.pulse_status,
7313
+ connection_classification: event.connection_classification,
7314
+ daemon_running: event.daemon_running,
7315
+ shadow_candidate_reason: event.shadow_candidate_reason,
7316
+ ...event.metadata,
7317
+ binding_id: scope?.bindingId ?? null,
7318
+ connect_device_id: scope?.connectDeviceId ?? null,
7319
+ workspaceId: scope?.workspaceId ?? event.workspace_id ?? null,
7320
+ projectId: scope?.projectId ?? event.project_id ?? null,
7321
+ workspace_id: scope?.workspaceId ?? event.workspace_id ?? null,
7322
+ project_id: scope?.projectId ?? event.project_id ?? null,
7323
+ evidence_type: 'observed_runtime_session',
7324
+ });
6924
7325
  return {
6925
7326
  gaid: event.passport_gaid,
6926
7327
  apiKey,
6927
7328
  binding_id: scope?.bindingId ?? null,
6928
7329
  eventType: event.event_type,
6929
7330
  timestamp: event.occurred_at,
6930
- metadata: {
6931
- client_event_id: event.event_id, // traceable but excluded from server idempotency hash
6932
- runtime_gaid: event.runtime_gaid,
6933
- runtime_name: event.runtime_name,
6934
- runtime_type: event.runtime_type,
6935
- model_name: event.model_name,
6936
- discoveryHash: event.discovery_hash,
6937
- registrationKey: event.registration_key,
6938
- passportGaid: event.passport_gaid,
6939
- pulse_status: event.pulse_status,
6940
- connection_classification: event.connection_classification,
6941
- daemon_running: event.daemon_running,
6942
- shadow_candidate_reason: event.shadow_candidate_reason,
6943
- ...event.metadata,
6944
- binding_id: scope?.bindingId ?? null,
6945
- connect_device_id: scope?.connectDeviceId ?? null,
6946
- workspaceId: scope?.workspaceId ?? event.workspace_id ?? null,
6947
- projectId: scope?.projectId ?? event.project_id ?? null,
6948
- workspace_id: scope?.workspaceId ?? event.workspace_id ?? null,
6949
- project_id: scope?.projectId ?? event.project_id ?? null,
6950
- evidence_type: 'observed_runtime_session',
6951
- },
7331
+ metadata,
6952
7332
  };
6953
7333
  }
6954
7334
  runtimeRecommendedAction(runtimePassport) {
@@ -7087,6 +7467,7 @@ class ConnectV1Service {
7087
7467
  let succeeded = 0;
7088
7468
  let failed = 0;
7089
7469
  let firstError = null;
7470
+ const connectivityFailure = { value: null };
7090
7471
  // When the server signals rate-limit (429) we record the retry-after deadline and
7091
7472
  // stop sending for this cycle to avoid burning through the remaining budget.
7092
7473
  let rateLimitedUntilMs = null;
@@ -7154,7 +7535,13 @@ class ConnectV1Service {
7154
7535
  source: 'runtimeSignal',
7155
7536
  });
7156
7537
  const error = `c2_sync_${result.status}`;
7157
- this.stateStore.markC2EventFailed(event.event_id, error);
7538
+ if (result.status >= 500 && result.status <= 599) {
7539
+ this.stateStore.markC2EventRetryable(event.event_id, error);
7540
+ connectivityFailure.value ??= { event, reasonCode: error };
7541
+ }
7542
+ else {
7543
+ this.stateStore.markC2EventFailed(event.event_id, error);
7544
+ }
7158
7545
  firstError ??= error;
7159
7546
  failed += 1;
7160
7547
  if (halt) {
@@ -7177,7 +7564,8 @@ class ConnectV1Service {
7177
7564
  }
7178
7565
  catch (error) {
7179
7566
  const message = error instanceof Error ? error.message : 'c2_sync_failed';
7180
- this.stateStore.markC2EventFailed(event.event_id, message);
7567
+ this.stateStore.markC2EventRetryable(event.event_id, message);
7568
+ connectivityFailure.value ??= { event, reasonCode: 'c2_network_unavailable' };
7181
7569
  firstError ??= message;
7182
7570
  failed += 1;
7183
7571
  }
@@ -7228,6 +7616,35 @@ class ConnectV1Service {
7228
7616
  const lastSyncAt = attempted > 0 ? nowIso() : state.last_c2_sync_at;
7229
7617
  const lastSyncError = firstError;
7230
7618
  this.stateStore.updateC2SyncStatus(lastSyncAt, lastSyncError);
7619
+ const connectivityState = this.stateStore.readState();
7620
+ if (connectivityFailure.value && !connectivityState.c2_offline_since) {
7621
+ const failure = connectivityFailure.value;
7622
+ this.recordC2ConnectivityUnavailable(failure.event, failure.reasonCode);
7623
+ }
7624
+ else if (!connectivityFailure.value && connectivityState.c2_offline_since && succeeded > 0) {
7625
+ const restoredAt = nowIso();
7626
+ const offlineSince = connectivityState.c2_offline_since;
7627
+ const durationSeconds = Math.max(0, Math.round((Date.parse(restoredAt) - Date.parse(offlineSince)) / 1000));
7628
+ const offlineEvent = [...connectivityState.c2_events]
7629
+ .reverse()
7630
+ .find((event) => event.event_type === 'connect_sync_offline');
7631
+ const fallbackContext = this.getOperationalPassportContext(connectivityState);
7632
+ this.stateStore.updateC2ConnectivityStatus(null, restoredAt);
7633
+ this.recordC2LifecycleEvent({
7634
+ eventType: 'connect_sync_restored',
7635
+ occurredAt: restoredAt,
7636
+ passportGaid: offlineEvent?.passport_gaid ?? fallbackContext.passportGaid,
7637
+ workspaceId: offlineEvent?.workspace_id ?? fallbackContext.workspaceId,
7638
+ projectId: offlineEvent?.project_id ?? fallbackContext.projectId,
7639
+ metadata: {
7640
+ period_kind: 'connectivity_unavailable',
7641
+ offline_since: offlineSince,
7642
+ restored_at: restoredAt,
7643
+ duration_seconds: durationSeconds,
7644
+ evidence: 'successful_sync',
7645
+ },
7646
+ });
7647
+ }
7231
7648
  const finalState = this.stateStore.readState();
7232
7649
  const finalSummary = this.getC2EventSyncSummary(finalState);
7233
7650
  return {
@@ -7507,7 +7924,12 @@ class ConnectV1Service {
7507
7924
  const state = this.stateStore.readState();
7508
7925
  const daemonStatus = state.daemon_status;
7509
7926
  if (daemonStatus.daemon_running && daemonStatus.daemon_pid && !this.isProcessAlive(daemonStatus.daemon_pid)) {
7510
- return this.markDaemonStopped(daemonStatus.last_error ?? 'daemon_process_missing');
7927
+ return this.markDaemonStopped(daemonStatus.last_error ?? 'daemon_process_missing', {
7928
+ unclean: true,
7929
+ detectedAt: nowIso(),
7930
+ previousStartedAt: daemonStatus.started_at,
7931
+ previousLastScanAt: daemonStatus.last_scan_at,
7932
+ });
7511
7933
  }
7512
7934
  return daemonStatus;
7513
7935
  }
@@ -7519,13 +7941,13 @@ class ConnectV1Service {
7519
7941
  started_at: nowIso(),
7520
7942
  last_error: null,
7521
7943
  }).daemon_status;
7522
- const boundBinding = this.getPrimaryBoundBinding(state);
7944
+ const context = this.getOperationalPassportContext(state);
7523
7945
  this.recordC2LifecycleEvent({
7524
7946
  eventType: 'connect_daemon_started',
7525
7947
  occurredAt: nextStatus.started_at ?? nowIso(),
7526
- passportGaid: boundBinding?.gaid ?? null,
7527
- workspaceId: state.workspace_binding.workspaceId,
7528
- projectId: state.project_binding.projectId,
7948
+ passportGaid: context.passportGaid,
7949
+ workspaceId: context.workspaceId,
7950
+ projectId: context.projectId,
7529
7951
  daemonRunning: true,
7530
7952
  metadata: {
7531
7953
  daemon_pid: pid,
@@ -7533,24 +7955,48 @@ class ConnectV1Service {
7533
7955
  });
7534
7956
  return nextStatus;
7535
7957
  }
7536
- markDaemonStopped(lastError) {
7958
+ markDaemonStopped(lastError, options) {
7537
7959
  const state = this.stateStore.readState();
7960
+ const detectedAt = options?.detectedAt ?? nowIso();
7538
7961
  const nextStatus = this.stateStore.updateDaemonStatus({
7539
7962
  daemon_running: false,
7540
7963
  daemon_pid: null,
7541
7964
  last_error: lastError,
7542
7965
  }).daemon_status;
7543
- const boundBinding = this.getPrimaryBoundBinding(state);
7966
+ const context = this.getOperationalPassportContext(state);
7544
7967
  this.recordC2LifecycleEvent({
7545
7968
  eventType: 'connect_daemon_stopped',
7546
- passportGaid: boundBinding?.gaid ?? null,
7547
- workspaceId: state.workspace_binding.workspaceId,
7548
- projectId: state.project_binding.projectId,
7969
+ occurredAt: detectedAt,
7970
+ passportGaid: context.passportGaid,
7971
+ workspaceId: context.workspaceId,
7972
+ projectId: context.projectId,
7549
7973
  daemonRunning: false,
7550
7974
  metadata: {
7551
7975
  last_error: lastError,
7976
+ stop_classification: options?.unclean ? 'unclean' : 'clean',
7552
7977
  },
7553
7978
  });
7979
+ if (options?.unclean) {
7980
+ const gapStartedAt = options.previousLastScanAt ?? options.previousStartedAt ?? detectedAt;
7981
+ const parsedDuration = Math.round((Date.parse(detectedAt) - Date.parse(gapStartedAt)) / 1000);
7982
+ this.recordC2LifecycleEvent({
7983
+ eventType: 'connect_activity_gap_detected',
7984
+ occurredAt: detectedAt,
7985
+ passportGaid: context.passportGaid,
7986
+ workspaceId: context.workspaceId,
7987
+ projectId: context.projectId,
7988
+ daemonRunning: false,
7989
+ metadata: {
7990
+ gap_kind: 'unclean_shutdown',
7991
+ gap_started_at: gapStartedAt,
7992
+ gap_detected_at: detectedAt,
7993
+ gap_duration_seconds: Number.isFinite(parsedDuration) ? Math.max(0, parsedDuration) : 0,
7994
+ confidence: 'inferred',
7995
+ reason_code: lastError ?? 'daemon_process_missing',
7996
+ data_loss_confirmed: false,
7997
+ },
7998
+ });
7999
+ }
7554
8000
  return nextStatus;
7555
8001
  }
7556
8002
  recordDaemonScan(summary) {
@@ -8934,6 +9380,26 @@ class ConnectV1Service {
8934
9380
  queue_limit: MAX_RUNTIME_SIGNAL_QUEUE_ITEMS,
8935
9381
  },
8936
9382
  });
9383
+ if (trimmed > 0) {
9384
+ const overflowAlreadyPending = this.stateStore.readState().c2_events.some((event) => (event.event_type === 'connect_queue_overflow'
9385
+ && event.passport_gaid === gaid
9386
+ && event.sync_state === 'pending'
9387
+ && event.metadata?.overflow_kind === 'runtime_signal_local_queue_capacity'));
9388
+ if (!overflowAlreadyPending) {
9389
+ this.recordC2LifecycleEvent({
9390
+ eventType: 'connect_queue_overflow',
9391
+ passportGaid: gaid,
9392
+ workspaceId: runtimeScope?.workspaceId ?? state.workspace_binding.workspaceId,
9393
+ projectId: runtimeScope?.projectId ?? state.project_binding.projectId,
9394
+ metadata: {
9395
+ overflow_kind: 'runtime_signal_local_queue_capacity',
9396
+ dropped_event_count: trimmed,
9397
+ queue_limit: MAX_RUNTIME_SIGNAL_QUEUE_ITEMS,
9398
+ data_loss_confirmed: true,
9399
+ },
9400
+ });
9401
+ }
9402
+ }
8937
9403
  return queueItem.id;
8938
9404
  }
8939
9405
  queueConfiguredHeartbeatRuntimeSignal(gaid) {
@@ -33,6 +33,7 @@ export declare class LocalStateStore {
33
33
  pruneRedundantPendingC2Events(): number;
34
34
  backfillMissingGaidOnPendingC2Events(gaid: string): number;
35
35
  updateC2SyncStatus(lastSyncAt: string | null, lastSyncError: string | null): ConnectState;
36
+ updateC2ConnectivityStatus(offlineSince: string | null, restoredAt: string | null): ConnectState;
36
37
  /**
37
38
  * Backfill pending events that have no passport_gaid with the supplied gaid.
38
39
  * Events recorded before a model was bound lack a GAID; once binding is
@@ -42,6 +43,7 @@ export declare class LocalStateStore {
42
43
  backfillC2EventsWithGaid(gaid: string): number;
43
44
  markC2EventSynced(eventId: string): ConnectState;
44
45
  markC2EventFailed(eventId: string, errorMessage: string): ConnectState;
46
+ markC2EventRetryable(eventId: string, errorMessage: string): ConnectState;
45
47
  discardC2Event(eventId: string, errorMessage: string): ConnectState;
46
48
  updatePulseState(summary: VitalityPulseSummary | null, lastError: string | null, pulseEvents?: Array<Omit<VitalityPulseEvent, 'event_id'>>): ConnectState;
47
49
  upsertDetectedRuntime(runtime: DetectedRuntime): ConnectState;
package/dist/v1/state.js CHANGED
@@ -54,6 +54,8 @@ const STATE_SECTION_NAMES = [
54
54
  'daemon_status',
55
55
  'last_c2_sync_at',
56
56
  'last_c2_sync_error',
57
+ 'c2_offline_since',
58
+ 'last_c2_restored_at',
57
59
  'c2_events',
58
60
  'last_pulse_at',
59
61
  'last_pulse_summary',
@@ -401,6 +403,8 @@ function normalizeState(parsed) {
401
403
  daemon_status: normalizeDaemonStatus(value.daemon_status),
402
404
  last_c2_sync_at: typeof value.last_c2_sync_at === 'string' ? value.last_c2_sync_at : null,
403
405
  last_c2_sync_error: typeof value.last_c2_sync_error === 'string' ? value.last_c2_sync_error : null,
406
+ c2_offline_since: typeof value.c2_offline_since === 'string' ? value.c2_offline_since : null,
407
+ last_c2_restored_at: typeof value.last_c2_restored_at === 'string' ? value.last_c2_restored_at : null,
404
408
  c2_events: Array.isArray(value.c2_events) ? value.c2_events : [],
405
409
  last_pulse_at: typeof value.last_pulse_at === 'string' ? value.last_pulse_at : null,
406
410
  last_pulse_summary: value.last_pulse_summary && typeof value.last_pulse_summary === 'object'
@@ -468,6 +472,8 @@ function buildInitialState() {
468
472
  daemon_status: buildDefaultDaemonStatus(),
469
473
  last_c2_sync_at: null,
470
474
  last_c2_sync_error: null,
475
+ c2_offline_since: null,
476
+ last_c2_restored_at: null,
471
477
  c2_events: [],
472
478
  last_pulse_at: null,
473
479
  last_pulse_summary: null,
@@ -644,7 +650,55 @@ class LocalStateStore {
644
650
  const cutoff = Date.now() - C2_SYNCED_EVICTION_AGE_MS;
645
651
  state.c2_events = state.c2_events.filter((event) => event.sync_state !== 'synced' || !event.synced_at || new Date(event.synced_at).getTime() > cutoff);
646
652
  if (state.c2_events.length > C2_EVENT_RETENTION_LIMIT) {
647
- state.c2_events = state.c2_events.slice(-C2_EVENT_RETENTION_LIMIT);
653
+ const existingMarker = state.c2_events.find((event) => (event.event_type === 'connect_queue_overflow'
654
+ && event.sync_state === 'pending'
655
+ && event.metadata?.overflow_kind === 'c2_lifecycle_queue_capacity'));
656
+ const requiredDrops = state.c2_events.length - C2_EVENT_RETENTION_LIMIT + (existingMarker ? 0 : 1);
657
+ const droppedIds = new Set();
658
+ for (const event of state.c2_events) {
659
+ if (droppedIds.size >= requiredDrops)
660
+ break;
661
+ if (event.event_id === existingMarker?.event_id)
662
+ continue;
663
+ droppedIds.add(event.event_id);
664
+ }
665
+ state.c2_events = state.c2_events.filter((event) => !droppedIds.has(event.event_id));
666
+ const detectedAt = nowIso();
667
+ const scopeEvent = [...events, ...state.c2_events]
668
+ .reverse()
669
+ .find((event) => Boolean(event.passport_gaid));
670
+ const previousDropped = Number(existingMarker?.metadata?.dropped_event_count ?? 0);
671
+ const marker = {
672
+ event_id: existingMarker?.event_id ?? (0, node_crypto_1.randomUUID)(),
673
+ event_type: 'connect_queue_overflow',
674
+ occurred_at: detectedAt,
675
+ runtime_gaid: null,
676
+ runtime_name: null,
677
+ runtime_type: null,
678
+ model_name: null,
679
+ discovery_hash: null,
680
+ registration_key: null,
681
+ passport_gaid: scopeEvent?.passport_gaid ?? null,
682
+ workspace_id: scopeEvent?.workspace_id ?? null,
683
+ project_id: scopeEvent?.project_id ?? null,
684
+ pulse_status: null,
685
+ connection_classification: null,
686
+ daemon_running: null,
687
+ shadow_candidate_reason: null,
688
+ metadata: {
689
+ overflow_kind: 'c2_lifecycle_queue_capacity',
690
+ dropped_event_count: previousDropped + requiredDrops,
691
+ queue_limit: C2_EVENT_RETENTION_LIMIT,
692
+ detected_at: detectedAt,
693
+ data_loss_confirmed: true,
694
+ },
695
+ sync_state: 'pending',
696
+ synced_at: null,
697
+ last_error: null,
698
+ retry_count: 0,
699
+ };
700
+ state.c2_events = state.c2_events.filter((event) => event.event_id !== marker.event_id);
701
+ state.c2_events.push(marker);
648
702
  }
649
703
  });
650
704
  }
@@ -703,6 +757,12 @@ class LocalStateStore {
703
757
  state.last_c2_sync_error = lastSyncError;
704
758
  });
705
759
  }
760
+ updateC2ConnectivityStatus(offlineSince, restoredAt) {
761
+ return this.mutateState((state) => {
762
+ state.c2_offline_since = offlineSince;
763
+ state.last_c2_restored_at = restoredAt;
764
+ });
765
+ }
706
766
  /**
707
767
  * Backfill pending events that have no passport_gaid with the supplied gaid.
708
768
  * Events recorded before a model was bound lack a GAID; once binding is
@@ -759,6 +819,22 @@ class LocalStateStore {
759
819
  };
760
820
  });
761
821
  }
822
+ markC2EventRetryable(eventId, errorMessage) {
823
+ return this.mutateState((state) => {
824
+ const index = state.c2_events.findIndex((event) => event.event_id === eventId);
825
+ if (index < 0)
826
+ return;
827
+ const current = state.c2_events[index];
828
+ if (!current)
829
+ return;
830
+ state.c2_events[index] = {
831
+ ...current,
832
+ sync_state: 'pending',
833
+ synced_at: null,
834
+ last_error: errorMessage,
835
+ };
836
+ });
837
+ }
762
838
  discardC2Event(eventId, errorMessage) {
763
839
  return this.mutateState((state) => {
764
840
  const index = state.c2_events.findIndex((event) => event.event_id === eventId);
@@ -29,7 +29,7 @@ export type NotificationSuggestedAction = 'open_inbox' | 'reconnect_account' | '
29
29
  export type C2SessionMode = 'active' | 'paused' | 'standby' | 'sleeping' | 'offline' | 'revoked';
30
30
  export type C2SessionLocalSyncState = 'observed' | 'ack_pending' | 'acknowledged' | 'credential_reconnect_needed';
31
31
  export type C2SessionControlSource = 'website' | 'backend_governor' | 'local';
32
- export type C2LiteEventType = 'connect_runtime_detected' | 'connect_model_detected' | 'connect_passport_draft_requested' | 'connect_passport_draft_created' | 'connect_passport_bound' | 'connect_runtime_bound_to_model' | 'connect_runtime_linked_to_agent' | 'connect_runtime_health_changed' | 'connect_daemon_started' | 'connect_daemon_stopped' | 'connect_runtime_active' | 'connect_runtime_unavailable' | 'connect_runtime_restored' | 'connect_pulse_green' | 'connect_pulse_amber' | 'connect_pulse_red' | 'connect_shadow_candidate_detected' | 'connect_training_initialized' | 'connect_training_started' | 'connect_fine_tuning_started' | 'connect_dataset_changed' | 'connect_metric_logged' | 'connect_model_version_created' | 'connect_artifact_ready' | 'connect_training_completed' | 'connect_agent_detected' | 'connect_agent_connected' | 'connect_agent_model_linked' | 'connect_agent_tool_observed' | 'connect_agent_tool_blocked' | 'connect_agent_tool_timed_out' | 'connect_agent_inactive' | 'connect_model_evolution_detected' | 'connect_model_evolution_archived' | 'connect_session_paused_acknowledged' | 'connect_session_resumed_acknowledged' | 'connect_session_access_revoked_acknowledged' | 'connect_passport_governor_paused_observed' | 'connect_passport_governor_revoked_observed' | 'connect_credential_invalid_observed' | 'connect_silent_model_change' | 'connect_model_drift_detected' | 'connect_hallucination_pattern' | 'connect_agent_drift_detected' | 'connect_prompt_injection_suspected' | 'connect_adversarial_influence_observed' | 'connect_runtime_signal_key_auto_provisioned' | 'connect_lifecycle_monitor_pass_completed';
32
+ export type C2LiteEventType = 'connect_runtime_detected' | 'connect_model_detected' | 'connect_passport_draft_requested' | 'connect_passport_draft_created' | 'connect_passport_bound' | 'connect_runtime_bound_to_model' | 'connect_runtime_linked_to_agent' | 'connect_runtime_health_changed' | 'connect_daemon_started' | 'connect_daemon_stopped' | 'connect_sync_offline' | 'connect_sync_restored' | 'connect_activity_gap_detected' | 'connect_secure_storage_failed' | 'connect_queue_overflow' | 'connect_runtime_active' | 'connect_runtime_unavailable' | 'connect_runtime_restored' | 'connect_pulse_green' | 'connect_pulse_amber' | 'connect_pulse_red' | 'connect_shadow_candidate_detected' | 'connect_training_initialized' | 'connect_training_started' | 'connect_fine_tuning_started' | 'connect_dataset_changed' | 'connect_metric_logged' | 'connect_model_version_created' | 'connect_artifact_ready' | 'connect_training_completed' | 'connect_agent_detected' | 'connect_agent_connected' | 'connect_agent_connection_verified' | 'connect_agent_started' | 'connect_agent_adapter_unavailable' | 'connect_agent_model_linked' | 'connect_agent_tool_observed' | 'connect_agent_tool_blocked' | 'connect_agent_tool_timed_out' | 'connect_agent_inactive' | 'connect_model_evolution_detected' | 'connect_model_evolution_archived' | 'connect_session_paused_acknowledged' | 'connect_session_resumed_acknowledged' | 'connect_session_access_revoked_acknowledged' | 'connect_passport_governor_paused_observed' | 'connect_passport_governor_revoked_observed' | 'connect_credential_invalid_observed' | 'connect_silent_model_change' | 'connect_model_drift_detected' | 'connect_hallucination_pattern' | 'connect_agent_drift_detected' | 'connect_prompt_injection_suspected' | 'connect_adversarial_influence_observed' | 'connect_runtime_signal_key_auto_provisioned' | 'connect_lifecycle_monitor_pass_completed';
33
33
  export type C2LiteSyncState = 'pending' | 'synced' | 'dead_letter';
34
34
  export type PendingReviewKind = 'silent_model_change' | 'model_drift' | 'hallucination_pattern' | 'agent_drift' | 'prompt_injection' | 'adversarial_influence';
35
35
  export type PendingReviewStatus = 'pending' | 'confirmed' | 'dismissed' | 'auto_confirmed';
@@ -899,6 +899,8 @@ export interface ConnectState {
899
899
  daemon_status: DaemonStatus;
900
900
  last_c2_sync_at: string | null;
901
901
  last_c2_sync_error: string | null;
902
+ c2_offline_since: string | null;
903
+ last_c2_restored_at: string | null;
902
904
  c2_events: C2LifecycleEvent[];
903
905
  last_pulse_at: string | null;
904
906
  last_pulse_summary: VitalityPulseSummary | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forkit-connect",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "description": "Forkit Connect Local Engine - The Global AI Governance Fabric",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",