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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ the local OpenAI-compatible server.
| `og-veil stop` | Stop the background server. |
| `og-veil restart` | Stop and start the background server — e.g. after `og-veil update`. |
| `og-veil status` | Login + network config + whether the server is running. |
| `og-veil test ["prompt"]` | Send a one-off prompt to the running server and print the verified reply. |
| `og-veil test ["prompt"]` | Send a one-off prompt to the running server and print the verified reply. Automatically targets the port the server was started on; override with `--host`/`--port`. |
| `og-veil update` | Update og-veil to the latest version. |
| `og-veil login` | Authorize / re-authorize this device. |
| `og-veil setup` | Re-run the setup wizard. |
Expand Down
43 changes: 43 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,49 @@ def test_test_command_posts_prompt_to_localhost_and_prints_reply():
assert "0xabc" in out.output


def test_test_command_targets_running_servers_saved_port():
from veil.config import ServerConfig

resp = mock.MagicMock(status_code=200, headers={})
resp.json.return_value = {"choices": [{"message": {"content": "hi"}}]}
# Server was started on a non-default port; `test` should find it without
# the caller re-specifying anything.
with (
mock.patch("veil.daemon.load_runtime", return_value=ServerConfig(port=11435)),
mock.patch("requests.post", return_value=resp) as post,
):
out = CliRunner().invoke(cli.main, ["test", "hi"])
assert out.exit_code == 0
assert post.call_args.args[0] == "http://127.0.0.1:11435/v1/chat/completions"


def test_test_command_port_flag_overrides():
from veil.config import ServerConfig

resp = mock.MagicMock(status_code=200, headers={})
resp.json.return_value = {"choices": [{"message": {"content": "hi"}}]}
with (
mock.patch("veil.daemon.load_runtime", return_value=ServerConfig(port=11435)),
mock.patch("requests.post", return_value=resp) as post,
):
out = CliRunner().invoke(cli.main, ["test", "--port", "11500", "hi"])
assert out.exit_code == 0
# Explicit --port wins over the running server's recorded port.
assert post.call_args.args[0] == "http://127.0.0.1:11500/v1/chat/completions"


def test_start_server_records_runtime_config():
from veil.config import ServerConfig

with (
mock.patch("veil.daemon.start_background", return_value=4321),
mock.patch("veil.daemon.save_runtime") as save,
):
cli._start_server(ServerConfig(port=11435), foreground=False)
assert save.called
assert save.call_args.args[0].port == 11435


def test_test_command_reports_server_not_running():
import requests

Expand Down
33 changes: 33 additions & 0 deletions tests/test_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,42 @@ def test_start_writes_pidfile_and_stop_removes_it(home):

def test_running_pid_clears_stale_pidfile(home):
daemon.pid_path().write_text("999999")
daemon.runtime_path().write_text('{"port": 11500}')
with mock.patch("os.kill", side_effect=OSError): # process not found
assert daemon.running_pid() is None
assert not daemon.pid_path().exists()
# A dead server's recorded config must not linger and mislead `test`/`env`.
assert not daemon.runtime_path().exists()


def test_save_and_load_runtime_roundtrip(home):
from veil.config import ServerConfig

daemon.save_runtime(ServerConfig(port=11500, pii_scrub=True))
daemon.pid_path().write_text("4321")
with mock.patch("os.kill"): # pretend 4321 is alive
loaded = daemon.load_runtime()
assert loaded is not None
assert loaded.port == 11500
assert loaded.pii_scrub is True


def test_load_runtime_returns_none_when_not_running(home):
from veil.config import ServerConfig

daemon.save_runtime(ServerConfig(port=11500))
# No pidfile → no running server → callers should fall back to env/defaults.
assert daemon.load_runtime() is None


def test_stop_clears_recorded_runtime(home):
from veil.config import ServerConfig

