@ponythewhite/base-context 1.0.1 → 1.0.4

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/base-context-runtime/pyproject.toml +1 -1
  3. package/dist/base-context-runtime/src/rlm/mcp.py +39 -17
  4. package/dist/base-context-runtime/test/test_mcp.py +87 -1
  5. package/dist/base-context-runtime/uv.lock +1 -1
  6. package/dist/build-info.json +1 -1
  7. package/dist/bundle/{bundled-modules-TXUMJ2XQ.js → bundled-modules-R5EAJ62K.js} +2 -2
  8. package/dist/bundle/{chunk-S3INFPZZ.js → chunk-BV25CVRM.js} +378 -199
  9. package/dist/bundle/{chunk-V3L54UD6.js → chunk-XDJ56NJA.js} +26 -11
  10. package/dist/bundle/{cli-main-BYMSRPH6.js → cli-main-OUKN743U.js} +2 -2
  11. package/dist/bundle/cli.js +1 -1
  12. package/dist/bundle/{main-ODOYCEQC.js → main-QPCXSW6X.js} +2 -2
  13. package/dist/cli/daemon-ps.d.ts.map +1 -1
  14. package/dist/cli/daemon-ps.js +1 -0
  15. package/dist/cli/daemon-ps.js.map +1 -1
  16. package/dist/core/auth-storage.d.ts +4 -0
  17. package/dist/core/auth-storage.d.ts.map +1 -1
  18. package/dist/core/auth-storage.js +77 -19
  19. package/dist/core/auth-storage.js.map +1 -1
  20. package/dist/core/kernel/bootstrap.d.ts.map +1 -1
  21. package/dist/core/kernel/bootstrap.js +5 -2
  22. package/dist/core/kernel/bootstrap.js.map +1 -1
  23. package/dist/core/mcp/mcp-manager.d.ts.map +1 -1
  24. package/dist/core/mcp/mcp-manager.js +2 -2
  25. package/dist/core/mcp/mcp-manager.js.map +1 -1
  26. package/dist/installer.mjs +5 -2
  27. package/dist/modes/daemon/daemon-supervisor-ownership.d.ts.map +1 -1
  28. package/dist/modes/daemon/daemon-supervisor-ownership.js +16 -16
  29. package/dist/modes/daemon/daemon-supervisor-ownership.js.map +1 -1
  30. package/dist/modes/interactive/auth-flows.d.ts.map +1 -1
  31. package/dist/modes/interactive/auth-flows.js +7 -6
  32. package/dist/modes/interactive/auth-flows.js.map +1 -1
  33. package/dist/modes/interactive/components/login-dialog.d.ts +3 -1
  34. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  35. package/dist/modes/interactive/components/login-dialog.js +3 -1
  36. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  37. package/dist/modes/interactive/components/menu-panel.d.ts +1 -0
  38. package/dist/modes/interactive/components/menu-panel.d.ts.map +1 -1
  39. package/dist/modes/interactive/components/menu-panel.js +3 -0
  40. package/dist/modes/interactive/components/menu-panel.js.map +1 -1
  41. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  42. package/dist/modes/interactive/interactive-mode.js +7 -5
  43. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  44. package/dist/modes/rpc/rpc-client.d.ts +2 -0
  45. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  46. package/dist/modes/rpc/rpc-client.js +58 -31
  47. package/dist/modes/rpc/rpc-client.js.map +1 -1
  48. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.4] - 2026-09-17
4
+
5
+ - Fixed first-time Python setup failing to find its newly installed uv when XDG data paths are configured.
6
+
7
+ ## [1.0.3] - 2026-09-17
8
+
9
+ - Fixed late OAuth and API-key logins overwriting a newer login or undoing logout.
10
+ - Fixed configured MCP environment-token authentication falling back to stored credentials.
11
+ - Fixed MCP tool discovery to retrieve paginated inventories without partial results and protect cached schemas from caller edits.
12
+ - Masked API-key prompts and pasted sign-in codes and callback values while keeping browser sign-in links readable.
13
+ - Fixed provider logout reporting success when saved credentials could not be removed.
14
+ - Fixed shutdown admission loss after slow process scans or transient registry errors while the shutdown process still owns its record.
15
+ - Fixed source builds including the development Python virtual environment in release packages.
16
+
17
+ ## [1.0.2] - 2026-09-17
18
+
19
+ - Fixed RPC requests waiting for a timeout after the agent process exits, and removed the shutdown delay for an already-exited process.
20
+
3
21
  ## [1.0.1] - 2026-09-16
4
22
 
5
23
  - Made the Synerise base-context installer the primary setup path, with automatic prerequisites, optional shell-profile updates, and clear launch instructions.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "base-context-runtime"
