@modootoday/datalab-extension-mcp 1.8.9 → 1.8.11

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 CHANGED
@@ -55,10 +55,10 @@ Avoid sharing a screenshot or the copied setup. If it may have leaked, press **[
55
55
  One command finds **every** AI app on your computer and connects them automatically. Nothing to do per app.
56
56
 
57
57
  1. **Open a terminal** — on Windows type `cmd` in the search box → "Command Prompt"; on Mac press `⌘+Space` and type `Terminal`; on Linux open your terminal app (often `Ctrl+Alt+T`).
58
- 2. **Paste this command and press Enter.** (`@1.8.9` is the pinned version, so the connector never changes behind your back.)
58
+ 2. **Paste this command and press Enter.** (`@1.8.11` is the pinned version, so the connector never changes behind your back.)
59
59
 
60
60
  ```
61
- npx -y @modootoday/datalab-extension-mcp@1.8.9 install
61
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 install
62
62
  ```
63
63
 
64
64
  Lines of English scrolling by is normal. This only downloads and runs the connector — it touches nothing else.
@@ -130,10 +130,10 @@ Apps it **only prints instructions for** (follow the on-screen guide): VS Code,
130
130
  Installing also places the bundled **writing skills**. An AI app reads the name and description of every installed skill on every start, so the more are installed the less room the conversation has. The default therefore places only the ones that work right away.
131
131
 
132
132
  ```
133
- npx -y @modootoday/datalab-extension-mcp skills list
134
- npx -y @modootoday/datalab-extension-mcp skills status
135
- npx -y @modootoday/datalab-extension-mcp skills attach <name>
136
- npx -y @modootoday/datalab-extension-mcp skills detach <name>
133
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 skills list
134
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 skills status
135
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 skills attach <name>
136
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 skills detach <name>
137
137
  ```
138
138
 
139
139
  - `list` shows everything in this version, name and description.
@@ -152,9 +152,9 @@ The **workbench** is this computer, where the connector runs. If you use more th
152
152
  Make the code from the panel with [등록 코드 만들기], or from the terminal.
153
153
 
154
154
  ```
155
- npx -y @modootoday/datalab-extension-mcp browsers add "Work Chrome"
156
- npx -y @modootoday/datalab-extension-mcp browsers list
157
- npx -y @modootoday/datalab-extension-mcp browsers revoke <handle>
155
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 browsers add "Work Chrome"
156
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 browsers list
157
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 browsers revoke <handle>
158
158
  ```
159
159
 
160
160
  `add` **asks you to paste the pairing token** — the same value you used to
@@ -252,7 +252,7 @@ Start it again after a full quit and it picks the tools up. If they still aren't
252
252
  - **When the connection disappears** — run the very command you used at the start once more; it safely re-registers. (Some AI apps clean up their settings file and drop the connection.)
253
253
 
254
254
  ```
255
- npx -y @modootoday/datalab-extension-mcp@1.8.9 install
255
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 install
256
256
  ```
257
257
 
258
258
  - **`'npx' is not recognized...`** — Node.js isn't installed. The "No Node.js?" one-liner in Step 2 is the simplest fix.
@@ -282,7 +282,7 @@ Start it again after a full quit and it picks the tools up. If they still aren't
282
282
  1. Run this in a terminal (it removes the entries registered in your AI apps).
283
283
 
284
284
  ```
285
- npx -y @modootoday/datalab-extension-mcp@1.8.9 uninstall
285
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 uninstall
286
286
  ```
287
287
 
288
288
  2. Turn **MCP 연결** off in the extension settings. The panel is the bridge, so with it off a leftover config line can look nothing up.
package/README.md CHANGED
@@ -49,10 +49,10 @@ Claude 같은 AI 앱을 데이터랩툴즈에 연결해 주는 작은 무료 프
49
49
  명령 한 줄이면 컴퓨터에 있는 AI 앱을 **모두 자동으로** 찾아 연결해 줘요. 앱마다 따로 할 일은 없어요.
50
50
 
51
51
  1. **터미널을 열어요** — Windows는 검색창에 `cmd` → "명령 프롬프트", Mac은 `⌘+스페이스`로 `터미널`, Linux는 터미널 앱(보통 `Ctrl+Alt+T`)이에요.
52
- 2. **아래 명령을 붙여넣고 Enter를 눌러요.** (`@1.8.9`은 커넥터가 나도 모르게 바뀌지 않도록 고정한 버전이에요.)
52
+ 2. **아래 명령을 붙여넣고 Enter를 눌러요.** (`@1.8.11`은 커넥터가 나도 모르게 바뀌지 않도록 고정한 버전이에요.)
53
53
 
54
54
  ```
55
- npx -y @modootoday/datalab-extension-mcp@1.8.9 install
55
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 install
56
56
  ```
57
57
 
58
58
  영어 글자가 여러 줄 올라오는 건 정상이에요. 이 명령은 커넥터를 내려받아 실행할 뿐, 다른 건 건드리지 않아요.
@@ -118,10 +118,10 @@ Claude 같은 AI 앱을 데이터랩툴즈에 연결해 주는 작은 무료 프
118
118
  설치할 때 **글쓰기 도움말(스킬)** 도 함께 놓입니다. AI 앱은 켜질 때마다 설치된 도움말의 이름과 설명을 전부 읽기 때문에, 많이 깔수록 대화에 쓸 자리가 줄어들어요. 그래서 기본은 **바로 쓸 수 있는 것만** 놓습니다.
119
119
 
120
120
  ```
121
- npx -y @modootoday/datalab-extension-mcp skills list
122
- npx -y @modootoday/datalab-extension-mcp skills status
123
- npx -y @modootoday/datalab-extension-mcp skills attach <이름>
124
- npx -y @modootoday/datalab-extension-mcp skills detach <이름>
121
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 skills list
122
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 skills status
123
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 skills attach <이름>
124
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 skills detach <이름>
125
125
  ```
126
126
 
127
127
  - `list` 는 이 버전에 들어 있는 전부를 이름과 설명으로 보여 줘요.
@@ -140,9 +140,9 @@ npx -y @modootoday/datalab-extension-mcp skills detach <이름>
140
140
  패널에서 [등록 코드 만들기]를 눌러도 되고, 터미널을 쓰셔도 돼요.
141
141
 
142
142
  ```
143
- npx -y @modootoday/datalab-extension-mcp browsers add "회사 크롬"
144
- npx -y @modootoday/datalab-extension-mcp browsers list
145
- npx -y @modootoday/datalab-extension-mcp browsers revoke <handle>
143
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 browsers add "회사 크롬"
144
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 browsers list
145
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 browsers revoke <handle>
146
146
  ```
147
147
 
148
148
  `add` 를 실행하면 **연결 토큰을 붙여넣으라고 물어봐요** — 설치할 때와 같은 값이고, 패널의 `복사` 버튼으로 얻습니다. 그러면 코드가 **한 번만** 출력돼요. 커넥터는 그 코드를 저장하지 않고 지문만 남기므로 다시 보여 줄 수 없어요.
@@ -220,7 +220,7 @@ AI가 "그런 기능은 없다" 고 답하면, **"도구 목록을 확인해 보
220
220
  - **연결이 사라졌을 때** — 처음 실행했던 그 명령을 한 번 더 실행하면 안전하게 다시 등록돼요. (일부 AI 앱이 설정 파일을 정리하며 연결을 지우기도 해요.)
221
221
 
222
222
  ```
223
- npx -y @modootoday/datalab-extension-mcp@1.8.9 install
223
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 install
224
224
  ```
225
225
 
226
226
  - **`'npx'을(는) 내부 또는 외부 명령...`** — Node.js가 없는 거예요. 위 2단계의 "Node.js가 없다면" 한 줄 설치가 가장 간단해요.
@@ -250,7 +250,7 @@ AI가 "그런 기능은 없다" 고 답하면, **"도구 목록을 확인해 보
250
250
  1. 터미널에서 아래 명령을 실행해요(AI 앱에 등록된 설정을 지워 줘요).
251
251
 
252
252
  ```
253
- npx -y @modootoday/datalab-extension-mcp@1.8.9 uninstall
253
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 uninstall
254
254
  ```
255
255
 
256
256
  2. 확장 설정에서 **MCP 연결을 꺼요.** 패널이 브릿지라, 꺼 두면 어딘가 설정 줄이 남아 있어도 AI 앱이 아무것도 조회할 수 없어요.
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  DEFAULT_HOST,
3
3
  DEFAULT_PORT
4
- } from "./chunk-GCRJQLTV.js";
4
+ } from "./chunk-Q6SOGGKF.js";
5
5
  import {
6
6
  TOKEN_PROMPT_GUIDE,
7
7
  TOKEN_PROMPT_QUESTION,
8
8
  TOKEN_REQUIRED_NON_INTERACTIVE
9
- } from "./chunk-5S7RFXIU.js";
9
+ } from "./chunk-S6D7YVBB.js";
10
10
  import "./chunk-MLKGABMK.js";
