forkit-connect 0.1.35 → 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. */
@@ -2899,6 +2908,51 @@ class ConnectV1Service {
2899
2908
  })
2900
2909
  .at(0);
2901
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
+ }
2902
2956
  setRuntimeSignalApiKeyForGaid(gaid, apiKey) {
2903
2957
  if (!gaid || !apiKey)
2904
2958
  return null;
@@ -3236,6 +3290,27 @@ class ConnectV1Service {
3236
3290
  });
3237
3291
  }
3238
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
+ }
3239
3314
  getC2EventSyncSummary(state) {
3240
3315
  return state.c2_events.reduce((summary, event) => {
3241
3316
  if (event.sync_state === 'synced') {
@@ -3667,10 +3742,11 @@ class ConnectV1Service {
3667
3742
  process_signature_hash: processSignatureHash,
3668
3743
  detected_at: input.existing?.detected_at ?? input.detectedAt,
3669
3744
  last_seen_at: input.detectedAt,
3670
- status: isConnectSelf || input.existing?.status === 'connected' ? 'connected' : 'detected',
3745
+ status: isConnectSelf || existingPermanent ? 'connected' : 'detected',
3671
3746
  persistence,
3672
3747
  discovery_sources: [...new Set([...(input.existing?.discovery_sources ?? []), input.discoverySource])],
3673
3748
  metadata: {
3749
+ ...(input.existing?.metadata ?? {}),
3674
3750
  executable_name: input.candidate.executable_name,
3675
3751
  signature: input.candidate.signature,
3676
3752
  persistence,
@@ -3687,12 +3763,29 @@ class ConnectV1Service {
3687
3763
  };
3688
3764
  }
3689
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;
3690
3782
  const eventInput = {
3691
3783
  eventType,
3784
+ passportGaid,
3692
3785
  runtimeGaid: typeof metadata.runtime_gaid === 'string' ? metadata.runtime_gaid : null,
3693
3786
  modelName: null,
3694
- workspaceId: this.stateStore.readState().workspace_binding.workspaceId,
3695
- projectId: this.stateStore.readState().project_binding.projectId,
3787
+ workspaceId,
3788
+ projectId,
3696
3789
  metadata: {
3697
3790
  agent_id: agent.agent_id,
3698
3791
  agent_name: agent.agent_name,
@@ -3751,6 +3844,19 @@ class ConnectV1Service {
3751
3844
  byId.set(nextAgent.agent_id, nextAgent);
3752
3845
  if (existing) {
3753
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
+ }
3754
3860
  }
3755
3861
  else {
3756
3862
  newAgents += 1;
@@ -3883,6 +3989,234 @@ class ConnectV1Service {
3883
3989
  this.queueAgentC2Event(nextAgent, 'connect_agent_connected', {}, updatedAt);
3884
3990
  return nextAgent;
3885
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
+ }
3886
4220
  async registerObservedAgent(input) {
3887
4221
  const candidate = this.buildObservedAgentCandidate(input);
3888
4222
  const scanResult = await this.scanAgents({
@@ -7133,6 +7467,7 @@ class ConnectV1Service {
7133
7467
  let succeeded = 0;
7134
7468
  let failed = 0;
7135
7469
  let firstError = null;
7470
+ const connectivityFailure = { value: null };
7136
7471
  // When the server signals rate-limit (429) we record the retry-after deadline and
7137
7472
  // stop sending for this cycle to avoid burning through the remaining budget.
7138
7473
  let rateLimitedUntilMs = null;
@@ -7200,7 +7535,13 @@ class ConnectV1Service {
7200
7535
  source: 'runtimeSignal',
7201
7536
  });
7202
7537
  const error = `c2_sync_${result.status}`;
7203
- 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
+ }
7204
7545
  firstError ??= error;
7205
7546
  failed += 1;
7206
7547
  if (halt) {
@@ -7223,7 +7564,8 @@ class ConnectV1Service {
7223
7564
  }
7224
7565
  catch (error) {
7225
7566
  const message = error instanceof Error ? error.message : 'c2_sync_failed';
7226
- this.stateStore.markC2EventFailed(event.event_id, message);
7567
+ this.stateStore.markC2EventRetryable(event.event_id, message);
7568
+ connectivityFailure.value ??= { event, reasonCode: 'c2_network_unavailable' };
7227
7569
  firstError ??= message;
7228
7570
  failed += 1;
7229
7571
  }
@@ -7274,6 +7616,35 @@ class ConnectV1Service {
7274
7616
  const lastSyncAt = attempted > 0 ? nowIso() : state.last_c2_sync_at;
7275
7617
  const lastSyncError = firstError;
7276
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
+ }
7277
7648
  const finalState = this.stateStore.readState();
7278
7649
  const finalSummary = this.getC2EventSyncSummary(finalState);
7279
7650
  return {
@@ -7553,7 +7924,12 @@ class ConnectV1Service {
7553
7924
  const state = this.stateStore.readState();
7554
7925
  const daemonStatus = state.daemon_status;
7555
7926
  if (daemonStatus.daemon_running && daemonStatus.daemon_pid && !this.isProcessAlive(daemonStatus.daemon_pid)) {
7556
- 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
+ });
7557
7933
  }
7558
7934
  return daemonStatus;
7559
7935
  }
@@ -7565,13 +7941,13 @@ class ConnectV1Service {
7565
7941
  started_at: nowIso(),
7566
7942
  last_error: null,
7567
7943
  }).daemon_status;
7568
- const boundBinding = this.getPrimaryBoundBinding(state);
7944
+ const context = this.getOperationalPassportContext(state);
7569
7945
  this.recordC2LifecycleEvent({
7570
7946
  eventType: 'connect_daemon_started',
7571
7947
  occurredAt: nextStatus.started_at ?? nowIso(),
7572
- passportGaid: boundBinding?.gaid ?? null,
7573
- workspaceId: state.workspace_binding.workspaceId,
7574
- projectId: state.project_binding.projectId,
7948
+ passportGaid: context.passportGaid,
7949
+ workspaceId: context.workspaceId,
7950
+ projectId: context.projectId,
7575
7951
  daemonRunning: true,
7576
7952
  metadata: {
7577
7953
  daemon_pid: pid,
@@ -7579,24 +7955,48 @@ class ConnectV1Service {
7579
7955
  });
7580
7956
  return nextStatus;
7581
7957
  }
7582
- markDaemonStopped(lastError) {
7958
+ markDaemonStopped(lastError, options) {
7583
7959
  const state = this.stateStore.readState();
7960
+ const detectedAt = options?.detectedAt ?? nowIso();
7584
7961
  const nextStatus = this.stateStore.updateDaemonStatus({
7585
7962
  daemon_running: false,
7586
7963
  daemon_pid: null,
7587
7964
  last_error: lastError,
7588
7965
  }).daemon_status;
7589
- const boundBinding = this.getPrimaryBoundBinding(state);
7966
+ const context = this.getOperationalPassportContext(state);
7590
7967
  this.recordC2LifecycleEvent({
7591
7968
  eventType: 'connect_daemon_stopped',
7592
- passportGaid: boundBinding?.gaid ?? null,
7593
- workspaceId: state.workspace_binding.workspaceId,
7594
- projectId: state.project_binding.projectId,
7969
+ occurredAt: detectedAt,
7970
+ passportGaid: context.passportGaid,
7971
+ workspaceId: context.workspaceId,
7972
+ projectId: context.projectId,
7595
7973
  daemonRunning: false,
7596
7974
  metadata: {
7597
7975
  last_error: lastError,
7976
+ stop_classification: options?.unclean ? 'unclean' : 'clean',
7598
7977
  },
7599
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
+ }
7600
8000
  return nextStatus;
7601
8001
  }
7602
8002
  recordDaemonScan(summary) {
@@ -8980,6 +9380,26 @@ class ConnectV1Service {
8980
9380
  queue_limit: MAX_RUNTIME_SIGNAL_QUEUE_ITEMS,
8981
9381
  },
8982
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
+ }
8983
9403
  return queueItem.id;
8984
9404
  }
8985
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.35",
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",