@modootoday/datalab-extension-mcp 1.8.9 → 1.8.10

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.10` 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.10 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.
@@ -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.10 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.10 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.10`은 커넥터가 나도 모르게 바뀌지 않도록 고정한 버전이에요.)
53
53
 
54
54
  ```
55
- npx -y @modootoday/datalab-extension-mcp@1.8.9 install
55
+ npx -y @modootoday/datalab-extension-mcp@1.8.10 install
56
56
  ```
57
57
 
58
58
  영어 글자가 여러 줄 올라오는 건 정상이에요. 이 명령은 커넥터를 내려받아 실행할 뿐, 다른 건 건드리지 않아요.
@@ -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.10 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.10 uninstall
254
254
  ```
255
255
 
256
256
  2. 확장 설정에서 **MCP 연결을 꺼요.** 패널이 브릿지라, 꺼 두면 어딘가 설정 줄이 남아 있어도 AI 앱이 아무것도 조회할 수 없어요.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DEFAULT_HOST,
3
3
  DEFAULT_PORT
4
- } from "./chunk-GCRJQLTV.js";
4
+ } from "./chunk-DOPJ4OZ4.js";
5
5
  import {
6
6
  TOKEN_PROMPT_GUIDE,
7
7
  TOKEN_PROMPT_QUESTION,
@@ -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,6 +93,13 @@ 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;
@@ -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"]));
@@ -1,4 +1,4 @@
1
- // ../mcp-server/dist/chunk-TUYW5HMB.js
1
+ // ../mcp-server/dist/chunk-Q4ALBPO7.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;
@@ -14462,7 +14462,9 @@ var HelloNackReasonSchema = external_exports.enum([
14462
14462
  /** Origin header absent or not our extension. */
14463
14463
  "forbidden_origin",
14464
14464
  /** This browser already has an active event stream. */
14465
- "session_active"
14465
+ "session_active",
14466
+ /** Root authority cannot open a browser session. */
14467
+ "browser_registration_required"
14466
14468
  ]);
14467
14469
  var BrowserRoutingSchema = external_exports.object({
14468
14470
  targetBrowserId: external_exports.string(),
@@ -14652,6 +14654,14 @@ var REFUSED = {
14652
14654
  message: "Pairing token missing or invalid."
14653
14655
  };
14654
14656
  function resolveCredential(presented, known) {
14657
+ const root = checkToken(presented.token, known.rootToken);
14658
+ if (root.ok) {
14659
+ return {
14660
+ kind: "refused",
14661
+ reason: "browser_registration_required",
14662
+ message: "The root token is management-only. Register this browser with its own token."
14663
+ };
14664
+ }
14655
14665
  const { browserId } = presented;
14656
14666
  if (browserId !== void 0 && browserId.length > 0) {
14657
14667
  const expected = known.claimed.get(browserId);
@@ -14662,8 +14672,7 @@ function resolveCredential(presented, known) {
14662
14672
  return REFUSED;
14663
14673
  }
14664
14674
  }
14665
- const root = checkToken(presented.token, known.rootToken);
14666
- return root.ok ? { kind: "root" } : REFUSED;
14675
+ return REFUSED;
14667
14676
  }
14668
14677
  var READ_ONLY_TOOLS = [
14669
14678
  "datalab_browsers",
@@ -15615,10 +15624,6 @@ var PendingRegistry = class {
15615
15624
  function digestToken(token) {
15616
15625
  return createHash("sha256").update(token, "utf8").digest("hex");
15617
15626
  }
15618
- var LEGACY_SESSION_KEY = "legacy";
15619
- function sessionKeyFor(credential) {
15620
- return credential.kind === "claimed" ? credential.browserId : LEGACY_SESSION_KEY;
15621
- }
15622
15627
  var DEFAULT_RATE_LIMIT = BRIDGE_LIMITS;
15623
15628
  function daemonIdTag() {
15624
15629
  return Math.floor(Math.random() * 16777215).toString(36).padStart(4, "0");
@@ -15676,6 +15681,9 @@ var Bridge = class {
15676
15681
  primary: primary === id
15677
15682
  }));
15678
15683
  }
15684
+ browserRegistryStatus() {
15685
+ return this.#deps.browserRegistryStatus?.() ?? { state: "ready" };
15686
+ }
15679
15687
  /**
15680
15688
  * The only place a registry is born, and it is born already in the map.
15681
15689
  * One living anywhere else would be missed by `sweep` and by `#closeSession`,