daemon.save_runtime(ServerConfig(port=11500))
daemon.pid_path().write_text("4321")
with mock.patch("os.kill"): # alive check + SIGTERM both succeed
assert daemon.stop_background() == 4321
assert not daemon.runtime_path().exists()


def test_start_refuses_when_already_running(home):
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 43 additions & 6 deletions veil/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,18 @@ def serve(
_start_server(config, foreground=foreground or skip_setup)


def _client_config() -> ServerConfig:
"""Config for commands that talk to the running server (``test``, ``env``).

Prefers the config the background server was actually started with — so a
server launched on a non-default port is reached automatically — and falls
back to the environment/defaults when nothing is running.
"""
from veil.daemon import load_runtime

return load_runtime() or ServerConfig.from_env()


def _config_flags(config: ServerConfig) -> list[str]:
flags = ["--host", config.host, "--port", str(config.port)]
if config.pinned_tee_id:
Expand All @@ -161,7 +173,7 @@ def _start_server(config: ServerConfig, *, foreground: bool) -> None:
run_server(config)
return

from veil.daemon import log_path, running_pid, start_background
from veil.daemon import log_path, running_pid, save_runtime, start_background

try:
pid = start_background(_config_flags(config))
Expand All @@ -173,6 +185,9 @@ def _start_server(config: ServerConfig, *, foreground: bool) -> None:
click.echo(" Stop it with: og-veil stop")
return
raise click.ClickException(str(exc))
# Record where the server is listening so `test`/`env`/`status` find it
# without the caller having to re-specify the port.
save_runtime(config)
click.secho(f"✓ OpenGradient Veil running in the background (pid {pid}).", fg="green")
click.echo(f" Base URL : {config.advertised_base_url()}")
click.echo(f" Logs : {log_path()}")
Expand Down Expand Up @@ -252,7 +267,7 @@ def env_cmd() -> None:
"""Print the env vars to point your agent at OpenGradient Veil."""
from veil.daemon import running_pid

config = ServerConfig.from_env()
config = _client_config()
click.echo("Point your agent at OpenGradient Veil (one env var change):")
click.secho(f" export OPENAI_BASE_URL={config.advertised_base_url()}", bold=True)
click.echo(" export OPENAI_API_KEY=og-veil # ignored; your Chat session authenticates")
Expand Down Expand Up @@ -301,18 +316,35 @@ def login_cmd(app_url: str, no_browser: bool, manual: bool) -> None:
@main.command(name="test")
@click.argument("prompt", nargs=-1)
@click.option("--model", default="gpt-4.1", show_default=True, help="Model to send the prompt to.")
def test_cmd(prompt: tuple[str, ...], model: str) -> None:
@click.option(
"--host", default=None, help="Override the host to reach (default: the running server's)."
)
@click.option(
"--port",
type=int,
default=None,
help="Override the port to reach (default: the running server's).",
)
def test_cmd(prompt: tuple[str, ...], model: str, host: str | None, port: int | None) -> None:
"""Send a one-off PROMPT to the running local server and print the reply.

Posts to the localhost OpenAI-compatible endpoint — the same one your agent
uses — so the background server must already be running (start it with
``og-veil``). The reply is verified TEE output.

By default this targets whatever host/port the running server was started on
(e.g. ``og-veil serve --port 11435``), so you don't have to remember it. Pass
``--host``/``--port`` to point at a specific server instead.
"""
import requests

text = " ".join(prompt).strip() or "Say hello from a verified TEE in one short sentence."

config = ServerConfig.from_env()
config = _client_config()
if host:
config.host = host
if port:
config.port = port
base_url = config.advertised_base_url()
body = {"model": model, "messages": [{"role": "user", "content": text}]}
try:
Expand Down Expand Up @@ -352,7 +384,7 @@ def status() -> None:
session = Session.load()
except AuthError as exc:
raise click.ClickException(str(exc))
from veil.daemon import running_pid
from veil.daemon import load_runtime, running_pid

from veil import __version__

Expand All @@ -365,7 +397,12 @@ def status() -> None:
f"TEE registry : {cfg.tee_registry_address or '(none)'} @ {cfg.tee_registry_rpc_url or '(none)'}"
)
pid = running_pid()
click.echo(f"Background : running (pid {pid})" if pid else "Background : not running")
if pid:
running = load_runtime() or ServerConfig.from_env()
click.echo(f"Background : running (pid {pid})")
click.echo(f"Base URL : {running.advertised_base_url()}")
else:
click.echo("Background : not running")


