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 +267 -2
- package/dist/v1/api.d.ts +13 -0
- package/dist/v1/api.js +27 -0
- package/dist/v1/filesystem-models.js +3 -0
- package/dist/v1/service.d.ts +33 -1
- package/dist/v1/service.js +435 -15
- package/dist/v1/state.d.ts +2 -0
- package/dist/v1/state.js +77 -1
- package/dist/v1/types.d.ts +3 -1
- package/package.json +1 -1
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 & 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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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('/', (
|
|
11256
|
-
|
|
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
|
}
|
package/dist/v1/service.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/v1/service.js
CHANGED
|
@@ -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 ||
|
|
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
|
|
3695
|
-
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
|
-
|
|
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.
|
|
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
|
|
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:
|
|
7573
|
-
workspaceId:
|
|
7574
|
-
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
|
|
7966
|
+
const context = this.getOperationalPassportContext(state);
|
|
7590
7967
|
this.recordC2LifecycleEvent({
|
|
7591
7968
|
eventType: 'connect_daemon_stopped',
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
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) {
|
package/dist/v1/state.d.ts
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/dist/v1/types.d.ts
CHANGED
|
@@ -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;
|