Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docker/omnigraph-server.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ ARG OMNIGRAPH_VERSION=0.10.0
# Kept separate because on a moving tag the two differ — see
# witan_core/omnigraph_install.py :: _OMNIGRAPH_RELEASE_TAG.
ARG OMNIGRAPH_RELEASE_TAG=edge
ARG OMNIGRAPH_SHA256_X86_64=8ecabdbc3a11d60716f569b32de6710834ddcbba328c2342b77f5c529bb7bc4f
ARG OMNIGRAPH_SHA256_ARM64=aef871eeb070532947beee0f7644848552f59bbc8d4100a4bdad6d760acef647
ARG OMNIGRAPH_SHA256_X86_64=68099e33941cc5c252f36d4c2a26f1dfff6b28e4eb627ba1f7bf098856d34349
ARG OMNIGRAPH_SHA256_ARM64=d0e42176625584370a26c8a6ca43bc24447ea822483c46d971807475a5a5782a

# ── Fetch + checksum-verify the release, extract both binaries ────────────────
FROM debian:trixie-slim@sha256:3a39a0592364683e6bab97937b72cad5a8fa6dcbbee90edb3bb48c7f8e94f258 AS fetch
Expand Down
4 changes: 2 additions & 2 deletions docker/witan.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ ARG OMNIGRAPH_VERSION=0.10.0
# Kept separate because on a moving tag the two differ — see
# witan_core/omnigraph_install.py :: _OMNIGRAPH_RELEASE_TAG.
ARG OMNIGRAPH_RELEASE_TAG=edge
ARG OMNIGRAPH_SHA256_X86_64=8ecabdbc3a11d60716f569b32de6710834ddcbba328c2342b77f5c529bb7bc4f
ARG OMNIGRAPH_SHA256_ARM64=aef871eeb070532947beee0f7644848552f59bbc8d4100a4bdad6d760acef647
ARG OMNIGRAPH_SHA256_X86_64=68099e33941cc5c252f36d4c2a26f1dfff6b28e4eb627ba1f7bf098856d34349
ARG OMNIGRAPH_SHA256_ARM64=d0e42176625584370a26c8a6ca43bc24447ea822483c46d971807475a5a5782a
# Keep in lockstep with witan-council's version (mcp/servers/witan/pyproject.toml
# [project].version / [tool.bumpversion]); it labels the built image.
ARG WITAN_VERSION=0.8.0
Expand Down
9 changes: 9 additions & 0 deletions mcp/servers/witan-code/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/) (pre-1.0:
a MINOR bump may include breaking changes).

## [0.13.5] - 2026-08-21

### Changed

