Skip to content
Draft
9 changes: 9 additions & 0 deletions deploy/compose/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ OPENDOX_DATABASE_URL=postgresql://opendox_runtime:change-me-local-only-too@postg
# `opendox-runtime runtime migrate` alone; the served application never receives it.
OPENDOX_MIGRATION_DATABASE_URL=postgresql://opendox:change-me-local-only@postgres:5432/opendox

# The install shape: `hosted` or `local`, and this package is HOSTED — a
# broker, its pinned issuer and this file's own database. Unset means hosted
# too, which is the point: a hosted install that forgets its issuer REFUSES
# rather than falling into the local single-user mode (plan 034 T070; #1144
# 13.4, 13.5). `local` is the one-user install `opendox generate-and-open
# --local` starts on a laptop, with no broker and loopback only; it refuses the
# broker settings below, so it is never selected here.
OPENDOX_INSTALL_MODE=hosted

# REQUIRED. The Keycloak broker's issuer, pinned (RULING Q2): a token from any
# other issuer is refused rather than trusted.
OPENDOX_OIDC_ISSUER=https://keycloak.example/realms/opendox
Expand Down
64 changes: 63 additions & 1 deletion src/opendox/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@
# `opendox.corpus_adapter` besides the stdlib.
from opendox import corpus_adapter # noqa: E402
from opendox.runtime import local_git_adapter # noqa: E402
# THE INSTALL SHAPE (plan 034 T070; #1144 13.4-13.6): `generate-and-open`
# resolves `--local` against `OPENDOX_INSTALL_MODE` here, before it generates
# or serves anything. Stdlib-only, like `local_git_adapter` above, which
# already imports it, so this adds no reach and no import weight.
from opendox.runtime import config as runtime_config # noqa: E402
from opendox.boundary import ( # noqa: E402
BoundaryViolation, HumanGate, OutputBoundary,
)
Expand Down Expand Up @@ -409,11 +414,57 @@ def _validate(written: Path, args: argparse.Namespace, *,
return 0


def _resolve_install_shape(args: argparse.Namespace,
env=None) -> str:
"""The install shape this run serves as, or `ConfigurationError` naming why.

`--local` and `OPENDOX_INSTALL_MODE` are resolved by
`runtime_config.install_mode`, the one reading of the selector, which
refuses the two disagreeing (plan 034 T070's fail-closed reading) and
defaults to HOSTED (#1144 13.4, 13.5). Then each shape asks what it needs:

* LOCAL binds loopback only, with no opt-in: a non-loopback `--host` is
refused naming the rule (13.4), and so is anything a local install
cannot be (`refuse_what_a_local_install_cannot_be`: a broker setting
beside it, or a non-loopback `OPENDOX_BIND_HOST`). It needs no broker.
Its datastore is 13.1's, and arrives with T072.
* HOSTED, set or by default, refuses with no issuer, NAMING THE ISSUER
(13.5), and then loads the runtime's whole configuration, because the
serving process is the one whose settings are the install's (13.4a;
R1Q16 (i)). Otherwise unchanged (13.6).

Asked before anything is scanned, minted or bound, so a refused run leaves
nothing behind and exits at once rather than starting a server that a
bound would have to kill (F13.1's `test "$rc" -ne 124`).
"""
env = os.environ if env is None else env
mode = runtime_config.install_mode(
env, local_flag=bool(getattr(args, "local", False)))
if mode == runtime_config.INSTALL_MODE_LOCAL:
runtime_config.refuse_a_non_loopback_local_bind("--host", args.host)
runtime_config.refuse_what_a_local_install_cannot_be(env)
else:
runtime_config.require_the_hosted_issuer(env)
runtime_config.load_settings(env)
return mode


def cmd_generate_and_open(args: argparse.Namespace, *, opener=webbrowser.open) -> int:
"""Regenerate the snapshot from the working tree into a run dir, start the
local server, print the URL (ALWAYS), and open the browser. `--no-open`
suppresses the browser; `--no-serve` returns after printing the URL without
blocking (used by tests). `opener` is injectable for testing."""
blocking (used by tests). `opener` is injectable for testing.

THE INSTALL SHAPE IS RESOLVED FIRST (plan 034 T070): `--local`, or
`OPENDOX_INSTALL_MODE=local`, selects the local single-user install, and
with neither the install is hosted — see `_resolve_install_shape`. A
refusal there is printed on stderr and the command exits 1, before any
other work."""
try:
args.install_mode = _resolve_install_shape(args)
except runtime_config.ConfigurationError as exc:
print(f"generate-and-open refused: {exc}", file=sys.stderr)
return 1
# Ahead of minting the run dir, so a refused root leaves not even an empty
# temp directory behind. `_generate_and_write` is still the guard that MATTERS
# (it is the one no caller can skip); these are the same checks, earlier.
Expand Down Expand Up @@ -968,6 +1019,17 @@ def build_parser(*, subcommand_extensions: tuple = ()) -> argparse.ArgumentParse
gao.add_argument("--actor", default=None,
help="human identity for loopback gate actions "
"(default: the checkout's git user.name)")
# THE INSTALL SHAPE'S FLAG (plan 034 T070; R1Q15 (b), as T007 batch H's
# 13.4 addendum reads): the documented command is
# `opendox generate-and-open --local …`. The same selection as
# `OPENDOX_INSTALL_MODE=local`; with neither the install is hosted, and the
# flag beside `OPENDOX_INSTALL_MODE=hosted` is refused.
gao.add_argument(runtime_config.LOCAL_FLAG, action="store_true",
dest="local",
help="the LOCAL single-user install: no identity broker, "
"loopback only (the same selection as "
"OPENDOX_INSTALL_MODE=local; with neither, the "
"install is hosted and needs its broker's issuer)")
gao.add_argument("--host", default=serve_mod.DEFAULT_HOST,
help="bind host (default: 127.0.0.1, loopback only)")
gao.add_argument("--port", type=int, default=0, help="bind port (default: ephemeral)")
Expand Down
63 changes: 60 additions & 3 deletions src/opendox/runtime/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@

from opendox.runtime import identity, migrations
from opendox.runtime.config import (
INSTALL_MODE_LOCAL,
LOCAL_FLAG,
SECRET_NAMES,
SETTINGS,
ConfigurationError,
Expand Down Expand Up @@ -341,6 +343,11 @@ def _redacted_settings(settings: RuntimeSettings) -> dict[str, Any]:
values = {
"OPENDOX_DATABASE_URL": settings.database_url,
"OPENDOX_MIGRATION_DATABASE_URL": settings.migration_database_url,
# THE INSTALL SHAPE this process loaded (plan 034 T070): a name and
# never a credential, and the first thing an operator reading `status`
# needs to know, because it decides whether the broker lines below
# mean anything at all.
"OPENDOX_INSTALL_MODE": settings.install_mode,
# THE BROKER URLS ARE REDACTED HERE TOO. `load_settings` refuses
# userinfo in the issuer and in an explicit JWKS URL — but this report
# prints a DERIVED value, and a settings object can also be built by
Expand Down Expand Up @@ -565,10 +572,31 @@ def cmd_migrate(args: argparse.Namespace) -> int:


def cmd_serve(args: argparse.Namespace) -> int:
"""Run the API. The pool is opened by the application's lifespan."""
"""Run the API. The pool is opened by the application's lifespan.

NOT IN A LOCAL INSTALL (plan 034 T070; a holder reading on
openxFactory#656 that Brett may overrule). Every `/api/v1` route verifies
a token the BROKER signed (`oidc.build_verifier`), and the local mode has
no broker (#1144 13.4), so there is no identity this API could serve
with: started anyway, it would either refuse every request or, worse,
stand a local principal up that no task text defines. A local install is
served by `opendox generate-and-open --local`, and in release 1 its
document surface reads nothing from the store (R1Q16 (ii)). Refused
BEFORE anything is imported or bound, as evidence like every refusal.
"""
settings = _settings_or_refusal(args)
if isinstance(settings, int):
return settings
if settings.install_mode == INSTALL_MODE_LOCAL:
return _emit({"verb": "serve", "refusal": "local-mode-has-no-broker",
"message": "the runtime API authenticates every request "
"with a token its identity broker signed, "
"and a LOCAL install has no broker, so this "
"API has no identity to serve with. A local "
"install is served by `opendox "
f"generate-and-open {LOCAL_FLAG}`; the "
"runtime API is a HOSTED install's surface "
"(13.4)"}, ok=False)
try:
import uvicorn

Expand Down Expand Up @@ -636,6 +664,19 @@ def cmd_serve(args: argparse.Namespace) -> int:
"exiting normally"}, ok=True)


