Skip to content

feat(dashboard): add settings dashboard for proxy configuration#2101

Open
nangsontay wants to merge 7 commits into
headroomlabs-ai:mainfrom
nangsontay:feat/setting-panel
Open

feat(dashboard): add settings dashboard for proxy configuration#2101
nangsontay wants to merge 7 commits into
headroomlabs-ai:mainfrom
nangsontay:feat/setting-panel

Conversation

@nangsontay

@nangsontay nangsontay commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Description

Adds a loopback-only dashboard settings panel at /dashboard/settings for a curated, safe subset of Headroom runtime knobs. Settings persist to settings.json, are applied before Click resolves envvar= options, and keep precedence predictable: explicit shell export > stored settings > code default.

The panel also adds an Endpoints group for custom Anthropic/OpenAI upstream base URLs and optional extra forwarded headers for gateway deployments. Mutating routes are loopback-gated and same-origin guarded; secret header values are masked and admin audit records only changed key names.

Changes Made

  • Added headroom/settings_store.py with validation, masking, atomic save, partial-update merge semantics, and env application.
  • Added /settings/schema, /settings, /settings/apply, and /dashboard/settings routes.
  • Added same-origin protection for mutating local settings routes.
  • Added custom Anthropic/OpenAI endpoint and extra-header plumbing through CLI, provider registry, proxy config, and handlers.
  • Added deployment-aware apply/restart behavior for service/docker/foreground modes.
  • Added docs for the settings GUI and endpoint/header configuration.
  • Merged current main, added missing retry-delay settings registry entries, fixed the UI so no-op saves do not persist every default, and removed unrelated dependency/Cargo churn from the PR diff.

Testing

uvx ruff@0.15.17 check headroom/settings_store.py headroom/proxy/server.py headroom/proxy/loopback_guard.py headroom/providers/registry.py headroom/proxy/helpers.py headroom/cli/main.py headroom/cli/proxy.py tests/test_proxy/test_settings_store.py tests/test_proxy_settings_endpoints.py tests/test_provider_registry.py tests/test_cli_proxy_env.py tests/test_header_isolation.py tests/test_install/test_runtime.py tests/test_proxy/test_settings_fresh_process_precedence.py
All checks passed!

uvx ruff@0.15.17 format --check headroom/settings_store.py headroom/proxy/server.py headroom/proxy/loopback_guard.py headroom/providers/registry.py headroom/proxy/helpers.py headroom/cli/main.py headroom/cli/proxy.py tests/test_proxy/test_settings_store.py tests/test_proxy_settings_endpoints.py tests/test_provider_registry.py tests/test_cli_proxy_env.py tests/test_header_isolation.py tests/test_install/test_runtime.py tests/test_proxy/test_settings_fresh_process_precedence.py
14 files already formatted

uv run --extra dev python -m pytest tests/test_proxy/test_settings_store.py tests/test_provider_registry.py tests/test_cli_proxy_env.py tests/test_proxy_settings_endpoints.py tests/test_header_isolation.py tests/test_install/test_runtime.py tests/test_proxy/test_settings_fresh_process_precedence.py -q
192 passed, 1 skipped, 1 warning

git diff --check headroomlabs/main...HEAD
# no output

The pushed cleanup commits also passed local pre-commit hooks.

Review Readiness

  • Ready for review
  • Regression tests added
  • Documentation updated

Copilot AI review requested due to automatic review settings July 13, 2026 09:44
@nangsontay nangsontay changed the title # feat(dashboard): add settings dashboard for proxy configuration feat(dashboard): add settings dashboard for proxy configuration Jul 13, 2026
@nangsontay nangsontay force-pushed the feat/setting-panel branch from d9e28cd to a346c21 Compare July 13, 2026 09:53
@nangsontay

Copy link
Copy Markdown
Contributor Author

note: force push because I forgot to sign the previous commit with my new key (changed but forgot to assign to Github)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot couldn't run its full agentic review because no GitHub Actions runner was available. Make sure your repository has a runner available to run Copilot's review, or add a copilot-setup-steps.yml file specifying one with the runs-on attribute. See the docs for more details.

Adds a local web settings dashboard for configuring safe proxy settings, persisting them to a file-backed store, and supporting apply/restart flows across deployment modes.

