@ait-co/devtools 0.1.56 → 0.1.57
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/README.en.md +26 -32
- package/README.md +26 -32
- package/dist/mcp/cli.js +564 -341
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +16 -10
- package/dist/mcp/server.js.map +1 -1
- package/dist/panel/index.js +2 -2
- package/dist/relay-secret-store-DnTNl-9z.cjs +140 -0
- package/dist/relay-secret-store-DnTNl-9z.cjs.map +1 -0
- package/dist/relay-secret-store-DqyUoeXy.js +140 -0
- package/dist/relay-secret-store-DqyUoeXy.js.map +1 -0
- package/dist/{totp-CxHsagqY.js → totp-BkP5yU2K.js} +4 -2
- package/dist/totp-BkP5yU2K.js.map +1 -0
- package/dist/totp-CQFmgOhM.js +3 -0
- package/dist/totp-D0a8VwoR.js +187 -0
- package/dist/totp-D0a8VwoR.js.map +1 -0
- package/dist/{totp-BkKP4m8H.cjs → totp-DLgGbySX.cjs} +4 -1
- package/dist/totp-DLgGbySX.cjs.map +1 -0
- package/dist/{tunnel-Cj8g1LIL.js → tunnel-CI61NvPI.js} +2 -2
- package/dist/{tunnel-Cj8g1LIL.js.map → tunnel-CI61NvPI.js.map} +1 -1
- package/dist/{tunnel-p-q6eVWT.cjs → tunnel-nKYPtc-g.cjs} +2 -2
- package/dist/{tunnel-p-q6eVWT.cjs.map → tunnel-nKYPtc-g.cjs.map} +1 -1
- package/dist/unplugin/index.cjs +4 -2
- package/dist/unplugin/index.cjs.map +1 -1
- package/dist/unplugin/index.d.cts.map +1 -1
- package/dist/unplugin/index.d.ts.map +1 -1
- package/dist/unplugin/index.js +4 -2
- package/dist/unplugin/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/totp-BkKP4m8H.cjs.map +0 -1
- package/dist/totp-CxHsagqY.js.map +0 -1
package/dist/mcp/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { i as generateTotp, n as assertRelayAuthConfigured, r as buildRelayVerifyAuth } from "../totp-D0a8VwoR.js";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
4
|
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
5
|
import { argv } from "node:process";
|
|
@@ -12,8 +13,8 @@ import { createServer } from "node:http";
|
|
|
12
13
|
import { spawn } from "node:child_process";
|
|
13
14
|
import net from "node:net";
|
|
14
15
|
import { homedir, platform } from "node:os";
|
|
15
|
-
import { join } from "node:path";
|
|
16
|
-
import {
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import { randomBytes } from "node:crypto";
|
|
17
18
|
import { Tunnel, bin, install } from "cloudflared";
|
|
18
19
|
//#region \0rolldown/runtime.js
|
|
19
20
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
@@ -1582,12 +1583,71 @@ async function launchChromium(options = {}) {
|
|
|
1582
1583
|
/**
|
|
1583
1584
|
* 로컬 HTTP 서버를 127.0.0.1 random port(또는 `AIT_DEBUG_HTTP_PORT` env)로 시작한다.
|
|
1584
1585
|
* MCP debug server 생애주기에 묶어 사용 — `runDebugServer` shutdown 시 `close()`로 정리.
|
|
1586
|
+
*
|
|
1587
|
+
* @param getDashboardState - dashboard 상태를 반환하는 클로저. 주입 시 `GET /` dashboard와
|
|
1588
|
+
* `GET /events` SSE 스트림이 활성화된다. 미주입 시 두 라우트는 204/서비스 없음으로 응답.
|
|
1585
1589
|
*/
|
|
1586
|
-
async function startQrHttpServer() {
|
|
1590
|
+
async function startQrHttpServer(getDashboardState) {
|
|
1587
1591
|
const { default: QRCode } = await import("qrcode");
|
|
1588
|
-
|
|
1592
|
+
/** SSE 활성 연결 목록 — `notifyStateChange()` 시 전체 push. */
|
|
1593
|
+
const sseClients = [];
|
|
1594
|
+
/** SSE 연결 하나에 상태 이벤트를 flush한다. */
|
|
1595
|
+
function pushStateToClient(res, state) {
|
|
1596
|
+
const payload = JSON.stringify({
|
|
1597
|
+
tunnel: {
|
|
1598
|
+
up: state.tunnel.up,
|
|
1599
|
+
wssUrl: state.tunnel.wssUrl
|
|
1600
|
+
},
|
|
1601
|
+
pages: state.pages,
|
|
1602
|
+
attachUrl: state.attachUrl
|
|
1603
|
+
});
|
|
1604
|
+
res.write(`data: ${payload}\n\n`);
|
|
1605
|
+
}
|
|
1606
|
+
const server = createServer(async (req, res) => {
|
|
1589
1607
|
const [path, query = ""] = (req.url ?? "/").split("?", 2);
|
|
1590
1608
|
const params = new URLSearchParams(query ?? "");
|
|
1609
|
+
if (path === "/") {
|
|
1610
|
+
if (!getDashboardState) {
|
|
1611
|
+
res.writeHead(204, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1612
|
+
res.end();
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
const state = getDashboardState();
|
|
1616
|
+
let qrDataUrl = null;
|
|
1617
|
+
if (state.attachUrl) try {
|
|
1618
|
+
qrDataUrl = await QRCode.toDataURL(state.attachUrl, {
|
|
1619
|
+
type: "image/png",
|
|
1620
|
+
errorCorrectionLevel: "M"
|
|
1621
|
+
});
|
|
1622
|
+
} catch {}
|
|
1623
|
+
const html = buildDashboardHtml(state, qrDataUrl);
|
|
1624
|
+
res.writeHead(200, {
|
|
1625
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1626
|
+
"Cache-Control": "no-store"
|
|
1627
|
+
});
|
|
1628
|
+
res.end(html);
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
if (path === "/events") {
|
|
1632
|
+
if (!getDashboardState) {
|
|
1633
|
+
res.writeHead(204, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1634
|
+
res.end();
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
res.writeHead(200, {
|
|
1638
|
+
"Content-Type": "text/event-stream",
|
|
1639
|
+
"Cache-Control": "no-cache",
|
|
1640
|
+
Connection: "keep-alive",
|
|
1641
|
+
"X-Accel-Buffering": "no"
|
|
1642
|
+
});
|
|
1643
|
+
pushStateToClient(res, getDashboardState());
|
|
1644
|
+
sseClients.push(res);
|
|
1645
|
+
req.once("close", () => {
|
|
1646
|
+
const idx = sseClients.indexOf(res);
|
|
1647
|
+
if (idx !== -1) sseClients.splice(idx, 1);
|
|
1648
|
+
});
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1591
1651
|
if (path === "/attach") {
|
|
1592
1652
|
const encodedU = params.get("u") ?? "";
|
|
1593
1653
|
let attachUrl;
|
|
@@ -1661,6 +1721,13 @@ async function startQrHttpServer() {
|
|
|
1661
1721
|
buildAttachPageUrl(attachUrl) {
|
|
1662
1722
|
return `http://127.0.0.1:${port}/attach?u=${encodeURIComponent(attachUrl)}`;
|
|
1663
1723
|
},
|
|
1724
|
+
notifyStateChange() {
|
|
1725
|
+
if (!getDashboardState) return;
|
|
1726
|
+
const state = getDashboardState();
|
|
1727
|
+
for (const client of sseClients) try {
|
|
1728
|
+
pushStateToClient(client, state);
|
|
1729
|
+
} catch {}
|
|
1730
|
+
},
|
|
1664
1731
|
close() {
|
|
1665
1732
|
return new Promise((resolve, reject) => {
|
|
1666
1733
|
server.close((err) => err ? reject(err) : resolve());
|
|
@@ -1669,6 +1736,147 @@ async function startQrHttpServer() {
|
|
|
1669
1736
|
};
|
|
1670
1737
|
}
|
|
1671
1738
|
/**
|
|
1739
|
+
* Dashboard HTML — 터널/page/attachUrl 상태를 표시하고 SSE로 자동 갱신.
|
|
1740
|
+
*
|
|
1741
|
+
* SECRET-HANDLING:
|
|
1742
|
+
* - attachUrl은 url-box 안에서만 노출 (TOTP at= 코드 캡슐 그대로).
|
|
1743
|
+
* - tunnel wssUrl은 "터널 연결됨" 상태 표시에서 HOST가 아닌 UP/DOWN만 노출.
|
|
1744
|
+
* wssUrl 값 자체는 dashboard HTML에 넣지 않는다 — 브라우저 탭이 보안 경계 밖에 있음.
|
|
1745
|
+
* - inline <script>로 /events SSE 구독 — 빌드 파이프라인 추가 없음.
|
|
1746
|
+
*/
|
|
1747
|
+
function buildDashboardHtml(state, qrDataUrl) {
|
|
1748
|
+
const tunnelStatus = state.tunnel.up ? "연결됨" : "끊어짐";
|
|
1749
|
+
const tunnelClass = state.tunnel.up ? "status-up" : "status-down";
|
|
1750
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1751
|
+
const pagesHtml = state.pages.length > 0 ? state.pages.map((p) => {
|
|
1752
|
+
return `<li><span class="page-id">${p.id.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`)}</span> <span class="page-url">${p.url.slice(0, 120).replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`)}</span></li>`;
|
|
1753
|
+
}).join("\n") : "<li class=\"empty\">attach된 페이지 없음</li>";
|
|
1754
|
+
let attachSection;
|
|
1755
|
+
if (qrDataUrl && state.attachUrl) attachSection = `
|
|
1756
|
+
<img class="qr" src="${qrDataUrl}" alt="attach QR" />
|
|
1757
|
+
<p class="url-box">${state.attachUrl.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`)}</p>`;
|
|
1758
|
+
else attachSection = "<p class=\"hint\">build_attach_url MCP tool을 호출하면 QR이 여기에 표시됩니다.</p>";
|
|
1759
|
+
return `<!DOCTYPE html>
|
|
1760
|
+
<html lang="ko">
|
|
1761
|
+
<head>
|
|
1762
|
+
<meta charset="utf-8" />
|
|
1763
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1764
|
+
<title>AIT 디버그 Dashboard</title>
|
|
1765
|
+
<style>
|
|
1766
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
1767
|
+
body {
|
|
1768
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1769
|
+
background: #0d1117; color: #c9d1d9;
|
|
1770
|
+
display: flex; flex-direction: column; align-items: center;
|
|
1771
|
+
min-height: 100vh; margin: 0; padding: 2rem 1rem;
|
|
1772
|
+
gap: 1.5rem;
|
|
1773
|
+
}
|
|
1774
|
+
h1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }
|
|
1775
|
+
.updated { font-size: 0.75rem; opacity: 0.4; font-family: monospace; margin: 0; }
|
|
1776
|
+
section { width: 100%; max-width: 520px; }
|
|
1777
|
+
h2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }
|
|
1778
|
+
.status { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.8rem; font-weight: 600; }
|
|
1779
|
+
.status-up { background: #238636; color: #fff; }
|
|
1780
|
+
.status-down { background: #6e7681; color: #fff; }
|
|
1781
|
+
img.qr {
|
|
1782
|
+
width: min(80vw, 300px); height: auto;
|
|
1783
|
+
image-rendering: pixelated;
|
|
1784
|
+
background: #fff; padding: 0.75rem; border-radius: 10px;
|
|
1785
|
+
display: block; margin: 0.5rem auto;
|
|
1786
|
+
}
|
|
1787
|
+
.url-box {
|
|
1788
|
+
font-family: monospace; font-size: 0.7rem;
|
|
1789
|
+
word-break: break-all; opacity: 0.45;
|
|
1790
|
+
background: #161b22; padding: 0.6rem 0.85rem;
|
|
1791
|
+
border-radius: 6px; border: 1px solid #30363d; margin: 0.5rem 0 0;
|
|
1792
|
+
}
|
|
1793
|
+
.hint { font-size: 0.85rem; opacity: 0.5; margin: 0.25rem 0 0; }
|
|
1794
|
+
ul { margin: 0; padding-left: 1.25rem; }
|
|
1795
|
+
li { margin-bottom: 0.35rem; font-size: 0.85rem; line-height: 1.5; }
|
|
1796
|
+
li.empty { opacity: 0.4; list-style: none; padding-left: 0; }
|
|
1797
|
+
.page-id { font-family: monospace; font-size: 0.75rem; opacity: 0.5; margin-right: 0.4rem; }
|
|
1798
|
+
.page-url { word-break: break-all; }
|
|
1799
|
+
hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0; }
|
|
1800
|
+
</style>
|
|
1801
|
+
</head>
|
|
1802
|
+
<body>
|
|
1803
|
+
<h1>AIT 디버그 Dashboard</h1>
|
|
1804
|
+
<p class="updated" id="updated">마지막 갱신: ${now}</p>
|
|
1805
|
+
|
|
1806
|
+
<section>
|
|
1807
|
+
<h2>터널 상태</h2>
|
|
1808
|
+
<span class="status ${tunnelClass}" id="tunnel-status">${tunnelStatus}</span>
|
|
1809
|
+
</section>
|
|
1810
|
+
|
|
1811
|
+
<hr />
|
|
1812
|
+
|
|
1813
|
+
<section>
|
|
1814
|
+
<h2>Attach QR</h2>
|
|
1815
|
+
<div id="attach-section">${attachSection}</div>
|
|
1816
|
+
</section>
|
|
1817
|
+
|
|
1818
|
+
<hr />
|
|
1819
|
+
|
|
1820
|
+
<section>
|
|
1821
|
+
<h2>연결된 Pages</h2>
|
|
1822
|
+
<ul id="pages-list">${pagesHtml}</ul>
|
|
1823
|
+
</section>
|
|
1824
|
+
|
|
1825
|
+
<script>
|
|
1826
|
+
// SSE — /events 구독해 상태 자동 갱신. 빌드 파이프라인 없는 인라인 스크립트.
|
|
1827
|
+
(function () {
|
|
1828
|
+
var src = new EventSource('/events');
|
|
1829
|
+
src.onmessage = function (e) {
|
|
1830
|
+
try {
|
|
1831
|
+
var s = JSON.parse(e.data);
|
|
1832
|
+
// 터널 상태 갱신
|
|
1833
|
+
var el = document.getElementById('tunnel-status');
|
|
1834
|
+
if (el) {
|
|
1835
|
+
el.textContent = s.tunnel && s.tunnel.up ? '연결됨' : '끊어짐';
|
|
1836
|
+
el.className = 'status ' + (s.tunnel && s.tunnel.up ? 'status-up' : 'status-down');
|
|
1837
|
+
}
|
|
1838
|
+
// page 목록 갱신
|
|
1839
|
+
var ul = document.getElementById('pages-list');
|
|
1840
|
+
if (ul) {
|
|
1841
|
+
if (!s.pages || s.pages.length === 0) {
|
|
1842
|
+
ul.innerHTML = '<li class="empty">attach된 페이지 없음</li>';
|
|
1843
|
+
} else {
|
|
1844
|
+
ul.innerHTML = s.pages.map(function (p) {
|
|
1845
|
+
var sid = String(p.id || '').slice(0, 36).replace(/[<>&"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });
|
|
1846
|
+
var su = String(p.url || '').slice(0, 120).replace(/[<>&"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });
|
|
1847
|
+
return '<li><span class="page-id">' + sid + '</span> <span class="page-url">' + su + '</span></li>';
|
|
1848
|
+
}).join('');
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
// attachUrl QR 갱신 — attachUrl이 없으면 hint 표시.
|
|
1852
|
+
var sec = document.getElementById('attach-section');
|
|
1853
|
+
if (sec) {
|
|
1854
|
+
if (s.attachUrl) {
|
|
1855
|
+
// QR은 서버에서 새로 렌더한 /qr.png?u= 로 img src 교체.
|
|
1856
|
+
// TOTP at= 코드는 attachUrl 안에 캡슐화 — 별도 노출 없음.
|
|
1857
|
+
var encoded = encodeURIComponent(s.attachUrl);
|
|
1858
|
+
var safeUrl = String(s.attachUrl).slice(0, 2000).replace(/[<>&"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });
|
|
1859
|
+
sec.innerHTML =
|
|
1860
|
+
'<img class="qr" src="/qr.png?u=' + encoded + '" alt="attach QR" />' +
|
|
1861
|
+
'<p class="url-box">' + safeUrl + '</p>';
|
|
1862
|
+
} else {
|
|
1863
|
+
sec.innerHTML = '<p class="hint">build_attach_url MCP tool을 호출하면 QR이 여기에 표시됩니다.</p>';
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
// 갱신 시각
|
|
1867
|
+
var upd = document.getElementById('updated');
|
|
1868
|
+
if (upd) upd.textContent = '마지막 갱신: ' + new Date().toISOString();
|
|
1869
|
+
} catch (_) { /* 파싱 오류 무시 */ }
|
|
1870
|
+
};
|
|
1871
|
+
src.onerror = function () {
|
|
1872
|
+
// 재연결은 EventSource가 자동 처리 (spec 기본 동작).
|
|
1873
|
+
};
|
|
1874
|
+
})();
|
|
1875
|
+
<\/script>
|
|
1876
|
+
</body>
|
|
1877
|
+
</html>`;
|
|
1878
|
+
}
|
|
1879
|
+
/**
|
|
1672
1880
|
* QR 스캔 페이지 HTML 본문.
|
|
1673
1881
|
* dark theme, inline style, 외부 fetch 없음.
|
|
1674
1882
|
*/
|
|
@@ -1745,6 +1953,112 @@ function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl) {
|
|
|
1745
1953
|
</html>`;
|
|
1746
1954
|
}
|
|
1747
1955
|
//#endregion
|
|
1956
|
+
//#region src/mcp/relay-secret-store.ts
|
|
1957
|
+
/**
|
|
1958
|
+
* Project-local relay TOTP secret store (#394 first-run auto-mint, #396 moved to
|
|
1959
|
+
* a project-local single file `.ait_relay`).
|
|
1960
|
+
*
|
|
1961
|
+
* Two surfaces, intentionally split by who is allowed to write:
|
|
1962
|
+
*
|
|
1963
|
+
* - {@link ensureRelaySecret} — WRITE path, called ONLY from the unplugin
|
|
1964
|
+
* (env-2 relay boot). Mints a fresh secret on first run and persists it to
|
|
1965
|
+
* `<projectRoot>/.ait_relay` (0600). A single file — no directory is created.
|
|
1966
|
+
*
|
|
1967
|
+
* - {@link loadRelaySecretReadOnly} — READ-ONLY path, called from the MCP
|
|
1968
|
+
* daemon when switching into a relay environment. It NEVER mints, chmods, or
|
|
1969
|
+
* creates anything: it only reads an already-existing `.ait_relay` and injects
|
|
1970
|
+
* its value into `env`. A daemon that minted would defeat the #250 fail-fast
|
|
1971
|
+
* (the daemon is the verifier side — a self-minted secret would let a leaked
|
|
1972
|
+
* tunnel URL attach unauthenticated), so the daemon stays read-only.
|
|
1973
|
+
*
|
|
1974
|
+
* Why a per-session `projectRoot` instead of `process.cwd()`: the daemon cannot
|
|
1975
|
+
* trust its own cwd — agent-plugin spawns it via `npx` without `cwd`, so cwd is
|
|
1976
|
+
* frozen at Claude Code launch and a cwd-walk stops at the monorepo workspace
|
|
1977
|
+
* root (which always has a package.json). So the project root is supplied
|
|
1978
|
+
* per-debug-session through `start_debug`.
|
|
1979
|
+
*
|
|
1980
|
+
* SECRET-HANDLING: this module handles AIT_DEBUG_TOTP_SECRET — the raw value and
|
|
1981
|
+
* its length MUST NOT appear in any log, error message, stdout, stderr, or
|
|
1982
|
+
* assertion output. Only boolean pass/fail signals are safe to surface, and the
|
|
1983
|
+
* discovered file path is never logged either. The persist file is written mode
|
|
1984
|
+
* 0600.
|
|
1985
|
+
*/
|
|
1986
|
+
/** Project-local secret file name (single file, not a directory). */
|
|
1987
|
+
const RELAY_SECRET_FILE_NAME = ".ait_relay";
|
|
1988
|
+
/**
|
|
1989
|
+
* Walks upward from `start` and returns the nearest directory that contains a
|
|
1990
|
+
* `package.json`. Falls back to `start` itself when none is found (so a write
|
|
1991
|
+
* still lands somewhere deterministic).
|
|
1992
|
+
*
|
|
1993
|
+
* The write (unplugin) and read (daemon) sides use the SAME anchor so a secret
|
|
1994
|
+
* minted by `pnpm dev` is found by the daemon: real mini-apps keep
|
|
1995
|
+
* `vite.config.ts` and `package.json` in the same directory, so
|
|
1996
|
+
* `server.config.root === package.json-dir`. In a monorepo subdir the anchor is
|
|
1997
|
+
* the package's own directory — the one the daemon can also reach via the
|
|
1998
|
+
* per-session projectRoot.
|
|
1999
|
+
*
|
|
2000
|
+
* @param start - Directory to start the upward walk from.
|
|
2001
|
+
* @param existsSyncFn - Injectable existence check (defaults to node:fs).
|
|
2002
|
+
*/
|
|
2003
|
+
function nearestPackageJsonDir(start, existsSyncFn) {
|
|
2004
|
+
let dir = start;
|
|
2005
|
+
while (true) {
|
|
2006
|
+
if (existsSyncFn(join(dir, "package.json"))) return dir;
|
|
2007
|
+
const parent = dirname(dir);
|
|
2008
|
+
if (parent === dir) return start;
|
|
2009
|
+
dir = parent;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* Absolute path to the project-local `.ait_relay` file for a given start
|
|
2014
|
+
* directory (resolved against the nearest package.json directory).
|
|
2015
|
+
*
|
|
2016
|
+
* Exported so tests can compute the expected path without duplicating the
|
|
2017
|
+
* resolution logic.
|
|
2018
|
+
*/
|
|
2019
|
+
function relaySecretFilePath(start, existsSyncFn) {
|
|
2020
|
+
return join(nearestPackageJsonDir(start, existsSyncFn), RELAY_SECRET_FILE_NAME);
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* Reads an already-existing `<projectRoot>/.ait_relay` and, if its contents are a
|
|
2024
|
+
* valid relay TOTP secret, injects them into `env.AIT_DEBUG_TOTP_SECRET`.
|
|
2025
|
+
*
|
|
2026
|
+
* Strictly READ-ONLY: it uses only `existsSync` + `readFileSync` and NEVER mints,
|
|
2027
|
+
* chmods, or creates files/directories. The daemon must not mint because it is
|
|
2028
|
+
* the relay verifier side — a self-minted secret would defeat the #250 fail-fast
|
|
2029
|
+
* (a leaked tunnel URL could then attach unauthenticated). If no valid secret is
|
|
2030
|
+
* found the function leaves `env` untouched and returns without throwing, so the
|
|
2031
|
+
* downstream `assertRelayAuthConfigured()` stays the single fail-fast.
|
|
2032
|
+
*
|
|
2033
|
+
* Resolution order:
|
|
2034
|
+
* 1. `env.AIT_DEBUG_TOTP_SECRET` already valid → no-op (operator export wins).
|
|
2035
|
+
* 2. `projectRoot` given → read `<nearest package.json dir>/.ait_relay`; inject
|
|
2036
|
+
* iff the contents pass {@link isValidRelayAuthSecret}.
|
|
2037
|
+
* 3. Otherwise (no projectRoot, file absent, or invalid) → silent no-op.
|
|
2038
|
+
*
|
|
2039
|
+
* SECRET-HANDLING: the read value is passed ONLY to the boolean predicate before
|
|
2040
|
+
* assignment; its value, length, and the discovered file path are never logged.
|
|
2041
|
+
*
|
|
2042
|
+
* @param deps - Optional dependency overrides for testing.
|
|
2043
|
+
*/
|
|
2044
|
+
async function loadRelaySecretReadOnly(deps) {
|
|
2045
|
+
const { projectRoot, env = process.env, fs: fsDep, existsSync: existsSyncDep } = deps ?? {};
|
|
2046
|
+
const { isValidRelayAuthSecret } = await import("../totp-CQFmgOhM.js");
|
|
2047
|
+
if (isValidRelayAuthSecret(env.AIT_DEBUG_TOTP_SECRET)) return;
|
|
2048
|
+
if (projectRoot === void 0) return;
|
|
2049
|
+
const fs = fsDep ?? await import("node:fs");
|
|
2050
|
+
const secretPath = relaySecretFilePath(projectRoot, existsSyncDep ?? fs.existsSync);
|
|
2051
|
+
if (!fs.existsSync(secretPath)) return;
|
|
2052
|
+
let stored;
|
|
2053
|
+
try {
|
|
2054
|
+
stored = fs.readFileSync(secretPath, "utf8").trim();
|
|
2055
|
+
} catch {
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
if (!isValidRelayAuthSecret(stored)) return;
|
|
2059
|
+
env.AIT_DEBUG_TOTP_SECRET = stored;
|
|
2060
|
+
}
|
|
2061
|
+
//#endregion
|
|
1748
2062
|
//#region src/mcp/server-lock.ts
|
|
1749
2063
|
/**
|
|
1750
2064
|
* Single debug session lock for the `devtools-mcp` debug server.
|
|
@@ -2085,186 +2399,6 @@ function warnPassthrough(name) {
|
|
|
2085
2399
|
}
|
|
2086
2400
|
SIGNATURES.map((s) => s.name);
|
|
2087
2401
|
//#endregion
|
|
2088
|
-
//#region src/mcp/totp.ts
|
|
2089
|
-
/**
|
|
2090
|
-
* RFC 6238 TOTP implementation (Node.js, node:crypto only).
|
|
2091
|
-
*
|
|
2092
|
-
* External TOTP libraries (otplib, speakeasy, …) are intentionally NOT used
|
|
2093
|
-
* to keep the dependency surface minimal. This hand-roll is ~30 lines and
|
|
2094
|
-
* covers exactly what relay-side auth needs.
|
|
2095
|
-
*
|
|
2096
|
-
* Algorithm summary (RFC 6238 + RFC 4226):
|
|
2097
|
-
* T = floor(now / 30) — 30-second time step counter
|
|
2098
|
-
* K = Buffer.from(secret, 'hex') — shared secret (raw bytes, hex-encoded)
|
|
2099
|
-
* MAC = HMAC-SHA1(K, T as 8-byte big-endian uint64)
|
|
2100
|
-
* offset = MAC[19] & 0x0f
|
|
2101
|
-
* code = (MAC[offset..offset+4] & 0x7fffffff) % 10^6 — 6 digits
|
|
2102
|
-
*
|
|
2103
|
-
* Security note (keep this comment accurate):
|
|
2104
|
-
* The baked-in secret in a dogfood build is extractable from the bundle by a
|
|
2105
|
-
* determined reverse engineer. This mechanism raises the bar from
|
|
2106
|
-
* "anyone with the URL" to "URL + bundle extraction + live TOTP calculation".
|
|
2107
|
-
* Casual URL leaks (Slack paste, QR screenshot, shoulder-surfing) are
|
|
2108
|
-
* blocked; deliberate reverse engineering is not. See threat model in
|
|
2109
|
-
* src/mcp/chii-relay.ts and umbrella CLAUDE.md §4.
|
|
2110
|
-
*
|
|
2111
|
-
* SECRET-HANDLING: secret values and computed codes MUST NOT appear in any
|
|
2112
|
-
* log, error message, or string visible outside this module. Only boolean
|
|
2113
|
-
* pass/fail and reason enum values are safe to surface.
|
|
2114
|
-
*/
|
|
2115
|
-
/** Time step window in seconds (RFC 6238 default). */
|
|
2116
|
-
const TIME_STEP = 30;
|
|
2117
|
-
/** Number of digits in the generated code. */
|
|
2118
|
-
const DIGITS = 6;
|
|
2119
|
-
/**
|
|
2120
|
-
* Derives a 6-digit TOTP code from a hex-encoded secret at the given wall-
|
|
2121
|
-
* clock time.
|
|
2122
|
-
*
|
|
2123
|
-
* @param secret - The shared secret as a hex string (e.g. 64 hex chars = 32
|
|
2124
|
-
* bytes). Must be the output of `generateAttachToken()` or compatible.
|
|
2125
|
-
* @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
|
|
2126
|
-
* @returns A zero-padded 6-digit decimal string, e.g. `"042193"`.
|
|
2127
|
-
*/
|
|
2128
|
-
function generateTotp(secret, when = Date.now()) {
|
|
2129
|
-
const key = Buffer.from(secret, "hex");
|
|
2130
|
-
const counter = Math.max(0, Math.floor(when / 1e3 / TIME_STEP));
|
|
2131
|
-
const counterBuf = Buffer.alloc(8);
|
|
2132
|
-
const hi = Math.floor(counter / 4294967296);
|
|
2133
|
-
const lo = counter >>> 0;
|
|
2134
|
-
counterBuf.writeUInt32BE(hi, 0);
|
|
2135
|
-
counterBuf.writeUInt32BE(lo, 4);
|
|
2136
|
-
const mac = createHmac("sha1", key).update(counterBuf).digest();
|
|
2137
|
-
const offset = mac[19] & 15;
|
|
2138
|
-
return (((mac[offset] & 127) << 24 | (mac[offset + 1] & 255) << 16 | (mac[offset + 2] & 255) << 8 | mac[offset + 3] & 255) % 10 ** DIGITS).toString().padStart(DIGITS, "0");
|
|
2139
|
-
}
|
|
2140
|
-
/**
|
|
2141
|
-
* Verifies a TOTP code against the secret, accepting ±`skew` time steps to
|
|
2142
|
-
* tolerate clock drift between the relay host and the client device.
|
|
2143
|
-
*
|
|
2144
|
-
* Uses `timingSafeEqual` for constant-time comparison to prevent timing
|
|
2145
|
-
* side-channel attacks.
|
|
2146
|
-
*
|
|
2147
|
-
* @param secret - Hex-encoded shared secret.
|
|
2148
|
-
* @param code - The 6-digit code to verify (string or numeric).
|
|
2149
|
-
* @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
|
|
2150
|
-
* @param skew - Number of adjacent steps to accept on either side. Default 1
|
|
2151
|
-
* (accepts T-1, T, T+1 — a 90-second acceptance window).
|
|
2152
|
-
* @returns `true` if the code matches any accepted step, `false` otherwise.
|
|
2153
|
-
*/
|
|
2154
|
-
function verifyTotp(secret, code, when = Date.now(), skew = 1) {
|
|
2155
|
-
const normalised = String(code).padStart(DIGITS, "0");
|
|
2156
|
-
if (normalised.length !== DIGITS || !/^\d{6}$/.test(normalised)) return false;
|
|
2157
|
-
const candidateBuf = Buffer.from(normalised, "utf8");
|
|
2158
|
-
for (let delta = -skew; delta <= skew; delta++) {
|
|
2159
|
-
const expected = generateTotp(secret, when + delta * TIME_STEP * 1e3);
|
|
2160
|
-
if (timingSafeEqual(Buffer.from(expected, "utf8"), candidateBuf)) return true;
|
|
2161
|
-
}
|
|
2162
|
-
return false;
|
|
2163
|
-
}
|
|
2164
|
-
/**
|
|
2165
|
-
* Minimum length (in hex characters) accepted for `AIT_DEBUG_TOTP_SECRET`.
|
|
2166
|
-
*
|
|
2167
|
-
* The secret is hex-encoded (see {@link generateTotp} — `Buffer.from(secret,
|
|
2168
|
-
* 'hex')`). 32 hex chars = 16 bytes = 128 bits, the floor for an HMAC-SHA1 key
|
|
2169
|
-
* we are willing to gate a public relay behind. `generateAttachToken()` emits
|
|
2170
|
-
* 64 hex chars (32 bytes), comfortably above this bar.
|
|
2171
|
-
*/
|
|
2172
|
-
const MIN_SECRET_HEX_CHARS = 32;
|
|
2173
|
-
/** Hex string: one or more hex digits, case-insensitive (RFC 4648 base16). */
|
|
2174
|
-
const HEX_RE = /^[0-9a-fA-F]+$/;
|
|
2175
|
-
/**
|
|
2176
|
-
* Human-facing guidance printed when {@link assertRelayAuthConfigured} fails.
|
|
2177
|
-
*
|
|
2178
|
-
* SECRET-HANDLING: this message states only the REQUIREMENT (≥32 hex chars) and
|
|
2179
|
-
* how to mint one. It NEVER echoes the configured value, its length, or any
|
|
2180
|
-
* fragment derived from it — see {@link assertRelayAuthConfigured}.
|
|
2181
|
-
*
|
|
2182
|
-
* Note on encoding: the secret is hex (base16), not base32 — `generateTotp`
|
|
2183
|
-
* decodes it with `Buffer.from(secret, 'hex')`. A base32 string would be
|
|
2184
|
-
* silently mis-decoded and every TOTP code would fail to match, so the minting
|
|
2185
|
-
* command emits hex.
|
|
2186
|
-
*/
|
|
2187
|
-
const RELAY_AUTH_SECRET_MISSING_MESSAGE = [
|
|
2188
|
-
"[ait-debug] AIT_DEBUG_TOTP_SECRET이 필수입니다. 32자 이상 16진수(hex) 문자열을 설정하세요.",
|
|
2189
|
-
"발급: openssl rand -hex 32",
|
|
2190
|
-
"자세히: https://docs.aitc.dev/guides/relay-auth-totp"
|
|
2191
|
-
].join("\n");
|
|
2192
|
-
/**
|
|
2193
|
-
* Whether `secret` is a well-formed relay-auth TOTP secret: a hex string of at
|
|
2194
|
-
* least {@link MIN_SECRET_HEX_CHARS} characters with an even length (an odd
|
|
2195
|
-
* length would have its trailing nibble silently dropped by `Buffer.from(...,
|
|
2196
|
-
* 'hex')`, weakening the key without warning).
|
|
2197
|
-
*
|
|
2198
|
-
* Pure predicate so callers can test the validation independently of the
|
|
2199
|
-
* fail-fast side effect in {@link assertRelayAuthConfigured}.
|
|
2200
|
-
*
|
|
2201
|
-
* SECRET-HANDLING: returns only a boolean — the input value is never returned,
|
|
2202
|
-
* logged, or echoed.
|
|
2203
|
-
*/
|
|
2204
|
-
function isValidRelayAuthSecret(secret) {
|
|
2205
|
-
if (secret === void 0 || secret === "") return false;
|
|
2206
|
-
if (secret.length < MIN_SECRET_HEX_CHARS) return false;
|
|
2207
|
-
if (secret.length % 2 !== 0) return false;
|
|
2208
|
-
return HEX_RE.test(secret);
|
|
2209
|
-
}
|
|
2210
|
-
/**
|
|
2211
|
-
* Fail-fast guard enforcing that a relay-auth TOTP secret is configured before
|
|
2212
|
-
* a public-internet-exposed relay is booted (issue #250).
|
|
2213
|
-
*
|
|
2214
|
-
* Relay-auth (the §4 Layer C TOTP gate) is the only fail-fast layer that closes
|
|
2215
|
-
* the real gap: a leaked `wss://…trycloudflare.com` URL otherwise lets a third
|
|
2216
|
-
* party attach a debugger to a dogfood/live mini-app. Without a secret the relay
|
|
2217
|
-
* comes up unauthenticated, so this guard is called at every relay-boot site —
|
|
2218
|
-
* `bootRelayFamily` (intoss env 3/4) and `bootExternalRelayFamily` (env-2 PWA),
|
|
2219
|
-
* both eager and lazy. Local-only sessions never boot a relay and so never reach
|
|
2220
|
-
* this guard, matching the issue's exemption for non-relay debugging.
|
|
2221
|
-
*
|
|
2222
|
-
* Throws when the secret is unset, empty, too short, or not a valid hex string.
|
|
2223
|
-
* The thrown message is the bin entry's fatal stderr (see `cli.ts` `main().catch`)
|
|
2224
|
-
* — the same fatal model as the missing-`AIT_RELAY_BASE_URL` path.
|
|
2225
|
-
*
|
|
2226
|
-
* SECRET-HANDLING: the env value is read once, passed ONLY to the boolean
|
|
2227
|
-
* predicate, and never logged. The thrown message names the requirement, never
|
|
2228
|
-
* the value, its length, or any derived fragment.
|
|
2229
|
-
*
|
|
2230
|
-
* @param env - Environment to read from. Defaults to `process.env`; injectable
|
|
2231
|
-
* for tests so they never mutate the real process environment.
|
|
2232
|
-
*/
|
|
2233
|
-
function assertRelayAuthConfigured(env = process.env) {
|
|
2234
|
-
if (!isValidRelayAuthSecret(env.AIT_DEBUG_TOTP_SECRET)) throw new Error(RELAY_AUTH_SECRET_MISSING_MESSAGE);
|
|
2235
|
-
}
|
|
2236
|
-
/**
|
|
2237
|
-
* Reads `AIT_DEBUG_TOTP_SECRET` from `process.env` at runtime and builds a
|
|
2238
|
-
* `verifyAuth` predicate for the Chii relay's WebSocket upgrade gate.
|
|
2239
|
-
*
|
|
2240
|
-
* The predicate checks the `at` query parameter against the current and
|
|
2241
|
-
* adjacent TOTP time steps (±1 skew) using {@link verifyTotp}.
|
|
2242
|
-
*
|
|
2243
|
-
* Returns `undefined` when the env var is not set — callers treat that as
|
|
2244
|
-
* "auth disabled" (no predicate registered on the relay). Note that since
|
|
2245
|
-
* issue #250 the secret is MANDATORY at every relay-boot site (enforced by
|
|
2246
|
-
* {@link assertRelayAuthConfigured} BEFORE the relay starts), so in production
|
|
2247
|
-
* this never returns `undefined` for a relay that actually boots; the
|
|
2248
|
-
* `undefined` branch only matters for the no-relay local path and tests.
|
|
2249
|
-
*
|
|
2250
|
-
* Lives here (not in the MCP server) so the unplugin's env-2 relay can wire the
|
|
2251
|
-
* same gate without importing the heavy MCP server module graph. Re-exported
|
|
2252
|
-
* from `debug-server.ts` for back-compat.
|
|
2253
|
-
*
|
|
2254
|
-
* SECRET-HANDLING: The secret value read from env is captured in a closure and
|
|
2255
|
-
* is NEVER written to any log, error message, or process output.
|
|
2256
|
-
*/
|
|
2257
|
-
function buildRelayVerifyAuth(env = process.env) {
|
|
2258
|
-
const secret = env.AIT_DEBUG_TOTP_SECRET;
|
|
2259
|
-
if (!secret) return void 0;
|
|
2260
|
-
return (req) => {
|
|
2261
|
-
const rawUrl = req.url ?? "";
|
|
2262
|
-
const qIndex = rawUrl.indexOf("?");
|
|
2263
|
-
const queryStr = qIndex === -1 ? "" : rawUrl.slice(qIndex + 1);
|
|
2264
|
-
return verifyTotp(secret, new URLSearchParams(queryStr).get("at") ?? "");
|
|
2265
|
-
};
|
|
2266
|
-
}
|
|
2267
|
-
//#endregion
|
|
2268
2402
|
//#region src/mcp/tools.ts
|
|
2269
2403
|
/** Static MCP tool descriptors (name + JSONSchema) for the full debug tool surface. */
|
|
2270
2404
|
const DEBUG_TOOL_DEFINITIONS = [
|
|
@@ -2300,13 +2434,13 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
2300
2434
|
},
|
|
2301
2435
|
{
|
|
2302
2436
|
name: "build_attach_url",
|
|
2303
|
-
description: "The tool result already shows the QR to the user directly (Claude Code renders MCP tool output to the user's screen; they press Ctrl+O to expand if it's collapsed). Do NOT re-print or re-render the QR in your reply — that just wastes output tokens. Simply tell the user to scan the QR shown in this tool's output with their phone camera. Builds a self-attaching deep link for the active relay environment and returns a QR code. Scan the QR with the phone camera to open the mini-app and attach it to this debug session (QR is the single entry path — no USB cable or platform CLI needed). Call list_pages first to confirm the relay/tunnel is up. If the tunnel is not up, restart: `npx @ait-co/devtools devtools-mcp`.\n\nEnvironment-specific behaviour:\n • env 3 / staging (start_debug mode=\"staging\"): requires scheme_url — the intoss-private://…?_deploymentId=<uuid> URL from `ait deploy --scheme-only`. Splices debug=1 + relay URL into the scheme URL to produce a self-attach deep link.\n • env 2 /
|
|
2437
|
+
description: "The tool result already shows the QR to the user directly (Claude Code renders MCP tool output to the user's screen; they press Ctrl+O to expand if it's collapsed). Do NOT re-print or re-render the QR in your reply — that just wastes output tokens. Simply tell the user to scan the QR shown in this tool's output with their phone camera. Builds a self-attaching deep link for the active relay environment and returns a QR code. Scan the QR with the phone camera to open the mini-app and attach it to this debug session (QR is the single entry path — no USB cable or platform CLI needed). Call list_pages first to confirm the relay/tunnel is up. If the tunnel is not up, restart: `npx @ait-co/devtools devtools-mcp`.\n\nEnvironment-specific behaviour:\n • env 3 / relay-staging (start_debug mode=\"relay-staging\"): requires scheme_url — the intoss-private://…?_deploymentId=<uuid> URL from `ait deploy --scheme-only`. Splices debug=1 + relay URL into the scheme URL to produce a self-attach deep link.\n • env 2 / relay-sandbox (start_debug mode=\"relay-sandbox\"): scheme_url is NOT used. Instead, reads AIT_TUNNEL_BASE_URL (the https://*.trycloudflare.com app tunnel from `tunnel:{cdp:true}`) and builds a launcher PWA deep-link (https://devtools.aitc.dev/launcher/?url=…&debug=1&relay=…). Scan the QR with the phone to open the launcher, which frames the tunnel URL and attaches CDP.\n\nSet wait_for_attach=true to block until a page attaches (polls up to 30 s). On timeout, call build_attach_url again to resume polling. When open_in_browser=true (default), saves the QR as a PNG and opens it in the OS default browser — only works when the MCP server runs on a local GUI machine (not headless/remote containers). \n\nTOTP auth: when AIT_DEBUG_TOTP_SECRET is set on the MCP server, the returned attachUrl automatically includes the current one-time code (at=<code>) — the URL is single-use for that 30-second step. The response includes a `totp` field with `expiresAt` (ISO timestamp). If the phone scan happens after expiresAt, the relay will reject the code — just call build_attach_url again to get a fresh one-time URL. Without AIT_DEBUG_TOTP_SECRET, the attachUrl has no expiry.",
|
|
2304
2438
|
inputSchema: {
|
|
2305
2439
|
type: "object",
|
|
2306
2440
|
properties: {
|
|
2307
2441
|
scheme_url: {
|
|
2308
2442
|
type: "string",
|
|
2309
|
-
description: "The intoss-private:// scheme URL from `ait deploy --scheme-only` (must carry _deploymentId). Required for env 3/staging mode. Not used in env 2/
|
|
2443
|
+
description: "The intoss-private:// scheme URL from `ait deploy --scheme-only` (must carry _deploymentId). Required for env 3/relay-staging mode. Not used in env 2/relay-sandbox mode (use AIT_TUNNEL_BASE_URL instead). The authority (host) must be the app name (e.g. intoss-private://aitc-sdk-example?_deploymentId=…). Generic values like \"web\" or an empty host indicate a malformed URL."
|
|
2310
2444
|
},
|
|
2311
2445
|
wait_for_attach: {
|
|
2312
2446
|
type: "boolean",
|
|
@@ -2449,23 +2583,27 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
2449
2583
|
},
|
|
2450
2584
|
{
|
|
2451
2585
|
name: "start_debug",
|
|
2452
|
-
description: "Switches the active debug environment in-place (issue #348) — no Claude Code restart and no MCP re-handshake. One daemon holds both a local (env 1, mock SDK in a Chromium) and a relay (env 3/4, real-device Toss WebView over the Chii relay + cloudflared tunnel) connection at once; this tool flips which one every other tool reads from, lazily booting the requested family's infra on first use and keeping the inactive one warm so an existing attach survives the switch. After switching it emits notifications/tools/list_changed — call tools/list again to see the updated tool surface for the new environment.\n\nmodes:\n local — env 1: desktop Chromium with the MOCK SDK and a local CDP attach. Side-effect tools (call_sdk/evaluate) run unguarded against the mock; nothing touches a real device or real users. No prerequisites — the default, always-available environment for state/contract and visual-layout work.\n
|
|
2586
|
+
description: "Switches the active debug environment in-place (issue #348) — no Claude Code restart and no MCP re-handshake. One daemon holds both a local (env 1, mock SDK in a Chromium) and a relay (env 3/4, real-device Toss WebView over the Chii relay + cloudflared tunnel) connection at once; this tool flips which one every other tool reads from, lazily booting the requested family's infra on first use and keeping the inactive one warm so an existing attach survives the switch. After switching it emits notifications/tools/list_changed — call tools/list again to see the updated tool surface for the new environment.\n\nmodes:\n local-browser — env 1: desktop Chromium with the MOCK SDK and a local CDP attach. Side-effect tools (call_sdk/evaluate) run unguarded against the mock; nothing touches a real device or real users. No prerequisites — the default, always-available environment for state/contract and visual-layout work.\n relay-sandbox — env 2: a real-device PWA (real WebKit engine, MOCK SDK) over an external Chii relay that the unplugin already started with tunnel:{cdp:true}. CDP covers real-device WebKit DOM, console, exceptions, and safe-area observation; call_sdk still hits the mock (SDK fidelity needs relay-staging). liveIntent off — dev-intent, LIVE guard inactive, side-effect tools run unguarded against the mock. Prerequisites: AIT_RELAY_BASE_URL env var set + unplugin running with tunnel:{cdp:true} so the relay tunnel is already up.\n relay-staging — env 3: a real-device Toss WebView dogfood build with the REAL SDK over the intoss-private relay. The first environment where call_sdk exercises the genuine native bridge. Side-effect tools run unguarded (dogfood, not released to real users). Prerequisite: a deployed dogfood candidate bundle + the device cold-loaded via the intoss-private deep-link/QR relay injection.\n relay-live — env 4: the REVIEW-PASSED, released production runtime with the REAL SDK over the intoss relay — real end users are on the other side. Read-only debugging is the intent: the LIVE guard is armed, so call_sdk/evaluate require confirm:true per call, and ENTERING relay-live ALSO requires confirm:true on this call. Use it only to observe a shipped regression; verify fixes in relay-staging first.\n\nSwitching back to local-browser automatically disarms the LIVE guard.\n\nFor a relay mode (relay-sandbox/relay-staging/relay-live), also pass projectRoot — the absolute mini-app project root — so the daemon can read the relay auth secret from <projectRoot>/.ait_relay (read-only; the daemon never mints it). Omit it for local-browser.",
|
|
2453
2587
|
inputSchema: {
|
|
2454
2588
|
type: "object",
|
|
2455
2589
|
properties: {
|
|
2456
2590
|
mode: {
|
|
2457
2591
|
type: "string",
|
|
2458
2592
|
enum: [
|
|
2459
|
-
"local",
|
|
2460
|
-
"
|
|
2461
|
-
"staging",
|
|
2462
|
-
"live"
|
|
2593
|
+
"local-browser",
|
|
2594
|
+
"relay-sandbox",
|
|
2595
|
+
"relay-staging",
|
|
2596
|
+
"relay-live"
|
|
2463
2597
|
],
|
|
2464
|
-
description: "Target environment to switch to. mode=live additionally requires confirm: true (and arms the read-only LIVE guard)."
|
|
2598
|
+
description: "Target environment to switch to. mode=relay-live additionally requires confirm: true (and arms the read-only LIVE guard)."
|
|
2465
2599
|
},
|
|
2466
2600
|
confirm: {
|
|
2467
2601
|
type: "boolean",
|
|
2468
|
-
description: "Required when mode=live — set true to acknowledge entering LIVE (env 4) debugging that can affect real users. Ignored for the other modes."
|
|
2602
|
+
description: "Required when mode=relay-live — set true to acknowledge entering LIVE (env 4) debugging that can affect real users. Ignored for the other modes."
|
|
2603
|
+
},
|
|
2604
|
+
projectRoot: {
|
|
2605
|
+
type: "string",
|
|
2606
|
+
description: "Absolute path to the mini-app project root (the directory containing its package.json and .ait_relay). The daemon reads the relay auth secret from <projectRoot>/.ait_relay (read-only) when switching to a relay environment (relay-staging/relay-live/relay-sandbox). Pass this because the daemon's own cwd is fixed at launch and may not be the project being debugged. Omit for mode=local-browser (no secret needed)."
|
|
2469
2607
|
}
|
|
2470
2608
|
},
|
|
2471
2609
|
required: ["mode"]
|
|
@@ -3294,7 +3432,7 @@ async function readMcpSdkVersion() {
|
|
|
3294
3432
|
* some test environments that skip the build step).
|
|
3295
3433
|
*/
|
|
3296
3434
|
function readDevtoolsVersion() {
|
|
3297
|
-
return "0.1.
|
|
3435
|
+
return "0.1.57";
|
|
3298
3436
|
}
|
|
3299
3437
|
/**
|
|
3300
3438
|
* Derives the next recommended action from a completed diagnostics snapshot.
|
|
@@ -3712,13 +3850,13 @@ function extractDeploymentId(schemeUrl) {
|
|
|
3712
3850
|
}
|
|
3713
3851
|
}
|
|
3714
3852
|
/**
|
|
3715
|
-
* Returns `true` when the mode routes to a relay connection (`
|
|
3716
|
-
* `staging`, or `live`). `
|
|
3717
|
-
* are intoss-private relays — but all three surface
|
|
3718
|
-
* tool set.
|
|
3853
|
+
* Returns `true` when the mode routes to a relay connection (`relay-sandbox`,
|
|
3854
|
+
* `relay-staging`, or `relay-live`). `relay-sandbox` is an external-PWA relay;
|
|
3855
|
+
* `relay-staging`/`relay-live` are intoss-private relays — but all three surface
|
|
3856
|
+
* the Tier B / relay-only tool set.
|
|
3719
3857
|
*/
|
|
3720
3858
|
function isRelayMode(mode) {
|
|
3721
|
-
return mode === "
|
|
3859
|
+
return mode === "relay-sandbox" || mode === "relay-staging" || mode === "relay-live";
|
|
3722
3860
|
}
|
|
3723
3861
|
/**
|
|
3724
3862
|
* Waits for the first target matching `filterFn` to attach, using the
|
|
@@ -3774,14 +3912,15 @@ function waitForAttachWithEvents(connection, filterFn, timeoutMs, pollIntervalMs
|
|
|
3774
3912
|
* naturally via `enableDomains`). The tier only controls visibility.
|
|
3775
3913
|
*/
|
|
3776
3914
|
function createDebugServer(deps) {
|
|
3777
|
-
const { connection, router: routerDep, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 9e4, qrHttpServer, getEnvironment: getEnvDep, getEnvironmentReason: getEnvReasonDep, diagnosticsCollector: collectorDep, totpSecret } = deps;
|
|
3915
|
+
const { connection, router: routerDep, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 9e4, qrHttpServer, getEnvironment: getEnvDep, getEnvironmentReason: getEnvReasonDep, diagnosticsCollector: collectorDep, totpSecret, onAttachUrlBuilt } = deps;
|
|
3916
|
+
const getTotpSecret = deps.getTotpSecret ?? (() => totpSecret);
|
|
3778
3917
|
const router = routerDep ?? makeSingleConnectionRouter(connection);
|
|
3779
3918
|
const resolveEnvironment = getEnvDep ?? (() => deriveEnvironment(router.active.kind, getLiveIntent(), router.activeRelayOrigin));
|
|
3780
3919
|
const resolveEnvironmentReason = getEnvReasonDep ?? (() => `derived:kind=${router.active.kind},liveIntent=${getLiveIntent()}`);
|
|
3781
3920
|
const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
|
|
3782
3921
|
const server = new Server({
|
|
3783
3922
|
name: "ait-debug",
|
|
3784
|
-
version: "0.1.
|
|
3923
|
+
version: "0.1.57"
|
|
3785
3924
|
}, { capabilities: { tools: { listChanged: true } } });
|
|
3786
3925
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
3787
3926
|
const conn = router.active;
|
|
@@ -3803,10 +3942,12 @@ function createDebugServer(deps) {
|
|
|
3803
3942
|
if (name === "start_debug") {
|
|
3804
3943
|
const rawMode = request.params.arguments?.mode;
|
|
3805
3944
|
const mode = normalizeStartDebugMode(rawMode);
|
|
3806
|
-
if (mode === null) return mcpError("start_debug: mode가 올바르지 않습니다. 'local' | '
|
|
3945
|
+
if (mode === null) return mcpError("start_debug: mode가 올바르지 않습니다. 'local-browser' | 'relay-sandbox' | 'relay-staging' | 'relay-live' 중 하나를 전달하세요.");
|
|
3807
3946
|
const confirm = request.params.arguments?.confirm === true;
|
|
3947
|
+
const rawProjectRoot = request.params.arguments?.projectRoot;
|
|
3948
|
+
const projectRoot = typeof rawProjectRoot === "string" ? rawProjectRoot : void 0;
|
|
3808
3949
|
try {
|
|
3809
|
-
return jsonResult$1(await router.switchMode(mode, confirm));
|
|
3950
|
+
return jsonResult$1(await router.switchMode(mode, confirm, projectRoot));
|
|
3810
3951
|
} catch (err) {
|
|
3811
3952
|
return errorResult(err, name);
|
|
3812
3953
|
}
|
|
@@ -3858,11 +3999,12 @@ function createDebugServer(deps) {
|
|
|
3858
3999
|
if (tunnelHttpUrl === "") return mcpError("build_attach_url(mobile): AIT_TUNNEL_BASE_URL이 설정되지 않았습니다. unplugin tunnel:{cdp:true} 배너에 출력되는 앱 HTTP 터널 URL을 AIT_TUNNEL_BASE_URL 환경변수로 전달하세요.");
|
|
3859
4000
|
const tunnelStatus = getTunnelStatus();
|
|
3860
4001
|
if (!tunnelStatus.up || tunnelStatus.wssUrl === null) return mcpError("build_attach_url(mobile): relay wssUrl이 아직 설정되지 않았습니다. unplugin tunnel:{cdp:true}가 relay를 완전히 기동할 때까지 잠시 후 다시 시도하세요.");
|
|
4002
|
+
const secret = getTotpSecret();
|
|
3861
4003
|
let totpCode;
|
|
3862
4004
|
let totpMeta;
|
|
3863
|
-
if (
|
|
4005
|
+
if (secret !== void 0 && secret !== "") {
|
|
3864
4006
|
const now = Date.now();
|
|
3865
|
-
totpCode = generateTotp(
|
|
4007
|
+
totpCode = generateTotp(secret, now);
|
|
3866
4008
|
const STEP_SECONDS = 30;
|
|
3867
4009
|
const currentStep = Math.floor(now / 1e3 / STEP_SECONDS);
|
|
3868
4010
|
totpMeta = {
|
|
@@ -3872,6 +4014,7 @@ function createDebugServer(deps) {
|
|
|
3872
4014
|
};
|
|
3873
4015
|
}
|
|
3874
4016
|
const attachUrl = buildLauncherAttachUrl(tunnelHttpUrl, tunnelStatus.wssUrl, totpCode);
|
|
4017
|
+
onAttachUrlBuilt?.(attachUrl);
|
|
3875
4018
|
const relayUrl = tunnelStatus.wssUrl;
|
|
3876
4019
|
const totp = totpMeta;
|
|
3877
4020
|
const isMatchingPage = (pages) => pages.length > 0;
|
|
@@ -4040,7 +4183,8 @@ function createDebugServer(deps) {
|
|
|
4040
4183
|
return `${baseText}\n\nNo page${deploymentId ? ` matching deploymentId=${deploymentId}` : ""} attached within ${timeoutSec}s${observedNote} — call list_pages to retry.`;
|
|
4041
4184
|
};
|
|
4042
4185
|
try {
|
|
4043
|
-
const { attachUrl, relayUrl, authorityWarning, totp } = buildAttachUrl(schemeUrl, getTunnelStatus(),
|
|
4186
|
+
const { attachUrl, relayUrl, authorityWarning, totp } = buildAttachUrl(schemeUrl, getTunnelStatus(), getTotpSecret());
|
|
4187
|
+
onAttachUrlBuilt?.(attachUrl);
|
|
4044
4188
|
const warningPrefix = authorityWarning ? `⚠️ scheme_url 경고: ${authorityWarning}\n\n` : "";
|
|
4045
4189
|
const header = "This tool result is shown to the user directly — do NOT re-print the QR below in your reply (it wastes output tokens). Just tell the user to scan the QR in this output (Ctrl+O to expand if collapsed).";
|
|
4046
4190
|
const guiAvailable = canOpenBrowser();
|
|
@@ -4245,26 +4389,14 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
4245
4389
|
}
|
|
4246
4390
|
/**
|
|
4247
4391
|
* Normalizes a raw `start_debug` `mode` argument to a `StartDebugMode`, or
|
|
4248
|
-
* `null` when the value is not one of the accepted modes
|
|
4249
|
-
*
|
|
4250
|
-
*
|
|
4251
|
-
*
|
|
4252
|
-
*
|
|
4253
|
-
* 'mobile' → 'mobile' (canonical)
|
|
4254
|
-
* 'staging' → 'staging' (canonical)
|
|
4255
|
-
* 'live' → 'live' (canonical)
|
|
4256
|
-
* 'local-browser-dev' → 'local' (deprecated alias)
|
|
4257
|
-
* 'local-browser-cdp' → 'local' (deprecated alias)
|
|
4258
|
-
* 'relay-mobile' → 'mobile' (deprecated alias)
|
|
4259
|
-
* 'relay-dev' → 'staging' (deprecated alias)
|
|
4260
|
-
* 'relay-live' → 'live' (deprecated alias)
|
|
4392
|
+
* `null` when the value is not one of the four accepted modes:
|
|
4393
|
+
* 'local-browser' | 'relay-sandbox' | 'relay-staging' | 'relay-live'
|
|
4394
|
+
*
|
|
4395
|
+
* Hard rename (issue #398): the older `local`/`mobile`/`staging`/`live` names
|
|
4396
|
+
* and their aliases are no longer accepted — pre-1.0, no back-compat.
|
|
4261
4397
|
*/
|
|
4262
4398
|
function normalizeStartDebugMode(raw) {
|
|
4263
|
-
if (raw === "local" || raw === "
|
|
4264
|
-
if (raw === "local-browser-dev" || raw === "local-browser-cdp") return "local";
|
|
4265
|
-
if (raw === "relay-mobile") return "mobile";
|
|
4266
|
-
if (raw === "relay-dev") return "staging";
|
|
4267
|
-
if (raw === "relay-live") return "live";
|
|
4399
|
+
if (raw === "local-browser" || raw === "relay-sandbox" || raw === "relay-staging" || raw === "relay-live") return raw;
|
|
4268
4400
|
return null;
|
|
4269
4401
|
}
|
|
4270
4402
|
/**
|
|
@@ -4282,11 +4414,11 @@ function makeSingleConnectionRouter(connection) {
|
|
|
4282
4414
|
return connection;
|
|
4283
4415
|
},
|
|
4284
4416
|
activeRelayOrigin: void 0,
|
|
4285
|
-
switchMode(mode, confirm) {
|
|
4286
|
-
if (mode === "
|
|
4417
|
+
switchMode(mode, confirm, _projectRoot) {
|
|
4418
|
+
if (mode === "relay-sandbox") return Promise.reject(/* @__PURE__ */ new Error("start_debug: 이 세션은 단일 연결만 보유합니다 — 'relay-sandbox'(환경 2 PWA, 외부 relay)로 동적 전환할 수 없습니다 (dual-connection 데몬에서만 지원). MCP 서버를 relay-sandbox 모드로 재시작하세요."));
|
|
4287
4419
|
if (isRelayMode(mode) !== (connection.kind === "relay")) return Promise.reject(/* @__PURE__ */ new Error(`start_debug: 이 세션은 단일 ${connection.kind} 연결만 보유합니다 — '${mode}'로 동적 전환할 수 없습니다 (dual-connection 데몬에서만 지원). MCP 서버를 원하는 모드로 재시작하세요.`));
|
|
4288
|
-
if (mode === "live" && !confirm) return Promise.reject(/* @__PURE__ */ new Error("start_debug: live(실서비스 LIVE)는 confirm: true가 필요합니다 — 실유저에게 영향이 갈 수 있는 LIVE 디버깅 진입을 명시적으로 승인하세요."));
|
|
4289
|
-
setLiveIntent(mode === "live");
|
|
4420
|
+
if (mode === "relay-live" && !confirm) return Promise.reject(/* @__PURE__ */ new Error("start_debug: relay-live(실서비스 LIVE)는 confirm: true가 필요합니다 — 실유저에게 영향이 갈 수 있는 LIVE 디버깅 진입을 명시적으로 승인하세요."));
|
|
4421
|
+
setLiveIntent(mode === "relay-live");
|
|
4290
4422
|
const environment = deriveEnvironment(connection.kind, getLiveIntent());
|
|
4291
4423
|
return Promise.resolve({
|
|
4292
4424
|
mode,
|
|
@@ -4426,8 +4558,9 @@ function startParentWatcher(onOrphaned, opts) {
|
|
|
4426
4558
|
* Factory that constructs a `ChiiCdpConnection` for the given relay base URL.
|
|
4427
4559
|
*
|
|
4428
4560
|
* Introduced as a named seam so PR-2 (dual-connection, #348) can defer
|
|
4429
|
-
* construction to first-activation time by moving or replacing this call
|
|
4430
|
-
*
|
|
4561
|
+
* construction to first-activation time by moving or replacing this call. Since
|
|
4562
|
+
* #396 every family (relay included) is constructed lazily on its first
|
|
4563
|
+
* `start_debug`, so this is always called from the lazy boot path.
|
|
4431
4564
|
*
|
|
4432
4565
|
* The relay base URL is only available after `startChiiRelay()` resolves, so
|
|
4433
4566
|
* the factory is called right after that point (same as before this refactor).
|
|
@@ -4455,11 +4588,9 @@ var RoutingAitSource = class extends ChiiAitSource {
|
|
|
4455
4588
|
* `--remote-debugging-port` and returns a `LocalCdpConnection` attached to it,
|
|
4456
4589
|
* plus a `stop()` that kills both.
|
|
4457
4590
|
*
|
|
4458
|
-
*
|
|
4459
|
-
*
|
|
4460
|
-
*
|
|
4461
|
-
* - `runLocalDebugServer` (local-eager, #356): the eager family booted at
|
|
4462
|
-
* startup.
|
|
4591
|
+
* Booted lazily via the dual router's `bootLazyFor('local-browser')` callback,
|
|
4592
|
+
* at most once on the first `start_debug({ mode: 'local-browser' })` (all-lazy,
|
|
4593
|
+
* #396 — no run function boots a family at startup anymore).
|
|
4463
4594
|
*/
|
|
4464
4595
|
async function bootLocalFamily() {
|
|
4465
4596
|
const chromium = await launchChromium({
|
|
@@ -4484,10 +4615,10 @@ async function bootLocalFamily() {
|
|
|
4484
4615
|
* `getTunnelStatus()` reflects the live tunnel (it flips up once the background
|
|
4485
4616
|
* tunnel resolves and follows reissues).
|
|
4486
4617
|
*
|
|
4487
|
-
*
|
|
4488
|
-
*
|
|
4489
|
-
*
|
|
4490
|
-
*
|
|
4618
|
+
* Booted lazily via the dual router's `bootLazyFor('relay-intoss')` callback
|
|
4619
|
+
* (symmetry with {@link bootLocalFamily}), at most once on the first
|
|
4620
|
+
* `start_debug({ mode: 'relay-staging' | 'relay-live' })` (all-lazy, #396 — every
|
|
4621
|
+
* relay boot now flows through `switchMode` after the project-local secret load).
|
|
4491
4622
|
*
|
|
4492
4623
|
* The relay base URL is only known after `startChiiRelay()` resolves, so the
|
|
4493
4624
|
* `ChiiCdpConnection` (via {@link createRelayConnection}) is constructed inside
|
|
@@ -4592,14 +4723,15 @@ async function bootExternalRelayFamily(relayBaseUrl) {
|
|
|
4592
4723
|
}
|
|
4593
4724
|
/**
|
|
4594
4725
|
* Maps a `StartDebugMode` to the {@link FamilyKey} that serves it (issue #378).
|
|
4595
|
-
* local → 'local';
|
|
4726
|
+
* local-browser → 'local-browser'; relay-sandbox → 'relay-sandbox';
|
|
4727
|
+
* relay-staging/relay-live → 'relay-intoss' (the shared physical slot).
|
|
4596
4728
|
*/
|
|
4597
4729
|
function familyKeyForMode(mode) {
|
|
4598
4730
|
switch (mode) {
|
|
4599
|
-
case "local": return "local";
|
|
4600
|
-
case "
|
|
4601
|
-
case "staging":
|
|
4602
|
-
case "live": return "relay-intoss";
|
|
4731
|
+
case "local-browser": return "local-browser";
|
|
4732
|
+
case "relay-sandbox": return "relay-sandbox";
|
|
4733
|
+
case "relay-staging":
|
|
4734
|
+
case "relay-live": return "relay-intoss";
|
|
4603
4735
|
}
|
|
4604
4736
|
}
|
|
4605
4737
|
/** The error thrown / surfaced when entering `mobile` without AIT_RELAY_BASE_URL. */
|
|
@@ -4621,28 +4753,55 @@ function readMobileRelayBaseUrl(env = process.env) {
|
|
|
4621
4753
|
return value;
|
|
4622
4754
|
}
|
|
4623
4755
|
/**
|
|
4756
|
+
* Sentinel connection returned by {@link DualConnectionRouter.active} before the
|
|
4757
|
+
* first `start_debug` boots a family (all-lazy, issue #396). It satisfies the
|
|
4758
|
+
* full {@link CdpConnection} interface but holds nothing: `listTargets()` is
|
|
4759
|
+
* empty, every command rejects with a clear "call start_debug first" message,
|
|
4760
|
+
* and all event/teardown members are safe no-ops. Callers that read tools before
|
|
4761
|
+
* any switchMode therefore get an honest empty/down state instead of an NPE.
|
|
4762
|
+
*/
|
|
4763
|
+
const NULL_CDP_CONNECTION = {
|
|
4764
|
+
kind: "local",
|
|
4765
|
+
enableDomains: () => Promise.resolve(),
|
|
4766
|
+
listTargets: () => [],
|
|
4767
|
+
getBufferedEvents: () => [],
|
|
4768
|
+
on: () => () => {},
|
|
4769
|
+
send: () => Promise.reject(/* @__PURE__ */ new Error("no family booted yet — call start_debug first")),
|
|
4770
|
+
close: () => {}
|
|
4771
|
+
};
|
|
4772
|
+
/**
|
|
4624
4773
|
* Production `ConnectionRouter` (issues #348, #356, #378 — DUAL-CONNECTION-COEXIST).
|
|
4625
4774
|
*
|
|
4626
|
-
* Holds
|
|
4627
|
-
*
|
|
4775
|
+
* Holds a keyed set of lazily-booted families ({@link FamilyKey} →
|
|
4776
|
+
* `BootedFamily`, issue #378) with NO family active at startup (issue #396); the
|
|
4777
|
+
* first `start_debug` boots and activates one. Plus an `active` pointer and the
|
|
4628
4778
|
* single attach watcher armed on the active connection. The router is
|
|
4629
|
-
* **direction-neutral** (#356): any family can be the
|
|
4779
|
+
* **direction-neutral** (#356): any family can be the first one booted, so a
|
|
4630
4780
|
* `--target=local` session can hot-switch into relay (and vice versa) without
|
|
4631
4781
|
* restarting the MCP server.
|
|
4632
4782
|
*
|
|
4633
|
-
* Why a KEYED map and not a single lazy slot (#378): `
|
|
4634
|
-
* relay) and `staging`/`live` (intoss relay) are BOTH
|
|
4635
|
-
* "opposite-kind" slot could not warm-keep both at
|
|
4636
|
-
*
|
|
4637
|
-
* its own warm
|
|
4783
|
+
* Why a KEYED map and not a single lazy slot (#378): `relay-sandbox` (env-2
|
|
4784
|
+
* external relay) and `relay-staging`/`relay-live` (intoss relay) are BOTH
|
|
4785
|
+
* `kind: 'relay'`. A single "opposite-kind" slot could not warm-keep both at
|
|
4786
|
+
* once — they would collide. The three `FamilyKey`s
|
|
4787
|
+
* (`local-browser` / `relay-intoss` / `relay-sandbox`) give each its own warm
|
|
4788
|
+
* slot — `relay-staging` and `relay-live` deliberately share the one
|
|
4789
|
+
* `relay-intoss` slot (wire-identical, distinguished only by `liveIntent`).
|
|
4790
|
+
*
|
|
4791
|
+
* Why all-lazy (#396): the relay TOTP secret now lives in a project-local
|
|
4792
|
+
* `.ait_relay` file loaded read-only by `switchMode` BEFORE a relay family boots.
|
|
4793
|
+
* Booting any family eagerly at startup would bypass that load. With NO eager
|
|
4794
|
+
* boot every relay boot flows through `switchMode → loadRelaySecretReadOnly`, so
|
|
4795
|
+
* the secret is always populated before `assertRelayAuthConfigured()` /
|
|
4796
|
+
* `buildRelayVerifyAuth()` run at the boot site.
|
|
4638
4797
|
*
|
|
4639
4798
|
* `switchMode`:
|
|
4640
|
-
* 1. rejects re-entrant swaps (`swapInFlight`) and an unconfirmed `live`;
|
|
4641
|
-
* 2. resolves the requested mode's `FamilyKey`:
|
|
4642
|
-
*
|
|
4799
|
+
* 1. rejects re-entrant swaps (`swapInFlight`) and an unconfirmed `relay-live`;
|
|
4800
|
+
* 2. resolves the requested mode's `FamilyKey`:
|
|
4801
|
+
* `lazyFamilies.get(key) ?? (boot via bootLazyFor(key), store)`;
|
|
4643
4802
|
* 3. flips `active` (the MCP `Server` never re-handshakes — it reads through
|
|
4644
4803
|
* `active` per request);
|
|
4645
|
-
* 4. sets `liveIntent` (true only for `live`; `
|
|
4804
|
+
* 4. sets `liveIntent` (true only for `relay-live`; `relay-sandbox` is dev-intent → false);
|
|
4646
4805
|
* 5. stops the old attach watcher and re-arms one on the new connection
|
|
4647
4806
|
* (the watcher self-clears, so re-arm is mandatory);
|
|
4648
4807
|
* 6. emits `tools/list_changed`.
|
|
@@ -4653,37 +4812,37 @@ function readMobileRelayBaseUrl(env = process.env) {
|
|
|
4653
4812
|
*/
|
|
4654
4813
|
var DualConnectionRouter = class {
|
|
4655
4814
|
deps;
|
|
4656
|
-
/**
|
|
4815
|
+
/** Families, booted lazily and warm-kept per {@link FamilyKey} (#378, #396). */
|
|
4657
4816
|
lazyFamilies = /* @__PURE__ */ new Map();
|
|
4658
|
-
|
|
4817
|
+
/** `null` until the first `start_debug` boots a family (all-lazy, #396). */
|
|
4818
|
+
activeFamily = null;
|
|
4659
4819
|
server = null;
|
|
4660
4820
|
attachWatcher = null;
|
|
4661
4821
|
swapInFlight = false;
|
|
4662
4822
|
constructor(deps) {
|
|
4663
4823
|
this.deps = deps;
|
|
4664
|
-
this.activeFamily = deps.eager;
|
|
4665
4824
|
}
|
|
4666
4825
|
get active() {
|
|
4667
|
-
return this.activeFamily.connection;
|
|
4826
|
+
return this.activeFamily ? this.activeFamily.connection : NULL_CDP_CONNECTION;
|
|
4668
4827
|
}
|
|
4669
4828
|
/** Relay origin of the currently-active family (issue #378). */
|
|
4670
4829
|
get activeRelayOrigin() {
|
|
4671
|
-
return this.activeFamily
|
|
4830
|
+
return this.activeFamily?.relayOrigin;
|
|
4672
4831
|
}
|
|
4673
|
-
/** Every booted family (for unified shutdown). */
|
|
4832
|
+
/** Every booted family (for unified shutdown). All families are lazy (#396). */
|
|
4674
4833
|
bootedFamilies() {
|
|
4675
|
-
return [
|
|
4834
|
+
return [...this.lazyFamilies.values()];
|
|
4676
4835
|
}
|
|
4677
4836
|
/**
|
|
4678
4837
|
* Live tunnel status of the active relay family (issues #356, #378). Reads
|
|
4679
|
-
* the ACTIVE family's tunnel when it has one (so `
|
|
4680
|
-
* external relay wss and `staging`/`live` the intoss relay wss); otherwise
|
|
4838
|
+
* the ACTIVE family's tunnel when it has one (so `relay-sandbox` surfaces the
|
|
4839
|
+
* external relay wss and `relay-staging`/`relay-live` the intoss relay wss); otherwise
|
|
4681
4840
|
* falls back to the first booted family that has a tunnel. Returns "down"
|
|
4682
|
-
* until any relay family is booted (
|
|
4683
|
-
*
|
|
4841
|
+
* until any relay family is booted (any session before the first relay
|
|
4842
|
+
* start_debug) — the correct signal for `build_attach_url` (no tunnel yet).
|
|
4684
4843
|
*/
|
|
4685
4844
|
relayTunnelStatus() {
|
|
4686
|
-
if (this.activeFamily
|
|
4845
|
+
if (this.activeFamily?.getTunnelStatus) return this.activeFamily.getTunnelStatus();
|
|
4687
4846
|
for (const family of this.bootedFamilies()) if (family.getTunnelStatus) return family.getTunnelStatus();
|
|
4688
4847
|
return {
|
|
4689
4848
|
up: false,
|
|
@@ -4691,8 +4850,9 @@ var DualConnectionRouter = class {
|
|
|
4691
4850
|
};
|
|
4692
4851
|
}
|
|
4693
4852
|
/**
|
|
4694
|
-
* Binds the MCP `Server
|
|
4695
|
-
*
|
|
4853
|
+
* Binds the MCP `Server`; the attach watcher is armed by the first
|
|
4854
|
+
* `start_debug` since no family is active at startup (all-lazy, #396). Called
|
|
4855
|
+
* once after `createDebugServer` + `connect`.
|
|
4696
4856
|
*/
|
|
4697
4857
|
start(server) {
|
|
4698
4858
|
this.server = server;
|
|
@@ -4707,32 +4867,36 @@ var DualConnectionRouter = class {
|
|
|
4707
4867
|
armWatcher() {
|
|
4708
4868
|
const server = this.server;
|
|
4709
4869
|
if (!server) return;
|
|
4710
|
-
|
|
4870
|
+
const activeFamily = this.activeFamily;
|
|
4871
|
+
if (!activeFamily) return;
|
|
4872
|
+
this.attachWatcher = startAttachWatcher(activeFamily.connection, server, this.deps.attachWatcherIntervalMs ?? 1e3, () => {
|
|
4711
4873
|
this.deps.diagnosticsCollector.recordAttach();
|
|
4712
|
-
|
|
4874
|
+
this.deps.onPageAttach?.();
|
|
4875
|
+
if (activeFamily.connection.kind === "relay") this.deps.devtoolsOpener.open(this.relayTunnelStatus().wssUrl, deriveEnvironment(activeFamily.connection.kind, getLiveIntent(), activeFamily.relayOrigin));
|
|
4713
4876
|
});
|
|
4714
4877
|
}
|
|
4715
4878
|
/**
|
|
4716
|
-
* Resolves the `BootedFamily` for `key`: the
|
|
4717
|
-
*
|
|
4718
|
-
*
|
|
4879
|
+
* Resolves the `BootedFamily` for `key`: the warm family if already booted,
|
|
4880
|
+
* otherwise boots it via `bootLazyFor(key)` and stores it (once per key).
|
|
4881
|
+
* Since #396 every family is lazy, so this is the single boot path for all
|
|
4882
|
+
* three keys.
|
|
4719
4883
|
*/
|
|
4720
4884
|
async familyFor(key) {
|
|
4721
|
-
if (key === this.deps.eagerKey) return this.deps.eager;
|
|
4722
4885
|
const warm = this.lazyFamilies.get(key);
|
|
4723
4886
|
if (warm) return warm;
|
|
4724
4887
|
const booted = await this.deps.bootLazyFor(key);
|
|
4725
4888
|
this.lazyFamilies.set(key, booted);
|
|
4726
4889
|
return booted;
|
|
4727
4890
|
}
|
|
4728
|
-
async switchMode(mode, confirm) {
|
|
4891
|
+
async switchMode(mode, confirm, projectRoot) {
|
|
4729
4892
|
if (this.swapInFlight) throw new Error("start_debug: 이전 전환이 아직 진행 중입니다 — 잠시 후 다시 호출하세요.");
|
|
4730
|
-
if (mode === "live" && !confirm) throw new Error("start_debug: live(실서비스 LIVE)는 confirm: true가 필요합니다 — 실유저에게 영향이 갈 수 있는 LIVE 디버깅 진입을 명시적으로 승인하세요.");
|
|
4893
|
+
if (mode === "relay-live" && !confirm) throw new Error("start_debug: relay-live(실서비스 LIVE)는 confirm: true가 필요합니다 — 실유저에게 영향이 갈 수 있는 LIVE 디버깅 진입을 명시적으로 승인하세요.");
|
|
4731
4894
|
this.swapInFlight = true;
|
|
4732
4895
|
try {
|
|
4896
|
+
if (isRelayMode(mode)) await loadRelaySecretReadOnly({ projectRoot });
|
|
4733
4897
|
const target = await this.familyFor(familyKeyForMode(mode));
|
|
4734
4898
|
this.activeFamily = target;
|
|
4735
|
-
setLiveIntent(mode === "live");
|
|
4899
|
+
setLiveIntent(mode === "relay-live");
|
|
4736
4900
|
this.stopWatcher();
|
|
4737
4901
|
this.armWatcher();
|
|
4738
4902
|
this.server?.sendToolListChanged();
|
|
@@ -4759,27 +4923,39 @@ var DualConnectionRouter = class {
|
|
|
4759
4923
|
*/
|
|
4760
4924
|
async function runDebugServer(options = {}) {
|
|
4761
4925
|
const lockHandle = acquireLock({ force: options.force ?? false });
|
|
4762
|
-
const verifyAuth = buildRelayVerifyAuth();
|
|
4763
|
-
const relayFamily = await bootRelayFamily({
|
|
4764
|
-
relayPort: options.relayPort,
|
|
4765
|
-
verifyAuth,
|
|
4766
|
-
onWssUrl: (wssUrl) => lockHandle.updateWssUrl(wssUrl)
|
|
4767
|
-
});
|
|
4768
4926
|
const devtoolsOpener = new AutoDevtoolsOpener();
|
|
4769
4927
|
const diagnosticsCollector = new InMemoryDiagnosticsCollector();
|
|
4770
4928
|
const router = new DualConnectionRouter({
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4929
|
+
bootLazyFor: (key) => key === "relay-sandbox" ? bootExternalRelayFamily(readMobileRelayBaseUrl()) : key === "local-browser" ? bootLocalFamily() : bootRelayFamily({
|
|
4930
|
+
relayPort: options.relayPort,
|
|
4931
|
+
verifyAuth: buildRelayVerifyAuth(),
|
|
4932
|
+
onWssUrl: (wssUrl) => {
|
|
4933
|
+
lockHandle.updateWssUrl(wssUrl);
|
|
4934
|
+
qrServer?.notifyStateChange();
|
|
4935
|
+
}
|
|
4936
|
+
}),
|
|
4774
4937
|
diagnosticsCollector,
|
|
4775
|
-
devtoolsOpener
|
|
4938
|
+
devtoolsOpener,
|
|
4939
|
+
onPageAttach: () => qrServer?.notifyStateChange()
|
|
4776
4940
|
});
|
|
4777
4941
|
const aitSource = new RoutingAitSource(() => {
|
|
4778
4942
|
return router.active;
|
|
4779
4943
|
});
|
|
4944
|
+
let lastAttachUrl = null;
|
|
4945
|
+
const getDashboardState = () => ({
|
|
4946
|
+
tunnel: {
|
|
4947
|
+
up: router.relayTunnelStatus().up,
|
|
4948
|
+
wssUrl: router.relayTunnelStatus().wssUrl
|
|
4949
|
+
},
|
|
4950
|
+
pages: router.active.listTargets().map((t) => ({
|
|
4951
|
+
id: t.id,
|
|
4952
|
+
url: t.url
|
|
4953
|
+
})),
|
|
4954
|
+
attachUrl: lastAttachUrl
|
|
4955
|
+
});
|
|
4780
4956
|
let qrServer;
|
|
4781
4957
|
try {
|
|
4782
|
-
qrServer = await startQrHttpServer();
|
|
4958
|
+
qrServer = await startQrHttpServer(getDashboardState);
|
|
4783
4959
|
} catch (err) {
|
|
4784
4960
|
logWarn("server.start", { msg: `QR HTTP 서버 시작 실패 (text QR fallback 사용): ${err instanceof Error ? err.message : String(err)}` });
|
|
4785
4961
|
}
|
|
@@ -4792,7 +4968,11 @@ async function runDebugServer(options = {}) {
|
|
|
4792
4968
|
return qrServer;
|
|
4793
4969
|
},
|
|
4794
4970
|
diagnosticsCollector,
|
|
4795
|
-
|
|
4971
|
+
getTotpSecret: () => process.env.AIT_DEBUG_TOTP_SECRET,
|
|
4972
|
+
onAttachUrlBuilt: (url) => {
|
|
4973
|
+
lastAttachUrl = url;
|
|
4974
|
+
qrServer?.notifyStateChange();
|
|
4975
|
+
}
|
|
4796
4976
|
});
|
|
4797
4977
|
const transport = new StdioServerTransport();
|
|
4798
4978
|
let closed = false;
|
|
@@ -4853,13 +5033,15 @@ async function runDebugServer(options = {}) {
|
|
|
4853
5033
|
}
|
|
4854
5034
|
}
|
|
4855
5035
|
/**
|
|
4856
|
-
*
|
|
4857
|
-
*
|
|
4858
|
-
*
|
|
4859
|
-
*
|
|
4860
|
-
*
|
|
4861
|
-
*
|
|
4862
|
-
* `start_debug({ mode: 'relay
|
|
5036
|
+
* Serves the debug stack over stdio with the local browser as the default
|
|
5037
|
+
* target. Since #396 NOTHING boots at startup — every family (including the
|
|
5038
|
+
* local Chromium) is lazy-booted on its first `start_debug`:
|
|
5039
|
+
* 1. `start_debug({ mode: 'local-browser' })` launches a local Chromium with
|
|
5040
|
+
* `--remote-debugging-port=<port>` and attaches a `LocalCdpConnection`;
|
|
5041
|
+
* 2. the intoss/external relay families lazy-boot on the first
|
|
5042
|
+
* `start_debug({ mode: 'relay-staging' | 'relay-live' | 'relay-sandbox' })`;
|
|
5043
|
+
* 3. all of this runs through the SAME direction-neutral
|
|
5044
|
+
* `DualConnectionRouter` that `runDebugServer` uses (issue #356).
|
|
4863
5045
|
*
|
|
4864
5046
|
* Symmetry with `runDebugServer` (#356): starting with `--target=local` no
|
|
4865
5047
|
* longer pins a single-connection router. A `--target=local` session can
|
|
@@ -4881,38 +5063,55 @@ async function runDebugServer(options = {}) {
|
|
|
4881
5063
|
*/
|
|
4882
5064
|
async function runLocalDebugServer(options = {}) {
|
|
4883
5065
|
const lockHandle = acquireLock({ force: options.force ?? false });
|
|
4884
|
-
const
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
5066
|
+
const cdpPort = options.cdpPort ?? 0;
|
|
5067
|
+
const devUrl = options.devUrl ?? process.env.AIT_DEVTOOLS_URL ?? "http://localhost:5173";
|
|
5068
|
+
const bootLocalFamilyForEntry = async () => {
|
|
5069
|
+
const chromium = await launchChromium({
|
|
5070
|
+
port: cdpPort,
|
|
5071
|
+
devUrl
|
|
5072
|
+
});
|
|
5073
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
5074
|
+
const localConnection = new LocalCdpConnection({ devtoolsHttpUrl: chromium.devtoolsUrl });
|
|
5075
|
+
return {
|
|
5076
|
+
connection: localConnection,
|
|
5077
|
+
stop() {
|
|
5078
|
+
localConnection.close();
|
|
5079
|
+
chromium.stop();
|
|
5080
|
+
}
|
|
5081
|
+
};
|
|
4896
5082
|
};
|
|
4897
|
-
const verifyAuth = buildRelayVerifyAuth();
|
|
4898
5083
|
const devtoolsOpener = new AutoDevtoolsOpener();
|
|
4899
5084
|
const diagnosticsCollector = new InMemoryDiagnosticsCollector();
|
|
4900
5085
|
const router = new DualConnectionRouter({
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
5086
|
+
bootLazyFor: (key) => key === "relay-sandbox" ? bootExternalRelayFamily(readMobileRelayBaseUrl()) : key === "local-browser" ? bootLocalFamilyForEntry() : bootRelayFamily({
|
|
5087
|
+
verifyAuth: buildRelayVerifyAuth(),
|
|
5088
|
+
onWssUrl: (wssUrl) => {
|
|
5089
|
+
lockHandle.updateWssUrl(wssUrl);
|
|
5090
|
+
qrServer?.notifyStateChange();
|
|
5091
|
+
}
|
|
4906
5092
|
}),
|
|
4907
5093
|
diagnosticsCollector,
|
|
4908
|
-
devtoolsOpener
|
|
5094
|
+
devtoolsOpener,
|
|
5095
|
+
onPageAttach: () => qrServer?.notifyStateChange()
|
|
4909
5096
|
});
|
|
4910
5097
|
const aitSource = new RoutingAitSource(() => {
|
|
4911
5098
|
return router.active;
|
|
4912
5099
|
});
|
|
5100
|
+
let lastAttachUrl = null;
|
|
5101
|
+
const getDashboardState = () => ({
|
|
5102
|
+
tunnel: {
|
|
5103
|
+
up: router.relayTunnelStatus().up,
|
|
5104
|
+
wssUrl: router.relayTunnelStatus().wssUrl
|
|
5105
|
+
},
|
|
5106
|
+
pages: router.active.listTargets().map((t) => ({
|
|
5107
|
+
id: t.id,
|
|
5108
|
+
url: t.url
|
|
5109
|
+
})),
|
|
5110
|
+
attachUrl: lastAttachUrl
|
|
5111
|
+
});
|
|
4913
5112
|
let qrServer;
|
|
4914
5113
|
try {
|
|
4915
|
-
qrServer = await startQrHttpServer();
|
|
5114
|
+
qrServer = await startQrHttpServer(getDashboardState);
|
|
4916
5115
|
} catch (err) {
|
|
4917
5116
|
logWarn("server.start", { msg: `QR HTTP 서버 시작 실패 (text QR fallback 사용): ${err instanceof Error ? err.message : String(err)}` });
|
|
4918
5117
|
}
|
|
@@ -4925,7 +5124,11 @@ async function runLocalDebugServer(options = {}) {
|
|
|
4925
5124
|
return qrServer;
|
|
4926
5125
|
},
|
|
4927
5126
|
diagnosticsCollector,
|
|
4928
|
-
|
|
5127
|
+
getTotpSecret: () => process.env.AIT_DEBUG_TOTP_SECRET,
|
|
5128
|
+
onAttachUrlBuilt: (url) => {
|
|
5129
|
+
lastAttachUrl = url;
|
|
5130
|
+
qrServer?.notifyStateChange();
|
|
5131
|
+
}
|
|
4929
5132
|
});
|
|
4930
5133
|
const transport = new StdioServerTransport();
|
|
4931
5134
|
let closed = false;
|
|
@@ -4956,7 +5159,7 @@ async function runLocalDebugServer(options = {}) {
|
|
|
4956
5159
|
logError("tool.error", {
|
|
4957
5160
|
msg: `uncaughtException: ${String(err)}`,
|
|
4958
5161
|
errorKind: "uncaught",
|
|
4959
|
-
mode: "local"
|
|
5162
|
+
mode: "local-browser"
|
|
4960
5163
|
});
|
|
4961
5164
|
shutdown();
|
|
4962
5165
|
process.exit(1);
|
|
@@ -4965,7 +5168,7 @@ async function runLocalDebugServer(options = {}) {
|
|
|
4965
5168
|
logError("tool.error", {
|
|
4966
5169
|
msg: `unhandledRejection: ${String(reason)}`,
|
|
4967
5170
|
errorKind: "unhandled-rejection",
|
|
4968
|
-
mode: "local"
|
|
5171
|
+
mode: "local-browser"
|
|
4969
5172
|
});
|
|
4970
5173
|
shutdown();
|
|
4971
5174
|
process.exit(1);
|
|
@@ -4988,8 +5191,10 @@ async function runLocalDebugServer(options = {}) {
|
|
|
4988
5191
|
}
|
|
4989
5192
|
}
|
|
4990
5193
|
/**
|
|
4991
|
-
*
|
|
4992
|
-
* (issue #378).
|
|
5194
|
+
* Serves the env-2 (real-device PWA) debug stack over stdio with the external
|
|
5195
|
+
* Chii relay as the default target (issue #378). Since #396 NOTHING boots at
|
|
5196
|
+
* startup — the external relay family is lazy-booted on the first
|
|
5197
|
+
* `start_debug({ mode: 'relay-sandbox' })`.
|
|
4993
5198
|
*
|
|
4994
5199
|
* Unlike `runDebugServer` (which starts its own relay + cloudflared tunnel),
|
|
4995
5200
|
* `runMobileDebugServer` attaches to a relay the unplugin ALREADY brought up
|
|
@@ -4997,11 +5202,13 @@ async function runLocalDebugServer(options = {}) {
|
|
|
4997
5202
|
* opens a CDP client against that external relay — it never starts or tears down
|
|
4998
5203
|
* a relay or a tunnel it did not own (see {@link bootExternalRelayFamily}).
|
|
4999
5204
|
*
|
|
5000
|
-
* Symmetry with `runDebugServer` / `runLocalDebugServer` (#356, #378):
|
|
5001
|
-
*
|
|
5002
|
-
*
|
|
5003
|
-
* so a `--target=mobile`
|
|
5004
|
-
*
|
|
5205
|
+
* Symmetry with `runDebugServer` / `runLocalDebugServer` (#356, #378, #396): all
|
|
5206
|
+
* three families are lazy-booted — the env-2 external relay on the first
|
|
5207
|
+
* `start_debug({ mode: 'relay-sandbox' })`, the local family on `local-browser`,
|
|
5208
|
+
* the intoss relay on `relay-staging`/`relay-live` — so a `--target=mobile`
|
|
5209
|
+
* session can hot-switch
|
|
5210
|
+
* without a restart. The active env derives to `relay-mobile` (external-PWA
|
|
5211
|
+
* origin, liveIntent off).
|
|
5005
5212
|
*
|
|
5006
5213
|
* SECRET-HANDLING: `AIT_RELAY_BASE_URL` is read once here via
|
|
5007
5214
|
* {@link readMobileRelayBaseUrl}; when unset it throws
|
|
@@ -5013,26 +5220,38 @@ async function runLocalDebugServer(options = {}) {
|
|
|
5013
5220
|
async function runMobileDebugServer(options = {}) {
|
|
5014
5221
|
const relayBaseUrl = readMobileRelayBaseUrl();
|
|
5015
5222
|
const lockHandle = acquireLock({ force: options.force ?? false });
|
|
5016
|
-
const externalRelayFamily = await bootExternalRelayFamily(relayBaseUrl);
|
|
5017
|
-
const verifyAuth = buildRelayVerifyAuth();
|
|
5018
5223
|
const devtoolsOpener = new AutoDevtoolsOpener();
|
|
5019
5224
|
const diagnosticsCollector = new InMemoryDiagnosticsCollector();
|
|
5020
5225
|
const router = new DualConnectionRouter({
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5226
|
+
bootLazyFor: (key) => key === "relay-sandbox" ? bootExternalRelayFamily(relayBaseUrl) : key === "local-browser" ? bootLocalFamily() : bootRelayFamily({
|
|
5227
|
+
verifyAuth: buildRelayVerifyAuth(),
|
|
5228
|
+
onWssUrl: (wssUrl) => {
|
|
5229
|
+
lockHandle.updateWssUrl(wssUrl);
|
|
5230
|
+
qrServer?.notifyStateChange();
|
|
5231
|
+
}
|
|
5026
5232
|
}),
|
|
5027
5233
|
diagnosticsCollector,
|
|
5028
|
-
devtoolsOpener
|
|
5234
|
+
devtoolsOpener,
|
|
5235
|
+
onPageAttach: () => qrServer?.notifyStateChange()
|
|
5029
5236
|
});
|
|
5030
5237
|
const aitSource = new RoutingAitSource(() => {
|
|
5031
5238
|
return router.active;
|
|
5032
5239
|
});
|
|
5240
|
+
let lastAttachUrl = null;
|
|
5241
|
+
const getDashboardState = () => ({
|
|
5242
|
+
tunnel: {
|
|
5243
|
+
up: router.relayTunnelStatus().up,
|
|
5244
|
+
wssUrl: router.relayTunnelStatus().wssUrl
|
|
5245
|
+
},
|
|
5246
|
+
pages: router.active.listTargets().map((t) => ({
|
|
5247
|
+
id: t.id,
|
|
5248
|
+
url: t.url
|
|
5249
|
+
})),
|
|
5250
|
+
attachUrl: lastAttachUrl
|
|
5251
|
+
});
|
|
5033
5252
|
let qrServer;
|
|
5034
5253
|
try {
|
|
5035
|
-
qrServer = await startQrHttpServer();
|
|
5254
|
+
qrServer = await startQrHttpServer(getDashboardState);
|
|
5036
5255
|
} catch (err) {
|
|
5037
5256
|
logWarn("server.start", { msg: `QR HTTP 서버 시작 실패 (text QR fallback 사용): ${err instanceof Error ? err.message : String(err)}` });
|
|
5038
5257
|
}
|
|
@@ -5045,7 +5264,11 @@ async function runMobileDebugServer(options = {}) {
|
|
|
5045
5264
|
return qrServer;
|
|
5046
5265
|
},
|
|
5047
5266
|
diagnosticsCollector,
|
|
5048
|
-
|
|
5267
|
+
getTotpSecret: () => process.env.AIT_DEBUG_TOTP_SECRET,
|
|
5268
|
+
onAttachUrlBuilt: (url) => {
|
|
5269
|
+
lastAttachUrl = url;
|
|
5270
|
+
qrServer?.notifyStateChange();
|
|
5271
|
+
}
|
|
5049
5272
|
});
|
|
5050
5273
|
const transport = new StdioServerTransport();
|
|
5051
5274
|
let closed = false;
|
|
@@ -5076,7 +5299,7 @@ async function runMobileDebugServer(options = {}) {
|
|
|
5076
5299
|
logError("tool.error", {
|
|
5077
5300
|
msg: `uncaughtException: ${String(err)}`,
|
|
5078
5301
|
errorKind: "uncaught",
|
|
5079
|
-
mode: "
|
|
5302
|
+
mode: "relay-sandbox"
|
|
5080
5303
|
});
|
|
5081
5304
|
shutdown();
|
|
5082
5305
|
process.exit(1);
|
|
@@ -5085,7 +5308,7 @@ async function runMobileDebugServer(options = {}) {
|
|
|
5085
5308
|
logError("tool.error", {
|
|
5086
5309
|
msg: `unhandledRejection: ${String(reason)}`,
|
|
5087
5310
|
errorKind: "unhandled-rejection",
|
|
5088
|
-
mode: "
|
|
5311
|
+
mode: "relay-sandbox"
|
|
5089
5312
|
});
|
|
5090
5313
|
shutdown();
|
|
5091
5314
|
process.exit(1);
|
|
@@ -5535,7 +5758,7 @@ function createDevServer(deps = {}) {
|
|
|
5535
5758
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
5536
5759
|
const server = new Server({
|
|
5537
5760
|
name: "ait-devtools",
|
|
5538
|
-
version: "0.1.
|
|
5761
|
+
version: "0.1.57"
|
|
5539
5762
|
}, { capabilities: { tools: {} } });
|
|
5540
5763
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
5541
5764
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|