@ait-co/devtools 0.1.109 → 0.1.110
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 +13 -31
- package/README.md +13 -31
- package/dist/in-app/auto.d.ts.map +1 -1
- package/dist/in-app/auto.js +40 -3
- package/dist/in-app/auto.js.map +1 -1
- package/dist/in-app/index.d.ts.map +1 -1
- package/dist/in-app/index.js +39 -2
- package/dist/in-app/index.js.map +1 -1
- package/dist/mcp/cli.d.ts +4 -16
- package/dist/mcp/cli.d.ts.map +1 -1
- package/dist/mcp/cli.js +623 -678
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +47 -59
- package/dist/mcp/server.js.map +1 -1
- package/dist/mock/index.d.ts.map +1 -1
- package/dist/mock/index.js +21 -2
- package/dist/mock/index.js.map +1 -1
- package/dist/panel/index.js +47 -32
- package/dist/panel/index.js.map +1 -1
- package/dist/{pool-CuVMzWGB.d.ts → pool-Bf6rQci4.d.ts} +206 -44
- package/dist/pool-Bf6rQci4.d.ts.map +1 -0
- package/dist/{qr-http-server-D4EAA7Il.js → qr-http-server-BJJt3ush.js} +8 -17
- package/dist/qr-http-server-BJJt3ush.js.map +1 -0
- package/dist/{qr-http-server-A9vld8r7.cjs → qr-http-server-BVS-HZjU.cjs} +8 -17
- package/dist/qr-http-server-BVS-HZjU.cjs.map +1 -0
- package/dist/{qr-http-server-Dj3Z0NHi.cjs → qr-http-server-C1T4RNbq.cjs} +8 -17
- package/dist/qr-http-server-C1T4RNbq.cjs.map +1 -0
- package/dist/{qr-http-server-HzdCLU8s.js → qr-http-server-Cs93vEPH.js} +8 -17
- package/dist/qr-http-server-Cs93vEPH.js.map +1 -0
- package/dist/test-runner/config.d.ts +1 -1
- package/dist/test-runner/pool.d.ts +1 -1
- package/dist/{tunnel-BjJROkcj.js → tunnel-Cpn3mA4u.js} +3 -3
- package/dist/tunnel-Cpn3mA4u.js.map +1 -0
- package/dist/{tunnel-d_G9AIFn.cjs → tunnel-Dj8Kf2QS.cjs} +3 -3
- package/dist/tunnel-Dj8Kf2QS.cjs.map +1 -0
- package/dist/unplugin/index.cjs +1 -1
- package/dist/unplugin/index.d.cts +196 -34
- package/dist/unplugin/index.d.cts.map +1 -1
- package/dist/unplugin/index.d.ts +196 -34
- package/dist/unplugin/index.d.ts.map +1 -1
- package/dist/unplugin/index.js +1 -1
- package/dist/unplugin/tunnel.cjs +2 -2
- package/dist/unplugin/tunnel.cjs.map +1 -1
- package/dist/unplugin/tunnel.d.cts +1 -1
- package/dist/unplugin/tunnel.d.ts +1 -1
- package/dist/unplugin/tunnel.js +2 -2
- package/dist/unplugin/tunnel.js.map +1 -1
- package/package.json +14 -14
- package/dist/pool-CuVMzWGB.d.ts.map +0 -1
- package/dist/qr-http-server-A9vld8r7.cjs.map +0 -1
- package/dist/qr-http-server-D4EAA7Il.js.map +0 -1
- package/dist/qr-http-server-Dj3Z0NHi.cjs.map +0 -1
- package/dist/qr-http-server-HzdCLU8s.js.map +0 -1
- package/dist/tunnel-BjJROkcj.js.map +0 -1
- package/dist/tunnel-d_G9AIFn.cjs.map +0 -1
package/dist/mcp/cli.js
CHANGED
|
@@ -24,6 +24,95 @@ import { Tunnel, bin, install } from "cloudflared";
|
|
|
24
24
|
//#region \0rolldown/runtime.js
|
|
25
25
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
26
26
|
//#endregion
|
|
27
|
+
//#region src/in-app/gate.ts
|
|
28
|
+
/**
|
|
29
|
+
* The host suffix the Toss app uses to serve dogfood / private mini-apps.
|
|
30
|
+
*
|
|
31
|
+
* A `intoss-private://` (dogfood) entry maps to a host such as
|
|
32
|
+
* `aitc-sdk-example.private-apps.tossmini.com`. A production `intoss://`
|
|
33
|
+
* entry is served from `*.apps.tossmini.com` — the `.private-apps.` segment
|
|
34
|
+
* is absent. Confirmed live over CDP for mini-app 31146; the exact production
|
|
35
|
+
* host is to be re-confirmed once 31146 passes review (spec open question 2).
|
|
36
|
+
*/
|
|
37
|
+
const PRIVATE_APPS_HOST_SUFFIX = ".private-apps.tossmini.com";
|
|
38
|
+
/**
|
|
39
|
+
* The host suffix Cloudflare quick-tunnels serve from — the env 2 (PWA) entry.
|
|
40
|
+
* See {@link isTrycloudflareHost} for why this host kind bypasses Layer B1.
|
|
41
|
+
*/
|
|
42
|
+
const TRYCLOUDFLARE_HOST_SUFFIX = ".trycloudflare.com";
|
|
43
|
+
/**
|
|
44
|
+
* Returns whether `hostname` is a `*.private-apps.tossmini.com` subdomain —
|
|
45
|
+
* the host the Toss app reserves for dogfood / private mini-app entries.
|
|
46
|
+
*
|
|
47
|
+
* The match is an exact suffix check, not a substring `.includes()`: a
|
|
48
|
+
* substring test would also accept an attacker-controlled host like
|
|
49
|
+
* `private-apps.tossmini.com.evil.example`, which ends in `.example`, not in
|
|
50
|
+
* `.tossmini.com`. Requiring the string to END with the suffix closes that.
|
|
51
|
+
* The leading `.` in the suffix also forces a real subdomain label, so a
|
|
52
|
+
* bare `private-apps.tossmini.com` (no mini-app subdomain) does not match.
|
|
53
|
+
*/
|
|
54
|
+
function isPrivateAppsHost(hostname) {
|
|
55
|
+
return hostname.endsWith(PRIVATE_APPS_HOST_SUFFIX);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The host suffix Cloudflare quick-tunnels use — the env 2 (PWA) entry.
|
|
59
|
+
*
|
|
60
|
+
* Env 2 serves the local Vite dev server through a `*.trycloudflare.com` quick
|
|
61
|
+
* tunnel (`src/unplugin/tunnel.ts`). It has no Toss app, no `intoss-private://`
|
|
62
|
+
* scheme, and — critically — no production runtime: the SDK is the devtools
|
|
63
|
+
* mock, and the page is the developer's own dev build. The Layer B1 safety net
|
|
64
|
+
* (which stops a dogfood build that lands on a Toss *production* host from
|
|
65
|
+
* attaching) has nothing to protect against here, because env 2 has no
|
|
66
|
+
* production host. So a trycloudflare host is allowed past B1 — but ONLY past
|
|
67
|
+
* B1: the remaining layers (C1 opt-in, C2 relay, C3 TOTP) still apply, so a
|
|
68
|
+
* leaked tunnel URL is still blocked by TOTP exactly as on the Toss path.
|
|
69
|
+
*
|
|
70
|
+
* The match is the same exact-suffix `endsWith` check as
|
|
71
|
+
* {@link isPrivateAppsHost} — never a substring `.includes()`, which would
|
|
72
|
+
* accept an attacker-controlled `evil.trycloudflare.com.example.com`. The
|
|
73
|
+
* leading `.` forces a real subdomain label, so a bare `trycloudflare.com`
|
|
74
|
+
* (no tunnel subdomain) does not match.
|
|
75
|
+
*/
|
|
76
|
+
function isTrycloudflareHost(hostname) {
|
|
77
|
+
return hostname.endsWith(TRYCLOUDFLARE_HOST_SUFFIX);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Returns true when the hostname is a localhost/loopback address.
|
|
81
|
+
* Allowed: `localhost`, `127.x.x.x` (full RFC 5735 loopback block), `[::1]`,
|
|
82
|
+
* `0.0.0.0`, `*.localhost`.
|
|
83
|
+
*
|
|
84
|
+
* Security note: `hostname.startsWith('127.')` is intentionally NOT used —
|
|
85
|
+
* that pattern would accept `127.evil.com`, which starts with "127." but is an
|
|
86
|
+
* attacker-controlled hostname, not a loopback address. Instead, the 127/8
|
|
87
|
+
* loopback block is matched with a strict numeric-quad regex so only valid
|
|
88
|
+
* dotted-decimal IPv4 in the 127.x.x.x range pass (#665 작업 A fix).
|
|
89
|
+
*/
|
|
90
|
+
function isLocalhostHost(hostname) {
|
|
91
|
+
if (hostname === "localhost" || hostname === "0.0.0.0") return true;
|
|
92
|
+
if (hostname === "[::1]") return true;
|
|
93
|
+
if (/^127\.\d+\.\d+\.\d+$/.test(hostname)) return true;
|
|
94
|
+
if (hostname.endsWith(".localhost")) return true;
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Positive-allowlist kill-switch (#665): returns true when the hostname is a
|
|
99
|
+
* known debug-allowed host. The debug surface is ONLY active on:
|
|
100
|
+
* - localhost / loopback (env 1 desktop dev)
|
|
101
|
+
* - *.trycloudflare.com (env 2 PWA tunnel)
|
|
102
|
+
* - *.private-apps.tossmini.com (env 3 dog-food)
|
|
103
|
+
*
|
|
104
|
+
* Any other host (including apps.tossmini.com — the former env 4 LIVE host)
|
|
105
|
+
* is silently blocked. This is a positive allowlist — unlisted hosts never
|
|
106
|
+
* had debug surface regardless, but this function makes it explicit and
|
|
107
|
+
* auditable in a single place.
|
|
108
|
+
*
|
|
109
|
+
* SECRET-HANDLING: the hostname value MUST NOT be logged or included in any
|
|
110
|
+
* error reason string — only benign labels ('host not in allowlist') are safe.
|
|
111
|
+
*/
|
|
112
|
+
function isDebugAllowedHost(hostname) {
|
|
113
|
+
return isLocalhostHost(hostname) || isTrycloudflareHost(hostname) || isPrivateAppsHost(hostname);
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
27
116
|
//#region src/shared/parent-watcher.ts
|
|
28
117
|
/**
|
|
29
118
|
* Shared parent-PID watcher — used by both the MCP debug daemon and the
|
|
@@ -817,7 +906,7 @@ function logError(event, fields = {}) {
|
|
|
817
906
|
* Attach reliability (#281):
|
|
818
907
|
* `refreshTargets()` emits an internal 'target:attached' event whenever a
|
|
819
908
|
* new target is added to the relay. `waitForFirstTarget()` awaits that event
|
|
820
|
-
* (with a polling-interval fallback) so `
|
|
909
|
+
* (with a polling-interval fallback) so `start_attach`'s attach wait
|
|
821
910
|
* resolves deterministically rather than racing between polling rounds.
|
|
822
911
|
*/
|
|
823
912
|
/** Max events retained per domain ring buffer. */
|
|
@@ -1957,9 +2046,10 @@ function isCompatMode() {
|
|
|
1957
2046
|
return process.env.AIT_MCP_COMPAT === "chrome-devtools";
|
|
1958
2047
|
}
|
|
1959
2048
|
/**
|
|
1960
|
-
* Maps `McpEnvironment` to `EnvelopeEnv`.
|
|
1961
|
-
* union (`mock | relay-dev | relay-live`
|
|
1962
|
-
* named export for surface stability if
|
|
2049
|
+
* Maps `McpEnvironment` to `EnvelopeEnv`. These are now the same 3-value
|
|
2050
|
+
* union (`mock | relay-dev | relay-mobile`; `relay-live` removed in #665),
|
|
2051
|
+
* so this is identity — kept as a named export for surface stability if
|
|
2052
|
+
* envelope env diverges in the future.
|
|
1963
2053
|
*/
|
|
1964
2054
|
function toEnvelopeEnv(env) {
|
|
1965
2055
|
return env;
|
|
@@ -1995,9 +2085,9 @@ function wrapEnvelope(data, ctx) {
|
|
|
1995
2085
|
//#endregion
|
|
1996
2086
|
//#region src/mcp/environment.ts
|
|
1997
2087
|
/**
|
|
1998
|
-
* Returns `true` when the environment is any relay variant (`relay-dev
|
|
1999
|
-
* `relay-
|
|
2000
|
-
*
|
|
2088
|
+
* Returns `true` when the environment is any relay variant (`relay-dev` or
|
|
2089
|
+
* `relay-mobile`). Use this instead of `env === 'relay'` for tier checks —
|
|
2090
|
+
* every relay env surfaces the Tier B / relay-only tool set.
|
|
2001
2091
|
*
|
|
2002
2092
|
* Written as an exhaustive switch so a future `McpEnvironment` member that is
|
|
2003
2093
|
* missing an arm is a TS compile error rather than a silent `false`.
|
|
@@ -2005,78 +2095,49 @@ function wrapEnvelope(data, ctx) {
|
|
|
2005
2095
|
function isRelayEnv(env) {
|
|
2006
2096
|
switch (env) {
|
|
2007
2097
|
case "relay-dev":
|
|
2008
|
-
case "relay-live":
|
|
2009
2098
|
case "relay-mobile": return true;
|
|
2010
2099
|
case "mock": return false;
|
|
2011
2100
|
}
|
|
2012
2101
|
}
|
|
2013
2102
|
/**
|
|
2014
|
-
* Returns `true` when the environment is the LIVE relay (`relay-live`).
|
|
2015
|
-
* This is the guard condition for side-effect tool protection. `relay-mobile`
|
|
2016
|
-
* is a dev-intent env (env 2 PWA) and is NOT live.
|
|
2017
|
-
*/
|
|
2018
|
-
function isLiveRelayEnv(env) {
|
|
2019
|
-
return env === "relay-live";
|
|
2020
|
-
}
|
|
2021
|
-
/**
|
|
2022
2103
|
* Maps the `McpEnvironment` union to the legacy two-value union
|
|
2023
2104
|
* (`'mock' | 'relay'`) for backward-compatible fields in diagnostics output.
|
|
2024
|
-
* Every relay variant (
|
|
2105
|
+
* Every relay variant (`relay-dev`, `relay-mobile`) collapses to `'relay'`.
|
|
2106
|
+
* Written as an exhaustive switch so a missing arm is a TS compile error.
|
|
2025
2107
|
*/
|
|
2026
2108
|
function toLegacyEnv(env) {
|
|
2027
|
-
|
|
2028
|
-
|
|
2109
|
+
switch (env) {
|
|
2110
|
+
case "mock": return "mock";
|
|
2111
|
+
case "relay-dev":
|
|
2112
|
+
case "relay-mobile": return "relay";
|
|
2113
|
+
}
|
|
2029
2114
|
}
|
|
2030
2115
|
/**
|
|
2031
|
-
* Reconstructs the
|
|
2032
|
-
* orthogonal signals (issues #348, #378):
|
|
2116
|
+
* Reconstructs the three-value `McpEnvironment` output string from the
|
|
2117
|
+
* orthogonal signals (issues #348, #378, #665):
|
|
2033
2118
|
*
|
|
2034
|
-
* - `kind === 'local'`
|
|
2035
|
-
* - `kind === 'relay'` &&
|
|
2036
|
-
* - `kind === 'relay'` &&
|
|
2037
|
-
* - `kind === 'relay'` && !liveIntent && origin intoss/undefined → `'relay-dev'`
|
|
2119
|
+
* - `kind === 'local'` → `'mock'`
|
|
2120
|
+
* - `kind === 'relay'` && origin 'external-pwa' → `'relay-mobile'`
|
|
2121
|
+
* - `kind === 'relay'` && origin intoss/undefined → `'relay-dev'`
|
|
2038
2122
|
*
|
|
2039
2123
|
* `relayOrigin` is the booted-family discriminator (NOT sniffed from the URL)
|
|
2040
2124
|
* that distinguishes the env-2 external-PWA relay (`relay-mobile`) from the
|
|
2041
2125
|
* intoss-private dog-food relay (`relay-dev`); both are `kind: 'relay'`.
|
|
2042
2126
|
*
|
|
2127
|
+
* `relay-live` (env 4) has been removed (#665). `liveIntent` parameter is gone.
|
|
2128
|
+
*
|
|
2043
2129
|
* Pure — used at every output boundary (envelope `meta.env`, `get_debug_status`,
|
|
2044
2130
|
* `measure_safe_area` provenance) so the surface never sniffs a URL again.
|
|
2045
2131
|
*
|
|
2046
2132
|
* Written switch-style so a missing arm is a TS compile error (never falls
|
|
2047
2133
|
* through to a default).
|
|
2048
2134
|
*/
|
|
2049
|
-
function deriveEnvironment(kind,
|
|
2135
|
+
function deriveEnvironment(kind, relayOrigin) {
|
|
2050
2136
|
switch (kind) {
|
|
2051
2137
|
case "local": return "mock";
|
|
2052
|
-
case "relay":
|
|
2053
|
-
if (liveIntent) return "relay-live";
|
|
2054
|
-
return relayOrigin === "external-pwa" ? "relay-mobile" : "relay-dev";
|
|
2138
|
+
case "relay": return relayOrigin === "external-pwa" ? "relay-mobile" : "relay-dev";
|
|
2055
2139
|
}
|
|
2056
2140
|
}
|
|
2057
|
-
/**
|
|
2058
|
-
* Module-level `relay-dev` vs `relay-live` intent bit (issue #348).
|
|
2059
|
-
*
|
|
2060
|
-
* Armed by `start_debug({ mode: 'relay-live' })` (and seeded at boot by the
|
|
2061
|
-
* deprecated `MCP_ENV=relay-live` alias). Disarming is implicit: when the
|
|
2062
|
-
* active connection becomes local, the LIVE guard reads
|
|
2063
|
-
* `connection.kind === 'relay' && liveIntent`, so a stale `true` bit is inert.
|
|
2064
|
-
*
|
|
2065
|
-
* SECRET-HANDLING: this is a boolean — never a secret. Safe to read in logs.
|
|
2066
|
-
*/
|
|
2067
|
-
let liveIntent = false;
|
|
2068
|
-
/** Returns the current `liveIntent` bit. */
|
|
2069
|
-
function getLiveIntent() {
|
|
2070
|
-
return liveIntent;
|
|
2071
|
-
}
|
|
2072
|
-
/**
|
|
2073
|
-
* Sets the `liveIntent` bit. Called by `start_debug` (true for `relay-live`,
|
|
2074
|
-
* false for every other mode) and once at boot by the `MCP_ENV=relay-live`
|
|
2075
|
-
* deprecated alias.
|
|
2076
|
-
*/
|
|
2077
|
-
function setLiveIntent(value) {
|
|
2078
|
-
liveIntent = value;
|
|
2079
|
-
}
|
|
2080
2141
|
//#endregion
|
|
2081
2142
|
//#region src/mcp/errors.ts
|
|
2082
2143
|
/**
|
|
@@ -2103,12 +2164,12 @@ function mcpError(message) {
|
|
|
2103
2164
|
* (예: `derived:kind=relay,liveIntent=true`).
|
|
2104
2165
|
*/
|
|
2105
2166
|
function tierRejectionError(toolName, requiredEnv, currentEnv, reason) {
|
|
2106
|
-
return mcpError(`${`${toolName}은 ${requiredEnv === "relay" ? "relay (실기기 연결)" : "mock (로컬 브라우저)"} 환경에서만 사용할 수 있습니다. 현재 환경: ${currentEnv === "relay" ? "relay" : "mock"} (${reason}). ${requiredEnv === "relay" ? "relay로 전환하려면 MCP_ENV=relay 설정 후 서버를 재시작하고
|
|
2167
|
+
return mcpError(`${`${toolName}은 ${requiredEnv === "relay" ? "relay (실기기 연결)" : "mock (로컬 브라우저)"} 환경에서만 사용할 수 있습니다. 현재 환경: ${currentEnv === "relay" ? "relay" : "mock"} (${reason}). ${requiredEnv === "relay" ? "relay로 전환하려면 MCP_ENV=relay 설정 후 서버를 재시작하고 start_attach → QR 스캔으로 실기기를 attach하세요." : "mock으로 전환하려면 MCP_ENV=mock 설정 후 서버를 재시작하세요."}`}\n\n${`tool ${toolName} is available only in ${requiredEnv}. Current environment is ${currentEnv} (${reason}).`}`);
|
|
2107
2168
|
}
|
|
2108
2169
|
/**
|
|
2109
2170
|
* 상태 1: tunnel 미가동 — cloudflared 터널이 아직 뜨지 않았다.
|
|
2110
2171
|
*
|
|
2111
|
-
* `
|
|
2172
|
+
* `start_attach` 호출 시 tunnel.up === false 인 경우.
|
|
2112
2173
|
*/
|
|
2113
2174
|
function tunnelDownError() {
|
|
2114
2175
|
return mcpError("cloudflared 터널이 안 떠 있습니다. MCP 서버를 재시작하거나 잠시 후 list_pages로 터널 상태를 다시 확인하세요.");
|
|
@@ -2119,7 +2180,7 @@ function tunnelDownError() {
|
|
|
2119
2180
|
* enableDomains()가 "No mini-app page attached" 에러를 던질 때.
|
|
2120
2181
|
*/
|
|
2121
2182
|
function pageMissingError(toolName) {
|
|
2122
|
-
return mcpError(`${toolName ? `${toolName}: ` : ""}페이지가 attach 안 됨. dog-food 번들 배포 후
|
|
2183
|
+
return mcpError(`${toolName ? `${toolName}: ` : ""}페이지가 attach 안 됨. dog-food 번들 배포 후 start_attach를 호출해 QR 생성 + attach까지 진행하세요: \`ait deploy --scheme-only\` → \`start_attach(scheme_url)\` → QR 스캔.`);
|
|
2123
2184
|
}
|
|
2124
2185
|
/**
|
|
2125
2186
|
* 상태 3: page crash — 연결됐던 페이지가 crash/destroy됐다.
|
|
@@ -2128,7 +2189,7 @@ function pageMissingError(toolName) {
|
|
|
2128
2189
|
* 던질 때 이 메시지를 사용한다.
|
|
2129
2190
|
*/
|
|
2130
2191
|
function pageCrashError(toolName) {
|
|
2131
|
-
return mcpError(`${toolName ? `${toolName}: ` : ""}페이지가 crash됐습니다. 토스 앱을 재실행한 뒤
|
|
2192
|
+
return mcpError(`${toolName ? `${toolName}: ` : ""}페이지가 crash됐습니다. 토스 앱을 재실행한 뒤 start_attach → QR 스캔으로 재attach하세요.`);
|
|
2132
2193
|
}
|
|
2133
2194
|
/**
|
|
2134
2195
|
* 상태 4: SDK 부재 — window.__sdkCall이 주입되지 않았다.
|
|
@@ -2150,23 +2211,6 @@ function sdkAbsentError(toolName, isLocal = false) {
|
|
|
2150
2211
|
return mcpError(`${prefix}window.__sdkCall이 주입되지 않았습니다 (dog-food 빌드가 아닙니다). dog-food 채널(intoss-private)로 재배포 후 QR을 다시 스캔하세요: \`ait build && aitcc app deploy\`.`);
|
|
2151
2212
|
}
|
|
2152
2213
|
/**
|
|
2153
|
-
* relay-live 환경에서 side-effect 도구(`call_sdk`, `evaluate`)를 `confirm: true`
|
|
2154
|
-
* 없이 호출했을 때 반환하는 거부 메시지.
|
|
2155
|
-
*
|
|
2156
|
-
* 다음 행동을 두 가지로 제시한다:
|
|
2157
|
-
* 1. 같은 호출에 `confirm: true` 인자를 추가해 재시도.
|
|
2158
|
-
* 2. 읽기 전용 환경(relay-dev, mock)으로 전환.
|
|
2159
|
-
*/
|
|
2160
|
-
function liveGuardError(toolName) {
|
|
2161
|
-
return mcpError(`[LIVE relay guard] ${toolName}은 현재 relay-live(실 출시 런타임) 세션에서 side-effect 호출입니다. 실유저에게 영향을 줄 수 있어 명시적 동의가 필요합니다.
|
|
2162
|
-
|
|
2163
|
-
다음 중 하나를 선택하세요:
|
|
2164
|
-
1. \`confirm: true\` 인자를 추가해 재호출: ${toolName}(…, confirm: true)\n 2. 읽기 전용 도구(list_pages, list_console_messages, take_screenshot 등)를 사용하세요.
|
|
2165
|
-
3. dog-food 빌드(relay-dev 환경)에서 먼저 검증 후 live에 적용하세요.
|
|
2166
|
-
|
|
2167
|
-
live-guard: MCP_ENV=relay-live + confirm: true missing`);
|
|
2168
|
-
}
|
|
2169
|
-
/**
|
|
2170
2214
|
* relay WebSocket 연결이 끊겼을 때 — 크래시가 아닌 네트워크/프로세스 종료.
|
|
2171
2215
|
*/
|
|
2172
2216
|
function relayDisconnectError(toolName) {
|
|
@@ -2676,7 +2720,7 @@ const en = {
|
|
|
2676
2720
|
"dashboard.tunnel.up": "Connected",
|
|
2677
2721
|
"dashboard.tunnel.down": "Disconnected",
|
|
2678
2722
|
"dashboard.attach.section": "Attach QR",
|
|
2679
|
-
"dashboard.attach.hint": "Call the
|
|
2723
|
+
"dashboard.attach.hint": "Call the start_attach MCP tool to show the QR here.",
|
|
2680
2724
|
"dashboard.attach.tunnelDown": "Relay disconnected — this QR is no longer valid. Restart the relay, then regenerate the QR.",
|
|
2681
2725
|
"dashboard.pages.section": "Connected Pages",
|
|
2682
2726
|
"dashboard.pages.empty": "No attached pages",
|
|
@@ -2694,7 +2738,6 @@ const en = {
|
|
|
2694
2738
|
"attach.url.section": "URL (fallback)",
|
|
2695
2739
|
"attach.mode.sandbox": "env 2 — AITC Sandbox App (PWA)",
|
|
2696
2740
|
"attach.mode.intossDev": "env 3 — intoss-private relay dev",
|
|
2697
|
-
"attach.mode.intossLive": "env 4 — intoss live relay debug",
|
|
2698
2741
|
"attach.sandbox.step1": "Launch the launcher PWA icon on your home screen (if the Safari address bar is visible, it is not standalone).",
|
|
2699
2742
|
"attach.sandbox.step2": "Scan this QR code with <strong>\"Scan QR with camera\"</strong> inside the launcher.",
|
|
2700
2743
|
"attach.sandbox.step3": "The mini-app opens fullscreen and the debug session attaches automatically.",
|
|
@@ -2710,7 +2753,6 @@ const en = {
|
|
|
2710
2753
|
"attach.intoss.faq.prepare": "<strong>Mini-app stuck in PREPARE state</strong> — verify the deep-link has a <code>_deploymentId</code> parameter",
|
|
2711
2754
|
"attach.intoss.faq.chii": "<strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import",
|
|
2712
2755
|
"attach.intoss.faq.totp": "<strong>TOTP gate Layer C is inactive</strong> — check that <code>AIT_DEBUG_TOTP_SECRET</code> is set on the relay server",
|
|
2713
|
-
"attach.intoss.faq.liveReadOnly": "<strong>LIVE session is read-only</strong> — <code>call_sdk</code>/<code>evaluate</code> require an explicit <code>confirm</code>",
|
|
2714
2756
|
"launcher.title": "AITC DevTools Launcher",
|
|
2715
2757
|
"launcher.description": "Scan the terminal QR code or paste the tunnel URL.",
|
|
2716
2758
|
"launcher.installCta": "Install launcher to your phone",
|
|
@@ -2913,7 +2955,7 @@ const tables = {
|
|
|
2913
2955
|
"dashboard.tunnel.up": "연결됨",
|
|
2914
2956
|
"dashboard.tunnel.down": "끊어짐",
|
|
2915
2957
|
"dashboard.attach.section": "Attach QR",
|
|
2916
|
-
"dashboard.attach.hint": "
|
|
2958
|
+
"dashboard.attach.hint": "start_attach MCP tool을 호출하면 QR이 여기에 표시됩니다.",
|
|
2917
2959
|
"dashboard.attach.tunnelDown": "relay 연결이 끊겼습니다 — 이 QR은 더 이상 유효하지 않습니다. relay를 재시작한 뒤 QR을 다시 생성하세요.",
|
|
2918
2960
|
"dashboard.pages.section": "연결된 Pages",
|
|
2919
2961
|
"dashboard.pages.empty": "attach된 페이지 없음",
|
|
@@ -2931,7 +2973,6 @@ const tables = {
|
|
|
2931
2973
|
"attach.url.section": "URL (fallback)",
|
|
2932
2974
|
"attach.mode.sandbox": "환경 2 — AITC Sandbox App (PWA)",
|
|
2933
2975
|
"attach.mode.intossDev": "환경 3 — intoss-private relay dev",
|
|
2934
|
-
"attach.mode.intossLive": "환경 4 — intoss live relay debug",
|
|
2935
2976
|
"attach.sandbox.step1": "홈 화면의 launcher PWA 아이콘으로 실행하세요 (Safari 주소창이 보이면 standalone이 아닙니다).",
|
|
2936
2977
|
"attach.sandbox.step2": "launcher 안의 <strong>\"QR 카메라로 스캔\"</strong>으로 이 QR 코드를 스캔하세요.",
|
|
2937
2978
|
"attach.sandbox.step3": "미니앱이 풀스크린으로 열리고 디버그 세션이 자동으로 attach됩니다.",
|
|
@@ -2947,7 +2988,6 @@ const tables = {
|
|
|
2947
2988
|
"attach.intoss.faq.prepare": "<strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인",
|
|
2948
2989
|
"attach.intoss.faq.chii": "<strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인",
|
|
2949
2990
|
"attach.intoss.faq.totp": "<strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인",
|
|
2950
|
-
"attach.intoss.faq.liveReadOnly": "<strong>LIVE 세션은 read-only입니다</strong> — <code>call_sdk</code>/<code>evaluate</code> 실행에는 명시적 <code>confirm</code>이 필요합니다",
|
|
2951
2991
|
"launcher.title": "AITC DevTools Launcher",
|
|
2952
2992
|
"launcher.description": "터미널 QR을 스캔하거나 URL을 입력하세요.",
|
|
2953
2993
|
"launcher.installCta": "폰에 런처 설치하기",
|
|
@@ -3206,7 +3246,7 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0;
|
|
|
3206
3246
|
}
|
|
3207
3247
|
.inspector-link:hover { background: #388bfd; }
|
|
3208
3248
|
.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }
|
|
3209
|
-
</style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>__MODE_LABEL____LANG_SWITCHER__<p class="label">deployment: __SAFE_LABEL__</p><div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>스캔 절차</h2><ol><li>토스 앱을 실행하세요.</li><li>폰 카메라 앱으로 QR 코드를 스캔하세요.</li><li>팝업이 뜨면 <strong>"토스로 열기"</strong>를 탭하세요.</li><li>미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.</li></ol></section><hr/><section><h2>진단 체크리스트</h2><ul><li><strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)</li><li><strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인</li><li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li><li><strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인</li
|
|
3249
|
+
</style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>__MODE_LABEL____LANG_SWITCHER__<p class="label">deployment: __SAFE_LABEL__</p><div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>스캔 절차</h2><ol><li>토스 앱을 실행하세요.</li><li>폰 카메라 앱으로 QR 코드를 스캔하세요.</li><li>팝업이 뜨면 <strong>"토스로 열기"</strong>를 탭하세요.</li><li>미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.</li></ol></section><hr/><section><h2>진단 체크리스트</h2><ul><li><strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)</li><li><strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인</li><li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li><li><strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인</li></ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="복사">복사</button></div></section><hr/><section id="inspector-section"><h2>인스펙터</h2>__INSPECTOR_SECTION__</section></body></html>`;
|
|
3210
3250
|
const dashboardChromeHtmlEn = `<!DOCTYPE html>
|
|
3211
3251
|
<html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>AIT Debug Dashboard</title><style>
|
|
3212
3252
|
*, *::before, *::after { box-sizing: border-box; }
|
|
@@ -3387,7 +3427,7 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0;
|
|
|
3387
3427
|
}
|
|
3388
3428
|
.inspector-link:hover { background: #388bfd; }
|
|
3389
3429
|
.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }
|
|
3390
|
-
</style></head><body><h1>AIT Debug Session — QR Scan</h1>__MODE_LABEL____LANG_SWITCHER__<p class="label">deployment: __SAFE_LABEL__</p><div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>How to scan</h2><ol><li>Open the Toss app.</li><li>Scan the QR code with your phone camera app.</li><li>Tap <strong>"Open in Toss"</strong> when the popup appears.</li><li>The mini-app opens and the debug session attaches automatically.</li></ol></section><hr/><section><h2>Troubleshooting checklist</h2><ul><li><strong>Toss app does not open</strong> — check app version; scan with the system camera app (not the Toss in-app QR reader)</li><li><strong>Mini-app stuck in PREPARE state</strong> — verify the deep-link has a <code>_deploymentId</code> parameter</li><li><strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import</li><li><strong>TOTP gate Layer C is inactive</strong> — check that <code>AIT_DEBUG_TOTP_SECRET</code> is set on the relay server</li
|
|
3430
|
+
</style></head><body><h1>AIT Debug Session — QR Scan</h1>__MODE_LABEL____LANG_SWITCHER__<p class="label">deployment: __SAFE_LABEL__</p><div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>How to scan</h2><ol><li>Open the Toss app.</li><li>Scan the QR code with your phone camera app.</li><li>Tap <strong>"Open in Toss"</strong> when the popup appears.</li><li>The mini-app opens and the debug session attaches automatically.</li></ol></section><hr/><section><h2>Troubleshooting checklist</h2><ul><li><strong>Toss app does not open</strong> — check app version; scan with the system camera app (not the Toss in-app QR reader)</li><li><strong>Mini-app stuck in PREPARE state</strong> — verify the deep-link has a <code>_deploymentId</code> parameter</li><li><strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import</li><li><strong>TOTP gate Layer C is inactive</strong> — check that <code>AIT_DEBUG_TOTP_SECRET</code> is set on the relay server</li></ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="Copy">Copy</button></div></section><hr/><section id="inspector-section"><h2>Inspector</h2>__INSPECTOR_SECTION__</section></body></html>`;
|
|
3391
3431
|
/** Map from Locale to the precompiled dashboard chrome string. */
|
|
3392
3432
|
const dashboardChromeByLocale = {
|
|
3393
3433
|
ko: dashboardChromeHtmlKo,
|
|
@@ -3424,9 +3464,6 @@ function buildModeLabel(mode, s) {
|
|
|
3424
3464
|
case "relay-dev":
|
|
3425
3465
|
label = s("attach.mode.intossDev");
|
|
3426
3466
|
break;
|
|
3427
|
-
case "relay-live":
|
|
3428
|
-
label = s("attach.mode.intossLive");
|
|
3429
|
-
break;
|
|
3430
3467
|
case "mock":
|
|
3431
3468
|
case void 0: return "";
|
|
3432
3469
|
}
|
|
@@ -3700,12 +3737,11 @@ function buildSseScript(strings) {
|
|
|
3700
3737
|
* - __SAFE_LABEL__ : HTML-escaped deploymentId label (intoss family에만 존재)
|
|
3701
3738
|
* - __SAFE_ATTACH_URL__ : HTML-escaped attach URL (TOTP at= 코드 포함 — 의도된 전달)
|
|
3702
3739
|
* - __MODE_LABEL__ : 환경 배지 (`<p class="mode-label">…</p>` 또는 빈 문자열, #468)
|
|
3703
|
-
* - __LIVE_FAQ__ : 환경 4 LIVE read-only `<li>` 또는 빈 문자열 (intoss family에만 존재)
|
|
3704
3740
|
* - __INSPECTOR_SECTION__ : "디버그 툴 열기" 버튼 또는 대기 힌트 (#544)
|
|
3705
3741
|
*
|
|
3706
3742
|
* mode-aware 분기 (#468): mode가 `relay-mobile`이면 sandbox family chrome(launcher
|
|
3707
|
-
* PWA 절차), 그 외는 intoss family chrome(토스 앱 절차)을 선택한다.
|
|
3708
|
-
*
|
|
3743
|
+
* PWA 절차), 그 외는 intoss family chrome(토스 앱 절차)을 선택한다.
|
|
3744
|
+
* relay-live (env 4) 제거 (#665) — positive-allowlist kill-switch.
|
|
3709
3745
|
*
|
|
3710
3746
|
* SSE 스크립트도 주입 — `#attach-section` hook이 있으면 `/events` push 때 QR이
|
|
3711
3747
|
* `/qr.png?u=<fresh attachUrl>`로 자동 갱신된다. `#inspector-link`도 SSE push로
|
|
@@ -3718,11 +3754,10 @@ function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/a
|
|
|
3718
3754
|
const s = resolveLocaleStrings(locale);
|
|
3719
3755
|
const langSwitcher = buildLangSwitcher(path, params, locale, s);
|
|
3720
3756
|
const family = attachFamilyForMode(mode);
|
|
3721
|
-
const liveFaq = mode === "relay-live" ? `<li>${s("attach.intoss.faq.liveReadOnly")}</li>` : "";
|
|
3722
3757
|
let inspectorSection;
|
|
3723
3758
|
if (pagesAttached && inspectorStableUrl) inspectorSection = `<a class="inspector-link" id="inspector-link" href="${escapeHtml(inspectorStableUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(s("dashboard.inspector.open"))}</a>`;
|
|
3724
3759
|
else inspectorSection = `<span class="inspector-hint" id="inspector-link">${escapeHtml(s("dashboard.inspector.waiting"))}</span>`;
|
|
3725
|
-
const filled = attachChromeByLocale[locale][family].replaceAll("__LANG_SWITCHER__", langSwitcher).replaceAll("__MODE_LABEL__", buildModeLabel(mode, s)).replaceAll("
|
|
3760
|
+
const filled = attachChromeByLocale[locale][family].replaceAll("__LANG_SWITCHER__", langSwitcher).replaceAll("__MODE_LABEL__", buildModeLabel(mode, s)).replaceAll("__QR_DATA_URL__", qrDataUrl).replaceAll("__SAFE_LABEL__", safeLabel).replaceAll("__SAFE_ATTACH_URL__", safeAttachUrl).replaceAll("__INSPECTOR_SECTION__", inspectorSection);
|
|
3726
3761
|
const sseScript = buildSseScript({
|
|
3727
3762
|
tunnelUp: JSON.stringify(s("dashboard.tunnel.up")),
|
|
3728
3763
|
tunnelDown: JSON.stringify(s("dashboard.tunnel.down")),
|
|
@@ -4349,22 +4384,27 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4349
4384
|
availableIn: "both"
|
|
4350
4385
|
},
|
|
4351
4386
|
{
|
|
4352
|
-
name: "
|
|
4353
|
-
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.
|
|
4387
|
+
name: "start_attach",
|
|
4388
|
+
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. Single entry point to attach a real device: switches the debug mode (if `mode` is given), builds the self-attaching deep-link QR for the active relay environment, and waits for the phone to attach — all in one call (replaces the old attach-URL + start_debug two-step). 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).\n\nmode (optional): pass \"relay-sandbox\" (env 2) or \"relay-staging\" (env 3) to switch the active environment first. When omitted, the current relay environment is used as-is (no switch). Passing \"local-browser\" returns an error — start_attach is relay-only (env 2/3). When the session is already in the requested mode, the switch is skipped.\n\nEnvironment-specific behaviour:\n • env 3 / 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.\n • env 2 / 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=…). When projectRoot is given, the app name from <projectRoot>/package.json is added as name= so the launcher partner bar shows it.\n\nWaits for a page to attach by default (up to wait_timeout_seconds, default 60 s). The server automatically opens the QR dashboard in the OS default browser when running on a local GUI machine — headless/remote environments fall back to the text QR in the tool output.\n\nTOTP auto re-mint: when AIT_DEBUG_TOTP_SECRET is set on the MCP server, the attachUrl carries a one-time code (at=<code>) valid for ~3 minutes (the relay gate accepts ±6 TOTP steps). While waiting, start_attach AUTOMATICALLY re-mints a fresh code before the current one expires and refreshes the dashboard QR in place (no browser re-open). You do NOT need to re-call start_attach every time the code would expire — a single call covers the whole wait window. The response includes a `totp` field with `expiresAt` and a `reminted` count of how many fresh codes were issued during the wait. Without AIT_DEBUG_TOTP_SECRET, the attachUrl has no expiry.\n\nselfdebug (env 2 / relay-sandbox only): pass selfdebug=true to add &selfdebug=1 to the launcher deep-link. The launcher PWA then registers its own document as the CDP target instead of the framed mini-app. SINGLE-ATTACH MODEL: attaching the launcher self-target evicts any currently-attached mini-app target — use this mode exclusively for diagnosing the launcher document itself (DOM, safe-area, console). Not applicable in env 3 (relay-staging) — passing selfdebug=true there returns an error.",
|
|
4354
4389
|
inputSchema: {
|
|
4355
4390
|
type: "object",
|
|
4356
4391
|
properties: {
|
|
4392
|
+
mode: {
|
|
4393
|
+
type: "string",
|
|
4394
|
+
enum: [
|
|
4395
|
+
"local-browser",
|
|
4396
|
+
"relay-sandbox",
|
|
4397
|
+
"relay-staging"
|
|
4398
|
+
],
|
|
4399
|
+
description: "Optional debug mode to switch into before attaching. \"relay-sandbox\" = env 2 (launcher PWA), \"relay-staging\" = env 3 (intoss-private dog-food). \"local-browser\" returns an error (start_attach is relay-only). Omit to keep the current relay environment."
|
|
4400
|
+
},
|
|
4357
4401
|
scheme_url: {
|
|
4358
4402
|
type: "string",
|
|
4359
4403
|
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."
|
|
4360
4404
|
},
|
|
4361
|
-
wait_for_attach: {
|
|
4362
|
-
type: "boolean",
|
|
4363
|
-
description: "If true, block after returning the QR until a page attaches to the relay (polls listTargets ~1 s interval, default 60 s). On attach, the response includes the attached page list. On timeout, call build_attach_url again to resume polling."
|
|
4364
|
-
},
|
|
4365
4405
|
wait_timeout_seconds: {
|
|
4366
4406
|
type: "number",
|
|
4367
|
-
description: "Maximum seconds to wait
|
|
4407
|
+
description: "Maximum seconds to wait for a page to attach (default 60, range 1–600). Values outside the range or invalid inputs (0, negative, NaN) fall back to the default silently. During the wait the TOTP code is auto re-minted as needed, so a single call covers the whole window."
|
|
4368
4408
|
},
|
|
4369
4409
|
projectRoot: {
|
|
4370
4410
|
type: "string",
|
|
@@ -4372,7 +4412,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4372
4412
|
},
|
|
4373
4413
|
selfdebug: {
|
|
4374
4414
|
type: "boolean",
|
|
4375
|
-
description: "Env 2 / relay-sandbox only. When true, adds &selfdebug=1 to the launcher deep-link so the launcher PWA registers its own document as the CDP target (launcher diagnostics mode). SINGLE-ATTACH MODEL: self-target attach evicts any currently-attached mini-app target. Use only when you need to inspect the launcher itself (DOM, safe-area, console). Passing selfdebug=true in env 3
|
|
4415
|
+
description: "Env 2 / relay-sandbox only. When true, adds &selfdebug=1 to the launcher deep-link so the launcher PWA registers its own document as the CDP target (launcher diagnostics mode). SINGLE-ATTACH MODEL: self-target attach evicts any currently-attached mini-app target. Use only when you need to inspect the launcher itself (DOM, safe-area, console). Passing selfdebug=true in env 3 (relay-staging) returns an error. Default: false (omitted)."
|
|
4376
4416
|
}
|
|
4377
4417
|
},
|
|
4378
4418
|
required: []
|
|
@@ -4411,7 +4451,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4411
4451
|
},
|
|
4412
4452
|
{
|
|
4413
4453
|
name: "measure_safe_area",
|
|
4414
|
-
description: "Runs a safe-area probe on the attached mini-app page via Runtime.evaluate and returns normalized safe-area insets, viewport geometry, device pixel ratio, and User-Agent. Read-only — does not modify page state. Tier C per RFC #277: the same Runtime.evaluate probe runs in both `mock` (devtools panel page with window.__ait state) and `relay` (real-device WebView with window.__sdk). The result includes a `source: \"mock\" | \"relay-dev\" | \"relay-
|
|
4454
|
+
description: "Runs a safe-area probe on the attached mini-app page via Runtime.evaluate and returns normalized safe-area insets, viewport geometry, device pixel ratio, and User-Agent. Read-only — does not modify page state. Tier C per RFC #277: the same Runtime.evaluate probe runs in both `mock` (devtools panel page with window.__ait state) and `relay` (real-device WebView with window.__sdk). The result includes a `source: \"mock\" | \"relay-dev\" | \"relay-mobile\"` field so consumers can identify provenance without inspecting payload values. (`relay-mobile` = env 2 real-device PWA over an external relay; `relay-dev` = env 3 dog-food WebView; relay-live/env 4 removed #665.) Use in a relay session (phone attached) to get ground-truth values for upgrading a viewport preset from extrapolated/placeholder to measured. Requires a page to be attached — call list_pages first.",
|
|
4415
4455
|
inputSchema: {
|
|
4416
4456
|
type: "object",
|
|
4417
4457
|
properties: {},
|
|
@@ -4421,19 +4461,13 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4421
4461
|
},
|
|
4422
4462
|
{
|
|
4423
4463
|
name: "evaluate",
|
|
4424
|
-
description: "Evaluates an arbitrary JavaScript expression on the attached mini-app page via CDP Runtime.evaluate (returnByValue: true) and returns the result. NOT read-only — the expression can have side effects (DOM mutations, SDK calls, state changes). Requires the relay to be attached — call list_pages first. Throws if the evaluation throws an exception on the page.\n\nSECURITY: expression and result are not redacted — never include secrets or auth tokens in the expression.\n\
|
|
4464
|
+
description: "Evaluates an arbitrary JavaScript expression on the attached mini-app page via CDP Runtime.evaluate (returnByValue: true) and returns the result. NOT read-only — the expression can have side effects (DOM mutations, SDK calls, state changes). Requires the relay to be attached — call list_pages first. Throws if the evaluation throws an exception on the page.\n\nSECURITY: expression and result are not redacted — never include secrets or auth tokens in the expression.\n\nPositive-allowlist kill-switch (#665): this tool is blocked when the attached page is on a non-debug host (apps.tossmini.com / env 4). Only localhost, *.trycloudflare.com, and *.private-apps.tossmini.com are allowed. relay-live (env 4) and the LIVE confirm guard are removed.",
|
|
4425
4465
|
inputSchema: {
|
|
4426
4466
|
type: "object",
|
|
4427
|
-
properties: {
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
},
|
|
4432
|
-
confirm: {
|
|
4433
|
-
type: "boolean",
|
|
4434
|
-
description: "Required when MCP_ENV=relay-live. Set to `true` to explicitly acknowledge that this expression may have side effects on real/live users. Omitting this in a relay-live session results in a structured rejection error. Has no effect in mock or relay-dev sessions."
|
|
4435
|
-
}
|
|
4436
|
-
},
|
|
4467
|
+
properties: { expression: {
|
|
4468
|
+
type: "string",
|
|
4469
|
+
description: "JavaScript expression to evaluate in the page context."
|
|
4470
|
+
} },
|
|
4437
4471
|
required: ["expression"]
|
|
4438
4472
|
},
|
|
4439
4473
|
availableIn: "both"
|
|
@@ -4453,7 +4487,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4453
4487
|
},
|
|
4454
4488
|
{
|
|
4455
4489
|
name: "call_sdk",
|
|
4456
|
-
description: "Calls a dog-food SDK method via the window.__sdkCall bridge (exported by @apps-in-toss/web-framework only in __DEBUG_BUILD__ bundles). NOT read-only — SDK calls have side effects (navigation, payments, permissions, etc.). On env 3/4 (real device relay) this hits the real SDK; on env 1 (local mock) and env 2 (PWA relay — real WebKit, mock SDK) it hits the mock SDK. Requires the relay to be attached — call list_pages first. Returns {ok: true, value} on success or {ok: false, error} on failure. If a Runtime.exceptionThrown event was observed within [callStart-50ms, callEnd+200ms], the result also includes `recentException` for crash triage. Returns a clear error if window.__sdkCall is not available — on relay (env 3/4) that means a non-dog-food bundle (redeploy via `ait build && aitcc app deploy`); on local (--target=local, env 1) it means the dev bridge is not installed (start the dev server with `pnpm dev`).\n\nSECURITY: method name, args, and result value are not redacted — never include secrets.\n\
|
|
4490
|
+
description: "Calls a dog-food SDK method via the window.__sdkCall bridge (exported by @apps-in-toss/web-framework only in __DEBUG_BUILD__ bundles). NOT read-only — SDK calls have side effects (navigation, payments, permissions, etc.). On env 3/4 (real device relay) this hits the real SDK; on env 1 (local mock) and env 2 (PWA relay — real WebKit, mock SDK) it hits the mock SDK. Requires the relay to be attached — call list_pages first. Returns {ok: true, value} on success or {ok: false, error} on failure. If a Runtime.exceptionThrown event was observed within [callStart-50ms, callEnd+200ms], the result also includes `recentException` for crash triage. Returns a clear error if window.__sdkCall is not available — on relay (env 3/4) that means a non-dog-food bundle (redeploy via `ait build && aitcc app deploy`); on local (--target=local, env 1) it means the dev bridge is not installed (start the dev server with `pnpm dev`).\n\nSECURITY: method name, args, and result value are not redacted — never include secrets.\n\nPositive-allowlist kill-switch (#665): blocked when the attached page is on a non-debug host (apps.tossmini.com / env 4). relay-live and the LIVE guard removed.\n\nIMPORTANT — 인자 시그니처 (잘못된 인자로 호출하면 토스 앱 crash 위험):\n setDeviceOrientation: call_sdk(\"setDeviceOrientation\", [{ type: \"landscape\" }]) // NOT \"landscape\"\n setIosSwipeGestureEnabled: call_sdk(\"setIosSwipeGestureEnabled\", [{ isEnabled: false }])\n setSecureScreen: call_sdk(\"setSecureScreen\", [{ enabled: true }])\n setScreenAwakeMode: call_sdk(\"setScreenAwakeMode\", [{ enabled: true }])\n getOperationalEnvironment: call_sdk(\"getOperationalEnvironment\", [])\n getPlatformOS: call_sdk(\"getPlatformOS\", [])\n getDeviceId: call_sdk(\"getDeviceId\", [])\n getLocale: call_sdk(\"getLocale\", [])\n getNetworkStatus: call_sdk(\"getNetworkStatus\", [])\n getSchemeUri: call_sdk(\"getSchemeUri\", [])\n requestReview: call_sdk(\"requestReview\", [])\n closeView: call_sdk(\"closeView\", [])",
|
|
4457
4491
|
inputSchema: {
|
|
4458
4492
|
type: "object",
|
|
4459
4493
|
properties: {
|
|
@@ -4465,10 +4499,6 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4465
4499
|
type: "array",
|
|
4466
4500
|
description: "Arguments to pass to the SDK method (optional, default []).",
|
|
4467
4501
|
items: {}
|
|
4468
|
-
},
|
|
4469
|
-
confirm: {
|
|
4470
|
-
type: "boolean",
|
|
4471
|
-
description: "Required when MCP_ENV=relay-live. Set to `true` to explicitly acknowledge that this SDK call may have side effects on real/live users. Omitting this in a relay-live session results in a structured rejection error. Has no effect in mock or relay-dev sessions."
|
|
4472
4502
|
}
|
|
4473
4503
|
},
|
|
4474
4504
|
required: ["name"]
|
|
@@ -4507,7 +4537,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4507
4537
|
},
|
|
4508
4538
|
{
|
|
4509
4539
|
name: "start_debug",
|
|
4510
|
-
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
|
|
4540
|
+
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 2/3, real-device 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\nPositive-allowlist kill-switch (#665): relay sessions on apps.tossmini.com (env 4, released production) are silently blocked at both the in-app gate and this MCP layer — relay-live and the LIVE guard have been removed. Only localhost/loopback (env 1), *.trycloudflare.com (env 2), and *.private-apps.tossmini.com (env 3) are allowed.\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. CDP covers real-device WebKit DOM, console, exceptions, and safe-area observation; call_sdk still hits the mock (SDK fidelity needs relay-staging). Side-effect tools run unguarded against the mock. Only the dual-connection daemon can enter relay-sandbox in-place; a single-connection session rejects it with \"동적 전환할 수 없습니다 … relay-sandbox 모드로 재시작하세요\" — follow that hint and restart the MCP server in relay-sandbox mode rather than retrying. Prerequisites: both AIT_RELAY_BASE_URL (the relay base the unplugin emits when started with tunnel:{cdp:true}, used for the CDP attach) and AIT_TUNNEL_BASE_URL (the dev-server tunnel host, required by start_attach to render the launcher QR) must be set before the MCP server starts — the unplugin does not auto-forward either; set them explicitly. Both carry relay/tunnel hosts (secret-class) — keep them out of logs.\n relay-staging — env 3: a real-device Toss WebView dog-food 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 (dog-food, not released to real users). Prerequisite: a dog-food candidate bundle built with `RELEASE_CHANNEL=dogfood ait build`, then uploaded with `ait deploy` (add `--scheme-only` to print the resulting intoss-private://…?_deploymentId=… deep-link); open that deep-link/QR on the device to cold-load the bundle with the relay injected. Unlike env 2, env 3 is NOT a dev-server tunnel — it is a deployed bundle reached via the intoss-private scheme, so `pnpm dev` plays no part here.\n\nFor a relay mode (relay-sandbox/relay-staging), 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.",
|
|
4511
4541
|
inputSchema: {
|
|
4512
4542
|
type: "object",
|
|
4513
4543
|
properties: {
|
|
@@ -4516,18 +4546,13 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4516
4546
|
enum: [
|
|
4517
4547
|
"local-browser",
|
|
4518
4548
|
"relay-sandbox",
|
|
4519
|
-
"relay-staging"
|
|
4520
|
-
"relay-live"
|
|
4549
|
+
"relay-staging"
|
|
4521
4550
|
],
|
|
4522
|
-
description: "Target environment to switch to.
|
|
4523
|
-
},
|
|
4524
|
-
confirm: {
|
|
4525
|
-
type: "boolean",
|
|
4526
|
-
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."
|
|
4551
|
+
description: "Target environment to switch to. relay-live (env 4) has been removed (#665) — use relay-staging (env 3) for dog-food debugging."
|
|
4527
4552
|
},
|
|
4528
4553
|
projectRoot: {
|
|
4529
4554
|
type: "string",
|
|
4530
|
-
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-
|
|
4555
|
+
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-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)."
|
|
4531
4556
|
}
|
|
4532
4557
|
},
|
|
4533
4558
|
required: ["mode"]
|
|
@@ -4536,7 +4561,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4536
4561
|
},
|
|
4537
4562
|
{
|
|
4538
4563
|
name: "get_debug_status",
|
|
4539
|
-
description: "Reports the current debug session state — which environment/mode is active, whether a page is attached, and a full diagnostic snapshot — in one call. Use this any time to answer \"what mode am I in right now?\" or \"why is this not working?\" without chaining tools. Fields: mcpVersion (MCP SDK version), devtoolsVersion (@ait-co/devtools package version), tunnel (up/wssUrl/pid/startedAt), pages (list_pages result + lastSeenAt stats), lastAttachAt, lastDetachAt, recentErrors (last N server-side errors, PII/secret redacted), authRejects ({count, lastAt} — relay TOTP 401 rejections, secret-free; count > 0 with empty pages means the phone reached the relay but its code was rejected), environment (kind: mock|relay-dev|relay-
|
|
4564
|
+
description: "Reports the current debug session state — which environment/mode is active, whether a page is attached, and a full diagnostic snapshot — in one call. Use this any time to answer \"what mode am I in right now?\" or \"why is this not working?\" without chaining tools. Fields: mcpVersion (MCP SDK version), devtoolsVersion (@ait-co/devtools package version), tunnel (up/wssUrl/pid/startedAt), pages (list_pages result + lastSeenAt stats), lastAttachAt, lastDetachAt, recentErrors (last N server-side errors, PII/secret redacted), authRejects ({count, lastAt} — relay TOTP 401 rejections, secret-free; count > 0 with empty pages means the phone reached the relay but its code was rejected), environment (kind: mock|relay-dev|relay-mobile, env: mock|relay backward-compat, reason, liveGuardActive: always false — relay-live and LIVE guard removed (#665); start_debug mode→kind mapping: relay-sandbox→relay-mobile, relay-staging→relay-dev, local-browser→mock), serverLockHolder (pid + startedAt from the lock file, or null), nextRecommendedAction ({tool, reason} or null — the single next tool to call; in local-target mode tunnel.up=false is normal so \"restart\" is never recommended). All fields are nullable — missing data is null, not an error. debug-mode only — dev-mode (--mode=dev) does not support relay diagnostics. Tier C (both mock and relay).",
|
|
4540
4565
|
inputSchema: {
|
|
4541
4566
|
type: "object",
|
|
4542
4567
|
properties: { recent_errors_limit: {
|
|
@@ -4549,7 +4574,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4549
4574
|
},
|
|
4550
4575
|
{
|
|
4551
4576
|
name: "run_tests",
|
|
4552
|
-
description: "Runs mini-app test files on the attached page over CDP (Runtime.evaluate). Each matched file is bundled with esbuild (SDK imports redirected to the live mock/SDK), injected into the attached WebView, and executed; returns per-file results plus flattened totals (passed/failed/skipped/total). Requires an attached page — call list_pages first to confirm one is attached. Files run SEQUENTIALLY (single-attach model: the relay/local target serves one page), and one run_tests call runs at a time (a concurrent call is rejected). Test verification (assert/snapshot) is delegated to the in-page Vitest runtime; this tool is the transport + report. The per-file results array is the progress record — on partial failure you see exactly which files passed/failed/timed-out.
|
|
4577
|
+
description: "Runs mini-app test files on the attached page over CDP (Runtime.evaluate). Each matched file is bundled with esbuild (SDK imports redirected to the live mock/SDK), injected into the attached WebView, and executed; returns per-file results plus flattened totals (passed/failed/skipped/total). Requires an attached page — call list_pages first to confirm one is attached. Files run SEQUENTIALLY (single-attach model: the relay/local target serves one page), and one run_tests call runs at a time (a concurrent call is rejected). Test verification (assert/snapshot) is delegated to the in-page Vitest runtime; this tool is the transport + report. The per-file results array is the progress record — on partial failure you see exactly which files passed/failed/timed-out. Positive-allowlist kill-switch (#665): blocked when the attached page is on a non-debug host. debug-mode only — dev-mode (--mode=dev) has no CDP. Tier C (both mock/local and relay). The devtools-test CLI shares this run core and file discovery, but its standalone relay attach is not wired yet — run via this tool for now.",
|
|
4553
4578
|
inputSchema: {
|
|
4554
4579
|
type: "object",
|
|
4555
4580
|
properties: {
|
|
@@ -4565,10 +4590,6 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4565
4590
|
timeout_ms: {
|
|
4566
4591
|
type: "number",
|
|
4567
4592
|
description: "Per-file evaluate timeout in ms (default 30000, range 1000–600000). Out-of-range/invalid values fall back to the default."
|
|
4568
|
-
},
|
|
4569
|
-
confirm: {
|
|
4570
|
-
type: "boolean",
|
|
4571
|
-
description: "Required (true) to run in a relay-live session — test injection mutates page state. Ignored in every non-live session (mock/local, relay-dev, relay-mobile)."
|
|
4572
4593
|
}
|
|
4573
4594
|
},
|
|
4574
4595
|
required: ["files"]
|
|
@@ -4594,8 +4615,9 @@ function getToolAvailability(name) {
|
|
|
4594
4615
|
* Unknown tools return `false` — callers should reject them as unknown rather
|
|
4595
4616
|
* than as env-mismatched.
|
|
4596
4617
|
*
|
|
4597
|
-
* Relay variants (`relay-dev`, `relay-
|
|
4618
|
+
* Relay variants (`relay-dev`, `relay-mobile`) all satisfy the
|
|
4598
4619
|
* `'relay'` availability tier — `isRelayEnv()` is used for the check.
|
|
4620
|
+
* (`relay-live` removed #665.)
|
|
4599
4621
|
*/
|
|
4600
4622
|
function isToolAvailableIn(name, env) {
|
|
4601
4623
|
const availability = getToolAvailability(name);
|
|
@@ -4609,8 +4631,8 @@ function isToolAvailableIn(name, env) {
|
|
|
4609
4631
|
* matches the given env. Pure — preserves order; both Tier C ("both") and the
|
|
4610
4632
|
* matching single-env tier pass through.
|
|
4611
4633
|
*
|
|
4612
|
-
* Relay variants (`relay-dev`, `relay-
|
|
4613
|
-
* `'relay'` tier.
|
|
4634
|
+
* Relay variants (`relay-dev`, `relay-mobile`) all satisfy the
|
|
4635
|
+
* `'relay'` tier. (`relay-live` removed #665.)
|
|
4614
4636
|
*/
|
|
4615
4637
|
function filterToolsByEnvironment(tools, env) {
|
|
4616
4638
|
return tools.filter((t) => t.availableIn === "both" || t.availableIn === "relay" && isRelayEnv(env) || t.availableIn === env);
|
|
@@ -4618,14 +4640,14 @@ function filterToolsByEnvironment(tools, env) {
|
|
|
4618
4640
|
/**
|
|
4619
4641
|
* Tool names that are available before any page attaches (bootstrap tier).
|
|
4620
4642
|
*
|
|
4621
|
-
* `
|
|
4622
|
-
* `list_pages`
|
|
4643
|
+
* `start_attach` — mode switch + QR synthesis + attach wait, no prior attach needed.
|
|
4644
|
+
* `list_pages` — reports tunnel status + empty pages even pre-attach.
|
|
4623
4645
|
*
|
|
4624
4646
|
* All other tools require an attached page (`enableDomains` must succeed) and
|
|
4625
4647
|
* are only advertised in `tools/list` once a target appears.
|
|
4626
4648
|
*/
|
|
4627
4649
|
const BOOTSTRAP_TOOL_NAMES = new Set([
|
|
4628
|
-
"
|
|
4650
|
+
"start_attach",
|
|
4629
4651
|
"get_debug_status",
|
|
4630
4652
|
"list_pages",
|
|
4631
4653
|
"start_debug"
|
|
@@ -4729,57 +4751,6 @@ function listPages(connection, tunnel) {
|
|
|
4729
4751
|
};
|
|
4730
4752
|
}
|
|
4731
4753
|
/**
|
|
4732
|
-
* Builds a self-attaching dog-food deep-link from an `ait deploy --scheme-only`
|
|
4733
|
-
* URL plus this session's live relay. Throws if the tunnel is not up yet (no
|
|
4734
|
-
* relay URL to splice in) — the caller surfaces that as a tool error.
|
|
4735
|
-
*
|
|
4736
|
-
* When `AIT_DEBUG_TOTP_SECRET` is set, generates the current TOTP code and
|
|
4737
|
-
* splices it as `at=<code>` into the attach URL. The code is valid for ~3
|
|
4738
|
-
* minutes (the relay gate uses {@link RELAY_VERIFY_SKEW_STEPS}=6, accepting
|
|
4739
|
-
* past 6 steps = 180–210 s backwards from issuance). If the scan happens after
|
|
4740
|
-
* `totp.expiresAt`, call `build_attach_url` again to get a fresh code (#490).
|
|
4741
|
-
*
|
|
4742
|
-
* Also validates the scheme URL's authority. A suspicious authority (empty,
|
|
4743
|
-
* "web", "localhost", etc.) is surfaced as a non-fatal `authorityWarning` on
|
|
4744
|
-
* the result so the caller can show a helpful hint without blocking the link
|
|
4745
|
-
* generation (the warning is consistent with how other validation in
|
|
4746
|
-
* `buildDeepLinkAttachUrl` works — hard errors for relay, soft warning for
|
|
4747
|
-
* the scheme authority which is in the caller's input, not ours to own).
|
|
4748
|
-
*
|
|
4749
|
-
* SECRET-HANDLING: `totpSecret` (if provided) is used only to compute a code
|
|
4750
|
-
* and must never appear in any log, error message, or output outside of the
|
|
4751
|
-
* spliced `at=` param in `attachUrl`.
|
|
4752
|
-
*
|
|
4753
|
-
* @param schemeUrl - The `intoss-private://…?_deploymentId=<uuid>` URL.
|
|
4754
|
-
* @param tunnel - Current tunnel status from the running debug server.
|
|
4755
|
-
* @param totpSecret - Optional hex-encoded TOTP secret (from
|
|
4756
|
-
* `AIT_DEBUG_TOTP_SECRET`). When provided, the current code is spliced into
|
|
4757
|
-
* the attach URL as `at=<code>`.
|
|
4758
|
-
*/
|
|
4759
|
-
function buildAttachUrl(schemeUrl, tunnel, totpSecret) {
|
|
4760
|
-
if (!tunnel.up || tunnel.wssUrl === null) throw new Error("tunnel-down: cloudflared 터널이 안 떠 있습니다. MCP 서버를 재시작하거나 잠시 후 list_pages로 터널 상태를 다시 확인하세요.");
|
|
4761
|
-
const authorityWarning = validateSchemeAuthority(schemeUrl) ?? void 0;
|
|
4762
|
-
let totpCode;
|
|
4763
|
-
let totpMeta;
|
|
4764
|
-
if (totpSecret !== void 0 && totpSecret !== "") {
|
|
4765
|
-
const now = Date.now();
|
|
4766
|
-
totpCode = generateTotp(totpSecret, now);
|
|
4767
|
-
const STEP_SECONDS = 30;
|
|
4768
|
-
const expiresAtMs = now + 6 * STEP_SECONDS * 1e3;
|
|
4769
|
-
totpMeta = {
|
|
4770
|
-
enabled: true,
|
|
4771
|
-
ttlSeconds: 6 * STEP_SECONDS,
|
|
4772
|
-
expiresAt: new Date(expiresAtMs).toISOString()
|
|
4773
|
-
};
|
|
4774
|
-
}
|
|
4775
|
-
return {
|
|
4776
|
-
attachUrl: buildDeepLinkAttachUrl(schemeUrl, tunnel.wssUrl, totpCode),
|
|
4777
|
-
relayUrl: tunnel.wssUrl,
|
|
4778
|
-
...authorityWarning !== void 0 ? { authorityWarning } : {},
|
|
4779
|
-
...totpMeta !== void 0 ? { totp: totpMeta } : {}
|
|
4780
|
-
};
|
|
4781
|
-
}
|
|
4782
|
-
/**
|
|
4783
4754
|
* Heuristic: can this process open a GUI browser?
|
|
4784
4755
|
*
|
|
4785
4756
|
* Returns `true` when we think a GUI is available:
|
|
@@ -5396,7 +5367,7 @@ async function readMcpSdkVersion() {
|
|
|
5396
5367
|
* some test environments that skip the build step).
|
|
5397
5368
|
*/
|
|
5398
5369
|
function readDevtoolsVersion() {
|
|
5399
|
-
return "0.1.
|
|
5370
|
+
return "0.1.110";
|
|
5400
5371
|
}
|
|
5401
5372
|
/**
|
|
5402
5373
|
* Derives the next recommended action from a completed diagnostics snapshot.
|
|
@@ -5405,10 +5376,10 @@ function readDevtoolsVersion() {
|
|
|
5405
5376
|
* 0. tunnel.droppedAt non-null → restart (permanent tunnel drop — highest priority)
|
|
5406
5377
|
* 1. tunnel.up === false AND env is relay → restart (relay needs a live tunnel)
|
|
5407
5378
|
* 1b. tunnel.up === false AND env is mock → wait_for_page (local target: tunnel-less is normal)
|
|
5408
|
-
* 2a. authRejects.count > 0 AND pages empty →
|
|
5379
|
+
* 2a. authRejects.count > 0 AND pages empty → start_attach (relay TOTP 거부 관측 — QR 재스캔
|
|
5409
5380
|
* 또는 target-side `at` 전달 확인. 일반 rule 2보다 구체적이므로 먼저 평가 — issue #467)
|
|
5410
|
-
* 2. tunnel.up, pages empty, env === relay →
|
|
5411
|
-
* 3. pages has entry + crashDetectedAt non-null →
|
|
5381
|
+
* 2. tunnel.up, pages empty, env === relay → start_attach (start attach)
|
|
5382
|
+
* 3. pages has entry + crashDetectedAt non-null → start_attach (re-attach after crash)
|
|
5412
5383
|
* 4. otherwise → null (session looks healthy)
|
|
5413
5384
|
*
|
|
5414
5385
|
* Pure — does not throw; receives the final assembled snapshot fields.
|
|
@@ -5431,16 +5402,16 @@ function computeNextRecommendedAction(tunnel, pages, env, authRejects = null) {
|
|
|
5431
5402
|
reason: "tunnel not up — run `npx @ait-co/devtools devtools-mcp` to restart"
|
|
5432
5403
|
};
|
|
5433
5404
|
if (authRejects !== null && authRejects.count > 0 && pages !== null && pages.pages.length === 0) return {
|
|
5434
|
-
tool: "
|
|
5405
|
+
tool: "start_attach",
|
|
5435
5406
|
reason: `relay 인증(TOTP) 거부 ${authRejects.count}건 발생 (last ${authRejects.lastAt ?? "unknown"}) — QR을 다시 스캔해 새 코드로 attach하세요(코드는 ~3분마다 만료). 반복되면 폰 페이지 URL에 at 파라미터가 전달되는지(target-side TOTP 전달 경로)를 확인하세요`
|
|
5436
5407
|
};
|
|
5437
5408
|
if (isRelayEnv(env) && pages !== null && pages.pages.length === 0 && !pages.crashDetectedAt) return {
|
|
5438
|
-
tool: "
|
|
5439
|
-
reason: "tunnel ready, no pages attached — call
|
|
5409
|
+
tool: "start_attach",
|
|
5410
|
+
reason: "tunnel ready, no pages attached — call start_attach to generate the attach QR"
|
|
5440
5411
|
};
|
|
5441
5412
|
if (pages !== null && pages.crashDetectedAt !== null) return {
|
|
5442
|
-
tool: "
|
|
5443
|
-
reason: `page crashed at ${pages.crashDetectedAt} — call
|
|
5413
|
+
tool: "start_attach",
|
|
5414
|
+
reason: `page crashed at ${pages.crashDetectedAt} — call start_attach to re-attach`
|
|
5444
5415
|
};
|
|
5445
5416
|
return null;
|
|
5446
5417
|
}
|
|
@@ -5505,7 +5476,7 @@ async function getDiagnostics(input) {
|
|
|
5505
5476
|
kind: env,
|
|
5506
5477
|
env: toLegacyEnv(env),
|
|
5507
5478
|
reason: envReason,
|
|
5508
|
-
liveGuardActive:
|
|
5479
|
+
liveGuardActive: false
|
|
5509
5480
|
},
|
|
5510
5481
|
serverLockHolder,
|
|
5511
5482
|
process: {
|
|
@@ -5618,7 +5589,7 @@ async function startQuickTunnel(localPort) {
|
|
|
5618
5589
|
* every surface (terminal, VS Code, JetBrains, web) and can be scanned by a
|
|
5619
5590
|
* phone camera when shown verbatim in an agent response.
|
|
5620
5591
|
*
|
|
5621
|
-
* Shared by `renderAttachBanner` (relay wssUrl QR) and the `
|
|
5592
|
+
* Shared by `renderAttachBanner` (relay wssUrl QR) and the `start_attach`
|
|
5622
5593
|
* MCP tool response (attach deep-link QR).
|
|
5623
5594
|
*/
|
|
5624
5595
|
async function renderQr(text) {
|
|
@@ -5649,7 +5620,7 @@ async function renderQr(text) {
|
|
|
5649
5620
|
* The QR is produced by `renderQr` (a half-block matrix, not the
|
|
5650
5621
|
* `qrcode-terminal` ASCII art used by the unplugin banner) and encodes the
|
|
5651
5622
|
* base `wssUrl` only. When `totpEnabled` is true, a note
|
|
5652
|
-
* is added that attach URLs generated by `
|
|
5623
|
+
* is added that attach URLs generated by `start_attach` will include a
|
|
5653
5624
|
* live TOTP code (`at=`) appended at call time.
|
|
5654
5625
|
*
|
|
5655
5626
|
* SECRET-HANDLING: no secret value, TOTP code, or intermediate value is
|
|
@@ -5665,9 +5636,9 @@ async function renderAttachBanner(input) {
|
|
|
5665
5636
|
` relay (wss): ${input.wssUrl}`,
|
|
5666
5637
|
authNote,
|
|
5667
5638
|
"",
|
|
5668
|
-
" Use
|
|
5639
|
+
" Use start_attach to generate a deep link with the current TOTP code.",
|
|
5669
5640
|
" Scan the QR to locate the relay (open the dog-food URL separately with",
|
|
5670
|
-
" ?debug=1&relay=<wss>&at=<code> or use the
|
|
5641
|
+
" ?debug=1&relay=<wss>&at=<code> or use the start_attach tool):",
|
|
5671
5642
|
"",
|
|
5672
5643
|
qr
|
|
5673
5644
|
].join("\n");
|
|
@@ -5841,7 +5812,7 @@ function makeTunnelStatus(up, wssUrl, droppedAt = null, reissueAttempts = 0) {
|
|
|
5841
5812
|
* Dynamic tool registration (issue #208):
|
|
5842
5813
|
* The server advertises `listChanged: true` so MCP clients can subscribe to
|
|
5843
5814
|
* `notifications/tools/list_changed`. Before any page attaches, only bootstrap
|
|
5844
|
-
* tools (`
|
|
5815
|
+
* tools (`start_attach`, `list_pages`) are listed. Once a target appears,
|
|
5845
5816
|
* the full attach-dependent tool set is added and a `list_changed` notification
|
|
5846
5817
|
* is sent — without requiring a session restart. `runDebugServer` and
|
|
5847
5818
|
* `runLocalDebugServer` start a polling watcher that detects the 0→N target
|
|
@@ -5854,7 +5825,7 @@ function makeTunnelStatus(up, wssUrl, droppedAt = null, reissueAttempts = 0) {
|
|
|
5854
5825
|
*/
|
|
5855
5826
|
/**
|
|
5856
5827
|
* Maximum age (ms) of a page's `lastSeenAt` before it is treated as a ghost
|
|
5857
|
-
* and excluded from the `wait_for_attach` short-circuit in `
|
|
5828
|
+
* and excluded from the `wait_for_attach` short-circuit in `start_attach`
|
|
5858
5829
|
* (issue #610).
|
|
5859
5830
|
*
|
|
5860
5831
|
* Rationale: the env-2 relay is owned by the dev server (unplugin), so every
|
|
@@ -5869,7 +5840,15 @@ function makeTunnelStatus(up, wssUrl, droppedAt = null, reissueAttempts = 0) {
|
|
|
5869
5840
|
*/
|
|
5870
5841
|
const RELAY_SANDBOX_STALE_PAGE_MS = 300 * 1e3;
|
|
5871
5842
|
/**
|
|
5872
|
-
*
|
|
5843
|
+
* Segment length (ms) of the `start_attach` wait loop (issue #626 — TOTP in-call
|
|
5844
|
+
* re-mint). The single-shot `wait_for_attach` of the old attach tool could
|
|
5845
|
+
* not re-mint a TOTP code mid-wait; `start_attach` decomposes the wait into
|
|
5846
|
+
* SEGMENT_MS slices so it can detect an aging code between slices and re-mint a
|
|
5847
|
+
* fresh one without the agent re-calling the tool. 30 s = one TOTP step.
|
|
5848
|
+
*/
|
|
5849
|
+
const START_ATTACH_SEGMENT_MS = 3e4;
|
|
5850
|
+
/**
|
|
5851
|
+
* Predicate used by `start_attach`'s `wait_for_attach` loop to decide
|
|
5873
5852
|
* whether the relay-sandbox connection has a genuinely fresh page attached.
|
|
5874
5853
|
*
|
|
5875
5854
|
* Stale-ghost gating (issue #610): when the dev server restarts with a new
|
|
@@ -5919,13 +5898,28 @@ function extractDeploymentId(schemeUrl) {
|
|
|
5919
5898
|
}
|
|
5920
5899
|
}
|
|
5921
5900
|
/**
|
|
5922
|
-
* Returns `true` when the mode routes to a relay connection (`relay-sandbox
|
|
5923
|
-
* `relay-staging
|
|
5924
|
-
* `relay-staging`/`relay-live` are intoss-private relays — but all three surface
|
|
5925
|
-
* the Tier B / relay-only tool set.
|
|
5901
|
+
* Returns `true` when the mode routes to a relay connection (`relay-sandbox` or
|
|
5902
|
+
* `relay-staging`). Both surface the Tier B / relay-only tool set.
|
|
5926
5903
|
*/
|
|
5927
5904
|
function isRelayMode(mode) {
|
|
5928
|
-
return mode === "relay-sandbox" || mode === "relay-staging"
|
|
5905
|
+
return mode === "relay-sandbox" || mode === "relay-staging";
|
|
5906
|
+
}
|
|
5907
|
+
/**
|
|
5908
|
+
* Maps a `StartDebugMode` to the `McpEnvironment` it routes to (issue #626).
|
|
5909
|
+
* Used by `start_attach`'s mode prologue to decide whether a `switchMode` is
|
|
5910
|
+
* needed: when the active env already equals `envForMode(mode)`, the switch is
|
|
5911
|
+
* skipped (no `tools/list_changed` churn).
|
|
5912
|
+
*
|
|
5913
|
+
* - `local-browser` → `mock`
|
|
5914
|
+
* - `relay-sandbox` → `relay-mobile` (env 2 external-PWA relay)
|
|
5915
|
+
* - `relay-staging` → `relay-dev` (env 3 intoss-private relay)
|
|
5916
|
+
*/
|
|
5917
|
+
function envForMode(mode) {
|
|
5918
|
+
switch (mode) {
|
|
5919
|
+
case "local-browser": return "mock";
|
|
5920
|
+
case "relay-sandbox": return "relay-mobile";
|
|
5921
|
+
case "relay-staging": return "relay-dev";
|
|
5922
|
+
}
|
|
5929
5923
|
}
|
|
5930
5924
|
/**
|
|
5931
5925
|
* Single-attach guard for `run_tests` (#646). Two concurrent runs injecting
|
|
@@ -5948,7 +5942,7 @@ let runTestsInFlight = false;
|
|
|
5948
5942
|
* to resolve before the relay had observed the first inbound CDP message from
|
|
5949
5943
|
* the phone.
|
|
5950
5944
|
*
|
|
5951
|
-
* Timeout note: callers (e.g. the `
|
|
5945
|
+
* Timeout note: callers (e.g. the `start_attach` path) always pass an
|
|
5952
5946
|
* explicit `timeoutMs`, sourced from the factory's `waitForAttachTimeoutMs`
|
|
5953
5947
|
* (default 60 000). That value is forwarded to `waitForFirstTarget`, so it
|
|
5954
5948
|
* overrides that method's own 90 000 signature default — the effective
|
|
@@ -5990,7 +5984,7 @@ function waitForAttachWithEvents(connection, filterFn, timeoutMs, pollIntervalMs
|
|
|
5990
5984
|
* tunnel, which is what makes the tool surface unit-testable.
|
|
5991
5985
|
*
|
|
5992
5986
|
* `tools/list` is two-tiered (issue #208):
|
|
5993
|
-
* - bootstrap (always): `
|
|
5987
|
+
* - bootstrap (always): `start_attach`, `list_pages`
|
|
5994
5988
|
* - attach-dependent (after `connection.listTargets().length > 0`): all others
|
|
5995
5989
|
*
|
|
5996
5990
|
* `CallTool` is NOT tiered — hidden tools still execute (attach errors surface
|
|
@@ -6001,12 +5995,299 @@ function createDebugServer(deps) {
|
|
|
6001
5995
|
const getTotpSecret = deps.getTotpSecret ?? (() => totpSecret);
|
|
6002
5996
|
const readLockFn = readLockDep ?? readServerLock;
|
|
6003
5997
|
const router = routerDep ?? makeSingleConnectionRouter(connection);
|
|
6004
|
-
const resolveEnvironment = getEnvDep ?? (() => deriveEnvironment(router.active.kind,
|
|
6005
|
-
const resolveEnvironmentReason = getEnvReasonDep ?? (() => `derived:kind=${router.active.kind},
|
|
5998
|
+
const resolveEnvironment = getEnvDep ?? (() => deriveEnvironment(router.active.kind, router.activeRelayOrigin));
|
|
5999
|
+
const resolveEnvironmentReason = getEnvReasonDep ?? (() => `derived:kind=${router.active.kind},relayOrigin=${router.activeRelayOrigin ?? "none"}`);
|
|
6006
6000
|
const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
|
|
6001
|
+
/**
|
|
6002
|
+
* Synthesizes an attach URL from stored components with a FRESHLY-minted TOTP
|
|
6003
|
+
* code (issue #626 §3/§4 — the single mint point). Reads the late-bound secret
|
|
6004
|
+
* via `getTotpSecret()` so the project-local `.ait_relay` secret loaded by
|
|
6005
|
+
* `switchMode` is visible. SECRET-HANDLING: the minted code rides inside the
|
|
6006
|
+
* URL's `at=` param only — never logged or returned separately.
|
|
6007
|
+
*/
|
|
6008
|
+
function mintAttachUrl(parts) {
|
|
6009
|
+
const secret = getTotpSecret();
|
|
6010
|
+
const code = secret ? generateTotp(secret) : void 0;
|
|
6011
|
+
return parts.kind === "launcher" ? buildLauncherAttachUrl(parts.tunnelHttpUrl, parts.wssUrl, code, {
|
|
6012
|
+
name: parts.appName,
|
|
6013
|
+
...parts.selfdebug ? { selfdebug: true } : {}
|
|
6014
|
+
}) : buildDeepLinkAttachUrl(parts.schemeUrl, parts.wssUrl, code);
|
|
6015
|
+
}
|
|
6016
|
+
/** Builds the fresh TOTP metadata (expiresAt window) for a tool result. */
|
|
6017
|
+
function buildTotpMeta() {
|
|
6018
|
+
const secret = getTotpSecret();
|
|
6019
|
+
if (secret === void 0 || secret === "") return void 0;
|
|
6020
|
+
const STEP_SECONDS = 30;
|
|
6021
|
+
const expiresAtMs = nowMs() + 6 * STEP_SECONDS * 1e3;
|
|
6022
|
+
return {
|
|
6023
|
+
enabled: true,
|
|
6024
|
+
ttlSeconds: 6 * STEP_SECONDS,
|
|
6025
|
+
expiresAt: new Date(expiresAtMs).toISOString()
|
|
6026
|
+
};
|
|
6027
|
+
}
|
|
6028
|
+
/**
|
|
6029
|
+
* Env-specific validation + component bundle for `start_attach` (issue #626).
|
|
6030
|
+
* Branches on `env`: `relay-mobile` reads AIT_TUNNEL_BASE_URL + builds launcher
|
|
6031
|
+
* parts; `relay-dev` requires scheme_url + builds scheme parts. Returns
|
|
6032
|
+
* `{ ok: false, error }` with a ready McpResult on any failure.
|
|
6033
|
+
*/
|
|
6034
|
+
async function prepareAttach(env, args, conn) {
|
|
6035
|
+
const selfdebug = args?.selfdebug === true;
|
|
6036
|
+
if (selfdebug && env !== "relay-mobile") return {
|
|
6037
|
+
ok: false,
|
|
6038
|
+
error: mcpError("start_attach: selfdebug=true는 env 2 / relay-sandbox 전용 기능입니다. 현재 환경(env 3)에서는 launcher가 없어 self-target 모드를 지원하지 않습니다. launcher self-target이 필요하다면 relay-sandbox 모드로 전환하세요.")
|
|
6039
|
+
};
|
|
6040
|
+
if (env === "relay-mobile") {
|
|
6041
|
+
const rawProjectRoot = args?.projectRoot;
|
|
6042
|
+
const buildProjectRoot = typeof rawProjectRoot === "string" ? rawProjectRoot : void 0;
|
|
6043
|
+
let tunnelHttpUrl = process.env.AIT_TUNNEL_BASE_URL?.trim() ?? "";
|
|
6044
|
+
if (tunnelHttpUrl === "" && buildProjectRoot !== void 0) {
|
|
6045
|
+
const { readRelayUrls } = await import("../relay-url-store-CwKT7i04.js");
|
|
6046
|
+
tunnelHttpUrl = (await readRelayUrls({ projectRoot: buildProjectRoot }))?.tunnelBaseUrl ?? "";
|
|
6047
|
+
}
|
|
6048
|
+
if (tunnelHttpUrl === "") return {
|
|
6049
|
+
ok: false,
|
|
6050
|
+
error: mcpError("start_attach(mobile): AIT_TUNNEL_BASE_URL이 설정되지 않았습니다. dev 서버가 tunnel:{cdp:true}로 기동 중이면 .ait_urls 파일이 자동 생성돼 있어야 합니다. 자동 발견이 되지 않을 경우 앱 HTTP 터널 URL을 AIT_TUNNEL_BASE_URL 환경변수로 직접 전달하세요.")
|
|
6051
|
+
};
|
|
6052
|
+
const tunnelStatus = getTunnelStatus();
|
|
6053
|
+
if (!tunnelStatus.up || tunnelStatus.wssUrl === null) return {
|
|
6054
|
+
ok: false,
|
|
6055
|
+
error: mcpError("start_attach(mobile): relay wssUrl이 아직 설정되지 않았습니다. unplugin tunnel:{cdp:true}가 relay를 완전히 기동할 때까지 잠시 후 다시 시도하세요.")
|
|
6056
|
+
};
|
|
6057
|
+
const secret = getTotpSecret();
|
|
6058
|
+
if (secret === void 0 || secret === "") return {
|
|
6059
|
+
ok: false,
|
|
6060
|
+
error: mcpError("start_attach(relay): TOTP secret(AIT_DEBUG_TOTP_SECRET)이 설정되지 않았습니다. relay 환경은 TOTP 인증이 필수입니다 — relay를 secret과 함께 재기동하세요.")
|
|
6061
|
+
};
|
|
6062
|
+
let launcherAppName;
|
|
6063
|
+
if (buildProjectRoot !== void 0) try {
|
|
6064
|
+
const { readFileSync } = await import("node:fs");
|
|
6065
|
+
const pkgRaw = readFileSync(`${buildProjectRoot}/package.json`, "utf8");
|
|
6066
|
+
const pkg = JSON.parse(pkgRaw);
|
|
6067
|
+
const rawName = typeof pkg.name === "string" ? pkg.name : "";
|
|
6068
|
+
launcherAppName = (rawName.includes("/") ? rawName.slice(rawName.indexOf("/") + 1) : rawName).trim() || void 0;
|
|
6069
|
+
} catch {}
|
|
6070
|
+
const parts = {
|
|
6071
|
+
kind: "launcher",
|
|
6072
|
+
tunnelHttpUrl,
|
|
6073
|
+
wssUrl: tunnelStatus.wssUrl,
|
|
6074
|
+
appName: launcherAppName,
|
|
6075
|
+
...selfdebug ? { selfdebug: true } : {}
|
|
6076
|
+
};
|
|
6077
|
+
const connAsAny = conn;
|
|
6078
|
+
const getLastSeenAt = typeof connAsAny.getTargetLastSeenAt === "function" ? (id) => connAsAny.getTargetLastSeenAt(id) : null;
|
|
6079
|
+
const callNow = nowMs();
|
|
6080
|
+
const isMatchingPage = (pages) => isSandboxPageFresh(pages, getLastSeenAt, callNow, stalePageThresholdMs);
|
|
6081
|
+
const buildTimeoutError = (baseText, timeoutSec, observed) => {
|
|
6082
|
+
const observedUrls = observed.slice(0, 3).map((p) => p.url.slice(0, 80)).join(", ");
|
|
6083
|
+
return `${baseText}\n\nNo page attached within ${timeoutSec}s${observed.length > 0 ? ` — previously attached pages: [${observedUrls}]` : ""} — launcher QR을 폰 카메라로 스캔한 뒤 call list_pages를 다시 호출하세요.`;
|
|
6084
|
+
};
|
|
6085
|
+
return {
|
|
6086
|
+
ok: true,
|
|
6087
|
+
parts,
|
|
6088
|
+
isMatchingPage,
|
|
6089
|
+
buildTimeoutError,
|
|
6090
|
+
authorityWarning: void 0,
|
|
6091
|
+
totpMeta: buildTotpMeta()
|
|
6092
|
+
};
|
|
6093
|
+
}
|
|
6094
|
+
const schemeUrl = args?.scheme_url;
|
|
6095
|
+
if (typeof schemeUrl !== "string" || schemeUrl === "") return {
|
|
6096
|
+
ok: false,
|
|
6097
|
+
error: mcpError("start_attach: scheme_url이 비어 있습니다. `ait deploy --scheme-only`가 출력하는 intoss-private:// URL을 인자로 전달하세요. 환경 2(mobile)라면 scheme_url 대신 AIT_TUNNEL_BASE_URL을 설정하세요.")
|
|
6098
|
+
};
|
|
6099
|
+
{
|
|
6100
|
+
const relaySecret = getTotpSecret();
|
|
6101
|
+
if (relaySecret === void 0 || relaySecret === "") return {
|
|
6102
|
+
ok: false,
|
|
6103
|
+
error: mcpError("start_attach(relay): TOTP secret(AIT_DEBUG_TOTP_SECRET)이 설정되지 않았습니다. relay 환경은 TOTP 인증이 필수입니다 — relay를 secret과 함께 재기동하세요.")
|
|
6104
|
+
};
|
|
6105
|
+
}
|
|
6106
|
+
const tunnelForBuild = getTunnelStatus();
|
|
6107
|
+
if (!tunnelForBuild.up || tunnelForBuild.wssUrl === null) return {
|
|
6108
|
+
ok: false,
|
|
6109
|
+
error: classifyToolError(/* @__PURE__ */ new Error("tunnel-down:"), "start_attach")
|
|
6110
|
+
};
|
|
6111
|
+
const authorityWarning = validateSchemeAuthority(schemeUrl) ?? void 0;
|
|
6112
|
+
const parts = {
|
|
6113
|
+
kind: "scheme",
|
|
6114
|
+
schemeUrl,
|
|
6115
|
+
wssUrl: tunnelForBuild.wssUrl
|
|
6116
|
+
};
|
|
6117
|
+
const deploymentId = extractDeploymentId(schemeUrl);
|
|
6118
|
+
if (!deploymentId) logInfo("tool.call", {
|
|
6119
|
+
tool: "start_attach",
|
|
6120
|
+
msg: "no _deploymentId in scheme_url; matching on presence only"
|
|
6121
|
+
});
|
|
6122
|
+
const isMatchingPage = (pages) => {
|
|
6123
|
+
if (pages.length === 0) return false;
|
|
6124
|
+
if (deploymentId === null) return true;
|
|
6125
|
+
return pages.some((p) => p.url.includes(deploymentId));
|
|
6126
|
+
};
|
|
6127
|
+
const buildTimeoutError = (baseText, timeoutSec, observed) => {
|
|
6128
|
+
const observedUrls = observed.slice(0, 3).map((p) => p.url.slice(0, 80)).join(", ");
|
|
6129
|
+
const observedNote = observed.length > 0 ? ` — previously attached pages: [${observedUrls}]` : "";
|
|
6130
|
+
return `${baseText}\n\nNo page${deploymentId ? ` matching deploymentId=${deploymentId}` : ""} attached within ${timeoutSec}s${observedNote} — call list_pages to retry.`;
|
|
6131
|
+
};
|
|
6132
|
+
return {
|
|
6133
|
+
ok: true,
|
|
6134
|
+
parts,
|
|
6135
|
+
isMatchingPage,
|
|
6136
|
+
buildTimeoutError,
|
|
6137
|
+
authorityWarning,
|
|
6138
|
+
totpMeta: buildTotpMeta()
|
|
6139
|
+
};
|
|
6140
|
+
}
|
|
6141
|
+
/**
|
|
6142
|
+
* QR render + browser open + segmented attach wait with in-call TOTP re-mint
|
|
6143
|
+
* (issue #626 §3). Shared by env-2 and env-3 (4 render paths:
|
|
6144
|
+
* headless / browser-opened / browser-open-failed / no-http-server).
|
|
6145
|
+
*
|
|
6146
|
+
* The wait is decomposed into `START_ATTACH_SEGMENT_MS` slices. Between slices,
|
|
6147
|
+
* if the current TOTP code has aged past `START_ATTACH_REMINT_THRESHOLD_MS`,
|
|
6148
|
+
* a fresh URL is minted via `mintAttachUrl` and pushed to the dashboard via
|
|
6149
|
+
* `onAttachUrlBuilt` (SSE refresh — NO browser re-open). The `reminted` count
|
|
6150
|
+
* rides in the success/timeout result.
|
|
6151
|
+
*
|
|
6152
|
+
* SECRET-HANDLING: attachUrl encodes tunnel/scheme host + the TOTP `at=` code
|
|
6153
|
+
* in the QR payload only. The browser is opened on a 127.0.0.1 URL only. The
|
|
6154
|
+
* tool result carries `totp.expiresAt` + `reminted` count — never the code.
|
|
6155
|
+
*/
|
|
6156
|
+
async function renderAndMaybeWait(prep, waitForAttach, callTimeoutMs, conn) {
|
|
6157
|
+
const { parts, isMatchingPage, buildTimeoutError, authorityWarning, totpMeta } = prep;
|
|
6158
|
+
let attachUrl = mintAttachUrl(parts);
|
|
6159
|
+
onAttachUrlBuilt?.(parts);
|
|
6160
|
+
let totpIssuedAt = nowMs();
|
|
6161
|
+
let reminted = 0;
|
|
6162
|
+
const relayUrl = parts.wssUrl;
|
|
6163
|
+
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).";
|
|
6164
|
+
const warningPrefix = authorityWarning ? `⚠️ scheme_url 경고: ${authorityWarning}\n\n` : "";
|
|
6165
|
+
const guiAvailable = canOpenBrowser();
|
|
6166
|
+
/** Builds the totp object surfaced in results (fresh expiresAt + reminted). */
|
|
6167
|
+
const totpResult = () => {
|
|
6168
|
+
if (!totpMeta) return void 0;
|
|
6169
|
+
const expiresAtMs = totpIssuedAt + 180 * 1e3;
|
|
6170
|
+
return {
|
|
6171
|
+
enabled: true,
|
|
6172
|
+
ttlSeconds: totpMeta.ttlSeconds,
|
|
6173
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
6174
|
+
...reminted > 0 ? { reminted } : {}
|
|
6175
|
+
};
|
|
6176
|
+
};
|
|
6177
|
+
/**
|
|
6178
|
+
* Segmented wait with TOTP re-mint (issue #626 §3). Resolves with the
|
|
6179
|
+
* attached page list, or rejects on timeout. Between SEGMENT_MS slices it
|
|
6180
|
+
* re-mints when the code has aged past the threshold (max ~4 re-mints over
|
|
6181
|
+
* 600 s). Returns immediately once a matching page attaches (no re-mint).
|
|
6182
|
+
*/
|
|
6183
|
+
async function waitWithRemint() {
|
|
6184
|
+
const deadline = nowMs() + callTimeoutMs;
|
|
6185
|
+
if (isMatchingPage(conn.listTargets())) return conn.listTargets();
|
|
6186
|
+
for (;;) {
|
|
6187
|
+
const remaining = deadline - nowMs();
|
|
6188
|
+
if (remaining <= 0) throw new Error(`start_attach: 타임아웃 (${callTimeoutMs}ms)`);
|
|
6189
|
+
const segmentMs = Math.min(START_ATTACH_SEGMENT_MS, remaining);
|
|
6190
|
+
try {
|
|
6191
|
+
return await waitForAttachWithEvents(conn, isMatchingPage, segmentMs);
|
|
6192
|
+
} catch {
|
|
6193
|
+
if (totpMeta && nowMs() - totpIssuedAt >= 15e4) {
|
|
6194
|
+
attachUrl = mintAttachUrl(parts);
|
|
6195
|
+
onAttachUrlBuilt?.(parts);
|
|
6196
|
+
totpIssuedAt = nowMs();
|
|
6197
|
+
reminted += 1;
|
|
6198
|
+
}
|
|
6199
|
+
}
|
|
6200
|
+
}
|
|
6201
|
+
}
|
|
6202
|
+
/**
|
|
6203
|
+
* Assembles the success result after a page attaches. `baseText` carries the
|
|
6204
|
+
* QR + pre-wait JSON block (the QR the user already scanned). The attach
|
|
6205
|
+
* itself ends the wait, so the QR is moot — what matters now is the final
|
|
6206
|
+
* TOTP state. If the segmented wait re-minted (issue #626 §3), surface the
|
|
6207
|
+
* post-wait `totp` block (fresh `expiresAt` + `reminted` count) so the result
|
|
6208
|
+
* reflects how many times the code rotated during the wait. SECRET-HANDLING:
|
|
6209
|
+
* the totp block carries expiresAt + reminted only — never the code value.
|
|
6210
|
+
*/
|
|
6211
|
+
const successResult = (baseText) => {
|
|
6212
|
+
const pagesResult = listPages(conn, getTunnelStatus());
|
|
6213
|
+
const finalTotp = totpResult();
|
|
6214
|
+
const remintNote = finalTotp && reminted > 0 ? `\n\n${JSON.stringify({ totp: finalTotp }, null, 2)}` : "";
|
|
6215
|
+
return { content: [{
|
|
6216
|
+
type: "text",
|
|
6217
|
+
text: `${baseText}\n\n${JSON.stringify(pagesResult, null, 2)}${remintNote}`
|
|
6218
|
+
}] };
|
|
6219
|
+
};
|
|
6220
|
+
/** Runs the wait (when requested) and returns success/timeout result. */
|
|
6221
|
+
const runWait = async (baseText) => {
|
|
6222
|
+
if (!waitForAttach) return { content: [{
|
|
6223
|
+
type: "text",
|
|
6224
|
+
text: baseText
|
|
6225
|
+
}] };
|
|
6226
|
+
try {
|
|
6227
|
+
await waitWithRemint();
|
|
6228
|
+
} catch {
|
|
6229
|
+
const observed = conn.listTargets();
|
|
6230
|
+
return {
|
|
6231
|
+
content: [{
|
|
6232
|
+
type: "text",
|
|
6233
|
+
text: buildTimeoutError(baseText, callTimeoutMs / 1e3, observed)
|
|
6234
|
+
}],
|
|
6235
|
+
isError: true
|
|
6236
|
+
};
|
|
6237
|
+
}
|
|
6238
|
+
return successResult(baseText);
|
|
6239
|
+
};
|
|
6240
|
+
if (!guiAvailable) {
|
|
6241
|
+
const headlessNote = "GUI 환경이 감지되지 않았습니다 (headless/remote 환경). 텍스트 QR을 폰 카메라로 스캔하거나, 로컬 GUI 환경에서 실행하세요.\n\n";
|
|
6242
|
+
const qr = await renderQr(attachUrl);
|
|
6243
|
+
return runWait(`${warningPrefix}${headlessNote}${header}\n${JSON.stringify({
|
|
6244
|
+
attachUrl,
|
|
6245
|
+
relayUrl,
|
|
6246
|
+
...totpResult() ? { totp: totpResult() } : {}
|
|
6247
|
+
}, null, 2)}\n\n${qr}`);
|
|
6248
|
+
}
|
|
6249
|
+
if (guiAvailable && qrHttpServer) {
|
|
6250
|
+
const browserResult = await openQrInBrowser(qrHttpServer.buildAttachPageUrl(attachUrl), `http://127.0.0.1:${qrHttpServer.port}/qr.png?u=${encodeURIComponent(attachUrl)}`);
|
|
6251
|
+
if (browserResult.opened) {
|
|
6252
|
+
const retriedNote = browserResult.retried ? " (1회 retry 후 성공)" : "";
|
|
6253
|
+
const openResult = {
|
|
6254
|
+
attempted: true,
|
|
6255
|
+
succeeded: true,
|
|
6256
|
+
...browserResult.retried ? { retried: true } : {}
|
|
6257
|
+
};
|
|
6258
|
+
return runWait(`${warningPrefix}${header}\n${JSON.stringify({
|
|
6259
|
+
relayUrl,
|
|
6260
|
+
openResult,
|
|
6261
|
+
...totpResult() ? { totp: totpResult() } : {}
|
|
6262
|
+
}, null, 2)}\n\n브라우저에서 QR을 열었습니다${retriedNote}. 폰 카메라로 스캔하세요.\nURL: ${browserResult.httpUrl}`);
|
|
6263
|
+
}
|
|
6264
|
+
const openResult = {
|
|
6265
|
+
attempted: true,
|
|
6266
|
+
succeeded: false,
|
|
6267
|
+
failureReason: browserResult.error ?? "브라우저 실행 후보 모두 실패",
|
|
6268
|
+
pngUrl: browserResult.pngUrl,
|
|
6269
|
+
...browserResult.stderrSummary ? { stderrSummary: browserResult.stderrSummary } : {}
|
|
6270
|
+
};
|
|
6271
|
+
const stderrNote = browserResult.stderrSummary ? `\nstderr: ${browserResult.stderrSummary}` : "";
|
|
6272
|
+
const fallbackNote = `브라우저 자동 열기에 실패했습니다. 다음 URL을 직접 브라우저에서 여세요:\n${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stderrNote + "\n\n";
|
|
6273
|
+
const qr = await renderQr(attachUrl);
|
|
6274
|
+
return runWait(`${warningPrefix}${fallbackNote}${header}\n${JSON.stringify({
|
|
6275
|
+
attachUrl,
|
|
6276
|
+
relayUrl,
|
|
6277
|
+
openResult,
|
|
6278
|
+
...totpResult() ? { totp: totpResult() } : {}
|
|
6279
|
+
}, null, 2)}\n\n${qr}`);
|
|
6280
|
+
}
|
|
6281
|
+
const qr = await renderQr(attachUrl);
|
|
6282
|
+
return runWait(`${warningPrefix}${header}\n${JSON.stringify({
|
|
6283
|
+
attachUrl,
|
|
6284
|
+
relayUrl,
|
|
6285
|
+
...totpResult() ? { totp: totpResult() } : {}
|
|
6286
|
+
}, null, 2)}\n\n${qr}`);
|
|
6287
|
+
}
|
|
6007
6288
|
const server = new Server({
|
|
6008
6289
|
name: "ait-debug",
|
|
6009
|
-
version: "0.1.
|
|
6290
|
+
version: "0.1.110"
|
|
6010
6291
|
}, { capabilities: { tools: { listChanged: true } } });
|
|
6011
6292
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
6012
6293
|
const conn = router.active;
|
|
@@ -6028,12 +6309,47 @@ function createDebugServer(deps) {
|
|
|
6028
6309
|
if (name === "start_debug") {
|
|
6029
6310
|
const rawMode = request.params.arguments?.mode;
|
|
6030
6311
|
const mode = normalizeStartDebugMode(rawMode);
|
|
6031
|
-
if (mode === null) return mcpError("start_debug: mode가 올바르지 않습니다. 'local-browser' | 'relay-sandbox' | 'relay-staging'
|
|
6032
|
-
const confirm = request.params.arguments?.confirm === true;
|
|
6312
|
+
if (mode === null) return mcpError("start_debug: mode가 올바르지 않습니다. 'local-browser' | 'relay-sandbox' | 'relay-staging' 중 하나를 전달하세요. (relay-live / env 4는 #665에서 제거됐습니다.)");
|
|
6033
6313
|
const rawProjectRoot = request.params.arguments?.projectRoot;
|
|
6034
6314
|
const projectRoot = typeof rawProjectRoot === "string" ? rawProjectRoot : void 0;
|
|
6035
6315
|
try {
|
|
6036
|
-
return jsonResult$1(await router.switchMode(mode,
|
|
6316
|
+
return jsonResult$1(await router.switchMode(mode, projectRoot));
|
|
6317
|
+
} catch (err) {
|
|
6318
|
+
return errorResult(err, name);
|
|
6319
|
+
}
|
|
6320
|
+
}
|
|
6321
|
+
if (name === "start_attach") {
|
|
6322
|
+
const args = request.params.arguments;
|
|
6323
|
+
let attachConn = conn;
|
|
6324
|
+
const rawMode = args?.mode;
|
|
6325
|
+
if (rawMode !== void 0) {
|
|
6326
|
+
const mode = normalizeStartDebugMode(rawMode);
|
|
6327
|
+
if (mode === null || mode === "local-browser") return mcpError("start_attach: mode가 올바르지 않습니다. 'relay-sandbox' | 'relay-staging' 중 하나를 전달하세요 (local-browser는 QR attach가 없어 start_attach에서 지원하지 않습니다).");
|
|
6328
|
+
const targetEnv = envForMode(mode);
|
|
6329
|
+
if (resolveEnvironment() !== targetEnv) {
|
|
6330
|
+
const rawProjectRoot = args?.projectRoot;
|
|
6331
|
+
const projectRoot = typeof rawProjectRoot === "string" ? rawProjectRoot : void 0;
|
|
6332
|
+
try {
|
|
6333
|
+
await router.switchMode(mode, projectRoot);
|
|
6334
|
+
} catch (err) {
|
|
6335
|
+
return errorResult(err, name);
|
|
6336
|
+
}
|
|
6337
|
+
attachConn = router.active;
|
|
6338
|
+
}
|
|
6339
|
+
}
|
|
6340
|
+
const attachEnv = resolveEnvironment();
|
|
6341
|
+
if (!isRelayEnv(attachEnv)) return mcpError("start_attach: relay 전용 tool입니다 (env 2 / relay-sandbox 또는 env 3 / relay-staging). 현재 환경은 'local-browser'(mock)입니다 — mode 인자로 'relay-sandbox' 또는 'relay-staging'을 전달하거나, 먼저 relay 모드로 전환하세요.");
|
|
6342
|
+
const waitForAttach = true;
|
|
6343
|
+
const rawWaitTimeout = args?.wait_timeout_seconds;
|
|
6344
|
+
const callTimeoutMs = (() => {
|
|
6345
|
+
if (typeof rawWaitTimeout !== "number" || !Number.isFinite(rawWaitTimeout)) return waitForAttachTimeoutMs;
|
|
6346
|
+
if (rawWaitTimeout <= 0) return waitForAttachTimeoutMs;
|
|
6347
|
+
return Math.round(Math.max(1, Math.min(600, rawWaitTimeout))) * 1e3;
|
|
6348
|
+
})();
|
|
6349
|
+
try {
|
|
6350
|
+
const prep = await prepareAttach(attachEnv, args, attachConn);
|
|
6351
|
+
if (!prep.ok) return prep.error;
|
|
6352
|
+
return await renderAndMaybeWait(prep, waitForAttach, callTimeoutMs, attachConn);
|
|
6037
6353
|
} catch (err) {
|
|
6038
6354
|
return errorResult(err, name);
|
|
6039
6355
|
}
|
|
@@ -6078,386 +6394,6 @@ function createDebugServer(deps) {
|
|
|
6078
6394
|
} catch (err) {
|
|
6079
6395
|
return errorResult(err, name);
|
|
6080
6396
|
}
|
|
6081
|
-
if (name === "build_attach_url") {
|
|
6082
|
-
const waitForAttach = request.params.arguments?.wait_for_attach === true;
|
|
6083
|
-
const selfdebug = request.params.arguments?.selfdebug === true;
|
|
6084
|
-
const rawWaitTimeout = request.params.arguments?.wait_timeout_seconds;
|
|
6085
|
-
const callTimeoutMs = (() => {
|
|
6086
|
-
if (typeof rawWaitTimeout !== "number" || !Number.isFinite(rawWaitTimeout)) return waitForAttachTimeoutMs;
|
|
6087
|
-
const clamped = Math.max(1, Math.min(600, rawWaitTimeout));
|
|
6088
|
-
if (rawWaitTimeout <= 0) return waitForAttachTimeoutMs;
|
|
6089
|
-
return Math.round(clamped) * 1e3;
|
|
6090
|
-
})();
|
|
6091
|
-
if (selfdebug && env !== "relay-mobile") return mcpError("build_attach_url: selfdebug=true는 env 2 / relay-sandbox 전용 기능입니다. 현재 환경(env 3/4)에서는 launcher가 없어 self-target 모드를 지원하지 않습니다. launcher self-target이 필요하다면 relay-sandbox 모드로 재시작하세요.");
|
|
6092
|
-
if (env === "relay-mobile") {
|
|
6093
|
-
const rawBuildProjectRoot = request.params.arguments?.projectRoot;
|
|
6094
|
-
const buildProjectRoot = typeof rawBuildProjectRoot === "string" ? rawBuildProjectRoot : void 0;
|
|
6095
|
-
let tunnelHttpUrl = process.env.AIT_TUNNEL_BASE_URL?.trim() ?? "";
|
|
6096
|
-
if (tunnelHttpUrl === "" && buildProjectRoot !== void 0) {
|
|
6097
|
-
const { readRelayUrls } = await import("../relay-url-store-CwKT7i04.js");
|
|
6098
|
-
tunnelHttpUrl = (await readRelayUrls({ projectRoot: buildProjectRoot }))?.tunnelBaseUrl ?? "";
|
|
6099
|
-
}
|
|
6100
|
-
if (tunnelHttpUrl === "") return mcpError("build_attach_url(mobile): AIT_TUNNEL_BASE_URL이 설정되지 않았습니다. dev 서버가 tunnel:{cdp:true}로 기동 중이면 .ait_urls 파일이 자동 생성돼 있어야 합니다. 자동 발견이 되지 않을 경우 앱 HTTP 터널 URL을 AIT_TUNNEL_BASE_URL 환경변수로 직접 전달하세요.");
|
|
6101
|
-
const tunnelStatus = getTunnelStatus();
|
|
6102
|
-
if (!tunnelStatus.up || tunnelStatus.wssUrl === null) return mcpError("build_attach_url(mobile): relay wssUrl이 아직 설정되지 않았습니다. unplugin tunnel:{cdp:true}가 relay를 완전히 기동할 때까지 잠시 후 다시 시도하세요.");
|
|
6103
|
-
const secret = getTotpSecret();
|
|
6104
|
-
if (secret === void 0 || secret === "") return mcpError("build_attach_url(relay): TOTP secret(AIT_DEBUG_TOTP_SECRET)이 설정되지 않았습니다. relay 환경은 TOTP 인증이 필수입니다 — relay를 secret과 함께 재기동하세요.");
|
|
6105
|
-
let totpCode;
|
|
6106
|
-
let totpMeta;
|
|
6107
|
-
{
|
|
6108
|
-
const now = Date.now();
|
|
6109
|
-
totpCode = generateTotp(secret, now);
|
|
6110
|
-
const STEP_SECONDS = 30;
|
|
6111
|
-
const expiresAtMs = now + 6 * STEP_SECONDS * 1e3;
|
|
6112
|
-
totpMeta = {
|
|
6113
|
-
enabled: true,
|
|
6114
|
-
ttlSeconds: 6 * STEP_SECONDS,
|
|
6115
|
-
expiresAt: new Date(expiresAtMs).toISOString()
|
|
6116
|
-
};
|
|
6117
|
-
}
|
|
6118
|
-
let launcherAppName;
|
|
6119
|
-
if (buildProjectRoot !== void 0) try {
|
|
6120
|
-
const { readFileSync } = await import("node:fs");
|
|
6121
|
-
const pkgRaw = readFileSync(`${buildProjectRoot}/package.json`, "utf8");
|
|
6122
|
-
const pkg = JSON.parse(pkgRaw);
|
|
6123
|
-
const rawName = typeof pkg.name === "string" ? pkg.name : "";
|
|
6124
|
-
launcherAppName = (rawName.includes("/") ? rawName.slice(rawName.indexOf("/") + 1) : rawName).trim() || void 0;
|
|
6125
|
-
} catch {}
|
|
6126
|
-
const attachUrl = buildLauncherAttachUrl(tunnelHttpUrl, tunnelStatus.wssUrl, totpCode, {
|
|
6127
|
-
name: launcherAppName,
|
|
6128
|
-
...selfdebug ? { selfdebug: true } : {}
|
|
6129
|
-
});
|
|
6130
|
-
onAttachUrlBuilt?.({
|
|
6131
|
-
kind: "launcher",
|
|
6132
|
-
tunnelHttpUrl,
|
|
6133
|
-
wssUrl: tunnelStatus.wssUrl,
|
|
6134
|
-
appName: launcherAppName
|
|
6135
|
-
});
|
|
6136
|
-
const relayUrl = tunnelStatus.wssUrl;
|
|
6137
|
-
const totp = totpMeta;
|
|
6138
|
-
const connAsAny = conn;
|
|
6139
|
-
const getLastSeenAt = typeof connAsAny.getTargetLastSeenAt === "function" ? (id) => connAsAny.getTargetLastSeenAt(id) : null;
|
|
6140
|
-
const callNow = nowMs();
|
|
6141
|
-
const isMatchingPage = (pages) => isSandboxPageFresh(pages, getLastSeenAt, callNow, stalePageThresholdMs);
|
|
6142
|
-
const buildTimeoutError = (baseText, timeoutSec, observed) => {
|
|
6143
|
-
const observedUrls = observed.slice(0, 3).map((p) => p.url.slice(0, 80)).join(", ");
|
|
6144
|
-
return `${baseText}\n\nNo page attached within ${timeoutSec}s${observed.length > 0 ? ` — previously attached pages: [${observedUrls}]` : ""} — launcher QR을 폰 카메라로 스캔한 뒤 call list_pages를 다시 호출하세요.`;
|
|
6145
|
-
};
|
|
6146
|
-
return await (async () => {
|
|
6147
|
-
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).";
|
|
6148
|
-
const warningPrefix = "";
|
|
6149
|
-
const guiAvailable = canOpenBrowser();
|
|
6150
|
-
if (!guiAvailable) {
|
|
6151
|
-
const headlessNote = "GUI 환경이 감지되지 않았습니다 (headless/remote 환경). 텍스트 QR을 폰 카메라로 스캔하거나, 로컬 GUI 환경에서 실행하세요.\n\n";
|
|
6152
|
-
const qrHeadless = await renderQr(attachUrl);
|
|
6153
|
-
const headlessText = `${warningPrefix}${headlessNote}${header}\n${JSON.stringify({
|
|
6154
|
-
attachUrl,
|
|
6155
|
-
relayUrl,
|
|
6156
|
-
...totp ? { totp } : {}
|
|
6157
|
-
}, null, 2)}\n\n${qrHeadless}`;
|
|
6158
|
-
if (!waitForAttach) return { content: [{
|
|
6159
|
-
type: "text",
|
|
6160
|
-
text: headlessText
|
|
6161
|
-
}] };
|
|
6162
|
-
let attachedPagesHl = [];
|
|
6163
|
-
try {
|
|
6164
|
-
attachedPagesHl = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
|
|
6165
|
-
} catch {
|
|
6166
|
-
attachedPagesHl = conn.listTargets();
|
|
6167
|
-
return {
|
|
6168
|
-
content: [{
|
|
6169
|
-
type: "text",
|
|
6170
|
-
text: buildTimeoutError(headlessText, callTimeoutMs / 1e3, attachedPagesHl)
|
|
6171
|
-
}],
|
|
6172
|
-
isError: true
|
|
6173
|
-
};
|
|
6174
|
-
}
|
|
6175
|
-
const pagesResultHl = listPages(conn, getTunnelStatus());
|
|
6176
|
-
return { content: [{
|
|
6177
|
-
type: "text",
|
|
6178
|
-
text: `${headlessText}\n\n${JSON.stringify(pagesResultHl, null, 2)}`
|
|
6179
|
-
}] };
|
|
6180
|
-
}
|
|
6181
|
-
if (guiAvailable && qrHttpServer) {
|
|
6182
|
-
const browserResult = await openQrInBrowser(qrHttpServer.buildAttachPageUrl(attachUrl), `http://127.0.0.1:${qrHttpServer.port}/qr.png?u=${encodeURIComponent(attachUrl)}`);
|
|
6183
|
-
if (browserResult.opened) {
|
|
6184
|
-
const retriedNote = browserResult.retried ? " (1회 retry 후 성공)" : "";
|
|
6185
|
-
const openResult = {
|
|
6186
|
-
attempted: true,
|
|
6187
|
-
succeeded: true,
|
|
6188
|
-
...browserResult.retried ? { retried: true } : {}
|
|
6189
|
-
};
|
|
6190
|
-
const shortText = `${warningPrefix}${header}\n${JSON.stringify({
|
|
6191
|
-
relayUrl,
|
|
6192
|
-
openResult,
|
|
6193
|
-
...totp ? { totp } : {}
|
|
6194
|
-
}, null, 2)}\n\n브라우저에서 QR을 열었습니다${retriedNote}. 폰 카메라로 스캔하세요.\nURL: ${browserResult.httpUrl}`;
|
|
6195
|
-
if (!waitForAttach) return { content: [{
|
|
6196
|
-
type: "text",
|
|
6197
|
-
text: shortText
|
|
6198
|
-
}] };
|
|
6199
|
-
let attachedPages = [];
|
|
6200
|
-
try {
|
|
6201
|
-
attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
|
|
6202
|
-
} catch {
|
|
6203
|
-
attachedPages = conn.listTargets();
|
|
6204
|
-
return {
|
|
6205
|
-
content: [{
|
|
6206
|
-
type: "text",
|
|
6207
|
-
text: buildTimeoutError(shortText, callTimeoutMs / 1e3, attachedPages)
|
|
6208
|
-
}],
|
|
6209
|
-
isError: true
|
|
6210
|
-
};
|
|
6211
|
-
}
|
|
6212
|
-
const pagesResult = listPages(conn, getTunnelStatus());
|
|
6213
|
-
return { content: [{
|
|
6214
|
-
type: "text",
|
|
6215
|
-
text: `${shortText}\n\n${JSON.stringify(pagesResult, null, 2)}`
|
|
6216
|
-
}] };
|
|
6217
|
-
}
|
|
6218
|
-
const openResult = {
|
|
6219
|
-
attempted: true,
|
|
6220
|
-
succeeded: false,
|
|
6221
|
-
failureReason: browserResult.error ?? "브라우저 실행 후보 모두 실패",
|
|
6222
|
-
pngUrl: browserResult.pngUrl,
|
|
6223
|
-
...browserResult.stderrSummary ? { stderrSummary: browserResult.stderrSummary } : {}
|
|
6224
|
-
};
|
|
6225
|
-
const stderrNote = browserResult.stderrSummary ? `\nstderr: ${browserResult.stderrSummary}` : "";
|
|
6226
|
-
const fallbackNote = `브라우저 자동 열기에 실패했습니다. 다음 URL을 직접 브라우저에서 여세요:\n${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stderrNote + "\n\n";
|
|
6227
|
-
const qr = await renderQr(attachUrl);
|
|
6228
|
-
const baseText = `${warningPrefix}${fallbackNote}${header}\n${JSON.stringify({
|
|
6229
|
-
attachUrl,
|
|
6230
|
-
relayUrl,
|
|
6231
|
-
openResult,
|
|
6232
|
-
...totp ? { totp } : {}
|
|
6233
|
-
}, null, 2)}\n\n${qr}`;
|
|
6234
|
-
if (!waitForAttach) return { content: [{
|
|
6235
|
-
type: "text",
|
|
6236
|
-
text: baseText
|
|
6237
|
-
}] };
|
|
6238
|
-
let attachedPagesFb = [];
|
|
6239
|
-
try {
|
|
6240
|
-
attachedPagesFb = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
|
|
6241
|
-
} catch {
|
|
6242
|
-
attachedPagesFb = conn.listTargets();
|
|
6243
|
-
return {
|
|
6244
|
-
content: [{
|
|
6245
|
-
type: "text",
|
|
6246
|
-
text: buildTimeoutError(baseText, callTimeoutMs / 1e3, attachedPagesFb)
|
|
6247
|
-
}],
|
|
6248
|
-
isError: true
|
|
6249
|
-
};
|
|
6250
|
-
}
|
|
6251
|
-
const pagesResultFb = listPages(conn, getTunnelStatus());
|
|
6252
|
-
return { content: [{
|
|
6253
|
-
type: "text",
|
|
6254
|
-
text: `${baseText}\n\n${JSON.stringify(pagesResultFb, null, 2)}`
|
|
6255
|
-
}] };
|
|
6256
|
-
}
|
|
6257
|
-
const qr = await renderQr(attachUrl);
|
|
6258
|
-
const baseText = `${warningPrefix}${header}\n${JSON.stringify({
|
|
6259
|
-
attachUrl,
|
|
6260
|
-
relayUrl,
|
|
6261
|
-
...totp ? { totp } : {}
|
|
6262
|
-
}, null, 2)}\n\n${qr}`;
|
|
6263
|
-
if (!waitForAttach) return { content: [{
|
|
6264
|
-
type: "text",
|
|
6265
|
-
text: baseText
|
|
6266
|
-
}] };
|
|
6267
|
-
let attachedPages = [];
|
|
6268
|
-
try {
|
|
6269
|
-
attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
|
|
6270
|
-
} catch {
|
|
6271
|
-
attachedPages = conn.listTargets();
|
|
6272
|
-
return {
|
|
6273
|
-
content: [{
|
|
6274
|
-
type: "text",
|
|
6275
|
-
text: buildTimeoutError(baseText, callTimeoutMs / 1e3, attachedPages)
|
|
6276
|
-
}],
|
|
6277
|
-
isError: true
|
|
6278
|
-
};
|
|
6279
|
-
}
|
|
6280
|
-
const pagesResult = listPages(conn, getTunnelStatus());
|
|
6281
|
-
return { content: [{
|
|
6282
|
-
type: "text",
|
|
6283
|
-
text: `${baseText}\n\n${JSON.stringify(pagesResult, null, 2)}`
|
|
6284
|
-
}] };
|
|
6285
|
-
})();
|
|
6286
|
-
}
|
|
6287
|
-
const schemeUrl = request.params.arguments?.scheme_url;
|
|
6288
|
-
if (typeof schemeUrl !== "string" || schemeUrl === "") return mcpError("build_attach_url: scheme_url이 비어 있습니다. `ait deploy --scheme-only`가 출력하는 intoss-private:// URL을 인자로 전달하세요. 환경 2(mobile)라면 scheme_url 대신 AIT_TUNNEL_BASE_URL을 설정하세요.");
|
|
6289
|
-
const deploymentId = extractDeploymentId(schemeUrl);
|
|
6290
|
-
if (!deploymentId) logInfo("tool.call", {
|
|
6291
|
-
tool: "build_attach_url",
|
|
6292
|
-
msg: "no _deploymentId in scheme_url; matching on presence only"
|
|
6293
|
-
});
|
|
6294
|
-
/** Returns true when the page list satisfies the attach condition. */
|
|
6295
|
-
const isMatchingPage = (pages) => {
|
|
6296
|
-
if (pages.length === 0) return false;
|
|
6297
|
-
if (deploymentId === null) return true;
|
|
6298
|
-
return pages.some((p) => p.url.includes(deploymentId));
|
|
6299
|
-
};
|
|
6300
|
-
/** Builds a timeout error message with diagnostic context. */
|
|
6301
|
-
const buildTimeoutError = (baseText, timeoutSec, observed) => {
|
|
6302
|
-
const observedUrls = observed.slice(0, 3).map((p) => p.url.slice(0, 80)).join(", ");
|
|
6303
|
-
const observedNote = observed.length > 0 ? ` — previously attached pages: [${observedUrls}]` : "";
|
|
6304
|
-
return `${baseText}\n\nNo page${deploymentId ? ` matching deploymentId=${deploymentId}` : ""} attached within ${timeoutSec}s${observedNote} — call list_pages to retry.`;
|
|
6305
|
-
};
|
|
6306
|
-
{
|
|
6307
|
-
const relaySecret = getTotpSecret();
|
|
6308
|
-
if (relaySecret === void 0 || relaySecret === "") return mcpError("build_attach_url(relay): TOTP secret(AIT_DEBUG_TOTP_SECRET)이 설정되지 않았습니다. relay 환경은 TOTP 인증이 필수입니다 — relay를 secret과 함께 재기동하세요.");
|
|
6309
|
-
}
|
|
6310
|
-
try {
|
|
6311
|
-
const tunnelForBuild = getTunnelStatus();
|
|
6312
|
-
const { attachUrl, relayUrl, authorityWarning, totp } = buildAttachUrl(schemeUrl, tunnelForBuild, getTotpSecret());
|
|
6313
|
-
if (tunnelForBuild.wssUrl !== null) onAttachUrlBuilt?.({
|
|
6314
|
-
kind: "scheme",
|
|
6315
|
-
schemeUrl,
|
|
6316
|
-
wssUrl: tunnelForBuild.wssUrl
|
|
6317
|
-
});
|
|
6318
|
-
const warningPrefix = authorityWarning ? `⚠️ scheme_url 경고: ${authorityWarning}\n\n` : "";
|
|
6319
|
-
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).";
|
|
6320
|
-
const guiAvailable = canOpenBrowser();
|
|
6321
|
-
if (!guiAvailable) {
|
|
6322
|
-
const headlessNote = "GUI 환경이 감지되지 않았습니다 (headless/remote 환경). 텍스트 QR을 폰 카메라로 스캔하거나, 로컬 GUI 환경에서 실행하세요.\n\n";
|
|
6323
|
-
const qrHeadless = await renderQr(attachUrl);
|
|
6324
|
-
const headlessText = `${warningPrefix}${headlessNote}${header}\n${JSON.stringify({
|
|
6325
|
-
attachUrl,
|
|
6326
|
-
relayUrl,
|
|
6327
|
-
...totp ? { totp } : {}
|
|
6328
|
-
}, null, 2)}\n\n${qrHeadless}`;
|
|
6329
|
-
if (!waitForAttach) return { content: [{
|
|
6330
|
-
type: "text",
|
|
6331
|
-
text: headlessText
|
|
6332
|
-
}] };
|
|
6333
|
-
let attachedPagesHl = [];
|
|
6334
|
-
try {
|
|
6335
|
-
attachedPagesHl = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
|
|
6336
|
-
} catch {
|
|
6337
|
-
attachedPagesHl = conn.listTargets();
|
|
6338
|
-
return {
|
|
6339
|
-
content: [{
|
|
6340
|
-
type: "text",
|
|
6341
|
-
text: buildTimeoutError(headlessText, callTimeoutMs / 1e3, attachedPagesHl)
|
|
6342
|
-
}],
|
|
6343
|
-
isError: true
|
|
6344
|
-
};
|
|
6345
|
-
}
|
|
6346
|
-
const pagesResultHl = listPages(conn, getTunnelStatus());
|
|
6347
|
-
return { content: [{
|
|
6348
|
-
type: "text",
|
|
6349
|
-
text: `${headlessText}\n\n${JSON.stringify(pagesResultHl, null, 2)}`
|
|
6350
|
-
}] };
|
|
6351
|
-
}
|
|
6352
|
-
if (guiAvailable && qrHttpServer) {
|
|
6353
|
-
const browserResult = await openQrInBrowser(qrHttpServer.buildAttachPageUrl(attachUrl), `http://127.0.0.1:${qrHttpServer.port}/qr.png?u=${encodeURIComponent(attachUrl)}`);
|
|
6354
|
-
if (browserResult.opened) {
|
|
6355
|
-
const retriedNote = browserResult.retried ? " (1회 retry 후 성공)" : "";
|
|
6356
|
-
const openResult = {
|
|
6357
|
-
attempted: true,
|
|
6358
|
-
succeeded: true,
|
|
6359
|
-
...browserResult.retried ? { retried: true } : {}
|
|
6360
|
-
};
|
|
6361
|
-
const shortText = `${warningPrefix}${header}\n${JSON.stringify({
|
|
6362
|
-
relayUrl,
|
|
6363
|
-
openResult,
|
|
6364
|
-
...totp ? { totp } : {}
|
|
6365
|
-
}, null, 2)}\n\n브라우저에서 QR을 열었습니다${retriedNote}. 폰 카메라로 스캔하세요.\nURL: ${browserResult.httpUrl}`;
|
|
6366
|
-
if (!waitForAttach) return { content: [{
|
|
6367
|
-
type: "text",
|
|
6368
|
-
text: shortText
|
|
6369
|
-
}] };
|
|
6370
|
-
let attachedPages = [];
|
|
6371
|
-
try {
|
|
6372
|
-
attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
|
|
6373
|
-
} catch {
|
|
6374
|
-
attachedPages = conn.listTargets();
|
|
6375
|
-
return {
|
|
6376
|
-
content: [{
|
|
6377
|
-
type: "text",
|
|
6378
|
-
text: buildTimeoutError(shortText, callTimeoutMs / 1e3, attachedPages)
|
|
6379
|
-
}],
|
|
6380
|
-
isError: true
|
|
6381
|
-
};
|
|
6382
|
-
}
|
|
6383
|
-
const pagesResult = listPages(conn, getTunnelStatus());
|
|
6384
|
-
return { content: [{
|
|
6385
|
-
type: "text",
|
|
6386
|
-
text: `${shortText}\n\n${JSON.stringify(pagesResult, null, 2)}`
|
|
6387
|
-
}] };
|
|
6388
|
-
}
|
|
6389
|
-
const openResult = {
|
|
6390
|
-
attempted: true,
|
|
6391
|
-
succeeded: false,
|
|
6392
|
-
failureReason: browserResult.error ?? "브라우저 실행 후보 모두 실패",
|
|
6393
|
-
pngUrl: browserResult.pngUrl,
|
|
6394
|
-
...browserResult.stderrSummary ? { stderrSummary: browserResult.stderrSummary } : {}
|
|
6395
|
-
};
|
|
6396
|
-
const stderrNote = browserResult.stderrSummary ? `\nstderr: ${browserResult.stderrSummary}` : "";
|
|
6397
|
-
const fallbackNote = `브라우저 자동 열기에 실패했습니다. 다음 URL을 직접 브라우저에서 여세요:
|
|
6398
|
-
${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stderrNote + "\n\n";
|
|
6399
|
-
const qr = await renderQr(attachUrl);
|
|
6400
|
-
const baseText = `${warningPrefix}${fallbackNote}${header}\n${JSON.stringify({
|
|
6401
|
-
attachUrl,
|
|
6402
|
-
relayUrl,
|
|
6403
|
-
openResult,
|
|
6404
|
-
...totp ? { totp } : {}
|
|
6405
|
-
}, null, 2)}\n\n${qr}`;
|
|
6406
|
-
if (!waitForAttach) return { content: [{
|
|
6407
|
-
type: "text",
|
|
6408
|
-
text: baseText
|
|
6409
|
-
}] };
|
|
6410
|
-
let attachedPagesFb = [];
|
|
6411
|
-
try {
|
|
6412
|
-
attachedPagesFb = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
|
|
6413
|
-
} catch {
|
|
6414
|
-
attachedPagesFb = conn.listTargets();
|
|
6415
|
-
return {
|
|
6416
|
-
content: [{
|
|
6417
|
-
type: "text",
|
|
6418
|
-
text: buildTimeoutError(baseText, callTimeoutMs / 1e3, attachedPagesFb)
|
|
6419
|
-
}],
|
|
6420
|
-
isError: true
|
|
6421
|
-
};
|
|
6422
|
-
}
|
|
6423
|
-
const pagesResultFb = listPages(conn, getTunnelStatus());
|
|
6424
|
-
return { content: [{
|
|
6425
|
-
type: "text",
|
|
6426
|
-
text: `${baseText}\n\n${JSON.stringify(pagesResultFb, null, 2)}`
|
|
6427
|
-
}] };
|
|
6428
|
-
}
|
|
6429
|
-
const qr = await renderQr(attachUrl);
|
|
6430
|
-
const baseText = `${warningPrefix}${header}\n${JSON.stringify({
|
|
6431
|
-
attachUrl,
|
|
6432
|
-
relayUrl,
|
|
6433
|
-
...totp ? { totp } : {}
|
|
6434
|
-
}, null, 2)}\n\n${qr}`;
|
|
6435
|
-
if (!waitForAttach) return { content: [{
|
|
6436
|
-
type: "text",
|
|
6437
|
-
text: baseText
|
|
6438
|
-
}] };
|
|
6439
|
-
let attachedPages = [];
|
|
6440
|
-
try {
|
|
6441
|
-
attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
|
|
6442
|
-
} catch {
|
|
6443
|
-
attachedPages = conn.listTargets();
|
|
6444
|
-
return {
|
|
6445
|
-
content: [{
|
|
6446
|
-
type: "text",
|
|
6447
|
-
text: buildTimeoutError(baseText, callTimeoutMs / 1e3, attachedPages)
|
|
6448
|
-
}],
|
|
6449
|
-
isError: true
|
|
6450
|
-
};
|
|
6451
|
-
}
|
|
6452
|
-
const pagesResult = listPages(conn, getTunnelStatus());
|
|
6453
|
-
return { content: [{
|
|
6454
|
-
type: "text",
|
|
6455
|
-
text: `${baseText}\n\n${JSON.stringify(pagesResult, null, 2)}`
|
|
6456
|
-
}] };
|
|
6457
|
-
} catch (err) {
|
|
6458
|
-
return errorResult(err, name);
|
|
6459
|
-
}
|
|
6460
|
-
}
|
|
6461
6397
|
try {
|
|
6462
6398
|
await conn.enableDomains();
|
|
6463
6399
|
} catch (err) {
|
|
@@ -6496,7 +6432,7 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
6496
6432
|
case "evaluate": {
|
|
6497
6433
|
const expression = request.params.arguments?.expression;
|
|
6498
6434
|
if (typeof expression !== "string" || expression === "") return mcpError("evaluate: expression 인자가 비어 있습니다. 평가할 JavaScript 표현식을 전달하세요.");
|
|
6499
|
-
if (conn
|
|
6435
|
+
if (!connectionHostsAllowed(conn)) return mcpError("evaluate: 현재 연결된 페이지는 debug 허용 호스트가 아닙니다 (#665). 허용 호스트: localhost, *.trycloudflare.com, *.private-apps.tossmini.com.");
|
|
6500
6436
|
return jsonResult$1(await evaluate(conn, expression));
|
|
6501
6437
|
}
|
|
6502
6438
|
case "call_sdk": {
|
|
@@ -6504,7 +6440,7 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
6504
6440
|
if (typeof sdkName !== "string" || sdkName === "") return mcpError("call_sdk: name 인자가 비어 있습니다. 호출할 SDK 메서드 이름을 전달하세요.");
|
|
6505
6441
|
const rawArgs = request.params.arguments?.args;
|
|
6506
6442
|
const sdkArgs = Array.isArray(rawArgs) ? rawArgs : [];
|
|
6507
|
-
if (conn
|
|
6443
|
+
if (!connectionHostsAllowed(conn)) return mcpError("call_sdk: 현재 연결된 페이지는 debug 허용 호스트가 아닙니다 (#665). 허용 호스트: localhost, *.trycloudflare.com, *.private-apps.tossmini.com.");
|
|
6508
6444
|
const sdkResult = await callSdk(conn, sdkName, sdkArgs);
|
|
6509
6445
|
if (!sdkResult.ok && typeof sdkResult.error === "string" && sdkResult.error.startsWith("sdk-absent:")) return sdkAbsentError("call_sdk", conn.kind === "local");
|
|
6510
6446
|
return envelopeResult$1(sdkResult, name, env, conn.listTargets().length > 0);
|
|
@@ -6518,7 +6454,7 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
6518
6454
|
const projectRoot = typeof rawRoot === "string" ? rawRoot : process.cwd();
|
|
6519
6455
|
const rawTimeout = request.params.arguments?.timeout_ms;
|
|
6520
6456
|
const timeoutMs = typeof rawTimeout === "number" && rawTimeout >= 1e3 && rawTimeout <= 6e5 ? rawTimeout : void 0;
|
|
6521
|
-
if (conn
|
|
6457
|
+
if (!connectionHostsAllowed(conn)) return mcpError("run_tests: 현재 연결된 페이지는 debug 허용 호스트가 아닙니다 (#665). 허용 호스트: localhost, *.trycloudflare.com, *.private-apps.tossmini.com.");
|
|
6522
6458
|
if (runTestsInFlight) return mcpError("run_tests: 이미 다른 테스트 실행이 진행 중입니다 (single-attach 모델: 페이지는 한 번에 하나의 실행만 처리). 완료 후 다시 시도하세요.");
|
|
6523
6459
|
runTestsInFlight = true;
|
|
6524
6460
|
try {
|
|
@@ -6548,24 +6484,52 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
6548
6484
|
}
|
|
6549
6485
|
/**
|
|
6550
6486
|
* Normalizes a raw `start_debug` `mode` argument to a `StartDebugMode`, or
|
|
6551
|
-
* `null` when the value is not one of the
|
|
6552
|
-
* 'local-browser' | 'relay-sandbox' | 'relay-staging'
|
|
6487
|
+
* `null` when the value is not one of the three accepted modes:
|
|
6488
|
+
* 'local-browser' | 'relay-sandbox' | 'relay-staging'
|
|
6553
6489
|
*
|
|
6554
6490
|
* Hard rename (issue #398): the older `local`/`mobile`/`staging`/`live` names
|
|
6555
6491
|
* and their aliases are no longer accepted — pre-1.0, no back-compat.
|
|
6492
|
+
* `relay-live` (env 4) removed in #665.
|
|
6556
6493
|
*/
|
|
6557
6494
|
function normalizeStartDebugMode(raw) {
|
|
6558
|
-
if (raw === "local-browser" || raw === "relay-sandbox" || raw === "relay-staging"
|
|
6495
|
+
if (raw === "local-browser" || raw === "relay-sandbox" || raw === "relay-staging") return raw;
|
|
6559
6496
|
return null;
|
|
6560
6497
|
}
|
|
6561
6498
|
/**
|
|
6499
|
+
* Positive-allowlist kill-switch for side-effect MCP tools (#665).
|
|
6500
|
+
*
|
|
6501
|
+
* Returns `true` when the connection's attached targets are all on allowed
|
|
6502
|
+
* debug hosts (localhost / trycloudflare / private-apps). Returns `false` when
|
|
6503
|
+
* any target's page URL is on a non-allowed host (e.g. `apps.tossmini.com`).
|
|
6504
|
+
*
|
|
6505
|
+
* For local connections this always returns `true` — the local Chromium is
|
|
6506
|
+
* always on localhost. For relay connections without any pages it returns
|
|
6507
|
+
* `true` (no pages = nothing to block; the caller's page-missing guard fires
|
|
6508
|
+
* first).
|
|
6509
|
+
*
|
|
6510
|
+
* SECRET-HANDLING: hostnames are NEVER logged here — only the boolean result
|
|
6511
|
+
* is returned to the caller.
|
|
6512
|
+
*/
|
|
6513
|
+
function connectionHostsAllowed(conn) {
|
|
6514
|
+
if (conn.kind === "local") return true;
|
|
6515
|
+
const pages = conn.listTargets();
|
|
6516
|
+
if (pages.length === 0) return true;
|
|
6517
|
+
return pages.every((p) => {
|
|
6518
|
+
try {
|
|
6519
|
+
return isDebugAllowedHost(new URL(p.url ?? "").hostname);
|
|
6520
|
+
} catch {
|
|
6521
|
+
return false;
|
|
6522
|
+
}
|
|
6523
|
+
});
|
|
6524
|
+
}
|
|
6525
|
+
/**
|
|
6562
6526
|
* Builds a trivial `ConnectionRouter` pinned to a single connection (issue
|
|
6563
6527
|
* #348). Used by `createDebugServer` when no real dual router is injected —
|
|
6564
6528
|
* every existing single-connection test and the `local`-only / `relay`-only
|
|
6565
6529
|
* boot path. `switchMode` here cannot lazily boot another family, so it only
|
|
6566
|
-
* honors a request that matches the connection's own kind
|
|
6567
|
-
*
|
|
6568
|
-
*
|
|
6530
|
+
* honors a request that matches the connection's own kind; any cross-family
|
|
6531
|
+
* request is rejected with a clear "dynamic switch unavailable in this session"
|
|
6532
|
+
* error. `confirm` parameter and `relay-live` gate removed (#665).
|
|
6569
6533
|
*/
|
|
6570
6534
|
function makeSingleConnectionRouter(connection) {
|
|
6571
6535
|
return {
|
|
@@ -6573,18 +6537,15 @@ function makeSingleConnectionRouter(connection) {
|
|
|
6573
6537
|
return connection;
|
|
6574
6538
|
},
|
|
6575
6539
|
activeRelayOrigin: void 0,
|
|
6576
|
-
switchMode(mode,
|
|
6540
|
+
switchMode(mode, _projectRoot) {
|
|
6577
6541
|
if (mode === "relay-sandbox") return Promise.reject(/* @__PURE__ */ new Error("start_debug: 이 세션은 단일 연결만 보유합니다 — 'relay-sandbox'(환경 2 PWA, 외부 relay)로 동적 전환할 수 없습니다 (dual-connection 데몬에서만 지원). MCP 서버를 relay-sandbox 모드로 재시작하세요."));
|
|
6578
6542
|
if (isRelayMode(mode) !== (connection.kind === "relay")) return Promise.reject(/* @__PURE__ */ new Error(`start_debug: 이 세션은 단일 ${connection.kind} 연결만 보유합니다 — '${mode}'로 동적 전환할 수 없습니다 (dual-connection 데몬에서만 지원). MCP 서버를 원하는 모드로 재시작하세요.`));
|
|
6579
|
-
|
|
6580
|
-
setLiveIntent(mode === "relay-live");
|
|
6581
|
-
const environment = deriveEnvironment(connection.kind, getLiveIntent());
|
|
6543
|
+
const environment = deriveEnvironment(connection.kind);
|
|
6582
6544
|
return Promise.resolve({
|
|
6583
6545
|
mode,
|
|
6584
6546
|
environment,
|
|
6585
6547
|
kind: connection.kind,
|
|
6586
|
-
|
|
6587
|
-
nextStep: connection.kind === "relay" ? "build_attach_url로 attach QR을 생성하세요." : "list_pages로 로컬 페이지 attach를 확인하세요."
|
|
6548
|
+
nextStep: connection.kind === "relay" ? "start_attach로 attach QR 생성 + 폰 attach까지 한 번에 진행하세요." : "list_pages로 로컬 페이지 attach를 확인하세요."
|
|
6588
6549
|
});
|
|
6589
6550
|
}
|
|
6590
6551
|
};
|
|
@@ -6599,7 +6560,10 @@ function makeSingleConnectionRouter(connection) {
|
|
|
6599
6560
|
function rebuildAttachUrl(parts) {
|
|
6600
6561
|
const secret = process.env.AIT_DEBUG_TOTP_SECRET;
|
|
6601
6562
|
const code = secret ? generateTotp(secret) : void 0;
|
|
6602
|
-
return parts.kind === "launcher" ? buildLauncherAttachUrl(parts.tunnelHttpUrl, parts.wssUrl, code, {
|
|
6563
|
+
return parts.kind === "launcher" ? buildLauncherAttachUrl(parts.tunnelHttpUrl, parts.wssUrl, code, {
|
|
6564
|
+
name: parts.appName,
|
|
6565
|
+
...parts.selfdebug ? { selfdebug: true } : {}
|
|
6566
|
+
}) : buildDeepLinkAttachUrl(parts.schemeUrl, parts.wssUrl, code);
|
|
6603
6567
|
}
|
|
6604
6568
|
function jsonResult$1(value) {
|
|
6605
6569
|
return { content: [{
|
|
@@ -6792,8 +6756,9 @@ async function bootLocalFamily() {
|
|
|
6792
6756
|
*
|
|
6793
6757
|
* Booted lazily via the dual router's `bootLazyFor('relay-intoss')` callback
|
|
6794
6758
|
* (symmetry with {@link bootLocalFamily}), at most once on the first
|
|
6795
|
-
* `start_debug({ mode: 'relay-staging'
|
|
6796
|
-
*
|
|
6759
|
+
* `start_debug({ mode: 'relay-staging' })` (all-lazy, #396 — every relay boot now
|
|
6760
|
+
* flows through `switchMode` after the project-local secret load). `relay-live`
|
|
6761
|
+
* removed (#665).
|
|
6797
6762
|
*
|
|
6798
6763
|
* The relay base URL is only known after `startChiiRelay()` resolves, so the
|
|
6799
6764
|
* `ChiiCdpConnection` (via {@link createRelayConnection}) is constructed inside
|
|
@@ -6880,7 +6845,7 @@ async function bootRelayFamily(options = {}) {
|
|
|
6880
6845
|
* we did not start.
|
|
6881
6846
|
*
|
|
6882
6847
|
* `getTunnelStatus()` reports `up: true` with a `wssUrl` derived from
|
|
6883
|
-
* `relayBaseUrl` (http→ws, https→wss) so the `
|
|
6848
|
+
* `relayBaseUrl` (http→ws, https→wss) so the `start_attach` gate
|
|
6884
6849
|
* (`up: true && wssUrl !== null`) is satisfied even though we never opened a
|
|
6885
6850
|
* cloudflared tunnel ourselves.
|
|
6886
6851
|
*
|
|
@@ -6925,14 +6890,14 @@ async function bootExternalRelayFamily(relayBaseUrl, relayLocalUrl) {
|
|
|
6925
6890
|
/**
|
|
6926
6891
|
* Maps a `StartDebugMode` to the {@link FamilyKey} that serves it (issue #378).
|
|
6927
6892
|
* local-browser → 'local-browser'; relay-sandbox → 'relay-sandbox';
|
|
6928
|
-
* relay-staging
|
|
6893
|
+
* relay-staging → 'relay-intoss' (the intoss-private relay slot).
|
|
6894
|
+
* `relay-live` removed (#665).
|
|
6929
6895
|
*/
|
|
6930
6896
|
function familyKeyForMode(mode) {
|
|
6931
6897
|
switch (mode) {
|
|
6932
6898
|
case "local-browser": return "local-browser";
|
|
6933
6899
|
case "relay-sandbox": return "relay-sandbox";
|
|
6934
|
-
case "relay-staging":
|
|
6935
|
-
case "relay-live": return "relay-intoss";
|
|
6900
|
+
case "relay-staging": return "relay-intoss";
|
|
6936
6901
|
}
|
|
6937
6902
|
}
|
|
6938
6903
|
/** The error thrown / surfaced when entering `mobile` without AIT_RELAY_BASE_URL. */
|
|
@@ -6991,12 +6956,11 @@ const NULL_CDP_CONNECTION = {
|
|
|
6991
6956
|
* restarting the MCP server.
|
|
6992
6957
|
*
|
|
6993
6958
|
* Why a KEYED map and not a single lazy slot (#378): `relay-sandbox` (env-2
|
|
6994
|
-
* external relay) and `relay-staging
|
|
6995
|
-
*
|
|
6996
|
-
*
|
|
6997
|
-
*
|
|
6998
|
-
*
|
|
6999
|
-
* `relay-intoss` slot (wire-identical, distinguished only by `liveIntent`).
|
|
6959
|
+
* external relay) and `relay-staging` (intoss relay) are BOTH `kind: 'relay'`.
|
|
6960
|
+
* A single "opposite-kind" slot could not warm-keep both at once — they would
|
|
6961
|
+
* collide. The three `FamilyKey`s (`local-browser` / `relay-intoss` /
|
|
6962
|
+
* `relay-sandbox`) give each its own warm slot. `relay-live` (env 4) removed
|
|
6963
|
+
* (#665) — `relay-intoss` slot now maps only to `relay-staging`.
|
|
7000
6964
|
*
|
|
7001
6965
|
* Why all-lazy (#396): the relay TOTP secret now lives in a project-local
|
|
7002
6966
|
* `.ait_relay` file loaded read-only by `switchMode` BEFORE a relay family boots.
|
|
@@ -7006,15 +6970,14 @@ const NULL_CDP_CONNECTION = {
|
|
|
7006
6970
|
* `buildRelayVerifyAuth()` run at the boot site.
|
|
7007
6971
|
*
|
|
7008
6972
|
* `switchMode`:
|
|
7009
|
-
* 1. rejects re-entrant swaps (`swapInFlight`)
|
|
6973
|
+
* 1. rejects re-entrant swaps (`swapInFlight`);
|
|
7010
6974
|
* 2. resolves the requested mode's `FamilyKey`:
|
|
7011
6975
|
* `lazyFamilies.get(key) ?? (boot via bootLazyFor(key), store)`;
|
|
7012
6976
|
* 3. flips `active` (the MCP `Server` never re-handshakes — it reads through
|
|
7013
6977
|
* `active` per request);
|
|
7014
|
-
* 4.
|
|
7015
|
-
* 5. stops the old attach watcher and re-arms one on the new connection
|
|
6978
|
+
* 4. stops the old attach watcher and re-arms one on the new connection
|
|
7016
6979
|
* (the watcher self-clears, so re-arm is mandatory);
|
|
7017
|
-
*
|
|
6980
|
+
* 5. emits `tools/list_changed`.
|
|
7018
6981
|
*
|
|
7019
6982
|
* Inactive infra is left WARM — teardown happens only at process exit (the
|
|
7020
6983
|
* unified shutdown in the run functions), which is what keeps a phone attach
|
|
@@ -7060,10 +7023,10 @@ var DualConnectionRouter = class {
|
|
|
7060
7023
|
/**
|
|
7061
7024
|
* Live tunnel status of the active relay family (issues #356, #378). Reads
|
|
7062
7025
|
* the ACTIVE family's tunnel when it has one (so `relay-sandbox` surfaces the
|
|
7063
|
-
* external relay wss and `relay-staging
|
|
7026
|
+
* external relay wss and `relay-staging` the intoss relay wss); otherwise
|
|
7064
7027
|
* falls back to the first booted family that has a tunnel. Returns "down"
|
|
7065
7028
|
* until any relay family is booted (any session before the first relay
|
|
7066
|
-
* start_debug) — the correct signal for `
|
|
7029
|
+
* start_debug) — the correct signal for `start_attach` (no tunnel yet).
|
|
7067
7030
|
*/
|
|
7068
7031
|
relayTunnelStatus() {
|
|
7069
7032
|
if (this.activeFamily?.getTunnelStatus) return this.activeFamily.getTunnelStatus();
|
|
@@ -7098,7 +7061,7 @@ var DualConnectionRouter = class {
|
|
|
7098
7061
|
this.deps.onPageAttach?.();
|
|
7099
7062
|
if (activeFamily.connection.kind === "relay") {
|
|
7100
7063
|
const firstTarget = activeFamily.connection.listTargets()[0];
|
|
7101
|
-
const env = deriveEnvironment(activeFamily.connection.kind,
|
|
7064
|
+
const env = deriveEnvironment(activeFamily.connection.kind, activeFamily.relayOrigin);
|
|
7102
7065
|
const inspectorStableUrl = this.deps.getInspectorStableUrl?.() ?? null;
|
|
7103
7066
|
this.deps.devtoolsOpener.open({
|
|
7104
7067
|
inspectorStableUrl,
|
|
@@ -7156,25 +7119,22 @@ var DualConnectionRouter = class {
|
|
|
7156
7119
|
this.lazyFamilies.set(key, booted);
|
|
7157
7120
|
return booted;
|
|
7158
7121
|
}
|
|
7159
|
-
async switchMode(mode,
|
|
7122
|
+
async switchMode(mode, projectRoot) {
|
|
7160
7123
|
if (this.swapInFlight) throw new Error("start_debug: 이전 전환이 아직 진행 중입니다 — 잠시 후 다시 호출하세요.");
|
|
7161
|
-
if (mode === "relay-live" && !confirm) throw new Error("start_debug: relay-live(실서비스 LIVE)는 confirm: true가 필요합니다 — 실유저에게 영향이 갈 수 있는 LIVE 디버깅 진입을 명시적으로 승인하세요.");
|
|
7162
7124
|
this.swapInFlight = true;
|
|
7163
7125
|
try {
|
|
7164
7126
|
if (isRelayMode(mode)) await loadRelaySecretReadOnly({ projectRoot });
|
|
7165
7127
|
const target = await this.familyFor(familyKeyForMode(mode), projectRoot);
|
|
7166
7128
|
this.activeFamily = target;
|
|
7167
|
-
setLiveIntent(mode === "relay-live");
|
|
7168
7129
|
this.stopWatcher();
|
|
7169
7130
|
this.armWatcher();
|
|
7170
7131
|
this.server?.sendToolListChanged();
|
|
7171
7132
|
const wantRelay = isRelayMode(mode);
|
|
7172
7133
|
return {
|
|
7173
7134
|
mode,
|
|
7174
|
-
environment: deriveEnvironment(target.connection.kind,
|
|
7135
|
+
environment: deriveEnvironment(target.connection.kind, target.relayOrigin),
|
|
7175
7136
|
kind: target.connection.kind,
|
|
7176
|
-
|
|
7177
|
-
nextStep: wantRelay ? "build_attach_url로 attach QR을 생성하세요 (relay 세션)." : "list_pages로 로컬 Chromium 페이지 attach를 확인하세요."
|
|
7137
|
+
nextStep: wantRelay ? "start_attach로 attach QR 생성 + 폰 attach까지 한 번에 진행하세요 (relay 세션)." : "list_pages로 로컬 Chromium 페이지 attach를 확인하세요."
|
|
7178
7138
|
};
|
|
7179
7139
|
} finally {
|
|
7180
7140
|
this.swapInFlight = false;
|
|
@@ -7233,7 +7193,7 @@ async function runDebugServer(options = {}) {
|
|
|
7233
7193
|
})),
|
|
7234
7194
|
attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null,
|
|
7235
7195
|
inspectorUrl,
|
|
7236
|
-
mode: deriveEnvironment(router.active.kind,
|
|
7196
|
+
mode: deriveEnvironment(router.active.kind, router.activeRelayOrigin)
|
|
7237
7197
|
};
|
|
7238
7198
|
};
|
|
7239
7199
|
const getDirectInspectorUrl = () => {
|
|
@@ -7365,7 +7325,7 @@ async function runDebugServer(options = {}) {
|
|
|
7365
7325
|
* 1. `start_debug({ mode: 'local-browser' })` launches a local Chromium with
|
|
7366
7326
|
* `--remote-debugging-port=<port>` and attaches a `LocalCdpConnection`;
|
|
7367
7327
|
* 2. the intoss/external relay families lazy-boot on the first
|
|
7368
|
-
* `start_debug({ mode: 'relay-staging' | 'relay-
|
|
7328
|
+
* `start_debug({ mode: 'relay-staging' | 'relay-sandbox' })` (#665: relay-live removed);
|
|
7369
7329
|
* 3. all of this runs through the SAME direction-neutral
|
|
7370
7330
|
* `DualConnectionRouter` that `runDebugServer` uses (issue #356).
|
|
7371
7331
|
*
|
|
@@ -7377,7 +7337,7 @@ async function runDebugServer(options = {}) {
|
|
|
7377
7337
|
* env 1 (local), then env 3 (intoss-private) in ONE session, no restart" — now
|
|
7378
7338
|
* works from either entry point.
|
|
7379
7339
|
*
|
|
7380
|
-
* `
|
|
7340
|
+
* `start_attach` (relay-specific) stays effectively hidden / non-applicable
|
|
7381
7341
|
* until the relay family is booted: before the first relay switch the env
|
|
7382
7342
|
* derives to `mock` and `relayTunnelStatus()` reports "down", so the tool fails
|
|
7383
7343
|
* with a clear "tunnel not up" message. After a relay switch the relay tunnel
|
|
@@ -7588,10 +7548,9 @@ async function runLocalDebugServer(options = {}) {
|
|
|
7588
7548
|
* Symmetry with `runDebugServer` / `runLocalDebugServer` (#356, #378, #396): all
|
|
7589
7549
|
* three families are lazy-booted — the env-2 external relay on the first
|
|
7590
7550
|
* `start_debug({ mode: 'relay-sandbox' })`, the local family on `local-browser`,
|
|
7591
|
-
* the intoss relay on `relay-staging
|
|
7592
|
-
* session can hot-switch
|
|
7593
|
-
*
|
|
7594
|
-
* origin, liveIntent off).
|
|
7551
|
+
* the intoss relay on `relay-staging` (#665: relay-live removed) — so a
|
|
7552
|
+
* `--target=mobile` session can hot-switch without a restart. The active env
|
|
7553
|
+
* derives to `relay-mobile` (external-PWA origin).
|
|
7595
7554
|
*
|
|
7596
7555
|
* SECRET-HANDLING: `AIT_RELAY_BASE_URL` is read once here via
|
|
7597
7556
|
* {@link readMobileRelayBaseUrl}; when unset it throws
|
|
@@ -7972,25 +7931,30 @@ const DEV_TOOL_DEFINITIONS = [
|
|
|
7972
7931
|
availableIn: "both"
|
|
7973
7932
|
},
|
|
7974
7933
|
{
|
|
7975
|
-
name: "
|
|
7976
|
-
description: "
|
|
7934
|
+
name: "start_attach",
|
|
7935
|
+
description: "Switches into a relay mode (if given), builds a self-attaching deep-link QR for a real device, and waits for the phone to attach — all in one call. NOT available in dev-mode — requires a live cloudflared relay (Tier B, relay-only). To use this tool: restart the MCP server with `--mode=debug` (or omit --mode) and set MCP_ENV=relay, then call start_attach to generate the QR for phone scanning. See: https://docs.aitc.dev/guides/debug-relay",
|
|
7977
7936
|
inputSchema: {
|
|
7978
7937
|
type: "object",
|
|
7979
7938
|
properties: {
|
|
7980
|
-
|
|
7939
|
+
mode: {
|
|
7981
7940
|
type: "string",
|
|
7982
|
-
|
|
7941
|
+
enum: [
|
|
7942
|
+
"local-browser",
|
|
7943
|
+
"relay-sandbox",
|
|
7944
|
+
"relay-staging"
|
|
7945
|
+
],
|
|
7946
|
+
description: "Optional relay mode to switch into before attaching."
|
|
7983
7947
|
},
|
|
7984
|
-
|
|
7985
|
-
type: "
|
|
7986
|
-
description: "
|
|
7948
|
+
scheme_url: {
|
|
7949
|
+
type: "string",
|
|
7950
|
+
description: "The intoss-private:// URL from `ait deploy --scheme-only` (env 3/relay-staging)."
|
|
7987
7951
|
},
|
|
7988
7952
|
wait_timeout_seconds: {
|
|
7989
7953
|
type: "number",
|
|
7990
|
-
description: "Maximum seconds to wait
|
|
7954
|
+
description: "Maximum seconds to wait for a page to attach (default 60, range 1–600). Invalid inputs fall back to default."
|
|
7991
7955
|
}
|
|
7992
7956
|
},
|
|
7993
|
-
required: [
|
|
7957
|
+
required: []
|
|
7994
7958
|
},
|
|
7995
7959
|
availableIn: "relay"
|
|
7996
7960
|
},
|
|
@@ -8088,10 +8052,6 @@ const DEV_TOOL_DEFINITIONS = [
|
|
|
8088
8052
|
timeout_ms: {
|
|
8089
8053
|
type: "number",
|
|
8090
8054
|
description: "Per-file evaluate timeout in ms."
|
|
8091
|
-
},
|
|
8092
|
-
confirm: {
|
|
8093
|
-
type: "boolean",
|
|
8094
|
-
description: "Required in relay-live sessions."
|
|
8095
8055
|
}
|
|
8096
8056
|
},
|
|
8097
8057
|
required: ["files"]
|
|
@@ -8117,7 +8077,7 @@ const CDP_ONLY_TOOL_NAMES = new Set([
|
|
|
8117
8077
|
* Listed in dev-mode tool surface (issue #323) so agents get a hand-off hint
|
|
8118
8078
|
* toward `--mode=debug` instead of "Unknown tool".
|
|
8119
8079
|
*/
|
|
8120
|
-
const TIER_B_TOOL_NAMES = new Set(["
|
|
8080
|
+
const TIER_B_TOOL_NAMES = new Set(["start_attach"]);
|
|
8121
8081
|
/**
|
|
8122
8082
|
* Builds the `list_pages` dev-mode shim response.
|
|
8123
8083
|
* Returns the Vite dev URL as a single-entry page list with `devMode: true`.
|
|
@@ -8227,7 +8187,7 @@ function createDevServer(deps = {}) {
|
|
|
8227
8187
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
8228
8188
|
const server = new Server({
|
|
8229
8189
|
name: "ait-devtools",
|
|
8230
|
-
version: "0.1.
|
|
8190
|
+
version: "0.1.110"
|
|
8231
8191
|
}, { capabilities: { tools: {} } });
|
|
8232
8192
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
8233
8193
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -8302,7 +8262,7 @@ async function runDevServer() {
|
|
|
8302
8262
|
*
|
|
8303
8263
|
* --mode=debug (default)
|
|
8304
8264
|
* --target=relay (default) — CDP/Chii relay + cloudflared quick tunnel.
|
|
8305
|
-
* Attach a running mini-app (real Toss WebView, env 3
|
|
8265
|
+
* Attach a running mini-app (real Toss WebView, env 3) and read its
|
|
8306
8266
|
* console + network over CDP without a human watching a phone.
|
|
8307
8267
|
* --target=local — CDP direct-attach to a local Chromium launched by the
|
|
8308
8268
|
* MCP server (env 1). No relay or tunnel; the browser is launched
|
|
@@ -8318,26 +8278,12 @@ async function runDevServer() {
|
|
|
8318
8278
|
* Back-compat (issue #348): the legacy `--mode`/`--target` flags and `MCP_ENV`
|
|
8319
8279
|
* still work. `--target=relay`/`local` select the initial active connection;
|
|
8320
8280
|
* the in-session `start_debug(mode)` MCP tool can then flip between them with no
|
|
8321
|
-
* restart. `MCP_ENV
|
|
8322
|
-
* `
|
|
8323
|
-
* (the active connection's `kind` is authoritative).
|
|
8281
|
+
* restart. `MCP_ENV` values are accepted and ignored (the active connection's
|
|
8282
|
+
* `kind` is authoritative; `relay-live` and `liveIntent` are removed, #665).
|
|
8324
8283
|
*
|
|
8325
8284
|
* Node-only stdio process.
|
|
8326
8285
|
*/
|
|
8327
8286
|
/**
|
|
8328
|
-
* Seeds the module-level `liveIntent` bit from the deprecated `MCP_ENV` alias
|
|
8329
|
-
* (issue #348). `MCP_ENV=relay-live` is the only value that matters now — it
|
|
8330
|
-
* arms LIVE intent at boot so a session launched straight into env 4 has the
|
|
8331
|
-
* guard active without a `start_debug({ mode: 'relay-live' })` round-trip. All
|
|
8332
|
-
* other `MCP_ENV` values (`mock`, `relay`, `relay-dev`) are accepted-and-ignored
|
|
8333
|
-
* for env derivation — the active connection's `kind` is authoritative.
|
|
8334
|
-
*
|
|
8335
|
-
* SECRET-HANDLING: reads only the env-var string; never logs a secret.
|
|
8336
|
-
*/
|
|
8337
|
-
function seedLiveIntentFromEnv(env = process.env) {
|
|
8338
|
-
if (env.MCP_ENV === "relay-live") setLiveIntent(true);
|
|
8339
|
-
}
|
|
8340
|
-
/**
|
|
8341
8287
|
* Returns `true` when `--force` or `--takeover` is present in argv.
|
|
8342
8288
|
*
|
|
8343
8289
|
* Both flags are accepted as aliases — `--force` is the short form listed in
|
|
@@ -8394,7 +8340,6 @@ function normalizeTarget(value) {
|
|
|
8394
8340
|
}
|
|
8395
8341
|
async function main() {
|
|
8396
8342
|
const args = process.argv.slice(2);
|
|
8397
|
-
seedLiveIntentFromEnv();
|
|
8398
8343
|
if (parseMode(args) === "dev") await runDevServer();
|
|
8399
8344
|
else {
|
|
8400
8345
|
const target = parseTarget(args);
|
|
@@ -8429,6 +8374,6 @@ if (isEntrypoint()) main().catch((err) => {
|
|
|
8429
8374
|
process.exitCode = 1;
|
|
8430
8375
|
});
|
|
8431
8376
|
//#endregion
|
|
8432
|
-
export { parseForce, parseMode, parseTarget
|
|
8377
|
+
export { parseForce, parseMode, parseTarget };
|
|
8433
8378
|
|
|
8434
8379
|
//# sourceMappingURL=cli.js.map
|