Changes:

  • Introduces headroom/settings_store.py for validated, file-backed settings persisted under the workspace and applied to os.environ early.
  • Adds dashboard UI + new loopback-gated settings endpoints, plus same-origin protection for mutating routes.
  • Adds support for provider “extra forwarded headers” via CLI/env/config and merges them into upstream requests.

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
wiki/proxy.md Documents new CLI flags for extra forwarded headers.
wiki/configuration.md Adds “Settings GUI” documentation (persistence/precedence/CSRF/apply modes).
wiki/cli.md Documents new flags and env vars for extra headers.
tests/test_proxy_settings_endpoints.py Covers settings endpoints, origin guard, masking, and apply/restart behavior.
tests/test_proxy/test_settings_store.py Covers settings store validation, atomic save, masking, and precedence.
tests/test_proxy/test_settings_fresh_process_precedence.py Verifies precedence in a real subprocess.
tests/test_provider_registry.py Adds tests for resolve_extra_headers behavior and errors.
tests/test_install/test_runtime.py Adds tests for deployment detection and restart dispatch.
tests/test_header_isolation.py Adds tests validating extra headers are forwarded/overriding behavior.
tests/test_cli_proxy_env.py Verifies settings file applies before Click envvar parsing.
headroom/settings_store.py Implements curated registry, validation/coercion, masking, load/save, schema.
headroom/proxy/server.py Applies stored settings defensively; adds /settings* API + settings page route.
headroom/proxy/models.py Extends ProxyConfig with anthropic_extra_headers / openai_extra_headers.
headroom/proxy/loopback_guard.py Adds require_same_origin guard for mutating admin routes.
headroom/proxy/helpers.py Adds merge_extra_headers utility for forwarding overrides.
headroom/proxy/handlers/openai.py Merges configured extra headers into OpenAI forwarded requests (incl WS).
headroom/proxy/handlers/anthropic.py Merges configured extra headers into Anthropic forwarded requests and redaction set.
headroom/providers/registry.py Adds resolve_extra_headers JSON parsing/validation (CLI over env).
headroom/paths.py Adds settings path resolution (HEADROOM_SETTINGS_PATH_ENV, settings_path).
headroom/install/runtime.py Adds deployment detection + restart dispatch for apply/restart.
headroom/dashboard/templates/settings.html New settings UI (tabs, save/apply, masking/clear).
headroom/dashboard/templates/dashboard.html Adds link to settings page from dashboard.
headroom/dashboard/init.py Adds get_settings_html() template loader.
headroom/cli/proxy.py Adds CLI options and wiring to resolve extra headers into ProxyConfig.
headroom/cli/main.py Applies stored settings to env before Click parses envvar-backed options.

Comment thread headroom/proxy/helpers.py
Comment on lines 1516 to 1525
Emitted once per outbound request (paired with ``log_outbound_request``).
Per realignment build constraint #8 we log every cache-affecting
decision; per #8/#11 we never log header values, only the count of
stripped internal headers.
"""
logger.info(
"event=outbound_headers forwarder=%s stripped_count=%d request_id=%s",
forwarder,
stripped_count,
request_id or "",
Comment on lines +484 to +497
if field.type in ("int", "float"):
if isinstance(value, bool): # bool is an int subclass — reject explicitly
raise ValueError(f"expected a number, got {value!r}")
if field.type == "int":
if isinstance(value, float) and not value.is_integer():
raise ValueError(f"expected an integer, got {value!r}")
number = int(value)
else:
number = float(value)
if field.minimum is not None and number < field.minimum:
raise ValueError(f"must be >= {field.minimum}")
if field.maximum is not None and number > field.maximum:
raise ValueError(f"must be <= {field.maximum}")
return number
Comment thread headroom/settings_store.py Outdated
Comment on lines +34 to +69
class SettingsStore:
"""Instance-based settings store for a workspace directory."""

def __init__(self, workspace_dir: str | Path) -> None:
"""Initialize settings store for a specific workspace directory."""
self.workspace_dir = Path(workspace_dir)
self.settings_file = self.workspace_dir / "settings.json"

def load(self) -> dict[str, Any]:
"""Load settings from workspace settings.json."""
if not self.settings_file.exists():
return {}
try:
return json.loads(self.settings_file.read_text())
except (json.JSONDecodeError, OSError):
return {}

def save(self, values: dict[str, Any]) -> None:
"""Save settings to workspace settings.json."""
validated = validate(values)
self.workspace_dir.mkdir(parents=True, exist_ok=True)
serialized = {k: _serialize(f, validated[k]) for k, f in _BY_KEY.items() if k in validated}
_atomic_write_text(self.settings_file, json.dumps(serialized, indent=2))

def validate(self, values: dict[str, Any]) -> dict[str, Any]:
"""Validate and coerce settings values."""
return validate(values)

def load_schema_with_secrets_masked(self) -> dict[str, Any]:
"""Load stored values with secrets masked for display."""
stored = self.load()
result = {}
for field in SETTINGS:
if field.key in stored:
result[field.key] = _mask(field, stored[field.key])
return result
Comment thread headroom/proxy/server.py
Comment on lines +3061 to +3066
status_code=422,
content={"error": "invalid settings value(s)", "field_errors": exc.field_errors},
)
_manifest, mode = install_runtime.detect_current_deployment()
record_admin_action(
request=request,
Comment on lines +7 to +8
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/alpinejs@3.13.3/dist/cdn.min.js" defer></script>
Comment on lines +223 to +225
this.status = 'Saved.';
this.banner = 'Restart required to apply: ' + (data.changed_keys || []).join(', ');
this.applyMode = ''; this.applyCommand = '';
Comment on lines +467 to +482
def _coerce(field: SettingField, value: Any) -> Any:
"""Coerce a raw JSON/env value to the field's Python type.

