forkit-connect 0.1.34 → 0.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/launcher.js +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 +503 -37
- 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. */
|
|
@@ -52,9 +61,54 @@ const SMART_INBOX_CONFIDENCE_PRIORITY = {
|
|
|
52
61
|
low: 2,
|
|
53
62
|
};
|
|
54
63
|
const SMART_INBOX_FRESH_MAX_AGE_MS = 45 * 1000;
|
|
64
|
+
const C2_RUNTIME_SIGNAL_SENSITIVE_METADATA_PATTERNS = [
|
|
65
|
+
/prompt(?!_?tokens?$)/i,
|
|
66
|
+
/message/i,
|
|
67
|
+
/conversation/i,
|
|
68
|
+
/transcript/i,
|
|
69
|
+
/chat/i,
|
|
70
|
+
/log(s|_?text|_?content)?$/i,
|
|
71
|
+
/secret/i,
|
|
72
|
+
/password/i,
|
|
73
|
+
/api[_-]?key/i,
|
|
74
|
+
/token(?!s?($|_?(count|in|out|total|usage)$)|Usage$)/i,
|
|
75
|
+
/authorization/i,
|
|
76
|
+
/cookie/i,
|
|
77
|
+
/private[_-]?key/i,
|
|
78
|
+
/credential/i,
|
|
79
|
+
/email/i,
|
|
80
|
+
/phone/i,
|
|
81
|
+
/ssn/i,
|
|
82
|
+
/address/i,
|
|
83
|
+
/file[_-]?(content|body|text|data)/i,
|
|
84
|
+
/model[_-]?(weight|weights|binary|blob)/i,
|
|
85
|
+
/dataset[_-]?(row|rows|sample|content|record)/i,
|
|
86
|
+
];
|
|
55
87
|
function isRecord(value) {
|
|
56
88
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
57
89
|
}
|
|
90
|
+
function isSensitiveC2RuntimeSignalMetadataKey(key) {
|
|
91
|
+
return C2_RUNTIME_SIGNAL_SENSITIVE_METADATA_PATTERNS.some((pattern) => pattern.test(key));
|
|
92
|
+
}
|
|
93
|
+
function sanitizeC2RuntimeSignalMetadataValue(value) {
|
|
94
|
+
if (Array.isArray(value)) {
|
|
95
|
+
return value.map((item) => sanitizeC2RuntimeSignalMetadataValue(item));
|
|
96
|
+
}
|
|
97
|
+
if (!isRecord(value)) {
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
return sanitizeC2RuntimeSignalMetadata(value);
|
|
101
|
+
}
|
|
102
|
+
function sanitizeC2RuntimeSignalMetadata(metadata) {
|
|
103
|
+
const safeMetadata = {};
|
|
104
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
105
|
+
if (isSensitiveC2RuntimeSignalMetadataKey(key)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
safeMetadata[key] = sanitizeC2RuntimeSignalMetadataValue(value);
|
|
109
|
+
}
|
|
110
|
+
return safeMetadata;
|
|
111
|
+
}
|
|
58
112
|
function extractApiErrorCode(body) {
|
|
59
113
|
if (!isRecord(body))
|
|
60
114
|
return null;
|
|
@@ -2854,6 +2908,51 @@ class ConnectV1Service {
|
|
|
2854
2908
|
})
|
|
2855
2909
|
.at(0);
|
|
2856
2910
|
}
|
|
2911
|
+
getOperationalPassportContext(state) {
|
|
2912
|
+
const normalize = (value) => typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
2913
|
+
const agent = [...state.detected_agents]
|
|
2914
|
+
.filter((item) => normalize(item.metadata?.passport_gaid))
|
|
2915
|
+
.sort((left, right) => {
|
|
2916
|
+
const connectedDelta = Number(right.status === 'connected') - Number(left.status === 'connected');
|
|
2917
|
+
if (connectedDelta !== 0)
|
|
2918
|
+
return connectedDelta;
|
|
2919
|
+
return String(right.last_seen_at || '').localeCompare(String(left.last_seen_at || ''));
|
|
2920
|
+
})
|
|
2921
|
+
.at(0);
|
|
2922
|
+
if (agent) {
|
|
2923
|
+
return {
|
|
2924
|
+
passportGaid: normalize(agent.metadata?.passport_gaid),
|
|
2925
|
+
workspaceId: normalize(agent.metadata?.workspace_id),
|
|
2926
|
+
projectId: normalize(agent.metadata?.project_id),
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2929
|
+
const binding = this.getPrimaryBoundBinding(state);
|
|
2930
|
+
return {
|
|
2931
|
+
passportGaid: normalize(binding?.gaid),
|
|
2932
|
+
workspaceId: normalize(binding?.workspaceId) ?? normalize(state.workspace_binding.workspaceId),
|
|
2933
|
+
projectId: normalize(binding?.projectId) ?? normalize(state.project_binding.projectId),
|
|
2934
|
+
};
|
|
2935
|
+
}
|
|
2936
|
+
recordAgentConnectionFault(input) {
|
|
2937
|
+
const alreadyRecorded = this.stateStore.readState().c2_events.some((event) => (event.event_type === input.eventType
|
|
2938
|
+
&& event.passport_gaid === input.passportGaid
|
|
2939
|
+
&& event.metadata?.agent_id === input.agent.agent_id
|
|
2940
|
+
&& event.metadata?.failure_stage === input.metadata.failure_stage));
|
|
2941
|
+
if (alreadyRecorded)
|
|
2942
|
+
return;
|
|
2943
|
+
this.recordC2LifecycleEvent({
|
|
2944
|
+
eventType: input.eventType,
|
|
2945
|
+
passportGaid: input.passportGaid,
|
|
2946
|
+
workspaceId: input.workspaceId,
|
|
2947
|
+
projectId: input.projectId,
|
|
2948
|
+
metadata: {
|
|
2949
|
+
agent_id: input.agent.agent_id,
|
|
2950
|
+
agent_name: input.agent.agent_name,
|
|
2951
|
+
agent_type: input.agent.agent_type,
|
|
2952
|
+
...input.metadata,
|
|
2953
|
+
},
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2857
2956
|
setRuntimeSignalApiKeyForGaid(gaid, apiKey) {
|
|
2858
2957
|
if (!gaid || !apiKey)
|
|
2859
2958
|
return null;
|
|
@@ -3191,6 +3290,27 @@ class ConnectV1Service {
|
|
|
3191
3290
|
});
|
|
3192
3291
|
}
|
|
3193
3292
|
}
|
|
3293
|
+
recordC2ConnectivityUnavailable(event, reasonCode) {
|
|
3294
|
+
const state = this.stateStore.readState();
|
|
3295
|
+
if (state.c2_offline_since)
|
|
3296
|
+
return;
|
|
3297
|
+
const offlineSince = nowIso();
|
|
3298
|
+
this.stateStore.updateC2ConnectivityStatus(offlineSince, state.last_c2_restored_at);
|
|
3299
|
+
this.recordC2LifecycleEvent({
|
|
3300
|
+
eventType: 'connect_sync_offline',
|
|
3301
|
+
occurredAt: offlineSince,
|
|
3302
|
+
passportGaid: event.passport_gaid,
|
|
3303
|
+
workspaceId: event.workspace_id,
|
|
3304
|
+
projectId: event.project_id,
|
|
3305
|
+
metadata: {
|
|
3306
|
+
period_kind: 'connectivity_unavailable',
|
|
3307
|
+
offline_since: offlineSince,
|
|
3308
|
+
detected_at: offlineSince,
|
|
3309
|
+
reason_code: reasonCode,
|
|
3310
|
+
evidence: 'observed_sync_failure',
|
|
3311
|
+
},
|
|
3312
|
+
});
|
|
3313
|
+
}
|
|
3194
3314
|
getC2EventSyncSummary(state) {
|
|
3195
3315
|
return state.c2_events.reduce((summary, event) => {
|
|
3196
3316
|
if (event.sync_state === 'synced') {
|
|
@@ -3622,10 +3742,11 @@ class ConnectV1Service {
|
|
|
3622
3742
|
process_signature_hash: processSignatureHash,
|
|
3623
3743
|
detected_at: input.existing?.detected_at ?? input.detectedAt,
|
|
3624
3744
|
last_seen_at: input.detectedAt,
|
|
3625
|
-
status: isConnectSelf ||
|
|
3745
|
+
status: isConnectSelf || existingPermanent ? 'connected' : 'detected',
|
|
3626
3746
|
persistence,
|
|
3627
3747
|
discovery_sources: [...new Set([...(input.existing?.discovery_sources ?? []), input.discoverySource])],
|
|
3628
3748
|
metadata: {
|
|
3749
|
+
...(input.existing?.metadata ?? {}),
|
|
3629
3750
|
executable_name: input.candidate.executable_name,
|
|
3630
3751
|
signature: input.candidate.signature,
|
|
3631
3752
|
persistence,
|
|
@@ -3642,12 +3763,29 @@ class ConnectV1Service {
|
|
|
3642
3763
|
};
|
|
3643
3764
|
}
|
|
3644
3765
|
queueAgentC2Event(agent, eventType, metadata = {}, occurredAt) {
|
|
3766
|
+
const passportGaid = typeof metadata.passport_gaid === 'string' && metadata.passport_gaid.trim()
|
|
3767
|
+
? metadata.passport_gaid.trim()
|
|
3768
|
+
: typeof agent.metadata?.passport_gaid === 'string' && agent.metadata.passport_gaid.trim()
|
|
3769
|
+
? agent.metadata.passport_gaid.trim()
|
|
3770
|
+
: null;
|
|
3771
|
+
const state = this.stateStore.readState();
|
|
3772
|
+
const workspaceId = typeof metadata.workspace_id === 'string' && metadata.workspace_id.trim()
|
|
3773
|
+
? metadata.workspace_id.trim()
|
|
3774
|
+
: typeof agent.metadata?.workspace_id === 'string' && agent.metadata.workspace_id.trim()
|
|
3775
|
+
? agent.metadata.workspace_id.trim()
|
|
3776
|
+
: state.workspace_binding.workspaceId;
|
|
3777
|
+
const projectId = typeof metadata.project_id === 'string' && metadata.project_id.trim()
|
|
3778
|
+
? metadata.project_id.trim()
|
|
3779
|
+
: typeof agent.metadata?.project_id === 'string' && agent.metadata.project_id.trim()
|
|
3780
|
+
? agent.metadata.project_id.trim()
|
|
3781
|
+
: state.project_binding.projectId;
|
|
3645
3782
|
const eventInput = {
|
|
3646
3783
|
eventType,
|
|
3784
|
+
passportGaid,
|
|
3647
3785
|
runtimeGaid: typeof metadata.runtime_gaid === 'string' ? metadata.runtime_gaid : null,
|
|
3648
3786
|
modelName: null,
|
|
3649
|
-
workspaceId
|
|
3650
|
-
projectId
|
|
3787
|
+
workspaceId,
|
|
3788
|
+
projectId,
|
|
3651
3789
|
metadata: {
|
|
3652
3790
|
agent_id: agent.agent_id,
|
|
3653
3791
|
agent_name: agent.agent_name,
|
|
@@ -3706,6 +3844,19 @@ class ConnectV1Service {
|
|
|
3706
3844
|
byId.set(nextAgent.agent_id, nextAgent);
|
|
3707
3845
|
if (existing) {
|
|
3708
3846
|
knownAgents += 1;
|
|
3847
|
+
if (existing.status === 'inactive' && nextAgent.status === 'connected') {
|
|
3848
|
+
this.stateStore.addEvidenceEvent({
|
|
3849
|
+
type: 'agent_connected',
|
|
3850
|
+
details: {
|
|
3851
|
+
agent_id: nextAgent.agent_id,
|
|
3852
|
+
agent_name: nextAgent.agent_name,
|
|
3853
|
+
source: 'process_reappeared',
|
|
3854
|
+
},
|
|
3855
|
+
});
|
|
3856
|
+
this.queueAgentC2Event(nextAgent, 'connect_agent_started', {
|
|
3857
|
+
observation: 'process_reappeared',
|
|
3858
|
+
}, detectedAt);
|
|
3859
|
+
}
|
|
3709
3860
|
}
|
|
3710
3861
|
else {
|
|
3711
3862
|
newAgents += 1;
|
|
@@ -3838,6 +3989,234 @@ class ConnectV1Service {
|
|
|
3838
3989
|
this.queueAgentC2Event(nextAgent, 'connect_agent_connected', {}, updatedAt);
|
|
3839
3990
|
return nextAgent;
|
|
3840
3991
|
}
|
|
3992
|
+
async resolveWebsiteAgentHandoff(handoffToken) {
|
|
3993
|
+
const token = String(handoffToken || '').trim();
|
|
3994
|
+
if (!token)
|
|
3995
|
+
throw new Error('AGENT_HANDOFF_INVALID');
|
|
3996
|
+
if (!this.hasSessionRef())
|
|
3997
|
+
throw new Error('AGENT_HANDOFF_SIGN_IN_REQUIRED');
|
|
3998
|
+
const result = await this.getApiClient(this.stateStore.readState()).resolveAgentHandoff(token);
|
|
3999
|
+
if (!result.ok || !isRecord(result.body)) {
|
|
4000
|
+
const code = this.extractBackendErrorCode(result.body);
|
|
4001
|
+
if (result.status === 401)
|
|
4002
|
+
throw new Error(code || 'AGENT_HANDOFF_SIGN_IN_REQUIRED');
|
|
4003
|
+
throw new Error(code || `AGENT_HANDOFF_RESOLVE_FAILED:${result.status}`);
|
|
4004
|
+
}
|
|
4005
|
+
const passport = isRecord(result.body.passport) ? result.body.passport : null;
|
|
4006
|
+
const protocol = normalizeDisplayText(result.body.protocol);
|
|
4007
|
+
const expiresAt = normalizeDisplayText(result.body.expiresAt);
|
|
4008
|
+
const gaid = normalizeDisplayText(passport?.gaid);
|
|
4009
|
+
const name = normalizeDisplayText(passport?.name);
|
|
4010
|
+
const passportType = normalizeDisplayText(passport?.passportType)?.toLowerCase();
|
|
4011
|
+
if (protocol !== 'agent-handoff.v1' || !expiresAt || !gaid || !name || passportType !== 'agent') {
|
|
4012
|
+
throw new Error('AGENT_HANDOFF_RESPONSE_INVALID');
|
|
4013
|
+
}
|
|
4014
|
+
return {
|
|
4015
|
+
protocol: 'agent-handoff.v1',
|
|
4016
|
+
expiresAt,
|
|
4017
|
+
passport: {
|
|
4018
|
+
gaid,
|
|
4019
|
+
name,
|
|
4020
|
+
passportType: 'agent',
|
|
4021
|
+
workspaceId: normalizeDisplayText(passport?.workspaceId),
|
|
4022
|
+
projectId: normalizeDisplayText(passport?.projectId),
|
|
4023
|
+
},
|
|
4024
|
+
};
|
|
4025
|
+
}
|
|
4026
|
+
async syncWebsiteAgentConnectionEvent(gaid, apiKey) {
|
|
4027
|
+
const state = this.stateStore.readState();
|
|
4028
|
+
const event = [...state.c2_events]
|
|
4029
|
+
.reverse()
|
|
4030
|
+
.find((item) => item.passport_gaid === gaid && item.event_type === 'connect_agent_connection_verified');
|
|
4031
|
+
if (!event)
|
|
4032
|
+
return { confirmed: false, pending: false };
|
|
4033
|
+
if (event.sync_state === 'synced')
|
|
4034
|
+
return { confirmed: true, pending: false };
|
|
4035
|
+
try {
|
|
4036
|
+
const result = await this.getApiClientWithSessionToken(apiKey).pushRuntimeSignalEvent(this.buildC2RuntimeSignalPayload(event, apiKey, null));
|
|
4037
|
+
if (!result.ok) {
|
|
4038
|
+
const error = `c2_sync_${result.status}`;
|
|
4039
|
+
if (result.status === 429 || (result.status >= 500 && result.status <= 599)) {
|
|
4040
|
+
this.stateStore.markC2EventRetryable(event.event_id, error);
|
|
4041
|
+
if (result.status >= 500) {
|
|
4042
|
+
this.recordC2ConnectivityUnavailable(event, error);
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
else {
|
|
4046
|
+
this.stateStore.markC2EventFailed(event.event_id, error);
|
|
4047
|
+
}
|
|
4048
|
+
return { confirmed: false, pending: true };
|
|
4049
|
+
}
|
|
4050
|
+
this.stateStore.markC2EventSynced(event.event_id);
|
|
4051
|
+
this.stateStore.addEvidenceEvent({
|
|
4052
|
+
type: 'runtimeSignal_synced',
|
|
4053
|
+
details: {
|
|
4054
|
+
eventId: event.event_id,
|
|
4055
|
+
eventType: event.event_type,
|
|
4056
|
+
source: 'website_agent_handoff',
|
|
4057
|
+
status: result.status,
|
|
4058
|
+
gaid,
|
|
4059
|
+
},
|
|
4060
|
+
});
|
|
4061
|
+
return { confirmed: true, pending: false };
|
|
4062
|
+
}
|
|
4063
|
+
catch (error) {
|
|
4064
|
+
this.stateStore.markC2EventRetryable(event.event_id, error instanceof Error ? error.message : 'c2_sync_failed');
|
|
4065
|
+
this.recordC2ConnectivityUnavailable(event, 'c2_network_unavailable');
|
|
4066
|
+
return { confirmed: false, pending: true };
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
async approveWebsiteAgentHandoff(input) {
|
|
4070
|
+
const handoff = await this.resolveWebsiteAgentHandoff(input.handoffToken);
|
|
4071
|
+
const state = this.stateStore.readState();
|
|
4072
|
+
const agent = this.findAgentByIdOrName(state, input.agentSelector);
|
|
4073
|
+
if (!agent || agent.metadata?.signature === 'forkit-connect-desktop') {
|
|
4074
|
+
throw new Error('AGENT_NOT_FOUND');
|
|
4075
|
+
}
|
|
4076
|
+
const existingGaid = normalizeDisplayText(agent.metadata?.passport_gaid);
|
|
4077
|
+
if (existingGaid && existingGaid !== handoff.passport.gaid) {
|
|
4078
|
+
throw new Error('AGENT_ALREADY_CONNECTED_TO_ANOTHER_PASSPORT');
|
|
4079
|
+
}
|
|
4080
|
+
const alreadyConnected = existingGaid === handoff.passport.gaid && this.hasRuntimeSignalKey(handoff.passport.gaid);
|
|
4081
|
+
let rawKey = alreadyConnected
|
|
4082
|
+
? this.credentialStore.getRuntimeSignalApiKey(handoff.passport.gaid)
|
|
4083
|
+
: null;
|
|
4084
|
+
let keyRef = normalizeDisplayText(agent.metadata?.runtime_signal_key_ref);
|
|
4085
|
+
if (!alreadyConnected) {
|
|
4086
|
+
const secureStorage = this.getCredentialStoreStatus();
|
|
4087
|
+
if (!secureStorage.available || secureStorage.plaintextFallbackActive) {
|
|
4088
|
+
this.recordAgentConnectionFault({
|
|
4089
|
+
eventType: 'connect_secure_storage_failed',
|
|
4090
|
+
agent,
|
|
4091
|
+
passportGaid: handoff.passport.gaid,
|
|
4092
|
+
workspaceId: handoff.passport.workspaceId,
|
|
4093
|
+
projectId: handoff.passport.projectId,
|
|
4094
|
+
metadata: {
|
|
4095
|
+
failure_stage: 'preflight',
|
|
4096
|
+
reason_code: secureStorage.plaintextFallbackActive
|
|
4097
|
+
? 'plaintext_fallback_not_allowed'
|
|
4098
|
+
: 'secure_storage_unavailable',
|
|
4099
|
+
storage_backend: secureStorage.backend,
|
|
4100
|
+
credential_persisted: false,
|
|
4101
|
+
},
|
|
4102
|
+
});
|
|
4103
|
+
throw new Error('SECURE_STORAGE_REQUIRED');
|
|
4104
|
+
}
|
|
4105
|
+
const keyResult = await this.getApiClient(this.stateStore.readState()).createPassportApiKey(handoff.passport.gaid, `Forkit Connect · ${agent.agent_name}`.slice(0, 100));
|
|
4106
|
+
if (!keyResult.ok || !isRecord(keyResult.body)) {
|
|
4107
|
+
throw new Error(this.extractBackendErrorCode(keyResult.body) || `AGENT_KEY_PROVISION_FAILED:${keyResult.status}`);
|
|
4108
|
+
}
|
|
4109
|
+
rawKey = normalizeDisplayText(keyResult.body.rawKey);
|
|
4110
|
+
const keyId = normalizeDisplayText(keyResult.body.keyId);
|
|
4111
|
+
if (!rawKey || !keyId)
|
|
4112
|
+
throw new Error('AGENT_KEY_RESPONSE_INVALID');
|
|
4113
|
+
try {
|
|
4114
|
+
const credential = this.setRuntimeSignalApiKeyForGaid(handoff.passport.gaid, rawKey);
|
|
4115
|
+
keyRef = credential?.credentialRef ?? null;
|
|
4116
|
+
}
|
|
4117
|
+
catch {
|
|
4118
|
+
let registryKeyRevocationConfirmed = false;
|
|
4119
|
+
try {
|
|
4120
|
+
const revocation = await this.getApiClient(this.stateStore.readState()).revokePassportApiKey(handoff.passport.gaid, keyId);
|
|
4121
|
+
registryKeyRevocationConfirmed = revocation.ok;
|
|
4122
|
+
}
|
|
4123
|
+
catch {
|
|
4124
|
+
// The original secure-storage failure is the actionable local error.
|
|
4125
|
+
// Registry-side key expiry/revocation remains independently auditable.
|
|
4126
|
+
}
|
|
4127
|
+
this.recordAgentConnectionFault({
|
|
4128
|
+
eventType: 'connect_secure_storage_failed',
|
|
4129
|
+
agent,
|
|
4130
|
+
passportGaid: handoff.passport.gaid,
|
|
4131
|
+
workspaceId: handoff.passport.workspaceId,
|
|
4132
|
+
projectId: handoff.passport.projectId,
|
|
4133
|
+
metadata: {
|
|
4134
|
+
failure_stage: 'credential_write',
|
|
4135
|
+
reason_code: 'secure_storage_write_failed',
|
|
4136
|
+
storage_backend: secureStorage.backend,
|
|
4137
|
+
credential_persisted: false,
|
|
4138
|
+
registry_key_revocation_confirmed: registryKeyRevocationConfirmed,
|
|
4139
|
+
},
|
|
4140
|
+
});
|
|
4141
|
+
throw new Error('SECURE_STORAGE_WRITE_FAILED');
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
const approvedAt = nowIso();
|
|
4145
|
+
const agentSignature = normalizeDisplayText(agent.metadata?.signature)?.toLowerCase() ?? null;
|
|
4146
|
+
const activityAdapter = agentSignature ? AGENT_ACTIVITY_ADAPTER_BY_SIGNATURE[agentSignature] ?? null : null;
|
|
4147
|
+
const connectedAgent = {
|
|
4148
|
+
...agent,
|
|
4149
|
+
status: 'connected',
|
|
4150
|
+
persistence: 'permanent',
|
|
4151
|
+
last_seen_at: approvedAt,
|
|
4152
|
+
discovery_sources: [...new Set([...agent.discovery_sources, 'manual_connect'])],
|
|
4153
|
+
metadata: {
|
|
4154
|
+
...agent.metadata,
|
|
4155
|
+
passport_gaid: handoff.passport.gaid,
|
|
4156
|
+
passport_name: handoff.passport.name,
|
|
4157
|
+
workspace_id: handoff.passport.workspaceId,
|
|
4158
|
+
project_id: handoff.passport.projectId,
|
|
4159
|
+
tracking_status: 'connected',
|
|
4160
|
+
activity_adapter: activityAdapter,
|
|
4161
|
+
activity_observation_level: activityAdapter ? 'detailed' : 'presence_only',
|
|
4162
|
+
website_handoff_approved_at: approvedAt,
|
|
4163
|
+
runtime_signal_key_ref: keyRef,
|
|
4164
|
+
},
|
|
4165
|
+
};
|
|
4166
|
+
this.stateStore.upsertDetectedAgent(connectedAgent);
|
|
4167
|
+
let verificationEvent = [...this.stateStore.readState().c2_events]
|
|
4168
|
+
.reverse()
|
|
4169
|
+
.find((item) => item.passport_gaid === handoff.passport.gaid && item.event_type === 'connect_agent_connection_verified');
|
|
4170
|
+
if (!verificationEvent) {
|
|
4171
|
+
verificationEvent = this.recordC2LifecycleEvent({
|
|
4172
|
+
eventType: 'connect_agent_connection_verified',
|
|
4173
|
+
occurredAt: approvedAt,
|
|
4174
|
+
passportGaid: handoff.passport.gaid,
|
|
4175
|
+
workspaceId: handoff.passport.workspaceId,
|
|
4176
|
+
projectId: handoff.passport.projectId,
|
|
4177
|
+
metadata: {
|
|
4178
|
+
agent_id: connectedAgent.agent_id,
|
|
4179
|
+
agent_name: connectedAgent.agent_name,
|
|
4180
|
+
agent_type: connectedAgent.agent_type,
|
|
4181
|
+
process_signature_hash: connectedAgent.process_signature_hash,
|
|
4182
|
+
source: 'website_agent_handoff',
|
|
4183
|
+
safe_metadata_only: true,
|
|
4184
|
+
},
|
|
4185
|
+
});
|
|
4186
|
+
this.stateStore.addEvidenceEvent({
|
|
4187
|
+
type: 'agent_connected',
|
|
4188
|
+
details: {
|
|
4189
|
+
agent_id: connectedAgent.agent_id,
|
|
4190
|
+
agent_name: connectedAgent.agent_name,
|
|
4191
|
+
gaid: handoff.passport.gaid,
|
|
4192
|
+
source: 'website_agent_handoff',
|
|
4193
|
+
},
|
|
4194
|
+
});
|
|
4195
|
+
}
|
|
4196
|
+
if (!activityAdapter) {
|
|
4197
|
+
this.recordAgentConnectionFault({
|
|
4198
|
+
eventType: 'connect_agent_adapter_unavailable',
|
|
4199
|
+
agent: connectedAgent,
|
|
4200
|
+
passportGaid: handoff.passport.gaid,
|
|
4201
|
+
workspaceId: handoff.passport.workspaceId,
|
|
4202
|
+
projectId: handoff.passport.projectId,
|
|
4203
|
+
metadata: {
|
|
4204
|
+
adapter_status: 'unavailable',
|
|
4205
|
+
reason_code: 'no_supported_activity_adapter',
|
|
4206
|
+
observation_level: 'presence_only',
|
|
4207
|
+
detected_signature: agentSignature ?? 'unknown',
|
|
4208
|
+
},
|
|
4209
|
+
});
|
|
4210
|
+
}
|
|
4211
|
+
const sync = await this.syncWebsiteAgentConnectionEvent(handoff.passport.gaid, rawKey);
|
|
4212
|
+
return {
|
|
4213
|
+
action: alreadyConnected ? 'already_connected' : 'connected',
|
|
4214
|
+
agent: connectedAgent,
|
|
4215
|
+
passport: handoff.passport,
|
|
4216
|
+
activityConfirmed: sync.confirmed,
|
|
4217
|
+
syncPending: sync.pending,
|
|
4218
|
+
};
|
|
4219
|
+
}
|
|
3841
4220
|
async registerObservedAgent(input) {
|
|
3842
4221
|
const candidate = this.buildObservedAgentCandidate(input);
|
|
3843
4222
|
const scanResult = await this.scanAgents({
|
|
@@ -6921,34 +7300,35 @@ class ConnectV1Service {
|
|
|
6921
7300
|
return nextSession;
|
|
6922
7301
|
}
|
|
6923
7302
|
buildC2RuntimeSignalPayload(event, apiKey, scope) {
|
|
7303
|
+
const metadata = sanitizeC2RuntimeSignalMetadata({
|
|
7304
|
+
client_event_id: event.event_id, // traceable but excluded from server idempotency hash
|
|
7305
|
+
runtime_gaid: event.runtime_gaid,
|
|
7306
|
+
runtime_name: event.runtime_name,
|
|
7307
|
+
runtime_type: event.runtime_type,
|
|
7308
|
+
model_name: event.model_name,
|
|
7309
|
+
discoveryHash: event.discovery_hash,
|
|
7310
|
+
registrationKey: event.registration_key,
|
|
7311
|
+
passportGaid: event.passport_gaid,
|
|
7312
|
+
pulse_status: event.pulse_status,
|
|
7313
|
+
connection_classification: event.connection_classification,
|
|
7314
|
+
daemon_running: event.daemon_running,
|
|
7315
|
+
shadow_candidate_reason: event.shadow_candidate_reason,
|
|
7316
|
+
...event.metadata,
|
|
7317
|
+
binding_id: scope?.bindingId ?? null,
|
|
7318
|
+
connect_device_id: scope?.connectDeviceId ?? null,
|
|
7319
|
+
workspaceId: scope?.workspaceId ?? event.workspace_id ?? null,
|
|
7320
|
+
projectId: scope?.projectId ?? event.project_id ?? null,
|
|
7321
|
+
workspace_id: scope?.workspaceId ?? event.workspace_id ?? null,
|
|
7322
|
+
project_id: scope?.projectId ?? event.project_id ?? null,
|
|
7323
|
+
evidence_type: 'observed_runtime_session',
|
|
7324
|
+
});
|
|
6924
7325
|
return {
|
|
6925
7326
|
gaid: event.passport_gaid,
|
|
6926
7327
|
apiKey,
|
|
6927
7328
|
binding_id: scope?.bindingId ?? null,
|
|
6928
7329
|
eventType: event.event_type,
|
|
6929
7330
|
timestamp: event.occurred_at,
|
|
6930
|
-
metadata
|
|
6931
|
-
client_event_id: event.event_id, // traceable but excluded from server idempotency hash
|
|
6932
|
-
runtime_gaid: event.runtime_gaid,
|
|
6933
|
-
runtime_name: event.runtime_name,
|
|
6934
|
-
runtime_type: event.runtime_type,
|
|
6935
|
-
model_name: event.model_name,
|
|
6936
|
-
discoveryHash: event.discovery_hash,
|
|
6937
|
-
registrationKey: event.registration_key,
|
|
6938
|
-
passportGaid: event.passport_gaid,
|
|
6939
|
-
pulse_status: event.pulse_status,
|
|
6940
|
-
connection_classification: event.connection_classification,
|
|
6941
|
-
daemon_running: event.daemon_running,
|
|
6942
|
-
shadow_candidate_reason: event.shadow_candidate_reason,
|
|
6943
|
-
...event.metadata,
|
|
6944
|
-
binding_id: scope?.bindingId ?? null,
|
|
6945
|
-
connect_device_id: scope?.connectDeviceId ?? null,
|
|
6946
|
-
workspaceId: scope?.workspaceId ?? event.workspace_id ?? null,
|
|
6947
|
-
projectId: scope?.projectId ?? event.project_id ?? null,
|
|
6948
|
-
workspace_id: scope?.workspaceId ?? event.workspace_id ?? null,
|
|
6949
|
-
project_id: scope?.projectId ?? event.project_id ?? null,
|
|
6950
|
-
evidence_type: 'observed_runtime_session',
|
|
6951
|
-
},
|
|
7331
|
+
metadata,
|
|
6952
7332
|
};
|
|
6953
7333
|
}
|
|
6954
7334
|
runtimeRecommendedAction(runtimePassport) {
|
|
@@ -7087,6 +7467,7 @@ class ConnectV1Service {
|
|
|
7087
7467
|
let succeeded = 0;
|
|
7088
7468
|
let failed = 0;
|
|
7089
7469
|
let firstError = null;
|
|
7470
|
+
const connectivityFailure = { value: null };
|
|
7090
7471
|
// When the server signals rate-limit (429) we record the retry-after deadline and
|
|
7091
7472
|
// stop sending for this cycle to avoid burning through the remaining budget.
|
|
7092
7473
|
let rateLimitedUntilMs = null;
|
|
@@ -7154,7 +7535,13 @@ class ConnectV1Service {
|
|
|
7154
7535
|
source: 'runtimeSignal',
|
|
7155
7536
|
});
|
|
7156
7537
|
const error = `c2_sync_${result.status}`;
|
|
7157
|
-
|
|
7538
|
+
if (result.status >= 500 && result.status <= 599) {
|
|
7539
|
+
this.stateStore.markC2EventRetryable(event.event_id, error);
|
|
7540
|
+
connectivityFailure.value ??= { event, reasonCode: error };
|
|
7541
|
+
}
|
|
7542
|
+
else {
|
|
7543
|
+
this.stateStore.markC2EventFailed(event.event_id, error);
|
|
7544
|
+
}
|
|
7158
7545
|
firstError ??= error;
|
|
7159
7546
|
failed += 1;
|
|
7160
7547
|
if (halt) {
|
|
@@ -7177,7 +7564,8 @@ class ConnectV1Service {
|
|
|
7177
7564
|
}
|
|
7178
7565
|
catch (error) {
|
|
7179
7566
|
const message = error instanceof Error ? error.message : 'c2_sync_failed';
|
|
7180
|
-
this.stateStore.
|
|
7567
|
+
this.stateStore.markC2EventRetryable(event.event_id, message);
|
|
7568
|
+
connectivityFailure.value ??= { event, reasonCode: 'c2_network_unavailable' };
|
|
7181
7569
|
firstError ??= message;
|
|
7182
7570
|
failed += 1;
|
|
7183
7571
|
}
|
|
@@ -7228,6 +7616,35 @@ class ConnectV1Service {
|
|
|
7228
7616
|
const lastSyncAt = attempted > 0 ? nowIso() : state.last_c2_sync_at;
|
|
7229
7617
|
const lastSyncError = firstError;
|
|
7230
7618
|
this.stateStore.updateC2SyncStatus(lastSyncAt, lastSyncError);
|
|
7619
|
+
const connectivityState = this.stateStore.readState();
|
|
7620
|
+
if (connectivityFailure.value && !connectivityState.c2_offline_since) {
|
|
7621
|
+
const failure = connectivityFailure.value;
|
|
7622
|
+
this.recordC2ConnectivityUnavailable(failure.event, failure.reasonCode);
|
|
7623
|
+
}
|
|
7624
|
+
else if (!connectivityFailure.value && connectivityState.c2_offline_since && succeeded > 0) {
|
|
7625
|
+
const restoredAt = nowIso();
|
|
7626
|
+
const offlineSince = connectivityState.c2_offline_since;
|
|
7627
|
+
const durationSeconds = Math.max(0, Math.round((Date.parse(restoredAt) - Date.parse(offlineSince)) / 1000));
|
|
7628
|
+
const offlineEvent = [...connectivityState.c2_events]
|
|
7629
|
+
.reverse()
|
|
7630
|
+
.find((event) => event.event_type === 'connect_sync_offline');
|
|
7631
|
+
const fallbackContext = this.getOperationalPassportContext(connectivityState);
|
|
7632
|
+
this.stateStore.updateC2ConnectivityStatus(null, restoredAt);
|
|
7633
|
+
this.recordC2LifecycleEvent({
|
|
7634
|
+
eventType: 'connect_sync_restored',
|
|
7635
|
+
occurredAt: restoredAt,
|
|
7636
|
+
passportGaid: offlineEvent?.passport_gaid ?? fallbackContext.passportGaid,
|
|
7637
|
+
workspaceId: offlineEvent?.workspace_id ?? fallbackContext.workspaceId,
|
|
7638
|
+
projectId: offlineEvent?.project_id ?? fallbackContext.projectId,
|
|
7639
|
+
metadata: {
|
|
7640
|
+
period_kind: 'connectivity_unavailable',
|
|
7641
|
+
offline_since: offlineSince,
|
|
7642
|
+
restored_at: restoredAt,
|
|
7643
|
+
duration_seconds: durationSeconds,
|
|
7644
|
+
evidence: 'successful_sync',
|
|
7645
|
+
},
|
|
7646
|
+
});
|
|
7647
|
+
}
|
|
7231
7648
|
const finalState = this.stateStore.readState();
|
|
7232
7649
|
const finalSummary = this.getC2EventSyncSummary(finalState);
|
|
7233
7650
|
return {
|
|
@@ -7507,7 +7924,12 @@ class ConnectV1Service {
|
|
|
7507
7924
|
const state = this.stateStore.readState();
|
|
7508
7925
|
const daemonStatus = state.daemon_status;
|
|
7509
7926
|
if (daemonStatus.daemon_running && daemonStatus.daemon_pid && !this.isProcessAlive(daemonStatus.daemon_pid)) {
|
|
7510
|
-
return this.markDaemonStopped(daemonStatus.last_error ?? 'daemon_process_missing'
|
|
7927
|
+
return this.markDaemonStopped(daemonStatus.last_error ?? 'daemon_process_missing', {
|
|
7928
|
+
unclean: true,
|
|
7929
|
+
detectedAt: nowIso(),
|
|
7930
|
+
previousStartedAt: daemonStatus.started_at,
|
|
7931
|
+
previousLastScanAt: daemonStatus.last_scan_at,
|
|
7932
|
+
});
|
|
7511
7933
|
}
|
|
7512
7934
|
return daemonStatus;
|
|
7513
7935
|
}
|
|
@@ -7519,13 +7941,13 @@ class ConnectV1Service {
|
|
|
7519
7941
|
started_at: nowIso(),
|
|
7520
7942
|
last_error: null,
|
|
7521
7943
|
}).daemon_status;
|
|
7522
|
-
const
|
|
7944
|
+
const context = this.getOperationalPassportContext(state);
|
|
7523
7945
|
this.recordC2LifecycleEvent({
|
|
7524
7946
|
eventType: 'connect_daemon_started',
|
|
7525
7947
|
occurredAt: nextStatus.started_at ?? nowIso(),
|
|
7526
|
-
passportGaid:
|
|
7527
|
-
workspaceId:
|
|
7528
|
-
projectId:
|
|
7948
|
+
passportGaid: context.passportGaid,
|
|
7949
|
+
workspaceId: context.workspaceId,
|
|
7950
|
+
projectId: context.projectId,
|
|
7529
7951
|
daemonRunning: true,
|
|
7530
7952
|
metadata: {
|
|
7531
7953
|
daemon_pid: pid,
|
|
@@ -7533,24 +7955,48 @@ class ConnectV1Service {
|
|
|
7533
7955
|
});
|
|
7534
7956
|
return nextStatus;
|
|
7535
7957
|
}
|
|
7536
|
-
markDaemonStopped(lastError) {
|
|
7958
|
+
markDaemonStopped(lastError, options) {
|
|
7537
7959
|
const state = this.stateStore.readState();
|
|
7960
|
+
const detectedAt = options?.detectedAt ?? nowIso();
|
|
7538
7961
|
const nextStatus = this.stateStore.updateDaemonStatus({
|
|
7539
7962
|
daemon_running: false,
|
|
7540
7963
|
daemon_pid: null,
|
|
7541
7964
|
last_error: lastError,
|
|
7542
7965
|
}).daemon_status;
|
|
7543
|
-
const
|
|
7966
|
+
const context = this.getOperationalPassportContext(state);
|
|
7544
7967
|
this.recordC2LifecycleEvent({
|
|
7545
7968
|
eventType: 'connect_daemon_stopped',
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7969
|
+
occurredAt: detectedAt,
|
|
7970
|
+
passportGaid: context.passportGaid,
|
|
7971
|
+
workspaceId: context.workspaceId,
|
|
7972
|
+
projectId: context.projectId,
|
|
7549
7973
|
daemonRunning: false,
|
|
7550
7974
|
metadata: {
|
|
7551
7975
|
last_error: lastError,
|
|
7976
|
+
stop_classification: options?.unclean ? 'unclean' : 'clean',
|
|
7552
7977
|
},
|
|
7553
7978
|
});
|
|
7979
|
+
if (options?.unclean) {
|
|
7980
|
+
const gapStartedAt = options.previousLastScanAt ?? options.previousStartedAt ?? detectedAt;
|
|
7981
|
+
const parsedDuration = Math.round((Date.parse(detectedAt) - Date.parse(gapStartedAt)) / 1000);
|
|
7982
|
+
this.recordC2LifecycleEvent({
|
|
7983
|
+
eventType: 'connect_activity_gap_detected',
|
|
7984
|
+
occurredAt: detectedAt,
|
|
7985
|
+
passportGaid: context.passportGaid,
|
|
7986
|
+
workspaceId: context.workspaceId,
|
|
7987
|
+
projectId: context.projectId,
|
|
7988
|
+
daemonRunning: false,
|
|
7989
|
+
metadata: {
|
|
7990
|
+
gap_kind: 'unclean_shutdown',
|
|
7991
|
+
gap_started_at: gapStartedAt,
|
|
7992
|
+
gap_detected_at: detectedAt,
|
|
7993
|
+
gap_duration_seconds: Number.isFinite(parsedDuration) ? Math.max(0, parsedDuration) : 0,
|
|
7994
|
+
confidence: 'inferred',
|
|
7995
|
+
reason_code: lastError ?? 'daemon_process_missing',
|
|
7996
|
+
data_loss_confirmed: false,
|
|
7997
|
+
},
|
|
7998
|
+
});
|
|
7999
|
+
}
|
|
7554
8000
|
return nextStatus;
|
|
7555
8001
|
}
|
|
7556
8002
|
recordDaemonScan(summary) {
|
|
@@ -8934,6 +9380,26 @@ class ConnectV1Service {
|
|
|
8934
9380
|
queue_limit: MAX_RUNTIME_SIGNAL_QUEUE_ITEMS,
|
|
8935
9381
|
},
|
|
8936
9382
|
});
|
|
9383
|
+
if (trimmed > 0) {
|
|
9384
|
+
const overflowAlreadyPending = this.stateStore.readState().c2_events.some((event) => (event.event_type === 'connect_queue_overflow'
|
|
9385
|
+
&& event.passport_gaid === gaid
|
|
9386
|
+
&& event.sync_state === 'pending'
|
|
9387
|
+
&& event.metadata?.overflow_kind === 'runtime_signal_local_queue_capacity'));
|
|
9388
|
+
if (!overflowAlreadyPending) {
|
|
9389
|
+
this.recordC2LifecycleEvent({
|
|
9390
|
+
eventType: 'connect_queue_overflow',
|
|
9391
|
+
passportGaid: gaid,
|
|
9392
|
+
workspaceId: runtimeScope?.workspaceId ?? state.workspace_binding.workspaceId,
|
|
9393
|
+
projectId: runtimeScope?.projectId ?? state.project_binding.projectId,
|
|
9394
|
+
metadata: {
|
|
9395
|
+
overflow_kind: 'runtime_signal_local_queue_capacity',
|
|
9396
|
+
dropped_event_count: trimmed,
|
|
9397
|
+
queue_limit: MAX_RUNTIME_SIGNAL_QUEUE_ITEMS,
|
|
9398
|
+
data_loss_confirmed: true,
|
|
9399
|
+
},
|
|
9400
|
+
});
|
|
9401
|
+
}
|
|
9402
|
+
}
|
|
8937
9403
|
return queueItem.id;
|
|
8938
9404
|
}
|
|
8939
9405
|
queueConfiguredHeartbeatRuntimeSignal(gaid) {
|
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;
|