Lock down the local release console
This commit is contained in:
@@ -5,8 +5,9 @@ GovOPlaN releases. It belongs to the `govoplan` meta repository because it works
|
|||||||
across all local checkouts, release scripts, module manifests, migration audits,
|
across all local checkouts, release scripts, module manifests, migration audits,
|
||||||
catalog files, Git state, and signing keys.
|
catalog files, Git state, and signing keys.
|
||||||
|
|
||||||
The current implementation has a read-only dashboard plus guarded local
|
The current implementation has a read-only dashboard, preview-only legacy
|
||||||
candidate/publish actions:
|
controls, and a bounded durable executor for release steps whose inputs and
|
||||||
|
effects can be verified safely:
|
||||||
|
|
||||||
- inspect repositories from `repositories.json`
|
- inspect repositories from `repositories.json`
|
||||||
- show dirty, ahead, behind, missing, no-HEAD, and tag state
|
- show dirty, ahead, behind, missing, no-HEAD, and tag state
|
||||||
@@ -19,10 +20,12 @@ candidate/publish actions:
|
|||||||
- select the repositories that should advance through checkboxes
|
- select the repositories that should advance through checkboxes
|
||||||
- generate dry-run selective release plans for independently versioned packages
|
- generate dry-run selective release plans for independently versioned packages
|
||||||
- freeze a selective plan as a durable, resumable local release run
|
- freeze a selective plan as a durable, resumable local release run
|
||||||
- create annotated source release tags for selected clean, version-aligned
|
- durably preflight a creation-time-bound repository, create its annotated tag,
|
||||||
repositories and publish each branch/tag pair atomically
|
and publish its branch/tag pair atomically
|
||||||
- generate signed catalog candidates for the selected release units
|
- build selected Python wheels and generate a private, signed, receipt-bound
|
||||||
- preview/apply/push reviewed catalog candidates behind explicit confirmations
|
catalog candidate
|
||||||
|
- publish that exact candidate through a verified website commit and immutable
|
||||||
|
tag after explicit confirmation
|
||||||
|
|
||||||
Start it from the meta repository:
|
Start it from the meta repository:
|
||||||
|
|
||||||
@@ -30,8 +33,32 @@ Start it from the meta repository:
|
|||||||
./.venv/bin/python tools/release/release-console.py
|
./.venv/bin/python tools/release/release-console.py
|
||||||
```
|
```
|
||||||
|
|
||||||
The server binds to `127.0.0.1` by default and prints a URL containing a local
|
The launcher accepts only a numeric loopback address, binds to `127.0.0.1` by
|
||||||
API token. Open that URL in a browser on the same machine.
|
default, always creates a fresh API token, and prints that token only in the URL
|
||||||
|
fragment. Open that URL in a browser on the same machine. A deliberately
|
||||||
|
tokenless embedded app is read-only: non-GET `/api/` requests fail with `403`.
|
||||||
|
Non-loopback operation needs a separately deployed authenticated TLS boundary;
|
||||||
|
the local launcher will not expose the mutation API that way.
|
||||||
|
|
||||||
|
Run durable release mutation only against an operator-private workspace.
|
||||||
|
Repository roots, every path ancestor, critical and nested Git metadata, and
|
||||||
|
tracked worktree files must be owned by the console process UID (or root where
|
||||||
|
appropriate) and must not be group/world writable. Symlink/special Git
|
||||||
|
metadata, object alternates, and grafts are rejected. The console pins
|
||||||
|
`/usr/bin/git`, `/usr/bin/ssh`, a fixed system `PATH`, disabled hooks, isolated
|
||||||
|
Git configuration, and no replace objects. Shared or `nfsnobody`-owned
|
||||||
|
checkouts remain usable for read-only planning, but every durable executor
|
||||||
|
fails closed there; clone the registered origins into a private workspace
|
||||||
|
before releasing.
|
||||||
|
|
||||||
|
The runtime itself is part of the authority boundary. Durable run creation
|
||||||
|
verifies the meta checkout, release/check tooling, repository registry, Python
|
||||||
|
environment, loaded `govoplan_core` and cryptography packages, and Git/SSH
|
||||||
|
executables. It then binds a clean meta-repository HEAD and named branch that
|
||||||
|
exactly match the registered `origin`. Every execution and reconciliation
|
||||||
|
rechecks that receipt. Permission checks cannot prove that code was safe before
|
||||||
|
it entered a writable tree, so release from a fresh operator-private clone or a
|
||||||
|
separately verified installed console artifact.
|
||||||
|
|
||||||
The web UI starts with a repository table. Each repository can be checked
|
The web UI starts with a repository table. Each repository can be checked
|
||||||
independently and assigned its own target version. Repositories without version
|
independently and assigned its own target version. Repositories without version
|
||||||
@@ -57,6 +84,12 @@ into a versioned local run record. The server rebuilds the selective plan and
|
|||||||
requires the plan to resolve exactly the requested repositories and target
|
requires the plan to resolve exactly the requested repositories and target
|
||||||
versions; the browser cannot submit or replace the plan snapshot. The input and
|
versions; the browser cannot submit or replace the plan snapshot. The input and
|
||||||
plan are then immutable and covered by a canonical SHA-256 integrity digest.
|
plan are then immutable and covered by a canonical SHA-256 integrity digest.
|
||||||
|
Every executable repository step also carries its creation-time full HEAD,
|
||||||
|
branch, worktree state, target tag, and a SHA-256 over both the fetch and push
|
||||||
|
URLs of `origin`. Both URLs must exactly equal the remote registered in
|
||||||
|
`repositories.json`; a changed HEAD, branch, worktree, remote, or push URL
|
||||||
|
requires a new run rather than silently retargeting the frozen compatibility
|
||||||
|
decision.
|
||||||
The complete record also has a checksum so a valid-looking manual edit to its
|
The complete record also has a checksum so a valid-looking manual edit to its
|
||||||
mutable state fails closed. File permissions remain the authority boundary;
|
mutable state fails closed. File permissions remain the authority boundary;
|
||||||
these digests detect accidental or manual corruption, not an attacker who can
|
these digests detect accidental or manual corruption, not an attacker who can
|
||||||
@@ -101,8 +134,10 @@ checks it. An alternate workspace therefore cannot list or resume another
|
|||||||
workspace's runs. This remains true for an embedding/test `run_state_root`
|
workspace's runs. This remains true for an embedding/test `run_state_root`
|
||||||
override: the server always appends
|
override: the server always appends
|
||||||
`workspace-<full-sha256>/release-runs/` rather than treating the override as a
|
`workspace-<full-sha256>/release-runs/` rather than treating the override as a
|
||||||
shared record directory. A corrupt record from one workspace therefore cannot
|
shared record directory. Durable candidates use its private sibling
|
||||||
leak even its identifier or an integrity error into another workspace.
|
`release-candidates/` directory, never a checkout-local runtime path. A corrupt
|
||||||
|
record from one workspace therefore cannot leak even its identifier or an
|
||||||
|
integrity error into another workspace.
|
||||||
|
|
||||||
Each frozen plan step has an explicit `pending`, `running`, `succeeded`,
|
Each frozen plan step has an explicit `pending`, `running`, `succeeded`,
|
||||||
`failed`, or `interrupted` state. Plan order remains a prerequisite: a later
|
`failed`, or `interrupted` state. Plan order remains a prerequisite: a later
|
||||||
@@ -140,6 +175,27 @@ effect, and `unresolved` keeps the run blocked. Each outcome emits a bounded,
|
|||||||
code-only state event. A known failed attempt can likewise be prepared for
|
code-only state event. A known failed attempt can likewise be prepared for
|
||||||
retry. The UI keeps unavailable controls visible and disabled.
|
retry. The UI keeps unavailable controls visible and disabled.
|
||||||
|
|
||||||
|
Supported executors durably claim the step before invoking an effect. Exact
|
||||||
|
attempt replays return the recorded outcome and never invoke the executor a
|
||||||
|
second time. Successful repository preflight, tag, and push steps persist a
|
||||||
|
bounded repository-state receipt. Tag reconciliation independently requires an
|
||||||
|
annotated local tag at the frozen HEAD; push reconciliation additionally
|
||||||
|
requires both the remote annotated tag object and remote branch to match.
|
||||||
|
Catalog generation persists
|
||||||
|
only its server-issued opaque candidate ID and canonical catalog SHA-256, then
|
||||||
|
re-resolves and re-hashes that private candidate before publication.
|
||||||
|
|
||||||
|
The safe baseline is intentionally narrower than the complete dry-run plan.
|
||||||
|
Dirty worktrees and version changes still show commit/version steps, but those
|
||||||
|
steps have no durable executor because the frozen run does not bind the exact
|
||||||
|
proposed file content. A run selecting Core together with one or more modules
|
||||||
|
begins with a disabled dependency-ordering barrier: local module tags, Core
|
||||||
|
release-lock regeneration/commit, Core tagging, alignment verification, and
|
||||||
|
pushes need an explicit DAG executor before that composition can mutate.
|
||||||
|
Operators must prepare and review those changes outside the console and create
|
||||||
|
a new run from the resulting clean HEAD. The console never skips these steps or
|
||||||
|
claims an end-to-end release succeeded.
|
||||||
|
|
||||||
The browser likewise retains the request identifier for an uncertain
|
The browser likewise retains the request identifier for an uncertain
|
||||||
resume/retry/reconciliation response and replays it after reload. A successful
|
resume/retry/reconciliation response and replays it after reload. A successful
|
||||||
replay selects the returned run state. Transport and server failures retain the
|
replay selects the returned run state. Transport and server failures retain the
|
||||||
@@ -151,9 +207,10 @@ other `/api/` route:
|
|||||||
|
|
||||||
- `POST /api/release-runs` requires `request_id`, then idempotently rebuilds and
|
- `POST /api/release-runs` requires `request_id`, then idempotently rebuilds and
|
||||||
freezes a selective plan only when that creation is not already known.
|
freezes a selective plan only when that creation is not already known.
|
||||||
- `GET /api/release-runs` lists bounded summaries ordered by verified
|
- `GET /api/release-runs` lists bounded summaries ordered by immutable
|
||||||
`updated_at`, then `created_at` and run identifier. `next_cursor` advances a
|
`created_at` and run identifier. `next_cursor` advances a stable descending
|
||||||
stable descending traversal without offset duplicates. Unreadable entries
|
traversal even while older runs are updated, without offset duplicates or
|
||||||
|
skips. Unreadable entries
|
||||||
remain a deterministic final section and are therefore reachable through
|
remain a deterministic final section and are therefore reachable through
|
||||||
pagination instead of displacing newer verified runs.
|
pagination instead of displacing newer verified runs.
|
||||||
- `GET /api/release-runs/{run_id}` reads and verifies one exact record.
|
- `GET /api/release-runs/{run_id}` reads and verifies one exact record.
|
||||||
@@ -162,16 +219,28 @@ other `/api/` route:
|
|||||||
read-only interrupted step for another attempt.
|
read-only interrupted step for another attempt.
|
||||||
- `POST /api/release-runs/{run_id}/steps/{step_id}/reconcile` records a
|
- `POST /api/release-runs/{run_id}/steps/{step_id}/reconcile` records a
|
||||||
confirmed observed outcome for an interrupted mutating step.
|
confirmed observed outcome for an interrupted mutating step.
|
||||||
|
- `POST /api/release-runs/{run_id}/steps/{step_id}/execute` claims and invokes
|
||||||
|
only the narrow executor declared by the immutable plan step. Durable release
|
||||||
|
execution accepts only the registered `origin`, never a caller-selected
|
||||||
|
remote.
|
||||||
|
- `POST /api/release-runs/{run_id}/steps/{step_id}/preview` provides the
|
||||||
|
non-mutating preview for receipt-bound catalog publication.
|
||||||
|
|
||||||
Run-storage errors are confined to the Durable Run State card; dashboard and
|
Run-storage errors are confined to the Durable Run State card; dashboard and
|
||||||
release-preview collection continue and show the bounded storage remediation.
|
release-preview collection continue and show the bounded storage remediation.
|
||||||
|
|
||||||
This foundation tracks state only. It deliberately does not run a plan step or
|
The run record is execution evidence only for a supported step whose durable
|
||||||
infer success from a button click. Existing preview and executor controls stay
|
claim and bounded result receipt were persisted. The console never infers
|
||||||
separate and continue to require `COMMIT`, `TAG`, `PUBLISH`, `APPLY`, or `PUSH`
|
success from a button click or process exit alone. An executor exception or a
|
||||||
at their existing narrow boundaries. Wiring confirmed executors to claim and
|
lost result write leaves the attempt interrupted and non-retriable until
|
||||||
finish run steps, plus package/install verification, remains follow-up work;
|
explicit recovery. Catalog publication persists the candidate and keyring
|
||||||
until then the console must not present a run as execution evidence.
|
hashes, exact website commit, annotated tag object and peeled commit, branch,
|
||||||
|
tag name, and registered-origin digest. A successful reconciliation revalidates
|
||||||
|
the candidate against the trust anchor in the frozen website parent, requires
|
||||||
|
the exact deterministic catalog/keyring/module blobs and full commit delta, a
|
||||||
|
sole frozen parent, and matching local and remote branch/tag identities. If any
|
||||||
|
part cannot be proved, the run remains interrupted and may only be recorded as
|
||||||
|
`unresolved`.
|
||||||
|
|
||||||
Dashboard collection is also fail closed. Unreadable Core version metadata or a
|
Dashboard collection is also fail closed. Unreadable Core version metadata or a
|
||||||
malformed module contract is returned as a bounded `collection_errors` entry
|
malformed module contract is returned as a bounded `collection_errors` entry
|
||||||
@@ -198,11 +267,12 @@ Plain repository pushes are separate from catalog publication. `Preview Push`
|
|||||||
shows the selected repository push commands. `Push Selected` requires `PUSH` in
|
shows the selected repository push commands. `Push Selected` requires `PUSH` in
|
||||||
the repository push confirmation field.
|
the repository push confirmation field.
|
||||||
|
|
||||||
The source release panel closes the former gap between a release plan and a
|
The source release panel retains `Preview Tag + Publish` as a non-mutating
|
||||||
catalog candidate. `Preview Tag + Publish` is non-mutating. `Create Tags`
|
inspection. Its legacy `Create Tags` and `Publish Tags` controls stay visible
|
||||||
requires `TAG`; `Publish Tags` requires `PUBLISH` and atomically pushes the
|
but disabled; the corresponding mutation endpoint rejects apply requests.
|
||||||
selected branch and annotated tag. The gate requires an aligned target version,
|
Creation and atomic branch/tag publication use the `TAG` and `PUBLISH`
|
||||||
a clean named branch with a HEAD, and a checkout that is not behind. Existing
|
confirmations on the durable run steps. The gate requires an aligned target
|
||||||
|
version, a clean named branch with a HEAD, and a checkout that is not behind. Existing
|
||||||
local or remote tags must resolve to the selected HEAD and are never moved.
|
local or remote tags must resolve to the selected HEAD and are never moved.
|
||||||
The local and remote annotated tag objects must also be identical, not merely
|
The local and remote annotated tag objects must also be identical, not merely
|
||||||
point at the same commit. Before any source tag is created, the console loads
|
point at the same commit. Before any source tag is created, the console loads
|
||||||
@@ -210,15 +280,17 @@ the cross-repository module registry. This release gate also rejects every
|
|||||||
user-facing workflow documentation topic that has no scope condition, or has
|
user-facing workflow documentation topic that has no scope condition, or has
|
||||||
an alternative condition without `required_scopes` or `any_scopes`.
|
an alternative condition without `required_scopes` or `any_scopes`.
|
||||||
|
|
||||||
The catalog workflow panel can also operate on the same selected rows:
|
The catalog workflow panel can also operate on the same selected rows for
|
||||||
|
generation and preview. Its legacy apply/push controls stay disabled; durable
|
||||||
|
publication consumes only the candidate receipt recorded by that run:
|
||||||
|
|
||||||
- `Generate` creates a signed candidate in the operator's private XDG state
|
- `Generate` creates a signed candidate in the operator's private XDG state
|
||||||
directory and records only an opaque candidate ID plus the canonical catalog
|
directory and records only an opaque candidate ID plus the canonical catalog
|
||||||
SHA-256 in durable run state.
|
SHA-256 in durable run state.
|
||||||
- `Preview` validates a candidate and shows what would be copied into the
|
- `Preview` validates a candidate and shows what would be copied into the
|
||||||
website repository.
|
website repository.
|
||||||
- `Apply + Website Tag` requires `APPLY` in the confirmation field.
|
- `Apply + Website Tag` remains visible but disabled outside a durable run.
|
||||||
- `Push Website Release` requires `PUSH` in the confirmation field.
|
- `Push Website Release` remains visible but disabled outside a durable run.
|
||||||
|
|
||||||
Source release tags belong to Core or module repositories. Website catalog
|
Source release tags belong to Core or module repositories. Website catalog
|
||||||
publication creates a separate catalog tag in the website repository; the UI
|
publication creates a separate catalog tag in the website repository; the UI
|
||||||
@@ -243,14 +315,29 @@ cannot be applied or published while any referenced source tag is absent or
|
|||||||
inconsistent; the preview and API response list each repository and missing or
|
inconsistent; the preview and API response list each repository and missing or
|
||||||
invalid ref so the operator can repair the exact source releases first.
|
invalid ref so the operator can repair the exact source releases first.
|
||||||
|
|
||||||
Every selected Python release must also supply its exact built wheel. Candidate
|
Every selected Python release must also supply its exact built wheel. Durable
|
||||||
generation computes the archive SHA-256 and an install-stable payload identity
|
generation first verifies that the receipt-bound annotated tag is the same
|
||||||
from one bounded, regular-file descriptor and signs those values into
|
object locally and on the registered origin. It clones an isolated checkout at
|
||||||
`release.artifacts`. Updating a Python version without a matching wheel removes
|
the receipt's exact commit and builds from that checkout, never from the mutable
|
||||||
the stale identity. A source-only preview can still explain the gap, but apply,
|
live worktree. Every authenticated base-catalog source is likewise cloned from
|
||||||
commit, tag, and push fail closed until every selected Python unit has a matching
|
its registered origin at an annotated release tag; only selected repositories
|
||||||
built-artifact identity. Repositories selected only for a meta/source tag do not
|
contribute synthesized fields. The base catalog and keyring are read as one
|
||||||
need a Python artifact.
|
authenticated, exact private snapshot before synthesis.
|
||||||
|
|
||||||
|
Python wheels are built by a required Bubblewrap worker with no network,
|
||||||
|
private temporary/home directories, a read-only exact source mount, no host
|
||||||
|
home or file keys, cleared environment, fixed system tools, and CPU/address
|
||||||
|
space/process/file-size limits. If a trusted Bubblewrap launcher is unavailable,
|
||||||
|
generation fails closed. The worker must return one bounded regular wheel; the
|
||||||
|
console copies it through no-follow descriptors into a fresh private file,
|
||||||
|
`fsync`s it, then validates its package identity. Generation computes the
|
||||||
|
archive SHA-256 and an install-stable
|
||||||
|
payload identity from one bounded, regular-file descriptor and signs those
|
||||||
|
values into `release.artifacts`. Updating a Python version without a matching
|
||||||
|
wheel removes the stale identity. A source-only preview can still explain the
|
||||||
|
gap, but apply, commit, tag, and push fail closed until every selected Python
|
||||||
|
unit has a matching built-artifact identity. Repositories selected only for a
|
||||||
|
meta/source tag do not need a Python artifact.
|
||||||
|
|
||||||
Candidate directories and files are operator-owned `0700`/`0600` state. Before
|
Candidate directories and files are operator-owned `0700`/`0600` state. Before
|
||||||
any later release step consumes one, the executor re-resolves the opaque handle
|
any later release step consumes one, the executor re-resolves the opaque handle
|
||||||
@@ -258,6 +345,14 @@ below the configured root and re-hashes the signed catalog against its persisted
|
|||||||
receipt. Shared roots, symlinks, channel path fragments, altered candidates, and
|
receipt. Shared roots, symlinks, channel path fragments, altered candidates, and
|
||||||
unowned files are rejected.
|
unowned files are rejected.
|
||||||
|
|
||||||
|
The current same-host worker is a containment baseline, not the final hostile
|
||||||
|
build-service boundary. `RLIMIT_FSIZE` is per file, and the worker does not yet
|
||||||
|
have a size-limited filesystem/cgroup quota, a fresh kernel keyring, or a
|
||||||
|
seccomp profile denying keyctl/request-key/ptrace/mount operations. A malicious
|
||||||
|
backend could therefore exhaust scratch disk/inodes or target same-UID kernel
|
||||||
|
facilities. Closing that denial-of-service/isolation gap requires a dedicated
|
||||||
|
quota/cgroup worker with a fresh keyring and seccomp policy.
|
||||||
|
|
||||||
The server-owned wheel builds remain under the private candidate's `artifacts/`
|
The server-owned wheel builds remain under the private candidate's `artifacts/`
|
||||||
directory. This slice signs their identities but does **not** upload the wheel or
|
directory. This slice signs their identities but does **not** upload the wheel or
|
||||||
add a download URL to the public catalog; the existing Git `python_ref` is source
|
add a download URL to the public catalog; the existing Git `python_ref` is source
|
||||||
@@ -274,7 +369,10 @@ configured Git remote.
|
|||||||
|
|
||||||
The default signing key is
|
The default signing key is
|
||||||
`$HOME/.config/govoplan/release-keys/release-key-1.pem` when no signing key is
|
`$HOME/.config/govoplan/release-keys/release-key-1.pem` when no signing key is
|
||||||
entered in the UI.
|
entered in the UI. Signing-key files must be regular, owned by the operator, and
|
||||||
|
inaccessible to group/other users. The browser sends a configured key path only
|
||||||
|
on the initial execution request; it never persists signing material in session
|
||||||
|
storage, and request-ID recovery replays no key path.
|
||||||
|
|
||||||
Generate a selective release plan from the terminal:
|
Generate a selective release plan from the terminal:
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -36,6 +37,30 @@ class ReleaseConsoleSecurityTests(unittest.TestCase):
|
|||||||
self.assertIn("window.location.hash.slice(1)", webui)
|
self.assertIn("window.location.hash.slice(1)", webui)
|
||||||
self.assertIn("history.replaceState", webui)
|
self.assertIn("history.replaceState", webui)
|
||||||
|
|
||||||
|
def test_tokenless_embedding_is_read_only(self) -> None:
|
||||||
|
with TestClient(create_app(workspace_root=META_ROOT)) as client:
|
||||||
|
read_response = client.get("/api/health")
|
||||||
|
write_response = client.post("/api/selective-plan", json={})
|
||||||
|
|
||||||
|
self.assertEqual(200, read_response.status_code)
|
||||||
|
self.assertEqual(403, write_response.status_code)
|
||||||
|
self.assertIn("read-only", write_response.json()["detail"])
|
||||||
|
|
||||||
|
def test_launcher_rejects_non_loopback_and_tokenless_modes(self) -> None:
|
||||||
|
launcher = RELEASE_ROOT / "release-console.py"
|
||||||
|
for arguments in (("--host", "0.0.0.0"), ("--no-token",)):
|
||||||
|
with self.subTest(arguments=arguments):
|
||||||
|
result = subprocess.run(
|
||||||
|
(sys.executable, str(launcher), *arguments),
|
||||||
|
cwd=META_ROOT,
|
||||||
|
check=False,
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
self.assertEqual(2, result.returncode)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import ipaddress
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import secrets
|
import secrets
|
||||||
import sys
|
import sys
|
||||||
@@ -11,12 +12,31 @@ import sys
|
|||||||
from govoplan_release.workspace import DEFAULT_WORKSPACE_ROOT
|
from govoplan_release.workspace import DEFAULT_WORKSPACE_ROOT
|
||||||
|
|
||||||
|
|
||||||
|
def loopback_host(value: str) -> str:
|
||||||
|
try:
|
||||||
|
address = ipaddress.ip_address(value)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
"release console host must be a numeric loopback address"
|
||||||
|
) from exc
|
||||||
|
if not address.is_loopback:
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
"release console refuses non-loopback hosts without an authenticated "
|
||||||
|
"TLS deployment boundary"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument("--host", default="127.0.0.1", help="Bind host. Defaults to 127.0.0.1.")
|
parser.add_argument(
|
||||||
|
"--host",
|
||||||
|
type=loopback_host,
|
||||||
|
default="127.0.0.1",
|
||||||
|
help="Numeric loopback bind address. Defaults to 127.0.0.1.",
|
||||||
|
)
|
||||||
parser.add_argument("--port", type=int, default=8765, help="Bind port. Defaults to 8765.")
|
parser.add_argument("--port", type=int, default=8765, help="Bind port. Defaults to 8765.")
|
||||||
parser.add_argument("--workspace-root", type=Path, default=DEFAULT_WORKSPACE_ROOT)
|
parser.add_argument("--workspace-root", type=Path, default=DEFAULT_WORKSPACE_ROOT)
|
||||||
parser.add_argument("--no-token", action="store_true", help="Disable the local API token guard.")
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -28,13 +48,13 @@ def main() -> int:
|
|||||||
from server.app import create_app
|
from server.app import create_app
|
||||||
|
|
||||||
workspace_root = args.workspace_root.expanduser().resolve()
|
workspace_root = args.workspace_root.expanduser().resolve()
|
||||||
token = None if args.no_token else secrets.token_urlsafe(24)
|
token = secrets.token_urlsafe(24)
|
||||||
app = create_app(workspace_root=workspace_root, token=token)
|
app = create_app(workspace_root=workspace_root, token=token)
|
||||||
url = f"http://{args.host}:{args.port}/"
|
display_host = f"[{args.host}]" if ":" in args.host else args.host
|
||||||
if token:
|
url = f"http://{display_host}:{args.port}/"
|
||||||
# URL fragments are never sent in HTTP requests. The WebUI transfers
|
# URL fragments are never sent in HTTP requests. The WebUI transfers
|
||||||
# this one-time bootstrap token to sessionStorage and clears the hash.
|
# this one-time bootstrap token to sessionStorage and clears the hash.
|
||||||
url = f"{url}#token={token}"
|
url = f"{url}#token={token}"
|
||||||
print("GovOPlaN release console")
|
print("GovOPlaN release console")
|
||||||
print(f" workspace: {workspace_root}")
|
print(f" workspace: {workspace_root}")
|
||||||
print(f" url: {url}")
|
print(f" url: {url}")
|
||||||
|
|||||||
Reference in New Issue
Block a user