11
11
 
12
12
  // src/browsers.ts
@@ -40,9 +40,9 @@ async function runBrowsers(argv, deps) {
40
40
  const send = deps.fetchImpl ?? globalThis.fetch;
41
41
  const rest = withoutFlag(argv, "--token");
42
42
  const action = rest[0] ?? "list";
43
- if (action !== "add" && action !== "list" && action !== "revoke") {
43
+ if (action !== "add" && action !== "list" && action !== "revoke" && action !== "repair") {
44
44
  deps.out(
45
- `usage: browsers [add <label>|list|revoke <handle>] [--token <t>]`
45
+ `usage: browsers [add <label>|list|revoke <handle>|repair] [--token <t>]`
46
46
  );
47
47
  return 2;
48
48
  }
@@ -93,12 +93,19 @@ async function runBrowsers(argv, deps) {
93
93
  }
94
94
  if (action === "list") {
95
95
  const rows = parsed["browsers"] ?? [];
96
+ const registryStatus = parsed["registryStatus"];
97
+ if (registryStatus?.state === "degraded") {
98
+ deps.out(
99
+ `Registry warning: ${String(registryStatus.error ?? "degraded")}.`
100
+ );
101
+ deps.out("Run: datalab-extension-mcp browsers repair");
102
+ }
96
103
  if (rows.length === 0) {
97
104
  deps.out("No browsers registered.");
98
105
  return 0;
99
106
  }
100
107
  for (const row of rows) {
101
- const state = row.browserId === null ? "waiting" : "connected";
108
+ const state = row.browserId === null ? "waiting" : row.connected === true ? "connected" : "detached";
102
109
  deps.out(`${row.handle} ${state.padEnd(9)} ${row.label}`);
103
110
  }
104
111
  return 0;
@@ -109,6 +116,13 @@ async function runBrowsers(argv, deps) {
109
116
  );
110
117
  return parsed["removed"] === true ? 0 : 1;
111
118
  }
119
+ if (action === "repair") {
120
+ const removed = Array.isArray(parsed["removed"]) ? parsed["removed"].length : 0;
121
+ deps.out(
122
+ removed > 0 ? `Repaired ${String(removed)} conflicting browser registration.` : "No conflicting browser registrations found."
123
+ );
124
+ return 0;
125
+ }
112
126
  deps.out(`Registered ${String(parsed["handle"])}.`);
113
127
  deps.out(`Paste this into the browser's panel \u2014 it is shown only now:`);
114
128
  deps.out(String(parsed["token"]));
@@ -4,7 +4,7 @@ import {
4
4
  mediaToolDescriptors,
5
5
  spawnDaemon,
6
6
  tryConnect
7
- } from "./chunk-GCRJQLTV.js";
7
+ } from "./chunk-Q6SOGGKF.js";
8
8
  import {
9
9
  __export
10
10
  } from "./chunk-MLKGABMK.js";
@@ -14583,6 +14583,14 @@ var HelloSchema = external_exports.object({
14583
14583
  */
14584
14584
  capabilities: external_exports.array(external_exports.string()).default([])
14585
14585
  });
14586
+ var BRIDGE_AUTH_CAPABILITY = "bridge-auth-bearer";
14587
+ var BROWSER_ROUTING_CAPABILITY = "browser-routing-v1";
14588
+ var LOCAL_FILE_REQUEST_CAPABILITY = "local-file-request-v1";
14589
+ var REQUIRED_BROWSER_CAPABILITIES = Object.freeze([
14590
+ BRIDGE_AUTH_CAPABILITY,
14591
+ BROWSER_ROUTING_CAPABILITY,
14592
+ LOCAL_FILE_REQUEST_CAPABILITY
14593
+ ]);
14586
14594
  var HelloAckSchema = external_exports.object({
14587
14595
  t: external_exports.literal("hello_ack"),
14588
14596
  /** The single version both sides will use for this session. */
@@ -14601,7 +14609,11 @@ var HelloNackReasonSchema = external_exports.enum([
14601
14609
  /** Origin header absent or not our extension. */
14602
14610
  "forbidden_origin",
14603
14611
  /** This browser already has an active event stream. */
14604
- "session_active"
14612
+ "session_active",
14613
+ /** Root authority cannot open a browser session. */
14614
+ "browser_registration_required",
14615
+ /** This panel predates mandatory browser routing and file permits. */
14616
+ "browser_capability_required"
14605
14617
  ]);
14606
14618
  var BrowserRoutingSchema = external_exports.object({
14607
14619
  targetBrowserId: external_exports.string(),
@@ -15031,6 +15043,7 @@ var REASON_POLICY = Object.freeze({
15031
15043
  // --- The request itself must change ---
15032
15044
  unknown_tool: Object.freeze({ retryable: false }),
15033
15045
  invalid_args: Object.freeze({ retryable: false }),
15046
+ browser_required: Object.freeze({ retryable: false }),
15034
15047
  tool_closed: Object.freeze({ retryable: false }),
15035
15048
  missing_in_build: Object.freeze({ retryable: false }),
15036
15049
  // --- A human must act; re-calling only nags them ---
@@ -15444,7 +15457,7 @@ var BRIDGE_USER_MESSAGES = Object.freeze({
15444
15457
  panelClosed: "\uD06C\uB86C\uC5D0\uC11C \uB370\uC774\uD130\uB7A9\uD234\uC988 \uD328\uB110\uC774 \uB2EB\uD600 \uC788\uC5B4\uC694. \uD06C\uB86C\uC744 \uC5F4\uACE0 \uB370\uC774\uD130\uB7A9\uD234\uC988 \uD328\uB110\uC744 \uC5F4\uC5B4 \uB450\uBA74 \uBC14\uB85C \uB3D9\uC791\uD574\uC694.",
15445
15458
  /** The panel accepted the call but never answered inside the deadline. */
15446
15459
  toolTimeout: "\uBE0C\uB77C\uC6B0\uC800 \uC751\uB2F5\uC774 \uB2A6\uC5B4\uC9C0\uACE0 \uC788\uC5B4\uC694. \uD06C\uB86C\uC5D0\uC11C \uB124\uC774\uBC84\uC5D0 \uB85C\uADF8\uC778\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778\uD558\uACE0 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694.",
15447
- /** The session hit the per-session rate limit. */
15460
+ /** The daemon request budget is empty. */
15448
15461
  rateLimited: "\uC694\uCCAD\uC774 \uB108\uBB34 \uC7A6\uC544\uC694. \uC7A0\uAE50 \uAE30\uB2E4\uB838\uB2E4\uAC00 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694.",
15449
15462
  /**
15450
15463
  * The local connector accepted nothing back inside its ceiling. Distinct
@@ -15456,7 +15469,7 @@ var BRIDGE_USER_MESSAGES = Object.freeze({
15456
15469
  var BRIDGE_AGENT_HINT = Object.freeze({
15457
15470
  /** Panel closed / not connected — the panel must be open for any tool to run. */
15458
15471
  notConnected: "The Datalab extension side panel is not open, so this tool cannot run right now. Ask the user to open the extension side panel in their browser, then call this tool again.",
15459
- /** Per-session rate limit tripped — transient. */
15472
+ /** Daemon-wide rate limit tripped — transient. */
15460
15473
  rateLimited: "This tool is temporarily rate-limited. Wait a few seconds, then call it again.",
15461
15474
  /** The local connector did not answer at all — retrying immediately will not help. */
15462
15475
  connectorStuck: "The local connector stopped responding, so this tool cannot run right now. Do not retry immediately; ask the user to turn the MCP connection off and on again in the extension side panel."
@@ -1,4 +1,4 @@
1
- // ../mcp-server/dist/chunk-TUYW5HMB.js
1
+ // ../mcp-server/dist/chunk-377F4BGN.js
2
2
  import { createHash } from "crypto";
3
3
  import { createHash as createHash2, randomUUID } from "crypto";
4
4
  import { mkdir, readFile, readdir, rm, writeFile } from "fs/promises";
@@ -24,8 +24,8 @@ import { spawn } from "child_process";
24
24
  import { closeSync } from "fs";
25
25
  import { randomBytes } from "crypto";
26
26
  import { readFileSync } from "fs";
27
- import { rename, writeFile as writeFile2 } from "fs/promises";
28
- import { join as join3 } from "path";
27
+ import { mkdir as mkdir2, rename, writeFile as writeFile2 } from "fs/promises";
28
+ import { dirname, join as join3 } from "path";
29
29
  import { createRequire } from "module";
30
30
  import { join as join4 } from "path";
31
31
  var __defProp = Object.defineProperty;
@@ -14439,6 +14439,11 @@ var HelloSchema = external_exports.object({
14439
14439
  var BRIDGE_AUTH_CAPABILITY = "bridge-auth-bearer";
14440
14440
  var BROWSER_ROUTING_CAPABILITY = "browser-routing-v1";
14441
14441
  var LOCAL_FILE_REQUEST_CAPABILITY = "local-file-request-v1";
14442
+ var REQUIRED_BROWSER_CAPABILITIES = Object.freeze([
14443
+ BRIDGE_AUTH_CAPABILITY,
14444
+ BROWSER_ROUTING_CAPABILITY,
14445
+ LOCAL_FILE_REQUEST_CAPABILITY
14446
+ ]);
14442
14447
  function bearerToken(header) {
14443
14448
  if (typeof header !== "string") return null;
14444
14449
  const match = /^Bearer (.+)$/i.exec(header.trim());
@@ -14462,7 +14467,11 @@ var HelloNackReasonSchema = external_exports.enum([
14462
14467
  /** Origin header absent or not our extension. */
14463
14468
  "forbidden_origin",
14464
14469
  /** This browser already has an active event stream. */
14465
- "session_active"
14470
+ "session_active",
14471
+ /** Root authority cannot open a browser session. */
14472
+ "browser_registration_required",
14473
+ /** This panel predates mandatory browser routing and file permits. */
14474
+ "browser_capability_required"
14466
14475
  ]);
14467
14476
  var BrowserRoutingSchema = external_exports.object({
14468
14477
  targetBrowserId: external_exports.string(),
@@ -14652,6 +14661,14 @@ var REFUSED = {
14652
14661
  message: "Pairing token missing or invalid."
14653
14662
  };
14654
14663
  function resolveCredential(presented, known) {
14664
+ const root = checkToken(presented.token, known.rootToken);
14665
+ if (root.ok) {
14666
+ return {
14667
+ kind: "refused",
14668
+ reason: "browser_registration_required",
14669
+ message: "The root token is management-only. Register this browser with its own token."
14670
+ };
14671
+ }
14655
14672
  const { browserId } = presented;
14656
14673
  if (browserId !== void 0 && browserId.length > 0) {
14657
14674
  const expected = known.claimed.get(browserId);
@@ -14662,8 +14679,7 @@ function resolveCredential(presented, known) {
14662
14679
  return REFUSED;
14663
14680
  }
14664
14681
  }
14665
- const root = checkToken(presented.token, known.rootToken);
14666
- return root.ok ? { kind: "root" } : REFUSED;
14682
+ return REFUSED;
14667
14683
  }
14668
14684
  var READ_ONLY_TOOLS = [
14669
14685
  "datalab_browsers",
@@ -15015,6 +15031,7 @@ var REASON_POLICY = Object.freeze({
15015
15031
  // --- The request itself must change ---
15016
15032
  unknown_tool: Object.freeze({ retryable: false }),
15017
15033
  invalid_args: Object.freeze({ retryable: false }),
15034
+ browser_required: Object.freeze({ retryable: false }),
15018
15035
  tool_closed: Object.freeze({ retryable: false }),
15019
15036
  missing_in_build: Object.freeze({ retryable: false }),
15020
15037
  // --- A human must act; re-calling only nags them ---
@@ -15483,7 +15500,7 @@ var BRIDGE_USER_MESSAGES = Object.freeze({
15483
15500
  panelClosed: "\uD06C\uB86C\uC5D0\uC11C \uB370\uC774\uD130\uB7A9\uD234\uC988 \uD328\uB110\uC774 \uB2EB\uD600 \uC788\uC5B4\uC694. \uD06C\uB86C\uC744 \uC5F4\uACE0 \uB370\uC774\uD130\uB7A9\uD234\uC988 \uD328\uB110\uC744 \uC5F4\uC5B4 \uB450\uBA74 \uBC14\uB85C \uB3D9\uC791\uD574\uC694.",
15484
15501
  /** The panel accepted the call but never answered inside the deadline. */
15485
15502
  toolTimeout: "\uBE0C\uB77C\uC6B0\uC800 \uC751\uB2F5\uC774 \uB2A6\uC5B4\uC9C0\uACE0 \uC788\uC5B4\uC694. \uD06C\uB86C\uC5D0\uC11C \uB124\uC774\uBC84\uC5D0 \uB85C\uADF8\uC778\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778\uD558\uACE0 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694.",
15486
- /** The session hit the per-session rate limit. */
15503
+ /** The daemon request budget is empty. */
15487
15504
  rateLimited: "\uC694\uCCAD\uC774 \uB108\uBB34 \uC7A6\uC544\uC694. \uC7A0\uAE50 \uAE30\uB2E4\uB838\uB2E4\uAC00 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694.",
15488
15505
  /**
15489
15506
  * The local connector accepted nothing back inside its ceiling. Distinct
@@ -15495,7 +15512,7 @@ var BRIDGE_USER_MESSAGES = Object.freeze({
15495
15512
  var BRIDGE_AGENT_HINT = Object.freeze({
15496
15513
  /** Panel closed / not connected — the panel must be open for any tool to run. */
15497
15514
  notConnected: "The Datalab extension side panel is not open, so this tool cannot run right now. Ask the user to open the extension side panel in their browser, then call this tool again.",
15498
- /** Per-session rate limit tripped — transient. */
15515
+ /** Daemon-wide rate limit tripped — transient. */
15499
15516
  rateLimited: "This tool is temporarily rate-limited. Wait a few seconds, then call it again.",
15500
15517
  /** The local connector did not answer at all — retrying immediately will not help. */
15501
15518
  connectorStuck: "The local connector stopped responding, so this tool cannot run right now. Do not retry immediately; ask the user to turn the MCP connection off and on again in the extension side panel."
@@ -15615,10 +15632,6 @@ var PendingRegistry = class {
15615
15632
  function digestToken(token) {
15616
15633
  return createHash("sha256").update(token, "utf8").digest("hex");
15617
15634
  }
15618
- var LEGACY_SESSION_KEY = "legacy";
15619
- function sessionKeyFor(credential) {
15620
- return credential.kind === "claimed" ? credential.browserId : LEGACY_SESSION_KEY;
15621
- }
15622
15635
  var DEFAULT_RATE_LIMIT = BRIDGE_LIMITS;
15623
15636
  function daemonIdTag() {
15624
15637
  return Math.floor(Math.random() * 16777215).toString(36).padStart(4, "0");
@@ -15629,7 +15642,6 @@ var Bridge = class {
15629
15642
  #idTag = daemonIdTag();
15630
15643
  #sessionSeq = 0;
15631
15644
  #defaultKey = null;
15632
- #retireSession = null;
15633
15645
  #bucketTokens = 0;
15634
15646
  #bucketRefilledAt = 0;
15635
15647
  // Last catalog the panel reported, kept across disconnects: many hosts cache
@@ -15638,12 +15650,9 @@ var Bridge = class {
15638
15650
  #cachedTools = [];
15639
15651
  constructor(deps) {
15640
15652
  this.#deps = deps;
15641
- }
15642
- bindRetireSession(retire) {
15643
- if (this.#retireSession !== null) {
15644
- throw new Error("bridge transport retirement is already bound");
15645
- }
15646
- this.#retireSession = retire;
15653
+ const limit = deps.rateLimit ?? DEFAULT_RATE_LIMIT;
15654
+ this.#bucketTokens = limit.capacity;
15655
+ this.#bucketRefilledAt = (deps.now ?? Date.now)();
15647
15656
  }
15648
15657
  /**
15649
15658
  * The session a request with no browser named goes to. Map preserves
@@ -15669,13 +15678,16 @@ var Bridge = class {
15669
15678
  */
15670
15679
  connectedBrowsers() {
15671
15680
  const keys = [...this.#sessions.values()].filter((entry) => entry.streamActive).map((entry) => entry.key);
15672
- const primary = this.#defaultKey === null ? this.#primary()?.key ?? null : this.#defaultKey;
15681
+ const primary = this.#primary()?.key ?? null;
15673
15682
  return keys.map((id) => ({
15674
15683
  id,
15675
15684
  name: this.#deps.browserLabels.get(id) ?? null,
15676
15685
  primary: primary === id
15677
15686
  }));
15678
15687
  }
15688
+ browserRegistryStatus() {
15689
+ return this.#deps.browserRegistryStatus?.() ?? { state: "ready" };
15690
+ }
15679
15691
  /**
15680
15692
  * The only place a registry is born, and it is born already in the map.
15681
15693
  * One living anywhere else would be missed by `sweep` and by `#closeSession`,
@@ -15699,7 +15711,6 @@ var Bridge = class {
15699
15711
  key,
15700
15712
  session,
15701
15713
  tokenHash: auth.tokenHash,
15702
- authRequired: auth.authRequired,
15703
15714
  routingRequired: auth.routingRequired,
15704
15715
  fileRequestBound: auth.fileRequestBound,
15705
15716
  streamActive: false,
@@ -15751,9 +15762,6 @@ var Bridge = class {
15751
15762
  sessionForKey(key) {
15752
15763
  return this.#sessions.get(key)?.session ?? null;
15753
15764
  }
15754
- legacySessionKey() {
15755
- return this.#sessions.has(LEGACY_SESSION_KEY) ? LEGACY_SESSION_KEY : null;
15756
- }
15757
15765
  /** Is a panel connected and ready to serve tool calls? */
15758
15766
  get connected() {
15759
15767
  return this.#primary() !== null;
@@ -15800,9 +15808,6 @@ var Bridge = class {
15800
15808
  if (!timingSafeEqual(hash2, entry.tokenHash) || this.#deps.wasRevoked(hash2)) {
15801
15809
  return null;
15802
15810
  }
15803
- if (entry.key === LEGACY_SESSION_KEY) {
15804
- return this.verifyRootToken(presented) ? "root" : null;
15805
- }
15806
15811
  const current = this.#deps.claimedBrowsers.get(entry.key);
15807
15812
  return current !== void 0 && timingSafeEqual(hash2, current) ? "claimed_browser" : null;
15808
15813
  }
@@ -15814,33 +15819,6 @@ var Bridge = class {
15814
15819
  }
15815
15820
  return "unknown";
15816
15821
  }
15817
- authorizeLegacyFileCredential(presented) {
15818
- if (typeof presented !== "string" || presented.length === 0) {
15819
- return {
15820
- ok: false,
15821
- reason: "file_session_token_missing",
15822
- credential: "missing"
15823
- };
15824
- }
15825
- const credential = this.#fileCredentialClass(presented);
15826
- for (const entry of this.#sessions.values()) {
15827
- const accepted = this.#fileCredential(entry, presented);
15828
- if (accepted && !entry.fileRequestBound) {
15829
- return {
15830
- ok: true,
15831
- key: entry.key,
15832
- sessionId: entry.session.sessionId,
15833
- credential: accepted,
15834
- requestBound: false
15835
- };
15836
- }
15837
- }
15838
- return {
15839
- ok: false,
15840
- reason: "file_session_token_mismatch",
15841
- credential
15842
- };
15843
- }
15844
15822
  authorizeFileRequest(input) {
15845
15823
  const entry = [...this.#sessions.values()].find(
15846
15824
  (candidate) => candidate.session.sessionId === input.sessionId
@@ -15859,7 +15837,7 @@ var Bridge = class {
15859
15837
  credential: "missing",
15860
15838
  key: entry.key,
15861
15839
  sessionId: entry.session.sessionId,
15862
- ...input.requestId === void 0 ? {} : { requestId: input.requestId }
15840
+ requestId: input.requestId
15863
15841
  };
15864
15842
  }
15865
15843
  const credential = this.#fileCredential(entry, input.token);
@@ -15870,32 +15848,23 @@ var Bridge = class {
15870
15848
  credential: this.#fileCredentialClass(input.token),
15871
15849
  key: entry.key,
15872
15850
  sessionId: entry.session.sessionId,
15873
- ...input.requestId === void 0 ? {} : { requestId: input.requestId }
15851
+ requestId: input.requestId
15874
15852
  };
15875
15853
  }
15876
15854
  if (!entry.fileRequestBound) {
15877
15855
  return {
15878
- ok: true,
15856
+ ok: false,
15857
+ reason: "file_request_expired",
15858
+ credential,
15879
15859
  key: entry.key,
15880
15860
  sessionId: entry.session.sessionId,
15881
- ...input.requestId === void 0 ? {} : { requestId: input.requestId },
15882
- credential,
15883
- requestBound: false
15861
+ requestId: input.requestId
15884
15862
  };
15885
15863
  }
15886
15864
  const now = (this.#deps.now ?? Date.now)();
15887
15865
  for (const [requestId, permit2] of entry.filePermits) {
15888
15866
  if (permit2.expiresAt <= now) entry.filePermits.delete(requestId);
15889
15867
  }
15890
- if (input.requestId === void 0) {
15891
- return {
15892
- ok: false,
15893
- reason: "file_request_expired",
15894
- credential,
15895
- key: entry.key,
15896
- sessionId: entry.session.sessionId
15897
- };
15898
- }
15899
15868
  const permit = entry.filePermits.get(input.requestId);
15900
15869
  if (!permit) {
15901
15870
  return {
@@ -15981,6 +15950,32 @@ var Bridge = class {
15981
15950
  message: idCheck.message
15982
15951
  };
15983
15952
  }
15953
+ const negotiated = negotiateProtocol(parsed.data.protocolVersions);
15954
+ if (!negotiated.ok) {
15955
+ return {
15956
+ t: "hello_nack",
15957
+ reason: negotiated.reason,
15958
+ supported: negotiated.supported,
15959
+ message: negotiated.message
15960
+ };
15961
+ }
15962
+ const bearerRequired = parsed.data.capabilities.includes(
15963
+ BRIDGE_AUTH_CAPABILITY
15964
+ );
15965
+ const routingRequired = parsed.data.capabilities.includes(
15966
+ BROWSER_ROUTING_CAPABILITY
15967
+ );
15968
+ const fileRequestBound = parsed.data.capabilities.includes(
15969
+ LOCAL_FILE_REQUEST_CAPABILITY
15970
+ );
15971
+ if (!bearerRequired || !routingRequired || !fileRequestBound) {
15972
+ return {
15973
+ t: "hello_nack",
15974
+ reason: "browser_capability_required",
15975
+ supported: [],
15976
+ message: "This browser panel is outdated. Close and reopen the side panel before reconnecting."
15977
+ };
15978
+ }
15984
15979
  const tokenHash = digestToken(parsed.data.token);
15985
15980
  const browserId = parsed.data.browserId;
15986
15981
  if (browserId !== void 0 && browserId.length > 0 && !this.#deps.claimedBrowsers.has(browserId)) {
@@ -16001,19 +15996,23 @@ var Bridge = class {
16001
15996
  message: credential.message
16002
15997
  };
16003
15998
  }
16004
- const negotiated = negotiateProtocol(parsed.data.protocolVersions);
16005
- if (!negotiated.ok) {
15999
+ if (credential.kind !== "claimed") {
16006
16000
  return {
16007
16001
  t: "hello_nack",
16008
- reason: negotiated.reason,
16009
- supported: negotiated.supported,
16010
- message: negotiated.message
16002
+ reason: "browser_registration_required",
16003
+ supported: [],
16004
+ message: "The root token is management-only. Register this browser with its own token."
16011
16005
  };
16012
16006
  }
16013
- const key = sessionKeyFor(credential);
16007
+ const key = credential.browserId;
16014
16008
  const held = this.#sessions.get(key);
16015
16009
  if (held?.streamActive) {
16016
- this.#retireSession?.(key, held.session.sessionId);
16010
+ return {
16011
+ t: "hello_nack",
16012
+ reason: "session_active",
16013
+ supported: [],
16014
+ message: "This browser already has an active side-panel session."
16015
+ };
16017
16016
  }
16018
16017
  const session = {
16019
16018
  // A counter, not a timestamp: a panel reload produces two handshakes
@@ -16023,34 +16022,17 @@ var Bridge = class {
16023
16022
  protocolVersion: negotiated.version,
16024
16023
  extensionVersion: parsed.data.extensionVersion
16025
16024
  };
16026
- const authRequired = parsed.data.capabilities.includes(
16027
- BRIDGE_AUTH_CAPABILITY
16028
- );
16029
- const routingRequired = credential.kind === "claimed" && parsed.data.capabilities.includes(BROWSER_ROUTING_CAPABILITY);
16030
- const fileRequestBound = parsed.data.capabilities.includes(
16031
- LOCAL_FILE_REQUEST_CAPABILITY
16032
- );
16033
16025
  this.#installSession(key, session, {
16034
16026
  tokenHash,
16035
- authRequired,
16036
16027
  routingRequired,
16037
16028
  fileRequestBound
16038
16029
  });
16039
- const limit = this.#deps.rateLimit ?? DEFAULT_RATE_LIMIT;
16040
- this.#bucketTokens = limit.capacity;
16041
- this.#bucketRefilledAt = (this.#deps.now ?? Date.now)();
16042
16030
  return {
16043
16031
  t: "hello_ack",
16044
16032
  protocolVersion: negotiated.version,
16045
16033
  serverVersion: this.#deps.serverVersion,
16046
16034
  sessionId: session.sessionId,
16047
- // Echoed only when it is actually on, so the panel can tell a server that
16048
- // is holding it to the header from one that predates the gate entirely.
16049
- capabilities: [
16050
- ...authRequired ? [BRIDGE_AUTH_CAPABILITY] : [],
16051
- ...routingRequired ? [BROWSER_ROUTING_CAPABILITY] : [],
16052
- ...fileRequestBound ? [LOCAL_FILE_REQUEST_CAPABILITY] : []
16053
- ]
16035
+ capabilities: [...REQUIRED_BROWSER_CAPABILITIES]
16054
16036
  };
16055
16037
  }
16056
16038
  /**
@@ -16068,15 +16050,13 @@ var Bridge = class {
16068
16050
  return this.#mayAct(entry, presented);
16069
16051
  }
16070
16052
  #mayAct(entry, presented) {
16071
- if (!entry.authRequired) return true;
16072
16053
  if (typeof presented !== "string" || presented.length === 0) return false;
16073
16054
  return timingSafeEqual(digestToken(presented), entry.tokenHash);
16074
16055
  }
16075
16056
  /**
16076
16057
  * Route a frame from the panel to whoever is waiting for it.
16077
16058
  *
16078
- * `presented` is the credential the poster carried. A session that asked to
16079
- * be held to it refuses anything else; one that did not is unaffected.
16059
+ * `presented` is the credential the poster carried.
16080
16060
  */
16081
16061
  receive(raw, presented) {
16082
16062
  const classified = classifyFrame(
@@ -16244,7 +16224,7 @@ var Bridge = class {
16244
16224
  };
16245
16225
  }
16246
16226
  /**
16247
- * Refill-then-take on the session's token bucket. The refill is computed
16227
+ * Refill-then-take on the daemon's token bucket. The refill is computed
16248
16228
  * lazily on each take, so there is no timer to leak and an injected clock
16249
16229
  * keeps it testable without waiting real seconds.
16250
16230
  */
@@ -16268,7 +16248,7 @@ var Bridge = class {
16268
16248
  if (browser === void 0) {
16269
16249
  entry = this.#primary();
16270
16250
  } else {
16271
- const known = this.#deps.claimedBrowsers.has(browser) || browser === LEGACY_SESSION_KEY && this.#sessions.has(browser);
16251
+ const known = this.#deps.claimedBrowsers.has(browser);
16272
16252
  if (!known) {
16273
16253
  return {
16274
16254
  ok: false,
@@ -16857,14 +16837,22 @@ async function handleMcpRequest(bridge, body, accept, stage) {
16857
16837
  delete args["browser"];
16858
16838
  }
16859
16839
  if (params.name === BROWSERS_TOOL) {
16840
+ const registryStatus = bridge.browserRegistryStatus();
16841
+ const { state: registryState, ...registryDetail } = registryStatus;
16842
+ const payload = {
16843
+ browsers: bridge.connectedBrowsers(),
16844
+ registryStatus: registryState,
16845
+ ...registryState === "ready" ? {} : registryDetail
16846
+ };
16860
16847
  return frameMcpResponse(
16861
16848
  rpcResult(id, {
16862
16849
  content: [
16863
16850
  {
16864
16851
  type: "text",
16865
- text: JSON.stringify({ browsers: bridge.connectedBrowsers() })
16852
+ text: JSON.stringify(payload)
16866
16853
  }
16867
- ]
16854
+ ],
16855
+ ...registryState === "disabled" ? { isError: true } : {}
16868
16856
  }),
16869
16857
  accept
16870
16858
  );
@@ -17164,13 +17152,6 @@ function createHttpBridge(opts) {
17164
17152
  opts.bridge.disconnect(key);
17165
17153
  if (wasPrimary) scheduleToolsDrop();
17166
17154
  };
17167
- opts.bridge.bindRetireSession((key, sessionId) => {
17168
- const stream = streams.get(key);
17169
- if (!stream || stream.sessionId !== sessionId) return;
17170
- const wasPrimary = key === opts.bridge.sessionKey;
17171
- removeStream(key)?.end();
17172
- if (wasPrimary) scheduleToolsDrop();
17173
- });
17174
17155
  const server = createServer((req, res) => {
17175
17156
  const url2 = new URL(req.url ?? "/", `http://${host}`);
17176
17157
  const origin = req.headers.origin;
@@ -17240,7 +17221,11 @@ function createHttpBridge(opts) {
17240
17221
  }
17241
17222
  if (req.method === "GET" && url2.pathname === "/bridge/events") {
17242
17223
  const claimed = url2.searchParams.get("session");
17243
- const key = claimed === null ? opts.bridge.legacySessionKey() : opts.bridge.keyForSession(claimed);
17224
+ if (claimed === null || claimed.length === 0) {
17225
+ json2(res, 400, { error: "session_required" });
17226
+ return;
17227
+ }
17228
+ const key = claimed === null ? null : opts.bridge.keyForSession(claimed);
17244
17229
  if (key === null) {
17245
17230
  opts.log?.("refused a stream for an unknown session");
17246
17231
  res.writeHead(200, { "content-type": "text/event-stream" });
@@ -17267,13 +17252,8 @@ function createHttpBridge(opts) {
17267
17252
  // by nature.
17268
17253
  "x-accel-buffering": "no"
17269
17254
  });
17270
- const sessionId = claimed ?? opts.bridge.sessionForKey(key)?.sessionId ?? "";
17271
- if (!sessionId) {
17272
- res.end();
17273
- return;
17274
- }
17275
17255
  try {
17276
- installStream(key, sessionId, res);
17256
+ installStream(key, claimed, res);
17277
17257
  } catch {
17278
17258
  res.end();
17279
17259
  return;
@@ -17314,7 +17294,18 @@ function createHttpBridge(opts) {
17314
17294
  }
17315
17295
  const action = b?.["action"];
17316
17296
  if (action === "list") {
17317
- json2(res, 200, { ok: true, browsers: await registry2.list() });
17297
+ const active = new Set(
17298
+ opts.bridge.connectedBrowsers().map((row) => row.id)
17299
+ );
17300
+ const browsers = (await registry2.list()).map((row) => ({
17301
+ ...row,
17302
+ connected: row.browserId !== null && active.has(row.browserId)
17303
+ }));
17304
+ json2(res, 200, {
17305
+ ok: true,
17306
+ browsers,
17307
+ registryStatus: registry2.status()
17308
+ });
17318
17309
  return;
17319
17310
  }
17320
17311
  if (action === "add") {
@@ -17330,6 +17321,10 @@ function createHttpBridge(opts) {
17330
17321
  });
17331
17322
  return;
17332
17323
  }
17324
+ if (action === "repair") {
17325
+ json2(res, 200, { ok: true, ...await registry2.repair() });
17326
+ return;
17327
+ }
17333
17328
  json2(res, 400, { error: "unknown action" });
17334
17329
  }).catch((err) => {
17335
17330
  const why = err instanceof Error ? err.message : String(err);
@@ -17347,20 +17342,20 @@ function createHttpBridge(opts) {
17347
17342
  const hasRequestId = b !== null && Object.hasOwn(b, "requestId");
17348
17343
  const invalidSessionId = hasSessionId && (typeof b?.sessionId !== "string" || b.sessionId.length === 0);
17349
17344
  const invalidRequestId = hasRequestId && (typeof b?.requestId !== "string" || b.requestId.length === 0);
17350
- if (invalidSessionId || invalidRequestId) {
17345
+ if (!hasSessionId || !hasRequestId || invalidSessionId || invalidRequestId) {
17351
17346
  json2(res, 403, {
17352
17347
  error: "unauthorized",
17353
17348
  message: "\uC0AC\uC9C4 \uC694\uCCAD\uC758 \uC5F0\uACB0 \uC815\uBCF4\uB97C \uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC5B4\uC694."
17354
17349
  });
17355
17350
  return;
17356
17351
  }
17357
- const auth = hasSessionId ? opts.bridge.authorizeFileRequest({
17352
+ const auth = opts.bridge.authorizeFileRequest({
17358
17353
  sessionId: b.sessionId,
17359
- ...hasRequestId ? { requestId: b.requestId } : {},
17354
+ requestId: b.requestId,
17360
17355
  token,
17361
17356
  path,
17362
17357
  ...typeof b?.tool === "string" ? { tool: b.tool } : {}
17363
- }) : opts.bridge.authorizeLegacyFileCredential(token);
17358
+ });
17364
17359
  if (!auth.ok) {
17365
17360
  opts.log?.(
17366
17361
  `file request refused: reason=${auth.reason} credential=${auth.credential} tokenPresent=${String(token.length > 0)} sessionMatch=${String(auth.key !== void 0)} open=not-entered`
@@ -17383,7 +17378,7 @@ function createHttpBridge(opts) {
17383
17378
  return;
17384
17379
  }
17385
17380
  opts.log?.(
17386
- `file request authorised: browser=${correlationValue(auth.key, 8)} session=${correlationValue(auth.sessionId, 32)} request=${auth.requestId ? correlationValue(auth.requestId, 96) : "legacy"} credential=${auth.credential} registryMatch=${String(auth.credential === "claimed_browser")} open=entered`
17381
+ `file request authorised: browser=${correlationValue(auth.key, 8)} session=${correlationValue(auth.sessionId, 32)} request=${correlationValue(auth.requestId, 96)} credential=${auth.credential} registryMatch=${String(auth.credential === "claimed_browser")} open=entered`
17387
17382
  );
17388
17383
  let source = null;
17389
17384
  let owner = null;
@@ -17842,6 +17837,7 @@ async function writeLedger(path, claims) {
17842
17837
  var writing = /* @__PURE__ */ new Map();
17843
17838
  async function writeOnce(path, claims) {
17844
17839
  const tmp = `${path}.${process.pid}.tmp`;
17840
+ await mkdir2(dirname(path), { recursive: true, mode: 448 });
17845
17841
  await writeFile2(tmp, `${JSON.stringify(claims, null, 2)}
17846
17842
  `, {
17847
17843
  mode: 384
@@ -17873,16 +17869,53 @@ function matches(claim, handle) {
17873
17869
  function createBrowserRegistry(opts) {
17874
17870
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
17875
17871
  const revoked = /* @__PURE__ */ new Set();
17876
- const current = () => {
17877
- const read = readLedger(opts.path);
17878
- if (read.state === "unreadable") {
17879
- throw new Error(`browser registry is unreadable: ${read.reason}`);
17880
- }
17881
- const rows = read.state === "ok" ? [...read.rows] : [];
17882
- if (opts.forbiddenTokenHash && ledgerOverlapsToken(rows, opts.forbiddenTokenHash)) {
17883
- throw new Error("browser registry overlaps the root authority");
17872
+ const initial = readLedger(opts.path);
17873
+ const unreadable = initial.state === "unreadable" ? initial.reason : null;
17874
+ let rows = initial.state === "ok" ? initial.rows.map((row) => ({ ...row })) : [];
17875
+ let mutationTail = Promise.resolve();
17876
+ let pendingMutations = 0;
17877
+ const view = (source = rows) => {
17878
+ if (unreadable !== null) {
17879
+ throw new Error(`browser registry is unreadable: ${unreadable}`);
17880
+ }
17881
+ if (!opts.forbiddenTokenHash) return { rows: [...source], quarantined: [] };
17882
+ const quarantined = source.filter(
17883
+ (row) => timingSafeEqual(row.tokenHash, opts.forbiddenTokenHash)
17884
+ );
17885
+ return {
17886
+ rows: source.filter(
17887
+ (row) => !timingSafeEqual(row.tokenHash, opts.forbiddenTokenHash)
17888
+ ),
17889
+ quarantined
17890
+ };
17891
+ };
17892
+ const current = () => view().rows;
17893
+ const serialize = (work) => {
17894
+ pendingMutations += 1;
17895
+ const result = mutationTail.then(work, work);
17896
+ mutationTail = result.then(
17897
+ () => {
17898
+ pendingMutations -= 1;
17899
+ },
17900
+ () => {
17901
+ pendingMutations -= 1;
17902
+ }
17903
+ );
17904
+ return result;
17905
+ };
17906
+ const transact = (propose) => {
17907
+ if (unreadable !== null) {
17908
+ return Promise.reject(
17909
+ new Error(`browser registry is unreadable: ${unreadable}`)
17910
+ );
17884
17911
  }
17885
- return rows;
17912
+ return serialize(async () => {
17913
+ const proposal = propose(rows.map((row) => ({ ...row })));
17914
+ await writeLedger(opts.path, proposal.next);
17915
+ rows = proposal.next;
17916
+ proposal.commit?.();
17917
+ return proposal.result;
17918
+ });
17886
17919
  };
17887
17920
  return {
17888
17921
  async add(label) {
@@ -17893,8 +17926,10 @@ function createBrowserRegistry(opts) {
17893
17926
  label,
17894
17927
  createdAt: now().toISOString()
17895
17928
  };
17896
- await writeLedger(opts.path, [...current(), claim]);
17897
- return { handle: handleOf(claim), token };
17929
+ return transact((snapshot) => ({
17930
+ next: [...snapshot, claim],
17931
+ result: { handle: handleOf(claim), token }
17932
+ }));
17898
17933
  },
17899
17934
  async list() {
17900
17935
  return current().map((claim) => ({
@@ -17905,6 +17940,7 @@ function createBrowserRegistry(opts) {
17905
17940
  }));
17906
17941
  },
17907
17942
  claim(browserId, tokenHash) {
17943
+ if (pendingMutations > 0) return false;
17908
17944
  if (opts.claimed.has(browserId)) return false;
17909
17945
  for (const held of opts.claimed.values()) {
17910
17946
  if (timingSafeEqual(held, tokenHash)) return false;
@@ -17919,26 +17955,81 @@ function createBrowserRegistry(opts) {
17919
17955
  (claim) => claim.browserId === null && timingSafeEqual(claim.tokenHash, tokenHash)
17920
17956
  );
17921
17957
  if (!row) return false;
17922
- row.browserId = browserId;
17958
+ const stored = rows.find((claim) => claim.tokenHash === row.tokenHash);
17959
+ if (!stored) return false;
17960
+ stored.browserId = browserId;
17923
17961
  opts.claimed.set(browserId, row.tokenHash);
17924
- void writeLedger(opts.path, claims).catch(() => {
17925
- });
17962
+ opts.onClaim?.(browserId, stored.label);
17963
+ const snapshot = rows.map((entry) => ({ ...entry }));
17964
+ void serialize(async () => {
17965
+ try {
17966
+ await writeLedger(opts.path, snapshot);
17967
+ } catch (error51) {
17968
+ if (stored.browserId === browserId) stored.browserId = null;
17969
+ if (opts.claimed.get(browserId) === row.tokenHash) {
17970
+ opts.claimed.delete(browserId);
17971
+ opts.onRevoke?.(browserId);
17972
+ }
17973
+ throw error51;
17974
+ }
17975
+ }).catch(() => void 0);
17926
17976
  return true;
17927
17977
  },
17928
17978
  async revoke(handle) {
17929
- const claims = current();
17930
- const kept = claims.filter((claim) => !matches(claim, handle));
17931
- if (kept.length === claims.length) return false;
17932
- await writeLedger(opts.path, kept);
17933
- for (const claim of claims) {
17934
- if (!matches(claim, handle)) continue;
17935
- revoked.add(claim.tokenHash);
17936
- if (claim.browserId !== null) {
17937
- opts.claimed.delete(claim.browserId);
17938
- opts.onRevoke?.(claim.browserId);
17979
+ return transact((snapshot) => {
17980
+ const claims = view(snapshot).rows;
17981
+ const removed = claims.filter((claim) => matches(claim, handle));
17982
+ if (removed.length === 0) {
17983
+ return { next: snapshot, result: false };
17939
17984
  }
17985
+ const removedHashes = new Set(removed.map((claim) => claim.tokenHash));
17986
+ return {
17987
+ next: snapshot.filter((claim) => !removedHashes.has(claim.tokenHash)),
17988
+ result: true,
17989
+ commit: () => {
17990
+ for (const claim of removed) {
17991
+ revoked.add(claim.tokenHash);
17992
+ if (claim.browserId !== null) {
17993
+ opts.claimed.delete(claim.browserId);
17994
+ opts.onRevoke?.(claim.browserId);
17995
+ }
17996
+ }
17997
+ }
17998
+ };
17999
+ });
18000
+ },
18001
+ async repair() {
18002
+ return transact((snapshot) => {
18003
+ const currentView = view(snapshot);
18004
+ const removed = currentView.quarantined.map(handleOf);
18005
+ const removedHashes = new Set(
18006
+ currentView.quarantined.map((row) => row.tokenHash)
18007
+ );
18008
+ return {
18009
+ next: snapshot.filter((row) => !removedHashes.has(row.tokenHash)),
18010
+ result: { removed }
18011
+ };
18012
+ });
18013
+ },
18014
+ status() {
18015
+ if (unreadable !== null) {
18016
+ return {
18017
+ state: "disabled",
18018
+ error: "browser_registry_unreadable",
18019
+ detail: unreadable
18020
+ };
17940
18021
  }
17941
- return true;
18022
+ if (opts.forbiddenTokenHash && ledgerOverlapsToken(rows, opts.forbiddenTokenHash)) {
18023
+ return {
18024
+ state: "degraded",
18025
+ error: "browser_registry_root_authority_overlap",
18026
+ quarantinedHandles: rows.filter(
18027
+ (row) => timingSafeEqual(row.tokenHash, opts.forbiddenTokenHash)
18028
+ ).map(handleOf),
18029
+ recovery: "browsers repair"
18030
+ };
18031
+ }
18032
+ return { state: "ready" };
17942
18033
  },
17943
18034
  wasRevoked(tokenHash) {
17944
18035
  return revoked.has(tokenHash);
@@ -17972,12 +18063,12 @@ function runDaemon(env, deps = {}) {
17972
18063
  const browserLabels = /* @__PURE__ */ new Map();
17973
18064
  const ledger = readLedger(ledgerPath(deps.home));
17974
18065
  const rootTokenHash = digestToken(token);
17975
- const ledgerAuthorityValid = ledger.state !== "ok" || !ledgerOverlapsToken(ledger.rows, rootTokenHash);
17976
- if (ledger.state === "ok" && ledgerAuthorityValid) {
17977
- for (const [id, hash2] of claimedDigestsFrom(ledger.rows)) {
18066
+ const safeLedgerRows = ledger.state === "ok" ? ledger.rows.filter((row) => row.tokenHash !== rootTokenHash) : [];
18067
+ if (ledger.state === "ok") {
18068
+ for (const [id, hash2] of claimedDigestsFrom(safeLedgerRows)) {
17978
18069
  claimedBrowsers.set(id, hash2);
17979
18070
  }
17980
- for (const row of ledger.rows) {
18071
+ for (const row of safeLedgerRows) {
17981
18072
  if (row.browserId !== null) browserLabels.set(row.browserId, row.label);
17982
18073
  }
17983
18074
  }
@@ -17986,8 +18077,10 @@ function runDaemon(env, deps = {}) {
17986
18077
  `browser registry is unreadable (${ledger.reason}); registering is off`
17987
18078
  );
17988
18079
  }
17989
- if (!ledgerAuthorityValid) {
17990
- log("browser registry overlaps the root authority; registering is off");
18080
+ if (ledger.state === "ok" && ledgerOverlapsToken(ledger.rows, rootTokenHash)) {
18081
+ log(
18082
+ "browser registry overlaps the root authority; conflicting rows are quarantined"
18083
+ );
17991
18084
  }
17992
18085
  const bridge = new Bridge({
17993
18086
  send: (frame, key) => http.send(frame, key),
@@ -17995,24 +18088,11 @@ function runDaemon(env, deps = {}) {
17995
18088
  extensionIds,
17996
18089
  serverVersion: version2,
17997
18090
  rateLimit: deps.rateLimit,
17998
- // Empty until a browser is registered, and nothing issues registrations
17999
- // yet — so every panel still shares the one slot it always had.
18091
+ // Empty until a browser is registered.
18000
18092
  claimedBrowsers,
18001
18093
  browserLabels,
18002
- claimBrowser: (browserId, tokenHash) => {
18003
- const took = registry2.claim(browserId, tokenHash);
18004
- if (took) {
18005
- const now = readLedger(ledgerPath(deps.home));
18006
- if (now.state === "ok") {
18007
- for (const row of now.rows) {
18008
- if (row.browserId === browserId) {
18009
- browserLabels.set(browserId, row.label);
18010
- }
18011
- }
18012
- }
18013
- }
18014
- return took;
18015
- },
18094
+ browserRegistryStatus: () => registry2.status(),
18095
+ claimBrowser: (browserId, tokenHash) => registry2.claim(browserId, tokenHash),
18016
18096
  wasRevoked: (tokenHash) => registry2.wasRevoked(tokenHash),
18017
18097
  log
18018
18098
  });
@@ -18021,6 +18101,7 @@ function runDaemon(env, deps = {}) {
18021
18101
  const registry2 = createBrowserRegistry({
18022
18102
  path: ledgerPath(deps.home),
18023
18103
  claimed: claimedBrowsers,
18104
+ onClaim: (browserId, label) => browserLabels.set(browserId, label),
18024
18105
  forbiddenTokenHash: rootTokenHash,
18025
18106
  onRevoke: (browserId) => http.disconnect(browserId)
18026
18107
  });
@@ -1109,7 +1109,7 @@ async function removeSkills(io, detectedIds, slugs) {
1109
1109
  var RESTART_NOTICE = "\uB9C8\uC9C0\uB9C9 \uD55C \uB2E8\uACC4: AI \uC571\uC744 \uC644\uC804\uD788 \uC885\uB8CC\uD588\uB2E4\uAC00 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC138\uC694.\n(Windows: \uC791\uC5C5 \uD45C\uC2DC\uC904 \uD2B8\uB808\uC774 \uC544\uC774\uCF58\uC5D0\uC11C \uC885\uB8CC)";
1110
1110
  var NOTHING_CHANGED = "\uC544\uBB34\uAC83\uB3C4 \uBC14\uAFB8\uC9C0 \uC54A\uC558\uC5B4\uC694.";
1111
1111
  var UNINSTALL_DONE = "\uC815\uB9AC\uAC00 \uB05D\uB0AC\uC5B4\uC694.";
1112
- var UNINSTALL_TOKEN_REMINDER = "\uC5F0\uACB0 \uD1A0\uD070\uAE4C\uC9C0 \uC5C6\uC560\uB824\uBA74 \uB370\uC774\uD130\uB7A9\uD234\uC988 \uD328\uB110\uC5D0\uC11C '\uC5F0\uB3D9 \uD574\uC81C' \uBC84\uD2BC\uC744 \uB20C\uB7EC \uC8FC\uC138\uC694. \uD3D0\uAE30\uB41C \uD1A0\uD070\uC740 \uC124\uC815\uC5D0 \uB0A8\uC740 \uD56D\uBAA9\uC744 \uC804\uBD80 \uBB34\uB825\uD654\uD574\uC694.";
1112
+ var UNINSTALL_TOKEN_REMINDER = "\uC774 \uBE0C\uB77C\uC6B0\uC800\uC758 \uB85C\uCEEC \uC5F0\uACB0 \uC815\uBCF4\uB3C4 \uC9C0\uC6B0\uB824\uBA74 \uB370\uC774\uD130\uB7A9\uD234\uC988 \uD328\uB110\uC5D0\uC11C '\uC774 \uBE0C\uB77C\uC6B0\uC800 \uC5F0\uACB0 \uB044\uAE30'\uB97C \uB20C\uB7EC \uC8FC\uC138\uC694. \uC6CC\uD06C\uBCA4\uCE58 \uB4F1\uB85D\uC740 \uBE0C\uB77C\uC6B0\uC800 \uBAA9\uB85D\uC5D0\uC11C \uBCC4\uB3C4\uB85C \uB0B4\uB9B4 \uC218 \uC788\uC5B4\uC694.";
1113
1113
  var PERMISSION_DENIED_HINT = "\uD30C\uC77C \uAD8C\uD55C \uB54C\uBB38\uC5D0 \uC124\uC815\uC744 \uC4F8 \uC218 \uC5C6\uC5C8\uC5B4\uC694. \uD30C\uC77C \uAD8C\uD55C\uC744 \uD655\uC778\uD55C \uB4A4 \uAC19\uC740 \uBA85\uB839\uC5B4\uB97C \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC138\uC694.";
1114
1114
  var NO_HOSTS_DETECTED = "\uC5F0\uACB0\uD560 \uC218 \uC788\uB294 AI \uD504\uB85C\uADF8\uB7A8\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC5B4\uC694.";
1115
1115
  var SUPPORTED_APPS_HEADER = "\uC9C0\uC6D0\uD558\uB294 \uD504\uB85C\uADF8\uB7A8\uACFC \uB2E4\uC6B4\uB85C\uB4DC \uC8FC\uC18C\uC608\uC694:";
package/dist/cli.js CHANGED
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  dispatchCli,
4
4
  runAdapter
5
- } from "./chunk-WBMZLPXC.js";
5
+ } from "./chunk-65FDP6LF.js";
6
6
  import {
7
7
  runDaemon
8
- } from "./chunk-GCRJQLTV.js";
8
+ } from "./chunk-Q6SOGGKF.js";
9
9
  import "./chunk-MLKGABMK.js";
10
10
 
11
11
  // src/cli.ts
@@ -17,7 +17,7 @@ function log(message) {
17
17
  `);
18
18
  }
19
19
  async function runInstaller(sub, argv) {
20
- const { createNodeIo, runInstall, runUninstall } = await import("./dist-JICXADYR.js");
20
+ const { createNodeIo, runInstall, runUninstall } = await import("./dist-63JYX33Z.js");
21
21
  const opts = { version: VERSION };
22
22
  for (let i = 0; i < argv.length; i += 1) {
23
23
  const flag = argv[i];
@@ -62,8 +62,8 @@ async function main() {
62
62
  // Loaded lazily, like the installer: the adapter path is what an MCP host
63
63
  // spawns and must not pay for a command only an operator types.
64
64
  browsers: async (argv) => {
65
- const { runBrowsers } = await import("./browsers-DRGNGPSI.js");
66
- const { createNodeIo } = await import("./dist-JICXADYR.js");
65
+ const { runBrowsers } = await import("./browsers-EMS5V4M4.js");
66
+ const { createNodeIo } = await import("./dist-63JYX33Z.js");
67
67
  const io = createNodeIo();
68
68
  const code = await runBrowsers(argv, {
69
69
  env: process.env,
@@ -88,7 +88,7 @@ async function main() {
88
88
  attachSkill,
89
89
  detachSkill,
90
90
  skillsPayloadRoot
91
- } = await import("./dist-JICXADYR.js");
91
+ } = await import("./dist-63JYX33Z.js");
92
92
  const io = createNodeIo();
93
93
  const root = skillsPayloadRoot();
94
94
  const code = await runSkills(argv, {
@@ -109,7 +109,7 @@ async function main() {
109
109
  unknown: (sub) => {
110
110
  log(`unknown command "${sub}".`);
111
111
  log(`usage: ${NAME} [install|uninstall|serve|browsers|skills]`);
112
- log(` browsers [add <label>|list|revoke <handle>] [--token <t>]`);
112
+ log(` browsers [add <label>|list|revoke <handle>|repair] [--token <t>]`);
113
113
  log(` skills [list|status|attach <slug>|detach <slug>]`);
114
114
  log(`run with no command to serve an MCP host over stdio.`);
115
115
  process.exit(2);
@@ -49,7 +49,7 @@ import {
49
49
  upsertServerKey,
50
50
  validateInstallOptions,
51
51
  validateUninstallOptions
52
- } from "./chunk-5S7RFXIU.js";
52
+ } from "./chunk-S6D7YVBB.js";
53
53
  import "./chunk-MLKGABMK.js";
54
54
  export {
55
55
  BACKUP_KEEP,
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ import {
3
3
  dispatchCli,
4
4
  ensureDaemonRunning,
5
5
  runAdapter
6
- } from "./chunk-WBMZLPXC.js";
7
- import "./chunk-GCRJQLTV.js";
6
+ } from "./chunk-65FDP6LF.js";
7
+ import "./chunk-Q6SOGGKF.js";
8
8
  import "./chunk-MLKGABMK.js";
9
9
  export {
10
10
  createAdapterServer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modootoday/datalab-extension-mcp",
3
- "version": "1.8.9",
3
+ "version": "1.8.11",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",