3
- version = "1.0.1"
3
+ version = "1.0.4"
4
4
  description = "Kernel-side runtime shim for Base Context recursion"
5
5
  license = "MIT"
6
6
  license-files = ["LICENSE", "NOTICE"]
@@ -10,6 +10,7 @@ import re
10
10
  import threading
11
11
  import time
12
12
  from contextlib import AsyncExitStack
13
+ from copy import deepcopy
13
14
  from collections.abc import Awaitable, Callable
14
15
  from typing import Any, TypeVar
15
16
 
@@ -20,6 +21,7 @@ __all__ = ["McpStartupError", "call_tool", "close", "list_tools", "reload"]
20
21
 
21
22
  _DEFAULT_STARTUP_TIMEOUT = 20.0
22
23
  _DEFAULT_CALL_TIMEOUT = 60.0
24
+ _MAX_TOOL_LIST_PAGES = 100
23
25
  # Must stay strictly below the host's KERNEL_SHUTDOWN_TIMEOUT_MS (5s) kill deadline.
24
26
  _SHUTDOWN_TIMEOUT = 2.5
25
27
  _T = TypeVar("_T")
@@ -259,21 +261,41 @@ class _Generation:
259
261
  return await self.stack.enter_async_context(stdio_client(params, errlog=self._stderr))
260
262
 
261
263
  async def discover(self) -> None:
262
- response = await self.session.list_tools()
264
+ from mcp.types import PaginatedRequestParams
265
+
263
266
  tools: dict[str, dict[str, Any]] = {}
264
- for tool in response.tools:
265
- name = getattr(tool, "name", None)
266
- if not isinstance(name, str):
267
- continue
268
- schema = getattr(tool, "input_schema", None)
269
- if schema is None:
270
- schema = getattr(tool, "inputSchema", None)
271
- tools[name] = {
272
- "name": name,
273
- "description": getattr(tool, "description", "") or "",
274
- "inputSchema": schema if isinstance(schema, dict) else {},
275
- }
276
- self.tools = tools
267
+ cursor: str | None = None
268
+ seen_cursors: set[str] = set()
269
+ for _ in range(_MAX_TOOL_LIST_PAGES):
270
+ response = (
271
+ await self.session.list_tools()
272
+ if cursor is None
273
+ else await self.session.list_tools(params=PaginatedRequestParams(cursor=cursor))
274
+ )
275
+ for tool in response.tools:
276
+ name = getattr(tool, "name", None)
277
+ if not isinstance(name, str):
278
+ continue
279
+ schema = getattr(tool, "input_schema", None)
280
+ if schema is None:
281
+ schema = getattr(tool, "inputSchema", None)
282
+ tools[name] = {
283
+ "name": name,
284
+ "description": getattr(tool, "description", "") or "",
285
+ "inputSchema": schema if isinstance(schema, dict) else {},
286
+ }
287
+ cursor = getattr(response, "next_cursor", None)
288
+ if cursor is None:
289
+ cursor = getattr(response, "nextCursor", None)
290
+ if cursor is None:
291
+ self.tools = tools
292
+ return
293
+ if not isinstance(cursor, str):
294
+ raise RuntimeError(f"MCP server '{self.server}' returned an invalid tools/list cursor")
295
+ if cursor in seen_cursors:
296
+ raise RuntimeError(f"MCP server '{self.server}' repeated a tools/list cursor")
297
+ seen_cursors.add(cursor)
298
+ raise RuntimeError(f"MCP server '{self.server}' exceeded the tools/list page limit")
277
299
 
278
300
  def allows(self, tool: str) -> bool:
279
301
  enabled = self.config.get("enabledTools")
@@ -379,7 +401,7 @@ class _Registry:
379
401
  async def tools(self, server: str) -> list[dict[str, Any]]:
380
402
  async def operation() -> list[dict[str, Any]]:
381
403
  generation = await self._get(server)
382
- return [dict(tool) for name, tool in generation.tools.items() if generation.allows(name)]
404
+ return [deepcopy(tool) for name, tool in generation.tools.items() if generation.allows(name)]
383
405
 
384
406
  return await self._tracked(operation)
385
407
 
@@ -529,7 +551,7 @@ def _bound_auth(provider: str, config: dict[str, Any]) -> dict[str, Any] | None:
529
551
  async def _auth_identity(server: str, config: dict[str, Any]) -> str:
530
552
  env_name = config.get("bearerTokenEnvVar")
531
553
  token = os.environ.get(env_name, "").strip() if isinstance(env_name, str) else ""