@@ -15699,7 +15707,6 @@ var Bridge = class {
15699
15707
  key,
15700
15708
  session,
15701
15709
  tokenHash: auth.tokenHash,
15702
- authRequired: auth.authRequired,
15703
15710
  routingRequired: auth.routingRequired,
15704
15711
  fileRequestBound: auth.fileRequestBound,
15705
15712
  streamActive: false,
@@ -15751,9 +15758,6 @@ var Bridge = class {
15751
15758
  sessionForKey(key) {
15752
15759
  return this.#sessions.get(key)?.session ?? null;
15753
15760
  }
15754
- legacySessionKey() {
15755
- return this.#sessions.has(LEGACY_SESSION_KEY) ? LEGACY_SESSION_KEY : null;
15756
- }
15757
15761
  /** Is a panel connected and ready to serve tool calls? */
15758
15762
  get connected() {
15759
15763
  return this.#primary() !== null;
@@ -15800,9 +15804,6 @@ var Bridge = class {
15800
15804
  if (!timingSafeEqual(hash2, entry.tokenHash) || this.#deps.wasRevoked(hash2)) {
15801
15805
  return null;
15802
15806
  }
15803
- if (entry.key === LEGACY_SESSION_KEY) {
15804
- return this.verifyRootToken(presented) ? "root" : null;
15805
- }
15806
15807
  const current = this.#deps.claimedBrowsers.get(entry.key);
15807
15808
  return current !== void 0 && timingSafeEqual(hash2, current) ? "claimed_browser" : null;
15808
15809
  }
@@ -15814,33 +15815,6 @@ var Bridge = class {
15814
15815
  }
15815
15816
  return "unknown";
15816
15817
  }
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
15818
  authorizeFileRequest(input) {
15845
15819
  const entry = [...this.#sessions.values()].find(
15846
15820
  (candidate) => candidate.session.sessionId === input.sessionId
@@ -15859,7 +15833,7 @@ var Bridge = class {
15859
15833
  credential: "missing",
15860
15834
  key: entry.key,
15861
15835
  sessionId: entry.session.sessionId,
15862
- ...input.requestId === void 0 ? {} : { requestId: input.requestId }
15836
+ requestId: input.requestId
15863
15837
  };
15864
15838
  }
15865
15839
  const credential = this.#fileCredential(entry, input.token);
@@ -15870,32 +15844,23 @@ var Bridge = class {
15870
15844
  credential: this.#fileCredentialClass(input.token),
15871
15845
  key: entry.key,
15872
15846
  sessionId: entry.session.sessionId,
15873
- ...input.requestId === void 0 ? {} : { requestId: input.requestId }
15847
+ requestId: input.requestId
15874
15848
  };
15875
15849
  }
15876
15850
  if (!entry.fileRequestBound) {
15877
15851
  return {
15878
- ok: true,
15852
+ ok: false,
15853
+ reason: "file_request_expired",
15854
+ credential,
15879
15855
  key: entry.key,
15880
15856
  sessionId: entry.session.sessionId,
15881
- ...input.requestId === void 0 ? {} : { requestId: input.requestId },
15882
- credential,
15883
- requestBound: false
15857
+ requestId: input.requestId
15884
15858
  };
15885
15859
  }
15886
15860
  const now = (this.#deps.now ?? Date.now)();
15887
15861
  for (const [requestId, permit2] of entry.filePermits) {
15888
15862
  if (permit2.expiresAt <= now) entry.filePermits.delete(requestId);
15889
15863
  }
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
15864
  const permit = entry.filePermits.get(input.requestId);
15900
15865
  if (!permit) {
15901
15866
  return {
@@ -16001,6 +15966,14 @@ var Bridge = class {
16001
15966
  message: credential.message
16002
15967
  };
16003
15968
  }
15969
+ if (credential.kind !== "claimed") {
15970
+ return {
15971
+ t: "hello_nack",
15972
+ reason: "browser_registration_required",
15973
+ supported: [],
15974
+ message: "The root token is management-only. Register this browser with its own token."
15975
+ };
15976
+ }
16004
15977
  const negotiated = negotiateProtocol(parsed.data.protocolVersions);
16005
15978
  if (!negotiated.ok) {
16006
15979
  return {
@@ -16010,7 +15983,7 @@ var Bridge = class {
16010
15983
  message: negotiated.message
16011
15984
  };
16012
15985
  }
16013
- const key = sessionKeyFor(credential);
15986
+ const key = credential.browserId;
16014
15987
  const held = this.#sessions.get(key);
16015
15988
  if (held?.streamActive) {
16016
15989
  this.#retireSession?.(key, held.session.sessionId);
@@ -16023,16 +15996,12 @@ var Bridge = class {
16023
15996
  protocolVersion: negotiated.version,
16024
15997
  extensionVersion: parsed.data.extensionVersion
16025
15998
  };
16026
- const authRequired = parsed.data.capabilities.includes(
16027
- BRIDGE_AUTH_CAPABILITY
16028
- );
16029
15999
  const routingRequired = credential.kind === "claimed" && parsed.data.capabilities.includes(BROWSER_ROUTING_CAPABILITY);
16030
16000
  const fileRequestBound = parsed.data.capabilities.includes(
16031
16001
  LOCAL_FILE_REQUEST_CAPABILITY
16032
16002
  );
16033
16003
  this.#installSession(key, session, {
16034
16004
  tokenHash,
16035
- authRequired,
16036
16005
  routingRequired,
16037
16006
  fileRequestBound
16038
16007
  });
@@ -16044,10 +16013,8 @@ var Bridge = class {
16044
16013
  protocolVersion: negotiated.version,
16045
16014
  serverVersion: this.#deps.serverVersion,
16046
16015
  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
16016
  capabilities: [
16050
- ...authRequired ? [BRIDGE_AUTH_CAPABILITY] : [],
16017
+ BRIDGE_AUTH_CAPABILITY,
16051
16018
  ...routingRequired ? [BROWSER_ROUTING_CAPABILITY] : [],
16052
16019
  ...fileRequestBound ? [LOCAL_FILE_REQUEST_CAPABILITY] : []
16053
16020
  ]
@@ -16068,15 +16035,13 @@ var Bridge = class {
16068
16035
  return this.#mayAct(entry, presented);
16069
16036
  }
16070
16037
  #mayAct(entry, presented) {
16071
- if (!entry.authRequired) return true;
16072
16038
  if (typeof presented !== "string" || presented.length === 0) return false;
16073
16039
  return timingSafeEqual(digestToken(presented), entry.tokenHash);
16074
16040
  }
16075
16041
  /**
16076
16042
  * Route a frame from the panel to whoever is waiting for it.
16077
16043
  *
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.
16044
+ * `presented` is the credential the poster carried.
16080
16045
  */
16081
16046
  receive(raw, presented) {
16082
16047
  const classified = classifyFrame(
@@ -16268,7 +16233,7 @@ var Bridge = class {
16268
16233
  if (browser === void 0) {
16269
16234
  entry = this.#primary();
16270
16235
  } else {
16271
- const known = this.#deps.claimedBrowsers.has(browser) || browser === LEGACY_SESSION_KEY && this.#sessions.has(browser);
16236
+ const known = this.#deps.claimedBrowsers.has(browser);
16272
16237
  if (!known) {
16273
16238
  return {
16274
16239
  ok: false,
@@ -16857,14 +16822,22 @@ async function handleMcpRequest(bridge, body, accept, stage) {
16857
16822
  delete args["browser"];
16858
16823
  }
16859
16824
  if (params.name === BROWSERS_TOOL) {
16825
+ const registryStatus = bridge.browserRegistryStatus();
16826
+ const { state: registryState, ...registryDetail } = registryStatus;
16827
+ const payload = {
16828
+ browsers: bridge.connectedBrowsers(),
16829
+ registryStatus: registryState,
16830
+ ...registryState === "ready" ? {} : registryDetail
16831
+ };
16860
16832
  return frameMcpResponse(
16861
16833
  rpcResult(id, {
16862
16834
  content: [
16863
16835
  {
16864
16836
  type: "text",
16865
- text: JSON.stringify({ browsers: bridge.connectedBrowsers() })
16837
+ text: JSON.stringify(payload)
16866
16838
  }
16867
- ]
16839
+ ],
16840
+ ...registryState === "disabled" ? { isError: true } : {}
16868
16841
  }),
16869
16842
  accept
16870
16843
  );
@@ -17240,7 +17213,11 @@ function createHttpBridge(opts) {
17240
17213
  }
17241
17214
  if (req.method === "GET" && url2.pathname === "/bridge/events") {
17242
17215
  const claimed = url2.searchParams.get("session");
17243
- const key = claimed === null ? opts.bridge.legacySessionKey() : opts.bridge.keyForSession(claimed);
17216
+ if (claimed === null || claimed.length === 0) {
17217
+ json2(res, 400, { error: "session_required" });
17218
+ return;
17219
+ }
17220
+ const key = claimed === null ? null : opts.bridge.keyForSession(claimed);
17244
17221
  if (key === null) {
17245
17222
  opts.log?.("refused a stream for an unknown session");
17246
17223
  res.writeHead(200, { "content-type": "text/event-stream" });
@@ -17267,13 +17244,8 @@ function createHttpBridge(opts) {
17267
17244
  // by nature.
17268
17245
  "x-accel-buffering": "no"
17269
17246
  });
17270
- const sessionId = claimed ?? opts.bridge.sessionForKey(key)?.sessionId ?? "";
17271
- if (!sessionId) {
17272
- res.end();
17273
- return;
17274
- }
17275
17247
  try {
17276
- installStream(key, sessionId, res);
17248
+ installStream(key, claimed, res);
17277
17249
  } catch {
17278
17250
  res.end();
17279
17251
  return;
@@ -17314,7 +17286,11 @@ function createHttpBridge(opts) {
17314
17286
  }
17315
17287
  const action = b?.["action"];
17316
17288
  if (action === "list") {
17317
- json2(res, 200, { ok: true, browsers: await registry2.list() });
17289
+ json2(res, 200, {
17290
+ ok: true,
17291
+ browsers: await registry2.list(),
17292
+ registryStatus: registry2.status()
17293
+ });
17318
17294
  return;
17319
17295
  }
17320
17296
  if (action === "add") {
@@ -17330,6 +17306,10 @@ function createHttpBridge(opts) {
17330
17306
  });
17331
17307
  return;
17332
17308
  }
17309
+ if (action === "repair") {
17310
+ json2(res, 200, { ok: true, ...await registry2.repair() });
17311
+ return;
17312
+ }
17333
17313
  json2(res, 400, { error: "unknown action" });
17334
17314
  }).catch((err) => {
17335
17315
  const why = err instanceof Error ? err.message : String(err);
@@ -17347,20 +17327,20 @@ function createHttpBridge(opts) {
17347
17327
  const hasRequestId = b !== null && Object.hasOwn(b, "requestId");
17348
17328
  const invalidSessionId = hasSessionId && (typeof b?.sessionId !== "string" || b.sessionId.length === 0);
17349
17329
  const invalidRequestId = hasRequestId && (typeof b?.requestId !== "string" || b.requestId.length === 0);
17350
- if (invalidSessionId || invalidRequestId) {
17330
+ if (!hasSessionId || !hasRequestId || invalidSessionId || invalidRequestId) {
17351
17331
  json2(res, 403, {
17352
17332
  error: "unauthorized",
17353
17333
  message: "\uC0AC\uC9C4 \uC694\uCCAD\uC758 \uC5F0\uACB0 \uC815\uBCF4\uB97C \uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC5B4\uC694."
17354
17334
  });
17355
17335
  return;
17356
17336
  }
17357
- const auth = hasSessionId ? opts.bridge.authorizeFileRequest({
17337
+ const auth = opts.bridge.authorizeFileRequest({
17358
17338
  sessionId: b.sessionId,
17359
- ...hasRequestId ? { requestId: b.requestId } : {},
17339
+ requestId: b.requestId,
17360
17340
  token,
17361
17341
  path,
17362
17342
  ...typeof b?.tool === "string" ? { tool: b.tool } : {}
17363
- }) : opts.bridge.authorizeLegacyFileCredential(token);
17343
+ });
17364
17344
  if (!auth.ok) {
17365
17345
  opts.log?.(
17366
17346
  `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 +17363,7 @@ function createHttpBridge(opts) {
17383
17363
  return;
17384
17364
  }
17385
17365
  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`
17366
+ `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
17367
  );
17388
17368
  let source = null;
17389
17369
  let owner = null;
@@ -17842,6 +17822,7 @@ async function writeLedger(path, claims) {
17842
17822
  var writing = /* @__PURE__ */ new Map();
17843
17823
  async function writeOnce(path, claims) {
17844
17824
  const tmp = `${path}.${process.pid}.tmp`;
17825
+ await mkdir2(dirname(path), { recursive: true, mode: 448 });
17845
17826
  await writeFile2(tmp, `${JSON.stringify(claims, null, 2)}
17846
17827
  `, {
17847
17828
  mode: 384
@@ -17873,17 +17854,24 @@ function matches(claim, handle) {
17873
17854
  function createBrowserRegistry(opts) {
17874
17855
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
17875
17856
  const revoked = /* @__PURE__ */ new Set();
17876
- const current = () => {
17857
+ const readCurrent = () => {
17877
17858
  const read = readLedger(opts.path);
17878
17859
  if (read.state === "unreadable") {
17879
17860
  throw new Error(`browser registry is unreadable: ${read.reason}`);
17880
17861
  }
17881
17862
  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");
17884
- }
17885
- return rows;
17863
+ if (!opts.forbiddenTokenHash) return { rows, quarantined: [] };
17864
+ const quarantined = rows.filter(
17865
+ (row) => timingSafeEqual(row.tokenHash, opts.forbiddenTokenHash)
17866
+ );
17867
+ return {
17868
+ rows: rows.filter(
17869
+ (row) => !timingSafeEqual(row.tokenHash, opts.forbiddenTokenHash)
17870
+ ),
17871
+ quarantined
17872
+ };
17886
17873
  };
17874
+ const current = () => readCurrent().rows;
17887
17875
  return {
17888
17876
  async add(label) {
17889
17877
  const token = mintToken();
@@ -17940,6 +17928,33 @@ function createBrowserRegistry(opts) {
17940
17928
  }
17941
17929
  return true;
17942
17930
  },
17931
+ async repair() {
17932
+ const view = readCurrent();
17933
+ if (view.quarantined.length === 0) return { removed: [] };
17934
+ await writeLedger(opts.path, view.rows);
17935
+ return { removed: view.quarantined.map(handleOf) };
17936
+ },
17937
+ status() {
17938
+ const read = readLedger(opts.path);
17939
+ if (read.state === "unreadable") {
17940
+ return {
17941
+ state: "disabled",
17942
+ error: "browser_registry_unreadable",
17943
+ detail: read.reason
17944
+ };
17945
+ }
17946
+ if (read.state === "ok" && opts.forbiddenTokenHash && ledgerOverlapsToken(read.rows, opts.forbiddenTokenHash)) {
17947
+ return {
17948
+ state: "degraded",
17949
+ error: "browser_registry_root_authority_overlap",
17950
+ quarantinedHandles: read.rows.filter(
17951
+ (row) => timingSafeEqual(row.tokenHash, opts.forbiddenTokenHash)
17952
+ ).map(handleOf),
17953
+ recovery: "browsers repair"
17954
+ };
17955
+ }
17956
+ return { state: "ready" };
17957
+ },
17943
17958
  wasRevoked(tokenHash) {
17944
17959
  return revoked.has(tokenHash);
17945
17960
  }
@@ -17972,12 +17987,12 @@ function runDaemon(env, deps = {}) {
17972
17987
  const browserLabels = /* @__PURE__ */ new Map();
17973
17988
  const ledger = readLedger(ledgerPath(deps.home));
17974
17989
  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)) {
17990
+ const safeLedgerRows = ledger.state === "ok" ? ledger.rows.filter((row) => row.tokenHash !== rootTokenHash) : [];
17991
+ if (ledger.state === "ok") {
17992
+ for (const [id, hash2] of claimedDigestsFrom(safeLedgerRows)) {
17978
17993
  claimedBrowsers.set(id, hash2);
17979
17994
  }
17980
- for (const row of ledger.rows) {
17995
+ for (const row of safeLedgerRows) {
17981
17996
  if (row.browserId !== null) browserLabels.set(row.browserId, row.label);
17982
17997
  }
17983
17998
  }
@@ -17986,8 +18001,10 @@ function runDaemon(env, deps = {}) {
17986
18001
  `browser registry is unreadable (${ledger.reason}); registering is off`
17987
18002
  );
17988
18003
  }
17989
- if (!ledgerAuthorityValid) {
17990
- log("browser registry overlaps the root authority; registering is off");
18004
+ if (ledger.state === "ok" && ledgerOverlapsToken(ledger.rows, rootTokenHash)) {
18005
+ log(
18006
+ "browser registry overlaps the root authority; conflicting rows are quarantined"
18007
+ );
17991
18008
  }
17992
18009
  const bridge = new Bridge({
17993
18010
  send: (frame, key) => http.send(frame, key),
@@ -17995,10 +18012,10 @@ function runDaemon(env, deps = {}) {
17995
18012
  extensionIds,
17996
18013
  serverVersion: version2,
17997
18014
  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.
18015
+ // Empty until a browser is registered.
18000
18016
  claimedBrowsers,
18001
18017
  browserLabels,
18018
+ browserRegistryStatus: () => registry2.status(),
18002
18019
  claimBrowser: (browserId, tokenHash) => {
18003
18020
  const took = registry2.claim(browserId, tokenHash);
18004
18021
  if (took) {
@@ -4,7 +4,7 @@ import {
4
4
  mediaToolDescriptors,
5
5
  spawnDaemon,
6
6
  tryConnect
7
- } from "./chunk-GCRJQLTV.js";
7
+ } from "./chunk-DOPJ4OZ4.js";
8
8
  import {
9
9
  __export
10
10
  } from "./chunk-MLKGABMK.js";
@@ -14601,7 +14601,9 @@ var HelloNackReasonSchema = external_exports.enum([
14601
14601
  /** Origin header absent or not our extension. */
14602
14602
  "forbidden_origin",
14603
14603
  /** This browser already has an active event stream. */
14604
- "session_active"
14604
+ "session_active",
14605
+ /** Root authority cannot open a browser session. */
14606
+ "browser_registration_required"
14605
14607
  ]);
14606
14608
  var BrowserRoutingSchema = external_exports.object({
14607
14609
  targetBrowserId: external_exports.string(),
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-YB7L4SZC.js";
6
6
  import {
7
7
  runDaemon
8
- } from "./chunk-GCRJQLTV.js";
8
+ } from "./chunk-DOPJ4OZ4.js";
9
9
  import "./chunk-MLKGABMK.js";
10
10
 
11
11
  // src/cli.ts
@@ -62,7 +62,7 @@ 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");
65
+ const { runBrowsers } = await import("./browsers-AGEUPMJA.js");
66
66
  const { createNodeIo } = await import("./dist-JICXADYR.js");
67
67
  const io = createNodeIo();
68
68
  const code = await runBrowsers(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);
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-YB7L4SZC.js";
7
+ import "./chunk-DOPJ4OZ4.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.10",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",