chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""Credential-free dev/CI stubs for reviewing provider-gated MLflow UI.
|
||||
|
||||
``run_dev_server.py --stub-providers <names>`` installs these before launching
|
||||
the dev server so features gated on external providers/credentials render
|
||||
without real keys, cost, or nondeterminism.
|
||||
|
||||
- ``claude`` -- a fake ``claude`` CLI on PATH, satisfying the MLflow Assistant's
|
||||
Claude Code provider auth probe (see ``claude_cli.py``).
|
||||
|
||||
The CI ui-review bot passes the names a PR needs; locally, pass them yourself.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
_DEV_STUBS_DIR = Path(__file__).resolve().parent
|
||||
_CLAUDE_CLI = _DEV_STUBS_DIR / "claude_cli.py"
|
||||
|
||||
|
||||
@dataclass
|
||||
class StubResult:
|
||||
"""What a launcher must apply after installing stubs."""
|
||||
|
||||
path_prepend: list[Path] = field(default_factory=list)
|
||||
cleanup_paths: list[Path] = field(default_factory=list)
|
||||
messages: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def _install_claude(result: StubResult) -> None:
|
||||
"""Stage a ``claude`` shim that runs the stub CLI with the current interpreter."""
|
||||
shim_dir = Path(tempfile.mkdtemp(prefix="mlflow-dev-stub-bin-"))
|
||||
shim = shim_dir / "claude"
|
||||
# Use sys.executable (not a bare `python3`) so resolution doesn't depend on PATH.
|
||||
# Quote both paths so the shim survives spaces in the interpreter or repo path.
|
||||
interpreter = shlex.quote(sys.executable)
|
||||
script = shlex.quote(str(_CLAUDE_CLI))
|
||||
shim.write_text(f'#!/usr/bin/env bash\nexec {interpreter} {script} "$@"\n')
|
||||
shim.chmod(0o755)
|
||||
result.path_prepend.append(shim_dir)
|
||||
result.cleanup_paths.append(shim_dir)
|
||||
result.messages.append(f"Staged stub `claude` at {shim}")
|
||||
|
||||
|
||||
_INSTALLERS = {
|
||||
"claude": _install_claude,
|
||||
}
|
||||
|
||||
AVAILABLE_STUBS = tuple(_INSTALLERS)
|
||||
|
||||
|
||||
def _cleanup(result: StubResult) -> None:
|
||||
"""Remove whatever a (possibly partial) install staged, e.g. on failure."""
|
||||
for path in result.cleanup_paths:
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
|
||||
|
||||
def install_stubs(names: list[str]) -> StubResult:
|
||||
"""Install the named stubs, returning PATH/cleanup changes for the caller to apply."""
|
||||
if unknown := [n for n in names if n not in _INSTALLERS]:
|
||||
raise ValueError(
|
||||
f"Unknown stub(s): {', '.join(unknown)}. Available: {', '.join(AVAILABLE_STUBS)}"
|
||||
)
|
||||
result = StubResult()
|
||||
try:
|
||||
for name in names:
|
||||
_INSTALLERS[name](result)
|
||||
except BaseException:
|
||||
# An installer failed partway; clean up what earlier ones staged before
|
||||
# re-raising so we don't leak temp dirs.
|
||||
_cleanup(result)
|
||||
raise
|
||||
return result
|
||||
|
||||
|
||||
def apply_to_environ(result: StubResult) -> None:
|
||||
"""Apply a StubResult's PATH prepend to ``os.environ`` in place."""
|
||||
if result.path_prepend:
|
||||
prepend = os.pathsep.join(str(p) for p in result.path_prepend)
|
||||
os.environ["PATH"] = f"{prepend}{os.pathsep}{os.environ.get('PATH', '')}"
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Credential-free stub `claude` CLI for reviewing provider-gated Assistant UI.
|
||||
|
||||
The MLflow Assistant's "Claude Code" provider only reveals its chat panel after
|
||||
an auth probe succeeds: it shells out to
|
||||
``claude -p hi --max-turns 1 --output-format json`` and unlocks the UI when that
|
||||
exits 0 (see ``mlflow/assistant/providers/claude_code.py``). When the dev server
|
||||
runs without ``ANTHROPIC_API_KEY`` -- the CI ui-review bot (to keep secrets out
|
||||
of PR backend code), or a local dev who hasn't installed/authed the real CLI --
|
||||
the provider reports "not authenticated" and the panel never renders, leaving
|
||||
provider-gated UI (e.g. restore-chat-on-reload) unreviewable.
|
||||
|
||||
This script stands in for ``claude``. ``run_dev_server.py --stub-providers claude``
|
||||
wraps it in a ``claude`` shim and prepends its directory to the dev server's PATH
|
||||
(only the dev server process and its children; a real ``claude`` elsewhere on the
|
||||
machine -- e.g. the ui-review bot's own review step -- is untouched).
|
||||
|
||||
It never contacts Anthropic, so it costs nothing, needs no credentials, and is
|
||||
deterministic:
|
||||
|
||||
- ``--output-format json`` (the auth probe): print one success result and exit 0.
|
||||
- ``--output-format stream-json`` (a live chat turn): emit canned stream-json
|
||||
events -- an assistant text message plus a closing result -- so the chat panel
|
||||
can be exercised end to end and its messages persisted for restore-on-reload.
|
||||
|
||||
The reply is clearly labeled synthetic so reviewers don't mistake it for a real
|
||||
model response. Real provider behavior stays covered by unit/integration tests.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
STUB_REPLY = (
|
||||
"This is a synthetic reply from the MLflow dev stub Claude CLI. The real "
|
||||
"Claude Code provider is replaced so the Assistant chat panel can be reviewed "
|
||||
"without credentials or LLM calls. No model was invoked to produce this message."
|
||||
)
|
||||
|
||||
STUB_MODEL = "mlflow-dev-stub"
|
||||
|
||||
|
||||
def _emit(obj: dict[str, Any]) -> None:
|
||||
sys.stdout.write(json.dumps(obj) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def _result_event(session_id: str) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "result",
|
||||
"subtype": "success",
|
||||
"is_error": False,
|
||||
"result": STUB_REPLY,
|
||||
"session_id": session_id,
|
||||
"duration_ms": 1,
|
||||
"num_turns": 1,
|
||||
"total_cost_usd": 0.0,
|
||||
"usage": {
|
||||
"input_tokens": 8,
|
||||
"output_tokens": 24,
|
||||
"cache_creation_input_tokens": 0,
|
||||
"cache_read_input_tokens": 0,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
# The real `claude` CLI takes many flags; parse only the few the stub needs and
|
||||
# let parse_known_args drop the rest. add_help/allow_abbrev are off so `-p`,
|
||||
# `--verbose`, etc. fall through to the ignored extras rather than being matched.
|
||||
parser = argparse.ArgumentParser(add_help=False, allow_abbrev=False)
|
||||
parser.add_argument("--version", action="store_true")
|
||||
parser.add_argument("--output-format", default="text")
|
||||
parser.add_argument("--resume", default=None)
|
||||
args, _ = parser.parse_known_args(argv)
|
||||
|
||||
if args.version:
|
||||
print(f"0.0.0 ({STUB_MODEL})")
|
||||
return 0
|
||||
|
||||
# Reuse the resume id so a continued conversation keeps a stable session.
|
||||
session_id = args.resume or f"mlflow-dev-stub-{uuid.uuid4().hex[:12]}"
|
||||
|
||||
if args.output_format == "stream-json":
|
||||
_emit({
|
||||
"type": "system",
|
||||
"subtype": "init",
|
||||
"session_id": session_id,
|
||||
"model": STUB_MODEL,
|
||||
"tools": [],
|
||||
})
|
||||
_emit({
|
||||
"type": "assistant",
|
||||
"message": {"role": "assistant", "content": [{"type": "text", "text": STUB_REPLY}]},
|
||||
})
|
||||
_emit(_result_event(session_id))
|
||||
return 0
|
||||
|
||||
# Auth probe (`--output-format json`) and any other invocation: the provider
|
||||
# only checks the exit code, but emit a valid result object for good measure.
|
||||
_emit(_result_event(session_id))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
Reference in New Issue
Block a user