def _update_command() -> list[str]:
Expand Down
29 changes: 28 additions & 1 deletion veil/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from __future__ import annotations

import os
from dataclasses import dataclass
from dataclasses import dataclass, fields
from pathlib import Path

# Relay endpoint on the chat-api (the OpenGradient OHTTP relay). The relay holds
Expand Down Expand Up @@ -38,6 +38,16 @@ def session_path() -> Path:
return config_home() / "session.json"


def runtime_path() -> Path:
"""File recording the config the background server actually started with.

Written when the server is launched and removed when it stops, so client
commands (``test``, ``env``, ``status``) can discover the host/port the
server is really listening on instead of guessing the default.
"""
return config_home() / "server.json"


@dataclass
class ServerConfig:
"""Settings for the local OpenAI-compatible server."""
Expand Down Expand Up @@ -84,6 +94,23 @@ def advertised_base_url(self) -> str:
"""The ``/v1`` base URL to tell agents to use."""
return f"http://{self.host}:{self.port}/v1"

def to_dict(self) -> dict:
"""Serializable view used to persist the running server's config."""
return {
"host": self.host,
"port": self.port,
"expected_pcr_hash": self.expected_pcr_hash,
"pinned_tee_id": self.pinned_tee_id,
"pii_scrub": self.pii_scrub,
"tee_refresh_interval": self.tee_refresh_interval,
}

@classmethod
def from_dict(cls, data: dict) -> "ServerConfig":
"""Rebuild from :meth:`to_dict`, ignoring unknown/missing keys."""
known = {f.name for f in fields(cls)}
return cls(**{k: v for k, v in data.items() if k in known})


def _env_bool(value: str | None) -> bool:
return value is not None and value.strip().lower() in ("1", "true", "yes", "on")
Expand Down
30 changes: 29 additions & 1 deletion veil/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@

from __future__ import annotations

import json
import os
import signal
import subprocess
import sys
from pathlib import Path

from veil.config import config_home
from veil.config import ServerConfig, config_home, runtime_path


def pid_path() -> Path:
Expand All @@ -38,10 +39,36 @@ def running_pid() -> int | None:
os.kill(pid, 0) # signal 0 just checks the process exists
except OSError:
path.unlink(missing_ok=True) # stale pidfile
clear_runtime() # and the config it recorded
return None
return pid


def save_runtime(config: ServerConfig) -> None:
"""Record the config the background server was started with."""
runtime_path().write_text(json.dumps(config.to_dict()))


def clear_runtime() -> None:
runtime_path().unlink(missing_ok=True)


def load_runtime() -> ServerConfig | None:
"""Config of the running background server, or None if none is running.

Lets ``test``/``env``/``status`` point at whatever host/port the server was
actually started on instead of assuming the default. Returns None when no
server is running so callers fall back to the environment/defaults.
"""
if running_pid() is None:
return None
try:
data = json.loads(runtime_path().read_text())
except (OSError, ValueError):
return None
return ServerConfig.from_dict(data)


def start_background(serve_flags: list[str]) -> int:
"""Launch ``og-veil serve --skip-setup <flags>`` detached; return its pid.

Expand Down Expand Up @@ -75,6 +102,7 @@ def stop_background() -> int | None:
except OSError:
pass
pid_path().unlink(missing_ok=True)
clear_runtime()
return pid


Expand Down
Loading