@ait-co/devtools 0.1.109 → 0.1.111
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 +633 -680
- 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/{relay-secret-store-DBwzoCXQ.js → relay-secret-store-BPhN1upr.js} +61 -2
- package/dist/relay-secret-store-BPhN1upr.js.map +1 -0
- package/dist/{relay-secret-store-CPBBlV3J.cjs → relay-secret-store-DWKdV-eY.cjs} +61 -2
- package/dist/relay-secret-store-DWKdV-eY.cjs.map +1 -0
- package/dist/relay-secret-store-DhzAnnj-.js.map +1 -1
- package/dist/{relay-url-store-C9QLhB2p.cjs → relay-url-store-1FGuSYAn.cjs} +2 -2
- package/dist/{relay-url-store-C9QLhB2p.cjs.map → relay-url-store-1FGuSYAn.cjs.map} +1 -1
- package/dist/{relay-url-store-j16TRTiJ.js → relay-url-store-Bskcyeg8.js} +2 -2
- package/dist/{relay-url-store-j16TRTiJ.js.map → relay-url-store-Bskcyeg8.js.map} +1 -1
- 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 +3 -3
- 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 +3 -3
- 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/relay-secret-store-CPBBlV3J.cjs.map +0 -1
- package/dist/relay-secret-store-DBwzoCXQ.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"
|
|
@@ -4714,7 +4736,7 @@ function listPages(connection, tunnel) {
|
|
|
4714
4736
|
return {
|
|
4715
4737
|
id: t.id,
|
|
4716
4738
|
title: t.title,
|
|
4717
|
-
url: t.url,
|
|
4739
|
+
url: redactAtParam(t.url),
|
|
4718
4740
|
lastSeenAt: lastSeenMs !== null ? new Date(lastSeenMs).toISOString() : null
|
|
4719
4741
|
};
|
|
4720
4742
|
});
|
|
@@ -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:
|
|
@@ -4867,9 +4838,17 @@ function getBrowserCandidates(httpUrl) {
|
|
|
4867
4838
|
}
|
|
4868
4839
|
];
|
|
4869
4840
|
}
|
|
4841
|
+
/**
|
|
4842
|
+
* Redacts ONLY the `at=<value>` (TOTP) query param to `at=<redacted>`, leaving
|
|
4843
|
+
* every other query param (_deploymentId, debug, relay) intact. SECRET-HANDLING:
|
|
4844
|
+
* the TOTP code is the single short-lived secret carried in a CDP page url.
|
|
4845
|
+
*/
|
|
4846
|
+
function redactAtParam(text) {
|
|
4847
|
+
return text.replace(/\bat=([^&\s"']+)/g, "at=<redacted>");
|
|
4848
|
+
}
|
|
4870
4849
|
/** stderr에서 at= TOTP 코드 값을 redact한다. */
|
|
4871
4850
|
function redactSecrets(text) {
|
|
4872
|
-
return text
|
|
4851
|
+
return redactAtParam(text);
|
|
4873
4852
|
}
|
|
4874
4853
|
/** spawnSync exit 0이어도 stderr에 launch 실패 시그널이 있으면 실패로 판단한다. */
|
|
4875
4854
|
const LAUNCH_FAILURE_PATTERNS = [
|
|
@@ -5396,7 +5375,7 @@ async function readMcpSdkVersion() {
|
|
|
5396
5375
|
* some test environments that skip the build step).
|
|
5397
5376
|
*/
|
|
5398
5377
|
function readDevtoolsVersion() {
|
|
5399
|
-
return "0.1.
|
|
5378
|
+
return "0.1.111";
|
|
5400
5379
|
}
|
|
5401
5380
|
/**
|
|
5402
5381
|
* Derives the next recommended action from a completed diagnostics snapshot.
|
|
@@ -5405,10 +5384,10 @@ function readDevtoolsVersion() {
|
|
|
5405
5384
|
* 0. tunnel.droppedAt non-null → restart (permanent tunnel drop — highest priority)
|
|
5406
5385
|
* 1. tunnel.up === false AND env is relay → restart (relay needs a live tunnel)
|
|
5407
5386
|
* 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 →
|
|
5387
|
+
* 2a. authRejects.count > 0 AND pages empty → start_attach (relay TOTP 거부 관측 — QR 재스캔
|
|
5409
5388
|
* 또는 target-side `at` 전달 확인. 일반 rule 2보다 구체적이므로 먼저 평가 — issue #467)
|
|
5410
|
-
* 2. tunnel.up, pages empty, env === relay →
|
|
5411
|
-
* 3. pages has entry + crashDetectedAt non-null →
|
|
5389
|
+
* 2. tunnel.up, pages empty, env === relay → start_attach (start attach)
|
|
5390
|
+
* 3. pages has entry + crashDetectedAt non-null → start_attach (re-attach after crash)
|
|
5412
5391
|
* 4. otherwise → null (session looks healthy)
|
|
5413
5392
|
*
|
|
5414
5393
|
* Pure — does not throw; receives the final assembled snapshot fields.
|
|
@@ -5431,16 +5410,16 @@ function computeNextRecommendedAction(tunnel, pages, env, authRejects = null) {
|
|
|
5431
5410
|
reason: "tunnel not up — run `npx @ait-co/devtools devtools-mcp` to restart"
|
|
5432
5411
|
};
|
|
5433
5412
|
if (authRejects !== null && authRejects.count > 0 && pages !== null && pages.pages.length === 0) return {
|
|
5434
|
-
tool: "
|
|
5413
|
+
tool: "start_attach",
|
|
5435
5414
|
reason: `relay 인증(TOTP) 거부 ${authRejects.count}건 발생 (last ${authRejects.lastAt ?? "unknown"}) — QR을 다시 스캔해 새 코드로 attach하세요(코드는 ~3분마다 만료). 반복되면 폰 페이지 URL에 at 파라미터가 전달되는지(target-side TOTP 전달 경로)를 확인하세요`
|
|
5436
5415
|
};
|
|
5437
5416
|
if (isRelayEnv(env) && pages !== null && pages.pages.length === 0 && !pages.crashDetectedAt) return {
|
|
5438
|
-
tool: "
|
|
5439
|
-
reason: "tunnel ready, no pages attached — call
|
|
5417
|
+
tool: "start_attach",
|
|
5418
|
+
reason: "tunnel ready, no pages attached — call start_attach to generate the attach QR"
|
|
5440
5419
|
};
|
|
5441
5420
|
if (pages !== null && pages.crashDetectedAt !== null) return {
|
|
5442
|
-
tool: "
|
|
5443
|
-
reason: `page crashed at ${pages.crashDetectedAt} — call
|
|
5421
|
+
tool: "start_attach",
|
|
5422
|
+
reason: `page crashed at ${pages.crashDetectedAt} — call start_attach to re-attach`
|
|
5444
5423
|
};
|
|
5445
5424
|
return null;
|
|
5446
5425
|
}
|
|
@@ -5505,7 +5484,7 @@ async function getDiagnostics(input) {
|
|
|
5505
5484
|
kind: env,
|
|
5506
5485
|
env: toLegacyEnv(env),
|
|
5507
5486
|
reason: envReason,
|
|
5508
|
-
liveGuardActive:
|
|
5487
|
+
liveGuardActive: false
|
|
5509
5488
|
},
|
|
5510
5489
|
serverLockHolder,
|
|
5511
5490
|
process: {
|
|
@@ -5618,7 +5597,7 @@ async function startQuickTunnel(localPort) {
|
|
|
5618
5597
|
* every surface (terminal, VS Code, JetBrains, web) and can be scanned by a
|
|
5619
5598
|
* phone camera when shown verbatim in an agent response.
|
|
5620
5599
|
*
|
|
5621
|
-
* Shared by `renderAttachBanner` (relay wssUrl QR) and the `
|
|
5600
|
+
* Shared by `renderAttachBanner` (relay wssUrl QR) and the `start_attach`
|
|
5622
5601
|
* MCP tool response (attach deep-link QR).
|
|
5623
5602
|
*/
|
|
5624
5603
|
async function renderQr(text) {
|
|
@@ -5649,7 +5628,7 @@ async function renderQr(text) {
|
|
|
5649
5628
|
* The QR is produced by `renderQr` (a half-block matrix, not the
|
|
5650
5629
|
* `qrcode-terminal` ASCII art used by the unplugin banner) and encodes the
|
|
5651
5630
|
* base `wssUrl` only. When `totpEnabled` is true, a note
|
|
5652
|
-
* is added that attach URLs generated by `
|
|
5631
|
+
* is added that attach URLs generated by `start_attach` will include a
|
|
5653
5632
|
* live TOTP code (`at=`) appended at call time.
|
|
5654
5633
|
*
|
|
5655
5634
|
* SECRET-HANDLING: no secret value, TOTP code, or intermediate value is
|
|
@@ -5665,9 +5644,9 @@ async function renderAttachBanner(input) {
|
|
|
5665
5644
|
` relay (wss): ${input.wssUrl}`,
|
|
5666
5645
|
authNote,
|
|
5667
5646
|
"",
|
|
5668
|
-
" Use
|
|
5647
|
+
" Use start_attach to generate a deep link with the current TOTP code.",
|
|
5669
5648
|
" Scan the QR to locate the relay (open the dog-food URL separately with",
|
|
5670
|
-
" ?debug=1&relay=<wss>&at=<code> or use the
|
|
5649
|
+
" ?debug=1&relay=<wss>&at=<code> or use the start_attach tool):",
|
|
5671
5650
|
"",
|
|
5672
5651
|
qr
|
|
5673
5652
|
].join("\n");
|
|
@@ -5841,7 +5820,7 @@ function makeTunnelStatus(up, wssUrl, droppedAt = null, reissueAttempts = 0) {
|
|
|
5841
5820
|
* Dynamic tool registration (issue #208):
|
|
5842
5821
|
* The server advertises `listChanged: true` so MCP clients can subscribe to
|
|
5843
5822
|
* `notifications/tools/list_changed`. Before any page attaches, only bootstrap
|
|
5844
|
-
* tools (`
|
|
5823
|
+
* tools (`start_attach`, `list_pages`) are listed. Once a target appears,
|
|
5845
5824
|
* the full attach-dependent tool set is added and a `list_changed` notification
|
|
5846
5825
|
* is sent — without requiring a session restart. `runDebugServer` and
|
|
5847
5826
|
* `runLocalDebugServer` start a polling watcher that detects the 0→N target
|
|
@@ -5854,7 +5833,7 @@ function makeTunnelStatus(up, wssUrl, droppedAt = null, reissueAttempts = 0) {
|
|
|
5854
5833
|
*/
|
|
5855
5834
|
/**
|
|
5856
5835
|
* 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 `
|
|
5836
|
+
* and excluded from the `wait_for_attach` short-circuit in `start_attach`
|
|
5858
5837
|
* (issue #610).
|
|
5859
5838
|
*
|
|
5860
5839
|
* Rationale: the env-2 relay is owned by the dev server (unplugin), so every
|
|
@@ -5869,7 +5848,15 @@ function makeTunnelStatus(up, wssUrl, droppedAt = null, reissueAttempts = 0) {
|
|
|
5869
5848
|
*/
|
|
5870
5849
|
const RELAY_SANDBOX_STALE_PAGE_MS = 300 * 1e3;
|
|
5871
5850
|
/**
|
|
5872
|
-
*
|
|
5851
|
+
* Segment length (ms) of the `start_attach` wait loop (issue #626 — TOTP in-call
|
|
5852
|
+
* re-mint). The single-shot `wait_for_attach` of the old attach tool could
|
|
5853
|
+
* not re-mint a TOTP code mid-wait; `start_attach` decomposes the wait into
|
|
5854
|
+
* SEGMENT_MS slices so it can detect an aging code between slices and re-mint a
|
|
5855
|
+
* fresh one without the agent re-calling the tool. 30 s = one TOTP step.
|
|
5856
|
+
*/
|
|
5857
|
+
const START_ATTACH_SEGMENT_MS = 3e4;
|
|
5858
|
+
/**
|
|
5859
|
+
* Predicate used by `start_attach`'s `wait_for_attach` loop to decide
|
|
5873
5860
|
* whether the relay-sandbox connection has a genuinely fresh page attached.
|
|
5874
5861
|
*
|
|
5875
5862
|
* Stale-ghost gating (issue #610): when the dev server restarts with a new
|
|
@@ -5919,13 +5906,28 @@ function extractDeploymentId(schemeUrl) {
|
|
|
5919
5906
|
}
|
|
5920
5907
|
}
|
|
5921
5908
|
/**
|
|
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.
|
|
5909
|
+
* Returns `true` when the mode routes to a relay connection (`relay-sandbox` or
|
|
5910
|
+
* `relay-staging`). Both surface the Tier B / relay-only tool set.
|
|
5926
5911
|
*/
|
|
5927
5912
|
function isRelayMode(mode) {
|
|
5928
|
-
return mode === "relay-sandbox" || mode === "relay-staging"
|
|
5913
|
+
return mode === "relay-sandbox" || mode === "relay-staging";
|
|
5914
|
+
}
|
|
5915
|
+
/**
|
|
5916
|
+
* Maps a `StartDebugMode` to the `McpEnvironment` it routes to (issue #626).
|
|
5917
|
+
* Used by `start_attach`'s mode prologue to decide whether a `switchMode` is
|
|
5918
|
+
* needed: when the active env already equals `envForMode(mode)`, the switch is
|
|
5919
|
+
* skipped (no `tools/list_changed` churn).
|
|
5920
|
+
*
|
|
5921
|
+
* - `local-browser` → `mock`
|
|
5922
|
+
* - `relay-sandbox` → `relay-mobile` (env 2 external-PWA relay)
|
|
5923
|
+
* - `relay-staging` → `relay-dev` (env 3 intoss-private relay)
|
|
5924
|
+
*/
|
|
5925
|
+
function envForMode(mode) {
|
|
5926
|
+
switch (mode) {
|
|
5927
|
+
case "local-browser": return "mock";
|
|
5928
|
+
case "relay-sandbox": return "relay-mobile";
|
|
5929
|
+
case "relay-staging": return "relay-dev";
|
|
5930
|
+
}
|
|
5929
5931
|
}
|
|
5930
5932
|
/**
|
|
5931
5933
|
* Single-attach guard for `run_tests` (#646). Two concurrent runs injecting
|
|
@@ -5948,7 +5950,7 @@ let runTestsInFlight = false;
|
|
|
5948
5950
|
* to resolve before the relay had observed the first inbound CDP message from
|
|
5949
5951
|
* the phone.
|
|
5950
5952
|
*
|
|
5951
|
-
* Timeout note: callers (e.g. the `
|
|
5953
|
+
* Timeout note: callers (e.g. the `start_attach` path) always pass an
|
|
5952
5954
|
* explicit `timeoutMs`, sourced from the factory's `waitForAttachTimeoutMs`
|
|
5953
5955
|
* (default 60 000). That value is forwarded to `waitForFirstTarget`, so it
|
|
5954
5956
|
* overrides that method's own 90 000 signature default — the effective
|
|
@@ -5990,7 +5992,7 @@ function waitForAttachWithEvents(connection, filterFn, timeoutMs, pollIntervalMs
|
|
|
5990
5992
|
* tunnel, which is what makes the tool surface unit-testable.
|
|
5991
5993
|
*
|
|
5992
5994
|
* `tools/list` is two-tiered (issue #208):
|
|
5993
|
-
* - bootstrap (always): `
|
|
5995
|
+
* - bootstrap (always): `start_attach`, `list_pages`
|
|
5994
5996
|
* - attach-dependent (after `connection.listTargets().length > 0`): all others
|
|
5995
5997
|
*
|
|
5996
5998
|
* `CallTool` is NOT tiered — hidden tools still execute (attach errors surface
|
|
@@ -6001,12 +6003,299 @@ function createDebugServer(deps) {
|
|
|
6001
6003
|
const getTotpSecret = deps.getTotpSecret ?? (() => totpSecret);
|
|
6002
6004
|
const readLockFn = readLockDep ?? readServerLock;
|
|
6003
6005
|
const router = routerDep ?? makeSingleConnectionRouter(connection);
|
|
6004
|
-
const resolveEnvironment = getEnvDep ?? (() => deriveEnvironment(router.active.kind,
|
|
6005
|
-
const resolveEnvironmentReason = getEnvReasonDep ?? (() => `derived:kind=${router.active.kind},
|
|
6006
|
+
const resolveEnvironment = getEnvDep ?? (() => deriveEnvironment(router.active.kind, router.activeRelayOrigin));
|
|
6007
|
+
const resolveEnvironmentReason = getEnvReasonDep ?? (() => `derived:kind=${router.active.kind},relayOrigin=${router.activeRelayOrigin ?? "none"}`);
|
|
6006
6008
|
const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
|
|
6009
|
+
/**
|
|
6010
|
+
* Synthesizes an attach URL from stored components with a FRESHLY-minted TOTP
|
|
6011
|
+
* code (issue #626 §3/§4 — the single mint point). Reads the late-bound secret
|
|
6012
|
+
* via `getTotpSecret()` so the project-local `.ait_relay` secret loaded by
|
|
6013
|
+
* `switchMode` is visible. SECRET-HANDLING: the minted code rides inside the
|
|
6014
|
+
* URL's `at=` param only — never logged or returned separately.
|
|
6015
|
+
*/
|
|
6016
|
+
function mintAttachUrl(parts) {
|
|
6017
|
+
const secret = getTotpSecret();
|
|
6018
|
+
const code = secret ? generateTotp(secret) : void 0;
|
|
6019
|
+
return parts.kind === "launcher" ? buildLauncherAttachUrl(parts.tunnelHttpUrl, parts.wssUrl, code, {
|
|
6020
|
+
name: parts.appName,
|
|
6021
|
+
...parts.selfdebug ? { selfdebug: true } : {}
|
|
6022
|
+
}) : buildDeepLinkAttachUrl(parts.schemeUrl, parts.wssUrl, code);
|
|
6023
|
+
}
|
|
6024
|
+
/** Builds the fresh TOTP metadata (expiresAt window) for a tool result. */
|
|
6025
|
+
function buildTotpMeta() {
|
|
6026
|
+
const secret = getTotpSecret();
|
|
6027
|
+
if (secret === void 0 || secret === "") return void 0;
|
|
6028
|
+
const STEP_SECONDS = 30;
|
|
6029
|
+
const expiresAtMs = nowMs() + 6 * STEP_SECONDS * 1e3;
|
|
6030
|
+
return {
|
|
6031
|
+
enabled: true,
|
|
6032
|
+
ttlSeconds: 6 * STEP_SECONDS,
|
|
6033
|
+
expiresAt: new Date(expiresAtMs).toISOString()
|
|
6034
|
+
};
|
|
6035
|
+
}
|
|
6036
|
+
/**
|
|
6037
|
+
* Env-specific validation + component bundle for `start_attach` (issue #626).
|
|
6038
|
+
* Branches on `env`: `relay-mobile` reads AIT_TUNNEL_BASE_URL + builds launcher
|
|
6039
|
+
* parts; `relay-dev` requires scheme_url + builds scheme parts. Returns
|
|
6040
|
+
* `{ ok: false, error }` with a ready McpResult on any failure.
|
|
6041
|
+
*/
|
|
6042
|
+
async function prepareAttach(env, args, conn) {
|
|
6043
|
+
const selfdebug = args?.selfdebug === true;
|
|
6044
|
+
if (selfdebug && env !== "relay-mobile") return {
|
|
6045
|
+
ok: false,
|
|
6046
|
+
error: mcpError("start_attach: selfdebug=true는 env 2 / relay-sandbox 전용 기능입니다. 현재 환경(env 3)에서는 launcher가 없어 self-target 모드를 지원하지 않습니다. launcher self-target이 필요하다면 relay-sandbox 모드로 전환하세요.")
|
|
6047
|
+
};
|
|
6048
|
+
if (env === "relay-mobile") {
|
|
6049
|
+
const rawProjectRoot = args?.projectRoot;
|
|
6050
|
+
const buildProjectRoot = typeof rawProjectRoot === "string" ? rawProjectRoot : void 0;
|
|
6051
|
+
let tunnelHttpUrl = process.env.AIT_TUNNEL_BASE_URL?.trim() ?? "";
|
|
6052
|
+
if (tunnelHttpUrl === "" && buildProjectRoot !== void 0) {
|
|
6053
|
+
const { readRelayUrls } = await import("../relay-url-store-CwKT7i04.js");
|
|
6054
|
+
tunnelHttpUrl = (await readRelayUrls({ projectRoot: buildProjectRoot }))?.tunnelBaseUrl ?? "";
|
|
6055
|
+
}
|
|
6056
|
+
if (tunnelHttpUrl === "") return {
|
|
6057
|
+
ok: false,
|
|
6058
|
+
error: mcpError("start_attach(mobile): AIT_TUNNEL_BASE_URL이 설정되지 않았습니다. dev 서버가 tunnel:{cdp:true}로 기동 중이면 .ait_urls 파일이 자동 생성돼 있어야 합니다. 자동 발견이 되지 않을 경우 앱 HTTP 터널 URL을 AIT_TUNNEL_BASE_URL 환경변수로 직접 전달하세요.")
|
|
6059
|
+
};
|
|
6060
|
+
const tunnelStatus = getTunnelStatus();
|
|
6061
|
+
if (!tunnelStatus.up || tunnelStatus.wssUrl === null) return {
|
|
6062
|
+
ok: false,
|
|
6063
|
+
error: mcpError("start_attach(mobile): relay wssUrl이 아직 설정되지 않았습니다. unplugin tunnel:{cdp:true}가 relay를 완전히 기동할 때까지 잠시 후 다시 시도하세요.")
|
|
6064
|
+
};
|
|
6065
|
+
const secret = getTotpSecret();
|
|
6066
|
+
if (secret === void 0 || secret === "") return {
|
|
6067
|
+
ok: false,
|
|
6068
|
+
error: mcpError("start_attach(relay): TOTP secret(AIT_DEBUG_TOTP_SECRET)이 설정되지 않았습니다. relay 환경은 TOTP 인증이 필수입니다 — relay를 secret과 함께 재기동하세요.")
|
|
6069
|
+
};
|
|
6070
|
+
let launcherAppName;
|
|
6071
|
+
if (buildProjectRoot !== void 0) try {
|
|
6072
|
+
const { readFileSync } = await import("node:fs");
|
|
6073
|
+
const pkgRaw = readFileSync(`${buildProjectRoot}/package.json`, "utf8");
|
|
6074
|
+
const pkg = JSON.parse(pkgRaw);
|
|
6075
|
+
const rawName = typeof pkg.name === "string" ? pkg.name : "";
|
|
6076
|
+
launcherAppName = (rawName.includes("/") ? rawName.slice(rawName.indexOf("/") + 1) : rawName).trim() || void 0;
|
|
6077
|
+
} catch {}
|
|
6078
|
+
const parts = {
|
|
6079
|
+
kind: "launcher",
|
|
6080
|
+
tunnelHttpUrl,
|
|
6081
|
+
wssUrl: tunnelStatus.wssUrl,
|
|
6082
|
+
appName: launcherAppName,
|
|
6083
|
+
...selfdebug ? { selfdebug: true } : {}
|
|
6084
|
+
};
|
|
6085
|
+
const connAsAny = conn;
|
|
6086
|
+
const getLastSeenAt = typeof connAsAny.getTargetLastSeenAt === "function" ? (id) => connAsAny.getTargetLastSeenAt(id) : null;
|
|
6087
|
+
const callNow = nowMs();
|
|
6088
|
+
const isMatchingPage = (pages) => isSandboxPageFresh(pages, getLastSeenAt, callNow, stalePageThresholdMs);
|
|
6089
|
+
const buildTimeoutError = (baseText, timeoutSec, observed) => {
|
|
6090
|
+
const observedUrls = observed.slice(0, 3).map((p) => p.url.slice(0, 80)).join(", ");
|
|
6091
|
+
return `${baseText}\n\nNo page attached within ${timeoutSec}s${observed.length > 0 ? ` — previously attached pages: [${observedUrls}]` : ""} — launcher QR을 폰 카메라로 스캔한 뒤 call list_pages를 다시 호출하세요.`;
|
|
6092
|
+
};
|
|
6093
|
+
return {
|
|
6094
|
+
ok: true,
|
|
6095
|
+
parts,
|
|
6096
|
+
isMatchingPage,
|
|
6097
|
+
buildTimeoutError,
|
|
6098
|
+
authorityWarning: void 0,
|
|
6099
|
+
totpMeta: buildTotpMeta()
|
|
6100
|
+
};
|
|
6101
|
+
}
|
|
6102
|
+
const schemeUrl = args?.scheme_url;
|
|
6103
|
+
if (typeof schemeUrl !== "string" || schemeUrl === "") return {
|
|
6104
|
+
ok: false,
|
|
6105
|
+
error: mcpError("start_attach: scheme_url이 비어 있습니다. `ait deploy --scheme-only`가 출력하는 intoss-private:// URL을 인자로 전달하세요. 환경 2(mobile)라면 scheme_url 대신 AIT_TUNNEL_BASE_URL을 설정하세요.")
|
|
6106
|
+
};
|
|
6107
|
+
{
|
|
6108
|
+
const relaySecret = getTotpSecret();
|
|
6109
|
+
if (relaySecret === void 0 || relaySecret === "") return {
|
|
6110
|
+
ok: false,
|
|
6111
|
+
error: mcpError("start_attach(relay): TOTP secret(AIT_DEBUG_TOTP_SECRET)이 설정되지 않았습니다. relay 환경은 TOTP 인증이 필수입니다 — relay를 secret과 함께 재기동하세요.")
|
|
6112
|
+
};
|
|
6113
|
+
}
|
|
6114
|
+
const tunnelForBuild = getTunnelStatus();
|
|
6115
|
+
if (!tunnelForBuild.up || tunnelForBuild.wssUrl === null) return {
|
|
6116
|
+
ok: false,
|
|
6117
|
+
error: classifyToolError(/* @__PURE__ */ new Error("tunnel-down:"), "start_attach")
|
|
6118
|
+
};
|
|
6119
|
+
const authorityWarning = validateSchemeAuthority(schemeUrl) ?? void 0;
|
|
6120
|
+
const parts = {
|
|
6121
|
+
kind: "scheme",
|
|
6122
|
+
schemeUrl,
|
|
6123
|
+
wssUrl: tunnelForBuild.wssUrl
|
|
6124
|
+
};
|
|
6125
|
+
const deploymentId = extractDeploymentId(schemeUrl);
|
|
6126
|
+
if (!deploymentId) logInfo("tool.call", {
|
|
6127
|
+
tool: "start_attach",
|
|
6128
|
+
msg: "no _deploymentId in scheme_url; matching on presence only"
|
|
6129
|
+
});
|
|
6130
|
+
const isMatchingPage = (pages) => {
|
|
6131
|
+
if (pages.length === 0) return false;
|
|
6132
|
+
if (deploymentId === null) return true;
|
|
6133
|
+
return pages.some((p) => p.url.includes(deploymentId));
|
|
6134
|
+
};
|
|
6135
|
+
const buildTimeoutError = (baseText, timeoutSec, observed) => {
|
|
6136
|
+
const observedUrls = observed.slice(0, 3).map((p) => p.url.slice(0, 80)).join(", ");
|
|
6137
|
+
const observedNote = observed.length > 0 ? ` — previously attached pages: [${observedUrls}]` : "";
|
|
6138
|
+
return `${baseText}\n\nNo page${deploymentId ? ` matching deploymentId=${deploymentId}` : ""} attached within ${timeoutSec}s${observedNote} — call list_pages to retry.`;
|
|
6139
|
+
};
|
|
6140
|
+
return {
|
|
6141
|
+
ok: true,
|
|
6142
|
+
parts,
|
|
6143
|
+
isMatchingPage,
|
|
6144
|
+
buildTimeoutError,
|
|
6145
|
+
authorityWarning,
|
|
6146
|
+
totpMeta: buildTotpMeta()
|
|
6147
|
+
};
|
|
6148
|
+
}
|
|
6149
|
+
/**
|
|
6150
|
+
* QR render + browser open + segmented attach wait with in-call TOTP re-mint
|
|
6151
|
+
* (issue #626 §3). Shared by env-2 and env-3 (4 render paths:
|
|
6152
|
+
* headless / browser-opened / browser-open-failed / no-http-server).
|
|
6153
|
+
*
|
|
6154
|
+
* The wait is decomposed into `START_ATTACH_SEGMENT_MS` slices. Between slices,
|
|
6155
|
+
* if the current TOTP code has aged past `START_ATTACH_REMINT_THRESHOLD_MS`,
|
|
6156
|
+
* a fresh URL is minted via `mintAttachUrl` and pushed to the dashboard via
|
|
6157
|
+
* `onAttachUrlBuilt` (SSE refresh — NO browser re-open). The `reminted` count
|
|
6158
|
+
* rides in the success/timeout result.
|
|
6159
|
+
*
|
|
6160
|
+
* SECRET-HANDLING: attachUrl encodes tunnel/scheme host + the TOTP `at=` code
|
|
6161
|
+
* in the QR payload only. The browser is opened on a 127.0.0.1 URL only. The
|
|
6162
|
+
* tool result carries `totp.expiresAt` + `reminted` count — never the code.
|
|
6163
|
+
*/
|
|
6164
|
+
async function renderAndMaybeWait(prep, waitForAttach, callTimeoutMs, conn) {
|
|
6165
|
+
const { parts, isMatchingPage, buildTimeoutError, authorityWarning, totpMeta } = prep;
|
|
6166
|
+
let attachUrl = mintAttachUrl(parts);
|
|
6167
|
+
onAttachUrlBuilt?.(parts);
|
|
6168
|
+
let totpIssuedAt = nowMs();
|
|
6169
|
+
let reminted = 0;
|
|
6170
|
+
const relayUrl = parts.wssUrl;
|
|
6171
|
+
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).";
|
|
6172
|
+
const warningPrefix = authorityWarning ? `⚠️ scheme_url 경고: ${authorityWarning}\n\n` : "";
|
|
6173
|
+
const guiAvailable = canOpenBrowser();
|
|
6174
|
+
/** Builds the totp object surfaced in results (fresh expiresAt + reminted). */
|
|
6175
|
+
const totpResult = () => {
|
|
6176
|
+
if (!totpMeta) return void 0;
|
|
6177
|
+
const expiresAtMs = totpIssuedAt + 180 * 1e3;
|
|
6178
|
+
return {
|
|
6179
|
+
enabled: true,
|
|
6180
|
+
ttlSeconds: totpMeta.ttlSeconds,
|
|
6181
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
6182
|
+
...reminted > 0 ? { reminted } : {}
|
|
6183
|
+
};
|
|
6184
|
+
};
|
|
6185
|
+
/**
|
|
6186
|
+
* Segmented wait with TOTP re-mint (issue #626 §3). Resolves with the
|
|
6187
|
+
* attached page list, or rejects on timeout. Between SEGMENT_MS slices it
|
|
6188
|
+
* re-mints when the code has aged past the threshold (max ~4 re-mints over
|
|
6189
|
+
* 600 s). Returns immediately once a matching page attaches (no re-mint).
|
|
6190
|
+
*/
|
|
6191
|
+
async function waitWithRemint() {
|
|
6192
|
+
const deadline = nowMs() + callTimeoutMs;
|
|
6193
|
+
if (isMatchingPage(conn.listTargets())) return conn.listTargets();
|
|
6194
|
+
for (;;) {
|
|
6195
|
+
const remaining = deadline - nowMs();
|
|
6196
|
+
if (remaining <= 0) throw new Error(`start_attach: 타임아웃 (${callTimeoutMs}ms)`);
|
|
6197
|
+
const segmentMs = Math.min(START_ATTACH_SEGMENT_MS, remaining);
|
|
6198
|
+
try {
|
|
6199
|
+
return await waitForAttachWithEvents(conn, isMatchingPage, segmentMs);
|
|
6200
|
+
} catch {
|
|
6201
|
+
if (totpMeta && nowMs() - totpIssuedAt >= 15e4) {
|
|
6202
|
+
attachUrl = mintAttachUrl(parts);
|
|
6203
|
+
onAttachUrlBuilt?.(parts);
|
|
6204
|
+
totpIssuedAt = nowMs();
|
|
6205
|
+
reminted += 1;
|
|
6206
|
+
}
|
|
6207
|
+
}
|
|
6208
|
+
}
|
|
6209
|
+
}
|
|
6210
|
+
/**
|
|
6211
|
+
* Assembles the success result after a page attaches. `baseText` carries the
|
|
6212
|
+
* QR + pre-wait JSON block (the QR the user already scanned). The attach
|
|
6213
|
+
* itself ends the wait, so the QR is moot — what matters now is the final
|
|
6214
|
+
* TOTP state. If the segmented wait re-minted (issue #626 §3), surface the
|
|
6215
|
+
* post-wait `totp` block (fresh `expiresAt` + `reminted` count) so the result
|
|
6216
|
+
* reflects how many times the code rotated during the wait. SECRET-HANDLING:
|
|
6217
|
+
* the totp block carries expiresAt + reminted only — never the code value.
|
|
6218
|
+
*/
|
|
6219
|
+
const successResult = (baseText) => {
|
|
6220
|
+
const pagesResult = listPages(conn, getTunnelStatus());
|
|
6221
|
+
const finalTotp = totpResult();
|
|
6222
|
+
const remintNote = finalTotp && reminted > 0 ? `\n\n${JSON.stringify({ totp: finalTotp }, null, 2)}` : "";
|
|
6223
|
+
return { content: [{
|
|
6224
|
+
type: "text",
|
|
6225
|
+
text: `${baseText}\n\n${JSON.stringify(pagesResult, null, 2)}${remintNote}`
|
|
6226
|
+
}] };
|
|
6227
|
+
};
|
|
6228
|
+
/** Runs the wait (when requested) and returns success/timeout result. */
|
|
6229
|
+
const runWait = async (baseText) => {
|
|
6230
|
+
if (!waitForAttach) return { content: [{
|
|
6231
|
+
type: "text",
|
|
6232
|
+
text: baseText
|
|
6233
|
+
}] };
|
|
6234
|
+
try {
|
|
6235
|
+
await waitWithRemint();
|
|
6236
|
+
} catch {
|
|
6237
|
+
const observed = conn.listTargets();
|
|
6238
|
+
return {
|
|
6239
|
+
content: [{
|
|
6240
|
+
type: "text",
|
|
6241
|
+
text: buildTimeoutError(baseText, callTimeoutMs / 1e3, observed)
|
|
6242
|
+
}],
|
|
6243
|
+
isError: true
|
|
6244
|
+
};
|
|
6245
|
+
}
|
|
6246
|
+
return successResult(baseText);
|
|
6247
|
+
};
|
|
6248
|
+
if (!guiAvailable) {
|
|
6249
|
+
const headlessNote = "GUI 환경이 감지되지 않았습니다 (headless/remote 환경). 텍스트 QR을 폰 카메라로 스캔하거나, 로컬 GUI 환경에서 실행하세요.\n\n";
|
|
6250
|
+
const qr = await renderQr(attachUrl);
|
|
6251
|
+
return runWait(`${warningPrefix}${headlessNote}${header}\n${JSON.stringify({
|
|
6252
|
+
attachUrl,
|
|
6253
|
+
relayUrl,
|
|
6254
|
+
...totpResult() ? { totp: totpResult() } : {}
|
|
6255
|
+
}, null, 2)}\n\n${qr}`);
|
|
6256
|
+
}
|
|
6257
|
+
if (guiAvailable && qrHttpServer) {
|
|
6258
|
+
const browserResult = await openQrInBrowser(qrHttpServer.buildAttachPageUrl(attachUrl), `http://127.0.0.1:${qrHttpServer.port}/qr.png?u=${encodeURIComponent(attachUrl)}`);
|
|
6259
|
+
if (browserResult.opened) {
|
|
6260
|
+
const retriedNote = browserResult.retried ? " (1회 retry 후 성공)" : "";
|
|
6261
|
+
const openResult = {
|
|
6262
|
+
attempted: true,
|
|
6263
|
+
succeeded: true,
|
|
6264
|
+
...browserResult.retried ? { retried: true } : {}
|
|
6265
|
+
};
|
|
6266
|
+
return runWait(`${warningPrefix}${header}\n${JSON.stringify({
|
|
6267
|
+
relayUrl,
|
|
6268
|
+
openResult,
|
|
6269
|
+
...totpResult() ? { totp: totpResult() } : {}
|
|
6270
|
+
}, null, 2)}\n\n브라우저에서 QR을 열었습니다${retriedNote}. 폰 카메라로 스캔하세요.\nURL: ${browserResult.httpUrl}`);
|
|
6271
|
+
}
|
|
6272
|
+
const openResult = {
|
|
6273
|
+
attempted: true,
|
|
6274
|
+
succeeded: false,
|
|
6275
|
+
failureReason: browserResult.error ?? "브라우저 실행 후보 모두 실패",
|
|
6276
|
+
pngUrl: browserResult.pngUrl,
|
|
6277
|
+
...browserResult.stderrSummary ? { stderrSummary: browserResult.stderrSummary } : {}
|
|
6278
|
+
};
|
|
6279
|
+
const stderrNote = browserResult.stderrSummary ? `\nstderr: ${browserResult.stderrSummary}` : "";
|
|
6280
|
+
const fallbackNote = `브라우저 자동 열기에 실패했습니다. 다음 URL을 직접 브라우저에서 여세요:\n${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stderrNote + "\n\n";
|
|
6281
|
+
const qr = await renderQr(attachUrl);
|
|
6282
|
+
return runWait(`${warningPrefix}${fallbackNote}${header}\n${JSON.stringify({
|
|
6283
|
+
attachUrl,
|
|
6284
|
+
relayUrl,
|
|
6285
|
+
openResult,
|
|
6286
|
+
...totpResult() ? { totp: totpResult() } : {}
|
|
6287
|
+
}, null, 2)}\n\n${qr}`);
|
|
6288
|
+
}
|
|
6289
|
+
const qr = await renderQr(attachUrl);
|
|
6290
|
+
return runWait(`${warningPrefix}${header}\n${JSON.stringify({
|
|
6291
|
+
attachUrl,
|
|
6292
|
+
relayUrl,
|
|
6293
|
+
...totpResult() ? { totp: totpResult() } : {}
|
|
6294
|
+
}, null, 2)}\n\n${qr}`);
|
|
6295
|
+
}
|
|
6007
6296
|
const server = new Server({
|
|
6008
6297
|
name: "ait-debug",
|
|
6009
|
-
version: "0.1.
|
|
6298
|
+
version: "0.1.111"
|
|
6010
6299
|
}, { capabilities: { tools: { listChanged: true } } });
|
|
6011
6300
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
6012
6301
|
const conn = router.active;
|
|
@@ -6028,12 +6317,47 @@ function createDebugServer(deps) {
|
|
|
6028
6317
|
if (name === "start_debug") {
|
|
6029
6318
|
const rawMode = request.params.arguments?.mode;
|
|
6030
6319
|
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;
|
|
6320
|
+
if (mode === null) return mcpError("start_debug: mode가 올바르지 않습니다. 'local-browser' | 'relay-sandbox' | 'relay-staging' 중 하나를 전달하세요. (relay-live / env 4는 #665에서 제거됐습니다.)");
|
|
6033
6321
|
const rawProjectRoot = request.params.arguments?.projectRoot;
|
|
6034
6322
|
const projectRoot = typeof rawProjectRoot === "string" ? rawProjectRoot : void 0;
|
|
6035
6323
|
try {
|
|
6036
|
-
return jsonResult$1(await router.switchMode(mode,
|
|
6324
|
+
return jsonResult$1(await router.switchMode(mode, projectRoot));
|
|
6325
|
+
} catch (err) {
|
|
6326
|
+
return errorResult(err, name);
|
|
6327
|
+
}
|
|
6328
|
+
}
|
|
6329
|
+
if (name === "start_attach") {
|
|
6330
|
+
const args = request.params.arguments;
|
|
6331
|
+
let attachConn = conn;
|
|
6332
|
+
const rawMode = args?.mode;
|
|
6333
|
+
if (rawMode !== void 0) {
|
|
6334
|
+
const mode = normalizeStartDebugMode(rawMode);
|
|
6335
|
+
if (mode === null || mode === "local-browser") return mcpError("start_attach: mode가 올바르지 않습니다. 'relay-sandbox' | 'relay-staging' 중 하나를 전달하세요 (local-browser는 QR attach가 없어 start_attach에서 지원하지 않습니다).");
|
|
6336
|
+
const targetEnv = envForMode(mode);
|
|
6337
|
+
if (resolveEnvironment() !== targetEnv) {
|
|
6338
|
+
const rawProjectRoot = args?.projectRoot;
|
|
6339
|
+
const projectRoot = typeof rawProjectRoot === "string" ? rawProjectRoot : void 0;
|
|
6340
|
+
try {
|
|
6341
|
+
await router.switchMode(mode, projectRoot);
|
|
6342
|
+
} catch (err) {
|
|
6343
|
+
return errorResult(err, name);
|
|
6344
|
+
}
|
|
6345
|
+
attachConn = router.active;
|
|
6346
|
+
}
|
|
6347
|
+
}
|
|
6348
|
+
const attachEnv = resolveEnvironment();
|
|
6349
|
+
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 모드로 전환하세요.");
|
|
6350
|
+
const waitForAttach = true;
|
|
6351
|
+
const rawWaitTimeout = args?.wait_timeout_seconds;
|
|
6352
|
+
const callTimeoutMs = (() => {
|
|
6353
|
+
if (typeof rawWaitTimeout !== "number" || !Number.isFinite(rawWaitTimeout)) return waitForAttachTimeoutMs;
|
|
6354
|
+
if (rawWaitTimeout <= 0) return waitForAttachTimeoutMs;
|
|
6355
|
+
return Math.round(Math.max(1, Math.min(600, rawWaitTimeout))) * 1e3;
|
|
6356
|
+
})();
|
|
6357
|
+
try {
|
|
6358
|
+
const prep = await prepareAttach(attachEnv, args, attachConn);
|
|
6359
|
+
if (!prep.ok) return prep.error;
|
|
6360
|
+
return await renderAndMaybeWait(prep, waitForAttach, callTimeoutMs, attachConn);
|
|
6037
6361
|
} catch (err) {
|
|
6038
6362
|
return errorResult(err, name);
|
|
6039
6363
|
}
|
|
@@ -6078,386 +6402,6 @@ function createDebugServer(deps) {
|
|
|
6078
6402
|
} catch (err) {
|
|
6079
6403
|
return errorResult(err, name);
|
|
6080
6404
|
}
|
|
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
6405
|
try {
|
|
6462
6406
|
await conn.enableDomains();
|
|
6463
6407
|
} catch (err) {
|
|
@@ -6496,7 +6440,7 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
6496
6440
|
case "evaluate": {
|
|
6497
6441
|
const expression = request.params.arguments?.expression;
|
|
6498
6442
|
if (typeof expression !== "string" || expression === "") return mcpError("evaluate: expression 인자가 비어 있습니다. 평가할 JavaScript 표현식을 전달하세요.");
|
|
6499
|
-
if (conn
|
|
6443
|
+
if (!connectionHostsAllowed(conn)) return mcpError("evaluate: 현재 연결된 페이지는 debug 허용 호스트가 아닙니다 (#665). 허용 호스트: localhost, *.trycloudflare.com, *.private-apps.tossmini.com.");
|
|
6500
6444
|
return jsonResult$1(await evaluate(conn, expression));
|
|
6501
6445
|
}
|
|
6502
6446
|
case "call_sdk": {
|
|
@@ -6504,7 +6448,7 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
6504
6448
|
if (typeof sdkName !== "string" || sdkName === "") return mcpError("call_sdk: name 인자가 비어 있습니다. 호출할 SDK 메서드 이름을 전달하세요.");
|
|
6505
6449
|
const rawArgs = request.params.arguments?.args;
|
|
6506
6450
|
const sdkArgs = Array.isArray(rawArgs) ? rawArgs : [];
|
|
6507
|
-
if (conn
|
|
6451
|
+
if (!connectionHostsAllowed(conn)) return mcpError("call_sdk: 현재 연결된 페이지는 debug 허용 호스트가 아닙니다 (#665). 허용 호스트: localhost, *.trycloudflare.com, *.private-apps.tossmini.com.");
|
|
6508
6452
|
const sdkResult = await callSdk(conn, sdkName, sdkArgs);
|
|
6509
6453
|
if (!sdkResult.ok && typeof sdkResult.error === "string" && sdkResult.error.startsWith("sdk-absent:")) return sdkAbsentError("call_sdk", conn.kind === "local");
|
|
6510
6454
|
return envelopeResult$1(sdkResult, name, env, conn.listTargets().length > 0);
|
|
@@ -6518,7 +6462,7 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
6518
6462
|
const projectRoot = typeof rawRoot === "string" ? rawRoot : process.cwd();
|
|
6519
6463
|
const rawTimeout = request.params.arguments?.timeout_ms;
|
|
6520
6464
|
const timeoutMs = typeof rawTimeout === "number" && rawTimeout >= 1e3 && rawTimeout <= 6e5 ? rawTimeout : void 0;
|
|
6521
|
-
if (conn
|
|
6465
|
+
if (!connectionHostsAllowed(conn)) return mcpError("run_tests: 현재 연결된 페이지는 debug 허용 호스트가 아닙니다 (#665). 허용 호스트: localhost, *.trycloudflare.com, *.private-apps.tossmini.com.");
|
|
6522
6466
|
if (runTestsInFlight) return mcpError("run_tests: 이미 다른 테스트 실행이 진행 중입니다 (single-attach 모델: 페이지는 한 번에 하나의 실행만 처리). 완료 후 다시 시도하세요.");
|
|
6523
6467
|
runTestsInFlight = true;
|
|
6524
6468
|
try {
|
|
@@ -6548,24 +6492,52 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
6548
6492
|
}
|
|
6549
6493
|
/**
|
|
6550
6494
|
* 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'
|
|
6495
|
+
* `null` when the value is not one of the three accepted modes:
|
|
6496
|
+
* 'local-browser' | 'relay-sandbox' | 'relay-staging'
|
|
6553
6497
|
*
|
|
6554
6498
|
* Hard rename (issue #398): the older `local`/`mobile`/`staging`/`live` names
|
|
6555
6499
|
* and their aliases are no longer accepted — pre-1.0, no back-compat.
|
|
6500
|
+
* `relay-live` (env 4) removed in #665.
|
|
6556
6501
|
*/
|
|
6557
6502
|
function normalizeStartDebugMode(raw) {
|
|
6558
|
-
if (raw === "local-browser" || raw === "relay-sandbox" || raw === "relay-staging"
|
|
6503
|
+
if (raw === "local-browser" || raw === "relay-sandbox" || raw === "relay-staging") return raw;
|
|
6559
6504
|
return null;
|
|
6560
6505
|
}
|
|
6561
6506
|
/**
|
|
6507
|
+
* Positive-allowlist kill-switch for side-effect MCP tools (#665).
|
|
6508
|
+
*
|
|
6509
|
+
* Returns `true` when the connection's attached targets are all on allowed
|
|
6510
|
+
* debug hosts (localhost / trycloudflare / private-apps). Returns `false` when
|
|
6511
|
+
* any target's page URL is on a non-allowed host (e.g. `apps.tossmini.com`).
|
|
6512
|
+
*
|
|
6513
|
+
* For local connections this always returns `true` — the local Chromium is
|
|
6514
|
+
* always on localhost. For relay connections without any pages it returns
|
|
6515
|
+
* `true` (no pages = nothing to block; the caller's page-missing guard fires
|
|
6516
|
+
* first).
|
|
6517
|
+
*
|
|
6518
|
+
* SECRET-HANDLING: hostnames are NEVER logged here — only the boolean result
|
|
6519
|
+
* is returned to the caller.
|
|
6520
|
+
*/
|
|
6521
|
+
function connectionHostsAllowed(conn) {
|
|
6522
|
+
if (conn.kind === "local") return true;
|
|
6523
|
+
const pages = conn.listTargets();
|
|
6524
|
+
if (pages.length === 0) return true;
|
|
6525
|
+
return pages.every((p) => {
|
|
6526
|
+
try {
|
|
6527
|
+
return isDebugAllowedHost(new URL(p.url ?? "").hostname);
|
|
6528
|
+
} catch {
|
|
6529
|
+
return false;
|
|
6530
|
+
}
|
|
6531
|
+
});
|
|
6532
|
+
}
|
|
6533
|
+
/**
|
|
6562
6534
|
* Builds a trivial `ConnectionRouter` pinned to a single connection (issue
|
|
6563
6535
|
* #348). Used by `createDebugServer` when no real dual router is injected —
|
|
6564
6536
|
* every existing single-connection test and the `local`-only / `relay`-only
|
|
6565
6537
|
* 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
|
-
*
|
|
6538
|
+
* honors a request that matches the connection's own kind; any cross-family
|
|
6539
|
+
* request is rejected with a clear "dynamic switch unavailable in this session"
|
|
6540
|
+
* error. `confirm` parameter and `relay-live` gate removed (#665).
|
|
6569
6541
|
*/
|
|
6570
6542
|
function makeSingleConnectionRouter(connection) {
|
|
6571
6543
|
return {
|
|
@@ -6573,18 +6545,15 @@ function makeSingleConnectionRouter(connection) {
|
|
|
6573
6545
|
return connection;
|
|
6574
6546
|
},
|
|
6575
6547
|
activeRelayOrigin: void 0,
|
|
6576
|
-
switchMode(mode,
|
|
6548
|
+
switchMode(mode, _projectRoot) {
|
|
6577
6549
|
if (mode === "relay-sandbox") return Promise.reject(/* @__PURE__ */ new Error("start_debug: 이 세션은 단일 연결만 보유합니다 — 'relay-sandbox'(환경 2 PWA, 외부 relay)로 동적 전환할 수 없습니다 (dual-connection 데몬에서만 지원). MCP 서버를 relay-sandbox 모드로 재시작하세요."));
|
|
6578
6550
|
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());
|
|
6551
|
+
const environment = deriveEnvironment(connection.kind);
|
|
6582
6552
|
return Promise.resolve({
|
|
6583
6553
|
mode,
|
|
6584
6554
|
environment,
|
|
6585
6555
|
kind: connection.kind,
|
|
6586
|
-
|
|
6587
|
-
nextStep: connection.kind === "relay" ? "build_attach_url로 attach QR을 생성하세요." : "list_pages로 로컬 페이지 attach를 확인하세요."
|
|
6556
|
+
nextStep: connection.kind === "relay" ? "start_attach로 attach QR 생성 + 폰 attach까지 한 번에 진행하세요." : "list_pages로 로컬 페이지 attach를 확인하세요."
|
|
6588
6557
|
});
|
|
6589
6558
|
}
|
|
6590
6559
|
};
|
|
@@ -6599,7 +6568,10 @@ function makeSingleConnectionRouter(connection) {
|
|
|
6599
6568
|
function rebuildAttachUrl(parts) {
|
|
6600
6569
|
const secret = process.env.AIT_DEBUG_TOTP_SECRET;
|
|
6601
6570
|
const code = secret ? generateTotp(secret) : void 0;
|
|
6602
|
-
return parts.kind === "launcher" ? buildLauncherAttachUrl(parts.tunnelHttpUrl, parts.wssUrl, code, {
|
|
6571
|
+
return parts.kind === "launcher" ? buildLauncherAttachUrl(parts.tunnelHttpUrl, parts.wssUrl, code, {
|
|
6572
|
+
name: parts.appName,
|
|
6573
|
+
...parts.selfdebug ? { selfdebug: true } : {}
|
|
6574
|
+
}) : buildDeepLinkAttachUrl(parts.schemeUrl, parts.wssUrl, code);
|
|
6603
6575
|
}
|
|
6604
6576
|
function jsonResult$1(value) {
|
|
6605
6577
|
return { content: [{
|
|
@@ -6792,8 +6764,9 @@ async function bootLocalFamily() {
|
|
|
6792
6764
|
*
|
|
6793
6765
|
* Booted lazily via the dual router's `bootLazyFor('relay-intoss')` callback
|
|
6794
6766
|
* (symmetry with {@link bootLocalFamily}), at most once on the first
|
|
6795
|
-
* `start_debug({ mode: 'relay-staging'
|
|
6796
|
-
*
|
|
6767
|
+
* `start_debug({ mode: 'relay-staging' })` (all-lazy, #396 — every relay boot now
|
|
6768
|
+
* flows through `switchMode` after the project-local secret load). `relay-live`
|
|
6769
|
+
* removed (#665).
|
|
6797
6770
|
*
|
|
6798
6771
|
* The relay base URL is only known after `startChiiRelay()` resolves, so the
|
|
6799
6772
|
* `ChiiCdpConnection` (via {@link createRelayConnection}) is constructed inside
|
|
@@ -6880,7 +6853,7 @@ async function bootRelayFamily(options = {}) {
|
|
|
6880
6853
|
* we did not start.
|
|
6881
6854
|
*
|
|
6882
6855
|
* `getTunnelStatus()` reports `up: true` with a `wssUrl` derived from
|
|
6883
|
-
* `relayBaseUrl` (http→ws, https→wss) so the `
|
|
6856
|
+
* `relayBaseUrl` (http→ws, https→wss) so the `start_attach` gate
|
|
6884
6857
|
* (`up: true && wssUrl !== null`) is satisfied even though we never opened a
|
|
6885
6858
|
* cloudflared tunnel ourselves.
|
|
6886
6859
|
*
|
|
@@ -6925,14 +6898,14 @@ async function bootExternalRelayFamily(relayBaseUrl, relayLocalUrl) {
|
|
|
6925
6898
|
/**
|
|
6926
6899
|
* Maps a `StartDebugMode` to the {@link FamilyKey} that serves it (issue #378).
|
|
6927
6900
|
* local-browser → 'local-browser'; relay-sandbox → 'relay-sandbox';
|
|
6928
|
-
* relay-staging
|
|
6901
|
+
* relay-staging → 'relay-intoss' (the intoss-private relay slot).
|
|
6902
|
+
* `relay-live` removed (#665).
|
|
6929
6903
|
*/
|
|
6930
6904
|
function familyKeyForMode(mode) {
|
|
6931
6905
|
switch (mode) {
|
|
6932
6906
|
case "local-browser": return "local-browser";
|
|
6933
6907
|
case "relay-sandbox": return "relay-sandbox";
|
|
6934
|
-
case "relay-staging":
|
|
6935
|
-
case "relay-live": return "relay-intoss";
|
|
6908
|
+
case "relay-staging": return "relay-intoss";
|
|
6936
6909
|
}
|
|
6937
6910
|
}
|
|
6938
6911
|
/** The error thrown / surfaced when entering `mobile` without AIT_RELAY_BASE_URL. */
|
|
@@ -6991,12 +6964,11 @@ const NULL_CDP_CONNECTION = {
|
|
|
6991
6964
|
* restarting the MCP server.
|
|
6992
6965
|
*
|
|
6993
6966
|
* 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`).
|
|
6967
|
+
* external relay) and `relay-staging` (intoss relay) are BOTH `kind: 'relay'`.
|
|
6968
|
+
* A single "opposite-kind" slot could not warm-keep both at once — they would
|
|
6969
|
+
* collide. The three `FamilyKey`s (`local-browser` / `relay-intoss` /
|
|
6970
|
+
* `relay-sandbox`) give each its own warm slot. `relay-live` (env 4) removed
|
|
6971
|
+
* (#665) — `relay-intoss` slot now maps only to `relay-staging`.
|
|
7000
6972
|
*
|
|
7001
6973
|
* Why all-lazy (#396): the relay TOTP secret now lives in a project-local
|
|
7002
6974
|
* `.ait_relay` file loaded read-only by `switchMode` BEFORE a relay family boots.
|
|
@@ -7006,15 +6978,14 @@ const NULL_CDP_CONNECTION = {
|
|
|
7006
6978
|
* `buildRelayVerifyAuth()` run at the boot site.
|
|
7007
6979
|
*
|
|
7008
6980
|
* `switchMode`:
|
|
7009
|
-
* 1. rejects re-entrant swaps (`swapInFlight`)
|
|
6981
|
+
* 1. rejects re-entrant swaps (`swapInFlight`);
|
|
7010
6982
|
* 2. resolves the requested mode's `FamilyKey`:
|
|
7011
6983
|
* `lazyFamilies.get(key) ?? (boot via bootLazyFor(key), store)`;
|
|
7012
6984
|
* 3. flips `active` (the MCP `Server` never re-handshakes — it reads through
|
|
7013
6985
|
* `active` per request);
|
|
7014
|
-
* 4.
|
|
7015
|
-
* 5. stops the old attach watcher and re-arms one on the new connection
|
|
6986
|
+
* 4. stops the old attach watcher and re-arms one on the new connection
|
|
7016
6987
|
* (the watcher self-clears, so re-arm is mandatory);
|
|
7017
|
-
*
|
|
6988
|
+
* 5. emits `tools/list_changed`.
|
|
7018
6989
|
*
|
|
7019
6990
|
* Inactive infra is left WARM — teardown happens only at process exit (the
|
|
7020
6991
|
* unified shutdown in the run functions), which is what keeps a phone attach
|
|
@@ -7060,10 +7031,10 @@ var DualConnectionRouter = class {
|
|
|
7060
7031
|
/**
|
|
7061
7032
|
* Live tunnel status of the active relay family (issues #356, #378). Reads
|
|
7062
7033
|
* the ACTIVE family's tunnel when it has one (so `relay-sandbox` surfaces the
|
|
7063
|
-
* external relay wss and `relay-staging
|
|
7034
|
+
* external relay wss and `relay-staging` the intoss relay wss); otherwise
|
|
7064
7035
|
* falls back to the first booted family that has a tunnel. Returns "down"
|
|
7065
7036
|
* until any relay family is booted (any session before the first relay
|
|
7066
|
-
* start_debug) — the correct signal for `
|
|
7037
|
+
* start_debug) — the correct signal for `start_attach` (no tunnel yet).
|
|
7067
7038
|
*/
|
|
7068
7039
|
relayTunnelStatus() {
|
|
7069
7040
|
if (this.activeFamily?.getTunnelStatus) return this.activeFamily.getTunnelStatus();
|
|
@@ -7098,7 +7069,7 @@ var DualConnectionRouter = class {
|
|
|
7098
7069
|
this.deps.onPageAttach?.();
|
|
7099
7070
|
if (activeFamily.connection.kind === "relay") {
|
|
7100
7071
|
const firstTarget = activeFamily.connection.listTargets()[0];
|
|
7101
|
-
const env = deriveEnvironment(activeFamily.connection.kind,
|
|
7072
|
+
const env = deriveEnvironment(activeFamily.connection.kind, activeFamily.relayOrigin);
|
|
7102
7073
|
const inspectorStableUrl = this.deps.getInspectorStableUrl?.() ?? null;
|
|
7103
7074
|
this.deps.devtoolsOpener.open({
|
|
7104
7075
|
inspectorStableUrl,
|
|
@@ -7156,25 +7127,22 @@ var DualConnectionRouter = class {
|
|
|
7156
7127
|
this.lazyFamilies.set(key, booted);
|
|
7157
7128
|
return booted;
|
|
7158
7129
|
}
|
|
7159
|
-
async switchMode(mode,
|
|
7130
|
+
async switchMode(mode, projectRoot) {
|
|
7160
7131
|
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
7132
|
this.swapInFlight = true;
|
|
7163
7133
|
try {
|
|
7164
7134
|
if (isRelayMode(mode)) await loadRelaySecretReadOnly({ projectRoot });
|
|
7165
7135
|
const target = await this.familyFor(familyKeyForMode(mode), projectRoot);
|
|
7166
7136
|
this.activeFamily = target;
|
|
7167
|
-
setLiveIntent(mode === "relay-live");
|
|
7168
7137
|
this.stopWatcher();
|
|
7169
7138
|
this.armWatcher();
|
|
7170
7139
|
this.server?.sendToolListChanged();
|
|
7171
7140
|
const wantRelay = isRelayMode(mode);
|
|
7172
7141
|
return {
|
|
7173
7142
|
mode,
|
|
7174
|
-
environment: deriveEnvironment(target.connection.kind,
|
|
7143
|
+
environment: deriveEnvironment(target.connection.kind, target.relayOrigin),
|
|
7175
7144
|
kind: target.connection.kind,
|
|
7176
|
-
|
|
7177
|
-
nextStep: wantRelay ? "build_attach_url로 attach QR을 생성하세요 (relay 세션)." : "list_pages로 로컬 Chromium 페이지 attach를 확인하세요."
|
|
7145
|
+
nextStep: wantRelay ? "start_attach로 attach QR 생성 + 폰 attach까지 한 번에 진행하세요 (relay 세션)." : "list_pages로 로컬 Chromium 페이지 attach를 확인하세요."
|
|
7178
7146
|
};
|
|
7179
7147
|
} finally {
|
|
7180
7148
|
this.swapInFlight = false;
|
|
@@ -7233,7 +7201,7 @@ async function runDebugServer(options = {}) {
|
|
|
7233
7201
|
})),
|
|
7234
7202
|
attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null,
|
|
7235
7203
|
inspectorUrl,
|
|
7236
|
-
mode: deriveEnvironment(router.active.kind,
|
|
7204
|
+
mode: deriveEnvironment(router.active.kind, router.activeRelayOrigin)
|
|
7237
7205
|
};
|
|
7238
7206
|
};
|
|
7239
7207
|
const getDirectInspectorUrl = () => {
|
|
@@ -7365,7 +7333,7 @@ async function runDebugServer(options = {}) {
|
|
|
7365
7333
|
* 1. `start_debug({ mode: 'local-browser' })` launches a local Chromium with
|
|
7366
7334
|
* `--remote-debugging-port=<port>` and attaches a `LocalCdpConnection`;
|
|
7367
7335
|
* 2. the intoss/external relay families lazy-boot on the first
|
|
7368
|
-
* `start_debug({ mode: 'relay-staging' | 'relay-
|
|
7336
|
+
* `start_debug({ mode: 'relay-staging' | 'relay-sandbox' })` (#665: relay-live removed);
|
|
7369
7337
|
* 3. all of this runs through the SAME direction-neutral
|
|
7370
7338
|
* `DualConnectionRouter` that `runDebugServer` uses (issue #356).
|
|
7371
7339
|
*
|
|
@@ -7377,7 +7345,7 @@ async function runDebugServer(options = {}) {
|
|
|
7377
7345
|
* env 1 (local), then env 3 (intoss-private) in ONE session, no restart" — now
|
|
7378
7346
|
* works from either entry point.
|
|
7379
7347
|
*
|
|
7380
|
-
* `
|
|
7348
|
+
* `start_attach` (relay-specific) stays effectively hidden / non-applicable
|
|
7381
7349
|
* until the relay family is booted: before the first relay switch the env
|
|
7382
7350
|
* derives to `mock` and `relayTunnelStatus()` reports "down", so the tool fails
|
|
7383
7351
|
* with a clear "tunnel not up" message. After a relay switch the relay tunnel
|
|
@@ -7588,10 +7556,9 @@ async function runLocalDebugServer(options = {}) {
|
|
|
7588
7556
|
* Symmetry with `runDebugServer` / `runLocalDebugServer` (#356, #378, #396): all
|
|
7589
7557
|
* three families are lazy-booted — the env-2 external relay on the first
|
|
7590
7558
|
* `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).
|
|
7559
|
+
* the intoss relay on `relay-staging` (#665: relay-live removed) — so a
|
|
7560
|
+
* `--target=mobile` session can hot-switch without a restart. The active env
|
|
7561
|
+
* derives to `relay-mobile` (external-PWA origin).
|
|
7595
7562
|
*
|
|
7596
7563
|
* SECRET-HANDLING: `AIT_RELAY_BASE_URL` is read once here via
|
|
7597
7564
|
* {@link readMobileRelayBaseUrl}; when unset it throws
|
|
@@ -7972,25 +7939,30 @@ const DEV_TOOL_DEFINITIONS = [
|
|
|
7972
7939
|
availableIn: "both"
|
|
7973
7940
|
},
|
|
7974
7941
|
{
|
|
7975
|
-
name: "
|
|
7976
|
-
description: "
|
|
7942
|
+
name: "start_attach",
|
|
7943
|
+
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
7944
|
inputSchema: {
|
|
7978
7945
|
type: "object",
|
|
7979
7946
|
properties: {
|
|
7980
|
-
|
|
7947
|
+
mode: {
|
|
7981
7948
|
type: "string",
|
|
7982
|
-
|
|
7949
|
+
enum: [
|
|
7950
|
+
"local-browser",
|
|
7951
|
+
"relay-sandbox",
|
|
7952
|
+
"relay-staging"
|
|
7953
|
+
],
|
|
7954
|
+
description: "Optional relay mode to switch into before attaching."
|
|
7983
7955
|
},
|
|
7984
|
-
|
|
7985
|
-
type: "
|
|
7986
|
-
description: "
|
|
7956
|
+
scheme_url: {
|
|
7957
|
+
type: "string",
|
|
7958
|
+
description: "The intoss-private:// URL from `ait deploy --scheme-only` (env 3/relay-staging)."
|
|
7987
7959
|
},
|
|
7988
7960
|
wait_timeout_seconds: {
|
|
7989
7961
|
type: "number",
|
|
7990
|
-
description: "Maximum seconds to wait
|
|
7962
|
+
description: "Maximum seconds to wait for a page to attach (default 60, range 1–600). Invalid inputs fall back to default."
|
|
7991
7963
|
}
|
|
7992
7964
|
},
|
|
7993
|
-
required: [
|
|
7965
|
+
required: []
|
|
7994
7966
|
},
|
|
7995
7967
|
availableIn: "relay"
|
|
7996
7968
|
},
|
|
@@ -8088,10 +8060,6 @@ const DEV_TOOL_DEFINITIONS = [
|
|
|
8088
8060
|
timeout_ms: {
|
|
8089
8061
|
type: "number",
|
|
8090
8062
|
description: "Per-file evaluate timeout in ms."
|
|
8091
|
-
},
|
|
8092
|
-
confirm: {
|
|
8093
|
-
type: "boolean",
|
|
8094
|
-
description: "Required in relay-live sessions."
|
|
8095
8063
|
}
|
|
8096
8064
|
},
|
|
8097
8065
|
required: ["files"]
|
|
@@ -8117,7 +8085,7 @@ const CDP_ONLY_TOOL_NAMES = new Set([
|
|
|
8117
8085
|
* Listed in dev-mode tool surface (issue #323) so agents get a hand-off hint
|
|
8118
8086
|
* toward `--mode=debug` instead of "Unknown tool".
|
|
8119
8087
|
*/
|
|
8120
|
-
const TIER_B_TOOL_NAMES = new Set(["
|
|
8088
|
+
const TIER_B_TOOL_NAMES = new Set(["start_attach"]);
|
|
8121
8089
|
/**
|
|
8122
8090
|
* Builds the `list_pages` dev-mode shim response.
|
|
8123
8091
|
* Returns the Vite dev URL as a single-entry page list with `devMode: true`.
|
|
@@ -8227,7 +8195,7 @@ function createDevServer(deps = {}) {
|
|
|
8227
8195
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
8228
8196
|
const server = new Server({
|
|
8229
8197
|
name: "ait-devtools",
|
|
8230
|
-
version: "0.1.
|
|
8198
|
+
version: "0.1.111"
|
|
8231
8199
|
}, { capabilities: { tools: {} } });
|
|
8232
8200
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
8233
8201
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -8302,7 +8270,7 @@ async function runDevServer() {
|
|
|
8302
8270
|
*
|
|
8303
8271
|
* --mode=debug (default)
|
|
8304
8272
|
* --target=relay (default) — CDP/Chii relay + cloudflared quick tunnel.
|
|
8305
|
-
* Attach a running mini-app (real Toss WebView, env 3
|
|
8273
|
+
* Attach a running mini-app (real Toss WebView, env 3) and read its
|
|
8306
8274
|
* console + network over CDP without a human watching a phone.
|
|
8307
8275
|
* --target=local — CDP direct-attach to a local Chromium launched by the
|
|
8308
8276
|
* MCP server (env 1). No relay or tunnel; the browser is launched
|
|
@@ -8318,26 +8286,12 @@ async function runDevServer() {
|
|
|
8318
8286
|
* Back-compat (issue #348): the legacy `--mode`/`--target` flags and `MCP_ENV`
|
|
8319
8287
|
* still work. `--target=relay`/`local` select the initial active connection;
|
|
8320
8288
|
* 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).
|
|
8289
|
+
* restart. `MCP_ENV` values are accepted and ignored (the active connection's
|
|
8290
|
+
* `kind` is authoritative; `relay-live` and `liveIntent` are removed, #665).
|
|
8324
8291
|
*
|
|
8325
8292
|
* Node-only stdio process.
|
|
8326
8293
|
*/
|
|
8327
8294
|
/**
|
|
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
8295
|
* Returns `true` when `--force` or `--takeover` is present in argv.
|
|
8342
8296
|
*
|
|
8343
8297
|
* Both flags are accepted as aliases — `--force` is the short form listed in
|
|
@@ -8394,7 +8348,6 @@ function normalizeTarget(value) {
|
|
|
8394
8348
|
}
|
|
8395
8349
|
async function main() {
|
|
8396
8350
|
const args = process.argv.slice(2);
|
|
8397
|
-
seedLiveIntentFromEnv();
|
|
8398
8351
|
if (parseMode(args) === "dev") await runDevServer();
|
|
8399
8352
|
else {
|
|
8400
8353
|
const target = parseTarget(args);
|
|
@@ -8429,6 +8382,6 @@ if (isEntrypoint()) main().catch((err) => {
|
|
|
8429
8382
|
process.exitCode = 1;
|
|
8430
8383
|
});
|
|
8431
8384
|
//#endregion
|
|
8432
|
-
export { parseForce, parseMode, parseTarget
|
|
8385
|
+
export { parseForce, parseMode, parseTarget };
|
|
8433
8386
|
|
|
8434
8387
|
//# sourceMappingURL=cli.js.map
|