def _report_the_local_broker(report: dict[str, Any]) -> None:
"""What `status` says of a LOCAL install's broker, on every path.

Its broker is NOT CONFIGURED (plan 034 T070; #1144 13.4): a statement
about the install's configuration rather than a probe's result, so there
is no discovery URL to report and nothing counts against `ok`. `status`
returns from two places, and both write it here, so the two answers
cannot drift apart (Copilot review of openDox-code#67).
"""
report["broker_keys"] = "not configured (local mode)"
report["broker_discovery"] = None


def cmd_status(args: argparse.Namespace) -> int:
"""Report, never change: configuration, the schema pin, the ledger, the broker.

Expand Down Expand Up @@ -665,10 +706,18 @@ def cmd_status(args: argparse.Namespace) -> int:

try:
from opendox.runtime.db import Database
except ImportError as exc: # pragma: no cover - the extra is absent
except ImportError as exc:
report["runtime_extra"] = f"absent: {_safe_message(exc)}"
report["database"] = "not probed"
report["broker_keys"] = "not probed"
# A LOCAL INSTALL'S BROKER IS NOT CONFIGURED WHETHER OR NOT THE EXTRA
# IS PRESENT (plan 034 T070; Copilot review of openDox-code#67). That
# answer comes from its configuration, not from a probe, so this early
# return gives the same one the full report gives below. A hosted
# install's broker was never probed, and says so, as before.
if settings.install_mode == INSTALL_MODE_LOCAL:
_report_the_local_broker(report)
else:
report["broker_keys"] = "not probed"
return _emit(report, ok=False)
report["runtime_extra"] = "present"

Expand Down Expand Up @@ -744,6 +793,14 @@ def cmd_status(args: argparse.Namespace) -> int:
f"unreachable: {type(exc).__name__}: {_safe_message(exc)}")
ok = False

# A LOCAL INSTALL HAS NO BROKER TO PROBE (plan 034 T070; #1144 13.4),
# and that is its configuration rather than a fault: reported by name, and
# NOT counted against `ok`, so a healthy local install's `status` exits 0
# — F13.1 runs it under `set -e`, and a verdict of "unhealthy" for a
# broker the install was never meant to have would be false.
if settings.install_mode == INSTALL_MODE_LOCAL:
_report_the_local_broker(report)
return _emit(report, ok=ok)
Comment thread
brettheap marked this conversation as resolved.
try:
from opendox.runtime.oidc import build_verifier

Expand Down
Loading
Loading