532
- if config.get("oauth") is True and not token:
554
+ if config.get("oauth") is True and not env_name:
533
555
  provider = f"mcp:{server}"
534
556
  cred = _bound_auth(provider, config)
535
557
  expires = (cred or {}).get("expires")
@@ -556,7 +578,7 @@ async def _headers(server: str, config: dict[str, Any]) -> dict[str, str]:
556
578
  return headers
557
579
  env_name = config.get("bearerTokenEnvVar")
558
580
  token = os.environ.get(env_name, "").strip() if isinstance(env_name, str) else ""
559
- if config.get("oauth") is True and not token:
581
+ if config.get("oauth") is True and not env_name:
560
582
  cred = _bound_auth(f"mcp:{server}", config)
561
583
  token = _resolve_config_value(str((cred or {}).get("access") or (cred or {}).get("key") or ""))
562
584
  if token:
@@ -15,7 +15,7 @@ from pathlib import Path
15
15
  from types import SimpleNamespace
16
16
  from unittest import mock
17
17
 
18
- from mcp.types import CallToolResult, TextContent
18
+ from mcp.types import CallToolResult, PaginatedRequestParams, TextContent
19
19
  from rlm import McpToolError, mcp
20
20
  from rlm.mcp_base import _parse_result
21
21
 
@@ -104,6 +104,55 @@ class McpRegistryTest(unittest.TestCase):
104
104
  generation = self.generation({"type": "http"}, [tool])
105
105
  self.assertEqual(generation.tools["raw.tool/name"]["inputSchema"], schema)
106
106
 
107
+ def test_listing_detaches_nested_cached_schemas(self):
108
+ schema = {"type": "object", "properties": {"x": {"enum": ["original"]}}}
109
+ generation = self.generation({"type": "http"}, [SimpleNamespace(name="tool", inputSchema=schema)])
110
+
111
+ async def scenario():
112
+ with mock.patch.object(mcp._registry, "_get_locked", mock.AsyncMock(return_value=generation)):
113
+ first = await mcp.list_tools("svc")
114
+ first[0]["inputSchema"]["properties"]["x"]["enum"].append("caller mutation")
115
+ second = await mcp.list_tools("svc")
116
+ self.assertEqual(second[0]["inputSchema"], schema)
117
+ self.assertEqual(schema["properties"]["x"]["enum"], ["original"])
118
+
119
+ run(scenario())
120
+
121
+ def test_discovers_all_pages_with_sdk_params_and_cursor_aliases(self):
122
+ generation = mcp._Generation("svc", {"type": "http"})
123
+ generation.session = SimpleNamespace(list_tools=mock.AsyncMock(side_effect=[
124
+ SimpleNamespace(tools=[SimpleNamespace(name="first")], next_cursor=""),
125
+ SimpleNamespace(tools=[SimpleNamespace(name="second")], nextCursor="page-3"),
126
+ SimpleNamespace(tools=[SimpleNamespace(name="third")], next_cursor=None),
127
+ ]))
128
+ run(generation.discover())
129
+ self.assertEqual(list(generation.tools), ["first", "second", "third"])
130
+ self.assertEqual(generation.session.list_tools.call_args_list, [
131
+ mock.call(),
132
+ mock.call(params=PaginatedRequestParams(cursor="")),
133
+ mock.call(params=PaginatedRequestParams(cursor="page-3")),
134
+ ])
135
+
136
+ def test_failed_pagination_never_publishes_partial_inventory(self):
137
+ for case, cursors, message in (
138
+ ("cycle", ["repeat", "repeat"], "repeated"),
139
+ ("malformed", [17], "invalid"),
140
+ ("excessive", ["page-2", "page-3"], "page limit"),
141
+ ):
142
+ with self.subTest(case=case):
143
+ generation = self.generation({"type": "http"}, [SimpleNamespace(name="previous")])
144
+ previous = generation.tools
145
+ generation.session = SimpleNamespace(list_tools=mock.AsyncMock(side_effect=[
146
+ SimpleNamespace(tools=[SimpleNamespace(name="partial")], next_cursor=cursor)
147
+ for cursor in cursors
148
+ ]))
149
+ with mock.patch.object(mcp, "_MAX_TOOL_LIST_PAGES", 2):
150
+ with self.assertRaisesRegex(RuntimeError, message):
151
+ run(generation.discover())
152
+ self.assertIs(generation.tools, previous)
153
+ self.assertNotIn("partial", generation.tools)
154
+ self.assertLessEqual(generation.session.list_tools.call_count, 2)
155
+
107
156
  def test_sdk_result_aliases_preserve_structured_output_and_errors(self):
108
157
  structured = CallToolResult(content=[], structuredContent={})
109
158
  self.assertEqual(_parse_result(structured), {})
@@ -317,6 +366,43 @@ class McpRegistryTest(unittest.TestCase):
317
366
  env = mcp._stdio_env({"credentialSource": "acp", "env": {"TOKEN": "task-secret"}})
318
367
  self.assertEqual(env, {"PATH": "/bin", "TOKEN": "task-secret"})
319
368
 
369
+ def test_selected_environment_token_refuses_missing_values_before_connection(self):
370
+ config = {
371
+ "type": "http", "url": "https://srv.example/mcp", "oauth": True,
372
+ "bearerTokenEnvVar": "SELECTED_MCP_TOKEN",
373
+ "headers": {"Authorization": "Bearer configured-header"},
374
+ }
375
+ for token in (None, "", " "):
376
+ with self.subTest(token=token), mock.patch.dict(
377
+ os.environ, {} if token is None else {"SELECTED_MCP_TOKEN": token}, clear=True
378
+ ), mock.patch.object(mcp, "host_request", new=mock.AsyncMock(return_value=config)) as request, mock.patch.object(
379
+ mcp, "_read_auth", side_effect=AssertionError("must not read stored OAuth")
380
+ ), mock.patch.object(mcp._Generation, "_open_http", new=mock.AsyncMock()) as connect:
381
+ with self.assertRaisesRegex(RuntimeError, "credentials.*not available"):
382
+ run(mcp.list_tools("remote"))
383
+ with self.assertRaisesRegex(RuntimeError, "credentials.*not available"):
384
+ run(mcp._headers("remote", config))
385
+ request.assert_awaited_once_with("mcp.config", {"server": "remote"})
386
+ connect.assert_not_called()
387
+ self.assertEqual(mcp._registry._generations, {})
388
+ mcp._registry = mcp._Registry()
389
+
390
+ def test_selected_environment_token_is_the_only_auth_source(self):
391
+ config = {
392
+ "oauth": True, "url": "https://srv.example/mcp", "bearerTokenEnvVar": "SELECTED_MCP_TOKEN",
393
+ "headers": {"Authorization": "Bearer configured-header", "X-Other": "keep"},
394
+ }
395
+ with mock.patch.dict(os.environ, {"SELECTED_MCP_TOKEN": " selected-token "}), mock.patch.object(
396
+ mcp, "_read_auth", side_effect=AssertionError("must not read stored OAuth")
397
+ ), mock.patch.object(mcp, "host_request", new=mock.AsyncMock()) as request:
398
+ identity = run(mcp._auth_identity("remote", config))
399
+ self.assertNotEqual(identity, "anonymous")
400
+ self.assertNotIn("selected-token", identity)
401
+ self.assertEqual(run(mcp._headers("remote", config)), {
402
+ "Authorization": "Bearer selected-token", "X-Other": "keep",
403
+ })
404
+ request.assert_not_called()
405
+
320
406
  def test_endpoint_bound_credential_never_attaches_to_another_url(self):
321
407
  cred = {"access": "old-token", "endpoint": "https://old.example/mcp"}
322
408
  config = {"oauth": True, "url": "https://new.example/mcp"}
@@ -367,7 +367,7 @@ wheels = [
367
367
 
368
368
  [[package]]
369
369
  name = "base-context-runtime"
370
- version = "1.0.1"
370
+ version = "1.0.4"
371
371
  source = { editable = "." }
372
372
  dependencies = [
373
373
  { name = "mcp" },
@@ -1,5 +1,5 @@
1
1
  {
2
- "sourceCommit": "9247ae92d4cc1a43fcb003316e6762be579a6a05",
2
+ "sourceCommit": "84d72c5846ae0d65614b6b2bf966c48dd18ad7aa",
3
3
  "sourceDirty": false,
4
4
  "buildHost": {
5
5
  "platform": "linux",
@@ -51,7 +51,7 @@ import {
51
51
  oauth_exports,
52
52
  runPrintMode,
53
53
  runRpcMode
54
- } from "./chunk-S3INFPZZ.js";
54
+ } from "./chunk-BV25CVRM.js";
55
55
  import {
56
56
  AgentSession,
57
57
  CURRENT_SESSION_VERSION,
@@ -143,7 +143,7 @@ import {
143
143
  withFileMutationQueue,
144
144
  wrapRegisteredTool,
145
145
  wrapRegisteredTools
146
- } from "./chunk-V3L54UD6.js";
146
+ } from "./chunk-XDJ56NJA.js";
147
147
  import "./chunk-CFNASUYO.js";
148
148
  import "./chunk-HKINIBP4.js";
149
149
  import {