- Raised the `witan-core` floor to `>=0.28` for the refreshed omnigraph
`edge` digest (see witan-core's CHANGELOG) — a version below it fails
`witan-code setup`'s checksum and leaves the indexer with no binary to
shell out to.

## [0.13.4] - 2026-08-19

### Changed
Expand Down
15 changes: 12 additions & 3 deletions mcp/servers/witan-code/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "witan-code"
version = "0.13.4"
version = "0.13.5"
description = "witan-code — tree-sitter code graph + cross-repo bridge (mounts under `witan code`)"
readme = "README.md"
license = "BSD-3-Clause"
Expand Down Expand Up @@ -78,7 +78,16 @@ dependencies = [
# published, so anything resolving below 0.24 would silently install
# without sentry-sdk (or error on the unknown extra) rather than getting
# Sentry reporting at all.
"witan-core[cli,remote,observability,sentry]>=0.24,<1",
#
# ★ >=0.28 IS NOT ABOUT AN IMPORT, same exception witan-council's floor
# documents: witan-core 0.28.0 carries the current omnigraph `edge`
# digest, and every version below it fails `witan setup`'s checksum and
# installs no binary at all — indexer tests skip without one
# (tests/conftest.py) and a real indexer run has nothing to shell out to.
# Expect this floor to keep moving while omnigraph 0.10.0 stays on the
# moving `edge` tag; see
# packages/witan-core/witan_core/omnigraph_install.py.
"witan-core[cli,remote,observability,sentry]>=0.28,<1",
"fastmcp>=3.4.2,<5",
"cyclopts>=4,<5",
# [targets.<name>] override models (config.py) — also transitively pulled
Expand Down Expand Up @@ -162,7 +171,7 @@ testpaths = ["tests"]
packages = ["witan_code"]

[tool.bumpversion]
current_version = "0.13.4"
current_version = "0.13.5"
allow_dirty = true

[[tool.bumpversion.files]]
Expand Down
24 changes: 24 additions & 0 deletions mcp/servers/witan/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/) (pre-1.0:
a MINOR bump may include breaking changes).

## [0.21.0] - 2026-08-21

### Fixed

- **`witan project show`, `witan trace show`, and `witan session list` no
longer crash against a deployed target.** All three bypassed the MCP tool
layer with `s.client.read(...)` to reach queries with no dedicated tool —
correct for the in-process local server, where `s.client` is a real
omnigraph client, but `RemoteServerProxy` has no `client`: `__getattr__`
handed back a plain dispatch closure for that name, and `.read(...)` on it
raised `AttributeError: 'function' object has no attribute 'read'`.

Routed through existing tools instead — `workflow_project_get_blockers`,
`workflow_trace_get`, and a new `include_superseded` flag on
`workflow_session_list` (for `session list`'s dedupe view, the one caller
that wants superseded rows back) — which dispatch correctly against either
target. Regression-tested end to end against a real `RemoteServerProxy`.

### Changed

- Raised the `witan-core` floor to `>=0.28` for the refreshed omnigraph
`edge` digest (see witan-core's CHANGELOG) — a version below it fails
`witan setup`'s checksum and leaves the CLI with no binary at all.

## [0.20.0] - 2026-08-20

### Fixed
Expand Down
20 changes: 11 additions & 9 deletions mcp/servers/witan/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "witan-council"
version = "0.20.0"
version = "0.21.0"
description = "witan — agent memory, planning, and collaboration graph (work-coordination layer + umbrella CLI)"
readme = "README.md"
license = "BSD-3-Clause"
Expand Down Expand Up @@ -38,12 +38,14 @@ dependencies = [
# Hence >=0.21 below. Nothing in CI catches this — read the sentence above
# before adding a witan_core symbol, because the tests will not tell you.
#
# ★ >=0.27 IS THE ONE ENTRY HERE THAT IS NOT ABOUT AN IMPORT. witan-core
# 0.27.0 refreshed the pinned omnigraph asset digests after upstream moved
# the `edge` tag; 0.26.0 carries the stale ones, so `witan setup` against it
# fails the checksum and installs no binary at all — and witan/server.py
# bootstraps a graph at import time, so the CLI is unusable rather than
# degraded. Same consequence as a missing symbol, different cause.
# ★ >=0.28 CONTINUES THE ONE ENTRY HERE THAT IS NOT ABOUT AN IMPORT. `edge`
# moved again the day after 0.27.0's refresh; witan-core 0.28.0 carries the
# new digest, and every version below it fails `witan setup`'s checksum and
# installs no binary at all — witan/server.py bootstraps a graph at import
# time, so the CLI is unusable rather than degraded. Same consequence as a
# missing symbol, different cause. Expect this floor to keep moving while
# omnigraph 0.10.0 stays on the moving `edge` tag; see
# packages/witan-core/witan_core/omnigraph_install.py.
#
# (>=0.21 for remote.oidc.SessionLife, imported at module scope by
# witan/remote/oidc.py and re-exported in its __all__;
Expand Down Expand Up @@ -120,7 +122,7 @@ dependencies = [
# failure mode. No AttributeError, no test failure — a published
# witan-council on 0.24 just quietly ships the bug this floor exists to
# keep fixed.
"witan-core[cli,remote,observability,sentry]>=0.27,<1",
"witan-core[cli,remote,observability,sentry]>=0.28,<1",
"pyyaml>=6,<7",
"tomli-w>=1,<2",
# Direct import in witan/remote/oidc.py for the CLI's OIDC device-code
Expand Down Expand Up @@ -165,7 +167,7 @@ packages = ["witan"]
"schema" = "schema"

[tool.bumpversion]
current_version = "0.20.0"
current_version = "0.21.0"
allow_dirty = true

[[tool.bumpversion.files]]
Expand Down
164 changes: 145 additions & 19 deletions mcp/servers/witan/tests/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -794,39 +794,165 @@ def test_inject_context_cli_survives_an_unreachable_deployment(
):
"""A configured-but-down deployment must not reach an agent's prompt.

Worth pinning rather than assuming, because the shape here is not the one
the rest of the CLI has: this command never calls ``_srv()``, so it does not
go through ``RemoteMCPProxy`` at all — a remote-configured hook still reads
``graph_uri`` directly. That means the unreachable-remote classification
added for every other command is not what protects this path; the blanket
guard inside ``ctx_module.inject_context`` is. Both a dead deployment and a
dead graph URI are set, so whichever one it does reach, stdout stays empty.
A `remote_url`-only target has no direct graph endpoint the CLI is
allowed to open, so this command routes through the same tool-calling
proxy `_srv()` builds for every other command (agent-kit#261's mechanism,
fixed here for this path too — see
tk-witan-hook-context-reads-the-local-store-on-a-de-dfb2c9). It must
degrade to an empty block rather than raise when the deployment cannot be
reached, exactly like `session-checkpoint` already does.
"""
from witan.cli import hooks
from witan.graph import OmnigraphClient
from witan_core.remote.proxy import RemoteMCPProxy

cfg_file = tmp_path / "config.toml"
cfg_file.write_text("")
monkeypatch.setenv("WITAN_CONFIG", str(cfg_file))
monkeypatch.delenv("WITAN_TARGET", raising=False)
monkeypatch.setenv("WITAN_REMOTE_URL", "https://witan.invalid/mcp")
monkeypatch.setenv("WITAN_OIDC_ISSUER", "https://sso.invalid/realms/ol")
# An http(s) URI so the "local store missing" early return is skipped and
# the read is genuinely attempted. Raised rather than actually dialled: the
# real client rides out a restart with a connect-retry budget, which is
# right in production and minutes of nothing in a test.
monkeypatch.setenv("WITAN_MEMORY_URI", "https://omnigraph.invalid")
attempted: list[str] = []

def _refused(_self, _query_file, query_name, _params):
attempted.append(query_name)
raise RuntimeError("omnigraph: connection refused")
async def _refused(_self, name, _args, _kwargs):
attempted.append(name)
raise RuntimeError("witan.invalid: connection refused")

monkeypatch.setattr(OmnigraphClient, "read", _refused)
monkeypatch.setattr(RemoteMCPProxy, "_invoke", _refused)

hooks.inject_context() # must not raise

assert capsys.readouterr().out == ""
# Not vacuous: the hook got as far as a read before degrading. A cached
# block, or an early return, would leave this empty.
# Not vacuous: the hook got as far as a tool call before degrading. A
# cached block, or an early return, would leave this empty.
assert attempted


class _FakeRemoteServer:
"""Records every call it receives and answers with fixed, known data.

Stands in for the tool-calling proxy ``_srv()``/``remote_proxy()`` build —
``inject_context_remote`` only ever calls attribute-style methods on it, the
same shape ``RemoteServerProxy.__getattr__`` returns, so a plain class with
matching method names is a faithful substitute without dialling out.
"""

def __init__(self, projects, ready, sessions_by_project):
self.calls: list[tuple[str, dict]] = []
self._projects = projects
self._ready = ready
self._sessions_by_project = sessions_by_project

def workflow_project_list(self, **kwargs):
self.calls.append(("workflow_project_list", kwargs))
return self._projects

def task_ready(self, **kwargs):
self.calls.append(("task_ready", kwargs))
return self._ready

def workflow_session_list(self, **kwargs):
self.calls.append(("workflow_session_list", kwargs))
return self._sessions_by_project.get(kwargs.get("project_slug"), [])


def test_inject_context_remote_reads_through_the_proxy(tmp_path, monkeypatch):
"""The successful remote path, not just its failure mode.

``test_inject_context_cli_survives_an_unreachable_deployment`` above only
proves a dead proxy degrades to empty output — it never exercises a
proxy that actually answers, so a regression in the tool names/arguments,
session aggregation, or rendering could ship with that test still green
(Copilot review on agent-kit#272). This pins the exact calls made and the
resulting text.
"""
from witan import context as ctx_module

monkeypatch.setenv("TMPDIR", str(tmp_path))
monkeypatch.setenv("WITAN_CONTEXT_TTL", "0")
import tempfile

monkeypatch.setattr(tempfile, "tempdir", None)

repo = "https://github.com/test/ctx-remote"
monkeypatch.setenv("WITAN_REPO", repo)

server = _FakeRemoteServer(
projects=[
{"slug": "wp-remote", "title": "Remote Project", "phase": "implementation"}
],
ready=[
{
"slug": "tk-remote",
"title": "Remote Task",
"priority": "p1",
"status": "open",
}
],
sessions_by_project={},
)

text = ctx_module.inject_context_remote(server, "https://witan.example.org/mcp")

assert ("workflow_project_list", {"repo": repo, "status": "active"}) in server.calls
assert ("task_ready", {"repo": repo, "limit": 10000}) in server.calls
assert (
"workflow_session_list",
{"project_slug": "wp-remote"},
) in server.calls

assert "## Active Workflow Projects" in text
assert "Remote Project" in text
assert "wp-remote" in text
assert "## Ready Tasks" in text
assert "Remote Task" in text
# No remote-tool equivalent exists yet for either — must be absent, not
# just incidentally empty (see inject_context_remote's docstring).
assert "## In-Flight Branch" not in text
assert "Unmigrated Repo Keys" not in text


def test_inject_context_remote_no_repo_skips_project_list(tmp_path, monkeypatch):
"""Mirrors the local path's own gating: outside a detected repo, no
project list is fetched at all (calling ``workflow_project_list(repo="")``
would return every repo's active projects, not "none" — see its
docstring), but ready tasks still resolve to the unscoped set."""
from witan import context as ctx_module

monkeypatch.setenv("TMPDIR", str(tmp_path))
monkeypatch.setenv("WITAN_CONTEXT_TTL", "0")
import tempfile

monkeypatch.setattr(tempfile, "tempdir", None)
monkeypatch.setenv("WITAN_REPO", "") # explicitly disables repo detection

server = _FakeRemoteServer(projects=[], ready=[], sessions_by_project={})

ctx_module.inject_context_remote(server, "https://witan.example.org/mcp")

assert not any(name == "workflow_project_list" for name, _ in server.calls)
assert ("task_ready", {"repo": "", "limit": 10000}) in server.calls


def test_inject_context_remote_cache_key_is_the_deployment_url(tmp_path, monkeypatch):
"""The cache key is the deployment URL, not a store path — two different
deployments must not collide on one cache entry, and a repeat call within
the TTL must not re-hit the proxy at all."""
from witan import context as ctx_module

monkeypatch.setenv("TMPDIR", str(tmp_path))
import tempfile

monkeypatch.setattr(tempfile, "tempdir", None)
monkeypatch.setenv("WITAN_REPO", "https://github.com/test/ctx-remote-cache")

server = _FakeRemoteServer(projects=[], ready=[], sessions_by_project={})

ctx_module.inject_context_remote(server, "https://witan-a.example.org/mcp")
calls_after_first = len(server.calls)
ctx_module.inject_context_remote(server, "https://witan-a.example.org/mcp")
# Same deployment, second call: served from cache, no fresh proxy read.
assert len(server.calls) == calls_after_first

ctx_module.inject_context_remote(server, "https://witan-b.example.org/mcp")
# A different deployment's URL must not read the first one's cache.
assert len(server.calls) > calls_after_first
Loading
Loading