Returns ``None`` for null/empty. Raises ``ValueError`` on bad input so
callers can surface a per-field message.
"""
if value is None:
return None
if field.type in ("bool", "optional-bool"):
if isinstance(value, bool):
return value
token = str(value).strip().lower()
if token in ("1", "true", "yes", "on"):
return True
if token in ("0", "false", "no", "off", ""):
return False
- merge_extra_headers: override client headers case-insensitively so a
  configured gateway header never ships upstream alongside the client's
  differently-cased duplicate
- settings_store: reject non-finite floats (NaN/Infinity) in coercion,
  which previously slipped past min/max bounds on unbounded fields
- settings_store: coerce an empty optional-bool to None (inherit) instead
  of False, and correct the _coerce docstring
- settings_store: drop the unused SettingsStore class that diverged from
  the module-level load/save API
- server: loopback-gate GET /dashboard/settings to match its APIs
- settings.html: don't render an empty "Restart required to apply:" banner
  when no keys changed

Add regression tests for each behavioral fix.
@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

PR governance

This PR does not yet satisfy the required template fields:

  • Missing required section Type of Change.
  • Missing required section Real Behavior Proof.
  • Check at least one verification item in Testing.
  • Check I have performed a self-review before requesting human review.
  • Check This PR is ready for human review or convert the PR back to draft.

Please update the PR body, or move the PR back to draft while it is still in progress.

@github-actions github-actions Bot added status: needs author action Pull request body or readiness checklist still needs author updates status: ready for review Pull request body is complete and the author marked it ready for human review and removed status: needs author action Pull request body or readiness checklist still needs author updates labels Jul 13, 2026

@JerrettDavis JerrettDavis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed current head 0d31939. The settings store/API surfaces are loopback/same-origin guarded, secret header fields stay masked, stored settings preserve explicit env precedence, and the extra-header plumbing is scoped to upstream forwarding. I fixed the post-main registry drift for retry delay settings, corrected the settings UI so no-op saves don't persist every default value, and removed unrelated dependency/Cargo churn from the diff. Focused ruff, format, pytest, pre-commit hooks, and whitespace checks passed locally.

@github-actions github-actions Bot added status: needs author action Pull request body or readiness checklist still needs author updates and removed status: ready for review Pull request body is complete and the author marked it ready for human review labels Jul 14, 2026
@nangsontay

Copy link
Copy Markdown
Contributor Author

@JerrettDavis is it safe to skip the security issue of deps?

@JerrettDavis JerrettDavis added status: ready for review Pull request body is complete and the author marked it ready for human review and removed status: needs author action Pull request body or readiness checklist still needs author updates labels Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: ready for review Pull request body is complete and the author marked it ready for human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants